mockpit 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/bin/mockpit.js +24 -3
- package/dist/mcp/server.js +20 -3
- package/dist/server/apiViews.d.ts +11 -8
- package/dist/server/apiViews.js +21 -16
- package/dist/server/app.js +344 -59
- package/dist/server/appIcon.d.ts +22 -0
- package/dist/server/appIcon.js +43 -0
- package/dist/server/appIconPng.d.ts +1 -0
- package/dist/server/appIconPng.js +6 -0
- package/dist/server/askNudges.d.ts +3 -0
- package/dist/server/askNudges.js +40 -0
- package/dist/server/base64.d.ts +1 -0
- package/dist/server/base64.js +8 -0
- package/dist/server/designGuide.js +15 -13
- package/dist/server/feedbackBatch.d.ts +16 -4
- package/dist/server/feedbackBatch.js +59 -9
- package/dist/server/handoff.d.ts +6 -0
- package/dist/server/handoff.js +38 -0
- package/dist/server/knobs.js +2 -3
- package/dist/server/mcpHttp.d.ts +2 -2
- package/dist/server/mcpHttp.js +11 -5
- package/dist/server/mcpSpec.d.ts +8 -0
- package/dist/server/mcpSpec.js +26 -1
- package/dist/server/run.js +2 -2
- package/dist/server/runApi.d.ts +1 -1
- package/dist/server/runApi.js +1 -1
- package/dist/server/sqlStore.d.ts +3 -1
- package/dist/server/sqlStore.js +40 -8
- package/dist/server/types.d.ts +25 -2
- package/dist/server/types.js +47 -4
- package/extensions/mockpit.js +1 -0
- package/guide/topics/asks.md +19 -7
- package/guide/topics/feedback.md +15 -5
- package/guide/topics/knobs.md +3 -3
- package/guide/topics/surfaces.md +8 -8
- package/package.json +1 -1
- package/skills/mockpit/SKILL.md +4 -2
- package/viewer/dist/index.html +9 -9
package/README.md
CHANGED
|
@@ -70,8 +70,10 @@ mockpit turns that into a design-decision loop:
|
|
|
70
70
|
|
|
71
71
|
**Home** lists the project's mocks with a thumbnail, their states and how many
|
|
72
72
|
questions are open, with "Answer next ›" to jump to the first one. Dark and
|
|
73
|
-
light themes, toggled from the top bar. On a
|
|
74
|
-
bottom sheet
|
|
73
|
+
light themes, toggled from the top bar. On a phone or tablet the panel becomes a
|
|
74
|
+
bottom sheet, the stage scrolls under your finger and renders the design at a
|
|
75
|
+
chosen width (Phone 390, Tablet 744, Desktop 820, Wide 1280), and Inspect opens
|
|
76
|
+
the stage full screen to pan and pinch-zoom it.
|
|
75
77
|
|
|
76
78
|
## Quick start
|
|
77
79
|
|
package/bin/mockpit.js
CHANGED
|
@@ -37,7 +37,9 @@ the loop (never blocks on the user):
|
|
|
37
37
|
1. mockpit init once per repo; read the brief (mockpit guide).
|
|
38
38
|
2. mockpit publish once per variant; mark parts with data-part.
|
|
39
39
|
3. A choice is several variants plus one ask that binds them: mockpit ask
|
|
40
|
-
with options bound to variants.
|
|
40
|
+
with options bound to variants. A variant decides everything it differs
|
|
41
|
+
on; don't re-ask it. Visual options carry a variant or knob set. One
|
|
42
|
+
render plus a control: --knobs.
|
|
41
43
|
4. Tell the user in one line where to look, then end your turn. The question
|
|
42
44
|
itself lives in the mock (ask), never in chat. The user answers in the
|
|
43
45
|
browser at their own pace. Never poll.
|
|
@@ -72,7 +74,8 @@ verbs:
|
|
|
72
74
|
--data is a JSON tree)
|
|
73
75
|
mockpit ask --mock <slug> "<question>" --option <label[=variant]> ...
|
|
74
76
|
ask the user; bind options to variants
|
|
75
|
-
--option <l[=v]> an option (repeatable); "=variant" binds it to a variant
|
|
77
|
+
--option <l[=v]> an option (repeatable); "=variant" binds it to a variant,
|
|
78
|
+
"=knob:path=value" sets one knob
|
|
76
79
|
--scope <s> mock|state|part (default mock)
|
|
77
80
|
--state <label> the state a state-scoped ask is about
|
|
78
81
|
--part <name> the part a part-scoped ask is about
|
|
@@ -958,8 +961,23 @@ async function publishMock() {
|
|
|
958
961
|
return result;
|
|
959
962
|
}
|
|
960
963
|
|
|
961
|
-
// "Quiet" is an unbound option; "Quiet=quiet" binds it to the quiet variant
|
|
964
|
+
// "Quiet" is an unbound option; "Quiet=quiet" binds it to the quiet variant;
|
|
965
|
+
// "Above=knob:trim.position=top" sets one knob. The value is JSON when it
|
|
966
|
+
// parses (17, true), else a string, so slider and toggle values keep their type.
|
|
962
967
|
function parseOption(raw) {
|
|
968
|
+
const knob = raw.indexOf("=knob:");
|
|
969
|
+
if (knob > 0) {
|
|
970
|
+
const pair = raw.slice(knob + "=knob:".length);
|
|
971
|
+
const eq = pair.indexOf("=");
|
|
972
|
+
if (eq <= 0)
|
|
973
|
+
die(`option "${raw}" needs knob:<path>=<value>`, "--option Above=knob:trim.position=top");
|
|
974
|
+
const text = pair.slice(eq + 1);
|
|
975
|
+
let value = text;
|
|
976
|
+
try {
|
|
977
|
+
value = JSON.parse(text);
|
|
978
|
+
} catch {}
|
|
979
|
+
return { label: raw.slice(0, knob), set: { [pair.slice(0, eq)]: value } };
|
|
980
|
+
}
|
|
963
981
|
const at = raw.lastIndexOf("=");
|
|
964
982
|
if (at <= 0) return { label: raw };
|
|
965
983
|
return { label: raw.slice(0, at), variant: raw.slice(at + 1) };
|
|
@@ -986,6 +1004,7 @@ function printRun(r) {
|
|
|
986
1004
|
function watchLines(batch) {
|
|
987
1005
|
const lines = [];
|
|
988
1006
|
const name = batch.mock ?? "a mock";
|
|
1007
|
+
if (batch.handoff) lines.push(`mockpit handoff on ${name}: ${batch.handoff}`);
|
|
989
1008
|
if (batch.reply) {
|
|
990
1009
|
const r = batch.reply;
|
|
991
1010
|
const bits = [
|
|
@@ -1004,6 +1023,7 @@ function watchLines(batch) {
|
|
|
1004
1023
|
.join(", ")}`
|
|
1005
1024
|
: "",
|
|
1006
1025
|
r.comments?.length ? `${r.comments.length} comment${r.comments.length === 1 ? "" : "s"}` : "",
|
|
1026
|
+
r.reactions?.length ? r.reactions.join(", ") : "",
|
|
1007
1027
|
r.decision ? r.decision.kind : "",
|
|
1008
1028
|
r.text ?? "",
|
|
1009
1029
|
].filter(Boolean);
|
|
@@ -1380,6 +1400,7 @@ const commands = {
|
|
|
1380
1400
|
`asked on ${result.mock}: ${ask.text} [${ask.options.map((o) => o.label).join(" | ")}]`,
|
|
1381
1401
|
);
|
|
1382
1402
|
}
|
|
1403
|
+
for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
|
|
1383
1404
|
console.log(`tell the user to look at ${result.url}, then end your turn`);
|
|
1384
1405
|
printFeedback(result.feedback);
|
|
1385
1406
|
},
|
package/dist/mcp/server.js
CHANGED
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
8
|
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
-
import { MCP_INSTRUCTIONS, MCP_SERVER_INFO, runToolResult, STDIO_MCP_CATALOG, STDIO_MCP_TOOLS, STDIO_RUN_CATALOG, STDIO_RUN_TOOLS, toolResult, } from "../server/mcpSpec.js";
|
|
9
|
+
import { MCP_INSTRUCTIONS, MCP_SERVER_INFO, runToolResult, STDIO_MCP_CATALOG, STDIO_MCP_TOOLS, STDIO_RUN_CATALOG, STDIO_RUN_TOOLS, toolResult, withFeedbackImages, } from "../server/mcpSpec.js";
|
|
10
10
|
import { RUN_INSTRUCTIONS } from "../server/runApi.js";
|
|
11
11
|
// Point at a deployed instance later by setting MOCKPIT_URL.
|
|
12
12
|
const API = process.env.MOCKPIT_URL ?? "http://localhost:8228";
|
|
@@ -230,18 +230,35 @@ async function run(args) {
|
|
|
230
230
|
body: JSON.stringify({ code, session, project: resolveProject(args.project), agent: AGENT }),
|
|
231
231
|
});
|
|
232
232
|
}
|
|
233
|
+
async function loadImage(ref) {
|
|
234
|
+
const res = await fetch(`${API}/a/${encodeURIComponent(ref.id)}`, {
|
|
235
|
+
headers: TOKEN ? { authorization: `Bearer ${TOKEN}` } : {},
|
|
236
|
+
});
|
|
237
|
+
if (!res.ok)
|
|
238
|
+
return null;
|
|
239
|
+
return {
|
|
240
|
+
data: new Uint8Array(await res.arrayBuffer()),
|
|
241
|
+
mimeType: res.headers.get("content-type") ?? "image/png",
|
|
242
|
+
};
|
|
243
|
+
}
|
|
233
244
|
const server = new McpServer(MCP_SERVER_INFO, {
|
|
234
245
|
instructions: CODE_MODE ? RUN_INSTRUCTIONS : MCP_INSTRUCTIONS,
|
|
235
246
|
});
|
|
236
247
|
if (CODE_MODE) {
|
|
237
248
|
const [{ name, ...config }] = STDIO_RUN_TOOLS;
|
|
238
|
-
server.registerTool(name, config, async (args) =>
|
|
249
|
+
server.registerTool(name, config, async (args) => {
|
|
250
|
+
const envelope = await run(args);
|
|
251
|
+
return withFeedbackImages(runToolResult(envelope), envelope, loadImage);
|
|
252
|
+
});
|
|
239
253
|
}
|
|
240
254
|
else {
|
|
241
255
|
for (const tool of STDIO_MCP_TOOLS) {
|
|
242
256
|
const handler = handlers[tool.name];
|
|
243
257
|
const { name, ...config } = tool;
|
|
244
|
-
server.registerTool(name, config, async (args) =>
|
|
258
|
+
server.registerTool(name, config, async (args) => {
|
|
259
|
+
const value = await handler(args);
|
|
260
|
+
return withFeedbackImages(toolResult(name, value), value, loadImage);
|
|
261
|
+
});
|
|
245
262
|
}
|
|
246
263
|
}
|
|
247
264
|
// Replaces the SDK's own listing (registered above), so stdio advertises the
|
|
@@ -203,6 +203,15 @@ export interface StateParts {
|
|
|
203
203
|
}
|
|
204
204
|
export declare function partsByState(mock: Mock, posts: Post[]): StateParts[];
|
|
205
205
|
export declare const mockSummaryView: (mock: Mock, posts: Post[]) => {
|
|
206
|
+
sessionId: string | null;
|
|
207
|
+
createdAt: string;
|
|
208
|
+
updatedAt: string;
|
|
209
|
+
thumbnail: {
|
|
210
|
+
postId: string;
|
|
211
|
+
surface: number;
|
|
212
|
+
version: number;
|
|
213
|
+
} | null;
|
|
214
|
+
drafted?: number | undefined;
|
|
206
215
|
id: string;
|
|
207
216
|
project: string;
|
|
208
217
|
slug: string;
|
|
@@ -212,14 +221,6 @@ export declare const mockSummaryView: (mock: Mock, posts: Post[]) => {
|
|
|
212
221
|
stateCount: number;
|
|
213
222
|
variants: number;
|
|
214
223
|
open: number;
|
|
215
|
-
sessionId: string | null;
|
|
216
|
-
createdAt: string;
|
|
217
|
-
updatedAt: string;
|
|
218
|
-
thumbnail: {
|
|
219
|
-
postId: string;
|
|
220
|
-
surface: number;
|
|
221
|
-
version: number;
|
|
222
|
-
} | null;
|
|
223
224
|
};
|
|
224
225
|
export declare const mockDetailView: (mock: Mock, posts: Post[], opts?: VariantViewOptions & {
|
|
225
226
|
tuned?: Record<string, unknown>;
|
|
@@ -326,4 +327,6 @@ export declare const sessionRowView: (session: Session, postCount: number) => {
|
|
|
326
327
|
lastActiveAt: string;
|
|
327
328
|
agentSeq: number;
|
|
328
329
|
project: string | null;
|
|
330
|
+
agentActiveAt: string | null;
|
|
331
|
+
keyed: boolean;
|
|
329
332
|
};
|
package/dist/server/apiViews.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Response shapes shared by every tier (REST, both MCP transports, the CLI), so
|
|
2
2
|
// one mock reads the same however an agent asks for it.
|
|
3
3
|
import { mergeParts, partsInSurfaces } from "./parts.js";
|
|
4
|
-
import { isSandboxedSurfaceKind, openCount, SURFACE_CONTENT_FIELDS, } from "./types.js";
|
|
4
|
+
import { isSandboxedSurfaceKind, draftedCount, openCount, SURFACE_CONTENT_FIELDS, } from "./types.js";
|
|
5
5
|
export const surfaceRef = (surface, index) => ({
|
|
6
6
|
id: surface.id,
|
|
7
7
|
kind: surface.kind,
|
|
@@ -94,21 +94,26 @@ function thumbnail(mock, posts) {
|
|
|
94
94
|
const surface = pick.surfaces.findIndex((s) => isSandboxedSurfaceKind(s.kind));
|
|
95
95
|
return { postId: pick.id, surface: Math.max(surface, 0), version: pick.version };
|
|
96
96
|
}
|
|
97
|
-
export const mockSummaryView = (mock, posts) =>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
97
|
+
export const mockSummaryView = (mock, posts) => {
|
|
98
|
+
const drafted = draftedCount(mock, posts);
|
|
99
|
+
return {
|
|
100
|
+
id: mock.id,
|
|
101
|
+
project: mock.project,
|
|
102
|
+
slug: mock.slug,
|
|
103
|
+
title: mock.title,
|
|
104
|
+
kind: mock.kind,
|
|
105
|
+
states: mock.states,
|
|
106
|
+
stateCount: Math.max(mock.states.length, 1),
|
|
107
|
+
variants: posts.length,
|
|
108
|
+
open: openCount(mock, posts),
|
|
109
|
+
// Only once the user has started one, so a list with no drafts costs nothing.
|
|
110
|
+
...(drafted === null ? {} : { drafted }),
|
|
111
|
+
sessionId: mock.sessionId,
|
|
112
|
+
createdAt: mock.createdAt,
|
|
113
|
+
updatedAt: mock.updatedAt,
|
|
114
|
+
thumbnail: thumbnail(mock, posts),
|
|
115
|
+
};
|
|
116
|
+
};
|
|
112
117
|
// One mock as anyone outside the viewer's own draft sees it: the draft is the
|
|
113
118
|
// user's unsent work and never leaves through this view.
|
|
114
119
|
export const mockDetailView = (mock, posts, opts = {}) => ({
|