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.
- package/README.md +66 -202
- package/bin/demoData.js +5 -5
- package/bin/initDesign.js +6 -6
- package/bin/mockpit.js +687 -192
- package/bin/poster.js +377 -0
- package/dist/mcp/server.js +110 -19
- package/dist/server/answerAdditions.d.ts +7 -0
- package/dist/server/answerAdditions.js +76 -0
- package/dist/server/apiViews.d.ts +563 -41
- package/dist/server/apiViews.js +223 -74
- package/dist/server/app.d.ts +10 -2
- package/dist/server/app.js +3284 -782
- package/dist/server/appIcon.js +15 -5
- package/dist/server/appIconPng.js +3 -3
- package/dist/server/askNudges.js +1 -1
- package/dist/server/cdnInline.d.ts +8 -0
- package/dist/server/cdnInline.js +638 -0
- package/dist/server/codeWorker.js +2 -1
- package/dist/server/decisionList.d.ts +45 -0
- package/dist/server/decisionList.js +302 -0
- package/dist/server/decisions.d.ts +8 -8
- package/dist/server/decisions.js +18 -17
- package/dist/server/designGuide.d.ts +10 -6
- package/dist/server/designGuide.js +173 -78
- package/dist/server/embeds.d.ts +4 -1
- package/dist/server/embeds.js +57 -6
- package/dist/server/eventLog.d.ts +2 -2
- package/dist/server/eventLog.js +11 -11
- package/dist/server/events.d.ts +9 -6
- package/dist/server/feedbackBatch.d.ts +38 -16
- package/dist/server/feedbackBatch.js +121 -50
- package/dist/server/fills.d.ts +4 -0
- package/dist/server/fills.js +55 -1
- package/dist/server/guidance.d.ts +23 -0
- package/dist/server/guidance.js +71 -0
- package/dist/server/handoff.d.ts +4 -1
- package/dist/server/handoff.js +8 -2
- package/dist/server/icons.js +1 -1
- package/dist/server/index.js +49 -11
- package/dist/server/jump.d.ts +21 -0
- package/dist/server/jump.js +175 -0
- package/dist/server/knobs.d.ts +3 -3
- package/dist/server/knobs.js +6 -6
- package/dist/server/mcpHttp.d.ts +8 -1
- package/dist/server/mcpHttp.js +44 -7
- package/dist/server/mcpSpec.d.ts +2 -1
- package/dist/server/mcpSpec.js +172 -50
- package/dist/server/parts.js +9 -1
- package/dist/server/postSurfaces.js +3 -0
- package/dist/server/run.d.ts +1 -1
- package/dist/server/run.js +31 -17
- package/dist/server/runApi.d.ts +3 -3
- package/dist/server/runApi.js +34 -24
- package/dist/server/sqlStore.d.ts +117 -94
- package/dist/server/sqlStore.js +1089 -141
- package/dist/server/standings.d.ts +25 -0
- package/dist/server/standings.js +113 -0
- package/dist/server/surfacePage.d.ts +6 -9
- package/dist/server/surfacePage.js +361 -86
- package/dist/server/types.d.ts +252 -69
- package/dist/server/types.js +87 -69
- package/extensions/mockpit.js +245 -65
- package/guide/AGENT_SETUP.md +13 -1
- package/guide/packs/design-craft.md +34 -0
- package/guide/topics/asks.md +117 -69
- package/guide/topics/feedback.md +156 -119
- package/guide/topics/html.md +19 -32
- package/guide/topics/http.md +43 -40
- package/guide/topics/knobs.md +7 -10
- package/guide/topics/scripts.md +24 -35
- package/guide/topics/surfaces.md +50 -41
- package/opencode/mockpit.js +120 -0
- package/opencode/package.json +8 -0
- package/package.json +4 -6
- package/skills/launch-video/SKILL.md +12 -12
- package/skills/mockpit/SKILL.md +35 -23
- package/viewer/dist/index.html +9 -8
package/extensions/mockpit.js
CHANGED
|
@@ -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
|
|
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;
|
|
32
|
-
"A choice is several variants plus one
|
|
33
|
-
"Tell the user in one line where to look, then end your turn. The question lives in the
|
|
34
|
-
"
|
|
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
|
|
217
|
-
// knob values, part comments) plus any
|
|
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.
|
|
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.
|
|
252
|
+
if (a.comment) lines.push(` comment: ${a.comment}`);
|
|
230
253
|
}
|
|
231
|
-
for (const [part, variant] of Object.entries(reply.
|
|
232
|
-
lines.push(`- ${prefix}
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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 ›
|
|
338
|
-
const
|
|
339
|
-
|
|
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
|
|
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
|
|
353
|
-
promptSnippet: "Publish a UI
|
|
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
|
-
...
|
|
359
|
-
title: { type: "string", description: "
|
|
360
|
-
kind: { type: "string", enum: ["component", "
|
|
361
|
-
|
|
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
|
|
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: ["
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
436
|
-
project:
|
|
552
|
+
design: designProps.design,
|
|
553
|
+
project: designProps.project,
|
|
437
554
|
asks: {
|
|
438
555
|
type: "array",
|
|
439
556
|
description:
|
|
440
|
-
"Questions: {id?, text, scope:
|
|
557
|
+
"Questions: {id?, text, scope: design|state|part, state?, part?, multi?, options}",
|
|
441
558
|
items: { type: "object" },
|
|
442
559
|
},
|
|
443
560
|
},
|
|
444
|
-
required: ["
|
|
561
|
+
required: ["design", "asks"],
|
|
445
562
|
},
|
|
446
563
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
447
|
-
const result = await requestJson(`${
|
|
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.
|
|
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
|
|
475
|
-
promptSnippet: "Read mockpit
|
|
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
|
|
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
|
-
|
|
483
|
-
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.
|
|
491
|
-
const result = await requestJson(`/api/
|
|
492
|
-
const lines = result.
|
|
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
|
|
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
|
|
510
|
-
return { content: [{ type: "text", text: jsonText(
|
|
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
|
|
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:
|
|
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
|
|
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: { ...
|
|
554
|
-
required: ["
|
|
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(`${
|
|
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.
|
|
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
|
|
584
|
-
promptSnippet: "Export
|
|
585
|
-
parameters: { type: "object", properties:
|
|
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(`${
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
874
|
+
project: designProps.project,
|
|
695
875
|
},
|
|
696
876
|
},
|
|
697
877
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
package/guide/AGENT_SETUP.md
CHANGED
|
@@ -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
|
|
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.
|