@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.
- package/README.md +12 -4
- package/dist/bin/ai.js +46 -288
- package/dist/src/api/chat.js +101 -22
- package/dist/src/help-text.js +16 -5
- package/dist/src/project-index.js +13 -1
- package/dist/src/turn-failure-marker.js +11 -0
- package/dist/src/ui/repl.js +24 -13
- package/dist/src/ui/tui/bridge.js +3 -0
- package/dist/src/ui/tui/build-frame.js +19 -11
- package/dist/src/ui/tui/markdown-render.js +72 -73
- package/dist/src/ui/tui/terminal-title.js +3 -0
- package/dist/src/ui/tui/terminal-writes.js +48 -0
- package/dist/src/ui/tui/text.js +158 -4
- package/package.json +18 -6
- package/dist/src/markdown-renderer.js +0 -112
|
@@ -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
|
|
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
|
+
}
|
package/dist/src/ui/repl.js
CHANGED
|
@@ -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 (
|
|
1286
|
-
throw new Error('
|
|
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
|
});
|
|
@@ -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 =
|
|
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
|
|
139
|
+
if (displayWidth(content) <= maxWidth)
|
|
140
140
|
return content;
|
|
141
141
|
if (maxWidth === 1)
|
|
142
142
|
return '…';
|
|
143
|
-
return `${content
|
|
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 +
|
|
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
|
|
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
|
|
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
|
|
114
|
-
if (
|
|
115
|
-
return
|
|
116
|
-
const
|
|
117
|
-
const budget = Math.
|
|
118
|
-
const total =
|
|
119
|
-
if (
|
|
120
|
-
return
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
138
|
+
widths[index]++;
|
|
139
|
+
remaining--;
|
|
140
|
+
grew = true;
|
|
145
141
|
}
|
|
146
|
-
if (
|
|
142
|
+
if (!grew)
|
|
147
143
|
break;
|
|
148
|
-
scaled[targetIndex]--;
|
|
149
|
-
remaining++;
|
|
150
144
|
}
|
|
151
|
-
return
|
|
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(
|
|
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
|
-
|
|
284
|
-
const limit = safeWidth - (indent ? prefix
|
|
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
|
-
|
|
287
|
+
const width = displayWidth(remaining);
|
|
288
|
+
if (width <= room) {
|
|
293
289
|
current.push(span(remaining, styleFromSpan(part)));
|
|
294
|
-
rowWidth +=
|
|
290
|
+
rowWidth += width;
|
|
295
291
|
remaining = '';
|
|
296
292
|
break;
|
|
297
293
|
}
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
345
|
-
renderRow(table.headers, true),
|
|
346
|
-
|
|
347
|
-
...table.rows.
|
|
348
|
-
|
|
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
|
+
}
|