mockpit 1.0.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.
Files changed (45) hide show
  1. package/README.md +12 -3
  2. package/bin/mockpit.js +47 -21
  3. package/dist/mcp/server.js +43 -4
  4. package/dist/server/apiViews.d.ts +11 -8
  5. package/dist/server/apiViews.js +21 -16
  6. package/dist/server/app.d.ts +3 -1
  7. package/dist/server/app.js +374 -67
  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/designGuide.js +15 -13
  17. package/dist/server/eventLog.d.ts +27 -0
  18. package/dist/server/eventLog.js +187 -0
  19. package/dist/server/feedbackBatch.d.ts +16 -4
  20. package/dist/server/feedbackBatch.js +59 -9
  21. package/dist/server/handoff.d.ts +6 -0
  22. package/dist/server/handoff.js +38 -0
  23. package/dist/server/index.js +12 -0
  24. package/dist/server/knobs.js +2 -3
  25. package/dist/server/logFile.d.ts +2 -0
  26. package/dist/server/logFile.js +27 -0
  27. package/dist/server/mcpHttp.d.ts +2 -2
  28. package/dist/server/mcpHttp.js +11 -5
  29. package/dist/server/mcpSpec.d.ts +8 -0
  30. package/dist/server/mcpSpec.js +26 -1
  31. package/dist/server/run.js +2 -2
  32. package/dist/server/runApi.d.ts +1 -1
  33. package/dist/server/runApi.js +1 -1
  34. package/dist/server/sqlStore.d.ts +3 -1
  35. package/dist/server/sqlStore.js +40 -8
  36. package/dist/server/types.d.ts +25 -2
  37. package/dist/server/types.js +47 -4
  38. package/extensions/mockpit.js +24 -9
  39. package/guide/topics/asks.md +19 -7
  40. package/guide/topics/feedback.md +17 -7
  41. package/guide/topics/knobs.md +3 -3
  42. package/guide/topics/surfaces.md +8 -8
  43. package/package.json +2 -2
  44. package/skills/mockpit/SKILL.md +4 -2
  45. package/viewer/dist/index.html +9 -9
@@ -0,0 +1,22 @@
1
+ import { type Mode } from "./themes.ts";
2
+ export declare const ICON_SVG: string;
3
+ export declare const ICON_SIZES: readonly [180, 192, 512];
4
+ export type IconSize = (typeof ICON_SIZES)[number];
5
+ export declare const themeColor: (mode: Mode) => string;
6
+ export declare function webManifest(mode: Mode, startUrl?: string): {
7
+ name: string;
8
+ short_name: string;
9
+ description: string;
10
+ id: string;
11
+ start_url: string;
12
+ scope: string;
13
+ display: string;
14
+ background_color: string;
15
+ theme_color: string;
16
+ icons: {
17
+ src: string;
18
+ sizes: string;
19
+ type: string;
20
+ purpose: string;
21
+ }[];
22
+ };
@@ -0,0 +1,43 @@
1
+ // The installable app: its web app manifest and its icon. Runtime-agnostic (no
2
+ // `node:` imports) so the Worker serves the same bytes. The PNG renders of the
3
+ // icon are generated from ICON_SVG by scripts/app-icons.ts into appIconPng.ts,
4
+ // because iOS takes no SVG for a home-screen icon.
5
+ import { DIALKIT } from "./themes.js";
6
+ const BG = DIALKIT.dark.bg;
7
+ const ACCENT = DIALKIT.dark.info.text;
8
+ // A stage frame with a comment pin on its corner. Full-bleed background and the
9
+ // mark inside the central 80% circle, so the same art works as `maskable`.
10
+ export const ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
11
+ <rect width="512" height="512" fill="${BG}"/>
12
+ <rect x="126" y="142" width="260" height="196" rx="34" fill="none" stroke="${ACCENT}" stroke-width="28"/>
13
+ <path d="M170 206h120M170 262h76" stroke="${ACCENT}" stroke-width="24" stroke-linecap="round" opacity="0.55"/>
14
+ <circle cx="366" cy="338" r="58" fill="${ACCENT}" stroke="${BG}" stroke-width="22"/>
15
+ </svg>`;
16
+ export const ICON_SIZES = [180, 192, 512];
17
+ export const themeColor = (mode) => DIALKIT[mode].bg;
18
+ // Relative URLs resolve against the manifest's own URL, so the app works under
19
+ // any base path. `startUrl` carries `?key=` on a tokened workspace: a
20
+ // home-screen app may not share Safari's cookie jar, and the key is how the
21
+ // viewer gets its cookie back on first launch.
22
+ export function webManifest(mode, startUrl = "./") {
23
+ return {
24
+ name: "mockpit",
25
+ short_name: "mockpit",
26
+ description: "Answer your agent's design questions.",
27
+ id: "./",
28
+ start_url: startUrl,
29
+ scope: "./",
30
+ display: "standalone",
31
+ background_color: themeColor(mode),
32
+ theme_color: themeColor(mode),
33
+ icons: [
34
+ { src: "icon.svg", sizes: "any", type: "image/svg+xml", purpose: "any" },
35
+ ...ICON_SIZES.filter((s) => s !== 180).map((s) => ({
36
+ src: `icon-${s}.png`,
37
+ sizes: `${s}x${s}`,
38
+ type: "image/png",
39
+ purpose: "any maskable",
40
+ })),
41
+ ],
42
+ };
43
+ }
@@ -0,0 +1 @@
1
+ export declare const ICON_PNG_BASE64: Record<number, string>;
@@ -0,0 +1,6 @@
1
+ // Generated by scripts/app-icons.ts from ICON_SVG; do not edit.
2
+ export const ICON_PNG_BASE64 = {
3
+ 180: "iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAIAAACyr5FlAAAKnklEQVR4nOzdfXAU9R3H8W9yyT0k95BcEhJCHgiJEXkIjTRqRAYUpDyNQBGKteq0xZbOtDqdtuPUGTt22k7/czr9wz+EtsoMijwjT/IgIGIxahRJCCQBQsjzcy6Xu9xDHlzMwDDmvktCbve3e/m8Jn/dLoTk3uz+9rd7uzEOh4MAQokmAAbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiAFYMaVV+zvL7p62clDTDbEq0mJ1mk40ihc/v7vN1+vxdLe0XK68drL5+hDQpSmtPTbDFT55f9Mqs+9cbY+NoYggEveWVO858/s9ebwtpiYbiMBntCx95rajgRZqoSs6/KSXiD7hJG7QSh92a8eyqfUmJeTSxtXVceveDtW5PI2mAwWw2k2hpKXNeWPuhw5ZBE158XMqs/Kdr6j/2eFtJNPFHK4mOac+tORBnSSL4jjU+9fkfH3Im5JJoguMwmxzS3kQabRDcQfqF/PSpPcZYwQdoIuOIjo7ZsHJngj2LYIQEe/b6Fe9GRYl8g0SOOYoKfl048wUChtSHx9vW1PoVCSJyEqy48OVRrtnT29jlqhkaGiL9i4qKSnTk2K3po1l53g//UFq+hQQRFscDuats1jT5dXz+nsOnf3+19rg/0EORRRps5WY9uWzhG2aT3HjLbp0szRRX1RwmEYTt0qbnrZJfobntwlvvPVpRvTvyyqCb3bsuVu+SfsDWjovyaz6Qt5oEERNHdHRsfs4ymRXcnuZ3di/t6a2niCb9gG/vWur2NMmsk5+zVPp1kQhi4piW+URsjEVmhX3HNgb7vTQBBILufcd+JbOCdFibk7mQRBATR2rKbJmlja1f1zacpQmjtuGTxpbzMiukJReQCGLiMMvOetU1nqMJpr65RGap0ShmNkxMHPJTonVNn9EEI/8jW0xiTo6KOZQ1GIwyS6VZDZpgXO46maUGg4lEwGWCwEIcwEIcwEIcwEIcwNLuRxNGMsbastIftcanGgRNJw8LBD2u3vobE2CaTjdx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",
4
+ 192: "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",
5
+ 512: "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",
6
+ };
@@ -0,0 +1,3 @@
1
+ import type { Ask } from "./types.ts";
2
+ export declare const looksVisual: (label: string) => boolean;
3
+ export declare function askNudges(added: Ask[], all: Ask[]): string[];
@@ -0,0 +1,40 @@
1
+ // Ask discipline on `ask`: the two habits that make the user type or re-answer
2
+ // what they could have seen. Pure, so the rules are tested on their own.
3
+ // Words that name how something looks, matched on option labels only: a
4
+ // question's text names the topic, not the options. Words with a common
5
+ // non-visual sense ("size", "background", "compact", "white") are left out, so
6
+ // a pill about file size or a background job is not flagged.
7
+ const VISUAL = /\b(colou?rs?|borders?|radius|rounded|corners?|spacing|padding|fonts?|typeface|serif|shadows?|tint|hue|opacity|red|orange|yellow|green|olive|teal|blue|navy|purple|violet|pink|gr[ae]y|\d+\s?(px|rem|em|pt))\b|#[0-9a-f]{3,8}\b/i;
8
+ const plain = (o) => !o.variant && !(o.set && Object.keys(o.set).length);
9
+ export const looksVisual = (label) => VISUAL.test(label);
10
+ // A Look-style ask: one whose options choose between variants.
11
+ const bindsVariants = (a) => a.options.some((o) => o.variant);
12
+ const quoted = (opts) => opts.map((o) => `"${o.label}"`).join(", ");
13
+ export function askNudges(added, all) {
14
+ const out = [];
15
+ for (const ask of added) {
16
+ const pills = ask.options.filter(plain);
17
+ if (!pills.length)
18
+ continue;
19
+ const visual = pills.filter((o) => looksVisual(o.label));
20
+ const look = ask.scope === "part" || bindsVariants(ask)
21
+ ? undefined
22
+ : all.find((a) => a.id !== ask.id &&
23
+ a.scope !== "part" &&
24
+ bindsVariants(a) &&
25
+ (a.scope === "mock" || ask.scope === "mock" || a.state === ask.state));
26
+ // Only a pill that names a look or reads visual overlaps the variants; a
27
+ // plain question on another axis ("Drawer or Margin?") stands beside them.
28
+ const names = new Set(look?.options.flatMap((o) => [o.label, o.variant ?? ""].map((n) => n.toLowerCase())));
29
+ const reask = look
30
+ ? pills.filter((o) => names.has(o.label.toLowerCase()) || looksVisual(o.label))
31
+ : [];
32
+ if (look && reask.length) {
33
+ out.push(`ask "${ask.id}": ${quoted(reask)} may re-ask what "${look.id}" decides. Drop it or use knob sets.`);
34
+ }
35
+ else if (visual.length) {
36
+ out.push(`ask "${ask.id}": ${quoted(visual)} look visual. Give each a knob set or variant.`);
37
+ }
38
+ }
39
+ return out;
40
+ }
@@ -1 +1,2 @@
1
1
  export declare function decodeBase64(b64: string): Uint8Array;
2
+ export declare function encodeBase64(bytes: Uint8Array): string;
@@ -14,3 +14,11 @@ export function decodeBase64(b64) {
14
14
  bytes[i] = bin.charCodeAt(i);
15
15
  return bytes;
16
16
  }
17
+ // bytes -> base64, in chunks so a multi-MB image never spreads into one call.
18
+ export function encodeBase64(bytes) {
19
+ let bin = "";
20
+ for (let i = 0; i < bytes.length; i += 0x8000) {
21
+ bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
22
+ }
23
+ return btoa(bin);
24
+ }
@@ -29,7 +29,7 @@ export function iconsLine(design) {
29
29
  }
30
30
  const TOPIC_SUMMARY = {
31
31
  knobs: "every knob shape, how values reach html",
32
- asks: "the ask JSON, scopes, Look, Which one?, multi",
32
+ asks: "the ask JSON, scopes, the Look ask, multi",
33
33
  surfaces: "markdown, diff, mermaid, code, terminal, json, image; uploads",
34
34
  html: "the contract, finish rules, tokens, kits, Tailwind, icons",
35
35
  feedback: "the feedback JSON, pending, delivery, export",
@@ -227,7 +227,7 @@ project › mock › state › variant › version. A project is the repo; a moc
227
227
  page or component, by slug; a state is one moment of it in the user's words
228
228
  ("Writing"), omitted for a single-state mock; a variant is a parallel design of
229
229
  a state; versions are its history. A part is an element marked \`data-part\`.
230
- Asks are questions, knobs are values; the user presses Send once, you get one reply.
230
+ The user answers asks, tunes knobs, presses Send once; you get one reply.
231
231
 
232
232
  ## The loop
233
233
 
@@ -245,7 +245,7 @@ mockpit export --mock writer # the accepted html per state
245
245
  \`\`\`
246
246
 
247
247
  Server: \`$MOCKPIT_URL\` (default http://localhost:8228; \`mockpit serve\`).
248
- MCP tools have the verb names (\`publish\`, \`feedback\`, …).
248
+ MCP tools share the verb names.
249
249
 
250
250
  ## Parts
251
251
 
@@ -256,8 +256,10 @@ declare geometry; the viewer measures it.
256
256
 
257
257
  ## Ask or knob
258
258
 
259
- A choice is several variants plus one ask that binds them. Look question
260
- first. One render plus a control: declare a knob with \`--knobs '{...}'\`.
259
+ A choice is several variants plus one ask that binds them. Ask it first. A
260
+ variant decides everything it differs on; don't re-ask it. Visual options
261
+ carry a \`variant\` or knob \`set\`. One render plus a control: a knob,
262
+ \`--knobs '{...}'\`.
261
263
 
262
264
  | knob | control |
263
265
  | --- | --- |
@@ -267,19 +269,19 @@ first. One render plus a control: declare a knob with \`--knobs '{...}'\`.
267
269
  | \`"trim.position": {"type": "select", "options": ["top", "bottom"]}\` | select |
268
270
 
269
271
  Values reach the html unitless: \`calc(var(--k-body-size, 17) * 1px)\`, and as
270
- \`data-k-trim-position\` on \`<html>\`. Topics \`knobs\` and \`asks\` have the rest.
272
+ \`data-k-trim-position\` on \`<html>\`.
271
273
 
272
274
  ## Feedback
273
275
 
274
276
  \`feedback\` returns \`{feedback, pending}\`. A batch is \`{mock, reply: {asks, tuned,
275
- mix, comments, decision, text}, accepted, archived}\`. In order: answers decide
276
- structure (a variant answer already accepted it and archived its siblings);
277
- tuned values become new defaults; mix names a part to take from another variant;
278
- address each comment on its part; decision (accept, revise, drop) stands in for
279
- answers when there were no asks. Each Send arrives exactly once: in a write's
277
+ mix, comments, reactions, decision, text}, accepted, archived}\`. In order: answers
278
+ decide structure (a variant answer already accepted it); tuned values become
279
+ defaults; mix names a part to take from another variant (if it conflicts with an
280
+ answer, the answer wins); address each comment and reaction on its part; decision
281
+ (accept, revise, drop) replaces answers when there were no asks. Each Send arrives exactly once: in a write's
280
282
  \`feedback\`, \`mockpit feedback\` or a \`mockpit watch\` line.
281
- A \`pending\` draft means the user is still answering. Variants no ask binds
282
- earn a \`suggestedAsk\` on publish: send it with \`ask\`.
283
+ A \`pending\` draft means the user is still answering. Send a nudge's
284
+ \`suggestedAsk\` with \`ask\`.
283
285
 
284
286
  ## HTML
285
287
 
@@ -0,0 +1,27 @@
1
+ import type { MiddlewareHandler } from "hono";
2
+ export interface LogEntry {
3
+ t: string;
4
+ op: string;
5
+ client: string;
6
+ status: number;
7
+ ms: number;
8
+ session?: string;
9
+ project?: string;
10
+ mock?: string;
11
+ state?: string | null;
12
+ variant?: string;
13
+ version?: number;
14
+ delivered?: number[];
15
+ pending?: string[];
16
+ nudges?: number;
17
+ reply?: number;
18
+ viewing?: string;
19
+ key?: string;
20
+ error?: string;
21
+ }
22
+ export type LogSink = (entry: LogEntry) => void;
23
+ export declare function clientOf(header: string | undefined, viewer: boolean, mcp: boolean): string;
24
+ export declare function fromRequest(query: URLSearchParams, body: unknown, pathMock: string | undefined): Partial<LogEntry>;
25
+ export declare function fromResponse(body: unknown): Partial<LogEntry>;
26
+ export declare function writeLog(sink: LogSink | undefined, entry: LogEntry): void;
27
+ export declare function logMiddleware(sink: LogSink): MiddlewareHandler;
@@ -0,0 +1,187 @@
1
+ // The event log: one entry per call that moves the loop (agent writes and
2
+ // reads, viewer writes, the viewer's live feed), so a session can be diagnosed
3
+ // after the fact — which client sent what, which reply seqs it was handed, what
4
+ // the agent was told was pending. Shapes and ids only, never html or comment
5
+ // text: the store already holds the content under the same ids, and a log that
6
+ // copied it would be a second, unbounded store. `scripts/inspect.ts` reads it.
7
+ const MAX_ERROR = 300;
8
+ const CLIENT = /^[\w.@/-]{1,64}$/;
9
+ export function clientOf(header, viewer, mcp) {
10
+ if (header && CLIENT.test(header))
11
+ return header;
12
+ if (viewer)
13
+ return "viewer";
14
+ return mcp ? "mcp-http" : "http";
15
+ }
16
+ const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
17
+ const text = (v) => typeof v === "string" && v ? v.slice(0, 200) : undefined;
18
+ // What the request itself names, for calls whose response doesn't (errors,
19
+ // feedback reads).
20
+ export function fromRequest(query, body, pathMock) {
21
+ const b = isObj(body) ? body : {};
22
+ return prune({
23
+ session: text(query.get("session")) ?? text(b.session),
24
+ project: text(query.get("project")) ?? text(b.project),
25
+ mock: pathMock ?? text(b.mock),
26
+ variant: text(b.variant),
27
+ viewing: text(query.get("viewing")),
28
+ });
29
+ }
30
+ // What a flow's response body says about the call.
31
+ export function fromResponse(body) {
32
+ if (!isObj(body))
33
+ return {};
34
+ const mock = isObj(body.mock) ? body.mock : undefined;
35
+ const post = isObj(body.post) ? body.post : undefined;
36
+ // POST /api/sessions answers with the bare session (201 new, 200 reclaimed by key).
37
+ const session = typeof body.agentSeq === "number" ? body : undefined;
38
+ const delivered = deliveredSeqs(body.feedback);
39
+ const pending = pendingLines(body.pending);
40
+ return prune({
41
+ session: text(body.sessionId) ?? text(post?.sessionId) ?? text(session?.id),
42
+ key: text(session?.key),
43
+ project: text(mock?.project) ?? text(body.project) ?? text(session?.project),
44
+ mock: text(mock?.slug) ??
45
+ text(body.slug) ??
46
+ (typeof body.mock === "string" ? text(body.mock) : undefined),
47
+ state: post ? (post.state ?? null) : undefined,
48
+ variant: text(post?.variant),
49
+ version: typeof post?.version === "number" ? post.version : undefined,
50
+ delivered: delivered.length ? delivered : undefined,
51
+ pending: pending.length ? pending : undefined,
52
+ nudges: Array.isArray(body.nudges) && body.nudges.length ? body.nudges.length : undefined,
53
+ reply: isObj(body.reply) && typeof body.reply.seq === "number" ? body.reply.seq : undefined,
54
+ error: typeof body.error === "string" ? body.error.slice(0, MAX_ERROR) : undefined,
55
+ });
56
+ }
57
+ function deliveredSeqs(feedback) {
58
+ if (!Array.isArray(feedback))
59
+ return [];
60
+ const seqs = [];
61
+ for (const batch of feedback) {
62
+ if (!isObj(batch))
63
+ continue;
64
+ if (isObj(batch.reply) && typeof batch.reply.seq === "number")
65
+ seqs.push(batch.reply.seq);
66
+ for (const c of Array.isArray(batch.comments) ? batch.comments : []) {
67
+ if (isObj(c) && typeof c.seq === "number")
68
+ seqs.push(c.seq);
69
+ }
70
+ }
71
+ return seqs.sort((a, b) => a - b);
72
+ }
73
+ function pendingLines(pending) {
74
+ const rows = Array.isArray(pending) ? pending : isObj(pending) ? [pending] : [];
75
+ return rows.flatMap((p) => {
76
+ if (!isObj(p) || typeof p.mock !== "string")
77
+ return [];
78
+ if (isObj(p.draft))
79
+ return [`${p.mock} ${p.draft.answered}/${p.draft.of} answered`];
80
+ return p.viewerOpen ? [`${p.mock} open`] : [];
81
+ });
82
+ }
83
+ function prune(entry) {
84
+ for (const k of Object.keys(entry))
85
+ if (entry[k] === undefined)
86
+ delete entry[k];
87
+ return entry;
88
+ }
89
+ // A sink that throws must never fail the request it describes.
90
+ export function writeLog(sink, entry) {
91
+ if (!sink)
92
+ return;
93
+ try {
94
+ sink(entry);
95
+ }
96
+ catch {
97
+ // The log is diagnostics; losing an entry beats losing the call.
98
+ }
99
+ }
100
+ // The viewer's reads (it refetches on every feed event) would drown the agent's
101
+ // calls, so only its writes and its feed connection are logged.
102
+ function loggable(c, viewer) {
103
+ const path = c.req.path;
104
+ if (path === "/mcp")
105
+ return c.req.method === "POST";
106
+ if (!path.startsWith("/api/"))
107
+ return false;
108
+ return path === "/api/events" || c.req.method !== "GET" || !viewer;
109
+ }
110
+ const isJson = (type) => !!type?.includes("application/json");
111
+ export function logMiddleware(sink) {
112
+ return async (c, next) => {
113
+ const start = Date.now();
114
+ await next();
115
+ const viewer = c.req.header("sec-fetch-site") === "same-origin";
116
+ if (!loggable(c, viewer))
117
+ return;
118
+ try {
119
+ const url = new URL(c.req.url);
120
+ // Handlers already parsed the body; Hono hands back its cached copy.
121
+ const reqBody = isJson(c.req.header("content-type"))
122
+ ? await c.req.json().catch(() => undefined)
123
+ : undefined;
124
+ // Never clone a non-JSON body: a clone of the SSE stream is a tee branch
125
+ // nobody reads, which buffers every event for the connection's life.
126
+ const resBody = isJson(c.res.headers.get("content-type"))
127
+ ? await c.res
128
+ .clone()
129
+ .json()
130
+ .catch(() => undefined)
131
+ : undefined;
132
+ const mcp = c.req.path === "/mcp";
133
+ const entry = {
134
+ t: new Date().toISOString(),
135
+ op: mcp ? mcpOp(reqBody) : `${c.req.method} ${c.req.path}`,
136
+ client: clientOf(c.req.header("x-mockpit-client"), viewer, mcp),
137
+ status: c.res.status,
138
+ ms: Date.now() - start,
139
+ };
140
+ if (mcp) {
141
+ const call = isObj(reqBody) && isObj(reqBody.params) ? reqBody.params : {};
142
+ Object.assign(entry, fromRequest(url.searchParams, call.arguments, undefined));
143
+ Object.assign(entry, fromMcpResult(resBody));
144
+ }
145
+ else {
146
+ const pathMock = /^\/api\/mocks\/([^/]+)/.exec(c.req.path)?.[1];
147
+ Object.assign(entry, fromRequest(url.searchParams, reqBody, pathMock));
148
+ Object.assign(entry, fromResponse(resBody));
149
+ }
150
+ writeLog(sink, entry);
151
+ }
152
+ catch {
153
+ // Same as writeLog: diagnostics never fail the call.
154
+ }
155
+ };
156
+ }
157
+ function mcpOp(body) {
158
+ if (!isObj(body))
159
+ return "mcp";
160
+ if (body.method === "tools/call" && isObj(body.params)) {
161
+ return `mcp ${text(body.params.name) ?? "?"}`;
162
+ }
163
+ return `mcp ${text(body.method) ?? "?"}`;
164
+ }
165
+ // A tool result carries the flow's JSON body: structured where the tool
166
+ // declares an output schema, else as its text content.
167
+ function fromMcpResult(body) {
168
+ if (!isObj(body))
169
+ return {};
170
+ if (isObj(body.error))
171
+ return { error: String(body.error.message).slice(0, MAX_ERROR) };
172
+ const result = isObj(body.result) ? body.result : {};
173
+ if (isObj(result.structuredContent))
174
+ return fromResponse(result.structuredContent);
175
+ const first = Array.isArray(result.content) ? result.content[0] : undefined;
176
+ const raw = isObj(first) && typeof first.text === "string" ? first.text : undefined;
177
+ if (raw === undefined)
178
+ return {};
179
+ if (result.isError)
180
+ return { error: raw.slice(0, MAX_ERROR) };
181
+ try {
182
+ return fromResponse(JSON.parse(raw));
183
+ }
184
+ catch {
185
+ return {};
186
+ }
187
+ }
@@ -1,4 +1,4 @@
1
- import { type AskOption, type Comment, type CommentAnchor, type Mock, OTHER_ID, type Post, type Reply, type Store } from "./types.ts";
1
+ import { type AskOption, type Comment, type CommentAnchor, type Mock, OTHER_ID, type PartComment, type Post, type Reaction, type Reply, type Store } from "./types.ts";
2
2
  export interface VariantRef {
3
3
  state: string | null;
4
4
  variant: string;
@@ -13,21 +13,28 @@ export interface FeedbackComment {
13
13
  anchors?: Comment["anchors"];
14
14
  viewport?: number;
15
15
  }
16
+ export interface ImageRef {
17
+ id: string;
18
+ url: string;
19
+ }
16
20
  export interface OtherChoice {
17
21
  id: typeof OTHER_ID;
18
22
  label: string;
19
23
  other: true;
24
+ images?: ImageRef[];
20
25
  }
21
26
  export interface AnsweredAsk {
22
27
  ask: string;
23
28
  text: string;
24
29
  chosen: (AskOption | OtherChoice)[];
25
30
  note?: string;
31
+ noteImages?: ImageRef[];
26
32
  }
27
- export interface FeedbackReply extends Omit<Reply, "others" | "notes"> {
33
+ export interface FeedbackReply extends Omit<Reply, "others" | "notes" | "otherImages" | "noteImages"> {
28
34
  seq: number;
29
35
  at: string;
30
36
  asks: AnsweredAsk[];
37
+ reactions?: string[];
31
38
  }
32
39
  export interface FeedbackBatch {
33
40
  mockId: string | null;
@@ -38,6 +45,11 @@ export interface FeedbackBatch {
38
45
  comments: FeedbackComment[];
39
46
  accepted: VariantRef[];
40
47
  archived: VariantRef[];
48
+ handoff?: string;
41
49
  }
42
- export declare function groupFeedback(comments: Comment[], mocks: Map<string, Mock>, posts: Map<string, Post[]>): FeedbackBatch[];
43
- export declare function buildFeedbackBatches(store: Store, comments: Comment[]): Promise<FeedbackBatch[]>;
50
+ export declare function reactionLine(c: PartComment & {
51
+ reaction: Reaction;
52
+ }): string;
53
+ export declare function groupFeedback(comments: Comment[], mocks: Map<string, Mock>, posts: Map<string, Post[]>, base?: string): FeedbackBatch[];
54
+ export declare function buildFeedbackBatches(store: Store, comments: Comment[], base: string): Promise<FeedbackBatch[]>;
55
+ export declare function feedbackImages(batches: FeedbackBatch[]): ImageRef[];
@@ -2,11 +2,35 @@
2
2
  // The `feedback` verb, the watch long-poll and the write piggyback all return this,
3
3
  // so an agent reads one grouped object instead of re-deriving which mock a flat
4
4
  // comment list belongs to. Runtime-agnostic (no node imports).
5
- import { builtinAsk, OTHER_ID, } from "./types.js";
5
+ import { builtinAsk, DEFAULT_VARIANT, OTHER_ID, } from "./types.js";
6
6
  const ref = (p) => ({ state: p.state, variant: p.variant });
7
- function answered(reply, mock, posts) {
7
+ const REACTION_LEAD = {
8
+ up: "👍",
9
+ down: "👎",
10
+ more: "more like this:",
11
+ };
12
+ // `👍 part "hero" (variant B, state Mobile)`, `👎 variant B`, `more like this: the page`.
13
+ export function reactionLine(c) {
14
+ // A mock published without variant names has only "default": naming it is noise.
15
+ const variant = c.variant && c.variant !== DEFAULT_VARIANT ? c.variant : null;
16
+ const where = [
17
+ c.part !== null && variant ? `variant ${variant}` : "",
18
+ c.state ? `state ${c.state}` : "",
19
+ ].filter(Boolean);
20
+ const target = c.part !== null
21
+ ? `part ${JSON.stringify(c.part)}`
22
+ : variant
23
+ ? `variant ${variant}`
24
+ : "the page";
25
+ const tail = where.length ? ` (${where.join(", ")})` : "";
26
+ const note = c.text ? ` “${c.text}”` : "";
27
+ return `${REACTION_LEAD[c.reaction]} ${target}${tail}${note}`;
28
+ }
29
+ const isReaction = (c) => !!c.reaction;
30
+ function answered(reply, mock, posts, base) {
8
31
  const others = reply.others ?? {};
9
32
  const notes = reply.notes ?? {};
33
+ const refs = (ids) => (ids ?? []).map((id) => ({ id, url: `${base}/a/${encodeURIComponent(id)}` }));
10
34
  const askIds = new Set([
11
35
  ...Object.keys(reply.answers),
12
36
  ...Object.keys(others),
@@ -18,13 +42,21 @@ function answered(reply, mock, posts) {
18
42
  const answer = reply.answers[askId];
19
43
  const ids = answer === undefined ? [] : Array.isArray(answer) ? answer : [answer];
20
44
  const chosen = ids.map((id) => ask?.options.find((o) => o.id === id) ?? { id, label: id });
45
+ const otherImages = refs(reply.otherImages?.[askId]);
21
46
  if (others[askId] !== undefined)
22
- chosen.push({ id: OTHER_ID, label: others[askId], other: true });
47
+ chosen.push({
48
+ id: OTHER_ID,
49
+ label: others[askId],
50
+ other: true,
51
+ ...(otherImages.length ? { images: otherImages } : {}),
52
+ });
53
+ const noteImages = refs(reply.noteImages?.[askId]);
23
54
  out.push({
24
55
  ask: askId,
25
56
  text: ask?.text ?? "",
26
57
  chosen,
27
58
  ...(notes[askId] !== undefined ? { note: notes[askId] } : {}),
59
+ ...(noteImages.length ? { noteImages } : {}),
28
60
  });
29
61
  }
30
62
  return out;
@@ -33,8 +65,8 @@ function answered(reply, mock, posts) {
33
65
  // one. A mock gets a fresh batch whenever a second reply arrives for it, so a
34
66
  // batch carries at most one reply and nothing is merged away. A comment whose
35
67
  // mock is unknown still gets a batch — feedback is never dropped for want of
36
- // context.
37
- export function groupFeedback(comments, mocks, posts) {
68
+ // context. `base` (origin + base path) makes image URLs absolute.
69
+ export function groupFeedback(comments, mocks, posts, base = "") {
38
70
  const batches = [];
39
71
  const open = new Map();
40
72
  for (const c of comments) {
@@ -57,12 +89,15 @@ export function groupFeedback(comments, mocks, posts) {
57
89
  batches.push(batch);
58
90
  }
59
91
  if (c.kind === "reply" && c.payload) {
60
- const { others: _others, notes: _notes, ...payload } = c.payload;
92
+ const { others: _others, notes: _notes, otherImages: _otherImages, noteImages: _noteImages, ...payload } = c.payload;
93
+ const reactions = (payload.comments ?? []).filter(isReaction).map(reactionLine);
61
94
  batch.reply = {
62
95
  ...payload,
96
+ comments: (payload.comments ?? []).filter((x) => !x.reaction),
63
97
  seq: c.seq,
64
98
  at: c.createdAt,
65
- asks: answered(c.payload, mock, mockPosts),
99
+ asks: answered(c.payload, mock, mockPosts, base),
100
+ ...(reactions.length ? { reactions } : {}),
66
101
  };
67
102
  batch.accepted = mockPosts.filter((p) => p.status === "accepted").map(ref);
68
103
  batch.archived = mockPosts.filter((p) => p.status === "archived").map(ref);
@@ -82,7 +117,7 @@ export function groupFeedback(comments, mocks, posts) {
82
117
  }
83
118
  return batches;
84
119
  }
85
- export async function buildFeedbackBatches(store, comments) {
120
+ export async function buildFeedbackBatches(store, comments, base) {
86
121
  const mocks = new Map();
87
122
  const posts = new Map();
88
123
  for (const c of comments) {
@@ -94,5 +129,20 @@ export async function buildFeedbackBatches(store, comments) {
94
129
  mocks.set(c.mockId, mock);
95
130
  posts.set(c.mockId, await store.listPosts({ mockId: c.mockId }));
96
131
  }
97
- return groupFeedback(comments, mocks, posts);
132
+ return groupFeedback(comments, mocks, posts, base);
133
+ }
134
+ // Every image a list of batches carries, in order — what the MCP tiers attach
135
+ // as image content so the model sees what the user attached.
136
+ export function feedbackImages(batches) {
137
+ const out = [];
138
+ for (const b of batches) {
139
+ for (const a of b.reply?.asks ?? []) {
140
+ for (const c of a.chosen)
141
+ if ("other" in c && c.images)
142
+ out.push(...c.images);
143
+ if (a.noteImages)
144
+ out.push(...a.noteImages);
145
+ }
146
+ }
147
+ return out;
98
148
  }
@@ -0,0 +1,6 @@
1
+ import type { Comment, Session } from "./types.ts";
2
+ export declare const HANDOFF_GRACE_MS: number;
3
+ export declare const HANDOFF_NOTE = "Sent to an earlier agent session in this project that has gone quiet; it is yours now. Read the mock before acting.";
4
+ export declare const ownerActiveAt: (s: Session) => string;
5
+ export declare function supersedes(taker: Session, owner: Session, listening: boolean): boolean;
6
+ export declare function handoffReady(comment: Comment, owner: Session, taker: Session, now: number): boolean;