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