vipcode 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +2 -0
- package/README.md +11 -2
- package/dist/agent.js +20 -1
- package/dist/app.js +21 -11
- package/dist/index.js +5 -1
- package/dist/labels.js +20 -0
- package/dist/session.js +121 -51
- package/dist/tui/app.js +58 -32
- package/dist/tui/dialogs.js +3 -1
- package/dist/ui.js +30 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -41,6 +41,8 @@ vipcode skills add anthropics/skills # install skills from GitHub
|
|
|
41
41
|
vipcode update # update vipcode
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
Every conversation is saved automatically in `~/.vipcode/sessions/` as `.json` (to resume it) and `.md` (the whole conversation, readable and never truncated by context compaction). When you resume one (`vipcode -c`, `/sessions`), the previous conversation is shown on screen.
|
|
45
|
+
|
|
44
46
|
Inside the UI: Enter sends, Tab switches between the `build` and `plan` agents, Ctrl+P opens the command palette, `@path` attaches files, `!command` runs a shell command, Esc interrupts and `/help` lists everything.
|
|
45
47
|
|
|
46
48
|
## Security
|
package/README.md
CHANGED
|
@@ -118,7 +118,7 @@ vipcode trust # confiar en la configuración que trae esta carpet
|
|
|
118
118
|
- **Con npm** (`npm install -g vipcode` o el paquete de la release): mira cuál es la última versión publicada en npm y en GitHub y, si es más nueva que la tuya, la instala.
|
|
119
119
|
- **Desde el código** (`git clone` + `npm link`): trae lo último del repositorio con `git pull`, instala las dependencias y compila. Si tienes cambios sin guardar en esa carpeta, no toca nada y te avisa.
|
|
120
120
|
|
|
121
|
-
Después cierra vipcode y vuelve a abrirlo. Además, una vez al día vipcode mira si hay una versión nueva y te avisa al abrirlo; se desactiva con `"checkUpdates": false` en la configuración.
|
|
121
|
+
Después cierra vipcode y vuelve a abrirlo. Además, una vez al día vipcode mira si hay una versión nueva y te avisa al abrirlo con un recuadro verde, y te lo recuerda en la barra de abajo hasta que actualizas; se desactiva con `"checkUpdates": false` en la configuración.
|
|
122
122
|
|
|
123
123
|
### En la interfaz
|
|
124
124
|
|
|
@@ -134,6 +134,15 @@ Mientras trabaja ves qué modelo está pensando, cada herramienta con su acción
|
|
|
134
134
|
|
|
135
135
|
Antes de editar un fichero, ejecutar un comando o salir a internet, vipcode pregunta enseñando el diff o el comando. Puedes contestar que sí una vez, que sí a todo lo de ese tipo durante la sesión, que sí siempre a un comando concreto (por ejemplo `npm test*`), que no, o que no y explicarle qué hacer.
|
|
136
136
|
|
|
137
|
+
### Sesiones
|
|
138
|
+
|
|
139
|
+
Cada conversación se guarda sola en `~/.vipcode/sessions/`, en dos ficheros:
|
|
140
|
+
|
|
141
|
+
- **`.json`**, para poder seguirla.
|
|
142
|
+
- **`.md`**, con la conversación entera para leerla o compartirla: lo que escribiste, cada respuesta con el modelo que contestó, las herramientas que usó y los cambios que hizo en cada fichero. Se actualiza solo mientras hablas y no pierde nada aunque la conversación se resuma para que quepa en el modelo.
|
|
143
|
+
|
|
144
|
+
Para volver a una conversación: `vipcode -c` (la última de esta carpeta), `/sessions` (eliges de la lista) o `vipcode -s <id>`. Al volver ves en pantalla toda la conversación anterior, y el modelo tiene su contexto para seguir donde lo dejasteis. `/export` guarda además una copia en `~/.vipcode/exports/`.
|
|
145
|
+
|
|
137
146
|
### Comandos
|
|
138
147
|
|
|
139
148
|
| Comando | Qué hace |
|
|
@@ -363,7 +372,7 @@ Los planes gratuitos tienen letra pequeña: los proveedores anónimos (Kilo, OVH
|
|
|
363
372
|
Todo está en `~/.vipcode` (o en `VIPCODE_HOME` si la defines):
|
|
364
373
|
|
|
365
374
|
- `config.json`: tu configuración. `auth.json`: claves (permisos 600). `state.json`: pausas de los proveedores. `notices.json`: avisos que ya has visto. `update.json`: última comprobación de versiones nuevas. `trusted.json`: carpetas en las que confías.
|
|
366
|
-
- `models.json`: modelos descubiertos. `ranking.json`: puntuaciones de LLM Stats descargadas con tu clave. `sessions/`: conversaciones. `history.jsonl`: lo que has escrito.
|
|
375
|
+
- `models.json`: modelos descubiertos. `ranking.json`: puntuaciones de LLM Stats descargadas con tu clave. `sessions/`: conversaciones (un `.json` para seguirlas y un `.md` para leerlas). `history.jsonl`: lo que has escrito.
|
|
367
376
|
- `snapshots/`: instantáneas para `/undo` (un repositorio git aparte por proyecto; no toca tu `.git`).
|
|
368
377
|
- `exports/`: conversaciones exportadas. `agents/`, `commands/` y `skills/`: tus agentes, comandos y skills globales.
|
|
369
378
|
|
package/dist/agent.js
CHANGED
|
@@ -94,6 +94,10 @@ export class Agent {
|
|
|
94
94
|
addNote(text) {
|
|
95
95
|
this.pendingNote += text;
|
|
96
96
|
}
|
|
97
|
+
// Apunta algo en el historial de la sesión (lo que se enseña al volver y se guarda en el .md)
|
|
98
|
+
logEntry(entry) {
|
|
99
|
+
this.session.log.push({ ...entry, time: Date.now() });
|
|
100
|
+
}
|
|
97
101
|
toolContext(agent, signal, depth) {
|
|
98
102
|
const lsp = this.deps.lsp;
|
|
99
103
|
const ctx = {
|
|
@@ -195,6 +199,7 @@ export class Agent {
|
|
|
195
199
|
{ role: "assistant", content: "Entendido, sigo desde aquí." },
|
|
196
200
|
];
|
|
197
201
|
this.session.epoch++;
|
|
202
|
+
this.logEntry({ role: "info", text: "Conversación resumida para ahorrar contexto: el modelo sigue con un resumen, pero aquí sigue todo lo anterior." });
|
|
198
203
|
saveSession(this.session);
|
|
199
204
|
this.events.onInfo?.(`Conversación resumida (~${estimateTokens(this.session.messages)} tokens).`);
|
|
200
205
|
}
|
|
@@ -274,6 +279,13 @@ export class Agent {
|
|
|
274
279
|
this.events.onToolStart?.(event);
|
|
275
280
|
const result = await runTool(tools, name, args, { ...ctx, onProgress: (text) => this.events.onToolProgress?.(event, text) }, agent.readOnly);
|
|
276
281
|
this.events.onToolEnd?.({ ...event, output: result.output, display: result.display, isError: Boolean(result.isError) });
|
|
282
|
+
if (depth === 0) {
|
|
283
|
+
this.logEntry({
|
|
284
|
+
role: "tool",
|
|
285
|
+
text: truncate(result.output, 4000),
|
|
286
|
+
tool: { name, summary: event.summary, isError: Boolean(result.isError), display: result.display ? truncate(result.display, 4000) : undefined },
|
|
287
|
+
});
|
|
288
|
+
}
|
|
277
289
|
if (result.images) {
|
|
278
290
|
images.push(...result.images);
|
|
279
291
|
}
|
|
@@ -332,11 +344,12 @@ export class Agent {
|
|
|
332
344
|
await this.ensureRoom(tools, signal);
|
|
333
345
|
const before = this.deps.snapshots ? await this.deps.snapshots.track() : null;
|
|
334
346
|
beginTurn();
|
|
335
|
-
turn = { messageIndex: this.session.messages.length, before, after: null, epoch: this.session.epoch, userText: input.text };
|
|
347
|
+
turn = { messageIndex: this.session.messages.length, before, after: null, epoch: this.session.epoch, userText: input.display ?? input.text };
|
|
336
348
|
this.session.turns.push(turn);
|
|
337
349
|
this.session.redo = [];
|
|
338
350
|
this.session.agent = this.agent.name;
|
|
339
351
|
this.session.messages.push(this.buildUserMessage(input));
|
|
352
|
+
this.logEntry({ role: "user", text: input.display ?? input.text, images: input.images?.length || undefined });
|
|
340
353
|
saveSession(this.session);
|
|
341
354
|
await this.loop(tools, signal);
|
|
342
355
|
}
|
|
@@ -361,6 +374,9 @@ export class Agent {
|
|
|
361
374
|
const result = await this.step(this.fullMessages(tools), tools, agent, signal, 0);
|
|
362
375
|
const message = result.message;
|
|
363
376
|
this.session.messages.push(message);
|
|
377
|
+
if (typeof message.content === "string" && message.content.trim() !== "") {
|
|
378
|
+
this.logEntry({ role: "assistant", text: message.content, model: result.slot.key });
|
|
379
|
+
}
|
|
364
380
|
saveSession(this.session);
|
|
365
381
|
if (!message.tool_calls || message.tool_calls.length === 0) {
|
|
366
382
|
if (this.events.onAnswer) {
|
|
@@ -387,6 +403,7 @@ export class Agent {
|
|
|
387
403
|
if (this.queue.length > 0) {
|
|
388
404
|
for (const queued of this.queue.splice(0)) {
|
|
389
405
|
this.session.messages.push(this.buildUserMessage({ text: `[Mensaje nuevo del usuario mientras trabajabas] ${queued.text}`, images: queued.images }));
|
|
406
|
+
this.logEntry({ role: "user", text: queued.text, images: queued.images?.length || undefined });
|
|
390
407
|
}
|
|
391
408
|
this.events.onInfo?.("(mensaje entregado al agente)");
|
|
392
409
|
saveSession(this.session);
|
|
@@ -464,6 +481,7 @@ export class Agent {
|
|
|
464
481
|
}
|
|
465
482
|
this.session.redo.push({ turn, messages: removed });
|
|
466
483
|
this.readTimes.clear();
|
|
484
|
+
this.logEntry({ role: "info", text: `Deshecho: «${turn.userText}»${files.length > 0 ? ` (ficheros restaurados: ${files.join(", ")})` : ""}` });
|
|
467
485
|
saveSession(this.session);
|
|
468
486
|
return { files, text: turn.userText, restoredMessages };
|
|
469
487
|
}
|
|
@@ -481,6 +499,7 @@ export class Agent {
|
|
|
481
499
|
}
|
|
482
500
|
this.session.turns.push(entry.turn);
|
|
483
501
|
this.readTimes.clear();
|
|
502
|
+
this.logEntry({ role: "info", text: `Rehecho: «${entry.turn.userText}»` });
|
|
484
503
|
saveSession(this.session);
|
|
485
504
|
return { files };
|
|
486
505
|
}
|
package/dist/app.js
CHANGED
|
@@ -16,7 +16,7 @@ import { SessionGrants, createAsker, permissionsFor } from "./permissions.js";
|
|
|
16
16
|
import { Ranking, loadRankingData, rankingKey, refreshRanking } from "./ranking.js";
|
|
17
17
|
import { Router } from "./router.js";
|
|
18
18
|
import { loadSkills, skillContent } from "./skills.js";
|
|
19
|
-
import { deleteSession, listSessions, newSession, saveExport, saveSession } from "./session.js";
|
|
19
|
+
import { deleteSession, listSessions, newSession, saveExport, saveSession, sessionMarkdownPath } from "./session.js";
|
|
20
20
|
import { Snapshots } from "./snapshot.js";
|
|
21
21
|
import { updateVipcode } from "./update.js";
|
|
22
22
|
import { formatTodos, truncate } from "./tools.js";
|
|
@@ -127,6 +127,10 @@ export class App {
|
|
|
127
127
|
yolo = false;
|
|
128
128
|
local = {};
|
|
129
129
|
lastSessions = [];
|
|
130
|
+
// Versión nueva de vipcode publicada: la interfaz la recuerda abajo hasta que la instalas con /update
|
|
131
|
+
newVersion = null;
|
|
132
|
+
// /update ya la ha instalado: se usa al cerrar y volver a abrir vipcode
|
|
133
|
+
updateInstalled = false;
|
|
130
134
|
// La interfaz pone aquí cómo preguntar los permisos
|
|
131
135
|
promptPermission = async () => ({
|
|
132
136
|
allow: false,
|
|
@@ -206,7 +210,11 @@ export class App {
|
|
|
206
210
|
}
|
|
207
211
|
privacyNotice() {
|
|
208
212
|
const names = [...new Set(this.router.slots.filter((slot) => slot.anonymous).map((slot) => (slot.provider.label || slot.provider.name).replace(/ \(sin clave\)$/, "")))];
|
|
209
|
-
return
|
|
213
|
+
return [
|
|
214
|
+
`Para que puedas empezar ya mismo, vipcode usa IAs gratuitas que no piden registro (${names.join(", ")}).`,
|
|
215
|
+
"Como son gratis, pueden guardar lo que les escribes para mejorar sus modelos, así que mejor no compartas contraseñas, claves ni datos privados.",
|
|
216
|
+
"Cuando quieras, conecta una cuenta gratuita con /connect y tendrás más mensajes y más modelos.",
|
|
217
|
+
].join("\n");
|
|
210
218
|
}
|
|
211
219
|
acceptPrivacyNotice() {
|
|
212
220
|
let data = {};
|
|
@@ -303,7 +311,7 @@ export class App {
|
|
|
303
311
|
}
|
|
304
312
|
anonymousNotice() {
|
|
305
313
|
const names = [...new Set(this.router.slots.map((slot) => (slot.provider.label || slot.provider.name).replace(/ \(sin clave\)/, "")))];
|
|
306
|
-
return `
|
|
314
|
+
return `Estás usando IAs gratuitas sin registro (${names.join(", ")}). Consejo: conecta una cuenta gratuita con /connect para tener más mensajes y más modelos (y recuerda no compartir datos privados).`;
|
|
307
315
|
}
|
|
308
316
|
// Siguiente agente principal (Tab)
|
|
309
317
|
cycleAgent(direction = 1) {
|
|
@@ -819,7 +827,7 @@ export class App {
|
|
|
819
827
|
})));
|
|
820
828
|
if (choice) {
|
|
821
829
|
this.loadSession(choice);
|
|
822
|
-
|
|
830
|
+
return { loaded: true };
|
|
823
831
|
}
|
|
824
832
|
return {};
|
|
825
833
|
}
|
|
@@ -832,11 +840,9 @@ export class App {
|
|
|
832
840
|
const chosen = this.lastSessions[Number(arg) - 1];
|
|
833
841
|
if (chosen) {
|
|
834
842
|
this.loadSession(chosen);
|
|
835
|
-
|
|
836
|
-
}
|
|
837
|
-
else {
|
|
838
|
-
ui.error("Número no válido. Usa /sessions primero.");
|
|
843
|
+
return { loaded: true };
|
|
839
844
|
}
|
|
845
|
+
ui.error("Número no válido. Usa /sessions primero.");
|
|
840
846
|
return {};
|
|
841
847
|
}
|
|
842
848
|
case "/delete": {
|
|
@@ -856,7 +862,7 @@ export class App {
|
|
|
856
862
|
ui.info("La conversación está vacía.");
|
|
857
863
|
}
|
|
858
864
|
else {
|
|
859
|
-
ui.info(`Conversación guardada en ${saveExport(this.agent.session)}`);
|
|
865
|
+
ui.info(`Conversación guardada en ${saveExport(this.agent.session)} (además se guarda sola en ${sessionMarkdownPath(this.agent.session)})`);
|
|
860
866
|
}
|
|
861
867
|
return {};
|
|
862
868
|
}
|
|
@@ -915,6 +921,8 @@ export class App {
|
|
|
915
921
|
}
|
|
916
922
|
else {
|
|
917
923
|
ui.info(result.message);
|
|
924
|
+
this.newVersion = null;
|
|
925
|
+
this.updateInstalled ||= result.status === "updated";
|
|
918
926
|
}
|
|
919
927
|
return {};
|
|
920
928
|
}
|
|
@@ -935,13 +943,13 @@ export class App {
|
|
|
935
943
|
const custom = this.commands.find((item) => item.name === name);
|
|
936
944
|
if (custom) {
|
|
937
945
|
const expanded = await expandCommand(custom, arg, this.root);
|
|
938
|
-
return { run: { input: { text: expanded }, options: { agent: custom.agent, model: custom.model, subtask: custom.subtask } } };
|
|
946
|
+
return { run: { input: { text: expanded, display: text.trim() }, options: { agent: custom.agent, model: custom.model, subtask: custom.subtask } } };
|
|
939
947
|
}
|
|
940
948
|
// /nombre-de-skill tarea: se le da la skill ya cargada junto con la tarea
|
|
941
949
|
const skill = this.skills.find((item) => item.name === name && item.userInvocable);
|
|
942
950
|
if (skill) {
|
|
943
951
|
const task = arg ? `\n\nTarea: ${arg}` : "";
|
|
944
|
-
return { run: { input: { text: `Usa la skill «${skill.name}» para esto.${task}\n\n${skillContent(skill)}
|
|
952
|
+
return { run: { input: { text: `Usa la skill «${skill.name}» para esto.${task}\n\n${skillContent(skill)}`, display: text.trim() }, options: {} } };
|
|
945
953
|
}
|
|
946
954
|
const agent = this.primaryAgents().find((item) => item.name === name);
|
|
947
955
|
if (agent) {
|
|
@@ -1010,6 +1018,8 @@ export class App {
|
|
|
1010
1018
|
signal?.removeEventListener("abort", stop);
|
|
1011
1019
|
const clean = output.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "").trimEnd();
|
|
1012
1020
|
this.agent.addNote(`(El usuario ha ejecutado en la terminal \`${command}\`, código de salida ${code ?? "desconocido"}:\n\`\`\`\n${truncate(clean, 8000)}\n\`\`\`)\n\n`);
|
|
1021
|
+
this.agent.logEntry({ role: "tool", text: truncate(clean, 4000), tool: { name: "bash", summary: `(tú) ${command}`, isError: code !== 0 } });
|
|
1022
|
+
saveSession(this.agent.session);
|
|
1013
1023
|
resolve({ output: clean, code });
|
|
1014
1024
|
};
|
|
1015
1025
|
child.on("error", (err) => {
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { createSkill, installSkills, loadSkills, removeSkill } from "./skills.js
|
|
|
15
15
|
import { projectTrust, trustProject, untrustProject } from "./trust.js";
|
|
16
16
|
import { updateVipcode } from "./update.js";
|
|
17
17
|
import { completeMention } from "./tools.js";
|
|
18
|
-
import { plainAdapter, plainEvents, plainPermissionPrompt, ui } from "./ui.js";
|
|
18
|
+
import { plainAdapter, plainEvents, plainPermissionPrompt, printLog, ui } from "./ui.js";
|
|
19
19
|
const USAGE = `vipcode ${VERSION} · agente de programación para la terminal con IAs gratuitas
|
|
20
20
|
|
|
21
21
|
Uso:
|
|
@@ -420,6 +420,7 @@ async function plainLoop(app, options, root) {
|
|
|
420
420
|
};
|
|
421
421
|
const adapter = { ...plainAdapter, setInput: (text) => ui.info(`Tu mensaje era: ${text}`) };
|
|
422
422
|
ui.banner(app.agent.agent.name, app.router.slots.length);
|
|
423
|
+
printLog(app.agent.session);
|
|
423
424
|
if (options.args.length > 0) {
|
|
424
425
|
await runTask(options.args.join(" "), {}, loadImages(options.images, root));
|
|
425
426
|
}
|
|
@@ -453,6 +454,9 @@ async function plainLoop(app, options, root) {
|
|
|
453
454
|
if (result.exit) {
|
|
454
455
|
break;
|
|
455
456
|
}
|
|
457
|
+
if (result.loaded) {
|
|
458
|
+
printLog(app.agent.session);
|
|
459
|
+
}
|
|
456
460
|
if (result.run) {
|
|
457
461
|
await runTask(result.run.input.text, result.run.options);
|
|
458
462
|
}
|
package/dist/labels.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Cómo se cuenta cada herramienta en pantalla y en las conversaciones guardadas
|
|
2
|
+
const TOOL_LABEL = {
|
|
3
|
+
read_file: "Lee",
|
|
4
|
+
write_file: "Escribe",
|
|
5
|
+
edit_file: "Edita",
|
|
6
|
+
list_dir: "Lista",
|
|
7
|
+
glob: "Busca ficheros",
|
|
8
|
+
grep: "Busca",
|
|
9
|
+
bash: "Ejecuta",
|
|
10
|
+
webfetch: "Abre",
|
|
11
|
+
websearch: "Busca en internet",
|
|
12
|
+
todowrite: "Tareas",
|
|
13
|
+
todoread: "Tareas",
|
|
14
|
+
diagnostics: "Diagnósticos",
|
|
15
|
+
task: "Subagente",
|
|
16
|
+
skill: "Skill",
|
|
17
|
+
};
|
|
18
|
+
export function toolLabel(name) {
|
|
19
|
+
return TOOL_LABEL[name] || name;
|
|
20
|
+
}
|
package/dist/session.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { toolLabel } from "./labels.js";
|
|
3
4
|
import { EXPORTS_DIR, HISTORY_PATH, SESSIONS_DIR, ensureHomeDir } from "./paths.js";
|
|
5
|
+
import { describeCall } from "./tools.js";
|
|
4
6
|
export function newSession(cwd) {
|
|
5
7
|
const now = new Date();
|
|
6
8
|
// id legible y ordenable: 20261029-174512-k3x
|
|
@@ -18,26 +20,98 @@ export function newSession(cwd) {
|
|
|
18
20
|
redo: [],
|
|
19
21
|
epoch: 0,
|
|
20
22
|
usage: { prompt: 0, completion: 0 },
|
|
23
|
+
log: [],
|
|
21
24
|
};
|
|
22
25
|
}
|
|
26
|
+
function contentText(content) {
|
|
27
|
+
if (typeof content === "string") {
|
|
28
|
+
return content;
|
|
29
|
+
}
|
|
30
|
+
if (Array.isArray(content)) {
|
|
31
|
+
return content
|
|
32
|
+
.map((part) => {
|
|
33
|
+
if (part.type === "text") {
|
|
34
|
+
return part.text;
|
|
35
|
+
}
|
|
36
|
+
if (part.type === "image_url") {
|
|
37
|
+
return "[imagen]";
|
|
38
|
+
}
|
|
39
|
+
return "";
|
|
40
|
+
})
|
|
41
|
+
.join("\n");
|
|
42
|
+
}
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
// Lo que escribiste, sin los ficheros adjuntos con @ ni las notas que añade vipcode
|
|
46
|
+
function userText(text) {
|
|
47
|
+
let clean = text;
|
|
48
|
+
const attached = clean.indexOf("\n\nFicheros adjuntos con @");
|
|
49
|
+
if (attached >= 0) {
|
|
50
|
+
const names = [...clean.slice(attached).matchAll(/<(?:fichero|carpeta) ruta="([^"]+)">/g)].map((match) => match[1]);
|
|
51
|
+
clean = `${clean.slice(0, attached)}\n\n_Adjuntos: ${names.join(", ")}_`;
|
|
52
|
+
}
|
|
53
|
+
return clean.replace(/^\((?:Nota|El usuario ha ejecutado)[\s\S]*?\)\n\n/, "").replace(/^\[Mensaje nuevo del usuario mientras trabajabas\] /, "");
|
|
54
|
+
}
|
|
55
|
+
// Las sesiones de antes no tenían historial: se saca de los mensajes que quedan
|
|
56
|
+
function logFromMessages(messages, time) {
|
|
57
|
+
const log = [];
|
|
58
|
+
const results = new Map();
|
|
59
|
+
for (const message of messages) {
|
|
60
|
+
if (message.role === "tool") {
|
|
61
|
+
results.set(String(message.tool_call_id), contentText(message.content));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const message of messages) {
|
|
65
|
+
if (message.role === "user") {
|
|
66
|
+
const text = contentText(message.content);
|
|
67
|
+
if (/^(Continúa con la tarea pendiente\.|Has hecho la misma llamada|Has repetido la misma llamada|Imágenes devueltas por las herramientas:)/.test(text)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (text.startsWith("Resumen de la conversación anterior:")) {
|
|
71
|
+
log.push({ role: "info", text: "Conversación resumida para ahorrar contexto (lo anterior al resumen ya no se guardaba entonces).", time });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
log.push({ role: "user", text: userText(text), time });
|
|
75
|
+
}
|
|
76
|
+
else if (message.role === "assistant") {
|
|
77
|
+
const text = contentText(message.content);
|
|
78
|
+
if (text.trim() !== "" && text !== "Entendido, sigo desde aquí.") {
|
|
79
|
+
log.push({ role: "assistant", text, time });
|
|
80
|
+
}
|
|
81
|
+
for (const call of (message.tool_calls || [])) {
|
|
82
|
+
const name = String(call.function?.name || "");
|
|
83
|
+
const output = results.get(String(call.id)) || "";
|
|
84
|
+
log.push({ role: "tool", text: output.slice(0, 4000), time, tool: { name, summary: describeCall(name, call.function?.arguments || ""), isError: output.startsWith("Error") } });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return log;
|
|
89
|
+
}
|
|
23
90
|
// Las sesiones de la versión anterior no tenían todos los campos
|
|
24
91
|
function normalize(data) {
|
|
92
|
+
const messages = Array.isArray(data.messages) ? data.messages : [];
|
|
25
93
|
return {
|
|
26
94
|
id: String(data.id),
|
|
27
95
|
title: data.title || "(sin título)",
|
|
28
96
|
cwd: data.cwd || "",
|
|
29
97
|
created: data.created || data.updated || Date.now(),
|
|
30
98
|
updated: data.updated || Date.now(),
|
|
31
|
-
messages
|
|
99
|
+
messages,
|
|
32
100
|
todos: Array.isArray(data.todos) ? data.todos : [],
|
|
33
101
|
turns: Array.isArray(data.turns) ? data.turns : [],
|
|
34
102
|
redo: Array.isArray(data.redo) ? data.redo : [],
|
|
35
103
|
epoch: data.epoch || 0,
|
|
36
104
|
usage: data.usage || { prompt: 0, completion: 0 },
|
|
37
105
|
agent: data.agent,
|
|
106
|
+
log: Array.isArray(data.log) ? data.log : logFromMessages(messages, data.updated || Date.now()),
|
|
38
107
|
};
|
|
39
108
|
}
|
|
40
109
|
function firstUserText(session) {
|
|
110
|
+
// Lo que escribiste (por ejemplo «/review»), mejor que el mensaje ya expandido
|
|
111
|
+
const logged = session.log.find((entry) => entry.role === "user");
|
|
112
|
+
if (logged) {
|
|
113
|
+
return logged.text;
|
|
114
|
+
}
|
|
41
115
|
const first = session.messages.find((m) => m.role === "user");
|
|
42
116
|
if (!first) {
|
|
43
117
|
return "";
|
|
@@ -65,6 +139,16 @@ export function saveSession(session) {
|
|
|
65
139
|
}
|
|
66
140
|
}
|
|
67
141
|
fs.writeFileSync(path.join(SESSIONS_DIR, `${session.id}.json`), JSON.stringify(session));
|
|
142
|
+
// Y la conversación entera en Markdown, para leerla cuando quieras
|
|
143
|
+
try {
|
|
144
|
+
fs.writeFileSync(sessionMarkdownPath(session), exportMarkdown(session));
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// no es grave: lo importante es el .json
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export function sessionMarkdownPath(session) {
|
|
151
|
+
return path.join(SESSIONS_DIR, `${session.id}.md`);
|
|
68
152
|
}
|
|
69
153
|
export function listSessions(cwd) {
|
|
70
154
|
if (!fs.existsSync(SESSIONS_DIR)) {
|
|
@@ -80,6 +164,10 @@ export function listSessions(cwd) {
|
|
|
80
164
|
if (!cwd || data.cwd === cwd) {
|
|
81
165
|
sessions.push(data);
|
|
82
166
|
}
|
|
167
|
+
// Las conversaciones de antes de que existiera el .md también lo tienen
|
|
168
|
+
if (data.log.length > 0 && !fs.existsSync(sessionMarkdownPath(data))) {
|
|
169
|
+
fs.writeFileSync(sessionMarkdownPath(data), exportMarkdown(data));
|
|
170
|
+
}
|
|
83
171
|
}
|
|
84
172
|
catch {
|
|
85
173
|
// sesión corrupta: la ignoramos
|
|
@@ -102,75 +190,57 @@ export function deleteSession(id) {
|
|
|
102
190
|
return false;
|
|
103
191
|
}
|
|
104
192
|
fs.rmSync(file);
|
|
193
|
+
fs.rmSync(path.join(SESSIONS_DIR, `${id}.md`), { force: true });
|
|
105
194
|
return true;
|
|
106
195
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return content
|
|
113
|
-
.map((part) => {
|
|
114
|
-
if (part.type === "text") {
|
|
115
|
-
return part.text;
|
|
116
|
-
}
|
|
117
|
-
if (part.type === "image_url") {
|
|
118
|
-
return "[imagen]";
|
|
119
|
-
}
|
|
120
|
-
return "";
|
|
121
|
-
})
|
|
122
|
-
.join("\n");
|
|
123
|
-
}
|
|
124
|
-
return "";
|
|
196
|
+
// Un bloque de código que no se rompe aunque el texto lleve ```
|
|
197
|
+
function fence(text, lang = "") {
|
|
198
|
+
const longest = Math.max(2, ...[...text.matchAll(/`+/g)].map((match) => match[0].length));
|
|
199
|
+
const ticks = "`".repeat(longest + 1);
|
|
200
|
+
return [`${ticks}${lang}`, text, ticks];
|
|
125
201
|
}
|
|
126
|
-
//
|
|
202
|
+
// En español aunque el sistema no diga el idioma: 29/10/2026, 20:06:06
|
|
203
|
+
function when(time) {
|
|
204
|
+
return new Date(time).toLocaleString("es-ES");
|
|
205
|
+
}
|
|
206
|
+
// La conversación entera en Markdown (sale del historial, que no se resume nunca)
|
|
127
207
|
export function exportMarkdown(session) {
|
|
128
208
|
const lines = [
|
|
129
209
|
`# ${session.title}`,
|
|
130
210
|
"",
|
|
131
211
|
`- Carpeta: \`${session.cwd}\``,
|
|
132
|
-
`- Empezada: ${
|
|
133
|
-
`- Última actividad: ${new Date(session.updated).toLocaleString()}`,
|
|
212
|
+
`- Empezada: ${when(session.created)} · Última actividad: ${when(session.updated)}`,
|
|
134
213
|
`- Tokens: ${session.usage.prompt} de entrada, ${session.usage.completion} de salida`,
|
|
135
214
|
"",
|
|
215
|
+
"---",
|
|
216
|
+
"",
|
|
136
217
|
];
|
|
137
|
-
for (const
|
|
138
|
-
if (
|
|
139
|
-
|
|
218
|
+
for (const entry of session.log) {
|
|
219
|
+
if (entry.role === "user") {
|
|
220
|
+
const images = entry.images ? ` · ${entry.images} imagen${entry.images > 1 ? "es" : ""}` : "";
|
|
221
|
+
lines.push(`### Tú · ${when(entry.time)}${images}`, "", entry.text, "");
|
|
140
222
|
}
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
const attached = text.indexOf("\n\nFicheros adjuntos con @");
|
|
144
|
-
if (attached >= 0) {
|
|
145
|
-
const names = [...text.slice(attached).matchAll(/<(?:fichero|carpeta) ruta="([^"]+)">/g)].map((match) => match[1]);
|
|
146
|
-
text = `${text.slice(0, attached)}\n\n_Adjuntos: ${names.join(", ")}_`;
|
|
147
|
-
}
|
|
148
|
-
lines.push("## Tú", "", text, "");
|
|
223
|
+
else if (entry.role === "assistant") {
|
|
224
|
+
lines.push(`### vipcode${entry.model ? ` · ${entry.model}` : ""}`, "", entry.text, "");
|
|
149
225
|
}
|
|
150
|
-
else if (
|
|
151
|
-
const
|
|
152
|
-
lines.push("
|
|
153
|
-
if (
|
|
154
|
-
lines.push(
|
|
226
|
+
else if (entry.role === "tool" && entry.tool) {
|
|
227
|
+
const summary = entry.tool.summary ? ` \`${entry.tool.summary.replace(/`/g, "'")}\`` : "";
|
|
228
|
+
lines.push(`- **${toolLabel(entry.tool.name)}**${summary}${entry.tool.isError ? " — ✗ falló" : ""}`);
|
|
229
|
+
if (entry.tool.display) {
|
|
230
|
+
lines.push("", ...fence(entry.tool.display, "diff"), "");
|
|
155
231
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
args = `${args.slice(0, 600)}…`;
|
|
232
|
+
else if (entry.tool.isError || entry.tool.name === "bash") {
|
|
233
|
+
const output = entry.text.split("\n").slice(0, 15).join("\n");
|
|
234
|
+
if (output.trim() !== "") {
|
|
235
|
+
lines.push("", ...fence(output), "");
|
|
161
236
|
}
|
|
162
|
-
lines.push(`> 🔧 **${call.function?.name}** \`${args.replace(/`/g, "'")}\``, "");
|
|
163
237
|
}
|
|
164
238
|
}
|
|
165
|
-
else if (
|
|
166
|
-
|
|
167
|
-
if (text.length > 2000) {
|
|
168
|
-
text = `${text.slice(0, 2000)}\n[…]`;
|
|
169
|
-
}
|
|
170
|
-
lines.push("<details><summary>Resultado</summary>", "", "```", text.replace(/```/g, "'''"), "```", "", "</details>", "");
|
|
239
|
+
else if (entry.role === "info") {
|
|
240
|
+
lines.push("", `_${entry.text}_`, "");
|
|
171
241
|
}
|
|
172
242
|
}
|
|
173
|
-
return lines.join("\n")
|
|
243
|
+
return `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
|
|
174
244
|
}
|
|
175
245
|
export function saveExport(session) {
|
|
176
246
|
fs.mkdirSync(EXPORTS_DIR, { recursive: true });
|
package/dist/tui/app.js
CHANGED
|
@@ -8,36 +8,22 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|
|
8
8
|
import { configNotices } from "../config.js";
|
|
9
9
|
import { discoveryIsStale } from "../discovery.js";
|
|
10
10
|
import { isImagePath, readClipboardImage, toDataUrl } from "../images.js";
|
|
11
|
+
import { toolLabel } from "../labels.js";
|
|
11
12
|
import { renderMarkdown } from "../markdown.js";
|
|
12
13
|
import { VERSION } from "../meta.js";
|
|
13
14
|
import { NoSlotError } from "../router.js";
|
|
14
15
|
import { showControl, stripControl, stripUnsafe } from "../sanitize.js";
|
|
15
|
-
import { appendHistory, loadHistory } from "../session.js";
|
|
16
|
+
import { appendHistory, loadHistory, sessionMarkdownPath } from "../session.js";
|
|
16
17
|
import { suggestFiles } from "../tools.js";
|
|
17
18
|
import { colorPreview } from "../ui.js";
|
|
18
19
|
import { checkForUpdate } from "../update.js";
|
|
19
20
|
import { PermissionDialog, PromptDialog, SelectDialog } from "./dialogs.js";
|
|
20
21
|
import * as E from "./editor.js";
|
|
22
|
+
// Al volver a una sesión se enseñan sus últimos pasos (el .md los tiene todos)
|
|
23
|
+
const REPLAY_MAX = 300;
|
|
21
24
|
// Comandos que se pueden usar mientras el agente trabaja (solo consultan)
|
|
22
25
|
const SAFE_WHILE_BUSY = ["/help", "/usage", "/todos", "/mcp", "/lsp", "/providers", "/skills"];
|
|
23
26
|
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
24
|
-
// Cómo se cuenta cada herramienta en pantalla
|
|
25
|
-
const TOOL_LABEL = {
|
|
26
|
-
read_file: "Lee",
|
|
27
|
-
write_file: "Escribe",
|
|
28
|
-
edit_file: "Edita",
|
|
29
|
-
list_dir: "Lista",
|
|
30
|
-
glob: "Busca ficheros",
|
|
31
|
-
grep: "Busca",
|
|
32
|
-
bash: "Ejecuta",
|
|
33
|
-
webfetch: "Abre",
|
|
34
|
-
websearch: "Busca en internet",
|
|
35
|
-
todowrite: "Tareas",
|
|
36
|
-
todoread: "Tareas",
|
|
37
|
-
diagnostics: "Diagnósticos",
|
|
38
|
-
task: "Subagente",
|
|
39
|
-
skill: "Skill",
|
|
40
|
-
};
|
|
41
27
|
function Spinner(props) {
|
|
42
28
|
const { frame } = useAnimation({ interval: 80 });
|
|
43
29
|
return _jsx(Text, { color: props.color || "cyan", children: SPINNER[frame % SPINNER.length] });
|
|
@@ -166,7 +152,7 @@ function toolDetail(event) {
|
|
|
166
152
|
}
|
|
167
153
|
// El resumen sale de lo que pidió el modelo (comandos, rutas): los caracteres de control se ven, no actúan
|
|
168
154
|
function ToolHeader(props) {
|
|
169
|
-
return (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children:
|
|
155
|
+
return (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: showControl(toolLabel(props.name)) }), props.summary ? _jsxs(Text, { children: [" ", showControl(props.summary)] }) : null] }));
|
|
170
156
|
}
|
|
171
157
|
function ToolView(props) {
|
|
172
158
|
const { event } = props;
|
|
@@ -214,6 +200,8 @@ function ItemView(props) {
|
|
|
214
200
|
const shown = lines.slice(-30);
|
|
215
201
|
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: [_jsxs(Text, { color: "yellow", bold: true, children: ["$", " "] }), _jsx(Text, { bold: true, children: showControl(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"] })] }));
|
|
216
202
|
}
|
|
203
|
+
case "update":
|
|
204
|
+
return (_jsxs(Box, { marginY: 1, alignSelf: "flex-start", flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 2, children: [_jsx(Text, { bold: true, color: "green", children: "\u2191 \u00A1Hay una versi\u00F3n nueva de vipcode!" }), _jsxs(Text, { children: [VERSION, " \u2192 ", _jsx(Text, { bold: true, color: "green", children: stripControl(item.version) }), " \u00B7 escribe", " ", _jsx(Text, { bold: true, color: "cyan", children: "/update" }), " ", "para instalarla"] })] }));
|
|
217
205
|
// Avisos y bloques pueden llevar mensajes de los proveedores o datos de fuera: solo se dejan los colores
|
|
218
206
|
case "info":
|
|
219
207
|
return _jsx(Text, { dimColor: true, children: stripUnsafe(item.text) });
|
|
@@ -289,11 +277,17 @@ function StatusBar(props) {
|
|
|
289
277
|
const usage = app.agent.session.usage;
|
|
290
278
|
const spent = usage.prompt + usage.completion;
|
|
291
279
|
const queued = app.agent.queue.length;
|
|
292
|
-
let hint = "ctrl+p comandos
|
|
280
|
+
let hint = _jsx(Text, { dimColor: true, children: "ctrl+p comandos \u00B7 /help" });
|
|
293
281
|
if (props.busy) {
|
|
294
|
-
hint = "esc interrumpe
|
|
282
|
+
hint = _jsx(Text, { dimColor: true, children: "esc interrumpe \u00B7 escribe para a\u00F1adir instrucciones" });
|
|
295
283
|
}
|
|
296
|
-
|
|
284
|
+
else if (app.updateInstalled) {
|
|
285
|
+
hint = _jsx(Text, { color: "green", children: "\u2713 vipcode actualizado \u00B7 ci\u00E9rralo y vuelve a abrirlo para usarlo" });
|
|
286
|
+
}
|
|
287
|
+
else if (app.newVersion) {
|
|
288
|
+
hint = (_jsxs(Text, { bold: true, color: "green", children: ["\u2191 vipcode ", stripControl(app.newVersion), " disponible \u00B7 escribe /update"] }));
|
|
289
|
+
}
|
|
290
|
+
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, { wrap: "truncate-start", children: hint })] }));
|
|
297
291
|
}
|
|
298
292
|
function Main(props) {
|
|
299
293
|
const { app } = props;
|
|
@@ -505,6 +499,9 @@ function Main(props) {
|
|
|
505
499
|
await quit();
|
|
506
500
|
return;
|
|
507
501
|
}
|
|
502
|
+
if (result.loaded) {
|
|
503
|
+
replay(app.agent.session);
|
|
504
|
+
}
|
|
508
505
|
if (result.run) {
|
|
509
506
|
void runAgent({ ...result.run.input, images }, result.run.options);
|
|
510
507
|
}
|
|
@@ -524,10 +521,10 @@ function Main(props) {
|
|
|
524
521
|
};
|
|
525
522
|
// La primera vez que se van a usar proveedores sin clave, explicamos qué pasa con lo que envías
|
|
526
523
|
const askPrivacy = async () => {
|
|
527
|
-
const choice = await adapter.select(
|
|
528
|
-
{ label: "
|
|
529
|
-
{ label: "
|
|
530
|
-
{ label: "
|
|
524
|
+
const choice = await adapter.select(`¡Te damos la bienvenida a vipcode!\n${app.privacyNotice()}`, [
|
|
525
|
+
{ label: "Empezar", description: "con las IAs gratuitas sin registro", value: "ok" },
|
|
526
|
+
{ label: "Conectar antes una cuenta gratuita", description: "para tener más mensajes y más modelos", value: "connect" },
|
|
527
|
+
{ label: "Prefiero usar solo mis cuentas", description: "y no las IAs sin registro", value: "off" },
|
|
531
528
|
]);
|
|
532
529
|
if (!choice) {
|
|
533
530
|
// Con Esc se vuelve a preguntar la próxima vez
|
|
@@ -539,13 +536,42 @@ function Main(props) {
|
|
|
539
536
|
}
|
|
540
537
|
else if (choice === "off") {
|
|
541
538
|
app.disableKeyless();
|
|
542
|
-
push({ kind: "info", text:
|
|
539
|
+
push({ kind: "info", text: 'Hecho: solo usaré las cuentas que conectes. Si cambias de idea, pon "keyless": true en tu configuración (/config te dice dónde está).' });
|
|
543
540
|
if (app.router.slots.length === 0) {
|
|
544
541
|
await app.connect(adapter, "");
|
|
545
542
|
}
|
|
546
543
|
}
|
|
547
544
|
refresh();
|
|
548
545
|
};
|
|
546
|
+
// La conversación de una sesión a la que vuelves, tal como la viste
|
|
547
|
+
const replay = (session) => {
|
|
548
|
+
const entries = session.log;
|
|
549
|
+
if (entries.length === 0) {
|
|
550
|
+
push({ kind: "info", text: `Sesión «${session.title}» cargada.` });
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const shown = entries.slice(-REPLAY_MAX);
|
|
554
|
+
push({ kind: "info", text: `── Conversación anterior: «${session.title}» · ${new Date(session.updated).toLocaleString()} ──` });
|
|
555
|
+
if (entries.length > shown.length) {
|
|
556
|
+
push({ kind: "info", text: `… ${entries.length - shown.length} pasos antes; la conversación entera está en ${sessionMarkdownPath(session)}` });
|
|
557
|
+
}
|
|
558
|
+
shown.forEach((entry, index) => {
|
|
559
|
+
if (entry.role === "user") {
|
|
560
|
+
push({ kind: "user", text: entry.text, images: entry.images || 0, color: agentColor() });
|
|
561
|
+
}
|
|
562
|
+
else if (entry.role === "assistant") {
|
|
563
|
+
push({ kind: "assistant", text: entry.text, color: agentColor() });
|
|
564
|
+
}
|
|
565
|
+
else if (entry.role === "tool" && entry.tool) {
|
|
566
|
+
const event = { id: `historial-${index}`, name: entry.tool.name, summary: entry.tool.summary, depth: 0, agent: "" };
|
|
567
|
+
push({ kind: "tool", event: { ...event, output: entry.text, display: entry.tool.display, isError: entry.tool.isError }, ms: 0 });
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
push({ kind: "info", text: entry.text });
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
push({ kind: "info", text: "── Sigues desde aquí (/new empieza otra conversación) ──" });
|
|
574
|
+
};
|
|
549
575
|
// Paleta de comandos (Ctrl+P): todos los comandos, skills y agentes con su descripción
|
|
550
576
|
const openPalette = async () => {
|
|
551
577
|
const commands = app.commandSuggestions();
|
|
@@ -659,14 +685,13 @@ function Main(props) {
|
|
|
659
685
|
for (const notice of configNotices) {
|
|
660
686
|
push({ kind: "notice", text: notice });
|
|
661
687
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
push({ kind: "info", text: `Continúas «${session.title}» (${session.turns.length} mensajes tuyos). /new empieza otra.` });
|
|
688
|
+
if (app.agent.session.log.length > 0) {
|
|
689
|
+
replay(app.agent.session);
|
|
665
690
|
}
|
|
666
691
|
if (app.router.slots.length === 0) {
|
|
667
692
|
push({
|
|
668
693
|
kind: "notice",
|
|
669
|
-
text: "
|
|
694
|
+
text: "Para empezar, conecta una IA gratuita: elige una de la lista y sigue los pasos (puedes repetirlo cuando quieras con /connect).",
|
|
670
695
|
});
|
|
671
696
|
void app.connect(adapter, "").then(refresh);
|
|
672
697
|
}
|
|
@@ -688,7 +713,8 @@ function Main(props) {
|
|
|
688
713
|
if (app.config.checkUpdates !== false) {
|
|
689
714
|
void checkForUpdate().then((version) => {
|
|
690
715
|
if (version) {
|
|
691
|
-
|
|
716
|
+
app.newVersion = version;
|
|
717
|
+
push({ kind: "update", version });
|
|
692
718
|
}
|
|
693
719
|
});
|
|
694
720
|
}
|
package/dist/tui/dialogs.js
CHANGED
|
@@ -154,7 +154,9 @@ export function SelectDialog(props) {
|
|
|
154
154
|
const windowSize = 12;
|
|
155
155
|
const start = Math.max(0, Math.min(selected - Math.floor(windowSize / 2), filtered.length - windowSize));
|
|
156
156
|
const visible = filtered.slice(start, start + windowSize);
|
|
157
|
-
|
|
157
|
+
// La primera línea del título va destacada; las siguientes, si las hay, explican la pregunta
|
|
158
|
+
const [heading, ...body] = props.title.split("\n");
|
|
159
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: heading }), body.length > 0 ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: body.join("\n") }) })) : null, _jsx(Text, { dimColor: true, children: filter ? `Filtro: ${filter}` : "Escribe para filtrar · ↑↓ elegir · Enter aceptar · Esc salir" }), visible.length === 0 ? _jsx(Text, { dimColor: true, children: "(nada coincide)" }) : null, visible.map((item, index) => {
|
|
158
160
|
const active = start + index === selected;
|
|
159
161
|
return (_jsxs(Text, { color: active ? "cyan" : undefined, wrap: "truncate-end", children: [active ? "❯ " : " ", stripControl(item.label), item.description ? _jsxs(Text, { dimColor: true, children: [" ", stripControl(item.description)] }) : null] }, `${start + index}-${item.label}`));
|
|
160
162
|
}), filtered.length > windowSize ? (_jsxs(Text, { dimColor: true, children: [selected + 1, "/", filtered.length] })) : null] }));
|
package/dist/ui.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
|
+
import { toolLabel } from "./labels.js";
|
|
3
|
+
import { renderMarkdown } from "./markdown.js";
|
|
2
4
|
import { suggestPattern } from "./permissions.js";
|
|
3
5
|
import { showControl, stripControl, stripUnsafe } from "./sanitize.js";
|
|
6
|
+
import { sessionMarkdownPath } from "./session.js";
|
|
4
7
|
import { formatTodos } from "./tools.js";
|
|
5
8
|
export const ui = {
|
|
6
9
|
text(chunk) {
|
|
@@ -175,6 +178,33 @@ export function plainPermissionPrompt(getReadline, grants) {
|
|
|
175
178
|
}
|
|
176
179
|
};
|
|
177
180
|
}
|
|
181
|
+
// La conversación de una sesión a la que vuelves, en el modo de texto simple
|
|
182
|
+
export function printLog(session, max = 300) {
|
|
183
|
+
const entries = session.log;
|
|
184
|
+
if (entries.length === 0) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const shown = entries.slice(-max);
|
|
188
|
+
console.log(pc.dim(`── Conversación anterior: «${stripControl(session.title)}» · ${new Date(session.updated).toLocaleString()} ──`));
|
|
189
|
+
if (entries.length > shown.length) {
|
|
190
|
+
console.log(pc.dim(`… ${entries.length - shown.length} pasos antes; la conversación entera está en ${sessionMarkdownPath(session)}`));
|
|
191
|
+
}
|
|
192
|
+
for (const entry of shown) {
|
|
193
|
+
if (entry.role === "user") {
|
|
194
|
+
console.log(`\n${pc.magenta("❯")} ${stripControl(entry.text)}`);
|
|
195
|
+
}
|
|
196
|
+
else if (entry.role === "assistant") {
|
|
197
|
+
console.log(`\n${renderMarkdown(entry.text, process.stdout.columns || 80)}`);
|
|
198
|
+
}
|
|
199
|
+
else if (entry.role === "tool" && entry.tool) {
|
|
200
|
+
ui.tool(toolLabel(entry.tool.name), entry.tool.summary);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
console.log(pc.dim(stripControl(entry.text)));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
console.log(pc.dim("\n── Sigues desde aquí (/new empieza otra conversación) ──"));
|
|
207
|
+
}
|
|
178
208
|
// Interfaz mínima para los comandos / en el modo de texto
|
|
179
209
|
export const plainAdapter = {
|
|
180
210
|
info: (text) => ui.info(text),
|