@thegitai/cli 1.0.0-preview.4 → 1.0.0-preview.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -4
- package/dist/bin/ai.js +119 -111
- package/dist/bin/browser-host.js +265 -0
- package/dist/src/agent-mode.js +11 -6
- package/dist/src/api/auth.js +6 -4
- package/dist/src/api/browser-login.js +152 -37
- package/dist/src/api/chat.js +286 -44
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/api/default-host.js +1 -0
- package/dist/src/api/http.js +127 -7
- package/dist/src/api/models.js +29 -10
- package/dist/src/background-jobs.js +4 -2
- package/dist/src/browser/bridge.js +232 -0
- package/dist/src/browser/framing.js +42 -0
- package/dist/src/browser/native-host.js +209 -0
- package/dist/src/browser/protocol.js +46 -0
- package/dist/src/browser/session-bridge.js +104 -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 +58 -17
- package/dist/src/patcher.js +43 -4
- package/dist/src/permissions.js +288 -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 +12 -46
- package/dist/src/tools/browser.js +628 -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-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 +802 -195
- package/dist/src/ui/tui/bridge.js +10 -0
- package/dist/src/ui/tui/build-frame.js +517 -165
- package/dist/src/ui/tui/composer-layout.js +76 -0
- package/dist/src/ui/tui/markdown-render.js +81 -73
- package/dist/src/ui/tui/shell-input.js +227 -67
- 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 +20 -8
- package/dist/src/markdown-renderer.js +0 -112
- package/dist/src/project-index.js +0 -221
- 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,11 @@
|
|
|
1
1
|
import { agentModeLabel } from '../../agent-mode.js';
|
|
2
|
-
import { truncate } from '../../utils.js';
|
|
2
|
+
import { singleLinePreview, truncate } from '../../utils.js';
|
|
3
3
|
import { formatClientTokenUsage } from '../repl.js';
|
|
4
4
|
import { renderFormattedBodyLines, renderPreformattedBodyLines, } from './markdown-render.js';
|
|
5
|
-
import { line, plainLine, span, wrapText } from './text.js';
|
|
5
|
+
import { displayWidth, line, padToWidth, plainLine, sliceToWidth, span, wrapText, } from './text.js';
|
|
6
|
+
import { isDarkTerminalBackground, mutedColor, mutedStyle } from './terminal-theme.js';
|
|
7
|
+
import { buildUserInputOverlayLines, } from './user-input.js';
|
|
8
|
+
import { composerPromptLabel, frameContentWidth, splitComposerInput, } from './composer-layout.js';
|
|
6
9
|
const WORKING_CLOCK_ICON = '◷';
|
|
7
10
|
const BRAILLE_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴'];
|
|
8
11
|
const TODO_PANEL_MAX_ROWS = 12;
|
|
@@ -10,12 +13,27 @@ const TODO_IN_PROGRESS_COLOR = 'ansi256(214)';
|
|
|
10
13
|
const COMMAND_PREVIEW_LINES = 10;
|
|
11
14
|
const WORKING_TOOL_PREVIEW_ROWS = 3;
|
|
12
15
|
const TRANSCRIPT_DIFF_PREVIEW_LINES = 24;
|
|
16
|
+
const APPROVAL_PREVIEW_MAX_ROWS = 20;
|
|
17
|
+
const APPROVAL_PREVIEW_MIN_ROWS = 4;
|
|
18
|
+
const APPROVAL_OVERLAY_CHROME_ROWS = 11;
|
|
19
|
+
const APPROVAL_PADDING_CHROME_ROWS = 4;
|
|
20
|
+
const APPROVAL_PADDING_MIN_ROWS = 30;
|
|
21
|
+
const APPROVAL_SCROLLBAR_COLUMNS = 2;
|
|
22
|
+
const APPROVAL_ACCENT_COLOR = 'cyan';
|
|
23
|
+
const SUDO_PASSWORD_ASSURANCE = 'The model never sees this. Your password goes straight to sudo on this ' +
|
|
24
|
+
'computer, then is discarded — never sent to our servers, saved, or logged.';
|
|
25
|
+
const SUDO_ASSURANCE_COLOR = 'ansi256(248)';
|
|
26
|
+
const APPROVAL_SCROLL_STATUS_COLOR = 'ansi256(248)';
|
|
27
|
+
const APPROVAL_SCROLLBAR_THUMB = '█';
|
|
28
|
+
const APPROVAL_SCROLLBAR_TRACK = '░';
|
|
13
29
|
const THINKING_NOTE_PREVIEW_ROWS = 3;
|
|
14
30
|
const COMPOSER_INPUT_MAX_ROWS = 6;
|
|
15
31
|
const AGENT_MODE_LABEL_WIDTH = 16;
|
|
16
32
|
const OVERLAY_PANEL_MAX_WIDTH = 86;
|
|
17
33
|
const OVERLAY_PANEL_MARGIN_LINES = 2;
|
|
34
|
+
const OVERLAY_PANEL_MARGIN_MIN_ROWS = 30;
|
|
18
35
|
const OVERLAY_BORDER_COLOR = 'yellow';
|
|
36
|
+
const USER_INPUT_BORDER_COLOR = 'cyan';
|
|
19
37
|
const OVERLAY_WARNING_COLOR = 'ansi256(208)';
|
|
20
38
|
const MODEL_PICKER_PANEL_MAX_WIDTH = 144;
|
|
21
39
|
const MODEL_PICKER_PANEL_MARGIN_LINES = 2;
|
|
@@ -23,75 +41,74 @@ const MODEL_PICKER_BORDER_COLOR = 'cyan';
|
|
|
23
41
|
const MODEL_PICKER_ACCENT_COLOR = 'cyan';
|
|
24
42
|
const MODEL_PICKER_HIGHLIGHT_BG = 'ansi256(87)';
|
|
25
43
|
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;
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
cursorRow = rows.length - 1;
|
|
44
|
-
cursorCol = current.length;
|
|
44
|
+
const COMPOSER_MARKER_PATTERN = /\[Image #\d+\]|\[Pasted Text: [^\]\n]*\]/g;
|
|
45
|
+
function composerMarkerMask(text) {
|
|
46
|
+
const mask = new Array(text.length).fill(false);
|
|
47
|
+
for (const match of text.matchAll(COMPOSER_MARKER_PATTERN)) {
|
|
48
|
+
const start = match.index ?? 0;
|
|
49
|
+
for (let offset = start; offset < start + match[0].length; offset += 1) {
|
|
50
|
+
mask[offset] = true;
|
|
45
51
|
}
|
|
46
|
-
|
|
52
|
+
}
|
|
53
|
+
return mask;
|
|
54
|
+
}
|
|
55
|
+
function composerRowSpans(text, mask, cursorCol) {
|
|
56
|
+
const spans = [];
|
|
57
|
+
const push = (value, marker, cursor) => {
|
|
58
|
+
if (!value)
|
|
59
|
+
return;
|
|
60
|
+
spans.push(span(value, {
|
|
61
|
+
...(marker ? { color: 'cyan' } : {}),
|
|
62
|
+
...(cursor ? { inverse: true } : {}),
|
|
63
|
+
}));
|
|
47
64
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
let cut = 0;
|
|
66
|
+
const flushTo = (end) => {
|
|
67
|
+
while (cut < end) {
|
|
68
|
+
const marker = mask[cut] ?? false;
|
|
69
|
+
let runEnd = cut + 1;
|
|
70
|
+
while (runEnd < end && (mask[runEnd] ?? false) === marker)
|
|
71
|
+
runEnd += 1;
|
|
72
|
+
push(text.slice(cut, runEnd), marker, false);
|
|
73
|
+
cut = runEnd;
|
|
54
74
|
}
|
|
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();
|
|
75
|
+
};
|
|
76
|
+
if (cursorCol === null) {
|
|
77
|
+
flushTo(text.length);
|
|
78
|
+
return spans;
|
|
68
79
|
}
|
|
69
|
-
|
|
80
|
+
flushTo(Math.min(cursorCol, text.length));
|
|
81
|
+
push(text[cursorCol] ?? ' ', mask[cursorCol] ?? false, true);
|
|
82
|
+
cut = Math.min(cursorCol + 1, text.length);
|
|
83
|
+
flushTo(text.length);
|
|
84
|
+
return spans;
|
|
70
85
|
}
|
|
71
86
|
function buildComposerInputLines(input, cursor, promptLabel, placeholder, width) {
|
|
72
87
|
if (!input) {
|
|
73
88
|
return [
|
|
74
|
-
line(span(promptLabel, { color: 'cyan' }), span(' ', { inverse: true }), span(placeholder, { color:
|
|
89
|
+
line(span(promptLabel, { color: 'cyan' }), span(' ', { inverse: true }), span(placeholder, { color: mutedColor() })),
|
|
75
90
|
];
|
|
76
91
|
}
|
|
77
92
|
const labelWidth = promptLabel.length;
|
|
78
93
|
const inputWidth = Math.max(1, width - labelWidth);
|
|
79
94
|
const { cursorCol, cursorRow, rows } = splitComposerInput(input, cursor, inputWidth);
|
|
80
95
|
const firstRow = Math.min(Math.max(cursorRow - COMPOSER_INPUT_MAX_ROWS + 1, 0), Math.max(rows.length - COMPOSER_INPUT_MAX_ROWS, 0));
|
|
96
|
+
const rowOffsets = [];
|
|
97
|
+
let consumed = 0;
|
|
98
|
+
for (const row of rows) {
|
|
99
|
+
rowOffsets.push(consumed);
|
|
100
|
+
consumed += row.length;
|
|
101
|
+
}
|
|
102
|
+
const mask = composerMarkerMask(rows.join(''));
|
|
81
103
|
return rows
|
|
82
104
|
.slice(firstRow, firstRow + COMPOSER_INPUT_MAX_ROWS)
|
|
83
105
|
.map((text, index) => {
|
|
84
106
|
const absoluteRow = firstRow + index;
|
|
107
|
+
const rowStart = rowOffsets[absoluteRow] ?? 0;
|
|
85
108
|
const label = index === 0
|
|
86
109
|
? span(promptLabel, { color: 'cyan' })
|
|
87
110
|
: 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));
|
|
111
|
+
return line(label, ...composerRowSpans(text, mask.slice(rowStart, rowStart + text.length), absoluteRow === cursorRow ? cursorCol : null));
|
|
95
112
|
});
|
|
96
113
|
}
|
|
97
114
|
function getEntryColor(kind) {
|
|
@@ -133,14 +150,14 @@ function diffLinePrefix(kind) {
|
|
|
133
150
|
return ' ';
|
|
134
151
|
}
|
|
135
152
|
}
|
|
136
|
-
function fitLine(content, maxWidth) {
|
|
153
|
+
export function fitLine(content, maxWidth) {
|
|
137
154
|
if (maxWidth <= 0)
|
|
138
155
|
return '';
|
|
139
|
-
if (content
|
|
156
|
+
if (displayWidth(content) <= maxWidth)
|
|
140
157
|
return content;
|
|
141
158
|
if (maxWidth === 1)
|
|
142
159
|
return '…';
|
|
143
|
-
return `${content
|
|
160
|
+
return `${sliceToWidth(content, maxWidth - 1)}…`;
|
|
144
161
|
}
|
|
145
162
|
export function formatPromptDirectoryLabel(projectRoot, homeDir = process.env.HOME ?? '') {
|
|
146
163
|
const trimmed = String(projectRoot ?? '').trim();
|
|
@@ -163,7 +180,9 @@ const CLIENT_SLASH_COMMANDS = [
|
|
|
163
180
|
{ command: '/model', description: 'Switch the active model' },
|
|
164
181
|
{ command: '/resume', description: 'Open the session picker to resume a previous session' },
|
|
165
182
|
{ command: '/jobs', description: 'Background jobs: pick to view output or kill' },
|
|
166
|
-
{ command: '/
|
|
183
|
+
{ command: '/report', description: 'Report a bug privately on the TheGitAI website' },
|
|
184
|
+
{ command: '/new', description: 'start a new conversation; this session remains saved' },
|
|
185
|
+
{ command: '/logout', description: 'Sign out and quit' },
|
|
167
186
|
{ command: '/exit', description: 'Quit the current session' },
|
|
168
187
|
];
|
|
169
188
|
function buildModelPickerOptions(currentModelId, serverModels) {
|
|
@@ -212,6 +231,9 @@ export function getSlashCommandSuggestions(input) {
|
|
|
212
231
|
function formatModelLabel(modelId, serverModels) {
|
|
213
232
|
return serverModels.find((model) => model.id === modelId)?.label ?? 'Unknown model';
|
|
214
233
|
}
|
|
234
|
+
function pickerModelLabel(modelId, serverModels) {
|
|
235
|
+
return formatModelLabel(modelId, serverModels).replace(/\s*\([^)]*\)\s*$/, '');
|
|
236
|
+
}
|
|
215
237
|
function filterResumeSessions(sessions, filter, serverModels) {
|
|
216
238
|
const q = filter.trim().toLowerCase();
|
|
217
239
|
if (!q)
|
|
@@ -235,26 +257,90 @@ function formatRelativeTime(isoDate) {
|
|
|
235
257
|
return `${hours}h ago`;
|
|
236
258
|
return `${Math.floor(hours / 24)}d ago`;
|
|
237
259
|
}
|
|
238
|
-
|
|
260
|
+
const TODO_IN_PROGRESS_FRAMES = [
|
|
261
|
+
'\u25CB',
|
|
262
|
+
'\u25D4',
|
|
263
|
+
'\u25D1',
|
|
264
|
+
'\u25D5',
|
|
265
|
+
'\u25CF',
|
|
266
|
+
];
|
|
267
|
+
const TODO_IN_PROGRESS_STATIC_GLYPH = '\u25D1';
|
|
268
|
+
export const TODO_IN_PROGRESS_STEP_MS = 1_500;
|
|
269
|
+
export function todoProgressTick(nowMs) {
|
|
270
|
+
return Math.floor(nowMs / TODO_IN_PROGRESS_STEP_MS);
|
|
271
|
+
}
|
|
272
|
+
function todoInProgressGlyph(progressTick) {
|
|
273
|
+
if (progressTick === null)
|
|
274
|
+
return TODO_IN_PROGRESS_STATIC_GLYPH;
|
|
275
|
+
const index = ((progressTick % TODO_IN_PROGRESS_FRAMES.length) +
|
|
276
|
+
TODO_IN_PROGRESS_FRAMES.length) %
|
|
277
|
+
TODO_IN_PROGRESS_FRAMES.length;
|
|
278
|
+
return TODO_IN_PROGRESS_FRAMES[index];
|
|
279
|
+
}
|
|
280
|
+
function todoItemLine(item, width, progressTick = null) {
|
|
239
281
|
const text = fitLine(item.text, Math.max(8, width - 5));
|
|
240
282
|
if (item.status === 'completed') {
|
|
241
|
-
return line(span(' ✔ ', { color: 'green' }), span(text,
|
|
283
|
+
return line(span(' ✔ ', { color: 'green' }), span(text, mutedStyle()));
|
|
242
284
|
}
|
|
243
285
|
if (item.status === 'in_progress') {
|
|
244
|
-
return line(span(
|
|
286
|
+
return line(span(` ${todoInProgressGlyph(progressTick)} `, {
|
|
287
|
+
color: TODO_IN_PROGRESS_COLOR,
|
|
288
|
+
}), span(text, { color: TODO_IN_PROGRESS_COLOR, bold: true }));
|
|
289
|
+
}
|
|
290
|
+
return line(span(' ○ ', { color: mutedColor() }), isDarkTerminalBackground()
|
|
291
|
+
? span(text, { color: mutedColor() })
|
|
292
|
+
: span(text));
|
|
293
|
+
}
|
|
294
|
+
const TURN_MESSAGE_PANEL_MAX_ROWS = 6;
|
|
295
|
+
const TURN_MESSAGE_LABEL = {
|
|
296
|
+
queued: 'queued',
|
|
297
|
+
sending: 'Processing . . .',
|
|
298
|
+
delivered: 'delivered',
|
|
299
|
+
};
|
|
300
|
+
function turnMessageLine(message, width, progressTick) {
|
|
301
|
+
const DECORATION = 6;
|
|
302
|
+
const budget = Math.max(1, width - DECORATION);
|
|
303
|
+
const preferredLabel = Math.max(...Object.values(TURN_MESSAGE_LABEL).map((value) => displayWidth(value)));
|
|
304
|
+
const labelWidth = Math.min(Math.max(1, Math.floor(budget / 2)), Math.max(preferredLabel, displayWidth(message.note ?? '')));
|
|
305
|
+
const label = fitLine(message.note ?? TURN_MESSAGE_LABEL[message.state], labelWidth);
|
|
306
|
+
const textWidth = Math.max(1, budget - labelWidth);
|
|
307
|
+
const text = padToWidth(fitLine(`"${message.text}"`, textWidth), textWidth);
|
|
308
|
+
if (message.state === 'delivered') {
|
|
309
|
+
return line(span(' ● ', { color: 'green' }), span(text, { color: 'gray', dim: true }), span(` ${label}`, { color: 'green' }));
|
|
310
|
+
}
|
|
311
|
+
if (message.state === 'sending') {
|
|
312
|
+
return line(span(` ${todoInProgressGlyph(progressTick)} `, {
|
|
313
|
+
color: TODO_IN_PROGRESS_COLOR,
|
|
314
|
+
}), span(text, { color: TODO_IN_PROGRESS_COLOR, bold: true }), span(` ${label}`, { color: TODO_IN_PROGRESS_COLOR }));
|
|
315
|
+
}
|
|
316
|
+
return line(span(' ○ ', { color: 'cyan' }), span(text, { color: 'cyan' }), span(` ${label}`, { color: 'gray' }));
|
|
317
|
+
}
|
|
318
|
+
export function buildTurnMessageLines(messages, width, progressTick = null) {
|
|
319
|
+
if (messages.length === 0)
|
|
320
|
+
return [];
|
|
321
|
+
const lines = [
|
|
322
|
+
line(span('Your messages', { color: 'cyan', bold: true })),
|
|
323
|
+
];
|
|
324
|
+
const visible = messages.slice(-(TURN_MESSAGE_PANEL_MAX_ROWS - 1));
|
|
325
|
+
const hidden = messages.length - visible.length;
|
|
326
|
+
if (hidden > 0) {
|
|
327
|
+
lines.push(line(span(` ● ${hidden} earlier`, { color: 'gray', dim: true })));
|
|
328
|
+
}
|
|
329
|
+
for (const message of visible) {
|
|
330
|
+
lines.push(turnMessageLine(message, width, progressTick));
|
|
245
331
|
}
|
|
246
|
-
return
|
|
332
|
+
return lines;
|
|
247
333
|
}
|
|
248
334
|
export function formatTodoProgress(items) {
|
|
249
335
|
const done = items.filter((item) => item.status === 'completed').length;
|
|
250
336
|
return `${done}/${items.length} done`;
|
|
251
337
|
}
|
|
252
|
-
export function renderTodoListLines(items, width, { header = false } = {}) {
|
|
338
|
+
export function renderTodoListLines(items, width, { header = false, progressTick = null } = {}) {
|
|
253
339
|
if (items.length === 0)
|
|
254
340
|
return [];
|
|
255
341
|
const lines = [];
|
|
256
342
|
if (header) {
|
|
257
|
-
lines.push(line(span('To-dos', { color: 'cyan', bold: true }), span(` · ${formatTodoProgress(items)}`, { color:
|
|
343
|
+
lines.push(line(span('To-dos', { color: 'cyan', bold: true }), span(` · ${formatTodoProgress(items)}`, { color: mutedColor() })));
|
|
258
344
|
}
|
|
259
345
|
const itemRowBudget = TODO_PANEL_MAX_ROWS - (header ? 1 : 0);
|
|
260
346
|
let collapsedDone = 0;
|
|
@@ -268,19 +354,16 @@ export function renderTodoListLines(items, width, { header = false } = {}) {
|
|
|
268
354
|
collapsedDone = Math.min(over, leadingDone);
|
|
269
355
|
}
|
|
270
356
|
if (collapsedDone > 0) {
|
|
271
|
-
lines.push(line(span(' ✔ ', { color: 'green' }), span(`${collapsedDone} completed`,
|
|
357
|
+
lines.push(line(span(' ✔ ', { color: 'green' }), span(`${collapsedDone} completed`, mutedStyle())));
|
|
272
358
|
}
|
|
273
359
|
const remaining = items.slice(collapsedDone);
|
|
274
360
|
const budget = itemRowBudget - (collapsedDone > 0 ? 1 : 0);
|
|
275
361
|
const visible = remaining.length > budget ? remaining.slice(0, budget - 1) : remaining;
|
|
276
362
|
for (const item of visible) {
|
|
277
|
-
lines.push(todoItemLine(item, width));
|
|
363
|
+
lines.push(todoItemLine(item, width, progressTick));
|
|
278
364
|
}
|
|
279
365
|
if (remaining.length > visible.length) {
|
|
280
|
-
lines.push(plainLine(` … +${remaining.length - visible.length} more`,
|
|
281
|
-
color: 'gray',
|
|
282
|
-
dim: true,
|
|
283
|
-
}));
|
|
366
|
+
lines.push(plainLine(` … +${remaining.length - visible.length} more`, mutedStyle()));
|
|
284
367
|
}
|
|
285
368
|
return lines;
|
|
286
369
|
}
|
|
@@ -293,20 +376,45 @@ export function renderTranscriptEntryLines(entry, width) {
|
|
|
293
376
|
? renderPreformattedBodyLines(entry.body, width, entry.kind)
|
|
294
377
|
: renderFormattedBodyLines(entry.body, width, entry.kind)));
|
|
295
378
|
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
|
-
}
|
|
379
|
+
lines.push(...renderDiffPreviewLines(entry.diffPreview, width));
|
|
302
380
|
}
|
|
303
381
|
if (entry.todoList && entry.todoList.length > 0) {
|
|
304
382
|
lines.push(...renderTodoListLines(entry.todoList, width));
|
|
305
383
|
}
|
|
306
384
|
return lines;
|
|
307
385
|
}
|
|
386
|
+
const transcriptEntryLineCache = new WeakMap();
|
|
387
|
+
const MAX_CACHED_TRANSCRIPT_WIDTHS = 2;
|
|
388
|
+
function cachedTranscriptEntryLines(entry, width) {
|
|
389
|
+
let byWidth = transcriptEntryLineCache.get(entry);
|
|
390
|
+
if (!byWidth) {
|
|
391
|
+
byWidth = new Map();
|
|
392
|
+
transcriptEntryLineCache.set(entry, byWidth);
|
|
393
|
+
}
|
|
394
|
+
const cached = byWidth.get(width);
|
|
395
|
+
if (cached)
|
|
396
|
+
return cached;
|
|
397
|
+
const lines = renderTranscriptEntryLines(entry, width);
|
|
398
|
+
if (byWidth.size >= MAX_CACHED_TRANSCRIPT_WIDTHS) {
|
|
399
|
+
const oldest = byWidth.keys().next().value;
|
|
400
|
+
if (oldest !== undefined)
|
|
401
|
+
byWidth.delete(oldest);
|
|
402
|
+
}
|
|
403
|
+
byWidth.set(width, lines);
|
|
404
|
+
return lines;
|
|
405
|
+
}
|
|
406
|
+
function renderDiffPreviewLines(preview, width, maxDiffLines = TRANSCRIPT_DIFF_PREVIEW_LINES) {
|
|
407
|
+
return [
|
|
408
|
+
plainLine(` Added ${preview.added} line${preview.added === 1 ? '' : 's'}, removed ${preview.removed} line${preview.removed === 1 ? '' : 's'}`, { color: 'gray' }),
|
|
409
|
+
...preview.lines.slice(0, maxDiffLines).map((diffLine) => line(span(`${diffLinePrefix(diffLine.kind)} `, {
|
|
410
|
+
color: diffLineColor(diffLine.kind),
|
|
411
|
+
}), span(fitLine(diffLine.content || ' ', width - 4), {
|
|
412
|
+
color: diffLineColor(diffLine.kind),
|
|
413
|
+
}))),
|
|
414
|
+
];
|
|
415
|
+
}
|
|
308
416
|
function tokenUsageLines(usage) {
|
|
309
|
-
const match = usage.match(/^Session tokens • in ([^•]+) • out ([^•]+)(?: • think ([^•]+))? • cache ([^•]+)(?: • write ([^•]+))
|
|
417
|
+
const match = usage.match(/^Session tokens • in ([^•]+) • out ([^•]+)(?: • think ([^•]+))? • cache ([^•]+)(?: • write ([^•]+))?$/);
|
|
310
418
|
if (!match) {
|
|
311
419
|
return [plainLine(usage, { color: 'cyan' })];
|
|
312
420
|
}
|
|
@@ -324,9 +432,6 @@ function tokenUsageLines(usage) {
|
|
|
324
432
|
if (match[5]) {
|
|
325
433
|
spans.push(span(' Write ', { color: 'gray' }), span(match[5].trim(), { color: 'yellow' }));
|
|
326
434
|
}
|
|
327
|
-
if (match[6]) {
|
|
328
|
-
spans.push(span(' Index ', { color: 'gray' }), span(match[6].trim(), { color: 'blue' }));
|
|
329
|
-
}
|
|
330
435
|
return [line(...spans)];
|
|
331
436
|
}
|
|
332
437
|
function footerTransientStatus(status) {
|
|
@@ -349,10 +454,11 @@ function backgroundJobIndicatorSpans(state) {
|
|
|
349
454
|
if (runningCount === 0)
|
|
350
455
|
return [];
|
|
351
456
|
const noun = runningCount === 1 ? 'shell' : 'shells';
|
|
457
|
+
const hint = state.busy ? ' · ctrl+b' : ' · /jobs';
|
|
352
458
|
return [
|
|
353
459
|
span(' ', { color: 'gray', dim: true }),
|
|
354
460
|
span(`● ${runningCount} ${noun} running`, { color: 'green', bold: true }),
|
|
355
|
-
span(
|
|
461
|
+
span(hint, { color: 'gray', dim: true }),
|
|
356
462
|
];
|
|
357
463
|
}
|
|
358
464
|
function todoIndicatorSpans(state) {
|
|
@@ -405,20 +511,26 @@ function composerFooterLines(state) {
|
|
|
405
511
|
}));
|
|
406
512
|
return lines;
|
|
407
513
|
}
|
|
408
|
-
const
|
|
409
|
-
?
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
514
|
+
const busyHelperText = state.queuedMessage
|
|
515
|
+
? 'Enter sends it to the agent • ↑ edit • Esc / Ctrl+C discard'
|
|
516
|
+
: state.input
|
|
517
|
+
? 'Enter queues • Esc cancels turn • Ctrl+C clears draft'
|
|
518
|
+
: 'Enter queues • Esc / Ctrl+C cancel turn';
|
|
519
|
+
const helperText = state.userInputPrompt
|
|
520
|
+
? 'Answering agent questions • Ctrl+C cancels turn'
|
|
521
|
+
: state.busy
|
|
522
|
+
? busyHelperText
|
|
523
|
+
: process.platform === 'win32'
|
|
524
|
+
? 'Enter sends • Shift+Tab mode • Alt+V image • Esc cancel turn • Ctrl+C clears / quits'
|
|
525
|
+
: process.platform === 'darwin'
|
|
526
|
+
? 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C clears / quits'
|
|
527
|
+
: 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C clears / quits';
|
|
417
528
|
const agentLabel = agentModeLabel(state.agentMode).padEnd(AGENT_MODE_LABEL_WIDTH);
|
|
418
529
|
const tokenUsageText = state.tokenUsage || formatClientTokenUsage(null);
|
|
530
|
+
const footerMuted = mutedStyle();
|
|
419
531
|
const footerSpans = [
|
|
420
|
-
span(helperText,
|
|
421
|
-
span(' '.repeat(Math.max(1, 8)),
|
|
532
|
+
span(helperText, footerMuted),
|
|
533
|
+
span(' '.repeat(Math.max(1, 8)), footerMuted),
|
|
422
534
|
span(agentLabel, {
|
|
423
535
|
color: state.agentMode === 'plan' ? 'yellow' : 'cyan',
|
|
424
536
|
}),
|
|
@@ -468,15 +580,29 @@ function jobStatusDescriptor(job, nowMs) {
|
|
|
468
580
|
? { glyph: '✓', color: 'green', text: `exited (0) · ran ${ran}` }
|
|
469
581
|
: { glyph: '✖', color: 'red', text: `exited (${job.exitCode ?? 1}) · ran ${ran}` };
|
|
470
582
|
}
|
|
471
|
-
|
|
583
|
+
const JOBS_PICKER_CHROME_ROWS = 2;
|
|
584
|
+
const JOBS_PICKER_MIN_ROWS = 8;
|
|
585
|
+
function buildJobsPickerLines(state, width, height, nowMs) {
|
|
472
586
|
const jobs = state.backgroundJobs ?? [];
|
|
587
|
+
const bodyBudget = Math.max(1, height - JOBS_PICKER_CHROME_ROWS);
|
|
473
588
|
const lines = [
|
|
474
589
|
plainLine('Background jobs', { color: 'cyan', bold: true }),
|
|
475
590
|
];
|
|
476
591
|
if (jobs.length === 0) {
|
|
477
592
|
lines.push(plainLine('No background jobs in this session.', { color: 'gray' }));
|
|
478
593
|
}
|
|
479
|
-
|
|
594
|
+
let start = 0;
|
|
595
|
+
let visible = jobs;
|
|
596
|
+
if (jobs.length > bodyBudget) {
|
|
597
|
+
start = Math.min(Math.max(0, state.jobsPickerIndex - Math.floor(bodyBudget / 2)), jobs.length - bodyBudget);
|
|
598
|
+
visible = jobs.slice(start, start + bodyBudget);
|
|
599
|
+
if (start > 0) {
|
|
600
|
+
lines.push(plainLine(` … ${start} newer`, { color: 'gray', dim: true }));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const detailBudget = Math.max(0, bodyBudget - visible.length - (start > 0 ? 1 : 0));
|
|
604
|
+
for (const [offset, job] of visible.entries()) {
|
|
605
|
+
const index = start + offset;
|
|
480
606
|
const selected = index === state.jobsPickerIndex;
|
|
481
607
|
const expanded = state.jobsPickerExpandedId === job.id;
|
|
482
608
|
const { glyph, color, text } = jobStatusDescriptor(job, nowMs);
|
|
@@ -485,10 +611,11 @@ function buildJobsPickerLines(state, width, nowMs) {
|
|
|
485
611
|
bold: selected,
|
|
486
612
|
}), span(` ${text}`, { color: 'gray' })));
|
|
487
613
|
if (expanded) {
|
|
488
|
-
const
|
|
614
|
+
const allDetail = (job.detailLines ?? []).length
|
|
489
615
|
? job.detailLines ?? []
|
|
490
616
|
: backgroundJobPreviewLines(job, 3);
|
|
491
|
-
|
|
617
|
+
const detailLines = allDetail.slice(-detailBudget);
|
|
618
|
+
if (detailBudget > 0 && detailLines.length === 0) {
|
|
492
619
|
lines.push(plainLine(' (no output captured)', { color: 'gray' }));
|
|
493
620
|
}
|
|
494
621
|
else {
|
|
@@ -501,22 +628,57 @@ function buildJobsPickerLines(state, width, nowMs) {
|
|
|
501
628
|
}
|
|
502
629
|
}
|
|
503
630
|
}
|
|
631
|
+
const remaining = jobs.length - (start + visible.length);
|
|
632
|
+
if (remaining > 0) {
|
|
633
|
+
lines.push(plainLine(` … ${remaining} older`, { color: 'gray', dim: true }));
|
|
634
|
+
}
|
|
504
635
|
lines.push(plainLine('↑/↓ move enter expand/collapse k kill esc close', {
|
|
505
636
|
color: 'gray',
|
|
506
637
|
}));
|
|
507
638
|
return lines;
|
|
508
639
|
}
|
|
640
|
+
export const THINKING_FALLBACK_PHRASES = [
|
|
641
|
+
'Working out where to start...',
|
|
642
|
+
'Deciding what comes first...',
|
|
643
|
+
'Lining up the pieces...',
|
|
644
|
+
'Untangling the details...',
|
|
645
|
+
'Deciding what not to touch...',
|
|
646
|
+
'Checking what this would break...',
|
|
647
|
+
'Choosing the smaller change...',
|
|
648
|
+
'Picking the least clever option...',
|
|
649
|
+
'Resisting the obvious answer...',
|
|
650
|
+
'Trying the boring explanation first...',
|
|
651
|
+
'Checking whether the assumption holds...',
|
|
652
|
+
'Asking what would have to be true...',
|
|
653
|
+
'Reading it the way the machine would...',
|
|
654
|
+
'Testing the story against the code...',
|
|
655
|
+
'Working out what actually changed...',
|
|
656
|
+
'Finding the smallest thing that explains it...',
|
|
657
|
+
'Looking for the part that is not settled yet...',
|
|
658
|
+
'Making sure this is the simple version...',
|
|
659
|
+
];
|
|
660
|
+
export function pickThinkingFallbackPhrase() {
|
|
661
|
+
const index = Math.floor(Math.random() * THINKING_FALLBACK_PHRASES.length);
|
|
662
|
+
return THINKING_FALLBACK_PHRASES[index];
|
|
663
|
+
}
|
|
664
|
+
function withProgressEllipsis(title) {
|
|
665
|
+
const text = title.trim();
|
|
666
|
+
if (!text)
|
|
667
|
+
return text;
|
|
668
|
+
return /(?:\.\.\.|…)$/.test(text) ? text : `${text}...`;
|
|
669
|
+
}
|
|
509
670
|
function thinkingHeaderLine(spinnerFrame, title, width) {
|
|
510
671
|
const spinner = `${BRAILLE_SPINNER_FRAMES[spinnerFrame % BRAILLE_SPINNER_FRAMES.length]} `;
|
|
511
|
-
const
|
|
512
|
-
|
|
672
|
+
const decorated = withProgressEllipsis(title);
|
|
673
|
+
const fittedTitle = decorated
|
|
674
|
+
? fitLine(decorated, Math.max(8, width - spinner.length - 'Thinking'.length - 3))
|
|
513
675
|
: '';
|
|
514
676
|
return line(span(spinner, { color: 'green' }), span('Thinking', { color: 'green', bold: true }), ...(fittedTitle ? [span(` · ${fittedTitle}`, { color: 'ansi256(248)' })] : []));
|
|
515
677
|
}
|
|
516
678
|
function thinkingNoteLine(note, width) {
|
|
517
679
|
return line(span('│ ', { color: 'green' }), span(fitLine(note, Math.max(8, width - 3)), { color: 'ansi256(248)' }));
|
|
518
680
|
}
|
|
519
|
-
function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
681
|
+
function buildLiveLines(state, width, spinnerFrame, elapsedSeconds, nowMs) {
|
|
520
682
|
if (!state.busy)
|
|
521
683
|
return [];
|
|
522
684
|
const lines = [plainLine('')];
|
|
@@ -546,11 +708,19 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
546
708
|
}
|
|
547
709
|
lines.push(plainLine(''));
|
|
548
710
|
}
|
|
549
|
-
lines.push(plainLine(
|
|
711
|
+
lines.push(plainLine(buildWorkingClockLine(state, elapsedSeconds), { color: 'yellow' }));
|
|
712
|
+
const turnMessages = state.turnMessages ?? [];
|
|
713
|
+
if (turnMessages.length > 0) {
|
|
714
|
+
lines.push(plainLine(''));
|
|
715
|
+
lines.push(...buildTurnMessageLines(turnMessages, width, todoProgressTick(nowMs)));
|
|
716
|
+
}
|
|
550
717
|
const todos = state.todos ?? [];
|
|
551
718
|
if (todos.length > 0) {
|
|
552
719
|
lines.push(plainLine(''));
|
|
553
|
-
lines.push(...renderTodoListLines(todos, width, {
|
|
720
|
+
lines.push(...renderTodoListLines(todos, width, {
|
|
721
|
+
header: true,
|
|
722
|
+
progressTick: todoProgressTick(nowMs),
|
|
723
|
+
}));
|
|
554
724
|
}
|
|
555
725
|
const visibleTitle = state.thinkingTitle.trim();
|
|
556
726
|
const visibleNotes = state.thinkingNotes.filter(Boolean);
|
|
@@ -560,11 +730,12 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
560
730
|
const minimalText = visibleTitle && visibleTitle !== 'Thinking'
|
|
561
731
|
? visibleTitle
|
|
562
732
|
: (visibleNotes[visibleNotes.length - 1] ?? '');
|
|
563
|
-
lines.push(thinkingHeaderLine(spinnerFrame, minimalText, width));
|
|
733
|
+
lines.push(thinkingHeaderLine(spinnerFrame, minimalText || THINKING_FALLBACK_PHRASES[0], width));
|
|
564
734
|
}
|
|
565
735
|
else {
|
|
566
|
-
|
|
567
|
-
|
|
736
|
+
const headerTitle = visibleTitle && visibleTitle !== 'Thinking' ? visibleTitle : '';
|
|
737
|
+
lines.push(thinkingHeaderLine(spinnerFrame, headerTitle || THINKING_FALLBACK_PHRASES[0], width));
|
|
738
|
+
for (const note of visibleNotes.slice(0, THINKING_NOTE_PREVIEW_ROWS)) {
|
|
568
739
|
lines.push(thinkingNoteLine(note, width));
|
|
569
740
|
}
|
|
570
741
|
}
|
|
@@ -573,16 +744,30 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
573
744
|
return lines;
|
|
574
745
|
}
|
|
575
746
|
function lineCharCount(row) {
|
|
576
|
-
return row.spans.reduce((total, item) => total +
|
|
747
|
+
return row.spans.reduce((total, item) => total + displayWidth(item.text), 0);
|
|
577
748
|
}
|
|
578
749
|
function overlayPanelLine(row, width, color) {
|
|
579
750
|
const padding = Math.max(0, width - lineCharCount(row));
|
|
580
751
|
return line(span('│ ', { color }), ...row.spans, span(' '.repeat(padding)), span(' │', { color }));
|
|
581
752
|
}
|
|
582
|
-
function
|
|
753
|
+
function overlayPanelMarginLineCount(height) {
|
|
754
|
+
return height < OVERLAY_PANEL_MARGIN_MIN_ROWS
|
|
755
|
+
? 0
|
|
756
|
+
: OVERLAY_PANEL_MARGIN_LINES;
|
|
757
|
+
}
|
|
758
|
+
function overlayPanelContentBudget(height) {
|
|
759
|
+
if (!Number.isFinite(height)) {
|
|
760
|
+
return Number.POSITIVE_INFINITY;
|
|
761
|
+
}
|
|
762
|
+
const margins = overlayPanelMarginLineCount(height) * 2;
|
|
763
|
+
return Math.max(1, Math.floor(height) - margins - 2);
|
|
764
|
+
}
|
|
765
|
+
function buildOverlayPanel(rows, width, color, height = Number.POSITIVE_INFINITY) {
|
|
583
766
|
const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
|
|
584
767
|
const innerWidth = Math.max(1, panelWidth - 4);
|
|
585
|
-
const margin = Array.from({
|
|
768
|
+
const margin = Array.from({
|
|
769
|
+
length: overlayPanelMarginLineCount(height),
|
|
770
|
+
}, () => plainLine(''));
|
|
586
771
|
return [
|
|
587
772
|
...margin,
|
|
588
773
|
plainLine(`╭${'─'.repeat(panelWidth - 2)}╮`, { color }),
|
|
@@ -603,7 +788,6 @@ function modelPickerPanelSideLine(content, innerWidth) {
|
|
|
603
788
|
}
|
|
604
789
|
const MODEL_PICKER_COST_WIDTH = 6;
|
|
605
790
|
const MODEL_PICKER_MODEL_WIDTH = 42;
|
|
606
|
-
const MODEL_PICKER_NUMBER_WIDTH = 4;
|
|
607
791
|
const MODEL_PICKER_SEPARATOR = ' │ ';
|
|
608
792
|
const MODEL_PICKER_WIDE_MIN_WIDTH = 78;
|
|
609
793
|
function modelPickerTopBorder(panelWidth) {
|
|
@@ -630,7 +814,6 @@ function modelPickerCostText(rating) {
|
|
|
630
814
|
}
|
|
631
815
|
function modelPickerNotesWidth(innerWidth) {
|
|
632
816
|
return Math.max(18, innerWidth -
|
|
633
|
-
MODEL_PICKER_NUMBER_WIDTH -
|
|
634
817
|
MODEL_PICKER_MODEL_WIDTH -
|
|
635
818
|
MODEL_PICKER_COST_WIDTH -
|
|
636
819
|
MODEL_PICKER_SEPARATOR.length * 2);
|
|
@@ -653,12 +836,10 @@ function modelPickerItemLines(option, selected, innerWidth) {
|
|
|
653
836
|
: selected
|
|
654
837
|
? { color: 'cyan', bold: true }
|
|
655
838
|
: {};
|
|
656
|
-
const numberCell = modelPickerCell(String(option.publicId), MODEL_PICKER_NUMBER_WIDTH, { color: 'cyan', bold: selected, ...selectedStyle });
|
|
657
839
|
const cost = modelPickerCostText(option.costRating);
|
|
658
840
|
if (innerWidth < MODEL_PICKER_WIDE_MIN_WIDTH) {
|
|
659
|
-
const modelWidth = Math.max(12, innerWidth -
|
|
841
|
+
const modelWidth = Math.max(12, innerWidth - MODEL_PICKER_COST_WIDTH - 2);
|
|
660
842
|
const row = [
|
|
661
|
-
numberCell,
|
|
662
843
|
...modelPickerModelSpans(option, selected, modelWidth, false, labelStyle, selectedStyle),
|
|
663
844
|
span(' ', selectedStyle),
|
|
664
845
|
modelPickerCell(cost, MODEL_PICKER_COST_WIDTH, selectedStyle),
|
|
@@ -668,7 +849,6 @@ function modelPickerItemLines(option, selected, innerWidth) {
|
|
|
668
849
|
];
|
|
669
850
|
}
|
|
670
851
|
const row = [
|
|
671
|
-
numberCell,
|
|
672
852
|
...modelPickerModelSpans(option, selected, MODEL_PICKER_MODEL_WIDTH, true, labelStyle, selectedStyle),
|
|
673
853
|
span(MODEL_PICKER_SEPARATOR, { color: 'gray', ...selectedStyle }),
|
|
674
854
|
modelPickerCell(cost, MODEL_PICKER_COST_WIDTH, selectedStyle),
|
|
@@ -685,9 +865,9 @@ function modelPickerItemLines(option, selected, innerWidth) {
|
|
|
685
865
|
function modelPickerHeaderLine(innerWidth) {
|
|
686
866
|
const heading = { color: MODEL_PICKER_ACCENT_COLOR, bold: true };
|
|
687
867
|
if (innerWidth < MODEL_PICKER_WIDE_MIN_WIDTH) {
|
|
688
|
-
return line(modelPickerCell('
|
|
868
|
+
return line(modelPickerCell('Model', Math.max(1, innerWidth), heading));
|
|
689
869
|
}
|
|
690
|
-
return line(modelPickerCell('
|
|
870
|
+
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
871
|
}
|
|
692
872
|
function buildModelPickerPanel(options, selectedIndex, width, availableHeight) {
|
|
693
873
|
const panelWidth = Math.max(28, Math.min(width, MODEL_PICKER_PANEL_MAX_WIDTH));
|
|
@@ -782,10 +962,105 @@ function buildCommandPalettePanel(suggestions, selectedIndex, width) {
|
|
|
782
962
|
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
963
|
return [...margin, ...body, ...margin];
|
|
784
964
|
}
|
|
965
|
+
export function formatElapsedClock(elapsedSeconds) {
|
|
966
|
+
if (elapsedSeconds < 60)
|
|
967
|
+
return `${elapsedSeconds}s`;
|
|
968
|
+
return `${Math.floor(elapsedSeconds / 60)}m ${String(elapsedSeconds % 60).padStart(2, '0')}s`;
|
|
969
|
+
}
|
|
970
|
+
export function buildWorkingClockLine(state, elapsedSeconds) {
|
|
971
|
+
const elapsed = formatElapsedClock(elapsedSeconds);
|
|
972
|
+
if (state.busyPausedAt != null) {
|
|
973
|
+
return `${WORKING_CLOCK_ICON} Paused · ${elapsed} · waiting for your response`;
|
|
974
|
+
}
|
|
975
|
+
const label = state.generatingImage
|
|
976
|
+
? 'Generating image'
|
|
977
|
+
: state.analyzingImages > 0
|
|
978
|
+
? state.analyzingImages > 1
|
|
979
|
+
? 'Analyzing images'
|
|
980
|
+
: 'Analyzing image'
|
|
981
|
+
: 'Working';
|
|
982
|
+
return `${WORKING_CLOCK_ICON} ${label} · ${elapsed}`;
|
|
983
|
+
}
|
|
984
|
+
export function approvalPanelInnerWidth(width) {
|
|
985
|
+
return Math.max(1, Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH)) - 4);
|
|
986
|
+
}
|
|
987
|
+
export function approvalPaddingEnabled(height) {
|
|
988
|
+
return height >= APPROVAL_PADDING_MIN_ROWS;
|
|
989
|
+
}
|
|
990
|
+
export function approvalPreviewBudget(height) {
|
|
991
|
+
const chrome = APPROVAL_OVERLAY_CHROME_ROWS +
|
|
992
|
+
(approvalPaddingEnabled(height) ? APPROVAL_PADDING_CHROME_ROWS : 0);
|
|
993
|
+
return Math.max(APPROVAL_PREVIEW_MIN_ROWS, Math.min(APPROVAL_PREVIEW_MAX_ROWS, Math.floor(height / 2) - chrome));
|
|
994
|
+
}
|
|
995
|
+
export function approvalPreviewRows(prompt, innerWidth) {
|
|
996
|
+
const previewWidth = Math.max(8, innerWidth - APPROVAL_SCROLLBAR_COLUMNS);
|
|
997
|
+
if (prompt.diffPreview) {
|
|
998
|
+
return renderDiffPreviewLines(prompt.diffPreview, previewWidth, prompt.diffPreview.lines.length);
|
|
999
|
+
}
|
|
1000
|
+
return wrapText(prompt.body, previewWidth).map((text) => plainLine(text, { color: OVERLAY_BORDER_COLOR }));
|
|
1001
|
+
}
|
|
1002
|
+
export function approvalScrollLimit(prompt, width, height) {
|
|
1003
|
+
if (!prompt)
|
|
1004
|
+
return 0;
|
|
1005
|
+
return Math.max(0, approvalPreviewRows(prompt, approvalPanelInnerWidth(width)).length -
|
|
1006
|
+
approvalPreviewBudget(height));
|
|
1007
|
+
}
|
|
1008
|
+
function approvalScrollbarGlyphs(totalRows, visibleRows, offset) {
|
|
1009
|
+
const thumbRows = Math.max(1, Math.min(visibleRows, Math.round((visibleRows * visibleRows) / totalRows)));
|
|
1010
|
+
const maxOffset = totalRows - visibleRows;
|
|
1011
|
+
const thumbTop = maxOffset <= 0
|
|
1012
|
+
? 0
|
|
1013
|
+
: Math.round((offset / maxOffset) * (visibleRows - thumbRows));
|
|
1014
|
+
return Array.from({ length: visibleRows }, (_, index) => index >= thumbTop && index < thumbTop + thumbRows
|
|
1015
|
+
? APPROVAL_SCROLLBAR_THUMB
|
|
1016
|
+
: APPROVAL_SCROLLBAR_TRACK);
|
|
1017
|
+
}
|
|
1018
|
+
function withScrollbarGlyph(target, glyph, column) {
|
|
1019
|
+
const padding = Math.max(1, column - lineCharCount(target));
|
|
1020
|
+
return {
|
|
1021
|
+
spans: [
|
|
1022
|
+
...target.spans,
|
|
1023
|
+
span(' '.repeat(padding)),
|
|
1024
|
+
span(glyph, {
|
|
1025
|
+
color: 'gray',
|
|
1026
|
+
dim: glyph === APPROVAL_SCROLLBAR_TRACK,
|
|
1027
|
+
}),
|
|
1028
|
+
],
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
export function buildApprovalPreviewWindow(prompt, innerWidth, height, requestedOffset) {
|
|
1032
|
+
const rows = approvalPreviewRows(prompt, innerWidth);
|
|
1033
|
+
const budget = approvalPreviewBudget(height);
|
|
1034
|
+
if (rows.length <= budget) {
|
|
1035
|
+
return {
|
|
1036
|
+
firstVisibleRow: rows.length === 0 ? 0 : 1,
|
|
1037
|
+
lastVisibleRow: rows.length,
|
|
1038
|
+
lines: rows,
|
|
1039
|
+
offset: 0,
|
|
1040
|
+
totalRows: rows.length,
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
const offset = Math.max(0, Math.min(requestedOffset, rows.length - budget));
|
|
1044
|
+
const visible = rows.slice(offset, offset + budget);
|
|
1045
|
+
const glyphs = approvalScrollbarGlyphs(rows.length, budget, offset);
|
|
1046
|
+
return {
|
|
1047
|
+
firstVisibleRow: offset + 1,
|
|
1048
|
+
lastVisibleRow: offset + visible.length,
|
|
1049
|
+
lines: visible.map((target, index) => withScrollbarGlyph(target, glyphs[index], innerWidth - 1)),
|
|
1050
|
+
offset,
|
|
1051
|
+
totalRows: rows.length,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
export function approvalScrollStatusLine(window) {
|
|
1055
|
+
return `lines ${window.firstVisibleRow}–${window.lastVisibleRow} of ${window.totalRows} · PgUp/PgDn scrolls`;
|
|
1056
|
+
}
|
|
1057
|
+
export function rightAlignedLine(text, width, style = {}) {
|
|
1058
|
+
return plainLine(`${' '.repeat(Math.max(0, width - displayWidth(text)))}${text}`, style);
|
|
1059
|
+
}
|
|
785
1060
|
function buildOverlayLines(state, width, height, nowMs) {
|
|
786
1061
|
const lines = [];
|
|
787
1062
|
const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
|
|
788
|
-
const innerWidth =
|
|
1063
|
+
const innerWidth = approvalPanelInnerWidth(width);
|
|
789
1064
|
if (state.sudoPrompt) {
|
|
790
1065
|
const prompt = state.sudoPrompt;
|
|
791
1066
|
const passwordWidth = Math.max(0, innerWidth - 11);
|
|
@@ -805,75 +1080,117 @@ function buildOverlayLines(state, width, height, nowMs) {
|
|
|
805
1080
|
: line(span(' '), span(text, { color: OVERLAY_BORDER_COLOR })));
|
|
806
1081
|
});
|
|
807
1082
|
lines.push(plainLine(''));
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
}
|
|
1083
|
+
lines.push(...wrapText(SUDO_PASSWORD_ASSURANCE, innerWidth).map((text) => plainLine(text, { color: SUDO_ASSURANCE_COLOR })));
|
|
1084
|
+
lines.push(plainLine(''));
|
|
811
1085
|
lines.push(line(span('Password: ', { color: 'cyan', bold: true }), span('•'.repeat(Math.min(prompt.passwordLength, passwordWidth)), {
|
|
812
1086
|
color: 'cyan',
|
|
813
1087
|
}), span(' ', { inverse: true })));
|
|
814
1088
|
lines.push(plainLine('Press Enter to submit, Escape to cancel', { color: 'gray' }));
|
|
815
|
-
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR);
|
|
1089
|
+
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR, height);
|
|
1090
|
+
}
|
|
1091
|
+
if (state.userInputPrompt) {
|
|
1092
|
+
if (height < 3) {
|
|
1093
|
+
return [];
|
|
1094
|
+
}
|
|
1095
|
+
return buildOverlayPanel(buildUserInputOverlayLines(state.userInputPrompt, innerWidth, overlayPanelContentBudget(height)), width, USER_INPUT_BORDER_COLOR, height);
|
|
816
1096
|
}
|
|
817
1097
|
if (state.approvalPrompt) {
|
|
818
1098
|
const prompt = state.approvalPrompt;
|
|
1099
|
+
const padded = approvalPaddingEnabled(height);
|
|
819
1100
|
lines.push(plainLine(prompt.title, {
|
|
820
|
-
color:
|
|
1101
|
+
color: APPROVAL_ACCENT_COLOR,
|
|
821
1102
|
bold: true,
|
|
822
1103
|
}));
|
|
1104
|
+
if (padded)
|
|
1105
|
+
lines.push(plainLine(''));
|
|
823
1106
|
if (prompt.diffPreview && prompt.filePath) {
|
|
824
1107
|
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
1108
|
}
|
|
832
|
-
|
|
833
|
-
|
|
1109
|
+
const previewWindow = buildApprovalPreviewWindow(prompt, innerWidth, height, state.approvalScrollOffset ?? 0);
|
|
1110
|
+
lines.push(...previewWindow.lines);
|
|
1111
|
+
if (padded)
|
|
1112
|
+
lines.push(plainLine(''));
|
|
1113
|
+
if (previewWindow.totalRows > previewWindow.lines.length) {
|
|
1114
|
+
lines.push(rightAlignedLine(approvalScrollStatusLine(previewWindow), innerWidth, {
|
|
1115
|
+
color: APPROVAL_SCROLL_STATUS_COLOR,
|
|
1116
|
+
}));
|
|
1117
|
+
if (padded)
|
|
1118
|
+
lines.push(plainLine(''));
|
|
834
1119
|
}
|
|
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) => {
|
|
1120
|
+
const options = (prompt.options ?? []).map((option) => option.label);
|
|
1121
|
+
options.forEach((label, index) => {
|
|
841
1122
|
const selected = index === state.approvalCursor;
|
|
842
1123
|
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,
|
|
1124
|
+
color: selected ? APPROVAL_ACCENT_COLOR : 'gray',
|
|
1125
|
+
}), span(label, {
|
|
1126
|
+
color: selected ? APPROVAL_ACCENT_COLOR : undefined,
|
|
1127
|
+
bold: selected,
|
|
849
1128
|
})));
|
|
850
1129
|
});
|
|
851
|
-
|
|
1130
|
+
if (padded)
|
|
1131
|
+
lines.push(plainLine(''));
|
|
1132
|
+
lines.push(plainLine('↑/↓ moves • Enter confirms • Esc denies', {
|
|
852
1133
|
color: 'gray',
|
|
853
1134
|
}));
|
|
854
|
-
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR);
|
|
1135
|
+
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR, height);
|
|
855
1136
|
}
|
|
856
1137
|
if (state.modelPickerOpen) {
|
|
857
1138
|
const options = buildModelPickerOptions(state.currentModelId, state.serverModels);
|
|
858
1139
|
lines.push(...buildModelPickerPanel(options, state.modelPickerIndex, width, height));
|
|
859
1140
|
}
|
|
860
1141
|
if (state.jobsPickerOpen) {
|
|
861
|
-
lines.push(...buildJobsPickerLines(state, width, nowMs));
|
|
1142
|
+
lines.push(...buildJobsPickerLines(state, width, height, nowMs));
|
|
862
1143
|
}
|
|
863
1144
|
if (state.resumePickerOpen) {
|
|
864
1145
|
const filtered = filterResumeSessions(state.resumePickerSessions, state.resumePickerFilter, state.serverModels);
|
|
1146
|
+
const pickerWidth = Math.max(20, width - 2);
|
|
1147
|
+
const divider = () => plainLine('─'.repeat(pickerWidth), { color: 'gray', dim: true });
|
|
865
1148
|
lines.push(plainLine('Resume a previous session', { color: 'cyan', bold: true }));
|
|
866
1149
|
lines.push(line(span('Search: ', { color: 'gray' }), span(state.resumePickerFilter, {}), span('█', { color: 'gray' })));
|
|
867
|
-
|
|
1150
|
+
lines.push(divider());
|
|
1151
|
+
const maxCards = Math.max(2, Math.min(filtered.length, Math.floor((height - 10) / 3)));
|
|
1152
|
+
let start = 0;
|
|
1153
|
+
if (filtered.length > maxCards) {
|
|
1154
|
+
start = Math.min(Math.max(0, state.resumePickerIndex - Math.floor(maxCards / 2)), filtered.length - maxCards);
|
|
1155
|
+
}
|
|
1156
|
+
const visible = filtered.slice(start, start + maxCards);
|
|
1157
|
+
if (start > 0) {
|
|
1158
|
+
lines.push(plainLine(` … ${start} newer`, { color: 'gray', dim: true }));
|
|
1159
|
+
}
|
|
1160
|
+
for (const [offset, session] of visible.entries()) {
|
|
1161
|
+
const index = start + offset;
|
|
868
1162
|
const selected = index === state.resumePickerIndex;
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
1163
|
+
const prompt = singleLinePreview(session.lastUserMessage, pickerWidth) ||
|
|
1164
|
+
session.name ||
|
|
1165
|
+
`Session ${session.id}`;
|
|
1166
|
+
lines.push(line(span(selected ? '› ' : ' ', { color: 'cyan' }), span(fitLine(prompt, Math.max(10, pickerWidth - 2)), selected
|
|
1167
|
+
? { color: 'cyan', bold: true }
|
|
1168
|
+
: session.lastUserMessage
|
|
1169
|
+
? {}
|
|
1170
|
+
: { color: 'gray', dim: true })));
|
|
1171
|
+
const meta = [
|
|
1172
|
+
formatRelativeTime(session.updatedAt),
|
|
1173
|
+
pickerModelLabel(session.modelId, state.serverModels),
|
|
1174
|
+
session.branch ?? null,
|
|
1175
|
+
]
|
|
1176
|
+
.filter(Boolean)
|
|
1177
|
+
.join(' · ');
|
|
1178
|
+
lines.push(line(span(' '), span(fitLine(meta, Math.max(10, pickerWidth - 4)), {
|
|
1179
|
+
color: selected ? 'cyan' : 'gray',
|
|
1180
|
+
dim: !selected,
|
|
1181
|
+
})));
|
|
1182
|
+
if (offset < visible.length - 1)
|
|
1183
|
+
lines.push(plainLine(''));
|
|
1184
|
+
}
|
|
1185
|
+
const remaining = filtered.length - (start + visible.length);
|
|
1186
|
+
if (remaining > 0) {
|
|
1187
|
+
lines.push(plainLine(` … ${remaining} older`, { color: 'gray', dim: true }));
|
|
872
1188
|
}
|
|
873
1189
|
if (filtered.length === 0) {
|
|
874
1190
|
lines.push(plainLine('No sessions match.', { color: 'gray' }));
|
|
875
1191
|
}
|
|
876
|
-
lines.push(
|
|
1192
|
+
lines.push(divider());
|
|
1193
|
+
lines.push(plainLine('↑/↓ move · enter resume · esc start new · ctrl+c quit', {
|
|
877
1194
|
color: 'gray',
|
|
878
1195
|
}));
|
|
879
1196
|
}
|
|
@@ -892,6 +1209,15 @@ function buildOverlayLines(state, width, height, nowMs) {
|
|
|
892
1209
|
function countSectionLines(sections) {
|
|
893
1210
|
return sections.reduce((sum, section) => sum + section.lines.length, 0);
|
|
894
1211
|
}
|
|
1212
|
+
export function userInputViewportForFrame(state, cols, rows, spinnerFrame, elapsedSeconds, nowMs = 0) {
|
|
1213
|
+
const contentWidth = frameContentWidth(cols);
|
|
1214
|
+
const liveRows = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds, nowMs).length;
|
|
1215
|
+
const overlayHeight = Math.max(0, rows - liveRows - composerFooterLines(state).length);
|
|
1216
|
+
return {
|
|
1217
|
+
width: approvalPanelInnerWidth(contentWidth),
|
|
1218
|
+
maxRows: overlayPanelContentBudget(overlayHeight),
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
895
1221
|
function sliceTranscriptLines(lines, maxLines, scrollOffset) {
|
|
896
1222
|
if (maxLines <= 0 || lines.length <= maxLines) {
|
|
897
1223
|
return lines;
|
|
@@ -902,33 +1228,39 @@ function sliceTranscriptLines(lines, maxLines, scrollOffset) {
|
|
|
902
1228
|
return lines.slice(start, start + maxLines);
|
|
903
1229
|
}
|
|
904
1230
|
export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, nowMs = 0) {
|
|
905
|
-
const contentWidth =
|
|
1231
|
+
const contentWidth = frameContentWidth(cols);
|
|
906
1232
|
const gutter = Math.max(Math.floor((cols - contentWidth) / 2), 0);
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1233
|
+
const buildTranscriptLines = (wrapWidth) => {
|
|
1234
|
+
const blocks = state.transcript.map((entry) => cachedTranscriptEntryLines(entry, wrapWidth));
|
|
1235
|
+
const lines = [];
|
|
1236
|
+
blocks.forEach((block, index) => {
|
|
1237
|
+
if (index > 0) {
|
|
1238
|
+
lines.push(plainLine(''));
|
|
1239
|
+
}
|
|
1240
|
+
lines.push(...block);
|
|
1241
|
+
});
|
|
1242
|
+
return lines;
|
|
1243
|
+
};
|
|
1244
|
+
let transcriptLines = buildTranscriptLines(contentWidth);
|
|
915
1245
|
const sections = [];
|
|
916
|
-
const liveLines = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds);
|
|
1246
|
+
const liveLines = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds, nowMs);
|
|
917
1247
|
if (liveLines.length > 0) {
|
|
918
1248
|
sections.push({ kind: 'live', lines: liveLines });
|
|
919
1249
|
}
|
|
920
|
-
const overlayActive = Boolean(state.approvalPrompt || state.sudoPrompt);
|
|
1250
|
+
const overlayActive = Boolean(state.approvalPrompt || state.sudoPrompt || state.userInputPrompt);
|
|
921
1251
|
if (!state.resumePickerOpen && !state.modelPickerOpen && !state.jobsPickerOpen && !overlayActive) {
|
|
922
1252
|
const composerLines = [];
|
|
923
|
-
if (state.
|
|
924
|
-
|
|
1253
|
+
if (state.busy && state.status === 'Starting a new conversation...') {
|
|
1254
|
+
composerLines.push(line(span('Starting a new conversation…', { color: 'gray', dim: true })));
|
|
1255
|
+
}
|
|
1256
|
+
else if (state.queuedMessage) {
|
|
925
1257
|
const imageCount = state.queuedMessage.imageAttachments.length;
|
|
926
|
-
composerLines.push(line(span(
|
|
927
|
-
?
|
|
928
|
-
:
|
|
1258
|
+
composerLines.push(line(span('Queued', { color: 'cyan', bold: true }), span(' Enter', { color: 'cyan', bold: true }), span(imageCount > 0
|
|
1259
|
+
? ' sends it with the next prompt'
|
|
1260
|
+
: ' 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
1261
|
}
|
|
930
1262
|
else {
|
|
931
|
-
const promptLabel = state
|
|
1263
|
+
const promptLabel = composerPromptLabel(state);
|
|
932
1264
|
const placeholder = state.busy
|
|
933
1265
|
? 'Type to queue the next message'
|
|
934
1266
|
: 'Type a request or /help';
|
|
@@ -939,14 +1271,34 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
|
|
|
939
1271
|
lines: [...composerLines, plainLine(''), ...composerFooterLines(state)],
|
|
940
1272
|
});
|
|
941
1273
|
}
|
|
942
|
-
|
|
1274
|
+
if (state.userInputPrompt) {
|
|
1275
|
+
sections.push({
|
|
1276
|
+
kind: 'busyFooter',
|
|
1277
|
+
lines: composerFooterLines(state),
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
const overlayHeight = state.userInputPrompt
|
|
1281
|
+
? Math.max(0, rows - countSectionLines(sections))
|
|
1282
|
+
: state.jobsPickerOpen
|
|
1283
|
+
? Math.max(Math.min(rows, JOBS_PICKER_MIN_ROWS), rows - countSectionLines(sections))
|
|
1284
|
+
: rows;
|
|
1285
|
+
const overlayLines = buildOverlayLines(state, contentWidth, overlayHeight, nowMs);
|
|
943
1286
|
if (overlayLines.length > 0) {
|
|
944
1287
|
sections.push({ kind: 'overlay', lines: overlayLines });
|
|
945
1288
|
}
|
|
946
1289
|
const reservedLines = countSectionLines(sections.filter((section) => section.kind !== 'transcript'));
|
|
947
|
-
const composerReserve = state.resumePickerOpen
|
|
1290
|
+
const composerReserve = state.resumePickerOpen ||
|
|
1291
|
+
state.approvalPrompt ||
|
|
1292
|
+
state.sudoPrompt ||
|
|
1293
|
+
state.userInputPrompt
|
|
1294
|
+
? 0
|
|
1295
|
+
: 4;
|
|
948
1296
|
const transcriptBudget = Math.max(1, rows - reservedLines - composerReserve - 1);
|
|
949
|
-
|
|
1297
|
+
let transcriptScrollLimit = Math.max(0, transcriptLines.length - transcriptBudget);
|
|
1298
|
+
if (transcriptScrollLimit > 0 && contentWidth > 2) {
|
|
1299
|
+
transcriptLines = buildTranscriptLines(contentWidth - 2);
|
|
1300
|
+
transcriptScrollLimit = Math.max(0, transcriptLines.length - transcriptBudget);
|
|
1301
|
+
}
|
|
950
1302
|
const transcriptScrollOffset = Math.min(Math.max(state.transcriptScrollOffset, 0), transcriptScrollLimit);
|
|
951
1303
|
sections.unshift({
|
|
952
1304
|
kind: 'transcript',
|