baa-conductor


baa-conductor / plugins / baa-firefox
codex@macbookpro  ·  2026-04-01

content-script.js

  1const CONTENT_SCRIPT_RUNTIME_KEY = "__baaFirefoxContentScriptRuntime__";
  2const CONTROL_STATE_STORAGE_KEY = "baaFirefox.controlState";
  3const PAGE_CONTROL_STATE_STORAGE_KEY = "baaFirefox.pageControls";
  4const PAGE_CONTROL_OVERLAY_ID = "__baaFirefoxPageControlOverlay__";
  5
  6const previousContentScriptRuntime = window[CONTENT_SCRIPT_RUNTIME_KEY];
  7if (previousContentScriptRuntime && typeof previousContentScriptRuntime.dispose === "function") {
  8  previousContentScriptRuntime.dispose();
  9}
 10
 11function sendBridgeMessage(type, data) {
 12  browser.runtime.sendMessage({
 13    type,
 14    data
 15  }).catch(() => {});
 16}
 17
 18const deliveryRuntime = typeof globalThis.BAADeliveryAdapters?.createDeliveryRuntime === "function"
 19  ? globalThis.BAADeliveryAdapters.createDeliveryRuntime()
 20  : null;
 21
 22function trimToNull(value) {
 23  if (typeof value !== "string") {
 24    return null;
 25  }
 26
 27  const normalized = value.trim();
 28  return normalized === "" ? null : normalized;
 29}
 30
 31function sendDiagnosticLog(eventName, detail = {}) {
 32  const normalizedEventName = trimToNull(eventName);
 33  if (!normalizedEventName) {
 34    return;
 35  }
 36
 37  sendBridgeMessage("baa_diagnostic_log", {
 38    ...(detail || {}),
 39    event: normalizedEventName,
 40    source: trimToNull(detail?.source) || "content-script",
 41    url: trimToNull(detail?.url) || location.href
 42  });
 43}
 44
 45function normalizeMode(value) {
 46  const normalized = String(value || "").trim().toLowerCase();
 47
 48  if (normalized === "running" || normalized === "paused" || normalized === "draining") {
 49    return normalized;
 50  }
 51
 52  return "unknown";
 53}
 54
 55function normalizeControlConnection(value) {
 56  const normalized = String(value || "").trim().toLowerCase();
 57
 58  if (normalized === "connecting" || normalized === "connected" || normalized === "retrying" || normalized === "disconnected") {
 59    return normalized;
 60  }
 61
 62  return "disconnected";
 63}
 64
 65function formatModeLabel(mode) {
 66  switch (normalizeMode(mode)) {
 67    case "running":
 68      return "运行中";
 69    case "paused":
 70      return "已暂停";
 71    case "draining":
 72      return "排空中";
 73    default:
 74      return "未知";
 75  }
 76}
 77
 78function formatConnectionLabel(connection) {
 79  switch (normalizeControlConnection(connection)) {
 80    case "connecting":
 81      return "连接中";
 82    case "connected":
 83      return "已连接";
 84    case "retrying":
 85      return "重试中";
 86    default:
 87      return "已断开";
 88  }
 89}
 90
 91function resolveOverlayVisualState(snapshot, pendingAction) {
 92  if (trimToNull(pendingAction)) {
 93    return "pending";
 94  }
 95
 96  const connection = normalizeControlConnection(snapshot?.controlConnection);
 97  if (connection === "connecting" || connection === "retrying") {
 98    return "pending";
 99  }
100
101  if (connection !== "connected") {
102    return "disconnected";
103  }
104
105  return normalizeMode(snapshot?.mode);
106}
107
108function resolveCompactControlAction(snapshot, pendingAction) {
109  if (trimToNull(pendingAction)) {
110    return {
111      action: null,
112      label: "处理中",
113      title: `正在执行 ${pendingAction}`
114    };
115  }
116
117  const mode = normalizeMode(snapshot?.mode);
118
119  if (mode === "paused" || mode === "draining") {
120    return {
121      action: "resume",
122      label: "恢复系统",
123      title: "恢复系统自动化"
124    };
125  }
126
127  return {
128    action: "pause",
129    label: "暂停系统",
130    title: "暂停系统自动化"
131  };
132}
133
134function normalizePageAutomationStatus(value) {
135  const normalized = String(value || "").trim().toLowerCase();
136  return normalized === "auto" || normalized === "manual" || normalized === "paused"
137    ? normalized
138    : null;
139}
140
141function normalizePageControlSnapshot(value) {
142  if (!value || typeof value !== "object" || Array.isArray(value)) {
143    return null;
144  }
145
146  return {
147    key: trimToNull(value.key),
148    platform: trimToNull(value.platform),
149    tabId: Number.isInteger(value.tabId) ? value.tabId : null,
150    conversationId: trimToNull(value.conversationId),
151    localConversationId: trimToNull(value.localConversationId),
152    pageUrl: trimToNull(value.pageUrl),
153    pageTitle: trimToNull(value.pageTitle),
154    automationStatus: normalizePageAutomationStatus(value.automationStatus),
155    lastNonPausedAutomationStatus: normalizePageAutomationStatus(value.lastNonPausedAutomationStatus),
156    paused: value.paused === true,
157    pauseReason: trimToNull(value.pauseReason),
158    shellPage: value.shellPage === true,
159    updatedAt: Number(value.updatedAt) || 0
160  };
161}
162
163function selectPageControlSnapshot(rawMap, key) {
164  if (!key || !rawMap || typeof rawMap !== "object" || Array.isArray(rawMap)) {
165    return null;
166  }
167
168  return normalizePageControlSnapshot(rawMap[key]);
169}
170
171function formatPageStatusLabel(snapshot) {
172  if (!snapshot) {
173    return "未识别";
174  }
175
176  switch (normalizePageAutomationStatus(snapshot.automationStatus)) {
177    case "auto":
178      return "自动";
179    case "manual":
180      return "手动";
181    case "paused":
182      return "已暂停";
183    default:
184      return snapshot.paused ? "本页已暂停" : "本页运行中";
185  }
186}
187
188function formatPauseReasonLabel(reason) {
189  switch (trimToNull(reason)) {
190    case "ai_pause":
191      return "AI 主动暂停";
192    case "error_loop":
193      return "错误循环";
194    case "execution_failure":
195      return "执行失败";
196    case "page_paused_by_user":
197    case "user_pause":
198      return "用户暂停";
199    case "repeated_message":
200      return "重复消息";
201    case "repeated_renewal":
202      return "重复续命";
203    case "rescue_wait":
204      return "等待救援";
205    case "system_pause":
206      return "系统暂停";
207    default:
208      return trimToNull(reason);
209  }
210}
211
212function formatPendingActionLabel(action) {
213  switch (String(action || "").trim().toLowerCase()) {
214    case "pause":
215      return "系统暂停";
216    case "resume":
217      return "系统恢复";
218    case "drain":
219      return "系统排空";
220    case "page_manual":
221      return "设为手动";
222    case "page_pause":
223      return "暂停本页";
224    case "page_resume":
225      return "恢复本页";
226    default:
227      return null;
228  }
229}
230
231function removeExistingOverlayRoots() {
232  if (typeof document?.querySelectorAll === "function") {
233    const roots = Array.from(document.querySelectorAll(`#${PAGE_CONTROL_OVERLAY_ID}`) || []);
234    for (const entry of roots) {
235      if (entry && typeof entry.remove === "function") {
236        entry.remove();
237      }
238    }
239    return;
240  }
241
242  if (typeof document?.getElementById !== "function") {
243    return;
244  }
245
246  const existing = document.getElementById(PAGE_CONTROL_OVERLAY_ID);
247  if (existing && typeof existing.remove === "function") {
248    existing.remove();
249  }
250}
251
252function createPageControlOverlayRuntime() {
253  let root = null;
254  let shadow = null;
255  let dock = null;
256  let panel = null;
257  let statusText = null;
258  let errorText = null;
259  let pendingAction = null;
260  let controlSnapshot = null;
261  let pageControlSnapshot = null;
262  let pageControlKey = null;
263  let destroyed = false;
264  let dismissButton = null;
265  let hidden = false;
266  let buttonMap = {};
267  let pageButtonMap = {};
268  let badgeButton = null;
269  let compactActionButton = null;
270
271  const overlayStyle = `
272    :host {
273      all: initial;
274    }
275
276    .baa-overlay {
277      position: fixed;
278      right: 14px;
279      bottom: 14px;
280      z-index: 2147483647;
281      width: 252px;
282      box-sizing: border-box;
283      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
284      color: #f7f3e8;
285    }
286
287    .baa-panel {
288      box-sizing: border-box;
289      border: 1px solid rgba(242, 203, 120, 0.35);
290      border-radius: 12px;
291      background: rgba(24, 20, 17, 0.92);
292      backdrop-filter: blur(10px);
293      box-shadow: 0 10px 28px rgba(0, 0, 0, 0.3);
294      padding: 10px;
295    }
296
297    .baa-panel[hidden] {
298      display: none;
299    }
300
301    .baa-head {
302      display: flex;
303      align-items: center;
304      justify-content: space-between;
305      gap: 8px;
306      margin-bottom: 8px;
307    }
308
309    .baa-title {
310      font-size: 11px;
311      line-height: 1.2;
312      letter-spacing: 0.06em;
313      text-transform: uppercase;
314      color: #f2cb78;
315      font-weight: 700;
316    }
317
318    .baa-dismiss {
319      appearance: none;
320      border: 0;
321      background: transparent;
322      color: rgba(247, 243, 232, 0.72);
323      cursor: pointer;
324      font-size: 14px;
325      line-height: 1;
326      padding: 0;
327    }
328
329    .baa-dismiss:hover {
330      color: #ffffff;
331    }
332
333    .baa-status {
334      font-size: 12px;
335      line-height: 1.4;
336      color: #f7f3e8;
337      margin-bottom: 8px;
338      white-space: pre-wrap;
339      word-break: break-word;
340    }
341
342    .baa-page-actions {
343      display: grid;
344      grid-template-columns: repeat(3, minmax(0, 1fr));
345      gap: 6px;
346      margin-bottom: 6px;
347    }
348
349    .baa-error {
350      min-height: 14px;
351      font-size: 11px;
352      line-height: 1.35;
353      color: #ffb7a1;
354      margin-bottom: 8px;
355    }
356
357    .baa-actions {
358      display: grid;
359      grid-template-columns: repeat(3, minmax(0, 1fr));
360      gap: 6px;
361    }
362
363    .baa-btn {
364      appearance: none;
365      border: 1px solid rgba(255, 255, 255, 0.14);
366      background: rgba(255, 255, 255, 0.08);
367      color: #f7f3e8;
368      border-radius: 8px;
369      font-size: 12px;
370      line-height: 1;
371      padding: 8px 6px;
372      cursor: pointer;
373      transition: background 120ms ease, border-color 120ms ease, opacity 120ms ease;
374    }
375
376    .baa-btn:hover:not(:disabled) {
377      background: rgba(255, 255, 255, 0.14);
378      border-color: rgba(242, 203, 120, 0.48);
379    }
380
381    .baa-btn:disabled {
382      opacity: 0.52;
383      cursor: default;
384    }
385
386    .baa-btn[data-action="pause"] {
387      color: #ffcfb3;
388    }
389
390    .baa-btn[data-action="resume"] {
391      color: #b9f0c7;
392    }
393
394    .baa-btn[data-action="drain"] {
395      color: #f2cb78;
396    }
397
398    .baa-btn[data-role="page"] {
399      color: #9fd9ff;
400    }
401
402    .baa-btn[data-role="page"][data-state="manual"] {
403      color: #f2cb78;
404    }
405
406    .baa-btn[data-role="page"][data-state="paused"] {
407      color: #b9f0c7;
408    }
409
410    .baa-dock {
411      position: fixed;
412      right: 14px;
413      bottom: 14px;
414      z-index: 2147483647;
415      display: none;
416      align-items: center;
417      gap: 8px;
418    }
419
420    .baa-dock[data-visible="true"] {
421      display: flex;
422    }
423
424    .baa-badge,
425    .baa-compact-action {
426      appearance: none;
427      border: 1px solid rgba(242, 203, 120, 0.35);
428      border-radius: 999px;
429      background: rgba(24, 20, 17, 0.92);
430      color: #f2cb78;
431      box-shadow: 0 10px 28px rgba(0, 0, 0, 0.3);
432      padding: 8px 10px;
433      font-size: 11px;
434      line-height: 1;
435      cursor: pointer;
436      transition: border-color 120ms ease, color 120ms ease, background 120ms ease, opacity 120ms ease;
437    }
438
439    .baa-badge {
440      min-width: 48px;
441      font-weight: 700;
442      letter-spacing: 0.08em;
443    }
444
445    .baa-compact-action {
446      min-width: 52px;
447    }
448
449    .baa-badge:hover,
450    .baa-compact-action:hover:not(:disabled) {
451      background: rgba(255, 255, 255, 0.12);
452    }
453
454    .baa-compact-action:disabled {
455      opacity: 0.58;
456      cursor: default;
457    }
458
459    .baa-badge[data-state="running"],
460    .baa-compact-action[data-state="running"] {
461      color: #9df3bb;
462      border-color: rgba(98, 214, 139, 0.45);
463    }
464
465    .baa-badge[data-state="paused"],
466    .baa-compact-action[data-state="paused"] {
467      color: #f2cb78;
468      border-color: rgba(242, 203, 120, 0.48);
469    }
470
471    .baa-badge[data-state="draining"],
472    .baa-compact-action[data-state="draining"] {
473      color: #ffbf7d;
474      border-color: rgba(255, 191, 125, 0.48);
475    }
476
477    .baa-badge[data-state="pending"],
478    .baa-compact-action[data-state="pending"] {
479      color: #c7d2ff;
480      border-color: rgba(199, 210, 255, 0.42);
481    }
482
483    .baa-badge[data-state="disconnected"],
484    .baa-compact-action[data-state="disconnected"] {
485      color: #d0cec7;
486      border-color: rgba(208, 206, 199, 0.36);
487    }
488  `;
489
490  function setError(message) {
491    if (!errorText) {
492      return;
493    }
494
495    errorText.textContent = trimToNull(message) || "";
496  }
497
498  function updateStatus() {
499    if (!statusText) {
500      return;
501    }
502
503    const snapshot = controlSnapshot || {};
504    const pageSnapshot = pageControlSnapshot || null;
505    const modeLabel = formatModeLabel(snapshot.mode);
506    const connectionLabel = formatConnectionLabel(snapshot.controlConnection);
507    const pageLabel = formatPageStatusLabel(pageSnapshot);
508    const pauseReasonLabel = pageSnapshot?.paused && pageSnapshot?.pauseReason
509      ? `\n暂停原因: ${formatPauseReasonLabel(pageSnapshot.pauseReason) || pageSnapshot.pauseReason}`
510      : "";
511    const conversationLabel = pageSnapshot?.conversationId ? `\n对话: ${pageSnapshot.conversationId}` : "";
512    const actionLabel = formatPendingActionLabel(pendingAction)
513      ? `\n动作: ${formatPendingActionLabel(pendingAction)}`
514      : "";
515
516    statusText.textContent = `系统: ${modeLabel}\n连接: ${connectionLabel}\n当前对话: ${pageLabel}${pauseReasonLabel}${conversationLabel}${actionLabel}`;
517  }
518
519  function updateButtons() {
520    const activePending = trimToNull(pendingAction);
521    const normalizedMode = normalizeMode(controlSnapshot?.mode);
522    const automationStatus = normalizePageAutomationStatus(pageControlSnapshot?.automationStatus);
523    const pagePaused = pageControlSnapshot?.paused === true;
524    const hasConversationAutomation = trimToNull(pageControlSnapshot?.localConversationId) != null || automationStatus != null;
525
526    for (const [action, button] of Object.entries(pageButtonMap)) {
527      if (!button) {
528        continue;
529      }
530
531      let disabled = activePending != null || !pageControlSnapshot;
532
533      if (!disabled) {
534        switch (action) {
535          case "pause":
536            disabled = pagePaused;
537            break;
538          case "resume":
539            disabled = !pagePaused;
540            break;
541          case "manual":
542            disabled = !hasConversationAutomation || automationStatus === "manual";
543            break;
544          default:
545            disabled = true;
546            break;
547        }
548      }
549
550      button.disabled = disabled;
551      button.dataset.state = automationStatus || (pagePaused ? "paused" : "auto");
552    }
553
554    for (const [action, button] of Object.entries(buttonMap)) {
555      if (!button) {
556        continue;
557      }
558
559      button.disabled = activePending != null || normalizedMode === action;
560    }
561  }
562
563  function updateCompactControls() {
564    const visualState = resolveOverlayVisualState(controlSnapshot, pendingAction);
565    const quickAction = resolveCompactControlAction(controlSnapshot, pendingAction);
566    const connectionLabel = formatConnectionLabel(controlSnapshot?.controlConnection);
567    const modeLabel = formatModeLabel(controlSnapshot?.mode);
568
569    if (badgeButton) {
570      badgeButton.dataset.state = visualState;
571      badgeButton.title = `打开自动化控制面板(${connectionLabel} / ${modeLabel}`;
572    }
573
574    if (compactActionButton) {
575      compactActionButton.dataset.state = visualState;
576      compactActionButton.dataset.action = quickAction.action || "";
577      compactActionButton.disabled = quickAction.action == null;
578      compactActionButton.textContent = quickAction.label;
579      compactActionButton.title = quickAction.title;
580    }
581  }
582
583  function updateVisibility() {
584    if (panel) {
585      panel.hidden = hidden;
586    }
587
588    if (dock) {
589      dock.dataset.visible = hidden ? "true" : "false";
590    }
591
592    if (dismissButton) {
593      dismissButton.textContent = hidden ? "+" : "×";
594      dismissButton.title = hidden ? "显示自动化控制" : "隐藏自动化控制";
595    }
596  }
597
598  function render() {
599    updateStatus();
600    updateButtons();
601    updateCompactControls();
602    updateVisibility();
603  }
604
605  async function readStoredOverlayState() {
606    try {
607      const stored = await browser.storage.local.get([
608        CONTROL_STATE_STORAGE_KEY,
609        PAGE_CONTROL_STATE_STORAGE_KEY
610      ]);
611      controlSnapshot = stored?.[CONTROL_STATE_STORAGE_KEY] || null;
612      pageControlSnapshot = selectPageControlSnapshot(stored?.[PAGE_CONTROL_STATE_STORAGE_KEY], pageControlKey);
613      render();
614    } catch (error) {
615      setError(error instanceof Error ? error.message : String(error));
616    }
617  }
618
619  async function loadRuntimeOverlayState() {
620    try {
621      const result = await browser.runtime.sendMessage({
622        type: "get_page_control_state",
623        source: "page_overlay"
624      });
625
626      if (!result?.ok) {
627        throw new Error(trimToNull(result?.error) || "get_page_control_state failed");
628      }
629
630      controlSnapshot = result.control || controlSnapshot;
631      pageControlSnapshot = normalizePageControlSnapshot(result.page);
632      pageControlKey = trimToNull(result?.page?.key) || pageControlKey;
633      render();
634    } catch (error) {
635      setError(error instanceof Error ? error.message : String(error));
636    }
637  }
638
639  async function runControlAction(action) {
640    pendingAction = action;
641    setError("");
642    render();
643
644    try {
645      const result = await browser.runtime.sendMessage({
646        type: "control_plane_command",
647        action,
648        source: "page_overlay"
649      });
650
651      if (!result?.ok) {
652        throw new Error(trimToNull(result?.error) || `${action} failed`);
653      }
654
655      controlSnapshot = result.snapshot || controlSnapshot;
656    } catch (error) {
657      setError(error instanceof Error ? error.message : String(error));
658    } finally {
659      pendingAction = null;
660      render();
661    }
662  }
663
664  async function runPageControlAction(action) {
665    pendingAction =
666      action === "manual"
667        ? "page_manual"
668        : (action === "pause" ? "page_pause" : "page_resume");
669    setError("");
670    render();
671
672    try {
673      const result = await browser.runtime.sendMessage({
674        type: "page_control_command",
675        action,
676        source: "page_overlay"
677      });
678
679      if (!result?.ok) {
680        throw new Error(trimToNull(result?.error) || `${action} page control failed`);
681      }
682
683      controlSnapshot = result.control || controlSnapshot;
684      pageControlSnapshot = normalizePageControlSnapshot(result.page);
685      pageControlKey = trimToNull(result?.page?.key) || pageControlKey;
686    } catch (error) {
687      setError(error instanceof Error ? error.message : String(error));
688    } finally {
689      pendingAction = null;
690      render();
691    }
692  }
693
694  function handleStorageChanged(changes, areaName) {
695    if (destroyed || areaName !== "local") {
696      return;
697    }
698
699    if (changes?.[CONTROL_STATE_STORAGE_KEY]) {
700      controlSnapshot = changes[CONTROL_STATE_STORAGE_KEY].newValue || null;
701    }
702
703    if (changes?.[PAGE_CONTROL_STATE_STORAGE_KEY]) {
704      pageControlSnapshot = selectPageControlSnapshot(
705        changes[PAGE_CONTROL_STATE_STORAGE_KEY].newValue,
706        pageControlKey
707      );
708    }
709
710    render();
711  }
712
713  function handleDismiss() {
714    hidden = !hidden;
715    render();
716  }
717
718  function handleOpenPanel() {
719    hidden = false;
720    render();
721  }
722
723  function handleCompactAction() {
724    const quickAction = resolveCompactControlAction(controlSnapshot, pendingAction);
725
726    if (!quickAction.action) {
727      return;
728    }
729
730    runControlAction(quickAction.action).catch(() => {});
731  }
732
733  function mount() {
734    if (destroyed || root?.isConnected) {
735      return;
736    }
737
738    removeExistingOverlayRoots();
739
740    root = document.createElement("div");
741    root.id = PAGE_CONTROL_OVERLAY_ID;
742    shadow = root.attachShadow({ mode: "open" });
743
744    const style = document.createElement("style");
745    style.textContent = overlayStyle;
746    shadow.appendChild(style);
747
748    dock = document.createElement("div");
749    dock.className = "baa-dock";
750    dock.dataset.visible = "false";
751
752    badgeButton = document.createElement("button");
753    badgeButton.type = "button";
754    badgeButton.className = "baa-badge";
755    badgeButton.textContent = "BAA";
756    badgeButton.addEventListener("click", handleOpenPanel);
757    dock.appendChild(badgeButton);
758
759    compactActionButton = document.createElement("button");
760    compactActionButton.type = "button";
761    compactActionButton.className = "baa-compact-action";
762    compactActionButton.addEventListener("click", handleCompactAction);
763    dock.appendChild(compactActionButton);
764
765    shadow.appendChild(dock);
766
767    panel = document.createElement("div");
768    panel.className = "baa-overlay baa-panel";
769
770    const head = document.createElement("div");
771    head.className = "baa-head";
772
773    const title = document.createElement("div");
774    title.className = "baa-title";
775    title.textContent = "自动化控制";
776    head.appendChild(title);
777
778    dismissButton = document.createElement("button");
779    dismissButton.type = "button";
780    dismissButton.className = "baa-dismiss";
781    dismissButton.addEventListener("click", handleDismiss);
782    head.appendChild(dismissButton);
783
784    panel.appendChild(head);
785
786    statusText = document.createElement("div");
787    statusText.className = "baa-status";
788    panel.appendChild(statusText);
789
790    const pageActions = document.createElement("div");
791    pageActions.className = "baa-page-actions";
792
793    for (const entry of [
794      { action: "pause", label: "暂停本页" },
795      { action: "resume", label: "恢复本页" },
796      { action: "manual", label: "设为手动" }
797    ]) {
798      const button = document.createElement("button");
799      button.type = "button";
800      button.className = "baa-btn";
801      button.dataset.role = "page";
802      button.dataset.state = "auto";
803      button.textContent = entry.label;
804      button.addEventListener("click", () => {
805        runPageControlAction(entry.action).catch(() => {});
806      });
807      pageButtonMap[entry.action] = button;
808      pageActions.appendChild(button);
809    }
810    panel.appendChild(pageActions);
811
812    errorText = document.createElement("div");
813    errorText.className = "baa-error";
814    panel.appendChild(errorText);
815
816    const actions = document.createElement("div");
817    actions.className = "baa-actions";
818
819    for (const entry of [
820      { action: "pause", label: "系统暂停" },
821      { action: "resume", label: "系统恢复" },
822      { action: "drain", label: "系统排空" }
823    ]) {
824      const button = document.createElement("button");
825      button.type = "button";
826      button.className = "baa-btn";
827      button.dataset.action = entry.action;
828      button.textContent = entry.label;
829      button.addEventListener("click", () => {
830        runControlAction(entry.action).catch(() => {});
831      });
832      buttonMap[entry.action] = button;
833      actions.appendChild(button);
834    }
835
836    panel.appendChild(actions);
837    shadow.appendChild(panel);
838
839    const parent = document.body || document.documentElement;
840    parent?.appendChild(root);
841    render();
842  }
843
844  if (document.readyState === "loading") {
845    document.addEventListener("DOMContentLoaded", mount, { once: true });
846  }
847
848  mount();
849  loadRuntimeOverlayState().catch(() => {});
850  readStoredOverlayState().catch(() => {});
851  browser.storage.onChanged.addListener(handleStorageChanged);
852
853  return {
854    dispose() {
855      destroyed = true;
856      browser.storage.onChanged.removeListener(handleStorageChanged);
857
858      if (root?.isConnected) {
859        root.remove();
860      }
861
862      root = null;
863      shadow = null;
864      dock = null;
865      panel = null;
866      statusText = null;
867      errorText = null;
868      dismissButton = null;
869      badgeButton = null;
870      compactActionButton = null;
871      buttonMap = {};
872      pageButtonMap = {};
873    }
874  };
875}
876
877async function handleDeliveryCommand(data = {}) {
878  if (!deliveryRuntime) {
879    return {
880      ok: false,
881      code: "runtime_unavailable",
882      command: trimToNull(data?.command),
883      details: {
884        url: location.href
885      },
886      platform: trimToNull(data?.platform),
887      reason: "delivery.runtime_unavailable: delivery adapter runtime is not loaded"
888    };
889  }
890
891  return await deliveryRuntime.handleCommand(data);
892}
893
894function handlePageReady(event) {
895  try { console.log("[BAA-CS]", "page_ready", event.detail?.platform, event.detail?.source); } catch (_) {}
896  sendBridgeMessage("baa_page_bridge_ready", {
897    ...(event.detail || {}),
898    url: event.detail?.url || location.href,
899    source: event.detail?.source || "page-interceptor"
900  });
901}
902
903function handlePageNetwork(event) {
904  try { console.log("[BAA-CS]", "network", event.detail?.platform, event.detail?.method, event.detail?.url?.slice(0, 120)); } catch (_) {}
905  sendBridgeMessage("baa_page_network", event.detail);
906}
907
908function handlePageSse(event) {
909  try {
910    if (event.detail?.done || event.detail?.error || event.detail?.open) {
911      console.log("[BAA-CS]", "sse", event.detail?.done ? "done" : event.detail?.error ? "error" : "open", event.detail?.platform, event.detail?.url?.slice(0, 120));
912    }
913  } catch (_) {}
914  sendBridgeMessage("baa_page_sse", event.detail);
915}
916
917function handleProxyResponse(event) {
918  try { console.log("[BAA-CS]", "proxy_response", event.detail?.id, event.detail?.status); } catch (_) {}
919  sendBridgeMessage("baa_page_proxy_response", event.detail);
920}
921
922function handleDiagnosticEvent(event) {
923  const detail = event?.detail && typeof event.detail === "object" ? event.detail : {};
924  const eventName = trimToNull(detail.event);
925  if (!eventName) {
926    return;
927  }
928
929  try { console.log("[BAA-CS]", "diagnostic", eventName, detail.platform, detail.method, detail.url?.slice(0, 120)); } catch (_) {}
930  sendDiagnosticLog(eventName, detail);
931}
932
933function handleRuntimeMessage(message) {
934  if (!message || typeof message !== "object") return undefined;
935
936  if (message.type === "baa_page_proxy_request") {
937    try { console.log("[BAA-CS]", "proxy_request_dispatch", message.data?.id, message.data?.method, message.data?.path?.slice(0, 120)); } catch (_) {}
938    window.dispatchEvent(new CustomEvent("__baa_proxy_request__", {
939      detail: JSON.stringify(message.data || {})
940    }));
941
942    return undefined;
943  }
944
945  if (message.type === "baa_page_proxy_cancel") {
946    window.dispatchEvent(new CustomEvent("__baa_proxy_cancel__", {
947      detail: JSON.stringify(message.data || {})
948    }));
949    return undefined;
950  }
951
952  if (message.type === "baa_delivery_command") {
953    return handleDeliveryCommand(message.data || {});
954  }
955
956  return undefined;
957}
958
959window.addEventListener("__baa_ready__", handlePageReady);
960window.addEventListener("__baa_net__", handlePageNetwork);
961window.addEventListener("__baa_sse__", handlePageSse);
962window.addEventListener("__baa_diagnostic__", handleDiagnosticEvent);
963window.addEventListener("__baa_proxy_response__", handleProxyResponse);
964browser.runtime.onMessage.addListener(handleRuntimeMessage);
965
966const pageControlOverlayRuntime = createPageControlOverlayRuntime();
967
968try { console.log("[BAA-CS]", "content_script_loaded", location.href.slice(0, 120)); } catch (_) {}
969sendBridgeMessage("baa_page_bridge_ready", {
970  url: location.href,
971  source: "content-script"
972});
973sendDiagnosticLog("page_bridge_ready", {
974  source: "content-script",
975  url: location.href
976});
977
978const contentScriptRuntime = {
979  dispose() {
980    window.removeEventListener("__baa_ready__", handlePageReady);
981    window.removeEventListener("__baa_net__", handlePageNetwork);
982    window.removeEventListener("__baa_sse__", handlePageSse);
983    window.removeEventListener("__baa_diagnostic__", handleDiagnosticEvent);
984    window.removeEventListener("__baa_proxy_response__", handleProxyResponse);
985    browser.runtime.onMessage.removeListener(handleRuntimeMessage);
986    pageControlOverlayRuntime?.dispose?.();
987
988    if (window[CONTENT_SCRIPT_RUNTIME_KEY] === contentScriptRuntime) {
989      try {
990        delete window[CONTENT_SCRIPT_RUNTIME_KEY];
991      } catch (_) {
992        window[CONTENT_SCRIPT_RUNTIME_KEY] = null;
993      }
994    }
995  }
996};
997
998window[CONTENT_SCRIPT_RUNTIME_KEY] = contentScriptRuntime;