@thegitai/cli 1.0.0-preview.15 → 1.0.0-preview.17

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.
@@ -2,7 +2,7 @@ import { agentModeLabel } from '../../agent-mode.js';
2
2
  import { singleLinePreview, truncate } from '../../utils.js';
3
3
  import { formatClientTokenUsage } from '../repl.js';
4
4
  import { renderFormattedBodyLines, renderPreformattedBodyLines, } from './markdown-render.js';
5
- import { displayWidth, line, plainLine, sliceToWidth, span, wrapText, } from './text.js';
5
+ import { displayWidth, line, padToWidth, plainLine, sliceToWidth, span, wrapText, } from './text.js';
6
6
  import { buildUserInputOverlayLines, } from './user-input.js';
7
7
  const WORKING_CLOCK_ICON = '◷';
8
8
  const BRAILLE_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴'];
@@ -286,6 +286,46 @@ function todoItemLine(item, width, progressTick = null) {
286
286
  }
287
287
  return line(span(' ○ ', { color: 'gray' }), span(text));
288
288
  }
289
+ const TURN_MESSAGE_PANEL_MAX_ROWS = 6;
290
+ const TURN_MESSAGE_LABEL = {
291
+ queued: 'queued',
292
+ sending: 'Processing . . .',
293
+ delivered: 'delivered',
294
+ };
295
+ function turnMessageLine(message, width, progressTick) {
296
+ const DECORATION = 6;
297
+ const budget = Math.max(1, width - DECORATION);
298
+ const preferredLabel = Math.max(...Object.values(TURN_MESSAGE_LABEL).map((value) => displayWidth(value)));
299
+ const labelWidth = Math.min(Math.max(1, Math.floor(budget / 2)), Math.max(preferredLabel, displayWidth(message.note ?? '')));
300
+ const label = fitLine(message.note ?? TURN_MESSAGE_LABEL[message.state], labelWidth);
301
+ const textWidth = Math.max(1, budget - labelWidth);
302
+ const text = padToWidth(fitLine(`"${message.text}"`, textWidth), textWidth);
303
+ if (message.state === 'delivered') {
304
+ return line(span(' ● ', { color: 'green' }), span(text, { color: 'gray', dim: true }), span(` ${label}`, { color: 'green' }));
305
+ }
306
+ if (message.state === 'sending') {
307
+ return line(span(` ${todoInProgressGlyph(progressTick)} `, {
308
+ color: TODO_IN_PROGRESS_COLOR,
309
+ }), span(text, { color: TODO_IN_PROGRESS_COLOR, bold: true }), span(` ${label}`, { color: TODO_IN_PROGRESS_COLOR }));
310
+ }
311
+ return line(span(' ○ ', { color: 'cyan' }), span(text, { color: 'cyan' }), span(` ${label}`, { color: 'gray' }));
312
+ }
313
+ export function buildTurnMessageLines(messages, width, progressTick = null) {
314
+ if (messages.length === 0)
315
+ return [];
316
+ const lines = [
317
+ line(span('Your messages', { color: 'cyan', bold: true })),
318
+ ];
319
+ const visible = messages.slice(-(TURN_MESSAGE_PANEL_MAX_ROWS - 1));
320
+ const hidden = messages.length - visible.length;
321
+ if (hidden > 0) {
322
+ lines.push(line(span(` ● ${hidden} earlier`, { color: 'gray', dim: true })));
323
+ }
324
+ for (const message of visible) {
325
+ lines.push(turnMessageLine(message, width, progressTick));
326
+ }
327
+ return lines;
328
+ }
289
329
  export function formatTodoProgress(items) {
290
330
  const done = items.filter((item) => item.status === 'completed').length;
291
331
  return `${done}/${items.length} done`;
@@ -472,7 +512,7 @@ function composerFooterLines(state) {
472
512
  return lines;
473
513
  }
474
514
  const busyHelperText = state.queuedMessage
475
- ? 'Enter re-queues • ↑ edit queued • Esc / Ctrl+C clear queued'
515
+ ? 'Enter sends it to the agent • ↑ edit • Esc / Ctrl+C discard'
476
516
  : state.input
477
517
  ? 'Enter queues • Esc cancels turn • Ctrl+C clears draft'
478
518
  : 'Enter queues • Esc / Ctrl+C cancel turn';
@@ -649,6 +689,11 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds, nowMs) {
649
689
  lines.push(plainLine(''));
650
690
  }
651
691
  lines.push(plainLine(buildWorkingClockLine(state, elapsedSeconds), { color: 'yellow' }));
692
+ const turnMessages = state.turnMessages ?? [];
693
+ if (turnMessages.length > 0) {
694
+ lines.push(plainLine(''));
695
+ lines.push(...buildTurnMessageLines(turnMessages, width, todoProgressTick(nowMs)));
696
+ }
652
697
  const todos = state.todos ?? [];
653
698
  if (todos.length > 0) {
654
699
  lines.push(plainLine(''));
@@ -1055,25 +1100,19 @@ function buildOverlayLines(state, width, height, nowMs) {
1055
1100
  if (padded)
1056
1101
  lines.push(plainLine(''));
1057
1102
  }
1058
- const options = [
1059
- { value: 'y', label: 'Approve once' },
1060
- { value: 'a', label: 'Approve all remaining actions' },
1061
- { value: 'n', label: 'Deny' },
1062
- ];
1063
- options.forEach((option, index) => {
1103
+ const options = (prompt.options ?? []).map((option) => option.label);
1104
+ options.forEach((label, index) => {
1064
1105
  const selected = index === state.approvalCursor;
1065
1106
  lines.push(line(span(selected ? '› ' : ' ', {
1066
1107
  color: selected ? APPROVAL_ACCENT_COLOR : 'gray',
1067
- }), span(option.value, {
1108
+ }), span(label, {
1068
1109
  color: selected ? APPROVAL_ACCENT_COLOR : undefined,
1069
1110
  bold: selected,
1070
- }), span(` ${option.label}`, {
1071
- color: selected ? APPROVAL_ACCENT_COLOR : undefined,
1072
1111
  })));
1073
1112
  });
1074
1113
  if (padded)
1075
1114
  lines.push(plainLine(''));
1076
- lines.push(plainLine('Press y, a, or n ↑/↓ movesEnter confirms', {
1115
+ lines.push(plainLine('↑/↓ movesEnter confirmsEsc denies', {
1077
1116
  color: 'gray',
1078
1117
  }));
1079
1118
  return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR, height);
@@ -1198,11 +1237,10 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
1198
1237
  composerLines.push(line(span('Starting a new conversation…', { color: 'gray', dim: true })));
1199
1238
  }
1200
1239
  else if (state.queuedMessage) {
1201
- const preview = truncate(state.queuedMessage.body.trim().replace(/\s+/g, ' '), 60);
1202
1240
  const imageCount = state.queuedMessage.imageAttachments.length;
1203
- composerLines.push(line(span(`↳ Queued · "${preview}"`, { color: 'gray', dim: true }), ...(imageCount > 0
1204
- ? [span(` +${imageCount} img`, { color: 'gray', dim: true })]
1205
- : []), span(' edit · esc cancel', { color: 'gray', dim: true })));
1241
+ composerLines.push(line(span('Queued', { color: 'cyan', bold: true }), span(' Enter', { color: 'cyan', bold: true }), span(imageCount > 0
1242
+ ? ' sends it with the next prompt'
1243
+ : ' sends it to the agent now', { color: 'gray' }), span(' · ', { color: 'cyan', bold: true }), span(' edit', { color: 'gray' }), span(' · Esc', { color: 'cyan', bold: true }), span(' discard', { color: 'gray' })));
1206
1244
  }
1207
1245
  else {
1208
1246
  const promptLabel = state.busy ? 'queue> ' : '❯ ';
@@ -1,5 +1,5 @@
1
1
  import { readClipboardImage, readClipboardText } from '../../core/clipboard.js';
2
- import { applySlashCommandSuggestion, buildModelPickerOptions, deleteAtCursor, deleteBeforeCursor, getApprovalChoiceForCursor, getInputCommandToken, getNextApprovalCursor, getNextModelPickerIndex, getSlashCommandSuggestions, insertAtCursor, isExactSlashCommandToken, navigatePromptHistory, resolveApprovalChoiceFromInput, shouldRemountLiveFrameForComposerInputChange, } from '../repl.js';
2
+ import { applySlashCommandSuggestion, buildModelPickerOptions, deleteAtCursor, deleteBeforeCursor, getInputCommandToken, getNextApprovalCursor, getNextModelPickerIndex, getSlashCommandSuggestions, insertAtCursor, isExactSlashCommandToken, navigatePromptHistory, shouldRemountLiveFrameForComposerInputChange, } from '../repl.js';
3
3
  import { buildPastePlaceholder, shouldCollapsePaste, } from '../paste-collapse.js';
4
4
  import { handleUserInputPromptEvent, } from './user-input.js';
5
5
  const APPROVAL_PREVIEW_PAGE_ROWS = 3;
@@ -11,6 +11,13 @@ function isClipboardImagePasteKey(key) {
11
11
  }
12
12
  return key.ctrl && key.input === 'v' && !key.shift && !key.meta;
13
13
  }
14
+ export const APPROVAL_INPUT_GUARD_MS = 500;
15
+ function approvalIsGuarded(state) {
16
+ const openedAt = state.approvalOpenedAt;
17
+ if (typeof openedAt !== 'number')
18
+ return false;
19
+ return Date.now() - openedAt < APPROVAL_INPUT_GUARD_MS;
20
+ }
14
21
  function applyUserInputPromptEvent(store, handlers, event) {
15
22
  const current = store.getState();
16
23
  if (!current.userInputPrompt)
@@ -283,24 +290,22 @@ export function handleShellKeyEvent(store, handlers, event) {
283
290
  }
284
291
  return;
285
292
  }
286
- const directChoice = key.ctrl || key.meta ? null : resolveApprovalChoiceFromInput(key.input);
287
- if (directChoice) {
288
- void handlers.onResolveApproval(directChoice);
289
- return;
290
- }
291
- if (key.escape) {
292
- void handlers.onResolveApproval('n');
293
- return;
294
- }
295
293
  if (key.upArrow || key.downArrow) {
296
294
  store.update((current) => ({
297
295
  ...current,
298
- approvalCursor: getNextApprovalCursor(current.approvalCursor, key.upArrow ? -1 : 1),
296
+ approvalCursor: getNextApprovalCursor(current.approvalCursor, key.upArrow ? -1 : 1, current.approvalPrompt?.options?.length ?? 0),
299
297
  }));
300
298
  return;
301
299
  }
300
+ if (approvalIsGuarded(state)) {
301
+ return;
302
+ }
303
+ if (key.escape) {
304
+ void handlers.onResolveApproval(-1);
305
+ return;
306
+ }
302
307
  if (key.returnKey) {
303
- void handlers.onResolveApproval(getApprovalChoiceForCursor(state.approvalCursor));
308
+ void handlers.onResolveApproval(state.approvalCursor);
304
309
  }
305
310
  return;
306
311
  }
@@ -436,6 +441,15 @@ export function handleShellKeyEvent(store, handlers, event) {
436
441
  handlers.onCycleAgentMode();
437
442
  return;
438
443
  }
444
+ if (state.busy && state.queuedMessage) {
445
+ if (key.returnKey) {
446
+ void handlers.onFireQueuedMessage?.();
447
+ return;
448
+ }
449
+ if (!key.upArrow) {
450
+ return;
451
+ }
452
+ }
439
453
  if (commandPaletteActive && (key.upArrow || key.downArrow)) {
440
454
  store.update((current) => ({
441
455
  ...current,
@@ -444,7 +458,7 @@ export function handleShellKeyEvent(store, handlers, event) {
444
458
  return;
445
459
  }
446
460
  if (key.upArrow) {
447
- if (state.busy && state.input.trim() === '' && state.queuedMessage) {
461
+ if (state.busy && state.queuedMessage) {
448
462
  handlers.onLiveFrameShapeChange();
449
463
  store.update((current) => {
450
464
  const queued = current.queuedMessage;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thegitai/cli",
3
- "version": "1.0.0-preview.15",
3
+ "version": "1.0.0-preview.17",
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.15",
41
- "@thegitai/tui-darwin-x64": "1.0.0-preview.15",
42
- "@thegitai/tui-linux-x64": "1.0.0-preview.15",
43
- "@thegitai/tui-win32-x64": "1.0.0-preview.15",
40
+ "@thegitai/tui-darwin-arm64": "1.0.0-preview.17",
41
+ "@thegitai/tui-darwin-x64": "1.0.0-preview.17",
42
+ "@thegitai/tui-linux-x64": "1.0.0-preview.17",
43
+ "@thegitai/tui-win32-x64": "1.0.0-preview.17",
44
44
  "@vscode/ripgrep": "1.18.0"
45
45
  },
46
46
  "publishConfig": {