mockpit 1.3.0 → 3.0.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.
Files changed (77) hide show
  1. package/README.md +66 -202
  2. package/bin/demoData.js +5 -5
  3. package/bin/initDesign.js +6 -6
  4. package/bin/mockpit.js +687 -192
  5. package/bin/poster.js +377 -0
  6. package/dist/mcp/server.js +110 -19
  7. package/dist/server/answerAdditions.d.ts +7 -0
  8. package/dist/server/answerAdditions.js +76 -0
  9. package/dist/server/apiViews.d.ts +563 -41
  10. package/dist/server/apiViews.js +223 -74
  11. package/dist/server/app.d.ts +10 -2
  12. package/dist/server/app.js +3284 -782
  13. package/dist/server/appIcon.js +15 -5
  14. package/dist/server/appIconPng.js +3 -3
  15. package/dist/server/askNudges.js +1 -1
  16. package/dist/server/cdnInline.d.ts +8 -0
  17. package/dist/server/cdnInline.js +638 -0
  18. package/dist/server/codeWorker.js +2 -1
  19. package/dist/server/decisionList.d.ts +45 -0
  20. package/dist/server/decisionList.js +302 -0
  21. package/dist/server/decisions.d.ts +8 -8
  22. package/dist/server/decisions.js +18 -17
  23. package/dist/server/designGuide.d.ts +10 -6
  24. package/dist/server/designGuide.js +173 -78
  25. package/dist/server/embeds.d.ts +4 -1
  26. package/dist/server/embeds.js +57 -6
  27. package/dist/server/eventLog.d.ts +2 -2
  28. package/dist/server/eventLog.js +11 -11
  29. package/dist/server/events.d.ts +9 -6
  30. package/dist/server/feedbackBatch.d.ts +38 -16
  31. package/dist/server/feedbackBatch.js +121 -50
  32. package/dist/server/fills.d.ts +4 -0
  33. package/dist/server/fills.js +55 -1
  34. package/dist/server/guidance.d.ts +23 -0
  35. package/dist/server/guidance.js +71 -0
  36. package/dist/server/handoff.d.ts +4 -1
  37. package/dist/server/handoff.js +8 -2
  38. package/dist/server/icons.js +1 -1
  39. package/dist/server/index.js +49 -11
  40. package/dist/server/jump.d.ts +21 -0
  41. package/dist/server/jump.js +175 -0
  42. package/dist/server/knobs.d.ts +3 -3
  43. package/dist/server/knobs.js +6 -6
  44. package/dist/server/mcpHttp.d.ts +8 -1
  45. package/dist/server/mcpHttp.js +44 -7
  46. package/dist/server/mcpSpec.d.ts +2 -1
  47. package/dist/server/mcpSpec.js +172 -50
  48. package/dist/server/parts.js +9 -1
  49. package/dist/server/postSurfaces.js +3 -0
  50. package/dist/server/run.d.ts +1 -1
  51. package/dist/server/run.js +31 -17
  52. package/dist/server/runApi.d.ts +3 -3
  53. package/dist/server/runApi.js +34 -24
  54. package/dist/server/sqlStore.d.ts +117 -94
  55. package/dist/server/sqlStore.js +1089 -141
  56. package/dist/server/standings.d.ts +25 -0
  57. package/dist/server/standings.js +113 -0
  58. package/dist/server/surfacePage.d.ts +6 -9
  59. package/dist/server/surfacePage.js +361 -86
  60. package/dist/server/types.d.ts +252 -69
  61. package/dist/server/types.js +87 -69
  62. package/extensions/mockpit.js +245 -65
  63. package/guide/AGENT_SETUP.md +13 -1
  64. package/guide/packs/design-craft.md +34 -0
  65. package/guide/topics/asks.md +117 -69
  66. package/guide/topics/feedback.md +156 -119
  67. package/guide/topics/html.md +19 -32
  68. package/guide/topics/http.md +43 -40
  69. package/guide/topics/knobs.md +7 -10
  70. package/guide/topics/scripts.md +24 -35
  71. package/guide/topics/surfaces.md +50 -41
  72. package/opencode/mockpit.js +120 -0
  73. package/opencode/package.json +8 -0
  74. package/package.json +4 -6
  75. package/skills/launch-video/SKILL.md +12 -12
  76. package/skills/mockpit/SKILL.md +35 -23
  77. package/viewer/dist/index.html +9 -8
@@ -1,9 +1,7 @@
1
1
  # mockpit topic: html
2
2
 
3
- An `html` surface is a blank canvas. Custom SVG, bespoke layout, small
4
- interactions and animation are all fine. Below are the hard rules, the finish
5
- guardrails, and the vocabulary available inside the frame. `mockpit guide`
6
- prints this project's real palette, kit and icons.
3
+ An `html` surface is a blank canvas: custom SVG, bespoke layout, small interactions and
4
+ animation are all fine. `mockpit guide` prints this project's palette, kit, icons and rules.
7
5
 
8
6
  ## Contract
9
7
 
@@ -12,18 +10,16 @@ prints this project's real palette, kit and icons.
12
10
  - The stage renders your fragment in an 820px-wide frame, scaled to fit the
13
11
  user's window. Question pictures and Home thumbnails are the same document,
14
12
  shrunk. Height is measured from your content.
15
- - Keep content in normal flow. The frame measures the document box, so anything
16
- out of flow is invisible to the sizer and can leave the surface clipped or
17
- stuck at the wrong height.
13
+ - Keep content in normal flow: the frame measures the document box, so anything out of
14
+ flow can leave the surface clipped or at the wrong height.
18
15
  - Never use `position: fixed`.
19
16
  - Don't stack `position: absolute` layers over a box with a fixed `height` or
20
17
  `min-height`. The overlay grows `scrollHeight` but not the measured box.
21
18
  - To overlap elements, grid-stack them: `display: grid` on the container,
22
19
  `grid-area: 1 / 1` on each child. The container sizes to the tallest child.
23
20
  The `slides` kit does this.
24
- - Never hardcode a color. `color: #333` disappears in dark mode. Drive every
25
- color from the tokens below. Test: if the background were near-black, would
26
- everything still read?
21
+ - Never hardcode a color (`color: #333` disappears in dark mode). Drive every color from
22
+ the tokens below.
27
23
  - `<style>` and `<script>` are allowed. Scripts run in a sandboxed iframe with
28
24
  no access to the host page.
29
25
 
@@ -41,22 +37,16 @@ Mark the components you want feedback on with `data-part="name"` (optional
41
37
  apart and keep comments anchored across a rename). The viewer measures their
42
38
  boxes, so never hard-code geometry for it. Knob values arrive as unitless
43
39
  `--k-<path>` custom properties and `data-k-<path>` attributes on `<html>`; topic
44
- `knobs` has the details. Alternatives of one region are a slot: the same
40
+ `knobs` has the details. Alternatives of one region are a part with fills: the same
45
41
  `data-part` with a `data-fill` each; topic `asks`.
46
42
 
47
- The page must read before any web font or remote stylesheet loads: they are
48
- decoration, and a slow one delays the frame's layout.
43
+ The page must read before any web font or remote stylesheet loads; a slow one delays the
44
+ frame's layout.
49
45
 
50
- ## Finish
46
+ ## Scope
51
47
 
52
- These keep mocks feeling native to the viewer. They shape the finish, not the
53
- idea.
54
-
55
- - Flat: no gradients, drop shadows or decorative effects.
56
- - Sentence case for headings and labels. No emoji.
57
- - Two font weights: 400 and 500.
58
48
  - For diagrams, `<svg width="100%" viewBox="0 0 680 H">` with the classes below.
59
- - One concept per mock. Publish several small mocks with their own slugs rather
49
+ - One concept per design. Publish several small designs with their own slugs rather
60
50
  than one giant page.
61
51
 
62
52
  ## Theme tokens
@@ -142,26 +132,23 @@ The builtin, issues and slides kits also ship layout helpers (`.row`, `.stack`,
142
132
  `.num`, `.kbd`, `.hr`).
143
133
 
144
134
  ```sh
145
- mockpit publish --mock ci-status --html status.html --kit issues # repeat --kit for more
135
+ mockpit publish --design ci-status --html status.html --kit issues # repeat --kit for more
146
136
  ```
147
137
 
148
138
  ```js
149
- publish({ mock: "ci-status", surfaces: [{ kind: "html", html, kits: ["issues"] }] });
139
+ publish({ design: "ci-status", surfaces: [{ kind: "html", html, kits: ["issues"] }] });
150
140
  ```
151
141
 
152
142
  A kit only adds vocabulary. Write custom markup right beside kit classes.
153
143
 
154
144
  ## Tailwind projects
155
145
 
156
- When `mockpit init` found Tailwind v4, the sandbox compiles the repo's own
157
- Tailwind stylesheet with the browser build, so write the same utility classes
158
- you write in the repo, including theme classes (`bg-card`,
159
- `text-muted-foreground`). The browser build cannot load plugins or non-core
160
- imports, so init strips them; their utilities (`tw-animate-css`, typography)
161
- are not available, and the brief lists what was stripped. In dark mode the
162
- frame's `<html>` carries `class="dark"`, so the repo's `.dark` theme applies.
163
- A Tailwind v3 repo keeps its theme in `tailwind.config`, which the browser
164
- build cannot load: core utilities work, theme classes do not.
146
+ When `mockpit init` found Tailwind v4, the sandbox compiles the repo's own stylesheet with
147
+ the browser build, so write the repo's utility classes, including theme classes (`bg-card`,
148
+ `text-muted-foreground`). The build can't load plugins or non-core imports, so init strips
149
+ them and the brief lists what was stripped (`tw-animate-css`, typography). In dark mode
150
+ `<html>` carries `class="dark"`, so the repo's `.dark` theme applies. In a Tailwind v3 repo
151
+ core utilities work and theme classes do not, since `tailwind.config` can't load.
165
152
 
166
153
  ## Icons
167
154
 
@@ -1,61 +1,65 @@
1
1
  # mockpit topic: http
2
2
 
3
- Every verb works the same on the CLI, MCP and raw HTTP, with the same fields.
4
- None of them waits for the user.
5
-
6
- | CLI | MCP | HTTP |
7
- | ---------------------------- | ------------------------------------------- | ------------------------------------------------ |
8
- | `mockpit init` | none | `PUT /api/projects/:name/design` |
9
- | `mockpit publish` | `publish` | `POST /api/mocks` |
10
- | `mockpit ask` | `ask` | `POST /api/mocks/:id/asks` |
11
- | `mockpit read [slug]` | `read` | `GET /api/mocks?project=…`, `GET /api/mocks/:id` |
12
- | `mockpit feedback` | `feedback` | `GET /api/feedback?session=…` |
13
- | `mockpit say` | `say` | `POST /api/mocks/:id/say` |
14
- | `mockpit export` | `export` | `GET /api/mocks/:id/export` |
15
- | `mockpit upload` | `upload` | `POST /api/assets` |
16
- | `mockpit guide` | `guide` | `GET /agent-howto?project=…` |
17
- | `mockpit guide --topic <id>` | `guide({ topic })` | `GET /agent-howto?topic=<id>` |
18
- | `mockpit run <file>` | `run` on `/mcp?mode=code` (topic `scripts`) | `POST /api/run` |
19
- | `mockpit watch` | none | none: a CLI stream for background monitors |
20
-
21
- `:id` is the mock id or its slug (add `project` for a slug). Over HTTP the first
22
- publish creates a session. Pass its `sessionId` as `session` on every later
23
- call; otherwise each call starts a new session and the reply goes elsewhere.
24
- `GET /api/feedback` requires it.
3
+ Every verb takes the same fields on the CLI, MCP and raw HTTP. None waits for the user.
4
+
5
+ | CLI | MCP | HTTP |
6
+ | ------------------------------ | ------------------------------------------- | ------------------------------------------------------------------------------ |
7
+ | `mockpit init` | none | `PUT /api/projects/:name/design` |
8
+ | `mockpit publish` | `publish` | `POST /api/designs` |
9
+ | `mockpit ask` | `ask` | `POST /api/designs/:id/asks` |
10
+ | `mockpit read [slug]` | `read` | `GET /api/designs?project=…`, `GET /api/designs/:id` (`?body=1` adds the html) |
11
+ | `mockpit read --decisions` | `read({decisions: true})` | `GET /api/projects/:name/decisions` |
12
+ | `mockpit feedback` | `feedback` | `GET /api/feedback?session=…` |
13
+ | `mockpit say` | `say` | `POST /api/designs/:id/say` |
14
+ | `mockpit move` | `move` | `POST /api/designs/:id/move` |
15
+ | `mockpit edit --design <slug>` | `edit` | `PATCH /api/designs/:id` |
16
+ | `mockpit edit --notes <t>` | `edit({ notes })` | `PATCH /api/designs/:id` `{notes}` |
17
+ | `mockpit edit --rename <n>` | `edit({ rename })` | `PATCH /api/projects/:name` `{name}` |
18
+ | `mockpit edit --area-order` | `edit({ areaOrder })` | `PATCH /api/projects/:name` `{areaOrder}` |
19
+ | `mockpit export` | `export` | `GET /api/designs/:id/export` |
20
+ | `mockpit upload` | `upload` | `POST /api/assets` |
21
+ | `mockpit guide` | `guide` | `GET /agent-howto?project=…` |
22
+ | `mockpit guide --topic <id>` | `guide({ topic })` | `GET /agent-howto?topic=<id>` |
23
+ | `mockpit run <file>` | `run` on `/mcp?mode=code` (topic `scripts`) | `POST /api/run` |
24
+ | `mockpit watch` | none | none: a CLI stream for background monitors |
25
+
26
+ `:id` is the design id or its slug (add `project` for a slug). Over HTTP the first publish
27
+ creates a session: pass its `sessionId` as `session` on every later call, or each call starts
28
+ a new session and the reply goes elsewhere. `GET /api/feedback` requires it.
25
29
 
26
30
  ## Walkthrough
27
31
 
28
32
  ```sh
29
33
  B=http://localhost:8228
30
- curl -s -X POST $B/api/mocks -H 'content-type: application/json' -d '{
31
- "project": "demo", "mock": "checkout", "title": "Checkout", "state": "Empty cart",
34
+ curl -s -X POST $B/api/designs -H 'content-type: application/json' -d '{
35
+ "project": "demo", "design": "checkout", "title": "Checkout", "state": "Empty cart",
32
36
  "agent": "claude", "knobs": {"pad": [16, 8, 32, 2]}, "widths": [390, 1280],
33
- "note": "One column; the summary carries the empty state",
37
+ "description": "One column; the summary carries the empty state",
34
38
  "html": "<section data-part=\"summary\" data-part-label=\"Summary\" style=\"padding:calc(var(--k-pad,16)*1px)\">Your cart is empty</section>"}'
35
- # → {"mock":{…},"post":{…},"sessionId":"S","url":"…","parts":[{"state":"Empty cart","parts":[{"name":"summary","label":"Summary"}]}],"feedback":[]}
39
+ # → {"design":{…},"post":{…},"sessionId":"S","url":"…","parts":[{"state":"Empty cart","parts":[{"name":"summary","label":"Summary"}]}],"feedback":[]}
36
40
 
37
- # the same (mock, state, variant) again: its next version
38
- curl -s -X POST $B/api/mocks -H 'content-type: application/json' -d '{
39
- "session": "S", "project": "demo", "mock": "checkout", "state": "Empty cart",
41
+ # the same (design, state, variant) again: its next version
42
+ curl -s -X POST $B/api/designs -H 'content-type: application/json' -d '{
43
+ "session": "S", "project": "demo", "design": "checkout", "state": "Empty cart",
40
44
  "html": "<section data-part=\"total\" data-part-label=\"Summary\">Nothing here yet</section>"}'
41
45
  # → …,"partChanges":{"vanished":[],"renamed":[{"from":"summary","to":"total"}]}
42
46
 
43
- curl -s -X POST $B/api/mocks/checkout/asks -H 'content-type: application/json' -d '{
47
+ curl -s -X POST $B/api/designs/checkout/asks -H 'content-type: application/json' -d '{
44
48
  "session": "S", "project": "demo", "asks": [{"id": "pad", "text": "How roomy?", "scope": "part",
45
49
  "part": "total", "options": [{"label": "Tight", "set": {"pad": 8}}, {"label": "Roomy", "set": {"pad": 24}}]}]}'
46
50
 
47
51
  # tell the user where to look and end your turn; once they say they answered:
48
52
  curl -s "$B/api/feedback?session=S"
49
- # → {"feedback":[{"mock":"checkout","reply":{…},"accepted":[…],"archived":[]}],
50
- # "pending":[{"mock":"checkout","viewerOpen":true,"draft":null}]}
53
+ # → {"feedback":[{"design":"checkout","reply":{…},"messages":[],"inUse":[…],"archived":[]}],
54
+ # "pending":[{"design":"checkout","viewerOpen":true,"draft":null}]}
51
55
 
52
- curl -s -X POST $B/api/mocks/checkout/say -H 'content-type: application/json' -d '{
53
- "session": "S", "project": "demo", "message": "Accepted; wiring it up"}'
56
+ curl -s -X POST $B/api/designs/checkout/say -H 'content-type: application/json' -d '{
57
+ "session": "S", "project": "demo", "message": "In use; wiring it up"}'
54
58
 
55
- curl -s "$B/api/mocks/checkout/export?project=demo"
59
+ curl -s "$B/api/designs/checkout/export?project=demo"
56
60
  ```
57
61
 
58
- A publish that leaves two or more variants with no ask binding them adds
62
+ A publish that leaves two or more variants with no question binding them adds
59
63
  `nudges` and a ready `suggestedAsk`; POST it to `/asks` as `{"asks": [...]}`.
60
64
 
61
65
  ## Errors
@@ -63,7 +67,7 @@ A publish that leaves two or more variants with no ask binding them adds
63
67
  Every CLI command fails with one line plus an optional fix, exit code 2:
64
68
 
65
69
  ```
66
- error demo has no mock "writr"
70
+ error demo has no design "writr"
67
71
  fix: mockpit read --project demo
68
72
  ```
69
73
 
@@ -72,6 +76,5 @@ Nothing is written on a failed command, so a retry is safe. HTTP errors are
72
76
 
73
77
  ## Remote
74
78
 
75
- A deployed mockpit needs `MOCKPIT_URL` and `MOCKPIT_TOKEN` in your environment.
76
- The CLI and MCP server send the token for you. For curl, add
77
- `-H "Authorization: Bearer $MOCKPIT_TOKEN"`.
79
+ A deployed mockpit needs `MOCKPIT_URL` and `MOCKPIT_TOKEN` in your environment. The CLI and
80
+ MCP server send the token. For curl, add `-H "Authorization: Bearer $MOCKPIT_TOKEN"`.
@@ -1,21 +1,19 @@
1
1
  # mockpit topic: knobs
2
2
 
3
- A knob is a value the user tunes live on one render. Use one when one render
4
- plus a control shows the choice. A choice is several variants plus one ask
5
- that binds them; for that, publish variants and ask instead (topic `asks`).
6
- A 2–3 option knob is a choice: ask with options that `set` it. Publishing
7
- one returns that nudge. Declare knobs for what is worth fine-tuning (size,
8
- spacing, colour, density) even when the user did not ask; they wait in Tune.
3
+ A knob is a value the user tunes live on one render. For a choice between renders, publish
4
+ variants and ask (topic `asks`). A 2–3 option knob is a choice: ask with options that `set`
5
+ it; publishing one returns that nudge. Declare knobs for what is worth fine-tuning (size,
6
+ spacing, colour, density) even unasked; they wait in Tune.
9
7
 
10
8
  ## Declaring
11
9
 
12
10
  Knobs use tunekit's `usePane` shape, keyed by path. `"size"` is global,
13
- `"body.size"` belongs to the `body` part. Mock-wide knobs go in `--knobs` (MCP
11
+ `"body.size"` belongs to the `body` part. Design-wide knobs go in `--knobs` (MCP
14
12
  and HTTP `knobs`). Over MCP and HTTP, knobs only one variant has go in
15
13
  `variantKnobs`.
16
14
 
17
15
  ```sh
18
- mockpit publish --mock writer --state "Writing" --html w.html --knobs '{"body.size":[17,14,22,1]}'
16
+ mockpit publish --design writer --state "Writing" --html w.html --knobs '{"body.size":[17,14,22,1]}'
19
17
  ```
20
18
 
21
19
  | shape | control |
@@ -69,5 +67,4 @@ from script.
69
67
  <div class="trim-bottom" data-part="trim">Chapter 7</div>
70
68
  ```
71
69
 
72
- An ask option can set knob values too (`"set": {"trim.position": "bottom"}`),
73
- which turns a knob into a question. See topic `asks`.
70
+ Topic `asks`: an option's `"set": {"trim.position": "bottom"}` turns a knob into a question.
@@ -1,29 +1,23 @@
1
1
  # mockpit topic: scripts
2
2
 
3
- `run` executes a JavaScript script on the server against the mockpit API. One
4
- run can publish two variants and ask which one: three tool calls in one. A
5
- script never waits for the user. End the turn after the run that asks; when
6
- the user says they answered, a later run reads `mockpit.feedback()`.
3
+ `run` executes a JavaScript script on the server against the mockpit API: one run can
4
+ publish two variants and ask which. A script never waits for the user. End the turn after the
5
+ run that asks; once they say they answered, a later run reads `mockpit.feedback()`.
7
6
 
8
7
  ## When to use it
9
8
 
10
- - Your client loads every MCP tool into each conversation and has no codemode
11
- of its own (claude.ai, Desktop and ChatGPT connectors). Connect to
12
- `/mcp?mode=code` instead of `/mcp`: one `run` tool.
13
- - You work from a shell and want the whole loop in one file:
14
- `mockpit run loop.js`.
15
- - Skip it when your harness already turns MCP tools into code (pi, Cloudflare
16
- Agents). A script inside a script means double escaping; use the plain tools.
17
- - The Cloudflare Worker has no sandbox. There `run` answers that it is
18
- unavailable.
9
+ - Your client loads every MCP tool into each conversation and has no codemode of its own
10
+ (claude.ai, Desktop and ChatGPT connectors): connect to `/mcp?mode=code` for one `run` tool.
11
+ - You work from a shell and want the whole loop in one file: `mockpit run loop.js`.
12
+ - Skip it when your harness already turns MCP tools into code (pi, Cloudflare Agents).
13
+ - On the Cloudflare Worker, which has no sandbox, `run` answers that it is unavailable.
19
14
 
20
15
  | CLI | MCP | HTTP |
21
16
  | ----------------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------- |
22
17
  | `mockpit run <file>` (`-` is stdin) | `run` on `/mcp?mode=code`, or stdio with `MOCKPIT_MCP_MODE=code` (`path`) | `POST /api/run {code, session?, project?, agent?}` |
23
18
 
24
- The session, project and agent are fixed for a run. The result carries the
25
- `session`; pass it to the next run so its `feedback()` hears the reply. The CLI and the
26
- stdio server keep one session for you.
19
+ The session, project and agent are fixed for a run. The result carries the `session`; pass it
20
+ to the next run so its `feedback()` hears the reply. The CLI and stdio server keep one for you.
27
21
 
28
22
  ## The API
29
23
 
@@ -40,12 +34,12 @@ const card = (tone) => `
40
34
  <h1>Writer</h1>
41
35
  <button data-part="cta">Start writing</button>
42
36
  </section>`;
43
- await mockpit.publish({ mock: "writer", title: "Writer", variant: "calm", html: card("calm") });
44
- await mockpit.publish({ mock: "writer", variant: "bold", html: card("bold") });
37
+ await mockpit.publish({ design: "writer", title: "Writer", variant: "calm", html: card("calm") });
38
+ await mockpit.publish({ design: "writer", variant: "bold", html: card("bold") });
45
39
  const { url } = await mockpit.ask("writer", [
46
40
  {
47
- id: "look",
48
- text: "Which look?",
41
+ id: "tone",
42
+ text: "Calm or bold?",
49
43
  options: [
50
44
  { label: "Calm", variant: "calm" },
51
45
  { label: "Bold", variant: "bold" },
@@ -55,8 +49,7 @@ const { url } = await mockpit.ask("writer", [
55
49
  return url;
56
50
  ```
57
51
 
58
- Tell the user in one line where to look, then end your turn. Once they say
59
- they answered:
52
+ Tell the user where to look and end your turn. Once they say they answered:
60
53
 
61
54
  ```js
62
55
  const { feedback, pending } = await mockpit.feedback();
@@ -80,32 +73,29 @@ return feedback.length ? feedback : pending;
80
73
  }
81
74
  ```
82
75
 
83
- - `calls` lists every host call in order. After a failure it tells you which
84
- writes landed. A rerun publishes new versions, so check it first.
85
- - `feedback` holds every batch the run received, from `feedback()` and from
86
- the feedback that rides on writes. It is there even when the script threw or
87
- ran out of time. Feedback is delivered once: read it here, not from a later
88
- call.
76
+ - `calls` lists every host call in order. After a failure it shows which writes landed; a
77
+ rerun publishes new versions, so check it first.
78
+ - `feedback` holds every batch the run received, from `feedback()` and from writes, even when
79
+ the script threw or timed out. It is delivered once: read it here.
89
80
  - `error.kind` is `script` (a throw or a syntax error, with `line` and
90
81
  `column`), `timeout`, `limit` (CPU, memory, code size, call count) or
91
82
  `aborted` (the caller went away).
92
83
 
93
84
  ## Errors
94
85
 
95
- A host call that fails rejects with the message REST would give, so
96
- `try`/`catch` works:
86
+ A failed host call rejects with REST's message, so `try`/`catch` works:
97
87
 
98
88
  ```js
99
89
  try {
100
90
  await mockpit.publish({
101
- mock: "writer",
91
+ design: "writer",
102
92
  variant: "calm",
103
93
  parts: { hero: '<section data-part="hero">…</section>' },
104
94
  });
105
95
  } catch (e) {
106
96
  print("part splice failed:", e.message);
107
97
  await mockpit.publish({
108
- mock: "writer",
98
+ design: "writer",
109
99
  variant: "calm",
110
100
  html: '<section data-part="hero">…</section>',
111
101
  });
@@ -124,6 +114,5 @@ try {
124
114
  | Host calls | 100 per run, 4 in flight |
125
115
  | Runs | 4 at a time per server |
126
116
 
127
- No network, timers, imports, `require` or `process` exist in the sandbox. The
128
- only way out is `mockpit.*`, and it can do what your token can over REST, no
129
- more.
117
+ The sandbox has no network, timers, imports, `require` or `process`. The only way out is
118
+ `mockpit.*`, with what your token can do over REST.
@@ -5,6 +5,7 @@ A variant version is an ordered list of surfaces. Each has a `kind`:
5
5
  ```
6
6
  { "kind": "html", "html": "<p>...</p>" }
7
7
  { "kind": "html", "html": "<ul class=\"tree\">…</ul>", "kits": ["issues"] }
8
+ { "kind": "html", "html": "<canvas>…</canvas>", "poster": "<id from an upload>" }
8
9
  { "kind": "markdown", "markdown": "## Plan\n\n1. …" }
9
10
  { "kind": "mermaid", "mermaid": "flowchart TD; A[Start] --> B{Ok?}" }
10
11
  { "kind": "diff", "patch": "<unified or git diff text>", "layout": "split" }
@@ -20,10 +21,8 @@ On the CLI: `--html`, `--md`, `--diff`, `--mermaid`, `--terminal`, `--code`,
20
21
 
21
22
  ## Picking a kind
22
23
 
23
- Pick by what the thing is:
24
-
25
- - `html`: you are drawing. UI, diagrams, data viz, anything interactive. Design
26
- mocks use this. Topic `html` has its contract.
24
+ - `html`: you are drawing: UI, diagrams, data viz, anything interactive. Topic `html` has its
25
+ contract.
27
26
  - `markdown`: prose, plans, tradeoffs. Tag fenced code to highlight it.
28
27
  `![caption](/a/<id>)` embeds an uploaded image. Raw HTML in the source is
29
28
  escaped.
@@ -47,40 +46,33 @@ from data, so send data, never markup.
47
46
 
48
47
  ## Composition: embeds
49
48
 
50
- Any mock can embed another mock of the project in its html with
51
- `<mockpit-embed slug="button"></mockpit-embed>` (`<mockpit-slot>` still works;
52
- a slot is now a region with fills, topic `asks`). The server inlines that
53
- mock's html body in place, inside the embedding frame, and expands the
54
- component's own embeds in turn (up to 6 levels), so build a page from
55
- sections and sections from components, each its own mock with its own
56
- variants and questions.
57
-
58
- - No `version`: the embed follows the component's version in use once the user
59
- accepted one, else its latest, so a new version of the button shows in every
60
- page that uses it without republishing them. `version="3"` pins it.
61
- - No `variant`: the component's accepted variant (else `default`). Name one to
49
+ Any design can embed another of the project in its html with
50
+ `<mockpit-embed slug="button"></mockpit-embed>`. The server inlines that design's html body in
51
+ place and expands its own embeds (up to 6 levels), so build a screen from sections and sections
52
+ from components, each its own design with its own variants and questions.
53
+
54
+ - No `version`: follows the component's version in use, else its latest, without republishing
55
+ what embeds it. `version="3"` pins it.
56
+ - No `variant`: the component's variant in use (else `default`). Name one to
62
57
  show a specific variant.
63
- - The component shows as decided: its slots keep only the fills the user
58
+ - The component shows as decided: its parts with fills keep only the fills the user
64
59
  settled on, and its other settled knob values ride on the embed's wrapper
65
60
  (`data-k-<path>` and `--k-<path>`), so write its knob CSS as
66
61
  `[data-k-tone="loud"] .chip`, not `html[data-k-tone=…]`.
67
62
  - `read` lists each variant's `embeds` as they render now (nested included,
68
63
  with every version, for the user to peek back) and `publishedWith`, the
69
64
  versions it rendered with when you published it.
70
- - A chain that loops back on itself is refused at publish. An unknown slug
71
- renders as an empty placeholder, not silently dropped.
65
+ - A chain that loops back is refused at publish. An unknown slug renders as an empty placeholder.
72
66
 
73
67
  ## Editing one surface
74
68
 
75
- `publish` with `surfaces` takes the full ordered list, so one call adds, edits,
76
- removes and reorders. Every write returns each surface's `id`
77
- (`post.surfaces`). In the next list, an entry that is only `{ "id": "…" }`
78
- keeps that surface unchanged, an `id` with content replaces it in place, an
79
- entry without an `id` is new, an id you leave out is removed, and the list
80
- order is the new order:
69
+ `publish` with `surfaces` takes the full ordered list, so one call adds, edits, removes and
70
+ reorders. Every write returns each surface's `id` (`post.surfaces`). In the next list: `{ "id": "…" }`
71
+ alone keeps a surface, an `id` with content replaces it, no `id` is new, a missing id is removed,
72
+ and list order is the new order:
81
73
 
82
74
  ```json
83
- { "mock": "writer", "surfaces": [{ "kind": "markdown", "markdown": "## Why" }, { "id": "s1" }] }
75
+ { "design": "writer", "surfaces": [{ "kind": "markdown", "markdown": "## Why" }, { "id": "s1" }] }
84
76
  ```
85
77
 
86
78
  An unknown id fails with the surfaces present and writes nothing.
@@ -96,21 +88,38 @@ POST /api/assets (raw) Content-Type: image/png <bytes> ?filename=shot.pn
96
88
  POST /api/assets (json) { "data": "<base64>", "contentType": "image/png", "filename": "shot.png" }
97
89
  ```
98
90
 
99
- An asset's id is the SHA-256 of its bytes. You can derive the URL locally
100
- (`mockpit asset-url shot.png`) and write `<img src="/a/<hash>">` before the
101
- upload finishes; the viewer waits briefly for an in-flight asset. Identical
102
- bytes dedupe. An asset lives as long as something references it. The limit is
103
- 5 MB per asset.
91
+ An asset's id is the SHA-256 of its bytes, so identical bytes dedupe. You can derive the URL
92
+ locally (`mockpit asset-url shot.png`) and write `<img src="/a/<hash>">` before the upload
93
+ finishes; the viewer waits briefly for an in-flight asset. An asset lives as long as something
94
+ references it. Limit: 5 MB per asset.
104
95
 
105
- ## What the user sees
96
+ ## Posters
97
+
98
+ A heavy page (3D, WebGL, a running canvas) costs a live frame in every overview card, and a
99
+ browser holds only about 16. A still as the html's `poster` fixes that: Studio and Home show the
100
+ picture, Stage still runs the page.
101
+
102
+ The CLI and the stdio MCP server take one for you, with a local Chrome or Chromium, just after
103
+ each publish that doesn't send its own. It arrives seconds later and the publish doesn't wait for
104
+ it. With no browser there is none, and the page runs live as before. Send your own to choose the
105
+ moment; yours always wins. Over raw HTTP and `/mcp`, only yours exists:
106
106
 
107
- Each thing you add lights up its part of the viewer.
107
+ ```
108
+ CLI mockpit publish --design onni --variant felt --html felt.html --poster felt.png
109
+ MCP publish { design, variant, html, poster: "<asset id, or an image path on stdio>" }
110
+ HTTP POST /api/designs { …, "html": "…", "poster": "<asset id>" }
111
+ ```
112
+
113
+ A version that changes the page without a new poster (`--parts`, `--fills`, a fork with edits)
114
+ drops it, so send one with each, or let the CLI or stdio server take the new one.
115
+
116
+ ## What the user sees
108
117
 
109
- | you publish | the user gets |
110
- | ---------------------- | ---------------------------------------------------- |
111
- | a plain mock, any kind | the stage and Thread, with Accept / Revise / Drop |
112
- | several variants | a switcher, plus a built-in Look ask until you ask |
113
- | asks | Questions: picture options on the stage, then Send |
114
- | `data-part` | Tune's component list and comments anchored on parts |
115
- | `knobs` | Tune's controls, live on the stage |
116
- | several states | the state strip under the stage |
118
+ | you publish | the user gets |
119
+ | ------------------------ | ---------------------------------------------------- |
120
+ | a plain design, any kind | the stage and Thread, with Use this / New version |
121
+ | several variants | a switcher, plus a built-in variant question |
122
+ | questions (`ask`) | Questions: picture options on the stage, then Send |
123
+ | `data-part` | Tune's component list and comments anchored on parts |
124
+ | `knobs` | Tune's controls, live on the stage |
125
+ | several states | the state tabs above the stage |
@@ -0,0 +1,120 @@
1
+ // Waits for a Send on each mockpit session this OpenCode process has used, and
2
+ // wakes the OpenCode session that last called it. Nothing runs before a mockpit call.
3
+ // One default export serves both OpenCode lines: 1.x calls server() and 2.x setup(),
4
+ // and 2.x doesn't run 1.x plugins. No imports, so the package has no dependencies.
5
+
6
+ const BASE = (process.env.MOCKPIT_URL || "http://localhost:8228").replace(/\/+$/, "");
7
+ const HEADERS = {
8
+ "x-mockpit-client": "opencode",
9
+ ...(process.env.MOCKPIT_TOKEN ? { authorization: `Bearer ${process.env.MOCKPIT_TOKEN}` } : {}),
10
+ };
11
+
12
+ const pause = (ms, signal) =>
13
+ new Promise((resolve) => {
14
+ const timer = setTimeout(resolve, ms);
15
+ signal.addEventListener("abort", () => (clearTimeout(timer), resolve()), { once: true });
16
+ });
17
+
18
+ // A shell command, or 2.x's code tool, which is how it calls MCP tools, names mockpit
19
+ // only in its arguments.
20
+ const isMockpitCall = (tool, args) =>
21
+ /mockpit/i.test(tool) ||
22
+ (/^(bash|shell|execute)$/i.test(tool) && /\bmockpit/.test(JSON.stringify(args ?? "")));
23
+
24
+ // A write's result names its session. The rest don't, and the MCP server records the
25
+ // directory it runs in, so the newest session there is this process's own.
26
+ async function mockpitSession(output, directory) {
27
+ const named = /\\?"sessionId\\?"\s*:\s*\\?"([^"\\]+)/.exec(output ?? "")?.[1];
28
+ if (named) return named;
29
+ const res = await fetch(`${BASE}/api/sessions`, { headers: HEADERS });
30
+ if (!res.ok) return undefined;
31
+ const newest = (await res.json())
32
+ .filter((s) => s.cwd === directory)
33
+ .sort((a, b) => String(b.lastActiveAt).localeCompare(String(a.lastActiveAt)))[0];
34
+ return newest?.id;
35
+ }
36
+
37
+ const wakeText = (batches) =>
38
+ `The user replied in the mockpit viewer on ${[...new Set(batches.map((b) => b.design ?? "a design"))].join(", ")}. Call mockpit's feedback for the reply.`;
39
+
40
+ function createWaker(wake) {
41
+ const callers = new Map();
42
+ const stop = new AbortController();
43
+
44
+ async function wait(session) {
45
+ while (!stop.signal.aborted) {
46
+ try {
47
+ const query = new URLSearchParams({ session, author: "user", wait: "60", peek: "1" });
48
+ const res = await fetch(`${BASE}/api/comments?${query}`, {
49
+ headers: HEADERS,
50
+ signal: stop.signal,
51
+ });
52
+ if (!res.ok) {
53
+ await pause(2000, stop.signal);
54
+ continue;
55
+ }
56
+ const batches = (await res.json()).feedback ?? [];
57
+ if (batches.length === 0 || stop.signal.aborted) continue;
58
+ // The reply stays on the session's cursor, so a wake that fails loses only the
59
+ // wake: feedback or the next write still delivers it.
60
+ await wake(callers.get(session), wakeText(batches));
61
+ } catch {
62
+ await pause(2000, stop.signal);
63
+ }
64
+ }
65
+ }
66
+
67
+ return {
68
+ async noteCall({ tool, args, output, sessionID, directory }) {
69
+ if (!isMockpitCall(tool, args)) return;
70
+ const session = await mockpitSession(output, directory).catch(() => undefined);
71
+ if (!session) return;
72
+ const known = callers.has(session);
73
+ callers.set(session, sessionID);
74
+ if (!known) void wait(session);
75
+ },
76
+ stop: () => stop.abort(),
77
+ };
78
+ }
79
+
80
+ const text = (result) => (typeof result === "string" ? result : JSON.stringify(result ?? ""));
81
+
82
+ export default {
83
+ id: "mockpit",
84
+
85
+ server: async ({ client, directory }) => {
86
+ const waker = createWaker(async (sessionID, message) => {
87
+ await client.session.promptAsync({
88
+ path: { id: sessionID },
89
+ body: { parts: [{ type: "text", text: message }] },
90
+ });
91
+ });
92
+ return {
93
+ "tool.execute.after": async (input, output) => {
94
+ await waker.noteCall({
95
+ tool: input.tool,
96
+ args: input.args,
97
+ output: output?.output,
98
+ sessionID: input.sessionID,
99
+ directory,
100
+ });
101
+ },
102
+ };
103
+ },
104
+
105
+ setup: async (ctx) => {
106
+ const waker = createWaker(async (sessionID, message) => {
107
+ await ctx.session.synthetic({ sessionID, text: message, description: "mockpit reply" });
108
+ });
109
+ await ctx.tool.hook("execute.after", async (event) => {
110
+ await waker.noteCall({
111
+ tool: event.tool,
112
+ args: event.input,
113
+ output: event.status === "completed" ? text(event.result) : undefined,
114
+ sessionID: event.sessionID,
115
+ directory: process.cwd(),
116
+ });
117
+ });
118
+ return () => waker.stop();
119
+ },
120
+ };