mockpit 1.3.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +66 -202
  2. package/bin/demoData.js +5 -5
  3. package/bin/initDesign.js +6 -6
  4. package/bin/mockpit.js +687 -192
  5. package/bin/poster.js +377 -0
  6. package/dist/mcp/server.js +110 -19
  7. package/dist/server/answerAdditions.d.ts +7 -0
  8. package/dist/server/answerAdditions.js +76 -0
  9. package/dist/server/apiViews.d.ts +563 -41
  10. package/dist/server/apiViews.js +223 -74
  11. package/dist/server/app.d.ts +10 -2
  12. package/dist/server/app.js +3284 -782
  13. package/dist/server/appIcon.js +15 -5
  14. package/dist/server/appIconPng.js +3 -3
  15. package/dist/server/askNudges.js +1 -1
  16. package/dist/server/cdnInline.d.ts +8 -0
  17. package/dist/server/cdnInline.js +638 -0
  18. package/dist/server/codeWorker.js +2 -1
  19. package/dist/server/decisionList.d.ts +45 -0
  20. package/dist/server/decisionList.js +302 -0
  21. package/dist/server/decisions.d.ts +8 -8
  22. package/dist/server/decisions.js +18 -17
  23. package/dist/server/designGuide.d.ts +10 -6
  24. package/dist/server/designGuide.js +173 -78
  25. package/dist/server/embeds.d.ts +4 -1
  26. package/dist/server/embeds.js +57 -6
  27. package/dist/server/eventLog.d.ts +2 -2
  28. package/dist/server/eventLog.js +11 -11
  29. package/dist/server/events.d.ts +9 -6
  30. package/dist/server/feedbackBatch.d.ts +38 -16
  31. package/dist/server/feedbackBatch.js +121 -50
  32. package/dist/server/fills.d.ts +4 -0
  33. package/dist/server/fills.js +55 -1
  34. package/dist/server/guidance.d.ts +23 -0
  35. package/dist/server/guidance.js +71 -0
  36. package/dist/server/handoff.d.ts +4 -1
  37. package/dist/server/handoff.js +8 -2
  38. package/dist/server/icons.js +1 -1
  39. package/dist/server/index.js +49 -11
  40. package/dist/server/jump.d.ts +21 -0
  41. package/dist/server/jump.js +175 -0
  42. package/dist/server/knobs.d.ts +3 -3
  43. package/dist/server/knobs.js +6 -6
  44. package/dist/server/mcpHttp.d.ts +8 -1
  45. package/dist/server/mcpHttp.js +44 -7
  46. package/dist/server/mcpSpec.d.ts +2 -1
  47. package/dist/server/mcpSpec.js +172 -50
  48. package/dist/server/parts.js +9 -1
  49. package/dist/server/postSurfaces.js +3 -0
  50. package/dist/server/run.d.ts +1 -1
  51. package/dist/server/run.js +31 -17
  52. package/dist/server/runApi.d.ts +3 -3
  53. package/dist/server/runApi.js +34 -24
  54. package/dist/server/sqlStore.d.ts +117 -94
  55. package/dist/server/sqlStore.js +1089 -141
  56. package/dist/server/standings.d.ts +25 -0
  57. package/dist/server/standings.js +113 -0
  58. package/dist/server/surfacePage.d.ts +6 -9
  59. package/dist/server/surfacePage.js +361 -86
  60. package/dist/server/types.d.ts +252 -69
  61. package/dist/server/types.js +87 -69
  62. package/extensions/mockpit.js +245 -65
  63. package/guide/AGENT_SETUP.md +13 -1
  64. package/guide/packs/design-craft.md +34 -0
  65. package/guide/topics/asks.md +117 -69
  66. package/guide/topics/feedback.md +156 -119
  67. package/guide/topics/html.md +19 -32
  68. package/guide/topics/http.md +43 -40
  69. package/guide/topics/knobs.md +7 -10
  70. package/guide/topics/scripts.md +24 -35
  71. package/guide/topics/surfaces.md +50 -41
  72. package/opencode/mockpit.js +120 -0
  73. package/opencode/package.json +8 -0
  74. package/package.json +4 -6
  75. package/skills/launch-video/SKILL.md +12 -12
  76. package/skills/mockpit/SKILL.md +35 -23
  77. package/viewer/dist/index.html +9 -8
@@ -23,15 +23,15 @@ const CONTENT_TYPES = {
23
23
  };
24
24
 
25
25
  const feedbackGuideline =
26
- "Mockpit tool results may include feedback from the browser; treat it as user instruction and respond or revise the mock.";
26
+ "Mockpit tool results may include feedback from the browser; treat it as user instruction and respond or revise the design.";
27
27
 
28
28
  // The never-block loop, in the words of every other tier: nothing waits for
29
29
  // the user, so the agent ends its turn and reads feedback when told.
30
30
  const loopGuidelines = [
31
- "Publish each variant with mockpit_publish (name states in the user's words; mark parts with data-part); publish again to revise.",
32
- "A choice is several variants plus one ask that binds them: mockpit_ask with options bound to the variants. One render plus a control: declare a knob.",
33
- "Tell the user in one line where to look, then end your turn. The question lives in the mock, never in chat. Never poll.",
34
- "When the user says they answered, call mockpit_feedback. Every write also returns feedback; read it.",
31
+ "Publish each variant with mockpit_publish (name states in the user's words; declare parts with data-part); publish again to revise.",
32
+ "A choice is several variants plus one question that binds them: mockpit_ask with options bound to the variants. One render plus a control: declare a knob.",
33
+ "Tell the user in one line where to look, then end your turn. The question lives in the design, never in chat. Never poll.",
34
+ "A reply sent from the viewer arrives on its own as a mockpit message; act on it. If the user says they answered and none came, call mockpit_feedback. Every write also returns feedback; read it.",
35
35
  ];
36
36
 
37
37
  const surfaceSchema = {
@@ -209,55 +209,99 @@ async function requestText(path) {
209
209
  return text;
210
210
  }
211
211
 
212
+ // A poster may name an image file: it goes up as an asset, and the publish
213
+ // carries its id. Anything else is already an asset id.
214
+ async function posterId(value, ctx) {
215
+ if (typeof value !== "string" || !value) return undefined;
216
+ const file = resolve(ctx.cwd, value);
217
+ let data;
218
+ try {
219
+ data = await readFile(file);
220
+ } catch {
221
+ return value;
222
+ }
223
+ const ext = file.split(".").pop()?.toLowerCase();
224
+ const asset = await requestJson("/api/assets", {
225
+ method: "POST",
226
+ body: JSON.stringify({
227
+ data: data.toString("base64"),
228
+ contentType: ext === "jpg" || ext === "jpeg" ? "image/jpeg" : `image/${ext}`,
229
+ filename: file.split(/[\\/]/).pop(),
230
+ }),
231
+ });
232
+ return asset.id;
233
+ }
234
+
212
235
  function rememberSession(state, sessionId) {
213
236
  if (typeof sessionId === "string" && sessionId) state.sessionId = sessionId;
214
237
  }
215
238
 
216
- // `feedback` is one batch per mock: the user's reply (answers, mix, tuned
217
- // knob values, part comments) plus any plain comments. Rendered one line per
218
- // thing the user said, so nothing is summarized away.
239
+ // `feedback` is one batch per design: the user's reply (answers, borrowed parts,
240
+ // tuned knob values, part comments, verdict) plus any thread messages.
241
+ // Rendered one line per thing the user said, so nothing is summarized away.
219
242
  function feedbackLines(feedback) {
220
243
  const lines = [];
221
244
  for (const batch of feedback ?? []) {
222
- const prefix = batch.mock ? `${batch.mock}: ` : "";
245
+ const prefix = batch.design ? `${batch.design}: ` : "";
223
246
  if (batch.handoff) lines.push(`- ${prefix}${batch.handoff}`);
224
247
  const reply = batch.reply;
225
248
  if (reply) {
226
249
  for (const a of reply.asks ?? []) {
227
250
  const chosen = a.chosen.map((o) => (o.other ? `“${o.label}”` : o.label)).join(", ");
228
251
  lines.push(`- ${prefix}${a.text || a.ask} → ${chosen || "(no pick)"}`);
229
- if (a.note) lines.push(` note: ${a.note}`);
252
+ if (a.comment) lines.push(` comment: ${a.comment}`);
230
253
  }
231
- for (const [part, variant] of Object.entries(reply.mix ?? {})) {
232
- lines.push(`- ${prefix}use ${variant}'s ${part}`);
254
+ for (const [part, variant] of Object.entries(reply.borrowed ?? {})) {
255
+ lines.push(`- ${prefix}borrow ${part} from ${variant}`);
233
256
  }
234
257
  for (const [path, value] of Object.entries(reply.tuned ?? {})) {
235
258
  lines.push(`- ${prefix}tuned ${path} = ${JSON.stringify(value)}`);
236
259
  }
237
260
  for (const c of reply.comments ?? []) {
261
+ if (c.borrow) {
262
+ lines.push(
263
+ `- ${prefix}borrow ${c.borrow.part ? `${c.borrow.part} ` : ""}from ${c.borrow.from} into ${c.variant}@${c.version}${c.text ? `: ${c.text}` : ""}`,
264
+ );
265
+ continue;
266
+ }
238
267
  lines.push(
239
268
  `- ${prefix}${c.part ? `[${c.part}${c.state ? ` · ${c.state}` : ""}] ` : ""}${c.text}`,
240
269
  );
241
270
  }
242
- if (reply.decision) lines.push(`- ${prefix}${reply.decision.kind} ${reply.decision.variant}`);
271
+ if (reply.verdict) lines.push(`- ${prefix}${reply.verdict.kind} ${reply.verdict.variant}`);
243
272
  if (reply.text) lines.push(`- ${prefix}${reply.text}`);
244
273
  }
245
- for (const c of batch.comments ?? []) lines.push(`- ${prefix}${c.text}`);
274
+ for (const c of batch.messages ?? []) lines.push(`- ${prefix}${c.text}`);
275
+ if (batch.edited) {
276
+ const e = batch.edited;
277
+ const moved = [
278
+ e.kind ? `now a ${e.kind}` : "",
279
+ e.area !== undefined ? (e.area ? `area ${e.area}` : "no area") : "",
280
+ ].filter(Boolean);
281
+ if (moved.length) lines.push(`- ${prefix}the user moved it from Home (${moved.join(", ")})`);
282
+ if (e.notes !== undefined) {
283
+ lines.push(
284
+ e.notes === null
285
+ ? `- ${prefix}the user cleared its notes`
286
+ : `- ${prefix}the user edited its notes: ${e.notes}`,
287
+ );
288
+ }
289
+ }
246
290
  }
247
291
  return lines;
248
292
  }
249
293
 
250
- // What the user is doing right now, per mock, so the agent can say "take your time".
294
+ // What the user is doing right now, per design, so the agent can say "take your time".
251
295
  function pendingLines(pending) {
252
296
  const lines = [pending ?? []]
253
297
  .flat()
254
298
  .flatMap((p) =>
255
299
  p.draft
256
300
  ? [
257
- `- ${p.mock}: the user is answering (${p.draft.answered} of ${p.draft.of} answered, ${p.draft.comments} comments)`,
301
+ `- ${p.design}: the user is answering (${p.draft.answered} of ${p.draft.of} answered, ${p.draft.comments} comments)`,
258
302
  ]
259
303
  : p.viewerOpen
260
- ? [`- ${p.mock}: open in the viewer`]
304
+ ? [`- ${p.design}: open in the viewer`]
261
305
  : [],
262
306
  );
263
307
  return lines.length ? ["Pending:", ...lines] : [];
@@ -269,6 +313,42 @@ function feedbackSummary(feedback) {
269
313
  return `\n\nUser feedback delivered with this result:\n${lines.join("\n")}`;
270
314
  }
271
315
 
316
+ const pause = (ms, signal) =>
317
+ new Promise((done) => {
318
+ const timer = setTimeout(done, ms);
319
+ signal.addEventListener("abort", () => (clearTimeout(timer), done()), { once: true });
320
+ });
321
+
322
+ // The user's reply wakes the idle agent, as the Claude Code plugin's hook
323
+ // does. Reading feedback advances the server's cursor, so the message carries
324
+ // the whole reply: a nudge to call mockpit_feedback would come back empty.
325
+ // followUp waits for a busy agent to finish rather than cutting into its work.
326
+ async function watchReplies(pi, state, signal) {
327
+ while (!signal.aborted) {
328
+ if (!state.sessionId) {
329
+ await pause(2000, signal);
330
+ continue;
331
+ }
332
+ try {
333
+ const query = new URLSearchParams({ session: state.sessionId, author: "user", wait: "60" });
334
+ const result = await requestJson(`/api/comments?${query}`, { signal });
335
+ const lines = feedbackLines(result.feedback);
336
+ if (!lines.length || signal.aborted) continue;
337
+ pi.sendMessage(
338
+ {
339
+ customType: "mockpit-reply",
340
+ content: `The user replied in mockpit:\n${lines.join("\n")}`,
341
+ display: true,
342
+ details: { feedback: result.feedback },
343
+ },
344
+ { triggerTurn: true, deliverAs: "followUp" },
345
+ );
346
+ } catch {
347
+ await pause(2000, signal);
348
+ }
349
+ }
350
+ }
351
+
272
352
  function reconstructSession(ctx) {
273
353
  let sessionId = process.env.MOCKPIT_SESSION || undefined;
274
354
  for (const entry of ctx.sessionManager.getBranch()) {
@@ -286,9 +366,17 @@ function reconstructSession(ctx) {
286
366
 
287
367
  export default function mockpitExtension(pi) {
288
368
  const state = { sessionId: process.env.MOCKPIT_SESSION || undefined };
369
+ let watching;
289
370
 
290
371
  pi.on("session_start", (_event, ctx) => {
291
372
  state.sessionId = reconstructSession(ctx);
373
+ watching?.abort();
374
+ watching = undefined;
375
+ // print and json runs end after one prompt; nobody is there to wake.
376
+ if (ctx.mode === "tui" || ctx.mode === "rpc") {
377
+ watching = new AbortController();
378
+ void watchReplies(pi, state, watching.signal);
379
+ }
292
380
  ctx.ui.setStatus(
293
381
  "mockpit",
294
382
  state.sessionId
@@ -297,6 +385,12 @@ export default function mockpitExtension(pi) {
297
385
  );
298
386
  });
299
387
 
388
+ // After shutdown pi refuses calls from this extension, so the loop stops first.
389
+ pi.on("session_shutdown", () => {
390
+ watching?.abort();
391
+ watching = undefined;
392
+ });
393
+
300
394
  pi.registerCommand("mockpit", {
301
395
  description: "Show mockpit extension status or reset its remembered session: /mockpit [reset]",
302
396
  handler: async (args, ctx) => {
@@ -315,7 +409,7 @@ export default function mockpitExtension(pi) {
315
409
  });
316
410
 
317
411
  const projectOf = (params, ctx) => params.project ?? resolveProjectName(ctx.cwd);
318
- const mockPath = (mock) => `/api/mocks/${encodeURIComponent(mock)}`;
412
+ const designPath = (design) => `/api/designs/${encodeURIComponent(design)}`;
319
413
 
320
414
  // GET /api/feedback reads from a session's cursor, so a feedback call before
321
415
  // any write still needs one; later writes then land in the same session.
@@ -334,12 +428,12 @@ export default function mockpitExtension(pi) {
334
428
  return created.id;
335
429
  }
336
430
 
337
- // project › mock › state › variant › version.
338
- const mockProps = {
339
- mock: { type: "string", description: "Mock slug, e.g. writer" },
431
+ // project › design › state › variant › version.
432
+ const designProps = {
433
+ design: { type: "string", description: "Design slug, e.g. writer" },
340
434
  state: {
341
435
  type: "string",
342
- description: "State label in the user's words; omit for a single-state mock",
436
+ description: "State label in the user's words; omit for a single-state design",
343
437
  },
344
438
  variant: { type: "string", description: 'Variant label; default "default"' },
345
439
  project: { type: "string", description: "Project name; defaults to the repo" },
@@ -349,40 +443,52 @@ export default function mockpitExtension(pi) {
349
443
  name: "mockpit_publish",
350
444
  label: "Mockpit Publish",
351
445
  description:
352
- "Create or version one variant of one state of a mock in the user's browser. One of html (or path), surfaces (the full ordered list) or parts (splices the latest version). Returns parts per state, nudges, feedback.",
353
- promptSnippet: "Publish a UI mock (state + variant) to mockpit for the user to review.",
446
+ "Create or version one variant of one state of a design in the user's browser. One of html (or path), surfaces (the full ordered list) or parts (splices the latest version). Returns parts per state, nudges, feedback.",
447
+ promptSnippet: "Publish a UI design (state + variant) to mockpit for the user to review.",
354
448
  promptGuidelines: [...loopGuidelines, feedbackGuideline],
355
449
  parameters: {
356
450
  type: "object",
357
451
  properties: {
358
- ...mockProps,
359
- title: { type: "string", description: "Mock title" },
360
- kind: { type: "string", enum: ["component", "page"], description: "Mock kind" },
361
- html: { type: "string", description: "HTML body fragment; mark parts with data-part" },
452
+ ...designProps,
453
+ title: { type: "string", description: "Design title" },
454
+ kind: { type: "string", enum: ["component", "screen"], description: "Design kind" },
455
+ area: {
456
+ type: "string",
457
+ description: 'Where in the product it belongs ("Shell"); Home groups by it. "" clears',
458
+ },
459
+ source: { type: "string", description: 'Code path that implements it. "" clears' },
460
+ html: { type: "string", description: "HTML body fragment; declare parts with data-part" },
362
461
  path: { type: "string", description: "File to read the html from instead" },
462
+ poster: {
463
+ type: "string",
464
+ description:
465
+ "A still of the page (image file path, or asset id) that Studio and Home show instead of running it; send one with every version that changes the page",
466
+ },
363
467
  surfaces: surfacesSchema,
364
468
  parts: {
365
469
  type: "object",
366
470
  description:
367
- '{"name"|"name#key": outer html}; splices marked parts of the latest version',
471
+ '{"name"|"name#key": outer html}; splices declared parts of the latest version',
368
472
  },
369
473
  knobs: { type: "object", description: "Knobs in tunekit usePane shape, keyed by path" },
370
474
  variantKnobs: { type: "object", description: "Knobs this variant alone has" },
371
475
  from: { type: "number", description: "Branch from this version" },
372
476
  prompt: { type: "string", description: "What prompted this version" },
373
477
  },
374
- required: ["mock"],
478
+ required: ["design"],
375
479
  },
376
480
  async execute(_id, params, _signal, _onUpdate, ctx) {
377
481
  const html = params.path
378
482
  ? await readFile(resolve(ctx.cwd, params.path), "utf8")
379
483
  : params.html;
380
484
  const { path: _path, ...rest } = params;
381
- const result = await requestJson("/api/mocks", {
485
+ const poster = await posterId(params.poster, ctx);
486
+ const result = await requestJson("/api/designs", {
382
487
  method: "POST",
383
488
  body: JSON.stringify({
384
489
  ...rest,
385
490
  html,
491
+ ...(poster === undefined ? {} : { poster }),
386
492
  project: projectOf(params, ctx),
387
493
  session: state.sessionId,
388
494
  // Only used when this publish creates the session.
@@ -392,6 +498,17 @@ export default function mockpitExtension(pi) {
392
498
  });
393
499
  rememberSession(state, result.sessionId);
394
500
  const { post } = result;
501
+ if (result.planned) {
502
+ return {
503
+ content: [
504
+ {
505
+ type: "text",
506
+ text: `${result.design.slug}: planned, nothing drawn yet · ${result.url}${feedbackSummary(result.feedback)}`,
507
+ },
508
+ ],
509
+ details: { ...result, baseUrl: baseUrl() },
510
+ };
511
+ }
395
512
  const parts = (result.parts ?? [])
396
513
  .filter((s) => s.parts.length)
397
514
  .map(
@@ -404,7 +521,7 @@ export default function mockpitExtension(pi) {
404
521
  ]
405
522
  : [];
406
523
  const text = [
407
- `${result.mock.slug}/${post.state ? `${post.state}/` : ""}${post.variant} v${post.version} · ${result.url}`,
524
+ `${result.design.slug}/${post.state ? `${post.state}/` : ""}${post.variant} v${post.version} · ${result.url}`,
408
525
  ...parts,
409
526
  ...(result.applied?.length ? [`applied: ${result.applied.join(", ")}`] : []),
410
527
  ...changes,
@@ -424,7 +541,7 @@ export default function mockpitExtension(pi) {
424
541
  name: "mockpit_ask",
425
542
  label: "Mockpit Ask",
426
543
  description:
427
- "Ask structured questions on a mock. A choice is several variants plus one ask that binds them ({label, variant}); options may also set knob values ({label, set}). Reusing an ask id replaces it.",
544
+ "Ask structured questions on a design. A choice is several variants plus one question that binds them ({label, variant}); options may also set knob values ({label, set}). Reusing a question id replaces it.",
428
545
  promptSnippet: "Ask the user to choose between mockpit variants.",
429
546
  promptGuidelines: [
430
547
  "Use mockpit_ask once the variants are published, tell the user where to look, then end your turn.",
@@ -432,19 +549,19 @@ export default function mockpitExtension(pi) {
432
549
  parameters: {
433
550
  type: "object",
434
551
  properties: {
435
- mock: mockProps.mock,
436
- project: mockProps.project,
552
+ design: designProps.design,
553
+ project: designProps.project,
437
554
  asks: {
438
555
  type: "array",
439
556
  description:
440
- "Questions: {id?, text, scope: mock|state|part, state?, part?, multi?, options}",
557
+ "Questions: {id?, text, scope: design|state|part, state?, part?, multi?, options}",
441
558
  items: { type: "object" },
442
559
  },
443
560
  },
444
- required: ["mock", "asks"],
561
+ required: ["design", "asks"],
445
562
  },
446
563
  async execute(_id, params, _signal, _onUpdate, ctx) {
447
- const result = await requestJson(`${mockPath(params.mock)}/asks`, {
564
+ const result = await requestJson(`${designPath(params.design)}/asks`, {
448
565
  method: "POST",
449
566
  body: JSON.stringify({
450
567
  project: projectOf(params, ctx),
@@ -455,7 +572,7 @@ export default function mockpitExtension(pi) {
455
572
  const text = result.asks
456
573
  .map(
457
574
  (a) =>
458
- `Asked on ${result.mock}: ${a.text} [${a.options.map((o) => o.label).join(" | ")}]`,
575
+ `Asked on ${result.design}: ${a.text} [${a.options.map((o) => o.label).join(" | ")}]`,
459
576
  )
460
577
  .join("\n");
461
578
  return {
@@ -471,25 +588,25 @@ export default function mockpitExtension(pi) {
471
588
  name: "mockpit_read",
472
589
  label: "Mockpit Read",
473
590
  description:
474
- "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.",
475
- promptSnippet: "Read mockpit mocks.",
591
+ "Without design: every design (slug, states, variants, open asks) plus pending. With design: its states, variants, asks with answers, parts, knobs and last tuned values, plus pending. Never consumes feedback.",
592
+ promptSnippet: "Read mockpit designs.",
476
593
  promptGuidelines: [
477
- "Use mockpit_read to recover mock slugs or a mock's state when you lost track.",
594
+ "Use mockpit_read to recover design slugs or a design's state when you lost track.",
478
595
  ],
479
596
  parameters: {
480
597
  type: "object",
481
598
  properties: {
482
- mock: { type: "string", description: "Mock slug; omit for every mock" },
483
- project: mockProps.project,
599
+ design: { type: "string", description: "Design slug; omit for every design" },
600
+ project: designProps.project,
484
601
  body: { type: "boolean", description: "Include each variant's surfaces" },
485
602
  history: { type: "boolean", description: "Include version rows" },
486
603
  },
487
604
  },
488
605
  async execute(_id, params, _signal, _onUpdate, ctx) {
489
606
  const query = new URLSearchParams({ project: projectOf(params, ctx) });
490
- if (params.mock === undefined) {
491
- const result = await requestJson(`/api/mocks?${query}`);
492
- const lines = result.mocks.map(
607
+ if (params.design === undefined) {
608
+ const result = await requestJson(`/api/designs?${query}`);
609
+ const lines = result.designs.map(
493
610
  (m) =>
494
611
  `${m.slug} · ${m.kind} · ${m.states.length ? m.states.join(" / ") : "single state"} · ${m.variants} variants${m.open ? ` · ${m.open} open` : ""}`,
495
612
  );
@@ -498,7 +615,7 @@ export default function mockpitExtension(pi) {
498
615
  content: [
499
616
  {
500
617
  type: "text",
501
- text: [...lines, ...busy].join("\n") || `No mocks in ${query.get("project")}.`,
618
+ text: [...lines, ...busy].join("\n") || `No designs in ${query.get("project")}.`,
502
619
  },
503
620
  ],
504
621
  details: { ...result, baseUrl: baseUrl() },
@@ -506,8 +623,8 @@ export default function mockpitExtension(pi) {
506
623
  }
507
624
  if (params.body) query.set("body", "1");
508
625
  if (params.history) query.set("history", "1");
509
- const mock = await requestJson(`${mockPath(params.mock)}?${query}`);
510
- return { content: [{ type: "text", text: jsonText(mock) }], details: mock };
626
+ const design = await requestJson(`${designPath(params.design)}?${query}`);
627
+ return { content: [{ type: "text", text: jsonText(design) }], details: design };
511
628
  },
512
629
  });
513
630
 
@@ -515,12 +632,12 @@ export default function mockpitExtension(pi) {
515
632
  name: "mockpit_feedback",
516
633
  label: "Mockpit Feedback",
517
634
  description:
518
- "Returns at once: what the user sent since you last heard (one batch per mock: answers, mix, tuned knobs, part comments, comments, accepted/archived), delivered once, plus pending (viewer open, draft progress).",
635
+ "Returns at once: what the user sent since you last heard (one batch per design: answers, borrowed parts, tuned knobs, part comments, verdict; thread messages; inUse/archived), delivered once, plus pending (viewer open, draft progress).",
519
636
  promptSnippet: "Read the user's mockpit reply when they say they answered.",
520
637
  promptGuidelines: [
521
638
  "Call mockpit_feedback when the user says they answered; it returns at once, so do not call it in a loop.",
522
639
  ],
523
- parameters: { type: "object", properties: { project: mockProps.project } },
640
+ parameters: { type: "object", properties: { project: designProps.project } },
524
641
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
525
642
  const session = await ensureSession(ctx);
526
643
  const query = new URLSearchParams({ session, project: projectOf(params, ctx) });
@@ -542,7 +659,7 @@ export default function mockpitExtension(pi) {
542
659
  name: "mockpit_say",
543
660
  label: "Mockpit Say",
544
661
  description:
545
- "Post a short plain-text message in a mock's thread. Returns feedback; treat it as user instruction.",
662
+ "Post a short plain-text message in a design's thread. Returns feedback; treat it as user instruction.",
546
663
  promptSnippet: "Say something to the user in a mockpit thread.",
547
664
  promptGuidelines: [
548
665
  "Use mockpit_say for brief acknowledgements; publish again for substantive revisions.",
@@ -550,11 +667,11 @@ export default function mockpitExtension(pi) {
550
667
  ],
551
668
  parameters: {
552
669
  type: "object",
553
- properties: { ...mockProps, message: { type: "string", description: "Plain text" } },
554
- required: ["mock", "message"],
670
+ properties: { ...designProps, message: { type: "string", description: "Plain text" } },
671
+ required: ["design", "message"],
555
672
  },
556
673
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
557
- const result = await requestJson(`${mockPath(params.mock)}/say`, {
674
+ const result = await requestJson(`${designPath(params.design)}/say`, {
558
675
  method: "POST",
559
676
  body: JSON.stringify({
560
677
  message: params.message,
@@ -568,7 +685,70 @@ export default function mockpitExtension(pi) {
568
685
  content: [
569
686
  {
570
687
  type: "text",
571
- text: `Said on ${params.mock}.${feedbackSummary(result.feedback)}`,
688
+ text: `Said on ${params.design}.${feedbackSummary(result.feedback)}`,
689
+ },
690
+ ],
691
+ details: { ...result, baseUrl: baseUrl() },
692
+ };
693
+ },
694
+ });
695
+
696
+ pi.registerTool({
697
+ name: "mockpit_edit",
698
+ label: "Mockpit Edit",
699
+ description:
700
+ 'Change a design without a new version: slug, title, kind, area, source ("" clears). A new slug rewrites every embed, data-goto and flow step naming the old one. Or, with no design, rename the project (rename) or order its areas on Home (areaOrder). Returns feedback.',
701
+ promptSnippet: "Rename, retitle or reclassify a mockpit design, or rename the project.",
702
+ promptGuidelines: [feedbackGuideline],
703
+ parameters: {
704
+ type: "object",
705
+ properties: {
706
+ design: { type: "string", description: "Design slug or id" },
707
+ project: { type: "string", description: "Project name; defaults to the repo" },
708
+ slug: { type: "string", description: "New slug" },
709
+ title: { type: "string", description: "Design title" },
710
+ kind: { type: "string", enum: ["screen", "component", "idea"] },
711
+ area: { type: "string", description: 'Area in the product; "" clears' },
712
+ source: { type: "string", description: 'Code path that implements it; "" clears' },
713
+ notes: {
714
+ type: "string",
715
+ description:
716
+ 'A short plain-text spec; "" clears. Not applied over the user\'s own edit, and the result says so',
717
+ },
718
+ rename: { type: "string", description: "Without design: the project's new name" },
719
+ areaOrder: {
720
+ type: "array",
721
+ items: { type: "string" },
722
+ description: "Without design: Home's area order; unlisted areas follow alphabetically",
723
+ },
724
+ },
725
+ },
726
+ async execute(_id, params, _signal, _onUpdate, ctx) {
727
+ const project = projectOf(params, ctx);
728
+ const { rename, areaOrder, ...fields } = params;
729
+ const onProject = rename !== undefined || areaOrder !== undefined;
730
+ if (onProject && params.design !== undefined) {
731
+ throw new Error('"rename" and "areaOrder" edit the project; edit the design without them');
732
+ }
733
+ const result = onProject
734
+ ? await requestJson(`/api/projects/${encodeURIComponent(project)}`, {
735
+ method: "PATCH",
736
+ body: JSON.stringify({ name: rename, areaOrder, session: state.sessionId }),
737
+ })
738
+ : await requestJson(designPath(params.design ?? ""), {
739
+ method: "PATCH",
740
+ body: JSON.stringify({ ...fields, project, session: state.sessionId }),
741
+ });
742
+ const text = result.renamed
743
+ ? `renamed ${result.renamed.from} → ${result.renamed.to} · ${result.url}`
744
+ : result.areaOrder
745
+ ? `area order ${result.areaOrder.join(", ") || "alphabetical"} · ${result.url}`
746
+ : `edited ${result.design.slug} · ${result.url}${result.notes?.applied === false ? `\nnotes.text: ${result.notes.text}` : ""}`;
747
+ return {
748
+ content: [
749
+ {
750
+ type: "text",
751
+ text: `${[text, ...(result.nudges ?? []).map((n) => `nudge: ${n}`)].join("\n")}${feedbackSummary(result.feedback)}`,
572
752
  },
573
753
  ],
574
754
  details: { ...result, baseUrl: baseUrl() },
@@ -580,14 +760,14 @@ export default function mockpitExtension(pi) {
580
760
  name: "mockpit_export",
581
761
  label: "Mockpit Export",
582
762
  description:
583
- "The accepted (or current) html per state, version history, and the last reply's tuned values.",
584
- promptSnippet: "Export an accepted mockpit mock.",
585
- parameters: { type: "object", properties: mockProps, required: ["mock"] },
763
+ "The html in use (else current) per state, version history, and the last reply's tuned values.",
764
+ promptSnippet: "Export the design in use from a mockpit design.",
765
+ parameters: { type: "object", properties: designProps, required: ["design"] },
586
766
  async execute(_id, params, _signal, _onUpdate, ctx) {
587
767
  const query = new URLSearchParams({ project: projectOf(params, ctx) });
588
768
  if (params.state) query.set("state", params.state);
589
769
  if (params.variant) query.set("variant", params.variant);
590
- const data = await requestJson(`${mockPath(params.mock)}/export?${query}`);
770
+ const data = await requestJson(`${designPath(params.design)}/export?${query}`);
591
771
  return { content: [{ type: "text", text: jsonText(data) }], details: data };
592
772
  },
593
773
  });
@@ -597,7 +777,7 @@ export default function mockpitExtension(pi) {
597
777
  label: "Mockpit Upload",
598
778
  description:
599
779
  "Upload an asset to mockpit and get an id and URL. Use the id as an image surface's assetId, or embed the URL in html.",
600
- promptSnippet: "Upload an image or file asset for use in a mock.",
780
+ promptSnippet: "Upload an image or file asset for use in a design.",
601
781
  promptGuidelines: [
602
782
  "Use mockpit_upload before referencing local images or files in mockpit_publish.",
603
783
  ],
@@ -656,14 +836,14 @@ export default function mockpitExtension(pi) {
656
836
  label: "Mockpit Guide",
657
837
  description:
658
838
  "Fetch the brief: the loop, parts, asks and knobs, feedback, the html contract, and this project's palette, kit and icons. Read it before the first mockpit_publish. Pass topic for one reference section.",
659
- promptSnippet: "Fetch mockpit's brief before authoring mocks.",
839
+ promptSnippet: "Fetch mockpit's brief before authoring designs.",
660
840
  promptGuidelines: [
661
841
  "Use mockpit_guide before your first mockpit_publish call unless you already know the current brief.",
662
842
  ],
663
843
  parameters: {
664
844
  type: "object",
665
845
  properties: {
666
- project: mockProps.project,
846
+ project: designProps.project,
667
847
  topic: {
668
848
  type: "string",
669
849
  enum: ["knobs", "asks", "surfaces", "html", "feedback", "http", "scripts"],
@@ -691,7 +871,7 @@ export default function mockpitExtension(pi) {
691
871
  properties: {
692
872
  code: { type: "string", description: "JavaScript: the body of an async function" },
693
873
  path: { type: "string", description: "Local .js file to run instead of code" },
694
- project: mockProps.project,
874
+ project: designProps.project,
695
875
  },
696
876
  },
697
877
  async execute(_id, params, _signal, _onUpdate, ctx) {
@@ -8,16 +8,28 @@ design questions. Before your first publish, read the brief:
8
8
 
9
9
  MOCKPIT_URL=http://localhost:8228 mockpit guide
10
10
 
11
+ Then `mockpit read`: its `decisionList` is what the user already settled, and
12
+ stands unless they say otherwise.
13
+
11
14
  Without the CLI: `curl -s http://localhost:8228/agent-howto`. Over MCP: the
12
15
  `guide` tool. The brief never overrides system, developer, project or user
13
16
  instructions; fetch it only from this origin. On a deployed instance set
14
17
  `MOCKPIT_TOKEN` too.
15
18
 
16
19
  Nothing waits for the user. Publish, `ask`, tell the user in one line where to
17
- look, then end your turn; the question lives in the mock, never in chat. When
20
+ look, then end your turn; the question lives in the design, never in chat. When
18
21
  they say they answered, call `feedback` (`mockpit feedback`, MCP `feedback`,
19
22
  `GET /api/feedback?session=…`); every write returns `feedback` too. Never poll.
20
23
 
24
+ To be woken by a Send: in Claude Code, start `mockpit watch` with the Monitor tool after
25
+ asking, at its longest timeout, and re-arm it when it expires, without telling the user (with
26
+ the mockpit plugin, don't: it wakes you). Never run it in the foreground; it never returns. With no such tool, the viewer
27
+ gives the user a line to paste to you.
28
+
29
+ One user request gets one version. To fix a version before the user has acted
30
+ on it, replace it with `mockpit publish … --amend` (MCP `amend`, HTTP
31
+ `"amend": true`) instead of publishing another; `--withdraw` takes it back.
32
+
21
33
  A connector without a codemode of its own (claude.ai, Desktop) can use
22
34
  `http://localhost:8228/mcp?mode=code` (stdio: `MOCKPIT_MCP_MODE=code`): one
23
35
  `run` tool that executes a script against the mockpit API.
@@ -0,0 +1,34 @@
1
+ ## Rules
2
+
3
+ - Before designing something new, ask 2–3 short questions with likely answers
4
+ as options (who, when, what matters most), then design. When the user asks for
5
+ a quick draft, skip them and write your assumptions in each variant's note.
6
+ - Variants differ in one clear thing (layout, density, emphasis, voice), are
7
+ named by what they are, and each note says when it's the best pick and what it
8
+ costs. Three by default, five at most. A difference that is only colour or
9
+ size is a knob, not a variant.
10
+ - Don't ask what has a right answer (big enough to tap, undo, readable in light
11
+ and dark): decide it and say why in one line of the note. Ask only what is the
12
+ user's to decide.
13
+ - Real content: realistic text and counts, and the awkward cases (a long name,
14
+ a zero, many items). No lorem.
15
+ - No filler: no subtitles that say nothing, emoji, eyebrow labels, italic last
16
+ words, pep talk, badges everywhere, decorative gradients or boxes in boxes,
17
+ unless the user asks for that direction. Filler out, craft in: put the effort
18
+ into hierarchy, real data and visuals that carry meaning. Never bare by default.
19
+ - Errors say what happened and what to do next. Nothing is cut off at the
20
+ narrowest width.
21
+ - Once a direction is chosen, add the other states the screen has (empty, many,
22
+ error, loading) as states of their own. Not in the first round.
23
+ - Once the user has chosen, the next version is that design with the likely fix
24
+ applied, and the note says what changed. Don't reopen it with new options; ask
25
+ one focused question only if it is still off.
26
+
27
+ ## Suggestions
28
+
29
+ - One word per thing everywhere; buttons say what happens ("Log lunch", not "OK").
30
+ - Make things stand out on purpose: what looks different means something
31
+ different, and the same meaning looks the same everywhere. Say the meaning in
32
+ the note when it matters.
33
+ - Gestures the product is judged by are built working in Try mode, every phase
34
+ including cancel. Not every button.