* Add version-matched bundled skill guides
* Clarify skill freshness rollout PRs
* Add canonical skills show alias
* fix(skills): address guide review feedback
* fix(skills): make guide commands cross-platform
* fix(skills): apply the ORCA convention to the emulator guides
Review follow-up: the emulator guides still instructed literal
`orca emulator ...` in sh fences with no Linux disambiguation, so on
unmanaged Linux they could launch the GNOME screen reader — the exact
failure the executable-selection preamble prevents. Both emulator
guides now carry the preamble and ORCA placeholder across fences,
tables, and prose, and the cross-platform safety test covers all four
converted guides. Also replaces computer-use's "unless a block names a
shell" carve-out, which contradicted its own POSIX example, with the
unconditional placeholder rule.
The shell-ready zsh wrapper restored ZDOTDIR from the env-imported
`${ZDOTDIR}`. On Windows+WSL the wrappers are generated with a Windows
path baked in but sourced via /mnt/c, and for non-ASCII Windows
usernames (e.g. a Korean login) zsh corrupts environment values whose
UTF-8 bytes fall in its 0x84-0x9D token range while processing startup
files. The corrupted `${ZDOTDIR}` failed the self-check, so the wrapper
fell back to the unusable baked Windows literal and the user's ~/.zshrc
never loaded — a bare `HOSTNAME%` prompt with no theme/aliases/PATH.
Derive the wrapper dir from `${${(%):-%x}:h}` instead — %x is zsh's
internal script name for the file being sourced and is not subject to
the env-import corruption. `${ZDOTDIR}` is kept only as a fallback when
%x expansion yields nothing; the existing final restore still validates
with -f before trusting the value. On native macOS/Linux the derived
value equals the old one, so behavior there is unchanged.
Covers local PTYs, the daemon, and the Windows→WSL launch path, which
all share getZshEnvTemplate. Adds a live-zsh regression test that sources
the wrappers from a non-ASCII (token-range) runtime path.
Supersedes the ORCA_ORIG_ZDOTDIR path-normalization approach with a wrapper self-location fix that also covers the non-ASCII (token-range) corruption trigger.
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
* fix(daemon): replace a permanently wedged daemon instead of preserving it forever (#8689)
A daemon whose socket accepts connections but whose event loop never answers the
'hello' handshake was adopted by the launcher unconditionally and never re-evaluated,
so every terminal spawn failed with 'DaemonProtocolError: Hello response timed out'
with no recovery.
- daemon-init.ts: bound the launcher's 'preserve any unresponsive-but-connectable
daemon' with a grace window. A transient wedge (Windows update-relaunch AV/disk
pressure) drains within ~20s and is preserved WITH its live sessions; a permanent
wedge exhausts the grace and is replaced. Stays well under the 60s local-PTY
fail-open cap.
- daemon-pty-adapter.ts: isDaemonGoneError now treats 'Hello response timed out' as
daemon-gone, so a runtime wedge triggers withDaemonRetry's respawn (re-entering the
same grace-bounded launcher) instead of failing every spawn until app restart.
Tests pin the grace magnitude so it cannot be silently shrunk.
Co-authored-by: Orca <help@stably.ai>
* fix(daemon): widen wedged-daemon grace window to ~60s
Bump WEDGED_DAEMON_GRACE_RETRIES 3 -> 11 (~20s -> ~60s) to keep live-session
loss on the transient-wedge (Windows update-relaunch) path as close to zero as
possible. A transient wedge drains early and stays under the 60s fail-open cap;
only a permanent wedge runs the full window. Export the constant and pin its
floor in tests so it can't be silently shrunk.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(mobile): re-fit terminal PTY when the frame height settles
A freshly-created agent terminal fits its PTY to rows = floor(frameHeight
/ cellHeight) before the accessory/live-input dock has laid out, so the
frame is briefly too tall and the PTY gets too many rows. Claude/Codex
pin their input box to the bottom of the grid, so those extra bottom rows
— the input box and status lines — render behind the dock and you can't
see what you're typing. Leaving and re-entering the workspace worked
around it by re-measuring against the settled layout.
The refit hook previously re-fit only on width changes and deliberately
ignored height-only changes, so the over-fit was never corrected. Track
the measured frame height and re-fit on its change too, mirroring the
width path. Safe because Expo SDK 55's edge-to-edge IME overlays instead
of resizing, so the frame height doesn't change on keyboard toggle and
the PTY is never reflowed while typing; the refit's row-count guard makes
sub-row jitter a no-op.
* fix(mobile): guard height refit against IME resize; test the decision
Address review on #8647:
- Extract shouldRefitOnFrameHeightChange (pure) and gate the height refit on
keyboard-visible, so an IME that resizes the window (Android adjustResize)
can never reflow the PTY while typing — no longer relies on the edge-to-edge
no-resize assumption alone.
- Add a behavioral test for the decision helper (height transition, same-value
no-op, keyboard-open skip) instead of only source-string assertions.
- Trim the added comments to 1-2 lines per AGENTS.md.
Take-over of #8605 (issue #8591). Ships #8498 (worktree resync + pull-to-refresh + cache write-through) and #8129 (idempotent notification replay on reconnect). Fixes the original PR's field mismatch (seq vs notificationSeq) and adds the missing notifications.getMissedSince mobile RPC allowlist entry. #6784 and #4500 held back to avoid conflicting with the relay work (#8536). Co-authored-by: Brandon Bennett (@branben).
fix(sidebar): guard kanban worktree sort against undefined displayName (crash 99657ab1)
A worktree reached the sidebar with an undefined displayName, so the
Manual-sort comparator ran `a.displayName.localeCompare(...)` and threw
`TypeError: Cannot read properties of undefined (reading localeCompare)`,
unmounting the sidebar.worktrees error boundary.
Route both kanban comparators through a null-safe compareDisplayName so a
missing name only affects tie-break order instead of crashing. Behavior is
unchanged when displayName is present.
Adds regression tests for both the manual and recent sort paths.
@
#8651 added 'base_not_on_remote' to HostedReviewCreationBlockedReason but
did not add a matching case to getMobilePrCreateBlockMessage, leaving the
switch non-exhaustive so the function can fall through and implicitly
return undefined against its string | null type. That fails `tsc` on main
(verify does not run on direct pushes, so it surfaced on PR merge commits).
Add the missing case with actionable copy consistent with the desktop
create-time message.
* fix(github): pin work-item list ordering to updated-desc so cursor pagination reaches every page
The Tasks page paginates work items with an updatedAt cursor
(updated:<oldest-item), but the underlying gh calls never pinned a sort:
'gh issue list' defaults to created-desc and '--search' defaults to
best-match. Items created long ago but updated recently therefore never
appeared on any page — page 0 (created order) skipped them and every
later page excluded them via the cursor — so the pager advertised pages
the fetch chain could never reach, clicks on them clamped to the last
real page, and cross-page ordering was scrambled.
Append sort:updated-desc to every list/search invocation so the fetch
order matches the cursor field on the first and all subsequent pages.
Verified against a live 588-issue repo: the cursor chain previously
died around page 5; it now traverses 585/588 unique issues (the
remainder is the pre-existing strict '<' boundary edge for items
sharing the cursor's exact timestamp).
Fixes#8649
* fix(github): make work-item cursor pagination lossless at updatedAt boundaries
Builds on the sort-pin fix: switch the pagination cursor from strict
'updated:<' to inclusive 'updated:<=' so items sharing the boundary row's
exact updatedAt are no longer skipped between pages (the residual 3/588 edge
in #8649).
The inclusive bound re-fetches the boundary rows, so dedupe them by repoId+id
(a bare item.id like 'issue:9' collides across repos). Extract the page
accumulation out of the 12k-line TaskPage component into a pure, unit-tested
helper (accumulateWorkItemPages) that dedupes and backfills: it accumulates
fresh rows across fetches and emits uniform pageSize pages, so deduped pages
never shrink below the size totalPages (count / effectivePageSize) assumes —
which would otherwise strand the tail items and break the no-count degraded
pager.
Also hoist the updated-desc ordering into a named WORK_ITEM_LIST_SORT_QUALIFIER
constant so the cursor's ordering contract has one home.
Tradeoff: when per-repo fetch size equals pageSize, the boundary dedupe costs
one extra fetch per page; acceptable for interactive pagination and bounded by
the gh rate-limit guard. Persisting the cursor/buffer across calls is a
possible follow-up.
---------
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
Follow-up to the pane self-heal: its input-undeliverable detector is
gated off while a transport connect is settling (correct — remounting
mid-reattach used to kill the resolving shell), but the gate had no
timeout. A connect that never settles (the SSH RPC-timeout class) left
the one state recovery still could not reach: no output flows, so no
other detector fires, and every keystroke is silently dropped forever.
Two changes that only make sense together:
- the gate becomes a 60s settle grace; past it, undeliverable input may
request recovery again (still pty:hasPty-confirmed).
- the transport's destroyed-while-connecting check now kills only what
the connect CREATED. A fresh spawn is ours to reap; a reattach
(sessionId) resolves to a pre-existing session owned by the tab
lifecycle — tab close kills it by id through its own path, so killing
it here turned a remount racing a slow reattach into the loss of a
live shell. This is what makes the grace safe: recovery firing at 60s
against a connect that resolves at 61s now costs a wasted view
rebuild, not the user's session.
Verified: transport unit test pins the reattach/fresh-spawn kill split;
the recovery e2e pair passes 6/6 consecutive iterations with this
change (an earlier flaky signal bisected to machine load: failures only
under parallel-build load, two unrelated failure modes, 4/4 then 6/6
green on quiet hardware with and without the change re-applied).
* fix(mobile): host Create Workspace drawers in one Modal so dropdowns open
The Create Workspace form and each of its pickers (repository, agent,
source, trust) rendered their own native Modal. Opening a dropdown
dismissed the form's Modal and presented the picker's Modal in the same
beat, but iOS cannot present one native modal while another is being
dismissed, so the incoming picker was dropped and the form collapsed —
the repository and agent dropdowns did nothing. On slower real devices
the race is lost every time; on the simulator it was intermittent.
Host the whole flow in a single persistent native Modal via a new
BottomDrawerModalHost + React context. When a BottomDrawer detects it is
inside the host it renders its sheet without its own Modal, so switching
from the form to a picker is an in-window view swap instead of a native
modal present/dismiss — no race. Every BottomDrawer outside this flow is
unchanged (the context defaults to off), and the picker wrappers need no
changes because the context propagates through them.
* fix(mobile): route native back through the setup-trust guard
The BottomDrawerModalHost onRequestClose sent every non-form view through
transitionDrawer('form'). For the setup-trust prompt that bypassed
closeSetupTrust(), whose guard keeps the prompt up while a trust decision
or workspace creation is in flight and which clears setupTrustPrompt.
Route drawerView === 'trust' back presses through closeSetupTrust() so
the guard and cleanup run.
* fix(terminal): self-heal panes whose renderer dies while the PTY stays alive
A pane's xterm write pipeline can die while its shell keeps running: a
synchronous throw escaping an unguarded write callback wedges WriteBuffer
(issue #2836), and write() on a disposed terminal silently drops its
completion callback (verified against vendored xterm 6.1.0-beta.287 — it
does NOT throw, falsifying the output scheduler's disposed-race catch).
Every recovery path we have (dead-session reconcile #6514/#7002,
hibernation wake #7145, the allDead activation generation bump) gates on
the PTY being dead, so these panes stayed fossils: last frame painted,
every keystroke and byte of output silently dropped, delivery ack credits
leaking, until the user reloaded the window (issue #8104 class).
Detection is probe-certified, mirroring replay-guard.ts:
- a scheduler write whose completion stalls gets an empty probe write; a
probe that never parses certifies the pipeline dead (catches both the
wedged WriteBuffer and the disposed-terminal case) and credits the
queued deliveries so main's in-flight window no longer leaks
- the replay guard's existing wedged release ("pane likely needs
recovery") now actually hands the pane to recovery
- user input rejected by an unbound transport (detached during a
remount/move and never rebound) arms recovery after confirming the PTY
is alive via pty:hasPty
Recovery reuses the proven remount seam: bump the tab's generation so
TerminalPane unmounts, detach() preserves the live PTY, and the remounted
pane builds a fresh xterm that reattaches and replays the daemon
snapshot — no shell restart, capped per tab to prevent remount storms.
The new e2e spec pins both phenotypes end-to-end (wedge → recover,
dispose-under-live-bindings → recover); both fail on main and pass with
the fix, and the same arc was validated live in a pnpm dev instance.
* fix(terminal): make pane recovery strictly best-effort in timer contexts
Recovery fires from stall-watch timers, replay-guard releases, and onData —
contexts where a throw becomes an unhandled error (CI verify caught this:
pty-connection.test.ts mocks a partial store, other tests advance fake
timers past the stall window, and the certification path hit a missing
remountTerminalTabForRecovery). Guard the store action call and the
ptyIdsByTabId reads so a partial surface yields a false return, never a
throw, and pin it with a regression test.
* fix(terminal): guard pane recovery against in-flight reattach and remote liveness blind spots
Review findings on the self-heal (adversarial pass):
1. HIGH: typing during an in-flight connect/reattach (startup restore,
app-SSH) hits sendInput while the transport is legitimately unbound; an
input-undeliverable remount there destroys the unbound transport (no
ptyId yet, so unmount cannot detach), and pty-transport's destroyed
check then kills the PTY the resolving reattach returns — the live
shell recovery exists to preserve. Gate the input detector on a
transport-connect-in-flight flag (set around all three connect sites)
and on disposed, so "not deliverable YET" never remounts. The fossil
case (detached and never rebound) has no pending connect and still
recovers.
2. pty:hasPty answers null for ids the local registry does not own, which
made the liveness gate inert for remote panes: a disconnected remote
runtime would remount-churn on every cooldown window while typing.
Remote panes (connection-tagged or remote:-prefixed) now require an
authoritative true; local panes keep the lenient null-proceeds gate.
Flagged for follow-up, not changed here: pty-transport's destroyed check
kills reattached sessions without discriminating isReattach — a
pre-existing hazard that tab-close covers by killing per id anyway.
* fix(terminal): keep certification throw-proof end to end
Guard the two remaining throw paths in the certification chain — the
entry-discard callback and the recovery handler — so nothing can escape a
timer as an unhandled error, and a throwing discard cannot suppress the
recovery notification it exists to precede. Pinned by two new tests.
* fix(terminal): breadcrumb swallowed recovery failures
A store-action throw in recovery returns false without consuming budget,
so the detector retries each cooldown — an invisible loop unless it
leaves a trace. Breadcrumb it (the recorder is self-guarded and cannot
throw where recovery runs). Also invoke transport.isConnected optionally
so partial test transports fail the gate quietly instead of logging a
contained TypeError.
* fix(terminal): end the zombie-pane replay loop at its root
Root-caused the production "wedged release drip" (1,302 breadcrumbs in one
day on one machine, 4-write bursts on a fixed timer phase, idle-required):
once a pane's xterm pipeline dies while its connection lives, the delivery
watchdog's heal (60s cooldown, fires only while idle because a dead xterm
never ACKs its in-flight bytes) re-delivers restore markers, the hidden
output restore replays 3-4 chunks into the dead parser, each write arms a
replay guard destined for another wedged release — and nothing ever
learns. The loop runs forever and re-forms after app restart.
Three fixes so the loop learns:
- replayIntoTerminal/Async short-circuit on a probe-certified dead
pipeline: no more futile writes, so no more guard drips, and awaited
restore chains resolve instead of hanging.
- requestHiddenOutputRestoreIfNeeded is gated the same way, so the
watchdog heal stops refetching snapshots for a pane recovery owns.
- a window-cap recovery decline now schedules one retry for when the
budget window reopens (deduped per tab, cancelled by any successful
remount). Without it, the certified-dead latch plus the new write
silence made a capped pane a permanent zombie: nothing would ever
re-request recovery. Cooldown declines deliberately do not retry —
remounts are tab-scoped, so the just-made remount already replaced
every pane's xterm in the tab.
Still open (tracked separately): the deterministic wedge surface that
creates the dead pipeline on the release build in the first place — an
unguarded, unreported parse-path throw or silent disposed-write; zero
guard breadcrumbs fired all day, so the trigger predates the guards'
coverage.
* feat(terminal): name the silent zombie producer in breadcrumbs
The last unproven link in the zombie-pane chain is HOW a pane's xterm
dies on the release build. Field discriminators eliminated every
reporting channel: zero terminal guard breadcrumbs and zero xterm-stack
renderer_error/unhandled_rejection events across days of logs, while the
drip re-formed after a clean app restart. Every content-triggered wedge
surface would have reported; the only fully silent mechanism left is a
restore write into an already-disposed xterm instance (write() drops its
completion callback without a throw — verified against the vendored
6.1.0-beta.287).
Instrument that exact moment: a version-pinned disposal probe (its test
runs against the real vendored build so an upgrade that moves the private
field fails loudly), a terminal_restore_write_target_disposed breadcrumb
where startup scrollback restore would write into a disposed instance,
and a terminal_restore_write_failed breadcrumb replacing the fully silent
restore catch. The next zombie formation logs its own root cause.
The unread-activity bell landed with a hardcoded aria-label, and the
localization-coverage gate now fails on main itself (verify does not run
on pushes to main, so the break landed silently and blocks every PR's
CI). Route it through translate() with catalog entries for all five
locales.
* Redesign native chat interactive UI to match assistant-turn styling
- Restyle approval and question cards as bordered cards docked in the
composer region instead of top-border strips, and widen the chat
column (max-w-3xl → max-w-4xl) to match the rest of the app
- Rework the question card into a numbered pick-list with an always-
present free-text row and explicit Next/Skip/Send action, replacing
the prior "Other…" toggle flow
- Hide the composer while a question card is active since it supplies
its own answer input, and hold the card open until the paced answer
send settles instead of dismissing on click
- Auto-repin the message list to the bottom during in-place streaming
growth via a ResizeObserver on the content, not just the viewport
- Widen question-answer pacing (800ms→1000ms step, 300ms→500ms buffer)
for reliability on slower machines / higher-latency SSH sessions
- Restyle tool-run/tool-line disclosure to a right-side hover chevron
and add a formatted (pretty-printed) detail view for diff-less calls
- Add "Skip" i18n string across all locales
* Extend paste bridge and question-card fixes for native chat interactive
- Route pane-level Paste to the question card's free-text input when
the composer is unmounted, so Cmd/Ctrl+V keeps working while a
question prompt is showing.
- Fix a stale-timer bug where a new interactive prompt could inherit
the previous card's dismiss/submitting state and swallow its first
answer.
- Only render tool-run detail when there's actually content to show,
and mark selected question options with aria-pressed for
accessibility.
The WorktreeStatus union feeding activityDotState widened, and the
switch-exhaustiveness gate now fails on main itself (verify does not run
on pushes to main, so the break landed silently and blocks every PR's
CI). Cover the two glyph-less states explicitly — matching the
function's documented intent — and drop the now-unreachable default.
* Fix stacked-worktree PR creation targeting a local-only parent branch
- Resolve the eligibility default base to a remote-tracking ref instead
of blindly trusting the submitted parent branch, since a stacked
worktree's base is often a local-only branch the remote can't resolve
- Add a create-time hard block (base_not_on_remote) so a stale or
unpushed submitted base fails with actionable copy instead of the
provider's opaque error
- Update the dialog's default-base resolution and blocked-action/
dropdown copy to match the new remote-validated default
* Split hosted-review-creation.test.ts to fix max-lines lint error
Moved getHostedReviewCreationEligibility tests to a separate file (hosted-review-creation-eligibility.test.ts) to reduce the original file size from 880 to 579 lines, satisfying the max-lines lint constraint.
Co-authored-by: Orca <help@stably.ai>
* Fix Create PR intent flow to use remote-validated eligibility default fo
Prefer eligibilityDefaultBaseRef over the raw compare base when resolving
the review base for the one-click Create PR intent flow, since eligibility
is recomputed from the same compare base right before creation and already
corrects a local-only stacked parent to the repo default. Falls back to
the compare base only when eligibility supplies no default.
* Simplify base-ref remote existence check into a single for-each-ref call
Combine the wildcard and exact-tracking-ref lookups into one for-each-ref
invocation with multiple patterns instead of two sequential git calls,
removing the redundant rev-parse fallback path.
---------
Co-authored-by: Orca <help@stably.ai>
* fix(runtime): preserve terminals during headless desktop activation
* rm design doc
* Fix desktop activation launch ordering and blocked-window status resolut
- Check desktopWindowStatus before spawning the Orca app so a blocked
runtime no longer launches a doomed second instance.
- Reuse resolveDesktopWindowStatus for remote runtime status so it
honors the same authoritativeWindowId fallback as local status.
- Re-check the authoritative window at spawn time instead of trusting
a possibly-stale snapshot, since it can be destroyed mid-await.
- Harden the e2e activation spec against silent spawn failures.
---------
Co-authored-by: bbingz <zzb@gxsmjx.com>
* fix(editor): avoid undo history for read-only live tails
* Add reliability-gate evidence for read-only live-tail undo-history fix
- Records the passing vitest run that verifies live-tail appends leave
canUndo false while ordinary external updates stay undoable, backing
the recent editor undo-history fix.
* fix watcher lifecycle cancellation bounds
* fix(review): guard remote watcher installs against post-shutdown resurrection
closeAllWatchers aborted the in-flight install *tokens* it could see, but a
same-key joiner awaiting a 'cancelled' resolution (and a fired retry tick)
calls installRemoteWatcher directly and, on the fresh-generation recursion,
builds a brand-new non-aborted AbortController and calls provider.watch()
after teardown — leaking an SSH watcher into the just-cleared remoteWatchers
map. Latch the subsystem shut in closeAllWatchers and refuse installs while
latched; a genuine new fs:watchWorktree clears it. Adds a regression test
(fails without the latch) plus a test for the same-tick handoff-revival guard
that had no coverage.
Also extract the duplicated isolated-quarantine-vs-fuse branch shared by
retireSlot and releaseFailedRoot into quarantineOrFuse (behavior-preserving).
* Add lifecycle generation guard to refuse stale remote-watcher joiners
- A boolean latch alone can't distinguish a pre-shutdown joiner from a
fresh call once a genuine new watch reopens the subsystem, letting a
stale joiner recurse and register a post-shutdown provider.watch()
- Each installRemoteWatcher call now captures a generation counter that
closeAllWatchers bumps, so a waiter that resumes after a later
shutdown+reopen is refused instead of resurrecting
- Adds a regression test covering the shutdown-then-reopen race
Silently swallowing chmod failures let installs proceed with a
non-executable launcher, surfacing as a confusing runtime error later
instead of a clear install-time failure.
* fix(mobile): validate Windows firewall remote scope
* fix(review): simplify string-guard ternary to boolean AND
The ternary returned only boolean literals, so cond ? f() : false is
equivalent to cond && f() (addressScopeIsSufficient returns boolean).
Co-authored-by: Orca <help@stably.ai>
* fix(mobile): accept dotted-netmask firewall scopes and lock in fail-safe edges
- Parse dotted-netmask CIDR (192.168.0.0/255.255.255.0) via a contiguous-mask
check, failing closed on holey masks.
- Factor subnetFromParsed so CIDR parsing no longer re-parses the address.
- Document why single-host (/32, /128) subnets and family-specific keywords with
an unknown interface family fail closed, and add regression tests covering
those deliberate false-deny edges plus policy-defined keywords (Intranet, DNS).
Co-authored-by: Orca <help@stably.ai>
* Add explanatory comment on why firewall scope check isn't unioned
Documents the fail-safe rationale behind checking coverage per rule
instead of merging rule scopes, so future edits don't "fix" this
into a less conservative union check.
---------
Co-authored-by: Orca <help@stably.ai>
* fix(cli): harden Windows launcher transports
* Fix csc.exe compile failures on space-bearing Windows install paths
- Legacy csc.exe mangles absolute paths containing spaces, so the
compile step now cd's into the bin directory and passes bare
file names for /out and the source file instead of full paths
* feat(mobile): show usage reset countdown on accounts screen
Surface the rate-limit reset time ("5h resets in 3h 54m · 7d resets in
6d 7h") under the usage bars on the mobile accounts screen, matching the
desktop status-bar tooltip copy. The resetsAt timestamps already arrive
in the accounts.subscribe snapshot; this only adds the presentation.
Claude-Session: https://claude.ai/code/session_01FvjvCsc9QoyQALqvxkvDqQ
* docs(mobile): JSDoc for new usage reset selectors
Claude-Session: https://claude.ai/code/session_01FvjvCsc9QoyQALqvxkvDqQ
* refactor(mobile): per-bar reset countdown instead of combined line
Drop the redundant "5h/7d" prefixes — each countdown now renders under
its own bar ("Resets in 3h 54m"), matching the desktop tooltip copy
exactly.
Claude-Session: https://claude.ai/code/session_01FvjvCsc9QoyQALqvxkvDqQ
* Extract shared reset-countdown formatter for desktop and mobile
- Move duration/countdown formatting out of tooltip.tsx into
src/shared/rate-limit-reset-format.ts so mobile's account-usage-state
can reuse it instead of a duplicated copy (with tests).
- Re-export formatResetCountdown from tooltip.tsx to avoid touching
existing import paths.
- Resend the pairing deep link once more in start-emulator.mjs since
the first can arrive before the Expo app's JS router is ready.
---------
Co-authored-by: kaynan <kaynan.camargo@terceiro-sky.com.br>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
The store bumps agentStatusEpoch at the 30m stale boundary without
replacing the agentStatusByPaneKey map. Keying the flag cache on map
reference alone kept serving stale flags (old timestamp, abandoned
spinning), while the sidebar — which keys on agentStatusEpoch — correctly
de-spun. Invalidate on either changing.
Co-authored-by: Orca <help@stably.ai>
* Show agent status in terminal tabs
* Resolve terminal tab activity via the canonical worktree-status engine
The first pass hand-rolled resolveTerminalTabAgentActivityState, a fourth
parallel copy of the pane-iteration/freshness/title-heuristic loop that already
lives in smart-attention.ts and worktree-agent-activity-summary.ts. It diverged
from every existing surface (novel blocked>waiting split, a phantom 'interrupted'
red state the sidebar treats as idle) and re-scanned the global agentStatusByPaneKey
map per tab per store write (O(tabs*agents)).
Replace it with resolveTerminalTabActivityStatus, which reuses resolveWorktreeStatus
(the WorktreeCard resolver: freshness gate, live-PTY liveness, per-leaf title dedup,
permission>working>done priority) over a per-tab flag summary bucketed once per store
snapshot (O(tabs+agents)). Tabs now speak the same WorktreeStatus vocabulary as the
sidebar, so their live states can't disagree with the worktree card.
- Map WorktreeStatus -> AgentStateDot: working=spinner, permission=amber, done=check;
active/inactive fall through to the agent/shell identity icon.
- Parse legacy numeric pane keys too, matching the sidebar summary, so restored/
imported sessions light the tab.
- Drop the bespoke resolver + its tests; add focused coverage for the new resolver
and the leading-icon component.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
getStaleDispatches compared space-format datetime('now') columns
(dispatched_at, last_heartbeat_at) against an ISO-Z threshold using raw
TEXT ordering. The space (0x20) sorts below 'T' (0x54) at index 10, so
every fresh same-UTC-date dispatch/heartbeat was ordered below the
threshold and wrongly flagged stale, producing a false stale warning on
each coordinator tick. Wrap both column comparisons and both bound
threshold params in julianday(), which parses the space and ISO-Z
formats as UTC for a correct numeric comparison.
Fixes#8452
(cherry picked from commit 1d40312bf2)
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* fix(codex): debounce auto-resolved approval notifications (#8387)
Codex fires its PermissionRequest hook at the human-input boundary before
the approval decision, so under 'Approve for me' the review agent approves
and Codex resumes within ~1s. The completion coordinator dispatched the OS
attention notification immediately on that 'waiting'/'blocked' hook, so the
self-resolving pause raised a false 'approval required' notification.
Debounce the Codex OS attention notification behind a 1500ms quiet window
and cancel it when a working/completion hook lands inside the window. Scoped
strictly to agentType==='codex'; every other agent's pause still notifies
immediately. The visual sidebar status is set upstream (store.setAgentStatus)
before the coordinator runs, so it stays immediate.
Co-authored-by: Orca <help@stably.ai>
* review: harden #8387 fix — protect debounce evidence + cancel on title resume
Two adversarial-review hardening fixes to the Codex attention debounce, both
preserving the fail-open contract (a genuine 'needs input' pause must always
notify):
- handleProcessInspectionResult: extend the evidence-teardown guard to also
hold while a pendingCodexAttentionTimer is armed. A transient null/shell
foreground blip (or a remote/SSH inspection that returns null foreground)
could otherwise tear down agent evidence mid-window and make the timer's
hasAgentRunEvidence guard silently drop a genuine pause banner — or fire a
false process-exit completion racing the pause. Mirrors the pendingHookDone
guard.
- recordTitleWorking: cancel the debounced attention on a genuine title-driven
resume (placed after the replay guard, so a stale post-completion title
replay never drops a still-pending genuine pause banner).
Adds 6 discriminating fake-timer tests (guard, title resume, blocked parity,
done cancels attention, dispose clears timer, second distinct pause re-arms).
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(source-control): route GHES remotes to the GitHub provider for PR creation
A GitHub Enterprise Server user could not submit a PR — Orca demanded
ORCA_GITEA_TOKEN — while issue sync worked fine (#8312).
Root cause: GitHub owner/repo resolution (parseGitHubOwnerRepo) hard-rejects
any host that is not literally github.com. A GHES remote lives on a custom
host, so GitHub's forge resolveRepository returned null and provider detection
fell through the list to Gitea, whose KNOWN_NON_GITEA_HOSTS denylist cannot
enumerate arbitrary GHES domains. Issue sync was unaffected because gh
issue/pr list run with cwd=repoPath and let gh resolve the GHES host natively.
Fix mirrors GitLab self-hosted detection (getGlabKnownHosts): a new
getEnterpriseGitHubRepoSlug resolves a custom-host origin to owner/repo only
when gh is authenticated to that host — gh only ever manages GitHub/GHES
credentials, so a logged-in host is definitively GitHub. Wired into:
- forge-provider GitHub resolveRepository (fallback after github.com miss),
so detection claims GHES before Gitea is consulted;
- createGitHubPullRequest owner/repo resolution;
- isGitHubAuthenticated, which now probes the repo's real host instead of a
hardcoded --hostname github.com.
github.com repos keep the cached getRepoSlug fast path and never spawn the
extra gh auth probe.
* fix(github): host-qualify GHES gh commands and probe auth in the repo runtime
Addresses two correctness issues found in review of the #8312 fix.
1. GHES host was discarded before `gh pr create`. `--repo owner/repo` shorthand
resolves against gh's default host (usually github.com), so for a user
authed to both github.com and GHES it could target a same-named github.com
repo or fail — deterministic for SSH repos, which run gh with no cwd. Now
`createGitHubPullRequest` and the `findOpenPRByHeadBase` fallback pass a
host-qualified `HOST/owner/repo` for GHES (github.com keeps the shorthand).
Also generalize `parseCreatePRPayload`'s URL regex off github.com so a GHES
PR URL parses directly instead of limping through the list fallback.
2. GHES auth was probed on the wrong gh runtime. `getAuthenticatedGitHubHosts`
ran a global `gh auth status` with no cwd/WSL/SSH context and cached every
runtime under one "local" key, so a GHES login present only in the repo's
WSL distro was missed and the repo fell back to Gitea. Replaced with
`isGitHubHostAuthenticated`, which runs `gh auth status --hostname <host>`
with the repository's execution options (cwd/WSL distro, or SSH-local like
the create path) and caches per runtime+host — mirroring GitLab's
isGlabConfiguredForRemoteHost. This also honors GH_ENTERPRISE_TOKEN inferred
from repo context. Spawn failures stay indeterminate (uncached).
Adds createGitHubPullRequest-level tests asserting the actual gh `--repo`
arguments (create + fallback) and the WSL/SSH runtime of the auth probe.
* perf(source-control): drop redundant GHES gh auth probe in eligibility
Review follow-up. Detection only routes a GHES remote to the GitHub provider
after getEnterpriseGitHubRepoSlug has confirmed gh is authenticated to its
host, so isGitHubAuthenticated can trust a non-null slug as authenticated and
skip a second, rate-limited `gh auth status` spawn per eligibility poll.
Reaching the github.com probe now implies the remote is github.com. Tests
assert the enterprise path fires no redundant gh probe.
Once git status hit the 10,000-entry limit, the huge flag disabled every
status-refresh lane — including the push signals (repo metadata watcher,
terminal command-finished) that carry the evidence needed to clear it.
The flag only clears when a fresh non-huge status result arrives, so the
worktree deadlocked into stale Source Control and explorer badges until
an app restart.
Split the gate: evidence-free interval polling stays paused while huge
(preserving the #7983 idle-CPU fix), but push-signal refreshes now ride
the coalesced change-signal lane, so a commit made in the integrated
terminal clears the flag and resumes normal polling. A visibilitychange
listener (active only while huge pauses polling) catches up signals
dropped behind a hidden window, matching the becoming-visible catch-up
the normal lane already has.
* Allow replying to any comment in a thread, not just the root
Reply state now tracks a comment id instead of a group id, and reply composers/handlers are threaded through each comment row (root and replies alike) so any comment can receive an inline reply rather than only the thread root.
* Fix reply cancellation clearing the wrong comment's reply box
When multiple comment threads had reply boxes open, cancelling one
reply cleared replyingCommentId unconditionally, closing whichever
box was open instead of the one actually cancelled. onCancelReply now
threads through the specific commentId so cancellation only clears
state if it matches the currently open reply.
* fix(window): extend startup reveal fallback to Linux so first launch never stays hidden (#8421)
On Linux/X11, ready-to-show can never fire (GPU/driver quirks), leaving the
only BrowserWindow hidden until a second launch triggers the second-instance
reveal path. Reuse the existing bounded Windows fallback timer on Linux; the
handledInitialReadyToShow guard and headless E2E check already make the
reveal idempotent and safe.
Claude-Session: https://claude.ai/code/session_017rio4rnPiCUh8jHWxkq4xH
* docs(window): drop win32-only qualifier from tray-fallback comments
The tray-create fallback comments referenced 'createMainWindow's win32 10s
reveal fallback', but #8421 extends that reveal fallback to Linux too. Since
the tray itself is win32-only (createSystemTray no-ops off win32), naming a
platform in these comments is both stale and misleading. Drop the qualifier;
the surrounding Windows-only tray context already scopes it.
---------
Co-authored-by: kaynan <kaynan.camargo@terceiro-sky.com.br>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>