@thegitai/cli 1.0.0-preview.3 → 1.0.0-preview.31
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/README.md +39 -6
- package/dist/bin/ai.js +142 -383
- package/dist/src/agent-mode.js +1 -6
- package/dist/src/api/auth.js +6 -4
- package/dist/src/api/browser-login.js +152 -37
- package/dist/src/api/chat.js +258 -38
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/api/default-host.js +1 -0
- package/dist/src/api/http.js +69 -7
- package/dist/src/api/models.js +19 -10
- package/dist/src/background-jobs.js +2 -2
- package/dist/src/cli-args.js +19 -5
- package/dist/src/core/clipboard.js +7 -13
- package/dist/src/core/image-limits.js +56 -0
- package/dist/src/core/image-path-extractor.js +70 -3
- package/dist/src/core/session-image-store.js +199 -0
- package/dist/src/executor.js +25 -3
- package/dist/src/help-text.js +67 -18
- package/dist/src/permissions.js +243 -0
- package/dist/src/session-safety.js +0 -12
- package/dist/src/session-store.js +121 -20
- package/dist/src/session.js +14 -3
- package/dist/src/signin.js +58 -0
- package/dist/src/tool-executor.js +11 -46
- package/dist/src/tools/delete-file.js +15 -3
- package/dist/src/tools/index.js +13 -10
- package/dist/src/tools/patch-file.js +12 -26
- package/dist/src/tools/read-image-file.js +85 -0
- package/dist/src/tools/replace-document-text.js +28 -18
- package/dist/src/tools/restore-checkpoint.js +0 -1
- package/dist/src/tools/run-command.js +14 -71
- package/dist/src/tools/run-node-script.js +12 -81
- package/dist/src/tools/save-generated-image.js +120 -0
- package/dist/src/tools/str-replace.js +12 -26
- package/dist/src/tools/undo-edit.js +1 -6
- package/dist/src/tools/write-file.js +67 -11
- package/dist/src/turn-failure-marker.js +11 -0
- package/dist/src/ui/prompt-history-store.js +1 -1
- package/dist/src/ui/repl.js +649 -164
- package/dist/src/ui/tui/bridge.js +10 -0
- package/dist/src/ui/tui/build-frame.js +453 -115
- package/dist/src/ui/tui/markdown-render.js +81 -73
- package/dist/src/ui/tui/shell-input.js +206 -63
- package/dist/src/ui/tui/terminal-theme.js +28 -0
- package/dist/src/ui/tui/terminal-title.js +3 -0
- package/dist/src/ui/tui/terminal-writes.js +48 -0
- package/dist/src/ui/tui/text.js +158 -4
- package/dist/src/ui/tui/user-input.js +568 -0
- package/dist/src/utils.js +9 -0
- package/package.json +29 -6
- package/dist/src/markdown-renderer.js +0 -112
- package/dist/src/project-index.js +0 -221
- package/dist/src/tools/code-intel.js +0 -472
- package/dist/src/tools/find-symbol.js +0 -70
- package/dist/src/tools/hover-symbol.js +0 -95
- package/dist/src/tools/list-symbols.js +0 -55
- package/dist/src/tools/search-code.js +0 -37
- package/dist/src/tools/signature-help.js +0 -118
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { agentModeLabel } from '../../agent-mode.js';
|
|
2
|
-
import { truncate } from '../../utils.js';
|
|
2
|
+
import { singleLinePreview, truncate } from '../../utils.js';
|
|
3
3
|
import { formatClientTokenUsage } from '../repl.js';
|
|
4
4
|
import { renderFormattedBodyLines, renderPreformattedBodyLines, } from './markdown-render.js';
|
|
5
|
-
import { line, plainLine, span, wrapText } from './text.js';
|
|
5
|
+
import { displayWidth, line, padToWidth, plainLine, sliceToWidth, span, wrapText, } from './text.js';
|
|
6
|
+
import { isDarkTerminalBackground, mutedColor, mutedStyle } from './terminal-theme.js';
|
|
7
|
+
import { buildUserInputOverlayLines, } from './user-input.js';
|
|
6
8
|
const WORKING_CLOCK_ICON = '◷';
|
|
7
9
|
const BRAILLE_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴'];
|
|
8
10
|
const TODO_PANEL_MAX_ROWS = 12;
|
|
@@ -10,14 +12,29 @@ const TODO_IN_PROGRESS_COLOR = 'ansi256(214)';
|
|
|
10
12
|
const COMMAND_PREVIEW_LINES = 10;
|
|
11
13
|
const WORKING_TOOL_PREVIEW_ROWS = 3;
|
|
12
14
|
const TRANSCRIPT_DIFF_PREVIEW_LINES = 24;
|
|
15
|
+
const APPROVAL_PREVIEW_MAX_ROWS = 20;
|
|
16
|
+
const APPROVAL_PREVIEW_MIN_ROWS = 4;
|
|
17
|
+
const APPROVAL_OVERLAY_CHROME_ROWS = 11;
|
|
18
|
+
const APPROVAL_PADDING_CHROME_ROWS = 4;
|
|
19
|
+
const APPROVAL_PADDING_MIN_ROWS = 30;
|
|
20
|
+
const APPROVAL_SCROLLBAR_COLUMNS = 2;
|
|
21
|
+
const APPROVAL_ACCENT_COLOR = 'cyan';
|
|
22
|
+
const SUDO_PASSWORD_ASSURANCE = 'The model never sees this. Your password goes straight to sudo on this ' +
|
|
23
|
+
'computer, then is discarded — never sent to our servers, saved, or logged.';
|
|
24
|
+
const SUDO_ASSURANCE_COLOR = 'ansi256(248)';
|
|
25
|
+
const APPROVAL_SCROLL_STATUS_COLOR = 'ansi256(248)';
|
|
26
|
+
const APPROVAL_SCROLLBAR_THUMB = '█';
|
|
27
|
+
const APPROVAL_SCROLLBAR_TRACK = '░';
|
|
13
28
|
const THINKING_NOTE_PREVIEW_ROWS = 3;
|
|
14
29
|
const COMPOSER_INPUT_MAX_ROWS = 6;
|
|
15
30
|
const AGENT_MODE_LABEL_WIDTH = 16;
|
|
16
31
|
const OVERLAY_PANEL_MAX_WIDTH = 86;
|
|
17
32
|
const OVERLAY_PANEL_MARGIN_LINES = 2;
|
|
33
|
+
const OVERLAY_PANEL_MARGIN_MIN_ROWS = 30;
|
|
18
34
|
const OVERLAY_BORDER_COLOR = 'yellow';
|
|
35
|
+
const USER_INPUT_BORDER_COLOR = 'cyan';
|
|
19
36
|
const OVERLAY_WARNING_COLOR = 'ansi256(208)';
|
|
20
|
-
const MODEL_PICKER_PANEL_MAX_WIDTH =
|
|
37
|
+
const MODEL_PICKER_PANEL_MAX_WIDTH = 144;
|
|
21
38
|
const MODEL_PICKER_PANEL_MARGIN_LINES = 2;
|
|
22
39
|
const MODEL_PICKER_BORDER_COLOR = 'cyan';
|
|
23
40
|
const MODEL_PICKER_ACCENT_COLOR = 'cyan';
|
|
@@ -71,7 +88,7 @@ function splitComposerInput(input, cursor, width) {
|
|
|
71
88
|
function buildComposerInputLines(input, cursor, promptLabel, placeholder, width) {
|
|
72
89
|
if (!input) {
|
|
73
90
|
return [
|
|
74
|
-
line(span(promptLabel, { color: 'cyan' }), span(' ', { inverse: true }), span(placeholder, { color:
|
|
91
|
+
line(span(promptLabel, { color: 'cyan' }), span(' ', { inverse: true }), span(placeholder, { color: mutedColor() })),
|
|
75
92
|
];
|
|
76
93
|
}
|
|
77
94
|
const labelWidth = promptLabel.length;
|
|
@@ -133,14 +150,14 @@ function diffLinePrefix(kind) {
|
|
|
133
150
|
return ' ';
|
|
134
151
|
}
|
|
135
152
|
}
|
|
136
|
-
function fitLine(content, maxWidth) {
|
|
153
|
+
export function fitLine(content, maxWidth) {
|
|
137
154
|
if (maxWidth <= 0)
|
|
138
155
|
return '';
|
|
139
|
-
if (content
|
|
156
|
+
if (displayWidth(content) <= maxWidth)
|
|
140
157
|
return content;
|
|
141
158
|
if (maxWidth === 1)
|
|
142
159
|
return '…';
|
|
143
|
-
return `${content
|
|
160
|
+
return `${sliceToWidth(content, maxWidth - 1)}…`;
|
|
144
161
|
}
|
|
145
162
|
export function formatPromptDirectoryLabel(projectRoot, homeDir = process.env.HOME ?? '') {
|
|
146
163
|
const trimmed = String(projectRoot ?? '').trim();
|
|
@@ -163,7 +180,9 @@ const CLIENT_SLASH_COMMANDS = [
|
|
|
163
180
|
{ command: '/model', description: 'Switch the active model' },
|
|
164
181
|
{ command: '/resume', description: 'Open the session picker to resume a previous session' },
|
|
165
182
|
{ command: '/jobs', description: 'Background jobs: pick to view output or kill' },
|
|
166
|
-
{ command: '/
|
|
183
|
+
{ command: '/report', description: 'Report a bug: opens the public GitHub issue form' },
|
|
184
|
+
{ command: '/new', description: 'start a new conversation; this session remains saved' },
|
|
185
|
+
{ command: '/logout', description: 'Sign out and quit' },
|
|
167
186
|
{ command: '/exit', description: 'Quit the current session' },
|
|
168
187
|
];
|
|
169
188
|
function buildModelPickerOptions(currentModelId, serverModels) {
|
|
@@ -212,6 +231,9 @@ export function getSlashCommandSuggestions(input) {
|
|
|
212
231
|
function formatModelLabel(modelId, serverModels) {
|
|
213
232
|
return serverModels.find((model) => model.id === modelId)?.label ?? 'Unknown model';
|
|
214
233
|
}
|
|
234
|
+
function pickerModelLabel(modelId, serverModels) {
|
|
235
|
+
return formatModelLabel(modelId, serverModels).replace(/\s*\([^)]*\)\s*$/, '');
|
|
236
|
+
}
|
|
215
237
|
function filterResumeSessions(sessions, filter, serverModels) {
|
|
216
238
|
const q = filter.trim().toLowerCase();
|
|
217
239
|
if (!q)
|
|
@@ -235,26 +257,90 @@ function formatRelativeTime(isoDate) {
|
|
|
235
257
|
return `${hours}h ago`;
|
|
236
258
|
return `${Math.floor(hours / 24)}d ago`;
|
|
237
259
|
}
|
|
238
|
-
|
|
260
|
+
const TODO_IN_PROGRESS_FRAMES = [
|
|
261
|
+
'\u25CB',
|
|
262
|
+
'\u25D4',
|
|
263
|
+
'\u25D1',
|
|
264
|
+
'\u25D5',
|
|
265
|
+
'\u25CF',
|
|
266
|
+
];
|
|
267
|
+
const TODO_IN_PROGRESS_STATIC_GLYPH = '\u25D1';
|
|
268
|
+
export const TODO_IN_PROGRESS_STEP_MS = 1_500;
|
|
269
|
+
export function todoProgressTick(nowMs) {
|
|
270
|
+
return Math.floor(nowMs / TODO_IN_PROGRESS_STEP_MS);
|
|
271
|
+
}
|
|
272
|
+
function todoInProgressGlyph(progressTick) {
|
|
273
|
+
if (progressTick === null)
|
|
274
|
+
return TODO_IN_PROGRESS_STATIC_GLYPH;
|
|
275
|
+
const index = ((progressTick % TODO_IN_PROGRESS_FRAMES.length) +
|
|
276
|
+
TODO_IN_PROGRESS_FRAMES.length) %
|
|
277
|
+
TODO_IN_PROGRESS_FRAMES.length;
|
|
278
|
+
return TODO_IN_PROGRESS_FRAMES[index];
|
|
279
|
+
}
|
|
280
|
+
function todoItemLine(item, width, progressTick = null) {
|
|
239
281
|
const text = fitLine(item.text, Math.max(8, width - 5));
|
|
240
282
|
if (item.status === 'completed') {
|
|
241
|
-
return line(span(' ✔ ', { color: 'green' }), span(text,
|
|
283
|
+
return line(span(' ✔ ', { color: 'green' }), span(text, mutedStyle()));
|
|
242
284
|
}
|
|
243
285
|
if (item.status === 'in_progress') {
|
|
244
|
-
return line(span(
|
|
286
|
+
return line(span(` ${todoInProgressGlyph(progressTick)} `, {
|
|
287
|
+
color: TODO_IN_PROGRESS_COLOR,
|
|
288
|
+
}), span(text, { color: TODO_IN_PROGRESS_COLOR, bold: true }));
|
|
289
|
+
}
|
|
290
|
+
return line(span(' ○ ', { color: mutedColor() }), isDarkTerminalBackground()
|
|
291
|
+
? span(text, { color: mutedColor() })
|
|
292
|
+
: span(text));
|
|
293
|
+
}
|
|
294
|
+
const TURN_MESSAGE_PANEL_MAX_ROWS = 6;
|
|
295
|
+
const TURN_MESSAGE_LABEL = {
|
|
296
|
+
queued: 'queued',
|
|
297
|
+
sending: 'Processing . . .',
|
|
298
|
+
delivered: 'delivered',
|
|
299
|
+
};
|
|
300
|
+
function turnMessageLine(message, width, progressTick) {
|
|
301
|
+
const DECORATION = 6;
|
|
302
|
+
const budget = Math.max(1, width - DECORATION);
|
|
303
|
+
const preferredLabel = Math.max(...Object.values(TURN_MESSAGE_LABEL).map((value) => displayWidth(value)));
|
|
304
|
+
const labelWidth = Math.min(Math.max(1, Math.floor(budget / 2)), Math.max(preferredLabel, displayWidth(message.note ?? '')));
|
|
305
|
+
const label = fitLine(message.note ?? TURN_MESSAGE_LABEL[message.state], labelWidth);
|
|
306
|
+
const textWidth = Math.max(1, budget - labelWidth);
|
|
307
|
+
const text = padToWidth(fitLine(`"${message.text}"`, textWidth), textWidth);
|
|
308
|
+
if (message.state === 'delivered') {
|
|
309
|
+
return line(span(' ● ', { color: 'green' }), span(text, { color: 'gray', dim: true }), span(` ${label}`, { color: 'green' }));
|
|
310
|
+
}
|
|
311
|
+
if (message.state === 'sending') {
|
|
312
|
+
return line(span(` ${todoInProgressGlyph(progressTick)} `, {
|
|
313
|
+
color: TODO_IN_PROGRESS_COLOR,
|
|
314
|
+
}), span(text, { color: TODO_IN_PROGRESS_COLOR, bold: true }), span(` ${label}`, { color: TODO_IN_PROGRESS_COLOR }));
|
|
245
315
|
}
|
|
246
|
-
return line(span(' ○ ', { color: '
|
|
316
|
+
return line(span(' ○ ', { color: 'cyan' }), span(text, { color: 'cyan' }), span(` ${label}`, { color: 'gray' }));
|
|
317
|
+
}
|
|
318
|
+
export function buildTurnMessageLines(messages, width, progressTick = null) {
|
|
319
|
+
if (messages.length === 0)
|
|
320
|
+
return [];
|
|
321
|
+
const lines = [
|
|
322
|
+
line(span('Your messages', { color: 'cyan', bold: true })),
|
|
323
|
+
];
|
|
324
|
+
const visible = messages.slice(-(TURN_MESSAGE_PANEL_MAX_ROWS - 1));
|
|
325
|
+
const hidden = messages.length - visible.length;
|
|
326
|
+
if (hidden > 0) {
|
|
327
|
+
lines.push(line(span(` ● ${hidden} earlier`, { color: 'gray', dim: true })));
|
|
328
|
+
}
|
|
329
|
+
for (const message of visible) {
|
|
330
|
+
lines.push(turnMessageLine(message, width, progressTick));
|
|
331
|
+
}
|
|
332
|
+
return lines;
|
|
247
333
|
}
|
|
248
334
|
export function formatTodoProgress(items) {
|
|
249
335
|
const done = items.filter((item) => item.status === 'completed').length;
|
|
250
336
|
return `${done}/${items.length} done`;
|
|
251
337
|
}
|
|
252
|
-
export function renderTodoListLines(items, width, { header = false } = {}) {
|
|
338
|
+
export function renderTodoListLines(items, width, { header = false, progressTick = null } = {}) {
|
|
253
339
|
if (items.length === 0)
|
|
254
340
|
return [];
|
|
255
341
|
const lines = [];
|
|
256
342
|
if (header) {
|
|
257
|
-
lines.push(line(span('To-dos', { color: 'cyan', bold: true }), span(` · ${formatTodoProgress(items)}`, { color:
|
|
343
|
+
lines.push(line(span('To-dos', { color: 'cyan', bold: true }), span(` · ${formatTodoProgress(items)}`, { color: mutedColor() })));
|
|
258
344
|
}
|
|
259
345
|
const itemRowBudget = TODO_PANEL_MAX_ROWS - (header ? 1 : 0);
|
|
260
346
|
let collapsedDone = 0;
|
|
@@ -268,19 +354,16 @@ export function renderTodoListLines(items, width, { header = false } = {}) {
|
|
|
268
354
|
collapsedDone = Math.min(over, leadingDone);
|
|
269
355
|
}
|
|
270
356
|
if (collapsedDone > 0) {
|
|
271
|
-
lines.push(line(span(' ✔ ', { color: 'green' }), span(`${collapsedDone} completed`,
|
|
357
|
+
lines.push(line(span(' ✔ ', { color: 'green' }), span(`${collapsedDone} completed`, mutedStyle())));
|
|
272
358
|
}
|
|
273
359
|
const remaining = items.slice(collapsedDone);
|
|
274
360
|
const budget = itemRowBudget - (collapsedDone > 0 ? 1 : 0);
|
|
275
361
|
const visible = remaining.length > budget ? remaining.slice(0, budget - 1) : remaining;
|
|
276
362
|
for (const item of visible) {
|
|
277
|
-
lines.push(todoItemLine(item, width));
|
|
363
|
+
lines.push(todoItemLine(item, width, progressTick));
|
|
278
364
|
}
|
|
279
365
|
if (remaining.length > visible.length) {
|
|
280
|
-
lines.push(plainLine(` … +${remaining.length - visible.length} more`,
|
|
281
|
-
color: 'gray',
|
|
282
|
-
dim: true,
|
|
283
|
-
}));
|
|
366
|
+
lines.push(plainLine(` … +${remaining.length - visible.length} more`, mutedStyle()));
|
|
284
367
|
}
|
|
285
368
|
return lines;
|
|
286
369
|
}
|
|
@@ -293,20 +376,45 @@ export function renderTranscriptEntryLines(entry, width) {
|
|
|
293
376
|
? renderPreformattedBodyLines(entry.body, width, entry.kind)
|
|
294
377
|
: renderFormattedBodyLines(entry.body, width, entry.kind)));
|
|
295
378
|
if (entry.diffPreview) {
|
|
296
|
-
lines.push(
|
|
297
|
-
for (const diffLine of entry.diffPreview.lines.slice(0, TRANSCRIPT_DIFF_PREVIEW_LINES)) {
|
|
298
|
-
lines.push(line(span(`${diffLinePrefix(diffLine.kind)} `, { color: diffLineColor(diffLine.kind) }), span(fitLine(diffLine.content || ' ', width - 4), {
|
|
299
|
-
color: diffLineColor(diffLine.kind),
|
|
300
|
-
})));
|
|
301
|
-
}
|
|
379
|
+
lines.push(...renderDiffPreviewLines(entry.diffPreview, width));
|
|
302
380
|
}
|
|
303
381
|
if (entry.todoList && entry.todoList.length > 0) {
|
|
304
382
|
lines.push(...renderTodoListLines(entry.todoList, width));
|
|
305
383
|
}
|
|
306
384
|
return lines;
|
|
307
385
|
}
|
|
386
|
+
const transcriptEntryLineCache = new WeakMap();
|
|
387
|
+
const MAX_CACHED_TRANSCRIPT_WIDTHS = 2;
|
|
388
|
+
function cachedTranscriptEntryLines(entry, width) {
|
|
389
|
+
let byWidth = transcriptEntryLineCache.get(entry);
|
|
390
|
+
if (!byWidth) {
|
|
391
|
+
byWidth = new Map();
|
|
392
|
+
transcriptEntryLineCache.set(entry, byWidth);
|
|
393
|
+
}
|
|
394
|
+
const cached = byWidth.get(width);
|
|
395
|
+
if (cached)
|
|
396
|
+
return cached;
|
|
397
|
+
const lines = renderTranscriptEntryLines(entry, width);
|
|
398
|
+
if (byWidth.size >= MAX_CACHED_TRANSCRIPT_WIDTHS) {
|
|
399
|
+
const oldest = byWidth.keys().next().value;
|
|
400
|
+
if (oldest !== undefined)
|
|
401
|
+
byWidth.delete(oldest);
|
|
402
|
+
}
|
|
403
|
+
byWidth.set(width, lines);
|
|
404
|
+
return lines;
|
|
405
|
+
}
|
|
406
|
+
function renderDiffPreviewLines(preview, width, maxDiffLines = TRANSCRIPT_DIFF_PREVIEW_LINES) {
|
|
407
|
+
return [
|
|
408
|
+
plainLine(` Added ${preview.added} line${preview.added === 1 ? '' : 's'}, removed ${preview.removed} line${preview.removed === 1 ? '' : 's'}`, { color: 'gray' }),
|
|
409
|
+
...preview.lines.slice(0, maxDiffLines).map((diffLine) => line(span(`${diffLinePrefix(diffLine.kind)} `, {
|
|
410
|
+
color: diffLineColor(diffLine.kind),
|
|
411
|
+
}), span(fitLine(diffLine.content || ' ', width - 4), {
|
|
412
|
+
color: diffLineColor(diffLine.kind),
|
|
413
|
+
}))),
|
|
414
|
+
];
|
|
415
|
+
}
|
|
308
416
|
function tokenUsageLines(usage) {
|
|
309
|
-
const match = usage.match(/^Session tokens • in ([^•]+) • out ([^•]+)(?: • think ([^•]+))? • cache ([^•]+)(?: • write ([^•]+))
|
|
417
|
+
const match = usage.match(/^Session tokens • in ([^•]+) • out ([^•]+)(?: • think ([^•]+))? • cache ([^•]+)(?: • write ([^•]+))?$/);
|
|
310
418
|
if (!match) {
|
|
311
419
|
return [plainLine(usage, { color: 'cyan' })];
|
|
312
420
|
}
|
|
@@ -324,9 +432,6 @@ function tokenUsageLines(usage) {
|
|
|
324
432
|
if (match[5]) {
|
|
325
433
|
spans.push(span(' Write ', { color: 'gray' }), span(match[5].trim(), { color: 'yellow' }));
|
|
326
434
|
}
|
|
327
|
-
if (match[6]) {
|
|
328
|
-
spans.push(span(' Index ', { color: 'gray' }), span(match[6].trim(), { color: 'blue' }));
|
|
329
|
-
}
|
|
330
435
|
return [line(...spans)];
|
|
331
436
|
}
|
|
332
437
|
function footerTransientStatus(status) {
|
|
@@ -405,20 +510,26 @@ function composerFooterLines(state) {
|
|
|
405
510
|
}));
|
|
406
511
|
return lines;
|
|
407
512
|
}
|
|
408
|
-
const
|
|
409
|
-
?
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
513
|
+
const busyHelperText = state.queuedMessage
|
|
514
|
+
? 'Enter sends it to the agent • ↑ edit • Esc / Ctrl+C discard'
|
|
515
|
+
: state.input
|
|
516
|
+
? 'Enter queues • Esc cancels turn • Ctrl+C clears draft'
|
|
517
|
+
: 'Enter queues • Esc / Ctrl+C cancel turn';
|
|
518
|
+
const helperText = state.userInputPrompt
|
|
519
|
+
? 'Answering agent questions • Ctrl+C cancels turn'
|
|
520
|
+
: state.busy
|
|
521
|
+
? busyHelperText
|
|
522
|
+
: process.platform === 'win32'
|
|
523
|
+
? 'Enter sends • Shift+Tab mode • Alt+V image • Esc cancel turn • Ctrl+C clears / quits'
|
|
524
|
+
: process.platform === 'darwin'
|
|
525
|
+
? 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C clears / quits'
|
|
526
|
+
: 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C clears / quits';
|
|
417
527
|
const agentLabel = agentModeLabel(state.agentMode).padEnd(AGENT_MODE_LABEL_WIDTH);
|
|
418
528
|
const tokenUsageText = state.tokenUsage || formatClientTokenUsage(null);
|
|
529
|
+
const footerMuted = mutedStyle();
|
|
419
530
|
const footerSpans = [
|
|
420
|
-
span(helperText,
|
|
421
|
-
span(' '.repeat(Math.max(1, 8)),
|
|
531
|
+
span(helperText, footerMuted),
|
|
532
|
+
span(' '.repeat(Math.max(1, 8)), footerMuted),
|
|
422
533
|
span(agentLabel, {
|
|
423
534
|
color: state.agentMode === 'plan' ? 'yellow' : 'cyan',
|
|
424
535
|
}),
|
|
@@ -506,17 +617,48 @@ function buildJobsPickerLines(state, width, nowMs) {
|
|
|
506
617
|
}));
|
|
507
618
|
return lines;
|
|
508
619
|
}
|
|
620
|
+
export const THINKING_FALLBACK_PHRASES = [
|
|
621
|
+
'Working out where to start...',
|
|
622
|
+
'Deciding what comes first...',
|
|
623
|
+
'Lining up the pieces...',
|
|
624
|
+
'Untangling the details...',
|
|
625
|
+
'Deciding what not to touch...',
|
|
626
|
+
'Checking what this would break...',
|
|
627
|
+
'Choosing the smaller change...',
|
|
628
|
+
'Picking the least clever option...',
|
|
629
|
+
'Resisting the obvious answer...',
|
|
630
|
+
'Trying the boring explanation first...',
|
|
631
|
+
'Checking whether the assumption holds...',
|
|
632
|
+
'Asking what would have to be true...',
|
|
633
|
+
'Reading it the way the machine would...',
|
|
634
|
+
'Testing the story against the code...',
|
|
635
|
+
'Working out what actually changed...',
|
|
636
|
+
'Finding the smallest thing that explains it...',
|
|
637
|
+
'Looking for the part that is not settled yet...',
|
|
638
|
+
'Making sure this is the simple version...',
|
|
639
|
+
];
|
|
640
|
+
export function pickThinkingFallbackPhrase() {
|
|
641
|
+
const index = Math.floor(Math.random() * THINKING_FALLBACK_PHRASES.length);
|
|
642
|
+
return THINKING_FALLBACK_PHRASES[index];
|
|
643
|
+
}
|
|
644
|
+
function withProgressEllipsis(title) {
|
|
645
|
+
const text = title.trim();
|
|
646
|
+
if (!text)
|
|
647
|
+
return text;
|
|
648
|
+
return /(?:\.\.\.|…)$/.test(text) ? text : `${text}...`;
|
|
649
|
+
}
|
|
509
650
|
function thinkingHeaderLine(spinnerFrame, title, width) {
|
|
510
651
|
const spinner = `${BRAILLE_SPINNER_FRAMES[spinnerFrame % BRAILLE_SPINNER_FRAMES.length]} `;
|
|
511
|
-
const
|
|
512
|
-
|
|
652
|
+
const decorated = withProgressEllipsis(title);
|
|
653
|
+
const fittedTitle = decorated
|
|
654
|
+
? fitLine(decorated, Math.max(8, width - spinner.length - 'Thinking'.length - 3))
|
|
513
655
|
: '';
|
|
514
656
|
return line(span(spinner, { color: 'green' }), span('Thinking', { color: 'green', bold: true }), ...(fittedTitle ? [span(` · ${fittedTitle}`, { color: 'ansi256(248)' })] : []));
|
|
515
657
|
}
|
|
516
658
|
function thinkingNoteLine(note, width) {
|
|
517
659
|
return line(span('│ ', { color: 'green' }), span(fitLine(note, Math.max(8, width - 3)), { color: 'ansi256(248)' }));
|
|
518
660
|
}
|
|
519
|
-
function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
661
|
+
function buildLiveLines(state, width, spinnerFrame, elapsedSeconds, nowMs) {
|
|
520
662
|
if (!state.busy)
|
|
521
663
|
return [];
|
|
522
664
|
const lines = [plainLine('')];
|
|
@@ -546,11 +688,19 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
546
688
|
}
|
|
547
689
|
lines.push(plainLine(''));
|
|
548
690
|
}
|
|
549
|
-
lines.push(plainLine(
|
|
691
|
+
lines.push(plainLine(buildWorkingClockLine(state, elapsedSeconds), { color: 'yellow' }));
|
|
692
|
+
const turnMessages = state.turnMessages ?? [];
|
|
693
|
+
if (turnMessages.length > 0) {
|
|
694
|
+
lines.push(plainLine(''));
|
|
695
|
+
lines.push(...buildTurnMessageLines(turnMessages, width, todoProgressTick(nowMs)));
|
|
696
|
+
}
|
|
550
697
|
const todos = state.todos ?? [];
|
|
551
698
|
if (todos.length > 0) {
|
|
552
699
|
lines.push(plainLine(''));
|
|
553
|
-
lines.push(...renderTodoListLines(todos, width, {
|
|
700
|
+
lines.push(...renderTodoListLines(todos, width, {
|
|
701
|
+
header: true,
|
|
702
|
+
progressTick: todoProgressTick(nowMs),
|
|
703
|
+
}));
|
|
554
704
|
}
|
|
555
705
|
const visibleTitle = state.thinkingTitle.trim();
|
|
556
706
|
const visibleNotes = state.thinkingNotes.filter(Boolean);
|
|
@@ -560,11 +710,12 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
560
710
|
const minimalText = visibleTitle && visibleTitle !== 'Thinking'
|
|
561
711
|
? visibleTitle
|
|
562
712
|
: (visibleNotes[visibleNotes.length - 1] ?? '');
|
|
563
|
-
lines.push(thinkingHeaderLine(spinnerFrame, minimalText, width));
|
|
713
|
+
lines.push(thinkingHeaderLine(spinnerFrame, minimalText || THINKING_FALLBACK_PHRASES[0], width));
|
|
564
714
|
}
|
|
565
715
|
else {
|
|
566
|
-
|
|
567
|
-
|
|
716
|
+
const headerTitle = visibleTitle && visibleTitle !== 'Thinking' ? visibleTitle : '';
|
|
717
|
+
lines.push(thinkingHeaderLine(spinnerFrame, headerTitle || THINKING_FALLBACK_PHRASES[0], width));
|
|
718
|
+
for (const note of visibleNotes.slice(0, THINKING_NOTE_PREVIEW_ROWS)) {
|
|
568
719
|
lines.push(thinkingNoteLine(note, width));
|
|
569
720
|
}
|
|
570
721
|
}
|
|
@@ -573,16 +724,30 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
573
724
|
return lines;
|
|
574
725
|
}
|
|
575
726
|
function lineCharCount(row) {
|
|
576
|
-
return row.spans.reduce((total, item) => total +
|
|
727
|
+
return row.spans.reduce((total, item) => total + displayWidth(item.text), 0);
|
|
577
728
|
}
|
|
578
729
|
function overlayPanelLine(row, width, color) {
|
|
579
730
|
const padding = Math.max(0, width - lineCharCount(row));
|
|
580
731
|
return line(span('│ ', { color }), ...row.spans, span(' '.repeat(padding)), span(' │', { color }));
|
|
581
732
|
}
|
|
582
|
-
function
|
|
733
|
+
function overlayPanelMarginLineCount(height) {
|
|
734
|
+
return height < OVERLAY_PANEL_MARGIN_MIN_ROWS
|
|
735
|
+
? 0
|
|
736
|
+
: OVERLAY_PANEL_MARGIN_LINES;
|
|
737
|
+
}
|
|
738
|
+
function overlayPanelContentBudget(height) {
|
|
739
|
+
if (!Number.isFinite(height)) {
|
|
740
|
+
return Number.POSITIVE_INFINITY;
|
|
741
|
+
}
|
|
742
|
+
const margins = overlayPanelMarginLineCount(height) * 2;
|
|
743
|
+
return Math.max(1, Math.floor(height) - margins - 2);
|
|
744
|
+
}
|
|
745
|
+
function buildOverlayPanel(rows, width, color, height = Number.POSITIVE_INFINITY) {
|
|
583
746
|
const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
|
|
584
747
|
const innerWidth = Math.max(1, panelWidth - 4);
|
|
585
|
-
const margin = Array.from({
|
|
748
|
+
const margin = Array.from({
|
|
749
|
+
length: overlayPanelMarginLineCount(height),
|
|
750
|
+
}, () => plainLine(''));
|
|
586
751
|
return [
|
|
587
752
|
...margin,
|
|
588
753
|
plainLine(`╭${'─'.repeat(panelWidth - 2)}╮`, { color }),
|
|
@@ -603,7 +768,6 @@ function modelPickerPanelSideLine(content, innerWidth) {
|
|
|
603
768
|
}
|
|
604
769
|
const MODEL_PICKER_COST_WIDTH = 6;
|
|
605
770
|
const MODEL_PICKER_MODEL_WIDTH = 42;
|
|
606
|
-
const MODEL_PICKER_NUMBER_WIDTH = 4;
|
|
607
771
|
const MODEL_PICKER_SEPARATOR = ' │ ';
|
|
608
772
|
const MODEL_PICKER_WIDE_MIN_WIDTH = 78;
|
|
609
773
|
function modelPickerTopBorder(panelWidth) {
|
|
@@ -630,7 +794,6 @@ function modelPickerCostText(rating) {
|
|
|
630
794
|
}
|
|
631
795
|
function modelPickerNotesWidth(innerWidth) {
|
|
632
796
|
return Math.max(18, innerWidth -
|
|
633
|
-
MODEL_PICKER_NUMBER_WIDTH -
|
|
634
797
|
MODEL_PICKER_MODEL_WIDTH -
|
|
635
798
|
MODEL_PICKER_COST_WIDTH -
|
|
636
799
|
MODEL_PICKER_SEPARATOR.length * 2);
|
|
@@ -653,12 +816,10 @@ function modelPickerItemLines(option, selected, innerWidth) {
|
|
|
653
816
|
: selected
|
|
654
817
|
? { color: 'cyan', bold: true }
|
|
655
818
|
: {};
|
|
656
|
-
const numberCell = modelPickerCell(String(option.publicId), MODEL_PICKER_NUMBER_WIDTH, { color: 'cyan', bold: selected, ...selectedStyle });
|
|
657
819
|
const cost = modelPickerCostText(option.costRating);
|
|
658
820
|
if (innerWidth < MODEL_PICKER_WIDE_MIN_WIDTH) {
|
|
659
|
-
const modelWidth = Math.max(12, innerWidth -
|
|
821
|
+
const modelWidth = Math.max(12, innerWidth - MODEL_PICKER_COST_WIDTH - 2);
|
|
660
822
|
const row = [
|
|
661
|
-
numberCell,
|
|
662
823
|
...modelPickerModelSpans(option, selected, modelWidth, false, labelStyle, selectedStyle),
|
|
663
824
|
span(' ', selectedStyle),
|
|
664
825
|
modelPickerCell(cost, MODEL_PICKER_COST_WIDTH, selectedStyle),
|
|
@@ -668,7 +829,6 @@ function modelPickerItemLines(option, selected, innerWidth) {
|
|
|
668
829
|
];
|
|
669
830
|
}
|
|
670
831
|
const row = [
|
|
671
|
-
numberCell,
|
|
672
832
|
...modelPickerModelSpans(option, selected, MODEL_PICKER_MODEL_WIDTH, true, labelStyle, selectedStyle),
|
|
673
833
|
span(MODEL_PICKER_SEPARATOR, { color: 'gray', ...selectedStyle }),
|
|
674
834
|
modelPickerCell(cost, MODEL_PICKER_COST_WIDTH, selectedStyle),
|
|
@@ -685,14 +845,19 @@ function modelPickerItemLines(option, selected, innerWidth) {
|
|
|
685
845
|
function modelPickerHeaderLine(innerWidth) {
|
|
686
846
|
const heading = { color: MODEL_PICKER_ACCENT_COLOR, bold: true };
|
|
687
847
|
if (innerWidth < MODEL_PICKER_WIDE_MIN_WIDTH) {
|
|
688
|
-
return line(modelPickerCell('
|
|
848
|
+
return line(modelPickerCell('Model', Math.max(1, innerWidth), heading));
|
|
689
849
|
}
|
|
690
|
-
return line(modelPickerCell('
|
|
850
|
+
return line(modelPickerCell('Model', MODEL_PICKER_MODEL_WIDTH, heading), span(MODEL_PICKER_SEPARATOR, { color: 'gray' }), modelPickerCell('Cost', MODEL_PICKER_COST_WIDTH, heading), span(MODEL_PICKER_SEPARATOR, { color: 'gray' }), modelPickerCell('Notes', modelPickerNotesWidth(innerWidth), heading));
|
|
691
851
|
}
|
|
692
|
-
function buildModelPickerPanel(options, selectedIndex, width) {
|
|
852
|
+
function buildModelPickerPanel(options, selectedIndex, width, availableHeight) {
|
|
693
853
|
const panelWidth = Math.max(28, Math.min(width, MODEL_PICKER_PANEL_MAX_WIDTH));
|
|
694
854
|
const innerWidth = Math.max(1, panelWidth - 4);
|
|
695
|
-
const
|
|
855
|
+
const compactBodyLineCount = options.length + 6;
|
|
856
|
+
const spacerLineCount = Math.max(0, options.length - 1);
|
|
857
|
+
const useRowSpacing = compactBodyLineCount + spacerLineCount <= availableHeight;
|
|
858
|
+
const bodyLineCount = compactBodyLineCount + (useRowSpacing ? spacerLineCount : 0);
|
|
859
|
+
const marginLineCount = Math.max(0, Math.min(MODEL_PICKER_PANEL_MARGIN_LINES, Math.floor((availableHeight - bodyLineCount) / 2)));
|
|
860
|
+
const margin = Array.from({ length: marginLineCount }, () => plainLine(''));
|
|
696
861
|
const body = [
|
|
697
862
|
modelPickerTopBorder(panelWidth),
|
|
698
863
|
modelPickerPanelSideLine(modelPickerHeaderLine(innerWidth), innerWidth),
|
|
@@ -700,6 +865,9 @@ function buildModelPickerPanel(options, selectedIndex, width) {
|
|
|
700
865
|
];
|
|
701
866
|
options.forEach((option, index) => {
|
|
702
867
|
body.push(...modelPickerItemLines(option, index === selectedIndex, innerWidth));
|
|
868
|
+
if (useRowSpacing && index < options.length - 1) {
|
|
869
|
+
body.push(modelPickerPanelSideLine(plainLine(''), innerWidth));
|
|
870
|
+
}
|
|
703
871
|
});
|
|
704
872
|
const fullHint = '↑/↓ navigate • Enter select • Esc cancel';
|
|
705
873
|
const compactHint = '↑/↓ • enter • esc';
|
|
@@ -774,10 +942,105 @@ function buildCommandPalettePanel(suggestions, selectedIndex, width) {
|
|
|
774
942
|
body.push(modelPickerPanelSideLine(plainLine(''), innerWidth), modelPickerPanelSideLine(line(span('─'.repeat(innerWidth), { color: MODEL_PICKER_BORDER_COLOR })), innerWidth), modelPickerPanelSideLine(plainLine('↑/↓ choose • Tab or Enter accept • Esc cancel', { color: 'gray' }), innerWidth), plainLine(`╰${'─'.repeat(panelWidth - 2)}╯`, { color: MODEL_PICKER_BORDER_COLOR }));
|
|
775
943
|
return [...margin, ...body, ...margin];
|
|
776
944
|
}
|
|
777
|
-
function
|
|
945
|
+
export function formatElapsedClock(elapsedSeconds) {
|
|
946
|
+
if (elapsedSeconds < 60)
|
|
947
|
+
return `${elapsedSeconds}s`;
|
|
948
|
+
return `${Math.floor(elapsedSeconds / 60)}m ${String(elapsedSeconds % 60).padStart(2, '0')}s`;
|
|
949
|
+
}
|
|
950
|
+
export function buildWorkingClockLine(state, elapsedSeconds) {
|
|
951
|
+
const elapsed = formatElapsedClock(elapsedSeconds);
|
|
952
|
+
if (state.busyPausedAt != null) {
|
|
953
|
+
return `${WORKING_CLOCK_ICON} Paused · ${elapsed} · waiting for your response`;
|
|
954
|
+
}
|
|
955
|
+
const label = state.generatingImage
|
|
956
|
+
? 'Generating image'
|
|
957
|
+
: state.analyzingImages > 0
|
|
958
|
+
? state.analyzingImages > 1
|
|
959
|
+
? 'Analyzing images'
|
|
960
|
+
: 'Analyzing image'
|
|
961
|
+
: 'Working';
|
|
962
|
+
return `${WORKING_CLOCK_ICON} ${label} · ${elapsed}`;
|
|
963
|
+
}
|
|
964
|
+
export function approvalPanelInnerWidth(width) {
|
|
965
|
+
return Math.max(1, Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH)) - 4);
|
|
966
|
+
}
|
|
967
|
+
export function approvalPaddingEnabled(height) {
|
|
968
|
+
return height >= APPROVAL_PADDING_MIN_ROWS;
|
|
969
|
+
}
|
|
970
|
+
export function approvalPreviewBudget(height) {
|
|
971
|
+
const chrome = APPROVAL_OVERLAY_CHROME_ROWS +
|
|
972
|
+
(approvalPaddingEnabled(height) ? APPROVAL_PADDING_CHROME_ROWS : 0);
|
|
973
|
+
return Math.max(APPROVAL_PREVIEW_MIN_ROWS, Math.min(APPROVAL_PREVIEW_MAX_ROWS, Math.floor(height / 2) - chrome));
|
|
974
|
+
}
|
|
975
|
+
export function approvalPreviewRows(prompt, innerWidth) {
|
|
976
|
+
const previewWidth = Math.max(8, innerWidth - APPROVAL_SCROLLBAR_COLUMNS);
|
|
977
|
+
if (prompt.diffPreview) {
|
|
978
|
+
return renderDiffPreviewLines(prompt.diffPreview, previewWidth, prompt.diffPreview.lines.length);
|
|
979
|
+
}
|
|
980
|
+
return wrapText(prompt.body, previewWidth).map((text) => plainLine(text, { color: OVERLAY_BORDER_COLOR }));
|
|
981
|
+
}
|
|
982
|
+
export function approvalScrollLimit(prompt, width, height) {
|
|
983
|
+
if (!prompt)
|
|
984
|
+
return 0;
|
|
985
|
+
return Math.max(0, approvalPreviewRows(prompt, approvalPanelInnerWidth(width)).length -
|
|
986
|
+
approvalPreviewBudget(height));
|
|
987
|
+
}
|
|
988
|
+
function approvalScrollbarGlyphs(totalRows, visibleRows, offset) {
|
|
989
|
+
const thumbRows = Math.max(1, Math.min(visibleRows, Math.round((visibleRows * visibleRows) / totalRows)));
|
|
990
|
+
const maxOffset = totalRows - visibleRows;
|
|
991
|
+
const thumbTop = maxOffset <= 0
|
|
992
|
+
? 0
|
|
993
|
+
: Math.round((offset / maxOffset) * (visibleRows - thumbRows));
|
|
994
|
+
return Array.from({ length: visibleRows }, (_, index) => index >= thumbTop && index < thumbTop + thumbRows
|
|
995
|
+
? APPROVAL_SCROLLBAR_THUMB
|
|
996
|
+
: APPROVAL_SCROLLBAR_TRACK);
|
|
997
|
+
}
|
|
998
|
+
function withScrollbarGlyph(target, glyph, column) {
|
|
999
|
+
const padding = Math.max(1, column - lineCharCount(target));
|
|
1000
|
+
return {
|
|
1001
|
+
spans: [
|
|
1002
|
+
...target.spans,
|
|
1003
|
+
span(' '.repeat(padding)),
|
|
1004
|
+
span(glyph, {
|
|
1005
|
+
color: 'gray',
|
|
1006
|
+
dim: glyph === APPROVAL_SCROLLBAR_TRACK,
|
|
1007
|
+
}),
|
|
1008
|
+
],
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
export function buildApprovalPreviewWindow(prompt, innerWidth, height, requestedOffset) {
|
|
1012
|
+
const rows = approvalPreviewRows(prompt, innerWidth);
|
|
1013
|
+
const budget = approvalPreviewBudget(height);
|
|
1014
|
+
if (rows.length <= budget) {
|
|
1015
|
+
return {
|
|
1016
|
+
firstVisibleRow: rows.length === 0 ? 0 : 1,
|
|
1017
|
+
lastVisibleRow: rows.length,
|
|
1018
|
+
lines: rows,
|
|
1019
|
+
offset: 0,
|
|
1020
|
+
totalRows: rows.length,
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
const offset = Math.max(0, Math.min(requestedOffset, rows.length - budget));
|
|
1024
|
+
const visible = rows.slice(offset, offset + budget);
|
|
1025
|
+
const glyphs = approvalScrollbarGlyphs(rows.length, budget, offset);
|
|
1026
|
+
return {
|
|
1027
|
+
firstVisibleRow: offset + 1,
|
|
1028
|
+
lastVisibleRow: offset + visible.length,
|
|
1029
|
+
lines: visible.map((target, index) => withScrollbarGlyph(target, glyphs[index], innerWidth - 1)),
|
|
1030
|
+
offset,
|
|
1031
|
+
totalRows: rows.length,
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
export function approvalScrollStatusLine(window) {
|
|
1035
|
+
return `lines ${window.firstVisibleRow}–${window.lastVisibleRow} of ${window.totalRows} · PgUp/PgDn scrolls`;
|
|
1036
|
+
}
|
|
1037
|
+
export function rightAlignedLine(text, width, style = {}) {
|
|
1038
|
+
return plainLine(`${' '.repeat(Math.max(0, width - displayWidth(text)))}${text}`, style);
|
|
1039
|
+
}
|
|
1040
|
+
function buildOverlayLines(state, width, height, nowMs) {
|
|
778
1041
|
const lines = [];
|
|
779
1042
|
const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
|
|
780
|
-
const innerWidth =
|
|
1043
|
+
const innerWidth = approvalPanelInnerWidth(width);
|
|
781
1044
|
if (state.sudoPrompt) {
|
|
782
1045
|
const prompt = state.sudoPrompt;
|
|
783
1046
|
const passwordWidth = Math.max(0, innerWidth - 11);
|
|
@@ -797,75 +1060,117 @@ function buildOverlayLines(state, width, nowMs) {
|
|
|
797
1060
|
: line(span(' '), span(text, { color: OVERLAY_BORDER_COLOR })));
|
|
798
1061
|
});
|
|
799
1062
|
lines.push(plainLine(''));
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
}
|
|
1063
|
+
lines.push(...wrapText(SUDO_PASSWORD_ASSURANCE, innerWidth).map((text) => plainLine(text, { color: SUDO_ASSURANCE_COLOR })));
|
|
1064
|
+
lines.push(plainLine(''));
|
|
803
1065
|
lines.push(line(span('Password: ', { color: 'cyan', bold: true }), span('•'.repeat(Math.min(prompt.passwordLength, passwordWidth)), {
|
|
804
1066
|
color: 'cyan',
|
|
805
1067
|
}), span(' ', { inverse: true })));
|
|
806
1068
|
lines.push(plainLine('Press Enter to submit, Escape to cancel', { color: 'gray' }));
|
|
807
|
-
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR);
|
|
1069
|
+
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR, height);
|
|
1070
|
+
}
|
|
1071
|
+
if (state.userInputPrompt) {
|
|
1072
|
+
if (height < 3) {
|
|
1073
|
+
return [];
|
|
1074
|
+
}
|
|
1075
|
+
return buildOverlayPanel(buildUserInputOverlayLines(state.userInputPrompt, innerWidth, overlayPanelContentBudget(height)), width, USER_INPUT_BORDER_COLOR, height);
|
|
808
1076
|
}
|
|
809
1077
|
if (state.approvalPrompt) {
|
|
810
1078
|
const prompt = state.approvalPrompt;
|
|
1079
|
+
const padded = approvalPaddingEnabled(height);
|
|
811
1080
|
lines.push(plainLine(prompt.title, {
|
|
812
|
-
color:
|
|
1081
|
+
color: APPROVAL_ACCENT_COLOR,
|
|
813
1082
|
bold: true,
|
|
814
1083
|
}));
|
|
1084
|
+
if (padded)
|
|
1085
|
+
lines.push(plainLine(''));
|
|
815
1086
|
if (prompt.diffPreview && prompt.filePath) {
|
|
816
1087
|
lines.push(line(span('● ', { color: 'green' }), span('Update(', { bold: true }), span(prompt.filePath, { color: 'cyan', bold: true }), span(')', { bold: true })));
|
|
817
|
-
lines.push(...renderTranscriptEntryLines({
|
|
818
|
-
body: '',
|
|
819
|
-
diffPreview: prompt.diffPreview,
|
|
820
|
-
kind: 'diff',
|
|
821
|
-
title: '',
|
|
822
|
-
}, innerWidth));
|
|
823
1088
|
}
|
|
824
|
-
|
|
825
|
-
|
|
1089
|
+
const previewWindow = buildApprovalPreviewWindow(prompt, innerWidth, height, state.approvalScrollOffset ?? 0);
|
|
1090
|
+
lines.push(...previewWindow.lines);
|
|
1091
|
+
if (padded)
|
|
1092
|
+
lines.push(plainLine(''));
|
|
1093
|
+
if (previewWindow.totalRows > previewWindow.lines.length) {
|
|
1094
|
+
lines.push(rightAlignedLine(approvalScrollStatusLine(previewWindow), innerWidth, {
|
|
1095
|
+
color: APPROVAL_SCROLL_STATUS_COLOR,
|
|
1096
|
+
}));
|
|
1097
|
+
if (padded)
|
|
1098
|
+
lines.push(plainLine(''));
|
|
826
1099
|
}
|
|
827
|
-
const options = [
|
|
828
|
-
|
|
829
|
-
{ value: 'a', label: 'Approve all remaining actions', color: 'cyan' },
|
|
830
|
-
{ value: 'n', label: 'Deny', color: 'red' },
|
|
831
|
-
];
|
|
832
|
-
options.forEach((option, index) => {
|
|
1100
|
+
const options = (prompt.options ?? []).map((option) => option.label);
|
|
1101
|
+
options.forEach((label, index) => {
|
|
833
1102
|
const selected = index === state.approvalCursor;
|
|
834
1103
|
lines.push(line(span(selected ? '› ' : ' ', {
|
|
835
|
-
color: selected ?
|
|
836
|
-
}), span(
|
|
837
|
-
color: selected ?
|
|
838
|
-
bold:
|
|
839
|
-
}), span(` ${option.label}`, {
|
|
840
|
-
color: selected ? OVERLAY_BORDER_COLOR : undefined,
|
|
1104
|
+
color: selected ? APPROVAL_ACCENT_COLOR : 'gray',
|
|
1105
|
+
}), span(label, {
|
|
1106
|
+
color: selected ? APPROVAL_ACCENT_COLOR : undefined,
|
|
1107
|
+
bold: selected,
|
|
841
1108
|
})));
|
|
842
1109
|
});
|
|
843
|
-
|
|
1110
|
+
if (padded)
|
|
1111
|
+
lines.push(plainLine(''));
|
|
1112
|
+
lines.push(plainLine('↑/↓ moves • Enter confirms • Esc denies', {
|
|
844
1113
|
color: 'gray',
|
|
845
1114
|
}));
|
|
846
|
-
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR);
|
|
1115
|
+
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR, height);
|
|
847
1116
|
}
|
|
848
1117
|
if (state.modelPickerOpen) {
|
|
849
1118
|
const options = buildModelPickerOptions(state.currentModelId, state.serverModels);
|
|
850
|
-
lines.push(...buildModelPickerPanel(options, state.modelPickerIndex, width));
|
|
1119
|
+
lines.push(...buildModelPickerPanel(options, state.modelPickerIndex, width, height));
|
|
851
1120
|
}
|
|
852
1121
|
if (state.jobsPickerOpen) {
|
|
853
1122
|
lines.push(...buildJobsPickerLines(state, width, nowMs));
|
|
854
1123
|
}
|
|
855
1124
|
if (state.resumePickerOpen) {
|
|
856
1125
|
const filtered = filterResumeSessions(state.resumePickerSessions, state.resumePickerFilter, state.serverModels);
|
|
1126
|
+
const pickerWidth = Math.max(20, width - 2);
|
|
1127
|
+
const divider = () => plainLine('─'.repeat(pickerWidth), { color: 'gray', dim: true });
|
|
857
1128
|
lines.push(plainLine('Resume a previous session', { color: 'cyan', bold: true }));
|
|
858
1129
|
lines.push(line(span('Search: ', { color: 'gray' }), span(state.resumePickerFilter, {}), span('█', { color: 'gray' })));
|
|
859
|
-
|
|
1130
|
+
lines.push(divider());
|
|
1131
|
+
const maxCards = Math.max(2, Math.min(filtered.length, Math.floor((height - 10) / 3)));
|
|
1132
|
+
let start = 0;
|
|
1133
|
+
if (filtered.length > maxCards) {
|
|
1134
|
+
start = Math.min(Math.max(0, state.resumePickerIndex - Math.floor(maxCards / 2)), filtered.length - maxCards);
|
|
1135
|
+
}
|
|
1136
|
+
const visible = filtered.slice(start, start + maxCards);
|
|
1137
|
+
if (start > 0) {
|
|
1138
|
+
lines.push(plainLine(` … ${start} newer`, { color: 'gray', dim: true }));
|
|
1139
|
+
}
|
|
1140
|
+
for (const [offset, session] of visible.entries()) {
|
|
1141
|
+
const index = start + offset;
|
|
860
1142
|
const selected = index === state.resumePickerIndex;
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
|
|
1143
|
+
const prompt = singleLinePreview(session.lastUserMessage, pickerWidth) ||
|
|
1144
|
+
session.name ||
|
|
1145
|
+
`Session ${session.id}`;
|
|
1146
|
+
lines.push(line(span(selected ? '› ' : ' ', { color: 'cyan' }), span(fitLine(prompt, Math.max(10, pickerWidth - 2)), selected
|
|
1147
|
+
? { color: 'cyan', bold: true }
|
|
1148
|
+
: session.lastUserMessage
|
|
1149
|
+
? {}
|
|
1150
|
+
: { color: 'gray', dim: true })));
|
|
1151
|
+
const meta = [
|
|
1152
|
+
formatRelativeTime(session.updatedAt),
|
|
1153
|
+
pickerModelLabel(session.modelId, state.serverModels),
|
|
1154
|
+
session.branch ?? null,
|
|
1155
|
+
]
|
|
1156
|
+
.filter(Boolean)
|
|
1157
|
+
.join(' · ');
|
|
1158
|
+
lines.push(line(span(' '), span(fitLine(meta, Math.max(10, pickerWidth - 4)), {
|
|
1159
|
+
color: selected ? 'cyan' : 'gray',
|
|
1160
|
+
dim: !selected,
|
|
1161
|
+
})));
|
|
1162
|
+
if (offset < visible.length - 1)
|
|
1163
|
+
lines.push(plainLine(''));
|
|
1164
|
+
}
|
|
1165
|
+
const remaining = filtered.length - (start + visible.length);
|
|
1166
|
+
if (remaining > 0) {
|
|
1167
|
+
lines.push(plainLine(` … ${remaining} older`, { color: 'gray', dim: true }));
|
|
864
1168
|
}
|
|
865
1169
|
if (filtered.length === 0) {
|
|
866
1170
|
lines.push(plainLine('No sessions match.', { color: 'gray' }));
|
|
867
1171
|
}
|
|
868
|
-
lines.push(
|
|
1172
|
+
lines.push(divider());
|
|
1173
|
+
lines.push(plainLine('↑/↓ move · enter resume · esc start new · ctrl+c quit', {
|
|
869
1174
|
color: 'gray',
|
|
870
1175
|
}));
|
|
871
1176
|
}
|
|
@@ -884,6 +1189,15 @@ function buildOverlayLines(state, width, nowMs) {
|
|
|
884
1189
|
function countSectionLines(sections) {
|
|
885
1190
|
return sections.reduce((sum, section) => sum + section.lines.length, 0);
|
|
886
1191
|
}
|
|
1192
|
+
export function userInputViewportForFrame(state, cols, rows, spinnerFrame, elapsedSeconds, nowMs = 0) {
|
|
1193
|
+
const contentWidth = Math.max(20, Math.floor(cols * 0.95) - 2);
|
|
1194
|
+
const liveRows = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds, nowMs).length;
|
|
1195
|
+
const overlayHeight = Math.max(0, rows - liveRows - composerFooterLines(state).length);
|
|
1196
|
+
return {
|
|
1197
|
+
width: approvalPanelInnerWidth(contentWidth),
|
|
1198
|
+
maxRows: overlayPanelContentBudget(overlayHeight),
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
887
1201
|
function sliceTranscriptLines(lines, maxLines, scrollOffset) {
|
|
888
1202
|
if (maxLines <= 0 || lines.length <= maxLines) {
|
|
889
1203
|
return lines;
|
|
@@ -896,28 +1210,34 @@ function sliceTranscriptLines(lines, maxLines, scrollOffset) {
|
|
|
896
1210
|
export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, nowMs = 0) {
|
|
897
1211
|
const contentWidth = Math.max(20, Math.floor(cols * 0.95) - 2);
|
|
898
1212
|
const gutter = Math.max(Math.floor((cols - contentWidth) / 2), 0);
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1213
|
+
const buildTranscriptLines = (wrapWidth) => {
|
|
1214
|
+
const blocks = state.transcript.map((entry) => cachedTranscriptEntryLines(entry, wrapWidth));
|
|
1215
|
+
const lines = [];
|
|
1216
|
+
blocks.forEach((block, index) => {
|
|
1217
|
+
if (index > 0) {
|
|
1218
|
+
lines.push(plainLine(''));
|
|
1219
|
+
}
|
|
1220
|
+
lines.push(...block);
|
|
1221
|
+
});
|
|
1222
|
+
return lines;
|
|
1223
|
+
};
|
|
1224
|
+
let transcriptLines = buildTranscriptLines(contentWidth);
|
|
907
1225
|
const sections = [];
|
|
908
|
-
const liveLines = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds);
|
|
1226
|
+
const liveLines = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds, nowMs);
|
|
909
1227
|
if (liveLines.length > 0) {
|
|
910
1228
|
sections.push({ kind: 'live', lines: liveLines });
|
|
911
1229
|
}
|
|
912
|
-
const overlayActive = Boolean(state.approvalPrompt || state.sudoPrompt);
|
|
1230
|
+
const overlayActive = Boolean(state.approvalPrompt || state.sudoPrompt || state.userInputPrompt);
|
|
913
1231
|
if (!state.resumePickerOpen && !state.modelPickerOpen && !state.jobsPickerOpen && !overlayActive) {
|
|
914
1232
|
const composerLines = [];
|
|
915
|
-
if (state.
|
|
916
|
-
|
|
1233
|
+
if (state.busy && state.status === 'Starting a new conversation...') {
|
|
1234
|
+
composerLines.push(line(span('Starting a new conversation…', { color: 'gray', dim: true })));
|
|
1235
|
+
}
|
|
1236
|
+
else if (state.queuedMessage) {
|
|
917
1237
|
const imageCount = state.queuedMessage.imageAttachments.length;
|
|
918
|
-
composerLines.push(line(span(
|
|
919
|
-
?
|
|
920
|
-
:
|
|
1238
|
+
composerLines.push(line(span('Queued', { color: 'cyan', bold: true }), span(' Enter', { color: 'cyan', bold: true }), span(imageCount > 0
|
|
1239
|
+
? ' sends it with the next prompt'
|
|
1240
|
+
: ' sends it to the agent now', { color: 'gray' }), span(' · ↑', { color: 'cyan', bold: true }), span(' edit', { color: 'gray' }), span(' · Esc', { color: 'cyan', bold: true }), span(' discard', { color: 'gray' })));
|
|
921
1241
|
}
|
|
922
1242
|
else {
|
|
923
1243
|
const promptLabel = state.busy ? 'queue> ' : '❯ ';
|
|
@@ -931,14 +1251,32 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
|
|
|
931
1251
|
lines: [...composerLines, plainLine(''), ...composerFooterLines(state)],
|
|
932
1252
|
});
|
|
933
1253
|
}
|
|
934
|
-
|
|
1254
|
+
if (state.userInputPrompt) {
|
|
1255
|
+
sections.push({
|
|
1256
|
+
kind: 'busyFooter',
|
|
1257
|
+
lines: composerFooterLines(state),
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
const overlayHeight = state.userInputPrompt
|
|
1261
|
+
? Math.max(0, rows - countSectionLines(sections))
|
|
1262
|
+
: rows;
|
|
1263
|
+
const overlayLines = buildOverlayLines(state, contentWidth, overlayHeight, nowMs);
|
|
935
1264
|
if (overlayLines.length > 0) {
|
|
936
1265
|
sections.push({ kind: 'overlay', lines: overlayLines });
|
|
937
1266
|
}
|
|
938
1267
|
const reservedLines = countSectionLines(sections.filter((section) => section.kind !== 'transcript'));
|
|
939
|
-
const composerReserve = state.resumePickerOpen
|
|
1268
|
+
const composerReserve = state.resumePickerOpen ||
|
|
1269
|
+
state.approvalPrompt ||
|
|
1270
|
+
state.sudoPrompt ||
|
|
1271
|
+
state.userInputPrompt
|
|
1272
|
+
? 0
|
|
1273
|
+
: 4;
|
|
940
1274
|
const transcriptBudget = Math.max(1, rows - reservedLines - composerReserve - 1);
|
|
941
|
-
|
|
1275
|
+
let transcriptScrollLimit = Math.max(0, transcriptLines.length - transcriptBudget);
|
|
1276
|
+
if (transcriptScrollLimit > 0 && contentWidth > 2) {
|
|
1277
|
+
transcriptLines = buildTranscriptLines(contentWidth - 2);
|
|
1278
|
+
transcriptScrollLimit = Math.max(0, transcriptLines.length - transcriptBudget);
|
|
1279
|
+
}
|
|
942
1280
|
const transcriptScrollOffset = Math.min(Math.max(state.transcriptScrollOffset, 0), transcriptScrollLimit);
|
|
943
1281
|
sections.unshift({
|
|
944
1282
|
kind: 'transcript',
|