chocolatito-code 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/agent/huecos.d.ts +58 -0
  2. package/dist/agent/huecos.js +81 -0
  3. package/dist/agent/loop.js +144 -24
  4. package/dist/agent/salidaDelAgente.js +10 -24
  5. package/dist/agent/toolGate.js +6 -1
  6. package/dist/config/engine.d.ts +21 -1
  7. package/dist/config/engine.js +22 -1
  8. package/dist/config/quota.d.ts +11 -0
  9. package/dist/config/quota.js +18 -0
  10. package/dist/index.js +35 -26
  11. package/dist/prompts/systemPrompt.js +186 -150
  12. package/dist/tools/browserExtension.js +118 -3
  13. package/dist/tools/computerUse.d.ts +1 -1
  14. package/dist/tools/computerUse.js +183 -2
  15. package/dist/tools/toolDefsComputer.js +6 -2
  16. package/dist/tools/visionBridge.js +60 -3
  17. package/dist/tools/win/hostScript.js +154 -0
  18. package/dist/ui/banner.d.ts +31 -0
  19. package/dist/ui/banner.js +45 -0
  20. package/dist/ui/comandos.d.ts +33 -0
  21. package/dist/ui/comandos.js +49 -0
  22. package/dist/ui/informeDeUso.d.ts +17 -0
  23. package/dist/ui/informeDeUso.js +81 -0
  24. package/dist/ui/interrupt.d.ts +10 -0
  25. package/dist/ui/interrupt.js +50 -2
  26. package/dist/ui/marco.d.ts +35 -0
  27. package/dist/ui/marco.js +161 -0
  28. package/dist/ui/pieFijo.d.ts +56 -0
  29. package/dist/ui/pieFijo.js +309 -0
  30. package/dist/ui/prompt.d.ts +4 -41
  31. package/dist/ui/prompt.js +24 -157
  32. package/dist/ui/reasoningStream.js +22 -12
  33. package/dist/ui/renderer.d.ts +13 -0
  34. package/dist/ui/renderer.js +41 -12
  35. package/dist/ui/spinner.d.ts +43 -8
  36. package/dist/ui/spinner.js +118 -43
  37. package/extension/background.js +9 -3
  38. package/extension/iconos/128.png +0 -0
  39. package/extension/iconos/16.png +0 -0
  40. package/extension/iconos/32.png +0 -0
  41. package/extension/iconos/48.png +0 -0
  42. package/extension/iconos/ORIGEN.md +48 -0
  43. package/extension/manifest.json +60 -60
  44. package/package.json +67 -67
@@ -1,6 +1,21 @@
1
+ /**
2
+ * POR QUE ESTO NO USA `console.log`
3
+ *
4
+ * Mientras el agente trabaja hay un bloque pintado abajo del todo -el spinner y
5
+ * la barra de escribir- que se borra y se repinta alrededor de cada escritura.
6
+ * Un `console.log` no pasa por ahi: escribe donde este el cursor, encima del
7
+ * bloque, y el repintado siguiente ya no sabe donde empieza el suyo. En pantalla
8
+ * eso se ve como un spinner que se queda atras y filas huerfanas por todos lados.
9
+ *
10
+ * Ademas era el ultimo sitio que ataba el agente a una terminal. `salida` existe
11
+ * justamente para que el bucle no sepa donde acaba lo que dice, y el
12
+ * renderizador -que es quien mas imprime- seguia escribiendo directo.
13
+ */
1
14
  import chalk from "chalk";
2
15
  import { Marked } from "marked";
3
16
  import { markedTerminal } from "marked-terminal";
17
+ import { salida } from "../agent/salidaDelAgente.js";
18
+ import { isManagedMode } from "../config/quota.js";
4
19
  const marked = new Marked(markedTerminal({
5
20
  heading: chalk.bold.hex("#D97757"),
6
21
  firstHeading: chalk.bold.hex("#D97757").underline,
@@ -22,14 +37,14 @@ const marked = new Marked(markedTerminal({
22
37
  export function renderUserPrompt(text) {
23
38
  const symbol = chalk.bold.hex("#D97757")("❯");
24
39
  const shaded = chalk.bgHex("#242730").hex("#F4F4F5").bold(` ${text.trim()} `);
25
- console.log(`\n${symbol} ${shaded}\n`);
40
+ salida.linea(`\n${symbol} ${shaded}\n`);
26
41
  }
27
42
  // Tool call rendering as specified in §B5
28
43
  export function renderToolStart(name, summary) {
29
44
  const bullet = chalk.hex("#FFA500")("●"); // Amber while starting
30
45
  const toolName = chalk.bold.white(formatToolName(name));
31
46
  const argPart = summary ? chalk.gray(`(${summary.length > 60 ? summary.slice(0, 57) + '...' : summary})`) : "";
32
- console.log(`\n${bullet} ${toolName}${argPart}`);
47
+ salida.linea(`\n${bullet} ${toolName}${argPart}`);
33
48
  }
34
49
  export function renderToolSuccess(name, resultSummary) {
35
50
  const rawLines = resultSummary.trim().split("\n");
@@ -40,23 +55,23 @@ export function renderToolSuccess(name, resultSummary) {
40
55
  const prefix = idx === 0 ? ` ${chalk.gray("⎿")} ` : " ";
41
56
  return `${prefix}${colorizeText(line)}`;
42
57
  }).join("\n");
43
- console.log(indented);
58
+ salida.linea(indented);
44
59
  if (remainingCount > 0) {
45
- console.log(chalk.gray(` … +${remainingCount} líneas más`));
60
+ salida.linea(chalk.gray(` … +${remainingCount} líneas más`));
46
61
  }
47
62
  }
48
63
  export function renderToolError(name, errorSummary) {
49
- console.log(` ${chalk.red("⎿")} ${chalk.red(errorSummary)}`);
64
+ salida.linea(` ${chalk.red("⎿")} ${chalk.red(errorSummary)}`);
50
65
  }
51
66
  export function renderDiff(filePath, diffText) {
52
- console.log(`\n${diffText}\n`);
67
+ salida.linea(`\n${diffText}\n`);
53
68
  }
54
69
  export function renderReasoning(reasoningText) {
55
70
  if (!reasoningText.trim())
56
71
  return;
57
72
  const lines = reasoningText.trim().split("\n");
58
73
  const formatted = lines.map((l) => ` ${chalk.hex("#8B5CF6")("│")} ${chalk.gray(l)}`).join("\n");
59
- console.log(`\n ${chalk.bold.hex("#8B5CF6")("💭 Razonamiento:")}\n${formatted}\n`);
74
+ salida.linea(`\n ${chalk.bold.hex("#8B5CF6")("💭 Razonamiento:")}\n${formatted}\n`);
60
75
  }
61
76
  export function renderAssistantResponse(markdownText) {
62
77
  try {
@@ -68,23 +83,37 @@ export function renderAssistantResponse(markdownText) {
68
83
  let parsed = String(marked.parse(cleaned));
69
84
  // Convert raw asterisks bullets to terracotta bullets
70
85
  parsed = parsed.replace(/^(\s*)\*\s+/gm, `$1${chalk.hex("#D97757")("•")} `);
71
- console.log(`\n${parsed}`);
86
+ salida.linea(`\n${parsed}`);
72
87
  }
73
88
  catch {
74
- console.log(`\n${markdownText.trim()}\n`);
89
+ salida.linea(`\n${markdownText.trim()}\n`);
75
90
  }
76
91
  }
92
+ /**
93
+ * La linea de cada turno.
94
+ *
95
+ * `⚡ 3.0s · 64 tokens salida (10.8k en caché) · Espectro 1.0 (Ágil) · 99%
96
+ * contexto libre · 28% de la cuota semanal`
97
+ *
98
+ * Con cuenta gestionada NO lleva importe. Se quito primero de /cost y esta se
99
+ * quedo atras; el usuario lo vio enseguida -"aun sigue saliendo el precio que se
100
+ * gasta"- y con razon: /cost se mira cuando uno se acuerda, esta linea sale en
101
+ * TODOS los turnos. De los dos sitios, era el que mas importaba.
102
+ *
103
+ * Con clave propia si sale: ahi el dinero es del usuario y es su factura.
104
+ */
77
105
  export function renderFooter(costStr, outTokens, cachedTokens, modelName, durationMs, contextPercent, quotaStr) {
78
106
  const durationPart = durationMs ? `${(durationMs / 1000).toFixed(1)}s · ` : "";
79
107
  const outPart = `${outTokens.toLocaleString()} tokens salida`;
80
108
  const cachePart = cachedTokens > 0 ? ` (${(cachedTokens / 1000).toFixed(1)}k en caché)` : "";
81
109
  const contextPart = contextPercent !== undefined ? ` · ${contextPercent}% contexto libre` : "";
82
110
  const quotaPart = quotaStr ? ` · ${quotaStr}` : "";
83
- const line = chalk.gray(` ${chalk.hex("#D97757")("")} ${durationPart}${outPart}${cachePart} · ${chalk.green(costStr)} · ${modelName}${contextPart}${quotaPart}`);
84
- console.log(`${line}\n`);
111
+ const costPart = isManagedMode() ? "" : ` · ${chalk.green(costStr)}`;
112
+ const line = chalk.gray(` ${chalk.hex("#D97757")("⚡")} ${durationPart}${outPart}${cachePart}${costPart} · ${modelName}${contextPart}${quotaPart}`);
113
+ salida.linea(`${line}\n`);
85
114
  }
86
115
  export function renderError(message) {
87
- console.log(`\n ${chalk.bgRed.white.bold(" ERROR ")} ${chalk.red(message)}\n`);
116
+ salida.linea(`\n ${chalk.bgRed.white.bold(" ERROR ")} ${chalk.red(message)}\n`);
88
117
  }
89
118
  function colorizeText(text) {
90
119
  // Highlight file paths (C:\..., /path/...) in cyan
@@ -6,19 +6,54 @@ export declare class DynamicSpinner {
6
6
  private isRunning;
7
7
  private prefix;
8
8
  tokenCount: number;
9
- /** Lineas extra pintadas debajo del spinner en el ultimo tick. */
10
- private extraRows;
11
9
  constructor(prefix?: string);
12
10
  start(initialPrefix?: string): void;
13
11
  private tick;
14
12
  /**
15
- * Segunda linea con lo que el usuario esta escribiendo mientras se genera.
13
+ * `✳ Compactando historial… (3s · 1.2k tokens · esc para pausar) ⇥ modo`
16
14
  *
17
- * Antes el teclado se tragaba todo sin pintar nada y parecia que el terminal
18
- * se habia colgado. Se dibuja debajo y el cursor vuelve arriba, para que el
19
- * siguiente tick sobrescriba las dos lineas sin dejar rastro.
15
+ * El prefijo se pasaba en `start("Compactando historial")` y no se pintaba en
16
+ * ningun sitio: `tick()` usaba siempre los verbos rotatorios. Asi que las
17
+ * cuatro esperas con nombre propio -compactar, optimizar memoria, verificar-
18
+ * salian todas como "Rumiando…", indistinguibles de una peticion normal.
20
19
  */
21
- private drawTypeAhead;
22
- private clearExtraRows;
20
+ private lineaDelSpinner;
21
+ /**
22
+ * LA BARRA. La MISMA que se ve al escribir una orden, no una parecida.
23
+ *
24
+ * Antes habia dos: al esperar salia el marco entero -barra, entrada, barra,
25
+ * linea de ayuda-; al trabajar, una version recortada con otro texto. Se
26
+ * parecian lo justo para que el cambio de una a otra se viera como un salto.
27
+ * Palabras del usuario: "aun no es fija... debe ser fija y por debajo deben
28
+ * pasar los otros mensajes".
29
+ *
30
+ * Ahora las dos salen de `buildPromptFrame`, asi que no pueden divergir: quien
31
+ * toque el marco lo toca en los dos estados a la vez.
32
+ *
33
+ * Sin desplegable: el autocompletado de "@" y "/" es del prompt de verdad, que
34
+ * es quien tiene el teclado. Aqui lo tecleado solo se encola.
35
+ */
36
+ private lineasDeEntrada;
23
37
  stop(): void;
24
38
  }
39
+ /**
40
+ * DEJA EL PIE PUESTO MIENTRAS CORRE ALGO
41
+ *
42
+ * El spinner solo vivia mientras hablaba el modelo. En cuanto empezaba una
43
+ * herramienta se paraba, y ahi se acababa lo unico fijo de la pantalla: durante
44
+ * un `run_command` de veinte segundos, o una lectura de un archivo enorme, no
45
+ * habia ni cronometro ni forma de saber que seguia vivo. Justo el rato en que
46
+ * uno se pregunta si se ha colgado.
47
+ *
48
+ * EL RETRASO NO ES UN CAPRICHO
49
+ *
50
+ * La mayoria de las herramientas tardan milisegundos. Pintar el pie y borrarlo
51
+ * en el mismo suspiro es un parpadeo en cada llamada, y encima el pie mueve
52
+ * lineas: veinte herramientas seguidas serian veinte sacudidas. Con 150 ms de
53
+ * espera, lo rapido no pinta nada y lo lento sale igual.
54
+ *
55
+ * El `finally` es obligatorio. Un spinner que se queda corriendo repinta el pie
56
+ * ocho veces por segundo encima de lo que venga despues -incluido el prompt-, y
57
+ * de ahi no se sale sin cerrar el programa.
58
+ */
59
+ export declare function conSpinner<T>(etiqueta: string, tarea: () => Promise<T>, retrasoMs?: number): Promise<T>;
@@ -2,6 +2,23 @@ import chalk from "chalk";
2
2
  import process from "node:process";
3
3
  import { typedText, queuedCount } from "./typeAhead.js";
4
4
  import { modeBadge } from "./modes.js";
5
+ import { fijarSecciones, quitarSeccion } from "./pieFijo.js";
6
+ import { buildPromptFrame } from "./marco.js";
7
+ /**
8
+ * LA LINEA QUE DICE QUE ESTO SIGUE VIVO
9
+ *
10
+ * Antes se pintaba con `\r\x1b[2K` sobre la fila donde estuviera el cursor. Eso
11
+ * la deja clavada a esa fila, y en cuanto la respuesta empieza a salir el
12
+ * terminal desplaza el contenido y se lleva el spinner hacia arriba con el.
13
+ * Palabras del usuario: "la nubecita y el rayito suben con lo que me respondes".
14
+ *
15
+ * Ahora se entrega a `pieFijo`, que la vuelve a pintar abajo despues de cada
16
+ * escritura. La linea deja de moverse aunque salgan cien lineas de respuesta.
17
+ *
18
+ * Y por eso mismo ya no hace falta pararlo al llegar el primer trozo: el
19
+ * contador de segundos y de tokens sigue a la vista MIENTRAS el modelo habla,
20
+ * que es justo cuando interesa saber cuanto lleva.
21
+ */
5
22
  const GLYPHS = ["✢", "✳", "∗", "✻", "✽"];
6
23
  const VERBS = [
7
24
  "Analizando",
@@ -15,6 +32,9 @@ const VERBS = [
15
32
  "Tejiendo",
16
33
  "Cocinando",
17
34
  ];
35
+ function sinColores(texto) {
36
+ return texto.replace(/\x1b\[[0-9;]*m/g, "");
37
+ }
18
38
  export class DynamicSpinner {
19
39
  timer = null;
20
40
  glyphIdx = 0;
@@ -23,8 +43,6 @@ export class DynamicSpinner {
23
43
  isRunning = false;
24
44
  prefix = "";
25
45
  tokenCount = 0;
26
- /** Lineas extra pintadas debajo del spinner en el ultimo tick. */
27
- extraRows = 0;
28
46
  constructor(prefix = "") {
29
47
  this.prefix = prefix;
30
48
  }
@@ -37,7 +55,7 @@ export class DynamicSpinner {
37
55
  this.startTime = Date.now();
38
56
  this.glyphIdx = 0;
39
57
  this.verbIdx = 0;
40
- this.extraRows = 0;
58
+ this.tick();
41
59
  this.timer = setInterval(() => {
42
60
  this.tick();
43
61
  }, 120);
@@ -47,55 +65,71 @@ export class DynamicSpinner {
47
65
  return;
48
66
  this.glyphIdx = (this.glyphIdx + 1) % GLYPHS.length;
49
67
  const elapsedSec = Math.floor((Date.now() - this.startTime) / 1000);
50
- // Rotate verbs every 2.5 seconds
51
68
  this.verbIdx = Math.floor(elapsedSec / 2.5) % VERBS.length;
69
+ // Las dos juntas: por separado se repinta el bloque dos veces por tick.
70
+ fijarSecciones([
71
+ ["spinner", [this.lineaDelSpinner(elapsedSec)]],
72
+ ["entrada", this.lineasDeEntrada()],
73
+ ]);
74
+ }
75
+ /**
76
+ * `✳ Compactando historial… (3s · ↓ 1.2k tokens · esc para pausar) ⇥ modo`
77
+ *
78
+ * El prefijo se pasaba en `start("Compactando historial")` y no se pintaba en
79
+ * ningun sitio: `tick()` usaba siempre los verbos rotatorios. Asi que las
80
+ * cuatro esperas con nombre propio -compactar, optimizar memoria, verificar-
81
+ * salian todas como "Rumiando…", indistinguibles de una peticion normal.
82
+ */
83
+ lineaDelSpinner(elapsedSec) {
84
+ const cols = Math.max(20, process.stdout.columns || 80);
52
85
  const glyph = chalk.hex("#D97757")(GLYPHS[this.glyphIdx]);
53
- const verb = chalk.italic.hex("#D97757")(`${VERBS[this.verbIdx]}…`);
54
- const timeStr = chalk.gray(`(${elapsedSec}s`);
55
- const tokenStr = this.tokenCount > 0 ? chalk.gray(` · ${(this.tokenCount / 1000).toFixed(1)}k tokens`) : "";
56
- const escStr = chalk.gray(` · esc para pausar)`);
86
+ const verbo = this.prefix || VERBS[this.verbIdx];
87
+ const verb = chalk.italic.hex("#D97757")(`${verbo}…`);
88
+ // El contador va hacia abajo: son los tokens que ESTAN LLEGANDO del modelo.
89
+ const tokenStr = this.tokenCount > 0 ? ` · ${(this.tokenCount / 1000).toFixed(1)}k tokens` : "";
90
+ const cola = chalk.gray(`(${elapsedSec}s${tokenStr} · esc para pausar)`);
91
+ const izquierda = `${glyph} ${verb} ${cola}`;
92
+ const anchoIzq = 1 + 1 + verbo.length + 1 + 1 + `(${elapsedSec}s${tokenStr} · esc para pausar)`.length;
93
+ // La chapa del modo se pega a la derecha, y solo si sobra sitio: antes se
94
+ // concatenaba siempre y en una consola estrecha empujaba la linea a una
95
+ // segunda fila, que es justo lo que descuadra el repintado.
57
96
  const badge = modeBadge();
58
- const modeStr = badge ? ` ${badge}` : "";
59
- this.clearExtraRows();
60
- process.stdout.write(`\r\x1b[2K${glyph} ${verb} ${timeStr}${tokenStr}${escStr}${modeStr}`);
61
- this.drawTypeAhead();
97
+ if (!badge)
98
+ return izquierda;
99
+ const anchoBadge = sinColores(badge).length;
100
+ const hueco = cols - anchoIzq - anchoBadge;
101
+ return hueco >= 2 ? `${izquierda}${" ".repeat(hueco)}${badge}` : izquierda;
62
102
  }
63
103
  /**
64
- * Segunda linea con lo que el usuario esta escribiendo mientras se genera.
104
+ * LA BARRA. La MISMA que se ve al escribir una orden, no una parecida.
65
105
  *
66
- * Antes el teclado se tragaba todo sin pintar nada y parecia que el terminal
67
- * se habia colgado. Se dibuja debajo y el cursor vuelve arriba, para que el
68
- * siguiente tick sobrescriba las dos lineas sin dejar rastro.
106
+ * Antes habia dos: al esperar salia el marco entero -barra, entrada, barra,
107
+ * linea de ayuda-; al trabajar, una version recortada con otro texto. Se
108
+ * parecian lo justo para que el cambio de una a otra se viera como un salto.
109
+ * Palabras del usuario: "aun no es fija... debe ser fija y por debajo deben
110
+ * pasar los otros mensajes".
111
+ *
112
+ * Ahora las dos salen de `buildPromptFrame`, asi que no pueden divergir: quien
113
+ * toque el marco lo toca en los dos estados a la vez.
114
+ *
115
+ * Sin desplegable: el autocompletado de "@" y "/" es del prompt de verdad, que
116
+ * es quien tiene el teclado. Aqui lo tecleado solo se encola.
69
117
  */
70
- drawTypeAhead() {
71
- const typing = typedText();
118
+ lineasDeEntrada() {
119
+ const marco = buildPromptFrame({
120
+ cols: Math.max(20, process.stdout.columns || 80),
121
+ rows: process.stdout.rows || 24,
122
+ input: typedText(),
123
+ cursorIndex: typedText().length,
124
+ items: [],
125
+ });
126
+ const filas = [...marco.lines];
72
127
  const pendientes = queuedCount();
73
- const rows = [];
74
- if (typing.length > 0) {
75
- rows.push(`${chalk.hex("#D97757")("❯")} ${chalk.white(typing)}${chalk.gray(" (enter para encolarlo)")}`);
76
- }
77
128
  if (pendientes > 0) {
78
129
  const plural = pendientes === 1 ? "mensaje en cola" : "mensajes en cola";
79
- rows.push(chalk.gray(` ${pendientes} ${plural}; se enviará al terminar este turno`));
130
+ filas.push(chalk.gray(` ${pendientes} ${plural}; se enviará al terminar este turno`));
80
131
  }
81
- if (rows.length === 0) {
82
- this.extraRows = 0;
83
- return;
84
- }
85
- for (const row of rows) {
86
- process.stdout.write(`\n\r\x1b[2K${row}`);
87
- }
88
- process.stdout.write(`\x1b[${rows.length}A\r`);
89
- this.extraRows = rows.length;
90
- }
91
- clearExtraRows() {
92
- if (this.extraRows === 0)
93
- return;
94
- for (let i = 0; i < this.extraRows; i++) {
95
- process.stdout.write("\x1b[1B\r\x1b[2K");
96
- }
97
- process.stdout.write(`\x1b[${this.extraRows}A\r`);
98
- this.extraRows = 0;
132
+ return filas;
99
133
  }
100
134
  stop() {
101
135
  if (!this.isRunning)
@@ -105,7 +139,48 @@ export class DynamicSpinner {
105
139
  clearInterval(this.timer);
106
140
  this.timer = null;
107
141
  }
108
- this.clearExtraRows();
109
- process.stdout.write("\r\x1b[2K");
142
+ // Solo se va la linea del spinner. La BARRA se queda.
143
+ //
144
+ // Es lo que la hace fija de verdad: entre una herramienta y la siguiente el
145
+ // spinner se para y vuelve a arrancar, y si la barra se fuera con el, se
146
+ // veria desaparecer y reaparecer en cada llamada. Quien la retira es el
147
+ // prompt al abrirse (soltarPie), y ahi el relevo no se nota porque lo que
148
+ // dibuja justo despues es exactamente el mismo marco.
149
+ quitarSeccion("spinner");
150
+ }
151
+ }
152
+ /**
153
+ * DEJA EL PIE PUESTO MIENTRAS CORRE ALGO
154
+ *
155
+ * El spinner solo vivia mientras hablaba el modelo. En cuanto empezaba una
156
+ * herramienta se paraba, y ahi se acababa lo unico fijo de la pantalla: durante
157
+ * un `run_command` de veinte segundos, o una lectura de un archivo enorme, no
158
+ * habia ni cronometro ni forma de saber que seguia vivo. Justo el rato en que
159
+ * uno se pregunta si se ha colgado.
160
+ *
161
+ * EL RETRASO NO ES UN CAPRICHO
162
+ *
163
+ * La mayoria de las herramientas tardan milisegundos. Pintar el pie y borrarlo
164
+ * en el mismo suspiro es un parpadeo en cada llamada, y encima el pie mueve
165
+ * lineas: veinte herramientas seguidas serian veinte sacudidas. Con 150 ms de
166
+ * espera, lo rapido no pinta nada y lo lento sale igual.
167
+ *
168
+ * El `finally` es obligatorio. Un spinner que se queda corriendo repinta el pie
169
+ * ocho veces por segundo encima de lo que venga despues -incluido el prompt-, y
170
+ * de ahi no se sale sin cerrar el programa.
171
+ */
172
+ export async function conSpinner(etiqueta, tarea, retrasoMs = 150) {
173
+ let girando = null;
174
+ const aviso = setTimeout(() => {
175
+ girando = new DynamicSpinner(etiqueta);
176
+ girando.start();
177
+ }, retrasoMs);
178
+ try {
179
+ return await tarea();
180
+ }
181
+ finally {
182
+ clearTimeout(aviso);
183
+ if (girando)
184
+ girando.stop();
110
185
  }
111
186
  }
@@ -747,7 +747,13 @@ async function typeTrusted(tabId, frameId, ref, text, submit, replace) {
747
747
  async function capturar(tabId) {
748
748
  try {
749
749
  return await withTrustedTab(tabId, async (target) => {
750
- const res = await cdp(target, "Page.captureScreenshot", { format: "png", captureBeyondViewport: false });
750
+ // JPEG y no PNG, y no es cosmetico. Una captura en PNG de un monitor
751
+ // normal son 1,3 MB, que en base64 se convierten en 1,75 MB dentro de la
752
+ // peticion al modelo de vision. Eso no llega: la peticion se muere por el
753
+ // camino, no vuelve nada, y se lleva por delante uno de los cuatro huecos
754
+ // de la licencia durante minutos. En JPEG al 70 la misma captura ocupa
755
+ // unos 120 KB y se lee igual de bien.
756
+ const res = await cdp(target, "Page.captureScreenshot", { format: "jpeg", quality: 70, captureBeyondViewport: false });
751
757
  return { data: res.data, via: "cdp" };
752
758
  });
753
759
  } catch (err) {
@@ -762,9 +768,9 @@ async function capturar(tabId) {
762
768
  if (activa && activa.id !== tabId) previaId = activa.id;
763
769
  if (!tab.active) await chrome.tabs.update(tabId, { active: true });
764
770
  await sleep(250);
765
- const dataUrl = await chrome.tabs.captureVisibleTab(windowId, { format: "png" });
771
+ const dataUrl = await chrome.tabs.captureVisibleTab(windowId, { format: "jpeg", quality: 70 });
766
772
  return {
767
- data: String(dataUrl).replace(/^data:image\/png;base64,/, ""),
773
+ data: String(dataUrl).replace(/^data:image\/\w+;base64,/, ""),
768
774
  via: "tabs",
769
775
  warning: "el depurador no estaba disponible (¿DevTools abierto?): captura por la via normal",
770
776
  };
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,48 @@
1
+ # De dónde salen estos iconos
2
+
3
+ **Fuente:** `assets/logos/mascota-chocolatito-code.png` — el zorrito **pixelado**, el mismo
4
+ que aparece en la cabecera del CLI y en la ficha del producto.
5
+
6
+ ## Cuidado con cuál se coge
7
+
8
+ En `assets/logos/` hay dos zorros y **no son la misma marca**:
9
+
10
+ | Archivo | Qué es | ¿Sirve para la extensión? |
11
+ | --- | --- | --- |
12
+ | `mascota-chocolatito-code.png` | El zorrito pixelado. **Chocolatito Code** | ✅ Este |
13
+ | `logo-chocolatito.png` | El zorro vectorial. **Chocolatito**, la marca madre | ❌ Este no |
14
+
15
+ Ya se coló el equivocado una vez: la extensión salió con el zorro de la marca madre y lo
16
+ detectó el dueño de un vistazo. No hay prueba automática que lo cace — se distinguirían por el
17
+ número de colores (674 contra 1707), pero ese umbral se rompe con cualquier retoque legítimo, y
18
+ una prueba frágil es peor que ninguna. Por eso está escrito aquí.
19
+
20
+ ## Cómo se regeneran
21
+
22
+ Con Python y Pillow, desde la raíz del repositorio:
23
+
24
+ ```python
25
+ from PIL import Image
26
+ img = Image.open("assets/logos/mascota-chocolatito-code.png").convert("RGBA")
27
+ img = img.crop(img.split()[3].getbbox()) # fuera el aire de alrededor
28
+
29
+ lado = max(img.size)
30
+ margen = max(1, int(lado * 0.08)) # sin margen, Chrome recorta el borde
31
+ lienzo = Image.new("RGBA", (lado + margen * 2,) * 2, (0, 0, 0, 0))
32
+ lienzo.paste(img, ((lienzo.width - img.width) // 2, (lienzo.height - img.height) // 2), img)
33
+
34
+ for tam in (16, 32, 48, 128):
35
+ # NEAREST hasta 48: es pixel art, y suavizarlo lo convierte en un borron.
36
+ # A 16 px la diferencia es entre verse y no verse.
37
+ filtro = Image.NEAREST if tam <= 48 else Image.LANCZOS
38
+ lienzo.resize((tam, tam), filtro).save(f"extension/iconos/{tam}.png")
39
+ ```
40
+
41
+ **Míralos a 16 px antes de darlos por buenos.** A 128 todo parece correcto; el tamaño real es
42
+ donde se cae. Es lo que fallo con el primer intento, que recortaba una "C" del logotipo y salían
43
+ las dos "C" de las dos filas apiladas.
44
+
45
+ ## El otro archivo
46
+
47
+ `logotipo.png` es el logotipo completo de dos filas a 320 px, y va en la cabecera de
48
+ `popup.html` — ahí sí hay sitio para que se lea entero.
@@ -1,60 +1,60 @@
1
- {
2
- "manifest_version": 3,
3
- "minimum_chrome_version": "102",
4
- "name": "Chocolatito Code",
5
- "version": "1.2.0",
6
- "description": "Deja que Chocolatito Code trabaje dentro de tu Chrome, en segundo plano y con tus sesiones ya iniciadas.",
7
- "permissions": [
8
- "tabs",
9
- "tabGroups",
10
- "scripting",
11
- "debugger",
12
- "webNavigation",
13
- "storage",
14
- "downloads"
15
- ],
16
- "host_permissions": [
17
- "<all_urls>"
18
- ],
19
- "icons": {
20
- "16": "iconos/16.png",
21
- "32": "iconos/32.png",
22
- "48": "iconos/48.png",
23
- "128": "iconos/128.png"
24
- },
25
- "background": {
26
- "service_worker": "background.js"
27
- },
28
- "content_scripts": [
29
- {
30
- "matches": [
31
- "<all_urls>"
32
- ],
33
- "js": [
34
- "content.js"
35
- ],
36
- "run_at": "document_idle",
37
- "all_frames": true
38
- }
39
- ],
40
- "action": {
41
- "default_title": "Chocolatito Code",
42
- "default_popup": "popup.html",
43
- "default_icon": {
44
- "16": "iconos/16.png",
45
- "32": "iconos/32.png",
46
- "48": "iconos/48.png"
47
- }
48
- },
49
- "web_accessible_resources": [
50
- {
51
- "resources": [
52
- "iconos/*",
53
- "fuentes/*"
54
- ],
55
- "matches": [
56
- "<all_urls>"
57
- ]
58
- }
59
- ]
60
- }
1
+ {
2
+ "manifest_version": 3,
3
+ "minimum_chrome_version": "102",
4
+ "name": "Chocolatito Code",
5
+ "version": "1.3.1",
6
+ "description": "Deja que Chocolatito Code trabaje dentro de tu Chrome, en segundo plano y con tus sesiones ya iniciadas.",
7
+ "permissions": [
8
+ "tabs",
9
+ "tabGroups",
10
+ "scripting",
11
+ "debugger",
12
+ "webNavigation",
13
+ "storage",
14
+ "downloads"
15
+ ],
16
+ "host_permissions": [
17
+ "<all_urls>"
18
+ ],
19
+ "icons": {
20
+ "16": "iconos/16.png",
21
+ "32": "iconos/32.png",
22
+ "48": "iconos/48.png",
23
+ "128": "iconos/128.png"
24
+ },
25
+ "background": {
26
+ "service_worker": "background.js"
27
+ },
28
+ "content_scripts": [
29
+ {
30
+ "matches": [
31
+ "<all_urls>"
32
+ ],
33
+ "js": [
34
+ "content.js"
35
+ ],
36
+ "run_at": "document_idle",
37
+ "all_frames": true
38
+ }
39
+ ],
40
+ "action": {
41
+ "default_title": "Chocolatito Code",
42
+ "default_popup": "popup.html",
43
+ "default_icon": {
44
+ "16": "iconos/16.png",
45
+ "32": "iconos/32.png",
46
+ "48": "iconos/48.png"
47
+ }
48
+ },
49
+ "web_accessible_resources": [
50
+ {
51
+ "resources": [
52
+ "iconos/*",
53
+ "fuentes/*"
54
+ ],
55
+ "matches": [
56
+ "<all_urls>"
57
+ ]
58
+ }
59
+ ]
60
+ }