chocolatito-code 1.1.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/README.md +66 -0
- package/dist/agent/huecos.d.ts +58 -0
- package/dist/agent/huecos.js +81 -0
- package/dist/agent/loop.js +218 -41
- package/dist/agent/salidaDelAgente.d.ts +43 -0
- package/dist/agent/salidaDelAgente.js +39 -0
- 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/nodeVersion.d.ts +13 -0
- package/dist/config/nodeVersion.js +26 -0
- package/dist/index.js +22 -7
- package/dist/mcp/client.d.ts +28 -0
- package/dist/mcp/client.js +62 -8
- package/dist/mcp/ide.d.ts +76 -0
- package/dist/mcp/ide.js +160 -0
- package/dist/mcp/manager.js +9 -0
- package/dist/tools/browserExtension.js +153 -7
- package/dist/tools/computerUse.d.ts +1 -1
- package/dist/tools/computerUse.js +183 -2
- package/dist/tools/definitions.js +9 -2
- package/dist/tools/grepSearch.js +111 -23
- package/dist/tools/runner.js +1 -1
- package/dist/tools/toolDefsComputer.js +6 -2
- package/dist/tools/visionBridge.js +60 -3
- package/dist/tools/win/hostScript.js +154 -0
- package/dist/tools/writeFile.d.ts +16 -1
- package/dist/tools/writeFile.js +22 -1
- package/dist/ui/interrupt.d.ts +10 -0
- package/dist/ui/interrupt.js +53 -4
- package/dist/ui/keyboardGuard.d.ts +21 -0
- package/dist/ui/keyboardGuard.js +30 -1
- package/dist/ui/pegados.d.ts +50 -0
- package/dist/ui/pegados.js +83 -0
- package/dist/ui/pieFijo.d.ts +56 -0
- package/dist/ui/pieFijo.js +309 -0
- package/dist/ui/prompt.d.ts +5 -0
- package/dist/ui/prompt.js +109 -16
- package/dist/ui/reasoningStream.js +22 -12
- package/dist/ui/salida.d.ts +10 -0
- package/dist/ui/salida.js +50 -0
- package/dist/ui/spinner.d.ts +15 -8
- package/dist/ui/spinner.js +80 -42
- package/extension/background.js +191 -14
- package/extension/content.js +446 -48
- package/extension/fuentes/Chocolatito-Marca.ttf +0 -0
- 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/iconos/logotipo.png +0 -0
- package/extension/manifest.json +27 -4
- package/extension/popup.html +16 -6
- package/package.json +3 -2
|
@@ -82,7 +82,10 @@ export const COMPUTER_USE_TOOL = {
|
|
|
82
82
|
"REGLA DE ORO: nunca hagas clic en una coordenada que no te haya dado antes una herramienta. " +
|
|
83
83
|
"FLUJO CORRECTO: 'list_windows' para saber que hay abierto -> 'ui_snapshot' con la ventana objetivo para listar sus controles reales -> " +
|
|
84
84
|
"'ui_click' / 'ui_type' con el numero de ref. Eso actua en segundo plano, sin mover el raton ni robar el foco al usuario. " +
|
|
85
|
-
"
|
|
85
|
+
"CUANTO CUESTA CADA COSA, que decide cual usar: ui_snapshot tarda unos 40 ms y te devuelve texto que entiendes directo. "
|
|
86
|
+
+ "screenshot cuesta una llamada entera a otro modelo -varios segundos- mas los tokens de la imagen. "
|
|
87
|
+
+ "O sea que screenshot es unas cien veces mas caro. Usalo SOLO cuando el arbol de accesibilidad no vea el control: lienzos, juegos, imagenes, interfaces dibujadas a mano. "
|
|
88
|
+
+ "Para todo lo demas, ui_snapshot y luego find_element. " +
|
|
86
89
|
"Las coordenadas siempre son pixeles fisicos de pantalla.",
|
|
87
90
|
parameters: {
|
|
88
91
|
type: "object",
|
|
@@ -109,6 +112,7 @@ export const COMPUTER_USE_TOOL = {
|
|
|
109
112
|
"type",
|
|
110
113
|
"key",
|
|
111
114
|
"wait",
|
|
115
|
+
"wait_change",
|
|
112
116
|
"cursor_position",
|
|
113
117
|
"focus_window",
|
|
114
118
|
"get_active_window",
|
|
@@ -116,7 +120,7 @@ export const COMPUTER_USE_TOOL = {
|
|
|
116
120
|
],
|
|
117
121
|
description: "list_windows: inventario de ventanas. ui_snapshot: lista los controles reales de una ventana con su ref. " +
|
|
118
122
|
"ui_click/ui_type/ui_focus: actuan sobre un ref sin tocar el raton. find_element: busca un control por texto y, si hace falta, lo localiza con vision. " +
|
|
119
|
-
"screenshot: captura una ventana concreta (aunque este tapada) y la describe. click/type/key: entrada real, solo como ultimo recurso.",
|
|
123
|
+
"wait_change: se suscribe a la ventana y vuelve EN CUANTO cambia algo, con el estado nuevo. Usalo en vez de wait siempre que esperes una reaccion: wait duerme un tiempo fijo (corto si la app tarda, desperdiciado si no). " + "screenshot: captura una ventana concreta (aunque este tapada) y la describe con otro modelo; lento y caro, ultimo recurso para lo que el arbol no ve. click/type/key: entrada real, solo como ultimo recurso.",
|
|
120
124
|
},
|
|
121
125
|
window: {
|
|
122
126
|
type: "string",
|
|
@@ -5,15 +5,72 @@ import { apuntarUso } from "../agent/usageMeter.js";
|
|
|
5
5
|
function client(apiKey) {
|
|
6
6
|
return createEngineClient(apiKey, "vision");
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Cuanto puede pesar una imagen antes de que la peticion se muera por el camino.
|
|
10
|
+
*
|
|
11
|
+
* EL FALLO QUE CIERRA ESTO
|
|
12
|
+
*
|
|
13
|
+
* `toDataUrl` leia el archivo entero y lo metia en base64 sin mirar el tamano.
|
|
14
|
+
* Una captura de pantalla en PNG de un monitor normal son 1,3 MB, que en base64
|
|
15
|
+
* se convierten en 1,75 MB dentro del JSON de la peticion. Eso no llega: el
|
|
16
|
+
* Worker del proxy se queda sin margen a mitad de camino, la peticion muere sin
|
|
17
|
+
* respuesta, y ENCIMA se lleva por delante uno de los cuatro huecos de la
|
|
18
|
+
* licencia.
|
|
19
|
+
*
|
|
20
|
+
* Desde fuera se veia asi, y el usuario lo sufrio una tarde entera:
|
|
21
|
+
*
|
|
22
|
+
* chrome(screenshot) → chrome_tab.png (1314.8 KB)
|
|
23
|
+
* 💭 penso 4s
|
|
24
|
+
* ⚠ El motor no devolvio nada.
|
|
25
|
+
* ...
|
|
26
|
+
* ⏸ Tienes 4 peticiones en curso. Se libera en 169s.
|
|
27
|
+
*
|
|
28
|
+
* Cuatro capturas grandes y la licencia queda bloqueada tres minutos, sin que
|
|
29
|
+
* nada explique por que. Ahora se para ANTES de mandarla y se dice como
|
|
30
|
+
* arreglarlo, que cuesta lo mismo y no gasta un hueco.
|
|
31
|
+
*
|
|
32
|
+
* El limite no es caprichoso: 900 KB de base64 son ~675 KB de imagen, muy por
|
|
33
|
+
* encima de lo que ocupa una captura en JPEG (unos 120 KB) y muy por debajo de
|
|
34
|
+
* donde empiezan los problemas.
|
|
35
|
+
*/
|
|
36
|
+
const MAX_BASE64 = 900_000;
|
|
37
|
+
/** El tipo real de la imagen, mirando sus primeros bytes y no su extension. */
|
|
38
|
+
function tipoDeImagen(buf) {
|
|
39
|
+
if (buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
|
|
40
|
+
return "image/jpeg";
|
|
41
|
+
if (buf.length > 8 && buf[0] === 0x89 && buf[1] === 0x50)
|
|
42
|
+
return "image/png";
|
|
43
|
+
if (buf.length > 12 && buf.toString("ascii", 8, 12) === "WEBP")
|
|
44
|
+
return "image/webp";
|
|
45
|
+
// Se deja pasar como PNG: equivocarse aqui es un error del servidor, no un
|
|
46
|
+
// cuelgue silencioso.
|
|
47
|
+
return "image/png";
|
|
48
|
+
}
|
|
49
|
+
function prepararImagen(imagePath) {
|
|
9
50
|
const buf = fs.readFileSync(imagePath);
|
|
10
|
-
|
|
51
|
+
const b64 = buf.toString("base64");
|
|
52
|
+
if (b64.length > MAX_BASE64) {
|
|
53
|
+
const kb = Math.round(buf.length / 1024);
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
error: `La imagen pesa ${kb} KB y no cabe en una peticion (el tope son ${Math.round(MAX_BASE64 / 1024)} KB de datos). ` +
|
|
57
|
+
`Mandarla igualmente no falla con un error: la peticion se muere por el camino, no vuelve nada, y se pierde ` +
|
|
58
|
+
`uno de los cuatro huecos de peticion durante minutos.\n\n` +
|
|
59
|
+
`Vuelve a capturar en JPEG, o recorta la zona que te interesa en vez de la pantalla entera.`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return { ok: true, url: `data:${tipoDeImagen(buf)};base64,${b64}` };
|
|
11
63
|
}
|
|
12
64
|
async function ask(apiKey, prompt, imagePath, maxTokens) {
|
|
13
65
|
if (!apiKey)
|
|
14
66
|
return { ok: false, error: "No hay API key disponible para el modelo de vision." };
|
|
15
67
|
if (!fs.existsSync(imagePath))
|
|
16
68
|
return { ok: false, error: `No existe la imagen "${imagePath}".` };
|
|
69
|
+
// Se comprueba ANTES de gastar un hueco de peticion: una imagen demasiado
|
|
70
|
+
// grande no da error, mata la peticion en silencio. Ver prepararImagen().
|
|
71
|
+
const imagen = prepararImagen(imagePath);
|
|
72
|
+
if (!imagen.ok)
|
|
73
|
+
return { ok: false, error: imagen.error };
|
|
17
74
|
try {
|
|
18
75
|
const res = await client(apiKey).chat.completions.create({
|
|
19
76
|
model: VISION_MODEL,
|
|
@@ -24,7 +81,7 @@ async function ask(apiKey, prompt, imagePath, maxTokens) {
|
|
|
24
81
|
role: "user",
|
|
25
82
|
content: [
|
|
26
83
|
{ type: "text", text: prompt },
|
|
27
|
-
{ type: "image_url", image_url: { url:
|
|
84
|
+
{ type: "image_url", image_url: { url: imagen.url } },
|
|
28
85
|
],
|
|
29
86
|
},
|
|
30
87
|
],
|
|
@@ -518,6 +518,155 @@ public static class Choco
|
|
|
518
518
|
return null;
|
|
519
519
|
}
|
|
520
520
|
|
|
521
|
+
// Lo que el elemento dice SER AHORA MISMO, con el mismo formato que se
|
|
522
|
+
// guardo al hacer la foto. Si un elemento murio, aqui salta y se trata como
|
|
523
|
+
// cambio, que es lo correcto.
|
|
524
|
+
static string EtiquetaViva(AutomationElement e)
|
|
525
|
+
{
|
|
526
|
+
try { return RoleOf(e) + " " + (e.Current.Name == null ? "" : e.Current.Name); }
|
|
527
|
+
catch { return "(elemento muerto)"; }
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Un ref solo vale si sigue siendo lo que era.
|
|
531
|
+
//
|
|
532
|
+
// POR QUE
|
|
533
|
+
//
|
|
534
|
+
// ui_snapshot guarda los elementos en _snap y su etiqueta en _snapLabel, y
|
|
535
|
+
// hasta ahora _snapLabel solo servia para INFORMAR: nunca se comparaba. Pero
|
|
536
|
+
// entre mirar y actuar pasa tiempo, y las interfaces cambian solas -una
|
|
537
|
+
// lista que se refresca, un menu que se cierra, un dialogo que aparece-. El
|
|
538
|
+
// elemento guardado en el ref 3 sigue ahi, pero ya no es el boton que el
|
|
539
|
+
// modelo vio: es otro. Y se pulsa ese otro, sin que nada avise.
|
|
540
|
+
//
|
|
541
|
+
// Es la misma familia del fallo que ya mordio dos veces en el navegador
|
|
542
|
+
// (hallazgos #19 y #20): actuar sobre lo que habia antes creyendo que es lo
|
|
543
|
+
// de ahora. Alli se arreglo; aqui seguia abierto.
|
|
544
|
+
//
|
|
545
|
+
// Comparar la etiqueta cuesta una lectura y convierte un clic en el sitio
|
|
546
|
+
// equivocado -que el usuario descubre cuando ya se ha borrado algo- en un
|
|
547
|
+
// mensaje que el modelo puede resolver volviendo a mirar.
|
|
548
|
+
static string RefCambiado(int id)
|
|
549
|
+
{
|
|
550
|
+
AutomationElement e = Ref(id);
|
|
551
|
+
if (e == null) return "ref inexistente; repite ui_snapshot";
|
|
552
|
+
|
|
553
|
+
string antes;
|
|
554
|
+
if (!_snapLabel.TryGetValue(id, out antes)) return null;
|
|
555
|
+
|
|
556
|
+
string ahora = EtiquetaViva(e);
|
|
557
|
+
if (ahora == antes) return null;
|
|
558
|
+
|
|
559
|
+
if (ahora == "(elemento muerto)")
|
|
560
|
+
{
|
|
561
|
+
return "el elemento del ref " + N(id) + " (" + antes + ") ya no existe: la ventana cambio. Repite ui_snapshot.";
|
|
562
|
+
}
|
|
563
|
+
return "el ref " + N(id) + " ya no es lo que viste: era \"" + antes + "\" y ahora es \"" + ahora +
|
|
564
|
+
"\". Actuar sobre el seria pulsar otra cosa. Repite ui_snapshot y usa el numero nuevo.";
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ------------------------------------------------------------- esperar cambios
|
|
568
|
+
//
|
|
569
|
+
// Esperar a que la ventana haga algo, en vez de preguntarle cada tanto.
|
|
570
|
+
//
|
|
571
|
+
// POR QUE
|
|
572
|
+
//
|
|
573
|
+
// Hasta ahora, para saber si una accion habia surtido efecto se leia el arbol
|
|
574
|
+
// varias veces seguidas hasta que dos salieran iguales. Funciona, pero tiene
|
|
575
|
+
// dos costes: se lee el arbol entero N veces, y la respuesta llega en el
|
|
576
|
+
// siguiente sondeo, no cuando pasa la cosa. Con una ventana que tarda 40 ms en
|
|
577
|
+
// reaccionar y un sondeo cada 120, se pierden 80 ms de nada; con una que tarda
|
|
578
|
+
// 2 s, se leyo el arbol veinte veces para nada.
|
|
579
|
+
//
|
|
580
|
+
// UI Automation puede avisar. Aqui se suscribe a los cambios de la ventana, se
|
|
581
|
+
// espera bloqueado, y se vuelve en cuanto ocurre el primero.
|
|
582
|
+
//
|
|
583
|
+
// LO QUE HAY QUE HACER BIEN O NO HACERLO
|
|
584
|
+
//
|
|
585
|
+
// Los manejadores de UIA viven dentro del proceso de la aplicacion observada.
|
|
586
|
+
// Si se registran y no se quitan, esa aplicacion se queda mas lenta PARA
|
|
587
|
+
// SIEMPRE, hasta que el usuario la cierre. Por eso todo va dentro de un
|
|
588
|
+
// try/finally y se quitan pase lo que pase, incluido si salta una excepcion o
|
|
589
|
+
// se agota el tiempo.
|
|
590
|
+
//
|
|
591
|
+
// El evento llega en un hilo interno de UIA, no en este: por eso lo que se
|
|
592
|
+
// comparte es un ManualResetEventSlim y una cadena, y nada mas.
|
|
593
|
+
static string _cambioQue = "";
|
|
594
|
+
static readonly object _cambioLock = new object();
|
|
595
|
+
|
|
596
|
+
static void ApuntarCambio(string texto)
|
|
597
|
+
{
|
|
598
|
+
lock (_cambioLock) { if (_cambioQue.Length == 0) _cambioQue = texto; }
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
public static string WaitChange(IntPtr hwnd, int timeoutMs)
|
|
602
|
+
{
|
|
603
|
+
if (hwnd == IntPtr.Zero) return "{\"ok\":false,\"error\":\"ventana no encontrada\"}";
|
|
604
|
+
if (timeoutMs < 100) timeoutMs = 100;
|
|
605
|
+
if (timeoutMs > 30000) timeoutMs = 30000;
|
|
606
|
+
|
|
607
|
+
AutomationElement raiz;
|
|
608
|
+
try { raiz = AutomationElement.FromHandle(hwnd); }
|
|
609
|
+
catch (Exception ex) { return "{\"ok\":false,\"error\":" + Q(ex.Message) + "}"; }
|
|
610
|
+
if (raiz == null) return "{\"ok\":false,\"error\":\"la ventana no expone arbol de accesibilidad\"}";
|
|
611
|
+
|
|
612
|
+
lock (_cambioLock) { _cambioQue = ""; }
|
|
613
|
+
ManualResetEventSlim listo = new ManualResetEventSlim(false);
|
|
614
|
+
|
|
615
|
+
AutomationPropertyChangedEventHandler onProp = delegate(object src, AutomationPropertyChangedEventArgs e)
|
|
616
|
+
{
|
|
617
|
+
string prop = e.Property == ValuePattern.ValueProperty ? "valor"
|
|
618
|
+
: e.Property == AutomationElement.NameProperty ? "nombre"
|
|
619
|
+
: e.Property == AutomationElement.IsEnabledProperty ? "habilitado"
|
|
620
|
+
: "propiedad";
|
|
621
|
+
string quien = "";
|
|
622
|
+
try { AutomationElement el = src as AutomationElement; if (el != null) quien = RoleOf(el) + " " + el.Current.Name; }
|
|
623
|
+
catch { quien = "un control"; }
|
|
624
|
+
ApuntarCambio("cambio el " + prop + " de " + quien);
|
|
625
|
+
listo.Set();
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
StructureChangedEventHandler onStruct = delegate(object src, StructureChangedEventArgs e)
|
|
629
|
+
{
|
|
630
|
+
ApuntarCambio("la ventana anadio o quito controles");
|
|
631
|
+
listo.Set();
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
bool propOk = false, structOk = false;
|
|
635
|
+
try
|
|
636
|
+
{
|
|
637
|
+
AutomationProperty[] props = new AutomationProperty[] {
|
|
638
|
+
ValuePattern.ValueProperty,
|
|
639
|
+
AutomationElement.NameProperty,
|
|
640
|
+
AutomationElement.IsEnabledProperty
|
|
641
|
+
};
|
|
642
|
+
try { Automation.AddAutomationPropertyChangedEventHandler(raiz, TreeScope.Subtree, onProp, props); propOk = true; } catch { }
|
|
643
|
+
try { Automation.AddStructureChangedEventHandler(raiz, TreeScope.Subtree, onStruct); structOk = true; } catch { }
|
|
644
|
+
|
|
645
|
+
if (!propOk && !structOk)
|
|
646
|
+
{
|
|
647
|
+
return "{\"ok\":false,\"error\":\"esta ventana no admite avisos de cambio; usa ui_snapshot para mirar\"}";
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
bool paso = listo.Wait(timeoutMs);
|
|
651
|
+
string que;
|
|
652
|
+
lock (_cambioLock) { que = _cambioQue; }
|
|
653
|
+
|
|
654
|
+
if (!paso)
|
|
655
|
+
{
|
|
656
|
+
return "{\"ok\":true,\"changed\":false,\"waited\":" + N(timeoutMs) + "}";
|
|
657
|
+
}
|
|
658
|
+
return "{\"ok\":true,\"changed\":true,\"what\":" + Q(que.Length > 0 ? que : "algo cambio") + "}";
|
|
659
|
+
}
|
|
660
|
+
finally
|
|
661
|
+
{
|
|
662
|
+
// Sin esto, la aplicacion observada se queda mas lenta hasta que el
|
|
663
|
+
// usuario la cierre. Se quitan siempre, aunque haya saltado todo.
|
|
664
|
+
if (propOk) { try { Automation.RemoveAutomationPropertyChangedEventHandler(raiz, onProp); } catch { } }
|
|
665
|
+
if (structOk) { try { Automation.RemoveStructureChangedEventHandler(raiz, onStruct); } catch { } }
|
|
666
|
+
listo.Dispose();
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
521
670
|
public static string RefRect(int id)
|
|
522
671
|
{
|
|
523
672
|
AutomationElement e = Ref(id);
|
|
@@ -537,6 +686,8 @@ public static class Choco
|
|
|
537
686
|
{
|
|
538
687
|
AutomationElement e = Ref(id);
|
|
539
688
|
if (e == null) return "{\"ok\":false,\"error\":\"ref inexistente; repite ui_snapshot\"}";
|
|
689
|
+
string cambio = RefCambiado(id);
|
|
690
|
+
if (cambio != null) return "{\"ok\":false,\"error\":" + Q(cambio) + "}";
|
|
540
691
|
string label = _snapLabel.ContainsKey(id) ? _snapLabel[id] : ("ref " + N(id));
|
|
541
692
|
try
|
|
542
693
|
{
|
|
@@ -555,6 +706,8 @@ public static class Choco
|
|
|
555
706
|
{
|
|
556
707
|
AutomationElement e = Ref(id);
|
|
557
708
|
if (e == null) return "{\"ok\":false,\"error\":\"ref inexistente; repite ui_snapshot\"}";
|
|
709
|
+
string cambio = RefCambiado(id);
|
|
710
|
+
if (cambio != null) return "{\"ok\":false,\"error\":" + Q(cambio) + "}";
|
|
558
711
|
try
|
|
559
712
|
{
|
|
560
713
|
// ValuePattern.SetValue SUSTITUYE todo el contenido del control. En un
|
|
@@ -902,6 +1055,7 @@ while ($true) {
|
|
|
902
1055
|
'ui_snapshot' {
|
|
903
1056
|
$res = [Choco]::Snapshot((Resolve-Hwnd $cmd), [int](Get-Prop $cmd 'max' 250), [string](Get-Prop $cmd 'filter' ''), [bool](Get-Prop $cmd 'interactiveOnly' $true))
|
|
904
1057
|
}
|
|
1058
|
+
'wait_change' { $res = [Choco]::WaitChange((Resolve-Hwnd $cmd), [int](Get-Prop $cmd 'timeoutMs' 3000)) }
|
|
905
1059
|
'ui_click' { $res = [Choco]::RefInvoke([int](Get-Prop $cmd 'ref' 0)) }
|
|
906
1060
|
'ui_set_value' { $res = [Choco]::RefSetValue([int](Get-Prop $cmd 'ref' 0), [string](Get-Prop $cmd 'text' ''), [bool](Get-Prop $cmd 'replace' $false)) }
|
|
907
1061
|
'ui_focus' { $res = [Choco]::RefFocus([int](Get-Prop $cmd 'ref' 0)) }
|
|
@@ -1 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Escribe un archivo, entero o por partes.
|
|
3
|
+
*
|
|
4
|
+
* POR QUE EXISTE "anadir"
|
|
5
|
+
*
|
|
6
|
+
* La respuesta del modelo tiene un techo de salida. Cuando le piden una pagina
|
|
7
|
+
* entera, el contenido no cabe, la respuesta se corta a mitad de los argumentos
|
|
8
|
+
* y no se escribe nada. Se le decia "hazlo en varias llamadas mas pequenas"...
|
|
9
|
+
* y no habia forma de hacerlo: write_file solo sabia sobrescribir, asi que la
|
|
10
|
+
* segunda llamada borraba la primera. Le pediamos algo imposible y luego le
|
|
11
|
+
* echabamos la culpa.
|
|
12
|
+
*
|
|
13
|
+
* Con `modo: "anadir"` la instruccion pasa a ser ejecutable: primera llamada
|
|
14
|
+
* con el principio, las siguientes anadiendo, y el archivo sale entero.
|
|
15
|
+
*/
|
|
16
|
+
export declare function writeFile(filePath: string, content: string, cwd?: string, modo?: "sobrescribir" | "anadir"): Promise<string>;
|
package/dist/tools/writeFile.js
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { binaryExtensionLabel, fileLooksBinary } from "./binary.js";
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Escribe un archivo, entero o por partes.
|
|
6
|
+
*
|
|
7
|
+
* POR QUE EXISTE "anadir"
|
|
8
|
+
*
|
|
9
|
+
* La respuesta del modelo tiene un techo de salida. Cuando le piden una pagina
|
|
10
|
+
* entera, el contenido no cabe, la respuesta se corta a mitad de los argumentos
|
|
11
|
+
* y no se escribe nada. Se le decia "hazlo en varias llamadas mas pequenas"...
|
|
12
|
+
* y no habia forma de hacerlo: write_file solo sabia sobrescribir, asi que la
|
|
13
|
+
* segunda llamada borraba la primera. Le pediamos algo imposible y luego le
|
|
14
|
+
* echabamos la culpa.
|
|
15
|
+
*
|
|
16
|
+
* Con `modo: "anadir"` la instruccion pasa a ser ejecutable: primera llamada
|
|
17
|
+
* con el principio, las siguientes anadiendo, y el archivo sale entero.
|
|
18
|
+
*/
|
|
19
|
+
export async function writeFile(filePath, content, cwd = process.cwd(), modo = "sobrescribir") {
|
|
5
20
|
const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
6
21
|
const dir = path.dirname(resolved);
|
|
7
22
|
const isNew = !fs.existsSync(resolved);
|
|
@@ -24,6 +39,12 @@ export async function writeFile(filePath, content, cwd = process.cwd()) {
|
|
|
24
39
|
if (!fs.existsSync(dir)) {
|
|
25
40
|
fs.mkdirSync(dir, { recursive: true });
|
|
26
41
|
}
|
|
42
|
+
if (modo === "anadir") {
|
|
43
|
+
fs.appendFileSync(resolved, content, "utf-8");
|
|
44
|
+
const total = fs.statSync(resolved).size;
|
|
45
|
+
return (`✔ Anadido a "${filePath}" (${content.length} caracteres nuevos, ${total} en total). ` +
|
|
46
|
+
`Sigue con otra llamada en modo "anadir" si falta contenido.`);
|
|
47
|
+
}
|
|
27
48
|
fs.writeFileSync(resolved, content, "utf-8");
|
|
28
49
|
const action = isNew ? "creado" : "sobreescrito";
|
|
29
50
|
return `✔ Archivo "${filePath}" ${action} exitosamente (${content.length} caracteres).`;
|
package/dist/ui/interrupt.d.ts
CHANGED
|
@@ -3,6 +3,14 @@ export declare function tomarTecladoParaPrompt(): void;
|
|
|
3
3
|
/** Lo llama el prompt al cerrarse, para que el siguiente turno pueda restaurar. */
|
|
4
4
|
export declare function devolverTecladoDelPrompt(): void;
|
|
5
5
|
export declare function releaseKeyboard(): void;
|
|
6
|
+
/**
|
|
7
|
+
* Devuelve el teclado a quien lo tenia antes del dialogo.
|
|
8
|
+
*
|
|
9
|
+
* NO se llama a start(): eso crearia un AbortController nuevo, y la peticion en
|
|
10
|
+
* curso se quedaria escuchando al viejo. Esc dejaria de cancelar justo lo que el
|
|
11
|
+
* usuario quiere cancelar.
|
|
12
|
+
*/
|
|
13
|
+
export declare function retomarTeclado(): void;
|
|
6
14
|
export declare class InterruptWatcher {
|
|
7
15
|
private controller;
|
|
8
16
|
private wasRaw;
|
|
@@ -13,6 +21,8 @@ export declare class InterruptWatcher {
|
|
|
13
21
|
get interrupted(): boolean;
|
|
14
22
|
get signal(): AbortSignal | undefined;
|
|
15
23
|
start(): AbortSignal | undefined;
|
|
24
|
+
/** Vuelve a escuchar despues de un dialogo, sin tocar la senal de aborto. */
|
|
25
|
+
reanudar(): void;
|
|
16
26
|
private abort;
|
|
17
27
|
stop(): void;
|
|
18
28
|
}
|
package/dist/ui/interrupt.js
CHANGED
|
@@ -3,6 +3,8 @@ import process from "node:process";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { cycleMode, modeAnnouncement } from "./modes.js";
|
|
5
5
|
import { pushTyped, backspaceTyped, commitTyped } from "./typeAhead.js";
|
|
6
|
+
import { reafirmarModoCrudo } from "./keyboardGuard.js";
|
|
7
|
+
import { dibujar } from "./salida.js";
|
|
6
8
|
/**
|
|
7
9
|
* Interrupcion con Esc mientras el agente trabaja.
|
|
8
10
|
*
|
|
@@ -68,11 +70,39 @@ export function tomarTecladoParaPrompt() {
|
|
|
68
70
|
export function devolverTecladoDelPrompt() {
|
|
69
71
|
promptConElTeclado = false;
|
|
70
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Vigilantes que se soltaron para ensenar un dialogo, y que hay que devolver.
|
|
75
|
+
*
|
|
76
|
+
* El dialogo de permisos suelta el teclado antes de dibujarse -si no, el
|
|
77
|
+
* vigilante se come las teclas y la caja no responde-, pero hasta ahora nadie lo
|
|
78
|
+
* devolvia despues. El resto del turno se quedaba SIN vigilante: en modo cocido
|
|
79
|
+
* el terminal hacia el eco por su cuenta, Esc dejaba de cancelar, y lo que el
|
|
80
|
+
* usuario escribiera no se encolaba.
|
|
81
|
+
*
|
|
82
|
+
* Se veia asi, y el usuario dio con la pista sin saberlo: "a veces se queda
|
|
83
|
+
* trabado y no se puede escribir, pero con darle Enter vuelve a la normalidad".
|
|
84
|
+
* Claro: en modo cocido el terminal guarda la linea y Enter la suelta de golpe.
|
|
85
|
+
*/
|
|
86
|
+
let soltados = [];
|
|
71
87
|
export function releaseKeyboard() {
|
|
72
|
-
|
|
88
|
+
soltados = Array.from(activos);
|
|
89
|
+
for (const w of soltados) {
|
|
73
90
|
w.stop();
|
|
74
91
|
}
|
|
75
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Devuelve el teclado a quien lo tenia antes del dialogo.
|
|
95
|
+
*
|
|
96
|
+
* NO se llama a start(): eso crearia un AbortController nuevo, y la peticion en
|
|
97
|
+
* curso se quedaria escuchando al viejo. Esc dejaria de cancelar justo lo que el
|
|
98
|
+
* usuario quiere cancelar.
|
|
99
|
+
*/
|
|
100
|
+
export function retomarTeclado() {
|
|
101
|
+
const devolver = soltados;
|
|
102
|
+
soltados = [];
|
|
103
|
+
for (const w of devolver)
|
|
104
|
+
w.reanudar();
|
|
105
|
+
}
|
|
76
106
|
export class InterruptWatcher {
|
|
77
107
|
controller = null;
|
|
78
108
|
wasRaw = false;
|
|
@@ -112,7 +142,7 @@ export class InterruptWatcher {
|
|
|
112
142
|
Boolean(key && key.sequence === "\x1b[Z");
|
|
113
143
|
if (isShiftTab) {
|
|
114
144
|
cycleMode();
|
|
115
|
-
|
|
145
|
+
dibujar(`\r\x1b[2K${modeAnnouncement()}\n`);
|
|
116
146
|
return;
|
|
117
147
|
}
|
|
118
148
|
// Ignorar secuencias de escape multi-byte (flechas, inicio, fin, etc.) para que no aborten
|
|
@@ -130,7 +160,7 @@ export class InterruptWatcher {
|
|
|
130
160
|
const texto = commitTyped();
|
|
131
161
|
if (texto) {
|
|
132
162
|
const corto = texto.length > 70 ? `${texto.slice(0, 67)}...` : texto;
|
|
133
|
-
|
|
163
|
+
dibujar(`\r\x1b[2K${chalk.gray("↳ en cola:")} ${chalk.white(corto)}\n`);
|
|
134
164
|
}
|
|
135
165
|
return;
|
|
136
166
|
}
|
|
@@ -163,6 +193,23 @@ export class InterruptWatcher {
|
|
|
163
193
|
activos.add(this);
|
|
164
194
|
return this.controller.signal;
|
|
165
195
|
}
|
|
196
|
+
/** Vuelve a escuchar despues de un dialogo, sin tocar la senal de aborto. */
|
|
197
|
+
reanudar() {
|
|
198
|
+
if (this.listening || !this.onKey)
|
|
199
|
+
return;
|
|
200
|
+
if (!process.stdin.isTTY || !process.stdin.setRawMode)
|
|
201
|
+
return;
|
|
202
|
+
readline.emitKeypressEvents(process.stdin);
|
|
203
|
+
if (activos.size === 0)
|
|
204
|
+
modoCrudoOriginal = process.stdin.isRaw || false;
|
|
205
|
+
// Reafirmar y no solo poner: el readline del dialogo deja la consola en modo
|
|
206
|
+
// cocido, y un setRawMode(true) a secas puede ser un no-op.
|
|
207
|
+
reafirmarModoCrudo(process.stdin);
|
|
208
|
+
process.stdin.resume();
|
|
209
|
+
process.stdin.on("keypress", this.onKey);
|
|
210
|
+
this.listening = true;
|
|
211
|
+
activos.add(this);
|
|
212
|
+
}
|
|
166
213
|
abort() {
|
|
167
214
|
if (this.aborted)
|
|
168
215
|
return;
|
|
@@ -175,7 +222,9 @@ export class InterruptWatcher {
|
|
|
175
222
|
stop() {
|
|
176
223
|
if (this.onKey) {
|
|
177
224
|
process.stdin.removeListener("keypress", this.onKey);
|
|
178
|
-
|
|
225
|
+
// El manejador NO se tira: reanudar() lo vuelve a enganchar tal cual, con
|
|
226
|
+
// su AbortController intacto. Rehacerlo desde start() romperia el Esc de
|
|
227
|
+
// la peticion que ya esta en vuelo.
|
|
179
228
|
}
|
|
180
229
|
const escuchaba = this.listening;
|
|
181
230
|
this.listening = false;
|
|
@@ -53,3 +53,24 @@ export interface EntradaDeTeclado {
|
|
|
53
53
|
export declare function repararTeclado(entrada: EntradaDeTeclado): string[];
|
|
54
54
|
/** Cada cuanto se comprueba. Cuatro veces por segundo se nota como instantaneo. */
|
|
55
55
|
export declare const MS_ENTRE_COMPROBACIONES = 250;
|
|
56
|
+
/**
|
|
57
|
+
* Vuelve a poner el modo crudo DE VERDAD, no solo en la cuenta de Node.
|
|
58
|
+
*
|
|
59
|
+
* EL AGUJERO QUE TAPA
|
|
60
|
+
*
|
|
61
|
+
* `setRawMode(true)` no siempre llega al terminal. Por debajo, libuv guarda el
|
|
62
|
+
* modo que cree tener y empieza con `if (tty->mode == mode) return 0;`. Asi que
|
|
63
|
+
* si algo cambia el modo de la consola por fuera de Node -un proceso hijo, la
|
|
64
|
+
* propia shell, el terminal- Node sigue creyendo que esta en crudo, y a partir
|
|
65
|
+
* de ahi TODOS los `setRawMode(true)` se convierten en no-op. Incluido el que
|
|
66
|
+
* hace el prompt al abrirse.
|
|
67
|
+
*
|
|
68
|
+
* Se veia asi, y se reporto cuatro veces: el usuario teclea, su texto sale al
|
|
69
|
+
* final de la linea de ayuda -lo esta haciendo el eco del terminal, no el
|
|
70
|
+
* prompt- y el recuadro no se entera de nada. El vigilante tampoco reparaba
|
|
71
|
+
* nada, porque preguntaba a Node y Node decia que todo estaba bien.
|
|
72
|
+
*
|
|
73
|
+
* Pasar por `false` primero rompe esa cache: la segunda llamada ya ve un cambio
|
|
74
|
+
* de verdad y lo aplica sobre la consola.
|
|
75
|
+
*/
|
|
76
|
+
export declare function reafirmarModoCrudo(entrada: EntradaDeTeclado): void;
|
package/dist/ui/keyboardGuard.js
CHANGED
|
@@ -49,7 +49,7 @@ export function repararTeclado(entrada) {
|
|
|
49
49
|
return [];
|
|
50
50
|
const reparado = [];
|
|
51
51
|
if (entrada.isRaw !== true) {
|
|
52
|
-
entrada
|
|
52
|
+
reafirmarModoCrudo(entrada);
|
|
53
53
|
reparado.push("modo crudo");
|
|
54
54
|
}
|
|
55
55
|
// `pause()` corta el flujo sin avisar a nadie: el oyente de `keypress` sigue
|
|
@@ -63,3 +63,32 @@ export function repararTeclado(entrada) {
|
|
|
63
63
|
}
|
|
64
64
|
/** Cada cuanto se comprueba. Cuatro veces por segundo se nota como instantaneo. */
|
|
65
65
|
export const MS_ENTRE_COMPROBACIONES = 250;
|
|
66
|
+
/**
|
|
67
|
+
* Vuelve a poner el modo crudo DE VERDAD, no solo en la cuenta de Node.
|
|
68
|
+
*
|
|
69
|
+
* EL AGUJERO QUE TAPA
|
|
70
|
+
*
|
|
71
|
+
* `setRawMode(true)` no siempre llega al terminal. Por debajo, libuv guarda el
|
|
72
|
+
* modo que cree tener y empieza con `if (tty->mode == mode) return 0;`. Asi que
|
|
73
|
+
* si algo cambia el modo de la consola por fuera de Node -un proceso hijo, la
|
|
74
|
+
* propia shell, el terminal- Node sigue creyendo que esta en crudo, y a partir
|
|
75
|
+
* de ahi TODOS los `setRawMode(true)` se convierten en no-op. Incluido el que
|
|
76
|
+
* hace el prompt al abrirse.
|
|
77
|
+
*
|
|
78
|
+
* Se veia asi, y se reporto cuatro veces: el usuario teclea, su texto sale al
|
|
79
|
+
* final de la linea de ayuda -lo esta haciendo el eco del terminal, no el
|
|
80
|
+
* prompt- y el recuadro no se entera de nada. El vigilante tampoco reparaba
|
|
81
|
+
* nada, porque preguntaba a Node y Node decia que todo estaba bien.
|
|
82
|
+
*
|
|
83
|
+
* Pasar por `false` primero rompe esa cache: la segunda llamada ya ve un cambio
|
|
84
|
+
* de verdad y lo aplica sobre la consola.
|
|
85
|
+
*/
|
|
86
|
+
export function reafirmarModoCrudo(entrada) {
|
|
87
|
+
if (!entrada || typeof entrada.setRawMode !== "function")
|
|
88
|
+
return;
|
|
89
|
+
try {
|
|
90
|
+
entrada.setRawMode(false);
|
|
91
|
+
}
|
|
92
|
+
catch { }
|
|
93
|
+
entrada.setRawMode(true);
|
|
94
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Un pegado grande no se mete entero en el recuadro.
|
|
3
|
+
*
|
|
4
|
+
* EL PROBLEMA
|
|
5
|
+
*
|
|
6
|
+
* Pegar un error de compilacion de 300 lineas metia las 300 lineas dentro del
|
|
7
|
+
* prompt. El recuadro crecia hasta llenar la pantalla, el marco se volvia
|
|
8
|
+
* inutil, y para deshacerlo habia que pulsar borrar trescientas veces. Es el
|
|
9
|
+
* caso normal de usar un agente: uno pega un log, una traza, un archivo entero.
|
|
10
|
+
*
|
|
11
|
+
* COMO QUEDA
|
|
12
|
+
*
|
|
13
|
+
* El texto se guarda aparte y en el recuadro solo aparece una marca corta:
|
|
14
|
+
*
|
|
15
|
+
* ❯ arregla esto [pegado #1 · 347 lineas]
|
|
16
|
+
*
|
|
17
|
+
* Al enviar, la marca se sustituye por el texto completo, asi que el modelo
|
|
18
|
+
* recibe exactamente lo que se pego. Y un solo borrar se lleva la marca entera:
|
|
19
|
+
* lo que ocupa una linea se borra como una linea.
|
|
20
|
+
*
|
|
21
|
+
* POR QUE UNA MARCA DE TEXTO Y NO UNA ESTRUCTURA APARTE
|
|
22
|
+
*
|
|
23
|
+
* Porque el buffer del prompt es una cadena y todo lo demas -el cursor, el
|
|
24
|
+
* historial, el recorte del marco, la busqueda de menciones- cuenta caracteres.
|
|
25
|
+
* Meter ahi un objeto obligaria a tocar las seis cosas. Una marca de texto se
|
|
26
|
+
* lleva bien con todo lo que ya funciona.
|
|
27
|
+
*/
|
|
28
|
+
/** A partir de aqui, un pegado se guarda aparte en vez de llenar el recuadro. */
|
|
29
|
+
export declare const MINIMO_LINEAS = 5;
|
|
30
|
+
export declare const MINIMO_CARACTERES = 400;
|
|
31
|
+
/** Si merece la pena guardarlo aparte, o es lo bastante corto para verlo entero. */
|
|
32
|
+
export declare function esGrande(texto: string): boolean;
|
|
33
|
+
/** Guarda el texto y devuelve la marca que va en el recuadro. */
|
|
34
|
+
export declare function guardarPegado(texto: string): string;
|
|
35
|
+
/** Devuelve el texto de verdad, con las marcas sustituidas. Es lo que ve el modelo. */
|
|
36
|
+
export declare function expandirPegados(entrada: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Si justo antes del cursor termina una marca, dice donde empieza.
|
|
39
|
+
*
|
|
40
|
+
* Es lo que hace que un solo borrar se lleve el pegado entero. Sin esto habria
|
|
41
|
+
* que borrar la marca letra a letra, que es un poco menos absurdo que borrar
|
|
42
|
+
* las 347 lineas pero sigue siendo absurdo.
|
|
43
|
+
*/
|
|
44
|
+
export declare function marcaQueTerminaEn(buffer: string, cursor: number): {
|
|
45
|
+
inicio: number;
|
|
46
|
+
} | null;
|
|
47
|
+
/** Se olvidan al cerrar el prompt: cada turno empieza con su propia cuenta. */
|
|
48
|
+
export declare function limpiarPegados(): void;
|
|
49
|
+
/** Cuantos hay guardados ahora mismo. Solo para las pruebas. */
|
|
50
|
+
export declare function cuantosPegados(): number;
|