@thegitai/cli 1.0.0-preview.11 → 1.0.0-preview.12

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.
@@ -160,7 +160,7 @@ function publicStatusMessage(data) {
160
160
  ? event.toolName
161
161
  : 'tool';
162
162
  if (event.phase === 'thinking')
163
- return 'Thinking...';
163
+ return 'Exploring options...';
164
164
  if (event.phase === 'analyzing_image') {
165
165
  return (event.imageCount ?? 1) > 1 ? 'Analyzing images...' : 'Analyzing image...';
166
166
  }
@@ -1,5 +1,5 @@
1
1
  import { createRatatuiBridge } from './tui/bridge.js';
2
- import { buildTuiFrame, formatJobElapsed, formatTodoProgress, renderTranscriptEntryLines, } from './tui/build-frame.js';
2
+ import { buildTuiFrame, formatJobElapsed, formatTodoProgress, pickThinkingFallbackPhrase, renderTranscriptEntryLines, THINKING_FALLBACK_PHRASES, } from './tui/build-frame.js';
3
3
  import { createTerminalTitleController } from './tui/terminal-title.js';
4
4
  import { captureTerminalWrites, releaseTerminalWrites, } from './tui/terminal-writes.js';
5
5
  export { getSlashCommandSuggestions } from './tui/build-frame.js';
@@ -1074,10 +1074,21 @@ function splitThinkingLines(text) {
1074
1074
  .filter(Boolean));
1075
1075
  }
1076
1076
  const LIVE_STATUS_PANEL_MAX_CHARS = 72;
1077
+ const TOOL_PROGRESS_STATUS_PATTERN = /^(?:Tool:\s|Running\s\S+(?:\slocally)?\.\.\.$)/i;
1078
+ export function isToolProgressStatus(status) {
1079
+ return TOOL_PROGRESS_STATUS_PATTERN.test(String(status ?? '').trim());
1080
+ }
1081
+ function holdOrPickFallback(currentTitle) {
1082
+ return THINKING_FALLBACK_PHRASES.includes(currentTitle.trim())
1083
+ ? currentTitle
1084
+ : pickThinkingFallbackPhrase();
1085
+ }
1077
1086
  function liveStatusPanelTitle(status) {
1078
1087
  const text = String(status ?? '').trim();
1079
1088
  if (!text || text.includes('\n'))
1080
1089
  return '';
1090
+ if (isToolProgressStatus(text))
1091
+ return '';
1081
1092
  return text.length <= LIVE_STATUS_PANEL_MAX_CHARS ? text : '';
1082
1093
  }
1083
1094
  function thinkingPanelFromStatus(status) {
@@ -1103,7 +1114,7 @@ function thinkingPanelFromStatus(status) {
1103
1114
  const bodyLines = rawLines.length > 1 && rawLines[0].length <= 72 ? rawLines.slice(1) : rawLines;
1104
1115
  return {
1105
1116
  title,
1106
- notes: splitThinkingLines(bodyLines.join('\n')).slice(-3),
1117
+ notes: splitThinkingLines(bodyLines.join('\n')).slice(0, 3),
1107
1118
  };
1108
1119
  }
1109
1120
  export const EXIT_CTRL_C_CONFIRM_MESSAGE = 'Press Ctrl+C again to quit.';
@@ -1339,6 +1350,36 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1339
1350
  let exitCtrlCArmed = false;
1340
1351
  let exitCtrlCTimer = null;
1341
1352
  let transientStatusTimer = null;
1353
+ const THINKING_MIN_DWELL_MS = 3_000;
1354
+ let thinkingAppliedAt = 0;
1355
+ let pendingThinking = null;
1356
+ const applyThinking = (next) => {
1357
+ thinkingAppliedAt = Date.now();
1358
+ pendingThinking = null;
1359
+ store.update((current) => ({
1360
+ ...current,
1361
+ thinkingTitle: next.title || holdOrPickFallback(current.thinkingTitle),
1362
+ thinkingNotes: next.notes,
1363
+ }));
1364
+ };
1365
+ const queueThinkingUpdate = (next) => {
1366
+ if (Date.now() - thinkingAppliedAt >= THINKING_MIN_DWELL_MS) {
1367
+ applyThinking(next);
1368
+ return;
1369
+ }
1370
+ pendingThinking = next;
1371
+ };
1372
+ const flushPendingThinking = () => {
1373
+ if (!pendingThinking)
1374
+ return;
1375
+ if (Date.now() - thinkingAppliedAt < THINKING_MIN_DWELL_MS)
1376
+ return;
1377
+ applyThinking(pendingThinking);
1378
+ };
1379
+ const resetThinkingPacer = () => {
1380
+ thinkingAppliedAt = 0;
1381
+ pendingThinking = null;
1382
+ };
1342
1383
  const done = new Promise((resolve) => {
1343
1384
  resolveDone = resolve;
1344
1385
  });
@@ -1538,6 +1579,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1538
1579
  dismissPendingApproval();
1539
1580
  dismissPendingSudoPassword();
1540
1581
  activeTurnGeneration += 1;
1582
+ resetThinkingPacer();
1541
1583
  activeTurnAbort?.abort();
1542
1584
  activeTurnAbort = null;
1543
1585
  cancelActiveCommand();
@@ -2335,6 +2377,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2335
2377
  };
2336
2378
  pendingTurnEntries = [];
2337
2379
  queueTurnEntry(userEntry);
2380
+ resetThinkingPacer();
2338
2381
  store.update((current) => ({
2339
2382
  ...current,
2340
2383
  activeTurnInput: input,
@@ -2474,10 +2517,12 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2474
2517
  store.update((current) => ({
2475
2518
  ...current,
2476
2519
  status: message,
2477
- thinkingTitle: panel ? panel.title : liveStatusPanelTitle(message),
2478
- thinkingNotes: panel ? panel.notes : [],
2479
2520
  tokenUsage: formatClientTokenUsage(current.busySince == null ? null : Date.now() - current.busySince, latestUsageSummary),
2480
2521
  }));
2522
+ queueThinkingUpdate({
2523
+ title: panel ? panel.title : liveStatusPanelTitle(message),
2524
+ notes: panel ? panel.notes : [],
2525
+ });
2481
2526
  };
2482
2527
  session.onContextLog = (message) => {
2483
2528
  store.update((current) => ({
@@ -2619,6 +2664,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2619
2664
  const spinnerTimer = setInterval(() => {
2620
2665
  spinnerFrame = (spinnerFrame + 1) % 6;
2621
2666
  if (store.getState().busy) {
2667
+ flushPendingThinking();
2622
2668
  renderCurrentFrame();
2623
2669
  }
2624
2670
  }, 120);
@@ -238,13 +238,35 @@ function formatRelativeTime(isoDate) {
238
238
  return `${hours}h ago`;
239
239
  return `${Math.floor(hours / 24)}d ago`;
240
240
  }
241
- function todoItemLine(item, width) {
241
+ const TODO_IN_PROGRESS_FRAMES = [
242
+ '\u25CB',
243
+ '\u25D4',
244
+ '\u25D1',
245
+ '\u25D5',
246
+ '\u25CF',
247
+ ];
248
+ const TODO_IN_PROGRESS_STATIC_GLYPH = '\u25D1';
249
+ export const TODO_IN_PROGRESS_STEP_MS = 1_500;
250
+ export function todoProgressTick(nowMs) {
251
+ return Math.floor(nowMs / TODO_IN_PROGRESS_STEP_MS);
252
+ }
253
+ function todoInProgressGlyph(progressTick) {
254
+ if (progressTick === null)
255
+ return TODO_IN_PROGRESS_STATIC_GLYPH;
256
+ const index = ((progressTick % TODO_IN_PROGRESS_FRAMES.length) +
257
+ TODO_IN_PROGRESS_FRAMES.length) %
258
+ TODO_IN_PROGRESS_FRAMES.length;
259
+ return TODO_IN_PROGRESS_FRAMES[index];
260
+ }
261
+ function todoItemLine(item, width, progressTick = null) {
242
262
  const text = fitLine(item.text, Math.max(8, width - 5));
243
263
  if (item.status === 'completed') {
244
264
  return line(span(' ✔ ', { color: 'green' }), span(text, { color: 'gray', dim: true }));
245
265
  }
246
266
  if (item.status === 'in_progress') {
247
- return line(span(' ◐ ', { color: TODO_IN_PROGRESS_COLOR }), span(text, { color: TODO_IN_PROGRESS_COLOR, bold: true }));
267
+ return line(span(` ${todoInProgressGlyph(progressTick)} `, {
268
+ color: TODO_IN_PROGRESS_COLOR,
269
+ }), span(text, { color: TODO_IN_PROGRESS_COLOR, bold: true }));
248
270
  }
249
271
  return line(span(' ○ ', { color: 'gray' }), span(text));
250
272
  }
@@ -252,7 +274,7 @@ export function formatTodoProgress(items) {
252
274
  const done = items.filter((item) => item.status === 'completed').length;
253
275
  return `${done}/${items.length} done`;
254
276
  }
255
- export function renderTodoListLines(items, width, { header = false } = {}) {
277
+ export function renderTodoListLines(items, width, { header = false, progressTick = null } = {}) {
256
278
  if (items.length === 0)
257
279
  return [];
258
280
  const lines = [];
@@ -277,7 +299,7 @@ export function renderTodoListLines(items, width, { header = false } = {}) {
277
299
  const budget = itemRowBudget - (collapsedDone > 0 ? 1 : 0);
278
300
  const visible = remaining.length > budget ? remaining.slice(0, budget - 1) : remaining;
279
301
  for (const item of visible) {
280
- lines.push(todoItemLine(item, width));
302
+ lines.push(todoItemLine(item, width, progressTick));
281
303
  }
282
304
  if (remaining.length > visible.length) {
283
305
  lines.push(plainLine(` … +${remaining.length - visible.length} more`, {
@@ -512,17 +534,48 @@ function buildJobsPickerLines(state, width, nowMs) {
512
534
  }));
513
535
  return lines;
514
536
  }
537
+ export const THINKING_FALLBACK_PHRASES = [
538
+ 'Working out where to start...',
539
+ 'Deciding what comes first...',
540
+ 'Lining up the pieces...',
541
+ 'Untangling the details...',
542
+ 'Deciding what not to touch...',
543
+ 'Checking what this would break...',
544
+ 'Choosing the smaller change...',
545
+ 'Picking the least clever option...',
546
+ 'Resisting the obvious answer...',
547
+ 'Trying the boring explanation first...',
548
+ 'Checking whether the assumption holds...',
549
+ 'Asking what would have to be true...',
550
+ 'Reading it the way the machine would...',
551
+ 'Testing the story against the code...',
552
+ 'Working out what actually changed...',
553
+ 'Finding the smallest thing that explains it...',
554
+ 'Looking for the part that is not settled yet...',
555
+ 'Making sure this is the simple version...',
556
+ ];
557
+ export function pickThinkingFallbackPhrase() {
558
+ const index = Math.floor(Math.random() * THINKING_FALLBACK_PHRASES.length);
559
+ return THINKING_FALLBACK_PHRASES[index];
560
+ }
561
+ function withProgressEllipsis(title) {
562
+ const text = title.trim();
563
+ if (!text)
564
+ return text;
565
+ return /(?:\.\.\.|…)$/.test(text) ? text : `${text}...`;
566
+ }
515
567
  function thinkingHeaderLine(spinnerFrame, title, width) {
516
568
  const spinner = `${BRAILLE_SPINNER_FRAMES[spinnerFrame % BRAILLE_SPINNER_FRAMES.length]} `;
517
- const fittedTitle = title
518
- ? fitLine(title, Math.max(8, width - spinner.length - 'Thinking'.length - 3))
569
+ const decorated = withProgressEllipsis(title);
570
+ const fittedTitle = decorated
571
+ ? fitLine(decorated, Math.max(8, width - spinner.length - 'Thinking'.length - 3))
519
572
  : '';
520
573
  return line(span(spinner, { color: 'green' }), span('Thinking', { color: 'green', bold: true }), ...(fittedTitle ? [span(` · ${fittedTitle}`, { color: 'ansi256(248)' })] : []));
521
574
  }
522
575
  function thinkingNoteLine(note, width) {
523
576
  return line(span('│ ', { color: 'green' }), span(fitLine(note, Math.max(8, width - 3)), { color: 'ansi256(248)' }));
524
577
  }
525
- function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
578
+ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds, nowMs) {
526
579
  if (!state.busy)
527
580
  return [];
528
581
  const lines = [plainLine('')];
@@ -561,7 +614,10 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
561
614
  const todos = state.todos ?? [];
562
615
  if (todos.length > 0) {
563
616
  lines.push(plainLine(''));
564
- lines.push(...renderTodoListLines(todos, width, { header: true }));
617
+ lines.push(...renderTodoListLines(todos, width, {
618
+ header: true,
619
+ progressTick: todoProgressTick(nowMs),
620
+ }));
565
621
  }
566
622
  const visibleTitle = state.thinkingTitle.trim();
567
623
  const visibleNotes = state.thinkingNotes.filter(Boolean);
@@ -571,11 +627,12 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
571
627
  const minimalText = visibleTitle && visibleTitle !== 'Thinking'
572
628
  ? visibleTitle
573
629
  : (visibleNotes[visibleNotes.length - 1] ?? '');
574
- lines.push(thinkingHeaderLine(spinnerFrame, minimalText, width));
630
+ lines.push(thinkingHeaderLine(spinnerFrame, minimalText || THINKING_FALLBACK_PHRASES[0], width));
575
631
  }
576
632
  else {
577
- lines.push(thinkingHeaderLine(spinnerFrame, visibleTitle, width));
578
- for (const note of visibleNotes.slice(-THINKING_NOTE_PREVIEW_ROWS)) {
633
+ const headerTitle = visibleTitle && visibleTitle !== 'Thinking' ? visibleTitle : '';
634
+ lines.push(thinkingHeaderLine(spinnerFrame, headerTitle || THINKING_FALLBACK_PHRASES[0], width));
635
+ for (const note of visibleNotes.slice(0, THINKING_NOTE_PREVIEW_ROWS)) {
579
636
  lines.push(thinkingNoteLine(note, width));
580
637
  }
581
638
  }
@@ -960,7 +1017,7 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
960
1017
  transcriptLines.push(...block);
961
1018
  });
962
1019
  const sections = [];
963
- const liveLines = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds);
1020
+ const liveLines = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds, nowMs);
964
1021
  if (liveLines.length > 0) {
965
1022
  sections.push({ kind: 'live', lines: liveLines });
966
1023
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thegitai/cli",
3
- "version": "1.0.0-preview.11",
3
+ "version": "1.0.0-preview.12",
4
4
  "description": "TheGitAI is an AI coding agent for your terminal. It indexes your repository, writes and edits files, runs commands, and builds features with you.",
5
5
  "keywords": [
6
6
  "ai",
@@ -37,10 +37,10 @@
37
37
  "@lydell/node-pty-linux-x64": "1.1.0",
38
38
  "@lydell/node-pty-win32-arm64": "1.1.0",
39
39
  "@lydell/node-pty-win32-x64": "1.1.0",
40
- "@thegitai/tui-darwin-arm64": "1.0.0-preview.11",
41
- "@thegitai/tui-darwin-x64": "1.0.0-preview.11",
42
- "@thegitai/tui-linux-x64": "1.0.0-preview.11",
43
- "@thegitai/tui-win32-x64": "1.0.0-preview.11",
40
+ "@thegitai/tui-darwin-arm64": "1.0.0-preview.12",
41
+ "@thegitai/tui-darwin-x64": "1.0.0-preview.12",
42
+ "@thegitai/tui-linux-x64": "1.0.0-preview.12",
43
+ "@thegitai/tui-win32-x64": "1.0.0-preview.12",
44
44
  "@vscode/ripgrep": "1.18.0"
45
45
  },
46
46
  "publishConfig": {