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.
- package/README.md +12 -3
- package/bin/mockpit.js +47 -21
- package/dist/mcp/server.js +43 -4
- package/dist/server/apiViews.d.ts +11 -8
- package/dist/server/apiViews.js +21 -16
- package/dist/server/app.d.ts +3 -1
- package/dist/server/app.js +374 -67
- 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/eventLog.d.ts +27 -0
- package/dist/server/eventLog.js +187 -0
- 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/index.js +12 -0
- package/dist/server/knobs.js +2 -3
- package/dist/server/logFile.d.ts +2 -0
- package/dist/server/logFile.js +27 -0
- 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 +24 -9
- package/guide/topics/asks.md +19 -7
- package/guide/topics/feedback.md +17 -7
- package/guide/topics/knobs.md +3 -3
- package/guide/topics/surfaces.md +8 -8
- package/package.json +2 -2
- 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
|
|
|
@@ -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
|
|
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;
|