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.
- package/dist/agent/huecos.d.ts +58 -0
- package/dist/agent/huecos.js +81 -0
- package/dist/agent/loop.js +144 -24
- 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/config/quota.d.ts +11 -0
- package/dist/config/quota.js +18 -0
- package/dist/index.js +35 -26
- package/dist/prompts/systemPrompt.js +186 -150
- 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/banner.d.ts +31 -0
- package/dist/ui/banner.js +45 -0
- package/dist/ui/comandos.d.ts +33 -0
- package/dist/ui/comandos.js +49 -0
- package/dist/ui/informeDeUso.d.ts +17 -0
- package/dist/ui/informeDeUso.js +81 -0
- package/dist/ui/interrupt.d.ts +10 -0
- package/dist/ui/interrupt.js +50 -2
- package/dist/ui/marco.d.ts +35 -0
- package/dist/ui/marco.js +161 -0
- package/dist/ui/pieFijo.d.ts +56 -0
- package/dist/ui/pieFijo.js +309 -0
- package/dist/ui/prompt.d.ts +4 -41
- package/dist/ui/prompt.js +24 -157
- package/dist/ui/reasoningStream.js +22 -12
- package/dist/ui/renderer.d.ts +13 -0
- package/dist/ui/renderer.js +41 -12
- package/dist/ui/spinner.d.ts +43 -8
- package/dist/ui/spinner.js +118 -43
- 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 +60 -60
- package/package.json +67 -67
package/dist/ui/renderer.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
+
salida.linea(indented);
|
|
44
59
|
if (remainingCount > 0) {
|
|
45
|
-
|
|
60
|
+
salida.linea(chalk.gray(` … +${remainingCount} líneas más`));
|
|
46
61
|
}
|
|
47
62
|
}
|
|
48
63
|
export function renderToolError(name, errorSummary) {
|
|
49
|
-
|
|
64
|
+
salida.linea(` ${chalk.red("⎿")} ${chalk.red(errorSummary)}`);
|
|
50
65
|
}
|
|
51
66
|
export function renderDiff(filePath, diffText) {
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
+
salida.linea(`\n${parsed}`);
|
|
72
87
|
}
|
|
73
88
|
catch {
|
|
74
|
-
|
|
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
|
|
84
|
-
|
|
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
|
-
|
|
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
|
package/dist/ui/spinner.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
13
|
+
* `✳ Compactando historial… (3s · ↓ 1.2k tokens · esc para pausar) ⇥ modo`
|
|
16
14
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
22
|
-
|
|
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>;
|
package/dist/ui/spinner.js
CHANGED
|
@@ -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.
|
|
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
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
*
|
|
104
|
+
* LA BARRA. La MISMA que se ve al escribir una orden, no una parecida.
|
|
65
105
|
*
|
|
66
|
-
* Antes
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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
|
-
|
|
71
|
-
const
|
|
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
|
-
|
|
130
|
+
filas.push(chalk.gray(` ${pendientes} ${plural}; se enviará al terminar este turno`));
|
|
80
131
|
}
|
|
81
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
}
|
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
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
{
|
|
2
|
-
"manifest_version": 3,
|
|
3
|
-
"minimum_chrome_version": "102",
|
|
4
|
-
"name": "Chocolatito Code",
|
|
5
|
-
"version": "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
|
-
}
|
|
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
|
+
}
|