@thegitai/cli 1.0.0-preview.3 → 1.0.0-preview.5

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.
@@ -39,10 +39,21 @@ function removeFile(index, relPath) {
39
39
  index.chunksByFile.delete(relPath);
40
40
  index.fileSignatures.delete(relPath);
41
41
  }
42
+ function countIndexedChunks(index) {
43
+ return Array.from(index.chunksByFile.values()).reduce((sum, chunks) => sum + chunks.length, 0);
44
+ }
42
45
  async function initializeIndex(index) {
43
46
  if (index.initialized) {
44
- return Array.from(index.chunksByFile.values()).reduce((sum, chunks) => sum + chunks.length, 0);
47
+ return countIndexedChunks(index);
48
+ }
49
+ if (!index._initializing) {
50
+ index._initializing = scanProjectIntoIndex(index).finally(() => {
51
+ index._initializing = null;
52
+ });
45
53
  }
54
+ return index._initializing;
55
+ }
56
+ async function scanProjectIntoIndex(index) {
46
57
  const files = listProjectFiles(index.rootDir);
47
58
  const chunks = await scanFiles(index.rootDir, files);
48
59
  index.fileSignatures.clear();
@@ -106,6 +117,7 @@ export function createIndex({ rootDir, onStatus = null, onContextLog = null, })
106
117
  return {
107
118
  rootDir: path.resolve(rootDir),
108
119
  initialized: false,
120
+ _initializing: null,
109
121
  fileSignatures: new Map(),
110
122
  chunksByFile: new Map(),
111
123
  onStatus,
@@ -0,0 +1,11 @@
1
+ const TURN_FAILURE_MARKER_PATTERN = /^Turn failed before completion: ([a-z][a-z0-9_-]*)\.?$/i;
2
+ export function formatTurnFailureMarker(category) {
3
+ const normalized = category.trim().toLowerCase();
4
+ const safeCategory = /^[a-z][a-z0-9_-]*$/.test(normalized)
5
+ ? normalized
6
+ : 'unknown_error';
7
+ return `Turn failed before completion: ${safeCategory}.`;
8
+ }
9
+ export function isTurnFailureMarker(text) {
10
+ return TURN_FAILURE_MARKER_PATTERN.test(text.trim());
11
+ }
@@ -1,6 +1,7 @@
1
1
  import { createRatatuiBridge } from './tui/bridge.js';
2
2
  import { buildTuiFrame, formatJobElapsed, formatTodoProgress, renderTranscriptEntryLines, } from './tui/build-frame.js';
3
3
  import { createTerminalTitleController } from './tui/terminal-title.js';
4
+ import { captureTerminalWrites, releaseTerminalWrites, } from './tui/terminal-writes.js';
4
5
  export { getSlashCommandSuggestions } from './tui/build-frame.js';
5
6
  import { agentModeLabel, nextAgentMode, } from '../agent-mode.js';
6
7
  import { chat, models } from '../api/index.js';
@@ -9,6 +10,7 @@ import { getJobBufferedOutput, getJobOutputPreview, hasRunningBackgroundJobs, ki
9
10
  import { clearTodos, listTodos, setTodoSession } from '../todo-list.js';
10
11
  import { setScratchSession } from '../scratch-dir.js';
11
12
  import { cancelActiveCommand } from '../executor.js';
13
+ import { isTurnFailureMarker } from '../turn-failure-marker.js';
12
14
  import { setCommandOutputHook, withTuiMode } from '../runtime-mode.js';
13
15
  import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../tool-executor.js';
14
16
  import { clearConversation, } from '../session.js';
@@ -780,7 +782,7 @@ function displayUserTextFromHistoryEntry(entry) {
780
782
  .slice(contentStart, contentEnd === -1 ? text.length : contentEnd)
781
783
  .trim();
782
784
  }
783
- function buildTranscriptFromSessionHistory(history) {
785
+ export function buildTranscriptFromSessionHistory(history) {
784
786
  const entries = [];
785
787
  const pendingCalls = new Map();
786
788
  for (const entry of history) {
@@ -826,6 +828,14 @@ function buildTranscriptFromSessionHistory(history) {
826
828
  }
827
829
  const text = textFromHistoryEntry(entry);
828
830
  if ((entry.role === 'model' || entry.role === 'assistant') && text) {
831
+ if (isTurnFailureMarker(text)) {
832
+ entries.push({
833
+ body: 'This request did not complete.',
834
+ kind: 'system',
835
+ title: 'Previous turn',
836
+ });
837
+ continue;
838
+ }
829
839
  entries.push({ body: text, kind: 'assistant', title: 'Response' });
830
840
  }
831
841
  }
@@ -1273,17 +1283,12 @@ async function saveSessionBoth({ serverSessionClient, session, }) {
1273
1283
  saveSessionState(session);
1274
1284
  await serverSessionClient.save(session);
1275
1285
  }
1276
- function shouldUseRatatuiShell() {
1277
- if (process.env.THEGITAI_PLAIN === '1')
1278
- return false;
1279
- return Boolean(process.stdin.isTTY && process.stdout.isTTY);
1280
- }
1281
- export function shouldUseClientRatatuiShell() {
1282
- return shouldUseRatatuiShell();
1283
- }
1284
1286
  export async function runClientInteractive({ appendPromptHistory, authConfig, debugUi, projectIndex, serverModels, serverSessionClient, session, usageText, initialPrompt, }) {
1285
- if (!shouldUseRatatuiShell()) {
1286
- throw new Error('Client TUI requires an interactive terminal.');
1287
+ if (process.stdin.isTTY !== true) {
1288
+ throw new Error('stdin is not a terminal');
1289
+ }
1290
+ if (process.stdout.isTTY !== true) {
1291
+ throw new Error('stdout is not a terminal');
1287
1292
  }
1288
1293
  await withTuiMode(async () => {
1289
1294
  setBackgroundJobSession(session.sessionId);
@@ -1299,6 +1304,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1299
1304
  let cleanupSudoPasswordPrompt = null;
1300
1305
  let sudoPasswordBuffer = '';
1301
1306
  const bridge = createRatatuiBridge();
1307
+ captureTerminalWrites();
1302
1308
  const { handleShellKeyEvent } = await import('./tui/shell-input.js');
1303
1309
  let terminalCols = 80;
1304
1310
  let terminalRows = 24;
@@ -1343,7 +1349,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1343
1349
  store.update((current) => current.status === status ? { ...current, status: 'Ready' } : current);
1344
1350
  }, 2500);
1345
1351
  };
1346
- const terminalTitle = createTerminalTitleController();
1352
+ const terminalTitle = createTerminalTitleController({
1353
+ write: (title) => bridge.setTitle(title),
1354
+ });
1347
1355
  const syncTerminalTitle = () => {
1348
1356
  const state = store.getState();
1349
1357
  terminalTitle.sync({
@@ -1649,6 +1657,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1649
1657
  status: 'Exiting...',
1650
1658
  }));
1651
1659
  void bridge.close().then(() => {
1660
+ releaseTerminalWrites();
1652
1661
  resolveDone?.();
1653
1662
  });
1654
1663
  };
@@ -1968,7 +1977,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1968
1977
  syncBackgroundJobsState();
1969
1978
  setTodoSession(session.sessionId);
1970
1979
  syncTodosState();
1971
- await saveActiveSession();
1972
1980
  syncShellStateFromSession();
1973
1981
  store.update((next) => ({
1974
1982
  ...next,
@@ -2228,6 +2236,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2228
2236
  activeTurnAbort = turnAbort;
2229
2237
  lastTurnStartedAt = turnStartedAt;
2230
2238
  todosTouchedThisTurn = false;
2239
+ clearTodos();
2240
+ syncTodosState();
2231
2241
  const userEntry = {
2232
2242
  body: input,
2233
2243
  kind: 'user',
@@ -2567,6 +2577,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2567
2577
  setTodoSession(null);
2568
2578
  setBackgroundJobUpdateHook(null);
2569
2579
  await bridge.close();
2580
+ releaseTerminalWrites();
2570
2581
  setCommandOutputHook(null);
2571
2582
  }
2572
2583
  });
@@ -155,6 +155,9 @@ export function createRatatuiBridge() {
155
155
  clear() {
156
156
  writeParent({ op: 'clear' });
157
157
  },
158
+ setTitle(title) {
159
+ writeParent({ op: 'title', text: title });
160
+ },
158
161
  async close() {
159
162
  if (closed)
160
163
  return;
@@ -2,7 +2,7 @@ import { agentModeLabel } from '../../agent-mode.js';
2
2
  import { truncate } from '../../utils.js';
3
3
  import { formatClientTokenUsage } from '../repl.js';
4
4
  import { renderFormattedBodyLines, renderPreformattedBodyLines, } from './markdown-render.js';
5
- import { line, plainLine, span, wrapText } from './text.js';
5
+ import { displayWidth, line, plainLine, sliceToWidth, span, wrapText, } from './text.js';
6
6
  const WORKING_CLOCK_ICON = '◷';
7
7
  const BRAILLE_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴'];
8
8
  const TODO_PANEL_MAX_ROWS = 12;
@@ -17,7 +17,7 @@ const OVERLAY_PANEL_MAX_WIDTH = 86;
17
17
  const OVERLAY_PANEL_MARGIN_LINES = 2;
18
18
  const OVERLAY_BORDER_COLOR = 'yellow';
19
19
  const OVERLAY_WARNING_COLOR = 'ansi256(208)';
20
- const MODEL_PICKER_PANEL_MAX_WIDTH = 120;
20
+ const MODEL_PICKER_PANEL_MAX_WIDTH = 144;
21
21
  const MODEL_PICKER_PANEL_MARGIN_LINES = 2;
22
22
  const MODEL_PICKER_BORDER_COLOR = 'cyan';
23
23
  const MODEL_PICKER_ACCENT_COLOR = 'cyan';
@@ -133,14 +133,14 @@ function diffLinePrefix(kind) {
133
133
  return ' ';
134
134
  }
135
135
  }
136
- function fitLine(content, maxWidth) {
136
+ export function fitLine(content, maxWidth) {
137
137
  if (maxWidth <= 0)
138
138
  return '';
139
- if (content.length <= maxWidth)
139
+ if (displayWidth(content) <= maxWidth)
140
140
  return content;
141
141
  if (maxWidth === 1)
142
142
  return '…';
143
- return `${content.slice(0, maxWidth - 1)}…`;
143
+ return `${sliceToWidth(content, maxWidth - 1)}…`;
144
144
  }
145
145
  export function formatPromptDirectoryLabel(projectRoot, homeDir = process.env.HOME ?? '') {
146
146
  const trimmed = String(projectRoot ?? '').trim();
@@ -573,7 +573,7 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
573
573
  return lines;
574
574
  }
575
575
  function lineCharCount(row) {
576
- return row.spans.reduce((total, item) => total + [...item.text].length, 0);
576
+ return row.spans.reduce((total, item) => total + displayWidth(item.text), 0);
577
577
  }
578
578
  function overlayPanelLine(row, width, color) {
579
579
  const padding = Math.max(0, width - lineCharCount(row));
@@ -689,10 +689,15 @@ function modelPickerHeaderLine(innerWidth) {
689
689
  }
690
690
  return line(modelPickerCell('#', MODEL_PICKER_NUMBER_WIDTH, heading), modelPickerCell('Model', MODEL_PICKER_MODEL_WIDTH, heading), span(MODEL_PICKER_SEPARATOR, { color: 'gray' }), modelPickerCell('Cost', MODEL_PICKER_COST_WIDTH, heading), span(MODEL_PICKER_SEPARATOR, { color: 'gray' }), modelPickerCell('Notes', modelPickerNotesWidth(innerWidth), heading));
691
691
  }
692
- function buildModelPickerPanel(options, selectedIndex, width) {
692
+ function buildModelPickerPanel(options, selectedIndex, width, availableHeight) {
693
693
  const panelWidth = Math.max(28, Math.min(width, MODEL_PICKER_PANEL_MAX_WIDTH));
694
694
  const innerWidth = Math.max(1, panelWidth - 4);
695
- const margin = Array.from({ length: MODEL_PICKER_PANEL_MARGIN_LINES }, () => plainLine(''));
695
+ const compactBodyLineCount = options.length + 6;
696
+ const spacerLineCount = Math.max(0, options.length - 1);
697
+ const useRowSpacing = compactBodyLineCount + spacerLineCount <= availableHeight;
698
+ const bodyLineCount = compactBodyLineCount + (useRowSpacing ? spacerLineCount : 0);
699
+ const marginLineCount = Math.max(0, Math.min(MODEL_PICKER_PANEL_MARGIN_LINES, Math.floor((availableHeight - bodyLineCount) / 2)));
700
+ const margin = Array.from({ length: marginLineCount }, () => plainLine(''));
696
701
  const body = [
697
702
  modelPickerTopBorder(panelWidth),
698
703
  modelPickerPanelSideLine(modelPickerHeaderLine(innerWidth), innerWidth),
@@ -700,6 +705,9 @@ function buildModelPickerPanel(options, selectedIndex, width) {
700
705
  ];
701
706
  options.forEach((option, index) => {
702
707
  body.push(...modelPickerItemLines(option, index === selectedIndex, innerWidth));
708
+ if (useRowSpacing && index < options.length - 1) {
709
+ body.push(modelPickerPanelSideLine(plainLine(''), innerWidth));
710
+ }
703
711
  });
704
712
  const fullHint = '↑/↓ navigate • Enter select • Esc cancel';
705
713
  const compactHint = '↑/↓ • enter • esc';
@@ -774,7 +782,7 @@ function buildCommandPalettePanel(suggestions, selectedIndex, width) {
774
782
  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 }));
775
783
  return [...margin, ...body, ...margin];
776
784
  }
777
- function buildOverlayLines(state, width, nowMs) {
785
+ function buildOverlayLines(state, width, height, nowMs) {
778
786
  const lines = [];
779
787
  const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
780
788
  const innerWidth = Math.max(1, panelWidth - 4);
@@ -847,7 +855,7 @@ function buildOverlayLines(state, width, nowMs) {
847
855
  }
848
856
  if (state.modelPickerOpen) {
849
857
  const options = buildModelPickerOptions(state.currentModelId, state.serverModels);
850
- lines.push(...buildModelPickerPanel(options, state.modelPickerIndex, width));
858
+ lines.push(...buildModelPickerPanel(options, state.modelPickerIndex, width, height));
851
859
  }
852
860
  if (state.jobsPickerOpen) {
853
861
  lines.push(...buildJobsPickerLines(state, width, nowMs));
@@ -931,7 +939,7 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
931
939
  lines: [...composerLines, plainLine(''), ...composerFooterLines(state)],
932
940
  });
933
941
  }
934
- const overlayLines = buildOverlayLines(state, contentWidth, nowMs);
942
+ const overlayLines = buildOverlayLines(state, contentWidth, rows, nowMs);
935
943
  if (overlayLines.length > 0) {
936
944
  sections.push({ kind: 'overlay', lines: overlayLines });
937
945
  }
@@ -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,33 @@ 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];
278
+ const limit = safeWidth - (rows.length === 1 && indent ? displayWidth(prefix) : 0);
285
279
  let remaining = part.text;
286
280
  while (remaining.length > 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
+ const head = sliceToWidth(remaining, room);
295
+ if (displayWidth(head) > room && current.length > 0) {
296
+ startRow();
297
+ current = rows[rows.length - 1];
298
+ continue;
299
+ }
300
+ current.push(span(head, styleFromSpan(part)));
301
+ remaining = remaining.slice(head.length);
300
302
  startRow();
303
+ current = rows[rows.length - 1];
301
304
  }
302
305
  };
303
306
  for (const segment of segments) {
@@ -316,36 +319,35 @@ function wrapInlineToLines(text, width, bodyColor, prefix = '') {
316
319
  return line(...bodySpans);
317
320
  });
318
321
  }
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
322
  function renderTableLines(table, width) {
326
323
  const columnWidths = fitTableColumnWidths(table.columnWidths, width);
324
+ const border = (left, joint, right) => line(span(left +
325
+ columnWidths.map((colWidth) => '─'.repeat(colWidth + 2)).join(joint) +
326
+ right, { color: 'cyan', dim: true }));
327
327
  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' }));
328
+ const wrapped = columnWidths.map((colWidth, index) => wrapToWidth(stripInlineFormattingForWidth(cells[index] ?? ''), colWidth));
329
+ const height = Math.max(1, ...wrapped.map((cellLines) => cellLines.length));
330
+ const rows = [];
331
+ for (let row = 0; row < height; row++) {
332
+ const spans = [span('│', { color: 'cyan' })];
333
+ for (let column = 0; column < columnWidths.length; column++) {
334
+ const text = wrapped[column]?.[row] ?? '';
335
+ spans.push(span(` ${padToWidth(text, columnWidths[column])} `, {
336
+ color: 'cyan',
337
+ bold,
338
+ }));
339
+ spans.push(span('│', { color: 'cyan' }));
340
+ }
341
+ rows.push(line(...spans));
342
+ }
343
+ return rows;
333
344
  };
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
345
  return [
344
- top,
345
- renderRow(table.headers, true),
346
- separator,
347
- ...table.rows.map((row) => renderRow(row, false)),
348
- bottom,
346
+ border('┌', '┬', '┐'),
347
+ ...renderRow(table.headers, true),
348
+ border('├', '┼', '┤'),
349
+ ...table.rows.flatMap((row) => renderRow(row, false)),
350
+ border('└', '┴', '┘'),
349
351
  ];
350
352
  }
351
353
  function getEntryColor(kind) {
@@ -410,10 +412,7 @@ export function renderFormattedBodyLines(body, width, kind) {
410
412
  }
411
413
  if (formattedLine.kind === 'table' && formattedLine.table) {
412
414
  output.push(...renderTableLines(formattedLine.table, bodyWidth).map((tableLine) => {
413
- const spans = tableLine.spans.map((part) => ({
414
- ...part,
415
- text: ` ${part.text}`,
416
- }));
415
+ const spans = tableLine.spans.map((part, index) => index === 0 ? { ...part, text: ` ${part.text}` } : part);
417
416
  return { spans };
418
417
  }));
419
418
  continue;
@@ -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
+ }