mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 08:03:12 +00:00
837ea3feecd2bf3fa4bd7151a7ddaf8153adf6e2
509
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c7850eb59c | fix(terminals): prevent retired workers from cold-resuming (#13708) | ||
|
|
090678a32b | [Perf-SS] Keep reattach live queues linear during replay (#13478) | ||
|
|
a70291a918 | perf(orchestration): index mutation receipt capacity (#13647) | ||
|
|
1b02cedfff |
fix(tabs): keep widths stable during title updates (#11476)
* fix(tabs): keep widths stable during title updates * review(tabs): reduce the width rule to the definite width and harden its tests Why: once the tab container has a definite width the shrink-wrapped strip never has free space, so flex-grow and max-w-[280px] were unreachable and only duplicated the 180/220 numbers across two properties. Measured widths, strip scrollWidth and clientWidth are identical to the flex-based version across four window widths, five tab counts and short/long titles. Tests: pin the width classes literally so the guarantee cannot be edited away via the constant, and guard the e2e check against passing vacuously on a saturated strip. --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
6b9852af2b |
perf(terminal): remove output-driven WebGL atlas resets (#13657)
* perf(terminal): stop clearing glyph atlas on output * test(terminal): gate output atlas reset removal * test(terminal): harden atlas recovery gates |
||
|
|
d50adec2d2 |
feat(ai-vault): isolate scanning from terminal workloads (#13411)
* feat(ai-vault): isolate scanning in service processes * fix(ai-vault): retire idle service processes * fix(ai-vault): discard unverified cache processes * fix(ai-vault): clear relay sidecar cancel watchdog on acknowledgement A cancelled relay call is settled before its 2s cancel watchdog is armed, so the acknowledgement path bailed out of settle() before clearing the timer. The watchdog then faulted a healthy sidecar two seconds after every aborted scan, killing whatever request had since become active. * fix(ai-vault): clear the pending restart before scheduling another recordFault overwrote this.timer, stranding a restart that dispose() could no longer cancel. * refactor(ai-vault): drop the orphaned first-prompt IPC wrapper session-first-user-prompt-handler.ts now owns this entry point and routes through the service; the copy left in the read module had no callers. * fix(ai-vault): retry a faulted cold start before surfacing it A slow first start surfaced a raw 'did not become ready' error to the caller even though the supervisor was already respawning. Requeue an unsent call once onto the scheduled respawn instead. Also stop arming the cancellation watchdog for a call the child never received: no acknowledgement is coming, so it killed a healthy service and stalled the lane. Invalidation bookkeeping and ready-waiter construction move to the state module to stay under the max-lines cap. * fix(ai-vault): give relay title reads their own lane Before this branch the relay read title files directly, concurrently with scans. Routing both through one sidecar lane put title resolution behind a list scan that may run up to 130s, so SSH tab titles could lag minutes behind. Split cache and interactive lanes in both the relay client and the sidecar entry, mirroring the desktop service. Also: clear the ready deadline on fault, so a sidecar that dies before ready cannot fault its healthy replacement five seconds later; retry an unsent call once across a respawn; and skip the cancellation watchdog for a call the sidecar never received. Restart/circuit bookkeeping moves to its own module, mirroring the desktop policy, to stay under the max-lines cap. * fix(ai-vault): degrade relay title resolution on sidecar failure listSessions already returns a host issue when the sidecar is unavailable; titles propagated the raw RPC error instead. Return no titles so callers fall back to preview text, and keep cancellation propagating. * fix(ai-vault): scrub the service child environment The children are forked with a 384 MiB heap cap and no loader, but both spawn sites handed them the full parent environment, so an exported NODE_OPTIONS silently raised the cap or --require'd code into them. Allowlist both, following the plugin worker. The desktop child keeps the eleven agent-root overrides it resolves its own roots from; the relay sidecar takes remoteHome and hostPlatform from its init message and so needs none of them. Both children share one priority module while they share this one. * fix(ai-vault): soft-disable relay vault when the service is missing A missing service threw out of the constructor, so a Vault wiring bug would abort relay startup and take every PTY on the host with it. The unsupported-platform branch three lines above already treats a Vault failure as a soft disable; do the same here. Threading the service through the two handlers instead of a field also retires the definite-assignment assertion the throw was propping up. * fix(ai-vault): drain consumed cache invalidations invalidatedPaths was re-applied in every request's finally and never drained, so once N paths had been invalidated every later request paid N evictions for the life of the process; the 4096 cap only bounded how bad that got. The re-apply exists to cover a read that overlapped the invalidation, so drain once nothing is executing. Clearing unconditionally would drop the re-apply for a request still running on the other lane. * fix(ai-vault): keep a busy child through slow invalidation acks invalidate() reused the 5s ready budget as its acknowledgement deadline and killed the child on expiry, so a delete issued during a large scan could kill a healthy process mid-scan and burn a slot toward the restart circuit. Fault only when nothing is executing. Fork IPC ordering already puts the invalidation ahead of any later request, so a busy child owes no ack here, and the 130s/15s request deadlines still catch a wedged one. The start-retry predicate moves to the state module to stay under the line cap, matching the shape the relay client already uses. * fix(ai-vault): report a failed local scan as a host issue A local-scope scan let its error escape to the renderer, which paints it over the session list. Service supervision now produces those errors, so "AI Vault service restart circuit is open." replaced the list. Route local scope through the degradation the all-hosts leg and every SSH leg already use, so it lands as a retryable host issue row instead. Same result shape either way, so no IPC or wire contract changes. * test(ai-vault): cover the relay restart circuit transitions The relay policy shipped without tests. Pin both circuit edges, the aging-out case, the forced-refresh reopen the relay has and the desktop does not, and the backoff schedule. * fix(ai-vault): keep the OpenCode roots in the service child env The scrubbed allowlist dropped XDG_DATA_HOME and OPENCODE_DB, which the child reads to locate the OpenCode store and database. The pre-PR worker thread inherited them, so a user who sets either lost every OpenCode session. * test(ai-vault): anchor the service spawn env assertion |
||
|
|
ec7e3ea477 |
fix(terminal): prevent paired activity renderer starvation (#13508)
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
c0c893d171 |
fix(pty): bound cooked reply queue (#13422)
* fix(pty): bound cooked reply queue * fix(pty): bound cooked reply queue Implement bounded storage for cooked-echo-safe replies: 64 pending replies and 4096 UTF-16 code units. Shed oldest replies on overflow, never ordinary input. Add drain failure containment with generation fencing to prevent stale operations from clearing fresh input after clear() reuse. * fix(pty): report pty id in drain failures When the async drain yields, the owner may rebind to a different PTY before the failure surfaces. Pass the failing pty id so the transport can ignore stale failures from a rebound owner. Also tighten the pending reply queue size bound to prevent half-written entries. |
||
|
|
016df33f00 |
fix(github): complete PR reactions for CodeRabbit reviews (#13456)
* feat(github): add PR comment reaction controls * feat(github): add full PR comment reaction picker * fix(github): cover all reactable PR comment paths * fix(github): reconcile comment reactions with main * fix(github): preserve focus on failed reaction removal |
||
|
|
7dce0442ab |
Add Caffeinate controls to the status bar (#13480)
* feat: add caffeinate status controls * refactor: compact caffeinate status * fix: harden caffeinate readiness |
||
|
|
158212b8b3 |
feat(github): add PR comment reactions (#13470)
* feat(github): add PR comment reactions * fix(github): harden comment reaction updates --------- Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com> |
||
|
|
d9e84d88ef | fix(persistence): salvage corrupt workspace session entries (#13431) | ||
|
|
4cca31f36f | fix(quick-open): compact Cmd+P file rows (#13454) | ||
|
|
9ccec550c3 |
Add emoji picker to workspace editing and search (#13429)
* Add emoji picker to workspace editing and search * fix(sidebar): scope sticky headers by host * fix: address emoji picker review findings |
||
|
|
b075a95b06 |
Strip liveness gate from AI Vault session delete (#13279)
* Strip liveness gate from AI Vault session delete Delete now requires only path validation + user confirmation — no process roster, no liveness check, no quiescence, no ownership ledger. Co-authored-by: Orca <help@stably.ai> * Remove obsolete AI Vault liveness delete reliability gate Session delete no longer checks process liveness, so drop the manifest entry that still referenced the deleted test files. * minor fix --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
774bbc788f |
fix(terminal): preserve OSC 8 links across cold parking (#13382)
* fix(terminal): preserve OSC 8 links across cold parking * test(terminal): make OSC 8 e2e cross-platform * test(terminal): focus OSC restore e2e on activation |
||
|
|
d23dda6d48 |
Move Linear issue view to localStorage (#13391)
Prevent older paired hosts from discarding the entire taskResumeState when they encounter the strict linearIssueView field, which was causing silent loss of github and jira queries during remote pairing. Layout, grouping, ordering, and per-workspace filters are now device-local. |
||
|
|
9aa9d45467 |
test(e2e): cover a macOS system key remap reaching the terminal (#11170) (#13314)
* fix(terminal): show a preedit the IME resumes without a compositionstart Typing 2-Set Korean shows committed syllables but not the in-progress jamo, so the user composes each syllable blind. Long-standing hole in the vendored terminal library, not a regression: the same test fails identically against the bundle this branch starts from. The `.active` class that CSS keys `display: block` off is added only in `compositionstart` and dropped in `_finalizeComposition`. Some IMEs (observed on Windows/WSL Korean) resume a composition with a bare `compositionupdate` and no second `compositionstart`, by which point `compositionend` has already hidden the overlay, so the resumed preedit is written into a hidden element and never positioned. `updateCompositionElements` also early-returned on `!_isComposing`, so it would not lay the overlay out either. Re-show the overlay on an update that carries data, and key the layout guard on the shown overlay instead. `_isComposing` is deliberately left alone, so no commit bookkeeping changes and `onData` stays byte-identical. The two guards are equivalent on every pre-existing path: `compositionstart` sets both, `_finalizeComposition` clears both. The bundle hunks are the same two edits applied to the shipped minified output; the sourcemaps are carried through unchanged. * test(terminal): prove the resumed-preedit fix against a recorded Windows capture The synthetic test pins the shape; this replays events a real Microsoft Korean IME emitted on Windows/WSL. The capture holds three compositionupdates that resume a composition with no second compositionstart — the exact ordering that wrote the preedit into a hidden overlay. Without the fix all three report shown:false; with it all three are visible. Fixture derived from the sealed 11919-windows-wsl-current capture, which is read-only and unmodified. Co-authored-by: Orca <help@stably.ai> * test(terminal): stop the recorded Hangul fixture pinning a derivation artifact The capture logs each event twice — a dispatch record and a batched next-frame re-log. Deriving from both replayed every event twice, which made three compositionupdates appear to land after a session had ended. Filtered to dispatch records the capture holds zero resumes and 11 balanced sessions, so the previous toHaveLength(3) was pinning an artifact of the derivation. Re-scoped to what the capture does prove: the preedit stays visible across all 37 real updates. Verified by reverting the patch that this passes either way, so it is coverage and the synthetic test remains the discriminator. Both facts are now stated in the file. Co-authored-by: Orca <help@stably.ai> * fix(terminal): restore the preedit visibility patch onto its own branch The previous commit accidentally reverted it: checking main's patch and lockfile into the worktree to test whether a test discriminates also stages them, so the commit that followed swept them up. Co-authored-by: Orca <help@stably.ai> * fix(terminal): claim printable keydowns structurally so committed text survives Co-authored-by: Orca <help@stably.ai> * chore(reliability-gates): retarget the IME forwarding gate after the allowlist removal The gate listed terminal-ime-input-source.test.ts, which went with the input-source allowlist. Points at the substituted-text commit test instead, which covers what the gate is actually protecting: text committed outside a composition session reaching the pty exactly once. Co-authored-by: Orca <help@stably.ai> * docs(terminal): record why withholding a claimed keydown needs no timer The predicate withholds a keydown's byte until the commit arrives, so a key the IME eats without committing would be dropped. Measured across the recorded corpus that case does not occur, and the browser marks IME-owned presses on the keydown itself. Both facts belong next to the predicate rather than only in a handoff note, since the obvious fix for the imagined gap is a timer, and a timer here once wrote a newline the user never typed. Co-authored-by: Orca <help@stably.ai> * test(terminal): pin the kitty all-keys-as-escape-codes hole explicitly Flag 8 asks for every printable key as an escape code; this path sends the committed text raw instead. That is a deliberate trade, not an oversight, but it was untested — the suite only covered the disambiguate flag. Pinning it makes the choice visible and records the gate to use if it ever needs closing. Co-authored-by: Orca <help@stably.ai> * fix(terminal): keep the kitty key-release report for presses that reached the pty Claiming the keyup unconditionally suppressed xterm's release report. That was sized for the old design, which claimed only a short punctuation list; the structural claim takes every printable keydown, so on macOS an app that negotiated kitty report_event_types stopped seeing releases for ordinary typing and would treat every printable key as held down. Suppress the release only when the press put nothing on the wire — swallowed by the input source, or owned by a composition transaction. xterm emits nothing from keyup unless kitty report_event_types (or win32 input mode) is on, so letting it through is inert everywhere else. Co-authored-by: Orca <help@stably.ai> * test(e2e): assert IME preedit geometry headlessly for Korean and CJK input Both IME defects that shipped and were reverted walked through a suite of ~3000 passing assertions, because every one of them was about bytes reaching the PTY. A preedit rendered into a hidden overlay satisfies all of them while the user composes blind. The real-geometry coverage that would have caught it existed but was headful, env-gated and macOS-only, so it never ran in CI. Drives composition through CDP Input.imeSetComposition instead of a native input source, which removes the accessibility grant and the system input source that forced the headful gate. The suite runs in the normal headless project in about 55s serially, and asserts the composition overlay's real bounding rect — the one property an overlay clipped to max-width:0 cannot fake and a DOM emulator cannot produce. Three tests are red on main and marked test.fail() so they stay visible in CI and flip loud when their fix lands: the preedit resumed by a bare compositionupdate, and full-width punctuation and digits committed from a keydown that still carries the ASCII layout key. Co-authored-by: Orca <help@stably.ai> * test(e2e): drop the known-broken markers now the stack closes all three Validated on real macOS hardware: with the two fixes below this layer, all three report "Expected to fail, but passed". Korean preedit renders at non-zero geometry through every jamo, and an Apple pinyin source sends ef bc 8c e3 80 82 to the pty where main sends ASCII. Worth recording why the punctuation case looked green on main once: an input source whose id happens to contain an allowlist term, as Sogou's does, satisfies the old gate. Correctness there depended on which IME the user had selected. Co-authored-by: Orca <help@stably.ai> * test(e2e): cover the Linux and Windows IME ownership shapes headlessly The ten headless IME specs on this branch all decided ownership through a macOS user-agent override, so the two platforms whose failure mode is a *dropped* character rather than a downgraded one had no coverage at all, and the one Windows-recorded trace already in the suite was replayed under whichever policy the runner happened to report — macOS locally, Linux on the CI shards. Adds four Linux specs and two Windows specs, every one of them replaying a native capture rather than a hand-authored ordering: - IBus/X11 Hangul mixed with literal ASCII. Its `compositionend` is EMPTY and the syllable arrives afterwards as a bare `insertText`, so reading the commit off `compositionend.data` — which the Windows capture rewards — drops every syllable on this framework. - fcitx5/Wayland Hangul. No keydown at all for a composing key, not even 229, and physically wrong `code` values on the literal keys. Any ownership rule reading 229 or `code` fails here. - Numeric pinyin candidate selection under both frameworks, with the ordinary digit kept as the negative control, so the two directions are pinned against each other rather than separately. - Windows Microsoft Korean captured with real scan codes, including the two lines committed with Shift held. Each asserts both sides of the boundary: the preedit's real geometry at every frame the user would see, and the exact byte stream the native run put on the PTY. The recorded `onData` the Windows/WSL fixture already carried is now asserted instead of sitting unused. Chinese moves up to first-class alongside Korean: pinyin preedit width is now pinned the way the Japanese phrase already was, and full-width punctuation is covered in the composition-session shape the Windows and Linux frameworks use, not only the macOS insertText shape. Two harness fixes fell out of the recorded traces and are why the IBus one passes. The replay applied each event's recorded textarea state *after* dispatch, one event too late for the handlers that read `textarea.value`; and it left a task boundary between `compositionend` and the `input` carrying the commit, which Chromium never inserts, letting xterm's deferred finalizer settle against a textarea the committed text had not reached yet. Co-authored-by: Orca <help@stably.ai> * test(e2e): replay the recorded macOS IME shapes instead of only synthesising them The macOS coverage on this branch drives Chromium composition through CDP, which is genuine but hand-ordered, so it could not assert the one property that decides the macOS rule: a composing keydown arrives with keyCode 229 while `key` is still the single translated character the input source produced — `ㅎ`, not `Process` — which is indistinguishable by length from an ordinary printable key. Three native captures were sitting unused in the evidence set. Adds a recorded 2-Set Korean session, including the syllable boundary where one composition closes and the next opens with no keydown between them, asserted against its own recorded byte stream. Adds the third failure mode, which had no coverage in any shape: an abandoned preedit leaking to the shell. Pinyin and Cangjie both backspace a composition away to nothing, and the assertion is not "the right bytes" but "no bytes". Both cancellation captures continue with a literal `ordinary` typed as bare keydowns, the recorder's own negative control. That tail carries no `input` events because the build it was captured on produced the byte from the keydown itself, so replaying it would measure the recorder rather than the product; the specs cut at the `compositionend` and say so. Co-authored-by: Orca <help@stably.ai> * test(e2e): promote the non-allowlist input-source punctuation spec to the suite Qingg matches none of the terms the pre-structural build enumerated, so on that build its bypass never installs. It is the one arm no headless spec can express, and the only test here that the pre-structural build cannot pass. Promoted from scratch with four changes, each forced by a measurement rather than by taste: - A non-attached input method is a refusal, not a negative result. macOS attaches per app instance and the attach can simply fail — 3 of 6 instances under exclusive host access, and re-selecting the source did not recover one of them across 9 keystrokes. That is now `test.skip()` with a reason naming the rerun, not a thrown error, and the suite does not gate on a fully green session. - Attachment is probed with a LETTER. Punctuation substitution emits no compositionstart and no keyCode 229 even under a fully attached source, so at the keydown it is indistinguishable from having no source at all. The punctuation arms are judged on PTY bytes alone. - The ASCII-layout control is now part of the spec rather than a side experiment. Without it a build that rewrote every `.` into `。` unconditionally would pass the Qingg arm and be badly wrong. - The assertion runs by default instead of behind a strict-mode flag, and gained a non-vacuity check: the input source must have committed something. That is the sharp end of the mechanism — on the old build the DOM carries only keydown and keyup, so nothing is committed at all and the character is destroyed before the source is asked. The verdict stays an equality between two measurements, never a comparison against a hardcoded glyph, so it holds whatever punctuation mode the operator's input source happens to be in. It reads `beforeinput`, not `input`: the forwarder consumes `input` in the capture phase on the pane element, so a probe on the helper textarea never sees it and a strict run fails with correct bytes underneath. Co-authored-by: Orca <help@stably.ai> * feat(terminal): encode IME commits as CSI-u under the all-keys kitty flag A pane that negotiates `report_all_keys_as_escape_codes` (bit 3) asked for every printable key as a CSI-u report. The commit path wrote IME-committed text raw, so such a pane got a legacy byte stream it had declined. That predicate has no IME-specific condition, so it affected every macOS user in such a pane, not just CJK users. Encode the press that produced the commit instead, reusing xterm's own kitty encoder rather than hand-rolling CSI-u. `claimKeyEvent` is untouched: still unconditional, still structural, still no kitty read on the keydown. The flag read happens once per commit. The gate is bit 3 alone. Flags 1/2/4/16 leave printable keys as text, so panes negotiating only those keep receiving substituted characters; gating on "kitty active" would strip the substitution from every pane that negotiates anything. Known limit, pinned by test: the report carries the physical key's codepoint, not the committed glyph. Bit 3 is the app declaring it does not want text, and bit 4 is how it asks for text back — but xterm's encoder derives that text field from the same `key` it derives the keycode from, so carrying the committed glyph needs an encoder change, not a wider gate. * fix(terminal): report a held key's repeats as REPEAT under the kitty flags The commit encoder never passed an event type, so xterm's encoder applied its PRESS default to every auto-repeat keydown. A pane negotiating report_event_types alongside bit 3 saw one held key as N separate strikes. Carry the keydown's `repeat` on the claimed press and map it to the protocol's REPEAT. The event type only reaches the wire when report_event_types is negotiated, so this is inert for panes that asked only for bit 3. Co-authored-by: Orca <help@stably.ai> * test(e2e): cover a macOS system key remap reaching the terminal (#11170) Co-authored-by: Orca <help@stably.ai> * chore: drop non-mergeable IME e2e scratch files --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
050ad3b32f |
test(e2e): normalise the pty line terminator so the IME specs can run on Windows (#13313)
* fix(terminal): show a preedit the IME resumes without a compositionstart Typing 2-Set Korean shows committed syllables but not the in-progress jamo, so the user composes each syllable blind. Long-standing hole in the vendored terminal library, not a regression: the same test fails identically against the bundle this branch starts from. The `.active` class that CSS keys `display: block` off is added only in `compositionstart` and dropped in `_finalizeComposition`. Some IMEs (observed on Windows/WSL Korean) resume a composition with a bare `compositionupdate` and no second `compositionstart`, by which point `compositionend` has already hidden the overlay, so the resumed preedit is written into a hidden element and never positioned. `updateCompositionElements` also early-returned on `!_isComposing`, so it would not lay the overlay out either. Re-show the overlay on an update that carries data, and key the layout guard on the shown overlay instead. `_isComposing` is deliberately left alone, so no commit bookkeeping changes and `onData` stays byte-identical. The two guards are equivalent on every pre-existing path: `compositionstart` sets both, `_finalizeComposition` clears both. The bundle hunks are the same two edits applied to the shipped minified output; the sourcemaps are carried through unchanged. * test(terminal): prove the resumed-preedit fix against a recorded Windows capture The synthetic test pins the shape; this replays events a real Microsoft Korean IME emitted on Windows/WSL. The capture holds three compositionupdates that resume a composition with no second compositionstart — the exact ordering that wrote the preedit into a hidden overlay. Without the fix all three report shown:false; with it all three are visible. Fixture derived from the sealed 11919-windows-wsl-current capture, which is read-only and unmodified. Co-authored-by: Orca <help@stably.ai> * test(terminal): stop the recorded Hangul fixture pinning a derivation artifact The capture logs each event twice — a dispatch record and a batched next-frame re-log. Deriving from both replayed every event twice, which made three compositionupdates appear to land after a session had ended. Filtered to dispatch records the capture holds zero resumes and 11 balanced sessions, so the previous toHaveLength(3) was pinning an artifact of the derivation. Re-scoped to what the capture does prove: the preedit stays visible across all 37 real updates. Verified by reverting the patch that this passes either way, so it is coverage and the synthetic test remains the discriminator. Both facts are now stated in the file. Co-authored-by: Orca <help@stably.ai> * fix(terminal): restore the preedit visibility patch onto its own branch The previous commit accidentally reverted it: checking main's patch and lockfile into the worktree to test whether a test discriminates also stages them, so the commit that followed swept them up. Co-authored-by: Orca <help@stably.ai> * fix(terminal): claim printable keydowns structurally so committed text survives Co-authored-by: Orca <help@stably.ai> * chore(reliability-gates): retarget the IME forwarding gate after the allowlist removal The gate listed terminal-ime-input-source.test.ts, which went with the input-source allowlist. Points at the substituted-text commit test instead, which covers what the gate is actually protecting: text committed outside a composition session reaching the pty exactly once. Co-authored-by: Orca <help@stably.ai> * docs(terminal): record why withholding a claimed keydown needs no timer The predicate withholds a keydown's byte until the commit arrives, so a key the IME eats without committing would be dropped. Measured across the recorded corpus that case does not occur, and the browser marks IME-owned presses on the keydown itself. Both facts belong next to the predicate rather than only in a handoff note, since the obvious fix for the imagined gap is a timer, and a timer here once wrote a newline the user never typed. Co-authored-by: Orca <help@stably.ai> * test(terminal): pin the kitty all-keys-as-escape-codes hole explicitly Flag 8 asks for every printable key as an escape code; this path sends the committed text raw instead. That is a deliberate trade, not an oversight, but it was untested — the suite only covered the disambiguate flag. Pinning it makes the choice visible and records the gate to use if it ever needs closing. Co-authored-by: Orca <help@stably.ai> * fix(terminal): keep the kitty key-release report for presses that reached the pty Claiming the keyup unconditionally suppressed xterm's release report. That was sized for the old design, which claimed only a short punctuation list; the structural claim takes every printable keydown, so on macOS an app that negotiated kitty report_event_types stopped seeing releases for ordinary typing and would treat every printable key as held down. Suppress the release only when the press put nothing on the wire — swallowed by the input source, or owned by a composition transaction. xterm emits nothing from keyup unless kitty report_event_types (or win32 input mode) is on, so letting it through is inert everywhere else. Co-authored-by: Orca <help@stably.ai> * test(e2e): assert IME preedit geometry headlessly for Korean and CJK input Both IME defects that shipped and were reverted walked through a suite of ~3000 passing assertions, because every one of them was about bytes reaching the PTY. A preedit rendered into a hidden overlay satisfies all of them while the user composes blind. The real-geometry coverage that would have caught it existed but was headful, env-gated and macOS-only, so it never ran in CI. Drives composition through CDP Input.imeSetComposition instead of a native input source, which removes the accessibility grant and the system input source that forced the headful gate. The suite runs in the normal headless project in about 55s serially, and asserts the composition overlay's real bounding rect — the one property an overlay clipped to max-width:0 cannot fake and a DOM emulator cannot produce. Three tests are red on main and marked test.fail() so they stay visible in CI and flip loud when their fix lands: the preedit resumed by a bare compositionupdate, and full-width punctuation and digits committed from a keydown that still carries the ASCII layout key. Co-authored-by: Orca <help@stably.ai> * test(e2e): drop the known-broken markers now the stack closes all three Validated on real macOS hardware: with the two fixes below this layer, all three report "Expected to fail, but passed". Korean preedit renders at non-zero geometry through every jamo, and an Apple pinyin source sends ef bc 8c e3 80 82 to the pty where main sends ASCII. Worth recording why the punctuation case looked green on main once: an input source whose id happens to contain an allowlist term, as Sogou's does, satisfies the old gate. Correctness there depended on which IME the user had selected. Co-authored-by: Orca <help@stably.ai> * test(e2e): cover the Linux and Windows IME ownership shapes headlessly The ten headless IME specs on this branch all decided ownership through a macOS user-agent override, so the two platforms whose failure mode is a *dropped* character rather than a downgraded one had no coverage at all, and the one Windows-recorded trace already in the suite was replayed under whichever policy the runner happened to report — macOS locally, Linux on the CI shards. Adds four Linux specs and two Windows specs, every one of them replaying a native capture rather than a hand-authored ordering: - IBus/X11 Hangul mixed with literal ASCII. Its `compositionend` is EMPTY and the syllable arrives afterwards as a bare `insertText`, so reading the commit off `compositionend.data` — which the Windows capture rewards — drops every syllable on this framework. - fcitx5/Wayland Hangul. No keydown at all for a composing key, not even 229, and physically wrong `code` values on the literal keys. Any ownership rule reading 229 or `code` fails here. - Numeric pinyin candidate selection under both frameworks, with the ordinary digit kept as the negative control, so the two directions are pinned against each other rather than separately. - Windows Microsoft Korean captured with real scan codes, including the two lines committed with Shift held. Each asserts both sides of the boundary: the preedit's real geometry at every frame the user would see, and the exact byte stream the native run put on the PTY. The recorded `onData` the Windows/WSL fixture already carried is now asserted instead of sitting unused. Chinese moves up to first-class alongside Korean: pinyin preedit width is now pinned the way the Japanese phrase already was, and full-width punctuation is covered in the composition-session shape the Windows and Linux frameworks use, not only the macOS insertText shape. Two harness fixes fell out of the recorded traces and are why the IBus one passes. The replay applied each event's recorded textarea state *after* dispatch, one event too late for the handlers that read `textarea.value`; and it left a task boundary between `compositionend` and the `input` carrying the commit, which Chromium never inserts, letting xterm's deferred finalizer settle against a textarea the committed text had not reached yet. Co-authored-by: Orca <help@stably.ai> * test(e2e): replay the recorded macOS IME shapes instead of only synthesising them The macOS coverage on this branch drives Chromium composition through CDP, which is genuine but hand-ordered, so it could not assert the one property that decides the macOS rule: a composing keydown arrives with keyCode 229 while `key` is still the single translated character the input source produced — `ㅎ`, not `Process` — which is indistinguishable by length from an ordinary printable key. Three native captures were sitting unused in the evidence set. Adds a recorded 2-Set Korean session, including the syllable boundary where one composition closes and the next opens with no keydown between them, asserted against its own recorded byte stream. Adds the third failure mode, which had no coverage in any shape: an abandoned preedit leaking to the shell. Pinyin and Cangjie both backspace a composition away to nothing, and the assertion is not "the right bytes" but "no bytes". Both cancellation captures continue with a literal `ordinary` typed as bare keydowns, the recorder's own negative control. That tail carries no `input` events because the build it was captured on produced the byte from the keydown itself, so replaying it would measure the recorder rather than the product; the specs cut at the `compositionend` and say so. Co-authored-by: Orca <help@stably.ai> * test(e2e): promote the non-allowlist input-source punctuation spec to the suite Qingg matches none of the terms the pre-structural build enumerated, so on that build its bypass never installs. It is the one arm no headless spec can express, and the only test here that the pre-structural build cannot pass. Promoted from scratch with four changes, each forced by a measurement rather than by taste: - A non-attached input method is a refusal, not a negative result. macOS attaches per app instance and the attach can simply fail — 3 of 6 instances under exclusive host access, and re-selecting the source did not recover one of them across 9 keystrokes. That is now `test.skip()` with a reason naming the rerun, not a thrown error, and the suite does not gate on a fully green session. - Attachment is probed with a LETTER. Punctuation substitution emits no compositionstart and no keyCode 229 even under a fully attached source, so at the keydown it is indistinguishable from having no source at all. The punctuation arms are judged on PTY bytes alone. - The ASCII-layout control is now part of the spec rather than a side experiment. Without it a build that rewrote every `.` into `。` unconditionally would pass the Qingg arm and be badly wrong. - The assertion runs by default instead of behind a strict-mode flag, and gained a non-vacuity check: the input source must have committed something. That is the sharp end of the mechanism — on the old build the DOM carries only keydown and keyup, so nothing is committed at all and the character is destroyed before the source is asked. The verdict stays an equality between two measurements, never a comparison against a hardcoded glyph, so it holds whatever punctuation mode the operator's input source happens to be in. It reads `beforeinput`, not `input`: the forwarder consumes `input` in the capture phase on the pane element, so a probe on the helper textarea never sees it and a strict run fails with correct bytes underneath. Co-authored-by: Orca <help@stably.ai> * test(e2e): normalise the pty line terminator so IME specs can run on Windows A Unix pty's line discipline turns the terminal's CR into a bare LF, but Windows ConPTY hands the reading process CRLF. Every spec compares against a recorded line ending in LF, so on Windows 16 of 21 failed on the terminator alone while the IME payload bytes ahead of it were byte-for-byte correct. Verified on real Windows: the renderer-to-pty boundary assertion passed there, so the app writes a bare CR and the LF is added by the console downstream of anything we control. Normalising in the reader keeps the specs asserting the IME bytes unchanged rather than loosening them. Co-authored-by: Orca <help@stably.ai> * chore: drop non-mergeable IME e2e scratch files --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
45c1cb979a |
fix(orchestration): release context-only dispatches (#13376)
* fix(orchestration): release context-only dispatches Refs #13005 * test(orchestration): align PTY readiness timeout --------- Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com> |
||
|
|
bd9addb449 | fix(tasks): restore GitHub page and scroll position on reopen (#13096) | ||
|
|
970696a008 |
fix(sidebar): show Cursor rows and stop a stray "claude" title hijacking OpenCode (#12466)
* fix(sidebar): show Cursor rows and stop a stray "claude" title hijacking OpenCode Two defects in the same title-resolution path. **#10258** — Cursor's only native OSC title is the literal `cursor agent`, which both title trackers dropped unconditionally. A hookless Cursor pane therefore had neither a status entry nor any title carrying Cursor identity, so the worktree card showed nothing at all. **#8940** — two owner-blind paths let an incidental `claude` token anywhere in an OpenCode session or task title outrank the pane's known owner, so the tab icon and sidebar row flipped to Claude Code. #10258: let the literal through exactly once as identity, so a restored or mobile tab keeps its Cursor row instead of vanishing. #8940: require an *identity frame* — after stripping status decoration the title must PRESENT Claude, not merely mention it — before a Claude title may reclaim a pane from its prior identity, and make the sidebar row builder owner-aware. > These two are in one PR because they share the `ownerAgentType` plumbing through `buildTitleDerivedAgentRow` — split apart, neither half compiles on its own. Fixes #10258 Fixes #8940 Co-authored-by: Orca <help@stably.ai> * test(e2e): add recordable proof for sidebar-agent-row-identity Fails on origin/main, passes on this branch. Test: sidebar keeps a Cursor pane visible and an OpenCode pane out of Claude Code hands Co-authored-by: Orca <help@stably.ai> * fix(terminal): preserve restored Cursor identity * test(terminal): cover restored Cursor redraw suppression * refactor(terminal): tighten Cursor identity handling and Claude frame matching Review follow-ups on the title-resolution path: - pty-transport dropped a native Cursor literal that main emits whenever a non-Cursor title preceded it, re-introducing the #10258 blank row in the renderer path. The pre-filter now projects the predecessor the drain will actually see, and defers to the drain gate while facts are still queued. - applyTrackedPtyTitle threaded the cursor flag through 12 sites, including ptyRecordChanged bookkeeping the sole caller ignores. Force the status null once, and the activity-gated effects fall out unchanged. - isClaudeIdentityFrameTitle missed a multiplexer-wrapped Claude title ("zsh | Claude Code"), costing a genuine Claude pane its identity. Reuse the ' | ' segment split that agent-title-owner already had inline. - Keep title normalization on launchAgent: it only rewrites within an identity group (OMP wraps Pi), so a split does not make it wrong, and the hook-row path normalizes the same way. - Drop the tab.ptyId tracker fallback, which read a pty that the pane identity check had just rejected. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
ea8881a3c7 | fix: keep cross-project dialog actions in bounds (#13317) | ||
|
|
17cfc968cf |
Revert the terminal IME composition-ownership change (#13282)
* Revert "test(ime): restore coverage the composition-ownership change removed (#13168)" This reverts commit |
||
|
|
8a773a5e3f |
Focus search inputs for immediate typing (#13264)
* Focus search inputs for immediate typing - Autofocus inputs in AutomationListSearchField, SettingsSidebar, and WorktreeParentPickerPopover - Only autofocus Settings search when opening directly, not via deep-link - Use modal mode and explicit focus management in popover for proper restoration - Forward CommandInput ref and add autofocus test coverage * Restore focus when closing worktree parent picker popover - Find the nearest focusable ancestor of the anchor row to restore focus to instead of letting it drop on the detached input element - Simplify focus assertion in AutomationListSearchField test to verify actual focus behavior rather than autofocus attribute presence |
||
|
|
fc8441194c | fix(terminal): preserve remote pane output after host restart (#13158) | ||
|
|
17b3dff3c4 |
refactor(terminal): return IME composition ownership to xterm (#13128)
* fix(terminal): return IME composition ownership to xterm * fix(mobile): derive terminal input from native replacement ranges * test(mobile): record iOS Japanese IME traces * fix(mobile): preserve native IME replacement ranges * fix(xterm): flush queued application input after IME commit * test(terminal): pin Korean intermediate commit * test: pin Windows IME shortcut ownership * test: replay IBus number candidate commit * fix: preserve native macOS input-method punctuation * refactor(terminal): remove stale mac focus override * fix(mobile): preserve soft keyboard deletion ranges * fix: keep IME-owned palette chords in renderer * fix: stop carried IME shortcuts at renderer owner * fix: preserve carried IME shortcut dispatch * fix: narrow main-owned shortcut actions * test(mobile): pin Japanese IME replacement traces * test(terminal): retain paired native IME trace * fix(chat): preserve browser IME composition ownership * fix(chat): retain macOS IME confirm gesture * fix(chat): expire unmatched IME confirm carry * fix(chat): isolate IME confirmation expiry * fix(chat): retain active IME confirmation * refactor(terminal): remove dead composition handler * feat(ime): add shared Enter-ownership seams for CJK composition The confirming Enter of a CJK composition arrives as two keydowns and the orderings differ by platform: Windows/Linux redispatch the unmarked Enter/13 before keyup, macOS delivers keyup first. A guard reading only isComposing or keyCode 229 misses the redispatch, so surfaces submitted on a confirm. Adds useImeEnterGestureOwnership (carry token, next-frame expiry), a shared ImeEnterGuardedForm for native implicit submission, and the cmdk seam covering 18 CommandInput surfaces at one site. A chorded Enter arms the carry but is never swallowed — the reverse would eat a user's deliberate Cmd/Ctrl+Enter. Both failure modes are pinned by ime-enter-gesture-ownership-contract.test.ts. Co-authored-by: Orca <help@stably.ai> * refactor(terminal): consolidate native input listeners and parked-screen owner Extracts the shared native-input listener installer and renames the parked-screen detector for what it actually does, replacing per-call-site duplication. The listener installer keeps a forgetOptionKeyLocationOnBlur flag so per-window semantics are preserved rather than flattened. Net deletion; no behaviour change intended. Co-authored-by: Orca <help@stably.ai> * test(terminal): pin recorded IME shapes as regression tests Nine regression tests built from hashed affected-platform captures, each with a paired ordinary negative and a discriminating mutation verified to take the file from all-passing to exactly one failure. Covers the Windows MS-Korean Shift family (#12179, #11878, #12151, #11946, #12152) and the Korean TUI line-break rows (STA-3237, STA-3222, STA-3129). STA-3237 pins the empirical 3-Shift / 2-active-composition / 2-newline ratio the device run established — the third Shift produces nothing because Space has already committed. That ratio is not derivable from a static capture. Co-authored-by: Orca <help@stably.ai> * fix(ime): guard Enter-commit surfaces against CJK confirm Applies the Enter-ownership guards across the surfaces whose Enter commits something: publishes, clones, pairs, installs, posts, or persists. Tiered deliberately rather than uniformly. Irreversible and remote-effect sites take the carry token, which also blocks the unmarked redispatch. Locally reversible sites take the oracle check with a one-line comment naming the residual, because a spurious commit there costs one undo. Three numeric fields are left unguarded with the reason in-code: Chromium blanks number inputs at compositionstart, so a confirm-Enter only ever reaches an empty-draft reset. Measured with a CDP probe rather than assumed — a guard that cannot fire is noise. Co-authored-by: Orca <help@stably.ai> * test(ime): teeth-check the Enter guards on every guarded surface One suite per guarded surface, each verified by deleting the guard and confirming the test fails. A green guard test without that check is unverified, not verified. Two shapes pass vacuously in happy-dom and are avoided here: native implicit form submission never fires, and blur() is inert on an unfocused element. Both made "the commit did not happen" assertions pass with the guard removed, so the suites assert the guard's contract directly instead. Co-authored-by: Orca <help@stably.ai> * fix(mobile): keep iOS Korean commits whole through the live-input path iOS Korean reports isComposing: false on every event, so it bypasses the composition guard entirely. The strict owner rejected UIKit's transformed post-change field and sent only the leading jamo — the reported symptom. Prefers the authoritative same-event field text over the predicted text when the supplied operation cannot produce it. Generic: no Korean special-case, no locale classifier, no normalization. Adds the RN-target-keyed submit carry alongside it. Co-authored-by: Orca <help@stably.ai> * test(e2e): make IME capture harnesses fail loudly instead of silently Four instruments recorded silence as success, so a void run scored as a clean one: - readTerminalImeBoundaryTrace returned an empty trace when the probe never installed, making every "nothing leaked" negative pass vacuously - summarizeLatencies([]) returned a perfect zero distribution that passed all three latency thresholds - the macOS Vietnamese spec pinned an input-source ID that does not exist, and failed as though the operator had chosen the wrong source - the expectedLineCount=1 prefix property was undocumented and one edit from silently downgrading a PTY assertion Input sources now resolve by enumeration and name the near-matches on failure. Co-authored-by: Orca <help@stably.ai> * test(terminal): cover Cangjie cancellation and fix a cross-namespace assertion Adds #11951's recorded Cangjie cancel shape to the existing cancellation suite, which covered Pinyin and Sogou but not Cangjie. One keystroke then Backspace arriving as deleteContentBackward with data: null, so the stale preedit is the only thing a fallback could replay. Verified against the historical pre-6cd944c62b3 bundle: the positive fails with ['尸'] where [] is expected, while the ordinary negative stays green. Also fixes the Vietnamese spec, which asserted a TIS-space input-source ID against getKeyboardInputSourceId(). Those two Orca APIs report the same source in different namespaces — TIS nests it under VietnameseIM, the app API does not. The resolver stays as an installation precondition; the assertion matches the leaf. Co-authored-by: Orca <help@stably.ai> * test(e2e): add a real-IME macOS arm for the Korean chord commit The existing korean-ime-terminal-shift-enter-commit spec synthesizes composition over CDP: Input.imeSetComposition sets the preedit directly and Input.insertText performs the commit. Asserting the IME produced events you injected yourself is circular, so that spec cannot certify real-IME behaviour. This arm selects 2-Set Korean via TIS, reads it back live, and injects through System Events key codes, so the OS owns the preedit, the commit instant, and isComposing. PTY byte expectations are preserved verbatim. Covers 2 of the original 4 cases by design. The other two are the Windows/Linux redispatch-before-keyup ordering, which macOS cannot produce and which cannot be selected -- the OS decides it. Reintroducing synthesis to "restore coverage" would reintroduce the circularity. Co-authored-by: Orca <help@stably.ai> * test(e2e): assert the macOS chord arm at the PTY boundary, not the renderer The byte expectations were transcribed from korean-ime-terminal-shift-enter-commit :364/:383, which assert against onData -- a renderer boundary where the terminator is CR. This spec reads the PTY child, where the tty has already converted CR to LF. Names both forms per row rather than swapping the constant, so the conversion reads as evidence that the capture reached past the renderer, as #11936 and #11951 record. Ctrl+Enter's CSI-u sequence is unaffected and is identical at both boundaries. Co-authored-by: Orca <help@stably.ai> * test(e2e): measure composer-to-onData latency and stop dropping IME keystrokes Two defects in the echo latency probe. It hooked onWriteParsed and onRender but never onData, so it measured key->parse->render echo rather than the composer-vs-onData delta the latency rows need. Adds a third hook feeding its own sample set. And `event.key.length !== 1` silently dropped IME keystrokes: Pinyin and Cangjie keydowns arrive as key:'Process' (length 7). Replayed over the captured corpus, the old filter accepted 580 of 4137 Chinese IME keydowns -- it was discarding 80% of them. The new filter matches the shape the owner itself branches on. Attribution charges each onData to the latest keydown rather than a FIFO head, because composing jamo emit no onData at all and a queue would credit a whole composition to its first keystroke. The consumer now asserts sample count before any percentile, so a zero-sample run cannot render as a flawless distribution. Co-authored-by: Orca <help@stably.ai> * test(terminal): pin the WSL shifted-jamo newline shape for #11919 In Korean 2-set, Shift types ordinary letters -- the double consonants and the compound vowels. Each such keystroke reaches Chromium as key='Process', keyCode=229, shiftKey=true. The v1.4.163 classifier matched exactly that pattern with no code guard, so it called those keystrokes Enter, rewrote them to a synthetic Shift+Enter, and injected a newline into the middle of the word -- with no Enter key pressed. That is why the reporters said "no modifier key pressed": they had not chorded Shift+Enter, but they had pressed Shift, to type the double consonant. Asserts the row's own recorded capture: 40 immediate keydowns, exactly 3 of them Shift-carrying inside a single syllable, and an onData stream with one newline per Enter press and none mid-word. Two ordinary negatives keep it from being a blanket mute -- the same session's non-IME keydowns still reach shortcut policy, and an ordinary Shift+Enter still resolves through the real policy. Co-authored-by: Orca <help@stably.ai> * test(terminal): pin the composition commit lag that made Korean type one behind macOS Korean 2-Set commits syllable N only when the first jamo of N+1 arrives, so compositionend and compositionstart land in the same task. A composition-start handler cancelled the pending finalizer that was the only path to triggerDataEvent and ended the session without emitting bytes, so every committed syllable reached onData exactly one syllable late and the backlog cleared only at a Space or Enter. Types continuously with no Enter and no Space -- either would flush the backlog and hide it -- and samples onData at every syllable boundary. Paired with a length-matched ASCII arm that stays green throughout, so the positive is a fact about composition rather than about timing in general. Bisected to a single call site across five builds: pristine, 1.4.155 and 1.4.162 pass, 1.4.163 fails, removing the one call repairs it, restoring it fails identically. That window is exactly the reporter's "started immediately after updating". Co-authored-by: Orca <help@stably.ai> * test(mobile): cover the send-queue abort that silently drops queued keystrokes One failed send in use-terminal-live-input-commit aborts every keystroke queued behind it, with the error swallowed by .catch(() => false). The existing test resolves(true) on every send, so the failure branch was uncovered. Four arms: the abort itself, an ordinary negative on the healthy path, a throwing sender, and a liveness control proving the queue recovers once the chain settles. Deleting the abort takes 4 passed to 3 failed, with the ordinary negative correctly surviving. Scope is stated in the docblock: this is a transport send-queue abort, reachable only via a real disconnect or RPC error. REQUEST_TIMEOUT_MS is 30s, so latency alone cannot reach the branch — consistent with #7094's symptom class, not proven to be its cause. * test(terminal): pin that daemon snapshot/restore cannot disturb a composition Two independent reporters attributed broken Korean composition to the always-on PTY daemon repainting terminal state over the preedit. The attribution is wrong on ancestry — the daemon shipped three months before the version both call good — but the boundary was never actually tested. Runs the real applyMainBufferSnapshot choreography against a live composition, including the full 2J/3J/H wipe plus the resize and alt-screen branches. textarea.value, selectionStart/End, compositionView.textContent and .active all survive byte-identical, and interleaving a restore between every jamo of 문제 still commits 문제 at onData. Also pins that the uncommitted preedit is absent from the captured snapshot: it lives in the textarea, never the buffer, so a restore has nothing stale to echo back. Injecting one textarea.value = '' into the restore fails exactly the three restore-boundary tests. * test(terminal): pin that Cmd tears down a composition where Ctrl and Shift do not xterm's composition keydown exempts only keyCode 16/17/18 (Shift/Ctrl/Alt) plus 20/229. macOS Meta — 91/93/224 — is absent, so a Cmd press mid-composition takes _finalizeComposition(false): the overlay goes dark and never recovers, because compositionstart is not re-fired. The user composes the rest of the word blind. Linux and Windows users press Ctrl and are exempt. xterm already has a Meta-aware modifier predicate in wasModifierKeyOnlyEvent, so this is an internal inconsistency rather than a deliberate choice. Owns no reported row and is version-neutral: 5/5 on both 1.4.162 and 1.4.163. The branch is unexercised in all 328 recorded traces, so this is a hazard pin, not a regression guard. Only the teardown is asserted; the likely duplicated commit needs a compositionend the IME kept alive across the Cmd, which no capture contains. Deleting the exemption fails exactly the three paired negatives; adding Meta to it fails exactly the two Cmd arms. * test(native-chat): characterize preedit loss when a question card replaces the composer An AskUserQuestion card fully replaces the composer by design, but the in-flight composition goes with it: the composer unmounts before compositionend reaches it, so the preedit is never committed to the draft. The committed text survives only because the draft is cached and restored via defaultValue. Node identity changes, value 'abc' is preserved, the 가 is gone. Drives the real NativeChatView -> SessionGate -> InteractiveCard -> questionActive swap -> Composer -> ComposerField, flipped by writing the same store field an AskUserQuestion hook event writes. Flipping questionActive to false fails exactly this test and nothing else across 639 native-chat tests, so the path was entirely unguarded. CHARACTERIZATION TEST: it asserts the loss. Fixing the defect — committing the preedit before the swap, or keeping the composer mounted — will make this file fail. Update the expectations to the new contract rather than working around them. Owns no reported row. #12118/STA-3219 flicker is keyed to token counters, which provably do not remount, and a question card arrives once per question. * test(terminal): pin the duplicated commit when Meta interrupts a composition _finalizeComposition(false) sends textarea.value.substring(start, end) but cannot clear the IME-owned textarea, so a later compositionend re-sends the same range. Meta reaches that path because CompositionHelper exempts only Shift/Ctrl/Alt; xterm's own wasModifierKeyOnlyEvent covers Meta four ways, so the omission is an internal inconsistency rather than a choice. Companion to the modifier-exemption guard, which deliberately pins only the overlay teardown. This pins the data consequence. HAZARD PIN: owns no reported row. The trigger is unverified on hardware — no capture in the corpus contains a Meta-during-composition gesture, and whether macOS keeps the composition alive across it is unmeasured. The duplication follows from the code given that sequence; whether users reach the sequence is the open half. An earlier premise that Space (keyCode 32) reaches this path was refuted by a corpus scan: 0 of 731 evidence files carry a keyCode-32 Space while composing, against 171 at 229, and 229 returns early. * test(terminal): characterize the syllable lost when the textarea blurs mid-composition CoreBrowserTerminal._handleTextAreaBlur clears the helper textarea unconditionally — "Text can safely be removed on blur" — while CompositionHelper._finalizeComposition reads the committed text back out of that same value from a deferred timeout. By the time it runs the value is empty, the substring is '', and triggerDataEvent never sees the syllable. xterm checks composition state in _syncTextArea and omits the same check here. Six cases. Blurring mid-composition loses the syllable in every ordering, including compositionend-before-blur, which is Chromium's real order — so it is not an ordering artifact. A bare textarea.blur() with no Orca code loses it too, which places the owner upstream: Orca's unguarded release on outside pointerdown is one trigger, not the cause. Committing 한 then blurring mid-가 yields ['한'] where ['한','가'] is correct: one syllable gone, surrounding text intact. Teeth checked by inverting — adding an Orca-side composition guard flips exactly the three cases that route through the release path and leaves the bare-blur and no-blur cases green, which is the scope split: a fix in regular-terminal-focus-ownership alone would not close this. HAZARD PIN, but unlike the others this one has a real production injector — clicking outside the terminal mid-composition. Owns no reported row. The shape matches #9738's report; the injector does not, and a shape match with a mismatched injector is not an owner. * test(terminal): say which arm the STA-3237 fixture came from The recorded keydowns are wave 4's A-shift-unmarked-only — the arm that emits no PTY bytes. Nothing in the file said so, so two readers concluded the row's events fail the owner's predicate and that STA-3237 and STA-3222 were different defects. They share an owner; the arm that fires is Process/229+Shift, absent from this bubble-phase trace because the owner claims it in the capture phase. Also corrects "code-blind": the v1.4.163 policy emits \x1b\r only for a shift-only key:'Enter', and a jamo keydown reaches that branch solely via the isTerminalImeProcessEnter rewrite. The mock is deliberately wider so the ownership guard stays under test if that rewrite moves. Comments only — no assertion, fixture value, or mock behaviour changed. * test(e2e): track the input-source selector the macOS specs shell out to Five tracked macOS IME specs ran `swift .tmp/select-input-source.swift`, a file that is gitignored and existed only on one machine. Anyone else checking out the repo — or the same machine after .tmp is cleaned — could not run them, and they are the capture drivers for the macOS rows that are blocked waiting for exactly those runs. Moves it to tests/e2e/ beside its callers. The chord spec now resolves it from __dirname rather than reaching two levels up into .tmp. * test(terminal): pin the CJK repaint decision against the reporter's own output #12164 comment 1 and #5921 report agent output with double-width glyphs rendering duplicated character-by-character while ASCII in the same line stays clean. No IME, no composition, no keystroke — the user never types the CJK. Segmenting all three verbatim samples into maximal same-risk-class runs gives 33 runs and zero violations of "this run is corrupted iff the production detector flags it": 17 wide runs all corrupted, 16 narrow runs all byte-identical. The paired negative is co-located in the same line rather than in a separate run — the reporter supplied it without knowing. Doubling is asserted as present, not uniform: 자바스크립트 and 시스템 each leave a jamo undoubled, which is a repaint-region boundary artifact rather than a per-character transform. The discriminating arm is in the test rather than a source mutation: |
||
|
|
bba32bd00c |
fix(daemon): let the publisher replace a dead endpoint, not a third party (#12882)
Terminals froze app-wide several times daily, needing a manual pkill. libuv unlinks the pathname a server bound to when it closes, with no ownership check, so a departing daemon deleted whichever socket then sat at the canonical path — including a live replacement's. The replacement kept hosting PTYs no client could reach. #12709 fixed that mechanism; this replaces the shape around it. Two invariants: only a daemon publishing itself onto the canonical endpoint may mutate that entry, and only by replacing one it has itself just proven dead; and no actor removes a name it did not create. Publish binds a private name, takes the canonical one with an exclusive link, and on EEXIST proves the incumbent dead by connecting before replacing it in a single rename. Only 'connected' means occupied and only refused/missing prove death — a timeout proves nothing and declines. Deletes the claim sweeper, the reclaim tail of killStaleDaemon, and three unfenced unlinkSync(socketPath) calls in the launcher. Measured: rename exposed no gap across 6,525 darwin / 8,004 linux probes of a live handover, where unlink-then-link gapped on 200 of 200. Verified on all three platforms: full suite on macOS and Linux, and daemon restart e2e on a real windows-2022 host. Contract in src/main/daemon/AGENTS.md. |
||
|
|
ce20a109da |
Persist the Linear issue list view and per-workspace filters (#12710)
* Persist the Linear issue list view and per-workspace filters
Layout, grouping, ordering, columns, and attribute filters survive a restart.
Facet ids are workspace-scoped, so filters are kept per Linear workspace and the
active filter is *derived* from the selected workspace rather than reset by an
effect on switch — no ordering race can apply workspace A's facets to B, and an
unresolved or cross-workspace selection reads as unfiltered without erasing
anything.
A single shared catalog backs the renderer state, `TaskResumeState`, and the
strict `ui.set` schema, so a new view option cannot leave paired web/mobile/relay
clients rejecting the whole payload. Persisted values are normalized as untrusted
input: a corrupt preference or a single bad workspace entry is dropped without
taking the rest of the resume state with it.
Deriving the filter also removed the guard that used to make three neighbouring
behaviours safe, so they are re-scoped here:
- The primary-team facet reset now fires only on an in-workspace team change.
A workspace switch also changes the primary team, and clearing there wiped the
filter that had just been restored for the workspace being switched *to*.
- The list-read force check no longer fires on the session's first read, so a
restored filter serves warm cache instead of forcing a network round trip
behind a blocking spinner on every cold start.
- The filter dropdown derives "no single workspace" from `workspaceId` alone.
With an unresolved workspace it previously rendered the statically populated
priority section, whose clicks now have nowhere to be stored.
* Harden Linear view persistence against the failures review surfaced
Five issues, each found by a reviewer and reproduced before fixing:
- The filter dropdown's prune effect only ran when the user opened the popover,
because the filter was always empty at startup. Restoration makes it run on
mount, where `availableTeams` may still be the issue-scraped fallback rather
than the real fetch. Metadata complete for a *partial* team set passes every
R12 guard, so it pruned facets belonging to teams it simply hadn't seen — and
the write persisted, deleting them permanently. Gated on `teamsSettled`.
- `canonicalize` dedupes but enforces none of the transport bounds; only the
throwing parser does. So `serialize` could emit a 101-label filter that the
strict `ui.set` schema rejects, which drops the WHOLE taskResumeState — github,
jira and linear query included — on every subsequent write, since the renderer
resends the merged object each time. Added `boundLinearIssueAttributeFilter`
and a round-trip test built from serializer output rather than a literal, which
is the only kind that can catch renderer/schema drift.
- `linearIssueView` now carries `.catch(undefined)`: value tolerance stops at the
top level, so any future instance of the above is a cosmetic reset of the view
instead of silent loss of every other resume field.
- A workspace switch forced an uncached list read in both directions. The switch
is a later observation, so the null-baseline fix didn't cover it; the cache is
already workspace-keyed, making the force pure cost.
- Recency for the 20-workspace cap came from object key order, which is wrong
twice: re-filtering an existing workspace left it at the head (first evicted,
though just used), and an array-index-like key enumerates first regardless of
insertion, so a write could evict the very entry it added. Recency is now an
explicit ordered key list.
Also adds the nested parity assertion — the top-level one compares only
TaskResumeState's own keys, so a field added to LinearIssueViewResumeState stayed
invisible to it, which is exactly what `.strict()` rejects.
The wiring test was blind: deleting the hydration guard outright left all four
assertions green. The gate is now `shouldPersistLinearIssueView`, unit-tested
directly, and the file is renamed to the repo's `*-boundary.test.ts` convention
with an assertion that fails on that mutation.
* Log discarded Linear views and fix empty-filter serialization
- Schema now logs when linearIssueView is discarded, making validation failures visible
- Fixed serialization: filters that become empty after bounding are now omitted
- Added AssertNoExtraKeys type check for bidirectional schema/type parity
- Refactored view option catalogs to use canonical constants, preventing UI/schema drift
* Remove workspace persistence limits and LRU eviction
Stop capping persisted Linear workspace filters at 20 and evicting
least-recently-used workspaces. Simplify persistence to store all
workspace filters, gate persistence only on resume state application,
and remove tests that pinned implementation details. Users can now
persist filters for all their workspaces without arbitrary limits.
* add test for linear persistence
* Improve Linear filter test clarity and fix e2e overlay dismissal for CI
- Convert parameterized filter-pruning test to sequential assertions
- Fix dismissOverlayChrome to toggle overlay triggers instead of
force-clicking inert page elements in headless CI
* Prevent TaskPage from stealing Escape from Radix menus
- Add check to detect open Radix dropdown menus and popovers; return
early from Escape handler to respect their capture-phase ownership
- Update overlay dismissal in e2e tests to use keyboard.press('Escape'),
now that TaskPage no longer interferes
* The capture-phase Escape guard in TaskPage bailed out for open dropdown menus and popovers, but an open Radix Select matches none of those selectors: the shared SelectContent wrapper (src/renderer/src/components/ui/select.tsx:60) renders data-slot="select-content" and Radix gives its content role="listbox", not role="menu". So with a select open, the window-level capture handler ran first, called preventDefault() and closeTaskPage() — closing the whole task page instead of just the select. Added [data-slot="select-content"] to the guard, as suggested. I did not add [role="listbox"]; the reviewer explicitly notes it's too broad, and the data-slot selector covers every select rendered through the shared wrapper.
---------
Co-authored-by: m4air <m4air@MacBook-Air.localdomain>
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
|
||
|
|
2f30eb9af5 |
fix(ai-vault): block deletion of live sessions (#13108)
* fix(ai-vault): block deletion of live sessions * fix(ai-vault): retain external session authority --------- Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com> |
||
|
|
4c26ef626c |
Extract GitHub task search commit debouncing into a hook (#13112)
* Extract GitHub task search commit debouncing into a hook Move debounce logic from TaskPage into useGitHubTaskSearchCommit to prevent excessive GitHub API calls on every keystroke. Uses a 750ms idle window before committing search values. Add tests for the new hook. * Keep task rows visible while typing search query Removed premature row-hiding logic from the search input handler that was triggering before debounced queries fire. The handler now only updates the input state; debouncing and query timing are handled by a dedicated hook. Added e2e test verifying search idles before fetching and Enter doesn't double-fetch. * Test that GitHub task search commits cancel on disable and unmount Verify the useGitHubTaskSearchCommit hook properly cleans up pending commits when disabled or when the component unmounts. This prevents unnecessary GitHub API calls during normal user interaction. Also fix e2e test instrumentation to find the active repo through the worktree relationship rather than assuming the first repo with a path. |
||
|
|
2396e5e3e5 | fix(browser-pane): reschedule remote stream restart with bounded backoff (STA-3483) (#12787) | ||
|
|
8c3e9535c7 | fix(terminal): remove permanent link tooltip gap (#13075) | ||
|
|
a77002c42b |
feat(ai-vault): delete a provider session from the AI Vault list (#10249)
* feat(ai-vault): validate session-delete targets for single-file providers Add the pure judgement layer for deleting an Agent Session History entry. `validateAiVaultSessionDeleteTarget` decides whether a session may be removed: the agent must be one of the nine providers where a single file is the whole session (gemini, copilot, cursor, hermes, devin, openclaw, droid, pi, omp), the host must be local, and the renderer-supplied path must resolve inside that agent's own session roots and match its discovery predicate. To keep the delete roots from drifting from the scanner's own roots, the WSL-expansion helper moves to session-scanner-root-dirs.ts and the OpenClaw root derivation + session predicate become shared helpers that discoverOpenClawFiles itself consumes. The result is path-only and never touches the filesystem; a returned `allowed: true` still requires an lstat/realpath re-check in the executor (S-2) before removal, documented as a caller contract on the result type. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XDLggjSAjDnaWi3Y8U622i * feat(ai-vault): move a validated session transcript to the trash Add the filesystem executor behind session deletion. It calls the S-1 path validator, then performs the fs-side guards that validator documented it could not: lstat().isFile() rejects a directory or symlink, and realpath is re-fed through the validator so a regular file reached through a symlinked parent that escapes the agent's roots is rejected too. Only then is the file moved to the OS trash via shell.trashItem, with ENOENT treated as success so a delete racing an external removal stays idempotent. WSL UNC paths (no Recycle Bin) are delegated to tryDeleteWslUncPath before the Windows-local fs guards, mirroring fs:deletePath. Any non-ENOENT error is returned as a failure result rather than thrown, since IPC payloads are untyped at runtime. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XDLggjSAjDnaWi3Y8U622i * feat(ai-vault): delete-session IPC handler, preload bridge, cache invalidation Wire the S-2 delete executor to an IPC endpoint and expose it on the preload bridge. The renderer calls aiVault:deleteSession with { agent, filePath, executionHostId }; the handler fetches WSL homes, delegates to the executor (which re-validates and trashes), and on a real delete invalidates the caches that could otherwise keep serving the deleted session. Cache invalidation is generation-guarded: a scan already in flight when the delete lands carries an older generation and must not write its pre-delete result back into the cache. Without this, an in-flight scan resolving just after the delete would resurrect the deleted session for the 15s TTL — and force-refreshing the panel only masks it for the desktop, not for the paired mobile client or runtime RPC that share the same cache module. Both the shared local-scope cache and the desktop multi-host cache carry the guard, with regression tests for the in-flight race. The delete result type moves to shared/ai-vault-types.ts so the renderer can import the same contract the executor returns. To keep ai-vault.ts within the max-lines budget after adding the delete wiring, two cohesive pieces are extracted to their own files: the delete orchestration (ai-vault-delete.ts) and listAiVaultSubagentSessions (ai-vault-subagent-list.ts). The latter is the only handler with no dependency on this module's private cache state, so it is the one piece that moves verbatim without threading state through a seam. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XDLggjSAjDnaWi3Y8U622i * feat(ai-vault): renderer judgement for whether Delete is offered Add the renderer counterpart to the main-side delete validator: given a session, decide whether the row menu shows Delete enabled, or disabled with a reason a tooltip can render. It reuses the shared deletable-agent set and unsupported-reason map so the two sides can never disagree about which agents are deletable, and reuses the existing local-host / synthetic-path renderer helpers. This is intentionally not a security boundary — it validates neither the path root nor the file predicate. Those are the main process's untrusted-input defense; the renderer only picks the affordance, and the main side re-checks on delete regardless. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XDLggjSAjDnaWi3Y8U622i * docs(ai-vault): correct deletability parity claim; test multi-reason agent The renderer deletability check runs host -> synthetic -> agent, while the main validator runs agent -> host -> synthetic. The two layers agree only on deletable-or-not (renderer-false is a subset of main-false), not on the reason code a doubly-failing session carries. Document that explicitly instead of implying the orders match, and add the antigravity case (two reason codes) so the agentReasonCodes array shape is actually exercised. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XDLggjSAjDnaWi3Y8U622i * feat(ai-vault): add Delete to the session row menu with a confirmation dialog Wire the delete affordance into AI Vault. Both the dropdown and the context menu gain a destructive Delete item; a session that can't be completely deleted (remote host, synthetic OpenCode-SQLite path, or a directory/registry-backed agent) shows the item disabled with a reason surfaced both as a tooltip and as an aria-label so keyboard and screen-reader users learn why. Confirming opens a dialog that names the session and states it will no longer be resumable from the provider's own CLI, then calls the delete IPC and force-refreshes the list for immediate feedback (the main side has already invalidated its caches). The confirmation copy says the session "will be deleted" rather than "moved to the trash": on Windows a WSL session is deleted with rm inside the distro (no Recycle Bin), so promising recoverability would be a lie on that platform. Deletability is computed once per row and shared by both menus so they can never disagree. New pure logic — the reason-to-tooltip mapping (including the multi-reason join) and the delete action hook's deleted/rejected/failed branches — is covered by unit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XDLggjSAjDnaWi3Y8U622i * fix(ai-vault): state that Delete is unavailable without naming the cause The disabled Delete item explained a provider's storage layout to the user ("Claude sessions can't be deleted here: stores sessions as a folder, not a single file"). That is Orca's problem, not the reader's — the tooltip now says which sessions are affected and stops there. The non-local-host string stays as it was: it states scope, not a cause, and tells the user what would work. The reason-code plumbing existed only to compose that tooltip, so AI_VAULT_UNSUPPORTED_DELETE_REASONS, AiVaultUnsupportedDeleteReasonCode, and the renderer result's agentReasonCodes field go with it. Why each agent is excluded moves into the comment above AI_VAULT_DELETABLE_AGENTS, where a reader looking up the deletable set will find it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGuzChimmQ1dYX2raecrH7 * feat(ai-vault): delete claude, rovo, and grok sessions by their directory These three were excluded only because the delete unit was one file. Their sessions are directories — claude keeps Task subagent transcripts in a sibling `<uuid>/subagents/`, rovo and grok keep everything under `<sessionId>/` — and nothing in them is shared with another session, so a directory-aware delete is still a complete delete. Supported goes from 9 agents to 12; the four that remain (antigravity, kimi, codex, opencode) are blocked by a registry or a SQLite row, which no delete unit fixes. Validation now returns an ordered removal plan instead of a single path. Each removal carries the kind it must be on disk and the roots its realpath must stay inside, so the executor's guard is the same shape for a file and for a directory. Companions come first and the transcript last: the transcript is what puts the row on screen, so a part-way failure leaves the row to retry from rather than dropping it and stranding the rest on disk. Claude's `session-env/<uuid>/` goes with the transcript — it holds that session's generated shell exports and nothing else. Its sibling `file-history/<uuid>/` deliberately does not: it is the rewind buffer holding earlier versions of the user's own files, and retiring a session is no reason to take away the only copy that can restore them. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGuzChimmQ1dYX2raecrH7 * fix(ai-vault): remove a claude session's own directory, not just its subagents Deleting a claude session trashed `<uuid>/subagents/` and left `<uuid>/` behind as an empty directory — one per deleted session, accumulating under every project. The directory is named after the transcript, so it belongs to that session as a whole; take it rather than the one subdirectory inside it. Still derived from the scanner's own subagents path, so the two cannot drift. Reaching the parent means a degenerate stem now matters: `..jsonl` passes the extension check and its stem is `.`, which would resolve the session directory to the project directory holding every session. Reject it instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGuzChimmQ1dYX2raecrH7 * fix(ai-vault): keep a session row collapsed when a menu action is chosen Radix portals the row's dropdown and context menus out of its DOM, but React still bubbles their clicks back through the component tree, so every menu selection also hit the row's own click handler and expanded it. The trigger button already stopped propagation, which is why opening the menu looked fine and only choosing an item misbehaved. It shows worst on Delete: the row expands behind the confirm dialog, so cancelling leaves the list rearranged under a dialog the user just backed out of. Toggle details only for clicks that land in the row's own subtree — that covers the context menu and any future portalled surface, not just this one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGuzChimmQ1dYX2raecrH7 * fix(ai-vault): harden the delete-confirmation flow against IPC rejection and mid-delete dismissal Two robustness gaps flagged in review: - handleConfirmDelete only branched on result.outcome. The main handler resolves with a 'failed'/'rejected' outcome rather than throwing, but the IPC invoke itself can still reject on a transport/serialization error, and the caller fires it with `void`. That reject would surface as an unhandled rejection with no toast. Catch it and show the same generic failure toast. - handleDialogOpenChange cleared sessionPendingDelete on every open=false. The Cancel button is disabled mid-delete, but Radix still fires its Escape/outside-click/X close, which could dismiss an in-flight delete out from under itself. Ignore close requests while deletingSession is true. Both covered by regression tests (verified failing without the fix). * fix(ai-vault): route WSL UNC directory removals through the WSL rm branch Directory-shaped deletes (claude's subagents/session-env dirs, rovo/grok's session dir) gated the WSL branch on kind === 'file', so on Windows a session under a WSL distro home fell through to shell.trashItem — which can't trash a WSL-volume item (no Recycle Bin) and throws, or worse is silently stranded when the 9P filesystem's unreliable lstat false-reports ENOENT and the executor treats that as success. Single-file deletes predate the directory kinds, so the file-only gate was correct until directory removals were added. tryDeleteWslUncPath already supports recursive removal; pass recursive for directory removals so they take the same WSL rm path as files instead of shell.trashItem. Covered by two regression tests (file: non-recursive, directory: recursive), verified failing without the fix. Also drops the internal ledger-ID references (D-*, S-*) from comments in these two files; they pointed at a private design doc a reader can't see. * docs(ai-vault): drop internal design-ledger IDs from shipped comments Comments across the session-delete feature cited decision/slice IDs (D-1..D-7, S-1..S-5) from a private design document. Those references are meaningless to anyone reading the code without that doc, so remove the IDs while keeping the reasoning each comment carried. No behavior change. * test(ai-vault): e2e-cover the real on-disk session delete The unit tests mock lstat/realpath/trashItem, so nothing proved the whole IPC path actually removes files. This spec seeds sessions into the E2E harness's isolated HOME and deletes them through window.api.aiVault.deleteSession: - a single-file session (gemini): the transcript is gone from disk and drops out of the list. - a directory-shaped session (claude): the transcript, the <uuid>/ session directory (subagents included, no empty shell left), and the session-env companion are all gone, while the file-history rewind buffer is preserved. Verified failing when the executor's removal is stubbed out. Runs on Linux CI. * fix(ai-vault): address review findings on the session-delete flow Three points raised in review: - Disable Delete for a still-running session. resolveAiVaultSessionDeletability now gates on liveState (working/blocked/waiting) last — an otherwise-deletable session that is mid-run shows "wait for it to finish" instead of an enabled Delete, so trashing a live agent's transcript can't drop writes it is still appending. Unsupported/remote sessions keep their permanent reason. - Realpath the roots, not just the target, in the executor's escape check. The roots were only resolve()'d (text), so a session under a symlinked root (~/.claude -> /Volumes/…) was falsely rejected; realpath each root (falling back to its text form when it can't be resolved) before the membership check. - Invalidate the parse cache with the raw filePath, not resolve(filePath). The cache is keyed by the exact path the scanner discovered, so resolve() could normalise it away from the stored key and miss. Drops the now-unused import. Also moves AiVaultDeleteSessionArgs/Result out of ai-vault-types.ts (which the upstream merge pushed over the max-lines limit) into the ai-vault-session-deletion domain module they belong to, and updates importers. Regression tests added for the live gate, the symlinked-root accept, and the reason string; verified failing without each fix. * fix(ai-vault): type the deleteSession preload bridge as its real result The bridge declared Promise<unknown> while AiVaultApi.deleteSession promises AiVaultDeleteSessionResult, so the preload object leaned on the api-types declaration to stay honest instead of being checked against it. Co-authored-by: Orca <help@stably.ai> * refactor(ai-vault): tighten the session-delete code to house style Comments across the delete flow explained HOW alongside WHY and ran to a dozen lines; they now carry only the non-obvious reasoning. The excluded-agent rationale, the caller contract on the validator, and the file-history carve-out are kept — those are knowledge, not narration. Also removes three duplications the feature introduced: - AiVaultSessionDeleteExecutionResult was an alias for AiVaultDeleteSessionResult whose comment pointed at a module the type no longer lives in. - The synthetic-path predicate existed twice under near-identical names; the renderer now re-exports the shared one it already had a sibling import of. - The delete-failure toast was written out verbatim in both the rejected and the thrown branch. Co-authored-by: Orca <help@stably.ai> * refactor(ai-vault): use a design-system dialog width and a stable row selector The confirm dialog pinned an arbitrary sm:max-w-[440px]; every other dialog in the right sidebar uses a scale token, and md (448px) covers the role. The row-expand test selected the row by [draggable="true"], which stopped naming the row when draggable moved to the title element upstream. It still passed by bubbling, so the comment was the only thing wrong — now it selects the title deliberately and says why the query is first-match (Radix's asChild trigger repeats the subtree, so screen.get* sees duplicates). Also types the e2e delete helper as AiVaultDeleteSessionResult instead of a hand-written { outcome: string }, now that the preload bridge returns it. Co-authored-by: Orca <help@stably.ai> * refactor(ai-vault): consolidate agent sources and use system dialog Discovery and deletion now share the same agent source definitions, eliminating the risk of them drifting apart. A single `AI_VAULT_AGENT_SOURCES` table declares each agent's root directories, file extensions, and acceptance predicates. Replaced the custom delete confirmation dialog with the system dialog, simplifying the delete action hook and removing boilerplate state management. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Co-authored-by: Orca <help@stably.ai> |
||
|
|
bc1e049b3f |
fix(terminal): defer metric option writes to unmeasurable panes (#12944)
* fix(terminal): defer metric option writes to unmeasurable panes Writing fontSize/fontFamily/fontWeight/lineHeight makes xterm re-measure cell size against the pane's current box. A hidden or mid-layout pane can measure a wrong-but-nonzero size, which latches (hasValidSize) and mis-keys the shared WebGL glyph atlas until a manual resize — the stuck variant of the P0 bold/blurry-font reports. Metric writes now land only on measurable panes; otherwise the latest values park per-pane and flush on the next safe fit or reveal (with a refit on the light tab-resume path, which otherwise skips fitting). Measurability helpers move to pane-fit-measurability.ts to stay under the pane-fit.ts line cap. * fix(terminal): key metric deferral by terminal, not pane view getPanes() returns a fresh toPublicPane() wrapper per call, so a WeakMap keyed on ManagedPane never matched across call sites: deferred metric options were dropped, not deferred. Key on pane.terminal, which is carried by reference and dies with the pane. Also from review: - flushDeferredPaneMetricOptionsIfMeasurable checks the pending WeakMap before the measurability probe, so the common no-deferral case costs zero forced style/layout on every reveal. - applyTerminalAppearance skips the apply (and the probe) when all five values are already live and nothing is parked; any settings write re-runs the pass over every mounted pane, and arming a no-op deferral would trigger a refit on the next reveal. - fitRevealedPane flushes first: its pixel/grid checks can both no-op and return without fitting, stranding parked options. - Font zoom folds its direct fontSize write into any pending deferral so the flush inside safeFit cannot clobber the user's zoom. Corrects comments that asserted a cell-size re-measure mechanism xterm does not have: CharSizeService measures via OffscreenCanvas TextMetrics, independent of the pane box, and only fontSize/fontFamily re-measure. Test fixtures now allocate a fresh pane view per getPanes() call, which is what production does and what hid the keying bug. * fix(terminal): re-check the fit floor after a metric flush performSafeFit evaluated the min cols/rows gate with the pre-flush cell size, then flushed and fit unconditionally. A large font jump on a narrow pane passes the gate at the old size and lands under it at the new one, so fit() pinned the PTY to the tiny grid the floor exists to reject. Re-check after a flush that actually landed. The parked values still apply, so the pane is never stuck on stale metrics; only the fit is skipped. * fix(terminal): route a reveal metric flush through the stable fit fitRevealedPane's new flush branch called safeFit directly, which is exactly what the function's contract forbids on reveal: resumeRendering has just re-attached WebGL, whose cell metrics transiently differ from the DOM renderer's, so a raw fit can propose a one-column-off grid and reflow — and xterm's wrap/unwrap is not a perfect inverse, leaving a diff-painting inline TUI corrupted. A landed flush leaves pixels unchanged with a diverged grid, the same shape as a snapshot resize, so it takes the same steady-grid repair. A real resize still fits synchronously, after the flush. Reachable via window wake, which calls fitAllRevealedPanes with no pre-flush loop. * fix(terminal): gate metric writes on the pixel box, not the fit floor canApplyPaneMetricOptions reused canMeasurePaneForFit, whose >=8 cols / >=4 rows floor exists to stop a fit pinning the PTY to a sliver. But the divider clamp is 50px, which clears the 48px pixel floor and proposes ~5 cols — so a pane dragged to the clamp deferred every font change and never flushed: it never hides, and its box never changes, so no reveal and no ResizeObserver entry ever arrives. It rendered a stale font until widened, where pre-PR the write was unconditional. Gate metric writes on display plus the pixel box only. Hidden panes and the transient worktree-switch overlay are near-zero, so they still defer — the deferral's purpose is unchanged. The cols/rows floor stays on the fit, including the post-flush re-check in performSafeFit. Apply and flush share the same predicate, so no "applies but never flushes" state can open up. * fix(terminal): flush heavy reveal metrics after WebGL resume |
||
|
|
ddf58d6d6a |
fix(terminal): restore preserved remote PTYs after host relaunch (#12990)
* fix(terminal): foreground preserved daemon PTYs * fix(terminal): keep snapshot sequence domains distinct * test(terminal): use active reconnect control * test(terminal): await reconnect control activation * test(terminal): validate reconnect with fresh control * test(terminal): tighten host restart evidence * fix(terminal): retry preserved PTY attach after inventory * fix(terminal): retry attach after overlapping inventory --------- Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com> |
||
|
|
2b42de1f52 |
fix(orchestration): wake coordinators with mail pointers (#12988)
Wake idle Run coordinators with durable orchestration mail pointers while keeping message payloads in the store until check consumes them. Preserve waiter, Cursor, restart, real Codex title, and PTY replacement behavior.\n\nPart of #12953. |
||
|
|
c9485fdded |
fix(computer): fence macOS HID coordinate clicks (#12981)
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com> |
||
|
|
4b2603420e |
fix(terminal): protect local ConPTY Ctrl+Enter without breaking TUIs (#12462)
* fix(terminal): gate Ctrl+Enter CSI-u on a negotiated kitty pane Ctrl+Enter emitted \x1b[13;5u unconditionally, so a pane that never negotiated the kitty keyboard protocol (local Windows ConPTY, plain shell) printed the escape verbatim into the prompt. Mirror the Shift+Enter guard and fall back to the legacy CR every emulator sends for this chord. Keeps the intercept, so IME commit ordering and the single-send dedupe still apply. Fixes #12329 Co-authored-by: Orca <help@stably.ai> * test(e2e): negotiate kitty via PTY output in the Ctrl+Enter spec The Ctrl+Enter gate reads the PTY-output kitty tracker, which enableKittyKeyboardReporting never feeds (it writes straight into xterm's parser), so the spec pressed the chord on a pane the policy still saw as un-negotiated and got the CR fallback. Negotiate from the application side like the neighbouring Shift+Enter spec, and reset the flags afterwards for the serial suite. Co-authored-by: Orca <help@stably.ai> * fix(terminal): preserve trusted Ctrl+Enter routing * fix(terminal): scope IME redispatch ownership * fix(terminal): reject conflicting Ctrl+Enter evidence --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
a025a71447 |
fix(orchestration): deliver pending mail to already-idle agents (#12584)
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com> |
||
|
|
8ddf575fe6 |
Revert "Remove source control group order preference (#12785)" (#12955)
This reverts commit
|
||
|
|
f71373953b |
fix: prioritize filenames in cmd+p quick open (#12679)
* fix: show full paths in quick open results * refactor: use native file path tooltips * fix: position file path tooltips * refactor: share the cursor path tooltip with quick open Co-authored-by: Orca <help@stably.ai> * fix: let path tooltips run wider before wrapping Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
ead6b8e225 |
fix: prioritize filenames in new-tab file results (#12670)
* fix: prioritize filenames in new-tab file results * fix: preserve root separator in filename-first paths * refactor: use native file path tooltips * fix: position file path tooltips * fix: use the native OS tooltip for new-tab file paths Co-authored-by: Orca <help@stably.ai> * fix: show new-tab file paths in a system-style tooltip Co-authored-by: Orca <help@stably.ai> * fix: anchor new-tab path tooltip to the cursor Co-authored-by: Orca <help@stably.ai> * fix: tighten cursor tooltip to file rows and design tokens Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
f82d5c3e6c |
fix(terminal): clear stranded link hover tooltip (#12786)
* fix(terminal): clear stranded link hover tooltip * fix(terminal): declare the tooltip reserve var where it resolves --orca-terminal-link-tooltip-height was declared on .pane-manager-root, a class no live element carries, so both .xterm-container height calc()s were invalid at computed-value time and collapsed to height:auto — the element FitAddon measures, making rows a fixed point. Also isolate _clearCurrentLink() so a throwing provider leave() cannot skip the cache invalidation, and bound the e2e gap assertion on both sides. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
c2da0e47f9 |
fix(computer): deliver macOS coordinate clicks via the HID event tap (STA-3433) (#12839)
Mouse events posted with CGEventPostToPid reach the target app with no window association, so AppKit never routes the press to a view: hover states fire but the control is never activated, and the mouseUp is dropped outright when posted back-to-back. Post click events to the HID event tap instead (as keyboard synthesis already does), pace them, and stamp mouseEventClickState so multi-clicks register. Synthetic clicks now also report verification unverified/synthetic_input from the helper itself, matching the other synthetic actions. |
||
|
|
b70e33764f |
Revert #12658: show PR status on active and done workspaces too (#12825)
Stacked on the #12793 revert. Widens the passive-identity set from {inactive} back to {active, done, inactive}, so the PR/check glyph returns to the left status lane for workspaces that are actively being worked, not just idle ones. Tradeoff, deliberate: #12658 was not purely a regression. It also fixed #8813, where an active workspace with branch identity and no PR showed the grey branch glyph instead of the emerald Active dot. This revert reintroduces that, and removes its e2e guard. The left lane holds one glyph, so activity, branch identity, and review status cannot all be shown. This picks review status. |
||
|
|
aa64ac9606 |
fix(runtime): degrade focus-requested terminal create on headless serve (#12791)
`orca serve` publishes a ready graph under HEADLESS_RUNTIME_WINDOW_ID with no BrowserWindow behind it. `shouldCreateInBackground` only degraded when the create was renderer-backed, so any focus-requested create fell through to getAuthoritativeWindow() and threw "No renderer window available" — leaving `terminal create --focus` with no workaround on a remote server (#10333). With a worktree selector and no renderer window, a background spawn is the only usable path, so collapse the renderer-backed window check into a plain "no window" check. That is the existing rendererBacked clause plus exactly the missing focus case, and it drops the confusing `rendererWindow === null` indirection (rendererWindow is already gated on rendererBacked). Focus is not lost by the degrade: the spawned pane is still published to the session-tab model and revealed with `activate: true`, which is how a paired client learns about it. Mirrors the in-tree precedent in runCreateMobileSessionTerminal. Headed hosts are unaffected — the clause only fires when no window exists. Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com> |
||
|
|
ae1ed5e886 |
Remove source control group order preference (#12785)
* Reorder source control to show staged changes first by default Stages are closest to the commit action and most relevant to the commit workflow. Merges untracked files into Changes visually while preserving their Git area. Removes the untracked-first preset and includes migration logic for existing user settings. * Drop source control group order user preference Remove the sourceControlGroupOrder setting and related UI, migrations, and persistence logic. The source control view now always displays sections in the order: staged changes, unstaged changes, untracked files. * Reorder source control to show changes before staged Aligns with the edit-stage-commit workflow by showing unstaged changes (active edits) before staged changes (queued for commit). |
||
|
|
2c2a3266a6 |
Change question card submit button label to 'Submit' (#12782)
- Replace 'Send answer' with 'Submit' for clarity and consistency - Update all locale translations (en, es, ja, ko, zh) - Remove fixed button width and add whitespace-nowrap for flexible sizing - Update component and test references |
||
|
|
4c49989c2e |
refactor(codex): delete the unreachable managed shared-mirror lane (#12614)
PR 9501 shipped real-home routing for the host system default, and the env override that could turn it back off was never a shipped control. The managed-account half of the shared runtime mirror has been unreachable since: every host account routes to its own self-contained CODEX_HOME before that code runs. Delete the flag module and its env plumbing plus the managed branch of syncForCurrentSelection and the six helpers only it called. The three lanes that still use the shared mirror -- Windows, a custom CODEX_HOME, and a hook-lane gate that reports unusable -- are untouched, as are every legacy migration and the WSL read-back helpers. |