@thegitai/cli 1.0.0-beta.2 → 1.0.0-beta.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 +37 -2
- package/dist/bin/ai.js +148 -75
- package/dist/parsers/NOTICE +18 -0
- package/dist/src/agent-mode.js +5 -0
- package/dist/src/api/auth.js +6 -4
- package/dist/src/api/browser-login.js +7 -41
- package/dist/src/api/chat.js +77 -20
- package/dist/src/api/http.js +81 -4
- package/dist/src/api/models.js +26 -18
- package/dist/src/artifact-policy.js +12 -0
- package/dist/src/background-jobs.js +410 -0
- package/dist/src/cli-args.js +60 -0
- package/dist/src/client-environment.js +129 -0
- package/dist/src/colors.js +50 -0
- package/dist/src/core/clipboard.js +75 -0
- package/dist/src/core/image-path-extractor.js +144 -0
- package/dist/src/edit-journal.js +39 -6
- package/dist/src/executor.js +48 -12
- package/dist/src/help-text.js +24 -5
- package/dist/src/markdown-renderer.js +1 -1
- package/dist/src/patcher.js +17 -2
- package/dist/src/scanner.js +58 -17
- package/dist/src/scratch-dir.js +57 -0
- package/dist/src/secret-preview.js +0 -10
- package/dist/src/session-safety.js +64 -31
- package/dist/src/session-store.js +0 -1
- package/dist/src/todo-list.js +106 -0
- package/dist/src/tool-executor.js +164 -18
- package/dist/src/tools/delete-file.js +1 -1
- package/dist/src/tools/index.js +8 -0
- package/dist/src/tools/patch-file.js +16 -2
- package/dist/src/tools/path-suggest.js +139 -0
- package/dist/src/tools/read-document.js +15 -4
- package/dist/src/tools/read-file.js +23 -7
- package/dist/src/tools/replace-document-text.js +234 -0
- package/dist/src/tools/restore-checkpoint.js +1 -1
- package/dist/src/tools/run-command.js +83 -16
- package/dist/src/tools/run-node-script.js +3 -1
- 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 +16 -2
- package/dist/src/tools/undo-edit.js +7 -5
- package/dist/src/tools/update-todos.js +27 -0
- package/dist/src/tools/write-file.js +14 -1
- package/dist/src/tree-sitter-runtime.js +8 -1
- package/dist/src/ui/repl.js +315 -24
- package/dist/src/ui/tui/bridge.js +2 -6
- package/dist/src/ui/tui/build-frame.js +224 -25
- package/dist/src/ui/tui/shell-input.js +42 -5
- package/dist/src/version.js +29 -0
- 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 +14 -15
|
@@ -5,6 +5,8 @@ import { renderFormattedBodyLines, renderPreformattedBodyLines, } from './markdo
|
|
|
5
5
|
import { line, plainLine, 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;
|
|
@@ -131,6 +133,15 @@ function diffLinePrefix(kind) {
|
|
|
131
133
|
return ' ';
|
|
132
134
|
}
|
|
133
135
|
}
|
|
136
|
+
function fitLine(content, maxWidth) {
|
|
137
|
+
if (maxWidth <= 0)
|
|
138
|
+
return '';
|
|
139
|
+
if (content.length <= maxWidth)
|
|
140
|
+
return content;
|
|
141
|
+
if (maxWidth === 1)
|
|
142
|
+
return '…';
|
|
143
|
+
return `${content.slice(0, 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,6 +162,7 @@ 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' },
|
|
165
|
+
{ command: '/jobs', description: 'Background jobs: pick to view output or kill' },
|
|
154
166
|
{ command: '/clear', description: 'Clear conversation history' },
|
|
155
167
|
{ command: '/exit', description: 'Quit the current session' },
|
|
156
168
|
];
|
|
@@ -165,7 +177,10 @@ function buildModelPickerOptions(currentModelId, serverModels) {
|
|
|
165
177
|
function getInputCommandToken(input) {
|
|
166
178
|
const trimmed = String(input ?? '').trimStart();
|
|
167
179
|
const match = trimmed.match(/^\/[^\s]*/);
|
|
168
|
-
|
|
180
|
+
const token = match?.[0] ?? '';
|
|
181
|
+
if (token.indexOf('/', 1) !== -1)
|
|
182
|
+
return '';
|
|
183
|
+
return token;
|
|
169
184
|
}
|
|
170
185
|
function scoreSlashCommand(option, token) {
|
|
171
186
|
if (option.command === token)
|
|
@@ -217,6 +232,55 @@ function formatRelativeTime(isoDate) {
|
|
|
217
232
|
return `${hours}h ago`;
|
|
218
233
|
return `${Math.floor(hours / 24)}d ago`;
|
|
219
234
|
}
|
|
235
|
+
function todoItemLine(item, width) {
|
|
236
|
+
const text = fitLine(item.text, Math.max(8, width - 5));
|
|
237
|
+
if (item.status === 'completed') {
|
|
238
|
+
return line(span(' ✔ ', { color: 'green' }), span(text, { color: 'gray', dim: true }));
|
|
239
|
+
}
|
|
240
|
+
if (item.status === 'in_progress') {
|
|
241
|
+
return line(span(' ◐ ', { color: TODO_IN_PROGRESS_COLOR }), span(text, { color: TODO_IN_PROGRESS_COLOR, bold: true }));
|
|
242
|
+
}
|
|
243
|
+
return line(span(' ○ ', { color: 'gray' }), span(text));
|
|
244
|
+
}
|
|
245
|
+
export function formatTodoProgress(items) {
|
|
246
|
+
const done = items.filter((item) => item.status === 'completed').length;
|
|
247
|
+
return `${done}/${items.length} done`;
|
|
248
|
+
}
|
|
249
|
+
export function renderTodoListLines(items, width, { header = false } = {}) {
|
|
250
|
+
if (items.length === 0)
|
|
251
|
+
return [];
|
|
252
|
+
const lines = [];
|
|
253
|
+
if (header) {
|
|
254
|
+
lines.push(line(span('To-dos', { color: 'cyan', bold: true }), span(` · ${formatTodoProgress(items)}`, { color: 'gray' })));
|
|
255
|
+
}
|
|
256
|
+
const itemRowBudget = TODO_PANEL_MAX_ROWS - (header ? 1 : 0);
|
|
257
|
+
let collapsedDone = 0;
|
|
258
|
+
if (items.length > itemRowBudget) {
|
|
259
|
+
const over = items.length - (itemRowBudget - 1);
|
|
260
|
+
let leadingDone = 0;
|
|
261
|
+
while (leadingDone < items.length &&
|
|
262
|
+
items[leadingDone].status === 'completed') {
|
|
263
|
+
leadingDone += 1;
|
|
264
|
+
}
|
|
265
|
+
collapsedDone = Math.min(over, leadingDone);
|
|
266
|
+
}
|
|
267
|
+
if (collapsedDone > 0) {
|
|
268
|
+
lines.push(line(span(' ✔ ', { color: 'green' }), span(`${collapsedDone} completed`, { color: 'gray', dim: true })));
|
|
269
|
+
}
|
|
270
|
+
const remaining = items.slice(collapsedDone);
|
|
271
|
+
const budget = itemRowBudget - (collapsedDone > 0 ? 1 : 0);
|
|
272
|
+
const visible = remaining.length > budget ? remaining.slice(0, budget - 1) : remaining;
|
|
273
|
+
for (const item of visible) {
|
|
274
|
+
lines.push(todoItemLine(item, width));
|
|
275
|
+
}
|
|
276
|
+
if (remaining.length > visible.length) {
|
|
277
|
+
lines.push(plainLine(` … +${remaining.length - visible.length} more`, {
|
|
278
|
+
color: 'gray',
|
|
279
|
+
dim: true,
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
return lines;
|
|
283
|
+
}
|
|
220
284
|
export function renderTranscriptEntryLines(entry, width) {
|
|
221
285
|
const color = getEntryColor(entry.kind);
|
|
222
286
|
const lines = [
|
|
@@ -228,11 +292,14 @@ export function renderTranscriptEntryLines(entry, width) {
|
|
|
228
292
|
if (entry.diffPreview) {
|
|
229
293
|
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
294
|
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(
|
|
295
|
+
lines.push(line(span(`${diffLinePrefix(diffLine.kind)} `, { color: diffLineColor(diffLine.kind) }), span(fitLine(diffLine.content || ' ', width - 4), {
|
|
232
296
|
color: diffLineColor(diffLine.kind),
|
|
233
297
|
})));
|
|
234
298
|
}
|
|
235
299
|
}
|
|
300
|
+
if (entry.todoList && entry.todoList.length > 0) {
|
|
301
|
+
lines.push(...renderTodoListLines(entry.todoList, width));
|
|
302
|
+
}
|
|
236
303
|
return lines;
|
|
237
304
|
}
|
|
238
305
|
function tokenUsageLines(usage) {
|
|
@@ -274,6 +341,34 @@ function footerTransientStatus(status) {
|
|
|
274
341
|
}
|
|
275
342
|
return null;
|
|
276
343
|
}
|
|
344
|
+
function backgroundJobIndicatorSpans(state) {
|
|
345
|
+
const runningCount = (state.backgroundJobs ?? []).filter((job) => job.status === 'running').length;
|
|
346
|
+
if (runningCount === 0)
|
|
347
|
+
return [];
|
|
348
|
+
const noun = runningCount === 1 ? 'shell' : 'shells';
|
|
349
|
+
return [
|
|
350
|
+
span(' ', { color: 'gray', dim: true }),
|
|
351
|
+
span(`● ${runningCount} ${noun} running`, { color: 'green', bold: true }),
|
|
352
|
+
span(' · /jobs', { color: 'gray', dim: true }),
|
|
353
|
+
];
|
|
354
|
+
}
|
|
355
|
+
function todoIndicatorSpans(state) {
|
|
356
|
+
if (state.busy)
|
|
357
|
+
return [];
|
|
358
|
+
const items = state.todos ?? [];
|
|
359
|
+
if (items.length === 0)
|
|
360
|
+
return [];
|
|
361
|
+
const done = items.filter((item) => item.status === 'completed').length;
|
|
362
|
+
if (done >= items.length)
|
|
363
|
+
return [];
|
|
364
|
+
return [
|
|
365
|
+
span(' ', { color: 'gray', dim: true }),
|
|
366
|
+
span(`◐ ${done}/${items.length} to-dos`, {
|
|
367
|
+
color: TODO_IN_PROGRESS_COLOR,
|
|
368
|
+
bold: true,
|
|
369
|
+
}),
|
|
370
|
+
];
|
|
371
|
+
}
|
|
277
372
|
function composerFooterLines(state) {
|
|
278
373
|
const visibleSessionId = formatPromptSessionIdLabel(state.showSessionId, state.sessionId);
|
|
279
374
|
const transientStatus = footerTransientStatus(state.status);
|
|
@@ -292,7 +387,7 @@ function composerFooterLines(state) {
|
|
|
292
387
|
]
|
|
293
388
|
: []), ...(visibleSessionId
|
|
294
389
|
? [span(` ${visibleSessionId}`, { color: 'gray', dim: true })]
|
|
295
|
-
: [])),
|
|
390
|
+
: []), ...backgroundJobIndicatorSpans(state), ...todoIndicatorSpans(state)),
|
|
296
391
|
plainLine(''),
|
|
297
392
|
plainLine(formatModelLabel(state.currentModelId, state.serverModels), {
|
|
298
393
|
color: 'cyan',
|
|
@@ -311,7 +406,11 @@ function composerFooterLines(state) {
|
|
|
311
406
|
? state.queuedMessage
|
|
312
407
|
? 'Enter re-queues • ↑ edit queued • Esc cancels queued'
|
|
313
408
|
: 'Enter queues • Esc / Ctrl+C cancel turn'
|
|
314
|
-
:
|
|
409
|
+
: process.platform === 'win32'
|
|
410
|
+
? 'Enter sends • Shift+Tab mode • Alt+V image • Esc cancel turn • Ctrl+C quits'
|
|
411
|
+
: process.platform === 'darwin'
|
|
412
|
+
? 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C quits'
|
|
413
|
+
: 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C quits';
|
|
315
414
|
const agentLabel = agentModeLabel(state.agentMode).padEnd(AGENT_MODE_LABEL_WIDTH);
|
|
316
415
|
const tokenUsageText = state.tokenUsage || formatClientTokenUsage(null);
|
|
317
416
|
const footerSpans = [
|
|
@@ -325,6 +424,95 @@ function composerFooterLines(state) {
|
|
|
325
424
|
lines.push(line(...footerSpans));
|
|
326
425
|
return lines;
|
|
327
426
|
}
|
|
427
|
+
export function formatJobElapsed(elapsedMs) {
|
|
428
|
+
const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1000));
|
|
429
|
+
if (totalSeconds < 60)
|
|
430
|
+
return `${totalSeconds}s`;
|
|
431
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
432
|
+
if (minutes < 60) {
|
|
433
|
+
return `${minutes}m${String(totalSeconds % 60).padStart(2, '0')}s`;
|
|
434
|
+
}
|
|
435
|
+
return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}m`;
|
|
436
|
+
}
|
|
437
|
+
function backgroundJobPreviewLines(job, maxTailLines) {
|
|
438
|
+
const lines = [];
|
|
439
|
+
const first = String(job.firstOutputLine ?? '').trim();
|
|
440
|
+
if (first) {
|
|
441
|
+
lines.push(first);
|
|
442
|
+
}
|
|
443
|
+
for (const outputLine of (job.tailLines ?? []).slice(-maxTailLines)) {
|
|
444
|
+
const trimmed = String(outputLine ?? '').trim();
|
|
445
|
+
if (!trimmed)
|
|
446
|
+
continue;
|
|
447
|
+
if (lines.length === 1 && trimmed === first)
|
|
448
|
+
continue;
|
|
449
|
+
lines.push(trimmed);
|
|
450
|
+
}
|
|
451
|
+
return lines;
|
|
452
|
+
}
|
|
453
|
+
function jobStatusDescriptor(job, nowMs) {
|
|
454
|
+
const ran = formatJobElapsed((job.endedAt ?? (nowMs > 0 ? nowMs : Date.now())) - job.startedAt);
|
|
455
|
+
if (job.status === 'running') {
|
|
456
|
+
return { glyph: '●', color: 'green', text: `running · ${ran}` };
|
|
457
|
+
}
|
|
458
|
+
if (job.status === 'killed') {
|
|
459
|
+
return { glyph: '■', color: 'gray', text: `killed · ran ${ran}` };
|
|
460
|
+
}
|
|
461
|
+
if (job.status === 'error') {
|
|
462
|
+
return { glyph: '✖', color: 'red', text: 'failed to start' };
|
|
463
|
+
}
|
|
464
|
+
return job.exitCode === 0
|
|
465
|
+
? { glyph: '✓', color: 'green', text: `exited (0) · ran ${ran}` }
|
|
466
|
+
: { glyph: '✖', color: 'red', text: `exited (${job.exitCode ?? 1}) · ran ${ran}` };
|
|
467
|
+
}
|
|
468
|
+
function buildJobsPickerLines(state, width, nowMs) {
|
|
469
|
+
const jobs = state.backgroundJobs ?? [];
|
|
470
|
+
const lines = [
|
|
471
|
+
plainLine('Background jobs', { color: 'cyan', bold: true }),
|
|
472
|
+
];
|
|
473
|
+
if (jobs.length === 0) {
|
|
474
|
+
lines.push(plainLine('No background jobs in this session.', { color: 'gray' }));
|
|
475
|
+
}
|
|
476
|
+
for (const [index, job] of jobs.entries()) {
|
|
477
|
+
const selected = index === state.jobsPickerIndex;
|
|
478
|
+
const expanded = state.jobsPickerExpandedId === job.id;
|
|
479
|
+
const { glyph, color, text } = jobStatusDescriptor(job, nowMs);
|
|
480
|
+
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)))}`, {
|
|
481
|
+
color: selected ? 'cyan' : undefined,
|
|
482
|
+
bold: selected,
|
|
483
|
+
}), span(` ${text}`, { color: 'gray' })));
|
|
484
|
+
if (expanded) {
|
|
485
|
+
const detailLines = (job.detailLines ?? []).length
|
|
486
|
+
? job.detailLines ?? []
|
|
487
|
+
: backgroundJobPreviewLines(job, 3);
|
|
488
|
+
if (detailLines.length === 0) {
|
|
489
|
+
lines.push(plainLine(' (no output captured)', { color: 'gray' }));
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
for (const outputLine of detailLines) {
|
|
493
|
+
lines.push(line(span(' │ ', { color: 'gray', dim: true }), span(truncate(outputLine, Math.max(8, width - 8)), {
|
|
494
|
+
color: 'gray',
|
|
495
|
+
dim: true,
|
|
496
|
+
})));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
lines.push(plainLine('↑/↓ move enter expand/collapse k kill esc close', {
|
|
502
|
+
color: 'gray',
|
|
503
|
+
}));
|
|
504
|
+
return lines;
|
|
505
|
+
}
|
|
506
|
+
function thinkingHeaderLine(spinnerFrame, title, width) {
|
|
507
|
+
const spinner = `${BRAILLE_SPINNER_FRAMES[spinnerFrame % BRAILLE_SPINNER_FRAMES.length]} `;
|
|
508
|
+
const fittedTitle = title
|
|
509
|
+
? fitLine(title, Math.max(8, width - spinner.length - 'Thinking'.length - 3))
|
|
510
|
+
: '';
|
|
511
|
+
return line(span(spinner, { color: 'green' }), span('Thinking', { color: 'green', bold: true }), ...(fittedTitle ? [span(` · ${fittedTitle}`, { color: 'ansi256(248)' })] : []));
|
|
512
|
+
}
|
|
513
|
+
function thinkingNoteLine(note, width) {
|
|
514
|
+
return line(span('│ ', { color: 'green' }), span(fitLine(note, Math.max(8, width - 3)), { color: 'ansi256(248)' }));
|
|
515
|
+
}
|
|
328
516
|
function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
329
517
|
if (!state.busy)
|
|
330
518
|
return [];
|
|
@@ -338,33 +526,44 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
338
526
|
}, width));
|
|
339
527
|
lines.push(plainLine(''));
|
|
340
528
|
}
|
|
341
|
-
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' }));
|
|
342
|
-
const visibleTitle = state.thinkingTitle.trim();
|
|
343
|
-
const visibleNotes = state.thinkingNotes.filter(Boolean).slice(-THINKING_NOTE_PREVIEW_ROWS);
|
|
344
|
-
if (visibleTitle || visibleNotes.length > 0) {
|
|
345
|
-
lines.push(plainLine(''));
|
|
346
|
-
lines.push(line(span(`${BRAILLE_SPINNER_FRAMES[spinnerFrame % BRAILLE_SPINNER_FRAMES.length]} `, {
|
|
347
|
-
color: 'green',
|
|
348
|
-
}), span('Thinking', { color: 'green', bold: true }), ...(visibleTitle ? [span(` · ${visibleTitle}`, { color: 'ansi256(248)' })] : [])));
|
|
349
|
-
for (const note of visibleNotes) {
|
|
350
|
-
lines.push(line(span('│ ', { color: 'green' }), span(note, { color: 'ansi256(248)' })));
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
529
|
const toolEntries = state.workingTools.slice(-WORKING_TOOL_PREVIEW_ROWS);
|
|
354
530
|
if (toolEntries.length > 0) {
|
|
355
|
-
lines.push(plainLine(''));
|
|
356
531
|
for (const entry of toolEntries) {
|
|
357
532
|
const color = getEntryColor(entry.kind);
|
|
358
533
|
const body = entry.body.split('\n')[0]?.trim();
|
|
359
534
|
lines.push(line(span('● ', { color }), span(entry.title, { color, bold: true }), ...(body ? [span(` ${truncate(body, 140)}`, { color })] : [])));
|
|
360
535
|
}
|
|
536
|
+
lines.push(plainLine(''));
|
|
361
537
|
}
|
|
362
538
|
const logLines = state.commandLog.slice(-COMMAND_PREVIEW_LINES);
|
|
363
539
|
if (logLines.length > 0) {
|
|
364
|
-
lines.push(plainLine(''));
|
|
540
|
+
lines.push(plainLine('⋮ output', { color: 'gray', dim: true }));
|
|
365
541
|
for (const outputLine of logLines) {
|
|
366
542
|
lines.push(plainLine(outputLine, { color: 'gray', dim: true }));
|
|
367
543
|
}
|
|
544
|
+
lines.push(plainLine(''));
|
|
545
|
+
}
|
|
546
|
+
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' }));
|
|
547
|
+
if (state.todos.length > 0) {
|
|
548
|
+
lines.push(plainLine(''));
|
|
549
|
+
lines.push(...renderTodoListLines(state.todos, width, { header: true }));
|
|
550
|
+
}
|
|
551
|
+
const visibleTitle = state.thinkingTitle.trim();
|
|
552
|
+
const visibleNotes = state.thinkingNotes.filter(Boolean);
|
|
553
|
+
if (visibleTitle || visibleNotes.length > 0) {
|
|
554
|
+
lines.push(plainLine(''));
|
|
555
|
+
if (state.todos.length > 0) {
|
|
556
|
+
const minimalText = visibleTitle && visibleTitle !== 'Thinking'
|
|
557
|
+
? visibleTitle
|
|
558
|
+
: (visibleNotes[visibleNotes.length - 1] ?? '');
|
|
559
|
+
lines.push(thinkingHeaderLine(spinnerFrame, minimalText, width));
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
lines.push(thinkingHeaderLine(spinnerFrame, visibleTitle, width));
|
|
563
|
+
for (const note of visibleNotes.slice(-THINKING_NOTE_PREVIEW_ROWS)) {
|
|
564
|
+
lines.push(thinkingNoteLine(note, width));
|
|
565
|
+
}
|
|
566
|
+
}
|
|
368
567
|
}
|
|
369
568
|
lines.push(plainLine(''));
|
|
370
569
|
return lines;
|
|
@@ -535,7 +734,7 @@ function buildCommandPalettePanel(suggestions, selectedIndex, width) {
|
|
|
535
734
|
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 }));
|
|
536
735
|
return [...margin, ...body, ...margin];
|
|
537
736
|
}
|
|
538
|
-
function buildOverlayLines(state, width) {
|
|
737
|
+
function buildOverlayLines(state, width, nowMs) {
|
|
539
738
|
const lines = [];
|
|
540
739
|
const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
|
|
541
740
|
const innerWidth = Math.max(1, panelWidth - 4);
|
|
@@ -610,6 +809,9 @@ function buildOverlayLines(state, width) {
|
|
|
610
809
|
const options = buildModelPickerOptions(state.currentModelId, state.serverModels);
|
|
611
810
|
lines.push(...buildModelPickerPanel(options, state.modelPickerIndex, width));
|
|
612
811
|
}
|
|
812
|
+
if (state.jobsPickerOpen) {
|
|
813
|
+
lines.push(...buildJobsPickerLines(state, width, nowMs));
|
|
814
|
+
}
|
|
613
815
|
if (state.resumePickerOpen) {
|
|
614
816
|
const filtered = filterResumeSessions(state.resumePickerSessions, state.resumePickerFilter, state.serverModels);
|
|
615
817
|
lines.push(plainLine('Resume a previous session', { color: 'cyan', bold: true }));
|
|
@@ -651,7 +853,7 @@ function sliceTranscriptLines(lines, maxLines, scrollOffset) {
|
|
|
651
853
|
const start = Math.max(0, lines.length - maxLines - offset);
|
|
652
854
|
return lines.slice(start, start + maxLines);
|
|
653
855
|
}
|
|
654
|
-
export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds) {
|
|
856
|
+
export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, nowMs = 0) {
|
|
655
857
|
const contentWidth = Math.max(20, Math.floor(cols * 0.95) - 2);
|
|
656
858
|
const gutter = Math.max(Math.floor((cols - contentWidth) / 2), 0);
|
|
657
859
|
const transcriptBlocks = state.transcript.map((entry) => renderTranscriptEntryLines(entry, contentWidth));
|
|
@@ -668,10 +870,7 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds) {
|
|
|
668
870
|
sections.push({ kind: 'live', lines: liveLines });
|
|
669
871
|
}
|
|
670
872
|
const overlayActive = Boolean(state.approvalPrompt || state.sudoPrompt);
|
|
671
|
-
|
|
672
|
-
// one message is already queued, the queued chip is shown here in place of the
|
|
673
|
-
// input box (only one message is ever held) so there is no empty prompt.
|
|
674
|
-
if (!state.resumePickerOpen && !state.modelPickerOpen && !overlayActive) {
|
|
873
|
+
if (!state.resumePickerOpen && !state.modelPickerOpen && !state.jobsPickerOpen && !overlayActive) {
|
|
675
874
|
const composerLines = [];
|
|
676
875
|
if (state.queuedMessage) {
|
|
677
876
|
const preview = truncate(state.queuedMessage.body.trim().replace(/\s+/g, ' '), 60);
|
|
@@ -692,7 +891,7 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds) {
|
|
|
692
891
|
lines: [...composerLines, plainLine(''), ...composerFooterLines(state)],
|
|
693
892
|
});
|
|
694
893
|
}
|
|
695
|
-
const overlayLines = buildOverlayLines(state, contentWidth);
|
|
894
|
+
const overlayLines = buildOverlayLines(state, contentWidth, nowMs);
|
|
696
895
|
if (overlayLines.length > 0) {
|
|
697
896
|
sections.push({ kind: 'overlay', lines: overlayLines });
|
|
698
897
|
}
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { readClipboardImage, readClipboardText } from '../../core/clipboard.js';
|
|
2
2
|
import { applySlashCommandSuggestion, buildModelPickerOptions, deleteAtCursor, deleteBeforeCursor, getApprovalChoiceForCursor, getInputCommandToken, getNextApprovalCursor, getNextModelPickerIndex, getSlashCommandSuggestions, insertAtCursor, isExactSlashCommandToken, navigatePromptHistory, resolveApprovalChoiceFromInput, shouldRemountLiveFrameForComposerInputChange, } from '../repl.js';
|
|
3
3
|
import { buildPastePlaceholder, shouldCollapsePaste, } from '../paste-collapse.js';
|
|
4
|
+
function isClipboardImagePasteKey(key) {
|
|
5
|
+
if (process.platform === 'win32') {
|
|
6
|
+
return ((key.ctrl || key.meta) &&
|
|
7
|
+
key.input.toLowerCase() === 'v' &&
|
|
8
|
+
!key.shift);
|
|
9
|
+
}
|
|
10
|
+
return key.ctrl && key.input === 'v' && !key.shift && !key.meta;
|
|
11
|
+
}
|
|
4
12
|
function shouldShowCommandPalette(state) {
|
|
5
13
|
if (state.busy ||
|
|
6
14
|
state.exiting ||
|
|
15
|
+
state.jobsPickerOpen ||
|
|
7
16
|
state.modelPickerOpen ||
|
|
8
17
|
state.resumePickerOpen) {
|
|
9
18
|
return false;
|
|
@@ -49,6 +58,7 @@ function pasteTextFromClipboard(store, handlers) {
|
|
|
49
58
|
const current = store.getState();
|
|
50
59
|
if (current.exiting ||
|
|
51
60
|
current.approvalPrompt ||
|
|
61
|
+
current.jobsPickerOpen ||
|
|
52
62
|
current.modelPickerOpen ||
|
|
53
63
|
current.resumePickerOpen ||
|
|
54
64
|
current.sudoPrompt) {
|
|
@@ -202,6 +212,37 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
202
212
|
});
|
|
203
213
|
return;
|
|
204
214
|
}
|
|
215
|
+
if (state.jobsPickerOpen) {
|
|
216
|
+
if (key.escape) {
|
|
217
|
+
store.update((current) => ({
|
|
218
|
+
...current,
|
|
219
|
+
jobsPickerExpandedId: null,
|
|
220
|
+
jobsPickerOpen: false,
|
|
221
|
+
status: 'Ready',
|
|
222
|
+
}));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (key.upArrow || key.downArrow) {
|
|
226
|
+
store.update((current) => {
|
|
227
|
+
const count = (current.backgroundJobs ?? []).length;
|
|
228
|
+
const next = current.jobsPickerIndex + (key.upArrow ? -1 : 1);
|
|
229
|
+
return {
|
|
230
|
+
...current,
|
|
231
|
+
jobsPickerExpandedId: null,
|
|
232
|
+
jobsPickerIndex: Math.max(0, Math.min(next, Math.max(count - 1, 0))),
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (key.returnKey) {
|
|
238
|
+
void handlers.onJobsPickerOutput();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (key.input === 'k' && !key.ctrl && !key.meta && !key.shift) {
|
|
242
|
+
void handlers.onJobsPickerKill();
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
205
246
|
if (state.modelPickerOpen) {
|
|
206
247
|
if (key.escape) {
|
|
207
248
|
store.update((current) => ({ ...current, modelPickerOpen: false }));
|
|
@@ -267,8 +308,6 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
267
308
|
}
|
|
268
309
|
if (key.escape) {
|
|
269
310
|
if (state.busy) {
|
|
270
|
-
// With a queued message, Esc clears the queue only (turn keeps running).
|
|
271
|
-
// With nothing queued, Esc cancels the turn (Ctrl+C also cancels).
|
|
272
311
|
if (state.queuedMessage) {
|
|
273
312
|
handlers.onLiveFrameShapeChange();
|
|
274
313
|
store.update((current) => ({ ...current, queuedMessage: null }));
|
|
@@ -309,8 +348,6 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
309
348
|
return;
|
|
310
349
|
}
|
|
311
350
|
if (key.upArrow) {
|
|
312
|
-
// While busy with an empty composer, Up recalls and dequeues the queued
|
|
313
|
-
// message for editing; otherwise it walks prompt history as usual.
|
|
314
351
|
if (state.busy && state.input.trim() === '' && state.queuedMessage) {
|
|
315
352
|
handlers.onLiveFrameShapeChange();
|
|
316
353
|
store.update((current) => {
|
|
@@ -428,7 +465,7 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
428
465
|
});
|
|
429
466
|
return;
|
|
430
467
|
}
|
|
431
|
-
if (key
|
|
468
|
+
if (isClipboardImagePasteKey(key)) {
|
|
432
469
|
const current = store.getState();
|
|
433
470
|
if (current.busy)
|
|
434
471
|
return;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const PACKAGE_NAME = '@thegitai/cli';
|
|
5
|
+
const UNKNOWN_VERSION = '0.0.0';
|
|
6
|
+
export function getCliVersion() {
|
|
7
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
for (let depth = 0; depth < 6; depth++) {
|
|
9
|
+
try {
|
|
10
|
+
const pkg = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
11
|
+
if (pkg.name === PACKAGE_NAME && typeof pkg.version === 'string') {
|
|
12
|
+
return pkg.version;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
}
|
|
17
|
+
const parent = path.dirname(dir);
|
|
18
|
+
if (parent === dir)
|
|
19
|
+
break;
|
|
20
|
+
dir = parent;
|
|
21
|
+
}
|
|
22
|
+
return UNKNOWN_VERSION;
|
|
23
|
+
}
|
|
24
|
+
export function getPlatformTag() {
|
|
25
|
+
return `${process.platform}-${process.arch}`;
|
|
26
|
+
}
|
|
27
|
+
export function formatVersionLine() {
|
|
28
|
+
return `ai ${getCliVersion()} (${getPlatformTag()}, node ${process.version})`;
|
|
29
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 Max Brunsfeld
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
web-tree-sitter
|
|
2
|
+
===============
|
|
3
|
+
|
|
4
|
+
The files `web-tree-sitter.cjs` and `web-tree-sitter.wasm` in this directory are
|
|
5
|
+
vendored, unmodified, from the `web-tree-sitter` npm package, version 0.26.6.
|
|
6
|
+
|
|
7
|
+
They are bundled here (rather than installed as a runtime dependency) so the
|
|
8
|
+
published `@thegitai/cli` package declares zero runtime dependencies while still
|
|
9
|
+
shipping local tree-sitter code intelligence. They are used only as a local
|
|
10
|
+
parsing runtime; no part of web-tree-sitter is modified.
|
|
11
|
+
|
|
12
|
+
Upstream: https://github.com/tree-sitter/tree-sitter
|
|
13
|
+
License: MIT (see the adjacent LICENSE file)
|