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.
- package/dist/agent/huecos.d.ts +58 -0
- package/dist/agent/huecos.js +81 -0
- package/dist/agent/loop.js +99 -15
- package/dist/agent/salidaDelAgente.js +10 -24
- package/dist/agent/toolGate.js +6 -1
- package/dist/config/engine.d.ts +21 -1
- package/dist/config/engine.js +22 -1
- package/dist/tools/browserExtension.js +118 -3
- package/dist/tools/computerUse.d.ts +1 -1
- package/dist/tools/computerUse.js +183 -2
- package/dist/tools/toolDefsComputer.js +6 -2
- package/dist/tools/visionBridge.js +60 -3
- package/dist/tools/win/hostScript.js +154 -0
- package/dist/ui/interrupt.d.ts +10 -0
- package/dist/ui/interrupt.js +50 -2
- package/dist/ui/pieFijo.d.ts +56 -0
- package/dist/ui/pieFijo.js +309 -0
- package/dist/ui/prompt.js +9 -1
- package/dist/ui/reasoningStream.js +22 -12
- package/dist/ui/spinner.d.ts +15 -8
- package/dist/ui/spinner.js +80 -42
- package/extension/background.js +9 -3
- package/extension/iconos/128.png +0 -0
- package/extension/iconos/16.png +0 -0
- package/extension/iconos/32.png +0 -0
- package/extension/iconos/48.png +0 -0
- package/extension/iconos/ORIGEN.md +48 -0
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
package/dist/ui/spinner.js
CHANGED
|
@@ -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.
|
|
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
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
*
|
|
112
|
+
* La barra de escribir, debajo del spinner.
|
|
65
113
|
*
|
|
66
|
-
* Antes
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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
|
-
|
|
119
|
+
lineasDeEntrada() {
|
|
120
|
+
const cols = Math.max(20, process.stdout.columns || 80);
|
|
71
121
|
const typing = typedText();
|
|
72
122
|
const pendientes = queuedCount();
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
}
|
package/extension/background.js
CHANGED
|
@@ -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
|
-
|
|
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: "
|
|
771
|
+
const dataUrl = await chrome.tabs.captureVisibleTab(windowId, { format: "jpeg", quality: 70 });
|
|
766
772
|
return {
|
|
767
|
-
data: String(dataUrl).replace(/^data:image
|
|
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
|
};
|
package/extension/iconos/128.png
CHANGED
|
Binary file
|
package/extension/iconos/16.png
CHANGED
|
Binary file
|
package/extension/iconos/32.png
CHANGED
|
Binary file
|
package/extension/iconos/48.png
CHANGED
|
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.
|
package/extension/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"minimum_chrome_version": "102",
|
|
4
4
|
"name": "Chocolatito Code",
|
|
5
|
-
"version": "1.
|
|
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