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
@@ -4,23 +4,23 @@ import { GUIDE_TOPICS } from "./designGuide.js";
4
4
  import { feedbackImages } from "./feedbackBatch.js";
5
5
  import { KIT_IDS } from "./kits.js";
6
6
  import { RUN_DESCRIPTION } from "./runApi.js";
7
- import { SURFACE_KINDS } from "./types.js";
7
+ import { MAX_NOTES, SURFACE_KINDS } from "./types.js";
8
8
  export const MCP_SERVER_INFO = { name: "mockpit", version: "0.2.0" };
9
9
  // The `kind` enum both MCP transports advertise — derived from the one canonical
10
10
  // list (types.ts) so the MCP tier can never fall behind what REST/CLI accept.
11
11
  const SURFACE_KIND_ENUM = [...SURFACE_KINDS];
12
- export const MCP_INSTRUCTIONS = "Mockpit shows design work to the user: project > mock > state > variant > version. publish " +
13
- "each variant (name states in the user's words; mark parts with data-part); publish again to " +
14
- "revise. A choice is several variants plus one ask that binds them. One render plus a " +
12
+ export const MCP_INSTRUCTIONS = "Mockpit shows design work to the user: project > design > state > variant > version. publish " +
13
+ "each variant (name states in the user's words; declare parts with data-part); publish again to " +
14
+ "revise. A choice is several variants plus one question that binds them (ask). One render plus a " +
15
15
  "control: declare a knob. Say where to look, then end your turn; nothing waits. When the " +
16
16
  "user says they answered, call feedback. Writes return feedback too; it is delivered once. " +
17
17
  "Fetch guide before html.";
18
18
  const field = {
19
- mock: "Mock slug or id",
19
+ design: "Design slug, title or id",
20
20
  state: "State label; omit if single-state",
21
21
  variant: 'Variant label; default "default"',
22
22
  project: "Default: the session's project",
23
- title: "Mock title",
23
+ title: "Design title",
24
24
  session: "Session id from an earlier publish",
25
25
  sessionTitle: "Task name; used only when creating the session",
26
26
  agent: "Agent name for a new session",
@@ -28,13 +28,19 @@ const field = {
28
28
  knobs: 'Global knobs (tunekit usePane shape) keyed by path: [default,min,max,step], boolean, {type:"select",options}, "#hex"',
29
29
  variantKnobs: "Knobs this variant alone has",
30
30
  from: "Branch from this version",
31
+ amend: "Replace the latest version in place: a fix before the user acts on it",
32
+ withdraw: "Take the latest version back, before the user acts on it",
31
33
  fork: 'Start this new variant from a sibling: "variant" or "variant@N"; alone it copies it, with parts/fills it edits the copy',
32
34
  prompt: "What prompted this version; usually omit",
33
- note: "What this variant is; shown beside it, never in the html",
35
+ description: "What this variant is; shown beside it, never in the html",
34
36
  widths: "Widths it ships at, side by side: [390,1280]",
35
- flow: "Flow this screen is a step of; alone, only places it",
37
+ kind: "Only when building the product's screens: screen, component (a reusable design screens embed). With no html it plans one, drawn by a later publish. Omit for ideas, diagrams and reviews",
38
+ area: 'Product area in the project\'s words; Home groups by it. "" clears',
39
+ source: 'Code path implementing it. "" clears',
40
+ flow: "Flow this state is a step of; alone, only places it",
36
41
  step: "1-based place; 0 removes",
37
42
  kits: `Opt-in bundles: ${KIT_IDS.join("|")}`,
43
+ poster: "Still of the page (asset id) shown instead of running it; resend when the page changes",
38
44
  };
39
45
  const diffFileSchema = z.object({
40
46
  filename: z.string(),
@@ -53,6 +59,7 @@ export const mcpSurfaceSchema = z
53
59
  kind: z.enum(SURFACE_KIND_ENUM).optional(),
54
60
  html: z.string().optional().describe(field.html),
55
61
  kits: z.array(z.string()).optional().describe(field.kits),
62
+ poster: z.string().optional().describe(field.poster),
56
63
  markdown: z.string().optional().describe("Prose; raw HTML is escaped"),
57
64
  mermaid: z.string().optional().describe("Diagram source; do not set colors"),
58
65
  patch: z.string().optional().describe("Preferred over files"),
@@ -74,10 +81,14 @@ export const mcpSurfaceSchema = z
74
81
  description: "Set kind and its field: html, markdown, mermaid, patch|files (diff), assetId (image), text (terminal), data (json), code",
75
82
  });
76
83
  const askSchema = z.object({
77
- id: z.string().optional().describe("Stable id; reusing one replaces that ask"),
84
+ id: z.string().optional().describe("Stable id; reusing one replaces that question"),
78
85
  text: z.string().describe("The question, one sentence"),
79
- scope: z.enum(["mock", "state", "part"]).optional(),
86
+ scope: z.enum(["design", "state", "part"]).optional(),
80
87
  state: z.string().optional(),
88
+ states: z
89
+ .array(z.string())
90
+ .optional()
91
+ .describe("Design-wide question: the states its answers can be seen on, best first"),
81
92
  part: z.string().optional().describe("data-part name, for scope part"),
82
93
  multi: z.boolean().optional(),
83
94
  after: z
@@ -90,6 +101,10 @@ const askSchema = z.object({
90
101
  variant: z.string().optional().describe("Bind to a published variant"),
91
102
  version: z.number().int().optional().describe("And one version of it"),
92
103
  set: z.record(z.string(), z.unknown()).optional().describe("Or to knob values {path: value}"),
104
+ style: z.string().optional().describe("With a set of a new value: its CSS"),
105
+ fill: z.string().optional().describe("With a set of {part: new fill}: its html"),
106
+ html: z.string().optional().describe("Or a whole page"),
107
+ recommended: z.boolean().optional().describe("The one you would pick; one per ask"),
93
108
  })),
94
109
  });
95
110
  function zodParam(p) {
@@ -113,6 +128,9 @@ function zodParam(p) {
113
128
  case "integers":
114
129
  s = z.array(z.number().int());
115
130
  break;
131
+ case "strings":
132
+ s = z.array(z.string());
133
+ break;
116
134
  default:
117
135
  s = p.t === "string" ? z.string() : p.t === "number" ? z.number() : z.boolean();
118
136
  }
@@ -121,37 +139,42 @@ function zodParam(p) {
121
139
  return p.req ? s : s.optional();
122
140
  }
123
141
  const P = {
124
- mock: { t: "string", d: field.mock, req: true },
142
+ design: { t: "string", d: field.design, req: true },
125
143
  state: { t: "string", d: field.state },
126
144
  variant: { t: "string", d: field.variant },
127
145
  project: { t: "string", d: field.project },
128
146
  session: { t: "string", d: field.session },
129
147
  };
130
- const VARIANT_REF = { mock: P.mock, state: P.state, variant: P.variant, project: P.project };
148
+ const VARIANT_REF = { design: P.design, state: P.state, variant: P.variant, project: P.project };
131
149
  export const MCP_TOOL_DEFS = [
132
150
  {
133
151
  name: "publish",
134
- description: "Create or version one variant of one state of a mock. One of html, surfaces (the full ordered list), parts or fills (splice the latest version). Returns parts per state, nudges, feedback.",
152
+ description: "Create or version one variant of one state of a design. One of html, surfaces (the full ordered list), parts or fills (splice the latest version); or kind screen or component and none of them, to plan it. Returns parts per state, nudges, feedback.",
135
153
  params: {
136
154
  ...VARIANT_REF,
137
155
  title: { t: "string", d: field.title },
138
- kind: { t: "enum", values: ["component", "page"] },
156
+ kind: { t: "enum", values: ["screen", "component", "idea"], d: field.kind },
157
+ area: { t: "string", d: field.area },
158
+ source: { t: "string", d: field.source },
139
159
  html: { t: "string", d: field.html },
160
+ poster: { t: "string", d: field.poster },
140
161
  surfaces: {
141
162
  t: "surfaces",
142
163
  d: "Full ordered list: {id} keeps a surface, a missing id removes it",
143
164
  },
144
- parts: { t: "object", d: '{"name"|"name#key": outer html}; splices marked parts' },
165
+ parts: { t: "object", d: '{"name"|"name#key": outer html}; splices declared parts' },
145
166
  fills: {
146
167
  t: "object",
147
- d: '{"part#fill": outer html}; replaces that fill of a slot, or adds it after the others',
168
+ d: '{"part#fill": outer html}; replaces that fill of a part, or adds it after its others',
148
169
  },
149
170
  knobs: { t: "object", d: field.knobs },
150
171
  variantKnobs: { t: "object", d: field.variantKnobs },
151
172
  from: { t: "integer", d: field.from },
152
173
  fork: { t: "string", d: field.fork },
174
+ amend: { t: "boolean", d: field.amend },
175
+ withdraw: { t: "boolean", d: field.withdraw },
153
176
  prompt: { t: "string", d: field.prompt },
154
- note: { t: "string", d: field.note },
177
+ description: { t: "string", d: field.description },
155
178
  widths: { t: "integers", d: field.widths },
156
179
  flow: { t: "string", d: field.flow },
157
180
  step: { t: "integer", d: field.step },
@@ -163,9 +186,9 @@ export const MCP_TOOL_DEFS = [
163
186
  },
164
187
  {
165
188
  name: "ask",
166
- description: "Ask structured questions on a mock. A choice is several variants plus one ask that binds them; options may also set knob values. Reusing an ask id replaces it.",
189
+ description: "Ask structured questions on a design. A choice is several variants plus one question that binds them; options may also set knob values. Reusing a question id replaces it.",
167
190
  params: {
168
- mock: P.mock,
191
+ design: P.design,
169
192
  project: P.project,
170
193
  asks: { t: "asks", d: "Questions with options", req: true },
171
194
  session: P.session,
@@ -174,21 +197,25 @@ export const MCP_TOOL_DEFS = [
174
197
  },
175
198
  {
176
199
  name: "read",
177
- description: "Without mock: every mock (slug, states, variants, open asks), flows, plus pending. With mock: its states, variants, asks with answers, parts, knobs and settled values (every answer's set and tuning sent so far), plus pending. Never consumes feedback.",
200
+ description: "Read this first. Both forms carry decisionList: what the user already settled in this project; it stands unless they say otherwise. Without design: every design (slug, states, variants, open questions), flows, plus pending. With design: its notes (a spec; the user's edits stand), states, variants, questions with answers, parts, knobs and settled values (every answer's set and tuning sent so far), plus pending. Never consumes feedback.",
178
201
  params: {
179
- mock: { t: "string", d: field.mock },
202
+ design: { t: "string", d: field.design },
180
203
  project: P.project,
181
204
  body: { t: "boolean", d: "Include each variant's surfaces" },
182
205
  history: { t: "boolean", d: "Include version rows" },
183
206
  tree: {
184
207
  t: "boolean",
185
- d: "Without mock: the project as a tree (each mock's variants with status and forkedFrom, the mocks it uses and is used in)",
208
+ d: "Without design: the project as a tree (each design's variants with status and forkedFrom, the designs it embeds and is embedded in)",
209
+ },
210
+ decisions: {
211
+ t: "boolean",
212
+ d: "Only the whole decision list, when decisionList says it was cut",
186
213
  },
187
214
  },
188
215
  },
189
216
  {
190
217
  name: "feedback",
191
- description: "Returns at once: what the user sent since you last heard (one batch per mock: reply answers, mix, tuned knobs, part comments, comments, accepted/archived/dropped, or resolved with the asks it dismissed), delivered once, plus pending (viewer open, draft progress).",
218
+ description: "Returns at once: what the user sent since you last heard (one batch per design: reply answers, borrowed parts, tuned knobs, part comments, verdict; thread messages; inUse/archived), delivered once, plus pending (viewer open, draft progress).",
192
219
  params: {
193
220
  project: P.project,
194
221
  session: { t: "string", d: "Session id returned by publish", req: true },
@@ -197,7 +224,7 @@ export const MCP_TOOL_DEFS = [
197
224
  },
198
225
  {
199
226
  name: "say",
200
- description: "Post a short plain-text message in a mock's thread. Returns feedback.",
227
+ description: "Post a short plain-text message in a design's thread. Returns feedback.",
201
228
  params: {
202
229
  ...VARIANT_REF,
203
230
  message: { t: "string", d: "Plain text", req: true },
@@ -205,11 +232,51 @@ export const MCP_TOOL_DEFS = [
205
232
  },
206
233
  httpOnly: ["session"],
207
234
  },
235
+ {
236
+ name: "move",
237
+ description: "Tidy up variants: to or toState moves them there and out of here; archive takes them out of view; restore brings them back; a group goes along; one in use stays. Only when the user asks: hideOlder hides each variant's older versions (variants optional: all of them), and deleteOlder deletes them for good, skipping any that a reply, comment, question, embed, copy or fork still points at, or that is in use. Confirm a delete first: it can't be undone. Returns feedback.",
238
+ params: {
239
+ design: P.design,
240
+ state: P.state,
241
+ variants: { t: "strings", d: "Variant names; required except with hideOlder or deleteOlder" },
242
+ to: { t: "string", d: "Target design; default this one" },
243
+ toState: { t: "string", d: "Target state; created if new" },
244
+ archive: { t: "boolean" },
245
+ restore: { t: "boolean" },
246
+ hideOlder: { t: "boolean", d: "Hide all but each variant's newest visible version" },
247
+ deleteOlder: { t: "boolean", d: "Delete them instead; permanent, skips what's referenced" },
248
+ project: P.project,
249
+ session: P.session,
250
+ },
251
+ httpOnly: ["session"],
252
+ },
208
253
  {
209
254
  name: "export",
210
255
  description: "The html in use (else current) per state, version history, screenshot URL, knobs, the settled values and the last reply.",
211
256
  params: VARIANT_REF,
212
257
  },
258
+ {
259
+ name: "edit",
260
+ description: "Change a design without a new version; a new slug rewrites every reference to the old one. Without design, rename renames the project and areaOrder orders its areas. Returns feedback.",
261
+ params: {
262
+ design: { t: "string", d: field.design },
263
+ project: P.project,
264
+ slug: { t: "string" },
265
+ title: { t: "string" },
266
+ kind: { t: "enum", values: ["screen", "component", "idea"] },
267
+ area: { t: "string" },
268
+ source: { t: "string" },
269
+ firstState: { t: "string", d: "Names a design's one unnamed state, so it can have more" },
270
+ notes: {
271
+ t: "string",
272
+ d: `A short plain-text spec, at most ${MAX_NOTES} characters; "" clears. Not applied over the user's own edit, and the result says so`,
273
+ },
274
+ rename: { t: "string", d: "New project name" },
275
+ areaOrder: { t: "strings", d: "Without design: Home's area order" },
276
+ session: P.session,
277
+ },
278
+ httpOnly: ["session"],
279
+ },
213
280
  {
214
281
  name: "upload",
215
282
  description: "Upload bytes and return id and URL. Reference id as an image surface's assetId.",
@@ -263,52 +330,69 @@ const bag = z.record(z.string(), z.unknown());
263
330
  const variantRef = o({ state: nullStr, variant: str });
264
331
  const inUseRef = o({ state: nullStr, variant: str, version: num });
265
332
  const partsByState = z.array(o({ state: nullStr, parts: z.array(o({ name: str })) }));
266
- const flowSchema = o({ name: str, steps: z.array(o({ mock: str, state: nullStr })) });
333
+ const flowSchema = o({ name: str, steps: z.array(o({ design: str, state: nullStr })) });
267
334
  const feedbackSchema = o({
268
- mock: nullStr,
335
+ design: nullStr,
269
336
  reply: o({
270
337
  asks: z.array(o({
271
338
  ask: str,
272
339
  chosen: z.array(o({ label: str, other: z.literal(true).optional() })),
273
- note: str.optional(),
340
+ description: str.optional(),
274
341
  })),
275
- mix: bag,
342
+ borrowed: bag.describe("Part → the variant to take it from"),
276
343
  tuned: bag,
277
344
  comments: z.array(o({ part: nullStr, text: str })),
278
345
  reactions: strs.optional().describe('One-tap reactions, e.g. 👍 part "hero" (variant B)'),
346
+ verdict: o({ kind: z.enum(["use", "revise"]), state: nullStr, variant: str })
347
+ .optional()
348
+ .describe("Use this or New version on one variant"),
349
+ more: o({
350
+ ask: str,
351
+ text: str,
352
+ existing: strs,
353
+ note: str.optional(),
354
+ answered: z.array(o({ ask: str, text: str, chosen: strs, comment: str.optional() })),
355
+ })
356
+ .optional()
357
+ .describe("The user wants more answers to this question: ask it again, same id, with them"),
279
358
  })
280
359
  .nullable()
281
- .describe("The user's Send; null if only comments"),
282
- comments: z.array(o({ seq: num, text: str })),
283
- accepted: z.array(inUseRef).describe("Accepted, each at the version put in use"),
360
+ .describe("The user's Send; null if only messages"),
361
+ messages: z.array(o({ seq: num, text: str })).describe("What the user wrote in the thread"),
362
+ inUse: z.array(inUseRef).describe("In use, each at the version the user put in use"),
284
363
  archived: z.array(variantRef),
285
- dropped: z.array(variantRef),
286
- resolved: z
287
- .literal(true)
288
- .optional()
289
- .describe("The user resolved the mock: its open asks were closed unanswered"),
290
- dismissed: z
291
- .array(o({ ask: str, text: str }))
364
+ edited: o({
365
+ kind: str.optional(),
366
+ area: nullStr.optional(),
367
+ notes: nullStr.optional(),
368
+ })
292
369
  .optional()
293
- .describe("The asks the resolution closed; don't ask them again"),
370
+ .describe("The user moved the design from Home (its kind or area now) or edited its notes"),
294
371
  }).meta({ id: "Feedback" });
295
372
  // Typed once, on feedback: repeating it on every write would cost more than
296
373
  // the rest of the write's output schema.
297
374
  const feedbackRide = z.array(bag).describe("Feedback batches as the feedback tool returns");
298
375
  const pendingSchema = o({
299
- mock: str,
300
- resolved: z.literal(true).optional(),
376
+ design: str,
377
+ archived: z.literal(true).optional(),
301
378
  viewerOpen: z.boolean(),
302
379
  draft: o({ answered: num, of: num, comments: num, touchedAt: str }).nullable(),
303
380
  }).meta({ id: "Pending" });
304
381
  const writeOutput = {
305
- post: o({ state: nullStr, variant: str, version: num }),
382
+ post: o({ state: nullStr, variant: str, version: num })
383
+ .optional()
384
+ .describe("Absent when planned: nothing is drawn yet"),
385
+ planned: z.boolean().optional(),
306
386
  sessionId: str,
307
387
  url: str.describe("Viewer link for the user"),
308
- parts: partsByState,
388
+ parts: partsByState.optional(),
309
389
  partChanges: o({ vanished: strs, renamed: z.array(o({ from: str, to: str })) }).optional(),
310
390
  nudges: strs.optional(),
311
391
  suggestedAsk: bag.optional().describe("Send with ask when a nudge names it"),
392
+ suggestedFillAsks: z
393
+ .array(bag)
394
+ .optional()
395
+ .describe("A question per part with fills that none sets; send with ask"),
312
396
  flow: flowSchema.optional(),
313
397
  feedback: feedbackRide,
314
398
  };
@@ -338,21 +422,43 @@ const OUTPUT_SCHEMAS = {
338
422
  feedback: feedbackRide,
339
423
  },
340
424
  feedback: {
341
- feedback: z.array(feedbackSchema).describe("One batch per mock; empty when nothing is new"),
425
+ feedback: z.array(feedbackSchema).describe("One batch per design; empty when nothing is new"),
342
426
  pending: z.array(pendingSchema),
343
427
  },
344
428
  say: { feedback: feedbackRide },
429
+ move: {
430
+ variants: z.array(o({ state: str.nullable(), variant: str, status: str })),
431
+ versions: z
432
+ .array(o({ state: str.nullable(), variant: str, version: num }))
433
+ .optional()
434
+ .describe("With hideOlder or deleteOlder: what was hidden or deleted"),
435
+ skipped: z.array(o({ state: str.nullable().optional(), variant: str, version: num.optional(), why: str })),
436
+ to: o({ design: str, title: str, state: str.nullable() }).optional(),
437
+ saved: z
438
+ .array(o({ state: str.nullable(), variant: str, already: z.boolean() }))
439
+ .optional()
440
+ .describe("What each is called there"),
441
+ url: str,
442
+ feedback: feedbackRide,
443
+ },
444
+ edit: {
445
+ renamed: o({ from: str, to: str }).optional(),
446
+ areaOrder: strs.optional(),
447
+ url: str,
448
+ nudges: strs.optional(),
449
+ feedback: feedbackRide,
450
+ },
345
451
  read: {
346
- mocks: z.array(o({ slug: str, states: strs, variants: num, open: num })).optional(),
452
+ designs: z.array(o({ slug: str, states: strs, variants: num, open: num })).optional(),
347
453
  states: strs.optional(),
348
454
  asks: z.array(o({ id: str, text: str })).optional(),
349
455
  variants: z
350
456
  .array(o({
351
457
  state: nullStr,
352
458
  variant: str,
353
- status: str,
459
+ status: z.enum(["open", "inUse", "archived"]),
354
460
  version: num,
355
- acceptedVersion: num.optional().describe("Set when accepted: the version in use"),
461
+ inUseVersion: num.optional().describe("Set while in use: the version in use"),
356
462
  history: z
357
463
  .array(o({ version: num, hidden: z.boolean().optional() }))
358
464
  .optional()
@@ -367,7 +473,20 @@ const OUTPUT_SCHEMAS = {
367
473
  settled: bag.optional().describe("Every knob value the user has sent, later replies winning"),
368
474
  tree: z.array(bag).optional(),
369
475
  flows: z.array(flowSchema).optional(),
370
- pending: z.union([z.array(pendingSchema), pendingSchema]),
476
+ decisionList: o({
477
+ total: num,
478
+ lines: z.array(o({
479
+ design: str,
480
+ text: str,
481
+ here: z.boolean().optional().describe("From the design read"),
482
+ edited: z.boolean().optional(),
483
+ archived: z.boolean().optional(),
484
+ })),
485
+ more: str.optional(),
486
+ }).optional(),
487
+ total: num.optional(),
488
+ lines: z.array(bag).optional(),
489
+ pending: z.union([z.array(pendingSchema), pendingSchema]).optional(),
371
490
  },
372
491
  export: {
373
492
  states: z.array(o({
@@ -375,7 +494,8 @@ const OUTPUT_SCHEMAS = {
375
494
  variant: str,
376
495
  version: num,
377
496
  latest: num.optional().describe("Your newer version, not in use yet"),
378
- html: str,
497
+ html: str.describe("With fills: only the chosen ones"),
498
+ allFills: str.optional().describe("With fills: the html with every fill"),
379
499
  markdown: str,
380
500
  })),
381
501
  settled: bag,
@@ -395,7 +515,9 @@ function shapeFor(tool, transport) {
395
515
  shape[key] =
396
516
  transport === "stdio" && key === "html" && tool.name === "publish"
397
517
  ? z.string().optional().describe(`${field.html}; or a file path`)
398
- : zodParam(p);
518
+ : transport === "stdio" && key === "poster" && tool.name === "publish"
519
+ ? z.string().optional().describe(`${p.d}; or an image file path`)
520
+ : zodParam(p);
399
521
  }
400
522
  return shape;
401
523
  }
@@ -48,8 +48,16 @@ export function partsInHtml(html) {
48
48
  }
49
49
  return [...byName.values()];
50
50
  }
51
+ // Cached per surfaces array, which a stored post never mutates: a design's view
52
+ // asks for every variant's parts on every read. Callers must not change the result.
53
+ const bySurfaces = new WeakMap();
51
54
  export function partsInSurfaces(surfaces) {
52
- return mergeParts(surfaces.flatMap((s) => (s.kind === "html" ? [partsInHtml(s.html)] : [])));
55
+ const hit = bySurfaces.get(surfaces);
56
+ if (hit)
57
+ return hit;
58
+ const parts = mergeParts(surfaces.flatMap((s) => (s.kind === "html" ? [partsInHtml(s.html)] : [])));
59
+ bySurfaces.set(surfaces, parts);
60
+ return parts;
53
61
  }
54
62
  // Union several part lists by name, first occurrence first.
55
63
  export function mergeParts(lists) {
@@ -44,17 +44,20 @@ const strictHtmlSurface = z.object({
44
44
  kind: z.literal("html"),
45
45
  html: requiredString("html"),
46
46
  kits: z.array(z.string()).optional(),
47
+ poster: z.string().min(1).optional(),
47
48
  });
48
49
  const looseHtmlSurface = z
49
50
  .object({
50
51
  kind: z.literal("html"),
51
52
  html: requiredString("html"),
52
53
  kits: z.unknown().optional(),
54
+ poster: optionalLooseString,
53
55
  })
54
56
  .transform((p) => ({
55
57
  kind: "html",
56
58
  html: p.html,
57
59
  ...(Array.isArray(p.kits) ? { kits: p.kits.filter((k) => typeof k === "string") } : {}),
60
+ ...(p.poster ? { poster: p.poster } : {}),
58
61
  }));
59
62
  // Strict mode rejects an unknown id with the valid set, so a CLI/REST typo is a
60
63
  // clean 400; loose mode filters unknown ids out rather than dropping the whole
@@ -77,6 +77,6 @@ export interface RunDeps {
77
77
  status: number;
78
78
  }>;
79
79
  }
80
- export declare const RUN_UNAVAILABLE = "run is not available on this deployment: it has no script sandbox. Use the mock tools (or REST) instead";
80
+ export declare const RUN_UNAVAILABLE = "run is not available on this deployment: it has no script sandbox. Use the design tools (or REST) instead";
81
81
  export declare function cut(s: string, max: number): string;
82
82
  export declare function createRunFlow(deps: RunDeps): (body: any, ctx: FlowContext) => Promise<FlowResult>;
@@ -9,7 +9,7 @@
9
9
  // that is counted, bounded and logged.
10
10
  import { decodeBase64 } from "./base64.js";
11
11
  import { isGuideTopic, unknownTopicMessage } from "./designGuide.js";
12
- import { unwrap } from "./mcpHttp.js";
12
+ import { editCall, unwrap } from "./mcpHttp.js";
13
13
  import { RUN_FUNCTIONS } from "./runApi.js";
14
14
  // A script never waits for the user, so the wall clock is only a sandbox guard:
15
15
  // 10 s clears a run of publishes with room to spare. 32 MiB and a 256 KiB stack are what the spike showed safe (512 KiB overflows the host
@@ -26,7 +26,7 @@ export const DEFAULT_RUN_LIMITS = {
26
26
  maxInflight: 4,
27
27
  maxRuns: 4,
28
28
  };
29
- export const RUN_UNAVAILABLE = "run is not available on this deployment: it has no script sandbox. Use the mock tools (or REST) instead";
29
+ export const RUN_UNAVAILABLE = "run is not available on this deployment: it has no script sandbox. Use the design tools (or REST) instead";
30
30
  const text = (v, max) => typeof v === "string" && v.trim() ? v.trim().slice(0, max) : undefined;
31
31
  const plain = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
32
32
  // Keep the head and the tail: the start says what a value is, the end is where
@@ -40,7 +40,7 @@ export function cut(s, max) {
40
40
  }
41
41
  function writeSummary(body) {
42
42
  const post = body?.post;
43
- const slug = body?.mock?.slug ?? "?";
43
+ const slug = body?.design?.slug ?? "?";
44
44
  if (!post)
45
45
  return slug;
46
46
  return `${slug}/${post.state ? `${post.state}/` : ""}${post.variant} v${post.version}`;
@@ -152,26 +152,30 @@ export function createRunFlow(deps) {
152
152
  throw new Error(unknownTopicMessage(String(topic)));
153
153
  return [await deps.guide("", topic), String(topic)];
154
154
  },
155
- async read([mock, opts]) {
156
- if ((mock === undefined || mock === null) && plain(opts).tree === true) {
155
+ async read([design, opts]) {
156
+ if (plain(opts).decisions === true) {
157
+ const value = await take(flows.decisions({ project: await projectOf() }));
158
+ return [value, `${value.total} decisions`];
159
+ }
160
+ if ((design === undefined || design === null) && plain(opts).tree === true) {
157
161
  const value = await take(flows.tree({ project: await projectOf() }));
158
- return [value, `${value.tree.length} mocks`];
162
+ return [value, `${value.tree.length} designs`];
159
163
  }
160
- if (mock === undefined || mock === null) {
164
+ if (design === undefined || design === null) {
161
165
  const value = await take(flows.list({ project: await projectOf() }));
162
- return [value, `${value.mocks.length} mocks`];
166
+ return [value, `${value.designs.length} designs`];
163
167
  }
164
168
  const o = plain(opts);
165
- const value = await take(flows.get(mock, { project, body: o.body === true, history: o.history === true }));
166
- return [value, String(value?.slug ?? mock)];
169
+ const value = await take(flows.get(design, { project, body: o.body === true, history: o.history === true }));
170
+ return [value, String(value?.slug ?? design)];
167
171
  },
168
172
  async publish([v]) {
169
173
  const value = await take(flows.publish({ ...(await mine(plain(v))), agent }, flowCtx));
170
174
  return [value, writeSummary(value)];
171
175
  },
172
- async ask([mock, asks]) {
173
- const value = await take(flows.ask(mock, await mine({ asks }), flowCtx));
174
- return [value, `${value.mock} ${value.asks.length} ask(s)`];
176
+ async ask([design, asks]) {
177
+ const value = await take(flows.ask(design, await mine({ asks }), flowCtx));
178
+ return [value, `${value.design} ${value.asks.length} ask(s)`];
175
179
  },
176
180
  async feedback() {
177
181
  const value = await take(flows.feedback(await mine({}), flowCtx));
@@ -180,8 +184,18 @@ export function createRunFlow(deps) {
180
184
  },
181
185
  async say([v, message]) {
182
186
  const r = plain(v);
183
- await take(flows.say(r.mock, { ...(await mine(r)), message }, flowCtx));
184
- return [null, String(r.mock ?? "?")];
187
+ await take(flows.say(r.design, { ...(await mine(r)), message }, flowCtx));
188
+ return [null, String(r.design ?? "?")];
189
+ },
190
+ async move([v]) {
191
+ const r = plain(v);
192
+ const value = await take(flows.move(r.design, await mine(r), flowCtx));
193
+ return [value, `${value.variants.length} moved`];
194
+ },
195
+ async edit([v]) {
196
+ const r = await mine(plain(v));
197
+ const value = await take(editCall(flows, r, flowCtx, project));
198
+ return [value, value.renamed ? `${value.renamed.from} → ${value.renamed.to}` : "edited"];
185
199
  },
186
200
  async upload([data, opts]) {
187
201
  if (typeof data !== "string" || !data)
@@ -201,8 +215,8 @@ export function createRunFlow(deps) {
201
215
  },
202
216
  async export([v]) {
203
217
  const r = plain(v);
204
- const value = await take(flows.exportMock(r.mock, { ...r, project }, flowCtx));
205
- return [value, `${value.mock} ${value.states.length} state(s)`];
218
+ const value = await take(flows.exportDesign(r.design, { ...r, project }, flowCtx));
219
+ return [value, `${value.design} ${value.states.length} state(s)`];
206
220
  },
207
221
  };
208
222
  const call = (name, argsJson) => {
@@ -1,6 +1,6 @@
1
- export declare const RUN_API = "// The body of an async function, in plain JavaScript (no TypeScript syntax).\n// `await mockpit.*`, `return` a JSON value, print() adds an output line.\n// No network, timers, imports or process. Session, project and agent are fixed\n// for the run; pass the returned session to the next run.\ntype Ref = { mock: string; state?: string; variant?: string };\ntype Knobs = Record<string, unknown>; // path \u2192 [default,min,max,step] | boolean | {type:\"select\",options} | \"#hex\"\ntype Surface =\n | { kind: \"html\"; html: string; kits?: string[] }\n | { kind: \"markdown\"; markdown: string }\n | { kind: \"diff\"; patch?: string; files?: { filename: string; before: string; after: string }[]; layout?: \"unified\" | \"split\" }\n | { kind: \"image\"; assetId: string; alt?: string; caption?: string }\n | { kind: \"terminal\"; text: string; cols?: number; title?: string }\n | { kind: \"mermaid\"; mermaid: string }\n | { kind: \"json\"; data: unknown; title?: string }\n | { kind: \"code\"; code: string; language?: string; lineStart?: number };\ntype Ask = {\n id?: string; text: string; scope?: \"mock\" | \"state\" | \"part\"; state?: string; part?: string; multi?: boolean;\n options: { id?: string; label: string; variant?: string; set?: Knobs }[];\n};\ntype Written = {\n post: { state: string | null; variant: string; version: number }; url: string;\n parts: { state: string | null; parts: { name: string }[] }[];\n partChanges?: { vanished: string[]; renamed: { from: string; to: string }[] };\n nudges?: string[]; suggestedAsk?: Ask; // send it with ask when nudged\n};\ntype Feedback = {\n mock: string | null;\n reply: null | { asks: { ask: string; chosen: { id: string; label: string; other?: true }[]; note?: string }[]; mix: Knobs; tuned: Knobs; comments: { part: string | null; text: string }[] };\n comments: { text: string; state: string | null; variant: string | null }[];\n accepted: { state: string | null; variant: string }[]; archived: { state: string | null; variant: string }[];\n};\ntype Pending = { mock: string; viewerOpen: boolean; draft: null | { answered: number; of: number; comments: number; touchedAt: string } };\ndeclare const mockpit: {\n guide(topic?: \"knobs\" | \"asks\" | \"surfaces\" | \"html\" | \"feedback\" | \"http\" | \"scripts\"): Promise<string>;\n /** No mock: every mock, the flows, plus pending. With one: its states, variants, asks, parts, knobs. */\n read(mock?: string, opts?: { body?: boolean; history?: boolean }): Promise<unknown>;\n /** Creates or versions (mock, state, variant). One of: html; surfaces, the full ordered list\n * ({id} alone keeps that surface, a missing id is removed); parts {\"name\"|\"name#key\": outer html};\n * fills {\"part#fill\": outer html}, replacing or adding one alternative of a slot. */\n publish(v: Ref & { title?: string; kind?: \"component\" | \"page\"; html?: string; surfaces?: (Surface | { id: string })[]; parts?: Record<string, string>; fills?: Record<string, string>; knobs?: Knobs; variantKnobs?: Knobs; from?: number; prompt?: string; note?: string; widths?: number[]; flow?: string; step?: number }): Promise<Written>;\n ask(mock: string, asks: Ask[]): Promise<{ asks: { id: string; text: string }[]; url: string; nudges?: string[] }>;\n /** Never waits: what the user sent since you last heard, and what they are doing now. */\n feedback(): Promise<{ feedback: Feedback[]; pending: Pending[] }>;\n say(v: Ref, message: string): Promise<void>;\n export(v: Ref): Promise<{ states: { state: string | null; variant: string; version: number; html: string }[]; reply: unknown }>;\n /** data: base64 bytes. Use id as an image surface's assetId. */\n upload(data: string, opts: { contentType: string; filename?: string; kind?: \"image\" | \"file\" }): Promise<{ id: string; url: string }>;\n};\ndeclare function print(...values: unknown[]): void;";
2
- export declare const RUN_FUNCTIONS: readonly ["guide", "read", "publish", "ask", "feedback", "say", "export", "upload"];
1
+ export declare const RUN_API = "// The body of an async function, in plain JavaScript (no TypeScript syntax).\n// `await mockpit.*`, `return` a JSON value, print() adds an output line.\n// No network, timers, imports or process. Session, project and agent are fixed\n// for the run; pass the returned session to the next run.\ntype Ref = { design: string; state?: string; variant?: string };\ntype Knobs = Record<string, unknown>; // path \u2192 [default,min,max,step] | boolean | {type:\"select\",options} | \"#hex\"\ntype Surface =\n | { kind: \"html\"; html: string; kits?: string[] }\n | { kind: \"markdown\"; markdown: string }\n | { kind: \"diff\"; patch?: string; files?: { filename: string; before: string; after: string }[]; layout?: \"unified\" | \"split\" }\n | { kind: \"image\"; assetId: string; alt?: string; caption?: string }\n | { kind: \"terminal\"; text: string; cols?: number; title?: string }\n | { kind: \"mermaid\"; mermaid: string }\n | { kind: \"json\"; data: unknown; title?: string }\n | { kind: \"code\"; code: string; language?: string; lineStart?: number };\ntype Ask = {\n id?: string; text: string; scope?: \"design\" | \"state\" | \"part\"; state?: string; part?: string; multi?: boolean;\n options: { id?: string; label: string; variant?: string; set?: Knobs; recommended?: boolean }[];\n};\ntype Written = {\n post?: { state: string | null; variant: string; version: number }; planned?: true; url: string;\n parts?: { state: string | null; parts: { name: string }[] }[];\n partChanges?: { vanished: string[]; renamed: { from: string; to: string }[] };\n nudges?: string[]; suggestedAsk?: Ask; suggestedFillAsks?: Ask[]; // send with ask when nudged\n};\ntype Feedback = {\n design: string | null;\n reply: null | { asks: { ask: string; chosen: { id: string; label: string; other?: true }[]; comment?: string }[]; borrowed: Record<string, string>; tuned: Knobs; comments: { part: string | null; text: string }[]; verdict?: { kind: \"use\" | \"revise\"; state: string | null; variant: string; version?: number } };\n messages: { text: string; state: string | null; variant: string | null }[];\n inUse: { state: string | null; variant: string; version: number }[]; archived: { state: string | null; variant: string }[];\n};\ntype Pending = { design: string; archived?: true; viewerOpen: boolean; draft: null | { answered: number; of: number; comments: number; touchedAt: string } };\ndeclare const mockpit: {\n guide(topic?: \"knobs\" | \"asks\" | \"surfaces\" | \"html\" | \"feedback\" | \"http\" | \"scripts\"): Promise<string>;\n /** Read first: decisionList is what the user already settled; it stands unless they say otherwise.\n * No design: every design, the flows, plus pending. With one: its states, variants, asks, parts, knobs.\n * opts.decisions: only the whole list, when a read cut it. */\n read(design?: string, opts?: { body?: boolean; history?: boolean; decisions?: boolean }): Promise<unknown>;\n /** Creates or versions (design, state, variant). One of: html; surfaces, the full ordered list\n * ({id} alone keeps that surface, a missing id is removed); parts {\"name\"|\"name#key\": outer html};\n * fills {\"part#fill\": outer html}, replacing or adding one fill of a part.\n * None of them, with kind screen or component: plans it, for a later publish to draw. */\n publish(v: Ref & { title?: string; kind?: \"screen\" | \"component\" | \"idea\"; area?: string; source?: string; html?: string; poster?: string; surfaces?: (Surface | { id: string })[]; parts?: Record<string, string>; fills?: Record<string, string>; knobs?: Knobs; variantKnobs?: Knobs; from?: number; prompt?: string; description?: string; widths?: number[]; flow?: string; step?: number }): Promise<Written>;\n ask(design: string, asks: Ask[]): Promise<{ asks: { id: string; text: string }[]; url: string; nudges?: string[] }>;\n /** Never waits: what the user sent since you last heard, and what they are doing now. */\n feedback(): Promise<{ feedback: Feedback[]; pending: Pending[] }>;\n say(v: Ref, message: string): Promise<void>;\n /** to or toState moves variants there; archive hides them; restore brings them back. */\n move(v: Ref & { variants: string[]; to?: string; toState?: string; archive?: boolean; restore?: boolean }): Promise<{ variants: { state: string | null; variant: string; status: string }[]; skipped: { variant: string; why: string }[]; url: string }>;\n /** No new version; \"\" clears. { rename } or { areaOrder } without design edits the project.\n * notes: a short plain-text spec; not applied over the user's own edit (notes.applied says). */\n edit(v: { design?: string; slug?: string; title?: string; kind?: string; area?: string; source?: string; firstState?: string; notes?: string; rename?: string; areaOrder?: string[] }): Promise<{ url: string; notes?: { applied: boolean; text?: string } }>;\n export(v: Ref): Promise<{ states: { state: string | null; variant: string; version: number; html: string; allFills?: string }[]; reply: unknown }>;\n /** data: base64 bytes. Use id as an image surface's assetId. */\n upload(data: string, opts: { contentType: string; filename?: string; kind?: \"image\" | \"file\" }): Promise<{ id: string; url: string }>;\n};\ndeclare function print(...values: unknown[]): void;";
2
+ export declare const RUN_FUNCTIONS: readonly ["guide", "read", "publish", "ask", "feedback", "say", "move", "edit", "export", "upload"];
3
3
  export type RunFunction = (typeof RUN_FUNCTIONS)[number];
4
- export declare const RUN_EXAMPLE = "const html = (tone) => `<section data-part=\"hero\" class=\"${tone}\"><h1>Writer</h1></section>`;\nawait mockpit.publish({ mock: \"writer\", variant: \"calm\", html: html(\"calm\") });\nawait mockpit.publish({ mock: \"writer\", variant: \"bold\", html: html(\"bold\") });\nconst { url } = await mockpit.ask(\"writer\", [{ id: \"look\", text: \"Which look?\",\n options: [{ label: \"Calm\", variant: \"calm\" }, { label: \"Bold\", variant: \"bold\" }] }]);\n// Tell the user where to look and end the turn; a later run reads mockpit.feedback().\nreturn url;";
4
+ export declare const RUN_EXAMPLE = "const html = (tone) => `<section data-part=\"hero\" class=\"${tone}\"><h1>Writer</h1></section>`;\nawait mockpit.publish({ design: \"writer\", variant: \"calm\", html: html(\"calm\") });\nawait mockpit.publish({ design: \"writer\", variant: \"bold\", html: html(\"bold\") });\nconst { url } = await mockpit.ask(\"writer\", [{ id: \"tone\", text: \"Calm or bold?\",\n options: [{ label: \"Calm\", variant: \"calm\" }, { label: \"Bold\", variant: \"bold\" }] }]);\n// Tell the user where to look and end the turn; a later run reads mockpit.feedback().\nreturn url;";
5
5
  export declare const RUN_DESCRIPTION: string;
6
6
  export declare const RUN_INSTRUCTIONS: string;