mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
fix(tui): Improve the Write tool UX. (#81)
* fix(tui): cap Write preview at args-finalize to preserve terminal scrollback The Write tool preview rendered the full file content between args-finalize and result, then snapped to the 10-line collapsed cap once the result landed. That large shrink triggered pi-tui's full-redraw path, which emits \x1b[3J and wipes the terminal scrollback (including history from before TUI start). Cap the preview as soon as args finalize so the rendered height stays stable across the args-finalize → result transition, avoiding the full redraw. * chore: update changeset message
This commit is contained in:
parent
6f22ae48f8
commit
1fbefc9939
3 changed files with 43 additions and 20 deletions
5
.changeset/fix-write-preview-scrollback.md
Normal file
5
.changeset/fix-write-preview-scrollback.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Improve the Write tool UX.
|
||||
|
|
@ -1390,10 +1390,6 @@ export class ToolCallComponent extends Container {
|
|||
this.buildStreamingPreview(this.toolCall.streamingArguments);
|
||||
return;
|
||||
}
|
||||
// Collapse only kicks in once the result lands (bullet turns
|
||||
// green). Between args-finalize and result we may sit in approval
|
||||
// for a while — keep the preview fully visible during that gap so
|
||||
// the user reviews the full change in context.
|
||||
const shouldCap = this.result !== undefined && !this.expanded;
|
||||
if (name === 'Write') {
|
||||
const content = str(this.toolCall.args['content']);
|
||||
|
|
@ -1401,13 +1397,18 @@ export class ToolCallComponent extends Container {
|
|||
const filePath = str(this.toolCall.args['file_path'] ?? this.toolCall.args['path']);
|
||||
const lang = langFromPath(filePath);
|
||||
const allLines = highlightLines(content, lang);
|
||||
const shown = shouldCap ? allLines.slice(0, COMMAND_PREVIEW_LINES) : allLines;
|
||||
// Cap as soon as args finalize, not just when result lands. Otherwise the
|
||||
// brief render tick between finalized args and result draws the full file,
|
||||
// and the snap back to the collapsed cap triggers pi-tui's full-redraw
|
||||
// path which wipes the terminal scrollback (pre-TUI history).
|
||||
const writeShouldCap = !this.expanded;
|
||||
const shown = writeShouldCap ? allLines.slice(0, COMMAND_PREVIEW_LINES) : allLines;
|
||||
const remaining = allLines.length - shown.length;
|
||||
for (const [i, line] of shown.entries()) {
|
||||
const lineNum = chalk.dim(String(i + 1).padStart(4) + ' ');
|
||||
this.addChild(new Text(lineNum + line, 2, 0));
|
||||
}
|
||||
if (shouldCap && remaining > 0) {
|
||||
if (writeShouldCap && remaining > 0) {
|
||||
this.addChild(
|
||||
new Text(
|
||||
chalk.dim(
|
||||
|
|
@ -1455,8 +1456,17 @@ export class ToolCallComponent extends Container {
|
|||
'';
|
||||
const lang = langFromPath(filePath);
|
||||
const allLines = highlightLines(content, lang);
|
||||
for (const [i, line] of allLines.entries()) {
|
||||
const lineNum = chalk.dim(String(i + 1).padStart(4) + ' ');
|
||||
const maxLines = COMMAND_PREVIEW_LINES;
|
||||
const scrollLines =
|
||||
allLines.length > maxLines
|
||||
? allLines.slice(allLines.length - maxLines)
|
||||
: allLines;
|
||||
for (const [i, line] of scrollLines.entries()) {
|
||||
const originalLineNumber =
|
||||
allLines.length > maxLines
|
||||
? allLines.length - maxLines + i
|
||||
: i;
|
||||
const lineNum = chalk.dim(String(originalLineNumber + 1).padStart(4) + ' ');
|
||||
this.addChild(new Text(lineNum + line, 2, 0));
|
||||
}
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -733,7 +733,7 @@ describe('ToolCallComponent', () => {
|
|||
expect(out).not.toContain('Used Agent');
|
||||
});
|
||||
|
||||
it('renders the full Write content during the streaming delta window (no cap)', () => {
|
||||
it('scrolls the Write streaming preview to the last COMMAND_PREVIEW_LINES', () => {
|
||||
const lines: string[] = [];
|
||||
for (let i = 1; i <= 30; i++) lines.push(`line${String(i)}`);
|
||||
const escaped = lines.join('\\n');
|
||||
|
|
@ -750,10 +750,14 @@ describe('ToolCallComponent', () => {
|
|||
|
||||
const out = strip(component.render(100).join('\n'));
|
||||
expect(out).toContain('Using Write');
|
||||
// All 30 lines must be present — no 10-line cap during streaming.
|
||||
expect(out).toContain('line1');
|
||||
expect(out).toContain('line25');
|
||||
// Streaming preview caps at COMMAND_PREVIEW_LINES (10) and shows the tail.
|
||||
expect(out).not.toContain('line1');
|
||||
expect(out).not.toContain('line20');
|
||||
expect(out).toContain('line21');
|
||||
expect(out).toContain('line30');
|
||||
// Line numbers should reflect actual file positions.
|
||||
expect(out).toContain(' 21');
|
||||
expect(out).toContain(' 30');
|
||||
expect(out).not.toContain('ctrl+o to expand');
|
||||
});
|
||||
|
||||
|
|
@ -824,12 +828,15 @@ describe('ToolCallComponent', () => {
|
|||
expect(out).not.toContain('ctrl+o to expand');
|
||||
});
|
||||
|
||||
it('keeps Write preview uncapped between finalized args and result (approval window)', () => {
|
||||
it('caps the Write preview between finalized args and result to keep transcript height stable', () => {
|
||||
// The wire sequence is: tool.call.delta → ... → tool.call (final
|
||||
// args, no streamingArguments) → approval pending ... → tool.result.
|
||||
// Between tool.call and tool.result the user may sit in an approval
|
||||
// panel; the preview must stay fully visible during that gap, only
|
||||
// collapsing once setResult fires (bullet turns green).
|
||||
// args, no streamingArguments) → tool.result. Between tool.call and
|
||||
// tool.result we briefly sit with finalized args and no result yet —
|
||||
// even without an approval panel, at least one render tick can land
|
||||
// in this state. The preview must stay capped so the transcript
|
||||
// height does not balloon and then snap back when the result lands;
|
||||
// a big shrink triggers pi-tui's full-redraw path which wipes the
|
||||
// terminal scrollback (history before TUI start).
|
||||
const lines: string[] = [];
|
||||
for (let i = 1; i <= 30; i++) lines.push(`line${String(i)}`);
|
||||
const component = new ToolCallComponent(
|
||||
|
|
@ -844,9 +851,10 @@ describe('ToolCallComponent', () => {
|
|||
);
|
||||
const out = strip(component.render(100).join('\n'));
|
||||
expect(out).toContain('line1');
|
||||
expect(out).toContain('line25');
|
||||
expect(out).toContain('line30');
|
||||
expect(out).not.toContain('ctrl+o to expand');
|
||||
expect(out).toContain('line10');
|
||||
expect(out).not.toContain('line11');
|
||||
expect(out).not.toContain('line25');
|
||||
expect(out).toContain('ctrl+o to expand');
|
||||
});
|
||||
|
||||
it('snaps a long Write preview to the collapsed cap when the result arrives', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue