mockpit 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +13 -6
  2. package/bin/mockpit.js +139 -18
  3. package/dist/mcp/server.js +30 -10
  4. package/dist/server/apiViews.d.ts +129 -25
  5. package/dist/server/apiViews.js +86 -23
  6. package/dist/server/app.d.ts +2 -1
  7. package/dist/server/app.js +1258 -200
  8. package/dist/server/appIcon.d.ts +22 -0
  9. package/dist/server/appIcon.js +43 -0
  10. package/dist/server/appIconPng.d.ts +1 -0
  11. package/dist/server/appIconPng.js +6 -0
  12. package/dist/server/askNudges.d.ts +3 -0
  13. package/dist/server/askNudges.js +40 -0
  14. package/dist/server/base64.d.ts +1 -0
  15. package/dist/server/base64.js +8 -0
  16. package/dist/server/decisions.d.ts +23 -0
  17. package/dist/server/decisions.js +99 -0
  18. package/dist/server/designGuide.js +53 -16
  19. package/dist/server/embeds.d.ts +25 -0
  20. package/dist/server/embeds.js +118 -0
  21. package/dist/server/eventLog.js +2 -1
  22. package/dist/server/feedbackBatch.d.ts +28 -5
  23. package/dist/server/feedbackBatch.js +78 -11
  24. package/dist/server/fills.d.ts +41 -0
  25. package/dist/server/fills.js +328 -0
  26. package/dist/server/handoff.d.ts +6 -0
  27. package/dist/server/handoff.js +38 -0
  28. package/dist/server/index.js +15 -1
  29. package/dist/server/knobs.d.ts +6 -2
  30. package/dist/server/knobs.js +13 -4
  31. package/dist/server/mcpHttp.d.ts +5 -2
  32. package/dist/server/mcpHttp.js +14 -5
  33. package/dist/server/mcpSpec.d.ts +21 -13
  34. package/dist/server/mcpSpec.js +99 -9
  35. package/dist/server/postSurfaces.js +3 -12
  36. package/dist/server/richRender.js +3 -1
  37. package/dist/server/run.js +6 -2
  38. package/dist/server/runApi.d.ts +1 -1
  39. package/dist/server/runApi.js +5 -4
  40. package/dist/server/sqlStore.d.ts +92 -5
  41. package/dist/server/sqlStore.js +188 -30
  42. package/dist/server/surfacePage.d.ts +6 -2
  43. package/dist/server/surfacePage.js +142 -10
  44. package/dist/server/themes.d.ts +2 -0
  45. package/dist/server/themes.js +23 -3
  46. package/dist/server/types.d.ts +95 -10
  47. package/dist/server/types.js +89 -6
  48. package/dist/server/viewerRequest.d.ts +2 -0
  49. package/dist/server/viewerRequest.js +14 -0
  50. package/extensions/mockpit.js +1 -0
  51. package/guide/topics/asks.md +69 -10
  52. package/guide/topics/feedback.md +104 -16
  53. package/guide/topics/html.md +5 -1
  54. package/guide/topics/http.md +2 -1
  55. package/guide/topics/knobs.md +4 -3
  56. package/guide/topics/surfaces.md +32 -16
  57. package/package.json +4 -13
  58. package/skills/launch-video/SKILL.md +1 -1
  59. package/skills/mockpit/SKILL.md +14 -4
  60. package/viewer/dist/index.html +9 -9
  61. package/dist/server/slots.d.ts +0 -14
  62. package/dist/server/slots.js +0 -71
@@ -8,6 +8,8 @@ export interface Session {
8
8
  lastActiveAt: string;
9
9
  agentSeq: number;
10
10
  project: string | null;
11
+ agentActiveAt: string | null;
12
+ keyed: boolean;
11
13
  }
12
14
  export declare const SURFACE_KINDS: readonly ["html", "diff", "image", "markdown", "terminal", "mermaid", "json", "code"];
13
15
  export type SurfaceKind = (typeof SURFACE_KINDS)[number];
@@ -195,14 +197,22 @@ export interface PadValue {
195
197
  }
196
198
  export type KnobValue = number | boolean | string | PadValue | SpringKnob | EasingKnob;
197
199
  export type MockKind = "page" | "component";
198
- export type PostStatus = "open" | "accepted" | "archived";
200
+ export type PostStatus = "open" | "accepted" | "archived" | "dropped";
201
+ export declare const isLive: (status: PostStatus) => boolean;
202
+ export declare const inUseVersion: (post: Pick<Post, "status" | "version" | "acceptedVersion">) => number;
203
+ export declare function postAtVersion(post: Post, version: number): Post | null;
204
+ export declare function versionAt(post: Pick<Post, "version" | "history">, n: number): number;
199
205
  export interface AskOption {
200
206
  id: string;
201
207
  label: string;
202
208
  variant?: string;
209
+ version?: number;
203
210
  set?: Record<string, KnobValue>;
204
211
  }
205
212
  export type AskAnswer = string | string[];
213
+ export declare const optionState: (ask: Pick<Ask, "state">, mock: {
214
+ states: string[];
215
+ }) => string | null;
206
216
  export interface Ask {
207
217
  id: string;
208
218
  text: string;
@@ -211,9 +221,13 @@ export interface Ask {
211
221
  part?: string;
212
222
  options: AskOption[];
213
223
  multi?: boolean;
224
+ after?: string;
214
225
  answer?: AskAnswer;
215
226
  other?: string;
216
227
  note?: string;
228
+ otherImages?: string[];
229
+ noteImages?: string[];
230
+ dismissed?: true;
217
231
  at: string;
218
232
  }
219
233
  export declare const OTHER_ID = "other";
@@ -222,17 +236,23 @@ export interface PartCommentAnchor {
222
236
  quote?: string;
223
237
  selector?: string;
224
238
  box?: number[];
239
+ width?: number;
225
240
  }
241
+ export declare const REACTIONS: readonly ["up", "down", "more"];
242
+ export type Reaction = (typeof REACTIONS)[number];
226
243
  export interface PartComment {
227
244
  part: string | null;
228
245
  state: string | null;
229
246
  text: string;
230
247
  anchor?: PartCommentAnchor;
248
+ reaction?: Reaction;
249
+ variant?: string;
231
250
  }
232
251
  export interface ReplyDecision {
233
252
  kind: "accept" | "revise" | "drop";
234
253
  state: string | null;
235
254
  variant: string;
255
+ version?: number;
236
256
  }
237
257
  export interface Draft {
238
258
  version: number;
@@ -242,6 +262,8 @@ export interface Draft {
242
262
  comments: PartComment[];
243
263
  others?: Record<string, string>;
244
264
  notes?: Record<string, string>;
265
+ otherImages?: Record<string, string[]>;
266
+ noteImages?: Record<string, string[]>;
245
267
  updatedAt: string;
246
268
  }
247
269
  export interface Reply {
@@ -253,8 +275,13 @@ export interface Reply {
253
275
  comments: PartComment[];
254
276
  others?: Record<string, string>;
255
277
  notes?: Record<string, string>;
278
+ otherImages?: Record<string, string[]>;
279
+ noteImages?: Record<string, string[]>;
256
280
  text?: string;
257
281
  decision?: ReplyDecision;
282
+ resolved?: {
283
+ dismissed: string[];
284
+ };
258
285
  }
259
286
  export interface Mock {
260
287
  id: string;
@@ -265,15 +292,18 @@ export interface Mock {
265
292
  states: string[];
266
293
  asks: Ask[];
267
294
  knobs: Knobs;
295
+ widths: number[];
268
296
  draft: Draft | null;
269
297
  sessionId: string | null;
298
+ resolvedAt: string | null;
270
299
  createdAt: string;
271
300
  updatedAt: string;
272
301
  }
273
- export interface Slot {
302
+ export interface Embed {
274
303
  slug: string;
275
304
  variant: string;
276
305
  version: number;
306
+ pinned?: true;
277
307
  }
278
308
  export interface PostVersion {
279
309
  version: number;
@@ -284,6 +314,10 @@ export interface PostVersion {
284
314
  prompt?: string;
285
315
  author?: string;
286
316
  }
317
+ export interface ForkRef {
318
+ variant: string;
319
+ version: number;
320
+ }
287
321
  export interface Post {
288
322
  id: string;
289
323
  sessionId: string;
@@ -298,11 +332,28 @@ export interface Post {
298
332
  version: number;
299
333
  history: PostVersion[];
300
334
  knobs?: Knobs;
301
- slots: Slot[];
335
+ note?: string;
336
+ embeds: Embed[];
337
+ forkedFrom?: ForkRef;
338
+ starred?: true;
339
+ acceptedVersion?: number;
340
+ hiddenVersions?: number[];
302
341
  from?: number;
303
342
  prompt?: string;
304
343
  author?: string;
305
344
  }
345
+ export interface FlowStep {
346
+ mock: string;
347
+ state: string | null;
348
+ }
349
+ export interface Flow {
350
+ project: string;
351
+ name: string;
352
+ steps: FlowStep[];
353
+ createdAt: string;
354
+ updatedAt: string;
355
+ }
356
+ export declare const sameScreen: (a: FlowStep, b: FlowStep) => boolean;
306
357
  export interface ProjectSummary {
307
358
  name: string;
308
359
  mocks: number;
@@ -457,6 +508,7 @@ export interface CreateMockInput {
457
508
  kind?: MockKind;
458
509
  states?: string[];
459
510
  knobs?: Knobs;
511
+ widths?: number[];
460
512
  sessionId?: string | null;
461
513
  }
462
514
  export interface UpdateMockInput {
@@ -465,7 +517,9 @@ export interface UpdateMockInput {
465
517
  states?: string[];
466
518
  asks?: Ask[];
467
519
  knobs?: Knobs;
520
+ widths?: number[];
468
521
  sessionId?: string | null;
522
+ resolvedAt?: string | null;
469
523
  }
470
524
  export interface CreatePostInput {
471
525
  sessionId: string;
@@ -475,7 +529,9 @@ export interface CreatePostInput {
475
529
  title?: string;
476
530
  surfaces: Surface[];
477
531
  knobs?: Knobs;
478
- slots?: Slot[];
532
+ note?: string;
533
+ embeds?: Embed[];
534
+ forkedFrom?: ForkRef;
479
535
  from?: number;
480
536
  prompt?: string;
481
537
  author?: string;
@@ -484,7 +540,8 @@ export interface UpdatePostInput {
484
540
  title?: string;
485
541
  surfaces?: Surface[];
486
542
  knobs?: Knobs | null;
487
- slots?: Slot[];
543
+ note?: string;
544
+ embeds?: Embed[];
488
545
  from?: number;
489
546
  prompt?: string;
490
547
  author?: string;
@@ -515,21 +572,29 @@ export interface CommitReplyInput {
515
572
  text: string;
516
573
  payload: Reply;
517
574
  asks: Ask[];
518
- accept: string[];
575
+ accept: {
576
+ id: string;
577
+ version: number;
578
+ }[];
519
579
  archive: string[];
580
+ drop?: string[];
581
+ resolvedAt?: string;
582
+ keepDraft?: true;
520
583
  }
521
584
  export interface PostQuery {
522
585
  mockId?: string;
523
586
  sessionId?: string;
524
587
  }
525
588
  export interface Store {
526
- listSessions(): Promise<Session[]>;
589
+ listSessions(project?: string): Promise<Session[]>;
527
590
  getSession(id: string): Promise<Session | null>;
528
591
  getSessionByKey(key: string): Promise<Session | null>;
529
592
  createSession(input: CreateSessionInput): Promise<Session>;
530
593
  renameSession(id: string, title: string): Promise<Session | null>;
531
594
  removeSession(id: string): Promise<boolean>;
532
595
  markAgentSeen(sessionId: string, seq: number): Promise<void>;
596
+ touchAgent(sessionId: string): Promise<void>;
597
+ handOff(sessionId: string, seen: Pick<Session, "agentSeq" | "agentActiveAt">, toSeq: number): Promise<boolean>;
533
598
  getSetting(key: string): Promise<string | null>;
534
599
  setSetting(key: string, value: string): Promise<void>;
535
600
  listProjects(): Promise<ProjectSummary[]>;
@@ -539,6 +604,8 @@ export interface Store {
539
604
  createMock(input: CreateMockInput): Promise<Mock>;
540
605
  updateMock(id: string, patch: UpdateMockInput): Promise<Mock | null>;
541
606
  removeMock(id: string): Promise<boolean>;
607
+ listFlows(project: string): Promise<Flow[]>;
608
+ placeFlowStep(project: string, name: string, screen: FlowStep, step: number): Promise<Flow | null>;
542
609
  putDraft(mockId: string, draft: Draft | null): Promise<Mock | null>;
543
610
  listPosts(query?: PostQuery): Promise<Post[]>;
544
611
  countPostsBySession(): Promise<Map<string, number>>;
@@ -547,7 +614,9 @@ export interface Store {
547
614
  createPost(input: CreatePostInput): Promise<Post | null>;
548
615
  updatePost(id: string, patch: UpdatePostInput): Promise<Post | null>;
549
616
  removePost(id: string): Promise<boolean>;
550
- setPostStatus(id: string, status: PostStatus): Promise<Post | null>;
617
+ setPostStatus(id: string, status: PostStatus, acceptedVersion?: number): Promise<Post | null>;
618
+ setPostStarred(id: string, starred: boolean): Promise<Post | null>;
619
+ setVersionHidden(id: string, version: number, hidden: boolean): Promise<Post | null>;
551
620
  listComments(query: CommentQuery): Promise<Comment[]>;
552
621
  createComment(input: CreateCommentInput): Promise<Comment | null>;
553
622
  removeComment(id: string): Promise<Comment | null>;
@@ -572,6 +641,8 @@ export declare const HISTORY_LIMIT = 20;
572
641
  export declare function reservedAgent(name: string): string;
573
642
  export declare function stripNul<T extends string | null | undefined>(s: T): T;
574
643
  export declare const MAX_ASSET_BYTES: number;
644
+ export declare const ATTACHABLE_IMAGE_TYPES: ReadonlySet<string>;
645
+ export declare const MAX_ASK_IMAGES = 6;
575
646
  export declare const MAX_WORKSPACE_ASSET_BYTES: number;
576
647
  export declare const newId: () => string;
577
648
  export declare function hashAssetId(data: Uint8Array): Promise<string>;
@@ -583,13 +654,27 @@ export declare function projectFromCwd(cwd: string | null | undefined): string |
583
654
  export declare const htmlSurface: (html: string, kits?: unknown) => HtmlSurface;
584
655
  export declare const askAnswered: (a: Pick<Ask, "answer" | "other" | "note">) => boolean;
585
656
  export declare const openAsks: (mock: Pick<Mock, "asks">) => Ask[];
586
- export declare function builtinAskCount(mock: Pick<Mock, "asks" | "states">, posts: Pick<Post, "mock" | "state" | "variant" | "status">[], mockId: string): number;
587
- export declare const openCount: (mock: Pick<Mock, "id" | "asks" | "states">, posts: Pick<Post, "mock" | "state" | "variant" | "status">[]) => number;
657
+ export declare function builtinAskIds(mock: Pick<Mock, "asks" | "states"> & {
658
+ resolvedAt?: string | null;
659
+ }, posts: Pick<Post, "mock" | "state" | "variant" | "status">[], mockId: string): string[];
660
+ export declare const builtinAskCount: (mock: Pick<Mock, "asks" | "states"> & {
661
+ resolvedAt?: string | null;
662
+ }, posts: Pick<Post, "mock" | "state" | "variant" | "status">[], mockId: string) => number;
663
+ export declare const openCount: (mock: Pick<Mock, "id" | "asks" | "states"> & {
664
+ resolvedAt?: string | null;
665
+ }, posts: Pick<Post, "mock" | "state" | "variant" | "status">[]) => number;
666
+ export declare function draftedCount(mock: Pick<Mock, "id" | "asks" | "states" | "draft"> & {
667
+ resolvedAt?: string | null;
668
+ }, posts: Pick<Post, "mock" | "state" | "variant" | "status">[]): number | null;
588
669
  export declare const BUILTIN_ASK_ID = "variant";
589
670
  export declare function builtinAsk(mock: Pick<Mock, "states">, posts: Pick<Post, "state" | "variant">[], id: string): Ask | undefined;
590
671
  export declare function summarizeProjects(mocks: Mock[], sessions: Session[], posts?: Post[]): ProjectSummary[];
591
672
  export declare function surfacesByteLength(surfaces: Surface[]): number;
592
673
  export declare function collectAssetIds(surfaces: Surface[], out: Set<string>): void;
674
+ export declare function collectAttachedIds(holder: {
675
+ otherImages?: unknown;
676
+ noteImages?: unknown;
677
+ } | null | undefined, out: Set<string>): void;
593
678
  export interface EvictionCandidate {
594
679
  id: string;
595
680
  byteLength: number;
@@ -51,9 +51,50 @@ export function isSurfaceKind(kind) {
51
51
  export function isSandboxedSurfaceKind(kind) {
52
52
  return isSurfaceKind(kind) && SURFACE_KIND_METADATA[kind].sandboxed;
53
53
  }
54
+ // Still in the running: what the user can choose between.
55
+ export const isLive = (status) => status === "open" || status === "accepted";
56
+ // The version a variant is resolved at when nothing names one: the version in
57
+ // use when accepted, else its latest.
58
+ export const inUseVersion = (post) => post.status === "accepted" ? (post.acceptedVersion ?? post.version) : post.version;
59
+ // A variant as it stood at one of its versions (current or retained), or null
60
+ // when that version is gone.
61
+ export function postAtVersion(post, version) {
62
+ if (version === post.version)
63
+ return post;
64
+ const old = post.history.find((h) => h.version === version);
65
+ if (!old)
66
+ return null;
67
+ const { from: _f, prompt: _p, author: _a, ...rest } = post;
68
+ return {
69
+ ...rest,
70
+ version: old.version,
71
+ title: old.title,
72
+ surfaces: old.surfaces,
73
+ updatedAt: old.at,
74
+ ...(old.from === undefined ? {} : { from: old.from }),
75
+ ...(old.prompt === undefined ? {} : { prompt: old.prompt }),
76
+ ...(old.author === undefined ? {} : { author: old.author }),
77
+ };
78
+ }
79
+ // The variant's newest version not after mock version `n` — what the stage
80
+ // showed when a draft or reply was bound to n (the viewer's frameVersion).
81
+ export function versionAt(post, n) {
82
+ const versions = [post.version, ...post.history.map((h) => h.version)];
83
+ const fit = versions.filter((v) => v <= n);
84
+ return fit.length ? Math.max(...fit) : Math.min(...versions);
85
+ }
86
+ // The state a version option names a version in: the ask's state, else the
87
+ // mock's first (`ask` accepts version options without a state only while the
88
+ // mock has one).
89
+ export const optionState = (ask, mock) => ask.state ?? mock.states[0] ?? null;
54
90
  // The viewer's write-in option on every ask. Reserved, so a delivered
55
91
  // `{id: "other", other: true}` choice never shadows an option the agent declared.
56
92
  export const OTHER_ID = "other";
93
+ // A one-tap verdict on a part (or, with part null, on the variant itself),
94
+ // for a reviewer on a phone who would rather not type: like, dislike, or
95
+ // "more like this". Rides as a part comment, so it travels in the one reply.
96
+ export const REACTIONS = ["up", "down", "more"];
97
+ export const sameScreen = (a, b) => a.mock === b.mock && a.state === b.state;
57
98
  export const HISTORY_LIMIT = 20;
58
99
  // "user" is the reserved trust label for genuine human comments. A session
59
100
  // agent with that name could otherwise have its programmatic comments delivered
@@ -73,6 +114,15 @@ export function stripNul(s) {
73
114
  // budget the store evicts down to. One Durable Object holds the whole workspace, so
74
115
  // the budget sits well under its ~10 GB SQLite ceiling.
75
116
  export const MAX_ASSET_BYTES = 5 * 1024 * 1024;
117
+ // What the user may attach to a write-in or a note: raster formats a model can
118
+ // read. SVG is active content, so it is never attachable.
119
+ export const ATTACHABLE_IMAGE_TYPES = new Set([
120
+ "image/png",
121
+ "image/jpeg",
122
+ "image/webp",
123
+ "image/gif",
124
+ ]);
125
+ export const MAX_ASK_IMAGES = 6;
76
126
  export const MAX_WORKSPACE_ASSET_BYTES = 2 * 1024 * 1024 * 1024;
77
127
  // Short, unguessable id: 8 random bytes (64 bits) as 11 url-safe base64 chars.
78
128
  // These double as bearer capabilities: in publicRead mode `/s/:id` and
@@ -139,15 +189,18 @@ export const htmlSurface = (html, kits) => ({
139
189
  // An open ask is one no reply has answered yet: a write-in or a note alone
140
190
  // answers it too ("Neither, because…").
141
191
  export const askAnswered = (a) => a.answer !== undefined || a.other !== undefined || a.note !== undefined;
142
- export const openAsks = (mock) => mock.asks.filter((a) => !askAnswered(a));
192
+ export const openAsks = (mock) => mock.asks.filter((a) => !askAnswered(a) && !a.dismissed);
143
193
  // How many built-in "Which one?" asks the viewer shows: one mock-wide when every
144
194
  // state needs one and the live names line up, else one per needing state. Same
145
195
  // rule as viewer/src/logic.ts builtinAsks, so Home, `read` and `pending` count
146
196
  // the question the user actually sees. Answering it archives the losers, which
147
197
  // is what makes it stop counting.
148
- export function builtinAskCount(mock, posts, mockId) {
198
+ export function builtinAskIds(mock, posts, mockId) {
199
+ // Resolving closed the choice along with every other open question.
200
+ if (mock.resolvedAt)
201
+ return [];
149
202
  const live = (state) => posts
150
- .filter((p) => p.mock === mockId && p.state === state && p.status !== "archived")
203
+ .filter((p) => p.mock === mockId && p.state === state && isLive(p.status))
151
204
  .map((p) => p.variant);
152
205
  const needs = (state) => {
153
206
  const names = live(state);
@@ -159,14 +212,30 @@ export function builtinAskCount(mock, posts, mockId) {
159
212
  const states = mock.states.length ? mock.states : [null];
160
213
  const needing = states.filter(needs);
161
214
  if (!needing.length)
162
- return 0;
215
+ return [];
163
216
  const key = (state) => [...live(state)].sort().join("\n");
164
217
  const first = key(needing[0]);
165
218
  return needing.length === states.length && states.every((st) => key(st) === first)
166
- ? 1
167
- : needing.length;
219
+ ? [BUILTIN_ASK_ID]
220
+ : needing.map((st) => (st === null ? BUILTIN_ASK_ID : `${BUILTIN_ASK_ID}:${st}`));
168
221
  }
222
+ export const builtinAskCount = (mock, posts, mockId) => builtinAskIds(mock, posts, mockId).length;
169
223
  export const openCount = (mock, posts) => openAsks(mock).length + builtinAskCount(mock, posts, mock.id);
224
+ // How many of the open asks the user's unsent draft already speaks for (a
225
+ // pick, a write-in or a note), so Home can show a started mock as "draft 2/4";
226
+ // null when there is no draft. A count only: the draft's content stays private.
227
+ export function draftedCount(mock, posts) {
228
+ const d = mock.draft;
229
+ if (!d)
230
+ return null;
231
+ const open = [...openAsks(mock).map((a) => a.id), ...builtinAskIds(mock, posts, mock.id)];
232
+ const said = (id) => {
233
+ const a = d.answers[id];
234
+ const picked = Array.isArray(a) ? a.length > 0 : a !== undefined;
235
+ return picked || d.others?.[id] !== undefined || d.notes?.[id] !== undefined;
236
+ };
237
+ return open.filter(said).length;
238
+ }
170
239
  // The viewer's built-in "Which one?" for variants no agent ask binds: `variant`
171
240
  // mock-wide, `variant:<state>` per state. Never stored; synthesized from the
172
241
  // variants so a reply answering it validates, flips and reads like any ask. An
@@ -275,6 +344,20 @@ export function collectAssetIds(surfaces, out) {
275
344
  out.add(p.assetId);
276
345
  }
277
346
  }
347
+ // The images a draft, a reply or a sent ask carries (all keep the user's
348
+ // attachments in otherImages / noteImages).
349
+ export function collectAttachedIds(holder, out) {
350
+ for (const map of [holder?.otherImages, holder?.noteImages]) {
351
+ if (!map || typeof map !== "object")
352
+ continue;
353
+ for (const ids of Array.isArray(map) ? [map] : Object.values(map)) {
354
+ if (Array.isArray(ids))
355
+ for (const id of ids)
356
+ if (typeof id === "string")
357
+ out.add(id);
358
+ }
359
+ }
360
+ }
278
361
  // Pick the assets to evict so `incomingBytes` fits under `budget`. Oldest
279
362
  // (lastAccessedAt) first, but unreferenced assets go before referenced ones —
280
363
  // a live embed is only evicted as a last resort, once unreferenced candidates
@@ -0,0 +1,2 @@
1
+ export declare const VIEWER_CLIENT = "viewer";
2
+ export declare function isViewerRequest(request: Request): boolean;
@@ -0,0 +1,14 @@
1
+ // Whether a request comes from the trusted viewer, the page this server
2
+ // served: only it may act as the user (drafts, replies, user comments).
3
+ // Fetch Metadata says so directly, but browsers omit it on plain http away
4
+ // from localhost (a LAN or tailnet address), where the viewer then looked
5
+ // like any other client and every draft call failed. The viewer also names
6
+ // itself with a custom header, which a cross-origin caller cannot send: that
7
+ // header forces a CORS preflight, and this server answers none, so a sandboxed
8
+ // surface (opaque origin) or another site is refused by the browser itself.
9
+ export const VIEWER_CLIENT = "viewer";
10
+ export function isViewerRequest(request) {
11
+ if (request.headers.get("sec-fetch-site") === "same-origin")
12
+ return true;
13
+ return request.headers.get("x-mockpit-client") === VIEWER_CLIENT;
14
+ }
@@ -220,6 +220,7 @@ function feedbackLines(feedback) {
220
220
  const lines = [];
221
221
  for (const batch of feedback ?? []) {
222
222
  const prefix = batch.mock ? `${batch.mock}: ` : "";
223
+ if (batch.handoff) lines.push(`- ${prefix}${batch.handoff}`);
223
224
  const reply = batch.reply;
224
225
  if (reply) {
225
226
  for (const a of reply.asks ?? []) {
@@ -7,9 +7,12 @@ where to look and end your turn.
7
7
 
8
8
  ```sh
9
9
  mockpit ask --mock writer "Which look?" --option Quiet=quiet --option Dark=dark --id look
10
+ mockpit ask --mock writer "Trim?" --option Above=knob:trim.position=top --option Below=knob:trim.position=bottom
10
11
  mockpit ask --mock writer --asks asks.json
11
12
  ```
12
13
 
14
+ `=knob:` sets one knob; for an option that sets several, use `--asks` JSON.
15
+
13
16
  ## Shape
14
17
 
15
18
  The same JSON goes to `--asks`, MCP `ask` and `POST /api/mocks/:id/asks`:
@@ -49,39 +52,95 @@ The same JSON goes to `--asks`, MCP `ask` and `POST /api/mocks/:id/asks`:
49
52
  ## Scope
50
53
 
51
54
  `scope` says what an ask decides: `mock` (the default), `state` (add `state`)
52
- or `part` (add `part`, the `data-part` name).
55
+ or `part` (add `part`, the `data-part` name). Scope also says where the user
56
+ sees it: the viewer shows each state with its own questions. A `state` ask
57
+ shows on that state, a `part` ask on every state that marks the part, and a
58
+ `mock` ask whose options name variants on every state that has them (any
59
+ other `mock` ask on every state); answered once, it counts once.
60
+
61
+ Only open asks are shown and counted ("1 of 3"). Once a Send answers one, it
62
+ folds into a read-only Answered list and stays out of the next round, so ask
63
+ new questions for what is still open rather than re-sending answered ones; the
64
+ user changes a sent answer only on purpose, and the change arrives as a normal
65
+ Send.
53
66
 
54
67
  ## Options
55
68
 
56
69
  An option binds to one of:
57
70
 
58
71
  - a `variant`: picking it accepts that variant and archives its siblings in
59
- that state.
72
+ that state. Add `version` to offer one version of it (`"version": 2`, CLI
73
+ `--option Before=quiet@2`): two options can be two versions of one variant,
74
+ and the picked one is put in use. It needs the ask's `state` when the mock
75
+ has several.
60
76
  - a knob `set`: picking it applies those values live.
61
77
  - nothing: a plain pill.
62
78
 
79
+ Options that change how something looks carry a `variant` or a knob `set`,
80
+ so the user sees them live. Plain pills only for what can't be seen.
81
+
82
+ When one question only makes sense once another is answered (an accent's
83
+ placement depends on the palette), give it `after: "<that ask's id>"` (CLI
84
+ `--after palette`). It stays closed until that one is answered, then its
85
+ pictures render with that answer and say so. Ask the deciding question first.
86
+
63
87
  The viewer shows a picture of each option it can render. `multi: true` allows
64
88
  several answers. Option ids default to the slugged label. Reusing an ask `id`
65
89
  replaces that ask. The viewer adds an "Other…" write-in and a note to every
66
- ask, so don't add your own; the option id `other` is reserved.
90
+ ask (both take attached images), so don't add your own; the option id `other`
91
+ is reserved.
92
+
93
+ ## Slots
94
+
95
+ A slot is one region with alternative fills, in one html. Give each
96
+ alternative the same `data-part` and its own `data-fill`:
97
+
98
+ ```html
99
+ <section data-part="entry" data-part-label="Entry" data-fill="dots" data-fill-label="Typing dots">
100
+ …
101
+ </section>
102
+ <section data-part="entry" data-fill="skeleton">…</section>
103
+ <section data-part="entry" data-fill="filling" data-fill-default>…</section>
104
+ ```
105
+
106
+ The first fill is the default unless one carries `data-fill-default`. The
107
+ server derives a select knob at the part's name and shows only the chosen
108
+ fill, so you write no switching CSS; give each fill whatever display it
109
+ needs. Draw anything inside a fill, including further slots: a slot inside a
110
+ fill is asked only while that fill is chosen, right after its parent's
111
+ question.
112
+
113
+ Publish returns the slots it found and, for each one no ask binds, a ready
114
+ `suggestedSlotAsks` entry: `scope: "part"`, one option per fill with
115
+ `set: {"entry": "<fill>"}`. Send them with `ask`, relabelled if you like. On a
116
+ pick the viewer outlines the fill on stage. `fills` in publish replaces or
117
+ adds one fill without resending the page (topic `feedback`).
118
+
119
+ Use a slot when the alternatives differ in one region; use variants when the
120
+ whole screen differs. Both mix: variants are looks, each look may have slots.
67
121
 
68
122
  ## The Look ask
69
123
 
70
124
  When you publish variants, ask `scope: "mock"` with one option per variant, and
71
125
  ask it first. Questions appear in the order you ask them. This one is titled
72
- "Look" and unlocks Mix, which lets the user borrow a part from another look.
126
+ "Look". Tune then offers, per part, to borrow it from another look.
73
127
  Variants without a Look ask get a plain switcher in the frame header.
74
128
 
129
+ A variant decides everything it differs on; don't re-ask it. Ask other axes as
130
+ knob `set`s.
131
+
75
132
  ## When you forget to ask
76
133
 
77
134
  A publish that leaves a state with two or more open variants and no ask
78
135
  binding them returns a nudge and a ready `suggestedAsk` (mock-wide when the
79
- variant names line up across states, else for that state). Send it as is with
80
- `ask`, or write your own question with options bound to the variants. The CLI
81
- prints the `mockpit ask` command for you.
82
-
83
- Until an ask binds them, the viewer shows a built-in "Which one?" with a
84
- picture of each variant, so the user can still choose in one Send. Its answer
136
+ variant names line up across states, else for that state), or your existing
137
+ ask plus the new variants. Send it as is with `ask`, or write your own question
138
+ with options bound to the variants. The CLI prints the `mockpit ask` command
139
+ for you. Sending it stops the nudge and keeps the answered Look ask on the
140
+ mock; the built-in one is gone once answered.
141
+
142
+ Until an ask binds them, the viewer shows a built-in Look ask with a picture
143
+ of each variant, so the user can still choose in one Send. Its answer
85
144
  reads like any other ask under the reserved id `variant` (per state:
86
145
  `variant:<state>`), and accepts and archives per state. Your own ask over
87
146
  those variants replaces it.