* fix(ui): align multiline filter toggle row text and icons to top-left
- Add `text-left` and `items-start` to FilterToggleRow button and label wrapper.
- Add `mt-0.5` top margin to icon and switch indicator for optical alignment with multiline text.
- Resolves center-aligned wrapped text and vertically misaligned toggles under long localized labels (Korean, Japanese, Spanish, etc.).
* fix(ui): align multiline filter toggle labels with their controls
---------
Co-authored-by: Neil <neil@stably.ai>
* fix(sidebar): keep Cmd+Shift+Arrow cycling in place after closing a workspace
Closing a workspace's last tab clears activeWorktreeId (landing fallback), so
cycling had no anchor and re-entered the sidebar from its far end — every
subsequent Cmd+Shift+Down/Up restarted there instead of continuing from where
the user was.
Resolve the anchor from the nav-history cursor when nothing is selected. A
selection that is present but uncyclable still passes through untouched, so a
row inside a collapsed group keeps entering from the matching end (#10513).
History is read via getState() at press time; subscribing would re-render the
virtualized sidebar on every visit.
* fix(sidebar): include folder workspaces in Cmd+Shift+Arrow cycling
getCyclableWorktreeIds dropped every non-item row, so a folder workspace sitting
in the sidebar could not be reached with the arrow chord even though Cmd/Ctrl+1-9
already numbers it. Cycling also activated through activateAndRevealWorktree,
which has no route for a `folder:` key.
Build the rotation with getRenderedWorktreesInSidebarOrder — the helper Cmd+1-9
already uses, so both shortcuts cover the same rows in the same visual order —
and activate through activateAndRevealWorkspace, which dispatches on the key.
Rows still come from what the sidebar rendered, so collapsed groups stay out.
* fix(sidebar): keep the cycle anchor host-qualified after a close clears the selection
Closing a workspace's last tab nulls both the selection and its host, and
nav history stores no host, so the history anchor cycling falls back to
resolved to the first rendered row with that id. With the same worktree
id on two hosts, closing the twin on the second host made the shortcut
cycle from the first host's row.
The keyboard hook now remembers the last selected (workspace, host) pair
and hands that host to the anchor when the anchor is that workspace. Any
other history anchor stays host-less as before. Nav history is persisted,
so its entry format is left alone.
---------
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
* fix(gitea): treat HTTP 404 as no-PR in throwing hosted-review lookup
hostedReview:forBranch always uses the throwing Gitea lookup variant, and
requestJsonAtBase threw on any non-OK status. A 404 (PR deleted, repo absent
on the forge, or Gitea masking a private repo) rejected the IPC handler and
spammed the main-process log on every refresh for every unaffiliated branch.
404 means the resource is absent — an accepted "no PR", not a transport or
auth failure. Map it to null even with throwOnFailure; 401/403/5xx/network
errors still throw so creation eligibility keeps reporting 'unavailable'.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(gitea): scope the 404 no-PR exemption to the linked-PR fetch
A blanket status !== 404 exemption in the shared request helper also
covered the repo-level /pulls scan, where a 404 means a wrong API base
URL, a repo absent from the forge, or a token without scope. Collapsing
that to "no PR" would make a misconfigured Gitea host look permanently
PR-less instead of surfacing reviewLookupOutcome: 'unavailable'.
Gate the exemption on an explicit allowNotFound request option, set only
on the single-resource /pulls/{n} fetch.
Co-authored-by: Orca <help@stably.ai>
* test(gitea): pin the 200-with-empty-list no-PR shape alongside the 404 case
Co-authored-by: Orca <help@stably.ai>
* fix(gitea): preserve deleted linked pull request absence handling
Refresh the original Donach PR and its prior maintainer correction on current main, keeping the HTTP404 exemption limited to the linked pull resource.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* fix(editor): render <br> in markdown table cells as line breaks in rich mode
The rich Markdown editor encoded every inline HTML tag — including `<br>` and
`<br/>` — into an inert raw-HTML atom that displayed the literal tag text. In a
table cell, where a blank-line break is impossible, this meant `<br>` showed up
as raw "<br>" instead of breaking the line.
Render `<br>`/`<br/>` raw inline nodes as real break elements while keeping
`renderMarkdown` emitting the verbatim tag, so the value round-trips exactly on
save. A native hardBreak is intentionally not used: it serializes to a space
inside table cells and would silently drop the tag. The raw-source node's
parse/render DOM logic is extracted into a dedicated module to keep it cohesive
and within the file's line budget.
Closes#8838
* fix(editor): validate preserved break source
Use the editor model's supported DOM parse-rule type instead of a private duplicate. Exercise the rendered-DOM round trip and reject forged preservation attributes before they can become hidden Markdown source.
* test(editor): assert forged break marker exists
* test(editor): preserve authored rich markdown break spelling
---------
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Neil <neil@stably.ai>
The workspace space-manager rendered every hosted review as "PR #N", so a
GitLab merge request showed as "PR #5" instead of "MR !5". The cached review
already carries its provider (HostedReviewInfo.provider) — the panel's local
input type just narrowed it away. Thread provider through and format via the
same localizedHostedReviewCopy / '!' vs '#' helpers the rest of the app uses.
Extract the label formatting into hosted-review-label.ts (a pure module, away
from the panel's heavy React/store imports) so it can be unit-tested directly,
and add hosted-review-label.test.ts covering github / gitlab / azure-devops /
gitea and the status suffix.
linkedPR stays "PR #N" — it is the GitHub-specific workspace field, distinct
from linkedGitLabMR.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Neil <neil@stably.ai>
* fix(review): show Remove Project label in PR panel for main worktree
When a merged PR is linked to the main worktree (the project root /
default branch), the PR sidepanel delete button now reads "Remove
Project" instead of "Delete Workspace", matching the project-removal
dialog that runWorktreeDelete already opens for the main worktree.
Adds the b1a767b1ae i18n key (Remove Project) across en/ko/ja/zh.
* chore(i18n): apply AI code review suggestions
Rename the newly added HostedReviewActions key from the codemod-style
content hash b1a767b1ae to the intent-named id removeProject, per the
message ID policy in config/i18n-translation-source.md: new keys must
use intent-named IDs, and hash IDs minted after 2026-07-30 are not
grandfathered.
---------
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
findActivePathCommand now .catch(()=>null)s inspectSymlink so an execute-only foreign PATH `orca` is skipped instead of rethrowing EACCES; blocks the readFile EACCES at cli-installer.ts:551.
Co-authored-by: orca-bug-scan-bot <orca-bug-scan-bot@stably.ai>
Co-authored-by: Neil <neil@stably.ai>
Added [@media(hover:none)]:ml-0/max-w-5/opacity-100 touch reveal to REPO_HEADER_ACTION_REVEAL_CLASS so repo/group header action buttons show on touch devices.
Co-authored-by: orca-bug-scan-bot <orca-bug-scan-bot@stably.ai>
Co-authored-by: Neil <neil@stably.ai>
* fix(ui): use focusable tooltip menu triggers
* test(tooltips): give each menu surface its own remount check
Keep all hover, focus, description, keyboard, pointer and two-remount
assertions while avoiding a shared 120-second budget across three surfaces.
The failed CI trace passed five full exercises before global teardown.
---------
Co-authored-by: Yuris Auzins <zuz666@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
* fix(composer): keep the imported group as the target after a grouped nested import
Grouped folder imports create a ProjectGroup, but the hosted Add Project
handoff selected only the first imported repo, dropping the group the user
explicitly chose. Carry the root group id through the completion path and
select the group's folder-workspace target in the composer, falling back to
the single-project handoff when no group was created or it is no longer
selectable. Sidebar default-checkout and separate imports are unchanged.
Part A of #9529.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(sidebar): document the CompleteGitRepoAdd handoff contract
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: just-zzp <264744947+just-zzp@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
* fix(orchestration): refuse send to a Run whose home is another computer
A remote Dispatch's Run id also names a mirror row on the worker's host. Once the
worker lost its link to the home, send --to run:<id> wrote into that mirror and the
message reached no one. Now the host refuses with run_home_unreachable, and a linked
worker that names its own Run home is delivered there.
Reported by @cookapps-jhlee8 in #32292.
* fix(orchestration): refuse only mirror Runs, and answer coordinator mail at its Run home
A loopback runtime's attachment names a real local Run, so the refusal now keys on the
Run row's remote home, not on any attachment. A reply to coordinator mail imported under
a remote Run relays to the home through the replying pane's Dispatch link, and is refused
with run_home_unreachable when that link is gone.
* fix(orchestration): a remote reply answers only mail sent to the replier's own Dispatch
* test: replace process and subagent status waits with controlled clocks
* test: establish permission waits and deliverable stale results
* test: consolidate launcher checks and require retained relay state
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(ssh): keep a paired server's SSH access link across its restarts, and let it relink
orcad mints a new runtimeId at every start. A linked server refused to record it,
and an unlinked one hid its sidecar entry behind the stale id, so relinking failed.
A new paired device or a pending link is still refused.
Fixes#32957. Reported by @badaz.
* fix(ssh): keep SSH access operations valid across a server restart
runtimeId is per-process, so it no longer binds an SSH access operation. A lost
completion retry now checks the runtime the link verified, and a pending unlink no
longer refuses a restarted runtime.
* fix(terminal): recover restored SSH terminals after password sign-in
* fix(terminal): also recover after a cancelled SSH sign-in, and share the in-progress copy
A restored pane refused because the user cancelled the password prompt
stayed frozen after a later successful sign-in too. Treat that refusal like
the other pending sign-in states, and share the in-progress wording with
main so the renderer match cannot drift.
Co-authored-by: ray3w <61264633+ray3w@users.noreply.github.com>
---------
Co-authored-by: ray3w <61264633+ray3w@users.noreply.github.com>
* feat(connectors): cancel a conversation's queued work and stop its agent, behind experimentalConnectors (B4)
* test(connectors): drop a type assertion from the interrupt test
* fix(connectors): interrupt only a running main-agent turn, never background-only work
* chore(connectors): shorten the interrupt comment
* fix(connectors): fence cancel and prompt keys at transport handoff
* fix(connectors): clear the prompt Claude restores after our cancel, and end a turn our cancel rejected
* fix(connectors): require stronger evidence for cancel cleanup and settlement
* fix(connectors): cancel waits for a published agent's link, keeps cancel headroom in a full backlog, and pastes nothing after an unsettled clear
* fix(connectors): bound cancellation settlement and preserve unresolved lanes
* refactor(connectors): drop the leftover-prompt clear after cancel; keep the rejected-turn end
The alias, store and fact codecs each carried a private copy of the same
nonempty/trimmed/length-bounded/no-control-character check. Use the existing
isBoundedString from agent-status-child-work-value-guards instead.
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(terminal): a file link opens in the workspace it was clicked in
A terminal or chat link to a file another workspace on the same host holds used to switch to that workspace. It now opens as a plain read-only file in the workspace it was clicked in (local and WSL read on this computer; paired and managed servers read through the owning workspace's scope), with a toast naming the owner and an Open there action, and a Read-only badge in the editor header. HTML on server hosts, server workspaces the client does not know, directories, shift-click download and direct-SSH workspaces keep the switch. The view is watched with its owner through one derived scope (lib/open-file-owner-scope.ts); phones skip plain file tabs they cannot read by workspace-relative path.
Fixes#32158
* fix(terminal): a fresh id per other-workspace hint, and owner lookups cached per route inputs
* fix(terminal): only this computer's files open in place; SSH and server links keep opening in their workspace
Servers read files only by workspace-relative path, so a read-only view of another workspace's server file needed owner rerouting in every read path. Paired and managed servers keep today's switch; the loader moves any restored read-only server tab of another workspace's file there as before.
* feat(orcad): run --reclaim-stale-lock on a managed host's active slot
The desktop can now dry-run and confirm the stale instance-lock recovery
on a Linux managed host, serialized with launches, then start the server
again and reconnect the way the restart action does.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* feat(settings): offer Recover server for a managed host refused on a stale lock
When a managed orcad start names --reclaim-stale-lock, the SSH host row
offers Recover server. The dialog dry-runs the check, shows the host's
evidence, and clears the lock only after the user confirms no other
machine sharing the home runs Orca.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* feat(mobile): point a host refused on a stale lock to the desktop's Recover server
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(settings): mount the Recover server dialog per opening
A fresh mount starts each opening from a new check instead of resetting
state in an effect.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(orcad): end the readiness wait once the host proves the launch exited
A managed orcad refused at start (such as on a possibly stale instance
lock) left the client waiting out the full readiness deadline. Each
host-side wait now also checks the launch's PID file with kill -0 and
ends early on a proven exit, so the caller reports the same pending
readiness, with the orcad.log tail, within a poll. No PID, a denied
probe or a reused PID keeps the old deadline.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(orcad): refuse a managed slot without --reclaim-stale-lock before running it
An older orcad ignores the flag and starts serving in the foreground. The
same remote exec now checks that the slot's server bundle names the flag
and answers unsupported otherwise, without invoking orcad.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(settings): show the manual stale-lock command as literal mono text
The rendered dialog ran the command into the sentence, so it was hard to
read or copy. It now sits in its own mono block, like Details.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(settings): offer only a manual delete when the host can't reclaim, and format the lock's age
An unsupported host was told to run the very command it lacks. Main now
names the lock path for that case, and the dialog shows only a copyable
rm of it with the shared-home caution. The lock's age uses the UI's
relative-time formatter instead of a hand-rolled minutes count.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(orcad): pin a confirmed stale-lock reclaim to the record the user approved
--yes reclaimed whatever record held the lock at that moment, so a lock a
same-named machine wrote while the dialog sat open could be cleared.
orcad now takes --expect-nonce and refuses unless the lock still holds
it; the dry run reports the nonce, the desktop sends it back on confirm,
and the managed slot gate requires a build that knows the pin. A
reclaimed result carries the evidence it acted on.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(settings): keep the Recover server dialog open through its own reclaim
The dialog's mount was gated on the status that offered it, and the
reclaim clears that note before its answer arrives, so the result was
never shown and the card kept the dialog marked open. Only the triggers
are gated now; an open dialog keeps the server it opened for until the
user closes it.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(settings): put the Recover server confirmation on the security surface
It asks permission to run a remote command that clears an ownership lock
and starts server code, which STYLEGUIDE puts on that surface.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(orcad): trust a gone launch PID only on the machine that launched it
The PID file sits in a home other login nodes may share, so a wait exec
that lands on another node read a starting server as exited. Both launch
paths now record the launching machine's boot id and node name beside
the PID, and the readiness wait ends early only when its own exec reads
the same record. No record, a host without a boot id, or another machine
keeps the deadline. The PID path is named once in the launch command so
it stays within its sent-byte ceiling.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(orcad): keep a slot's unreadable stale-lock answer from offering a manual delete
A slot that passed the gate but gave no JSON, or a status this client
does not know, read as unsupported and offered rm of the lock. It now
reads as no-answer, which points to the slot's own reclaim command, and
unsupported is left to a slot without the command, a non-Linux host or
no active version.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* style(settings): use the text-2xs step for the stale-lock command block
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(mobile): offer the desktop-recovery hint on the same refusal the desktop does
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* test(orcad): name the extra fields a fake stale-lock answer carries
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(settings): end the Recover server flow once the lock is cleared, and keep an unpinned reclaim manual
After a reclaim whose restart failed, the dialog kept the old evidence,
the enabled confirmation and manual recovery for a lock that was gone.
It now shows only that the lock was cleared and why the server did not
start, with Close. A slot that has the reclaim but not the nonce pin is
now told apart from one without the reclaim, and is pointed to its own
reclaim command rather than rm.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(orcad): keep a stale-lock reclaim's result when the restart after it throws
A wake or tunnel rebuild that threw after the lock was cleared discarded
the reclaimed result, so the caller skipped its reconnect and note
clearing and the dialog kept a confirmation for a lock that was gone.
The restart's failure now becomes the result's notRestarted reason.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(orcad): report a reclaimed server as started when only its forward failed to follow
A tunnel rebuild that failed after the wake started the server was
reported as the server not starting. The rebuild now goes through
followLaunchedServerPort, as other relaunches do, which warns and leaves
the forward to the next connect. Also drops a caller test that only
covered behaviour the fix did not change.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* test(mobile): re-record the execution-target goldens for the recovery hint field
The SSH gate's projected state gained `needsDesktopServerRecovery`, which
is false for every recorded reply (no recorded host sends a managed
server refusal). Re-recorded with `pnpm --dir mobile rpc:record` for the
two pilot recordings and the four execution-target reply partitions; the
only change is that field, plus the value pool reordering around it.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
---------
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(terminal): verify an exact stop through an overlapping liveness inventory
terminal.stopExact re-read the PTY inventory once after the stop. When a newer
inventory started meanwhile (the exit's own session republish triggers one),
the older read was voided as stale and the confirmed stop came back as
terminal_liveness_unavailable, so hibernating a remote pane on a busy host
rolled back. Retry the post-stop read within one list budget.
* test(terminal): keep the post-stop liveness failure fixture unavailable across retries
* fix(agents): preserve quick-launch presets in legacy tab creation
* fix(agents): + quick launch on a paired host uses the desktop's launch settings
An empty-prompt agent tab opened with + on a paired or SSH-tunnelled host
sent only the agent id, so modern hosts used their own default args and
legacy hosts rebuilt the command from host settings. Send the same
startup plan the prompted launch already sends, so both
terminal.createAgentSession (agentArgs) and the legacy
session.tabs.createTerminal fallback get the desktop's arguments.
Replaces the legacy-only legacyStartupPlan field and its linux/darwin gate:
one launch shape now serves both paths, matching prompted launches.
Fixes#31986
Co-authored-by: StrangerG <8511644+StrangerG@users.noreply.github.com>
---------
Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: StrangerG <8511644+StrangerG@users.noreply.github.com>
* perf(emulator): accumulate MJPEG chunks instead of re-joining partial frames
A simulator frame spans many socket chunks, and extractJpegFrames re-concatenated
the growing partial frame on every chunk, which is quadratic in chunks per frame on
the Electron main thread. Hold chunks until one could complete the frame, then join
once. Held chunks are copied, so no caller chunk memory is retained.
* perf(emulator): ignore empty MJPEG chunks in the accumulator
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* refactor(worktrees): remove unreachable legacy worktree list IPC
The desktop and web preloads both always provide `worktrees.listDetected`,
so the renderer's fallback to `worktrees.list` never ran, and nothing calls
`worktrees.listAll`. Remove both IPC handlers, their preload/web wrappers,
the renderer fallback, and helpers only they used. Tests that observed
behaviour through the removed handlers now go through `worktrees:listDetected`.
* test(worktrees): drop removed list channels from SSH listing matrix
Address review: the canonical SSH listing test built channel names dynamically
and still invoked worktrees:list/listAll. Also fold the detected-result reader
into the shared worktree test fixtures, refresh stale comments, and assert the
backfill no-op test listed authoritatively.
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* refactor: project structured status once per host emission
* test: keep host fixture literals checked against Tab
* test: declare the nullable bridge store without a cast
* refactor: compare tab membership directly at the read gate
* test: isolate approval activity fixture from GitHub refresh
* fix: recover status projection cleanup and bound removal work
* test: give status apply spy a callable type
* Scope structured status membership to host groups
* Type structured status lifecycle fixture tabs
* fix(native-chat): a goal set while the agent rests clears the composer
Setting a goal starts the agent when none is running, which moves the
session's runtime fence before the host answers. The composer dropped
that successful answer as stale, so the objective and Goal chip stayed.
Keep the reply for goal changes as for the other writes that may start
the agent.
* fix(native-chat): settle each command independently of runtime changes
* fix(native-chat): scope rewind timeout feedback to its issuing pane
* test: use checked native chat command fixtures
* test: complete native chat presentation fixtures
* fix(native-chat): admit Stop before capability discovery
* test(native-chat): consolidate overlapping startup coverage
* test(native-chat): preserve IME composition on goal acceptance
* fix(native-chat): capture each action before async preparation
* fix(native-chat): preserve shared Stop failure reporting
* test(native-chat): cover already-sent queue notices after disposal
* fix: keep stale linked-reference status inline
* fix: shimmer stale reference rows without a clock
* docs: compare row and shadcn text shimmer treatments
* fix: shimmer only stale reference metadata
* fix: remove obsolete row shimmer class
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* test: strengthen hook, build, and mobile lifecycle coverage
* fix: use relocated Jira types in issue user field
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>