Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/thePrnvBot/dispel-web-stylist/llms.txt

Use this file to discover all available pages before exploring further.

Dispel uses a typed runtime messaging protocol built on @webext-core/messaging. All messages exchanged between extension components — the background service worker, content script, side panel, and options page — are defined in a single ProtocolMap interface. This central contract means TypeScript enforces both payload shapes and return types at every call site, eliminating an entire class of runtime messaging bugs that plague loosely-typed extensions.
All messages use the @webext-core/messaging library, which provides type-safe send and receive with fully inferred payloads. You never cast to any to satisfy the message bus.

The ProtocolMap

Every message Dispel can send is declared as a method signature on ProtocolMap. The parameter type is the payload, and the return type is what the handler resolves with. No-argument messages use () and void returns use void.
import type { IPickedElement } from "@/utils/schemas/storage";

export interface IPageMeta {
  title: string;
  url: string;
}

export interface IViewportInfo {
  height: number;
  scrollX: number;
  scrollY: number;
  width: number;
}

export interface PageContextResponse {
  meta: IPageMeta;
  viewport: IViewportInfo;
}

/** Typed protocol for all extension runtime messaging. */
export interface ProtocolMap {
  ACTIVATE_PICKER(): void;
  APPLY_CSS(data: { css: string }): void;
  CLEAR_STYLES(): void;
  DEACTIVATE_PICKER(): void;
  ELEMENT_PICKED(data: IPickedElement): void;
  GET_PAGE_HTML(data: { selector?: string }): string;
  GET_PAGE_META(): PageContextResponse;
  PICKER_CANCELLED(): void;
  REFRESH_STYLES(): void;
  VIEWPORT_HTML_SIZE(data: { size: number }): void;
}
Two utility types are derived from ProtocolMap to separate commands sent to the content script from events broadcast by it:
/** Messages sent to the content script via tabs.sendMessage. */
export type ContentCommand = {
  [K in keyof ProtocolMap]: ProtocolMap[K] extends (data: infer D) => unknown
    ? undefined extends D
      ? never
      : { data: D; type: K }
    : { type: K };
}[keyof ProtocolMap];

/** Messages broadcast from the content script to extension pages. */
export type ContentEvent = Extract<
  ContentCommand,
  { type: "ELEMENT_PICKED" | "PICKER_CANCELLED" | "VIEWPORT_HTML_SIZE" }
>;
The ten messages in ProtocolMap cover the full lifecycle of a styling session:

ACTIVATE_PICKER

Sent from the side panel to the content script. Puts the content script into element-picker mode, overlaying a hover highlight on the page so the user can click an element to target it.

DEACTIVATE_PICKER

Sent from the side panel to the content script. Tears down the picker overlay and restores normal pointer events without selecting an element.

APPLY_CSS

Sent to the content script with { css: string }. The content script injects the provided CSS into a managed <style> tag, replacing any previously injected rules for an instant live preview.

CLEAR_STYLES

Sent from the side panel to the content script. Removes all Dispel-injected CSS from the page, reverting it to its original appearance.

ELEMENT_PICKED

Broadcast from the content script to the side panel when the user clicks an element while picker mode is active. Carries an IPickedElement payload with outerHTML, label, and optional id.

PICKER_CANCELLED

Broadcast from the content script to the side panel when the picker is dismissed (e.g. via Escape key) without an element being selected.

GET_PAGE_HTML

Sent to the content script from the background AI agent. Accepts an optional { selector?: string } to scope extraction to a specific element. Returns a sanitized HTML string of the visible viewport, or the matched element’s subtree.

GET_PAGE_META

Sent to the content script from the background or side panel. Returns a PageContextResponse containing the page title, URL, and viewport dimensions including scroll offsets.

REFRESH_STYLES

Sent to the content script on page load. Instructs the content script to re-read saved styles from the prompt history and re-inject them, restoring the persisted appearance after navigation.

VIEWPORT_HTML_SIZE

Broadcast from the content script after extracting viewport HTML. Carries { size: number } — the character count of the extracted HTML — so the side panel can display a debug indicator about the size of DOM context sent to the AI.

Message Directions

Each message flows in a well-defined direction. The table below shows the originating component and the receiving component for every protocol message.
MessageFromTo
ACTIVATE_PICKERSide panelContent script
APPLY_CSSBackground / side panelContent script
CLEAR_STYLESSide panelContent script
DEACTIVATE_PICKERSide panelContent script
ELEMENT_PICKEDContent scriptSide panel
GET_PAGE_HTMLBackground (AI agent)Content script
GET_PAGE_METABackground / side panelContent script
PICKER_CANCELLEDContent scriptSide panel
REFRESH_STYLESContent scriptContent script
VIEWPORT_HTML_SIZEContent scriptSide panel

Using sendTabMessage

When the background service worker or any extension page needs to send a message to a specific browser tab’s content script, it calls sendTabMessage. The helper wraps @webext-core/messaging’s sendMessage in a Result-returning function, so the caller always handles the case where no content script is listening.
export function sendTabMessage<K extends keyof ProtocolMap>(
  tabId: number,
  type: K,
  data?: ProtocolMap[K] extends (arg: infer D) => unknown ? D : never
): Promise<
  Result<
    ProtocolMap[K] extends (...args: never[]) => infer R ? Awaited<R> : void,
    AppError
  >
>
Two distinct error codes distinguish the two failure modes:
  • TAB_NO_RECEIVER — the content script is not yet injected or has been unloaded. This is common immediately after a navigation event.
  • TAB_MESSAGE_FAILED — the message was sent but an unexpected error occurred during delivery or handling.
For runtime (non-tab) messages — such as those sent between the side panel and the background — use sendRuntimeMessage, which has an equivalent signature but omits the tabId parameter and maps failures to MESSAGING_SEND_FAILED.

Error Codes

APP_ERROR_CODES

AppErrorCode is a union of all recognised error strings. Every AppError object carries one of these codes, enabling exhaustive pattern matching in error handlers.
export const APP_ERROR_CODES = [
  "MESSAGING_SEND_FAILED",
  "TAB_GET_FAILED",
  "TAB_QUERY_FAILED",
  "TAB_NOT_FOUND",
  "TAB_MESSAGE_FAILED",
  "TAB_NO_RECEIVER",
  "TAB_CREATE_FAILED",
  "SIDEBAR_TOGGLE_FAILED",
  "SIDEPANEL_OPEN_FAILED",
  "SIDEPANEL_SETUP_FAILED",
  "OPEN_OPTIONS_PAGE_FAILED",
  "STORAGE_READ_FAILED",
  "STORAGE_WRITE_FAILED",
  "VALIDATION_FAILED",
  "INVALID_HOSTNAME",
  "NO_ACTIVE_TAB",
  "MISSING_API_KEY",
  "RATE_LIMITED",
  "CONTEXT_LIMIT_REACHED",
  "STREAM_INCOMPLETE",
  "API_ERROR",
  "ELEMENT_NOT_FOUND",
  "AMBIGUOUS_REQUEST",
  "BROWSER_ACTION_UNAVAILABLE",
] as const;

export type AppErrorCode = (typeof APP_ERROR_CODES)[number];
CodeDescription
MESSAGING_SEND_FAILEDA runtime message (non-tab) could not be delivered.
TAB_GET_FAILEDbrowser.tabs.get() threw an unexpected error.
TAB_QUERY_FAILEDbrowser.tabs.query() threw an unexpected error.
TAB_NOT_FOUNDNo tab matched the requested tab ID.
TAB_MESSAGE_FAILEDA tab-targeted message was sent but delivery or handling failed.
TAB_NO_RECEIVERNo content script listener exists in the target tab.
TAB_CREATE_FAILEDbrowser.tabs.create() threw an unexpected error.
SIDEBAR_TOGGLE_FAILEDThe sidebar could not be toggled open or closed.
SIDEPANEL_OPEN_FAILEDbrowser.sidePanel.open() failed.
SIDEPANEL_SETUP_FAILEDSide-panel initial setup threw an error.
OPEN_OPTIONS_PAGE_FAILEDThe options page could not be opened.
STORAGE_READ_FAILEDchrome.storage.local read threw an unexpected error.
STORAGE_WRITE_FAILEDchrome.storage.local write (via modifyStorage) failed.
VALIDATION_FAILEDA Zod schema parse failed on user-supplied input.
INVALID_HOSTNAMEThe active tab’s URL has no valid hostname.
NO_ACTIVE_TABThere is no active tab in the current window.
MISSING_API_KEYNo API key is configured for the selected provider.
RATE_LIMITEDThe provider returned HTTP 429.
CONTEXT_LIMIT_REACHEDThe prompt exceeds the model’s context window.
STREAM_INCOMPLETEThe model completed without calling generateCss or reportError.
API_ERRORA generic, unclassified provider API error.
ELEMENT_NOT_FOUNDThe AI could not find the requested element in the DOM.
AMBIGUOUS_REQUESTThe request matches too many elements for safe targeting.
BROWSER_ACTION_UNAVAILABLEThe browser action API is unavailable in this context.

LLM_ERROR_CODES

LLM_ERROR_CODES is a strict subset of APP_ERROR_CODES that the AI agent may emit via its reportError tool. These codes are the only values the model is permitted to report — the Zod schema on reportError’s input enforces this constraint.
export const LLM_ERROR_CODES = [
  "RATE_LIMITED",
  "CONTEXT_LIMIT_REACHED",
  "STREAM_INCOMPLETE",
  "API_ERROR",
  "ELEMENT_NOT_FOUND",
  "AMBIGUOUS_REQUEST",
] as const satisfies readonly AppErrorCode[];
CodeWhen the model should use it
RATE_LIMITEDThe upstream provider is rate-limiting requests.
CONTEXT_LIMIT_REACHEDThe combined prompt and page HTML exceed the model’s context window.
STREAM_INCOMPLETEThe generation was interrupted before a terminal tool was called.
API_ERRORAn unclassified API-level failure occurred.
ELEMENT_NOT_FOUNDThe requested element is not present in the visible viewport.
AMBIGUOUS_REQUESTThe request cannot be safely resolved to a unique selector.

Result Type

All fallible operations in Dispel — messaging, storage reads and writes, and AI streaming — return a Result<T, E> discriminated union rather than throwing exceptions. This pattern forces callers to handle both outcomes before accessing a value.
/**
 * A discriminated union representing either a successful computation (`Ok`)
 * containing a value of type `T`, or a failure (`Err`) containing an error of type `E`.
 */
export type Result<T, E = string> =
  | { readonly _tag: "Ok"; readonly value: T }
  | { readonly _tag: "Err"; readonly error: E };

/** Wraps a successful value into a Result. */
export const Ok = <T>(value: T): Result<T, never> => ({
  _tag: "Ok",
  value,
});

/** Wraps an error value into a Result. */
export const Err = <E>(error: E): Result<never, E> => ({
  _tag: "Err",
  error,
});
Check the _tag discriminant to branch on success or failure:
const result = await sendTabMessage(tabId, "GET_PAGE_META");

if (result._tag === "Err") {
  console.error(result.error.code, result.error.message);
  return;
}

const { meta, viewport } = result.value;
Additional utility functions — map, flatMap, getOrElse, isOk, and fromPromise — are exported from @/utils/result for composing Result chains without nested if blocks.

Build docs developers (and LLMs) love