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

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.
@@ -248,6 +248,26 @@ async function postUserInputResult({ config, turnId, requestId, result, fetchImp
248
248
  throw await readErrorResponse(response, trace.traceId);
249
249
  }
250
250
  }
251
+ export async function postInterjection({ config, turnId, text, messageId, fetchImpl = globalThis.fetch, traceId, }) {
252
+ const payload = { text, messageId };
253
+ const trace = createTraceContext(traceId);
254
+ const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/interject`, {
255
+ method: 'POST',
256
+ headers: {
257
+ authorization: `Bearer ${config.token}`,
258
+ 'content-type': 'application/json',
259
+ ...trace.headers,
260
+ },
261
+ body: JSON.stringify(payload),
262
+ });
263
+ if (response.status === 410) {
264
+ return 'stale';
265
+ }
266
+ if (!response.ok) {
267
+ throw await readErrorResponse(response, trace.traceId);
268
+ }
269
+ return 'delivered';
270
+ }
251
271
  const turnIdOverrides = new WeakMap();
252
272
  function enterServerTurnId(session, serverSessionTurnId) {
253
273
  const active = turnIdOverrides.get(session);
@@ -309,7 +329,7 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
309
329
  }
310
330
  }
311
331
  }
312
- async function consumeTurnStream({ response, config, projectIndex, session, input, fetchImpl, signal, traceId, }) {
332
+ async function consumeTurnStream({ response, config, projectIndex, session, input, fetchImpl, signal, traceId, onTurnStart, onInterjectionDelivered, }) {
313
333
  if (!response.body) {
314
334
  throw new Error('Server returned an empty chat stream.');
315
335
  }
@@ -345,6 +365,16 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
345
365
  }
346
366
  }
347
367
  async function handleEvent(event) {
368
+ if (event.event === 'turn-start') {
369
+ const turnId = String(event.data?.turnId ?? '').trim();
370
+ if (turnId)
371
+ onTurnStart?.(turnId);
372
+ return;
373
+ }
374
+ if (event.event === 'interjection-delivered') {
375
+ onInterjectionDelivered?.(event.data);
376
+ return;
377
+ }
348
378
  if (event.event === 'status') {
349
379
  const data = event.data;
350
380
  if (data?.phase === 'analyzing_image') {
@@ -478,7 +508,7 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
478
508
  }
479
509
  return finalResult.current;
480
510
  }
481
- export async function sendServerUserMessage({ config, projectIndex, session, input, imageAttachments = [], fetchImpl = globalThis.fetch, signal, }) {
511
+ export async function sendServerUserMessage({ config, projectIndex, session, input, imageAttachments = [], fetchImpl = globalThis.fetch, signal, onTurnStart, onInterjectionDelivered, }) {
482
512
  const autoAttach = autoAttachImages(input, session.rootDir, imageAttachments);
483
513
  const requestImageAttachments = autoAttach.attachments.length > 0
484
514
  ? [...imageAttachments, ...autoAttach.attachments]
@@ -535,6 +565,8 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
535
565
  fetchImpl,
536
566
  signal,
537
567
  traceId: trace.traceId,
568
+ onTurnStart,
569
+ onInterjectionDelivered,
538
570
  });
539
571
  applySessionSnapshot(session, result.snapshot, { preserveAgentMode: true });
540
572
  return {
@@ -59,6 +59,13 @@ const HELP_MARKDOWN = [
59
59
  '- **Enter** sends • **Shift+Tab** cycles modes • **Esc** cancels the turn •',
60
60
  ' **Ctrl+C** clears the composer or the queued message, and quits once there',
61
61
  ' is nothing left to clear. These are the same on macOS, Linux, and Windows.',
62
+ '- **While the agent is working**, Enter queues your message and locks the',
63
+ ' composer. Press **Enter** again to send it into the running turn: the agent',
64
+ ' picks it up at its next step instead of waiting for the turn to finish, and',
65
+ ' the Your messages panel tracks it from queued to delivered. **↑** brings it',
66
+ ' back for editing and **Esc**/**Ctrl+C** discards it; both unlock the',
67
+ ' composer for the next message. Messages with images cannot be sent mid-turn',
68
+ ' and are sent with the next prompt instead.',
62
69
  `- **Paste** into the composer with your terminal's paste shortcut (\`${PASTE_SHORTCUT}\``,
63
70
  ' on this system) or by right-clicking the composer.',
64
71
  '- **Copy** from the transcript by dragging to select; double-click copies a',
@@ -85,8 +92,11 @@ const HELP_MARKDOWN = [
85
92
  '## Safety & approvals',
86
93
  '',
87
94
  '- TheGitAI asks before running shell commands or applying file edits.',
88
- '- At each prompt: **y** approves once, **a** approves the rest of the',
89
- ' session, **n** denies.',
95
+ '- At each prompt: **↑/↓** moves between choices, **Enter** confirms the',
96
+ ' highlighted one, and **Esc** denies. Deny is selected by default, and the',
97
+ ' choices are Approve once, Approve all remaining actions, and Deny.',
98
+ ' Single-letter shortcuts were removed on purpose: a prompt can appear',
99
+ ' while you are typing, and a stray letter must never approve anything.',
90
100
  '- If an approved `sudo` command needs a password, the TUI shows the exact',
91
101
  ' command and keeps the password masked and local.',
92
102
  '- `-y` / `--yes` at startup auto-approves every shell command and file',
@@ -852,6 +852,13 @@ export function buildTranscriptFromSessionHistory(history) {
852
852
  }
853
853
  continue;
854
854
  }
855
+ if (entry.role === 'user' && entry.kind === 'userInterjection') {
856
+ const text = String(entry.userInput ?? '').trim();
857
+ if (text) {
858
+ entries.push({ body: text, kind: 'user', title: 'You · sent mid-turn' });
859
+ }
860
+ continue;
861
+ }
855
862
  const text = textFromHistoryEntry(entry);
856
863
  if ((entry.role === 'model' || entry.role === 'assistant') && text) {
857
864
  if (isTurnFailureMarker(text)) {
@@ -1022,6 +1029,7 @@ function createInitialShellState(session, serverModels, debugUi) {
1022
1029
  resumePickerSessions: [],
1023
1030
  transcriptScrollOffset: 0,
1024
1031
  queuedMessage: null,
1032
+ turnMessages: [],
1025
1033
  serverModels: serverModels.models,
1026
1034
  sudoPrompt: null,
1027
1035
  status: 'Ready',
@@ -1282,16 +1290,6 @@ export function getApprovalChoiceForCursor(cursor) {
1282
1290
  return (APPROVAL_OPTIONS[Math.min(Math.max(cursor, 0), APPROVAL_OPTIONS.length - 1)]
1283
1291
  ?.value ?? 'n');
1284
1292
  }
1285
- export function resolveApprovalChoiceFromInput(input) {
1286
- const normalizedInput = String(input ?? '').trim().toLowerCase();
1287
- if (normalizedInput === 'y')
1288
- return 'y';
1289
- if (normalizedInput === 'a')
1290
- return 'a';
1291
- if (normalizedInput === 'n')
1292
- return 'n';
1293
- return null;
1294
- }
1295
1293
  export function pauseBusyClock(state, nowMs) {
1296
1294
  if (state.busySince === null || state.busyPausedAt !== null)
1297
1295
  return state;
@@ -1393,6 +1391,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1393
1391
  let latestUsageSummary = null;
1394
1392
  let pendingTurnEntries = [];
1395
1393
  let activeTurnAbort = null;
1394
+ let activeServerTurnId = null;
1395
+ let unacknowledged = new Map();
1396
+ const blockedRows = new WeakMap();
1396
1397
  let newConversationInFlight = false;
1397
1398
  let todosTouchedThisTurn = false;
1398
1399
  const syncTodosState = () => {
@@ -1677,6 +1678,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1677
1678
  status: 'Ready',
1678
1679
  thinkingTitle: '',
1679
1680
  thinkingNotes: [],
1681
+ turnMessages: [],
1680
1682
  workingTools: [],
1681
1683
  tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
1682
1684
  }));
@@ -1936,6 +1938,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1936
1938
  store.update((current) => ({
1937
1939
  ...pauseBusyClock(current, Date.now()),
1938
1940
  approvalCursor: getDefaultApprovalCursor(),
1941
+ approvalOpenedAt: Date.now(),
1939
1942
  approvalScrollOffset: 0,
1940
1943
  approvalPrompt: {
1941
1944
  title,
@@ -2230,6 +2233,129 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2230
2233
  }));
2231
2234
  await handleSubmit(queued.body);
2232
2235
  };
2236
+ let turnMessageCounter = 0;
2237
+ const upsertTurnMessage = (id, patch) => {
2238
+ store.update((current) => ({
2239
+ ...current,
2240
+ turnMessages: current.turnMessages.map((message) => message.id === id ? { ...message, ...patch } : message),
2241
+ }));
2242
+ };
2243
+ const recoverUnacknowledgedMessages = (autoSubmits = true) => {
2244
+ if (unacknowledged.size === 0)
2245
+ return;
2246
+ const stranded = [...unacknowledged.values()];
2247
+ unacknowledged = new Map();
2248
+ for (const { row } of stranded) {
2249
+ upsertTurnMessage(row, {
2250
+ state: 'queued',
2251
+ note: autoSubmits
2252
+ ? 'not read — sending as the next prompt'
2253
+ : 'not read — left in the composer',
2254
+ });
2255
+ }
2256
+ const existing = store.getState().queuedMessage;
2257
+ const combined = [
2258
+ ...stranded.map((entry) => entry.queued),
2259
+ ...(existing ? [existing] : []),
2260
+ ];
2261
+ const body = combined
2262
+ .map((entry) => entry.body)
2263
+ .filter((entry) => entry.trim())
2264
+ .join('\n\n');
2265
+ if (!body.trim())
2266
+ return;
2267
+ scheduleLiveFrameRemount();
2268
+ store.update((current) => ({
2269
+ ...current,
2270
+ queuedMessage: {
2271
+ body,
2272
+ imageAttachments: combined.flatMap((entry) => entry.imageAttachments),
2273
+ pastedChunks: combined.flatMap((entry) => entry.pastedChunks),
2274
+ },
2275
+ }));
2276
+ };
2277
+ const fireQueuedMessage = async () => {
2278
+ const state = store.getState();
2279
+ const queued = state.queuedMessage;
2280
+ if (!queued || !state.busy || exiting)
2281
+ return;
2282
+ const text = (queued.pastedChunks.length
2283
+ ? expandPastedChunks(queued.body, queued.pastedChunks)
2284
+ : queued.body).trim();
2285
+ if (!text)
2286
+ return;
2287
+ const turnId = activeServerTurnId;
2288
+ const blockedReason = queued.imageAttachments.length > 0
2289
+ ? 'waiting — images go with the next prompt'
2290
+ : !turnId
2291
+ ? 'waiting for the turn to end'
2292
+ : null;
2293
+ const existingRow = blockedRows.get(queued);
2294
+ if (existingRow !== undefined) {
2295
+ upsertTurnMessage(existingRow, {
2296
+ state: 'queued',
2297
+ ...(blockedReason ? { note: blockedReason } : {}),
2298
+ });
2299
+ if (blockedReason || !turnId)
2300
+ return;
2301
+ }
2302
+ const id = existingRow ?? ++turnMessageCounter;
2303
+ const messageId = `m${id}_${Date.now().toString(36)}`;
2304
+ if (existingRow === undefined) {
2305
+ scheduleLiveFrameRemount();
2306
+ store.update((current) => ({
2307
+ ...current,
2308
+ turnMessages: [
2309
+ ...current.turnMessages,
2310
+ {
2311
+ id,
2312
+ text,
2313
+ state: blockedReason ? 'queued' : 'sending',
2314
+ ...(blockedReason ? { note: blockedReason } : {}),
2315
+ },
2316
+ ],
2317
+ }));
2318
+ }
2319
+ if (blockedReason || !turnId) {
2320
+ blockedRows.set(queued, id);
2321
+ return;
2322
+ }
2323
+ blockedRows.delete(queued);
2324
+ unacknowledged.set(messageId, { row: id, queued });
2325
+ scheduleLiveFrameRemount();
2326
+ store.update((current) => current.queuedMessage === queued
2327
+ ? { ...current, queuedMessage: null }
2328
+ : current);
2329
+ queueTurnEntry({
2330
+ body: text,
2331
+ kind: 'user',
2332
+ preformatted: queued.pastedChunks.length > 0,
2333
+ title: 'You · sent mid-turn',
2334
+ });
2335
+ const rollback = (note) => {
2336
+ if (!unacknowledged.delete(messageId))
2337
+ return;
2338
+ upsertTurnMessage(id, { state: 'queued', note });
2339
+ scheduleLiveFrameRemount();
2340
+ store.update((current) => current.queuedMessage ? current : { ...current, queuedMessage: queued });
2341
+ };
2342
+ let outcome;
2343
+ try {
2344
+ outcome = await chat.postInterjection({
2345
+ config: authConfig,
2346
+ turnId,
2347
+ text,
2348
+ messageId,
2349
+ });
2350
+ }
2351
+ catch (error) {
2352
+ rollback(`not sent — ${error?.message ?? 'request failed'}`);
2353
+ return;
2354
+ }
2355
+ if (outcome === 'stale') {
2356
+ rollback('turn ended — sending as the next prompt');
2357
+ }
2358
+ };
2233
2359
  const handleSubmit = async (rawInput) => {
2234
2360
  const chunks = store.getState().pastedChunks;
2235
2361
  const expanded = chunks.length
@@ -2481,6 +2607,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2481
2607
  }
2482
2608
  latestUsageSummary = null;
2483
2609
  disarmExitConfirm();
2610
+ unacknowledged = new Map();
2484
2611
  const turnStartedAt = Date.now();
2485
2612
  const turnGeneration = ++activeTurnGeneration;
2486
2613
  const turnAbort = new AbortController();
@@ -2513,6 +2640,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2513
2640
  transcriptScrollOffset: 0,
2514
2641
  tokenUsage: formatClientTokenUsage(0, latestUsageSummary),
2515
2642
  turnCounter: current.turnCounter + 1,
2643
+ turnMessages: [],
2516
2644
  workingTools: [],
2517
2645
  }));
2518
2646
  try {
@@ -2523,6 +2651,25 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2523
2651
  input,
2524
2652
  imageAttachments,
2525
2653
  signal: turnAbort.signal,
2654
+ onTurnStart: (serverTurnId) => {
2655
+ if (turnGeneration !== activeTurnGeneration)
2656
+ return;
2657
+ activeServerTurnId = serverTurnId;
2658
+ },
2659
+ onInterjectionDelivered: (event) => {
2660
+ if (turnGeneration !== activeTurnGeneration)
2661
+ return;
2662
+ for (const messageId of event.messageIds ?? []) {
2663
+ const pending = unacknowledged.get(messageId);
2664
+ if (!pending)
2665
+ continue;
2666
+ unacknowledged.delete(messageId);
2667
+ upsertTurnMessage(pending.row, {
2668
+ state: 'delivered',
2669
+ note: undefined,
2670
+ });
2671
+ }
2672
+ },
2526
2673
  });
2527
2674
  if (turnGeneration !== activeTurnGeneration)
2528
2675
  return;
@@ -2571,6 +2718,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2571
2718
  status: result.waitingForApproval ? 'Awaiting approval' : 'Ready',
2572
2719
  tokenUsage: formatClientTokenUsage(Date.now() - turnStartedAt, latestUsageSummary),
2573
2720
  }));
2721
+ recoverUnacknowledgedMessages();
2574
2722
  await flushQueuedMessage();
2575
2723
  }
2576
2724
  catch (error) {
@@ -2616,6 +2764,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2616
2764
  appendError(error.message);
2617
2765
  }
2618
2766
  await remountTui();
2767
+ recoverUnacknowledgedMessages(!cancelled);
2619
2768
  if (!cancelled && turnGeneration === activeTurnGeneration) {
2620
2769
  await flushQueuedMessage();
2621
2770
  }
@@ -2626,7 +2775,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2626
2775
  }
2627
2776
  if (turnGeneration === activeTurnGeneration) {
2628
2777
  lastTurnStartedAt = null;
2778
+ activeServerTurnId = null;
2629
2779
  }
2780
+ recoverUnacknowledgedMessages();
2630
2781
  }
2631
2782
  };
2632
2783
  session.onImageAnalysis = (activeImageCount) => {
@@ -2791,6 +2942,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2791
2942
  onJobsPickerKill: handleJobsPickerKill,
2792
2943
  onSudoPasswordInput: handleSudoPasswordInput,
2793
2944
  onSubmit: handleSubmit,
2945
+ onFireQueuedMessage: fireQueuedMessage,
2794
2946
  };
2795
2947
  const unsubscribe = store.subscribe(() => {
2796
2948
  renderCurrentFrame();
@@ -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 = ['Approve once', 'Approve all remaining actions', 'Deny'];
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, getApprovalChoiceForCursor, 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,15 +290,6 @@ 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,
@@ -299,6 +297,13 @@ export function handleShellKeyEvent(store, handlers, event) {
299
297
  }));
300
298
  return;
301
299
  }
300
+ if (approvalIsGuarded(state)) {
301
+ return;
302
+ }
303
+ if (key.escape) {
304
+ void handlers.onResolveApproval('n');
305
+ return;
306
+ }
302
307
  if (key.returnKey) {
303
308
  void handlers.onResolveApproval(getApprovalChoiceForCursor(state.approvalCursor));
304
309
  }
@@ -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.16",
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.16",
41
+ "@thegitai/tui-darwin-x64": "1.0.0-preview.16",
42
+ "@thegitai/tui-linux-x64": "1.0.0-preview.16",
43
+ "@thegitai/tui-win32-x64": "1.0.0-preview.16",
44
44
  "@vscode/ripgrep": "1.18.0"
45
45
  },
46
46
  "publishConfig": {