@agentprojectcontext/apx 1.54.0 → 1.55.0
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/package.json +1 -1
- package/src/core/stores/messages.js +69 -0
- package/src/host/daemon/api/conversations.js +25 -0
- package/src/interfaces/cli/commands/code.js +56 -0
- package/src/interfaces/cli/index.js +1 -1
- package/src/interfaces/web/dist/assets/{index-Bks35ks-.js → index-J3_Jhr99.js} +126 -126
- package/src/interfaces/web/dist/assets/{index-Bks35ks-.js.map → index-J3_Jhr99.js.map} +1 -1
- package/src/interfaces/web/dist/index.html +1 -1
- package/src/interfaces/web/src/components/chat/ChatList.tsx +73 -18
- package/src/interfaces/web/src/hooks/useChat.ts +29 -1
- package/src/interfaces/web/src/i18n/en.ts +1 -0
- package/src/interfaces/web/src/i18n/es.ts +1 -0
- package/src/interfaces/web/src/lib/api/conversations.ts +7 -1
- package/src/interfaces/web/src/screens/project/ChatTab.tsx +35 -18
- package/src/interfaces/web/src/types/daemon.ts +16 -0
- package/src/interfaces/cli/commands/sys.js +0 -862
- package/src/interfaces/cli/terminal-chat/renderer.js +0 -520
|
@@ -1,520 +0,0 @@
|
|
|
1
|
-
import readline from "node:readline";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
|
|
4
|
-
export const MODES = ["Build", "Plan", "Zen"];
|
|
5
|
-
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
6
|
-
|
|
7
|
-
export const C = {
|
|
8
|
-
reset: "\x1b[0m",
|
|
9
|
-
altOn: "\x1b[?1049h",
|
|
10
|
-
altOff: "\x1b[?1049l",
|
|
11
|
-
showCursor: "\x1b[?25h",
|
|
12
|
-
hideCursor: "\x1b[?25l",
|
|
13
|
-
setBgBlack: "\x1b]11;#000000\x07",
|
|
14
|
-
resetBg: "\x1b]111\x07",
|
|
15
|
-
bg: "\x1b[48;2;0;0;0m",
|
|
16
|
-
panel: "\x1b[48;2;26;26;26m",
|
|
17
|
-
panel2: "\x1b[48;2;31;31;31m",
|
|
18
|
-
text: "\x1b[38;2;237;237;237m",
|
|
19
|
-
muted: "\x1b[38;2;135;135;135m",
|
|
20
|
-
dim: "\x1b[38;2;69;69;69m",
|
|
21
|
-
primary: "\x1b[38;2;82;168;255m",
|
|
22
|
-
warning: "\x1b[38;2;255;178;36m",
|
|
23
|
-
error: "\x1b[38;2;229;72;77m",
|
|
24
|
-
success: "\x1b[38;2;70;167;88m",
|
|
25
|
-
bold: "\x1b[1m",
|
|
26
|
-
normal: "\x1b[22m",
|
|
27
|
-
italic: "\x1b[3m",
|
|
28
|
-
noItalic: "\x1b[23m",
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// Output buffer — all rendering writes here; flushed once per frame.
|
|
33
|
-
// This eliminates flash: the terminal sees clear+draw as a single atomic op.
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
let _buf = "";
|
|
36
|
-
|
|
37
|
-
function _w(s) {
|
|
38
|
-
_buf += s;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function _moveTo(row, col) {
|
|
42
|
-
_buf += `\x1b[${Math.max(1, row + 1)};${Math.max(1, col + 1)}H`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function _writeAt(row, col, text, width, bg = C.bg) {
|
|
46
|
-
_moveTo(row, col);
|
|
47
|
-
_buf += bg + padAnsi(text, width) + C.bg;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
|
|
52
|
-
export function titlecase(value) {
|
|
53
|
-
const clean = String(value || "").trim();
|
|
54
|
-
if (!clean) return "";
|
|
55
|
-
return clean.slice(0, 1).toUpperCase() + clean.slice(1);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function readPackageVersion() {
|
|
59
|
-
try {
|
|
60
|
-
return JSON.parse(fs.readFileSync(new URL("../../../package.json", import.meta.url), "utf8")).version || "dev";
|
|
61
|
-
} catch {
|
|
62
|
-
return "dev";
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function visible(text) {
|
|
67
|
-
return String(text).replace(ANSI_RE, "").length;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function stripAnsi(text) {
|
|
71
|
-
return String(text).replace(ANSI_RE, "");
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function fit(text, width) {
|
|
75
|
-
const clean = stripAnsi(text).replace(/\r?\n/g, " ");
|
|
76
|
-
if (clean.length <= width) return text;
|
|
77
|
-
return clean.slice(0, Math.max(0, width - 1)) + "…";
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function padAnsi(text, width) {
|
|
81
|
-
return String(text) + " ".repeat(Math.max(0, width - visible(text)));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function terminalSize() {
|
|
85
|
-
return {
|
|
86
|
-
width: Math.max(40, process.stdout.columns || 80),
|
|
87
|
-
height: Math.max(12, process.stdout.rows || 24),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Legacy direct-write moveTo used for final cursor positioning after flush
|
|
92
|
-
function moveTo(row, col) {
|
|
93
|
-
readline.cursorTo(process.stdout, Math.max(0, col), Math.max(0, row));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function centerLeft(width, contentWidth) {
|
|
97
|
-
return Math.max(0, Math.floor((width - contentWidth) / 2));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function wrapText(text, width) {
|
|
101
|
-
const raw = String(text || "").replace(/\t/g, " ").split(/\r?\n/);
|
|
102
|
-
const out = [];
|
|
103
|
-
for (const line of raw) {
|
|
104
|
-
if (!line) {
|
|
105
|
-
out.push("");
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
let rest = line;
|
|
109
|
-
while (rest.length > width) {
|
|
110
|
-
let cut = rest.lastIndexOf(" ", width);
|
|
111
|
-
if (cut < Math.floor(width * 0.5)) cut = width;
|
|
112
|
-
out.push(rest.slice(0, cut));
|
|
113
|
-
rest = rest.slice(cut).trimStart();
|
|
114
|
-
}
|
|
115
|
-
out.push(rest);
|
|
116
|
-
}
|
|
117
|
-
return out;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function renderLogo(termWidth, top) {
|
|
121
|
-
const lines = [
|
|
122
|
-
" █████╗ ██████╗ ██╗ ██╗",
|
|
123
|
-
"██╔══██╗██╔══██╗╚██╗██╔╝",
|
|
124
|
-
"███████║██████╔╝ ╚███╔╝ ",
|
|
125
|
-
"██╔══██║██╔═══╝ ██╔██╗ ",
|
|
126
|
-
"██║ ██║██║ ██╔╝ ██╗",
|
|
127
|
-
"╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝",
|
|
128
|
-
];
|
|
129
|
-
for (let i = 0; i < lines.length; i++) {
|
|
130
|
-
const line = lines[i];
|
|
131
|
-
const left = centerLeft(termWidth, visible(line));
|
|
132
|
-
const color = i < 2 ? C.dim : i < 4 ? C.muted : C.text;
|
|
133
|
-
_writeAt(top + i, left, C.bold + color + line + C.normal, visible(line));
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function renderModeMeta(currentModeIdx, activeAgent, activeModel, maxWidth) {
|
|
138
|
-
const modeText = MODES.map((mode, i) =>
|
|
139
|
-
i === currentModeIdx ? C.primary + mode : C.muted + mode
|
|
140
|
-
).join(C.muted + " · ");
|
|
141
|
-
const meta = `${modeText}${C.muted} · ${C.text}${C.bold}${activeAgent}${C.normal} ${C.muted}${activeModel}`;
|
|
142
|
-
return visible(meta) <= maxWidth ? meta : C.muted + fit(stripAnsi(meta), maxWidth);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function promptGeometry(centered, hasStarted, chatWidth) {
|
|
146
|
-
const { width, height } = terminalSize();
|
|
147
|
-
const boxWidth = hasStarted
|
|
148
|
-
? Math.max(36, chatWidth - 4)
|
|
149
|
-
: Math.min(75, Math.max(36, width - 4));
|
|
150
|
-
const left = centered ? centerLeft(width, boxWidth) : 2;
|
|
151
|
-
const top = hasStarted ? Math.max(0, height - 7) : Math.max(1, Math.floor(height / 2));
|
|
152
|
-
return { left, top, boxWidth };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function renderPromptBlock(state, chatWidth) {
|
|
156
|
-
const {
|
|
157
|
-
currentModeIdx,
|
|
158
|
-
activeAgent,
|
|
159
|
-
activeModel,
|
|
160
|
-
inputText,
|
|
161
|
-
cursorIndex,
|
|
162
|
-
hasStarted,
|
|
163
|
-
} = state;
|
|
164
|
-
const { left, top, boxWidth } = promptGeometry(!hasStarted, hasStarted, chatWidth);
|
|
165
|
-
const contentWidth = boxWidth - 3;
|
|
166
|
-
const placeholder = `Ask anything... "Fix broken tests"`;
|
|
167
|
-
const displayStart = Math.max(0, cursorIndex - contentWidth + 1);
|
|
168
|
-
const inputVisible = inputText.slice(displayStart, displayStart + contentWidth);
|
|
169
|
-
const beforeCursor = inputText.slice(displayStart, cursorIndex);
|
|
170
|
-
const promptLine = inputText
|
|
171
|
-
? C.text + fit(inputVisible, contentWidth)
|
|
172
|
-
: C.muted + C.italic + fit(placeholder, contentWidth) + C.noItalic;
|
|
173
|
-
const metaLine = renderModeMeta(currentModeIdx, activeAgent, activeModel, contentWidth);
|
|
174
|
-
|
|
175
|
-
_writeAt(top, left, C.primary + "┃" + C.panel + " " + " ".repeat(contentWidth), boxWidth, C.bg);
|
|
176
|
-
_writeAt(top + 1, left, C.primary + "┃" + C.panel + " " + padAnsi(promptLine, contentWidth), boxWidth, C.bg);
|
|
177
|
-
_writeAt(top + 2, left, C.primary + "┃" + C.panel + " " + " ".repeat(contentWidth), boxWidth, C.bg);
|
|
178
|
-
_writeAt(top + 3, left, C.primary + "┃" + C.panel + " " + padAnsi(metaLine, contentWidth), boxWidth, C.bg);
|
|
179
|
-
_writeAt(top + 4, left, C.primary + "╹" + C.panel + " " + " ".repeat(contentWidth), boxWidth, C.bg);
|
|
180
|
-
|
|
181
|
-
const hotkeys =
|
|
182
|
-
C.bold + C.text + "tab" + C.normal + C.muted + " agents " +
|
|
183
|
-
C.bold + C.text + "ctrl+p" + C.normal + C.muted + " commands " +
|
|
184
|
-
(state.hasStarted && state.transcript?.some((t) => t.type === "user" && t.meta === "queued")
|
|
185
|
-
? C.bold + C.warning + "ctrl+i" + C.normal + C.muted + " interrupt "
|
|
186
|
-
: "") +
|
|
187
|
-
(state.hasStarted && state.transcript?.length > 0
|
|
188
|
-
? C.bold + C.text + "click" + C.normal + C.muted + " actions "
|
|
189
|
-
: "") +
|
|
190
|
-
C.bold + C.text + "enter" + C.normal + C.muted + " send";
|
|
191
|
-
const hotkeyLeft = Math.max(left, left + boxWidth - visible(hotkeys));
|
|
192
|
-
_writeAt(top + 5, hotkeyLeft, hotkeys, visible(hotkeys), C.bg);
|
|
193
|
-
|
|
194
|
-
return { row: top + 1, col: left + 2 + visible(beforeCursor) };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function addLine(lines, text = "", bg = C.bg) {
|
|
198
|
-
lines.push({ text, bg });
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function toolLabel(tool) {
|
|
202
|
-
return {
|
|
203
|
-
read_file: "Read",
|
|
204
|
-
write_file: "Wrote",
|
|
205
|
-
edit_file: "Edit",
|
|
206
|
-
search_files: "Search",
|
|
207
|
-
run_shell: "Shell",
|
|
208
|
-
list_files: "List",
|
|
209
|
-
list_projects: "Projects",
|
|
210
|
-
list_agents: "Agents",
|
|
211
|
-
tail_messages: "Messages",
|
|
212
|
-
}[tool] || tool;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function parseTraceResult(result) {
|
|
216
|
-
if (typeof result !== "string") return result;
|
|
217
|
-
try {
|
|
218
|
-
return JSON.parse(result);
|
|
219
|
-
} catch {
|
|
220
|
-
return result;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function resultPreview(result) {
|
|
225
|
-
const parsed = parseTraceResult(result);
|
|
226
|
-
if (!parsed) return "";
|
|
227
|
-
if (typeof parsed === "string") return parsed;
|
|
228
|
-
if (parsed.error) return String(parsed.error);
|
|
229
|
-
if (parsed.content) return String(parsed.content);
|
|
230
|
-
if (parsed.stdout) return String(parsed.stdout);
|
|
231
|
-
if (parsed.path) return String(parsed.path);
|
|
232
|
-
return JSON.stringify(parsed);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function addToolBlock(lines, item, width) {
|
|
236
|
-
const trace = item.trace || {};
|
|
237
|
-
const args = trace.args || {};
|
|
238
|
-
const label = toolLabel(trace.tool);
|
|
239
|
-
const target = args.path || args.query || args.command || args.project || "";
|
|
240
|
-
const inner = Math.max(12, width - 12);
|
|
241
|
-
const margin = " ";
|
|
242
|
-
|
|
243
|
-
addLine(lines, "", C.bg);
|
|
244
|
-
addLine(lines, margin + C.muted + `→ ${label}${target ? " " + fit(String(target), inner) : ""}`, C.bg);
|
|
245
|
-
|
|
246
|
-
if (trace.pending) {
|
|
247
|
-
addLine(lines, margin + C.dim + " " + C.muted + "running...", C.bg);
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (trace.tool === "write_file") {
|
|
252
|
-
const heading = `# Wrote ${args.path || "file"}`;
|
|
253
|
-
addLine(lines, margin + C.panel + " " + C.muted + heading + " ".repeat(Math.max(0, inner - visible(heading))), C.bg);
|
|
254
|
-
for (const chunk of wrapText(args.content || "", inner).slice(0, 8)) {
|
|
255
|
-
addLine(lines, margin + C.panel + " " + C.text + padAnsi(chunk, inner), C.bg);
|
|
256
|
-
}
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (trace.tool === "edit_file") {
|
|
261
|
-
const heading = `← Edit ${args.path || "file"}`;
|
|
262
|
-
addLine(lines, margin + C.panel + " " + C.muted + heading + " ".repeat(Math.max(0, inner - visible(heading))), C.bg);
|
|
263
|
-
for (const chunk of wrapText(args.search || "", inner - 2).slice(0, 5)) {
|
|
264
|
-
addLine(lines, margin + C.panel + " " + C.error + "- " + padAnsi(chunk, inner - 2), C.bg);
|
|
265
|
-
}
|
|
266
|
-
for (const chunk of wrapText(args.replace || "", inner - 2).slice(0, 5)) {
|
|
267
|
-
addLine(lines, margin + C.panel + " " + C.success + "+ " + padAnsi(chunk, inner - 2), C.bg);
|
|
268
|
-
}
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const preview = resultPreview(trace.result);
|
|
273
|
-
if (!preview) return;
|
|
274
|
-
for (const chunk of wrapText(preview, inner).slice(0, 6)) {
|
|
275
|
-
addLine(lines, margin + C.dim + " " + C.muted + chunk, C.bg);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function transcriptLines(transcript, width) {
|
|
280
|
-
const lines = [];
|
|
281
|
-
const inner = Math.max(10, width - 8);
|
|
282
|
-
const margin = " ";
|
|
283
|
-
|
|
284
|
-
for (const item of transcript) {
|
|
285
|
-
if (item.type === "user") {
|
|
286
|
-
addLine(lines, "", C.bg);
|
|
287
|
-
const chunks = wrapText(item.text, inner - 1);
|
|
288
|
-
addLine(lines, margin + C.primary + "┃" + C.panel + " " + " ".repeat(inner), C.bg);
|
|
289
|
-
for (const chunk of chunks) {
|
|
290
|
-
addLine(lines, margin + C.primary + "┃" + C.panel + " " + C.text + padAnsi(chunk, inner), C.bg);
|
|
291
|
-
}
|
|
292
|
-
if (item.meta) {
|
|
293
|
-
const badgeText = item.meta === "queued"
|
|
294
|
-
? C.warning + C.bold + "QUEUED" + C.normal + C.muted + " click to send/remove"
|
|
295
|
-
: C.muted + item.meta;
|
|
296
|
-
addLine(lines, margin + C.primary + "┃" + C.panel + " " + badgeText + padAnsi("", Math.max(0, inner - (item.meta === "queued" ? 28 : visible(item.meta)))), C.bg);
|
|
297
|
-
}
|
|
298
|
-
addLine(lines, margin + C.primary + "┃" + C.panel + " " + " ".repeat(inner), C.bg);
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (item.type === "assistant") {
|
|
303
|
-
addLine(lines, "", C.bg);
|
|
304
|
-
addLine(lines, margin + C.primary + "■ " + C.text + C.bold + `${item.name}:` + C.normal, C.bg);
|
|
305
|
-
for (const raw of String(item.text || "").split(/\r?\n/)) {
|
|
306
|
-
const isThinking = raw.trim().startsWith("Thinking:");
|
|
307
|
-
const color = isThinking ? C.warning + C.italic : C.text;
|
|
308
|
-
const end = isThinking ? C.noItalic : "";
|
|
309
|
-
const prefix = isThinking ? margin + C.dim + "┃ " : margin + " ";
|
|
310
|
-
if (isThinking) addLine(lines, "", C.bg);
|
|
311
|
-
for (const chunk of wrapText(raw, width - 6)) {
|
|
312
|
-
addLine(lines, prefix + color + chunk + end, C.bg);
|
|
313
|
-
}
|
|
314
|
-
if (isThinking) addLine(lines, "", C.bg);
|
|
315
|
-
}
|
|
316
|
-
if (item.meta) addLine(lines, margin + " " + C.muted + item.meta, C.bg);
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (item.type === "tool") {
|
|
321
|
-
const trace = item.trace || {};
|
|
322
|
-
const isQuestion = trace.tool === "ask_questions";
|
|
323
|
-
const label = isQuestion ? C.warning + C.bold + "QUESTION" : C.muted + "TOOL";
|
|
324
|
-
const name = isQuestion ? "" : C.text + trace.tool;
|
|
325
|
-
|
|
326
|
-
if (isQuestion && trace.args?.questions) {
|
|
327
|
-
addLine(lines, "", C.bg);
|
|
328
|
-
addLine(lines, margin + label + C.muted + " (…)" + C.bg, C.bg);
|
|
329
|
-
for (const rawQ of trace.args.questions) {
|
|
330
|
-
// Rich shape: {question, options[], multiSelect, allowText}. Legacy: string.
|
|
331
|
-
const q = typeof rawQ === "string" ? { question: rawQ } : (rawQ || {});
|
|
332
|
-
const qText = typeof q.question === "string" ? q.question : "";
|
|
333
|
-
if (!qText) continue;
|
|
334
|
-
const qWrapped = wrapText(`• ${qText}`, inner - 2);
|
|
335
|
-
for (const line of qWrapped) {
|
|
336
|
-
addLine(lines, margin + C.primary + "┃ " + C.text + padAnsi(line, inner - 2), C.bg);
|
|
337
|
-
}
|
|
338
|
-
const opts = Array.isArray(q.options) ? q.options : [];
|
|
339
|
-
if (opts.length > 0) {
|
|
340
|
-
const marker = q.multiSelect ? "[ ]" : "( )";
|
|
341
|
-
opts.forEach((opt, i) => {
|
|
342
|
-
const label = (opt && typeof opt === "object" && typeof opt.label === "string")
|
|
343
|
-
? opt.label
|
|
344
|
-
: (typeof opt === "string" ? opt : "");
|
|
345
|
-
if (!label) return;
|
|
346
|
-
const desc = (opt && typeof opt === "object" && typeof opt.description === "string")
|
|
347
|
-
? ` — ${opt.description}` : "";
|
|
348
|
-
const optLine = ` ${marker} ${i + 1}. ${label}${desc}`;
|
|
349
|
-
for (const line of wrapText(optLine, inner - 2)) {
|
|
350
|
-
addLine(lines, margin + C.primary + "┃ " + C.muted + padAnsi(line, inner - 2), C.bg);
|
|
351
|
-
}
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
} else {
|
|
356
|
-
addLine(lines, margin + label + " " + name + C.bg, C.bg);
|
|
357
|
-
}
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (item.type === "status") {
|
|
362
|
-
addLine(lines, margin + C.muted + C.italic + item.text + C.noItalic, C.bg);
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (item.type === "error") {
|
|
367
|
-
addLine(lines, margin + C.error + "✖ Error: " + C.text + item.text, C.bg);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return lines;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function renderChat(state, chatWidth, height, promptTop) {
|
|
375
|
-
const maxRows = Math.max(1, promptTop - 1);
|
|
376
|
-
const lines = transcriptLines(state.transcript, chatWidth - 2);
|
|
377
|
-
const maxOffset = Math.max(0, lines.length - maxRows);
|
|
378
|
-
const offset = Math.min(Math.max(0, state.chatScrollOffset || 0), maxOffset);
|
|
379
|
-
state.chatScrollOffset = offset;
|
|
380
|
-
const start = Math.max(0, lines.length - maxRows - offset);
|
|
381
|
-
const slice = lines.slice(start, start + maxRows);
|
|
382
|
-
|
|
383
|
-
for (let i = 0; i < slice.length && i < maxRows; i++) {
|
|
384
|
-
_writeAt(i, 0, slice[i].text, chatWidth - 1, slice[i].bg);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (offset > 0 && maxRows > 1) {
|
|
388
|
-
_writeAt(0, 0, C.muted + `↑ ${offset} lines above bottom`, chatWidth - 1, C.bg);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function renderSidebar(state) {
|
|
393
|
-
const { width, height } = terminalSize();
|
|
394
|
-
if (width < 84) return null;
|
|
395
|
-
|
|
396
|
-
const sideWidth = Math.min(34, Math.max(28, Math.floor(width * 0.3)));
|
|
397
|
-
const left = width - sideWidth;
|
|
398
|
-
for (let row = 0; row < height; row++) {
|
|
399
|
-
_writeAt(row, left, "", sideWidth, C.panel);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const contentWidth = sideWidth - 4;
|
|
403
|
-
const totalTokens = state.usage.input + state.usage.output;
|
|
404
|
-
|
|
405
|
-
_writeAt(1, left + 2, C.text + C.bold + "Sesión" + C.normal, contentWidth, C.panel);
|
|
406
|
-
_writeAt(2, left + 2, C.muted + fit(state.sessionTitle || "chat local", contentWidth), contentWidth, C.panel);
|
|
407
|
-
_writeAt(3, left + 2, C.muted + "agent " + C.text + state.activeAgent, contentWidth, C.panel);
|
|
408
|
-
_writeAt(4, left + 2, C.muted + "app " + C.text + `APX ${state.version}`, contentWidth, C.panel);
|
|
409
|
-
|
|
410
|
-
_writeAt(6, left + 2, C.text + C.bold + "Modelo" + C.normal, contentWidth, C.panel);
|
|
411
|
-
const modelLines = wrapText(state.activeModel || "(none)", contentWidth).slice(0, 2);
|
|
412
|
-
modelLines.forEach((line, index) => {
|
|
413
|
-
_writeAt(7 + index, left + 2, C.muted + line, contentWidth, C.panel);
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
_writeAt(10, left + 2, C.text + C.bold + "Contexto" + C.normal, contentWidth, C.panel);
|
|
417
|
-
_writeAt(11, left + 2, C.muted + `${totalTokens.toLocaleString()} tokens total`, contentWidth, C.panel);
|
|
418
|
-
_writeAt(12, left + 2, C.muted + `${state.usage.input.toLocaleString()} in · ${state.usage.output.toLocaleString()} out`, contentWidth, C.panel);
|
|
419
|
-
_writeAt(13, left + 2, C.muted + `${state.usage.percent}% usado`, contentWidth, C.panel);
|
|
420
|
-
_writeAt(14, left + 2, C.muted + "$0.00 spent", contentWidth, C.panel);
|
|
421
|
-
|
|
422
|
-
_writeAt(16, left + 2, C.text + C.bold + "LSP" + C.normal, contentWidth, C.panel);
|
|
423
|
-
_writeAt(17, left + 2, C.muted + "LSPs are disabled", contentWidth, C.panel);
|
|
424
|
-
|
|
425
|
-
const cwdLines = wrapText(process.cwd(), contentWidth).slice(-4);
|
|
426
|
-
let row = Math.max(19, height - cwdLines.length - 4);
|
|
427
|
-
_writeAt(row++, left + 2, C.text + C.bold + "Directorio" + C.normal, contentWidth, C.panel);
|
|
428
|
-
for (const line of cwdLines) _writeAt(row++, left + 2, C.muted + line, contentWidth, C.panel);
|
|
429
|
-
_writeAt(height - 1, left + 2, C.success + "• " + C.text + "APX" + C.muted + ` ${state.version}`, contentWidth, C.panel);
|
|
430
|
-
|
|
431
|
-
return { left, width: sideWidth };
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function renderPaletteOverlay(state) {
|
|
435
|
-
const { width, height } = terminalSize();
|
|
436
|
-
const title = state.paletteState === "main" ? "COMMAND PALETTE" : "SELECT MODEL";
|
|
437
|
-
const boxWidth = Math.min(
|
|
438
|
-
62,
|
|
439
|
-
Math.max(32, Math.max(title.length + 8, ...state.paletteOptions.map((x) => visible(x) + 8)))
|
|
440
|
-
);
|
|
441
|
-
const boxHeight = state.paletteOptions.length + 4;
|
|
442
|
-
const left = centerLeft(width, boxWidth);
|
|
443
|
-
const top = Math.max(1, Math.floor((height - boxHeight) / 2));
|
|
444
|
-
|
|
445
|
-
_writeAt(top, left, C.text + C.bold + " " + title + C.normal, boxWidth, C.panel);
|
|
446
|
-
_writeAt(top + 1, left, C.dim + "▀".repeat(boxWidth), boxWidth, C.panel);
|
|
447
|
-
for (let i = 0; i < state.paletteOptions.length; i++) {
|
|
448
|
-
const active = i === state.paletteSelection;
|
|
449
|
-
const marker = active ? "›" : " ";
|
|
450
|
-
const bg = active ? C.panel2 : C.panel;
|
|
451
|
-
const fg = active ? C.primary + C.bold : C.text;
|
|
452
|
-
_writeAt(top + 2 + i, left, fg + ` ${marker} ${state.paletteOptions[i]}` + C.normal, boxWidth, bg);
|
|
453
|
-
}
|
|
454
|
-
_writeAt(top + 2 + state.paletteOptions.length, left, C.dim + "▄".repeat(boxWidth), boxWidth, C.panel);
|
|
455
|
-
_writeAt(
|
|
456
|
-
top + 3 + state.paletteOptions.length,
|
|
457
|
-
left,
|
|
458
|
-
C.muted + "↑↓ select " + C.text + C.bold + "enter" + C.normal + C.muted + " choose " + C.text + C.bold + "esc" + C.normal + C.muted + " close",
|
|
459
|
-
boxWidth,
|
|
460
|
-
C.bg
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function renderMsgActionsOverlay(state) {
|
|
465
|
-
const { width, height } = terminalSize();
|
|
466
|
-
const title = "Message Actions";
|
|
467
|
-
const opts = state.msgActionsOptions;
|
|
468
|
-
const preview = fit(state.msgActionsTarget?.text || "", 42);
|
|
469
|
-
const boxWidth = Math.min(62, Math.max(title.length + 8, preview.length + 6, ...opts.map((x) => visible(x) + 8)));
|
|
470
|
-
const boxHeight = opts.length + 5;
|
|
471
|
-
const left = centerLeft(width, boxWidth);
|
|
472
|
-
const top = Math.max(1, Math.floor((height - boxHeight) / 2));
|
|
473
|
-
|
|
474
|
-
_writeAt(top, left, C.text + C.bold + " " + title + C.normal, boxWidth, C.panel);
|
|
475
|
-
_writeAt(top + 1, left, C.muted + " " + C.italic + preview + C.noItalic, boxWidth, C.panel);
|
|
476
|
-
_writeAt(top + 2, left, C.dim + "▀".repeat(boxWidth), boxWidth, C.panel);
|
|
477
|
-
for (let i = 0; i < opts.length; i++) {
|
|
478
|
-
const active = i === state.msgActionsSelection;
|
|
479
|
-
const marker = active ? "›" : " ";
|
|
480
|
-
const bg = active ? C.panel2 : C.panel;
|
|
481
|
-
const fg = active ? C.primary + C.bold : C.text;
|
|
482
|
-
_writeAt(top + 3 + i, left, fg + ` ${marker} ${opts[i]}` + C.normal, boxWidth, bg);
|
|
483
|
-
}
|
|
484
|
-
_writeAt(top + 3 + opts.length, left, C.dim + "▄".repeat(boxWidth), boxWidth, C.panel);
|
|
485
|
-
_writeAt(
|
|
486
|
-
top + 4 + opts.length,
|
|
487
|
-
left,
|
|
488
|
-
C.muted + "↑↓ select " + C.text + C.bold + "enter" + C.normal + C.muted + " choose " + C.text + C.bold + "esc" + C.normal + C.muted + " close",
|
|
489
|
-
boxWidth,
|
|
490
|
-
C.bg
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
export function renderTerminalChat(state) {
|
|
495
|
-
// Reset buffer — clear screen and home cursor in ONE atomic write (no flash)
|
|
496
|
-
_buf = C.bg + "\x1b[2J\x1b[H";
|
|
497
|
-
|
|
498
|
-
const { width, height } = terminalSize();
|
|
499
|
-
const sidebar = state.hasStarted ? renderSidebar(state) : null;
|
|
500
|
-
const chatWidth = sidebar ? sidebar.left : width;
|
|
501
|
-
|
|
502
|
-
if (!state.hasStarted) {
|
|
503
|
-
renderLogo(chatWidth, Math.max(1, Math.floor(height / 2) - 8));
|
|
504
|
-
} else {
|
|
505
|
-
const prompt = promptGeometry(false, true, chatWidth);
|
|
506
|
-
renderChat(state, chatWidth, height, prompt.top);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const cursor = renderPromptBlock(state, chatWidth);
|
|
510
|
-
|
|
511
|
-
if (state.inCommandPalette) renderPaletteOverlay(state);
|
|
512
|
-
if (state.inMsgActions) renderMsgActionsOverlay(state);
|
|
513
|
-
|
|
514
|
-
// Position cursor in buffer before final flush
|
|
515
|
-
_buf += `\x1b[${Math.max(1, cursor.row + 1)};${Math.max(1, cursor.col + 1)}H`;
|
|
516
|
-
|
|
517
|
-
// Single atomic write — terminal renders entire frame at once, no flash
|
|
518
|
-
process.stdout.write(_buf);
|
|
519
|
-
_buf = "";
|
|
520
|
-
}
|