Compare commits

...
Author SHA1 Message Date
Paul BakausandClaude Opus 5 7dc88f670d Give the Live UI surface inventory one definition again
The list of Live chrome surfaces was inlined into live-browser.js as a
function-scope const when live/ui-core.mjs was deleted for having zero
in-repo references. It had one out-of-repo reference. The private
impeccable-site repo imports it at build time: its Live UI lab must hold
a snapshot for every surface Live defines, and the site build fails with
the surface name when one is missing. Inlining put the list out of reach
of every Node importer, so the site had to regex it back out of the
browser script, and the guard only kept passing because the site's
materialized copy of skill/ was stale.

A guard that reads a list the site itself maintains guards nothing, so
the fix is a real export rather than a better parser.

skill/scripts/live/ui-surfaces.mjs is now the single definition. The
browser-runtime constraint is unchanged and satisfied the same way the
command palette already solves it: live-browser.js is served raw and
injected as a classic <script>, so it cannot import an ES module. The
/live.js assembler serializes the module into
window.__IMPECCABLE_LIVE_UI_SURFACES__ in the prelude it already writes
for the token, port and vocabulary, and live-browser.js reads the global.
assembleLiveBrowserScript defaults the value from the module rather than
taking it from live-server.mjs, so the bundle carries the canonical
inventory by construction instead of by a caller remembering to pass it.

The emitted inventory is byte-identical to the inlined one.

tests/live-ui-surfaces.test.mjs pins both halves of the seam: the module
is the definition (live-browser.js must not redeclare it), the prefix the
module builds ids from matches the PREFIX live-browser.js hardcodes, and
the assembled bundle still carries the list. live-server.test.mjs gets
the matching integration check against a served /live.js.

One existing assertion changed. live-browser-regression.test.mjs checked
that the steer Send control is registered as live chrome by matching the
text of the inline literal's last line. That encoded where the list was
written, not what it contains; it now asserts membership in the imported
LIVE_UI_COMPONENT_IDS, which is the behaviour it was after.

Verified with the full default suite plus a live-e2e fixture run
(vite8-react-modal), so the overlay is exercised end to end in a browser.

AI-assisted via Claude Code under maintainer direction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 14:12:19 -07:00
7 changed files with 227 additions and 22 deletions
+1
View File
@@ -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',
],
+14 -17
View File
@@ -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))];
//
+24 -2
View File
@@ -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 || '');
+75
View File
@@ -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 <script>, so it cannot
* import this module at runtime; it reads the injected global instead, the
* same path live/vocabulary.mjs already takes for the command palette.
* - the private impeccable-site repo — site/components/LiveUiGallery.astro
* and tests/live-ui-lab.test.mjs import LIVE_UI_SURFACES at build time and
* fail the site build when the Live UI lab has no snapshot for a surface
* defined here. That guard only guards if it reads this list rather than a
* copy the site keeps, so this module must stay importable from Node.
* Renaming a key or the module is a breaking change for that build; the
* list was briefly inlined into live-browser.js and the site had to parse
* it back out with a regex.
*
* Add a surface here and both the browser bundle and the site lab follow.
*/
/** Id prefix every Live chrome element carries. Mirrored by PREFIX in live-browser.js. */
export const LIVE_UI_PREFIX = 'impeccable-live';
const id = (suffix) => `${LIVE_UI_PREFIX}-${suffix}`;
/**
* The mount contract every Live chrome adapter (DOM, Svelte, ...) satisfies.
* Published alongside the surfaces on __IMPECCABLE_LIVE_CHROME_CORE__.
*/
export const LIVE_CHROME_MOUNT_CONTRACT = Object.freeze(['root', 'transport', 'state', 'actions']);
export const LIVE_UI_SURFACES = Object.freeze([
{
key: 'global-bottom-bar',
ids: [
id('global-bar'), id('global-bar-brand'), id('pick-toggle'), id('insert-toggle'),
id('detect-toggle'), id('detect-badge'), id('design-toggle'), id('page-chat'),
id('page-chat-input'), id('page-chat-voice'), id('page-chat-send'),
],
},
{ key: 'pending-copy-edit-dock', ids: [id('pending-dock')] },
{
key: 'element-selection-chrome',
ids: [
id('highlight'), id('tooltip'), id('bar'), id('selection-pill'), id('input'),
id('configure-voice'), id('configure-bar-tooltip'),
],
},
{ key: 'action-picker', ids: [id('picker')] },
{ key: 'edit-chrome', ids: [id('edit-badge')] },
{ key: 'generating-row', ids: [id('bar'), id('shader')] },
{ key: 'variant-cycling-row', ids: [id('bar'), id('params-panel')] },
{ key: 'variant-params-panel', ids: [id('params-panel')] },
{ key: 'saving-confirmed-rows', ids: [id('bar')] },
{
key: 'insert-mode-chrome',
ids: [
id('insert-line'), id('insert-placeholder'), id('placeholder-resize'), id('insert-input'),
id('insert-voice'), id('insert-create'), id('insert-create-tooltip'),
],
},
{ key: 'annotation-chrome', ids: [id('annot'), id('annot-svg'), id('annot-pins'), id('annot-clear')] },
{ key: 'design-system-panel', ids: [id('design-host')] },
{ key: 'toasts-and-errors', ids: [id('toast'), id('mount-error')] },
{ key: 'css-isolation-boundary', ids: [id('root')] },
].map((surface) => Object.freeze({ ...surface, ids: Object.freeze(surface.ids) })));
/** Every id any surface owns, de-duplicated, in surface order. */
export const LIVE_UI_COMPONENT_IDS = Object.freeze([
...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids)),
]);
+8 -3
View File
@@ -18,6 +18,8 @@ import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { LIVE_UI_COMPONENT_IDS, LIVE_UI_PREFIX } from '../skill/scripts/live/ui-surfaces.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const LIVE_BROWSER = path.resolve(
__dirname,
@@ -1280,9 +1282,12 @@ describe('live-browser.js regression guards', () => {
/if \(e\.key === 'Enter'\) \{\s*e\.preventDefault\(\);\s*submitSteerMessage\(\);/,
'keyboard submit stays',
);
assert.match(
SOURCE,
/PREFIX \+ '-page-chat-send'\] \}/,
// The inventory used to be an inline literal here, so this used to be a
// source-text match on the surface entry. It now lives in
// live/ui-surfaces.mjs (importable, one definition), so assert membership
// in the real list instead of the shape of the line that declares it.
assert.ok(
LIVE_UI_COMPONENT_IDS.includes(`${LIVE_UI_PREFIX}-page-chat-send`),
'the Send control must be registered as live UI chrome so it is excluded from capture',
);
});
+11
View File
@@ -200,6 +200,17 @@ describe('live-server integration', () => {
assert.deepEqual(injected, LIVE_COMMANDS);
});
it('/live.js injects the canonical Live UI surface inventory', async () => {
// Same path as the vocabulary: live-browser.js is a classic script and
// cannot import live/ui-surfaces.mjs, so the served bundle must carry the
// module's list. Node consumers (including the impeccable-site Live UI lab)
// import the module, and this is what keeps the two the same list.
const { LIVE_UI_SURFACES } = await import('../skill/scripts/live/ui-surfaces.mjs');
const body = await (await fetch(`http://localhost:${server.port}/live.js?token=${server.token}`)).text();
const injected = JSON.parse(body.match(/window\.__IMPECCABLE_LIVE_UI_SURFACES__\s*=\s*(\[.*?\]);\n/s)[1]);
assert.deepEqual(injected, JSON.parse(JSON.stringify(LIVE_UI_SURFACES)));
});
it('/status returns durable recovery state', async () => {
await drainPolls(server);
const eventRes = await fetch(`http://localhost:${server.port}/events`, {
+94
View File
@@ -0,0 +1,94 @@
/**
* The Live chrome inventory has exactly one definition, and it is importable.
*
* live-browser.js is served raw as a classic script and cannot import an ES
* module, which is why the list was once inlined there as a function-scope
* const. That put it out of reach of every Node consumer: this repo's own
* tooling, and the private impeccable-site repo, whose Live UI lab fails its
* build when a surface defined here has no snapshot. That guard is only a guard
* while it reads Live's real list, so these tests pin both halves: the module
* stays the definition, and the browser bundle still receives it at runtime.
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { assembleLiveBrowserScript } from '../skill/scripts/live/browser-script-parts.mjs';
import {
LIVE_CHROME_MOUNT_CONTRACT,
LIVE_UI_COMPONENT_IDS,
LIVE_UI_PREFIX,
LIVE_UI_SURFACES,
} from '../skill/scripts/live/ui-surfaces.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const LIVE_BROWSER_SOURCE = fs.readFileSync(
path.resolve(__dirname, '..', 'skill/scripts/live-browser.js'),
'utf-8',
);
describe('live UI surfaces module', () => {
it('exports a non-empty inventory with unique keys', () => {
assert.ok(LIVE_UI_SURFACES.length > 0);
const keys = LIVE_UI_SURFACES.map((surface) => surface.key);
assert.equal(new Set(keys).size, keys.length, 'surface keys must be unique');
for (const surface of LIVE_UI_SURFACES) {
assert.equal(typeof surface.key, 'string');
assert.ok(surface.ids.length > 0, `${surface.key} must own at least one element id`);
}
});
it('builds every id from the canonical prefix', () => {
for (const id of LIVE_UI_COMPONENT_IDS) {
assert.ok(id.startsWith(`${LIVE_UI_PREFIX}-`), `${id} must carry the ${LIVE_UI_PREFIX} prefix`);
}
});
it('agrees with the PREFIX live-browser.js hardcodes', () => {
// live-browser.js cannot import LIVE_UI_PREFIX, so it declares the string
// itself and every id it creates at runtime is built from that. If the two
// drift, the inventory names ids that no element on the page ever has.
const declared = LIVE_BROWSER_SOURCE.match(/const PREFIX = '([^']+)';/);
assert.ok(declared, 'live-browser.js must declare `const PREFIX = ...`');
assert.equal(declared[1], LIVE_UI_PREFIX);
});
it('is not redefined inside live-browser.js', () => {
// The list living in two places is the failure this module exists to end.
assert.doesNotMatch(LIVE_BROWSER_SOURCE, /const LIVE_UI_SURFACES = \[/);
assert.match(LIVE_BROWSER_SOURCE, /window\.__IMPECCABLE_LIVE_UI_SURFACES__/);
});
});
describe('live UI surfaces reach the browser bundle', () => {
const assemble = () => assembleLiveBrowserScript({
token: 'token-a',
port: 8421,
vocabulary: [],
parts: [{ name: 'browser-ui', file: 'live-browser.js', source: LIVE_BROWSER_SOURCE }],
});
it('serializes the canonical inventory into the prelude by default', () => {
const script = assemble();
const surfaces = JSON.parse(
script.match(/window\.__IMPECCABLE_LIVE_UI_SURFACES__ = (\[.*?\]);\n/s)[1],
);
assert.deepEqual(surfaces, JSON.parse(JSON.stringify(LIVE_UI_SURFACES)));
const contract = JSON.parse(
script.match(/window\.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = (\[.*?\]);\n/s)[1],
);
assert.deepEqual(contract, [...LIVE_CHROME_MOUNT_CONTRACT]);
});
it('injects the globals before the script part that reads them', () => {
const script = assemble();
assert.ok(
script.indexOf('window.__IMPECCABLE_LIVE_UI_SURFACES__ =')
< script.indexOf('impeccable live script part: browser-ui'),
);
});
});