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
package/README.md CHANGED
@@ -51,11 +51,14 @@ mockpit turns that into a design-decision loop:
51
51
  panel goes, which of two layouts. The question lives in the mock, not in
52
52
  chat; the agent tells you where to look and ends its turn. Options bound to
53
53
  a variant or to knob values render as pictures; hovering one previews it on
54
- the stage, clicking picks it. After the look, **Mix** offers to borrow a part
55
- from another look. Variants the agent forgot to ask about get a built-in
56
- **Which one?**.
54
+ the stage, clicking picks it, and the part it changed is outlined. A
55
+ region the agent drew several ways (a slot: one `data-part`, a `data-fill`
56
+ per alternative) is asked about once; a slot inside a fill is asked only
57
+ while that fill is chosen. Variants the agent forgot to ask about get a
58
+ built-in **Which one?**.
57
59
  3. **Tune.** Parts the agent marked are selectable on the stage. Tune lists
58
- them with the knobs it declared for each, plus a comment field. Presets
60
+ them with the knobs it declared for each, a **Borrow** line for a part
61
+ another look draws differently, plus a comment field. Presets
59
62
  save a set of tuned values in your browser.
60
63
  4. **Send.** Everything above is a draft (it survives a reload) until one
61
64
  **Send**. A mock with a single variant and no questions gets **Accept /
@@ -70,8 +73,12 @@ mockpit turns that into a design-decision loop:
70
73
 
71
74
  **Home** lists the project's mocks with a thumbnail, their states and how many
72
75
  questions are open, with "Answer next ›" to jump to the first one. Dark and
73
- light themes, toggled from the top bar. On a narrow screen the panel becomes a
74
- bottom sheet.
76
+ light themes, toggled from the top bar. On a phone or an iPad in portrait the
77
+ design is a canvas you pan and pinch, at each width the agent names (or one you
78
+ pick: Phone 390, Tablet 744, Desktop 820, Wide 1280) side by side, and the
79
+ questions float over it in a card you can snap to a corner, a side or the
80
+ bottom, or tuck against an edge. On a desktop, Inspect opens the stage full
81
+ screen to pan and pinch-zoom it.
75
82
 
76
83
  ## Quick start
77
84
 
package/bin/mockpit.js CHANGED
@@ -37,7 +37,9 @@ the loop (never blocks on the user):
37
37
  1. mockpit init once per repo; read the brief (mockpit guide).
38
38
  2. mockpit publish once per variant; mark parts with data-part.
39
39
  3. A choice is several variants plus one ask that binds them: mockpit ask
40
- with options bound to variants. One render plus a control: --knobs.
40
+ with options bound to variants. A variant decides everything it differs
41
+ on; don't re-ask it. Visual options carry a variant or knob set. One
42
+ render plus a control: --knobs.
41
43
  4. Tell the user in one line where to look, then end your turn. The question
42
44
  itself lives in the mock (ask), never in chat. The user answers in the
43
45
  browser at their own pace. Never poll.
@@ -60,11 +62,17 @@ verbs:
60
62
  alone keeps a surface, a missing id removes it
61
63
  --parts <name=file|-> instead: replace just that data-part's element in
62
64
  the latest version (repeatable; name#key targets one instance)
65
+ --fills <part#fill=file|-> instead: replace or add one fill of a slot in
66
+ the latest version (repeatable)
63
67
  --title <t> mock title
64
68
  --kind <k> component|page (default component)
65
69
  --knobs <json|file> knobs in tunekit usePane shape, keyed by path
66
70
  --from <N> branch from version N
67
71
  --prompt <text> what prompted this version
72
+ --note <text> one line on what this variant is, shown beside it
73
+ --widths <list> device widths to show it at, e.g. 390,1280
74
+ --flow <name> [--step <N>] make this screen step N of a flow (0 takes
75
+ it out); with no html it only places the screen
68
76
  --project <name> project (default: git remote, else directory name)
69
77
  --kit <id> opt the html surface into a kit (repeatable; see "mockpit kits")
70
78
  --md/--mermaid/--diff/--terminal/--data/--code/--image <file|->
@@ -72,7 +80,8 @@ verbs:
72
80
  --data is a JSON tree)
73
81
  mockpit ask --mock <slug> "<question>" --option <label[=variant]> ...
74
82
  ask the user; bind options to variants
75
- --option <l[=v]> an option (repeatable); "=variant" binds it to a variant
83
+ --option <l[=v]> an option (repeatable); "=variant" binds it to a variant,
84
+ "=variant@3" to its version 3, "=knob:path=value" sets one knob
76
85
  --scope <s> mock|state|part (default mock)
77
86
  --state <label> the state a state-scoped ask is about
78
87
  --part <name> the part a part-scoped ask is about
@@ -153,6 +162,7 @@ mockpit icons remove <set> [<set>...]
153
162
  publish: `mockpit publish --mock <slug> --html <file> [options]
154
163
  mockpit publish --mock <slug> --surfaces <json|file> [options]
155
164
  mockpit publish --mock <slug> --parts <name=file|-> ... [options]
165
+ mockpit publish --mock <slug> --fills <part#fill=file|-> ... [options]
156
166
  Creates the mock, state or variant, or the next version of an existing one.
157
167
  --state <label> state in the user's words (omit for a single-state mock)
158
168
  --variant <name> variant label (default "default")
@@ -160,30 +170,48 @@ mockpit publish --mock <slug> --parts <name=file|-> ... [options]
160
170
  a missing id removes it, order is the list order
161
171
  --parts <name=file|-> replace only the element carrying data-part="name"
162
172
  (name#key for one instance) in the latest version; repeatable
173
+ --fills <part#fill=file|-> replace the element carrying data-part="part"
174
+ data-fill="fill" in the latest version, or add it after
175
+ that slot's other fills; the marks are added if missing
163
176
  --title <t> mock title
164
177
  --kind <k> component|page (default component)
165
178
  --knobs <json|file> knobs in tunekit usePane shape
166
179
  --from <N> branch from version N
180
+ --fork <v[@N]> start this new variant from sibling v (its version N, else
181
+ its latest); alone it copies v, with --parts/--fills it
182
+ edits the copy
167
183
  --prompt <text> what prompted this version
184
+ --note <text> one line on what this variant is, shown beside it (never
185
+ inside the design)
186
+ --widths <list> device widths the mock is judged at, side by side, e.g.
187
+ 390,1280 (the ones the user ships to)
188
+ --flow <name> a flow this screen is a step of: the screens the user moves
189
+ between, in order, across mocks; without html it only
190
+ places the existing screen
191
+ --step <N> its 1-based place in the flow (moves it if it is there);
192
+ 0 takes it out; omitted, it joins at the end
168
193
  --project <name> project (default: git remote, else directory name)
169
194
  --md/--mermaid/--diff/--terminal/--data/--code/--image <file> more surfaces
170
195
  --json / --quiet
171
196
  Prints the parts found per state, parts that vanished or were renamed,
172
197
  nudges, a ready-to-run "mockpit ask" when variants have no ask, and feedback.`,
173
198
  ask: `mockpit ask --mock <slug> "<question>" --option <label[=variant]> ... [--scope mock|state|part]
174
- [--state s] [--part p] [--multi] [--id id]
199
+ [--state s] [--part p] [--multi] [--id id] [--after <ask id>]
175
200
  mockpit ask --mock <slug> --asks <json|file>
176
201
  Ask the user. A choice is several variants plus one ask that binds them.
177
202
  Then tell the user where to look and end your turn.`,
178
- read: `mockpit read [slug] [--project <name>] [--body] [--history]
179
- No slug: one line per mock (states, variants, open asks) plus pending, what
203
+ read: `mockpit read [slug] [--project <name>] [--body] [--history] [--tree]
204
+ No slug: one line per mock (states, variants, open asks), one per flow, plus pending, what
180
205
  the user is doing now. A slug: the mock's states, variants, asks with
181
- answers, parts, knobs and last tuned values, plus its pending, as JSON.
182
- --body includes surfaces, --history version rows. Never consumes feedback.`,
206
+ answers, parts, knobs and settled values, plus its pending, as JSON.
207
+ --body includes surfaces, --history version rows. --tree (no slug): the
208
+ project as a tree, each mock's variants with status and forkedFrom, and the
209
+ mocks it uses and is used in, as JSON. Never consumes feedback.`,
183
210
  feedback: `mockpit feedback [--project <name>] [--session <id>]
184
211
  Returns at once with {feedback, pending} as JSON: one batch per mock the user
185
212
  sent since you last heard ({mock, reply: {asks, mix, tuned, comments},
186
- comments, accepted, archived}), delivered once across feedback, watch and
213
+ comments, accepted, archived, dropped}, or {mock, resolved: true, dismissed} when the
214
+ user closed it unanswered), delivered once across feedback, watch and
187
215
  every write; and pending ({mock, viewerOpen, draft}) so you can tell the
188
216
  user "take your time" or "2 of 3 answered". Run it when the user says they
189
217
  answered.`,
@@ -200,7 +228,8 @@ mockpit publish --mock <slug> --parts <name=file|-> ... [options]
200
228
  Claude Code, arm it under Monitor after asking, then end the turn).
201
229
  Delivery is shared with feedback and every write: each Send arrives once.`,
202
230
  export: `mockpit export --mock <slug> [--state s] [--variant v] [--out <dir>]
203
- Write index.html + history.json per state to .mockpit/accepted/<mock>/<state>/.`,
231
+ Write index.html + history.json per state to .mockpit/accepted/<mock>/<state>/;
232
+ with slots, index.html is the chosen screen, plus source.html and decisions.json.`,
204
233
  upload: `mockpit upload <file> [--kind image|file] [--session <id>]
205
234
  Upload an asset; prints its id and URL. Use the id as an image surface's assetId.`,
206
235
  guide: `mockpit guide [--topic <id>] [--project <name>]
@@ -807,6 +836,7 @@ function printPublished(result, flags) {
807
836
  console.log(`parts${state ? ` (${state})` : ""}: ${parts.map((p) => p.name).join(", ")}`);
808
837
  }
809
838
  if (result.applied?.length) console.log(`applied: ${result.applied.join(", ")}`);
839
+ if (result.flow) console.log(flowLine(result.flow));
810
840
  const changes = result.partChanges;
811
841
  if (changes?.renamed?.length) {
812
842
  console.log(`renamed: ${changes.renamed.map((r) => `${r.from} → ${r.to}`).join(", ")}`);
@@ -819,6 +849,11 @@ function printPublished(result, flags) {
819
849
  printFeedback(result.feedback);
820
850
  }
821
851
 
852
+ function flowLine(flow) {
853
+ const steps = flow.steps.map((x) => (x.state === null ? x.mock : `${x.mock}/${x.state}`));
854
+ return `flow ${flow.name}: ${steps.length ? steps.join(" → ") : "empty, removed"}`;
855
+ }
856
+
822
857
  // Only speak when there IS feedback: an empty line per write is pure token
823
858
  // cost, and the cursor guarantees anything pending arrives on some write.
824
859
  function printFeedback(feedback) {
@@ -849,9 +884,15 @@ function parsePublishFlags() {
849
884
  html: { type: "string" },
850
885
  surfaces: { type: "string" },
851
886
  parts: { type: "string", multiple: true },
887
+ fills: { type: "string", multiple: true },
852
888
  knobs: { type: "string" },
853
889
  from: { type: "string" },
890
+ fork: { type: "string" },
854
891
  prompt: { type: "string" },
892
+ note: { type: "string" },
893
+ widths: { type: "string" },
894
+ flow: { type: "string" },
895
+ step: { type: "string" },
855
896
  kit: { type: "string", multiple: true },
856
897
  layout: { type: "string" },
857
898
  md: { type: "string", multiple: true },
@@ -870,18 +911,27 @@ function parsePublishFlags() {
870
911
 
871
912
  // `--parts name=file` (repeatable; `name#key` targets one instance): the new
872
913
  // outer html of one part, spliced server-side into the latest version.
873
- function readPartFlags(values, slug) {
914
+ // `--fills part#fill=file` is the same for one alternative of a slot.
915
+ function readPartFlags(values, slug, flag = "parts") {
874
916
  if (!values?.length) return undefined;
875
- const example = `mockpit publish --mock ${slug} --parts body=body.html`;
917
+ const example =
918
+ flag === "parts"
919
+ ? `mockpit publish --mock ${slug} --parts body=body.html`
920
+ : `mockpit publish --mock ${slug} --fills entry#skeleton=skeleton.html`;
876
921
  const parts = {};
877
922
  let stdin = false;
878
923
  for (const raw of values) {
879
924
  const at = raw.indexOf("=");
880
- if (at <= 0 || at === raw.length - 1) die(`--parts needs name=file (got "${raw}")`, example);
925
+ if (at <= 0 || at === raw.length - 1) {
926
+ die(
927
+ `--${flag} needs ${flag === "parts" ? "name" : "part#fill"}=file (got "${raw}")`,
928
+ example,
929
+ );
930
+ }
881
931
  const name = raw.slice(0, at);
882
932
  const file = raw.slice(at + 1);
883
933
  if (file === "-") {
884
- if (stdin) die("only one --parts can read stdin", example);
934
+ if (stdin) die(`only one --${flag} can read stdin`, example);
885
935
  stdin = true;
886
936
  } else if (!existsSync(file)) die(`cannot read ${file}`, `ls ${file}`);
887
937
  parts[name] = readContent(file);
@@ -897,6 +947,13 @@ async function publishMock() {
897
947
  if (from !== undefined && !Number.isInteger(from)) {
898
948
  die(`--from must be a version number (got "${flags.from}")`, `mockpit read ${slug} --history`);
899
949
  }
950
+ const step = flags.step === undefined ? undefined : Number(flags.step);
951
+ if (step !== undefined && !(Number.isInteger(step) && step >= 0)) {
952
+ die(
953
+ `--step must be a place in the flow, 1 or more, or 0 (got "${flags.step}")`,
954
+ `mockpit read`,
955
+ );
956
+ }
900
957
  const file = flags.html ?? positionals[0];
901
958
  const composed =
902
959
  file !== undefined || tokens.some((t) => t.kind === "option" && SURFACE_FLAGS.has(t.name));
@@ -904,16 +961,18 @@ async function publishMock() {
904
961
  composed && "--html",
905
962
  flags.surfaces !== undefined && "--surfaces",
906
963
  flags.parts?.length && "--parts",
964
+ flags.fills?.length && "--fills",
907
965
  ].filter(Boolean);
908
966
  if (given.length > 1) {
909
967
  die(
910
- `pass one of --html, --surfaces or --parts, not ${given.join(" and ")}`,
968
+ `pass one of --html, --surfaces, --parts or --fills, not ${given.join(" and ")}`,
911
969
  `mockpit publish --help`,
912
970
  );
913
971
  }
914
972
  // Checked before the session exists so a typo'd path leaves nothing behind on the server.
915
973
  if (file && file !== "-" && !existsSync(file)) die(`cannot read ${file}`, `ls ${file}`);
916
974
  const parts = readPartFlags(flags.parts, slug);
975
+ const fills = readPartFlags(flags.fills, slug, "fills");
917
976
  let surfaces =
918
977
  flags.surfaces === undefined ? undefined : readJsonFlag("surfaces", flags.surfaces);
919
978
  if (surfaces !== undefined && !Array.isArray(surfaces)) {
@@ -933,7 +992,7 @@ async function publishMock() {
933
992
  }
934
993
  surfaces.push(...(await surfacesFromFlags(flags, tokens, { session, layout: flags.layout })));
935
994
  }
936
- if (!surfaces && !parts)
995
+ if (!surfaces && !parts && !fills && !flags.fork && flags.flow === undefined)
937
996
  die(`no html for ${slug}`, `mockpit publish --mock ${slug} --html <file>`);
938
997
  const body = {
939
998
  session,
@@ -945,9 +1004,20 @@ async function publishMock() {
945
1004
  kind: flags.kind,
946
1005
  ...(flags.knobs !== undefined && { knobs: readJsonFlag("knobs", flags.knobs) }),
947
1006
  ...(from !== undefined && { from }),
1007
+ ...(flags.fork !== undefined && { fork: flags.fork }),
948
1008
  prompt: flags.prompt,
1009
+ note: flags.note,
1010
+ ...(flags.widths !== undefined && {
1011
+ widths: flags.widths
1012
+ .split(",")
1013
+ .filter((w) => w.trim())
1014
+ .map((w) => Number(w)),
1015
+ }),
1016
+ ...(flags.flow !== undefined && { flow: flags.flow }),
1017
+ ...(step !== undefined && { step }),
949
1018
  ...(surfaces && { surfaces }),
950
1019
  ...(parts && { parts }),
1020
+ ...(fills && { fills }),
951
1021
  };
952
1022
  const result = await api(
953
1023
  "/api/mocks",
@@ -958,11 +1028,31 @@ async function publishMock() {
958
1028
  return result;
959
1029
  }
960
1030
 
961
- // "Quiet" is an unbound option; "Quiet=quiet" binds it to the quiet variant.
1031
+ // "Quiet" is an unbound option; "Quiet=quiet" binds it to the quiet variant;
1032
+ // "Before=quiet@2" to its version 2 ("=@2" names the state's only variant);
1033
+ // "Above=knob:trim.position=top" sets one knob. The value is JSON when it
1034
+ // parses (17, true), else a string, so slider and toggle values keep their type.
962
1035
  function parseOption(raw) {
1036
+ const knob = raw.indexOf("=knob:");
1037
+ if (knob > 0) {
1038
+ const pair = raw.slice(knob + "=knob:".length);
1039
+ const eq = pair.indexOf("=");
1040
+ if (eq <= 0)
1041
+ die(`option "${raw}" needs knob:<path>=<value>`, "--option Above=knob:trim.position=top");
1042
+ const text = pair.slice(eq + 1);
1043
+ let value = text;
1044
+ try {
1045
+ value = JSON.parse(text);
1046
+ } catch {}
1047
+ return { label: raw.slice(0, knob), set: { [pair.slice(0, eq)]: value } };
1048
+ }
963
1049
  const at = raw.lastIndexOf("=");
964
1050
  if (at <= 0) return { label: raw };
965
- return { label: raw.slice(0, at), variant: raw.slice(at + 1) };
1051
+ const label = raw.slice(0, at);
1052
+ const bound = raw.slice(at + 1);
1053
+ const pin = /^(.*)@(\d+)$/.exec(bound);
1054
+ if (!pin) return { label, variant: bound };
1055
+ return { label, ...(pin[1] ? { variant: pin[1] } : {}), version: Number(pin[2]) };
966
1056
  }
967
1057
 
968
1058
  // One feedback batch, one line — for a background monitor's notifications.
@@ -986,6 +1076,13 @@ function printRun(r) {
986
1076
  function watchLines(batch) {
987
1077
  const lines = [];
988
1078
  const name = batch.mock ?? "a mock";
1079
+ if (batch.handoff) lines.push(`mockpit handoff on ${name}: ${batch.handoff}`);
1080
+ if (batch.resolved) {
1081
+ const n = batch.dismissed?.length ?? 0;
1082
+ lines.push(
1083
+ `mockpit resolved ${name}: ${n ? `${n} question${n === 1 ? "" : "s"} dismissed unanswered (${batch.dismissed.map((d) => d.text || d.ask).join("; ")}); don't ask again` : "closed by the user"}`,
1084
+ );
1085
+ }
989
1086
  if (batch.reply) {
990
1087
  const r = batch.reply;
991
1088
  const bits = [
@@ -1004,6 +1101,7 @@ function watchLines(batch) {
1004
1101
  .join(", ")}`
1005
1102
  : "",
1006
1103
  r.comments?.length ? `${r.comments.length} comment${r.comments.length === 1 ? "" : "s"}` : "",
1104
+ r.reactions?.length ? r.reactions.join(", ") : "",
1007
1105
  r.decision ? r.decision.kind : "",
1008
1106
  r.text ?? "",
1009
1107
  ].filter(Boolean);
@@ -1320,6 +1418,7 @@ const commands = {
1320
1418
  part: { type: "string" },
1321
1419
  multi: { type: "boolean" },
1322
1420
  id: { type: "string" },
1421
+ after: { type: "string" },
1323
1422
  asks: { type: "string" },
1324
1423
  },
1325
1424
  });
@@ -1357,6 +1456,7 @@ const commands = {
1357
1456
  ...(flags.state && { state: flags.state }),
1358
1457
  ...(flags.part && { part: flags.part }),
1359
1458
  ...(flags.multi && { multi: true }),
1459
+ ...(flags.after && { after: flags.after }),
1360
1460
  },
1361
1461
  ];
1362
1462
  }
@@ -1380,6 +1480,7 @@ const commands = {
1380
1480
  `asked on ${result.mock}: ${ask.text} [${ask.options.map((o) => o.label).join(" | ")}]`,
1381
1481
  );
1382
1482
  }
1483
+ for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
1383
1484
  console.log(`tell the user to look at ${result.url}, then end your turn`);
1384
1485
  printFeedback(result.feedback);
1385
1486
  },
@@ -1393,10 +1494,20 @@ const commands = {
1393
1494
  project: { type: "string" },
1394
1495
  body: { type: "boolean" },
1395
1496
  history: { type: "boolean" },
1497
+ tree: { type: "boolean" },
1396
1498
  },
1397
1499
  });
1398
1500
  const project = resolveProject(flags).name;
1399
1501
  if (positionals.length > 1) die("read takes one mock slug", "mockpit read writer");
1502
+ if (flags.tree) {
1503
+ return out(
1504
+ await api(
1505
+ `/api/projects/${encodeURIComponent(project)}/tree`,
1506
+ {},
1507
+ { report: "die", fix: `mockpit read --project ${project}` },
1508
+ ),
1509
+ );
1510
+ }
1400
1511
  if (positionals.length === 1) {
1401
1512
  const slug = slugify(positionals[0]);
1402
1513
  return out(
@@ -1424,9 +1535,10 @@ const commands = {
1424
1535
  for (const m of mocks) {
1425
1536
  const states = m.states.length ? m.states.join(" / ") : "single state";
1426
1537
  console.log(
1427
- ` ${m.slug} · ${m.kind} · ${states} · ${m.variants} variant${m.variants === 1 ? "" : "s"}${m.open ? ` · ${m.open} open` : ""}`,
1538
+ ` ${m.slug} · ${m.kind} · ${states} · ${m.variants} variant${m.variants === 1 ? "" : "s"}${m.resolvedAt ? " · resolved" : m.open ? ` · ${m.open} open` : ""}`,
1428
1539
  );
1429
1540
  }
1541
+ for (const flow of list.flows ?? []) console.log(` ${flowLine(flow)}`);
1430
1542
  for (const line of pendingLines(list.pending)) console.log(line);
1431
1543
  },
1432
1544
  async export() {
@@ -1452,6 +1564,15 @@ const commands = {
1452
1564
  const dir = join(root, entry.state === null ? "default" : slugify(entry.state));
1453
1565
  mkdirSync(dir, { recursive: true });
1454
1566
  writeFileSync(join(dir, "index.html"), entry.html ?? "");
1567
+ // The screen with every fill, when index.html is the composed choice.
1568
+ if (entry.source) writeFileSync(join(dir, "source.html"), entry.source);
1569
+ const decided = data.decisions?.states?.[entry.state ?? ""];
1570
+ if (decided) {
1571
+ writeFileSync(
1572
+ join(dir, "decisions.json"),
1573
+ JSON.stringify({ ...decided, knobs: data.decisions.knobs }, null, 2) + "\n",
1574
+ );
1575
+ }
1455
1576
  writeFileSync(
1456
1577
  join(dir, "history.json"),
1457
1578
  JSON.stringify(
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
8
  import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
9
- import { MCP_INSTRUCTIONS, MCP_SERVER_INFO, runToolResult, STDIO_MCP_CATALOG, STDIO_MCP_TOOLS, STDIO_RUN_CATALOG, STDIO_RUN_TOOLS, toolResult, } from "../server/mcpSpec.js";
9
+ import { MCP_INSTRUCTIONS, MCP_SERVER_INFO, runToolResult, STDIO_MCP_CATALOG, STDIO_MCP_TOOLS, STDIO_RUN_CATALOG, STDIO_RUN_TOOLS, toolResult, withFeedbackImages, } from "../server/mcpSpec.js";
10
10
  import { RUN_INSTRUCTIONS } from "../server/runApi.js";
11
11
  // Point at a deployed instance later by setting MOCKPIT_URL.
12
12
  const API = process.env.MOCKPIT_URL ?? "http://localhost:8228";
@@ -156,6 +156,7 @@ const handlers = {
156
156
  ...args,
157
157
  html,
158
158
  parts: readPartFiles(args.parts),
159
+ fills: readPartFiles(args.fills),
159
160
  session,
160
161
  agent: AGENT,
161
162
  project: resolveProject(args.project),
@@ -169,13 +170,15 @@ const handlers = {
169
170
  project: resolveProject(args.project),
170
171
  });
171
172
  },
172
- read: (args) => json(args.mock === undefined
173
- ? `/api/mocks${query({ project: resolveProject(args.project) })}`
174
- : `/api/mocks/${enc(args.mock)}${query({
175
- project: resolveProject(args.project),
176
- body: args.body,
177
- history: args.history,
178
- })}`),
173
+ read: (args) => json(args.mock === undefined && args.tree === true
174
+ ? `/api/projects/${enc(resolveProject(args.project))}/tree`
175
+ : args.mock === undefined
176
+ ? `/api/mocks${query({ project: resolveProject(args.project) })}`
177
+ : `/api/mocks/${enc(args.mock)}${query({
178
+ project: resolveProject(args.project),
179
+ body: args.body,
180
+ history: args.history,
181
+ })}`),
179
182
  // Never waits: the server reads from the session's cursor, shared with
180
183
  // piggyback and `mockpit watch`, so each Send is delivered exactly once.
181
184
  async feedback(args) {
@@ -230,18 +233,35 @@ async function run(args) {
230
233
  body: JSON.stringify({ code, session, project: resolveProject(args.project), agent: AGENT }),
231
234
  });
232
235
  }
236
+ async function loadImage(ref) {
237
+ const res = await fetch(`${API}/a/${encodeURIComponent(ref.id)}`, {
238
+ headers: TOKEN ? { authorization: `Bearer ${TOKEN}` } : {},
239
+ });
240
+ if (!res.ok)
241
+ return null;
242
+ return {
243
+ data: new Uint8Array(await res.arrayBuffer()),
244
+ mimeType: res.headers.get("content-type") ?? "image/png",
245
+ };
246
+ }
233
247
  const server = new McpServer(MCP_SERVER_INFO, {
234
248
  instructions: CODE_MODE ? RUN_INSTRUCTIONS : MCP_INSTRUCTIONS,
235
249
  });
236
250
  if (CODE_MODE) {
237
251
  const [{ name, ...config }] = STDIO_RUN_TOOLS;
238
- server.registerTool(name, config, async (args) => runToolResult(await run(args)));
252
+ server.registerTool(name, config, async (args) => {
253
+ const envelope = await run(args);
254
+ return withFeedbackImages(runToolResult(envelope), envelope, loadImage);
255
+ });
239
256
  }
240
257
  else {
241
258
  for (const tool of STDIO_MCP_TOOLS) {
242
259
  const handler = handlers[tool.name];
243
260
  const { name, ...config } = tool;
244
- server.registerTool(name, config, async (args) => toolResult(name, await handler(args)));
261
+ server.registerTool(name, config, async (args) => {
262
+ const value = await handler(args);
263
+ return withFeedbackImages(toolResult(name, value), value, loadImage);
264
+ });
245
265
  }
246
266
  }
247
267
  // Replaces the SDK's own listing (registered above), so stdio advertises the