chocolatito-code 1.2.0 → 1.3.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.
@@ -2,6 +2,22 @@ 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 } from "./pieFijo.js";
6
+ /**
7
+ * LA LINEA QUE DICE QUE ESTO SIGUE VIVO
8
+ *
9
+ * Antes se pintaba con `\r\x1b[2K` sobre la fila donde estuviera el cursor. Eso
10
+ * la deja clavada a esa fila, y en cuanto la respuesta empieza a salir el
11
+ * terminal desplaza el contenido y se lleva el spinner hacia arriba con el.
12
+ * Palabras del usuario: "la nubecita y el rayito suben con lo que me respondes".
13
+ *
14
+ * Ahora se entrega a `pieFijo`, que la vuelve a pintar abajo despues de cada
15
+ * escritura. La linea deja de moverse aunque salgan cien lineas de respuesta.
16
+ *
17
+ * Y por eso mismo ya no hace falta pararlo al llegar el primer trozo: el
18
+ * contador de segundos y de tokens sigue a la vista MIENTRAS el modelo habla,
19
+ * que es justo cuando interesa saber cuanto lleva.
20
+ */
5
21
  const GLYPHS = ["✢", "✳", "∗", "✻", "✽"];
6
22
  const VERBS = [
7
23
  "Analizando",
@@ -15,6 +31,18 @@ const VERBS = [
15
31
  "Tejiendo",
16
32
  "Cocinando",
17
33
  ];
34
+ const PISTA_VACIA = "escribe para encolar un mensaje…";
35
+ function sinColores(texto) {
36
+ return texto.replace(/\x1b\[[0-9;]*m/g, "");
37
+ }
38
+ /** Deja las ULTIMAS `ancho` columnas: al escribir, lo que importa es el final. */
39
+ function colaVisible(texto, ancho) {
40
+ if (ancho <= 0)
41
+ return "";
42
+ if (texto.length <= ancho)
43
+ return texto;
44
+ return `…${texto.slice(-(ancho - 1))}`;
45
+ }
18
46
  export class DynamicSpinner {
19
47
  timer = null;
20
48
  glyphIdx = 0;
@@ -23,8 +51,6 @@ export class DynamicSpinner {
23
51
  isRunning = false;
24
52
  prefix = "";
25
53
  tokenCount = 0;
26
- /** Lineas extra pintadas debajo del spinner en el ultimo tick. */
27
- extraRows = 0;
28
54
  constructor(prefix = "") {
29
55
  this.prefix = prefix;
30
56
  }
@@ -37,7 +63,7 @@ export class DynamicSpinner {
37
63
  this.startTime = Date.now();
38
64
  this.glyphIdx = 0;
39
65
  this.verbIdx = 0;
40
- this.extraRows = 0;
66
+ this.tick();
41
67
  this.timer = setInterval(() => {
42
68
  this.tick();
43
69
  }, 120);
@@ -47,55 +73,63 @@ export class DynamicSpinner {
47
73
  return;
48
74
  this.glyphIdx = (this.glyphIdx + 1) % GLYPHS.length;
49
75
  const elapsedSec = Math.floor((Date.now() - this.startTime) / 1000);
50
- // Rotate verbs every 2.5 seconds
51
76
  this.verbIdx = Math.floor(elapsedSec / 2.5) % VERBS.length;
77
+ // Las dos juntas: por separado se repinta el bloque dos veces por tick.
78
+ fijarSecciones([
79
+ ["spinner", [this.lineaDelSpinner(elapsedSec)]],
80
+ ["entrada", this.lineasDeEntrada()],
81
+ ]);
82
+ }
83
+ /**
84
+ * `✳ Compactando historial… (3s · ↓ 1.2k tokens · esc para pausar) ⇥ modo`
85
+ *
86
+ * El prefijo se pasaba en `start("Compactando historial")` y no se pintaba en
87
+ * ningun sitio: `tick()` usaba siempre los verbos rotatorios. Asi que las
88
+ * cuatro esperas con nombre propio -compactar, optimizar memoria, verificar-
89
+ * salian todas como "Rumiando…", indistinguibles de una peticion normal.
90
+ */
91
+ lineaDelSpinner(elapsedSec) {
92
+ const cols = Math.max(20, process.stdout.columns || 80);
52
93
  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)`);
94
+ const verbo = this.prefix || VERBS[this.verbIdx];
95
+ const verb = chalk.italic.hex("#D97757")(`${verbo}…`);
96
+ // El contador va hacia abajo: son los tokens que ESTAN LLEGANDO del modelo.
97
+ const tokenStr = this.tokenCount > 0 ? ` · ${(this.tokenCount / 1000).toFixed(1)}k tokens` : "";
98
+ const cola = chalk.gray(`(${elapsedSec}s${tokenStr} · esc para pausar)`);
99
+ const izquierda = `${glyph} ${verb} ${cola}`;
100
+ const anchoIzq = 1 + 1 + verbo.length + 1 + 1 + `(${elapsedSec}s${tokenStr} · esc para pausar)`.length;
101
+ // La chapa del modo se pega a la derecha, y solo si sobra sitio: antes se
102
+ // concatenaba siempre y en una consola estrecha empujaba la linea a una
103
+ // segunda fila, que es justo lo que descuadra el repintado.
57
104
  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();
105
+ if (!badge)
106
+ return izquierda;
107
+ const anchoBadge = sinColores(badge).length;
108
+ const hueco = cols - anchoIzq - anchoBadge;
109
+ return hueco >= 2 ? `${izquierda}${" ".repeat(hueco)}${badge}` : izquierda;
62
110
  }
63
111
  /**
64
- * Segunda linea con lo que el usuario esta escribiendo mientras se genera.
112
+ * La barra de escribir, debajo del spinner.
65
113
  *
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.
114
+ * Antes solo aparecia si ya habias escrito algo, asi que mientras el agente
115
+ * trabajaba no habia ninguna senal de que se pudiera escribir: el teclado es
116
+ * del vigilante y lo tecleado se acumulaba a ciegas. Ahora la barra esta
117
+ * siempre, y es la misma que se ve al escribir el prompt.
69
118
  */
70
- drawTypeAhead() {
119
+ lineasDeEntrada() {
120
+ const cols = Math.max(20, process.stdout.columns || 80);
71
121
  const typing = typedText();
72
122
  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
- }
123
+ const simbolo = chalk.bold.hex("#D97757")("❯");
124
+ const cuerpo = typing.length > 0
125
+ ? `${chalk.white(colaVisible(typing, cols - 3))}${chalk.hex("#D97757")("▌")}`
126
+ : chalk.gray(colaVisible(PISTA_VACIA, cols - 2));
127
+ const filas = [chalk.gray("─".repeat(cols)), `${simbolo} ${cuerpo}`];
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`));
80
- }
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");
130
+ filas.push(chalk.gray(` ${pendientes} ${plural}; se enviará al terminar este turno`));
96
131
  }
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,11 @@ 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
+ // Las dos de golpe: quitarlas por separado deja un fotograma con la barra
143
+ // de escribir suelta y sin spinner encima.
144
+ fijarSecciones([
145
+ ["spinner", []],
146
+ ["entrada", []],
147
+ ]);
110
148
  }
111
149
  }
@@ -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.
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "minimum_chrome_version": "102",
4
4
  "name": "Chocolatito Code",
5
- "version": "1.2.0",
5
+ "version": "1.3.0",
6
6
  "description": "Deja que Chocolatito Code trabaje dentro de tu Chrome, en segundo plano y con tus sesiones ya iniciadas.",
7
7
  "permissions": [
8
8
  "tabs",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chocolatito-code",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Agente autónomo de programación para la terminal, con control real del ordenador y del navegador. Desarrollado por Chocolatito.",
5
5
  "type": "module",
6
6
  "bin": {