quiver-cli 0.8.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -45
- package/bin/quiver-cli.mjs +3 -1
- package/dist/cli.js +666 -436
- package/package.json +2 -2
- package/template/.agents/AGENTS.md +4 -3
- package/template/.agents/config.json +7 -0
- package/template/.agents/plugins/opencode/rtk.ts +34 -0
- package/template/.agents/skills/agent-browser/SKILL.md +1 -0
- package/template/.agents/skills/apps/skybridge/SKILL.md +4 -0
- package/template/.agents/skills/design/impeccable/SKILL.md +36 -118
- package/template/.agents/skills/design/impeccable/reference/adapt.md +1 -0
- package/template/.agents/skills/design/impeccable/reference/adapt.native.md +58 -0
- package/template/.agents/skills/design/impeccable/reference/android.md +40 -0
- package/template/.agents/skills/design/impeccable/reference/animate.md +73 -188
- package/template/.agents/skills/design/impeccable/reference/audit.md +12 -9
- package/template/.agents/skills/design/impeccable/reference/audit.native.md +139 -0
- package/template/.agents/skills/design/impeccable/reference/bolder.md +19 -101
- package/template/.agents/skills/design/impeccable/reference/clarify.md +59 -253
- package/template/.agents/skills/design/impeccable/reference/colorize.md +51 -222
- package/template/.agents/skills/design/impeccable/reference/craft-floor.md +42 -0
- package/template/.agents/skills/design/impeccable/reference/craft.md +3 -121
- package/template/.agents/skills/design/impeccable/reference/critique.md +44 -23
- package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +90 -0
- package/template/.agents/skills/design/impeccable/reference/degraded/documenter.md +24 -0
- package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +37 -0
- package/template/.agents/skills/design/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/template/.agents/skills/design/impeccable/reference/delight.md +47 -279
- package/template/.agents/skills/design/impeccable/reference/distill.md +2 -2
- package/template/.agents/skills/design/impeccable/reference/doctor.md +53 -0
- package/template/.agents/skills/design/impeccable/reference/document.md +60 -73
- package/template/.agents/skills/design/impeccable/reference/harden.md +1 -12
- package/template/.agents/skills/design/impeccable/reference/hooks.md +20 -5
- package/template/.agents/skills/design/impeccable/reference/init.md +72 -119
- package/template/.agents/skills/design/impeccable/reference/ios.md +45 -0
- package/template/.agents/skills/design/impeccable/reference/layout.md +54 -131
- package/template/.agents/skills/design/impeccable/reference/live-setup.md +102 -0
- package/template/.agents/skills/design/impeccable/reference/live.md +116 -511
- package/template/.agents/skills/design/impeccable/reference/new-work.md +105 -0
- package/template/.agents/skills/design/impeccable/reference/{product.md → operate.md} +6 -5
- package/template/.agents/skills/design/impeccable/reference/optimize.md +4 -4
- package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -4
- package/template/.agents/skills/design/impeccable/reference/polish.md +68 -212
- package/template/.agents/skills/design/impeccable/reference/quieter.md +3 -3
- package/template/.agents/skills/design/impeccable/reference/routing.md +18 -0
- package/template/.agents/skills/design/impeccable/reference/shape.md +38 -144
- package/template/.agents/skills/design/impeccable/reference/typeset.md +51 -250
- package/template/.agents/skills/design/impeccable/reference/visualize.md +47 -0
- package/template/.agents/skills/design/impeccable/scripts/command-metadata.json +1 -1
- package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +558 -0
- package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +119 -10
- package/template/.agents/skills/design/impeccable/scripts/context.mjs +534 -45
- package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +18 -47
- package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
- package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +174 -26
- package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +233 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +3348 -203
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +297 -97
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +187 -16
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +41 -11
- package/template/.agents/skills/design/impeccable/scripts/detector/findings.mjs +7 -1
- package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +16 -2
- package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +207 -38
- package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +3082 -173
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +11 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/fonts.mjs +30 -0
- package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +336 -0
- package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +133 -0
- package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +240 -0
- package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +98 -18
- package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +46 -6
- package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +518 -50
- package/template/.agents/skills/design/impeccable/scripts/hook.mjs +25 -8
- package/template/.agents/skills/design/impeccable/scripts/lib/artifact-schema.mjs +93 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/composition-catalog.mjs +200 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +357 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +27 -7
- package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-paths.mjs +17 -8
- package/template/.agents/skills/design/impeccable/scripts/lib/provider.mjs +5 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +362 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +457 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness-notice.mjs +169 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +457 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +151 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/target-slug.mjs +33 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/template-extensions.mjs +146 -0
- package/template/.agents/skills/design/impeccable/scripts/live/accept-css.mjs +617 -0
- package/template/.agents/skills/design/impeccable/scripts/live/accept-verify.mjs +60 -0
- package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +7 -1
- package/template/.agents/skills/design/impeccable/scripts/live/completion.mjs +10 -1
- package/template/.agents/skills/design/impeccable/scripts/live/event-validation.mjs +67 -5
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/astro.mjs +47 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/detect-utils.mjs +73 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/index.mjs +143 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/journal.mjs +197 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nextjs.mjs +49 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nuxt.mjs +161 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/script-src.mjs +17 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/static-html.mjs +26 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/sveltekit.mjs +71 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tag-strategy.mjs +247 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tanstack-start.mjs +70 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/vite-generic.mjs +42 -0
- package/template/.agents/skills/design/impeccable/scripts/live/generation-preflight.mjs +149 -0
- package/template/.agents/skills/design/impeccable/scripts/live/instructions.mjs +142 -0
- package/template/.agents/skills/design/impeccable/scripts/live/poll-lanes.mjs +14 -0
- package/template/.agents/skills/design/impeccable/scripts/live/roots.mjs +508 -0
- package/template/.agents/skills/design/impeccable/scripts/live/session-store.mjs +324 -50
- package/template/.agents/skills/design/impeccable/scripts/live/source-lock.mjs +105 -0
- package/template/.agents/skills/design/impeccable/scripts/live/source-search.mjs +105 -0
- package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +961 -0
- package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +588 -72
- package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +59 -17
- package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
- package/template/.agents/skills/design/impeccable/scripts/live/vocabulary.mjs +135 -0
- package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +210 -68
- package/template/.agents/skills/design/impeccable/scripts/live-browser.js +1562 -223
- package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
- package/template/.agents/skills/design/impeccable/scripts/live-complete.mjs +33 -1
- package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +175 -255
- package/template/.agents/skills/design/impeccable/scripts/live-insert.mjs +26 -6
- package/template/.agents/skills/design/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
- package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +61 -16
- package/template/.agents/skills/design/impeccable/scripts/live-resume.mjs +39 -10
- package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +573 -47
- package/template/.agents/skills/design/impeccable/scripts/live-status.mjs +17 -7
- package/template/.agents/skills/design/impeccable/scripts/live-wrap.mjs +124 -91
- package/template/.agents/skills/design/impeccable/scripts/live.mjs +88 -26
- package/template/.agents/skills/design/impeccable/scripts/palette.mjs +76 -81
- package/template/.agents/skills/design/impeccable/scripts/pin.mjs +18 -11
- package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +932 -0
- package/template/.agents/skills/design/impeccable/scripts/surface-brief.mjs +74 -0
- package/template/.agents/skills/design/shadcn/SKILL.md +46 -11
- package/template/.agents/skills/design/shadcn/cli.md +49 -16
- package/template/.agents/skills/design/shadcn/customization.md +14 -7
- package/template/.agents/skills/design/shadcn/evals/evals.json +30 -0
- package/template/.agents/skills/design/shadcn/mcp.md +27 -16
- package/template/.agents/skills/design/shadcn/registry.md +277 -0
- package/template/.agents/skills/design/shadcn/rules/chat.md +224 -0
- package/template/.agents/skills/design/shadcn/rules/composition.md +20 -2
- package/template/.agents/skills/design/shadcn/rules/styling.md +23 -0
- package/template/.agents/skills/find-skills/SKILL.md +2 -3
- package/template/.agents/skills/hono/SKILL.md +579 -0
- package/template/.agents/skills/integrations/langfuse/SKILL.md +10 -6
- package/template/.agents/skills/integrations/langfuse/references/ci-cd.md +41 -0
- package/template/.agents/skills/integrations/langfuse/references/cli.md +8 -0
- package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +15 -27
- package/template/.agents/skills/integrations/langfuse/references/instrumentation.md +39 -52
- package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +5 -2
- package/template/.agents/skills/integrations/langfuse/references/prompt-engineering.md +35 -0
- package/template/.agents/skills/integrations/langfuse/references/prompt-migration.md +41 -196
- package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +3 -0
- package/template/.agents/skills/integrations/langfuse/references/trace-evaluator-upgrade.md +76 -0
- package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +4 -0
- package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +73 -0
- package/template/.agents/skills/supabase/CHANGELOG.md +71 -0
- package/template/.agents/skills/supabase/SKILL.md +145 -0
- package/template/.agents/skills/supabase/assets/feedback-issue-template.md +17 -0
- package/template/.agents/skills/supabase/references/skill-feedback.md +17 -0
- package/template/.agents/skills/supabase-postgres-best-practices/CHANGELOG.md +73 -0
- package/template/.agents/skills/supabase-postgres-best-practices/SKILL.md +64 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/_contributing.md +170 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/_sections.md +39 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/_template.md +34 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-performance.md +63 -0
- package/template/.agents/upstreams.json +34 -12
- package/template/.agents/skills/design/impeccable/reference/brand.md +0 -108
- package/template/.agents/skills/design/impeccable/reference/codex.md +0 -105
- package/template/.agents/skills/design/impeccable/reference/interaction-design.md +0 -189
- package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +0 -175
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
const TOKEN = window.__IMPECCABLE_TOKEN__;
|
|
23
23
|
const PORT = window.__IMPECCABLE_PORT__;
|
|
24
|
+
const APP_ROOT = window.__IMPECCABLE_APP_ROOT__ || null;
|
|
24
25
|
if (!TOKEN || !PORT) {
|
|
25
26
|
window.__IMPECCABLE_LIVE_INIT__ = false; // reset so the real load can init
|
|
26
27
|
return;
|
|
@@ -57,6 +58,7 @@
|
|
|
57
58
|
const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 };
|
|
58
59
|
const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint
|
|
59
60
|
const PREFIX = 'impeccable-live';
|
|
61
|
+
const IMPECCABLE_COMMAND = (window.__IMPECCABLE_COMMAND_PREFIX__ || '/') + 'impeccable';
|
|
60
62
|
const PICK_CURSOR_STYLE_ID = PREFIX + '-pick-cursor-style';
|
|
61
63
|
const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000;
|
|
62
64
|
const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({
|
|
@@ -97,7 +99,7 @@
|
|
|
97
99
|
|
|
98
100
|
const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions'];
|
|
99
101
|
const LIVE_UI_SURFACES = [
|
|
100
|
-
{ key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice'] },
|
|
102
|
+
{ key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] },
|
|
101
103
|
{ key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] },
|
|
102
104
|
{ key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] },
|
|
103
105
|
{ key: 'action-picker', ids: [PREFIX + '-picker'] },
|
|
@@ -109,7 +111,7 @@
|
|
|
109
111
|
{ key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] },
|
|
110
112
|
{ key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] },
|
|
111
113
|
{ key: 'design-system-panel', ids: [PREFIX + '-design-host'] },
|
|
112
|
-
{ key: 'toasts-and-errors', ids: [PREFIX + '-toast'] },
|
|
114
|
+
{ key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] },
|
|
113
115
|
{ key: 'css-isolation-boundary', ids: [PREFIX + '-root'] },
|
|
114
116
|
];
|
|
115
117
|
const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))];
|
|
@@ -125,16 +127,69 @@
|
|
|
125
127
|
let expectedVariants = 0;
|
|
126
128
|
let arrivedVariants = 0;
|
|
127
129
|
let visibleVariant = 0;
|
|
130
|
+
let generationPhase = null;
|
|
131
|
+
// Ascending order of the agent-generation lifecycle. The visible progress bar
|
|
132
|
+
// must never regress: a `browser_resumed`/behind checkpoint re-broadcasts an
|
|
133
|
+
// earlier phase (the server regresses the snapshot phase to `generating` on a
|
|
134
|
+
// behind checkpoint), and without this the bar jumps backward mid-generation.
|
|
135
|
+
// Unranked phases always pass so we never block a phase we do not model.
|
|
136
|
+
//
|
|
137
|
+
// Every `agent_phase` name here is emitted by recordAgentPhase() in
|
|
138
|
+
// live-server.mjs and listed in AGENT_PHASES in live/vocabulary.mjs, which the
|
|
139
|
+
// event validator enforces. This file is served raw and injected as an IIFE,
|
|
140
|
+
// so it cannot import that list; adding a phase means adding it in both.
|
|
141
|
+
// `queued`, `generating`, `variants_progress`, and `variants_ready` are set
|
|
142
|
+
// locally by this file and never arrive over the wire.
|
|
143
|
+
const PHASE_RANK = {
|
|
144
|
+
queued: 0,
|
|
145
|
+
picked_up: 1,
|
|
146
|
+
scaffolding: 2,
|
|
147
|
+
scaffold_fallback: 3,
|
|
148
|
+
source_ready: 4,
|
|
149
|
+
generation_ready: 5,
|
|
150
|
+
generating: 5,
|
|
151
|
+
variants_progress: 5,
|
|
152
|
+
first_reviewable: 8,
|
|
153
|
+
second_reviewable: 11,
|
|
154
|
+
all_variants_ready: 12,
|
|
155
|
+
variants_ready: 12,
|
|
156
|
+
};
|
|
157
|
+
function shouldAdvancePhase(current, next) {
|
|
158
|
+
if (!next || next === current) return false;
|
|
159
|
+
const nextRank = PHASE_RANK[next];
|
|
160
|
+
const currentRank = PHASE_RANK[current];
|
|
161
|
+
// Only block a known-lower phase from overwriting a known-higher one.
|
|
162
|
+
if (nextRank === undefined || currentRank === undefined) return true;
|
|
163
|
+
return nextRank >= currentRank;
|
|
164
|
+
}
|
|
165
|
+
let parameterGenerationState = 'idle';
|
|
166
|
+
let parameterReadyAnnouncedSession = null;
|
|
128
167
|
let svelteComponentSession = null;
|
|
129
168
|
let svelteRuntimePromise = null;
|
|
130
169
|
let pendingSvelteComponentRetryObserver = null;
|
|
170
|
+
// The persistent mount-error card. A failed import/mount used to wipe local
|
|
171
|
+
// session state and flash a 5s toast, which destroyed the only handle the
|
|
172
|
+
// user had on a session the server still considered live. The card stays up
|
|
173
|
+
// until the variant mounts, the user retries, or a new cycle starts.
|
|
174
|
+
let mountErrorEl = null;
|
|
175
|
+
let mountErrorState = null;
|
|
176
|
+
let lastReportedMountFailure = null;
|
|
131
177
|
let currentSourceFile = null;
|
|
132
178
|
let currentPreviewFile = null;
|
|
133
179
|
let currentPreviewMode = null;
|
|
134
180
|
let recoveryWaitingForAnchor = false;
|
|
135
181
|
let pickedAnchorSnapshot = null;
|
|
182
|
+
let pickedAnchorViewportTop = null;
|
|
136
183
|
let pendingVariantAnchorRetryObserver = null;
|
|
137
184
|
let pendingAcceptedSession = null;
|
|
185
|
+
// Survives cleanupAcceptedSession on purpose: the id of an accept whose
|
|
186
|
+
// POST was acknowledged (intent durable, epoch fenced) but whose actual
|
|
187
|
+
// source promotion hasn't reported back yet. Accept is optimistic, so the
|
|
188
|
+
// teardown nulls pendingAcceptedSession long before live-accept.mjs runs;
|
|
189
|
+
// this marker is what lets the SSE 'error' branch still recognize a late
|
|
190
|
+
// accept failure and say the variant was not saved (issue #384). Released
|
|
191
|
+
// when the real accept result arrives or a new session starts.
|
|
192
|
+
let awaitingAcceptResult = null;
|
|
138
193
|
let variantObserver = null;
|
|
139
194
|
let variantSelectionInFlight = false;
|
|
140
195
|
let variantSelectionPromise = null;
|
|
@@ -150,9 +205,12 @@
|
|
|
150
205
|
// startScrollLock / stopScrollLock below.
|
|
151
206
|
let scrollLockObserver = null;
|
|
152
207
|
let scrollLockTargetY = null;
|
|
208
|
+
let scrollLockAnchorTop = null;
|
|
153
209
|
let scrollLockRaf = null;
|
|
154
210
|
let scrollLockAbort = null;
|
|
155
211
|
const SCROLL_ANCHOR_LOCK_ID = 'impeccable-scroll-anchor-lock';
|
|
212
|
+
const VARIANT_STATE_STYLE_ID = 'impeccable-variant-state';
|
|
213
|
+
const DISCARD_STATE_STYLE_ID = 'impeccable-discard-state';
|
|
156
214
|
|
|
157
215
|
// Dedicated key for scroll position - SEPARATE from LS_KEY so that
|
|
158
216
|
// saveSession's state updates don't clobber a carefully-captured scrollY.
|
|
@@ -250,6 +308,13 @@
|
|
|
250
308
|
barConnected: !!barEl?.isConnected,
|
|
251
309
|
hasSvelteComponentSession: !!svelteComponentSession,
|
|
252
310
|
mountedSvelteVariant: svelteComponentSession?.mountedVariant || 0,
|
|
311
|
+
pickActive,
|
|
312
|
+
pendingApplyInFlight,
|
|
313
|
+
hoveredElement: hoveredElement ? {
|
|
314
|
+
tag: hoveredElement.tagName,
|
|
315
|
+
classes: hoveredElement.className,
|
|
316
|
+
pickable: pickable(hoveredElement),
|
|
317
|
+
} : null,
|
|
253
318
|
pendingSvelteComponentRetry: !!pendingSvelteComponentRetryObserver,
|
|
254
319
|
recoveryWaitingForAnchor,
|
|
255
320
|
evtSourceReadyState: evtSource ? evtSource.readyState : null,
|
|
@@ -1112,14 +1177,15 @@
|
|
|
1112
1177
|
});
|
|
1113
1178
|
}
|
|
1114
1179
|
|
|
1115
|
-
function hideBar() {
|
|
1180
|
+
function hideBar(instant) {
|
|
1116
1181
|
if (!barEl) return;
|
|
1117
1182
|
const hideSeq = ++barHideSeq;
|
|
1118
1183
|
stopVoice({ suppressSubmit: true });
|
|
1119
1184
|
if (configureKind === 'insert') clearInsertPicking();
|
|
1120
1185
|
barEl.style.opacity = '0';
|
|
1121
|
-
barEl.style.transform = 'translateY(6px)';
|
|
1122
|
-
|
|
1186
|
+
barEl.style.transform = instant ? 'translateY(0)' : 'translateY(6px)';
|
|
1187
|
+
if (instant) barEl.style.display = 'none';
|
|
1188
|
+
else setTimeout(() => { if (barEl && hideSeq === barHideSeq) barEl.style.display = 'none'; }, 250);
|
|
1123
1189
|
hideActionPicker();
|
|
1124
1190
|
closeTunePopover();
|
|
1125
1191
|
hideConfigureBarTooltip();
|
|
@@ -1964,7 +2030,11 @@
|
|
|
1964
2030
|
*/
|
|
1965
2031
|
function setLiveState(next) {
|
|
1966
2032
|
state = next;
|
|
2033
|
+
window.__IMPECCABLE_LIVE_STATE__ = next;
|
|
1967
2034
|
syncPageInteractionCursor();
|
|
2035
|
+
// Whether a queued steer is still behind a generation is a function of this
|
|
2036
|
+
// state, so the hint has to move with it, not only with the 5s poll.
|
|
2037
|
+
syncSteerQueueHint();
|
|
1968
2038
|
}
|
|
1969
2039
|
|
|
1970
2040
|
/** Element used to position the floating bar / shader during a session. */
|
|
@@ -2514,22 +2584,53 @@
|
|
|
2514
2584
|
fontSize: '11px', color: BP.textDim, whiteSpace: 'nowrap',
|
|
2515
2585
|
marginLeft: 'auto',
|
|
2516
2586
|
});
|
|
2517
|
-
// Variants currently arrive atomically in a single file edit, so a
|
|
2518
|
-
// per-variant counter would lie. Say what's true.
|
|
2519
2587
|
status.textContent = recoveryWaitingForAnchor
|
|
2520
2588
|
? 'Variants ready. Reveal the selected element to resume.'
|
|
2521
|
-
: (
|
|
2522
|
-
? 'Generating ' + expectedVariants + ' variants...'
|
|
2523
|
-
: 'Done');
|
|
2589
|
+
: generationStatusText();
|
|
2524
2590
|
row.appendChild(status);
|
|
2525
2591
|
|
|
2526
2592
|
return row;
|
|
2527
2593
|
}
|
|
2528
2594
|
|
|
2595
|
+
function generationStatusText() {
|
|
2596
|
+
if (arrivedVariants >= expectedVariants && expectedVariants > 0) return 'Done';
|
|
2597
|
+
if (generationPhase === 'picked_up') return 'Agent picked up the request...';
|
|
2598
|
+
if (generationPhase === 'scaffolding') return 'Finding the source...';
|
|
2599
|
+
if (generationPhase === 'source_ready') return 'Source ready. Generating...';
|
|
2600
|
+
if (generationPhase === 'scaffold_fallback') return 'Agent is locating the source...';
|
|
2601
|
+
if (generationPhase === 'first_reviewable') return 'First variant is ready. Exploring more...';
|
|
2602
|
+
if (generationPhase === 'second_reviewable') return 'Checking the remaining variants...';
|
|
2603
|
+
return 'Generating ' + expectedVariants + ' variants...';
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2529
2606
|
// Cycling row
|
|
2530
2607
|
|
|
2531
2608
|
const TUNE_ICON_SVG = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" style="flex-shrink:0"><line x1="4" y1="8" x2="20" y2="8"/><circle cx="14" cy="8" r="2.4" fill="currentColor" stroke="none"/><line x1="4" y1="16" x2="20" y2="16"/><circle cx="10" cy="16" r="2.4" fill="currentColor" stroke="none"/></svg>';
|
|
2532
2609
|
|
|
2610
|
+
/**
|
|
2611
|
+
* Which variant the user is actually looking at. For component previews the
|
|
2612
|
+
* mounted component is the truth; `visibleVariant` is the intent, and the two
|
|
2613
|
+
* differ while a mount is in flight.
|
|
2614
|
+
*/
|
|
2615
|
+
function cyclingShownVariant() {
|
|
2616
|
+
return svelteComponentSession?.sessionId === currentSessionId && svelteComponentSession.mountedVariant > 0
|
|
2617
|
+
? svelteComponentSession.mountedVariant
|
|
2618
|
+
: visibleVariant;
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
/**
|
|
2622
|
+
* The single counter string. It is built here rather than at each call site
|
|
2623
|
+
* because the row builder and the incremental sync used to disagree on the
|
|
2624
|
+
* denominator: one showed the planned count, the other the arrived count, so
|
|
2625
|
+
* "2/3" turned into "2/2" on the next sync without anything changing on
|
|
2626
|
+
* screen. Arrived wins once anything has arrived; expected covers the window
|
|
2627
|
+
* before the first variant lands.
|
|
2628
|
+
*/
|
|
2629
|
+
function cyclingCounterText() {
|
|
2630
|
+
const total = arrivedVariants > 0 ? arrivedVariants : expectedVariants;
|
|
2631
|
+
return cyclingShownVariant() + '/' + total;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2533
2634
|
function buildCyclingRow() {
|
|
2534
2635
|
if (!ensureCyclingRenderable('build-cycling-row')) {
|
|
2535
2636
|
return el('div', { display: 'none' });
|
|
@@ -2543,7 +2644,7 @@
|
|
|
2543
2644
|
const prev = navBtn('\u2190');
|
|
2544
2645
|
prev.id = PREFIX + '-variant-prev';
|
|
2545
2646
|
prev.addEventListener('click', (e) => { e.stopPropagation(); cycleVariant(-1); });
|
|
2546
|
-
if (
|
|
2647
|
+
if (cyclingShownVariant() <= 1) prev.style.opacity = '0.3';
|
|
2547
2648
|
row.appendChild(prev);
|
|
2548
2649
|
|
|
2549
2650
|
// Dots (clickable)
|
|
@@ -2555,20 +2656,22 @@
|
|
|
2555
2656
|
color: BP.textDim, minWidth: '24px', textAlign: 'center',
|
|
2556
2657
|
});
|
|
2557
2658
|
counter.id = PREFIX + '-variant-counter';
|
|
2558
|
-
counter.textContent =
|
|
2659
|
+
counter.textContent = cyclingCounterText();
|
|
2559
2660
|
row.appendChild(counter);
|
|
2560
2661
|
|
|
2561
2662
|
// Next
|
|
2562
2663
|
const next = navBtn('\u2192');
|
|
2563
2664
|
next.id = PREFIX + '-variant-next';
|
|
2564
2665
|
next.addEventListener('click', (e) => { e.stopPropagation(); cycleVariant(1); });
|
|
2565
|
-
if (
|
|
2666
|
+
if (cyclingShownVariant() >= arrivedVariants) next.style.opacity = '0.3';
|
|
2566
2667
|
row.appendChild(next);
|
|
2567
2668
|
|
|
2568
|
-
// Tune chip
|
|
2669
|
+
// Tune chip stays visible while the deferred parameter phase is running,
|
|
2670
|
+
// then becomes interactive as soon as this variant exposes controls.
|
|
2569
2671
|
const visParams = parseVariantParams(getVisibleVariantEl());
|
|
2570
2672
|
const hasParams = visParams.length > 0;
|
|
2571
|
-
|
|
2673
|
+
const paramsPending = !hasParams && (parameterGenerationState === 'pending' || parameterGenerationState === 'loading');
|
|
2674
|
+
if (hasParams || paramsPending) {
|
|
2572
2675
|
const tune = el('button', {
|
|
2573
2676
|
display: 'inline-flex', alignItems: 'center', gap: '6px',
|
|
2574
2677
|
padding: '4px 10px', borderRadius: '5px',
|
|
@@ -2576,35 +2679,54 @@
|
|
|
2576
2679
|
background: tuneOpen ? BP.accentSoft : 'transparent',
|
|
2577
2680
|
color: tuneOpen ? BP.accent : BP.text,
|
|
2578
2681
|
fontFamily: FONT, fontSize: '11px', fontWeight: '500',
|
|
2579
|
-
cursor: 'pointer',
|
|
2682
|
+
cursor: paramsPending ? 'wait' : 'pointer',
|
|
2580
2683
|
transition: 'color 0.12s ease, background 0.12s ease',
|
|
2581
2684
|
whiteSpace: 'nowrap',
|
|
2582
2685
|
});
|
|
2583
|
-
|
|
2686
|
+
if (paramsPending) {
|
|
2687
|
+
const spinner = el('span', {
|
|
2688
|
+
width: '11px', height: '11px', borderRadius: '50%',
|
|
2689
|
+
border: '1.5px solid ' + BP.hairline,
|
|
2690
|
+
borderTopColor: BP.accent,
|
|
2691
|
+
animation: 'impeccable-spin 0.6s linear infinite',
|
|
2692
|
+
boxSizing: 'border-box', flexShrink: '0',
|
|
2693
|
+
});
|
|
2694
|
+
spinner.setAttribute('aria-hidden', 'true');
|
|
2695
|
+
tune.appendChild(spinner);
|
|
2696
|
+
} else {
|
|
2697
|
+
tune.innerHTML = TUNE_ICON_SVG;
|
|
2698
|
+
}
|
|
2584
2699
|
const tuneLabel = document.createElement('span');
|
|
2585
2700
|
tuneLabel.textContent = 'Tune';
|
|
2586
2701
|
tune.appendChild(tuneLabel);
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2702
|
+
if (hasParams) {
|
|
2703
|
+
const tuneBadge = document.createElement('span');
|
|
2704
|
+
Object.assign(tuneBadge.style, {
|
|
2705
|
+
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
2706
|
+
minWidth: '16px', height: '16px', padding: '0 4px',
|
|
2707
|
+
borderRadius: '999px',
|
|
2708
|
+
background: tuneOpen ? C.brand : BP.hairline,
|
|
2709
|
+
color: tuneOpen ? C.ink : 'inherit',
|
|
2710
|
+
fontFamily: MONO, fontSize: '9.5px', fontWeight: '600',
|
|
2711
|
+
lineHeight: '1',
|
|
2712
|
+
boxSizing: 'border-box',
|
|
2713
|
+
});
|
|
2714
|
+
tuneBadge.textContent = String(visParams.length);
|
|
2715
|
+
tune.appendChild(tuneBadge);
|
|
2716
|
+
tune.title = 'Tune this variant (' + visParams.length + ' knob' + (visParams.length === 1 ? '' : 's') + ')';
|
|
2717
|
+
tune.addEventListener('mouseenter', () => {
|
|
2718
|
+
if (!tuneOpen) tune.style.background = BP.accentSoft;
|
|
2719
|
+
});
|
|
2720
|
+
tune.addEventListener('mouseleave', () => {
|
|
2721
|
+
if (!tuneOpen) tune.style.background = 'transparent';
|
|
2722
|
+
});
|
|
2723
|
+
tune.addEventListener('click', (e) => { e.stopPropagation(); toggleTunePopover(); });
|
|
2724
|
+
} else {
|
|
2725
|
+
tune.disabled = true;
|
|
2726
|
+
tune.setAttribute('aria-label', 'Tune controls are still being prepared');
|
|
2727
|
+
tune.title = 'Tune controls are still being prepared';
|
|
2728
|
+
tune.style.opacity = '0.72';
|
|
2729
|
+
}
|
|
2608
2730
|
tune.dataset.iceqTune = '1';
|
|
2609
2731
|
row.appendChild(tune);
|
|
2610
2732
|
}
|
|
@@ -2612,6 +2734,15 @@
|
|
|
2612
2734
|
// Spacer
|
|
2613
2735
|
row.appendChild(el('div', { flex: '1' }));
|
|
2614
2736
|
|
|
2737
|
+
if (arrivedVariants < expectedVariants) {
|
|
2738
|
+
const remaining = expectedVariants - arrivedVariants;
|
|
2739
|
+
const progress = el('span', {
|
|
2740
|
+
fontSize: '11px', color: BP.textDim, whiteSpace: 'nowrap',
|
|
2741
|
+
});
|
|
2742
|
+
progress.textContent = remaining + ' more arriving...';
|
|
2743
|
+
row.appendChild(progress);
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2615
2746
|
// Accept - primary action, kinpaku gold + lacquer-deep (matches demo .live-demo-ctx-accept)
|
|
2616
2747
|
const accept = el('button', {
|
|
2617
2748
|
padding: '5px 14px', borderRadius: '5px',
|
|
@@ -2626,7 +2757,11 @@
|
|
|
2626
2757
|
accept.addEventListener('mousedown', () => accept.style.transform = 'scale(0.97)');
|
|
2627
2758
|
accept.addEventListener('mouseup', () => accept.style.transform = 'scale(1)');
|
|
2628
2759
|
accept.addEventListener('click', (e) => { e.stopPropagation(); handleAccept(); });
|
|
2629
|
-
if (arrivedVariants === 0) {
|
|
2760
|
+
if (arrivedVariants === 0) {
|
|
2761
|
+
accept.style.opacity = '0.3';
|
|
2762
|
+
accept.style.pointerEvents = 'none';
|
|
2763
|
+
accept.title = 'Accept becomes available when the first variant arrives';
|
|
2764
|
+
}
|
|
2630
2765
|
row.appendChild(accept);
|
|
2631
2766
|
|
|
2632
2767
|
// Discard
|
|
@@ -2893,7 +3028,7 @@
|
|
|
2893
3028
|
console.warn('[impeccable] Refusing to render empty variant cycling state:', reason);
|
|
2894
3029
|
const message = 'No variants were mounted. Please try again.';
|
|
2895
3030
|
if (svelteComponentSession?.sessionId === currentSessionId) {
|
|
2896
|
-
|
|
3031
|
+
resetSvelteComponentSession(currentSessionId, message);
|
|
2897
3032
|
return;
|
|
2898
3033
|
}
|
|
2899
3034
|
cleanup();
|
|
@@ -3035,16 +3170,26 @@
|
|
|
3035
3170
|
function applyParamValue(variantEl, param, value) {
|
|
3036
3171
|
if (!variantEl) return;
|
|
3037
3172
|
const attr = 'data-p-' + param.id;
|
|
3038
|
-
if (param.kind === '
|
|
3039
|
-
variantEl.style.setProperty('--p-' + param.id, String(value));
|
|
3040
|
-
} else if (param.kind === 'toggle') {
|
|
3173
|
+
if (param.kind === 'toggle') {
|
|
3041
3174
|
const on = !!value;
|
|
3042
|
-
variantEl.style.setProperty('--p-' + param.id, on ? '1' : '0');
|
|
3043
3175
|
if (on) variantEl.setAttribute(attr, 'on');
|
|
3044
3176
|
else variantEl.removeAttribute(attr);
|
|
3045
3177
|
} else if (param.kind === 'steps') {
|
|
3046
3178
|
variantEl.setAttribute(attr, String(value));
|
|
3047
3179
|
}
|
|
3180
|
+
// Svelte component variants are client-mounted into
|
|
3181
|
+
// [data-impeccable-component-mount] with no [data-impeccable-variant="N"]
|
|
3182
|
+
// wrapper for the state stylesheet to target, and the element is not SSR'd,
|
|
3183
|
+
// so there is no React hydration to mismatch. Drive range/toggle --p-* inline
|
|
3184
|
+
// on the mounted element so scoped preview CSS resolves them.
|
|
3185
|
+
if (svelteComponentSession?.sessionId === currentSessionId) {
|
|
3186
|
+
if (param.kind === 'range') variantEl.style.setProperty('--p-' + param.id, String(value));
|
|
3187
|
+
else if (param.kind === 'toggle') variantEl.style.setProperty('--p-' + param.id, value ? '1' : '0');
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
// range/toggle --p-* custom properties are driven through the injected
|
|
3191
|
+
// variant-state stylesheet so we never mutate inline style on SSR'd divs.
|
|
3192
|
+
updateVariantStateStylesheet(currentSessionId, visibleVariant);
|
|
3048
3193
|
}
|
|
3049
3194
|
|
|
3050
3195
|
function applyParamDefaults(variantEl, params) {
|
|
@@ -3119,7 +3264,7 @@
|
|
|
3119
3264
|
position: 'absolute', top: '2px',
|
|
3120
3265
|
left: initial ? '18px' : '2px',
|
|
3121
3266
|
width: '16px', height: '16px', borderRadius: '50%',
|
|
3122
|
-
background:
|
|
3267
|
+
background: C.ink,
|
|
3123
3268
|
transition: 'left 0.18s ' + EASE,
|
|
3124
3269
|
boxShadow: '0 1px 2px oklch(0% 0 0 / 0.2)',
|
|
3125
3270
|
});
|
|
@@ -3153,7 +3298,7 @@
|
|
|
3153
3298
|
const b = el('button', {
|
|
3154
3299
|
padding: '5px 4px', border: 'none', borderRadius: '3px',
|
|
3155
3300
|
background: active ? C.brand : 'transparent',
|
|
3156
|
-
color: active ?
|
|
3301
|
+
color: active ? C.ink : P.text,
|
|
3157
3302
|
fontFamily: FONT, fontSize: '10.5px', fontWeight: '500',
|
|
3158
3303
|
cursor: 'pointer', whiteSpace: 'nowrap',
|
|
3159
3304
|
transition: 'background 0.1s ease, color 0.1s ease',
|
|
@@ -3166,7 +3311,7 @@
|
|
|
3166
3311
|
segBtns.forEach(({ btn, val }) => {
|
|
3167
3312
|
const on = val === o.value;
|
|
3168
3313
|
btn.style.background = on ? C.brand : 'transparent';
|
|
3169
|
-
btn.style.color = on ?
|
|
3314
|
+
btn.style.color = on ? C.ink : P.text;
|
|
3170
3315
|
});
|
|
3171
3316
|
applyParamValue(variantEl, p, o.value);
|
|
3172
3317
|
queueCheckpoint('param_changed');
|
|
@@ -4714,6 +4859,7 @@
|
|
|
4714
4859
|
paramsCurrentValues = {};
|
|
4715
4860
|
tuneOpen = false;
|
|
4716
4861
|
hideParamsPanel();
|
|
4862
|
+
if (currentSessionId && visibleVariant) updateVariantStateStylesheet(currentSessionId, visibleVariant);
|
|
4717
4863
|
return;
|
|
4718
4864
|
}
|
|
4719
4865
|
applyParamDefaults(variantEl, params);
|
|
@@ -4730,6 +4876,32 @@
|
|
|
4730
4876
|
}
|
|
4731
4877
|
}
|
|
4732
4878
|
|
|
4879
|
+
function mountedParameterCount() {
|
|
4880
|
+
if (svelteComponentSession?.sessionId === currentSessionId) {
|
|
4881
|
+
return Object.values(svelteComponentSession.paramsByVariant || {})
|
|
4882
|
+
.reduce((total, params) => total + (Array.isArray(params) ? params.length : 0), 0);
|
|
4883
|
+
}
|
|
4884
|
+
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
|
|
4885
|
+
if (!wrapper) return 0;
|
|
4886
|
+
return [...wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])')]
|
|
4887
|
+
.reduce((total, variant) => total + parseVariantParams(variant).length, 0);
|
|
4888
|
+
}
|
|
4889
|
+
|
|
4890
|
+
function completeParameterPublication() {
|
|
4891
|
+
if (!currentSessionId) return;
|
|
4892
|
+
const ready = mountedParameterCount() > 0;
|
|
4893
|
+
parameterGenerationState = ready ? 'ready' : 'none';
|
|
4894
|
+
if (ready && parameterReadyAnnouncedSession !== currentSessionId) {
|
|
4895
|
+
parameterReadyAnnouncedSession = currentSessionId;
|
|
4896
|
+
showToast('Tune controls are ready.', 3000);
|
|
4897
|
+
}
|
|
4898
|
+
if (state === 'CYCLING') {
|
|
4899
|
+
refreshParamsPanel();
|
|
4900
|
+
showOrUpdateCyclingBar();
|
|
4901
|
+
}
|
|
4902
|
+
saveSession();
|
|
4903
|
+
}
|
|
4904
|
+
|
|
4733
4905
|
function toggleTunePopover() {
|
|
4734
4906
|
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
|
|
4735
4907
|
if (tuneOpen) { closeTunePopover(); return; }
|
|
@@ -4771,20 +4943,7 @@
|
|
|
4771
4943
|
|
|
4772
4944
|
function isVariantShown(el) {
|
|
4773
4945
|
if (!el) return false;
|
|
4774
|
-
|
|
4775
|
-
if (el.style?.display === 'none') return false;
|
|
4776
|
-
return true;
|
|
4777
|
-
}
|
|
4778
|
-
|
|
4779
|
-
function setVariantShown(el, shown) {
|
|
4780
|
-
if (!el) return;
|
|
4781
|
-
if (shown) {
|
|
4782
|
-
el.removeAttribute('hidden');
|
|
4783
|
-
el.style.display = '';
|
|
4784
|
-
} else {
|
|
4785
|
-
el.setAttribute('hidden', '');
|
|
4786
|
-
el.style.display = 'none';
|
|
4787
|
-
}
|
|
4946
|
+
return getComputedStyle(el).display !== 'none';
|
|
4788
4947
|
}
|
|
4789
4948
|
|
|
4790
4949
|
function scheduleCyclingBarSync(sessionId, variantNum) {
|
|
@@ -4799,11 +4958,9 @@
|
|
|
4799
4958
|
}
|
|
4800
4959
|
|
|
4801
4960
|
function syncCyclingControls() {
|
|
4802
|
-
const shown =
|
|
4803
|
-
? svelteComponentSession.mountedVariant
|
|
4804
|
-
: visibleVariant;
|
|
4961
|
+
const shown = cyclingShownVariant();
|
|
4805
4962
|
const counter = uiGetById(PREFIX + '-variant-counter');
|
|
4806
|
-
if (counter
|
|
4963
|
+
if (counter) counter.textContent = cyclingCounterText();
|
|
4807
4964
|
const prev = uiGetById(PREFIX + '-variant-prev');
|
|
4808
4965
|
const next = uiGetById(PREFIX + '-variant-next');
|
|
4809
4966
|
if (prev) prev.style.opacity = shown <= 1 ? '0.3' : '1';
|
|
@@ -4823,11 +4980,7 @@
|
|
|
4823
4980
|
}
|
|
4824
4981
|
const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
|
|
4825
4982
|
if (!wrapper) return false;
|
|
4826
|
-
|
|
4827
|
-
const v = child.dataset ? child.dataset.impeccableVariant : null;
|
|
4828
|
-
if (!v) continue;
|
|
4829
|
-
setVariantShown(child, v === String(num));
|
|
4830
|
-
}
|
|
4983
|
+
updateVariantStateStylesheet(sessionId, num);
|
|
4831
4984
|
// Unconditional refresh - covers first-reveal (no-op if state isn't
|
|
4832
4985
|
// CYCLING yet, the subsequent CYCLING transition triggers its own
|
|
4833
4986
|
// refresh) and every cycle step.
|
|
@@ -4839,6 +4992,10 @@
|
|
|
4839
4992
|
return String(filePath || '').endsWith('manifest.json');
|
|
4840
4993
|
}
|
|
4841
4994
|
|
|
4995
|
+
function isFrameworkComponentPreviewMode(mode) {
|
|
4996
|
+
return mode === 'svelte-component';
|
|
4997
|
+
}
|
|
4998
|
+
|
|
4842
4999
|
function parseOriginalMarkupElement(originalMarkup) {
|
|
4843
5000
|
const parser = new DOMParser();
|
|
4844
5001
|
const doc = parser.parseFromString('<div id="impeccable-anchor">' + originalMarkup + '</div>', 'text/html');
|
|
@@ -5033,11 +5190,74 @@
|
|
|
5033
5190
|
}
|
|
5034
5191
|
}
|
|
5035
5192
|
|
|
5036
|
-
|
|
5193
|
+
// The dev server may serve under a non-root base (vite `base`) or a root
|
|
5194
|
+
// that differs from where the helper wrote the preview tree. Root-relative
|
|
5195
|
+
// URLs are tried against the detected base first; the /@fs/ absolute form
|
|
5196
|
+
// is the fallback that works regardless of base and root, as long as the
|
|
5197
|
+
// path is inside the server's fs.allow.
|
|
5198
|
+
let detectedDevBase = null;
|
|
5199
|
+
function detectDevServerBase() {
|
|
5200
|
+
if (detectedDevBase !== null) return detectedDevBase;
|
|
5201
|
+
detectedDevBase = '/';
|
|
5202
|
+
const scripts = document.querySelectorAll('script[type="module"][src]');
|
|
5203
|
+
for (const script of scripts) {
|
|
5204
|
+
const src = script.getAttribute('src') || '';
|
|
5205
|
+
const idx = src.indexOf('/@vite/client');
|
|
5206
|
+
if (idx > 0) { detectedDevBase = src.slice(0, idx) + '/'; break; }
|
|
5207
|
+
if (idx === 0) { detectedDevBase = '/'; break; }
|
|
5208
|
+
}
|
|
5209
|
+
return detectedDevBase;
|
|
5210
|
+
}
|
|
5211
|
+
|
|
5212
|
+
function componentModuleCandidates(manifest, modulePath, absPath) {
|
|
5213
|
+
const base = detectDevServerBase();
|
|
5214
|
+
const rel = String(modulePath || '').replace(/^\/+/, '');
|
|
5215
|
+
const candidates = [new URL(base + rel, location.origin).href];
|
|
5216
|
+
if (base !== '/') candidates.push(new URL('/' + rel, location.origin).href);
|
|
5217
|
+
if (absPath) {
|
|
5218
|
+
const fsRel = '@fs/' + String(absPath).replace(/^\/+/, '');
|
|
5219
|
+
candidates.push(new URL(base + fsRel, location.origin).href);
|
|
5220
|
+
// Vite versions differ on whether @fs is served under base or at the
|
|
5221
|
+
// server root; with a non-root base, try both.
|
|
5222
|
+
if (base !== '/') candidates.push(new URL('/' + fsRel, location.origin).href);
|
|
5223
|
+
}
|
|
5224
|
+
return candidates;
|
|
5225
|
+
}
|
|
5226
|
+
|
|
5227
|
+
async function importFirstReachable(candidates, bust) {
|
|
5228
|
+
let lastErr = null;
|
|
5229
|
+
for (const candidate of candidates) {
|
|
5230
|
+
try {
|
|
5231
|
+
const url = bust ? candidate + (candidate.includes('?') ? '&' : '?') + 't=' + Date.now() : candidate;
|
|
5232
|
+
const mod = await import(/* @vite-ignore */ url);
|
|
5233
|
+
return { mod, url: candidate };
|
|
5234
|
+
} catch (err) {
|
|
5235
|
+
lastErr = err;
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
5238
|
+
throw Object.assign(lastErr || new Error('no module candidates'), {
|
|
5239
|
+
impeccableTriedUrls: candidates,
|
|
5240
|
+
});
|
|
5241
|
+
}
|
|
5242
|
+
|
|
5243
|
+
// Distinguishes "this variant is broken" from "the preview tree is not
|
|
5244
|
+
// reachable from the dev server at all" (wrong root, unserved directory).
|
|
5245
|
+
async function probePreviewTree(manifest) {
|
|
5246
|
+
if (!manifest?.probeModule) return { ok: true, skipped: true };
|
|
5247
|
+
const candidates = componentModuleCandidates(manifest, manifest.probeModule, manifest.probeModuleAbs);
|
|
5248
|
+
try {
|
|
5249
|
+
await importFirstReachable(candidates, false);
|
|
5250
|
+
return { ok: true };
|
|
5251
|
+
} catch (err) {
|
|
5252
|
+
return { ok: false, tried: err.impeccableTriedUrls || candidates };
|
|
5253
|
+
}
|
|
5254
|
+
}
|
|
5255
|
+
|
|
5256
|
+
function loadSvelteRuntime(runtimeModule, manifest) {
|
|
5037
5257
|
const modulePath = runtimeModule || '/src/lib/impeccable/__runtime.js';
|
|
5038
|
-
const url = new URL(modulePath, location.origin).href;
|
|
5039
5258
|
if (!svelteRuntimePromise) {
|
|
5040
|
-
|
|
5259
|
+
const candidates = componentModuleCandidates(manifest, modulePath, manifest?.runtimeModuleAbs);
|
|
5260
|
+
svelteRuntimePromise = importFirstReachable(candidates, false).then((r) => r.mod);
|
|
5041
5261
|
}
|
|
5042
5262
|
return svelteRuntimePromise;
|
|
5043
5263
|
}
|
|
@@ -5047,7 +5267,7 @@
|
|
|
5047
5267
|
// attribute with JSON braces can't survive the Svelte compiler. Returns a map of
|
|
5048
5268
|
// { "1": [...params], "2": [...] }; an empty object when the agent declared none.
|
|
5049
5269
|
async function loadSvelteComponentParams(manifest) {
|
|
5050
|
-
const dir = String(manifest?.componentDir || '').replace(/^\/+/, '');
|
|
5270
|
+
const dir = String(manifest?.revisionDir || manifest?.componentDir || '').replace(/^\/+/, '');
|
|
5051
5271
|
if (!dir) return {};
|
|
5052
5272
|
const paramsPath = dir + '/params.json';
|
|
5053
5273
|
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(paramsPath);
|
|
@@ -5066,41 +5286,14 @@
|
|
|
5066
5286
|
}
|
|
5067
5287
|
}
|
|
5068
5288
|
|
|
5069
|
-
async function loadSvelteComponentVariantSource(manifest, variantNum) {
|
|
5070
|
-
const dir = String(manifest?.componentDir || '').replace(/^\/+/, '');
|
|
5071
|
-
if (!dir || !variantNum) return '';
|
|
5072
|
-
const sourcePath = dir + '/v' + variantNum + '.svelte';
|
|
5073
|
-
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(sourcePath);
|
|
5074
|
-
try {
|
|
5075
|
-
const res = await fetch(url);
|
|
5076
|
-
if (!res.ok) return '';
|
|
5077
|
-
return await res.text();
|
|
5078
|
-
} catch {
|
|
5079
|
-
return '';
|
|
5080
|
-
}
|
|
5081
|
-
}
|
|
5082
5289
|
|
|
5083
|
-
function extractSvelteComponentStyle(source) {
|
|
5084
|
-
const match = String(source || '').match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/i);
|
|
5085
|
-
return match ? match[1].trim() : '';
|
|
5086
|
-
}
|
|
5087
5290
|
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
if (!css) return;
|
|
5095
|
-
const scopedCss = scopeCssToSveltePreview(css, sessionId);
|
|
5096
|
-
if (!scopedCss) return;
|
|
5097
|
-
const style = document.createElement('style');
|
|
5098
|
-
style.dataset.impeccableSvelteComponentStyle = sessionId;
|
|
5099
|
-
style.dataset.impeccableVariant = String(variantNum);
|
|
5100
|
-
style.textContent = scopedCss;
|
|
5101
|
-
document.head.appendChild(style);
|
|
5102
|
-
svelteComponentSession.styleEl = style;
|
|
5103
|
-
}
|
|
5291
|
+
// NOTE: the compiled component imported from the dev server already carries
|
|
5292
|
+
// its own scoped styles (vite-plugin-svelte injects them on module
|
|
5293
|
+
// evaluation). The old second injection re-fetched the raw source through
|
|
5294
|
+
// the helper and re-prefixed every selector un-hashed, so the same rules
|
|
5295
|
+
// applied twice with different specificity: preview and accepted cascades
|
|
5296
|
+
// disagreed. The single compiled copy is the truth now.
|
|
5104
5297
|
|
|
5105
5298
|
function removeSvelteComponentVariantStyle(session = svelteComponentSession) {
|
|
5106
5299
|
const style = session?.styleEl;
|
|
@@ -5108,10 +5301,6 @@
|
|
|
5108
5301
|
if (session) session.styleEl = null;
|
|
5109
5302
|
}
|
|
5110
5303
|
|
|
5111
|
-
function scopeCssToSveltePreview(css, sessionId) {
|
|
5112
|
-
const prefix = '[data-impeccable-variants="' + String(sessionId).replace(/"/g, '\\"') + '"] ';
|
|
5113
|
-
return scopeCssBlock(String(css || ''), prefix).trim();
|
|
5114
|
-
}
|
|
5115
5304
|
|
|
5116
5305
|
function scopeCssBlock(css, prefix) {
|
|
5117
5306
|
let out = '';
|
|
@@ -5221,26 +5410,212 @@
|
|
|
5221
5410
|
const contract = manifest?.propContract || [];
|
|
5222
5411
|
const values = {};
|
|
5223
5412
|
if (!liveEl || contract.length === 0) return values;
|
|
5413
|
+
if (Number(manifest.contractVersion) === 2) {
|
|
5414
|
+
return buildSveltePropValuesV2(liveEl, manifest);
|
|
5415
|
+
}
|
|
5224
5416
|
const sourceOriginal = parseOriginalMarkupElement(manifest.originalMarkup || '');
|
|
5225
5417
|
if (!sourceOriginal) return values;
|
|
5226
5418
|
const map = buildSvelteExpressionTextMap(sourceOriginal, liveEl);
|
|
5227
5419
|
for (const entry of contract) {
|
|
5228
|
-
const token = '{' + entry.expr + '}';
|
|
5420
|
+
const token = entry.previewToken || ('{' + entry.expr + '}');
|
|
5229
5421
|
values[entry.prop] = map.get(token) || '';
|
|
5230
5422
|
}
|
|
5231
5423
|
return values;
|
|
5232
5424
|
}
|
|
5233
5425
|
|
|
5426
|
+
// Contract v2 hydration. The scaffolder preserved control flow, so props
|
|
5427
|
+
// come in kinds: `collection` hydrates from the live DOM's rendered items
|
|
5428
|
+
// (count by the item root selector, texts by slot order), `condition` from
|
|
5429
|
+
// whether the branch's probe element is currently rendered, `text` from the
|
|
5430
|
+
// v1 index-zip run over the markup WITH control-flow regions stripped and
|
|
5431
|
+
// the live tree WITH item elements excluded, so loop tokens can never shift
|
|
5432
|
+
// slots again. `handler` props keep their no-op defaults.
|
|
5433
|
+
function buildSveltePropValuesV2(liveEl, manifest) {
|
|
5434
|
+
const contract = manifest.propContract || [];
|
|
5435
|
+
const values = {};
|
|
5436
|
+
const itemElsByProp = new Map();
|
|
5437
|
+
|
|
5438
|
+
for (const entry of contract) {
|
|
5439
|
+
if (entry.kind === 'collection' && entry.item && entry.item.rootTag) {
|
|
5440
|
+
const selector = entry.item.rootTag + (entry.item.rootClasses || []).map((c) => '.' + cssEscapeIdent(c)).join('');
|
|
5441
|
+
let matches = [];
|
|
5442
|
+
try { matches = Array.from(liveEl.querySelectorAll(selector)); } catch { matches = []; }
|
|
5443
|
+
itemElsByProp.set(entry.prop, matches);
|
|
5444
|
+
const statics = new Set((entry.item.staticTexts || []).map((t) => String(t).trim()));
|
|
5445
|
+
const slots = entry.item.textSlots || [];
|
|
5446
|
+
values[entry.prop] = matches.map((itemEl, index) => {
|
|
5447
|
+
const texts = collectVisibleTexts(itemEl).filter((t) => !statics.has(t));
|
|
5448
|
+
const item = {};
|
|
5449
|
+
slots.forEach((slot, i) => { item[slot.key] = texts[i] != null ? texts[i] : ''; });
|
|
5450
|
+
// Attribute-bound values (href={link.href}) hydrate from the
|
|
5451
|
+
// rendered attribute on the live item element or a descendant.
|
|
5452
|
+
for (const slot of entry.item.attrSlots || []) {
|
|
5453
|
+
if (item[slot.key] != null || !slot.tag) continue;
|
|
5454
|
+
const sel = slot.tag + (slot.classes || []).map((c) => '.' + cssEscapeIdent(c)).join('');
|
|
5455
|
+
let el = null;
|
|
5456
|
+
try { el = itemEl.matches(sel) ? itemEl : itemEl.querySelector(sel); } catch { el = null; }
|
|
5457
|
+
const value = el ? el.getAttribute(slot.attr) : null;
|
|
5458
|
+
if (value != null) item[slot.key] = value;
|
|
5459
|
+
}
|
|
5460
|
+
// Keyed each: the key field is never rendered, so hydrate it with a
|
|
5461
|
+
// unique per-index value or Svelte throws each_key_duplicate.
|
|
5462
|
+
if (entry.item.keyField && item[entry.item.keyField] == null) {
|
|
5463
|
+
item[entry.item.keyField] = 'impeccable-live-' + index;
|
|
5464
|
+
}
|
|
5465
|
+
return item;
|
|
5466
|
+
});
|
|
5467
|
+
} else if (entry.kind === 'condition') {
|
|
5468
|
+
if (entry.probe && entry.probe.tag) {
|
|
5469
|
+
const selector = entry.probe.tag + (entry.probe.classes || []).map((c) => '.' + cssEscapeIdent(c)).join('');
|
|
5470
|
+
try { values[entry.prop] = !!liveEl.querySelector(selector); } catch { /* keep default */ }
|
|
5471
|
+
} else if (entry.probe && entry.probe.className) {
|
|
5472
|
+
// class:name directive: the live DOM answers directly, either on
|
|
5473
|
+
// the picked element itself or on a descendant carrying the class.
|
|
5474
|
+
try {
|
|
5475
|
+
values[entry.prop] = liveEl.classList.contains(entry.probe.className)
|
|
5476
|
+
|| !!liveEl.querySelector('.' + cssEscapeIdent(entry.probe.className));
|
|
5477
|
+
} catch { /* keep default */ }
|
|
5478
|
+
}
|
|
5479
|
+
}
|
|
5480
|
+
}
|
|
5481
|
+
|
|
5482
|
+
// Text props outside control flow: strip block regions from the source
|
|
5483
|
+
// markup, exclude live text nodes inside any hydrated item element, then
|
|
5484
|
+
// run the existing zip.
|
|
5485
|
+
const textEntries = contract.filter((e) => e.kind === 'text' || e.kind === 'raw');
|
|
5486
|
+
if (textEntries.length > 0) {
|
|
5487
|
+
const strippedMarkup = stripSvelteBlockRegions(manifest.originalMarkup || '');
|
|
5488
|
+
const sourceOriginal = parseOriginalMarkupElement(strippedMarkup);
|
|
5489
|
+
if (sourceOriginal) {
|
|
5490
|
+
const excluded = [];
|
|
5491
|
+
for (const els of itemElsByProp.values()) excluded.push(...els);
|
|
5492
|
+
const filteredLive = cloneWithoutElements(liveEl, excluded);
|
|
5493
|
+
const map = buildSvelteExpressionTextMap(sourceOriginal, filteredLive);
|
|
5494
|
+
for (const entry of textEntries) {
|
|
5495
|
+
const token = '{' + entry.expr + '}';
|
|
5496
|
+
if (map.has(token)) values[entry.prop] = map.get(token) || '';
|
|
5497
|
+
}
|
|
5498
|
+
}
|
|
5499
|
+
}
|
|
5500
|
+
return values;
|
|
5501
|
+
}
|
|
5502
|
+
|
|
5503
|
+
function cssEscapeIdent(value) {
|
|
5504
|
+
try { return CSS.escape(value); } catch { return String(value).replace(/[^a-zA-Z0-9_-]/g, ''); }
|
|
5505
|
+
}
|
|
5506
|
+
|
|
5507
|
+
function collectVisibleTexts(rootEl) {
|
|
5508
|
+
const texts = [];
|
|
5509
|
+
const walker = document.createTreeWalker(rootEl, NodeFilter.SHOW_TEXT);
|
|
5510
|
+
let node;
|
|
5511
|
+
while ((node = walker.nextNode())) {
|
|
5512
|
+
const trimmed = String(node.textContent || '').trim();
|
|
5513
|
+
if (trimmed) texts.push(trimmed);
|
|
5514
|
+
}
|
|
5515
|
+
return texts;
|
|
5516
|
+
}
|
|
5517
|
+
|
|
5518
|
+
// Remove balanced {#each}...{/each} and {#if}...{/if} regions (including
|
|
5519
|
+
// the delimiters) from a markup string. Nesting-aware. {#key} blocks keep
|
|
5520
|
+
// their CONTENT (it always renders) but lose their delimiter tokens, which
|
|
5521
|
+
// would otherwise consume live text slots in the zip and shift every
|
|
5522
|
+
// following expression.
|
|
5523
|
+
function stripSvelteBlockRegions(markup) {
|
|
5524
|
+
let out = String(markup || '');
|
|
5525
|
+
out = stripSvelteKeyDelimiters(out);
|
|
5526
|
+
for (const kind of ['each', 'if']) {
|
|
5527
|
+
const open = '{#' + kind;
|
|
5528
|
+
const close = '{/' + kind + '}';
|
|
5529
|
+
for (;;) {
|
|
5530
|
+
const start = out.indexOf(open);
|
|
5531
|
+
if (start === -1) break;
|
|
5532
|
+
let depth = 0;
|
|
5533
|
+
let i = start;
|
|
5534
|
+
let end = -1;
|
|
5535
|
+
while (i < out.length) {
|
|
5536
|
+
if (out.startsWith(open, i)) { depth++; i += open.length; continue; }
|
|
5537
|
+
if (out.startsWith(close, i)) {
|
|
5538
|
+
depth--;
|
|
5539
|
+
i += close.length;
|
|
5540
|
+
if (depth === 0) { end = i; break; }
|
|
5541
|
+
continue;
|
|
5542
|
+
}
|
|
5543
|
+
i++;
|
|
5544
|
+
}
|
|
5545
|
+
if (end === -1) break;
|
|
5546
|
+
out = out.slice(0, start) + out.slice(end);
|
|
5547
|
+
}
|
|
5548
|
+
}
|
|
5549
|
+
return out;
|
|
5550
|
+
}
|
|
5551
|
+
|
|
5552
|
+
function stripSvelteKeyDelimiters(markup) {
|
|
5553
|
+
let out = String(markup || '');
|
|
5554
|
+
for (;;) {
|
|
5555
|
+
const start = out.indexOf('{#key');
|
|
5556
|
+
if (start === -1) break;
|
|
5557
|
+
// The opening tag runs to its matching close brace (expressions inside
|
|
5558
|
+
// may nest braces).
|
|
5559
|
+
let depth = 0;
|
|
5560
|
+
let i = start;
|
|
5561
|
+
let openEnd = -1;
|
|
5562
|
+
while (i < out.length) {
|
|
5563
|
+
if (out[i] === '{') depth++;
|
|
5564
|
+
else if (out[i] === '}') {
|
|
5565
|
+
depth--;
|
|
5566
|
+
if (depth === 0) { openEnd = i + 1; break; }
|
|
5567
|
+
}
|
|
5568
|
+
i++;
|
|
5569
|
+
}
|
|
5570
|
+
if (openEnd === -1) break;
|
|
5571
|
+
out = out.slice(0, start) + out.slice(openEnd);
|
|
5572
|
+
}
|
|
5573
|
+
return out.split('{/key}').join('');
|
|
5574
|
+
}
|
|
5575
|
+
|
|
5576
|
+
function cloneWithoutElements(rootEl, excludedEls) {
|
|
5577
|
+
if (!excludedEls || excludedEls.length === 0) return rootEl;
|
|
5578
|
+
const excludedSet = new Set(excludedEls);
|
|
5579
|
+
// Mark originals, clone, then strip marked clones: identity does not
|
|
5580
|
+
// survive cloneNode, attributes do.
|
|
5581
|
+
const MARK = 'data-impeccable-hydration-excluded';
|
|
5582
|
+
for (const el of excludedSet) { try { el.setAttribute(MARK, '1'); } catch { /* detached */ } }
|
|
5583
|
+
let clone;
|
|
5584
|
+
try {
|
|
5585
|
+
clone = rootEl.cloneNode(true);
|
|
5586
|
+
clone.querySelectorAll('[' + MARK + ']').forEach((el) => el.remove());
|
|
5587
|
+
} finally {
|
|
5588
|
+
for (const el of excludedSet) { try { el.removeAttribute(MARK); } catch { /* detached */ } }
|
|
5589
|
+
}
|
|
5590
|
+
return clone || rootEl;
|
|
5591
|
+
}
|
|
5592
|
+
|
|
5234
5593
|
async function mountSvelteComponentVariant(variantNum) {
|
|
5235
5594
|
if (!svelteComponentSession || !variantNum) return false;
|
|
5236
5595
|
const { manifest, mountTargetEl, sessionId } = svelteComponentSession;
|
|
5596
|
+
// Resolved before the first await so the failure report can name the module
|
|
5597
|
+
// the browser could not reach, whichever step threw.
|
|
5598
|
+
const extension = manifest.componentExtension || 'svelte';
|
|
5599
|
+
// Prefer the server-stamped revision dir: its path changes on every
|
|
5600
|
+
// publish, which is what defeats stale transform caches for files the
|
|
5601
|
+
// dev server does not watch.
|
|
5602
|
+
const dirRel = manifest.revisionDir || manifest.componentDir || '';
|
|
5603
|
+
const dirAbs = manifest.revisionDirAbs || manifest.componentDirAbs || null;
|
|
5604
|
+
const moduleBase = manifest.componentModuleBase
|
|
5605
|
+
|| ('/' + String(dirRel).replace(/^\/+/, ''));
|
|
5606
|
+
const modulePath = String(moduleBase).replace(/\/+$/, '') + '/v' + variantNum + '.' + extension;
|
|
5607
|
+
const moduleAbs = dirAbs
|
|
5608
|
+
? String(dirAbs).replace(/\/+$/, '') + '/v' + variantNum + '.' + extension
|
|
5609
|
+
: null;
|
|
5610
|
+
const candidates = componentModuleCandidates(manifest, modulePath, moduleAbs);
|
|
5611
|
+
let moduleUrl = candidates[0];
|
|
5237
5612
|
try {
|
|
5238
5613
|
const previousAnchor = getMountedSvelteComponentAnchor(svelteComponentSession) || selectedElement;
|
|
5239
5614
|
svelteComponentSession.swapAnchor = makeFrozenAnchor(previousAnchor) || svelteComponentSession.swapAnchor || null;
|
|
5240
|
-
const runtime = await loadSvelteRuntime(manifest.runtimeModule);
|
|
5241
|
-
const
|
|
5242
|
-
|
|
5243
|
-
const mod =
|
|
5615
|
+
const runtime = await loadSvelteRuntime(manifest.runtimeModule, manifest);
|
|
5616
|
+
const imported = await importFirstReachable(candidates, true);
|
|
5617
|
+
moduleUrl = imported.url;
|
|
5618
|
+
const mod = imported.mod;
|
|
5244
5619
|
const Component = mod.default;
|
|
5245
5620
|
if (svelteComponentSession.mountedInstance && runtime.unmount) {
|
|
5246
5621
|
await runtime.unmount(svelteComponentSession.mountedInstance);
|
|
@@ -5253,7 +5628,7 @@
|
|
|
5253
5628
|
});
|
|
5254
5629
|
svelteComponentSession.mountedVariant = variantNum;
|
|
5255
5630
|
svelteComponentSession.runtime = runtime;
|
|
5256
|
-
|
|
5631
|
+
removeSvelteComponentVariantStyle(svelteComponentSession);
|
|
5257
5632
|
if (state === 'CYCLING') syncCyclingControls();
|
|
5258
5633
|
const nextAnchor = getMountedSvelteComponentAnchor(svelteComponentSession);
|
|
5259
5634
|
if (nextAnchor) {
|
|
@@ -5274,16 +5649,44 @@
|
|
|
5274
5649
|
selectedElement = settledAnchor;
|
|
5275
5650
|
});
|
|
5276
5651
|
}
|
|
5652
|
+
// Render truth, not publish truth: this is the only point in the whole
|
|
5653
|
+
// pipeline that proves the user can see variant N.
|
|
5654
|
+
reportVariantMounted(sessionId, variantNum, moduleUrl);
|
|
5655
|
+
if (mountErrorState?.sessionId === sessionId && mountErrorState.variant === variantNum) {
|
|
5656
|
+
clearMountErrorCard();
|
|
5657
|
+
}
|
|
5277
5658
|
return true;
|
|
5278
5659
|
} catch (err) {
|
|
5279
5660
|
if (svelteComponentSession?.sessionId === sessionId) {
|
|
5280
5661
|
svelteComponentSession.swapAnchor = null;
|
|
5281
5662
|
}
|
|
5282
|
-
console.error('[impeccable] Failed to mount
|
|
5663
|
+
console.error('[impeccable] Failed to mount component variant ' + variantNum + ' for ' + sessionId + ':', err);
|
|
5664
|
+
reportVariantMountFailed(sessionId, variantNum, moduleUrl, err);
|
|
5665
|
+
// Every mount failure gets the card, so the variant-switch path (which
|
|
5666
|
+
// used to revert with no feedback whatsoever) says what broke too.
|
|
5667
|
+
showMountErrorCard(sessionId, {
|
|
5668
|
+
variant: variantNum,
|
|
5669
|
+
url: moduleUrl,
|
|
5670
|
+
message: await describeMountFailure(manifest, err),
|
|
5671
|
+
});
|
|
5283
5672
|
return false;
|
|
5284
5673
|
}
|
|
5285
5674
|
}
|
|
5286
5675
|
|
|
5676
|
+
// Distinguishes a broken variant from an unreachable preview tree; the
|
|
5677
|
+
// recovery differs (fix the component vs fix the root/dev-server pair).
|
|
5678
|
+
async function describeMountFailure(manifest, err) {
|
|
5679
|
+
try {
|
|
5680
|
+
const probe = await probePreviewTree(manifest);
|
|
5681
|
+
if (probe.ok === false) {
|
|
5682
|
+
return 'The preview tree is not reachable from the dev server (probe failed on '
|
|
5683
|
+
+ (probe.tried || []).join(', ')
|
|
5684
|
+
+ '). The resolved app root and the dev server root likely disagree; restart live from the app the dev server serves.';
|
|
5685
|
+
}
|
|
5686
|
+
} catch { /* probe is best-effort */ }
|
|
5687
|
+
return 'The compiled component could not be imported or mounted. ' + (err?.message || 'Unknown error');
|
|
5688
|
+
}
|
|
5689
|
+
|
|
5287
5690
|
function teardownSvelteComponentSession(restoreOriginal) {
|
|
5288
5691
|
if (!svelteComponentSession) return;
|
|
5289
5692
|
const { wrapperEl, detachedOriginal, runtime, mountedInstance } = svelteComponentSession;
|
|
@@ -5336,48 +5739,78 @@
|
|
|
5336
5739
|
}
|
|
5337
5740
|
|
|
5338
5741
|
async function injectSvelteComponentsFromManifest(manifestPath, sessionId) {
|
|
5742
|
+
// Every (re)injection is a fresh attempt: reset the failure dedupe so a
|
|
5743
|
+
// republish that is STILL broken at the same URL reports again instead of
|
|
5744
|
+
// being swallowed while the agent believes the repair landed.
|
|
5745
|
+
lastReportedMountFailure = null;
|
|
5339
5746
|
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(manifestPath);
|
|
5340
5747
|
try {
|
|
5341
5748
|
const res = await fetch(url);
|
|
5342
5749
|
if (!res.ok) throw new Error(String(res.status));
|
|
5343
5750
|
const manifest = JSON.parse(await res.text());
|
|
5344
|
-
if (manifest.id !== sessionId)
|
|
5751
|
+
if (manifest.id !== sessionId) {
|
|
5752
|
+
// A manifest at the expected path belonging to a different session is
|
|
5753
|
+
// an agent-side publish error. Left as a bare return it stranded the
|
|
5754
|
+
// bar in GENERATING with no explanation and no event.
|
|
5755
|
+
const mismatch = 'Manifest at ' + manifestPath + ' belongs to session ' + (manifest.id || 'unknown') + ', not ' + sessionId + '.';
|
|
5756
|
+
reportVariantMountFailed(sessionId, visibleVariant || 1, manifestPath, mismatch);
|
|
5757
|
+
showMountErrorCard(sessionId, {
|
|
5758
|
+
variant: visibleVariant || 0,
|
|
5759
|
+
url: manifestPath,
|
|
5760
|
+
message: 'The variant manifest is for a different session. Ask the agent to republish.',
|
|
5761
|
+
previewFile: manifestPath,
|
|
5762
|
+
});
|
|
5763
|
+
return;
|
|
5764
|
+
}
|
|
5345
5765
|
|
|
5346
5766
|
const paramsByVariant = await loadSvelteComponentParams(manifest);
|
|
5767
|
+
const availableVariants = Number(manifest.arrivedVariants) || Number(manifest.count) || 1;
|
|
5768
|
+
const componentPreviewMode = isFrameworkComponentPreviewMode(manifest.previewMode)
|
|
5769
|
+
? manifest.previewMode
|
|
5770
|
+
: 'svelte-component';
|
|
5347
5771
|
currentSessionId = sessionId;
|
|
5348
5772
|
expectedVariants = Number(manifest.count) || expectedVariants || 1;
|
|
5349
5773
|
rememberSessionFileMeta({
|
|
5350
5774
|
sourceFile: manifest.sourceFile,
|
|
5351
5775
|
previewFile: manifestPath,
|
|
5352
|
-
previewMode:
|
|
5776
|
+
previewMode: componentPreviewMode,
|
|
5353
5777
|
});
|
|
5354
5778
|
if (state !== 'CYCLING') setLiveState('GENERATING');
|
|
5355
5779
|
|
|
5356
5780
|
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
|
|
5357
5781
|
if (existingWrapper && svelteComponentSession?.sessionId === sessionId) {
|
|
5358
5782
|
recoveryWaitingForAnchor = false;
|
|
5783
|
+
svelteComponentSession.manifest = manifest;
|
|
5359
5784
|
svelteComponentSession.paramsByVariant = paramsByVariant;
|
|
5360
|
-
arrivedVariants =
|
|
5361
|
-
expectedVariants = arrivedVariants;
|
|
5785
|
+
arrivedVariants = availableVariants;
|
|
5786
|
+
expectedVariants = Number(manifest.count) || expectedVariants || arrivedVariants;
|
|
5362
5787
|
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants ? visibleVariant : 1;
|
|
5363
|
-
await mountSvelteComponentVariant(visibleVariant || 1);
|
|
5788
|
+
const remounted = await mountSvelteComponentVariant(visibleVariant || 1);
|
|
5789
|
+
if (!remounted) {
|
|
5790
|
+
// The mount already reported the failure and raised the card.
|
|
5791
|
+
// Advancing to CYCLING here would show a bar claiming variants are
|
|
5792
|
+
// ready over a page where nothing rendered.
|
|
5793
|
+
saveSession();
|
|
5794
|
+
return;
|
|
5795
|
+
}
|
|
5364
5796
|
setLiveState('CYCLING');
|
|
5365
5797
|
showOrUpdateCyclingBar();
|
|
5366
5798
|
saveSession();
|
|
5799
|
+
if (parameterGenerationState === 'loading') completeParameterPublication();
|
|
5367
5800
|
return;
|
|
5368
5801
|
}
|
|
5369
5802
|
|
|
5370
5803
|
const liveEl = findLiveElementForSvelteManifest(manifest);
|
|
5371
5804
|
if (!liveEl?.parentElement) {
|
|
5372
5805
|
console.warn('[impeccable] Could not find original element in live DOM.');
|
|
5373
|
-
arrivedVariants =
|
|
5374
|
-
expectedVariants = arrivedVariants;
|
|
5806
|
+
arrivedVariants = availableVariants;
|
|
5807
|
+
expectedVariants = Number(manifest.count) || expectedVariants || arrivedVariants;
|
|
5375
5808
|
const saved = loadSession();
|
|
5376
5809
|
const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0;
|
|
5377
5810
|
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants
|
|
5378
5811
|
? visibleVariant
|
|
5379
5812
|
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
|
|
5380
|
-
enterRecoveryWaitingForAnchor({ checkpointReason: '
|
|
5813
|
+
enterRecoveryWaitingForAnchor({ checkpointReason: 'component_preview_anchor_missing', trackScroll: true });
|
|
5381
5814
|
waitForSvelteComponentTargetAndRetry({ manifestPath, sessionId, manifest });
|
|
5382
5815
|
return;
|
|
5383
5816
|
}
|
|
@@ -5385,7 +5818,7 @@
|
|
|
5385
5818
|
const wrapper = document.createElement('div');
|
|
5386
5819
|
wrapper.dataset.impeccableVariants = sessionId;
|
|
5387
5820
|
wrapper.dataset.impeccableVariantCount = String(manifest.count || expectedVariants || 1);
|
|
5388
|
-
wrapper.dataset.impeccablePreview =
|
|
5821
|
+
wrapper.dataset.impeccablePreview = componentPreviewMode;
|
|
5389
5822
|
wrapper.style.display = 'contents';
|
|
5390
5823
|
|
|
5391
5824
|
const mountTarget = document.createElement('div');
|
|
@@ -5423,8 +5856,8 @@
|
|
|
5423
5856
|
recoveryWaitingForAnchor = false;
|
|
5424
5857
|
|
|
5425
5858
|
const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0;
|
|
5426
|
-
arrivedVariants =
|
|
5427
|
-
expectedVariants = arrivedVariants;
|
|
5859
|
+
arrivedVariants = availableVariants;
|
|
5860
|
+
expectedVariants = Number(manifest.count) || expectedVariants || arrivedVariants;
|
|
5428
5861
|
const saved = loadSession();
|
|
5429
5862
|
const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0;
|
|
5430
5863
|
visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants
|
|
@@ -5434,9 +5867,11 @@
|
|
|
5434
5867
|
const mounted = await mountSvelteComponentVariant(visibleVariant);
|
|
5435
5868
|
if (!mounted) {
|
|
5436
5869
|
// The compiled component threw (e.g. a Svelte compile error in the
|
|
5437
|
-
// variant file).
|
|
5438
|
-
//
|
|
5439
|
-
|
|
5870
|
+
// variant file). mountSvelteComponentVariant already reported the
|
|
5871
|
+
// failure and raised the card; tear the half-built preview down but
|
|
5872
|
+
// keep the session so Retry and a republish still have something to
|
|
5873
|
+
// act on.
|
|
5874
|
+
abortSvelteComponentInjection(sessionId);
|
|
5440
5875
|
return;
|
|
5441
5876
|
}
|
|
5442
5877
|
|
|
@@ -5449,10 +5884,19 @@
|
|
|
5449
5884
|
refreshParamsPanel();
|
|
5450
5885
|
positionBar();
|
|
5451
5886
|
saveSession();
|
|
5452
|
-
|
|
5887
|
+
if (parameterGenerationState === 'loading') completeParameterPublication();
|
|
5888
|
+
console.log('[impeccable] Mounted ' + arrivedVariants + ' ' + manifest.framework + ' component variants.');
|
|
5453
5889
|
} catch (err) {
|
|
5454
|
-
console.error('[impeccable] Failed to mount
|
|
5455
|
-
|
|
5890
|
+
console.error('[impeccable] Failed to mount component-preview variants:', err);
|
|
5891
|
+
// Report the manifest PATH, never the fetch URL: that URL carries the
|
|
5892
|
+
// live helper token and this string is journaled.
|
|
5893
|
+
reportVariantMountFailed(sessionId, visibleVariant || 1, manifestPath, err);
|
|
5894
|
+
abortSvelteComponentInjection(sessionId, {
|
|
5895
|
+
variant: visibleVariant || 0,
|
|
5896
|
+
url: manifestPath,
|
|
5897
|
+
message: 'Could not read the variant manifest. ' + (err?.message || 'Unknown error'),
|
|
5898
|
+
previewFile: manifestPath,
|
|
5899
|
+
});
|
|
5456
5900
|
}
|
|
5457
5901
|
}
|
|
5458
5902
|
|
|
@@ -5473,10 +5917,187 @@
|
|
|
5473
5917
|
pendingSvelteComponentRetryObserver.observe(document.body, { childList: true, subtree: true });
|
|
5474
5918
|
}
|
|
5475
5919
|
|
|
5476
|
-
//
|
|
5477
|
-
//
|
|
5478
|
-
//
|
|
5479
|
-
|
|
5920
|
+
//
|
|
5921
|
+
// Mount acknowledgements
|
|
5922
|
+
//
|
|
5923
|
+
// The agent's `done` says it published files. Only the browser knows whether
|
|
5924
|
+
// the import resolved and the component reached the DOM. These two events
|
|
5925
|
+
// carry that answer back, so the journal, `live-status`, and `live-resume`
|
|
5926
|
+
// can tell "the user is comparing variants" from "nothing ever rendered".
|
|
5927
|
+
|
|
5928
|
+
// Mirror of the caps in live/event-validation.mjs. Trimming here keeps a
|
|
5929
|
+
// stack-trace-sized error from being rejected outright and lost.
|
|
5930
|
+
const MOUNT_URL_MAX = 2000;
|
|
5931
|
+
const MOUNT_ERROR_MAX = 1000;
|
|
5932
|
+
|
|
5933
|
+
function reportVariantMounted(sessionId, variantNum, moduleUrl) {
|
|
5934
|
+
const variant = Math.floor(Number(variantNum) || 0);
|
|
5935
|
+
if (!sessionId || variant < 1) return;
|
|
5936
|
+
sendEvent({
|
|
5937
|
+
type: 'variant_mounted',
|
|
5938
|
+
id: sessionId,
|
|
5939
|
+
variant,
|
|
5940
|
+
url: moduleUrl ? String(moduleUrl).slice(0, MOUNT_URL_MAX) : undefined,
|
|
5941
|
+
});
|
|
5942
|
+
}
|
|
5943
|
+
|
|
5944
|
+
function reportVariantMountFailed(sessionId, variantNum, moduleUrl, error) {
|
|
5945
|
+
if (!sessionId) return;
|
|
5946
|
+
const parsed = Math.floor(Number(variantNum) || 0);
|
|
5947
|
+
const variant = parsed >= 1 ? parsed : 1;
|
|
5948
|
+
const url = String(moduleUrl || 'unknown').slice(0, MOUNT_URL_MAX);
|
|
5949
|
+
const message = String(error?.message || error || 'Unknown mount error').slice(0, MOUNT_ERROR_MAX);
|
|
5950
|
+
// Progressive delivery and the Retry button both re-enter the same failure.
|
|
5951
|
+
// Report each distinct one once so the agent's poll queue and the journal
|
|
5952
|
+
// stay readable; a genuinely new failure (different variant, URL, or
|
|
5953
|
+
// message) still gets through.
|
|
5954
|
+
const key = sessionId + '|' + variant + '|' + url + '|' + message;
|
|
5955
|
+
if (lastReportedMountFailure === key) return;
|
|
5956
|
+
lastReportedMountFailure = key;
|
|
5957
|
+
sendEvent({ type: 'variant_mount_failed', id: sessionId, variant, url, error: message });
|
|
5958
|
+
}
|
|
5959
|
+
|
|
5960
|
+
function truncateMiddle(value, max) {
|
|
5961
|
+
const text = String(value || '');
|
|
5962
|
+
if (text.length <= max) return text;
|
|
5963
|
+
const head = Math.ceil((max - 1) / 2);
|
|
5964
|
+
const tail = max - 1 - head;
|
|
5965
|
+
return text.slice(0, head) + '…' + text.slice(text.length - tail);
|
|
5966
|
+
}
|
|
5967
|
+
|
|
5968
|
+
/**
|
|
5969
|
+
* Persistent failure surface. Replaces the old 5s toast: a toast that
|
|
5970
|
+
* disappears while the session is unusable is indistinguishable from no
|
|
5971
|
+
* feedback at all, and the wipe that came with it deleted the only handle on
|
|
5972
|
+
* a session the server still considered live.
|
|
5973
|
+
*/
|
|
5974
|
+
function showMountErrorCard(sessionId, details) {
|
|
5975
|
+
mountErrorState = {
|
|
5976
|
+
sessionId: sessionId || currentSessionId || null,
|
|
5977
|
+
variant: Math.floor(Number(details?.variant) || 0),
|
|
5978
|
+
url: details?.url ? String(details.url) : '',
|
|
5979
|
+
message: details?.message || 'A variant failed to load.',
|
|
5980
|
+
previewFile: details?.previewFile || currentPreviewFile || null,
|
|
5981
|
+
};
|
|
5982
|
+
renderMountErrorCard();
|
|
5983
|
+
}
|
|
5984
|
+
|
|
5985
|
+
function clearMountErrorCard() {
|
|
5986
|
+
mountErrorState = null;
|
|
5987
|
+
if (mountErrorEl) {
|
|
5988
|
+
mountErrorEl.remove();
|
|
5989
|
+
mountErrorEl = null;
|
|
5990
|
+
}
|
|
5991
|
+
}
|
|
5992
|
+
|
|
5993
|
+
function mountErrorCardBottomOffset() {
|
|
5994
|
+
const barRect = globalBarEl?.getBoundingClientRect();
|
|
5995
|
+
return barRect && barRect.height > 0
|
|
5996
|
+
? Math.max(16, window.innerHeight - barRect.top + 12)
|
|
5997
|
+
: 16;
|
|
5998
|
+
}
|
|
5999
|
+
|
|
6000
|
+
function renderMountErrorCard() {
|
|
6001
|
+
if (!mountErrorState) return;
|
|
6002
|
+
if (mountErrorEl) mountErrorEl.remove();
|
|
6003
|
+
const P = BP || barPaletteForTheme(detectPageTheme());
|
|
6004
|
+
const card = el('div', {
|
|
6005
|
+
position: 'fixed', bottom: mountErrorCardBottomOffset() + 'px', left: '50%',
|
|
6006
|
+
transform: 'translateX(-50%)',
|
|
6007
|
+
display: 'flex', flexDirection: 'column', gap: '6px',
|
|
6008
|
+
background: P.surface, color: P.text,
|
|
6009
|
+
border: '1px solid oklch(65% 0.18 30 / 0.55)',
|
|
6010
|
+
borderRadius: '8px', padding: '10px 12px',
|
|
6011
|
+
fontFamily: FONT, fontSize: '12px',
|
|
6012
|
+
boxShadow: P.shadow, zIndex: Z.toast,
|
|
6013
|
+
maxWidth: 'min(520px, calc(100vw - 32px))',
|
|
6014
|
+
pointerEvents: 'auto', textAlign: 'left',
|
|
6015
|
+
});
|
|
6016
|
+
card.id = PREFIX + '-mount-error';
|
|
6017
|
+
|
|
6018
|
+
const head = el('div', { display: 'flex', alignItems: 'center', gap: '8px' });
|
|
6019
|
+
const glyph = el('span', { fontSize: '13px', lineHeight: '1', color: 'oklch(62% 0.19 30)', flexShrink: '0' });
|
|
6020
|
+
glyph.textContent = '⚠';
|
|
6021
|
+
head.appendChild(glyph);
|
|
6022
|
+
const title = el('span', { fontWeight: '600', flex: '1' });
|
|
6023
|
+
title.textContent = mountErrorState.variant > 0
|
|
6024
|
+
? 'Variant ' + mountErrorState.variant + ' failed to load'
|
|
6025
|
+
: 'Variants failed to load';
|
|
6026
|
+
head.appendChild(title);
|
|
6027
|
+
const dismiss = el('button', {
|
|
6028
|
+
border: 'none', background: 'transparent', color: P.textDim,
|
|
6029
|
+
cursor: 'pointer', fontFamily: FONT, fontSize: '14px', lineHeight: '1',
|
|
6030
|
+
padding: '0 2px', flexShrink: '0',
|
|
6031
|
+
});
|
|
6032
|
+
dismiss.textContent = '×';
|
|
6033
|
+
dismiss.setAttribute('aria-label', 'Dismiss');
|
|
6034
|
+
dismiss.addEventListener('click', (e) => {
|
|
6035
|
+
e.stopPropagation();
|
|
6036
|
+
clearMountErrorCard();
|
|
6037
|
+
// The card was the only recovery affordance while the bar is hidden;
|
|
6038
|
+
// dismissing it must hand the user back a usable surface. PICKING
|
|
6039
|
+
// reactivates the global mark and the picker. The saved session and
|
|
6040
|
+
// server truth survive, so a later republish (SSE `done`) still
|
|
6041
|
+
// resurrects the comparison through the normal handlers.
|
|
6042
|
+
if (state === 'GENERATING') setLiveState('PICKING');
|
|
6043
|
+
});
|
|
6044
|
+
head.appendChild(dismiss);
|
|
6045
|
+
card.appendChild(head);
|
|
6046
|
+
|
|
6047
|
+
const body = el('div', { color: P.textDim, lineHeight: '1.4' });
|
|
6048
|
+
body.textContent = mountErrorState.message;
|
|
6049
|
+
card.appendChild(body);
|
|
6050
|
+
|
|
6051
|
+
if (mountErrorState.url) {
|
|
6052
|
+
const urlLine = el('div', {
|
|
6053
|
+
fontFamily: MONO, fontSize: '11px', color: P.textDim,
|
|
6054
|
+
wordBreak: 'break-all', opacity: '0.85',
|
|
6055
|
+
});
|
|
6056
|
+
urlLine.textContent = truncateMiddle(mountErrorState.url, 72);
|
|
6057
|
+
urlLine.title = mountErrorState.url;
|
|
6058
|
+
card.appendChild(urlLine);
|
|
6059
|
+
}
|
|
6060
|
+
|
|
6061
|
+
const actions = el('div', { display: 'flex', gap: '8px', marginTop: '2px' });
|
|
6062
|
+
const retry = el('button', {
|
|
6063
|
+
border: '1px solid ' + P.hairline, background: 'transparent',
|
|
6064
|
+
color: P.text, fontFamily: FONT, fontSize: '12px', fontWeight: '500',
|
|
6065
|
+
borderRadius: '5px', padding: '4px 10px', cursor: 'pointer',
|
|
6066
|
+
});
|
|
6067
|
+
retry.textContent = 'Retry';
|
|
6068
|
+
retry.dataset.impeccableMountRetry = 'true';
|
|
6069
|
+
retry.addEventListener('click', (e) => { e.stopPropagation(); retryMountErrorCard(); });
|
|
6070
|
+
actions.appendChild(retry);
|
|
6071
|
+
card.appendChild(actions);
|
|
6072
|
+
|
|
6073
|
+
mountErrorEl = card;
|
|
6074
|
+
uiAppend(card);
|
|
6075
|
+
defangOutsideHandlers(card);
|
|
6076
|
+
}
|
|
6077
|
+
|
|
6078
|
+
function retryMountErrorCard() {
|
|
6079
|
+
const info = mountErrorState;
|
|
6080
|
+
if (!info) return;
|
|
6081
|
+
const sessionId = info.sessionId || currentSessionId;
|
|
6082
|
+
const manifestPath = info.previewFile || currentPreviewFile;
|
|
6083
|
+
clearMountErrorCard();
|
|
6084
|
+
if (!sessionId || !manifestPath) {
|
|
6085
|
+
showToast('No variant manifest to retry. Ask the agent to republish.', 5000);
|
|
6086
|
+
return;
|
|
6087
|
+
}
|
|
6088
|
+
// A retry must be able to report the same failure again, otherwise a second
|
|
6089
|
+
// attempt against an unchanged broken module would look silent.
|
|
6090
|
+
lastReportedMountFailure = null;
|
|
6091
|
+
if (state !== 'CYCLING') setLiveState('GENERATING');
|
|
6092
|
+
injectSvelteComponentsFromManifest(manifestPath, sessionId);
|
|
6093
|
+
}
|
|
6094
|
+
|
|
6095
|
+
// Tear down a component preview that could not mount, WITHOUT touching
|
|
6096
|
+
// session identity. The old version cleared localStorage, nulled
|
|
6097
|
+
// currentSessionId, and reset to PICKING, which orphaned a session the server
|
|
6098
|
+
// still had in its journal and made every recovery path unreachable. The DOM
|
|
6099
|
+
// teardown and observer cleanup are still right; the state wipe never was.
|
|
6100
|
+
function abortSvelteComponentInjection(sessionId, details) {
|
|
5480
6101
|
try {
|
|
5481
6102
|
if (svelteComponentSession?.sessionId === sessionId) {
|
|
5482
6103
|
teardownSvelteComponentSession(true);
|
|
@@ -5488,14 +6109,52 @@
|
|
|
5488
6109
|
console.warn('[impeccable] Svelte component abort cleanup failed:', err);
|
|
5489
6110
|
}
|
|
5490
6111
|
hideShaderOverlay();
|
|
6112
|
+
if (pendingSvelteComponentRetryObserver) { pendingSvelteComponentRetryObserver.disconnect(); pendingSvelteComponentRetryObserver = null; }
|
|
6113
|
+
if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
|
|
6114
|
+
// The generate submit armed a scroll lock and a variant observer; a page
|
|
6115
|
+
// the user cannot scroll, watched by a stale observer, is exactly the
|
|
6116
|
+
// wrong place to show a card asking them to act.
|
|
6117
|
+
stopScrollLock();
|
|
6118
|
+
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
|
|
6119
|
+
removeVariantStateStylesheet();
|
|
6120
|
+
hideBar(true);
|
|
6121
|
+
// currentSessionId, the saved session, and the file metadata all survive on
|
|
6122
|
+
// purpose: Retry, a republish from the agent, and a page reload all need
|
|
6123
|
+
// them. saveSession keeps the localStorage cache in step with the server.
|
|
6124
|
+
saveSession();
|
|
6125
|
+
if (details) showMountErrorCard(sessionId, details);
|
|
6126
|
+
else if (!mountErrorState) {
|
|
6127
|
+
showMountErrorCard(sessionId, { message: 'Variants could not be mounted. Retry, or ask the agent to republish.' });
|
|
6128
|
+
}
|
|
6129
|
+
}
|
|
6130
|
+
|
|
6131
|
+
// Hard reset for the one case that is not a mount failure: a cycling state
|
|
6132
|
+
// with nothing to cycle. There is no variant to retry and no URL to report,
|
|
6133
|
+
// so the session really is over.
|
|
6134
|
+
function resetSvelteComponentSession(sessionId, message) {
|
|
6135
|
+
try {
|
|
6136
|
+
if (svelteComponentSession?.sessionId === sessionId) {
|
|
6137
|
+
teardownSvelteComponentSession(true);
|
|
6138
|
+
} else {
|
|
6139
|
+
const orphan = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
|
|
6140
|
+
if (orphan) orphan.remove();
|
|
6141
|
+
}
|
|
6142
|
+
} catch (err) {
|
|
6143
|
+
console.warn('[impeccable] Svelte component reset cleanup failed:', err);
|
|
6144
|
+
}
|
|
6145
|
+
hideShaderOverlay();
|
|
5491
6146
|
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
|
|
5492
6147
|
if (pendingSvelteComponentRetryObserver) { pendingSvelteComponentRetryObserver.disconnect(); pendingSvelteComponentRetryObserver = null; }
|
|
5493
6148
|
if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
|
|
5494
6149
|
stopScrollLock();
|
|
6150
|
+
removeVariantStateStylesheet();
|
|
6151
|
+
clearMountErrorCard();
|
|
5495
6152
|
clearSession();
|
|
5496
6153
|
clearHandled();
|
|
5497
6154
|
resetSessionFileMeta();
|
|
5498
6155
|
currentSessionId = null;
|
|
6156
|
+
parameterGenerationState = 'idle';
|
|
6157
|
+
parameterReadyAnnouncedSession = null;
|
|
5499
6158
|
expectedVariants = 0;
|
|
5500
6159
|
arrivedVariants = 0;
|
|
5501
6160
|
visibleVariant = 0;
|
|
@@ -5505,12 +6164,40 @@
|
|
|
5505
6164
|
if (message) showToast(message, 5000);
|
|
5506
6165
|
}
|
|
5507
6166
|
|
|
6167
|
+
// How many delayed re-reads a completion-driven source fallback gets when
|
|
6168
|
+
// the fetched source still shows only the preflight scaffold, before the
|
|
6169
|
+
// failure is surfaced via recoverEmptyCycling.
|
|
6170
|
+
const COMPLETED_SOURCE_FALLBACK_RETRIES = 3;
|
|
6171
|
+
const COMPLETED_SOURCE_FALLBACK_RETRY_MS = 1200;
|
|
6172
|
+
|
|
6173
|
+
/**
|
|
6174
|
+
* Terminal recovery for a session whose source-side scaffolding no longer
|
|
6175
|
+
* exists. The discard event is best-effort: with no agent polling it parks
|
|
6176
|
+
* the durable session in discard_requested, which no resume path adopts;
|
|
6177
|
+
* with an agent attached it triggers the normal discard finalization.
|
|
6178
|
+
*/
|
|
6179
|
+
function discardOrphanedSession(reason) {
|
|
6180
|
+
const sessionId = currentSessionId;
|
|
6181
|
+
if (!sessionId) return;
|
|
6182
|
+
console.warn('[impeccable] Discarding orphaned session ' + sessionId + ': ' + reason);
|
|
6183
|
+
sendEvent({ type: 'discard', id: sessionId, orphaned: true }).catch(() => {});
|
|
6184
|
+
markSessionHandled();
|
|
6185
|
+
cleanup({ instantChrome: true });
|
|
6186
|
+
showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000);
|
|
6187
|
+
}
|
|
6188
|
+
|
|
5508
6189
|
/**
|
|
5509
6190
|
* No-HMR fallback: fetch the raw source file from the live server,
|
|
5510
6191
|
* parse it, extract the variant wrapper, and inject it into the live DOM.
|
|
5511
6192
|
* This works even when the dev server caches HTML (Bun, static servers).
|
|
6193
|
+
*
|
|
6194
|
+
* opts.generationCompleted marks callers that KNOW the agent finished (a
|
|
6195
|
+
* `done` arrived or the server reported a completed generation). For them an
|
|
6196
|
+
* empty read is a stale source view and no further event is coming, so the
|
|
6197
|
+
* read retries a few times and then surfaces recovery. Callers without the
|
|
6198
|
+
* flag may be mid-generation and wait indefinitely for the real completion.
|
|
5512
6199
|
*/
|
|
5513
|
-
function injectVariantsFromSource(filePath, sessionId) {
|
|
6200
|
+
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
|
|
5514
6201
|
if (isSvelteComponentManifestPath(filePath)) {
|
|
5515
6202
|
injectSvelteComponentsFromManifest(filePath, sessionId);
|
|
5516
6203
|
return;
|
|
@@ -5535,6 +6222,25 @@
|
|
|
5535
6222
|
srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
|
|
5536
6223
|
if (!srcWrapper) {
|
|
5537
6224
|
console.warn('[impeccable] Variant wrapper not found in source file.');
|
|
6225
|
+
// A resumed cycling session whose wrapper is gone from source is an
|
|
6226
|
+
// ORPHAN: the file was edited or regenerated out from under it, so
|
|
6227
|
+
// no reload, HMR push, or server restart can ever complete it, and
|
|
6228
|
+
// the frozen picker it leaves behind used to need a manual
|
|
6229
|
+
// live-complete --discarded. Retry a few reads first (an agent
|
|
6230
|
+
// rewrite or HMR patch may be mid-flight), then self-discard and
|
|
6231
|
+
// hand the surface back to the picker.
|
|
6232
|
+
if (opts.orphanDiscard && sessionId === currentSessionId) {
|
|
6233
|
+
const attempt = opts._orphanAttempt || 0;
|
|
6234
|
+
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
|
|
6235
|
+
setTimeout(() => {
|
|
6236
|
+
if (sessionId !== currentSessionId) return;
|
|
6237
|
+
if (state !== 'GENERATING' && state !== 'CYCLING') return;
|
|
6238
|
+
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
|
|
6239
|
+
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
|
|
6240
|
+
} else {
|
|
6241
|
+
discardOrphanedSession('variant wrapper missing from source');
|
|
6242
|
+
}
|
|
6243
|
+
}
|
|
5538
6244
|
return;
|
|
5539
6245
|
}
|
|
5540
6246
|
|
|
@@ -5576,6 +6282,32 @@
|
|
|
5576
6282
|
arrivedVariants = variants.length;
|
|
5577
6283
|
expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants);
|
|
5578
6284
|
if (arrivedVariants <= 0) {
|
|
6285
|
+
if (state === 'GENERATING') {
|
|
6286
|
+
// Mid-generation the source legitimately holds a scaffold wrapper
|
|
6287
|
+
// with no variants yet (the server-side preflight wraps before the
|
|
6288
|
+
// agent writes). Tearing the session down here would destroy an
|
|
6289
|
+
// in-flight generation; stay in GENERATING — the variant observer
|
|
6290
|
+
// is armed and the server re-delivers a missed `done`.
|
|
6291
|
+
if (!opts.generationCompleted) {
|
|
6292
|
+
console.log('[impeccable] Source has scaffold but no variants yet; still generating.');
|
|
6293
|
+
return;
|
|
6294
|
+
}
|
|
6295
|
+
// Generation finished, yet the read shows only the scaffold: the
|
|
6296
|
+
// source view is stale and no further event will fire. Re-read a
|
|
6297
|
+
// few times before surfacing recovery — a single silent return
|
|
6298
|
+
// here would strand the tab in GENERATING forever.
|
|
6299
|
+
const attempt = opts.attempt || 0;
|
|
6300
|
+
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
|
|
6301
|
+
console.log('[impeccable] Generation is done but source shows no variants yet; retrying read ('
|
|
6302
|
+
+ (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').');
|
|
6303
|
+
setTimeout(() => {
|
|
6304
|
+
if (state !== 'GENERATING' || currentSessionId !== sessionId) return;
|
|
6305
|
+
if (arrivedVariants > 0) return;
|
|
6306
|
+
injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 });
|
|
6307
|
+
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
|
|
6308
|
+
return;
|
|
6309
|
+
}
|
|
6310
|
+
}
|
|
5579
6311
|
recoverEmptyCycling('source-fallback-empty');
|
|
5580
6312
|
return;
|
|
5581
6313
|
}
|
|
@@ -5597,6 +6329,7 @@
|
|
|
5597
6329
|
refreshParamsPanel();
|
|
5598
6330
|
positionBar();
|
|
5599
6331
|
saveSession();
|
|
6332
|
+
if (parameterGenerationState === 'loading') completeParameterPublication();
|
|
5600
6333
|
console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
|
|
5601
6334
|
})
|
|
5602
6335
|
.catch(err => {
|
|
@@ -5806,13 +6539,98 @@
|
|
|
5806
6539
|
return variantDiv;
|
|
5807
6540
|
}
|
|
5808
6541
|
|
|
6542
|
+
// Variant visibility and range/toggle params are expressed through ONE
|
|
6543
|
+
// injected stylesheet, never inline attributes on the variant divs. Those
|
|
6544
|
+
// divs are scaffolded into page source, so SSR frameworks (Next.js App
|
|
6545
|
+
// Router) server-render them; toggling their `hidden` / inline `style` /
|
|
6546
|
+
// `--p-*` client-side trips a React 19 hydration mismatch on the next
|
|
6547
|
+
// Fast-Refresh re-render — the same failure mode the scroll-anchor (#276)
|
|
6548
|
+
// and pick-cursor (#286) fixes address. A stylesheet rule has the same
|
|
6549
|
+
// computed effect without mutating any hydrated element's attributes.
|
|
6550
|
+
// (steps params keep driving `data-p-*` attributes, matching scoped CSS.)
|
|
6551
|
+
const VARIANT_HIDE_DECL = 'display: none !important;';
|
|
6552
|
+
const VARIANT_SHOW_DECL = 'display: block !important;';
|
|
6553
|
+
|
|
6554
|
+
// Build a direct-child variant selector for a session. With `num`, targets a
|
|
6555
|
+
// single variant (`… > [data-impeccable-variant="N"]`); without it, targets
|
|
6556
|
+
// every variant via the bare `[data-impeccable-variant]` attribute.
|
|
6557
|
+
function variantStateSelector(sessionId, num) {
|
|
6558
|
+
const wrapper = '[data-impeccable-variants="' + sessionId + '"]';
|
|
6559
|
+
const variant = num == null
|
|
6560
|
+
? '[data-impeccable-variant]'
|
|
6561
|
+
: '[data-impeccable-variant="' + num + '"]';
|
|
6562
|
+
return wrapper + ' > ' + variant;
|
|
6563
|
+
}
|
|
6564
|
+
|
|
6565
|
+
// Serialize the visible variant's knob values into `--p-<id>` custom-property
|
|
6566
|
+
// declarations. Only range (number) and toggle (boolean) values become a
|
|
6567
|
+
// custom property; steps params drive `data-p-*` attributes instead.
|
|
6568
|
+
function variantParamDecls(values) {
|
|
6569
|
+
return Object.entries(values || {})
|
|
6570
|
+
.map(([id, val]) => {
|
|
6571
|
+
if (typeof val === 'number') return ' --p-' + id + ': ' + val + ';';
|
|
6572
|
+
if (typeof val === 'boolean') return ' --p-' + id + ': ' + (val ? '1' : '0') + ';';
|
|
6573
|
+
return '';
|
|
6574
|
+
})
|
|
6575
|
+
.join('');
|
|
6576
|
+
}
|
|
6577
|
+
|
|
6578
|
+
function updateVariantStateStylesheet(sessionId, num) {
|
|
6579
|
+
if (!sessionId || num == null || num < 1) return;
|
|
6580
|
+
|
|
6581
|
+
let styleEl = document.getElementById(VARIANT_STATE_STYLE_ID);
|
|
6582
|
+
if (!styleEl) {
|
|
6583
|
+
styleEl = document.createElement('style');
|
|
6584
|
+
styleEl.id = VARIANT_STATE_STYLE_ID;
|
|
6585
|
+
(document.head || document.documentElement).appendChild(styleEl);
|
|
6586
|
+
}
|
|
6587
|
+
|
|
6588
|
+
// Hide every variant except the visible one (incl. the SSR'd "original").
|
|
6589
|
+
const hideOthers = variantStateSelector(sessionId)
|
|
6590
|
+
+ ':not([data-impeccable-variant="' + num + '"]) { ' + VARIANT_HIDE_DECL + ' }';
|
|
6591
|
+
|
|
6592
|
+
// Force-show the visible variant (beats the source inline display:none on
|
|
6593
|
+
// v2/v3) and apply its knob values as custom properties.
|
|
6594
|
+
const showVisible = variantStateSelector(sessionId, num)
|
|
6595
|
+
+ ' { ' + VARIANT_SHOW_DECL + variantParamDecls(paramsCurrentValues) + ' }';
|
|
6596
|
+
|
|
6597
|
+
styleEl.textContent = hideOthers + '\n' + showVisible + '\n';
|
|
6598
|
+
}
|
|
6599
|
+
|
|
6600
|
+
function removeVariantStateStylesheet() {
|
|
6601
|
+
document.getElementById(VARIANT_STATE_STYLE_ID)?.remove();
|
|
6602
|
+
}
|
|
6603
|
+
|
|
6604
|
+
function showOriginalDuringDiscard(sessionId) {
|
|
6605
|
+
if (!sessionId) return;
|
|
6606
|
+
let styleEl = document.getElementById(DISCARD_STATE_STYLE_ID);
|
|
6607
|
+
if (!styleEl) {
|
|
6608
|
+
styleEl = document.createElement('style');
|
|
6609
|
+
styleEl.id = DISCARD_STATE_STYLE_ID;
|
|
6610
|
+
(document.head || document.documentElement).appendChild(styleEl);
|
|
6611
|
+
}
|
|
6612
|
+
const wrapper = '[data-impeccable-variants="' + sessionId + '"]';
|
|
6613
|
+
styleEl.textContent = wrapper + ' > [data-impeccable-variant]:not([data-impeccable-variant="original"]) { display:none !important; }\n'
|
|
6614
|
+
+ wrapper + ' > [data-impeccable-variant="original"] { display:block !important; }';
|
|
6615
|
+
}
|
|
6616
|
+
|
|
6617
|
+
function resolveScrollLockAnchorTop() {
|
|
6618
|
+
const anchor = resolveBarAnchor();
|
|
6619
|
+
if (!anchor?.isConnected) return null;
|
|
6620
|
+
const top = anchor.getBoundingClientRect().top;
|
|
6621
|
+
return Number.isFinite(top) ? top : null;
|
|
6622
|
+
}
|
|
6623
|
+
|
|
5809
6624
|
// Hold window.scrollY at a fixed value across DOM mutations inside the
|
|
5810
6625
|
// session's wrapper (HMR patches, variant inserts, cycle swaps).
|
|
5811
|
-
function startScrollLock(sessionId, initialTargetY) {
|
|
6626
|
+
function startScrollLock(sessionId, initialTargetY, initialAnchorTop) {
|
|
5812
6627
|
stopScrollLock();
|
|
5813
6628
|
scrollLockTargetY = typeof initialTargetY === 'number' && isFinite(initialTargetY)
|
|
5814
6629
|
? initialTargetY
|
|
5815
6630
|
: window.scrollY;
|
|
6631
|
+
scrollLockAnchorTop = typeof initialAnchorTop === 'number' && isFinite(initialAnchorTop)
|
|
6632
|
+
? initialAnchorTop
|
|
6633
|
+
: resolveScrollLockAnchorTop();
|
|
5816
6634
|
|
|
5817
6635
|
try { history.scrollRestoration = 'manual'; } catch {}
|
|
5818
6636
|
|
|
@@ -5836,6 +6654,17 @@
|
|
|
5836
6654
|
const correct = (why) => {
|
|
5837
6655
|
scrollLockRaf = null;
|
|
5838
6656
|
if (scrollLockTargetY == null) return;
|
|
6657
|
+
const anchor = resolveBarAnchor();
|
|
6658
|
+
if (anchor?.isConnected && typeof scrollLockAnchorTop === 'number' && isFinite(scrollLockAnchorTop)) {
|
|
6659
|
+
const anchorTop = anchor.getBoundingClientRect().top;
|
|
6660
|
+
const anchorDelta = anchorTop - scrollLockAnchorTop;
|
|
6661
|
+
if (Math.abs(anchorDelta) >= 0.5) {
|
|
6662
|
+
window.scrollTo({ top: window.scrollY + anchorDelta, left: window.scrollX, behavior: 'instant' });
|
|
6663
|
+
scrollLockTargetY = window.scrollY;
|
|
6664
|
+
writeScrollY(scrollLockTargetY);
|
|
6665
|
+
return;
|
|
6666
|
+
}
|
|
6667
|
+
}
|
|
5839
6668
|
const before = window.scrollY;
|
|
5840
6669
|
const delta = before - scrollLockTargetY;
|
|
5841
6670
|
if (Math.abs(delta) < 0.5) {
|
|
@@ -5880,6 +6709,7 @@
|
|
|
5880
6709
|
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
|
|
5881
6710
|
const prevTarget = scrollLockTargetY;
|
|
5882
6711
|
scrollLockTargetY = window.scrollY;
|
|
6712
|
+
scrollLockAnchorTop = resolveScrollLockAnchorTop();
|
|
5883
6713
|
writeScrollY(scrollLockTargetY);
|
|
5884
6714
|
};
|
|
5885
6715
|
const markGesture = (why) => {
|
|
@@ -5917,6 +6747,7 @@
|
|
|
5917
6747
|
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
|
|
5918
6748
|
if (scrollLockAbort) { scrollLockAbort.abort(); scrollLockAbort = null; }
|
|
5919
6749
|
scrollLockTargetY = null;
|
|
6750
|
+
scrollLockAnchorTop = null;
|
|
5920
6751
|
// NOTE: do NOT clear the persistent scroll key here. startScrollLock
|
|
5921
6752
|
// calls us as a reset, and clearing the key would nuke the Go-time
|
|
5922
6753
|
// scrollY that the next resume needs to read.
|
|
@@ -5990,6 +6821,7 @@
|
|
|
5990
6821
|
|
|
5991
6822
|
updating = true;
|
|
5992
6823
|
arrivedVariants = count;
|
|
6824
|
+
generationPhase = arrivedVariants >= expectedVariants ? 'variants_ready' : 'variants_progress';
|
|
5993
6825
|
if (visibleVariant === 0 && arrivedVariants > 0) {
|
|
5994
6826
|
const saved = loadSession();
|
|
5995
6827
|
const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0;
|
|
@@ -6005,7 +6837,7 @@
|
|
|
6005
6837
|
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
|
|
6006
6838
|
if (expected > 0) expectedVariants = expected;
|
|
6007
6839
|
|
|
6008
|
-
if (arrivedVariants
|
|
6840
|
+
if (arrivedVariants > 0) {
|
|
6009
6841
|
setLiveState('CYCLING');
|
|
6010
6842
|
recoveryWaitingForAnchor = false;
|
|
6011
6843
|
hideShaderOverlay();
|
|
@@ -6013,13 +6845,18 @@
|
|
|
6013
6845
|
updateSelectedElement();
|
|
6014
6846
|
showOrUpdateCyclingBar();
|
|
6015
6847
|
disableInlineEdit();
|
|
6016
|
-
refreshParamsPanel();
|
|
6848
|
+
if (arrivedVariants >= expectedVariants && expectedVariants > 0) refreshParamsPanel();
|
|
6849
|
+
else hideParamsPanel();
|
|
6017
6850
|
positionBar();
|
|
6018
6851
|
} else if (state === 'GENERATING') {
|
|
6019
6852
|
updateBarContent('generating');
|
|
6020
6853
|
}
|
|
6021
6854
|
saveSession();
|
|
6022
|
-
|
|
6855
|
+
sendCheckpoint(
|
|
6856
|
+
arrivedVariants >= expectedVariants && expectedVariants > 0
|
|
6857
|
+
? 'variants_ready'
|
|
6858
|
+
: 'variants_progress',
|
|
6859
|
+
);
|
|
6023
6860
|
updating = false;
|
|
6024
6861
|
});
|
|
6025
6862
|
|
|
@@ -6087,11 +6924,12 @@
|
|
|
6087
6924
|
switch (msg.type) {
|
|
6088
6925
|
case 'connected':
|
|
6089
6926
|
hasProjectContext = !!msg.hasProjectContext;
|
|
6090
|
-
if (!hasProjectContext) showToast(
|
|
6927
|
+
if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000);
|
|
6091
6928
|
console.log('[impeccable] Live mode connected.');
|
|
6092
6929
|
syncAgentPollingUi(!!msg.agentPolling);
|
|
6093
6930
|
startAgentStatusPoll();
|
|
6094
6931
|
restoreFromActiveSessions(msg.activeSessions, 'sse_connected');
|
|
6932
|
+
recoverMissedGenerationCompletion(msg.activeSessions);
|
|
6095
6933
|
if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
|
|
6096
6934
|
syncPageInteractionCursor();
|
|
6097
6935
|
syncPageChatFocus('sse-connected');
|
|
@@ -6099,6 +6937,38 @@
|
|
|
6099
6937
|
case 'agent_polling':
|
|
6100
6938
|
syncAgentPollingUi(!!msg.connected);
|
|
6101
6939
|
break;
|
|
6940
|
+
case 'agent_phase':
|
|
6941
|
+
if (msg.id === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
|
|
6942
|
+
// Advance the visible phase monotonically. A behind/resumed
|
|
6943
|
+
// checkpoint may carry an earlier phase for internal bookkeeping,
|
|
6944
|
+
// but the bar must not move backward.
|
|
6945
|
+
if (shouldAdvancePhase(generationPhase, msg.phase)) generationPhase = msg.phase;
|
|
6946
|
+
// The deferred parameter pass reports through `variant_progress`
|
|
6947
|
+
// with publicationKind 'params', not through agent_phase.
|
|
6948
|
+
updateBarContent(state === 'CYCLING' ? 'cycling' : 'generating');
|
|
6949
|
+
saveSession();
|
|
6950
|
+
}
|
|
6951
|
+
break;
|
|
6952
|
+
case 'variant_progress':
|
|
6953
|
+
if (msg.id === currentSessionId) {
|
|
6954
|
+
if (msg.publicationKind === 'params') parameterGenerationState = 'loading';
|
|
6955
|
+
rememberSessionFileMeta(msg);
|
|
6956
|
+
if (isFrameworkComponentPreviewMode(msg.previewMode) && msg.previewFile) {
|
|
6957
|
+
// Component-preview (Svelte/Vue) progressive delivery: the browser
|
|
6958
|
+
// mounts compiled components, so there is no framework-owned DOM
|
|
6959
|
+
// to race. Keep streaming each checkpoint into the preview.
|
|
6960
|
+
injectSvelteComponentsFromManifest(msg.previewFile, msg.id);
|
|
6961
|
+
}
|
|
6962
|
+
// Source-preview targets: do NOT source-inject per checkpoint.
|
|
6963
|
+
// Immediate injection races framework (React/Vue) ownership mid-
|
|
6964
|
+
// generation and triggers removeChild errors on the next HMR
|
|
6965
|
+
// commit. Let HMR own reconciliation while variants stream in;
|
|
6966
|
+
// source injection runs only on the final `done` (which keeps its
|
|
6967
|
+
// 750ms settle + retry ladder for non-HMR harnesses like Cursor).
|
|
6968
|
+
// The visible progress count still advances from the variant
|
|
6969
|
+
// MutationObserver as HMR lands each variant.
|
|
6970
|
+
}
|
|
6971
|
+
break;
|
|
6102
6972
|
case 'steer_done':
|
|
6103
6973
|
maybeCompleteSteer(msg);
|
|
6104
6974
|
break;
|
|
@@ -6116,6 +6986,10 @@
|
|
|
6116
6986
|
case 'done':
|
|
6117
6987
|
if (maybeCompleteSteer(msg)) break;
|
|
6118
6988
|
rememberSessionFileMeta(msg);
|
|
6989
|
+
if (msg.id === currentSessionId && isFrameworkComponentPreviewMode(currentPreviewMode) && currentPreviewFile) {
|
|
6990
|
+
injectSvelteComponentsFromManifest(currentPreviewFile, msg.id);
|
|
6991
|
+
break;
|
|
6992
|
+
}
|
|
6119
6993
|
// Variants already arrived via HMR → normal transition.
|
|
6120
6994
|
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
|
|
6121
6995
|
if (state === 'GENERATING') {
|
|
@@ -6131,7 +7005,7 @@
|
|
|
6131
7005
|
setTimeout(() => {
|
|
6132
7006
|
if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
|
|
6133
7007
|
if (state !== 'GENERATING' || msg.id !== currentSessionId) return;
|
|
6134
|
-
injectVariantsFromSource(msg.file, msg.id);
|
|
7008
|
+
injectVariantsFromSource(msg.file, msg.id, { generationCompleted: true });
|
|
6135
7009
|
}, 750);
|
|
6136
7010
|
break;
|
|
6137
7011
|
}
|
|
@@ -6153,12 +7027,21 @@
|
|
|
6153
7027
|
break;
|
|
6154
7028
|
case 'complete':
|
|
6155
7029
|
case 'accept':
|
|
7030
|
+
// The real accept result arrived: the awaited failure window closed.
|
|
7031
|
+
if (awaitingAcceptResult?.id && msg.id === awaitingAcceptResult.id) awaitingAcceptResult = null;
|
|
6156
7032
|
if (maybeCompleteAcceptedSession(msg)) break;
|
|
6157
7033
|
break;
|
|
6158
7034
|
case 'agent_done':
|
|
6159
|
-
//
|
|
6160
|
-
//
|
|
6161
|
-
//
|
|
7035
|
+
// The deterministic accept has already committed the reviewed DOM
|
|
7036
|
+
// and fenced generation. Carbonize may continue in the background;
|
|
7037
|
+
// it must not hold the foreground picker hostage.
|
|
7038
|
+
// Only a carbonize agent_done is provably accept-side: accept
|
|
7039
|
+
// unlocks at the first variant, so a late generation agent_done
|
|
7040
|
+
// for the same session id can still arrive after Accept and must
|
|
7041
|
+
// not close the awaited failure window early (the SSE broadcast
|
|
7042
|
+
// carries no sourceEventType to tell the two apart).
|
|
7043
|
+
if (msg.data?.carbonize === true && awaitingAcceptResult?.id && msg.id === awaitingAcceptResult.id) awaitingAcceptResult = null;
|
|
7044
|
+
if (msg.data?.carbonize === true && maybeCompleteAcceptedSession(msg)) break;
|
|
6162
7045
|
break;
|
|
6163
7046
|
case 'discarded':
|
|
6164
7047
|
if (msg.id && msg.id === currentSessionId) {
|
|
@@ -6169,14 +7052,43 @@
|
|
|
6169
7052
|
case 'error':
|
|
6170
7053
|
if (pendingAcceptedSession?.id && msg.id === pendingAcceptedSession.id) {
|
|
6171
7054
|
pendingAcceptedSession = null;
|
|
7055
|
+
awaitingAcceptResult = null;
|
|
6172
7056
|
setLiveState('CYCLING');
|
|
6173
7057
|
updateBarContent('cycling');
|
|
6174
7058
|
showToast('Could not complete accept cleanup. Try Accept again.', 5000);
|
|
6175
7059
|
break;
|
|
6176
7060
|
}
|
|
7061
|
+
// The optimistic teardown already released the session, so the
|
|
7062
|
+
// CYCLING recovery above can no longer match; without this branch
|
|
7063
|
+
// the failure fell through to the generic toast and the user had
|
|
7064
|
+
// no hint their variant was never written (issue #384).
|
|
7065
|
+
if (awaitingAcceptResult?.id && msg.id === awaitingAcceptResult.id) {
|
|
7066
|
+
awaitingAcceptResult = null;
|
|
7067
|
+
console.error('[impeccable] Accept failed after teardown:', msg.message);
|
|
7068
|
+
// Hedged on purpose: a carbonize-phase failure raises this same
|
|
7069
|
+
// error after the source WAS promoted, so "was not saved" would
|
|
7070
|
+
// overclaim. Normalize the server message's terminal punctuation
|
|
7071
|
+
// so the two sentences don't run together.
|
|
7072
|
+
const acceptFailDetail = String(msg.message || 'unknown error').trim().replace(/[.!?]?$/, '.');
|
|
7073
|
+
showToast('Accept failed: ' + acceptFailDetail + ' The variant may not have been saved. If the change is missing, pick the element and generate again.', 8000);
|
|
7074
|
+
break;
|
|
7075
|
+
}
|
|
6177
7076
|
if (maybeCompleteSteer(msg)) break;
|
|
6178
7077
|
console.error('[impeccable] Error:', msg.message);
|
|
6179
7078
|
showToast('Error: ' + msg.message, 5000);
|
|
7079
|
+
// An agent error reply is terminal for the session it names: tear
|
|
7080
|
+
// it down exactly like 'discarded' (cleanup includes clearSession),
|
|
7081
|
+
// or the durable localStorage checkpoint survives and every reload
|
|
7082
|
+
// resurrects a GENERATING bar for a session the server no longer
|
|
7083
|
+
// knows about (issue #362).
|
|
7084
|
+
if (msg.id && msg.id === currentSessionId) {
|
|
7085
|
+
markSessionHandled();
|
|
7086
|
+
cleanup();
|
|
7087
|
+
break;
|
|
7088
|
+
}
|
|
7089
|
+
// A stored-but-not-current checkpoint naming the errored session
|
|
7090
|
+
// (the error raced a reload) must not resurrect either.
|
|
7091
|
+
if (msg.id && loadSession()?.id === msg.id) clearSession();
|
|
6180
7092
|
hideBar();
|
|
6181
7093
|
renderEditBadge('hidden');
|
|
6182
7094
|
setLiveState('PICKING');
|
|
@@ -6202,7 +7114,7 @@
|
|
|
6202
7114
|
function handleServerLost() {
|
|
6203
7115
|
const recoveryState = currentSessionId ? state : 'IDLE';
|
|
6204
7116
|
if (state === 'GENERATING' || state === 'CYCLING' || state === 'SAVING') {
|
|
6205
|
-
showToast('Live server
|
|
7117
|
+
showToast('Live server connection lost. Your session is saved; reopen this page or restart live-poll.mjs to continue.', 6000);
|
|
6206
7118
|
}
|
|
6207
7119
|
hideBar();
|
|
6208
7120
|
hideHighlight();
|
|
@@ -6221,6 +7133,13 @@
|
|
|
6221
7133
|
if (currentSessionId) saveSession();
|
|
6222
7134
|
}
|
|
6223
7135
|
|
|
7136
|
+
// Progress events must never overtake the event that CREATES their session:
|
|
7137
|
+
// the Go-time checkpoint and the generate POST are concurrent fetches, and
|
|
7138
|
+
// when the checkpoint lands first the server rightly refuses it as
|
|
7139
|
+
// unknown_session — which must mean "foreign leftovers", not "you raced
|
|
7140
|
+
// your own Go click". The gate serializes creation before progress.
|
|
7141
|
+
let sessionCreationGate = Promise.resolve();
|
|
7142
|
+
|
|
6224
7143
|
function sendEvent(msg, opts) {
|
|
6225
7144
|
msg.token = TOKEN;
|
|
6226
7145
|
function handleFailure(err) {
|
|
@@ -6231,15 +7150,42 @@
|
|
|
6231
7150
|
console.debug('[impeccable] Dropped optional live event:', err);
|
|
6232
7151
|
return null;
|
|
6233
7152
|
}
|
|
6234
|
-
|
|
7153
|
+
const doSend = () => fetch('http://localhost:' + PORT + '/events', {
|
|
6235
7154
|
method: 'POST',
|
|
6236
7155
|
headers: { 'Content-Type': 'application/json' },
|
|
6237
7156
|
body: JSON.stringify(msg),
|
|
6238
7157
|
}).then(async res => {
|
|
6239
7158
|
if (res.ok) return res;
|
|
6240
7159
|
const body = await res.json().catch(() => ({}));
|
|
7160
|
+
// The server refused to journal progress for a session it has never
|
|
7161
|
+
// seen: this browser is carrying state from another project or a
|
|
7162
|
+
// wiped store (two apps sharing a localhost port). Continuing to
|
|
7163
|
+
// report it would freeze the picker behind a session that can never
|
|
7164
|
+
// complete, so drop the local state and hand the surface back.
|
|
7165
|
+
if (body.error === 'unknown_session' && msg.type === 'checkpoint'
|
|
7166
|
+
&& msg.id && msg.id === currentSessionId) {
|
|
7167
|
+
abandonForeignSession(msg.id);
|
|
7168
|
+
return null;
|
|
7169
|
+
}
|
|
6241
7170
|
return handleFailure(new Error(body.error || ('HTTP ' + res.status + ' ' + res.statusText)));
|
|
6242
7171
|
}).catch(handleFailure);
|
|
7172
|
+
|
|
7173
|
+
if (msg.type === 'generate' || msg.type === 'steer') {
|
|
7174
|
+
const creation = doSend();
|
|
7175
|
+
sessionCreationGate = creation.then(() => {}, () => {});
|
|
7176
|
+
return creation;
|
|
7177
|
+
}
|
|
7178
|
+
return sessionCreationGate.then(doSend);
|
|
7179
|
+
}
|
|
7180
|
+
|
|
7181
|
+
let abandonedForeignSessionId = null;
|
|
7182
|
+
function abandonForeignSession(sessionId) {
|
|
7183
|
+
if (abandonedForeignSessionId === sessionId || sessionId !== currentSessionId) return;
|
|
7184
|
+
abandonedForeignSessionId = sessionId;
|
|
7185
|
+
console.warn('[impeccable] The live server has no record of session ' + sessionId + '; clearing stale local state.');
|
|
7186
|
+
markSessionHandled();
|
|
7187
|
+
cleanup({ instantChrome: true });
|
|
7188
|
+
showToast('A saved live session belonged to a different project, so it was cleared. Pick an element to start fresh.', 6000);
|
|
6243
7189
|
}
|
|
6244
7190
|
|
|
6245
7191
|
function checkpointPayload(reason) {
|
|
@@ -6247,6 +7193,7 @@
|
|
|
6247
7193
|
type: 'checkpoint',
|
|
6248
7194
|
id: currentSessionId,
|
|
6249
7195
|
revision: sessionState.nextCheckpointRevision(),
|
|
7196
|
+
revisionDomain: 'browser',
|
|
6250
7197
|
owner: browserOwner,
|
|
6251
7198
|
phase: String(state || '').toLowerCase(),
|
|
6252
7199
|
reason,
|
|
@@ -6272,6 +7219,7 @@
|
|
|
6272
7219
|
type: 'checkpoint',
|
|
6273
7220
|
id,
|
|
6274
7221
|
revision: sessionState.nextCheckpointRevision(),
|
|
7222
|
+
revisionDomain: 'browser',
|
|
6275
7223
|
owner: browserOwner,
|
|
6276
7224
|
phase: 'steer',
|
|
6277
7225
|
reason,
|
|
@@ -6620,11 +7568,21 @@
|
|
|
6620
7568
|
disableInlineEdit();
|
|
6621
7569
|
stripManualEditRuntimeState(selectedElement);
|
|
6622
7570
|
|
|
7571
|
+
// A new cycle publishes new modules, so the previous cycle's mount failure
|
|
7572
|
+
// is about files that no longer matter.
|
|
7573
|
+
clearMountErrorCard();
|
|
7574
|
+
lastReportedMountFailure = null;
|
|
6623
7575
|
pendingAcceptedSession = null;
|
|
7576
|
+
// A new session supersedes any accept still awaiting its result; a late
|
|
7577
|
+
// failure toast for the previous session would only mislead here.
|
|
7578
|
+
awaitingAcceptResult = null;
|
|
6624
7579
|
currentSessionId = id8();
|
|
6625
7580
|
expectedVariants = selectedCount;
|
|
6626
7581
|
arrivedVariants = 0;
|
|
6627
7582
|
visibleVariant = 0;
|
|
7583
|
+
generationPhase = 'queued';
|
|
7584
|
+
parameterGenerationState = 'pending';
|
|
7585
|
+
parameterReadyAnnouncedSession = null;
|
|
6628
7586
|
resetSessionFileMeta();
|
|
6629
7587
|
|
|
6630
7588
|
// Flip to GENERATING immediately so the bar morphs without waiting on
|
|
@@ -6634,6 +7592,7 @@
|
|
|
6634
7592
|
const elForCapture = selectedElement;
|
|
6635
7593
|
pickedAnchorSnapshot = buildPickedAnchorSnapshot(elForCapture);
|
|
6636
7594
|
const captureRect = elForCapture.getBoundingClientRect();
|
|
7595
|
+
pickedAnchorViewportTop = captureRect.top;
|
|
6637
7596
|
const snapshot = {
|
|
6638
7597
|
comments: annotState.comments.map(c => ({ x: c.x, y: c.y, text: c.text })),
|
|
6639
7598
|
strokes: annotState.strokes.map(s => ({ points: s.points.map(p => [p[0], p[1]]) })),
|
|
@@ -6665,7 +7624,7 @@
|
|
|
6665
7624
|
writeScrollY(window.scrollY);
|
|
6666
7625
|
if (variantObserver) variantObserver.disconnect();
|
|
6667
7626
|
variantObserver = startVariantObserver(currentSessionId);
|
|
6668
|
-
startScrollLock(currentSessionId);
|
|
7627
|
+
startScrollLock(currentSessionId, window.scrollY, pickedAnchorViewportTop);
|
|
6669
7628
|
|
|
6670
7629
|
captureAndEmit(elForCapture, basePayload, snapshot, captureRect);
|
|
6671
7630
|
}
|
|
@@ -6695,17 +7654,28 @@
|
|
|
6695
7654
|
if (!canCreateInsert({ prompt, comments: snapshot.comments, strokes: snapshot.strokes })) return;
|
|
6696
7655
|
|
|
6697
7656
|
stopVoice({ suppressSubmit: true });
|
|
7657
|
+
// A new cycle publishes new modules, so the previous cycle's mount failure
|
|
7658
|
+
// is about files that no longer matter.
|
|
7659
|
+
clearMountErrorCard();
|
|
7660
|
+
lastReportedMountFailure = null;
|
|
6698
7661
|
pendingAcceptedSession = null;
|
|
7662
|
+
// A new session supersedes any accept still awaiting its result; a late
|
|
7663
|
+
// failure toast for the previous session would only mislead here.
|
|
7664
|
+
awaitingAcceptResult = null;
|
|
6699
7665
|
currentSessionId = id8();
|
|
6700
7666
|
expectedVariants = selectedCount;
|
|
6701
7667
|
arrivedVariants = 0;
|
|
6702
7668
|
visibleVariant = 0;
|
|
7669
|
+
generationPhase = 'queued';
|
|
7670
|
+
parameterGenerationState = 'pending';
|
|
7671
|
+
parameterReadyAnnouncedSession = null;
|
|
6703
7672
|
resetSessionFileMeta();
|
|
6704
7673
|
selectedElement = placeholderElement;
|
|
6705
7674
|
insertPlaceholderSnapshot = buildInsertPlaceholderSnapshotFromDom(insertAnchorElement, placeholderElement);
|
|
6706
7675
|
|
|
6707
7676
|
const elForCapture = placeholderElement;
|
|
6708
7677
|
const captureRect = elForCapture.getBoundingClientRect();
|
|
7678
|
+
pickedAnchorViewportTop = captureRect.top;
|
|
6709
7679
|
const basePayload = {
|
|
6710
7680
|
type: 'generate',
|
|
6711
7681
|
mode: 'insert',
|
|
@@ -6736,7 +7706,7 @@
|
|
|
6736
7706
|
writeScrollY(window.scrollY);
|
|
6737
7707
|
if (variantObserver) variantObserver.disconnect();
|
|
6738
7708
|
variantObserver = startVariantObserver(currentSessionId);
|
|
6739
|
-
startScrollLock(currentSessionId);
|
|
7709
|
+
startScrollLock(currentSessionId, window.scrollY, pickedAnchorViewportTop);
|
|
6740
7710
|
captureAndEmit(elForCapture, basePayload, snapshot, captureRect);
|
|
6741
7711
|
}
|
|
6742
7712
|
|
|
@@ -6916,9 +7886,9 @@
|
|
|
6916
7886
|
// preview mounts are covered by the same shader regression checks.
|
|
6917
7887
|
const adapter = String(window.__IMPECCABLE_LIVE_ADAPTER__ || '').toLowerCase();
|
|
6918
7888
|
if (adapter === 'svelte' || adapter === 'sveltekit') return true;
|
|
6919
|
-
if (currentPreviewMode
|
|
7889
|
+
if (isFrameworkComponentPreviewMode(currentPreviewMode) || svelteComponentSession) return true;
|
|
6920
7890
|
const wrapper = el?.closest?.('[data-impeccable-variants]');
|
|
6921
|
-
return wrapper?.dataset?.impeccablePreview
|
|
7891
|
+
return isFrameworkComponentPreviewMode(wrapper?.dataset?.impeccablePreview);
|
|
6922
7892
|
}
|
|
6923
7893
|
|
|
6924
7894
|
function paintsShaderProxySurface(node) {
|
|
@@ -7046,6 +8016,17 @@
|
|
|
7046
8016
|
}
|
|
7047
8017
|
|
|
7048
8018
|
async function captureAndEmit(el, basePayload, snapshot, rect) {
|
|
8019
|
+
const hasAnnotations = snapshot && (snapshot.comments.length > 0 || snapshot.strokes.length > 0);
|
|
8020
|
+
|
|
8021
|
+
// Plain requests do not send a screenshot to the agent, so capture is
|
|
8022
|
+
// presentation-only. Wait only for the helper to accept the event before
|
|
8023
|
+
// starting CPU-heavy capture; this yields the browser task and prevents
|
|
8024
|
+
// rasterization from delaying the fetch itself.
|
|
8025
|
+
if (!hasAnnotations) {
|
|
8026
|
+
basePayload.clientSentAt = Date.now();
|
|
8027
|
+
await sendEvent(basePayload);
|
|
8028
|
+
}
|
|
8029
|
+
|
|
7049
8030
|
let screenshotPath;
|
|
7050
8031
|
let blob;
|
|
7051
8032
|
let paper;
|
|
@@ -7063,7 +8044,6 @@
|
|
|
7063
8044
|
// are present. Without annotations the image is pure visual anchoring -
|
|
7064
8045
|
// it biases the model toward the current rendering and works against the
|
|
7065
8046
|
// three-distinct-directions brief.
|
|
7066
|
-
const hasAnnotations = snapshot && (snapshot.comments.length > 0 || snapshot.strokes.length > 0);
|
|
7067
8047
|
if (blob && hasAnnotations) {
|
|
7068
8048
|
try {
|
|
7069
8049
|
const uploadRes = await fetch(
|
|
@@ -7081,7 +8061,12 @@
|
|
|
7081
8061
|
console.warn('[impeccable] annotation upload failed:', err);
|
|
7082
8062
|
}
|
|
7083
8063
|
}
|
|
7084
|
-
|
|
8064
|
+
// Annotated requests must wait for capture + upload because the screenshot
|
|
8065
|
+
// is semantic input. Plain requests were already dispatched above.
|
|
8066
|
+
if (hasAnnotations) {
|
|
8067
|
+
basePayload.clientSentAt = Date.now();
|
|
8068
|
+
sendEvent(screenshotPath ? { ...basePayload, screenshotPath } : basePayload);
|
|
8069
|
+
}
|
|
7085
8070
|
}
|
|
7086
8071
|
|
|
7087
8072
|
//
|
|
@@ -7461,7 +8446,6 @@ void main() {
|
|
|
7461
8446
|
if (variantSelectionPromise) {
|
|
7462
8447
|
try { await variantSelectionPromise; } catch { /* failed selection falls back below */ }
|
|
7463
8448
|
}
|
|
7464
|
-
if (!currentSessionId || arrivedVariants === 0) return;
|
|
7465
8449
|
const domVisibleVariant = readVisibleVariantFromDOM(currentSessionId);
|
|
7466
8450
|
if (domVisibleVariant > 0) visibleVariant = domVisibleVariant;
|
|
7467
8451
|
const acceptPayload = {
|
|
@@ -7469,7 +8453,9 @@ void main() {
|
|
|
7469
8453
|
id: currentSessionId,
|
|
7470
8454
|
variantId: String(visibleVariant),
|
|
7471
8455
|
pageUrl: location.pathname,
|
|
8456
|
+
clientSentAt: Date.now(),
|
|
7472
8457
|
};
|
|
8458
|
+
if (!currentSessionId || arrivedVariants === 0) return;
|
|
7473
8459
|
const acceptWrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
|
|
7474
8460
|
if (Object.keys(paramsCurrentValues).length > 0) {
|
|
7475
8461
|
acceptPayload.paramValues = { ...paramsCurrentValues };
|
|
@@ -7483,7 +8469,7 @@ void main() {
|
|
|
7483
8469
|
const acceptedSessionId = currentSessionId;
|
|
7484
8470
|
const acceptedVariant = visibleVariant;
|
|
7485
8471
|
const acceptedIsSvelteComponent = svelteComponentSession?.sessionId === acceptedSessionId
|
|
7486
|
-
|| acceptWrapper?.dataset?.impeccablePreview
|
|
8472
|
+
|| isFrameworkComponentPreviewMode(acceptWrapper?.dataset?.impeccablePreview);
|
|
7487
8473
|
const acceptedSnapshot = snapshotAcceptedVariantDom(acceptedSessionId, acceptedVariant);
|
|
7488
8474
|
|
|
7489
8475
|
setLiveState('SAVING');
|
|
@@ -7498,7 +8484,18 @@ void main() {
|
|
|
7498
8484
|
saveSession();
|
|
7499
8485
|
|
|
7500
8486
|
sendEvent(acceptPayload, { throwOnError: true })
|
|
7501
|
-
.then(() => {
|
|
8487
|
+
.then(() => {
|
|
8488
|
+
const pending = pendingAcceptedSession;
|
|
8489
|
+
if (!pending || pending.id !== acceptedSessionId) return;
|
|
8490
|
+
// POST /events returns only after the accept intent is durable and the
|
|
8491
|
+
// generation epoch is fenced. Source promotion/carbonize can finish in
|
|
8492
|
+
// the background; the foreground picker is free immediately.
|
|
8493
|
+
markSessionHandled();
|
|
8494
|
+
setLiveState('CONFIRMED');
|
|
8495
|
+
document.documentElement.dataset.impeccableAcceptToPickingMs = String(Date.now() - acceptPayload.clientSentAt);
|
|
8496
|
+
awaitingAcceptResult = { id: acceptedSessionId };
|
|
8497
|
+
scheduleAcceptCleanup(pending);
|
|
8498
|
+
})
|
|
7502
8499
|
.catch(() => {
|
|
7503
8500
|
if (pendingAcceptedSession?.id === acceptedSessionId) pendingAcceptedSession = null;
|
|
7504
8501
|
setLiveState('CYCLING');
|
|
@@ -7527,17 +8524,26 @@ void main() {
|
|
|
7527
8524
|
}
|
|
7528
8525
|
|
|
7529
8526
|
function scheduleAcceptCleanup(accepted) {
|
|
7530
|
-
|
|
7531
|
-
if (
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
8527
|
+
queueMicrotask(function() {
|
|
8528
|
+
if (pendingAcceptedSession?.id !== accepted?.id) return;
|
|
8529
|
+
// Svelte previews live in an adapter-owned mount rather than in source
|
|
8530
|
+
// wrapper markup. Promote the mounted variant before releasing the
|
|
8531
|
+
// session so the old adapter instance cannot linger behind the next
|
|
8532
|
+
// Pick → Go loop while carbonize finishes in the background.
|
|
8533
|
+
if (accepted?.isSvelteComponent) {
|
|
8534
|
+
commitAcceptedSvelteComponentToDom(accepted.id);
|
|
7538
8535
|
}
|
|
7539
8536
|
cleanupAcceptedSession();
|
|
7540
|
-
}
|
|
8537
|
+
});
|
|
8538
|
+
// Let React/Vue/Svelte own the HMR reconciliation. Mutating their DOM in
|
|
8539
|
+
// the same turn as the source update causes removeChild/NotFoundError
|
|
8540
|
+
// races. Static servers still need a fallback, but it must not keep Live
|
|
8541
|
+
// in SAVING or block the user's next pick.
|
|
8542
|
+
if (!accepted?.isSvelteComponent) {
|
|
8543
|
+
setTimeout(function() {
|
|
8544
|
+
if (!acceptedDomAlreadyClean(accepted)) ensureAcceptedDomClean(accepted);
|
|
8545
|
+
}, 1200);
|
|
8546
|
+
}
|
|
7541
8547
|
}
|
|
7542
8548
|
|
|
7543
8549
|
function snapshotAcceptedVariantDom(sessionId, variantId) {
|
|
@@ -7636,11 +8642,16 @@ void main() {
|
|
|
7636
8642
|
stopScrollTracking();
|
|
7637
8643
|
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
|
|
7638
8644
|
stopScrollLock();
|
|
8645
|
+
removeVariantStateStylesheet();
|
|
7639
8646
|
clearScrollY();
|
|
7640
8647
|
clearSession();
|
|
7641
8648
|
resetSessionFileMeta();
|
|
7642
8649
|
selectedElement = null;
|
|
8650
|
+
hoveredElement = null;
|
|
8651
|
+
pagePickSkipClick = false;
|
|
7643
8652
|
currentSessionId = null;
|
|
8653
|
+
parameterGenerationState = 'idle';
|
|
8654
|
+
parameterReadyAnnouncedSession = null;
|
|
7644
8655
|
selectedAction = 'impeccable';
|
|
7645
8656
|
pendingAcceptedSession = null;
|
|
7646
8657
|
renderEditBadge('hidden');
|
|
@@ -7675,7 +8686,7 @@ void main() {
|
|
|
7675
8686
|
sendEvent({ type: 'discard', id: currentSessionId }, { throwOnError: true })
|
|
7676
8687
|
.then(() => {
|
|
7677
8688
|
markSessionHandled();
|
|
7678
|
-
cleanup();
|
|
8689
|
+
cleanup({ restoreOriginal: true, instantChrome: true });
|
|
7679
8690
|
})
|
|
7680
8691
|
.catch(() => showToast('Could not confirm discard with the live server. Session kept for recovery.', 5000));
|
|
7681
8692
|
}
|
|
@@ -7697,6 +8708,7 @@ void main() {
|
|
|
7697
8708
|
currentPreviewMode = null;
|
|
7698
8709
|
recoveryWaitingForAnchor = false;
|
|
7699
8710
|
pickedAnchorSnapshot = null;
|
|
8711
|
+
pickedAnchorViewportTop = null;
|
|
7700
8712
|
}
|
|
7701
8713
|
|
|
7702
8714
|
function rememberSessionFileMeta(meta = {}) {
|
|
@@ -7705,8 +8717,8 @@ void main() {
|
|
|
7705
8717
|
const previewFile = normalizeSessionPath(meta.previewFile);
|
|
7706
8718
|
const previewMode = meta.previewMode || (isSvelteComponentManifestPath(previewFile || file) ? 'svelte-component' : null);
|
|
7707
8719
|
|
|
7708
|
-
if (previewMode
|
|
7709
|
-
currentPreviewMode = 'svelte-component';
|
|
8720
|
+
if (isFrameworkComponentPreviewMode(previewMode) || isSvelteComponentManifestPath(file)) {
|
|
8721
|
+
currentPreviewMode = isFrameworkComponentPreviewMode(previewMode) ? previewMode : 'svelte-component';
|
|
7710
8722
|
currentPreviewFile = previewFile || (isSvelteComponentManifestPath(file) ? file : currentPreviewFile);
|
|
7711
8723
|
currentSourceFile = sourceFile || currentSourceFile;
|
|
7712
8724
|
return;
|
|
@@ -7722,12 +8734,15 @@ void main() {
|
|
|
7722
8734
|
rememberSessionFileMeta(saved);
|
|
7723
8735
|
if (saved.insertPlaceholder) insertPlaceholderSnapshot = saved.insertPlaceholder;
|
|
7724
8736
|
if (saved.pickedAnchor) pickedAnchorSnapshot = saved.pickedAnchor;
|
|
8737
|
+
if (Number.isFinite(saved.pickedAnchorViewportTop)) pickedAnchorViewportTop = saved.pickedAnchorViewportTop;
|
|
7725
8738
|
if (saved.action) selectedAction = saved.action;
|
|
7726
8739
|
if (saved.count) selectedCount = saved.count;
|
|
7727
8740
|
if (saved.previewMode) currentPreviewMode = saved.previewMode;
|
|
7728
8741
|
if (saved.paramValues && typeof saved.paramValues === 'object') {
|
|
7729
8742
|
paramsCurrentValues = { ...saved.paramValues };
|
|
7730
8743
|
}
|
|
8744
|
+
if (saved.parameterState) parameterGenerationState = saved.parameterState;
|
|
8745
|
+
if (saved.generationPhase) generationPhase = saved.generationPhase;
|
|
7731
8746
|
}
|
|
7732
8747
|
|
|
7733
8748
|
function normalizePagePath(value) {
|
|
@@ -7765,8 +8780,62 @@ void main() {
|
|
|
7765
8780
|
return Math.floor(num);
|
|
7766
8781
|
}
|
|
7767
8782
|
|
|
8783
|
+
/**
|
|
8784
|
+
* A durable server session this page can adopt when the browser has no local
|
|
8785
|
+
* record of it. Requires an explicit pageUrl match: a summary with no page is
|
|
8786
|
+
* not evidence that it belongs to THIS page, and adopting it would hijack an
|
|
8787
|
+
* unrelated route.
|
|
8788
|
+
*/
|
|
8789
|
+
// Phases in which the user is (or should be) comparing variants. Only these
|
|
8790
|
+
// are adoptable by a browser with no local record. Steer and manual-edit
|
|
8791
|
+
// sessions have no wrapper to restore, and accept/carbonize phases are
|
|
8792
|
+
// agent-side work: a reload mid-carbonize must not resurrect the bar over a
|
|
8793
|
+
// page whose comparison is already decided (a slow-CI reload hit exactly
|
|
8794
|
+
// that window and left the bar stranded after accept).
|
|
8795
|
+
const ADOPTABLE_SESSION_PHASES = new Set([
|
|
8796
|
+
'generate_requested', 'variants_ready', 'generating', 'cycling',
|
|
8797
|
+
]);
|
|
8798
|
+
|
|
8799
|
+
function findAdoptableServerSession(activeSessions) {
|
|
8800
|
+
if (!Array.isArray(activeSessions)) return null;
|
|
8801
|
+
return activeSessions.find((session) => (
|
|
8802
|
+
session?.id
|
|
8803
|
+
&& !isTerminalSessionSummary(session)
|
|
8804
|
+
&& !isSessionHandled(session.id)
|
|
8805
|
+
&& session.pageUrl
|
|
8806
|
+
&& pageMatchesCurrent(session.pageUrl)
|
|
8807
|
+
&& (session.previewFile || session.sourceFile)
|
|
8808
|
+
&& Number(session.expectedVariants) > 0
|
|
8809
|
+
&& ADOPTABLE_SESSION_PHASES.has(String(session.phase || ''))
|
|
8810
|
+
)) || null;
|
|
8811
|
+
}
|
|
8812
|
+
|
|
8813
|
+
// Shape a server summary like a saved local session so one restore path
|
|
8814
|
+
// serves both. The server has no browser state machine, so an adopted session
|
|
8815
|
+
// always re-enters GENERATING and lets the injection settle the final state.
|
|
8816
|
+
function serverSessionAsSavedShape(session) {
|
|
8817
|
+
return {
|
|
8818
|
+
id: session.id,
|
|
8819
|
+
state: 'GENERATING',
|
|
8820
|
+
expected: Number(session.expectedVariants) || 0,
|
|
8821
|
+
arrived: Number(session.arrivedVariants) || 0,
|
|
8822
|
+
visible: Number(session.visibleVariant) || 0,
|
|
8823
|
+
sourceFile: session.sourceFile || undefined,
|
|
8824
|
+
previewFile: session.previewFile || undefined,
|
|
8825
|
+
previewMode: session.previewMode || undefined,
|
|
8826
|
+
pageUrl: session.pageUrl || undefined,
|
|
8827
|
+
paramValues: session.paramValues && typeof session.paramValues === 'object' ? session.paramValues : {},
|
|
8828
|
+
};
|
|
8829
|
+
}
|
|
8830
|
+
|
|
7768
8831
|
function restoreSessionWithoutWrapper(reason, activeSessions) {
|
|
7769
|
-
const
|
|
8832
|
+
const cached = loadSession();
|
|
8833
|
+
// localStorage is a cache, not a gate. A cleared tab, a second browser
|
|
8834
|
+
// profile, or a teardown that dropped local state all leave the durable
|
|
8835
|
+
// server session as the only record of work in progress; adopt it instead
|
|
8836
|
+
// of stranding a session the server still considers live.
|
|
8837
|
+
const adopted = cached?.id ? null : findAdoptableServerSession(activeSessions);
|
|
8838
|
+
const saved = cached?.id ? cached : (adopted ? serverSessionAsSavedShape(adopted) : null);
|
|
7770
8839
|
if (!saved?.id || isSessionHandled(saved.id)) return false;
|
|
7771
8840
|
const savedState = String(saved.state || '').toUpperCase();
|
|
7772
8841
|
if (savedState !== 'GENERATING' && savedState !== 'CYCLING') return false;
|
|
@@ -7799,11 +8868,18 @@ void main() {
|
|
|
7799
8868
|
saveSession();
|
|
7800
8869
|
queueCheckpoint(reason || 'browser_restore_without_wrapper');
|
|
7801
8870
|
|
|
7802
|
-
const restoreFile = currentPreviewMode
|
|
8871
|
+
const restoreFile = isFrameworkComponentPreviewMode(currentPreviewMode)
|
|
7803
8872
|
? currentPreviewFile
|
|
7804
8873
|
: (currentSourceFile || currentPreviewFile);
|
|
7805
8874
|
if (restoreFile) {
|
|
7806
|
-
|
|
8875
|
+
// A restored CYCLING session promises variants already written into
|
|
8876
|
+
// source; if they are not there (after retries), the session is an
|
|
8877
|
+
// orphan and must self-discard instead of freezing the picker (#439).
|
|
8878
|
+
// GENERATING restores make no such promise: deferred-wrapper flows
|
|
8879
|
+
// legitimately have no wrapper in source until the agent's write lands.
|
|
8880
|
+
injectVariantsFromSource(restoreFile, currentSessionId, {
|
|
8881
|
+
orphanDiscard: savedState === 'CYCLING' && !isFrameworkComponentPreviewMode(currentPreviewMode),
|
|
8882
|
+
});
|
|
7807
8883
|
return true;
|
|
7808
8884
|
}
|
|
7809
8885
|
|
|
@@ -7812,17 +8888,48 @@ void main() {
|
|
|
7812
8888
|
|
|
7813
8889
|
function restoreFromActiveSessions(activeSessions, reason) {
|
|
7814
8890
|
const wrapper = document.querySelector('[data-impeccable-variants]');
|
|
7815
|
-
if (wrapper && wrapper.dataset.impeccablePreview
|
|
8891
|
+
if (wrapper && !isFrameworkComponentPreviewMode(wrapper.dataset.impeccablePreview)) return false;
|
|
7816
8892
|
if (svelteComponentSession?.sessionId === currentSessionId) return false;
|
|
7817
8893
|
return restoreSessionWithoutWrapper(reason || 'sse_connected', activeSessions);
|
|
7818
8894
|
}
|
|
7819
8895
|
|
|
8896
|
+
// Self-heal on SSE (re)connect. The preflight scaffold write triggers a
|
|
8897
|
+
// framework full-reload (Astro reloads pages for any .astro edit); if the
|
|
8898
|
+
// agent's variant write + `done` broadcast land while this page is
|
|
8899
|
+
// mid-reload, both the done SSE and the second HMR reload are missed and
|
|
8900
|
+
// the resumed page would wait in GENERATING at 0/N forever. The server's
|
|
8901
|
+
// session summary carries the durable generationCompletedAt marker, so on
|
|
8902
|
+
// every connect compare it against our own progress and pull the finished
|
|
8903
|
+
// variants from source when behind. Mirrors the `done` handler's source
|
|
8904
|
+
// fallback, including its give-HMR-the-first-chance settle delay.
|
|
8905
|
+
function recoverMissedGenerationCompletion(activeSessions) {
|
|
8906
|
+
if (!currentSessionId || state !== 'GENERATING') return;
|
|
8907
|
+
if (!Array.isArray(activeSessions)) return;
|
|
8908
|
+
const summary = activeSessions.find((session) => session?.id === currentSessionId);
|
|
8909
|
+
if (!summary?.generationCompletedAt || summary.generationCanceled) return;
|
|
8910
|
+
if (isTerminalSessionSummary(summary)) return;
|
|
8911
|
+
if (arrivedVariants > 0 && arrivedVariants >= expectedVariants) return;
|
|
8912
|
+
rememberSessionFileMeta(summary);
|
|
8913
|
+
const sessionId = currentSessionId;
|
|
8914
|
+
const file = isFrameworkComponentPreviewMode(currentPreviewMode)
|
|
8915
|
+
? currentPreviewFile
|
|
8916
|
+
: (summary.sourceFile || summary.previewFile || currentSourceFile || currentPreviewFile);
|
|
8917
|
+
if (!file) return;
|
|
8918
|
+
console.log('[impeccable] Reconnected after generation completed; recovering variants from source.');
|
|
8919
|
+
setTimeout(() => {
|
|
8920
|
+
if (sessionId !== currentSessionId || state !== 'GENERATING') return;
|
|
8921
|
+
if (arrivedVariants > 0 && arrivedVariants >= expectedVariants) return;
|
|
8922
|
+
injectVariantsFromSource(file, sessionId, { generationCompleted: true });
|
|
8923
|
+
}, 750);
|
|
8924
|
+
}
|
|
8925
|
+
|
|
7820
8926
|
function saveSession() {
|
|
7821
8927
|
if (!currentSessionId) return;
|
|
7822
8928
|
// NOTE: scrollY is stored under a separate key (writeScrollY). Storing
|
|
7823
8929
|
// it here would overwrite the Go-time value every time state changes.
|
|
7824
8930
|
sessionState.saveSession({
|
|
7825
8931
|
id: currentSessionId,
|
|
8932
|
+
appRoot: APP_ROOT || undefined,
|
|
7826
8933
|
state,
|
|
7827
8934
|
action: selectedAction,
|
|
7828
8935
|
count: selectedCount,
|
|
@@ -7834,13 +8941,27 @@ void main() {
|
|
|
7834
8941
|
previewMode: currentPreviewMode || undefined,
|
|
7835
8942
|
pageUrl: location.pathname,
|
|
7836
8943
|
paramValues: { ...paramsCurrentValues },
|
|
8944
|
+
parameterState: parameterGenerationState,
|
|
7837
8945
|
insertPlaceholder: insertPlaceholderSnapshot || undefined,
|
|
7838
8946
|
pickedAnchor: pickedAnchorSnapshot || undefined,
|
|
8947
|
+
pickedAnchorViewportTop: Number.isFinite(pickedAnchorViewportTop) ? pickedAnchorViewportTop : undefined,
|
|
8948
|
+
pageHash: location.hash || undefined,
|
|
8949
|
+
pageSearch: location.search || undefined,
|
|
7839
8950
|
});
|
|
7840
8951
|
}
|
|
7841
8952
|
|
|
7842
8953
|
function loadSession() {
|
|
7843
|
-
|
|
8954
|
+
const saved = sessionState.loadSession();
|
|
8955
|
+
// localStorage is per-origin, and two projects routinely reuse the same
|
|
8956
|
+
// localhost port. A saved session stamped with another project's appRoot
|
|
8957
|
+
// is that project's leftover, never a session this server can complete;
|
|
8958
|
+
// resuming it freezes the picker behind an unfinishable banner.
|
|
8959
|
+
if (saved?.appRoot && APP_ROOT && saved.appRoot !== APP_ROOT) {
|
|
8960
|
+
console.warn('[impeccable] Ignoring saved live session from another project (' + saved.appRoot + ').');
|
|
8961
|
+
sessionState.clearSession();
|
|
8962
|
+
return null;
|
|
8963
|
+
}
|
|
8964
|
+
return saved;
|
|
7844
8965
|
}
|
|
7845
8966
|
|
|
7846
8967
|
function clearSession() {
|
|
@@ -7863,20 +8984,28 @@ void main() {
|
|
|
7863
8984
|
sessionState.clearHandled();
|
|
7864
8985
|
}
|
|
7865
8986
|
|
|
7866
|
-
function cleanup() {
|
|
8987
|
+
function cleanup(options) {
|
|
8988
|
+
const restoreOriginal = options?.restoreOriginal === true;
|
|
8989
|
+
const instantChrome = options?.instantChrome === true;
|
|
7867
8990
|
const cleanupSessionId = currentSessionId;
|
|
8991
|
+
clearMountErrorCard();
|
|
8992
|
+
lastReportedMountFailure = null;
|
|
7868
8993
|
if (svelteComponentSession?.sessionId === cleanupSessionId) {
|
|
7869
8994
|
teardownSvelteComponentSession(true);
|
|
7870
8995
|
} else if (cleanupSessionId) {
|
|
7871
|
-
//
|
|
7872
|
-
//
|
|
8996
|
+
// Switch visibility immediately without structurally mutating the DOM.
|
|
8997
|
+
// HMR from the agent's source rewrite may still be on its way,
|
|
7873
8998
|
// and a manual replaceChild under React causes NotFoundError when the
|
|
7874
8999
|
// reconciler later tries to remove a wrapper we already removed.
|
|
7875
9000
|
// Schedule a 2s fallback that does the manual swap only if HMR hasn't
|
|
7876
9001
|
// replaced the wrapper by then (keeps static-server / no-HMR flows alive).
|
|
7877
9002
|
const wrapper = document.querySelector('[data-impeccable-variants="' + cleanupSessionId + '"]');
|
|
7878
|
-
if (wrapper)
|
|
9003
|
+
if (wrapper) {
|
|
9004
|
+
if (restoreOriginal) showOriginalDuringDiscard(cleanupSessionId);
|
|
9005
|
+
else wrapper.style.display = 'none';
|
|
9006
|
+
}
|
|
7879
9007
|
setTimeout(function() {
|
|
9008
|
+
document.getElementById(DISCARD_STATE_STYLE_ID)?.remove();
|
|
7880
9009
|
if (!cleanupSessionId) return;
|
|
7881
9010
|
const lateWrapper = document.querySelector('[data-impeccable-variants="' + cleanupSessionId + '"]');
|
|
7882
9011
|
if (!lateWrapper) return;
|
|
@@ -7891,18 +9020,23 @@ void main() {
|
|
|
7891
9020
|
lateWrapper.remove();
|
|
7892
9021
|
}, 2000);
|
|
7893
9022
|
}
|
|
7894
|
-
hideBar();
|
|
9023
|
+
hideBar(instantChrome);
|
|
7895
9024
|
hideHighlight();
|
|
7896
9025
|
stopScrollTracking();
|
|
7897
9026
|
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
|
|
7898
9027
|
if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
|
|
7899
9028
|
stopScrollLock();
|
|
9029
|
+
removeVariantStateStylesheet();
|
|
7900
9030
|
clearScrollY();
|
|
7901
9031
|
finalizeInsertSession();
|
|
7902
9032
|
clearSession();
|
|
7903
9033
|
resetSessionFileMeta();
|
|
7904
9034
|
selectedElement = null;
|
|
9035
|
+
hoveredElement = null;
|
|
9036
|
+
pagePickSkipClick = false;
|
|
7905
9037
|
currentSessionId = null;
|
|
9038
|
+
parameterGenerationState = 'idle';
|
|
9039
|
+
parameterReadyAnnouncedSession = null;
|
|
7906
9040
|
selectedAction = 'impeccable';
|
|
7907
9041
|
renderEditBadge('hidden');
|
|
7908
9042
|
setLiveState('PICKING');
|
|
@@ -7928,7 +9062,7 @@ void main() {
|
|
|
7928
9062
|
const barTopFromBottom = barRect && barRect.height > 0
|
|
7929
9063
|
? Math.max(16, window.innerHeight - barRect.top + 12)
|
|
7930
9064
|
: 16;
|
|
7931
|
-
|
|
9065
|
+
const currentToast = el('div', {
|
|
7932
9066
|
position: 'fixed', bottom: barTopFromBottom + 'px', left: '50%',
|
|
7933
9067
|
transform: 'translateX(-50%) translateY(8px)',
|
|
7934
9068
|
background: C.ink, color: C.white,
|
|
@@ -7938,19 +9072,24 @@ void main() {
|
|
|
7938
9072
|
transition: 'opacity 0.25s ' + EASE + ', transform 0.25s ' + EASE,
|
|
7939
9073
|
pointerEvents: 'none', maxWidth: '420px', textAlign: 'center',
|
|
7940
9074
|
});
|
|
7941
|
-
toastEl
|
|
7942
|
-
|
|
7943
|
-
|
|
9075
|
+
toastEl = currentToast;
|
|
9076
|
+
currentToast.id = PREFIX + '-toast';
|
|
9077
|
+
currentToast.textContent = message;
|
|
9078
|
+
uiAppend(currentToast);
|
|
7944
9079
|
requestAnimationFrame(() => {
|
|
7945
|
-
toastEl
|
|
7946
|
-
|
|
9080
|
+
if (toastEl !== currentToast) return;
|
|
9081
|
+
currentToast.style.opacity = '1';
|
|
9082
|
+
currentToast.style.transform = 'translateX(-50%) translateY(0)';
|
|
7947
9083
|
});
|
|
7948
9084
|
setTimeout(() => {
|
|
7949
|
-
if (toastEl)
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
7953
|
-
|
|
9085
|
+
if (toastEl !== currentToast) return;
|
|
9086
|
+
currentToast.style.opacity = '0';
|
|
9087
|
+
currentToast.style.transform = 'translateX(-50%) translateY(8px)';
|
|
9088
|
+
setTimeout(() => {
|
|
9089
|
+
if (toastEl !== currentToast) return;
|
|
9090
|
+
currentToast.remove();
|
|
9091
|
+
toastEl = null;
|
|
9092
|
+
}, 250);
|
|
7954
9093
|
}, duration);
|
|
7955
9094
|
}
|
|
7956
9095
|
|
|
@@ -7981,7 +9120,7 @@ void main() {
|
|
|
7981
9120
|
// would strand the bar in CYCLING at 0/0. If there's no live in-memory mount
|
|
7982
9121
|
// for this wrapper, it's an orphan (reload / failed mount): drop it and let
|
|
7983
9122
|
// the live-server's SSE re-inject the manifest if the session is still live.
|
|
7984
|
-
if (wrapper.dataset.impeccablePreview
|
|
9123
|
+
if (isFrameworkComponentPreviewMode(wrapper.dataset.impeccablePreview)
|
|
7985
9124
|
&& svelteComponentSession?.sessionId !== sessionId) {
|
|
7986
9125
|
wrapper.remove();
|
|
7987
9126
|
if (restoreSessionWithoutWrapper('browser_resumed_svelte_orphan_wrapper')) return true;
|
|
@@ -7990,7 +9129,7 @@ void main() {
|
|
|
7990
9129
|
return false;
|
|
7991
9130
|
}
|
|
7992
9131
|
|
|
7993
|
-
if (wrapper.dataset.impeccablePreview
|
|
9132
|
+
if (isFrameworkComponentPreviewMode(wrapper.dataset.impeccablePreview)) {
|
|
7994
9133
|
if (!svelteComponentSession?.mountedVariant) {
|
|
7995
9134
|
return true;
|
|
7996
9135
|
}
|
|
@@ -8038,7 +9177,7 @@ void main() {
|
|
|
8038
9177
|
insertPlaceholderSnapshot = saved.insertPlaceholder;
|
|
8039
9178
|
}
|
|
8040
9179
|
|
|
8041
|
-
const resumedState = arrivedVariants
|
|
9180
|
+
const resumedState = arrivedVariants > 0 ? 'CYCLING' : 'GENERATING';
|
|
8042
9181
|
|
|
8043
9182
|
// Find the visible variant's content element for highlight positioning.
|
|
8044
9183
|
const isInsert = wrapper.dataset.impeccableMode === 'insert';
|
|
@@ -8062,7 +9201,11 @@ void main() {
|
|
|
8062
9201
|
// hid. Now that state is CYCLING, re-fire.
|
|
8063
9202
|
if (state === 'CYCLING') refreshParamsPanel();
|
|
8064
9203
|
saveSession();
|
|
8065
|
-
|
|
9204
|
+
if (arrivedVariants > 0 && arrivedVariants < expectedVariants) {
|
|
9205
|
+
sendCheckpoint('variants_progress');
|
|
9206
|
+
} else {
|
|
9207
|
+
queueCheckpoint('browser_resumed');
|
|
9208
|
+
}
|
|
8066
9209
|
|
|
8067
9210
|
// Start observing for more variants AFTER initial setup
|
|
8068
9211
|
if (variantObserver) variantObserver.disconnect();
|
|
@@ -8070,7 +9213,7 @@ void main() {
|
|
|
8070
9213
|
|
|
8071
9214
|
// Hold the target at its saved viewport top through any subsequent
|
|
8072
9215
|
// HMR patches, variant inserts, or cycle swaps.
|
|
8073
|
-
startScrollLock(currentSessionId, readScrollY());
|
|
9216
|
+
startScrollLock(currentSessionId, readScrollY(), pickedAnchorViewportTop);
|
|
8074
9217
|
|
|
8075
9218
|
// If we reloaded mid-generation (Bun's HTML HMR destroys the shader
|
|
8076
9219
|
// canvas), re-capture the original's content and restart the shader so
|
|
@@ -8105,6 +9248,7 @@ void main() {
|
|
|
8105
9248
|
let globalBarBrandEl = null;
|
|
8106
9249
|
let agentPollTooltipEl = null;
|
|
8107
9250
|
let agentPollingConnected = false;
|
|
9251
|
+
let agentStatusMessage = null;
|
|
8108
9252
|
let agentStatusPollTimer = null;
|
|
8109
9253
|
let steerFocusSuspended = false;
|
|
8110
9254
|
let steerFocusPauseUntil = 0;
|
|
@@ -8188,6 +9332,8 @@ void main() {
|
|
|
8188
9332
|
let pageChatInput = null;
|
|
8189
9333
|
let pageChatHint = null;
|
|
8190
9334
|
let pageChatVoiceBtn = null;
|
|
9335
|
+
let pageChatSendBtn = null;
|
|
9336
|
+
let pageChatQueueHintEl = null;
|
|
8191
9337
|
let pageChatExpanded = false;
|
|
8192
9338
|
let steerLocked = false;
|
|
8193
9339
|
let steerRequestId = null;
|
|
@@ -8202,13 +9348,24 @@ void main() {
|
|
|
8202
9348
|
/** @type {{ mode: 'steer'|'configure', input: HTMLInputElement, submit: () => void, beforeStart?: () => void } | null} */
|
|
8203
9349
|
let voiceCtx = null;
|
|
8204
9350
|
const PAGE_CHAT_COLLAPSED_W = '104px';
|
|
8205
|
-
const
|
|
9351
|
+
const PAGE_CHAT_QUEUED_W = '212px';
|
|
8206
9352
|
const PAGE_CHAT_PLACEHOLDER_COLLAPSED = 'Steer…';
|
|
8207
9353
|
const PAGE_CHAT_PLACEHOLDER_EXPANDED = 'Steer the page…';
|
|
8208
9354
|
const STEER_AWAIT_TIMEOUT_MS = 120000;
|
|
8209
9355
|
const AGENT_STATUS_POLL_MS = 5000;
|
|
8210
9356
|
const AGENT_DISCONNECTED_MARK = 'oklch(62% 0 0 / 0.78)';
|
|
8211
9357
|
const AGENT_DISCONNECTED_TIP = 'Agent disconnected - run live-poll.mjs to connect';
|
|
9358
|
+
// The indicator tracks whether a poll is parked, which is what decides if
|
|
9359
|
+
// steering can reach the agent right now. That goes quiet two ways, and they
|
|
9360
|
+
// need different copy: nobody is polling at all, or the agent took the work
|
|
9361
|
+
// and is busy with it. Under one-shot foreground polling the second case is
|
|
9362
|
+
// every normal generation, and telling the user to start a poll loop then is
|
|
9363
|
+
// wrong advice about a healthy session.
|
|
9364
|
+
const AGENT_BUSY_TIP = 'Agent is working - steering resumes when it finishes';
|
|
9365
|
+
// Same distinction, said where the steer request is waiting. A submitted
|
|
9366
|
+
// steer that lands while a generate holds the poll lease is not stuck, it is
|
|
9367
|
+
// second in line, and the pulsing dots alone read as "nothing is happening".
|
|
9368
|
+
const STEER_QUEUED_HINT = 'Queued behind current generation';
|
|
8212
9369
|
const GLOBAL_BAR_SECTION_GAP = 8;
|
|
8213
9370
|
const GLOBAL_BAR_INNER_GAP = 2;
|
|
8214
9371
|
const GLOBAL_BAR_INNER_PAD_LEFT = 2;
|
|
@@ -8361,10 +9518,85 @@ void main() {
|
|
|
8361
9518
|
}
|
|
8362
9519
|
|
|
8363
9520
|
function syncPageChatVisual() {
|
|
8364
|
-
if (!pageChatInput || steerLocked)
|
|
9521
|
+
if (!pageChatInput || steerLocked) {
|
|
9522
|
+
syncPageChatSendButton();
|
|
9523
|
+
return;
|
|
9524
|
+
}
|
|
8365
9525
|
const hasText = pageChatInput.value.length > 0;
|
|
8366
9526
|
if (hasText && !pageChatExpanded) expandPageChat({ focus: false });
|
|
8367
9527
|
else if (!hasText && pageChatExpanded) collapsePageChat();
|
|
9528
|
+
syncPageChatSendButton();
|
|
9529
|
+
}
|
|
9530
|
+
|
|
9531
|
+
/**
|
|
9532
|
+
* Send is visible once the pill is open for typing and enabled once there is
|
|
9533
|
+
* something to send. It disappears entirely while a steer is in flight, the
|
|
9534
|
+
* same way the mic does: a second submit during the lock has nowhere to go.
|
|
9535
|
+
*/
|
|
9536
|
+
function syncPageChatSendButton() {
|
|
9537
|
+
if (!pageChatSendBtn) return;
|
|
9538
|
+
const P = pageChatPalette();
|
|
9539
|
+
const hasText = !!pageChatInput?.value.trim();
|
|
9540
|
+
const focused = pageChatInput && activeElementDeep() === pageChatInput;
|
|
9541
|
+
const visible = !steerLocked && (pageChatExpanded || hasText || focused);
|
|
9542
|
+
pageChatSendBtn.style.display = visible ? 'inline-flex' : 'none';
|
|
9543
|
+
pageChatSendBtn.disabled = !visible || !hasText;
|
|
9544
|
+
pageChatSendBtn.style.background = P.accent;
|
|
9545
|
+
pageChatSendBtn.style.color = C.ink;
|
|
9546
|
+
pageChatSendBtn.style.borderLeft = '1px solid ' + P.hairline;
|
|
9547
|
+
pageChatSendBtn.style.opacity = pageChatSendBtn.disabled ? '0.42' : '1';
|
|
9548
|
+
pageChatSendBtn.style.cursor = pageChatSendBtn.disabled ? 'not-allowed' : 'pointer';
|
|
9549
|
+
pageChatSendBtn.title = pageChatSendBtn.disabled ? 'Type what to change first' : 'Send (Enter)';
|
|
9550
|
+
}
|
|
9551
|
+
|
|
9552
|
+
/**
|
|
9553
|
+
* A submitted steer is queued, not ignored, whenever no poll is parked and
|
|
9554
|
+
* the browser knows a generation is in flight: the agent holds the lease and
|
|
9555
|
+
* will not see the steer until it finishes. Saying so beats three dots that
|
|
9556
|
+
* look identical to a lost request.
|
|
9557
|
+
*/
|
|
9558
|
+
function steerQueuedBehindGeneration() {
|
|
9559
|
+
return steerLocked && !agentPollingConnected && agentHasWorkInFlight();
|
|
9560
|
+
}
|
|
9561
|
+
|
|
9562
|
+
function buildSteerQueueHint() {
|
|
9563
|
+
const P = pageChatPalette();
|
|
9564
|
+
const hint = el('span', {
|
|
9565
|
+
display: 'inline-flex', alignItems: 'center', justifyContent: 'flex-end',
|
|
9566
|
+
flex: '1', minWidth: '0', marginLeft: 'auto',
|
|
9567
|
+
padding: '0 10px 0 6px',
|
|
9568
|
+
fontFamily: FONT, fontSize: '10.5px', fontWeight: '500',
|
|
9569
|
+
color: P.patinaPale,
|
|
9570
|
+
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
|
9571
|
+
pointerEvents: 'none',
|
|
9572
|
+
});
|
|
9573
|
+
hint.id = PREFIX + '-page-chat-queue';
|
|
9574
|
+
return hint;
|
|
9575
|
+
}
|
|
9576
|
+
|
|
9577
|
+
function syncSteerQueueHint() {
|
|
9578
|
+
if (!pageChatEl) return;
|
|
9579
|
+
const queued = steerQueuedBehindGeneration();
|
|
9580
|
+
if (queued) {
|
|
9581
|
+
if (!pageChatQueueHintEl) {
|
|
9582
|
+
pageChatQueueHintEl = buildSteerQueueHint();
|
|
9583
|
+
pageChatEl.appendChild(pageChatQueueHintEl);
|
|
9584
|
+
}
|
|
9585
|
+
pageChatQueueHintEl.textContent = STEER_QUEUED_HINT;
|
|
9586
|
+
if (pageChatDotsEl) pageChatDotsEl.style.display = 'none';
|
|
9587
|
+
pageChatEl.style.width = PAGE_CHAT_QUEUED_W;
|
|
9588
|
+
pageChatEl.setAttribute('aria-label', STEER_QUEUED_HINT);
|
|
9589
|
+
return;
|
|
9590
|
+
}
|
|
9591
|
+
if (pageChatQueueHintEl?.parentNode) {
|
|
9592
|
+
pageChatQueueHintEl.remove();
|
|
9593
|
+
pageChatQueueHintEl = null;
|
|
9594
|
+
if (steerLocked) {
|
|
9595
|
+
pageChatEl.style.width = pageChatExpanded ? pageChatExpandedWidth() : PAGE_CHAT_COLLAPSED_W;
|
|
9596
|
+
pageChatEl.setAttribute('aria-label', 'Processing steer request');
|
|
9597
|
+
}
|
|
9598
|
+
}
|
|
9599
|
+
if (pageChatDotsEl) pageChatDotsEl.style.display = '';
|
|
8368
9600
|
}
|
|
8369
9601
|
|
|
8370
9602
|
function shouldFocusSteerChat() {
|
|
@@ -8536,6 +9768,7 @@ void main() {
|
|
|
8536
9768
|
|
|
8537
9769
|
function syncPageChatFocusRing() {
|
|
8538
9770
|
if (!pageChatEl || !pageChatInput) return;
|
|
9771
|
+
syncPageChatSendButton();
|
|
8539
9772
|
const focused = activeElementDeep() === pageChatInput;
|
|
8540
9773
|
const typingReady = focused && !steerLocked;
|
|
8541
9774
|
pageChatEl.dataset.inputFocused = focused ? 'true' : 'false';
|
|
@@ -8612,9 +9845,9 @@ void main() {
|
|
|
8612
9845
|
function buildSteerProcessingDots() {
|
|
8613
9846
|
const P = pageChatPalette();
|
|
8614
9847
|
const wrap = el('span', {
|
|
8615
|
-
display: 'inline-flex', alignItems: 'center', justifyContent: '
|
|
8616
|
-
gap: '5px', flex: '
|
|
8617
|
-
padding: '0 12px 0
|
|
9848
|
+
display: 'inline-flex', alignItems: 'center', justifyContent: 'flex-end',
|
|
9849
|
+
gap: '5px', flex: '0 0 auto', minWidth: '0', marginLeft: 'auto',
|
|
9850
|
+
padding: '0 12px 0 8px',
|
|
8618
9851
|
pointerEvents: 'none',
|
|
8619
9852
|
});
|
|
8620
9853
|
wrap.setAttribute('aria-hidden', 'true');
|
|
@@ -8689,12 +9922,28 @@ void main() {
|
|
|
8689
9922
|
steerAwaitTimer = setTimeout(() => {
|
|
8690
9923
|
if (!steerLocked || steerRequestId !== id) return;
|
|
8691
9924
|
unlockSteerChat({
|
|
8692
|
-
error:
|
|
9925
|
+
error: steerTimeoutMessage(),
|
|
8693
9926
|
restoreMessage: steerPendingMessage,
|
|
8694
9927
|
});
|
|
8695
9928
|
}, STEER_AWAIT_TIMEOUT_MS);
|
|
8696
9929
|
}
|
|
8697
9930
|
|
|
9931
|
+
/**
|
|
9932
|
+
* Two minutes of silence has three different causes and only one of them is
|
|
9933
|
+
* "live-poll is not running". Naming the wrong one sends the user to restart
|
|
9934
|
+
* a poll loop that was never the problem.
|
|
9935
|
+
*/
|
|
9936
|
+
function steerTimeoutMessage() {
|
|
9937
|
+
const head = 'Steer timed out after 2 minutes. ';
|
|
9938
|
+
if (steerQueuedBehindGeneration()) {
|
|
9939
|
+
return head + 'The agent is still busy with the current generation - your message was not lost, but it never got picked up. Send it again once the variants land.';
|
|
9940
|
+
}
|
|
9941
|
+
if (!agentPollingConnected) {
|
|
9942
|
+
return head + 'No agent is polling right now. Run live-poll.mjs, then send it again.';
|
|
9943
|
+
}
|
|
9944
|
+
return head + 'The agent picked it up but never replied with steer_done. Check the agent session for a stalled or failed steer.';
|
|
9945
|
+
}
|
|
9946
|
+
|
|
8698
9947
|
function lockSteerChat() {
|
|
8699
9948
|
if (!pageChatEl || !pageChatInput) return;
|
|
8700
9949
|
stopVoice({ suppressSubmit: true });
|
|
@@ -8718,6 +9967,7 @@ void main() {
|
|
|
8718
9967
|
pageChatDotsEl = buildSteerProcessingDots();
|
|
8719
9968
|
pageChatEl.appendChild(pageChatDotsEl);
|
|
8720
9969
|
}
|
|
9970
|
+
syncSteerQueueHint();
|
|
8721
9971
|
syncPageChatFocusRing();
|
|
8722
9972
|
syncPageChatChrome();
|
|
8723
9973
|
}
|
|
@@ -8759,6 +10009,10 @@ void main() {
|
|
|
8759
10009
|
pageChatDotsEl.remove();
|
|
8760
10010
|
pageChatDotsEl = null;
|
|
8761
10011
|
}
|
|
10012
|
+
if (pageChatQueueHintEl?.parentNode) {
|
|
10013
|
+
pageChatQueueHintEl.remove();
|
|
10014
|
+
pageChatQueueHintEl = null;
|
|
10015
|
+
}
|
|
8762
10016
|
steerPendingMessage = keepExpanded ? restoreMessage : '';
|
|
8763
10017
|
steerInputWasFocused = false;
|
|
8764
10018
|
syncPageChatChrome();
|
|
@@ -8997,10 +10251,11 @@ void main() {
|
|
|
8997
10251
|
const id = id8();
|
|
8998
10252
|
steerRequestId = id;
|
|
8999
10253
|
steerPendingMessage = text;
|
|
9000
|
-
if (steerInputWasFocused) sendSteerCheckpoint(id, 'steer_input_focused', { focused: true });
|
|
9001
10254
|
lockSteerChat();
|
|
9002
10255
|
scheduleSteerAwaitTimeout(id);
|
|
9003
|
-
|
|
10256
|
+
// Checkpoints follow the steer event, never precede it: the steer event
|
|
10257
|
+
// is what creates the session journal server-side, and a checkpoint for
|
|
10258
|
+
// a not-yet-created session is rejected as unknown_session.
|
|
9004
10259
|
sendEvent({
|
|
9005
10260
|
type: 'steer',
|
|
9006
10261
|
id,
|
|
@@ -9008,9 +10263,11 @@ void main() {
|
|
|
9008
10263
|
pageUrl: location.href,
|
|
9009
10264
|
}).then((res) => {
|
|
9010
10265
|
if (!res) {
|
|
9011
|
-
sendSteerCheckpoint(id, 'steer_send_failed', { message: text });
|
|
9012
10266
|
unlockSteerChat({ error: 'Could not reach live server', restoreMessage: text });
|
|
10267
|
+
return;
|
|
9013
10268
|
}
|
|
10269
|
+
if (steerInputWasFocused) sendSteerCheckpoint(id, 'steer_input_focused', { focused: true });
|
|
10270
|
+
sendSteerCheckpoint(id, 'steer_submitted', { message: text, pageUrl: location.href });
|
|
9014
10271
|
});
|
|
9015
10272
|
}
|
|
9016
10273
|
|
|
@@ -9126,10 +10383,41 @@ void main() {
|
|
|
9126
10383
|
pageChatVoiceBtn.setAttribute('aria-label', 'Voice input');
|
|
9127
10384
|
pageChatVoiceBtn.innerHTML = ICON_PAGE_VOICE;
|
|
9128
10385
|
|
|
10386
|
+
// Visible Send, same affordance the element-level Go bar gets from
|
|
10387
|
+
// buildConfigureSubmitButton. Enter still submits; the button exists so a
|
|
10388
|
+
// typed steer does not look like a dead-end text field.
|
|
10389
|
+
pageChatSendBtn = el('button', {
|
|
10390
|
+
display: 'none', alignItems: 'center', justifyContent: 'center',
|
|
10391
|
+
padding: '0', boxSizing: 'border-box',
|
|
10392
|
+
width: '28px', height: '28px', flexShrink: '0',
|
|
10393
|
+
border: 'none', borderLeft: '1px solid ' + P.hairline,
|
|
10394
|
+
borderRadius: '0',
|
|
10395
|
+
background: P.accent, color: C.ink,
|
|
10396
|
+
cursor: 'pointer',
|
|
10397
|
+
transition: 'filter 0.12s ease, opacity 0.12s ease',
|
|
10398
|
+
});
|
|
10399
|
+
pageChatSendBtn.id = PREFIX + '-page-chat-send';
|
|
10400
|
+
pageChatSendBtn.type = 'button';
|
|
10401
|
+
pageChatSendBtn.setAttribute('aria-label', 'Send steer message');
|
|
10402
|
+
pageChatSendBtn.innerHTML = ICON_CONFIGURE_SUBMIT;
|
|
10403
|
+
pageChatSendBtn.addEventListener('pointerdown', keepSteerPointerInside);
|
|
10404
|
+
pageChatSendBtn.addEventListener('mousedown', keepSteerPointerInside);
|
|
10405
|
+
pageChatSendBtn.addEventListener('mouseenter', () => {
|
|
10406
|
+
if (!pageChatSendBtn.disabled) pageChatSendBtn.style.filter = 'brightness(1.1)';
|
|
10407
|
+
});
|
|
10408
|
+
pageChatSendBtn.addEventListener('mouseleave', () => { pageChatSendBtn.style.filter = 'none'; });
|
|
10409
|
+
pageChatSendBtn.addEventListener('click', (e) => {
|
|
10410
|
+
e.stopPropagation();
|
|
10411
|
+
keepSteerPointerInside(e);
|
|
10412
|
+
if (steerLocked || pageChatSendBtn.disabled) return;
|
|
10413
|
+
submitSteerMessage();
|
|
10414
|
+
});
|
|
10415
|
+
|
|
9129
10416
|
pageChatEl.appendChild(chatIcon);
|
|
9130
10417
|
pageChatEl.appendChild(pageChatHint);
|
|
9131
10418
|
pageChatEl.appendChild(pageChatInput);
|
|
9132
10419
|
pageChatEl.appendChild(pageChatVoiceBtn);
|
|
10420
|
+
pageChatEl.appendChild(pageChatSendBtn);
|
|
9133
10421
|
|
|
9134
10422
|
if (!uiGetById(PREFIX + '-page-chat-style')) {
|
|
9135
10423
|
const s = document.createElement('style');
|
|
@@ -9152,14 +10440,14 @@ void main() {
|
|
|
9152
10440
|
|
|
9153
10441
|
pageChatEl.addEventListener('pointerdown', (e) => {
|
|
9154
10442
|
keepSteerPointerInside(e);
|
|
9155
|
-
if (steerLocked || pageChatVoiceBtn.contains(e.target)) return;
|
|
10443
|
+
if (steerLocked || pageChatVoiceBtn.contains(e.target) || pageChatSendBtn.contains(e.target)) return;
|
|
9156
10444
|
armPageChatForTyping({ expand: true, focus: false });
|
|
9157
10445
|
});
|
|
9158
10446
|
pageChatEl.addEventListener('mousedown', keepSteerPointerInside);
|
|
9159
10447
|
pageChatEl.addEventListener('click', (e) => {
|
|
9160
10448
|
keepSteerPointerInside(e);
|
|
9161
10449
|
if (steerLocked) return;
|
|
9162
|
-
if (pageChatVoiceBtn.contains(e.target)) return;
|
|
10450
|
+
if (pageChatVoiceBtn.contains(e.target) || pageChatSendBtn.contains(e.target)) return;
|
|
9163
10451
|
armPageChatForTyping({ expand: true, focus: true });
|
|
9164
10452
|
});
|
|
9165
10453
|
|
|
@@ -9180,6 +10468,7 @@ void main() {
|
|
|
9180
10468
|
|
|
9181
10469
|
pageChatInput.addEventListener('input', () => {
|
|
9182
10470
|
syncPageChatVisual();
|
|
10471
|
+
syncPageChatSendButton();
|
|
9183
10472
|
});
|
|
9184
10473
|
|
|
9185
10474
|
pageChatInput.addEventListener('focus', () => {
|
|
@@ -9229,24 +10518,47 @@ void main() {
|
|
|
9229
10518
|
</svg>`;
|
|
9230
10519
|
}
|
|
9231
10520
|
|
|
10521
|
+
/**
|
|
10522
|
+
* True while the browser is waiting on work it already handed to the agent.
|
|
10523
|
+
* In these states a quiet poll indicator means "busy", not "absent".
|
|
10524
|
+
*/
|
|
10525
|
+
function agentHasWorkInFlight() {
|
|
10526
|
+
return state === 'GENERATING' || state === 'SAVING';
|
|
10527
|
+
}
|
|
10528
|
+
|
|
10529
|
+
/**
|
|
10530
|
+
* Derived at read time, not cached: which of the two reasons applies depends on
|
|
10531
|
+
* the live state, which moves between the 5s status polls. The truthiness is
|
|
10532
|
+
* the same either way, so the indicator's visuals can stay driven by the
|
|
10533
|
+
* cached value while the wording stays current.
|
|
10534
|
+
*/
|
|
10535
|
+
function agentStatusText() {
|
|
10536
|
+
if (agentPollingConnected) return null;
|
|
10537
|
+
return agentHasWorkInFlight() ? AGENT_BUSY_TIP : AGENT_DISCONNECTED_TIP;
|
|
10538
|
+
}
|
|
10539
|
+
|
|
9232
10540
|
function syncAgentPollingUi(connected) {
|
|
9233
10541
|
agentPollingConnected = !!connected;
|
|
10542
|
+
syncSteerQueueHint();
|
|
9234
10543
|
if (!globalBarBrandEl) return;
|
|
9235
10544
|
const P = barPaletteForTheme(globalBarEl?.dataset.theme || detectPageTheme());
|
|
10545
|
+
agentStatusMessage = agentStatusText();
|
|
9236
10546
|
globalBarBrandEl.dataset.agentConnected = connected ? 'true' : 'false';
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
|
|
10547
|
+
// The tooltip is mouse-only, so carry the same distinction in the label or
|
|
10548
|
+
// screen-reader users are left with the vaguer of the two readings.
|
|
10549
|
+
globalBarBrandEl.setAttribute('aria-label', agentStatusMessage
|
|
10550
|
+
? 'Impeccable live mode - ' + (agentHasWorkInFlight() ? 'agent is working' : 'agent not polling')
|
|
10551
|
+
: 'Impeccable live mode');
|
|
9240
10552
|
globalBarBrandEl.removeAttribute('title');
|
|
9241
|
-
globalBarBrandEl.style.cursor =
|
|
10553
|
+
globalBarBrandEl.style.cursor = agentStatusMessage ? 'help' : 'default';
|
|
9242
10554
|
const mark = globalBarBrandEl.querySelector('[data-brand-mark]');
|
|
9243
10555
|
if (mark) {
|
|
9244
10556
|
mark.innerHTML = brandMarkSvg(connected ? P.accent : AGENT_DISCONNECTED_MARK, 18);
|
|
9245
10557
|
mark.style.opacity = '1';
|
|
9246
10558
|
}
|
|
9247
10559
|
const dot = globalBarBrandEl.querySelector('[data-agent-dot]');
|
|
9248
|
-
if (dot) dot.style.display =
|
|
9249
|
-
if (
|
|
10560
|
+
if (dot) dot.style.display = agentStatusMessage ? 'block' : 'none';
|
|
10561
|
+
if (!agentStatusMessage) hideAgentPollTooltip();
|
|
9250
10562
|
}
|
|
9251
10563
|
|
|
9252
10564
|
function ensureAgentPollTooltip() {
|
|
@@ -9273,14 +10585,17 @@ void main() {
|
|
|
9273
10585
|
whiteSpace: 'normal',
|
|
9274
10586
|
});
|
|
9275
10587
|
agentPollTooltipEl.id = PREFIX + '-agent-poll-tooltip';
|
|
9276
|
-
agentPollTooltipEl.textContent = AGENT_DISCONNECTED_TIP;
|
|
10588
|
+
agentPollTooltipEl.textContent = agentStatusText() || AGENT_DISCONNECTED_TIP;
|
|
9277
10589
|
uiAppend(agentPollTooltipEl);
|
|
9278
10590
|
return agentPollTooltipEl;
|
|
9279
10591
|
}
|
|
9280
10592
|
|
|
9281
10593
|
function showAgentPollTooltip(anchor) {
|
|
9282
|
-
if (
|
|
10594
|
+
if (!agentStatusMessage || !anchor) return;
|
|
9283
10595
|
const tip = ensureAgentPollTooltip();
|
|
10596
|
+
// Re-derive rather than reuse the cached copy: the live state may have moved
|
|
10597
|
+
// since the last status poll set it.
|
|
10598
|
+
tip.textContent = agentStatusText() || AGENT_DISCONNECTED_TIP;
|
|
9284
10599
|
tip.style.transition = 'none';
|
|
9285
10600
|
tip.style.display = 'block';
|
|
9286
10601
|
tip.style.opacity = '1';
|
|
@@ -9310,7 +10625,9 @@ void main() {
|
|
|
9310
10625
|
fetch('http://localhost:' + PORT + '/status?token=' + TOKEN, { cache: 'no-store' })
|
|
9311
10626
|
.then((res) => (res.ok ? res.json() : null))
|
|
9312
10627
|
.then((data) => {
|
|
9313
|
-
if (data && typeof data.agentPolling === 'boolean')
|
|
10628
|
+
if (data && typeof data.agentPolling === 'boolean') {
|
|
10629
|
+
syncAgentPollingUi(data.agentPolling);
|
|
10630
|
+
}
|
|
9314
10631
|
})
|
|
9315
10632
|
.catch(() => { /* server loss handled elsewhere */ });
|
|
9316
10633
|
}
|
|
@@ -9984,6 +11301,7 @@ void main() {
|
|
|
9984
11301
|
window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*');
|
|
9985
11302
|
setLiveState('IDLE');
|
|
9986
11303
|
document.getElementById(PICK_CURSOR_STYLE_ID)?.remove();
|
|
11304
|
+
removeVariantStateStylesheet();
|
|
9987
11305
|
window.__IMPECCABLE_LIVE_INIT__ = false;
|
|
9988
11306
|
console.log('[impeccable] Live mode exited.');
|
|
9989
11307
|
}
|
|
@@ -10472,7 +11790,7 @@ void main() {
|
|
|
10472
11790
|
if (designState.present === false) {
|
|
10473
11791
|
const empty = document.createElement('div');
|
|
10474
11792
|
empty.className = 'empty';
|
|
10475
|
-
empty.innerHTML = `<strong>No DESIGN.md yet</strong>Create one by running <code
|
|
11793
|
+
empty.innerHTML = `<strong>No DESIGN.md yet</strong>Create one by running <code>${IMPECCABLE_COMMAND} document</code> in your terminal, then re-open this panel.`;
|
|
10476
11794
|
body.appendChild(empty);
|
|
10477
11795
|
return;
|
|
10478
11796
|
}
|
|
@@ -10502,7 +11820,7 @@ void main() {
|
|
|
10502
11820
|
box.className = 'stale';
|
|
10503
11821
|
box.innerHTML = `
|
|
10504
11822
|
<span class="stale-dot"></span>
|
|
10505
|
-
<span class="stale-text"><strong>DESIGN.md is newer than .impeccable/design.json.</strong> Run <code
|
|
11823
|
+
<span class="stale-text"><strong>DESIGN.md is newer than .impeccable/design.json.</strong> Run <code>${IMPECCABLE_COMMAND} document</code> to refresh the sidecar.</span>
|
|
10506
11824
|
`;
|
|
10507
11825
|
return box;
|
|
10508
11826
|
}
|
|
@@ -10510,13 +11828,34 @@ void main() {
|
|
|
10510
11828
|
function renderParsedMdCta() {
|
|
10511
11829
|
const box = document.createElement('div');
|
|
10512
11830
|
box.className = 'parsed-md-cta';
|
|
10513
|
-
box.innerHTML = `<strong>Basic view</strong>This panel reads the tokens in your <code>DESIGN.md</code> frontmatter. Running <code
|
|
11831
|
+
box.innerHTML = `<strong>Basic view</strong>This panel reads the tokens in your <code>DESIGN.md</code> frontmatter. Running <code>${IMPECCABLE_COMMAND} document</code> also generates a <code>.impeccable/design.json</code> sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`;
|
|
10514
11832
|
return box;
|
|
10515
11833
|
}
|
|
10516
11834
|
|
|
10517
11835
|
// Unified render: merge parsed DESIGN.md frontmatter with sidecar v2
|
|
10518
11836
|
|
|
11837
|
+
/**
|
|
11838
|
+
* The empty state has to say which emptiness it is. `present:false` (no
|
|
11839
|
+
* DESIGN.md at all) is handled upstream in renderDesignBody; everything here
|
|
11840
|
+
* means the helper found a design system and this panel found nothing in it
|
|
11841
|
+
* worth drawing. Telling that user "no design system data" reads as "your
|
|
11842
|
+
* DESIGN.md is missing" and sends them to write a file they already have.
|
|
11843
|
+
*/
|
|
11844
|
+
function designEmptyMessage() {
|
|
11845
|
+
if (designState.hasMd && !designState.hasSidecar) {
|
|
11846
|
+
return 'DESIGN.md found, no structured tokens to display. Run ' + IMPECCABLE_COMMAND + ' document to generate the .impeccable/design.json sidecar.';
|
|
11847
|
+
}
|
|
11848
|
+
if (designState.hasMd) {
|
|
11849
|
+
return 'DESIGN.md and its sidecar were found, but neither carries colors, type, radii, or components to display.';
|
|
11850
|
+
}
|
|
11851
|
+
return 'No design system data available.';
|
|
11852
|
+
}
|
|
11853
|
+
|
|
10519
11854
|
function renderDesignVisual(body, parsed, sidecar) {
|
|
11855
|
+
// Count only what this function draws: renderDesignBody may already have
|
|
11856
|
+
// appended a stale-sidecar hint or the basic-view CTA, and those must not
|
|
11857
|
+
// pass for token content.
|
|
11858
|
+
const beforeCount = body.childElementCount;
|
|
10520
11859
|
const frontmatter = parsed?.frontmatter || {};
|
|
10521
11860
|
const extensions = sidecar?.extensions || {};
|
|
10522
11861
|
const proseColors = parsed?.colors || null;
|
|
@@ -10544,8 +11883,8 @@ void main() {
|
|
|
10544
11883
|
body.appendChild(renderOverviewCollapsible(narrative));
|
|
10545
11884
|
}
|
|
10546
11885
|
|
|
10547
|
-
if (body.childElementCount ===
|
|
10548
|
-
body.appendChild(msgDiv('empty',
|
|
11886
|
+
if (body.childElementCount === beforeCount) {
|
|
11887
|
+
body.appendChild(msgDiv('empty', designEmptyMessage()));
|
|
10549
11888
|
}
|
|
10550
11889
|
}
|
|
10551
11890
|
|