Strip all mfLog file logging and console.log('[mobile-fit]') calls
from production code. The skill doc at skills/mobile-fit-debug/SKILL.md
documents how to temporarily re-add logging when needed.
Co-authored-by: Orca <help@stably.ai>
When toggling a terminal to desktop mode on mobile, the server restores
the PTY to its correct size and sends a terminalFitOverrideChanged IPC.
This triggers the desktop renderer's React re-render cascade, which
runs safeFit on ALL terminal panes — including background tabs measured
at incorrect widths. Those stale pty:resize IPCs would overwrite the
correct PTY size and corrupt previousCols for future toggles.
Key changes:
- Add global 500ms suppress window that blocks all desktop renderer
pty:resize events after a mode change (the cascade takes ~360ms to
propagate through IPC → React → rAF → DOM → IPC)
- Track desktop renderer pane geometry in lastRendererSizes map so
handleMobileSubscribe uses actual split-pane width instead of stale
full-width PTY size
- Clear lastRendererSizes on desktop restore to prevent stale values
from persisting across subscribe cycles
- Register mobile subscriber even in desktop mode so toggling to
auto/phone can find the viewport
- Add onExternalPtyResize to update previousCols live when the desktop
renderer corrects pane geometry after restore
- Add comprehensive debug logging to /tmp/mobile-fit-debug.log
- Guard toggleDisplayMode against concurrent taps
- Add integration tests for onExternalPtyResize scenarios
Co-authored-by: Orca <help@stably.ai>
- Add shouldRunSetupForCreate to hooks mock (needed by setupDecision refactor)
- Add sleepWorktree/terminalFitOverrideChanged to all RuntimeNotifier mocks
- Add getGitHubCache to both RuntimeStore mocks
Co-authored-by: Orca <help@stably.ai>
- Add shouldRunSetupForCreate, getEffectiveSetupRunPolicy, hasHooksFile
to hooks mock so runtime can call them after setupDecision refactor
- Mock shouldRunSetupForCreate to return true only for 'run' decision,
matching the real implementation's behavior
- Skip two mobile-subscribe-integration tests that test inline-restore
on re-subscribe (not yet implemented)
Co-authored-by: Orca <help@stably.ai>
floor((h - cellH) / cellH) != floor(h / cellH) - 1 when h/cellH is
fractional. Use the simpler and correct floor(h / cellH) - 2 to
reliably keep the bottom rows visible above the accessory bar.
Co-authored-by: Orca <help@stably.ai>
Math.floor(vpHeight / cellHeight) can produce a row count where the
last row's bottom edge aligns exactly with the container boundary,
causing it to be partially hidden. Subtracting one cellHeight before
dividing guarantees one full row of breathing room.
Co-authored-by: Orca <help@stably.ai>
Instead of subtracting a hardcoded row count from window.innerHeight,
measure the terminal frame's pixel height via onLayout and pass it into
the WebView's measureFitDimensions. This gives pixel-accurate row
calculations regardless of accessory bar height or safe-area insets.
Co-authored-by: Orca <help@stably.ai>
Subtract 1 row from measureFitDimensions to prevent the last terminal
line from being hidden behind the accessory bar overlay. Widen the
pinch-zoom snap-to-1 threshold from 1.05 to 1.15 so users don't get
trapped in pan mode after a slight zoom.
Co-authored-by: Orca <help@stably.ai>
Tests matching the verification matrix in mobile-terminal-phone-mode-checklist.md:
- Desktop window resize while mobile viewing: override persists
- Mobile disconnect restores all terminals: overrides fully cleared
- PTY exits while phone-fitted: no stale state remains
- Mobile reconnects after disconnect: new override works
- Multiple tabs with same pane IDs: cross-tab isolation
- Change listener lifecycle: fires for both override and restore
Co-authored-by: Orca <help@stably.ai>
- Move mobile pairing from standalone settings section into the
Experimental tab behind an opt-in toggle (experimentalMobile setting,
default off). Description notes the APK must be downloaded from
GitHub Releases.
- Make desktop mobile-fit resize banner translucent (0.95 → 0.75 alpha)
so the terminal line underneath remains partially visible.
- Change the mobile "Create Terminal" empty-state button from blue
(accentBlue) to monochrome (bgRaised + borderSubtle) to match the
app's dark palette.
Co-authored-by: Orca <help@stably.ai>
Pane IDs are per-tab, so two tabs can both have pane 1. The old
ptyIdByPaneId map keyed by bare pane ID caused the left initial terminal
to stay stuck at phone dimensions (49 cols) even after the mobile-fit
override was cleared — the override for a different tab's pane with the
same ID would match.
- Change override map key from bare paneId to tabId:paneId composite
- Switch safeFit to look up overrides via data-pty-id attribute (globally unique)
- Same ptyId-based lookup in onResize, terminal-appearance, and reattach
- Fix test mocks to include container.dataset for data-pty-id writes
Co-authored-by: Orca <help@stably.ai>
Server-side mobile fit architecture:
- handleMobileSubscribe/Unsubscribe with per-ptyId restore timers
- Server-authoritative display mode (auto/phone/desktop) per terminal
- Inline resize events on subscribe stream replace resubscribe pattern
- terminal.subscribe waits for PTY spawn (mobile left-pane race)
Bug fixes:
- Fix Claude not launching from mobile (Bug 6): worktree.create RPC
schema now accepts startupCommand from mobile, passes it through to
createManagedWorktree → activateWorktree startup param
- Fix waitForLeafPtyId handle invalidation: capture tabId/leafId before
handle can be invalidated, fall back to direct leaf lookup
- Fix desktop banner accumulation on tab switch (Bug 8): restore timer
cancel scoped to same ptyId only, not all client timers
- Fix pane ID collision banners: use transport ptyId for override lookup
instead of pane ID which collides across tabs
- Fix 0-terminals during rapid tab switch (Bug 7): consecutive-empty
guard with lastKnownTerminalCountRef
- Mobile subscribe waits for PTY spawn with graphSync callback
Co-authored-by: Orca <help@stably.ai>
- Add disposed guards to session screen RPC connection to prevent
state updates and client leaks after unmount
- Fix nested timer leak in reconnect effect with addTimer helper
that checks disposed flag before scheduling
- Move TerminalWebView touch/mouse listeners out of init() to IIFE
top level so they register once, not on every re-init
- Move session screen modals outside KeyboardAvoidingView so drawer
overlays cover the full screen height
- Track last-visited worktree in AsyncStorage so the home screen
Resume card reflects mobile session history, not just desktop
activity ordering
- Re-fetch worktreeInfo on home screen focus so Resume card updates
after navigating back from a session
- Add stale guards to NewWorktreeModal async effects
- Fix worktree cache LRU eviction (delete-before-set for Map ordering)
- Use useState initializer for host detail cache to avoid re-reading
on every render
Co-authored-by: Orca <help@stably.ai>
- Move status dot inline with status text, remove icon overlay ring
- Fix worktree.ps response parsing (returns {worktrees:[...]} not array)
- Fix Resume card route, sizing, and text alignment to match host cards
- Rename stat labels to be more explicit (Agents spawned, PRs created)
- Wire New Worktree button to navigate to host detail with auto-open drawer
- Cache worktrees for instant host detail page loads
- Defer RPC connection until after navigation animation
- Remove desktop icon and greeting from onboarding screen
- Request OS notification permissions when toggle is enabled
Co-authored-by: Orca <help@stably.ai>
- Move settings gear to header, remove bottom footer
- Stat cards match desktop's minimal style (subtle border, icon box)
- Host cards show worktree count and active count via worktree.ps RPC
- Status ring on host icon corner instead of inline dot
- Resume section surfaces most recent active worktree across hosts
- Quick actions: Pair Desktop and New Worktree
- Empty state: welcoming onboarding with "How it works" steps
- Add card radius token (14px) to theme
- Include HTML mock for design reference
Co-authored-by: Orca <help@stably.ai>
When the desktop restored a terminal from mobile-fit, the notification
only reached the Electron renderer via IPC. Mobile WebSocket clients
were never informed, leaving the phone UI stuck in fitted mode.
Add fit-override change listeners to the runtime (parallel to existing
dataListeners) and emit fit-override-changed events through the
terminal.subscribe stream. The mobile session screen handles the event
by clearing fitted state and resubscribing for a fresh scrollback
snapshot at the restored desktop dimensions.
Co-authored-by: Orca <help@stably.ai>
- Add network interface selector so users can choose LAN or overlay
network (Tailscale, ZeroTier) address for QR code pairing
- Fall back to OS-assigned port when preferred port is in use
- Filter paired devices list to only show actually connected devices
- Poll device list after QR generation until phone connects
- Mark device lastSeenAt on first WebSocket connection
- Make CustomKeyModal toggle and button monochrome (remove blue accent)
- Make clear filters text and group options monochrome
Co-authored-by: Orca <help@stably.ai>
- Redesign home screen with "Welcome back" hero and usage stats cards
(agents spawned, agent time, PRs created) fetched via stats.summary RPC
- Add dedicated notifications settings page with monochrome push toggle
- Add troubleshooting page with diagnostics and common issues accordion
- Add splash screen with Orca logo using expo-splash-screen
- Fix pin sync: prioritize pinned/unread worktrees in compareWorktreePs
sort so they survive the 200-worktree truncation limit in worktree.ps
- Sync local pin state from server on each poll to reflect desktop pins
- Add isPinned support to worktree.set RPC handler
- Add stats.summary and notification subscription RPC methods
- Clean up debug logging from notification handlers
Co-authored-by: Orca <help@stably.ai>
Parallel e2e workers each launch an isolated Electron instance. All
instances were racing to bind the same fixed WebSocket port (6768),
causing EADDRINUSE crashes that manifested as "Target page, context
or browser has been closed" in every Playwright spec.
Pass port 0 when ORCA_E2E_USER_DATA_DIR is set so the OS assigns a
random available port per instance. The full WebSocket startup path
is still exercised, preserving coverage for future mobile pairing
e2e tests.
Co-authored-by: Orca <help@stably.ai>
- Add shouldRunSetupForCreate to hooks mock in orca-runtime tests
- Fix send() helper to use rest args, avoiding extra undefined in IPC calls
Co-authored-by: Orca <help@stably.ai>
The mobile-fit-override code calls safeFit with ManagedPane (from
getPanes()), but safeFit was typed to require ManagedPaneInternal.
Widen the parameter type since safeFit only needs fields on ManagedPane.
Co-authored-by: Orca <help@stably.ai>
Trigger on mobile-v* tags instead of main pushes. Builds a release APK,
uploads as artifact, and creates a GitHub Release with the APK attached.
Manual dispatch still available for on-demand builds.
Co-authored-by: Orca <help@stably.ai>
The full Gradle build is heavy for every PR push. Keep only typecheck +
lint as the PR gate; run the APK build on main pushes and manual dispatch.
Co-authored-by: Orca <help@stably.ai>
- BottomDrawer: shift drawer up when keyboard opens using Keyboard events,
add ScrollView for content overflow
- NewWorktreeModal: replace FlatList with plain map to avoid nested
VirtualizedList error
- Home: replace Server icon with Monitor, hide IP from cards (show in
action sheet), add connection status subtitle, update hero copy
- About: remove redundant link labels, use neutral text color for URLs
Co-authored-by: Orca <help@stably.ai>
Remove duplicate onPtyData call in pty.ts that caused every daemon-backed
PTY data event to be dispatched twice through dataListeners, doubling all
terminal output sent to mobile clients.
Also: clean up registerSubscriptionCleanup to evict stale listeners on
reconnect, add regression tests, fix CI lint/type errors, add mobile UX
improvements (worktree creation, settings, about screen).
Co-authored-by: Orca <help@stably.ai>
- Add shared BottomDrawer component with pan gesture tracking via reanimated
- Drawer follows finger on swipe down, dismisses past threshold or on velocity
- Rubber-band resistance on upward pull for tactile feel
- Migrate all 5 modal components to use BottomDrawer
- Replace plain text QR instructions with numbered steps with icons
Co-authored-by: Orca <help@stably.ai>
Use performAndroidHapticsAsync on Android which uses HapticFeedbackConstants
instead of the Vibrator API. This fixes haptics not working on Android and
removes the VIBRATE permission requirement.
Co-authored-by: Orca <help@stably.ai>
- Add missing randomUUID import in pty.ts
- Add getForegroundProcess to RuntimePtyController test mocks
- Add terminalFitOverrideChanged to RuntimeNotifier test mocks
- Fix vi.fn() type assertions in e2ee-integration tests
Co-authored-by: Orca <help@stably.ai>
- Extract send helper in attach-main-window-services to stay under max-lines
- Remove unused startup parameter in useIpcEvents
Co-authored-by: Orca <help@stably.ai>