204 lines
12 KiB
Markdown
204 lines
12 KiB
Markdown
# Mobile Phone-Fit Debug Status
|
|
|
|
## Architecture Overview
|
|
|
|
```
|
|
Mobile Client Server (orca-runtime) Desktop Renderer
|
|
───────────── ──────────────────── ────────────────
|
|
subscribeToTerminal(handle) → terminal.subscribe handler → IPC: terminalFitOverrideChanged
|
|
sends { client, viewport } calls handleMobileSubscribe() setFitOverride() → banner render
|
|
resizes PTY, sets override
|
|
serializes scrollback
|
|
← scrollback { cols, rows,
|
|
serialized, displayMode }
|
|
|
|
switchTab(handle) → terminal.unsubscribe (old)
|
|
unsub old, subscribe new handleMobileUnsubscribe()
|
|
starts 300ms restore timer
|
|
→ terminal.subscribe (new)
|
|
handleMobileSubscribe()
|
|
cancels timer, inline-restores old
|
|
resizes new PTY
|
|
|
|
toggleDisplayMode(handle) → terminal.setDisplayMode
|
|
applyMobileDisplayMode()
|
|
← resized event on stream
|
|
```
|
|
|
|
## Key Data Structures (Server)
|
|
|
|
- `mobileSubscribers: Map<ptyId, { clientId, viewport, wasResizedToPhone, previousCols, previousRows }>`
|
|
- `pendingRestoreTimers: Map<ptyId, { timer, clientId }>` (changed from clientId-keyed to ptyId-keyed)
|
|
- `terminalFitOverrides: Map<ptyId, { mode, cols, rows, previousCols, previousRows, clientId }>`
|
|
- `mobileDisplayModes: Map<ptyId, 'auto' | 'phone' | 'desktop'>`
|
|
|
|
## Key Data Structures (Mobile Client)
|
|
|
|
- `viewportRef: { cols, rows } | null` — measured once from xterm, passed with every subscribe
|
|
- `viewportMeasuredRef: boolean` — true after first successful measurement
|
|
- `terminalUnsubsRef: Map<handle, unsub()>` — active subscription cleanup closures
|
|
- `initializedHandlesRef: Set<handle>` — tracks which terminals have been init'd (prevents double-init)
|
|
- `subscribeSeqRef: Map<handle, number>` — monotonic counter to ignore stale scrollback
|
|
|
|
---
|
|
|
|
## Bug 1: pendingRestoreTimers lost when 2 unsubscribes happen back-to-back
|
|
|
|
**Status: FIXED (verification inconclusive — may need more testing)**
|
|
|
|
**Root cause**: `pendingRestoreTimers` was keyed by `clientId` (one slot per device). When two terminals were unsubscribed in quick succession, the second timer overwrote the first.
|
|
|
|
**Fix applied**:
|
|
1. Changed `pendingRestoreTimers` from `Map<clientId, {timer, ptyId}>` to `Map<ptyId, {timer, clientId}>`
|
|
2. `handleMobileSubscribe` cancels only the restore timer for the SAME ptyId (re-subscribe case). Other terminals' timers fire normally so their desktop banners clear.
|
|
3. `handleMobileSubscribe` skips resize if PTY is already at target phone dims
|
|
4. `handleCreateTerminal` now unsubscribes the old active terminal before setting the new one
|
|
5. Restore happens via: 300ms timer (tab switch), or `onClientDisconnected` (full disconnect)
|
|
|
|
**Bug 8 (banners accumulate on tab switch)**: The original Bug 1 fix was too aggressive — it cancelled ALL pending restore timers for the client, not just the one for the ptyId being re-subscribed. This prevented the 300ms restore timer from firing for the old terminal, so its desktop banner persisted. Fixed by narrowing the cancel scope to only the same ptyId.
|
|
|
|
**Files**: `orca-runtime.ts`, `[worktreeId].tsx`
|
|
|
|
---
|
|
|
|
## Bug 2: Intermittent blank terminal on tab switch
|
|
|
|
**Status: FIX v2 APPLIED — needs testing**
|
|
|
|
**Symptom**: After creating a new terminal tab, the original 2 terminals occasionally show blank. Leaving the worktree and re-entering fixes it.
|
|
|
|
**Root cause**: The WebView loads xterm.js from CDN, which takes time. Messages sent before `web-ready` queue in `pendingMessagesRef` and flush when ready. The original `handleTerminalWebReady` would unsub+resub ALL initialized terminals (even inactive ones), creating stale server-side subscriptions and disrupting the data stream.
|
|
|
|
**Fix v1 (FAILED)**: Gated `subscribeToTerminal` on webReady. This was too aggressive — it prevented subscriptions entirely, so no scrollback arrived, no init was queued, and the terminal stayed blank.
|
|
|
|
**Fix v2 (current)**:
|
|
1. `subscribeToTerminal` has NO webReady guard — subscriptions start immediately, init messages queue in `pendingMessagesRef`, and flush when `web-ready` fires
|
|
2. `handleTerminalWebReady` distinguishes first load vs reload:
|
|
- **First load** (`wasAlreadyReady=false`): just marks webReady, triggers viewport measurement for active terminal. Pending messages flush after this callback returns → terminal renders.
|
|
- **Reload** (`wasAlreadyReady=true`): unsubscribes and resubscribes to get fresh scrollback (old xterm buffer is gone). Only resubscribes if active.
|
|
3. `setTerminalWebViewRef` simplified to just store the ref (no subscription logic)
|
|
|
|
**Flow (first load)**:
|
|
1. `fetchTerminals` → `subscribeToTerminal(active)` → stream starts
|
|
2. Scrollback arrives → `init()` queued in pendingMessages (WebView not ready yet)
|
|
3. WebView loads xterm from CDN → `web-ready` fires
|
|
4. `handleTerminalWebReady`: marks webReady, triggers viewport measurement (async)
|
|
5. `flushPendingMessages()`: sends queued init → terminal renders
|
|
6. Viewport measured → resubscribe with dims → server phone-fits → reinit with phone dims
|
|
|
|
---
|
|
|
|
## Bug 5: Desktop banner not showing for initial split pane after creating new tab
|
|
|
|
**Status: FIXED (verified via CDP + e2e testing)**
|
|
|
|
**Root cause**: The bug was caused by the inline restore mechanism in `handleMobileSubscribe`. When mobile switched tabs, the server would restore the previous terminal to desktop dims (sending `desktop-fit` IPC), which cleared the override from `overridesByPtyId`. After the next subscribe, the override was re-set via `mobile-fit` IPC, but the timing was tight and the banner could flicker or fail to appear.
|
|
|
|
**Fix**: Removed inline restore from `handleMobileSubscribe` (Bug 1 fix). PTYs now stay at phone dims when the mobile client switches tabs. Overrides persist in `overridesByPtyId` until the mobile client disconnects. This means ALL terminals the mobile client has subscribed to show the banner — which is correct since the mobile client "owns" those terminals.
|
|
|
|
**Verification**: E2e testing via CDP (desktop renderer) and agent-device (mobile) confirmed:
|
|
- `setFitOverride()` is called correctly via IPC
|
|
- `onOverrideChange` fires and triggers re-renders
|
|
- `getFitOverrideForPane()` finds overrides when `ptyIdByPaneId` bindings exist
|
|
- Banners show correctly for all terminals (1, 2, and 3 tabs) including after creating new tabs
|
|
- The earlier diagnostic showed HMR clearing module-level maps was a test artifact, not a production issue
|
|
|
|
---
|
|
|
|
## Bug 7: Terminals disappear ("0 terminals") during rapid tab switching
|
|
|
|
**Status: FIXED (verified via e2e testing)**
|
|
|
|
**Symptom**: During rapid tab switching (3+ toggles in quick succession), all terminals disappear from the mobile UI. The terminal list shows "0 terminals". Leaving the worktree and re-entering fixes it.
|
|
|
|
**Root cause**: The periodic `fetchTerminals()` (every 2s) calls with `allowEmptyLoaded: true`. The server can transiently return an empty terminal list during rapid operations. The old code's subscription cleanup loop (`liveHandles` check) ran before the empty guard, unsubscribing all terminals, which then made the empty guard's `terminalUnsubsRef.current.size > 0` check fail.
|
|
|
|
**Fix applied**:
|
|
1. Added `lastKnownTerminalCountRef` to track the previous non-zero terminal count
|
|
2. When the server returns 0 terminals but `lastKnownTerminalCountRef > 0`, skip the FIRST empty response (set ref to 0 and return early)
|
|
3. On the NEXT fetch, if still empty, `lastKnownTerminalCountRef` is 0, so the guard doesn't trigger and terminals are cleared normally
|
|
4. This gives a ~2s grace period (one polling interval) to filter out transient empty responses
|
|
5. The guard runs BEFORE the subscription cleanup loop, preventing premature unsubscription
|
|
|
|
**Verification**: Rapid tab switching (8 cycles across 3 terminals in ~1.6s) with 20s wait — terminals survived. Previous code cleared to "0 terminals" within 18s.
|
|
|
|
**Files**: `[worktreeId].tsx`
|
|
|
|
---
|
|
|
|
## Bug 3: Viewport measurement chicken-and-egg
|
|
|
|
**Status: SOLVED (workaround in place)**
|
|
|
|
The first subscribe has `viewport=none`. After scrollback init, async measure viewport, resubscribe with dims. Takes 2-3 round-trips. Works reliably.
|
|
|
|
---
|
|
|
|
## Bug 4: Desktop terminal focus doesn't follow mobile tab switching
|
|
|
|
**Status: FIXED**
|
|
|
|
`switchTab` now calls `terminal.focus` RPC.
|
|
|
|
---
|
|
|
|
## Bug 6: Claude not launching on new workspace
|
|
|
|
**Status: FIXED**
|
|
|
|
**Root cause**: Mobile's `NewWorktreeModal` sends `startupCommand` (e.g. `'claude'`) in the `worktree.create` RPC call, but the Zod schema (`WorktreeCreate`) did not include `startupCommand`, so it was silently stripped during validation. The runtime's `createManagedWorktree` never received the startup command, so `args.startup` was always undefined and the activation IPC never included a startup payload. The left pane spawned a plain shell instead of the selected agent.
|
|
|
|
**Fix applied**:
|
|
1. Added `startupCommand: OptionalString` to the `WorktreeCreate` Zod schema in `src/main/runtime/rpc/methods/worktree.ts`
|
|
2. RPC handler maps `params.startupCommand` → `{ command }` and passes to `runtime.createManagedWorktree()`
|
|
3. Added `startup?: WorktreeStartupLaunch` to `createManagedWorktree`'s args type in `orca-runtime.ts`
|
|
|
|
**Bug 6b: `waitForLeafPtyId` times out due to handle invalidation**
|
|
|
|
When a leaf's ptyId changes from null to a real value, `syncWindowGraph` invalidates the old handle (deletes it from `this.handles`). The `waitForLeafPtyId` callback calls `resolveLeafForHandle(handle)` which returns null because the handle no longer exists. The wait never resolves.
|
|
|
|
**Fix**: `waitForLeafPtyId` now captures the handle's `tabId` and `leafId` before the handle can be invalidated. The callback falls back to direct `this.leaves.get(getLeafKey(tabId, leafId))` lookup when the handle-based lookup fails.
|
|
|
|
**Files**: `src/main/runtime/rpc/methods/worktree.ts`, `src/main/runtime/orca-runtime.ts`
|
|
|
|
---
|
|
|
|
## Diagnostic Logging
|
|
|
|
All logs prefixed with `[mobile-fit]`.
|
|
|
|
### Server-side (main process stdout)
|
|
| Location | What it logs |
|
|
|----------|-------------|
|
|
| `terminal.subscribe` handler | handle, ptyId, client type, viewport |
|
|
| `handleMobileSubscribe` | ptyId, mode, viewport, skip reasons, resize details |
|
|
| `handleMobileUnsubscribe` | ptyId, subscriber state, wasResized |
|
|
| `applyMobileDisplayMode` | ptyId, mode, subscriber state |
|
|
|
|
### Mobile client (React Native console)
|
|
| Location | What it logs |
|
|
|----------|-------------|
|
|
| `subscribeToTerminal` | handle, seq, viewport, measured state |
|
|
| scrollback handler | cols, rows, displayMode, hasSerialized, alreadyInit |
|
|
| resized handler | cols, rows, displayMode, reason |
|
|
| `switchTab` | prev/next handle, hasUnsub, hasRef |
|
|
| `toggleDisplayMode` | handle, current mode, next mode |
|
|
| `setTerminalWebViewRef` | handle, isActive, activeHandle |
|
|
|
|
### Desktop renderer (DevTools console)
|
|
| Location | What it logs |
|
|
|----------|-------------|
|
|
| `useIpcEvents.ts` | fitOverrideChanged IPC events received |
|
|
| `TerminalPane.tsx` | onOverrideChange callbacks fired |
|
|
|
|
---
|
|
|
|
## Priority Order
|
|
|
|
1. ~~**Bug 2 (blank terminal)**~~ — **FIXED**. WebView readiness lifecycle corrected.
|
|
2. ~~**Duplicate prompt lines**~~ — **FIXED**. Removed inline restore, added alreadyAtTarget skip.
|
|
3. ~~**Bug 7 (0 terminals)**~~ — **FIXED**. Consecutive-empty guard with `lastKnownTerminalCountRef`.
|
|
4. ~~**Bug 5 (banner missing)**~~ — **FIXED**. Side effect of inline restore removal + verified via CDP.
|
|
5. ~~**Bug 1 (timer overwrite)**~~ — **FIXED**. `pendingRestoreTimers` keyed by ptyId, cancel without restore.
|
|
6. **Cleanup** — Remove `[mobile-fit]` diagnostic logs, Debug Test button, dead code.
|