@thegitai/cli 1.0.0-beta.1
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/LICENSE +20 -0
- package/README.md +30 -0
- package/dist/bin/ai.js +438 -0
- package/dist/parsers/tree-sitter-c-sharp.wasm +0 -0
- package/dist/parsers/tree-sitter-c.wasm +0 -0
- package/dist/parsers/tree-sitter-cpp.wasm +0 -0
- package/dist/parsers/tree-sitter-css.wasm +0 -0
- package/dist/parsers/tree-sitter-go.wasm +0 -0
- package/dist/parsers/tree-sitter-html.wasm +0 -0
- package/dist/parsers/tree-sitter-java.wasm +0 -0
- package/dist/parsers/tree-sitter-javascript.wasm +0 -0
- package/dist/parsers/tree-sitter-objc.wasm +0 -0
- package/dist/parsers/tree-sitter-php.wasm +0 -0
- package/dist/parsers/tree-sitter-python.wasm +0 -0
- package/dist/parsers/tree-sitter-ruby.wasm +0 -0
- package/dist/parsers/tree-sitter-rust.wasm +0 -0
- package/dist/parsers/tree-sitter-tsx.wasm +0 -0
- package/dist/parsers/tree-sitter-typescript.wasm +0 -0
- package/dist/src/agent-mode.js +142 -0
- package/dist/src/api/auth.js +81 -0
- package/dist/src/api/browser-login.js +184 -0
- package/dist/src/api/chat.js +346 -0
- package/dist/src/api/contracts.js +1 -0
- package/dist/src/api/http.js +44 -0
- package/dist/src/api/index.js +11 -0
- package/dist/src/api/models.js +110 -0
- package/dist/src/api/sessions.js +72 -0
- package/dist/src/artifact-policy.js +207 -0
- package/dist/src/client-state.js +14 -0
- package/dist/src/core/clipboard.js +208 -0
- package/dist/src/core/open-url.js +32 -0
- package/dist/src/edit-journal.js +133 -0
- package/dist/src/executor.js +924 -0
- package/dist/src/extractors/cpp.js +18 -0
- package/dist/src/extractors/csharp.js +16 -0
- package/dist/src/extractors/css.js +12 -0
- package/dist/src/extractors/go.js +27 -0
- package/dist/src/extractors/index.js +52 -0
- package/dist/src/extractors/java.js +14 -0
- package/dist/src/extractors/javascript.js +33 -0
- package/dist/src/extractors/objc.js +14 -0
- package/dist/src/extractors/php.js +20 -0
- package/dist/src/extractors/python.js +11 -0
- package/dist/src/extractors/ruby.js +13 -0
- package/dist/src/extractors/rust.js +17 -0
- package/dist/src/extractors/utils.js +58 -0
- package/dist/src/help-text.js +125 -0
- package/dist/src/markdown-renderer.js +112 -0
- package/dist/src/patcher.js +279 -0
- package/dist/src/project-index.js +221 -0
- package/dist/src/repo-map-languages.js +100 -0
- package/dist/src/runtime-mode.js +35 -0
- package/dist/src/scanner.js +362 -0
- package/dist/src/secret-preview.js +137 -0
- package/dist/src/session-exit.js +17 -0
- package/dist/src/session-safety.js +1012 -0
- package/dist/src/session-store.js +266 -0
- package/dist/src/session.js +93 -0
- package/dist/src/tool-executor.js +188 -0
- package/dist/src/tools/code-intel.js +472 -0
- package/dist/src/tools/delete-file.js +27 -0
- package/dist/src/tools/exec-utils.js +17 -0
- package/dist/src/tools/find-symbol.js +70 -0
- package/dist/src/tools/get-diagnostics.js +22 -0
- package/dist/src/tools/grep-code.js +331 -0
- package/dist/src/tools/hover-symbol.js +95 -0
- package/dist/src/tools/index.js +73 -0
- package/dist/src/tools/list-checkpoints.js +11 -0
- package/dist/src/tools/list-directories.js +16 -0
- package/dist/src/tools/list-files.js +13 -0
- package/dist/src/tools/list-session-edits.js +9 -0
- package/dist/src/tools/list-symbols.js +55 -0
- package/dist/src/tools/patch-file.js +88 -0
- package/dist/src/tools/path-listing.js +83 -0
- package/dist/src/tools/read-document.js +111 -0
- package/dist/src/tools/read-file.js +109 -0
- package/dist/src/tools/restore-checkpoint.js +100 -0
- package/dist/src/tools/ripgrep.js +29 -0
- package/dist/src/tools/run-command.js +94 -0
- package/dist/src/tools/run-node-script.js +210 -0
- package/dist/src/tools/search-code.js +37 -0
- package/dist/src/tools/shell-diagnostics.js +707 -0
- package/dist/src/tools/signature-help.js +118 -0
- package/dist/src/tools/str-replace.js +193 -0
- package/dist/src/tools/types.js +1 -0
- package/dist/src/tools/undo-edit.js +202 -0
- package/dist/src/tools/write-file.js +59 -0
- package/dist/src/tree-sitter-runtime.js +135 -0
- package/dist/src/types.js +1 -0
- package/dist/src/ui/paste-collapse.js +22 -0
- package/dist/src/ui/prompt-history-store.js +96 -0
- package/dist/src/ui/repl.js +2238 -0
- package/dist/src/ui/tui/bridge.js +175 -0
- package/dist/src/ui/tui/build-frame.js +718 -0
- package/dist/src/ui/tui/markdown-render.js +455 -0
- package/dist/src/ui/tui/shell-input.js +488 -0
- package/dist/src/ui/tui/text.js +30 -0
- package/dist/src/ui/tui/types.js +1 -0
- package/dist/src/usage.js +47 -0
- package/dist/src/utils.js +38 -0
- package/package.json +38 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import { agentModeLabel } from '../../agent-mode.js';
|
|
2
|
+
import { truncate } from '../../utils.js';
|
|
3
|
+
import { formatClientTokenUsage } from '../repl.js';
|
|
4
|
+
import { renderFormattedBodyLines, renderPreformattedBodyLines, } from './markdown-render.js';
|
|
5
|
+
import { line, plainLine, span, wrapText } from './text.js';
|
|
6
|
+
const WORKING_CLOCK_ICON = '◷';
|
|
7
|
+
const BRAILLE_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴'];
|
|
8
|
+
const COMMAND_PREVIEW_LINES = 10;
|
|
9
|
+
const WORKING_TOOL_PREVIEW_ROWS = 3;
|
|
10
|
+
const TRANSCRIPT_DIFF_PREVIEW_LINES = 24;
|
|
11
|
+
const THINKING_NOTE_PREVIEW_ROWS = 3;
|
|
12
|
+
const COMPOSER_INPUT_MAX_ROWS = 6;
|
|
13
|
+
const AGENT_MODE_LABEL_WIDTH = 16;
|
|
14
|
+
const OVERLAY_PANEL_MAX_WIDTH = 86;
|
|
15
|
+
const OVERLAY_PANEL_MARGIN_LINES = 2;
|
|
16
|
+
const OVERLAY_BORDER_COLOR = 'yellow';
|
|
17
|
+
const OVERLAY_WARNING_COLOR = 'ansi256(208)';
|
|
18
|
+
const MODEL_PICKER_PANEL_MAX_WIDTH = 86;
|
|
19
|
+
const MODEL_PICKER_PANEL_MARGIN_LINES = 2;
|
|
20
|
+
const MODEL_PICKER_BORDER_COLOR = 'gray';
|
|
21
|
+
const MODEL_PICKER_ACCENT_COLOR = 'cyan';
|
|
22
|
+
const MODEL_PICKER_HIGHLIGHT_BG = 'ansi256(87)';
|
|
23
|
+
const MODEL_PICKER_META_INDENT = ' ';
|
|
24
|
+
function splitComposerInput(input, cursor, width) {
|
|
25
|
+
const safeWidth = Math.max(1, width);
|
|
26
|
+
const normalizedCursor = Math.min(Math.max(cursor, 0), input.length);
|
|
27
|
+
const rows = [''];
|
|
28
|
+
let cursorRow = 0;
|
|
29
|
+
let cursorCol = 0;
|
|
30
|
+
let capturedCursor = false;
|
|
31
|
+
const captureCursor = () => {
|
|
32
|
+
if (capturedCursor)
|
|
33
|
+
return;
|
|
34
|
+
const current = rows[rows.length - 1] ?? '';
|
|
35
|
+
if (current.length >= safeWidth) {
|
|
36
|
+
rows.push('');
|
|
37
|
+
cursorRow = rows.length - 1;
|
|
38
|
+
cursorCol = 0;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
cursorRow = rows.length - 1;
|
|
42
|
+
cursorCol = current.length;
|
|
43
|
+
}
|
|
44
|
+
capturedCursor = true;
|
|
45
|
+
};
|
|
46
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
47
|
+
const char = input[index] ?? '';
|
|
48
|
+
if (char !== '\n' &&
|
|
49
|
+
char !== '\r' &&
|
|
50
|
+
(rows[rows.length - 1]?.length ?? 0) >= safeWidth) {
|
|
51
|
+
rows.push('');
|
|
52
|
+
}
|
|
53
|
+
if (index === normalizedCursor) {
|
|
54
|
+
captureCursor();
|
|
55
|
+
}
|
|
56
|
+
if (char === '\r')
|
|
57
|
+
continue;
|
|
58
|
+
if (char === '\n') {
|
|
59
|
+
rows.push('');
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
rows[rows.length - 1] = `${rows[rows.length - 1] ?? ''}${char}`;
|
|
63
|
+
}
|
|
64
|
+
if (!capturedCursor) {
|
|
65
|
+
captureCursor();
|
|
66
|
+
}
|
|
67
|
+
return { cursorCol, cursorRow, rows };
|
|
68
|
+
}
|
|
69
|
+
function buildComposerInputLines(input, cursor, promptLabel, placeholder, width) {
|
|
70
|
+
if (!input) {
|
|
71
|
+
return [
|
|
72
|
+
line(span(promptLabel, { color: 'cyan' }), span(' ', { inverse: true }), span(placeholder, { color: 'gray' })),
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
const labelWidth = promptLabel.length;
|
|
76
|
+
const inputWidth = Math.max(1, width - labelWidth);
|
|
77
|
+
const { cursorCol, cursorRow, rows } = splitComposerInput(input, cursor, inputWidth);
|
|
78
|
+
const firstRow = Math.min(Math.max(cursorRow - COMPOSER_INPUT_MAX_ROWS + 1, 0), Math.max(rows.length - COMPOSER_INPUT_MAX_ROWS, 0));
|
|
79
|
+
return rows
|
|
80
|
+
.slice(firstRow, firstRow + COMPOSER_INPUT_MAX_ROWS)
|
|
81
|
+
.map((text, index) => {
|
|
82
|
+
const absoluteRow = firstRow + index;
|
|
83
|
+
const label = index === 0
|
|
84
|
+
? span(promptLabel, { color: 'cyan' })
|
|
85
|
+
: span(' '.repeat(labelWidth));
|
|
86
|
+
if (absoluteRow !== cursorRow) {
|
|
87
|
+
return line(label, span(text));
|
|
88
|
+
}
|
|
89
|
+
const before = text.slice(0, cursorCol);
|
|
90
|
+
const cursorChar = text[cursorCol] ?? ' ';
|
|
91
|
+
const after = text.slice(cursorCol + 1);
|
|
92
|
+
return line(label, span(before), span(cursorChar, { inverse: true }), span(after));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function getEntryColor(kind) {
|
|
96
|
+
switch (kind) {
|
|
97
|
+
case 'assistant':
|
|
98
|
+
case 'diff':
|
|
99
|
+
return 'green';
|
|
100
|
+
case 'error':
|
|
101
|
+
return 'red';
|
|
102
|
+
case 'system':
|
|
103
|
+
return 'yellow';
|
|
104
|
+
case 'tool':
|
|
105
|
+
return 'blue';
|
|
106
|
+
case 'user':
|
|
107
|
+
return 'cyan';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function diffLineColor(kind) {
|
|
111
|
+
switch (kind) {
|
|
112
|
+
case 'add':
|
|
113
|
+
return 'green';
|
|
114
|
+
case 'remove':
|
|
115
|
+
return 'red';
|
|
116
|
+
case 'hunk':
|
|
117
|
+
return 'cyan';
|
|
118
|
+
case 'context':
|
|
119
|
+
return 'gray';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function diffLinePrefix(kind) {
|
|
123
|
+
switch (kind) {
|
|
124
|
+
case 'add':
|
|
125
|
+
return '+';
|
|
126
|
+
case 'remove':
|
|
127
|
+
return '-';
|
|
128
|
+
case 'hunk':
|
|
129
|
+
return '@@';
|
|
130
|
+
case 'context':
|
|
131
|
+
return ' ';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export function formatPromptDirectoryLabel(projectRoot, homeDir = process.env.HOME ?? '') {
|
|
135
|
+
const trimmed = String(projectRoot ?? '').trim();
|
|
136
|
+
if (!trimmed)
|
|
137
|
+
return '.';
|
|
138
|
+
const normalizedHome = String(homeDir ?? '').trim().replace(/\/+$/, '');
|
|
139
|
+
if (normalizedHome && trimmed === normalizedHome)
|
|
140
|
+
return '~';
|
|
141
|
+
if (normalizedHome && trimmed.startsWith(`${normalizedHome}/`)) {
|
|
142
|
+
return `~/${trimmed.slice(normalizedHome.length + 1)}`;
|
|
143
|
+
}
|
|
144
|
+
return trimmed;
|
|
145
|
+
}
|
|
146
|
+
export function formatPromptSessionIdLabel(showSessionId, sessionId) {
|
|
147
|
+
return showSessionId ? String(sessionId ?? '').trim() : '';
|
|
148
|
+
}
|
|
149
|
+
const CLIENT_SLASH_COMMANDS = [
|
|
150
|
+
{ command: '/help', description: 'Show available chat commands' },
|
|
151
|
+
{ command: '/usage', description: 'Show account usage percentage and reset times' },
|
|
152
|
+
{ command: '/model', description: 'Switch the active model' },
|
|
153
|
+
{ command: '/resume', description: 'Open the session picker to resume a previous session' },
|
|
154
|
+
{ command: '/clear', description: 'Clear conversation history' },
|
|
155
|
+
{ command: '/exit', description: 'Quit the current session' },
|
|
156
|
+
];
|
|
157
|
+
function buildModelPickerOptions(currentModelId, serverModels) {
|
|
158
|
+
return serverModels.map((model) => ({
|
|
159
|
+
id: model.id,
|
|
160
|
+
label: model.label,
|
|
161
|
+
meta: model.id === currentModelId ? 'current' : '',
|
|
162
|
+
disabled: false,
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
function getInputCommandToken(input) {
|
|
166
|
+
const trimmed = String(input ?? '').trimStart();
|
|
167
|
+
const match = trimmed.match(/^\/[^\s]*/);
|
|
168
|
+
return match?.[0] ?? '';
|
|
169
|
+
}
|
|
170
|
+
function scoreSlashCommand(option, token) {
|
|
171
|
+
if (option.command === token)
|
|
172
|
+
return 0;
|
|
173
|
+
if (option.command.startsWith(token))
|
|
174
|
+
return 1;
|
|
175
|
+
if (option.command.includes(token.slice(1)))
|
|
176
|
+
return 2;
|
|
177
|
+
if (option.description.toLowerCase().includes(token.slice(1)))
|
|
178
|
+
return 3;
|
|
179
|
+
return 4;
|
|
180
|
+
}
|
|
181
|
+
export function getSlashCommandSuggestions(input) {
|
|
182
|
+
const token = getInputCommandToken(input).toLowerCase();
|
|
183
|
+
if (!token)
|
|
184
|
+
return [];
|
|
185
|
+
const ranked = [...CLIENT_SLASH_COMMANDS].sort((a, b) => {
|
|
186
|
+
const scoreDiff = scoreSlashCommand(a, token) - scoreSlashCommand(b, token);
|
|
187
|
+
if (scoreDiff !== 0)
|
|
188
|
+
return scoreDiff;
|
|
189
|
+
return a.command.localeCompare(b.command);
|
|
190
|
+
});
|
|
191
|
+
const matches = ranked.filter((option) => scoreSlashCommand(option, token) < 4);
|
|
192
|
+
return (matches.length ? matches : ranked).slice(0, 5);
|
|
193
|
+
}
|
|
194
|
+
function formatModelLabel(modelId, serverModels) {
|
|
195
|
+
return serverModels.find((model) => model.id === modelId)?.label ?? 'Unknown model';
|
|
196
|
+
}
|
|
197
|
+
function filterResumeSessions(sessions, filter, serverModels) {
|
|
198
|
+
const q = filter.trim().toLowerCase();
|
|
199
|
+
if (!q)
|
|
200
|
+
return sessions;
|
|
201
|
+
return sessions.filter((s) => {
|
|
202
|
+
const branch = (s.branch ?? '').toLowerCase();
|
|
203
|
+
const model = formatModelLabel(s.modelId, serverModels).toLowerCase();
|
|
204
|
+
const conv = s.lastUserMessage.toLowerCase();
|
|
205
|
+
return branch.includes(q) || model.includes(q) || conv.includes(q);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function formatRelativeTime(isoDate) {
|
|
209
|
+
const diff = Date.now() - new Date(isoDate).getTime();
|
|
210
|
+
const minutes = Math.floor(diff / 60000);
|
|
211
|
+
if (minutes < 1)
|
|
212
|
+
return 'just now';
|
|
213
|
+
if (minutes < 60)
|
|
214
|
+
return `${minutes}m ago`;
|
|
215
|
+
const hours = Math.floor(minutes / 60);
|
|
216
|
+
if (hours < 24)
|
|
217
|
+
return `${hours}h ago`;
|
|
218
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
219
|
+
}
|
|
220
|
+
export function renderTranscriptEntryLines(entry, width) {
|
|
221
|
+
const color = getEntryColor(entry.kind);
|
|
222
|
+
const lines = [
|
|
223
|
+
line(span('● ', { color }), span(entry.title, { color, bold: true })),
|
|
224
|
+
];
|
|
225
|
+
lines.push(...(entry.preformatted
|
|
226
|
+
? renderPreformattedBodyLines(entry.body, width, entry.kind)
|
|
227
|
+
: renderFormattedBodyLines(entry.body, width, entry.kind)));
|
|
228
|
+
if (entry.diffPreview) {
|
|
229
|
+
lines.push(plainLine(` Added ${entry.diffPreview.added} line${entry.diffPreview.added === 1 ? '' : 's'}, removed ${entry.diffPreview.removed} line${entry.diffPreview.removed === 1 ? '' : 's'}`, { color: 'gray' }));
|
|
230
|
+
for (const diffLine of entry.diffPreview.lines.slice(0, TRANSCRIPT_DIFF_PREVIEW_LINES)) {
|
|
231
|
+
lines.push(line(span(`${diffLinePrefix(diffLine.kind)} `, { color: diffLineColor(diffLine.kind) }), span(truncate(diffLine.content || ' ', width - 4), {
|
|
232
|
+
color: diffLineColor(diffLine.kind),
|
|
233
|
+
})));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return lines;
|
|
237
|
+
}
|
|
238
|
+
function tokenUsageLines(usage) {
|
|
239
|
+
const match = usage.match(/^Session tokens • in ([^•]+) • out ([^•]+)(?: • think ([^•]+))? • cache ([^•]+)(?: • write ([^•]+))?(?: • index ([^•]+))?$/);
|
|
240
|
+
if (!match) {
|
|
241
|
+
return [plainLine(usage, { color: 'cyan' })];
|
|
242
|
+
}
|
|
243
|
+
const spans = [
|
|
244
|
+
span('Tokens', { color: 'cyan', bold: true }),
|
|
245
|
+
span(' In ', { color: 'gray' }),
|
|
246
|
+
span(match[1]?.trim() ?? '', { color: 'green' }),
|
|
247
|
+
span(' Out ', { color: 'gray' }),
|
|
248
|
+
span(match[2]?.trim() ?? '', { color: 'magenta' }),
|
|
249
|
+
];
|
|
250
|
+
if (match[3]) {
|
|
251
|
+
spans.push(span(' Think ', { color: 'gray' }), span(match[3].trim(), { color: 'magenta' }));
|
|
252
|
+
}
|
|
253
|
+
spans.push(span(' Cache ', { color: 'gray' }), span(match[4]?.trim() ?? '', { color: 'yellow' }));
|
|
254
|
+
if (match[5]) {
|
|
255
|
+
spans.push(span(' Write ', { color: 'gray' }), span(match[5].trim(), { color: 'yellow' }));
|
|
256
|
+
}
|
|
257
|
+
if (match[6]) {
|
|
258
|
+
spans.push(span(' Index ', { color: 'gray' }), span(match[6].trim(), { color: 'blue' }));
|
|
259
|
+
}
|
|
260
|
+
return [line(...spans)];
|
|
261
|
+
}
|
|
262
|
+
function footerTransientStatus(status) {
|
|
263
|
+
const text = String(status ?? '').trim();
|
|
264
|
+
if (!text)
|
|
265
|
+
return null;
|
|
266
|
+
if (text === 'Copied selection' || text === 'Copied link') {
|
|
267
|
+
return { color: 'green', text, icon: '✓' };
|
|
268
|
+
}
|
|
269
|
+
if (text === 'Opened link') {
|
|
270
|
+
return { color: 'cyan', text, icon: '↗' };
|
|
271
|
+
}
|
|
272
|
+
if (text === 'Could not open link' || text === 'Clipboard has no text to paste') {
|
|
273
|
+
return { color: 'yellow', text, icon: '!' };
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
function composerFooterLines(state) {
|
|
278
|
+
const visibleSessionId = formatPromptSessionIdLabel(state.showSessionId, state.sessionId);
|
|
279
|
+
const transientStatus = footerTransientStatus(state.status);
|
|
280
|
+
const lines = [
|
|
281
|
+
line(span('TheGitAI', { color: 'cyan', bold: true }), span(' • ', { color: 'gray', dim: true }), span(formatPromptDirectoryLabel(state.projectRoot), { color: 'cyan' }), ...(transientStatus
|
|
282
|
+
? [
|
|
283
|
+
span(' ', { color: 'gray', dim: true }),
|
|
284
|
+
span(transientStatus.text, {
|
|
285
|
+
color: transientStatus.color,
|
|
286
|
+
bold: true,
|
|
287
|
+
}),
|
|
288
|
+
span(` ${transientStatus.icon}`, {
|
|
289
|
+
color: transientStatus.color,
|
|
290
|
+
bold: true,
|
|
291
|
+
}),
|
|
292
|
+
]
|
|
293
|
+
: []), ...(visibleSessionId
|
|
294
|
+
? [span(` ${visibleSessionId}`, { color: 'gray', dim: true })]
|
|
295
|
+
: [])),
|
|
296
|
+
plainLine(''),
|
|
297
|
+
plainLine(formatModelLabel(state.currentModelId, state.serverModels), {
|
|
298
|
+
color: 'cyan',
|
|
299
|
+
dim: true,
|
|
300
|
+
}),
|
|
301
|
+
plainLine(''),
|
|
302
|
+
];
|
|
303
|
+
if (state.exitConfirmUntil != null) {
|
|
304
|
+
lines.push(plainLine(state.status || 'Press Ctrl+C again to quit.', {
|
|
305
|
+
color: 'red',
|
|
306
|
+
bold: true,
|
|
307
|
+
}));
|
|
308
|
+
return lines;
|
|
309
|
+
}
|
|
310
|
+
const helperText = state.busy
|
|
311
|
+
? state.queuedMessage
|
|
312
|
+
? 'Enter re-queues • ↑ edit queued • Esc cancels queued'
|
|
313
|
+
: 'Enter queues • Esc / Ctrl+C cancel turn'
|
|
314
|
+
: 'Enter sends • Shift+Tab mode • Esc cancel turn • Ctrl+C quits';
|
|
315
|
+
const agentLabel = agentModeLabel(state.agentMode).padEnd(AGENT_MODE_LABEL_WIDTH);
|
|
316
|
+
const tokenUsageText = state.tokenUsage || formatClientTokenUsage(null);
|
|
317
|
+
const footerSpans = [
|
|
318
|
+
span(helperText, { color: 'gray', dim: true }),
|
|
319
|
+
span(' '.repeat(Math.max(1, 8)), { color: 'gray', dim: true }),
|
|
320
|
+
span(agentLabel, {
|
|
321
|
+
color: state.agentMode === 'plan' ? 'yellow' : 'cyan',
|
|
322
|
+
}),
|
|
323
|
+
span(tokenUsageText, { color: 'cyan' }),
|
|
324
|
+
];
|
|
325
|
+
lines.push(line(...footerSpans));
|
|
326
|
+
return lines;
|
|
327
|
+
}
|
|
328
|
+
function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
329
|
+
if (!state.busy)
|
|
330
|
+
return [];
|
|
331
|
+
const lines = [plainLine('')];
|
|
332
|
+
if (state.activeTurnInput) {
|
|
333
|
+
lines.push(...renderTranscriptEntryLines({
|
|
334
|
+
body: state.activeTurnInput,
|
|
335
|
+
kind: 'user',
|
|
336
|
+
preformatted: state.activeTurnInputPreformatted,
|
|
337
|
+
title: 'You',
|
|
338
|
+
}, width));
|
|
339
|
+
lines.push(plainLine(''));
|
|
340
|
+
}
|
|
341
|
+
lines.push(plainLine(`${WORKING_CLOCK_ICON} Working · ${elapsedSeconds < 60 ? `${elapsedSeconds}s` : `${Math.floor(elapsedSeconds / 60)}m ${String(elapsedSeconds % 60).padStart(2, '0')}s`}`, { color: 'yellow' }));
|
|
342
|
+
const visibleTitle = state.thinkingTitle.trim();
|
|
343
|
+
const visibleNotes = state.thinkingNotes.filter(Boolean).slice(-THINKING_NOTE_PREVIEW_ROWS);
|
|
344
|
+
if (visibleTitle || visibleNotes.length > 0) {
|
|
345
|
+
lines.push(plainLine(''));
|
|
346
|
+
lines.push(line(span(`${BRAILLE_SPINNER_FRAMES[spinnerFrame % BRAILLE_SPINNER_FRAMES.length]} `, {
|
|
347
|
+
color: 'green',
|
|
348
|
+
}), span('Thinking', { color: 'green', bold: true }), ...(visibleTitle ? [span(` · ${visibleTitle}`, { color: 'ansi256(248)' })] : [])));
|
|
349
|
+
for (const note of visibleNotes) {
|
|
350
|
+
lines.push(line(span('│ ', { color: 'green' }), span(note, { color: 'ansi256(248)' })));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const toolEntries = state.workingTools.slice(-WORKING_TOOL_PREVIEW_ROWS);
|
|
354
|
+
if (toolEntries.length > 0) {
|
|
355
|
+
lines.push(plainLine(''));
|
|
356
|
+
for (const entry of toolEntries) {
|
|
357
|
+
const color = getEntryColor(entry.kind);
|
|
358
|
+
const body = entry.body.split('\n')[0]?.trim();
|
|
359
|
+
lines.push(line(span('● ', { color }), span(entry.title, { color, bold: true }), ...(body ? [span(` ${truncate(body, 140)}`, { color })] : [])));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const logLines = state.commandLog.slice(-COMMAND_PREVIEW_LINES);
|
|
363
|
+
if (logLines.length > 0) {
|
|
364
|
+
lines.push(plainLine(''));
|
|
365
|
+
for (const outputLine of logLines) {
|
|
366
|
+
lines.push(plainLine(outputLine, { color: 'gray', dim: true }));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
lines.push(plainLine(''));
|
|
370
|
+
return lines;
|
|
371
|
+
}
|
|
372
|
+
function lineCharCount(row) {
|
|
373
|
+
return row.spans.reduce((total, item) => total + [...item.text].length, 0);
|
|
374
|
+
}
|
|
375
|
+
function overlayPanelLine(row, width, color) {
|
|
376
|
+
const padding = Math.max(0, width - lineCharCount(row));
|
|
377
|
+
return line(span('│ ', { color }), ...row.spans, span(' '.repeat(padding)), span(' │', { color }));
|
|
378
|
+
}
|
|
379
|
+
function buildOverlayPanel(rows, width, color) {
|
|
380
|
+
const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
|
|
381
|
+
const innerWidth = Math.max(1, panelWidth - 4);
|
|
382
|
+
const margin = Array.from({ length: OVERLAY_PANEL_MARGIN_LINES }, () => plainLine(''));
|
|
383
|
+
return [
|
|
384
|
+
...margin,
|
|
385
|
+
plainLine(`╭${'─'.repeat(panelWidth - 2)}╮`, { color }),
|
|
386
|
+
...rows.map((row) => overlayPanelLine(row, innerWidth, color)),
|
|
387
|
+
plainLine(`╰${'─'.repeat(panelWidth - 2)}╯`, { color }),
|
|
388
|
+
...margin,
|
|
389
|
+
];
|
|
390
|
+
}
|
|
391
|
+
function padSpansToInnerWidth(spans, innerWidth, fill) {
|
|
392
|
+
const used = spans.reduce((total, part) => total + [...part.text].length, 0);
|
|
393
|
+
const pad = Math.max(0, innerWidth - used);
|
|
394
|
+
if (pad === 0)
|
|
395
|
+
return spans;
|
|
396
|
+
return [...spans, span(' '.repeat(pad), fill)];
|
|
397
|
+
}
|
|
398
|
+
function modelPickerPanelSideLine(content, innerWidth) {
|
|
399
|
+
return overlayPanelLine(content, innerWidth, MODEL_PICKER_BORDER_COLOR);
|
|
400
|
+
}
|
|
401
|
+
function modelPickerTopBorder(panelWidth) {
|
|
402
|
+
const prefix = '╭─ Models ';
|
|
403
|
+
const suffix = '╮';
|
|
404
|
+
const dashCount = Math.max(0, panelWidth - prefix.length - suffix.length);
|
|
405
|
+
return line(span('╭─ ', { color: MODEL_PICKER_BORDER_COLOR }), span('Models', { color: MODEL_PICKER_ACCENT_COLOR, bold: true }), span(` ${'─'.repeat(dashCount)}╮`, { color: MODEL_PICKER_BORDER_COLOR }));
|
|
406
|
+
}
|
|
407
|
+
function modelPickerItemLines(option, selected, innerWidth) {
|
|
408
|
+
const highlight = { bgColor: MODEL_PICKER_HIGHLIGHT_BG };
|
|
409
|
+
if (selected) {
|
|
410
|
+
const titleSpans = padSpansToInnerWidth([
|
|
411
|
+
span('▌', {
|
|
412
|
+
color: MODEL_PICKER_ACCENT_COLOR,
|
|
413
|
+
bold: true,
|
|
414
|
+
...highlight,
|
|
415
|
+
}),
|
|
416
|
+
span('▶ ', {
|
|
417
|
+
color: MODEL_PICKER_ACCENT_COLOR,
|
|
418
|
+
bold: true,
|
|
419
|
+
...highlight,
|
|
420
|
+
}),
|
|
421
|
+
span('o ', { color: MODEL_PICKER_ACCENT_COLOR, ...highlight }),
|
|
422
|
+
span(option.label, {
|
|
423
|
+
color: MODEL_PICKER_ACCENT_COLOR,
|
|
424
|
+
bold: true,
|
|
425
|
+
...highlight,
|
|
426
|
+
}),
|
|
427
|
+
], innerWidth, highlight);
|
|
428
|
+
const lines = [modelPickerPanelSideLine(line(...titleSpans), innerWidth)];
|
|
429
|
+
if (option.meta) {
|
|
430
|
+
const metaSpans = padSpansToInnerWidth([
|
|
431
|
+
span(`${MODEL_PICKER_META_INDENT}${option.meta}`, {
|
|
432
|
+
color: 'gray',
|
|
433
|
+
...highlight,
|
|
434
|
+
}),
|
|
435
|
+
], innerWidth, highlight);
|
|
436
|
+
lines.push(modelPickerPanelSideLine(line(...metaSpans), innerWidth));
|
|
437
|
+
}
|
|
438
|
+
return lines;
|
|
439
|
+
}
|
|
440
|
+
const labelColor = option.disabled ? 'gray' : MODEL_PICKER_ACCENT_COLOR;
|
|
441
|
+
const lines = [
|
|
442
|
+
modelPickerPanelSideLine(line(span(' o ', { color: labelColor }), span(option.label, { color: labelColor, bold: !option.disabled })), innerWidth),
|
|
443
|
+
];
|
|
444
|
+
if (option.meta) {
|
|
445
|
+
lines.push(modelPickerPanelSideLine(line(span(`${MODEL_PICKER_META_INDENT}${option.meta}`, { color: 'gray' })), innerWidth));
|
|
446
|
+
}
|
|
447
|
+
return lines;
|
|
448
|
+
}
|
|
449
|
+
function modelPickerSeparatorLine(innerWidth) {
|
|
450
|
+
return modelPickerPanelSideLine(line(span('┈'.repeat(Math.max(1, innerWidth)), { color: 'gray', dim: true })), innerWidth);
|
|
451
|
+
}
|
|
452
|
+
function buildModelPickerPanel(options, selectedIndex, width) {
|
|
453
|
+
const panelWidth = Math.max(28, Math.min(width, MODEL_PICKER_PANEL_MAX_WIDTH));
|
|
454
|
+
const innerWidth = Math.max(1, panelWidth - 4);
|
|
455
|
+
const margin = Array.from({ length: MODEL_PICKER_PANEL_MARGIN_LINES }, () => plainLine(''));
|
|
456
|
+
const body = [
|
|
457
|
+
plainLine('TheGitAI - Model Selection', {
|
|
458
|
+
color: MODEL_PICKER_ACCENT_COLOR,
|
|
459
|
+
bold: true,
|
|
460
|
+
}),
|
|
461
|
+
plainLine(''),
|
|
462
|
+
modelPickerTopBorder(panelWidth),
|
|
463
|
+
modelPickerPanelSideLine(plainLine(''), innerWidth),
|
|
464
|
+
];
|
|
465
|
+
options.forEach((option, index) => {
|
|
466
|
+
body.push(...modelPickerItemLines(option, index === selectedIndex, innerWidth));
|
|
467
|
+
if (index < options.length - 1) {
|
|
468
|
+
body.push(modelPickerSeparatorLine(innerWidth));
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
body.push(modelPickerPanelSideLine(plainLine(''), innerWidth), modelPickerPanelSideLine(line(span('─'.repeat(innerWidth), { color: MODEL_PICKER_BORDER_COLOR })), innerWidth), modelPickerPanelSideLine(plainLine('↑/↓ choose • Enter select • Esc cancel', { color: 'gray' }), innerWidth), plainLine(`╰${'─'.repeat(panelWidth - 2)}╯`, { color: MODEL_PICKER_BORDER_COLOR }));
|
|
472
|
+
return [...margin, ...body, ...margin];
|
|
473
|
+
}
|
|
474
|
+
function commandPaletteTopBorder(panelWidth) {
|
|
475
|
+
const prefix = '╭─ Commands ';
|
|
476
|
+
const suffix = '╮';
|
|
477
|
+
const dashCount = Math.max(0, panelWidth - prefix.length - suffix.length);
|
|
478
|
+
return line(span('╭─ ', { color: MODEL_PICKER_BORDER_COLOR }), span('Commands', { color: MODEL_PICKER_ACCENT_COLOR, bold: true }), span(` ${'─'.repeat(dashCount)}╮`, { color: MODEL_PICKER_BORDER_COLOR }));
|
|
479
|
+
}
|
|
480
|
+
function commandPaletteItemLines(option, selected, innerWidth) {
|
|
481
|
+
const highlight = { bgColor: MODEL_PICKER_HIGHLIGHT_BG };
|
|
482
|
+
if (selected) {
|
|
483
|
+
const titleSpans = padSpansToInnerWidth([
|
|
484
|
+
span('▌', {
|
|
485
|
+
color: MODEL_PICKER_ACCENT_COLOR,
|
|
486
|
+
bold: true,
|
|
487
|
+
...highlight,
|
|
488
|
+
}),
|
|
489
|
+
span('▶ ', {
|
|
490
|
+
color: MODEL_PICKER_ACCENT_COLOR,
|
|
491
|
+
bold: true,
|
|
492
|
+
...highlight,
|
|
493
|
+
}),
|
|
494
|
+
span(option.command, {
|
|
495
|
+
color: MODEL_PICKER_ACCENT_COLOR,
|
|
496
|
+
bold: true,
|
|
497
|
+
...highlight,
|
|
498
|
+
}),
|
|
499
|
+
], innerWidth, highlight);
|
|
500
|
+
const metaSpans = padSpansToInnerWidth([
|
|
501
|
+
span(`${MODEL_PICKER_META_INDENT}${option.description}`, {
|
|
502
|
+
color: 'gray',
|
|
503
|
+
...highlight,
|
|
504
|
+
}),
|
|
505
|
+
], innerWidth, highlight);
|
|
506
|
+
return [
|
|
507
|
+
modelPickerPanelSideLine(line(...titleSpans), innerWidth),
|
|
508
|
+
modelPickerPanelSideLine(line(...metaSpans), innerWidth),
|
|
509
|
+
];
|
|
510
|
+
}
|
|
511
|
+
return [
|
|
512
|
+
modelPickerPanelSideLine(line(span(' ', { color: 'gray' }), span(option.command, { color: MODEL_PICKER_ACCENT_COLOR })), innerWidth),
|
|
513
|
+
modelPickerPanelSideLine(line(span(`${MODEL_PICKER_META_INDENT}${option.description}`, { color: 'gray' })), innerWidth),
|
|
514
|
+
];
|
|
515
|
+
}
|
|
516
|
+
function buildCommandPalettePanel(suggestions, selectedIndex, width) {
|
|
517
|
+
const panelWidth = Math.max(28, Math.min(width, MODEL_PICKER_PANEL_MAX_WIDTH));
|
|
518
|
+
const innerWidth = Math.max(1, panelWidth - 4);
|
|
519
|
+
const margin = Array.from({ length: MODEL_PICKER_PANEL_MARGIN_LINES }, () => plainLine(''));
|
|
520
|
+
const body = [
|
|
521
|
+
plainLine('TheGitAI - Commands', {
|
|
522
|
+
color: MODEL_PICKER_ACCENT_COLOR,
|
|
523
|
+
bold: true,
|
|
524
|
+
}),
|
|
525
|
+
plainLine(''),
|
|
526
|
+
commandPaletteTopBorder(panelWidth),
|
|
527
|
+
modelPickerPanelSideLine(plainLine(''), innerWidth),
|
|
528
|
+
];
|
|
529
|
+
suggestions.forEach((suggestion, index) => {
|
|
530
|
+
body.push(...commandPaletteItemLines(suggestion, index === selectedIndex, innerWidth));
|
|
531
|
+
if (index < suggestions.length - 1) {
|
|
532
|
+
body.push(modelPickerSeparatorLine(innerWidth));
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
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 }));
|
|
536
|
+
return [...margin, ...body, ...margin];
|
|
537
|
+
}
|
|
538
|
+
function buildOverlayLines(state, width) {
|
|
539
|
+
const lines = [];
|
|
540
|
+
const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
|
|
541
|
+
const innerWidth = Math.max(1, panelWidth - 4);
|
|
542
|
+
if (state.sudoPrompt) {
|
|
543
|
+
const prompt = state.sudoPrompt;
|
|
544
|
+
const passwordWidth = Math.max(0, innerWidth - 11);
|
|
545
|
+
lines.push(plainLine('Sudo Authentication Required', {
|
|
546
|
+
color: OVERLAY_BORDER_COLOR,
|
|
547
|
+
bold: true,
|
|
548
|
+
}));
|
|
549
|
+
lines.push(...wrapText('Agents can make mistakes. This command may change your system. Proceed at your own risk.', innerWidth).map((text) => plainLine(text, { color: OVERLAY_WARNING_COLOR })));
|
|
550
|
+
lines.push(plainLine(''));
|
|
551
|
+
const commandLines = wrapText(prompt.command, Math.max(1, innerWidth - 9));
|
|
552
|
+
commandLines.forEach((text, index) => {
|
|
553
|
+
lines.push(index === 0
|
|
554
|
+
? line(span('Command: ', {
|
|
555
|
+
color: OVERLAY_BORDER_COLOR,
|
|
556
|
+
bold: true,
|
|
557
|
+
}), span(text, { color: OVERLAY_BORDER_COLOR }))
|
|
558
|
+
: line(span(' '), span(text, { color: OVERLAY_BORDER_COLOR })));
|
|
559
|
+
});
|
|
560
|
+
lines.push(plainLine(''));
|
|
561
|
+
if (prompt.prompt.trim()) {
|
|
562
|
+
lines.push(plainLine(prompt.prompt.trim(), { color: 'gray' }));
|
|
563
|
+
}
|
|
564
|
+
lines.push(line(span('Password: ', { color: 'cyan', bold: true }), span('•'.repeat(Math.min(prompt.passwordLength, passwordWidth)), {
|
|
565
|
+
color: 'cyan',
|
|
566
|
+
}), span(' ', { inverse: true })));
|
|
567
|
+
lines.push(plainLine('Press Enter to submit, Escape to cancel', { color: 'gray' }));
|
|
568
|
+
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR);
|
|
569
|
+
}
|
|
570
|
+
if (state.approvalPrompt) {
|
|
571
|
+
const prompt = state.approvalPrompt;
|
|
572
|
+
lines.push(plainLine(prompt.title, {
|
|
573
|
+
color: OVERLAY_BORDER_COLOR,
|
|
574
|
+
bold: true,
|
|
575
|
+
}));
|
|
576
|
+
if (prompt.diffPreview && prompt.filePath) {
|
|
577
|
+
lines.push(line(span('● ', { color: 'green' }), span('Update(', { bold: true }), span(prompt.filePath, { color: 'cyan', bold: true }), span(')', { bold: true })));
|
|
578
|
+
lines.push(...renderTranscriptEntryLines({
|
|
579
|
+
body: '',
|
|
580
|
+
diffPreview: prompt.diffPreview,
|
|
581
|
+
kind: 'diff',
|
|
582
|
+
title: '',
|
|
583
|
+
}, innerWidth));
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
lines.push(...wrapText(prompt.body, innerWidth).map((text) => plainLine(text, { color: OVERLAY_BORDER_COLOR })));
|
|
587
|
+
}
|
|
588
|
+
const options = [
|
|
589
|
+
{ value: 'y', label: 'Approve once', color: 'green' },
|
|
590
|
+
{ value: 'a', label: 'Approve all remaining actions', color: 'cyan' },
|
|
591
|
+
{ value: 'n', label: 'Deny', color: 'red' },
|
|
592
|
+
];
|
|
593
|
+
options.forEach((option, index) => {
|
|
594
|
+
const selected = index === state.approvalCursor;
|
|
595
|
+
lines.push(line(span(selected ? '› ' : ' ', {
|
|
596
|
+
color: selected ? OVERLAY_BORDER_COLOR : 'gray',
|
|
597
|
+
}), span(option.value, {
|
|
598
|
+
color: selected ? OVERLAY_BORDER_COLOR : option.color,
|
|
599
|
+
bold: true,
|
|
600
|
+
}), span(` ${option.label}`, {
|
|
601
|
+
color: selected ? OVERLAY_BORDER_COLOR : undefined,
|
|
602
|
+
})));
|
|
603
|
+
});
|
|
604
|
+
lines.push(plainLine('Press y, a, or n • ↑/↓ moves • Enter confirms', {
|
|
605
|
+
color: 'gray',
|
|
606
|
+
}));
|
|
607
|
+
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR);
|
|
608
|
+
}
|
|
609
|
+
if (state.modelPickerOpen) {
|
|
610
|
+
const options = buildModelPickerOptions(state.currentModelId, state.serverModels);
|
|
611
|
+
lines.push(...buildModelPickerPanel(options, state.modelPickerIndex, width));
|
|
612
|
+
}
|
|
613
|
+
if (state.resumePickerOpen) {
|
|
614
|
+
const filtered = filterResumeSessions(state.resumePickerSessions, state.resumePickerFilter, state.serverModels);
|
|
615
|
+
lines.push(plainLine('Resume a previous session', { color: 'cyan', bold: true }));
|
|
616
|
+
lines.push(line(span('Search: ', { color: 'gray' }), span(state.resumePickerFilter, {}), span('█', { color: 'gray' })));
|
|
617
|
+
for (const [index, session] of filtered.entries()) {
|
|
618
|
+
const selected = index === state.resumePickerIndex;
|
|
619
|
+
const color = selected ? 'cyan' : undefined;
|
|
620
|
+
const model = truncate(formatModelLabel(session.modelId, state.serverModels), 26);
|
|
621
|
+
lines.push(plainLine(`${selected ? '› ' : ' '}${formatRelativeTime(session.updatedAt).padEnd(11)} ${(session.branch ?? '(no branch)').slice(0, 13).padEnd(14)} ${model.padEnd(26)} ${(session.lastUserMessage.slice(0, 40) || '(empty)')}`, { color, bold: selected }));
|
|
622
|
+
}
|
|
623
|
+
if (filtered.length === 0) {
|
|
624
|
+
lines.push(plainLine('No sessions match.', { color: 'gray' }));
|
|
625
|
+
}
|
|
626
|
+
lines.push(plainLine('↑/↓ move enter resume esc start new ctrl+c quit', {
|
|
627
|
+
color: 'gray',
|
|
628
|
+
}));
|
|
629
|
+
}
|
|
630
|
+
const suggestions = getSlashCommandSuggestions(state.input);
|
|
631
|
+
if (!state.busy &&
|
|
632
|
+
!state.exiting &&
|
|
633
|
+
!state.modelPickerOpen &&
|
|
634
|
+
!state.resumePickerOpen &&
|
|
635
|
+
state.input.trimStart().startsWith('/') &&
|
|
636
|
+
!state.input.includes(' ') &&
|
|
637
|
+
suggestions.length > 0) {
|
|
638
|
+
lines.push(...buildCommandPalettePanel(suggestions, state.commandCursor, width));
|
|
639
|
+
}
|
|
640
|
+
return lines;
|
|
641
|
+
}
|
|
642
|
+
function countSectionLines(sections) {
|
|
643
|
+
return sections.reduce((sum, section) => sum + section.lines.length, 0);
|
|
644
|
+
}
|
|
645
|
+
function sliceTranscriptLines(lines, maxLines, scrollOffset) {
|
|
646
|
+
if (maxLines <= 0 || lines.length <= maxLines) {
|
|
647
|
+
return lines;
|
|
648
|
+
}
|
|
649
|
+
const maxOffset = Math.max(0, lines.length - maxLines);
|
|
650
|
+
const offset = Math.min(Math.max(scrollOffset, 0), maxOffset);
|
|
651
|
+
const start = Math.max(0, lines.length - maxLines - offset);
|
|
652
|
+
return lines.slice(start, start + maxLines);
|
|
653
|
+
}
|
|
654
|
+
export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds) {
|
|
655
|
+
const contentWidth = Math.max(20, Math.floor(cols * 0.95) - 2);
|
|
656
|
+
const gutter = Math.max(Math.floor((cols - contentWidth) / 2), 0);
|
|
657
|
+
const transcriptBlocks = state.transcript.map((entry) => renderTranscriptEntryLines(entry, contentWidth));
|
|
658
|
+
const transcriptLines = [];
|
|
659
|
+
transcriptBlocks.forEach((block, index) => {
|
|
660
|
+
if (index > 0) {
|
|
661
|
+
transcriptLines.push(plainLine(''));
|
|
662
|
+
}
|
|
663
|
+
transcriptLines.push(...block);
|
|
664
|
+
});
|
|
665
|
+
const sections = [];
|
|
666
|
+
const liveLines = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds);
|
|
667
|
+
if (liveLines.length > 0) {
|
|
668
|
+
sections.push({ kind: 'live', lines: liveLines });
|
|
669
|
+
}
|
|
670
|
+
const overlayActive = Boolean(state.approvalPrompt || state.sudoPrompt);
|
|
671
|
+
// Composer stays visible while busy unless a blocking overlay is active. When
|
|
672
|
+
// one message is already queued, the queued chip is shown here in place of the
|
|
673
|
+
// input box (only one message is ever held) so there is no empty prompt.
|
|
674
|
+
if (!state.resumePickerOpen && !state.modelPickerOpen && !overlayActive) {
|
|
675
|
+
const composerLines = [];
|
|
676
|
+
if (state.queuedMessage) {
|
|
677
|
+
const preview = truncate(state.queuedMessage.body.trim().replace(/\s+/g, ' '), 60);
|
|
678
|
+
const imageCount = state.queuedMessage.imageAttachments.length;
|
|
679
|
+
composerLines.push(line(span(`↳ Queued · "${preview}"`, { color: 'gray', dim: true }), ...(imageCount > 0
|
|
680
|
+
? [span(` +${imageCount} img`, { color: 'gray', dim: true })]
|
|
681
|
+
: []), span(' ↑ edit · esc cancel', { color: 'gray', dim: true })));
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
const promptLabel = state.busy ? 'queue> ' : '❯ ';
|
|
685
|
+
const placeholder = state.busy
|
|
686
|
+
? 'Type to queue the next message'
|
|
687
|
+
: 'Type a request or /help';
|
|
688
|
+
composerLines.push(...buildComposerInputLines(state.input, state.cursor, promptLabel, placeholder, contentWidth));
|
|
689
|
+
}
|
|
690
|
+
sections.push({
|
|
691
|
+
kind: 'composer',
|
|
692
|
+
lines: [...composerLines, plainLine(''), ...composerFooterLines(state)],
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
const overlayLines = buildOverlayLines(state, contentWidth);
|
|
696
|
+
if (overlayLines.length > 0) {
|
|
697
|
+
sections.push({ kind: 'overlay', lines: overlayLines });
|
|
698
|
+
}
|
|
699
|
+
const reservedLines = countSectionLines(sections.filter((section) => section.kind !== 'transcript'));
|
|
700
|
+
const composerReserve = state.resumePickerOpen ? 0 : 4;
|
|
701
|
+
const transcriptBudget = Math.max(1, rows - reservedLines - composerReserve - 1);
|
|
702
|
+
const transcriptScrollLimit = Math.max(0, transcriptLines.length - transcriptBudget);
|
|
703
|
+
const transcriptScrollOffset = Math.min(Math.max(state.transcriptScrollOffset, 0), transcriptScrollLimit);
|
|
704
|
+
sections.unshift({
|
|
705
|
+
kind: 'transcript',
|
|
706
|
+
lines: sliceTranscriptLines(transcriptLines, transcriptBudget, transcriptScrollOffset),
|
|
707
|
+
});
|
|
708
|
+
return {
|
|
709
|
+
cols,
|
|
710
|
+
rows,
|
|
711
|
+
gutter,
|
|
712
|
+
contentWidth,
|
|
713
|
+
spinnerFrame,
|
|
714
|
+
transcriptScrollLimit,
|
|
715
|
+
transcriptScrollOffset,
|
|
716
|
+
sections,
|
|
717
|
+
};
|
|
718
|
+
}
|