Dispel’s AI layer is built on the Vercel AI SDK (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.
ai package). Rather than asking the model to return CSS as plain text in a chat reply, Dispel uses a tool-calling loop where the model must invoke a generateCss or reportError tool — ensuring structured, parseable output every time. This design means the side panel never has to parse markdown fences or strip prose from a model response; it receives either a clean CSS string or a typed error object with an AppErrorCode.
The AI model never outputs plain text. It must call
generateCss or reportError. If neither tool is called within four steps, Dispel automatically emits a STREAM_INCOMPLETE error to the caller.Agent Tools
Three tools are registered with the Vercel AI SDKstreamText call. Two are static singleton objects (generateCss and reportError) and one is created per-request (getPageHTML) because it must capture the current tabId.
generateCss
The terminal success tool. When the model has enough context to produce styles, it callsgenerateCss with a raw CSS string. The execute function is intentionally trivial — it returns the CSS value unchanged so the streaming pipeline can forward it directly to the content script.
reportError
The terminal failure tool. When the model cannot satisfy the request — because the target element is not in the DOM, the request is ambiguous, or the operation is unsafe — it callsreportError with one of the permitted LLM_ERROR_CODES values and a human-readable explanation. The Zod enum on code ensures the model cannot invent arbitrary error strings.
getPageHTML (dynamic)
A per-request tool bound to a specific browser tab. It is created fresh for eachstreamChat call via createGetPageHTMLTool(tabId). When invoked by the model, it sends the GET_PAGE_HTML message to the content script and returns the sanitized viewport HTML as a string. The optional selector parameter lets the model narrow the extraction to a single element when it already has a candidate selector and wants to verify its structure before writing rules.
System Prompt Structure
The system prompt is assembled from two parts: a staticSTATIC_GUIDELINES constant and a dynamic section generated by generateSystemPrompt(). Together they tell the model what CSS practices to follow, what constraints apply, and the specifics of the current page and request.
STATIC_GUIDELINES
The static portion never changes across requests. It defines selector strategy, scoping rules, forbidden patterns, modern CSS preferences, and the Shadow DOM constraint. Keeping it static means it can be sent as a single string without recomputation.generateSystemPrompt()
The dynamic portion is appended afterSTATIC_GUIDELINES and contains three optional sections, each rendered only when the relevant data is present:
PAGE CONTEXT
Appended when
pageContext.meta is available. Provides the page URL, title, and viewport dimensions (width × height with scroll offsets). The model uses this to understand layout constraints and write responsive CSS.SELECTED ELEMENTS
Appended when the user has picked one or more elements via the picker tool. Each entry is the element’s
outerHTML prefixed with its tag label. The model is instructed to prioritise these elements when writing selectors.CURRENT STYLES
Appended when there is already a compiled CSS string applied to the page (from a previous turn in the draft session). Critically, this section ends with an instruction that the model must output a complete replacement stylesheet — not just the new rules — so that each
generateCss call is idempotent and the <style> tag can be replaced wholesale.Streaming Pipeline
ThestreamChat function is an async generator that manages the full AI request lifecycle. It accepts a StreamChatOptions bag, constructs the model, initiates streamText, and yields StreamChatChunk items.
Model construction
createModel selects the correct provider factory from modelFactories using the provider field, calls it with the apiKey and baseUrl, then calls the returned function with modelId to obtain a LanguageModel instance.streamText invocation
The Vercel AI SDK
streamText is called with the assembled system prompt, the user’s message, and the three agent tools (generateCss, reportError, getPageHTML).prepareStep hook — DOM fetch guard
Before each step,
prepareStep inspects the steps completed so far. If getPageHTML has already been called in any previous step, it is removed from activeTools. This prevents the model from making repeated DOM fetches in a single session, keeping the request count and cost predictable.stopWhen — termination conditions
The stream stops as soon as any of three conditions is met:
hasToolCall("generateCss")— the model produced CSShasToolCall("reportError")— the model reported an errorstepCountIs(4)— the model has taken four steps without a terminal call
fullStream filtering
result.fullStream is piped through asyncFilterMap with toStreamChunk. Only tool-result events with toolName === "generateCss" or toolName === "reportError" are yielded as StreamChatChunk values; all other stream events (text deltas, step metadata) are dropped.Error Classification
When thestreamText call throws, Dispel classifies the error using an ordered list of matchers before falling back to API_ERROR.
| Condition | Emitted code |
|---|---|
APICallError with statusCode === 429 | RATE_LIMITED |
APICallError with statusCode === 413 | CONTEXT_LIMIT_REACHED |
APICallError with statusCode === 400 or 422 and message/body matches CONTEXT_LIMIT_RE | CONTEXT_LIMIT_REACHED |
| Any other thrown value | API_ERROR |
CONTEXT_LIMIT_RE pattern is intentionally broad to catch the varied wording that different providers use for context-window errors — OpenAI uses context_length_exceeded, Anthropic uses prompt too long, and OpenRouter may relay either.
Provider Factories
Each supported provider has a factory function that wraps the respective AI SDK constructor. ThemodelFactories record maps every PROVIDER_TYPES value to a ModelFactory — a function that takes an API key and optional base URL and returns a function from modelId to LanguageModel.
local provider uses @ai-sdk/openai-compatible so it works with any OpenAI-compatible server (Ollama, LM Studio, vLLM). For OpenRouter, the baseUrl is normalised: if the configured URL’s origin matches https://openrouter.ai, the baseUrl override is discarded and the SDK default is used, because the OpenRouter SDK ignores custom base URLs anyway.
CSS Application Flow
OncestreamChat yields a { type: "css", css: string } chunk, the CSS propagates to the page through the following chain:
streamCSS accumulates chunks
streamCSS in stream-css.ts iterates over streamChat’s async generator and calls accumulate on each chunk, building a growing CSS string in state.css.onChunk callback is invoked
Each time
state.css grows, streamCSS calls the onChunk callback provided by the caller (the side panel’s submit workflow), passing the current accumulated CSS string.applyCssToTab sends APPLY_CSS
Immediately after the
onChunk callback returns, streamCSS awaits applyCssToTab(tabId, state.css), which sends the APPLY_CSS message to the content script in the active tab. The two calls are sequential within each chunk iteration.