@thegitai/cli 1.0.0-preview.5 → 1.0.0-preview.51
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 +93 -4
- package/dist/bin/ai.js +142 -112
- package/dist/bin/browser-host.js +265 -0
- package/dist/src/agent-mode.js +32 -7
- package/dist/src/api/auth.js +10 -4
- package/dist/src/api/browser-login.js +149 -37
- package/dist/src/api/chat.js +680 -166
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/api/default-host.js +1 -0
- package/dist/src/api/http.js +151 -7
- package/dist/src/api/models.js +29 -10
- package/dist/src/api/startup-handshake.js +36 -0
- package/dist/src/api/turn-recovery.js +189 -0
- package/dist/src/background-jobs.js +4 -2
- package/dist/src/browser/bridge.js +262 -0
- package/dist/src/browser/framing.js +42 -0
- package/dist/src/browser/native-host.js +219 -0
- package/dist/src/browser/protocol.js +46 -0
- package/dist/src/browser/session-bridge.js +130 -0
- package/dist/src/cli-args.js +19 -5
- package/dist/src/client-environment.js +2 -0
- package/dist/src/core/clipboard.js +7 -13
- package/dist/src/core/image-limits.js +56 -0
- package/dist/src/core/image-path-extractor.js +70 -3
- package/dist/src/core/session-image-store.js +246 -0
- package/dist/src/edit-journal.js +33 -2
- package/dist/src/executor.js +25 -3
- package/dist/src/help-text.js +112 -21
- package/dist/src/patcher.js +43 -4
- package/dist/src/permissions.js +288 -0
- package/dist/src/preferences.js +39 -0
- package/dist/src/quota-notice.js +35 -0
- package/dist/src/session-safety.js +0 -12
- package/dist/src/session-store.js +121 -20
- package/dist/src/session.js +19 -4
- package/dist/src/signin.js +58 -0
- package/dist/src/tool-executor.js +13 -46
- package/dist/src/tools/browser.js +641 -0
- package/dist/src/tools/delete-file.js +20 -4
- package/dist/src/tools/index.js +32 -10
- package/dist/src/tools/patch-file.js +12 -26
- package/dist/src/tools/read-file.js +5 -0
- package/dist/src/tools/read-image-file.js +85 -0
- package/dist/src/tools/replace-document-text.js +28 -18
- package/dist/src/tools/restore-checkpoint.js +0 -1
- package/dist/src/tools/run-command.js +14 -71
- package/dist/src/tools/run-node-script.js +12 -81
- package/dist/src/tools/save-generated-image.js +120 -0
- package/dist/src/tools/str-replace.js +12 -26
- package/dist/src/tools/undo-edit.js +1 -6
- package/dist/src/tools/write-file.js +67 -11
- package/dist/src/ui/prompt-history-store.js +1 -1
- package/dist/src/ui/repl.js +949 -279
- package/dist/src/ui/slash-commands.js +121 -0
- package/dist/src/ui/tui/attention.js +25 -0
- package/dist/src/ui/tui/bridge.js +38 -5
- package/dist/src/ui/tui/build-frame.js +686 -223
- package/dist/src/ui/tui/composer-layout.js +76 -0
- package/dist/src/ui/tui/desktop-notification.js +118 -0
- package/dist/src/ui/tui/markdown-render.js +11 -2
- package/dist/src/ui/tui/shell-input.js +256 -73
- package/dist/src/ui/tui/shortcuts.js +61 -0
- package/dist/src/ui/tui/terminal-theme.js +28 -0
- package/dist/src/ui/tui/user-input.js +593 -0
- package/dist/src/update-check.js +205 -0
- package/dist/src/usage.js +5 -2
- package/dist/src/utils.js +13 -0
- package/dist/src/version.js +52 -2
- package/package.json +22 -9
- package/dist/src/project-index.js +0 -233
- package/dist/src/tools/code-intel.js +0 -472
- package/dist/src/tools/find-symbol.js +0 -70
- package/dist/src/tools/hover-symbol.js +0 -95
- package/dist/src/tools/list-symbols.js +0 -55
- package/dist/src/tools/search-code.js +0 -37
- package/dist/src/tools/signature-help.js +0 -118
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { truncate } from '../../utils.js';
|
|
1
|
+
import { agentModeColor, agentModeLabelTiers, agentModeShortLabel, } from '../../agent-mode.js';
|
|
2
|
+
import { singleLinePreview, truncate } from '../../utils.js';
|
|
3
3
|
import { formatClientTokenUsage } from '../repl.js';
|
|
4
|
+
import { getHiddenSlashCommandCount, getSlashCommandSuggestions, } from '../slash-commands.js';
|
|
4
5
|
import { renderFormattedBodyLines, renderPreformattedBodyLines, } from './markdown-render.js';
|
|
5
|
-
import { displayWidth, line, plainLine, sliceToWidth, span, wrapText, } from './text.js';
|
|
6
|
+
import { displayWidth, line, padToWidth, plainLine, sliceToWidth, span, wrapText, } from './text.js';
|
|
7
|
+
import { isDarkTerminalBackground, mutedColor, mutedStyle } from './terminal-theme.js';
|
|
8
|
+
import { buildShortcutGroups, shortcutsPanelAvailable, } from './shortcuts.js';
|
|
9
|
+
import { buildUserInputOverlayLines, } from './user-input.js';
|
|
10
|
+
import { composerPromptLabel, frameContentWidth, splitComposerInput, } from './composer-layout.js';
|
|
6
11
|
const WORKING_CLOCK_ICON = '◷';
|
|
7
12
|
const BRAILLE_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴'];
|
|
8
13
|
const TODO_PANEL_MAX_ROWS = 12;
|
|
@@ -10,12 +15,41 @@ const TODO_IN_PROGRESS_COLOR = 'ansi256(214)';
|
|
|
10
15
|
const COMMAND_PREVIEW_LINES = 10;
|
|
11
16
|
const WORKING_TOOL_PREVIEW_ROWS = 3;
|
|
12
17
|
const TRANSCRIPT_DIFF_PREVIEW_LINES = 24;
|
|
18
|
+
const APPROVAL_PREVIEW_MAX_ROWS = 20;
|
|
19
|
+
const APPROVAL_PREVIEW_MIN_ROWS = 4;
|
|
20
|
+
const APPROVAL_OVERLAY_CHROME_ROWS = 11;
|
|
21
|
+
const APPROVAL_PADDING_CHROME_ROWS = 4;
|
|
22
|
+
const APPROVAL_PADDING_MIN_ROWS = 30;
|
|
23
|
+
const APPROVAL_SCROLLBAR_COLUMNS = 2;
|
|
24
|
+
const APPROVAL_ACCENT_COLOR = 'cyan';
|
|
25
|
+
const SUDO_PASSWORD_ASSURANCE = 'The model never sees this. Your password goes straight to sudo on this ' +
|
|
26
|
+
'computer, then is discarded — never sent to our servers, saved, or logged.';
|
|
27
|
+
const SUDO_ASSURANCE_COLOR = 'ansi256(248)';
|
|
28
|
+
const APPROVAL_SCROLL_STATUS_COLOR = 'ansi256(248)';
|
|
29
|
+
const APPROVAL_SCROLLBAR_THUMB = '█';
|
|
30
|
+
const APPROVAL_SCROLLBAR_TRACK = '░';
|
|
13
31
|
const THINKING_NOTE_PREVIEW_ROWS = 3;
|
|
14
32
|
const COMPOSER_INPUT_MAX_ROWS = 6;
|
|
15
|
-
const
|
|
33
|
+
const FOOTER_HINT_MIN_GAP = 2;
|
|
34
|
+
const FOOTER_HELP_HINT = '? for shortcuts';
|
|
35
|
+
const FOOTER_CANCEL_HINT = 'esc cancels turn';
|
|
36
|
+
const FOOTER_NOTIFICATIONS_ON = 'Notifications ON';
|
|
37
|
+
const FOOTER_NOTIFICATIONS_OFF = 'Notifications OFF';
|
|
38
|
+
export function footerNotificationHint(status) {
|
|
39
|
+
const text = String(status ?? '').trim();
|
|
40
|
+
if (text === 'Notifications on')
|
|
41
|
+
return FOOTER_NOTIFICATIONS_ON;
|
|
42
|
+
if (text === 'Notifications off')
|
|
43
|
+
return FOOTER_NOTIFICATIONS_OFF;
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
const FOOTER_CYCLE_HINT = ' (shift+tab to cycle)';
|
|
16
47
|
const OVERLAY_PANEL_MAX_WIDTH = 86;
|
|
48
|
+
const USER_INPUT_PANEL_MAX_WIDTH = 120;
|
|
17
49
|
const OVERLAY_PANEL_MARGIN_LINES = 2;
|
|
50
|
+
const OVERLAY_PANEL_MARGIN_MIN_ROWS = 30;
|
|
18
51
|
const OVERLAY_BORDER_COLOR = 'yellow';
|
|
52
|
+
const USER_INPUT_BORDER_COLOR = 'cyan';
|
|
19
53
|
const OVERLAY_WARNING_COLOR = 'ansi256(208)';
|
|
20
54
|
const MODEL_PICKER_PANEL_MAX_WIDTH = 144;
|
|
21
55
|
const MODEL_PICKER_PANEL_MARGIN_LINES = 2;
|
|
@@ -23,75 +57,74 @@ const MODEL_PICKER_BORDER_COLOR = 'cyan';
|
|
|
23
57
|
const MODEL_PICKER_ACCENT_COLOR = 'cyan';
|
|
24
58
|
const MODEL_PICKER_HIGHLIGHT_BG = 'ansi256(87)';
|
|
25
59
|
const MODEL_PICKER_META_INDENT = ' ';
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const captureCursor = () => {
|
|
34
|
-
if (capturedCursor)
|
|
35
|
-
return;
|
|
36
|
-
const current = rows[rows.length - 1] ?? '';
|
|
37
|
-
if (current.length >= safeWidth) {
|
|
38
|
-
rows.push('');
|
|
39
|
-
cursorRow = rows.length - 1;
|
|
40
|
-
cursorCol = 0;
|
|
60
|
+
const COMPOSER_MARKER_PATTERN = /\[Image #\d+\]|\[Pasted Text: [^\]\n]*\]/g;
|
|
61
|
+
function composerMarkerMask(text) {
|
|
62
|
+
const mask = new Array(text.length).fill(false);
|
|
63
|
+
for (const match of text.matchAll(COMPOSER_MARKER_PATTERN)) {
|
|
64
|
+
const start = match.index ?? 0;
|
|
65
|
+
for (let offset = start; offset < start + match[0].length; offset += 1) {
|
|
66
|
+
mask[offset] = true;
|
|
41
67
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
68
|
+
}
|
|
69
|
+
return mask;
|
|
70
|
+
}
|
|
71
|
+
function composerRowSpans(text, mask, cursorCol) {
|
|
72
|
+
const spans = [];
|
|
73
|
+
const push = (value, marker, cursor) => {
|
|
74
|
+
if (!value)
|
|
75
|
+
return;
|
|
76
|
+
spans.push(span(value, {
|
|
77
|
+
...(marker ? { color: 'cyan' } : {}),
|
|
78
|
+
...(cursor ? { inverse: true } : {}),
|
|
79
|
+
}));
|
|
47
80
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
81
|
+
let cut = 0;
|
|
82
|
+
const flushTo = (end) => {
|
|
83
|
+
while (cut < end) {
|
|
84
|
+
const marker = mask[cut] ?? false;
|
|
85
|
+
let runEnd = cut + 1;
|
|
86
|
+
while (runEnd < end && (mask[runEnd] ?? false) === marker)
|
|
87
|
+
runEnd += 1;
|
|
88
|
+
push(text.slice(cut, runEnd), marker, false);
|
|
89
|
+
cut = runEnd;
|
|
54
90
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
continue;
|
|
60
|
-
if (char === '\n') {
|
|
61
|
-
rows.push('');
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
rows[rows.length - 1] = `${rows[rows.length - 1] ?? ''}${char}`;
|
|
65
|
-
}
|
|
66
|
-
if (!capturedCursor) {
|
|
67
|
-
captureCursor();
|
|
91
|
+
};
|
|
92
|
+
if (cursorCol === null) {
|
|
93
|
+
flushTo(text.length);
|
|
94
|
+
return spans;
|
|
68
95
|
}
|
|
69
|
-
|
|
96
|
+
flushTo(Math.min(cursorCol, text.length));
|
|
97
|
+
push(text[cursorCol] ?? ' ', mask[cursorCol] ?? false, true);
|
|
98
|
+
cut = Math.min(cursorCol + 1, text.length);
|
|
99
|
+
flushTo(text.length);
|
|
100
|
+
return spans;
|
|
70
101
|
}
|
|
71
102
|
function buildComposerInputLines(input, cursor, promptLabel, placeholder, width) {
|
|
72
103
|
if (!input) {
|
|
73
104
|
return [
|
|
74
|
-
line(span(promptLabel, { color: 'cyan' }), span(' ', { inverse: true }), span(placeholder, { color:
|
|
105
|
+
line(span(promptLabel, { color: 'cyan' }), span(' ', { inverse: true }), span(placeholder, { color: mutedColor() })),
|
|
75
106
|
];
|
|
76
107
|
}
|
|
77
108
|
const labelWidth = promptLabel.length;
|
|
78
109
|
const inputWidth = Math.max(1, width - labelWidth);
|
|
79
110
|
const { cursorCol, cursorRow, rows } = splitComposerInput(input, cursor, inputWidth);
|
|
80
111
|
const firstRow = Math.min(Math.max(cursorRow - COMPOSER_INPUT_MAX_ROWS + 1, 0), Math.max(rows.length - COMPOSER_INPUT_MAX_ROWS, 0));
|
|
112
|
+
const rowOffsets = [];
|
|
113
|
+
let consumed = 0;
|
|
114
|
+
for (const row of rows) {
|
|
115
|
+
rowOffsets.push(consumed);
|
|
116
|
+
consumed += row.length;
|
|
117
|
+
}
|
|
118
|
+
const mask = composerMarkerMask(rows.join(''));
|
|
81
119
|
return rows
|
|
82
120
|
.slice(firstRow, firstRow + COMPOSER_INPUT_MAX_ROWS)
|
|
83
121
|
.map((text, index) => {
|
|
84
122
|
const absoluteRow = firstRow + index;
|
|
123
|
+
const rowStart = rowOffsets[absoluteRow] ?? 0;
|
|
85
124
|
const label = index === 0
|
|
86
125
|
? span(promptLabel, { color: 'cyan' })
|
|
87
126
|
: span(' '.repeat(labelWidth));
|
|
88
|
-
|
|
89
|
-
return line(label, span(text));
|
|
90
|
-
}
|
|
91
|
-
const before = text.slice(0, cursorCol);
|
|
92
|
-
const cursorChar = text[cursorCol] ?? ' ';
|
|
93
|
-
const after = text.slice(cursorCol + 1);
|
|
94
|
-
return line(label, span(before), span(cursorChar, { inverse: true }), span(after));
|
|
127
|
+
return line(label, ...composerRowSpans(text, mask.slice(rowStart, rowStart + text.length), absoluteRow === cursorRow ? cursorCol : null));
|
|
95
128
|
});
|
|
96
129
|
}
|
|
97
130
|
function getEntryColor(kind) {
|
|
@@ -157,15 +190,6 @@ export function formatPromptDirectoryLabel(projectRoot, homeDir = process.env.HO
|
|
|
157
190
|
export function formatPromptSessionIdLabel(showSessionId, sessionId) {
|
|
158
191
|
return showSessionId ? String(sessionId ?? '').trim() : '';
|
|
159
192
|
}
|
|
160
|
-
const CLIENT_SLASH_COMMANDS = [
|
|
161
|
-
{ command: '/help', description: 'Show available chat commands' },
|
|
162
|
-
{ command: '/usage', description: 'Show account usage percentage and reset times' },
|
|
163
|
-
{ command: '/model', description: 'Switch the active model' },
|
|
164
|
-
{ command: '/resume', description: 'Open the session picker to resume a previous session' },
|
|
165
|
-
{ command: '/jobs', description: 'Background jobs: pick to view output or kill' },
|
|
166
|
-
{ command: '/clear', description: 'Clear conversation history' },
|
|
167
|
-
{ command: '/exit', description: 'Quit the current session' },
|
|
168
|
-
];
|
|
169
193
|
function buildModelPickerOptions(currentModelId, serverModels) {
|
|
170
194
|
return serverModels.map((model) => ({
|
|
171
195
|
id: model.id,
|
|
@@ -177,41 +201,13 @@ function buildModelPickerOptions(currentModelId, serverModels) {
|
|
|
177
201
|
note: model.description,
|
|
178
202
|
}));
|
|
179
203
|
}
|
|
180
|
-
|
|
181
|
-
const trimmed = String(input ?? '').trimStart();
|
|
182
|
-
const match = trimmed.match(/^\/[^\s]*/);
|
|
183
|
-
const token = match?.[0] ?? '';
|
|
184
|
-
if (token.indexOf('/', 1) !== -1)
|
|
185
|
-
return '';
|
|
186
|
-
return token;
|
|
187
|
-
}
|
|
188
|
-
function scoreSlashCommand(option, token) {
|
|
189
|
-
if (option.command === token)
|
|
190
|
-
return 0;
|
|
191
|
-
if (option.command.startsWith(token))
|
|
192
|
-
return 1;
|
|
193
|
-
if (option.command.includes(token.slice(1)))
|
|
194
|
-
return 2;
|
|
195
|
-
if (option.description.toLowerCase().includes(token.slice(1)))
|
|
196
|
-
return 3;
|
|
197
|
-
return 4;
|
|
198
|
-
}
|
|
199
|
-
export function getSlashCommandSuggestions(input) {
|
|
200
|
-
const token = getInputCommandToken(input).toLowerCase();
|
|
201
|
-
if (!token)
|
|
202
|
-
return [];
|
|
203
|
-
const ranked = [...CLIENT_SLASH_COMMANDS].sort((a, b) => {
|
|
204
|
-
const scoreDiff = scoreSlashCommand(a, token) - scoreSlashCommand(b, token);
|
|
205
|
-
if (scoreDiff !== 0)
|
|
206
|
-
return scoreDiff;
|
|
207
|
-
return a.command.localeCompare(b.command);
|
|
208
|
-
});
|
|
209
|
-
const matches = ranked.filter((option) => scoreSlashCommand(option, token) < 4);
|
|
210
|
-
return (matches.length ? matches : ranked).slice(0, 5);
|
|
211
|
-
}
|
|
204
|
+
export { getSlashCommandSuggestions };
|
|
212
205
|
function formatModelLabel(modelId, serverModels) {
|
|
213
206
|
return serverModels.find((model) => model.id === modelId)?.label ?? 'Unknown model';
|
|
214
207
|
}
|
|
208
|
+
function pickerModelLabel(modelId, serverModels) {
|
|
209
|
+
return formatModelLabel(modelId, serverModels).replace(/\s*\([^)]*\)\s*$/, '');
|
|
210
|
+
}
|
|
215
211
|
function filterResumeSessions(sessions, filter, serverModels) {
|
|
216
212
|
const q = filter.trim().toLowerCase();
|
|
217
213
|
if (!q)
|
|
@@ -235,26 +231,90 @@ function formatRelativeTime(isoDate) {
|
|
|
235
231
|
return `${hours}h ago`;
|
|
236
232
|
return `${Math.floor(hours / 24)}d ago`;
|
|
237
233
|
}
|
|
238
|
-
|
|
234
|
+
const TODO_IN_PROGRESS_FRAMES = [
|
|
235
|
+
'\u25CB',
|
|
236
|
+
'\u25D4',
|
|
237
|
+
'\u25D1',
|
|
238
|
+
'\u25D5',
|
|
239
|
+
'\u25CF',
|
|
240
|
+
];
|
|
241
|
+
const TODO_IN_PROGRESS_STATIC_GLYPH = '\u25D1';
|
|
242
|
+
export const TODO_IN_PROGRESS_STEP_MS = 1_500;
|
|
243
|
+
export function todoProgressTick(nowMs) {
|
|
244
|
+
return Math.floor(nowMs / TODO_IN_PROGRESS_STEP_MS);
|
|
245
|
+
}
|
|
246
|
+
function todoInProgressGlyph(progressTick) {
|
|
247
|
+
if (progressTick === null)
|
|
248
|
+
return TODO_IN_PROGRESS_STATIC_GLYPH;
|
|
249
|
+
const index = ((progressTick % TODO_IN_PROGRESS_FRAMES.length) +
|
|
250
|
+
TODO_IN_PROGRESS_FRAMES.length) %
|
|
251
|
+
TODO_IN_PROGRESS_FRAMES.length;
|
|
252
|
+
return TODO_IN_PROGRESS_FRAMES[index];
|
|
253
|
+
}
|
|
254
|
+
function todoItemLine(item, width, progressTick = null) {
|
|
239
255
|
const text = fitLine(item.text, Math.max(8, width - 5));
|
|
240
256
|
if (item.status === 'completed') {
|
|
241
|
-
return line(span(' ✔ ', { color: 'green' }), span(text,
|
|
257
|
+
return line(span(' ✔ ', { color: 'green' }), span(text, mutedStyle()));
|
|
242
258
|
}
|
|
243
259
|
if (item.status === 'in_progress') {
|
|
244
|
-
return line(span(
|
|
260
|
+
return line(span(` ${todoInProgressGlyph(progressTick)} `, {
|
|
261
|
+
color: TODO_IN_PROGRESS_COLOR,
|
|
262
|
+
}), span(text, { color: TODO_IN_PROGRESS_COLOR, bold: true }));
|
|
263
|
+
}
|
|
264
|
+
return line(span(' ○ ', { color: mutedColor() }), isDarkTerminalBackground()
|
|
265
|
+
? span(text, { color: mutedColor() })
|
|
266
|
+
: span(text));
|
|
267
|
+
}
|
|
268
|
+
const TURN_MESSAGE_PANEL_MAX_ROWS = 6;
|
|
269
|
+
const TURN_MESSAGE_LABEL = {
|
|
270
|
+
queued: 'queued',
|
|
271
|
+
sending: 'Processing . . .',
|
|
272
|
+
delivered: 'delivered',
|
|
273
|
+
};
|
|
274
|
+
function turnMessageLine(message, width, progressTick) {
|
|
275
|
+
const DECORATION = 6;
|
|
276
|
+
const budget = Math.max(1, width - DECORATION);
|
|
277
|
+
const preferredLabel = Math.max(...Object.values(TURN_MESSAGE_LABEL).map((value) => displayWidth(value)));
|
|
278
|
+
const labelWidth = Math.min(Math.max(1, Math.floor(budget / 2)), Math.max(preferredLabel, displayWidth(message.note ?? '')));
|
|
279
|
+
const label = fitLine(message.note ?? TURN_MESSAGE_LABEL[message.state], labelWidth);
|
|
280
|
+
const textWidth = Math.max(1, budget - labelWidth);
|
|
281
|
+
const text = padToWidth(fitLine(`"${message.text}"`, textWidth), textWidth);
|
|
282
|
+
if (message.state === 'delivered') {
|
|
283
|
+
return line(span(' ● ', { color: 'green' }), span(text, { color: 'gray', dim: true }), span(` ${label}`, { color: 'green' }));
|
|
284
|
+
}
|
|
285
|
+
if (message.state === 'sending') {
|
|
286
|
+
return line(span(` ${todoInProgressGlyph(progressTick)} `, {
|
|
287
|
+
color: TODO_IN_PROGRESS_COLOR,
|
|
288
|
+
}), span(text, { color: TODO_IN_PROGRESS_COLOR, bold: true }), span(` ${label}`, { color: TODO_IN_PROGRESS_COLOR }));
|
|
289
|
+
}
|
|
290
|
+
return line(span(' ○ ', { color: 'cyan' }), span(text, { color: 'cyan' }), span(` ${label}`, { color: 'gray' }));
|
|
291
|
+
}
|
|
292
|
+
export function buildTurnMessageLines(messages, width, progressTick = null) {
|
|
293
|
+
if (messages.length === 0)
|
|
294
|
+
return [];
|
|
295
|
+
const lines = [
|
|
296
|
+
line(span('Your messages', { color: 'cyan', bold: true })),
|
|
297
|
+
];
|
|
298
|
+
const visible = messages.slice(-(TURN_MESSAGE_PANEL_MAX_ROWS - 1));
|
|
299
|
+
const hidden = messages.length - visible.length;
|
|
300
|
+
if (hidden > 0) {
|
|
301
|
+
lines.push(line(span(` ● ${hidden} earlier`, { color: 'gray', dim: true })));
|
|
245
302
|
}
|
|
246
|
-
|
|
303
|
+
for (const message of visible) {
|
|
304
|
+
lines.push(turnMessageLine(message, width, progressTick));
|
|
305
|
+
}
|
|
306
|
+
return lines;
|
|
247
307
|
}
|
|
248
308
|
export function formatTodoProgress(items) {
|
|
249
309
|
const done = items.filter((item) => item.status === 'completed').length;
|
|
250
310
|
return `${done}/${items.length} done`;
|
|
251
311
|
}
|
|
252
|
-
export function renderTodoListLines(items, width, { header = false } = {}) {
|
|
312
|
+
export function renderTodoListLines(items, width, { header = false, progressTick = null } = {}) {
|
|
253
313
|
if (items.length === 0)
|
|
254
314
|
return [];
|
|
255
315
|
const lines = [];
|
|
256
316
|
if (header) {
|
|
257
|
-
lines.push(line(span('To-dos', { color: 'cyan', bold: true }), span(` · ${formatTodoProgress(items)}`, { color:
|
|
317
|
+
lines.push(line(span('To-dos', { color: 'cyan', bold: true }), span(` · ${formatTodoProgress(items)}`, { color: mutedColor() })));
|
|
258
318
|
}
|
|
259
319
|
const itemRowBudget = TODO_PANEL_MAX_ROWS - (header ? 1 : 0);
|
|
260
320
|
let collapsedDone = 0;
|
|
@@ -268,19 +328,16 @@ export function renderTodoListLines(items, width, { header = false } = {}) {
|
|
|
268
328
|
collapsedDone = Math.min(over, leadingDone);
|
|
269
329
|
}
|
|
270
330
|
if (collapsedDone > 0) {
|
|
271
|
-
lines.push(line(span(' ✔ ', { color: 'green' }), span(`${collapsedDone} completed`,
|
|
331
|
+
lines.push(line(span(' ✔ ', { color: 'green' }), span(`${collapsedDone} completed`, mutedStyle())));
|
|
272
332
|
}
|
|
273
333
|
const remaining = items.slice(collapsedDone);
|
|
274
334
|
const budget = itemRowBudget - (collapsedDone > 0 ? 1 : 0);
|
|
275
335
|
const visible = remaining.length > budget ? remaining.slice(0, budget - 1) : remaining;
|
|
276
336
|
for (const item of visible) {
|
|
277
|
-
lines.push(todoItemLine(item, width));
|
|
337
|
+
lines.push(todoItemLine(item, width, progressTick));
|
|
278
338
|
}
|
|
279
339
|
if (remaining.length > visible.length) {
|
|
280
|
-
lines.push(plainLine(` … +${remaining.length - visible.length} more`,
|
|
281
|
-
color: 'gray',
|
|
282
|
-
dim: true,
|
|
283
|
-
}));
|
|
340
|
+
lines.push(plainLine(` … +${remaining.length - visible.length} more`, mutedStyle()));
|
|
284
341
|
}
|
|
285
342
|
return lines;
|
|
286
343
|
}
|
|
@@ -293,20 +350,45 @@ export function renderTranscriptEntryLines(entry, width) {
|
|
|
293
350
|
? renderPreformattedBodyLines(entry.body, width, entry.kind)
|
|
294
351
|
: renderFormattedBodyLines(entry.body, width, entry.kind)));
|
|
295
352
|
if (entry.diffPreview) {
|
|
296
|
-
lines.push(
|
|
297
|
-
for (const diffLine of entry.diffPreview.lines.slice(0, TRANSCRIPT_DIFF_PREVIEW_LINES)) {
|
|
298
|
-
lines.push(line(span(`${diffLinePrefix(diffLine.kind)} `, { color: diffLineColor(diffLine.kind) }), span(fitLine(diffLine.content || ' ', width - 4), {
|
|
299
|
-
color: diffLineColor(diffLine.kind),
|
|
300
|
-
})));
|
|
301
|
-
}
|
|
353
|
+
lines.push(...renderDiffPreviewLines(entry.diffPreview, width));
|
|
302
354
|
}
|
|
303
355
|
if (entry.todoList && entry.todoList.length > 0) {
|
|
304
356
|
lines.push(...renderTodoListLines(entry.todoList, width));
|
|
305
357
|
}
|
|
306
358
|
return lines;
|
|
307
359
|
}
|
|
360
|
+
const transcriptEntryLineCache = new WeakMap();
|
|
361
|
+
const MAX_CACHED_TRANSCRIPT_WIDTHS = 2;
|
|
362
|
+
function cachedTranscriptEntryLines(entry, width) {
|
|
363
|
+
let byWidth = transcriptEntryLineCache.get(entry);
|
|
364
|
+
if (!byWidth) {
|
|
365
|
+
byWidth = new Map();
|
|
366
|
+
transcriptEntryLineCache.set(entry, byWidth);
|
|
367
|
+
}
|
|
368
|
+
const cached = byWidth.get(width);
|
|
369
|
+
if (cached)
|
|
370
|
+
return cached;
|
|
371
|
+
const lines = renderTranscriptEntryLines(entry, width);
|
|
372
|
+
if (byWidth.size >= MAX_CACHED_TRANSCRIPT_WIDTHS) {
|
|
373
|
+
const oldest = byWidth.keys().next().value;
|
|
374
|
+
if (oldest !== undefined)
|
|
375
|
+
byWidth.delete(oldest);
|
|
376
|
+
}
|
|
377
|
+
byWidth.set(width, lines);
|
|
378
|
+
return lines;
|
|
379
|
+
}
|
|
380
|
+
function renderDiffPreviewLines(preview, width, maxDiffLines = TRANSCRIPT_DIFF_PREVIEW_LINES) {
|
|
381
|
+
return [
|
|
382
|
+
plainLine(` Added ${preview.added} line${preview.added === 1 ? '' : 's'}, removed ${preview.removed} line${preview.removed === 1 ? '' : 's'}`, { color: 'gray' }),
|
|
383
|
+
...preview.lines.slice(0, maxDiffLines).map((diffLine) => line(span(`${diffLinePrefix(diffLine.kind)} `, {
|
|
384
|
+
color: diffLineColor(diffLine.kind),
|
|
385
|
+
}), span(fitLine(diffLine.content || ' ', width - 4), {
|
|
386
|
+
color: diffLineColor(diffLine.kind),
|
|
387
|
+
}))),
|
|
388
|
+
];
|
|
389
|
+
}
|
|
308
390
|
function tokenUsageLines(usage) {
|
|
309
|
-
const match = usage.match(/^Session tokens • in ([^•]+) • out ([^•]+)(?: • think ([^•]+))? • cache ([^•]+)(?: • write ([^•]+))
|
|
391
|
+
const match = usage.match(/^Session tokens • in ([^•]+) • out ([^•]+)(?: • think ([^•]+))? • cache ([^•]+)(?: • write ([^•]+))?$/);
|
|
310
392
|
if (!match) {
|
|
311
393
|
return [plainLine(usage, { color: 'cyan' })];
|
|
312
394
|
}
|
|
@@ -324,15 +406,16 @@ function tokenUsageLines(usage) {
|
|
|
324
406
|
if (match[5]) {
|
|
325
407
|
spans.push(span(' Write ', { color: 'gray' }), span(match[5].trim(), { color: 'yellow' }));
|
|
326
408
|
}
|
|
327
|
-
if (match[6]) {
|
|
328
|
-
spans.push(span(' Index ', { color: 'gray' }), span(match[6].trim(), { color: 'blue' }));
|
|
329
|
-
}
|
|
330
409
|
return [line(...spans)];
|
|
331
410
|
}
|
|
411
|
+
export const AGENT_MODE_LOCKED_STATUS = 'Mode locked while the agent is working';
|
|
332
412
|
function footerTransientStatus(status) {
|
|
333
413
|
const text = String(status ?? '').trim();
|
|
334
414
|
if (!text)
|
|
335
415
|
return null;
|
|
416
|
+
if (text === AGENT_MODE_LOCKED_STATUS) {
|
|
417
|
+
return { color: 'yellow', text, icon: '!' };
|
|
418
|
+
}
|
|
336
419
|
if (text === 'Copied selection' || text === 'Copied link') {
|
|
337
420
|
return { color: 'green', text, icon: '✓' };
|
|
338
421
|
}
|
|
@@ -349,10 +432,11 @@ function backgroundJobIndicatorSpans(state) {
|
|
|
349
432
|
if (runningCount === 0)
|
|
350
433
|
return [];
|
|
351
434
|
const noun = runningCount === 1 ? 'shell' : 'shells';
|
|
435
|
+
const hint = state.busy ? ' · ctrl+b' : ' · /jobs';
|
|
352
436
|
return [
|
|
353
437
|
span(' ', { color: 'gray', dim: true }),
|
|
354
438
|
span(`● ${runningCount} ${noun} running`, { color: 'green', bold: true }),
|
|
355
|
-
span(
|
|
439
|
+
span(hint, { color: 'gray', dim: true }),
|
|
356
440
|
];
|
|
357
441
|
}
|
|
358
442
|
function todoIndicatorSpans(state) {
|
|
@@ -372,9 +456,47 @@ function todoIndicatorSpans(state) {
|
|
|
372
456
|
}),
|
|
373
457
|
];
|
|
374
458
|
}
|
|
375
|
-
function
|
|
459
|
+
function composerFooterRightHints(state) {
|
|
460
|
+
const cancelHint = state.busy && !state.queuedMessage && !state.userInputPrompt
|
|
461
|
+
? FOOTER_CANCEL_HINT
|
|
462
|
+
: '';
|
|
463
|
+
const helpHint = shortcutsPanelAvailable(state) ? FOOTER_HELP_HINT : '';
|
|
464
|
+
const notifyHint = footerNotificationHint(state.transientStatus);
|
|
465
|
+
const join = (...parts) => parts.filter(Boolean).join(' · ');
|
|
466
|
+
const tiers = [];
|
|
467
|
+
if (notifyHint) {
|
|
468
|
+
tiers.push(join(cancelHint, notifyHint, helpHint));
|
|
469
|
+
tiers.push(join(notifyHint, helpHint));
|
|
470
|
+
tiers.push(join(cancelHint, notifyHint));
|
|
471
|
+
tiers.push(notifyHint);
|
|
472
|
+
}
|
|
473
|
+
if (cancelHint && helpHint)
|
|
474
|
+
tiers.push(`${cancelHint} · ${helpHint}`);
|
|
475
|
+
if (cancelHint)
|
|
476
|
+
tiers.push(cancelHint);
|
|
477
|
+
if (helpHint)
|
|
478
|
+
tiers.push(helpHint);
|
|
479
|
+
tiers.push('');
|
|
480
|
+
return tiers.filter((tier, index) => tiers.indexOf(tier) === index);
|
|
481
|
+
}
|
|
482
|
+
export function fitComposerFooterRow({ modeLabels, modeLabelFallback, cycleHint, rightHints, leadWidth, tailWidth, contentWidth, }) {
|
|
483
|
+
const candidates = rightHints.flatMap((rightHint) => (cycleHint ? [cycleHint, ''] : ['']).flatMap((cycle) => modeLabels.map((modeLabel) => ({ modeLabel, cycleHint: cycle, rightHint }))));
|
|
484
|
+
const hasTail = (rightHint) => Boolean(rightHint) || tailWidth > 0;
|
|
485
|
+
const rowWidth = (choice, gap) => leadWidth +
|
|
486
|
+
displayWidth(choice.modeLabel) +
|
|
487
|
+
displayWidth(choice.cycleHint) +
|
|
488
|
+
(hasTail(choice.rightHint) ? gap : 0) +
|
|
489
|
+
displayWidth(choice.rightHint) +
|
|
490
|
+
tailWidth;
|
|
491
|
+
const chosen = candidates.find((choice) => rowWidth(choice, FOOTER_HINT_MIN_GAP) <= contentWidth) ?? { modeLabel: modeLabelFallback, cycleHint: '', rightHint: '' };
|
|
492
|
+
const gap = hasTail(chosen.rightHint)
|
|
493
|
+
? Math.max(FOOTER_HINT_MIN_GAP, contentWidth - rowWidth(chosen, 0))
|
|
494
|
+
: 0;
|
|
495
|
+
return { ...chosen, gap };
|
|
496
|
+
}
|
|
497
|
+
function composerFooterLines(state, contentWidth) {
|
|
376
498
|
const visibleSessionId = formatPromptSessionIdLabel(state.showSessionId, state.sessionId);
|
|
377
|
-
const transientStatus = footerTransientStatus(state.
|
|
499
|
+
const transientStatus = footerTransientStatus(state.transientStatus ?? null);
|
|
378
500
|
const lines = [
|
|
379
501
|
line(span('TheGitAI', { color: 'cyan', bold: true }), span(' • ', { color: 'gray', dim: true }), span(formatPromptDirectoryLabel(state.projectRoot), { color: 'cyan' }), ...(transientStatus
|
|
380
502
|
? [
|
|
@@ -392,11 +514,6 @@ function composerFooterLines(state) {
|
|
|
392
514
|
? [span(` ${visibleSessionId}`, { color: 'gray', dim: true })]
|
|
393
515
|
: []), ...backgroundJobIndicatorSpans(state), ...todoIndicatorSpans(state)),
|
|
394
516
|
plainLine(''),
|
|
395
|
-
plainLine(formatModelLabel(state.currentModelId, state.serverModels), {
|
|
396
|
-
color: 'cyan',
|
|
397
|
-
dim: true,
|
|
398
|
-
}),
|
|
399
|
-
plainLine(''),
|
|
400
517
|
];
|
|
401
518
|
if (state.exitConfirmUntil != null) {
|
|
402
519
|
lines.push(plainLine(state.status || 'Press Ctrl+C again to quit.', {
|
|
@@ -405,28 +522,36 @@ function composerFooterLines(state) {
|
|
|
405
522
|
}));
|
|
406
523
|
return lines;
|
|
407
524
|
}
|
|
408
|
-
const helperText = state.busy
|
|
409
|
-
? state.queuedMessage
|
|
410
|
-
? 'Enter re-queues • ↑ edit queued • Esc cancels queued'
|
|
411
|
-
: 'Enter queues • Esc / Ctrl+C cancel turn'
|
|
412
|
-
: process.platform === 'win32'
|
|
413
|
-
? 'Enter sends • Shift+Tab mode • Alt+V image • Esc cancel turn • Ctrl+C quits'
|
|
414
|
-
: process.platform === 'darwin'
|
|
415
|
-
? 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C quits'
|
|
416
|
-
: 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C quits';
|
|
417
|
-
const agentLabel = agentModeLabel(state.agentMode).padEnd(AGENT_MODE_LABEL_WIDTH);
|
|
418
525
|
const tokenUsageText = state.tokenUsage || formatClientTokenUsage(null);
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
lines.push(line(...footerSpans));
|
|
526
|
+
lines.push(composerFooterModeLine(state, contentWidth, formatModelLabel(state.currentModelId, state.serverModels), tokenUsageText
|
|
527
|
+
? displayWidth(tokenUsageText) + FOOTER_USAGE_SEPARATOR.length
|
|
528
|
+
: 0, tokenUsageText
|
|
529
|
+
? [
|
|
530
|
+
span(FOOTER_USAGE_SEPARATOR, mutedStyle()),
|
|
531
|
+
span(tokenUsageText, { color: 'cyan' }),
|
|
532
|
+
]
|
|
533
|
+
: []));
|
|
428
534
|
return lines;
|
|
429
535
|
}
|
|
536
|
+
function composerFooterModeLine(state, contentWidth, modelLabel, usageWidth, usageSpans) {
|
|
537
|
+
const lead = modelLabel ? `${modelLabel}${FOOTER_MODEL_SEPARATOR}` : '';
|
|
538
|
+
const row = fitComposerFooterRow({
|
|
539
|
+
modeLabels: agentModeLabelTiers(state.agentMode),
|
|
540
|
+
modeLabelFallback: agentModeShortLabel(state.agentMode),
|
|
541
|
+
cycleHint: state.busy ? '' : FOOTER_CYCLE_HINT,
|
|
542
|
+
rightHints: composerFooterRightHints(state),
|
|
543
|
+
leadWidth: displayWidth(lead),
|
|
544
|
+
tailWidth: usageWidth,
|
|
545
|
+
contentWidth,
|
|
546
|
+
});
|
|
547
|
+
const footerMuted = mutedStyle();
|
|
548
|
+
return line(...(lead ? [span(lead, { color: 'cyan', dim: true })] : []), span(row.modeLabel, {
|
|
549
|
+
color: agentModeColor(state.agentMode),
|
|
550
|
+
bold: state.agentMode === 'auto-accept',
|
|
551
|
+
}), ...(row.cycleHint ? [span(row.cycleHint, footerMuted)] : []), ...(row.gap ? [span(' '.repeat(row.gap), footerMuted)] : []), ...(row.rightHint ? [span(row.rightHint, footerMuted)] : []), ...usageSpans);
|
|
552
|
+
}
|
|
553
|
+
const FOOTER_USAGE_SEPARATOR = ' ';
|
|
554
|
+
const FOOTER_MODEL_SEPARATOR = ' ';
|
|
430
555
|
export function formatJobElapsed(elapsedMs) {
|
|
431
556
|
const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1000));
|
|
432
557
|
if (totalSeconds < 60)
|
|
@@ -468,15 +593,29 @@ function jobStatusDescriptor(job, nowMs) {
|
|
|
468
593
|
? { glyph: '✓', color: 'green', text: `exited (0) · ran ${ran}` }
|
|
469
594
|
: { glyph: '✖', color: 'red', text: `exited (${job.exitCode ?? 1}) · ran ${ran}` };
|
|
470
595
|
}
|
|
471
|
-
|
|
596
|
+
const JOBS_PICKER_CHROME_ROWS = 2;
|
|
597
|
+
const JOBS_PICKER_MIN_ROWS = 8;
|
|
598
|
+
function buildJobsPickerLines(state, width, height, nowMs) {
|
|
472
599
|
const jobs = state.backgroundJobs ?? [];
|
|
600
|
+
const bodyBudget = Math.max(1, height - JOBS_PICKER_CHROME_ROWS);
|
|
473
601
|
const lines = [
|
|
474
602
|
plainLine('Background jobs', { color: 'cyan', bold: true }),
|
|
475
603
|
];
|
|
476
604
|
if (jobs.length === 0) {
|
|
477
605
|
lines.push(plainLine('No background jobs in this session.', { color: 'gray' }));
|
|
478
606
|
}
|
|
479
|
-
|
|
607
|
+
let start = 0;
|
|
608
|
+
let visible = jobs;
|
|
609
|
+
if (jobs.length > bodyBudget) {
|
|
610
|
+
start = Math.min(Math.max(0, state.jobsPickerIndex - Math.floor(bodyBudget / 2)), jobs.length - bodyBudget);
|
|
611
|
+
visible = jobs.slice(start, start + bodyBudget);
|
|
612
|
+
if (start > 0) {
|
|
613
|
+
lines.push(plainLine(` … ${start} newer`, { color: 'gray', dim: true }));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const detailBudget = Math.max(0, bodyBudget - visible.length - (start > 0 ? 1 : 0));
|
|
617
|
+
for (const [offset, job] of visible.entries()) {
|
|
618
|
+
const index = start + offset;
|
|
480
619
|
const selected = index === state.jobsPickerIndex;
|
|
481
620
|
const expanded = state.jobsPickerExpandedId === job.id;
|
|
482
621
|
const { glyph, color, text } = jobStatusDescriptor(job, nowMs);
|
|
@@ -485,10 +624,11 @@ function buildJobsPickerLines(state, width, nowMs) {
|
|
|
485
624
|
bold: selected,
|
|
486
625
|
}), span(` ${text}`, { color: 'gray' })));
|
|
487
626
|
if (expanded) {
|
|
488
|
-
const
|
|
627
|
+
const allDetail = (job.detailLines ?? []).length
|
|
489
628
|
? job.detailLines ?? []
|
|
490
629
|
: backgroundJobPreviewLines(job, 3);
|
|
491
|
-
|
|
630
|
+
const detailLines = allDetail.slice(-detailBudget);
|
|
631
|
+
if (detailBudget > 0 && detailLines.length === 0) {
|
|
492
632
|
lines.push(plainLine(' (no output captured)', { color: 'gray' }));
|
|
493
633
|
}
|
|
494
634
|
else {
|
|
@@ -501,22 +641,71 @@ function buildJobsPickerLines(state, width, nowMs) {
|
|
|
501
641
|
}
|
|
502
642
|
}
|
|
503
643
|
}
|
|
644
|
+
const remaining = jobs.length - (start + visible.length);
|
|
645
|
+
if (remaining > 0) {
|
|
646
|
+
lines.push(plainLine(` … ${remaining} older`, { color: 'gray', dim: true }));
|
|
647
|
+
}
|
|
504
648
|
lines.push(plainLine('↑/↓ move enter expand/collapse k kill esc close', {
|
|
505
649
|
color: 'gray',
|
|
506
650
|
}));
|
|
507
651
|
return lines;
|
|
508
652
|
}
|
|
653
|
+
export const THINKING_FALLBACK_PHRASES = [
|
|
654
|
+
'Working out where to start...',
|
|
655
|
+
'Deciding what comes first...',
|
|
656
|
+
'Lining up the pieces...',
|
|
657
|
+
'Untangling the details...',
|
|
658
|
+
'Deciding what not to touch...',
|
|
659
|
+
'Checking what this would break...',
|
|
660
|
+
'Choosing the smaller change...',
|
|
661
|
+
'Picking the least clever option...',
|
|
662
|
+
'Resisting the obvious answer...',
|
|
663
|
+
'Trying the boring explanation first...',
|
|
664
|
+
'Checking whether the assumption holds...',
|
|
665
|
+
'Asking what would have to be true...',
|
|
666
|
+
'Reading it the way the machine would...',
|
|
667
|
+
'Testing the story against the code...',
|
|
668
|
+
'Working out what actually changed...',
|
|
669
|
+
'Finding the smallest thing that explains it...',
|
|
670
|
+
'Looking for the part that is not settled yet...',
|
|
671
|
+
'Making sure this is the simple version...',
|
|
672
|
+
];
|
|
673
|
+
export function pickThinkingFallbackPhrase() {
|
|
674
|
+
const index = Math.floor(Math.random() * THINKING_FALLBACK_PHRASES.length);
|
|
675
|
+
return THINKING_FALLBACK_PHRASES[index];
|
|
676
|
+
}
|
|
677
|
+
function withProgressEllipsis(title) {
|
|
678
|
+
const text = title.trim();
|
|
679
|
+
if (!text)
|
|
680
|
+
return text;
|
|
681
|
+
return /(?:\.\.\.|…)$/.test(text) ? text : `${text}...`;
|
|
682
|
+
}
|
|
509
683
|
function thinkingHeaderLine(spinnerFrame, title, width) {
|
|
510
684
|
const spinner = `${BRAILLE_SPINNER_FRAMES[spinnerFrame % BRAILLE_SPINNER_FRAMES.length]} `;
|
|
511
|
-
const
|
|
512
|
-
|
|
685
|
+
const decorated = withProgressEllipsis(title);
|
|
686
|
+
const fittedTitle = decorated
|
|
687
|
+
? fitLine(decorated, Math.max(8, width - spinner.length - 'Thinking'.length - 3))
|
|
513
688
|
: '';
|
|
514
689
|
return line(span(spinner, { color: 'green' }), span('Thinking', { color: 'green', bold: true }), ...(fittedTitle ? [span(` · ${fittedTitle}`, { color: 'ansi256(248)' })] : []));
|
|
515
690
|
}
|
|
516
691
|
function thinkingNoteLine(note, width) {
|
|
517
692
|
return line(span('│ ', { color: 'green' }), span(fitLine(note, Math.max(8, width - 3)), { color: 'ansi256(248)' }));
|
|
518
693
|
}
|
|
519
|
-
function
|
|
694
|
+
function buildRecoveryStatusLines(status, width) {
|
|
695
|
+
const [label = '', ...rest] = status.split(' · ');
|
|
696
|
+
const detail = rest.join(' · ');
|
|
697
|
+
const labelSpan = span(fitLine(label, Math.max(1, width - 2)), { color: 'yellow', bold: true });
|
|
698
|
+
if (!detail)
|
|
699
|
+
return [line(span('● ', { color: 'yellow' }), labelSpan)];
|
|
700
|
+
if (displayWidth(`● ${label} · ${detail}`) <= width) {
|
|
701
|
+
return [line(span('● ', { color: 'yellow' }), labelSpan, span(` · ${detail}`))];
|
|
702
|
+
}
|
|
703
|
+
return [
|
|
704
|
+
line(span('● ', { color: 'yellow' }), labelSpan),
|
|
705
|
+
...wrapText(detail, Math.max(1, width - 2)).map((row) => plainLine(` ${row}`)),
|
|
706
|
+
];
|
|
707
|
+
}
|
|
708
|
+
function buildLiveLines(state, width, spinnerFrame, elapsedSeconds, nowMs) {
|
|
520
709
|
if (!state.busy)
|
|
521
710
|
return [];
|
|
522
711
|
const lines = [plainLine('')];
|
|
@@ -546,11 +735,22 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
546
735
|
}
|
|
547
736
|
lines.push(plainLine(''));
|
|
548
737
|
}
|
|
549
|
-
lines.push(plainLine(
|
|
738
|
+
lines.push(plainLine(buildWorkingClockLine(state, elapsedSeconds), { color: 'yellow' }));
|
|
739
|
+
if (state.recoveryStatus) {
|
|
740
|
+
lines.push(...buildRecoveryStatusLines(state.recoveryStatus, width));
|
|
741
|
+
}
|
|
742
|
+
const turnMessages = state.turnMessages ?? [];
|
|
743
|
+
if (turnMessages.length > 0) {
|
|
744
|
+
lines.push(plainLine(''));
|
|
745
|
+
lines.push(...buildTurnMessageLines(turnMessages, width, todoProgressTick(nowMs)));
|
|
746
|
+
}
|
|
550
747
|
const todos = state.todos ?? [];
|
|
551
748
|
if (todos.length > 0) {
|
|
552
749
|
lines.push(plainLine(''));
|
|
553
|
-
lines.push(...renderTodoListLines(todos, width, {
|
|
750
|
+
lines.push(...renderTodoListLines(todos, width, {
|
|
751
|
+
header: true,
|
|
752
|
+
progressTick: todoProgressTick(nowMs),
|
|
753
|
+
}));
|
|
554
754
|
}
|
|
555
755
|
const visibleTitle = state.thinkingTitle.trim();
|
|
556
756
|
const visibleNotes = state.thinkingNotes.filter(Boolean);
|
|
@@ -560,11 +760,12 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
560
760
|
const minimalText = visibleTitle && visibleTitle !== 'Thinking'
|
|
561
761
|
? visibleTitle
|
|
562
762
|
: (visibleNotes[visibleNotes.length - 1] ?? '');
|
|
563
|
-
lines.push(thinkingHeaderLine(spinnerFrame, minimalText, width));
|
|
763
|
+
lines.push(thinkingHeaderLine(spinnerFrame, minimalText || THINKING_FALLBACK_PHRASES[0], width));
|
|
564
764
|
}
|
|
565
765
|
else {
|
|
566
|
-
|
|
567
|
-
|
|
766
|
+
const headerTitle = visibleTitle && visibleTitle !== 'Thinking' ? visibleTitle : '';
|
|
767
|
+
lines.push(thinkingHeaderLine(spinnerFrame, headerTitle || THINKING_FALLBACK_PHRASES[0], width));
|
|
768
|
+
for (const note of visibleNotes.slice(0, THINKING_NOTE_PREVIEW_ROWS)) {
|
|
568
769
|
lines.push(thinkingNoteLine(note, width));
|
|
569
770
|
}
|
|
570
771
|
}
|
|
@@ -579,10 +780,24 @@ function overlayPanelLine(row, width, color) {
|
|
|
579
780
|
const padding = Math.max(0, width - lineCharCount(row));
|
|
580
781
|
return line(span('│ ', { color }), ...row.spans, span(' '.repeat(padding)), span(' │', { color }));
|
|
581
782
|
}
|
|
582
|
-
function
|
|
583
|
-
|
|
783
|
+
function overlayPanelMarginLineCount(height) {
|
|
784
|
+
return height < OVERLAY_PANEL_MARGIN_MIN_ROWS
|
|
785
|
+
? 0
|
|
786
|
+
: OVERLAY_PANEL_MARGIN_LINES;
|
|
787
|
+
}
|
|
788
|
+
function overlayPanelContentBudget(height) {
|
|
789
|
+
if (!Number.isFinite(height)) {
|
|
790
|
+
return Number.POSITIVE_INFINITY;
|
|
791
|
+
}
|
|
792
|
+
const margins = overlayPanelMarginLineCount(height) * 2;
|
|
793
|
+
return Math.max(1, Math.floor(height) - margins - 2);
|
|
794
|
+
}
|
|
795
|
+
function buildOverlayPanel(rows, width, color, height = Number.POSITIVE_INFINITY, maxWidth = OVERLAY_PANEL_MAX_WIDTH) {
|
|
796
|
+
const panelWidth = Math.max(24, Math.min(width, maxWidth));
|
|
584
797
|
const innerWidth = Math.max(1, panelWidth - 4);
|
|
585
|
-
const margin = Array.from({
|
|
798
|
+
const margin = Array.from({
|
|
799
|
+
length: overlayPanelMarginLineCount(height),
|
|
800
|
+
}, () => plainLine(''));
|
|
586
801
|
return [
|
|
587
802
|
...margin,
|
|
588
803
|
plainLine(`╭${'─'.repeat(panelWidth - 2)}╮`, { color }),
|
|
@@ -603,7 +818,6 @@ function modelPickerPanelSideLine(content, innerWidth) {
|
|
|
603
818
|
}
|
|
604
819
|
const MODEL_PICKER_COST_WIDTH = 6;
|
|
605
820
|
const MODEL_PICKER_MODEL_WIDTH = 42;
|
|
606
|
-
const MODEL_PICKER_NUMBER_WIDTH = 4;
|
|
607
821
|
const MODEL_PICKER_SEPARATOR = ' │ ';
|
|
608
822
|
const MODEL_PICKER_WIDE_MIN_WIDTH = 78;
|
|
609
823
|
function modelPickerTopBorder(panelWidth) {
|
|
@@ -630,7 +844,6 @@ function modelPickerCostText(rating) {
|
|
|
630
844
|
}
|
|
631
845
|
function modelPickerNotesWidth(innerWidth) {
|
|
632
846
|
return Math.max(18, innerWidth -
|
|
633
|
-
MODEL_PICKER_NUMBER_WIDTH -
|
|
634
847
|
MODEL_PICKER_MODEL_WIDTH -
|
|
635
848
|
MODEL_PICKER_COST_WIDTH -
|
|
636
849
|
MODEL_PICKER_SEPARATOR.length * 2);
|
|
@@ -653,12 +866,10 @@ function modelPickerItemLines(option, selected, innerWidth) {
|
|
|
653
866
|
: selected
|
|
654
867
|
? { color: 'cyan', bold: true }
|
|
655
868
|
: {};
|
|
656
|
-
const numberCell = modelPickerCell(String(option.publicId), MODEL_PICKER_NUMBER_WIDTH, { color: 'cyan', bold: selected, ...selectedStyle });
|
|
657
869
|
const cost = modelPickerCostText(option.costRating);
|
|
658
870
|
if (innerWidth < MODEL_PICKER_WIDE_MIN_WIDTH) {
|
|
659
|
-
const modelWidth = Math.max(12, innerWidth -
|
|
871
|
+
const modelWidth = Math.max(12, innerWidth - MODEL_PICKER_COST_WIDTH - 2);
|
|
660
872
|
const row = [
|
|
661
|
-
numberCell,
|
|
662
873
|
...modelPickerModelSpans(option, selected, modelWidth, false, labelStyle, selectedStyle),
|
|
663
874
|
span(' ', selectedStyle),
|
|
664
875
|
modelPickerCell(cost, MODEL_PICKER_COST_WIDTH, selectedStyle),
|
|
@@ -668,7 +879,6 @@ function modelPickerItemLines(option, selected, innerWidth) {
|
|
|
668
879
|
];
|
|
669
880
|
}
|
|
670
881
|
const row = [
|
|
671
|
-
numberCell,
|
|
672
882
|
...modelPickerModelSpans(option, selected, MODEL_PICKER_MODEL_WIDTH, true, labelStyle, selectedStyle),
|
|
673
883
|
span(MODEL_PICKER_SEPARATOR, { color: 'gray', ...selectedStyle }),
|
|
674
884
|
modelPickerCell(cost, MODEL_PICKER_COST_WIDTH, selectedStyle),
|
|
@@ -685,9 +895,9 @@ function modelPickerItemLines(option, selected, innerWidth) {
|
|
|
685
895
|
function modelPickerHeaderLine(innerWidth) {
|
|
686
896
|
const heading = { color: MODEL_PICKER_ACCENT_COLOR, bold: true };
|
|
687
897
|
if (innerWidth < MODEL_PICKER_WIDE_MIN_WIDTH) {
|
|
688
|
-
return line(modelPickerCell('
|
|
898
|
+
return line(modelPickerCell('Model', Math.max(1, innerWidth), heading));
|
|
689
899
|
}
|
|
690
|
-
return line(modelPickerCell('
|
|
900
|
+
return line(modelPickerCell('Model', MODEL_PICKER_MODEL_WIDTH, heading), span(MODEL_PICKER_SEPARATOR, { color: 'gray' }), modelPickerCell('Cost', MODEL_PICKER_COST_WIDTH, heading), span(MODEL_PICKER_SEPARATOR, { color: 'gray' }), modelPickerCell('Notes', modelPickerNotesWidth(innerWidth), heading));
|
|
691
901
|
}
|
|
692
902
|
function buildModelPickerPanel(options, selectedIndex, width, availableHeight) {
|
|
693
903
|
const panelWidth = Math.max(28, Math.min(width, MODEL_PICKER_PANEL_MAX_WIDTH));
|
|
@@ -715,6 +925,71 @@ function buildModelPickerPanel(options, selectedIndex, width, availableHeight) {
|
|
|
715
925
|
body.push(modelPickerDivider(panelWidth), modelPickerPanelSideLine(plainLine(fitLine(hint, innerWidth), { color: 'gray' }), innerWidth), plainLine(`╰${'─'.repeat(panelWidth - 2)}╯`, { color: MODEL_PICKER_BORDER_COLOR }));
|
|
716
926
|
return [...margin, ...body, ...margin];
|
|
717
927
|
}
|
|
928
|
+
const SHORTCUTS_PANEL_MAX_WIDTH = 96;
|
|
929
|
+
const SHORTCUTS_KEY_COLUMN_MIN = 10;
|
|
930
|
+
function shortcutsTopBorder(panelWidth) {
|
|
931
|
+
const fullTitle = ' TheGitAI - Keyboard & mouse ';
|
|
932
|
+
const compactTitle = ' Shortcuts ';
|
|
933
|
+
const title = panelWidth >= fullTitle.length + 4 ? fullTitle : compactTitle;
|
|
934
|
+
const available = Math.max(0, panelWidth - 2 - [...title].length);
|
|
935
|
+
const left = Math.floor(available / 2);
|
|
936
|
+
const right = available - left;
|
|
937
|
+
return line(span(`╭${'─'.repeat(left)}`, { color: MODEL_PICKER_BORDER_COLOR }), span(title, { color: MODEL_PICKER_ACCENT_COLOR, bold: true }), span(`${'─'.repeat(right)}╮`, { color: MODEL_PICKER_BORDER_COLOR }));
|
|
938
|
+
}
|
|
939
|
+
export function buildShortcutsPanel(groups, width, availableHeight) {
|
|
940
|
+
const panelWidth = Math.max(28, Math.min(width, SHORTCUTS_PANEL_MAX_WIDTH));
|
|
941
|
+
const innerWidth = Math.max(1, panelWidth - 4);
|
|
942
|
+
const keyWidth = Math.max(SHORTCUTS_KEY_COLUMN_MIN, ...groups.flatMap((group) => group.rows.map((row) => displayWidth(row.keys))));
|
|
943
|
+
const side = (row) => overlayPanelLine(row, innerWidth, MODEL_PICKER_BORDER_COLOR);
|
|
944
|
+
const groupLines = (group, spaced) => [
|
|
945
|
+
...(spaced ? [side(plainLine(''))] : []),
|
|
946
|
+
side(plainLine(fitLine(group.title, innerWidth), {
|
|
947
|
+
color: MODEL_PICKER_ACCENT_COLOR,
|
|
948
|
+
bold: true,
|
|
949
|
+
})),
|
|
950
|
+
...group.rows.map((row) => side(line(span(` ${row.keys}${' '.repeat(Math.max(0, keyWidth - displayWidth(row.keys)))} `, { bold: true }), span(fitLine(row.description, Math.max(1, innerWidth - keyWidth - 4)), { color: 'gray' })))),
|
|
951
|
+
];
|
|
952
|
+
const chromeRows = 4;
|
|
953
|
+
const budget = Math.max(1, availableHeight - chromeRows);
|
|
954
|
+
const spacedBody = groups.flatMap((group, index) => groupLines(group, index > 0));
|
|
955
|
+
const plainBody = groups.flatMap((group) => groupLines(group, false));
|
|
956
|
+
let body = spacedBody;
|
|
957
|
+
let hidden = 0;
|
|
958
|
+
if (spacedBody.length > budget) {
|
|
959
|
+
body = plainBody;
|
|
960
|
+
if (plainBody.length > budget) {
|
|
961
|
+
body = [];
|
|
962
|
+
hidden = groups.reduce((total, group) => total + group.rows.length, 0);
|
|
963
|
+
for (const group of groups) {
|
|
964
|
+
const whole = groupLines(group, false);
|
|
965
|
+
if (body.length + whole.length <= budget) {
|
|
966
|
+
body = [...body, ...whole];
|
|
967
|
+
hidden -= group.rows.length;
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
const room = budget - body.length - 1;
|
|
971
|
+
if (room >= 1) {
|
|
972
|
+
const kept = group.rows.slice(0, room);
|
|
973
|
+
body = [...body, ...groupLines({ ...group, rows: kept }, false)];
|
|
974
|
+
hidden -= kept.length;
|
|
975
|
+
}
|
|
976
|
+
break;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
const hint = hidden > 0 ? `esc close • ${hidden} more in /help` : 'esc close';
|
|
981
|
+
return [
|
|
982
|
+
shortcutsTopBorder(panelWidth),
|
|
983
|
+
...body,
|
|
984
|
+
plainLine(`├${'─'.repeat(panelWidth - 2)}┤`, {
|
|
985
|
+
color: MODEL_PICKER_BORDER_COLOR,
|
|
986
|
+
}),
|
|
987
|
+
side(plainLine(fitLine(hint, innerWidth), { color: 'gray' })),
|
|
988
|
+
plainLine(`╰${'─'.repeat(panelWidth - 2)}╯`, {
|
|
989
|
+
color: MODEL_PICKER_BORDER_COLOR,
|
|
990
|
+
}),
|
|
991
|
+
];
|
|
992
|
+
}
|
|
718
993
|
function commandPaletteTopBorder(panelWidth) {
|
|
719
994
|
const prefix = '╭─ Commands ';
|
|
720
995
|
const suffix = '╮';
|
|
@@ -760,7 +1035,7 @@ function commandPaletteItemLines(option, selected, innerWidth) {
|
|
|
760
1035
|
modelPickerPanelSideLine(line(span(`${MODEL_PICKER_META_INDENT}${option.description}`, { color: 'gray' })), innerWidth),
|
|
761
1036
|
];
|
|
762
1037
|
}
|
|
763
|
-
function buildCommandPalettePanel(suggestions, selectedIndex, width) {
|
|
1038
|
+
function buildCommandPalettePanel(suggestions, selectedIndex, hiddenCount, width) {
|
|
764
1039
|
const panelWidth = Math.max(28, Math.min(width, MODEL_PICKER_PANEL_MAX_WIDTH));
|
|
765
1040
|
const innerWidth = Math.max(1, panelWidth - 4);
|
|
766
1041
|
const margin = Array.from({ length: MODEL_PICKER_PANEL_MARGIN_LINES }, () => plainLine(''));
|
|
@@ -779,13 +1054,117 @@ function buildCommandPalettePanel(suggestions, selectedIndex, width) {
|
|
|
779
1054
|
body.push(commandPaletteSeparatorLine(innerWidth));
|
|
780
1055
|
}
|
|
781
1056
|
});
|
|
1057
|
+
if (hiddenCount > 0) {
|
|
1058
|
+
body.push(commandPaletteSeparatorLine(innerWidth), modelPickerPanelSideLine(plainLine(` ${hiddenCount} more • keep typing to narrow`, {
|
|
1059
|
+
color: 'gray',
|
|
1060
|
+
dim: true,
|
|
1061
|
+
}), innerWidth));
|
|
1062
|
+
}
|
|
782
1063
|
body.push(modelPickerPanelSideLine(plainLine(''), innerWidth), modelPickerPanelSideLine(line(span('─'.repeat(innerWidth), { color: MODEL_PICKER_BORDER_COLOR })), innerWidth), modelPickerPanelSideLine(plainLine('↑/↓ choose • Tab or Enter accept • Esc cancel', { color: 'gray' }), innerWidth), plainLine(`╰${'─'.repeat(panelWidth - 2)}╯`, { color: MODEL_PICKER_BORDER_COLOR }));
|
|
783
1064
|
return [...margin, ...body, ...margin];
|
|
784
1065
|
}
|
|
1066
|
+
export function formatElapsedClock(elapsedSeconds) {
|
|
1067
|
+
if (elapsedSeconds < 60)
|
|
1068
|
+
return `${elapsedSeconds}s`;
|
|
1069
|
+
return `${Math.floor(elapsedSeconds / 60)}m ${String(elapsedSeconds % 60).padStart(2, '0')}s`;
|
|
1070
|
+
}
|
|
1071
|
+
export function buildWorkingClockLine(state, elapsedSeconds) {
|
|
1072
|
+
const elapsed = formatElapsedClock(elapsedSeconds);
|
|
1073
|
+
if (state.busyPausedAt != null) {
|
|
1074
|
+
return `${WORKING_CLOCK_ICON} Paused · ${elapsed} · waiting for your response`;
|
|
1075
|
+
}
|
|
1076
|
+
const label = state.generatingImage
|
|
1077
|
+
? 'Generating image'
|
|
1078
|
+
: state.analyzingImages > 0
|
|
1079
|
+
? state.analyzingImages > 1
|
|
1080
|
+
? 'Analyzing images'
|
|
1081
|
+
: 'Analyzing image'
|
|
1082
|
+
: 'Working';
|
|
1083
|
+
return `${WORKING_CLOCK_ICON} ${label} · ${elapsed}`;
|
|
1084
|
+
}
|
|
1085
|
+
export function approvalPanelInnerWidth(width) {
|
|
1086
|
+
return Math.max(1, Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH)) - 4);
|
|
1087
|
+
}
|
|
1088
|
+
export function userInputPanelInnerWidth(width) {
|
|
1089
|
+
return Math.max(1, Math.max(24, Math.min(width, USER_INPUT_PANEL_MAX_WIDTH)) - 4);
|
|
1090
|
+
}
|
|
1091
|
+
export function approvalPaddingEnabled(height) {
|
|
1092
|
+
return height >= APPROVAL_PADDING_MIN_ROWS;
|
|
1093
|
+
}
|
|
1094
|
+
export function approvalPreviewBudget(height) {
|
|
1095
|
+
const chrome = APPROVAL_OVERLAY_CHROME_ROWS +
|
|
1096
|
+
(approvalPaddingEnabled(height) ? APPROVAL_PADDING_CHROME_ROWS : 0);
|
|
1097
|
+
return Math.max(APPROVAL_PREVIEW_MIN_ROWS, Math.min(APPROVAL_PREVIEW_MAX_ROWS, Math.floor(height / 2) - chrome));
|
|
1098
|
+
}
|
|
1099
|
+
export function approvalPreviewRows(prompt, innerWidth) {
|
|
1100
|
+
const previewWidth = Math.max(8, innerWidth - APPROVAL_SCROLLBAR_COLUMNS);
|
|
1101
|
+
if (prompt.diffPreview) {
|
|
1102
|
+
return renderDiffPreviewLines(prompt.diffPreview, previewWidth, prompt.diffPreview.lines.length);
|
|
1103
|
+
}
|
|
1104
|
+
return wrapText(prompt.body, previewWidth).map((text) => plainLine(text, { color: OVERLAY_BORDER_COLOR }));
|
|
1105
|
+
}
|
|
1106
|
+
export function approvalScrollLimit(prompt, width, height) {
|
|
1107
|
+
if (!prompt)
|
|
1108
|
+
return 0;
|
|
1109
|
+
return Math.max(0, approvalPreviewRows(prompt, approvalPanelInnerWidth(width)).length -
|
|
1110
|
+
approvalPreviewBudget(height));
|
|
1111
|
+
}
|
|
1112
|
+
function approvalScrollbarGlyphs(totalRows, visibleRows, offset) {
|
|
1113
|
+
const thumbRows = Math.max(1, Math.min(visibleRows, Math.round((visibleRows * visibleRows) / totalRows)));
|
|
1114
|
+
const maxOffset = totalRows - visibleRows;
|
|
1115
|
+
const thumbTop = maxOffset <= 0
|
|
1116
|
+
? 0
|
|
1117
|
+
: Math.round((offset / maxOffset) * (visibleRows - thumbRows));
|
|
1118
|
+
return Array.from({ length: visibleRows }, (_, index) => index >= thumbTop && index < thumbTop + thumbRows
|
|
1119
|
+
? APPROVAL_SCROLLBAR_THUMB
|
|
1120
|
+
: APPROVAL_SCROLLBAR_TRACK);
|
|
1121
|
+
}
|
|
1122
|
+
function withScrollbarGlyph(target, glyph, column) {
|
|
1123
|
+
const padding = Math.max(1, column - lineCharCount(target));
|
|
1124
|
+
return {
|
|
1125
|
+
spans: [
|
|
1126
|
+
...target.spans,
|
|
1127
|
+
span(' '.repeat(padding)),
|
|
1128
|
+
span(glyph, {
|
|
1129
|
+
color: 'gray',
|
|
1130
|
+
dim: glyph === APPROVAL_SCROLLBAR_TRACK,
|
|
1131
|
+
}),
|
|
1132
|
+
],
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
export function buildApprovalPreviewWindow(prompt, innerWidth, height, requestedOffset) {
|
|
1136
|
+
const rows = approvalPreviewRows(prompt, innerWidth);
|
|
1137
|
+
const budget = approvalPreviewBudget(height);
|
|
1138
|
+
if (rows.length <= budget) {
|
|
1139
|
+
return {
|
|
1140
|
+
firstVisibleRow: rows.length === 0 ? 0 : 1,
|
|
1141
|
+
lastVisibleRow: rows.length,
|
|
1142
|
+
lines: rows,
|
|
1143
|
+
offset: 0,
|
|
1144
|
+
totalRows: rows.length,
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
const offset = Math.max(0, Math.min(requestedOffset, rows.length - budget));
|
|
1148
|
+
const visible = rows.slice(offset, offset + budget);
|
|
1149
|
+
const glyphs = approvalScrollbarGlyphs(rows.length, budget, offset);
|
|
1150
|
+
return {
|
|
1151
|
+
firstVisibleRow: offset + 1,
|
|
1152
|
+
lastVisibleRow: offset + visible.length,
|
|
1153
|
+
lines: visible.map((target, index) => withScrollbarGlyph(target, glyphs[index], innerWidth - 1)),
|
|
1154
|
+
offset,
|
|
1155
|
+
totalRows: rows.length,
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
export function approvalScrollStatusLine(window) {
|
|
1159
|
+
return `lines ${window.firstVisibleRow}–${window.lastVisibleRow} of ${window.totalRows} · PgUp/PgDn scrolls`;
|
|
1160
|
+
}
|
|
1161
|
+
export function rightAlignedLine(text, width, style = {}) {
|
|
1162
|
+
return plainLine(`${' '.repeat(Math.max(0, width - displayWidth(text)))}${text}`, style);
|
|
1163
|
+
}
|
|
785
1164
|
function buildOverlayLines(state, width, height, nowMs) {
|
|
786
1165
|
const lines = [];
|
|
787
1166
|
const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
|
|
788
|
-
const innerWidth =
|
|
1167
|
+
const innerWidth = approvalPanelInnerWidth(width);
|
|
789
1168
|
if (state.sudoPrompt) {
|
|
790
1169
|
const prompt = state.sudoPrompt;
|
|
791
1170
|
const passwordWidth = Math.max(0, innerWidth - 11);
|
|
@@ -805,75 +1184,120 @@ function buildOverlayLines(state, width, height, nowMs) {
|
|
|
805
1184
|
: line(span(' '), span(text, { color: OVERLAY_BORDER_COLOR })));
|
|
806
1185
|
});
|
|
807
1186
|
lines.push(plainLine(''));
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
}
|
|
1187
|
+
lines.push(...wrapText(SUDO_PASSWORD_ASSURANCE, innerWidth).map((text) => plainLine(text, { color: SUDO_ASSURANCE_COLOR })));
|
|
1188
|
+
lines.push(plainLine(''));
|
|
811
1189
|
lines.push(line(span('Password: ', { color: 'cyan', bold: true }), span('•'.repeat(Math.min(prompt.passwordLength, passwordWidth)), {
|
|
812
1190
|
color: 'cyan',
|
|
813
1191
|
}), span(' ', { inverse: true })));
|
|
814
1192
|
lines.push(plainLine('Press Enter to submit, Escape to cancel', { color: 'gray' }));
|
|
815
|
-
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR);
|
|
1193
|
+
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR, height);
|
|
1194
|
+
}
|
|
1195
|
+
if (state.userInputPrompt) {
|
|
1196
|
+
if (height < 3) {
|
|
1197
|
+
return [];
|
|
1198
|
+
}
|
|
1199
|
+
return buildOverlayPanel(buildUserInputOverlayLines(state.userInputPrompt, userInputPanelInnerWidth(width), overlayPanelContentBudget(height)), width, USER_INPUT_BORDER_COLOR, height, USER_INPUT_PANEL_MAX_WIDTH);
|
|
816
1200
|
}
|
|
817
1201
|
if (state.approvalPrompt) {
|
|
818
1202
|
const prompt = state.approvalPrompt;
|
|
1203
|
+
const padded = approvalPaddingEnabled(height);
|
|
819
1204
|
lines.push(plainLine(prompt.title, {
|
|
820
|
-
color:
|
|
1205
|
+
color: APPROVAL_ACCENT_COLOR,
|
|
821
1206
|
bold: true,
|
|
822
1207
|
}));
|
|
1208
|
+
if (padded)
|
|
1209
|
+
lines.push(plainLine(''));
|
|
823
1210
|
if (prompt.diffPreview && prompt.filePath) {
|
|
824
1211
|
lines.push(line(span('● ', { color: 'green' }), span('Update(', { bold: true }), span(prompt.filePath, { color: 'cyan', bold: true }), span(')', { bold: true })));
|
|
825
|
-
lines.push(...renderTranscriptEntryLines({
|
|
826
|
-
body: '',
|
|
827
|
-
diffPreview: prompt.diffPreview,
|
|
828
|
-
kind: 'diff',
|
|
829
|
-
title: '',
|
|
830
|
-
}, innerWidth));
|
|
831
1212
|
}
|
|
832
|
-
|
|
833
|
-
|
|
1213
|
+
const previewWindow = buildApprovalPreviewWindow(prompt, innerWidth, height, state.approvalScrollOffset ?? 0);
|
|
1214
|
+
lines.push(...previewWindow.lines);
|
|
1215
|
+
if (padded)
|
|
1216
|
+
lines.push(plainLine(''));
|
|
1217
|
+
if (previewWindow.totalRows > previewWindow.lines.length) {
|
|
1218
|
+
lines.push(rightAlignedLine(approvalScrollStatusLine(previewWindow), innerWidth, {
|
|
1219
|
+
color: APPROVAL_SCROLL_STATUS_COLOR,
|
|
1220
|
+
}));
|
|
1221
|
+
if (padded)
|
|
1222
|
+
lines.push(plainLine(''));
|
|
834
1223
|
}
|
|
835
|
-
const options = [
|
|
836
|
-
|
|
837
|
-
{ value: 'a', label: 'Approve all remaining actions', color: 'cyan' },
|
|
838
|
-
{ value: 'n', label: 'Deny', color: 'red' },
|
|
839
|
-
];
|
|
840
|
-
options.forEach((option, index) => {
|
|
1224
|
+
const options = (prompt.options ?? []).map((option) => option.label);
|
|
1225
|
+
options.forEach((label, index) => {
|
|
841
1226
|
const selected = index === state.approvalCursor;
|
|
842
1227
|
lines.push(line(span(selected ? '› ' : ' ', {
|
|
843
|
-
color: selected ?
|
|
844
|
-
}), span(
|
|
845
|
-
color: selected ?
|
|
846
|
-
bold:
|
|
847
|
-
}), span(` ${option.label}`, {
|
|
848
|
-
color: selected ? OVERLAY_BORDER_COLOR : undefined,
|
|
1228
|
+
color: selected ? APPROVAL_ACCENT_COLOR : 'gray',
|
|
1229
|
+
}), span(label, {
|
|
1230
|
+
color: selected ? APPROVAL_ACCENT_COLOR : undefined,
|
|
1231
|
+
bold: selected,
|
|
849
1232
|
})));
|
|
850
1233
|
});
|
|
851
|
-
|
|
1234
|
+
if (padded)
|
|
1235
|
+
lines.push(plainLine(''));
|
|
1236
|
+
lines.push(plainLine('↑/↓ moves • Enter confirms • Esc denies', {
|
|
852
1237
|
color: 'gray',
|
|
853
1238
|
}));
|
|
854
|
-
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR);
|
|
1239
|
+
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR, height);
|
|
1240
|
+
}
|
|
1241
|
+
if (state.shortcutsOpen) {
|
|
1242
|
+
lines.push(...buildShortcutsPanel(buildShortcutGroups(), width, height));
|
|
855
1243
|
}
|
|
856
1244
|
if (state.modelPickerOpen) {
|
|
857
1245
|
const options = buildModelPickerOptions(state.currentModelId, state.serverModels);
|
|
858
1246
|
lines.push(...buildModelPickerPanel(options, state.modelPickerIndex, width, height));
|
|
859
1247
|
}
|
|
860
1248
|
if (state.jobsPickerOpen) {
|
|
861
|
-
lines.push(...buildJobsPickerLines(state, width, nowMs));
|
|
1249
|
+
lines.push(...buildJobsPickerLines(state, width, height, nowMs));
|
|
862
1250
|
}
|
|
863
1251
|
if (state.resumePickerOpen) {
|
|
864
1252
|
const filtered = filterResumeSessions(state.resumePickerSessions, state.resumePickerFilter, state.serverModels);
|
|
1253
|
+
const pickerWidth = Math.max(20, width - 2);
|
|
1254
|
+
const divider = () => plainLine('─'.repeat(pickerWidth), { color: 'gray', dim: true });
|
|
865
1255
|
lines.push(plainLine('Resume a previous session', { color: 'cyan', bold: true }));
|
|
866
1256
|
lines.push(line(span('Search: ', { color: 'gray' }), span(state.resumePickerFilter, {}), span('█', { color: 'gray' })));
|
|
867
|
-
|
|
1257
|
+
lines.push(divider());
|
|
1258
|
+
const maxCards = Math.max(2, Math.min(filtered.length, Math.floor((height - 10) / 3)));
|
|
1259
|
+
let start = 0;
|
|
1260
|
+
if (filtered.length > maxCards) {
|
|
1261
|
+
start = Math.min(Math.max(0, state.resumePickerIndex - Math.floor(maxCards / 2)), filtered.length - maxCards);
|
|
1262
|
+
}
|
|
1263
|
+
const visible = filtered.slice(start, start + maxCards);
|
|
1264
|
+
if (start > 0) {
|
|
1265
|
+
lines.push(plainLine(` … ${start} newer`, { color: 'gray', dim: true }));
|
|
1266
|
+
}
|
|
1267
|
+
for (const [offset, session] of visible.entries()) {
|
|
1268
|
+
const index = start + offset;
|
|
868
1269
|
const selected = index === state.resumePickerIndex;
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
1270
|
+
const prompt = singleLinePreview(session.lastUserMessage, pickerWidth) ||
|
|
1271
|
+
session.name ||
|
|
1272
|
+
`Session ${session.id}`;
|
|
1273
|
+
lines.push(line(span(selected ? '› ' : ' ', { color: 'cyan' }), span(fitLine(prompt, Math.max(10, pickerWidth - 2)), selected
|
|
1274
|
+
? { color: 'cyan', bold: true }
|
|
1275
|
+
: session.lastUserMessage
|
|
1276
|
+
? {}
|
|
1277
|
+
: { color: 'gray', dim: true })));
|
|
1278
|
+
const meta = [
|
|
1279
|
+
formatRelativeTime(session.updatedAt),
|
|
1280
|
+
pickerModelLabel(session.modelId, state.serverModels),
|
|
1281
|
+
session.branch ?? null,
|
|
1282
|
+
]
|
|
1283
|
+
.filter(Boolean)
|
|
1284
|
+
.join(' · ');
|
|
1285
|
+
lines.push(line(span(' '), span(fitLine(meta, Math.max(10, pickerWidth - 4)), {
|
|
1286
|
+
color: selected ? 'cyan' : 'gray',
|
|
1287
|
+
dim: !selected,
|
|
1288
|
+
})));
|
|
1289
|
+
if (offset < visible.length - 1)
|
|
1290
|
+
lines.push(plainLine(''));
|
|
1291
|
+
}
|
|
1292
|
+
const remaining = filtered.length - (start + visible.length);
|
|
1293
|
+
if (remaining > 0) {
|
|
1294
|
+
lines.push(plainLine(` … ${remaining} older`, { color: 'gray', dim: true }));
|
|
872
1295
|
}
|
|
873
1296
|
if (filtered.length === 0) {
|
|
874
1297
|
lines.push(plainLine('No sessions match.', { color: 'gray' }));
|
|
875
1298
|
}
|
|
876
|
-
lines.push(
|
|
1299
|
+
lines.push(divider());
|
|
1300
|
+
lines.push(plainLine('↑/↓ move · enter resume · esc start new · ctrl+c quit', {
|
|
877
1301
|
color: 'gray',
|
|
878
1302
|
}));
|
|
879
1303
|
}
|
|
@@ -885,13 +1309,22 @@ function buildOverlayLines(state, width, height, nowMs) {
|
|
|
885
1309
|
state.input.trimStart().startsWith('/') &&
|
|
886
1310
|
!state.input.includes(' ') &&
|
|
887
1311
|
suggestions.length > 0) {
|
|
888
|
-
lines.push(...buildCommandPalettePanel(suggestions, state.commandCursor, width));
|
|
1312
|
+
lines.push(...buildCommandPalettePanel(suggestions, state.commandCursor, getHiddenSlashCommandCount(state.input), width));
|
|
889
1313
|
}
|
|
890
1314
|
return lines;
|
|
891
1315
|
}
|
|
892
1316
|
function countSectionLines(sections) {
|
|
893
1317
|
return sections.reduce((sum, section) => sum + section.lines.length, 0);
|
|
894
1318
|
}
|
|
1319
|
+
export function userInputViewportForFrame(state, cols, rows, spinnerFrame, elapsedSeconds, nowMs = 0) {
|
|
1320
|
+
const contentWidth = frameContentWidth(cols);
|
|
1321
|
+
const liveRows = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds, nowMs).length;
|
|
1322
|
+
const overlayHeight = Math.max(0, rows - liveRows - composerFooterLines(state, contentWidth).length);
|
|
1323
|
+
return {
|
|
1324
|
+
width: userInputPanelInnerWidth(contentWidth),
|
|
1325
|
+
maxRows: overlayPanelContentBudget(overlayHeight),
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
895
1328
|
function sliceTranscriptLines(lines, maxLines, scrollOffset) {
|
|
896
1329
|
if (maxLines <= 0 || lines.length <= maxLines) {
|
|
897
1330
|
return lines;
|
|
@@ -902,33 +1335,43 @@ function sliceTranscriptLines(lines, maxLines, scrollOffset) {
|
|
|
902
1335
|
return lines.slice(start, start + maxLines);
|
|
903
1336
|
}
|
|
904
1337
|
export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, nowMs = 0) {
|
|
905
|
-
const contentWidth =
|
|
1338
|
+
const contentWidth = frameContentWidth(cols);
|
|
906
1339
|
const gutter = Math.max(Math.floor((cols - contentWidth) / 2), 0);
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1340
|
+
const buildTranscriptLines = (wrapWidth) => {
|
|
1341
|
+
const blocks = state.transcript.map((entry) => cachedTranscriptEntryLines(entry, wrapWidth));
|
|
1342
|
+
const lines = [];
|
|
1343
|
+
blocks.forEach((block, index) => {
|
|
1344
|
+
if (index > 0) {
|
|
1345
|
+
lines.push(plainLine(''));
|
|
1346
|
+
}
|
|
1347
|
+
lines.push(...block);
|
|
1348
|
+
});
|
|
1349
|
+
return lines;
|
|
1350
|
+
};
|
|
1351
|
+
let transcriptLines = buildTranscriptLines(contentWidth);
|
|
915
1352
|
const sections = [];
|
|
916
|
-
const liveLines = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds);
|
|
1353
|
+
const liveLines = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds, nowMs);
|
|
917
1354
|
if (liveLines.length > 0) {
|
|
918
1355
|
sections.push({ kind: 'live', lines: liveLines });
|
|
919
1356
|
}
|
|
920
|
-
const overlayActive = Boolean(state.approvalPrompt || state.sudoPrompt);
|
|
921
|
-
if (!state.resumePickerOpen &&
|
|
1357
|
+
const overlayActive = Boolean(state.approvalPrompt || state.sudoPrompt || state.userInputPrompt);
|
|
1358
|
+
if (!state.resumePickerOpen &&
|
|
1359
|
+
!state.modelPickerOpen &&
|
|
1360
|
+
!state.jobsPickerOpen &&
|
|
1361
|
+
!state.shortcutsOpen &&
|
|
1362
|
+
!overlayActive) {
|
|
922
1363
|
const composerLines = [];
|
|
923
|
-
if (state.
|
|
924
|
-
|
|
1364
|
+
if (state.busy && state.status === 'Starting a new conversation...') {
|
|
1365
|
+
composerLines.push(line(span('Starting a new conversation…', { color: 'gray', dim: true })));
|
|
1366
|
+
}
|
|
1367
|
+
else if (state.queuedMessage) {
|
|
925
1368
|
const imageCount = state.queuedMessage.imageAttachments.length;
|
|
926
|
-
composerLines.push(line(span(
|
|
927
|
-
?
|
|
928
|
-
:
|
|
1369
|
+
composerLines.push(line(span('Queued', { color: 'cyan', bold: true }), span(' Enter', { color: 'cyan', bold: true }), span(imageCount > 0
|
|
1370
|
+
? ' sends it with the next prompt'
|
|
1371
|
+
: ' sends it to the agent now', { color: 'gray' }), span(' · ↑', { color: 'cyan', bold: true }), span(' edit', { color: 'gray' }), span(' · Esc', { color: 'cyan', bold: true }), span(' discard', { color: 'gray' })));
|
|
929
1372
|
}
|
|
930
1373
|
else {
|
|
931
|
-
const promptLabel = state
|
|
1374
|
+
const promptLabel = composerPromptLabel(state);
|
|
932
1375
|
const placeholder = state.busy
|
|
933
1376
|
? 'Type to queue the next message'
|
|
934
1377
|
: 'Type a request or /help';
|
|
@@ -936,17 +1379,37 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
|
|
|
936
1379
|
}
|
|
937
1380
|
sections.push({
|
|
938
1381
|
kind: 'composer',
|
|
939
|
-
lines: [...composerLines, plainLine(''), ...composerFooterLines(state)],
|
|
1382
|
+
lines: [...composerLines, plainLine(''), ...composerFooterLines(state, contentWidth)],
|
|
940
1383
|
});
|
|
941
1384
|
}
|
|
942
|
-
|
|
1385
|
+
if (state.userInputPrompt) {
|
|
1386
|
+
sections.push({
|
|
1387
|
+
kind: 'busyFooter',
|
|
1388
|
+
lines: composerFooterLines(state, contentWidth),
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
const overlayHeight = state.userInputPrompt
|
|
1392
|
+
? Math.max(0, rows - countSectionLines(sections))
|
|
1393
|
+
: state.jobsPickerOpen || state.shortcutsOpen
|
|
1394
|
+
? Math.max(Math.min(rows, JOBS_PICKER_MIN_ROWS), rows - countSectionLines(sections))
|
|
1395
|
+
: rows;
|
|
1396
|
+
const overlayLines = buildOverlayLines(state, contentWidth, overlayHeight, nowMs);
|
|
943
1397
|
if (overlayLines.length > 0) {
|
|
944
1398
|
sections.push({ kind: 'overlay', lines: overlayLines });
|
|
945
1399
|
}
|
|
946
1400
|
const reservedLines = countSectionLines(sections.filter((section) => section.kind !== 'transcript'));
|
|
947
|
-
const composerReserve = state.resumePickerOpen
|
|
1401
|
+
const composerReserve = state.resumePickerOpen ||
|
|
1402
|
+
state.approvalPrompt ||
|
|
1403
|
+
state.sudoPrompt ||
|
|
1404
|
+
state.userInputPrompt
|
|
1405
|
+
? 0
|
|
1406
|
+
: 4;
|
|
948
1407
|
const transcriptBudget = Math.max(1, rows - reservedLines - composerReserve - 1);
|
|
949
|
-
|
|
1408
|
+
let transcriptScrollLimit = Math.max(0, transcriptLines.length - transcriptBudget);
|
|
1409
|
+
if (transcriptScrollLimit > 0 && contentWidth > 2) {
|
|
1410
|
+
transcriptLines = buildTranscriptLines(contentWidth - 2);
|
|
1411
|
+
transcriptScrollLimit = Math.max(0, transcriptLines.length - transcriptBudget);
|
|
1412
|
+
}
|
|
950
1413
|
const transcriptScrollOffset = Math.min(Math.max(state.transcriptScrollOffset, 0), transcriptScrollLimit);
|
|
951
1414
|
sections.unshift({
|
|
952
1415
|
kind: 'transcript',
|