Commit Graph
9911 Commits
Author SHA1 Message Date
Brennan Benson bd3789ef24 refactor(native-chat): publish only where each submission was sent
A rejected send's own row now moves to its rejection (#24710), so the row that
resolved a send is the item's own position and needs no second copy. Keep
submittedSequence, the one journal-order record of where a send was sent.
2026-10-05 07:41:41 -07:00
Brennan Benson 4646472f09 Merge remote-tracking branch 'origin/main' into brennanb2025/publish-submission-positions 2026-10-05 07:15:08 -07:00
Neilandkambarakun 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>
2026-10-05 03:51:25 -07:00
Neil 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.
2026-10-05 03:43:14 -07:00
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>
2026-10-05 03:30:48 -07:00
Neilandnwparker 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>
2026-10-05 03:08:22 -07:00
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>
2026-10-05 01:51:49 -07:00
Neil 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
2026-10-05 01:29:44 -07:00
Neil 67fc708b3b Group CSV editor modules and tests in their own folder (#25476) 2026-10-05 01:16:20 -07:00
Ken Fukuyama 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>
2026-10-05 01:10:43 -07:00
Neil 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
2026-10-05 01:00:20 -07:00
Neilandmmarabel 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>
2026-10-05 00:07:23 -07:00
Brennan Benson 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
2026-10-04 23:19:58 -07:00
Jinjing 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
2026-10-04 23:01:02 -07:00
Jinwoo Hong 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.
2026-10-05 01:53:53 -04:00
Jinjing f873aaac5b Fix duplicate session option flags and preserve argument values (#25382)
* Fix duplicate session option flags and preserve argument values

* improve tests
2026-10-04 21:57:55 -07:00
Neil 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
2026-10-04 21:53:19 -07:00
Neil 7863871270 Keep OpenCode foreground evidence when its background service starts (#25378) 2026-10-04 21:34:09 -07:00
Jinwoo Hong 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
2026-10-04 23:08:02 -04:00
Neil 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
2026-10-04 19:58:35 -07:00
Neil 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.
2026-10-04 19:50:45 -07:00
+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: 23fc08b4e9
Related-to: stablya/orca#17551
Credits: @coelho-doti (stablya/orca#13065)

* Prepare complete private OpenCode launch validation source

Integrate the complete reviewed readiness, capability, native prompt, overlay and positional-run source onto frozen main. Preserve canonical atomic ACL retry, status generator/disposal, restrictive-umask fixtures and unowned source. Keep supported wrapped run commands positional.

Private-validation-source: a44345ce49
Original-full-source: 23fc08b4e9
Original-core-base: 8186ded0bd
Frozen-main: 08ee7ba9ef
Owned-source-paths: 111
Publication-policy: private validation only; preserve the six separate PR boundaries and held model/provider drafts

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Adnan Khan <adnank11427@gmail.com>
Credits: juli-gonzalez readiness contribution; Ahmed Nagy atomic plugin writer; coelho-doti positional run contribution

* Prepare private complete 111-path launch validation on current main

Private validation only. Preserve main credential additions and original launch ownership. Held model and provider topics remain excluded.

* Recognize env options before positional OpenCode run messages

* STRICT launch CI contract correction

* CAPS launch CI contract correction

* INTENT launch CI contract correction

* test(opencode): wait for malformed claim retries before expiring intent

Observe real endpoint I/O completion under fake timers before forcing expiry.

* test: initialize Claude prompt state in output retention fixture

* Verify OpenCode catalog model launches and preserve current launch behavior

* Verify OpenCode catalog model launches and preserve current launch behavior

* Refuse unverified new-worktree OpenCode models and complete host audits

* Wait for OpenCode location hydration in intent startup

* Refuse unverified new-worktree OpenCode model launches and record startup attribution

* fix(opencode): bind startup readiness to the composer location

* Bind OpenCode startup readiness to the current location in intent startup

* Restore the owning Orca CLI path after shell profiles

* Use a literal marker for the Bash lookup regression

* Preserve plain panes and initialize zsh after prompt hook replacement

* Preserve user line-editor dispatchers during deferred startup

* fix: retain CLI startup when global Zsh replaces prompt hooks

* test: replay global Zsh hook replacement after host startup

* test: isolate controlled Zsh widgets from distro keyboard setup

* fix(shell): preserve user hooks during deferred zsh initialization

* Retry interrupted OpenCode startup prompt claims

* Keep completed Zsh startup hooks retired when the wrapper is sourced again

* Reject truncated OpenCode catalogs and explain worktree model limits

* Use a template literal in truncated-catalog coverage

* Refuse unfinished OpenCode model catalogs

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: user141514 <user141514@users.noreply.github.com>
Co-authored-by: Ahmed Nagy <ahmednagy25t@gmail.com>
Co-authored-by: Adnan Khan <adnank11427@gmail.com>
Co-authored-by: Orca startup hydration review <agents@stably.ai>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Orca maintenance <orca-maintenance@users.noreply.github.com>
Co-authored-by: Orca <dev@stably.ai>
Co-authored-by: Orca campaign <orca-campaign@local.invalid>
Co-authored-by: Orca OpenCode issue campaign <opencode-campaign@stably.ai>
Co-authored-by: OpenCode issue campaign <codex@localhost>
Co-authored-by: Nathan Parker <nwparker@users.noreply.github.com>
2026-10-04 19:27:20 -07:00
Jinwoo Hong 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.
2026-10-04 21:39:22 -04:00
Brennan Benson 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 ae0b9f3fea.

* Revert "fix(notes): a new chat's saved message keeps its notes out of another send across a reload"

This reverts commit b88d0dc2cc.

* fix(native-chat): a new chat joins only a re-delivery of the same user action

"The same request" was recognised by its content (agent, workspace, trimmed
text, sent or drafted), which split one action whose text changes between
deliveries ("Fix with AI" re-fetches logs) and merged two different actions
that happen to carry the same text.

Each user action now mints one request id where it is handled (the click,
menu pick, notes send, shortcut, Fix with AI press, quick command; a worktree
create uses its creation id; programmatic starts mint once at their entry)
and passes it through the launch plan, its verdict and the structured launch
options, where it is required. A new start joins a chat only when its first
attempt carries the same id: a double click or a caller retrying its own call
makes one chat and sends the first delivery's text once; any other action
opens its own chat, whatever its text. An empty chat that is still starting
is still claimed once by the first action with text, and then belongs to that
action's id. Resume launches keep joining their conversation's launch.

The + menu and new-tab search no longer grey out an agent while one of its
chats starts: every pick is a new action and opens its own chat. The id lives
in memory only; nothing reads it after a reload (a restored launch is reached
by its session id through Retry, never joined).

* fix(native-chat): a new chat with no text reuses an empty one instead of stacking another

Two bare "+ > Claude" picks (or new-tab search, the new-agent shortcut, the dashboard) opened two
empty chats. A request with no text now focuses an existing empty chat for that agent in that
workspace: one still starting (joined in the launch, as a re-delivery is) or one that published and
sits idle (its host's journal holds no request, read live from the status feed). Empty also means
nothing queued, no composer text or images, and no launch draft its composer has not taken. Failed,
unconfirmed, resumed and other-agent/workspace chats are never reused. A request with text still
opens its own chat, or claims an empty starting one as before; request-id dedupe is unchanged.

* fix(native-chat): a new chat with no text reuses an empty chat only in the split it was opened from

Pressing + in the right-hand split focused an empty chat sitting in the left-hand split. The reuse
now looks only in the tab group the pick targets (the caller's group, else the workspace's active
group, which is where its tab would open); a pick in another split opens a new chat there. Applies
to both an empty chat still starting and one that published and sits idle.

* fix(native-chat): text sent to a new agent claims an empty starting chat only in its own split

Notes sent from the right-hand split could land in, and focus, an empty chat still starting in the
left-hand one. A request with text now claims an empty starting chat only in the tab group it opens
in, the same rule a request without text follows; elsewhere it opens its own chat there.
2026-10-04 18:12:10 -07:00
Jinwoo Hong 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.
2026-10-04 21:08:15 -04:00
Brennan Benson 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 `![](NUL.png%2F.)`) 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.
2026-10-04 16:55:32 -07:00
Neil 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
2026-10-04 16:49:12 -07:00
+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: 23fc08b4e9
Related-to: stablya/orca#17551
Credits: @coelho-doti (stablya/orca#13065)

* Prepare complete private OpenCode launch validation source

Integrate the complete reviewed readiness, capability, native prompt, overlay and positional-run source onto frozen main. Preserve canonical atomic ACL retry, status generator/disposal, restrictive-umask fixtures and unowned source. Keep supported wrapped run commands positional.

Private-validation-source: a44345ce49
Original-full-source: 23fc08b4e9
Original-core-base: 8186ded0bd
Frozen-main: 08ee7ba9ef
Owned-source-paths: 111
Publication-policy: private validation only; preserve the six separate PR boundaries and held model/provider drafts

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Adnan Khan <adnank11427@gmail.com>
Credits: juli-gonzalez readiness contribution; Ahmed Nagy atomic plugin writer; coelho-doti positional run contribution

* Prepare private complete 111-path launch validation on current main

Private validation only. Preserve main credential additions and original launch ownership. Held model and provider topics remain excluded.

* Recognize env options before positional OpenCode run messages

* STRICT launch CI contract correction

* CAPS launch CI contract correction

* INTENT launch CI contract correction

* test(opencode): wait for malformed claim retries before expiring intent

Observe real endpoint I/O completion under fake timers before forcing expiry.

* test: initialize Claude prompt state in output retention fixture

* Verify OpenCode catalog model launches and preserve current launch behavior

* Verify OpenCode catalog model launches and preserve current launch behavior

* Wait for OpenCode location hydration in intent startup

* Refuse unverified new-worktree OpenCode model launches and record startup attribution

* fix(opencode): bind startup readiness to the composer location

* Bind OpenCode startup readiness to the current location in intent startup

* Restore the owning Orca CLI path after shell profiles

* Use a literal marker for the Bash lookup regression

* Preserve plain panes and initialize zsh after prompt hook replacement

* Preserve user line-editor dispatchers during deferred startup

* fix: retain CLI startup when global Zsh replaces prompt hooks

* test: replay global Zsh hook replacement after host startup

* test: isolate controlled Zsh widgets from distro keyboard setup

* fix(shell): preserve user hooks during deferred zsh initialization

* Retry interrupted OpenCode startup prompt claims

* Keep completed Zsh startup hooks retired when the wrapper is sourced again

* Reject truncated OpenCode catalogs and explain worktree model limits

* Use a template literal in truncated-catalog coverage

* Refuse unfinished OpenCode model catalogs

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: user141514 <user141514@users.noreply.github.com>
Co-authored-by: Ahmed Nagy <ahmednagy25t@gmail.com>
Co-authored-by: Adnan Khan <adnank11427@gmail.com>
Co-authored-by: Orca startup hydration review <agents@stably.ai>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Orca maintenance <orca-maintenance@users.noreply.github.com>
Co-authored-by: Orca <dev@stably.ai>
Co-authored-by: Orca campaign <orca-campaign@local.invalid>
Co-authored-by: Orca OpenCode issue campaign <opencode-campaign@stably.ai>
Co-authored-by: OpenCode issue campaign <codex@localhost>
2026-10-04 16:44:38 -07:00
Neil 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
2026-10-04 16:39:12 -07:00
Neil d3afb5c5a9 Preserve large paste destinations and native Undo boundaries (#25177) 2026-10-04 16:22:23 -07:00
keiandsetodeve 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 b30f095.
Verified on required stock Linux X11/Wayland checks and independent frozen-source review.

Co-authored-by: setodeve <keinick11@outlook.com>
2026-10-04 16:21:04 -07:00
Brennan Benson 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.
2026-10-04 16:08:40 -07:00
Jinjing 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.
2026-10-04 16:05:00 -07:00
Jinwoo Hong 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.
2026-10-04 19:00:09 -04:00
Jinwoo Hong 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.
2026-10-04 18:59:50 -04:00
Jinjing 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.
2026-10-04 15:57:39 -07:00
Jinjing 4a41e246db Fix: settle sortEpoch in store to prevent React update depth exceeded (#25313)
* fix(sidebar): stop Manual sort from mirroring sortEpoch into state

In Manual mode the sort hook copied every sortEpoch bump into state from an
effect, adding a nested React update per bump. A burst of store bumps at
startup stacked those into 'Maximum update depth exceeded' (React #185).
Manual now reads the live epoch directly; debounced modes are unchanged.

* feat(sidebar): tell people when a drop switches sort to Manual

Reordering by drag still switches the sidebar to Manual so the drop sticks,
but it used to happen silently. Show a toast with a 'Back to <previous sort>'
action; it retires itself on any later sort change so it can't override a
newer choice. Drops while already in Manual stay silent.

* feat(store): settle sortEpoch in the store instead of in React state

Add settledSortEpoch plus a 3 s settle timer owned by a store listener
installed in the state creator. Every write path (slice actions, the web
session sync patch) goes through it, so the settled value stays correct
with no sidebar mounted. Manual, a sort-mode switch, and a bump that
changes the non-archived row count settle in the same notify; other bumps
restart the window. A reset that lands settled clears the timer, and the
disposer runs on HMR teardown.

* fix(sidebar): read the settled sort epoch instead of mirroring it into state

The sort hook no longer copies sortEpoch into React state from an effect in
any mode; it reads the store's settledSortEpoch directly. That pattern added
a nested update per bump and stacked into "Maximum update depth exceeded"
(React #185) under flushSync bursts. Tests cover Manual, add/remove, burst
reset, no-bump row changes, mode switch, and 80 flushSync bumps in Recent
while worktrees are added.

* cleaning up

* fix(store): settle bumps when rows update during pending window

Detect structural changes on worktreesByRepo updates in addition to sort
epoch changes. When a row arrives without its own bump during a pending
settlement window, the changed composition must still trigger settlement.
2026-10-04 15:55:52 -07:00
Neil a753affffe Isolate code editor text and Undo history by execution host (#25174)
* Isolate code editor text and undo history by execution host

* Verify editor owner resolution and Windows model path isolation

* Respect native model line endings in content sync history coverage

* Preserve selection and scroll when editor ownership resolves

* Verify owner synchronization against a reachable editor change callback
2026-10-04 15:27:30 -07:00
Neil 8e8efb1947 Reduce avoidable work in PR checks and SSH test setup (#25309) 2026-10-04 15:26:53 -07:00
8ff6ec9fb1 Install OpenCode hooks in the terminal's config directory (#25296)
* test: reproduce OpenCode plugin installation in the wrong config root

* fix: install OpenCode hooks in the execution config directory

* test: isolate config installation from CLI version probing

* style: format consumer config installation controls

* fix: use checked startup environment and supported relay shell context

* fix: install OpenCode hooks using the selected execution shell

* test(opencode): assert consumer config root in PTY fixtures

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Orca <orca@stably.ai>
2026-10-04 15:20:07 -07:00
Neil f371532707 Bound search preview retention and stop canceled remote scans (#25303) 2026-10-04 15:12:32 -07:00
Neil 6c07570040 fix(cursor): keep usage cookies on the selected account (#25243)
Keep Cursor quota requests tied to the selected account by omitting ambient Electron session credentials. Preserve explicit account cookies and redirect handling.

Credit: Li-Sanze for the original credential-mode fix in #23626, carried through #24575; jjongsta and chengjiaxiao for the reports. Native HTTP/HTTPS cookie isolation, mutation controls and proxy behavior were verified before merging. This does not claim to resolve the separate initial-authentication failure in #23612.
2026-10-04 14:47:05 -07:00
Borys Papevis ca774091d7 fix(agents): recognize Pi bundled npm entrypoint
Carry the focused patch from boris-papevis/orca PR #25040 onto current main, with independent npm runtime and regression verification.
2026-10-04 14:46:24 -07:00
Brennan Benson b1e12d7bb4 fix(native-chat): a new structured chat's model list comes from the machine that runs it (#25143)
* fix(native-chat): a new structured chat's model list comes from the host that runs it

agentSession.modelCatalog builds the structured-session host like agentSession.options,
so a host with no saved chats since it started answers instead of refusing. When the
host answers unknown because its first listing runs in the background, the picker
re-reads on a bounded schedule until that listing lands. Local terminal-backed chat
skips the structured catalog when a custom launch command is configured.

* fix(native-chat): wait on the host's first model listing instead of re-reading on a timer

A new structured chat's picker read the host catalog once; on an account the
host had never listed, the answer was "unknown" while a background listing ran,
and the client re-read on a 1-30 s schedule. Replace the schedule with the
host's own completion signal:

- The host answers a cold read with `listingInProgress: true` (new optional
  field) once it has started or joined that listing. A read that passes the new
  optional `waitForListing` param awaits the same joined listing and answers
  with it, or a plain "unknown" if it failed. The at-rest options read never
  waits. A host that predates the field never sends it, so the client never
  sends the param to a host that would refuse it.
- The picker reads once per open and attach; after the host's report it sends
  one waiting read, with a 90 s client timeout above the slowest listing.
  While that read is out, the model pill keeps its label but cannot open or be
  set (typed /model included). An answer, failure, timeout, hide, attach or
  the provider's own list releases it.
- `agentSession.modelCatalog` builds the structured-session host only for a
  read that names a session (a structured chat). Terminal-backed chat's
  session-less read keeps the non-building gate, so a desktop that never runs
  structured chat never opens the session journal.

* fix(native-chat): one waiting model-list read per chat, and no late menu open

The waiting catalog read was owned by one run of the picker's effect. Attach
(a new fence), hide/show or a send re-ran the effect: the cleanup released the
model picker onto the built-in list for a round trip, and the new run sent a
second waiting read while the first, which cannot be withdrawn, kept a remote
call slot until the listing ended.

The waiting read now belongs to the chat (runtime target + agent + session):
a small registry keeps one in flight per chat, every re-run or remount joins
it, and the entry is deleted when the read settles. The picker hold is derived
from that entry being in flight, so it lasts across attach and hide/show and
ends when the read settles, the provider reports its own list, or the pane
switches to another session. Answers still pass the stale and record checks.

A bare /model typed while the list loads no longer opens the model menu by
itself when the list lands: the menu stays keyed on the request, and only its
initial open is suppressed while pending, so the request is spent shut and the
end of the pending period never remounts it.

* fix(native-chat): release the model picker in the same commit as the host list

When the waiting catalog read settled in the chat that started it, the
registry dropped its entry and told subscribers first, and the host list was
applied a few microtasks later. React committed once with the picker enabled
on the built-in list, then again with the host's list.

Joiners now hand the registry their apply callback, and the registry runs
every joiner (with the answer, or nothing when the read failed or timed out)
before it deletes the entry and notifies. The release and the list land in one
commit. An effect cleanup leaves the wait instead of flagging itself stale.

* fix(runtime): queue model catalog reads in the long-wait lane

A model catalog read that waits on a host's first listing replies only when
that listing ends, yet it took one of the 8 foreground call slots for its
server. Enough chats opened during one cold listing would stall that server's
sends and interrupts until a wait settled.

agentSession.modelCatalog now joins worktree.rm in the long-wait lane: same
concurrency, counted apart from the foreground calls. The queue classifies by
method only, and a warm catalog read answers at once, so the whole method
moves.
2026-10-04 14:37:40 -07:00
Brennan Benson e42768fb23 Update in-app Android APK links to mobile 0.0.52 (#25168) 2026-10-04 14:32:12 -07:00
f0b5b8566c Bound OpenCode history reads and repeated worker failures (#25292)
* fix(opencode): keep scan budgets across queue waits and batches

Reuse the scan-owned lifetime proposed in #10708 by @AmethystLiang with the existing shared worker queue.

* test(opencode): check nonempty session fixtures and lint scoped controls

* Derive OpenCode scan deadline message from its budget

---------

Co-authored-by: Neil Parker <nwparker@MacBook-Pro-3.localdomain>
Co-authored-by: OpenCode issue campaign <codex@localhost>
2026-10-04 14:31:22 -07:00
Brennan Benson 97fa6aee74 fix(native-chat): show a message Orca accepted and then failed to deliver as "Not sent" in the chat (#24710)
* fix(native-chat): keep a message the host accepted then rejected in the desktop chat as not sent

Draw it in place from the host's history, so a crash that loses the outbox no
longer makes it vanish. A later copy of the same body supersedes it; the outbox
row wins while it holds the message; the phone is unchanged.

* test(native-chat): pin the same-id rule apart from the body match

* test(native-chat): type the rejected-in-place fixture body as a text block

* fix(native-chat): let the host's row own a message it recorded and then rejected

Once the host's journal records a send as rejected, the desktop outbox lets it
go, as it already does for delivered and Stop-withdrawn sends: the host's row
shows it as not sent, with the host's reason and no Retry. The outbox keeps
only sends the host refused before recording them, which keep their Retry.
A send whose own reply says it was rejected is drawn by its outbox entry, with
no Retry, until the journal carries the row; a copy left by an earlier session
is dropped when the chat opens.

- the transcript no longer hides a host row behind an outbox entry with the
  same id or the same text; those rules and their cache are gone
- a rejected message the queue holds (a draft's hand-off, or a live card under
  its id) is drawn as its card, not as a row
- a later copy of the same text hides a rejected row only when it was sent
  once the rejection was known, so a deliberate repeat stays
- delivery notices read the same visibility rule as the transcript; a chat
  whose only rejection a Stop withdrew no longer rebuilds them per batch
- the body fingerprint helper goes back to the host, its only user

* test(native-chat): keep one row when copies of a rejected message share an instant

* refactor(native-chat): let the host's notice replace the outbox's under the same id

* test(native-chat): pass the queued card ids in the tool-stream cost transcript

* fix(native-chat): keep the host's record as what lets a rejected message go

- the outbox no longer drops a host-rejected message when a chat opens; the
  reconcile lets it go once the journal's submissions say it was rejected, and
  that drop is written to storage, so nothing reads as still owed
- a message the host rejected while the chat watched waits for its journal row
  with no Retry; one read back from storage with no row loaded keeps its Retry
  under a new id, since the host may have lost it
- the delivery notices keep the same map and notice objects across a batch that
  words every row the same, so a submission batch re-renders no row
- a rejected command such as /compact stays hidden: its own reply reports it
- the desktop transcript requires the queued card ids, with a controller-level
  test that a card holding a rejected message keeps its row hidden

* fix(native-chat): draw a queued message where the host rejected it

A message accepted to hand over later and rejected before any handover now sits
at its rejection, as a handover places one: what the agent did while it waited
happened before it, and the newest history page holds it. One handed over, or
dispatched as it was recorded, keeps its place. An older host does not move it,
so it stays at its submission, still drawn.

A failed start now rejects the queued messages and writes its row in ONE
journal append, the messages first: no reader ever meets one without the
other, and the messages still draw above the row that says why.

* test(native-chat): pin that rows written together roll back together

* fix(native-chat): draw every rejected message where it was rejected

Not only a queued message: one handed over into a turn and then rejected, or
sent directly and rejected, also sits at its rejection, in no turn. A message
in doubt stays where it was, a plain bubble: it may have reached the agent.

* fix(native-chat): decide a rejected message's Retry from the host's stored fact

- a message the host recorded and then rejected has no Retry on any mount,
  however that mount learned of it, and a Dismiss that clears it from storage;
  a send refused before the host recorded it keeps its Retry
- the rule that keeps a rejected command such as /compact out of the
  transcript moves into the one visibility function rows and notices share
- the outbox state docs say what lets a recorded message go: the client holding
  its rejected submission, whose row the host places at the rejection

* fix(native-chat): write no start-failure row when a Stop withdrew every queued message first

* test(native-chat): pass the Dismiss action in the delivery-notice hook tests

* fix(native-chat): keep a rejected message's outbox copy until its row loads

An older host leaves a rejected message where it was sent, which may be older
than the loaded window: the chat then holds the rejected submission but not the
row that draws it. The outbox copy now stays until that row loads, marked as
the host recorded it (Dismiss, no Retry, in the host's words), and leaves once
the page holding the row is loaded. Derived from the loaded rows each time.

Tests that label their projection as the phone's now pass the phone's own
setting.

* test(native-chat): type the outbox hook props that carry loaded rows

* fix(native-chat): write nothing when a journal batch settles nothing in the outbox

The outbox re-reads the journal on every batch since it waits for a rejected
message's row to load. Its reconcile now returns each unchanged entry, and the
list, as themselves (a message left in doubt included), so a batch that changes
nothing writes nothing to storage. The reconcile moves to its own module.

A copy the host recorded and rejected owes no delivery, so it no longer keeps a
hidden pane reading the journal.

* test(native-chat): count storage writes on the outbox's own storage object

* fix(native-chat): let a recorded rejected message's outbox copy leave on its own, with no Dismiss

The outbox copy of a message the host recorded and then rejected draws it only
while the host's row is not loaded, and leaves on the batch or page that loads
that row. It owes no delivery and offers no control: sending it again is a new
message. The Dismiss that let the user clear it is gone, from the outbox, the
notices and the session controller.
2026-10-04 14:28:10 -07:00
Neil ab41610ba6 Fix reordering workspaces with collapsed children (#25302) 2026-10-04 14:24:50 -07:00
Brennan Benson 6f4fcc2dc4 Merge remote-tracking branch 'origin/main' into brennanb2025/publish-submission-positions 2026-10-04 14:24:13 -07:00
Brennan Benson b99d28e32f A resent chat message gets its recorded answer, never an early refusal or a made-up record (#25158)
* fix(native-chat): a resent send id gets its recorded answer, never an early refusal or a made-up record

The host now looks a resent send id up before preparing the session. A row
that settled refused answers with its refusal before the chat is opened. A
resend whose chat cannot be opened or made ready answers unknown instead of a
refusal. A /clear in flight refuses only ids the ledger does not hold.

A send row now records the journal epoch it was admitted into. An unsettled
row with nothing written in that same epoch runs for the first time; under a
later epoch the host answers unknown instead of reconstructing a submission
it never had. The host advertises agent-session.send-answers-proof.v1.

* test(native-chat): pass the ledger row to the thread-goal rerun check

* fix(native-chat): a send's answer commits with its write, and a resend is answered before any write

A send (and /compact) settles its ledger row `succeeded` in the same SQLite transaction as the
submission or queued draft that accepts it, so a row still `pending` proves nothing was written and
a resend runs it for the first time. The unknown-before-run mark and the per-row journal epoch go.

A resent id is answered from its row and the journal before preparation starts an agent and before
any write transaction: a recorded refusal with nothing opened; otherwise the conversation is opened
(no agent start for a send) and replayed, and a conversation that will not open answers unknown.

* fix(native-chat): a ledger refusal is answered first, and a /clear refuses only a send's first run

An id the ledger refuses (expired, conflict, invalid, capacity) is answered as admission would,
with no journal read, preparation or write, so a closed chat or a read-only store answers it too.
A re-read after the replay open that comes back refused returns that refusal.

Whether a /clear is in flight is read when a send arrives and applied in the send's preparation
for a first run only: an id the ledger holds by the send's turn, including one whose earlier
attempt was queued ahead of the clear, is answered from its record. MutationPlan makes
settlesWithWrite and settledOutcome exclusive; the capability text no longer promises a refused
id never sends.

* docs(native-chat): say what a replay's preparation does for every plan
2026-10-04 14:01:29 -07:00