mockpit 1.1.0 → 1.3.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 (62) hide show
  1. package/README.md +13 -6
  2. package/bin/mockpit.js +139 -18
  3. package/dist/mcp/server.js +30 -10
  4. package/dist/server/apiViews.d.ts +129 -25
  5. package/dist/server/apiViews.js +86 -23
  6. package/dist/server/app.d.ts +2 -1
  7. package/dist/server/app.js +1258 -200
  8. package/dist/server/appIcon.d.ts +22 -0
  9. package/dist/server/appIcon.js +43 -0
  10. package/dist/server/appIconPng.d.ts +1 -0
  11. package/dist/server/appIconPng.js +6 -0
  12. package/dist/server/askNudges.d.ts +3 -0
  13. package/dist/server/askNudges.js +40 -0
  14. package/dist/server/base64.d.ts +1 -0
  15. package/dist/server/base64.js +8 -0
  16. package/dist/server/decisions.d.ts +23 -0
  17. package/dist/server/decisions.js +99 -0
  18. package/dist/server/designGuide.js +53 -16
  19. package/dist/server/embeds.d.ts +25 -0
  20. package/dist/server/embeds.js +118 -0
  21. package/dist/server/eventLog.js +2 -1
  22. package/dist/server/feedbackBatch.d.ts +28 -5
  23. package/dist/server/feedbackBatch.js +78 -11
  24. package/dist/server/fills.d.ts +41 -0
  25. package/dist/server/fills.js +328 -0
  26. package/dist/server/handoff.d.ts +6 -0
  27. package/dist/server/handoff.js +38 -0
  28. package/dist/server/index.js +15 -1
  29. package/dist/server/knobs.d.ts +6 -2
  30. package/dist/server/knobs.js +13 -4
  31. package/dist/server/mcpHttp.d.ts +5 -2
  32. package/dist/server/mcpHttp.js +14 -5
  33. package/dist/server/mcpSpec.d.ts +21 -13
  34. package/dist/server/mcpSpec.js +99 -9
  35. package/dist/server/postSurfaces.js +3 -12
  36. package/dist/server/richRender.js +3 -1
  37. package/dist/server/run.js +6 -2
  38. package/dist/server/runApi.d.ts +1 -1
  39. package/dist/server/runApi.js +5 -4
  40. package/dist/server/sqlStore.d.ts +92 -5
  41. package/dist/server/sqlStore.js +188 -30
  42. package/dist/server/surfacePage.d.ts +6 -2
  43. package/dist/server/surfacePage.js +142 -10
  44. package/dist/server/themes.d.ts +2 -0
  45. package/dist/server/themes.js +23 -3
  46. package/dist/server/types.d.ts +95 -10
  47. package/dist/server/types.js +89 -6
  48. package/dist/server/viewerRequest.d.ts +2 -0
  49. package/dist/server/viewerRequest.js +14 -0
  50. package/extensions/mockpit.js +1 -0
  51. package/guide/topics/asks.md +69 -10
  52. package/guide/topics/feedback.md +104 -16
  53. package/guide/topics/html.md +5 -1
  54. package/guide/topics/http.md +2 -1
  55. package/guide/topics/knobs.md +4 -3
  56. package/guide/topics/surfaces.md +32 -16
  57. package/package.json +4 -13
  58. package/skills/launch-video/SKILL.md +1 -1
  59. package/skills/mockpit/SKILL.md +14 -4
  60. package/viewer/dist/index.html +9 -9
  61. package/dist/server/slots.d.ts +0 -14
  62. package/dist/server/slots.js +0 -71
@@ -10,6 +10,10 @@ export declare function explicitKnob(config: KnobConfig): ExplicitKnob;
10
10
  export declare function checkKnobConfig(path: string, raw: unknown): Checked<KnobConfig>;
11
11
  export declare function checkKnobs(raw: unknown): Checked<Knobs>;
12
12
  export declare function checkKnobValue(path: string, config: KnobConfig, value: unknown): Checked<KnobValue>;
13
- export declare function knobFor(path: string, mock: Pick<Mock, "knobs">, posts: Pick<Post, "knobs">[]): KnobConfig | undefined;
14
- export declare function checkKnobValues(raw: unknown, mock: Pick<Mock, "knobs">, posts: Pick<Post, "knobs">[], what: string): Checked<Record<string, KnobValue>>;
13
+ export declare function knobFor(path: string, mock: Pick<Mock, "knobs">, posts: (Pick<Post, "knobs"> & {
14
+ surfaces?: Post["surfaces"];
15
+ })[]): KnobConfig | undefined;
16
+ export declare function checkKnobValues(raw: unknown, mock: Pick<Mock, "knobs">, posts: (Pick<Post, "knobs"> & {
17
+ surfaces?: Post["surfaces"];
18
+ })[], what: string): Checked<Record<string, KnobValue>>;
15
19
  export declare function discreteChoices(config: KnobConfig): number | null;
@@ -1,6 +1,7 @@
1
1
  // Knob schema and value validation (plan Q3: values are checked against the
2
2
  // declared schema before they are stored or used). Runtime-agnostic. The config
3
3
  // shape mirrors tunekit's usePane config; see the knob types in types.ts.
4
+ import { slotKnobs, slotsInSurfaces } from "./fills.js";
4
5
  const MAX_KNOBS = 100;
5
6
  const MAX_OPTIONS = 100;
6
7
  const MAX_STRING = 500;
@@ -292,13 +293,22 @@ export function checkKnobValue(path, config, value) {
292
293
  }
293
294
  }
294
295
  }
295
- // The declared knob for a path: the mock's own, else a variant's override.
296
+ // The declared knob for a path: the mock's own, else a variant's override,
297
+ // else the knob a variant's slot implies (fills.ts), so an option can `set` a
298
+ // slot's fill without the agent declaring anything.
296
299
  export function knobFor(path, mock, posts) {
297
300
  if (Object.hasOwn(mock.knobs, path))
298
301
  return mock.knobs[path];
299
302
  for (const p of posts)
300
303
  if (p.knobs && Object.hasOwn(p.knobs, path))
301
304
  return p.knobs[path];
305
+ for (const p of posts) {
306
+ if (!p.surfaces)
307
+ continue;
308
+ const implied = slotKnobs(slotsInSurfaces(p.surfaces));
309
+ if (Object.hasOwn(implied, path))
310
+ return implied[path];
311
+ }
302
312
  return undefined;
303
313
  }
304
314
  export function checkKnobValues(raw, mock, posts, what) {
@@ -320,11 +330,10 @@ export function checkKnobValues(raw, mock, posts, what) {
320
330
  return { ok: true, value: out };
321
331
  }
322
332
  // How many discrete choices a knob offers, or null when it is continuous. A knob
323
- // with only a handful is usually a decision dressed as a control (plan D12).
333
+ // with only a handful is usually a decision dressed as a control (plan D12). A
334
+ // toggle is null: on/off reads fine on one render.
324
335
  export function discreteChoices(config) {
325
336
  const knob = explicitKnob(config);
326
- if (knob.type === "toggle")
327
- return 2;
328
337
  if (knob.type === "select")
329
338
  return knob.options.length;
330
339
  if (knob.type === "image" && knob.options)
@@ -8,6 +8,9 @@ export interface McpFlows {
8
8
  list: Flow<[query: {
9
9
  project?: string;
10
10
  }]>;
11
+ tree: Flow<[query: {
12
+ project?: string;
13
+ }]>;
11
14
  get: Flow<[ref: unknown, query: {
12
15
  project?: unknown;
13
16
  body?: boolean;
@@ -23,11 +26,11 @@ export interface McpFlows {
23
26
  },
24
27
  ctx: FlowContext
25
28
  ]>;
26
- say: Flow<[ref: unknown, body: any]>;
29
+ say: Flow<[ref: unknown, body: any, ctx: FlowContext]>;
27
30
  feedback: Flow<[query: {
28
31
  session?: unknown;
29
32
  project?: unknown;
30
- }]>;
33
+ }, ctx: FlowContext]>;
31
34
  }
32
35
  export interface McpDeps {
33
36
  store: Store;
@@ -1,6 +1,6 @@
1
1
  import { decodeBase64 } from "./base64.js";
2
2
  import { isGuideTopic, unknownTopicMessage } from "./designGuide.js";
3
- import { HTTP_MCP_TOOLS, HTTP_RUN_TOOLS, MCP_INSTRUCTIONS, MCP_SERVER_INFO, runToolResult, toolResult, } from "./mcpSpec.js";
3
+ import { HTTP_MCP_TOOLS, HTTP_RUN_TOOLS, MCP_INSTRUCTIONS, MCP_SERVER_INFO, runToolResult, toolResult, withFeedbackImages, } from "./mcpSpec.js";
4
4
  import { RUN_INSTRUCTIONS } from "./runApi.js";
5
5
  // A flow's error status becomes a tool error carrying the same JSON body the
6
6
  // REST tier returns, so the agent sees the hint (states, variants) either way.
@@ -24,6 +24,10 @@ export function registerMcp(app, deps) {
24
24
  const projects = await store.listProjects();
25
25
  return projects[0]?.name ?? "workspace";
26
26
  }
27
+ async function loadImage(ref) {
28
+ const asset = await store.getAsset(ref.id);
29
+ return asset ? { data: asset.data, mimeType: asset.contentType } : null;
30
+ }
27
31
  async function callTool(name, args, ctx) {
28
32
  switch (name) {
29
33
  case "publish":
@@ -31,6 +35,9 @@ export function registerMcp(app, deps) {
31
35
  case "ask":
32
36
  return unwrap(await flows.ask(args.mock, args, ctx));
33
37
  case "read":
38
+ if (args.mock === undefined && args.tree === true) {
39
+ return unwrap(await flows.tree({ project: args.project }));
40
+ }
34
41
  if (args.mock === undefined)
35
42
  return unwrap(await flows.list({ project: args.project }));
36
43
  return unwrap(await flows.get(args.mock, {
@@ -39,9 +46,9 @@ export function registerMcp(app, deps) {
39
46
  history: args.history === true,
40
47
  }));
41
48
  case "feedback":
42
- return unwrap(await flows.feedback({ session: args.session, project: args.project }));
49
+ return unwrap(await flows.feedback({ session: args.session, project: args.project }, ctx));
43
50
  case "say":
44
- return unwrap(await flows.say(args.mock, args));
51
+ return unwrap(await flows.say(args.mock, args, ctx));
45
52
  case "export":
46
53
  return unwrap(await flows.exportMock(args.mock, args, ctx));
47
54
  case "upload": {
@@ -124,9 +131,11 @@ export function registerMcp(app, deps) {
124
131
  const name = msg.params?.name;
125
132
  if (name === "run") {
126
133
  const args = msg.params?.arguments ?? {};
127
- return rpc(msg.id, runToolResult(unwrap(await deps.run(args, ctx))));
134
+ const envelope = unwrap(await deps.run(args, ctx));
135
+ return rpc(msg.id, await withFeedbackImages(runToolResult(envelope), envelope, loadImage));
128
136
  }
129
- return rpc(msg.id, toolResult(name, await callTool(name, msg.params?.arguments ?? {}, ctx)));
137
+ const value = await callTool(name, msg.params?.arguments ?? {}, ctx);
138
+ return rpc(msg.id, await withFeedbackImages(toolResult(name, value), value, loadImage));
130
139
  }
131
140
  catch (err) {
132
141
  return rpc(msg.id, {
@@ -1,4 +1,5 @@
1
1
  import * as z from "zod/v4";
2
+ import { type ImageRef } from "./feedbackBatch.ts";
2
3
  export declare const MCP_SERVER_INFO: {
3
4
  name: string;
4
5
  version: string;
@@ -52,7 +53,7 @@ type Param = {
52
53
  d?: string;
53
54
  req?: boolean;
54
55
  } | {
55
- t: "surfaces" | "asks" | "object";
56
+ t: "surfaces" | "asks" | "object" | "integers";
56
57
  d?: string;
57
58
  req?: boolean;
58
59
  };
@@ -130,9 +131,9 @@ export declare const STDIO_MCP_TOOLS: ({
130
131
  _meta: {
131
132
  "anthropic/alwaysLoad": boolean;
132
133
  };
133
- outputSchema?: z.ZodObject<Readonly<{
134
- [k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
135
- }>, z.core.$loose> | undefined;
134
+ outputSchema?: z.ZodObject<{
135
+ [x: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
136
+ }, z.core.$loose> | undefined;
136
137
  name: string;
137
138
  description: string;
138
139
  inputSchema: Readonly<{
@@ -140,9 +141,9 @@ export declare const STDIO_MCP_TOOLS: ({
140
141
  }>;
141
142
  } | {
142
143
  _meta?: undefined;
143
- outputSchema?: z.ZodObject<Readonly<{
144
- [k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
145
- }>, z.core.$loose> | undefined;
144
+ outputSchema?: z.ZodObject<{
145
+ [x: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
146
+ }, z.core.$loose> | undefined;
146
147
  name: string;
147
148
  description: string;
148
149
  inputSchema: Readonly<{
@@ -153,9 +154,9 @@ export declare const STDIO_RUN_TOOLS: ({
153
154
  _meta: {
154
155
  "anthropic/alwaysLoad": boolean;
155
156
  };
156
- outputSchema?: z.ZodObject<Readonly<{
157
- [k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
158
- }>, z.core.$loose> | undefined;
157
+ outputSchema?: z.ZodObject<{
158
+ [x: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
159
+ }, z.core.$loose> | undefined;
159
160
  name: string;
160
161
  description: string;
161
162
  inputSchema: Readonly<{
@@ -163,9 +164,9 @@ export declare const STDIO_RUN_TOOLS: ({
163
164
  }>;
164
165
  } | {
165
166
  _meta?: undefined;
166
- outputSchema?: z.ZodObject<Readonly<{
167
- [k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
168
- }>, z.core.$loose> | undefined;
167
+ outputSchema?: z.ZodObject<{
168
+ [x: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
169
+ }, z.core.$loose> | undefined;
169
170
  name: string;
170
171
  description: string;
171
172
  inputSchema: Readonly<{
@@ -180,6 +181,13 @@ export declare function toolResult(name: string, value: unknown): {
180
181
  text: string;
181
182
  }[];
182
183
  };
184
+ export declare const MAX_RESULT_IMAGES = 8;
185
+ export declare function withFeedbackImages<T extends {
186
+ content: unknown[];
187
+ }>(result: T, value: unknown, load: (ref: ImageRef) => Promise<{
188
+ data: Uint8Array;
189
+ mimeType: string;
190
+ } | null>): Promise<T>;
183
191
  export declare function runToolResult(envelope: unknown): {
184
192
  isError?: boolean | undefined;
185
193
  structuredContent?: Record<string, unknown> | undefined;
@@ -1,5 +1,7 @@
1
1
  import * as z from "zod/v4";
2
+ import { encodeBase64 } from "./base64.js";
2
3
  import { GUIDE_TOPICS } from "./designGuide.js";
4
+ import { feedbackImages } from "./feedbackBatch.js";
3
5
  import { KIT_IDS } from "./kits.js";
4
6
  import { RUN_DESCRIPTION } from "./runApi.js";
5
7
  import { SURFACE_KINDS } from "./types.js";
@@ -26,7 +28,12 @@ const field = {
26
28
  knobs: 'Global knobs (tunekit usePane shape) keyed by path: [default,min,max,step], boolean, {type:"select",options}, "#hex"',
27
29
  variantKnobs: "Knobs this variant alone has",
28
30
  from: "Branch from this version",
31
+ fork: 'Start this new variant from a sibling: "variant" or "variant@N"; alone it copies it, with parts/fills it edits the copy',
29
32
  prompt: "What prompted this version; usually omit",
33
+ note: "What this variant is; shown beside it, never in the html",
34
+ widths: "Widths it ships at, side by side: [390,1280]",
35
+ flow: "Flow this screen is a step of; alone, only places it",
36
+ step: "1-based place; 0 removes",
30
37
  kits: `Opt-in bundles: ${KIT_IDS.join("|")}`,
31
38
  };
32
39
  const diffFileSchema = z.object({
@@ -73,10 +80,15 @@ const askSchema = z.object({
73
80
  state: z.string().optional(),
74
81
  part: z.string().optional().describe("data-part name, for scope part"),
75
82
  multi: z.boolean().optional(),
83
+ after: z
84
+ .string()
85
+ .optional()
86
+ .describe("Id of an earlier ask this depends on; it opens once that one is answered"),
76
87
  options: z.array(z.object({
77
88
  id: z.string().optional(),
78
89
  label: z.string(),
79
90
  variant: z.string().optional().describe("Bind to a published variant"),
91
+ version: z.number().int().optional().describe("And one version of it"),
80
92
  set: z.record(z.string(), z.unknown()).optional().describe("Or to knob values {path: value}"),
81
93
  })),
82
94
  });
@@ -98,6 +110,9 @@ function zodParam(p) {
98
110
  case "integer":
99
111
  s = z.number().int();
100
112
  break;
113
+ case "integers":
114
+ s = z.array(z.number().int());
115
+ break;
101
116
  default:
102
117
  s = p.t === "string" ? z.string() : p.t === "number" ? z.number() : z.boolean();
103
118
  }
@@ -116,7 +131,7 @@ const VARIANT_REF = { mock: P.mock, state: P.state, variant: P.variant, project:
116
131
  export const MCP_TOOL_DEFS = [
117
132
  {
118
133
  name: "publish",
119
- 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.",
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.",
120
135
  params: {
121
136
  ...VARIANT_REF,
122
137
  title: { t: "string", d: field.title },
@@ -127,10 +142,19 @@ export const MCP_TOOL_DEFS = [
127
142
  d: "Full ordered list: {id} keeps a surface, a missing id removes it",
128
143
  },
129
144
  parts: { t: "object", d: '{"name"|"name#key": outer html}; splices marked parts' },
145
+ fills: {
146
+ t: "object",
147
+ d: '{"part#fill": outer html}; replaces that fill of a slot, or adds it after the others',
148
+ },
130
149
  knobs: { t: "object", d: field.knobs },
131
150
  variantKnobs: { t: "object", d: field.variantKnobs },
132
151
  from: { t: "integer", d: field.from },
152
+ fork: { t: "string", d: field.fork },
133
153
  prompt: { t: "string", d: field.prompt },
154
+ note: { t: "string", d: field.note },
155
+ widths: { t: "integers", d: field.widths },
156
+ flow: { t: "string", d: field.flow },
157
+ step: { t: "integer", d: field.step },
134
158
  session: P.session,
135
159
  sessionTitle: { t: "string", d: field.sessionTitle },
136
160
  agent: { t: "string", d: field.agent },
@@ -150,17 +174,21 @@ export const MCP_TOOL_DEFS = [
150
174
  },
151
175
  {
152
176
  name: "read",
153
- 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.",
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.",
154
178
  params: {
155
179
  mock: { t: "string", d: field.mock },
156
180
  project: P.project,
157
181
  body: { t: "boolean", d: "Include each variant's surfaces" },
158
182
  history: { t: "boolean", d: "Include version rows" },
183
+ tree: {
184
+ 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)",
186
+ },
159
187
  },
160
188
  },
161
189
  {
162
190
  name: "feedback",
163
- 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).",
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).",
164
192
  params: {
165
193
  project: P.project,
166
194
  session: { t: "string", d: "Session id returned by publish", req: true },
@@ -179,7 +207,7 @@ export const MCP_TOOL_DEFS = [
179
207
  },
180
208
  {
181
209
  name: "export",
182
- description: "The accepted (or current) html per state, version history, screenshot URL, knobs and the last reply's tuned values.",
210
+ description: "The html in use (else current) per state, version history, screenshot URL, knobs, the settled values and the last reply.",
183
211
  params: VARIANT_REF,
184
212
  },
185
213
  {
@@ -233,7 +261,9 @@ const nullStr = z.string().nullable();
233
261
  const strs = z.array(z.string());
234
262
  const bag = z.record(z.string(), z.unknown());
235
263
  const variantRef = o({ state: nullStr, variant: str });
264
+ const inUseRef = o({ state: nullStr, variant: str, version: num });
236
265
  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 })) });
237
267
  const feedbackSchema = o({
238
268
  mock: nullStr,
239
269
  reply: o({
@@ -245,18 +275,29 @@ const feedbackSchema = o({
245
275
  mix: bag,
246
276
  tuned: bag,
247
277
  comments: z.array(o({ part: nullStr, text: str })),
278
+ reactions: strs.optional().describe('One-tap reactions, e.g. 👍 part "hero" (variant B)'),
248
279
  })
249
280
  .nullable()
250
281
  .describe("The user's Send; null if only comments"),
251
282
  comments: z.array(o({ seq: num, text: str })),
252
- accepted: z.array(variantRef),
283
+ accepted: z.array(inUseRef).describe("Accepted, each at the version put in use"),
253
284
  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 }))
292
+ .optional()
293
+ .describe("The asks the resolution closed; don't ask them again"),
254
294
  }).meta({ id: "Feedback" });
255
295
  // Typed once, on feedback: repeating it on every write would cost more than
256
296
  // the rest of the write's output schema.
257
297
  const feedbackRide = z.array(bag).describe("Feedback batches as the feedback tool returns");
258
298
  const pendingSchema = o({
259
299
  mock: str,
300
+ resolved: z.literal(true).optional(),
260
301
  viewerOpen: z.boolean(),
261
302
  draft: o({ answered: num, of: num, comments: num, touchedAt: str }).nullable(),
262
303
  }).meta({ id: "Pending" });
@@ -268,6 +309,7 @@ const writeOutput = {
268
309
  partChanges: o({ vanished: strs, renamed: z.array(o({ from: str, to: str })) }).optional(),
269
310
  nudges: strs.optional(),
270
311
  suggestedAsk: bag.optional().describe("Send with ask when a nudge names it"),
312
+ flow: flowSchema.optional(),
271
313
  feedback: feedbackRide,
272
314
  };
273
315
  const OUTPUT_SCHEMAS = {
@@ -288,7 +330,13 @@ const OUTPUT_SCHEMAS = {
288
330
  session: nullStr,
289
331
  },
290
332
  publish: writeOutput,
291
- ask: { asks: z.array(o({ id: str, text: str })), open: num, url: str, feedback: feedbackRide },
333
+ ask: {
334
+ asks: z.array(o({ id: str, text: str })),
335
+ open: num,
336
+ url: str,
337
+ nudges: strs.optional(),
338
+ feedback: feedbackRide,
339
+ },
292
340
  feedback: {
293
341
  feedback: z.array(feedbackSchema).describe("One batch per mock; empty when nothing is new"),
294
342
  pending: z.array(pendingSchema),
@@ -298,13 +346,39 @@ const OUTPUT_SCHEMAS = {
298
346
  mocks: z.array(o({ slug: str, states: strs, variants: num, open: num })).optional(),
299
347
  states: strs.optional(),
300
348
  asks: z.array(o({ id: str, text: str })).optional(),
301
- variants: z.array(o({ state: nullStr, variant: str, status: str, version: num })).optional(),
349
+ variants: z
350
+ .array(o({
351
+ state: nullStr,
352
+ variant: str,
353
+ status: str,
354
+ version: num,
355
+ acceptedVersion: num.optional().describe("Set when accepted: the version in use"),
356
+ history: z
357
+ .array(o({ version: num, hidden: z.boolean().optional() }))
358
+ .optional()
359
+ .describe("With history: version rows; hidden ones the user put away are flagged"),
360
+ }))
361
+ .optional(),
362
+ inUse: z
363
+ .array(inUseRef)
364
+ .optional()
365
+ .describe("Per state, the version the user put in use; export and embeds resolve to it"),
302
366
  knobs: bag.optional(),
303
- tuned: bag.optional().describe("Knob values from the latest reply"),
367
+ settled: bag.optional().describe("Every knob value the user has sent, later replies winning"),
368
+ tree: z.array(bag).optional(),
369
+ flows: z.array(flowSchema).optional(),
304
370
  pending: z.union([z.array(pendingSchema), pendingSchema]),
305
371
  },
306
372
  export: {
307
- states: z.array(o({ state: nullStr, variant: str, version: num, html: str, markdown: str })),
373
+ states: z.array(o({
374
+ state: nullStr,
375
+ variant: str,
376
+ version: num,
377
+ latest: num.optional().describe("Your newer version, not in use yet"),
378
+ html: str,
379
+ markdown: str,
380
+ })),
381
+ settled: bag,
308
382
  reply: bag.nullable(),
309
383
  },
310
384
  };
@@ -394,6 +468,22 @@ export function toolResult(name, value) {
394
468
  ...(structured ? { structuredContent: value } : {}),
395
469
  };
396
470
  }
471
+ // The images a result's feedback carries, inlined as image content so the
472
+ // model sees what the user attached instead of only its URL. Capped per result:
473
+ // the rest stay reachable by URL in the JSON.
474
+ export const MAX_RESULT_IMAGES = 8;
475
+ export async function withFeedbackImages(result, value, load) {
476
+ const batches = value?.feedback;
477
+ if (!Array.isArray(batches))
478
+ return result;
479
+ const images = [];
480
+ for (const ref of feedbackImages(batches).slice(0, MAX_RESULT_IMAGES)) {
481
+ const bytes = await load(ref).catch(() => null);
482
+ if (bytes)
483
+ images.push({ type: "image", data: encodeBase64(bytes.data), mimeType: bytes.mimeType });
484
+ }
485
+ return images.length ? { ...result, content: [...result.content, ...images] } : result;
486
+ }
397
487
  // A run that failed is still a full envelope (calls, feedback); isError tells
398
488
  // the client the script did not finish.
399
489
  export function runToolResult(envelope) {
@@ -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
@@ -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; }
@@ -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`];
@@ -170,13 +174,13 @@ export function createRunFlow(deps) {
170
174
  return [value, `${value.mock} ${value.asks.length} ask(s)`];
171
175
  },
172
176
  async feedback() {
173
- const value = await take(flows.feedback(await mine({})));
177
+ const value = await take(flows.feedback(await mine({}), flowCtx));
174
178
  const n = value.feedback.length;
175
179
  return [value, n ? `${n} batch(es)` : "no feedback"];
176
180
  },
177
181
  async say([v, message]) {
178
182
  const r = plain(v);
179
- await take(flows.say(r.mock, { ...(await mine(r)), message }));
183
+ await take(flows.say(r.mock, { ...(await mine(r)), message }, flowCtx));
180
184
  return [null, String(r.mock ?? "?")];
181
185
  },
182
186
  async upload([data, opts]) {
@@ -1,4 +1,4 @@
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 }>;\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;";
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
2
  export declare const RUN_FUNCTIONS: readonly ["guide", "read", "publish", "ask", "feedback", "say", "export", "upload"];
3
3
  export type RunFunction = (typeof RUN_FUNCTIONS)[number];
4
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;";
@@ -38,12 +38,13 @@ type Feedback = {
38
38
  type Pending = { mock: string; viewerOpen: boolean; draft: null | { answered: number; of: number; comments: number; touchedAt: string } };
39
39
  declare const mockpit: {
40
40
  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. */
41
+ /** No mock: every mock, the flows, plus pending. With one: its states, variants, asks, parts, knobs. */
42
42
  read(mock?: string, opts?: { body?: boolean; history?: boolean }): Promise<unknown>;
43
43
  /** 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>;
46
- ask(mock: string, asks: Ask[]): Promise<{ asks: { id: string; text: string }[]; url: string }>;
44
+ * ({id} alone keeps that surface, a missing id is removed); parts {"name"|"name#key": outer html};
45
+ * fills {"part#fill": outer html}, replacing or adding one alternative of a slot. */
46
+ 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>;
47
+ ask(mock: string, asks: Ask[]): Promise<{ asks: { id: string; text: string }[]; url: string; nudges?: string[] }>;
47
48
  /** Never waits: what the user sent since you last heard, and what they are doing now. */
48
49
  feedback(): Promise<{ feedback: Feedback[]; pending: Pending[] }>;
49
50
  say(v: Ref, message: string): Promise<void>;