* Fix Vim alternate-screen terminal redraw recovery
* Decide alternate-screen atlas recovery from parsed buffer state
Replace the raw-chunk scan for alternate-screen enter sequences with the
xterm parser's own verdict: in-place rewrite chunks check
buffer.active.type (plus a parse-time buffer-switch count) inside the
write callback, where the buffer is authoritative. Covers enter/exit
sequences split across PTY chunk boundaries and full enter-exit cycles
coalesced into one write, and deletes the hand-rolled CSI parser.
Adds regression tests from a real captured vim session (including a
CSI sequence split at a 1024-byte PTY read boundary) and a contract
pin against the real @xterm/headless parser.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(emulator): recycle iOS simulators that wedge-boot without a display framebuffer
CoreSimulator can report a device Booted while its display IO ports never
came up (HID alive, no com.apple.framebuffer.display port), so
ensureSimulatorBooted passes and serve-sim --detach dies with 'No
framebuffer display descriptor found'. Reconnecting hits the same Booted
early-return, so the pane could never recover without a manual
simctl shutdown/boot.
startSession now recognizes that helper failure signature, recycles the
device once (shutdown + boot), and retries; if the display still fails to
come up it surfaces an actionable erase/recreate message instead of the
raw helper log dump.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* review: harden simulator framebuffer recovery
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* Fix mirrored Codex relative config paths
Orca mirrors ~/.codex/config.toml into a managed CODEX_HOME before launching Codex. Relative path-valued Codex settings were then resolved from the runtime home instead of the user's real Codex home, which made config loading fail in Orca while the same CLI worked in a normal terminal. Rewrite known relative path settings to absolute paths rooted at the system Codex home while preserving runtime-owned trust sections.
* Dedupe Codex TOML line scanner and include path rewrite in CLI tsconfig
* Harden Codex config path rewrite and cover managed account homes
- Track multiline arrays in the shared TOML line scanner so array lines
are never mistaken for table headers or path keys
- Escape control characters and reject lone-surrogate unicode escapes so
the rewritten runtime config always stays valid TOML
- Extend the rewrite allowlist with profiles.* file settings and
debug.config_lockfile.* (both can abort Codex config loading)
- Rewrite relative paths when mirroring the canonical config into
managed account homes (codex login CODEX_HOMEs), anchoring WSL
accounts to the Linux-side ~/.codex with posix join semantics
---------
Co-authored-by: Neil <neil@stably.ai>
Fixes#7116.
Use a forward-slash bare .cmd path for Claude/OpenClaude Windows hooks when the managed script path is Git-Bash-safe, avoiding the per-hook PowerShell startup overhead in the common case. Unsafe paths with spaces or shell metacharacters continue to use the encoded PowerShell launcher from #6078.
Keep Codex on its cmd.exe-safe fast path and leave the other agent integrations on the hardened encoded launcher.
Co-authored-by: Eren Çakar <hey@erencakar.com>
Run independent SSH relay bootstrap probes concurrently when the connection can safely support overlapping execs. Preserve the old sequential path for system SSH without reusable ControlMaster and for remotes that reject concurrent session channels.
* Verify Windows app executable signing
* Isolate Windows signing verifier tests
* Handle direct Windows installer extraction
---------
Co-authored-by: Neil <neil@stably.ai>
* Garbage-collect stale worktreeMeta for externally-deleted worktrees at load
worktreeMeta rows are minted for every worktree Orca ever discovers but
were only deleted by Orca-initiated removal flows. Worktrees deleted
outside Orca (git worktree remove, rm -rf, agent scripts) purge renderer
session state without ever removing the meta, so the map grew
monotonically - 318 of 503 entries (63%, ~110KB) on the reference
machine pointed at long-deleted paths. These entries never reach the
renderer; they only cost disk bytes, parse time, and memory.
GC at load is deliberately narrow: local-host entries only (SSH/runtime
repos, remote meta.hostId, and WSL UNC paths are skipped - a local
existsSync would falsely condemn remote paths), only when the checkout
path no longer exists, only after a 30-day idle grace (preserving
pushTarget cleanup for recently-vanished worktrees and quick
recreations), and never when the entry lacks timestamps to prove
idleness. Lineage rows cascade like removeWorktreeMeta.
Co-authored-by: Orca <help@stably.ai>
* Fix GC false positives: folder-workspace instances, WSL paths on Windows
From adversarial review of the GC commit:
- Folder-project workspace instances are keyed repoId::path::workspace:
<uuid>; splitting on the first :: made the existence probe test a
path-with-suffix that never exists, deleting idle folder workspaces -
whose meta IS the workspace record - and cascading into terminal-
history deletion. Instance keys are now skipped entirely.
- On Windows, WSL linked worktrees carry Linux-style paths from git
porcelain that a Windows existsSync cannot probe; skip non-Windows
paths on win32.
- Idle-grace check now runs before existsSync (skips the stat fan-out
for active entries and shrinks the slow-NFS startup tail), and a
null worktreeMeta map in the file is normalized instead of throwing
outside the parse guard.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* Move githubCache out of the durable state file into a quit-time sidecar
Every PR/issue poll restamps fetchedAt even when nothing changed, which
defeated the content-hash write guard by construction: ~100KB of
refetchable 5-min-TTL cache rewrote the whole 1.6MB orca-data.json once
per refresh cycle - the last remaining non-user-driven writer.
The cache is now memory-only during the session (setGitHubCache no
longer schedules a save; the durable payload and state hash both omit
it) and is snapshotted best-effort to orca-github-cache.json at flush
(quit/reload), so PR/issue badges still paint instantly on next launch.
A legacy in-file cache is used as the seed and stripped by the next
durable write; downgrades just recreate the key, which the next upgrade
strips again. Sidecar loss costs nothing - the data is refetchable.
Co-authored-by: Orca <help@stably.ai>
* Reject null/array pr-issue shapes when reading the github cache sidecar
A corrupt-but-valid-JSON sidecar ({"pr":null}) would seed
state.githubCache.pr = null and crash mobile worktree.ps PR grouping
until the renderer's first cache save repaired it.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix: correct IME cursor after terminal reattach
* fix: stabilize Cursor Agent IME anchor
* Fix Cursor Agent reattach IME detection
* Harden live-agent reattach detection and always restore cursor visibility
- POST_REPLAY_LIVE_AGENT_REATTACH_RESET now includes ?25h so a false-positive
agent signal (or dead TUI leftovers) can never leave a shell with a
permanently hidden cursor; only focus reporting (?1004h) is preserved.
- Replace the broad getAgentLabel token match (fires on titles like
'ssh devin@host') with an exact cursor-agent native-title check.
- Anchor the replay-payload screen signal on the LAST 'Cursor Agent' header so
scrollback from an earlier finished run does not classify the current screen.
- Make the payload-signal flag replay-shape-aware: incremental
(clearBeforeReplay:false) frames can only add evidence; full-screen replays
remain authoritative.
Co-authored-by: Orca <help@stably.ai>
* Fix anchor scan direction and drop non-decaying launch metadata from liveness
- Scan the visible screen bottom-up in resolveCursorAgentImeAnchor so a
transcript line containing an arrow (e.g. a rename diff) cannot hijack the
IME anchor away from the input box.
- Remove getAuthoritativePaneAgent() from hasLiveAgentReattachSignal:
tab.launchAgent never decays after the agent exits, so it would preserve
?1004h and inject focus-in into the shell left behind by a dead agent.
- Clear a stale replay-payload agent signal on an authoritative empty clearing
frame.
Co-authored-by: Orca <help@stably.ai>
* Veto scrollback-only Cursor Agent signal against the parsed viewport
The replay-byte screen signal also matches a dead run's screen sitting in
scrollback (daemon snapshots serialize full scrollback; the relay buffer is
never cleared). After xterm parses the replay, confirm a payload-only signal
against the visible screen via resolveCursorAgentImeAnchor; when the viewport
shows a plain shell, drop focus reporting (?1004l) and skip the focus-in so a
bare shell never inherits stale focus modes or receives stray focus bytes.
Co-authored-by: Orca <help@stably.ai>
* Make the reattach viewport veto shape-based and latest-frame-only
- parsedViewportShowsCursorAgentScreen now checks the screen shape (header +
input row) via viewportShowsCursorAgentScreen instead of requiring the
parked-cursor precondition, so a live agent whose cursor sits at the caret
is not downgraded.
- Add a replay-signal generation counter so a stale post-parse callback stands
down when a newer replay frame has updated the signal.
Co-authored-by: Orca <help@stably.ai>
* Require the parked cursor in the reattach viewport confirmation
A dead cursor-agent screen can still be painted in the viewport with the
shell prompt below it, which passes a shape-only check and would preserve
?1004h and inject a focus-in into the shell. Confirm a payload-only signal
with resolveCursorAgentImeAnchor (shape + parked cursor): a live agent that
needs the focus-in is by definition parked, and an unparked live agent only
falls back to the pre-fix reattach behavior.
Co-authored-by: Orca <help@stably.ai>
* Complete window stub in pty-connection tests for command-finished event
Main's onCommandFinished now dispatches a window CustomEvent synchronously,
but the node-env connectPanePty window stub omits dispatchEvent. These tests
only passed when another test file in the same vitest worker happened to leave
a window with dispatchEvent; this branch's added test files shift sharding and
expose the gap. Add dispatchEvent/addEventListener/removeEventListener to the
stub so the command-finished path is covered deterministically.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* Fix Korean IME composition by deferring live terminal preedit
The mobile terminal capture field previously sent and cleared every TextInput change, which can break Hangul composition on Android keyboards. Introduce a small commit model and extracted live-input hook so composed text is flushed deliberately while ASCII remains immediate.
Constraint: React Native TextInput has no portable composition event for this path; the fix uses a bounded commit delay for likely IME text.
Rejected: Native-module IME integration | unnecessary for the confirmed JS dispatch/clear failure and higher maintenance risk.
Confidence: high
Scope-risk: moderate
Directive: Keep terminal.send payload shape and buffered command input unchanged; do not claim physical Samsung Keyboard QA without device evidence.
Tested: cd mobile && pnpm exec vitest run src/terminal/terminal-live-text-commit.test.ts src/terminal/terminal-live-input.test.ts src/terminal/terminal-text-input-normalization.test.ts src/terminal/terminal-keyboard-type.test.ts --reporter=verbose
Tested: cd mobile && pnpm exec tsc --noEmit
Tested: cd mobile && pnpm exec oxlint src/terminal/terminal-live-text-commit.ts src/terminal/terminal-live-text-commit.test.ts src/terminal/use-terminal-live-input-commit.ts app/h/[hostId]/session/[worktreeId].tsx
Not-tested: Physical Galaxy Fold7/Samsung Keyboard and Android emulator/Gboard QA were unavailable; device probes recorded no attached Android device.
* Preserve pending Korean IME text before mobile accessory controls
Accessory keys share the same pending live-input commit gate as TextInput keypress and submit paths, so control bytes cannot race ahead of composed Hangul.
Constraint: React Native mobile input does not expose portable composition events for Samsung/Gboard IME paths.
Rejected: Let accessory buttons keep sending directly | Direct sends can drop pending Hangul before Tab/Esc/Enter/Backspace reaches the PTY.
Confidence: high
Scope-risk: narrow
Directive: Keep all terminal control-byte paths behind the pending live-input flush/local-edit decision before sending to the PTY.
Tested: pnpm --dir mobile test; pnpm --dir mobile lint; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile exec oxfmt --check src/terminal/terminal-live-text-commit.ts src/terminal/terminal-live-text-commit.test.ts src/terminal/use-terminal-live-input-commit.ts src/terminal/use-terminal-live-accessory-input-commit.ts app/h/[hostId]/session/[worktreeId].tsx; git diff --cached --check
Not-tested: Physical Galaxy Fold7 Samsung keyboard manual QA is still external-device only.
* Prevent stale IME timer flushes after mobile terminal teardown
Pending live-input timers now clear on hook unmount, and accessory Delete documents why it stays local without trimming pending IME text.
Constraint: React Native TextInput lacks a portable composition lifecycle, so pending IME text is guarded by a bounded timer that must not survive screen teardown.
Rejected: Use clearPendingLiveInputCommit during unmount | it would also touch React state/native props during teardown when only timer/ref cleanup is required.
Confidence: high
Scope-risk: narrow
Directive: Any delayed terminal input commit must have an owner-lifecycle cleanup path before sending to the PTY.
Tested: pnpm --dir mobile test; pnpm --dir mobile lint; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile exec vitest run src/terminal/terminal-live-text-commit.test.ts --reporter=verbose; pnpm --dir mobile exec oxfmt --check src/terminal/terminal-live-text-commit.ts src/terminal/use-terminal-live-input-commit.ts; git diff --check
Not-tested: Physical Galaxy Fold7 Samsung keyboard manual QA remains unavailable in this environment.
* Use semantic accessory edits for mobile IME commits
Accessory Backspace/Delete now carry semantic local-edit intent from built-in keys instead of inferring intent from raw bytes, and submit handling is reconnected to the pure submit-sequence model.
Constraint: Custom terminal accessory keys may produce the same bytes as built-ins but should still flush pending IME text before sending rather than being silently treated as hidden-input edits.
Rejected: Classify local accessory edits by raw bytes | That couples future custom controls to current built-in byte encodings.
Confidence: high
Scope-risk: narrow
Directive: Keep semantic input intent separate from terminal byte payloads when pending IME text is present.
Tested: pnpm --dir mobile test; pnpm --dir mobile lint; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile exec oxfmt --check src/terminal/terminal-live-text-commit.ts src/terminal/terminal-live-text-commit.test.ts src/terminal/use-terminal-live-input-commit.ts src/terminal/use-terminal-live-accessory-input-commit.ts app/h/[hostId]/session/[worktreeId].tsx; git diff --check
Not-tested: Physical Galaxy Fold7 Samsung keyboard manual QA remains unavailable in this environment.
* Respect IME flush failures before control input
Propagate terminal.send success from pending Korean IME text before sending Enter, Tab, or accessory bytes, while keeping custom no-pending accessory bytes on the original direct path.
Constraint: PR #7011 review required follow-up control bytes only after the pending composed text send actually succeeds.
Rejected: Treating send invocation as success | It can still reject or no-op when RPC state changed.
Confidence: high
Scope-risk: narrow
Directive: Keep pending IME flush paths async-success-aware before adding new terminal control inputs.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; pnpm --dir mobile exec oxfmt --check changed files; targeted no-excuse clean for mobile/src/terminal changed files.
Not-tested: Physical Galaxy Fold7 Samsung keyboard; full session file no-excuse audit still reports pre-existing unrelated violations.
* Serialize mobile IME flushes before live controls
Treat terminal.send as successful only when the RPC response is ok and the runtime send result is accepted, then route all live-input control sends through a shared in-flight pending-flush barrier.
Constraint: PR #7011 review found that resolved RPC promises and per-call sequencing were not enough to prove pending Hangul text reached the PTY before follow-up controls.
Rejected: Only awaiting each flush-then-send call | Repeatable accessory keys and no-pending sends can arrive while the first flush is still in flight.
Confidence: high
Scope-risk: moderate
Directive: Keep future mobile terminal control paths behind the pending-flush barrier whenever IME text may be in flight.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; pnpm --dir mobile exec oxfmt --check changed files; no-excuse clean for terminal changed files.
Not-tested: Physical Galaxy Fold7 Samsung keyboard; full session file no-excuse audit still reports pre-existing unrelated violations.
* Queue current IME snapshots behind active flushes
Drain the pending snapshot captured by a control action after any already-active terminal send, and make accessory commit handling explicit so raw fallback is not encoded as an inverted boolean.
Constraint: Architecture review found the previous single-slot barrier could wait for an older flush while skipping newly pending Hangul text.
Rejected: Reusing the prior in-flight promise as the current flush result | It proves only an older snapshot, not the current pending buffer.
Confidence: high
Scope-risk: narrow
Directive: New mobile terminal control paths must distinguish allow-raw, handled, and suppress-raw outcomes explicitly.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; pnpm --dir mobile exec oxfmt --check changed files; no-excuse clean for terminal changed files.
Not-tested: Physical Galaxy Fold7 Samsung keyboard; full session file no-excuse audit still reports pre-existing unrelated violations.
* Preserve accessory raw-send terminal targets
Capture the terminal handle at accessory keypress time and suppress raw fallback if the active live terminal changes while waiting for pending IME flushes.
Constraint: Independent review found raw accessory bytes could retarget to a different terminal after an async IME flush barrier.
Rejected: Re-reading activeHandleRef as the send target after await | It can point at a different terminal than the keypress belonged to.
Confidence: high
Scope-risk: narrow
Directive: Raw accessory fallback must use the keypress-time target and revalidate it after any await.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; pnpm --dir mobile exec oxfmt --check changed files; no-excuse clean for terminal changed files.
Not-tested: Physical Galaxy Fold7 Samsung keyboard; full session file no-excuse audit still reports pre-existing unrelated violations.
* Document accessory flush barrier intent
Make the non-obvious raw accessory wait/suppress behavior explicit so future changes preserve IME-before-control ordering.
Constraint: CodeRabbit requested a why-comment for the send-now accessory branch.
Rejected: Leaving the barrier semantics implicit | The branch can otherwise look like unnecessary async defensive code.
Confidence: high
Scope-risk: narrow
Directive: Keep comments focused on why raw accessory bytes wait behind IME flushes.
Tested: targeted terminal vitest suite; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; oxfmt check for changed file.
Not-tested: Physical Galaxy Fold7 Samsung keyboard.
* Preserve buffered accessory raw sends
Keep the stale-handle guard focused on the captured active terminal instead of live-input opt-in state, so buffered mode keeps existing accessory key behavior while async live-input waits still cannot retarget to another terminal.
Constraint: Buffered command input behavior must remain unchanged while fixing mobile Korean IME live input ordering.
Rejected: Requiring live-input enabled handles for raw accessory fallback | suppresses valid buffered-mode accessory sends.
Confidence: high
Scope-risk: narrow
Directive: Do not use live-input opt-in state as terminal liveness for raw accessory sends; validate captured target, active terminal tab, connection, and client instead.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; oxfmt --check changed mobile terminal/session files; TypeScript no-excuse checker for changed terminal files.
Not-tested: Physical Galaxy Fold7 Samsung Keyboard manual QA and GitHub Actions jobs, blocked by unavailable device and upstream fork workflow approval.
* Keep Hangul IME text pending until explicit flush
Avoid timer-driven PTY writes for Hangul candidates so paused Korean composition cannot leak intermediate jamo, while preserving the bounded settle timer for non-Hangul IME text. Also keep disabled live-input accessory fallback behind any existing pending flush barrier.
Constraint: React Native TextInput does not expose a portable composition lifecycle on this mobile surface.
Rejected: Fixed 150ms auto-flush for Hangul | can emit ㅎ or 하 if the user pauses mid-composition.
Confidence: high
Scope-risk: narrow
Directive: Treat Hangul candidates as pending until submit/control/accessory flush; do not reintroduce idle timer commits for Hangul without device-level composition evidence.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; oxfmt --check changed mobile terminal/session files; TypeScript no-excuse checker for changed terminal files.
Not-tested: Physical Galaxy Fold7 Samsung Keyboard manual QA and GitHub Actions jobs, blocked by unavailable device and upstream fork workflow approval.
* Gate dictation toast on accepted live send
Honor the async live-input sender contract so the mobile UI reports dictation insertion only after terminal.send is accepted.
Constraint: sendLiveTerminalInput now returns false for stale, disconnected, oversized, or rejected terminal sends.
Rejected: Toasting immediately after dispatch | reports success for sends that never reached the PTY.
Confidence: high
Scope-risk: narrow
Directive: Treat live-input UI success as terminal.send acceptance, not request dispatch.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; oxfmt --check app/h/[hostId]/session/[worktreeId].tsx.
Not-tested: Physical Galaxy Fold7 Samsung Keyboard manual QA and GitHub Actions jobs, blocked by unavailable device and upstream fork workflow approval.
* Keep accessory edits on Hangul pending path
Make accessory local edits reuse the Hangul-aware defer policy so built-in Backspace/Delete cannot reintroduce timer-driven Hangul PTY writes.
Constraint: Hangul IME candidates must remain pending until explicit submit/control/accessory flush.
Rejected: Reusing the non-Hangul 150ms settle timer for accessory local edits | can leak pending Hangul after Backspace/Delete.
Confidence: high
Scope-risk: narrow
Directive: Any future pending-text reschedule must use getTerminalLiveDeferredTextDelayMs instead of a hardcoded timer.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; oxfmt --check changed mobile terminal/session files; TypeScript no-excuse checker for changed terminal files.
Not-tested: Physical Galaxy Fold7 Samsung Keyboard manual QA and GitHub Actions jobs, blocked by unavailable device and upstream fork workflow approval.
* Prove Hangul live-input hook ordering
Add a direct hook-level regression so Android Korean IME fixes are covered at the orchestration boundary, not only by lower-level helpers.
Constraint: React Native mobile TextInput lacks portable composition lifecycle events in this path.
Rejected: Relying only on helper tests | misses hook-level pending flush and submit ordering.
Confidence: high
Scope-risk: narrow
Directive: Keep Hangul candidates pending until an explicit terminal action flushes them.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; oxfmt --check changed mobile files; no-excuse on terminal modules
Not-tested: Physical Galaxy Fold Samsung Keyboard manual QA is not available in this environment.
* Keep accessory raw-send tests precise
Remove a duplicate raw-target assertion whose title implied disabled live-input behavior that is covered at the accessory commit boundary instead.
Constraint: Anti-slop cleanup must preserve existing Hangul/accessory behavior and stay within changed terminal tests.
Rejected: Keeping the duplicate disabled-input wording | it tests the same active-terminal predicate as the preceding case.
Confidence: high
Scope-risk: narrow
Directive: Test disabled live-input buffering in the accessory commit layer, not in the raw-target predicate helper.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; pnpm --dir mobile exec oxfmt --check changed mobile files; terminal no-excuse checker
Not-tested: Physical Galaxy Fold Samsung Keyboard manual QA is not available in this environment.
* Explain stale mobile terminal send gates
Document why async IME flush paths re-check terminal/client refs before sending raw bytes or reporting live-send success.
Constraint: CodeRabbit review requested short why comments for non-obvious stale-send safety gates.
Rejected: Leaving the gates undocumented | future edits could remove the stale-target suppression contract.
Confidence: high
Scope-risk: narrow
Directive: Keep async terminal sends guarded by current client, active handle, tab type, and connection state.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; pnpm --dir mobile exec oxfmt --check changed mobile files; terminal no-excuse checker
Not-tested: Physical Galaxy Fold Samsung Keyboard manual QA is not available in this environment.
* Run mobile IME hook tests through effects
Move the Hangul live-input hook regression from server rendering to react-test-renderer so effect cleanup and unmount timer cancellation are exercised.
Constraint: @testing-library/react-native imports React Native's Flow entry under this Vitest setup, so the narrow effect-running renderer is the compatible test surface.
Rejected: Keeping renderToString | it never runs useEffect cleanup and missed the pending timer cleanup path.
Rejected: Adding @testing-library/react-native directly | it failed before tests with React Native Flow syntax under the current Vitest transform.
Confidence: high
Scope-risk: narrow
Directive: Hook-level IME tests must use a renderer that runs effects when asserting pending flush cleanup.
Tested: vitest targeted terminal tests; pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; oxfmt --check changed mobile files; terminal no-excuse checker
Not-tested: Physical Galaxy Fold Samsung Keyboard manual QA is not available in this environment.
* Keep hook lifecycle tests quiet
Suppress only the react-test-renderer deprecation warning around the effect-running hook harness so real console errors still surface.
Constraint: CodeRabbit flagged React 19 renderer warning noise; @testing-library/react-native remains incompatible with the current Vitest/RN Flow transform path.
Rejected: Global console silencing | it would hide unrelated test failures.
Confidence: high
Scope-risk: narrow
Directive: Keep the renderer warning suppression scoped to this hook harness and pass all other console errors through.
Tested: vitest targeted terminal tests; pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; oxfmt --check changed mobile files; terminal no-excuse checker
Not-tested: Physical Galaxy Fold Samsung Keyboard manual QA is not available in this environment.
* fix: flush pending mobile IME input before external sends
* fix: guard terminal command finished event dispatch
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* Skip no-op state writes and bound save postponement in persistence
orca-data.json (1.6MB live) was fully rewritten (pretty-print + tmp +
rename) on every debounced save even when the state content had not
changed - measured 3 rewrites/min (~5MB/min) on an idle production
instance, and a sync-flush storm of identical multi-MB writes at every
warm start via persistPtyBinding re-binds.
- Content-hash guard in both writers: a save whose plaintext state hash
matches the last successful write skips serialize+write+rename
entirely. Hashing plaintext (not the payload) because encrypt() uses
a random IV per call. Safe under flushOrThrow's durability contract:
a matching hash means the file already holds exactly this state.
- Debounce 300ms -> 1s trailing with a 5s max-wait. The old timer reset
on every mutation with no bound, so sustained sub-interval mutation
bursts could postpone the write indefinitely; now staleness is capped
at 5s while bursts coalesce.
Co-authored-by: Orca <help@stably.ai>
* Guard the state-hash against sync-flush interleaving mid-rename
From adversarial review: an async writer that had already passed its
generation check and dispatched the rename could have a sync flush
interleave during the await, write fresher state, and record its hash -
the async continuation then clobbered lastWrittenStateHash with a value
describing content NOT on disk, making later saves (including the quit
flush) silently skip. Re-check writeGeneration before recording.
Twin edge: a hash-matching sync flush could skip its write while a
stale dispatched rename lands afterwards, leaving stale content
unchallenged. flushOrThrow now forces the sync write whenever an async
write chain was in flight at entry, restoring the sync-write-last disk
ordering.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* Refresh AI Vault session list when the window regains focus
Sessions started after the panel mounted never appeared until a manual
refresh, since the hook only scanned on mount and scope changes. Listen
for window focus and visibilitychange (to visible) while the panel is
mounted and trigger a non-force re-scan, letting the main process's 15s
scan cache rate-limit rapid focus flips. The manual refresh button keeps
its force (cache-bypassing) behavior.
Co-authored-by: Orca <help@stably.ai>
* Keep AI Vault refocus refresh render-free on cache hits
Refocus refreshes now run as background: the loading flag stays down (no
spinner flash on every alt-tab back), and when the main process replays
the cached snapshot (same scope key + scannedAt) the state updates are
skipped entirely so nothing re-renders. Fresh scans and the manual force
refresh apply results exactly as before.
Co-authored-by: Orca <help@stably.ai>
* Bypass AI Vault scan cache on panel entry and refocus, throttled
Non-force refreshes were served the 15s-old cached snapshot, so a
session started right before re-entering the panel or refocusing the
window still didn't appear — only the manual force refresh showed it.
Panel entry and refocus now request a force scan, throttled in module
scope to one forced scan per 5s (surviving panel remounts), so rapid
tab/focus flips still resolve from the main-process cache. Manual force
refreshes count against the throttle to avoid back-to-back full scans.
Co-authored-by: Orca <help@stably.ai>
* Deliver AI Vault refocus via main-process signal; calm in-app triggers
Renderer DOM focus/visibility events never fire on macOS app activation
(verified live: document.hasFocus() stays true and no focus/blur event
lands when the window loses/gains OS focus), so the refocus refresh was
inert. Main now broadcasts browser-window-focus to the renderer as
aiVault:windowFocused and the hook subscribes to that instead.
Sessions started inside Orca (window never blurs) get their own trigger
from agent hooks: re-scan only when an unseen provider session id
appears in agent status. State transitions and message pings on known
sessions are deliberately ignored — keying on them made the panel churn
on every AI message. Event-driven rescans that land inside the 5s
throttle window defer to one trailing scan instead of being dropped.
Verified end-to-end against a dev instance: a headless claude session
appeared in the panel on OS-level refocus and another on panel
re-entry, without the manual refresh button.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>