mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 16:02:29 +00:00
2a68ea94967caf926aa60ebbd2bb32797bebb146
12739
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2a68ea9496 |
refactor(terminal): one commit module for terminal topology closes, with a boundary check (#25329)
* refactor(terminal): move the topology revision and leaf-lookup helpers into terminal-topology
advanceTerminalTopologyRevision, findTerminalTabIdForLeaf and
hasHostAuthoritativeTerminalMembership move verbatim from the renderer-save
membership rebase into persistence/terminal-topology, the home of the commit
boundary. Importers are repointed; no behavior change.
* test(terminal): test-only guard for topology writes outside the commit boundary
The three session sinks now publish through one commitWorkspaceSessionPartition
helper, which hands the prior and published partition to the topology write
guard. Production never arms the guard, so each sink pays one global lookup.
The unit suite arms it in report mode: a sink write that changes class-(a)
topology (membership, root, bindings, titles, incarnations, sleeping records,
remote session ids, tombstones, default-applied, revision) outside a commit
scope is attributed to its writer's file by stack, and fails the test unless
the writer is on the unrouted-writers allowlist that later routing PRs shrink.
Renderer saves and test seeding are exempt. Deep-freeze is available but stays
off suite-wide until the in-place writers return new sessions.
* refactor(terminal): add the topology commit module with bindLeaf, closeLeaf and closeTab
terminal-topology-commit.ts is the boundary for class-(a) terminal topology.
bindLeaf forwards to persistPtyBinding, whose write now runs in a commit
scope; the spawn commits and the relay reattach bind through it. closeLeaf and
closeTab wrap the existing close mutation in a commit scope and one
persistence.terminal-topology span (kind, outcome, refusal reason; no ids),
and the runtime close goes through them. Session output is unchanged: tests
compare it byte for byte with the old writers for local, ssh: and folder
workspaces.
* chore(terminal): drop an unused lint suppression from the topology write guard
* fix(terminal): attribute topology guard writers relative to the repo root
The guard read a frame's file through its last /src/ segment, so a test under
tests/ (folder-upgrade-identity-persistence.unit.test.ts) had no source frame
and failed as an unknown writer. Frames are now taken relative to the repo
root the setup passes in, tests/ counts as test seeding, Windows backslashes
are normalized before the node_modules skip, and nested src/ paths keep their
full path. The two runtime funnel files skip only their funnel function, so
another writer in them still shows. The R9 allowlist key names the file whose
frame actually writes. The class-(a) diff and the attribution move into their
own files; the stack limit is restored in a finally.
* refactor(terminal): drop bindLeaf until binding reaches a sink; add the boundary ratchet
bindLeaf and the commit scope inside persistPtyBinding changed nothing: the
binding write never reaches a session sink, and a scope inside the Store method
would have admitted every direct caller once it did. Both return in B1-4; the
spawn commits and the relay reattach call persistPtyBinding directly again.
The runtime close now calls one closeLeafOrTab entry, so its callbacks keep
their contextual types. A census test is the primary enforcement: only
persistence/terminal-topology and the callers it lists may call
persistPtyBinding, the session setters or the three sinks, and every
unrouted-writer allowlist entry must name an existing file.
* fix(test): resolve the topology guard's repo root without the global URL
Under happy-dom the global URL is not Node's, so fileURLToPath(new URL(...))
threw in the setup file and failed every happy-dom test file.
* refactor(terminal): drop the runtime topology write guard; the boundary ratchet enforces
The AST boundary ratchet is the enforcement for B1. The stack-attributed
runtime guard, its class-(a) diff, the unrouted-writer allowlist, the vitest
setup and the freeze option are removed; it saw two writers in the whole unit
suite and its real value starts only once binding reaches a sink (B1-4).
Also: one closeLeafOrTab wraps the close in the span (no per-kind copies or
narrowed types), the span has one finish like persistence.pty-binding, the
sink helper is publishWorkspaceSessionPartition (it publishes; the commit
boundary is the module), the ratchet drops the private publishSession row and
checks that every listed caller file exists, and the close comparison keeps
its two meaningful cases with span cases chosen by name.
* refactor(terminal): trim the B1-1 commit module and ratchet to what they enforce
- Point the acknowledged-tab-retirement audit fixture at the moved
advanceTerminalTopologyRevision; its old import no longer resolved.
- Drop publishWorkspaceSessionPartition: it was the removed guard's
interception point, so the three session sinks return to origin/main.
- One traced(kind, mutate) wrapper in the commit file replaces the span
factory; closeLeafOrTab is one call.
- The ratchet walks src/main with the shared scanSourceTree, drops the
loading-store-internal rows and the redundant file-exists test; exact-set
equality already fails on a missing file.
- The commit test is a pure unit test of the span outcomes: no Store
harness, electron mock or self-comparing close.
* refactor(terminal): census the runtime session controller's write and drop stage ids from comments
The controller's setter was named set, which the boundary census could not
list without matching every Map.set, so a new OrcaRuntime mixin could write
sessions through it unseen. Rename it setForWorktree and census it.
Comments now describe state instead of citing plan stage ids.
* refactor(terminal): census writer references and trace refusals by callback
- traced() takes refusalOf instead of assuming an Error refusal, and only
mutate() sits in the try, so a span outcome of threw means the write threw.
- The boundary ratchet counts references, not just direct calls: non-null
calls, bracket keys, aliases, destructures, .call/.bind and parenthesized
callees all count; declared names and type positions do not.
- Census terminalSurfaceCloseMutation (boundary-only) and the partition sinks
setLocalWorkspaceSession / setHostWorkspaceSession.
* test(terminal): count writer uses in extends clauses and instantiations, skip type-only imports and local declarations
The census skipped ExpressionWithTypeArguments as a type, which also holds
`extends f(x)` and `x<T>` value expressions. Type-only import/export
specifiers and declared names (variables, parameters, accessors, enum
members) no longer count as uses. The audit fixture is listed in the table
instead of a separate exemption.
* test(terminal): count quoted and assignment-pattern destructures of layout writers
* test(terminal): count every mention of a layout writer except its definition
Telling definitions from uses per syntax kind kept missing nested and
for-of destructures. Exempt only the writer's own function or class-member
definition; any other mention (including object-literal keys) counts, so the
census errs toward a loud false alarm rather than a silent miss. Quoted names
count only in member-name position.
* test(terminal): count every string literal naming a layout writer
Member-name positions missed wrapped keys like store[('name')] and
store['name' as const]. Counting every string literal outside types is
shorter and errs toward a loud false alarm.
* test(terminal): exempt only class members and functions as writer definitions
Object-literal methods and accessors were exempt while equivalent arrow
properties counted; all object-literal keys now count alike.
* test(terminal): parse files with unicode escapes in the writer census
A name spelled with a \u escape never appears verbatim, so the text
prefilter skipped it.
* test(terminal): parse any file with an escape in the writer census
\x, identity and line-continuation escapes also decode to a writer name
without it appearing verbatim.
|
||
|
|
d17351401d | Update README downloads badge | ||
|
|
e2da3a1eb1 |
fix: prevent IME confirmation from submitting text fields (#25480)
Keep IME confirmation out of text-field submission and command selection. Reuse shared gesture ownership, preserve marked-release redispatch, guard overlay Escape capture, and retain Markdown save shortcuts. Verified with independent adversarial reviews, 891 tests across 71 files, six hidden Electron tests, project typecheck, full lint, the changed-code quality gate, and all final-head CI checks. Fixes #25035. Co-authored-by: kambarakun <kambarakun@gmail.com> |
||
|
|
1569e062d9 |
Reduce repeated terminal scans and unused Qoder test imports (#25425)
Name the local hook confirmation function to avoid misclassifying internal selectors as dialog text. |
||
|
|
fa180a9ee2 |
feat(jira): pick the assignee when creating an issue — Automatic, me, or anyone (#24376)
* feat(jira): pick the assignee when creating an issue — Automatic, me, or anyone
The new-issue dialog gains an Assignee picker: Automatic (Jira applies the
project's default assignee), one-click "Assign to me" resolved from the
target site's stored identity, or any user via search. The selection resets
with the custom field values on project/type switches, since account ids
are site-scoped.
Creates now also send userFieldKeys, so the host shapes user-typed values
into the {accountId}/{name} refs Jira requires — previously the dialog
never passed them and user create fields were sent as bare strings. Keys
are only named when a value is present, keeping older remote hosts without
the user-fields capability working for assignee-less creates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(jira): address assignee review — project-scoped assignable search, per-site picker remount, keep assignee across type changes
- Search the create dialog's assignee picker against /user/assignable/search?project=…
(new listAssignableUsersForProject, IPC split into jira-user-search.ts for the
max-lines budget); remote environment targets fall back to the existing
site-wide search since older hosts have no such RPC
- Remount the picker per target project so cached results from the previous
project or site cannot be selected after a switch
- Reset the picked assignee on project selection (account ids are site-scoped)
instead of on every create-fields reload, so issue-type changes keep it
- Hide the assignee field and omit it from the payload when the target create
screen does not accept an assignee — Jira rejects fields absent from the screen
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(jira): scope create assignee to the effective project and provider
* fix(jira): isolate assignee search candidates across provider changes
* fix(jira): search assignable users with Server-compatible project keys
* fix(jira): refresh assignee identity with provider connection status
* fix: distinguish hook confirmation from browser confirm
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Neil <neil@stably.ai>
|
||
|
|
8b5de39217 |
Submit initial OpenCode 2.0.12 prompts through the native startup plugin (#25428)
* fix(opencode): submit initial prompts for reviewed 2.0.12 Route the reported release through the existing native startup intent. Retain conservative behavior for other unreviewed versions. * Wait for OpenCode startup fixture file reads in the hydration test --------- Co-authored-by: nwparker <nwparker@users.noreply.github.com> |
||
|
|
40f9e7a3fc |
fix(codex): resume account switches without blanking the terminal (#25485)
fix(codex): restart account switches without blanking or repinning the pane Integrate the atomic pane replacement from #24350 and adapt the explicit conversation resume from #14877. Keep terminal protocol replies flowing while account notices block user input. Ordinary restores retain provenance. Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Co-authored-by: itisvincent <vincentcgamer@gmail.com> Co-authored-by: rayim <rayim@fxy.global> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b3328390f9 |
Remove completed SSH picker E2E plan (#24824)
Co-authored-by: kazooooo-ma <zooooomer-com@gmail.com> |
||
|
|
c2c7649849 |
fix(mobile): stop Android from selecting words while the chat transcript scrolls (#22871)
* fix(mobile): stop Android from selecting words while the chat transcript scrolls On Android every paragraph, heading, quote, code block and table cell in the native chat transcript was a selectable TextView. Android starts a word selection, with the magnifier, on a double tap or a long press, and two flicks in the same spot while scrolling a FlatList register as a double tap, so scrolling the chat kept selecting words. iOS is unaffected: its UITextView path arbitrates scroll against selection itself. Android now renders transcript text without inline selection: one gate in MarkdownText covers every selectable span, and the user bubble follows it. A long press on a message opens a sheet with "Copy message" and "Select text", the latter a screen whose only content is one selectable Text, so a selection can only start where the user asked for it. The message row owns that sheet and mounts it only while open. iOS and web keep their inline selection and get no long-press handler. Verified on a Pixel 10 Pro Fold (Android 17): an adb double tap on the transcript selects nothing, the same double tap inside "Select text" selects a word, tool rows inside the bubble still expand on tap, and the long press opens the sheet. * fix(mobile): scope the Android selection gate to the transcript and route span long presses Review follow-ups on #22871: - The gate now applies only where `rangeSelectable` is passed (the chat transcript). Task comments and file previews keep their selectable text on Android as before. - On the Android transcript, spans that take taps (links, file paths) also take the row's long press, so a link under the finger no longer swallows the copy/select sheet. - Copied text keeps its whitespace; only whitespace-only blocks are dropped. - The Android markdown test compares `String(node.type)` instead of a type assertion, which the changed-code quality gate rejects. * fix(mobile): route long presses on Markdown images to the row on Android An image block is a Pressable of its own, so on the Android transcript it now carries the row's onLongPress like tappable spans do; a long press on an image opens the copy/select sheet instead of being swallowed. Test extended with an image block. * test(mobile): pin the Android long press from a chat row to its actions sheet The existing row suite runs as iOS, where the bubble has no long press. This one runs as Android: the bubble's long press mounts the actions sheet with the message, the markdown receives the same handler, closing unmounts the sheet, and the user bubble carries no inline selection. * fix(mobile): preserve Android message selection while replies stream --------- Co-authored-by: Neil <neil@stably.ai> |
||
|
|
1b957b70a8 |
fix: close worktree dialog before slow script checks (#25472)
* fix: move worktree script checks out of creation dialog * fix: retain creation host across background preparation |
||
|
|
67fc708b3b | Group CSV editor modules and tests in their own folder (#25476) | ||
|
|
e46fa0df13 |
test(linear): cover numeric-leading issue identifiers (#10241)
Expand numeric-leading Linear identifier coverage in the current parser and workspace source flow while rejecting numeric-only team keys in bare inputs and URLs. Main already implements the production behavior through #23423. Repair two stale localization test assumptions found by full CI. All changes are regression tests. Validation: 369 focused tests across 46 suites, local typechecking/lint/formatting, and full final-head CI passed. Co-authored-by: Ken Fukuyama <kenfdev@gmail.com> |
||
|
|
c53b6f235a |
Edit CSV tables directly and remember column widths (#25442)
* Add direct CSV table editing and persistent column widths * Preserve pending CSV edits across concurrent saves and panes * Keep CSV input stable during autosave and table commands * Cancel delayed menu paste after CSV pane focus changes * Prove delayed CSV menu paste starts before focus changes |
||
|
|
2b0ce17514 |
Show provider credit balances alongside usage limits (#25408)
Display provider credit and currency balances separately from rate limits. Preserve quotas when balance data is unavailable and translate complete labels. Supersedes #9363; credit to @mmarabel for the original implementation. Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com> |
||
|
|
a154562a89 |
fix(native-chat): show a reply cut off by an Orca crash or quit like a finished turn, with one explanation (#25043)
* fix(native-chat): read a crash-cut reply like a finished turn, with one explanation
A reply that an Orca crash or restart cut off said it failed three times: the
turn bar read "Failed after N", the chat's notice row said Claude stopped, and
the sidebar dot stayed red after the chat was read.
The turn bar now reads "Worked for N" for a proven crash/restart cut, the
notice row stays the one explanation, and the sidebar card's dot and agent
rows read failed only until the user has visited the chat (the same
acknowledgement that un-bolds the row), then read done. A failure, a user's
Stop, a replaced turn and an unconfirmed end keep their labels. The stored
outcome is unchanged.
* fix(native-chat): explain a turn a quit or eviction cut, once
A turn cut off by quitting Orca, an idle eviction or a teardown recorded the
same outcome as a crash-cut turn but wrote no notice row, so with the turn bar
now reading "Worked for N" nothing in the chat said it stopped.
The host stop's settle now writes the existing providerExited notice for the
latest turn when it ends as news (no person's Stop decided it): in the same
write as the host's own turn end, or right after the adapter's. It is keyed by
the turn, and skipped when an error row already explains that turn, so a retry
or an earlier exit row never leaves two.
* test(native-chat): narrow the turn scope and add the seen map in the crash-cut tests
* fix(native-chat): derive a cut turn's one notice on read instead of writing it at stop time
A turn cut short when the agent stopped without anyone asking now reads
"Worked for N", so it needs a row saying it stopped. Writing that row only on
the quit and eviction paths missed journals written before this change, a quit
that died between its drain and its settle, and any future stop cause.
The transcript now derives it from the journal on desktop and phone alike: a
root turn that ended interrupted with no verdict and no row about the stop
gets one notice in the provider-exit row's words. A stored exit row, matched by
its exit fact or its writer's identity rather than its tone, stays the
explanation, and so does the restart continuation's own outcome note, so a
refused resume says it once. The host's stop path is back to what it was.
* fix(sidebar): read a cut-short turn's red mark from its acknowledgement on every surface
A turn cut short with nobody asking reads failed only until the user has seen
it. The previous revision passed an optional "seen" flag to each caller, so any
surface that did not pass it, the chat's own tab dot among them, stayed red
beside a sidebar that read Done.
The verdict's display now takes the acknowledgement as part of what it reads:
the entry's stateStartedAt beside the time the user last acknowledged it, both
required, judged by one shared rule. Each surface joins it once where it builds
its rows: the sidebar's agent rows (and so the notes send menu), the workspace
card summary, the terminal tab bar, Cmd-J recent rows, and Activity threads.
Failures, Stops, replaced turns and unproven ends keep their marks as before;
the phone, which has no acknowledgement record, keeps the unseen reading.
* refactor(attention): use the one acknowledgement rule where it was copied
Auto-acknowledgement, the Activity unread count, dashboard row buckets and
notification acknowledgement each spelled the same "acknowledged at or after
the current state began" comparison; they now call the shared rule.
* test(mobile): type the cut-turn notice test's client and hook holder
* test: pin an older host's exit row by its writer and the sidebar rows' acknowledgement join
* test(sidebar): re-read the card and the tab when only an acknowledgement changes
* fix(native-chat): keep a cut turn's notice through a resume that carries on
A resume after a quit writes its "asked this agent to continue" note after its
own message, so counting that note as the cut's explanation removed the notice
once the resume went on, and made it flash away under automatic resume. Only the
notes that say the chat was not carried on (refused, not connected, not
confirmed) stand in for the notice now.
A row about the whole conversation now explains a cut turn only when no other
turn lies between them, and a failed start's row, which is about a start, never
does. The host writers and the rule share the row identity prefixes, with the
contract that a new row explaining a stop carries the provider-exited fact or
one of them.
* fix(sidebar): judge a cut as seen by the main agent's clock when a subagent holds the row
A subagent can keep a row working after its main agent was cut, and the row's
clock then predates the cut, so a look at the working chat counted as having
seen the cut and no red mark showed. The mark now compares the acknowledgement
with the later of the row's and the main agent's clocks; auto-acknowledgement of
the chat on screen reads the same clock, and its stamp covers it, so looking at
the chat still clears the mark. Bold rows, dashboard buckets, notifications and
unread counts keep the row's clock.
* fix(native-chat): tie a conversation-wide exit row to a cut only with no message sent since
A send after a quit's cut whose new agent died before its turn opened leaves a
provider-exit row about the whole conversation. That row is about the send,
not the earlier cut, so the cut kept no explanation. An exit row now explains
a preceding cut only when no message was sent in between; the restart notes,
which follow the continuation's own message, still need only no turn between.
The notes that say a resume did not carry the chat on are now told apart from
the continued note by their tone ('error' or 'warning'), which every host that
wrote them has set, instead of by their words.
* fix(native-chat): keep an owner's proven death the explanation of its cut after a send
A chat read before the startup reconcile settles its cut turn unverifiable;
if the user then sends a message, the reconcile proves the old agent dead and
writes its row about the conversation after that message. The row names the
old owner's death, never the send, so a message since no longer detaches it
from the cut. Only a provider-exit row, which a later start can write, still
needs no message sent since.
* test(sidebar): give the activity-status store mock the acknowledgement map
The card summary now reads acknowledgedAgentsByPaneKey; this test's hand-built
store state lacked it, so every summary read threw.
* refactor: move the seen-gated red mark out of this change
This change now keeps only the cut turn's label and its one derived notice.
The red mark that clears once the user has seen a cut turn moves to its own
change, so each can land alone; until it lands, the sidebar, tab bar, Cmd-J,
Activity and the notes send menu read a cut turn as failed, as before.
* test(native-chat): pin that a restart note after a continuation's turn leaves the cut's notice
* test(native-chat): keep a cut turn's notice beside a later message drawn as not sent
|
||
|
|
e02adc5320 |
Add translations for terminal shell settings (#25418)
* Add translations for terminal shell settings and search Wrap Terminal Pane shell configuration strings and terminal search keywords with translation calls to enable localization across supported languages. Add translated strings to all locale files and update the localization coverage allowlist to reflect properly translated content. * fix translation |
||
|
|
d279adc6ef |
fix(activity): keep code blocks in row previews from rendering as scroll boxes (#25410)
The activity row previews the agent's last reply with the compact markdown renderer, which draws fenced code blocks and tables as their own scrollable boxes. Those boxes escaped the row's line clamp, so a reply with a code block put a tall grey box with scrollbars in the middle of the list. Row-scoped overrides now flatten code blocks and tables into plain wrapping text, so the existing clamp cuts them like any other line. Other markdown surfaces are unchanged. |
||
|
|
f873aaac5b |
Fix duplicate session option flags and preserve argument values (#25382)
* Fix duplicate session option flags and preserve argument values * improve tests |
||
|
|
16d937d7be |
Keep large CSV previews responsive and add column resizing and links (#25381)
* Bound CSV preview memory and virtualize resizable linked cells * Fix dense CSV previews and retain bounded viewport pages * Align CSV record limits across BOMs and line endings * Respect headful mode in CSV visibility checks |
||
|
|
7863871270 | Keep OpenCode foreground evidence when its background service starts (#25378) | ||
|
|
b94cfa7fd0 |
feat(relay): declare Asia spare cell c34 as migration-only (#25336)
Adds a sixth asia-east2 cell at the C31 shape (cap 3000, 6000 request units, pool 16, e2-standard-4) in asia-east2-c, pinned to the f30b5cb1 cell image. It gets its own topology and registration wave but no promotion wave, so the admission script and workflow refuse to promote it; it stays a migration-only landing zone and out of the fleet pool list. Claude-Session: 1145a80d-dec4-4a9b-9373-bbbb876b9041 |
||
|
|
e347aa4e67 |
fix(mobile): honour the desktop pinned-worktree placement setting (#25301)
* fix(mobile): honour the desktop pinned-worktree placement setting Mobile always kept pinned rows in their groups as well as in Pinned. Desktop's showPinnedWorktreesInGroups (default off) shows them only in Pinned. Read that setting with its own settings.get operation, refreshed with the list's view settings, and leave host and device-local pins out of their groups unless it is on. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): keep lineage children with a pinned parent Under single-location a pinned parent left its group while its unpinned child stayed behind as a root. Visible descendants now follow a pinned ancestor into Pinned, nested under it, as on desktop. Drop the inert host-id half of the pinned-policy stale-reply guard; a host switch replaces the client. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): fold pinned placement into the desktop view-settings sync One "sync from desktop" callback now also reads settings.get for pinned placement, unawaited so a slow read never holds the ui.get merge; the separate callback and its plumbing are gone. Re-record the three host.view-settings goldens, which gain only that settings.get send. The pin expansion walks the lineage children index the renderer now shares, over visible rows only. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): one stale-reply rule for the desktop view-settings sync Both reads in syncViewSettingsFromDesktop now drop a reply only when the client was replaced; the host-id half compared a captured value with itself. Pin the no-wait invariant: the ui.get merge applies while settings.get never answers. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): follow a pin through rows hidden by search Desktop's Pinned section walks lineage over every worktree and keeps the visible descendants, so a grandchild still follows a pinned root when search hides the middle row. Mobile walked visible rows only and left it in its group. Build the children index from the unfiltered list; membership stays visible-only. Drop the hostId arg the view-settings sync no longer reads. Pinned-lineage tests move to their own file to stay under max-lines. * refactor(mobile): read pinned placement in its own hook The placement read was folded into the desktop view-settings sync, so that callback juggled an awaited and an unawaited read and three goldens recorded a settings.get that never answers. useHostShowPinnedInGroups now owns the setting and the catalog refreshes it beside the view-settings sync on connect, focus and mount; the sync, screen state and goldens are back to main. The reader returns desktop's boolean, as its siblings do, so transport no longer imports a worktree type and the two-literal policy union is gone. makeSection always applies lineage, the pinned walk is a Set worklist, and the lineage children index drops a set that duplicated its map's keys. * fix(mobile): key pinned placement to the client that reported it The host screen is reused across hosts, so the previous host's "show in groups" value survived a switch until the new read landed, or for good if it failed. The setting now carries the client that reported it and counts only for that client, which also makes a late reply from a replaced client inert and drops the clientRef plumbing. * fix(mobile): drop a replaced client's late placement reply Keying the value to its client hid a previous host's value but still let that host's late reply overwrite the current host's, reverting the list to the default until the next refresh. Restore the clientRef write guard; the client key still covers the effect-long window before clientRef follows a switch. * refactor(mobile): treat pinned placement like the screen's other host state The placement hook kept its own host-switch handling (a client-keyed value plus a clientRef guard) beside the screen's existing one. showPinnedInGroups now lives in HostScreenState, resets with the other host-scoped values in useHostScreenIdentity, and is read in the catalog refresh behind the same clientRef check as the catalog fetch. The hook and the catalog argument go. |
||
|
|
3f6225deeb |
fix(orchestration): accept max and ultra effort for Codex models Orca does not list yet (#25375)
Codex models newer than Orca's built-in list (gpt-6.x) were capped at xhigh, so worker-start refused --effort max/ultra that the installed Codex supports. Unknown Codex models now accept the full known effort vocabulary; unknown effort names are still refused. Refs STA-9312 |
||
|
|
52bd9afe9d |
refactor(relay): stop creating three tables nothing writes (#25354)
* refactor(relay): stop creating three tables nothing writes relay_confirmable_splices, relay_cell_drain_attempts and relay_migration_leases are created at every boot and referenced nowhere else on main except tests. In production all three hold 0 rows and pg_stat_user_tables shows 0 inserts, 0 sequential and 0 index scans since the 2026-09-28 stats reset. This removes them from the schema and the transaction-phase table, and drops the test references. It does not drop them: an older image still runs CREATE TABLE IF NOT EXISTS at boot, and a drop racing that create can fail the older boot's schema step. A follow-up can drop them once no image that creates them can be rolled back to. * docs(relay): note the account-erasure prerequisite for dropping retired tables |
||
|
|
1bec53ceb2 |
Reduce repeated CI setup and overlap mobile typechecks (#25359)
* Measure remaining CI import, diagnostic and checkout savings * Qualify remaining CI candidates on hosted runners * Qualify independent mobile typecheck overlap on Actions * Keep explicit RPC test registries from loading unused methods * Qualify complete RPC registry cohort and mobile cancellation * Promote measured CI setup and typecheck savings * Recognize the shared RPC test guard in lint policy * Align the mobile barrier contract with independent typechecks |
||
|
|
e884681eca |
fix(relay): retain confirm results for a week and audit events for 90 days (#25353)
* fix(relay): retain confirm results for a week and audit events for 90 days relay_confirm_results (8.3M rows, 5.8 GB) and relay_audit_events (8.4M rows, 3.6 GB) were never deleted. The director's credential cleanup now reaps both after its existing passes: - Confirm results older than 7 days. The only reader replays a stored result for a retry of the same request on the same connection basis; a different basis is already refused as a tuple mismatch. committed_at has no index, so this uses the TID-window reaper from the reservation prune, capped at 250 rows a tick (the 7.4M-row backlog drains over 2-3 days). - Audit events older than 90 days, ordered by `at` so the batch walks relay_audit_events_at. Nothing in the relay reads them back. The oldest row is from 2026-07-14, so this deletes nothing until 2026-10-12. reapBatch now deletes by `ctid = ANY(ARRAY(...))` on Postgres: with `ctid IN (...)` the planner can choose a hash join over a sequential scan of the whole table. * chore(relay): record the decided audit retention |
||
|
|
ebeea319b2 |
Support modern Qoder commands and verify authenticated resume
Start and resume Qoder through the existing execution-host selector when only the documented qoder command exists. Preserve legacy qodercli preference, explicit commands, quoting and session identity; disconnected SSH execution refuses without local fallback. Mobile history uses command-at-create only with the optional owned-create capability and an existing stable mutation identity. Reconcile authoritative execution-host inventory before retrying creation, adopt the same surviving operation, preserve WSL/incarnation metadata and restore only missing original launch metadata. Changed retry settings cannot replace original capture. Bounded evidence expires after 15 minutes; unavailable inventory or original evidence refuses recovery and leaves surviving execution untouched. Authoritative inventory proving absence preserves the existing recreation behavior; this is not a durable exactly-once ledger for completed one-shot side effects. Older hosts retain the acknowledged create-then-send path. Scope mobile launch authority to the current committed host/client generation, and recheck operation ownership after asynchronous preparation before later sends. Retire the mutation once the host acknowledges the resume; later ownership changes stop navigation without reporting a completed resume as failed or reusing a cached legacy pane. Preserve genuinely interrupted mutation identity. Preserve current-main OpenCode validation and merged Pi/Cursor behavior. Correct unchanged-main editor test fixtures to their production insertion-range and store contracts while retaining original assertions and production behavior. Credit: Neil Parker (@nwparker); Soperf and jyang for Qoder integration/history groundwork in #24614; actual Pullfrog and CodeRabbit reviews and the independent Source reviewer for the mobile retry, launch capture, evidence lifetime and connection ownership findings. Conservative positional process recognition and existing folder-history matching-worktree limitations remain documented. |
||
|
|
aac1c8f9c2 |
chore(relay): move US cells c32 and c33 to the general same-cap list after promotion (#25367)
Both were promoted to general on 2026-10-01 (selector 344 shows them general), but the same-cap job still classed them migration-only. Rollback mode on either would isolate (general -> migration-only) before its no-op check failed, demoting a live cell. They stay out of the fleet pool list (pool 10, not 16). Claude-Session: 1145a80d-dec4-4a9b-9373-bbbb876b9041 |
||
|
|
e9daf9b746 |
fix(relay): prune released control-connection reservations in bounded batches (#25352)
relay_control_connection_reservations was never deleted: 13.8M rows and 9.1 GB in production, 99.999% of them in state 'released' and ~400k more a day. Nothing reads a released row (every reader filters it out by state), but each placement still locks all of its host's rows, ~160 on average. A new director sweep step deletes released rows older than a day. It walks the heap in TID ranges of 16 pages, because without an index on released_at a `LIMIT n` delete plans as a sequential scan from page 0 that gets slower as the head of the heap empties (production EXPLAIN). Each statement selects its rows FOR UPDATE SKIP LOCKED, so a row a request holds is skipped rather than waited on, and deletes them by `ctid = ANY(ARRAY(...))` so the delete is always a TID scan. A tick stops at 400 rows, 128 pages or 250 ms, which drains the backlog over about three days at five directors. |
||
|
|
b9b0f29172 |
fix(relay): skip the dead-cell sweep's host scan when no cell qualifies (#25350)
evacuateDeadCells ran one query that joined every assignment to its cell's liveness and fence state, ordered by primary key with LIMIT 100. In production no row ever matched (the only dead cells are stale existing-only cells without a committed fence), so the planner walked the whole relay_assignments primary key every call: 551 ms mean over 85k calls, ~12% of relay database time. A cell-level pre-check now evaluates the predicate's cell-only half over the ~33-row cell tables (3 ms in production). It is a superset of the cells the host query can act on, so the sweep returns early when it is empty and otherwise restricts the host query to those cells. The host predicate is unchanged. |
||
|
|
822fc5bed4 |
Add repository OpenCode permission defaults (#25326)
* test(config): reproduce rejected repository OpenCode config * Add repository OpenCode permissions and allow its reviewed root config * fix: preserve sensitive OpenCode confirmation prompts --------- Co-authored-by: Orca campaign recovery <campaign-recovery@example.invalid> Co-authored-by: Orca OpenCode Campaign <opencode-campaign@local.invalid> |
||
|
+3 |
46c3c3b44d |
Keep OpenCode worker model selection separate from the default (#24768)
* feat(orchestration): support OpenCode worker model selection Allow supervised OpenCode workers to use per-launch model overrides through the existing launch-preference and receipt path. Preserve existing OpenCode agent arguments while replacing only model flags, and reject unsupported effort values explicitly. Keep Native Chat option exposure unchanged and update the worker CLI and orchestration guidance. * fix: wait for OpenCode worker composer before first dispatch Reuse captured composer readiness on local and paired execution hosts and revoke launching-shell paste anchors. Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> * feat(opencode): probe execution-host CLI capabilities * fix(opencode): select plugin default for execution host loader * fix(opencode): limit prompt prefill capability to verified release * feat(orchestration): gate OpenCode worker model preferences by host capability Co-authored-by: user141514 <user141514@users.noreply.github.com> * feat(opencode): probe launch capabilities on the execution host * feat(opencode): probe execution-host CLI capabilities * feat(opencode): probe launch capabilities on the execution host * feat(orchestration): resolve explicitly configured command aliases * fix(opencode): select plugin loader for the launched host binary * fix(opencode): match WSL probe cwd and declared guest environment * fix(orchestration): verify available OpenCode model on execution host * Add host-owned OpenCode and Devin account profiles * fix(orchestration): inspect OpenCode models with selected account environment * fix(orchestration): bind model validation to direct existing-workspace launch * fix(opencode): preserve launch environment deletion boundaries * fix(orchestration): limit effective model contract to verified OpenCode release * wip(opencode): authorize native startup prompt intent at execution owner * fix(opencode): atomically replace status plugin entrypoints * fix(opencode): retain plugin permissions across restrictive umasks * test(opencode): resolve permission fixture from primary cwd * feat(opencode): install startup prompt plugin independently of status hooks * Restore inherited account environment and preserve cleanup retries * fix(opencode): select plugin loader for the launched host binary * fix(opencode): match WSL probe cwd and declared guest environment * fix(opencode): preserve launch environment deletion boundaries * wip(opencode): authorize native startup prompt intent at execution owner * fix(opencode): atomically replace status plugin entrypoints * fix(opencode): retain plugin permissions across restrictive umasks * test(opencode): resolve permission fixture from primary cwd * feat(opencode): install startup prompt plugin independently of status hooks * Check relay environment values before merging * fix(opencode): wait for admitted startup intent and preserve failed-launch briefs * fix(opencode): confine overlay manifest cleanup to owned directories Co-authored-by: Adnan Khan <adnank11427@gmail.com> * fix: wait for OpenCode worker composer before first dispatch Reuse captured composer readiness on local and paired execution hosts and revoke launching-shell paste anchors. Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> * fix(orchestration): validate configured aliases with target shell grammar * fix(orchestration): use actual shell and refuse assignment-only aliases * fix(opencode): wait for admitted startup intent and preserve failed-launch briefs * Support verified OpenCode v2 model selection in fresh native workers * fix(opencode): refuse unsupported startup preferences before model probing * feat(opencode): probe execution-host CLI capabilities * fix(opencode): select plugin default for execution host loader * fix(opencode): limit prompt prefill capability to verified release * feat(opencode): probe launch capabilities on the execution host * fix(opencode): select plugin loader for the launched host binary * fix(opencode): match WSL probe cwd and declared guest environment * fix(opencode): preserve launch environment deletion boundaries * wip(opencode): authorize native startup prompt intent at execution owner * fix(opencode): atomically replace status plugin entrypoints * fix(opencode): retain plugin permissions across restrictive umasks * test(opencode): resolve permission fixture from primary cwd * feat(opencode): install startup prompt plugin independently of status hooks * fix(opencode): wait for admitted startup intent and preserve failed-launch briefs * fix(opencode): unsubscribe hook settings during async host shutdown * fix(opencode): confine overlay manifest cleanup to owned directories Co-authored-by: Adnan Khan <adnank11427@gmail.com> * test: preserve typed calls in configured worker target checks Replace Reflect.apply with the existing typed prototype call pattern so the unchanged regression cases pass the anti-slop lint gate. * test: check typed model host calls and terminal delay state Replace Reflect dispatch and property reads with checked access and typed calls. Complete the existing resume request fixture with its required identity fields without changing the rejection boundary or assertions. * test(readiness): census recorded OpenCode composer boots * fix(opencode): reject redirected overlay parents before cleanup * fix(orcad): retain runtime cleanup when subscribing to hook settings * refactor(launch): extract OpenCode config and attachment authority * fix(opencode): retain host version selection across relay restarts * fix(opencode): pass run prompts as positional messages Preserve run flags and use the existing shell quoting and run-command detector to append the initial message after --, reusing an existing separator. TUI launches retain their version-selected prompt transport and draft behavior. Original run-order work: @coelho-doti (#13065, tracked in #17551). * fix(opencode): keep wrapped run tasks positional Recognize supported environment prefixes and PowerShell call operators without mistaking prompt arguments for executables. Keep environment and run separators separate, preserve the task text and exclude run commands from native submission. Source-parent: |
||
|
|
d9846e64fe |
fix(test): give async image-insert store mocks the openFiles list (#25357)
#25176 and #24489 landed together and collided in insertRichMarkdownImageFromPath. #25176 added three test files whose @/store mock returns { settings, folderWorkspaces, worktreesByRepo }. #24489 made the same function look up the open document with state.openFiles.find(...) to pass document-folder access to the import. Under the mock, openFiles is undefined, so the lookup throws, the catch shows "Failed to insert image", and no image is inserted -- 38 tests fail on main. The real store always carries openFiles, so production behavior of both PRs is intact; the mocks were just written against the pre-#24489 shape. Add openFiles: [] to the three mocks. |
||
|
|
0a8c5722f3 |
fix(native-chat): start a new chat after an earlier start failed (#24917)
* fix(native-chat): start a new chat after an earlier start failed A chat whose start the host refused stayed registered under its agent and workspace, so the + menu and new-tab search kept that agent disabled with a spinner, and any later "new chat" for that agent (for example "Send notes to > New agent") restarted the failed chat instead and dropped its own prompt. Only launches in flight now hold the agent/workspace slot that later starts join. The registry keeps every launch by session id and derives the holder, so a failed chat keeps its own Retry while a new start makes a new chat. * test: import the launch status from its own module * test: name the owning host on the launch intents the new tests build * fix(native-chat): a new start never joins an unconfirmed or retried chat A new start for an agent joined a chat whose create answer was lost, or a failed chat whose Retry was in flight, and dropped its own text: the source-control "Fix with AI" action reported success with nothing sent, "Send notes to > New agent" sent nothing, and after a reload the text became an unsent draft in the old chat. Only a new start's own first create now coalesces later starts (double click, two callers at once). Each attempt records whether it is that first create or a Retry/re-check of an existing chat; an unconfirmed or retried blank chat keeps its own Retry and a new start opens a new chat with its text. A resume still re-checks or joins its conversation's launch, since the host refuses a second adoption. * fix(native-chat): a different new request always opens its own chat A new start used to join any blank chat still in its first create or still sending its opening text, so a second "Fix with AI" for another check, or notes sent to a new agent, landed in that first chat. Each first attempt now keeps the request it was started with (its text and whether it is sent or drafted). Only a repeat of that request (a double click, a retried call) joins it, and it shares the text already staged, so the text is sent once. Any other request opens a new chat with its own text. The + menu, new-tab search and send-notes menus disable an agent only when their own pick would join. Resume launches are unchanged. * fix(native-chat): an empty chat still starting takes the first text sent to it A blank chat that is still starting (a + pick, the empty-workspace default chat) is empty, so the first request with text, such as notes sent to a new agent, now goes into it instead of opening a second chat beside it. That request becomes the chat's own request: an identical repeat joins and is sent once, and any other request opens a new chat. Move the logic that decides which launch a start joins out of the launch registry into its own module, so the registry stays under the line limit once #24904 lands beside it. Pin that a repeat arriving while the opening text is still sending is sent once. * test: an empty chat delivers the claiming text the way its request asked * fix(native-chat): only a chat its user has not used yet takes another request's text A blank chat that is still starting was claimable by the first request with text even after its user had sent a message into it or typed into its composer, so notes or a Fix with AI prompt could land in a conversation the user had already started. Emptiness is now read from what the chat holds: nothing in its outbox and no text in its composer draft. A chat its user has used stays theirs, and the request opens a new chat. If the claiming text cannot be saved, the claim is not recorded: the request falls through to a new launch, whose save failure shows on that chat as for any new launch, instead of reporting a failure nothing showed. * fix(notes): notes handed to a send leave the next send until it settles Notes sent to an agent stayed in the notes shelf until their chat delivered them, so a second "Send notes" made while the first new chat was still starting collected the first notes again. With every different request now opening its own chat, those notes reached two chats. When notes or browser annotations are handed to a send (a new agent, a running agent from the menu, or a sidebar agent row), they are held in memory against that send's own delivery result and left out of the next send. Delivered notes are removed as before; a failed, refused or undelivered send releases the hold, so they come back for the next send. The notes menu now builds each scope's prompt from the notes it will send. * test: give the annotation tray fixture its hand-off callback * fix(notes): offer no send when every note is already on its way When every note or annotation in a send is held by an earlier send still in flight, the built prompt is empty. The notes menu already disabled its trigger then, but its New agent rows still started an agent with no text, the annotation Send buttons still opened, and a sidebar agent row could be sent an empty prompt. The New agent rows, the annotation Send buttons and the sidebar send now offer nothing when there is nothing to send. * fix(notes): a new chat's staged prompt decides when its notes leave the shelf Notes sent to a new agent were released back to the shelf as soon as the chat's start failed, while that failed chat kept the same text staged for its own Retry. Re-sending and pressing Retry put the notes in two chats, and Retry alone left delivered notes listed as unsent. For a new chat, the notes now follow the prompt it staged: held while that message is in the chat's outbox, cleared from the shelf when any dispatch (Retry or re-check included) sends it on, and back on the shelf when the chat is closed and its outbox thrown away. A paired server's chat is found once its start settles. Running-agent sends keep their own result. While notes are only on their way, the send button reads "Sending…" rather than "All notes sent". * fix(notes): a new chat's saved message keeps its notes out of another send across a reload The hold that keeps notes sent to a new agent out of the next send lived only in memory. A reload while that chat had not yet sent them put the notes back on the shelf while the chat's saved message still carried their text, so a second "Send notes" sent them twice. The staged message now saves the send keys of the notes it was built from. The shelf treats a note as on its way while any saved message carries its key, read from the saved outboxes on first use and kept current by the outbox's one write funnel. When a message carrying notes is sent on (its own start, a Retry or the re-check), those notes are cleared from their shelf; when it is thrown away with its chat, they come back. Browser annotations sent to a new chat use the same keys while the app runs. Running-agent sends keep their in-memory hold. This replaces the per-message watch and outcome promise from the previous change. * fix(notes): release a gone chat's notes, and clear sent ones in the web client too Two gaps in keeping notes out of another send while a saved chat message carries them: - The web client never installed the clearing that removes notes once a chat sends them on, so after a Retry there the notes stayed listed. It is now installed, once per renderer, by the background services both the desktop and the web client load with App. - A saved message carrying notes was thrown away only by this window's own close. A chat closed from the phone, another window or while Orca was off, or closed here without a known host, kept its notes held for good. A chat the host stops listing (affirmed, and not a launch still starting or failed) now has its queued messages thrown away once that sync lands, as does a close that can name no host, so the notes come back. * Revert "fix(notes): release a gone chat's notes, and clear sent ones in the web client too" This reverts commit |
||
|
|
ccfc8fe2c4 | Update README downloads badge | ||
|
|
6f8eee3776 |
fix(test): pass getInsertionRange in the image-insert access test (#25351)
#25176 replaced insertPos with getInsertionRange and #24489 added a test using insertPos; together they broke main's typecheck. |
||
|
|
51fe6f3fba |
fix(editor): restored tabs for files outside your projects no longer fail with Access denied (#24489)
* fix(editor): read files outside projects without a grant a restart loses
A file opened from outside every project (e.g. ~/notes.txt from the floating
workspace) read through an in-memory grant. After a restart the restored tab
only renewed that grant when it stored a full path, so a tab saved relative to
the floating workspace folder failed with "Access denied" and Retry repeated it.
Single-file reads (read, stat, exists) and open-editor-tab saves now resolve a
path outside every project in place. Paths inside a project keep the full
containment check, so a project's symlinks still cannot escape it, and every
other write stays inside projects.
* fix(editor): re-grant restored floating-workspace tabs by owner, not path shape
Problem: a file opened from the floating workspace (e.g. ~/notes.txt via
Cmd-click in the floating terminal, the floating markdown picker, or a .md
opened from the OS) loads until restart, then shows "Access denied: path
resolves outside allowed directories". Main's external-path grants live only
in memory. On restore the editor re-granted only tabs that stored an absolute
path, but floating tabs store a path relative to the floating root (~ by
default), which is deliberately not an authorized root, so they were never
re-granted. Restored floating notebooks also failed to start a kernel.
The previous commit on this branch let main read and save any path outside a
project without a grant. That widened fs:readFile/stat/pathExists for every
caller, including automatic reads of untrusted content (markdown preview
images), which opened a Windows UNC credential leak and a /dev/zero
main-process memory blowup. This reverts that model entirely.
Fix: one helper decides which client-local path a tab needs re-granted by
ownership: a floating-workspace tab, or a tab stored outside its own project.
It never grants paths a local project root covers (a grant would also
authorize a project symlink's outside target), and skips SSH-owned,
runtime-owned and not-yet-hydrated owners. Every reader that can touch a
restored tab before or without the editor loader uses it: the loader, the
restored dirty-tab conflict scan, and the paired-mobile markdown bridge.
* fix(editor): let main decide which restored-tab paths a project already covers
Problem: the restore re-grant helper decided "already inside a project" in
the renderer from its worktree list. At startup that list only holds repos
the session references, so a floating tab inside an unlisted repo was granted
(including a symlink's outside target), and the renderer's path matcher
disagrees with main's on WSL \\wsl$ vs \\wsl.localhost, which stranded a
folder-workspace tab with "Access denied" after restart. The helper also
treated a folder workspace with a missing or ambiguous host as local.
Fix: the renderer now decides only by owner (a floating-workspace tab, or a
tab stored outside a project whose owner is explicitly local) and asks main
with `skipIfInsideAllowedRoots`. Main checks the path against its own allowed
and registered roots, in both the named and canonical-parent spelling against
both root spellings: a path a project covers gets no grant, an alias spelling
of a project path gets only that spelling, and a project symlink's outside
target is never granted. Explicit-open grants (Cmd-click, drag, explorer) are
unchanged. Tests now prove each reader waits for the grant before reading.
* fix(fs): decide a restored tab's project membership from every ancestor's real path
Problem: the restore re-grant decided "inside a project" from the named path
and the real path of its parent only. When a tab path crossed a project
directory symlink and named the project through a spelling that was neither
the registered root nor its realpath (a second alias, a `..` segment, a case
variant on a case-insensitive disk, a /var-style alias of an ancestor), no
check matched, the path took the full grant, and the symlink's outside target
became readable and writable.
Fix: a path is inside a project when the named path is inside a root, or the
real path of any ancestor folder is inside a root in its registered or real
spelling. Such a path gets at most its named spelling, never its realpath. An
ancestor that fails to resolve for any reason other than "missing" now fails
closed to the named-spelling grant instead of falling through to the full one.
Tests cover each spelling; the non-symlink case also runs on Windows.
* fix(fs): read local files as regular files only, from one bounded handle
Problem: fs:readFile stat'ed a path and then read it to EOF. A character
device such as /dev/zero reports size 0, passes the size limit and never ends,
so the main process buffers until memory runs out; a FIFO hangs the open.
Writes could also target an existing device or FIFO.
Fix: every local fs:readFile (editor and log snapshot) opens the path once,
non-blocking, refuses anything but a regular file, and reads the size check,
binary probe and content from that same handle, capped at the limit even if
the file lies about its size. The AI Vault log tail opens non-blocking too,
and fs:writeFile refuses an existing non-regular target.
* feat(fs): let desktop file requests declare their shape
Problem: main decided every local file request against one allow-list plus a
set of in-memory grants the renderer had to recreate after every restart, so
a file the user opened outside a project (for example from the floating
workspace) was denied once Orca restarted.
This adds the request shape the common pattern uses, alongside the grants for
now:
- no shape (the default): the path must be inside a project root main
recognises, symlinks included. Desktop requests also accept the app-owned
floating-workspace folder; paired-client RPC never does.
- user-file: a single file the user named by absolute path, used in place.
Only fs:readFile/stat/pathExists and saving (fs:writeFile) accept it.
- document-resource: an image or PDF a document references, limited to every
project root when the document is in one, else to the document's folder,
and refused by path text before any disk or network access.
Notebook kernels and AI Vault log tails check their open file as user-named.
* feat(editor): send each local file request's shape from the renderer
Problem: after a restart, a tab opened outside every project (a floating
workspace file, a file opened by absolute path, an OS-opened markdown) could
only be read if the renderer first re-granted its path, and readers that ran
before the editor loaded the tab had no grant at all.
The renderer now says what kind of request it is making, and main checks that:
- A persisted tab opened outside its owner's root (floating workspace, or an
absolute stored path) whose owner is explicitly local reads and saves as a
user-named file, from every reader: the editor loader, the restored-tab
conflict scan, the change banner and compare dialog, the paired-phone
markdown bridge and the save queue. Project tabs stay inside their root.
- Clicks, drops, typed paths and browser-opened notebooks stat as user-named.
- Markdown preview and rich-editor images are document resources, limited to
the document's roots or folder. Images the user pasted or attached into a
chat show as user-named; agent images stay inside the project.
- The image cache keys on the shape, so one shape's image never answers
another's request.
A ratchet test lists every renderer file allowed to create a user-named
request.
* refactor(fs): delete the in-memory path grant system
Problem: main kept a set of paths the renderer had asked it to allow
(fs:authorizeExternalPath). The set lived only in memory, so a file the user
opened outside every project could be read until Orca restarted and was then
denied, and every new reader of a restored tab had to remember to recreate
the grant first. Three rounds of re-deriving grants at restore each found
another reader or path spelling it missed.
Now that every desktop request declares its shape, nothing needs a grant:
- delete the grant set, authorizeExternalPath, the restore re-grant from the
earlier commits on this branch, the fs:authorizeExternalPath channel and its
preload and web-client entries;
- delete every renderer grant call (terminal and markdown link clicks, drops,
typed paths, the file explorer, AI Vault logs, chat attachments, browser
notebooks) and every main one (floating markdown picker and folder, OS-opened
markdown, keybindings.json, pasted images, import and upload sources);
- the floating workspace's picker-approved folders stay a terminal-cwd
allowlist only.
Main now holds no per-path permission, so a restart can't change any answer.
* feat(editor): open project links that lead outside the project as named files
Problem: a file inside a project that is a symlink to something outside it
opened fine from the file explorer or a terminal Cmd-click, then showed
"Access denied" after a restart: its tab was stored as a project file, and a
project request is refused when it resolves out of the project. A folder link
out of the project expanded in the explorer until restart and then failed with
a raw access error.
Now the click decides and the tab keeps that decision. Both gestures stat the
path inside the project first; if only the user-named check passes, the path
leads out of the project:
- a file opens by its absolute path, so it reads and saves as a file the user
named, the same before and after a restart;
- the explorer does not follow a folder link out of the project and says so
("This folder links outside the project, so it can't be opened here.").
Paths that stay inside the project still open as contained project tabs. Also
drops the AI Vault "path not authorized" message, which nothing shows now.
* chore: drop the casts the changed-code quality gate flags on this branch
The FileContent casts in the editor loader and the paired-phone markdown
bridge were never needed (the read result is already assignable). Tests stub
window.api through vi.stubGlobal and pass narrow stores without casting; the
one test store that still needs a cast states why.
* fix(fs): load chat images by type, and keep escaping project links readable
Problems found in review:
- Chat transcript images were trusted by message role: any user-role
"[Image: source: <path>]" (an injected Claude record, `orca terminal send`,
a paired client's image-ref) became an automatic user-named read as the row
scrolled into view, of any file type, and on Windows a network-share path
would have opened an SMB connection to that host.
- A document image named like an image but linking to a text file
(logo.png -> .env) was read as text.
- Windows device names (NUL.png, COM1.jpg) passed the path-text check of the
automatic image loads.
- A project symlink leading out of the project, opened by a typed path, a
tab-strip drop or a browser file:// notebook, was stored as a project tab
and immediately refused.
Fix:
- New chat-image request shape for every transcript image and the composer
preview, whoever's turn named it: an absolute local path whose requested and
real targets are image files, a regular file, size-capped; network-share and
device-namespace paths and Windows device names are refused by path text
before any filesystem call. Pasted screenshots still show after a restart,
and agent images outside the project now render.
- Document resources check the real target's type too, and refuse Windows
device names by path text.
- Typed paths, tab-strip drops and browser notebooks stat through the same
check as the explorer and terminal, and open an escaping link by its
absolute path.
- Tests pin the shape at the change banner, compare dialog, markdown preview
and image prewarm; a second ratchet lists every file that can open a tab the
tab rule reads as user-named, and its comment says what it can't see.
- Stale grant wording removed.
* fix(fs): tighten automatic image loads and the project-link check
Problems found in review:
- Two unit tests went red on this branch: the browser-share test still
expected reads without a shape, and the rename test's electron mock had no
app, which the desktop root check now needs.
- The device-name check ran on the raw path, so `NUL.png\.` or
`COM1.png\x\..` (reachable from markdown ``) reached the
filesystem; a document image whose real target was a device name passed.
- Chat images in a project that lives on a Windows network share no longer
rendered, though the markdown preview showed them.
- Any failed project check (a missing file, a dropped connection) was taken
as "this link leads out of the project" and opened as an absolute tab.
- Every local read allocated about 2 MiB, even for a tiny image.
Fix:
- Device names and device-namespace paths are checked on the resolved path
and on the real target, for chat images and document resources alike.
- A network-share path in an automatic load is read only inside a project
root (the user chose that share when adding the project); anywhere else it
is still refused by path text before any filesystem call.
- Only main's "outside allowed directories" refusal marks a project path as
leading out of the project; other errors surface as before. The message now
lives in shared code so both sides agree on it.
- Reads size their first buffer from fstat and confirm EOF with a 1-byte
probe; a file that grows past its reported size is still read in bounded
chunks up to the cap.
- Fixed the two red tests.
* refactor(fs): name file access by its role, not its structure
Problem: the static-analysis anti-slop check failed the PR because the new
code named the request's file access a "shape" (`shape`, `RequestShape`,
`TabShape`), which describes structure rather than the role.
Rename the main-process module filesystem-request-shape.ts (and its tests) to
local-file-access-resolution.ts, rename the symbols to fileAccess,
FileAccessResolution and TabFileAccessFields, and say "file access" or
"access kind" in the comments and test names. No behaviour change.
* fix(fs): refuse every Windows device-name spelling in automatic image loads
Problem: the device-name check split a file name only on '.', so names such
as NUL:.png, COM1:.png, NUL:stream.png (an alternate data stream) slipped
through, and CONIN$, CONOUT$, CLOCK$, COM0 and LPT0 were not listed. Those
reached the filesystem from a document or chat image before being refused.
Split on ':' as well, list the missing device names, and test each with
Windows path rules and zero filesystem calls. Also cover the case of a local
link that leads onto a network share outside every project (refused for chat
images), and correct the shared comment on chat-image access.
* fix(editor): let users rename and insert images into files opened outside projects
Renaming a file opened outside every project (tab double-click, editor
header) and inserting an image into such a markdown document failed with
"Access denied", even before a restart: both writes only passed the
project-root check. Document resources and chat images were also limited
by file type more strictly than users expect.
- Add a "document-folder" access kind for writes beside a document the
user opened: main allows renaming only that document, to a name inside
its own folder, and importing new files only into that folder, checked
by path text and again by real path, with Windows device names refused.
The renderer sends it only for local user-named, writable tabs (rename,
its undo/rollback, image insert); SSH and runtime requests never carry it.
- Document resources: drop the image/PDF type allowlist; folder
confinement, regular-file reads, the cap and path-text refusals remain.
- Chat images: judge only the real target's type, against every
previewable image type (AVIF added).
* fix(fs): a declared file-access kind never refuses what the project check allows
A full-path tab for a file inside a project (for example a link that
leads out, opened by its absolute path) was renamed under the
document-folder rule, which limited the new name to the file's own
folder, although the same rename with no declared access could move it
anywhere in the project. Any declared kind could be stricter than the
default in the same way.
Every desktop local file request now goes through one resolver,
resolveLocalRequestPath: it runs the default project check first (roots,
Orca's floating folder, symlink containment, outside-root path text
refused before any filesystem call) and only on a refusal applies the
declared kind's rule, which adds paths outside projects. Reads, saves,
rename source and target, and import destinations all use it, so a new
kind gets the rule for free. Automatic loads (document and chat) still
refuse Windows device paths and names by text first, even inside a
project; a device is never a file to show.
The document-resource rule no longer needs its own project branch, and
chat images no longer re-run the roots check for shares.
* fix(fs): symmetric outside-project renames, notebook real folder, same-share images
- Renaming a file opened outside every project accepted a name in a
subfolder (`archive/todo.md`), but the Undo and the rollback rename,
declared from the moved file, were then refused and the file stayed
moved. A rename under document-folder access must now land directly in
the document's own folder (checked by path text before any filesystem
call), so rename, Undo and rollback are symmetric. Image import still
accepts the folder or a folder under it. Inside projects the default
check still allows any in-project target.
- A notebook opened through a link inside a project started its kernel in
the link's folder instead of the real file's folder (main's behaviour),
because notebook and AI Vault log-tail paths skipped the project check.
Both now resolve through resolveLocalRequestPath (project check first,
then the user-file rule).
- A markdown file opened from a Windows share outside every project could
not show the images beside it. Document images on a share are now
allowed inside the document's own folder; the folder text check refuses
every other host and share before any filesystem call.
- resolveDesktopAuthorizedPath is async, so a synchronous failure in the
default check rejects like any other refusal.
* fix(fs): refuse share images outside projects again; keep renames and kernels as on main
- Reverts the same-share document image rule from the previous commit.
Its folder check compared hosts case-insensitively, so a host spelled
with U+212A KELVIN SIGN (or a decomposed accent) passed as the
document's own share and was contacted, reopening the network
credential leak. Document and chat images on a share outside every
project are again refused by path text before any filesystem call;
tests now cover the look-alike hosts with zero filesystem calls.
- Renaming a file opened outside every project into a project folder
passed the project check, but its Undo (declared from the new path)
was refused and the file stayed moved. When the rename source is
allowed only as the opened document, the new name must now land
directly in the document's folder even if a project would accept it
(resolveLocalRenamePaths).
- A notebook opened through a link outside every project started its
kernel in the link's folder; main used the real file's folder. The
kernel cwd is now the real file's folder in every case.
* fix(fs): a file opened outside every project renames to any path, and keeps its access
Renaming a document the user opened (floating workspace or full-path tab) now
follows the user-file rule: the source must be the opened document, and the
new path can be any absolute path, so a rename into another folder, a
subfolder or a project works, and its Undo (declared from the moved file)
comes back from there. Any other rename keeps the project check only. Remove
the same-folder rename rule and its tests; image import stays in the
document's own folder.
After a move, a tab stored by its full path keeps its full path instead of
being recomputed project-relative, so it keeps user-file access for save,
the next rename, image insert and restore after restart. Folder moves go
through the same remap.
Also un-export unused resolver exports and avoid a copy for single-chunk reads.
|
||
|
|
955dce5a5a |
Keep workspace deletion dialogs steady while changes load (#25321)
* Keep workspace deletion warnings from shifting the dialog * Tighten spacing in workspace deletion confirmations * Address deletion dialog review and synchronize localization catalogs |
||
|
+2 |
8e5080c132 |
Validate OpenCode worker model preferences on the execution host (#24624)
* feat(orchestration): support OpenCode worker model selection Allow supervised OpenCode workers to use per-launch model overrides through the existing launch-preference and receipt path. Preserve existing OpenCode agent arguments while replacing only model flags, and reject unsupported effort values explicitly. Keep Native Chat option exposure unchanged and update the worker CLI and orchestration guidance. * fix: wait for OpenCode worker composer before first dispatch Reuse captured composer readiness on local and paired execution hosts and revoke launching-shell paste anchors. Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> * feat(opencode): probe execution-host CLI capabilities * fix(opencode): select plugin default for execution host loader * fix(opencode): limit prompt prefill capability to verified release * feat(orchestration): gate OpenCode worker model preferences by host capability Co-authored-by: user141514 <user141514@users.noreply.github.com> * feat(opencode): probe launch capabilities on the execution host * feat(opencode): probe execution-host CLI capabilities * feat(opencode): probe launch capabilities on the execution host * feat(orchestration): resolve explicitly configured command aliases * fix(opencode): select plugin loader for the launched host binary * fix(opencode): match WSL probe cwd and declared guest environment * fix(orchestration): verify available OpenCode model on execution host * fix(opencode): preserve launch environment deletion boundaries * wip(opencode): authorize native startup prompt intent at execution owner * fix(opencode): atomically replace status plugin entrypoints * fix(opencode): retain plugin permissions across restrictive umasks * test(opencode): resolve permission fixture from primary cwd * feat(opencode): install startup prompt plugin independently of status hooks * fix(opencode): wait for admitted startup intent and preserve failed-launch briefs * fix(opencode): confine overlay manifest cleanup to owned directories Co-authored-by: Adnan Khan <adnank11427@gmail.com> * fix: wait for OpenCode worker composer before first dispatch Reuse captured composer readiness on local and paired execution hosts and revoke launching-shell paste anchors. Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> * feat(opencode): probe execution-host CLI capabilities * fix(opencode): select plugin default for execution host loader * fix(opencode): limit prompt prefill capability to verified release * feat(opencode): probe launch capabilities on the execution host * fix(opencode): select plugin loader for the launched host binary * fix(opencode): match WSL probe cwd and declared guest environment * fix(opencode): preserve launch environment deletion boundaries * wip(opencode): authorize native startup prompt intent at execution owner * fix(opencode): atomically replace status plugin entrypoints * fix(opencode): retain plugin permissions across restrictive umasks * test(opencode): resolve permission fixture from primary cwd * feat(opencode): install startup prompt plugin independently of status hooks * fix(opencode): wait for admitted startup intent and preserve failed-launch briefs * fix(opencode): unsubscribe hook settings during async host shutdown * fix(opencode): confine overlay manifest cleanup to owned directories Co-authored-by: Adnan Khan <adnank11427@gmail.com> * test(readiness): census recorded OpenCode composer boots * fix(opencode): reject redirected overlay parents before cleanup * fix(orcad): retain runtime cleanup when subscribing to hook settings * refactor(launch): extract OpenCode config and attachment authority * fix(opencode): retain host version selection across relay restarts * fix(opencode): pass run prompts as positional messages Preserve run flags and use the existing shell quoting and run-command detector to append the initial message after --, reusing an existing separator. TUI launches retain their version-selected prompt transport and draft behavior. Original run-order work: @coelho-doti (#13065, tracked in #17551). * fix(opencode): keep wrapped run tasks positional Recognize supported environment prefixes and PowerShell call operators without mistaking prompt arguments for executables. Keep environment and run separators separate, preserve the task text and exclude run commands from native submission. Source-parent: |
||
|
|
e2460907c3 |
Preserve image clipboard targets and content across editor changes (#25176)
* Preserve image insertion targets with one captured destination * Set image paste test caret through the editor selection |
||
|
|
d3afb5c5a9 | Preserve large paste destinations and native Undo boundaries (#25177) | ||
|
|
cecb62158a |
fix(ui): restore IME Enter protection in workspace details (#24099)
Restore IME Enter protection in workspace details by reusing the existing composition tracker. Reset Notes ownership at textarea detachment and preserve sizing behavior. Repair isolated native test-window delivery without changing the production foreground policy or original native input assertions.
Fixes #24097
Related contributor history: #10711, #11067, #13128, #13282.
Original implementation and macOS recordings: @setodeve, commit
|
||
|
|
75344e5850 | docs: align contributor guidance with the PR template (#25034) | ||
|
|
1978469fd2 |
fix(mobile): keep the working rings turning on the OTA page (#25299)
* fix(mobile): keep the working rings turning on the OTA page Animated.loop starts a native loop whenever the timing asks for the native driver; the web has none, so the JS fallback ran one turn and froze at 360deg. Ask for the native driver only off the web. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the native driver on native spinners Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): share the working ring rotation between both rings Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
3b01ba6d0d |
fix(native-chat): show "Sending…" on a message until the host confirms it (#24606)
* fix(native-chat): no "unconfirmed" line while Orca resends a send on its own A send whose answer was lost (for example after reopening the chat mid-send) showed "Message delivery is unconfirmed." with a Retry for the moment before the automatic resend under the same message id confirmed it. One rule now decides both: the resend runs, and the notice stays quiet, only while the send's answer is lost, the user never retried it, it was not still going out when the user pressed Stop, and the host has no record of it yet. Once the host has any record of it, the line and Retry are exactly as before. * fix(native-chat): say "Sending…" on a message until the host confirms it A send whose answer was lost was resent quietly under the same id but looked like any delivered message. Every message still in the outbox with no failure to show now says "Sending…", muted, in place of its time, until the journal holds a row for it. Rows that say a message did not go through keep only that line and its Retry. The notices read the outbox through the same reconcile as the transcript, so a row that lands clears the marker in one step. * fix(native-chat): keep "Sending…" until the host has the message, in the time's slot A message the user retried, a second message in doubt, or one requeued over a rejected row had a journal row that did not hold it, so it showed nothing and looked sent. "Sending…" now stays until the row is pending or accepted. The marker took the place of the whole meta row, so the copy button went away while sending and the row jumped when it cleared. The row now stays mounted: copy keeps its hover reveal and "Sending…" sits where the time goes. * test(native-chat): move the delivery probe tests into their own file NativeChatStructuredSessionDelivery.test.tsx went over the 800-line lint limit after main was merged. The eight tests for the automatic probe of an unconfirmed message move unchanged to NativeChatStructuredSessionDelivery.probe.test.tsx, which uses the shared structured-session test harness for its mocks. The outbox seeding and probe clock helpers move into that harness so both files share them. The at-most-once reliability gate now lists the new file, with a fresh evidence run. |
||
|
|
52b3766f48 |
Clarify keep-awake tooltip behavior by platform (#25323)
* fix: make keep-awake tooltip lid behavior platform-aware Extracted platform-specific lid behavior notes into a reusable function and updated the keep-awake tooltip to show accurate descriptions for each OS: macOS explains that closing the lid may still sleep the device, Windows references device power settings, and Linux clarifies Orca's lid-close request behavior. Updated copy to be device-agnostic instead of Caffeinate/MacBook specific. * fix: clarify macOS keep-awake tooltip copy and add translations Update agent keep-awake descriptions on macOS to accurately explain that the feature prevents idle sleep (not all sleep modes) and only works with the lid open. Add translations for Spanish, French, Japanese, Korean, and Chinese. |
||
|
|
0c761a7610 | Admit short required auxiliary checks after PR preflight (#25317) | ||
|
|
3655bd9fc2 |
fix(terminal): stop old output bleeding into Claude's screen when revisiting a remote tab (#24926)
* fix(terminal): leave the alt screen before replaying a pushed host snapshot A remote terminal running a full-screen agent (Claude Code) could show old output (e.g. a setup script's pnpm log) interleaved with the agent's screen after switching back to the tab. The host's pushed snapshot is a serialized image that starts on the normal buffer and enters the alternate screen itself, but the replay drain cleared with ESC[2J without leaving alt. The image's history painted into the agent's screen, its own ?1049h was a no-op, and the agent's diff paints landed on top of the stale cells. The remote-runtime transport now marks snapshots as serialized images, and the drain grounds them with the shared snapshot prologue (switching to the normal buffer first). Raw byte replays (SSH relay ring buffers) keep the in-place clear. * fix(terminal): paint folded remote snapshots from the normal buffer everywhere Review follow-ups: - Rename the flag to carriesNormalBuffer: what the painters rely on is that the image starts on the normal buffer and enters alt itself. - Hidden-output restore had the same bug for remote requested snapshots (history folded into data, alternateScreen set): the painter entered alt first and painted the normal buffer into the TUI's screen. Requested remote snapshots now carry the flag and take the normal-buffer start. - Flag pushed snapshots replayed after a cancelled shutdown too. - Pushed snapshots carry only the screen, so over a live TUI the drain keeps the normal-buffer history it covers instead of wiping it. - Read the pane's buffer after queued output parses. - Tests compare whole buffers against a fresh terminal, use production image shapes, and pin the raw-replay path with the same oracle. * fix(terminal): let the replay drain own the recovery snapshot clear The recovery prefix's own \x1b[3J ran after the drain's prologue and wiped the history the drain keeps under a live TUI. Keep only the latch release, which still makes an empty recovery snapshot non-empty so it is applied. * fix(terminal): keep the recovery snapshot's own screen clear Consumers that write recovery snapshots straight into xterm (no replay drain) rely on the prefix clearing the stale screen. Restore \x1b[2J\x1b[H and drop only \x1b[3J, which wiped history the image does not carry. * test(terminal): drive the drain with the real recovery payload The multiplexer prepends its own screen clear; replaying that exact payload pins that the prefix cannot wipe the history a TUI covers. * fix(terminal): keep TUI-covered history only when the grids match Second review follow-ups: - A pushed image carries only the host's screen; the pane's frozen history continues it exactly only on the same grid. On another grid keeping it duplicated or dropped lines, so the image replaces it there. - Tests replay the pane's writes and grid changes into a real terminal and compare whole buffers with the host, including a mismatched-grid case. - The split branch shares the normal-buffer preamble; drop the now single-use abort helper. - Type serializeBuffer as RemoteRuntimeSnapshotImage so the flag is carried by type, not by object pass-through. - Register the grid and raw-replay cases in the reliability gate. * fix(terminal): keep TUI-covered history only while the host is still on alt The drain kept the pane's history whenever the pane was on the alt screen. If the host's TUI exited while the tab was hidden, the shell wrote past that history and the kept lines no longer continued the host's screen. Require the host's own alternateScreen too; an absent flag proves nothing, so the image replaces history as on main. Also: one buildSnapshotReplayPreamble for every first replay write, the drain's image and raw clears as separate branches (raw byte-identical to main, comment restored), and a single paneAtSourceGrid check. * fix(terminal): gate kept history on the host's shell-owner proof The host sends alternateScreen only with terminalOwner 'shell', i.e. once it has proven the TUI exited, so `alternateScreen === true` never held for a live TUI and the drain wiped the history it should keep. Replace the history only once the host proves the TUI exited; tests now use production snapshot shapes. Move the relay-overlap comment into the raw-replay branch it describes. * fix(terminal): keep TUI-covered history only on a proven shared grid An image without its grid cannot prove the pane's history continues its screen, so it now replaces history. Also update a stale recovery-prefix test comment. * test(terminal): replay host-serialized snapshots through the real wire and drain Hand-written replay meta hid a gate that production never satisfies. Drive the host's own emulator, ownership mirror, serializer and recovery publisher through the real wire, client parser, remote transport and pane drain, for a live TUI and after the host proves it exited. * fix(terminal): repaint only the alt frame over a live TUI, leaving history alone Keeping the pane's history by clearing only the normal screen assumed a pushed image carries no history, which the host does not guarantee: a 0-row push can reuse a concurrent requested capture, and its history then landed twice in scrollback. It also wiped history on a grid mismatch, where main kept it. When pane and image are both on alt, the image's normal part adds nothing (the normal buffers froze together), so paint only its alt payload after an alt-side clear, split at the host's own boundary (splitAtAlternateScreenEntry, now shared with the daemon). Any other image paints from the normal buffer. This drops keepScrollback, the owner and grid gates, and the recovery-prefix change, so history behaves exactly as on main. * test(terminal): pin the parse wait and the cancelled-shutdown flag Final-review follow-ups: a drain test where the TUI's ?1049h is still queued when the image arrives, and a transport test replaying a push buffered during a cancelled shutdown; each fails when its guard is removed. The requested-image test now names the exited-TUI case it covers, requestSnapshot shares the snapshot image type, and the clear comment no longer implies every clear drops scrollback. |
||
|
|
baa56fd10d |
Simplify the phone-control and phone-size terminal dialogs (#25307)
* Redesign the phone-control and phone-size terminal dialogs Drop the eyebrow label and circled icon, shorten the copy so it no longer restates the buttons, and give each state one primary action with a quieter "all" action. Collapse moves out of the button row into a Minimize icon in the corner. Behavior is unchanged. * Point the phone settings copy at the renamed Restore button; drop dead ko overrides The phone app and desktop update independently, so name only "Restore", which matches both the old and new desktop banner labels. |
||
|
|
fb77c14386 |
fix: update Caffeinate tooltip copy about MacBook lid behavior (#23091)
Update tooltip and settings pane help text to accurately describe Caffeinate's behavior: it prevents idle sleep while active, but MacBook lid closing may still trigger sleep per device power policy. The previous copy incorrectly suggested Orca could prevent lid-close sleep. |