fix(terminal): keep final-cell IME anchor on-screen

This commit is contained in:
Neil
2026-08-29 15:42:54 -07:00
parent 751dbebcf1
commit 910b845c7d
8 changed files with 176 additions and 69 deletions
File diff suppressed because one or more lines are too long
@@ -88,7 +88,7 @@ index 497afcf535f3eaca00889525a77e15eb633ccd96..96d499b34605f860608382114c3fbdc0
export interface IBrowser {
diff --git a/src/browser/input/CompositionHelper.ts b/src/browser/input/CompositionHelper.ts
index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..06c35cac2a9fd20f2dffcf2fed64797dbb613252 100644
index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..b91682d6c17d81e9ad527b22dfcbc05393bc52a5 100644
--- a/src/browser/input/CompositionHelper.ts
+++ b/src/browser/input/CompositionHelper.ts
@@ -3,8 +3,10 @@
@@ -1072,7 +1072,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..06c35cac2a9fd20f2dffcf2fed64797d
if (this._bufferService.buffer.isCursorInViewport) {
const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1);
@@ -265,20 +978,37 @@ export class CompositionHelper {
@@ -265,20 +978,38 @@ export class CompositionHelper {
const maxWidth = this._bufferService.cols * this._renderService.dimensions.css.cell.width - cursorLeft;
this._compositionView.style.maxWidth = maxWidth + 'px';
this._compositionView.style.overflow = 'hidden';
@@ -1080,8 +1080,10 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..06c35cac2a9fd20f2dffcf2fed64797d
- // Sync the textarea to the exact position of the composition view so the IME knows where the
- // text is.
- const compositionViewBounds = this._compositionView.getBoundingClientRect();
- this._textarea.style.left = cursorLeft + 'px';
+ const anchorBounds =
+ (this._compositionPreedit ?? this._compositionView).getBoundingClientRect();
+ const anchorLeft = cursorLeft + Math.min(0, maxWidth - anchorBounds.width);
+ const showsRemainder =
+ Boolean(this._compositionRemainder) && anchorBounds.width < maxWidth;
+ if (this._compositionRemainder) {
@@ -1100,7 +1102,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..06c35cac2a9fd20f2dffcf2fed64797d
+ '#FFF';
+ // Sync the textarea to the exact position of the preedit so the IME knows where the text is,
+ // and so candidate dialogs anchor to it rather than to the end of the rendered tail.
this._textarea.style.left = cursorLeft + 'px';
+ this._textarea.style.left = anchorLeft + 'px';
this._textarea.style.top = cursorTop + 'px';
// Ensure the text area is at least 1x1, otherwise certain IMEs may break
- this._textarea.style.width = Math.max(compositionViewBounds.width, 1) + 'px';
@@ -1119,14 +1121,13 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..06c35cac2a9fd20f2dffcf2fed64797d
}
}
diff --git a/src/common/SortedList.ts b/src/common/SortedList.ts
index 8a10076e3963e33b4a7d1e4602333eb3f4772dc9..da6e7691371909f31748b1cb2cb8a261fbd2b279 100644
index 8a10076e3963e33b4a7d1e4602333eb3f4772dc9..df0761c35907ddc48eb102ba181b0dac8e61f00d 100644
--- a/src/common/SortedList.ts
+++ b/src/common/SortedList.ts
@@ -87,7 +87,25 @@ export class SortedList<T> {
@@ -87,6 +87,24 @@ export class SortedList<T> {
if (key === undefined) {
return false;
}
- i = this._search(key);
+ if (this._deleteAtKey(value, key)) {
+ return true;
+ }
@@ -1145,7 +1146,6 @@ index 8a10076e3963e33b4a7d1e4602333eb3f4772dc9..da6e7691371909f31748b1cb2cb8a261
+ }
+
+ private _deleteAtKey(value: T, key: number): boolean {
+ let i = this._search(key);
i = this._search(key);
if (i === -1) {
return false;
}
+55 -6
View File
@@ -11095,21 +11095,26 @@
"providers": ["local", "daemon", "ssh", "remote-runtime"],
"coveredPlatforms": ["macos", "linux"],
"coveredProviders": [],
"coverageNotes": "Local macOS and containerized Linux evidence, deterministic renderer-unit coverage for the Linux/Sogou candidate-key policy including the legacy orphaned-keyup fallback, Electron/CDP live-PTY Sogou-style repros, and macOS renderer/main slices for Preview kitty commits and layout-native Option input. The Option slice covers complete CSI-u flag fields including lock state, repeat/release event types, authoritative committed associated text, native dead-key release suppression, functional-versus-printable numpad codes, press-pinned release identity, live keyup modifiers, pending-layout text fallback, atomic layout refresh, rapid in-focus input-source changes, stable native layout characters, and both native/web preload fallbacks. Snapshot flag provenance keeps a proven 0, a proven non-zero, and an absent field distinct from daemon through the sequenced remote SnapshotStart frame. Real Linux/Sogou OS IME automation, Windows ConPTY post-agent reset, and the CJK/Vietnamese/Arabic matrix remain registered gaps.",
"coverageNotes": "Local macOS and containerized Linux evidence, deterministic renderer-unit coverage for the Linux/Sogou candidate-key policy including the legacy orphaned-keyup fallback, xterm preedit visibility and row-occlusion behavior, Electron/CDP preedit geometry, Electron/CDP live-PTY Sogou-style repros, and macOS renderer/main slices for Preview kitty commits and layout-native Option input. The Option slice covers complete CSI-u flag fields including lock state, repeat/release event types, authoritative committed associated text, native dead-key release suppression, functional-versus-printable numpad codes, press-pinned release identity, live keyup modifiers, pending-layout text fallback, atomic layout refresh, rapid in-focus input-source changes, stable native layout characters, and both native/web preload fallbacks. Snapshot flag provenance keeps a proven 0, a proven non-zero, and an absent field distinct from daemon through the sequenced remote SnapshotStart frame. Real Linux/Sogou OS IME automation, native Windows candidate-window placement, Windows ConPTY post-agent reset, and the CJK/Vietnamese/Arabic matrix remain registered gaps.",
"motivatingLinks": [
"https://github.com/stablyai/orca/pull/6699",
"https://github.com/stablyai/orca/pull/6682",
"https://github.com/stablyai/orca/pull/6513",
"https://github.com/stablyai/orca/pull/6999"
"https://github.com/stablyai/orca/pull/6999",
"https://github.com/stablyai/orca/issues/12545",
"https://github.com/stablyai/orca/issues/12729",
"https://github.com/stablyai/orca/pull/17170"
],
"invariant": "Composition, native text forwarding, synthetic input, paste, and platform keyboard bypass paths must not send preedit/control bytes before commit and must commit text exactly once to the intended PTY. Layout-native Option text must preserve negotiated CSI-u alternate/associated fields and lock modifiers, distinguish repeat from press, and resolve releases with the press's primary identity plus the actual keyup modifiers without exposing a partial or stale layout refresh. An in-focus macOS input-source change must invalidate both renderer authorities before the next key can use the old source. When the snapshot source proves kitty state, macOS Preview commits must use the application's effective flags after the sequenced snapshot and every proven replay/live transition, and a claimed press delivered under report_event_types must produce exactly one matching release regardless of whether keyup arrives before or after insertText and regardless of xterm's defensive internal reset.",
"oracle": "Exact outbound bytes from the REAL native-text forwarder behind installPreviewImeBridge after initial snapshot restore, replacement resync, live transitions, and both keydown->insertText->keyup and keydown->keyup->insertText orderings; a wiring-only mock is supporting evidence, not the oracle. The current renderer-unit slice asserts native text commits route to the intended PTY once, composition/preedit bookkeeping does not leak premature text, input-source classification handles synthetic/native paths, paste/runtime forwarding avoids duplicate terminal payloads for covered fixtures, and Linux/Sogou candidate Space/digit selectors do not leak keydown/keypress/keyup while ordinary and long-held letter-to-digit typing remains available. A main/preload/renderer notification chain proves every live window invalidates immediately, the old native read settles before a coalesced refresh signal, and older refreshes are generation-fenced. The Electron/CDP live-PTY repro verifies Sogou-style Space and digit selectors submit only the committed Chinese text, while the legacy orphaned-letter-keyup sequence sends no selector byte to the PTY. Real legacy IME commit preservation and the full CJK/Vietnamese/Arabic/JIS-yen matrix run in follow-up platform soak where automation is possible.",
"invariant": "Composition, native text forwarding, synthetic input, paste, and platform keyboard bypass paths must not send preedit/control bytes before commit and must commit text exactly once to the intended PTY. A live preedit must stay visible across an IME-resumed transaction, preserve every committed non-dim cell hidden by its opaque overlay, keep the themed insertion caret and native candidate anchor inside the terminal at the final column, and clean up without stale overlay children. Layout-native Option text must preserve negotiated CSI-u alternate/associated fields and lock modifiers, distinguish repeat from press, and resolve releases with the press's primary identity plus the actual keyup modifiers without exposing a partial or stale layout refresh. An in-focus macOS input-source change must invalidate both renderer authorities before the next key can use the old source. When the snapshot source proves kitty state, macOS Preview commits must use the application's effective flags after the sequenced snapshot and every proven replay/live transition, and a claimed press delivered under report_event_types must produce exactly one matching release regardless of whether keyup arrives before or after insertText and regardless of xterm's defensive internal reset.",
"oracle": "Exact outbound bytes from the REAL native-text forwarder behind installPreviewImeBridge after initial snapshot restore, replacement resync, live transitions, and both keydown->insertText->keyup and keydown->keyup->insertText orderings; a wiring-only mock is supporting evidence, not the oracle. The current renderer-unit slice asserts native text commits route to the intended PTY once, composition/preedit bookkeeping does not leak premature text, resumed Windows event traces reactivate the visible preedit, an opaque mid-line overlay reproduces the committed row tail it covers, and final-column layout keeps both caret and textarea candidate anchor within the available screen width. Rendered Electron/CDP geometry independently proves the overlay reproduces every committed cell it hides and the final-column caret stays on-screen. Input-source classification handles synthetic/native paths, paste/runtime forwarding avoids duplicate terminal payloads for covered fixtures, and Linux/Sogou candidate Space/digit selectors do not leak keydown/keypress/keyup while ordinary and long-held letter-to-digit typing remains available. A main/preload/renderer notification chain proves every live window invalidates immediately, the old native read settles before a coalesced refresh signal, and older refreshes are generation-fenced. The Electron/CDP live-PTY repro verifies Sogou-style Space and digit selectors submit only the committed Chinese text, while the legacy orphaned-letter-keyup sequence sends no selector byte to the PTY. Real legacy IME commit preservation, native candidate-window placement, and the full CJK/Vietnamese/Arabic/JIS-yen matrix run in follow-up platform soak where automation is possible.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-ime-native-text-forwarder.test.ts src/renderer/src/components/terminal-pane/terminal-ime-substituted-text-commit.test.ts src/renderer/src/components/terminal-pane/terminal-ime-macos-keybinding-dict-trace.test.ts src/renderer/src/components/terminal-pane/terminal-paste-runtime.test.ts src/renderer/src/components/terminal-pane/terminal-ime-composition-tracker.test.ts src/renderer/src/components/terminal-pane/terminal-ime-candidate-key-release-guard.test.ts src/renderer/src/components/terminal-pane/xterm-bypass-policy-non-mac.test.ts src/renderer/src/components/terminal-pane/xterm-bypass-policy.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-ime-linux-candidate-state.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-ime-xterm-midline-preedit-tail.test.ts src/renderer/src/components/terminal-pane/terminal-ime-xterm-resumed-preedit-visibility.test.ts src/renderer/src/components/terminal-pane/terminal-ime-xterm-trailing-preedit-occlusion.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge-kitty-bytes.test.ts src/renderer/src/components/dashboard-popout/preview-terminal-snapshot-replay.test.ts src/renderer/src/components/dashboard-popout/AgentTerminalPreview.test.tsx src/renderer/src/components/dashboard-popout/AgentTerminalPreview.option-dead-key.test.tsx src/shared/terminal-kitty-keyboard-mode-tracker.test.ts src/main/ipc/terminal-preview.test.ts src/main/ipc/terminal-preview-output-stream.test.ts src/renderer/src/runtime/remote-runtime-terminal-snapshot-kitty-flags.test.ts src/main/daemon/daemon-pty-adapter.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-kitty-csi-u-encoding.test.ts src/renderer/src/components/terminal-pane/terminal-shortcut-option-compose.test.ts src/renderer/src/components/terminal-pane/terminal-option-kitty-release.test.ts src/renderer/src/lib/keyboard-layout/layout-base-character.test.ts src/renderer/src/lib/keyboard-layout/option-as-alt-probe.test.ts src/main/ipc/macos-keyboard-layout-snapshot.test.ts src/main/ipc/macos-keyboard-layout-change-notifications.test.ts src/preload/app-restart-checkpoint-routing.test.ts src/renderer/src/web/web-preload-api-ui.test.ts",
"pnpm run test:e2e -- tests/e2e/chinese-ime-chat-input-repro.spec.ts",
"pnpm run test:e2e -- tests/e2e/terminal-korean-endofrow-preedit-cell-span.spec.ts tests/e2e/terminal-korean-midline-preedit-occlusion.spec.ts",
"pnpm run test:e2e -- tests/e2e/terminal-option-composed-ascii.spec.ts"
],
"testFiles": [
@@ -11121,6 +11126,9 @@
"src/renderer/src/components/terminal-pane/terminal-ime-composition-tracker.test.ts",
"src/renderer/src/components/terminal-pane/terminal-ime-candidate-key-release-guard.test.ts",
"src/renderer/src/components/terminal-pane/terminal-ime-linux-candidate-state.test.ts",
"src/renderer/src/components/terminal-pane/terminal-ime-xterm-midline-preedit-tail.test.ts",
"src/renderer/src/components/terminal-pane/terminal-ime-xterm-resumed-preedit-visibility.test.ts",
"src/renderer/src/components/terminal-pane/terminal-ime-xterm-trailing-preedit-occlusion.test.ts",
"src/renderer/src/components/terminal-pane/xterm-bypass-policy-non-mac.test.ts",
"src/renderer/src/components/terminal-pane/xterm-bypass-policy.test.ts",
"src/renderer/src/components/terminal-pane/terminal-kitty-csi-u-encoding.test.ts",
@@ -11141,7 +11149,9 @@
"src/main/ipc/terminal-preview-output-stream.test.ts",
"src/renderer/src/runtime/remote-runtime-terminal-snapshot-kitty-flags.test.ts",
"src/main/daemon/daemon-pty-adapter.test.ts",
"tests/e2e/chinese-ime-chat-input-repro.spec.ts"
"tests/e2e/chinese-ime-chat-input-repro.spec.ts",
"tests/e2e/terminal-korean-endofrow-preedit-cell-span.spec.ts",
"tests/e2e/terminal-korean-midline-preedit-occlusion.spec.ts"
],
"assertionRefs": [
{
@@ -11194,6 +11204,30 @@
"physical letter tracking survives cross-pane focus handoff and clears on renderer-window blur"
]
},
{
"file": "src/renderer/src/components/terminal-pane/terminal-ime-xterm-midline-preedit-tail.test.ts",
"assertions": [
"a mid-line preedit reproduces committed cells hidden by its opaque overlay and refreshes that tail after terminal renders",
"fully dimmed text stays masked instead of being repeated after the preedit",
"the final-column caret stays visible and the textarea candidate anchor shifts left with an over-wide preedit",
"cancel, empty resumed updates, and disposal remove transient overlay state"
]
},
{
"file": "src/renderer/src/components/terminal-pane/terminal-ime-xterm-resumed-preedit-visibility.test.ts",
"assertions": [
"recorded Windows Korean compositionupdate-without-compositionstart traces reactivate a visible preedit transaction",
"the resumed transaction commits exactly once and cleans up normally",
"the ordinary Linux compositionstart sequence remains unchanged"
]
},
{
"file": "src/renderer/src/components/terminal-pane/terminal-ime-xterm-trailing-preedit-occlusion.test.ts",
"assertions": [
"rolling Korean input keeps only the uncommitted trailing syllable in the themed overlay",
"the overlay remains anchored to the live cursor cell and clears after commit"
]
},
{
"file": "src/renderer/src/components/terminal-pane/xterm-bypass-policy-non-mac.test.ts",
"assertions": [
@@ -11363,6 +11397,20 @@
"Post-composition Sogou-style digit selection stays out of the PTY after compositionend",
"Legacy orphaned-letter-keyup digit selection submits no selector byte to the PTY; real legacy candidate commit preservation remains a platform gap"
]
},
{
"file": "tests/e2e/terminal-korean-endofrow-preedit-cell-span.spec.ts",
"assertions": [
"the rendered final-column preedit caret remains inside the terminal screen",
"a rendered full-width Korean preedit spans materially more than the one terminal cell reported in #12729"
]
},
{
"file": "tests/e2e/terminal-korean-midline-preedit-occlusion.spec.ts",
"assertions": [
"the rendered opaque overlay reproduces every non-dim committed cell its measured rectangle hides",
"a fully dimmed row stays masked rather than repeated and an end-of-row composition adds no tail"
]
}
],
"evidenceRuns": [
@@ -11471,7 +11519,7 @@
},
"performanceBudget": {
"required": true,
"evidence": "Terminal input hot-path audit clean on 2026-08-16: candidate guards retain O(1) checks and bounded per-pane state. Layout changes add one process-lifetime native listener, two singleton renderer subscriptions, one O(live windows) broadcast per OS event, and generation-fenced refreshes whose native helper read is deduplicated in flight. The path is event-driven with will-quit/dispose cleanup and adds no polling, timers, per-key IPC, SSH/remote transport work, unbounded queues, or retained terminal data."
"evidence": "Terminal input hot-path audit clean through 2026-08-29: candidate guards retain O(1) checks and bounded per-pane state. While composition is active, overlay reconciliation scans only the current row, bounded by terminal columns, retains at most three current DOM children, reuses one preedit-bounds layout read for visual and native-candidate geometry, and cancels its single deferred timer on replacement and disposal. Layout changes add one process-lifetime native listener, two singleton renderer subscriptions, one O(live windows) broadcast per OS event, and generation-fenced refreshes whose native helper read is deduplicated in flight. The paths are event-driven and add no polling, per-key IPC, SSH/remote transport work, unbounded queues, or retained terminal data."
},
"promotionCriteria": [
"Cover deterministic byte/cell oracles first.",
@@ -11480,6 +11528,7 @@
],
"knownGaps": [
"Current commands include renderer-unit coverage and a CDP-driven Electron repro; real OS IME automation may need manual or soak evidence.",
"Textarea geometry is a deterministic lower-layer oracle for native candidate placement; no automated run observes the operating system's candidate window itself.",
"Backspace/Enter during composition, JIS yen, Arabic/RTL, paste edge cases, and Windows ConPTY post-agent key reset still need representative gate coverage.",
"An older remote host omits the optional SnapshotStart kitty field, so a new client falls back to raw commits until live negotiation is observed.",
"Snapshot authorities expose only the active screen's effective flags, not kitty's complete two-screen push stacks; a pop that crosses the restored boundary degrades to unknown rather than a proven value."
+24 -24
View File
@@ -113,7 +113,7 @@ patchedDependencies:
'@xterm/addon-ligatures@0.11.0-beta.287': 47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920
'@xterm/addon-serialize@0.15.0-beta.287': af3b156143ed2ee9903b753145b63dd4a2ee72cadd7ef333ab0f4d5d3ebfc32c
'@xterm/addon-webgl@0.20.0-beta.286': 2c301a06ad9caa635d746147dcb2b1c69aa026904f65e1183564f08a0e601b91
'@xterm/xterm@6.1.0-beta.287': b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1
'@xterm/xterm@6.1.0-beta.287': a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426
lint-staged@16.4.0: 7333b3837f80a7fbd045964db6d76ba4fc118e49134bdbabb00585b6b7b60673
node-pty@1.1.0: 9a2eedbf2448b8ff1387a8a740ee5e4e968bf8484d7d80e12a3f6a2dc26b0e17
@@ -138,7 +138,7 @@ importers:
version: 2.5.6
'@xterm/addon-serialize':
specifier: 0.15.0-beta.287
version: 0.15.0-beta.287(patch_hash=af3b156143ed2ee9903b753145b63dd4a2ee72cadd7ef333ab0f4d5d3ebfc32c)(@xterm/xterm@6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1))
version: 0.15.0-beta.287(patch_hash=af3b156143ed2ee9903b753145b63dd4a2ee72cadd7ef333ab0f4d5d3ebfc32c)(@xterm/xterm@6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426))
'@xterm/headless':
specifier: 6.1.0-beta.287
version: 6.1.0-beta.287
@@ -313,25 +313,25 @@ importers:
version: 5.2.0(rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(jiti@2.7.0)(yaml@2.8.4))
'@xterm/addon-fit':
specifier: 0.12.0-beta.287
version: 0.12.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1))
version: 0.12.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426))
'@xterm/addon-ligatures':
specifier: 0.11.0-beta.287
version: 0.11.0-beta.287(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1))
version: 0.11.0-beta.287(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426))
'@xterm/addon-search':
specifier: 0.17.0-beta.287
version: 0.17.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1))
version: 0.17.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426))
'@xterm/addon-unicode11':
specifier: 0.10.0-beta.287
version: 0.10.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1))
version: 0.10.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426))
'@xterm/addon-web-links':
specifier: 0.13.0-beta.287
version: 0.13.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1))
version: 0.13.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426))
'@xterm/addon-webgl':
specifier: 0.20.0-beta.286
version: 0.20.0-beta.286(patch_hash=2c301a06ad9caa635d746147dcb2b1c69aa026904f65e1183564f08a0e601b91)(@xterm/xterm@6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1))
version: 0.20.0-beta.286(patch_hash=2c301a06ad9caa635d746147dcb2b1c69aa026904f65e1183564f08a0e601b91)(@xterm/xterm@6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426))
'@xterm/xterm':
specifier: 6.1.0-beta.287
version: 6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1)
version: 6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -9718,39 +9718,39 @@ snapshots:
'@xmldom/xmldom@0.8.13': {}
'@xterm/addon-fit@0.12.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1))':
'@xterm/addon-fit@0.12.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426))':
dependencies:
'@xterm/xterm': 6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1)
'@xterm/xterm': 6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426)
'@xterm/addon-ligatures@0.11.0-beta.287(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1))':
'@xterm/addon-ligatures@0.11.0-beta.287(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426))':
dependencies:
'@xterm/xterm': 6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1)
'@xterm/xterm': 6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426)
lru-cache: 11.5.1
opentype.js: 2.0.0
'@xterm/addon-search@0.17.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1))':
'@xterm/addon-search@0.17.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426))':
dependencies:
'@xterm/xterm': 6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1)
'@xterm/xterm': 6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426)
'@xterm/addon-serialize@0.15.0-beta.287(patch_hash=af3b156143ed2ee9903b753145b63dd4a2ee72cadd7ef333ab0f4d5d3ebfc32c)(@xterm/xterm@6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1))':
'@xterm/addon-serialize@0.15.0-beta.287(patch_hash=af3b156143ed2ee9903b753145b63dd4a2ee72cadd7ef333ab0f4d5d3ebfc32c)(@xterm/xterm@6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426))':
dependencies:
'@xterm/xterm': 6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1)
'@xterm/xterm': 6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426)
'@xterm/addon-unicode11@0.10.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1))':
'@xterm/addon-unicode11@0.10.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426))':
dependencies:
'@xterm/xterm': 6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1)
'@xterm/xterm': 6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426)
'@xterm/addon-web-links@0.13.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1))':
'@xterm/addon-web-links@0.13.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426))':
dependencies:
'@xterm/xterm': 6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1)
'@xterm/xterm': 6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426)
'@xterm/addon-webgl@0.20.0-beta.286(patch_hash=2c301a06ad9caa635d746147dcb2b1c69aa026904f65e1183564f08a0e601b91)(@xterm/xterm@6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1))':
'@xterm/addon-webgl@0.20.0-beta.286(patch_hash=2c301a06ad9caa635d746147dcb2b1c69aa026904f65e1183564f08a0e601b91)(@xterm/xterm@6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426))':
dependencies:
'@xterm/xterm': 6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1)
'@xterm/xterm': 6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426)
'@xterm/headless@6.1.0-beta.287': {}
'@xterm/xterm@6.1.0-beta.287(patch_hash=b0ad265dfcc619bb8f1bdc1fcbea268c71202a54f14ec0b249065ff21f33c5f1)': {}
'@xterm/xterm@6.1.0-beta.287(patch_hash=a08f655f2f2ee2201070fb9b004b2b58a93138b42d0e82c9bf6a0614d1f6a426)': {}
abbrev@4.0.0: {}
@@ -240,7 +240,7 @@ describe('mid-line composition renders the covered row tail after the preedit',
expect([THEME.cursor, 'rgb(221, 238, 255)']).toContain(caret!.style.backgroundColor)
})
it('keeps the caret visible over committed text in the final cell', async () => {
it('keeps the caret and IME candidate anchor visible over committed text in the final cell', async () => {
const rig = openTerminal({ cursorWidth: 2 })
await rig.write('x'.repeat(80))
let preeditWidth = CELL_WIDTH_PX * 2
@@ -261,12 +261,16 @@ describe('mid-line composition renders the covered row tail after the preedit',
expect(rig.compositionView.style.display).toBe('flex')
expect(rig.compositionView.style.justifyContent).toBe('flex-end')
expect(caret!.style.width).toBe('2px')
expect(rig.textarea.style.left).toBe(`${rig.terminal.cols * CELL_WIDTH_PX - preeditWidth}px`)
expect(rig.textarea.style.width).toBe(`${preeditWidth}px`)
preeditWidth = CELL_WIDTH_PX / 2
await rig.writeAwaitingRender('\x1b[0m')
expect(remainder!.style.display).toBe('')
expect(rig.compositionView.style.display).toBe('')
expect(rig.compositionView.style.justifyContent).toBe('')
expect(rig.textarea.style.left).toBe(`${(rig.terminal.cols - 1) * CELL_WIDTH_PX}px`)
expect(rig.textarea.style.width).toBe(`${preeditWidth}px`)
})
it('keeps the default cursor visible on a light background', async () => {
@@ -23,6 +23,8 @@ export type MidlinePreeditOcclusionSample = {
remainderDisplay: string | null
remainderVisibility: string | null
caretRect: { left: number; right: number; width: number; height: number } | null
preeditRect: { left: number; right: number; width: number; height: number } | null
textareaRect: { left: number; right: number; width: number; height: number }
overlayActive: boolean
cursorColumn: number
terminalColumns: number
@@ -51,21 +53,25 @@ function readMidlinePreeditOcclusion(): MidlinePreeditOcclusionSample {
const terminal = pane.terminal
const screen = pane.container.querySelector<HTMLElement>('.xterm-screen')
const view = pane.container.querySelector<HTMLElement>('.composition-view')
if (!screen || !view) {
throw new Error('Active terminal has no screen element or composition view')
const textarea = terminal.textarea
if (!screen || !view || !textarea) {
throw new Error('Active terminal has no screen, composition view, or helper textarea')
}
const buffer = terminal.buffer.active
const line = buffer.getLine(buffer.baseY + buffer.cursorY)
const cursorColumn = buffer.cursorX
const cursorColumn = Math.min(buffer.cursorX, terminal.cols - 1)
const rowTailFromCursor = (line?.translateToString(true, cursorColumn) ?? '').trimEnd()
const screenRect = screen.getBoundingClientRect()
const overlayRect = view.getBoundingClientRect()
const cellWidth = terminal.cols > 0 ? screenRect.width / terminal.cols : 0
const caret = view.querySelector<HTMLElement>('.xterm-composition-caret')
const preedit = view.querySelector<HTMLElement>('.xterm-composition-preedit')
const remainder = view.querySelector<HTMLElement>('.xterm-composition-remainder')
const caretBounds = caret?.getBoundingClientRect()
const preeditBounds = preedit?.getBoundingClientRect()
const textareaBounds = textarea.getBoundingClientRect()
// A column counts as hidden when the overlay covers most of its cell, which keeps the sample
// stable against the sub-pixel width a proportional text node lands on.
@@ -112,6 +118,20 @@ function readMidlinePreeditOcclusion(): MidlinePreeditOcclusionSample {
height: caretBounds.height
}
: null,
preeditRect: preeditBounds
? {
left: preeditBounds.left,
right: preeditBounds.right,
width: preeditBounds.width,
height: preeditBounds.height
}
: null,
textareaRect: {
left: textareaBounds.left,
right: textareaBounds.right,
width: textareaBounds.width,
height: textareaBounds.height
},
overlayActive: view.classList.contains('active'),
cursorColumn,
terminalColumns: terminal.cols,
+10 -1
View File
@@ -1,7 +1,8 @@
import type { CDPSession, Page, TestInfo } from '@stablyai/playwright-test'
import { expect, type CDPSession, type Page, type TestInfo } from '@stablyai/playwright-test'
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import {
focusActiveTerminalInput,
getTerminalContent,
sendToTerminal,
waitForActivePanePtyId,
waitForActiveTerminalManager
@@ -25,6 +26,14 @@ export async function openTerminalImePaneArena(page: Page): Promise<TerminalImeP
await ensureTerminalVisible(page)
await waitForActiveTerminalManager(page, 30_000)
const ptyId = await waitForActivePanePtyId(page)
// The PTY binding precedes its initial snapshot replay; wait for the prompt before replacing the
// grid directly, otherwise that replay can overwrite the test row after composition starts.
await expect
.poll(() => getTerminalContent(page), {
timeout: 15_000,
message: 'Active terminal did not render its initial PTY frame'
})
.not.toBe('')
const session = await page.context().newCDPSession(page)
await focusActiveTerminalInput(page)
await installTerminalImeBoundaryProbe(page)
@@ -42,7 +42,7 @@ async function sampleOpenComposition(page: Page): Promise<MidlinePreeditOcclusio
.poll(
async () => {
const overlay = await samplePreeditOverlay(page)
return overlay.active && overlay.rect.width > 0 && overlay.text === '가'
return overlay.active && overlay.rect.width > 0 && overlay.text.startsWith('가')
},
{ message: 'the preedit never reached the overlay at a non-zero size' }
)
@@ -60,12 +60,15 @@ test.describe('Terminal end-of-row Korean preedit cell span', () => {
const arena = await openTerminalImePaneArena(orcaPage)
let completed = false
try {
// CHA clamps to the last column; write a cell there, then move back onto it.
await writeToActiveTerminal(orcaPage, '\x1b[2J\x1b[H\x1b[999Gx\x1b[D')
// CHA clamps to the last column; xterm's wrap-pending cursor is the final-cell shape the
// composition helper itself clamps onto.
await writeToActiveTerminal(orcaPage, '\x1b[2J\x1b[H\x1b[999Gx')
await setImeComposition(arena.session, '가')
const sample = await sampleOpenComposition(orcaPage)
const caret = sample.caretRect
const preedit = sample.preeditRect
const textarea = sample.textareaRect
const screenRight = sample.screenRect.left + sample.screenRect.width
expect(sample.cursorColumn, 'the cursor is not in the final column').toBe(
sample.terminalColumns - 1
@@ -87,6 +90,28 @@ test.describe('Terminal end-of-row Korean preedit cell span', () => {
caret!.right,
'the preedit caret overflows the terminal at the right edge'
).toBeLessThanOrEqual(screenRight + 0.5)
expect(preedit, 'the active composition has no preedit element').not.toBeNull()
expect(textarea.width, 'the IME candidate anchor has zero width').toBeGreaterThan(0)
expect(
textarea.left,
'the IME candidate anchor overflows the terminal at the left edge'
).toBeGreaterThanOrEqual(sample.screenRect.left - 0.5)
expect(
textarea.right,
'the IME candidate anchor overflows the terminal at the right edge'
).toBeLessThanOrEqual(screenRight + 0.5)
expect(
textarea.right,
'the IME candidate anchor is not aligned with the preedit right edge'
).toBeCloseTo(preedit!.right, 0)
expect(
textarea.right,
'the IME candidate anchor is not aligned with the caret right edge'
).toBeCloseTo(caret!.right, 0)
expect(
textarea.right,
'the IME candidate anchor is not aligned with the terminal right edge'
).toBeCloseTo(screenRight, 0)
completed = true
} finally {
await closeTerminalImePaneArena(