Refine Live
Two modes
Persistent (recommended — run and forget)
Run npx transitions-refine live from your project. The CLI starts the relay and wires REFINE_AGENT_CMD so the relay spawns your agent CLI per Refine click. No chat loop; idle = zero credit burn. Works hours later as long as the relay process keeps running. Stop with Ctrl-C (or npx transitions-refine stop).
In-chat loop (fallback — this skill)
Run /refine live in Cursor/Claude/Codex when the relay is up but has no REFINE_AGENT_CMD. You become the poller via GET /jobs/next. The Agent tab stays available only while you keep polling — each idle poll cycle consumes chat turns/credits. Say "stop refine" to exit.
Use the in-chat loop only when you cannot wire a persistent agent CLI.
Turn yourself into the LLM behind the Timeline Inspector's Refine button (in-chat fallback mode). While
this loop runs, the panel's LLM tab is "available": each click sends one
transition here, you reason about it, and your suggestions appear in the panel.
You are the poller. Nothing is installed — you just talk to a small local relay
(default http://localhost:7331) that the npx injector already started.
How it works
Browser (Refine, LLM tab) ──POST /jobs──► relay ──GET /jobs/next──► YOU
◄──GET /jobs/:id── relay ◄──POST /jobs/:id/result── YOU
The loop — stay live, but don't burn credits forever
Keep polling so the panel's LLM tab stays "available", but this loop costs chat
turns/credits even while idle, so it is not truly run-and-forget — it has
three exits, in priority order:
- Relay stop signal (authoritative).
GET /jobs/next may return 200 with
{"stop": true}. The relay sends this when the user clicks Stop in the
panel, or automatically after ~10 min with no jobs. Always honor it: stop
looping immediately, tell the user the LLM tab will go unavailable and how to
resume (/refine live), and end your turn. Never re-poll after a stop signal.
- The user says so — "stop refine", "exit live", etc.
- Your own idle backoff (safety net). A long stretch of
204s is normal —
it just means no one has clicked Refine yet — but to avoid spending credits on
a forgotten loop, back off as idle grows instead of hammering immediately:
re-poll right away for the first few empty cycles, then pause ~5s between polls,
and after ~10 min of unbroken idle stop on your own (same as the relay's
auto-stop) and tell the user how to resume. Any real job resets the backoff.
The relay reports the agent as "available" for ~120s after your last poll, so
short pauses keep you live. A successful job always resets idle, so an active
session never backs off.
Announce yourself once, before the first poll. The relay keeps a sticky
Stop latch: after a panel Stop (or the idle auto-stop) it answers every
GET /jobs/next with {"stop": true} until a new agent explicitly resumes —
so a stopped session can't silently come back. Clear the latch a single time
at startup, then begin polling:
curl -s -X POST http://localhost:7331/poller/start
Do not call this again mid-loop (it would defeat a user's Stop). Only on a
fresh /refine live.
Claim the next job (long-poll). This call blocks up to ~25s, then returns.
curl -s http://localhost:7331/jobs/next
HTTP 204 / empty body → no work yet. Poll again, applying the idle
backoff above (immediate at first, then ~5s pauses, then stop after ~10 min).
HTTP 200 with {"stop": true} → the loop must end. Stop polling, tell
the user the LLM tab is now unavailable and that /refine live resumes it,
and end your turn. Do not treat it as a job.
HTTP 200 with a job JSON → work to do. Shape:
{
"id": "uuid",
"request": {
"label": "Resize + Color",
"selector": ".box-resize",
"mode": "llm",
"refineType": "small",
"timings": [
{ "property": "width", "durationMs": 400, "delayMs": 0, "easing": "ease-out" },
{ "property": "background", "durationMs": 400, "delayMs": 0, "easing": "ease-out" }
]
}
}
If request.kind === "scan" this is not a suggestion job — the panel is
asking you to group the page's transitions by reading the source. Jump to
## Scan jobs and return groups instead of
suggestions.
If request.kind === "apply" this is not a suggestion job — the user
pressed Accept to write changes to their code. Jump to
## Apply jobs and edit the source instead of
posting suggestions. Everything below (refineType, steps 3–4) is for the
normal Refine flow.
refineType chooses what kinds of suggestions to make (it mirrors the
panel's two tabs). The tabs scan independently, so answer only the one
you were asked for:
"small" (or missing) → Small refinements: nudge the existing
declarations toward the motion tokens only (step 3a). Do not
propose a recipe swap here — that's the Replace tab's separate job.
"replace" → Replace transition: suggest a whole-transition recipe
swap only (step 3b). Do not propose motion-token tweaks — skip
step 3a entirely.
(Optional) post progress so the panel shows what you're doing:
curl -s -X POST http://localhost:7331/jobs/<id>/status \
-H 'Content-Type: application/json' \
-d '{"message":"Matching to transitions.dev motion tokens…"}'
Answer in ONE shot — speed matters. Each click should feel instant, so
resolve the job from the data below plus what's already in this skill. Do not
spawn subagents or run a broad codebase search, and do not open the
transitions-dev SKILL.md — its tokens and decision rules are inlined here.
refineType === "small" → step 3a only, with zero file reads.
refineType === "replace" → step 3b only; open at most the one recipe
reference file you choose.
First, infer each declaration's usage from label + selector (modal
close, dropdown open, tooltip, badge, resize, color/theme change…). Match on
intent, not the nearest number.
3a. Motion-token tweaks (refineType === "small" only — no file reads).
Pick the token that fits the usage and propose a change only where the current
value actually differs.
- Durations: 40ms Stagger (per-item offset) · 80ms Micro (tooltip delay,
shake segment) · 150ms Quick (modal/dropdown close, text swap, tooltip
appear) · 250ms Fast (icon swap, dropdown/modal open, tabs slide, page
slide) · 350ms Medium (panel/toast close) · 400ms Slow (panel open, skeleton
reveal, input clear) · 500ms Very slow (emphasis, badge appear, text reveal,
success check).
- Default easing — "Smooth ease out":
cubic-bezier(0.22, 1, 0.36, 1)
(modal/dropdown/panel open+close, page slide, resize, position change).
- Other on-grid easings — LEAVE UNCHANGED:
ease-out (tooltip),
ease-in-out (icon/text swap, text reveal, skeleton reveal), linear
(shimmer, pulse, spinner), cubic-bezier(0.34, 1.36, 0.64, 1) (badge pop),
cubic-bezier(0.34, 3.85, 0.64, 1) (avatar return).
- Nudge toward Smooth ease out: generic
ease, ease-in, or any
hand-rolled cubic-bezier()/linear() that isn't a token above.
- Scale / blur / distance (the non-resting
transform pre-scale or
translate distance, and the filter pre-blur a lane carries): scale 0.96
Large (modal) · 0.97 Medium (dropdown open) · 0.98 Small (tooltip) · 0.99
Tiny (dropdown close); blur 2px (panel/icon/text swap, skeleton, number) ·
3px (page slide, text reveal) · 8px (success check); distance 4px (text
swap) · 6px (shake small) · 8px (badge, page slide, shake large) · 12px
(text reveal) · 30px (check badge). Propose kind:"scale"/"blur"/
"distance" (patch key scale/blur/translate, pass translateVarName
through) only where a lane's value differs from its usage token.
- Polish add-on (optional): if the project has the
transitions-polish
skill installed (.agents/skills/transitions-polish/_refine-rules.md,
.claude/…, or skills/…), read that ONE file a single time and also apply
its rules — open/close asymmetry (closes faster/quieter than opens), hover
in fast / out soft, stagger totals under ~300ms, 80ms intent delays, and
never delay a close. This is the only file read allowed on a Small job, and
only when that skill is present.
3b. Whole-transition recipe swap (refineType === "replace" only — no file
reads). Match the inferred usage to ONE recipe below (this list is the
decision rules — no SKILL.md or reference-file read needed). Emit ONE
kind: "replace" suggestion whose patch carries the motion-token
duration/easing for the recipe's phase (open vs close) on the property that
already transitions (or "all"), with a reference field naming the file and
the recipe in title + reason. The patch only drives the live preview —
exact keyframes/structure come from the user pasting that reference file, so you
never need to open it. If no recipe genuinely fits the usage, return an
empty suggestions array with a short summary.
- Card resize — a container changes width/height on a layout change (
01-card-resize.md)
- Number pop-in — a number/digit updates (
02-number-pop-in.md)
- Notification badge — a small dot/badge appears on a trigger (
03-notification-badge.md)
- Text states swap — text content changes in place (
04-text-states-swap.md)
- Menu dropdown — an anchored surface grows from its trigger (
05-menu-dropdown.md)
- Modal open/close — a centered dialog scales up, softer scale-down on close (
06-modal.md)
- Panel reveal — a surface slides into a region with a cross-blur (
07-panel-reveal.md)
- Page side-by-side — slide between list↔detail or step 1↔step 2 (
08-page-side-by-side.md)
- Icon swap — two icons cross-fade in the same slot (
09-icon-swap.md)
- Success check — a checkmark celebration: fade + rotate + bob + stroke-draw (
10-success-check.md)
- Avatar group hover — hover lifts an item in a horizontal stack (
11-avatar-group-hover.md)
- Error state shake — invalid-input shake (
12-error-state-shake.md)
- Input clear with dissolve — clearing a text field (
13-input-clear-dissolve.md)
- Skeleton loader and reveal — placeholder pulses then swaps to real content (
14-skeleton-reveal.md)
- Shimmer text — in-progress / "thinking" text shimmer (
15-shimmer-text.md)
- Tabs sliding — a moving highlight across segmented options (
16-tabs-sliding.md)
- Tooltip open/close — delayed fade+scale in, instant out (
17-tooltip.md)
- Texts reveal — staggered blurred rise of stacked text lines (
18-texts-reveal.md)
- Card hover tilt — 3D tilt toward the pointer (
19-card-tilt.md)
- Plus to menu morph — a circular trigger becomes the surface it opens (
20-plus-menu-morph.md)
- Accordion expand — a collapsible body grows/shrinks in height (
21-accordion.md)
Tie-break: prefer the lower-overhead recipe (card resize over panel reveal,
dropdown over modal). Only propose a swap when the current declarations are
clearly a hand-rolled version of a recipe or are missing the structure the usage
calls for; if the transition already is the right recipe, return empty.
Post the result (this completes the job and renders cards in the panel):
curl -s -X POST http://localhost:7331/jobs/<id>/result \
-H 'Content-Type: application/json' \
-d '{
"summary": "Tightened the resize and softened the color fade.",
"suggestions": [
{
"id": "width-duration",
"kind": "duration",
"property": "width",
"title": "Duration → Snappy (250ms)",
"from": "400ms",
"to": "250ms",
"patch": { "property": "width", "durationMs": 250 },
"reason": "A size change reads as direct manipulation — snappy is more responsive than 400ms."
}
]
}'
The example above is a small job (token tweaks only). A replace job instead
returns a single kind: "replace" card as its only suggestion:
{
"id": "replace-card-resize",
"kind": "replace",
"property": "width",
"title": "Replace with Card resize",
"from": "hand-rolled width tween",
"to": "transitions.dev · Card resize",
"patch": { "property": "width", "durationMs": 250, "easing": "cubic-bezier(0.22, 1, 0.36, 1)" },
"reference": "transitions-dev/01-card-resize.md",
"reason": "This is a width tween on layout change — the Card resize recipe handles it properly. Apply nudges the live timing; paste 01-card-resize.md (run `transitions apply card-resize`) for the full recipe."
}
If nothing should change, post "suggestions": [] with a short summary.
If something goes wrong, report it instead:
curl -s -X POST http://localhost:7331/jobs/<id>/error \
-H 'Content-Type: application/json' -d '{"message":"…"}'
Go back to step 1. Keep looping, but honor the three exits from
the loop section: a
{"stop": true} from the relay, the user telling you to stop, or your own idle
backoff/auto-stop after ~10 min quiet. A real job resets idle. Whenever you do
stop, tell them the LLM tab will go unavailable and how to restart
(/refine live).
Scan jobs (group from source)
When a claimed job has request.kind === "scan", the panel wants you to turn a
flat list of DOM-detected transitions into components with phases. A naive
DOM scan only sees each element's current computed transition — it can't tell
open from close, and lists related elements (panel, backdrop, staggered items)
separately. You fix that by reading the source. The request looks like:
{
"id": "uuid",
"request": {
"kind": "scan",
"url": "http://localhost:5173/",
"raw": [
{ "label": "div.dropdown-panel", "selector": ".dropdown-panel",
"properties": ["opacity","transform"],
"timings": [{ "property": "opacity", "durationMs": 200, "delayMs": 0, "easing": "ease-out" }],
"cssRules": [
".dropdown .dropdown-panel { opacity: 0; transition: opacity 200ms ease-out 0ms, transform 200ms cubic-bezier(0.22, 1, 0.36, 1) 0ms; }",
".dropdown.is-open .dropdown-panel { opacity: 1; transform: translateY(0); }",
".dropdown.is-closing .dropdown-panel { transition: opacity 150ms ease-in 0ms; opacity: 0; }"
] }
]
}
}
Be fast. The raw.timings are already accurate for each element's current
on-screen state — treat them as ground truth and reuse them verbatim. Most raw
entries also carry cssRules: the CSS rules harvested live from the page
(CSSOM) that drive that element across all states (base + open + close), with
var() already resolved to concrete values.
Fast path — prefer cssRules over the filesystem. When an entry has
cssRules, they are authoritative and contain everything you need: the opposite
phase's timings live on a state-variant selector inside them (e.g.
.dd.is-closing .dd-panel, .modal[data-closing] .dialog), and the toggled
state is visible in those selectors. Derive grouping, phases, toggled state, and
opposite-phase timings directly from cssRules + timings — do not
glob/grep/read files for any element whose cssRules is non-empty; it only
wastes time. Only fall back to reading source for entries with an empty/missing
cssRules (CORS-locked sheets, styled-components, Tailwind, etc.), and even then
read the minimum.
Do this:
Identify each animated component the raw entries belong to (dropdown,
modal, tooltip, accordion, drawer, toast…). The selectors/labels usually make
this obvious — only read source (plain CSS / CSS Modules,
styled-components/emotion, Tailwind, inline styles, Motion/Framer variants)
when the grouping is genuinely unclear.
Split each component into phases — usually open and close (a hover-only
component can be a single phase). The phase matching the current DOM reuses the
provided timings; the opposite phase often lives on a different selector
(.is-open vs .is-closing) with different timings — take it from the entry's
cssRules (or, only if it has none, read source). Report both even though
only one is in the DOM right now.
List each phase's members — the elements that animate in that phase. Give
each a stable id, a human label, a live-resolvable CSS selector, an
optional toState hint (the class/attribute that drives the phase, e.g.
.is-open), and its propertyTimings. For the current-state phase, copy the
provided raw.timings verbatim; for the opposite phase, quote the real
timings from the entry's cssRules (already var()-resolved) — or from source
if it has none — never invent.
Post the groups (this completes the job):
curl -s -X POST http://localhost:7331/jobs/<id>/result \
-H 'Content-Type: application/json' \
-d '{
"summary": "Grouped Dropdown into Open/Close.",
"groups": [
{ "id": "dropdown", "label": "Dropdown", "component": "src/Dropdown.tsx",
"phases": [
{ "id": "dropdown:open", "phase": "open", "label": "Open", "members": [
{ "id": "panel", "label": "Panel", "selector": ".dropdown-panel", "toState": ".is-open",
"propertyTimings": [
{ "property": "opacity", "durationMs": 200, "delayMs": 0, "easing": "ease-out" },
{ "property": "transform", "durationMs": 200, "delayMs": 0, "easing": "cubic-bezier(0.22, 1, 0.36, 1)" }
] }
] },
{ "id": "dropdown:close", "phase": "close", "label": "Close", "members": [
{ "id": "panel", "label": "Panel", "selector": ".dropdown-panel", "toState": ".is-closing",
"propertyTimings": [
{ "property": "opacity", "durationMs": 150, "delayMs": 0, "easing": "ease-in" }
] }
] }
] }
]
}'
If you can't confidently group anything, post {"groups":[],"summary":"…"} —
the panel keeps its flat DOM scan. Reserve /jobs/<id>/error for unexpected
failures.
Then go back to step 1 of the loop.
Apply jobs (write to source)
When a claimed job has request.kind === "apply", the user accepted their current
timeline values and wants them written to the codebase. The request looks like:
{
"id": "uuid",
"request": {
"kind": "apply",
"label": "Dropdown · Close",
"selector": ".dropdown-panel",
"component": "src/Dropdown.tsx",
"group": "Dropdown",
"phase": "close",
"changes": [
{ "property": "opacity", "member": "Panel", "selector": ".dropdown-panel",
"from": { "durationMs": 300, "delayMs": 0, "easing": "ease" },
"to": { "durationMs": 150, "delayMs": 0, "easing": "cubic-bezier(0.4, 0, 1, 1)" } }
]
}
}
Do this:
Locate the real declaration in the source. The selector is a DOM-path
hint, not necessarily the source selector. Use the component hint and search
by the label/class names; handle whatever the project uses: plain CSS / CSS
Modules, styled-components or emotion template literals, Tailwind utilities
(duration-300, arbitrary [transition-duration:300ms], or the
tailwind.config theme), inline style={{ transition: … }} objects, and
Motion/Framer variants. Match by the from values to disambiguate.
- If
phase is set (e.g. "open"/"close"), edit only that state's rule
(the .is-open rule for open, the .is-closing/base rule for close) — not
the other phase. Each change's member + selector says which element.
Edit each change's property to its to values (durationMs ms, easing,
delayMs ms) on the right member + phase. Keep the file's existing unit/format
(0.25s vs 250ms) and touch only that property's timing. If a CSS variable /
design token backs the value, update it at the single most sensible place.
Minimal edit — no reformatting or unrelated changes.
Post the outcome (this completes the job):
curl -s -X POST http://localhost:7331/jobs/<id>/result \
-H 'Content-Type: application/json' \
-d '{"applied":true,"summary":"Set .t-modal transition to 150ms ease-in","files":["src/Modal.css:42"]}'
If you cannot confidently find the declaration, post
{"applied":false,"summary":"<what you searched and why not found>"} (still a
result, not an error). Reserve /jobs/<id>/error for unexpected failures.
Then go back to step 1 of the loop.
Suggestion shape (must match the panel)
Each suggestion object:
| field |
meaning |
id |
unique within the job (e.g. "width-duration") — used to track "Applied" |
kind |
"duration" | "delay" | "easing" for token tweaks, or "replace" for a whole-transition swap (drives the card label) |
property |
the CSS property this targets, or "all" |
title |
short label shown on the card |
from / to |
human-readable before → after |
patch |
what actually gets applied — { "property", "durationMs"?, "delayMs"?, "easing"? }. Include only changed fields; property must match an input property (or "all"). For a replace, use the chosen recipe's recommended timing here so Apply still does something live. |
reference |
(replace only, optional) the transitions.dev reference file the user should paste for the full recipe, e.g. "transitions-dev/06-modal.md". |
reason |
one sentence of why, in usage terms |
The panel applies patch live in the browser via the property override. Values
are not written to source files — the user copies the ones they keep.
Notes
- Relay port:
http://localhost:7331 unless REFINE_RELAY_PORT was changed.
- Only LLM-mode jobs reach you; Deterministic-mode jobs are answered by
the relay itself (nearest-token snapping) and never appear here. Whole-transition
replace suggestions are therefore LLM-only — the deterministic path can't
infer usage well enough to pick a recipe, so a Deterministic + "Replace
transition" job just returns an empty result pointing the user back to the Agent
tab.
- A
replace card's Apply only changes the live timing in the patch. The recipe's
structural parts (keyframes, extra properties, JS hooks) aren't applied in the
browser — that's why the card points the user at the reference file to paste.
- The relay errors a waiting job after ~120s, so answer promptly once you claim
one. The long-poll itself returning
204 is normal — just poll again.
1---2name: refine-live3description: In-chat fallback for the Timeline Inspector Refine agent. Use when the user runs `/refine live`, asks to "refine live", "go live", or answer refine jobs — but ONLY when no persistent agent is wired (no `npx transitions-refine live`). Prefer `npx transitions-refine live` for run-and-forget (relay spawns agent per click, no idle credit burn). This skill long-polls the relay, posts suggestions, handles scan/apply jobs.4---567# Refine Live89## Two modes1011**Persistent (recommended — run and forget)** 12Run `npx transitions-refine live` from your project. The CLI starts the relay and wires `REFINE_AGENT_CMD` so the relay spawns your agent CLI **per Refine click**. No chat loop; idle = zero credit burn. Works hours later as long as the relay process keeps running. Stop with Ctrl-C (or `npx transitions-refine stop`).1314**In-chat loop (fallback — this skill)** 15Run `/refine live` in Cursor/Claude/Codex when the relay is up but has **no** `REFINE_AGENT_CMD`. **You** become the poller via `GET /jobs/next`. The Agent tab stays available only while you keep polling — **each idle poll cycle consumes chat turns/credits**. Say "stop refine" to exit.1617Use the in-chat loop only when you cannot wire a persistent agent CLI.1819---2021Turn yourself into the LLM behind the Timeline Inspector's **Refine** button (**in-chat fallback mode**). While22this loop runs, the panel's **LLM** tab is "available": each click sends one23transition here, you reason about it, and your suggestions appear in the panel.2425You are the poller. Nothing is installed — you just talk to a small local relay26(default `http://localhost:7331`) that the `npx` injector already started.2728## How it works2930```31Browser (Refine, LLM tab) ──POST /jobs──► relay ──GET /jobs/next──► YOU32 ◄──GET /jobs/:id── relay ◄──POST /jobs/:id/result── YOU33```3435## The loop — stay live, but don't burn credits forever3637Keep polling so the panel's LLM tab stays "available", but this loop costs chat38turns/credits even while idle, so it is **not** truly run-and-forget — it has39three exits, in priority order:40411. **Relay stop signal (authoritative).** `GET /jobs/next` may return `200` with42 `{"stop": true}`. The relay sends this when the user clicks **Stop** in the43 panel, or automatically after ~10 min with no jobs. **Always honor it: stop44 looping immediately**, tell the user the LLM tab will go unavailable and how to45 resume (`/refine live`), and end your turn. Never re-poll after a stop signal.462. **The user says so** — "stop refine", "exit live", etc.473. **Your own idle backoff (safety net).** A long stretch of `204`s is normal —48 it just means no one has clicked Refine yet — but to avoid spending credits on49 a forgotten loop, **back off as idle grows** instead of hammering immediately:50 re-poll right away for the first few empty cycles, then pause ~5s between polls,51 and after ~10 min of unbroken idle stop on your own (same as the relay's52 auto-stop) and tell the user how to resume. Any real job resets the backoff.5354The relay reports the agent as "available" for ~120s after your last poll, so55short pauses keep you live. A successful job always resets idle, so an active56session never backs off.57580. **Announce yourself once, before the first poll.** The relay keeps a *sticky*59 Stop latch: after a panel **Stop** (or the idle auto-stop) it answers every60 `GET /jobs/next` with `{"stop": true}` until a new agent explicitly resumes —61 so a stopped session can't silently come back. Clear the latch a single time62 at startup, then begin polling:6364 ```bash65 curl -s -X POST http://localhost:7331/poller/start66 ```6768 Do **not** call this again mid-loop (it would defeat a user's Stop). Only on a69 fresh `/refine live`.70711. **Claim the next job (long-poll).** This call blocks up to ~25s, then returns.7273 ```bash74 curl -s http://localhost:7331/jobs/next75 ```7677 - HTTP `204` / empty body → no work yet. Poll again, applying the idle78 backoff above (immediate at first, then ~5s pauses, then stop after ~10 min).79 - HTTP `200` with `{"stop": true}` → **the loop must end.** Stop polling, tell80 the user the LLM tab is now unavailable and that `/refine live` resumes it,81 and end your turn. Do not treat it as a job.82 - HTTP `200` with a job JSON → work to do. Shape:8384 ```json85 {86 "id": "uuid",87 "request": {88 "label": "Resize + Color",89 "selector": ".box-resize",90 "mode": "llm",91 "refineType": "small",92 "timings": [93 { "property": "width", "durationMs": 400, "delayMs": 0, "easing": "ease-out" },94 { "property": "background", "durationMs": 400, "delayMs": 0, "easing": "ease-out" }95 ]96 }97 }98 ```99100 - **If `request.kind === "scan"`** this is not a suggestion job — the panel is101 asking you to group the page's transitions by reading the source. Jump to102 [`## Scan jobs`](#scan-jobs-group-from-source) and return `groups` instead of103 suggestions.104 - **If `request.kind === "apply"`** this is not a suggestion job — the user105 pressed **Accept** to write changes to their code. Jump to106 [`## Apply jobs`](#apply-jobs-write-to-source) and edit the source instead of107 posting suggestions. Everything below (refineType, steps 3–4) is for the108 normal Refine flow.109 - `refineType` chooses what kinds of suggestions to make (it mirrors the110 panel's two tabs). The tabs scan **independently**, so answer only the one111 you were asked for:112 - `"small"` (or missing) → **Small refinements**: nudge the existing113 declarations toward the motion tokens **only** (step 3a). Do **not**114 propose a recipe swap here — that's the Replace tab's separate job.115 - `"replace"` → **Replace transition**: suggest a whole-transition recipe116 swap **only** (step 3b). Do **not** propose motion-token tweaks — skip117 step 3a entirely.1181192. **(Optional) post progress** so the panel shows what you're doing:120121 ```bash122 curl -s -X POST http://localhost:7331/jobs/<id>/status \123 -H 'Content-Type: application/json' \124 -d '{"message":"Matching to transitions.dev motion tokens…"}'125 ```1261273. **Answer in ONE shot — speed matters.** Each click should feel instant, so128 resolve the job from the data below plus what's already in this skill. Do **not**129 spawn subagents or run a broad codebase search, and do **not** open the130 transitions-dev `SKILL.md` — its tokens and decision rules are inlined here.131 - `refineType === "small"` → step 3a only, with **zero file reads**.132 - `refineType === "replace"` → step 3b only; open **at most the one** recipe133 reference file you choose.134135 First, infer each declaration's **usage** from `label` + `selector` (modal136 close, dropdown open, tooltip, badge, resize, color/theme change…). Match on137 **intent, not the nearest number**.138139 **3a. Motion-token tweaks (`refineType === "small"` only — no file reads).**140 Pick the token that fits the usage and propose a change **only where the current141 value actually differs**.142143 - **Durations:** 40ms Stagger (per-item offset) · 80ms Micro (tooltip delay,144 shake segment) · 150ms Quick (modal/dropdown close, text swap, tooltip145 appear) · 250ms Fast (icon swap, dropdown/modal open, tabs slide, page146 slide) · 350ms Medium (panel/toast close) · 400ms Slow (panel open, skeleton147 reveal, input clear) · 500ms Very slow (emphasis, badge appear, text reveal,148 success check).149 - **Default easing — "Smooth ease out":** `cubic-bezier(0.22, 1, 0.36, 1)`150 (modal/dropdown/panel open+close, page slide, resize, position change).151 - **Other on-grid easings — LEAVE UNCHANGED:** `ease-out` (tooltip),152 `ease-in-out` (icon/text swap, text reveal, skeleton reveal), `linear`153 (shimmer, pulse, spinner), `cubic-bezier(0.34, 1.36, 0.64, 1)` (badge pop),154 `cubic-bezier(0.34, 3.85, 0.64, 1)` (avatar return).155 - **Nudge toward Smooth ease out:** generic `ease`, `ease-in`, or any156 hand-rolled cubic-bezier()/linear() that isn't a token above.157 - **Scale / blur / distance** (the non-resting `transform` pre-scale or158 translate distance, and the `filter` pre-blur a lane carries): scale 0.96159 Large (modal) · 0.97 Medium (dropdown open) · 0.98 Small (tooltip) · 0.99160 Tiny (dropdown close); blur 2px (panel/icon/text swap, skeleton, number) ·161 3px (page slide, text reveal) · 8px (success check); distance 4px (text162 swap) · 6px (shake small) · 8px (badge, page slide, shake large) · 12px163 (text reveal) · 30px (check badge). Propose `kind:"scale"`/`"blur"`/164 `"distance"` (patch key `scale`/`blur`/`translate`, pass `translateVarName`165 through) only where a lane's value differs from its usage token.166 - **Polish add-on (optional):** if the project has the `transitions-polish`167 skill installed (`.agents/skills/transitions-polish/_refine-rules.md`,168 `.claude/…`, or `skills/…`), read that ONE file a single time and also apply169 its rules — open/close asymmetry (closes faster/quieter than opens), hover170 in fast / out soft, stagger totals under ~300ms, 80ms intent delays, and171 never delay a close. This is the only file read allowed on a Small job, and172 only when that skill is present.173174 **3b. Whole-transition recipe swap (`refineType === "replace"` only — no file175 reads).** Match the inferred usage to ONE recipe below (this list *is* the176 decision rules — no SKILL.md or reference-file read needed). Emit ONE177 `kind: "replace"` suggestion whose `patch` carries the **motion-token**178 duration/easing for the recipe's phase (open vs close) on the property that179 already transitions (or `"all"`), with a `reference` field naming the file and180 the recipe in `title` + `reason`. The patch only drives the live preview —181 exact keyframes/structure come from the user pasting that reference file, so you182 never need to open it. If no recipe genuinely fits the usage, return an183 **empty** `suggestions` array with a short `summary`.184185 - Card resize — a container changes width/height on a layout change (`01-card-resize.md`)186 - Number pop-in — a number/digit updates (`02-number-pop-in.md`)187 - Notification badge — a small dot/badge appears on a trigger (`03-notification-badge.md`)188 - Text states swap — text content changes in place (`04-text-states-swap.md`)189 - Menu dropdown — an anchored surface grows from its trigger (`05-menu-dropdown.md`)190 - Modal open/close — a centered dialog scales up, softer scale-down on close (`06-modal.md`)191 - Panel reveal — a surface slides into a region with a cross-blur (`07-panel-reveal.md`)192 - Page side-by-side — slide between list↔detail or step 1↔step 2 (`08-page-side-by-side.md`)193 - Icon swap — two icons cross-fade in the same slot (`09-icon-swap.md`)194 - Success check — a checkmark celebration: fade + rotate + bob + stroke-draw (`10-success-check.md`)195 - Avatar group hover — hover lifts an item in a horizontal stack (`11-avatar-group-hover.md`)196 - Error state shake — invalid-input shake (`12-error-state-shake.md`)197 - Input clear with dissolve — clearing a text field (`13-input-clear-dissolve.md`)198 - Skeleton loader and reveal — placeholder pulses then swaps to real content (`14-skeleton-reveal.md`)199 - Shimmer text — in-progress / "thinking" text shimmer (`15-shimmer-text.md`)200 - Tabs sliding — a moving highlight across segmented options (`16-tabs-sliding.md`)201 - Tooltip open/close — delayed fade+scale in, instant out (`17-tooltip.md`)202 - Texts reveal — staggered blurred rise of stacked text lines (`18-texts-reveal.md`)203 - Card hover tilt — 3D tilt toward the pointer (`19-card-tilt.md`)204 - Plus to menu morph — a circular trigger becomes the surface it opens (`20-plus-menu-morph.md`)205 - Accordion expand — a collapsible body grows/shrinks in height (`21-accordion.md`)206207 Tie-break: prefer the lower-overhead recipe (card resize over panel reveal,208 dropdown over modal). Only propose a swap when the current declarations are209 clearly a hand-rolled version of a recipe or are missing the structure the usage210 calls for; if the transition already *is* the right recipe, return empty.2112124. **Post the result** (this completes the job and renders cards in the panel):213214 ```bash215 curl -s -X POST http://localhost:7331/jobs/<id>/result \216 -H 'Content-Type: application/json' \217 -d '{218 "summary": "Tightened the resize and softened the color fade.",219 "suggestions": [220 {221 "id": "width-duration",222 "kind": "duration",223 "property": "width",224 "title": "Duration → Snappy (250ms)",225 "from": "400ms",226 "to": "250ms",227 "patch": { "property": "width", "durationMs": 250 },228 "reason": "A size change reads as direct manipulation — snappy is more responsive than 400ms."229 }230 ]231 }'232 ```233234 The example above is a `small` job (token tweaks only). A `replace` job instead235 returns a single `kind: "replace"` card as its **only** suggestion:236237 ```json238 {239 "id": "replace-card-resize",240 "kind": "replace",241 "property": "width",242 "title": "Replace with Card resize",243 "from": "hand-rolled width tween",244 "to": "transitions.dev · Card resize",245 "patch": { "property": "width", "durationMs": 250, "easing": "cubic-bezier(0.22, 1, 0.36, 1)" },246 "reference": "transitions-dev/01-card-resize.md",247 "reason": "This is a width tween on layout change — the Card resize recipe handles it properly. Apply nudges the live timing; paste 01-card-resize.md (run `transitions apply card-resize`) for the full recipe."248 }249 ```250251 If nothing should change, post `"suggestions": []` with a short `summary`.252 If something goes wrong, report it instead:253254 ```bash255 curl -s -X POST http://localhost:7331/jobs/<id>/error \256 -H 'Content-Type: application/json' -d '{"message":"…"}'257 ```2582595. **Go back to step 1.** Keep looping, but honor the three exits from260 [the loop section](#the-loop--stay-live-but-dont-burn-credits-forever): a261 `{"stop": true}` from the relay, the user telling you to stop, or your own idle262 backoff/auto-stop after ~10 min quiet. A real job resets idle. Whenever you do263 stop, tell them the LLM tab will go unavailable and how to restart264 (`/refine live`).265266## Scan jobs (group from source)267268When a claimed job has `request.kind === "scan"`, the panel wants you to turn a269flat list of DOM-detected transitions into **components with phases**. A naive270DOM scan only sees each element's *current* computed transition — it can't tell271open from close, and lists related elements (panel, backdrop, staggered items)272separately. You fix that by reading the source. The request looks like:273274```json275{276 "id": "uuid",277 "request": {278 "kind": "scan",279 "url": "http://localhost:5173/",280 "raw": [281 { "label": "div.dropdown-panel", "selector": ".dropdown-panel",282 "properties": ["opacity","transform"],283 "timings": [{ "property": "opacity", "durationMs": 200, "delayMs": 0, "easing": "ease-out" }],284 "cssRules": [285 ".dropdown .dropdown-panel { opacity: 0; transition: opacity 200ms ease-out 0ms, transform 200ms cubic-bezier(0.22, 1, 0.36, 1) 0ms; }",286 ".dropdown.is-open .dropdown-panel { opacity: 1; transform: translateY(0); }",287 ".dropdown.is-closing .dropdown-panel { transition: opacity 150ms ease-in 0ms; opacity: 0; }"288 ] }289 ]290 }291}292```293294**Be fast.** The `raw.timings` are already accurate for each element's *current*295on-screen state — treat them as ground truth and reuse them verbatim. Most `raw`296entries also carry **`cssRules`**: the CSS rules harvested live from the page297(CSSOM) that drive that element across *all* states (base + open + close), with298`var()` already resolved to concrete values.299300**Fast path — prefer `cssRules` over the filesystem.** When an entry has301`cssRules`, they are authoritative and contain everything you need: the opposite302phase's timings live on a state-variant selector inside them (e.g.303`.dd.is-closing .dd-panel`, `.modal[data-closing] .dialog`), and the toggled304state is visible in those selectors. Derive grouping, phases, toggled state, and305opposite-phase timings **directly from `cssRules` + `timings`** — do **not**306glob/grep/read files for any element whose `cssRules` is non-empty; it only307wastes time. Only fall back to reading source for entries with an empty/missing308`cssRules` (CORS-locked sheets, styled-components, Tailwind, etc.), and even then309read the minimum.310311Do this:3123131. **Identify each animated component** the raw entries belong to (dropdown,314 modal, tooltip, accordion, drawer, toast…). The selectors/labels usually make315 this obvious — only read source (plain CSS / CSS Modules,316 styled-components/emotion, Tailwind, inline styles, Motion/Framer variants)317 when the grouping is genuinely unclear.3182. **Split each component into phases** — usually `open` and `close` (a hover-only319 component can be a single phase). The phase matching the current DOM reuses the320 provided timings; the *opposite* phase often lives on a different selector321 (`.is-open` vs `.is-closing`) with different timings — take it from the entry's322 `cssRules` (or, only if it has none, read source). Report **both** even though323 only one is in the DOM right now.3243. **List each phase's members** — the elements that animate in that phase. Give325 each a stable `id`, a human `label`, a live-resolvable CSS `selector`, an326 optional `toState` hint (the class/attribute that drives the phase, e.g.327 `.is-open`), and its `propertyTimings`. For the current-state phase, **copy the328 provided `raw.timings` verbatim**; for the opposite phase, **quote the real329 timings from the entry's `cssRules`** (already var()-resolved) — or from source330 if it has none — **never invent.**3314. **Post the groups** (this completes the job):332333 ```bash334 curl -s -X POST http://localhost:7331/jobs/<id>/result \335 -H 'Content-Type: application/json' \336 -d '{337 "summary": "Grouped Dropdown into Open/Close.",338 "groups": [339 { "id": "dropdown", "label": "Dropdown", "component": "src/Dropdown.tsx",340 "phases": [341 { "id": "dropdown:open", "phase": "open", "label": "Open", "members": [342 { "id": "panel", "label": "Panel", "selector": ".dropdown-panel", "toState": ".is-open",343 "propertyTimings": [344 { "property": "opacity", "durationMs": 200, "delayMs": 0, "easing": "ease-out" },345 { "property": "transform", "durationMs": 200, "delayMs": 0, "easing": "cubic-bezier(0.22, 1, 0.36, 1)" }346 ] }347 ] },348 { "id": "dropdown:close", "phase": "close", "label": "Close", "members": [349 { "id": "panel", "label": "Panel", "selector": ".dropdown-panel", "toState": ".is-closing",350 "propertyTimings": [351 { "property": "opacity", "durationMs": 150, "delayMs": 0, "easing": "ease-in" }352 ] }353 ] }354 ] }355 ]356 }'357 ```358359 If you can't confidently group anything, post `{"groups":[],"summary":"…"}` —360 the panel keeps its flat DOM scan. Reserve `/jobs/<id>/error` for unexpected361 failures.362363Then go back to step 1 of the loop.364365## Apply jobs (write to source)366367When a claimed job has `request.kind === "apply"`, the user accepted their current368timeline values and wants them written to the codebase. The request looks like:369370```json371{372 "id": "uuid",373 "request": {374 "kind": "apply",375 "label": "Dropdown · Close",376 "selector": ".dropdown-panel",377 "component": "src/Dropdown.tsx",378 "group": "Dropdown",379 "phase": "close",380 "changes": [381 { "property": "opacity", "member": "Panel", "selector": ".dropdown-panel",382 "from": { "durationMs": 300, "delayMs": 0, "easing": "ease" },383 "to": { "durationMs": 150, "delayMs": 0, "easing": "cubic-bezier(0.4, 0, 1, 1)" } }384 ]385 }386}387```388389Do this:3903911. **Locate the real declaration in the source.** The `selector` is a DOM-path392 *hint*, not necessarily the source selector. Use the `component` hint and search393 by the label/class names; handle whatever the project uses: plain CSS / CSS394 Modules, styled-components or emotion template literals, Tailwind utilities395 (`duration-300`, arbitrary `[transition-duration:300ms]`, or the396 `tailwind.config` theme), inline `style={{ transition: … }}` objects, and397 Motion/Framer variants. Match by the `from` values to disambiguate.398 - **If `phase` is set** (e.g. `"open"`/`"close"`), edit only that state's rule399 (the `.is-open` rule for open, the `.is-closing`/base rule for close) — not400 the other phase. Each change's `member` + `selector` says which element.4012. **Edit each change's property** to its `to` values (`durationMs` ms, `easing`,402 `delayMs` ms) on the right member + phase. Keep the file's existing unit/format403 (`0.25s` vs `250ms`) and touch only that property's timing. If a CSS variable /404 design token backs the value, update it at the single most sensible place.4053. **Minimal edit** — no reformatting or unrelated changes.4064. **Post the outcome** (this completes the job):407408 ```bash409 curl -s -X POST http://localhost:7331/jobs/<id>/result \410 -H 'Content-Type: application/json' \411 -d '{"applied":true,"summary":"Set .t-modal transition to 150ms ease-in","files":["src/Modal.css:42"]}'412 ```413414 If you cannot confidently find the declaration, post415 `{"applied":false,"summary":"<what you searched and why not found>"}` (still a416 `result`, not an `error`). Reserve `/jobs/<id>/error` for unexpected failures.417418Then go back to step 1 of the loop.419420## Suggestion shape (must match the panel)421422Each suggestion object:423424| field | meaning |425| --- | --- |426| `id` | unique within the job (e.g. `"width-duration"`) — used to track "Applied" |427| `kind` | `"duration"` \| `"delay"` \| `"easing"` for token tweaks, or `"replace"` for a whole-transition swap (drives the card label) |428| `property` | the CSS property this targets, or `"all"` |429| `title` | short label shown on the card |430| `from` / `to` | human-readable before → after |431| `patch` | **what actually gets applied** — `{ "property", "durationMs"?, "delayMs"?, "easing"? }`. Include only changed fields; `property` must match an input property (or `"all"`). For a `replace`, use the chosen recipe's recommended timing here so Apply still does something live. |432| `reference` | *(replace only, optional)* the transitions.dev reference file the user should paste for the full recipe, e.g. `"transitions-dev/06-modal.md"`. |433| `reason` | one sentence of *why*, in usage terms |434435The panel applies `patch` live in the browser via the property override. Values436are not written to source files — the user copies the ones they keep.437438## Notes439440- Relay port: `http://localhost:7331` unless `REFINE_RELAY_PORT` was changed.441- Only **LLM**-mode jobs reach you; **Deterministic**-mode jobs are answered by442 the relay itself (nearest-token snapping) and never appear here. Whole-transition443 **replace** suggestions are therefore LLM-only — the deterministic path can't444 infer usage well enough to pick a recipe, so a Deterministic + "Replace445 transition" job just returns an empty result pointing the user back to the Agent446 tab.447- A `replace` card's Apply only changes the live timing in the patch. The recipe's448 structural parts (keyframes, extra properties, JS hooks) aren't applied in the449 browser — that's why the card points the user at the reference file to paste.450- The relay errors a waiting job after ~120s, so answer promptly once you claim451 one. The long-poll itself returning `204` is normal — just poll again.