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.
- package/README.md +13 -6
- package/bin/mockpit.js +139 -18
- package/dist/mcp/server.js +30 -10
- package/dist/server/apiViews.d.ts +129 -25
- package/dist/server/apiViews.js +86 -23
- package/dist/server/app.d.ts +2 -1
- package/dist/server/app.js +1258 -200
- package/dist/server/appIcon.d.ts +22 -0
- package/dist/server/appIcon.js +43 -0
- package/dist/server/appIconPng.d.ts +1 -0
- package/dist/server/appIconPng.js +6 -0
- package/dist/server/askNudges.d.ts +3 -0
- package/dist/server/askNudges.js +40 -0
- package/dist/server/base64.d.ts +1 -0
- package/dist/server/base64.js +8 -0
- package/dist/server/decisions.d.ts +23 -0
- package/dist/server/decisions.js +99 -0
- package/dist/server/designGuide.js +53 -16
- package/dist/server/embeds.d.ts +25 -0
- package/dist/server/embeds.js +118 -0
- package/dist/server/eventLog.js +2 -1
- package/dist/server/feedbackBatch.d.ts +28 -5
- package/dist/server/feedbackBatch.js +78 -11
- package/dist/server/fills.d.ts +41 -0
- package/dist/server/fills.js +328 -0
- package/dist/server/handoff.d.ts +6 -0
- package/dist/server/handoff.js +38 -0
- package/dist/server/index.js +15 -1
- package/dist/server/knobs.d.ts +6 -2
- package/dist/server/knobs.js +13 -4
- package/dist/server/mcpHttp.d.ts +5 -2
- package/dist/server/mcpHttp.js +14 -5
- package/dist/server/mcpSpec.d.ts +21 -13
- package/dist/server/mcpSpec.js +99 -9
- package/dist/server/postSurfaces.js +3 -12
- package/dist/server/richRender.js +3 -1
- package/dist/server/run.js +6 -2
- package/dist/server/runApi.d.ts +1 -1
- package/dist/server/runApi.js +5 -4
- package/dist/server/sqlStore.d.ts +92 -5
- package/dist/server/sqlStore.js +188 -30
- 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 +95 -10
- package/dist/server/types.js +89 -6
- package/dist/server/viewerRequest.d.ts +2 -0
- package/dist/server/viewerRequest.js +14 -0
- package/extensions/mockpit.js +1 -0
- package/guide/topics/asks.md +69 -10
- package/guide/topics/feedback.md +104 -16
- package/guide/topics/html.md +5 -1
- package/guide/topics/http.md +2 -1
- package/guide/topics/knobs.md +4 -3
- package/guide/topics/surfaces.md +32 -16
- package/package.json +4 -13
- package/skills/launch-video/SKILL.md +1 -1
- package/skills/mockpit/SKILL.md +14 -4
- package/viewer/dist/index.html +9 -9
- package/dist/server/slots.d.ts +0 -14
- package/dist/server/slots.js +0 -71
package/dist/server/types.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
587
|
-
|
|
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;
|
package/dist/server/types.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
?
|
|
167
|
-
: needing.
|
|
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,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
|
+
}
|
package/extensions/mockpit.js
CHANGED
|
@@ -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 ?? []) {
|
package/guide/topics/asks.md
CHANGED
|
@@ -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`
|
|
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"
|
|
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)
|
|
80
|
-
|
|
81
|
-
prints the `mockpit ask` command
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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.
|