Architecture

Content script owns the DOM; the service worker owns credentials and HTTP; the side panel is configuration and progress.

Runtime contexts

Context Entry point Responsibility
Content script src/content/index.ts Scans, applies, validates, navigates, streams progress
Service worker src/background/index.ts Message router; holds keys; builds prompts; all network calls
Side panel src/sidepanel/main.ts Provider config, custom request, run trigger, live log

The page never sees an API key: the content script asks the worker for values and receives only the values back.

System diagram

flowchart TB
  subgraph Page["Host page"]
    DOM[DOM / forms]
  end

  subgraph CS["Content script"]
    Entry[index.ts]
    Orch[fillOrchestrator]
    RunState["runState: step epochs"]
    FieldId["fieldId: content-hash sids"]
    Scan[scan]
    Widgets["widgets: native + aria adapters"]
    Settle["settle: MutationObserver"]
    Validate[validate]
    Nav[navigation]
  end

  subgraph BG["Service worker"]
    Router[message router]
    LLM[llm]
    Adapters["provider adapters"]
    Vault["keyVault: AES-GCM"]
    Store[storage]
  end

  subgraph SP["Side panel"]
    Panel[main.ts]
    Log[progress log]
  end

  Panel -->|RUN_FILL| Entry
  Entry --> Orch
  Orch --> RunState
  Orch --> Scan
  Scan --> FieldId
  Scan --> Widgets
  Widgets --> DOM
  Orch --> Settle
  Orch --> Validate
  Orch --> Nav
  Nav --> DOM
  Orch -->|RUN_PROGRESS| Log
  Orch <-->|LLM_FILL| Router
  Router --> LLM
  LLM --> Adapters
  Router --> Store
  Store --> Vault
  Adapters -->|HTTPS| Cloud[(Provider APIs)]
        

Fill workflow

sequenceDiagram
  participant U as User
  participant SP as Side panel
  participant CS as Content script
  participant DOM as Page DOM
  participant BG as Service worker
  participant API as Provider

  U->>SP: Fill this page
  SP->>CS: RUN_FILL
  CS->>BG: GET_SETTINGS
  BG-->>CS: settings (no key material)

  loop Each form step
    CS->>CS: beginEpoch, fresh applied map
    CS->>DOM: scan via widget adapters
    CS->>CS: chunk by fieldset / role=group / heading
    loop Each chunk, bounded by maxRounds
      CS->>BG: LLM_FILL(snapshot + run context)
      BG->>API: chat completion
      API-->>BG: JSON values
      BG-->>CS: values
      CS->>CS: validate, resolve option labels
      CS->>DOM: apply via native setters + events
      CS->>DOM: waitForDomQuiet (reveals conditionals)
      CS-->>SP: RUN_PROGRESS
    end
    CS->>DOM: click next, compare field fingerprint
    alt Page reported validation errors
      CS->>BG: LLM_FILL(corrections)
      CS->>DOM: repair, click next again
    end
  end

  CS-->>SP: fields filled, steps completed
        

Why reactive forms need special handling

1. Frameworks revert direct writes. React installs an instance-level value property. Assigning el.value = x updates that tracker too, so the following input event looks like a no-op. Writes go through the prototype’s setter instead, then the result is verified.

export function setNativeValue(el: HTMLElement, value: string): void {
  const descriptor =
    Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el) as object, "value") ??
    Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value");
  if (descriptor?.set) descriptor.set.call(el, value);
  else (el as HTMLInputElement).value = value;
}

2. DOM nodes are recycled between steps. React often reuses the same <input> elements across wizard steps. Synthetic ids are s{epoch}_{hash}; the epoch increments on every step change so a recycled node cannot collide with a previous step’s applied map. Framework-generated ids (:r3:, mui-1234, radix-…) are excluded from the hash.

3. Option values are not option labels. For <option value="13">Tokyo</option>, prompts carry [value, label] pairs. Resolution matches from strictest to loosest — exact value, exact label, normalized, then unique containment — and refuses ambiguous loose matches.

Project layout

ai-form-filler/
├── manifest.config.ts
├── _locales/{en,de,ja,es,pt,bn}/
├── docs/                     # This site (GitHub Pages)
└── src/
    ├── background/           # Router, LLM, storage, key vault
    ├── content/              # Orchestrator, scan, apply, navigation, widgets
    ├── shared/               # Types, providers, heuristics
    └── sidepanel/            # UI

Development

npm install
npm run build      # tsc --noEmit && vite build
npm run dev        # rebuild on change
npm run check      # typecheck only

Load dist/ via chrome://extensionsLoad unpacked.

New widget families: implement match / describe / read / apply in src/content/widgets/ and register in widgets/index.ts. ARIA adapters are registered before native ones.