src/content/content-script.ts

Total Symbols
6
Lines of Code
896
Avg Complexity
19.8
Symbol Types
2

File Relationships

graph LR FillableElement["FillableElement"] handleContentMessage["handleContentMessage"] showNotification["showNotification"] findSingleFieldTarget["findSingleFieldTarget"] initContentScript["initContentScript"] FillableElement -->|calls| handleContentMessage handleContentMessage -->|calls| showNotification handleContentMessage -->|calls| findSingleFieldTarget initContentScript -->|calls| showNotification FillableElement -->|calls| initContentScript click FillableElement "../symbols/2ecf5aaac3f668a8.html" click handleContentMessage "../symbols/25741f17eec7ff33.html" click showNotification "../symbols/8c9f4b84bc123ccf.html" click findSingleFieldTarget "../symbols/35389c7a7a8aca94.html" click initContentScript "../symbols/9e3cc66103159cb7.html"

Architecture violations

View all

  • [warning] max-cyclomatic-complexity: 'handleContentMessage' has cyclomatic complexity 99 (max 10)
  • [warning] max-lines: 'handleContentMessage' has 503 lines (max 80)
  • [warning] max-lines: 'initContentScript' has 105 lines (max 80)

Symbols by Kind

function 5
type 1

All Symbols

Name Kind Visibility Status Lines Signature
FillableElement type - 94-97 type FillableElement
generateId function - 121-123 generateId(): : string
handleContentMessage function exported- 145-647 handleContentMessage( message: ExtensionMessage, ): : Promise<unknown>
findSingleFieldTarget function - 649-672 findSingleFieldTarget(fields: FormField[]): : FormField | undefined
showNotification function - 674-702 showNotification(text: string): : void
initContentScript function - 705-809 initContentScript(): : Promise<void>

Full Source

/**
 * Content script — runs on every page, listens for fill commands
 */

import type {
  AIContextPayload,
  DetectedFieldSummary,
  ExtensionMessage,
  FormField,
  SavedForm,
  StreamedFieldMessage,
} from "@/types";
import {
  fillAllFields,
  fillSingleField,
  captureFormValues,
  applyTemplate,
  fillContextualAI,
} from "@/lib/form/form-filler";
import {
  detectAllFieldsAsync,
  detectFormFields,
  streamAllFields,
  reclassifyFieldBySelector,
} from "@/lib/form/form-detector";
import {
  saveForm,
  getSettings,
  getIgnoredFieldsForUrl,
} from "@/lib/storage/storage";
import { initI18n } from "@/lib/i18n";
import {
  buildCapturedActions,
  detectSubmitActions,
  generateE2EScript,
  generateE2EFromRecording,
  detectAssertions,
  detectNegativeAssertions,
  startRecording,
  stopRecording,
  pauseRecording,
  resumeRecording,
  getRecordingStatus,
  getRecordingSession,
  setOnStepAdded,
  setOnStepUpdated,
  removeStep,
  updateStep,
  clearSession,
  tryRestoreRecordingSession,
} from "@/lib/e2e-export";
import type {
  E2EFramework,
  E2EGenerateOptions,
  RecordingGenerateOptions,
} from "@/lib/e2e-export";
import {
  startWatching,
  stopWatching,
  isWatcherActive,
  getWatcherConfig,
} from "@/lib/form/dom-watcher";
import type { WatcherConfig } from "@/lib/form/dom-watcher";
import { initFieldIcon } from "@/lib/form/field-icon";
import {
  loadPretrainedModel,
  invalidateClassifier,
  reloadClassifier,
} from "@/lib/form/detectors/strategies";
import {
  setActiveClassifiers,
  buildClassifiersFromSettings,
} from "@/lib/form/detectors/classifiers";
import {
  parseIncomingMessage,
  parseSavedFormPayload,
  parseStartWatchingPayload,
  parseStringPayload,
  parseExportE2EPayload,
  parseExportRecordingPayload,
} from "@/lib/messaging/light-validators";
import { initLogger } from "@/lib/logger";
import { executeStep, highlightElement } from "@/lib/demo/step-executor";
import {
  initCursorOverlay,
  destroyCursorOverlay,
  showCursor,
  hideCursor,
  moveCursorTo,
  clickEffect,
} from "@/lib/demo/cursor-overlay";
import type { ExecuteStepPayload } from "@/lib/demo/demo.types";

type FillableElement =
  | HTMLInputElement
  | HTMLSelectElement
  | HTMLTextAreaElement;

let lastContextMenuElement: FillableElement | null = null;

// guard against test environment without DOM
if (typeof document !== "undefined") {
  document.addEventListener(
    "contextmenu",
    (event) => {
      const target = event.target;
      if (!(target instanceof Element)) return;
      const field = target.closest("input, select, textarea");
      if (
        field instanceof HTMLInputElement ||
        field instanceof HTMLSelectElement ||
        field instanceof HTMLTextAreaElement
      ) {
        lastContextMenuElement = field;
      }
    },
    true,
  );
}

function generateId(): string {
  return crypto.randomUUID();
}

// Listen for messages from background / popup
chrome.runtime.onMessage.addListener(
  (
    message: unknown,
    _sender: chrome.runtime.MessageSender,
    sendResponse: (response: unknown) => void,
  ) => {
    const parsed = parseIncomingMessage(message);
    if (!parsed) {
      sendResponse({ error: "Invalid message format" });
      return false;
    }

    handleContentMessage(parsed as ExtensionMessage)
      .then(sendResponse)
      .catch((err) => sendResponse({ error: (err as Error).message }));
    return true;
  },
);

export async function handleContentMessage(
  message: ExtensionMessage,
): Promise<unknown> {
  switch (message.type) {
    case "FILL_ALL_FIELDS": {
      const override =
        message.payload != null &&
        typeof message.payload === "object" &&
        "fillEmptyOnly" in (message.payload as object)
          ? {
              fillEmptyOnly: (message.payload as { fillEmptyOnly: boolean })
                .fillEmptyOnly,
            }
          : undefined;
      const results = await fillAllFields(override);
      showNotification(`✓ ${results.length} campos preenchidos`);
      return { success: true, filled: results.length };
    }

    case "FILL_CONTEXTUAL_AI": {
      const context = message.payload as AIContextPayload | undefined;
      const results = await fillContextualAI(context);
      showNotification(
        `✓ ${results.length} campos preenchidos com IA contextual`,
      );
      return { success: true, filled: results.length };
    }

    case "SAVE_FORM": {
      const values = await captureFormValues();
      const formData: SavedForm = {
        id: generateId(),
        name: `Form - ${new URL(window.location.href).hostname} - ${new Date().toLocaleDateString("pt-BR")}`,
        urlPattern: `${window.location.origin}${window.location.pathname}*`,
        fields: values,
        createdAt: Date.now(),
        updatedAt: Date.now(),
      };
      await saveForm(formData);
      showNotification(
        `✓ Formulário salvo com ${Object.keys(values).length} campos`,
      );
      return { success: true, form: formData };
    }

    case "LOAD_SAVED_FORM": {
      const form = parseSavedFormPayload(message.payload);
      if (!form) return { error: "Invalid payload for LOAD_SAVED_FORM" };

      // filter out ignored selectors so applyTemplate won't touch them
      const ignoredFields = await getIgnoredFieldsForUrl(window.location.href);
      const ignoredSelectors = new Set(ignoredFields.map((f) => f.selector));
      const sanitizedFields: typeof form.fields = {};
      for (const [key, value] of Object.entries(form.fields)) {
        // we only know selector-level ignores; if a key equals a selector, drop it
        if (ignoredSelectors.has(key)) continue;
        sanitizedFields[key] = value;
      }
      const sanitizedForm: SavedForm = { ...form, fields: sanitizedFields };

      const { filled } = await applyTemplate(sanitizedForm);
      showNotification(`✓ ${filled} campos carregados do template`);
      return { success: true, filled };
    }

    case "APPLY_TEMPLATE": {
      const form = parseSavedFormPayload(message.payload);
      if (!form) return { error: "Invalid payload for APPLY_TEMPLATE" };
      const { filled } = await applyTemplate(form);
      showNotification(`✓ Template "${form.name}" aplicado: ${filled} campos`);
      return { success: true, filled };
    }

    case "FILL_SINGLE_FIELD": {
      const fields = detectFormFields();
      const targetField = findSingleFieldTarget(fields);
      if (!targetField) return { error: "No target field found" };

      const result = await fillSingleField(targetField);
      if (result) {
        showNotification(
          `✓ Campo "${targetField.label || targetField.name || targetField.id || targetField.selector}" preenchido`,
        );
        return { success: true, ...result };
      }
      return { error: "Failed to fill field" };
    }

    case "GET_FORM_FIELDS": {
      const fields = detectFormFields();
      const mapped = fields.map((f) => ({
        selector: f.selector,
        fieldType: f.fieldType,
        label: f.label,
        name: f.name,
        id: f.id,
        placeholder: f.placeholder,
        required: f.required,
      }));
      return { count: mapped.length, fields: mapped };
    }

    case "DETECT_FIELDS": {
      const { fields: detected } = await detectAllFieldsAsync();
      return {
        count: detected.length,
        fields: detected.map((f): DetectedFieldSummary => {
          const item: DetectedFieldSummary = {
            selector: f.selector,
            fieldType: f.fieldType,
            label: f.label || f.name || f.id || "unknown",
            name: f.name,
            id: f.id,
            placeholder: f.placeholder,
            required: f.required,
            contextualType: f.contextualType,
            detectionMethod: f.detectionMethod,
            detectionConfidence: f.detectionConfidence,
          };
          if (f.element instanceof HTMLSelectElement) {
            item.options = Array.from(f.element.options).map((o) => ({
              value: o.value,
              text: o.text.trim(),
            }));
          }
          if (
            f.element instanceof HTMLInputElement &&
            (f.element.type === "checkbox" || f.element.type === "radio")
          ) {
            item.checkboxValue = f.element.value;
            item.checkboxChecked = f.element.checked;
          }
          return item;
        }),
      };
    }

    case "FILL_FIELD_BY_SELECTOR": {
      const selector = parseStringPayload(message.payload);
      if (!selector)
        return { error: "Invalid payload for FILL_FIELD_BY_SELECTOR" };
      const fields = detectFormFields();
      const field = fields.find((f) => f.selector === selector);
      if (!field) return { error: "Field not found" };
      const result = await fillSingleField(field);
      if (result) {
        showNotification(`✓ Campo "${field.label || selector}" preenchido`);
      }
      return result ?? { error: "Failed to fill field" };
    }

    case "RECLASSIFY_FIELD": {
      const selector = parseStringPayload(message.payload);
      if (!selector) return { error: "Invalid payload for RECLASSIFY_FIELD" };
      const classified = await reclassifyFieldBySelector(selector);
      if (!classified)
        return { error: "Field not found or could not be classified" };
      const summary: DetectedFieldSummary = {
        selector: classified.selector,
        fieldType: classified.fieldType,
        label:
          classified.label || classified.name || classified.id || "unknown",
        name: classified.name,
        id: classified.id,
        placeholder: classified.placeholder,
        required: classified.required,
        contextualType: classified.contextualType,
        detectionMethod: classified.detectionMethod,
        detectionConfidence: classified.detectionConfidence,
      };
      if (classified.element instanceof HTMLSelectElement) {
        summary.options = Array.from(classified.element.options).map((o) => ({
          value: o.value,
          text: o.text.trim(),
        }));
      }
      return summary;
    }

    case "START_WATCHING": {
      const payload = parseStartWatchingPayload(message.payload);
      if (!payload) return { error: "Invalid payload for START_WATCHING" };

      // If debounceMs/shadowDOM not provided in payload, read from settings
      let config: WatcherConfig;
      if (payload.debounceMs == null && payload.shadowDOM == null) {
        const settings = await getSettings();
        config = {
          autoRefill: payload.autoRefill ?? settings.watcherAutoRefill ?? true,
          debounceMs: settings.watcherDebounceMs,
          shadowDOM: settings.watcherShadowDOM,
        };
      } else {
        config = {
          autoRefill: payload.autoRefill ?? true,
          debounceMs: payload.debounceMs,
          shadowDOM: payload.shadowDOM,
        };
      }

      startWatching(
        (newFieldsCount) => {
          if (newFieldsCount > 0) {
            showNotification(
              `🔄 ${newFieldsCount} novo(s) campo(s) detectado(s) — re-preenchendo...`,
            );
          }
        },
        config.autoRefill,
        config,
      );
      return { success: true, watching: true };
    }

    case "STOP_WATCHING": {
      stopWatching();
      return { success: true, watching: false };
    }

    case "GET_WATCHER_STATUS": {
      return {
        watching: isWatcherActive(),
        config: isWatcherActive() ? getWatcherConfig() : null,
      };
    }

    case "INVALIDATE_CLASSIFIER": {
      invalidateClassifier();
      return { success: true };
    }

    case "RELOAD_CLASSIFIER": {
      void reloadClassifier();
      return { success: true };
    }

    case "EXPORT_E2E": {
      const parsed = parseExportE2EPayload(message.payload);
      if (!parsed) return { error: "Invalid payload for EXPORT_E2E" };

      const framework = parsed.framework as E2EFramework;
      const { fields } = await detectAllFieldsAsync();
      const results = await fillAllFields();
      const actions = buildCapturedActions(fields, results);

      // Detect submit buttons and append to actions
      const submitActions = detectSubmitActions();
      const allActions = [...actions, ...submitActions];

      // Detect assertions from the page
      const pageUrl = window.location.href;
      const assertions = detectAssertions(allActions, pageUrl);
      const negativeAssertions = detectNegativeAssertions(allActions);

      const options: E2EGenerateOptions = {
        pageUrl,
        includeAssertions: true,
        includeNegativeTest: true,
        useSmartSelectors: true,
        assertions: [...assertions, ...negativeAssertions],
      };

      const script = generateE2EScript(framework, allActions, options);

      if (!script) return { error: `Unsupported framework: ${framework}` };

      showNotification(
        `✓ Script ${framework} gerado (${allActions.length} ações)`,
      );
      return { success: true, script, actionsCount: allActions.length };
    }

    case "START_RECORDING": {
      startRecording();

      setOnStepAdded((step, index) => {
        chrome.runtime
          .sendMessage({
            type: "RECORDING_STEP_ADDED",
            payload: {
              step: {
                type: step.type,
                selector: step.selector,
                value: step.value,
                url: step.url,
                label: step.label,
                assertion: step.assertion,
              },
              index,
            },
          })
          .catch(() => {});
      });

      setOnStepUpdated((step, index) => {
        chrome.runtime
          .sendMessage({
            type: "RECORDING_STEP_UPDATED",
            payload: {
              step: {
                type: step.type,
                selector: step.selector,
                value: step.value,
                url: step.url,
                label: step.label,
                assertion: step.assertion,
              },
              index,
            },
          })
          .catch(() => {});
      });

      showNotification("🔴 Gravação iniciada");
      return { success: true };
    }

    case "STOP_RECORDING": {
      const session = stopRecording();
      if (!session) return { error: "No recording in progress" };
      showNotification(
        `⏹ Gravação finalizada (${session.steps.length} passos)`,
      );
      return { success: true, stepsCount: session.steps.length };
    }

    case "PAUSE_RECORDING": {
      pauseRecording();
      showNotification("⏸ Gravação pausada");
      return { success: true };
    }

    case "RESUME_RECORDING": {
      resumeRecording();
      showNotification("🔴 Gravação retomada");
      return { success: true };
    }

    case "GET_RECORDING_STATUS": {
      return { success: true, status: getRecordingStatus() };
    }

    case "GET_RECORDING_STEPS": {
      const session = getRecordingSession();
      if (!session) return { success: true, steps: [] };
      return {
        success: true,
        steps: session.steps.map((s, i, arr) => ({
          type: s.type,
          selector: s.selector,
          value: s.value,
          waitMs: i > 0 ? s.timestamp - arr[i - 1].timestamp : 0,
          url: s.url,
        })),
      };
    }

    case "EXPORT_RECORDING": {
      const recPayload = parseExportRecordingPayload(message.payload);
      if (!recPayload) return { error: "Invalid payload for EXPORT_RECORDING" };

      const session = getRecordingSession();
      if (!session || session.steps.length === 0) {
        return { error: "No recorded steps available" };
      }

      const recOptions: RecordingGenerateOptions = {
        testName: recPayload.testName,
        pageUrl: session.startUrl,
        includeAssertions: true,
        minWaitThreshold: 500,
      };

      const recScript = generateE2EFromRecording(
        recPayload.framework as E2EFramework,
        session.steps,
        recOptions,
      );

      if (!recScript) {
        return { error: `Unsupported framework: ${recPayload.framework}` };
      }

      showNotification(
        `✓ Script ${recPayload.framework} gerado (${session.steps.length} passos gravados)`,
      );
      return {
        success: true,
        script: recScript,
        stepsCount: session.steps.length,
      };
    }

    case "REMOVE_RECORDING_STEP": {
      const payload = message.payload as { index?: number } | undefined;
      if (typeof payload?.index !== "number") {
        return { error: "Invalid index" };
      }
      const removed = removeStep(payload.index);
      return { success: removed };
    }

    case "UPDATE_RECORDING_STEP": {
      const payload = message.payload as
        | { index?: number; patch?: { value?: string; waitTimeout?: number } }
        | undefined;
      if (typeof payload?.index !== "number" || !payload.patch) {
        return { error: "Invalid payload" };
      }
      const updated = updateStep(payload.index, payload.patch);
      return { success: updated };
    }

    case "CLEAR_RECORDING": {
      clearSession();
      return { success: true };
    }

    case "CLEAR_FORM": {
      const fields = detectFormFields();
      let cleared = 0;

      for (const field of fields) {
        const el = field.element;
        if (el instanceof HTMLInputElement) {
          if (el.type === "checkbox" || el.type === "radio") {
            el.checked = false;
          } else {
            el.value = "";
          }
          cleared++;
          el.dispatchEvent(new Event("input", { bubbles: true }));
          el.dispatchEvent(new Event("change", { bubbles: true }));
        } else if (el instanceof HTMLSelectElement) {
          el.value = "";
          cleared++;
          el.dispatchEvent(new Event("change", { bubbles: true }));
        } else if (el instanceof HTMLTextAreaElement) {
          el.value = "";
          cleared++;
          el.dispatchEvent(new Event("input", { bubbles: true }));
          el.dispatchEvent(new Event("change", { bubbles: true }));
        }
      }

      showNotification(`✓ ${cleared} campo(s) limpo(s)`);
      return { success: true, cleared };
    }

    case "DEMO_EXECUTE_STEP": {
      const payload = message.payload as ExecuteStepPayload | undefined;
      if (!payload?.step) {
        return { error: "Invalid payload for DEMO_EXECUTE_STEP" };
      }
      const result = await executeStep(payload);
      return { result };
    }

    case "DEMO_CURSOR_MOVE": {
      const cfg = message.payload as
        | { selector?: string; durationMs?: number }
        | undefined;
      if (cfg?.selector) {
        initCursorOverlay();
        showCursor();
        await moveCursorTo(cfg.selector, cfg.durationMs ?? 400);
      }
      return { success: true };
    }

    case "DEMO_CURSOR_CLICK": {
      initCursorOverlay();
      showCursor();
      await clickEffect();
      return { success: true };
    }

    case "DEMO_CURSOR_DESTROY": {
      hideCursor();
      destroyCursorOverlay();
      return { success: true };
    }

    case "DEMO_HIGHLIGHT_ELEMENT": {
      const hlPayload = message.payload as
        | { step?: unknown; durationMs?: number }
        | undefined;
      if (hlPayload?.step) {
        highlightElement(
          hlPayload.step as Parameters<typeof highlightElement>[0],
          hlPayload.durationMs ?? 300,
        );
      }
      return { success: true };
    }

    case "PING":
      return { pong: true };

    default:
      return { error: `Unknown message type: ${message.type}` };
  }
}

function findSingleFieldTarget(fields: FormField[]): FormField | undefined {
  if (lastContextMenuElement) {
    const byContextMenu = fields.find(
      (f) => f.element === lastContextMenuElement,
    );
    if (byContextMenu) return byContextMenu;
  }

  if (document.activeElement instanceof HTMLElement) {
    const activeField = document.activeElement.closest(
      "input, select, textarea",
    );
    if (
      activeField instanceof HTMLInputElement ||
      activeField instanceof HTMLSelectElement ||
      activeField instanceof HTMLTextAreaElement
    ) {
      const byFocus = fields.find((f) => f.element === activeField);
      if (byFocus) return byFocus;
    }
  }

  return fields.find((f) => !(f.element as HTMLInputElement).disabled);
}

function showNotification(text: string): void {
  const existing = document.getElementById("fill-all-notification");
  if (existing) existing.remove();

  const el = document.createElement("div");
  el.id = "fill-all-notification";
  el.textContent = text;
  Object.assign(el.style, {
    position: "fixed",
    bottom: "20px",
    right: "20px",
    padding: "12px 20px",
    background: "#4F46E5",
    color: "#fff",
    borderRadius: "8px",
    fontSize: "14px",
    fontFamily: "system-ui, sans-serif",
    zIndex: "999999",
    boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
    transition: "opacity 0.3s ease",
  });

  document.body.appendChild(el);

  setTimeout(() => {
    el.style.opacity = "0";
    setTimeout(() => el.remove(), 300);
  }, 3000);
}

// --- Init ---
async function initContentScript(): Promise<void> {
  await initLogger();
  const settings = await getSettings();
  await initI18n(settings.uiLanguage ?? "auto");

  // Configure the detection pipeline from user settings
  if (settings.detectionPipeline?.length) {
    setActiveClassifiers(
      buildClassifiersFromSettings(settings.detectionPipeline),
    );
  }

  // Init the per-field icon only if enabled
  if (settings.showFieldIcon !== false) {
    initFieldIcon(settings.fieldIconPosition ?? "inside");
  }

  // Load pre-trained model artefacts (generated by `npm run train:model`).
  // Falls back silently to runtime keyword classifier if artefacts are absent.
  loadPretrainedModel().catch(() => {});

  // Auto-start watcher if enabled in settings
  if (settings.watcherEnabled) {
    startWatching(
      (newFieldsCount) => {
        if (newFieldsCount > 0) {
          showNotification(
            `🔄 ${newFieldsCount} novo(s) campo(s) detectado(s) — re-preenchendo...`,
          );
        }
      },
      settings.watcherAutoRefill,
      {
        autoRefill: settings.watcherAutoRefill,
        debounceMs: settings.watcherDebounceMs,
        shadowDOM: settings.watcherShadowDOM,
      },
    );
  }

  // Restore a recording session that was persisted before a traditional form submit
  // (non-AJAX GET/POST) caused a full page navigation.
  const restoredSession = tryRestoreRecordingSession();
  if (restoredSession) {
    // Notify the devtools panel about the restored session (with all steps captured
    // before and during the form submit) so it can repopulate the action list.
    chrome.runtime
      .sendMessage({
        type: "RECORDING_RESTORED",
        payload: {
          steps: restoredSession.steps.map((step) => ({
            type: step.type,
            selector: step.selector,
            value: step.value,
            url: step.url,
            label: step.label,
            assertion: step.assertion,
          })),
        },
      })
      .catch(() => {});

    setOnStepAdded((step, index) => {
      chrome.runtime
        .sendMessage({
          type: "RECORDING_STEP_ADDED",
          payload: {
            step: {
              type: step.type,
              selector: step.selector,
              value: step.value,
              url: step.url,
              label: step.label,
              assertion: step.assertion,
            },
            index,
          },
        })
        .catch(() => {});
    });

    setOnStepUpdated((step, index) => {
      chrome.runtime
        .sendMessage({
          type: "RECORDING_STEP_UPDATED",
          payload: {
            step: {
              type: step.type,
              selector: step.selector,
              value: step.value,
              url: step.url,
              label: step.label,
              assertion: step.assertion,
            },
            index,
          },
        })
        .catch(() => {});
    });

    showNotification(
      `🔴 Gravação retomada (${restoredSession.steps.length} passos)`,
    );
  }
}

// Only automatically initialize when running in a real browser context.
// In unit tests we import this module directly, so avoid side effects.
if (typeof document !== "undefined" && typeof chrome !== "undefined") {
  void initContentScript();
}

/**
 * Handle streaming detection via Port-based communication.
 * DevTools connects with port name "field-detection-stream" to receive
 * fields incrementally as they are detected (instead of waiting for all).
 */
chrome.runtime.onConnect.addListener((port) => {
  if (port.name !== "field-detection-stream") return;

  let cancelled = false;

  // Clean up on disconnect
  port.onDisconnect.addListener(() => {
    cancelled = true;
  });

  // Start streaming fields
  (async () => {
    try {
      let index = 0;

      for await (const field of streamAllFields()) {
        if (cancelled) break;

        const summary: DetectedFieldSummary = {
          selector: field.selector,
          fieldType: field.fieldType,
          label: field.label || field.name || field.id || "unknown",
          name: field.name,
          id: field.id,
          placeholder: field.placeholder,
          required: field.required,
          contextualType: field.contextualType,
          detectionMethod: field.detectionMethod,
          detectionConfidence: field.detectionConfidence,
        };

        if (field.element instanceof HTMLSelectElement) {
          summary.options = Array.from(field.element.options).map((o) => ({
            value: o.value,
            text: o.text.trim(),
          }));
        }

        if (
          field.element instanceof HTMLInputElement &&
          (field.element.type === "checkbox" || field.element.type === "radio")
        ) {
          summary.checkboxValue = field.element.value;
          summary.checkboxChecked = field.element.checked;
        }

        index++;

        const message: StreamedFieldMessage = {
          type: "field",
          field: summary,
          current: index,
        };

        port.postMessage(message);
      }

      if (!cancelled) {
        port.postMessage({
          type: "complete",
          total: index,
          current: index,
        } as StreamedFieldMessage);
      }
    } catch (error) {
      if (!cancelled) {
        port.postMessage({
          type: "error",
          error: error instanceof Error ? error.message : "Unknown error",
        } as StreamedFieldMessage);
      }
    }
  })().catch(() => {});
});