@thegitai/cli 1.0.0-beta.9 → 1.0.0-preview.1
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 +36 -2
- package/dist/bin/ai.js +134 -18
- package/dist/parsers/NOTICE +18 -0
- package/dist/src/agent-mode.js +5 -0
- package/dist/src/api/auth.js +3 -3
- package/dist/src/api/browser-login.js +0 -16
- package/dist/src/api/chat.js +57 -11
- package/dist/src/api/http.js +49 -1
- package/dist/src/api/models.js +26 -20
- 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 +11 -6
- package/dist/src/markdown-renderer.js +1 -1
- package/dist/src/patcher.js +1 -3
- package/dist/src/scanner.js +50 -12
- package/dist/src/scratch-dir.js +57 -0
- package/dist/src/secret-preview.js +0 -10
- package/dist/src/session-safety.js +0 -19
- package/dist/src/session-store.js +0 -1
- package/dist/src/todo-list.js +106 -0
- package/dist/src/tool-executor.js +159 -18
- package/dist/src/tools/delete-file.js +1 -1
- package/dist/src/tools/index.js +6 -0
- package/dist/src/tools/patch-file.js +3 -2
- 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 +14 -7
- package/dist/src/tools/replace-document-text.js +3 -11
- 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 +3 -2
- 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 +1 -1
- package/dist/src/tree-sitter-runtime.js +8 -1
- package/dist/src/ui/repl.js +313 -23
- package/dist/src/ui/tui/bridge.js +0 -4
- package/dist/src/ui/tui/build-frame.js +220 -24
- package/dist/src/ui/tui/shell-input.js +33 -4
- package/dist/src/ui/tui/terminal-title.js +81 -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 +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',
|
|
@@ -329,6 +424,95 @@ function composerFooterLines(state) {
|
|
|
329
424
|
lines.push(line(...footerSpans));
|
|
330
425
|
return lines;
|
|
331
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
|
+
}
|
|
332
516
|
function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
333
517
|
if (!state.busy)
|
|
334
518
|
return [];
|
|
@@ -342,33 +526,45 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
342
526
|
}, width));
|
|
343
527
|
lines.push(plainLine(''));
|
|
344
528
|
}
|
|
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
529
|
const toolEntries = state.workingTools.slice(-WORKING_TOOL_PREVIEW_ROWS);
|
|
358
530
|
if (toolEntries.length > 0) {
|
|
359
|
-
lines.push(plainLine(''));
|
|
360
531
|
for (const entry of toolEntries) {
|
|
361
532
|
const color = getEntryColor(entry.kind);
|
|
362
533
|
const body = entry.body.split('\n')[0]?.trim();
|
|
363
534
|
lines.push(line(span('● ', { color }), span(entry.title, { color, bold: true }), ...(body ? [span(` ${truncate(body, 140)}`, { color })] : [])));
|
|
364
535
|
}
|
|
536
|
+
lines.push(plainLine(''));
|
|
365
537
|
}
|
|
366
538
|
const logLines = state.commandLog.slice(-COMMAND_PREVIEW_LINES);
|
|
367
539
|
if (logLines.length > 0) {
|
|
368
|
-
lines.push(plainLine(''));
|
|
540
|
+
lines.push(plainLine('⋮ output', { color: 'gray', dim: true }));
|
|
369
541
|
for (const outputLine of logLines) {
|
|
370
542
|
lines.push(plainLine(outputLine, { color: 'gray', dim: true }));
|
|
371
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
|
+
const todos = state.todos ?? [];
|
|
548
|
+
if (todos.length > 0) {
|
|
549
|
+
lines.push(plainLine(''));
|
|
550
|
+
lines.push(...renderTodoListLines(todos, width, { header: true }));
|
|
551
|
+
}
|
|
552
|
+
const visibleTitle = state.thinkingTitle.trim();
|
|
553
|
+
const visibleNotes = state.thinkingNotes.filter(Boolean);
|
|
554
|
+
if (visibleTitle || visibleNotes.length > 0) {
|
|
555
|
+
lines.push(plainLine(''));
|
|
556
|
+
if (todos.length > 0) {
|
|
557
|
+
const minimalText = visibleTitle && visibleTitle !== 'Thinking'
|
|
558
|
+
? visibleTitle
|
|
559
|
+
: (visibleNotes[visibleNotes.length - 1] ?? '');
|
|
560
|
+
lines.push(thinkingHeaderLine(spinnerFrame, minimalText, width));
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
lines.push(thinkingHeaderLine(spinnerFrame, visibleTitle, width));
|
|
564
|
+
for (const note of visibleNotes.slice(-THINKING_NOTE_PREVIEW_ROWS)) {
|
|
565
|
+
lines.push(thinkingNoteLine(note, width));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
372
568
|
}
|
|
373
569
|
lines.push(plainLine(''));
|
|
374
570
|
return lines;
|
|
@@ -539,7 +735,7 @@ function buildCommandPalettePanel(suggestions, selectedIndex, width) {
|
|
|
539
735
|
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
736
|
return [...margin, ...body, ...margin];
|
|
541
737
|
}
|
|
542
|
-
function buildOverlayLines(state, width) {
|
|
738
|
+
function buildOverlayLines(state, width, nowMs) {
|
|
543
739
|
const lines = [];
|
|
544
740
|
const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
|
|
545
741
|
const innerWidth = Math.max(1, panelWidth - 4);
|
|
@@ -614,6 +810,9 @@ function buildOverlayLines(state, width) {
|
|
|
614
810
|
const options = buildModelPickerOptions(state.currentModelId, state.serverModels);
|
|
615
811
|
lines.push(...buildModelPickerPanel(options, state.modelPickerIndex, width));
|
|
616
812
|
}
|
|
813
|
+
if (state.jobsPickerOpen) {
|
|
814
|
+
lines.push(...buildJobsPickerLines(state, width, nowMs));
|
|
815
|
+
}
|
|
617
816
|
if (state.resumePickerOpen) {
|
|
618
817
|
const filtered = filterResumeSessions(state.resumePickerSessions, state.resumePickerFilter, state.serverModels);
|
|
619
818
|
lines.push(plainLine('Resume a previous session', { color: 'cyan', bold: true }));
|
|
@@ -655,7 +854,7 @@ function sliceTranscriptLines(lines, maxLines, scrollOffset) {
|
|
|
655
854
|
const start = Math.max(0, lines.length - maxLines - offset);
|
|
656
855
|
return lines.slice(start, start + maxLines);
|
|
657
856
|
}
|
|
658
|
-
export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds) {
|
|
857
|
+
export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, nowMs = 0) {
|
|
659
858
|
const contentWidth = Math.max(20, Math.floor(cols * 0.95) - 2);
|
|
660
859
|
const gutter = Math.max(Math.floor((cols - contentWidth) / 2), 0);
|
|
661
860
|
const transcriptBlocks = state.transcript.map((entry) => renderTranscriptEntryLines(entry, contentWidth));
|
|
@@ -672,10 +871,7 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds) {
|
|
|
672
871
|
sections.push({ kind: 'live', lines: liveLines });
|
|
673
872
|
}
|
|
674
873
|
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) {
|
|
874
|
+
if (!state.resumePickerOpen && !state.modelPickerOpen && !state.jobsPickerOpen && !overlayActive) {
|
|
679
875
|
const composerLines = [];
|
|
680
876
|
if (state.queuedMessage) {
|
|
681
877
|
const preview = truncate(state.queuedMessage.body.trim().replace(/\s+/g, ' '), 60);
|
|
@@ -696,7 +892,7 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds) {
|
|
|
696
892
|
lines: [...composerLines, plainLine(''), ...composerFooterLines(state)],
|
|
697
893
|
});
|
|
698
894
|
}
|
|
699
|
-
const overlayLines = buildOverlayLines(state, contentWidth);
|
|
895
|
+
const overlayLines = buildOverlayLines(state, contentWidth, nowMs);
|
|
700
896
|
if (overlayLines.length > 0) {
|
|
701
897
|
sections.push({ kind: 'overlay', lines: overlayLines });
|
|
702
898
|
}
|
|
@@ -12,6 +12,7 @@ function isClipboardImagePasteKey(key) {
|
|
|
12
12
|
function shouldShowCommandPalette(state) {
|
|
13
13
|
if (state.busy ||
|
|
14
14
|
state.exiting ||
|
|
15
|
+
state.jobsPickerOpen ||
|
|
15
16
|
state.modelPickerOpen ||
|
|
16
17
|
state.resumePickerOpen) {
|
|
17
18
|
return false;
|
|
@@ -57,6 +58,7 @@ function pasteTextFromClipboard(store, handlers) {
|
|
|
57
58
|
const current = store.getState();
|
|
58
59
|
if (current.exiting ||
|
|
59
60
|
current.approvalPrompt ||
|
|
61
|
+
current.jobsPickerOpen ||
|
|
60
62
|
current.modelPickerOpen ||
|
|
61
63
|
current.resumePickerOpen ||
|
|
62
64
|
current.sudoPrompt) {
|
|
@@ -210,6 +212,37 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
210
212
|
});
|
|
211
213
|
return;
|
|
212
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
|
+
}
|
|
213
246
|
if (state.modelPickerOpen) {
|
|
214
247
|
if (key.escape) {
|
|
215
248
|
store.update((current) => ({ ...current, modelPickerOpen: false }));
|
|
@@ -275,8 +308,6 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
275
308
|
}
|
|
276
309
|
if (key.escape) {
|
|
277
310
|
if (state.busy) {
|
|
278
|
-
// With a queued message, Esc clears the queue only (turn keeps running).
|
|
279
|
-
// With nothing queued, Esc cancels the turn (Ctrl+C also cancels).
|
|
280
311
|
if (state.queuedMessage) {
|
|
281
312
|
handlers.onLiveFrameShapeChange();
|
|
282
313
|
store.update((current) => ({ ...current, queuedMessage: null }));
|
|
@@ -317,8 +348,6 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
317
348
|
return;
|
|
318
349
|
}
|
|
319
350
|
if (key.upArrow) {
|
|
320
|
-
// While busy with an empty composer, Up recalls and dequeues the queued
|
|
321
|
-
// message for editing; otherwise it walks prompt history as usual.
|
|
322
351
|
if (state.busy && state.input.trim() === '' && state.queuedMessage) {
|
|
323
352
|
handlers.onLiveFrameShapeChange();
|
|
324
353
|
store.update((current) => {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const BRAILLE_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴'];
|
|
2
|
+
const TITLE_BRAND = 'TheGitAI';
|
|
3
|
+
const TITLE_MARK_PREFIX = '❯_';
|
|
4
|
+
export const TERMINAL_TITLE_SPINNER_MS = 500;
|
|
5
|
+
export function resolveTerminalTitleState(input) {
|
|
6
|
+
if (input.awaitingReview)
|
|
7
|
+
return 'review';
|
|
8
|
+
if (input.busy)
|
|
9
|
+
return 'wip';
|
|
10
|
+
return 'clean';
|
|
11
|
+
}
|
|
12
|
+
export function formatTerminalTitle(state, spinnerFrame = 0) {
|
|
13
|
+
if (state === 'review')
|
|
14
|
+
return `${TITLE_MARK_PREFIX}▸ ${TITLE_BRAND}`;
|
|
15
|
+
if (state === 'wip') {
|
|
16
|
+
const frame = BRAILLE_SPINNER_FRAMES[spinnerFrame % BRAILLE_SPINNER_FRAMES.length];
|
|
17
|
+
return `${TITLE_MARK_PREFIX}${frame} ${TITLE_BRAND}`;
|
|
18
|
+
}
|
|
19
|
+
return `${TITLE_MARK_PREFIX}● ${TITLE_BRAND}`;
|
|
20
|
+
}
|
|
21
|
+
export function writeTerminalTitle(title, stream = process.stdout) {
|
|
22
|
+
if (!('isTTY' in stream) || !stream.isTTY)
|
|
23
|
+
return;
|
|
24
|
+
stream.write(`\x1b]0;${title}\x07`);
|
|
25
|
+
}
|
|
26
|
+
export function createTerminalTitleController(options) {
|
|
27
|
+
const write = options?.write ?? writeTerminalTitle;
|
|
28
|
+
let currentState = 'clean';
|
|
29
|
+
let spinnerFrame = 0;
|
|
30
|
+
let lastTitle = '';
|
|
31
|
+
let timer = null;
|
|
32
|
+
const paint = () => {
|
|
33
|
+
const title = formatTerminalTitle(currentState, spinnerFrame);
|
|
34
|
+
if (title === lastTitle)
|
|
35
|
+
return;
|
|
36
|
+
lastTitle = title;
|
|
37
|
+
write(title);
|
|
38
|
+
};
|
|
39
|
+
const stopTimer = () => {
|
|
40
|
+
if (!timer)
|
|
41
|
+
return;
|
|
42
|
+
clearInterval(timer);
|
|
43
|
+
timer = null;
|
|
44
|
+
};
|
|
45
|
+
const startTimer = () => {
|
|
46
|
+
if (timer)
|
|
47
|
+
return;
|
|
48
|
+
timer = setInterval(() => {
|
|
49
|
+
spinnerFrame = (spinnerFrame + 1) % BRAILLE_SPINNER_FRAMES.length;
|
|
50
|
+
paint();
|
|
51
|
+
}, TERMINAL_TITLE_SPINNER_MS);
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
sync(input) {
|
|
55
|
+
const next = resolveTerminalTitleState(input);
|
|
56
|
+
if (next !== currentState) {
|
|
57
|
+
currentState = next;
|
|
58
|
+
if (next === 'wip') {
|
|
59
|
+
spinnerFrame = 0;
|
|
60
|
+
startTimer();
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
stopTimer();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (next === 'wip') {
|
|
67
|
+
startTimer();
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
stopTimer();
|
|
71
|
+
}
|
|
72
|
+
paint();
|
|
73
|
+
},
|
|
74
|
+
dispose() {
|
|
75
|
+
stopTimer();
|
|
76
|
+
currentState = 'clean';
|
|
77
|
+
spinnerFrame = 0;
|
|
78
|
+
paint();
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
package/dist/src/version.js
CHANGED
|
@@ -3,11 +3,6 @@ import path from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
const PACKAGE_NAME = '@thegitai/cli';
|
|
5
5
|
const UNKNOWN_VERSION = '0.0.0';
|
|
6
|
-
// Resolve the package version at runtime by walking up from this module to the
|
|
7
|
-
// nearest package.json named @thegitai/cli. This works in both layouts: the
|
|
8
|
-
// compiled binary (dist/bin/ai.js → ../../package.json) and the source tree run
|
|
9
|
-
// under tsx in tests (src/version.ts → ../package.json). The name guard avoids
|
|
10
|
-
// picking up an unrelated manifest if the file is ever nested elsewhere.
|
|
11
6
|
export function getCliVersion() {
|
|
12
7
|
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
13
8
|
for (let depth = 0; depth < 6; depth++) {
|
|
@@ -18,7 +13,6 @@ export function getCliVersion() {
|
|
|
18
13
|
}
|
|
19
14
|
}
|
|
20
15
|
catch {
|
|
21
|
-
// No package.json at this level (or unreadable); keep walking up.
|
|
22
16
|
}
|
|
23
17
|
const parent = path.dirname(dir);
|
|
24
18
|
if (parent === dir)
|
|
@@ -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)
|