mockpit 1.2.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 (51) hide show
  1. package/README.md +13 -8
  2. package/bin/mockpit.js +116 -16
  3. package/dist/mcp/server.js +10 -7
  4. package/dist/server/apiViews.d.ts +118 -17
  5. package/dist/server/apiViews.js +66 -8
  6. package/dist/server/app.d.ts +2 -1
  7. package/dist/server/app.js +917 -144
  8. package/dist/server/decisions.d.ts +23 -0
  9. package/dist/server/decisions.js +99 -0
  10. package/dist/server/designGuide.js +45 -10
  11. package/dist/server/embeds.d.ts +25 -0
  12. package/dist/server/embeds.js +118 -0
  13. package/dist/server/eventLog.js +2 -1
  14. package/dist/server/feedbackBatch.d.ts +12 -1
  15. package/dist/server/feedbackBatch.js +20 -3
  16. package/dist/server/fills.d.ts +41 -0
  17. package/dist/server/fills.js +328 -0
  18. package/dist/server/index.js +15 -1
  19. package/dist/server/knobs.d.ts +6 -2
  20. package/dist/server/knobs.js +11 -1
  21. package/dist/server/mcpHttp.d.ts +3 -0
  22. package/dist/server/mcpHttp.js +3 -0
  23. package/dist/server/mcpSpec.d.ts +13 -13
  24. package/dist/server/mcpSpec.js +73 -8
  25. package/dist/server/postSurfaces.js +3 -12
  26. package/dist/server/richRender.js +3 -1
  27. package/dist/server/run.js +4 -0
  28. package/dist/server/runApi.d.ts +1 -1
  29. package/dist/server/runApi.js +4 -3
  30. package/dist/server/sqlStore.d.ts +89 -4
  31. package/dist/server/sqlStore.js +149 -23
  32. package/dist/server/surfacePage.d.ts +6 -2
  33. package/dist/server/surfacePage.js +142 -10
  34. package/dist/server/themes.d.ts +2 -0
  35. package/dist/server/themes.js +23 -3
  36. package/dist/server/types.d.ts +73 -11
  37. package/dist/server/types.js +42 -2
  38. package/dist/server/viewerRequest.d.ts +2 -0
  39. package/dist/server/viewerRequest.js +14 -0
  40. package/guide/topics/asks.md +50 -3
  41. package/guide/topics/feedback.md +89 -11
  42. package/guide/topics/html.md +5 -1
  43. package/guide/topics/http.md +2 -1
  44. package/guide/topics/knobs.md +2 -1
  45. package/guide/topics/surfaces.md +24 -8
  46. package/package.json +4 -13
  47. package/skills/launch-video/SKILL.md +1 -1
  48. package/skills/mockpit/SKILL.md +12 -4
  49. package/viewer/dist/index.html +8 -8
  50. package/dist/server/slots.d.ts +0 -14
  51. 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,10 +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 phone or tablet the panel becomes a
74
- bottom sheet, the stage scrolls under your finger and renders the design at a
75
- chosen width (Phone 390, Tablet 744, Desktop 820, Wide 1280), and Inspect opens
76
- the stage full screen to pan and pinch-zoom it.
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.
77
82
 
78
83
  ## Quick start
79
84
 
package/bin/mockpit.js CHANGED
@@ -62,11 +62,17 @@ verbs:
62
62
  alone keeps a surface, a missing id removes it
63
63
  --parts <name=file|-> instead: replace just that data-part's element in
64
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)
65
67
  --title <t> mock title
66
68
  --kind <k> component|page (default component)
67
69
  --knobs <json|file> knobs in tunekit usePane shape, keyed by path
68
70
  --from <N> branch from version N
69
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
70
76
  --project <name> project (default: git remote, else directory name)
71
77
  --kit <id> opt the html surface into a kit (repeatable; see "mockpit kits")
72
78
  --md/--mermaid/--diff/--terminal/--data/--code/--image <file|->
@@ -75,7 +81,7 @@ verbs:
75
81
  mockpit ask --mock <slug> "<question>" --option <label[=variant]> ...
76
82
  ask the user; bind options to variants
77
83
  --option <l[=v]> an option (repeatable); "=variant" binds it to a variant,
78
- "=knob:path=value" sets one knob
84
+ "=variant@3" to its version 3, "=knob:path=value" sets one knob
79
85
  --scope <s> mock|state|part (default mock)
80
86
  --state <label> the state a state-scoped ask is about
81
87
  --part <name> the part a part-scoped ask is about
@@ -156,6 +162,7 @@ mockpit icons remove <set> [<set>...]
156
162
  publish: `mockpit publish --mock <slug> --html <file> [options]
157
163
  mockpit publish --mock <slug> --surfaces <json|file> [options]
158
164
  mockpit publish --mock <slug> --parts <name=file|-> ... [options]
165
+ mockpit publish --mock <slug> --fills <part#fill=file|-> ... [options]
159
166
  Creates the mock, state or variant, or the next version of an existing one.
160
167
  --state <label> state in the user's words (omit for a single-state mock)
161
168
  --variant <name> variant label (default "default")
@@ -163,30 +170,48 @@ mockpit publish --mock <slug> --parts <name=file|-> ... [options]
163
170
  a missing id removes it, order is the list order
164
171
  --parts <name=file|-> replace only the element carrying data-part="name"
165
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
166
176
  --title <t> mock title
167
177
  --kind <k> component|page (default component)
168
178
  --knobs <json|file> knobs in tunekit usePane shape
169
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
170
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
171
193
  --project <name> project (default: git remote, else directory name)
172
194
  --md/--mermaid/--diff/--terminal/--data/--code/--image <file> more surfaces
173
195
  --json / --quiet
174
196
  Prints the parts found per state, parts that vanished or were renamed,
175
197
  nudges, a ready-to-run "mockpit ask" when variants have no ask, and feedback.`,
176
198
  ask: `mockpit ask --mock <slug> "<question>" --option <label[=variant]> ... [--scope mock|state|part]
177
- [--state s] [--part p] [--multi] [--id id]
199
+ [--state s] [--part p] [--multi] [--id id] [--after <ask id>]
178
200
  mockpit ask --mock <slug> --asks <json|file>
179
201
  Ask the user. A choice is several variants plus one ask that binds them.
180
202
  Then tell the user where to look and end your turn.`,
181
- read: `mockpit read [slug] [--project <name>] [--body] [--history]
182
- 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
183
205
  the user is doing now. A slug: the mock's states, variants, asks with
184
- answers, parts, knobs and last tuned values, plus its pending, as JSON.
185
- --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.`,
186
210
  feedback: `mockpit feedback [--project <name>] [--session <id>]
187
211
  Returns at once with {feedback, pending} as JSON: one batch per mock the user
188
212
  sent since you last heard ({mock, reply: {asks, mix, tuned, comments},
189
- 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
190
215
  every write; and pending ({mock, viewerOpen, draft}) so you can tell the
191
216
  user "take your time" or "2 of 3 answered". Run it when the user says they
192
217
  answered.`,
@@ -203,7 +228,8 @@ mockpit publish --mock <slug> --parts <name=file|-> ... [options]
203
228
  Claude Code, arm it under Monitor after asking, then end the turn).
204
229
  Delivery is shared with feedback and every write: each Send arrives once.`,
205
230
  export: `mockpit export --mock <slug> [--state s] [--variant v] [--out <dir>]
206
- 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.`,
207
233
  upload: `mockpit upload <file> [--kind image|file] [--session <id>]
208
234
  Upload an asset; prints its id and URL. Use the id as an image surface's assetId.`,
209
235
  guide: `mockpit guide [--topic <id>] [--project <name>]
@@ -810,6 +836,7 @@ function printPublished(result, flags) {
810
836
  console.log(`parts${state ? ` (${state})` : ""}: ${parts.map((p) => p.name).join(", ")}`);
811
837
  }
812
838
  if (result.applied?.length) console.log(`applied: ${result.applied.join(", ")}`);
839
+ if (result.flow) console.log(flowLine(result.flow));
813
840
  const changes = result.partChanges;
814
841
  if (changes?.renamed?.length) {
815
842
  console.log(`renamed: ${changes.renamed.map((r) => `${r.from} → ${r.to}`).join(", ")}`);
@@ -822,6 +849,11 @@ function printPublished(result, flags) {
822
849
  printFeedback(result.feedback);
823
850
  }
824
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
+
825
857
  // Only speak when there IS feedback: an empty line per write is pure token
826
858
  // cost, and the cursor guarantees anything pending arrives on some write.
827
859
  function printFeedback(feedback) {
@@ -852,9 +884,15 @@ function parsePublishFlags() {
852
884
  html: { type: "string" },
853
885
  surfaces: { type: "string" },
854
886
  parts: { type: "string", multiple: true },
887
+ fills: { type: "string", multiple: true },
855
888
  knobs: { type: "string" },
856
889
  from: { type: "string" },
890
+ fork: { type: "string" },
857
891
  prompt: { type: "string" },
892
+ note: { type: "string" },
893
+ widths: { type: "string" },
894
+ flow: { type: "string" },
895
+ step: { type: "string" },
858
896
  kit: { type: "string", multiple: true },
859
897
  layout: { type: "string" },
860
898
  md: { type: "string", multiple: true },
@@ -873,18 +911,27 @@ function parsePublishFlags() {
873
911
 
874
912
  // `--parts name=file` (repeatable; `name#key` targets one instance): the new
875
913
  // outer html of one part, spliced server-side into the latest version.
876
- 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") {
877
916
  if (!values?.length) return undefined;
878
- 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`;
879
921
  const parts = {};
880
922
  let stdin = false;
881
923
  for (const raw of values) {
882
924
  const at = raw.indexOf("=");
883
- 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
+ }
884
931
  const name = raw.slice(0, at);
885
932
  const file = raw.slice(at + 1);
886
933
  if (file === "-") {
887
- if (stdin) die("only one --parts can read stdin", example);
934
+ if (stdin) die(`only one --${flag} can read stdin`, example);
888
935
  stdin = true;
889
936
  } else if (!existsSync(file)) die(`cannot read ${file}`, `ls ${file}`);
890
937
  parts[name] = readContent(file);
@@ -900,6 +947,13 @@ async function publishMock() {
900
947
  if (from !== undefined && !Number.isInteger(from)) {
901
948
  die(`--from must be a version number (got "${flags.from}")`, `mockpit read ${slug} --history`);
902
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
+ }
903
957
  const file = flags.html ?? positionals[0];
904
958
  const composed =
905
959
  file !== undefined || tokens.some((t) => t.kind === "option" && SURFACE_FLAGS.has(t.name));
@@ -907,16 +961,18 @@ async function publishMock() {
907
961
  composed && "--html",
908
962
  flags.surfaces !== undefined && "--surfaces",
909
963
  flags.parts?.length && "--parts",
964
+ flags.fills?.length && "--fills",
910
965
  ].filter(Boolean);
911
966
  if (given.length > 1) {
912
967
  die(
913
- `pass one of --html, --surfaces or --parts, not ${given.join(" and ")}`,
968
+ `pass one of --html, --surfaces, --parts or --fills, not ${given.join(" and ")}`,
914
969
  `mockpit publish --help`,
915
970
  );
916
971
  }
917
972
  // Checked before the session exists so a typo'd path leaves nothing behind on the server.
918
973
  if (file && file !== "-" && !existsSync(file)) die(`cannot read ${file}`, `ls ${file}`);
919
974
  const parts = readPartFlags(flags.parts, slug);
975
+ const fills = readPartFlags(flags.fills, slug, "fills");
920
976
  let surfaces =
921
977
  flags.surfaces === undefined ? undefined : readJsonFlag("surfaces", flags.surfaces);
922
978
  if (surfaces !== undefined && !Array.isArray(surfaces)) {
@@ -936,7 +992,7 @@ async function publishMock() {
936
992
  }
937
993
  surfaces.push(...(await surfacesFromFlags(flags, tokens, { session, layout: flags.layout })));
938
994
  }
939
- if (!surfaces && !parts)
995
+ if (!surfaces && !parts && !fills && !flags.fork && flags.flow === undefined)
940
996
  die(`no html for ${slug}`, `mockpit publish --mock ${slug} --html <file>`);
941
997
  const body = {
942
998
  session,
@@ -948,9 +1004,20 @@ async function publishMock() {
948
1004
  kind: flags.kind,
949
1005
  ...(flags.knobs !== undefined && { knobs: readJsonFlag("knobs", flags.knobs) }),
950
1006
  ...(from !== undefined && { from }),
1007
+ ...(flags.fork !== undefined && { fork: flags.fork }),
951
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 }),
952
1018
  ...(surfaces && { surfaces }),
953
1019
  ...(parts && { parts }),
1020
+ ...(fills && { fills }),
954
1021
  };
955
1022
  const result = await api(
956
1023
  "/api/mocks",
@@ -962,6 +1029,7 @@ async function publishMock() {
962
1029
  }
963
1030
 
964
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);
965
1033
  // "Above=knob:trim.position=top" sets one knob. The value is JSON when it
966
1034
  // parses (17, true), else a string, so slider and toggle values keep their type.
967
1035
  function parseOption(raw) {
@@ -980,7 +1048,11 @@ function parseOption(raw) {
980
1048
  }
981
1049
  const at = raw.lastIndexOf("=");
982
1050
  if (at <= 0) return { label: raw };
983
- 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]) };
984
1056
  }
985
1057
 
986
1058
  // One feedback batch, one line — for a background monitor's notifications.
@@ -1005,6 +1077,12 @@ function watchLines(batch) {
1005
1077
  const lines = [];
1006
1078
  const name = batch.mock ?? "a mock";
1007
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
+ }
1008
1086
  if (batch.reply) {
1009
1087
  const r = batch.reply;
1010
1088
  const bits = [
@@ -1340,6 +1418,7 @@ const commands = {
1340
1418
  part: { type: "string" },
1341
1419
  multi: { type: "boolean" },
1342
1420
  id: { type: "string" },
1421
+ after: { type: "string" },
1343
1422
  asks: { type: "string" },
1344
1423
  },
1345
1424
  });
@@ -1377,6 +1456,7 @@ const commands = {
1377
1456
  ...(flags.state && { state: flags.state }),
1378
1457
  ...(flags.part && { part: flags.part }),
1379
1458
  ...(flags.multi && { multi: true }),
1459
+ ...(flags.after && { after: flags.after }),
1380
1460
  },
1381
1461
  ];
1382
1462
  }
@@ -1414,10 +1494,20 @@ const commands = {
1414
1494
  project: { type: "string" },
1415
1495
  body: { type: "boolean" },
1416
1496
  history: { type: "boolean" },
1497
+ tree: { type: "boolean" },
1417
1498
  },
1418
1499
  });
1419
1500
  const project = resolveProject(flags).name;
1420
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
+ }
1421
1511
  if (positionals.length === 1) {
1422
1512
  const slug = slugify(positionals[0]);
1423
1513
  return out(
@@ -1445,9 +1535,10 @@ const commands = {
1445
1535
  for (const m of mocks) {
1446
1536
  const states = m.states.length ? m.states.join(" / ") : "single state";
1447
1537
  console.log(
1448
- ` ${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` : ""}`,
1449
1539
  );
1450
1540
  }
1541
+ for (const flow of list.flows ?? []) console.log(` ${flowLine(flow)}`);
1451
1542
  for (const line of pendingLines(list.pending)) console.log(line);
1452
1543
  },
1453
1544
  async export() {
@@ -1473,6 +1564,15 @@ const commands = {
1473
1564
  const dir = join(root, entry.state === null ? "default" : slugify(entry.state));
1474
1565
  mkdirSync(dir, { recursive: true });
1475
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
+ }
1476
1576
  writeFileSync(
1477
1577
  join(dir, "history.json"),
1478
1578
  JSON.stringify(
@@ -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) {
@@ -1,5 +1,5 @@
1
1
  import { type PartInfo } from "./parts.ts";
2
- import { type Mock, type Post, type PostVersion, type Session, type Surface } from "./types.ts";
2
+ import { type Embed, type KnobValue, type Mock, type Post, type PostVersion, type Session, type Surface } from "./types.ts";
3
3
  export declare const surfaceRef: (surface: Pick<Surface, "id" | "kind">, index: number) => {
4
4
  id: string | undefined;
5
5
  kind: "html" | "diff" | "image" | "markdown" | "terminal" | "mermaid" | "json" | "code";
@@ -56,7 +56,7 @@ export declare const fullSurfaceView: (surface: Surface, index: number) => {
56
56
  lineStart?: number;
57
57
  id?: string;
58
58
  };
59
- export declare const hydratedSurfaceView: (surface: Surface, index: number) => Record<string, unknown> | {
59
+ export declare const hydratedSurfaceView: (surface: Surface, index: number) => {
60
60
  index: number;
61
61
  kind: "html";
62
62
  html: string;
@@ -106,7 +106,7 @@ export declare const hydratedSurfaceView: (surface: Surface, index: number) => R
106
106
  title?: string;
107
107
  lineStart?: number;
108
108
  id?: string;
109
- };
109
+ } | Record<string, unknown>;
110
110
  export declare const historyMetaView: (version: PostVersion) => {
111
111
  surfaceKinds: ("html" | "diff" | "image" | "markdown" | "terminal" | "mermaid" | "json" | "code")[];
112
112
  author?: string | undefined;
@@ -119,9 +119,17 @@ export declare const historyMetaView: (version: PostVersion) => {
119
119
  export interface VariantViewOptions {
120
120
  body?: boolean;
121
121
  history?: boolean;
122
+ embeds?: Map<string, EmbedView>;
123
+ }
124
+ export interface EmbedView {
125
+ key: string;
126
+ list: (Embed & {
127
+ latest: number;
128
+ versions: number[];
129
+ })[];
122
130
  }
123
131
  export declare const variantView: (post: Post, opts?: VariantViewOptions) => {
124
- history?: {
132
+ history?: ({
125
133
  surfaceKinds: ("html" | "diff" | "image" | "markdown" | "terminal" | "mermaid" | "json" | "code")[];
126
134
  author?: string | undefined;
127
135
  prompt?: string | undefined;
@@ -129,9 +137,18 @@ export declare const variantView: (post: Post, opts?: VariantViewOptions) => {
129
137
  version: number;
130
138
  title: string;
131
139
  at: string;
132
- }[] | undefined;
140
+ } | {
141
+ hidden: true;
142
+ surfaceKinds: ("html" | "diff" | "image" | "markdown" | "terminal" | "mermaid" | "json" | "code")[];
143
+ author?: string | undefined;
144
+ prompt?: string | undefined;
145
+ from?: number | undefined;
146
+ version: number;
147
+ title: string;
148
+ at: string;
149
+ })[] | undefined;
133
150
  parts: string[];
134
- surfaces: (Record<string, unknown> | {
151
+ surfaces: ({
135
152
  index: number;
136
153
  kind: "html";
137
154
  html: string;
@@ -181,9 +198,21 @@ export declare const variantView: (post: Post, opts?: VariantViewOptions) => {
181
198
  title?: string;
182
199
  lineStart?: number;
183
200
  id?: string;
184
- })[];
185
- slots?: import("./types.ts").Slot[] | undefined;
186
- knobs?: import("./types.ts").Knobs | undefined;
201
+ } | Record<string, unknown>)[];
202
+ acceptedVersion?: number | undefined;
203
+ starred?: boolean | undefined;
204
+ forkedFrom?: import("./types.ts").ForkRef | undefined;
205
+ publishedWith?: Embed[] | undefined;
206
+ embeds?: (Embed & {
207
+ latest: number;
208
+ versions: number[];
209
+ })[] | undefined;
210
+ embedsKey?: string | undefined;
211
+ note?: string | undefined;
212
+ slots?: import("./fills.ts").SlotInfo[] | undefined;
213
+ knobs?: {
214
+ [x: string]: import("./types.ts").KnobConfig;
215
+ } | undefined;
187
216
  author?: string | undefined;
188
217
  prompt?: string | undefined;
189
218
  from?: number | undefined;
@@ -197,6 +226,11 @@ export declare const variantView: (post: Post, opts?: VariantViewOptions) => {
197
226
  createdAt: string;
198
227
  updatedAt: string;
199
228
  };
229
+ export declare const inUseByState: (mock: Mock, posts: Post[]) => {
230
+ state: string | null;
231
+ variant: string;
232
+ version: number;
233
+ }[];
200
234
  export interface StateParts {
201
235
  state: string | null;
202
236
  parts: PartInfo[];
@@ -204,6 +238,7 @@ export interface StateParts {
204
238
  export declare function partsByState(mock: Mock, posts: Post[]): StateParts[];
205
239
  export declare const mockSummaryView: (mock: Mock, posts: Post[]) => {
206
240
  sessionId: string | null;
241
+ resolvedAt: string | null;
207
242
  createdAt: string;
208
243
  updatedAt: string;
209
244
  thumbnail: {
@@ -223,7 +258,7 @@ export declare const mockSummaryView: (mock: Mock, posts: Post[]) => {
223
258
  open: number;
224
259
  };
225
260
  export declare const mockDetailView: (mock: Mock, posts: Post[], opts?: VariantViewOptions & {
226
- tuned?: Record<string, unknown>;
261
+ settled?: Record<string, KnobValue>;
227
262
  }) => {
228
263
  id: string;
229
264
  project: string;
@@ -234,11 +269,22 @@ export declare const mockDetailView: (mock: Mock, posts: Post[], opts?: VariantV
234
269
  asks: import("./types.ts").Ask[];
235
270
  open: number;
236
271
  knobs: import("./types.ts").Knobs;
272
+ widths: number[];
237
273
  sessionId: string | null;
274
+ resolvedAt: string | null;
238
275
  createdAt: string;
239
276
  updatedAt: string;
240
277
  variants: {
241
- history?: {
278
+ history?: ({
279
+ surfaceKinds: ("html" | "diff" | "image" | "markdown" | "terminal" | "mermaid" | "json" | "code")[];
280
+ author?: string | undefined;
281
+ prompt?: string | undefined;
282
+ from?: number | undefined;
283
+ version: number;
284
+ title: string;
285
+ at: string;
286
+ } | {
287
+ hidden: true;
242
288
  surfaceKinds: ("html" | "diff" | "image" | "markdown" | "terminal" | "mermaid" | "json" | "code")[];
243
289
  author?: string | undefined;
244
290
  prompt?: string | undefined;
@@ -246,9 +292,9 @@ export declare const mockDetailView: (mock: Mock, posts: Post[], opts?: VariantV
246
292
  version: number;
247
293
  title: string;
248
294
  at: string;
249
- }[] | undefined;
295
+ })[] | undefined;
250
296
  parts: string[];
251
- surfaces: (Record<string, unknown> | {
297
+ surfaces: ({
252
298
  index: number;
253
299
  kind: "html";
254
300
  html: string;
@@ -298,9 +344,21 @@ export declare const mockDetailView: (mock: Mock, posts: Post[], opts?: VariantV
298
344
  title?: string;
299
345
  lineStart?: number;
300
346
  id?: string;
301
- })[];
302
- slots?: import("./types.ts").Slot[] | undefined;
303
- knobs?: import("./types.ts").Knobs | undefined;
347
+ } | Record<string, unknown>)[];
348
+ acceptedVersion?: number | undefined;
349
+ starred?: boolean | undefined;
350
+ forkedFrom?: import("./types.ts").ForkRef | undefined;
351
+ publishedWith?: Embed[] | undefined;
352
+ embeds?: (Embed & {
353
+ latest: number;
354
+ versions: number[];
355
+ })[] | undefined;
356
+ embedsKey?: string | undefined;
357
+ note?: string | undefined;
358
+ slots?: import("./fills.ts").SlotInfo[] | undefined;
359
+ knobs?: {
360
+ [x: string]: import("./types.ts").KnobConfig;
361
+ } | undefined;
304
362
  author?: string | undefined;
305
363
  prompt?: string | undefined;
306
364
  from?: number | undefined;
@@ -314,8 +372,13 @@ export declare const mockDetailView: (mock: Mock, posts: Post[], opts?: VariantV
314
372
  createdAt: string;
315
373
  updatedAt: string;
316
374
  }[];
375
+ inUse: {
376
+ state: string | null;
377
+ variant: string;
378
+ version: number;
379
+ }[];
317
380
  parts: StateParts[];
318
- tuned: Record<string, unknown>;
381
+ settled: Record<string, KnobValue>;
319
382
  };
320
383
  export declare const sessionRowView: (session: Session, postCount: number) => {
321
384
  postCount: number;
@@ -330,3 +393,41 @@ export declare const sessionRowView: (session: Session, postCount: number) => {
330
393
  agentActiveAt: string | null;
331
394
  keyed: boolean;
332
395
  };
396
+ export declare const treeNodeView: (mock: Mock, posts: Post[], extra: {
397
+ settled: Record<string, KnobValue>;
398
+ uses: string[];
399
+ embeds: Map<string, EmbedView>;
400
+ }) => {
401
+ id: string;
402
+ slug: string;
403
+ title: string;
404
+ kind: import("./types.ts").MockKind;
405
+ open: number;
406
+ states: string[] | null[];
407
+ widths: number[];
408
+ createdAt: string;
409
+ updatedAt: string;
410
+ resolvedAt: string | null;
411
+ settled: Record<string, KnobValue>;
412
+ uses: string[];
413
+ usedIn: string[];
414
+ inUse: {
415
+ state: string | null;
416
+ variant: string;
417
+ version: number;
418
+ }[];
419
+ variants: {
420
+ embedsKey?: string | undefined;
421
+ acceptedVersion?: number | undefined;
422
+ starred?: boolean | undefined;
423
+ forkedFrom?: import("./types.ts").ForkRef | undefined;
424
+ note?: string | undefined;
425
+ postId: string;
426
+ state: string | null;
427
+ variant: string;
428
+ status: import("./types.ts").PostStatus;
429
+ version: number;
430
+ updatedAt: string;
431
+ surface: number;
432
+ }[];
433
+ };