main
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
6c765d5ad3 | refactor(mobile): remove native chat re-export facades (#13436) | ||
|
|
faaf3d2588 | refactor(mobile): use shared diff and OSC implementations (#13433) | ||
|
|
f3ea376b6b | refactor(rate-limits): share initial account target policy (#13428) | ||
|
|
8a33ce2536 |
[leas code] Simplify boolean guards (#13401)
* refactor: remove unreachable code * refactor: simplify boolean guards |
||
|
|
403c60bbad | refactor(mobile): remove unused review code (#13421) | ||
|
|
934faaec76 | refactor: deduplicate Linear team readers (#13417) | ||
|
|
8a38e2f3f8 | refactor(renderer): reuse existing control paths (#13425) | ||
|
|
1e72534709 | refactor: deduplicate repository search entries (#13415) | ||
|
|
65a5e8470a | refactor: share GitHub link normalization (#13409) | ||
|
|
c56ff58dd7 | test(mobile): centralize renderer setup (#13408) | ||
|
|
954de38507 | refactor(mobile): remove unused PR and review code (#13404) | ||
|
|
ced2719b26 | refactor: remove unreachable code (#13400) | ||
|
|
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> |
||
|
|
94cc867e2a |
feat(terminal): encode IME commits as CSI-u under the all-keys kitty flag (#13310)
* 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> * chore: drop non-mergeable IME e2e scratch files --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
cb9aa12fff |
test(terminal): pin the macOS key-binding substitution against #11170 (#13315)
* 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(terminal): pin the macOS key-binding substitution against #11170 An OS key-binding remap of the character a Korean layout puts on Backquote is honoured everywhere on macOS except the terminal, which sent the raw layout character to the PTY. The substitution is applied inside the system text input path, so it exists only on keypress.charCode and the input event's data; the keydown still carries the layout character. Nothing needs to parse the binding file - Chromium has already applied it by the time `input` fires. The reported build sent the raw character. A later punctuation table happened to list that one character, which closed the issue by enumeration rather than by design, and the structural claim removes the table entirely. Without a test the fix could regress silently on a change that never mentions the issue. Replays the reporter's captured event shape and pairs it with the same physical key carrying no substitution, so a fix that rewrote the Backquote position unconditionally would fail. Discrimination checked by mutation: suppressing the structural claim, and separately removing the single table character on a pre-rewrite tree, each make the replay send the raw layout character while both negatives stay green. Co-authored-by: Orca <help@stably.ai> * test(terminal): cover the other Korean layout on the remapped key Korean layouts disagree about what the backquote position produces: two of them give the currency sign the issue reports, one gives an asterisk. One key-binding entry has to survive either, but honouring the substitution by listing characters covers only the ones someone remembered to list - which is why the reported character worked and this one did not. This arm discriminates without a mutation: it fails on the pre-rewrite tree and passes on the structural claim. The harness supplies no input-source classification, modelling a source the older design did not recognise, including the window before its async probe resolves. With the source recognised the older design claimed all ASCII punctuation and covered this too, so the gap was real but conditional; the header says so rather than letting the failure read as unconditional. Co-authored-by: Orca <help@stably.ai> * test(terminal): drop the Won-setting arm from the keybinding-dict replay The Won-to-backquote feature was reverted, so the module this replay imported no longer exists. The #11170 coverage is unaffected: the remaining arms pin the substitution itself, which never depended on that setting. Co-authored-by: Orca <help@stably.ai> * test(terminal): correct the fixture's provenance count The header said two derived cases when there are four, and counted the second layout arm as a negative when it is a positive. Each case already carries its own recorded flag and note; this stops the summary contradicting them, which matters in the one field whose whole job is provenance. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
01bcc8dca2 |
fix(terminal): claim printable keydowns structurally so committed IME text survives (#13288)
* 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> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
a47b9d1167 |
Show a preedit the IME resumes without a compositionstart (#13284)
* 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> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
34f2a62cda |
fix(pty): stop color-scheme 997 replies from painting cooked prompts (#13309)
Route cooked-echo-risk terminal replies through bounded echo-safe delivery across local, daemon, and SSH relay PTYs. Preserve repeated valid replies, bypass the daemon startup input gate, and keep ordinary input plus latency-critical replies on their existing paths. Closes #13137 Co-authored-by: bbingz <zzb@gxsmjx.com> |
||
|
|
9f7522dee9 | fix(workspace): stabilize smart entry keyboard flow (#13319) | ||
|
|
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) | ||
|
|
c6ded160b2 |
Revert the Korean Won to backquote mapping (#13312)
* Revert "fix(terminal): detach the Korean input-source probe when its setting goes off (#13283)" This reverts commit |
||
|
|
78f434dd85 |
fix(agents): deliver grok launch drafts on its composer frame (8s → 0.7s) (#13308)
* fix(agents): deliver grok launch drafts on its composer frame
Grok has no --prefill-style flag, so a launch draft (e.g. the issue URL of
a worktree created from a GitHub issue) always goes through Orca's
paste-after-ready path. That path used the default readiness signal: DECSET
2004 plus 1.5s of PTY silence. Grok shimmers its startup logo at ~12fps
until the session opens, so the quiet window never settled and the draft
fell through to the 8s hard timeout before it appeared in the composer.
Gate grok on its own composer glyph instead, anchored on the alternate-screen
switch rather than DECSET 2004: the shell that runs the launch command emits
2004 too, and its prompt may itself be the same glyph (starship, pure), so a
Codex-style anchor could paste into the shell. Grok keeps the quiet window
armed as a fallback because it renders differentially and paints the glyph
once, so a late-attaching scanner would otherwise wait out the hard timeout.
Measured against grok 1.0.0 driving the real scanner over a zsh -> grok PTY:
draft delivery moves from 8003ms to 689ms, with the URL landing unsubmitted
in the composer exactly as before.
* fix(agents): keep grok's quiet-window floor on DECSET 2004
The composer-glyph marker is anchored on the alternate-screen switch, but grok
can render inline (`--no-alt-screen`, `--minimal`, `[ui] screen_mode =
"minimal"`), where 1049h never arrives. Anchoring the quiet-window fallback
there too left those launches with no delivery path at all: readiness never
resolved, and the main-process caller drops the draft when it resolves null —
so the issue URL vanished instead of arriving late.
Give the signal two independent anchors: the marker still waits for the
alt-screen switch (so a starship/pure shell prompt can't trip it), while the
quiet window arms off DECSET 2004 exactly as the default signal does. Inline and
legacy-Windows-console launches keep their pre-existing timing; alt-screen
launches keep the fast marker path.
Verified on grok 1.0.0 over a real zsh -> grok PTY: alt-screen delivers at 687ms
via the marker, inline at 1949ms via the quiet window (the default signal
measures 1861ms on the same launch), URL landing unsubmitted in both. Adds a
recorded inline-mode trace fixture so the no-1049h path stays covered.
* fix(agents): revoke grok's alt-screen anchor when the screen is handed back
The composer-glyph anchor latched forever: once \x1b[?1049h had been seen, any
later `❯` counted as grok's composer. Two ways that pastes the launch draft into
the user's shell instead of into grok:
- grok enters the alternate screen and then dies before painting a composer;
the shell prompt that follows is `❯` under starship or pure.
- a pager or editor started from the user's shell rc enters and leaves the
alternate screen before grok is ever launched, arming the anchor against the
shell's own prompt.
Track the anchor in stream order instead of as a latch: \x1b[?1049l revokes it,
re-entering re-arms it, and a marker only counts inside a segment where the
anchor is actually held. The chunk is walked segment by segment so ordering
within a single PTY packet is honored, with a 7-char carry — one short of the
escape sequence — so a split sequence rejoins without re-walking scanned output
into a second transition. Signals with no `markerAnchorEnd` (codex, opencode,
the default) keep their existing latch semantics untouched.
Also makes the trace-replay test model the hard timeout: the real waiters settle
at 8s, so a marker landing after that is not a delivery time.
|
||
|
|
e172a51649 |
test(terminal): assert preedit visibility across the recorded IME traces (#13286)
Two IME defects shipped past this suite because every assertion here was about bytes reaching the PTY. A preedit written into a hidden overlay types blind and still satisfies all of them. Samples the composition overlay at each recorded compositionupdate and requires it to be shown, so the existing recorded corpus now covers what the user sees rather than only what the shell receives. Co-authored-by: Orca <help@stably.ai> |
||
|
|
5a84dbb564 |
fix(terminal): detach the Korean input-source probe when its setting goes off (#13283)
Gating the prefetch call site stopped the probe from ever attaching, but not from surviving. prefetchKoreanInputSource installs global focus/keydown/keyup listeners behind an idempotent listenerAttached guard, and the only teardown was test-only — so disabling the setting mid-session left the listeners live and still spawning defaults export | plutil | plutil on keyboard activity. Syncs in the effect body rather than its cleanup: cleanup also runs on tab switches and on any dep-identity change, so disposing there would detach and re-probe constantly. Also replaces the initial-probe abort guard with an epoch. It compared window identity, which is dead code in production where window is a singleton, so a stop/start could let a sleeping probe loop wake and race the new one. Co-authored-by: Orca <help@stably.ai> |
||
|
|
17cfc968cf |
Revert the terminal IME composition-ownership change (#13282)
* Revert "test(ime): restore coverage the composition-ownership change removed (#13168)" This reverts commit |
||
|
|
915050f30a |
fix(native-chat): resolve WSL Codex transcripts so Chat UI renders responses (#12473)
* fix(native-chat): resolve WSL Codex transcripts so Chat UI renders responses Codex reports a guest Linux transcript path. On a Windows host `existsSync` resolved it against the current drive (`C:\home\...`) and discarded it, and the id-based fallback only searched host roots. `resolveSessionFilePath` returned null forever while the watcher reported `watching: true`, leaving Chat UI permanently empty. Translate the guest path to its host-readable UNC twin, classifying **before** any `existsSync` probe on win32 so the `C:\home` false positive cannot fire. Adapted from #10639 with one required correction: it uses the **async cached** WSL seams (`listWslDistrosAsync` / `getWslHomeAsync`) rather than the `execFileSync` ones, which would stall the Electron main thread for up to 5s per tick of the resolve-poll loop on a cold distro. Fixes #10326 Co-authored-by: Orca <help@stably.ai> * fix(native-chat): stop the WSL transcript probe firing every poll tick Three follow-ups from review of the WSL Codex transcript fix: - The UNC translation was retried on every fast resolve-poll tick (measured 10 sync UNC stats per 100ms) because only a successful result was memoized. Gate the retry to the slow fallback cadence. - A non-empty WSL home list was cached for the process lifetime, so a distro that was still booting during the first probe stayed excluded forever. Expire both branches; getWslHomeAsync caches successes, so a refresh only re-spawns wsl.exe for the distros that actually failed. - codexSessionsDirs enumerated every distro's home eagerly, waking distros the user left stopped even for native-Windows panes. Make the WSL roots a lazy tier consulted only after the host's own Codex roots miss. The resolve-poll suite became platform-dependent and only passed off Windows; pin the platform and add explicit win32 coverage. --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: OrcaWin <alpha-eng@stably.ai> |
||
|
|
d0baa20d0e |
fix(terminal): resolve WSL file links from the execution runtime, not the worktree path (#12465)
`mapTerminalFilePath` derived the WSL distro only from the *shape* of `worktreePath`. A worktree on a native Windows drive whose project runs under the WSL runtime gets a shell whose paths are POSIX, so no distro was found, the path went verbatim to a Win32 stat probe, and the candidate was dropped — no underline, no tooltip, inert Ctrl+click. Resolve the pane's distro from the execution runtime, falling back to the old worktree-shape derivation so existing behaviour is unchanged. Note the half of #8156 covered by merged #8215 (worktree on the WSL filesystem) was already fixed; this closes the remaining gap. Fixes #8156 Co-authored-by: Orca <help@stably.ai> |
||
|
|
434959965a |
fix(terminal): stop the Korean gate caching an unknown input source as negative (#13182)
On a fresh session the Won rewrite silently did not fire — real-hardware capture on macOS 26.5.2 with 2-Set Korean read 3 of 3 Backquote presses as e2 82 a9 at the PTY, and the feature only started working after the first press or an input-source round trip. Cause: the reader returns null for 'no signal' — the IPC is not exposed yet at startup — as well as for a genuinely absent source, and refreshInputSourceId committed that as isKoreanInputSourceId(null) === false. An unknown became a confirmed negative that nothing retried, because a keystroke-triggered refresh is async and cannot classify the press that triggered it. Leaves the cache unknown on a null read and retries the startup probe with bounded backoff, so the gate is warm before the first key. Bounded because each probe spawns defaults export | plutil | plutil. Co-authored-by: Orca <help@stably.ai> |
||
|
|
d64ccc71bd |
fix(terminal): break the ConPTY foreground livelock that froze a repainting TUI (#12463)
`holdForeground` and `coalesceForeground` each cancelled the other's fallback timer on the Windows ConPTY DEC 2026 (synchronized output) path. A continuously repainting TUI such as Codex therefore left the foreground latch stuck open, so every later chunk was held instead of coalesced and output never reached the visible pane until the tab was refreshed. Break the mutual cancellation and mirror the hidden path's scan on the foreground path, carrying a marker tail so a ConPTY-split DEC 2026 marker is still detected. Nothing was wrong with Flutter — it was simply a long-running command behind a repainting TUI. Fixes #8754 Co-authored-by: Orca <help@stably.ai> |
||
|
|
42fc5375e8 |
perf(terminal): gate the Korean input-source probe on its setting (#13181)
prefetchKoreanInputSource ran under a bare isMac check, so every macOS user paid for it. Each refresh shells out to `defaults export | plutil | plutil` — four processes in the main process — and input-source toggle keys pass force: true, so one Caps Lock press costs two probes. terminalKoreanWonToBackquote defaults to false. Threads the setting through KeyboardHandlersDeps and into the effect's dependencies, so enabling it mid-session still warms the cache before the first Backquote. Co-authored-by: Orca <help@stably.ai> |
||
|
|
25a8c517e1 |
test(ime): restore coverage the composition-ownership change removed (#13168)
* test(terminal): pin the recorded Korean commit-before-newline order (STA-3132) Recorded first-party on Windows 11 + Microsoft Korean (HKL 0412) against the defect-era v1.4.164 build, with bytes read on the far side of the PTY: the terminal received ea b0 80 0d, the syllable strictly before the CR. The capture did not reproduce the suspected deferred-newline inversion. That route needed a session end carrying dataPendingReconciliation, which plain compose-then-Enter cannot produce because the IME finalizes first and the newline is never held; back-to-back arms at 25/60/120 ms did not reach it either. The test therefore pins the ordering rather than discriminating a fix. Co-authored-by: Orca <help@stably.ai> * test(terminal): restore Hangul back-to-back flush coverage deleted with the composition layer #12278 fixed a Hangul syllable that was not flushed before the next composition began — the force-end path, and the one that leaves stale glyphs behind. Returning composition ownership to xterm deleted both that patch and its test, so nothing guarded the behavior any more. Replays the recorded back-to-back arms (25/60/120 ms, read as 가\r나 at the PTY) against a real xterm Terminal. It passes on main: stock xterm flushes the committed syllable natively, so the removal was safe rather than a silent regression. Co-authored-by: Orca <help@stably.ai> * test(mobile): pin accessory-byte ordering behind a Hangul commit Returning composition ownership to xterm deleted the accessory-input commit tests along with the hook they targeted, but the guarantee they protected is user-visible and still applies: an accessory-bar keystroke must not overtake the syllable being committed, and must be suppressed when that commit fails. Drives the current hook with an Android composing-region trace rather than reconstructing the deleted coordinator. Co-authored-by: Orca <help@stably.ai> * test(terminal): replay recorded IBus and fcitx5 Hangul traces offline Commits interleaved with ASCII (한abc글) are the Linux IME gesture users report on, and its failure modes are a lost syllable and a doubled one. That gesture was only covered by tests/e2e/terminal-linux-ime-native.spec.ts, which needs a Linux host running a real input framework. Fixtures are the recorded captures from the sealed linux-final evidence run, replayed against a real xterm Terminal: exact onData, exactly-once counts across five repetitions, and the PTY bytes the recorded run actually received. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
f858c5a13a |
test(daemon): stop skipping killStaleDaemon checks on Windows (#13154)
Three guards in daemon-health.test.ts skipped tests that assert PID-record and start-time logic. The file's daemonTestSocketPath already returns a real named pipe on win32, so none of them needed a Unix socket — the skips cost coverage for nothing. Verified on a Windows host: the file goes from 29 passed / 4 skipped to 32 passed / 1 skipped, restoring Windows coverage of killStaleDaemon's ownership decisions, which #12882 changed. The fourth guard stays: that test spawns a real child and binds a filesystem socket path, which fails with listen EACCES on Windows. |
||
|
|
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. |
||
|
|
c3bf22b9a8 |
[P2] perf(windows): stop the capability poll respawning blocking wsl.exe probes (#11698)
* perf(windows): stop the capability poll respawning blocking wsl.exe probes #11295 added a 30s renderer interval to `useWindowsTerminalCapabilities` whose early-return only fires when WSL is available with at least one distro, so on the common Windows host (no WSL) it re-ran a full capability read forever. Each read IPCs four probes whose main-process handlers were synchronous `execFileSync` calls to wsl.exe/pwsh.exe, blocking the Electron main event loop for up to 5s a time. The un-latching intent is kept: a host that answers "no WSL" is still re-checked, now on an exponential backoff (30s, +60s, +120s) that parks once the answer stops moving, re-arms on window focus, is shared by all consumers of an owner key, and stops entirely when the last consumer unmounts. The wsl/pwsh IPC handlers now use async twins that share the existing caches and back off identically. * fix(windows): classify async wsl/pwsh probe failures with the execFile error shape The async twins feed `execFile` callback errors into classifiers written for `execFileSync`: a non-zero exit lands on `error.code` as a number rather than `error.status`, and a timeout is a SIGTERM kill rather than ETIMEDOUT. So a Windows host without WSL (wsl.exe ships in System32, so it exits non-zero instead of ENOENT) was cached as retryable, shrinking the shared window from 10min to 45s and making the still-sync callers re-pay their blocking spawn ~13x more often; and a pwsh cold start past 5s cached "pwsh missing" for 30s, demoting the user's PowerShell 7 preference — the exact case the ETIMEDOUT branch exists to prevent. Also drops a literal NUL byte from the new re-probe module's signature separator, which made the file binary to git, and seeds `lastProbeAt` at registration so focus churn right after mount cannot defer the first re-probe indefinitely. Co-authored-by: Orca <help@stably.ai> * perf(windows): route relay host-capability probes through the async wsl/pwsh twins A paired web/mobile client resolves `useWindowsTerminalCapabilities` to a local target (TabBar's `isWebClient` gate, and `useSettingsNavigationMetadata` forces `{kind:'local'}`), so the new re-probe arms there too. But `window.api.wsl/pwsh` on a web client is not the ipc/app.ts channel — it is `host.wsl.*`/`host.pwsh.*` over the runtime RPC, which still ran the sync probes and blocked the desktop main event loop on `execFileSync('wsl.exe' | 'pwsh.exe')` for up to 5s per call. Switch those handlers and the relay preflight capability probe to the async twins added here; they share the same caches, dedupe and backoff, so remote callers see no behavior change. * fix(windows): harden async capability reprobes * fix(windows): dedupe PowerShell shell probes --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
f968583e95 |
fix(remote): stop a reachable Orca server with a closed workspace window from reading Ready (#12477)
A remote Orca server whose workspace window is closed keeps answering status RPC, so Settings > Available Hosts showed "Ready" and the status bar showed "Connected" while every graph-backed operation failed. Adds the shared predicate `isRuntimeWorkspaceWindowClosed` (`graphStatus !== 'ready' && desktopWindowStatus === 'openable'`) and one host-health derivation with a new `workspace-window-closed` state, consumed by both surfaces. Hosts that omit `desktopWindowStatus` are unaffected, so the connected-host count and overall dot do not regress. Fixes #12350 Co-authored-by: gatsby74 <gatsby74@users.noreply.github.com> |
||
|
|
6ea99f6607 |
fix(runtime): isolate same-path folder workspace PTY identity (#12474)
Folder-project workspace ids (`repoId::/path::workspace:<uuid>`) were compared via the suffix-stripping `splitWorktreeIdForFilesystem`, so every workspace sharing one directory compared equal at ~35 runtime call sites — PTYs leaked between siblings and paired/mobile clients hung on "Loading terminal". Both identity helpers now use the suffix-preserving `splitWorktreeId`, `stopTerminalsForWorktree` routes through the shared helper, and `findResolvedWorktreeIdForPath` gains a `targetWorktreeId` tie-break. Co-authored-by: dgk-dev <dgk-dev@users.noreply.github.com> |
||
|
|
a7e31e5e10 |
chore(mobile): apply the supply-chain release-age gate to mobile
mobile/ had no .npmrc, so unlike the root workspace it would resolve packages published moments ago. #13113 surfaced this concretely: it pulled nanoid 3.3.18 and postcss 8.5.26 at 0.4 and 1.7 days old, both newer than anything the root gate would have allowed. Copies the root's minimum-release-age=4320 (3 days). Deliberately not shamefully-hoist -- that one is Electron-specific and would change how mobile hoists. Re-resolves nanoid to 3.3.17 and postcss to 8.5.25 in the same commit because the gate is otherwise unusable: pnpm install fails with ERR_PNPM_NO_MATCHING_VERSION on the locked nanoid 3.3.18. Both picks stay above their advisory floors (CVE-2026-67213 needs >=3.3.17, CVE-2026-69153 needs >=8.5.23), so this is not a security regression. Co-authored-by: Orca <help@stably.ai> |
||
|
|
757b785e43 |
fix(deps): resolve Dependabot security alerts across root and mobile (#13113)
Clears 47 of 49 open Dependabot alerts across the root and mobile lockfiles. The 2 remaining (image-size) have no patched upstream release. Direct bumps: pdfjs-dist 5.7.284 -> 6.2.108 (CVE-2026-16633), mermaid 11.16.0 -> 11.16.1 (root + mobile), dompurify 3.4.12 -> 3.4.13. In-range re-resolves: brace-expansion, fast-uri, hono, ip-address, js-yaml 4.3.1/3.15.1, nanoid, postcss, tar, undici 6.28.0/7.29.0. Drops the @modelcontextprotocol/sdk>@hono/node-server override by bumping shadcn's transitive SDK to 1.30.0, which widens its range to ^1.19.9 || ^2.0.5 so @hono/node-server resolves to a patched 2.1.0 on its own. The other two overrides must stay: monaco-editor hard-pins dompurify 3.2.7 and xcode wants uuid ^7.0.3, both vulnerable. pdf.js 6 removed PDFDocumentProxy.destroy(); PdfViewer now tears the document down via the loading task it was already destroying. Supersedes #13074, #13090, #12960, #12952. Co-authored-by: mondaychen <monday.chen@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai> |
||
|
|
597c84f36c |
fix(rate-limits): read Kimi usage credentials from the configured WSL runtime (#12475)
* fix(rate-limits): read Kimi usage credentials from the configured WSL runtime Kimi's usage fetch always read the Windows host's ~/.kimi-code, so a Kimi CLI running inside WSL rotated only the WSL-side token and the status bar was stuck on 'Run Kimi to refresh'. Resolve the Kimi home from the local-account runtime target (mirroring Codex's getDefaultWslDistro()/getWslHome() UNC pattern), pinned to host off Windows, and keep KIMI_CODE_HOME host-only. Fixes #12370 Co-authored-by: Orca <help@stably.ai> * fix(rate-limits): bound and offload the Kimi credentials read for WSL homes Adopted from @cengiz-io's #12372: read credentials through createAuthFilesystemOperation (async, per-path dedup, AbortSignal bound) so a stopped distro degrades to an error instead of parking Electron main on a UNC read. ENOTDIR joins ENOENT as "not signed in" to keep existsSync parity, and the WSL runtime target is now probed with the async wsl.exe helpers. Co-authored-by: Orca <help@stably.ai> * test(rate-limits): build Kimi credential-path expectations with path.join The host-home assertions hardcoded POSIX separators, so they only passed on a POSIX runner — on a Windows dev machine `join` emits backslashes and all four cases failed (three assertion mismatches plus a WSL-suite fixture whose map key never matched the path the fetcher read). --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: OrcaWin <alpha-eng@stably.ai> |
||
|
|
940f2ff1e4 |
fix(terminal): quote agent resume for the tab's real Windows shell (cmd.exe) (#12476)
* fix(terminal): quote agent resume commands for the tab's real Windows shell
Cold restore and sleeping-agent resume built their launch line without the
host shell family, so win32 fell back to PowerShell argv quoting. On cmd.exe
tabs those quotes arrived literally and agent CLIs rejected the resume argv
and permission flags after a reboot ("unexpected argument ''<uuid>'' found").
Both call sites now share resolveAgentResumeLaunchTarget, which resolves the
launch platform and the live shell family together via
resolveLocalWindowsAgentStartupShell, honoring a per-tab shell override for
cold restore and leaving SSH / remote-runtime / WSL workspaces on their own
default quoting.
Fixes #12320
Co-authored-by: Orca <help@stably.ai>
* test(shared): cover cmd.exe resume quoting at the plan layer
Adopted from #12321 by @CountClaw.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
|
||
|
|
8cdfa64e68 |
fix(grok): keep the Windows hook safe under an inherited /v:on (#12479)
A bare `setlocal` inherits delayed expansion from the caller, so every `!` in a percent-expanded value on the curl line is eaten as a delayed reference -- mangling paneKey and dropping worktreeId. `!` is legal in a Windows path. Also adds the test file the script never had: structural checks that run on every platform, plus real cmd.exe runs covering unset/empty/normal/trailing backslash/envelope/`!`/inherited `/v:on`. Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
f8786d5224 |
fix(agent-history): fold trailing-slash, NFD/NFC, and project-fallback folder group keys (#12458)
Co-authored-by: Orca <help@stably.ai> |
||
|
|
02a1251c2d |
fix(native-chat): classify diff lines whose content begins with -- or ++ (#12459)
* fix(native-chat): stop diff colouring from misreading -- / ++ content lines as file headers diffFromText skipped every line starting with --- / +++ as a file header, so a deleted SQL/Lua '-- comment' (git emits '---<content>') or an added '++flag' fell through to gray context with its marker still attached — and when it was the only change, the two-marker gate dropped the coloured diff entirely. Detect real headers structurally instead: an adjacent '--- <old>' / '+++ <new>' pair outside any hunk. A hunk header or 'diff --git' line now also proves the text is a diff, so a genuine single-line change renders while prose keeps the guard. Co-authored-by: Orca <help@stably.ai> * test(native-chat): adopt #12335 diff-collision vectors and add mobile parity Pulls in @YuriNachos's test vectors from #12335 (header-less --- deletion, an adjacent --x/++y content pair, mobile re-export parity) and adds the spaced -- / ++ pair inside a hunk, which the pair-only rule in that PR misreads. Co-authored-by: Orca <help@stably.ai> * fix(native-chat): keep bare --- / +++ rules out of the diff marker count Dropping the `---`/`+++` prefix exclusions made a bare `---` — a Markdown thematic break or YAML document separator — classify as a deletion. Tool results routinely carry those, so `---\na: 1\n---\nb: 2` went from correctly rejected to rendering as a red diff. A bare rule is never a file header (those need a path after the marker) and is only content inside a hunk, so treat it as meta when outside one. Fold the separate `isStructuredDiff` scan into the same pre-pass and skip non-marker lines early, so the added guard costs no extra traversal: 5.1 -> 4.3 us per 120-line prose result, diff path unchanged. --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
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> |
||
|
|
f2d62a7887 |
fix(i18n): localize the status bar Resource Manager tooltip and remote-host count (#12478)
* fix(i18n): localize the status bar Resource Manager tooltip and remote-host count The Resource Manager tooltip/aria label and the SSH segment's host count were assembled from bare English literals inside helper functions, so they stayed English under every non-English UI language while the labels around them translated. Route them through the catalog with _one/_other plural keys and whole-line messages (locales reorder and repunctuate the summary), and add en/es/ja/ko/zh entries. Root cause of the miss: audit-localization-coverage bailed on any ancestor binary expression whose operator was not `+`, which hid every string under a `cond && <JSX/>` guard or a `?? 'fallback'` — including this segment's 'Connecting…'. Only comparison operands are code, so keep `??`, `||` and `&&` walking, and localize the four real strings that surfaced. Co-authored-by: Orca <help@stably.ai> * fix(status-bar): flag the space-scan tooltip row instead of matching its English text The tooltip tinted a row with `line === 'Space scan ready'`, so routing that copy through the catalog silently dropped the tint in every translated build. Return `{ text, emphasized }` and let the segment read the flag. Adopted from #12439 by @smwbev. Co-authored-by: Evgenii <smwbev@users.noreply.github.com> Co-authored-by: Orca <help@stably.ai> * fix(status-bar): key Resource Manager tooltip rows by role instead of array index Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
87d1d1a8dc | fix(terminal): preserve pinned scroll across live redraws (#12783) | ||
|
|
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> |
||
|
|
a7ffb244e4 |
perf(terminal): bound the reattach payload agent-signal scan (#12681)
hasCursorAgentReattachPayloadScreenSignal built a char-by-char copy of the entire reattach payload so it could read the last header plus 5000 chars. On a 2MB daemon snapshot that cost 17.5ms of synchronous renderer main-thread work — ~75% of what xterm then spends parsing the same bytes — and the miss case paid it in full for a result that is always false. Two changes, both matching existing in-tree precedent: bound the scan to a 256KB tail (as the kitty tracker already bounds its own scan), and strip via the shared precompiled CSI_SEQUENCE_PATTERN instead of a hand-rolled loop, which is also faster in V8 because it copies spans rather than building a rope per character. 2MB snapshot, header hit 17.5ms -> 0.80ms (22x) 2MB snapshot, miss 8.7ms -> 0.52ms (17x) 200KB snapshot, header hit 1.5ms -> 0.62ms (2.4x) config/scripts/terminal-reattach-payload-scan-benchmark.mjs reproduces this and asserts every candidate agrees with the baseline before timing it. It also records a negative result: porting the daemon mouse mirror's includes() pre-filter to the kitty tracker makes reattach slower, because snapshots always contain the introducer. Adds guards for the two behaviours a future shortcut would silently break: a CSI-split header must still match, and a header behind the tail bound must not. Also byte-pins POST_REPLAY_REATTACH_RESET_KEEP_MOUSE, which shipped unpinned. 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> |
||
|
|
7da9368b78 |
fix(terminal): fence detached daemon endpoint ownership (#12709)
* fix(terminal): fence daemon endpoint ownership * fix(terminal): clean failed daemon PID claims * fix(terminal): close daemon ownership review gaps * test(daemon): release startup IPC in boot smoke * test(daemon): mirror production stdio in boot smoke * fix(daemon): exit after rpc shutdown cleanup * fix(terminal): make the socket name the daemon endpoint authority The reported failure was a live daemon hosting PTYs that nothing could reach: terminals acknowledged input and never ran it, listings diverged from reality, and restarting the app never helped because the detached helper survived. The ownership fence added for it could not fire in the sequence that produces the split brain. libuv unlinks the pathname a server bound to when that server closes, with no ownership check. A daemon that lost its endpoint name therefore deleted whichever socket then sat at that path — including a live replacement's — stranding a daemon that still hosted every session. Bind a private same-directory name and hard-link it into place instead: libuv can only ever unlink our own bind name, the exclusive link is a kernel-enforced endpoint claim, and the canonical name is removed only under an inode ownership check. The bind name replaces the basename rather than extending it, so it cannot overflow sun_path. killStaleDaemon removed the PID record unconditionally immediately before every fork, so the exclusive PID claim was always uncontested at bind time. It also unlinked a live daemon's endpoint whenever a connect probe merely timed out, and treated a `ps` timeout as proof of PID recycling. Now only positive evidence of a dead endpoint authorizes reclaiming it, SIGKILL is confirmed rather than assumed, and a daemon that cannot be proven stopped keeps its record and endpoint while the launcher refuses to fork beside it. A daemon whose endpoint was taken over now retires itself, draining rather than killing, so an unreachable orphan stops being permanent. A repaired PID record re-derives entryPath, appVersion and the Linux incarnation markers from the authenticated owner instead of dropping them; without appVersion a healthy daemon read as a permanently stale bundle and, on Windows, went unpinned against daemon-host pruning. Repair failure now fails open — abandoning a healthy daemon over a pid file write cost every persistent terminal on the machine. Also: treat only ENOENT as an unclaimed record so a Windows file lock is not reported as an ownership conflict; settle start() before close() so an accepted connection cannot defer it forever; sweep abandoned claim and bind names; and type the endpoint-identity seam so a rename cannot silently disable the fence. Adds a real-process handover smoke that reproduces the failure with two daemons racing one endpoint, and wires it into the native-smoke job. * fix(daemon): retire only on proven endpoint ownership loss The ownership watchdog read a null identity for any stat failure, so a transient EACCES or EIO on the runtime directory would retire a daemon that was still serving every terminal on the machine. Distinguish "the entry is gone" from "the probe failed" and act only on the former. Also require the loss to persist across two polls: a replacement publishes by unlink-then-link, and a single observation can land in that gap. * fix(daemon): source repaired ownership metadata from the authenticated hello Adversarial review found three defects in the previous two commits. Re-deriving entryPath from the owner's command line truncated it at the first space. A command line is a single space-joined string, so `C:\Program Files\Orca\...` and `/Applications/Orca 2.app/...` came back as `"C:\Program` and `/Applications/Orca`. getDaemonLaunchIdentity treats a present entryPath as authoritative, so a healthy daemon read as `different_app_path` and was killed and re-forked — worse than the missing-metadata case the derivation was added to fix. Carry entryPath and appVersion as optional fields on the daemon hello identity instead: the daemon already has both from its own argv, and per docs/reference/remote-wire-compatibility.md a new optional field is safe because every reader falls back when it is absent. This also removes a synchronous `ps` spawn from the Electron main thread during startup. `start()` rolled back the PID record even when it never published one. Losing the endpoint link now runs that path, and the ownership-checked unlink briefly renames the incumbent's record aside — enough to strand a live daemon's ownership. Roll back only what we actually wrote. publishDaemonSocketPath read its identity from the canonical name after linking, so a concurrent unlink returned null: no ownership watchdog and no endpoint cleanup on any shutdown path. Read it from the bound name before linking, which shares the inode. Refusing to fork beside an unconfirmed daemon left the user with no daemon at all and no in-app recovery, since restart re-entered the same fence. We have just proved something answers the endpoint, so adopt it in degraded mode: live sessions keep working, fresh terminals run locally. SIGTERM is also individually guarded now — an EPERM fell into the blanket catch and reported "nothing alive", authorizing the very duplicate this fence exists to prevent. Also reset the ownership-loss streak on an inconclusive probe so the confirmations are consecutive, and sweep scratch names before the launch so a failed launch still reclaims them. |
||
|
|
aca5e8b5b1 |
fix(terminal): keep a live TUI's mouse modes across daemon reattach (#12461)
Co-authored-by: Orca <help@stably.ai> |
||
|
|
e233d6a641 |
Recover automatically instead of getting stuck on "Multiplexer disposed" when an SSH relay drops (#12216)
* fix(ssh): recover instead of wedging when the relay channel dies mid-connect Three coupled defects made a dropped SSH relay look like a permanent bug: 1. SshRelaySession.establish()/reconnect() ran their last liveness gate before configureRelayGraceTime(), whose mux.notify() can dispose the mux synchronously (writer control-lane admission cap, or a throwing transport). The session then latched _state='ready' + _onReady (status bar "connected") while watchMuxForRelayLoss() silently no-op'd on the dead mux, so the bounded relay backoff in ipc/ssh.ts never ran and the fs/pty/git providers stayed registered against a dead multiplexer. Both sites now re-check mux.isDisposed() after the notify and take the existing failure path. 2. SshChannelMultiplexer.request()/notifyWithSettlement() always reported the permanent-shutdown string 'Multiplexer disposed' with no code, even when the recorded dispose reason was connection_lost. The reason is now recorded and a shared disposedError() factory serves dispose(), request(), notifyWithSettlement(), so a transient drop reports 'SSH connection lost, reconnecting...' / CONNECTION_LOST. onDispose() on an already-disposed mux now fires the handler synchronously with that reason instead of returning a silent no-op (without retaining it). 3. TerminalErrorToast no longer renders a transient relay drop in the red "please file an issue" style. The marker is matched with includes() because the message reaches the toast IPC-wrapped. ssh-git-response-stream-reader registers its onDispose subscriber after the abort wiring, since an already-dead mux now fails synchronously there and the cleanup must be able to drop the caller's abort listener. Closes #11953 * fix(ssh): treat a mux killed during PTY reattach as relay loss reconnect()'s post-reattach gate bare-returned when ownsAttempt() went false, and reattachKnownPtys swallows every per-PTY error, so a control-lane failure during a large reattach burst disposed the mux without ever reaching the catch: providers stayed bound to the dead mux, no relay-loss watcher was installed, and the session wedged in 'reconnecting' until restart. Take the failure path when our own mux is the one that died so ssh.ts's bounded backoff retries. Co-authored-by: Orca <help@stably.ai> * fix(ssh): recover when relay dies during setup instead of wedging Introduce verifyRelayAttempt() to detect mux disposal at each setup phase (consumer session, home resolution, provider registration, PTY reattach). Routes mid-setup connection loss into relay-loss recovery instead of hanging in reconnecting state. * Extract SSH disposal error factory Multiple sites were duplicating the disposal error creation logic with specific message and code values. The renderer uses these to distinguish temporary disconnects (show reconnection overlay) from permanent shutdown (show error toast), so all producers must use the same factory to avoid silent UI degradation. --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> |
||
|
|
e5f49e0e1d | fix(sidebar): preserve meaningful workspace status indicators (#12658) | ||
|
|
5f187e0836 |
fix(renderer): break Activity and terminal React #185 loops (#12600)
Breaks two independent React #185 (Maximum update depth exceeded) crash loops. - Activity portal publication is idempotent by descriptor value, so a semantic no-op no longer bounces synchronously through Terminal and back into Activity. - The Activity readiness burst budget survives slot, target, pane, and tab retargeting, and a quiet loading pane is rechecked when the window expires. - Terminal cold-parking pins verdict bursts to the safe mounted side before React reaches its nested-update limit, with an expiry so tabs can park again. Supersedes #12492 and #12485. Portal descriptor equality is keyed off keyof ActivityTerminalPortalTarget so a new field fails the build instead of silently suppressing a publish. Park-verdict damping and breadcrumbs gate on pin liveness, and churn crumbs coalesce by trigger so a burst cannot collapse into a slow-churn slot. |
||
|
|
69ca9f91b3 |
fix(status-bar): invalidate the CLI session count on kill and restart (#12468)
* fix(status-bar): invalidate the CLI session count on kill and restart `pty:management:killOne` / `killAll` / `restart` tear sessions down via `adapter.shutdown()` and broadcast nothing — unlike `pty:kill`, which ends in `sendPtyExitToRenderer`. The status-bar count is an event-sourced cache, so killing sessions from Manage Sessions or "Kill all terminals" left the `>_ N` chip frozen until the popover was opened, which itself triggers a refresh. > [!NOTE] > The dual-source split described in the issue text was already fixed by merged #9387. This closes a *different* remaining invalidation gap that produces the same reported symptom. Broadcast the teardown so the chip updates without needing the popover opened. Fixes #8372 Co-authored-by: Orca <help@stably.ai> * test(e2e): add recordable proof for status-bar-cli-session-count Fails on origin/main, passes on this branch. Test: drops after Manage Sessions kills a foreign daemon session, popover never opened Co-authored-by: Orca <help@stably.ai> * fix(status-bar): avoid duplicate inventory refresh after kill all --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
7956335cea |
fix(setup): stop caching an unreadable orca.yaml as "no setup script" (#12469)
* fix(setup): stop caching an unreadable orca.yaml as "no setup script"
`checkRepoHooks` returned `{hasHooks:false, hooks:null, mayNeedUpdate:false}` with no `status` field when the SSH filesystem provider was unavailable, and inside a blanket catch for any read error. The renderer only bails on `status === 'error'`, so that status-less false negative was cached as an authoritative "no setup script" and the prompt stayed on screen.
Mirror the `hooks:check` IPC twin exactly: `status:'error'` for a missing provider, ENOENT-aware in the catch, `status:'ok'` on the folder-repo, binary, SSH-success and local branches.
Fixes #8752
Co-authored-by: Orca <help@stably.ai>
* test(e2e): add recordable proof for setup-script-prompt-false-negative
Fails on origin/main, passes on this branch.
Test: recovers from an unreadable orca.yaml instead of pinning the failed verdict
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
|
||
|
|
7c26cceaf1 |
fix(workspaces): add space after emoji picker selection
Add a Slack-style trailing space after selecting an emoji from the create-worktree colon picker, while preserving existing separators and caret placement. |
||
|
|
fb27702100 |
feat(updater): restart hourly build numbers per version, restyle the timestamp (#12587)
The number answers "which build of 1.4.163 is this", so carrying it across versions made it meaningless — 1.4.164 opened at 38 for no reason a reader could see. It now counts titles matching the base version being built, so a version bump restarts the series at 01. Deriving it moves from workflow jq into the script, because the number depends on the base version and only the script knows which base the published tags resolved to. Timestamps go from `07-31 13:54` to `Jul 31, 1:54PM`, still Pacific. Co-authored-by: Orca <help@stably.ai> |
||
|
|
4734428654 |
fix(i18n): stop the repair policy de-localizing CJK UI labels
The build-time repair layer carried overrides that rewrite already-correct CJK values back to English. Most are inert against today's catalogs but fire on the next regeneration, so they read as latent regressions rather than policy: - zh workspace status picker (Play/Flag/Zinc/Rose/Emerald/Amber/Violet/Sky/ Blue/Neutral) and `sheet`/`page` were pinned to English while every sibling option, and ko/ja/es, stay translated — half a Chinese picker. - The zh `蓝色的`/`琥珀色`/`中性的` phrase fixes correctly flagged the adjectival 的 form but replaced it with English instead of the bare color noun. - ja `Play` was pinned to English though the catalog already reads 再生. - A value-wide zh `Open: '进行中'` mapped every "Open" to "in progress", including the button that opens an MCP config file. "Open" is a verb (打开) on buttons and a state (开放) beside 已关闭, so no single mapping fits. Catalog corrections in the same area: - The GitHub/PR state picker key override read 진행 중 / 进行中 for ko and zh while ja already had the correct オープン; now 열림 / 开放, matching 닫힘 / 已关闭 on the sibling entry. - The terminal cursor-color group is the on-screen cursor, not the Cursor editor; ja already had カーソル, ko/zh now get 커서/光标 instead of "Cursor". - Tailwind swatch labels 天空 (the sky) and 锌 (the metal) do not read as colors; now 天蓝/锌灰, and ja 空 becomes 空色. - The ko disk-usage heading was pinned to bare "Space" while its own description says 저장 공간; both now use 저장 공간. Two policy tests pinned the Play de-localization. They diagnosed the input correctly — 玩 / 遊ぶ are wrong for a play icon — so the expectations move to 播放 / 再生 rather than English. Destructive drift (localized -> English on regeneration) drops from 40 to 21 for zh, 6 to 5 for ja, and 5 to 3 for ko. What remains is deliberate: search qualifiers, path and filename literals, and product names. |
||
|
|
aa001101f2 |
fix(i18n): repair the stale SshTargetForm ko override
The override for SshTargetForm.137e88ce8d held a truncated relay-TTL sentence ending mid-clause at "최대:", which renders "Timeout after disconnect (seconds)" — the sibling key 55c56cf2c7 — not its own English source. #12192 only corrected the terminals token inside that wrong sentence. Point it at the value ko.json already ships, so a catalog repair cannot overwrite the correct string with the wrong one. |
||
|
|
5adc5d06c8 |
fix(i18n): pin the Orca Mobile "New" badge override and correct ja
The badge value is pinned in locale-key-overrides.mjs, so the ko/zh fix from #10664 would have been reverted by the next catalog repair. ja carried the same defect — 新規 reads as "create new" — and is corrected alongside. |
||
|
|
2548b816c0 |
Keep the app responsive when security software slows process creation (#12217)
* fix(ports): keep the app responsive when security software slows process creation Orca ran the workspace port scan's probe commands (lsof/ps on macOS, netstat + powershell.exe on Windows) directly in the Electron main process. libuv performs process creation inline on the calling event loop, which in the main process is the browser UI thread, so an endpoint-security module hooking CreateProcessW froze the whole window for the length of the spawn. The same stall also produced a false diagnosis: the 4s command watchdog was armed before execFile (local-workspace-port-scanner.ts:389 -> :410), so its deadline had already passed by the time the command started. Every scan on a hooked host reported a command timeout, tripping the 60s -> 5min backoff and the "Port scanning is temporarily paused after a command timeout" banner even though the commands themselves were healthy. Probe commands now run on a lazily created, unref'd worker thread with FIFO one-at-a-time dispatch, and the watchdog is armed after execFile returns so it measures the command rather than the spawn. Node's own execFile timeout kill (killed: true) is classified as a command timeout, keeping the backoff working for genuine hangs. A scan that observes a stalled spawn skips its optional metadata commands for that cycle, capping a hooked-host scan at roughly one stall instead of three. Closes #11161 * fix(ports): keep advertised URLs when a stalled spawn skips port metadata Review follow-up on #11161. The stalled-spawn early return handed scanWorkspacePorts raw ports with no cwd/commandLine, so every port failed attribution and reconcileAdvertisedUrls told the watcher each worktree's listeners had vanished. shouldEvictAfterScan then deleted every cached advertised URL and broadcast a removal event; those URLs are only ever captured from live PTY output, so the dev-server link was gone until the server restarted. The scanners now report metadataAvailable, and reconciliation is skipped for a scan that never gathered attribution evidence. The skip is also no longer self-perpetuating: on an EDR-hooked host every spawn stalls, so gating purely on the current scan's spawnMs made every port permanently external (Stop refused with 'Only workspace-owned local processes can be stopped here.'). Metadata is now re-probed on the scan after a skip, matching what the comment and test name already claimed. Co-authored-by: Orca <help@stably.ai> * test(windows): stop a temp-dir lock from failing the CLI launcher smoke test The native launcher assertions passed on windows-latest, but teardown's rmSync raced Windows' release of the image handle on the exe the test had just executed and threw EPERM, failing the job. Cleanup now retries and, on Windows only, tolerates a residual lock code instead of reporting it as a launcher regression. Co-authored-by: Orca <help@stably.ai> * fix(ports): scope the metadata skip away from attribution-dependent scans The metadata skip was a process-wide parity flag, so Stop and the localhost-label allowlist could land on a degraded cycle and reject a port the panel had just shown as workspace-owned. Give those callers an explicit requireMetadata option, and carry the previous cycle's listener metadata forward so a skipped background scan no longer republishes workspace ports as external. Also pin the watchdog ordering: the stall in the execution test was shorter than the watchdog budget, so a watchdog armed before execFile still passed. * build: guard worker-thread entries against electron imports (#11161) Electron's module is not registered on worker threads, so require("electron") throws "Cannot find module 'electron'" inside a main-process worker and kills it at startup (verified on Electron 43.1.0). plain-node-entry-guard covered only forked plain-Node entries, so the five worker entries relied on hand-written "must stay electron-free" comments. The port-scan probe worker is one import away from port-scan-command-client.ts, which deliberately contains require('electron'). A violation there fails closed at runtime while every unit test still passes, because the client's require is try/caught on the main thread. Covers stt-worker, warp-theme-parser-worker, session-scanner-opencode-sqlite-worker-entry, main-thread-hang-watchdog-entry and port-scan-command-worker-entry. The scan is transitive over the emitted chunk graph, so a shared chunk that reaches electron is caught too. Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> * test(windows): retry teardown for main's duplicate-PATH launcher fixture Main's new csc-compiled harness runs an exe from the temp tree, which is exactly the image-handle/AV lock the merged-in removeFixtureTree retry exists for; its bare rmSync would report a teardown lock as a launcher failure. Co-authored-by: Orca <help@stably.ai> * test(ports): pin the packaged-asar worker entry path resolveWorkerEntryPath's packaged branch never runs in dev or e2e, so the path construction had no coverage. Split the electron read out of it and unit-test both layouts. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
e1071f59e9 |
Say why an update install failed instead of stalling for three minutes (#12224)
Surfaces the real install-failure cause instead of letting a failed elevation stall silently, and keeps the reconnect wait inside its total budget by recomputing the remaining time after each awaited RPC. Relates to #11906 — this fixes the observability half. The functional half (a .deb/.rpm host cannot elevate and can never self-update) is unchanged, so the issue stays open. |
||
|
|
8f29f58ab7 |
Keep showing your workspaces when a project is too slow to answer instead of reporting zero (#12222)
A stalled per-repo git scan no longer publishes a healthy-looking empty catalog. Adds an execution-host ownership gate so a degraded host cannot republish another host's worktree rows under its own id. Relates to #11869 — this fixes the stall-publishes-zero half. The issue stays open for the remainder. |
||
|
|
96e31e7bf8 |
Remove a paired computer's deleted projects from every connected device (#12215)
* fix(repos): remove a paired computer's deleted projects from every connected device A project deleted on a paired Orca host stayed in every connected client's sidebar and could not be removed there. Two independent defects: 1. Host-local repo IPC mutations only sent `repos:changed` to the host's own renderer (src/main/ipc/repos.ts:2711). The runtime client-event stream was fed only by mutations arriving over runtime RPC, and clients refetch a remote catalog only on a `reposChanged` event -- there is no polling on desktop -- so the deleted rows persisted indefinitely. The shared `notifyReposChanged` helper now also calls the new `OrcaRuntimeService.notifyReposChangedForRemoteClients()` (src/main/runtime/orca-runtime.ts:5175), mirroring the existing `notifyWorktreesChangedForRemoteClients` precedent. This covers every repo, project-group and folder-workspace IPC mutation, so renames, colors, reorders and adds propagate too. 2. Deleting the ghost row on the client routed `repo.rm` to the owner, which answered `repo_not_found`. `removeProject` wrapped its whole body in one try/catch, so the rejection aborted the local purge before the `set()` (src/renderer/src/store/slices/repos.ts:3466) and the delete button appeared to do nothing. Only `repo_not_found` is now tolerated; any other failure still keeps the row, and an opt-in `errorFeedback: 'toast'` makes it visible at the three single-project user-initiated entry points. Bulk and background callers keep today's silence plus their own aggregate reporting. Closes #11994 Co-authored-by: Orca <help@stably.ai> * fix(repos): revert inert RepositoryPane removeProject arg The settings pane's only render site drops the argument; the toast is already delivered by removeSettingsProjectFromAllHosts. Co-authored-by: Orca <help@stably.ai> * fix(repos): scope duplicate-repo-id deletes to the owning execution host Cover the cross-host collisions #11994's broadcast now fans out to every paired device. Same-name projects on different hosts were already isolated (per-host UUIDs, host-scoped catalog merge and purge) and are pinned by regression tests. Two same-repo-id paths were not: `repo.rm` with a `path:`/`name:` selector and `deleteProjectHostSetup` both resolved one row and then deleted by bare id, taking the sibling host's registration with it. Co-authored-by: Orca <help@stably.ai> * test(mobile): align the poll-interval rationale with the new reposChanged emission Co-authored-by: Orca <help@stably.ai> * fix(repos): resolve deleteProjectHostSetup's repo row only on the setup's own host The sibling-host fallback could only ever pick a row on a host the caller did not name; with no exact match the setup is stale and the existing path already drops just the setup. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
5f9a5b80b3 |
Show "Except default branch" filter only when "Hide sleeping" is on (#12460)
* Show the "Except default branch" filter only when "Hide sleeping" is on The exemption only takes effect during the hide-sleeping sweep, so its row now renders (and counts toward the filter badge) only while its parent toggle is ticked. Co-authored-by: Orca <help@stably.ai> * Restore the sub-option indent on filter toggle rows Tailwind v4 emits px-2 as padding-inline, which outranks the physical pl-6 override, so the indented row rendered flush with its siblings. Split the base padding so the indent actually applies. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
b86880a9c1 |
Keep finished and interrupted agent sessions resumable after sleeping a workspace (#12214)
* fix(agent-sleep): keep finished and interrupted agent sessions resumable after sleeping a workspace Manual workspace sleep ran a liveness filter over the panes it was about to kill (isValidManualSleepLiveAgentEntry), then wiped every pre-existing sleeping record in the worktree. A done, interrupted, typed-into, or >30-min-idle pane therefore lost its only resume handle and woke as a bare shell. A second filter at wake discarded any record carrying `interrupted: true`, which also killed interrupted sessions across an app restart. Capture now records every resumable pane, normalizing only `updatedAt` and `interrupted` and preserving the entry's real `state` so a finished pane keeps its passive record and resumes in place when its tab is opened instead of spawning a duplicate tab. The wake-side interrupted check is gone. A legacy-orchestration-worker block is carried onto the replacement record, and the Pi-compatible promoted checkpoint is no longer overwritten by a re-derived live record. Closes #11598 * fix(agent-sleep): keep durable slept records a repeat sleep cannot re-derive removeSleepingRecordsReplacedByManualWorktreeSleep wiped every record in the worktree, and only the freshly captured set was merged back. A slept `done` pane stays passive until its tab is opened, so a second sleep found no live status row to rebuild its record from and deleted the pane's only `--resume` handle with nothing written back — the original loss of #11598, one wake/sleep cycle later. The wipe now skips a record with no replacement in the new capture set when it is a durable capture (`origin` `worktree-sleep` or `quit`). Provisional `live`/legacy checkpoints are still cleared, so an unresumable Pi row does not survive a sleep it cannot back. Co-authored-by: Orca <help@stably.ai> * fix(agent-sleep): keep a slept workspace's finished panes out of the mobile wake fan-out A manual sleep now records every finished pane, and those passive records fed wakeSleepingAgentsForWorktreeInBackground step (b), which background-mounts one tab per passive record. A phone opening a slept 12-tab workspace would have cold-restored 12 agents at once, undoing the process shedding the sleep was for. Manual-sleep captures of finished panes carry restoreOnTabOpenOnly; step (b) skips them and the pane resumes in place when its own tab is opened, which is what desktop activation already did and what the phone's per-tab mount provides. Co-authored-by: Orca <help@stably.ai> * fix(agent-sleep): close the retained-row and shared-claim gaps in slept-session capture Three narrow holes left by the manual-sleep capture rewrite, all in the same "a slept session must stay resumable" contract: - The retained pass captured `retained.entry` verbatim, so a retained row - stale by construction, since it exists only after the pane's pty died - produced a `working` record that wake then discarded on the >30min staleness rule. It now takes the same `updatedAt`/`interrupted` normalization and the same `automaticResumeBlockedBy` carry-over as the live pass. - The retained pass also ran after the `origin: 'live'` promotion loop without its guard, so a pane holding both a promoted checkpoint and a retained row had the checkpoint (connectionId, transcript identity, active `working` class) overwritten by a re-derived passive record. - The mobile wake filtered `restoreOnTabOpenOnly` records *after* canonicalization, so a lazy record sharing a provider-session claim with an eligible hibernated record could win the claim, delete the hibernated record as a duplicate, and then be skipped - stranding the session with nothing mounted. --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
f6878d660f |
Show Claude's AskUserQuestion card in desktop Chat when the agent runs on a paired headless server (#12223)
* fix(native-chat): show Claude's AskUserQuestion card when the agent runs on a paired headless host Three gaps kept the question card off the desktop when the agent ran on a remote `orca serve` host: - The `session.tabs` projection reduced HTTP agent-hook rows to identity only, hard-coding `state: 'done'` and an empty prompt, so `toolName` and the full `interactivePrompt` never left the host. It now publishes the newest fresh hook row's status fields, bounded by the same staleness window `agentType` uses, excluding `providerSessionOnly` resume rows, and yielding to live title evidence unless a question is actually pending. - Nothing republished `session.tabs` when only a hook row changed, and the re-emit carried an unchanged `snapshotVersion` that clients drop on their monotonic gate. Material hook transitions and pane/SSH status clears now bump the version and schedule a coalesced emit. - The desktop card resolved only from live status. It now falls back to the pending ask in the transcript, matching mobile, so a relay gap can no longer leave the composer mounted over a pane parked on a selector. Closes #11761 Co-authored-by: Orca <help@stably.ai> * fix(native-chat): date the hook-row recency guard against a real clock `resolveHookLiveAgentRow` compared a hook `receivedAt` (epoch ms) against title stamps that are title-observation sequence numbers, so the guard could never fire — any fresh hook row overrode live title-derived state, and a manual rename (the one epoch writer) inverted it. Stamp the live OSC title path with wall-clock ms and compare against that alone. The regression test fabricated epoch-valued title stamps production never writes; it now drives the title through `onPtyData`, and a new case pins the opposite direction (hook row newer than the title wins). Co-authored-by: Orca <help@stably.ai> * fix(native-chat): stop an orphaned tool call from pinning a dead question card extractPendingAsk pairs tool results to calls by a global FIFO (tool_use_id is dropped at decode time), so one call that never gets a result desyncs the queue for the rest of the transcript and strands an answered ask as pending. Real transcripts also hold asks the user escaped and typed past. On desktop that card replaces the composer, so the pane became unsendable. Drop in-flight calls at a turn boundary — a user turn or the decoders' interrupt row — since the turn that owned them is over. Claude's tool-result turns decode as role 'tool', so normal FIFO resolution is untouched. Co-authored-by: Orca <help@stably.ai> * refactor(native-chat): trim the headless AskUserQuestion projection Reuse rather than restate: the invalidator now takes the shared `AgentHookEventPayload` instead of a locally redeclared row shape, and the hook live row is a `Pick<>` of the retained OSC snapshot so one projection branch consumes either carrier. Fold the immediate/coalesced session-tabs emit into one method (also drops a redundant re-emit on the provider-session push). Drop card tests that re-route shared-parser assertions through React. Isolate pane-status-clear subscribers and prove the no-republish case by version arithmetic instead of a timed silence. Co-authored-by: Orca <help@stably.ai> * test(native-chat): pin the AskUserQuestion card render under real Electron Why: the 13 parser unit tests pin extraction, but nothing proved a card actually renders where an inert tool call used to. This spec reproduces the paired-headless topology from the client side — live status carrying agent identity and state 'working' but no interactivePrompt/toolName, with the pending ask present only in the transcript — and fails on main. Refs #11761 Co-authored-by: Orca <help@stably.ai> * test(native-chat): drop the unused testInfo parameter Why: oxlint no-unused-vars fails the lint gate on an unused test parameter. Co-authored-by: Orca <help@stably.ai> * test(native-chat): drop leftover proof scaffolding from the ask-card spec The env-var screenshot label and the fixed 2s settle only existed to make the pre-fix capture comparable; the card assertion already waits. Co-authored-by: Orca <help@stably.ai> * test(runtime): use a truly unresolvable pane key in the hook republish guard #11203 taught pane lookup to recover a reminted tab id by leaf id, so the old fixture (new tab id, live leaf id) resolved and bumped the snapshot a second time once this branch merged with main. Co-authored-by: Orca <help@stably.ai> * fix(runtime): refuse a hydrated unconfirmed hook row as live pane status #12346 landed on main after this branch was cut: a nonterminal row restored from last-status.json is stamped `restoredUnconfirmed` because its transition may have fired while no receiver was up, and every freshness gate treats it as never-fresh. The new headless `live` projection here only checked `receivedAt`, so a restart inside the 30-minute window would republish the hydrated row — resurrecting the AskUserQuestion card with no agent left to answer it. `agentType` still reads those rows: they prove identity, just not liveness. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: Neil <nwparker@users.noreply.github.com> |
||
|
|
29bfc1e22c |
Fix Windows git/gh attribution wrappers rejecting a piped stdin argument (#12440)
Co-authored-by: Orca <help@stably.ai> |
||
|
|
d8e5944b60 |
Stop a duplicate headless orca serve from crash-looping and exhausting AppImage FUSE mounts (#12212)
* fix(startup): stop a duplicate headless serve from crash-looping and leaking AppImage mounts A second Orca launch that loses the single-instance lock called app.quit() before `ready`. That quit is deferred, so the doomed process kept booting into Chromium's Linux display initialization, failed with "Missing X server or $DISPLAY", and died with SIGSEGV. systemd read that as a crash and restarted it forever; each restart re-mounted the AppImage and left the squashfuse mount behind, until the host hit the 1000-mount FUSE ceiling and every later launch failed. The lock-losing launch now calls app.exit(3), which terminates synchronously before any display init. Exit code 3 is a stable "another process already owns this userData profile" contract, and the documented systemd unit uses RestartPreventExitStatus=3 plus a real StartLimitIntervalSec/StartLimitBurst window so a permanently failing launch can no longer retry unbounded. Second-instance argv is now forwarded to the owner, and a duplicate `orca serve` no longer asks the live headless server to open a desktop window. Desktop activation for ordinary launches and macOS dock re-activation is unchanged. Closes #11935 * docs(headless): clear the start limit before the scripted service starts StartLimitIntervalSec=300/StartLimitBurst=5 rate-limits operator starts too, so after a crash-loop trips the burst systemd refuses a plain `systemctl start` for the rest of the window. The Upgrade and Roll back scripts run under `set -euo pipefail`, so that refusal aborted the rollback mid-flight and left the server down on the exact recovery path the doc prescribes. Both scripts (and their EXIT-trap recoveries) now run `systemctl reset-failed` first, the unit reference explains the interaction, and the crash-loop bullet points at it for manual starts. Co-authored-by: Orca <help@stably.ai> * test(startup): reproduce the #11935 duplicate-serve crash loop under real Electron The committed coverage for #11935 was source-text greps, so nothing gated the mechanism the fix rests on: pre-`ready` `app.quit()` is deferred, which is why the lock-losing headless `orca serve` kept booting into Linux display init. This runs two real Electron processes against one disposable profile. The duplicate executes the lock-loss gate's own `app.*` statement, lifted out of `src/main/index.ts`, so reverting to `app.quit()` fails the test. It also feeds the owner's real forwarded argv through `shouldActivateDesktopForSecondInstance`. Also record why the activation predicate matches `--serve` and not the `serve` subcommand: an AppImage launched as `orca serve` exits at the CLI redirect before requesting the lock. * test(startup): wait for the owner process to exit before removing its profile Windows holds the profile's handles for a beat after SIGKILL, so an immediate rmSync can fail with EBUSY/EPERM. Co-authored-by: Orca <help@stably.ai> * test(startup): pass the fixture marker path by env, not argv Chromium reorders argv and the duplicate's argv is itself under test, so a trailing positional was the wrong channel for it. Co-authored-by: Orca <help@stably.ai> * test(startup): only the activation case waits on the owner notification The exit-contract cases assert on the duplicate's own already-terminated process, so they should not block on cross-process delivery. Co-authored-by: Orca <help@stably.ai> * test(startup): drop the staged lock race, keep the real-Electron gate contract CI proved the two-process form cannot work on a display-less Linux runner: Chromium's ProcessSingleton needs the browser IO thread, which needs `ready`, which needs a display. The pre-`ready` owner looked stale and the duplicate took the lock (`expected [ 'DUPLICATE_WON_LOCK' ] to include 'DUPLICATE_LOST_LOCK'`). Lock acquisition and argv forwarding are already covered in single-instance-lock.test.ts. What only a real process can settle is what the loser does next, so that is all this file now runs -- display-independent. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
e8d4818b5a |
test(env): keep happy-dom MutationObserver callbacks alive across GC (#12438)
happy-dom holds each observer's internal callback in a WeakRef, so a GC pause under sharded CI load permanently kills a still-connected observer. That is what made the activity portal readiness test flake: once the observer died, no amount of re-applying ready DOM could release the latch. Co-authored-by: Orca <help@stably.ai> |
||
|
|
dbbeca0916 |
Let a timed-out remote terminal pane reconnect when the host session is still alive (#12213)
* fix(terminal): let a timed-out remote terminal pane reconnect again A remote-runtime PTY pane that missed the 60s auto-recovery window latched to "disconnected" permanently, even while the host kept the session alive and streaming. The cutoff destroyed every path back at once: it dropped the pending retry and evicted the pane from the shared retry registry (so window `online` and system-resume became no-ops), and the transport tore down the accepted host-snapshot listener. The listener was also inert for the common case, since a host that keeps publishing the same live handle never rotates it. The cutoff now only stops the retry timer; the pane keeps its pending retry and stays revivable. The accepted-snapshot listener survives the latch, and a post-cutoff snapshot is accepted as reattach evidence whether the host rotated the handle or republished the same one. The require-replacement published-wait guard is scoped to its own recovery epoch so Reconnect and online/resume are no longer swallowed. No new polling: a latched pane still issues zero self-initiated RPCs, and the same-handle reattach consumes a snapshot the client already receives. Closes #12097 * fix(terminal): drop the settled attach retry when the recovery cutoff lands The recovery cutoff now keeps a pending retry so online/resume can revive a latched pane, but the host-session attach wait schedules a single-shot closure that the cutoff itself resolves. Retaining it left the pane registered as revivable work: 'online' would bump the epoch, arm a fresh 60s deadline and flip the phase to 'recovering' while invoking a no-op, hiding the working Reconnect button and the same-handle snapshot reattach for a full minute. The attach wait now discards its own scheduled retry as it settles, so a pane latched on that path stays 'disconnected' and Reconnect keeps working. Retries scheduled by the resubscribe paths re-enter real work and are unaffected. Co-authored-by: Orca <help@stably.ai> * fix(test): type the reattach host snapshot factory so status narrows to 'ready' Co-authored-by: Orca <help@stably.ai> * fix(terminal): keep an exhausted remote pane reattachable through Reconnect and resume Reconnect and the online/system-resume trigger both opened a fresh recovery epoch, which switched off the accepted-snapshot reattach path (gated on the 'disconnected' phase) for 60s while the require-replacement inventory wait dead-ended without scheduling anything. Gate the same-handle reattach on a spent auto-recovery window instead of the live phase, consumed once per window, and park an unarmed retry at the require-replacement dead end so online/resume/Reconnect have work to revive. Co-authored-by: Orca <help@stably.ai> * test(terminal): pin latched-pane retention to one listener and one registry entry The recovery cutoff now keeps the retry-registry entry and the accepted-snapshot listener alive, so cover the two module-global collections that could accumulate: destroy/detach cycles, concurrent latched panes, revive storms and snapshot churn all return to baseline. Co-authored-by: Orca <help@stably.ai> * refactor(terminal): use the returned reattach epoch and pin the no-RPC claim Addresses both CodeRabbit nitpicks: subscribeToHandle now takes the epoch begin() returned rather than re-reading currentEpoch, and the latched attach-wait test asserts the runtime call count is unchanged instead of only the advanced count. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
e59a319ffe |
fix(sidebar): keep each project's entry-point workspace visible under "Hide sleeping" (#12257)
"Hide sleeping" swept each project's main workspace out of the sidebar as soon as it had no live PTY, browser tab or agent — even with "Hide default branch" off. For a project whose only row is that workspace (a folder workspace, a fresh clone, a detached-HEAD main), the entire project vanished with no in-place way back. Adds a shared `isSleepingSweepExemptWorkspace` predicate keyed on `isMainWorktree` rather than the branch name, so folder workspaces (no branch), detached-HEAD mains, and SSH rows whose head/branch are blanked while a provider is disconnected all stay put. Wired into `computeVisibleWorktreeIds` (sidebar, Cmd+1-9, workspace board), the jump palette's duplicate inline pass, and mobile's `filterWorktrees`. Ships default-on with an escape hatch: a persisted `alwaysShowDefaultBranchWorkspace` setting surfaced as "Except default branch" under "Hide sleeping". Explicit "Hide default branch" still wins, since it filters before the sleeping sweep. Mobile reads the setting but never writes it back, so a desktop opt-out can't be clobbered by a filter tap before the ui.get roundtrip lands. Combines the two PRs open against #8873. #8966's exempt set is a strict subset of this one, so its production diff was subsumed rather than ported; its jump-palette render harness and e2e spec were carried over, and are the only such coverage here. Fixes #8873 Closes #8966 Co-authored-by: Rod Boev <rod.boev@gmail.com> Co-authored-by: Orca <help@stably.ai> |
||
|
|
0927b9c156 |
fix(gitlab): load pipeline job traces in the Checks side panel (#7732) (#12266)
* test(repro): demonstrate #7732 GitLab pipeline job details never load in Checks panel Co-authored-by: Orca <help@stably.ai> * fix(gitlab): load pipeline job traces in the Checks side panel (#7732) Expanding a GitLab pipeline job in the Checks panel always showed "No inline details are available for this check.": the mapper dropped the numeric job id, `PRCheckDetail` had nowhere to carry it, and every consumer called the GitHub check-runs API, which returns null for a GitLab job. - carry `gitlabJobId` on `PRCheckDetail` and add the `gitlab-job:` branch to all three identity ladders (panel rows, editor tabs, fix-prompt keys) so same-stage jobs with no web_url stop colliding - add a runtime-routed trace client so SSH/remote workspaces work, not just local IPC, and thread the MR's `projectRef` for fork pipelines - bound the trace in main via the existing `sliceCheckLogTail` (now shared, not GitHub-only) so a multi-megabyte CI log never crosses the 1 MB transport frame cap; strip ANSI/section markers up to the CR only, which keeps each section's visible header and command echo - render the excerpt inline instead of "Log tail available in full details." - feed GitLab traces to "Fix with AI", which previously sent bare check names - skip the fetch for jobs that cannot have a trace (created/manual/skipped) so GitLab's 404 does not replace the benign empty state, and re-arm a failed load when the job's state changes since the panel has no retry Co-authored-by: Orca <help@stably.ai> * fix(gitlab): treat a missing job log as an empty log, not an error (#7732) Round-1 review follow-up. - a job canceled before it started (or whose log was erased/expired) is `completed`/`cancelled`, so the panel fetched its trace, GitLab answered 404, and `classifyGlabError`'s issue-edit copy ("Issue not found — it may have been deleted.") landed verbatim on the auto-expanded check row; main now maps that 404 to an empty trace so the row keeps its benign empty state - keep a missing project a real error (GitLab masks unauthorized projects as 404) and add `classifyJobLogError` so 403/unknown failures stop borrowing issue-edit wording on a job-log read - broaden the empty-log copy in all five catalogs: it now covers erased and expired logs, not only jobs that never ran - e2e: derive the repro screenshot dir from `process.cwd()` (or an env override) instead of a hardcoded POSIX path to a throwaway worktree - bound the raw trace before the ANSI/section passes so a multi-megabyte log is not scanned in full on the main-process event loop - drop the redundant `if (repo)` in `handleFixChecksWithAI` and the now-dead "Log tail available in full details." catalog entry Co-authored-by: Orca <help@stably.ai> * fix(gitlab): address review — project ref on reload, retry re-arm, IPC timeout - Carry the MR's GitLab project ref on the check-details tab so reloading a fork/cross-project job tab fetches the trace from the pipeline's own project. - Re-arm the sidebar retry when a details load resolves to null, not only when it throws; a detail-less row otherwise never retried after the job moved on. - Bound the local `gl.jobTrace` IPC call with the same 30s timeout the runtime RPC path uses — glab runs without a subprocess timeout in main. - Document that the trace 404 -> empty-log mapping is deliberately broad. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
ba83a71e30 |
fix(terminal): apply the running-process close confirmation to every tab close path (#10142) (#12272)
* test(repro): demonstrate #10142 tab X close bypasses running-process confirmation Unit repro: closeTerminalTab (the X-button/middle-click entry) never consults inspectRuntimeTerminalProcess and drops a tab with a live child. E2E repro: Cmd+W shows 'Stop running command?' for a tab running sleep 300; cancelling then clicking the tab X closes it silently. Co-authored-by: Orca <help@stably.ai> * fix(terminal): confirm running-process close on every tab close path (#10142) The tab-strip X button, middle-click and the tab context menu closed a terminal with a live child process without asking, while Cmd+W raised "Stop running command?" for the same tab. The probe lived only in TerminalPane's pane-level close handler; every mouse entry point reaches closeTerminalTab(), which guarded pinned tabs and nothing else. Move the decision into closeTerminalTab, above the web-runtime branch so paired/remote host-backed tabs are covered too, and give the last-pane keyboard close back to it instead of probing twice: - running-terminal-close-guard.ts probes every live PTY of the tab and fails open on a rejected probe or a stale remote handle, matching what Cmd+W already did. No live PTY ids => fully synchronous close, so idle, parked and hibernated tabs keep today's behavior. - shouldConfirmRunningTerminalClose keeps lifecycle echoes, bulk closes, CLI/RPC closes and the post-confirmation re-entry off the modal path. - A standalone confirm store drives RunningTerminalCloseDialog, which reuses the existing CloseTerminalDialog (no new user-visible strings). The request carries the tab label because a tab-strip close can target a tab the user is not looking at, and dedupes by tab id. - TerminalPane.handleRequestClosePane now delegates the last pane to closeTerminalTab. Its transport ptyId is nullable by design, so the old path silently skipped the prompt mid-reattach; the pane keeps its own probe only for closing one pane of a split. - Agent panes win the dialog copy when a split has both an agent and a plain command busy, instead of depending on PTY spawn order. - Tab-group closeItem ran leaveWorktreeIfEmpty synchronously after a close that can now defer; it moves to onClosed and still honors skipEmptyCheck. Co-authored-by: Orca <help@stably.ai> * fix(terminal): close the running-process confirmation gaps on every path (#10142) Follow-up hardening on the tab-close confirmation, from review of the first pass: - A pinned tab with `confirmClosePinnedTab` off never got the running-process prompt on any path, including Cmd+W, which is a regression against the old pane-level behavior: the pinned branch short-circuited on pinned-ness alone and re-entered with `force`, which the running guard excludes. The pin prompt now supersedes only when it will actually appear; with the setting off the close falls through to the running guard. - The probe chain had no `.catch`, so a throw in the decision (a copy-kind lookup on a tab id makePaneKey rejects, a store subscriber) left the tab silently unclosed with no user feedback. It now fails open, as the pane path it replaced did. - A wedged remote inspect RPC could leave the X button looking dead for its full 15s timeout. The probe is now bounded; every close path shares the bound, so keyboard and mouse still behave identically. - The agent-vs-command copy had two resolvers on exactly the keyboard/mouse seam this issue is about. terminal-close-copy-kind.ts is now the single policy; TerminalPane and the tab-strip guard both call it. - The running queue is async while the pinned queue is synchronous, so both could be pending at once and stack two modal overlays. The running dialog now waits for a visible pinned confirmation. - Deduping a repeat close request dropped the second caller's callbacks; it now folds them in, so both closes resolve from one prompt. Ticking "don't ask again" also drains queued prompts instead of showing one the user just opted out of, and a queued prompt no longer inherits the previous tab's tick. closeTerminalTab drops its private pinned predicate for the shared isUnifiedTabPinned, whose only consumer the previous commit had removed. * test(e2e): wait for `sleep` to own the terminal before closing it (#10142) The running-process close specs polled `hasChildProcesses` to decide the tab was busy, but macOS starts the shell under `login`, so an initialising terminal already reports a child before `sleep 300` runs. Both specs could therefore press close against a shell that never started the command: the probe correctly saw an idle terminal and closed without asking, and the adjudicated repro failed against a correct fix. Wait for `foregroundProcess === 'sleep'` instead. Assertions are unchanged, and the repro still fails at the pre-fix baseline (a92d8e0b0d). * fix(terminal): ask instead of closing when the close probe times out (#10142) Round-1 review follow-ups. - The 4s probe bound closed the tab outright, but `inspectRuntimeTerminalProcess` gives remote runtimes a 15s RPC timeout: any probe taking 4-15s silently killed a running remote command that Cmd+W used to prompt about, and the pane path now delegates its last-pane close to this guard. An unanswered probe is unknown, not idle, so the timeout raises the confirmation with every pty treated as a candidate. Failing open still applies to an *answered* probe (rejection, stale remote handle), which is the pre-existing pane behavior. - The split-pane Cmd+W probe had no bound at all; it now shares the same one, so the two paths give the same answer to the same question. - The renamed regression spec dropped its repro scaffolding: the hardcoded /tmp screenshot directory (also a cross-platform path violation) and the title/docblock that still described the bug as open. * fix(terminal): adopt the double-activation guard and layout pty lookup from #10167 (#10142) Cross-referenced against @innocarpe's #10167, which solved the same issue. Two things it got right that this branch did not: - Queue actions now hold off for 350ms after a queued request replaces the visible one, matching the sibling pinned-tab confirmation. Without it the second click of a double-click aimed at one tab lands on the next tab's prompt and kills a running process the user never saw asked about — the exact bug class this PR exists to close. - The pty lookup unions the layout bindings with ptyIdsByTabId. A mounting pane is bound into the layout before the liveness map catches up, and the store's own teardown collector unions both for that reason, so reading only the map let a close slip through that window with no prompt. Also replaces the render-time ref write that failed React Doctor's "Ref mutated during render" rule: the queued-request checkbox reset now goes through CloseTerminalDialog's existing subject-change reset instead of remounting via key, which keeps the exit animation on one element. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
a6b14eb04c |
fix(terminal): reset stale mouse tracking on cold restore (#12101); stop OSC color-reply echo leak in POSIX agent panes (#12112) (#12202)
* fix(terminal): reset stale mouse tracking on cold restore (#12101); stop OSC color-reply echo leak in POSIX agent panes (#12112) #12101: a force-killed TUI never emits its DECRST reset, so its armed mouse mode is latched into the on-disk checkpoint and re-derived into the replacement process's emulator via the cold-restore history seed -- through both rehydrateSequences and SerializeAddon's own mode trailer. The revived bare shell then echoed SGR motion reports at the prompt. Seed a RESET_MOUSE_REPORTING segment after the snapshot (before the torn escape tail), only when there is real recovered content so the empty-array "nothing to recover" sentinel survives. #12112: agent panes arm a main-side PtyStartupIngress that answered opencode's startup OSC 10/11 queries synchronously inside node-pty's onData, while the POSIX tty still had ECHO on. The line discipline echoed Orca's own reply back out as visible text. Echo suppression existed but was gated on windows-conpty. Add PtyStartupReplyDelivery: POSIX defers the write off the query's turn and recognizes its own echo anywhere in a span (bounded, non-destructive); ConPTY keeps its synchronous write; windows-wsl is byte-identical to before. Fixes #12101 Co-authored-by: Orca <help@stably.ai> * fix(terminal): read the slave's ECHO bit before answering a color query The startup color reply was written into a PTY still in cooked mode, so the line discipline echoed it back as visible junk (#12112). Whether that will happen is readable state on the slave rather than something to infer from returning bytes, so the reply now waits until the ECHO bit is observably clear instead of guessing at echo shapes. Two echo sources exist and only one is readable. A `quiet` verdict proves the kernel will not echo, so it retires the caret projection; readline echoes a master write in software with the tty already raw, so that projection stays armed on every path. Scoping `quiet` narrowly is the whole correctness argument here: reading it as "no suppression needed" reintroduces the bug at a plain shell prompt. Polling is bounded by a wall-clock budget rather than an attempt count, because each probe is a subprocess and a multi-pane restore serializes them on fork. Withholding measures flat at ~210ms from 1 to 100 panes. Also resets a cold-restored pane's mouse reporting (#12101). The armed mode is re-derived from the dead process's own persisted bytes through two channels, so the daemon seeds a reset into recovered history and the renderer stops trusting a persisted "live agent" signal after a cold restore. The reset literals move to one shared profile module. Fixes #12101 Fixes #12112 Co-authored-by: Orca <help@stably.ai> * test(terminal): pin the cold-restore reset on the spawn-adopted reattach path A spawn can be answered with an adopted session, which reaches the reattach handler by a door that skips the restored-session path. Pin that the cold-restore signal survives it, so #12101's junk cannot come back through it. Co-authored-by: Orca <help@stably.ai> * test(terminal): note why the adopted-reattach snapshot leaves the cursor visible Co-authored-by: Orca <help@stably.ai> * fix(terminal): harden startup reply delivery --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
50594c55a9 |
Stop the Windows Orca CLI from crashing when the environment carries both PATH and Path (#12218)
* fix(windows): stop the Orca CLI dying on a duplicated PATH/Path environment The packaged Windows `orca.exe` launcher read `ProcessStartInfo.EnvironmentVariables`, whose lazy getter copies the case-sensitive process block into a case-insensitive dictionary via `.Add`. An inherited block carrying both `PATH` and `Path` threw `ArgumentException: Item has already been added. Key in dictionary: 'PATH'`, so every `orca` invocation exited 1 before Electron started (native/windows-cli-launcher/OrcaCliLauncher.cs:46, printed at :67). The launcher now mutates its own environment with `Environment.SetEnvironmentVariable` and never touches either `ProcessStartInfo` env property, so `CreateProcess` passes a NULL environment block and the child inherits the live one verbatim. Orca was also minting the duplicate itself. `applyTerminalAttributionEnv` read `baseEnv.PATH` and unconditionally wrote `baseEnv.PATH`, so a Windows PTY that inherited `Path` got a second spelling; which one the child resolved was non-deterministic. `createLaunchEnv` did the same and, because its read always missed on Windows, shipped Agent Teams terminals a `PATH` containing only the tmux shim dir. `resolvePathEnvKey` (extracted from the existing precedent in windows-environment-path.ts) now drives every PATH read and write in the PTY env pipeline, and attribution collapses Windows onto the single OS-resolved spelling. Off Windows the resolver always returns `PATH`, so POSIX behavior is unchanged and a case-sensitive POSIX `Path` variable is never touched. Closes #12046 * test(windows): track the launcher's own-environment marker The #12046 fix moved ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER and ORCA_CLI_COMMAND off ProcessStartInfo.EnvironmentVariables, but this asset test still pinned the old dictionary writes and failed. Co-authored-by: Orca <help@stably.ai> * fix(windows): follow the host block's PATH spelling on sparse daemon env patches Resolving a path-less Windows env to `Path` handed the daemon's own `{...process.env, ...opts.env}` merge both spellings when the host block spelt `PATH`. Fall back to the host block's own key, and collapse again inside the daemon since that merge happens after attribution. Co-authored-by: Orca <help@stably.ai> * fix(windows): resolve the live PATH spelling by block order, not casing Win32 resolves a duplicated variable by taking the first case-insensitive match in the block, so `resolvePathEnvKey`'s hardcoded `Path`-first preference targeted the shadowed spelling on the reporter's own `["PATH","Path"]` block. Drop the attribution-side collapse with it: it deleted the other spelling's value, and deleting the live key promotes the shadowed one, so an env that stripped down to empty lost both. * chore: drop unrelated merge formatting --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
8ab7d8a110 |
fix(updater): base dev builds on published tags, not main's package.json (#12376)
main's version only moves on `release:` commits, and stable patches are cut from release branches that never merge back. On 2026-08-03 main read 1.4.165-rc.0 for twenty hours while 1.4.165, 1.4.166 and 1.4.167 all shipped, so every hourly built in that window was stamped 1.4.165-hourly.* while carrying code newer than 1.4.167 — and sorted below the stable its user was already running. Resolve the base from the main repo's published tags instead, taking the patch above the highest shipped stable. package.json stays a floor for the case where main leads the tags. Co-authored-by: Orca <help@stably.ai> |
||
|
|
c9c1683838 |
fix(ci): seed the adhoc release repo so releases can be tagged (#12375)
gh repo create leaves the repo empty, and publishing a release creates a git tag that needs a commit to point at. The adhoc build got all the way through notarization and upload before failing on "Repository is empty". Co-authored-by: Orca <help@stably.ai> |
||
|
|
caf6add53a | test(terminal): cover leading Korean vowels (#12282) | ||
|
|
035d8c2a54 | fix(terminal): preserve macOS Korean composition (#12280) | ||
|
|
339045b150 |
fix(runtime): coalesce concurrent host terminal focus (#11841)
Bound exclusive host navigation to a generation-aware latest-wins single-flight so bulk open and switch fan-out stay responsive on large remote fleets. Add freeze repro harnesses and navigated settlement. |
||
|
|
ca9562ceee |
fix(terminal): block IME-owned Process shortcuts
Adapted from #11616 commit 9f449d75b and #12120 commit 290f42a36. Co-authored-by: holdn2 <club.makersfarm@gmail.com> |
||
|
|
1dbf55e4df |
Stop reporting supported Linux hosts as an unsupported remote platform (#12209)
Co-authored-by: Orca <help@stably.ai> |
||
|
|
18dbcf001f | fix(tabs): restore Cmd/Ctrl+Shift+T tab positions (#12236) | ||
|
|
040734b18e | fix(sidebar): show collapse chevron on pinned section headers (#12147) | ||
|
|
0ae9174408 |
refactor(github): extract shared GitHubItemDialog/PullRequestPage code (#12092)
Both hosts carried token-identical copies of the GitHub work-item mutation wrappers, PR diff mapping, presentation formatters, and four components. Move them into src/renderer/src/components/github/ so there is one source. Behavior-neutral: getStateTone, WorkItemStateBadge, PRReviewersPanel, PRActionsPanel, CommentReplyForm, and the per-host work-item/PR-file caches stay in place because they genuinely differ between the two hosts. CommentCodeContext takes loadPRFileContents as a prop so each host keeps its own private file-contents cache. Also folds github-issue-comment-helpers.ts into github-user-avatar.tsx and retargets the textual boundary tests at the new modules. |
||
|
|
07bd574294 |
refactor(usage): share the Codex/OpenCode scan fold behind a provider contract (#12082)
* refactor(usage): share the session/daily fold between Codex and OpenCode The Codex and OpenCode scanners each carried their own byte-identical copy of the ~325-line aggregation pipeline (createEmptySession, the three breakdown folds, finalizeSessions, mergeSessions, mergeDailyAggregates). Two copies means a token-accounting fix — a bucket that double-counts, a merge that drops a breakdown row — lands in one provider and silently not the other. The copies had already started to drift in comments only; the next drift would have been in arithmetic. The providers differ in exactly one dimension: the extra metric folded alongside the token counters (Codex `hasInferredPricing`, OpenCode `estimatedCostUsd`). That is now injected as an empty/fromEvent/fold triple, so the shared code stays generic without collapsing the two record schemas into a nullable union. The clone strategy stays per-provider (`cloneSessionForMerge` vs `structuredClone`) rather than being unified on the assumption that the difference is accidental. `usage-provider-contract.ts` is the seam a plugin-contributed usage source will implement. It is deliberately generic over each provider's record types: Claude bills per turn while Codex/OpenCode bill per event, and `cachedInput` is a subset of `input` for the latter but a peer bucket for Claude, so a single normalized record would push nullable handling onto every consumer. No behavior change. Emitted objects are byte-identical, including key insertion order — verified by diffing JSON.stringify of the scan output before and after across mixed models, mixed locations, an inferred-pricing flip, and null vs non-null cost. Persisted field names and schemaVersion are untouched, so caches do not invalidate. * refactor(usage): make the provider contract load-bearing and dedupe worktree refs Follow-up to the aggregation extraction, addressing three review points. `UsageProvider`/`UsageScanResult` were declaration-only, which is the same speculative-interface problem #12077 just deleted 8,900 lines of. They are now implemented by both real providers via `satisfies`, so the seam is typechecked against actual scan functions rather than asserted. The blocker was that codex returns `processedFiles` and opencode returns `processedDatabases`; rather than rename persisted-adjacent fields, the source key is a type parameter, so each provider keeps its own on-disk name and the contract still binds. Verified the constraint bites: swapping the key to 'processedSources' fails typecheck. `schemaVersion` is part of provider identity in the contract, so each provider's SCHEMA_VERSION constant (with its cache-invalidation rationale) moves into the provider module and the store imports it. Values are unchanged (codex 5, opencode 2) and the stores compare them exactly as before, so no cache invalidates. This also keeps store -> provider -> scanner acyclic. `UsageWorktreeRef` collided with the existing export in usage-worktree-metadata (3 fields, no repoId). Two different exported types under one name in src/main is worse than the duplication being removed, so the scan-input type is now `UsageScanWorktreeRef`; usage-worktree-metadata is untouched. `createWorktreeRefs` was triplicated. Codex, OpenCode, and Claude copies are byte-identical apart from the return type name (verified by diff), and all three ref types have the same four fields, so one shared copy replaces all three. This is the only change to claude-usage/. No behavior change: same functions, same arguments, same call order. The store tests' `./scanner` mock still intercepts scanning because the provider captures the mocked binding; their now-inert `createWorktreeRefs` mock key is dropped so it does not read as still mocking something. |
||
|
|
60e6a192cb |
refactor(worktrees): reuse the shared push-target helpers on the SSH path (#12091)
findRemoteForUrlSsh, ensureUniqueRemoteNameSsh and
configureCreatedWorktreePushTargetSsh were byte-identical to
findRemoteForUrl, ensureUniqueRemoteName and
configureCreatedWorktreePushTargetWithExec in worktree-push-target-setup.ts,
differing only in calling provider.exec instead of the injected execGit.
|
||
|
|
484273844a |
feat(updater): add an adhoc release channel for branch builds (#12051)
* feat(updater): add an adhoc release channel for branch builds Hourly covers main. This covers everything that is not main yet: a dispatchable macOS build of an unlanded branch, published to stablyai/orca-adhoc, so the team can run an experimental feature for a few days instead of reasoning about it from a diff. Adhoc sits at the bottom of the version order — 'adhoc' < 'hourly' < 'rc' < stable — so no routine check can walk anyone onto somebody's branch; only an explicit pinned jump reaches one. It gets its own repo rather than sharing orca-hourly's, because a branch build must not appear in the list a developer riding main is looking at. Signed and notarized exactly like hourly, for the same reason: macOS anchors a notarized app's TCC grants on identifier + team, so an unnotarized build reads as a new client and silently loses file access under Documents/Desktop/Downloads. Tags stamp to the second rather than the minute. Hourly runs under a concurrency group and cannot overlap itself; adhoc builds are dispatched on demand, so two people cutting from different branches inside one minute is ordinary — and a minute-resolution tag would collide and fail the second build after its whole pack-and-notarize run. Channel-specific behaviour now derives from one DEDICATED_REPO_CHANNELS list: repo mapping, macOS-only support, and UpdateSource. The RPC schema that validates releaseChannelOverride was a hand-copied enum missing the new channel, which would have rejected the override on its way to the main process; it reads the predicate now. * fix(updater): merge the duplicated shared/types import Co-authored-by: Orca <help@stably.ai> * fix(ci): default the adhoc build ref to the dispatch branch The Actions UI puts its own "Use workflow from" branch picker directly above the ref field, and picking a branch there is what most people read as "build this". Making the field optional means the obvious action is also the correct one; naming a branch explicitly still wins, so main's copy of the workflow runs rather than a stale one on an old branch. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
5e9186f5ff |
chore(mobile): import the shared marine-creature corpus instead of mirroring it (#12090)
mobile/src/constants/marine-creatures.ts was a hand-maintained copy of src/shared/marine-creatures.ts, identical except for a comment header. The copy existed because Metro only watched mobile/ and could not resolve repo-root modules; mobile/metro.config.js:11 added src/shared to watchFolders five weeks later, and ~195 mobile files already import from src/shared. The renderer collapsed its copy to a re-export at the same time; mobile was the leftover. Point the one consumer at the shared corpus and delete the mirror, the bespoke regex-scraping parity test that policed it, and the now-stale max-lines baseline entry. No behavior change: same exported symbol, byte-identical name list. |
||
|
|
3f8654c26e |
fix(editor): make lazy-chunk recovery actually reload instead of being silently vetoed (#11929)
* fix(editor): stop filing crash reports for expected lazy-chunk swaps RichMarkdownErrorBoundary reported every caught error as a react-error-boundary crash, including the LazyChunkLoadError sentinel that lazy-with-retry throws after it has already exhausted its retries and its one guarded reload. That sentinel means "the chunk hash changed under a running window" (an app update), which is deliberate graceful degradation, not a crash. RecoverableRenderErrorBoundary already skips reporting it (#6206); this boundary was never updated. Crash b860def2 is exactly that path: a lazy_chunk_reload breadcrumb ("Unexpected token ':'") fires first, then the post-reload attempt surfaces LazyChunkLoadError and files a report. The fallback UI is unchanged, so the pane stays usable and offers retry. * fix(editor): prove the lazy-chunk reload landed before suppressing crash reports - lazy-with-retry: reload guard stores the requesting document's identity, so a vetoed reload() no longer reads as "recovery ran" (crash b860def2) - lazy-with-retry: bound the post-reload suspension so a vetoed navigation surfaces the real error instead of hanging the pane on a spinner - RichMarkdownErrorBoundary: contain the LazyChunkLoadError sentinel without a crash report, but record a lazy_chunk_boundary_degraded breadcrumb - EditorContent: name the rich markdown chunk at the lazy call site Co-authored-by: Orca <help@stably.ai> * fix(editor): route lazy-chunk recovery reload through the intentional-restart path Crash b860def2's recovery reload was requested and never landed: Terminal's beforeunload handler preventDefault()s while any editor tab is dirty and Electron cancels the navigation with no dialog, so chunk recovery could never run in the common case. Take the updater's path instead — hot-exit backup, one synchronous session checkpoint, restart latch — then reload. - Reject on ORCA_RENDERER_UNLOAD_PREVENTED_EVENT instead of a blind, never-cleared 10s timer; keep the timer only as a backstop. - Record a lazy_chunk_reload_vetoed breadcrumb in the same tick as the report it now files, so the 30-entry ring cannot evict the evidence. - Drop this document's own stale guard after a refused reload (capped in memory) so saving the blocking tab does not forfeit recovery for the session. - Carry reloadKey on LazyChunkLoadError and the degraded breadcrumb. - Move renderer-restart-preparation to src/shared: it is now a renderer/preload contract, and the composite web project cannot import preload runtime code. Co-authored-by: Orca <help@stably.ai> * fix(editor): clean up failed lazy chunk reload requests * test(preload): exercise restart IPC registrations --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
1e545f9c30 |
fix(speech): coalesce model download progress churn on the Settings/Voice path (#11962)
* fix(speech): coalesce model download progress churn A model download emits one state update per HTTP chunk (thousands for a 500MB model). Each one costs the renderer an IPC round-trip and a forced re-render of the speech-model menu, which stays open by design while a download runs — the Radix portal/Presence tree all four page.settings React #185 reports crashed inside. Emit at whole-percent granularity (what the UI renders) and keep the modelStates array identity stable when a refresh changed nothing, so a no-op refresh no longer forces a commit. Adds a speech_model_state_churn breadcrumb, registered in both coalescing sets, because no bundle in the cluster carried any speech telemetry to confirm a download was in flight. * fix(speech): quantise polled model state so download progress stops churning the renderer Adversarial round 1 found the renderer-side stabilisation was inert during a real download. The progress fan-out already coalesces to whole percent, but the renderer discards the event payload and re-polls getModelStates, and getModelState returned the cached downloading state verbatim - raw sub-percent progress. So every chunk produced a fresh object, resolveModelStates never matched, and all the real benefit came from the main-side coalescing alone. Quantise the polled reply to the precision the UI already renders (Math.round(progress * 100)), keeping the stored cache exact. Also from round 1: - the whole-status dedup swallowed downloadModel's already-downloaded branch, whose lone 'ready' is the only notification the requesting window ever gets - a dead click and a permanently stale pane with two windows open. Gate it on downloading -> downloading so every one-shot transition stays unconditional. - rename the storm test off '.react185.': it counts renders and asserts nothing about #185, and #185 could not be reproduced at IPC-realistic pacing. * fix(speech): stop the churn breadcrumb firing on every healthy download Adversarial round 2. CHURN_REFRESH_THRESHOLD was 60 per 5s window, but main clamps download progress at 0.9 and emits on whole-percent change, so one healthy download is capped at ~91 refreshes — and a 56MB model on a fast link lands all of them inside a single window. The breadcrumb fired on every normal download and burned a slot in the 30-entry ring it was coalesced into to protect. Raise it to 250 so it only fires on the per-chunk shape it was added to detect; noOpRefreshes in the payload still separates the two causes. Round 2 also found that every assertion round 1 added was one-sided (toBeLessThanOrEqual), so each of the three core behaviours could be mutated into "do nothing" with the whole suite still green: - suppress every downloading -> downloading event: progress bar frozen at 0% - toWholePercentState returning 0: every poll reports 0% - resolveModelStates never adopting a same-length change: the Voice pane never updates and a finished model never shows as ready The suite measured that the fix reduces work, never that it still does the work. Convert the two ceilings to exact series, assert the storm test's render floor as well as its ceiling, and add dictation-model-state-stabilisation.test.ts covering adoption per changed field, a full whole-percent download, and both sides of the churn threshold. Ruled out and deliberately not fixed: round 1's request-sequencing MEDIUM on refreshModelStates. 50 concurrent getModelStates() settle strictly FIFO over ipcRenderer.invoke at constant microtask depth, and migrationReady is assigned once in the constructor, so a monotonic request id would be dead code. * test(crash-reporting): cover speech churn breadcrumb name-coalescing The churn breadcrumb's registration in COALESCED_RENDERER_BREADCRUMB_NAMES and NAME_ONLY_COALESCED_BREADCRUMB_NAMES had no test: the storm test only asserted the constant equals its own literal. Removing either registration kept the whole suite green. It carries no message field, so without the name-only entry rendererBreadcrumbCoalesceKey returns undefined and every firing takes its own ring slot — the eviction this breadcrumb exists to avoid. * test(dictation): pin the churn threshold as a rate, not a lifetime total Both existing churn tests stopped at exactly 250 refreshes in one window, so two mutations survived: deleting the per-window reset (the counter degrades into a session total, and three healthy downloads at 91 refreshes each cry wolf), and === to >= (fires on every refresh past the threshold, flooding the ring). Pins Date.now rather than using fake timers so the window rule is measured, not the machine. * test(dictation): pin the churn clock and the window's lower bound The two 250-refresh churn tests measured real elapsed time against a 5s window, so a loaded runner that took longer than one window to run the loop would roll the window and go red. Pin Date.now in both. Pinning alone leaves CHURN_WINDOW_MS unpinned below: shrinking it 5_000 -> 50 kept all 13 tests green, yet a 50ms window can never accumulate 250 refreshes and the detector would be dead. Add a storm spread across most of one window so the constant has to be wide enough to hold a sustained storm, not just an instantaneous burst. Co-authored-by: Orca <help@stably.ai> * refactor(speech): narrow progress churn fix --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
006ce9d116 |
fix(dev): split the confirmation dialog so Fast Refresh can accept it (#11980)
* fix(dev): split the confirmation dialog so Fast Refresh can accept it `confirmation-dialog.tsx` exported both `ConfirmationDialogProvider` and `useConfirmationDialog`, so React Fast Refresh could never treat it as a boundary and Vite applied every edit to it in two passes under two `?t=` stamps. When a second file in the same subtree changed in one watcher batch, `createContext` ran twice and the provider published one context object while the consumer read the other — `useContext` returned null and the hook threw. Two field crash reports hit this at `ChecksPanel`, both dev-server sessions. The context and hook move to a new component-free `confirmation-dialog-context.ts`; `confirmation-dialog.tsx` keeps the provider and now exports only a component, so the refresh runtime accepts it. Not one line of the provider body changes — the 16 hook importers just point at the new module, `vi.mock` targets included, and `App.tsx` is untouched. * test(dev): pin the confirmation dialog Fast Refresh boundary The split that fixed the context-identity crash had no test behind it: no test imported ConfirmationDialogProvider, and the six vi.mock call sites replace the hook module wholesale, so they pass just as well with the provider and hook back in one file. Assert the module shapes the refresh transform actually keys on -- the context module registers no component, so it never gets an HMR footer to invalidate through. Co-authored-by: Orca <help@stably.ai> * test(dev): assert the refresh boundary on the module namespace The source-regex guard did not guard. Its patterns match only declaration forms, so `export { useConfirmationDialog } from './confirmation-dialog-context'` in the provider module -- which restores the crash, verified in a browser -- passed it 3/3. It also failed on a comment that merely contained the word createContext, and would fail on React 19's `<Ctx value={...}>` shorthand. Assert on the module namespace object instead, using react-refresh's own component criterion, so re-exports and default exports are visible. The third test renders the provider and resolves the hook through it, which is a real behavioural check rather than a shape one. Co-authored-by: Orca <help@stably.ai> * test(dev): classify boundary exports with the refresh runtime's own predicate The hand-rolled `^[A-Z]` name check called `export class Foo {}` a component; the runtime rejects any class whose prototype carries extra members, so that shape restored the two-pass split undetected. Use react-refresh's exported `isLikelyComponentType` and mirror `isCompoundComponent` instead of a third approximation. react-refresh was already resolvable only via shamefully-hoist, so it is now an explicit devDependency. * test(dev): tighten confirmation dialog boundary guard --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
673d7ca926 |
refactor(relay): collapse the duplicated FrameDecoder into one shared module (#12078)
src/relay/relay-frame-decoder.ts and src/main/ssh/relay-frame-decoder.ts were 264 identical lines apart from one default: the relay logs decode faults to stderr when no handler is supplied, the SSH side stays silent. Two copies of framing logic is exactly where a wire-format fix lands in one and not the other. The decoder's contract and buffer already live in src/shared, so the class joins them there. The relay keeps a thin subclass that supplies its stderr default, preserving behaviour for the call sites that omit onError. The SSH copy is deleted and relay-protocol.ts points at shared directly. Verified: pnpm typecheck, 102 tests across the 9 framing/backpressure/ handshake suites, and `pnpm build:relay` for all six platform targets plus the WSL hook relay — the standalone bundle has no new dependencies. |