src/devtools/panel.ts

Total Symbols
5
Lines of Code
186
Avg Complexity
2.6
Symbol Types
1

File Relationships

graph LR renderApp["renderApp"] renderShell["renderShell"] renderActiveTab["renderActiveTab"] switchTab["switchTab"] init["init"] renderApp -->|calls| renderShell renderApp -->|calls| renderActiveTab switchTab -->|calls| renderShell switchTab -->|calls| renderActiveTab renderShell -->|calls| renderActiveTab init -->|calls| renderApp click renderApp "../symbols/b94036094eefb024.html" click renderShell "../symbols/49d8574256a889e1.html" click renderActiveTab "../symbols/6d5bc0097c5c25ad.html" click switchTab "../symbols/b10a2a40730510e7.html" click init "../symbols/13c941f8edb316e8.html"

Symbols by Kind

function 5

All Symbols

Name Kind Visibility Status Lines Signature
renderShell function - 37-50 renderShell(): : void
renderApp function - 52-55 renderApp(): : void
switchTab function - 57-61 switchTab(tab: TabId): : void
renderActiveTab function - 63-86 renderActiveTab(): : void
init function - 165-183 init(): : Promise<void>

Full Source

/**
 * DevTools Panel — Coordinator
 *
 * Orchestrates tab rendering, UI lifecycle, and chrome event listeners.
 * All implementation details live in dedicated modules:
 *  - panel-state.ts   : Shared mutable state
 *  - panel-messaging.ts: Chrome runtime + inspected page communication
 *  - panel-utils.ts   : Pure helper functions (addLog, updateStatusBar, …)
 *  - tabs/actions-tab.tsx, fields-tab.tsx, forms-tab.tsx, record-tab.tsx, log-tab.tsx
 */

import { h } from "preact";
import "../lib/ui/searchable-select.css";
import "../lib/ui/components/field-editor-modal.css";
import { initI18n } from "@/lib/i18n";
import { getLogViewerStyles } from "@/lib/logger/log-viewer";
import type { TabId, RecordStep } from "./panel-state";
import { panelState } from "./panel-state";
import { sendToPage } from "./panel-messaging";
import { addLog, updateStatusBar } from "./panel-utils";
import { renderTo, AppShell } from "@/lib/ui/components";
import { renderActionsTab } from "./tabs/actions-tab";
import { renderFieldsTab } from "./tabs/fields-tab";
import { renderFormsTab, loadForms } from "./tabs/forms-tab";
import { renderRecordTab, renderRecordStepsTable } from "./tabs/record-tab";
import { renderLogTab } from "./tabs/log-tab";
import {
  renderDemoTab,
  loadDemoFlows,
  applyReplayProgress,
  applyReplayComplete,
} from "./tabs/demo-tab";
import type { ReplayProgress } from "@/lib/demo";

// ── App Shell ─────────────────────────────────────────────────────────────────

function renderShell(): void {
  const app = document.getElementById("app");
  renderTo(
    app,
    h(AppShell, {
      activeTab: panelState.activeTab,
      onTabSwitch: switchTab,
      onOptions: () => {
        chrome.runtime.openOptionsPage();
        addLog("Opening options…", "info");
      },
    }),
  );
}

function renderApp(): void {
  renderShell();
  renderActiveTab();
}

function switchTab(tab: TabId): void {
  panelState.activeTab = tab;
  renderShell(); // re-render toolbar to update active-tab class
  renderActiveTab(); // re-render content area
}

function renderActiveTab(): void {
  switch (panelState.activeTab) {
    case "actions":
      renderActionsTab();
      break;
    case "fields":
      renderFieldsTab();
      break;
    case "forms":
      renderFormsTab();
      void loadForms();
      break;
    case "record":
      renderRecordTab();
      break;
    case "demo":
      renderDemoTab();
      void loadDemoFlows();
      break;
    case "log":
      renderLogTab();
      break;
  }
}

// ── Recording Listener ────────────────────────────────────────────────────────

chrome.runtime.onMessage.addListener(
  (message: { type?: string; payload?: Record<string, unknown> }, sender) => {
    if (sender.tab?.id !== panelState.inspectedTabId) return;

    if (message.type === "RECORDING_RESTORED") {
      const p = message.payload as { steps?: RecordStep[] } | undefined;
      if (Array.isArray(p?.steps)) {
        panelState.recordedStepsPreview = p.steps;
        renderRecordStepsTable();
      }
    }

    if (message.type === "RECORDING_STEP_ADDED") {
      const p = message.payload as { step?: RecordStep } | undefined;
      if (p?.step) {
        panelState.recordedStepsPreview.push(p.step);
        renderRecordStepsTable();
      }
    }

    if (message.type === "RECORDING_STEP_UPDATED") {
      const p = message.payload as
        | { step?: RecordStep; index?: number }
        | undefined;
      if (
        p?.step &&
        typeof p.index === "number" &&
        panelState.recordedStepsPreview[p.index]
      ) {
        panelState.recordedStepsPreview[p.index] = p.step;
        renderRecordStepsTable();
      }
    }

    if (message.type === "DEMO_REPLAY_PROGRESS") {
      applyReplayProgress(message.payload as unknown as ReplayProgress);
    }

    if (message.type === "DEMO_REPLAY_COMPLETE") {
      const p = message.payload as
        | { status?: "completed" | "failed" }
        | undefined;
      applyReplayComplete(p?.status ?? "completed");
    }
  },
);

// ── Navigation Listener ───────────────────────────────────────────────────────

chrome.devtools.network.onNavigated.addListener(() => {
  panelState.detectedFields = [];
  panelState.watcherActive = false;
  // When recording is active, preserve recorded steps — RECORDING_RESTORED will
  // arrive from the content script with the full list (including the new navigate
  // step and any network assert). When stopped, clear so the panel is fresh.
  if (
    panelState.recordingState === "stopped" ||
    panelState.recordingState === "idle"
  ) {
    panelState.recordedStepsPreview = [];
  }
  if (
    panelState.recordingState !== "recording" &&
    panelState.recordingState !== "paused" &&
    panelState.recordingState !== "stopped"
  ) {
    panelState.recordingState = "idle";
  }
  panelState.ignoredSelectors.clear();
  renderActiveTab();
  updateStatusBar();
});

// ── Init ──────────────────────────────────────────────────────────────────────

async function init(): Promise<void> {
  const settings = (await chrome.runtime.sendMessage({
    type: "GET_SETTINGS",
  })) as { uiLanguage?: "auto" | "en" | "pt_BR" } | null;
  await initI18n(settings?.uiLanguage ?? "auto");

  // Sync watcher state from content script
  const watcherStatus = (await sendToPage({ type: "GET_WATCHER_STATUS" }).catch(
    () => null,
  )) as { watching: boolean } | null;
  panelState.watcherActive = watcherStatus?.watching ?? false;

  // Inject log viewer styles
  const lvStyle = document.createElement("style");
  lvStyle.textContent = getLogViewerStyles("devtools");
  document.head.appendChild(lvStyle);

  renderApp();
}

void init();