chocolatito-code 1.0.0 → 1.2.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.
Files changed (122) hide show
  1. package/README.md +155 -0
  2. package/dist/agent/compaction.d.ts +65 -0
  3. package/dist/agent/compaction.js +134 -0
  4. package/dist/agent/context.d.ts +3 -0
  5. package/dist/agent/context.js +35 -0
  6. package/dist/agent/loop.d.ts +91 -2
  7. package/dist/agent/loop.js +642 -91
  8. package/dist/agent/loopDetector.d.ts +57 -0
  9. package/dist/agent/loopDetector.js +0 -0
  10. package/dist/agent/mentions.d.ts +13 -0
  11. package/dist/agent/mentions.js +49 -0
  12. package/dist/agent/repoMap.d.ts +117 -0
  13. package/dist/agent/repoMap.js +560 -0
  14. package/dist/agent/result.d.ts +64 -0
  15. package/dist/agent/result.js +48 -0
  16. package/dist/agent/retry.d.ts +35 -0
  17. package/dist/agent/retry.js +123 -4
  18. package/dist/agent/salidaDelAgente.d.ts +43 -0
  19. package/dist/agent/salidaDelAgente.js +53 -0
  20. package/dist/agent/subagent.js +62 -15
  21. package/dist/agent/syntaxValidator.js +288 -52
  22. package/dist/agent/toolGate.d.ts +58 -0
  23. package/dist/agent/toolGate.js +111 -0
  24. package/dist/agent/tracker.js +4 -7
  25. package/dist/agent/undoManager.d.ts +44 -11
  26. package/dist/agent/undoManager.js +95 -24
  27. package/dist/agent/usageMeter.d.ts +56 -0
  28. package/dist/agent/usageMeter.js +51 -0
  29. package/dist/agent/verifier.d.ts +49 -0
  30. package/dist/agent/verifier.js +158 -0
  31. package/dist/config/constants.js +14 -0
  32. package/dist/config/engine.d.ts +19 -0
  33. package/dist/config/engine.js +35 -3
  34. package/dist/config/license.d.ts +5 -0
  35. package/dist/config/license.js +107 -9
  36. package/dist/config/limits.d.ts +28 -0
  37. package/dist/config/limits.js +51 -0
  38. package/dist/config/nodeVersion.d.ts +13 -0
  39. package/dist/config/nodeVersion.js +26 -0
  40. package/dist/config/permissions.d.ts +76 -1
  41. package/dist/config/permissions.js +379 -23
  42. package/dist/config/quota.js +24 -1
  43. package/dist/config/updater.d.ts +68 -0
  44. package/dist/config/updater.js +215 -0
  45. package/dist/hooks/manager.js +159 -14
  46. package/dist/index.js +240 -37
  47. package/dist/mcp/client.d.ts +50 -0
  48. package/dist/mcp/client.js +155 -19
  49. package/dist/mcp/ide.d.ts +76 -0
  50. package/dist/mcp/ide.js +160 -0
  51. package/dist/mcp/manager.js +9 -0
  52. package/dist/prompts/systemPrompt.js +12 -1
  53. package/dist/sessions/manager.d.ts +12 -0
  54. package/dist/sessions/manager.js +33 -5
  55. package/dist/sessions/resume.d.ts +36 -0
  56. package/dist/sessions/resume.js +43 -0
  57. package/dist/skills/manager.d.ts +1 -0
  58. package/dist/skills/manager.js +9 -0
  59. package/dist/tools/binary.d.ts +46 -0
  60. package/dist/tools/binary.js +100 -0
  61. package/dist/tools/browserCdp.js +13 -1
  62. package/dist/tools/browserExtension.d.ts +5 -0
  63. package/dist/tools/browserExtension.js +237 -22
  64. package/dist/tools/browserSession.d.ts +23 -0
  65. package/dist/tools/browserSession.js +28 -0
  66. package/dist/tools/computerUse.js +33 -1
  67. package/dist/tools/createImage.d.ts +11 -0
  68. package/dist/tools/createImage.js +13 -1
  69. package/dist/tools/definitions.js +17 -4
  70. package/dist/tools/editDiagnosis.d.ts +54 -0
  71. package/dist/tools/editDiagnosis.js +142 -0
  72. package/dist/tools/editFile.d.ts +1 -0
  73. package/dist/tools/editFile.js +118 -62
  74. package/dist/tools/findFiles.d.ts +21 -0
  75. package/dist/tools/findFiles.js +75 -6
  76. package/dist/tools/getSystemInfo.js +15 -7
  77. package/dist/tools/gitTools.d.ts +24 -0
  78. package/dist/tools/gitTools.js +87 -26
  79. package/dist/tools/grepSearch.js +111 -23
  80. package/dist/tools/moveCopyFile.d.ts +18 -1
  81. package/dist/tools/moveCopyFile.js +74 -4
  82. package/dist/tools/patchCascade.d.ts +57 -0
  83. package/dist/tools/patchCascade.js +338 -0
  84. package/dist/tools/runCommand.js +213 -25
  85. package/dist/tools/runner.js +58 -5
  86. package/dist/tools/toolDefsComputer.js +12 -2
  87. package/dist/tools/truncator.d.ts +27 -1
  88. package/dist/tools/truncator.js +117 -11
  89. package/dist/tools/viewFile.js +15 -11
  90. package/dist/tools/visionBridge.js +7 -0
  91. package/dist/tools/webSearch.d.ts +21 -0
  92. package/dist/tools/webSearch.js +125 -14
  93. package/dist/tools/writeFile.d.ts +16 -1
  94. package/dist/tools/writeFile.js +40 -2
  95. package/dist/ui/interrupt.d.ts +4 -1
  96. package/dist/ui/interrupt.js +59 -7
  97. package/dist/ui/keyboardGuard.d.ts +76 -0
  98. package/dist/ui/keyboardGuard.js +94 -0
  99. package/dist/ui/loopPrompt.d.ts +17 -0
  100. package/dist/ui/loopPrompt.js +59 -0
  101. package/dist/ui/pegados.d.ts +50 -0
  102. package/dist/ui/pegados.js +83 -0
  103. package/dist/ui/permissionPrompt.js +84 -2
  104. package/dist/ui/prompt.d.ts +40 -0
  105. package/dist/ui/prompt.js +436 -131
  106. package/dist/ui/reasoningStream.d.ts +8 -0
  107. package/dist/ui/reasoningStream.js +99 -7
  108. package/dist/ui/salida.d.ts +10 -0
  109. package/dist/ui/salida.js +50 -0
  110. package/extension/README.md +95 -74
  111. package/extension/background.js +772 -181
  112. package/extension/content.js +527 -46
  113. package/extension/fuentes/Chocolatito-Marca.ttf +0 -0
  114. package/extension/iconos/128.png +0 -0
  115. package/extension/iconos/16.png +0 -0
  116. package/extension/iconos/32.png +0 -0
  117. package/extension/iconos/48.png +0 -0
  118. package/extension/iconos/logotipo.png +0 -0
  119. package/extension/manifest.json +28 -4
  120. package/extension/popup.html +16 -6
  121. package/extension/popup.js +28 -12
  122. package/package.json +3 -2
@@ -17,7 +17,245 @@
17
17
 
18
18
  const IS_TOP = window.top === window;
19
19
 
20
- // ======================================================== aviso en pagina
20
+ // ======================================================== overlay visual y borde naranja (#D97757)
21
+
22
+ let overlayHost = null;
23
+ let overlayShadow = null;
24
+ let overlayFrame = null;
25
+ let overlayLayers = null;
26
+ let overlayActivo = false;
27
+
28
+ // Icono oficial del zorro de Chocolatito a 16px en base64 para cargar de inmediato
29
+ // sin depender de peticiones de red ni fallar por políticas CSP de páginas externas.
30
+ const LOGO_16_DATA_URI =
31
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC10lEQVR4nI1RS2hUZxT+zv+4uZk7c5MxNk5SiI/4IhZUhIJ2IVnYRbGQUqQU3YhS64OgGxdubDcqWIraldVaSqmIYLGL4mPhA6QUtFIxLlQ0iQnJSHQyjncmM/fe/z/lDplixBY/OItzOOc73/kO8AoYoCSGNnf1DGzs6klqV9dC4X8gZmQ/wCGA2zP4YFlOXA92Lfqo9zpi3g35VgS0BbWez/dKN4o/iyrhbI+qv5e+nLeVjsD8vfPNJNSQ/vTAh2rOyN3VAB8MQn5PKe1PxYysI2J4/kr65t4An4egPtgZCniaxAkm3HIo9hVqojIZ0plSaK5VIi5DsAJP7a5369dObiho4OrXy5rcMe/d1mq+1wWnx628tEBUL85pc0P6dmhxsizx6I0e8A7I3v33aqNDw/mcinbOy9KRdnA1oKZ9qKKE/4CqD5+DoE9hCtuWLM+qqdNBzfQgtFYL/mSc5Y9zY1rKv0ChCMkjiOkQzL8EZzdAJsPV7d1rmmRwIYzIL4bxVFprx1ecLabn83Bx8tjCjYiBeuDVUyhJHm1+X3fqJ4PNSnaOldm0OCS9rEahEG9pOzl6ivs7V8PqNXBlBqT/pMP3LzZIVMIy1JFmb5xvWbbrNSEMGXefTcQ/NUs5Yr7ouoLY9iJRXbGAYxD1z/2Ojg33836IuoJne7r7oqJJF9kMxqGWSkf+LOKPlbBbs46gfMVYJsTMsIogcr50bJhaJY8/uK0SGWUrnqZaza8oRI/IMV6rI3KuBsZKZPOWI0cJp0WRozVQqplEjBE6XAHgtkhkeEcf/vEiNJc72nV3SlJusmo4XzZ4J0Wi0yPH0xQqKW5aS6cJdAOSZAD5pP6Fr6ZdjWa1bcKL4vcphXU1ZtdXFGhX34Gg880Z9RsdevA4GRjcsTwja89PDLTIG9NGznzLyLYFizIi9p8HaqL758f1LY0erIX8a0mHMxr7su/U/ZdJ7R8LXTi7zKe+1AAAAABJRU5ErkJggg==";
32
+
33
+ function ensureOverlay() {
34
+ if (overlayHost && document.documentElement && document.documentElement.contains(overlayHost)) {
35
+ return overlayShadow;
36
+ }
37
+
38
+ overlayHost = document.createElement("div");
39
+ overlayHost.id = "__chocolatito_overlay__";
40
+ // CRÍTICO: pointer-events: none en el host y en todos los hijos para que
41
+ // JAMÁS capture clics ni altere el flujo de interacción de la página o del agente.
42
+ // all: initial aísla de las reglas CSS de la página que pudieran pisarlo.
43
+ overlayHost.style.cssText =
44
+ "all: initial !important; position: fixed !important; inset: 0 !important; " +
45
+ "width: 100vw !important; height: 100vh !important; pointer-events: none !important; " +
46
+ "z-index: 2147483646 !important; overflow: hidden !important;";
47
+
48
+ overlayShadow = overlayHost.attachShadow({ mode: "closed" });
49
+
50
+ const style = document.createElement("style");
51
+ style.textContent = `
52
+ :host {
53
+ all: initial !important;
54
+ pointer-events: none !important;
55
+ }
56
+ * {
57
+ box-sizing: border-box !important;
58
+ pointer-events: none !important;
59
+ }
60
+ /* Borde terracota (#D97757) hecho con box-shadow inset: no desplaza el layout ni el scroll */
61
+ .frame {
62
+ position: fixed !important;
63
+ inset: 0 !important;
64
+ pointer-events: none !important;
65
+ box-shadow: inset 0 0 0 3px #D97757 !important;
66
+ z-index: 2147483646 !important;
67
+ display: none;
68
+ animation: chocoFramePulse 2s ease-in-out infinite;
69
+ }
70
+ .frame.visible {
71
+ display: block !important;
72
+ }
73
+ @keyframes chocoFramePulse {
74
+ 0%, 100% { opacity: 1; }
75
+ 50% { opacity: 0.6; }
76
+ }
77
+ @media (prefers-reduced-motion: reduce) {
78
+ .frame {
79
+ animation: none !important;
80
+ opacity: 1 !important;
81
+ }
82
+ }
83
+ /* Etiqueta distintiva en la esquina superior */
84
+ .badge {
85
+ position: fixed !important;
86
+ top: 12px !important;
87
+ right: 14px !important;
88
+ display: inline-flex !important;
89
+ align-items: center !important;
90
+ gap: 7px !important;
91
+ padding: 5px 10px !important;
92
+ background: #D97757 !important;
93
+ color: #FFFFFF !important;
94
+ font: 600 12px/1.3 -apple-system, "Segoe UI", system-ui, sans-serif !important;
95
+ border-radius: 6px !important;
96
+ box-shadow: 0 3px 12px rgba(0, 0, 0, 0.35) !important;
97
+ pointer-events: none !important;
98
+ letter-spacing: 0.2px !important;
99
+ z-index: 2147483647 !important;
100
+ }
101
+ .badge-icon {
102
+ width: 16px !important;
103
+ height: 16px !important;
104
+ display: block !important;
105
+ flex: 0 0 16px !important;
106
+ }
107
+ /* Capa de efectos efímeros (recuadros de selección y ondas de clic) */
108
+ .layers {
109
+ position: fixed !important;
110
+ inset: 0 !important;
111
+ pointer-events: none !important;
112
+ z-index: 2147483647 !important;
113
+ }
114
+ .highlight-box {
115
+ position: fixed !important;
116
+ outline: 2px solid #D97757 !important;
117
+ background: rgba(217, 119, 87, 0.12) !important;
118
+ border-radius: 3px !important;
119
+ pointer-events: none !important;
120
+ transition: opacity 0.6s ease;
121
+ opacity: 1;
122
+ }
123
+ .highlight-box.fade-out {
124
+ opacity: 0 !important;
125
+ }
126
+ .highlight-tag {
127
+ position: absolute !important;
128
+ bottom: 100% !important;
129
+ left: 0 !important;
130
+ margin-bottom: 3px !important;
131
+ background: #D97757 !important;
132
+ color: #FFFFFF !important;
133
+ font: 600 11px/1.2 -apple-system, system-ui, sans-serif !important;
134
+ padding: 2px 6px !important;
135
+ border-radius: 3px !important;
136
+ white-space: nowrap !important;
137
+ pointer-events: none !important;
138
+ box-shadow: 0 2px 6px rgba(0,0,0,0.25) !important;
139
+ }
140
+ .click-ripple {
141
+ position: fixed !important;
142
+ width: 20px !important;
143
+ height: 20px !important;
144
+ border-radius: 50% !important;
145
+ background: rgba(217, 119, 87, 0.35) !important;
146
+ border: 2px solid #D97757 !important;
147
+ transform: translate(-50%, -50%) scale(0.3);
148
+ pointer-events: none !important;
149
+ animation: chocoRipple 0.4s ease-out forwards;
150
+ }
151
+ @keyframes chocoRipple {
152
+ 0% { transform: translate(-50%, -50%) scale(0.3); opacity: 1; }
153
+ 100% { transform: translate(-50%, -50%) scale(2.2); opacity: 0; }
154
+ }
155
+ `;
156
+
157
+ overlayShadow.appendChild(style);
158
+
159
+ // Solo el frame principal de la ventana (IS_TOP) dibuja el borde exterior y la insignia
160
+ // para no llenar de bordes anidados los iframes de la página.
161
+ if (IS_TOP) {
162
+ overlayFrame = document.createElement("div");
163
+ overlayFrame.className = "frame" + (overlayActivo ? " visible" : "");
164
+ overlayFrame.innerHTML = `
165
+ <div class="badge">
166
+ <img class="badge-icon" src="${LOGO_16_DATA_URI}" alt="" />
167
+ <span>Chocolatito está trabajando</span>
168
+ </div>
169
+ `;
170
+ overlayShadow.appendChild(overlayFrame);
171
+ }
172
+
173
+ overlayLayers = document.createElement("div");
174
+ overlayLayers.className = "layers";
175
+ overlayShadow.appendChild(overlayLayers);
176
+
177
+ (document.body || document.documentElement).appendChild(overlayHost);
178
+ return overlayShadow;
179
+ }
180
+
181
+ function setOverlayActive(activo) {
182
+ overlayActivo = Boolean(activo);
183
+ ensureOverlay();
184
+ if (overlayFrame) {
185
+ if (overlayActivo) overlayFrame.classList.add("visible");
186
+ else overlayFrame.classList.remove("visible");
187
+ }
188
+ }
189
+
190
+ function isOverlayActive() {
191
+ return overlayActivo;
192
+ }
193
+
194
+ /** Dibuja un recuadro sobre el elemento objetivo antes de actuar (C1) */
195
+ function resaltarElemento(el, ref, role, name) {
196
+ try {
197
+ ensureOverlay();
198
+ if (!overlayLayers) return;
199
+ const r = el.getBoundingClientRect();
200
+ if (r.width < 1 && r.height < 1) return;
201
+
202
+ const box = document.createElement("div");
203
+ box.className = "highlight-box";
204
+ box.style.left = `${Math.round(r.left)}px`;
205
+ box.style.top = `${Math.round(r.top)}px`;
206
+ box.style.width = `${Math.round(r.width)}px`;
207
+ box.style.height = `${Math.round(r.height)}px`;
208
+
209
+ const tag = document.createElement("div");
210
+ tag.className = "highlight-tag";
211
+ const etiqueta = `[#${ref}] ${role || ""}${name ? ' "' + name.slice(0, 30) + '"' : ""}`;
212
+ tag.textContent = etiqueta.trim();
213
+ box.appendChild(tag);
214
+
215
+ overlayLayers.appendChild(box);
216
+
217
+ setTimeout(() => {
218
+ box.classList.add("fade-out");
219
+ setTimeout(() => {
220
+ try { if (box.parentNode) box.parentNode.removeChild(box); } catch (_) {}
221
+ }, 600);
222
+ }, 400);
223
+ } catch (_) {}
224
+ }
225
+
226
+ /** Dibuja una onda en el punto exacto donde se pulsó (C2) */
227
+ function mostrarOndaClic(x, y) {
228
+ try {
229
+ ensureOverlay();
230
+ if (!overlayLayers) return;
231
+ const ripple = document.createElement("div");
232
+ ripple.className = "click-ripple";
233
+ ripple.style.left = `${Math.round(x)}px`;
234
+ ripple.style.top = `${Math.round(y)}px`;
235
+
236
+ overlayLayers.appendChild(ripple);
237
+
238
+ setTimeout(() => {
239
+ try { if (ripple.parentNode) ripple.parentNode.removeChild(ripple); } catch (_) {}
240
+ }, 400);
241
+ } catch (_) {}
242
+ }
243
+
244
+ // D3: Observador de mutaciones para saber cuándo la pantalla "está quieta" (reposo DOM)
245
+ let ultimaMutacion = Date.now();
246
+ try {
247
+ const mutObs = new MutationObserver(() => {
248
+ ultimaMutacion = Date.now();
249
+ });
250
+ mutObs.observe(document.documentElement || document, {
251
+ childList: true,
252
+ subtree: true,
253
+ attributes: true,
254
+ characterData: true,
255
+ });
256
+ } catch (_) {}
257
+
258
+ // ======================================================== aviso flotante inferior (banner)
21
259
 
22
260
  let bannerHost = null;
23
261
  let bannerBody = null;
@@ -29,8 +267,6 @@
29
267
 
30
268
  bannerHost = document.createElement("div");
31
269
  bannerHost.id = "__chocolatito_banner__";
32
- // Shadow DOM: los estilos de la pagina no pueden romper el aviso, ni el
33
- // aviso ensuciar la pagina.
34
270
  const shadow = bannerHost.attachShadow({ mode: "closed" });
35
271
  const style = document.createElement("style");
36
272
  style.textContent = `
@@ -84,7 +320,6 @@
84
320
  bannerBody.action.textContent = text || "";
85
321
  bannerBody.wrap.classList.remove("hide");
86
322
  clearTimeout(bannerHideTimer);
87
- // Se desvanece solo si el agente deja de trabajar, para no molestar.
88
323
  bannerHideTimer = setTimeout(() => {
89
324
  if (bannerBody) bannerBody.wrap.classList.add("hide");
90
325
  }, 12000);
@@ -155,12 +390,43 @@
155
390
  function norm(s) {
156
391
  return (s || "")
157
392
  .normalize("NFD")
393
+ // Rango de diacriticos combinantes. Escrito con escapes y no con los
394
+ // caracteres literales: en el fuente eran invisibles y cualquier editor
395
+ // que "normalizara" el archivo se los llevaba por delante sin que nadie
396
+ // lo notara hasta que el filtro dejaba de casar acentos.
158
397
  .replace(/[̀-ͯ]/g, "")
159
398
  .toLowerCase();
160
399
  }
161
400
 
162
- function buildSnapshot(startIndex, filter) {
163
- if (startIndex === 0) refIdentity.clear();
401
+ /**
402
+ * Quita las marcas del snapshot anterior.
403
+ *
404
+ * data-choco-ref se escribia en los elementos de la pagina del usuario y no
405
+ * lo borraba nadie: los atributos se acumulaban snapshot tras snapshot, y un
406
+ * ref viejo podia seguir resolviendo a un elemento que ya no estaba en la
407
+ * lista actual.
408
+ */
409
+ function limpiarMarcas() {
410
+ for (const root of allRoots()) {
411
+ let marcados;
412
+ try {
413
+ marcados = root.querySelectorAll("[data-choco-ref]");
414
+ } catch (_) {
415
+ continue;
416
+ }
417
+ for (const el of marcados) el.removeAttribute("data-choco-ref");
418
+ }
419
+ }
420
+
421
+ function buildSnapshot(startIndex, filter, nuevo) {
422
+ // Antes esto era `startIndex === 0`, que solo es cierto en el PRIMER frame.
423
+ // Cada frame tiene su propio content script con su propia tabla, asi que
424
+ // los subframes no la limpiaban nunca: acumulaban identidades de snapshots
425
+ // viejos y un ref podia resolver a un boton que ya no era ese.
426
+ if (nuevo || startIndex === 0) {
427
+ refIdentity.clear();
428
+ limpiarMarcas();
429
+ }
164
430
  const out = [];
165
431
  let idx = startIndex;
166
432
  const f = norm(filter);
@@ -213,19 +479,41 @@
213
479
  */
214
480
  const refIdentity = new Map();
215
481
 
216
- function findByRef(ref) {
482
+ /**
483
+ * Busca elementos en el DOM que tengan el mismo rol o nombre similar
484
+ * para dar mensajes de error útiles si un ref no se encuentra (D5).
485
+ */
486
+ function buscarSimilares(id) {
487
+ if (!id) return [];
488
+ const similares = [];
217
489
  for (const root of allRoots()) {
218
- const el = root.querySelector('[data-choco-ref="' + ref + '"]');
219
- if (el) return el;
490
+ let nodes;
491
+ try { nodes = root.querySelectorAll(SELECTOR); } catch (_) { continue; }
492
+ for (const el of nodes) {
493
+ const r = roleOf(el);
494
+ const n = accessibleName(el).slice(0, 60);
495
+ if (!n && r !== "textbox") continue;
496
+ const coincideRol = id.role && r === id.role;
497
+ const coincideNombre = id.name && n && (norm(n).includes(norm(id.name)) || norm(id.name).includes(norm(n)));
498
+ if (coincideRol || coincideNombre) {
499
+ similares.push({ role: r, name: n });
500
+ if (similares.length >= 3) return similares;
501
+ }
502
+ }
220
503
  }
504
+ return similares;
505
+ }
221
506
 
222
- // El nodo se fue en un re-render: se busca otro con la misma identidad.
223
- const id = refIdentity.get(ref);
507
+ /**
508
+ * Reubica un elemento tras re-renders de frameworks reactivos (React/Vue/Flow)
509
+ * buscando candidatos con la misma identidad registrada.
510
+ */
511
+ function reubicarPorIdentidad(id, ref) {
224
512
  if (!id) return null;
225
-
226
513
  const candidatos = [];
227
514
  for (const root of allRoots()) {
228
- const nodes = root.querySelectorAll(SELECTOR);
515
+ let nodes;
516
+ try { nodes = root.querySelectorAll(SELECTOR); } catch (_) { continue; }
229
517
  for (const el of nodes) {
230
518
  if (roleOf(el) !== id.role) continue;
231
519
  if (accessibleName(el).slice(0, 120) !== id.name) continue;
@@ -235,13 +523,68 @@
235
523
  }
236
524
  }
237
525
  if (candidatos.length === 0) return null;
238
-
239
- // Con varios iguales se elige el que ocupaba la misma posicion en la lista.
240
526
  const elegido = candidatos[Math.min(id.idx, candidatos.length - 1)];
241
527
  elegido.setAttribute("data-choco-ref", String(ref));
242
528
  return elegido;
243
529
  }
244
530
 
531
+ /**
532
+ * Resuelve un ref verificando que el elemento siga siendo el mismo (D4).
533
+ * Si el DOM mutó drásticamente o el elemento desapareció, devuelve un error
534
+ * explicativo con sugerencias de elementos similares (D5).
535
+ */
536
+ function resolverRef(ref) {
537
+ const id = refIdentity.get(ref);
538
+ let el = null;
539
+ for (const root of allRoots()) {
540
+ try {
541
+ el = root.querySelector('[data-choco-ref="' + ref + '"]');
542
+ if (el) break;
543
+ } catch (_) {}
544
+ }
545
+
546
+ if (el) {
547
+ if (id) {
548
+ const actualRole = roleOf(el);
549
+ const actualName = accessibleName(el).slice(0, 120);
550
+ const rolCoincide = actualRole === id.role;
551
+ const nombreCoincide = !id.name || !actualName || norm(actualName).includes(norm(id.name)) || norm(id.name).includes(norm(actualName));
552
+ if (!rolCoincide && !nombreCoincide) {
553
+ // El elemento fue reciclado por otro componente del framework; reintentar por identidad
554
+ const reubicado = reubicarPorIdentidad(id, ref);
555
+ if (reubicado) return { ok: true, el: reubicado, id };
556
+ const similares = buscarSimilares(id);
557
+ const simTxt = similares.length > 0 ? ` Elementos parecidos: ${similares.map(s => `<${s.role}> "${s.name}"`).join(", ")}.` : "";
558
+ return {
559
+ ok: false,
560
+ error: `El DOM cambió desde el último snapshot: el ref [${ref}] apuntaba a <${id.role}> "${id.name}", pero ahora hay <${actualRole}> "${actualName}".${simTxt} Reház el snapshot antes de continuar.`,
561
+ };
562
+ }
563
+ }
564
+ return { ok: true, el, id };
565
+ }
566
+
567
+ // Si el nodo desapareció en un re-render, se busca por identidad
568
+ if (id) {
569
+ const reubicado = reubicarPorIdentidad(id, ref);
570
+ if (reubicado) return { ok: true, el: reubicado, id };
571
+ }
572
+
573
+ const similares = buscarSimilares(id);
574
+ const simTxt = similares.length > 0
575
+ ? ` Elementos similares disponibles: ${similares.map(s => `<${s.role}> "${s.name}"`).join(", ")}.`
576
+ : " No se encontraron elementos similares en el DOM.";
577
+ return {
578
+ ok: false,
579
+ error: `Ref [${ref}] no encontrado en este frame. Se buscaba: <${id?.role || "elemento"}> "${id?.name || ""}".${simTxt} Reház el snapshot para actualizar los refs.`,
580
+ };
581
+ }
582
+
583
+ function findByRef(ref) {
584
+ const res = resolverRef(ref);
585
+ return res.ok ? res.el : null;
586
+ }
587
+
245
588
  // ======================================================== acciones
246
589
 
247
590
  function centerOf(el) {
@@ -249,9 +592,52 @@
249
592
  return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
250
593
  }
251
594
 
595
+ /** Maneja de forma nativa la selección en menús desplegables <select> (D6) */
596
+ function seleccionarOpcion(selectEl, textoOValor) {
597
+ const needle = norm(textoOValor);
598
+ let elegidoIdx = -1;
599
+ for (let i = 0; i < selectEl.options.length; i++) {
600
+ const opt = selectEl.options[i];
601
+ if (norm(opt.value) === needle || norm(opt.text).includes(needle) || norm(opt.label).includes(needle)) {
602
+ elegidoIdx = i;
603
+ break;
604
+ }
605
+ }
606
+ if (elegidoIdx === -1) {
607
+ const num = Number(textoOValor);
608
+ if (!isNaN(num) && num >= 0 && num < selectEl.options.length) elegidoIdx = num;
609
+ }
610
+ if (elegidoIdx !== -1) {
611
+ selectEl.selectedIndex = elegidoIdx;
612
+ const valor = selectEl.options[elegidoIdx].value;
613
+ const proto = HTMLSelectElement.prototype;
614
+ const desc = Object.getOwnPropertyDescriptor(proto, "value");
615
+ if (desc && desc.set) desc.set.call(selectEl, valor);
616
+ else selectEl.value = valor;
617
+ }
618
+ selectEl.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
619
+ selectEl.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
620
+ return selectEl.value;
621
+ }
622
+
623
+ /** Conmuta casillas de verificación y radios usando descriptors nativos y eventos (D6) */
624
+ function conmutarCasilla(inputEl, estadoDeseado) {
625
+ const proximo = estadoDeseado !== undefined ? Boolean(estadoDeseado) : (inputEl.type === "checkbox" ? !inputEl.checked : true);
626
+ const proto = HTMLInputElement.prototype;
627
+ const desc = Object.getOwnPropertyDescriptor(proto, "checked");
628
+ if (desc && desc.set) desc.set.call(inputEl, proximo);
629
+ else inputEl.checked = proximo;
630
+
631
+ inputEl.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, composed: true }));
632
+ inputEl.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
633
+ inputEl.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
634
+ return inputEl.checked;
635
+ }
636
+
252
637
  /**
253
- * Pulsa sin raton real. Se emite la secuencia completa de eventos porque
638
+ * Pulsa sin ratón real. Se emite la secuencia completa de eventos porque
254
639
  * muchos componentes escuchan pointerdown/mouseup y no solo click.
640
+ * Realiza scroll al centro antes de pulsar (C3) y muestra onda de clic (C2).
255
641
  */
256
642
  function clickElement(el) {
257
643
  try {
@@ -260,6 +646,12 @@
260
646
  try { el.scrollIntoView(); } catch (_) {}
261
647
  }
262
648
  const c = centerOf(el);
649
+ mostrarOndaClic(c.x, c.y);
650
+
651
+ if (el.type === "checkbox" || el.type === "radio") {
652
+ return conmutarCasilla(el);
653
+ }
654
+
263
655
  const base = { bubbles: true, cancelable: true, composed: true, view: window, clientX: c.x, clientY: c.y, button: 0 };
264
656
  const ptr = Object.assign({ pointerId: 1, isPrimary: true, pointerType: "mouse" }, base);
265
657
 
@@ -277,7 +669,7 @@
277
669
  }
278
670
 
279
671
  /**
280
- * Escribe de forma que React se entere.
672
+ * Escribe de forma que React/Vue se enteren.
281
673
  *
282
674
  * Asignar el.value directamente NO sirve: React guarda su propio estado y no
283
675
  * ve el cambio, asi que el campo parece relleno pero el boton de enviar sigue
@@ -299,6 +691,16 @@
299
691
  try { el.scrollIntoView({ block: "center", behavior: "instant" }); } catch (_) {}
300
692
  try { el.focus({ preventScroll: true }); } catch (_) {}
301
693
 
694
+ // Soporte nativo para <select> (D6)
695
+ if (el.tagName.toLowerCase() === "select") {
696
+ return seleccionarOpcion(el, text);
697
+ }
698
+
699
+ // Soporte para checkboxes y radios (D6)
700
+ if (el.type === "checkbox" || el.type === "radio") {
701
+ return conmutarCasilla(el);
702
+ }
703
+
302
704
  if (el.isContentEditable) {
303
705
  if (replace) {
304
706
  const sel = window.getSelection();
@@ -307,7 +709,6 @@
307
709
  sel.removeAllRanges();
308
710
  sel.addRange(range);
309
711
  }
310
- // execCommand emite beforeinput/input igual que si se hubiera tecleado.
311
712
  const ok = document.execCommand("insertText", false, text);
312
713
  if (!ok) {
313
714
  el.textContent = replace ? text : (el.textContent || "") + text;
@@ -324,15 +725,46 @@
324
725
  return true;
325
726
  }
326
727
 
728
+ /**
729
+ * Codigos de tecla.
730
+ */
731
+ const CODIGOS = {
732
+ Enter: 13, Escape: 27, Esc: 27, Tab: 9, Backspace: 8, Delete: 46,
733
+ ArrowUp: 38, ArrowDown: 40, ArrowLeft: 37, ArrowRight: 39,
734
+ Home: 36, End: 35, PageUp: 33, PageDown: 34, Insert: 45,
735
+ " ": 32, Space: 32, Shift: 16, Control: 17, Alt: 18,
736
+ F1: 112, F2: 113, F3: 114, F4: 115, F5: 116, F6: 117,
737
+ F7: 118, F8: 119, F9: 120, F10: 121, F11: 122, F12: 123,
738
+ };
739
+
740
+ /** Nombre DOM del codigo fisico ("KeyA", "Digit1", "Space"...). */
741
+ function codigoFisico(key) {
742
+ if (key === " " || key === "Space") return "Space";
743
+ if (/^[a-zA-Z]$/.test(key)) return "Key" + key.toUpperCase();
744
+ if (/^[0-9]$/.test(key)) return "Digit" + key;
745
+ return key;
746
+ }
747
+
327
748
  function pressKey(el, key) {
328
749
  const target = el || document.activeElement || document.body;
329
- const map = { Enter: 13, Escape: 27, Tab: 9, Backspace: 8, Delete: 46, ArrowUp: 38, ArrowDown: 40 };
330
- const code = map[key] || 0;
331
- const init = { key, code: key, keyCode: code, which: code, bubbles: true, cancelable: true, composed: true };
750
+ const nombre = key === "Space" ? " " : key;
751
+ const code = CODIGOS[key] !== undefined
752
+ ? CODIGOS[key]
753
+ : nombre.length === 1
754
+ ? nombre.toUpperCase().charCodeAt(0)
755
+ : 0;
756
+ const init = {
757
+ key: nombre,
758
+ code: codigoFisico(key),
759
+ keyCode: code,
760
+ which: code,
761
+ bubbles: true,
762
+ cancelable: true,
763
+ composed: true,
764
+ };
332
765
  target.dispatchEvent(new KeyboardEvent("keydown", init));
333
- target.dispatchEvent(new KeyboardEvent("keypress", init));
766
+ if (nombre.length === 1) target.dispatchEvent(new KeyboardEvent("keypress", init));
334
767
  target.dispatchEvent(new KeyboardEvent("keyup", init));
335
- // Si es un formulario y la tecla es Enter, se intenta enviar de verdad.
336
768
  if (key === "Enter" && target.form && typeof target.form.requestSubmit === "function") {
337
769
  try { target.form.requestSubmit(); } catch (_) {}
338
770
  }
@@ -343,9 +775,16 @@
343
775
 
344
776
  chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
345
777
  try {
346
- switch (msg.cmd) {
778
+ const comando = msg.cmd || msg.tipo;
779
+ switch (comando) {
780
+ case "overlay": {
781
+ setOverlayActive(msg.activo === true);
782
+ sendResponse({ ok: true, activo: isOverlayActive() });
783
+ break;
784
+ }
785
+
347
786
  case "ping":
348
- sendResponse({ ok: true, top: IS_TOP, url: location.href });
787
+ sendResponse({ ok: true, top: IS_TOP, url: location.href, overlay: isOverlayActive() });
349
788
  break;
350
789
 
351
790
  case "banner":
@@ -355,52 +794,57 @@
355
794
  break;
356
795
 
357
796
  case "snapshot": {
358
- const items = buildSnapshot(msg.startIndex || 0, msg.filter || "");
797
+ const items = buildSnapshot(msg.startIndex || 0, msg.filter || "", msg.nuevo === true);
359
798
  sendResponse({ ok: true, items, url: location.href, title: document.title, top: IS_TOP });
360
799
  break;
361
800
  }
362
801
 
363
802
  case "click": {
364
- const el = findByRef(msg.ref);
365
- if (!el) { sendResponse({ ok: false, error: "ref no encontrado en este frame" }); break; }
803
+ const resolucion = resolverRef(msg.ref);
804
+ if (!resolucion.ok) { sendResponse(resolucion); break; }
805
+ const el = resolucion.el;
366
806
  const label = accessibleName(el).slice(0, 60);
807
+ resaltarElemento(el, msg.ref, resolucion.id?.role, label);
367
808
  clickElement(el);
368
809
  sendResponse({ ok: true, label });
369
810
  break;
370
811
  }
371
812
 
372
813
  case "type": {
373
- const el = findByRef(msg.ref);
374
- if (!el) { sendResponse({ ok: false, error: "ref no encontrado en este frame" }); break; }
814
+ const resolucion = resolverRef(msg.ref);
815
+ if (!resolucion.ok) { sendResponse(resolucion); break; }
816
+ const el = resolucion.el;
817
+ const label = accessibleName(el).slice(0, 60);
818
+ resaltarElemento(el, msg.ref, resolucion.id?.role, label);
375
819
  typeInto(el, msg.text, msg.replace !== false);
376
- // Se devuelve el valor real tras escribir, para poder verificar.
377
820
  const after = el.isContentEditable ? (el.textContent || "") : (el.value || "");
378
821
  if (msg.submit) pressKey(el, "Enter");
379
- sendResponse({ ok: true, value: String(after).slice(0, 200) });
822
+ sendResponse({ ok: true, value: String(after).slice(0, 200), label });
380
823
  break;
381
824
  }
382
825
 
383
- // Devuelve el centro del elemento en coordenadas del viewport, que es lo
384
- // que necesita el protocolo DevTools para entregar un clic real.
385
826
  case "rect": {
386
- const el = findByRef(msg.ref);
387
- if (!el) { sendResponse({ ok: false, error: "ref no encontrado en este frame" }); break; }
388
- try { el.scrollIntoView({ block: "center", behavior: "instant" }); } catch (_) {}
827
+ const resolucion = resolverRef(msg.ref);
828
+ if (!resolucion.ok) { sendResponse(resolucion); break; }
829
+ const el = resolucion.el;
830
+ try { el.scrollIntoView({ block: "center", inline: "center", behavior: "instant" }); } catch (_) {}
831
+ const label = accessibleName(el).slice(0, 60);
832
+ resaltarElemento(el, msg.ref, resolucion.id?.role, label);
389
833
  const r = el.getBoundingClientRect();
390
834
  sendResponse({
391
835
  ok: true,
392
836
  x: Math.round(r.left + r.width / 2),
393
837
  y: Math.round(r.top + r.height / 2),
394
- label: accessibleName(el).slice(0, 60),
395
- editable: el.isContentEditable || "value" in el,
838
+ label,
839
+ editable: el.isContentEditable || "value" in el || el.tagName.toLowerCase() === "select",
396
840
  });
397
841
  break;
398
842
  }
399
843
 
400
- // Solo lee el valor actual, para verificar despues de escribir.
401
844
  case "value": {
402
- const el = findByRef(msg.ref);
403
- if (!el) { sendResponse({ ok: false, error: "ref no encontrado" }); break; }
845
+ const resolucion = resolverRef(msg.ref);
846
+ if (!resolucion.ok) { sendResponse(resolucion); break; }
847
+ const el = resolucion.el;
404
848
  sendResponse({
405
849
  ok: true,
406
850
  value: String(el.isContentEditable ? el.textContent || "" : el.value || "").slice(0, 300),
@@ -409,12 +853,39 @@
409
853
  }
410
854
 
411
855
  case "press": {
412
- const el = msg.ref ? findByRef(msg.ref) : null;
856
+ let el = null;
857
+ if (msg.ref !== undefined && msg.ref !== null) {
858
+ const resolucion = resolverRef(msg.ref);
859
+ if (resolucion.ok) {
860
+ el = resolucion.el;
861
+ resaltarElemento(el, msg.ref, resolucion.id?.role, accessibleName(el));
862
+ }
863
+ }
413
864
  pressKey(el, msg.keys || "Enter");
414
865
  sendResponse({ ok: true });
415
866
  break;
416
867
  }
417
868
 
869
+ case "waitForCondition": {
870
+ if (msg.selector) {
871
+ let found = false;
872
+ for (const root of allRoots()) {
873
+ try {
874
+ if (root.querySelector(msg.selector)) { found = true; break; }
875
+ } catch (_) {}
876
+ }
877
+ sendResponse({ ok: true, conditionMet: found });
878
+ break;
879
+ }
880
+ if (msg.stable) {
881
+ const quietFor = Date.now() - ultimaMutacion;
882
+ sendResponse({ ok: true, conditionMet: quietFor >= (msg.quietMs || 400) });
883
+ break;
884
+ }
885
+ sendResponse({ ok: true, conditionMet: true });
886
+ break;
887
+ }
888
+
418
889
  case "getText":
419
890
  sendResponse({
420
891
  ok: true,
@@ -427,16 +898,26 @@
427
898
  case "eval": {
428
899
  let out;
429
900
  try {
430
- out = String(eval(msg.script));
901
+ const valor = eval(msg.script);
902
+ if (valor === undefined) out = "undefined";
903
+ else if (valor === null) out = "null";
904
+ else if (typeof valor === "object") {
905
+ try {
906
+ out = JSON.stringify(valor, null, 2);
907
+ if (out === undefined) out = String(valor);
908
+ } catch (_) {
909
+ out = String(valor);
910
+ }
911
+ } else out = String(valor);
431
912
  } catch (e) {
432
913
  out = "Error JS: " + (e && e.message ? e.message : e);
433
914
  }
434
- sendResponse({ ok: true, result: out });
915
+ sendResponse({ ok: true, result: String(out).slice(0, 20000) });
435
916
  break;
436
917
  }
437
918
 
438
919
  default:
439
- sendResponse({ ok: false, error: "comando desconocido: " + msg.cmd });
920
+ sendResponse({ ok: false, error: "comando desconocido: " + (msg.cmd || msg.tipo) });
440
921
  }
441
922
  } catch (err) {
442
923
  sendResponse({ ok: false, error: String((err && err.message) || err) });