@thegitai/cli 1.0.0-preview.4 → 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/dist/src/api/chat.js +99 -21
- package/dist/src/project-index.js +13 -1
- package/dist/src/ui/repl.js +9 -1
- package/dist/src/ui/tui/bridge.js +3 -0
- package/dist/src/ui/tui/build-frame.js +5 -5
- 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 +5 -5
- package/dist/src/markdown-renderer.js +0 -112
package/dist/src/api/chat.js
CHANGED
|
@@ -52,6 +52,7 @@ function parseSseBlock(block) {
|
|
|
52
52
|
return { event, data: text };
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
let toolStateSeqCounter = 0;
|
|
55
56
|
function toolStateFromSession(session) {
|
|
56
57
|
return {
|
|
57
58
|
autoYes: session.autoYes,
|
|
@@ -185,6 +186,7 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
|
|
|
185
186
|
toolCallId: event.call.id,
|
|
186
187
|
result,
|
|
187
188
|
toolState: toolStateFromSession(session),
|
|
189
|
+
toolStateSeq: ++toolStateSeqCounter,
|
|
188
190
|
};
|
|
189
191
|
const trace = createTraceContext(traceId);
|
|
190
192
|
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/tool-result`, {
|
|
@@ -203,6 +205,29 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
|
|
|
203
205
|
throw await readErrorResponse(response, trace.traceId);
|
|
204
206
|
}
|
|
205
207
|
}
|
|
208
|
+
const turnIdOverrides = new WeakMap();
|
|
209
|
+
function enterServerTurnId(session, serverSessionTurnId) {
|
|
210
|
+
const active = turnIdOverrides.get(session);
|
|
211
|
+
if (active) {
|
|
212
|
+
active.depth += 1;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
turnIdOverrides.set(session, {
|
|
216
|
+
previousTurnId: session.turnState.id,
|
|
217
|
+
depth: 1,
|
|
218
|
+
});
|
|
219
|
+
session.turnState.id = serverSessionTurnId;
|
|
220
|
+
}
|
|
221
|
+
function exitServerTurnId(session) {
|
|
222
|
+
const active = turnIdOverrides.get(session);
|
|
223
|
+
if (!active)
|
|
224
|
+
return;
|
|
225
|
+
active.depth -= 1;
|
|
226
|
+
if (active.depth === 0) {
|
|
227
|
+
session.turnState.id = active.previousTurnId;
|
|
228
|
+
turnIdOverrides.delete(session);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
206
231
|
async function executeAndPostToolResult({ config, projectIndex, session, event, input, fetchImpl, signal, traceId, }) {
|
|
207
232
|
const turnId = String(event?.turnId ?? '').trim();
|
|
208
233
|
if (!turnId || !event?.call?.id || !event.call.name) {
|
|
@@ -211,10 +236,9 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
|
|
|
211
236
|
if (signal?.aborted) {
|
|
212
237
|
throw new TurnCancelledError();
|
|
213
238
|
}
|
|
214
|
-
const previousTurnId = session.turnState.id;
|
|
215
239
|
const serverSessionTurnId = String(event.sessionTurnId ?? '').trim();
|
|
216
240
|
if (serverSessionTurnId) {
|
|
217
|
-
session
|
|
241
|
+
enterServerTurnId(session, serverSessionTurnId);
|
|
218
242
|
if (!session.clientState.safety.checkpoints.some((checkpoint) => checkpoint.turnId === serverSessionTurnId)) {
|
|
219
243
|
createPromptCheckpoint(session.clientState.safety, 'prompt boundary', serverSessionTurnId);
|
|
220
244
|
}
|
|
@@ -237,7 +261,9 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
|
|
|
237
261
|
});
|
|
238
262
|
}
|
|
239
263
|
finally {
|
|
240
|
-
|
|
264
|
+
if (serverSessionTurnId) {
|
|
265
|
+
exitServerTurnId(session);
|
|
266
|
+
}
|
|
241
267
|
}
|
|
242
268
|
}
|
|
243
269
|
async function consumeTurnStream({ response, config, projectIndex, session, input, fetchImpl, signal, traceId, }) {
|
|
@@ -250,6 +276,31 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
250
276
|
const finalResult = {
|
|
251
277
|
current: null,
|
|
252
278
|
};
|
|
279
|
+
const pendingParallelTools = [];
|
|
280
|
+
let firstParallelFailure = null;
|
|
281
|
+
let rejectOnParallelFailure = null;
|
|
282
|
+
const parallelToolFailure = new Promise((_, reject) => {
|
|
283
|
+
rejectOnParallelFailure = reject;
|
|
284
|
+
});
|
|
285
|
+
parallelToolFailure.catch(() => { });
|
|
286
|
+
function recordParallelFailure(error) {
|
|
287
|
+
const failure = error ?? new Error('Local tool execution failed.');
|
|
288
|
+
if (firstParallelFailure == null) {
|
|
289
|
+
firstParallelFailure = failure;
|
|
290
|
+
rejectOnParallelFailure?.(failure);
|
|
291
|
+
}
|
|
292
|
+
return failure;
|
|
293
|
+
}
|
|
294
|
+
async function drainParallelTools() {
|
|
295
|
+
if (!pendingParallelTools.length)
|
|
296
|
+
return;
|
|
297
|
+
const pending = pendingParallelTools.splice(0);
|
|
298
|
+
const outcomes = await Promise.all(pending);
|
|
299
|
+
for (const outcome of outcomes) {
|
|
300
|
+
if (outcome != null)
|
|
301
|
+
throw outcome;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
253
304
|
async function handleEvent(event) {
|
|
254
305
|
if (event.event === 'status') {
|
|
255
306
|
const message = publicStatusMessage(event.data);
|
|
@@ -261,11 +312,26 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
261
312
|
return;
|
|
262
313
|
}
|
|
263
314
|
if (event.event === 'tool-call') {
|
|
315
|
+
const data = event.data;
|
|
316
|
+
if (data?.parallelSafe === true) {
|
|
317
|
+
pendingParallelTools.push(executeAndPostToolResult({
|
|
318
|
+
config,
|
|
319
|
+
projectIndex,
|
|
320
|
+
session,
|
|
321
|
+
event: data,
|
|
322
|
+
input,
|
|
323
|
+
fetchImpl,
|
|
324
|
+
signal,
|
|
325
|
+
traceId,
|
|
326
|
+
}).then(() => null, (error) => recordParallelFailure(error)));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
await drainParallelTools();
|
|
264
330
|
await executeAndPostToolResult({
|
|
265
331
|
config,
|
|
266
332
|
projectIndex,
|
|
267
333
|
session,
|
|
268
|
-
event:
|
|
334
|
+
event: data,
|
|
269
335
|
input,
|
|
270
336
|
fetchImpl,
|
|
271
337
|
signal,
|
|
@@ -281,10 +347,12 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
281
347
|
return;
|
|
282
348
|
}
|
|
283
349
|
if (event.event === 'result') {
|
|
350
|
+
await drainParallelTools();
|
|
284
351
|
finalResult.current = event.data;
|
|
285
352
|
return;
|
|
286
353
|
}
|
|
287
354
|
if (event.event === 'cancelled' || event.event === 'error') {
|
|
355
|
+
await drainParallelTools().catch(() => { });
|
|
288
356
|
const message = String(event.data?.message ?? 'Server chat failed.');
|
|
289
357
|
if (event.event === 'cancelled') {
|
|
290
358
|
throw new TurnCancelledError(message);
|
|
@@ -292,24 +360,34 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
292
360
|
throw new ChatTurnFailedError(message, typeof event.data?.category === 'string' ? event.data.category : 'unknown_error', Boolean(event.data?.retryable), typeof event.data?.traceId === 'string' ? event.data.traceId : traceId);
|
|
293
361
|
}
|
|
294
362
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
363
|
+
try {
|
|
364
|
+
while (true) {
|
|
365
|
+
if (signal?.aborted) {
|
|
366
|
+
await reader.cancel().catch(() => { });
|
|
367
|
+
throw new TurnCancelledError();
|
|
368
|
+
}
|
|
369
|
+
const read = await Promise.race([reader.read(), parallelToolFailure]);
|
|
370
|
+
if (read.done)
|
|
371
|
+
break;
|
|
372
|
+
buffer += decoder.decode(read.value, { stream: true });
|
|
373
|
+
let separatorIndex = buffer.indexOf('\n\n');
|
|
374
|
+
while (separatorIndex !== -1) {
|
|
375
|
+
const block = buffer.slice(0, separatorIndex);
|
|
376
|
+
buffer = buffer.slice(separatorIndex + 2);
|
|
377
|
+
const event = parseSseBlock(block);
|
|
378
|
+
if (event)
|
|
379
|
+
await handleEvent(event);
|
|
380
|
+
separatorIndex = buffer.indexOf('\n\n');
|
|
381
|
+
}
|
|
312
382
|
}
|
|
383
|
+
await drainParallelTools();
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
await reader.cancel().catch(() => { });
|
|
387
|
+
throw error;
|
|
388
|
+
}
|
|
389
|
+
finally {
|
|
390
|
+
await drainParallelTools().catch(() => { });
|
|
313
391
|
}
|
|
314
392
|
buffer += decoder.decode();
|
|
315
393
|
const tail = buffer.trim();
|
|
@@ -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,
|
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';
|
|
@@ -1303,6 +1304,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1303
1304
|
let cleanupSudoPasswordPrompt = null;
|
|
1304
1305
|
let sudoPasswordBuffer = '';
|
|
1305
1306
|
const bridge = createRatatuiBridge();
|
|
1307
|
+
captureTerminalWrites();
|
|
1306
1308
|
const { handleShellKeyEvent } = await import('./tui/shell-input.js');
|
|
1307
1309
|
let terminalCols = 80;
|
|
1308
1310
|
let terminalRows = 24;
|
|
@@ -1347,7 +1349,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1347
1349
|
store.update((current) => current.status === status ? { ...current, status: 'Ready' } : current);
|
|
1348
1350
|
}, 2500);
|
|
1349
1351
|
};
|
|
1350
|
-
const terminalTitle = createTerminalTitleController(
|
|
1352
|
+
const terminalTitle = createTerminalTitleController({
|
|
1353
|
+
write: (title) => bridge.setTitle(title),
|
|
1354
|
+
});
|
|
1351
1355
|
const syncTerminalTitle = () => {
|
|
1352
1356
|
const state = store.getState();
|
|
1353
1357
|
terminalTitle.sync({
|
|
@@ -1653,6 +1657,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1653
1657
|
status: 'Exiting...',
|
|
1654
1658
|
}));
|
|
1655
1659
|
void bridge.close().then(() => {
|
|
1660
|
+
releaseTerminalWrites();
|
|
1656
1661
|
resolveDone?.();
|
|
1657
1662
|
});
|
|
1658
1663
|
};
|
|
@@ -2231,6 +2236,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2231
2236
|
activeTurnAbort = turnAbort;
|
|
2232
2237
|
lastTurnStartedAt = turnStartedAt;
|
|
2233
2238
|
todosTouchedThisTurn = false;
|
|
2239
|
+
clearTodos();
|
|
2240
|
+
syncTodosState();
|
|
2234
2241
|
const userEntry = {
|
|
2235
2242
|
body: input,
|
|
2236
2243
|
kind: 'user',
|
|
@@ -2570,6 +2577,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2570
2577
|
setTodoSession(null);
|
|
2571
2578
|
setBackgroundJobUpdateHook(null);
|
|
2572
2579
|
await bridge.close();
|
|
2580
|
+
releaseTerminalWrites();
|
|
2573
2581
|
setCommandOutputHook(null);
|
|
2574
2582
|
}
|
|
2575
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;
|
|
@@ -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));
|
|
@@ -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
|
+
}
|
package/dist/src/ui/tui/text.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
const CSI_PATTERN = /\u001B\[[0-9;?]*[ -\/]*[@-~]/g;
|
|
2
|
+
const OSC_PATTERN = /\u001B\][^\u0007\u001B]*(?:\u0007|\u001B\\)?/g;
|
|
3
|
+
const CONTROL_CHAR_PATTERN = /[\u0000-\u0008\u000B-\u001F\u007F]/g;
|
|
4
|
+
export function stripControlCharacters(text) {
|
|
5
|
+
return String(text ?? '')
|
|
6
|
+
.replace(OSC_PATTERN, '')
|
|
7
|
+
.replace(CSI_PATTERN, '')
|
|
8
|
+
.replace(CONTROL_CHAR_PATTERN, '');
|
|
9
|
+
}
|
|
1
10
|
export function span(text, style = {}) {
|
|
2
|
-
return { text, ...style };
|
|
11
|
+
return { text: stripControlCharacters(text), ...style };
|
|
3
12
|
}
|
|
4
13
|
export function line(...spans) {
|
|
5
14
|
return { spans };
|
|
@@ -14,10 +23,11 @@ export function wrapText(text, width) {
|
|
|
14
23
|
const lines = [];
|
|
15
24
|
for (const rawLine of text.split('\n')) {
|
|
16
25
|
let remaining = rawLine;
|
|
17
|
-
while (remaining
|
|
18
|
-
|
|
26
|
+
while (displayWidth(remaining) > safeWidth) {
|
|
27
|
+
const head = sliceToWidth(remaining, safeWidth);
|
|
28
|
+
let breakAt = head.lastIndexOf(' ');
|
|
19
29
|
if (breakAt <= 0)
|
|
20
|
-
breakAt =
|
|
30
|
+
breakAt = head.length;
|
|
21
31
|
lines.push(remaining.slice(0, breakAt).trimEnd());
|
|
22
32
|
remaining = remaining.slice(breakAt).trimStart();
|
|
23
33
|
}
|
|
@@ -28,3 +38,147 @@ export function wrapText(text, width) {
|
|
|
28
38
|
export function joinLines(blocks) {
|
|
29
39
|
return blocks.flat();
|
|
30
40
|
}
|
|
41
|
+
function isZeroWidthCodePoint(codePoint) {
|
|
42
|
+
return (codePoint === 0x200d ||
|
|
43
|
+
(codePoint >= 0x0300 && codePoint <= 0x036f) ||
|
|
44
|
+
(codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
|
|
45
|
+
(codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
|
|
46
|
+
(codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
|
|
47
|
+
(codePoint >= 0xfe00 && codePoint <= 0xfe0f) ||
|
|
48
|
+
(codePoint >= 0xfe20 && codePoint <= 0xfe2f));
|
|
49
|
+
}
|
|
50
|
+
function isWideCodePoint(codePoint) {
|
|
51
|
+
return ((codePoint >= 0x1100 && codePoint <= 0x115f) ||
|
|
52
|
+
codePoint === 0x231a ||
|
|
53
|
+
codePoint === 0x231b ||
|
|
54
|
+
(codePoint >= 0x23e9 && codePoint <= 0x23ec) ||
|
|
55
|
+
codePoint === 0x23f0 ||
|
|
56
|
+
codePoint === 0x23f3 ||
|
|
57
|
+
(codePoint >= 0x25fd && codePoint <= 0x25fe) ||
|
|
58
|
+
(codePoint >= 0x2614 && codePoint <= 0x2615) ||
|
|
59
|
+
(codePoint >= 0x2648 && codePoint <= 0x2653) ||
|
|
60
|
+
codePoint === 0x267f ||
|
|
61
|
+
codePoint === 0x2693 ||
|
|
62
|
+
codePoint === 0x26a1 ||
|
|
63
|
+
(codePoint >= 0x26aa && codePoint <= 0x26ab) ||
|
|
64
|
+
(codePoint >= 0x26bd && codePoint <= 0x26be) ||
|
|
65
|
+
(codePoint >= 0x26c4 && codePoint <= 0x26c5) ||
|
|
66
|
+
codePoint === 0x26ce ||
|
|
67
|
+
codePoint === 0x26d4 ||
|
|
68
|
+
codePoint === 0x26ea ||
|
|
69
|
+
(codePoint >= 0x26f2 && codePoint <= 0x26f3) ||
|
|
70
|
+
codePoint === 0x26f5 ||
|
|
71
|
+
codePoint === 0x26fa ||
|
|
72
|
+
codePoint === 0x26fd ||
|
|
73
|
+
codePoint === 0x2705 ||
|
|
74
|
+
(codePoint >= 0x270a && codePoint <= 0x270b) ||
|
|
75
|
+
codePoint === 0x2728 ||
|
|
76
|
+
codePoint === 0x274c ||
|
|
77
|
+
codePoint === 0x274e ||
|
|
78
|
+
(codePoint >= 0x2753 && codePoint <= 0x2755) ||
|
|
79
|
+
codePoint === 0x2757 ||
|
|
80
|
+
(codePoint >= 0x2795 && codePoint <= 0x2797) ||
|
|
81
|
+
codePoint === 0x27b0 ||
|
|
82
|
+
codePoint === 0x27bf ||
|
|
83
|
+
(codePoint >= 0x2b1b && codePoint <= 0x2b1c) ||
|
|
84
|
+
codePoint === 0x2b50 ||
|
|
85
|
+
codePoint === 0x2b55 ||
|
|
86
|
+
(codePoint >= 0x2e80 && codePoint <= 0x303e) ||
|
|
87
|
+
(codePoint >= 0x3041 && codePoint <= 0x33ff) ||
|
|
88
|
+
(codePoint >= 0x3400 && codePoint <= 0x4dbf) ||
|
|
89
|
+
(codePoint >= 0x4e00 && codePoint <= 0x9fff) ||
|
|
90
|
+
(codePoint >= 0xa000 && codePoint <= 0xa4cf) ||
|
|
91
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
92
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
93
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
|
|
94
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
95
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
96
|
+
(codePoint >= 0x1f300 && codePoint <= 0x1f64f) ||
|
|
97
|
+
(codePoint >= 0x1f680 && codePoint <= 0x1f6ff) ||
|
|
98
|
+
(codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
|
|
99
|
+
(codePoint >= 0x1fa70 && codePoint <= 0x1faff) ||
|
|
100
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd));
|
|
101
|
+
}
|
|
102
|
+
export function displayWidth(text) {
|
|
103
|
+
const chars = [...String(text ?? '')];
|
|
104
|
+
let width = 0;
|
|
105
|
+
for (let index = 0; index < chars.length; index++) {
|
|
106
|
+
const codePoint = chars[index].codePointAt(0);
|
|
107
|
+
if (isZeroWidthCodePoint(codePoint))
|
|
108
|
+
continue;
|
|
109
|
+
const next = chars[index + 1]?.codePointAt(0);
|
|
110
|
+
if (next === 0xfe0f) {
|
|
111
|
+
width += 2;
|
|
112
|
+
index++;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (next === 0xfe0e) {
|
|
116
|
+
width += 1;
|
|
117
|
+
index++;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
width += isWideCodePoint(codePoint) ? 2 : 1;
|
|
121
|
+
}
|
|
122
|
+
return width;
|
|
123
|
+
}
|
|
124
|
+
export function sliceToWidth(text, width) {
|
|
125
|
+
const limit = Math.max(1, Math.floor(width));
|
|
126
|
+
const chars = [...String(text ?? '')];
|
|
127
|
+
let out = '';
|
|
128
|
+
let used = 0;
|
|
129
|
+
for (let index = 0; index < chars.length; index++) {
|
|
130
|
+
const char = chars[index];
|
|
131
|
+
const next = chars[index + 1];
|
|
132
|
+
const selector = next === '️' || next === '︎';
|
|
133
|
+
const cluster = selector ? `${char}${next}` : char;
|
|
134
|
+
const clusterWidth = displayWidth(cluster);
|
|
135
|
+
if (used + clusterWidth > limit)
|
|
136
|
+
break;
|
|
137
|
+
out += cluster;
|
|
138
|
+
used += clusterWidth;
|
|
139
|
+
if (selector)
|
|
140
|
+
index++;
|
|
141
|
+
}
|
|
142
|
+
if (!out)
|
|
143
|
+
return chars[0] ?? '';
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
export function padToWidth(text, width) {
|
|
147
|
+
const current = displayWidth(text);
|
|
148
|
+
if (current >= width)
|
|
149
|
+
return text;
|
|
150
|
+
return text + ' '.repeat(width - current);
|
|
151
|
+
}
|
|
152
|
+
export function wrapToWidth(text, width) {
|
|
153
|
+
const limit = Math.max(1, Math.floor(width));
|
|
154
|
+
const out = [];
|
|
155
|
+
for (const rawLine of String(text ?? '').split('\n')) {
|
|
156
|
+
let current = '';
|
|
157
|
+
const flush = () => {
|
|
158
|
+
out.push(current);
|
|
159
|
+
current = '';
|
|
160
|
+
};
|
|
161
|
+
for (const token of rawLine.split(/\s+/).filter(Boolean)) {
|
|
162
|
+
let rest = token;
|
|
163
|
+
while (displayWidth(rest) > limit) {
|
|
164
|
+
if (current)
|
|
165
|
+
flush();
|
|
166
|
+
const head = sliceToWidth(rest, limit);
|
|
167
|
+
out.push(head);
|
|
168
|
+
rest = rest.slice(head.length);
|
|
169
|
+
}
|
|
170
|
+
if (!rest)
|
|
171
|
+
continue;
|
|
172
|
+
const candidate = current ? `${current} ${rest}` : rest;
|
|
173
|
+
if (displayWidth(candidate) > limit) {
|
|
174
|
+
flush();
|
|
175
|
+
current = rest;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
current = candidate;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
flush();
|
|
182
|
+
}
|
|
183
|
+
return out.length > 0 ? out : [''];
|
|
184
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thegitai/cli",
|
|
3
|
-
"version": "1.0.0-preview.
|
|
3
|
+
"version": "1.0.0-preview.5",
|
|
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.
|
|
41
|
-
"@thegitai/tui-darwin-x64": "1.0.0-preview.
|
|
42
|
-
"@thegitai/tui-linux-x64": "1.0.0-preview.
|
|
43
|
-
"@thegitai/tui-win32-x64": "1.0.0-preview.
|
|
40
|
+
"@thegitai/tui-darwin-arm64": "1.0.0-preview.5",
|
|
41
|
+
"@thegitai/tui-darwin-x64": "1.0.0-preview.5",
|
|
42
|
+
"@thegitai/tui-linux-x64": "1.0.0-preview.5",
|
|
43
|
+
"@thegitai/tui-win32-x64": "1.0.0-preview.5",
|
|
44
44
|
"@vscode/ripgrep": "1.18.0"
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import chalk from './colors.js';
|
|
2
|
-
function renderInline(text) {
|
|
3
|
-
const parts = String(text ?? '').split(/(`[^`]+`)/g);
|
|
4
|
-
return parts
|
|
5
|
-
.map((part) => part.startsWith('`') && part.endsWith('`') && part.length > 1
|
|
6
|
-
? chalk.cyan(part.slice(1, -1))
|
|
7
|
-
: part)
|
|
8
|
-
.join('');
|
|
9
|
-
}
|
|
10
|
-
function isTableSeparator(line) {
|
|
11
|
-
const cells = splitTableRow(line);
|
|
12
|
-
return (cells.length > 1 &&
|
|
13
|
-
cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim())));
|
|
14
|
-
}
|
|
15
|
-
function splitTableRow(line) {
|
|
16
|
-
const trimmed = String(line ?? '').trim();
|
|
17
|
-
if (!trimmed.includes('|'))
|
|
18
|
-
return [];
|
|
19
|
-
return trimmed
|
|
20
|
-
.replace(/^\|/, '')
|
|
21
|
-
.replace(/\|$/, '')
|
|
22
|
-
.split('|')
|
|
23
|
-
.map((cell) => cell.trim());
|
|
24
|
-
}
|
|
25
|
-
function tableWidth(text) {
|
|
26
|
-
return text.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
27
|
-
}
|
|
28
|
-
function padCell(text, width) {
|
|
29
|
-
return `${text}${' '.repeat(Math.max(0, width - tableWidth(text)))}`;
|
|
30
|
-
}
|
|
31
|
-
function renderTable(rows) {
|
|
32
|
-
if (rows.length < 2 || !isTableSeparator(rows[1].join('|')))
|
|
33
|
-
return [];
|
|
34
|
-
const headers = rows[0];
|
|
35
|
-
const body = rows.slice(2).filter((row) => row.length > 0);
|
|
36
|
-
const columnCount = Math.max(headers.length, ...body.map((row) => row.length));
|
|
37
|
-
const normalizedRows = [headers, ...body].map((row) => Array.from({ length: columnCount }, (_, index) => renderInline(row[index] ?? '')));
|
|
38
|
-
const widths = Array.from({ length: columnCount }, (_, index) => Math.max(...normalizedRows.map((row) => tableWidth(row[index] ?? '')), 3));
|
|
39
|
-
const border = `+${widths.map((width) => '-'.repeat(width + 2)).join('+')}+`;
|
|
40
|
-
const renderRow = (row) => `| ${row.map((cell, index) => padCell(cell, widths[index])).join(' | ')} |`;
|
|
41
|
-
return [
|
|
42
|
-
border,
|
|
43
|
-
renderRow(normalizedRows[0].map((cell) => chalk.bold(cell))),
|
|
44
|
-
border,
|
|
45
|
-
...normalizedRows.slice(1).map(renderRow),
|
|
46
|
-
border,
|
|
47
|
-
];
|
|
48
|
-
}
|
|
49
|
-
function readTableBlock(lines, startIndex) {
|
|
50
|
-
if (startIndex + 1 >= lines.length)
|
|
51
|
-
return null;
|
|
52
|
-
const header = splitTableRow(lines[startIndex]);
|
|
53
|
-
const separator = splitTableRow(lines[startIndex + 1]);
|
|
54
|
-
if (header.length < 2 || separator.length < 2 || !isTableSeparator(lines[startIndex + 1])) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
const rows = [header, separator];
|
|
58
|
-
let index = startIndex + 2;
|
|
59
|
-
while (index < lines.length) {
|
|
60
|
-
const row = splitTableRow(lines[index]);
|
|
61
|
-
if (row.length < 2)
|
|
62
|
-
break;
|
|
63
|
-
rows.push(row);
|
|
64
|
-
index += 1;
|
|
65
|
-
}
|
|
66
|
-
const rendered = renderTable(rows);
|
|
67
|
-
return rendered.length ? { rendered, nextIndex: index } : null;
|
|
68
|
-
}
|
|
69
|
-
export function renderMarkdownForTerminal(markdown) {
|
|
70
|
-
const lines = String(markdown ?? '').replace(/\r\n?/g, '\n').split('\n');
|
|
71
|
-
const output = [];
|
|
72
|
-
let inCodeBlock = false;
|
|
73
|
-
for (let index = 0; index < lines.length; index++) {
|
|
74
|
-
const line = lines[index];
|
|
75
|
-
if (/^\s*```/.test(line)) {
|
|
76
|
-
inCodeBlock = !inCodeBlock;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (inCodeBlock) {
|
|
80
|
-
output.push(chalk.dim(` ${line}`));
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
const table = readTableBlock(lines, index);
|
|
84
|
-
if (table) {
|
|
85
|
-
output.push(...table.rendered);
|
|
86
|
-
index = table.nextIndex - 1;
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
const heading = line.match(/^\s{0,3}#{1,6}\s+(.+)$/);
|
|
90
|
-
if (heading) {
|
|
91
|
-
output.push(chalk.bold(renderInline(heading[1].trim())));
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
const bullet = line.match(/^(\s*)[-*]\s+(.+)$/);
|
|
95
|
-
if (bullet) {
|
|
96
|
-
output.push(`${bullet[1]}- ${renderInline(bullet[2].trim())}`);
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
const numbered = line.match(/^(\s*)\d+[.)]\s+(.+)$/);
|
|
100
|
-
if (numbered) {
|
|
101
|
-
output.push(`${numbered[1]}- ${renderInline(numbered[2].trim())}`);
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
const quote = line.match(/^\s*>\s?(.+)$/);
|
|
105
|
-
if (quote) {
|
|
106
|
-
output.push(chalk.dim(`> ${renderInline(quote[1].trim())}`));
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
output.push(renderInline(line));
|
|
110
|
-
}
|
|
111
|
-
return output.join('\n').trimEnd();
|
|
112
|
-
}
|