@thegitai/cli 1.0.0-preview.2 → 1.0.0-preview.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 (38) hide show
  1. package/README.md +32 -4
  2. package/dist/bin/ai.js +57 -291
  3. package/dist/src/agent-mode.js +1 -1
  4. package/dist/src/api/auth.js +2 -2
  5. package/dist/src/api/browser-login.js +72 -3
  6. package/dist/src/api/chat.js +232 -33
  7. package/dist/src/api/contracts.js +55 -1
  8. package/dist/src/api/http.js +16 -3
  9. package/dist/src/api/models.js +9 -4
  10. package/dist/src/executor.js +1 -1
  11. package/dist/src/help-text.js +51 -11
  12. package/dist/src/permissions.js +243 -0
  13. package/dist/src/project-index.js +13 -1
  14. package/dist/src/session-store.js +57 -20
  15. package/dist/src/session.js +14 -3
  16. package/dist/src/tool-executor.js +2 -2
  17. package/dist/src/tools/delete-file.js +14 -0
  18. package/dist/src/tools/patch-file.js +12 -16
  19. package/dist/src/tools/replace-document-text.js +28 -18
  20. package/dist/src/tools/run-command.js +13 -27
  21. package/dist/src/tools/run-node-script.js +11 -26
  22. package/dist/src/tools/str-replace.js +12 -16
  23. package/dist/src/tools/write-file.js +66 -0
  24. package/dist/src/turn-failure-marker.js +11 -0
  25. package/dist/src/ui/prompt-history-store.js +1 -1
  26. package/dist/src/ui/repl.js +569 -151
  27. package/dist/src/ui/tui/bridge.js +10 -0
  28. package/dist/src/ui/tui/build-frame.js +535 -159
  29. package/dist/src/ui/tui/markdown-render.js +81 -73
  30. package/dist/src/ui/tui/shell-input.js +155 -45
  31. package/dist/src/ui/tui/terminal-theme.js +28 -0
  32. package/dist/src/ui/tui/terminal-title.js +3 -0
  33. package/dist/src/ui/tui/terminal-writes.js +48 -0
  34. package/dist/src/ui/tui/text.js +158 -4
  35. package/dist/src/ui/tui/user-input.js +568 -0
  36. package/dist/src/utils.js +9 -0
  37. package/package.json +18 -6
  38. package/dist/src/markdown-renderer.js +0 -112
@@ -1,6 +1,8 @@
1
- import { line, plainLine, span, wrapText } from './text.js';
2
- const TABLE_BORDER_WIDTH = 2;
3
- const TABLE_CELL_OVERHEAD_WIDTH = 3;
1
+ import { displayWidth, line, padToWidth, plainLine, sliceToWidth, span, wrapText, wrapToWidth, } from './text.js';
2
+ const MIN_TABLE_COLUMN_WIDTH = 3;
3
+ function tableRowOverhead(columnCount) {
4
+ return 3 * columnCount + 1;
5
+ }
4
6
  function parseInlineSegments(text) {
5
7
  const source = String(text ?? '');
6
8
  const segments = [];
@@ -110,45 +112,37 @@ function stripInlineFormattingForWidth(text) {
110
112
  .replace(/\*\*([^*]+)\*\*/g, '$1');
111
113
  }
112
114
  function fitTableColumnWidths(columnWidths, maxWidth) {
113
- const widths = columnWidths.map((width) => Math.max(3, Math.floor(width)));
114
- if (widths.length === 0)
115
- return widths;
116
- const overhead = TABLE_BORDER_WIDTH + widths.length * TABLE_CELL_OVERHEAD_WIDTH;
117
- const budget = Math.max(widths.length * 3, maxWidth - overhead);
118
- const total = widths.reduce((sum, width) => sum + width, 0);
119
- if (total <= budget)
120
- return widths;
121
- let remaining = budget;
122
- const scaled = widths.map((width) => {
123
- const next = Math.max(3, Math.floor((width / total) * budget));
124
- remaining -= next;
125
- return next;
126
- });
127
- while (remaining > 0) {
128
- let targetIndex = 0;
129
- for (let index = 1; index < widths.length; index++) {
130
- if (widths[index] - scaled[index] > widths[targetIndex] - scaled[targetIndex]) {
131
- targetIndex = index;
132
- }
133
- }
134
- scaled[targetIndex]++;
135
- remaining--;
115
+ const columnCount = columnWidths.length;
116
+ if (columnCount === 0)
117
+ return [];
118
+ const natural = columnWidths.map((width) => Math.max(1, Math.floor(width)));
119
+ const budget = Math.floor(maxWidth) - tableRowOverhead(columnCount);
120
+ const total = natural.reduce((sum, width) => sum + width, 0);
121
+ if (budget >= total)
122
+ return natural;
123
+ const floorWidth = Math.max(1, Math.min(MIN_TABLE_COLUMN_WIDTH, Math.floor(budget / columnCount)));
124
+ const widths = natural.map((width) => Math.min(width, floorWidth));
125
+ let used = widths.reduce((sum, width) => sum + width, 0);
126
+ if (used > budget) {
127
+ const share = Math.max(1, Math.floor(budget / columnCount));
128
+ for (let index = 0; index < columnCount; index++)
129
+ widths[index] = share;
130
+ used = share * columnCount;
136
131
  }
137
- while (remaining < 0) {
138
- let targetIndex = -1;
139
- for (let index = 0; index < scaled.length; index++) {
140
- if (scaled[index] <= 3)
132
+ let remaining = budget - used;
133
+ while (remaining > 0) {
134
+ let grew = false;
135
+ for (let index = 0; index < columnCount && remaining > 0; index++) {
136
+ if (widths[index] >= natural[index])
141
137
  continue;
142
- if (targetIndex === -1 || scaled[index] > scaled[targetIndex]) {
143
- targetIndex = index;
144
- }
138
+ widths[index]++;
139
+ remaining--;
140
+ grew = true;
145
141
  }
146
- if (targetIndex === -1)
142
+ if (!grew)
147
143
  break;
148
- scaled[targetIndex]--;
149
- remaining++;
150
144
  }
151
- return scaled;
145
+ return widths;
152
146
  }
153
147
  function normalizeMarkdownTableCells(cells, columnCount) {
154
148
  return Array.from({ length: columnCount }, (_, index) => cells[index] ?? '');
@@ -176,7 +170,7 @@ function parseMarkdownTableBlock(lines, startIndex) {
176
170
  const normalizedHeaders = normalizeMarkdownTableCells(headers, columnCount);
177
171
  const columnWidths = normalizedHeaders.map((header, columnIndex) => {
178
172
  const values = [header, ...rows.map((row) => row[columnIndex] ?? '')];
179
- return Math.max(3, ...values.map((value) => stripInlineFormattingForWidth(value).length));
173
+ return Math.max(MIN_TABLE_COLUMN_WIDTH, ...values.map((value) => displayWidth(stripInlineFormattingForWidth(value))));
180
174
  });
181
175
  return {
182
176
  nextIndex,
@@ -280,24 +274,42 @@ function wrapInlineToLines(text, width, bodyColor, prefix = '') {
280
274
  rowWidth = 0;
281
275
  };
282
276
  const appendSpan = (part) => {
283
- const current = rows[rows.length - 1];
284
- const limit = safeWidth - (indent ? prefix.length : 0);
277
+ let current = rows[rows.length - 1];
285
278
  let remaining = part.text;
286
279
  while (remaining.length > 0) {
280
+ const limit = safeWidth - (rows.length === 1 && indent ? displayWidth(prefix) : 0);
287
281
  const room = limit - rowWidth;
288
282
  if (room <= 0) {
289
283
  startRow();
284
+ current = rows[rows.length - 1];
290
285
  continue;
291
286
  }
292
- if (remaining.length <= room) {
287
+ const width = displayWidth(remaining);
288
+ if (width <= room) {
293
289
  current.push(span(remaining, styleFromSpan(part)));
294
- rowWidth += remaining.length;
290
+ rowWidth += width;
295
291
  remaining = '';
296
292
  break;
297
293
  }
298
- current.push(span(remaining.slice(0, room), styleFromSpan(part)));
299
- remaining = remaining.slice(room);
294
+ if (rowWidth > 0) {
295
+ if (/^\s+$/.test(remaining)) {
296
+ remaining = '';
297
+ break;
298
+ }
299
+ startRow();
300
+ current = rows[rows.length - 1];
301
+ continue;
302
+ }
303
+ const head = sliceToWidth(remaining, room);
304
+ if (!head || (displayWidth(head) > room && current.length > 0)) {
305
+ startRow();
306
+ current = rows[rows.length - 1];
307
+ continue;
308
+ }
309
+ current.push(span(head, styleFromSpan(part)));
310
+ remaining = remaining.slice(head.length);
300
311
  startRow();
312
+ current = rows[rows.length - 1];
301
313
  }
302
314
  };
303
315
  for (const segment of segments) {
@@ -316,36 +328,35 @@ function wrapInlineToLines(text, width, bodyColor, prefix = '') {
316
328
  return line(...bodySpans);
317
329
  });
318
330
  }
319
- function padCell(text, width) {
320
- const plain = stripInlineFormattingForWidth(text);
321
- if (plain.length >= width)
322
- return plain.slice(0, width);
323
- return plain + ' '.repeat(width - plain.length);
324
- }
325
331
  function renderTableLines(table, width) {
326
332
  const columnWidths = fitTableColumnWidths(table.columnWidths, width);
333
+ const border = (left, joint, right) => line(span(left +
334
+ columnWidths.map((colWidth) => '─'.repeat(colWidth + 2)).join(joint) +
335
+ right, { color: 'cyan', dim: true }));
327
336
  const renderRow = (cells, bold) => {
328
- const parts = columnWidths.map((colWidth, index) => span(` ${padCell(cells[index] ?? '', colWidth)} `, {
329
- color: 'cyan',
330
- bold,
331
- }));
332
- return line(span('│', { color: 'cyan' }), ...parts, span('│', { color: 'cyan' }));
337
+ const wrapped = columnWidths.map((colWidth, index) => wrapToWidth(stripInlineFormattingForWidth(cells[index] ?? ''), colWidth));
338
+ const height = Math.max(1, ...wrapped.map((cellLines) => cellLines.length));
339
+ const rows = [];
340
+ for (let row = 0; row < height; row++) {
341
+ const spans = [span('│', { color: 'cyan' })];
342
+ for (let column = 0; column < columnWidths.length; column++) {
343
+ const text = wrapped[column]?.[row] ?? '';
344
+ spans.push(span(` ${padToWidth(text, columnWidths[column])} `, {
345
+ color: 'cyan',
346
+ bold,
347
+ }));
348
+ spans.push(span('│', { color: 'cyan' }));
349
+ }
350
+ rows.push(line(...spans));
351
+ }
352
+ return rows;
333
353
  };
334
- const separator = line(span('├' +
335
- columnWidths.map((colWidth) => '─'.repeat(colWidth + 2)).join('┼') +
336
- '┤', { color: 'cyan', dim: true }));
337
- const top = line(span('┌' +
338
- columnWidths.map((colWidth) => '─'.repeat(colWidth + 2)).join('┬') +
339
- '┐', { color: 'cyan', dim: true }));
340
- const bottom = line(span('└' +
341
- columnWidths.map((colWidth) => '─'.repeat(colWidth + 2)).join('┴') +
342
- '┘', { color: 'cyan', dim: true }));
343
354
  return [
344
- top,
345
- renderRow(table.headers, true),
346
- separator,
347
- ...table.rows.map((row) => renderRow(row, false)),
348
- bottom,
355
+ border('┌', '┬', '┐'),
356
+ ...renderRow(table.headers, true),
357
+ border('├', '┼', '┤'),
358
+ ...table.rows.flatMap((row) => renderRow(row, false)),
359
+ border('└', '┴', '┘'),
349
360
  ];
350
361
  }
351
362
  function getEntryColor(kind) {
@@ -410,10 +421,7 @@ export function renderFormattedBodyLines(body, width, kind) {
410
421
  }
411
422
  if (formattedLine.kind === 'table' && formattedLine.table) {
412
423
  output.push(...renderTableLines(formattedLine.table, bodyWidth).map((tableLine) => {
413
- const spans = tableLine.spans.map((part) => ({
414
- ...part,
415
- text: ` ${part.text}`,
416
- }));
424
+ const spans = tableLine.spans.map((part, index) => index === 0 ? { ...part, text: ` ${part.text}` } : part);
417
425
  return { spans };
418
426
  }));
419
427
  continue;
@@ -1,6 +1,8 @@
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
+ import { handleUserInputPromptEvent, } from './user-input.js';
5
+ const APPROVAL_PREVIEW_PAGE_ROWS = 3;
4
6
  function isClipboardImagePasteKey(key) {
5
7
  if (process.platform === 'win32') {
6
8
  return ((key.ctrl || key.meta) &&
@@ -9,6 +11,38 @@ function isClipboardImagePasteKey(key) {
9
11
  }
10
12
  return key.ctrl && key.input === 'v' && !key.shift && !key.meta;
11
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
+ }
21
+ function applyUserInputPromptEvent(store, handlers, event) {
22
+ const current = store.getState();
23
+ if (!current.userInputPrompt)
24
+ return false;
25
+ const outcome = handleUserInputPromptEvent(current.userInputPrompt, event, handlers.getUserInputViewport?.());
26
+ store.update((state) => ({
27
+ ...state,
28
+ userInputPrompt: outcome.state,
29
+ }));
30
+ if (outcome.result) {
31
+ void handlers.onResolveUserInput?.(outcome.result);
32
+ }
33
+ return true;
34
+ }
35
+ function composerIsEmpty(state) {
36
+ return (!state.input &&
37
+ state.cursor === 0 &&
38
+ state.pastedChunks.length === 0 &&
39
+ state.promptHistoryCursor === null);
40
+ }
41
+ function composerHasDiscardableDraft(state) {
42
+ if (!composerIsEmpty(state))
43
+ return true;
44
+ return !state.busy && state.imageAttachments.length > 0;
45
+ }
12
46
  function shouldShowCommandPalette(state) {
13
47
  if (state.busy ||
14
48
  state.exiting ||
@@ -74,6 +108,41 @@ function pasteTextFromClipboard(store, handlers) {
74
108
  }
75
109
  insertPastedText(store, handlers, text);
76
110
  }
111
+ function scrollTranscript(store, handlers, delta) {
112
+ if (delta === 0)
113
+ return;
114
+ store.update((current) => {
115
+ const limit = handlers.getTranscriptScrollLimit?.() ??
116
+ current.transcript.reduce((total, entry) => total + 2 + (entry.body ? entry.body.split('\n').length : 0), 0);
117
+ const next = current.transcriptScrollOffset + delta;
118
+ return {
119
+ ...current,
120
+ transcriptScrollOffset: Math.max(0, Math.min(next, limit)),
121
+ };
122
+ });
123
+ }
124
+ function scrollTranscriptTo(store, handlers, offset) {
125
+ store.update((current) => {
126
+ const limit = handlers.getTranscriptScrollLimit?.() ??
127
+ current.transcript.reduce((total, entry) => total + 2 + (entry.body ? entry.body.split('\n').length : 0), 0);
128
+ return {
129
+ ...current,
130
+ transcriptScrollOffset: Math.max(0, Math.min(Math.trunc(offset), limit)),
131
+ };
132
+ });
133
+ }
134
+ function scrollApprovalPreview(store, handlers, delta) {
135
+ if (delta === 0)
136
+ return;
137
+ store.update((current) => {
138
+ const limit = handlers.getApprovalScrollLimit?.() ?? 0;
139
+ const next = (current.approvalScrollOffset ?? 0) + delta;
140
+ return {
141
+ ...current,
142
+ approvalScrollOffset: Math.max(0, Math.min(next, limit)),
143
+ };
144
+ });
145
+ }
77
146
  function filterResumeSessionsLocal(sessions, filter, serverModels) {
78
147
  const q = filter.trim().toLowerCase();
79
148
  if (!q)
@@ -88,6 +157,12 @@ function filterResumeSessionsLocal(sessions, filter, serverModels) {
88
157
  }
89
158
  export function handleShellKeyEvent(store, handlers, event) {
90
159
  if (event.kind === 'paste') {
160
+ if (applyUserInputPromptEvent(store, handlers, {
161
+ kind: 'paste',
162
+ text: event.text,
163
+ })) {
164
+ return;
165
+ }
91
166
  insertPastedText(store, handlers, event.text);
92
167
  return;
93
168
  }
@@ -110,33 +185,67 @@ export function handleShellKeyEvent(store, handlers, event) {
110
185
  return;
111
186
  }
112
187
  if (event.kind === 'contextMenu') {
188
+ if (store.getState().userInputPrompt) {
189
+ const text = (handlers.readClipboardText ?? readClipboardText)();
190
+ if (text) {
191
+ applyUserInputPromptEvent(store, handlers, { kind: 'paste', text });
192
+ }
193
+ return;
194
+ }
113
195
  pasteTextFromClipboard(store, handlers);
114
196
  return;
115
197
  }
116
198
  if (event.kind === 'transcriptScroll') {
117
- const delta = Math.trunc(event.deltaLines);
118
- if (delta !== 0) {
119
- store.update((current) => {
120
- const limit = handlers.getTranscriptScrollLimit?.() ??
121
- current.transcript.reduce((total, entry) => total + 2 + (entry.body ? entry.body.split('\n').length : 0), 0);
122
- const next = current.transcriptScrollOffset + delta;
123
- return {
124
- ...current,
125
- transcriptScrollOffset: Math.max(0, Math.min(next, limit)),
126
- };
127
- });
128
- }
199
+ scrollTranscript(store, handlers, Math.trunc(event.deltaLines));
200
+ return;
201
+ }
202
+ if (event.kind === 'transcriptScrollTo') {
203
+ scrollTranscriptTo(store, handlers, Number(event.offset ?? 0));
129
204
  return;
130
205
  }
131
206
  if (event.kind !== 'key')
132
207
  return;
133
208
  const key = event;
134
209
  const state = store.getState();
210
+ const commandPaletteActive = shouldShowCommandPalette(state);
211
+ const commandSuggestions = commandPaletteActive
212
+ ? getSlashCommandSuggestions(state.input)
213
+ : [];
214
+ const prepareForComposerInputChange = (current, nextInput) => {
215
+ if (shouldRemountLiveFrameForComposerInputChange(current, nextInput)) {
216
+ handlers.onLiveFrameShapeChange();
217
+ }
218
+ };
135
219
  if (key.ctrl && key.input === 'c') {
220
+ if (state.userInputPrompt) {
221
+ handlers.onCtrlC?.();
222
+ return;
223
+ }
136
224
  if (state.sudoPrompt) {
137
225
  handlers.onSudoPasswordInput({ kind: 'cancel' });
138
226
  return;
139
227
  }
228
+ if (state.queuedMessage) {
229
+ handlers.onLiveFrameShapeChange();
230
+ store.update((current) => ({ ...current, queuedMessage: null }));
231
+ return;
232
+ }
233
+ if (composerHasDiscardableDraft(state)) {
234
+ store.update((current) => {
235
+ prepareForComposerInputChange(current, '');
236
+ return {
237
+ ...current,
238
+ commandCursor: 0,
239
+ cursor: 0,
240
+ imageAttachments: current.busy ? current.imageAttachments : [],
241
+ input: '',
242
+ pastedChunks: [],
243
+ promptHistoryCursor: null,
244
+ promptHistoryDraft: '',
245
+ };
246
+ });
247
+ return;
248
+ }
140
249
  if (handlers.onCtrlC) {
141
250
  handlers.onCtrlC();
142
251
  return;
@@ -144,15 +253,10 @@ export function handleShellKeyEvent(store, handlers, event) {
144
253
  handlers.onRequestExit();
145
254
  return;
146
255
  }
147
- const commandPaletteActive = shouldShowCommandPalette(state);
148
- const commandSuggestions = commandPaletteActive
149
- ? getSlashCommandSuggestions(state.input)
150
- : [];
151
- const prepareForComposerInputChange = (current, nextInput) => {
152
- if (shouldRemountLiveFrameForComposerInputChange(current, nextInput)) {
153
- handlers.onLiveFrameShapeChange();
154
- }
155
- };
256
+ if (state.userInputPrompt &&
257
+ applyUserInputPromptEvent(store, handlers, key)) {
258
+ return;
259
+ }
156
260
  if (state.sudoPrompt) {
157
261
  if (key.escape) {
158
262
  handlers.onSudoPasswordInput({ kind: 'cancel' });
@@ -176,24 +280,32 @@ export function handleShellKeyEvent(store, handlers, event) {
176
280
  return;
177
281
  }
178
282
  if (state.approvalPrompt) {
179
- const directChoice = resolveApprovalChoiceFromInput(key.input);
180
- if (directChoice) {
181
- void handlers.onResolveApproval(directChoice);
182
- return;
183
- }
184
- if (key.escape) {
185
- void handlers.onResolveApproval('n');
283
+ if (key.pageUp || key.pageDown) {
284
+ const delta = key.pageUp ? -APPROVAL_PREVIEW_PAGE_ROWS : APPROVAL_PREVIEW_PAGE_ROWS;
285
+ if (key.shift) {
286
+ scrollTranscript(store, handlers, key.pageUp ? 8 : -8);
287
+ }
288
+ else {
289
+ scrollApprovalPreview(store, handlers, delta);
290
+ }
186
291
  return;
187
292
  }
188
293
  if (key.upArrow || key.downArrow) {
189
294
  store.update((current) => ({
190
295
  ...current,
191
- approvalCursor: getNextApprovalCursor(current.approvalCursor, key.upArrow ? -1 : 1),
296
+ approvalCursor: getNextApprovalCursor(current.approvalCursor, key.upArrow ? -1 : 1, current.approvalPrompt?.options?.length ?? 0),
192
297
  }));
193
298
  return;
194
299
  }
300
+ if (approvalIsGuarded(state)) {
301
+ return;
302
+ }
303
+ if (key.escape) {
304
+ void handlers.onResolveApproval(-1);
305
+ return;
306
+ }
195
307
  if (key.returnKey) {
196
- void handlers.onResolveApproval(getApprovalChoiceForCursor(state.approvalCursor));
308
+ void handlers.onResolveApproval(state.approvalCursor);
197
309
  }
198
310
  return;
199
311
  }
@@ -201,15 +313,7 @@ export function handleShellKeyEvent(store, handlers, event) {
201
313
  return;
202
314
  }
203
315
  if (key.pageUp || key.pageDown) {
204
- store.update((current) => {
205
- const transcriptLines = handlers.getTranscriptScrollLimit?.() ??
206
- current.transcript.reduce((total, entry) => total + 2 + (entry.body ? entry.body.split('\n').length : 0), 0);
207
- const next = current.transcriptScrollOffset + (key.pageUp ? 8 : -8);
208
- return {
209
- ...current,
210
- transcriptScrollOffset: Math.max(0, Math.min(next, transcriptLines)),
211
- };
212
- });
316
+ scrollTranscript(store, handlers, key.pageUp ? 8 : -8);
213
317
  return;
214
318
  }
215
319
  if (state.jobsPickerOpen) {
@@ -317,10 +421,7 @@ export function handleShellKeyEvent(store, handlers, event) {
317
421
  return;
318
422
  }
319
423
  store.update((current) => {
320
- if (!current.input &&
321
- current.cursor === 0 &&
322
- current.pastedChunks.length === 0 &&
323
- current.promptHistoryCursor === null) {
424
+ if (composerIsEmpty(current)) {
324
425
  return current;
325
426
  }
326
427
  prepareForComposerInputChange(current, '');
@@ -340,6 +441,15 @@ export function handleShellKeyEvent(store, handlers, event) {
340
441
  handlers.onCycleAgentMode();
341
442
  return;
342
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
+ }
343
453
  if (commandPaletteActive && (key.upArrow || key.downArrow)) {
344
454
  store.update((current) => ({
345
455
  ...current,
@@ -348,7 +458,7 @@ export function handleShellKeyEvent(store, handlers, event) {
348
458
  return;
349
459
  }
350
460
  if (key.upArrow) {
351
- if (state.busy && state.input.trim() === '' && state.queuedMessage) {
461
+ if (state.busy && state.queuedMessage) {
352
462
  handlers.onLiveFrameShapeChange();
353
463
  store.update((current) => {
354
464
  const queued = current.queuedMessage;
@@ -0,0 +1,28 @@
1
+ export function isDarkTerminalBackground(env = process.env) {
2
+ const raw = String(env.COLORFGBG ?? '').trim();
3
+ if (!raw)
4
+ return false;
5
+ const parts = raw
6
+ .split(/[;:]/)
7
+ .map((part) => part.trim())
8
+ .filter(Boolean);
9
+ const bgToken = parts[parts.length - 1];
10
+ if (!bgToken)
11
+ return false;
12
+ const bg = Number(bgToken);
13
+ if (!Number.isFinite(bg))
14
+ return false;
15
+ return bg >= 0 && bg < 7;
16
+ }
17
+ export function mutedStyle(env = process.env) {
18
+ if (isDarkTerminalBackground(env)) {
19
+ return { color: 'ansi256(247)' };
20
+ }
21
+ return { color: 'gray' };
22
+ }
23
+ export function mutedColor(env = process.env) {
24
+ if (isDarkTerminalBackground(env)) {
25
+ return 'ansi256(247)';
26
+ }
27
+ return 'gray';
28
+ }
@@ -1,3 +1,4 @@
1
+ import { isTuiMode } from '../../runtime-mode.js';
1
2
  const BRAILLE_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴'];
2
3
  const TITLE_BRAND = 'TheGitAI';
3
4
  const TITLE_MARK_PREFIX = '❯_';
@@ -19,6 +20,8 @@ export function formatTerminalTitle(state, spinnerFrame = 0) {
19
20
  return `${TITLE_MARK_PREFIX}● ${TITLE_BRAND}`;
20
21
  }
21
22
  export function writeTerminalTitle(title, stream = process.stdout) {
23
+ if (isTuiMode())
24
+ return;
22
25
  if (!('isTTY' in stream) || !stream.isTTY)
23
26
  return;
24
27
  stream.write(`\x1b]0;${title}\x07`);
@@ -0,0 +1,48 @@
1
+ const MAX_CAPTURED_CHARS = 1_000_000;
2
+ let restore = null;
3
+ let captured = [];
4
+ let capturedChars = 0;
5
+ function chunkToString(chunk, encoding) {
6
+ if (typeof chunk === 'string')
7
+ return chunk;
8
+ if (chunk instanceof Uint8Array) {
9
+ return Buffer.from(chunk).toString(typeof encoding === 'string' ? encoding : 'utf8');
10
+ }
11
+ return String(chunk ?? '');
12
+ }
13
+ export function captureTerminalWrites() {
14
+ if (restore)
15
+ return;
16
+ const streams = [process.stdout, process.stderr];
17
+ const originals = streams.map((stream) => stream.write.bind(stream));
18
+ for (const stream of streams) {
19
+ stream.write = (chunk, encoding, callback) => {
20
+ if (capturedChars < MAX_CAPTURED_CHARS) {
21
+ const text = chunkToString(chunk, encoding);
22
+ captured.push(text);
23
+ capturedChars += text.length;
24
+ }
25
+ const done = typeof encoding === 'function' ? encoding : callback;
26
+ if (typeof done === 'function')
27
+ done();
28
+ return true;
29
+ };
30
+ }
31
+ restore = () => {
32
+ streams.forEach((stream, index) => {
33
+ stream.write = originals[index];
34
+ });
35
+ };
36
+ }
37
+ export function releaseTerminalWrites() {
38
+ if (!restore)
39
+ return;
40
+ restore();
41
+ restore = null;
42
+ if (captured.length > 0) {
43
+ const text = captured.join('');
44
+ captured = [];
45
+ capturedChars = 0;
46
+ process.stderr.write(text);
47
+ }
48
+ }