Commit Graph
15247 Commits
Author SHA1 Message Date
af6fe23623 A pane close by its terminal handle closes only that pane, even before the window lists it (dark, layout-owner build only) (#30920)
* fix(runtime): main's relisting for a close carries main's tab layout, so a pane close stays a pane close (dark, layout-owner build only)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(layout-owner): the relist touches only a row it added, with its own copy of the layout

* fix(layout-owner): store the relist's layout only when it changed anything

---------

Co-authored-by: E2E Test <e2e@test.local>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 21:23:52 -04:00
Neil f1135dd4ca fix(ssh): say when this computer can't download Node for a managed server, and where an offline copy goes (#30911) 2026-10-19 18:22:24 -07:00
Neil 28d9f959de Run selected SSH tests instead of the whole suite on PRs (#30928)
* fix(ci): run only selected managed SSH specs on pull requests

* fix(ci): retain full SSH coverage for routed shared helpers
2026-10-19 18:21:53 -07:00
Neil 8d0501622a feat(agents): add Auto to the global agent permission switch (#30677)
* feat(agents): configure Auto terminal permissions per supported agent

* feat(agents): restore global permission switch with Auto, per-agent exceptions

Replace the Set defaults menu and inline per-row dropdowns with a global
Manual | Auto | Yolo switch. Auto reveals an Other agents control beneath it,
mixed states badge exceptions, and each row's expanded panel owns its
per-agent permission choice.

* test(e2e): fail loudly when the store is unavailable in the Auto permissions spec
2026-10-19 18:15:07 -07:00
Kelvin Amoaba 883583b4c8 fix(native-chat): hide history before /clear (#30902) 2026-10-20 01:09:38 +00:00
Kelvin Amoaba 7cc9d95134 fix(native-chat): list Pi skills in the / menu as /skill:<name> (#30924) 2026-10-20 01:08:22 +00:00
Brennan Benson a817602e7c chore(ratchet): prune the tabs-group-actions layout-writer baseline (#30869)
#29150 removed one layout write from tabs-group-actions.ts without pruning
its baseline entry, so every PR's static analysis fails with 'baseline still
allows 13'.
2026-10-19 18:07:35 -07:00
Jinwoo HongandE2E Test 449cbb08a0 test(browser): wait for the popup fixture's first paint during startup, not in the first test (#30854)
* test(browser): temporary timing probe for the popup first-frame flake

* test(browser): extend the temporary popup flake probe to cold caches and the main process

* test(browser): wait for the popup fixture's first paint during startup, not in the first test

* test(browser): name the page whose first frame never arrived

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-19 21:07:15 -04:00
Neil d4b65b9302 fix(browser): a press an SSH host left held across commands is let go of when the host is revoked (#30847) 2026-10-19 18:03:14 -07:00
Neil 8316531ab3 fix(diff-comments): a review note on a twin worktree saves to the open copy and names its host (#30855)
* fix(diff-comments): a review note on a twin worktree saves to the open copy and names its host

* fix(diff-comments): rendered notes and queued saves follow the open twin

* fix(diff-comments): one host-qualified copy per change; async delivery finds its copy by note id

* fix(diff-comments): a copy is its row's own host; unstamped rows keep the owner their repo names

* fix(diff-comments): the open host picks a twin by the owner index and never falls back to another host's copy

* fix(diff-comments): a worktree's saves stay in one queue when its row gains a host stamp

* fix(diff-comments): one queue and floor per worktree id; paired servers stay separate copies

* refactor(diff-comments): keep main's queue and floors; scope rows only when two hosts publish the id

* fix(diff-comments): per-twin rollback floors that follow the mutation chain; write module split out

* fix(diff-comments): a queued save finds its copy by the list its chain produced

* fix(diff-comments): name the row's own host on every save, and treat a refused save as failed

* fix(diff-comments): a queued save keeps its rows' identity even before a twin appears

* fix(diff-comments): a queued save follows its row when a listing stamps it with the write's host

* fix(diff-comments): refuse a write when the open host's copy isn't listed and another host's is

* fix(diff-comments): a lone unstamped row's queued save survives a stamping refresh
2026-10-19 17:58:45 -07:00
Neil c9e7147f84 fix(managed-server): keep the tunnel manager under its line limit after #30888 and #30891 crossed (#30933)
The build's same-pairing check moves next to managedTunnelAccess, which it already wraps.
2026-10-19 17:56:49 -07:00
Brennan Benson 5c182e1b5a style: format the 22 files main merged unformatted (fix red main) (#30918)
* fix(ssh): move the managed tunnel's is-current check beside its record (fix red main)

orcad-managed-tunnel-manager.ts reached 301 counted lines when #30891 landed on
#30888, failing every PR's static analysis. The six-field check that the recorded
tunnel already matches the connection and deployment moves, unchanged, into
orcad-managed-tunnel-active.ts, which owns that record.

* style: format the 22 files main merged unformatted (fix red main)

CI's format check fails every PR on these files. Formatting pushes
NativeChatStructuredSession.tsx one line past its 400-line limit, so its one-use
question list is inlined where questionBody is already non-null (identical
behavior: agentSessionPromptQuestions is pure and ran on every render).
2026-10-19 17:56:14 -07:00
Kelvin Amoaba 5e4139a43c feat(browser): ask for a username and password when a site needs one (#30886) 2026-10-20 00:54:27 +00:00
Kelvin Amoaba 9c8bdb6148 fix(native-chat): keep a typed [Image #N] when the message has no such image (#30740) 2026-10-20 00:53:41 +00:00
Neil d42e6e0813 fix(renderer): host writes route only through the strict owner, and twin projections stay on their host (review-5 #2 #9 #11) (#30890)
* fix(renderer): host writes route only through the strict owner, and twin projections stay on their host (review-5 #2 #9 #11)

A selected ssh: host that two paired servers proxy now resolves ambiguous at the one
selected-host resolver, so no caller can fall back to this app's own SSH connection.
File Explorer and SSH mutation expectations derive the full {endpoint, place} pair from
resolveOwner, so a server folder's nested SSH target stays the write's host. A local
lineage write projects onto its own host's rows only. The id-only store-write ratchet
now also covers persistence repo-row lookups by id alone (review-5 #10's shape).

* fix(ratchet): the id-only lookup scan also sees optional chaining and destructured ids

* fix(renderer): an unstamped folder keeps reading its host from its repos; editor focus fallback left for a follow-up

* docs(renderer): the File Explorer owner comment names its folder fallback
2026-10-19 17:46:01 -07:00
Brennan Benson 08118ec6b8 fix(linear): show a saved Linear credential that cannot be decrypted (#28867)
* fix(linear): show a saved Linear credential that cannot be decrypted

The new-workspace Linear search treated a decrypt failure as an empty result, so a connected workspace still said to start typing. Record the existing decrypt message and offer Settings.

* fix(linear): open Linear settings without an Active Server read

The settings helper read activeRuntimeEnvironmentId, which the focus-read check counts as the Active Server setting. Resolve the source row's host id instead, which is the same host the helper already used.

* fix(linear): carry a Linear read's credential error to the composer

The composer showed "Start typing to search Linear issues." when the saved Linear credential could not be decrypted. The previous attempt wrote the read failure into the host-wide Linear status, which describes the Active Server and every workspace on it, while composer reads run on the repo's host and selected workspace. That caused search loops, blocked healthy hosts and workspaces, and a latch that stopped retrying.

Now Linear search and list reads reject with the plain decrypt message instead of resolving empty, the composer's URL lookup asks issue reads to do the same, and the composer keeps that error per request: it clears when the next read starts and shows in the Linear and Smart tabs (and the attach-a-link search) with a Settings link that opens the Linear card on the read's host. Nothing writes read failures into the shared status any more. The Tasks page keeps showing no rows plus the error, as before.

* fix(linear): keep attach actions and a steady error on Linear read failures

- Type the composer test's repo fixture so CI's typecheck passes.
- Attach a link: a Linear read error no longer hides the status row. The Jira-URL mode switch and the Linear-URL Retry stay; only the duplicate sentence is dropped when the empty list already shows it.
- Composer: the status refresh after a first credential failure re-runs the same read. Keep the error on screen through that re-read instead of flashing a loading skeleton.

* fix(linear): keep the attach status row in place; share the integrations Settings path

- Attach a link: keep the status row mounted (empty) when the result list already shows the Linear read error, so the Reference type button doesn't jump across the row.
- Composer: Jira and Linear Settings buttons now share one path that picks the source's host with getTaskSourceRuntimeSettings, so the Linear one no longer re-implements it. No behavior change.

* fix(linear): make the Linear Settings action a list row; keep Jira URLs on the Linear tab quiet

- Composer: the credential error and its Settings action are one list row (red sentence plus "Settings"). It fits the capped results popover and is reached with the arrow keys like every other row; Enter on it opens Settings. It is never highlighted by default, so a plain Enter still moves to the agent picker.
- Attach a link: on the Linear tab, a pasted Jira URL shows only the existing "Select Smart or Jira" status, as before; the Linear search of the URL text no longer adds a red Linear error beside it.

* fix(linear): show the Linear read error as one plain line with no Settings action

The Settings action inside the results list needed its own highlight and Enter rules and still could not fit the capped popover. The list now shows one compact red line, in the Linear and Smart tabs and in attach-a-link: "Orca can't read your saved Linear login. Reconnect Linear in Settings to search issues." It names the fix in the same words as the neighbouring not-connected hint. The highlight resolver, the input's Enter handling, the composer's Settings handlers and the prop plumbing are back to main.

* fix(linear): show an unreadable Linear login as a Reconnect Linear empty state on the Linear tab only

The composer's Smart tab no longer mentions Linear's login; the Linear tab, Edit workspace links and the Tasks page show a calm title and sentence where results would be, matching the not-connected state.

* fix(linear): drive the Tasks Reconnect card from the selected scope's read; plain words in workspace notices

Host-wide status flags any broken workspace, so a healthy selected workspace with no results no longer shows Reconnect Linear. Projects and Views notices say the unreadable login in plain words.

* fix(linear): keep the Tasks Reconnect card up while the same list re-reads

The list effect cleared its error on every re-run, so a refresh flashed the raw status bar until the read failed again. Clear it on success instead.

* refactor(linear): drop the unreachable typed-row padding from the Linear tab's Reconnect state

The Linear tab never has a typed-text row, so the state always uses the roomy padding.

* fix(linear): derive the Tasks Reconnect card from the plain list's read error in the board

Avoids widening the selection model's existing type assertion; project and view tabs report errors inside their results.

* fix(linear): say the unreadable Linear login in one short line
2026-10-19 17:45:00 -07:00
NeilandClaude 6f96efb662 fix(orcad-migration): a host fenced on a manifest no request can carry can be cancelled by its id (#30864)
* fix(orcad-migration): a source fenced on a manifest no request can carry can be aborted by its identity

A desktop that fenced a manifest just under the export cap before #30836
could never send its abort: every catalog request carries the whole
manifest and refuses it before sending. The abort now asks the server by
migration id and digest instead (orcad.migration.abortCatalogByReference,
behind a new capability), and releases only on the server's durable
absent answer. A committed server still refuses the release.

Co-authored-by: Claude <noreply@anthropic.com>

* test(orcad-migration): cover the durable abort by reference and its RPC method

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 17:25:47 -07:00
Neil c3d3df497f fix(sidebar): a Pinned drop or menu pin pins the card you chose, not its twin on another host (#30892)
setWorktreesPinnedAndReveal took bare worktree ids and looked each up with
getKnownWorktreeById(id) and no host, so pinning one card of a twin (the same
worktree published by a local and an SSH host) could pin the other copy.

The action now takes host-qualified targets and skips a host-less id that two
hosts publish. The sidebar Pinned drop resolves its ids through the drop
resolver (dropWriteRows), so it pins the dragged copies; the drop-commit
context hands the active drag's copies to the pin commit. The context-menu pin
passes the clicked row.
2026-10-19 16:54:15 -07:00
NeilandClaude acdf0663f0 fix(runtime): a terminal start on an SSH twin no longer wakes or is refused by this computer's sleep (#30860)
* fix(runtime): a terminal spawn on an SSH twin no longer wakes or is refused by this computer's sleep

The host's sleep state was keyed by the bare worktree id, which a repo
registered on two hosts publishes twice. A spawn on the SSH copy found the
local copy's sleep, so an automatic probe was refused and any other spawn
woke the local sleep. Key both sleep maps by host plus workspace, with the
sleep side deriving its host the way a spawn does.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(runtime): a runtime-stamped project's sleep keys on its nested SSH target, as its spawns do

Co-authored-by: Claude <noreply@anthropic.com>

* fix(runtime): derive the sleep key from the launch-host resolver the launch scope uses

Predicting the host from one repo row disagreed with the launch path for a
runtime-stamped project with a nested SSH target. Reuse resolveWorktreeLaunchHost
so a sleep and the spawns of the same workspace always name one key.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(runtime): a sleep stops and files only its own host's terminals, and refuses an unresolvable host

Sleeping this computer's copy also stopped the SSH twin's live PTYs and filed
them under this host's key, where the twin's spawn could never wake them.
Resolve one sleep scope (key and PTY fence) per sleep from the launch-host
resolver, fence the live sets with it, and refuse the sleep when rival rows
disagree about the host instead of guessing local. The maps are typed and
named by host-scoped key.

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 16:50:57 -07:00
Brennan Benson 787e7c0bf5 fix(native-chat): keep transcript text clear of the message rail (#30868)
* fix(native-chat): keep transcript text clear of the message rail

* fix(native-chat): measure the rail's width before first paint so a narrow chat opens without a text shift

* fix: synchronize native chat rail resize updates

* test: give rail navigation viewport a measured width
2026-10-19 16:38:50 -07:00
Brennan Benson 2bdef8d375 fix(settings): Antigravity's sign-in reads as an Account line, never an empty box (#30850)
Settings > Chat > Providers showed Antigravity's sign-in as a filled grey box holding only a
bright primary "Sign in" button. The host learns whether its chats are signed in only when a
chat starts, so after launch the status line had nothing to say and the box sat empty.

The area is now a plain row under the provider's name: "Account" on one line, the status on the
next, and an outline "Sign in" button. Until the host knows, the status line says chats need an
Antigravity sign-in separate from the agy terminal account.
2026-10-19 16:35:46 -07:00
Neil da0db9fc73 fix(ssh): orca open-url on a managed SSH host opens the page on the desktop through the browser bridge (#30849) 2026-10-19 16:33:20 -07:00
NeilandClaude fbd0a29eed fix(ssh): two overlapping bridged-terminal attaches hand back the viewer they replace (#30856)
* fix(ssh): two overlapping bridged attaches hand back the viewer they replace

Co-authored-by: Claude <noreply@anthropic.com>

* fix(ssh): the newest bridged attach keeps its viewer even when an older one answers last

Co-authored-by: Claude <noreply@anthropic.com>

* fix(ssh): an older bridged connect still paints its screen when a newer attach keeps the viewer

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 16:31:28 -07:00
Jinwoo HongandE2E Test 3f9e4ecc1b chore(format): format the files main's new formatting check rejects (#30929)
#30863 made the static job fail on unformatted files; 22 files that landed
around it were never run through oxfmt. Formatter output only, plus one
shared paneActive local in NativeChatStructuredSession.tsx so formatting
does not push that file past max-lines.

Co-authored-by: E2E Test <e2e@test.local>
2026-10-19 19:29:10 -04:00
ad5ef0d944 [mem] fix(memory): release consumed buffers, abandoned waits, and deleted diff views (#30650)
* fix(memory): release consumed stream buffers and abandoned request state

* fix(memory): preserve queued cancellation for shared promise callers

* fix(memory): keep workspace cache cleanup free of browser side effects

* fix(memory): follow renamed workspace owners in cached diff views

* fix(memory): a viewer unmounting after its workspace is removed cannot re-create cached diff entries

Combined-diff view state, scroll offset and scroll anchor now live in one owner-tagged
entry per view key. A mounted viewer claims its key; writes only update claimed entries,
so the late unmount write that follows a workspace removal is a no-op instead of
re-adding orphan scroll entries.

* fix(memory): tighten retention fixes and let evicted diff views heal

- Combined diff: writes go through a per-mount lease. Forgetting a workspace revokes its
  leases, so a late unmount write cannot resurrect entries, while an LRU-evicted entry of
  a still-mounted viewer is re-created on its next write. Drops the duplicated view scrollTop.
- NDJSON framer: copy a retained tail only when it is a strict slice of its chunk, copy an
  unfinished line once, and copy every rejected-line prefix.
- Binary frame reader: single exit path for the end-of-feed leftover copy.
- Relay readiness reuses waitForPromiseWithSignal instead of re-implementing it.
- PTY preconnect input: pass the listener-removing settle into createInput instead of
  mutating the input afterwards.

* docs(memory): state the deferSettled ordering guarantee precisely

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
2026-10-19 16:23:32 -07:00
Jinwoo HongandE2E Test e7d6ee645b chore(lint): bring orcad-managed-tunnel-manager.ts back under max-lines (#30921)
Move the 'recorded tunnel still matches this build' check into
orcad-managed-tunnel-active.ts as activeTunnelMatches(). No behaviour change.

Co-authored-by: E2E Test <e2e@test.local>
2026-10-19 19:17:28 -04:00
Neil cbb9f369b8 Stop running the experimental layout suite on every matching PR (#30907) 2026-10-19 16:16:50 -07:00
Wooseong KimandNeil f3a1238a34 perf(mobile): shorten output batching during terminal scroll gestures (#30319)
Co-authored-by: Neil <neil@stably.ai>
2026-10-19 16:15:24 -07:00
Jinjing f14475caa6 fix(browser): label popups from committed document origins (#30540)
* fix(browser): label popup chrome from committed documents

* fix(browser): keep opaque popup documents neutral

* test(browser): prepare real popup identity validation

* test(browser): apply background policy in popup probes

* test(browser): fix popup title and view-lifetime checks

* fix(browser): ignore subframe failures in popup chrome

* rm draft docs

* fix(browser): finish popup probe teardown after signal exit

* test(browser): use typed popup probe window access

* fix(browser): prevent automatic popup title updates

* commit

* fix test
2026-10-19 16:13:54 -07:00
Neil 30d1e8dba5 perf(gitlab): skip unused review details in Checks refreshes (#28643) 2026-10-19 16:12:08 -07:00
Neil c5d03fc475 fix(ssh): a system SSH forward owns its listener even when ssh_config sets a ControlMaster (#30887)
* fix(ssh): system SSH forward processes own their listener even under a user ControlMaster

Forward processes ran through the user's ssh_config ControlMaster, so the master held the
listener (it survived the child's kill) and logged sshd's refusals where the managed tunnel's
fallback check never looked. Every forward process now builds its args through one
unmultiplexed builder (-S none); orphan matching tolerates the older argv.

* docs(ssh): say why a forward never reused a live master's login

* docs(ssh): state the forward login trade-off plainly

* test(ssh): keep the real-OpenSSH test's test-double cast on its SAFETY line
2026-10-19 16:11:05 -07:00
Neil 92eb9790bc ci: fail the static job on unformatted files (#30863)
* ci: fail the static job on unformatted files

Runs oxfmt --check over the same tree pnpm format writes (~2s), and makes the
pre-commit hook format markdown and YAML so it covers what CI now checks.

* ci: route docs-only and workflow-only diffs through the formatting check

The format check reads the whole tree, so a diff that skipped the static job
could land drift that fails every later PR. Any changed file oxfmt formats now
selects static analysis (not typecheck or units), like the mobile-only route.
The path detector checks out .oxfmtrc.json for its ignores. The hook no longer
fails a commit whose only matches oxfmt ignores (lockfiles).
2026-10-19 16:08:52 -07:00
Neil 7e75a86caa refactor(orcad-migration): drop the host-side editor-drafts transfer; keep the durable-commit gate (#30884) 2026-10-19 16:05:34 -07:00
Neil 0a3d203f61 fix(ssh): Disconnect closes the managed server tunnels the SSH session carried (#30891)
* fix(ssh): Disconnect closes the managed server tunnels the SSH session carried

The managed tunnels live in their own manager, so the session teardown closed ordinary
forwards and the connection but left a system-SSH tunnel's ssh -L process authenticated and
listening. The teardown now closes every managed tunnel built or building on the target.

* fix(ssh): a session teardown also closes forwards still starting or awaiting their identity check

* fix(ssh): a connection teardown waits for every forward of it to settle, starts and closes under way included

* fix(ssh): bound a teardown's wait on starts, skip edit rollback after one, and never record a tunnel a teardown overtook

* fix(ssh): an edit a Disconnect cancelled leaves the target's saved forwards alone
2026-10-19 16:00:38 -07:00
Jinwoo HongandE2E Test 099350d85c fix(terminal): Restart on a failed terminal opens a plain shell instead of Codex (#30872)
* fix(terminal): Restart on a failed terminal opens a plain shell instead of Codex

The exit card's Restart passed a null startup, and a Codex account-restart
fallback added in #25485 turned that null into `codex`. Account switches
already name their agent explicitly, so the fallback had no legitimate caller.

* test(terminal): stub the exit-actions controller without Reflect.get

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-19 18:56:26 -04:00
Brennan Benson c1969010cb feat(native-chat): $ opens Codex skills in the composer (#30827)
* feat(native-chat): `$` opens Codex skills in the composer

Typing `$` in a Codex chat now opens a skills-only list, as Codex's own
apps do. `/` still lists commands and skills for every agent, and other
agents keep `$` as plain text.

* fix(native-chat): `$` skips shell variables and parameters, as Codex does

In Codex chats `$HOME`, `$5`, `$(pwd)`, `${X}` and `$?` stay prose instead of opening the skill list, so Enter sends them; one classifier now serves the trigger, the menu and the insert.

* fix(native-chat): `$` matches skill names only, as Codex does

A `$` query no longer matches skill descriptions, so `echo $CODEX_HOME` + Enter sends instead of picking skill-installer; `/` still searches descriptions.
2026-10-19 15:55:32 -07:00
Neil c8a4fc6047 refactor(editor): keep tab buffer state in a per-tab store record mirrored onto OpenFile (#30885) 2026-10-19 15:54:00 -07:00
Neil ff70e9a5e4 perf(file-explorer): reject nonmatching names before path checks (#28572) 2026-10-19 15:53:01 -07:00
Neil 98ee268825 fix(ssh): a managed tunnel follows the host's bound port after another desktop restarts the server (#30888)
The tunnel was rebuilt only when this desktop's own check saw the server start. When another
desktop restarted it onto a new port, the old forward stayed and every check answered
unverifiable. A check that finds the server not answering now reads the port the host's
readiness record names and, when it moved, rebuilds the tunnel there.
2026-10-19 15:39:54 -07:00
Neil daad41f892 perf: preserve matching Terminal settings sections during search (#23164)
* perf: preserve matching Terminal settings sections during search

* fix(settings): keep a retained Terminal sessions section fresh and share the section stack

Move the search-stable section wrapper into SettingsSectionStack so every settings
pane keys sections by their own element key instead of the array index, and subscribe
ManageSessionsSection to the daemon session inventory invalidation signal so a kill
routed through another surface still reaches a section that no longer remounts.

Restates the read-count oracles as node identity plus non-growing read counts so a
future freshness fix does not read as a regression.

* test(settings): mock the Windows administrator section in the Terminal section-lifetime suite

* style: format Terminal section-lifetime reliability gate
2026-10-19 15:39:20 -07:00
Neil 8e1696f5d7 perf(push): deliver queued notifications without waiting for the poll (#28624)
* perf(push): wake delivery worker after notifications are queued

* perf(push): wake one drain per send so bursts cost O(sends + drains) claims

A wake started all twelve drains, so each send arriving at an idle worker cost
~13 claim transactions. A wake now starts one drain; the poll tops up to the
full drain set.
2026-10-19 15:38:30 -07:00
Neil 93d89f57c8 perf(automations): release remote exit waits when a run stops being observed (#13641)
A remote-runtime automation whose terminal stays open after completion kept
its 24h terminal.wait and data stream alive, holding one of the server's 16
long-wait slots each. Abort the wait and close the stream on dispose.
2026-10-19 15:37:45 -07:00
Neil 3d880e5182 fix(session-tabs): a held paired-tab close survives an undecided re-pair, and a dropped one shows its tab at once (#30843)
* fix(session-tabs): a held paired-tab close keeps hiding through an undecided re-pair, and its expiry redraws the tab

* fix(session-tabs): dropping a held close also clears its hide under the current pairing

* fix(session-tabs): redraw only the worktree a dropped close un-hid; inventory frames see the redraw mark

* fix(session-tabs): a re-pair proven the same machine asks for the snapshot its moved closes wait on

* fix(session-tabs): a dropped close clears only the current-pairing hide it laid

* fix(session-tabs): every un-hide of a held close lets its worktree's current snapshot redraw
2026-10-19 15:36:46 -07:00
Neil 90797aebca fix(sidebar-inputs): a failed domain read is retried by the next flush or phone read (#30866)
* fix(sidebar-inputs): a failed domain read is retried by the next flush or phone read

A failed producer read used to clear the domain from the dirty set, so it stayed
stale until its own source announced another change. It now stays dirty without
a new mark: the next flush or phone read retries it, and nothing schedules one.

* docs(sidebar-inputs): flushDirty reads what was dirty; a failed read stays unpublished
2026-10-19 15:35:41 -07:00
Neil 209e468a7c fix(ssh): an SSH server update counts terminals again just before it stops the server (#30874)
* fix(ssh): an SSH server update counts terminals again just before it stops the server

The SSH update passed the terminal census taken before the artifact upload and no recount,
so a terminal opened during the upload was missed and, on a host running terminals inside
orcad, ended by the restart. The recount is now a required deploy option, so every runtime
(SSH, WSL, first deploy) supplies one and the stop step always recounts.

* fix(ssh): a recount that throws reads as unverifiable and defers the update
2026-10-19 15:34:16 -07:00
NeilandClaude 0f5c3a4a91 fix(renderer): restoring or re-selecting a workspace keeps the host it was selected on (#30839)
* fix(renderer): re-activating a workspace keeps the host it was selected on

A failed sleep, an automation's focus restore and a rename event re-activated
the workspace by id alone, which cleared activeWorkspaceExecutionHostId. For a
same-id twin (local + SSH) that left the active workspace ambiguous, so owner
resolution refused terminals and writes or guessed this computer. Each site now
passes back the host captured before the change.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(sidebar): sleep restore assertions name the restored host argument

* test(automations): type the selected host without an assertion

* fix(renderer): same-workspace re-selection keeps the selected host; restores yield to a newer pick

Link clicks and surface reveals inside the active workspace re-selected it by
id alone, clearing the selected twin's host. They now pass it back through one
getActiveWorkspaceExecutionHostId helper (the owner module's private copy moved
there). The failed-sleep and rename restores skip when the user picked another
workspace during their awaits.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(renderer): one re-select helper keeps a single-argument call when no host is selected

* fix(ipc-events): a rename restore also yields to a twin copy picked during the refresh

* refactor(renderer): capture and restore the workspace selection as one id+host value

Every restore site (failed sleep, automation focus restore, rename) now goes
through captureWorkspaceSelection / isWorkspaceSelected / restoreWorkspaceSelection,
so the host can't be dropped or replayed over a newer pick.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 15:16:24 -07:00
Brennan Benson 3d0973d0d4 A chat's visuals and sent images live as long as the chat (#30413)
* fix(native-chat): a chat's visuals folder lives as long as the chat

A chat's scratch folder was deleted once the chat had gone 30 days
unused, so an old chat's inline visuals turned into "Visualization
unavailable" while the chat and its history stayed. The common pattern
keeps a chat's files for as long as the chat and never deletes them on
an "unused for N days" timer.

The sweep now removes only folders no chat record maps to, plus
leftover .expired folders older builds parked. Everything that existed
only to make the age rule safe goes with it: the per-folder save lock
and its expiry candidates, the session-lane idle check, the journal
last-activity query, and the migration's fresh-window timestamp. The
visuals skill no longer tells agents the folder is temporary.

* fix(native-chat): an image sent in a local chat lives as long as the chat

A local chat's pasted images lived in one paste folder that deleted
every file older than 30 days, sent or not, so an old chat's images
turned into label chips. Paired-server chats already keep pastes in the
host's attachment store, where the send that names a file claims it for
its chat and only unclaimed files are ever swept: the common pattern.

A local composer paste now goes into this machine's own attachment
store (starting the local chat host first), so its send claims it the
same way. The old paste folder is no longer written or swept: messages
sent before this change may name its files. Restored drafts still read
it.

An unclaimed upload now waits 30 days instead of one before the sweep
takes it. A draft names its image by this path, so the window is how
long an unsent draft keeps its image; 30 days is what local drafts had,
and it outlasts the host's send-id window so a resent draft still finds
its image.

A chat folder an older build set aside as unused (<hash>.expired) goes
back to its chat instead of being deleted.

* fix(native-chat): a terminal-backed chat's pastes are never swept

A terminal-backed chat has no journal, so no send claims its pastes and
nothing can tell a sent one from an abandoned one. Its pastes therefore
stay in the paste folder, which is no longer swept, rather than in the
attachment store, whose sweep would take them after 30 days unclaimed.
Only a local structured chat's paste (forStructuredChat) goes to the
store, where its send claims it.

* fix(native-chat): keep the attachment owner's literal kind for structured local chats

* test(ipc): expect the local chat store option on the clipboard registrar

* fix(native-chat): preserve attachment ownership and restore held files safely

* fix(native-chat): settle restored image checks independently

* test(native-chat): assert restored image outcomes without batching

* Re-check draft images when the attachment sweep removes their upload

A draft checks its pasted images once, when its composer comes back. Every
chat in an open workspace mounts its composer at launch, so that check ran
before the store sweep (60 s after host start, then every 6 h) deleted an
expired unsent upload, and nothing checked again: the chip lost its picture
with no Not kept feedback and Send stayed enabled.

The sweep now reports each removal attempt; the main process forwards it to
its window, and the draft store starts a fresh finite check for every loaded
local draft image in that folder, mounted or not. A notice is a reason to
check, never proof of absence.

* Let the app effect remove the draft re-check listener it adds

startNativeChatDraftLoad dropped the unsubscribe for the upload-removal
listener, so each recovery through the root error boundary's Retry remounted
App and added another listener. Return it so the effect's cleanup removes it.
2026-10-19 15:11:24 -07:00
Neil 1529b0f73b fix(sidebar): a status or order drag writes the card you dragged, not its twin on another host (#30844)
The drag carried bare worktree ids, and the drop read each id's host from the
id-keyed worktree map, which keeps one row per id. When the same worktree is
published by two hosts (a local and an SSH copy), dragging one card could set
the status or manual order on the other host's copy.

The pointer and native drag sessions now record the dragged cards, and their
visible lineage children, as (id, host) copies; the lineage expansion runs by
host identity so a twin's children are not pulled in. The shared drop-commit
context hands the copies to every status, reorder and board-drop commit.

Every write then resolves through one choke point, dropWriteRows: a dragged id
writes only the dragged copies (read fresh; a copy gone by drop time is
refused), and any other id (a re-ranked sibling, a drop with no drag session)
only when a single host publishes it. The board drop indicator reads the same
rows, so the preview matches the commit.
2026-10-19 14:47:02 -07:00
Neil a201eacd06 fix(projects): link a cloned or imported folder into a GitLab or generic-remote project (#30785)
Setting a project up on another host (a cloned or imported folder)
only knew how to link the folder through a GitHub identity. A project
keyed by its Git remote (GitLab, Bitbucket, a self-hosted host) was
always refused with "Imported folder does not match the selected
project identity". A fresh clone's remote is not probed when it is
registered, so it never projected into that project by itself.

The link now probes an unprobed folder's remote. If the canonical key
of that remote is the project's, the remote is recorded on the repo, so
the folder projects into the project. SSH and HTTPS forms share a key.
A GitHub project is still linked by stamping upstream, and a remote
that names another repository is still refused.
2026-10-19 14:43:33 -07:00
Brennan Benson 4a33cd8e73 fix(native-chat): a usage limit reads as a pause with its reset time and a Continue, not a failure (#30396)
* fix(native-chat): a usage limit reads as a pause with its reset time and a Continue, not a failure

Claude, Codex and Grok chats that hit the account's usage limit now end the
turn on one warning row that names the limit and, in the reader's clock,
when it resets, with Continue to pick the turn back up. Each adapter reads
its protocol's typed limit signal into a `usageLimitReached` failure fact;
older clients keep the host's sentence.

* fix(native-chat): keep the failure words and chat session files within their line budgets

Moves the attachment-problem sentences to their own file and hands the composer's send straight to Continue.

* fix(native-chat): drop a check the shown branch already rules out

* fix(i18n): ship the usage-limit failure words in the runtime catalog

* fix(native-chat): reconcile usage failures by owning turn and typed cause

* fix(codex): keep snapshot import type-only

* fix(claude): open refusal turns without opening empty placeholders

* fix(native-chat): bind usage Continue to the latest idle failed turn

* test(native-chat): isolate stale Continue from ordinary delivery progress

* test(native-chat): use a braced journal assertion

* test(native-chat): build failure rows through their word factory

* fix(claude): retain terminal failure evidence through result reconciliation

* fix(native-chat): complete usage Continue through the composer sender

* test(native-chat): provide the composer options surface in Continue coverage

* test(native-chat): use typed failure words in renderer fixtures

* test(native-chat): match the remote send payload apart from transport options

* fix(native-chat): replace the previous sender notice on Continue

* fix(native-chat): clear superseded Continue notices before rendering

* fix(native-chat): preserve ordinary composer send arguments

* fix(claude): resolve terminal ownership and preserve resultless quota failures

* fix(claude): keep repeated terminal detail unchanged and complete test fixtures

* fix(claude): resolve deferred terminal replays after owning journal writes

* refactor(native-chat): share tombstone append within the sink line budget

* fix(claude): retain both input and echo correlation on the live turn

* fix(claude): preserve unowned result completion after terminal resolution

* fix(claude): allow unassociated successful goal completion

* fix(claude): keep terminal completion on the live turn

* fix(claude): keep typed notices separate from quota failures

* fix(claude): preserve context-limit failures as ordinary errors

* fix(native-chat): keep quota failures visible in collapsed turns

* fix(native-chat): return keyboard Continue focus to the composer

* test(native-chat): keep quota fixture context stable

* test(native-chat): hoist quota context to module scope

* fix(i18n): use one Chinese word for the agent in both Continue tooltips
2026-10-19 14:43:16 -07:00