@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.
Files changed (55) hide show
  1. package/README.md +36 -2
  2. package/dist/bin/ai.js +134 -18
  3. package/dist/parsers/NOTICE +18 -0
  4. package/dist/src/agent-mode.js +5 -0
  5. package/dist/src/api/auth.js +3 -3
  6. package/dist/src/api/browser-login.js +0 -16
  7. package/dist/src/api/chat.js +57 -11
  8. package/dist/src/api/http.js +49 -1
  9. package/dist/src/api/models.js +26 -20
  10. package/dist/src/artifact-policy.js +3 -0
  11. package/dist/src/background-jobs.js +410 -0
  12. package/dist/src/cli-args.js +0 -5
  13. package/dist/src/client-environment.js +2 -0
  14. package/dist/src/colors.js +50 -0
  15. package/dist/src/core/clipboard.js +19 -0
  16. package/dist/src/core/image-path-extractor.js +144 -0
  17. package/dist/src/executor.js +48 -12
  18. package/dist/src/help-text.js +11 -6
  19. package/dist/src/markdown-renderer.js +1 -1
  20. package/dist/src/patcher.js +1 -3
  21. package/dist/src/scanner.js +50 -12
  22. package/dist/src/scratch-dir.js +57 -0
  23. package/dist/src/secret-preview.js +0 -10
  24. package/dist/src/session-safety.js +0 -19
  25. package/dist/src/session-store.js +0 -1
  26. package/dist/src/todo-list.js +106 -0
  27. package/dist/src/tool-executor.js +159 -18
  28. package/dist/src/tools/delete-file.js +1 -1
  29. package/dist/src/tools/index.js +6 -0
  30. package/dist/src/tools/patch-file.js +3 -2
  31. package/dist/src/tools/path-suggest.js +81 -8
  32. package/dist/src/tools/read-document.js +2 -2
  33. package/dist/src/tools/read-file.js +14 -7
  34. package/dist/src/tools/replace-document-text.js +3 -11
  35. package/dist/src/tools/restore-checkpoint.js +1 -1
  36. package/dist/src/tools/run-command.js +83 -16
  37. package/dist/src/tools/run-node-script.js +3 -1
  38. package/dist/src/tools/shell-job-kill.js +48 -0
  39. package/dist/src/tools/shell-job-output.js +51 -0
  40. package/dist/src/tools/str-replace.js +3 -2
  41. package/dist/src/tools/undo-edit.js +1 -1
  42. package/dist/src/tools/update-todos.js +27 -0
  43. package/dist/src/tools/write-file.js +1 -1
  44. package/dist/src/tree-sitter-runtime.js +8 -1
  45. package/dist/src/ui/repl.js +313 -23
  46. package/dist/src/ui/tui/bridge.js +0 -4
  47. package/dist/src/ui/tui/build-frame.js +220 -24
  48. package/dist/src/ui/tui/shell-input.js +33 -4
  49. package/dist/src/ui/tui/terminal-title.js +81 -0
  50. package/dist/src/version.js +0 -6
  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',
@@ -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
- // Composer stays visible while busy unless a blocking overlay is active. When
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
+ }
@@ -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)