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
|
@@ -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,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
|
+
}
|
package/dist/server/base64.d.ts
CHANGED
package/dist/server/base64.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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.
|
|
260
|
-
|
|
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>\`.
|
|
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
|
|
276
|
-
structure (a variant answer already accepted it
|
|
277
|
-
|
|
278
|
-
address each comment on its part; decision
|
|
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.
|
|
282
|
-
|
|
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
|
|
|
@@ -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
|
|
43
|
-
|
|
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
|
-
|
|
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({
|
|
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;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Orphaned replies: a Send lands on the session that owns the mock, and when
|
|
2
|
+
// that agent restarts into a new session (a new conversation, /clear, a crash)
|
|
3
|
+
// nobody would ever read it. The rule below decides when a newer session may
|
|
4
|
+
// take such a reply instead. Runtime-agnostic (no node imports).
|
|
5
|
+
//
|
|
6
|
+
// The danger is the other way round: two agents can work in one repo at once,
|
|
7
|
+
// and an agent waiting on the user looks exactly like a dead one — both are
|
|
8
|
+
// silent. So each clause narrows who counts as "the same agent, restarted":
|
|
9
|
+
//
|
|
10
|
+
// - Same project, same cwd, same agent name. A restart reopens the same harness
|
|
11
|
+
// in the same checkout; a different worktree or tool is a parallel worker.
|
|
12
|
+
// - The owner was created first and has made no call since the taker appeared
|
|
13
|
+
// (agentActiveAt, which the user's own Sends don't bump). An owner that kept
|
|
14
|
+
// working after the taker started is running alongside it, not gone.
|
|
15
|
+
// - The owner has no feedback long-poll open right now (`mockpit watch`).
|
|
16
|
+
//
|
|
17
|
+
// A reply also waits HANDOFF_GRACE_MS after the Send, so the owner — the agent
|
|
18
|
+
// the user is most likely about to tell — gets first claim. The one exception:
|
|
19
|
+
// a keyless owner (no harness conversation id) can never come back, so a reply
|
|
20
|
+
// it got before the taker existed goes at once — the user answered, then
|
|
21
|
+
// restarted. A keyed owner always gets the grace: its conversation may resume.
|
|
22
|
+
export const HANDOFF_GRACE_MS = 2 * 60_000;
|
|
23
|
+
export 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.";
|
|
24
|
+
export const ownerActiveAt = (s) => s.agentActiveAt ?? s.lastActiveAt;
|
|
25
|
+
export function supersedes(taker, owner, listening) {
|
|
26
|
+
return (owner.id !== taker.id &&
|
|
27
|
+
owner.project !== null &&
|
|
28
|
+
owner.project === taker.project &&
|
|
29
|
+
owner.cwd === taker.cwd &&
|
|
30
|
+
owner.agent === taker.agent &&
|
|
31
|
+
owner.createdAt < taker.createdAt &&
|
|
32
|
+
ownerActiveAt(owner) < taker.createdAt &&
|
|
33
|
+
!listening);
|
|
34
|
+
}
|
|
35
|
+
export function handoffReady(comment, owner, taker, now) {
|
|
36
|
+
return ((!owner.keyed && comment.createdAt < taker.createdAt) ||
|
|
37
|
+
now - Date.parse(comment.createdAt) >= HANDOFF_GRACE_MS);
|
|
38
|
+
}
|
package/dist/server/knobs.js
CHANGED
|
@@ -320,11 +320,10 @@ export function checkKnobValues(raw, mock, posts, what) {
|
|
|
320
320
|
return { ok: true, value: out };
|
|
321
321
|
}
|
|
322
322
|
// How many discrete choices a knob offers, or null when it is continuous. A knob
|
|
323
|
-
// with only a handful is usually a decision dressed as a control (plan D12).
|
|
323
|
+
// with only a handful is usually a decision dressed as a control (plan D12). A
|
|
324
|
+
// toggle is null: on/off reads fine on one render.
|
|
324
325
|
export function discreteChoices(config) {
|
|
325
326
|
const knob = explicitKnob(config);
|
|
326
|
-
if (knob.type === "toggle")
|
|
327
|
-
return 2;
|
|
328
327
|
if (knob.type === "select")
|
|
329
328
|
return knob.options.length;
|
|
330
329
|
if (knob.type === "image" && knob.options)
|
package/dist/server/mcpHttp.d.ts
CHANGED
|
@@ -23,11 +23,11 @@ export interface McpFlows {
|
|
|
23
23
|
},
|
|
24
24
|
ctx: FlowContext
|
|
25
25
|
]>;
|
|
26
|
-
say: Flow<[ref: unknown, body: any]>;
|
|
26
|
+
say: Flow<[ref: unknown, body: any, ctx: FlowContext]>;
|
|
27
27
|
feedback: Flow<[query: {
|
|
28
28
|
session?: unknown;
|
|
29
29
|
project?: unknown;
|
|
30
|
-
}]>;
|
|
30
|
+
}, ctx: FlowContext]>;
|
|
31
31
|
}
|
|
32
32
|
export interface McpDeps {
|
|
33
33
|
store: Store;
|
package/dist/server/mcpHttp.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { decodeBase64 } from "./base64.js";
|
|
2
2
|
import { isGuideTopic, unknownTopicMessage } from "./designGuide.js";
|
|
3
|
-
import { HTTP_MCP_TOOLS, HTTP_RUN_TOOLS, MCP_INSTRUCTIONS, MCP_SERVER_INFO, runToolResult, toolResult, } from "./mcpSpec.js";
|
|
3
|
+
import { HTTP_MCP_TOOLS, HTTP_RUN_TOOLS, MCP_INSTRUCTIONS, MCP_SERVER_INFO, runToolResult, toolResult, withFeedbackImages, } from "./mcpSpec.js";
|
|
4
4
|
import { RUN_INSTRUCTIONS } from "./runApi.js";
|
|
5
5
|
// A flow's error status becomes a tool error carrying the same JSON body the
|
|
6
6
|
// REST tier returns, so the agent sees the hint (states, variants) either way.
|
|
@@ -24,6 +24,10 @@ export function registerMcp(app, deps) {
|
|
|
24
24
|
const projects = await store.listProjects();
|
|
25
25
|
return projects[0]?.name ?? "workspace";
|
|
26
26
|
}
|
|
27
|
+
async function loadImage(ref) {
|
|
28
|
+
const asset = await store.getAsset(ref.id);
|
|
29
|
+
return asset ? { data: asset.data, mimeType: asset.contentType } : null;
|
|
30
|
+
}
|
|
27
31
|
async function callTool(name, args, ctx) {
|
|
28
32
|
switch (name) {
|
|
29
33
|
case "publish":
|
|
@@ -39,9 +43,9 @@ export function registerMcp(app, deps) {
|
|
|
39
43
|
history: args.history === true,
|
|
40
44
|
}));
|
|
41
45
|
case "feedback":
|
|
42
|
-
return unwrap(await flows.feedback({ session: args.session, project: args.project }));
|
|
46
|
+
return unwrap(await flows.feedback({ session: args.session, project: args.project }, ctx));
|
|
43
47
|
case "say":
|
|
44
|
-
return unwrap(await flows.say(args.mock, args));
|
|
48
|
+
return unwrap(await flows.say(args.mock, args, ctx));
|
|
45
49
|
case "export":
|
|
46
50
|
return unwrap(await flows.exportMock(args.mock, args, ctx));
|
|
47
51
|
case "upload": {
|
|
@@ -124,9 +128,11 @@ export function registerMcp(app, deps) {
|
|
|
124
128
|
const name = msg.params?.name;
|
|
125
129
|
if (name === "run") {
|
|
126
130
|
const args = msg.params?.arguments ?? {};
|
|
127
|
-
|
|
131
|
+
const envelope = unwrap(await deps.run(args, ctx));
|
|
132
|
+
return rpc(msg.id, await withFeedbackImages(runToolResult(envelope), envelope, loadImage));
|
|
128
133
|
}
|
|
129
|
-
|
|
134
|
+
const value = await callTool(name, msg.params?.arguments ?? {}, ctx);
|
|
135
|
+
return rpc(msg.id, await withFeedbackImages(toolResult(name, value), value, loadImage));
|
|
130
136
|
}
|
|
131
137
|
catch (err) {
|
|
132
138
|
return rpc(msg.id, {
|
package/dist/server/mcpSpec.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as z from "zod/v4";
|
|
2
|
+
import { type ImageRef } from "./feedbackBatch.ts";
|
|
2
3
|
export declare const MCP_SERVER_INFO: {
|
|
3
4
|
name: string;
|
|
4
5
|
version: string;
|
|
@@ -180,6 +181,13 @@ export declare function toolResult(name: string, value: unknown): {
|
|
|
180
181
|
text: string;
|
|
181
182
|
}[];
|
|
182
183
|
};
|
|
184
|
+
export declare const MAX_RESULT_IMAGES = 8;
|
|
185
|
+
export declare function withFeedbackImages<T extends {
|
|
186
|
+
content: unknown[];
|
|
187
|
+
}>(result: T, value: unknown, load: (ref: ImageRef) => Promise<{
|
|
188
|
+
data: Uint8Array;
|
|
189
|
+
mimeType: string;
|
|
190
|
+
} | null>): Promise<T>;
|
|
183
191
|
export declare function runToolResult(envelope: unknown): {
|
|
184
192
|
isError?: boolean | undefined;
|
|
185
193
|
structuredContent?: Record<string, unknown> | undefined;
|
package/dist/server/mcpSpec.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as z from "zod/v4";
|
|
2
|
+
import { encodeBase64 } from "./base64.js";
|
|
2
3
|
import { GUIDE_TOPICS } from "./designGuide.js";
|
|
4
|
+
import { feedbackImages } from "./feedbackBatch.js";
|
|
3
5
|
import { KIT_IDS } from "./kits.js";
|
|
4
6
|
import { RUN_DESCRIPTION } from "./runApi.js";
|
|
5
7
|
import { SURFACE_KINDS } from "./types.js";
|
|
@@ -245,6 +247,7 @@ const feedbackSchema = o({
|
|
|
245
247
|
mix: bag,
|
|
246
248
|
tuned: bag,
|
|
247
249
|
comments: z.array(o({ part: nullStr, text: str })),
|
|
250
|
+
reactions: strs.optional().describe('One-tap reactions, e.g. 👍 part "hero" (variant B)'),
|
|
248
251
|
})
|
|
249
252
|
.nullable()
|
|
250
253
|
.describe("The user's Send; null if only comments"),
|
|
@@ -288,7 +291,13 @@ const OUTPUT_SCHEMAS = {
|
|
|
288
291
|
session: nullStr,
|
|
289
292
|
},
|
|
290
293
|
publish: writeOutput,
|
|
291
|
-
ask: {
|
|
294
|
+
ask: {
|
|
295
|
+
asks: z.array(o({ id: str, text: str })),
|
|
296
|
+
open: num,
|
|
297
|
+
url: str,
|
|
298
|
+
nudges: strs.optional(),
|
|
299
|
+
feedback: feedbackRide,
|
|
300
|
+
},
|
|
292
301
|
feedback: {
|
|
293
302
|
feedback: z.array(feedbackSchema).describe("One batch per mock; empty when nothing is new"),
|
|
294
303
|
pending: z.array(pendingSchema),
|
|
@@ -394,6 +403,22 @@ export function toolResult(name, value) {
|
|
|
394
403
|
...(structured ? { structuredContent: value } : {}),
|
|
395
404
|
};
|
|
396
405
|
}
|
|
406
|
+
// The images a result's feedback carries, inlined as image content so the
|
|
407
|
+
// model sees what the user attached instead of only its URL. Capped per result:
|
|
408
|
+
// the rest stay reachable by URL in the JSON.
|
|
409
|
+
export const MAX_RESULT_IMAGES = 8;
|
|
410
|
+
export async function withFeedbackImages(result, value, load) {
|
|
411
|
+
const batches = value?.feedback;
|
|
412
|
+
if (!Array.isArray(batches))
|
|
413
|
+
return result;
|
|
414
|
+
const images = [];
|
|
415
|
+
for (const ref of feedbackImages(batches).slice(0, MAX_RESULT_IMAGES)) {
|
|
416
|
+
const bytes = await load(ref).catch(() => null);
|
|
417
|
+
if (bytes)
|
|
418
|
+
images.push({ type: "image", data: encodeBase64(bytes.data), mimeType: bytes.mimeType });
|
|
419
|
+
}
|
|
420
|
+
return images.length ? { ...result, content: [...result.content, ...images] } : result;
|
|
421
|
+
}
|
|
397
422
|
// A run that failed is still a full envelope (calls, feedback); isError tells
|
|
398
423
|
// the client the script did not finish.
|
|
399
424
|
export function runToolResult(envelope) {
|
package/dist/server/run.js
CHANGED
|
@@ -170,13 +170,13 @@ export function createRunFlow(deps) {
|
|
|
170
170
|
return [value, `${value.mock} ${value.asks.length} ask(s)`];
|
|
171
171
|
},
|
|
172
172
|
async feedback() {
|
|
173
|
-
const value = await take(flows.feedback(await mine({})));
|
|
173
|
+
const value = await take(flows.feedback(await mine({}), flowCtx));
|
|
174
174
|
const n = value.feedback.length;
|
|
175
175
|
return [value, n ? `${n} batch(es)` : "no feedback"];
|
|
176
176
|
},
|
|
177
177
|
async say([v, message]) {
|
|
178
178
|
const r = plain(v);
|
|
179
|
-
await take(flows.say(r.mock, { ...(await mine(r)), message }));
|
|
179
|
+
await take(flows.say(r.mock, { ...(await mine(r)), message }, flowCtx));
|
|
180
180
|
return [null, String(r.mock ?? "?")];
|
|
181
181
|
},
|
|
182
182
|
async upload([data, opts]) {
|