@thegitai/cli 1.0.0-preview.2 → 1.0.0-preview.20
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 +32 -4
- package/dist/bin/ai.js +57 -291
- package/dist/src/agent-mode.js +1 -1
- package/dist/src/api/auth.js +2 -2
- package/dist/src/api/browser-login.js +72 -3
- package/dist/src/api/chat.js +232 -33
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/api/http.js +16 -3
- package/dist/src/api/models.js +9 -4
- package/dist/src/executor.js +1 -1
- package/dist/src/help-text.js +51 -11
- package/dist/src/permissions.js +243 -0
- package/dist/src/project-index.js +13 -1
- package/dist/src/session-store.js +57 -20
- package/dist/src/session.js +14 -3
- package/dist/src/tool-executor.js +2 -2
- package/dist/src/tools/delete-file.js +14 -0
- package/dist/src/tools/patch-file.js +12 -16
- package/dist/src/tools/replace-document-text.js +28 -18
- package/dist/src/tools/run-command.js +13 -27
- package/dist/src/tools/run-node-script.js +11 -26
- package/dist/src/tools/str-replace.js +12 -16
- package/dist/src/tools/write-file.js +66 -0
- 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 +569 -151
- package/dist/src/ui/tui/bridge.js +10 -0
- package/dist/src/ui/tui/build-frame.js +535 -159
- package/dist/src/ui/tui/markdown-render.js +81 -73
- package/dist/src/ui/tui/shell-input.js +155 -45
- 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 +18 -6
- package/dist/src/markdown-renderer.js +0 -112
|
@@ -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,16 +12,31 @@ 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
|
-
const MODEL_PICKER_BORDER_COLOR = '
|
|
39
|
+
const MODEL_PICKER_BORDER_COLOR = 'cyan';
|
|
23
40
|
const MODEL_PICKER_ACCENT_COLOR = 'cyan';
|
|
24
41
|
const MODEL_PICKER_HIGHLIGHT_BG = 'ansi256(87)';
|
|
25
42
|
const MODEL_PICKER_META_INDENT = ' ';
|
|
@@ -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,15 +180,18 @@ 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' },
|
|
167
184
|
{ command: '/exit', description: 'Quit the current session' },
|
|
168
185
|
];
|
|
169
186
|
function buildModelPickerOptions(currentModelId, serverModels) {
|
|
170
187
|
return serverModels.map((model) => ({
|
|
171
188
|
id: model.id,
|
|
172
189
|
label: model.label,
|
|
173
|
-
|
|
190
|
+
publicId: model.id,
|
|
191
|
+
costRating: model.costRating,
|
|
192
|
+
current: model.id === currentModelId,
|
|
174
193
|
disabled: false,
|
|
194
|
+
note: model.description,
|
|
175
195
|
}));
|
|
176
196
|
}
|
|
177
197
|
function getInputCommandToken(input) {
|
|
@@ -209,6 +229,9 @@ export function getSlashCommandSuggestions(input) {
|
|
|
209
229
|
function formatModelLabel(modelId, serverModels) {
|
|
210
230
|
return serverModels.find((model) => model.id === modelId)?.label ?? 'Unknown model';
|
|
211
231
|
}
|
|
232
|
+
function pickerModelLabel(modelId, serverModels) {
|
|
233
|
+
return formatModelLabel(modelId, serverModels).replace(/\s*\([^)]*\)\s*$/, '');
|
|
234
|
+
}
|
|
212
235
|
function filterResumeSessions(sessions, filter, serverModels) {
|
|
213
236
|
const q = filter.trim().toLowerCase();
|
|
214
237
|
if (!q)
|
|
@@ -232,26 +255,90 @@ function formatRelativeTime(isoDate) {
|
|
|
232
255
|
return `${hours}h ago`;
|
|
233
256
|
return `${Math.floor(hours / 24)}d ago`;
|
|
234
257
|
}
|
|
235
|
-
|
|
258
|
+
const TODO_IN_PROGRESS_FRAMES = [
|
|
259
|
+
'\u25CB',
|
|
260
|
+
'\u25D4',
|
|
261
|
+
'\u25D1',
|
|
262
|
+
'\u25D5',
|
|
263
|
+
'\u25CF',
|
|
264
|
+
];
|
|
265
|
+
const TODO_IN_PROGRESS_STATIC_GLYPH = '\u25D1';
|
|
266
|
+
export const TODO_IN_PROGRESS_STEP_MS = 1_500;
|
|
267
|
+
export function todoProgressTick(nowMs) {
|
|
268
|
+
return Math.floor(nowMs / TODO_IN_PROGRESS_STEP_MS);
|
|
269
|
+
}
|
|
270
|
+
function todoInProgressGlyph(progressTick) {
|
|
271
|
+
if (progressTick === null)
|
|
272
|
+
return TODO_IN_PROGRESS_STATIC_GLYPH;
|
|
273
|
+
const index = ((progressTick % TODO_IN_PROGRESS_FRAMES.length) +
|
|
274
|
+
TODO_IN_PROGRESS_FRAMES.length) %
|
|
275
|
+
TODO_IN_PROGRESS_FRAMES.length;
|
|
276
|
+
return TODO_IN_PROGRESS_FRAMES[index];
|
|
277
|
+
}
|
|
278
|
+
function todoItemLine(item, width, progressTick = null) {
|
|
236
279
|
const text = fitLine(item.text, Math.max(8, width - 5));
|
|
237
280
|
if (item.status === 'completed') {
|
|
238
|
-
return line(span(' ✔ ', { color: 'green' }), span(text,
|
|
281
|
+
return line(span(' ✔ ', { color: 'green' }), span(text, mutedStyle()));
|
|
239
282
|
}
|
|
240
283
|
if (item.status === 'in_progress') {
|
|
241
|
-
return line(span(
|
|
284
|
+
return line(span(` ${todoInProgressGlyph(progressTick)} `, {
|
|
285
|
+
color: TODO_IN_PROGRESS_COLOR,
|
|
286
|
+
}), span(text, { color: TODO_IN_PROGRESS_COLOR, bold: true }));
|
|
287
|
+
}
|
|
288
|
+
return line(span(' ○ ', { color: mutedColor() }), isDarkTerminalBackground()
|
|
289
|
+
? span(text, { color: mutedColor() })
|
|
290
|
+
: span(text));
|
|
291
|
+
}
|
|
292
|
+
const TURN_MESSAGE_PANEL_MAX_ROWS = 6;
|
|
293
|
+
const TURN_MESSAGE_LABEL = {
|
|
294
|
+
queued: 'queued',
|
|
295
|
+
sending: 'Processing . . .',
|
|
296
|
+
delivered: 'delivered',
|
|
297
|
+
};
|
|
298
|
+
function turnMessageLine(message, width, progressTick) {
|
|
299
|
+
const DECORATION = 6;
|
|
300
|
+
const budget = Math.max(1, width - DECORATION);
|
|
301
|
+
const preferredLabel = Math.max(...Object.values(TURN_MESSAGE_LABEL).map((value) => displayWidth(value)));
|
|
302
|
+
const labelWidth = Math.min(Math.max(1, Math.floor(budget / 2)), Math.max(preferredLabel, displayWidth(message.note ?? '')));
|
|
303
|
+
const label = fitLine(message.note ?? TURN_MESSAGE_LABEL[message.state], labelWidth);
|
|
304
|
+
const textWidth = Math.max(1, budget - labelWidth);
|
|
305
|
+
const text = padToWidth(fitLine(`"${message.text}"`, textWidth), textWidth);
|
|
306
|
+
if (message.state === 'delivered') {
|
|
307
|
+
return line(span(' ● ', { color: 'green' }), span(text, { color: 'gray', dim: true }), span(` ${label}`, { color: 'green' }));
|
|
308
|
+
}
|
|
309
|
+
if (message.state === 'sending') {
|
|
310
|
+
return line(span(` ${todoInProgressGlyph(progressTick)} `, {
|
|
311
|
+
color: TODO_IN_PROGRESS_COLOR,
|
|
312
|
+
}), span(text, { color: TODO_IN_PROGRESS_COLOR, bold: true }), span(` ${label}`, { color: TODO_IN_PROGRESS_COLOR }));
|
|
242
313
|
}
|
|
243
|
-
return line(span(' ○ ', { color: '
|
|
314
|
+
return line(span(' ○ ', { color: 'cyan' }), span(text, { color: 'cyan' }), span(` ${label}`, { color: 'gray' }));
|
|
315
|
+
}
|
|
316
|
+
export function buildTurnMessageLines(messages, width, progressTick = null) {
|
|
317
|
+
if (messages.length === 0)
|
|
318
|
+
return [];
|
|
319
|
+
const lines = [
|
|
320
|
+
line(span('Your messages', { color: 'cyan', bold: true })),
|
|
321
|
+
];
|
|
322
|
+
const visible = messages.slice(-(TURN_MESSAGE_PANEL_MAX_ROWS - 1));
|
|
323
|
+
const hidden = messages.length - visible.length;
|
|
324
|
+
if (hidden > 0) {
|
|
325
|
+
lines.push(line(span(` ● ${hidden} earlier`, { color: 'gray', dim: true })));
|
|
326
|
+
}
|
|
327
|
+
for (const message of visible) {
|
|
328
|
+
lines.push(turnMessageLine(message, width, progressTick));
|
|
329
|
+
}
|
|
330
|
+
return lines;
|
|
244
331
|
}
|
|
245
332
|
export function formatTodoProgress(items) {
|
|
246
333
|
const done = items.filter((item) => item.status === 'completed').length;
|
|
247
334
|
return `${done}/${items.length} done`;
|
|
248
335
|
}
|
|
249
|
-
export function renderTodoListLines(items, width, { header = false } = {}) {
|
|
336
|
+
export function renderTodoListLines(items, width, { header = false, progressTick = null } = {}) {
|
|
250
337
|
if (items.length === 0)
|
|
251
338
|
return [];
|
|
252
339
|
const lines = [];
|
|
253
340
|
if (header) {
|
|
254
|
-
lines.push(line(span('To-dos', { color: 'cyan', bold: true }), span(` · ${formatTodoProgress(items)}`, { color:
|
|
341
|
+
lines.push(line(span('To-dos', { color: 'cyan', bold: true }), span(` · ${formatTodoProgress(items)}`, { color: mutedColor() })));
|
|
255
342
|
}
|
|
256
343
|
const itemRowBudget = TODO_PANEL_MAX_ROWS - (header ? 1 : 0);
|
|
257
344
|
let collapsedDone = 0;
|
|
@@ -265,19 +352,16 @@ export function renderTodoListLines(items, width, { header = false } = {}) {
|
|
|
265
352
|
collapsedDone = Math.min(over, leadingDone);
|
|
266
353
|
}
|
|
267
354
|
if (collapsedDone > 0) {
|
|
268
|
-
lines.push(line(span(' ✔ ', { color: 'green' }), span(`${collapsedDone} completed`,
|
|
355
|
+
lines.push(line(span(' ✔ ', { color: 'green' }), span(`${collapsedDone} completed`, mutedStyle())));
|
|
269
356
|
}
|
|
270
357
|
const remaining = items.slice(collapsedDone);
|
|
271
358
|
const budget = itemRowBudget - (collapsedDone > 0 ? 1 : 0);
|
|
272
359
|
const visible = remaining.length > budget ? remaining.slice(0, budget - 1) : remaining;
|
|
273
360
|
for (const item of visible) {
|
|
274
|
-
lines.push(todoItemLine(item, width));
|
|
361
|
+
lines.push(todoItemLine(item, width, progressTick));
|
|
275
362
|
}
|
|
276
363
|
if (remaining.length > visible.length) {
|
|
277
|
-
lines.push(plainLine(` … +${remaining.length - visible.length} more`,
|
|
278
|
-
color: 'gray',
|
|
279
|
-
dim: true,
|
|
280
|
-
}));
|
|
364
|
+
lines.push(plainLine(` … +${remaining.length - visible.length} more`, mutedStyle()));
|
|
281
365
|
}
|
|
282
366
|
return lines;
|
|
283
367
|
}
|
|
@@ -290,18 +374,43 @@ export function renderTranscriptEntryLines(entry, width) {
|
|
|
290
374
|
? renderPreformattedBodyLines(entry.body, width, entry.kind)
|
|
291
375
|
: renderFormattedBodyLines(entry.body, width, entry.kind)));
|
|
292
376
|
if (entry.diffPreview) {
|
|
293
|
-
lines.push(
|
|
294
|
-
for (const diffLine of entry.diffPreview.lines.slice(0, TRANSCRIPT_DIFF_PREVIEW_LINES)) {
|
|
295
|
-
lines.push(line(span(`${diffLinePrefix(diffLine.kind)} `, { color: diffLineColor(diffLine.kind) }), span(fitLine(diffLine.content || ' ', width - 4), {
|
|
296
|
-
color: diffLineColor(diffLine.kind),
|
|
297
|
-
})));
|
|
298
|
-
}
|
|
377
|
+
lines.push(...renderDiffPreviewLines(entry.diffPreview, width));
|
|
299
378
|
}
|
|
300
379
|
if (entry.todoList && entry.todoList.length > 0) {
|
|
301
380
|
lines.push(...renderTodoListLines(entry.todoList, width));
|
|
302
381
|
}
|
|
303
382
|
return lines;
|
|
304
383
|
}
|
|
384
|
+
const transcriptEntryLineCache = new WeakMap();
|
|
385
|
+
const MAX_CACHED_TRANSCRIPT_WIDTHS = 2;
|
|
386
|
+
function cachedTranscriptEntryLines(entry, width) {
|
|
387
|
+
let byWidth = transcriptEntryLineCache.get(entry);
|
|
388
|
+
if (!byWidth) {
|
|
389
|
+
byWidth = new Map();
|
|
390
|
+
transcriptEntryLineCache.set(entry, byWidth);
|
|
391
|
+
}
|
|
392
|
+
const cached = byWidth.get(width);
|
|
393
|
+
if (cached)
|
|
394
|
+
return cached;
|
|
395
|
+
const lines = renderTranscriptEntryLines(entry, width);
|
|
396
|
+
if (byWidth.size >= MAX_CACHED_TRANSCRIPT_WIDTHS) {
|
|
397
|
+
const oldest = byWidth.keys().next().value;
|
|
398
|
+
if (oldest !== undefined)
|
|
399
|
+
byWidth.delete(oldest);
|
|
400
|
+
}
|
|
401
|
+
byWidth.set(width, lines);
|
|
402
|
+
return lines;
|
|
403
|
+
}
|
|
404
|
+
function renderDiffPreviewLines(preview, width, maxDiffLines = TRANSCRIPT_DIFF_PREVIEW_LINES) {
|
|
405
|
+
return [
|
|
406
|
+
plainLine(` Added ${preview.added} line${preview.added === 1 ? '' : 's'}, removed ${preview.removed} line${preview.removed === 1 ? '' : 's'}`, { color: 'gray' }),
|
|
407
|
+
...preview.lines.slice(0, maxDiffLines).map((diffLine) => line(span(`${diffLinePrefix(diffLine.kind)} `, {
|
|
408
|
+
color: diffLineColor(diffLine.kind),
|
|
409
|
+
}), span(fitLine(diffLine.content || ' ', width - 4), {
|
|
410
|
+
color: diffLineColor(diffLine.kind),
|
|
411
|
+
}))),
|
|
412
|
+
];
|
|
413
|
+
}
|
|
305
414
|
function tokenUsageLines(usage) {
|
|
306
415
|
const match = usage.match(/^Session tokens • in ([^•]+) • out ([^•]+)(?: • think ([^•]+))? • cache ([^•]+)(?: • write ([^•]+))?(?: • index ([^•]+))?$/);
|
|
307
416
|
if (!match) {
|
|
@@ -402,20 +511,26 @@ function composerFooterLines(state) {
|
|
|
402
511
|
}));
|
|
403
512
|
return lines;
|
|
404
513
|
}
|
|
405
|
-
const
|
|
406
|
-
?
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
514
|
+
const busyHelperText = state.queuedMessage
|
|
515
|
+
? 'Enter sends it to the agent • ↑ edit • Esc / Ctrl+C discard'
|
|
516
|
+
: state.input
|
|
517
|
+
? 'Enter queues • Esc cancels turn • Ctrl+C clears draft'
|
|
518
|
+
: 'Enter queues • Esc / Ctrl+C cancel turn';
|
|
519
|
+
const helperText = state.userInputPrompt
|
|
520
|
+
? 'Answering agent questions • Ctrl+C cancels turn'
|
|
521
|
+
: state.busy
|
|
522
|
+
? busyHelperText
|
|
523
|
+
: process.platform === 'win32'
|
|
524
|
+
? 'Enter sends • Shift+Tab mode • Alt+V image • Esc cancel turn • Ctrl+C clears / quits'
|
|
525
|
+
: process.platform === 'darwin'
|
|
526
|
+
? 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C clears / quits'
|
|
527
|
+
: 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C clears / quits';
|
|
414
528
|
const agentLabel = agentModeLabel(state.agentMode).padEnd(AGENT_MODE_LABEL_WIDTH);
|
|
415
529
|
const tokenUsageText = state.tokenUsage || formatClientTokenUsage(null);
|
|
530
|
+
const footerMuted = mutedStyle();
|
|
416
531
|
const footerSpans = [
|
|
417
|
-
span(helperText,
|
|
418
|
-
span(' '.repeat(Math.max(1, 8)),
|
|
532
|
+
span(helperText, footerMuted),
|
|
533
|
+
span(' '.repeat(Math.max(1, 8)), footerMuted),
|
|
419
534
|
span(agentLabel, {
|
|
420
535
|
color: state.agentMode === 'plan' ? 'yellow' : 'cyan',
|
|
421
536
|
}),
|
|
@@ -503,17 +618,48 @@ function buildJobsPickerLines(state, width, nowMs) {
|
|
|
503
618
|
}));
|
|
504
619
|
return lines;
|
|
505
620
|
}
|
|
621
|
+
export const THINKING_FALLBACK_PHRASES = [
|
|
622
|
+
'Working out where to start...',
|
|
623
|
+
'Deciding what comes first...',
|
|
624
|
+
'Lining up the pieces...',
|
|
625
|
+
'Untangling the details...',
|
|
626
|
+
'Deciding what not to touch...',
|
|
627
|
+
'Checking what this would break...',
|
|
628
|
+
'Choosing the smaller change...',
|
|
629
|
+
'Picking the least clever option...',
|
|
630
|
+
'Resisting the obvious answer...',
|
|
631
|
+
'Trying the boring explanation first...',
|
|
632
|
+
'Checking whether the assumption holds...',
|
|
633
|
+
'Asking what would have to be true...',
|
|
634
|
+
'Reading it the way the machine would...',
|
|
635
|
+
'Testing the story against the code...',
|
|
636
|
+
'Working out what actually changed...',
|
|
637
|
+
'Finding the smallest thing that explains it...',
|
|
638
|
+
'Looking for the part that is not settled yet...',
|
|
639
|
+
'Making sure this is the simple version...',
|
|
640
|
+
];
|
|
641
|
+
export function pickThinkingFallbackPhrase() {
|
|
642
|
+
const index = Math.floor(Math.random() * THINKING_FALLBACK_PHRASES.length);
|
|
643
|
+
return THINKING_FALLBACK_PHRASES[index];
|
|
644
|
+
}
|
|
645
|
+
function withProgressEllipsis(title) {
|
|
646
|
+
const text = title.trim();
|
|
647
|
+
if (!text)
|
|
648
|
+
return text;
|
|
649
|
+
return /(?:\.\.\.|…)$/.test(text) ? text : `${text}...`;
|
|
650
|
+
}
|
|
506
651
|
function thinkingHeaderLine(spinnerFrame, title, width) {
|
|
507
652
|
const spinner = `${BRAILLE_SPINNER_FRAMES[spinnerFrame % BRAILLE_SPINNER_FRAMES.length]} `;
|
|
508
|
-
const
|
|
509
|
-
|
|
653
|
+
const decorated = withProgressEllipsis(title);
|
|
654
|
+
const fittedTitle = decorated
|
|
655
|
+
? fitLine(decorated, Math.max(8, width - spinner.length - 'Thinking'.length - 3))
|
|
510
656
|
: '';
|
|
511
657
|
return line(span(spinner, { color: 'green' }), span('Thinking', { color: 'green', bold: true }), ...(fittedTitle ? [span(` · ${fittedTitle}`, { color: 'ansi256(248)' })] : []));
|
|
512
658
|
}
|
|
513
659
|
function thinkingNoteLine(note, width) {
|
|
514
660
|
return line(span('│ ', { color: 'green' }), span(fitLine(note, Math.max(8, width - 3)), { color: 'ansi256(248)' }));
|
|
515
661
|
}
|
|
516
|
-
function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
662
|
+
function buildLiveLines(state, width, spinnerFrame, elapsedSeconds, nowMs) {
|
|
517
663
|
if (!state.busy)
|
|
518
664
|
return [];
|
|
519
665
|
const lines = [plainLine('')];
|
|
@@ -543,11 +689,19 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
543
689
|
}
|
|
544
690
|
lines.push(plainLine(''));
|
|
545
691
|
}
|
|
546
|
-
lines.push(plainLine(
|
|
692
|
+
lines.push(plainLine(buildWorkingClockLine(state, elapsedSeconds), { color: 'yellow' }));
|
|
693
|
+
const turnMessages = state.turnMessages ?? [];
|
|
694
|
+
if (turnMessages.length > 0) {
|
|
695
|
+
lines.push(plainLine(''));
|
|
696
|
+
lines.push(...buildTurnMessageLines(turnMessages, width, todoProgressTick(nowMs)));
|
|
697
|
+
}
|
|
547
698
|
const todos = state.todos ?? [];
|
|
548
699
|
if (todos.length > 0) {
|
|
549
700
|
lines.push(plainLine(''));
|
|
550
|
-
lines.push(...renderTodoListLines(todos, width, {
|
|
701
|
+
lines.push(...renderTodoListLines(todos, width, {
|
|
702
|
+
header: true,
|
|
703
|
+
progressTick: todoProgressTick(nowMs),
|
|
704
|
+
}));
|
|
551
705
|
}
|
|
552
706
|
const visibleTitle = state.thinkingTitle.trim();
|
|
553
707
|
const visibleNotes = state.thinkingNotes.filter(Boolean);
|
|
@@ -557,11 +711,12 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
557
711
|
const minimalText = visibleTitle && visibleTitle !== 'Thinking'
|
|
558
712
|
? visibleTitle
|
|
559
713
|
: (visibleNotes[visibleNotes.length - 1] ?? '');
|
|
560
|
-
lines.push(thinkingHeaderLine(spinnerFrame, minimalText, width));
|
|
714
|
+
lines.push(thinkingHeaderLine(spinnerFrame, minimalText || THINKING_FALLBACK_PHRASES[0], width));
|
|
561
715
|
}
|
|
562
716
|
else {
|
|
563
|
-
|
|
564
|
-
|
|
717
|
+
const headerTitle = visibleTitle && visibleTitle !== 'Thinking' ? visibleTitle : '';
|
|
718
|
+
lines.push(thinkingHeaderLine(spinnerFrame, headerTitle || THINKING_FALLBACK_PHRASES[0], width));
|
|
719
|
+
for (const note of visibleNotes.slice(0, THINKING_NOTE_PREVIEW_ROWS)) {
|
|
565
720
|
lines.push(thinkingNoteLine(note, width));
|
|
566
721
|
}
|
|
567
722
|
}
|
|
@@ -570,16 +725,30 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
570
725
|
return lines;
|
|
571
726
|
}
|
|
572
727
|
function lineCharCount(row) {
|
|
573
|
-
return row.spans.reduce((total, item) => total +
|
|
728
|
+
return row.spans.reduce((total, item) => total + displayWidth(item.text), 0);
|
|
574
729
|
}
|
|
575
730
|
function overlayPanelLine(row, width, color) {
|
|
576
731
|
const padding = Math.max(0, width - lineCharCount(row));
|
|
577
732
|
return line(span('│ ', { color }), ...row.spans, span(' '.repeat(padding)), span(' │', { color }));
|
|
578
733
|
}
|
|
579
|
-
function
|
|
734
|
+
function overlayPanelMarginLineCount(height) {
|
|
735
|
+
return height < OVERLAY_PANEL_MARGIN_MIN_ROWS
|
|
736
|
+
? 0
|
|
737
|
+
: OVERLAY_PANEL_MARGIN_LINES;
|
|
738
|
+
}
|
|
739
|
+
function overlayPanelContentBudget(height) {
|
|
740
|
+
if (!Number.isFinite(height)) {
|
|
741
|
+
return Number.POSITIVE_INFINITY;
|
|
742
|
+
}
|
|
743
|
+
const margins = overlayPanelMarginLineCount(height) * 2;
|
|
744
|
+
return Math.max(1, Math.floor(height) - margins - 2);
|
|
745
|
+
}
|
|
746
|
+
function buildOverlayPanel(rows, width, color, height = Number.POSITIVE_INFINITY) {
|
|
580
747
|
const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
|
|
581
748
|
const innerWidth = Math.max(1, panelWidth - 4);
|
|
582
|
-
const margin = Array.from({
|
|
749
|
+
const margin = Array.from({
|
|
750
|
+
length: overlayPanelMarginLineCount(height),
|
|
751
|
+
}, () => plainLine(''));
|
|
583
752
|
return [
|
|
584
753
|
...margin,
|
|
585
754
|
plainLine(`╭${'─'.repeat(panelWidth - 2)}╮`, { color }),
|
|
@@ -598,77 +767,113 @@ function padSpansToInnerWidth(spans, innerWidth, fill) {
|
|
|
598
767
|
function modelPickerPanelSideLine(content, innerWidth) {
|
|
599
768
|
return overlayPanelLine(content, innerWidth, MODEL_PICKER_BORDER_COLOR);
|
|
600
769
|
}
|
|
770
|
+
const MODEL_PICKER_COST_WIDTH = 6;
|
|
771
|
+
const MODEL_PICKER_MODEL_WIDTH = 42;
|
|
772
|
+
const MODEL_PICKER_SEPARATOR = ' │ ';
|
|
773
|
+
const MODEL_PICKER_WIDE_MIN_WIDTH = 78;
|
|
601
774
|
function modelPickerTopBorder(panelWidth) {
|
|
602
|
-
const
|
|
603
|
-
const
|
|
604
|
-
const
|
|
605
|
-
|
|
775
|
+
const fullTitle = ' TheGitAI - Model Selection ';
|
|
776
|
+
const compactTitle = ' Model Selection ';
|
|
777
|
+
const title = panelWidth >= fullTitle.length + 4 ? fullTitle : compactTitle;
|
|
778
|
+
const available = Math.max(0, panelWidth - 2 - [...title].length);
|
|
779
|
+
const left = Math.floor(available / 2);
|
|
780
|
+
const right = available - left;
|
|
781
|
+
return line(span(`╭${'─'.repeat(left)}`, { color: MODEL_PICKER_BORDER_COLOR }), span(title, { color: MODEL_PICKER_ACCENT_COLOR, bold: true }), span(`${'─'.repeat(right)}╮`, { color: MODEL_PICKER_BORDER_COLOR }));
|
|
782
|
+
}
|
|
783
|
+
function modelPickerDivider(panelWidth) {
|
|
784
|
+
return plainLine(`├${'─'.repeat(panelWidth - 2)}┤`, {
|
|
785
|
+
color: MODEL_PICKER_BORDER_COLOR,
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
function modelPickerCell(text, width, style = {}) {
|
|
789
|
+
const fitted = fitLine(text, width);
|
|
790
|
+
return span(`${fitted}${' '.repeat(Math.max(0, width - [...fitted].length))}`, style);
|
|
791
|
+
}
|
|
792
|
+
function modelPickerCostText(rating) {
|
|
793
|
+
const steps = Math.max(1, Math.min(3, Math.round(rating)));
|
|
794
|
+
return '$'.repeat(steps);
|
|
795
|
+
}
|
|
796
|
+
function modelPickerNotesWidth(innerWidth) {
|
|
797
|
+
return Math.max(18, innerWidth -
|
|
798
|
+
MODEL_PICKER_MODEL_WIDTH -
|
|
799
|
+
MODEL_PICKER_COST_WIDTH -
|
|
800
|
+
MODEL_PICKER_SEPARATOR.length * 2);
|
|
801
|
+
}
|
|
802
|
+
function modelPickerModelSpans(option, selected, width, showCurrentTag, labelStyle, selectedStyle) {
|
|
803
|
+
const tag = option.current && showCurrentTag ? ' (current)' : '';
|
|
804
|
+
const labelWidth = Math.max(1, width - [...tag].length);
|
|
805
|
+
const label = fitLine(`${selected ? '▶ ' : ' '}${option.label}`, labelWidth);
|
|
806
|
+
const used = [...label].length + [...tag].length;
|
|
807
|
+
return [
|
|
808
|
+
span(label, { ...labelStyle, ...selectedStyle }),
|
|
809
|
+
span(tag, { color: 'gray', ...selectedStyle }),
|
|
810
|
+
span(' '.repeat(Math.max(0, width - used)), selectedStyle),
|
|
811
|
+
];
|
|
606
812
|
}
|
|
607
813
|
function modelPickerItemLines(option, selected, innerWidth) {
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
...highlight,
|
|
626
|
-
}),
|
|
627
|
-
], innerWidth, highlight);
|
|
628
|
-
const lines = [modelPickerPanelSideLine(line(...titleSpans), innerWidth)];
|
|
629
|
-
if (option.meta) {
|
|
630
|
-
const metaSpans = padSpansToInnerWidth([
|
|
631
|
-
span(`${MODEL_PICKER_META_INDENT}${option.meta}`, {
|
|
632
|
-
color: 'gray',
|
|
633
|
-
...highlight,
|
|
634
|
-
}),
|
|
635
|
-
], innerWidth, highlight);
|
|
636
|
-
lines.push(modelPickerPanelSideLine(line(...metaSpans), innerWidth));
|
|
637
|
-
}
|
|
638
|
-
return lines;
|
|
814
|
+
const selectedStyle = selected ? { bgColor: MODEL_PICKER_HIGHLIGHT_BG } : {};
|
|
815
|
+
const labelStyle = option.disabled
|
|
816
|
+
? { color: 'gray' }
|
|
817
|
+
: selected
|
|
818
|
+
? { color: 'cyan', bold: true }
|
|
819
|
+
: {};
|
|
820
|
+
const cost = modelPickerCostText(option.costRating);
|
|
821
|
+
if (innerWidth < MODEL_PICKER_WIDE_MIN_WIDTH) {
|
|
822
|
+
const modelWidth = Math.max(12, innerWidth - MODEL_PICKER_COST_WIDTH - 2);
|
|
823
|
+
const row = [
|
|
824
|
+
...modelPickerModelSpans(option, selected, modelWidth, false, labelStyle, selectedStyle),
|
|
825
|
+
span(' ', selectedStyle),
|
|
826
|
+
modelPickerCell(cost, MODEL_PICKER_COST_WIDTH, selectedStyle),
|
|
827
|
+
];
|
|
828
|
+
return [
|
|
829
|
+
modelPickerPanelSideLine(line(...padSpansToInnerWidth(row, innerWidth, selectedStyle)), innerWidth),
|
|
830
|
+
];
|
|
639
831
|
}
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
832
|
+
const row = [
|
|
833
|
+
...modelPickerModelSpans(option, selected, MODEL_PICKER_MODEL_WIDTH, true, labelStyle, selectedStyle),
|
|
834
|
+
span(MODEL_PICKER_SEPARATOR, { color: 'gray', ...selectedStyle }),
|
|
835
|
+
modelPickerCell(cost, MODEL_PICKER_COST_WIDTH, selectedStyle),
|
|
836
|
+
span(MODEL_PICKER_SEPARATOR, { color: 'gray', ...selectedStyle }),
|
|
837
|
+
modelPickerCell(option.note, modelPickerNotesWidth(innerWidth), {
|
|
838
|
+
color: 'gray',
|
|
839
|
+
...selectedStyle,
|
|
840
|
+
}),
|
|
841
|
+
];
|
|
842
|
+
return [
|
|
843
|
+
modelPickerPanelSideLine(line(...padSpansToInnerWidth(row, innerWidth, selectedStyle)), innerWidth),
|
|
643
844
|
];
|
|
644
|
-
if (option.meta) {
|
|
645
|
-
lines.push(modelPickerPanelSideLine(line(span(`${MODEL_PICKER_META_INDENT}${option.meta}`, { color: 'gray' })), innerWidth));
|
|
646
|
-
}
|
|
647
|
-
return lines;
|
|
648
845
|
}
|
|
649
|
-
function
|
|
650
|
-
|
|
846
|
+
function modelPickerHeaderLine(innerWidth) {
|
|
847
|
+
const heading = { color: MODEL_PICKER_ACCENT_COLOR, bold: true };
|
|
848
|
+
if (innerWidth < MODEL_PICKER_WIDE_MIN_WIDTH) {
|
|
849
|
+
return line(modelPickerCell('Model', Math.max(1, innerWidth), heading));
|
|
850
|
+
}
|
|
851
|
+
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));
|
|
651
852
|
}
|
|
652
|
-
function buildModelPickerPanel(options, selectedIndex, width) {
|
|
853
|
+
function buildModelPickerPanel(options, selectedIndex, width, availableHeight) {
|
|
653
854
|
const panelWidth = Math.max(28, Math.min(width, MODEL_PICKER_PANEL_MAX_WIDTH));
|
|
654
855
|
const innerWidth = Math.max(1, panelWidth - 4);
|
|
655
|
-
const
|
|
856
|
+
const compactBodyLineCount = options.length + 6;
|
|
857
|
+
const spacerLineCount = Math.max(0, options.length - 1);
|
|
858
|
+
const useRowSpacing = compactBodyLineCount + spacerLineCount <= availableHeight;
|
|
859
|
+
const bodyLineCount = compactBodyLineCount + (useRowSpacing ? spacerLineCount : 0);
|
|
860
|
+
const marginLineCount = Math.max(0, Math.min(MODEL_PICKER_PANEL_MARGIN_LINES, Math.floor((availableHeight - bodyLineCount) / 2)));
|
|
861
|
+
const margin = Array.from({ length: marginLineCount }, () => plainLine(''));
|
|
656
862
|
const body = [
|
|
657
|
-
plainLine('TheGitAI - Model Selection', {
|
|
658
|
-
color: MODEL_PICKER_ACCENT_COLOR,
|
|
659
|
-
bold: true,
|
|
660
|
-
}),
|
|
661
|
-
plainLine(''),
|
|
662
863
|
modelPickerTopBorder(panelWidth),
|
|
663
|
-
modelPickerPanelSideLine(
|
|
864
|
+
modelPickerPanelSideLine(modelPickerHeaderLine(innerWidth), innerWidth),
|
|
865
|
+
modelPickerDivider(panelWidth),
|
|
664
866
|
];
|
|
665
867
|
options.forEach((option, index) => {
|
|
666
868
|
body.push(...modelPickerItemLines(option, index === selectedIndex, innerWidth));
|
|
667
|
-
if (index < options.length - 1) {
|
|
668
|
-
body.push(
|
|
869
|
+
if (useRowSpacing && index < options.length - 1) {
|
|
870
|
+
body.push(modelPickerPanelSideLine(plainLine(''), innerWidth));
|
|
669
871
|
}
|
|
670
872
|
});
|
|
671
|
-
|
|
873
|
+
const fullHint = '↑/↓ navigate • Enter select • Esc cancel';
|
|
874
|
+
const compactHint = '↑/↓ • enter • esc';
|
|
875
|
+
const hint = [...fullHint].length <= innerWidth ? fullHint : compactHint;
|
|
876
|
+
body.push(modelPickerDivider(panelWidth), modelPickerPanelSideLine(plainLine(fitLine(hint, innerWidth), { color: 'gray' }), innerWidth), plainLine(`╰${'─'.repeat(panelWidth - 2)}╯`, { color: MODEL_PICKER_BORDER_COLOR }));
|
|
672
877
|
return [...margin, ...body, ...margin];
|
|
673
878
|
}
|
|
674
879
|
function commandPaletteTopBorder(panelWidth) {
|
|
@@ -677,6 +882,9 @@ function commandPaletteTopBorder(panelWidth) {
|
|
|
677
882
|
const dashCount = Math.max(0, panelWidth - prefix.length - suffix.length);
|
|
678
883
|
return line(span('╭─ ', { color: MODEL_PICKER_BORDER_COLOR }), span('Commands', { color: MODEL_PICKER_ACCENT_COLOR, bold: true }), span(` ${'─'.repeat(dashCount)}╮`, { color: MODEL_PICKER_BORDER_COLOR }));
|
|
679
884
|
}
|
|
885
|
+
function commandPaletteSeparatorLine(innerWidth) {
|
|
886
|
+
return modelPickerPanelSideLine(line(span('┈'.repeat(Math.max(1, innerWidth)), { color: 'gray', dim: true })), innerWidth);
|
|
887
|
+
}
|
|
680
888
|
function commandPaletteItemLines(option, selected, innerWidth) {
|
|
681
889
|
const highlight = { bgColor: MODEL_PICKER_HIGHLIGHT_BG };
|
|
682
890
|
if (selected) {
|
|
@@ -729,16 +937,109 @@ function buildCommandPalettePanel(suggestions, selectedIndex, width) {
|
|
|
729
937
|
suggestions.forEach((suggestion, index) => {
|
|
730
938
|
body.push(...commandPaletteItemLines(suggestion, index === selectedIndex, innerWidth));
|
|
731
939
|
if (index < suggestions.length - 1) {
|
|
732
|
-
body.push(
|
|
940
|
+
body.push(commandPaletteSeparatorLine(innerWidth));
|
|
733
941
|
}
|
|
734
942
|
});
|
|
735
943
|
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 }));
|
|
736
944
|
return [...margin, ...body, ...margin];
|
|
737
945
|
}
|
|
738
|
-
function
|
|
946
|
+
export function formatElapsedClock(elapsedSeconds) {
|
|
947
|
+
if (elapsedSeconds < 60)
|
|
948
|
+
return `${elapsedSeconds}s`;
|
|
949
|
+
return `${Math.floor(elapsedSeconds / 60)}m ${String(elapsedSeconds % 60).padStart(2, '0')}s`;
|
|
950
|
+
}
|
|
951
|
+
export function buildWorkingClockLine(state, elapsedSeconds) {
|
|
952
|
+
const elapsed = formatElapsedClock(elapsedSeconds);
|
|
953
|
+
if (state.busyPausedAt != null) {
|
|
954
|
+
return `${WORKING_CLOCK_ICON} Paused · ${elapsed} · waiting for your response`;
|
|
955
|
+
}
|
|
956
|
+
const label = 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) {
|
|
739
1040
|
const lines = [];
|
|
740
1041
|
const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
|
|
741
|
-
const innerWidth =
|
|
1042
|
+
const innerWidth = approvalPanelInnerWidth(width);
|
|
742
1043
|
if (state.sudoPrompt) {
|
|
743
1044
|
const prompt = state.sudoPrompt;
|
|
744
1045
|
const passwordWidth = Math.max(0, innerWidth - 11);
|
|
@@ -758,75 +1059,117 @@ function buildOverlayLines(state, width, nowMs) {
|
|
|
758
1059
|
: line(span(' '), span(text, { color: OVERLAY_BORDER_COLOR })));
|
|
759
1060
|
});
|
|
760
1061
|
lines.push(plainLine(''));
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
}
|
|
1062
|
+
lines.push(...wrapText(SUDO_PASSWORD_ASSURANCE, innerWidth).map((text) => plainLine(text, { color: SUDO_ASSURANCE_COLOR })));
|
|
1063
|
+
lines.push(plainLine(''));
|
|
764
1064
|
lines.push(line(span('Password: ', { color: 'cyan', bold: true }), span('•'.repeat(Math.min(prompt.passwordLength, passwordWidth)), {
|
|
765
1065
|
color: 'cyan',
|
|
766
1066
|
}), span(' ', { inverse: true })));
|
|
767
1067
|
lines.push(plainLine('Press Enter to submit, Escape to cancel', { color: 'gray' }));
|
|
768
|
-
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);
|
|
769
1075
|
}
|
|
770
1076
|
if (state.approvalPrompt) {
|
|
771
1077
|
const prompt = state.approvalPrompt;
|
|
1078
|
+
const padded = approvalPaddingEnabled(height);
|
|
772
1079
|
lines.push(plainLine(prompt.title, {
|
|
773
|
-
color:
|
|
1080
|
+
color: APPROVAL_ACCENT_COLOR,
|
|
774
1081
|
bold: true,
|
|
775
1082
|
}));
|
|
1083
|
+
if (padded)
|
|
1084
|
+
lines.push(plainLine(''));
|
|
776
1085
|
if (prompt.diffPreview && prompt.filePath) {
|
|
777
1086
|
lines.push(line(span('● ', { color: 'green' }), span('Update(', { bold: true }), span(prompt.filePath, { color: 'cyan', bold: true }), span(')', { bold: true })));
|
|
778
|
-
lines.push(...renderTranscriptEntryLines({
|
|
779
|
-
body: '',
|
|
780
|
-
diffPreview: prompt.diffPreview,
|
|
781
|
-
kind: 'diff',
|
|
782
|
-
title: '',
|
|
783
|
-
}, innerWidth));
|
|
784
1087
|
}
|
|
785
|
-
|
|
786
|
-
|
|
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(''));
|
|
787
1098
|
}
|
|
788
|
-
const options = [
|
|
789
|
-
|
|
790
|
-
{ value: 'a', label: 'Approve all remaining actions', color: 'cyan' },
|
|
791
|
-
{ value: 'n', label: 'Deny', color: 'red' },
|
|
792
|
-
];
|
|
793
|
-
options.forEach((option, index) => {
|
|
1099
|
+
const options = (prompt.options ?? []).map((option) => option.label);
|
|
1100
|
+
options.forEach((label, index) => {
|
|
794
1101
|
const selected = index === state.approvalCursor;
|
|
795
1102
|
lines.push(line(span(selected ? '› ' : ' ', {
|
|
796
|
-
color: selected ?
|
|
797
|
-
}), span(
|
|
798
|
-
color: selected ?
|
|
799
|
-
bold:
|
|
800
|
-
}), span(` ${option.label}`, {
|
|
801
|
-
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,
|
|
802
1107
|
})));
|
|
803
1108
|
});
|
|
804
|
-
|
|
1109
|
+
if (padded)
|
|
1110
|
+
lines.push(plainLine(''));
|
|
1111
|
+
lines.push(plainLine('↑/↓ moves • Enter confirms • Esc denies', {
|
|
805
1112
|
color: 'gray',
|
|
806
1113
|
}));
|
|
807
|
-
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR);
|
|
1114
|
+
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR, height);
|
|
808
1115
|
}
|
|
809
1116
|
if (state.modelPickerOpen) {
|
|
810
1117
|
const options = buildModelPickerOptions(state.currentModelId, state.serverModels);
|
|
811
|
-
lines.push(...buildModelPickerPanel(options, state.modelPickerIndex, width));
|
|
1118
|
+
lines.push(...buildModelPickerPanel(options, state.modelPickerIndex, width, height));
|
|
812
1119
|
}
|
|
813
1120
|
if (state.jobsPickerOpen) {
|
|
814
1121
|
lines.push(...buildJobsPickerLines(state, width, nowMs));
|
|
815
1122
|
}
|
|
816
1123
|
if (state.resumePickerOpen) {
|
|
817
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 });
|
|
818
1127
|
lines.push(plainLine('Resume a previous session', { color: 'cyan', bold: true }));
|
|
819
1128
|
lines.push(line(span('Search: ', { color: 'gray' }), span(state.resumePickerFilter, {}), span('█', { color: 'gray' })));
|
|
820
|
-
|
|
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;
|
|
821
1141
|
const selected = index === state.resumePickerIndex;
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
|
|
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 }));
|
|
825
1167
|
}
|
|
826
1168
|
if (filtered.length === 0) {
|
|
827
1169
|
lines.push(plainLine('No sessions match.', { color: 'gray' }));
|
|
828
1170
|
}
|
|
829
|
-
lines.push(
|
|
1171
|
+
lines.push(divider());
|
|
1172
|
+
lines.push(plainLine('↑/↓ move · enter resume · esc start new · ctrl+c quit', {
|
|
830
1173
|
color: 'gray',
|
|
831
1174
|
}));
|
|
832
1175
|
}
|
|
@@ -845,6 +1188,15 @@ function buildOverlayLines(state, width, nowMs) {
|
|
|
845
1188
|
function countSectionLines(sections) {
|
|
846
1189
|
return sections.reduce((sum, section) => sum + section.lines.length, 0);
|
|
847
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
|
+
}
|
|
848
1200
|
function sliceTranscriptLines(lines, maxLines, scrollOffset) {
|
|
849
1201
|
if (maxLines <= 0 || lines.length <= maxLines) {
|
|
850
1202
|
return lines;
|
|
@@ -857,28 +1209,34 @@ function sliceTranscriptLines(lines, maxLines, scrollOffset) {
|
|
|
857
1209
|
export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, nowMs = 0) {
|
|
858
1210
|
const contentWidth = Math.max(20, Math.floor(cols * 0.95) - 2);
|
|
859
1211
|
const gutter = Math.max(Math.floor((cols - contentWidth) / 2), 0);
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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);
|
|
868
1224
|
const sections = [];
|
|
869
|
-
const liveLines = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds);
|
|
1225
|
+
const liveLines = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds, nowMs);
|
|
870
1226
|
if (liveLines.length > 0) {
|
|
871
1227
|
sections.push({ kind: 'live', lines: liveLines });
|
|
872
1228
|
}
|
|
873
|
-
const overlayActive = Boolean(state.approvalPrompt || state.sudoPrompt);
|
|
1229
|
+
const overlayActive = Boolean(state.approvalPrompt || state.sudoPrompt || state.userInputPrompt);
|
|
874
1230
|
if (!state.resumePickerOpen && !state.modelPickerOpen && !state.jobsPickerOpen && !overlayActive) {
|
|
875
1231
|
const composerLines = [];
|
|
876
|
-
if (state.
|
|
877
|
-
|
|
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) {
|
|
878
1236
|
const imageCount = state.queuedMessage.imageAttachments.length;
|
|
879
|
-
composerLines.push(line(span(
|
|
880
|
-
?
|
|
881
|
-
:
|
|
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' })));
|
|
882
1240
|
}
|
|
883
1241
|
else {
|
|
884
1242
|
const promptLabel = state.busy ? 'queue> ' : '❯ ';
|
|
@@ -892,14 +1250,32 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
|
|
|
892
1250
|
lines: [...composerLines, plainLine(''), ...composerFooterLines(state)],
|
|
893
1251
|
});
|
|
894
1252
|
}
|
|
895
|
-
|
|
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);
|
|
896
1263
|
if (overlayLines.length > 0) {
|
|
897
1264
|
sections.push({ kind: 'overlay', lines: overlayLines });
|
|
898
1265
|
}
|
|
899
1266
|
const reservedLines = countSectionLines(sections.filter((section) => section.kind !== 'transcript'));
|
|
900
|
-
const composerReserve = state.resumePickerOpen
|
|
1267
|
+
const composerReserve = state.resumePickerOpen ||
|
|
1268
|
+
state.approvalPrompt ||
|
|
1269
|
+
state.sudoPrompt ||
|
|
1270
|
+
state.userInputPrompt
|
|
1271
|
+
? 0
|
|
1272
|
+
: 4;
|
|
901
1273
|
const transcriptBudget = Math.max(1, rows - reservedLines - composerReserve - 1);
|
|
902
|
-
|
|
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
|
+
}
|
|
903
1279
|
const transcriptScrollOffset = Math.min(Math.max(state.transcriptScrollOffset, 0), transcriptScrollLimit);
|
|
904
1280
|
sections.unshift({
|
|
905
1281
|
kind: 'transcript',
|