mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 16:03:26 +00:00
8757e40063af9f69cfcf4e2e353d8b39a21c77e4
6132
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8757e40063 |
fix(native-chat): keep a structured agent's tool line between tool calls (#22349)
* fix(native-chat): keep a structured agent's tool line between tool calls A structured session's status named a tool only while the call was still running, so the sidebar's tool line went blank whenever the agent was thinking or writing between calls. Terminal agents keep naming the finished tool until the next one starts, and clear it after a failure. The structured status projection now does the same: a running call wins, otherwise the turn's newest root call if it completed. * fix(native-chat): bound the structured tool line by the running turn, not the user row A send made while a turn is running writes its user row into the journal straight away, and the turn keeps going. Stopping the scan at that row blanked the tool line while a tool was still running. The scan now runs to the turn record and names a call only when that record is still running, so a turn that already ended never lends its last tool to a pending follow-up. This lookup was the running-only lookup's only production caller, so it replaces that lookup instead of sitting beside it. * fix(native-chat): keep naming a structured agent's failed tool until the next one Clearing the tool line after a failed call brought the blank gap back for much of a turn: Codex marks any nonzero exit as failed, so a search with no match or a red test run is enough. The failure already shows on the tool's own row in the transcript. The running turn's newest running call still wins; otherwise its newest root call is named whatever it settled to. * fix(native-chat): name a structured Codex edit on the tool line as the chat draws it Once a Codex edit's changes exist, its apply_patch call becomes a diff row, which the status lookup skipped, so the row named the command before the edit. The chat's tool-call block for a journal row now comes from one shared builder, and the status lookup reads the same definition: a diff is named as Diff with its path, and counts as settled since it carries no lifecycle. * docs(native-chat): describe the structured tool field as running-or-latest The status summary's toolName/toolInput now name the running turn's latest tool between calls, not only a running one. Update the wire type and status bridge comments that still said "the running tool". |
||
|
|
51d3cafc4f |
feat(editor): add a setting to turn off preview tabs (#22398)
Single-clicking a file in the Explorer, or following a link in Markdown source, opens it as a preview tab that the next preview open replaces. There was no way to turn that off, so browsing files kept swapping one tab. Adds `editorPreviewTabsEnabled` (General -> Navigation, on by default). A caller's `preview` flag is now an intent that `resolveEditorPreviewIntent` resolves against the setting, covering every open path - files, diffs, history diffs, conflicts - in one place. Preview-ness is derived rather than reconciled: readers treat a tab as a preview only when the stored flag and the setting agree, so a flag left over from a saved session, another window, or a host switch is inert while previews are off. Nothing rewrites stored flags when the setting changes, so no settings-landing path has to remember to clean up. Fixes #22397 |
||
|
|
83dd047fd9 |
fix(explorer): find files by name in large local workspaces (#22369)
* fix(explorer): search local workspaces by file name across every file The Explorer name filter only searched remote workspaces directly; local workspaces still filtered the first 20,001 listed files, so files beyond that cap never matched in large repos. Local name queries now rank the whole workspace on the host, and fall back to an uncapped git listing when ripgrep is not installed. * fix(explorer): filter capped local listings on the host with the Explorer word rule Replaces the Quick Open fuzzy top-32 routing, which dropped multi-word matches and capped visible results. The Explorer keeps its instant renderer-side filter; only when the local listing hits its cap does it re-list on the host with the same word rule applied before the cap. * fix(explorer): keep capped matches when host name filtering fails - Fall back to the capped listing (and stop re-listing) if the host scan fails - Keep primary matches when the ignored-file pass fails during a filtered scan - Key host scans on normalized filter words; reset capped state per filter session - Bound nameFilter size at the IPC boundary; drop the double readdir walk * fix(explorer): match name filters without locale-dependent lowercasing * fix(explorer): avoid render-time ref writes in the host name filter fallback |
||
|
|
ebed0964a2 |
feat(agents): add first-class Muse Code harness (#22216)
* feat(agents): add first-class Muse Code harness Add Muse as a supervised Orca agent across desktop, mobile, session history, source control, local hooks, SSH, WSL, and native Windows. Preserve user settings, support Muse 1.3 hook environment allowlists, and recognize versioned foreground processes. Include question, waiting, completion, resume, and readiness coverage. Co-authored-by: homesh-dev <300847526+homesh-dev@users.noreply.github.com> Co-authored-by: jeffhuen <32542276+jeffhuen@users.noreply.github.com> Co-authored-by: John Cusack <johncusackccm@gmail.com> Co-authored-by: Adrien De oliveira <75085839+adriendeoliveira@users.noreply.github.com> * test(agents): cover Muse remote hook registration * test(agents): cover Muse hook and source-control contracts * test(agents): exclude Muse hook metadata from script mode check * test(agents): keep Muse skill picker coverage stable * test(ai-vault): include Muse in every-agent fixture * test(mobile): repin Muse agent icon closure * fix(muse): detect questions and approvals from structured Muse signals Muse 1.3 fires no hook for request_user_input, so a pending question left the pane "working". Its internal reminder subagents also post hooks with their own session ids (even after Stop), which surfaced "tool failed" rows and flipped finished panes back to working. - Read pending questions from Muse's session log (user_input_prompt_requested/settled) via the existing transcript poll, now generalized from Codex subagents to Muse on main and relay. - Drop child-session hooks (SubagentStart ids, or turn_id === session_id). - Treat Notification permission_prompt as the approval wait; PermissionRequest also fires for auto-approved calls, so it only caches the approval card. - Ignore Notification copy as the prompt; poll replays are not new prompts or turn boundaries. - Allowlist USERPROFILE so Windows cmd AutoRun doesn't fail every hook. * perf(muse): parse only question events from the session log Most Muse session-log lines are large model/tool records. Filter raw lines by the user_input_prompt_ marker before JSON.parse via an optional readJsonlCursor line filter. * fix(muse): unwrap batched log records and scope questions to the live turn Review follow-ups: question events inside retained_frame batches were skipped, and a question left open by a crash or interrupt stayed pending for the pane's life. Share the history scanner's retained_frame unwrapper, and only report a pending question whose run_id matches the hook turn_id. * refactor(muse): drop type assertion in retained_frame unwrap * fix(agent-hooks): satisfy exhaustive-switch lint in transcript poll policy --------- Co-authored-by: Adrien De oliveira <75085839+adriendeoliveira@users.noreply.github.com> |
||
|
|
1b85be67d8 |
feat(native-chat): notify on every settled structured turn (#22105)
* feat(native-chat): notify on every settled structured turn
A structured chat that finished while you were elsewhere lit the sidebar
but never raised an OS notification, and a notification that did arrive
for one could not open the chat it came from.
Unread and delivery now come out of the single resolveAgentAttention
decision the terminal lane already uses: the structured dispatcher calls
applyAgentAttention instead of applyAgentAttentionUnread, so the same
policy that decides what to light also decides what to deliver, through
the same sound and blocked-permission tail.
Every settled turn notifies, as the CLI lane does. Success says
"finished"; failure and cancellation say "stopped" through the shipped
agentInterrupted flag rather than a second vocabulary. A turn whose
outcome the host never stated stays unknown and lights nothing.
The host now dedupes mobile fan-out by event identity (scope, session,
turn) beside the existing per-workspace burst cooldown, so a completion
two windows both saw reaches the phone once while each window still
decides its own banner. Clicking a structured notification reveals the
chat tab: its pane key's leaf is synthetic, so focusTerminal would hunt
a split-layout leaf that does not exist.
* fix(notifications): spend each mobile gate only when it actually notifies
Two review findings on the structured-chat notification lane, both real.
The mobile event gate consumed its reservation before the per-workspace
burst cooldown ran. Two chats in one workspace share that cooldown key,
so the second chat's completion could burn its event key and then lose
the cooldown to the first chat — never announced, yet permanently marked
as announced, so a later window dispatching it could no longer reach the
phone. The gate now peeks first and records the event at dispatch, which
also keeps a known duplicate from burning the cooldown slot.
A notification id is minted from the status row's stateStartedAt, and the
row re-projects that field as the turn settles: the working episode's
start moves into stateHistory and the settled start takes its place. A
banner raised in the window before that re-projection therefore carried
an id acknowledgement never rebuilt, leaving it on screen for good.
Acknowledgement now collects ids for the row's left episodes too — the
same episodes the unread check beside it already scanned, so the two
halves finally read the same turns. Lane-neutral: the terminal lane
mints its ids the same way and had the same gap.
* fix(notifications): drop the mobile event gate and reveal chats in folder workspaces
The per-event mobile dedupe defended against one completion being
dispatched by several Orca windows. Only one renderer mounts the
structured attention bridge, the completion feed is live-only with no
replay, and any in-process duplicate lands inside the existing 5s
per-workspace burst cooldown, which already collapses mobile and
desktop alike. The gate never acted on a real sequence, so the wire
field, the shared ledger and its tests go; mobile delivery is back to
main's behavior.
A folder workspace id ("folder:<id>") has no "repoId::" prefix, so the
click binding was skipped and clicking a chat notification there did
nothing. The chat route selects its workspace itself through
ui:focusEditorTab, so it now binds without a repoId; the terminal
route is unchanged.
* fix(notifications): retire the banner ids actually dispatched, not ids rebuilt from a moved row
A banner's id is minted from the status row's stateStartedAt at dispatch, and that field moves
afterwards: a completion can outrun the settled re-projection, and a settled structured row is
re-stamped with no history entry by any later journal row (a cancel appends a status note after
the turn settles). Rebuilding ids from the row's episodes at acknowledgement missed the second
case and fanned out up to 21 mobile dismissals per pane for ids never raised.
The shared delivery tail now records each dispatched id per subject; acknowledgement retires
those plus the current-row rebuild it always had. The acknowledgement collector is back to
main's single-field form.
* refactor(notifications): retire announced notifications by subject in main
Main now records, per pane, the ids it actually announced (a desktop banner
shown or a phone alert sent) and an acknowledgement passes the acknowledged
pane keys so main retires all of them. This replaces the renderer-side record
of dispatched ids: main is where the announcement happens, so it records only
real announcements, including phone alerts whose desktop banner focus
suppressed. The id rebuilt from the current row stays as the fallback after
a restart empties the in-memory record.
|
||
|
|
0b6f60ac3b |
fix(worktrees): a listing that predates a create can no longer retire the new workspace (#22311)
* fix(worktrees): a listing that predates a create can no longer retire the new workspace Creating a workspace while another one was being deleted could land the user on the empty Landing screen with the new workspace's Claude launch cancelled. A worktree listing whose scan began before the create finished (the prepared checkout is hidden from listings while it is still locked) was applied after the create returned, and the renderer read the new worktree's absence from that authoritative listing as a deletion: it purged the workspace's tabs, tombstoned the pending structured launch (so the host session was retired the moment it published) and cleared the active selection. Renderer: each provider invocation records the local create sequence when it begins (shared by every caller that coalesces onto it). The merge no longer drops or retires a worktree this client finished creating after that point, and the change-event diff purge defers to what the merge actually dropped. Hydrated rows keep being reaped. Host: a scan a worktree mutation overtook is published as non-authoritative instead of authoritative, and the create paths invalidate scans as soon as the new worktree is listable rather than only at the end of the create. * fix(worktrees): an overtaken scan re-derives the catalog, and the create fence rides every listing A scan a worktree mutation overtook was published as non-authoritative with a failure reason, which lit the sidebar's scan-failed indicator, hid external worktree cards and emptied the visibility dialog after a routine create/delete overlap, and its early return skipped the abort, currency and dual-owner guards every other branch honours. The listing now re-scans a bounded number of times instead (the invalidation already dropped the cache, so it scans afresh or joins a sibling's scan) and past the bound answers non-authoritative through the same guard chain, with no failure reason. The renderer's create-sequence inception is now captured where the provider invocation starts and released only when it settles, so a waiter released early cannot reset it for a later joiner, and the direct-SSH scheduler's refresh -- the only listing on hosts with no scan cache -- now carries it too. A re-created worktree id is re-inserted at the back of the create-record eviction order so the bound holds. * fix(worktrees): the host re-runs a scan a worktree change overtook, for cached and SSH listings alike; the renderer create fence is removed * fix(worktrees): the runtime listing re-runs a scan a worktree change overtook, and a headless host bumps the generation it witnesses * test(runtime): the change-invalidation suite lists through the public runtime method instead of casting to a protected one * fix(runtime): the runtime's own worktree change event bumps the scan generation its listing witnesses A headless host has no window notifier, so a delete (and every other change the runtime announces) never moved the generation the runtime listing re-runs against; a listing the delete overtook published the removed row as authoritative until the next refresh. The bump now rides the runtime's change event, ahead of the send, as the desktop notifier already does. * fix(worktrees): a listing that spends its re-scan bound logs the churn on the host Past the bound the listing answered without a trace, so continuous create/delete churn could not be diagnosed from a host log. One warning names the repo and the number of overtaken scans; a caller that withdrew stays silent. |
||
|
|
4f814c1dd4 |
fix(browser): scope floating browser shortcuts to their own panel (#22361)
* fix(browser): scope floating browser chrome shortcuts to their own panel The floating panel rendered its browser with the default 'focused' scope, so a focused split browser and the floating browser both answered chrome chords. The floating browser now answers only chords from inside its own overlay, and a focused split yields chords whose target sits inside the floating panel. * test(e2e): pin floating browser shortcut scope * test(e2e): reuse split shortcut fixtures in the floating scope spec |
||
|
|
dfff3915c4 |
fix(browser): scope back/forward/reload/zoom/grab shortcuts to the originating split (#22340)
* fix(browser): scope back/forward/reload/zoom/grab shortcuts to the originating split With two browser panes visible in a split, Back, Forward, Reload, Hard Reload, page zoom and Focus Address Bar fired in every visible pane. Main forwarded these guest chords without the page id, and each split's active pane subscribed. The renderer-side listeners for the same chords were also window-wide per pane, so a key pressed in the toolbar (or in a terminal in another split) reached every active browser pane. Guest-forwarded chords now carry the originating browserPageId; preload admits only well-formed payloads and each pane ignores ids that aren't its own. Toolbar-path listeners use the same focused-split scope Find already uses. The streamed remote pane's history chord moves onto that scoped hook. Cmd/Ctrl+C grab (STA-3319) gets the same scope and no longer arms while a text selection exists outside the browser pane, so copying from the native chat transcript works again. * refactor(browser): simplify split shortcut scoping per review Drop the preload payload admission (main and preload ship together), fold the three inline scope checks into browserChromeShortcutOwnsEvent, and replace the outside-overlay selection check with a plain live-selection rule so Cmd+C copies from surfaces that do not move split focus. * refactor(browser): share one zoom command type and tidy shortcut comments BrowserPageZoomEventDetail and BrowserPageZoomCommand were the same shape; keep one in shared/browser-page-zoom.ts and route guest and local zoom through a single handler. * refactor(browser): narrow the zoom event with instanceof instead of a cast * test(e2e): pin split-scoped browser shortcuts Two browser splits (and a terminal beside a browser) now prove that Back, Forward, Reload, Hard Reload, page zoom, Focus Address Bar, and the element grab chord act only on the split that sent them, from both the guest page and the browser toolbar. A native chat selection proves Cmd/Ctrl+C copies instead of arming grab. Split fixtures move to a shared helper so both specs reuse them. |
||
|
|
7cb4380179 |
fix(sidebar): hang the subagent chevron in the card gutter (#22331)
A top-level agent with subagents drew its expand chevron inline before the status dot, pushing the dot, icon, and title right of every other agent row. The chevron now hangs in the card's left gutter so all top-level rows share one status column, and leaf rows no longer reserve a spacer. |
||
|
|
4c696a1e2a |
fix(agent-status): a structured session with live child work reads as working (#22295)
* fix(agent-status): a structured session with live child work reads as working An idle native-chat session whose subagent was still running showed a green check in the sidebar, the collapsed worktree pill, and worktree ps, while a terminal Claude session in the same situation showed working. The two lanes folded child work into the parent's status with different code: the hook listener did, the structured lane did not. Both lanes now share one child-work liveness vocabulary and one lead-status fold. Live agent work makes a settled lead working; shells and monitors alone make it monitoring. The structured lane derives liveness from the background task list already on the wire, in both its readers, so the sidebar, the CLI, the dashboard and mobile agree. The Claude task-kind table is one shared file covering both the hook inventory and SDK stream names, and the renderer bridge reuses the shared child-work projection instead of carrying its own copy. * fix(agent-status): a blocked or out-of-contact subagent still holds its session working Child-work liveness retired an agent-kind child on any state but working/monitoring, while the shell beside it stayed live on everything except done/idle. A subagent waiting on a permission prompt, or one whose host lost contact, therefore counted for less than a backgrounded sleep and let the session read done. Both kinds now share the settlement rule `resolveAgentChildWorkFreshness` already reads rows by: only an explicit done/idle retires child work. Also keep empty task labels out of the shared background-task projection candidate, so a host that publishes `name: ''` cannot beat the child-row fallbacks. * test(agent-status): pin the widened hook-inventory agent names, and correct two stale claims The hook inventory now classifies through the shared kind table, which also maps the SDK stream's `local_agent` / `local_subagent`. Nothing pinned that widening, so add cases for all four agent names — including `teammate`, whose pane state stays `done` under the #8825 idle-squat rule. Two comments the fold made false: - the teardown marker rule's comment claimed it could not disagree with what the UI calls working; it is deliberately lead-only, so now it says that and why; - the agent-status store reference still described the structured row's `state` as the deleted `structuredAgentSessionStatusState`, and omitted the `workingMode` the ingest now writes. * fix(agent-status): the state clock restarts when monitoring becomes a real turn `stateStartedAt` carried forward whenever the prior `state` matched, which was sound while `state` meant "a turn is running". Now that it folds in child work, an idle lead watching a `sleep 3600` publishes `working`/`monitoring`; the user's prompt 45 minutes later keeps `state: 'working'`, so the row inherited the watch loop's clock and read "Working for 45m" the instant the turn began. Monitoring is its own displayed label (`worktree-card-compact-agent-row.tsx:40`), so the continuity key is now the whole published work identity — state AND workingMode — in both writers. Also record two facts the code stated wrongly: the structured lane's `interrupted: false` is inert (a projected session status has no interrupted member) rather than a decision, and the child-work liveness rule's escape hatch is the roster's session lifetime, not a settled state. * fix(agent-status): a workflow is watch work, and child work dates itself Two defects the fold introduced. `isAgentChildWorkKind` counted `workflow` as agent work, so a structured session whose only live task was a backgrounded `local_workflow` published a full working spinner while the children projection — which admits `kind === 'agent'` only — rendered nothing to expand, and the same workflow in a terminal pane showed the monitoring badge instead. The repo already decides this: `isClaudeSubagentTask` excludes workflows by name, and MATERIALIZED_TASK_KINDS leaves "the backgrounded shell command and the workflow" to the non-agent owner. The predicate is now `kind === 'agent'`, and the three sites that restated the same test route through it, so a new kind is decided in one place instead of three that merely agree. `evidenceObservedAt` dated every row by `summary.updatedAt`, the journal's last activity. The journal cannot date child work: its clock stopped when the lead's turn did, so a genuinely live roster aged past the 30-minute staleness window and mobile's dot decayed a running session to idle. The fold now reports whether child work alone holds the row open, and only then does the host's observation clock stand in — keeping "a restart's republish is not new evidence" for lead turns. * fix(agent-status): the sidebar dates child work the same way the host does `fromChildWork` reached the host ingest but not the renderer bridge, so after ~30 minutes of live child work with no journal activity the sidebar's row aged into staleness while `worktree ps` and mobile stayed fresh — two writers for one session answering differently, which is the defect this PR exists to remove. For a remote host the client's own receipt time is also the more honest clock, since the journal stamp is the host's and is never comparable against this machine's now. |
||
|
|
18696bd875 |
fix(native-chat): hide delete for structured history (#22106)
* fix(native-chat): hide delete for structured history * test(native-chat): fix duplicate session action suite |
||
|
|
9ae7a3de14 |
test(agent-launch): pin per-call-site behaviour of the launch funnel (#22116)
A regression net for the twelve production call sites of launchAgentInNewTab, written before the agent.launch migration rewrites the funnel's internals. Each call site is characterized as a profile — the argument object it builds — and fed through the REAL funnel, so the assertions are about the observable contract rather than about internals the migration will move. A census re-anchors every profile to its call site by source markers, so a caller that changes what it passes fails loudly instead of drifting out of coverage. Dimensions pinned per call site: resolved surface (terminal / structured / host-published), built command and argv, the permission-bypass flag inside the arguments string, prompt transport (argv / post-ready paste / environment), submit bit, delivery-promise exposure, group placement, activation, tab-bar order, initial cwd, and telemetry launch source. |
||
|
|
b57facc5bc |
Fix stale listings when search query changes (#22173)
* fix(renderer): prevent stale listings when query changes Bundle files and truncated state with the request key that produced them. Only display a listing when its request key matches the current query, preventing previous results from leaking through before the new request completes. * fix(renderer): associate loading state with request key When a query changes, there's a gap between the render and the effect that starts the new request. During this gap, the listing becomes empty (because it's scoped to the request key), but the loading indicator was stale. To fix this, track loading per request and also report true if this render is about to start a new request — so the empty listing correctly appears as "loading" rather than "no results". |
||
|
|
90d363afc9 |
fix(renderer): contain Monaco initialization failures (#21555)
* fix(renderer): add defensive error handling for Monaco editor crashes Analyzed 34 crash reports for v1.4.205 released 2026-09-17. Identified and added defensive fixes for React error boundary crashes in Monaco editor setup. - Error: ReferenceError: thũs is not defined - Location: Monaco editor initialization (editor.api2 bundle) - Platforms: Linux, Windows, macOS - Root cause: Undefined variable in Monaco setup or language registration - Status: Added try-catch to prevent cascade crash - Pattern: Cascading process deaths (network service + GPU service) - Platforms: Primarily Windows - Root cause: Infrastructure/concurrent process failure (not code defect) - Status: Documented, requires Electron/Chrome infrastructure review - Pattern: Renderer memory grows to 851MB on low-RAM Windows systems - Root cause: Memory exhaustion on systems with <2GB free RAM - Status: Existing memory monitoring detected; needs leak investigation - Status: Requires minidump analysis with source maps 1. Added try-catch to Monaco editor mount callback (use-monaco-editor-mount.ts) - Catches errors during editor initialization - Logs file path and error for better diagnostics - Prevents crash cascade to React error boundary 2. Added try-catch to Monaco language registration (monaco-setup.ts) - Catches errors during Vue/Svelte/Astro/Nim language registration - Logs failures without crashing Monaco setup - Allows app to continue even if optional features fail - Analyzed 34 crash reports across 3 categories - Examined crash dumps, diagnostics, and memory profiles - Reviewed Monaco setup and editor component code - Checked git history for recent changes - Crash breadcrumbs (memory, process state, user actions) - Process metrics (heap, private memory, system memory) - Component stacks (React error boundaries) - Exit codes and system signals - Error silently continues instead of crashing: Users get degraded experience instead of app crash, can still use editor in most cases - May hide underlying issues: Errors are logged for crash reports, but won't be surfaced as prominently - Type checking: pnpm tc:renderer (passed) - Changes preserve existing error reporting through crash breadcrumbs - Defensive coding only adds try-catch, no behavior change for success path * fix(renderer): keep Monaco mount failures inside error boundary * fix(renderer): isolate Monaco setup failures * fix(renderer): contain Monaco mount failures at the editor surface The try/catch around the onMount body did the opposite of containment: React already routed that throw to the page boundary, so swallowing it left a half-wired editor and hid the crash from the reporting pipeline. It also never saw the reported failure, which is raised inside @monaco-editor/react's own create effect before onMount runs. Revert the hook to main and wrap the editor element in RecoverableRenderErrorBoundary instead, so either throw degrades the file pane only, still files a crash report, and retries by remounting on the existing pane+path key. * refactor(renderer): drive Monaco setup steps from one guarded table Ten near-identical guarded calls, each repeating its own function name as a label, become one [label, step] table run by a single loop. Same behaviour: an optional registration that throws is logged and the rest still run. loader.config and the editor model registry stay unguarded — they are load-bearing, so catching there would only move the failure later. * fix(renderer): breadcrumb swallowed Monaco setup-step failures A guarded registration that throws was console-only, so a lost language or behaviour guard never reached crash reports. Record a breadcrumb so the containment stays visible in the field. Claude-Session: ab8ff806-4870-4ea8-bbf5-bbd123b1166e --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com> |
||
|
|
0232c03c43 |
Refactor editor header file rename to breadcrumb morph UI (#21265)
* Refactor editor header file rename to a breadcrumb morph UI - Display full breadcrumb (repo name + parent dirs) during rename for context - Separate basename field from extension suffix to clarify what users edit - Replace blur-to-commit with explicit confirm/cancel buttons - Auto-attach extension to basename; respect explicitly typed extensions - Add comprehensive tests for rename scenarios and edge cases * Fix markdown rename: drop blur-commit, handle IME, track active file - Blur no longer commits, preventing accidental renames when focus moves - IME composition keys properly handled for CJK input support - File switches during rename now cancel the operation - Extension display improved to show final filename - Comprehensive test coverage added for edge cases * Simplify markdown rename to inline field without breadcrumb or buttons Replaces breadcrumb-morph rename with confirm/cancel buttons with a simple inline field accepting full filenames. Commits on Enter, blur, or Escape to cancel, matching tab bar and file explorer behavior. Adds renameCancelledRef to prevent blur-commit after Escape. Removes unused i18n strings for buttons and simplifies state by dropping extension pinning and breadcrumb display. * Handle blur race when file changes during rename When switching files mid-rename, React may deliver the old input's blur event after the new file renders, causing a stale rename commit. Mark the rename as cancelled when the active file changes, and add test coverage verifying stale blur events are ignored. * Test blur-race condition in hook unit test Move blur-commit-after-file-change test from EditorPanelHeaderPath integration tests to useEditorHeaderFileRename unit test. Tests the blur-handling logic at the hook level where it belongs. |
||
|
|
9d4039b8c5 |
Lower filter chip contrast to indicate read-only status (#21751)
* fix(cmd-j): lower filter chip contrast to indicate read-only status The scope is seeded from the sidebar, not chosen in the command palette, so the chips should read as metadata rather than action pills offering to undo an action the user never took. - Redesign chips with reduced visual weight: no border, no background color - Add "Scoped to" prefix and muted text color to clarify metadata role - Separate chips with subtle dots instead of relying on spacing - Hide dismiss icon (X) until hover/focus to further de-emphasize the action * refactor(cmd-j): add visual icon to filter indicator - Add ListFilter icon anchor for better discoverability - Improve filter chip button styling with better hover/focus states - Move label to screen reader only for cleaner interface * refactor(cmd-j): adjust filter chip appearance and i18n Refine filter chip styling with adjusted height, padding, and colors to de-emphasize read-only state. Add "scoped to" label and complete internationalization coverage across all locales. * improve filter row alignement |
||
|
|
7650abe224 |
fix(macos): tell the user when Orca's terminal service can't read their folder, and walk them through the fix (#21923)
* fix(macos): tell the user when Orca's terminal service can't read their folder
On macOS, a terminal daemon that survived an app update can be refused access to
a workspace under Documents, Desktop, or Downloads while the Orca app itself can
still read it. Terminals opened there die with "Operation not permitted" and
nothing on screen explains why. The daemon has reported `cwdReadableByDaemon` on
every create since #18043 and main has emitted `daemon_pty_cwd_denied` on proven
divergence since then; the field data says 1,438 users hit it in 21 days. What
was missing was the notice.
The verdict itself moves off `access()`. A grant-less probe on an affected
machine showed a TCC mode where `access(R_OK|X_OK)` passes on `~/Documents` and
`opendir` still fails, so the check now does what a shell listing its cwd does:
`opendirSync`, one `readSync`, `closeSync`. Only EPERM/EACCES reads as denial —
a missing path, a non-directory, or an unexpected error still reads as readable,
so a non-permission failure can never masquerade as one. The same probe is what
the app side compares with, through one oracle shared by the telemetry emitter
and the notice, so the spawn path reads the directory once.
Proven divergence now also records evidence in main: one entry, keyed by the
daemon's pid, start time and launch nonce, carrying an opaque digest of that
identity and the folder class. No path leaves main. The existing focus-time
`macTccAttribution` poll carries it to the renderer, which raises a second toast
latched per daemon scope: dismissed stays dismissed, and a restart mints a new
identity so the poll returns null and the toast clears with no post-restart
probe. If the replacement daemon is denied too, about 31% of cases, the next
spawn re-records under the new scope and the notice returns, now with the
re-allow sentence doing the work.
No new IPC channel, no daemon protocol field, no polling change, and nothing new
on the spawn path beyond one `opendir`. `daemon_folder_access_notice` counts
shown, dismissed and open_manage_sessions against `daemon_pty_cwd_denied` as the
denominator; `shown` is emitted from main the first time a scope leaves the IPC
handler, so the renderer carries no telemetry plumbing for it.
* fix(macos): clear folder-access evidence only when the same folder class reads back
A readable spawn in ~/code said nothing about a Documents denial but was
hiding the notice; retire the evidence only when the daemon reads a folder
of the class it was denied on.
* fix(macos): say what a terminal-service restart actually does
The Manage Sessions restart confirmation still described the product as it was
before agents resumed themselves: it promised panes showing "Process exited"
that the user reopens by hand, and mentioned legacy-protocol sessions nobody
outside the daemon code can act on. Open terminals and agents come back on
their own now, so the old copy made a routine remedy sound like data loss.
It also called the thing a "daemon". The same restart is about to be offered
from a user-facing fix dialog, so both surfaces now say "terminal service", and
the confirm button is just "Restart".
The new body adds the one fact the old one never stated: terminals on remote
hosts are not affected. Translations of the two changed strings are dropped so
the five non-English locales fall back to English rather than keep showing copy
that is now wrong.
* feat(macos): give the denied-folder notice a fix the user can follow
The folder-access toast told the user their terminal service could not read
Documents and then handed them a paragraph: restart from Manage Sessions, and
if that does not work, re-allow Orca in System Settings. Both halves were
guesses. Roughly a third of restarts do not fix it, and the user had no way to
know which case they were in before spending every open terminal on finding
out.
Main can now answer that. `daemon-folder-access-probe.ts` forks a short-lived
child of the app binary the same way the daemon itself is forked, runs one
opendir/readdir/closedir against the denied path, and prints a single JSON
line. macOS attributes a TCC grant to the process that forked the child, so a
child of the app running now answers exactly the question the running daemon
cannot: would a replacement daemon get in? The child goes through the shared
child-process wrapper, never a shell, with a 3s deadline, a 1KB output cap and
an environment scrubbed to PATH/HOME/TMPDIR. Every failure — timeout, bad
output, spawn error — reads as `unknown`, never as a verdict.
That answer rides out as `restartWillHelp` on the evidence the existing
focus-time poll already carries, and the toast becomes a title and two buttons:
Fix… and Not now. Fix opens a dialog with the two real steps. When the grant is
already in place, step one is shown as done and Restart is live. When it is
not, step one is open and Restart is disabled until it completes — which it
does by itself, because the poll re-probes while the answer is still no, and
returning from System Settings is the moment that lands. An unanswered probe
never accuses the user of a missing grant; it leaves both steps open.
Restart calls the management API directly rather than stacking the Manage
Sessions confirmation on top, since the dialog already states the consequence.
Success replaces the steps with a done line and takes the toast down; failure
says so inline and leaves the button usable.
System Settings opens through the existing developer-permissions pane opener,
which takes an id rather than a URL, with Files and Folders added to it. The
event's action enum now also counts fix_opened, settings_opened,
restart_clicked and — emitted from main when a replacement daemon's first spawn
lands in the folder class the previous one was denied on — whether the restart
actually worked.
* fix(macos): let the folder-access notice return after a poll that read no daemon
A daemon identity reads as null during any reconnect blip, and the poll reports that as
"no mismatch". The notice dismissed itself and then never showed again for that daemon,
because the once-per-daemon latch still held its scope. Only "Not now" should latch.
* fix(macos): say what the folder-access notice costs the user
One line read like a stray warning. The toast now says who is blocked and what fails,
and still leaves the steps to the fix dialog.
* fix(macos): give the folder-access toast one action and the X, like every other toast
"Fix" is the only button; the X dismisses. Sonner fires onDismiss for programmatic
dismissals too, so the post-restart takedown now goes through the store and the hook,
and only a user's X is counted as dismissed.
* fix(macos): keep the fix dialog's steps a checklist and put the one action in the footer
Buttons inside each step made the list look like a form, and a footer Close duplicated
the X. The footer now carries the active step's action, with a ghost Cancel; a probe
that could not answer says so under step 1 instead of showing a check.
* fix(macos): let the checklist show the fix landed instead of saying so
A hedged sentence addressed to the user read like chat. On success both steps check
off and the footer offers Done; the unanswered-probe helper is a status, not advice.
* chore(i18n): drop the fix dialog's unused close key
* Revert "chore(i18n): drop the fix dialog's unused close key"
This reverts commit
|
||
|
|
0677271709 |
fix(orchestration): reap leaked worker terminals via process-incarnation fallback — stops an unbounded PTY/process leak on Remote Server (OOM / cgroup PID exhaustion) (#18790)
* fix(orchestration): remint live handle from process incarnation on worker release When a durable terminal handle goes stale (rendererGraphEpoch fence), inspectWorkerTerminal re-mints a live handle via resolveTerminalHandleByProcessIncarnation + matchesProcessIncarnation so release/stop/read act on the still-running PTY instead of reporting missing and leaking the agent process tree. - keep main shared host-scope re-exports; add matchesProcessIncarnation - wire observation.terminalHandle through control/stop/release - rebuild release-completion on main structured paths - on missing/unattached + provably exited: settleDead fence first, then same-incarnation settleWorker fall back (archive may block settleDead mid-request); settle before recovery defer * fix(orchestration): derive SSH host scope from the reminted handle; reuse fresh-request recovery guidance for structured workers Addresses two open CodeRabbit review comments on PR #18790. inspectWorkerTerminal read the dispatch authority with the stale durable terminalHandle, so after a remint the lookup resolved nowhere and currentHostScope was always undefined — an SSH worker with no liveness verdict and no persisted host_scope got classified from terminal.connected instead of unverifiable. It now reads the same effectiveHandle every other observation in the function uses. stopStructuredWorkerForRelease told the caller to repeat the release with the same --retry-request, which only replays the stale release_unknown receipt and made a structured-worker close failure permanently unretryable. It now sources releaseUnknownRecovery from worker-release-completion so the fresh-request-ID guidance lives in one place. Pre-commit lint-staged (oxlint + oxfmt) run manually: clean. * test(orchestration): exercise incarnation recovery through runtime paths * test(orchestration): pin the incarnation read scenario to the reminted terminal The read scenario only asserted that the call resolved, so it documented nothing about which handle the read reached. Assert that the handle readTerminal received resolves to the registered pane and incarnation, so the scenario proves the read went through the reminted terminal instead of passing on the incarnation fence's throw. * refactor(orchestration): drop redundant incarnation prefix check; require liveTerminalHandle * feat: add freebuff as a first-class TUI agent (#42) <!-- orca-pr-loc --> <!-- Programmatic LoC summary. Do not edit by hand; rewritten on every commit. --> | | Files | Added | Deleted | Net | | :--- | ---: | ---: | ---: | ---: | | Test | 0 | 0 | 0 | 0 | | Prod | 28 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$37 | 0 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$37 | <!-- /orca-pr-loc --> ## ELI5 Add Freebuff (`freebuff`) as a recognized first-class TUI coding agent in Orca alongside Codebuff and other supported agents. ## What Changed - Registered `freebuff` across shared TUI agent definitions, configuration catalogs, display names, and telemetry schemas. - Added agent icons, favicons, status mappings, and mobile asset references for Freebuff. - Added localization strings across supported language packs (`en`, `es`, `fr`, `ja`, `ko`, `zh`) and updated locale translation policy. - Documented Freebuff CLI in README agent table (`npm i -g freebuff`). ## Why Freebuff is a CLI coding agent twin of Codebuff (`npm i -g freebuff`). Adding it to the catalog enables users to launch worktrees, run automated sessions, and pick Freebuff directly within Orca. ## Linked Issue N/A ## Visual Proof `N/A` - Catalog registration and metadata definition for CLI agent launch; UI rendering uses existing TUI agent picker and status components. ## Testing - Verified TypeScript contracts, schemas, and catalog configurations. - Tested CLI detection / agent picker integration locally on Linux (`worktree create --agent freebuff`). ## AI Disclosure Assisted by AI coding tooling. ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why (including ELI5) - [x] Before/after screenshots or videos attached for UI changes, or `N/A` with reason - [x] Self-reviewed for correctness, security, and performance - [x] Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A) --------- Co-authored-by: Lesley Murfin <lesley@revivebusiness.ca> * test(orchestration): erase method overloads in worker reap fixtures * test: document worker fixture type boundaries * test: simplify worker fixture typing --------- Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> Co-authored-by: svc-orca[bot] <313947298+svc-orca[bot]@users.noreply.github.com> Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
6ce7208b98 |
fix(sidebar): stop a workspace with a structured chat reading as asleep (#22098)
* fix(sidebar): stop a workspace with a structured chat reading as asleep A workspace whose only surface is a structured native chat showed the sleeping moon, lost its pull-request glyph to it, and would vanish entirely under the hide-sleeping filter. hasActiveWorkspaceActivity asked three terminal-shaped questions: a tab in tabsByWorktree with a live PTY, a browser tab, or a fresh non-done agent-status row. A structured chat answers none of them. Its tab lives in unifiedTabsByWorktree, so the PTY term never sees it, and an idle session projects state 'done', which is exactly what isFreshNonDoneAgentStatus refuses. Both chats finishing their turn was enough to draw the moon. Add a fourth term keyed on the chat EXISTING. Not on a live provider child: that child is held only while the chat's pane is visible and is evicted 15s after it is not, so keying on it would flip the glyph on every worktree switch and report a process recycle the user never sees. The transcript, and the session's ability to take the next send, outlive the child. The term goes in the shared predicate, not the card, because the moon, the hide-sleeping filter and the Cmd+J palette all read it and must not disagree about which workspaces are asleep. Supporting moves, no behaviour change: the projection sits beside its siblings in visible-worktree-activity-inputs, and buildVisibleWorktreeOptionsFromState moves to its own module, which leaves the filter a pure function of its options and keeps both files under the 300-line cap without raising it. * fix(sidebar): keep structured chats visible across workspace surfaces * refactor(sidebar): keep jump palette inputs below lint limit |
||
|
|
15472cd4c6 |
feat(native-chat): keep restart recovery available in status bar (#21397)
* feat(native-chat): keep restart recovery available in status bar * fix(native-chat): source the restart offer from the host and retire it on recovery Closing the reconnect dialog spent the durable recovery offer, so looking around before deciding lost the recovery for good. The offer now survives a close, and the status bar carries it — but a durable offer needs a way to die, and it only had a reconnect, an explicit dismiss, and a 24h expiry. The claim's launch-scoped lifecycle moves into its own collaborator, which splits what the host ADVERTISES from the evidence it holds. A resume-capable hold that hands a marked chat its provider child back is the recovery the offer existed to perform, so it stops being advertised and stops being written back at quit, while the marker stays valid evidence — a user who reopened a chat can still ask the agent to carry on. Teardown re-derives the snoozed offer rather than round-tripping raw markers, and this teardown's own witness now outranks the stale claim for the same chat instead of being overwritten by it, which was silently persisting an old turn id and making the next launch refuse the chat that was actually mid-turn. On the renderer the candidate list gets its own producer against agentSession.restartResumable, so the status entry and the dialog read one host-owned answer instead of the dialog pushing its local state at a sibling. The entry re-reads the host before reopening, so a reopened list can never name a chat the host would now refuse; dialog open becomes the external one-shot request rather than a flag mirrored into render state, which is what let a reopen replay the launch answer and re-offer chats already reconnected. Dismiss all is quiet rather than destructive, saves the preference like every other exit, and reports a write the host never confirmed instead of trapping the dialog open. * fix(native-chat): keep a durable offer a launch never read, and settle the one a continuation spent Teardown replaced the recovery capsule with whatever this launch still owed, and a launch that never read the offer owes nothing — so a quit after a failed first read, a disabled flag, or a window that never mounted deleted a recovery the user was never shown. The write-back now distinguishes "claimed and still owed" from "never claimed": the first is re-derived as before, the second carries forward verbatim, because nothing revealed those sessions and the predicate would refuse every one for want of a journal nobody opened. Reconnect and continue spent the same claims Reconnect does but never shrank the offer, leaving the status bar counting chats the host had already handed back and sending the user to an entry that re-reads, finds nothing and does nothing. * fix(native-chat): stop a teardown answering for an offer it could not read Two ways the write-back deleted a durable recovery offer nobody had seen. A take that FAILED left the claim holding an empty list and reporting that this launch had answered for the offer. The markers were still on disk, unread and unknowable, and teardown then overwrote them with its own empty list. It now writes nothing at all unless it has a witness of its own. `owed()` read "has the capsule been touched" where it meant "did anything here LOOK at the offer" — and its own write-back read counted. Teardown is retried when a phase fails, so the second attempt re-derived carried markers against a session map eviction had already emptied, refused every one, and wiped what the first attempt had just carried forward. The flag is now set only by the paths that actually read or act on the offer. The mock guard for the carry could not fail: it indexed the session it claimed nothing had revealed, so re-deriving passed and the verbatim carry was never the reason it went green. It now runs against no indexed session, which is what an unread offer looks like. Also drops the `Not now` row from the preference table, where it was paired with a dismiss method it no longer calls, and asserts the same thing where the snooze is already covered. Splits the marker predicate's journal reader out of the resume host, which was at its line ceiling. * fix(native-chat): clear the corrupt recovery capsule the take refused A capsule whose contents no longer parse made take() throw before it ever reached the clear, so the bad file survived every launch. Nothing else rewrites it now that a teardown owing nothing readable declines to write, and the freshness filter runs after the parse, so the 24h window could not release it either: one corrupt file refused recovery forever. Clear it inside the same transaction that failed to read it, then rethrow, so the poison dies on the next launch while callers still see why the take failed. A clear that fails is swallowed rather than allowed to mask the parse error. Refusing to expose partial candidates is unchanged, and a read that fails for any other reason still writes nothing. * feat(native-chat): make resume the one restart action, and make it actually resume The restart prompt offered two actions: "Reconnect all", which reattached and sent nothing — exactly what opening the chat already does — and "Reconnect and continue", which reattached and asked the agent to carry on. The vacuous one is gone, the "Not now" button and the info popover with it, and the feature is now called resume throughout. "Don't ask again (resume automatically)" now runs the action the button runs: the launch calls agentSession.restartContinue instead of agentSession.restartResume, so the preference means what it says. Several comments asserted the opposite as a structural guarantee and are corrected. agentSession.restartResume stays: no in-app caller is left, but it is a published wire method a non-desktop or older client can call. * fix(native-chat): label the resume button with the number of chats selected The button read "Resume all" whenever every chat happened to be ticked, which described the selection rather than the action. It always acted on the selected chats only. Now it always names that count, with a singular variant so one chat does not read "1 chats". * refactor(native-chat): drop the reconnect vocabulary the resume action left behind Resuming became one action — reattach and ask the agent to carry on — so the notification helpers no longer need to be told which action they are reporting. Every caller passed `continue`; the `reconnect` branch, its helper and its catalog keys are gone. The dialog and the launch path had grown two copies of the same call: same RPC, same response shape, same announce-and-settle. That now lives once in the store module that owns the offer, which also takes the dismiss call, leaving the modal presentational. The two copies had drifted — only the dialog's caught a malformed payload — and the unified one keeps the defensive reading. No behaviour change. `agentSession.restartResume` stays: it is a published wire method even though nothing in the app calls it. * refactor(native-chat): derive the resume selection instead of intersecting it The modal's selection was intersected back against the host's candidate list before every action, as a guard against naming a chat the host never offered. That guard could never fire: the selection was already derived from that same list, so the intersection was the identity. The array of chosen ids is now the derived value and the lookup set falls out of it, which makes the property structural rather than checked. The helper had no other caller and is gone, along with its three tests. Three tests mocked the resume response in the shape the old API returned. Two never reached that branch at all; the third only passed because the unreadable shape happened to exercise the malformed-payload path. All three now use the real shape, and the malformed-payload behaviour — report an unconfirmed delivery, leave the offer standing — gets a test that says so. Also: the candidate reader took two trailing optional parameters, so one caller passed a placeholder `false` to reach the second; they are an options object now. `isFolderWorkspaceId` had no caller outside its own module and is no longer exported. `RestartActionOutcome` only ever describes a continuation row, so it is named for that. `dismissAll` set a busy flag that nothing could render, since it closes the dialog first. Several comments repeated an argument already made in the module they point at. Settings: the automatic-resume description is one sentence again. No behaviour change. * fix(native-chat): make restart recovery explicitly durable * fix(native-chat): preserve dismissal fence across new interruptions |
||
|
|
0bbaadafa4 |
Clear website annotations after successful delivery (#22060)
* Clear website annotations after prompt delivery Capture annotation snapshot at send time and selectively remove only the captured objects when delivered. Preserves edits and additions made during in-flight delivery. * rm design doc * Clear only delivered browser page annotations Annotations are now explicitly passed to the clear handler, allowing it to remove only delivered annotations by identity. This preserves any annotations edited or added after delivery began. * Distinguish delivered vs user-cleared annotations Add removeDeliveredBrowserPageAnnotations to remove only delivered annotations while preserving concurrent user edits. Simplify clearBrowserPageAnnotations to clear all annotations for a page when user explicitly clears. |
||
|
|
6dc00702d2 |
refactor(floating-workspace): launch the default agent through the shared launcher (#21390)
* refactor(floating-workspace): launch the default agent through the shared launcher The floating workspace titlebar agent button drove tab startup itself: it built its own `buildAgentStartupPlan`, created the tab, queued the startup command and rebuilt the tab-bar order by hand. That is a second copy of what `launchAgentInNewTab` already does for every other "start an agent here" button, so a launch-point change had two places to land. The button now calls `launchAgentInNewTab` and keeps only its own placement: selecting the tab inside the floating panel's unified group and focusing it. `launchAgentInNewTab` gains an optional `activate` so a caller that places the tab itself can keep the new terminal out of the global selection. The floating panel needs this — activating would move the main window's active tab to a tab it does not show — and it matches the other floating tab creators, which already pass `activate: false` to `createTab` and select via `activateTab`. Two behaviours change, both fixes: - tab-bar order now goes through `persistAgentLaunchTabOrder`, which reconciles editor and browser tabs. The hand-rolled loop rebuilt order from terminal tabs only, dropping the floating workspace's markdown and browser tabs. - the startup plan now carries the resolved Windows shell, so argument quoting matches the shell the PTY actually gets. * test(agent-launch): pin the floating button as a launch funnel caller The census exists so a new launchAgentInNewTab caller is a deliberate act. This entry is a bypass converging, not a bypass appearing: the button previously hand-rolled the helper's terminal arm against queueTabStartupCommand. * fix(agent-launch): preserve floating terminal launch boundaries * fix(agent-launch): honour the chat-view default in the floating workspace The floating launch button now routes through the shared launcher, so it should inherit the same launch policy as every other caller. The previous review pass added a `workspaceKind === 'floating'` guard to `decideInitialAgentTabViewMode`, which silently dropped `openAgentTabsInChatByDefault` (and the user's model and effort preferences) for that one button. That guard was not justified. `canToggleNativeChat` has no workspace-kind gate, so a floating terminal can already be switched into the chat view by hand, and `TerminalPaneNativeChatPortal` mounts into the pane's own container — the panel already renders it. Refusing the setting at launch while the same view sits one click away in the same panel is an inconsistency, not an invariant. Structured sessions stay out of the floating workspace on the pre-existing blocker in `resolveStructuredNativeChatSupport`: those open an `agent-session` tab, and the floating panel renders no such surface. * test(agent-launch): record why floating routes to the terminal-backed chat lane * refactor(floating-workspace): record why the launch does not take the global selection * refactor(agent-launch): lift launch execution-context resolution into its own module |
||
|
|
da982a4eb0 |
fix(native-chat): navigate to open history sessions (#21283)
* fix(native-chat): navigate to open history sessions * test(native-chat): provide structured session predicate |
||
|
|
73b726c64f |
refactor: use generic VirtualizedList in conflict review (#22092)
Replace SourceControlVirtualFileList with a reusable VirtualizedList component, and update related constants and test IDs to reflect the generic nature of the component. This extracts the virtualization logic to a shared utility that can be used across different features. |
||
|
|
7047cdc0d4 |
refactor: virtualize artifacts list with reusable component (#22061)
Extract SourceControlVirtualFileList to a generic VirtualizedList component and apply windowing to the artifacts table for efficient rendering of large lists. Add aria-setsize and aria-posinset announcements to windowed rows when opted in. |
||
|
|
297cfe0cf3 |
fix(usage): price GPT-6 Astra, and declare when the Codex cost total omits a model (#22073)
* feat(usage): price GPT-6 Astra token usage gpt-6-astra was missing from MODEL_PRICING, so normalizeModelForPricing returned null and estimateCostUsd dropped every event on that model from the total. Stats & Usage showed ~$0 for hundreds of millions of tokens with no unpriced indicator, since hasInferredPricing only covers a missing model name, not a missing table entry. Rates are the published ones: $10 input / $1 cached input / $50 output per 1M, with the >272K long-context tier at 2x input and cache and 1.5x output, which the existing tier fields already express. Fixes #22005 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(usage): say when the Codex cost total omits an unpriced model A daily row whose model has no `MODEL_PRICING` entry gets a null cost, and `buildSummary` simply skips it. As long as one other row is priced, `hasAnyBillableCost` is true, so the Codex card prints a confident dollar figure that silently leaves those tokens out. That is how GPT-6 Astra usage read as near-$0 before the entry landed, and it is how the next unpriced model will read too. `hasInferredPricing` does not cover this: it only fires when a rollout has no model name at all, and its label ("inferred pricing") describes a guess, not an omission. So the summary now carries `hasUnpricedModels`, set when a row has a model name and no price, and the estimated-cost card appends "• excludes unpriced models" — the same bullet-suffix idiom the breakdown rows already use for "• inferred pricing". The number stays; it stops claiming to be the whole bill. * fix(usage): caveat the Overview total too, and only when a remainder exists Review of #22073 found the Codex caveat stopped at the Codex tab. The Overview tab prints a combined total across providers and already has a "- some model prices are unavailable" line, but `hasPartialCost` only noticed a provider whose whole cost was null. A Codex range with one unpriced model among priced ones kept a real number, so the line stayed hidden and the Overview repeated the same confident, incomplete figure. `UsageProviderOverview` now carries `hasPartialCost` — set from `hasUnpricedModels` for Codex, false for the providers that cannot yet report it — and the reduction ORs it in. No new string. Second, the Codex card could read "n/a • excludes unpriced models" when nothing at all was priced. "Excludes" promises a remainder, and there was none. The suffix now also requires a non-null total; that case is still declared, on the Overview, through the null-cost path. `unpricedCostLabel` becomes `costCardLabel`, since it holds the plain label whenever there is nothing to qualify. --------- Co-authored-by: Alfred212121 <58665898+Alfred212121@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1a3f4e88c0 |
fix(opencode): support v2 plugins under plain executable name
Supports OpenCode 2 installed as opencode, including plugin loading and quick-command submission. |
||
|
|
72a1b148c1 |
fix(settings): make terminal theme selection override Ghostty colors (#22069)
* fix(settings): clear terminal overrides when selecting a theme * test(settings): cover light terminal theme override reset |
||
|
|
2739246058 |
feat(native-chat): light the unread indicators when a structured chat finishes (#21924)
* feat(native-chat): light the unread indicators when a structured chat finishes A structured native chat had no attention producer. The PTY lane reaches the unread markers through use-notification-dispatch, whose liveness reads PTY state and whose admission requires terminal panes, so a structured session — which runs on the execution host with no renderer PTY — could finish a turn with nothing lighting anywhere. A backgrounded chat was the worst case: with no mounted pane there was no reader to notice at all. The host derives the completion, because only the host can. The journal keeps committing whether or not a renderer holds a reader, so the new feed observes each commit at StructuredAgentSessionClientDelivery.publishJournal and emits on every running -> settled transition. That edge runs after the subscriber loop and independent of it, which is exactly why a chat nobody is watching can still complete. It is a separate capability-gated stream rather than a field on the status summary: the summary carries no turn identity and no outcome, and is re-broadcast on every status change, so folding a completion into it would make every status consumer a completion consumer. ONLY `success` LIGHTS ANYTHING. Outcome is A0's provider verdict and is never inferred: a turn the host merely watched stop carries no outcome and produces no event, because absent means UNKNOWN. `completed` alone proves nothing — a provider reports its own API error as a finished turn — so the host emits nothing for it and the renderer filters again on the way in. RECOVERY IS LIVE-ONLY. Nothing is retained, queued or replayed on either side. A subscriber learns what settles while it is subscribed and nothing else; on reconnect it re-opens an empty stream and whatever landed during the gap is gone. A retained completion would be a durable "unread is owed" obligation with nothing to retire it, and a reconnect would then light the dot for work the user already read. Tests on both sides pin this so a later refactor cannot quietly turn it into catch-up. The dot itself reuses the neutral policy in attention/agent-attention-policy and #21274's structured surface adapter, so suppression, acknowledgement and addressing keep exactly one implementation and the surface key is never omitted to evade a check. No second suppression rule is introduced. OS delivery is deliberately not wired: this calls applyAgentAttentionUnread, not applyAgentAttention. Also narrows the completion feed's journal dependency to the newest-turn reader it actually uses, and adds journal.newestTurn() beside the existing activeTurnId() on the one shared by-sequence scan rather than a second scan. * test(cross-version): register the turn-completion subscribe on the wire manifest The cross-version gate asserts the structured surface's method list by name and count, so an additive method has to be declared there deliberately. Adding the entry makes the suite call it in both skew directions and stubs the host side, which is the statement the gate exists to force. * fix(native-chat): rebaseline completion feed after rewinds |
||
|
|
f7955e81ff |
feat(conflict-review): virtualize large conflict file trees (#21920)
Implement windowing for the conflict review file tree using SourceControlVirtualFileList to efficiently handle large merge conflicts. Add comprehensive tests for virtualization behavior including scrolling, collapsing, and dynamic updates. |
||
|
|
91e6e1f355 |
fix(native-chat): collapse a finished turn to its answer (#22029)
* fix(native-chat): collapse a finished turn to its answer A finished turn's "Worked for N" row hid the turn's tool runs and nothing else. Every sentence the agent said on the way to its answer stayed in the transcript, so the resting state of a long chat was the narration, not the reply — one 16m 56s review turn left 21 assistant messages and roughly seven screens of scrolling behind a control that reads as if it had put the work away. The fold's unit is now the turn. A settled turn draws its prompt, its duration, and its answer; the narration and activity that produced it sit behind the caret. The answer is the turn's last assistant row that renders prose — derived, because the journal carries no marker saying which message is the reply. Collapsed stays derived rather than stored: nothing closes the disclosure when a turn ends, it arrives closed because the turn gained a duration. A running turn therefore folds nothing and the reader watches the work as it happens, which is what already happened and is now stated rather than inherited. Rows that outlive the turn that started them stay outside the fold — a spawn roster and a background task are often the only record of how that work ended. So does the reader's own message, question receipts, and the turn's diff rollup. A turn that produced no prose folds whole, its status row standing as the anchor. Two presentation changes come with it, both about the opened view: - A settled run's header was a call count followed by a monospace list of tool names and arguments. It is now one sentence in the transcript's own type — "Read 7 files, ran 17 commands, and searched 4 times" — built on the tool-category vocabulary that already picks the row's glyph, so the words and the icon cannot claim different things. A run of one command keeps that command as its header. - A tool call now owns its result instead of standing beside a separate `Result` row, so an opened run lists the work rather than twice as many rows half of which say `Result`. Output is one more click. Pairing is positional — a result answers the most recent unanswered call — because result blocks carry no call identifier to match on. Command previews also lose the `/bin/zsh -lc "…"` wrapper they all opened with. The unwrap happens inside `summarizeToolInput`, before truncation, because the clip at 80 characters removes the closing quote that proves the wrapper; one site fixes the header, the rows, and the running label. Measured on a real session journal at 1200x900: the turn above goes from 6,300px across 51 rows to 452px across 2, the whole session from 8,151px to 2,138px, and the same turn opened from 18,540px to 11,131px. The fold derivation lives in `src/shared` so the mobile transcript can read the same rule; wiring mobile's list to it is not part of this change. * fix(native-chat): preserve FIFO tool result pairing * fix(native-chat): keep tools collapsed when opening turn * test(native-chat): clarify independent tool disclosures |
||
|
|
7da9788c83 |
fix(updater): send the gh token and cache the release picker's build list (#21902)
* fix(updater): send the gh token and cache the release picker's build list The dev build picker listed releases through api.github.com with no Authorization header, so it spent GitHub's 60/hour per-IP bucket that every unauthenticated caller on the same network shares, and it refetched on every settings mount and channel click. When that bucket ran dry the picker showed "No builds found" with a rate-limit line even though GitHub was healthy and the user's own token had its full quota. Attach the local `gh auth token` when there is one so the request draws from the user's 5000/hour bucket, fall back to unauthenticated on a rejected token or a spent token bucket, cache the list per channel for five minutes in the main process (the refresh button forces a reload), classify 403 by the rate-limit headers, and say when the limit resets. Fixes #21898 * fix(updater): don't trip breaker for secondary rate limits GitHub sends x-ratelimit-remaining: 0 on both primary and secondary limits. Secondary limits carry Retry-After and shouldn't block all core gh commands — only the primary limit should trip the shared breaker. * Scope gh rate limits to execution environment * Add build list cache hint to release channel settings Inform users that build lists are cached for 5 minutes and they can refresh to check for new builds immediately. This makes the cache behavior visible and explains why a manual refresh is necessary to bypass the cache. |
||
|
|
83a031c081 |
refactor(agent-status): delete two launch-config accessors left with no callers (#22032)
#21844 removed the Codex launch-argument attention suppressor, which was the last production caller of two launch-config lookups. Both were left in place for a follow-up; this is it. `getAgentLaunchConfigForStatusMetadata` (renderer store) looked a launch config up from a loose metadata bag. Its sibling `getAgentLaunchConfigForStatusEntry` takes a real status entry and still serves the one live consumer, cold restore resume startup. Deleting the metadata accessor also orphaned its `getLaunchConfigForStatusMetadata` helper and the `AgentLaunchConfigStatusMetadata` parameter type, so those go too. `getAgentStatusLaunchConfigForPaneKey` (main runtime) returned a pane's launch config behind a launch-token fence. Its two remaining references were assertions in the launch-authority retirement test. They were a second view of a state bit the test already pins: retirement nulls `pty.launchToken`, and the surviving `verifyOrchestrationCompatibilityCaller` assertion fails when it does not. Verified by ablation — disabling only the `launchToken` nulling fails that assertion with the accessor already gone, so no coverage is lost. Retirement never cleared `launchConfig` itself, so there was no second property hiding in those assertions. Test mocks that existed only to satisfy the removed store method are stripped; the tests themselves are about other behaviour and stay. No behaviour change. |
||
|
|
f07bf8544c |
feat(session-search): sort search results by newest, and break relevance ties by recency (#21863)
* feat(session-search): sort search results by newest, and break relevance ties by recency Results were ordered by match score alone with the session id as the tiebreak, so equally good matches came out in an arbitrary order and nothing ever favoured recent work. The Sort menu now offers Most relevant and Newest while the box has text; the engine already knew both orders and the all-computers merge already honoured the newest one, so only the panel had to ask. Under Most relevant, equal scores now go to the newer session. The choice persists with the other view options, separately from the list's own Last updated / Created sort. * fix(session-search): label results by the order they are in The header subtitle and the results group said "best matches" whichever sort was chosen; under Newest they now say so. The panel's scope state and its two context effects move to use-ai-vault-panel-scope.ts, which keeps the panel under the line cap and gives that behaviour a name. * feat(session-search): move search sort onto a results bar above the hits Search mode gets a bar in the group header's place: the hit count on the left, a ghost menu button on the right that names the current order and opens the two-item radio group. The filter menu's Sort section keeps one meaning again (Last updated / Created), the header subtitle stops reporting sort, and search rows run flat with no group header. * style(session-search): drop the icons from the results-bar sort menu and match its text size * feat(session-search): one sort bar above the list in both modes Filters stay behind the header filter icon; sort moves onto the bar directly above the session list, in browse mode as well as search. The bar is mode-agnostic: it takes a label, the selected value, a typed option list, and a callback, and the panel configures it twice. - rename AiVaultSearchResultsBar to AiVaultSessionListBar and generalize it - add ai-vault-sort-options for the two option lists and their aria labels - drop the Sort section from the filter menu and stop counting sort in the badge - header subtitle now reads "Indexed history" in both modes * feat(session-search): count sessions plainly and offer Show more when the scan fills its depth * fix(session-search): step history depth 250 at a time and keep Show more visible while the rescan runs * style(session-search): let the sort menu hug its two options * fix(session-search): show more reads the depth its rows came from The row inferred "a deeper rescan is running" from the selected depth minus one page, which at the default depth is zero, so every foreground scan with at least one session painted a disabled "Loading more sessions…" footer the scan had room for. The scan now publishes the depth it ran at beside its sessions, and the row compares the two: it survives the rescan because that depth trails the selected one until the deeper scan lands. Drops the stepping arithmetic and nextAiVaultSessionLimit, and moves the row out of the menu file it was sharing. * refactor(session-search): an untitled group is what hides a header Search mode said "no group headers" twice, in two files, both keyed off the same flag: an empty label in the filters hook and a hideGroupHeaders prop on the list. The label is now the only fact. A null label means the group has no header of its own, the list renders its rows flat, and the prop is gone. The shared group type keeps its string label so the mobile sections that map it are untouched; the nullable label is the renderer list's own type. * refactor(session-search): plain labels, and a browse bar that can report zero Three small simplifications around the list bar: - The browse bar is guarded on the loaded history rather than the filtered rows, so "0 of 250 sessions" can actually appear when filters hide everything and the sort control stays reachable. Search keeps its own guard. - The two count labels were components whose whole body was a ternary over translate; they are functions returning a string, and the bar's label prop is a string. - The persistence guards stop being exported with no caller outside the file, and the search-sort guard reads the AI_VAULT_SEARCH_SORTS list instead of respelling the union. |
||
|
|
2a785274ad |
fix(remote-runtime): stop an unhydrated host graph from reading as a closed terminal (#21967)
A remote pane asks the host two different questions about its terminal, and they consult different amounts of state. `session.tabs.list` hydrates first — `listMobileSessionTabs` runs the workspace-session hydrate with `allowAttachedWindow: true`, then restores live paired-renderer terminals. `session.tabs.activate` hydrates with no options, which is a no-op whenever an authoritative window exists, so it answers `tab_not_found` from state the host never filled in. The client treated that as removal evidence and surfaced "Remote terminal was closed." over a terminal the same host was still reporting connected and writable. Activation's absence answer is now non-authoritative: the bounded inventory poll below adjudicates. A surviving sibling leaf with this leaf gone still returns removal evidence and still surfaces the toast; nothing conclusive inside the window stays unknown liveness, which parks a retry instead of asserting closure. `null` leaves both return unions. Fixes #21852 |
||
|
|
4feaaf5c5c |
feat(terminal): configure interactive Unix shell arguments (#21904)
* feat(terminal): configure interactive Unix shell args * fix(settings): clarify Unix shell argument defaults * fix(settings): improve Unix shell argument guidance * fix(settings): simplify shell argument guidance * fix(settings): clarify empty shell args * fix(settings): explain empty shell args * feat(settings): make shell argument modes explicit * fix(settings): keep no args inside custom mode * fix(terminal): apply configured shell args on the renderer spawn path The renderer's pty:spawn handler builds options in ipc/spawn-options, not the runtime controller, so the configured profile never reached a terminal pane. The local launch plan also dropped the args whenever shellOverride was set -- which the spawn path always fills from terminalDefaultShell. Both spawn paths now share one resolver. * chore(i18n): allowlist the new terminal shell argument strings Matches how the sibling Terminal shell settings strings are already handled. |
||
|
|
ac4d6b407a |
fix(devin): skip workspace trust for Orca launches (#21925)
* fix(devin): skip workspace trust in yolo launches * fix(devin): migrate existing workspace trust defaults * fix(devin): persist migrated launch arguments * fix(i18n): include required source control stop label |
||
|
|
1b77838d1c |
Consolidate source control tooltips to eliminate redundant hover text (#21733)
* refactor(source-control): consolidate tooltips to avoid duplicates - Remove native title attributes from buttons that also render Radix tooltips - Introduce PrimaryActionTooltip wrapper that decides whether to show a tooltip based on context - Hide pure repeats: enabled Stage All and Create PR have no tooltip since the label already states the action - Show tooltips only when they add information: disabled reasons, commit shortcut, and remote counts * Show Create PR intent tooltip to explain the prepare step The Create PR intent label doesn't convey that clicking it stages, commits, and pushes before opening the PR dialog. Keep the tooltip to surface this multi-step operation that users might not expect. |
||
|
|
afb618f2b3 |
refactor(agent-status): drop two superseded Codex attention workarounds (#21844)
* refactor(agent-status): drop two superseded Codex attention workarounds Codex fires its PermissionRequest hook as decider #1, before its own auto-reviewer and before the user, so the event never meant "a human is blocked". #21389 fixed that at the source: the execution host reads the turn's approvals_reviewer from the rollout at write time and keeps a reviewer-owned approval in `working`. Two older reader-side workarounds for the same bug are now redundant. The launch-argument suppressor guessed auto-approve mode by string-matching the launch args, then dropped the status row in the reader. It only matched Codex's bypass flag, and under that flag Codex's approval policy is `Never`, which takes the Skip path and fires no PermissionRequest at all. When the user turns on "Approve for me" inside a live session the args never change, so it never fired for the actually-reported case either. The Codex-only 1.5s notification quiet window could not do its job: measured auto-reviews take 3-20s and a human can answer in under a second, so no fixed constant separates them. Its deferred callback also re-checked liveness and returned without notifying, so a genuine prompt whose pane went non-live inside the window was dropped rather than delayed. Codex now notifies synchronously like every other agent. Also types the coordinator's completion state from the controller's exported CompletionState instead of asserting each field, which the changed-lines casting gate required once those lines moved. * fix(agent-status): settle transient process-exit evidence |
||
|
|
4da3a95d50 |
fix(native-chat): scope a tool row's hover reveal to that row (#21918)
Hovering one tool call in a chat turn revealed the expand chevron on every other row in the same message at once, so the whole message lit up and nothing said which row the click would open. The rows were reading a hover they do not own. Tailwind's unnamed `group-hover:` is not nearest-ancestor scoped — it compiles to `:is(:where(.group):hover *)`, which matches a hover on ANY `.group` ancestor. `NativeChatMessageRow` wraps the whole assistant message in a bare `group` for its own copy/timestamp reveal, so every collapsible row nested inside it answered to that wrapper as well as to itself. Each row now names its own group — `group/tool-line`, `group/tool-run`, `group/subagent-run`, `group/diff-card` — which compiles to `:is(:where(.group\/tool-line):hover *)` and reaches that row alone. The message-row reveal is left bare on purpose: its copy button and timestamp are meant to answer to a hover anywhere in the message. `NativeChatDiffCard` is included for the same defect, not as extra scope: its verb label was brightening on any hover in the message. |
||
|
|
8cf1c8594e | docs(opencode2): clarify quick command delivery (#21929) | ||
|
|
70c4f20466 |
fix(opencode2): auto-submit quick command prompts
OpenCode2 quick commands now submit through the ready-state delivery path. Focused regression coverage and full CI pass. |
||
|
|
98299d879b |
fix(terminal): persist a parked remote pane's scrollback across a hard restart (#21295) (#21367)
* fix(terminal): route a parked pane's scrollback patch to the remote host's partition A park capture changes only terminalLayoutsByTabId, so its debounced session patch carries no tabsByWorktree. splitWorkspaceSessionByHost built its tab->worktree index from the patch alone, resolved nothing, and routed every layout to the 'local' partition, where main's pruneLocalTerminalScrollbackBuffers strips scrollback it cannot attribute to a remote worktree. The remote host's runtime:<id> partition never received the capture, so anything parked since the last clean checkpoint was lost on a crash, SIGKILL, or a forced kill during an app update (#21295). Route tab-keyed patch fields with the renderer's live tab catalogs as a fallback when the payload names no tab rows. Payload rows still win, so full-payload writes are byte-identical. Once routed to runtime:<id>, main merges the partition's own prior tabsByWorktree and the prune preserves. Proven by tests/e2e/paired-remote-terminal-parked-scrollback-restart.spec.ts: a hard kill (no checkpoint) then relaunch, asserting the capture is in the remote host's partition on disk. Mutation: reverting the routing fix turns that assertion red and fails the 3 catalog-dependent unit routing tests. (cherry picked from commit |
||
|
|
f492054bf0 |
fix(runtime): an outage is not a handle-gap verdict (#20059)
* fix(runtime): an outage is not a handle-gap verdict
The per-pane handle-gap wait releases at a 15s deadline and records that
expiry as a verdict, which authorises the sleeping-agent resume. The
connection generation was the only thing voiding that verdict, and a plain
disconnect never advances it — runtime-status.ts advances on the reconnect,
under a new runtime id. So a network drop mid-turn expired the wait with a
generation that still matched, and the replay forked a second `--resume`
onto the transcript the host was still writing: #19735 through the
disconnect door.
Suppress the verdict while the client positively knows it is out of contact,
reusing the shared runtime-host connection derivation. The waiter still
releases and re-parks, so contact returning gets a full fresh budget and the
pane is still decided on real silence.
Not redundant with the landed-handle drain that follows this commit, nor with
the read-time pane identity from adv2-skew (
|
||
|
|
4feca6baa1 |
Keep new worktree dialog actions visible while scrolling (#21915)
* Keep new worktree dialog actions outside scrolling content * Trigger missing PR checks |
||
|
|
9324bb8137 |
fix(editor): preserve Markdown preview when following wiki links (#19790)
* fix(editor): preserve preview when following wiki document links * test(editor): avoid cast in markdown navigation fixture --------- Co-authored-by: Neil <neil@stably.ai> |
||
|
|
2872c3fccc |
fix(claude): prefill continuation context for manual submission (#21912)
* fix(claude): prefill continuation context for manual submission * fix(agents): force draft paste when inline prefill falls back |
||
|
|
253f0e3946 |
Fix Antigravity source-control model discovery and retired defaults (#21606)
* fix(antigravity): discover current source-control models and use CLI defaults * fix(antigravity): gate configured models on remote runtime support * fix(runtime): forward default TUI agent for remote git generation * test(runtime): cover inherited agent forwarding |
||
|
|
00da5fd556 |
test(worktrees): add comprehensive nested lineage coverage (#21903)
- Add 10 test cases for nested worktree rendering and collapse behavior - Handle edge cases: cycles, uneven siblings, multiple depth levels - Extract stopNestedWorktreeCardBubble to shared header-event-guards module |
||
|
|
ffb79c71e0 |
fix(editor): open plain details blocks in rich markdown mode (#19784)
* fix(editor): allow plain details blocks in rich markdown mode * fix(editor): preserve case-sensitive details class values |