@robota-sdk/agent-ui-terminal 3.0.0-beta.93 → 3.0.0-beta.94
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/node/index.js.map +1 -1
- package/package.json +11 -11
- package/src/__tests__/__fixtures__/korean-text.json +17 -0
- package/src/__tests__/cjk-defer-submit.test.tsx +5 -4
- package/src/__tests__/cjk-fallback-render.test.tsx +15 -4
- package/src/__tests__/cjk-text-input-flow.test.ts +4 -3
- package/src/__tests__/cjk-text-input.test.ts +21 -16
- package/src/__tests__/message-list-rendering.test.tsx +2 -1
- package/src/__tests__/pty/ime-cursor-tmux.ptytest.ts +2 -1
- package/src/__tests__/pty/ime-cursor.ptytest.ts +4 -3
- package/src/__tests__/real-cursor-positioning.test.tsx +6 -5
- package/src/__tests__/streaming-indicator.test.tsx +2 -1
- package/src/attention/__tests__/focus-input-filter.test.ts +3 -2
- package/src/flows/__tests__/defer-submit.test.ts +4 -3
- package/src/flows/__tests__/real-cursor-flow.test.ts +20 -5
- package/src/flows/defer-submit.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robota-sdk/agent-ui-terminal",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.94",
|
|
4
4
|
"description": "Terminal UI package (React + Ink) for the Robota SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/node/index.js",
|
|
@@ -38,15 +38,15 @@
|
|
|
38
38
|
"marked-terminal": "^7.3.0",
|
|
39
39
|
"react": "19.2.8",
|
|
40
40
|
"string-width": "^8.2.2",
|
|
41
|
-
"@robota-sdk/agent-core": "3.0.0-beta.
|
|
42
|
-
"@robota-sdk/agent-
|
|
43
|
-
"@robota-sdk/agent-
|
|
44
|
-
"@robota-sdk/agent-interface-
|
|
45
|
-
"@robota-sdk/agent-interface-execution": "3.0.0-beta.
|
|
46
|
-
"@robota-sdk/agent-interface-
|
|
47
|
-
"@robota-sdk/agent-interface-
|
|
48
|
-
"@robota-sdk/agent-interface-
|
|
49
|
-
"@robota-sdk/agent-transport": "3.0.0-beta.
|
|
41
|
+
"@robota-sdk/agent-core": "3.0.0-beta.94",
|
|
42
|
+
"@robota-sdk/agent-interface-analytics": "3.0.0-beta.94",
|
|
43
|
+
"@robota-sdk/agent-framework": "3.0.0-beta.94",
|
|
44
|
+
"@robota-sdk/agent-interface-session": "3.0.0-beta.94",
|
|
45
|
+
"@robota-sdk/agent-interface-execution": "3.0.0-beta.94",
|
|
46
|
+
"@robota-sdk/agent-interface-transport": "3.0.0-beta.94",
|
|
47
|
+
"@robota-sdk/agent-interface-tui": "3.0.0-beta.94",
|
|
48
|
+
"@robota-sdk/agent-interface-command": "3.0.0-beta.94",
|
|
49
|
+
"@robota-sdk/agent-transport": "3.0.0-beta.94"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@homebridge/node-pty-prebuilt-multiarch": "^0.14.1",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"tsx": "^4.23.1",
|
|
58
58
|
"typescript": "^6.0.3",
|
|
59
59
|
"vitest": "^3.2.6",
|
|
60
|
-
"@robota-sdk/agent-command": "3.0.0-beta.
|
|
60
|
+
"@robota-sdk/agent-command": "3.0.0-beta.94"
|
|
61
61
|
},
|
|
62
62
|
"license": "AGPL-3.0-only OR LicenseRef-Commercial",
|
|
63
63
|
"publishConfig": {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"greeting": "안녕하세요",
|
|
3
|
+
"greetingWithoutLastSyllable": "안녕하세",
|
|
4
|
+
"greetingWithoutMiddle": "안세요",
|
|
5
|
+
"greetingFirstTwoSyllables": "안녕",
|
|
6
|
+
"greetingFirstThreeSyllables": "안녕하",
|
|
7
|
+
"greetingMiddleTwoSyllables": "녕하",
|
|
8
|
+
"greetingFirstSyllable": "안",
|
|
9
|
+
"greetingThirdSyllable": "하",
|
|
10
|
+
"greetingLastSyllable": "요",
|
|
11
|
+
"hangulWord": "한글",
|
|
12
|
+
"hangulWordPlusTe": "한글테",
|
|
13
|
+
"hangulWordPlusTest": "한글테스트",
|
|
14
|
+
"hanSyllable": "한",
|
|
15
|
+
"longResponseStreaming": "긴 한국어 응답과 emoji 🎉 를 스트리밍합니다",
|
|
16
|
+
"longResponseDisplay": "긴 한국어 응답과 emoji 🎉 를 표시합니다"
|
|
17
|
+
}
|
|
@@ -4,6 +4,7 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
4
4
|
|
|
5
5
|
import CjkTextInput from '../CjkTextInput.js';
|
|
6
6
|
import { IME_SUBMIT_DEFER_MS } from '../flows/defer-submit.js';
|
|
7
|
+
import korean from './__fixtures__/korean-text.json';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* CLI-061 — integration proof that the CjkTextInput defer-submit includes a trailing IME character whose stdin
|
|
@@ -29,23 +30,23 @@ function Harness({ onSubmit }: { onSubmit: (value: string) => void }): React.Rea
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
describe('CLI-061 — CjkTextInput defer-submit (integration)', () => {
|
|
32
|
-
it('includes a trailing syllable that arrives after Enter (
|
|
33
|
+
it('includes a trailing syllable that arrives after Enter (greeting without its last syllable + Enter + last syllable → full greeting)', async () => {
|
|
33
34
|
const onSubmit = vi.fn();
|
|
34
35
|
const { stdin } = render(<Harness onSubmit={onSubmit} />);
|
|
35
36
|
await tick();
|
|
36
37
|
|
|
37
|
-
stdin.write(
|
|
38
|
+
stdin.write(korean.greetingWithoutLastSyllable); // composed so far
|
|
38
39
|
await tick();
|
|
39
40
|
stdin.write('\r'); // Enter — schedules the deferred submit, does NOT submit synchronously
|
|
40
41
|
await tick();
|
|
41
42
|
expect(onSubmit).not.toHaveBeenCalled(); // deferred, not immediate
|
|
42
43
|
|
|
43
|
-
stdin.write(
|
|
44
|
+
stdin.write(korean.greetingLastSyllable); // the final syllable's stdin event lands after Enter; the pipeline is still live
|
|
44
45
|
await tick();
|
|
45
46
|
|
|
46
47
|
await waitDefer(); // the defer window elapses → the deferred submit fires, re-reading the LATEST value
|
|
47
48
|
expect(onSubmit).toHaveBeenCalledTimes(1);
|
|
48
|
-
expect(onSubmit).toHaveBeenCalledWith(
|
|
49
|
+
expect(onSubmit).toHaveBeenCalledWith(korean.greeting);
|
|
49
50
|
}, 10_000);
|
|
50
51
|
|
|
51
52
|
it('a second Enter within the window does not double-submit', async () => {
|
|
@@ -16,6 +16,7 @@ import React from 'react';
|
|
|
16
16
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
17
17
|
|
|
18
18
|
import CjkTextInput from '../CjkTextInput.js';
|
|
19
|
+
import korean from './__fixtures__/korean-text.json';
|
|
19
20
|
|
|
20
21
|
const noop = (): void => {};
|
|
21
22
|
const tick = (): Promise<void> => new Promise((r) => setTimeout(r, 30));
|
|
@@ -43,9 +44,9 @@ describe('CLI-062 — fallback rendering is byte-identical to pre-change output'
|
|
|
43
44
|
});
|
|
44
45
|
|
|
45
46
|
it('CJK value with drawn end-of-line cursor', async () => {
|
|
46
|
-
const { lastFrame, unmount } = render(<CjkTextInput value=
|
|
47
|
+
const { lastFrame, unmount } = render(<CjkTextInput value={korean.greeting} onChange={noop} />);
|
|
47
48
|
await tick();
|
|
48
|
-
expect(lastFrame()).toBe(
|
|
49
|
+
expect(lastFrame()).toBe(`${korean.greeting}\u001b[7m \u001b[27m`);
|
|
49
50
|
unmount();
|
|
50
51
|
});
|
|
51
52
|
|
|
@@ -69,10 +70,20 @@ describe('CLI-062 — fallback rendering is byte-identical to pre-change output'
|
|
|
69
70
|
|
|
70
71
|
it('long value wraps identically (the <Box ref> wrapper must not change layout)', async () => {
|
|
71
72
|
const { lastFrame, unmount } = render(
|
|
72
|
-
<CjkTextInput
|
|
73
|
+
<CjkTextInput
|
|
74
|
+
value={'a'.repeat(150) + korean.greetingFirstTwoSyllables}
|
|
75
|
+
onChange={noop}
|
|
76
|
+
availableWidth={58}
|
|
77
|
+
/>,
|
|
73
78
|
);
|
|
74
79
|
await tick();
|
|
75
|
-
expect(lastFrame()).toBe(
|
|
80
|
+
expect(lastFrame()).toBe(
|
|
81
|
+
'a'.repeat(100) +
|
|
82
|
+
'\n' +
|
|
83
|
+
'a'.repeat(50) +
|
|
84
|
+
korean.greetingFirstTwoSyllables +
|
|
85
|
+
'\u001b[7m \u001b[27m',
|
|
86
|
+
);
|
|
76
87
|
unmount();
|
|
77
88
|
});
|
|
78
89
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import korean from './__fixtures__/korean-text.json';
|
|
2
3
|
import {
|
|
3
4
|
applyCjkTextInput,
|
|
4
5
|
applyCjkTextPaste,
|
|
@@ -53,10 +54,10 @@ describe('cjk text input flow', () => {
|
|
|
53
54
|
it('Given Ink usePaste single-line text When applied Then text is inserted at cursor', () => {
|
|
54
55
|
const state = { ...createCjkTextInputFlowState('ab'), cursor: 1 };
|
|
55
56
|
|
|
56
|
-
const result = applyCjkTextPaste(state,
|
|
57
|
+
const result = applyCjkTextPaste(state, korean.hangulWord, { canPaste: true });
|
|
57
58
|
|
|
58
|
-
expect(result.state).toMatchObject({ value:
|
|
59
|
-
expect(result.effect).toEqual({ type: 'change', value:
|
|
59
|
+
expect(result.state).toMatchObject({ value: `a${korean.hangulWord}b`, cursor: 3 });
|
|
60
|
+
expect(result.effect).toEqual({ type: 'change', value: `a${korean.hangulWord}b` });
|
|
60
61
|
});
|
|
61
62
|
|
|
62
63
|
it('Given Ink usePaste multiline text When applied Then normalized paste effect is emitted', () => {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import korean from './__fixtures__/korean-text.json';
|
|
9
10
|
import {
|
|
10
11
|
filterPrintable,
|
|
11
12
|
insertAtCursor,
|
|
@@ -31,7 +32,7 @@ describe('filterPrintable', () => {
|
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
it('passes through Korean characters', () => {
|
|
34
|
-
expect(filterPrintable(
|
|
35
|
+
expect(filterPrintable(korean.greeting)).toBe(korean.greeting);
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
it('passes through emoji', () => {
|
|
@@ -99,8 +100,12 @@ describe('insertAtCursor', () => {
|
|
|
99
100
|
});
|
|
100
101
|
|
|
101
102
|
it('handles Korean character insertion', () => {
|
|
102
|
-
const result = insertAtCursor(
|
|
103
|
-
|
|
103
|
+
const result = insertAtCursor(
|
|
104
|
+
korean.greetingWithoutMiddle,
|
|
105
|
+
1,
|
|
106
|
+
korean.greetingMiddleTwoSyllables,
|
|
107
|
+
);
|
|
108
|
+
expect(result.value).toBe(korean.greeting);
|
|
104
109
|
expect(result.cursor).toBe(3);
|
|
105
110
|
});
|
|
106
111
|
|
|
@@ -126,24 +131,24 @@ describe('displayOffset', () => {
|
|
|
126
131
|
});
|
|
127
132
|
|
|
128
133
|
it('cursor before CJK char is at end of previous content', () => {
|
|
129
|
-
// "abcd
|
|
130
|
-
// gap happens when
|
|
131
|
-
expect(displayOffset([
|
|
132
|
-
// cursor after
|
|
133
|
-
expect(displayOffset([
|
|
134
|
+
// "abcd" + one wide syllable, width 5: cursor before the syllable is at col 4 on line 0
|
|
135
|
+
// gap happens when the syllable renders, not at cursor position
|
|
136
|
+
expect(displayOffset([...`abcd${korean.hanSyllable}`], 4, 5)).toBe(4);
|
|
137
|
+
// cursor after the syllable: 4(gap included) + 2 = 7 → but via offset: gap is at render
|
|
138
|
+
expect(displayOffset([...`abcd${korean.hanSyllable}`], 5, 5)).toBe(7);
|
|
134
139
|
});
|
|
135
140
|
|
|
136
141
|
it('handles pure CJK', () => {
|
|
137
|
-
//
|
|
138
|
-
expect(displayOffset([...
|
|
142
|
+
// two wide syllables, width 5: 2 + 2 → total 4, fits on one line
|
|
143
|
+
expect(displayOffset([...korean.hangulWord], 2, 5)).toBe(4);
|
|
139
144
|
});
|
|
140
145
|
|
|
141
146
|
it('handles CJK wrapping', () => {
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
expect(displayOffset([...
|
|
145
|
-
// cursor after
|
|
146
|
-
expect(displayOffset([...
|
|
147
|
+
// three wide syllables, width 5: cursor before the third is after the second (offset 4)
|
|
148
|
+
// the third syllable doesn't fit at col 4, but cursor is BEFORE the wrap
|
|
149
|
+
expect(displayOffset([...korean.hangulWordPlusTe], 2, 5)).toBe(4);
|
|
150
|
+
// cursor after the third syllable: gap(1) + 2 = offset 7
|
|
151
|
+
expect(displayOffset([...korean.hangulWordPlusTe], 3, 5)).toBe(7);
|
|
147
152
|
});
|
|
148
153
|
});
|
|
149
154
|
|
|
@@ -171,7 +176,7 @@ describe('charIndexAtDisplayOffset', () => {
|
|
|
171
176
|
});
|
|
172
177
|
|
|
173
178
|
it('round-trips with displayOffset for CJK', () => {
|
|
174
|
-
const text =
|
|
179
|
+
const text = korean.hangulWordPlusTest;
|
|
175
180
|
const chars = [...text];
|
|
176
181
|
const width = 5;
|
|
177
182
|
for (let i = 0; i <= chars.length; i++) {
|
|
@@ -15,6 +15,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
15
15
|
import MessageList from '../MessageList.js';
|
|
16
16
|
import { STATUS_SYMBOL, statusGlyphColor } from '../status-glyph.js';
|
|
17
17
|
import { DARK_THEME } from '../theme/built-in-themes.js';
|
|
18
|
+
import korean from './__fixtures__/korean-text.json';
|
|
18
19
|
import type { IHistoryEntry } from '@robota-sdk/agent-core';
|
|
19
20
|
import {
|
|
20
21
|
createUserMessage,
|
|
@@ -154,7 +155,7 @@ describe('MessageList rendering', () => {
|
|
|
154
155
|
});
|
|
155
156
|
|
|
156
157
|
it('assistant message preserves CJK and emoji content', () => {
|
|
157
|
-
const content =
|
|
158
|
+
const content = korean.longResponseDisplay;
|
|
158
159
|
const history: IHistoryEntry[] = [messageToHistoryEntry(createAssistantMessage(content))];
|
|
159
160
|
const { lastFrame } = render(<MessageList history={history} />);
|
|
160
161
|
|
|
@@ -27,6 +27,7 @@ import { tmpdir } from 'node:os';
|
|
|
27
27
|
import { join, resolve } from 'node:path';
|
|
28
28
|
|
|
29
29
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
30
|
+
import korean from '../__fixtures__/korean-text.json';
|
|
30
31
|
|
|
31
32
|
import { fixtureProductEnvironment } from './isolated-home.js';
|
|
32
33
|
|
|
@@ -38,7 +39,7 @@ const GIT_BIN =
|
|
|
38
39
|
.map((directory) => join(directory, 'git'))
|
|
39
40
|
.find((candidate) => existsSync(candidate)) ?? '/usr/bin/git';
|
|
40
41
|
const COLS = 80;
|
|
41
|
-
const COMPOSITION =
|
|
42
|
+
const COMPOSITION = korean.greetingFirstTwoSyllables;
|
|
42
43
|
const COMPOSITION_WIDTH = 4;
|
|
43
44
|
|
|
44
45
|
function resolveTmux(): string | undefined {
|
|
@@ -23,6 +23,7 @@ import { tmpdir } from 'node:os';
|
|
|
23
23
|
import { join } from 'node:path';
|
|
24
24
|
|
|
25
25
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
26
|
+
import korean from '../__fixtures__/korean-text.json';
|
|
26
27
|
|
|
27
28
|
import { spawnTui, writeTuiProviderSettings } from './pty-driver.js';
|
|
28
29
|
import {
|
|
@@ -39,8 +40,8 @@ import type { ICursorShowEvent } from '../helpers/vt-cursor-interpreter.js';
|
|
|
39
40
|
|
|
40
41
|
const COLS = 80;
|
|
41
42
|
const PROMPT_PLACEHOLDER = /Type a message or \/help/;
|
|
42
|
-
const COMPOSITION =
|
|
43
|
-
/**
|
|
43
|
+
const COMPOSITION = korean.greetingFirstTwoSyllables;
|
|
44
|
+
/** The composition occupies 4 display columns; the prompt adds `> `. */
|
|
44
45
|
const COMPOSITION_WIDTH = 4;
|
|
45
46
|
/** Let ink's throttled (≤30fps) frame writes flush after the last keystroke. */
|
|
46
47
|
const flush = (): Promise<void> => new Promise((r) => setTimeout(r, 400));
|
|
@@ -48,7 +49,7 @@ const flush = (): Promise<void> => new Promise((r) => setTimeout(r, 400));
|
|
|
48
49
|
interface IProbeResult {
|
|
49
50
|
/** Cursor shows emitted after boot — the composition window the OS IME would act on. */
|
|
50
51
|
compositionShows: ICursorShowEvent[];
|
|
51
|
-
/** Screen row holding `>
|
|
52
|
+
/** Screen row holding `> ` followed by the composition, or -1 when the composition never rendered. */
|
|
52
53
|
inputRow: number;
|
|
53
54
|
/** Column of the `> ` prompt on the input row, or -1. */
|
|
54
55
|
promptCol: number;
|
|
@@ -23,6 +23,7 @@ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest
|
|
|
23
23
|
import { interpretVtStream } from './helpers/vt-cursor-interpreter.js';
|
|
24
24
|
import CjkTextInput from '../CjkTextInput.js';
|
|
25
25
|
import { TerminalCapabilitiesProvider } from '../terminal-capabilities-context.js';
|
|
26
|
+
import korean from './__fixtures__/korean-text.json';
|
|
26
27
|
|
|
27
28
|
const COLS = 60;
|
|
28
29
|
const ROWS = 24;
|
|
@@ -143,7 +144,7 @@ describe('CLI-062 — real cursor positioning (interactive ink render)', () => {
|
|
|
143
144
|
const { stdout, stdin, unmount } = renderScenario();
|
|
144
145
|
await settle();
|
|
145
146
|
|
|
146
|
-
stdin.send(
|
|
147
|
+
stdin.send(korean.greetingFirstTwoSyllables);
|
|
147
148
|
await settle();
|
|
148
149
|
|
|
149
150
|
const afterTwo = interpretVtStream(stdout.stream(), ROWS, COLS);
|
|
@@ -153,10 +154,10 @@ describe('CLI-062 — real cursor positioning (interactive ink render)', () => {
|
|
|
153
154
|
for (const event of showsAfterTwo) {
|
|
154
155
|
expect(event.row).toBe(INPUT_ROW);
|
|
155
156
|
}
|
|
156
|
-
// Composition column: '> ' (2 cols) +
|
|
157
|
+
// Composition column: '> ' (2 cols) + two syllables (2 wide chars = 4 cols).
|
|
157
158
|
expect(showsAfterTwo.at(-1)).toMatchObject({ row: INPUT_ROW, col: 6 });
|
|
158
159
|
|
|
159
|
-
stdin.send(
|
|
160
|
+
stdin.send(korean.greetingThirdSyllable); // one more wide char → +2 columns
|
|
160
161
|
await settle();
|
|
161
162
|
|
|
162
163
|
const afterThree = interpretVtStream(stdout.stream(), ROWS, COLS);
|
|
@@ -189,7 +190,7 @@ describe('CLI-062 — real cursor positioning (interactive ink render)', () => {
|
|
|
189
190
|
for (const bannerLines of [1, 4]) {
|
|
190
191
|
const { stdout, stdin, unmount } = renderScenario(true, bannerLines);
|
|
191
192
|
await settle();
|
|
192
|
-
stdin.send(
|
|
193
|
+
stdin.send(korean.greetingFirstTwoSyllables);
|
|
193
194
|
await settle();
|
|
194
195
|
|
|
195
196
|
const vt = interpretVtStream(stdout.stream(), ROWS, COLS);
|
|
@@ -234,7 +235,7 @@ describe('CLI-062 — real cursor positioning (interactive ink render)', () => {
|
|
|
234
235
|
},
|
|
235
236
|
);
|
|
236
237
|
await settle();
|
|
237
|
-
stdin.send(
|
|
238
|
+
stdin.send(korean.greetingFirstSyllable);
|
|
238
239
|
await settle();
|
|
239
240
|
expect(interpretVtStream(stdout.stream(), ROWS, COLS).showEvents.length).toBeGreaterThan(0);
|
|
240
241
|
|
|
@@ -3,6 +3,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
3
3
|
import { render } from 'ink-testing-library';
|
|
4
4
|
import StreamingIndicator from '../StreamingIndicator.js';
|
|
5
5
|
import { ScreenReaderProvider } from '../screen-reader-context.js';
|
|
6
|
+
import korean from './__fixtures__/korean-text.json';
|
|
6
7
|
|
|
7
8
|
describe('StreamingIndicator', () => {
|
|
8
9
|
it('renders empty when no tools and no text', () => {
|
|
@@ -52,7 +53,7 @@ describe('StreamingIndicator', () => {
|
|
|
52
53
|
});
|
|
53
54
|
|
|
54
55
|
it('preserves CJK and emoji text in streaming output', () => {
|
|
55
|
-
const text =
|
|
56
|
+
const text = korean.longResponseStreaming;
|
|
56
57
|
const { lastFrame } = render(<StreamingIndicator text={text} activeTools={[]} />);
|
|
57
58
|
|
|
58
59
|
expect(lastFrame()).toContain(text);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
|
|
3
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import korean from '../../__tests__/__fixtures__/korean-text.json';
|
|
4
5
|
|
|
5
6
|
import { filterFocusSequences, FocusReportingStdin } from '../focus-input-filter.js';
|
|
6
7
|
import { createFocusReportingWriter } from '../../terminal-focus-reporting.js';
|
|
@@ -33,8 +34,8 @@ describe('focus input filter (SCREEN-1992 TC-06)', () => {
|
|
|
33
34
|
expect(filterFocusSequences(Buffer.from('A'), true).forwarded.toString()).toBe('A');
|
|
34
35
|
const paste = '\x1b[200~pasted \x1b[I text\x1b[201~';
|
|
35
36
|
expect(filterFocusSequences(Buffer.from(paste), false).forwarded.toString()).toBe(paste);
|
|
36
|
-
const utf8 = Buffer.from(
|
|
37
|
-
expect(filterFocusSequences(utf8, true).forwarded.toString()).toBe(
|
|
37
|
+
const utf8 = Buffer.from(`${korean.hangulWord}\x1b[O😀`);
|
|
38
|
+
expect(filterFocusSequences(utf8, true).forwarded.toString()).toBe(`${korean.hangulWord}😀`);
|
|
38
39
|
});
|
|
39
40
|
|
|
40
41
|
it('proxies stdin: reports focus and keystrokes, forwards isTTY/setRawMode/ref/unref', async () => {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
createDeferSubmitState,
|
|
6
6
|
scheduleDeferredSubmit,
|
|
7
7
|
} from '../defer-submit.js';
|
|
8
|
+
import korean from '../../__tests__/__fixtures__/korean-text.json';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* CLI-061 — the deferred-submit orchestration. The load-bearing property: at fire time the submit reads
|
|
@@ -30,16 +31,16 @@ describe('scheduleDeferredSubmit (CLI-061)', () => {
|
|
|
30
31
|
const sched = manualScheduler();
|
|
31
32
|
|
|
32
33
|
// Value at Enter/schedule time is the pre-trailing-char string...
|
|
33
|
-
let live =
|
|
34
|
+
let live: string = korean.greetingWithoutLastSyllable;
|
|
34
35
|
scheduleDeferredSubmit(state, () => live, submit, 50, sched.schedule);
|
|
35
36
|
expect(submit).not.toHaveBeenCalled(); // deferred, not immediate
|
|
36
37
|
|
|
37
38
|
// ...the trailing syllable lands during the window...
|
|
38
|
-
live =
|
|
39
|
+
live = korean.greeting;
|
|
39
40
|
sched.fire();
|
|
40
41
|
|
|
41
42
|
expect(submit).toHaveBeenCalledTimes(1);
|
|
42
|
-
expect(submit).toHaveBeenCalledWith(
|
|
43
|
+
expect(submit).toHaveBeenCalledWith(korean.greeting); // FULL value — fails on the pre-fix immediate submit
|
|
43
44
|
});
|
|
44
45
|
|
|
45
46
|
it('ignores a second submit while one is in flight (no double-submit)', () => {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { describe, expect, it } from 'vitest';
|
|
12
|
+
import korean from '../../__tests__/__fixtures__/korean-text.json';
|
|
12
13
|
|
|
13
14
|
import { computeCursorCell, shouldPositionRealCursor } from '../real-cursor-flow.js';
|
|
14
15
|
|
|
@@ -23,8 +24,15 @@ describe('computeCursorCell', () => {
|
|
|
23
24
|
expect(computeCursorCell({ ...origin, value: 'hello', cursor: 0 })).toEqual({ x: 3, y: 2 });
|
|
24
25
|
});
|
|
25
26
|
|
|
26
|
-
it('CJK wide chars count 2 columns each (
|
|
27
|
-
expect(
|
|
27
|
+
it('CJK wide chars count 2 columns each (two syllables → 4 columns)', () => {
|
|
28
|
+
expect(
|
|
29
|
+
computeCursorCell({
|
|
30
|
+
...origin,
|
|
31
|
+
value: korean.greetingFirstTwoSyllables,
|
|
32
|
+
cursor: 2,
|
|
33
|
+
availableWidth: 20,
|
|
34
|
+
}),
|
|
35
|
+
).toEqual({
|
|
28
36
|
x: 7,
|
|
29
37
|
y: 2,
|
|
30
38
|
});
|
|
@@ -32,12 +40,14 @@ describe('computeCursorCell', () => {
|
|
|
32
40
|
|
|
33
41
|
it('mid-string cursor inside a CJK value', () => {
|
|
34
42
|
expect(
|
|
35
|
-
computeCursorCell({ ...origin, value:
|
|
43
|
+
computeCursorCell({ ...origin, value: korean.greeting, cursor: 2, availableWidth: 20 }),
|
|
36
44
|
).toEqual({ x: 7, y: 2 });
|
|
37
45
|
});
|
|
38
46
|
|
|
39
47
|
it('no wrap width: CJK offset is the plain display-width sum', () => {
|
|
40
|
-
expect(
|
|
48
|
+
expect(
|
|
49
|
+
computeCursorCell({ ...origin, value: korean.greetingFirstThreeSyllables, cursor: 3 }),
|
|
50
|
+
).toEqual({ x: 9, y: 2 });
|
|
41
51
|
});
|
|
42
52
|
|
|
43
53
|
it('wraps to the next row when the offset passes the width', () => {
|
|
@@ -56,7 +66,12 @@ describe('computeCursorCell', () => {
|
|
|
56
66
|
// 9 ASCII cols + a wide char that cannot fit in the last column: displayOffset skips the
|
|
57
67
|
// dead column (9 → 10) then adds the wide char (→ 12) — cursor lands at row+1, col 2.
|
|
58
68
|
expect(
|
|
59
|
-
computeCursorCell({
|
|
69
|
+
computeCursorCell({
|
|
70
|
+
...origin,
|
|
71
|
+
value: `aaaaaaaaa${korean.greetingFirstSyllable}`,
|
|
72
|
+
cursor: 10,
|
|
73
|
+
availableWidth: 10,
|
|
74
|
+
}),
|
|
60
75
|
).toEqual({ x: 5, y: 3 });
|
|
61
76
|
});
|
|
62
77
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* IME composition is finalized by the terminal, which writes the final composed character into the pty as its
|
|
5
5
|
* OWN stdin event that arrives JUST AFTER the confirming `\r`. If we submit synchronously on Enter, the trailing
|
|
6
|
-
* syllable has not yet been applied to the input state, so it is dropped (
|
|
6
|
+
* syllable has not yet been applied to the input state, so it is dropped (a five-syllable Korean greeting loses its last syllable). Deferring the
|
|
7
7
|
* submit a few milliseconds lets that trailing event land, after which we re-read the LATEST value and submit it.
|
|
8
8
|
*
|
|
9
9
|
* This is the same app-layer fix Gemini CLI applied on the same Ink (PR #4987/#7556); no framework change and
|