From 509f9f53011f75ea3f52b2fc2eb71ba5761005c9 Mon Sep 17 00:00:00 2001 From: Luca Martial Date: Tue, 12 May 2026 16:58:00 -0700 Subject: [PATCH] feat(cli): prefix text-input continuation lines with box-drawing characters Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/cli/src/prompt-navigation.test.ts | 18 ++++++++++++--- packages/cli/src/prompt-navigation.ts | 26 ++++++++++++++++++++-- packages/cli/src/setup-databases.test.ts | 4 ++-- packages/cli/src/setup-models.test.ts | 6 ++--- packages/cli/src/setup-project.test.ts | 2 +- packages/cli/src/setup-sources.test.ts | 4 ++-- packages/cli/src/setup.test.ts | 2 +- 7 files changed, 48 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/prompt-navigation.test.ts b/packages/cli/src/prompt-navigation.test.ts index 4dd428df..9338b56e 100644 --- a/packages/cli/src/prompt-navigation.test.ts +++ b/packages/cli/src/prompt-navigation.test.ts @@ -28,12 +28,12 @@ describe('prompt navigation helpers', () => { 'Name this PostgreSQL connection\nKTX will use this short name in commands and config. You can rename it now.', ), ).toBe( - 'Name this PostgreSQL connection\n\nKTX will use this short name in commands and config. You can rename it now.\nPress Escape to go back.\n', + 'Name this PostgreSQL connection\n│\n│ KTX will use this short name in commands and config. You can rename it now.\n│ Press Escape to go back.\n│', ); }); it('adds a blank separator before compact text input values', () => { - expect(withTextInputNavigation('Project folder path')).toBe('Project folder path\nPress Escape to go back.\n'); + expect(withTextInputNavigation('Project folder path')).toBe('Project folder path\n│ Press Escape to go back.\n│'); }); it('normalizes already hinted text input prompts without duplicating the hint', () => { @@ -42,7 +42,19 @@ describe('prompt navigation helpers', () => { 'Name this PostgreSQL connection\nKTX will use this short name in commands and config. You can rename it now.\nPress Escape to go back.', ), ).toBe( - 'Name this PostgreSQL connection\n\nKTX will use this short name in commands and config. You can rename it now.\nPress Escape to go back.\n', + 'Name this PostgreSQL connection\n│\n│ KTX will use this short name in commands and config. You can rename it now.\n│ Press Escape to go back.\n│', ); }); + + it('is idempotent when text input navigation is applied twice', () => { + const once = withTextInputNavigation('Project folder path'); + expect(withTextInputNavigation(once)).toBe(once); + }); + + it('is idempotent when text input navigation with body is applied twice', () => { + const once = withTextInputNavigation( + 'Name this PostgreSQL connection\nKTX will use this short name in commands and config.', + ); + expect(withTextInputNavigation(once)).toBe(once); + }); }); diff --git a/packages/cli/src/prompt-navigation.ts b/packages/cli/src/prompt-navigation.ts index d80f2f97..c3644338 100644 --- a/packages/cli/src/prompt-navigation.ts +++ b/packages/cli/src/prompt-navigation.ts @@ -6,6 +6,26 @@ function removeTrailingBlankLines(message: string): string { return message.replace(/\n+$/, ''); } +function prefixContinuationLines(message: string): string { + const lines = message.split('\n'); + if (lines.length <= 1) return message; + const [title, ...body] = lines; + let trailingEmptyCount = 0; + while (trailingEmptyCount < body.length && body[body.length - 1 - trailingEmptyCount] === '') { + trailingEmptyCount++; + } + const contentBody = trailingEmptyCount > 0 ? body.slice(0, -trailingEmptyCount) : body; + const trailingBody = trailingEmptyCount > 0 ? body.slice(-trailingEmptyCount) : []; + return [ + title, + ...contentBody.map((line) => { + const stripped = line.replace(/^│\s*/, ''); + return stripped === '' ? '│' : `│ ${stripped}`; + }), + ...trailingBody, + ].join('\n'); +} + function withTextInputBodySpacing(message: string): string { const normalized = removeTrailingBlankLines(message); if (!normalized.includes('\n')) { @@ -39,7 +59,9 @@ export function withMultiselectNavigation(message: string): string { export function withTextInputNavigation(message: string): string { const messageWithoutHint = removeTrailingBlankLines(message) .split('\n') - .filter((line) => line !== TEXT_INPUT_NAVIGATION_HINT) + .filter((line) => !line.includes(TEXT_INPUT_NAVIGATION_HINT)) + .map((line) => line.replace(/^│\s*/, '')) .join('\n'); - return `${withTextInputBodySpacing(messageWithoutHint)}\n${TEXT_INPUT_NAVIGATION_HINT}\n`; + const full = `${withTextInputBodySpacing(messageWithoutHint)}\n${TEXT_INPUT_NAVIGATION_HINT}`; + return `${prefixContinuationLines(full)}\n│`; } diff --git a/packages/cli/src/setup-databases.test.ts b/packages/cli/src/setup-databases.test.ts index 2f5d93c6..a7a695c8 100644 --- a/packages/cli/src/setup-databases.test.ts +++ b/packages/cli/src/setup-databases.test.ts @@ -58,10 +58,10 @@ function connectionNamePrompt(label: string): string { function textInputPrompt(message: string): string { const normalized = message.replace(/\n+$/, ''); if (!normalized.includes('\n')) { - return `${normalized}\nPress Escape to go back.\n`; + return `${normalized}\n│ Press Escape to go back.\n│`; } const [title, ...bodyLines] = normalized.split('\n'); - return `${title}\n\n${bodyLines.join('\n')}\nPress Escape to go back.\n`; + return `${title}\n│\n│ ${bodyLines.join('\n│ ')}\n│ Press Escape to go back.\n│`; } const legacyHistoricSqlServiceAccountPatternsKey = ['serviceAccount', 'UserPatterns'].join(''); diff --git a/packages/cli/src/setup-models.test.ts b/packages/cli/src/setup-models.test.ts index 96092b25..bc8ec72e 100644 --- a/packages/cli/src/setup-models.test.ts +++ b/packages/cli/src/setup-models.test.ts @@ -310,7 +310,7 @@ describe('setup Anthropic model step', () => { expect(result.status).toBe('ready'); expect(prompts.select).not.toHaveBeenCalledWith(expect.objectContaining({ message: 'Paste Anthropic API key now?' })); expect(prompts.password).toHaveBeenCalledWith({ - message: 'Anthropic API key\nPress Escape to go back.\n', + message: 'Anthropic API key\n│ Press Escape to go back.\n│', }); }); @@ -462,7 +462,7 @@ describe('setup Anthropic model step', () => { ); expect(prompts.text).toHaveBeenCalledWith( expect.objectContaining({ - message: 'Anthropic model ID\nPress Escape to go back.\n', + message: 'Anthropic model ID\n│ Press Escape to go back.\n│', placeholder: 'claude-sonnet-4-6', }), ); @@ -626,7 +626,7 @@ describe('setup Anthropic model step', () => { expect(result.status).toBe('ready'); expect(prompts.password).toHaveBeenCalledWith({ - message: 'Anthropic API key\nPress Escape to go back.\n', + message: 'Anthropic API key\n│ Press Escape to go back.\n│', }); await expect(readFile(join(tempDir, '.ktx/secrets/anthropic-api-key'), 'utf-8')).rejects.toMatchObject({ code: 'ENOENT', diff --git a/packages/cli/src/setup-project.test.ts b/packages/cli/src/setup-project.test.ts index 8cf94a9b..cc65dcfb 100644 --- a/packages/cli/src/setup-project.test.ts +++ b/packages/cli/src/setup-project.test.ts @@ -206,7 +206,7 @@ describe('setup project step', () => { expect(result.projectDir).toBe(projectDir); expect(prompts.text).toHaveBeenCalledWith( expect.objectContaining({ - message: 'Project folder path\nPress Escape to go back.\n', + message: 'Project folder path\n│ Press Escape to go back.\n│', placeholder: './analytics-ktx, ~/analytics-ktx, or /Users/you/projects/analytics-ktx', }), ); diff --git a/packages/cli/src/setup-sources.test.ts b/packages/cli/src/setup-sources.test.ts index c74dd642..01c3885f 100644 --- a/packages/cli/src/setup-sources.test.ts +++ b/packages/cli/src/setup-sources.test.ts @@ -65,10 +65,10 @@ function connectionNamePrompt(label: string): string { function textInputPrompt(message: string): string { const normalized = message.replace(/\n+$/, ''); if (!normalized.includes('\n')) { - return `${normalized}\nPress Escape to go back.\n`; + return `${normalized}\n│ Press Escape to go back.\n│`; } const [title, ...bodyLines] = normalized.split('\n'); - return `${title}\n\n${bodyLines.join('\n')}\nPress Escape to go back.\n`; + return `${title}\n│\n│ ${bodyLines.join('\n│ ')}\n│ Press Escape to go back.\n│`; } describe('setup sources step', () => { diff --git a/packages/cli/src/setup.test.ts b/packages/cli/src/setup.test.ts index 22d070f1..695c34ce 100644 --- a/packages/cli/src/setup.test.ts +++ b/packages/cli/src/setup.test.ts @@ -723,7 +723,7 @@ describe('setup status', () => { expect(projectPrompts.text).toHaveBeenCalledWith( expect.objectContaining({ - message: 'Project folder path\nPress Escape to go back.\n', + message: 'Project folder path\n│ Press Escape to go back.\n│', placeholder: './analytics-ktx, ~/analytics-ktx, or /Users/you/projects/analytics-ktx', }), );