Files
pbakaus_impeccable/docs/issue-150-svelte-stuck-bar-handoff.md
T
Abdul WahabandGitHub 6163ca0529 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
2026-06-02 00:08:57 -07:00

10 KiB

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.sveltevN.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)

// page console
Object.keys(localStorage).filter(k => k.startsWith('impeccable-live')).forEach(k => localStorage.removeItem(k));
location.reload();
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.