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.
- package/README.md +66 -202
- package/bin/demoData.js +5 -5
- package/bin/initDesign.js +6 -6
- package/bin/mockpit.js +687 -192
- package/bin/poster.js +377 -0
- package/dist/mcp/server.js +110 -19
- package/dist/server/answerAdditions.d.ts +7 -0
- package/dist/server/answerAdditions.js +76 -0
- package/dist/server/apiViews.d.ts +563 -41
- package/dist/server/apiViews.js +223 -74
- package/dist/server/app.d.ts +10 -2
- package/dist/server/app.js +3284 -782
- package/dist/server/appIcon.js +15 -5
- package/dist/server/appIconPng.js +3 -3
- package/dist/server/askNudges.js +1 -1
- package/dist/server/cdnInline.d.ts +8 -0
- package/dist/server/cdnInline.js +638 -0
- package/dist/server/codeWorker.js +2 -1
- package/dist/server/decisionList.d.ts +45 -0
- package/dist/server/decisionList.js +302 -0
- package/dist/server/decisions.d.ts +8 -8
- package/dist/server/decisions.js +18 -17
- package/dist/server/designGuide.d.ts +10 -6
- package/dist/server/designGuide.js +173 -78
- package/dist/server/embeds.d.ts +4 -1
- package/dist/server/embeds.js +57 -6
- package/dist/server/eventLog.d.ts +2 -2
- package/dist/server/eventLog.js +11 -11
- package/dist/server/events.d.ts +9 -6
- package/dist/server/feedbackBatch.d.ts +38 -16
- package/dist/server/feedbackBatch.js +121 -50
- package/dist/server/fills.d.ts +4 -0
- package/dist/server/fills.js +55 -1
- package/dist/server/guidance.d.ts +23 -0
- package/dist/server/guidance.js +71 -0
- package/dist/server/handoff.d.ts +4 -1
- package/dist/server/handoff.js +8 -2
- package/dist/server/icons.js +1 -1
- package/dist/server/index.js +49 -11
- package/dist/server/jump.d.ts +21 -0
- package/dist/server/jump.js +175 -0
- package/dist/server/knobs.d.ts +3 -3
- package/dist/server/knobs.js +6 -6
- package/dist/server/mcpHttp.d.ts +8 -1
- package/dist/server/mcpHttp.js +44 -7
- package/dist/server/mcpSpec.d.ts +2 -1
- package/dist/server/mcpSpec.js +172 -50
- package/dist/server/parts.js +9 -1
- package/dist/server/postSurfaces.js +3 -0
- package/dist/server/run.d.ts +1 -1
- package/dist/server/run.js +31 -17
- package/dist/server/runApi.d.ts +3 -3
- package/dist/server/runApi.js +34 -24
- package/dist/server/sqlStore.d.ts +117 -94
- package/dist/server/sqlStore.js +1089 -141
- package/dist/server/standings.d.ts +25 -0
- package/dist/server/standings.js +113 -0
- package/dist/server/surfacePage.d.ts +6 -9
- package/dist/server/surfacePage.js +361 -86
- package/dist/server/types.d.ts +252 -69
- package/dist/server/types.js +87 -69
- package/extensions/mockpit.js +245 -65
- package/guide/AGENT_SETUP.md +13 -1
- package/guide/packs/design-craft.md +34 -0
- package/guide/topics/asks.md +117 -69
- package/guide/topics/feedback.md +156 -119
- package/guide/topics/html.md +19 -32
- package/guide/topics/http.md +43 -40
- package/guide/topics/knobs.md +7 -10
- package/guide/topics/scripts.md +24 -35
- package/guide/topics/surfaces.md +50 -41
- package/opencode/mockpit.js +120 -0
- package/opencode/package.json +8 -0
- package/package.json +4 -6
- package/skills/launch-video/SKILL.md +12 -12
- package/skills/mockpit/SKILL.md +35 -23
- package/viewer/dist/index.html +9 -8
package/guide/topics/html.md
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# mockpit topic: html
|
|
2
2
|
|
|
3
|
-
An `html` surface is a blank canvas
|
|
4
|
-
|
|
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
|
|
16
|
-
|
|
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
|
|
25
|
-
|
|
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
|
|
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
|
|
48
|
-
|
|
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
|
-
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
|
|
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 --
|
|
135
|
+
mockpit publish --design ci-status --html status.html --kit issues # repeat --kit for more
|
|
146
136
|
```
|
|
147
137
|
|
|
148
138
|
```js
|
|
149
|
-
publish({
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
package/guide/topics/http.md
CHANGED
|
@@ -1,61 +1,65 @@
|
|
|
1
1
|
# mockpit topic: http
|
|
2
2
|
|
|
3
|
-
Every verb
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
|
7
|
-
|
|
|
8
|
-
| `mockpit
|
|
9
|
-
| `mockpit
|
|
10
|
-
| `mockpit
|
|
11
|
-
| `mockpit read
|
|
12
|
-
| `mockpit feedback`
|
|
13
|
-
| `mockpit say`
|
|
14
|
-
| `mockpit
|
|
15
|
-
| `mockpit
|
|
16
|
-
| `mockpit
|
|
17
|
-
| `mockpit
|
|
18
|
-
| `mockpit
|
|
19
|
-
| `mockpit
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
`
|
|
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/
|
|
31
|
-
"project": "demo", "
|
|
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
|
-
"
|
|
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
|
-
# → {"
|
|
39
|
+
# → {"design":{…},"post":{…},"sessionId":"S","url":"…","parts":[{"state":"Empty cart","parts":[{"name":"summary","label":"Summary"}]}],"feedback":[]}
|
|
36
40
|
|
|
37
|
-
# the same (
|
|
38
|
-
curl -s -X POST $B/api/
|
|
39
|
-
"session": "S", "project": "demo", "
|
|
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/
|
|
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":[{"
|
|
50
|
-
# "pending":[{"
|
|
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/
|
|
53
|
-
"session": "S", "project": "demo", "message": "
|
|
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/
|
|
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
|
|
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
|
|
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
|
-
|
|
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"`.
|
package/guide/topics/knobs.md
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
# mockpit topic: knobs
|
|
2
2
|
|
|
3
|
-
A knob is a value the user tunes live on one render.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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.
|
|
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 --
|
|
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
|
-
|
|
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.
|
package/guide/topics/scripts.md
CHANGED
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
# mockpit topic: scripts
|
|
2
2
|
|
|
3
|
-
`run` executes a JavaScript script on the server against the mockpit API
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
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
|
-
|
|
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({
|
|
44
|
-
await mockpit.publish({
|
|
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: "
|
|
48
|
-
text: "
|
|
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
|
|
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
|
|
84
|
-
|
|
85
|
-
- `feedback` holds every batch the run received, from `feedback()` and from
|
|
86
|
-
the
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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.
|
package/guide/topics/surfaces.md
CHANGED
|
@@ -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
|
-
|
|
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
|
`` 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
|
|
51
|
-
`<mockpit-embed slug="button"></mockpit-embed
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
{ "
|
|
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
|
|
100
|
-
(`mockpit asset-url shot.png`) and write `<img src="/a/<hash>">` before the
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
110
|
-
|
|
|
111
|
-
| a plain
|
|
112
|
-
| several variants
|
|
113
|
-
|
|
|
114
|
-
| `data-part`
|
|
115
|
-
| `knobs`
|
|
116
|
-
| several states
|
|
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
|
+
};
|