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 persists all user data to chrome.storage.local using six typed storage items. Each item is validated with a Zod schema on both read and write, with version-1 migration support provided by WXT’s storage.defineItem. The withSchemaValidation wrapper ensures that corrupt or outdated data never surfaces as a runtime type error — invalid values are silently replaced by a defined fallback rather than crashing the UI.
Storage data is encrypted by the browser and scoped to the extension origin. It is not accessible by websites, other extensions, or any script running outside the extension’s own pages.

Storage Keys Overview

Storage KeyTypeScript TypeDescription
local:modelsIModelConfig[]Configured AI model profiles
local:lastUsedModelstringID of the most recently selected model
local:promptHistoryIPromptEntry[]Saved prompt + CSS entries, one per styling action
local:activeDraftsRecord<string, IActiveDraft>In-progress draft sessions keyed by hostname
local:apiKeysApiKeyConfigAPI keys per provider
local:formStateIFormStateOptions page UI state and user settings

IModelConfig

A model config represents a single configured AI endpoint. The user may add multiple models — for example, a fast cheap model and a high-quality model — and switch between them from the side panel.
export const ProviderTypeSchema = z.enum([
  "openai",
  "openrouter",
  "local",
  "anthropic",
  "google",
]);

export type PROVIDER_TYPES = z.infer<typeof ProviderTypeSchema>;

export const ModelConfigSchema = z.object({
  baseUrl: z.string(),
  chef: z.string(),
  chefSlug: z.string(),
  id: z.string(),
  modelId: z.string(),
  provider: ProviderTypeSchema,
});

export type IModelConfig = z.infer<typeof ModelConfigSchema>;
id
string
required
A stable UUID that uniquely identifies this model configuration record. Used as the value for local:lastUsedModel.
provider
PROVIDER_TYPES
required
One of "openai", "openrouter", "local", "anthropic", or "google". Determines which AI SDK provider factory is used when calling streamChat.
modelId
string
required
The model identifier string passed to the provider SDK — for example "gpt-4o", "claude-opus-4-5", or "google/gemini-2.5-pro".
baseUrl
string
required
The base URL for the provider API. Pre-populated from PROVIDER_PRESETS but can be overridden for self-hosted or proxied endpoints.
chef
string
required
A human-readable display name for this model, shown in the model selector UI.
chefSlug
string
required
A URL-safe slug derived from the display name, used for sorting and identification in the UI.
Each provider has a preset base URL available in PROVIDER_PRESETS:
export const PROVIDER_PRESETS = {
  openai:      { baseUrl: "https://api.openai.com/v1" },
  openrouter:  { baseUrl: "https://openrouter.ai/api/v1" },
  local:       { baseUrl: "http://localhost:11434/v1" },
  anthropic:   { baseUrl: "https://api.anthropic.com/v1" },
  google:      { baseUrl: "https://generativelanguage.googleapis.com/v1beta" },
} satisfies Record<PROVIDER_TYPES, { baseUrl: string }>;

IPromptEntry

Every prompt the user submits — along with the CSS it produced — is saved as a IPromptEntry. The prompt history is stored as a flat array sorted by timestamp and filtered by url in the UI to show only entries relevant to the current site.
export const PromptEntrySchema = z.object({
  applied: z.boolean(),
  css: z.string(),
  id: z.string(),
  prompt: z.string(),
  timestamp: z.number(),
  url: z.string(),
});

export type IPromptEntry = z.infer<typeof PromptEntrySchema>;
id
string
required
A UUID generated at the time of saving. Used as the stable key when the user re-applies or deletes a history entry.
prompt
string
required
The exact text the user typed in the chat input.
css
string
required
The full compiled CSS string that was produced for this prompt. Saved after the stream completes.
url
string
required
The full URL of the page at the time the prompt was submitted, used to scope history display to the current site.
applied
boolean
required
Whether this entry’s CSS is currently injected into the page. The content script checks this flag on REFRESH_STYLES to determine which entries to re-inject after navigation.
timestamp
number
required
Unix epoch milliseconds (Date.now()) recorded when the entry is saved. Used for chronological ordering.

IDraftTurn and IActiveDraft

Draft sessions allow multi-turn refinement within a single page visit. Each turn records the incremental CSS patch produced by one prompt. The session persists across page reloads so the user can continue refining without losing context.
export const DraftTurnSchema = z.object({
  cssPatch: z.string(),
  id: z.string(),
  prompt: z.string(),
  timestamp: z.number(),
});

export type IDraftTurn = z.infer<typeof DraftTurnSchema>;

export const ActiveDraftSchema = z.object({
  createdAt: z.number(),
  hostname: z.string(),
  turns: z.array(DraftTurnSchema),
  updatedAt: z.number(),
  url: z.string(),
});

export type IActiveDraft = z.infer<typeof ActiveDraftSchema>;

export const ActiveDraftsSchema = z.record(z.string(), ActiveDraftSchema);
local:activeDrafts is a Record<string, IActiveDraft> keyed by hostname (e.g. "github.com"). This means a single draft session spans all pages under the same hostname.
turns
IDraftTurn[]
required
An ordered array of turns in this draft session. Each turn holds the prompt text, the CSS patch it produced, a UUID, and a timestamp. The compiled CSS shown in the side panel is the concatenation of all turn patches.
hostname
string
required
The hostname of the page where the draft was started, used as the record key in local:activeDrafts.
url
string
required
The full URL at the time the draft was created. Recorded for display purposes.
createdAt
number
required
Unix epoch milliseconds when the first turn was added.
updatedAt
number
required
Unix epoch milliseconds of the most recent mutation. Used to detect stale drafts.

ApiKeyConfig

API keys are stored as a flat object keyed by provider name. All fields are optional because the user configures only the providers they intend to use. The local provider (Ollama) accepts any non-empty string as a key or can be used without one.
export const ApiKeyConfigSchema = z.object({
  openai:      z.string().optional(),
  openrouter:  z.string().optional(),
  local:       z.string().optional(),
  anthropic:   z.string().optional(),
  google:      z.string().optional(),
});

export type ApiKeyConfig = z.infer<typeof ApiKeyConfigSchema>;
API keys are stored in chrome.storage.local, not chrome.storage.session. They persist across browser restarts. Users on shared machines should be aware of this when configuring provider keys.

ISettingsFormState and IFormState

IFormState is the full serialised state of the options page, combining per-section UI state with the user’s persistent settings.
export const ThemeSchema = z.enum(["light", "dark", "system"]);
export type Theme = z.infer<typeof ThemeSchema>;

export const SettingsFormStateSchema = z.object({
  debugViewportSize: z.boolean(),
  theme: ThemeSchema,
});

export type ISettingsFormState = z.infer<typeof SettingsFormStateSchema>;

export const FormStateSchema = z.object({
  models: z.object({
    editingId: z.string().nullable(),
  }),
  settings: SettingsFormStateSchema,
});

export type IFormState = z.infer<typeof FormStateSchema>;
settings.theme
"light" | "dark" | "system"
required
Controls the colour scheme of the side panel and options page. "system" follows the OS/browser preference via prefers-color-scheme.
settings.debugViewportSize
boolean
required
When true, the side panel displays the character count of the viewport HTML sent to the AI with each request. Useful for diagnosing context-limit errors.
models.editingId
string | null
required
The UUID of the model currently being edited in the model form, or null when no edit is in progress. Persisting this prevents the form from resetting on accidental panel close.
The default form state is defined once and reused as the fallback across both the storage item definition and the withSchemaValidation wrapper:
const defaultFormState = FormStateSchema.parse({
  models: { editingId: null },
  settings: { theme: "system", debugViewportSize: false },
});

modifyStorage Helper

All write operations go through modifyStorage, which serialises concurrent writes to the same key using a promise chain stored in a Map.
export function modifyStorage<T>(
  key: string,
  item: { getValue: () => Promise<T>; setValue: (v: T) => Promise<void> },
  fn: (value: T) => T
): Promise<Result<T, AppError>>
Internally, withStorageLock maintains a storageLocks map from key string to the last in-flight promise. Each new write is chained onto the previous one with .then(fn, fn), so writes always execute in submission order even if the caller does not await them. Once a write settles and is still the tail of the chain, its entry is deleted from the map to prevent unbounded memory growth.
const storageLocks = new Map<string, Promise<unknown>>();

async function withStorageLock<T>(
  key: string,
  fn: () => Promise<T>
): Promise<T> {
  const previous = storageLocks.get(key) ?? Promise.resolve();
  const current = previous.then(fn, fn);
  storageLocks.set(key, current);
  try {
    return await current;
  } finally {
    if (storageLocks.get(key) === current) {
      storageLocks.delete(key);
    }
  }
}
modifyStorage wraps the read-modify-write cycle and returns Ok(next) on success or Err(AppError) with code STORAGE_WRITE_FAILED on failure, so callers get a Result they can inspect without try/catch.

Schema Validation Pattern

Every storage item is wrapped in withSchemaValidation to provide a validated read path. On getValue, the raw value from chrome.storage.local is run through schema.safeParse. If parsing succeeds, the typed data is returned. If it fails — because the stored value pre-dates the current schema, was written by an older extension version, or is otherwise malformed — the configured fallback is returned silently.
function withSchemaValidation<T>(
  item: ValidatedStorageItem<T>,
  schema: z.ZodType<T>,
  fallback: T
): ValidatedStorageItem<T> {
  return {
    getValue: async () => {
      const raw = await item.getValue();
      const parsed = schema.safeParse(raw);
      return parsed.success ? parsed.data : fallback;
    },
    setValue: (v: T) => item.setValue(v),
    watch: (cb) =>
      item.watch((newValue, oldValue) => {
        const parsed = schema.safeParse(newValue);
        cb(parsed.success ? parsed.data : fallback, oldValue);
      }),
  };
}
Version-1 migrations run before schema validation. If you add a new required field to a schema, add a migration that supplies the default value so existing stored objects pass validation after the migration runs.
The watch callback is also validated: if a storage change event carries an invalid payload (for example due to an out-of-order write from another extension context), the watcher receives the fallback rather than a partial or corrupted object.

Build docs developers (and LLMs) love