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