@thegitai/cli 1.0.0-beta.9 → 1.0.0-preview.10
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 +49 -3
- package/dist/bin/ai.js +83 -197
- package/dist/parsers/NOTICE +18 -0
- package/dist/src/agent-mode.js +5 -0
- package/dist/src/api/auth.js +4 -4
- package/dist/src/api/browser-login.js +72 -19
- package/dist/src/api/chat.js +182 -35
- package/dist/src/api/http.js +65 -4
- package/dist/src/api/models.js +33 -22
- package/dist/src/artifact-policy.js +3 -0
- package/dist/src/background-jobs.js +410 -0
- package/dist/src/cli-args.js +0 -5
- package/dist/src/client-environment.js +2 -0
- package/dist/src/colors.js +50 -0
- package/dist/src/core/clipboard.js +19 -0
- package/dist/src/core/image-path-extractor.js +144 -0
- package/dist/src/executor.js +48 -12
- package/dist/src/help-text.js +30 -13
- package/dist/src/patcher.js +97 -12
- package/dist/src/project-index.js +13 -1
- package/dist/src/project-orientation.js +99 -0
- package/dist/src/scanner.js +50 -12
- package/dist/src/scratch-dir.js +75 -0
- package/dist/src/secret-preview.js +0 -10
- package/dist/src/session-safety.js +0 -19
- package/dist/src/session-store.js +52 -21
- package/dist/src/session.js +8 -0
- package/dist/src/todo-list.js +106 -0
- package/dist/src/tool-executor.js +194 -21
- package/dist/src/tools/delete-file.js +23 -5
- package/dist/src/tools/index.js +6 -0
- package/dist/src/tools/patch-file.js +33 -7
- package/dist/src/tools/path-suggest.js +81 -8
- package/dist/src/tools/read-document.js +2 -2
- package/dist/src/tools/read-file.js +17 -8
- package/dist/src/tools/replace-document-text.js +10 -12
- package/dist/src/tools/restore-checkpoint.js +1 -1
- package/dist/src/tools/run-command.js +109 -24
- package/dist/src/tools/run-node-script.js +27 -5
- package/dist/src/tools/shell-job-kill.js +48 -0
- package/dist/src/tools/shell-job-output.js +51 -0
- package/dist/src/tools/str-replace.js +33 -7
- package/dist/src/tools/undo-edit.js +1 -1
- package/dist/src/tools/update-todos.js +27 -0
- package/dist/src/tools/write-file.js +26 -6
- package/dist/src/tree-sitter-runtime.js +8 -1
- package/dist/src/turn-failure-marker.js +11 -0
- package/dist/src/ui/prompt-history-store.js +1 -1
- package/dist/src/ui/repl.js +500 -71
- package/dist/src/ui/tui/bridge.js +3 -4
- package/dist/src/ui/tui/build-frame.js +393 -100
- package/dist/src/ui/tui/markdown-render.js +72 -73
- package/dist/src/ui/tui/shell-input.js +75 -17
- package/dist/src/ui/tui/terminal-title.js +84 -0
- package/dist/src/ui/tui/terminal-writes.js +48 -0
- package/dist/src/ui/tui/text.js +158 -4
- package/dist/src/utils.js +9 -0
- package/dist/src/version.js +0 -6
- package/dist/vendor/web-tree-sitter/LICENSE +21 -0
- package/dist/vendor/web-tree-sitter/NOTICE +13 -0
- package/dist/vendor/web-tree-sitter/web-tree-sitter.cjs +4063 -0
- package/dist/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/package.json +27 -16
- package/dist/src/markdown-renderer.js +0 -112
|
@@ -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;
|
|
@@ -9,9 +9,21 @@ function isClipboardImagePasteKey(key) {
|
|
|
9
9
|
}
|
|
10
10
|
return key.ctrl && key.input === 'v' && !key.shift && !key.meta;
|
|
11
11
|
}
|
|
12
|
+
function composerIsEmpty(state) {
|
|
13
|
+
return (!state.input &&
|
|
14
|
+
state.cursor === 0 &&
|
|
15
|
+
state.pastedChunks.length === 0 &&
|
|
16
|
+
state.promptHistoryCursor === null);
|
|
17
|
+
}
|
|
18
|
+
function composerHasDiscardableDraft(state) {
|
|
19
|
+
if (!composerIsEmpty(state))
|
|
20
|
+
return true;
|
|
21
|
+
return !state.busy && state.imageAttachments.length > 0;
|
|
22
|
+
}
|
|
12
23
|
function shouldShowCommandPalette(state) {
|
|
13
24
|
if (state.busy ||
|
|
14
25
|
state.exiting ||
|
|
26
|
+
state.jobsPickerOpen ||
|
|
15
27
|
state.modelPickerOpen ||
|
|
16
28
|
state.resumePickerOpen) {
|
|
17
29
|
return false;
|
|
@@ -57,6 +69,7 @@ function pasteTextFromClipboard(store, handlers) {
|
|
|
57
69
|
const current = store.getState();
|
|
58
70
|
if (current.exiting ||
|
|
59
71
|
current.approvalPrompt ||
|
|
72
|
+
current.jobsPickerOpen ||
|
|
60
73
|
current.modelPickerOpen ||
|
|
61
74
|
current.resumePickerOpen ||
|
|
62
75
|
current.sudoPrompt) {
|
|
@@ -130,11 +143,41 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
130
143
|
return;
|
|
131
144
|
const key = event;
|
|
132
145
|
const state = store.getState();
|
|
146
|
+
const commandPaletteActive = shouldShowCommandPalette(state);
|
|
147
|
+
const commandSuggestions = commandPaletteActive
|
|
148
|
+
? getSlashCommandSuggestions(state.input)
|
|
149
|
+
: [];
|
|
150
|
+
const prepareForComposerInputChange = (current, nextInput) => {
|
|
151
|
+
if (shouldRemountLiveFrameForComposerInputChange(current, nextInput)) {
|
|
152
|
+
handlers.onLiveFrameShapeChange();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
133
155
|
if (key.ctrl && key.input === 'c') {
|
|
134
156
|
if (state.sudoPrompt) {
|
|
135
157
|
handlers.onSudoPasswordInput({ kind: 'cancel' });
|
|
136
158
|
return;
|
|
137
159
|
}
|
|
160
|
+
if (state.queuedMessage) {
|
|
161
|
+
handlers.onLiveFrameShapeChange();
|
|
162
|
+
store.update((current) => ({ ...current, queuedMessage: null }));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (composerHasDiscardableDraft(state)) {
|
|
166
|
+
store.update((current) => {
|
|
167
|
+
prepareForComposerInputChange(current, '');
|
|
168
|
+
return {
|
|
169
|
+
...current,
|
|
170
|
+
commandCursor: 0,
|
|
171
|
+
cursor: 0,
|
|
172
|
+
imageAttachments: current.busy ? current.imageAttachments : [],
|
|
173
|
+
input: '',
|
|
174
|
+
pastedChunks: [],
|
|
175
|
+
promptHistoryCursor: null,
|
|
176
|
+
promptHistoryDraft: '',
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
138
181
|
if (handlers.onCtrlC) {
|
|
139
182
|
handlers.onCtrlC();
|
|
140
183
|
return;
|
|
@@ -142,15 +185,6 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
142
185
|
handlers.onRequestExit();
|
|
143
186
|
return;
|
|
144
187
|
}
|
|
145
|
-
const commandPaletteActive = shouldShowCommandPalette(state);
|
|
146
|
-
const commandSuggestions = commandPaletteActive
|
|
147
|
-
? getSlashCommandSuggestions(state.input)
|
|
148
|
-
: [];
|
|
149
|
-
const prepareForComposerInputChange = (current, nextInput) => {
|
|
150
|
-
if (shouldRemountLiveFrameForComposerInputChange(current, nextInput)) {
|
|
151
|
-
handlers.onLiveFrameShapeChange();
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
188
|
if (state.sudoPrompt) {
|
|
155
189
|
if (key.escape) {
|
|
156
190
|
handlers.onSudoPasswordInput({ kind: 'cancel' });
|
|
@@ -210,6 +244,37 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
210
244
|
});
|
|
211
245
|
return;
|
|
212
246
|
}
|
|
247
|
+
if (state.jobsPickerOpen) {
|
|
248
|
+
if (key.escape) {
|
|
249
|
+
store.update((current) => ({
|
|
250
|
+
...current,
|
|
251
|
+
jobsPickerExpandedId: null,
|
|
252
|
+
jobsPickerOpen: false,
|
|
253
|
+
status: 'Ready',
|
|
254
|
+
}));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (key.upArrow || key.downArrow) {
|
|
258
|
+
store.update((current) => {
|
|
259
|
+
const count = (current.backgroundJobs ?? []).length;
|
|
260
|
+
const next = current.jobsPickerIndex + (key.upArrow ? -1 : 1);
|
|
261
|
+
return {
|
|
262
|
+
...current,
|
|
263
|
+
jobsPickerExpandedId: null,
|
|
264
|
+
jobsPickerIndex: Math.max(0, Math.min(next, Math.max(count - 1, 0))),
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (key.returnKey) {
|
|
270
|
+
void handlers.onJobsPickerOutput();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (key.input === 'k' && !key.ctrl && !key.meta && !key.shift) {
|
|
274
|
+
void handlers.onJobsPickerKill();
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
213
278
|
if (state.modelPickerOpen) {
|
|
214
279
|
if (key.escape) {
|
|
215
280
|
store.update((current) => ({ ...current, modelPickerOpen: false }));
|
|
@@ -275,8 +340,6 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
275
340
|
}
|
|
276
341
|
if (key.escape) {
|
|
277
342
|
if (state.busy) {
|
|
278
|
-
// With a queued message, Esc clears the queue only (turn keeps running).
|
|
279
|
-
// With nothing queued, Esc cancels the turn (Ctrl+C also cancels).
|
|
280
343
|
if (state.queuedMessage) {
|
|
281
344
|
handlers.onLiveFrameShapeChange();
|
|
282
345
|
store.update((current) => ({ ...current, queuedMessage: null }));
|
|
@@ -286,10 +349,7 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
286
349
|
return;
|
|
287
350
|
}
|
|
288
351
|
store.update((current) => {
|
|
289
|
-
if (
|
|
290
|
-
current.cursor === 0 &&
|
|
291
|
-
current.pastedChunks.length === 0 &&
|
|
292
|
-
current.promptHistoryCursor === null) {
|
|
352
|
+
if (composerIsEmpty(current)) {
|
|
293
353
|
return current;
|
|
294
354
|
}
|
|
295
355
|
prepareForComposerInputChange(current, '');
|
|
@@ -317,8 +377,6 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
317
377
|
return;
|
|
318
378
|
}
|
|
319
379
|
if (key.upArrow) {
|
|
320
|
-
// While busy with an empty composer, Up recalls and dequeues the queued
|
|
321
|
-
// message for editing; otherwise it walks prompt history as usual.
|
|
322
380
|
if (state.busy && state.input.trim() === '' && state.queuedMessage) {
|
|
323
381
|
handlers.onLiveFrameShapeChange();
|
|
324
382
|
store.update((current) => {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { isTuiMode } from '../../runtime-mode.js';
|
|
2
|
+
const BRAILLE_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴'];
|
|
3
|
+
const TITLE_BRAND = 'TheGitAI';
|
|
4
|
+
const TITLE_MARK_PREFIX = '❯_';
|
|
5
|
+
export const TERMINAL_TITLE_SPINNER_MS = 500;
|
|
6
|
+
export function resolveTerminalTitleState(input) {
|
|
7
|
+
if (input.awaitingReview)
|
|
8
|
+
return 'review';
|
|
9
|
+
if (input.busy)
|
|
10
|
+
return 'wip';
|
|
11
|
+
return 'clean';
|
|
12
|
+
}
|
|
13
|
+
export function formatTerminalTitle(state, spinnerFrame = 0) {
|
|
14
|
+
if (state === 'review')
|
|
15
|
+
return `${TITLE_MARK_PREFIX}▸ ${TITLE_BRAND}`;
|
|
16
|
+
if (state === 'wip') {
|
|
17
|
+
const frame = BRAILLE_SPINNER_FRAMES[spinnerFrame % BRAILLE_SPINNER_FRAMES.length];
|
|
18
|
+
return `${TITLE_MARK_PREFIX}${frame} ${TITLE_BRAND}`;
|
|
19
|
+
}
|
|
20
|
+
return `${TITLE_MARK_PREFIX}● ${TITLE_BRAND}`;
|
|
21
|
+
}
|
|
22
|
+
export function writeTerminalTitle(title, stream = process.stdout) {
|
|
23
|
+
if (isTuiMode())
|
|
24
|
+
return;
|
|
25
|
+
if (!('isTTY' in stream) || !stream.isTTY)
|
|
26
|
+
return;
|
|
27
|
+
stream.write(`\x1b]0;${title}\x07`);
|
|
28
|
+
}
|
|
29
|
+
export function createTerminalTitleController(options) {
|
|
30
|
+
const write = options?.write ?? writeTerminalTitle;
|
|
31
|
+
let currentState = 'clean';
|
|
32
|
+
let spinnerFrame = 0;
|
|
33
|
+
let lastTitle = '';
|
|
34
|
+
let timer = null;
|
|
35
|
+
const paint = () => {
|
|
36
|
+
const title = formatTerminalTitle(currentState, spinnerFrame);
|
|
37
|
+
if (title === lastTitle)
|
|
38
|
+
return;
|
|
39
|
+
lastTitle = title;
|
|
40
|
+
write(title);
|
|
41
|
+
};
|
|
42
|
+
const stopTimer = () => {
|
|
43
|
+
if (!timer)
|
|
44
|
+
return;
|
|
45
|
+
clearInterval(timer);
|
|
46
|
+
timer = null;
|
|
47
|
+
};
|
|
48
|
+
const startTimer = () => {
|
|
49
|
+
if (timer)
|
|
50
|
+
return;
|
|
51
|
+
timer = setInterval(() => {
|
|
52
|
+
spinnerFrame = (spinnerFrame + 1) % BRAILLE_SPINNER_FRAMES.length;
|
|
53
|
+
paint();
|
|
54
|
+
}, TERMINAL_TITLE_SPINNER_MS);
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
sync(input) {
|
|
58
|
+
const next = resolveTerminalTitleState(input);
|
|
59
|
+
if (next !== currentState) {
|
|
60
|
+
currentState = next;
|
|
61
|
+
if (next === 'wip') {
|
|
62
|
+
spinnerFrame = 0;
|
|
63
|
+
startTimer();
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
stopTimer();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (next === 'wip') {
|
|
70
|
+
startTimer();
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
stopTimer();
|
|
74
|
+
}
|
|
75
|
+
paint();
|
|
76
|
+
},
|
|
77
|
+
dispose() {
|
|
78
|
+
stopTimer();
|
|
79
|
+
currentState = 'clean';
|
|
80
|
+
spinnerFrame = 0;
|
|
81
|
+
paint();
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -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
|
+
}
|