@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.
Files changed (55) hide show
  1. package/README.md +37 -2
  2. package/dist/bin/ai.js +148 -75
  3. package/dist/parsers/NOTICE +18 -0
  4. package/dist/src/agent-mode.js +5 -0
  5. package/dist/src/api/auth.js +6 -4
  6. package/dist/src/api/browser-login.js +7 -41
  7. package/dist/src/api/chat.js +77 -20
  8. package/dist/src/api/http.js +81 -4
  9. package/dist/src/api/models.js +26 -18
  10. package/dist/src/artifact-policy.js +12 -0
  11. package/dist/src/background-jobs.js +410 -0
  12. package/dist/src/cli-args.js +60 -0
  13. package/dist/src/client-environment.js +129 -0
  14. package/dist/src/colors.js +50 -0
  15. package/dist/src/core/clipboard.js +75 -0
  16. package/dist/src/core/image-path-extractor.js +144 -0
  17. package/dist/src/edit-journal.js +39 -6
  18. package/dist/src/executor.js +48 -12
  19. package/dist/src/help-text.js +24 -5
  20. package/dist/src/markdown-renderer.js +1 -1
  21. package/dist/src/patcher.js +17 -2
  22. package/dist/src/scanner.js +58 -17
  23. package/dist/src/scratch-dir.js +57 -0
  24. package/dist/src/secret-preview.js +0 -10
  25. package/dist/src/session-safety.js +64 -31
  26. package/dist/src/session-store.js +0 -1
  27. package/dist/src/todo-list.js +106 -0
  28. package/dist/src/tool-executor.js +164 -18
  29. package/dist/src/tools/delete-file.js +1 -1
  30. package/dist/src/tools/index.js +8 -0
  31. package/dist/src/tools/patch-file.js +16 -2
  32. package/dist/src/tools/path-suggest.js +139 -0
  33. package/dist/src/tools/read-document.js +15 -4
  34. package/dist/src/tools/read-file.js +23 -7
  35. package/dist/src/tools/replace-document-text.js +234 -0
  36. package/dist/src/tools/restore-checkpoint.js +1 -1
  37. package/dist/src/tools/run-command.js +83 -16
  38. package/dist/src/tools/run-node-script.js +3 -1
  39. package/dist/src/tools/shell-job-kill.js +48 -0
  40. package/dist/src/tools/shell-job-output.js +51 -0
  41. package/dist/src/tools/str-replace.js +16 -2
  42. package/dist/src/tools/undo-edit.js +7 -5
  43. package/dist/src/tools/update-todos.js +27 -0
  44. package/dist/src/tools/write-file.js +14 -1
  45. package/dist/src/tree-sitter-runtime.js +8 -1
  46. package/dist/src/ui/repl.js +315 -24
  47. package/dist/src/ui/tui/bridge.js +2 -6
  48. package/dist/src/ui/tui/build-frame.js +224 -25
  49. package/dist/src/ui/tui/shell-input.js +42 -5
  50. package/dist/src/version.js +29 -0
  51. package/dist/vendor/web-tree-sitter/LICENSE +21 -0
  52. package/dist/vendor/web-tree-sitter/NOTICE +13 -0
  53. package/dist/vendor/web-tree-sitter/web-tree-sitter.cjs +4063 -0
  54. package/dist/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
  55. 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
- return match?.[0] ?? '';
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(truncate(diffLine.content || ' ', width - 4), {
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
- : 'Enter sends • Shift+Tab mode • Esc cancel turn • Ctrl+C quits';
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
- // Composer stays visible while busy unless a blocking overlay is active. When
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.ctrl && key.input === 'v') {
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)