wiggum-cli 0.7.3 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/conversation/conversation-manager.d.ts +31 -1
- package/dist/ai/conversation/conversation-manager.d.ts.map +1 -1
- package/dist/ai/conversation/conversation-manager.js +48 -3
- package/dist/ai/conversation/conversation-manager.js.map +1 -1
- package/dist/ai/conversation/index.d.ts +3 -2
- package/dist/ai/conversation/index.d.ts.map +1 -1
- package/dist/ai/conversation/index.js +1 -0
- package/dist/ai/conversation/index.js.map +1 -1
- package/dist/ai/conversation/interview-tools.d.ts +85 -0
- package/dist/ai/conversation/interview-tools.d.ts.map +1 -0
- package/dist/ai/conversation/interview-tools.js +255 -0
- package/dist/ai/conversation/interview-tools.js.map +1 -0
- package/dist/ai/conversation/spec-generator.d.ts +36 -0
- package/dist/ai/conversation/spec-generator.d.ts.map +1 -1
- package/dist/ai/conversation/spec-generator.js +219 -30
- package/dist/ai/conversation/spec-generator.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +10 -20
- package/dist/commands/init.js.map +1 -1
- package/dist/utils/repl-prompts.d.ts +58 -0
- package/dist/utils/repl-prompts.d.ts.map +1 -0
- package/dist/utils/repl-prompts.js +231 -0
- package/dist/utils/repl-prompts.js.map +1 -0
- package/dist/utils/tui.d.ts +61 -0
- package/dist/utils/tui.d.ts.map +1 -0
- package/dist/utils/tui.js +214 -0
- package/dist/utils/tui.js.map +1 -0
- package/package.json +1 -1
- package/src/ai/conversation/conversation-manager.ts +66 -4
- package/src/ai/conversation/index.ts +7 -0
- package/src/ai/conversation/interview-tools.ts +293 -0
- package/src/ai/conversation/spec-generator.ts +287 -34
- package/src/commands/init.ts +10 -22
- package/src/utils/repl-prompts.ts +286 -0
- package/src/utils/tui.ts +262 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL-Friendly Prompts
|
|
3
|
+
* Simple prompts using readline that work properly in REPL context
|
|
4
|
+
* (Clack prompts conflict with REPL readline management)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import readline from 'node:readline';
|
|
8
|
+
import pc from 'picocolors';
|
|
9
|
+
import { simpson } from './colors.js';
|
|
10
|
+
|
|
11
|
+
export interface SelectOption<T> {
|
|
12
|
+
value: T;
|
|
13
|
+
label: string;
|
|
14
|
+
hint?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Simple select prompt for REPL
|
|
19
|
+
*/
|
|
20
|
+
export async function select<T>(options: {
|
|
21
|
+
message: string;
|
|
22
|
+
options: SelectOption<T>[];
|
|
23
|
+
}): Promise<T | null> {
|
|
24
|
+
const { message, options: choices } = options;
|
|
25
|
+
|
|
26
|
+
console.log('');
|
|
27
|
+
console.log(`${simpson.yellow('?')} ${message}`);
|
|
28
|
+
console.log('');
|
|
29
|
+
|
|
30
|
+
choices.forEach((choice, index) => {
|
|
31
|
+
const hint = choice.hint ? pc.dim(` (${choice.hint})`) : '';
|
|
32
|
+
console.log(` ${pc.cyan(`${index + 1})`)} ${choice.label}${hint}`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
console.log('');
|
|
36
|
+
|
|
37
|
+
const rl = readline.createInterface({
|
|
38
|
+
input: process.stdin,
|
|
39
|
+
output: process.stdout,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
rl.question(`${pc.dim('Enter number (1-' + choices.length + '):')} `, (answer) => {
|
|
44
|
+
rl.close();
|
|
45
|
+
const num = parseInt(answer.trim(), 10);
|
|
46
|
+
if (num >= 1 && num <= choices.length) {
|
|
47
|
+
const selected = choices[num - 1];
|
|
48
|
+
console.log(`${pc.green('✓')} Selected: ${selected.label}`);
|
|
49
|
+
resolve(selected.value);
|
|
50
|
+
} else {
|
|
51
|
+
console.log(pc.red('Invalid selection'));
|
|
52
|
+
resolve(null);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
rl.on('SIGINT', () => {
|
|
57
|
+
rl.close();
|
|
58
|
+
resolve(null);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Simple password prompt for REPL
|
|
65
|
+
* Masks input with asterisks
|
|
66
|
+
*/
|
|
67
|
+
export async function password(options: {
|
|
68
|
+
message: string;
|
|
69
|
+
}): Promise<string | null> {
|
|
70
|
+
const { message } = options;
|
|
71
|
+
|
|
72
|
+
console.log('');
|
|
73
|
+
console.log(`${simpson.yellow('?')} ${message}`);
|
|
74
|
+
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
let input = '';
|
|
77
|
+
|
|
78
|
+
// Show initial cursor position
|
|
79
|
+
process.stdout.write(`${pc.dim('>')} `);
|
|
80
|
+
|
|
81
|
+
const onData = (char: string) => {
|
|
82
|
+
// Ctrl+C
|
|
83
|
+
if (char === '\u0003') {
|
|
84
|
+
cleanup();
|
|
85
|
+
console.log('');
|
|
86
|
+
resolve(null);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Enter
|
|
91
|
+
if (char === '\r' || char === '\n') {
|
|
92
|
+
cleanup();
|
|
93
|
+
// Clear the line and show fixed mask
|
|
94
|
+
process.stdout.write('\r' + pc.dim('>') + ' ' + '*'.repeat(32) + '\n');
|
|
95
|
+
console.log(`${pc.green('✓')} API key entered`);
|
|
96
|
+
resolve(input.trim() || null);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Backspace
|
|
101
|
+
if (char === '\u007F' || char === '\b') {
|
|
102
|
+
if (input.length > 0) {
|
|
103
|
+
input = input.slice(0, -1);
|
|
104
|
+
// Clear and rewrite asterisks
|
|
105
|
+
process.stdout.write('\r' + pc.dim('>') + ' ' + '*'.repeat(input.length) + ' \b');
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Regular character
|
|
111
|
+
if (char.charCodeAt(0) >= 32 && char.charCodeAt(0) <= 126) {
|
|
112
|
+
input += char;
|
|
113
|
+
process.stdout.write('*');
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const cleanup = () => {
|
|
118
|
+
if (process.stdin.isTTY) {
|
|
119
|
+
process.stdin.setRawMode(false);
|
|
120
|
+
}
|
|
121
|
+
process.stdin.removeListener('data', onData);
|
|
122
|
+
process.stdin.pause();
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
if (process.stdin.isTTY) {
|
|
126
|
+
process.stdin.setRawMode(true);
|
|
127
|
+
}
|
|
128
|
+
process.stdin.resume();
|
|
129
|
+
process.stdin.setEncoding('utf8');
|
|
130
|
+
process.stdin.on('data', onData);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Simple confirm prompt for REPL
|
|
136
|
+
*/
|
|
137
|
+
export async function confirm(options: {
|
|
138
|
+
message: string;
|
|
139
|
+
initialValue?: boolean;
|
|
140
|
+
}): Promise<boolean | null> {
|
|
141
|
+
const { message, initialValue = true } = options;
|
|
142
|
+
|
|
143
|
+
console.log('');
|
|
144
|
+
const defaultHint = initialValue ? 'Y/n' : 'y/N';
|
|
145
|
+
|
|
146
|
+
const rl = readline.createInterface({
|
|
147
|
+
input: process.stdin,
|
|
148
|
+
output: process.stdout,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
rl.question(`${simpson.yellow('?')} ${message} ${pc.dim(`(${defaultHint}):`)} `, (answer) => {
|
|
153
|
+
rl.close();
|
|
154
|
+
const trimmed = answer.trim().toLowerCase();
|
|
155
|
+
|
|
156
|
+
if (trimmed === '') {
|
|
157
|
+
console.log(`${pc.green('✓')} ${initialValue ? 'Yes' : 'No'}`);
|
|
158
|
+
resolve(initialValue);
|
|
159
|
+
} else if (trimmed === 'y' || trimmed === 'yes') {
|
|
160
|
+
console.log(`${pc.green('✓')} Yes`);
|
|
161
|
+
resolve(true);
|
|
162
|
+
} else if (trimmed === 'n' || trimmed === 'no') {
|
|
163
|
+
console.log(`${pc.green('✓')} No`);
|
|
164
|
+
resolve(false);
|
|
165
|
+
} else {
|
|
166
|
+
console.log(pc.red('Invalid input, using default'));
|
|
167
|
+
resolve(initialValue);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
rl.on('SIGINT', () => {
|
|
172
|
+
rl.close();
|
|
173
|
+
resolve(null);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Check if user cancelled (similar to clack's isCancel)
|
|
180
|
+
*/
|
|
181
|
+
export function isCancel(value: unknown): value is null {
|
|
182
|
+
return value === null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Multi-line input for paste support
|
|
187
|
+
* Reads input until an empty line (after content) or Ctrl+D
|
|
188
|
+
*
|
|
189
|
+
* @param prompt - The prompt to display
|
|
190
|
+
* @returns The collected input or null if cancelled
|
|
191
|
+
*/
|
|
192
|
+
export async function multilineInput(options: {
|
|
193
|
+
prompt?: string;
|
|
194
|
+
/** Hint to show for how to end input */
|
|
195
|
+
endHint?: string;
|
|
196
|
+
}): Promise<string | null> {
|
|
197
|
+
const { prompt = '>', endHint = 'Press Enter twice when done' } = options;
|
|
198
|
+
|
|
199
|
+
console.log(pc.dim(` (${endHint})`));
|
|
200
|
+
|
|
201
|
+
return new Promise((resolve) => {
|
|
202
|
+
const lines: string[] = [];
|
|
203
|
+
let lastLineEmpty = false;
|
|
204
|
+
|
|
205
|
+
const rl = readline.createInterface({
|
|
206
|
+
input: process.stdin,
|
|
207
|
+
output: process.stdout,
|
|
208
|
+
prompt: `${pc.dim(prompt)} `,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
rl.prompt();
|
|
212
|
+
|
|
213
|
+
rl.on('line', (line) => {
|
|
214
|
+
const trimmed = line.trim();
|
|
215
|
+
|
|
216
|
+
// If we get an empty line after having content, we're done
|
|
217
|
+
if (trimmed === '' && lines.length > 0 && lastLineEmpty) {
|
|
218
|
+
rl.close();
|
|
219
|
+
// Remove the trailing empty line we added
|
|
220
|
+
const result = lines.slice(0, -1).join('\n').trim();
|
|
221
|
+
if (result) {
|
|
222
|
+
console.log(pc.green('✓') + pc.dim(` Received ${result.split('\n').length} line(s)`));
|
|
223
|
+
}
|
|
224
|
+
resolve(result || null);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
lastLineEmpty = trimmed === '';
|
|
229
|
+
lines.push(line);
|
|
230
|
+
rl.prompt();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
rl.on('close', () => {
|
|
234
|
+
// Ctrl+D or EOF
|
|
235
|
+
const result = lines.join('\n').trim();
|
|
236
|
+
if (result) {
|
|
237
|
+
console.log(pc.green('✓') + pc.dim(` Received ${result.split('\n').length} line(s)`));
|
|
238
|
+
}
|
|
239
|
+
resolve(result || null);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
rl.on('SIGINT', () => {
|
|
243
|
+
rl.close();
|
|
244
|
+
console.log('');
|
|
245
|
+
resolve(null);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Simple text input prompt
|
|
252
|
+
*
|
|
253
|
+
* @param prompt - The prompt to display
|
|
254
|
+
* @returns The input text or null if cancelled
|
|
255
|
+
*/
|
|
256
|
+
export async function textInput(options: {
|
|
257
|
+
message: string;
|
|
258
|
+
placeholder?: string;
|
|
259
|
+
}): Promise<string | null> {
|
|
260
|
+
const { message, placeholder } = options;
|
|
261
|
+
|
|
262
|
+
const hint = placeholder ? pc.dim(` (${placeholder})`) : '';
|
|
263
|
+
|
|
264
|
+
const rl = readline.createInterface({
|
|
265
|
+
input: process.stdin,
|
|
266
|
+
output: process.stdout,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return new Promise((resolve) => {
|
|
270
|
+
rl.question(`${simpson.yellow('?')} ${message}${hint}: `, (answer) => {
|
|
271
|
+
rl.close();
|
|
272
|
+
const trimmed = answer.trim();
|
|
273
|
+
if (trimmed) {
|
|
274
|
+
resolve(trimmed);
|
|
275
|
+
} else {
|
|
276
|
+
resolve(null);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
rl.on('SIGINT', () => {
|
|
281
|
+
rl.close();
|
|
282
|
+
console.log('');
|
|
283
|
+
resolve(null);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
}
|
package/src/utils/tui.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI (Text User Interface) Utilities
|
|
3
|
+
* Claude Code-like display helpers for the interview agent
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import pc from 'picocolors';
|
|
7
|
+
import { simpson } from './colors.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generation phases for the spec generator
|
|
11
|
+
*/
|
|
12
|
+
export type Phase = 'context' | 'goals' | 'interview' | 'generation' | 'complete';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Phase display configuration
|
|
16
|
+
*/
|
|
17
|
+
interface PhaseConfig {
|
|
18
|
+
label: string;
|
|
19
|
+
description: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PHASE_CONFIG: Record<Phase, PhaseConfig> = {
|
|
23
|
+
context: { label: 'Context', description: 'Share reference URLs or files' },
|
|
24
|
+
goals: { label: 'Goals', description: 'Describe what you want to build' },
|
|
25
|
+
interview: { label: 'Interview', description: 'Answer clarifying questions' },
|
|
26
|
+
generation: { label: 'Spec', description: 'Generate specification' },
|
|
27
|
+
complete: { label: 'Complete', description: 'Spec generated' },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Tool icons for display
|
|
32
|
+
*/
|
|
33
|
+
const TOOL_ICONS: Record<string, string> = {
|
|
34
|
+
read_file: '📂',
|
|
35
|
+
search_codebase: '🔍',
|
|
36
|
+
list_directory: '📁',
|
|
37
|
+
tavily_search: '🌐',
|
|
38
|
+
resolveLibraryId: '📚',
|
|
39
|
+
queryDocs: '📚',
|
|
40
|
+
context7: '📚',
|
|
41
|
+
default: '🔧',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Display the phase header (Claude Code style box)
|
|
46
|
+
*/
|
|
47
|
+
export function displayPhaseHeader(
|
|
48
|
+
featureName: string,
|
|
49
|
+
currentPhase: Phase,
|
|
50
|
+
questionCount?: { current: number; max: number }
|
|
51
|
+
): void {
|
|
52
|
+
const width = 66;
|
|
53
|
+
|
|
54
|
+
// Build phase indicators
|
|
55
|
+
const phases: Phase[] = ['context', 'goals', 'interview', 'generation'];
|
|
56
|
+
const phaseLabels = phases.map(phase => {
|
|
57
|
+
const config = PHASE_CONFIG[phase];
|
|
58
|
+
if (phase === currentPhase) {
|
|
59
|
+
return `[${config.label}]`;
|
|
60
|
+
}
|
|
61
|
+
return config.label;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const phaseString = phaseLabels.join(' → ');
|
|
65
|
+
|
|
66
|
+
// Build right-side info (question count)
|
|
67
|
+
const rightInfo = questionCount
|
|
68
|
+
? `Questions: ${questionCount.current}/${questionCount.max}`
|
|
69
|
+
: '';
|
|
70
|
+
|
|
71
|
+
// Create the header box
|
|
72
|
+
const title = `Feature: ${featureName}`;
|
|
73
|
+
const topLine = '┌─' + ' '.repeat(width - 4) + '─┐';
|
|
74
|
+
const bottomLine = '└─' + ' '.repeat(width - 4) + '─┘';
|
|
75
|
+
|
|
76
|
+
// Title line
|
|
77
|
+
const titlePadding = width - 4 - title.length;
|
|
78
|
+
const titleLine = `│ ${simpson.yellow(title)}${' '.repeat(Math.max(0, titlePadding))} │`;
|
|
79
|
+
|
|
80
|
+
// Phase line with right info
|
|
81
|
+
const phasePadding = width - 4 - phaseString.length - rightInfo.length;
|
|
82
|
+
const phaseLine = `│ ${pc.dim(phaseString)}${' '.repeat(Math.max(0, phasePadding))}${pc.dim(rightInfo)} │`;
|
|
83
|
+
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(pc.dim(topLine));
|
|
86
|
+
console.log(titleLine);
|
|
87
|
+
console.log(phaseLine);
|
|
88
|
+
console.log(pc.dim(bottomLine));
|
|
89
|
+
console.log('');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Display a tool usage indicator (like Claude Code)
|
|
94
|
+
*/
|
|
95
|
+
export function displayToolUse(toolName: string, args: Record<string, unknown>): void {
|
|
96
|
+
const icon = TOOL_ICONS[toolName] || TOOL_ICONS.default;
|
|
97
|
+
const formattedCall = formatToolCall(toolName, args);
|
|
98
|
+
console.log(pc.dim(` ${icon} ${formattedCall}`));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Format a tool call for display
|
|
103
|
+
*/
|
|
104
|
+
function formatToolCall(toolName: string, args: Record<string, unknown>): string {
|
|
105
|
+
switch (toolName) {
|
|
106
|
+
case 'read_file':
|
|
107
|
+
return `Reading ${args.path}`;
|
|
108
|
+
|
|
109
|
+
case 'search_codebase':
|
|
110
|
+
const dir = args.directory ? ` in ${args.directory}/` : '';
|
|
111
|
+
return `Searching for "${args.pattern}"${dir}`;
|
|
112
|
+
|
|
113
|
+
case 'list_directory':
|
|
114
|
+
return `Listing ${args.path || '.'}`;
|
|
115
|
+
|
|
116
|
+
case 'tavily_search':
|
|
117
|
+
return `Web: "${args.query}"`;
|
|
118
|
+
|
|
119
|
+
case 'resolveLibraryId':
|
|
120
|
+
return `Looking up library: ${args.libraryName}`;
|
|
121
|
+
|
|
122
|
+
case 'queryDocs':
|
|
123
|
+
return `Docs: ${args.libraryId} - "${(args.query as string)?.slice(0, 40)}..."`;
|
|
124
|
+
|
|
125
|
+
default:
|
|
126
|
+
// Generic format
|
|
127
|
+
const argStr = Object.entries(args)
|
|
128
|
+
.slice(0, 2)
|
|
129
|
+
.map(([k, v]) => `${k}=${JSON.stringify(v)}`.slice(0, 30))
|
|
130
|
+
.join(', ');
|
|
131
|
+
return `${toolName}(${argStr})`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Display session context (project info)
|
|
137
|
+
*/
|
|
138
|
+
export function displaySessionContext(context: {
|
|
139
|
+
projectName?: string;
|
|
140
|
+
stack?: string;
|
|
141
|
+
entryPoints?: string[];
|
|
142
|
+
tools?: { tavily: boolean; context7: boolean; codebase: boolean };
|
|
143
|
+
}): void {
|
|
144
|
+
const width = 66;
|
|
145
|
+
const topLine = '┌─' + ' '.repeat(width - 4) + '─┐';
|
|
146
|
+
const bottomLine = '└─' + ' '.repeat(width - 4) + '─┘';
|
|
147
|
+
|
|
148
|
+
const lines: string[] = [];
|
|
149
|
+
|
|
150
|
+
if (context.projectName) {
|
|
151
|
+
lines.push(`Project: ${context.projectName}`);
|
|
152
|
+
}
|
|
153
|
+
if (context.stack) {
|
|
154
|
+
lines.push(`Stack: ${context.stack}`);
|
|
155
|
+
}
|
|
156
|
+
if (context.entryPoints && context.entryPoints.length > 0) {
|
|
157
|
+
lines.push(`Entry: ${context.entryPoints.slice(0, 2).join(', ')}`);
|
|
158
|
+
}
|
|
159
|
+
if (context.tools) {
|
|
160
|
+
const toolList = [];
|
|
161
|
+
if (context.tools.tavily) toolList.push('Tavily ✓');
|
|
162
|
+
if (context.tools.context7) toolList.push('Context7 ✓');
|
|
163
|
+
if (context.tools.codebase) toolList.push('Codebase ✓');
|
|
164
|
+
if (toolList.length > 0) {
|
|
165
|
+
lines.push(`Tools: ${toolList.join(' ')}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (lines.length === 0) return;
|
|
170
|
+
|
|
171
|
+
console.log(pc.dim(topLine));
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
const padding = width - 4 - line.length;
|
|
174
|
+
console.log(`│ ${pc.dim(line)}${' '.repeat(Math.max(0, padding))} │`);
|
|
175
|
+
}
|
|
176
|
+
console.log(pc.dim(bottomLine));
|
|
177
|
+
console.log('');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Display progress phases with visual indicators
|
|
182
|
+
*/
|
|
183
|
+
export function displayProgressPhases(currentPhase: Phase): void {
|
|
184
|
+
const phases: Phase[] = ['context', 'goals', 'interview', 'generation'];
|
|
185
|
+
|
|
186
|
+
console.log('');
|
|
187
|
+
for (const phase of phases) {
|
|
188
|
+
const config = PHASE_CONFIG[phase];
|
|
189
|
+
let indicator: string;
|
|
190
|
+
let style: (text: string) => string;
|
|
191
|
+
|
|
192
|
+
if (phase === currentPhase) {
|
|
193
|
+
indicator = '◐'; // In progress
|
|
194
|
+
style = simpson.yellow;
|
|
195
|
+
} else if (getPhaseIndex(phase) < getPhaseIndex(currentPhase)) {
|
|
196
|
+
indicator = '●'; // Completed
|
|
197
|
+
style = pc.green;
|
|
198
|
+
} else {
|
|
199
|
+
indicator = '○'; // Pending
|
|
200
|
+
style = pc.dim;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const current = phase === currentPhase ? ' ← current' : '';
|
|
204
|
+
console.log(` ${style(indicator)} ${style(config.label)} ${pc.dim(`- ${config.description}`)}${pc.dim(current)}`);
|
|
205
|
+
}
|
|
206
|
+
console.log('');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get the index of a phase for comparison
|
|
211
|
+
*/
|
|
212
|
+
function getPhaseIndex(phase: Phase): number {
|
|
213
|
+
const order: Phase[] = ['context', 'goals', 'interview', 'generation', 'complete'];
|
|
214
|
+
return order.indexOf(phase);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Display a warning for garbled input
|
|
219
|
+
*/
|
|
220
|
+
export function displayGarbledInputWarning(received: string): void {
|
|
221
|
+
console.log('');
|
|
222
|
+
console.log(pc.yellow(`⚠️ Received incomplete input: "${received.slice(0, 50)}${received.length > 50 ? '...' : ''}"`));
|
|
223
|
+
console.log(pc.dim(' Please paste again or type your answer.'));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Display AI prefix before streaming
|
|
228
|
+
*/
|
|
229
|
+
export function displayAIPrefix(): void {
|
|
230
|
+
process.stdout.write(simpson.blue('AI: '));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Display a simple separator line
|
|
235
|
+
*/
|
|
236
|
+
export function displaySeparator(): void {
|
|
237
|
+
console.log(pc.dim('─'.repeat(60)));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Display thinking indicator
|
|
242
|
+
*/
|
|
243
|
+
export function displayThinking(): void {
|
|
244
|
+
process.stdout.write(pc.dim('Thinking...'));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Clear thinking indicator
|
|
249
|
+
*/
|
|
250
|
+
export function clearThinking(): void {
|
|
251
|
+
// Move cursor back and clear line
|
|
252
|
+
process.stdout.write('\r' + ' '.repeat(20) + '\r');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Format bytes for display
|
|
257
|
+
*/
|
|
258
|
+
export function formatBytes(bytes: number): string {
|
|
259
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
260
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
261
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
262
|
+
}
|