Commit Graph
12733 Commits
Author SHA1 Message Date
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
kazuma.mikiandkazooooo-ma b3328390f9 Remove completed SSH picker E2E plan (#24824)
Co-authored-by: kazooooo-ma <zooooomer-com@gmail.com>
2026-10-05 01:49:54 -07:00
Daiki HirayamaandNeil c2c7649849 fix(mobile): stop Android from selecting words while the chat transcript scrolls (#22871)
* fix(mobile): stop Android from selecting words while the chat transcript scrolls

On Android every paragraph, heading, quote, code block and table cell in
the native chat transcript was a selectable TextView. Android starts a
word selection, with the magnifier, on a double tap or a long press, and
two flicks in the same spot while scrolling a FlatList register as a
double tap, so scrolling the chat kept selecting words. iOS is unaffected:
its UITextView path arbitrates scroll against selection itself.

Android now renders transcript text without inline selection: one gate in
MarkdownText covers every selectable span, and the user bubble follows
it. A long press on a message opens a sheet with "Copy message" and
"Select text", the latter a screen whose only content is one selectable
Text, so a selection can only start where the user asked for it. The
message row owns that sheet and mounts it only while open. iOS and web
keep their inline selection and get no long-press handler.

Verified on a Pixel 10 Pro Fold (Android 17): an adb double tap on the
transcript selects nothing, the same double tap inside "Select text"
selects a word, tool rows inside the bubble still expand on tap, and the
long press opens the sheet.

* fix(mobile): scope the Android selection gate to the transcript and route span long presses

Review follow-ups on #22871:

- The gate now applies only where `rangeSelectable` is passed (the chat
  transcript). Task comments and file previews keep their selectable text
  on Android as before.
- On the Android transcript, spans that take taps (links, file paths)
  also take the row's long press, so a link under the finger no longer
  swallows the copy/select sheet.
- Copied text keeps its whitespace; only whitespace-only blocks are dropped.
- The Android markdown test compares `String(node.type)` instead of a
  type assertion, which the changed-code quality gate rejects.

* fix(mobile): route long presses on Markdown images to the row on Android

An image block is a Pressable of its own, so on the Android transcript it
now carries the row's onLongPress like tappable spans do; a long press on
an image opens the copy/select sheet instead of being swallowed. Test
extended with an image block.

* test(mobile): pin the Android long press from a chat row to its actions sheet

The existing row suite runs as iOS, where the bubble has no long press.
This one runs as Android: the bubble's long press mounts the actions
sheet with the message, the markdown receives the same handler, closing
unmounts the sheet, and the user bubble carries no inline selection.

* fix(mobile): preserve Android message selection while replies stream

---------

Co-authored-by: Neil <neil@stably.ai>
2026-10-05 01:41:19 -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 b94cfa7fd0 feat(relay): declare Asia spare cell c34 as migration-only (#25336)
Adds a sixth asia-east2 cell at the C31 shape (cap 3000, 6000 request
units, pool 16, e2-standard-4) in asia-east2-c, pinned to the f30b5cb1
cell image. It gets its own topology and registration wave but no
promotion wave, so the admission script and workflow refuse to promote
it; it stays a migration-only landing zone and out of the fleet pool list.

Claude-Session: 1145a80d-dec4-4a9b-9373-bbbb876b9041
2026-10-05 00:26:33 -04:00
Jinwoo Hong e347aa4e67 fix(mobile): honour the desktop pinned-worktree placement setting (#25301)
* fix(mobile): honour the desktop pinned-worktree placement setting

Mobile always kept pinned rows in their groups as well as in Pinned. Desktop's
showPinnedWorktreesInGroups (default off) shows them only in Pinned. Read that
setting with its own settings.get operation, refreshed with the list's view
settings, and leave host and device-local pins out of their groups unless it
is on.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): keep lineage children with a pinned parent

Under single-location a pinned parent left its group while its unpinned child
stayed behind as a root. Visible descendants now follow a pinned ancestor into
Pinned, nested under it, as on desktop. Drop the inert host-id half of the
pinned-policy stale-reply guard; a host switch replaces the client.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): fold pinned placement into the desktop view-settings sync

One "sync from desktop" callback now also reads settings.get for pinned
placement, unawaited so a slow read never holds the ui.get merge; the separate
callback and its plumbing are gone. Re-record the three host.view-settings
goldens, which gain only that settings.get send. The pin expansion walks the
lineage children index the renderer now shares, over visible rows only.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): one stale-reply rule for the desktop view-settings sync

Both reads in syncViewSettingsFromDesktop now drop a reply only when the client
was replaced; the host-id half compared a captured value with itself. Pin the
no-wait invariant: the ui.get merge applies while settings.get never answers.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): follow a pin through rows hidden by search

Desktop's Pinned section walks lineage over every worktree and keeps the
visible descendants, so a grandchild still follows a pinned root when search
hides the middle row. Mobile walked visible rows only and left it in its group.
Build the children index from the unfiltered list; membership stays visible-only.
Drop the hostId arg the view-settings sync no longer reads. Pinned-lineage
tests move to their own file to stay under max-lines.

* refactor(mobile): read pinned placement in its own hook

The placement read was folded into the desktop view-settings sync, so that
callback juggled an awaited and an unawaited read and three goldens recorded a
settings.get that never answers. useHostShowPinnedInGroups now owns the setting
and the catalog refreshes it beside the view-settings sync on connect, focus and
mount; the sync, screen state and goldens are back to main.

The reader returns desktop's boolean, as its siblings do, so transport no longer
imports a worktree type and the two-literal policy union is gone. makeSection
always applies lineage, the pinned walk is a Set worklist, and the lineage
children index drops a set that duplicated its map's keys.

* fix(mobile): key pinned placement to the client that reported it

The host screen is reused across hosts, so the previous host's "show in groups"
value survived a switch until the new read landed, or for good if it failed.
The setting now carries the client that reported it and counts only for that
client, which also makes a late reply from a replaced client inert and drops
the clientRef plumbing.

* fix(mobile): drop a replaced client's late placement reply

Keying the value to its client hid a previous host's value but still let that
host's late reply overwrite the current host's, reverting the list to the
default until the next refresh. Restore the clientRef write guard; the client
key still covers the effect-long window before clientRef follows a switch.

* refactor(mobile): treat pinned placement like the screen's other host state

The placement hook kept its own host-switch handling (a client-keyed value plus
a clientRef guard) beside the screen's existing one. showPinnedInGroups now
lives in HostScreenState, resets with the other host-scoped values in
useHostScreenIdentity, and is read in the catalog refresh behind the same
clientRef check as the catalog fetch. The hook and the catalog argument go.
2026-10-04 23:53:01 -04: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
Jinwoo Hong 52bd9afe9d refactor(relay): stop creating three tables nothing writes (#25354)
* refactor(relay): stop creating three tables nothing writes

relay_confirmable_splices, relay_cell_drain_attempts and
relay_migration_leases are created at every boot and referenced nowhere
else on main except tests. In production all three hold 0 rows and
pg_stat_user_tables shows 0 inserts, 0 sequential and 0 index scans since
the 2026-09-28 stats reset.

This removes them from the schema and the transaction-phase table, and
drops the test references. It does not drop them: an older image still
runs CREATE TABLE IF NOT EXISTS at boot, and a drop racing that create can
fail the older boot's schema step. A follow-up can drop them once no image
that creates them can be rolled back to.

* docs(relay): note the account-erasure prerequisite for dropping retired tables
2026-10-04 23:04:17 -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
Jinwoo Hong e884681eca fix(relay): retain confirm results for a week and audit events for 90 days (#25353)
* fix(relay): retain confirm results for a week and audit events for 90 days

relay_confirm_results (8.3M rows, 5.8 GB) and relay_audit_events (8.4M
rows, 3.6 GB) were never deleted. The director's credential cleanup now
reaps both after its existing passes:

- Confirm results older than 7 days. The only reader replays a stored
  result for a retry of the same request on the same connection basis; a
  different basis is already refused as a tuple mismatch. committed_at has
  no index, so this uses the TID-window reaper from the reservation prune,
  capped at 250 rows a tick (the 7.4M-row backlog drains over 2-3 days).
- Audit events older than 90 days, ordered by `at` so the batch walks
  relay_audit_events_at. Nothing in the relay reads them back. The oldest
  row is from 2026-07-14, so this deletes nothing until 2026-10-12.

reapBatch now deletes by `ctid = ANY(ARRAY(...))` on Postgres: with
`ctid IN (...)` the planner can choose a hash join over a sequential scan
of the whole table.

* chore(relay): record the decided audit retention
2026-10-04 22:56:31 -04: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
Jinwoo Hong aac1c8f9c2 chore(relay): move US cells c32 and c33 to the general same-cap list after promotion (#25367)
Both were promoted to general on 2026-10-01 (selector 344 shows them general),
but the same-cap job still classed them migration-only. Rollback mode on either
would isolate (general -> migration-only) before its no-op check failed, demoting
a live cell. They stay out of the fleet pool list (pool 10, not 16).

Claude-Session: 1145a80d-dec4-4a9b-9373-bbbb876b9041
2026-10-04 22:39:12 -04:00
Jinwoo Hong e9daf9b746 fix(relay): prune released control-connection reservations in bounded batches (#25352)
relay_control_connection_reservations was never deleted: 13.8M rows and
9.1 GB in production, 99.999% of them in state 'released' and ~400k more
a day. Nothing reads a released row (every reader filters it out by
state), but each placement still locks all of its host's rows, ~160 on
average.

A new director sweep step deletes released rows older than a day. It walks
the heap in TID ranges of 16 pages, because without an index on
released_at a `LIMIT n` delete plans as a sequential scan from page 0 that
gets slower as the head of the heap empties (production EXPLAIN). Each
statement selects its rows FOR UPDATE SKIP LOCKED, so a row a request holds
is skipped rather than waited on, and deletes them by `ctid = ANY(ARRAY(...))`
so the delete is always a TID scan. A tick stops at 400 rows, 128 pages or
250 ms, which drains the backlog over about three days at five directors.
2026-10-04 22:38:24 -04:00
Jinwoo Hong b9b0f29172 fix(relay): skip the dead-cell sweep's host scan when no cell qualifies (#25350)
evacuateDeadCells ran one query that joined every assignment to its cell's
liveness and fence state, ordered by primary key with LIMIT 100. In
production no row ever matched (the only dead cells are stale existing-only
cells without a committed fence), so the planner walked the whole
relay_assignments primary key every call: 551 ms mean over 85k calls,
~12% of relay database time.

A cell-level pre-check now evaluates the predicate's cell-only half over
the ~33-row cell tables (3 ms in production). It is a superset of the cells
the host query can act on, so the sweep returns early when it is empty and
otherwise restricts the host query to those cells. The host predicate is
unchanged.
2026-10-04 22:38:16 -04:00
822fc5bed4 Add repository OpenCode permission defaults (#25326)
* test(config): reproduce rejected repository OpenCode config

* Add repository OpenCode permissions and allow its reviewed root config

* fix: preserve sensitive OpenCode confirmation prompts

---------

Co-authored-by: Orca campaign recovery <campaign-recovery@example.invalid>
Co-authored-by: Orca OpenCode Campaign <opencode-campaign@local.invalid>
2026-10-04 19:34:47 -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
github-actions[bot] ccfc8fe2c4 Update README downloads badge 2026-10-05 01:10:28 +00: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
PM 75344e5850 docs: align contributor guidance with the PR template (#25034) 2026-10-04 16:19:53 -07:00
Jinwoo Hong 1978469fd2 fix(mobile): keep the working rings turning on the OTA page (#25299)
* fix(mobile): keep the working rings turning on the OTA page

Animated.loop starts a native loop whenever the timing asks for the native
driver; the web has none, so the JS fallback ran one turn and froze at 360deg.
Ask for the native driver only off the web.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): pin the native driver on native spinners

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): share the working ring rotation between both rings

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-04 19:12:00 -04: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
Neil 0c761a7610 Admit short required auxiliary checks after PR preflight (#25317) 2026-10-04 16:01:22 -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