@thegitai/cli 1.0.0-beta.9 → 1.0.0-preview.10
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 +49 -3
- package/dist/bin/ai.js +83 -197
- package/dist/parsers/NOTICE +18 -0
- package/dist/src/agent-mode.js +5 -0
- package/dist/src/api/auth.js +4 -4
- package/dist/src/api/browser-login.js +72 -19
- package/dist/src/api/chat.js +182 -35
- package/dist/src/api/http.js +65 -4
- package/dist/src/api/models.js +33 -22
- package/dist/src/artifact-policy.js +3 -0
- package/dist/src/background-jobs.js +410 -0
- package/dist/src/cli-args.js +0 -5
- package/dist/src/client-environment.js +2 -0
- package/dist/src/colors.js +50 -0
- package/dist/src/core/clipboard.js +19 -0
- package/dist/src/core/image-path-extractor.js +144 -0
- package/dist/src/executor.js +48 -12
- package/dist/src/help-text.js +30 -13
- package/dist/src/patcher.js +97 -12
- package/dist/src/project-index.js +13 -1
- package/dist/src/project-orientation.js +99 -0
- package/dist/src/scanner.js +50 -12
- package/dist/src/scratch-dir.js +75 -0
- package/dist/src/secret-preview.js +0 -10
- package/dist/src/session-safety.js +0 -19
- package/dist/src/session-store.js +52 -21
- package/dist/src/session.js +8 -0
- package/dist/src/todo-list.js +106 -0
- package/dist/src/tool-executor.js +194 -21
- package/dist/src/tools/delete-file.js +23 -5
- package/dist/src/tools/index.js +6 -0
- package/dist/src/tools/patch-file.js +33 -7
- package/dist/src/tools/path-suggest.js +81 -8
- package/dist/src/tools/read-document.js +2 -2
- package/dist/src/tools/read-file.js +17 -8
- package/dist/src/tools/replace-document-text.js +10 -12
- package/dist/src/tools/restore-checkpoint.js +1 -1
- package/dist/src/tools/run-command.js +109 -24
- package/dist/src/tools/run-node-script.js +27 -5
- package/dist/src/tools/shell-job-kill.js +48 -0
- package/dist/src/tools/shell-job-output.js +51 -0
- package/dist/src/tools/str-replace.js +33 -7
- package/dist/src/tools/undo-edit.js +1 -1
- package/dist/src/tools/update-todos.js +27 -0
- package/dist/src/tools/write-file.js +26 -6
- package/dist/src/tree-sitter-runtime.js +8 -1
- 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 +500 -71
- package/dist/src/ui/tui/bridge.js +3 -4
- package/dist/src/ui/tui/build-frame.js +393 -100
- package/dist/src/ui/tui/markdown-render.js +72 -73
- package/dist/src/ui/tui/shell-input.js +75 -17
- package/dist/src/ui/tui/terminal-title.js +84 -0
- package/dist/src/ui/tui/terminal-writes.js +48 -0
- package/dist/src/ui/tui/text.js +158 -4
- package/dist/src/utils.js +9 -0
- package/dist/src/version.js +0 -6
- package/dist/vendor/web-tree-sitter/LICENSE +21 -0
- package/dist/vendor/web-tree-sitter/NOTICE +13 -0
- package/dist/vendor/web-tree-sitter/web-tree-sitter.cjs +4063 -0
- package/dist/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/package.json +27 -16
- package/dist/src/markdown-renderer.js +0 -112
|
@@ -1,10 +1,12 @@
|
|
|
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, plainLine, sliceToWidth, span, wrapText, } from './text.js';
|
|
6
6
|
const WORKING_CLOCK_ICON = '◷';
|
|
7
7
|
const BRAILLE_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴'];
|
|
8
|
+
const TODO_PANEL_MAX_ROWS = 12;
|
|
9
|
+
const TODO_IN_PROGRESS_COLOR = 'ansi256(214)';
|
|
8
10
|
const COMMAND_PREVIEW_LINES = 10;
|
|
9
11
|
const WORKING_TOOL_PREVIEW_ROWS = 3;
|
|
10
12
|
const TRANSCRIPT_DIFF_PREVIEW_LINES = 24;
|
|
@@ -15,9 +17,9 @@ const OVERLAY_PANEL_MAX_WIDTH = 86;
|
|
|
15
17
|
const OVERLAY_PANEL_MARGIN_LINES = 2;
|
|
16
18
|
const OVERLAY_BORDER_COLOR = 'yellow';
|
|
17
19
|
const OVERLAY_WARNING_COLOR = 'ansi256(208)';
|
|
18
|
-
const MODEL_PICKER_PANEL_MAX_WIDTH =
|
|
20
|
+
const MODEL_PICKER_PANEL_MAX_WIDTH = 144;
|
|
19
21
|
const MODEL_PICKER_PANEL_MARGIN_LINES = 2;
|
|
20
|
-
const MODEL_PICKER_BORDER_COLOR = '
|
|
22
|
+
const MODEL_PICKER_BORDER_COLOR = 'cyan';
|
|
21
23
|
const MODEL_PICKER_ACCENT_COLOR = 'cyan';
|
|
22
24
|
const MODEL_PICKER_HIGHLIGHT_BG = 'ansi256(87)';
|
|
23
25
|
const MODEL_PICKER_META_INDENT = ' ';
|
|
@@ -131,6 +133,15 @@ function diffLinePrefix(kind) {
|
|
|
131
133
|
return ' ';
|
|
132
134
|
}
|
|
133
135
|
}
|
|
136
|
+
export function fitLine(content, maxWidth) {
|
|
137
|
+
if (maxWidth <= 0)
|
|
138
|
+
return '';
|
|
139
|
+
if (displayWidth(content) <= maxWidth)
|
|
140
|
+
return content;
|
|
141
|
+
if (maxWidth === 1)
|
|
142
|
+
return '…';
|
|
143
|
+
return `${sliceToWidth(content, maxWidth - 1)}…`;
|
|
144
|
+
}
|
|
134
145
|
export function formatPromptDirectoryLabel(projectRoot, homeDir = process.env.HOME ?? '') {
|
|
135
146
|
const trimmed = String(projectRoot ?? '').trim();
|
|
136
147
|
if (!trimmed)
|
|
@@ -151,21 +162,28 @@ const CLIENT_SLASH_COMMANDS = [
|
|
|
151
162
|
{ command: '/usage', description: 'Show account usage percentage and reset times' },
|
|
152
163
|
{ command: '/model', description: 'Switch the active model' },
|
|
153
164
|
{ command: '/resume', description: 'Open the session picker to resume a previous session' },
|
|
154
|
-
{ command: '/
|
|
165
|
+
{ command: '/jobs', description: 'Background jobs: pick to view output or kill' },
|
|
166
|
+
{ command: '/new', description: 'start a new conversation; this session remains saved' },
|
|
155
167
|
{ command: '/exit', description: 'Quit the current session' },
|
|
156
168
|
];
|
|
157
169
|
function buildModelPickerOptions(currentModelId, serverModels) {
|
|
158
170
|
return serverModels.map((model) => ({
|
|
159
171
|
id: model.id,
|
|
160
172
|
label: model.label,
|
|
161
|
-
|
|
173
|
+
publicId: model.id,
|
|
174
|
+
costRating: model.costRating,
|
|
175
|
+
current: model.id === currentModelId,
|
|
162
176
|
disabled: false,
|
|
177
|
+
note: model.description,
|
|
163
178
|
}));
|
|
164
179
|
}
|
|
165
180
|
function getInputCommandToken(input) {
|
|
166
181
|
const trimmed = String(input ?? '').trimStart();
|
|
167
182
|
const match = trimmed.match(/^\/[^\s]*/);
|
|
168
|
-
|
|
183
|
+
const token = match?.[0] ?? '';
|
|
184
|
+
if (token.indexOf('/', 1) !== -1)
|
|
185
|
+
return '';
|
|
186
|
+
return token;
|
|
169
187
|
}
|
|
170
188
|
function scoreSlashCommand(option, token) {
|
|
171
189
|
if (option.command === token)
|
|
@@ -194,6 +212,9 @@ export function getSlashCommandSuggestions(input) {
|
|
|
194
212
|
function formatModelLabel(modelId, serverModels) {
|
|
195
213
|
return serverModels.find((model) => model.id === modelId)?.label ?? 'Unknown model';
|
|
196
214
|
}
|
|
215
|
+
function pickerModelLabel(modelId, serverModels) {
|
|
216
|
+
return formatModelLabel(modelId, serverModels).replace(/\s*\([^)]*\)\s*$/, '');
|
|
217
|
+
}
|
|
197
218
|
function filterResumeSessions(sessions, filter, serverModels) {
|
|
198
219
|
const q = filter.trim().toLowerCase();
|
|
199
220
|
if (!q)
|
|
@@ -217,6 +238,55 @@ function formatRelativeTime(isoDate) {
|
|
|
217
238
|
return `${hours}h ago`;
|
|
218
239
|
return `${Math.floor(hours / 24)}d ago`;
|
|
219
240
|
}
|
|
241
|
+
function todoItemLine(item, width) {
|
|
242
|
+
const text = fitLine(item.text, Math.max(8, width - 5));
|
|
243
|
+
if (item.status === 'completed') {
|
|
244
|
+
return line(span(' ✔ ', { color: 'green' }), span(text, { color: 'gray', dim: true }));
|
|
245
|
+
}
|
|
246
|
+
if (item.status === 'in_progress') {
|
|
247
|
+
return line(span(' ◐ ', { color: TODO_IN_PROGRESS_COLOR }), span(text, { color: TODO_IN_PROGRESS_COLOR, bold: true }));
|
|
248
|
+
}
|
|
249
|
+
return line(span(' ○ ', { color: 'gray' }), span(text));
|
|
250
|
+
}
|
|
251
|
+
export function formatTodoProgress(items) {
|
|
252
|
+
const done = items.filter((item) => item.status === 'completed').length;
|
|
253
|
+
return `${done}/${items.length} done`;
|
|
254
|
+
}
|
|
255
|
+
export function renderTodoListLines(items, width, { header = false } = {}) {
|
|
256
|
+
if (items.length === 0)
|
|
257
|
+
return [];
|
|
258
|
+
const lines = [];
|
|
259
|
+
if (header) {
|
|
260
|
+
lines.push(line(span('To-dos', { color: 'cyan', bold: true }), span(` · ${formatTodoProgress(items)}`, { color: 'gray' })));
|
|
261
|
+
}
|
|
262
|
+
const itemRowBudget = TODO_PANEL_MAX_ROWS - (header ? 1 : 0);
|
|
263
|
+
let collapsedDone = 0;
|
|
264
|
+
if (items.length > itemRowBudget) {
|
|
265
|
+
const over = items.length - (itemRowBudget - 1);
|
|
266
|
+
let leadingDone = 0;
|
|
267
|
+
while (leadingDone < items.length &&
|
|
268
|
+
items[leadingDone].status === 'completed') {
|
|
269
|
+
leadingDone += 1;
|
|
270
|
+
}
|
|
271
|
+
collapsedDone = Math.min(over, leadingDone);
|
|
272
|
+
}
|
|
273
|
+
if (collapsedDone > 0) {
|
|
274
|
+
lines.push(line(span(' ✔ ', { color: 'green' }), span(`${collapsedDone} completed`, { color: 'gray', dim: true })));
|
|
275
|
+
}
|
|
276
|
+
const remaining = items.slice(collapsedDone);
|
|
277
|
+
const budget = itemRowBudget - (collapsedDone > 0 ? 1 : 0);
|
|
278
|
+
const visible = remaining.length > budget ? remaining.slice(0, budget - 1) : remaining;
|
|
279
|
+
for (const item of visible) {
|
|
280
|
+
lines.push(todoItemLine(item, width));
|
|
281
|
+
}
|
|
282
|
+
if (remaining.length > visible.length) {
|
|
283
|
+
lines.push(plainLine(` … +${remaining.length - visible.length} more`, {
|
|
284
|
+
color: 'gray',
|
|
285
|
+
dim: true,
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
288
|
+
return lines;
|
|
289
|
+
}
|
|
220
290
|
export function renderTranscriptEntryLines(entry, width) {
|
|
221
291
|
const color = getEntryColor(entry.kind);
|
|
222
292
|
const lines = [
|
|
@@ -228,11 +298,14 @@ export function renderTranscriptEntryLines(entry, width) {
|
|
|
228
298
|
if (entry.diffPreview) {
|
|
229
299
|
lines.push(plainLine(` Added ${entry.diffPreview.added} line${entry.diffPreview.added === 1 ? '' : 's'}, removed ${entry.diffPreview.removed} line${entry.diffPreview.removed === 1 ? '' : 's'}`, { color: 'gray' }));
|
|
230
300
|
for (const diffLine of entry.diffPreview.lines.slice(0, TRANSCRIPT_DIFF_PREVIEW_LINES)) {
|
|
231
|
-
lines.push(line(span(`${diffLinePrefix(diffLine.kind)} `, { color: diffLineColor(diffLine.kind) }), span(
|
|
301
|
+
lines.push(line(span(`${diffLinePrefix(diffLine.kind)} `, { color: diffLineColor(diffLine.kind) }), span(fitLine(diffLine.content || ' ', width - 4), {
|
|
232
302
|
color: diffLineColor(diffLine.kind),
|
|
233
303
|
})));
|
|
234
304
|
}
|
|
235
305
|
}
|
|
306
|
+
if (entry.todoList && entry.todoList.length > 0) {
|
|
307
|
+
lines.push(...renderTodoListLines(entry.todoList, width));
|
|
308
|
+
}
|
|
236
309
|
return lines;
|
|
237
310
|
}
|
|
238
311
|
function tokenUsageLines(usage) {
|
|
@@ -274,6 +347,34 @@ function footerTransientStatus(status) {
|
|
|
274
347
|
}
|
|
275
348
|
return null;
|
|
276
349
|
}
|
|
350
|
+
function backgroundJobIndicatorSpans(state) {
|
|
351
|
+
const runningCount = (state.backgroundJobs ?? []).filter((job) => job.status === 'running').length;
|
|
352
|
+
if (runningCount === 0)
|
|
353
|
+
return [];
|
|
354
|
+
const noun = runningCount === 1 ? 'shell' : 'shells';
|
|
355
|
+
return [
|
|
356
|
+
span(' ', { color: 'gray', dim: true }),
|
|
357
|
+
span(`● ${runningCount} ${noun} running`, { color: 'green', bold: true }),
|
|
358
|
+
span(' · /jobs', { color: 'gray', dim: true }),
|
|
359
|
+
];
|
|
360
|
+
}
|
|
361
|
+
function todoIndicatorSpans(state) {
|
|
362
|
+
if (state.busy)
|
|
363
|
+
return [];
|
|
364
|
+
const items = state.todos ?? [];
|
|
365
|
+
if (items.length === 0)
|
|
366
|
+
return [];
|
|
367
|
+
const done = items.filter((item) => item.status === 'completed').length;
|
|
368
|
+
if (done >= items.length)
|
|
369
|
+
return [];
|
|
370
|
+
return [
|
|
371
|
+
span(' ', { color: 'gray', dim: true }),
|
|
372
|
+
span(`◐ ${done}/${items.length} to-dos`, {
|
|
373
|
+
color: TODO_IN_PROGRESS_COLOR,
|
|
374
|
+
bold: true,
|
|
375
|
+
}),
|
|
376
|
+
];
|
|
377
|
+
}
|
|
277
378
|
function composerFooterLines(state) {
|
|
278
379
|
const visibleSessionId = formatPromptSessionIdLabel(state.showSessionId, state.sessionId);
|
|
279
380
|
const transientStatus = footerTransientStatus(state.status);
|
|
@@ -292,7 +393,7 @@ function composerFooterLines(state) {
|
|
|
292
393
|
]
|
|
293
394
|
: []), ...(visibleSessionId
|
|
294
395
|
? [span(` ${visibleSessionId}`, { color: 'gray', dim: true })]
|
|
295
|
-
: [])),
|
|
396
|
+
: []), ...backgroundJobIndicatorSpans(state), ...todoIndicatorSpans(state)),
|
|
296
397
|
plainLine(''),
|
|
297
398
|
plainLine(formatModelLabel(state.currentModelId, state.serverModels), {
|
|
298
399
|
color: 'cyan',
|
|
@@ -307,15 +408,18 @@ function composerFooterLines(state) {
|
|
|
307
408
|
}));
|
|
308
409
|
return lines;
|
|
309
410
|
}
|
|
411
|
+
const busyHelperText = state.queuedMessage
|
|
412
|
+
? 'Enter re-queues • ↑ edit queued • Esc / Ctrl+C clear queued'
|
|
413
|
+
: state.input
|
|
414
|
+
? 'Enter queues • Esc cancels turn • Ctrl+C clears draft'
|
|
415
|
+
: 'Enter queues • Esc / Ctrl+C cancel turn';
|
|
310
416
|
const helperText = state.busy
|
|
311
|
-
?
|
|
312
|
-
? 'Enter re-queues • ↑ edit queued • Esc cancels queued'
|
|
313
|
-
: 'Enter queues • Esc / Ctrl+C cancel turn'
|
|
417
|
+
? busyHelperText
|
|
314
418
|
: process.platform === 'win32'
|
|
315
|
-
? 'Enter sends • Shift+Tab mode • Alt+V image • Esc cancel turn • Ctrl+C quits'
|
|
419
|
+
? 'Enter sends • Shift+Tab mode • Alt+V image • Esc cancel turn • Ctrl+C clears / quits'
|
|
316
420
|
: process.platform === 'darwin'
|
|
317
|
-
? 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C quits'
|
|
318
|
-
: 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C quits';
|
|
421
|
+
? 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C clears / quits'
|
|
422
|
+
: 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C clears / quits';
|
|
319
423
|
const agentLabel = agentModeLabel(state.agentMode).padEnd(AGENT_MODE_LABEL_WIDTH);
|
|
320
424
|
const tokenUsageText = state.tokenUsage || formatClientTokenUsage(null);
|
|
321
425
|
const footerSpans = [
|
|
@@ -329,6 +433,95 @@ function composerFooterLines(state) {
|
|
|
329
433
|
lines.push(line(...footerSpans));
|
|
330
434
|
return lines;
|
|
331
435
|
}
|
|
436
|
+
export function formatJobElapsed(elapsedMs) {
|
|
437
|
+
const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1000));
|
|
438
|
+
if (totalSeconds < 60)
|
|
439
|
+
return `${totalSeconds}s`;
|
|
440
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
441
|
+
if (minutes < 60) {
|
|
442
|
+
return `${minutes}m${String(totalSeconds % 60).padStart(2, '0')}s`;
|
|
443
|
+
}
|
|
444
|
+
return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}m`;
|
|
445
|
+
}
|
|
446
|
+
function backgroundJobPreviewLines(job, maxTailLines) {
|
|
447
|
+
const lines = [];
|
|
448
|
+
const first = String(job.firstOutputLine ?? '').trim();
|
|
449
|
+
if (first) {
|
|
450
|
+
lines.push(first);
|
|
451
|
+
}
|
|
452
|
+
for (const outputLine of (job.tailLines ?? []).slice(-maxTailLines)) {
|
|
453
|
+
const trimmed = String(outputLine ?? '').trim();
|
|
454
|
+
if (!trimmed)
|
|
455
|
+
continue;
|
|
456
|
+
if (lines.length === 1 && trimmed === first)
|
|
457
|
+
continue;
|
|
458
|
+
lines.push(trimmed);
|
|
459
|
+
}
|
|
460
|
+
return lines;
|
|
461
|
+
}
|
|
462
|
+
function jobStatusDescriptor(job, nowMs) {
|
|
463
|
+
const ran = formatJobElapsed((job.endedAt ?? (nowMs > 0 ? nowMs : Date.now())) - job.startedAt);
|
|
464
|
+
if (job.status === 'running') {
|
|
465
|
+
return { glyph: '●', color: 'green', text: `running · ${ran}` };
|
|
466
|
+
}
|
|
467
|
+
if (job.status === 'killed') {
|
|
468
|
+
return { glyph: '■', color: 'gray', text: `killed · ran ${ran}` };
|
|
469
|
+
}
|
|
470
|
+
if (job.status === 'error') {
|
|
471
|
+
return { glyph: '✖', color: 'red', text: 'failed to start' };
|
|
472
|
+
}
|
|
473
|
+
return job.exitCode === 0
|
|
474
|
+
? { glyph: '✓', color: 'green', text: `exited (0) · ran ${ran}` }
|
|
475
|
+
: { glyph: '✖', color: 'red', text: `exited (${job.exitCode ?? 1}) · ran ${ran}` };
|
|
476
|
+
}
|
|
477
|
+
function buildJobsPickerLines(state, width, nowMs) {
|
|
478
|
+
const jobs = state.backgroundJobs ?? [];
|
|
479
|
+
const lines = [
|
|
480
|
+
plainLine('Background jobs', { color: 'cyan', bold: true }),
|
|
481
|
+
];
|
|
482
|
+
if (jobs.length === 0) {
|
|
483
|
+
lines.push(plainLine('No background jobs in this session.', { color: 'gray' }));
|
|
484
|
+
}
|
|
485
|
+
for (const [index, job] of jobs.entries()) {
|
|
486
|
+
const selected = index === state.jobsPickerIndex;
|
|
487
|
+
const expanded = state.jobsPickerExpandedId === job.id;
|
|
488
|
+
const { glyph, color, text } = jobStatusDescriptor(job, nowMs);
|
|
489
|
+
lines.push(line(span(selected ? '› ' : ' ', { color: selected ? 'cyan' : 'gray' }), span(`${glyph} `, { color }), span(job.id.padEnd(6), { color: selected ? 'cyan' : undefined, bold: selected }), span(` ${truncate(job.command, Math.max(12, width - 34)).padEnd(Math.max(12, Math.min(40, width - 34)))}`, {
|
|
490
|
+
color: selected ? 'cyan' : undefined,
|
|
491
|
+
bold: selected,
|
|
492
|
+
}), span(` ${text}`, { color: 'gray' })));
|
|
493
|
+
if (expanded) {
|
|
494
|
+
const detailLines = (job.detailLines ?? []).length
|
|
495
|
+
? job.detailLines ?? []
|
|
496
|
+
: backgroundJobPreviewLines(job, 3);
|
|
497
|
+
if (detailLines.length === 0) {
|
|
498
|
+
lines.push(plainLine(' (no output captured)', { color: 'gray' }));
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
for (const outputLine of detailLines) {
|
|
502
|
+
lines.push(line(span(' │ ', { color: 'gray', dim: true }), span(truncate(outputLine, Math.max(8, width - 8)), {
|
|
503
|
+
color: 'gray',
|
|
504
|
+
dim: true,
|
|
505
|
+
})));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
lines.push(plainLine('↑/↓ move enter expand/collapse k kill esc close', {
|
|
511
|
+
color: 'gray',
|
|
512
|
+
}));
|
|
513
|
+
return lines;
|
|
514
|
+
}
|
|
515
|
+
function thinkingHeaderLine(spinnerFrame, title, width) {
|
|
516
|
+
const spinner = `${BRAILLE_SPINNER_FRAMES[spinnerFrame % BRAILLE_SPINNER_FRAMES.length]} `;
|
|
517
|
+
const fittedTitle = title
|
|
518
|
+
? fitLine(title, Math.max(8, width - spinner.length - 'Thinking'.length - 3))
|
|
519
|
+
: '';
|
|
520
|
+
return line(span(spinner, { color: 'green' }), span('Thinking', { color: 'green', bold: true }), ...(fittedTitle ? [span(` · ${fittedTitle}`, { color: 'ansi256(248)' })] : []));
|
|
521
|
+
}
|
|
522
|
+
function thinkingNoteLine(note, width) {
|
|
523
|
+
return line(span('│ ', { color: 'green' }), span(fitLine(note, Math.max(8, width - 3)), { color: 'ansi256(248)' }));
|
|
524
|
+
}
|
|
332
525
|
function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
333
526
|
if (!state.busy)
|
|
334
527
|
return [];
|
|
@@ -342,39 +535,56 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
342
535
|
}, width));
|
|
343
536
|
lines.push(plainLine(''));
|
|
344
537
|
}
|
|
345
|
-
lines.push(plainLine(`${WORKING_CLOCK_ICON} Working · ${elapsedSeconds < 60 ? `${elapsedSeconds}s` : `${Math.floor(elapsedSeconds / 60)}m ${String(elapsedSeconds % 60).padStart(2, '0')}s`}`, { color: 'yellow' }));
|
|
346
|
-
const visibleTitle = state.thinkingTitle.trim();
|
|
347
|
-
const visibleNotes = state.thinkingNotes.filter(Boolean).slice(-THINKING_NOTE_PREVIEW_ROWS);
|
|
348
|
-
if (visibleTitle || visibleNotes.length > 0) {
|
|
349
|
-
lines.push(plainLine(''));
|
|
350
|
-
lines.push(line(span(`${BRAILLE_SPINNER_FRAMES[spinnerFrame % BRAILLE_SPINNER_FRAMES.length]} `, {
|
|
351
|
-
color: 'green',
|
|
352
|
-
}), span('Thinking', { color: 'green', bold: true }), ...(visibleTitle ? [span(` · ${visibleTitle}`, { color: 'ansi256(248)' })] : [])));
|
|
353
|
-
for (const note of visibleNotes) {
|
|
354
|
-
lines.push(line(span('│ ', { color: 'green' }), span(note, { color: 'ansi256(248)' })));
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
538
|
const toolEntries = state.workingTools.slice(-WORKING_TOOL_PREVIEW_ROWS);
|
|
358
539
|
if (toolEntries.length > 0) {
|
|
359
|
-
lines.push(plainLine(''));
|
|
360
540
|
for (const entry of toolEntries) {
|
|
361
541
|
const color = getEntryColor(entry.kind);
|
|
362
542
|
const body = entry.body.split('\n')[0]?.trim();
|
|
363
543
|
lines.push(line(span('● ', { color }), span(entry.title, { color, bold: true }), ...(body ? [span(` ${truncate(body, 140)}`, { color })] : [])));
|
|
364
544
|
}
|
|
545
|
+
lines.push(plainLine(''));
|
|
365
546
|
}
|
|
366
547
|
const logLines = state.commandLog.slice(-COMMAND_PREVIEW_LINES);
|
|
367
548
|
if (logLines.length > 0) {
|
|
368
|
-
lines.push(plainLine(''));
|
|
549
|
+
lines.push(plainLine('⋮ output', { color: 'gray', dim: true }));
|
|
369
550
|
for (const outputLine of logLines) {
|
|
370
551
|
lines.push(plainLine(outputLine, { color: 'gray', dim: true }));
|
|
371
552
|
}
|
|
553
|
+
lines.push(plainLine(''));
|
|
554
|
+
}
|
|
555
|
+
const busyLabel = state.analyzingImages > 0
|
|
556
|
+
? state.analyzingImages > 1
|
|
557
|
+
? 'Analyzing images'
|
|
558
|
+
: 'Analyzing image'
|
|
559
|
+
: 'Working';
|
|
560
|
+
lines.push(plainLine(`${WORKING_CLOCK_ICON} ${busyLabel} · ${elapsedSeconds < 60 ? `${elapsedSeconds}s` : `${Math.floor(elapsedSeconds / 60)}m ${String(elapsedSeconds % 60).padStart(2, '0')}s`}`, { color: 'yellow' }));
|
|
561
|
+
const todos = state.todos ?? [];
|
|
562
|
+
if (todos.length > 0) {
|
|
563
|
+
lines.push(plainLine(''));
|
|
564
|
+
lines.push(...renderTodoListLines(todos, width, { header: true }));
|
|
565
|
+
}
|
|
566
|
+
const visibleTitle = state.thinkingTitle.trim();
|
|
567
|
+
const visibleNotes = state.thinkingNotes.filter(Boolean);
|
|
568
|
+
if (visibleTitle || visibleNotes.length > 0) {
|
|
569
|
+
lines.push(plainLine(''));
|
|
570
|
+
if (todos.length > 0) {
|
|
571
|
+
const minimalText = visibleTitle && visibleTitle !== 'Thinking'
|
|
572
|
+
? visibleTitle
|
|
573
|
+
: (visibleNotes[visibleNotes.length - 1] ?? '');
|
|
574
|
+
lines.push(thinkingHeaderLine(spinnerFrame, minimalText, width));
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
lines.push(thinkingHeaderLine(spinnerFrame, visibleTitle, width));
|
|
578
|
+
for (const note of visibleNotes.slice(-THINKING_NOTE_PREVIEW_ROWS)) {
|
|
579
|
+
lines.push(thinkingNoteLine(note, width));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
372
582
|
}
|
|
373
583
|
lines.push(plainLine(''));
|
|
374
584
|
return lines;
|
|
375
585
|
}
|
|
376
586
|
function lineCharCount(row) {
|
|
377
|
-
return row.spans.reduce((total, item) => total +
|
|
587
|
+
return row.spans.reduce((total, item) => total + displayWidth(item.text), 0);
|
|
378
588
|
}
|
|
379
589
|
function overlayPanelLine(row, width, color) {
|
|
380
590
|
const padding = Math.max(0, width - lineCharCount(row));
|
|
@@ -402,77 +612,118 @@ function padSpansToInnerWidth(spans, innerWidth, fill) {
|
|
|
402
612
|
function modelPickerPanelSideLine(content, innerWidth) {
|
|
403
613
|
return overlayPanelLine(content, innerWidth, MODEL_PICKER_BORDER_COLOR);
|
|
404
614
|
}
|
|
615
|
+
const MODEL_PICKER_COST_WIDTH = 6;
|
|
616
|
+
const MODEL_PICKER_MODEL_WIDTH = 42;
|
|
617
|
+
const MODEL_PICKER_NUMBER_WIDTH = 4;
|
|
618
|
+
const MODEL_PICKER_SEPARATOR = ' │ ';
|
|
619
|
+
const MODEL_PICKER_WIDE_MIN_WIDTH = 78;
|
|
405
620
|
function modelPickerTopBorder(panelWidth) {
|
|
406
|
-
const
|
|
407
|
-
const
|
|
408
|
-
const
|
|
409
|
-
|
|
621
|
+
const fullTitle = ' TheGitAI - Model Selection ';
|
|
622
|
+
const compactTitle = ' Model Selection ';
|
|
623
|
+
const title = panelWidth >= fullTitle.length + 4 ? fullTitle : compactTitle;
|
|
624
|
+
const available = Math.max(0, panelWidth - 2 - [...title].length);
|
|
625
|
+
const left = Math.floor(available / 2);
|
|
626
|
+
const right = available - left;
|
|
627
|
+
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 }));
|
|
628
|
+
}
|
|
629
|
+
function modelPickerDivider(panelWidth) {
|
|
630
|
+
return plainLine(`├${'─'.repeat(panelWidth - 2)}┤`, {
|
|
631
|
+
color: MODEL_PICKER_BORDER_COLOR,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
function modelPickerCell(text, width, style = {}) {
|
|
635
|
+
const fitted = fitLine(text, width);
|
|
636
|
+
return span(`${fitted}${' '.repeat(Math.max(0, width - [...fitted].length))}`, style);
|
|
637
|
+
}
|
|
638
|
+
function modelPickerCostText(rating) {
|
|
639
|
+
const steps = Math.max(1, Math.min(3, Math.round(rating)));
|
|
640
|
+
return '$'.repeat(steps);
|
|
641
|
+
}
|
|
642
|
+
function modelPickerNotesWidth(innerWidth) {
|
|
643
|
+
return Math.max(18, innerWidth -
|
|
644
|
+
MODEL_PICKER_NUMBER_WIDTH -
|
|
645
|
+
MODEL_PICKER_MODEL_WIDTH -
|
|
646
|
+
MODEL_PICKER_COST_WIDTH -
|
|
647
|
+
MODEL_PICKER_SEPARATOR.length * 2);
|
|
648
|
+
}
|
|
649
|
+
function modelPickerModelSpans(option, selected, width, showCurrentTag, labelStyle, selectedStyle) {
|
|
650
|
+
const tag = option.current && showCurrentTag ? ' (current)' : '';
|
|
651
|
+
const labelWidth = Math.max(1, width - [...tag].length);
|
|
652
|
+
const label = fitLine(`${selected ? '▶ ' : ' '}${option.label}`, labelWidth);
|
|
653
|
+
const used = [...label].length + [...tag].length;
|
|
654
|
+
return [
|
|
655
|
+
span(label, { ...labelStyle, ...selectedStyle }),
|
|
656
|
+
span(tag, { color: 'gray', ...selectedStyle }),
|
|
657
|
+
span(' '.repeat(Math.max(0, width - used)), selectedStyle),
|
|
658
|
+
];
|
|
410
659
|
}
|
|
411
660
|
function modelPickerItemLines(option, selected, innerWidth) {
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
span('
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
], innerWidth, highlight);
|
|
432
|
-
const lines = [modelPickerPanelSideLine(line(...titleSpans), innerWidth)];
|
|
433
|
-
if (option.meta) {
|
|
434
|
-
const metaSpans = padSpansToInnerWidth([
|
|
435
|
-
span(`${MODEL_PICKER_META_INDENT}${option.meta}`, {
|
|
436
|
-
color: 'gray',
|
|
437
|
-
...highlight,
|
|
438
|
-
}),
|
|
439
|
-
], innerWidth, highlight);
|
|
440
|
-
lines.push(modelPickerPanelSideLine(line(...metaSpans), innerWidth));
|
|
441
|
-
}
|
|
442
|
-
return lines;
|
|
661
|
+
const selectedStyle = selected ? { bgColor: MODEL_PICKER_HIGHLIGHT_BG } : {};
|
|
662
|
+
const labelStyle = option.disabled
|
|
663
|
+
? { color: 'gray' }
|
|
664
|
+
: selected
|
|
665
|
+
? { color: 'cyan', bold: true }
|
|
666
|
+
: {};
|
|
667
|
+
const numberCell = modelPickerCell(String(option.publicId), MODEL_PICKER_NUMBER_WIDTH, { color: 'cyan', bold: selected, ...selectedStyle });
|
|
668
|
+
const cost = modelPickerCostText(option.costRating);
|
|
669
|
+
if (innerWidth < MODEL_PICKER_WIDE_MIN_WIDTH) {
|
|
670
|
+
const modelWidth = Math.max(12, innerWidth - MODEL_PICKER_NUMBER_WIDTH - MODEL_PICKER_COST_WIDTH - 2);
|
|
671
|
+
const row = [
|
|
672
|
+
numberCell,
|
|
673
|
+
...modelPickerModelSpans(option, selected, modelWidth, false, labelStyle, selectedStyle),
|
|
674
|
+
span(' ', selectedStyle),
|
|
675
|
+
modelPickerCell(cost, MODEL_PICKER_COST_WIDTH, selectedStyle),
|
|
676
|
+
];
|
|
677
|
+
return [
|
|
678
|
+
modelPickerPanelSideLine(line(...padSpansToInnerWidth(row, innerWidth, selectedStyle)), innerWidth),
|
|
679
|
+
];
|
|
443
680
|
}
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
681
|
+
const row = [
|
|
682
|
+
numberCell,
|
|
683
|
+
...modelPickerModelSpans(option, selected, MODEL_PICKER_MODEL_WIDTH, true, labelStyle, selectedStyle),
|
|
684
|
+
span(MODEL_PICKER_SEPARATOR, { color: 'gray', ...selectedStyle }),
|
|
685
|
+
modelPickerCell(cost, MODEL_PICKER_COST_WIDTH, selectedStyle),
|
|
686
|
+
span(MODEL_PICKER_SEPARATOR, { color: 'gray', ...selectedStyle }),
|
|
687
|
+
modelPickerCell(option.note, modelPickerNotesWidth(innerWidth), {
|
|
688
|
+
color: 'gray',
|
|
689
|
+
...selectedStyle,
|
|
690
|
+
}),
|
|
691
|
+
];
|
|
692
|
+
return [
|
|
693
|
+
modelPickerPanelSideLine(line(...padSpansToInnerWidth(row, innerWidth, selectedStyle)), innerWidth),
|
|
447
694
|
];
|
|
448
|
-
if (option.meta) {
|
|
449
|
-
lines.push(modelPickerPanelSideLine(line(span(`${MODEL_PICKER_META_INDENT}${option.meta}`, { color: 'gray' })), innerWidth));
|
|
450
|
-
}
|
|
451
|
-
return lines;
|
|
452
695
|
}
|
|
453
|
-
function
|
|
454
|
-
|
|
696
|
+
function modelPickerHeaderLine(innerWidth) {
|
|
697
|
+
const heading = { color: MODEL_PICKER_ACCENT_COLOR, bold: true };
|
|
698
|
+
if (innerWidth < MODEL_PICKER_WIDE_MIN_WIDTH) {
|
|
699
|
+
return line(modelPickerCell('#', MODEL_PICKER_NUMBER_WIDTH, heading), modelPickerCell('Model', Math.max(1, innerWidth - MODEL_PICKER_NUMBER_WIDTH), heading));
|
|
700
|
+
}
|
|
701
|
+
return line(modelPickerCell('#', MODEL_PICKER_NUMBER_WIDTH, heading), 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));
|
|
455
702
|
}
|
|
456
|
-
function buildModelPickerPanel(options, selectedIndex, width) {
|
|
703
|
+
function buildModelPickerPanel(options, selectedIndex, width, availableHeight) {
|
|
457
704
|
const panelWidth = Math.max(28, Math.min(width, MODEL_PICKER_PANEL_MAX_WIDTH));
|
|
458
705
|
const innerWidth = Math.max(1, panelWidth - 4);
|
|
459
|
-
const
|
|
706
|
+
const compactBodyLineCount = options.length + 6;
|
|
707
|
+
const spacerLineCount = Math.max(0, options.length - 1);
|
|
708
|
+
const useRowSpacing = compactBodyLineCount + spacerLineCount <= availableHeight;
|
|
709
|
+
const bodyLineCount = compactBodyLineCount + (useRowSpacing ? spacerLineCount : 0);
|
|
710
|
+
const marginLineCount = Math.max(0, Math.min(MODEL_PICKER_PANEL_MARGIN_LINES, Math.floor((availableHeight - bodyLineCount) / 2)));
|
|
711
|
+
const margin = Array.from({ length: marginLineCount }, () => plainLine(''));
|
|
460
712
|
const body = [
|
|
461
|
-
plainLine('TheGitAI - Model Selection', {
|
|
462
|
-
color: MODEL_PICKER_ACCENT_COLOR,
|
|
463
|
-
bold: true,
|
|
464
|
-
}),
|
|
465
|
-
plainLine(''),
|
|
466
713
|
modelPickerTopBorder(panelWidth),
|
|
467
|
-
modelPickerPanelSideLine(
|
|
714
|
+
modelPickerPanelSideLine(modelPickerHeaderLine(innerWidth), innerWidth),
|
|
715
|
+
modelPickerDivider(panelWidth),
|
|
468
716
|
];
|
|
469
717
|
options.forEach((option, index) => {
|
|
470
718
|
body.push(...modelPickerItemLines(option, index === selectedIndex, innerWidth));
|
|
471
|
-
if (index < options.length - 1) {
|
|
472
|
-
body.push(
|
|
719
|
+
if (useRowSpacing && index < options.length - 1) {
|
|
720
|
+
body.push(modelPickerPanelSideLine(plainLine(''), innerWidth));
|
|
473
721
|
}
|
|
474
722
|
});
|
|
475
|
-
|
|
723
|
+
const fullHint = '↑/↓ navigate • Enter select • Esc cancel';
|
|
724
|
+
const compactHint = '↑/↓ • enter • esc';
|
|
725
|
+
const hint = [...fullHint].length <= innerWidth ? fullHint : compactHint;
|
|
726
|
+
body.push(modelPickerDivider(panelWidth), modelPickerPanelSideLine(plainLine(fitLine(hint, innerWidth), { color: 'gray' }), innerWidth), plainLine(`╰${'─'.repeat(panelWidth - 2)}╯`, { color: MODEL_PICKER_BORDER_COLOR }));
|
|
476
727
|
return [...margin, ...body, ...margin];
|
|
477
728
|
}
|
|
478
729
|
function commandPaletteTopBorder(panelWidth) {
|
|
@@ -481,6 +732,9 @@ function commandPaletteTopBorder(panelWidth) {
|
|
|
481
732
|
const dashCount = Math.max(0, panelWidth - prefix.length - suffix.length);
|
|
482
733
|
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 }));
|
|
483
734
|
}
|
|
735
|
+
function commandPaletteSeparatorLine(innerWidth) {
|
|
736
|
+
return modelPickerPanelSideLine(line(span('┈'.repeat(Math.max(1, innerWidth)), { color: 'gray', dim: true })), innerWidth);
|
|
737
|
+
}
|
|
484
738
|
function commandPaletteItemLines(option, selected, innerWidth) {
|
|
485
739
|
const highlight = { bgColor: MODEL_PICKER_HIGHLIGHT_BG };
|
|
486
740
|
if (selected) {
|
|
@@ -533,13 +787,13 @@ function buildCommandPalettePanel(suggestions, selectedIndex, width) {
|
|
|
533
787
|
suggestions.forEach((suggestion, index) => {
|
|
534
788
|
body.push(...commandPaletteItemLines(suggestion, index === selectedIndex, innerWidth));
|
|
535
789
|
if (index < suggestions.length - 1) {
|
|
536
|
-
body.push(
|
|
790
|
+
body.push(commandPaletteSeparatorLine(innerWidth));
|
|
537
791
|
}
|
|
538
792
|
});
|
|
539
793
|
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 }));
|
|
540
794
|
return [...margin, ...body, ...margin];
|
|
541
795
|
}
|
|
542
|
-
function buildOverlayLines(state, width) {
|
|
796
|
+
function buildOverlayLines(state, width, height, nowMs) {
|
|
543
797
|
const lines = [];
|
|
544
798
|
const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
|
|
545
799
|
const innerWidth = Math.max(1, panelWidth - 4);
|
|
@@ -612,22 +866,61 @@ function buildOverlayLines(state, width) {
|
|
|
612
866
|
}
|
|
613
867
|
if (state.modelPickerOpen) {
|
|
614
868
|
const options = buildModelPickerOptions(state.currentModelId, state.serverModels);
|
|
615
|
-
lines.push(...buildModelPickerPanel(options, state.modelPickerIndex, width));
|
|
869
|
+
lines.push(...buildModelPickerPanel(options, state.modelPickerIndex, width, height));
|
|
870
|
+
}
|
|
871
|
+
if (state.jobsPickerOpen) {
|
|
872
|
+
lines.push(...buildJobsPickerLines(state, width, nowMs));
|
|
616
873
|
}
|
|
617
874
|
if (state.resumePickerOpen) {
|
|
618
875
|
const filtered = filterResumeSessions(state.resumePickerSessions, state.resumePickerFilter, state.serverModels);
|
|
876
|
+
const pickerWidth = Math.max(20, width - 2);
|
|
877
|
+
const divider = () => plainLine('─'.repeat(pickerWidth), { color: 'gray', dim: true });
|
|
619
878
|
lines.push(plainLine('Resume a previous session', { color: 'cyan', bold: true }));
|
|
620
879
|
lines.push(line(span('Search: ', { color: 'gray' }), span(state.resumePickerFilter, {}), span('█', { color: 'gray' })));
|
|
621
|
-
|
|
880
|
+
lines.push(divider());
|
|
881
|
+
const maxCards = Math.max(2, Math.min(filtered.length, Math.floor((height - 10) / 3)));
|
|
882
|
+
let start = 0;
|
|
883
|
+
if (filtered.length > maxCards) {
|
|
884
|
+
start = Math.min(Math.max(0, state.resumePickerIndex - Math.floor(maxCards / 2)), filtered.length - maxCards);
|
|
885
|
+
}
|
|
886
|
+
const visible = filtered.slice(start, start + maxCards);
|
|
887
|
+
if (start > 0) {
|
|
888
|
+
lines.push(plainLine(` … ${start} newer`, { color: 'gray', dim: true }));
|
|
889
|
+
}
|
|
890
|
+
for (const [offset, session] of visible.entries()) {
|
|
891
|
+
const index = start + offset;
|
|
622
892
|
const selected = index === state.resumePickerIndex;
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
893
|
+
const prompt = singleLinePreview(session.lastUserMessage, pickerWidth) ||
|
|
894
|
+
session.name ||
|
|
895
|
+
`Session ${session.id}`;
|
|
896
|
+
lines.push(line(span(selected ? '› ' : ' ', { color: 'cyan' }), span(fitLine(prompt, Math.max(10, pickerWidth - 2)), selected
|
|
897
|
+
? { color: 'cyan', bold: true }
|
|
898
|
+
: session.lastUserMessage
|
|
899
|
+
? {}
|
|
900
|
+
: { color: 'gray', dim: true })));
|
|
901
|
+
const meta = [
|
|
902
|
+
formatRelativeTime(session.updatedAt),
|
|
903
|
+
pickerModelLabel(session.modelId, state.serverModels),
|
|
904
|
+
session.branch ?? null,
|
|
905
|
+
]
|
|
906
|
+
.filter(Boolean)
|
|
907
|
+
.join(' · ');
|
|
908
|
+
lines.push(line(span(' '), span(fitLine(meta, Math.max(10, pickerWidth - 4)), {
|
|
909
|
+
color: selected ? 'cyan' : 'gray',
|
|
910
|
+
dim: !selected,
|
|
911
|
+
})));
|
|
912
|
+
if (offset < visible.length - 1)
|
|
913
|
+
lines.push(plainLine(''));
|
|
914
|
+
}
|
|
915
|
+
const remaining = filtered.length - (start + visible.length);
|
|
916
|
+
if (remaining > 0) {
|
|
917
|
+
lines.push(plainLine(` … ${remaining} older`, { color: 'gray', dim: true }));
|
|
626
918
|
}
|
|
627
919
|
if (filtered.length === 0) {
|
|
628
920
|
lines.push(plainLine('No sessions match.', { color: 'gray' }));
|
|
629
921
|
}
|
|
630
|
-
lines.push(
|
|
922
|
+
lines.push(divider());
|
|
923
|
+
lines.push(plainLine('↑/↓ move · enter resume · esc start new · ctrl+c quit', {
|
|
631
924
|
color: 'gray',
|
|
632
925
|
}));
|
|
633
926
|
}
|
|
@@ -655,7 +948,7 @@ function sliceTranscriptLines(lines, maxLines, scrollOffset) {
|
|
|
655
948
|
const start = Math.max(0, lines.length - maxLines - offset);
|
|
656
949
|
return lines.slice(start, start + maxLines);
|
|
657
950
|
}
|
|
658
|
-
export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds) {
|
|
951
|
+
export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, nowMs = 0) {
|
|
659
952
|
const contentWidth = Math.max(20, Math.floor(cols * 0.95) - 2);
|
|
660
953
|
const gutter = Math.max(Math.floor((cols - contentWidth) / 2), 0);
|
|
661
954
|
const transcriptBlocks = state.transcript.map((entry) => renderTranscriptEntryLines(entry, contentWidth));
|
|
@@ -672,12 +965,12 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds) {
|
|
|
672
965
|
sections.push({ kind: 'live', lines: liveLines });
|
|
673
966
|
}
|
|
674
967
|
const overlayActive = Boolean(state.approvalPrompt || state.sudoPrompt);
|
|
675
|
-
|
|
676
|
-
// one message is already queued, the queued chip is shown here in place of the
|
|
677
|
-
// input box (only one message is ever held) so there is no empty prompt.
|
|
678
|
-
if (!state.resumePickerOpen && !state.modelPickerOpen && !overlayActive) {
|
|
968
|
+
if (!state.resumePickerOpen && !state.modelPickerOpen && !state.jobsPickerOpen && !overlayActive) {
|
|
679
969
|
const composerLines = [];
|
|
680
|
-
if (state.
|
|
970
|
+
if (state.busy && state.status === 'Starting a new conversation...') {
|
|
971
|
+
composerLines.push(line(span('Starting a new conversation…', { color: 'gray', dim: true })));
|
|
972
|
+
}
|
|
973
|
+
else if (state.queuedMessage) {
|
|
681
974
|
const preview = truncate(state.queuedMessage.body.trim().replace(/\s+/g, ' '), 60);
|
|
682
975
|
const imageCount = state.queuedMessage.imageAttachments.length;
|
|
683
976
|
composerLines.push(line(span(`↳ Queued · "${preview}"`, { color: 'gray', dim: true }), ...(imageCount > 0
|
|
@@ -696,7 +989,7 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds) {
|
|
|
696
989
|
lines: [...composerLines, plainLine(''), ...composerFooterLines(state)],
|
|
697
990
|
});
|
|
698
991
|
}
|
|
699
|
-
const overlayLines = buildOverlayLines(state, contentWidth);
|
|
992
|
+
const overlayLines = buildOverlayLines(state, contentWidth, rows, nowMs);
|
|
700
993
|
if (overlayLines.length > 0) {
|
|
701
994
|
sections.push({ kind: 'overlay', lines: overlayLines });
|
|
702
995
|
}
|