@legionworks/facet 1.9.0 → 1.10.1
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 +50 -21
- package/dist/gallery/{chunk-c6s9k9ty.js → chunk-0by8qn4x.js} +86 -3
- package/dist/gallery/{chunk-4g8rem85.css → chunk-2dge4gwb.css} +22 -1
- package/dist/gallery/frame/artifact.css +3 -0
- package/dist/gallery/frame/chunks/{markdown-fvfncadf.js → markdown-g3asgaw3.js} +8 -0
- package/dist/gallery/frame/frame.css +11 -24
- package/dist/gallery/frame/runtime/chart.js +24 -2
- package/dist/gallery/frame/runtime/html.js +1 -1
- package/dist/gallery/frame/runtime/markdown.js +1 -1
- package/dist/gallery/frame/runtime/mermaid.js +1 -1
- package/dist/gallery/frame/runtime/svg.js +1 -1
- package/dist/gallery/frame/runtime/tsx.js +1 -1
- package/dist/gallery/index.html +4 -2
- package/docs/reference/cli.md +30 -2
- package/docs/reference/export.md +4 -0
- package/docs/reference/html.md +3 -3
- package/docs/reference/mcp.md +29 -8
- package/docs/reference/security.md +7 -0
- package/docs/reference/storage.md +6 -1
- package/docs/reference/tsx.md +16 -0
- package/docs/reference/validation.md +34 -6
- package/package.json +4 -4
- package/skills/facet/SKILL.md +24 -2
- package/src/cli/commands/promote.ts +1 -0
- package/src/cli/commands/templates.ts +25 -0
- package/src/cli/main.ts +4 -0
- package/src/cli/parser.ts +4 -0
- package/src/cli/presenter.ts +48 -0
- package/src/gallery-web/app.ts +65 -2
- package/src/gallery-web/favicon.ts +1 -0
- package/src/gallery-web/frame/renderers/chart.ts +31 -1
- package/src/gallery-web/frame/runtime.ts +7 -1
- package/src/gallery-web/frame/styles/artifact.css +3 -0
- package/src/gallery-web/frame/styles/frame.css +11 -24
- package/src/gallery-web/frame-html.ts +7 -2
- package/src/gallery-web/index.html +3 -1
- package/src/gallery-web/styles/verdict.css +19 -4
- package/src/harness-adapters/mcp/cli-bridge.ts +11 -1
- package/src/harness-adapters/mcp/main.ts +2 -0
- package/src/harness-adapters/mcp/server.ts +92 -69
- package/src/harness-adapters/mcp/tool-schemas.ts +7 -0
- package/src/service/dispatcher.ts +22 -0
- package/src/service/router-guards.ts +2 -0
- package/src/service/store/migrations.ts +12 -2
- package/src/service/store/repository-lifecycle.ts +91 -19
- package/src/service/store/repository.ts +60 -2
- package/src/service/store/schema.ts +4 -0
- package/src/service/stored-verdict.ts +3 -0
- package/src/shared/contracts/artifact.ts +1 -0
- package/src/shared/contracts/commands/index.ts +10 -0
- package/src/shared/contracts/commands/names.ts +2 -0
- package/src/shared/contracts/commands/requests.ts +7 -0
- package/src/shared/contracts/commands/results.ts +20 -1
- package/src/shared/contracts/promotion.ts +16 -0
- package/src/shared/contracts/validation.ts +7 -0
- package/src/shared/errors/facet-error.ts +2 -0
- package/src/shared/errors/store-error.ts +4 -0
- package/src/shared/html/artifact-main.ts +3 -0
- package/src/shared/html/style-vocabulary.ts +1 -0
- package/src/shared/storage-version.ts +1 -1
- package/src/validation/tier0/dom-shim.ts +3 -18
- package/src/validation/tier0/markdown.ts +28 -5
- package/src/validation/tier0/mermaid.ts +17 -8
- package/src/validation/tier0/worker-dispatch.ts +1 -1
- package/src/validation/tier1/entries/tsx.ts +1 -0
- package/src/validation/tier1/harness.ts +2 -1
- package/src/validation/tier1/isolated-probe.ts +4 -0
- package/src/validation/tier1/protocol-probe.ts +37 -0
- package/src/validation/tier1/runner.ts +21 -4
- package/src/validation/tier1/verdict.ts +24 -39
- package/templates/capacity-report.tsx +1 -1
- package/templates/fleet-dashboard.html +1 -1
- package/templates/html-release-ledger.html +1 -1
- package/templates/html-status-report.html +1 -1
- package/templates/incident-console.tsx +1 -1
- package/templates/tsx-interactive-counter.tsx +1 -1
- package/templates/tsx-status-report.tsx +1 -1
package/docs/reference/mcp.md
CHANGED
|
@@ -4,12 +4,22 @@ On harnesses with shell access, the CLI is the integration; the MCP adapter is f
|
|
|
4
4
|
|
|
5
5
|
Facet's npm package includes the `facet-mcp` bin. It needs Bun `1.4.0`; npm and pnpm install the package, but Bun remains the runtime.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Install globally, then run the bin with Bun `1.4.0` or newer:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
|
|
10
|
+
bun add -g @legionworks/facet
|
|
11
|
+
facet-mcp
|
|
11
12
|
```
|
|
12
13
|
|
|
14
|
+
Releases through `1.9.0` ship a `facet-mcp` bin that cannot launch: its entry file has no `#!/usr/bin/env bun` line. Use `1.10.0` or newer. With `1.10.0` on Bun `1.4.0`, each of these answered JSON-RPC `initialize` and listed six tools:
|
|
15
|
+
|
|
16
|
+
- `bun add -g @legionworks/facet`, then `facet-mcp`
|
|
17
|
+
- `npm i -g @legionworks/facet`, then `facet-mcp`
|
|
18
|
+
- `npx -p @legionworks/facet facet-mcp`
|
|
19
|
+
- `bunx -p @legionworks/facet facet-mcp`
|
|
20
|
+
|
|
21
|
+
Run `npx -p` from outside a Facet checkout. Inside one, npx resolves the local project instead of the published package and fails with `facet-mcp: command not found`.
|
|
22
|
+
|
|
13
23
|
The adapter resolves the CLI in this order: `FACET_CLI`, then `bun <adapter-relative-repository>/src/cli/main.ts`, then `facet` on `PATH`. Set `FACET_CLI` to an absolute CLI executable when the adapter should use another installation.
|
|
14
24
|
|
|
15
25
|
## Register the server
|
|
@@ -21,7 +31,7 @@ OpenCode config:
|
|
|
21
31
|
"mcp": {
|
|
22
32
|
"facet": {
|
|
23
33
|
"type": "local",
|
|
24
|
-
"command": ["
|
|
34
|
+
"command": ["facet-mcp"],
|
|
25
35
|
"enabled": true
|
|
26
36
|
}
|
|
27
37
|
}
|
|
@@ -34,8 +44,7 @@ Claude Code project config (`.mcp.json`):
|
|
|
34
44
|
{
|
|
35
45
|
"mcpServers": {
|
|
36
46
|
"facet": {
|
|
37
|
-
"command": "
|
|
38
|
-
"args": ["-p", "@legionworks/facet", "facet-mcp"]
|
|
47
|
+
"command": "facet-mcp"
|
|
39
48
|
}
|
|
40
49
|
}
|
|
41
50
|
}
|
|
@@ -45,8 +54,7 @@ Codex config (`~/.codex/config.toml`):
|
|
|
45
54
|
|
|
46
55
|
```toml
|
|
47
56
|
[mcp_servers.facet]
|
|
48
|
-
command = "
|
|
49
|
-
args = ["-p", "@legionworks/facet", "facet-mcp"]
|
|
57
|
+
command = "facet-mcp"
|
|
50
58
|
```
|
|
51
59
|
|
|
52
60
|
Set `FACET_HOME` in the host configuration when the adapter must use a non-default Facet runtime directory. Set `FACET_CLI` only for an alternate CLI executable.
|
|
@@ -55,6 +63,7 @@ Set `FACET_HOME` in the host configuration when the adapter must use a non-defau
|
|
|
55
63
|
|
|
56
64
|
| Tool | Inputs | Effect |
|
|
57
65
|
| ----------------- | ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
66
|
+
| `facet_create` | `projectId`, `slug`, `title` | Creates an artifact; pass its returned `data.artifact.id` to publish. |
|
|
58
67
|
| `facet_publish` | `artifactId`, `type`, exactly one of `sourceText` or `file`; optional `execution`, `renderer`, `note`, `parentRevisionId` | Publishes inline source through CLI stdin or reads the named local file. |
|
|
59
68
|
| `facet_read_back` | `artifactId`; optional `revisionSha`, `tier` (`0` \| `1` \| `visual`) | Reads the latest or named revision. Tier 1 and visual need browser evidence. |
|
|
60
69
|
| `facet_status` | optional `artifactId`, `start` | Reads status. Set `start` only when activation is intended. |
|
|
@@ -65,7 +74,15 @@ Set `FACET_HOME` in the host configuration when the adapter must use a non-defau
|
|
|
65
74
|
|
|
66
75
|
Exactly one of `sourceText` or `file` is required. The adapter returns `invalid_request` when both or neither are supplied.
|
|
67
76
|
|
|
68
|
-
|
|
77
|
+
Tier 0 means structure was parsed and compiled, not rendered. Request
|
|
78
|
+
`facet_read_back` with `tier: "visual"` before reporting a render. Visual
|
|
79
|
+
evidence covers the first rendered state of an interactive TSX artifact;
|
|
80
|
+
later interaction is not validated and does not change the stored verdict.
|
|
81
|
+
When testing by hand with the MCP Inspector CLI, `--tool-arg key=value` passes
|
|
82
|
+
`\n` literally, so multi-line `sourceText` arrives as one line; pass `file`
|
|
83
|
+
instead.
|
|
84
|
+
|
|
85
|
+
The MCP surface has six artifact tools. Start a cold-home flow with `facet_create`, then call `facet_publish`, `facet_read_back`, and `facet_export`. Each tool advertises its input schema in `tools/list`; run `facet doctor` through the CLI.
|
|
69
86
|
|
|
70
87
|
## Result and error handling
|
|
71
88
|
|
|
@@ -73,6 +90,10 @@ Every tool returns one text content item containing the complete versioned Facet
|
|
|
73
90
|
|
|
74
91
|
Typed Facet failures return that same envelope with `isError: true`. The JSON body preserves `error.code`, `error.message`, `error.retryable`, and `error.details`. The adapter converts malformed CLI stdout and subprocess failures into typed `invalid_envelope` errors instead of throwing raw process text through MCP.
|
|
75
92
|
|
|
93
|
+
For `output_unwritable`, `error.details.out` is the absolute path whose write
|
|
94
|
+
failed. A malformed Mermaid fence is a verdict result, not a tool failure: the
|
|
95
|
+
call returns `isError: false` with `data.verdict.status: "error"`.
|
|
96
|
+
|
|
76
97
|
## Boundary
|
|
77
98
|
|
|
78
99
|
The adapter only shells out to `facet` and parses the shared wire envelope. It does not import service, validation, or gallery code. The boundary checker permits only the MCP SDK, Zod, Node builtins, adapter-local modules, shared contracts, and the shared product version in `src/harness-adapters/mcp/`.
|
|
@@ -107,6 +107,13 @@ service.
|
|
|
107
107
|
| `2` | Removes Tier 0 and Tier 1 network-namespace isolation. Real validators still run. |
|
|
108
108
|
| `3` | Performs no validation and records `insecure:unvalidated`. |
|
|
109
109
|
|
|
110
|
+
Level 3 skips publish-time validation, not an explicit visual read-back.
|
|
111
|
+
`facet read-back --tier visual` still runs Tier 1, and that verdict carries the
|
|
112
|
+
insecure marker. Evidence consumers must check the marker, not infer trust from
|
|
113
|
+
`status` alone. A level-3 visual verdict does not count toward `facet promote`
|
|
114
|
+
either — the gate refuses it as `insecure:unvalidated` unless the operator
|
|
115
|
+
passes `--allow-unverified`.
|
|
116
|
+
|
|
110
117
|
Levels compose as a forced floor: the effective level is never below the
|
|
111
118
|
operator's `FACET_INSECURE` value. `FACET_INSECURE_AUTO=1` may raise a level when
|
|
112
119
|
startup probes fail, but it never selects level 3. With auto mode off, hard
|
|
@@ -23,7 +23,7 @@ the evidence root and every per-run evidence directory.
|
|
|
23
23
|
## Schema migrations
|
|
24
24
|
|
|
25
25
|
`runMigrations` records applied versions in `schema_migrations` and applies
|
|
26
|
-
additive fragments in order. The current schema is
|
|
26
|
+
additive fragments in order. The current schema is v10:
|
|
27
27
|
|
|
28
28
|
| version | change |
|
|
29
29
|
| ------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -36,6 +36,7 @@ additive fragments in order. The current schema is v9:
|
|
|
36
36
|
| v7 | Backfills HTML observation defaults. |
|
|
37
37
|
| v8 | Adds `tsx`, declared revision execution, and nullable `render_runs.compiled_path`. |
|
|
38
38
|
| v9 | Adds `render_runs.screenshot_format`, recorded as `png` or `webp` for retained evidence. |
|
|
39
|
+
| v10 | Adds nullable `templates.promotion_override`; null for pre-v10 templates and for promotions that passed the verification gate. |
|
|
39
40
|
|
|
40
41
|
Migrations are additive and transactional. Existing revisions are not rewritten
|
|
41
42
|
when a later schema version is applied.
|
|
@@ -81,6 +82,10 @@ artifact and unlinks older screenshot and console files. Rows marked
|
|
|
81
82
|
`retained = 1` are exempt. Cleanup is best-effort; the database row remains the
|
|
82
83
|
authority and the orphan sweep can recover from stale files.
|
|
83
84
|
|
|
85
|
+
Facet targets one operator on a local machine. The evidence bound is the last
|
|
86
|
+
`EVIDENCE_LAST_N_PER_ARTIFACT` Tier 1 runs per artifact, plus any retained runs.
|
|
87
|
+
There is no quota; retained evidence can grow without a fixed limit.
|
|
88
|
+
|
|
84
89
|
## Templates
|
|
85
90
|
|
|
86
91
|
A template records one immutable revision ID. Later publication to the source
|
package/docs/reference/tsx.md
CHANGED
|
@@ -29,6 +29,15 @@ and must not call `createRoot` or self-mount.
|
|
|
29
29
|
|
|
30
30
|
## Imports and styling
|
|
31
31
|
|
|
32
|
+
Artifacts cannot fetch data (`connect-src 'none'`). Generate source with the
|
|
33
|
+
data embedded, then publish it. For repeated updates, regenerate the file and
|
|
34
|
+
use `facet publish --watch` to publish each changed version:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
node -e 'const data = [{label:"A", value:3}]; require("node:fs").writeFileSync("report.tsx", `export default function Report(){ return <pre>{JSON.stringify(${JSON.stringify(data)})}</pre> }`)'
|
|
38
|
+
facet publish --artifact-id <id> --type tsx --file report.tsx
|
|
39
|
+
```
|
|
40
|
+
|
|
32
41
|
The compiler accepts only vendored modules: `react`, `react-dom`,
|
|
33
42
|
`react-dom/client`, `react/jsx-runtime`, and `react/jsx-dev-runtime`.
|
|
34
43
|
Pinned versions are React and React DOM `19.2.8`, TypeScript `5.7.3`.
|
|
@@ -62,6 +71,13 @@ has no host port or service capability.
|
|
|
62
71
|
|
|
63
72
|
## Storage and export
|
|
64
73
|
|
|
74
|
+
The TSX verifier loads the vendored `artifact.css` used by the gallery frame.
|
|
75
|
+
A runtime error after initial render, such as one thrown by a click handler,
|
|
76
|
+
appears as a gallery signal in an `aria-live="polite"` status region. It does
|
|
77
|
+
not rewrite the immutable verdict. It clears on revision swap. Switching the
|
|
78
|
+
theme keeps the frame mounted, including interactive component state and any
|
|
79
|
+
runtime error signal already displayed.
|
|
80
|
+
|
|
65
81
|
Revision source remains immutable. Compilation creates derived bytes recorded at
|
|
66
82
|
the run's `compiled_path`; TSX source export writes the original `.tsx` bytes.
|
|
67
83
|
Render export remains the retained Tier 1 screenshot. Evidence retains the last
|
|
@@ -18,6 +18,7 @@ Every other layer is bound to it through `VerdictSchema.status`.
|
|
|
18
18
|
| `partial:opaque_content` | An opaque DOM region was observed, so structural contents were not verified. MUST carry a screenshot path, or a typed `screenshotError` marker when capture fails transiently. |
|
|
19
19
|
| `partial:external_resources` | The artifact references external HTTPS images the no-egress verifier could not observe. MUST carry a screenshot path, or a typed `screenshotError` marker when capture fails transiently. |
|
|
20
20
|
| `partial:unstable` | TSX interactive mode: the structure observed at the render barrier differed from the structure observed after a bounded stability window. Deliberately NOT `tampered` — a legitimately animated or async-loading component also changes structure between observations, and branding that a forgery would manufacture the false-verdict class this project has spent three arcs eliminating. `tampered` stays reserved for channel divergence. MUST carry a screenshot path, or a typed `screenshotError` marker when capture fails transiently. |
|
|
21
|
+
| `partial:empty_render` | TSX Tier 1: all authoritative observations agree that the renderer root has no element children and no non-whitespace text. MUST carry a screenshot path, or a typed `screenshotError` marker when capture fails transiently. |
|
|
21
22
|
| `tampered` | Page-shim or isolated-world observation diverges from protocol authority. |
|
|
22
23
|
| `timeout` | The harness did not emit `render-complete` within `TIER1_RENDER_BARRIER_MS`. |
|
|
23
24
|
| `shim_only` | Isolated-world channel missing; only the untrusted page-shim produced usable counts. |
|
|
@@ -43,11 +44,12 @@ ordering, top to bottom:
|
|
|
43
44
|
3. `probe_only` / `shim_only` — channel availability (meta-claim about the channels, not the page).
|
|
44
45
|
4. `error` — discriminative errors are non-empty, or observed counts disagree with the lexical expectation. Both survive a structure change: a parse error is a parse error whenever it was observed, and a count that never matched the source's expectation never matches.
|
|
45
46
|
5. **`partial:unstable`** — TSX interactive mode; structure changed between the render barrier and the stability window.
|
|
46
|
-
6. `
|
|
47
|
-
7. `
|
|
48
|
-
8. `partial:
|
|
49
|
-
9. `partial:
|
|
50
|
-
10. `
|
|
47
|
+
6. `partial:empty_render` — TSX root emptiness, after instability and before other single-snapshot partials.
|
|
48
|
+
7. `error` (declared-opaque, observed-zero) — the artifact declared opaque regions and none were seen.
|
|
49
|
+
8. `partial:opaque_content` — single-snapshot claim: structure has opaque regions.
|
|
50
|
+
9. `partial:external_resources` — single-snapshot claim: structure has external HTTP references.
|
|
51
|
+
10. `partial:layout_unverified` — single-snapshot claim: visible SVG with zeroed viewBoxes.
|
|
52
|
+
11. `ok`.
|
|
51
53
|
|
|
52
54
|
The rule generalizing rows 4 and 6: a claim that depends on ONE observation
|
|
53
55
|
loses to `partial:unstable`; a claim that holds regardless of when it was
|
|
@@ -68,13 +70,21 @@ mode 0700 — the canonical secret-bearing layout matches the DB file
|
|
|
68
70
|
permissions.
|
|
69
71
|
|
|
70
72
|
Tier 1 capture happens AFTER the verdict is derived so the
|
|
71
|
-
`partial:layout_unverified`, `partial:opaque_content`, `partial:external_resources`, and `partial:
|
|
73
|
+
`partial:layout_unverified`, `partial:opaque_content`, `partial:external_resources`, `partial:unstable`, and `partial:empty_render` screenshot mandates are honored. The runner
|
|
72
74
|
measures the whole artifact, bounds each axis at 4096 pixels and the total at
|
|
73
75
|
8,388,608 pixels, and encodes evidence under the 8 MiB cap. New captures are
|
|
74
76
|
WebP; legacy PNG evidence remains readable and exportable. Before static
|
|
75
77
|
capture it emulates `prefers-reduced-motion: reduce` and awaits
|
|
76
78
|
`document.fonts.ready`; these pre-flights keep repeated captures byte-identical.
|
|
77
79
|
|
|
80
|
+
Tier 1 uses the same artifact-type layout rules as the gallery frame: Mermaid,
|
|
81
|
+
SVG, and chart roots are safely centered on both axes, with oversized content
|
|
82
|
+
remaining reachable by scrolling; Markdown is top-aligned in a horizontally
|
|
83
|
+
centered column capped at about 92ch; HTML and TSX use top-aligned document
|
|
84
|
+
flow, where a fixed-width layout can center itself with `mx-auto`. The renderer-parity gate
|
|
85
|
+
compares both the gallery and verifier renderer-module sets and CSS/stylesheet
|
|
86
|
+
sets.
|
|
87
|
+
|
|
78
88
|
Interactive TSX declares animated-capture eligibility; it does not imply that
|
|
79
89
|
the component is always visibly changing. CSS/Web Animations are probed, and
|
|
80
90
|
eligible captures may contain multiple WebP frames. Static artifacts retain a
|
|
@@ -90,8 +100,26 @@ browser or network-namespace failure records a Tier 1 `error` verdict with its
|
|
|
90
100
|
typed `tier1_*` code; it does not erase the Tier 0 verdict or turn visual
|
|
91
101
|
read-back into `revision_not_found`.
|
|
92
102
|
|
|
103
|
+
Tier 0 `ok` means the source passed structural checks; rendering is not
|
|
104
|
+
verified. The TTY presenter says `structure checked · rendering not verified`
|
|
105
|
+
and includes a visual read-back command. TSX Tier 0 reports `ok · compiled`.
|
|
106
|
+
The JSON envelope is unchanged. A malformed Mermaid fence returns `error` with
|
|
107
|
+
`mermaid_parse_error`, the parser message, and a zero-based `mermaid fence <i>`
|
|
108
|
+
location.
|
|
109
|
+
|
|
110
|
+
Tier 0 grammar-checks Mermaid fences but does not sanitize Mermaid label HTML;
|
|
111
|
+
it never renders a diagram. Sanitization happens in Tier 1, when Mermaid runs
|
|
112
|
+
in the real browser.
|
|
113
|
+
|
|
93
114
|
## Observed fields and renderer expectations
|
|
94
115
|
|
|
116
|
+
For Vega-Lite charts, Facet supplies 640×360 view defaults through
|
|
117
|
+
`config.view` only when the spec omits `autosize`, `config.view`, and both
|
|
118
|
+
top-level `width` and `height`. A supplied width or height is preserved while
|
|
119
|
+
the other dimension receives its default. An authored `config.view` or
|
|
120
|
+
`autosize` prevents defaulting. The composite-view defaults reach facet,
|
|
121
|
+
concat, and repeat children; author sizing is not replaced.
|
|
122
|
+
|
|
95
123
|
The canonical observed fields include `rendererRootSvgCount`, `graphCount`,
|
|
96
124
|
`mermaidNodeCount`, `visibleSvgCount`, `opaqueRegionCount`, `viewBoxes`,
|
|
97
125
|
`errorCount`, and `discriminativeErrors`. `opaqueRegionCount` counts DOM
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@legionworks/facet",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"artifacts",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"url": "git+https://github.com/legion-works/facet.git"
|
|
21
21
|
},
|
|
22
22
|
"bin": {
|
|
23
|
-
"facet": "
|
|
24
|
-
"facet-mcp": "
|
|
23
|
+
"facet": "src/cli/main.ts",
|
|
24
|
+
"facet-mcp": "src/harness-adapters/mcp/main.ts"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"src/**",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"build": "bun scripts/build-gallery.ts",
|
|
43
43
|
"prepack": "bun scripts/build-gallery.ts --if-stale",
|
|
44
44
|
"lint": "oxlint --deny-warnings",
|
|
45
|
-
"format": "
|
|
45
|
+
"format": "bun scripts/check-format.ts --write",
|
|
46
46
|
"format:check": "bun scripts/check-format.ts",
|
|
47
47
|
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.runtime-egress-fixture.json",
|
|
48
48
|
"test": "bun test",
|
package/skills/facet/SKILL.md
CHANGED
|
@@ -67,15 +67,33 @@ the detected evidence format:
|
|
|
67
67
|
|
|
68
68
|
Promotion is operator-only. The CLI discovers the token from
|
|
69
69
|
`FACET_PROMOTE_TOKEN`, then `FACET_HOME/secrets/promote.token`; never put a
|
|
70
|
-
token on argv.
|
|
70
|
+
token on argv. First request visual read-back for the exact revision, then
|
|
71
|
+
promote it. Tier 0 or publication success is not visual verification. The
|
|
72
|
+
`templates` command lists promoted templates and their source verdicts.
|
|
73
|
+
`--allow-unverified` records the refusal reason as `promotionOverride` for
|
|
74
|
+
audit; it does not create verification or turn a refusal into a passing verdict.
|
|
75
|
+
A Tier 1 verdict carrying `insecure.level === 3` is refused as
|
|
76
|
+
`insecure:unvalidated` even when its status would otherwise be allow-listed —
|
|
77
|
+
levels 1 and 2 are real validator runs and keep status-based treatment.
|
|
71
78
|
|
|
72
79
|
## Error quick reference
|
|
73
80
|
|
|
81
|
+
Render statuses are a closed set: `ok`, `error`, `partial:layout_unverified`,
|
|
82
|
+
`partial:opaque_content`, `partial:external_resources`, `partial:unstable`,
|
|
83
|
+
`partial:empty_render`, `tampered`, `timeout`, `shim_only`, `probe_only`, and
|
|
84
|
+
`insecure:unvalidated`.
|
|
85
|
+
|
|
86
|
+
Tier 0 `ok` means structure checked, not rendered. For visual evidence, run
|
|
87
|
+
`facet read-back --artifact-id <id> --tier visual`. TSX Tier 0 `ok` means
|
|
88
|
+
`ok · compiled`. Tier 1 TSX can return `partial:empty_render` when all
|
|
89
|
+
authoritative channels agree that the renderer root has no elements or
|
|
90
|
+
non-whitespace text; the verdict requires a screenshot.
|
|
91
|
+
|
|
74
92
|
| Error | Do next |
|
|
75
93
|
| ------------------------ | ---------------------------------------------------------------------------------------------------------------- |
|
|
76
94
|
| `invalid_request` | Correct the command flags, artifact type, or source bytes, then retry. |
|
|
77
95
|
| `artifact_not_found` | List or create the artifact, then use its returned ID. |
|
|
78
|
-
| `revision_not_found` |
|
|
96
|
+
| `revision_not_found` | Check that the artifact has a revision; omit the SHA for latest, or replace it with a returned revision SHA. |
|
|
79
97
|
| `duplicate_revision` | Use `error.details.revisionSha` as the existing revision SHA; do not republish identical bytes. |
|
|
80
98
|
| `evidence_unavailable` | Request Tier 1 evidence for that revision before a render export, or export source instead. |
|
|
81
99
|
| `output_unwritable` | Choose a writable `--out` path, resolve collisions, or use `--force` deliberately. |
|
|
@@ -94,6 +112,10 @@ token on argv.
|
|
|
94
112
|
or ask a human to inspect.
|
|
95
113
|
- Preserve stdout exactly so callers can parse the versioned envelope.
|
|
96
114
|
- With shell access, the CLI is the integration — MCP is for structured-tool-only hosts, even when the host owns an MCP client. On that surface, `facet_open_url` is safe because it always uses `--no-launch`.
|
|
115
|
+
- For MCP registration, use `bun add -g @legionworks/facet` and the bare
|
|
116
|
+
`facet-mcp` command, or the no-install fallback `npx -p @legionworks/facet
|
|
117
|
+
facet-mcp` (`bunx -p` also works). Use `1.10.0` or newer; earlier releases
|
|
118
|
+
ship a `facet-mcp` bin that cannot launch.
|
|
97
119
|
|
|
98
120
|
## Deeper reference
|
|
99
121
|
|
|
@@ -43,6 +43,7 @@ export function buildPromoteRequest(
|
|
|
43
43
|
revisionId,
|
|
44
44
|
name,
|
|
45
45
|
promotedBy,
|
|
46
|
+
...(args["allow-unverified"] === true ? { allowUnverified: true } : {}),
|
|
46
47
|
...(typeof artifactId === "string" ? { artifactId } : {}),
|
|
47
48
|
...(typeof description === "string" ? { description } : {}),
|
|
48
49
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { generateRequestId } from "../../shared/util/time";
|
|
2
|
+
import { FacetError } from "../../shared/errors/facet-error";
|
|
3
|
+
import { MAX_LIST_LIMIT } from "../../shared/config/limits";
|
|
4
|
+
import type { TemplatesRequest } from "../../shared/contracts/commands/requests";
|
|
5
|
+
|
|
6
|
+
export function buildTemplatesRequest(
|
|
7
|
+
args: Readonly<Record<string, string | boolean>>,
|
|
8
|
+
): TemplatesRequest {
|
|
9
|
+
const limit = args["limit"];
|
|
10
|
+
const limitNumber = typeof limit === "string" ? Number(limit) : undefined;
|
|
11
|
+
if (
|
|
12
|
+
limitNumber !== undefined &&
|
|
13
|
+
(!Number.isSafeInteger(limitNumber) || limitNumber < 1 || limitNumber > MAX_LIST_LIMIT)
|
|
14
|
+
) {
|
|
15
|
+
throw new FacetError(
|
|
16
|
+
"invalid_request",
|
|
17
|
+
`--limit must be an integer between 1 and ${MAX_LIST_LIMIT}`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
command: "templates",
|
|
22
|
+
requestId: generateRequestId(),
|
|
23
|
+
...(limitNumber === undefined ? {} : { limit: limitNumber }),
|
|
24
|
+
};
|
|
25
|
+
}
|
package/src/cli/main.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { FacetClient } from "./client";
|
|
|
37
37
|
|
|
38
38
|
import { buildCreateRequest } from "./commands/create";
|
|
39
39
|
import { buildListRequest } from "./commands/list";
|
|
40
|
+
import { buildTemplatesRequest } from "./commands/templates";
|
|
40
41
|
import { buildOpenRequest, launchDisplay } from "./commands/open";
|
|
41
42
|
import { buildPinRequest } from "./commands/pin";
|
|
42
43
|
import { buildPromoteRequest } from "./commands/promote";
|
|
@@ -168,6 +169,9 @@ async function executeVerb(
|
|
|
168
169
|
case "list":
|
|
169
170
|
request = buildListRequest(args);
|
|
170
171
|
break;
|
|
172
|
+
case "templates":
|
|
173
|
+
request = buildTemplatesRequest(args);
|
|
174
|
+
break;
|
|
171
175
|
case "readBack":
|
|
172
176
|
request = buildReadBackRequest(args);
|
|
173
177
|
break;
|
package/src/cli/parser.ts
CHANGED
|
@@ -77,6 +77,7 @@ const VERB_FLAGS: Readonly<Record<CommandName, readonly FlagDefinition[]>> = {
|
|
|
77
77
|
{ flag: "--slug-prefix", takesValue: true },
|
|
78
78
|
{ flag: "--limit", takesValue: true },
|
|
79
79
|
],
|
|
80
|
+
templates: [{ flag: "--limit", takesValue: true }],
|
|
80
81
|
readBack: [
|
|
81
82
|
{ flag: "--artifact-id", takesValue: true, required: true },
|
|
82
83
|
{ flag: "--revision-sha", takesValue: true },
|
|
@@ -97,6 +98,7 @@ const VERB_FLAGS: Readonly<Record<CommandName, readonly FlagDefinition[]>> = {
|
|
|
97
98
|
{ flag: "--name", takesValue: true, required: true },
|
|
98
99
|
{ flag: "--description", takesValue: true },
|
|
99
100
|
{ flag: "--promoted-by", takesValue: true, required: true },
|
|
101
|
+
{ flag: "--allow-unverified", takesValue: false },
|
|
100
102
|
],
|
|
101
103
|
instantiate: [
|
|
102
104
|
{ flag: "--name", takesValue: true, required: true },
|
|
@@ -121,6 +123,7 @@ const VERB_TO_COMMAND: Readonly<Record<string, CommandName>> = {
|
|
|
121
123
|
create: "create",
|
|
122
124
|
publish: "publish",
|
|
123
125
|
list: "list",
|
|
126
|
+
templates: "templates",
|
|
124
127
|
"read-back": "readBack",
|
|
125
128
|
status: "status",
|
|
126
129
|
open: "open",
|
|
@@ -134,6 +137,7 @@ const COMMAND_TO_VERB: Readonly<Record<CommandName, string>> = {
|
|
|
134
137
|
create: "create",
|
|
135
138
|
publish: "publish",
|
|
136
139
|
list: "list",
|
|
140
|
+
templates: "templates",
|
|
137
141
|
readBack: "read-back",
|
|
138
142
|
status: "status",
|
|
139
143
|
open: "open",
|
package/src/cli/presenter.ts
CHANGED
|
@@ -55,6 +55,7 @@ export const VERDICT_GLYPH: Record<RenderStatus, string> = {
|
|
|
55
55
|
"partial:opaque_content": "◐",
|
|
56
56
|
"partial:external_resources": "◐",
|
|
57
57
|
"partial:unstable": "◐",
|
|
58
|
+
"partial:empty_render": "◐",
|
|
58
59
|
tampered: "⊘",
|
|
59
60
|
timeout: "◌",
|
|
60
61
|
shim_only: "◇",
|
|
@@ -82,6 +83,7 @@ const STATUS_TONE: Record<RenderStatus, Tone> = {
|
|
|
82
83
|
"partial:opaque_content": "yellow",
|
|
83
84
|
"partial:external_resources": "yellow",
|
|
84
85
|
"partial:unstable": "yellow",
|
|
86
|
+
"partial:empty_render": "yellow",
|
|
85
87
|
tampered: "redInverse",
|
|
86
88
|
timeout: "dim",
|
|
87
89
|
shim_only: "dim",
|
|
@@ -101,6 +103,10 @@ function row(label: string, value: string): string {
|
|
|
101
103
|
return ` ${label.padEnd(10, " ")}${value}`;
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
function shortCell(value: string, width: number): string {
|
|
107
|
+
return value.length <= width ? value : `${value.slice(0, width - 1)}…`;
|
|
108
|
+
}
|
|
109
|
+
|
|
104
110
|
function verdictLines(verdict: Verdict, caps: PresenterCaps): string[] {
|
|
105
111
|
const paint = makePaint(caps);
|
|
106
112
|
const tone = STATUS_TONE[verdict.status];
|
|
@@ -108,6 +114,9 @@ function verdictLines(verdict: Verdict, caps: PresenterCaps): string[] {
|
|
|
108
114
|
const head = [
|
|
109
115
|
paint(tone, `${VERDICT_GLYPH[verdict.status]} ${word}`),
|
|
110
116
|
...(detail === undefined ? [] : [paint("dim", detail)]),
|
|
117
|
+
...(verdict.tier === 0 && verdict.status === "ok" && verdict.execution !== undefined
|
|
118
|
+
? [paint("dim", "compiled")]
|
|
119
|
+
: []),
|
|
111
120
|
paint("dim", `tier ${verdict.tier}`),
|
|
112
121
|
`${verdict.artifactId} ${paint("dim", "@")} ${paint("cyan", sha8(verdict.revisionSha))}`,
|
|
113
122
|
].join(paint("dim", " · "));
|
|
@@ -121,6 +130,12 @@ function verdictLines(verdict: Verdict, caps: PresenterCaps): string[] {
|
|
|
121
130
|
"observed",
|
|
122
131
|
`svg ${o.rendererRootSvgCount} · graphs ${o.graphCount} · nodes ${o.mermaidNodeCount} · errors ${o.errorCount}`,
|
|
123
132
|
),
|
|
133
|
+
...(verdict.tier === 0 && verdict.status === "ok"
|
|
134
|
+
? [
|
|
135
|
+
" structure checked · rendering not verified",
|
|
136
|
+
row("verify", `facet read-back --artifact-id ${verdict.artifactId} --tier visual`),
|
|
137
|
+
]
|
|
138
|
+
: []),
|
|
124
139
|
];
|
|
125
140
|
const first = o.discriminativeErrors?.[0];
|
|
126
141
|
if (first !== undefined)
|
|
@@ -163,6 +178,15 @@ export function presentEnvelope(envelope: FacetEnvelope<unknown>, caps: Presente
|
|
|
163
178
|
if (revision?.sha256 !== undefined) {
|
|
164
179
|
lines.push(row("read-back", `facet read-back --revision-sha ${revision.sha256}`));
|
|
165
180
|
}
|
|
181
|
+
const tier0 = data["verdict"];
|
|
182
|
+
if (
|
|
183
|
+
typeof tier0 === "object" &&
|
|
184
|
+
tier0 !== null &&
|
|
185
|
+
(tier0 as Verdict).tier === 0 &&
|
|
186
|
+
(tier0 as Verdict).status === "ok"
|
|
187
|
+
) {
|
|
188
|
+
lines.push(...verdictLines(tier0 as Verdict, caps));
|
|
189
|
+
}
|
|
166
190
|
const tier1 = data["tier1Verdict"];
|
|
167
191
|
if (typeof tier1 === "object" && tier1 !== null) {
|
|
168
192
|
lines.push(...verdictLines(tier1 as Verdict, caps));
|
|
@@ -188,6 +212,30 @@ export function presentEnvelope(envelope: FacetEnvelope<unknown>, caps: Presente
|
|
|
188
212
|
}
|
|
189
213
|
}
|
|
190
214
|
|
|
215
|
+
if (command === "templates") {
|
|
216
|
+
const templates = Array.isArray(data["templates"]) ? data["templates"] : [];
|
|
217
|
+
const lines = ["NAME · SHA · VERDICT · OVERRIDE · BY · DATE"];
|
|
218
|
+
for (const entry of templates) {
|
|
219
|
+
const item = entry as Record<string, unknown>;
|
|
220
|
+
const verdict = item["sourceVerdict"] as { status: RenderStatus; tier: number } | null;
|
|
221
|
+
lines.push(
|
|
222
|
+
[
|
|
223
|
+
shortCell(String(item["name"]), 12),
|
|
224
|
+
String(item["revisionSha"]).slice(0, 12),
|
|
225
|
+
verdict === null
|
|
226
|
+
? "—"
|
|
227
|
+
: `${VERDICT_GLYPH[verdict.status]} ${verdict.status} t${verdict.tier}`,
|
|
228
|
+
item["promotionOverride"] === null
|
|
229
|
+
? ""
|
|
230
|
+
: shortCell(String(item["promotionOverride"]), 12),
|
|
231
|
+
shortCell(String(item["promotedBy"]), 8),
|
|
232
|
+
String(item["promotedAt"]).slice(0, 10),
|
|
233
|
+
].join(" · "),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return lines;
|
|
237
|
+
}
|
|
238
|
+
|
|
191
239
|
if (command === "doctor") {
|
|
192
240
|
const probes = Array.isArray(data["probes"]) ? data["probes"] : [];
|
|
193
241
|
const lines: string[] = [];
|
package/src/gallery-web/app.ts
CHANGED
|
@@ -23,7 +23,12 @@
|
|
|
23
23
|
* surfaces an error badge.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
assertLoopbackHostname,
|
|
28
|
+
buildFrameAttributes,
|
|
29
|
+
usesArtifactStylesheet,
|
|
30
|
+
type FrameAttributes,
|
|
31
|
+
} from "./frame-html";
|
|
27
32
|
import { planSwap, type SwapPlanStep } from "./swap";
|
|
28
33
|
import { connectRevisionStream } from "./sse-client";
|
|
29
34
|
import type { VerdictObserved } from "../shared/contracts/validation";
|
|
@@ -376,6 +381,7 @@ export interface CreatedArtifactFrame {
|
|
|
376
381
|
payload: FrameRenderPayload,
|
|
377
382
|
timeoutMs: number,
|
|
378
383
|
) => Promise<FrameRenderResultHandle>;
|
|
384
|
+
readonly setTheme: (theme: ResolvedGalleryTheme) => void;
|
|
379
385
|
/** Handle from the last successful render (null before the first). */
|
|
380
386
|
readonly renderResult: FrameRenderResultHandle | null;
|
|
381
387
|
}
|
|
@@ -488,6 +494,14 @@ export function createArtifactFrame(options: CreateArtifactFrameOptions): Create
|
|
|
488
494
|
renderResult = handle;
|
|
489
495
|
return handle;
|
|
490
496
|
},
|
|
497
|
+
setTheme(theme) {
|
|
498
|
+
// oxlint-disable-next-line no-underscore-dangle
|
|
499
|
+
const api = raw.contentWindow?.__facetFrame;
|
|
500
|
+
if (api === null || api === undefined || typeof api.setTheme !== "function") {
|
|
501
|
+
throw new Error("frame theme API unavailable");
|
|
502
|
+
}
|
|
503
|
+
api.setTheme(theme);
|
|
504
|
+
},
|
|
491
505
|
get renderResult(): FrameRenderResultHandle | null {
|
|
492
506
|
return renderResult;
|
|
493
507
|
},
|
|
@@ -888,7 +902,9 @@ function setGalleryVerdict(document: Document, verdict: Verdict | null): void {
|
|
|
888
902
|
? "external"
|
|
889
903
|
: verdict.status === "partial:unstable"
|
|
890
904
|
? "unstable"
|
|
891
|
-
:
|
|
905
|
+
: verdict.status === "partial:empty_render"
|
|
906
|
+
? "empty"
|
|
907
|
+
: null;
|
|
892
908
|
const insecure = verdict.insecure === undefined ? null : `INSECURE L${verdict.insecure.level}`;
|
|
893
909
|
const suffix = insecure === null ? `T${verdict.tier}` : `${insecure} · T${verdict.tier}`;
|
|
894
910
|
tier.textContent = detail === null ? `· ${suffix}` : `· ${detail} · ${suffix}`;
|
|
@@ -1091,6 +1107,42 @@ export async function startGallery(runtime = browserGalleryRuntime()): Promise<v
|
|
|
1091
1107
|
let removeThemePreferenceListener: () => void = noOp;
|
|
1092
1108
|
let generation = 0;
|
|
1093
1109
|
let activeFrame: CreatedArtifactFrame | null = null;
|
|
1110
|
+
let removeInteractionListener: () => void = noOp;
|
|
1111
|
+
// Every path that leaves the displayed frame ends the signal: a stale flag would paint the
|
|
1112
|
+
// previous artifact's runtime error onto whatever the shell shows next.
|
|
1113
|
+
const endInteractionSignal = (): void => {
|
|
1114
|
+
removeInteractionListener();
|
|
1115
|
+
const badge = document.getElementById("facet-verdict");
|
|
1116
|
+
if (badge !== null) delete badge.dataset.interactionError;
|
|
1117
|
+
};
|
|
1118
|
+
const activateInteractionSignal = (frame: CreatedArtifactFrame): void => {
|
|
1119
|
+
endInteractionSignal();
|
|
1120
|
+
const badge = document.getElementById("facet-verdict");
|
|
1121
|
+
updateGalleryStatus("displayed");
|
|
1122
|
+
const frameWindow = (frame.element.raw as HTMLIFrameElement).contentWindow;
|
|
1123
|
+
if (frameWindow === null || typeof frameWindow.addEventListener !== "function") return;
|
|
1124
|
+
const markFailure = (): void => {
|
|
1125
|
+
if (expired || activeFrame !== frame || frame.renderResult === null) return;
|
|
1126
|
+
if (badge !== null) badge.dataset.interactionError = "true";
|
|
1127
|
+
updateGalleryStatus("displayed · runtime error during interaction");
|
|
1128
|
+
};
|
|
1129
|
+
frameWindow.addEventListener("error", markFailure, true);
|
|
1130
|
+
frameWindow.addEventListener("unhandledrejection", markFailure, true);
|
|
1131
|
+
const frameDocument = (frame.element.raw as HTMLIFrameElement).contentDocument;
|
|
1132
|
+
const observer =
|
|
1133
|
+
frameDocument == null || typeof MutationObserver !== "function"
|
|
1134
|
+
? null
|
|
1135
|
+
: new MutationObserver(() => {
|
|
1136
|
+
if (frameDocument.querySelector("[data-facet-error]")) markFailure();
|
|
1137
|
+
});
|
|
1138
|
+
if (frameDocument !== null)
|
|
1139
|
+
observer?.observe(frameDocument, { childList: true, subtree: true });
|
|
1140
|
+
removeInteractionListener = (): void => {
|
|
1141
|
+
frameWindow.removeEventListener("error", markFailure, true);
|
|
1142
|
+
frameWindow.removeEventListener("unhandledrejection", markFailure, true);
|
|
1143
|
+
observer?.disconnect();
|
|
1144
|
+
};
|
|
1145
|
+
};
|
|
1094
1146
|
const expireSession = (): void => {
|
|
1095
1147
|
if (expired) return;
|
|
1096
1148
|
generation += 1;
|
|
@@ -1100,6 +1152,7 @@ export async function startGallery(runtime = browserGalleryRuntime()): Promise<v
|
|
|
1100
1152
|
exportMenu?.sync();
|
|
1101
1153
|
activeFrame?.renderResult?.setGestureMode("native");
|
|
1102
1154
|
swaps?.close();
|
|
1155
|
+
endInteractionSignal();
|
|
1103
1156
|
removeThemePreferenceListener();
|
|
1104
1157
|
clearSession(window.sessionStorage);
|
|
1105
1158
|
renderSessionExpired(
|
|
@@ -1237,6 +1290,7 @@ export async function startGallery(runtime = browserGalleryRuntime()): Promise<v
|
|
|
1237
1290
|
updateGalleryFavicon(source.verdict?.status ?? "unverified");
|
|
1238
1291
|
updateGalleryVerdict(source.verdict ?? null);
|
|
1239
1292
|
updateGalleryStatus("displayed");
|
|
1293
|
+
activateInteractionSignal(current);
|
|
1240
1294
|
updateLiveState("live");
|
|
1241
1295
|
const themeToggle = document.getElementById("facet-theme-toggle");
|
|
1242
1296
|
const updateThemeToggle = (mode: GalleryThemeMode): void => {
|
|
@@ -1269,6 +1323,11 @@ export async function startGallery(runtime = browserGalleryRuntime()): Promise<v
|
|
|
1269
1323
|
const canCommit = (): boolean =>
|
|
1270
1324
|
themeGeneration === generation && nextMode === requestedThemeMode;
|
|
1271
1325
|
if (!canCommit()) return;
|
|
1326
|
+
if (usesArtifactStylesheet(source.artifactType)) {
|
|
1327
|
+
current.setTheme(nextResolvedTheme);
|
|
1328
|
+
commitTheme(nextMode, nextResolvedTheme);
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1272
1331
|
const next = createArtifactFrame({
|
|
1273
1332
|
artifactType: source.artifactType,
|
|
1274
1333
|
renderer: source.renderer,
|
|
@@ -1295,6 +1354,7 @@ export async function startGallery(runtime = browserGalleryRuntime()): Promise<v
|
|
|
1295
1354
|
if (!canCommit() || result.cancelled || result.failedNewFrameReady) return;
|
|
1296
1355
|
current = next;
|
|
1297
1356
|
activeFrame = next;
|
|
1357
|
+
activateInteractionSignal(next);
|
|
1298
1358
|
commitTheme(nextMode, nextResolvedTheme);
|
|
1299
1359
|
syncPanZoomToggle();
|
|
1300
1360
|
syncZoomButtons();
|
|
@@ -1373,6 +1433,7 @@ export async function startGallery(runtime = browserGalleryRuntime()): Promise<v
|
|
|
1373
1433
|
if (!result.failedNewFrameReady) {
|
|
1374
1434
|
current = frame;
|
|
1375
1435
|
activeFrame = frame;
|
|
1436
|
+
activateInteractionSignal(frame);
|
|
1376
1437
|
source = revision;
|
|
1377
1438
|
commitTheme(revisionThemeMode, revisionTheme);
|
|
1378
1439
|
syncPanZoomToggle();
|
|
@@ -1439,6 +1500,7 @@ export async function startGallery(runtime = browserGalleryRuntime()): Promise<v
|
|
|
1439
1500
|
updateSwapBar("start");
|
|
1440
1501
|
updateGalleryFavicon("unverified");
|
|
1441
1502
|
updateGalleryVerdict(null);
|
|
1503
|
+
endInteractionSignal();
|
|
1442
1504
|
swaps?.enqueue({ kind: "revision", ...event });
|
|
1443
1505
|
},
|
|
1444
1506
|
onClose: (event) => {
|
|
@@ -1472,6 +1534,7 @@ export async function startGallery(runtime = browserGalleryRuntime()): Promise<v
|
|
|
1472
1534
|
// releases the lease when the TTL fires, which is the only path
|
|
1473
1535
|
// that lets "refresh the tab" reach the same displayed canvas.
|
|
1474
1536
|
stream.close();
|
|
1537
|
+
removeInteractionListener();
|
|
1475
1538
|
removeThemePreferenceListener();
|
|
1476
1539
|
if (zoomButtonPoll !== undefined) clearInterval(zoomButtonPoll);
|
|
1477
1540
|
};
|