* fix(mobile): clear native-chat composer optimistically at send time
Over relay the send RPC round trip is visible and a lost ack (or a
relay/direct cutover) could strand the sent prompt in the composer
forever: the unconfirmed-send deadline dropped its tracking entry, so a
late transcript echo could never clear the draft.
Clear the draft at send time and restore it only on a definite
rejection. holdUnconfirmedSend now only manages the delivery-unconfirmed
notice; it no longer touches drafts.
* fix(mobile): isolate question answers from composer drafts
* fix(terminal): stop bold flash on worktree switch
Worktree hide disposes WebGL and falls back to xterm's DOM renderer.
On reveal, resume ran after paint and flushed backlog against DOM first,
so the first frame used heavier CSS-AA glyphs before WebGL settled.
Resume in useLayoutEffect and reattach WebGL before backlog flush so the
first painted frame stays on the GPU path. No cold-park policy change.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): fit WebGL grid before backlog flush on resume
Adversarial review: resume-before-flush alone wrote TUI backlog onto the
transient DOM↔WebGL one-column-off metrics window. Order is now
resume → fitAllRevealedPanes → flush on heavy reveal and window wake.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): latch viewport intent before WebGL wake resume
Adversarial review: wake path synced intents after resume/fit, which can
re-latch a pinned viewport as followOutput. Capture before reattach and
drop the post-resume re-sync on heavy reveal (outer path already latched).
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): complete visibility bookkeeping before PaneManager exists
useLayoutEffect runs before the passive lifecycle creates PaneManager, so
the mount-visible path never set hasCompletedVisibleResume. The first
intra-worktree hide then wrongly suspended WebGL. Bookkeep completion
even when managerRef is still null (extracted helper for max-lines).
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): re-sync pin geometry after resume backlog flush
Keep the pre-resume intent latch (reattach must not re-latch pins as
followOutput), then re-sync after flush with preservePinnedAtBottom so
scrollback trim updates absolute pin lines before enforce.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): drop same-tick post-flush intent re-sync
flushTerminalOutput only queues terminal.write and returns before parse, so
a same-tick re-sync read pre-parse resume/fit geometry and could overwrite
pre-resume pins. Keep pre-resume latch + enforce only.
Co-authored-by: Orca <help@stably.ai>
* fix(test): expect default worktree card properties to include cli
#10712 added 'cli' to DEFAULT_WORKTREE_CARD_PROPERTIES, but the fresh
default-profile assertion still omitted it and fails verify.
Co-authored-by: Orca <help@stably.ai>
* perf(terminal): retain Windows WebGL across worktree hides
* perf(terminal): bound retained WebGL contexts
* fix(terminal): harden retained WebGL lifecycle
* fix(terminal): preserve healthy WebGL on wake
* fix(terminal): preserve reveal recovery ordering
---------
Co-authored-by: Orca <help@stably.ai>
* Add SenseVoice speech-to-text model (Korean/Japanese support)
SenseVoice (zh/en/ja/ko/yue) is the only bundled local STT model with
Korean and Japanese support. The existing local models cover only
English and Chinese (Parakeet, Zipformer, Paraformer); Whisper Tiny is
multilingual but trades accuracy for breadth.
- Add 'senseVoice' to SpeechModelType
- Register the sherpa-onnx SenseVoice archive in the model catalog
(pinned SHA-256, single-file model.int8.onnx + tokens.txt layout)
- Handle the senseVoice type in the STT worker via createOfflineRecognizer
with the senseVoice model config (auto language detection + ITN)
- Add model-catalog regression tests for the new entry
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(speech): add Parakeet TDT-CTC 0.6B JA to the speech model catalog
* test(speech): cover stt-worker-model-config file resolution incl. single-file models
* feat(speech): decode Parakeet TDT-CTC JA via sherpa-onnx nemoCtc offline recognizer
* fix(speech): use int8-only SenseVoice archive
* fix(speech): refresh SenseVoice catalog metadata
---------
Co-authored-by: xsacdw <xsacdw@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: LauraGPT <LauraGPT@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
* Add SenseVoice speech-to-text model (Korean/Japanese support)
SenseVoice (zh/en/ja/ko/yue) is the only bundled local STT model with
Korean and Japanese support. The existing local models cover only
English and Chinese (Parakeet, Zipformer, Paraformer); Whisper Tiny is
multilingual but trades accuracy for breadth.
- Add 'senseVoice' to SpeechModelType
- Register the sherpa-onnx SenseVoice archive in the model catalog
(pinned SHA-256, single-file model.int8.onnx + tokens.txt layout)
- Handle the senseVoice type in the STT worker via createOfflineRecognizer
with the senseVoice model config (auto language detection + ITN)
- Add model-catalog regression tests for the new entry
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(speech): use int8-only SenseVoice archive
* fix(speech): refresh SenseVoice catalog metadata
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: LauraGPT <LauraGPT@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
* feat: implement Cmd+Enter as commit shortcut in Source Control
* test: add unit tests for commit shortcut and tooltip formatting
* fix: address review feedback on modifier keys and test coverage
* test: split mac and windows/linux shortcut and keydown tests
---------
Co-authored-by: Andres Van Reepingen <andres.vanreepingen@datacamp.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
* Add Close Tabs to the Left and complete Close Others across tab menus and mobile long-press sheets
* Fold the per-sheet Close action into the bulk-close module (session route max-lines)
* fix(mobile): preserve pinned tabs during bulk close
---------
Co-authored-by: Tom de Bres <tomdebres@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Expand repository icon auto-detection to conventional Tauri and public/icon paths with PNG/WebP magic and dimension validation. Bound SSH probing while preserving candidate priority and PNG-only user uploads; SVG remains rejected.
* feat(editor): toggle Word Wrap from file tab actions and Alt+Z
Long single-line and structured files wrap by default and misalign.
Surface Word Wrap on the editor more-actions menu for normal file tabs
(diff already had it) and add editor.toggleWordWrap (Alt+Z) so users can
unwrap without opening Settings.
Closes#9974
* fix(editor): toggle diffWordWrap for diff surfaces on Alt+Z
CodeRabbit: Alt+Z previously always flipped editorWordWrap, leaving
diff panes out of sync with the markdown actions menu.
* test(editor): verify word wrap shortcut routing
Cover editor/diff setting callbacks and the cross-platform Alt+Z binding.
---------
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
* fix(gitlab): stop refresh button overlapping dialog close X
The GitLab item dialog's SheetContent renders its own close (X) at
absolute right-4, but the header refresh button sat at the header's
px-5 right padding and overlapped it. Reserve pr-10 on the header so
the refresh button clears the close X, and lift it -mt-1.5 so its icon
aligns with the close X on the same line.
* fix(gitlab): integrate sheet controls into header
---------
Co-authored-by: viniciussilva <vinicius.silva@plus10.de>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(history): quarantine unreadable recovery files
* fix(history): preserve mixed recovery generations
* fix(history): reanchor reconciled live sessions
* fix(history): serialize final checkpoint queue
* fix(history): drain sleep shutdowns before disconnect
* fix(history): restore legacy wide sessions
* fix(history): preserve malformed mixed logs
* fix(history): preserve malformed log tails
* refactor tests to reduce file size
* refactor(history-recovery): extract freeze helper and improve test robus
- Extract takeRecoveryFreeze to eliminate duplicated freeze-and-clear pattern across five call sites
- Skip permission-mode tests on root CI containers (chmod 0o500 doesn't block root writes)
- Replace fixed sleep with deterministic wait for queued exclusive checkpoints
- Distinguish ENOENT (missing) from corrupt in history metadata reads
- Add ceiling-dimension restore test and torn-tail exclusion assertion
- Wrap chmod operations in try/finally to prevent leaked permissions from masking test failures
- Add .catch() to checkpoint promise to prevent unhandled rejections from finally re-throws
* test(history-recovery): consolidate checkpoint assertions
Wait for both the checkpoint call and set clear atomically to avoid
a timing race where the spy fires before the set is cleared.
* fix(persistence): fsync state writes so a rename is actually durable
`Store` wrote `orca-data.json` to a temp file and renamed it. rename() is
atomic for readers but says nothing about durability: without an fsync the
directory entry can reach disk before the data does. After power loss or a
hard crash the file can come back holding the previous state or, worse,
zero bytes — and `JSON.parse('')` throws, so an empty file takes the
full corrupt-file path rather than degrading.
This is the same empty-file symptom as #1158 from a different cause. That
issue fixed a logic path that persisted empty state and added the .bak ring
as a safety net; the ring also catches this, which is why it went unnoticed.
Recovery costs up to an hour of tabs/layouts/session state (backups are
throttled to >=1h spacing), and a user in their first hour has no backup
slot yet, so they land on defaults indistinguishable from a fresh install.
Both write paths now fsync the temp file *before* the rename, then fsync
the containing directory. Directory fsync is best-effort by design: Windows
cannot open a directory for fsync and some filesystems reject it, so it is
swallowed. The file fsync is the load-bearing part and works everywhere.
Measured cost on a 3 MB payload: ~0.2 ms per write, against a 1s debounce.
The async path does not block the main thread.
The syscall-order test mocks `node:fs` and counts fsync targets at the
module boundary, asserting ['file', 'directory'] — proving the ordering
rather than inferring it from reading the implementation, since a fsync
after the rename would still pass every content assertion.
* test(persistence): make the syscall proof platform-aware and actually prove the order
Two problems, both found from CodeRabbit's Windows observation.
The assertion hardcoded ['file', 'directory']. Directory fsync is
deliberately best-effort — Windows cannot open a directory for fsync and
some filesystems reject it — so on Windows the helper swallows the failure,
only the file fsync is observed, and the test fails. The expectation now
probes the real platform instead of assuming, keeping the guarantee tight
where directory fsync works rather than dropping it everywhere.
Worse, the test did not prove what its name claimed. Moving the fsync to
*after* the rename still passes: the file is fsynced either way, and only
fsyncs were recorded, so the correct and broken orders produced an identical
log. Mutation-testing the "before rename" claim is what surfaced this — the
mutation passed.
The rename is now recorded in the same sequence, since it is the boundary
the ordering is defined against. Re-running the same mutation fails, so the
ordering claim is now backed by the test rather than asserted in a comment.
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(github): resolve owner/repo through SSH Host aliases (#10284)
Expand OpenSSH Host → HostName via ssh -G before classifying github.com
identity so PR merge works when origin is git@alias:owner/repo.git.
Transport URLs stay unchanged so IdentityFile selection is preserved.
Do not long-negative-cache indeterminate ssh -G failures.
* fix(github): harden SSH alias resolution
* fix(github): align PR source and review head origin
* fix(github): pin number-based work item open to the repo source preference
Open-by-number and details still ran the upstream-first multi-candidate PR
probe, so a fork and its upstream sharing a PR number opened different PRs
than the list and start-point paths did once #10677 pinned those to origin.
Thread repo.issueSourcePreference through dispatchWorkItem, getWorkItemDetails,
getRepoWorkItem, and getRepoWorkItemDetails. getWorkItemByOwnerRepo is left
alone: explicit owner/repo already pins identity. auto/upstream/undefined keep
the multi-candidate probe.
Co-authored-by: Orca <help@stably.ai>
* test(github): enforce origin preference in review head origin resolution
The explicit origin preference must short-circuit before any identity probe, so no remote queries should occur. Add validation to reject unexpected remotes and tighten the test assertion to verify no remote get-url calls happen at all.
* fix(github): enforce origin preference in issue open-by-number lookup
listWorkItems and getWorkItem must share preference so origin/upstream
toggles cannot disagree. Explicit origin preference now fail-closes when
origin identity is unresolved (no bare-lookup fallback), matching the
PR candidate resolution rule.
---------
Co-authored-by: Orca <help@stably.ai>
* fix(windows): fail closed on unknown PTY identity
* Gate Windows tree-kill on PTY identity verification
Only taskkill when the identity probe returns `own`; skip tree-kill for
`unknown`/`foreign`/`absent` to avoid terminating unrelated processes.
Clarifies the fail-closed behavior across agent and plain shell teardown
paths, matching the POSIX descendant-snapshot discipline.
Local and remote repos that resolve to the same git identity collapse into a
single Project, but badgeColor is per-host repo metadata. The host-refresh merge
spread the fetched project over the previous one, so whichever host's rows landed
last repainted the color — a remote Orca server sharing a project name would
overwrite the color chosen locally.
The sidebar reads the per-host repo row and stayed correct, while the
create-worktree composer reads the merged project and went grey, which is how the
mismatch surfaced.
Anchor the merged color to the local host's repo row when the project has one, so
the result no longer depends on host fetch order. Remote-only projects keep their
own host color.
* test(e2e): make Codex typing-latency harness measure real echo latency
The local Codex typing-latency spec produced meaningless numbers. Four
defects, all fixed here:
1. False-positive readiness. `/Ask Codex|OpenAI/i` matched "OpenAI's
command-line coding agent" on the *sign-in* screen, so the test went
"ready" against a login prompt and measured typing into a non-composer.
Now gated on the composer status bar (`/Context \d+% used/i`), which
only the live composer draws. Banner text is unusable: the serialized
buffer interleaves ANSI escapes through those glyphs.
2. Missing auth. The E2E profile runs an isolated HOME with a managed
CODEX_HOME that has no auth.json, guaranteeing the sign-in screen. The
launch now pins the real ~/.codex, and skips with a clear message when
auth.json is absent instead of silently measuring a login screen.
3. Measurement overhead swamped the signal. Per-key latency was measured
by polling getTerminalContent() every 5ms, so each sample was real echo
latency + full buffer serialize + CDP round-trip + poll granularity.
Measurement now happens entirely in-renderer: an in-page hook stamps
performance.now() on keydown (window capture phase, before xterm
forwards to the PTY) and again in xterm's onWriteParsed once the glyph
is in the viewport, with onRender giving a separate time-to-paint.
Samples are drained in one page.evaluate after typing ends — zero CDP
round-trips inside the measured window.
4. Thresholds were meaningless (median<150ms / worst<500ms). Replaced with
p50<35 / p95<60 / max<120, based on 10 local runs.
Also: 60 keystrokes instead of 24 with the first 10 discarded as warmup,
p50/p95/max instead of a lone median, lowercase-only input so the slash
and file-mention popups can't perturb later keys, an assertion that no
keystroke went unechoed, and a terminal dump on readiness failure.
Measured (10 local runs, headless, real Codex 0.145.0):
echo (key->parse) p50 21.6-22.6ms, p95 23.2-41.5ms, max 23.4-58.7ms
paint (key->render) p50 25.5-32.9ms, p95 34.3-49.7ms
A plain-shell control on the same probe reads p50 2.0ms / p95 3.0ms,
confirming the ~22ms is Codex composer redraw cost rather than a harness
floor — the old harness reported ~29-30ms for everything.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): widen Codex latency tail budgets and assert terminal focus
Follow-up calibration over ~20 local runs: the per-key distribution is
unimodal at p50 21.3-22.7ms with rare isolated spikes to ~90-125ms that
are not a steady-state shift. Tail budgets move to p95<80 / max<150 so
only a sustained regression fails; p50<35 still gates the steady state.
Also assert the xterm helper textarea actually took focus. One run typed
all 60 keys with only 5 parse events because focus was lost, which
previously surfaced as an opaque sample-count mismatch.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): make Zellij/TUI OSC 52 clipboard copy work by default
Zellij and other multiplexers copy via OSC 52. Empty Pc is a valid XTerm
default for clipboard, but we rejected it, and the feature defaulted off so
copy silently failed inside Zellij. Accept empty Pc as clipboard, default the
setting on (query still blocked; size capped), and surface Zellij in settings.
Closes#10567
* fix(review): make the OSC 52 default actually reach existing installs
Review fixes for #10588:
- Persistence: profiles saved under the old off default persisted `false`,
which is indistinguishable from a real opt-out, so the default flip never
reached #10567's reporter. Added the repo's one-shot stamp
(terminalAllowOsc52ClipboardDefaultedOnForAllUsers) so unmigrated profiles
flip once and a later opt-out sticks.
- Replay: reattach/cold-restore re-writes recorded PTY bytes through the same
parser, so a stale `\e]52;c;...` silently clobbered the clipboard on every
restart. Gated behind isPaneReplaying via a new resolveOsc52ClipboardGate.
- Blocked toast latches once per renderer session and could be burned by a
pre-hydration read; it now fires only for a real opt-out.
- An empty Pd decoded to '' and, with the gate default-on, silently blanked
the clipboard. Now rejected as invalid.
- Localization: en.json is bundled and the catalog beats the code fallback,
so all three copy changes were inert. Resynced across five locales.
- Corrected the empty-Pc rationale: tmux (not Zellij) emits `\e]52;;<b64>`.
* test(terminal): cover the OSC 52 gate wiring and settings copy
Extracts createOsc52OscHandler so the replay/hydration gate wiring is
covered, not just the pure gate — dropping the isReplaying getter now
fails a test instead of passing silently.
Adds catalog assertions for the two OSC 52 settings strings. Only the
toast key was pinned, so the same inert-copy regression (code fallback
edited, bundled en.json not) could still ship for the settings pane.
* docs(settings): note that the OSC 52 default only covers new profiles
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): migrate the web settings store to the OSC 52 default-on flip
The default-on flip only reached the Electron store. The web/remote client
keeps its own settings in localStorage, so a profile that persisted the old
`false` there stayed opted out — the same bug the Electron migration fixed,
in the second store.
Extract the migration into shared/osc52-clipboard-settings.ts and call it
from both stores. Also coalesce OSC 52 writes onto a microtask so a hostile
chunk of ~15-byte sequences cannot fan out into a million clipboard writes,
and latch the blocked-write toast after it renders rather than before.
* feat(terminal): tell users when the OSC 52 flip overrides their opt-out
The default-on migration cannot distinguish a deliberate opt-out from a
profile that simply never touched the setting — both persisted `false` under
the old default. Flipping everyone is the only way to fix#10567 for existing
installs, but doing it silently reverses a security choice the user made.
Arm a one-shot notice at load when the migration overrides a persisted
`false`, on both settings stores, and show it once the renderer hydrates.
Profiles that never opted out are never notified.
* fix(terminal): clear the OSC 52 notice after it renders, not before
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): keep the web OSC 52 notice armed against an unmigrated host
The host store always projects osc52ClipboardDefaultOnNoticePending, so the
plain spread in the web client's runtime UI merge overwrote an arm raised by
its own localStorage settings migration — flipping the opt-out in silence.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): stop the OSC 52 notice overclaiming, and cover it
Round-3 review fixes:
- Rename the arming predicate to osc52ClipboardDefaultOnOverridesPersistedOff.
Both stores rewrite the whole settings object on every save, so every profile
saved under the old off default holds `false` — the deliberate-opt-out cohort
is not distinguishable on disk. Name, docs and test names now say so.
- Read settings before the UI snapshot in readLocalWebUIState: getStoredSettings()
arms the notice, so reading first snapshotted a pre-arm state that callers wrote
back, erasing an arm the stamp can never raise again.
- Give the notice toast a stable id; StrictMode re-runs the effect against the
same closure, so the early return cannot catch the second pass.
- Restore guardParserHandler parity in the coalescer microtask.
- Drop the unverified Zellij claim justifying all-selections routing; that routing
predates this branch and PRIMARY routing stays an open question.
- Cover the notice hook (order, single-fire, deep-link), the armed flag reaching
disk and surviving a clear, and pin the notice catalog to its code fallbacks.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): pin OSC 52 setting discovery by product name
The migration notice says to turn it off in Terminal settings, so searching
Zellij/Grok/tmux has to find it. Also note why the OSC 52 write-back clauses
stay despite an unrelated always-true clause in the same condition.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): consume the OSC 52 notice on close, and cover the guards it relies on
The notice was cleared the moment the toast was enqueued, so a quit inside its
15s window spent the profile's only warning on a launch where nothing was ever
seen — and the settings stamp means it can never re-arm. Clear on
onAutoClose/onDismiss instead, plus explicitly in the action handler, because
sonner's action path deletes the toast without firing onDismiss.
Also closes three coverage gaps a review found:
- ui.set must accept osc52ClipboardDefaultOnNoticePending. The update schema is
strict, so dropping the key rejects the whole call rather than stripping it,
and the renderer only logs that failure — every paired client would re-toast
forever with nothing red.
- the coalescer's try/catch and .catch had no test; the rejection case needs a
plain function because vi.fn tracks settled results and hides the leak.
- pin that every selection kind (including bare `p`) lands in the system
clipboard, so routing PRIMARY separately later is a deliberate break.
Co-authored-by: Orca <help@stably.ai>
* test(web): pin that ui.get arms the OSC 52 notice when it runs the migration
readLocalWebUIState reads settings before the UI blob so the migration's arm is
in place before the snapshot every caller writes back. Seeding localStorage
after install is what makes ui.get the first settings read, and therefore what
makes swapping those two lines fail.
Co-authored-by: Orca <help@stably.ai>
* test(store): cover the OSC 52 notice clear and its hydration
The clear sets local state before persisting so a rejected ui.set cannot leave
the toast re-firing for the rest of the session; losing the persist only re-arms
the notice next launch.
Co-authored-by: Orca <help@stably.ai>
* docs(terminal): state the real residual risk of default-on OSC 52
Three comment corrections from review:
- the safety note claimed exfil was the risk; queries are blocked, so it isn't.
The actual accepted risk is execute-on-paste: decoded text goes to the
clipboard verbatim, newlines included. Filtering here would break multi-line
TUI copies, which is the feature; bracketed paste is where that is handled,
and kitty/Ghostty take the same posture.
- the coalescer bounds a flood per parse yield, not overall.
- the replay gate reads at parse time while queued live bytes are drained
before the guard engages, so a copy racing a reattach is dropped silently.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): close the four OSC 52 gaps a full revert walked through
Mutation testing found four assertions that stayed green against the very
change they were written to pin.
The notice suite passed 8/9 against a complete revert to clear-at-enqueue:
`calls[0][1][callback]?.()` is a silent no-op when the option is absent, and
the call count was already satisfied by the enqueue-clear, so nothing
separated "cleared by this callback" from "cleared earlier". Assert the
option exists and the notice is unspent before invoking it.
The stable toast id was deletable with all 9 green despite the adjacent
comment calling it load-bearing for StrictMode. Pin it.
The blocked toast's latch-after-throw fix was unproven: both orderings pass
when `toast.info` succeeds. Only a throwing first call tells them apart.
Deleting the hook call in App.tsx silenced the desktop notice with every
suite green. Pin it alongside the static Toaster import, since sonner drops
a toast enqueued before any Toaster subscribes and never replays it.
Also retone the coalescer-latch comment, which claimed the reset ordering
was load-bearing on its own; the try/catch reaches the same end, so the
test binds the pair.
All four verified green->red by mutation, then restored.
* test(terminal): cover the OSC 52 notice and its guards
Add tests pinning the static Toaster mount required to prevent notice dropout (#10567), the stable toast ID deduping StrictMode double-invokes, that the notice stays unspent on toast throws, and that flush-latch guards prevent silent consumption across error boundaries.
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* feat(source-control-ai): add {linkedIssue} recipe variable for commit and PR prompts
Custom commit-message and pull-request recipes can now reference the GitHub
issue linked to the workspace, so a template like "Fixes #{linkedIssue}" lands
the closing trailer without the user retyping the number.
- register `linkedIssue` on the commitMessage and pullRequest actions only,
with the VARIABLE_INFO entry the chip hover card requires
- substitute unconditionally via `formatLinkedIssueTemplateValue` (empty string
when nothing resolves) so the token never survives into a prompt; enrich the
draft context conditionally via `withLinkedIssueDraftContext` so unlinked
workspaces keep their existing context shape
- attach at the 7 call boundaries (runtime commit x2, runtime PR shared, IPC
commit x2, IPC PR x2); the pure git gather stays pure
- validate the renderer-supplied worktreeId against the request path and repoId
before any meta read, comparing SSH paths as raw strings so a Windows host
cannot rewrite a remote POSIX path
- built-in prompts are unchanged; no GitLab dual-read and no default trailer
* fix(source-control-ai): resolve {linkedIssue} adversarial review findings
Addresses 13 of the 14 findings from the {linkedIssue} code review
(6 minor, 8 nit, 0 critical, 0 major); Issue 5 (GitLab provider naming)
is deferred to design Open Question 3 as product expansion.
Behavior:
- Dialog previews the workspace's real linked issue instead of the
synthetic 123, in both the chip hover card and the plan preview, so an
unlinked workspace previews the `Fixes #` it will actually generate.
Settings dry-runs stay fully synthetic.
- Reject non-positive, fractional and unsafe-integer issue numbers at the
IPC resolver via a shared isLinkedIssueNumber predicate, so corrupt meta
never reaches a draft context (previously -7 rendered `Fixes #-7` and
1e21 rendered `Fixes #1e+21`).
- Fail closed on an empty-string repoId instead of skipping the cross-check.
Structure:
- Split the variable registry into source-control-ai-action-variables.ts
and re-export it, restoring max-lines headroom with no consumer churn
and no lint disable.
- Constrain withLinkedIssueDraftContext to contexts declaring linkedIssue.
- Move the misplaced shared imports into their import group.
Docs and tests:
- Document that the IPC id/path validator guards relay/CLI/future callers,
not the renderer (whose path is id-derived), and rename the three tests
that read as proof of a protection that cannot fire.
- Add PR-side coverage that was missing: three git:generatePullRequestFields
handler tests, a built-in PR prompt no-leak guard, and the runtime PR
unlinked case.
- Replace the coincidental '42' assertion with a fixture-unique sentinel.
- Type the runtime worktree fixture with satisfies, which surfaced and
fixed pre-existing drift in its git sub-object.
- Add an e2e case covering the preload -> main -> meta -> template chain.
Co-authored-by: Orca <help@stably.ai>
* fix(source-control-ai): resolve {linkedIssue} adversarial re-review findings
Addresses all 8 findings from the {linkedIssue} code re-review
(2 minor, 6 nit, 0 critical, 0 major); none deferred.
Behavior:
- Revert the variableOverrides parameter on planSourceControlTextGeneration.
Its result is a Save/Generate gate, not a preview, and the recipe it
validates is saved repo- or globally scoped -- so rendering it against the
active workspace disabled both buttons with "Command input is empty." for a
{linkedIssue}-only template on any unlinked workspace, blocking a global
settings write. Validation is synthetic again; chip previews are unchanged.
- Make the chip hover card additive instead of either/or. A supplied preview
now appends a "This workspace" sample below the description and Example
rather than replacing them, so the GitLab-empty and dangling `Fixes #`
warning survives on the two dialogs where recipes are actually authored.
basePrompt keeps its preview-only shape, where the preview is the content.
Structure:
- Drop the registry re-export from source-control-ai-actions.ts and move the
last two consumers onto source-control-ai-action-variables, so one import
path per symbol keeps a grep of the registry's consumers complete.
- Split the registry/helper suites into source-control-ai-action-variables.test.ts
so each test file mirrors its module.
Tests:
- Cover the Save/Generate gate at the canRunGeneration level for a bare
{linkedIssue} recipe on linked and unlinked workspaces, with a negative
control proving the buttons can still be disabled.
- Cover the chip hover card directly; the dialog tests mock it away.
- Guard the PR mismatched-id test with toHaveLength(1) so it cannot pass
vacuously on an unrelated early return.
- Add an unlinked-workspace e2e case (saw-issue:empty), which is what
distinguishes a real resolver from one that always returns a number.
Spec now runs green: 3 passed.
- Rename the dialog test that claimed a synthetic-fallback assertion it did
not make, and route its renders through one shared helper.
Docs are worktree-local (.gitignore:84 ignores docs/**): the design doc's
plan-preview and chip-surface claims, the manual QA rows, and both reviews'
statements about pre-existing PR-handler tests are corrected there.
* fix(source-control-ai): make the {linkedIssue} e2e guard and dialog test falsifiable
The e2e unlinked case extracted the echoed issue with `ORCA_E2E_ISSUE=(\d*)`,
which matches zero digits in front of an unexpanded `{linkedIssue}` and reported
it as `empty` — so the case that exists to catch a literal token surviving into
a prompt passed on exactly that regression. Capture the whole line instead: a
literal now arrives as `saw-issue:{linkedIssue}` and fails, verified by dropping
the substitution key for unlinked contexts and watching the case go red.
Also drop the inert `not.toContain('Command input is empty.')` assertion — that
copy is click-driven `generationError` state and this suite renders statically,
so it could never fail; the claim it reached for is carried by the plan test.
Rename two plan tests off the "plan preview" framing the design now rejects.
Local review artifacts (design doc, implementation notes, final review) were
swept to match the tree in the same pass; they are gitignored here.
* Resolve {linkedIssue} from live metadata, not cache
Resolved worktrees are cached for a second, causing commit and PR
generation to use stale linked-issue state. Hosts now implement
getWorktreeLinkedIssue to provide fresh issue metadata by worktree id,
with proper fallback for unlinked workspaces. Updates both commit
message and PR field generation paths; includes integration and e2e
coverage.
* Keep cached linkedIssue when metadata is unavailable
Return undefined from getWorktreeLinkedIssue when live metadata cannot be read
(store not ready), distinguishing it from null (unlinked). The caller now falls
back to the cached worktree value instead of treating unavailable as unlinked.
Also extract the linked-issue echo generator as a shared e2e test helper.
---------
Co-authored-by: Orca <help@stably.ai>
* Fix ssh-relay install on hosts with split shell/SFTP namespaces
On Synology DSM and similar hosts, the SSH shell and SFTP subsystem expose
different absolute paths for the same directory (e.g., /var/services/homes/alice
vs /homes/alice). The relay installer silently picked the wrong path and failed
discovery. This fix implements SFTP namespace detection: each install creates an
unguessable ownership marker and probes both namespaces to detect divergence.
When paths differ, SFTP writes redirect to the candidate namespace while shell
commands keep the canonical path. Markers are random tokens redacted from logs.
* Fix ssh-relay install on hosts with split shell/SFTP namespaces
Strengthen path validation to catch traversal and empty segments in
absolute POSIX paths, preventing security issues. Improve split-namespace
handling with comprehensive wire tests for uploads and file writes.
Ensure system SSH connections bypass namespace mapping entirely rather
than attempting incorrect retargeting.
* Move language setting to primary interface section
Language is a first-class presentation preference alongside Theme and
Zoom, so it shouldn't be buried in the Advanced disclosure. Advanced
now contains only platform-chrome controls.
* Show selected language in Interface section summary
Extracts the interface summary logic into a dedicated module that includes theme, language, and font selections. This makes the chosen language visible in the collapsed Interface section summary, addressing the discoverability issue where language was hidden until Advanced was expanded.
Preserve object identity in pane-title overlay rect state to fix infinite
re-renders. A fresh {} literal creates a new reference on every call, causing
React to treat the state as changed even when logically equivalent, triggering
the layout effect to continuously re-measure and re-set state (React #185).
Add cold-park verdict flip telemetry to diagnose crash cluster C5: records
whether parking state churns in the field for next crash bundle analysis.
Document that AddressPicker crash (cluster C6) originates in radix-ui's
SelectItem unmount cleanup, not in our component code.
Terminal and Window & Sidebar sections now expand alongside Interface by
default so users don't miss advanced settings. Sections remain independently
collapsible and each can be force-open on deep-link navigation without
collapsing siblings. Search disables toggles to prevent unexpected collapse
when query clears. Remove unused "ghostty" translation key; product name
stays untranslated for search consistency.
* test(e2e): register a real runtime host and publish the alt-screen frame as its snapshot
Two long-running scheduled-E2E failures on main were stale test setup, not
product defects.
`onboarding.spec.ts:420` seeded the Active Server by faking a runtime
environment in the renderer store and writing `activeRuntimeEnvironmentId`
through the generic `settings:set` IPC. Since #10011 that setter strips the
key, and the dedicated `settings:set-active-runtime-environment-preference`
handler resolves the id against the main-process environment store — CI
logged `RuntimeEnvironmentStoreError: Unknown environment: env-e2e` from
`runtimeEnvironments:subscribe`/`:call` alongside the assertion failure.
Register the host for real via `runtimeEnvironments:addFromPairingCode`
(offline; no live server) and write the preference through its own channel.
`terminal-tab-switch-visual-restore.spec.ts:604` wrote alt-screen frames
straight into the renderer's xterm, so those bytes never transited the PTY
and main's model could not contain them. On cycle 0 the freshly spawned
shell still has queued startup output, so hiding the pane makes main's
hidden-delivery gate drop bytes and latch a reveal restore, which repaints
main's snapshot over the fabricated frame; later cycles run against an idle
shell and survive. Arm the existing `setHiddenSnapshotOverride` seam (already
used by sibling tests in this file) with the same frame so the live-write and
restore paths render identically, and keep the `markerPresent` assertion.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): keep the alt-screen restore path observable
Numbering the snapshot frame one higher than the live-written frame keeps
the marker assertion path-agnostic while leaving the frame number on
screen as the signal for which path painted. An unrecognised frame now
fails, and the per-cycle path is recorded rather than asserted because
which cycles latch a restore is load-dependent.
Frame authoring and readback move to a helper module; the additions
crossed the spec's max-lines cap.
Co-authored-by: Orca <help@stably.ai>
* Escape regex metacharacters in alt-screen marker pattern
Marker is treated as a literal string, so escape regex metacharacters
to prevent them from being interpreted as regex syntax.
---------
Co-authored-by: Orca <help@stably.ai>
* fix(worktree): collapse duplicate "Local Mac" run targets in the host picker
A linked worktree added as its own project projects a second ready host
setup on the same project+host, so the run-target picker rendered N
identical "Local Mac" rows differing only by path. Only the first was
reachable — resolveWorkspaceCreationTarget takes the first project+host
match — so the extras pointed at paths that may no longer exist.
- Dedupe ready setup options by host in the picker (display fix for
profiles that already hold duplicates).
- Canonicalize a stale draft's setup id to the setup the picker shows,
so the displayed path is the path the workspace is created in.
- Reject a linked worktree at repos:add when its main checkout is
already tracked, preventing new duplicates.
* fix(worktree): only dedupe a linked worktree against a git main checkout
Review follow-up: the repos:add guard matched any tracked repo on the main
checkout path, including a folder-kind record. A folder repo does not
project onto the same project as the git worktree, so matching it would
suppress a legitimate add without deduping anything.
* fix(editor): preserve Markdown focus handoffs
* fix(editor): scope focus requests to panes via viewStateId
When opening a file to focus it, tag the pending request with the pane's
viewStateId. This prevents split siblings from claiming each other's requests
and stops later remounts from stealing focus. Both Monaco and rich-markdown
editors now retire requests on mount.