Fix minimal TUI submitted-line duplicate appearance
This commit is contained in:
+13
-1
@@ -3,6 +3,18 @@
|
|||||||
"updated_at": "2026-02-23",
|
"updated_at": "2026-02-23",
|
||||||
"description": "Tracks the status of all Flynn plans and implementation phases",
|
"description": "Tracks the status of all Flynn plans and implementation phases",
|
||||||
"plans": {
|
"plans": {
|
||||||
|
"minimal-tui-submitted-line-dedupe": {
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2026-02-23",
|
||||||
|
"updated": "2026-02-23",
|
||||||
|
"summary": "Fixed duplicate-looking user messages in minimal TUI by clearing the just-submitted readline prompt line before rendering the timestamped `You` block, so sent content is shown once in the conversation transcript.",
|
||||||
|
"files_modified": [
|
||||||
|
"src/frontends/tui/minimal.ts",
|
||||||
|
"src/frontends/tui/minimal.test.ts",
|
||||||
|
"docs/plans/state.json"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm test:run src/frontends/tui/minimal.test.ts passing"
|
||||||
|
},
|
||||||
"dashboard-local-backend-update-actions": {
|
"dashboard-local-backend-update-actions": {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"date": "2026-02-23",
|
"date": "2026-02-23",
|
||||||
@@ -6010,7 +6022,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"overall_progress": {
|
"overall_progress": {
|
||||||
"total_test_count": 1941,
|
"total_test_count": 1942,
|
||||||
"all_tests_passing": true,
|
"all_tests_passing": true,
|
||||||
"p0_completion": "3/3 (100%)",
|
"p0_completion": "3/3 (100%)",
|
||||||
"p1_completion": "4/4 (100%)",
|
"p1_completion": "4/4 (100%)",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ function minimalTuiPrivates(value: MinimalTui): {
|
|||||||
handleCommand: (command: unknown) => Promise<void>;
|
handleCommand: (command: unknown) => Promise<void>;
|
||||||
handleEscapeAction: () => boolean;
|
handleEscapeAction: () => boolean;
|
||||||
handleCtrlCPress: (nowMs?: number) => boolean;
|
handleCtrlCPress: (nowMs?: number) => boolean;
|
||||||
|
clearSubmittedPromptLine: () => boolean;
|
||||||
prompt: (text: string) => Promise<string>;
|
prompt: (text: string) => Promise<string>;
|
||||||
rl: {
|
rl: {
|
||||||
once: (event: string, cb: () => void) => void;
|
once: (event: string, cb: () => void) => void;
|
||||||
@@ -61,6 +62,7 @@ function minimalTuiPrivates(value: MinimalTui): {
|
|||||||
handleCommand: (command: unknown) => Promise<void>;
|
handleCommand: (command: unknown) => Promise<void>;
|
||||||
handleEscapeAction: () => boolean;
|
handleEscapeAction: () => boolean;
|
||||||
handleCtrlCPress: (nowMs?: number) => boolean;
|
handleCtrlCPress: (nowMs?: number) => boolean;
|
||||||
|
clearSubmittedPromptLine: () => boolean;
|
||||||
prompt: (text: string) => Promise<string>;
|
prompt: (text: string) => Promise<string>;
|
||||||
rl: {
|
rl: {
|
||||||
once: (event: string, cb: () => void) => void;
|
once: (event: string, cb: () => void) => void;
|
||||||
@@ -428,6 +430,46 @@ describe('MinimalTui backend command', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('MinimalTui prompt cancellation', () => {
|
describe('MinimalTui prompt cancellation', () => {
|
||||||
|
it('omits leading newline when submitted prompt line was cleared', async () => {
|
||||||
|
const mockSession = {
|
||||||
|
id: 'test',
|
||||||
|
getHistory: () => [],
|
||||||
|
addMessage: vi.fn(),
|
||||||
|
clear: vi.fn(),
|
||||||
|
replaceHistory: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
||||||
|
try {
|
||||||
|
const mockAgent = {
|
||||||
|
process: vi.fn(async () => 'ok'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const tui = new MinimalTui({
|
||||||
|
session: asSession(mockSession),
|
||||||
|
modelClient: asRouter({}),
|
||||||
|
agent: asAgent(mockAgent),
|
||||||
|
systemPrompt: 'test',
|
||||||
|
});
|
||||||
|
|
||||||
|
const clearSpy = vi.fn(() => true);
|
||||||
|
minimalTuiPrivates(tui).clearSubmittedPromptLine = clearSpy;
|
||||||
|
|
||||||
|
await minimalTuiPrivates(tui).handleCommand({ type: 'message', content: 'hello' });
|
||||||
|
|
||||||
|
expect(clearSpy).toHaveBeenCalledOnce();
|
||||||
|
const userHeader = writeSpy.mock.calls
|
||||||
|
.map(([chunk]) => String(chunk))
|
||||||
|
.find((chunk) => chunk.includes('You'));
|
||||||
|
expect(userHeader).toBeDefined();
|
||||||
|
expect(userHeader?.startsWith('\n')).toBe(false);
|
||||||
|
} finally {
|
||||||
|
writeSpy.mockRestore();
|
||||||
|
logSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('cancels an active prompt without closing the TUI', async () => {
|
it('cancels an active prompt without closing the TUI', async () => {
|
||||||
const mockSession = {
|
const mockSession = {
|
||||||
id: 'test',
|
id: 'test',
|
||||||
|
|||||||
@@ -237,6 +237,21 @@ export class MinimalTui {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private clearSubmittedPromptLine(): boolean {
|
||||||
|
if (!process.stdout.isTTY) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
readline.moveCursor(process.stdout, 0, -1);
|
||||||
|
readline.clearLine(process.stdout, 0);
|
||||||
|
readline.cursorTo(process.stdout, 0);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private handleToolEvent(event: ToolUseEvent): void {
|
private handleToolEvent(event: ToolUseEvent): void {
|
||||||
if (!this.verbose) {
|
if (!this.verbose) {
|
||||||
return;
|
return;
|
||||||
@@ -1309,9 +1324,10 @@ export class MinimalTui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async handleMessage(content: string): Promise<void> {
|
private async handleMessage(content: string): Promise<void> {
|
||||||
|
const clearedPromptLine = this.clearSubmittedPromptLine();
|
||||||
const userTimestamp = formatMessageTimestampParts(Date.now());
|
const userTimestamp = formatMessageTimestampParts(Date.now());
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
`\n${colors.blue}${colors.bold}You${colors.reset} ${colors.gray}[${userTimestamp.date} | ${userTimestamp.time}]${colors.reset}\n`,
|
`${clearedPromptLine ? '' : '\n'}${colors.blue}${colors.bold}You${colors.reset} ${colors.gray}[${userTimestamp.date} | ${userTimestamp.time}]${colors.reset}\n`,
|
||||||
);
|
);
|
||||||
process.stdout.write(`${content}\n`);
|
process.stdout.write(`${content}\n`);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user