mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 16:03:20 +00:00
2d232171663a5a1fe24e2d2d7a95d7c6859155de
4272
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2d23217166 |
feat: add Trae CLI as a supported TUI agent (#10763)
* feat: [AI-GEN] add Trae CLI as a supported TUI agent Closes #10579. Wire trae-cli into the desktop and mobile agent catalogs following the same integration pattern as other CLI agents (e.g. Ante, Devin): - src/shared/types.ts, tui-agent-config.ts: register 'trae' with detectCmdAliases (traecli/trae-agent) and argv prompt injection, matching trae-cli's `trae-cli [prompt]` contract. The CLI's own third documented alias `ta` is intentionally excluded — too generic a 2-letter name to use as a PATH-existence detection signal without false-positiving on unrelated tools. - src/shared/trae-headless-command.ts: recognize `--print`/`-p` and `--output-format json|stream-json` as one-shot headless invocations (same shape as claude-headless-command.ts) so they aren't mistaken for a live interactive session. - agent-kind.ts, telemetry-events.ts, agent-status-types.ts, agent-type-label.ts, tui-agent-display-names.ts, tui-agent-permissions.ts (YOLO via trae-cli's own --yolo flag), tui-agent-selection.ts: standard per-agent registrations. - agent-catalog.tsx, agent-favicon-assets.ts, mobile/src/tasks/mobile-tui-agents.ts, mobile/src/components/mobile-agent-icon-assets.ts: catalog entries and bundled favicon (fetched from docs.trae.cn, required by mobile's offline-icon invariant test). - i18n: add the "Trae" label to all five locale catalogs (en/es/ja/ko/zh). - Tests: agent-process-recognition, agent-status, tui-agent-startup. Verified with `pnpm typecheck` (desktop + mobile), the relevant vitest suites (869 tests across 12 files, all green), oxlint (clean), and a real end-to-end launch of the actual trae-cli binary through Orca's pty.spawn IPC path (confirmed via the OS process table). * fix: [AI-GEN] point Trae catalog entry at the real CLI quick-start doc docs.trae.cn/cli (what the installed CLI's own --help text prints as its "User manual" link) soft-404s — the docs site restructured and the working page is docs.trae.cn/cli_get-started-with-trae-cli (confirmed by HTTP fetch: real page title "TRAE CLI 快速开始" vs the old path's "404 - 页面不存在"). Addresses CodeRabbit's homepageUrl review comment. * fix: [AI-GEN] detect Trae on traecli, not the ambiguous trae-cli name Per @AmethystLiang's review: the open-source bytedance/trae-agent project (MIT, ~12k stars) registers its own console script as `trae-cli` (pyproject.toml: `trae-cli = "trae_agent.cli:main"`), an entirely unrelated CLI with a different contract (`trae-cli run "task"`, `-p` short for `--provider`). Detecting on bare `trae-cli` would false-positive on that project's installs and break launch for anyone who has it instead of the actual TRAE CN CLI. - tui-agent-config.ts: detectCmd/launchCmd/expectedProcess -> `traecli` (TRAE CN's own installer symlinks this alias too, but the other project does not ship it). Dropped the `trae-agent` alias entirely — it's the colliding project's literal repo name, the highest false-positive string available. - agent-catalog.tsx: cmd -> `traecli` to match; faviconDomain -> `www.trae.cn` (bare `trae.cn` 404s on Google's favicon service; `www.trae.cn` is the product-root domain that actually resolves). - mobile-tui-agents.ts: faviconDomain -> `www.trae.cn` to match. - Tests updated: agent-process-recognition now asserts `trae-cli` and `trae-agent` are NOT recognized as Trae (regression guard against reintroducing the collision); tui-agent-startup updated for the new launch command. promptInjectionMode stays `argv` and the headless-command file stays as-is — both verified against the real TRAE CN CLI's actual --help output (pasted in the PR review thread), not assumptions. * refactor: [AI-GEN] share one print-mode headless matcher across agents trae-headless-command.ts was a rename-only fork of claude-headless-command.ts, and ante-headless-command.ts carried a third copy of optionName. Collapse both print-mode files into print-mode-headless-command.ts, dispatch from a Partial<Record<TuiAgent, ...>> table instead of an if-chain, and compress the Trae comments to the repo's one-line style. * fix: [AI-GEN] terminate Trae flag parsing before the positional prompt `traecli` is a Cobra CLI with subcommands, so an argv prompt starting with `help`, `config`, `-…` was dispatched as a subcommand or flag instead of being run as the task. Add `argvPromptSeparator: '--'` (same reason Grok has it), and stop the shared print-mode headless matcher at `--` so a prompt that reads like `--print` no longer drops the pane out of agent recognition. * docs: [AI-GEN] name both Trae CLIs explicitly in the detect-name comment Co-authored-by: Orca <help@stably.ai> * docs: [AI-GEN] drop the vendor tag from the Trae union comment Co-authored-by: Orca <help@stably.ai> * fix: [AI-GEN] guard the nullable startup plan in the Trae separator test Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: 陈泽榜 <chenzebang@jianzhikeji.com> Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Co-authored-by: Orca <help@stably.ai> |
||
|
|
1bd80931bd | fix(diff): stop file-tree navigation remounting combined diffs; make tree resizable (#11088) | ||
|
|
0404f27b3f |
chore(i18n): drop orphaned cadence catalog keys (#11154)
Removes the lowercase hourly/weekly/custom entries from the AutomationSchedulePicker catalog node in all five locales. They were superseded by keys derived from the rendered labels in #11068, and the extractor only adds keys, so they stayed behind unreferenced. Follow-up to #11068. |
||
|
|
16dcf865ed |
fix(i18n): translate automation cadence labels (#11068)
The cadence picker now resolves Hourly, Daily, Weekdays, Weekly, and Custom cron through the renderer i18n catalog, with corrected zh/ko/es translations. Supersedes #10044 (thanks @innocarpe) and #10045 (thanks @fsdwen). Fixes #10043 |
||
|
|
d548641f1d |
fix(sidebar): restore legible selected-workspace fill in dark mode (#11139)
#8321 mixed the selected card's wash into the opaque --worktree-sidebar surface, lifting dark mode to 16% (#4b4b4b). Card text lost too much contrast against it. Return both modes to a translucent wash (light 8%, dark 10%) so the brighter selection border added by #8321 carries the selected state instead of the fill. Co-authored-by: Orca <help@stably.ai> |
||
|
|
df9f55990b |
fix(browser): keep the address bar editable in a narrow toolbar (#11098)
* fix(browser): keep the address bar editable in a narrow toolbar Every other browser toolbar control is shrink-0, so the address bar was the only flexible item and absorbed the entire squeeze: below roughly 420px of pane width it collapsed to the leading globe icon with a zero-width input. Clicking it only opened the suggestion dropdown, which inherits `--radix-popover-trigger-width` and so rendered at icon width — there was no way to type or edit a URL in that tab. Focusing a squeezed bar now lifts the form out of the toolbar flow and overlays the row edge to edge, giving a full-width editable field that navigates on Enter (and a full-width suggestion list for free). A measured slot stays in flow so the overlay cannot feed back into its own width, and the slot keeps a min width so the globe remains a real hit target instead of being overlapped by neighbouring buttons. Fixes #11090 Claude-Session: https://claude.ai/code/session_01Mx53f7erbtw5NraS8HdXKE * fix(browser): use the documented floating shadow for the expanded bar STYLEGUIDE.md defines exactly three elevation levels and forbids a fourth; shadow-md was not one of them. The overlaid address bar is a floating surface, so it takes the documented floating shadow already used by the other floating surfaces in this pane. Claude-Session: https://claude.ai/code/session_01Mx53f7erbtw5NraS8HdXKE * test(browser): make the narrow-toolbar regression deterministic The spec passed only from a clean profile. Two preconditions it set once are actively undone by the app: - BrowserPane re-focuses a blank tab's address bar across several animation frames plus the blank-url did-finish-load handler, so a single blur() was reverted and the bar never reached its squeezed resting state. - Startup paths re-open the right sidebar. At a fixed 700px window that leaves the pane ~70px, so the overlay had nowhere to go and the field measured 0px. Settling these separately let whichever settled first drift back while the next one ran. Re-assert them in one loop until they hold simultaneously, and size the window from the chrome actually measured instead of assuming a fixed 700px. Verified 8/8 green, and still fails at the overlay assertion when the fix is disabled, so the regression coverage stays real. --------- Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
e551d3ec0d |
perf(lint): consolidate code-quality gates into Oxlint (#11117)
Consolidate standalone code-quality scanners into Oxlint, preserve focused native/type-aware enforcement, add custom plugin coverage, and harden deferred PTY test cleanup. |
||
|
|
700cde83e0 |
i18n: translate orchestration page and workflow messages (#11097)
Add translations for OrchestrationPage coordinator and child PR names, plus agent workflow status messages (initial states and progress beats) across all supported languages (English, Spanish, Japanese, Korean, Simplified Chinese). |
||
|
|
3f5098a0f2 | fix(workspaces): hide false repo error for remote servers (#11050) | ||
|
|
9de4519c82 |
fix(terminal): gate the developer menu behind Option and unblock manual parking (#11091)
The Developer submenu shipped visible on every worktree right-click, and its Park terminal action always refused with "These terminals cannot be parked safely." - reveal the Developer submenu only when Option/Alt is held at right-click, captured at open time so it can't shift rows mid-menu - stop a settled pendingActivationSpawn tag from refusing a manual park: first activation stamps it on every tab and only a fresh updateTabPtyId consumes it, so a reattached tab kept it forever - resolve the single leaf of a rootless layout for parked watcher coverage, so a workspace whose panes never mounted is no longer permanently uncoverable - restore the !isVisible park guard dropped in #11016, which let the workspace being viewed unmount its own terminals - split manual-park eligibility out of the automatic cold-park policy module |
||
|
|
49fbe5231d | feat(workspaces): add emoji shortcode picker | ||
|
|
038fd7a50c | feat(workspaces): derive readable emoji identifiers | ||
|
|
e73b1a1dd0 |
feat(new-workspace): type-ahead Project and Run-on pickers (#11062)
* feat(new-workspace): make the project picker a type-ahead field The Create-worktree Project slot read as bulky and unpolished: a label row, an add-project icon, a 36px outline trigger and a chevron, all spent before choosing anything — then a popover carrying its own *second* search box, two-line rows, and a footer that scrolled out of reach. The field is now the search. Typing filters in place, so the nested search box is gone. Exactly one row is armed at any time and Enter takes it; hovering arms, so pointer and keyboard drive one cursor rather than two competing highlights. Armed is tracked by row key, not index, so a list arriving late over SSH cannot slide a different project under a keypress the user already aimed. Rows are single-line at 28px with an on-row Enter cap that takes space only while armed, and "Add a new project" is pinned to the popover edge so it survives every state — scrolled, filtered to nothing, or no projects at all. Long names and deep paths degrade deliberately: the name keeps up to half the row and the path elides from its middle, so two monorepo siblings stay distinguishable as …/services/checkout-api vs -web where a flat truncate rendered both identically. Recency is derived from when each project last had a workspace created, which is the action this picker is about to repeat — no new store field. The shell keeps data-project-combobox-root + role=combobox and stays focusable, so the composer's initial-focus and project-required handlers still land on it. * fix(new-workspace): align, scroll and loosen the project picker Five fixes to the type-ahead picker, three reported and two found while checking for related breakage. Alignment: the name and its smaller detail line were centred as boxes, so the 12px path sat visibly high against the 14px name. Both now share a baseline, in the committed field and in every row. The dot mark and the Enter cap are chips rather than text, so they stay centred on the row. Scrolling: the mouse wheel did nothing over the list. The composer is a Radix Dialog, and react-remove-scroll cancels wheel events for portaled content outside the dialog's DOM tree — the scrollbar dragged fine but the wheel was dead. The old cmdk list carried a shim for exactly this; the plain scroll pane that replaced it did not, so it has its own now. Density: rows go 28px -> 32px, row text 13px -> 14px and detail 11px -> 12px, with a taller Add row and more air above section headings. Escape stranded a query: with the list closed but text still typed, Escape was ignored (it was gated on the list being open), leaving the field showing text that matched nothing and hid the committed project. Escape now always restores the committed display, and only bubbles when there is nothing to undo. Listbox ownership: options sat inside unroled section and scroll wrappers, which breaks the listbox -> option relationship assistive tech relies on. Sections are groups carrying the heading as their label, and the scroll pane is presentational. Both new behaviours are covered by tests verified to fail without the fix. * chore(tools): keep the project-picker design lab The exploration harness behind the picker rewrite: 16 interactive design variants rendered against the app's real tokens and shadcn primitives, so a prototype is a drop-in ProjectCombobox rather than a mockup. Worth keeping because the frames encode bugs that only reproduce in context. DialogFrame renders the picker inside a real Radix Dialog, which is the only way the react-remove-scroll wheel bug shows up; the fixtures carry duplicate display names and deep sibling paths that a naive truncate renders identically. Run with: npx vite --config tools/wt-picker-lab/vite.config.ts * fix(new-workspace): stop the project list flashing open, shrink its empty state Opening the picker read as a double flash. The shared popover surface is translucent and fades 0 -> 1, which is right over the app canvas but wrong here: this popover lands directly on the composer dialog, so for the length of the fade the Name field underneath showed straight through the list and you saw two layers at once. The list now uses an opaque surface and zooms without fading, so it is solid from the first frame. Every other popover keeps the blur and fade. The "No projects match your search." state was a 60px centred block sitting next to 32px rows, which read as a different kind of surface and made an empty result feel like an error. It is now sized and aligned like a row. The lab's dialog frame focused whatever Radix picked first, which popped the Add-project tooltip on open and masked the real problem; it now focuses the name field the way the real composer does. * fix(new-workspace): square mark, centred empty state, and keep the list open on tab-focus Four fixes, three reported and one found while sweeping for others. Square mark: the option dot had a `rounded-full` override, so a project read as a circle here and a square everywhere else (jump palette, sidebar). Drop the override and use RepoBadgeMark's own shape. Centred empty state: "No projects match your search." was left-aligned after being shrunk to row height; centre it. Tab-focus blinked the list shut: the field lives in the popover's anchor, not inside its content, so Radix's dismissable layer saw focus land "outside" and closed the list the instant you tabbed in. Focus and pointer events within this control no longer dismiss it; genuine outside events still do. Junk text could strand the field: typing a query that matched nothing and then clicking away left the text sitting there with the list closed, showing no project and no error. A query only means something while the list is open, so closing without committing now clears it. On pressing Create with no project: no change needed. The create gate has not depended on project selection since #4991, and both submit paths already call showProjectRequiredError(), which sets the inline message and turns the field red via aria-invalid. Verified end to end: the button is pressable, the press paints the field destructive, and the message appears beneath it. * feat(new-workspace): rebuild the Run-on picker to match the project picker "Run on" was the last composer field still built the old way: an outline trigger wrapping a cmdk list, two-line rows, and no way to search. It now matches the project picker, so the two fields in the same form read as one control. The field is the search — type to filter hosts, paths and recipes with no nested search box. Exactly one row is armed at a time and Enter takes it; hovering arms, so pointer and keyboard drive one cursor. Rows are 32px with the label and its path on a shared baseline, the path eliding from its middle so two deep sibling paths stay distinguishable. The popover surface is opaque and unfaded because it lands on the composer dialog, where a translucent fade shows the form underneath. Two behaviours the project picker doesn't have are preserved. Disconnected hosts keep their inline Connect action, tracked per host so one stalled connect never blocks the others, and the list stays open so the connecting state is visible. Two rows open nested lists rather than committing: VM recipes, and "Add host" pinned to the popover edge so it survives every state — scrolled, filtered to nothing, or with no hosts at all. Enter and ArrowRight open a submenu; Escape backs out one layer at a time. Extracted from NewWorkspaceComposerCard (-563 lines) into files that each stay under the line limit without a suppression. Tests: the run-target cases asserted cmdk internals (`[cmdk-item]`, aria-disabled, cmdk-separator) that no longer exist. Rewritten against behaviour and the listbox roles instead. All 22 composer tests pass, plus a live sweep of 11 interactions in a real dialog. * fix(new-workspace): drop the Enter cap, fix submenu hover, match the Add rows Three follow-ups on the two composer pickers. The ↵ cap on the hovered row is gone from both. On a run-target row it sat next to the Connect action and read as a second, competing affordance; the highlight already says what Enter will take. Submenu rows never highlighted under the pointer. They passed a hardcoded `armed={false}`, so the recipe list and the Add-host choices were the only rows in either picker with no hover state. They now track their own hover. "Add a new project" used a chunky FolderPlus where "Add host" uses a plain Plus. Both rows were already the same height and type, so matching the glyph is the whole difference. * fix(new-workspace): restore the folder glyph, two-line Add-host cards, quiet Connect rows Three follow-ups. "Add a new project" goes back to FolderPlus — matching "Add host"'s plain Plus made the two consistent but lost the glyph that says which kind of thing is being added. A disconnected host row no longer repeats its status. The Connect button already says the host isn't connected, so "Connect this host to set up projects" beside it was saying it twice. Rows without a Connect action keep their detail, since there it explains why the host can't run. The Add-host choices go back to two-line cards. Their descriptions explain what you're picking ("Use an existing machine over SSH" vs "Pair another Orca runtime"), unlike a host row's detail, which just labels a host you already recognise. RunTargetRow grows a `stacked` variant for that rather than making the single-line row do both jobs. * fix(new-workspace): give Run on the same vertical rhythm as the other fields Run on is nested inside the Project block so the two share its error and empty states, which also put it on that block's 4px internal spacing. It reads as its own field, so it sat noticeably tighter than the 16px gap every other field in the composer gets. Pad it to match. * refactor(new-workspace): share the type-ahead machinery between both pickers Project and Run on were built one after the other, so each grew its own copy of the same mechanics: query and open state, arming by row key, the arrow-key walk, scroll-the-armed-row-into-view, the react-remove-scroll wheel shim, and the closes-drops-the-query rule. Two copies of subtle behaviour is two places for it to drift. useTypeAheadCombobox now owns all of it. Callers pass a function that turns a query into row keys and get back the query, the armed key, and the movement helpers. Run on layers its submenu state on top by wrapping `close`, which is the only part that isn't shared. The two long class strings both files repeated verbatim — the field shell and the opaque unfaded popover surface — are named constants now, so the reason they differ from the stock popover recipe is written down once instead of implied by a duplicated literal. No behaviour change: 16,456 renderer tests pass, plus the 22-check live interaction sweep across both pickers in a real dialog. * fix(new-workspace): drop aria-expanded from option rows, remove the design lab `aria-expanded` isn't a supported prop on `role="option"`, so the submenu rows were claiming a state screen readers can't interpret there. `aria-haspopup` alone already says the row opens a menu. Removes tools/wt-picker-lab. It was the harness for exploring this redesign — 12 interactive variants — and it did its job, but the 11 that lost are dead code, and its prototypes were the only thing failing the react-doctor gate (5 errors, all in throwaway variants; the shipped pickers had none). |
||
|
|
badf91101b |
fix(quality): enforce performance-safe lint baseline (#11074)
* fix(quality): clear safe existing lint findings * fix(quality): keep lint cleanup allocation-free * fix(quality): enforce performance-safe baseline * test(terminal): drain deferred confirmation cleanup |
||
|
|
ee7ec43149 |
fix(codex): keep a host account switch inside the host lane (#10992)
* fix(codex): keep a host account switch inside the host lane markLiveCodexSessionsForRestart walked every tab's PTYs and carded any pane whose foreground looked like Codex. There was no lane check anywhere in that path, so a host account switch raised a restart notice on live SSH/relay panes — and a notice mutes the pane, so the user's remote terminal went deaf. The notice was provably spurious: a remote spawn carries a connectionId, so isDaemonHostSpawn is false and no CODEX_HOME is ever injected. The remote Codex uses the remote machine's own credentials; a local selection cannot reach it. Scope marking by lane instead. A pane's lane is (machine, runtime): `host`, `wsl:<distro>`, `env:<id>` for a relay environment, or an SSH connection that no managed selection can name. A switch made while a runtime environment is active still cards that environment's panes, which is the case that made the old "mark everything" behaviour look right. WSL was the same defect, not a separate one. A Windows run saw a WSL pane correctly escape a host switch, but only because its foreground read `wsl.exe`, which fails the Codex-foreground test — the Win32 process table cannot see into a WSL2 VM. That is incidental: `codex`, `node` and `python3` foregrounds are all eligible today, so a WSL pane that surfaces one (WSL1 pico-processes are in Win32_Process) would be carded by a host switch. The lane is now what decides. Also stop queueing remote and SSH panes into the bind-driven stale sweep at all. recordCodexPaneAccountForSpawn bails on anything that is not a daemon host spawn, so listStalePanes can never report one stale, yet each pane still spent every rung on a 15s-timeout remote RPC — ~75s per pane since the ladder widened to five rungs. The lane vocabulary moves to shared/ so the renderer keys panes exactly as a launch does rather than growing a third copy of the rules. Refs #10757 * fix(codex): key a WSL pane by the distro its launch actually used The lane guard derived a pane's WSL distro from the workspace UNC path alone. A launch does not: pty.ts hands getCodexSelectionTargetForPty a third argument, the resolved runtime's distro, so a wsl.exe pane on an ordinary Windows-path worktree launches under `wsl:Ubuntu`. The renderer keyed that same pane `wsl:__default__`, so the Ubuntu switch never reached it — the pane kept the old account with no notice, which is #10757 returning by a new route on the exact platform the issue was reported from. Resolve the distro the way the spawn does: the project execution runtime first, then terminalWindowsWslDistro. Both are already in renderer state. Also match a distro-less WSL switch against the whole `wsl:` family. Two mutations reach the renderer as `{runtime:'wsl', wslDistro:null}` while writing concrete distro slots: selecting the system default clears EVERY wsl slot (setSelectedCodexAccountIdForTarget), and `add` stores the distro it discovered from the machine. Keying those to `__default__` missed the very panes they re-pointed. The residual cost is over-marking a sibling distro after an add, bounded to this machine's WSL panes and far cheaper than a stranded pane. An owner-less remote pane colliding with the host lane was untested — that collision is what would mute a working remote terminal, so pin the disjointness rather than the literal key. Refs #10757 * fix(codex): resolve a pane's lane the way its launch resolved it Three more places where the renderer's lane and the launch's lane disagreed. Each disagreement is silent: too narrow and a stranded pane never gets its notice (#10757 returns), too wide and a healthy pane is muted, because a notice makes onData drop every keystroke. Shell: main runs the request through resolveLocalWindowsTerminalRuntimeOptions, so an unset shellOverride still lands on WSL when that is the Windows default. Reading tab.shellOverride alone called such a pane `host` — a host switch would have muted a working WSL terminal. Gate on the renderer platform, as pty.ts gates on process.platform. Cwd: a terminal's startup cwd is deliberately not constrained to the worktree (resolveTerminalStartupCwd, #7685), and main keys the lane off that cwd. Follow it through the same shared call instead of reading the workspace root, so a pane split after `cd \\wsl.localhost\...` is keyed where it actually runs. The comment claiming a pane can never start outside its workspace was simply wrong. Family match: narrow the previous commit. setSelectedCodexAccountIdForTarget only nulls every WSL slot when the account is null AND no distro is named; any other write lands in one slot. So claim the family only when the change actually cleared them all, and let `add` pass the created account's concrete target rather than the row's "WSL default". Both call sites already knew which case they were in. Refs #10757 * fix(codex): derive the pane's project runtime the way main does The previous commit reached for getLocalProjectExecutionRuntimeContext as a stand-in for main's resolveLocalProjectRuntimeForWorktreeId. They are not the same function, and the differences both produce wrong lanes: - It falls back to `state.activeRepoId` when the worktree is not a git worktree, so a folder-workspace pane inherited whichever repo happened to be selected. That is not a property of the pane at all — the lane moved when the sidebar selection moved. On a WSL project it both muted a healthy host pane and hid the notice a host switch owed it. - It synthesizes a runtime from `inherit-global` where main returns undefined, and its host branch rewrites an explicit `wsl.exe` to powershell.exe, keying a live WSL pane `host`. Walk repo -> project directly instead, which is what resolveLocalProjectRuntimeForRepo does, and use it only to supply a distro — never to downgrade a shell. That also drops the throwing call out of this path entirely; the lane runs outside scanCodexPanes' inspection guard, so a throw there would have lost the notice for every pane in the batch, not just one. Also find the added account by diffing the roster. Reading it back through the row's active id returns null once two distro slots are filled, which sent the notice to `wsl:__default__` while `add` had written a concrete distro. Refs #10757 * fix(codex): key the lane off the runtime the renderer actually shipped Reverses the project-runtime half of the previous commit. That commit assumed main resolved the project runtime itself, so it re-derived one by hand. It does not: for a local pane the RENDERER computes it with getLocalProjectExecutionRuntimeContext and ships it with the spawn (pty-connection.ts), and pty.ts feeds that straight to getCodexSelectionTargetForPty. So the helper is not an approximation to be improved on — it is the launch. The hand walk dropped the global Windows runtime default, which is what turns an `inherit-global` project preference into WSL. A user who set their runtime default to WSL but left terminalWindowsShell alone would have had every live WSL pane keyed `host`: muted by a host switch, and missed by their own. It also disagreed on folder workspaces, where the launch really does resolve through the active repo. Keep the repair-required early return: that call throws, and it sits outside the scan's per-pane failure guard, so a throw would lose the notice for every pane in the batch rather than one. Separately, floating terminals have no workspace root, so their startup cwd is used verbatim (resolveTerminalStartupCwdForWorkspace). Resolving one against a root that does not exist yielded no cwd at all, keying a floating Codex pane on a WSL filesystem as `host`. Read its cwd directly. Require exactly one new account before trusting the roster diff — an unloaded prior roster makes every account look new, and Add Account is not gated on it. Refs #10757 * fix(codex): stop claiming a floating-terminal cwd the tab never has The floating-terminal branch read tab.startupCwd, which no floating creation path ever sets (FloatingTerminalPanel, FloatingTerminalWindowControls, floating-workspace-tab-creation all pass none). Its cwd is resolved over IPC from settings.floatingTerminalCwd and handed to the transport as a prop, so it never reaches the store at all. The branch was inert and its comment described main's handling of args.cwd rather than what the code read. Say what is actually true: a floating pane is keyed by its shell, and the configured-WSL-cwd-under-a-host-shell case is a known gap. Guessing from the unresolved setting would risk the mute direction, which is the expensive one. Also pin the repair-required early return. resolveLocalWindowsTerminalRuntimeOptions throws there, and the lane runs outside scanCodexPanes' per-pane failure guard, so without it Promise.all rejects and every pane in the batch loses its notice. That guard had no coverage; removing it now fails with the spawn error. Refs #10757 * fix(codex): trust the lane main recorded at spawn over a re-derived one The switch path re-derived each pane's Codex lane from current state while main had already written the resolved shell, cwd and distro at spawn. Four review rounds each found another divergence between the two, and the derivation still answers for a launch that never happened once the user edits a runtime preference. Prefer the recorded lane where one exists; keep the derivation for the panes main never records — pre-feature panes, LocalPtyProvider spawns and remote ids — and log when the two disagree. * refactor(codex): drop a redundant guard around the recorded-lane lookup |
||
|
|
2cf91b8e69 |
fix(skills): complete plugin-cache scans without false attention (#10865)
Fixes the P0 where skill cards showed an unclearable amber "Needs attention" while the Details dialog reported everything up to date. Root cause: when the plugin-cache scan tripped one of its own bounds it recorded an incomplete path, and inventorySkillFreshness expanded that into one fabricated placement per manifest skill at a path it never stat'ed. Those synthetic "inaccessible" copies lit the pill, were filtered out of the dialog, and could never be cleared because plugin-cache is not an updatable topology. - Removes the fabrication; reports typed scan issues instead. - Requires readable SKILL.md evidence before promoting a directory to a candidate, so a same-named foreign plugin (Codex's own computer-use) no longer flags. - Prunes skill payloads and node_modules so ordinary vendor caches stop tripping the depth and entry bounds. - Partitions scan reasons: only a real read failure raises a pill; bounds that ended the walk block an all-clear claim; the rest are Details-only. Fixes #10633. Refs #10659, #10904, #10918, #10775, #10791, #10813. |
||
|
|
e4b113b11d | fix(native-chat): keep known sessions loading until flush (#11032) | ||
|
|
dbbeae3512 | fix(terminal): clear retained overlay for editor tabs (#11030) | ||
|
|
12ef12c55b |
chore(quality): ratchet Oxlint, React Doctor, and Zustand performance (#11034)
* chore(quality): ratchet lint and Zustand performance * fix(ci): stabilize React peer lock snapshot * fix(ci): isolate PR diff and React Doctor CLI |
||
|
|
55e98e8182 |
fix(native-chat): auto-grow the composer input up to 8 lines (#10848)
Reviewed with an independent reproduction. Replaced the JS measure pass with layout-native CSS on both surfaces and removed the shared hook, fixing 72-142px of Linear issue title hidden after a window resize. |
||
|
|
60c7faf930 |
fix(browser): restore configured zoom after page reload (#10800)
Reviewed with an independent reproduction. Rewrote the reload-zoom reassert to be per-pane instead of sharing the value zoom in/out writes, fixing Cmd/Ctrl+0 reset and cross-tab zoom leakage, with E2E coverage proven to fail on revert. |
||
|
|
872a9c3930 |
fix(terminal): make copy work in the HTTP web client (#10534)
Reviewed with an independent reproduction over a real plain-HTTP origin. Replaced the hidden-textarea fallback with a capture-phase copy event so clipboard text never enters the DOM, and added rejection handlers to the surfaces that had none. |
||
|
|
b601643960 |
fix(sidebar): stop worktree cycling from reopening collapsed groups (#10513)
Reviewed with an independent reproduction. Replaced a redundant per-keypress layout rebuild with cycling over rendered sidebar rows; verified the chord is not hardcoded to metaKey. |
||
|
|
f1d54c123b |
fix(file-explorer): commit inline rename on outside click and stop double-click rename flicker (#10867)
Reviewed with an independent reproduction. Both fixes verified; added a flush for a dropped directory toggle and an integration test connecting blur/Escape through to renameFileOnDisk. |
||
|
|
b316174525 |
fix(panes): stop a stray mouse hijacking a touch divider drag (#11021)
The drag predicate recorded only the active pointerId, never its type, so during a touch-started drag any primary mouse or pen event failed the id match but passed the non-touch fallback and was accepted as the active pointer. Record the type at pointerdown and require both ends to be non-touch, keeping the WSLg mouse-press/pen-motion relay working. |
||
|
|
edc6cc007d |
fix(new-workspace): center agent selection in create dialog (#11020)
* fix(new-workspace): center agent selection in create dialog Pin the Agent combobox mark to 14px, drop residual button padding, and use a full-width min-w-0 trigger so icon, label, and chevron align with Project/Name in the new worktree dialog. * fix(new-workspace): optically align agent picker content * fix(new-workspace): center selected agent content |
||
|
|
4340781c9f |
fix(codex): drop the resume argv when session provenance is unverifiable (#10805)
Closes #10793. When Orca could not verify the originating Codex session file it either threw — a red per-pane toast and a failed spawn, reported as constant spam on #10757 — or returned null. Returning null did NOT start a fresh session: the renderer had already baked ['codex','resume',<id>] into the command and pty.ts never rewrote it, so CODEX_HOME simply fell through to whichever account was selected. The resume argv is now dropped so a plain `codex` launches, with a banner telling the user. The invariant — never run `codex resume <id>` under an account that does not own that rollout — is now satisfied by construction rather than by refusing to spawn. A verified resume is unchanged and still pins CODEX_HOME to the originating home. Reviewed over two adversarial rounds; seven defects found and fixed, including a HIGH where local-provider (non-daemon) spawns still carried ORCA_SEQUENCED_STARTUP_COMMAND with `resume <id>` — the wrong account behind a banner claiming it started fresh. `env` is now declared after the strip so no point in the handler can reach the pre-strip value. Live-validated in a real Orca dev build: all five cases proven on the SPAWNED PROCESS, including a real rollout under an untrusted home (the only shape that discriminates) and the local-provider path forced by stopping the daemon. An earlier CI failure on multi-client-navigation-isolation.integration.test.ts was investigated and is a PRE-EXISTING flake — a ~4ms race in the session-tabs notify coalescer that fails 5-8/24 on clean main, more often than on this branch. Fixed separately in #11022. Not verified: no Windows execution — its POSIX-only tests skip there and the #10757 reporter is on Windows. SSH is partial: no spurious banner or drop observed against a real target, but headless spawn does not deliver startup commands so the remote argv could not be read. The relay/mobile notice channel deliberately has no banner; the argv drop does happen there, so the invariant holds. |
||
|
|
9c5d827d6a |
fix(codex): keep history, restarts, and account identity across an account switch (#10770)
Fixes #10757. Switching Codex accounts broke three ways, all rooted in the self-contained per-account CODEX_HOME from #9501. HISTORY DISAPPEARED. Codex's own /resume picker only lists rollouts under the launch CODEX_HOME, and nothing bridged history into a per-account home — only the AI Vault's discovery scan knew about the other homes. Every other Orca-visible home's rollouts are now hardlinked in, on selection and again at launch, so one physical log is listed everywhere. THE RESTART PANEL STUCK. A queued restart was only drained by a mounted TerminalPane, but the prompt covered every stale pane in the worktree including parked and cold-deferred tabs. Requesting a restart now answers the prompt immediately while the pane keeps its pending restart, and a pane drains it when its reconnected PTY binds. PANES STAYED ON THE OLD ACCOUNT. CODEX_HOME is fixed in a shell's environment at spawn and the daemon keeps those shells alive across app restarts, while the restart notices are renderer state and are discarded. Each PTY's launch account is now recorded on disk and compared against the current selection at startup. Also merged in: #10802 (a dismissed notice no longer kills the pane's keyboard), #10803 (the sweep arms on real PTY binds, and launcher Codex panes are no longer filtered out by Windows deepest-process reporting), #10804 (a resume-pinned pane now says which account it is on), #10870 (the restart card no longer parks focus on its destructive Restart button), #10853 (the retry ladder is widened past the Windows worst case). Six independent reviews found real defects in every original PR, several of them dead-keyboard bugs and three introduced by the fix for another defect in the same loop. Live QA on macOS covered every PR; Windows was validated three times. WINDOWS: pass 1 found two defects that made the stale-account fix a no-op there (the sweep fired before any PTY was bound and never retried; launcher panes were filtered out). Pass 3 at the merged head: the prompt appears on its own after a restart — warm ~3.7-4.2s, cold ~21s needing rung 4, so #10853's widening was load-bearing rather than precautionary; an ordinary sentence typed into a healthy pane while another pane's card is up reaches that pane and kills nothing; a pane running vim after exiting Codex gets no card, still none 45s later. auth.json byte-identical across every pass. KNOWN GAPS, stated rather than implied: #10804 is unverified on Windows (auto-resume could not be manufactured there); cross-volume Windows is untested and expected to yield no bridged history (EXDEV, and Codex ignores symlinked rollouts); a cold-parked pane never binds so the sweep never covers it; the subagent-deepest launcher shape could not be reproduced on Windows, so that branch is fixture-verified only; WSL passed isolation but the resume mechanism is host-lane only. A host-account switch also marks and mutes live SSH remote panes — confirmed pre-existing on main by two independent QA runs — tracked separately in #10992. Related pre-existing defect filed as #10863. |
||
|
|
0956d5ca3a |
feat(skills): run skill updates in the background without a terminal (#10843)
* feat(skills): run skill updates in the background without a terminal The Update skills dialog had no primary action at all — its footer was only Re-check and Close, and the real action was a pre-filled command in an embedded PTY that the user had to press Enter on. Orca already builds and validates that command, so it now runs it. - Add a headless runner for `npx --yes skills update <names> --global -y`. Both --yes flags are load-bearing: npx's skips the package-install prompt, and the skills CLI's takes its own non-interactive branch. stdin is ignored so `process.stdin.isTTY` stays falsy, which is the other half of that gate. - Own the run in main so closing the dialog backgrounds it instead of killing it, and surface it in the status bar: spinner while running, a green check on success that clears itself, and a failure that persists until acted on. - Derive per-skill outcomes by re-scanning the freshness inventory after exit rather than parsing stdout. `skills update` has no --json (that flag exists only on `list`) and reports progress per-source, not per-skill, so the run bar is deliberately indeterminate instead of faking a percentage. When the re-scan has a verdict it outranks the exit code. - Drop the version trail from the rows and surface the skill list and skip reasons directly instead of hiding them behind a disclosure. Also fixes a width bug the collapsed disclosure used to hide: deep plugin-cache paths set the dialog's width and pushed the footer actions off-screen. * refactor(skills): use one row component across every update state The ready and running views were separate components with different row shapes, so pressing Update swapped the dialog's body for a different layout. They are now the same `SkillUpdateRow` instances throughout — only the status slot's contents change — and a test asserts the row is literally the same DOM node from "update available" through pending to the result. - Collapse each skill's locations behind its own disclosure. A skill with several plugin-cache copies was dumping every path inline and burying the actions; the row now shows a location count and expands on demand. - Put status in a single slot between the name and the count rather than a leading icon column. A leading icon has nothing to show in the resting state and reserving its box just indented every name past an empty gap. - Pin the running/finished run's names in `groupSkillFreshness` so a successful update doesn't drop its own rows the instant the re-scan lands. `skill-freshness-group.tsx` becomes `skill-location-chip-copy.ts` — only its chip label/tooltip helpers survived, and it no longer holds JSX. * fix(skills): place the status glyph left of the skill name Review feedback on the row header: the badge belongs immediately right of the name so it reads as part of it, and the run's status circle/check belongs to the left of the name rather than sharing the badge's slot on the far right. Name, glyph and badge are now one left-aligned group; the location count and chevron stay right-aligned. `available` still has no leading glyph — an empty reserved box only indents the name past a gap with nothing in it. * fix(skills): correct the headless update run's verdict, cancel path, and stopping copy Review fixes for the headless skill-update runner. Main process: - Judge per-skill outcomes on a positive signal. "Absent from eligibleUpdateNames" is not success: a deleted, half-written, or unreadable skill also leaves that list, so a corrupt update reported a green check. skillUpdateFailedNames now requires every convergent placement to come back current or newer-known. - Retire a child's handlers with a per-run token. A failed spawn emits error *and* close, so the second settle clobbered the real spawn ENOENT; a cancelled child could also settle, or write output into, the run that replaced it. The token guards the rescan's finish closure too. - Hold the run `running` until the killed process tree is actually dead. Releasing on the synchronous path let an immediate re-Update spawn a second npx writing the same bundles, with a watchdog so a sweep that never settles cannot wedge the run. - Kill the tree, not just the npx wrapper, via killWithDescendantSweep. - Publish an error instead of a silent `started: false` when the cmd.exe rail rejects the resolved npx path, which a profile directory containing & or % is enough to trigger on Windows. - Coalesce captured output into one push per tick instead of structured-cloning the whole buffer to every window on each progress frame. Renderer: - Keep rows on screen while the settling re-scan runs. Refreshing the inventory nulls it synchronously, so every row vanished at the moment the result appeared. Rows render off the last good scan; eligibility stays on the live snapshot so nothing is authorized off stale bytes. - Retry the names that failed, not the eligibility list that same re-scan has just emptied. - Add Stop, restoring the escape hatch the embedded terminal used to provide, and say "stopping" on every surface rather than claiming the update keeps running in the background. - Show a skipped skill's reason outside the disclosure, so it no longer depends on a mount-time defaultOpen a later re-scan can never re-fire. - Drop the summary line telling users to open "Update details", a control this PR removes; it was translated into four languages. - Keep the success linger from retiring a result the open dialog is showing. - Delete skill-location-chip-copy.tsx: an unreferenced copy of the old row component, colliding on basename with the module that is actually imported. * fix(skills): divide update list from summary |
||
|
|
0edc95fa35 |
perf(editor): cut per-keystroke work on two rich-markdown paths (#10862)
* perf(editor): cut per-keystroke work on two rich-markdown paths Doc links: both plugins walked every text node and ran matchAll on each — the auto-convert appendTransaction once per keystroke, the preview decorations once per keystroke and again per caret move. A link needs `[[`, so gate on a native substring check first. The two walks had duplicated their guard sequence; they now share one predicate. 3.1x-3.8x over the repo's own markdown. Annotations: resolving a comment's block re-serializes the whole document (every node, plus every adjacent pair), and both the highlight-range and comment-at-position paths did that once per comment — O(comments x document). Build the blocks once and pass them down. On a 12-node fixture with 8 comments that is 184 serializations down to 23. * test(editor): pin the one-build serialization baseline Review feedback, all four points: - The serialize-count assertions compared many-comments against one-comment, so they would have passed if BOTH built blocks twice. Pin the absolute count (23 = 12 nodes + 11 adjacent pairs) derived from the fixture size, so a regression to per-comment building fails instead of comparing equal. Verified by reverting the hoist: 2 tests fail. - Skip an empty benchmark corpus instead of evaluating `index % 0` and dereferencing undefined. - Build fixture paths with path.join. - Condense the benchmark header to purpose plus parity guarantee. Co-authored-by: Orca <help@stably.ai> * test(editor): harden doc-link performance evidence Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
2dac0741b4 |
fix(terminal): stop answering mode-2031 toggles that the same chunk withdrew (#10817)
* fix(terminal): stop answering DECSET 2031 subscriptions fish already withdrew fish enables and disables mode 2031 around every prompt (tty_handoff.rs), so a single PTY chunk routinely carries `?2031h ... ?2031l`. All three responders answered the sticky "an h appeared anywhere" flag, so each prompt cycle wrote `?997;1n` into a shell that had already handed the tty to a child — it lands as literal text, or as stdin for whatever is reading. pty-connection.ts's hidden-pane responder already had the right shape (`finalState !== 'subscribed'`); this brings the other three in line: - shared tracker: gate the '2031-subscribe' fact on the chunk-final state - parked-tab byte sidecar: same guard - visible-pane xterm CSI handler: xterm dispatches mid-parse, so there is no chunk-final state to read. Defer the reply to a microtask and re-check the subscription, letting a same-chunk `?2031l` cancel it. Refs #9993 Co-authored-by: Orca <help@stably.ai> * fix(terminal): decide 2031 replies per PTY chunk, not per xterm parse The previous commit deferred the visible-pane reply to a microtask so a same-chunk `?2031l` could cancel it. That cannot work: xterm's WriteBuffer parses every queued `terminal.write()` synchronously in one batch before any microtask runs, so the microtask sees the net state of N PTY chunks, not of the one that carried the subscribe. A TUI that subscribes in chunk N gets no reply when chunk N+1 happens to withdraw, and a fish prompt straddling two writes still gets answered. Move the decision to where chunk boundaries actually exist — pty-connection's dataCallback, which receives one PTY chunk per call. It scans raw bytes with `scanMode2031Sequences`, carrying a tail across chunks so a CSI split mid- sequence still resolves, and replies only when that chunk *ends* subscribed. Ownership stays single: gate-managed PTYs are answered by main's '2031-subscribe' fact, so the chunk scanner returns early for them, and the xterm CSI handler now observes only panes the scanner does not own. The tail is dropped on PTY replacement — a partial prefix belongs to the stream that produced it. Removes the microtask responder and the seed-reply retry path it needed. Mutation-tested: 6 mutations applied, 6 killed. * fix(terminal): carry DECSET 2031 withdrawals as a side-effect fact The previous commit moved 2031 reply decisions to the PTY chunk boundary and gave gate-managed panes a single owner: main's '2031-subscribe' fact. But the fact union is subscribe-only, and that left the withdrawal unobserved. For a gate-managed pane, main drops renderer-bound bytes after model ingestion, the chunk scanner early-returns, and xterm's CSI handler is disabled. So when a TUI emits `?2031l` while hidden, nothing retires the subscription: paneMode2031 stays set, and the next theme flip has maybePushMode2031Flip push `CSI ?997;2n` into the shell that replaced the TUI — #9993 again, through the theme-change door. Before this branch, skipHiddenRendererOutput observed those withheld bytes; consolidating ownership removed that observer without replacing it. No renderer-side observer can close this: the bytes are gone before the renderer sees them. The state protocol has to carry the withdrawal, so add a '2031-unsubscribe' fact alongside the subscribe across the three fact unions (shared, provider, daemon). It fires only on a real chunk-final withdrawal — a chunk with no 2031 bytes scans to null and stays silent. The renderer handler clears both maps and sends nothing: a withdrawal is not a query. Also closes two gaps an adversarial review found by mutation, both previously resting on comments rather than tests: the lifecycle parser-ownership predicate (extracted as isPaneParserOwnedMode2031Observer so it is directly testable) and the scan-before-reconciliation ordering that lets a chunk the snapshot drops as a duplicate still answer its query. Mutation-tested: 12 mutations applied, 12 killed (6 from the prior round re-run, 6 new covering this fix and the two survivors). * fix(daemon): refuse 2031 authority from a daemon that cannot retract it Round-2 review found a wire-compatibility hole in the original #9993 fix. Daemons survive app updates, so a new desktop can drive a daemon that was started by the previous build. Pre-v29 daemons emit '2031-subscribe' but have no '2031-unsubscribe' fact at all. For a gate-managed pane, main drops the renderer-bound bytes before the renderer sees them, so main's transient facts are the ONLY thing that can retire a subscription. Against such a daemon a TUI exiting while its pane is hidden leaves the subscription registered forever, and the next theme flip injects CSI 997 into whatever shell replaced it -- #9993 all over again, reached through the upgrade path. Gate it: bump PROTOCOL_VERSION 28 -> 29, add MODE_2031_UNSUBSCRIBE_FACT_PROTOCOL_VERSION with supportsMode2031UnsubscribeFact(), and drop '2031-subscribe' from any daemon below that floor. Trade-off: a gate-managed pane on a preserved v28 daemon keeps renderer-scanner authority instead of daemon-fact authority. That is exactly the pre-fact behaviour -- correct for visible panes, no worse than today for hidden ones -- and it resolves on the daemon's next restart. Non-2031 transient facts (bell, etc.) are unaffected at every version. Tests: two adapter regression tests (v28 drops subscribe, v29 forwards it), plus a version-pin test asserting the floor sits above every entry in PREVIOUS_DAEMON_PROTOCOL_VERSIONS -- so adding a new preserved version cannot silently re-open the hole. Mutation-verified in both directions: `false &&` (under-block) and `true` (over-block) each fail the new tests. * fix(daemon): gate background delegation, not just the fact stream A pre-v29 daemon can announce a 2031 subscribe but never retract it. Filtering that fact is not enough: while a pane is visible main's own scanner registers the subscription, and scan authority only moves to the daemon when the session is backgrounded. So the gate belongs on setPtyBackgrounded — decline to hand a non-retracting daemon authority at all, and main stays authoritative over the whole stream. Co-authored-by: Orca <help@stably.ai> * fix(daemon): clear a preserved pre-v29 background hint at attach, not just at background Co-authored-by: Orca <help@stably.ai> * fix(terminal): don't answer a 2031 subscribe whose withdrawal straddles a chunk Review found the chunk-final-state fix left one hole open. When the kernel cuts fish's toggle pair mid-withdrawal — chunk 1 ends "...?2031h prompt ESC[?20", chunk 2 is "31l" — chunk 1 genuinely ends subscribed, so it answers, and the reply lands as literal text at the prompt. Chunk 2 then recognizes the withdrawal but cannot recall bytes already written. The same byte stream is safe or corrupting purely by where the kernel split it. The scanner already retains an incomplete private-mode tail; it just didn't tell the caller whether that tail could still resolve to 2031. It now does, and a subscribe is held one chunk while the answer is still in doubt. Only subscribes defer — retiring a subscription writes nothing to the pty, so withdrawals stay eager. Deferral is narrow: a trailing "ESC[?25" (cursor hide) can never become 2031, so a subscribe already seen in that chunk is still answered immediately. This case predates the branch — the old sticky-flag policy replied here too — so it is a residual this fix now closes rather than a regression it introduced. Tests: three cases pinned (split withdrawal, non-2031 partial must not defer, split re-subscribe answers once). Removing the deferral fails only the first. * fix(terminal): preserve mode 2031 reply decisions * fix(build): record daemon protocol v29 compatibility --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
025c242f0c |
feat(dashboard): tint agent cards by state, show the project as an icon, name the chat (#11012)
* feat(dashboard): tint agent cards by state, show the project as an icon, name the chat Three glanceability changes to the agent board: - The "Needs You" signal moves from the column border onto the cards themselves, and done agents get the same treatment in green. Idle cards that simply aren't running stay neutral, so a tint always means "this one wants you". - The repo is now its own icon with the name in a tooltip, instead of a mono label that truncated and competed with the worktree name. Icons ride the snapshot keyed by repoId — image icons are data URLs, and the snapshot republishes several times a second. - The user-message line is labelled with the tab's conversation name rather than "You", resolved through the same getAgentRowConversationName the sidebar's agent rows use. Status-only titles still fall back to "You". * refactor(dashboard): head the card with the session name, move the worktree beside the project The conversation name now sits next to the agent icon as the card's heading rather than prefixing the user-message line, and the worktree drops to the footer beside the project icon. The message line reads "You" again — the name moved up, so keeping it there said the same thing twice. Cards without a resolvable session name keep the worktree as the heading, and the footer omits it rather than repeating it. * fix(dashboard): thread settings into every snapshot builder caller Adding `settings` to DashboardSnapshotState left three callers constructing it without one. The in-window drawer's was a real defect, not just a type error: useLiveDashboardSnapshot derives its own snapshot rather than receiving the relayed one, so a dropped slice silently blanks generated conversation names in the drawer while the pop-out shows them. Bucket counts pass null deliberately — they never render a conversation name, so the sidebar stays unsubscribed from settings. Covers the drawer's wiring with a test, since `settings: null` type-checks and would blank names again without failing loudly. * test(dashboard): complete the terminal layout fixture TerminalLayoutSnapshot requires expandedLeafId; the neighbouring builder test hides this behind an `as unknown as` cast on the whole state object. |
||
|
|
a72068015f |
fix(panes): stop a stray touch hijacking a mouse divider drag (#11013)
Each pointer type has its own primary pointer, so a finger on a touchscreen arrives with isPrimary true while a mouse drag is in flight and satisfies the WSLg fallback in isActiveDragPointer. Exclude touch from that fallback; the pen path WSLg needs is unchanged and a touch-started drag still matches by pointerId. |
||
|
|
f48cb78646 |
perf(store): index worktrees and tabs once per hydration loop (#10891)
* perf(store): index worktrees and tabs once per hydration loop Four sites in hydrateWorkspaceSession and reconnectPersistedTerminals rebuilt a flattened array inside a loop and linearly searched it: Object.values(worktreesByRepo).flat().find((e) => e.id === worktreeId) That is O(rows x ids) for O(rows + ids) distinct work. Build one first-wins index per loop instead. Neither loop sets state or awaits, so a single index over the store snapshot is valid for every iteration. 54.5x at a real 10-repo / 423-worktree session with 188 pending reconnects; 2.1x on a one-repo session. This runs synchronously on renderer cold start and gates workspaceSessionReady, which blocks terminal pane mounting. First-wins matters: Array.prototype.find returns the first match, so an index that overwrote on collision would resolve a different repo for a duplicated worktree id. Both the tests and the benchmark fixture carry a deliberate cross-repo duplicate so that difference is observable. Co-authored-by: Orca <help@stably.ai> * test(store): count the generated worktree rows instead of multiplying Review feedback: the table printed repoCount * worktreesPerRepo, which misses the duplicate id makeStore injects for multi-repo cases (that duplicate is what makes first-wins observable). Count the generated map, and say plainly that the fixtures are synthetic at real-world scale rather than a replay of a real session. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
1fd0f731fc |
fix(automations): bind agent terminal output before publishing, and launch SSH folder workspaces on their own host (#10818)
* fix(terminal): bind the agent PTY before the run tab is ever published Why: launchAgentBackgroundSession created the hidden run tab synchronously and only then awaited the agent spawn, so the store briefly held a tab with ptyId: null. Terminal.tsx re-renders on that write, and for an already-visited worktree the tab can neither cold-park nor defer — a TerminalPane mounts, finds no adopt candidate, and starts a fresh default shell. When the agent PTY finally resolves it is rebound in state but the mounted pane still holds the shell, so the user sees a bare prompt and the agent PTY is orphaned (#2989). Reserve the tab id before the spawn and create the tab already bound to the live PTY, with no await in between. * fix(terminal): resolve folder workspaces, and fail closed on a tab-id collision allWorktrees() reads only worktreesByRepo, so every folder workspace looked absent and its automation died at resolution. getKnownWorktreeById covers both. On a reserved-id collision, re-keying the tab could never work: ORCA_TAB_ID and ORCA_PANE_KEY are already baked into the spawned process, so routing and hook identity would permanently disagree. Retire the launch instead. Co-authored-by: Orca <help@stably.ai> * test(automations): split the background-session suite so it stays under the 800-line cap Co-authored-by: Orca <help@stably.ai> * fix(automations): route folder-workspace agent launches to their owning SSH host Review of the bind-before-publish fix surfaced a second defect on the path it newly makes reachable. A folder workspace has no repo row — its synthetic repoId is `folder-workspace:<groupId>` — so `repos.find(...)` returns null and every repo-derived launch input silently degraded to a local default: connectionId null, platform CLIENT_PLATFORM, isRemote false. The automation then spawned on the user's machine with a cwd that only exists on the SSH host. Before this branch that path threw before reaching the spawn, so the bug was latent; making folder-workspace automations work is what exposes it. Host resolution now goes through resolveAgentBackgroundLaunchHost, which falls back to the workspace scope (the same getFolderWorkspaceConnectionId that ordinary terminal creation uses) when there is no repo. Ambiguous scopes — mixed local/remote children — still resolve to a local launch rather than guessing a host. Extracted to its own module rather than inlined: the added branch pushed launch-agent-background-session.ts over the 300-line oxlint cap, and a max-lines disable is forbidden. Tests: an SSH folder workspace must spawn with its connectionId and remote cwd; a local one must stay local. Reverting the fallback fails the first and leaves the rest green. * fix(automations): close folder dispatch and adoption races Read live state before adopting a reserved tab so a collision that lands during the spawn is retired instead of re-keyed. Route persisted folder-workspace dispatch through its ambiguity-aware owner, including SSH auth, remote trust, quiet-shell fallback, and WSL shell selection. --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
974447175f | feat(terminal): add manual parking developer action (#11016) | ||
|
+21 |
b31e66ed48 |
fix(browser): survive a transient Windows lock during cookie import (#10697)
* fix(browser): retry a transient Windows lock on the Chromium cookie snapshot copy The snapshot attempt loop only reacts to a `false` return, so a throwing copy escaped it entirely. On Windows, AV/EDR briefly opens a file literally named "Cookies" with FILE_SHARE_NONE, which turns an otherwise-fine copyFileSync into `EBUSY errno -4082 syscall=copyfile` and aborts the whole import (#9355). Route the main-database copy through the existing `copyFileWithWindowsRetry`, already used for the same AV window in #1507. It is a no-op off Windows (maxAttempts=1), so POSIX still fails fast and ENOENT is never retried on any platform. * fix(browser): degrade the cookie-import staging DB instead of aborting the import Staging exists only to back the cold-restart replay for cookies the in-memory path rejects, but three points in it were fatal to the whole import (#9355): - the staging copy from the live partition DB — also a file named "Cookies", so the same AV/EDR handle blocks it; - opening/PRAGMA-ing that staged file; - both were unguarded, so a throw escaped to the catch-all and returned `ok: false` even when every cookie could load in memory. Each is now non-fatal and diagnosed. Two invariants keep the degraded path honest: `imported++` moved out of the staging insert so the summary counts importable cookies rather than staged rows, and `setPendingCookieImport` is never called when staging is unavailable — registering a path that was never written would replay a missing or partial DB over the live partition on cold start. * fix(browser): stop a degraded cookie import from replaying a stale staged database Review round 1 found the staging-degradation path could leave an older pendingCookieImports entry registered while the import rewrote the live session, so the next cold start replayed the stale DB over fresh cookies. - add clearPendingCookieImport so a degraded import retires the old entry - degrade staging on BEGIN/insert/COMMIT failure instead of aborting the import - discard the staged cookie copy on every non-registering path - pin the stagingAvailable guard, which previously survived mutation Co-authored-by: Orca <help@stably.ai> * fix(browser): report a degraded cookie import honestly instead of as a clean success Making the staging failure non-fatal introduced a silent-loss path: the import clears the live jar before loading cookies, so when staging was unavailable AND Electron rejected cookies, the user lost their old jar, got none of the new cookies, and still saw "Imported N cookies". Adds an optional `warning` to the import summary, set only on that degraded branch, and routes every cookie-import toast through a shared emitter that raises a warning toast instead of an unqualified success. Also closes three test holes found in review: - clearPendingCookieImport had no direct tests; deleting the wrong partition key survived all 10 registry persistence tests. Now covered, mutation killed. - The staging-insert-failure test was vacuous (memoryFailed === 0 suppressed registration on its own). It now forces a memory failure. - No test pinned the success-path clear; removing it survived. Now covered. * fix(github): resolve owner/repo through SSH Host aliases (#10284) (#10361) * fix(github): resolve owner/repo through SSH Host aliases (#10284) Expand OpenSSH Host → HostName via ssh -G before classifying github.com identity so PR merge works when origin is git@alias:owner/repo.git. Transport URLs stay unchanged so IdentityFile selection is preserved. Do not long-negative-cache indeterminate ssh -G failures. * fix(github): harden SSH alias resolution * Update README downloads badge * fix(persistence): fsync state writes so a rename is actually durable (#10631) * fix(persistence): fsync state writes so a rename is actually durable `Store` wrote `orca-data.json` to a temp file and renamed it. rename() is atomic for readers but says nothing about durability: without an fsync the directory entry can reach disk before the data does. After power loss or a hard crash the file can come back holding the previous state or, worse, zero bytes — and `JSON.parse('')` throws, so an empty file takes the full corrupt-file path rather than degrading. This is the same empty-file symptom as #1158 from a different cause. That issue fixed a logic path that persisted empty state and added the .bak ring as a safety net; the ring also catches this, which is why it went unnoticed. Recovery costs up to an hour of tabs/layouts/session state (backups are throttled to >=1h spacing), and a user in their first hour has no backup slot yet, so they land on defaults indistinguishable from a fresh install. Both write paths now fsync the temp file *before* the rename, then fsync the containing directory. Directory fsync is best-effort by design: Windows cannot open a directory for fsync and some filesystems reject it, so it is swallowed. The file fsync is the load-bearing part and works everywhere. Measured cost on a 3 MB payload: ~0.2 ms per write, against a 1s debounce. The async path does not block the main thread. The syscall-order test mocks `node:fs` and counts fsync targets at the module boundary, asserting ['file', 'directory'] — proving the ordering rather than inferring it from reading the implementation, since a fsync after the rename would still pass every content assertion. * test(persistence): make the syscall proof platform-aware and actually prove the order Two problems, both found from CodeRabbit's Windows observation. The assertion hardcoded ['file', 'directory']. Directory fsync is deliberately best-effort — Windows cannot open a directory for fsync and some filesystems reject it — so on Windows the helper swallows the failure, only the file fsync is observed, and the test fails. The expectation now probes the real platform instead of assuming, keeping the guarantee tight where directory fsync works rather than dropping it everywhere. Worse, the test did not prove what its name claimed. Moving the fsync to *after* the rename still passes: the file is fsynced either way, and only fsyncs were recorded, so the correct and broken orders produced an identical log. Mutation-testing the "before rename" claim is what surfaced this — the mutation passed. The rename is now recorded in the same sequence, since it is the boundary the ordering is defined against. Re-running the same mutation fails, so the ordering claim is now backed by the test rather than asserted in a comment. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> * fix(window): stop burning macOS GPU on an invisible blur effect (#8482) (#10682) Co-authored-by: Orca <help@stably.ai> * feat(sidebar): distinguish and filter CLI-created workspaces (#10712) * perf(relay): stop snapshotting the whole pending-PTY map every drain tick (#10670) * perf(sidebar): share one worktree-keyed agent orchestration index (#10678) Co-authored-by: Orca <help@stably.ai> * fix(mobile): recover unreliable relay connections (#10709) * fix(mobile): recover unreliable relay connections * test(mobile): use valid raster preview fixtures --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> * release: v1.4.157-rc.0 * docs(relay): trim the pending-output drain comment (#10714) Co-authored-by: Orca <help@stably.ai> * fix(history): quarantine unreadable recovery generations. (#10713) * fix(history): quarantine unreadable recovery files * fix(history): preserve mixed recovery generations * fix(history): reanchor reconciled live sessions * fix(history): serialize final checkpoint queue * fix(history): drain sleep shutdowns before disconnect * fix(history): restore legacy wide sessions * fix(history): preserve malformed mixed logs * fix(history): preserve malformed log tails * refactor tests to reduce file size * refactor(history-recovery): extract freeze helper and improve test robus - Extract takeRecoveryFreeze to eliminate duplicated freeze-and-clear pattern across five call sites - Skip permission-mode tests on root CI containers (chmod 0o500 doesn't block root writes) - Replace fixed sleep with deterministic wait for queued exclusive checkpoints - Distinguish ENOENT (missing) from corrupt in history metadata reads - Add ceiling-dimension restore test and torn-tail exclusion assertion - Wrap chmod operations in try/finally to prevent leaked permissions from masking test failures - Add .catch() to checkpoint promise to prevent unhandled rejections from finally re-throws * test(history-recovery): consolidate checkpoint assertions Wait for both the checkpoint call and set clear atomically to avoid a timing race where the spy fires before the set is cleared. * fix(persistence): unbreak main by expecting the new 'cli' card property in fresh defaults (#10722) Co-authored-by: Orca <help@stably.ai> * fix(sidebar): stop worktree drag from spazzing when cards resize mid-drag (#10725) * fix(sidebar): make Cmd/Ctrl+1-9 match the rendered card order when the sidebar is closed (#10693) * fix(native-chat): wrap question text and option descriptions instead of truncating (#10025) * fix(mobile): pop to home when leaving a host so the back chevron animates backward (#9723) * fix(i18n/zh): correct technical literals and clear sense errors (#10048) * feat(speech): add Korean streaming zipformer STT model (#9893) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * feat(source-control): add copy relative path (#9018) * fix(gitlab): stop refresh button overlapping dialog close X (#9445) * fix(gitlab): stop refresh button overlapping dialog close X The GitLab item dialog's SheetContent renders its own close (X) at absolute right-4, but the header refresh button sat at the header's px-5 right padding and overlapped it. Reserve pr-10 on the header so the refresh button clears the close X, and lift it -mt-1.5 so its icon aligns with the close X on the same line. * fix(gitlab): integrate sheet controls into header --------- Co-authored-by: viniciussilva <vinicius.silva@plus10.de> Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> * fix(sidebar): raise selected workspace contrast in dark mode (#8321) * feat(editor): toggle Word Wrap from file tab actions and Alt+Z (#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 #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> * fix(repo-icon): detect Tauri and WebP icons (#7942) 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. * Add bulk tab closing to mobile long-press sheets (Close Others / Left / Right) and complete the desktop tab context menus (#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> * feat: implement Cmd+Enter as commit shortcut in Source Control (#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> * Add SenseVoice speech-to-text model (Korean/Japanese support) (#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> * fix(settings): show a way back to local accounts when a remote server owns provider-account scope (#8188) Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> * feat(speech): add Parakeet TDT-CTC 0.6B JA voice model (#8207) * 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 switch bold flash and Windows lag (#10692) * 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 #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> * fix(mobile): clear native-chat composer optimistically at send time (#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 (#10689) * fix(cli): bound orchestration ask timeouts * fix(cli): harden remote timeout boundaries * fix(crash-reporting): stop fit-retry bursts from erasing the pre-crash trail (#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 #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> * fix(speech): download verified model artifacts directly (#10735) * perf(renderer): give owner-routed settings a stable identity (#10743) Co-authored-by: Orca <help@stably.ai> * perf(agent-status): validate hook payloads without the JSON round trip (#10752) * fix(gpu-fallback): make the crash window rolling, not launch-anchored (#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> * feat(sidebar): add a filter to hide detached-HEAD workspaces (#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> * feat(daemon): add daemon_lifecycle replaced/retired telemetry event (#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 #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 |
||
|
|
10ca89ac8b |
feat(updater): switch to validated local mac builds (#10889)
* feat(updater): switch to validated local mac builds * test(updater): cover local build recovery actions * fix(types): keep local build contract in project sources |
||
|
|
97cb32c1cc |
fix(terminal): release an abandoned synchronized-output frame on reveal (STA-2694) (#10907)
* fix(terminal): release an abandoned synchronized-output frame on reveal Alt-screen agent TUIs (OpenCode/OpenTUI, Codex, grok) bracket every repaint in `?2026h … ?2026l`. Hiding a pane mid-bracket — which a worktree switch or cold-park lands on routinely, since these brackets are written many times a second — leaves xterm's `decPrivateModes.synchronizedOutput` latched. RenderService.refreshRows checks that latch *before* rendering, so while it holds, every repaint Orca owns is a no-op: the forced render-pause repaint, the plain `refresh()` fallback, and the shared glyph-atlas rebuild all render zero rows while the xterm buffer is perfectly correct. Release the latch at the two reveal repaint entry points so those repaints actually paint. Also adds an OpenCode-shaped alt-screen e2e fixture and spec. The existing inline-TUI convergence spec covers the normal-buffer shape (live block glued to the bottom, history scrolling into scrollback); this covers the full-screen alternate-buffer shape, where nothing scrolls and so no row ever self-heals through the scroll path. Scope note: xterm arms a 1s watchdog that clears this latch on its own, so this closes a bounded window rather than the whole STA-2694 report. The e2e spec passes with and without the production change for that reason; the unit tests are what pin the behavior. Refs STA-2694. * fix(terminal): clear the render model on the plain-refocus repaint path `schedulePaneRevealPresent` — the atlas-preserving path a plain window refocus takes — only called `terminal.refresh()`. xterm's renderers are diff-based: `_updateModel` early-continues on any cell whose code/fg/bg/ext still match the cached model, so a refresh repaints nothing for a pane whose buffer never changed. When an occluded window loses its canvas contents while that model stays populated, the refresh skips exactly the cells that went stale and the pane keeps compositing pre-hide pixels — until a window resize reallocates the model, which is the repair users find by hand. Clear the model first (`RenderService.clear()` → renderer `clear()` → `_clearModel(true)`) so the refresh becomes a guaranteed full repaint. That drops cached cells and glyph vertices but NOT the texture atlas, which is shared by every same-config terminal and whose mid-stream wipe re-arms xterm's page-merge garble race (xterm.js #4480) — the reason this path is atlas-preserving in the first place. Also covers the DOM-renderer fallback in `resetWebglTextureAtlas`: `clearTextureAtlas()` is what invalidated the model on the WebGL path, so a pane without an addon had nothing invalidate it and hit the same skip. Scope note: the e2e spec guards buffer/geometry convergence across the hide/reveal boundaries and adds idle-agent and headful desktop-hide cases, but it cannot observe a stale canvas — both oracles built for that (canvas-vs-buffer ink sampling, screenshot-vs-forced-repaint) were proven blind by injecting the defect, and the spec header documents why. The unit tests pin the ordering and the atlas-preservation invariant. Refs STA-2694. Co-authored-by: Orca <help@stably.ai> * docs(terminal): hand off the STA-2694 reveal-artifact investigation Records both fixed defects with their xterm mechanisms, the reveal/wake call graph, why every e2e oracle for a stale canvas was proven blind, how to arm the in-app render-desync sentinel on real hardware, and the one unverified lead (dimension staleness) that would explain why a window resize specifically is the repair users find. Refs STA-2694. Co-authored-by: Orca <help@stably.ai> * Revert "fix(terminal): clear the render model on the plain-refocus repaint path" This reverts commit |
||
|
|
1fcbf8e5fe |
fix(dashboard): stop the agent icon shrinking on long card titles (#11001)
A bare <svg> flex item shrinks with its row, so kanban cards and the terminal dialog rendered a squashed ~9px agent icon whenever the worktree name overflowed. Wrap both in the shrink-0 span every other surface already uses. |
||
|
|
3716a7bb49 | fix(markdown): render task continuations as text (#11008) | ||
|
|
0ab5f499cb | fix(cmd-j): restore focus when issue match routing declines (#11010) | ||
|
|
a065db154c |
fix(sidebar): spin the worktree dot while Claude Code is thinking (#9040) (#10684)
Co-authored-by: Orca <help@stably.ai> |
||
|
|
ab60045371 |
fix(shortcuts): gate Cmd/Ctrl+N folder-workspace jumps on path status (#10748)
Reviewed with an independent reproduction, including a live-app check. Replaced tests that mocked the module containing the fix, and reused the existing workspace activator instead of adding a 4th copy of the dispatch. |
||
|
|
c0734f039d |
fix(terminal): disarm stale TUI modes when a pane confirms return to shell (#9608)
A TUI killed hard (SIGKILL, OOM, crash) never restores the modes it armed. When its parent shell survives, the emulator keeps mouse tracking, focus reporting and Kitty keyboard flags on: every pointer move over the pane lands as typed SGR motion reports at the prompt, and the doomed process burns CPU parsing the motion firehose while it lives. Orca's existing mode cleanups all hang off dead-PTY paths (hibernation kill, daemon reattach), so an agent dying under a live shell crossed none of them. Fire POST_REPLAY_REATTACH_RESET at the pane-foreground-agent tracker's confirmed return-to-shell transition, next to the sibling stale-title cleanup. That transition is gated on a real foreground-process read rather than the bare OSC 133;D, because a full-screen agent's nested command shells leak their own D onto the main PTY. The write goes through the replay guard so xterm's auto replies cannot leak to the shell as input. The reused constant already excludes ?2004l, so the bracketed-paste protection the live shell re-arms at its prompt is preserved. |
||
|
|
cdd5ceb72b |
fix(jira): render issue description/comment images with lightbox (#8938)
* fix(jira): render issue images and open them in a lightbox Jira ADF media nodes were dropped when converting descriptions/comments to Markdown, so screenshots never appeared in the Tasks drawer. Download image attachments with authenticated Jira API access, embed them as data URLs on issue/comment detail loads, and add a viewport-centered lightbox. Closing with X/Esc only dismisses the preview, not the issue sheet. * fix(jira): open comment images in the same lightbox as description Jira issue comments still used compact markdown, so screenshots rendered but could not expand. Use the document renderer for comment bodies, add a regression test for the expand control, and sync MarkdownImageLightbox locale keys. * fix(jira): harden inline image handling * fix(jira): harden inline image discovery, escaping, and downloads Address PR review findings: correct media-attachment pairing, Server/DC attachment lookup base path, markdown-safe external URLs, wider HTML discovery with gated alt fallback, concurrent downloads outside the API semaphore, and a main-process attachment data-URL cache with lower caps. * fix(jira): Option A multi-same-name attachments and post-map media warns Fix discovery so repeated alts (image.png) get distinct attachment ids, flush resolution warns after ADF mapping using attachment-only stats, clear attachment cache on clearToken with epoch-guarded singleflight, and add Server comment path plus release-before-binary regression tests. * fix(jira): simplify comment media request skip condition Only needingCount determines whether to skip the attachment metadata request — htmlIds alone cannot produce a download without needing media. Add type annotation for mediaAttrs for clarity. --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> |
||
|
|
2bb3276a35 |
fix(cmd-j): focus the destination workspace's own terminal after a jump (#10695)
Co-authored-by: Orca <help@stably.ai> |
||
|
|
24706ccff0 | fix(terminals): negotiate explicit close intent for paired runtimes (#10129) | ||
|
|
a1ad4714e9 |
fix(gitlab): render item descriptions and comments with document markdown variant (#9161)
Co-authored-by: viniciussilva <vinicius.silva@plus10.de> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8b154d686c |
perf(runtime): remove timer clamps from cooperative yields (#10908)
* perf(runtime): remove timer clamps from cooperative yields Renderer paste and input loops can schedule more than a thousand zero-delay timer yields for a maximum-size payload. Chromium clamps nested timers to 4ms, adding seconds of idle wall time. Use MessageChannel tasks in renderer runtimes and setImmediate in Node while retaining a timer fallback for tests and unsupported environments. * fix(runtime): preserve pacing and release yield callbacks Adversarial review found that concurrent producers could retain resolved callbacks until global quiescence. Route renderer yields by token and delete each resolver before resuming its producer. Keep timer pacing in terminal paste and accepted-write loops where SSH and local PTYs do not provide drain acknowledgement. Use the shared scheduler for the OpenCode scanner. |