mockpit 1.2.0 → 2.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 +69 -48
- package/bin/demoData.js +3 -3
- package/bin/mockpit.js +426 -51
- package/dist/mcp/server.js +45 -7
- package/dist/server/apiViews.d.ts +407 -21
- package/dist/server/apiViews.js +110 -11
- package/dist/server/app.d.ts +5 -1
- package/dist/server/app.js +1454 -192
- package/dist/server/codeWorker.js +2 -1
- package/dist/server/decisions.d.ts +23 -0
- package/dist/server/decisions.js +100 -0
- package/dist/server/designGuide.d.ts +6 -2
- package/dist/server/designGuide.js +96 -27
- package/dist/server/embeds.d.ts +26 -0
- package/dist/server/embeds.js +144 -0
- package/dist/server/eventLog.js +2 -1
- package/dist/server/feedbackBatch.d.ts +15 -4
- package/dist/server/feedbackBatch.js +31 -7
- package/dist/server/fills.d.ts +41 -0
- package/dist/server/fills.js +328 -0
- package/dist/server/guidance.d.ts +23 -0
- package/dist/server/guidance.js +71 -0
- package/dist/server/index.js +25 -2
- package/dist/server/knobs.d.ts +6 -2
- package/dist/server/knobs.js +11 -1
- package/dist/server/mcpHttp.d.ts +6 -0
- package/dist/server/mcpHttp.js +35 -0
- package/dist/server/mcpSpec.d.ts +14 -13
- package/dist/server/mcpSpec.js +133 -18
- package/dist/server/postSurfaces.js +6 -12
- package/dist/server/richRender.js +3 -1
- package/dist/server/run.js +10 -1
- package/dist/server/runApi.d.ts +3 -3
- package/dist/server/runApi.js +18 -13
- package/dist/server/sqlStore.d.ts +97 -5
- package/dist/server/sqlStore.js +367 -27
- package/dist/server/surfacePage.d.ts +6 -2
- package/dist/server/surfacePage.js +142 -10
- package/dist/server/themes.d.ts +2 -0
- package/dist/server/themes.js +23 -3
- package/dist/server/types.d.ts +107 -16
- package/dist/server/types.js +73 -10
- package/dist/server/viewerRequest.d.ts +2 -0
- package/dist/server/viewerRequest.js +14 -0
- package/extensions/mockpit.js +179 -16
- package/guide/packs/design-craft.md +35 -0
- package/guide/topics/asks.md +82 -31
- package/guide/topics/feedback.md +151 -43
- package/guide/topics/html.md +8 -10
- package/guide/topics/http.md +8 -4
- package/guide/topics/knobs.md +4 -3
- package/guide/topics/scripts.md +2 -2
- package/guide/topics/surfaces.md +45 -11
- package/package.json +3 -15
- package/skills/launch-video/SKILL.md +2 -2
- package/skills/mockpit/SKILL.md +23 -7
- package/viewer/dist/index.html +9 -8
- package/dist/server/slots.d.ts +0 -14
- package/dist/server/slots.js +0 -71
package/extensions/mockpit.js
CHANGED
|
@@ -28,10 +28,10 @@ const feedbackGuideline =
|
|
|
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
|
|
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
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
|
-
"
|
|
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,18 +209,47 @@ 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,
|
|
217
|
-
// knob values, part comments) plus any
|
|
218
|
-
// thing the user said, so nothing is summarized away.
|
|
239
|
+
// `feedback` is one batch per mock: 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
245
|
const prefix = batch.mock ? `${batch.mock}: ` : "";
|
|
223
246
|
if (batch.handoff) lines.push(`- ${prefix}${batch.handoff}`);
|
|
247
|
+
if (batch.closed) {
|
|
248
|
+
const asks = (batch.dismissed ?? []).map((d) => d.text || d.ask).join("; ");
|
|
249
|
+
lines.push(
|
|
250
|
+
`- ${prefix}closed by the user${asks ? `, unanswered: ${asks}; don't ask again` : ""}`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
224
253
|
const reply = batch.reply;
|
|
225
254
|
if (reply) {
|
|
226
255
|
for (const a of reply.asks ?? []) {
|
|
@@ -228,21 +257,35 @@ function feedbackLines(feedback) {
|
|
|
228
257
|
lines.push(`- ${prefix}${a.text || a.ask} → ${chosen || "(no pick)"}`);
|
|
229
258
|
if (a.note) lines.push(` note: ${a.note}`);
|
|
230
259
|
}
|
|
231
|
-
for (const [part, variant] of Object.entries(reply.
|
|
232
|
-
lines.push(`- ${prefix}
|
|
260
|
+
for (const [part, variant] of Object.entries(reply.borrowed ?? {})) {
|
|
261
|
+
lines.push(`- ${prefix}borrow ${part} from ${variant}`);
|
|
233
262
|
}
|
|
234
263
|
for (const [path, value] of Object.entries(reply.tuned ?? {})) {
|
|
235
264
|
lines.push(`- ${prefix}tuned ${path} = ${JSON.stringify(value)}`);
|
|
236
265
|
}
|
|
237
266
|
for (const c of reply.comments ?? []) {
|
|
267
|
+
if (c.borrow) {
|
|
268
|
+
lines.push(
|
|
269
|
+
`- ${prefix}borrow ${c.borrow.part ? `${c.borrow.part} ` : ""}from ${c.borrow.from} into ${c.variant}@${c.version}${c.text ? `: ${c.text}` : ""}`,
|
|
270
|
+
);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
238
273
|
lines.push(
|
|
239
274
|
`- ${prefix}${c.part ? `[${c.part}${c.state ? ` · ${c.state}` : ""}] ` : ""}${c.text}`,
|
|
240
275
|
);
|
|
241
276
|
}
|
|
242
|
-
if (reply.
|
|
277
|
+
if (reply.verdict) lines.push(`- ${prefix}${reply.verdict.kind} ${reply.verdict.variant}`);
|
|
243
278
|
if (reply.text) lines.push(`- ${prefix}${reply.text}`);
|
|
244
279
|
}
|
|
245
|
-
for (const c of batch.
|
|
280
|
+
for (const c of batch.messages ?? []) lines.push(`- ${prefix}${c.text}`);
|
|
281
|
+
if (batch.edited) {
|
|
282
|
+
const e = batch.edited;
|
|
283
|
+
const moved = [
|
|
284
|
+
e.kind ? `now a ${e.kind}` : "",
|
|
285
|
+
e.area !== undefined ? (e.area ? `area ${e.area}` : "no area") : "",
|
|
286
|
+
].filter(Boolean);
|
|
287
|
+
lines.push(`- ${prefix}the user moved it from Home (${moved.join(", ")})`);
|
|
288
|
+
}
|
|
246
289
|
}
|
|
247
290
|
return lines;
|
|
248
291
|
}
|
|
@@ -269,6 +312,42 @@ function feedbackSummary(feedback) {
|
|
|
269
312
|
return `\n\nUser feedback delivered with this result:\n${lines.join("\n")}`;
|
|
270
313
|
}
|
|
271
314
|
|
|
315
|
+
const pause = (ms, signal) =>
|
|
316
|
+
new Promise((done) => {
|
|
317
|
+
const timer = setTimeout(done, ms);
|
|
318
|
+
signal.addEventListener("abort", () => (clearTimeout(timer), done()), { once: true });
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// The user's reply wakes the idle agent, as the Claude Code plugin's monitor
|
|
322
|
+
// does. Reading feedback advances the server's cursor, so the message carries
|
|
323
|
+
// the whole reply: a nudge to call mockpit_feedback would come back empty.
|
|
324
|
+
// followUp waits for a busy agent to finish rather than cutting into its work.
|
|
325
|
+
async function watchReplies(pi, state, signal) {
|
|
326
|
+
while (!signal.aborted) {
|
|
327
|
+
if (!state.sessionId) {
|
|
328
|
+
await pause(2000, signal);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
const query = new URLSearchParams({ session: state.sessionId, author: "user", wait: "60" });
|
|
333
|
+
const result = await requestJson(`/api/comments?${query}`, { signal });
|
|
334
|
+
const lines = feedbackLines(result.feedback);
|
|
335
|
+
if (!lines.length || signal.aborted) continue;
|
|
336
|
+
pi.sendMessage(
|
|
337
|
+
{
|
|
338
|
+
customType: "mockpit-reply",
|
|
339
|
+
content: `The user replied in mockpit:\n${lines.join("\n")}`,
|
|
340
|
+
display: true,
|
|
341
|
+
details: { feedback: result.feedback },
|
|
342
|
+
},
|
|
343
|
+
{ triggerTurn: true, deliverAs: "followUp" },
|
|
344
|
+
);
|
|
345
|
+
} catch {
|
|
346
|
+
await pause(2000, signal);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
272
351
|
function reconstructSession(ctx) {
|
|
273
352
|
let sessionId = process.env.MOCKPIT_SESSION || undefined;
|
|
274
353
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
@@ -286,9 +365,17 @@ function reconstructSession(ctx) {
|
|
|
286
365
|
|
|
287
366
|
export default function mockpitExtension(pi) {
|
|
288
367
|
const state = { sessionId: process.env.MOCKPIT_SESSION || undefined };
|
|
368
|
+
let watching;
|
|
289
369
|
|
|
290
370
|
pi.on("session_start", (_event, ctx) => {
|
|
291
371
|
state.sessionId = reconstructSession(ctx);
|
|
372
|
+
watching?.abort();
|
|
373
|
+
watching = undefined;
|
|
374
|
+
// print and json runs end after one prompt; nobody is there to wake.
|
|
375
|
+
if (ctx.mode === "tui" || ctx.mode === "rpc") {
|
|
376
|
+
watching = new AbortController();
|
|
377
|
+
void watchReplies(pi, state, watching.signal);
|
|
378
|
+
}
|
|
292
379
|
ctx.ui.setStatus(
|
|
293
380
|
"mockpit",
|
|
294
381
|
state.sessionId
|
|
@@ -297,6 +384,12 @@ export default function mockpitExtension(pi) {
|
|
|
297
384
|
);
|
|
298
385
|
});
|
|
299
386
|
|
|
387
|
+
// After shutdown pi refuses calls from this extension, so the loop stops first.
|
|
388
|
+
pi.on("session_shutdown", () => {
|
|
389
|
+
watching?.abort();
|
|
390
|
+
watching = undefined;
|
|
391
|
+
});
|
|
392
|
+
|
|
300
393
|
pi.registerCommand("mockpit", {
|
|
301
394
|
description: "Show mockpit extension status or reset its remembered session: /mockpit [reset]",
|
|
302
395
|
handler: async (args, ctx) => {
|
|
@@ -358,13 +451,23 @@ export default function mockpitExtension(pi) {
|
|
|
358
451
|
...mockProps,
|
|
359
452
|
title: { type: "string", description: "Mock title" },
|
|
360
453
|
kind: { type: "string", enum: ["component", "page"], description: "Mock kind" },
|
|
361
|
-
|
|
454
|
+
area: {
|
|
455
|
+
type: "string",
|
|
456
|
+
description: 'Where in the product it belongs ("Shell"); Home groups by it. "" clears',
|
|
457
|
+
},
|
|
458
|
+
source: { type: "string", description: 'Code path that implements it. "" clears' },
|
|
459
|
+
html: { type: "string", description: "HTML body fragment; declare parts with data-part" },
|
|
362
460
|
path: { type: "string", description: "File to read the html from instead" },
|
|
461
|
+
poster: {
|
|
462
|
+
type: "string",
|
|
463
|
+
description:
|
|
464
|
+
"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",
|
|
465
|
+
},
|
|
363
466
|
surfaces: surfacesSchema,
|
|
364
467
|
parts: {
|
|
365
468
|
type: "object",
|
|
366
469
|
description:
|
|
367
|
-
'{"name"|"name#key": outer html}; splices
|
|
470
|
+
'{"name"|"name#key": outer html}; splices declared parts of the latest version',
|
|
368
471
|
},
|
|
369
472
|
knobs: { type: "object", description: "Knobs in tunekit usePane shape, keyed by path" },
|
|
370
473
|
variantKnobs: { type: "object", description: "Knobs this variant alone has" },
|
|
@@ -378,11 +481,13 @@ export default function mockpitExtension(pi) {
|
|
|
378
481
|
? await readFile(resolve(ctx.cwd, params.path), "utf8")
|
|
379
482
|
: params.html;
|
|
380
483
|
const { path: _path, ...rest } = params;
|
|
484
|
+
const poster = await posterId(params.poster, ctx);
|
|
381
485
|
const result = await requestJson("/api/mocks", {
|
|
382
486
|
method: "POST",
|
|
383
487
|
body: JSON.stringify({
|
|
384
488
|
...rest,
|
|
385
489
|
html,
|
|
490
|
+
...(poster === undefined ? {} : { poster }),
|
|
386
491
|
project: projectOf(params, ctx),
|
|
387
492
|
session: state.sessionId,
|
|
388
493
|
// Only used when this publish creates the session.
|
|
@@ -424,7 +529,7 @@ export default function mockpitExtension(pi) {
|
|
|
424
529
|
name: "mockpit_ask",
|
|
425
530
|
label: "Mockpit Ask",
|
|
426
531
|
description:
|
|
427
|
-
"Ask structured questions on a mock. A choice is several variants plus one
|
|
532
|
+
"Ask structured questions on a mock. 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
533
|
promptSnippet: "Ask the user to choose between mockpit variants.",
|
|
429
534
|
promptGuidelines: [
|
|
430
535
|
"Use mockpit_ask once the variants are published, tell the user where to look, then end your turn.",
|
|
@@ -515,7 +620,7 @@ export default function mockpitExtension(pi) {
|
|
|
515
620
|
name: "mockpit_feedback",
|
|
516
621
|
label: "Mockpit Feedback",
|
|
517
622
|
description:
|
|
518
|
-
"Returns at once: what the user sent since you last heard (one batch per mock: answers,
|
|
623
|
+
"Returns at once: what the user sent since you last heard (one batch per mock: answers, borrowed parts, tuned knobs, part comments, verdict; thread messages; inUse/archived; or closed with the questions it dismissed), delivered once, plus pending (viewer open, draft progress).",
|
|
519
624
|
promptSnippet: "Read the user's mockpit reply when they say they answered.",
|
|
520
625
|
promptGuidelines: [
|
|
521
626
|
"Call mockpit_feedback when the user says they answered; it returns at once, so do not call it in a loop.",
|
|
@@ -576,12 +681,70 @@ export default function mockpitExtension(pi) {
|
|
|
576
681
|
},
|
|
577
682
|
});
|
|
578
683
|
|
|
684
|
+
pi.registerTool({
|
|
685
|
+
name: "mockpit_edit",
|
|
686
|
+
label: "Mockpit Edit",
|
|
687
|
+
description:
|
|
688
|
+
'Change a mock 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 mock, rename the project (rename) or order its areas on Home (areaOrder). Returns feedback.',
|
|
689
|
+
promptSnippet: "Rename, retitle or reclassify a mockpit mock, or rename the project.",
|
|
690
|
+
promptGuidelines: [feedbackGuideline],
|
|
691
|
+
parameters: {
|
|
692
|
+
type: "object",
|
|
693
|
+
properties: {
|
|
694
|
+
mock: { type: "string", description: "Mock slug or id" },
|
|
695
|
+
project: { type: "string", description: "Project name; defaults to the repo" },
|
|
696
|
+
slug: { type: "string", description: "New slug" },
|
|
697
|
+
title: { type: "string", description: "Mock title" },
|
|
698
|
+
kind: { type: "string", enum: ["page", "component", "sketch"] },
|
|
699
|
+
area: { type: "string", description: 'Area in the product; "" clears' },
|
|
700
|
+
source: { type: "string", description: 'Code path that implements it; "" clears' },
|
|
701
|
+
rename: { type: "string", description: "Without mock: the project's new name" },
|
|
702
|
+
areaOrder: {
|
|
703
|
+
type: "array",
|
|
704
|
+
items: { type: "string" },
|
|
705
|
+
description: "Without mock: Home's area order; unlisted areas follow alphabetically",
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
710
|
+
const project = projectOf(params, ctx);
|
|
711
|
+
const { rename, areaOrder, ...fields } = params;
|
|
712
|
+
const onProject = rename !== undefined || areaOrder !== undefined;
|
|
713
|
+
if (onProject && params.mock !== undefined) {
|
|
714
|
+
throw new Error('"rename" and "areaOrder" edit the project; edit the mock without them');
|
|
715
|
+
}
|
|
716
|
+
const result = onProject
|
|
717
|
+
? await requestJson(`/api/projects/${encodeURIComponent(project)}`, {
|
|
718
|
+
method: "PATCH",
|
|
719
|
+
body: JSON.stringify({ name: rename, areaOrder, session: state.sessionId }),
|
|
720
|
+
})
|
|
721
|
+
: await requestJson(mockPath(params.mock ?? ""), {
|
|
722
|
+
method: "PATCH",
|
|
723
|
+
body: JSON.stringify({ ...fields, project, session: state.sessionId }),
|
|
724
|
+
});
|
|
725
|
+
const text = result.renamed
|
|
726
|
+
? `renamed ${result.renamed.from} → ${result.renamed.to} · ${result.url}`
|
|
727
|
+
: result.areaOrder
|
|
728
|
+
? `area order ${result.areaOrder.join(", ") || "alphabetical"} · ${result.url}`
|
|
729
|
+
: `edited ${result.mock.slug} · ${result.url}`;
|
|
730
|
+
return {
|
|
731
|
+
content: [
|
|
732
|
+
{
|
|
733
|
+
type: "text",
|
|
734
|
+
text: `${[text, ...(result.nudges ?? []).map((n) => `nudge: ${n}`)].join("\n")}${feedbackSummary(result.feedback)}`,
|
|
735
|
+
},
|
|
736
|
+
],
|
|
737
|
+
details: { ...result, baseUrl: baseUrl() },
|
|
738
|
+
};
|
|
739
|
+
},
|
|
740
|
+
});
|
|
741
|
+
|
|
579
742
|
pi.registerTool({
|
|
580
743
|
name: "mockpit_export",
|
|
581
744
|
label: "Mockpit Export",
|
|
582
745
|
description:
|
|
583
|
-
"The
|
|
584
|
-
promptSnippet: "Export
|
|
746
|
+
"The html in use (else current) per state, version history, and the last reply's tuned values.",
|
|
747
|
+
promptSnippet: "Export the design in use from a mockpit mock.",
|
|
585
748
|
parameters: { type: "object", properties: mockProps, required: ["mock"] },
|
|
586
749
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
587
750
|
const query = new URLSearchParams({ project: projectOf(params, ctx) });
|
|
@@ -0,0 +1,35 @@
|
|
|
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 situations the state has (empty,
|
|
22
|
+
many, error, loading) as situations of the state (`--situation`). Not in the
|
|
23
|
+
first round.
|
|
24
|
+
- Once the user has chosen, the next version is that design with the likely fix
|
|
25
|
+
applied, and the note says what changed. Don't reopen it with new options; ask
|
|
26
|
+
one focused question only if it is still off.
|
|
27
|
+
|
|
28
|
+
## Suggestions
|
|
29
|
+
|
|
30
|
+
- One word per thing everywhere; buttons say what happens ("Log lunch", not "OK").
|
|
31
|
+
- Make things stand out on purpose: what looks different means something
|
|
32
|
+
different, and the same meaning looks the same everywhere. Say the meaning in
|
|
33
|
+
the note when it matters.
|
|
34
|
+
- Gestures the product is judged by are built working in Try mode, every phase
|
|
35
|
+
including cancel. Not every button.
|
package/guide/topics/asks.md
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
# mockpit topic: asks
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
plus one
|
|
5
|
-
and ask in the mock, never in chat: after asking, tell the
|
|
6
|
-
where to look and end your turn.
|
|
3
|
+
A question (`ask`) is your structured question on a mock. A choice is several
|
|
4
|
+
variants plus one question that binds them. Ask only when the decision is
|
|
5
|
+
really the user's, and ask in the mock, never in chat: after asking, tell the
|
|
6
|
+
user in one line where to look and end your turn. To the user they are
|
|
7
|
+
questions; `ask` is only the tool's name.
|
|
7
8
|
|
|
8
9
|
```sh
|
|
9
|
-
mockpit ask --mock writer "
|
|
10
|
+
mockpit ask --mock writer "Quiet or dark?" --option Quiet=quiet --option Dark=dark --id tone
|
|
10
11
|
mockpit ask --mock writer "Trim?" --option Above=knob:trim.position=top --option Below=knob:trim.position=bottom
|
|
11
12
|
mockpit ask --mock writer --asks asks.json
|
|
12
13
|
```
|
|
13
14
|
|
|
14
15
|
`=knob:` sets one knob; for an option that sets several, use `--asks` JSON.
|
|
16
|
+
`--recommend Quiet` (`"recommended": true` on one option) marks the one you
|
|
17
|
+
would pick with a quiet dot; the user sees why on hover, in the variant's note.
|
|
15
18
|
|
|
16
19
|
## Shape
|
|
17
20
|
|
|
@@ -20,8 +23,8 @@ The same JSON goes to `--asks`, MCP `ask` and `POST /api/mocks/:id/asks`:
|
|
|
20
23
|
```json
|
|
21
24
|
[
|
|
22
25
|
{
|
|
23
|
-
"id": "
|
|
24
|
-
"text": "
|
|
26
|
+
"id": "tone",
|
|
27
|
+
"text": "Quiet or dark?",
|
|
25
28
|
"scope": "mock",
|
|
26
29
|
"options": [
|
|
27
30
|
{ "label": "Quiet", "variant": "quiet" },
|
|
@@ -51,49 +54,97 @@ The same JSON goes to `--asks`, MCP `ask` and `POST /api/mocks/:id/asks`:
|
|
|
51
54
|
|
|
52
55
|
## Scope
|
|
53
56
|
|
|
54
|
-
`scope` says what
|
|
55
|
-
or `part` (add `part`, the `data-part` name).
|
|
57
|
+
`scope` says what a question decides: `mock` (the default), `state` (add
|
|
58
|
+
`state`) or `part` (add `part`, the `data-part` name). Scope also says where
|
|
59
|
+
the user sees it: the viewer shows each state with its own questions. A
|
|
60
|
+
`state` question shows on that state, a `part` question on every state that
|
|
61
|
+
declares the part, and a `mock` question whose options name variants on every
|
|
62
|
+
state that has them (any other `mock` question on every state); answered
|
|
63
|
+
once, it counts once.
|
|
64
|
+
|
|
65
|
+
Only open questions are shown and counted ("1 of 3"). Once a Send answers
|
|
66
|
+
one, it folds into a read-only Answered list and stays out of the next round,
|
|
67
|
+
so ask new questions for what is still open rather than re-sending answered
|
|
68
|
+
ones; the user changes a sent answer only on purpose, and the change arrives
|
|
69
|
+
as a normal Send.
|
|
56
70
|
|
|
57
71
|
## Options
|
|
58
72
|
|
|
59
73
|
An option binds to one of:
|
|
60
74
|
|
|
61
|
-
- a `variant`: picking it
|
|
62
|
-
that state.
|
|
75
|
+
- a `variant`: picking it puts that variant in use and archives its siblings
|
|
76
|
+
in that state. Add `version` to offer one version of it (`"version": 2`, CLI
|
|
77
|
+
`--option Before=quiet@2`): two options can be two versions of one variant,
|
|
78
|
+
and the picked one is put in use. It needs the question's `state` when the
|
|
79
|
+
mock has several.
|
|
63
80
|
- a knob `set`: picking it applies those values live.
|
|
64
81
|
- nothing: a plain pill.
|
|
65
82
|
|
|
66
83
|
Options that change how something looks carry a `variant` or a knob `set`,
|
|
67
84
|
so the user sees them live. Plain pills only for what can't be seen.
|
|
68
85
|
|
|
86
|
+
When one question only makes sense once another is answered (an accent's
|
|
87
|
+
placement depends on the palette), give it `after: "<that question's id>"`
|
|
88
|
+
(CLI `--after palette`). It stays closed until that one is answered, then its
|
|
89
|
+
pictures render with that answer and say so. Ask the deciding question first.
|
|
90
|
+
|
|
69
91
|
The viewer shows a picture of each option it can render. `multi: true` allows
|
|
70
|
-
several answers. Option ids default to the slugged label. Reusing
|
|
71
|
-
replaces that
|
|
72
|
-
|
|
73
|
-
is reserved.
|
|
92
|
+
several answers. Option ids default to the slugged label. Reusing a question
|
|
93
|
+
`id` replaces that question. The viewer adds an "Other…" write-in and a note to
|
|
94
|
+
every question (both take attached images), so don't add your own; the option
|
|
95
|
+
id `other` is reserved.
|
|
96
|
+
|
|
97
|
+
## Parts with fills
|
|
98
|
+
|
|
99
|
+
A part with fills is one region with alternatives, in one html. Give each
|
|
100
|
+
alternative the same `data-part` and its own `data-fill`:
|
|
101
|
+
|
|
102
|
+
```html
|
|
103
|
+
<section data-part="entry" data-part-label="Entry" data-fill="dots" data-fill-label="Typing dots">
|
|
104
|
+
…
|
|
105
|
+
</section>
|
|
106
|
+
<section data-part="entry" data-fill="skeleton">…</section>
|
|
107
|
+
<section data-part="entry" data-fill="filling" data-fill-default>…</section>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The first fill is the default unless one carries `data-fill-default`. The
|
|
111
|
+
server derives a select knob at the part's name and shows only the chosen
|
|
112
|
+
fill, so you write no switching CSS; give each fill whatever display it
|
|
113
|
+
needs. Draw anything inside a fill, including further parts with fills: one
|
|
114
|
+
inside a fill is asked only while that fill is chosen, right after its
|
|
115
|
+
parent's question.
|
|
116
|
+
|
|
117
|
+
Publish returns the parts with fills it found (`fills`) and, for each one no
|
|
118
|
+
question sets, a ready `suggestedFillAsks` entry: `scope: "part"`, one option
|
|
119
|
+
per fill with `set: {"entry": "<fill>"}`. Send them with `ask`, relabelled if
|
|
120
|
+
you like. On a pick the viewer outlines the fill on stage. `fills` in publish
|
|
121
|
+
replaces or adds one fill without resending the page (topic `feedback`).
|
|
122
|
+
|
|
123
|
+
Use fills when the alternatives differ in one region; use variants when the
|
|
124
|
+
whole design differs. Both combine: each variant may have parts with fills.
|
|
74
125
|
|
|
75
|
-
## The
|
|
126
|
+
## The variant question
|
|
76
127
|
|
|
77
|
-
When you publish variants, ask `scope: "mock"` with one option per variant,
|
|
78
|
-
ask it first. Questions appear in the order you ask them.
|
|
79
|
-
|
|
80
|
-
|
|
128
|
+
When you publish variants, ask `scope: "mock"` with one option per variant,
|
|
129
|
+
and ask it first. Questions appear in the order you ask them. Tune then
|
|
130
|
+
offers, per part, to borrow that part from another variant. Variants without
|
|
131
|
+
a variant question get a plain switcher in the frame header.
|
|
81
132
|
|
|
82
133
|
A variant decides everything it differs on; don't re-ask it. Ask other axes as
|
|
83
134
|
knob `set`s.
|
|
84
135
|
|
|
85
136
|
## When you forget to ask
|
|
86
137
|
|
|
87
|
-
A publish that leaves a state with two or more open variants and no
|
|
138
|
+
A publish that leaves a state with two or more open variants and no question
|
|
88
139
|
binding them returns a nudge and a ready `suggestedAsk` (mock-wide when the
|
|
89
140
|
variant names line up across states, else for that state), or your existing
|
|
90
|
-
|
|
91
|
-
with options bound to the variants. The CLI prints the `mockpit ask`
|
|
92
|
-
for you. Sending it stops the nudge and keeps
|
|
93
|
-
mock; the built-in one is gone once answered.
|
|
94
|
-
|
|
95
|
-
Until
|
|
96
|
-
of each variant, so the user can still choose in one Send. Its
|
|
97
|
-
reads like any other
|
|
98
|
-
`variant:<state>`), and
|
|
99
|
-
those variants replaces it.
|
|
141
|
+
question plus the new variants. Send it as is with `ask`, or write your own
|
|
142
|
+
question with options bound to the variants. The CLI prints the `mockpit ask`
|
|
143
|
+
command for you. Sending it stops the nudge and keeps your answered question
|
|
144
|
+
on the mock; the built-in one is gone once answered.
|
|
145
|
+
|
|
146
|
+
Until a question binds them, the viewer shows a built-in "Which variant?"
|
|
147
|
+
with a picture of each variant, so the user can still choose in one Send. Its
|
|
148
|
+
answer reads like any other under the reserved id `variant` (per state:
|
|
149
|
+
`variant:<state>`), and puts in use and archives per state. Your own question
|
|
150
|
+
over those variants replaces it.
|