diff --git a/scripts/test-suites.mjs b/scripts/test-suites.mjs index 9bb675c1f..4145cfc83 100644 --- a/scripts/test-suites.mjs +++ b/scripts/test-suites.mjs @@ -162,6 +162,7 @@ export const SUITES = { 'tests/live-svelte-component-accept.test.mjs', 'tests/live-tanstack-adapter.test.mjs', 'tests/live-target-context.test.mjs', + 'tests/live-ui-surfaces.test.mjs', 'tests/live-wrap.test.mjs', 'tests/live-wrap-buffer-aware.test.mjs', ], diff --git a/skill/scripts/live-browser.js b/skill/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/skill/scripts/live-browser.js +++ b/skill/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { 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'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { 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'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/skill/scripts/live/browser-script-parts.mjs b/skill/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/skill/scripts/live/browser-script-parts.mjs +++ b/skill/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/skill/scripts/live/ui-surfaces.mjs b/skill/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/skill/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic