mockpit 2.0.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 (73) hide show
  1. package/README.md +60 -212
  2. package/bin/demoData.js +2 -2
  3. package/bin/initDesign.js +6 -6
  4. package/bin/mockpit.js +407 -187
  5. package/bin/poster.js +377 -0
  6. package/dist/mcp/server.js +80 -24
  7. package/dist/server/answerAdditions.d.ts +7 -0
  8. package/dist/server/answerAdditions.js +76 -0
  9. package/dist/server/apiViews.d.ts +289 -52
  10. package/dist/server/apiViews.js +189 -81
  11. package/dist/server/app.d.ts +7 -2
  12. package/dist/server/app.js +2845 -832
  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 +1 -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 +4 -4
  22. package/dist/server/decisions.js +9 -9
  23. package/dist/server/designGuide.d.ts +5 -5
  24. package/dist/server/designGuide.js +128 -67
  25. package/dist/server/embeds.d.ts +3 -1
  26. package/dist/server/embeds.js +31 -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 +35 -13
  31. package/dist/server/feedbackBatch.js +111 -47
  32. package/dist/server/fills.d.ts +4 -0
  33. package/dist/server/fills.js +55 -1
  34. package/dist/server/handoff.d.ts +4 -1
  35. package/dist/server/handoff.js +8 -2
  36. package/dist/server/icons.js +1 -1
  37. package/dist/server/index.js +39 -10
  38. package/dist/server/jump.d.ts +21 -0
  39. package/dist/server/jump.js +175 -0
  40. package/dist/server/knobs.d.ts +3 -3
  41. package/dist/server/knobs.js +6 -6
  42. package/dist/server/mcpHttp.d.ts +5 -1
  43. package/dist/server/mcpHttp.js +16 -11
  44. package/dist/server/mcpSpec.js +118 -46
  45. package/dist/server/parts.js +9 -1
  46. package/dist/server/run.d.ts +1 -1
  47. package/dist/server/run.js +25 -16
  48. package/dist/server/runApi.d.ts +3 -3
  49. package/dist/server/runApi.js +25 -19
  50. package/dist/server/sqlStore.d.ts +112 -96
  51. package/dist/server/sqlStore.js +906 -172
  52. package/dist/server/standings.d.ts +25 -0
  53. package/dist/server/standings.js +113 -0
  54. package/dist/server/surfacePage.d.ts +6 -9
  55. package/dist/server/surfacePage.js +361 -86
  56. package/dist/server/types.d.ts +226 -72
  57. package/dist/server/types.js +73 -78
  58. package/extensions/mockpit.js +89 -72
  59. package/guide/AGENT_SETUP.md +13 -1
  60. package/guide/packs/design-craft.md +2 -3
  61. package/guide/topics/asks.md +106 -62
  62. package/guide/topics/feedback.md +126 -119
  63. package/guide/topics/html.md +17 -24
  64. package/guide/topics/http.md +41 -41
  65. package/guide/topics/knobs.md +7 -10
  66. package/guide/topics/scripts.md +22 -33
  67. package/guide/topics/surfaces.md +39 -48
  68. package/opencode/mockpit.js +120 -0
  69. package/opencode/package.json +8 -0
  70. package/package.json +4 -3
  71. package/skills/launch-video/SKILL.md +11 -11
  72. package/skills/mockpit/SKILL.md +26 -22
  73. package/viewer/dist/index.html +9 -9
@@ -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 and the
5
- vocabulary available inside the frame; how a design looks is the project's
6
- call. `mockpit guide` prints this project's real palette, kit, icons and rules.
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 @@ call. `mockpit guide` prints this project's real palette, kit, icons and rules.
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
 
@@ -44,13 +40,13 @@ boxes, so never hard-code geometry for it. Knob values arrive as unitless
44
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
46
  ## Scope
51
47
 
52
48
  - For diagrams, `<svg width="100%" viewBox="0 0 680 H">` with the classes below.
53
- - 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
54
50
  than one giant page.
55
51
 
56
52
  ## Theme tokens
@@ -136,26 +132,23 @@ The builtin, issues and slides kits also ship layout helpers (`.row`, `.stack`,
136
132
  `.num`, `.kbd`, `.hr`).
137
133
 
138
134
  ```sh
139
- 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
140
136
  ```
141
137
 
142
138
  ```js
143
- publish({ mock: "ci-status", surfaces: [{ kind: "html", html, kits: ["issues"] }] });
139
+ publish({ design: "ci-status", surfaces: [{ kind: "html", html, kits: ["issues"] }] });
144
140
  ```
145
141
 
146
142
  A kit only adds vocabulary. Write custom markup right beside kit classes.
147
143
 
148
144
  ## Tailwind projects
149
145
 
150
- When `mockpit init` found Tailwind v4, the sandbox compiles the repo's own
151
- Tailwind stylesheet with the browser build, so write the same utility classes
152
- you write in the repo, including theme classes (`bg-card`,
153
- `text-muted-foreground`). The browser build cannot load plugins or non-core
154
- imports, so init strips them; their utilities (`tw-animate-css`, typography)
155
- are not available, and the brief lists what was stripped. In dark mode the
156
- frame's `<html>` carries `class="dark"`, so the repo's `.dark` theme applies.
157
- A Tailwind v3 repo keeps its theme in `tailwind.config`, which the browser
158
- 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.
159
152
 
160
153
  ## Icons
161
154
 
@@ -1,61 +1,62 @@
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 edit --mock <slug>` | `edit` | `PATCH /api/mocks/:id` |
15
- | `mockpit edit --rename <n>` | `edit({ rename })` | `PATCH /api/projects/:name` `{name}` |
16
- | `mockpit edit --area-order` | `edit({ areaOrder })` | `PATCH /api/projects/:name` `{areaOrder}` |
17
- | `mockpit export` | `export` | `GET /api/mocks/:id/export` |
18
- | `mockpit upload` | `upload` | `POST /api/assets` |
19
- | `mockpit guide` | `guide` | `GET /agent-howto?project=…` |
20
- | `mockpit guide --topic <id>` | `guide({ topic })` | `GET /agent-howto?topic=<id>` |
21
- | `mockpit run <file>` | `run` on `/mcp?mode=code` (topic `scripts`) | `POST /api/run` |
22
- | `mockpit watch` | none | none: a CLI stream for background monitors |
23
-
24
- `:id` is the mock id or its slug (add `project` for a slug). Over HTTP the first
25
- publish creates a session. Pass its `sessionId` as `session` on every later
26
- call; otherwise each call starts a new session and the reply goes elsewhere.
27
- `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.
28
29
 
29
30
  ## Walkthrough
30
31
 
31
32
  ```sh
32
33
  B=http://localhost:8228
33
- curl -s -X POST $B/api/mocks -H 'content-type: application/json' -d '{
34
- "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",
35
36
  "agent": "claude", "knobs": {"pad": [16, 8, 32, 2]}, "widths": [390, 1280],
36
- "note": "One column; the summary carries the empty state",
37
+ "description": "One column; the summary carries the empty state",
37
38
  "html": "<section data-part=\"summary\" data-part-label=\"Summary\" style=\"padding:calc(var(--k-pad,16)*1px)\">Your cart is empty</section>"}'
38
- # → {"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":[]}
39
40
 
40
- # the same (mock, state, variant) again: its next version
41
- curl -s -X POST $B/api/mocks -H 'content-type: application/json' -d '{
42
- "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",
43
44
  "html": "<section data-part=\"total\" data-part-label=\"Summary\">Nothing here yet</section>"}'
44
45
  # → …,"partChanges":{"vanished":[],"renamed":[{"from":"summary","to":"total"}]}
45
46
 
46
- 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 '{
47
48
  "session": "S", "project": "demo", "asks": [{"id": "pad", "text": "How roomy?", "scope": "part",
48
49
  "part": "total", "options": [{"label": "Tight", "set": {"pad": 8}}, {"label": "Roomy", "set": {"pad": 24}}]}]}'
49
50
 
50
51
  # tell the user where to look and end your turn; once they say they answered:
51
52
  curl -s "$B/api/feedback?session=S"
52
- # → {"feedback":[{"mock":"checkout","reply":{…},"messages":[],"inUse":[…],"archived":[]}],
53
- # "pending":[{"mock":"checkout","viewerOpen":true,"draft":null}]}
53
+ # → {"feedback":[{"design":"checkout","reply":{…},"messages":[],"inUse":[…],"archived":[]}],
54
+ # "pending":[{"design":"checkout","viewerOpen":true,"draft":null}]}
54
55
 
55
- curl -s -X POST $B/api/mocks/checkout/say -H 'content-type: application/json' -d '{
56
+ curl -s -X POST $B/api/designs/checkout/say -H 'content-type: application/json' -d '{
56
57
  "session": "S", "project": "demo", "message": "In use; wiring it up"}'
57
58
 
58
- curl -s "$B/api/mocks/checkout/export?project=demo"
59
+ curl -s "$B/api/designs/checkout/export?project=demo"
59
60
  ```
60
61
 
61
62
  A publish that leaves two or more variants with no question binding them adds
@@ -66,7 +67,7 @@ A publish that leaves two or more variants with no question binding them adds
66
67
  Every CLI command fails with one line plus an optional fix, exit code 2:
67
68
 
68
69
  ```
69
- error demo has no mock "writr"
70
+ error demo has no design "writr"
70
71
  fix: mockpit read --project demo
71
72
  ```
72
73
 
@@ -75,6 +76,5 @@ Nothing is written on a failed command, so a retry is safe. HTTP errors are
75
76
 
76
77
  ## Remote
77
78
 
78
- A deployed mockpit needs `MOCKPIT_URL` and `MOCKPIT_TOKEN` in your environment.
79
- The CLI and MCP server send the token for you. For curl, add
80
- `-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 question
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
- A question's 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,8 +34,8 @@ 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
41
  id: "tone",
@@ -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.
@@ -21,10 +21,8 @@ On the CLI: `--html`, `--md`, `--diff`, `--mermaid`, `--terminal`, `--code`,
21
21
 
22
22
  ## Picking a kind
23
23
 
24
- Pick by what the thing is:
25
-
26
- - `html`: you are drawing. UI, diagrams, data viz, anything interactive. Design
27
- 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.
28
26
  - `markdown`: prose, plans, tradeoffs. Tag fenced code to highlight it.
29
27
  `![caption](/a/<id>)` embeds an uploaded image. Raw HTML in the source is
30
28
  escaped.
@@ -48,16 +46,13 @@ from data, so send data, never markup.
48
46
 
49
47
  ## Composition: embeds
50
48
 
51
- Any mock can embed another mock of the project in its html with
52
- `<mockpit-embed slug="button"></mockpit-embed>`. 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.
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.
57
53
 
58
- - No `version`: the embed follows the component's version in use, else its
59
- latest, so a new version of the button shows in every
60
- page that embeds it without republishing them. `version="3"` pins it.
54
+ - No `version`: follows the component's version in use, else its latest, without republishing
55
+ what embeds it. `version="3"` pins it.
61
56
  - No `variant`: the component's variant in use (else `default`). Name one to
62
57
  show a specific variant.
63
58
  - The component shows as decided: its parts with fills keep only the fills the user
@@ -67,20 +62,17 @@ variants and questions.
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,39 +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
96
  ## Posters
106
97
 
107
- A heavy page (3D, WebGL, a running canvas) costs a live frame in every
108
- overview card, and a browser holds only about 16. Send a still of it as the
109
- html's `poster`, and Studio and Home show the picture; Stage still runs the
110
- page.
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:
111
106
 
112
107
  ```
113
- CLI mockpit publish --mock onni --variant felt --html felt.html --poster felt.png
114
- MCP publish { mock, variant, html, poster: "<asset id, or an image path on stdio>" }
115
- HTTP POST /api/mocks { …, "html": "…", "poster": "<asset id>" }
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>" }
116
111
  ```
117
112
 
118
- Render it yourself (a headless screenshot of the page) at the moment that
119
- shows the design best. A poster belongs to the html it was taken of: a
120
- version that changes the page without a new one (`--parts`, `--fills`, a fork
121
- with edits) drops it, so send one with each.
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.
122
115
 
123
116
  ## What the user sees
124
117
 
125
- Each thing you add lights up its part of the viewer.
126
-
127
- | you publish | the user gets |
128
- | ---------------------- | ---------------------------------------------------- |
129
- | a plain mock, any kind | the stage and Thread, with Use this / Revise |
130
- | several variants | a switcher, plus a built-in variant question |
131
- | questions (`ask`) | Questions: picture options on the stage, then Send |
132
- | `data-part` | Tune's component list and comments anchored on parts |
133
- | `knobs` | Tune's controls, live on the stage |
134
- | 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
+ };
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "mockpit-opencode",
3
+ "private": true,
4
+ "type": "module",
5
+ "exports": {
6
+ "./server": "./mockpit.js"
7
+ }
8
+ }