Skip to content

Commit d66c16e

Browse files
fix: stdin buffer handling and terminal progress race (#1418)
* fix: stdin buffer handling improvements * Defer terminal progress update to avoid racing twinki render cycle Wrap syncTerminalProgress in setImmediate so the OSC 9;4 escape sequence lands after twinki's nextTick render frame, preventing the animation from taking priority over the actual render. * fix: wait for slash commands before asserting agent in e2e test The unknown-agent-fallback test was flaky because it asserted currentAgent before slash commands finished loading. Adding waitForSlashCommands() ensures the agent is fully initialized. --------- Co-authored-by: Kenneth S. <kennvene@amazon.com>
1 parent 34556e8 commit d66c16e

6 files changed

Lines changed: 26 additions & 10 deletions

File tree

packages/tui/e2e_tests/unknown-agent-fallback.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ describe('unknown agent fallback', () => {
2222
.launch();
2323

2424
await testCase.waitForText('ask a question', 15000);
25+
await testCase.waitForSlashCommands();
2526

2627
const store = await testCase.getStore();
2728
expect(store.currentAgent?.name).toBe('kiro_default');

packages/tui/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
"test:integ": "bun test ./integ_tests/",
2121
"test:e2e": "bun run scripts/run-e2e.ts",
2222
"bench:markdown": "bun run scripts/benchmark-markdown.ts",
23-
"dev:profile": "KIRO_AGENT_PATH=../../target/debug/chat_cli_v2 KIRO_INPUT_METRICS=true KIRO_PERF_METRICS=true KIRO_TUI_LOG_FILE=/tmp/kiro-tui-perf.log bun --cpu-prof --cpu-prof-dir=./profiles --watch ./src/index.tsx",
24-
"dev:profiletui": "KIRO_RENDERER=twinki KIRO_AGENT_PATH=../../target/debug/chat_cli_v2 KIRO_INPUT_METRICS=true KIRO_PERF_METRICS=true KIRO_TUI_LOG_FILE=/tmp/kiro-tui-perf.log bun --cpu-prof --cpu-prof-dir=./profiles --watch ./src/index.tsx",
23+
"dev:profile": "KIRO_AGENT_PATH=../../target/debug/chat_cli KIRO_INPUT_METRICS=true KIRO_PERF_METRICS=true KIRO_TUI_LOG_FILE=/tmp/kiro-tui-perf.log bun --cpu-prof --cpu-prof-dir=./profiles --watch ./src/index.tsx",
24+
"dev:profiletui": "KIRO_RENDERER=twinki KIRO_AGENT_PATH=../../target/debug/chat_cli KIRO_INPUT_METRICS=true KIRO_PERF_METRICS=true KIRO_TUI_LOG_FILE=/tmp/kiro-tui-perf.log bun --cpu-prof --cpu-prof-dir=./profiles --watch ./src/index.tsx",
2525
"analyze:profile": "bun run scripts/analyze-profile.ts --html"
2626
},
2727
"dependencies": {

packages/tui/src/stores/app-store.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1989,7 +1989,8 @@ export const createAppStore = (props: AppStoreProps) => {
19891989
const key = `${state.agentError ?? ''}|${state.pendingApproval != null}|${state.isProcessing}|${state.isCompacting}|${state.contextUsagePercent}`;
19901990
if (key !== lastProgressKey) {
19911991
lastProgressKey = key;
1992-
syncTerminalProgress(state);
1992+
// Defer so the OSC 9;4 escape lands after twinki's nextTick render frame.
1993+
setImmediate(() => syncTerminalProgress(state));
19931994
}
19941995
});
19951996

packages/twinki/packages/twinki/src/hooks/useInput.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ export function parseInputData(data: string): { input: string; key: Key } {
126126
let input = '';
127127
if (data.length === 1 && data.charCodeAt(0) >= 0x20) {
128128
input = data;
129+
} else if (data.length > 1 && data.charCodeAt(0) >= 0x20 && !parsed) {
130+
// Multi-character printable text (batched by StdinBuffer)
131+
input = data;
129132
} else if (key.ctrl && data.length === 1) {
130133
// Ctrl+letter (legacy): expose the letter as input, matching ink's behavior
131134
const code = data.charCodeAt(0);

packages/twinki/packages/twinki/src/input/stdin-buffer.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,21 @@ function extractCompleteSequences(buffer: string): { sequences: string[]; remain
207207
return { sequences, remainder: remaining };
208208
}
209209
} else {
210-
sequences.push(remaining[0]!);
211-
pos++;
210+
const ch = remaining.charCodeAt(0);
211+
if (ch >= 0x20) {
212+
// Batch consecutive printable characters into a single sequence,
213+
// matching Ink's behavior of passing whole chunks through.
214+
let end = 1;
215+
while (end < remaining.length && remaining.charCodeAt(end) >= 0x20 && remaining[end] !== ESC) {
216+
end++;
217+
}
218+
sequences.push(remaining.slice(0, end));
219+
pos += end;
220+
} else {
221+
// Control characters (0x00-0x1F except ESC) emitted individually
222+
sequences.push(remaining[0]!);
223+
pos++;
224+
}
212225
}
213226
}
214227

packages/twinki/packages/twinki/test/stdin-buffer.test.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,12 @@ describe('StdinBuffer', () => {
4949
expect(handler).toHaveBeenCalledWith('\x1b[A');
5050
});
5151

52-
it('splits multiple sequences in one chunk', () => {
52+
it('emits plain text as a single chunk', () => {
5353
const handler = vi.fn();
5454
buffer.on('data', handler);
5555
buffer.process('abc');
56-
expect(handler).toHaveBeenCalledTimes(3);
57-
expect(handler).toHaveBeenNthCalledWith(1, 'a');
58-
expect(handler).toHaveBeenNthCalledWith(2, 'b');
59-
expect(handler).toHaveBeenNthCalledWith(3, 'c');
56+
expect(handler).toHaveBeenCalledTimes(1);
57+
expect(handler).toHaveBeenCalledWith('abc');
6058
});
6159

6260
it('handles bracketed paste', () => {

0 commit comments

Comments
 (0)