Commit Graph
4895 Commits
Author SHA1 Message Date
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
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
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
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
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
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 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
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
Jinwoo Hong e8350f1990 fix(browser): route remote terminal links to owning host (#14117) 2026-08-12 16:15:39 -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
Jinjing e4c278eb52 Extract AutomationTimeField component with improved digit input (#13975)
* Extract AutomationTimeField component with improved digit input

Moves time parsing and selection logic into a reusable component
with enhanced UX: arrow key stepping, auto-commit on 2-digit entry,
and improved validation. Refactors AutomationSchedulePicker to use
the new component, reducing code duplication.

* fix(renderer): stop mutating clockRef during AutomationTimeField render

Move the prop→ref sync into useEffect so React Doctor no longer fails static analysis on ref writes during render. patchTime still updates the ref immediately for concurrent digit commits.

* fix(renderer): improve AM/PM toggle accessibility in AutomationTimeField

- Aria-label now includes current period (AM/PM) so screen readers announce which is selected
- Add aria-pressed attribute to communicate toggle button state
- Move translation keys to AutomationTimeField namespace reflecting component extraction

* fix(ci): retry Electron install on HTTP/2 stream and Fetch 503 errors

CI shards failed before tests when GitHub release downloads refused HTTP/2
streams or returned Fetch Response.status 503, which were not classified as
transient and skipped retries.

* fix(ci): give Electron package downloads more retry headroom

GitHub release CDN still returned 503 after the short 3-attempt budget under
CI fan-out; extend the default backoff so install can recover.
2026-08-12 14:40:39 -07:00
Jinjing 798b9b3d6b fix: persist review notes for folder workspaces (#14112)
* fix: persist review notes for folder workspaces

* test: satisfy duplicate import lint
2026-08-12 14:37:51 -07:00
Jinwoo Hong ebe5125476 Gate paired web creation actions by provider (#13909) 2026-08-12 13:42:42 -07:00
OrcaWin 09ec516ae5 fix(editor): index WSL watcher aliases per batch (#14015) 2026-08-12 03:24:31 -07:00
OrcaWin e5cd5bf542 fix: decouple Floating Workspace shell selection (#13995) 2026-08-12 02:09:57 -07:00
Jinwoo Hong 70ad65fc94 Fix paired remote HTML browser ownership and focus (#13876)
* Fix paired remote HTML browser preview

* Register paired HTML preview reliability gate

* Scope browser worktree selection guard

* Harden paired HTML preview ordering

* Simplify paired HTML preview ownership

* Record final paired HTML preview evidence

* fix(remote): simplify browser preview ownership

* test(remote): refresh HTML preview evidence

* fix(remote): reconcile browser create ordering

* fix(remote): guard browser focus through reconcile

* fix(remote): bound delayed browser focus

* fix(remote): guard browser focus context

* test(remote): record final HTML preview evidence

* fix(remote): keep HTML previews in background

* test(remote): cover browser focus modes

* test(remote): record focused browser contracts
2026-08-12 02:02:10 -07:00
Jinwoo Hong 346e59c879 fix(renderer): keep commit tooltip lines intact (#14000) 2026-08-12 01:10:47 -07:00
Brennan Benson 36d45af062 feat(browser): guide Google sign-in after cookie imports (STA-3811) (#13666) 2026-08-12 00:53:02 -07:00
Brennan Benson 2249330acf fix(browser): bulk clear cookies during native import (#13966)
* fix(browser): bulk clear cookies during native import

* fix(browser): separate Google cookie import warning

* fix(browser): report encrypted Google cookie exclusions

* fix(browser): skip key lookup for excluded cookies

* test(browser): cover excluded-cookie key bypass

* perf(browser): keep plaintext key scan cheap
2026-08-12 00:33:50 -07:00
NeilandOrca a90a18d43d perf(renderer): anchor spinner phase from animationstart only (#13987)
Co-authored-by: Orca <help@stably.ai>
2026-08-12 00:30:14 -07:00
Jinjing a81224614a fix(agents): detect a live OpenCode pane from its native OC | session title (#13957)
* fix(agents): detect a live OpenCode pane from its native OC | session title

OpenCode publishes `OC | <session>` as its OSC title, which carries no
agent-name token. detectAgentStatusFromTitle gates status on a whole-token
name match, so it returned null and every status consumer read a live
OpenCode pane as a plain shell: no "Send notes to" entry, no title-derived
sidebar row, and a title that the runtime's agent-presence check scored as
neutral. Identity already resolved (getAgentLabel returns OpenCode); only
activity was missing.

Treat the native marker as a live idle agent, placed after the spinner and
glyph checks so the decorated frames pinned by #8940 keep their status, and
accept it in the send-readiness gate the way Claude's U+2733 prefix is
accepted -- only a running OpenCode TUI ever publishes it.

* fix(agents): require spaced `OC | ` marker for native OpenCode detection

Unspaced pipes like `OC|Build` match other tools and would mistakenly
route non-OpenCode panes as send targets. Enforce literal ` | ` as
OpenCode emits it. Also clarify that only spinner decorations carry
working status, not keywords in the session summary.

* fix(agents): require OpenCode foreground process to validate native mark

OpenCode's native `OC | ` title marker now requires an active OpenCode process
to authorize agent sends, preventing false detection when the marker is left on
shell prompts. Extends wrapper prefix matching (ssh, tmux, etc.) and spinner
glyph support. Adds permission-prompt blocking signals for guarded writes.
2026-08-12 00:07:59 -07:00
Jinwoo Hong fd2afc16c8 Fix paired web Add Project folder browsing (#13885) 2026-08-11 23:25:50 -07:00
Neil 9e18e1456b fix(workspace-emoji): keep the ':' shortcode suggestions scrolled to the highlight (#13969) 2026-08-11 22:18:26 -07:00
Neil 3d4c968e9b perf(renderer): drop redundant worktree card cache-TTL subscription (#13903)
promptCacheTtlMs was a third store subscription for a field the card
already reads via foundation.settings; derive it instead.

Does NOT bundle the 26 foundation selectors as originally planned.
Measured on this store with React (150 cards, 400 unrelated writes,
26 keys): 26 separate useAppStore hooks 100.7ms; one useShallow bundle
402.5ms (4.0x worse); one bundle with a non-allocating keyed compare
141.5ms (1.4x worse). zustand v5 shallow() rebuilds two Maps from
Object.entries per compare, and a bundle must still build the whole
projection every notification, so collapsing cheap selectors only
trades away useSyncExternalStore dispatches that are cheaper than the
object it allocates. Listener count would have improved 26 -> 1 while
the hot path got slower.
2026-08-11 22:13:39 -07:00
NeilandOrca 0079623f26 Fix reveal active workspace needing multiple clicks (#13926)
Co-authored-by: Orca <help@stably.ai>
2026-08-11 22:10:44 -07:00
Jinjing 5bee7b5ce9 P1 STA 3887 design Preview Kitty IME (#13940)
* fix(terminal): carry kitty flags through Preview snapshots and pair rele

Preview was omitting the live kitty mirror from the IME bridge and dropping kitty flags from snapshots, so every commit was evaluated at flags 0. A TUI that negotiated bit-3 (report_all_keys_as_escape_codes) would receive the legacy raw text it declined.

Now the snapshot carries proven kitty flags beside their sequence boundary, the forwarder reads flags once per commit, and bit-1 (report_event_types) commits are paired with exactly one release regardless of keyup/insertText ordering. Snapshot authorities expose only the active screen's proven flags, so an old host's absent field stays unknown rather than downgraded to a manufactured zero.

* fix(terminal): sync kitty flags and IME releases across snapshots

* trim wordinesss

* fix(terminal): settle owed IME release before fresh same-key press

When a keyup is lost and the same key is pressed again, settle the stale
record's owed release instead of discarding it — this maintains correct
IME state during recovery. Also refine Kitty flag propagation to only
carry proven baselines across snapshots, and tighten related comments.

* fix(terminal): gate kitty flags on sequence boundaries

- Remote snapshots only include flags when seq is present
- Daemon uses parsed flags value when defined
- Ensures correct flag ordering in snapshot replay
2026-08-11 22:00:44 -07:00
NeilandOrca 5ea7df1a5b fix(terminal): make DECSET 2031 subscriptions silent (#13904)
fish arms `CSI ?2031h` before painting each prompt and withdraws it when it
hands the tty to a child — a ~1ms window. Orca answered that subscribe with
`CSI ?997;Nn` across a 1-3ms renderer hop, so the reply landed after the
withdrawal and was read as stdin by the next child, corrupting `brew`/`npx`
`[y/N]` prompts.

The reply is not stale by Orca's own view when written (measured
staleReplies: 0), so no suppress-the-stale-reply scheme can close this — the
information needed to suppress does not exist yet. Nothing asked for the reply
either. The Contour spec says a terminal "should only send out the DSR when the
palette has been updated"; Ghostty (Termio.zig:729 — force=true reachable only
from the ?996n DSR), iTerm2 (VT100Terminal.m:995 — flag only) and xterm.js
(InputHandler.ts:2035 — flag only) all emit nothing on the DECSET. So stop
entering the race: record the subscription, answer nothing.

Of 17 real programs measured under a pty, only fish, tmux, claude and opencode
subscribe; none block on a reply, and answering produces one redundant palette
re-query and zero rendering difference. tmux is the only one that sends `?996n`,
which Orca still answers.

- Subscribes are record-only at all four emitters (live scan, hidden-gate fact,
  parked byte watcher, parked responder — the last is deleted, it only replied).
- `?996n` answers, the subscription registry, and the theme-flip push are
  unchanged. `paneLastThemeMode` is still seeded at subscribe so the next
  appearance re-apply is not read as a flip.
- Replay grammar carries `?2031l` alongside `?2031h`, so a late-attaching remote
  client no longer registers a subscription the TUI already retired.

Also closes fish-integration gaps found alongside: `unset` (which fish lacks)
becomes `set -e` on paths parsed by the client's login shell, `config.fish` is
parsed for agent-home detection, and bracketed-paste startup delivery is made
consistent across local/daemon/relay.

Regression test drives real fish 4.7.1 under node-pty and asserts on what the
child process reads; it fails against pre-fix code with the exact payload from
the issue. CI installs fish 4 and fails loudly rather than skipping.

Closes #9993

Co-authored-by: Orca <help@stably.ai>
2026-08-11 21:16:36 -07:00
Brennan Benson 137e724119 fix(agent-title): treat Claude Code quarter-circle spinners as working (#13889) (#13925)
* fix(agent-title): treat Claude Code quarter-circle spinners as working

Claude Code 2.1.228 swapped its busy OSC title spinner from braille
(U+2800-U+28FF) to quarter circles (U+25D0/U+25D1). Orca recognized a busy
Claude title only by braille codepoints, so the new frames matched nothing.

The summary-bearing busy frame ("<glyph> Say hi in one word") carries no
"claude" name token, so it resolved to no-status. The tracker's "idle or
permission followed by no-status means the agent exited" rule then fired
mid-turn, confirmPtyAgentExit confirmed it, and the chat surface routed
exitChat -- kicking the tab to the terminal view on every message.

Widen the accepted glyph set via a shared containsAgentSpinnerGlyph helper.
Agent-specific braille frame shapes (Grok, Pi, synthetic Cursor) stay pinned
to their own glyph set.

Fixes #13889

* fix(agent-title): satisfy static analysis and trim scope
2026-08-11 21:13:27 -07:00
948d85cabc fix(terminal): rank cold-park recency by activation order, not random UUID (#13881)
* fix(terminal): rank cold-park recency by activation order, not random UUID

The keep-warm exemption (#8262) broke equal hiddenSinceMs with
id.localeCompare over UUIDv4 tab ids, so which tab stays warm was a coin
flip. Ties are routine, not exotic: use-terminal-tab-cold-parking takes one
Date.now() per effect pass (#7214) and stamps every tab first seen hidden in
that pass with it, so switching away from a worktree ties all of them. The
tab that wins the flip is skipped by selectIdsBeyondHotRetain and never parks.

Ranks by an explicit activation sequence recorded on the hidden->visible edge.
Unit test covers the tie; red before, green after.

Does NOT fix terminal-hidden-view-parking.spec.ts:467, which still fails with
the same 'did not park' error on CI. Shipping this on its own merit.

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

* fix(terminal): preserve focused tab across parking ties

---------

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: E2E Test <e2e@test.local>
2026-08-11 21:07:57 -07:00
Jinwoo Hong b51ef400ec fix(quick-commands): flatten the settings list and give the command editor room (#13922) 2026-08-11 19:53:22 -07:00
NeilandOrca 64aec94cb2 fix(sidebar): give draft reviews their own glyph instead of a red PR icon (#13919)
* fix(sidebar): give draft reviews their own glyph instead of a red PR icon

The worktree card review icon used one PR glyph for every state and
tinted it by check status, so a draft with failing checks rendered as a
red PR icon that read as closed. Shape now carries review state
(draft/closed/merged), and check tone applies only to open reviews so
the glyph agrees with its tooltip. Same fix for the source-control
header icon, where draft and closed differed only by a muted tone.

Closes #13088

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

* test(sidebar): pin stateless and GitLab-closed review icon behaviour

Documents why a stateless row keeps its check tone (folder cards render
one while a linked review loads or its details fail) and covers the
closed-MR glyph override, which previously read as already-merged.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-11 19:26:53 -07:00
Neil 991a3fe963 chore(lint): update oxlint to 1.77 and enable no-op cleanup rules (#13901)
Enable eleven oxlint rules that simplify code without changing behavior, and fix
every existing violation. Each candidate was gated on measured cost rather than
assumption, so rules that regressed runtime performance or type checking were
dropped instead of suppressed.

typescript/no-redundant-type-constituents is the largest addition: 113 sites, no
autofix. Dead constituents are deleted. Where the redundant literal existed to
document intent (`string | 'all'`), it is preserved as `(string & {})`, which
keeps the autocomplete hint the original code was reaching for instead of
flattening it away. The rule also caught a broken import —
remote-shared-control-retirement-probe.ts pulled RuntimeStatus from
src/shared/types, which does not export it, so the type silently degraded to
`any`; no tsconfig covers that file, so tsc never saw it.

oxlint stays at 1.77.0 rather than 1.78.0 because .npmrc sets
minimum-release-age=4320 and 1.78.0 is younger than that window.

Rules evaluated and rejected, with what disqualified each:
- prefer-string-raw: String.raw is a runtime call, not a literal (184x slower)
- prefer-string-replace-all: 26% slower
- text-encoding-identifier-case: ~5% slower, reproducible
- prefer-spread: [...str] is 110% slower than split('') and differs on surrogates
- no-implicit-coercion: `!!x` narrows types and `Boolean(x)` does not (22 tsc errors)
- prefer-arrow-callback: arrows are not constructible, breaking `new` on mocks
- object-shorthand: rewrites source text asserted by a tracked reliability gate
- switch-case-braces: pushes ten files past max-lines, which cannot be suppressed
- no-useless-switch-case: drops `case undefined:` that switch-exhaustiveness-check needs
- arrow-body-style: 115 violations have no fix, and it breaks max-lines
- newline-after-import: false-positives on the leading-semicolon ASI idiom

electron-vite-output-contract asserted on the literal
Object.prototype.hasOwnProperty.call text; retarget it to Object.hasOwn, which
rejects inherited keys identically.
2026-08-11 18:19:43 -07:00
JinjingandOrca 4c2a10d157 Fix smart sort ranking of done agents by completion time (#13899)
* Fix smart sort ranking of done agents by completion time

Completed entries stayed in the Done sort class indefinitely when
same-state writes refreshed updatedAt without moving stateStartedAt.
Introduce agentEntryCompletionAt() to use actual completion time for
both age display and sort eligibility, ensuring consistent aging
regardless of hook updates.

* Fix smart sort ranking of done agents by completion time

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-11 18:11:34 -07:00
Jinjing 96c17b44d5 Resolve threads as primary acknowledgement, not replies (#13894)
Collapsed threads visibly close on the host — this is the
acknowledgement. An extra reply only adds noise. Snapshot the
resolution target at launch to prevent silent drops if the panel
navigates before agent delivery. Only reply for comments without
resolution endpoints.
2026-08-11 17:38:15 -07:00
Jinjing 675400e318 fix(automations): give list table more breathing room (#13898)
Increase row height and the gap above the table so the Automations list feels less dense.
2026-08-11 17:33:42 -07:00
JinjingandOrca 0c13e51097 feat(pr-comments): sort grouped PR comment sections newest-first (#13893)
Grouped/triage sections now read newest-first so recent discussion surfaces
at the top, while Timeline keeps its oldest-first history order.

- rename sortPRCommentGroupsForTimeline to sortPRCommentGroupsByRecency and
  add an order parameter
- rank threads by their latest activity under newest-first so a fresh reply
  refreshes an old thread
- break timestamp ties by numeric comment id in the sort direction, so a
  GitHub review batch sharing one timestamp orders correctly
- sink groups with an unparseable createdAt to the bottom in both orders

Co-authored-by: Orca <help@stably.ai>
2026-08-11 17:26:48 -07:00