Conversation
…on (stablyai#10145) * fix(mobile): keep name input continuous during source picker transition Refactor create-workspace flow to maintain input continuity: single TextInput morphs from form slot to docked position above the keyboard while results reflow above it. Prevents field unmount/remount and keeps user focus on the input as it transitions. Add keyboard inset resolution utilities and fill-mode bottom drawer support for stable frame heights during result reflowing. * fix(mobile): keep name input continuous during source picker transition Extract drawer navigation into a custom hook and add `interactive` prop to BottomDrawer to pin the form sheet under the source picker. The form stays visible and laid out but non-interactive during source selection, revealing its original height when the picker dismisses. Fix bottom-drawer height calculation to never exceed the space above the keyboard. * fix(mobile): reset drawer state when create-workspace modal closes Prevent a queued transition timer from landing after the modal closes and leaving stale drawer/pin state for the next open. * fix(mobile): disable source field focus during drawer transitions Prevent the workspace-name field from reopening the source picker when the drawer closes. The drawer's dismiss restores focus to the field, which re-fires onFocus and reopens the drawer. Gate the field's focusability with an interactive prop so it only accepts focus when the form sheet is active.
…R loads (stablyai#10008) * fix(checks): stop the transient "PR already exists" card before the PR loads The Checks panel briefly showed the terminal "Pull request already exists" card (Open Review only) on a cold mount, and only rendered the real PR panel after a manual refresh. Root cause (regression from stablyai#9428): the eligibility probe resolves `existing_review` from cache faster than the renderable PR hydrates, and the new exhaustive selector ranks `existing_review` as a terminal safety blocker above every loading state. So during the fetch window the panel settled on the terminal card instead of a self-updating loading state. The auto-fetch also used a lazy SWR priority, so the PR landed slowly unless the user forced a foreground refresh. Fix: - Selector: while a PR fetch is queued/in-flight and we have positive-but-not- yet-renderable review evidence (existing_review or positive_unresolved), show the self-updating "Checking status" state instead of the terminal card. It flips to the review once details land. A concurrent branch blocker still owns the copy. - Fetch: when a review is known to exist but no renderable PR is cached, escalate the auto-fetch to a foreground `active`/80 request (matching manual refresh) so it resolves promptly. Reuses existing i18n keys; adds unit coverage for both parts. * fix(checks): bound unresolved review foreground refreshes * fix(types): exclude absent stat overloads * fix(checks): preserve refresh precedence and budget * fix(checks): avoid redundant foreground PR refreshes Fold unresolved-review promotion into the existing refresh effect so cached evidence produces one dispatch instead of an SWR plus active pair. Keep non-GitHub evidence on the budgeted path to avoid forced GitHub work for GitLab and other providers. * fix(checks): scope foreground refreshes by provider
…lyai#9984) * feat(diagnostics): name what grew in renderer OOM crash reports Renderer OOMs are the dominant crash class (heap pinned at the ~3.5GB V8 ceiling in crash-channel reports) but renderer_memory breadcrumbs carry only heap totals, so reports say "it grew" without saying what. Add a one-shot renderer_memory_highwater breadcrumb at 60%/80% of the heap limit carrying leak-diagnosis counts: DOM node census, terminal element count, and per-subsystem counts from a new contributor registry. The store registers the first contributor, reporting its 20 largest top-level collections. Counts only, capped per contributor; zero work on the 60s sample path while below threshold. * fix(diagnostics): retain renderer heap profiles * test(agent-status): remove subagent row order race * fix(diagnostics): bound aggregate heap profile work * fix(diagnostics): bound heap profile contributor calls * fix(types): avoid overloaded stat return inference * fix(diagnostics): fully bound heap profile registry * chore(skills): refresh release snapshot manifest * fix(diagnostics): retain browser counts in heap profiles
Improves clarity and consistency throughout mobile pairing, settings, and permission descriptions. Makes the distinction from Tailscale more explicit where relevant. Updates all translated locales.
* fix(mobile): show attached images in native (rich) chat Attaching an image in the mobile native chat did nothing visible — it reused the terminal attach flow, which pastes a bracketed host path into the hidden terminal, so there was no composer preview and nothing in the transcript. Give native chat the desktop model instead: - pick + upload shows a removable thumbnail chip in the composer (no early paste) - on submit, images ride along: Ctrl+U clear -> bracketed paste(s) -> settle -> text + Enter (idempotent on retry) - the optimistic echo carries the local preview URIs and the message renderer draws image-ref blocks as real thumbnails when the URI is loadable, so the sent photo appears in the conversation immediately - image-only echoes reconcile by ordinal against user turns after their tail (ignores agent replies / paginated history / the 'unknown' ack-loss path) Terminal chat attach is unchanged (both flows consolidated behind useMobileSessionImageAttachments). Adds unit coverage for pick+upload, the ride-along byte order, chip render/remove, and echo reconciliation. * test(mobile): interactive native-chat image proof (real hooks, click-driven) Replace the hand-fed component render with an interactive harness that mounts the real MobileNativeChatComposer/Message + useMobileNativeChatImageAttachments + drafts under react-native-web and drives the actual flow via clicks. Only the two OS boundaries are faked: the photo picker and the paired-host RPC socket. Screenshots (mobile/docs/native-chat-image-attachment/) are produced by real clicks, not props: - attach -> real upload pipeline -> chip appears, nothing pasted yet - send -> real ride-along emits Ctrl+U clear, bracketed image paste, text+Enter (shown in the live byte trace) and the sent bubble renders the photo thumbnail * fix(mobile): scope native-chat image attachments by active tab Images are now scoped to the tab that initiated the pick, so switching tabs during upload cannot ride an image into another terminal. Chips stay with their original tab, and only the active scope's images send with text. Improved error handling with user-facing toast messages for disconnection and send failures. * test(mobile): add image attachment tab-scoping and error tests Add comprehensive test coverage for tab-scoped attachment behavior, error handling when transport fails or lease is gated, and edge cases like attaching images during an in-flight send. Extract baseArgs and update helpers to reduce boilerplate across test cases. * fix(mobile): show attached images in native rich chat Images attached in the mobile native (rich) chat now display as: - Removable composer chips while composing - Thumbnails in the sent user bubble after sending (desktop parity) Implements proper image echo reconciliation by distinguishing image-source marker turns from text echoes, so an image send isn't cleared by an unrelated text echo. Adds scope isolation to prevent chips and drafts from leaking between tabs, and detects tab switches during the image-paste settle window to abort the send. Fixes Android tap-target positioning for the image removal badge and clears stale terminal input after failed pastes to avoid gluing fragments onto the next message. * rm stubs --------- Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
…tablyai#10215) * feat(source-control): show current branch without evicting Create PR stablyai#9787 added the current-branch identity to the Source Control header but did it by replacing the Create PR button's toolbar slot, so stablyai#10032 reverted the whole thing. Create PR is the primary entry point into the stage→commit→push→generate-PR flow, so it can't be traded away. Restore the branch identity as its own row above the toolbar so it coexists with the Create PR button (Option 2 layout). Detached HEAD renders in the same identity row via DetachedHeadBadge (re-adds its tabIndex/aria-label), replacing the separate below-toolbar badge row. * style(source-control): match branch identity text to the 'vs main' base ref Same font-mono / 10.5px / foreground-90 / underline treatment so the current branch name reads visually consistent with the base ref in the context row. * style(source-control): drop branch identity underline Keeps the 'vs main' font/size/color match but no underline — the label isn't clickable, so the underline read as a false affordance. * test(source-control): harden identity-row detached + no-identity contracts Add a stable data-testid to the identity row so the no-identity case proves no row renders, and assert the detached badge's accessible label + focusability. Addresses CodeRabbit review on stablyai#10215.
…resh (stablyai#10225) Switching, adding, re-authing, or removing a Codex account awaited refreshForCodexAccountChange before resolving the IPC call. Since the per-account CODEX_HOME rollout (stablyai#9501) that probe runs against a cold home (10s RPC + 15s PTY fallback, 25s WSL) and can queue behind an in-flight global usage fetch, so the switcher sat unresponsive for tens of seconds and a fresh login looked stuck on the loading screen. Worse, in addAccount the awaited refresh sat inside the login cleanup try/catch: a refresh rejection after the account was durably committed deleted the just-created managed home, leaving a registered account with no home ("account never connects"). Run the refresh as best-effort background work instead. Its synchronous prefix still flips usage to "fetching" before the first await, so the switcher updates instantly and usage fills in via the normal rate-limit state pushes; a probe failure is logged and can never trigger managed-home cleanup. Fixes stablyai#10141
Co-authored-by: kazu-42 <91968271+kazu-42@users.noreply.github.com>
…eed (stablyai#9387) * fix(status-bar): seed Resource Manager closed badge from daemon inventory The closed chip counted tab/layout PTY wake hints (inflating terminal count) and never fetched memory until the popover opened (showing "—"). Cache listSessions for the badge, seed memory on session ready, and drop the wake-hint closed selector. * fix(status-bar): update session inventory ref in an effect CodeRabbit/React Doctor flagged mutating sessionInventoryRef during render; keep the write in useEffect after commit. * fix(status-bar): follow daemon session lifecycle events * perf(status-bar): skip inventory scans for known PTYs * perf(status-bar): bound resource inventory refreshes --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
…ablyai#10213) * Reapply "Preserve Codex [tui] settings across managed CODEX_HOME remirrors (stablyai#9475)" (stablyai#10085) This reverts commit 09756df. * fix(codex): anchor settings baseline migration
…#9446) * perf(startup): defer full worktree scan until after session restore * fix(startup): normalize worktree recovery session keys * fix(startup): preserve persisted workspace hydration * fix(startup): keep SSH repos in pre-hydration worktree fetch Round 1 review fixes for the selective pre-hydration fetch: - The prefetch filter gated on LOCAL_EXECUTION_HOST_ID, which excluded SSH repos. Old full-scan code populated worktreesByRepo for disconnected SSH repos via the local metadata fallback (no network), so hydrateTabsSession / editor / browser preserved their chrome. Excluding SSH dropped that chrome at hydration and setHydrationSucceeded made the loss durable. Include SSH by excluding only runtime-owned repos (which hydrate via placeholders instead). - collectWorktreeHydrationRepoIdsFromSession never collected activeRepoId, so a repo referenced only by active-repo state (no active worktree, no tabs) was not prefetched, breaking hydrateWorkspaceSession's main-worktree fallback. * fix(startup): re-prune visit timestamps after deferred full worktree scan Round 2 review fix. The selective pre-hydration fetch only populates session-referenced repos, so the one-shot pruneLastVisitedTimestamps at startup no longer sees non-session repos and keeps their deleted-worktree entries (the prune defers unloaded repos to preserve SSH focus recency). The old full pre-hydration scan had populated all local repos before the prune. Re-run the prune after the deferred fetchAllWorktrees so stale lastVisitedAtByWorktreeId entries are dropped once every repo is authoritative; disconnected SSH stays non-authoritative and is kept. * fix(startup): exclude unbounded history maps from hydration repo enumeration Round 3 review fix. collectWorktreeHydrationRepoIdsFromSession collected repo ids from lastVisitedAtByWorktreeId and defaultTerminalTabsAppliedByWorktreeId, which accumulate one entry per worktree ever focused / given default tabs. On the long-lived large profile this fix targets (stablyai#9441), those maps reference nearly every repo, so the 'selective' pre-hydration fetch degenerated back toward the full all-repos scan the fix removed. Split the keyed-field list: repo enumeration now scans only chrome-bearing fields (open tabs/editors/browser), while folder-key detection keeps scanning all fields. The history maps hydrate unfiltered regardless, and the deferred re-prune reaps their stale entries, so no data or recency is lost. Also pin the round-2 re-prune ordering in the routing test. * fix(startup): prevent competing full worktree scans * fix(startup): keep selective hydration scans bounded * fix(startup): ignore stale tab selection markers * fix(startup): keep deferred worktree recovery selective * test(windows): dismiss startup overlays before terminal setup --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
…p scan degrades (stablyai#9991) * fix(session): preserve chrome after degraded repo scan * fix(session): retain terminal-free degraded workspaces
…ain process (stablyai#9990) * fix(terminal): bound cold restore replay * fix(terminal): bound cold restore replay slices * perf(terminal): avoid rescanning cold restore payloads
) * fix(editor): preserve PDF zoom when file content reloads PdfViewer rebuilt the pdf.js document on every content change and always reset to page-width, so external PDF updates wiped the user's zoom. Keep a fit-width vs absolute preference across reloads for the same file path. Closes stablyai#10165 * refactor(editor): dedupe PDF zoom handlers into shared callbacks Collapse six copies of the step-scale-and-record-preference logic (three directions duplicated across the keyboard handler and toolbar callbacks) into a single stepZoom callback; the keydown handler now reuses zoomIn/ zoomOut/zoomReset so keyboard and toolbar can't drift apart. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Co-authored-by: Orca <help@stably.ai>
…of a bare shell (stablyai#9454) (stablyai#10238) * fix(agent-status): resume finished agents after macOS logout instead of a bare shell (stablyai#9454) After the daemon-retirement fix (stablyai#9826) recovers terminals across a macOS logout, a running Claude Code (or other resumable TUI agent) that had finished its turn came back as a BARE SHELL — the user had to open a new tab and `claude --resume` in every worktree to get logged back in. Root cause: setAgentStatus writes a persisted 'live recovery' sleeping record (the cold-restore --resume anchor) for a live resumable agent, but drops it once the turn is 'done' — except for Pi, which was special-cased (retainsPiRecoveryIdentity). A finished Claude/Codex turn leaves the TUI alive and resumable at its prompt just like Pi, so an idle agent lost its anchor; a cold restore after an abrupt app death (logout) then found nothing to resume and dropped to a bare shell. Fix: generalize the Pi-only recovery-identity retention to every resumable TUI agent (isResumableTuiAgent), gated on a valid resume argv, so a finished agent keeps its persisted providerSession anchor and the already-shipped cold-restore --resume path brings it back logged in. Extends an existing pattern; no new subsystem. Reproduced + validated live in Electron via the login-session-death seam: idle Claude -> daemon retire -> app relaunch previously bare-shelled; now spawns 'claude --resume <session>' and restores the conversation logged in. * test(agent-status): update sleep-hygiene assertions for retained live recovery anchor (stablyai#9454)
…names (stablyai#9989) * feat(agent-rows): opt-in conversation-name labels for agent rows Sidebar worktree-card and dashboard agent rows always show the last message sent to the agent, so rows relabel on every turn and a 'continue' prompt becomes the row's name. Add an opt-in Agents setting that labels rows with the conversation name instead, resolved with the tab bar's precedence: manual rename, quick-command label, OpenCode session title, generated title (behind its existing setting), then the agent-set live title. Live titles count only when they carry a real name - status decoration is stripped, and pure status, identity-echo, spinner+cwd, and placeholder titles fall back to the last-message label. Subagent child rows keep their own descriptions. Locale note: sync:localization-catalog also restored parity for keys already missing on main (add-host, sleep-worktree copy). * fix(agent-rows): read the live tab so late renames and titles surface Row data patches live entries in place and keeps the tab snapshot from row creation, so a rename or agent-set title landing after the row was built never reached the conversation-name resolver. Select the current tab from the store in the hook; retained rows without a live tab keep the snapshot fallback. * fix(agent-rows): reject status labels and bound lookup work Reject native provider/status titles and Windows/UNC cwd frames so conversation-name mode falls back to the user's last message instead of relabeling rows with identity or path text. Keep default-off and subagent rows off the tab map, and share a WeakMap-backed tab index across mounted rows to avoid repeated linear scans on store writes. * feat(agent-rows): make conversation names the default Always prefer a usable conversation name for sidebar and dashboard agent rows, falling back to the last message when the resolver rejects a title. Remove the preference, settings UI, search copy, and catalog entries. Keep subagent labels unchanged and reject generic Terminal N placeholders so partial tab snapshots cannot hide meaningful prompts. * chore(i18n): sync catalog after main merge * fix(agent-rows): preserve same-tab child labels
…lyai#10242) Portal the non-blocking reconnect banner into each pane container so WebGL compositing cannot paint terminal text through it, raise z-index above pane-local chrome, and use a fully opaque card background.
…awn (stablyai#10237) * fix(claude-accounts): quote resolved claude path for Windows shell spawn runClaudeCommand spawns the resolved claude command with shell:true on Windows, but spawn concatenates the command into the cmd.exe line without quoting. When the CLI resolves to a path containing spaces (e.g. C:\Users\First Last\AppData\Roaming\npm\claude.cmd), cmd.exe splits at the first space and account add fails with: 'C:\Users\First' is not recognized as an internal or external command Quote the command the same way claude-pty.ts and quoteWindowsCmdArg already do for other Windows spawns. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(claude-accounts): own Windows cmd invocation --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
…nded freeze (stablyai#9447) (stablyai#9853) * fix(app): bound the wake/quit paths implicated in the phone-session-ended freeze (stablyai#9447) - relay-transport: waitForClose now times out (5s) so a half-open post-sleep socket can't wedge runtimeRpc.stop() - will-quit: race teardown against a 20s deadline so app.quit() always runs (Force Quit was the only escape when any teardown member never settled) - terminal-fit-restore: local restoreTerminalFit invoke gets the same 15s bound as the remote path so the held-fit modal buttons can't pin disabled * fix(app): close wake recovery timeout gaps * fix(relay): drop late frames after forced teardown * fix(relay): fence detached socket callbacks * fix(app): close timeout resource gaps * fix(relay): detach retired mobile transports * fix(types): exclude absent stat overloads * fix(runtime): expire wedged terminal restore dedupe * fix(runtime): keep restore retries on one reclaim * chore(skills): refresh bundled skill manifests * fix(window): fence quit acknowledgements by request * fix(relay): bound revoked device socket cleanup
…tablyai#10240) * fix(worktree): stop terminal removal fence error flashing on delete Deleting a worktree kills its PTYs for the filesystem teardown, then runs git worktree remove (~1s). During that window a doomed pane races a fresh respawn that main correctly fences with TerminalRemovalInProgressError, but the renderer surfaced that internal fence verbatim as a pane error banner until the worktree unmounted. - startFreshSpawn: skip the respawn when the pane's own worktree is being deleted (isDeleting) — no shell should spawn into a directory being removed and the pane is about to unmount. - reportError: swallow the removal fence at the single pane-error sink so it never reaches the banner, covering the parent-removal-fences-child case the own-worktree skip cannot see. - Share the fence messages between main (thrown) and renderer (recognized) via worktree-removal-fence-error.ts so the thrown text and predicate can't drift. * test(worktree): assert onError callback captured before invoking Optional invocation let the fence-suppression test false-pass if the transport onError wiring broke; require the callback so a broken wire fails loudly. Addresses CodeRabbit review on stablyai#10240.
…urvival harness (stablyai#9992) * fix(win-e2e): survive the agent-CLI feature-wall modal in harness driving The modal renders off an async capability check and can be up at (or appear right after) the harness's first interaction, swallowing every click until closed — both crash-survival attempts on stablyai#9446 timed out exactly there, while the app itself was healthy. Add its 'Close' button to the overlay dismiss list, dismiss overlays before ensureTerminal's first probe, and retry the first New-workspace click through a dismissal pass instead of hanging the full 60s. * fix(win-e2e): scope modal dismissal and bound retries * fix(win-e2e): keep overlay retries inside composer * fix(win-e2e): seed blank terminal selection * fix(win-e2e): avoid input during terminal restore * fix(win-e2e): bound whole create path by caller timeout createWorkspaceFromSeededRepo gave the composer wait and create-button retry fresh fixed 15s windows, so the create path could overrun the caller's timeoutMs. Thread one shared deadline through every step and cover a fast first click followed by a blocked submission.
…ablyai#10253) The show/restore repaint jiggle mutated the window frame synchronously inside AppKit's window-state dispatch. On macOS 26 (Tahoe), windows are scene-backed and a frame update sent re-entrantly from scene-update handling can self-deadlock the main thread in FrontBoardServices (kernel-confirmed spindump: performAsyncAndWait on the callout queue the main thread was draining). - Defer the setSize nudge to a fresh event-loop turn so it never runs inside the show/restore dispatch. - On Darwin 25+ (macOS 26) skip the nudge entirely and rely on webContents.invalidate(); the nudge works around a pre-Tahoe black-surface compositor bug.
…ring startup restore (stablyai#9970) * fix(main): log unhandled rejections durably and stop them killing the app (stablyai#9441) Main had no unhandledRejection handler — Node's default killed the process with a clean exit code and no macOS crash report, which is exactly the silent startup-restore exit in stablyai#9441. Rejections now record a durable crash breadcrumb (flushed to the trace sink) and the app stays alive; uncaught exceptions still terminate but record the same breadcrumb first so field exits stop being undiagnosable. * fix(main): harden process error guard diagnostics * fix(main): cap fatal-error records to stop a rejection storm stalling main One broken resource can reject hundreds of concurrent restore chains, and each durable record does a synchronous trace flush. Cap records at 20 per 60s window and carry the suppressed count into the next recorded event. * fix(main): never rate-limit the fatal uncaught-exception breadcrumb A rejection storm that exhausted the 60s record window would suppress the one main_uncaught_exception record taken right before the re-throw kills the process, recreating the undiagnosable death from issue stablyai#9441. Scope the fixed window to main_unhandled_rejection only; the uncaught path records at most once per install (handler removes itself, EPIPE/EIO never record), so it cannot storm. * fix(main): reset fatal-error window on backward clock jumps
…yai#10086) * feat(editor): toggle Word Wrap from file tab actions and Alt+Z Long single-line and structured files wrap by default and misalign. Surface Word Wrap on the editor more-actions menu for normal file tabs (diff already had it) and add editor.toggleWordWrap (Alt+Z) so users can unwrap without opening Settings. Closes stablyai#9974 * fix(editor): toggle diffWordWrap for diff surfaces on Alt+Z CodeRabbit: Alt+Z previously always flipped editorWordWrap, leaving diff panes out of sync with the markdown actions menu. * test(editor): verify word wrap shortcut routing Cover editor/diff setting callbacks and the cross-platform Alt+Z binding. --------- Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Expand repository icon auto-detection to conventional Tauri and public/icon paths with PNG/WebP magic and dimension validation. Bound SSH probing while preserving candidate priority and PNG-only user uploads; SVG remains rejected.
… / Right) and complete the desktop tab context menus (stablyai#9323) * Add Close Tabs to the Left and complete Close Others across tab menus and mobile long-press sheets * Fold the per-sheet Close action into the bulk-close module (session route max-lines) * fix(mobile): preserve pinned tabs during bulk close --------- Co-authored-by: Tom de Bres <tomdebres@users.noreply.github.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
…yai#9773) * feat: implement Cmd+Enter as commit shortcut in Source Control * test: add unit tests for commit shortcut and tooltip formatting * fix: address review feedback on modifier keys and test coverage * test: split mac and windows/linux shortcut and keydown tests --------- Co-authored-by: Andres Van Reepingen <andres.vanreepingen@datacamp.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
…ai#7436) * Add SenseVoice speech-to-text model (Korean/Japanese support) SenseVoice (zh/en/ja/ko/yue) is the only bundled local STT model with Korean and Japanese support. The existing local models cover only English and Chinese (Parakeet, Zipformer, Paraformer); Whisper Tiny is multilingual but trades accuracy for breadth. - Add 'senseVoice' to SpeechModelType - Register the sherpa-onnx SenseVoice archive in the model catalog (pinned SHA-256, single-file model.int8.onnx + tokens.txt layout) - Handle the senseVoice type in the STT worker via createOfflineRecognizer with the senseVoice model config (auto language detection + ITN) - Add model-catalog regression tests for the new entry Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(speech): use int8-only SenseVoice archive * fix(speech): refresh SenseVoice catalog metadata --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: LauraGPT <LauraGPT@users.noreply.github.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
… owns provider-account scope (stablyai#8188) Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
* Add SenseVoice speech-to-text model (Korean/Japanese support) SenseVoice (zh/en/ja/ko/yue) is the only bundled local STT model with Korean and Japanese support. The existing local models cover only English and Chinese (Parakeet, Zipformer, Paraformer); Whisper Tiny is multilingual but trades accuracy for breadth. - Add 'senseVoice' to SpeechModelType - Register the sherpa-onnx SenseVoice archive in the model catalog (pinned SHA-256, single-file model.int8.onnx + tokens.txt layout) - Handle the senseVoice type in the STT worker via createOfflineRecognizer with the senseVoice model config (auto language detection + ITN) - Add model-catalog regression tests for the new entry Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(speech): add Parakeet TDT-CTC 0.6B JA to the speech model catalog * test(speech): cover stt-worker-model-config file resolution incl. single-file models * feat(speech): decode Parakeet TDT-CTC JA via sherpa-onnx nemoCtc offline recognizer * fix(speech): use int8-only SenseVoice archive * fix(speech): refresh SenseVoice catalog metadata --------- Co-authored-by: xsacdw <xsacdw@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: LauraGPT <LauraGPT@users.noreply.github.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
* fix(terminal): stop bold flash on worktree switch Worktree hide disposes WebGL and falls back to xterm's DOM renderer. On reveal, resume ran after paint and flushed backlog against DOM first, so the first frame used heavier CSS-AA glyphs before WebGL settled. Resume in useLayoutEffect and reattach WebGL before backlog flush so the first painted frame stays on the GPU path. No cold-park policy change. Co-authored-by: Orca <help@stably.ai> * fix(terminal): fit WebGL grid before backlog flush on resume Adversarial review: resume-before-flush alone wrote TUI backlog onto the transient DOM↔WebGL one-column-off metrics window. Order is now resume → fitAllRevealedPanes → flush on heavy reveal and window wake. Co-authored-by: Orca <help@stably.ai> * fix(terminal): latch viewport intent before WebGL wake resume Adversarial review: wake path synced intents after resume/fit, which can re-latch a pinned viewport as followOutput. Capture before reattach and drop the post-resume re-sync on heavy reveal (outer path already latched). Co-authored-by: Orca <help@stably.ai> * fix(terminal): complete visibility bookkeeping before PaneManager exists useLayoutEffect runs before the passive lifecycle creates PaneManager, so the mount-visible path never set hasCompletedVisibleResume. The first intra-worktree hide then wrongly suspended WebGL. Bookkeep completion even when managerRef is still null (extracted helper for max-lines). Co-authored-by: Orca <help@stably.ai> * fix(terminal): re-sync pin geometry after resume backlog flush Keep the pre-resume intent latch (reattach must not re-latch pins as followOutput), then re-sync after flush with preservePinnedAtBottom so scrollback trim updates absolute pin lines before enforce. Co-authored-by: Orca <help@stably.ai> * fix(terminal): drop same-tick post-flush intent re-sync flushTerminalOutput only queues terminal.write and returns before parse, so a same-tick re-sync read pre-parse resume/fit geometry and could overwrite pre-resume pins. Keep pre-resume latch + enforce only. Co-authored-by: Orca <help@stably.ai> * fix(test): expect default worktree card properties to include cli stablyai#10712 added 'cli' to DEFAULT_WORKTREE_CARD_PROPERTIES, but the fresh default-profile assertion still omitted it and fails verify. Co-authored-by: Orca <help@stably.ai> * perf(terminal): retain Windows WebGL across worktree hides * perf(terminal): bound retained WebGL contexts * fix(terminal): harden retained WebGL lifecycle * fix(terminal): preserve healthy WebGL on wake * fix(terminal): preserve reveal recovery ordering --------- Co-authored-by: Orca <help@stably.ai>
…tablyai#10226) * fix(mobile): clear native-chat composer optimistically at send time Over relay the send RPC round trip is visible and a lost ack (or a relay/direct cutover) could strand the sent prompt in the composer forever: the unconfirmed-send deadline dropped its tracking entry, so a late transcript echo could never clear the draft. Clear the draft at send time and restore it only on a definite rejection. holdUnconfirmedSend now only manages the delivery-unconfirmed notice; it no longer touches drafts. * fix(mobile): isolate question answers from composer drafts
* fix(cli): bound orchestration ask timeouts * fix(cli): harden remote timeout boundaries
…h trail (stablyai#10729) * fix(crash-reporting): stop fit-retry bursts from erasing the pre-crash trail Windows renderer OOM F0BKR84AHEH (0xE0000008) arrived with a 30-entry breadcrumb ring in which two `terminal_safe_fit_retry_exhausted` bursts consumed 26-90% of the slots. Every hidden pane is `display:none` -> 0x0 -> unmeasurable, so one post-reload reattach wave exhausts the retry budget once per mounted pane inside ~60ms. The bursts were also uninterpretable: `pane.id` restarts at 1 per PaneManager and there is one manager per tab, so 34 identical `paneId: 1` crumbs cannot distinguish one pane looping from 34 panes firing once. Coalesce the crumb by name and carry the live-pane census on the payload instead, so the count survives without costing 34 ring slots. Same treatment for WebGL diagnostics, which were worse off: context-loss and atlas-reset crumbs only reached a DevTools-only ring (`window.n()`), so a renderer that dies takes them with it. That bundle had three GPU-process deaths in the 65s before the renderer OOM and zero WebGL evidence - absence of instrumentation, not absence of the event. Mirror them into the crash report, coalesced per kind so a routine atlas reset cannot mask a context loss. Evidence-only: no behavior, rendering, or lifecycle path changes. Co-authored-by: Orca <help@stably.ai> * perf(pane-manager): count panes without materializing public views The census runs on the crash path; getPanes() allocates a full ManagedPane projection per pane just to read .length. Co-authored-by: Orca <help@stably.ai> * test(crash-reporting): pin the fit-retry burst against the 30-entry ring Reproduces the F0BKR84AHEH ring loss directly: 10 pre-crash crumbs plus a 34-crumb per-pane burst. Uncoalesced, the burst takes all 30 slots and zero pre-crash crumbs survive; coalesced, it takes one slot, all 10 survive, and the pane count rides on the payload instead of on the crumb multiplicity. Co-authored-by: Orca <help@stably.ai> * fix(crash-reporting): name the WebGL census the same as the fit-retry census The context-loss crumb spread getLivePaneCensus() raw, so one ring described one measurement two ways: managers/panes here, livePanes/livePaneManagers on the fit crumb. Spreading also meant renaming the census return keys would silently reshape the crumb. Name the fields at the call site and pin them. Co-authored-by: Orca <help@stably.ai> * fix(crash-reporting): keep a hot coalesce key from being the first LRU eviction The suppression path returned before the delete-then-set that re-anchors recency, so a key hit continuously never moved from its original insertion slot and became the first eviction candidate — the inverse of the LRU's stated intent. `renderer_error` keys carry message+stack identity, so one noisy render loop mints unbounded distinct keys. Within a single 30s window that churn evicted the `terminal_safe_fit_retry_exhausted` key mid-burst, un-suppressing it and re-arming the exact ring flush the coalescing exists to prevent. Re-anchor position only; `recordedAt` is left alone so the suppression window still expires on schedule rather than renewing on every hit. Found while adversarially probing the LRU claim in stablyai#10729's own description, which asserted these keys "cannot evict live keys". * fix(crash-reporting): report the newest census of a coalesced burst The suppression path wrote nothing to the ring, so a coalesced burst froze its FIRST event. Panes mount progressively, so pane 1 exhausting alone legitimately measures livePanes: 1 -- and the 33 later crumbs, each carrying a truer census, were dropped. A 34-pane wave was recorded as `livePanes: 1` with no count: the exact "one pane looping" misread that coalescing by name was introduced to prevent. The existing burst test missed this because it fed a constant census on every crumb, making frozen-first and newest-wins indistinguishable. Stash the newest payload and fold it into the ring entry the key already owns: still one slot, now reading livePanes: 34 + suppressedSinceLast: 33. Resolution is deferred to snapshot time -- sanitizing per suppressed hit of a 1459/min crash loop measured 2194 ns/op vs 185 ns/op deferred. Two follow-on defects fixed alongside: an expiring key dropped its pending payload (it loses its only handle on the ring entry), and resolving the re-emitting key's own old slot double-counted a burst. --------- Co-authored-by: Orca <help@stably.ai>
…i#10743) Co-authored-by: Orca <help@stably.ai>
…stablyai#10707) * fix(gpu-fallback): make the crash window rolling, not launch-anchored Software-rendering fallback only ever considered GPU child crashes in the first 30s after launch: `if (msSinceLaunch > this.windowMs) return`. Session 12e6ee64 crashed the GPU child 4 times (242s / 920s / 926s / 946s since launch). The last three span 26.0s — inside windowMs, exactly threshold — but every one was rejected because the burst began 920s in. The renderer died of process OOM (0xE0000008) 39s later. GPU work is demand-driven, so the first heavy compositing often happens minutes into a session. What distinguishes a broken driver from normal Chromium churn is that the crashes *cluster*, not when the cluster starts. Keep a sorted array of recent crash times pruned to windowMs behind the newest, and engage when the count reaches threshold. Measured against real field telemetry (341 distinct win32 launches with >=1 GPU crash, from process_gone_suppressed breadcrumb trails): the rolling window engages on 2/341 launches (0.59%), one of which is 12e6ee64. Max GPU crashes in any single launch is 4. The closest non-firing sequence ([0, 29531, 55136, 74178] — consecutive gaps that each fit the window but never put 3 inside it) is pinned as a regression test. Also destroy the Windows tray before app.exit(0) on this path, matching the app:relaunch IPC handler — app.exit skips before-quit, and this can now fire deep into a session rather than only in the first 30s. Mutation-tested: 6 mutants (launch-anchored gate, dropped pruning, dropped monotonic clamp, cutoff </<=, threshold >=/>, dropped engaged latch), all killed by the suite. Co-authored-by: Orca <help@stably.ai> * fix(gpu-fallback): ask before restarting --------- Co-authored-by: Orca <help@stably.ai>
…i#10786) Adds "Hide detached HEAD" alongside the existing sidebar filters, wired through the same pipeline as Hide CLI-created: sidebar list, Cmd+J empty-query list, workspace board, active-filter badges, Clear/Reset Filters, and persisted UI state. The predicate reuses getWorktreeGitIdentityDisplay so the filter targets exactly what the card renders a Detached HEAD badge for. Requiring a real head (not just an empty branch) keeps folder workspaces and SSH-synthesized rows — which carry both empty — out of the filter. Activating a hidden detached workspace clears the filter, matching the existing reveal escape hatch for automation- and CLI-created workspaces. Splits the filter-state describes out of visible-worktrees.test.ts into sidebar-filter-state.test.ts to stay under the max-lines budget. Co-authored-by: Orca <help@stably.ai>
…tablyai#10058) * feat(daemon): add daemon_lifecycle replaced/retired telemetry event Implements STA-2376. Adds track('daemon_lifecycle', {transition, reason, live_session_count_bucket, version_skew?}) covering 'replaced' (unhealthy_resolver / stale_bundle / different_app_path / failed_health_check at daemon-init launcher sites) and 'retired' (died_respawn at the adapter respawn closures). Enum-only + .strict() + bucketed counts keep paths, versions, and raw counts off the wire; preserve-path transitions emit nothing. Cross-platform and SSH-safe; no-op in non-official builds. Test plan: affected vitest (158) green; typecheck/lint clean except pre-existing unrelated failures. * fix(daemon): prevent false lifecycle telemetry * test(daemon): restore once-ness on respawn reason assertions Keep STA-2376 reason checks without dropping concurrent-respawn coalescing coverage that prevents double died_respawn telemetry. * fix(daemon): emit replaced telemetry on runtime unhealthy_resolver respawn CodeRabbit: adapter-driven macOS resolver replacements forked a new daemon without a lifecycle event. Emit trackDaemonReplaced (not retired) so field diagnosis of stablyai#7936 covers the runtime path without mislabeling it as death. * fix(daemon): stop double-counting resolver replaces; drop redundant version_skew Three telemetry-correctness fixes to the STA-2376 daemon_lifecycle event. 1. The runtime macOS resolver respawn double-counted. doRespawn() disconnects but never kills the daemon, so the ensureRunning() that follows re-enters createOutOfProcessLauncher, which re-detects healthy + resolver-unhealthy + 0 sessions and emits the replace itself. The closure emitted a second one. It also emitted before the outcome was known, so a resolver that recovered mid-flight (or a session appearing) left a 'replaced' on the wire for a daemon the launcher went on to preserve. The launcher's emit is gated on a confirmed kill, so it is the correct sole emitter; this reverts the emit added in 1e60ca8. The reason plumbing stays -- it is what keeps a resolver respawn from being mislabelled died_respawn. 2. version_skew carried no information and lied to one cohort. It was present iff reason === 'stale_bundle' and always true, so it was a deterministic function of reason. isDaemonStaleForCurrentBundle also returns true when the pid file has appVersion: null -- a replace-once heuristic for pre-marker builds, where no version comparison happened at all -- so the field asserted skew for exactly the upgrade cohort the event exists to illuminate. Dropped from the schema, emitter, and call site, along with the dead branching. 3. track() is now failure-isolated in both emitters. Both call sites sit on the daemon launch/respawn path, where a throw costs the user every terminal. Tests: once-ness (toHaveBeenCalledTimes) on every emit assertion -- the old toHaveBeenCalledWith-only assertions passed under a doubled call; a regression guard that the resolver respawn closure stays silent; and a throwing-client test. Note the unit tests mock DaemonSpawner and never invoke the launcher, so no test could observe the double-emit; the once-ness assertions bound each emitter within its own seam. Known limitation, unchanged: a wedged-but-alive daemon (stablyai#8689) can still report died_respawn from the adapter and failed_health_check from the launcher -- the app cannot distinguish wedged from dead at that point. * fix(daemon): attribute the runtime resolver replace so it is not lost Round-2 review found the previous commit over-corrected. Removing the emit from the respawn closures was right about the premature emit but wrong about where the event would come from instead. doRespawn() does not kill the daemon, but it does drop its only authenticated client, and that is enough: the last fully-authenticated disconnect sets retirementRequested, and reevaluateIdleShutdown -> beginIdleShutdown runs with no grace timer, unlinking the token and PID files. So by the time ensureRunning() re-enters the launcher, the daemon is already gone -- killStaleDaemon finds no PID file, confirmedReplacement stays false, and the gate suppresses the emit. Net effect of the previous commit: zero events for a runtime macOS resolver replacement, the common case. The double-emit round 1 found was real but narrow: it needs a daemon holding non-alive sessions, which keeps host.listSessions() non-empty so isIdle() is false and the daemon survives the disconnect to be killed by the launcher. Fix: the adapter attributes the reason rather than emitting it, and the launch it triggers consumes the attribution and reports it. One emit point, exactly one event, correct reason -- whether the daemon self-retired or survived to be killed. The attribution is one-shot so a later unrelated launch cannot inherit it, and it is preferred over the launcher's own inference, which would otherwise mislabel this as failed_health_check. Also: suppress died_respawn while a manual restart is in flight. runRestartDaemon kills the daemon while the outgoing adapter is still live and undisposed, so a pane respawning on its synthetic exit billed a user action to the crash bucket. Tests: a regression test that drives the closure and the launcher across the seam the DaemonSpawner mock normally hides, with killStaleDaemon returning false to model the self-retired daemon, plus the one-shot assertion. Verified load-bearing by mutation (neutering the handoff fails it). Also reset killStaleDaemonMock's implementation in beforeEach -- mockClear left a previous test's mockResolvedValue in place, which silently disarmed the confirmedReplacement gate for every test after it. * fix(daemon): let a proven replacement reason outrank the attributed one Round-3 review. The attribution was preferred unconditionally, so a launch that independently proved a different cause reported the wrong one: resolver unhealthy at the adapter check, daemon survives the client drop (non-alive sessions keep isIdle() false), resolver recovers milliseconds later, and the launcher then proves a stale bundle or a different app path and kills on that basis -- but the event said unhealthy_resolver. A proven kill is grounded in post-kill truth, so it now wins; the attribution covers only what the gate cannot see, a daemon that self-retired leaving nothing to kill and no reason to infer. Every other case is unchanged: self-retired still reports unhealthy_resolver, and the surviving-daemon case reaches the same reason through the launcher's own inference. Also pins the invariant that makes the module-scoped one-shot safe -- the write reaches the launcher with no await in between -- at both the write and the consume, since the DaemonSpawner mock lets tests drive the two halves with an arbitrary gap and would not catch an await being introduced. Corrects the beforeEach comment from the previous commit: there was never a plain mockResolvedValue on killStaleDaemonMock in this file, so it did not silently disarm the gate for later tests. mockReset is still right -- it drops an unconsumed *Once queue, which mockClear does not. Tests: a guard that the launcher's proven reason wins over the attribution, verified load-bearing by mutation (restoring the old ordering fails it). * fix(daemon): don't let the residual health bucket absorb the resolver reason Round-4 review caught a regression from the previous commit. Letting any confirmed kill outrank the attribution was too broad: failed_health_check is not an identification, it is the residual bucket that also absorbs wedges and crashes, so preferring it discards the more specific reason the adapter already established. This is the likely shape of the incident, not a corner case. The dead macOS login session that fails the resolver probe also fails the PTY spawn probe, so checkDaemonHealth returns pty-spawn-unhealthy rather than healthy. The resolver branch is then never evaluated, and with zero live sessions the degraded preserve does not apply either, so the launch falls through to failed_health_check and kills a daemon that survived the adapter's disconnect (non-alive sessions keep it non-idle). Before this commit that reported failed_health_check and dropped unhealthy_resolver -- burying the flagship signal in the catch-all. Rank by how well each reason is evidenced instead: a confirmed kill that positively identified the daemon wins, the attribution beats the residual bucket, and the residual bucket is still reported when there is nothing better. Round 3's motivating case (a proven stale bundle must not be billed to the resolver) is unaffected and still covered. Tests: a guard for the pty-spawn-unhealthy shape, verified load-bearing by mutation (removing the residual-bucket exclusion fails it). * test(daemon): cover the confirmation path production actually uses Round-5 review. No runtime defect, but the three identified reasons were only ever proven through a mechanism the field never takes. confirmedReplacement is `(await killStaleDaemon(...)) || cleanupResult.cleaned`, and the two halves are disjoint in practice. unhealthy_resolver, stale_bundle, and different_app_path all require health === 'healthy', so cleanup reaches the daemon over RPC, shuts it down, and unlinks its pid file -- leaving nothing for killStaleDaemon to find. Production therefore confirms exclusively via .cleaned. Every test confirmed exclusively via killStaleDaemon, because the net.connect stub always errors, so cleanup always returned cleaned:false. The consequence was a silent trapdoor: simplifying the gate to `confirmedReplacement = await killStaleDaemon(...)` stops all three identified reasons from ever emitting in the field, and the whole suite stays green. Confirmed by flipping the killStaleDaemon default to false -- five tests fail, none of which are meant to be about the kill. Adds the missing case: cleanup confirms, the kill finds nothing, and different_app_path is still reported once. Verified load-bearing by mutation -- dropping the .cleaned half now fails this test specifically. Scoped the new test's mocks to *Once so the identity override cannot leak into the packaged-bundle test that follows, and hands probeSocketExists back on the way out since beforeEach only mockClear()s it. * test(daemon): cover the manual-restart retirement guard Round-6 review returned land-ready with one note: removing the !restartInFlight guard from both respawn closures left the whole suite green. That is the same silent-trapdoor shape the previous commit closed for the .cleaned gate, on a guard this PR introduced, so it gets the same treatment. The guard matters because runRestartDaemon tears the daemon down while the outgoing adapter is still live and undisposed; a pane respawning on its synthetic exit reaches the death path for what was a user action, and would bill a manual restart to the crash bucket. Drives the death from inside the restart's ensureRunning so restartInFlight is genuinely set by the code under test, rather than asserting against a flag the test poked itself, and then repeats the respawn after the restart settles to show the suppression is scoped rather than permanent. Verified load-bearing by mutation: removing the guard fails this test and nothing else. * test(daemon): close the two surviving telemetry mutations Round-7 review returned land-ready with two test-only gaps, both found by mutation and both the same trapdoor shape as the last two commits. The manual-restart guard exists in two respawn closures and only the first was covered. That is the wrong half: the restart installs its own adapter, so from the second restart onward the copy in runRestartDaemon is the one that actually runs in the field, and it could be deleted with the suite green. The test now drives a second restart through the adapter the first one installed. The privacy-invariant test only built 'replaced' payloads, so .strict() on the 'retired' member was never exercised -- someone adding a field to trackDaemonRetired after that .strict() was dropped would have reached PostHog with the test still passing. It now runs the leak set over both transitions, plus a sanity assertion that each base payload is itself valid so the rejections prove the leak and not a malformed base. Both verified load-bearing: neutering the second guard copy, and dropping .strict() from the retired member, each now fail exactly one test.
…A-2373) (stablyai#10065) * fix(daemon): respawn on PTY write dropped to a dead daemon socket (STA-2373) DaemonPtyAdapter.write() sends keystrokes via fire-and-forget client.notify(). When the daemon dies (retirement, crash, kill), the socket disconnects and the notify is silently dropped — no rejection reaches withDaemonRetry, so the dead-endpoint respawn never fires and the attached pane freezes. Only a request/reply RPC (e.g. createOrAttach from opening a new terminal) detected the death and forked a replacement. DaemonClient.notify() now reports delivery; a dropped write to a still-active session drives the shared respawn coalescer directly (reconnecting the permanent client before releasing the temporary adoption lease, mirroring withDaemonRetry's ordering), so the pane self-heals like the createOrAttach path. Cross-platform + SSH-safe: no platform assumptions, pure adapter logic. Complements (does not duplicate) stablyai#8426, which fixes the adjacent in-daemon bug where a thrown node-pty write no longer marks the handle dead. That is daemon-side; this is the app-side dropped-notify that never triggered respawn. * fix(daemon): restore adapter state after dropped-write respawn * fix(daemon): recover writes after endpoint respawn * fix(terminal): remount panes after daemon death * fix(daemon): recover sibling panes after daemon death, not just the written one When a daemon dies, its dropped-write respawn only remounted the pane whose write detected the dead endpoint. Sibling panes (alive at death but not typed into) were left frozen: stale prompt pixels, silently-dropped input, no live child, and no recovery even on later keystrokes — the exact STA-2373 frozen-typing symptom on non-triggering panes. DaemonPtyAdapter now fans a write-unavailable signal out to every active session when it recovers from a dead endpoint, emitted while the sessions are still in activeSessionIds so the renderer's liveness gate still reads them live. pty.ts forwards each to the existing pty:writeUnavailable channel, so all panes remount + re-attach through the same path the written pane already used. Adds a revert-sensitive regression test: with two sessions and only one written after the daemon dies, the sibling must also be signaled to recover. * revert(format): drop repo-wide oxfmt churn unrelated to STA-2373 A review pass ran `oxfmt --write .` across the tree, pulling seven files with no bearing on the dead-daemon respawn fix into the PR diff. Restored to origin/main byte-for-byte so the diff carries only the respawn change. * fix(daemon): snapshot active sessions before the write-unavailable fan-out A listener that kills a pane mutates activeSessionIds mid-iteration, which can skip the very sibling the fan-out exists to reach. Matches the snapshot fanoutSyntheticExits already takes. * fix(daemon): re-arm dead-endpoint recovery on every daemon death The respawn-storm latch was only released once every awaiting session rebound. Background sessions have no mounted pane, so nothing ever calls createOrAttach for them and they hold the awaiting set non-empty forever — latching the fan-out off after the first death and silently making the whole fix one-shot. Re-arm on the disconnect event instead, which fires once per established connection, so the storm guard still holds within a single incident. * fix(daemon): route the write-unavailable fan-out through the pty router Main subscribes on the routed provider, and DaemonPtyRouter is the live localProvider whenever a legacy daemon socket exists — the common case when an in-place update bumps PROTOCOL_VERSION with terminals running. It forwarded write but not onWriteUnavailable, so the fan-out reached no listener and only the written pane recovered: STA-2373 unfixed, silently. Also stop rejecting writes on adapters that cannot respawn. Legacy adapters have no respawn, so the remount reattaches to nothing and rebuilds the pane empty, losing scrollback the user could still read — worse than the pre-existing silent drop. And guard the renderer's write-unavailable handler on ptyId like its sibling data/replay handlers, so a transport that rebinds without detaching cannot remount a healthy pane. * fix(daemon): route the write-unavailable fan-out through the degraded provider DegradedDaemonPtyProvider is the live localProvider in degraded launch mode and main subscribes on it, but it forwarded onData/onExit/onReplay/ onBackgroundStreamEvent and not onWriteUnavailable — so the fan-out reached no listener and siblings stayed frozen. Same defect as the router, one provider over. The file sat at its max-lines ceiling, so make room by reusing one combineUnsubscribes helper across the three places that already repeated that loop rather than bumping the limit. Forward to the daemon adapters only: the local fallback has no dead-socket problem. * refactor(daemon): share the listener-fanout unsubscribe combination Adding onWriteUnavailable to both provider wrappers left each file at exactly 300/300 lines, so the next line anyone added would have broken max-lines with no sanctioned escape hatch. Both already repeated the same combine-unsubscribes loop, so lift it into one module: duplication drops and each file gets its headroom back. * fix(test): stop the fake emitter colliding with the private adapter emitter DaemonPtyAdapter.emitWriteUnavailable is private, so declaring a public member of the same name on a mock intersected with DaemonPtyAdapter collapsed the whole type to never — one collision produced 54 typecheck errors, taking out pre-existing assertions in both files too. Rename the fake to triggerWriteUnavailable and declare onWriteUnavailable on ProviderMock, which IPtyProvider does not carry on this branch. vitest does not typecheck, which is why a red build sat behind a green suite.
Run terminal visibility transitions pre-paint only on macOS. Restore passive disposal and recreation on Windows/Linux, remove the Windows retained-context LRU machinery, and preserve the normal 128-context startup ceiling.
…ai#10776) * fix(ssh): connect to Linux hosts that cannot compile node-pty node-pty ships no Linux prebuilt at any architecture, so it is compiled on the remote. On a host without a C/C++ toolchain that build fails, and because both native deps install in one npm command it also took down @parcel/watcher — which does have a working Linux prebuilt — and failed the whole connection. Every Linux image without build tools was unusable. node-pty only backs remote terminals; files, git, and the editor do not need it, and a missing native dep is already non-fatal further down the deploy. So when the existing toolchain probe confirms the compiler is missing, reinstall without node-pty instead of aborting. The manifest has to drop it too — npm reconciles every dependency in package.json, not just the ones named on the command line, so naming only @parcel/watcher still rebuilds node-pty. If that reinstall also fails the actionable build-tools error is rethrown, so a host broken for some other reason still reports the toolchain gap. The relay's PTY error now names the fix rather than saying only that node-pty is unavailable. Verified on a stock Rocky Linux 10.2 aarch64 container (openssh-server, git, nodejs, npm, no compiler): connect succeeds, /etc lists over SSH, node-pty is absent while @parcel/watcher installs its linux-arm64-glibc prebuilt, and spawning a terminal reports the install hint. * fix(ssh): keep the node-pty skip path honest about platform and watcher The PTY unavailable message named build tools unconditionally, but only Linux compiles node-pty — the deploy-side skip is gated on linux and the toolchain probe returns null on Windows. A Windows or macOS remote, where node-pty ships prebuilds, was told to install make/g++/python3. Pick the remedy by the relay's own platform. The skip path returned before the install probe, so a @parcel/watcher that installs but cannot require() (glibc below the floor) connected with dead file watching and nothing logged. Probe before returning and warn; no rebuild, since node-pty provably cannot compile on that host, and never fatal. Also log the pty-less reinstall's own failure and attach it as cause — the rethrown toolchain message is built from the original npm error, so an unrelated retry failure (registry, ENOSPC, EACCES) was lost. The reinstall now keeps the caller's resetDeps as well, so a repair reconnect still clears every dep the probe found broken. Tests: the skip-success fixture queued a chmod/probe/rebuild sequence production never runs, and the surplus slots were absorbed by launchRelay's readiness poll (1817ms vs 3-9ms for its peers). It now emits exactly the 12 execs production performs, and pins that no rebuild is issued. Adds the missing negative case: a gyp-shaped failure on a host whose probe reports a complete toolchain must still hard-fail rather than silently degrade. * fix(ssh): hedge the node-pty remedy and keep repair resets on the skip path
…lyai#10538) When a pane is mislabeled agent:codex but still holds a Claude transcriptPath, the Codex resume guard threw and blocked relaunch. Only hard-fail when the path claims Codex's dated rollout layout (sessions/YYYY/MM/DD/rollout-*.jsonl), under any home and without requiring the file to exist. Paths that never claimed Codex provenance return null so the pane can relaunch. Keying on rollout shape rather than trusted-home membership matters: returning null only declines to override CODEX_HOME, and the renderer has already baked 'codex resume <id>' into the command. A real rollout under an untrusted home would otherwise resume under whichever account is selected — and once the session bridge hardlinks rollouts across homes, codex would find that id and resume silently under the wrong account. Trust is decided upstream by findTrustedCodexSessionResume. Co-authored-by: Wooseong Kim <innocarpe@users.noreply.github.com> Closes stablyai#10517
…tablyai#10467) * fix(codex): cache weekly-only accounts when switching Codex accounts refreshForCodexAccountChange snapshotted the outgoing account only when this.state.codex.session was populated. Weekly-only plans report no session window, so their snapshot was dropped and the account switcher's inline bars rendered empty for exactly those accounts. Accept a populated weekly window as well. stablyai#10136 made this reachable: before duration-based classification, a weekly-only quota landed in the session slot, so the gate happened to pass. Claude is intentionally untouched; it has no weekly-only plan shape. * test(rate-limits): pin that a windowless outgoing Codex account is not cached The widened weekly-only gate had no test for its lower bound: replacing it with a bare truthy check on state.codex passed all 70 tests, which would cache an empty fetching placeholder and render a blank inline bar row in the switcher.
…ncy self-diagnostic (stablyai#10784) * perf(terminal): coalesce per-keystroke input stamps + add typing-latency diagnostic A user reports keystroke-echo lag on v1.4.156 that vanishes in Ghostty on the same machine, at a scale we cannot reproduce locally (nested worktrees, ~20 agents each). Static analysis across the 155..156 renderer diff found no perceptible regression, so this adds the instrument to measure it where it actually happens. Diagnostic (`window.__orcaTypingDiagnostic`, dev-console only, no shipped UI): reports keydown->paint percentiles from real typing plus a scale census — agent rows store-total vs mounted-DOM, store listener count, worktree nesting depth, the settings gating suspect paths, and the focused pane's agent and buffer mode. Nothing attaches to the keystroke path until start(), so it does not perturb the latency it measures. Coalescing: recordTerminalInput wrote the whole lastTerminalInputAtByPaneKey map on every keystroke, waking every zustand subscriber. Hibernation is a >=60s idle timeout, so the leading edge of a burst writes immediately and the rest collapse into one trailing flush. Imperative readers merge the pending stamp, and a late flush never revives a pane key teardown deleted. This write path is byte-identical in v1.4.155 and v1.4.156, so the coalescing is a general perf win, not a fix for the reported regression — the measured saving (~0.02ms/keystroke) is well below perception. * fix(diagnostic): count React store subscriptions in the listener census The census wrapped `subscribe` on the bound hook after `create()` had already run. zustand's `useStore()` reads the INNER `api.subscribe`, and `create()` copies subscribe onto the hook as a separate property slot — so patching the hook's copy counted only the 16 imperative `useAppStore.subscribe()` call sites and missed all ~2.2k React hook subscriptions, i.e. exactly the ones that scale with agent rows. The metric would have read a near-constant ~16 regardless of scale, which would have made "latency tracks listener count" read as false no matter the truth. The inner api is only reachable as the state creator's third argument, so the counter now installs there and lives in the store rather than the probe. Still per-subscribe (component mount), never per-setState: zustand notifies by iterating its listener Set directly, so this never touches the keystroke path. Tests pin both subscribe paths; reverting to the old wiring fails 3 of the 5.
…0778) * fix(ssh): stop remote terminals fail-opening to local PTY Docker SSH watcher isolation failed because an unhydrated remote worktree spawned through the local daemon with a container-only cwd. Fail closed while the SSH owner is still loading, ignore non-PTY mux notifications before mapping params.id, and harden the Docker SSH e2e connect helper so the repo connectionId is present before terminal activation. Co-authored-by: Orca <help@stably.ai> * fix(terminal): keep host-agnostic terminals off the hydration guard The unresolved-owner guard also caught floating and inline setup terminals, which have no repo row by design, regressing stablyai#10151. Scope it to repo-backed worktrees and cover the SSH hydration window with a regression test. Co-authored-by: Orca <help@stably.ai> * fix(terminal): treat a local-stamped worktree as a resolved host The hydration guard keyed off "no repo row", which also withheld spawn for a local worktree whose own hostId already proves its host. Key it off "nothing names the host" instead, and cover the local-stamped case with a test. Co-authored-by: Orca <help@stably.ai> * fix(terminal): recover parked panes when their host hydrates Withholding the spawn stopped the wrong-host PTY but left the pane inert: nothing bumped its generation once the repo row merged, so a remote terminal still never opened. Remount PTY-less tabs when repos:changed resolves their owner, and drop the e2e helper workaround that was masking this. Co-authored-by: Orca <help@stably.ai> * fix(terminal): scope hydration remounts to panes that actually parked Keying recovery off "tab has no PTY" also matched tabs whose shell merely exited, remounting them on every repos:changed. Track the panes that withheld their spawn and consume each entry once, so recovery cannot churn a live terminal or spin on repeated refreshes. Co-authored-by: Orca <help@stably.ai> * test(terminal): cover the repos:changed parked-pane remount wiring The recovery predicate was unit-tested, but nothing proved useIpcEvents actually calls it — the existing suite stubs repos.onChanged as a no-op. Drive the real listener and assert a parked pane is remounted only after its host resolves. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Upstream
Summary
Summary - Map guest Linux Codex transcript paths to host-readable
\\wsl.localhost\…so Chat UI can load WSL Codex sessions. - Search WSL managed + system Codex session roots; wire transcript watch to the translated path. - Also includes false agent-title exit suppress so ChaNote
innocarpe/orcamainuntil the upstream PR is merged.