* fix(ssh): reconnect persisted PTYs after shutdown races
* fix(ssh): harden shutdown reconnect-target derivation
- exclude runtime-owned (ephemeral-VM) targets: a renderer-driven startup
ssh.connect would dispose the runtime layer's live relay session
- exclude explicitly disconnected / auth-failed targets so startup never
dials a host the user chose to leave offline (sessions still restore
on tab focus via the deferred flow)
- avoid re-running buildTerminalSessionData via the default parameter
when the remote-session map is legitimately empty (null sentinel)
- recompute activeConnectionIdsAtShutdown alongside terminal-field
patches so a crash between patches cannot strand a stale target
- pin all of the above with tests, including the patch-writer path
Co-authored-by: Orca <help@stably.ai>
* fix(ssh): filter runtime-owned targets at startup reconnect consumption
Main's windowless-promotion path can persist runtime-owned ids into
activeConnectionIdsAtShutdown; renderer must never dial those.
Co-authored-by: Orca <help@stably.ai>
* test(ssh): pin runtime-owned exclusion independently of the status gate
Give the runtime-owned fixture a transient status so the status gate
alone cannot exclude it; verified by mutation (guard removal fails).
Co-authored-by: Orca <help@stably.ai>
* refactor(session): extract reconnect-target derivation to its own module
workspace-session.ts crossed the max-lines budget; the derivation is a
self-contained concern (session ids + live SSH state -> startup targets).
Parameter is now required (null = none) since every caller has the
terminal session data or must opt into the scan explicitly.
Co-authored-by: Orca <help@stably.ai>
* fix(ssh): exclude runtime-owned targets from the connected-states path too
A pane-level optimistic write can stamp a runtime-owned connection state;
the live 'connected' filter must apply the same exclusion as the
session-id union (CodeRabbit review finding). Pinned in the test on both
paths.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
The previous commit updated the README to docs/assets/wechat-qr.jpg and
removed the old dual-QR assets, but never added the new image file, so
GitHub could not load it.
- Replace outdated dual-QR (primary + backup) layout with one QR code, since the first group filled up and members should join the second group instead
- Applies to both README.md and the zh-CN translation
- Remove the now-unused backup/png QR image assets
- Refresh star-history chart and backup WeChat QR image assets
- Add spacing between the primary and backup WeChat QR codes in both README.md and README.zh-CN.md
Adds self-hosted Jira Server/Data Center support (personal access token or classic username + password) alongside Atlassian Cloud, fully addressing the older-instance ask in #6676. Takeover of #7724 (@wquintal's original PAT implementation), brought current with main and hardened via a multi-agent adversarial review.
Fixes#6676.
Co-authored-by: William Quintal <williamquintal95@gmail.com>
- Extracts a per-client localStorage persistence layer with normalization/fallback for malformed or catalog-stale stored values
- Adds a hook wrapping the previous local useState wiring so AiVaultPanel keeps the same setter API while reads/writes go through storage
- Scope is intentionally excluded from persistence since it depends on current workspace/project availability
The three pr-evidence/*.png files were accidentally committed with the
sidebar fix (#8527). They are e2e evidence outputs regenerated on demand
by worktree-lineage-agent-expansion.spec.ts only under
ORCA_CAPTURE_EVIDENCE=1, and nothing reads them — contradicting the
.gitignore intent that PR evidence screenshots not be committed.
- Remove the tracked PNGs (unreferenced generated artifacts).
- Ignore pr-evidence/ alongside notes/artifacts/.
- Defensively exclude pr-evidence/ from the electron-builder app.asar
include surface so a stray local capture never bloats the bundle.
- Assert the new exclusion in electron-builder-config.test.mjs.
Co-authored-by: Orca <help@stably.ai>
* feat(linear): use Linear branch names for worktrees
* fix(linear): preserve branch overrides across composer resets
Normalize Linear branch metadata at the shared workspace-source boundary, restore it when repo changes preserve the issue, and clear it when another provider replaces or removes the link. Add regression coverage for each lifecycle transition.
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Shift+Enter still submitted instead of inserting a newline when Droid ran in
Git Bash on Windows, even after the PowerShell fix (#7620/#8065/#8427).
On Windows, Orca sends Droid the kitty CSI-u byte (ESC [ 13;2u) for Shift+Enter
only when the active pane has fresh, trusted process evidence that Droid is the
foreground app; otherwise it falls back to the Codex-compatible ESC+CR, which
Droid decodes as a plain Enter and submits. That trusted evidence comes from a
confirming foreground read.
PowerShell triggers that read from its OSC 133 command-start event. Git Bash
(and cmd.exe) emit no OSC command boundary, and an Orca-launched agent delivers
its command programmatically (no typed-command inference), so a fresh Droid
spawn in those shells never earned routing trust -- leaving Shift+Enter on the
ESC+CR fallback forever.
Fix: on a fresh spawn into a no-OSC shell, seed the SAME command-start
confirmation the manually-typed launch path already uses
(paneForegroundAgentTracker.onCommandStarted). Its bounded retry ladder spans
Droid boot, and -- crucially -- a miss publishes shellForeground:false, so the
pane stays recoverable by later focus/reveal samples. (A visible-pty sample was
rejected here: on a pathologically slow boot its known-agent shell-confirm would
latch, clear launch identity, and permanently poison Shift+Enter for the rest of
the session.) A real OSC 133;C, if it arrives, simply supersedes this.
Verified on Windows 11 + Git Bash with the real Droid CLI and node-pty ConPTY:
- Droid's TUI treats ESC [ 13;2u as a newline (two-line composer) and ESC+CR as
a submit -- exactly the reported bug and the fix's mechanism.
- droid.exe is a descendant of bash inside the ConPTY console process list (so
the main-side scan resolves it) and becomes a live process ~0.8s after launch,
well within the retry ladder.
Tests: regression test drives the real connectPanePty spawn path and asserts
csi-u; a slow-boot test exercises the shell->droid ladder transition; a
recoverability test proves a total-miss does NOT poison the pane (it fails
against the visible-pty approach and passes with onCommandStarted).
* fix(pi): suppress subagent agent_end so nested sessions don't fire false notifications
Pi/OMP run subagents as nested child processes that inherit the lead
pane's env (including ORCA_PANE_KEY), each loading its own copy of the
orca-agent-status.ts extension. A child's agent_end was attributed to the
lead pane, firing a false "agent finished" notification on every subagent
completion.
Gate status reporting to the top-level Pi process per pane via an
ORCA_PI_STATUS_OWNED env marker: the first process claims the pane and
sets the marker; any Pi it spawns inherits it and stays silent. Keyed on
process nesting (not hasUI) so a top-level non-interactive `pi -p` run
still reports its own completion.
* test(pi): assert nested-subagent silence for every guarded event, not just agent_end
* fix(pi): key status owner on pid so extension reload doesn't silence the lead
The subagent-suppression gate marked the pane owner with a boolean
ORCA_PI_STATUS_OWNED='1'. Pi re-invokes an extension's default export on
every in-process reload (/reload, live edit, settings reload), so the lead
process re-ran the factory, read the '1' it had set on first load, and
treated itself as a nested subagent — silencing all its own status
(working, tools, done, notifications) for the rest of the session.
Record the owning process's pid instead. A same-process reload matches its
own pid and keeps reporting; a spawned subagent inherits the lead's pid,
sees it differs from its own, and stays silent. Startup was unaffected
(bindExtensions runs the factory once), which is why the single-run trace
missed it.
Add a reload regression test that re-invokes the factory in the same
process and asserts the lead still reports; it fails against the boolean
marker.
Co-authored-by: Orca <help@stably.ai>
* fix(pi): gate nested status hooks at registration
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* Route new-tab link clicks to Orca tabs instead of popup windows
- Adds an isolated-world click/auxclick listener that relabels
browser-native new-tab intents (target=_blank, cmd/ctrl-click,
middle-click) with a private frame name, so setWindowOpenHandler can
distinguish them from opener-dependent window.open() calls without
breaking OAuth popups.
- Wires matched clicks through to a new browser:open-link-in-orca-tab
IPC payload carrying foreground/background activation intent, so the
renderer opens a worktree tab instead of a native window.
- Adds unit and e2e coverage for modifier/middle-click routing,
cancellation/rewrite handling, and cleanup on guest teardown.
* Route new-tab link clicks to Orca tabs and stop background popups
Plain target=_blank clicks (main frame and iframes) now navigate the
current Orca tab in place instead of opening a new browsing context,
while explicit new-tab gestures (Cmd/Ctrl-click, middle-click,
Shift+modifier) route into Orca tabs via one-use private frame names,
including child frames. Drops the foreground/background frame-name
split and the activate flag now that all routed links always open
active — no more silent background popups from a plain link click.
* fix(mobile): background-mount never-mounted terminals on subscribe so they don't render blank (STA-1840)
A mobile terminal.subscribe to a tab the desktop never mounted this session —
a workspace the desktop isn't currently showing, a cold-activation-deferred tab
(#8597), or a cold-parked tab — has no attached PTY, so the runtime has no
headless emulator and the initial snapshot is empty. The mobile terminal then
renders blank (repro: on the phone, scroll to an old workspace whose terminals
the desktop unmounted; they stay blank).
When a mobile subscribe hits an empty snapshot (or resolves no PTY), the runtime
now asks the renderer to background-mount that tab so the PTY attaches and the
live data stream — already subscribed on that path — delivers its output.
A never-mounted workspace has no renderer-graph leaf, so its terminals are
surfaced to mobile via synthetic pty:<ptyId> handles that carry no real UI
tabId. The runtime passes the ptyId through the mount request and the renderer
resolves the owning tab from the persisted tab model (resolveTerminalTabIdForPtyId),
falling back to a whole-worktree background mount if it cannot. The request is
idempotent for already-mounted tabs and never unmounts a live pane, so the worst
case is the same blank as before — no regression.
Validated on an iOS simulator with a two-workspace desktop: opening the
workspace the desktop was not showing rendered a fully blank terminal without
the fix and the live shell prompt with it.
Tests: runtime mount-request gating (real-tab / pty-form / no-ptyId / unknown),
subscribe-handler empty-snapshot-vs-content, and the ptyId->tabId resolver; all
608 RPC method tests pass.
* test(mobile): mock onRequestTerminalTabMount in useIpcEvents tests (fixes CI for #8811)
* fix(mobile): harden blank terminal recovery mounts
* perf(mobile): avoid redundant terminal recovery mounts
* fix(mobile): ignore stale terminal mount requests
* fix(mobile): skip recovery mounts for closed streams
* fix(mobile): reject ambiguous terminal mount ownership
* fix(mobile): preserve terminal handle through recovery mount
* fix(mobile): recover legacy terminal history safely
---------
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
* feat(git-status): batch, cancel, and cache git status polling to cut idl
- Add a single duty-cycled refresh scheduler (activity debounce + 60s
safety timer) replacing multiple overlapping intervals, so status
polling no longer runs near-continuously on large repos (#7983).
- Let safety refreshes reuse cached numstat line counts instead of
re-running diff --numstat every cycle, invalidated by head change,
known mutations, and a bounded TTL.
- Thread AbortSignal/request-token cancellation through IPC, RPC, and
relay layers so a superseded or backgrounded git:status call is
killed instead of finishing wastefully.
- Fix automatic upstream/status apply ordering so a slow, older
refresh can no longer clobber a newer result, and so an earlier
refresh still applies when a later one fails.
* Fix aborted git status scans being mistaken for completed empty results
- An aborted scan/numstat pass now always rejects instead of silently
resolving, so a cancelled request can't look like a valid empty status.
- Stop clearing the line-stats cache key on abort, since an aborted pass
never wrote to it — clearing was evicting a concurrent scan's healthy
snapshot and forcing a redundant numstat recompute.
* Fix aborted git status scans resolving as completed results
Cancelled scans could still resolve with partial or stale data instead of
rejecting, letting callers treat an aborted refresh as a valid status. Also
stop counting aborted scan duration toward catch-up refresh pacing, which
was stretching the next refresh interval by the full length of a cancelled
(often slow) scan.
* Add cancellable, generation-aware git status polling to cut stale scans
- Route git.status through an abortable subscription per requestToken so
cancelStatus can actually abort the remote scan instead of being a
no-op, preventing wasted work and stale responses overwriting fresher
state.
- Bump the git status polling generation on push-target changes so an
in-flight refresh against the old remote/branch can't apply stale
upstream data to the new one.
- Guard the stale-conflict poller against writes after unmount.
- Retire pre-purge line-stat scans in the cache so an older in-flight
scan can't repopulate a key after a token-scoped purge.
* fix(issues): replace cursor-based pagination with page-number Search API
Problem
=======
Issue pagination (#8649) had two bugs:
1. Pages 6-16 were unreachable — clicking page 16 highlighted page 5;
clicking 6/7 did nothing. The old cursor-based approach
(updated:<CURSOR) broke with Search API's relevance sorting —
pages after the first few returned no items even though more
issues existed.
2. Issue numbers appeared out of order on loaded pages (e.g. #1082
between #1308 and #1499), because client-side sort used
updatedAt instead of issue number.
Root Cause
==========
The pagination used two separate GitHub API strategies:
- Initial page 0 load: REST endpoints (repos/:owner/:repo/issues,
repos/:owner/:repo/pulls) sorted by updatedAt
- Subsequent pages: Search API with cursor (updated:<DATE)
These two sources returned items in different orders, causing items
to go missing or appear on wrong pages across page boundaries.
Solution
========
1. Unified on GitHub Search API for all pages — initial load and
pagination both use search/issues?q=...&page=N, eliminating the
REST-vs-Search inconsistency.
2. Changed from cursor-based (update:<DATE) to page-number-based
pagination (page=N), which the Search API supports natively.
3. Switched client-side sort from updatedAt to issue number
(sortWorkItemsByNumber), matching GitHub's default Issues view.
4. Parallelized page fetches in handleLoadNextPage — clicking page
16 now fetches all intermediate pages concurrently (~2s) instead
of sequentially (~30s).
5. Cleaned up dead legacy gh issue list / gh pr list code path,
extracted quoteForSearch helper, shortened overlong comments.
Files changed: 11 files, +140/-127 lines
Closes#8649
* chore: remove unrelated merge formatting
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
The remote CLI installer deleted the legacy orca.cmd bridge before it
confirmed csc.exe existed, so a minimal Windows host without the .NET
compiler lost its existing CLI command and got nothing back.
Move the legacy-shim removal to run only after the compile and the
launcher-existence guard both pass. Missing-compiler and compile-failure
paths now exit non-zero while leaving the existing orca.cmd untouched; a
successful upgrade still clears the unsafe %* bridge (orca.exe already
shadows orca.cmd via PATHEXT). No %* forwarding is restored.
Co-authored-by: Orca <help@stably.ai>
Move toggle heading commands (toggle-h1..h4) into their own "Toggle headings"
group, separate from plain heading commands, so the slash menu distinguishes
static headings from collapsible toggle headings. Also shorten toggle heading
labels from "Toggle Heading N" to "Toggle HN" for consistency with the plain
heading labels.
* fix(sidebar): map header drags to the nearest boundary slot instead of a dead zone
Fixes#8879
* fix(sidebar): bound header edge drops to measured content
The native-smoke job ran bare `pnpm vitest run <files>` with no --config.
There is no root-level vitest/vite config, so vitest fell back to its
built-in defaults: a 5000ms testTimeout and no Windows worker cap. The
real csc.exe launcher-compile tests (build-windows-cli-launcher and
ssh-remote-cli-launcher) cold-start in 1.4-7.4s on hosted Windows and
tipped over the 5s default, failing the PR check.
Pass --config config/vitest.config.ts like every other vitest invocation
in the repo (pnpm test, pr.yml) so the job gets the shared 30s timeout and
Windows maxWorkers:4 cap (less CPU contention -> lower compile variance).
Positional file args still filter to just the listed files.
* feat(mobile): edit saved host endpoints
* fix(mobile): reject ambiguous numeric host addresses
* fix(mobile): label edit host inputs
* fix(mobile): make host edit save atomic and remove superseded mutators
Two independent review rounds found the same class of foot-gun: a
superseded mutator (updateHostEndpoint, then renameHost) left in
host-store.ts after the atomic updateHostNameAndEndpoint refactor, with
zero remaining callers. Either could be reintroduced by a future caller
and silently regress the non-atomic name/endpoint race the atomic
function was written to close, so both are removed.
Also covers reconnect-rejection and endpoint-only save paths that were
missing test coverage, and merges origin/main (#8789) so this lands
without reverting the mobile terminal restore fix.
Co-authored-by: Orca <help@stably.ai>
* Simplify save-race comment and reword host-removed error message
- Trims the redundant comment explaining the savingRef race guard down
to one line.
- Changes the "no longer saved" load-error copy to "was removed" for
clearer phrasing, updating the matching test expectation.
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* fix: address pr-bug-scan validated finding from #6335
Replaced unguarded crypto.randomUUID() with the established createBrowserUuid() fallback in both markup shape-id call sites.
* fix(review): cover markup crypto fallback call sites with regression tests
PR #8804 replaced two unguarded crypto.randomUUID() calls with
createBrowserUuid(), matching the convention established in #5471, but
shipped without extending that PR's regression-test pattern to the new
call sites. Add hook-level tests for useMarkupPointerHandlers.onPointerDown
and useMarkupEditor.commitPendingText under a simulated non-secure-context
crypto global.
Co-authored-by: Orca <help@stably.ai>
* fix(review): consolidate annotation bridge token onto createBrowserUuid
BrowserPane had its own inline crypto.randomUUID guard with a
Date.now()+Math.random() fallback, duplicating the getRandomValues-based
fallback already centralized in createBrowserUuid() and used at every
other call site in the renderer.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: orca-bug-scan-bot <orca-bug-scan-bot@stably.ai>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* docs(readme): fix Korean translation issues
* docs(readme): align agent list with English README across translations
Replace OpenClaude with Pi in Spanish, Japanese, and Chinese READMEs
to match the English README's agent list (Codex, ClaudeCode, OpenCode, Pi).
Portuguese already had the correct list.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* Reopen closed tabs with Cmd+Shift+T, including terminals
Cmd/Ctrl+Shift+T now reopens the most recently closed tab of any kind
(terminal, browser, or editor) in true recency order, walking back
through close history on repeated presses — Chrome/Ghostty semantics.
Terminal tabs previously had no reopen at all, and the shortcut picked
browser-vs-editor by focused surface instead of recency.
Terminal closes capture a snapshot (startupCwd, shellOverride,
customTitle, color) on explicit user closes only; PTY exits, launch
cleanup, onboarding, and floating-panel closes are excluded. Reopen
spawns a fresh shell through the same createTab path as the "+" button.
A per-worktree cross-type kind list (new recently-closed-tabs slice)
orders the three existing per-type stacks; both new maps are purged on
worktree removal and re-keyed on rename with startupCwd prefix remap.
createNewTerminalTab moved to terminal-tab-create.ts to keep
terminal-tab-actions.ts under the max-lines budget.
Requested via Discord feedback; STA-1287.
Co-authored-by: Orca <help@stably.ai>
* Update close-tab assertions for captureRecentlyClosed flag
Floating-panel, launch-failure, and stale-agent-tab closes now thread
{ captureRecentlyClosed: false } so system-initiated closes don't pollute
the reopen stack. Update the affected test expectations to match the new
call shape.
Co-authored-by: Orca <help@stably.ai>
* fix(reopen): skip local terminal reopen on remote-runtime worktrees
reopenClosedTerminalTab recreated terminals via a raw local createTab
without the isWebRuntimeSessionActive guard every sibling terminal-create
path uses, so Cmd+Shift+T on a remote-runtime-owned worktree (paired-web
or desktop-attached headless-serve) spawned an unbacked local phantom tab
not tied to the host session. Guard at reopen time via the cycle-free
getRuntimeEnvironmentIdForWorktree — importing web-runtime-session into a
store slice creates a store-index init cycle. Remote terminal reopen stays
deferred; the cross-type dispatcher falls through to browser/editor.
Adds a regression test and corrects an inaccurate removal-path comment.
* fix(test): seed runtime host id for remote-reopen guard test
The prior test set settings.activeRuntimeEnvironmentId by spreading the
store's settings, which widened workspaceDir to string|undefined and broke
`pnpm typecheck` (GlobalSettings requires string). Seed the worktree with a
`runtime:` hostId instead — getRuntimeEnvironmentIdForWorktree reads it
directly, so no settings spread is needed.
* Remove unrelated formatter changes
---------
Co-authored-by: Orca <help@stably.ai>
* Fix PR checks sticking to a stale linked PR after a terminal branch switch
A worktree's linked PR is a branch-scoped hint, but two refresh paths race
when a terminal switches branches: the git-status identity path clears
branch-scoped review links, while the worktree-listing path rehydrates the
new branch together with the stale persisted link and clears nothing. When
the listing lands first (the common case — worktree listing is much faster
than git status), the identity path sees no branch change and the stale
link survives. Every subsequent refresh then re-fetches the linked PR by
exact number, which ignores the branch, so Checks stays pinned to the old
branch's PR and the Refresh button cannot recover.
Two-part fix:
- Prevention: listing refreshes now route observed branch switches through
updateWorktreeGitIdentity before merging, so the existing link clear and
tombstone machinery runs no matter which refresh path wins. Gated on the
entry still carrying branch-scoped review context so a stale listing row
cannot roll back a newer branch identity.
- Recovery: PRInfo now carries headRefName, and a fetch that returns the
linked OPEN PR whose head branch matches neither the current branch, the
worktree push target, nor the worktree HEAD clears the durable link and
re-resolves by branch. Wired into both fetchPRForBranch and the
background refresh coordinator, mirroring the merged-PR divergence clear.
This also heals wedged workspaces persisted by earlier builds.
* Harden stale PR recovery across refresh races
* Avoid duplicate PR recovery refresh work
* Index linked PR refresh aliases once
* fix(worktrees): stop surfacing prunable git worktrees as live workspaces
A worktree still registered in git but whose directory was deleted
(git's `prunable` state) was enumerated as a normal workspace,
producing repeated pty:spawn DaemonProtocolError / fs:readDir ENOENT
loops and a blank pane.
- Parse the `prunable` porcelain field (Git >= 2.36) in both the main
and relay worktree-list parsers.
- For Git < 2.36 (no `prunable` field), probe each linked worktree
path for existence on the fallback line-block path, skipping locked
registrations to mirror git's own prunable rules.
- Omit prunable worktrees from the detected-workspace enumeration only;
removal/cleanup flows keep seeing them.
- Extend the real-binary compatibility contract with the 2.36
`prunable` boundary.
Fixes#8389
Claude-Session: https://claude.ai/code/session_018Rg1Bpq4GGwmz613hq6RSD
* fix(worktrees): pin the prunable/locked porcelain annotations to their real Git 2.31 boundary
The prunable and locked annotations landed in Git 2.31, five releases
before `worktree list -z` (2.36); only -z defines the capability
fallback boundary. Correct the compatibility contract so a future
matrix entry in the 2.31-2.35 range passes, and reword the fallback
comments: on 2.31-2.35 the annotations still parse and the existence
probe is a backstop; only Git <2.31 relies on it outright.
* fix(worktrees): omit prunable registrations from the Space scan
A prunable registration has no directory to size or reclaim, so Space
rendered it as a dead "Missing" row whose checkbox stayed disabled with
no prune/remove affordance (reported on macOS after a reboot cleared
/private/tmp under 16 registrations). Skip prunable entries in the scan,
matching the workspace enumeration; removal flows list worktrees
separately and still see them.
---------
Co-authored-by: kaynan <kaynan.camargo@terceiro-sky.com.br>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(runtime): retain watcher and PTY teardown ownership
* fix(runtime): restore watchers after interrupted cleanup
* fix(runtime): prevent stale watcher revival
* test(runtime): cover watcher shutdown ownership
* test(daemon): model physical PTY exit
* fix(daemon): keep shutdown terminating when disposal cannot prove exit
A rejecting host.dispose() (unreapable child past its exit deadline) left
the shutdown RPC without its process.nextTick(shutdown) and skipped socket
cleanup in shutdown(), stranding the daemon as an unreachable orphan after
the stale-daemon replacement flow unlinks its socket. Log and continue:
daemon exit reparents the child to init instead of blocking on it.
* fix(runtime): keep local watching alive after an idle-kill deadline miss
An idle child that outlived the exit deadline set shutdownRequested on the
shared desktop supervisor, which has no retire-and-replace path — every
later subscribe rejected supervisor_disposed and the roots were cached
unwatchable, silently ending local file watching for the session. The idle
path owns zero records, so there is no double-watch hazard; the zombie
keeps its capacity reservation until physical exit and the next subscribe
gets a fresh child.
* fix(renderer): resync replayed paired-web file watches
Transparent replay removed the implicit resync the old close-and-rebuild
path provided: a replayed files.watch only reports changes from its own
native setup, so changes during the reconnect gap were silently lost.
Deliver a conservative overflow to consumers once the replayed watch is
ready, matching the overflow-after-interruption contract everywhere else.
* fix(runtime): address teardown review findings
* fix(runtime): retry watches after teardown deadlines
* Fix PTY descendant leaks on forced teardown
* Fix jitter-sensitive terminal lifecycle test
* fix(crash-reporting): coalesce repeated renderer error breadcrumbs
The crash-breadcrumb ring holds only 30 entries, and renderer breadcrumbs
arrived via the plain uncoalesced path. A repeating renderer error — like the
ResizeObserver and SSH-rejection storms in #8260 (50+ identical events) —
flushes the entire ring in seconds, erasing the pre-crash trail exactly when a
crash report is about to snapshot it.
Route renderer_error and renderer_unhandled_rejection breadcrumbs through the
existing recordCoalescedCrashBreadcrumb, keyed by name plus message prefix
with a 30s window. Repeats collapse into one entry carrying
suppressedSinceLast, so a storm shows up as "error X fired N times" alongside
29 slots of surrounding context instead of 30 identical copies.
Other breadcrumb names keep the plain path; telemetry trace spans are
unchanged. Renderer breadcrumb routing tests move to a focused
crash-reporting-renderer-breadcrumbs.test.ts (the main suite is at the
max-lines ceiling).
* fix(crash-reporting): coalesce durable renderer traces
* fix: harden renderer crash guards
* fix(crash-reporting): preserve distinct error sources
* fix(ssh): AND lsof selectors so relay reset can't kill unrelated processes
`forceStopRelayForTarget` resolved PIDs with `lsof -t -U "$sock"`.
lsof ORs its selectors by default, so this selects every process holding
ANY unix socket in addition to the socket's owner. On hosts where lsof
cannot match AF_UNIX sockets by path, the path term matches nothing and
the sweep degrades to all unix-socket holders — including systemd --user
— which the TERM+KILL loop then takes down (#8762).
Add `-a` to AND the unix-socket and path selectors: where lsof supports
path matching the result is exactly the socket's holders (verified: 1
PID with -a vs 97 without on macOS); where it doesn't, the result is
empty and the existing pgrep fallback — scoped to command lines that
reference the relay's per-instance socket name — takes over.
Fixes#8762
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* review: add relay reset process-safety coverage
Exercise the generated POSIX reset script with controlled lsof and pgrep commands plus a mocked kill, and keep the selector rationale concise.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
* fix(native-chat): deliver AskUserQuestion answer by option number, not label
Claude Code's AskUserQuestion is an arrow-navigate selector: a bare Enter
commits the HIGHLIGHTED default (the first option), and pasted label text
does not move the highlight. Native chat delivered the answer as the option
LABEL text + Enter, so every non-first pick was silently committed as the
first option (STA-1860 Bug A).
Deliver the answer by each option's stable 1-based number instead (the marker
the selector commits on), paced so each keystroke lands after the selector
step renders. Covers single/multi-select, multi-question, and free-text
('Type something') answers. Verified live against Claude Code 2.1.210.
- native-chat-interactive-prompt.ts: buildAskAnswerKeys / AskAnswerSelection /
AskAnswerKeyGroup / hasAskAnswer; formatAskAnswer kept (index-based) for the
non-Claude label path
- native-chat-runtime-send.ts: sendNativeChatAskAnswer (paced keystroke groups)
replaces the old label-line sender
- card + interactive hook thread structured { indices, other } selections
STA-1860
* fix(native-chat): harden AskUserQuestion answer lifecycle
* fix(native-chat): stop stale paced answer writes
* fix(native-chat): cancel paced answers during commit
Cancel stale AskUserQuestion timers before replacement UI can commit, and localize the multi-step sending state in every shipped locale.
* fix(gitea): share and cache the /pulls scan so card refreshes can't hammer a self-hosted forge
Each worktree card resolved its branch by paginating the repo's full
/pulls listing (state=all, up to 5 pages) independently, with a 5s
timeout. Gitea/Forgejo have no head-branch filter, self-hosted Forgejo
takes ~5s per page (it loads reviewer data per PR), and a push event
refreshes every card at once — so one repo turned into hundreds of
near-simultaneous requests whose responses were aborted right as they
completed, and the burst OOM-killed a 512Mi Forgejo pod (#8807).
- Share one in-flight /pulls scan per repo across concurrent branch
lookups and cache the result for 30s, so a burst costs a single page
walk instead of one per worktree.
- Raise the list-scan timeout to 15s so slow-but-successful pages are
used instead of discarded and retried.
- Invalidate the cached scan after Orca itself creates a PR (and before
the post-create fallback lookup) so the new PR is seen immediately.
Fixes#8807
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(gitea): don't let an in-flight scan re-cache a listing from before an invalidation
Review feedback: if a PR is created while a card-refresh scan is in
flight, the invalidation cleared the cache but the scan then finished
and re-cached the pre-create listing, hiding the new PR for a full TTL.
Track a per-repo generation: invalidation bumps it (and drops the
in-flight promise), and a scan only writes the cache when its generation
is still current.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(gitea): bound PR scan cache and retry failures
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
* Preserve original markdown style when editing in the rich editor
The rich markdown editor re-serialized the whole document into a canonical
style on the first edit, so a one-character change rewrote every non-canonical
construct (`_x_`->`*x*`, `__x__`->`**x**`, `* item`->`- item`, dropped trailing
newline) and produced a large, unexpected diff.
Reconcile the canonical getMarkdown() output back toward the original source
bytes at every disk-bound serialize site (debounced autosave, flush, Cmd/Ctrl+S)
so untouched regions keep their markup and only the edited region changes. A
divergent-base fuzzy patch (diff-match-patch) carries the user's edit onto the
original style; a safety re-parse requires the result to render-equal what the
editor shows, else it falls back to today's canonical output — so content
semantics can never change. The rich editor stays the default for markdown.
Fixes#6080
* Bound markdown reconciliation diff work
* fix(terminal): kill agent descendant processes on session teardown (STA-1800)
Agent CLIs spawn tool children in detached process groups that PTY
SIGHUP can never reach. Killing an agent session (tab close, retire,
sleep) left those children running as orphans — eight orphaned git
processes burned ~8 cores for up to 11.5h under the agents-running
keep-awake and drained a battery to 8%.
New pty-descendant-termination module: snapshot the ppid tree BEFORE
signalling (a dead root's descendants reparent to pid 1 and become
unfindable), SIGTERM the root group and every descendant, then after a
2s grace SIGKILL survivors gated on a pid+start-time identity re-check
so a recycled pid is never signalled. Snapshot is bounded and never
rejects; failures degrade to today's shell-only kill.
Wired for agent sessions only (plain terminals keep nohup semantics) at
all three POSIX kill sites: local provider shutdown, daemon
TerminalHost immediate kill (the pty:kill path — force-kill bypassed
Session.kill entirely), and daemon Session graceful kill.
Verified live in the built app: an agent pane with a detached-pgid
child; the child survived on the unwired build (three control runs) and
dies within ~5s with the fix. Windows ConPTY and SSH-hosted PTYs keep
the previous foreground-tree contract (documented follow-ups).
* fix(terminal): harden descendant teardown
* fix(terminal): require fresh process snapshots
* fix(terminal): close descendant teardown races
* refactor(terminal): preserve teardown line budget
* fix(terminal): keep descendant teardown fresh and identity-safe
* docs(reliability): record integrated descendant E2E
* fix(terminal): bound descendant teardown work
* fix(terminal): share descendant snapshot indexes
* docs(reliability): record descendant review evidence
* revert: remove speculative descendant hardening
* fix: close Add Project host selector when an SSH credential prompt appears
The SSH password/passphrase dialog (z-50) renders below popovers (z-60),
so a host selector left open during a connect attempt covered the
credential prompt and its input field. Close the selector as soon as a
credential request is queued so the modal prompt is visible and usable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: raise SSH credential prompt above popovers instead of closing the host selector
The credential prompt is a preemptive global modal, but DialogContent's
default z-50 sits below popovers (z-60) and menus (z-70), so any surface
left open while a prompt fires covers the focused password input — the
Add Project host selector was one reproducible case (#8490).
Raise the prompt's overlay/content to the frontmost tier (!z-[140] /
!z-[150], matching link-routing-preference-dialog) so it renders above
every popover/menu, and drop the host-selector close effect: the picker
can stay open and is available again after cancel for an immediate retry.
---------
Co-authored-by: jonghoonlee <jonghoonlee@inocras.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(agent-status): reap finished Claude named agents/teammates from the sidebar roster
#8522 stopped one-shot subagents from squatting as idle rows, but named
background agents (Workflow/orchestration/ultracode lanes and agent-teams
teammates) still piled up permanent "Idle - <type>" child rows for the rest
of the session — the reported regression (11 idle rows under an
"Orchestration Messages" pane, all idle 3-6h after finishing).
Root cause, confirmed against live hook captures (claude 2.1.210): named
agents get teammate-shaped ids (a<name>-<hex>) AND now appear in Stop's
`background_tasks` as `type: "teammate"` entries whose status stays "running"
forever — even after the agent finished. The old code read that shape as a
"resumable teammate", so SubagentStop only marked it idle and the fold never
reaped it (a present teammate task kept hasTeammateTypedTask true). The rows
never left.
Fix: the roster now tracks only WORKING children.
- SubagentStop removes the child outright (teammate-shaped or not) — it is
the reliable finish signal; the teammate task's "running" status is not.
- TeammateIdle removes by name as the fallback when a SubagentStop is lost.
- A lead Stop's background_tasks still reaps unlisted children: hyphen-free
one-shots always, and teammate-shaped rows once a complete inventory shows
no teammate-typed task at all. A live named agent whose id never appears is
kept only while a teammate-typed task is still present (the done-gate).
- Hydration drops persisted idle snapshots so a restart can't re-pile them.
Verified live in a dev Electron instance driving a real Claude TUI that spawns
four named background agents: pre-fix the pane resolves to done with four
persistent "Idle - <probe>" rows; post-fix each row disappears the instant its
agent finishes and the roster drains to empty (done, zero child rows).
Tests: roster + row-lifecycle + hook-listener suites rewritten to the
working-only semantics, grounded in the captured 2.1.210 hook stream
(126 passing). Typecheck + oxlint clean.
* fix(agent-status): prune persisted idle Claude children
* fix(agent-status): persist Claude idle-row hydration cleanup
* fix(agent-status): avoid ambiguous teammate idle cleanup
* fix(agent-status): reconcile replacement children at roster cap
Reconstruct remote POSIX commands with bounded printf arguments so non-POSIX SSH login shells can forward them without requiring remote base64. Preserve relay and system-SSH stdin, and centralize login-shell flag selection for csh/tcsh compatibility.\n\nValidated against real csh and tcsh OpenSSH targets with built-in and system SSH, including cold relay deployment, stdin upload, PTY I/O, file mutation, and reconnect.