mockpit 1.1.0 → 1.2.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 +4 -2
- package/bin/mockpit.js +24 -3
- package/dist/mcp/server.js +20 -3
- package/dist/server/apiViews.d.ts +11 -8
- package/dist/server/apiViews.js +21 -16
- package/dist/server/app.js +344 -59
- 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/designGuide.js +15 -13
- package/dist/server/feedbackBatch.d.ts +16 -4
- package/dist/server/feedbackBatch.js +59 -9
- package/dist/server/handoff.d.ts +6 -0
- package/dist/server/handoff.js +38 -0
- package/dist/server/knobs.js +2 -3
- package/dist/server/mcpHttp.d.ts +2 -2
- package/dist/server/mcpHttp.js +11 -5
- package/dist/server/mcpSpec.d.ts +8 -0
- package/dist/server/mcpSpec.js +26 -1
- package/dist/server/run.js +2 -2
- package/dist/server/runApi.d.ts +1 -1
- package/dist/server/runApi.js +1 -1
- package/dist/server/sqlStore.d.ts +3 -1
- package/dist/server/sqlStore.js +40 -8
- package/dist/server/types.d.ts +25 -2
- package/dist/server/types.js +47 -4
- package/extensions/mockpit.js +1 -0
- package/guide/topics/asks.md +19 -7
- package/guide/topics/feedback.md +15 -5
- package/guide/topics/knobs.md +3 -3
- package/guide/topics/surfaces.md +8 -8
- package/package.json +1 -1
- package/skills/mockpit/SKILL.md +4 -2
- package/viewer/dist/index.html +9 -9
package/dist/server/runApi.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const RUN_API = "// The body of an async function, in plain JavaScript (no TypeScript syntax).\n// `await mockpit.*`, `return` a JSON value, print() adds an output line.\n// No network, timers, imports or process. Session, project and agent are fixed\n// for the run; pass the returned session to the next run.\ntype Ref = { mock: string; state?: string; variant?: string };\ntype Knobs = Record<string, unknown>; // path \u2192 [default,min,max,step] | boolean | {type:\"select\",options} | \"#hex\"\ntype Surface =\n | { kind: \"html\"; html: string; kits?: string[] }\n | { kind: \"markdown\"; markdown: string }\n | { kind: \"diff\"; patch?: string; files?: { filename: string; before: string; after: string }[]; layout?: \"unified\" | \"split\" }\n | { kind: \"image\"; assetId: string; alt?: string; caption?: string }\n | { kind: \"terminal\"; text: string; cols?: number; title?: string }\n | { kind: \"mermaid\"; mermaid: string }\n | { kind: \"json\"; data: unknown; title?: string }\n | { kind: \"code\"; code: string; language?: string; lineStart?: number };\ntype Ask = {\n id?: string; text: string; scope?: \"mock\" | \"state\" | \"part\"; state?: string; part?: string; multi?: boolean;\n options: { id?: string; label: string; variant?: string; set?: Knobs }[];\n};\ntype Written = {\n post: { state: string | null; variant: string; version: number }; url: string;\n parts: { state: string | null; parts: { name: string }[] }[];\n partChanges?: { vanished: string[]; renamed: { from: string; to: string }[] };\n nudges?: string[]; suggestedAsk?: Ask; // send it with ask when nudged\n};\ntype Feedback = {\n mock: string | null;\n reply: null | { asks: { ask: string; chosen: { id: string; label: string; other?: true }[]; note?: string }[]; mix: Knobs; tuned: Knobs; comments: { part: string | null; text: string }[] };\n comments: { text: string; state: string | null; variant: string | null }[];\n accepted: { state: string | null; variant: string }[]; archived: { state: string | null; variant: string }[];\n};\ntype Pending = { mock: string; viewerOpen: boolean; draft: null | { answered: number; of: number; comments: number; touchedAt: string } };\ndeclare const mockpit: {\n guide(topic?: \"knobs\" | \"asks\" | \"surfaces\" | \"html\" | \"feedback\" | \"http\" | \"scripts\"): Promise<string>;\n /** No mock: every mock plus pending. With one: its states, variants, asks, parts, knobs. */\n read(mock?: string, opts?: { body?: boolean; history?: boolean }): Promise<unknown>;\n /** Creates or versions (mock, state, variant). One of: html; surfaces, the full ordered list\n * ({id} alone keeps that surface, a missing id is removed); parts {\"name\"|\"name#key\": outer html}. */\n publish(v: Ref & { title?: string; kind?: \"component\" | \"page\"; html?: string; surfaces?: (Surface | { id: string })[]; parts?: Record<string, string>; knobs?: Knobs; variantKnobs?: Knobs; from?: number }): Promise<Written>;\n ask(mock: string, asks: Ask[]): Promise<{ asks: { id: string; text: string }[]; url: string }>;\n /** Never waits: what the user sent since you last heard, and what they are doing now. */\n feedback(): Promise<{ feedback: Feedback[]; pending: Pending[] }>;\n say(v: Ref, message: string): Promise<void>;\n export(v: Ref): Promise<{ states: { state: string | null; variant: string; version: number; html: string }[]; reply: unknown }>;\n /** data: base64 bytes. Use id as an image surface's assetId. */\n upload(data: string, opts: { contentType: string; filename?: string; kind?: \"image\" | \"file\" }): Promise<{ id: string; url: string }>;\n};\ndeclare function print(...values: unknown[]): void;";
|
|
1
|
+
export declare const RUN_API = "// The body of an async function, in plain JavaScript (no TypeScript syntax).\n// `await mockpit.*`, `return` a JSON value, print() adds an output line.\n// No network, timers, imports or process. Session, project and agent are fixed\n// for the run; pass the returned session to the next run.\ntype Ref = { mock: string; state?: string; variant?: string };\ntype Knobs = Record<string, unknown>; // path \u2192 [default,min,max,step] | boolean | {type:\"select\",options} | \"#hex\"\ntype Surface =\n | { kind: \"html\"; html: string; kits?: string[] }\n | { kind: \"markdown\"; markdown: string }\n | { kind: \"diff\"; patch?: string; files?: { filename: string; before: string; after: string }[]; layout?: \"unified\" | \"split\" }\n | { kind: \"image\"; assetId: string; alt?: string; caption?: string }\n | { kind: \"terminal\"; text: string; cols?: number; title?: string }\n | { kind: \"mermaid\"; mermaid: string }\n | { kind: \"json\"; data: unknown; title?: string }\n | { kind: \"code\"; code: string; language?: string; lineStart?: number };\ntype Ask = {\n id?: string; text: string; scope?: \"mock\" | \"state\" | \"part\"; state?: string; part?: string; multi?: boolean;\n options: { id?: string; label: string; variant?: string; set?: Knobs }[];\n};\ntype Written = {\n post: { state: string | null; variant: string; version: number }; url: string;\n parts: { state: string | null; parts: { name: string }[] }[];\n partChanges?: { vanished: string[]; renamed: { from: string; to: string }[] };\n nudges?: string[]; suggestedAsk?: Ask; // send it with ask when nudged\n};\ntype Feedback = {\n mock: string | null;\n reply: null | { asks: { ask: string; chosen: { id: string; label: string; other?: true }[]; note?: string }[]; mix: Knobs; tuned: Knobs; comments: { part: string | null; text: string }[] };\n comments: { text: string; state: string | null; variant: string | null }[];\n accepted: { state: string | null; variant: string }[]; archived: { state: string | null; variant: string }[];\n};\ntype Pending = { mock: string; viewerOpen: boolean; draft: null | { answered: number; of: number; comments: number; touchedAt: string } };\ndeclare const mockpit: {\n guide(topic?: \"knobs\" | \"asks\" | \"surfaces\" | \"html\" | \"feedback\" | \"http\" | \"scripts\"): Promise<string>;\n /** No mock: every mock plus pending. With one: its states, variants, asks, parts, knobs. */\n read(mock?: string, opts?: { body?: boolean; history?: boolean }): Promise<unknown>;\n /** Creates or versions (mock, state, variant). One of: html; surfaces, the full ordered list\n * ({id} alone keeps that surface, a missing id is removed); parts {\"name\"|\"name#key\": outer html}. */\n publish(v: Ref & { title?: string; kind?: \"component\" | \"page\"; html?: string; surfaces?: (Surface | { id: string })[]; parts?: Record<string, string>; knobs?: Knobs; variantKnobs?: Knobs; from?: number }): Promise<Written>;\n ask(mock: string, asks: Ask[]): Promise<{ asks: { id: string; text: string }[]; url: string; nudges?: string[] }>;\n /** Never waits: what the user sent since you last heard, and what they are doing now. */\n feedback(): Promise<{ feedback: Feedback[]; pending: Pending[] }>;\n say(v: Ref, message: string): Promise<void>;\n export(v: Ref): Promise<{ states: { state: string | null; variant: string; version: number; html: string }[]; reply: unknown }>;\n /** data: base64 bytes. Use id as an image surface's assetId. */\n upload(data: string, opts: { contentType: string; filename?: string; kind?: \"image\" | \"file\" }): Promise<{ id: string; url: string }>;\n};\ndeclare function print(...values: unknown[]): void;";
|
|
2
2
|
export declare const RUN_FUNCTIONS: readonly ["guide", "read", "publish", "ask", "feedback", "say", "export", "upload"];
|
|
3
3
|
export type RunFunction = (typeof RUN_FUNCTIONS)[number];
|
|
4
4
|
export declare const RUN_EXAMPLE = "const html = (tone) => `<section data-part=\"hero\" class=\"${tone}\"><h1>Writer</h1></section>`;\nawait mockpit.publish({ mock: \"writer\", variant: \"calm\", html: html(\"calm\") });\nawait mockpit.publish({ mock: \"writer\", variant: \"bold\", html: html(\"bold\") });\nconst { url } = await mockpit.ask(\"writer\", [{ id: \"look\", text: \"Which look?\",\n options: [{ label: \"Calm\", variant: \"calm\" }, { label: \"Bold\", variant: \"bold\" }] }]);\n// Tell the user where to look and end the turn; a later run reads mockpit.feedback().\nreturn url;";
|
package/dist/server/runApi.js
CHANGED
|
@@ -43,7 +43,7 @@ declare const mockpit: {
|
|
|
43
43
|
/** Creates or versions (mock, state, variant). One of: html; surfaces, the full ordered list
|
|
44
44
|
* ({id} alone keeps that surface, a missing id is removed); parts {"name"|"name#key": outer html}. */
|
|
45
45
|
publish(v: Ref & { title?: string; kind?: "component" | "page"; html?: string; surfaces?: (Surface | { id: string })[]; parts?: Record<string, string>; knobs?: Knobs; variantKnobs?: Knobs; from?: number }): Promise<Written>;
|
|
46
|
-
ask(mock: string, asks: Ask[]): Promise<{ asks: { id: string; text: string }[]; url: string }>;
|
|
46
|
+
ask(mock: string, asks: Ask[]): Promise<{ asks: { id: string; text: string }[]; url: string; nudges?: string[] }>;
|
|
47
47
|
/** Never waits: what the user sent since you last heard, and what they are doing now. */
|
|
48
48
|
feedback(): Promise<{ feedback: Feedback[]; pending: Pending[] }>;
|
|
49
49
|
say(v: Ref, message: string): Promise<void>;
|
|
@@ -26,7 +26,7 @@ export declare class SqlStore implements Store {
|
|
|
26
26
|
private rowToPost;
|
|
27
27
|
private rowToAsset;
|
|
28
28
|
private rowToComment;
|
|
29
|
-
listSessions(): Promise<Session[]>;
|
|
29
|
+
listSessions(project?: string): Promise<Session[]>;
|
|
30
30
|
getSession(id: string): Promise<Session | null>;
|
|
31
31
|
getSessionByKey(key: string): Promise<Session | null>;
|
|
32
32
|
createSession(input: CreateSessionInput): Promise<Session>;
|
|
@@ -34,6 +34,8 @@ export declare class SqlStore implements Store {
|
|
|
34
34
|
removeSession(id: string): Promise<boolean>;
|
|
35
35
|
private touch;
|
|
36
36
|
markAgentSeen(sessionId: string, seq: number): Promise<void>;
|
|
37
|
+
touchAgent(sessionId: string): Promise<void>;
|
|
38
|
+
handOff(sessionId: string, seen: Pick<Session, "agentSeq" | "agentActiveAt">, toSeq: number): Promise<boolean>;
|
|
37
39
|
getSetting(key: string): Promise<string | null>;
|
|
38
40
|
setSetting(key: string, value: string): Promise<void>;
|
|
39
41
|
listProjects(): Promise<import("./types.ts").ProjectSummary[]>;
|
package/dist/server/sqlStore.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { collectAssetIds, DEFAULT_PROJECT, DEFAULT_VARIANT, hashAssetId, HISTORY_LIMIT, htmlSurface, MAX_WORKSPACE_ASSET_BYTES, newId, normalizeSurfaceIds, projectFromCwd, reservedAgent, selectEvictions, slugify, stripNul, summarizeProjects, } from "./types.js";
|
|
1
|
+
import { collectAssetIds, collectAttachedIds, DEFAULT_PROJECT, DEFAULT_VARIANT, hashAssetId, HISTORY_LIMIT, htmlSurface, MAX_WORKSPACE_ASSET_BYTES, newId, normalizeSurfaceIds, projectFromCwd, reservedAgent, selectEvictions, slugify, stripNul, summarizeProjects, } from "./types.js";
|
|
2
2
|
function parseJson(raw, fallback) {
|
|
3
3
|
if (typeof raw !== "string" || !raw)
|
|
4
4
|
return fallback;
|
|
@@ -65,7 +65,7 @@ export class SqlStore {
|
|
|
65
65
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
66
66
|
id TEXT PRIMARY KEY, agent TEXT NOT NULL, title TEXT, cwd TEXT,
|
|
67
67
|
createdAt TEXT NOT NULL, lastActiveAt TEXT NOT NULL,
|
|
68
|
-
agentSeq INTEGER NOT NULL DEFAULT 0, project TEXT, key TEXT
|
|
68
|
+
agentSeq INTEGER NOT NULL DEFAULT 0, project TEXT, key TEXT, agentActiveAt TEXT
|
|
69
69
|
);
|
|
70
70
|
CREATE TABLE IF NOT EXISTS mocks (
|
|
71
71
|
id TEXT PRIMARY KEY, project TEXT NOT NULL, slug TEXT NOT NULL, title TEXT NOT NULL,
|
|
@@ -100,6 +100,7 @@ export class SqlStore {
|
|
|
100
100
|
agentSeq: "INTEGER NOT NULL DEFAULT 0",
|
|
101
101
|
project: "TEXT",
|
|
102
102
|
key: "TEXT",
|
|
103
|
+
agentActiveAt: "TEXT",
|
|
103
104
|
});
|
|
104
105
|
if (legacy)
|
|
105
106
|
this.migrateLegacy();
|
|
@@ -381,6 +382,8 @@ export class SqlStore {
|
|
|
381
382
|
lastActiveAt: r.lastActiveAt,
|
|
382
383
|
agentSeq: r.agentSeq ?? 0,
|
|
383
384
|
project: r.project ?? null,
|
|
385
|
+
agentActiveAt: r.agentActiveAt ?? null,
|
|
386
|
+
keyed: r.key != null,
|
|
384
387
|
};
|
|
385
388
|
}
|
|
386
389
|
rowToMock(r) {
|
|
@@ -458,11 +461,13 @@ export class SqlStore {
|
|
|
458
461
|
};
|
|
459
462
|
}
|
|
460
463
|
// --- sessions ---
|
|
461
|
-
async listSessions() {
|
|
462
|
-
|
|
463
|
-
.exec("SELECT * FROM sessions ORDER BY lastActiveAt DESC")
|
|
464
|
-
.
|
|
465
|
-
|
|
464
|
+
async listSessions(project) {
|
|
465
|
+
const rows = project === undefined
|
|
466
|
+
? this.sql.exec("SELECT * FROM sessions ORDER BY lastActiveAt DESC").toArray()
|
|
467
|
+
: this.sql
|
|
468
|
+
.exec("SELECT * FROM sessions WHERE project = ? ORDER BY lastActiveAt DESC", project)
|
|
469
|
+
.toArray();
|
|
470
|
+
return rows.map((r) => this.rowToSession(r));
|
|
466
471
|
}
|
|
467
472
|
async getSession(id) {
|
|
468
473
|
const rows = this.sql.exec("SELECT * FROM sessions WHERE id = ?", id).toArray();
|
|
@@ -485,8 +490,10 @@ export class SqlStore {
|
|
|
485
490
|
lastActiveAt: now,
|
|
486
491
|
agentSeq: 0,
|
|
487
492
|
project: stripNul(input.project)?.trim() || projectFromCwd(stripNul(input.cwd ?? null)) || null,
|
|
493
|
+
agentActiveAt: now,
|
|
494
|
+
keyed: !!stripNul(input.key)?.trim(),
|
|
488
495
|
};
|
|
489
|
-
this.sql.exec("INSERT INTO sessions (id, agent, title, cwd, createdAt, lastActiveAt, agentSeq, project, key) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)", session.id, session.agent, session.title, session.cwd, session.createdAt, session.lastActiveAt, session.project, stripNul(input.key)?.trim() || null);
|
|
496
|
+
this.sql.exec("INSERT INTO sessions (id, agent, title, cwd, createdAt, lastActiveAt, agentSeq, project, key, agentActiveAt) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)", session.id, session.agent, session.title, session.cwd, session.createdAt, session.lastActiveAt, session.project, stripNul(input.key)?.trim() || null, session.agentActiveAt);
|
|
490
497
|
return session;
|
|
491
498
|
}
|
|
492
499
|
async renameSession(id, title) {
|
|
@@ -528,6 +535,14 @@ export class SqlStore {
|
|
|
528
535
|
async markAgentSeen(sessionId, seq) {
|
|
529
536
|
this.sql.exec("UPDATE sessions SET agentSeq = ? WHERE id = ? AND agentSeq < ?", seq, sessionId, seq);
|
|
530
537
|
}
|
|
538
|
+
async touchAgent(sessionId) {
|
|
539
|
+
this.sql.exec("UPDATE sessions SET agentActiveAt = ? WHERE id = ?", new Date().toISOString(), sessionId);
|
|
540
|
+
}
|
|
541
|
+
async handOff(sessionId, seen, toSeq) {
|
|
542
|
+
return (this.sql
|
|
543
|
+
.exec("UPDATE sessions SET agentSeq = ? WHERE id = ? AND agentSeq = ? AND agentActiveAt IS ? AND agentSeq < ? RETURNING id", toSeq, sessionId, seen.agentSeq, seen.agentActiveAt, toSeq)
|
|
544
|
+
.toArray().length > 0);
|
|
545
|
+
}
|
|
531
546
|
// --- settings ---
|
|
532
547
|
async getSetting(key) {
|
|
533
548
|
return this.settingSync(key);
|
|
@@ -593,6 +608,8 @@ export class SqlStore {
|
|
|
593
608
|
updatedAt: new Date().toISOString(),
|
|
594
609
|
};
|
|
595
610
|
this.sql.exec("UPDATE mocks SET title = ?, kind = ?, states = ?, asks = ?, knobs = ?, sessionId = ?, updatedAt = ? WHERE id = ?", next.title, next.kind, JSON.stringify(next.states), JSON.stringify(next.asks), JSON.stringify(next.knobs), next.sessionId, next.updatedAt, id);
|
|
611
|
+
if (patch.asks !== undefined)
|
|
612
|
+
this.invalidateAssetRefs();
|
|
596
613
|
return next;
|
|
597
614
|
}
|
|
598
615
|
async removeMock(id) {
|
|
@@ -612,6 +629,7 @@ export class SqlStore {
|
|
|
612
629
|
if (!mock)
|
|
613
630
|
return null;
|
|
614
631
|
this.sql.exec("UPDATE mocks SET draft = ? WHERE id = ?", draft ? JSON.stringify(draft) : null, mockId);
|
|
632
|
+
this.invalidateAssetRefs();
|
|
615
633
|
return { ...mock, draft };
|
|
616
634
|
}
|
|
617
635
|
touchMock(mockId, at) {
|
|
@@ -826,6 +844,8 @@ export class SqlStore {
|
|
|
826
844
|
return null;
|
|
827
845
|
const comment = this.rowToComment(rows[0]);
|
|
828
846
|
this.sql.exec("DELETE FROM comments WHERE id = ?", id);
|
|
847
|
+
if (comment.kind === "reply")
|
|
848
|
+
this.invalidateAssetRefs();
|
|
829
849
|
this.touch(comment.sessionId);
|
|
830
850
|
return comment;
|
|
831
851
|
}
|
|
@@ -851,6 +871,7 @@ export class SqlStore {
|
|
|
851
871
|
for (const id of input.archive) {
|
|
852
872
|
this.sql.exec("UPDATE posts SET status = 'archived' WHERE id = ? AND mockId = ?", id, input.mockId);
|
|
853
873
|
}
|
|
874
|
+
this.invalidateAssetRefs();
|
|
854
875
|
return comment;
|
|
855
876
|
});
|
|
856
877
|
}
|
|
@@ -865,6 +886,17 @@ export class SqlStore {
|
|
|
865
886
|
for (const r of this.sql.exec("SELECT surfaces FROM post_versions").toArray()) {
|
|
866
887
|
collectAssetIds(parseJson(r.surfaces, []), out);
|
|
867
888
|
}
|
|
889
|
+
// The user's attachments: in a draft, in a sent reply, on an answered ask.
|
|
890
|
+
for (const r of this.sql.exec("SELECT asks, draft FROM mocks").toArray()) {
|
|
891
|
+
collectAttachedIds(parseJson(r.draft, null), out);
|
|
892
|
+
for (const ask of parseJson(r.asks, []))
|
|
893
|
+
collectAttachedIds(ask, out);
|
|
894
|
+
}
|
|
895
|
+
for (const r of this.sql
|
|
896
|
+
.exec("SELECT payload FROM comments WHERE kind = 'reply' AND payload IS NOT NULL")
|
|
897
|
+
.toArray()) {
|
|
898
|
+
collectAttachedIds(parseJson(r.payload, null), out);
|
|
899
|
+
}
|
|
868
900
|
// An installed icon set is referenced by its project's design, not by a
|
|
869
901
|
// surface; evicting it would blank that project's icons.
|
|
870
902
|
for (const r of this.sql
|
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];
|
|
@@ -214,6 +216,8 @@ export interface Ask {
|
|
|
214
216
|
answer?: AskAnswer;
|
|
215
217
|
other?: string;
|
|
216
218
|
note?: string;
|
|
219
|
+
otherImages?: string[];
|
|
220
|
+
noteImages?: string[];
|
|
217
221
|
at: string;
|
|
218
222
|
}
|
|
219
223
|
export declare const OTHER_ID = "other";
|
|
@@ -222,12 +226,17 @@ export interface PartCommentAnchor {
|
|
|
222
226
|
quote?: string;
|
|
223
227
|
selector?: string;
|
|
224
228
|
box?: number[];
|
|
229
|
+
width?: number;
|
|
225
230
|
}
|
|
231
|
+
export declare const REACTIONS: readonly ["up", "down", "more"];
|
|
232
|
+
export type Reaction = (typeof REACTIONS)[number];
|
|
226
233
|
export interface PartComment {
|
|
227
234
|
part: string | null;
|
|
228
235
|
state: string | null;
|
|
229
236
|
text: string;
|
|
230
237
|
anchor?: PartCommentAnchor;
|
|
238
|
+
reaction?: Reaction;
|
|
239
|
+
variant?: string;
|
|
231
240
|
}
|
|
232
241
|
export interface ReplyDecision {
|
|
233
242
|
kind: "accept" | "revise" | "drop";
|
|
@@ -242,6 +251,8 @@ export interface Draft {
|
|
|
242
251
|
comments: PartComment[];
|
|
243
252
|
others?: Record<string, string>;
|
|
244
253
|
notes?: Record<string, string>;
|
|
254
|
+
otherImages?: Record<string, string[]>;
|
|
255
|
+
noteImages?: Record<string, string[]>;
|
|
245
256
|
updatedAt: string;
|
|
246
257
|
}
|
|
247
258
|
export interface Reply {
|
|
@@ -253,6 +264,8 @@ export interface Reply {
|
|
|
253
264
|
comments: PartComment[];
|
|
254
265
|
others?: Record<string, string>;
|
|
255
266
|
notes?: Record<string, string>;
|
|
267
|
+
otherImages?: Record<string, string[]>;
|
|
268
|
+
noteImages?: Record<string, string[]>;
|
|
256
269
|
text?: string;
|
|
257
270
|
decision?: ReplyDecision;
|
|
258
271
|
}
|
|
@@ -523,13 +536,15 @@ export interface PostQuery {
|
|
|
523
536
|
sessionId?: string;
|
|
524
537
|
}
|
|
525
538
|
export interface Store {
|
|
526
|
-
listSessions(): Promise<Session[]>;
|
|
539
|
+
listSessions(project?: string): Promise<Session[]>;
|
|
527
540
|
getSession(id: string): Promise<Session | null>;
|
|
528
541
|
getSessionByKey(key: string): Promise<Session | null>;
|
|
529
542
|
createSession(input: CreateSessionInput): Promise<Session>;
|
|
530
543
|
renameSession(id: string, title: string): Promise<Session | null>;
|
|
531
544
|
removeSession(id: string): Promise<boolean>;
|
|
532
545
|
markAgentSeen(sessionId: string, seq: number): Promise<void>;
|
|
546
|
+
touchAgent(sessionId: string): Promise<void>;
|
|
547
|
+
handOff(sessionId: string, seen: Pick<Session, "agentSeq" | "agentActiveAt">, toSeq: number): Promise<boolean>;
|
|
533
548
|
getSetting(key: string): Promise<string | null>;
|
|
534
549
|
setSetting(key: string, value: string): Promise<void>;
|
|
535
550
|
listProjects(): Promise<ProjectSummary[]>;
|
|
@@ -572,6 +587,8 @@ export declare const HISTORY_LIMIT = 20;
|
|
|
572
587
|
export declare function reservedAgent(name: string): string;
|
|
573
588
|
export declare function stripNul<T extends string | null | undefined>(s: T): T;
|
|
574
589
|
export declare const MAX_ASSET_BYTES: number;
|
|
590
|
+
export declare const ATTACHABLE_IMAGE_TYPES: ReadonlySet<string>;
|
|
591
|
+
export declare const MAX_ASK_IMAGES = 6;
|
|
575
592
|
export declare const MAX_WORKSPACE_ASSET_BYTES: number;
|
|
576
593
|
export declare const newId: () => string;
|
|
577
594
|
export declare function hashAssetId(data: Uint8Array): Promise<string>;
|
|
@@ -583,13 +600,19 @@ export declare function projectFromCwd(cwd: string | null | undefined): string |
|
|
|
583
600
|
export declare const htmlSurface: (html: string, kits?: unknown) => HtmlSurface;
|
|
584
601
|
export declare const askAnswered: (a: Pick<Ask, "answer" | "other" | "note">) => boolean;
|
|
585
602
|
export declare const openAsks: (mock: Pick<Mock, "asks">) => Ask[];
|
|
586
|
-
export declare function
|
|
603
|
+
export declare function builtinAskIds(mock: Pick<Mock, "asks" | "states">, posts: Pick<Post, "mock" | "state" | "variant" | "status">[], mockId: string): string[];
|
|
604
|
+
export declare const builtinAskCount: (mock: Pick<Mock, "asks" | "states">, posts: Pick<Post, "mock" | "state" | "variant" | "status">[], mockId: string) => number;
|
|
587
605
|
export declare const openCount: (mock: Pick<Mock, "id" | "asks" | "states">, posts: Pick<Post, "mock" | "state" | "variant" | "status">[]) => number;
|
|
606
|
+
export declare function draftedCount(mock: Pick<Mock, "id" | "asks" | "states" | "draft">, posts: Pick<Post, "mock" | "state" | "variant" | "status">[]): number | null;
|
|
588
607
|
export declare const BUILTIN_ASK_ID = "variant";
|
|
589
608
|
export declare function builtinAsk(mock: Pick<Mock, "states">, posts: Pick<Post, "state" | "variant">[], id: string): Ask | undefined;
|
|
590
609
|
export declare function summarizeProjects(mocks: Mock[], sessions: Session[], posts?: Post[]): ProjectSummary[];
|
|
591
610
|
export declare function surfacesByteLength(surfaces: Surface[]): number;
|
|
592
611
|
export declare function collectAssetIds(surfaces: Surface[], out: Set<string>): void;
|
|
612
|
+
export declare function collectAttachedIds(holder: {
|
|
613
|
+
otherImages?: unknown;
|
|
614
|
+
noteImages?: unknown;
|
|
615
|
+
} | null | undefined, out: Set<string>): void;
|
|
593
616
|
export interface EvictionCandidate {
|
|
594
617
|
id: string;
|
|
595
618
|
byteLength: number;
|
package/dist/server/types.js
CHANGED
|
@@ -54,6 +54,10 @@ export function isSandboxedSurfaceKind(kind) {
|
|
|
54
54
|
// The viewer's write-in option on every ask. Reserved, so a delivered
|
|
55
55
|
// `{id: "other", other: true}` choice never shadows an option the agent declared.
|
|
56
56
|
export const OTHER_ID = "other";
|
|
57
|
+
// A one-tap verdict on a part (or, with part null, on the variant itself),
|
|
58
|
+
// for a reviewer on a phone who would rather not type: like, dislike, or
|
|
59
|
+
// "more like this". Rides as a part comment, so it travels in the one reply.
|
|
60
|
+
export const REACTIONS = ["up", "down", "more"];
|
|
57
61
|
export const HISTORY_LIMIT = 20;
|
|
58
62
|
// "user" is the reserved trust label for genuine human comments. A session
|
|
59
63
|
// agent with that name could otherwise have its programmatic comments delivered
|
|
@@ -73,6 +77,15 @@ export function stripNul(s) {
|
|
|
73
77
|
// budget the store evicts down to. One Durable Object holds the whole workspace, so
|
|
74
78
|
// the budget sits well under its ~10 GB SQLite ceiling.
|
|
75
79
|
export const MAX_ASSET_BYTES = 5 * 1024 * 1024;
|
|
80
|
+
// What the user may attach to a write-in or a note: raster formats a model can
|
|
81
|
+
// read. SVG is active content, so it is never attachable.
|
|
82
|
+
export const ATTACHABLE_IMAGE_TYPES = new Set([
|
|
83
|
+
"image/png",
|
|
84
|
+
"image/jpeg",
|
|
85
|
+
"image/webp",
|
|
86
|
+
"image/gif",
|
|
87
|
+
]);
|
|
88
|
+
export const MAX_ASK_IMAGES = 6;
|
|
76
89
|
export const MAX_WORKSPACE_ASSET_BYTES = 2 * 1024 * 1024 * 1024;
|
|
77
90
|
// Short, unguessable id: 8 random bytes (64 bits) as 11 url-safe base64 chars.
|
|
78
91
|
// These double as bearer capabilities: in publicRead mode `/s/:id` and
|
|
@@ -145,7 +158,7 @@ export const openAsks = (mock) => mock.asks.filter((a) => !askAnswered(a));
|
|
|
145
158
|
// rule as viewer/src/logic.ts builtinAsks, so Home, `read` and `pending` count
|
|
146
159
|
// the question the user actually sees. Answering it archives the losers, which
|
|
147
160
|
// is what makes it stop counting.
|
|
148
|
-
export function
|
|
161
|
+
export function builtinAskIds(mock, posts, mockId) {
|
|
149
162
|
const live = (state) => posts
|
|
150
163
|
.filter((p) => p.mock === mockId && p.state === state && p.status !== "archived")
|
|
151
164
|
.map((p) => p.variant);
|
|
@@ -159,14 +172,30 @@ export function builtinAskCount(mock, posts, mockId) {
|
|
|
159
172
|
const states = mock.states.length ? mock.states : [null];
|
|
160
173
|
const needing = states.filter(needs);
|
|
161
174
|
if (!needing.length)
|
|
162
|
-
return
|
|
175
|
+
return [];
|
|
163
176
|
const key = (state) => [...live(state)].sort().join("\n");
|
|
164
177
|
const first = key(needing[0]);
|
|
165
178
|
return needing.length === states.length && states.every((st) => key(st) === first)
|
|
166
|
-
?
|
|
167
|
-
: needing.
|
|
179
|
+
? [BUILTIN_ASK_ID]
|
|
180
|
+
: needing.map((st) => (st === null ? BUILTIN_ASK_ID : `${BUILTIN_ASK_ID}:${st}`));
|
|
168
181
|
}
|
|
182
|
+
export const builtinAskCount = (mock, posts, mockId) => builtinAskIds(mock, posts, mockId).length;
|
|
169
183
|
export const openCount = (mock, posts) => openAsks(mock).length + builtinAskCount(mock, posts, mock.id);
|
|
184
|
+
// How many of the open asks the user's unsent draft already speaks for (a
|
|
185
|
+
// pick, a write-in or a note), so Home can show a started mock as "draft 2/4";
|
|
186
|
+
// null when there is no draft. A count only: the draft's content stays private.
|
|
187
|
+
export function draftedCount(mock, posts) {
|
|
188
|
+
const d = mock.draft;
|
|
189
|
+
if (!d)
|
|
190
|
+
return null;
|
|
191
|
+
const open = [...openAsks(mock).map((a) => a.id), ...builtinAskIds(mock, posts, mock.id)];
|
|
192
|
+
const said = (id) => {
|
|
193
|
+
const a = d.answers[id];
|
|
194
|
+
const picked = Array.isArray(a) ? a.length > 0 : a !== undefined;
|
|
195
|
+
return picked || d.others?.[id] !== undefined || d.notes?.[id] !== undefined;
|
|
196
|
+
};
|
|
197
|
+
return open.filter(said).length;
|
|
198
|
+
}
|
|
170
199
|
// The viewer's built-in "Which one?" for variants no agent ask binds: `variant`
|
|
171
200
|
// mock-wide, `variant:<state>` per state. Never stored; synthesized from the
|
|
172
201
|
// variants so a reply answering it validates, flips and reads like any ask. An
|
|
@@ -275,6 +304,20 @@ export function collectAssetIds(surfaces, out) {
|
|
|
275
304
|
out.add(p.assetId);
|
|
276
305
|
}
|
|
277
306
|
}
|
|
307
|
+
// The images a draft, a reply or a sent ask carries (all keep the user's
|
|
308
|
+
// attachments in otherImages / noteImages).
|
|
309
|
+
export function collectAttachedIds(holder, out) {
|
|
310
|
+
for (const map of [holder?.otherImages, holder?.noteImages]) {
|
|
311
|
+
if (!map || typeof map !== "object")
|
|
312
|
+
continue;
|
|
313
|
+
for (const ids of Array.isArray(map) ? [map] : Object.values(map)) {
|
|
314
|
+
if (Array.isArray(ids))
|
|
315
|
+
for (const id of ids)
|
|
316
|
+
if (typeof id === "string")
|
|
317
|
+
out.add(id);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
278
321
|
// Pick the assets to evict so `incomingBytes` fits under `budget`. Oldest
|
|
279
322
|
// (lastAccessedAt) first, but unreferenced assets go before referenced ones —
|
|
280
323
|
// a live embed is only evicted as a last resort, once unreferenced candidates
|
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`:
|
|
@@ -60,10 +63,14 @@ An option binds to one of:
|
|
|
60
63
|
- a knob `set`: picking it applies those values live.
|
|
61
64
|
- nothing: a plain pill.
|
|
62
65
|
|
|
66
|
+
Options that change how something looks carry a `variant` or a knob `set`,
|
|
67
|
+
so the user sees them live. Plain pills only for what can't be seen.
|
|
68
|
+
|
|
63
69
|
The viewer shows a picture of each option it can render. `multi: true` allows
|
|
64
70
|
several answers. Option ids default to the slugged label. Reusing an ask `id`
|
|
65
71
|
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`
|
|
72
|
+
ask (both take attached images), so don't add your own; the option id `other`
|
|
73
|
+
is reserved.
|
|
67
74
|
|
|
68
75
|
## The Look ask
|
|
69
76
|
|
|
@@ -72,16 +79,21 @@ ask it first. Questions appear in the order you ask them. This one is titled
|
|
|
72
79
|
"Look" and unlocks Mix, which lets the user borrow a part from another look.
|
|
73
80
|
Variants without a Look ask get a plain switcher in the frame header.
|
|
74
81
|
|
|
82
|
+
A variant decides everything it differs on; don't re-ask it. Ask other axes as
|
|
83
|
+
knob `set`s.
|
|
84
|
+
|
|
75
85
|
## When you forget to ask
|
|
76
86
|
|
|
77
87
|
A publish that leaves a state with two or more open variants and no ask
|
|
78
88
|
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
|
-
|
|
89
|
+
variant names line up across states, else for that state), or your existing
|
|
90
|
+
ask plus the new variants. Send it as is with `ask`, or write your own question
|
|
91
|
+
with options bound to the variants. The CLI prints the `mockpit ask` command
|
|
92
|
+
for you. Sending it stops the nudge and keeps the answered Look ask on the
|
|
93
|
+
mock; the built-in one is gone once answered.
|
|
94
|
+
|
|
95
|
+
Until an ask binds them, the viewer shows a built-in Look ask with a picture
|
|
96
|
+
of each variant, so the user can still choose in one Send. Its answer
|
|
85
97
|
reads like any other ask under the reserved id `variant` (per state:
|
|
86
98
|
`variant:<state>`), and accepts and archives per state. Your own ask over
|
|
87
99
|
those variants replaces it.
|
package/guide/topics/feedback.md
CHANGED
|
@@ -24,7 +24,8 @@ It returns at once:
|
|
|
24
24
|
"ask": "trim",
|
|
25
25
|
"text": "Trim above or below?",
|
|
26
26
|
"chosen": [{ "id": "below", "label": "Below", "set": { "trim.position": "bottom" } }],
|
|
27
|
-
"note": "below on mobile only"
|
|
27
|
+
"note": "below on mobile only",
|
|
28
|
+
"noteImages": [{ "id": "k3Jf9xQ2aLw", "url": "http://localhost:8228/a/k3Jf9xQ2aLw" }]
|
|
28
29
|
},
|
|
29
30
|
{
|
|
30
31
|
"ask": "lang",
|
|
@@ -35,6 +36,7 @@ It returns at once:
|
|
|
35
36
|
"mix": { "body": "quiet" },
|
|
36
37
|
"tuned": { "body.size": 19 },
|
|
37
38
|
"comments": [{ "part": "title", "state": "Writing", "text": "bigger" }],
|
|
39
|
+
"reactions": ["👍 part \"title\" (variant dark, state Writing)"],
|
|
38
40
|
"text": "close, go dark"
|
|
39
41
|
},
|
|
40
42
|
"comments": [],
|
|
@@ -64,18 +66,26 @@ taking any feedback.
|
|
|
64
66
|
1. `answers` and `asks` decide structure. A variant-bound answer has already
|
|
65
67
|
accepted that variant and archived its siblings (`accepted`, `archived`).
|
|
66
68
|
Stop iterating on the archived ones. The ask `variant` (per state
|
|
67
|
-
`variant:<state>`) is the viewer's built-in
|
|
69
|
+
`variant:<state>`) is the viewer's built-in Look ask, read like any
|
|
68
70
|
other. A `chosen` entry with `other: true` is
|
|
69
71
|
the user's own answer (its `label`), not one of your options; it flips no
|
|
70
72
|
variant and sets no knob. `note` qualifies the answer and may come with
|
|
71
|
-
nothing chosen.
|
|
73
|
+
nothing chosen. The user can attach images (screenshots, references) to
|
|
74
|
+
either: `images` on the `other` choice, `noteImages` on the ask, each
|
|
75
|
+
`{id, url}`; the text may then be `""`. Look at every one. Over MCP they
|
|
76
|
+
also arrive as image content in the same result; elsewhere fetch the `url`
|
|
77
|
+
(with your token, if the server has one) and open the file.
|
|
72
78
|
2. `tuned` holds knob values. Write them back into the source as the new
|
|
73
79
|
defaults. tunekit's skill (`skills/tunekit` in the tunekit package) covers
|
|
74
80
|
applying copied values.
|
|
75
81
|
3. `mix` maps a part to the variant it should come from. Compose that part from
|
|
76
|
-
the other look into the chosen one.
|
|
82
|
+
the other look into the chosen one. If `mix` conflicts with an answer, the
|
|
83
|
+
answer wins.
|
|
77
84
|
4. `comments` are anchored on a part in a state (`part: null` means no part).
|
|
78
|
-
Address each one.
|
|
85
|
+
Address each one. `reactions` are one-tap verdicts, one line each:
|
|
86
|
+
👍 keep it, 👎 rework it, "more like this" go further that way. A reaction
|
|
87
|
+
without a part is about the whole variant; one may end with the user's
|
|
88
|
+
note in quotes. Reactions never also appear in `comments`.
|
|
79
89
|
5. `decision` (`{kind: accept|revise|drop, state, variant}`) comes instead of
|
|
80
90
|
answers when the mock has no asks and the user pressed Accept, Revise or
|
|
81
91
|
Drop.
|
package/guide/topics/knobs.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
A knob is a value the user tunes live on one render. Use one when one render
|
|
4
4
|
plus a control shows the choice. A choice is several variants plus one ask
|
|
5
|
-
that binds them; for that, publish variants and ask instead (topic `asks`).
|
|
6
|
-
|
|
7
|
-
one
|
|
5
|
+
that binds them; for that, publish variants and ask instead (topic `asks`).
|
|
6
|
+
A 2–3 option knob is a choice: ask with options that `set` it. Publishing
|
|
7
|
+
one returns that nudge.
|
|
8
8
|
|
|
9
9
|
## Declaring
|
|
10
10
|
|
package/guide/topics/surfaces.md
CHANGED
|
@@ -90,11 +90,11 @@ bytes dedupe. An asset lives as long as something references it. The limit is
|
|
|
90
90
|
|
|
91
91
|
Each thing you add lights up its part of the viewer.
|
|
92
92
|
|
|
93
|
-
| you publish | the user gets
|
|
94
|
-
| ---------------------- |
|
|
95
|
-
| a plain mock, any kind | the stage and Thread, with Accept / Revise / Drop
|
|
96
|
-
| several variants | a switcher, plus a built-in
|
|
97
|
-
| asks | Questions: picture options on the stage, then Send
|
|
98
|
-
| `data-part` | Tune's component list and comments anchored on parts
|
|
99
|
-
| `knobs` | Tune's controls, live on the stage
|
|
100
|
-
| several states | the state strip under the stage
|
|
93
|
+
| you publish | the user gets |
|
|
94
|
+
| ---------------------- | ---------------------------------------------------- |
|
|
95
|
+
| a plain mock, any kind | the stage and Thread, with Accept / Revise / Drop |
|
|
96
|
+
| several variants | a switcher, plus a built-in Look ask until you ask |
|
|
97
|
+
| asks | Questions: picture options on the stage, then Send |
|
|
98
|
+
| `data-part` | Tune's component list and comments anchored on parts |
|
|
99
|
+
| `knobs` | Tune's controls, live on the stage |
|
|
100
|
+
| several states | the state strip under the stage |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mockpit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A design-decision loop for terminal coding agents — agents publish mocks with states and variants, ask questions and expose knobs; you answer, tune and comment, then send one reply.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-tools",
|
package/skills/mockpit/SKILL.md
CHANGED
|
@@ -19,8 +19,10 @@ The loop never waits for the user:
|
|
|
19
19
|
2. `mockpit publish --mock <slug> --state "<user's words>" --variant <v> --html f.html`,
|
|
20
20
|
once per variant; mark parts with `data-part`.
|
|
21
21
|
3. A choice is several variants plus one ask that binds them: `mockpit ask`
|
|
22
|
-
with options bound to variants.
|
|
23
|
-
|
|
22
|
+
with options bound to variants. A variant decides everything it differs on;
|
|
23
|
+
don't re-ask it. Visual options carry a `variant` or knob `set`. One render
|
|
24
|
+
plus a control: `--knobs`. When a publish result nudges, send its
|
|
25
|
+
`suggestedAsk`.
|
|
24
26
|
4. Tell the user in one line where to look, then end your turn. The question
|
|
25
27
|
itself lives in the mock (`ask`), never in chat. The user answers in the
|
|
26
28
|
browser at their own pace. Never poll.
|