Skip to content

feat(mobile): rebase iOS PR #17 onto main + scaffold Flutter→RN migration#136

Draft
matej21 wants to merge 37 commits into
mainfrom
feat/mobile-rn
Draft

feat(mobile): rebase iOS PR #17 onto main + scaffold Flutter→RN migration#136
matej21 wants to merge 37 commits into
mainfrom
feat/mobile-rn

Conversation

@matej21

@matej21 matej21 commented Jun 8, 2026

Copy link
Copy Markdown
Member

What this is

Two layers on one branch:

  1. PR feat/ios #17 (feat/ios) rebased onto current main. PR feat/ios #17 was ~242 commits behind main and CONFLICTING. This branch replays it on top of current main (5 desktop conflicts resolved + one post-rebase API fixup). Builds and tests pass.
  2. Start of the Flutter → React Native migration (uniffi + native Skia rendering, no xterm.js), per mobile/RN_MIGRATION.md.

Closes the rebase need of #17; the RN work is additive and does not touch the desktop app.

Layer 1 — rebased iOS/mobile work (from #17)

Conflicts resolved against main's evolution:

  • okena-terminal/.../resize.rsterminal.rs was split into a terminal/ dir in main; the min-1 col/row clamp was re-applied in the new location.
  • remote_commands.rs — combined main's api_project_visibility with feat/ios #17's to_api_with_sizes (terminal sizes in state).
  • navigation.rs — semantic merge of main's multi-window + FocusManager with feat/ios #17's tab-aware navigation.
  • views/window/{mod,render}.rs — took main's side; dropped feat/ios #17's desktop "ensure-visible vs center scroll" tweak (it renamed a field main references in 5 other places; unrelated to mobile).
  • fix(rebase): deregister_pane_bounds gained a window_id arg in main; adapted the tab-container callsite.

Verified: cargo check -p okena -p okena_mobile_native ✅ · cargo test -p okena-terminal -p okena-layout → 92 passed ✅

Layer 2 — React Native migration (Phase 1–2 scaffold)

  • mobile/RN_MIGRATION.md — the plan: keep okena-core + alacritty, swap the binding (flutter_rust_bridge → uniffi/ubrn, JSI), render the terminal natively via react-native-skia. xterm.js explicitly rejected. Documents the hot-path packed-buffer bridge and a "drop Rust" fallback.
  • crates/okena-mobile-ffi — uniffi binding crate (uniffi 0.29, 61 exports) that reuses okena_mobile_native's ConnectionManager/handler/holder verbatim (no logic duplication; its crate-type gains "lib"). Adds get_visible_cells_packed() (compact LE cell buffer for the renderer). cargo check passes for it and okena_mobile_native (Flutter Rust side intact).
  • mobile/rn/ — the RN app: a typed OkenaNative binding contract + packed-cell decoder, a Skia TerminalView (3-pass paint ported from terminal_painter.dart), zustand state stores (ported from the Flutter providers), models, navigation, persistence, and the screens/widgets (ServerList, Pairing, Workspace, ProjectDrawer, KeyToolbar, LayoutRenderer, TerminalPane). All 15 Flutter app source files have an RN counterpart. Verified: npm install + npx tsc --noEmit (strict) pass over all 22 RN source files.

Not done yet (honest status — why this is a draft)

  • Never built/run on a device. No Android/iOS SDK here, and the ubrn-generated native module is not wired — getOkenaNative() throws until it is. So this is a structurally-complete, type-checked port, not a proven-equivalent running app.
  • TLS is a no-op in the uniffi connect() — client-side TLS lives on arch-review-fixes (PR Architecture review fixes: PTY starvation, persistence durability, remote/mobile leaks #134), not on main. Forward-compatible signature is in place.
  • Dart tests not ported (layout_node_test, saved_server_test, terminal_flags_test, widget_test); no RN test runner yet.
  • Deliberate adaptations: clipboard write, swipe-delete→long-press, split-divider drag-resize, SafeArea, and per-cell metrics are simplified/omitted (spike-level). Build tooling (cargokit) is replaced by ubrn, not ported.

Next steps (require a local RN toolchain)

  1. ubrn build android/ios --and-generate over crates/okena-mobile-ffi; replace getOkenaNative()'s body with the generated module (mobile/rn/README.md has exact commands).
  2. Run on device → validate the Skia renderer holds 60fps (Phase 0 spike S2).
  3. Wire TLS once PR Architecture review fixes: PTY starvation, persistence durability, remote/mobile leaks #134 lands on main.

Reviewing

The diff is large because it includes the #17 rebase. The desktop/Flutter portion was reviewed in #17; the new work in this PR is the RN migration: mobile/RN_MIGRATION.md, crates/okena-mobile-ffi/, and mobile/rn/.

🤖 Generated with Claude Code

jonasnobile and others added 30 commits June 10, 2026 15:52
Prevents alacritty from panicking when receiving zero-dimension resize
events, which can occur during layout transitions on mobile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ApiFolder struct, folders/project_order fields to StateResponse,
folder_color to ApiProject, and cols/rows to ApiLayoutNode::Terminal
for terminal size propagation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add collect_terminal_sizes() to walk the layout tree and build a size
map. Update ConnectionHandler::create_terminal to accept cols/rows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…state response

Build terminal size map from registry and use to_api_with_sizes() to
populate cols/rows in layout nodes. Include folders and project_order
in state responses for mobile/web clients.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Set up CocoaPods integration, development team, scene manifest,
local networking permissions, and rename native library to
okena_mobile_native.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduce centralized color palette (backgrounds, borders, accent, text
hierarchy, glass effects) and typography system (SF Pro Text) for
iOS-native dark theme.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add scroll_terminal, get_display_offset, and resize_local functions.
Improve wide char spacer handling and move inverse flag to painter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add folder info, project ordering, server terminal size, and
create/close/focus terminal actions. Update handler to use server
terminal dimensions when creating terminals.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…provements

Rewrite all screens and widgets with Cupertino-inspired design:
frosted glass headers, card-based layouts, haptic feedback, animated
status indicators. Add pinch-to-zoom, scroll support, auto-fit font
sizing, modifier key system, and text batching optimization to
terminal rendering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ment UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ntents FFI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…minal UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…color picker)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…orrectness

Rewrite to operate on byte offsets with char boundary checks instead of
collecting into a Vec<char>, which gave wrong results for multi-byte
characters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tion

Deregister pane map entries for inactive tabs so stale bounds don't
interfere with spatial navigation. Add try_switch_tab() so Left/Right
keys cycle tabs before falling through to cross-pane navigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The tab-pane deregistration added in feat/ios predates main's multi-window
support, which gave deregister_pane_bounds a leading WindowId parameter.
Adapt the inactive-tab cleanup callsite to main's signature so the rebased
branch compiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…endering)

Plan to replace the Flutter UI with React Native while keeping okena-core
and alacritty emulation. Primary path: uniffi-bindgen-react-native (JSI)
+ react-native-skia for native terminal rendering; xterm.js explicitly
rejected. Documents the FFI seam, the hot-path packed-buffer bridge, a
spike-gated phased plan, and the "drop Rust" fallback (reuse the web TS
protocol client, still native rendering).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (Phase 1)

New crate `crates/okena-mobile-ffi` re-expresses mobile/native's ~60-function
FFI surface via uniffi proc-macros (uniffi 0.29, JSI/ubrn-ready) — no logic
duplication: every fn delegates to okena_mobile_native's ConnectionManager,
which is reused verbatim (its crate-type gains "lib" so it can be a path dep).

- 31 genuinely-async fns exported with #[uniffi::export(async_runtime="tokio")]
  → JS Promises; sync getters (cells/cursor/scroll/selection/state) stay sync
  for the render hot path.
- Adds get_visible_cells_packed(): the visible grid as a compact little-endian
  buffer (4B cols/rows header + 13B/cell: codepoint u32, fg u32, bg u32, flags
  u8) for the RN Skia renderer (RN_MIGRATION.md Decision C).
- connect() accepts tls + pinned cert fingerprint at the boundary, but they are
  a documented no-op pass-through: client-side TLS lives on arch-review-fixes
  (PR #134), not on this branch's okena-core. Wiring is a follow-up.

Verified: cargo check passes for okena-mobile-ffi AND okena_mobile_native
(Flutter Rust side intact). Does not touch okena-core, Dart, or frb codegen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(Phase 2/3 scaffold)

Scaffolds the React Native side under mobile/rn/ — the two technically-meaty
pieces, not a full app:

- src/native/okena.ts — the native↔TS binding contract: typed OkenaNative
  interface for all ~60 FFI fns + record/enum types, sync/async split mirroring
  the Rust side. ubrn generates the real impl from crates/okena-mobile-ffi.
- src/native/cells.ts — packed cell-buffer decoder, byte-for-byte matching the
  Rust get_visible_cells_packed format (+ a zero-alloc PackedCells view).
- src/components/TerminalView.tsx — terminal_painter.dart's 3-pass paint ported
  to @shopify/react-native-skia (no xterm.js): bg rects, style-batched glyph
  runs, cursor; rAF repaint gated on isDirty (Decision C); onLayout sizing.
- src/theme.ts, package.json, tsconfig.json, README.md (exact local build steps).

Verified on this box: npm install + tsc --noEmit (strict) pass; Skia APIs
type-checked against @shopify/react-native-skia@1.5.x. Device build is not
possible here (no Android/iOS SDK, no ubrn-generated module) — README documents
the local steps. node_modules gitignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bar, layout (Phase 2)

Builds the RN UI on the existing contract + Skia renderer:

- Foundation: SavedServer + LayoutNode models (parseLayout mirrors the Flutter
  parser), zustand connection/workspace stores (polling cadence ported from the
  Flutter providers, native module injectable for tests), AsyncStorage-backed
  persistence behind a swappable interface, and a minimal state-driven router
  (no react-navigation native deps). App.tsx wires connection status → nav and
  drives workspace polling.
- Screens: ServerList (list + add-server sheet), Pairing (connect→code→paired
  with TLS-fingerprint footnote), Workspace (app bar + drawer + layout + toolbar).
- Widgets: ProjectDrawer (custom slide-in; projects/folders, add/reorder/color),
  KeyToolbar (ESC/TAB + sticky CTRL/ALT/CMD with control-char + CSI encoding,
  shared modifier store), LayoutRenderer (recursive split/tabs with portrait
  rotation), TerminalPane (hidden TextInput, tap-focus, scroll/selection gestures
  around the Skia TerminalView), StatusIndicator.

Ported from mobile/lib/src/{models,providers,screens,widgets}. Verified:
npm install + npx tsc --noEmit (strict) pass over all 22 RN source files.
No device build here (no SDK / ubrn module) — see mobile/rn/README.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
matej21 and others added 4 commits June 10, 2026 15:59
…0.31

Move the plain-Rust engine (ConnectionManager, MobileConnectionHandler,
TerminalHolder) and the api data structs out of the retired flutter_rust_bridge
crate mobile/native into crates/okena-mobile-ffi, stripping the #[frb] attributes.
Drop the okena_mobile_native path dependency (and the transitive
flutter_rust_bridge build) so the crate depends only on okena-core + uniffi.
Remove the now-dead execute_action/spawn helpers, and bump uniffi 0.29 -> 0.31
to match uniffi-bindgen-react-native 0.31 used by mobile/rn. Drop mobile/native
from the workspace members.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Turn the mobile/rn scaffold into a complete React Native 0.76 project (minus the
machine-generated native host dirs): index.js, app.json, metro/babel/
react-native config, jest + eslint + prettier, and the RN 0.76 devDependency set.
Add ubrn.config.yaml and ubrn:* scripts, and wire getOkenaNative() to load the
generated module from src/generated. Relocate the JetBrainsMono fonts to
mobile/rn/assets. npm run typecheck + lint + test all pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Delete the retired Flutter client (lib, ios/android/linux shells, tests,
cargokit, pubspec/flutter configs). Replace the Flutter build-mobile CI job with
a mobile-rn job (npm typecheck + lint) and drop it from the release deps. Update
the root README/CLAUDE, docs/mobile-status.md, .gitignore, and the RN_MIGRATION
status to describe the React Native + uniffi stack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pre-existing breakage surfaced by `cargo clippy --workspace --all-targets`
(CI's "Check compilation" runs `cargo check --release`, which skips tests,
so these test-only failures slipped through):

- remote_apply.rs / client/state.rs test helpers: add the `cols`/`rows`
  fields added in ac0a91b, plus the missing ApiProject fields and the
  `is_visible` → `show_in_overview` rename.
- text_utils.rs: silence newer clippy lints (is_some_and, drop unwrap()).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
matej21 and others added 3 commits June 10, 2026 19:56
`aws-lc-rs`'s jitter-entropy init (`jent_entropy_init`) segfaults on Android
when rustls builds a TLS client connection, crashing the mobile app on
connect (SIGSEGV in libokena-mobile-ffi during the TLS handshake setup).

Switch the rustls crypto provider to `ring`, which is portable on every
target (incl. the Android NDK) and is already what reqwest 0.12's
`rustls-tls` feature pulls in for hyper/tokio-rustls — so a single provider
is shared across reqwest + tungstenite. Applied to both the shared client
(okena-core) and the desktop remote server (src/remote/tls.rs) so the whole
workspace uses one provider; this also drops aws-lc-rs/aws-lc-sys from the
build entirely.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…re alias

Reconcile the hand-written `OkenaNative` contract with the shapes ubrn 0.31
actually emits, plus two build/render fixes surfaced by running the app:

- okena.ts: translate at the `getOkenaNative` boundary — ConnectionStatus
  (PascalCase `.tag` → lowercase `.kind`, Error payload from `.inner.message`),
  CursorShape (numeric enum → string union), and ProjectInfo.terminalNames
  (JS `Map` → plain object). Without these the status pill and terminal
  cursor crash on render (`Cannot read property 'color' of undefined`).
- TerminalView.tsx: drop the synthetic `setEmbolden` calls. react-native-skia
  1.12.4's native binding rejects the (typed `boolean`) arg with "Value is
  false, expected a number"; all four JetBrainsMono variants are bundled so
  the fallback is unused, and italic is still synthesized via `setSkewX`.
- metro.config.js: alias `@ubjs/core` (ubrn's renamed TS runtime, imported by
  the generated bindings) to the runtime already shipped inside
  uniffi-bindgen-react-native, avoiding a second, version-skewed copy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… Android integration

The ubrn turbo module is kept as a local package (`modules/okena-mobile-ffi/`,
a `file:` dependency of the app) rather than letting ubrn clobber the app's
root `android/build.gradle`. Track only its hand-authored `package.json` (so the
`file:` dep resolves on a fresh checkout / in CI — `npm ci` no longer fails);
everything else under it is ubrn-generated and stays gitignored (incl. the
multi-hundred-MB Rust static lib — must never be committed).

README: add a "Verified Android run" section documenting what the end-to-end
run actually required — the local-package layout, the committed fixes (rustls
`ring`, ubrn enum/record adapters, dropped Skia `setEmbolden`, `@ubjs/core`
Metro alias), the post-generation fixups (Node≥20 CMakeLists `require.resolve`,
missing `AndroidManifestNew.xml`), `okena pair` for the pairing code, and the
emulator GL-present crash that makes a physical device necessary for rendering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants