feat(cli): prefix text-input continuation lines with box-drawing characters

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Luca Martial 2026-05-12 16:58:00 -07:00
parent f091f948ee
commit 509f9f5301
7 changed files with 48 additions and 14 deletions

View file

@ -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\nKTX will use this short name in commands and config. You can rename it now.\nPress 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\nPress 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\nKTX will use this short name in commands and config. You can rename it now.\nPress 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);
});
});

View file

@ -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│`;
}

View file

@ -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}\nPress 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')}\nPress Escape to go back.\n`;
}
const legacyHistoricSqlServiceAccountPatternsKey = ['serviceAccount', 'UserPatterns'].join('');

View file

@ -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\nPress 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\nPress 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\nPress Escape to go back.\n',
});
await expect(readFile(join(tempDir, '.ktx/secrets/anthropic-api-key'), 'utf-8')).rejects.toMatchObject({
code: 'ENOENT',

View file

@ -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\nPress Escape to go back.\n',
placeholder: './analytics-ktx, ~/analytics-ktx, or /Users/you/projects/analytics-ktx',
}),
);

View file

@ -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}\nPress 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')}\nPress Escape to go back.\n`;
}
describe('setup sources step', () => {

View file

@ -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\nPress Escape to go back.\n',
placeholder: './analytics-ktx, ~/analytics-ktx, or /Users/you/projects/analytics-ktx',
}),
);