Commit Graph
8553 Commits
Author SHA1 Message Date
Jinjing 7c93aed6dc Fix fsync of read-only files on POSIX (#14235)
* fix(files): fsync read-only files on POSIX

* test(e2e): add golden E2E tests for POSIX profile index fsync

Validates that profile index files are properly persisted on POSIX systems, including with restrictive umask settings. These are release-blocking golden tests for Linux and macOS.

* test(terminal): wait for fish child ownership before stdin write

Fish 4.8 withdraws DECSET 2031 before spawning the child, so the
shell-contracts harness could send hello into an intermediate prompt
and hang waiting for CHILD-READ. Wait for the child's CHILD-READY
marker and answer split DA1/CPR/OSC queries across chunk boundaries.

* test(e2e): verify profile index persists to disk with restrictive umask

Strengthen the POSIX fsync test to verify the rebuilt index is actually
written to disk and has correct permissions under a restrictive umask,
not just cached in memory.
2026-08-13 00:58:55 -07:00
JinjingandOrca 2c28b6c92c Gate WSL transcript filesystem I/O to prevent stalls (STA-4049) (#14203)
* fix(ai-vault): gate post-resolution WSL transcript I/O (STA-4049)

PR #14090 admitted only path *resolution* through the WSL transcript
filesystem gate. Every byte read afterwards from the resulting
\\wsl.localhost\... UNC path ran raw, so a distro that answers the first
access() and then stalls hung Native Chat at "loading" and AI Vault at
"scanning" with no timeout and no error.

Route that I/O through a new wsl-transcript-fs-access accessor, which is
a verbatim node:fs passthrough off UNC and an admitted, deadlined task on
it. open/positional-read opt out of coalescing (dedupe: false): joiners
would share one FileHandle or one caller's buffer.

Refusals now surface as the existing retryable message rather than
notFound, per-root scan failures are contained to an AiVaultScanIssue,
and the memoized Codex/Kimi indexes evict on refusal so a stall cannot
pin "no titles"/"no cwd" until the index changes.

* fix(ai-vault): stop caching WSL gate refusals as results (STA-4049)

Code review 1 P1 fixes on top of the transcript gate:

- transcript-read-cache: never store a gate refusal. The refusal leaves the
  file's mtime untouched, so the cached error would have been served to every
  later call until the transcript itself changed.
- kimi/grok/opencode parsers: rethrow WslTranscriptFsError instead of folding it
  into "no session"/"no transcript", so the session parse cache cannot store a
  null or partial answer under an unchanged mtime. Ordinary missing/half-written
  files stay contained.
- opencode-usage scanner: gate the data-directory readdir and the absolute
  OPENCODE_DB stat. The AI Vault's primary OpenCode source reaches them
  transitively, which is why the direct-import guard never saw them.
- gated stat/lstat: accept an AbortSignal, matching gated open/read, so a
  cancelled watch install or title probe detaches immediately instead of holding
  a waiter to its deadline.
- gated open: close a FileHandle whose syscall lands after the last waiter gave
  up, and close handles off UNC verbatim (awaited, failures surfaced).

Co-authored-by: Orca <help@stably.ai>

* fix(native-chat): decode gated chunks incrementally and cancel drain I/O (STA-4049)

Addresses the CR2 blockers.

UTF-8 chunk-boundary corruption: the UNC branch yielded raw 1 MiB Buffer
slices that `decodeTranscriptStream` decoded independently, so any multibyte
codepoint straddling a boundary became U+FFFD on both sides — corrupting the
JSONL line and shifting `consumedBytes` (which seeds fallback message ids).
`gatedChunks` now holds a StringDecoder when `encoding` is set, and
`decodeTranscriptStream` holds one for the Buffer path, matching what
`createReadStream`'s decoder already did off UNC.

Watcher teardown: `installTranscriptWatcher` owns an AbortController that
`unsubscribe()` aborts, threaded through every gated call on the drain path.
Waiters now detach at teardown instead of holding to the 30s deadline, and
the gate's aborted-signal pre-check stops an in-flight drain from admitting
new tasks after close.

Rovo `session_context.json`: `readJsonObjectIfExists` rethrows
WslTranscriptFsError so `parseSessionCandidate` records a scan issue, instead
of caching an un-enriched session under an unchanged mtime that never re-reads.

Primary OpenCode source: `listOpenCodeDatabases` takes an optional refusal
reporter so a refused `OPENCODE_DB`/`XDG_DATA_HOME` surfaces an
AiVaultScanIssue, matching `listOpenCodeDatabasesInDirectory`.

`boundaryFingerprint` moved to its own module to keep the watcher engine
under the max-lines cap.

* refactor(native-chat): consolidate transcript I/O and remove fallback te

- Move boundaryFingerprint from its own module to transcript-file-version.ts
- Extract runPathOperation helper to eliminate duplicate UNC path routing
- Remove tests for fallback behaviors when transcripts are unavailable or incomplete
- Clean up implementation comments and verbose test documentation

* consolidate scan issues and gate session scanner I/O (STA-4049)

Both local and remote session scans hit stalled WSL distros identically:
one failed probe per discovered path. Unifying issue recording and gate
refusal handling prevents duplication and ensures consistent behavior.

- Gate all file operations (stat, readdir, read, open) through WSL
  stall detection instead of scattered or missing gates
- Serve cached transcripts when stat stalls; distinguish gate refusals
  from missing files
- Serialize UNC close operations to prevent thread pool exhaustion
- Incremental chunk decoding in streams handles codepoint boundaries
  correctly

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-13 00:54:19 -07:00
NeilandOrca a73f122c61 fix(store): keep project catalog identity when repo.addedAt is 0
* fix(store): keep project catalog identity when repo.addedAt is 0

projectHostSetupProjectionFromRepos used `repo.addedAt || now`, so a
missing or zero addedAt stamped Date.now() into createdAt/updatedAt on
every refresh. #13803's reconcile then treated the project as changed
and never reused it.

Use a finite check with a stable 0 fallback, and treat 0 as unknown in
merge so a persisted createdAt is not wiped. Refresh-identity tests use
production-shaped nested fixtures plus structuredClone and go red if
the fallback is reverted.

Co-authored-by: Orca <help@stably.ai>

* type(shared): return readonly setups from getProjectHostSetupsForProject

The helper already takes a readonly catalog and returns a filter subset.
Mark the return readonly so callers cannot mutate a live setups array.

Co-authored-by: Orca <help@stably.ai>

* style: oxfmt projection files and drop stale addedAt comment

oxfmt --check failed on the addedAt identity commit. Also stop claiming
the projection still restamps Date.now() when addedAt is 0.

Co-authored-by: Orca <help@stably.ai>

* fix(store): treat createdAt 0 as unknown when merging sibling repos

Repo order decided a project's createdAt: a zero-addedAt repo seeded the
accumulator with 0, and the merge only treated the *incoming* addedAt as
unknown, so min(0, 100) kept 0 when the unknown sibling came first.

Share unknown-aware mergeCatalogCreatedAt/mergeCatalogUpdatedAt helpers and
apply them on both sides of the projection merge and of the renderer's
cross-host mergeProjectCompatibilityProject, which had the same 0-poisoning
via Math.min(base.createdAt, overlay.createdAt).

Co-authored-by: Orca <help@stably.ai>

* test(store): pass a valid updateProject payload in createdAt merge case

updateProject only accepts localWindowsRuntimePreference. The new
unknown-vs-known createdAt test used displayName and failed typecheck.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-13 00:37:23 -07:00
JinjingandOrca d41cb21e94 Sta 4062 folder note rollback (#14232)
* fix(persistence): keep folder-workspace notes across a build rollback

normalizeFolderWorkspaces rebuilds each FolderWorkspace field-by-field, so the
inline diffComments field #14112 added is dropped by any build that predates it
— and the next full-state write makes the loss durable with no user edit.

Move the on-disk home to an optional top-level PersistedState.folderWorkspaceDiffComments,
which older builds round-trip untouched through their {...defaults, ...parsed} load
spread and omit-style getDurableState(). load() hydrates it onto the records and
deletes it from Store state; buildStateToSave() is the only producer. The in-memory
FolderWorkspace shape, and therefore every IPC/RPC/renderer/mobile path, is unchanged.

Co-authored-by: Orca <help@stably.ai>

* fix(persistence): prefer inline folder notes over a stale map entry

Hydrate preferred a non-empty folderWorkspaceDiffComments entry over non-empty
inline notes. A rollback to a notes-capable #14112 build writes notes inline and
leaves the older map untouched, so re-upgrading deleted everything authored while
rolled back. Inline now wins when present; the map only fills a stripped record.

Co-authored-by: Orca <help@stably.ai>

* Extract folder workspace diff comments to dedicated module

Moves normalizeFolderWorkspaceDiffComments and
collectFolderWorkspaceDiffComments from persistence.ts to a new
folder-workspace-diff-comments.ts module for better code organization.

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-13 00:34:57 -07:00
NeilandOrca ac68eead64 perf(renderer): keep worktree lineage map identity on no-op refresh
* perf(renderer): keep worktree lineage map identity on no-op refresh

A host-scoped lineage refresh always spread IPC clones into new maps, so
WorktreeList Object.is subscriptions missed every time even when capture,
taskId, and coordinatorHandle were unchanged. Reuse structurally equal
records, return the previous maps when nothing moved, and skip set() when
both maps are still current so a no-op refresh is actually a no-op.

Co-authored-by: Orca <help@stably.ai>

* type(store): mark worktree lineage maps readonly

Widen reuseEqualRecordMap and the renderer lineage store fields to
Readonly<Record<...>> so a reused no-op refresh map cannot be mutated
in place.

Co-authored-by: Orca <help@stably.ai>

* type(store): build lineage overlays on mutable records

mergeExactHostLineage was writing into AppState map types after they
became Readonly. Use local mutable Records, then reuseEqualRecordMap.

Co-authored-by: Orca <help@stably.ai>

* fix(renderer): keep direct-ssh host hydration under the max-lines cap

The lineage overlay rewrite pushed the file to 305 counted lines against the 300 cap, failing oxlint. Fold the duplicated per-map filter/overlay into one scoped helper so both maps share it.

Co-authored-by: Orca <help@stably.ai>

* perf(renderer): drop dead reuseEqualRecordMap key loop

A key missing from next always shows up as either a key-count mismatch or a failed previous lookup in the first pass, so the second pass over the previous keys could never flip identical. Cover the identity contract with focused unit tests, including the same-count key swap that the removed loop appeared to guard.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-13 00:18:47 -07:00
Neil 6afba56501 Update pull_request_template.md 2026-08-13 00:16:25 -07:00
Jinjing 939acf1c2b Fix remote server browser tab fallback behavior (#14194)
* fix(browser): require runtime browser capability

* fix(browser): reuse capability verdict and exclude floating terminals

Cache the runtime browser capability check across multiple remote port opens instead of checking for each port independently. Prevent floating-terminal workspaces from routing to remote browsers.

* fix(browser): pin remote pane link opens to owning runtime

Remote pane link opens must route to the pane's runtime, never the client.
Fails if the workspace has moved to a different host while the pane still
displays a remote page, preventing silent misrouting to a dev server.

* trim comments
2026-08-13 00:12:20 -07:00
NeilandOrca b9ae720597 perf(github): reuse workItemsCache identity on no-op refetch
* perf(github): reuse workItemsCache identity on no-op refetch

A force refresh always remapped IPC rows and allocated a new cache entry,
so TaskPage's useShallow selector treated every poll as a real change and
remapped visible work-item rows. Reconcile nested rows structurally, bump
fetchedAt on the existing entry when nothing changed, and only write a new
entry when a row or meta field actually differs.

Co-authored-by: Orca <help@stably.ai>

* type(github): mark workItemsCache data readonly

Store work-item rows as readonly GitHubWorkItem[] and assign the
reconciled catalog directly so a no-op or structurally-equal refetch
cannot mutate the live cache array.

Co-authored-by: Orca <help@stably.ai>

* type(github): propagate readonly workItemsCache through TaskPage

Widen fetchWorkItems, inflight promises, and TaskPage cache selectors
so CacheEntry<readonly GitHubWorkItem[]> typechecks against consumers.

Co-authored-by: Orca <help@stably.ai>

* perf(github): drop tautological workItemsCache data ternary

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-13 00:08:10 -07:00
NeilandOrca 3e0dddaa3e perf(renderer): keep project catalog identity on SSH readoption
* perf(renderer): keep project catalog identity on SSH readoption

SshPane calls recordSshRepoReadoptions on every Manage-pane importConfig(),
often with []. The publisher always rebuilt projects and projectHostSetups
via projectCompatibilityFromRepos then mergeProjectHostSetupCompatibility
and spread the new objects into set(), so WeakMap and Object.is selectors
missed even when nothing was re-adopted.

Fetch paths already reconcile through mergeFetchedProjectCompatibilityForHost,
but that helper is host-scoped and this write is all-repos. Reconcile the
merged rows against the previous catalog with the same identity keys, omit
unchanged keys from set(), and skip the work entirely when both the incoming
and pending readoption lists are empty.

Tests reuse the production-shaped nested repo fixture and the structuredClone
fetch path. A pending-only readoption stays identity-stable, and removing
the two reconcile calls turns that case red.

Co-authored-by: Orca <help@stably.ai>

* type(store): mark pending SSH readoptions readonly

Widen pendingSshRepoReadoptions, recordSshRepoReadoptions, and the
merge/reconcile helpers to readonly so a reused pending queue cannot be
mutated in place after a no-op catalog reconcile.

Co-authored-by: Orca <help@stably.ai>

* perf(renderer): drop redundant catalogRowsUnchanged after reconcile

reconcileCatalogRows already hands back the previous array when nothing
moved, so the extra element-wise compare could never disagree. Pin the
empty-readoption early return with a whole-state identity assertion,
which is the only guard the previous test could not distinguish.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-13 00:08:07 -07:00
Brennan Benson 6b84e33251 fix(mobile): trade a lease-only stream for output when leaving a chat tab (#14179)
* fix(mobile): trade a lease-only stream for output when leaving a chat tab

Tapping a terminal tab from a native-chat tab left the terminal blank. The
route subscribes the incoming handle synchronously in switchTab, while the
coverage it reads still describes the chat tab being left, so the handle gets
a `mobileInputLeaseOnly` subscribe — the host answers `subscribed` and nothing
else, no scrollback and no data frames. Input kept working because it rides a
separate terminal.send RPC.

The reconciler then cleared its covered marker (the active handle changed), so
the stream was active and uncovered — which its state machine could not tell
apart from a healthy one, because `streamActive` conflated the two. It settled
on 'none' and nothing else repaired it: the route's web-ready path bails on any
live subscription. The tab stayed blank until app restart.

Track which handles hold a lease-only subscribe and thread it into the
reconciler as `streamIsLeaseOnly`, so an uncovered handle holding one resumes
into a full stream. The covered branch is untouched, so the input lease that
keeps the chat composer from locking forever (#10681) still survives.

* fix(mobile): clarify stream reconciliation ownership

* fix(mobile): keep stream reconciliation checks clean
2026-08-13 00:07:28 -07:00
NeilandOrca 630371e879 perf(store): keep runtimeEnvironments identity across no-op catalog lists
* perf(store): keep runtimeEnvironments identity across no-op catalog lists

A no-op list or hydrate always allocated a new catalog because redact remaps
endpoints[] and IPC structuredClone rebuilds the rest. Object.is selectors
then missed on every 60s TTL refresh. Reconcile rows with areValuesEqual and
return the previous state when the catalog, statuses, and removed-id set are
unchanged after hydration.

Co-authored-by: Orca <help@stably.ai>

* type(store): mark runtimeEnvironments readonly

Widen the store field and setRuntimeEnvironments param to readonly and
stop slicing the reconciled catalog so a changed list still keeps the
reconcileCatalogRows identity instead of a fresh mutable copy.

Co-authored-by: Orca <help@stably.ai>

* type(store): propagate readonly runtimeEnvironments to consumers

Widen hydration helpers and terminal quick-command host state so the
readonly catalog typechecks, and stop asserting partial fixtures as
readonly AppState arrays.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-13 00:06:00 -07:00
NeilandOrca bc2df22328 fix(store): keep setup-script dismissal array identity on no-op repo fetches
* fix(store): keep setup-script dismissal array identity on no-op repo fetches

filterSetupScriptPromptDismissalsToValidRepos always allocated a new
array, so fetchRepos, fetchRuntimeEnvironmentRepos, and
validateRepoScopedUi replaced setupScriptPromptDismissedRepoIds even
when every entry was already a valid host-identity key.
SetupScriptPromptCard Object.is-subscribes to that array, so a no-op
catalog refresh missed 100% of the time.

Return the original store array when the filtered result is
element-wise identical to the input. Allocate only when an entry is
dropped, rewritten from a legacy generation-v1:repoId key, or deduped.
Call sites already assign the helper result.

Co-authored-by: Orca <help@stably.ai>

* type: make setupScriptPromptDismissedRepoIds readonly

Return readonly string[] from filterSetupScriptPromptDismissalsToValidRepos
and sanitizeSetupScriptPromptDismissals, and update the store field type to
readonly. This prevents accidental mutation of the live store array on no-op
prune cycles. Add isUnchangedDismissalList type predicate for identity check.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-13 00:05:56 -07:00
NeilandOrca c61f29639c fix(workspaces): recognize pasted Linear issue URLs (#14190)
* fix(workspaces): recognize pasted Linear issue URLs

* fix(workspaces): resolve legacy Linear URLs and keep typed-name fallback

Saved API-key Linear workspaces often omit organizationUrlKey, so pasted
issue URLs never called fetchLinearIssue. Probe those unknown-org
workspaces and accept only a matching issue URL.

Keep "Use … as workspace name" visible in Smart Entry when a Linear URL
owns the results. sourceIntent still focuses the issue row.

Co-authored-by: Orca <help@stably.ai>

* fix(workspaces): jump to existing worktrees from pasted task URLs

Cmd+J now treats GitHub, GitLab, Jira, and Linear issue/PR URLs as
decisive search, so pasting one lists already-linked worktrees first
and keeps a create preview underneath. GitHub/GitLab/Jira still hand
the raw URL to the composer for cross-project detection.

Co-authored-by: Orca <help@stably.ai>

* fix(workspaces): resolve GitHub issue titles in Cmd+J URL paste

Pasted GitHub issue/PR URLs now fetch the title for the create preview,
same as Linear. Existing linked worktrees stay selected first so Enter
jumps; create still hands the raw URL to the composer.

Co-authored-by: Orca <help@stably.ai>

* fix(workspaces): attach resolved GitHub items from Cmd+J create

Pasting a GitHub issue/PR URL into Cmd+J already previewed the title, but
Enter still opened the composer with the raw URL. Await the in-flight
lookup and hand the linked work item through, matching Linear and Task
page create. Also fix CI type, lint, and focus-routing source checks.

Co-authored-by: Orca <help@stably.ai>

* fix(workspaces): add Cmd+J task-URL locale keys

Unblock PR CI localization catalog checks and make the Linear lookup-miss
e2e wait out the resolving state before advancing to the agent field.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-12 23:24:06 -07:00
Neil 585dd6d3a9 fix terminal attribution shim removal edge cases (#14187)
* fix(terminal): fully retire attribution shim

* fix(terminal): harden shim tombstone path lookup
2026-08-12 23:22:48 -07:00
Neil 602f0cbe63 fix(sidebar): stabilize downward worktree card dragging
Fix downward worktree card dragging with virtualization-safe global indices and stable preview geometry. Add unit and Electron regression coverage.
2026-08-12 23:08:37 -07:00
Jinwoo Hong 9a10561258 fix(terminal): retain SSH startup delivery through reconnect (#14161) 2026-08-12 22:33:19 -07:00
Jinwoo Hong 889dd4c7d2 fix(agents): align Floating discovery and launch authority (#14106) 2026-08-12 21:56:07 -07:00
Jinwoo Hong de729d6067 Fix paired web creation failure handling (STA-4024, STA-4025, STA-4063) (#14100) 2026-08-12 21:35:35 -07:00
Neil e97faa1650 fix(browser): sanitize reserved Windows download names (#14145) 2026-08-12 21:33:18 -07:00
Neil 95ff19f35f chore(repo): ignore generated Clawpatch state (#14144) 2026-08-12 21:32:51 -07:00
BingZandBrennan Benson 204845fd78 fix(agent-hooks): fail closed without detected agent allowlist (#11676)
* fix(agent-hooks): fail closed without detected agent allowlist

Omit/empty agents no longer means install every remote managed hook.
Closes the remaining hole after #11442 that recreated uninstalled
agent config homes via the relay/SSH/WSL install path (#11641).

* test(agent-hooks): pin installManagedHooks fail-closed gate

The leaf guard already had a regression test, but the early return in
installManagedHooks was invisible to the suite: reverting it left every
agent-hooks and relay test green, because the leaf guard makes the summary
identical and the only delta is side effects (GROK_HOME login-shell probe,
host-identity read, ~/.orca mkdir + install lock).

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-12 21:32:26 -07:00
m4air b115f8d256 Add Windows golden E2E test for fresh-startup regression
Windows terminal rendering golden is flaky on CI runners. Re-enable
Windows in the golden E2E gate with a scoped test for the fresh-profile
startup regression from #14130. Terminal rendering continues on Linux
and macOS; Windows runs fresh-startup only.
2026-08-12 21:28:32 -07:00
Neil aeb7183afe Update PR template 2026-08-12 21:20:51 -07:00
Jinwoo Hong d3a2bf352f fix(terminal): preserve Fish readiness escape bytes (#14142) 2026-08-12 20:55:29 -07:00
Brennan Benson e5906837e4 perf(browser): clear cookies in one bulk call that excludes the Google family (STA-4065) (#14131)
* perf(browser): bulk clear cookies when an import excludes nothing (STA-4065)

PR #14086 replaced the bulk clear with one Electron cookies.remove() per
removable cookie so partitioned excluded cookies survive an import. That is
required whenever something must be preserved, but it also runs when nothing
is excluded, where a single clearStorageData is equivalent and far cheaper:
421ms -> 7.3ms at 1k cookies and 1764ms -> 20.1ms at 3k on Electron 43.

The fast path is gated on the same unfiltered cookies.get() the exclusion
predicate already reads, which an Electron fixture now proves returns
partitioned cookies, so "no exclusions in the snapshot" means "nothing in the
jar to preserve".

* fix(browser): protect cookie clear fast-path race (STA-4065)

* test(browser): keep the no-bulk-clear assertion honest after the clearData switch

The mocked session still exposed clearStorageData, so asserting it was never
called could no longer fail once the fast path moved to clearData.

* docs(browser): stop the fallback comment from asserting a rollback

The per-cookie path's rollback is being removed by STA-4061; the comment only
needs to say the fallback re-removes what survived.

* test(browser): keep the STA-4061 rollback fixture on the per-cookie path

The bulk fast path clears a jar with no excluded cookie in one call, which
would have made every assertion in the partition-rollback fixture vacuous.
Give it a live Google cookie so the partial-failure clear still runs, and
assert the bulk call count is zero so a future widening fails loudly.

* perf(browser): always bulk clear cookies, not only when nothing is excluded

The fast path only fired when the jar held no Google cookie, but this import
exists for Google, so a Google cookie is normally present and the common case
still paid one native remove per cookie. Measured on Electron 43, clearData
excludeOrigins preserves the whole google.com registrable family -- host,
leading-dot, subdomain and partitioned cookies -- so the bulk call is safe with
cookies to keep, and the gate was never load-bearing for preservation.

Drop the caller-supplied exclusion predicate so the preserved set has exactly
one source of truth: a predicate that disagreed with NON_TRANSPLANTABLE_DOMAINS
would have deleted cookies the bulk call is meant to keep. The per-cookie loop
stays as the fallback for a rejected bulk clear, carrying the same exclusion.

* fix(browser): re-read the cookie jar after a rejected bulk clear

clearData can empty part of the jar before rejecting, so the per-cookie
fallback was working from a snapshot taken before the attempt: it retried
removals for cookies that were already gone and could not see cookies that
arrived during the clear. Re-read on rejection only.

Also repairs three fixtures that mocked the session with clearStorageData
after the call moved to clearData. The missing method threw, the catch
swallowed it, and every one of those tests silently asserted per-cookie
behaviour while believing it covered exclusion. Records the registrable-domain
invariant at NON_TRANSPLANTABLE_DOMAINS, since excludeOrigins derives one
origin per entry, and extends the real-Electron fixture to prove http-only and
domain-scoped Google cookies survive alongside the partitioned one.
2026-08-12 20:40:47 -07:00
Jinjing 05dc845c93 Add filter and sorting to automations page (#14158)
* Add filter and sort to automations list

- Filter by status (enabled/paused) or last-run outcome (failed/succeeded/never)
- Sort by name or last run with toggleable direction
- Display last-run status with relative time in new table column

* Add filter and sorting to automations list

- Move run indexing to parent component for performance (avoid O(rows × runs) on re-render)
- Add internationalized labels for sort direction that include the column name
- Internationalize automation status labels and use locale-aware string sorting
2026-08-12 20:40:39 -07:00
NeilandOrca 58a926170c perf(renderer): index the repo catalog compat merge and keep catalog identity (#13803)
* perf(renderer): index project host ownership on repo catalog refresh

mergeFetchedProjectCompatibilityForHost runs synchronously inside a zustand
set() on every repo catalog refresh and was O(projects x (setups + repos)):
getProjectHostIds / getExplicitProjectHostIds rescanned all setups and all
repos once per project, and mergePreviousProjectMetadata rebuilt a full
catalog repo key-set per project.

Precompute the indexes once and hand each helper only its own project's slice.
No ownership logic is reimplemented — the same resolvers run, just on
pre-sliced inputs, memoized per project object.

  200 repos    1.1 ms -> 0.2 ms   (5.2x)
  600 repos   11.2 ms -> 0.7 ms  (15.3x)
  1200 repos  43.0 ms -> 1.6 ms  (26.6x)

Verified against 4,000 randomized differential cases (duplicate repo ids,
dangling sourceRepoIds, empty-repoId and orphan setups, dropped setups,
local/SSH/runtime host mixes) comparing output element provenance and order
against the previous implementation, not just deep structure. Each of the
three index helpers was negative-controlled: breaking any one of them turns
the differential red.

Co-authored-by: Orca <help@stably.ai>

* perf(renderer): keep the repo filter array identity across no-op refetches

Three fetch sites unconditionally reallocated filterRepoIds
(`s.filterRepoIds.filter(...)`) on every repo catalog refresh, even when
nothing was pruned. Six identity-sensitive subscribers select this array,
including App.tsx at the root, so every refresh woke the whole tree.

retainValidFilterRepoIds returns the input when every id is still valid;
`.every` short-circuits so the common case allocates nothing at all. It lives
in its own module rather than ui.ts: repos.ts does not import ui.ts and this
codebase has a documented circular-slice-import hazard.

The store type widens to `readonly string[]`, which takes three more
declaration edits (visible-worktrees, add-repo-skip-finalization, the setter).
PersistedUI.filterRepoIds deliberately stays `string[]`: main owns that array,
and widening it would fail the ui-state schema-parity assertion unless the zod
schema gained `.readonly()`, which Object.freeze()s every parsed `ui.set`
payload arriving from a paired client. Copy at the App.tsx IPC boundary
instead — one allocation per debounced 150ms persist, not per render.

Co-authored-by: Orca <help@stably.ai>

* perf(renderer): reconcile projects and host setups across no-op refetches

mergeFetchedProjectCompatibilityForHost always allocates — sourceRepoIds is
rebuilt per project and fetched setups arrive freshly cloned over IPC — so
`projects` and `projectHostSetups` lost both array and element identity on
every catalog refresh. #13770's identity work covered only projectGroups and
folderWorkspaces.

Reconcile both at the merge's single return site (five callers) against
`previous`, keyed on what each merge already dedups by: project.id, and
getProjectHostSetupOwnerKey for setups. setup.id is deliberately not the setup
key — the repo-derived fallback sets id = repo.id, so one repo on two hosts
yields two setups sharing an id, and keying on it would splice the wrong
host's routing metadata into a row.

filterSetupsForPrunedRepoRows had to stop returning `[...setups]`
unconditionally: three callers feed its result in as `previous`, so the
throwaway copy defeated the reconcile even though element identity survived.

Rather than add a third parallel deep-equality helper, generalize
reconcileFetchedRepos (#13744) into reconcileCatalogRows and delete repos.ts's
isPlainCatalogObject + areCatalogEntriesEqual (#13770). The two were the same
algorithm modulo the null-prototype branch; the survivor takes the union of
both prototype guards, which is strictly more permissive and so can only
reconcile more, never mask a change. repos-catalog-merge-identity.test.ts and
repo-identity-reconcile.test.ts stay green unmodified as the proof.

Element-level reuse means a consumer mutating a Project or ProjectHostSetup in
place would corrupt the previous render's object, so RepoSlice['projects'],
['projectHostSetups'] and ProjectHostSetupProjection widen to readonly. That
is the safety mechanism, not polish — it caught two real in-place accumulators
(ai-vault-scope-paths and the runtime-host purge in worktrees.ts), which now
annotate mutable locals per the repo's readonly precedent. No casts added.

Co-authored-by: Orca <help@stably.ai>

* test(renderer): cover repo catalog refresh identity

A dedicated file — repos.test.ts is already at the max-lines ceiling and these
are a distinct concern.

The fixture is production-shaped on purpose. A scalar-only repo reconciles even
when the structural compare is broken, which is how an earlier version of this
work shipped green while being completely inert; addedAt is pinned non-zero
because project-host-setup-projection falls back to `repo.addedAt || now`, so a
zero timestamp stamps Date.now() into every projection and nothing ever
reconciles. Every mocked fetch returns a structuredClone so identity can never
match by accident.

Every identity assertion was verified red against reverted source:
- drop reconcileCatalogRows from the merge return -> no-op-refetch identity and
  per-element reuse fail
- filterSetupsForPrunedRepoRows back to `[...setups]` -> no-op-refetch identity
  fails
- retainValidFilterRepoIds back to `.filter(...)` -> both filter-identity cases
  fail
- areValuesEqual reduced to `a === b` -> all three reconcile cases fail
- areValuesEqual forced to `true` -> all six change-propagation cases fail
- setup key switched to setup.id -> the two-hosts-one-repo-id case fails

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-12 20:20:47 -07:00
m4air 63a0462b1b Stop deriving interactive prompt for OMP ask tool
Why: OMP ask maps to blocked sidebar state but should not trigger
native prompt rendering. Only Pi's ask_user_question needs the
interactivePrompt payload for live card display.
2026-08-12 20:17:47 -07:00
FurmaPandaandcoderabbitai[bot] 49606af8f8 Update src/shared/agent-hook-listener.test.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-08-12 20:17:47 -07:00
FurmaPanda f04895f8b4 fix(agent-status): map omp ask tool to blocked sidebar state
The omp agent-hook path could never reach the blocked state: the
ask-question gate in normalizePiCompatibleEvent required
agentType === 'pi', isAskUserQuestionTool only matched
ask_user_question/request_user_input (omp's tool is named 'ask'),
and extractPiToolFields only derived interactivePrompt for Pi.
An omp agent parked on its ask tool therefore showed 'working'
in the sidebar with no question card.

Widen all three gates to include omp so tool_call /
tool_execution_start for 'ask' maps to blocked and carries the
pending-question envelope for the live card.
2026-08-12 20:17:47 -07:00
Brennan Benson bd12934362 fix(terminal): repaint a recovered remote pane from the host's retained buffer (#14095)
* fix(terminal): repaint a recovered remote pane from the host's retained buffer

A remote-runtime pane that loses its stream re-subscribes, but the recovery
subscribe only carries new bytes. When the host's push snapshot is empty --
an idle pane, or one whose PTY has exited and is preserved with its buffer --
the transport dropped it and nothing re-armed the restore, so the pane stayed
blank until a visibility flip issued the tagged snapshot request. That is why
switching worktrees once "fixed" it.

Emit onStreamRecovered from the recovery subscribe path only, and mark the
hidden-output restore needed so the pane pulls the buffer the host still holds.
The initial subscribe is untouched: it already carries the host snapshot, and
re-arming there would cost every pane a redundant restore request on open.

* perf(terminal): avoid duplicate recovery snapshot replay
2026-08-12 20:03:07 -07:00
e8044b1b30 fix(windows): restore fresh-profile startup after durable fsync (#14173)
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: DHTheOne <238933622+DHTheOne@users.noreply.github.com>
Co-authored-by: Anton Tupitsyn <70199858+PLUTONYY@users.noreply.github.com>
Co-authored-by: 7loop <48346764+7loop@users.noreply.github.com>
2026-08-12 19:53:04 -07:00
Brennan Benson 150cc6f50c fix(daemon): retain a flat per-session scrollback window (#13061)
* fix(daemon): bound retained scrollback across sessions

The daemon retains ~5000 rows of xterm grid per live session with no aggregate
bound, so retention scales with an unbounded session count. A host owning 100+
terminals held ~1 GB of grid, was killed under system memory exhaustion, and took
every session it owned with it.

Split a fixed row budget across live sessions instead: full depth until the budget
binds, then an even share down to a floor that still restores the command which
produced the visible screen. Re-applied on create and reap, so depth returns to
survivors as terminals exit.

The emulator itself stays load-bearing (on-disk history is a projection of it), so
this bounds the aggregate rather than lowering the per-session default.

* fix(daemon): preserve scrollback budget invariants

* fix(daemon): budget scrollback by retained cells

* fix(daemon): retain full scrollback for viewed sessions, trim only parked LRU overflow

Replace the even-split budget: dividing a fixed row budget across all sessions
shallowed every terminal as the count grew, so a user with 60 terminals silently
lost half their reachable scrollback — depth degradation with no signal.

Retention now trims by attention, not arithmetic. Attached sessions always keep
full depth and never consume the cap. Parked sessions keep full depth up to a
cap of 24, least-recently-viewed evicted first down to 1000 rows — enough to
reattach with the recent command context on screen. A reattached session
returns to full depth for everything it emits afterward, and a freed slot
returns the newest trimmed session to deep retention.

Worst case is bounded at cap x full depth plus trimmed remainder, the same
memory class as the old budget, without ever shallowing a terminal the user is
looking at.

* fix(daemon): release attachments on transport drop so dead clients cannot pin full depth

The attached-client exemption made an attachment that outlives its transport a
permanent full-depth pin: protocol detach was logging-only, and neither control-
nor stream-socket loss removed the Session.attachedClients entry. Enough dead
attachments would silently rebuild the unbounded retention the LRU cap prevents.

Track the attach token per session, release exactly the dropped client's
attachments in one batched retention pass, implement protocol detach for real,
and cancel an attach whose client vanished mid-flight. Move session-exit
bookkeeping to the host reap hook — it previously lived in the per-attachment
exit callback, which only fired for unattached sessions BECAUSE of the leak, so
fixing the leak made an unattached session's exit invisible to idle shutdown.

Also drop the create-time double recency increment.

Co-developed with a review pass; transport-drop release is regression-tested
against the unfixed daemon (fails without, passes with).

* refactor(daemon): move retention application into the policy module

terminal-host.ts crossed the 300-line cap; the entry-building and depth
application belong with the selection policy anyway, leaving the host with
only the recency bookkeeping it uniquely owns.

* fix(daemon): remove stray QA artifacts and restore the emulator line budget

The previous commit accidentally swept QA screenshots and a report from the
repo root into the tree (root-directory-guard failure), and headless-emulator
had crept to 301 counted lines. Screenshots live on the PR via the attachments
CDN, never in the tree. The retained-scrollback trim moves to
headless-emulator-modes with the OSC-link shift as a pure helper.

* fix(daemon): retain a flat per-session scrollback window

Replace the LRU parked-session retention with what every peer terminal ships:
a flat, small per-session window in the durable host. Sessions retain 1000
rows — the generous end of what terminal products restore on a rebuild — and
deep scrolling on an open terminal remains the renderer's live buffer.

This deletes the dynamic-retention machinery entirely: no recency tracking, no
attached-exemption, no runtime trimming, no OSC-link shift on trim. The window
is set once at session creation. Worst case at the incident's 100+ sessions is
~100k rows of grid, versus ~500k before.

The bounded env override can tune the window within [100, 5000]; anything
outside falls back to the default, since an unbounded daemon is the failure
this window exists to prevent.

The v29->v30 history-handoff fixture now pins the old-daemon depth explicitly:
it plays an old binary whose sessions retained ~5000 rows, and the new flat
window would otherwise shrink its history below the chunked-seed threshold and
silently skip the transfer path the test exists to cover.

* docs(daemon): retire retention-era wording the flat window obsoleted
2026-08-12 19:16:34 -07:00
Brennan Benson bc2e30000b fix(browser): never resurrect cookies after a failed import clear (STA-4061) (#14132)
* fix(browser): never resurrect cookies after a failed import clear (STA-4061)

removeAllCookiesExcept rolled a partial clear back by rebuilding each removed
cookie with cookies.set. Electron's cookies.get() omits partitionKey and
cookies.set() drops it, so a non-Google CHIPS cookie that had been removed came
back as an ordinary unpartitioned cookie — silent, restart-proof auth-state
corruption on a code path that reports failure.

The exclusion comment already stated the invariant ("Electron cannot round-trip
partition identity"); it was only applied to the Google family. Extend it to the
rollback: a failed clear now stays failed, which is retryable, instead of
reconstructing cookies whose identity cannot be reproduced.

* test(browser): require partitioned cookie removal (STA-4061)
2026-08-12 18:53:42 -07:00
NeilandOrca c86418eaad rm git shim (#14141)
* rm git shim

Drops the terminal git/gh PATH wrapper and its settings toggle. Renames the no-marker shell-ready launch config after what it does.

Co-authored-by: Orca <help@stably.ai>

* rm git shim: clear stale state from older installs

Deletes the orphaned wrapper dir and scrubs inherited env/PATH, so a daemon that outlives the upgrade cannot keep seeding it. Drops a now-unread spawn option.

Co-authored-by: Orca <help@stably.ai>

* rm git shim: cover the daemon and headless paths

Scrub after the PATH prepends (they re-read process.env on the sparse daemon env) and run the cleanup above the serve branch so remote hosts get it too. Retry a locked removal; match PATH case-insensitively.

Co-authored-by: Orca <help@stably.ai>

* rm git shim: keep the scrub final

Refuse to re-prepend a legacy entry during agent-teams PATH promotion, which runs after the scrub. Cover the removal guard.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-12 18:53:17 -07:00
NeilandOrca f1ee3088a5 perf: avoid polling remote runtimes on local repo changes (#13632)
* perf: keep local repo events off remote runtimes

* test(renderer): assert parked-pane remount on the runtime-active repos:changed path

The local-slice refresh 13632 introduces still has to release panes parked on
an unhydrated host; the prior test stubbed the remount but never asserted it.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-12 18:47:03 -07:00
Brennan Benson d9b390f4fd fix(daemon): let the connect prove a retired daemon, not the token read (#14128)
* fix(daemon): let the connect prove a retired daemon, not the token read

The daemon unlinks its token on exit but leaves its socket, and the client read
that token as the first statement of doConnect. So a retired endpoint failed at
the open, producing an errno no recovery predicate could classify — every one of
them keys on syscall 'connect' — and the pane showed raw errno text.

Read the token totally instead. A dead endpoint now fails at the connect, where
isDaemonGoneError already authorizes a respawn and isDaemonEndpointGoneError
already explains itself; a live daemon whose token was rotated or removed
rejects the empty token as 'Invalid token' rather than being declared dead by a
missing file. checkDaemonHealth has always read the token this way.

* fix(daemon): require endpoint failure for respawn
2026-08-12 18:28:35 -07:00
Brennan BensonandGldywn 6ac39b7331 fix(worktrees): make hidden agent worktrees recoverable from the visibility dialog (#13652)
* fix(worktrees): make hidden agent worktrees recoverable from the visibility dialog

A discovered agent scratch worktree (.claude/worktrees, .gsd-workspaces)
was a one-way door: the non-Orca visibility toggle never reveals scratch
by design (#9388), the inbox never announces it, and the dialog listed
nothing — so once hidden it was unreachable from the UI while sitting on
disk. The per-path import exception has outranked the hidden rule all
along; no surface offered it.

The dialog now refetches an authoritative list on open (a stale snapshot
must not read as 'nothing hidden'), lists hidden importable worktrees,
and offers a per-row Show wired to the existing inbox import action,
which already merges the import + baseline and rolls back on a failed
refresh. When the list cannot be read the dialog says so and offers a
retry instead of claiming the repo has nothing.

No new settings, schema, or persistence: recovery rides entirely on
importedExternalWorktreePaths, which every host already stores and
validates. The repo-wide toggle is untouched and still never reveals
scratch.

Fix #10324

Co-authored-by: Gldywn <14254051+Gldywn@users.noreply.github.com>

* fix(worktrees): honest scan states and race-safe row actions in the visibility dialog

- row Show stays disabled until the open-time authoritative scan settles;
  a click mid-scan could join the pre-write refetch and read success off a
  list computed before the import landed, a silent no-op on slow hosts
- checking/failed indicators follow the scan state alone, so a warm older
  snapshot cannot present stale rows as current with no failure indication
- ownership-neutral section copy: non-scratch rows are listed too when the
  repo-wide switch is off

* fix(worktrees): close the retry race window and clear stale failure state

- Try again is locked while a row import is in flight; a retry scan
  started before the import's write lands can absorb the import's own
  refetch and report success off a pre-import list
- a successful row import (which requires a successful authoritative
  refetch) clears an earlier failed open-time scan instead of leaving a
  contradictory alert over the refreshed list
- zh: 智能体 for agent (代理 reads as network proxy); polite live region
  on the checking hint

* fix(worktrees): serialize visibility dialog actions

* fix(worktrees): clarify persistent visibility policy

* fix(worktrees): clarify hidden worktree list

* Explain hidden worktree defaults

* Show agent worktrees with Always show

* fix(worktrees): bound visibility dialog state and rendering

* fix(worktrees): preserve visibility mutation fences across dismissal

* fix(worktrees): scope visibility mutations by host

---------

Co-authored-by: Gldywn <14254051+Gldywn@users.noreply.github.com>
2026-08-12 18:19:33 -07:00
Neil c1e517db56 fix(renderer): refresh runtime catalogs on remote repo events (#13787)
A runtime host emits one `reposChanged` client event for project-group and
folder-workspace mutations as well as repo mutations (createProjectGroup,
moveProjectToGroup, createFolderWorkspace, ... all call notifyReposChanged),
but the client's handler only refetched that host's repo catalog, worktrees,
and lineage. Group and folder-workspace rows for a remote runtime therefore
stayed stale for the rest of the session unless an unrelated *local*
`repos:changed` happened to fire the all-host sweep.

Refetch the environment's project group and folder workspace catalogs in the
same scheduled refresh. Groups are fetched first because folder workspaces
resolve their owning group from `projectGroups`. Both fetches are host-fenced
(`claimHostCatalogFence`), so they cannot clobber local or other runtimes'
rows, and the scheduler's existing debounce/min-interval still bounds them.
2026-08-12 18:14:21 -07:00
Brennan Benson a2c9f7e52b fix(terminal): never wipe a painted pane for a snapshot that carries no image (#14099)
* fix(terminal): never wipe a painted pane for a snapshot that carries no image

A hidden-output restore that comes back as a success frame with an empty image
was still applied. The normal-buffer replay opens with \x1b[2J\x1b[3J\x1b[H, so
it cleared screen and scrollback and painted nothing in their place -- turning a
host that had nothing to say into a blank pane.

An empty image is the absence of an answer, not proof the pane is empty, so keep
the frame the user can still see. Retry and banner behavior are unchanged: an
empty image still counts as a successful, non-bannered recovery.

* test(terminal): prove imageless restore preserves painted buffer
2026-08-12 18:05:05 -07:00
Neil 9772da844a perf(renderer): publish agent status bursts once (#13974)
Agent-status events fan out multiplicatively: every event pays an
O(worktrees x tabs) pane-routing scan and its own zustand publication, and
WorktreeList's unconditional sortEpoch subscription re-renders the whole
sidebar root on each one. A 256-pane reconnect replay meant 256 full sidebar
re-renders.

Fold a burst into one status publication (plus one generated-title and one
tab-title publication - three total, not 2N) via transactAgentStatuses, and
build the pane-routing index once per batch.

Coalescing is only safe for level-triggered consumers. Two edge-triggered
ones needed work:

- useAutomationDispatchEvents diffed entry.state, so a swallowed intermediate
  `done` lost the run's completion output. It now walks the newly appended
  stateHistory rows and reads the completed turn's output from the entry-level
  lastCompletedAssistantMessage slot (one message per pane - putting it on
  every history row would retain 20 transcripts per live status and reprise
  the renderer OOM in #9872).

- pty-connection's native-Windows ConPTY reset compared `state` against a
  closure-local previous value, so a coalesced done -> [working, done] never
  re-fired RESET_TERMINAL_CURSOR_STYLE / RESET_KITTY_KEYBOARD_PROTOCOL and
  left the pane with kitty keyboard protocol armed, corrupting plain input.
  It now tracks the newest completed turn's start across stateHistory, which
  also keeps same-turn `done` repaints from re-resetting.

The transaction commits as a MERGE patch of the keys the fold changed, not a
REPLACE of its snapshot: a batched action reaching another slice through
get() writes straight to the real store, and a REPLACE would silently revert
it. The shadowed `set` is typed without zustand's `replace` parameter so no
call site can reintroduce semantics the merge cannot express.
2026-08-12 17:46:25 -07:00
erish 1192f73f4d docs(i18n): sync Korean README with current content (#14124) 2026-08-12 17:32:36 -07:00
Brennan Benson 4665bd2797 test(agent-hooks): stop the GROK_HOME probe test from spawning a real shell (#14120)
resolveRelayGrokHome probes a login shell and folds every spawn failure into its
fallback, so the test was asserting the runner's scheduling latency rather than the
parser. On a loaded 16-way sharded CI box the 8s timeout expires, the probe falls
back, and the first case fails with '/home/orca/.grok' instead of '/srv/grok'. The
failing job's teardown terminated orphaned `sh` and `head` processes -- the probe's
own command, still live at job end.

Stub node:child_process so the parse, the shell-mode choice, and the fallback are
deterministic. Adds the probe-failure case, which is the branch that was silently
absorbing the flake, and pins -c for sh/dash versus -lc elsewhere.
2026-08-12 17:27:10 -07:00
ef8eea51af fix(terminal): retain alt frames through park reveal (#13882)
* fix(terminal): repaint an alt frame the fit never lands for

#13014 skips a too-wide alt frame on snapshot replay and relies on a post-fit
repaint. For a pane visible but permanently below the 48x24px fit floor,
readProposedTerminalCols is forever undefined, so skipIfTargetUnknown drops the
frame — and the only compensating repaint lives in a safe-fit continuation that
is failed rather than run for such a pane. The frame is lost until the user
widens the pane.

Adds a deferred repaint gated on display:none rather than on target-unknown,
preserving #13014's no-clip guarantee for a pane that later fits narrower.

Does NOT fix artificial-opencode-terminal-load.spec.ts:761. Two open concerns:
the repaint waits out the full 40-frame safe-fit retry budget (~1.3s) against
that spec's own 2s restore budget, and the cold-park reveal path at
pty-connection.ts:8222/8289 still has no deferral net.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): restore the deferred PTY input hop

Inlining forwardPtyInput into the onData handler dropped the
deps.deferPtyInput dispatch, leaving the callback and its type declared
and supplied but never called. Link-click mouse suppression silently
stopped withholding mouse-report frames, so a click in a mouse-tracking
TUI reached the PTY and the link-open fallback never fired.

Covers both branches, which had no test at all.

* fix(terminal): retain alt frames until final fit

* fix(terminal): preserve park intent across effect replay

* test(terminal): satisfy native static analysis

* fix(terminal): consume park intent after layout replay

* fix(terminal): re-read park intent instead of caching it per pane

useTerminalParkMountIntent cached park intent in a ref for the lifetime of
the component. The ref was never load-bearing: React finishes the render
phase before any effect runs, so StrictMode's passive-effect replay already
observes the same render-phase value. What makes the reveal work is reading
at render time rather than at connect time, which a plain call already does.

The ref did go stale. The pane lifecycle effect re-runs on [tabId, cwd] for
the same component instance, so a worktree path or startupCwd change
resupplied mountFollowsTerminalPark: true long after the park reveal, since
the post-replay consume clears the per-run ptyDeps copy and not the ref.
Each spurious run re-armed the pre-paint snapshot prefetch.

Also scope the post-replay consume behind a tested one-shot helper so splits
created after the restored-layout replay keep ordinary reconnect semantics,
and correct two comments that described the removed ref.

---------

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-12 16:46:27 -07:00
Brennan Benson 0ae0fd0434 fix(mobile-ios): set app_platform for distribute-only TestFlight upload (#14087) 2026-08-12 16:23:18 -07:00
Brennan Benson cb41f3c5f4 fix(terminal): require agent identity for guarded sends (STA-4028) (#14092)
* fix(terminal): require agent identity for guarded sends (STA-4028)

Quarter-circle spinner glyphs (U+25D0-U+25D3) started classifying a title
as "working" in #13925, and that status alone authorized guarded agent
sends — which auto-submit with Enter — into any pane whose TUI animates
those generic progress frames.

Keep the glyphs as an activity signal, but stop treating a title whose
only agent evidence is a quarter circle as proof an agent owns the pane:
send authorization now falls through to recognized-agent identity in the
title or a recognized foreground process. Braille-spinner behavior is
unchanged.

* fix(terminal): preserve verified managed busy identity

* fix(terminal): bind busy identity to process incarnation

* chore(test): avoid duplicate terminal gate suite
2026-08-12 16:22:06 -07:00
Jinwoo Hong e8350f1990 fix(browser): route remote terminal links to owning host (#14117) 2026-08-12 16:15:39 -07:00
Jinwoo Hong 9a81577e91 fix(browser): disable unsupported FedCM (#14023) 2026-08-12 16:07:08 -07:00
Brennan Benson 564968c31e fix(browser): guide Google sign-in after cookie import (STA-4019) (#14085)
* fix(browser): guide Google sign-in after cookie import

* fix(browser): show runtime name in import guidance

* fix(browser): honor host display labels in import guidance
2026-08-12 16:05:46 -07:00
Jinwoo Hong d349f9a972 Use provider-neutral Opus alias in orchestration guide (#14119) 2026-08-12 16:03:29 -07:00