vipcode 0.0.0-stage → 0.2.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.
@@ -0,0 +1,1015 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { Box, Static, Text, render, useAnimation, useApp, useInput, usePaste, useWindowSize } from "ink";
6
+ import pc from "picocolors";
7
+ import { useEffect, useMemo, useRef, useState } from "react";
8
+ import { configNotices } from "../config.js";
9
+ import { discoveryIsStale } from "../discovery.js";
10
+ import { isImagePath, readClipboardImage, toDataUrl } from "../images.js";
11
+ import { renderMarkdown } from "../markdown.js";
12
+ import { VERSION } from "../meta.js";
13
+ import { NoSlotError } from "../router.js";
14
+ import { appendHistory, loadHistory } from "../session.js";
15
+ import { suggestFiles } from "../tools.js";
16
+ import { colorPreview } from "../ui.js";
17
+ import { checkForUpdate } from "../update.js";
18
+ import { PermissionDialog, PromptDialog, SelectDialog } from "./dialogs.js";
19
+ import * as E from "./editor.js";
20
+ // Comandos que se pueden usar mientras el agente trabaja (solo consultan)
21
+ const SAFE_WHILE_BUSY = ["/help", "/usage", "/todos", "/mcp", "/lsp", "/providers", "/skills"];
22
+ const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
23
+ // Cómo se cuenta cada herramienta en pantalla
24
+ const TOOL_LABEL = {
25
+ read_file: "Lee",
26
+ write_file: "Escribe",
27
+ edit_file: "Edita",
28
+ list_dir: "Lista",
29
+ glob: "Busca ficheros",
30
+ grep: "Busca",
31
+ bash: "Ejecuta",
32
+ webfetch: "Abre",
33
+ websearch: "Busca en internet",
34
+ todowrite: "Tareas",
35
+ todoread: "Tareas",
36
+ diagnostics: "Diagnósticos",
37
+ task: "Subagente",
38
+ skill: "Skill",
39
+ };
40
+ function Spinner(props) {
41
+ const { frame } = useAnimation({ interval: 80 });
42
+ return _jsx(Text, { color: props.color || "cyan", children: SPINNER[frame % SPINNER.length] });
43
+ }
44
+ function formatDuration(ms) {
45
+ if (ms < 1000) {
46
+ return "<1 s";
47
+ }
48
+ const seconds = ms / 1000;
49
+ if (seconds < 10) {
50
+ return `${seconds.toFixed(1).replace(".", ",")} s`;
51
+ }
52
+ if (seconds < 60) {
53
+ return `${Math.round(seconds)} s`;
54
+ }
55
+ return `${Math.floor(seconds / 60)} min ${Math.round(seconds % 60)} s`;
56
+ }
57
+ function Elapsed(props) {
58
+ useAnimation({ interval: 1000 });
59
+ const ms = Date.now() - props.since;
60
+ if (props.hideUnderMs && ms < props.hideUnderMs) {
61
+ return null;
62
+ }
63
+ return _jsx(Text, { dimColor: true, children: formatDuration(ms) });
64
+ }
65
+ function shortTokens(tokens) {
66
+ if (tokens >= 1000000) {
67
+ return `${(tokens / 1000000).toFixed(1)}M`;
68
+ }
69
+ if (tokens >= 1000) {
70
+ return `${Math.round(tokens / 1000)}k`;
71
+ }
72
+ return String(tokens);
73
+ }
74
+ function lastLines(text, count) {
75
+ const lines = text.replace(/\s+$/, "").split("\n");
76
+ return lines.slice(-count).join("\n");
77
+ }
78
+ function firstLine(text, max = 200) {
79
+ return (text.split("\n")[0] || "").slice(0, max);
80
+ }
81
+ function tildify(file) {
82
+ const home = os.homedir();
83
+ if (file === home || file.startsWith(home + path.sep)) {
84
+ return `~${file.slice(home.length)}`;
85
+ }
86
+ return file;
87
+ }
88
+ // Líneas añadidas y quitadas de un diff
89
+ function diffStats(display) {
90
+ if (!display) {
91
+ return null;
92
+ }
93
+ let added = 0;
94
+ let removed = 0;
95
+ for (const line of display.split("\n")) {
96
+ if (line.startsWith("+") && !line.startsWith("+++")) {
97
+ added++;
98
+ }
99
+ else if (line.startsWith("-") && !line.startsWith("---")) {
100
+ removed++;
101
+ }
102
+ }
103
+ if (added === 0 && removed === 0) {
104
+ return null;
105
+ }
106
+ return { added, removed };
107
+ }
108
+ // Debajo de cada herramienta, un resumen corto de lo que pasó
109
+ function toolDetail(event) {
110
+ const output = event.output || "";
111
+ if (event.isError) {
112
+ return [pc.red(firstLine(output))];
113
+ }
114
+ if (event.display) {
115
+ const lines = colorPreview(event.display).split("\n");
116
+ if (lines.length > 14) {
117
+ return [...lines.slice(0, 14), pc.dim(`… ${lines.length - 14} líneas más del diff`)];
118
+ }
119
+ return lines;
120
+ }
121
+ const count = output.split("\n").filter((line) => line.trim() !== "").length;
122
+ switch (event.name) {
123
+ case "read_file":
124
+ if (output.startsWith("Imagen")) {
125
+ return [pc.dim(firstLine(output))];
126
+ }
127
+ return [pc.dim(`Leídas ${output.split("\n").length} líneas`)];
128
+ case "glob":
129
+ case "grep":
130
+ if (/^Sin (resultados|coincidencias)/.test(output)) {
131
+ return [pc.dim(firstLine(output))];
132
+ }
133
+ return [pc.dim(`${count} resultados`)];
134
+ case "list_dir":
135
+ return [pc.dim(`${count} entradas`)];
136
+ case "bash": {
137
+ const lines = output.split("\n");
138
+ const header = lines[0] || "";
139
+ const body = lines.slice(1).filter((line) => line.trim() !== "");
140
+ const tail = body.slice(-4).map((line) => pc.dim(line.slice(0, 200)));
141
+ const ok = header === "Código de salida: 0";
142
+ return [ok ? pc.dim(header) : pc.yellow(header), ...tail];
143
+ }
144
+ case "webfetch":
145
+ return [pc.dim(`${output.length} caracteres`)];
146
+ case "websearch":
147
+ return [pc.dim(`${(output.match(/^\d+\. /gm) || []).length} resultados`)];
148
+ case "todowrite":
149
+ return output
150
+ .split("\n")
151
+ .slice(1)
152
+ .map((line) => pc.dim(line));
153
+ case "task":
154
+ return output
155
+ .split("\n")
156
+ .filter((line) => line.trim() !== "")
157
+ .slice(0, 3)
158
+ .map((line) => pc.dim(line.slice(0, 200)));
159
+ case "skill":
160
+ return [pc.dim(event.summary.includes("·") ? "fichero leído" : "instrucciones cargadas")];
161
+ default:
162
+ return [pc.dim(firstLine(output))];
163
+ }
164
+ }
165
+ function ToolHeader(props) {
166
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: TOOL_LABEL[props.name] || props.name }), props.summary ? _jsxs(Text, { children: [" ", props.summary] }) : null] }));
167
+ }
168
+ function ToolView(props) {
169
+ const { event } = props;
170
+ const detail = toolDetail(event);
171
+ const stats = event.isError ? null : diffStats(event.display);
172
+ return (_jsxs(Box, { flexDirection: "column", marginLeft: event.depth > 0 ? 4 : 0, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: event.isError ? "red" : "green", children: "\u25CF " }), _jsx(ToolHeader, { name: event.name, summary: event.summary }), stats ? (_jsxs(Text, { children: [" ", _jsxs(Text, { color: "green", children: ["+", stats.added] }), " ", _jsxs(Text, { color: "red", children: ["\u2212", stats.removed] })] })) : null, props.ms >= 1000 ? _jsxs(Text, { dimColor: true, children: [" (", formatDuration(props.ms), ")"] }) : null] }), detail.map((line, index) => (_jsxs(Text, { children: [" ", index === 0 ? pc.dim("⎿ ") : " ", line] }, index)))] }));
173
+ }
174
+ // Pantalla de bienvenida
175
+ function Banner(props) {
176
+ const { app } = props;
177
+ const slots = app.router.slots;
178
+ const providers = new Set(slots.map((slot) => slot.provider.name)).size;
179
+ const first = slots[0];
180
+ const extras = [];
181
+ if (app.skills.length > 0) {
182
+ extras.push(`${app.skills.length} skill${app.skills.length > 1 ? "s" : ""}`);
183
+ }
184
+ const mcp = Object.keys(app.config.mcp || {}).length;
185
+ if (mcp > 0) {
186
+ extras.push(`${mcp} MCP`);
187
+ }
188
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 2, children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, color: "magenta", children: "\u25C6 vipcode" }), _jsxs(Text, { dimColor: true, children: [" v", VERSION, " \u00B7 programa con IAs gratuitas"] })] }), _jsx(Text, { dimColor: true, children: tildify(app.root) }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: slots.length > 0 ? "green" : "red", children: "\u25CF" }), " ", slots.length, " modelo", slots.length === 1 ? "" : "s", " de ", providers, " proveedor", providers === 1 ? "" : "es", extras.length > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 ", extras.join(" · ")] }) : null] }), first ? (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2605" }), " Empieza con ", _jsx(Text, { bold: true, children: first.key }), first.score !== null ? _jsxs(Text, { dimColor: true, children: [" (nota ", first.score, " en LLM Stats)"] }) : null] })) : null, _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: "Ctrl+P comandos \u00B7 @ adjunta \u00B7 ! terminal \u00B7 Tab cambia de agente \u00B7 /help ayuda" })] }));
189
+ }
190
+ function ItemView(props) {
191
+ const { item, width } = props;
192
+ switch (item.kind) {
193
+ case "banner":
194
+ return _jsx(Banner, { app: props.app });
195
+ case "user": {
196
+ const lines = item.text.split("\n");
197
+ let text = item.text;
198
+ if (lines.length > 20) {
199
+ text = `${lines.slice(0, 20).join("\n")}\n… (${lines.length - 20} líneas más)`;
200
+ }
201
+ return (_jsx(Box, { marginTop: 1, borderStyle: "bold", borderTop: false, borderRight: false, borderBottom: false, borderColor: item.color, paddingLeft: 1, children: _jsxs(Text, { children: [text, item.images > 0 ? pc.dim(` [${item.images} imagen${item.images > 1 ? "es" : ""}]`) : "", item.queued ? pc.dim(" (en cola: se le entrega al acabar el paso)") : ""] }) }));
202
+ }
203
+ case "assistant":
204
+ return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: item.color, children: "\u25CF " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsx(Text, { children: renderMarkdown(item.text, Math.max(20, width - 4)) }) })] }));
205
+ case "tool":
206
+ return _jsx(ToolView, { event: item.event, ms: item.ms });
207
+ case "answer":
208
+ return (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["\u25C6 ", item.slot, item.score !== null ? ` · nota ${item.score}` : "", " \u00B7 ", formatDuration(item.ms), item.tokens > 0 ? ` · ${shortTokens(item.tokens)} tokens` : ""] }) }));
209
+ case "shell": {
210
+ const lines = item.output ? item.output.split("\n") : [];
211
+ const shown = lines.slice(-30);
212
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: [_jsxs(Text, { color: "yellow", bold: true, children: ["$", " "] }), _jsx(Text, { bold: true, children: item.command })] }), lines.length > shown.length ? _jsxs(Text, { dimColor: true, children: [" \u2026 ", lines.length - shown.length, " l\u00EDneas antes"] }) : null, shown.length > 0 ? _jsx(Text, { children: shown.map((line) => ` ${line}`).join("\n") }) : null, _jsxs(Text, { dimColor: true, children: [" ", item.code === 0 ? "✓" : "✗", " c\u00F3digo ", item.code ?? "?", " \u00B7 el agente ver\u00E1 la salida con tu siguiente mensaje"] })] }));
213
+ }
214
+ case "info":
215
+ return _jsx(Text, { dimColor: true, children: item.text });
216
+ case "notice":
217
+ return _jsxs(Text, { color: "yellow", children: ["\u26A0 ", item.text] });
218
+ case "error":
219
+ return _jsxs(Text, { color: "red", children: ["\u2716 ", item.text] });
220
+ case "block":
221
+ return (_jsx(Box, { marginY: 1, children: _jsx(Text, { children: item.text }) }));
222
+ }
223
+ }
224
+ function TodoPanel(props) {
225
+ const open = props.todos.some((todo) => todo.status === "pending" || todo.status === "in_progress");
226
+ if (!open) {
227
+ return null;
228
+ }
229
+ const done = props.todos.filter((todo) => todo.status === "completed").length;
230
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingX: 1, children: [_jsxs(Text, { dimColor: true, children: ["Tareas (", done, "/", props.todos.length, ")"] }), props.todos.slice(0, 10).map((todo, index) => {
231
+ if (todo.status === "completed") {
232
+ return (_jsxs(Text, { color: "green", strikethrough: true, children: ["\u2611 ", todo.content] }, index));
233
+ }
234
+ if (todo.status === "in_progress") {
235
+ return (_jsxs(Text, { color: "yellow", children: ["\u25D0 ", todo.content] }, index));
236
+ }
237
+ if (todo.status === "cancelled") {
238
+ return (_jsxs(Text, { dimColor: true, strikethrough: true, children: ["\u2612 ", todo.content] }, index));
239
+ }
240
+ return _jsxs(Text, { children: ["\u2610 ", todo.content] }, index);
241
+ }), props.todos.length > 10 ? _jsxs(Text, { dimColor: true, children: ["\u2026 y ", props.todos.length - 10, " m\u00E1s"] }) : null] }));
242
+ }
243
+ function InputBox(props) {
244
+ const { editor } = props;
245
+ // Con ! delante es un comando para la terminal
246
+ const shell = editor.value.startsWith("!");
247
+ const color = shell ? "yellow" : props.color;
248
+ const lines = editor.value.split("\n");
249
+ const { line, column } = E.cursorPosition(editor);
250
+ // Con textos pegados muy largos solo se ve una ventana alrededor del cursor (el cuadro no puede pasar de la pantalla)
251
+ const start = Math.max(0, Math.min(line - Math.floor(props.maxLines / 2), lines.length - props.maxLines));
252
+ const end = Math.min(lines.length, start + props.maxLines);
253
+ const maxChars = Math.max(200, props.width * 3);
254
+ const clip = (text, from) => text.slice(from, from + maxChars);
255
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: color, paddingX: 1, marginTop: 1, children: [props.attachments > 0 ? (_jsxs(Text, { color: "cyan", children: [Array.from({ length: props.attachments }, (_, i) => `[Imagen ${i + 1}]`).join(" "), _jsx(Text, { dimColor: true, children: " (se env\u00EDan con el pr\u00F3ximo mensaje)" })] })) : null, start > 0 ? _jsxs(Text, { dimColor: true, children: [" \u2191 ", start, " l\u00EDneas m\u00E1s"] }) : null, lines.slice(start, end).map((text, offset) => {
256
+ const index = start + offset;
257
+ // En líneas larguísimas se enseña el trozo donde está el cursor
258
+ const from = index === line ? Math.max(0, column - Math.floor(maxChars / 2)) : 0;
259
+ return (_jsxs(Text, { children: [_jsx(Text, { color: color, children: index === 0 ? (shell ? "$ " : "❯ ") : " " }), editor.value === "" ? (_jsxs(_Fragment, { children: [_jsx(Text, { inverse: true, children: " " }), _jsx(Text, { dimColor: true, children: props.placeholder })] })) : index === line ? (_jsxs(_Fragment, { children: [from > 0 ? "…" : "", text.slice(from, column), _jsx(Text, { inverse: true, children: text[column] || " " }), clip(text, column + 1).slice(0, maxChars - (column - from))] })) : (clip(text, 0) + (text.length > maxChars ? "…" : ""))] }, index));
260
+ }), end < lines.length ? _jsxs(Text, { dimColor: true, children: [" \u2193 ", lines.length - end, " l\u00EDneas m\u00E1s"] }) : null, shell ? _jsxs(Text, { dimColor: true, children: ["Enter lo ejecuta en ", tildify(process.cwd()), " \u00B7 el agente ver\u00E1 la salida"] }) : null] }));
261
+ }
262
+ // Medidor de contexto: ▰▰▰▱▱▱▱▱ 34 %
263
+ function ContextGauge(props) {
264
+ if (props.used <= 0 || props.total <= 0) {
265
+ return null;
266
+ }
267
+ const ratio = Math.min(1, props.used / props.total);
268
+ const filled = Math.min(8, Math.ceil(ratio * 8));
269
+ let color = "green";
270
+ if (ratio >= 0.85) {
271
+ color = "red";
272
+ }
273
+ else if (ratio >= 0.6) {
274
+ color = "yellow";
275
+ }
276
+ return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: " \u00B7 " }), _jsx(Text, { color: color, children: "▰".repeat(filled) }), _jsxs(Text, { dimColor: true, children: ["▱".repeat(8 - filled), " ", Math.round(ratio * 100), "% de ", shortTokens(props.total)] })] }));
277
+ }
278
+ function StatusBar(props) {
279
+ const { app } = props;
280
+ const agent = app.agents.find((item) => item.name === props.agentName);
281
+ const current = props.model || app.router.pinned || app.agent.lastSlot;
282
+ const slot = app.router.slotFor(current);
283
+ const context = app.router.slotFor(app.agent.lastSlot)?.model.context || 0;
284
+ const available = app.router.availableCount();
285
+ const usage = app.agent.session.usage;
286
+ const spent = usage.prompt + usage.completion;
287
+ const queued = app.agent.queue.length;
288
+ let hint = "ctrl+p comandos · /help";
289
+ if (props.busy) {
290
+ hint = "esc interrumpe · escribe para añadir instrucciones";
291
+ }
292
+ return (_jsxs(Box, { justifyContent: "space-between", paddingX: 1, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: agent?.color || "magenta", children: props.agentName }), _jsxs(Text, { dimColor: true, children: [" ", "\u00B7 ", current || "rotación automática", slot && slot.score !== null ? ` ★${slot.score}` : "", app.router.pinned ? " (fijado)" : ""] }), _jsx(ContextGauge, { used: app.agent.lastTokens, total: context }), _jsxs(Text, { dimColor: true, children: [" ", "\u00B7 ", available, "/", app.router.slots.length, " modelos", spent > 0 ? ` · ${shortTokens(spent)} tokens` : "", queued > 0 ? ` · ${queued} en cola` : ""] }), app.yolo ? _jsx(Text, { color: "red", children: " \u00B7 yolo" }) : null] }), _jsx(Text, { dimColor: true, wrap: "truncate-start", children: hint })] }));
293
+ }
294
+ function Main(props) {
295
+ const { app } = props;
296
+ const { exit } = useApp();
297
+ const { columns, rows } = useWindowSize();
298
+ const [items, setItems] = useState([{ id: 0, kind: "banner" }]);
299
+ const nextId = useRef(1);
300
+ const [editor, setEditor] = useState(E.EMPTY);
301
+ const [busy, setBusy] = useState(false);
302
+ const [dialog, setDialog] = useState(null);
303
+ const [todos, setTodos] = useState(app.agent.session.todos);
304
+ const [attachments, setAttachments] = useState([]);
305
+ const [selectedSuggestion, setSelectedSuggestion] = useState(0);
306
+ const [suggestionsClosed, setSuggestionsClosed] = useState(false);
307
+ const [agentName, setAgentName] = useState(app.agent.agent.name);
308
+ const [, setVersion] = useState(0);
309
+ const live = useRef({ text: "", reasoning: "", running: new Map(), model: "", shell: null });
310
+ const dirty = useRef(false);
311
+ const controller = useRef(null);
312
+ const startedAt = useRef(Date.now());
313
+ // Para el pie de cada respuesta: modelo que contestó y tokens al empezar
314
+ const answered = useRef("");
315
+ const tokensAtStart = useRef(0);
316
+ const history = useRef(loadHistory());
317
+ const historyIndex = useRef(null);
318
+ const draft = useRef("");
319
+ const lastCtrlC = useRef(0);
320
+ const dialogRef = useRef(null);
321
+ dialogRef.current = dialog;
322
+ const refresh = () => setVersion((v) => v + 1);
323
+ const push = (item) => {
324
+ const id = nextId.current++;
325
+ setItems((previous) => [...previous, { ...item, id }]);
326
+ };
327
+ const agentColor = () => app.agents.find((item) => item.name === app.agent.agent.name)?.color || "magenta";
328
+ // Pasa el texto en directo al historial fijo de arriba
329
+ const flushText = () => {
330
+ const text = live.current.text;
331
+ if (text.trim() !== "") {
332
+ push({ kind: "assistant", text, color: agentColor() });
333
+ }
334
+ live.current.text = "";
335
+ live.current.reasoning = "";
336
+ dirty.current = true;
337
+ };
338
+ const adapter = useMemo(() => ({
339
+ info: (text) => push({ kind: "info", text }),
340
+ notice: (text) => push({ kind: "notice", text }),
341
+ error: (text) => push({ kind: "error", text }),
342
+ block: (text) => push({ kind: "block", text }),
343
+ select: (title, list) => new Promise((resolve) => {
344
+ setDialog({
345
+ kind: "select",
346
+ title,
347
+ items: list,
348
+ resolve: (value) => {
349
+ setDialog(null);
350
+ resolve(value);
351
+ },
352
+ });
353
+ }),
354
+ prompt: (title, options) => new Promise((resolve) => {
355
+ setDialog({
356
+ kind: "prompt",
357
+ title,
358
+ mask: options?.mask,
359
+ placeholder: options?.placeholder,
360
+ resolve: (value) => {
361
+ setDialog(null);
362
+ resolve(value);
363
+ },
364
+ });
365
+ }),
366
+ setInput: (text) => setEditor({ value: text, cursor: text.length }),
367
+ }), []);
368
+ // Si se interrumpe con un permiso pendiente, se contesta que no para que el agente no se quede esperando
369
+ const cancelDialog = () => {
370
+ const current = dialogRef.current;
371
+ if (!current) {
372
+ return;
373
+ }
374
+ if (current.kind === "permission") {
375
+ current.resolve({ reply: { allow: false } });
376
+ }
377
+ else if (current.kind === "select") {
378
+ current.resolve(null);
379
+ }
380
+ else {
381
+ current.resolve(null);
382
+ }
383
+ setDialog(null);
384
+ };
385
+ const quit = async () => {
386
+ controller.current?.abort();
387
+ cancelDialog();
388
+ await app.shutdown();
389
+ exit();
390
+ setTimeout(() => process.exit(0), 100);
391
+ };
392
+ const sessionTokens = () => app.agent.session.usage.prompt + app.agent.session.usage.completion;
393
+ const runAgent = async (input, options) => {
394
+ setBusy(true);
395
+ startedAt.current = Date.now();
396
+ answered.current = "";
397
+ tokensAtStart.current = sessionTokens();
398
+ live.current.model = "";
399
+ const abort = new AbortController();
400
+ controller.current = abort;
401
+ try {
402
+ await app.runPrompt(input, options, abort.signal);
403
+ }
404
+ catch (err) {
405
+ if (abort.signal.aborted) {
406
+ push({ kind: "info", text: "(interrumpido)" });
407
+ }
408
+ else if (err instanceof NoSlotError) {
409
+ push({ kind: "error", text: err.message });
410
+ }
411
+ else {
412
+ push({ kind: "error", text: err.message || String(err) });
413
+ }
414
+ }
415
+ finally {
416
+ flushText();
417
+ if (answered.current) {
418
+ push({
419
+ kind: "answer",
420
+ slot: answered.current,
421
+ score: app.router.slotFor(answered.current)?.score ?? null,
422
+ ms: Date.now() - startedAt.current,
423
+ tokens: sessionTokens() - tokensAtStart.current,
424
+ });
425
+ }
426
+ live.current.running.clear();
427
+ live.current.model = "";
428
+ controller.current = null;
429
+ setBusy(false);
430
+ setAgentName(app.agent.agent.name);
431
+ setTodos([...app.agent.session.todos]);
432
+ refresh();
433
+ // Lo que se escribió al final y no llegó a entregarse se lanza ahora
434
+ const next = app.agent.queue.shift();
435
+ if (next) {
436
+ void runAgent(next, {});
437
+ }
438
+ }
439
+ };
440
+ // !comando: se ejecuta en la carpeta del proyecto y la salida le llega al agente con el siguiente mensaje
441
+ const runShell = async (command) => {
442
+ setBusy(true);
443
+ startedAt.current = Date.now();
444
+ const abort = new AbortController();
445
+ controller.current = abort;
446
+ live.current.shell = { command, output: "" };
447
+ try {
448
+ const result = await app.runShell(command, (text) => {
449
+ if (live.current.shell) {
450
+ live.current.shell.output = (live.current.shell.output + text).slice(-4000);
451
+ dirty.current = true;
452
+ }
453
+ }, abort.signal);
454
+ push({ kind: "shell", command, output: result.output, code: result.code });
455
+ }
456
+ finally {
457
+ live.current.shell = null;
458
+ controller.current = null;
459
+ setBusy(false);
460
+ refresh();
461
+ }
462
+ };
463
+ const submit = async (raw) => {
464
+ const text = raw.trim();
465
+ if (text === "" && attachments.length === 0) {
466
+ return;
467
+ }
468
+ setEditor(E.EMPTY);
469
+ historyIndex.current = null;
470
+ if (text !== "") {
471
+ appendHistory(text);
472
+ history.current.push(text);
473
+ }
474
+ if (text.startsWith("!")) {
475
+ const command = text.slice(1).trim();
476
+ if (!command) {
477
+ return;
478
+ }
479
+ if (busy) {
480
+ push({ kind: "error", text: "Espera a que termine (o pulsa Esc) para ejecutar un comando." });
481
+ return;
482
+ }
483
+ void runShell(command);
484
+ return;
485
+ }
486
+ const images = attachments;
487
+ setAttachments([]);
488
+ if (text.startsWith("/")) {
489
+ const command = text.split(/\s+/)[0];
490
+ if (busy && !SAFE_WHILE_BUSY.includes(command)) {
491
+ push({ kind: "error", text: "Espera a que termine (o pulsa Esc) para usar ese comando." });
492
+ return;
493
+ }
494
+ push({ kind: "info", text: `❯ ${text}` });
495
+ try {
496
+ const result = await app.handleCommand(text, adapter);
497
+ setAgentName(app.agent.agent.name);
498
+ setTodos([...app.agent.session.todos]);
499
+ refresh();
500
+ if (result.exit) {
501
+ await quit();
502
+ return;
503
+ }
504
+ if (result.run) {
505
+ void runAgent({ ...result.run.input, images }, result.run.options);
506
+ }
507
+ }
508
+ catch (err) {
509
+ push({ kind: "error", text: err.message });
510
+ }
511
+ return;
512
+ }
513
+ if (busy) {
514
+ app.agent.queue.push({ text, images });
515
+ push({ kind: "user", text, images: images.length, queued: true, color: agentColor() });
516
+ return;
517
+ }
518
+ push({ kind: "user", text, images: images.length, color: agentColor() });
519
+ void runAgent({ text, images }, {});
520
+ };
521
+ // La primera vez que se van a usar proveedores sin clave, explicamos qué pasa con lo que envías
522
+ const askPrivacy = async () => {
523
+ const choice = await adapter.select(`Antes de empezar: ${app.privacyNotice()}`, [
524
+ { label: "Entendido, empezar", value: "ok" },
525
+ { label: "Añadir antes un proveedor con clave", description: "como /connect", value: "connect" },
526
+ { label: "No usar nunca los que no piden clave", description: 'guarda "keyless": false en tu configuración', value: "off" },
527
+ ]);
528
+ if (!choice) {
529
+ // Con Esc se vuelve a preguntar la próxima vez
530
+ return;
531
+ }
532
+ app.acceptPrivacyNotice();
533
+ if (choice === "connect") {
534
+ await app.connect(adapter, "");
535
+ }
536
+ else if (choice === "off") {
537
+ app.disableKeyless();
538
+ push({ kind: "info", text: "No usaré los proveedores sin clave (lo puedes cambiar con \"keyless\" en la configuración)." });
539
+ if (app.router.slots.length === 0) {
540
+ await app.connect(adapter, "");
541
+ }
542
+ }
543
+ refresh();
544
+ };
545
+ // Paleta de comandos (Ctrl+P): todos los comandos, skills y agentes con su descripción
546
+ const openPalette = async () => {
547
+ const commands = app.commandSuggestions();
548
+ const choice = await adapter.select("Comandos y skills", commands.map((command) => ({ label: `/${command.name}`, description: command.description, value: command })));
549
+ if (!choice) {
550
+ return;
551
+ }
552
+ if (choice.needsArgs) {
553
+ const value = `/${choice.name} `;
554
+ setEditor({ value, cursor: value.length });
555
+ return;
556
+ }
557
+ void submit(`/${choice.name}`);
558
+ };
559
+ // Conectamos el agente con la pantalla
560
+ useEffect(() => {
561
+ app.agent.events = {
562
+ onText: (chunk, depth) => {
563
+ if (depth > 0) {
564
+ return;
565
+ }
566
+ live.current.text += chunk;
567
+ live.current.reasoning = "";
568
+ dirty.current = true;
569
+ },
570
+ onReasoning: (chunk, depth) => {
571
+ if (depth > 0 || !app.config.showReasoning) {
572
+ return;
573
+ }
574
+ live.current.reasoning = (live.current.reasoning + chunk).slice(-600);
575
+ dirty.current = true;
576
+ },
577
+ onTextEnd: (depth) => {
578
+ if (depth === 0) {
579
+ flushText();
580
+ }
581
+ },
582
+ onToolStart: (event) => {
583
+ flushText();
584
+ live.current.running.set(`${event.depth}:${event.id}`, { event, progress: "", since: Date.now() });
585
+ dirty.current = true;
586
+ },
587
+ onToolProgress: (event, text) => {
588
+ const entry = live.current.running.get(`${event.depth}:${event.id}`);
589
+ if (entry) {
590
+ entry.progress = (entry.progress + text).slice(-2000);
591
+ dirty.current = true;
592
+ }
593
+ },
594
+ onToolEnd: (event) => {
595
+ const key = `${event.depth}:${event.id}`;
596
+ const since = live.current.running.get(key)?.since || Date.now();
597
+ live.current.running.delete(key);
598
+ push({ kind: "tool", event, ms: Date.now() - since });
599
+ dirty.current = true;
600
+ },
601
+ onModel: (slot, depth) => {
602
+ if (depth === 0) {
603
+ live.current.model = slot;
604
+ dirty.current = true;
605
+ }
606
+ },
607
+ onAnswer: (slot) => {
608
+ answered.current = slot;
609
+ dirty.current = true;
610
+ },
611
+ onNotice: (message) => {
612
+ flushText();
613
+ const text = message.trim();
614
+ // El router avisa cuando cambia de modelo: no es un problema, solo información
615
+ if (text.startsWith("usando ")) {
616
+ push({ kind: "info", text: `↻ sigue con ${text.slice("usando ".length)}` });
617
+ return;
618
+ }
619
+ push({ kind: "notice", text });
620
+ },
621
+ onInfo: (message) => {
622
+ if (message.trim().startsWith("↳")) {
623
+ return;
624
+ }
625
+ push({ kind: "info", text: message });
626
+ },
627
+ onTodos: (list) => setTodos([...list]),
628
+ onStep: () => refresh(),
629
+ };
630
+ app.promptPermission = (request, agent) => new Promise((resolve) => {
631
+ setDialog({
632
+ kind: "permission",
633
+ request,
634
+ agent: agent.name,
635
+ resolve: (choice) => {
636
+ if (choice.grant === "kind") {
637
+ app.grants.allowKind(request.kind);
638
+ }
639
+ else if (choice.grant === "pattern" && choice.pattern) {
640
+ app.grants.allowPattern("bash", choice.pattern);
641
+ }
642
+ setDialog(null);
643
+ resolve(choice.reply);
644
+ },
645
+ });
646
+ });
647
+ const timer = setInterval(() => {
648
+ if (dirty.current) {
649
+ dirty.current = false;
650
+ refresh();
651
+ }
652
+ }, 60);
653
+ // Tareas de arranque en segundo plano
654
+ let starting = Promise.resolve();
655
+ for (const notice of configNotices) {
656
+ push({ kind: "notice", text: notice });
657
+ }
658
+ const session = app.agent.session;
659
+ if (session.messages.length > 0) {
660
+ push({ kind: "info", text: `Continúas «${session.title}» (${session.turns.length} mensajes tuyos). /new empieza otra.` });
661
+ }
662
+ if (app.router.slots.length === 0) {
663
+ push({
664
+ kind: "notice",
665
+ text: "Todavía no tienes ningún modelo. Elige un proveedor gratuito y pega su clave (puedes repetirlo con /connect).",
666
+ });
667
+ void app.connect(adapter, "").then(refresh);
668
+ }
669
+ else if (app.needsPrivacyNotice()) {
670
+ starting = askPrivacy();
671
+ }
672
+ else if (app.onlyAnonymous()) {
673
+ push({ kind: "info", text: app.anonymousNotice() });
674
+ }
675
+ void app.startMcp(refresh).then(() => {
676
+ for (const server of app.mcp?.status() || []) {
677
+ if (server.state === "error") {
678
+ push({ kind: "notice", text: `MCP ${server.name}: ${server.error}` });
679
+ }
680
+ }
681
+ refresh();
682
+ });
683
+ // Una vez al día mira en GitHub si hay una versión nueva de vipcode
684
+ if (app.config.checkUpdates !== false) {
685
+ void checkForUpdate().then((version) => {
686
+ if (version) {
687
+ push({ kind: "notice", text: `Hay una versión nueva de vipcode (${version}, tienes la ${VERSION}). Escribe /update para instalarla.` });
688
+ }
689
+ });
690
+ }
691
+ if (app.config.autoUpdateModels && discoveryIsStale()) {
692
+ void app
693
+ .refreshModels()
694
+ .then((summary) => {
695
+ const added = summary.updated.reduce((total, entry) => total + entry.added.length, 0);
696
+ if (added > 0) {
697
+ push({ kind: "info", text: `He encontrado ${added} modelos gratis nuevos (/models para verlos).` });
698
+ }
699
+ refresh();
700
+ })
701
+ .catch(() => { });
702
+ }
703
+ void app.snapshots.cleanup().catch(() => { });
704
+ // El mensaje con el que se abrió (vipcode "tarea") espera a que contestes el aviso de privacidad
705
+ void starting.then(() => {
706
+ if (props.initial.trim() !== "") {
707
+ void submit(props.initial);
708
+ }
709
+ });
710
+ return () => clearInterval(timer);
711
+ }, []);
712
+ const suggestions = useMemo(() => {
713
+ if (dialog || suggestionsClosed) {
714
+ return [];
715
+ }
716
+ const before = editor.value.slice(0, editor.cursor);
717
+ if (editor.value.startsWith("/") && !before.includes(" ") && !editor.value.includes("\n")) {
718
+ const typed = before.slice(1).toLowerCase();
719
+ return app
720
+ .commandSuggestions()
721
+ .filter((command) => command.name.startsWith(typed))
722
+ .slice(0, 8)
723
+ .map((command) => ({
724
+ label: `/${command.name}`,
725
+ description: command.description,
726
+ apply: () => {
727
+ const value = `/${command.name} `;
728
+ return { value, cursor: value.length };
729
+ },
730
+ submit: !command.needsArgs,
731
+ }));
732
+ }
733
+ const mention = E.mentionAtCursor(editor);
734
+ if (mention) {
735
+ return suggestFiles(app.root, mention.text).map((file) => ({
736
+ label: `@${file}`,
737
+ apply: (state) => E.replaceRange(state, mention.start, state.cursor, `@${file}${file.endsWith("/") ? "" : " "}`),
738
+ submit: false,
739
+ }));
740
+ }
741
+ return [];
742
+ }, [editor, dialog, suggestionsClosed]);
743
+ const suggestionsVisible = suggestions.length > 0;
744
+ const selected = Math.min(selectedSuggestion, Math.max(0, suggestions.length - 1));
745
+ const update = (state) => {
746
+ setEditor(state);
747
+ setSuggestionsClosed(false);
748
+ setSelectedSuggestion(0);
749
+ };
750
+ const accept = (andSubmit) => {
751
+ const suggestion = suggestions[selected];
752
+ if (!suggestion) {
753
+ return;
754
+ }
755
+ const next = suggestion.apply(editor);
756
+ if (andSubmit && suggestion.submit) {
757
+ void submit(next.value);
758
+ return;
759
+ }
760
+ update(next);
761
+ if (!suggestion.label.endsWith("/")) {
762
+ setSuggestionsClosed(true);
763
+ }
764
+ };
765
+ const attachImage = (dataUrl, label) => {
766
+ setAttachments((previous) => [...previous, dataUrl]);
767
+ push({ kind: "info", text: `Imagen adjuntada: ${label}` });
768
+ };
769
+ useInput((input, key) => {
770
+ // Ctrl+C funciona siempre: interrumpe, cierra el diálogo, borra lo escrito o sale
771
+ if (key.ctrl && input === "c") {
772
+ if (busy && controller.current) {
773
+ controller.current.abort();
774
+ cancelDialog();
775
+ return;
776
+ }
777
+ if (dialogRef.current) {
778
+ cancelDialog();
779
+ return;
780
+ }
781
+ if (editor.value !== "") {
782
+ update(E.EMPTY);
783
+ return;
784
+ }
785
+ if (Date.now() - lastCtrlC.current < 1500) {
786
+ void quit();
787
+ return;
788
+ }
789
+ lastCtrlC.current = Date.now();
790
+ push({ kind: "info", text: "(pulsa Ctrl+C otra vez para salir)" });
791
+ return;
792
+ }
793
+ if (dialogRef.current) {
794
+ // Los diálogos tienen su propio teclado; Esc en el de permisos ya contesta que no
795
+ return;
796
+ }
797
+ if (key.escape) {
798
+ if (suggestionsVisible) {
799
+ setSuggestionsClosed(true);
800
+ }
801
+ else if (busy && controller.current) {
802
+ controller.current.abort();
803
+ }
804
+ return;
805
+ }
806
+ // Algunas terminales mandan \n en vez de \r al pulsar Enter
807
+ if (key.return || input === "\n") {
808
+ if (key.meta || key.shift) {
809
+ update(E.insert(editor, "\n"));
810
+ return;
811
+ }
812
+ if (suggestionsVisible) {
813
+ accept(true);
814
+ return;
815
+ }
816
+ if (editor.value.endsWith("\\") && editor.cursor === editor.value.length) {
817
+ update({ value: `${editor.value.slice(0, -1)}\n`, cursor: editor.value.length });
818
+ return;
819
+ }
820
+ void submit(editor.value);
821
+ return;
822
+ }
823
+ if (key.tab) {
824
+ if (suggestionsVisible) {
825
+ accept(false);
826
+ return;
827
+ }
828
+ if (!busy) {
829
+ const next = app.cycleAgent(key.shift ? -1 : 1);
830
+ setAgentName(next.name);
831
+ }
832
+ return;
833
+ }
834
+ if (key.upArrow) {
835
+ if (suggestionsVisible) {
836
+ setSelectedSuggestion(Math.max(0, selected - 1));
837
+ return;
838
+ }
839
+ const moved = E.moveVertical(editor, -1);
840
+ if (moved) {
841
+ setEditor(moved);
842
+ return;
843
+ }
844
+ const list = history.current;
845
+ if (list.length === 0) {
846
+ return;
847
+ }
848
+ if (historyIndex.current === null) {
849
+ draft.current = editor.value;
850
+ historyIndex.current = list.length;
851
+ }
852
+ if (historyIndex.current > 0) {
853
+ historyIndex.current--;
854
+ const value = list[historyIndex.current];
855
+ setEditor({ value, cursor: value.length });
856
+ setSuggestionsClosed(true);
857
+ }
858
+ return;
859
+ }
860
+ if (key.downArrow) {
861
+ if (suggestionsVisible) {
862
+ setSelectedSuggestion(Math.min(suggestions.length - 1, selected + 1));
863
+ return;
864
+ }
865
+ const moved = E.moveVertical(editor, 1);
866
+ if (moved) {
867
+ setEditor(moved);
868
+ return;
869
+ }
870
+ if (historyIndex.current === null) {
871
+ return;
872
+ }
873
+ historyIndex.current++;
874
+ if (historyIndex.current >= history.current.length) {
875
+ historyIndex.current = null;
876
+ setEditor({ value: draft.current, cursor: draft.current.length });
877
+ }
878
+ else {
879
+ const value = history.current[historyIndex.current];
880
+ setEditor({ value, cursor: value.length });
881
+ }
882
+ setSuggestionsClosed(true);
883
+ return;
884
+ }
885
+ if (key.leftArrow) {
886
+ setEditor(E.moveLeft(editor, key.meta || key.ctrl));
887
+ return;
888
+ }
889
+ if (key.rightArrow) {
890
+ setEditor(E.moveRight(editor, key.meta || key.ctrl));
891
+ return;
892
+ }
893
+ if (key.home || (key.ctrl && input === "a")) {
894
+ setEditor(E.lineStart(editor));
895
+ return;
896
+ }
897
+ if (key.end || (key.ctrl && input === "e")) {
898
+ setEditor(E.lineEnd(editor));
899
+ return;
900
+ }
901
+ if (key.backspace) {
902
+ update(key.meta ? E.deleteWordBack(editor) : E.backspace(editor));
903
+ return;
904
+ }
905
+ if (key.delete) {
906
+ update(E.deleteForward(editor));
907
+ return;
908
+ }
909
+ if (key.ctrl) {
910
+ if (input === "u") {
911
+ update(E.deleteToLineStart(editor));
912
+ }
913
+ else if (input === "k") {
914
+ update(E.deleteToLineEnd(editor));
915
+ }
916
+ else if (input === "w") {
917
+ update(E.deleteWordBack(editor));
918
+ }
919
+ else if (input === "j") {
920
+ update(E.insert(editor, "\n"));
921
+ }
922
+ else if (input === "p") {
923
+ void openPalette();
924
+ }
925
+ else if (input === "d") {
926
+ if (editor.value === "") {
927
+ void quit();
928
+ }
929
+ else {
930
+ update(E.deleteForward(editor));
931
+ }
932
+ }
933
+ else if (input === "v") {
934
+ void readClipboardImage().then((image) => {
935
+ if (image) {
936
+ attachImage(image, "portapapeles");
937
+ }
938
+ else {
939
+ push({ kind: "info", text: "No hay ninguna imagen en el portapapeles (para texto usa Cmd+V)." });
940
+ }
941
+ });
942
+ }
943
+ return;
944
+ }
945
+ if (key.meta) {
946
+ if (input === "b") {
947
+ setEditor(E.moveLeft(editor, true));
948
+ }
949
+ else if (input === "f") {
950
+ setEditor(E.moveRight(editor, true));
951
+ }
952
+ return;
953
+ }
954
+ if (input) {
955
+ // Algunas terminales mandan "texto + Enter" de golpe: el Enter final envía, no es un salto de línea
956
+ if (input.length > 1 && /[\r\n]$/.test(input) && !/[\r\n]/.test(input.slice(0, -1))) {
957
+ void submit(E.insert(editor, input.slice(0, -1)).value);
958
+ return;
959
+ }
960
+ update(E.insert(editor, input.replace(/\r\n?/g, "\n")));
961
+ }
962
+ });
963
+ usePaste((text) => {
964
+ // Arrastrar una imagen a la terminal pega su ruta: la adjuntamos como imagen
965
+ const candidate = text.trim().replace(/^['"]|['"]$/g, "").replace(/\\ /g, " ");
966
+ if (isImagePath(candidate)) {
967
+ const file = path.resolve(app.root, candidate);
968
+ if (fs.existsSync(file)) {
969
+ try {
970
+ attachImage(toDataUrl(file), path.basename(file));
971
+ }
972
+ catch (err) {
973
+ push({ kind: "error", text: err.message });
974
+ }
975
+ return;
976
+ }
977
+ }
978
+ update(E.insert(editor, text.replace(/\r\n?/g, "\n")));
979
+ }, { isActive: !dialog });
980
+ const agent = app.agents.find((item) => item.name === agentName);
981
+ const color = agent?.color || "magenta";
982
+ // La zona en directo no puede pasar del alto de la pantalla (Ink no sabe borrar lo que se ha ido hacia arriba)
983
+ const liveRoom = Math.max(4, rows - 14);
984
+ const liveText = live.current.text ? lastLines(renderMarkdown(live.current.text, Math.max(20, columns - 4)), liveRoom) : "";
985
+ const running = [...live.current.running.values()];
986
+ const shell = live.current.shell;
987
+ let dialogView = null;
988
+ if (dialog?.kind === "permission") {
989
+ const lines = dialog.request.preview.split("\n");
990
+ const room = Math.max(6, rows - 16);
991
+ let request = dialog.request;
992
+ if (lines.length > room) {
993
+ request = { ...request, preview: `${lines.slice(0, room).join("\n")}\n… ${lines.length - room} líneas más` };
994
+ }
995
+ dialogView = _jsx(PermissionDialog, { request: request, agent: dialog.agent, onDone: dialog.resolve }, "permission");
996
+ }
997
+ else if (dialog?.kind === "select") {
998
+ dialogView = _jsx(SelectDialog, { title: dialog.title, items: dialog.items, onDone: dialog.resolve }, "select");
999
+ }
1000
+ else if (dialog?.kind === "prompt") {
1001
+ dialogView = _jsx(PromptDialog, { title: dialog.title, mask: dialog.mask, placeholder: dialog.placeholder, onDone: dialog.resolve }, "prompt");
1002
+ }
1003
+ let placeholder = "Escribe tu mensaje… (@ ficheros · / comandos · ! terminal · Ctrl+P paleta)";
1004
+ if (busy) {
1005
+ placeholder = "Escribe para añadir instrucciones mientras trabaja…";
1006
+ }
1007
+ return (_jsxs(_Fragment, { children: [_jsx(Static, { items: items, children: (item) => _jsx(ItemView, { item: item, app: app, width: columns }, item.id) }), _jsxs(Box, { flexDirection: "column", children: [live.current.reasoning ? (_jsxs(Text, { dimColor: true, italic: true, wrap: "truncate-end", children: ["\uD83D\uDCAD ", lastLines(live.current.reasoning.replace(/\s+/g, " "), 1).slice(-Math.max(20, columns - 6))] })) : null, liveText ? (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: color, children: "\u25CF " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsxs(Text, { children: [liveText, _jsx(Text, { color: color, children: "\u258D" })] }) })] })) : null, running.map(({ event, progress, since }) => (_jsxs(Box, { flexDirection: "column", marginLeft: event.depth > 0 ? 4 : 0, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Spinner, {}), " ", _jsx(ToolHeader, { name: event.name, summary: event.summary }), " ", _jsx(Elapsed, { since: since, hideUnderMs: 2000 })] }), progress ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [" ", lastLines(progress, 1).slice(0, Math.max(20, columns - 8))] })) : null] }, `${event.depth}:${event.id}`))), shell ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: [_jsx(Spinner, { color: "yellow" }), " ", _jsx(Text, { bold: true, children: shell.command }), " ", _jsx(Elapsed, { since: startedAt.current, hideUnderMs: 1000 })] }), shell.output ? _jsx(Text, { dimColor: true, children: lastLines(shell.output, Math.min(8, liveRoom)).replace(/^/gm, " ") }) : null] })) : null, _jsx(TodoPanel, { todos: todos }), busy && !shell && running.length === 0 && !liveText && !answered.current ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Spinner, {}), " ", _jsxs(Text, { dimColor: true, children: ["Pensando", live.current.model ? ` con ${live.current.model}` : "", "\u2026 "] }), _jsx(Elapsed, { since: startedAt.current, hideUnderMs: 1000 })] }) })) : null, dialogView || (_jsx(InputBox, { editor: editor, color: color, placeholder: placeholder, attachments: attachments.length, maxLines: Math.max(3, Math.floor(rows / 2) - 4), width: columns })), suggestionsVisible && !dialog ? (_jsx(Box, { flexDirection: "column", paddingX: 2, children: suggestions.map((suggestion, index) => (_jsxs(Text, { color: index === selected ? "cyan" : undefined, wrap: "truncate-end", children: [index === selected ? "❯ " : " ", suggestion.label, suggestion.description ? _jsxs(Text, { dimColor: true, children: [" ", suggestion.description] }) : null] }, `${index}-${suggestion.label}`))) })) : null, _jsx(StatusBar, { app: app, agentName: agentName, busy: busy, model: busy ? live.current.model : "" })] })] }));
1008
+ }
1009
+ export async function startTui(app, options = {}) {
1010
+ const instance = render(_jsx(Main, { app: app, initial: options.initial || "" }), {
1011
+ exitOnCtrlC: false,
1012
+ kittyKeyboard: { mode: "auto" },
1013
+ });
1014
+ await instance.waitUntilExit();
1015
+ }