mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-21 02:26:31 +03:00
Add Svelte-native live mode adapter (#179)
* Fix live preview state for framework components * Complete stateful live preview coverage * Record Svelte manual validation * Fix Svelte live mode adapter * Fix live Steer apply flow * Fix Svelte live variant refresh recovery * Fix live exit bar teardown * Consolidate Svelte live DeepSeek sweep * Reconcile Svelte live browser after main rebase * Fix live accept review regressions * Fix carbonize column-zero indentation * Fix live poll lease expiry flake * Fix Svelte shader preview capture
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
# Issue 150 Live Preview Plan
|
||||
|
||||
## Current Bug Summary
|
||||
|
||||
Live preview can lose framework state when variants are written directly into watched component source. The Svelte reproduction is a stateful expense row: after adding an expense, generating variants for the row should not reset the component or render raw Svelte expressions such as `{expenses[0].name}`.
|
||||
|
||||
The current branch uses **Svelte component injection** for `.svelte` targets: variants are real components under `src/lib/impeccable/<id>/`, mounted in the browser via Svelte 5 `mount()`, and inlined back into the route source on live exit. Accept keeps the mounted component visible immediately while deferring the route write until `live-server stop`.
|
||||
|
||||
## Manual test apps (home directory)
|
||||
|
||||
Stateful framework repros live outside this repo:
|
||||
|
||||
- **Svelte:** `~/impeccable-live-svelte` (see its `README.md`)
|
||||
- **React:** `~/impeccable-live-react` (see its `README.md`)
|
||||
|
||||
Each app includes a copied `.cursor/skills/impeccable` build from the local impeccable branch for live-server / inject / poll.
|
||||
|
||||
## Current Status
|
||||
|
||||
- Svelte component-injection Accept fix is implemented.
|
||||
- Svelte manual pass with the user is complete (`~/impeccable-live-svelte`).
|
||||
- React manual user check and the DeepSeek-backed final run are still pending (`~/impeccable-live-react`).
|
||||
- Focused live tests, build, and full test suite should pass after harness refresh.
|
||||
|
||||
## Svelte Fix Plan
|
||||
|
||||
- Keep component injection scoped to `.svelte` targets.
|
||||
- Extract `propContract` from the picked route markup and author variants as real `.svelte` files with `{propName}` bindings.
|
||||
- Mount compiled variants in the browser with the app's shared Svelte runtime.
|
||||
- On Accept, keep the chosen mounted variant visible immediately.
|
||||
- Defer the real route source inline until live shutdown to avoid accept-time remounts.
|
||||
- On live shutdown, inline accepted markup + CSS into the route and remove temp component files.
|
||||
- Keep the connected indicator stable while an event is leased or actively being handled.
|
||||
|
||||
## React Parity Test Plan
|
||||
|
||||
- Same shape as the Svelte case in `~/impeccable-live-react`.
|
||||
- Run the same Go, cycle, Accept flow against the React row.
|
||||
- React already uses direct source wrap + Fast Refresh; keep that path unchanged unless manual testing shows state loss.
|
||||
|
||||
## Validation Checklist (impeccable repo)
|
||||
|
||||
- [ ] `node --test tests/live-browser-regression.test.mjs tests/live-accept.test.mjs tests/live-poll.test.mjs tests/live-server.test.mjs tests/live-svelte-component.test.mjs`
|
||||
- [ ] Manual Svelte run in `~/impeccable-live-svelte`
|
||||
- [ ] `bun run build`
|
||||
- [ ] `bun run test`
|
||||
|
||||
The PR should remain draft until the React manual check in `~/impeccable-live-react` passes.
|
||||
@@ -0,0 +1,132 @@
|
||||
# Handoff: Svelte live mode bar stuck at `0/0` (CYCLING with no variants)
|
||||
|
||||
Date: 2026-05-30
|
||||
Branch: `codex/issue-150-svelte-live`
|
||||
Status: **Unresolved.** Two rounds of fixes landed and were synced to the manual test repo, but the symptom still reproduces.
|
||||
|
||||
---
|
||||
|
||||
## Symptom
|
||||
|
||||
In `~/impeccable-live-svelte`, after running a live action (Polish/Bolder) on the expense row, the floating live bar shows the **CYCLING** layout (prev/next arrows, two faint dots, `✓ Accept`, `✕`) but the counter reads `0/0` and every control is disabled (`opacity: 0.3`, `pointer-events: none`). No variant is mounted. The bar persists across reloads.
|
||||
|
||||
Captured DOM (trimmed): `<div id="impeccable-live-bar">` … `<span>0/0</span>` … `✓ Accept` (disabled) `✕`. Full markup is in the chat history if needed.
|
||||
|
||||
`0/0` = `visibleVariant=0 / arrivedVariants=0`. The two dots = `expectedVariants=2` rendered as "pending" (see `buildDots`, both unfilled because `arrivedVariants=0`).
|
||||
|
||||
---
|
||||
|
||||
## Context: what this feature is
|
||||
|
||||
Issue 150 replaced the Svelte "source-shadow" live preview with **real component injection**. See `docs/issue-150-live-preview-plan.md` and `skill/reference/live.md` (the `svelte-component` paragraph). Key pieces:
|
||||
|
||||
- `skill/scripts/live-svelte-component.mjs` — scaffolds `src/lib/impeccable/<id>/` with `manifest.json`, `v1.svelte`…`vN.svelte`, and a one-time `__runtime.js`. Inlines the accepted variant back into the route on exit.
|
||||
- `skill/scripts/live-wrap.mjs` — `.svelte` targets return `previewMode: "svelte-component"`, `file` = manifest path.
|
||||
- `skill/scripts/live-browser.js` — mounts compiled variants via Svelte 5 `mount()` into a `display:contents` slot that replaces the original element. Cycling = unmount + remount.
|
||||
- `skill/scripts/live-accept.mjs` / `live-server.mjs` — defer the route source write to `live-server stop`.
|
||||
|
||||
The Svelte wrapper in the live DOM is **runtime-injected** and holds a single mount target (`[data-impeccable-component-mount]`), **not** `[data-impeccable-variant]` children like the HTML/JSX path. This distinction is the source of most of the trouble.
|
||||
|
||||
---
|
||||
|
||||
## What has already been fixed (and synced to the test repo)
|
||||
|
||||
All in `skill/scripts/live-browser.js`, rebuilt via `bun run build:skills` and rsync'd to `~/impeccable-live-svelte/.cursor/skills/impeccable/`.
|
||||
|
||||
1. **Params sidecar (the original compile error).** Svelte parses `{` inside an attribute value as an expression, so `data-impeccable-params='[{…}]'` broke compilation (`Expected token }`). Params now load from `componentDir/params.json` keyed by variant number:
|
||||
- `loadSvelteComponentParams(manifest)` fetches `params.json`.
|
||||
- `parseVariantParams()` reads from `svelteComponentSession.paramsByVariant` for the component path instead of the DOM attribute.
|
||||
- Agent contract updated in `live-svelte-component.mjs` (`buildSvelteComponentCssAuthoring`) and `skill/reference/live.md`.
|
||||
|
||||
2. **Resume guard.** `resumeSession()` now drops an orphaned `svelte-component` wrapper (no live in-memory mount) instead of resuming it into an empty bar. Without this, every reload resumed `arrivedVariants=0`.
|
||||
|
||||
3. **Abort-on-failure.** New `abortSvelteComponentInjection(sessionId, message)` resets the bar to PICKING (restores the original element, clears session, toast) when the picked element can't be found OR the initial `mountSvelteComponentVariant` returns false (compile/mount throw). Called from `injectSvelteComponentsFromManifest`.
|
||||
|
||||
Tests added in `tests/live-browser-regression.test.mjs` (all green): orphan reset, abort helper, sidecar params. Full focused suite passes: `node --test tests/live-browser-regression.test.mjs tests/live-svelte-component.test.mjs tests/live-accept.test.mjs`.
|
||||
|
||||
---
|
||||
|
||||
## Why it probably STILL reproduces (hypotheses, in priority order)
|
||||
|
||||
### H1. The served `live.js` is stale (verify FIRST)
|
||||
The browser loads `live.js` from the running live-server, not from disk directly. If the server wasn't restarted, or the `<script src=".../live.js">` is cached, the page is still running the OLD code without the abort/resume guards.
|
||||
- **Check:** in the page console, search the loaded script for `abortSvelteComponentInjection` / `loadSvelteComponentParams`. If absent, the fix isn't loaded.
|
||||
- **Fix:** `live-server.mjs --stop` then `--background`, hard-reload (Cmd+Shift+R), reload Cursor.
|
||||
- Confirm the test repo copy is current: `grep -l abortSvelteComponentInjection ~/impeccable-live-svelte/.cursor/skills/impeccable/scripts/live-browser.js`.
|
||||
|
||||
### H2. The variant still fails to compile, and a path other than `injectSvelteComponentsFromManifest` sets CYCLING
|
||||
The abort only fires inside `injectSvelteComponentsFromManifest`. If the agent-authored `v1.svelte` still has a compile error (another `{` in an attribute, a bad expression, etc.), the mount fails. Confirm the abort path is actually reached:
|
||||
- Look for the console line `[impeccable] Failed to mount Svelte variant N` and the toast.
|
||||
- If CYCLING is being set somewhere else, audit every `updateBarContent('cycling')` / `state = 'CYCLING'` site (grep finds ~12) for one that runs with `arrivedVariants===0`.
|
||||
|
||||
### H3. Server-side event replay re-injects after reset
|
||||
`live-server.mjs` redelivers unacknowledged events and persists a session journal. After the browser aborts to PICKING, a redelivered `done` event (SSE reconnect, or the agent reply not acked) could re-trigger injection. The `done` handler (`live-browser.js` ~line 4868) only injects when `state === 'GENERATING'`, so a clean PICKING state should be safe — but verify the state at the moment of replay. The durable session may also need clearing: check `.impeccable/live/sessions/` and `.impeccable/live/deferred-svelte-component-accepts.json` in the test repo.
|
||||
|
||||
### H4. Stale localStorage session keeps re-priming the bar
|
||||
The bar persists `state`/`id`/`visible` to localStorage (keys prefixed `impeccable-live`). A reload can rehydrate CYCLING before any wrapper exists.
|
||||
- **Check:** `Object.keys(localStorage).filter(k=>k.startsWith('impeccable-live'))` in the console.
|
||||
- **Clear:** remove those keys and reload (see "Reset procedure" below).
|
||||
|
||||
### H5. Defect-class root cause: empty CYCLING is representable at all
|
||||
The deepest fix is to make `0/0 CYCLING` an impossible state. A cheap, robust guard: in the cycling render path (`buildCyclingRow` / wherever `updateBarContent('cycling')` resolves), if `arrivedVariants === 0`, refuse to render CYCLING and fall back to PICKING (or hide). That self-heals regardless of which upstream path is buggy. Consider adding this as belt-and-suspenders even after the specific path is found.
|
||||
|
||||
---
|
||||
|
||||
## Reset procedure (clears the currently-stuck bar)
|
||||
|
||||
```js
|
||||
// page console
|
||||
Object.keys(localStorage).filter(k => k.startsWith('impeccable-live')).forEach(k => localStorage.removeItem(k));
|
||||
location.reload();
|
||||
```
|
||||
|
||||
```bash
|
||||
cd ~/impeccable-live-svelte
|
||||
node .cursor/skills/impeccable/scripts/live-server.mjs --stop
|
||||
node .cursor/skills/impeccable/scripts/live-server.mjs --background
|
||||
# also clear any orphaned session state if H3 suspected:
|
||||
rm -rf .impeccable/live/sessions/* .impeccable/live/deferred-svelte-component-accepts.json
|
||||
rm -rf src/lib/impeccable/*/ # leftover variant component dirs (keep __runtime.js)
|
||||
```
|
||||
Reload Cursor so `/impeccable` picks up the synced skill.
|
||||
|
||||
---
|
||||
|
||||
## Repro
|
||||
|
||||
1. `cd ~/impeccable-live-svelte && npm run dev`
|
||||
2. New terminal: `node .cursor/skills/impeccable/scripts/live-server.mjs --background`, note the port.
|
||||
3. `node .cursor/skills/impeccable/scripts/live-inject.mjs --port <port>` then `node .cursor/skills/impeccable/scripts/live-poll.mjs`.
|
||||
4. In the browser: add an expense, pick the expense row, run Polish/Bolder, let variants generate.
|
||||
5. Observe whether the bar reaches `1/3` (good) or `0/0` (the bug).
|
||||
6. With `IMPECCABLE_E2E_DEBUG`-style logging: open devtools console and watch for `[impeccable]` lines during step 4.
|
||||
|
||||
The expense row source is `src/routes/+page.svelte` lines ~34-37 (`<article class="expense-row …">{expenses[0].name}…`). `propContract` derives `name` and `amount`.
|
||||
|
||||
---
|
||||
|
||||
## Suggested investigation order for tomorrow
|
||||
|
||||
1. **Confirm the new code is actually live** (H1) — single biggest time-saver. Grep the loaded script + the test-repo file for `abortSvelteComponentInjection`.
|
||||
2. With confirmed-fresh code, reproduce and **capture the console**. Determine whether `injectSvelteComponentsFromManifest` runs and whether `mountSvelteComponentVariant` throws. Read the actual `src/lib/impeccable/<id>/v1.svelte` the agent wrote — is it valid Svelte?
|
||||
3. If mount throws: the agent is still authoring an invalid component. Tighten the `live.md` contract / scaffold stub, OR make the inline-accept/scaffold validate. Capture the exact error.
|
||||
4. If mount succeeds but bar still `0/0`: trace which `state='CYCLING'` site runs with `arrivedVariants=0` (H2) and add the H5 guard.
|
||||
5. Add an E2E regression once root cause is known. Note `tests/live-e2e.test.mjs` still references the old `source-shadow` markers for the Svelte fixture (lines ~311, ~891, ~918) — that opt-in suite needs updating to the component-injection model regardless.
|
||||
|
||||
---
|
||||
|
||||
## Key files / line anchors (as of this handoff)
|
||||
|
||||
| File | What |
|
||||
|---|---|
|
||||
| `skill/scripts/live-browser.js` ~4242 | `injectSvelteComponentsFromManifest` |
|
||||
| `skill/scripts/live-browser.js` (`abortSvelteComponentInjection`) | clean-reset helper |
|
||||
| `skill/scripts/live-browser.js` (`mountSvelteComponentVariant`) | dynamic import + `mount()` |
|
||||
| `skill/scripts/live-browser.js` (`loadSvelteComponentParams`, `parseVariantParams`) | sidecar params |
|
||||
| `skill/scripts/live-browser.js` (`resumeSession`) | orphan-wrapper guard |
|
||||
| `skill/scripts/live-browser.js` ~4868 | SSE `done` handler |
|
||||
| `skill/scripts/live-browser.js` (`buildCyclingRow`, `buildDots`, ~2102/2262) | bar render — candidate for H5 guard |
|
||||
| `skill/scripts/live-svelte-component.mjs` | scaffold / inline-accept / cssAuthoring contract |
|
||||
| `skill/reference/live.md` (`svelte-component` paragraph + Parameters §7) | agent contract |
|
||||
|
||||
Remember: source of truth is `skill/`. After any edit run `bun run build:skills`, then rsync to `~/impeccable-live-svelte/.cursor/skills/impeccable/`. Do not hand-edit the harness copies.
|
||||
Reference in New Issue
Block a user