mockpit 1.2.0 → 2.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 (59) hide show
  1. package/README.md +69 -48
  2. package/bin/demoData.js +3 -3
  3. package/bin/mockpit.js +426 -51
  4. package/dist/mcp/server.js +45 -7
  5. package/dist/server/apiViews.d.ts +407 -21
  6. package/dist/server/apiViews.js +110 -11
  7. package/dist/server/app.d.ts +5 -1
  8. package/dist/server/app.js +1454 -192
  9. package/dist/server/codeWorker.js +2 -1
  10. package/dist/server/decisions.d.ts +23 -0
  11. package/dist/server/decisions.js +100 -0
  12. package/dist/server/designGuide.d.ts +6 -2
  13. package/dist/server/designGuide.js +96 -27
  14. package/dist/server/embeds.d.ts +26 -0
  15. package/dist/server/embeds.js +144 -0
  16. package/dist/server/eventLog.js +2 -1
  17. package/dist/server/feedbackBatch.d.ts +15 -4
  18. package/dist/server/feedbackBatch.js +31 -7
  19. package/dist/server/fills.d.ts +41 -0
  20. package/dist/server/fills.js +328 -0
  21. package/dist/server/guidance.d.ts +23 -0
  22. package/dist/server/guidance.js +71 -0
  23. package/dist/server/index.js +25 -2
  24. package/dist/server/knobs.d.ts +6 -2
  25. package/dist/server/knobs.js +11 -1
  26. package/dist/server/mcpHttp.d.ts +6 -0
  27. package/dist/server/mcpHttp.js +35 -0
  28. package/dist/server/mcpSpec.d.ts +14 -13
  29. package/dist/server/mcpSpec.js +133 -18
  30. package/dist/server/postSurfaces.js +6 -12
  31. package/dist/server/richRender.js +3 -1
  32. package/dist/server/run.js +10 -1
  33. package/dist/server/runApi.d.ts +3 -3
  34. package/dist/server/runApi.js +18 -13
  35. package/dist/server/sqlStore.d.ts +97 -5
  36. package/dist/server/sqlStore.js +367 -27
  37. package/dist/server/surfacePage.d.ts +6 -2
  38. package/dist/server/surfacePage.js +142 -10
  39. package/dist/server/themes.d.ts +2 -0
  40. package/dist/server/themes.js +23 -3
  41. package/dist/server/types.d.ts +107 -16
  42. package/dist/server/types.js +73 -10
  43. package/dist/server/viewerRequest.d.ts +2 -0
  44. package/dist/server/viewerRequest.js +14 -0
  45. package/extensions/mockpit.js +179 -16
  46. package/guide/packs/design-craft.md +35 -0
  47. package/guide/topics/asks.md +82 -31
  48. package/guide/topics/feedback.md +151 -43
  49. package/guide/topics/html.md +8 -10
  50. package/guide/topics/http.md +8 -4
  51. package/guide/topics/knobs.md +4 -3
  52. package/guide/topics/scripts.md +2 -2
  53. package/guide/topics/surfaces.md +45 -11
  54. package/package.json +3 -15
  55. package/skills/launch-video/SKILL.md +2 -2
  56. package/skills/mockpit/SKILL.md +23 -7
  57. package/viewer/dist/index.html +9 -8
  58. package/dist/server/slots.d.ts +0 -14
  59. package/dist/server/slots.js +0 -71
@@ -19,6 +19,7 @@ export declare const mcpSurfaceSchema: z.ZodObject<{
19
19
  }>>;
20
20
  html: z.ZodOptional<z.ZodString>;
21
21
  kits: z.ZodOptional<z.ZodArray<z.ZodString>>;
22
+ poster: z.ZodOptional<z.ZodString>;
22
23
  markdown: z.ZodOptional<z.ZodString>;
23
24
  mermaid: z.ZodOptional<z.ZodString>;
24
25
  patch: z.ZodOptional<z.ZodString>;
@@ -53,7 +54,7 @@ type Param = {
53
54
  d?: string;
54
55
  req?: boolean;
55
56
  } | {
56
- t: "surfaces" | "asks" | "object";
57
+ t: "surfaces" | "asks" | "object" | "integers" | "strings";
57
58
  d?: string;
58
59
  req?: boolean;
59
60
  };
@@ -131,9 +132,9 @@ export declare const STDIO_MCP_TOOLS: ({
131
132
  _meta: {
132
133
  "anthropic/alwaysLoad": boolean;
133
134
  };
134
- outputSchema?: z.ZodObject<Readonly<{
135
- [k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
136
- }>, z.core.$loose> | undefined;
135
+ outputSchema?: z.ZodObject<{
136
+ [x: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
137
+ }, z.core.$loose> | undefined;
137
138
  name: string;
138
139
  description: string;
139
140
  inputSchema: Readonly<{
@@ -141,9 +142,9 @@ export declare const STDIO_MCP_TOOLS: ({
141
142
  }>;
142
143
  } | {
143
144
  _meta?: undefined;
144
- outputSchema?: z.ZodObject<Readonly<{
145
- [k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
146
- }>, z.core.$loose> | undefined;
145
+ outputSchema?: z.ZodObject<{
146
+ [x: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
147
+ }, z.core.$loose> | undefined;
147
148
  name: string;
148
149
  description: string;
149
150
  inputSchema: Readonly<{
@@ -154,9 +155,9 @@ export declare const STDIO_RUN_TOOLS: ({
154
155
  _meta: {
155
156
  "anthropic/alwaysLoad": boolean;
156
157
  };
157
- outputSchema?: z.ZodObject<Readonly<{
158
- [k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
159
- }>, z.core.$loose> | undefined;
158
+ outputSchema?: z.ZodObject<{
159
+ [x: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
160
+ }, z.core.$loose> | undefined;
160
161
  name: string;
161
162
  description: string;
162
163
  inputSchema: Readonly<{
@@ -164,9 +165,9 @@ export declare const STDIO_RUN_TOOLS: ({
164
165
  }>;
165
166
  } | {
166
167
  _meta?: undefined;
167
- outputSchema?: z.ZodObject<Readonly<{
168
- [k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
169
- }>, z.core.$loose> | undefined;
168
+ outputSchema?: z.ZodObject<{
169
+ [x: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
170
+ }, z.core.$loose> | undefined;
170
171
  name: string;
171
172
  description: string;
172
173
  inputSchema: Readonly<{
@@ -10,14 +10,15 @@ export const MCP_SERVER_INFO = { name: "mockpit", version: "0.2.0" };
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
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 " +
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
19
  mock: "Mock slug or id",
20
20
  state: "State label; omit if single-state",
21
+ situation: 'Another render of this state ("Couldn\'t save", "Loading"); publish the state itself first',
21
22
  variant: 'Variant label; default "default"',
22
23
  project: "Default: the session's project",
23
24
  title: "Mock title",
@@ -28,8 +29,17 @@ const field = {
28
29
  knobs: 'Global knobs (tunekit usePane shape) keyed by path: [default,min,max,step], boolean, {type:"select",options}, "#hex"',
29
30
  variantKnobs: "Knobs this variant alone has",
30
31
  from: "Branch from this version",
32
+ fork: 'Start this new variant from a sibling: "variant" or "variant@N"; alone it copies it, with parts/fills it edits the copy',
31
33
  prompt: "What prompted this version; usually omit",
34
+ note: "What this variant is; shown beside it, never in the html",
35
+ widths: "Widths it ships at, side by side: [390,1280]",
36
+ kind: "Only when building the product's screens: page (a screen), component (a piece pages embed). Omit for sketches, diagrams and reviews",
37
+ area: 'Product area in the project\'s words; Home groups by it. "" clears',
38
+ source: 'Code path implementing it. "" clears',
39
+ flow: "Flow this state is a step of; alone, only places it",
40
+ step: "1-based place; 0 removes",
32
41
  kits: `Opt-in bundles: ${KIT_IDS.join("|")}`,
42
+ poster: "Still of the page (asset id) shown instead of running it; resend when the page changes",
33
43
  };
34
44
  const diffFileSchema = z.object({
35
45
  filename: z.string(),
@@ -48,6 +58,7 @@ export const mcpSurfaceSchema = z
48
58
  kind: z.enum(SURFACE_KIND_ENUM).optional(),
49
59
  html: z.string().optional().describe(field.html),
50
60
  kits: z.array(z.string()).optional().describe(field.kits),
61
+ poster: z.string().optional().describe(field.poster),
51
62
  markdown: z.string().optional().describe("Prose; raw HTML is escaped"),
52
63
  mermaid: z.string().optional().describe("Diagram source; do not set colors"),
53
64
  patch: z.string().optional().describe("Preferred over files"),
@@ -69,17 +80,23 @@ export const mcpSurfaceSchema = z
69
80
  description: "Set kind and its field: html, markdown, mermaid, patch|files (diff), assetId (image), text (terminal), data (json), code",
70
81
  });
71
82
  const askSchema = z.object({
72
- id: z.string().optional().describe("Stable id; reusing one replaces that ask"),
83
+ id: z.string().optional().describe("Stable id; reusing one replaces that question"),
73
84
  text: z.string().describe("The question, one sentence"),
74
85
  scope: z.enum(["mock", "state", "part"]).optional(),
75
86
  state: z.string().optional(),
76
87
  part: z.string().optional().describe("data-part name, for scope part"),
77
88
  multi: z.boolean().optional(),
89
+ after: z
90
+ .string()
91
+ .optional()
92
+ .describe("Id of an earlier ask this depends on; it opens once that one is answered"),
78
93
  options: z.array(z.object({
79
94
  id: z.string().optional(),
80
95
  label: z.string(),
81
96
  variant: z.string().optional().describe("Bind to a published variant"),
97
+ version: z.number().int().optional().describe("And one version of it"),
82
98
  set: z.record(z.string(), z.unknown()).optional().describe("Or to knob values {path: value}"),
99
+ recommended: z.boolean().optional().describe("The one you would pick; one per ask"),
83
100
  })),
84
101
  });
85
102
  function zodParam(p) {
@@ -100,6 +117,12 @@ function zodParam(p) {
100
117
  case "integer":
101
118
  s = z.number().int();
102
119
  break;
120
+ case "integers":
121
+ s = z.array(z.number().int());
122
+ break;
123
+ case "strings":
124
+ s = z.array(z.string());
125
+ break;
103
126
  default:
104
127
  s = p.t === "string" ? z.string() : p.t === "number" ? z.number() : z.boolean();
105
128
  }
@@ -118,21 +141,34 @@ const VARIANT_REF = { mock: P.mock, state: P.state, variant: P.variant, project:
118
141
  export const MCP_TOOL_DEFS = [
119
142
  {
120
143
  name: "publish",
121
- description: "Create or version one variant of one state of a mock. One of html, surfaces (the full ordered list) or parts (splices the latest version). Returns parts per state, nudges, feedback.",
144
+ 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.",
122
145
  params: {
123
146
  ...VARIANT_REF,
147
+ situation: { t: "string", d: field.situation },
124
148
  title: { t: "string", d: field.title },
125
- kind: { t: "enum", values: ["component", "page"] },
149
+ kind: { t: "enum", values: ["page", "component", "sketch"], d: field.kind },
150
+ area: { t: "string", d: field.area },
151
+ source: { t: "string", d: field.source },
126
152
  html: { t: "string", d: field.html },
153
+ poster: { t: "string", d: field.poster },
127
154
  surfaces: {
128
155
  t: "surfaces",
129
156
  d: "Full ordered list: {id} keeps a surface, a missing id removes it",
130
157
  },
131
- parts: { t: "object", d: '{"name"|"name#key": outer html}; splices marked parts' },
158
+ parts: { t: "object", d: '{"name"|"name#key": outer html}; splices declared parts' },
159
+ fills: {
160
+ t: "object",
161
+ d: '{"part#fill": outer html}; replaces that fill of a part, or adds it after its others',
162
+ },
132
163
  knobs: { t: "object", d: field.knobs },
133
164
  variantKnobs: { t: "object", d: field.variantKnobs },
134
165
  from: { t: "integer", d: field.from },
166
+ fork: { t: "string", d: field.fork },
135
167
  prompt: { t: "string", d: field.prompt },
168
+ note: { t: "string", d: field.note },
169
+ widths: { t: "integers", d: field.widths },
170
+ flow: { t: "string", d: field.flow },
171
+ step: { t: "integer", d: field.step },
136
172
  session: P.session,
137
173
  sessionTitle: { t: "string", d: field.sessionTitle },
138
174
  agent: { t: "string", d: field.agent },
@@ -141,7 +177,7 @@ export const MCP_TOOL_DEFS = [
141
177
  },
142
178
  {
143
179
  name: "ask",
144
- 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.",
180
+ description: "Ask structured questions on a mock. A choice is several variants plus one question that binds them; options may also set knob values. Reusing a question id replaces it.",
145
181
  params: {
146
182
  mock: P.mock,
147
183
  project: P.project,
@@ -152,17 +188,21 @@ export const MCP_TOOL_DEFS = [
152
188
  },
153
189
  {
154
190
  name: "read",
155
- description: "Without mock: every mock (slug, states, variants, open asks) plus pending. With mock: its states, variants, asks with answers, parts, knobs and last tuned values, plus pending. Never consumes feedback.",
191
+ description: "Without mock: every mock (slug, states, variants, open questions), flows, plus pending. With mock: its states, variants, questions with answers, parts, knobs and settled values (every answer's set and tuning sent so far), plus pending. Never consumes feedback.",
156
192
  params: {
157
193
  mock: { t: "string", d: field.mock },
158
194
  project: P.project,
159
195
  body: { t: "boolean", d: "Include each variant's surfaces" },
160
196
  history: { t: "boolean", d: "Include version rows" },
197
+ tree: {
198
+ t: "boolean",
199
+ d: "Without mock: the project as a tree (each mock's variants with status and forkedFrom, the mocks it embeds and is embedded in)",
200
+ },
161
201
  },
162
202
  },
163
203
  {
164
204
  name: "feedback",
165
- 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), delivered once, plus pending (viewer open, draft progress).",
205
+ description: "Returns at once: what the user sent since you last heard (one batch per mock: reply answers, borrowed parts, tuned knobs, part comments, verdict; thread messages; inUse/archived; or closed with the questions it dismissed), delivered once, plus pending (viewer open, draft progress).",
166
206
  params: {
167
207
  project: P.project,
168
208
  session: { t: "string", d: "Session id returned by publish", req: true },
@@ -181,9 +221,26 @@ export const MCP_TOOL_DEFS = [
181
221
  },
182
222
  {
183
223
  name: "export",
184
- description: "The accepted (or current) html per state, version history, screenshot URL, knobs and the last reply's tuned values.",
224
+ description: "The html in use (else current) per state, version history, screenshot URL, knobs, the settled values and the last reply.",
185
225
  params: VARIANT_REF,
186
226
  },
227
+ {
228
+ name: "edit",
229
+ description: "Change a mock without a new version; a new slug rewrites every reference to the old one. Without mock, rename renames the project and areaOrder orders its areas. Returns feedback.",
230
+ params: {
231
+ mock: { t: "string", d: field.mock },
232
+ project: P.project,
233
+ slug: { t: "string" },
234
+ title: { t: "string" },
235
+ kind: { t: "enum", values: ["page", "component", "sketch"] },
236
+ area: { t: "string" },
237
+ source: { t: "string" },
238
+ rename: { t: "string", d: "New project name" },
239
+ areaOrder: { t: "strings", d: "Without mock: Home's area order" },
240
+ session: P.session,
241
+ },
242
+ httpOnly: ["session"],
243
+ },
187
244
  {
188
245
  name: "upload",
189
246
  description: "Upload bytes and return id and URL. Reference id as an image surface's assetId.",
@@ -235,7 +292,9 @@ const nullStr = z.string().nullable();
235
292
  const strs = z.array(z.string());
236
293
  const bag = z.record(z.string(), z.unknown());
237
294
  const variantRef = o({ state: nullStr, variant: str });
295
+ const inUseRef = o({ state: nullStr, variant: str, version: num });
238
296
  const partsByState = z.array(o({ state: nullStr, parts: z.array(o({ name: str })) }));
297
+ const flowSchema = o({ name: str, steps: z.array(o({ mock: str, state: nullStr })) });
239
298
  const feedbackSchema = o({
240
299
  mock: nullStr,
241
300
  reply: o({
@@ -244,22 +303,37 @@ const feedbackSchema = o({
244
303
  chosen: z.array(o({ label: str, other: z.literal(true).optional() })),
245
304
  note: str.optional(),
246
305
  })),
247
- mix: bag,
306
+ borrowed: bag.describe("Part → the variant to take it from"),
248
307
  tuned: bag,
249
308
  comments: z.array(o({ part: nullStr, text: str })),
250
309
  reactions: strs.optional().describe('One-tap reactions, e.g. 👍 part "hero" (variant B)'),
310
+ verdict: o({ kind: z.enum(["use", "revise"]), state: nullStr, variant: str })
311
+ .optional()
312
+ .describe("Use this or Revise on one variant"),
251
313
  })
252
314
  .nullable()
253
- .describe("The user's Send; null if only comments"),
254
- comments: z.array(o({ seq: num, text: str })),
255
- accepted: z.array(variantRef),
315
+ .describe("The user's Send; null if only messages"),
316
+ messages: z.array(o({ seq: num, text: str })).describe("What the user wrote in the thread"),
317
+ inUse: z.array(inUseRef).describe("In use, each at the version the user put in use"),
256
318
  archived: z.array(variantRef),
319
+ closed: z
320
+ .literal(true)
321
+ .optional()
322
+ .describe("The user closed the mock: its open questions were closed unanswered"),
323
+ dismissed: z
324
+ .array(o({ ask: str, text: str }))
325
+ .optional()
326
+ .describe("The questions the close dismissed; don't ask them again"),
327
+ edited: o({ kind: str.optional(), area: nullStr.optional() })
328
+ .optional()
329
+ .describe("The user moved the mock from Home: its kind or area now"),
257
330
  }).meta({ id: "Feedback" });
258
331
  // Typed once, on feedback: repeating it on every write would cost more than
259
332
  // the rest of the write's output schema.
260
333
  const feedbackRide = z.array(bag).describe("Feedback batches as the feedback tool returns");
261
334
  const pendingSchema = o({
262
335
  mock: str,
336
+ closed: z.literal(true).optional(),
263
337
  viewerOpen: z.boolean(),
264
338
  draft: o({ answered: num, of: num, comments: num, touchedAt: str }).nullable(),
265
339
  }).meta({ id: "Pending" });
@@ -271,6 +345,11 @@ const writeOutput = {
271
345
  partChanges: o({ vanished: strs, renamed: z.array(o({ from: str, to: str })) }).optional(),
272
346
  nudges: strs.optional(),
273
347
  suggestedAsk: bag.optional().describe("Send with ask when a nudge names it"),
348
+ suggestedFillAsks: z
349
+ .array(bag)
350
+ .optional()
351
+ .describe("A question per part with fills that none sets; send with ask"),
352
+ flow: flowSchema.optional(),
274
353
  feedback: feedbackRide,
275
354
  };
276
355
  const OUTPUT_SCHEMAS = {
@@ -303,17 +382,51 @@ const OUTPUT_SCHEMAS = {
303
382
  pending: z.array(pendingSchema),
304
383
  },
305
384
  say: { feedback: feedbackRide },
385
+ edit: {
386
+ renamed: o({ from: str, to: str }).optional(),
387
+ areaOrder: strs.optional(),
388
+ url: str,
389
+ nudges: strs.optional(),
390
+ feedback: feedbackRide,
391
+ },
306
392
  read: {
307
393
  mocks: z.array(o({ slug: str, states: strs, variants: num, open: num })).optional(),
308
394
  states: strs.optional(),
309
395
  asks: z.array(o({ id: str, text: str })).optional(),
310
- variants: z.array(o({ state: nullStr, variant: str, status: str, version: num })).optional(),
396
+ variants: z
397
+ .array(o({
398
+ state: nullStr,
399
+ variant: str,
400
+ status: z.enum(["open", "inUse", "archived"]),
401
+ version: num,
402
+ inUseVersion: num.optional().describe("Set while in use: the version in use"),
403
+ history: z
404
+ .array(o({ version: num, hidden: z.boolean().optional() }))
405
+ .optional()
406
+ .describe("With history: version rows; hidden ones the user put away are flagged"),
407
+ }))
408
+ .optional(),
409
+ inUse: z
410
+ .array(inUseRef)
411
+ .optional()
412
+ .describe("Per state, the version the user put in use; export and embeds resolve to it"),
311
413
  knobs: bag.optional(),
312
- tuned: bag.optional().describe("Knob values from the latest reply"),
414
+ settled: bag.optional().describe("Every knob value the user has sent, later replies winning"),
415
+ tree: z.array(bag).optional(),
416
+ flows: z.array(flowSchema).optional(),
313
417
  pending: z.union([z.array(pendingSchema), pendingSchema]),
314
418
  },
315
419
  export: {
316
- states: z.array(o({ state: nullStr, variant: str, version: num, html: str, markdown: str })),
420
+ states: z.array(o({
421
+ state: nullStr,
422
+ variant: str,
423
+ version: num,
424
+ latest: num.optional().describe("Your newer version, not in use yet"),
425
+ html: str.describe("With fills: only the chosen ones"),
426
+ allFills: str.optional().describe("With fills: the html with every fill"),
427
+ markdown: str,
428
+ })),
429
+ settled: bag,
317
430
  reply: bag.nullable(),
318
431
  },
319
432
  };
@@ -330,7 +443,9 @@ function shapeFor(tool, transport) {
330
443
  shape[key] =
331
444
  transport === "stdio" && key === "html" && tool.name === "publish"
332
445
  ? z.string().optional().describe(`${field.html}; or a file path`)
333
- : zodParam(p);
446
+ : transport === "stdio" && key === "poster" && tool.name === "publish"
447
+ ? z.string().optional().describe(`${p.d}; or an image file path`)
448
+ : zodParam(p);
334
449
  }
335
450
  return shape;
336
451
  }
@@ -6,23 +6,14 @@ const MERMAID_PARSE_NEXT_STEPS = [
6
6
  'Keep the diagram declaration on the first non-comment line (for example, "flowchart TD").',
7
7
  "Reduce the diagram to a minimal valid form, then add statements back until the failing line is isolated.",
8
8
  ];
9
- const requiredString = (name) => z.string({
10
- required_error: `requires string "${name}"`,
11
- invalid_type_error: `requires string "${name}"`,
12
- });
9
+ const requiredString = (name) => z.string({ error: `requires string "${name}"` });
13
10
  const optionalLooseString = z.preprocess((v) => (typeof v === "string" ? v : undefined), z.string().optional());
14
11
  const looseLayout = z.preprocess((v) => (v === "unified" || v === "split" ? v : undefined), z.enum(["unified", "split"]).optional());
15
12
  const optionalLooseNumber = z.preprocess((v) => (typeof v === "number" && Number.isFinite(v) ? v : undefined), z.number().optional());
16
13
  const strictDiffFile = z.object({
17
14
  filename: requiredString("filename"),
18
- before: z.string({
19
- required_error: 'requires string "before" and "after"',
20
- invalid_type_error: 'requires string "before" and "after"',
21
- }),
22
- after: z.string({
23
- required_error: 'requires string "before" and "after"',
24
- invalid_type_error: 'requires string "before" and "after"',
25
- }),
15
+ before: z.string({ error: 'requires string "before" and "after"' }),
16
+ after: z.string({ error: 'requires string "before" and "after"' }),
26
17
  language: z.string().optional(),
27
18
  });
28
19
  const looseDiffFile = z
@@ -53,17 +44,20 @@ const strictHtmlSurface = z.object({
53
44
  kind: z.literal("html"),
54
45
  html: requiredString("html"),
55
46
  kits: z.array(z.string()).optional(),
47
+ poster: z.string().min(1).optional(),
56
48
  });
57
49
  const looseHtmlSurface = z
58
50
  .object({
59
51
  kind: z.literal("html"),
60
52
  html: requiredString("html"),
61
53
  kits: z.unknown().optional(),
54
+ poster: optionalLooseString,
62
55
  })
63
56
  .transform((p) => ({
64
57
  kind: "html",
65
58
  html: p.html,
66
59
  ...(Array.isArray(p.kits) ? { kits: p.kits.filter((k) => typeof k === "string") } : {}),
60
+ ...(p.poster ? { poster: p.poster } : {}),
67
61
  }));
68
62
  // Strict mode rejects an unknown id with the valid set, so a CLI/REST typo is a
69
63
  // clean 400; loose mode filters unknown ids out rather than dropping the whole
@@ -139,7 +139,9 @@ blockquote {
139
139
  color: var(--muted);
140
140
  }
141
141
  table { border-collapse: collapse; font-size: 13px; }
142
- th, td { border: 0.5px solid var(--border); padding: 4px 8px; text-align: left; }
142
+ /* break-word, not the body's anywhere: anywhere lets a column shrink to one
143
+ letter, so a long cell beside it squeezes "Topic" into "Top/ic". */
144
+ th, td { border: 0.5px solid var(--border); padding: 4px 8px; text-align: left; overflow-wrap: break-word; }
143
145
  th { background: var(--hover); }
144
146
  img { max-width: 100%; height: auto; border-radius: 6px; }
145
147
  hr { border: none; border-top: 0.5px solid var(--border); margin: 1em 0; }
@@ -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
@@ -153,6 +153,10 @@ export function createRunFlow(deps) {
153
153
  return [await deps.guide("", topic), String(topic)];
154
154
  },
155
155
  async read([mock, opts]) {
156
+ if ((mock === undefined || mock === null) && plain(opts).tree === true) {
157
+ const value = await take(flows.tree({ project: await projectOf() }));
158
+ return [value, `${value.tree.length} mocks`];
159
+ }
156
160
  if (mock === undefined || mock === null) {
157
161
  const value = await take(flows.list({ project: await projectOf() }));
158
162
  return [value, `${value.mocks.length} mocks`];
@@ -179,6 +183,11 @@ export function createRunFlow(deps) {
179
183
  await take(flows.say(r.mock, { ...(await mine(r)), message }, flowCtx));
180
184
  return [null, String(r.mock ?? "?")];
181
185
  },
186
+ async edit([v]) {
187
+ const r = await mine(plain(v));
188
+ const value = await take(editCall(flows, r, flowCtx, project));
189
+ return [value, value.renamed ? `${value.renamed.from} → ${value.renamed.to}` : "edited"];
190
+ },
182
191
  async upload([data, opts]) {
183
192
  if (typeof data !== "string" || !data)
184
193
  throw new Error("upload needs base64 data");
@@ -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 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 publish(v: Ref & { title?: string; kind?: \"component\" | \"page\"; html?: string; surfaces?: (Surface | { id: string })[]; parts?: Record<string, string>; knobs?: Knobs; variantKnobs?: Knobs; from?: 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 = { 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; recommended?: boolean }[];\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; suggestedFillAsks?: Ask[]; // send 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 }[]; 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 closed?: true; dismissed?: { ask: string; text: string }[];\n};\ntype Pending = { mock: string; closed?: 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 /** 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 fill of a part. */\n publish(v: Ref & { situation?: string; title?: string; kind?: \"page\" | \"component\" | \"sketch\"; 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; 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 /** No new version; \"\" clears. { rename } or { areaOrder } without mock edits the project. */\n edit(v: { mock?: string; slug?: string; title?: string; kind?: string; area?: string; source?: string; rename?: string; areaOrder?: string[] }): Promise<{ url: 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", "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({ mock: \"writer\", variant: \"calm\", html: html(\"calm\") });\nawait mockpit.publish({ mock: \"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;
@@ -2,8 +2,8 @@
2
2
  // tool description, the scripts guide topic and `mockpit run --help`, so the
3
3
  // tiers cannot describe different APIs. Hand-shaped rather than generated from
4
4
  // mcpSpec: a generator repeats Surface per tool and the run-level params on
5
- // every function, roughly tripling the size. test/run.test.ts checks that every
6
- // host function named here exists and vice versa.
5
+ // every function, roughly tripling the size. Every host function named here
6
+ // must exist, and vice versa.
7
7
  export const RUN_API = `// The body of an async function, in plain JavaScript (no TypeScript syntax).
8
8
  // \`await mockpit.*\`, \`return\` a JSON value, print() adds an output line.
9
9
  // No network, timers, imports or process. Session, project and agent are fixed
@@ -21,33 +21,37 @@ type Surface =
21
21
  | { kind: "code"; code: string; language?: string; lineStart?: number };
22
22
  type Ask = {
23
23
  id?: string; text: string; scope?: "mock" | "state" | "part"; state?: string; part?: string; multi?: boolean;
24
- options: { id?: string; label: string; variant?: string; set?: Knobs }[];
24
+ options: { id?: string; label: string; variant?: string; set?: Knobs; recommended?: boolean }[];
25
25
  };
26
26
  type Written = {
27
27
  post: { state: string | null; variant: string; version: number }; url: string;
28
28
  parts: { state: string | null; parts: { name: string }[] }[];
29
29
  partChanges?: { vanished: string[]; renamed: { from: string; to: string }[] };
30
- nudges?: string[]; suggestedAsk?: Ask; // send it with ask when nudged
30
+ nudges?: string[]; suggestedAsk?: Ask; suggestedFillAsks?: Ask[]; // send with ask when nudged
31
31
  };
32
32
  type Feedback = {
33
33
  mock: string | null;
34
- reply: null | { asks: { ask: string; chosen: { id: string; label: string; other?: true }[]; note?: string }[]; mix: Knobs; tuned: Knobs; comments: { part: string | null; text: string }[] };
35
- comments: { text: string; state: string | null; variant: string | null }[];
36
- accepted: { state: string | null; variant: string }[]; archived: { state: string | null; variant: string }[];
34
+ reply: null | { asks: { ask: string; chosen: { id: string; label: string; other?: true }[]; note?: string }[]; borrowed: Record<string, string>; tuned: Knobs; comments: { part: string | null; text: string }[]; verdict?: { kind: "use" | "revise"; state: string | null; variant: string; version?: number } };
35
+ messages: { text: string; state: string | null; variant: string | null }[];
36
+ inUse: { state: string | null; variant: string; version: number }[]; archived: { state: string | null; variant: string }[];
37
+ closed?: true; dismissed?: { ask: string; text: string }[];
37
38
  };
38
- type Pending = { mock: string; viewerOpen: boolean; draft: null | { answered: number; of: number; comments: number; touchedAt: string } };
39
+ type Pending = { mock: string; closed?: true; viewerOpen: boolean; draft: null | { answered: number; of: number; comments: number; touchedAt: string } };
39
40
  declare const mockpit: {
40
41
  guide(topic?: "knobs" | "asks" | "surfaces" | "html" | "feedback" | "http" | "scripts"): Promise<string>;
41
- /** No mock: every mock plus pending. With one: its states, variants, asks, parts, knobs. */
42
+ /** No mock: every mock, the flows, plus pending. With one: its states, variants, asks, parts, knobs. */
42
43
  read(mock?: string, opts?: { body?: boolean; history?: boolean }): Promise<unknown>;
43
44
  /** Creates or versions (mock, state, variant). One of: html; surfaces, the full ordered list
44
- * ({id} alone keeps that surface, a missing id is removed); parts {"name"|"name#key": outer html}. */
45
- publish(v: Ref & { title?: string; kind?: "component" | "page"; html?: string; surfaces?: (Surface | { id: string })[]; parts?: Record<string, string>; knobs?: Knobs; variantKnobs?: Knobs; from?: number }): Promise<Written>;
45
+ * ({id} alone keeps that surface, a missing id is removed); parts {"name"|"name#key": outer html};
46
+ * fills {"part#fill": outer html}, replacing or adding one fill of a part. */
47
+ publish(v: Ref & { situation?: string; title?: string; kind?: "page" | "component" | "sketch"; 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; note?: string; widths?: number[]; flow?: string; step?: number }): Promise<Written>;
46
48
  ask(mock: string, asks: Ask[]): Promise<{ asks: { id: string; text: string }[]; url: string; nudges?: string[] }>;
47
49
  /** Never waits: what the user sent since you last heard, and what they are doing now. */
48
50
  feedback(): Promise<{ feedback: Feedback[]; pending: Pending[] }>;
49
51
  say(v: Ref, message: string): Promise<void>;
50
- export(v: Ref): Promise<{ states: { state: string | null; variant: string; version: number; html: string }[]; reply: unknown }>;
52
+ /** No new version; "" clears. { rename } or { areaOrder } without mock edits the project. */
53
+ edit(v: { mock?: string; slug?: string; title?: string; kind?: string; area?: string; source?: string; rename?: string; areaOrder?: string[] }): Promise<{ url: string }>;
54
+ export(v: Ref): Promise<{ states: { state: string | null; variant: string; version: number; html: string; allFills?: string }[]; reply: unknown }>;
51
55
  /** data: base64 bytes. Use id as an image surface's assetId. */
52
56
  upload(data: string, opts: { contentType: string; filename?: string; kind?: "image" | "file" }): Promise<{ id: string; url: string }>;
53
57
  };
@@ -60,13 +64,14 @@ export const RUN_FUNCTIONS = [
60
64
  "ask",
61
65
  "feedback",
62
66
  "say",
67
+ "edit",
63
68
  "export",
64
69
  "upload",
65
70
  ];
66
71
  export const RUN_EXAMPLE = `const html = (tone) => \`<section data-part="hero" class="\${tone}"><h1>Writer</h1></section>\`;
67
72
  await mockpit.publish({ mock: "writer", variant: "calm", html: html("calm") });
68
73
  await mockpit.publish({ mock: "writer", variant: "bold", html: html("bold") });
69
- const { url } = await mockpit.ask("writer", [{ id: "look", text: "Which look?",
74
+ const { url } = await mockpit.ask("writer", [{ id: "tone", text: "Calm or bold?",
70
75
  options: [{ label: "Calm", variant: "calm" }, { label: "Bold", variant: "bold" }] }]);
71
76
  // Tell the user where to look and end the turn; a later run reads mockpit.feedback().
72
77
  return url;`;