Signals Debugging
Use this skill when a user reports a UI bug in an app that uses @preact/signals-debug, the Signals devtools bridge, or @preact/signals-agent-vite.
The Vite plugin is configured with signalsVite().
What the Debug Stream Means
Signals debug data is structured, not just console text.
source: "signals", type: "update"
- A plain signal or computed changed value.
- Important fields:
signalName, signalType, prevValue, newValue, timestamp, page.pathname.
source: "signals", type: "effect"
- An effect ran because one of its dependencies changed.
- Important fields:
signalName, subscribedTo, allDependencies.
source: "signals", type: "component"
- A component render was triggered.
- Useful for confirming whether state changes reached the view layer.
source: "signals", type: "disposed"
- A signal/computed/effect was torn down.
- Useful for unmount bugs and stale subscriptions.
source: "network"
request, response, and error events are transport context.
- Use these to correlate auth failures, validation fetches, and retries.
source: "page"
ready, navigate, interaction, error, and unhandledrejection provide user-flow context.
How to Read a Signal Cascade
Think in this order:
- Interaction - what the user or test just did (
page.interaction, network.request)
- Root signal - which signal changed first (
signals.update with depth 0 in raw debug output)
- Derived state - which computed values re-ran because of it
- Effects/components - whether the change reached side effects or rendering
- Mismatch - compare the final signal state to the outcome
High-Signal Heuristics
- Network says
401 or 500, but a status signal becomes success
- The error path is mutating the wrong signal.
- A form submit interaction happens, but no relevant signal update follows
- The handler is not wired, is throwing early, or is reading stale state.
- A signal updates but no
component event follows
- The component is not subscribed, is reading via
peek(), or was disposed.
- The same signal flips rapidly between values
- Look for an effect loop or conflicting async writes.
disposed happens before the expected UI update
- The component/effect is unmounting or losing subscriptions too early.
Vite Plugin Workflow
When the app uses @preact/signals-agent-vite, use this flow:
- Create a session:
curl -X POST <YOUR_DEV_URL>/__signals_agent__/sessions \
-H 'content-type: application/json'
- Reproduce the issue in the browser or with Playwright.
- Fetch or stream the session events:
curl <YOUR_DEV_URL>/__signals_agent__/sessions/<session-id>/events
- Reset the local buffer between reproductions when you need a clean run:
curl -X POST <YOUR_DEV_URL>/__signals_agent__/reset
- Build a timeline:
- page interaction
- network request/response
- root signal update
- derived updates
- final rendered state
- Point to the first contradiction, not just the last error.
Signal Naming
- The Babel transform can name signals automatically from the variable they are assigned to.
- Example:
const count = signal(0) can become signal(0, { name: "count" }) in development transforms.
- This is why debug events often contain readable
signalName values even when the source code did not add one manually.
- Signals and computeds can also name themselves directly with the second options argument.
- Example:
signal(0, { name: "count" })
- Example:
computed(() => count.value * 2, { name: "doubled" })
- Prefer the explicit second argument when the local variable name is too generic or when you want stable names across refactors.
You can use these names in a param filterPatterns that you can pass to your session creation. This will make the debug stream only include events that match at least one pattern, which is helpful for noisy apps.
Example:
curl -X POST <YOUR_DEV_URL>/__signals_agent__/sessions \
-H 'content-type: application/json' \
-d '{"filterPatterns":["AuthForm","password"]}'
How to Filter Well
Start tight, then widen only if needed.
- Good first filters for form issues: component name, route name, feature name
- For auth flows:
AuthForm, auth, login, session, token
- If nothing shows up, remove component-specific filters and inspect the global stream
What to Say Back
Respond with:
- the triggering action
- the key network or page fact
- the contradictory signal transition
- the likely faulty branch or file
- the smallest fix that would align state with reality
Example:
Submitting `AuthForm` sends `POST /api/login`, which returns `401`.
The debug stream then shows `AuthForm.status` changing from `submitting` to `success` instead of `error`.
That means the catch path is writing the success state on failure.
Fix the submit error branch so it sets the error signal and leaves the form in an error state.
Cautions
- Treat sanitized/sensitive values like
[Redacted] as evidence that sensitive state exists, not as missing data.
- Do not assume every page or network error is causal; correlate it with nearby signal events.
- Prefer the earliest contradictory event in the timeline over the loudest downstream symptom.
Source: preactjs/signals — distributed by TomeVault.
1---2name: signals-debugging3description: Interprets `@preact/signals-debug` updates and the AI-native Vite event stream to diagnose reactive UI bugs. Use when this capability is needed.4---56# Signals Debugging78Use this skill when a user reports a UI bug in an app that uses `@preact/signals-debug`, the Signals devtools bridge, or `@preact/signals-agent-vite`.910The Vite plugin is configured with `signalsVite()`.1112## What the Debug Stream Means1314Signals debug data is structured, not just console text.1516- `source: "signals"`, `type: "update"`17 - A plain signal or computed changed value.18 - Important fields: `signalName`, `signalType`, `prevValue`, `newValue`, `timestamp`, `page.pathname`.19- `source: "signals"`, `type: "effect"`20 - An effect ran because one of its dependencies changed.21 - Important fields: `signalName`, `subscribedTo`, `allDependencies`.22- `source: "signals"`, `type: "component"`23 - A component render was triggered.24 - Useful for confirming whether state changes reached the view layer.25- `source: "signals"`, `type: "disposed"`26 - A signal/computed/effect was torn down.27 - Useful for unmount bugs and stale subscriptions.28- `source: "network"`29 - `request`, `response`, and `error` events are transport context.30 - Use these to correlate auth failures, validation fetches, and retries.31- `source: "page"`32 - `ready`, `navigate`, `interaction`, `error`, and `unhandledrejection` provide user-flow context.3334## How to Read a Signal Cascade3536Think in this order:37381. **Interaction** - what the user or test just did (`page.interaction`, `network.request`)392. **Root signal** - which signal changed first (`signals.update` with depth 0 in raw debug output)403. **Derived state** - which computed values re-ran because of it414. **Effects/components** - whether the change reached side effects or rendering425. **Mismatch** - compare the final signal state to the outcome4344## High-Signal Heuristics4546- Network says `401` or `500`, but a status signal becomes `success`47 - The error path is mutating the wrong signal.48- A form submit interaction happens, but no relevant signal update follows49 - The handler is not wired, is throwing early, or is reading stale state.50- A signal updates but no `component` event follows51 - The component is not subscribed, is reading via `peek()`, or was disposed.52- The same signal flips rapidly between values53 - Look for an effect loop or conflicting async writes.54- `disposed` happens before the expected UI update55 - The component/effect is unmounting or losing subscriptions too early.5657## Vite Plugin Workflow5859When the app uses `@preact/signals-agent-vite`, use this flow:60611. Create a session:6263```bash64curl -X POST <YOUR_DEV_URL>/__signals_agent__/sessions \65 -H 'content-type: application/json'66```67682. Reproduce the issue in the browser or with Playwright.693. Fetch or stream the session events:7071```bash72curl <YOUR_DEV_URL>/__signals_agent__/sessions/<session-id>/events73```74754. Reset the local buffer between reproductions when you need a clean run:7677```bash78curl -X POST <YOUR_DEV_URL>/__signals_agent__/reset79```80815. Build a timeline:82 - page interaction83 - network request/response84 - root signal update85 - derived updates86 - final rendered state876. Point to the first contradiction, not just the last error.8889## Signal Naming9091- The Babel transform can name signals automatically from the variable they are assigned to.92 - Example: `const count = signal(0)` can become `signal(0, { name: "count" })` in development transforms.93 - This is why debug events often contain readable `signalName` values even when the source code did not add one manually.94- Signals and computeds can also name themselves directly with the second options argument.95 - Example: `signal(0, { name: "count" })`96 - Example: `computed(() => count.value * 2, { name: "doubled" })`97- Prefer the explicit second argument when the local variable name is too generic or when you want stable names across refactors.9899You can use these names in a param `filterPatterns` that you can pass to your session creation. This will make the debug stream only include events that match at least one pattern, which is helpful for noisy apps.100101Example:102103```bash104curl -X POST <YOUR_DEV_URL>/__signals_agent__/sessions \105 -H 'content-type: application/json' \106 -d '{"filterPatterns":["AuthForm","password"]}'107```108109## How to Filter Well110111Start tight, then widen only if needed.112113- Good first filters for form issues: component name, route name, feature name114- For auth flows: `AuthForm`, `auth`, `login`, `session`, `token`115- If nothing shows up, remove component-specific filters and inspect the global stream116117## What to Say Back118119Respond with:1201211. the triggering action1222. the key network or page fact1233. the contradictory signal transition1244. the likely faulty branch or file1255. the smallest fix that would align state with reality126127Example:128129```130Submitting `AuthForm` sends `POST /api/login`, which returns `401`.131The debug stream then shows `AuthForm.status` changing from `submitting` to `success` instead of `error`.132That means the catch path is writing the success state on failure.133Fix the submit error branch so it sets the error signal and leaves the form in an error state.134```135136## Cautions137138- Treat sanitized/sensitive values like `[Redacted]` as evidence that sensitive state exists, not as missing data.139- Do not assume every page or network error is causal; correlate it with nearby signal events.140- Prefer the earliest contradictory event in the timeline over the loudest downstream symptom.141142---143> Source: [preactjs/signals](https://github.com/preactjs/signals) — distributed by [TomeVault](https://tomevault.io).144<!-- tomevault:4.0:skill_md:2026-06-25 -->