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;