chocolatito-code 1.1.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.
@@ -195,6 +195,9 @@ function connect(port) {
195
195
  ws.onclose = () => {
196
196
  clearInterval(link.timer);
197
197
  if (link.socket === ws) link.socket = null;
198
+ const oldTab = link.state && link.state.workingTabId;
199
+ if (link.state) link.state.workingTabId = null;
200
+ if (oldTab) setTabOverlay(oldTab, false);
198
201
  refreshBadge();
199
202
  scheduleReconnect(port);
200
203
  };
@@ -399,6 +402,86 @@ async function readTab(tabId, st) {
399
402
  return { tabId: activa.id, own: false };
400
403
  }
401
404
 
405
+ /**
406
+ * Comprueba si alguna sesión activa está usando la pestaña como pestaña de trabajo (B4).
407
+ * Permite que dos sesiones convivan sin que el cierre de una apague el borde
408
+ * de la pestaña si la otra aún la está utilizando.
409
+ */
410
+ function tabEnUso(tabId) {
411
+ if (!tabId) return false;
412
+ for (const link of links.values()) {
413
+ if (link.state && link.state.workingTabId === tabId) return true;
414
+ }
415
+ return false;
416
+ }
417
+
418
+ /**
419
+ * Pide al content script encender o apagar el borde naranja y la insignia (B1-B4).
420
+ * Si se pide apagar pero otra sesión aún usa la pestaña, se mantiene encendido.
421
+ * Si el content script no está inyectado todavía (páginas internas, cargando),
422
+ * se captura el error silenciosamente para no impedir la acción.
423
+ */
424
+ async function setTabOverlay(tabId, activo) {
425
+ if (!tabId) return;
426
+ if (!activo && tabEnUso(tabId)) return;
427
+ try {
428
+ await chrome.tabs.sendMessage(tabId, { cmd: "overlay", tipo: "overlay", activo: Boolean(activo) });
429
+ } catch (_) {
430
+ // Si la página aún no tiene content script o es una URL vetada, se ignora
431
+ }
432
+ }
433
+
434
+ // D8: Detección y reporte de descargas disparadas por acciones del agente
435
+ const descargasRecientes = [];
436
+ try {
437
+ if (typeof chrome !== "undefined" && chrome.downloads && chrome.downloads.onCreated) {
438
+ chrome.downloads.onCreated.addListener((item) => {
439
+ descargasRecientes.push({
440
+ id: item.id,
441
+ url: item.url,
442
+ filename: item.filename || "",
443
+ state: item.state || "in_progress",
444
+ timestamp: Date.now(),
445
+ });
446
+ if (descargasRecientes.length > 20) descargasRecientes.shift();
447
+ });
448
+ }
449
+ if (typeof chrome !== "undefined" && chrome.downloads && chrome.downloads.onChanged) {
450
+ chrome.downloads.onChanged.addListener((delta) => {
451
+ const item = descargasRecientes.find((d) => d.id === delta.id);
452
+ if (item) {
453
+ if (delta.filename && delta.filename.current) item.filename = delta.filename.current;
454
+ if (delta.state && delta.state.current) item.state = delta.state.current;
455
+ }
456
+ });
457
+ }
458
+ } catch (_) {}
459
+
460
+ async function detectarDescarga(tiempoInicio) {
461
+ try {
462
+ if (typeof chrome !== "undefined" && chrome.downloads && typeof chrome.downloads.search === "function") {
463
+ const items = await chrome.downloads.search({
464
+ startedAfter: new Date(tiempoInicio).toISOString(),
465
+ limit: 1,
466
+ });
467
+ if (items && items.length > 0) {
468
+ return {
469
+ id: items[0].id,
470
+ filename: items[0].filename || "",
471
+ state: items[0].state || "in_progress",
472
+ url: items[0].url,
473
+ };
474
+ }
475
+ }
476
+ } catch (_) {}
477
+ for (let i = descargasRecientes.length - 1; i >= 0; i--) {
478
+ if (descargasRecientes[i].timestamp >= tiempoInicio) {
479
+ return descargasRecientes[i];
480
+ }
481
+ }
482
+ return null;
483
+ }
484
+
402
485
  /**
403
486
  * Mete la pestana en un grupo visible llamado "Chocolatito Code", para que el
404
487
  * usuario vea de un vistazo donde esta trabajando el agente.
@@ -732,6 +815,7 @@ async function handle(msg, st) {
732
815
  await esperarCarga(tab.id, msg.timeout || 30000);
733
816
  await groupTab(tab.id, st);
734
817
  await banner(tab.id, msg.note || "pestana abierta");
818
+ await setTabOverlay(tab.id, true);
735
819
  const info = await chrome.tabs.get(tab.id);
736
820
  return { tabId: tab.id, title: info.title || "", url: info.url || "" };
737
821
  }
@@ -743,12 +827,18 @@ async function handle(msg, st) {
743
827
  (t) => (t.url || "").toLowerCase().includes(needle) || (t.title || "").toLowerCase().includes(needle)
744
828
  );
745
829
  if (!hit) throw new Error('ninguna pestana contiene "' + msg.match + '"');
830
+ if (st.workingTabId && st.workingTabId !== hit.id) {
831
+ const vieja = st.workingTabId;
832
+ st.workingTabId = null;
833
+ await setTabOverlay(vieja, false);
834
+ }
746
835
  st.workingTabId = hit.id;
747
836
  // Elegirla es un acto explicito del agente: a partir de aqui es suya y la
748
837
  // puede cerrar. Mirarla (readTab) nunca da ese derecho.
749
838
  st.propias.add(hit.id);
750
839
  await groupTab(hit.id, st);
751
840
  await banner(hit.id, "pestana seleccionada");
841
+ await setTabOverlay(hit.id, true);
752
842
  return { tabId: hit.id, title: hit.title || "", url: hit.url || "" };
753
843
  }
754
844
 
@@ -770,13 +860,17 @@ async function handle(msg, st) {
770
860
  await groupTab(tabId, st);
771
861
  const info = await chrome.tabs.get(tabId);
772
862
  await banner(tabId, "navegando a " + (info.url || "").slice(0, 60));
863
+ await setTabOverlay(tabId, true);
773
864
  return { tabId, title: info.title || "", url: info.url || "" };
774
865
  }
775
866
 
776
867
  if (cmd === "snapshot") {
777
868
  const { tabId, own } = await readTab(msg.tabId, st);
778
869
  // Solo se adopta como pestana de trabajo si ya era nuestra o vino con tabId.
779
- if (own) st.workingTabId = tabId;
870
+ if (own) {
871
+ st.workingTabId = tabId;
872
+ await setTabOverlay(tabId, true);
873
+ }
780
874
  await banner(tabId, "leyendo la pagina");
781
875
  st.refIndex.clear();
782
876
 
@@ -839,6 +933,9 @@ async function handle(msg, st) {
839
933
  const tabId = loc ? loc.tabId : await actTab(msg.tabId, st);
840
934
  const frameId = loc ? loc.frameId : 0;
841
935
  st.workingTabId = tabId;
936
+ await setTabOverlay(tabId, true);
937
+
938
+ const tiempoInicioAccion = Date.now() - 50;
842
939
 
843
940
  if (cmd === "click") await banner(tabId, "pulsando un elemento");
844
941
  if (cmd === "type") await banner(tabId, "escribiendo");
@@ -882,7 +979,24 @@ async function handle(msg, st) {
882
979
  // Se deja respirar a la pagina para que reaccione antes del siguiente paso.
883
980
  await sleep(msg.settle === undefined ? 600 : msg.settle);
884
981
  const info = await chrome.tabs.get(tabId);
885
- return { tabId, title: info.title || "", url: info.url || "", label: res.label, value: res.value, via: res.via, warning: res.warning };
982
+
983
+ // D8: Detectar si esta acción inició o completó alguna descarga de archivo
984
+ const descarga = await detectarDescarga(tiempoInicioAccion);
985
+ if (descarga) {
986
+ res.download = descarga;
987
+ res.label = (res.label ? res.label + " " : "") + `[Descarga: ${descarga.filename || descarga.url}]`;
988
+ }
989
+
990
+ return {
991
+ tabId,
992
+ title: info.title || "",
993
+ url: info.url || "",
994
+ label: res.label,
995
+ value: res.value,
996
+ via: res.via,
997
+ warning: res.warning,
998
+ download: res.download,
999
+ };
886
1000
  }
887
1001
 
888
1002
  if (cmd === "getText") {
@@ -903,7 +1017,23 @@ async function handle(msg, st) {
903
1017
 
904
1018
  if (cmd === "waitFor") {
905
1019
  const { tabId } = await readTab(msg.tabId, st);
906
- const deadline = Date.now() + (msg.timeout || 20000);
1020
+ const timeout = msg.timeout || 20000;
1021
+ const deadline = Date.now() + timeout;
1022
+
1023
+ // D3: Espera hasta que aparezca un selector CSS en el DOM
1024
+ if (msg.selector || msg.css) {
1025
+ const sel = msg.selector || msg.css;
1026
+ while (Date.now() < deadline) {
1027
+ const res = await tell(tabId, 0, { cmd: "waitForCondition", selector: sel });
1028
+ if (res && res.ok && res.conditionMet) {
1029
+ return { ok: true, found: true, selector: sel };
1030
+ }
1031
+ await sleep(300);
1032
+ }
1033
+ return { ok: false, found: false, error: `Tiempo agotado esperando el selector "${sel}"` };
1034
+ }
1035
+
1036
+ // D3: Espera hasta que aparezca un texto concreto
907
1037
  if (msg.text) {
908
1038
  while (Date.now() < deadline) {
909
1039
  const res = await tell(tabId, 0, { cmd: "getText" });
@@ -912,8 +1042,22 @@ async function handle(msg, st) {
912
1042
  }
913
1043
  return { found: false };
914
1044
  }
915
- await sleep(msg.ms || 2000);
916
- return { waited: msg.ms || 2000 };
1045
+
1046
+ // D3: Espera hasta que la pantalla "esté quieta" (sin mutaciones de DOM durante quietMs)
1047
+ if (msg.stable || msg.quiet || msg.condition === "stable") {
1048
+ while (Date.now() < deadline) {
1049
+ const res = await tell(tabId, 0, { cmd: "waitForCondition", stable: true, quietMs: msg.quietMs || 400 });
1050
+ if (res && res.ok && res.conditionMet) {
1051
+ return { ok: true, stable: true };
1052
+ }
1053
+ await sleep(250);
1054
+ }
1055
+ return { ok: true, stable: true, warning: "Tiempo límite alcanzado esperando quietud" };
1056
+ }
1057
+
1058
+ const ms = msg.ms || 2000;
1059
+ await sleep(ms);
1060
+ return { waited: ms };
917
1061
  }
918
1062
 
919
1063
  if (cmd === "screenshot") {
@@ -950,9 +1094,12 @@ async function handle(msg, st) {
950
1094
  }
951
1095
 
952
1096
  if (ids.length === 0) return { closed: 0 };
953
- await chrome.tabs.remove(ids);
954
- for (const id of ids) st.propias.delete(id);
955
1097
  if (ids.includes(st.workingTabId)) st.workingTabId = null;
1098
+ for (const id of ids) st.propias.delete(id);
1099
+ await chrome.tabs.remove(ids);
1100
+ for (const id of ids) {
1101
+ await setTabOverlay(id, false);
1102
+ }
956
1103
  return { closed: ids.length };
957
1104
  }
958
1105
 
@@ -962,16 +1109,17 @@ async function handle(msg, st) {
962
1109
  // no era de nadie, y el usuario tenia que deshacerlo a mano.
963
1110
  const aSoltar = new Set(st.propias);
964
1111
  if (st.workingTabId) aSoltar.add(st.workingTabId);
1112
+ st.groupId = null;
1113
+ st.workingTabId = null;
1114
+ st.propias.clear();
1115
+ st.refIndex.clear();
965
1116
  for (const id of aSoltar) {
966
1117
  try {
967
1118
  await chrome.tabs.ungroup(id);
968
1119
  } catch (_) {}
969
1120
  await banner(id, null);
1121
+ await setTabOverlay(id, false);
970
1122
  }
971
- st.groupId = null;
972
- st.workingTabId = null;
973
- st.propias.clear();
974
- st.refIndex.clear();
975
1123
  return { ok: true, soltadas: aSoltar.size };
976
1124
  }
977
1125
 
@@ -983,6 +1131,7 @@ async function handle(msg, st) {
983
1131
  // with id") en vez de decir que ya no hay donde trabajar.
984
1132
  try {
985
1133
  chrome.tabs.onRemoved.addListener((tabId) => {
1134
+ setTabOverlay(tabId, false);
986
1135
  for (const link of links.values()) {
987
1136
  if (!link.state) continue;
988
1137
  link.state.propias.delete(tabId);
@@ -992,4 +1141,26 @@ try {
992
1141
  });
993
1142
  } catch (_) {}
994
1143
 
1144
+ // Al navegar o recargar la página, se reinyecta el content script y el overlay
1145
+ // se apaga. Si la pestaña sigue siendo de trabajo para alguna sesión, se vuelve a activar (B4).
1146
+ try {
1147
+ if (typeof chrome !== "undefined" && chrome.webNavigation && chrome.webNavigation.onCompleted && typeof chrome.webNavigation.onCompleted.addListener === "function") {
1148
+ chrome.webNavigation.onCompleted.addListener((details) => {
1149
+ if (details && details.frameId === 0 && tabEnUso(details.tabId)) {
1150
+ setTabOverlay(details.tabId, true);
1151
+ }
1152
+ });
1153
+ }
1154
+ } catch (_) {}
1155
+
1156
+ try {
1157
+ if (typeof chrome !== "undefined" && chrome.tabs && chrome.tabs.onUpdated && typeof chrome.tabs.onUpdated.addListener === "function") {
1158
+ chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
1159
+ if (changeInfo && changeInfo.status === "complete" && tabEnUso(tabId)) {
1160
+ setTabOverlay(tabId, true);
1161
+ }
1162
+ });
1163
+ }
1164
+ } catch (_) {}
1165
+
995
1166
  for (const p of WS_PORTS) connect(p);