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 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 narrow screen the panel becomes 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. One render plus a control: --knobs.
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
  },
@@ -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) => runToolResult(await run(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) => toolResult(name, await handler(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
  };
@@ -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
- id: mock.id,
99
- project: mock.project,
100
- slug: mock.slug,
101
- title: mock.title,
102
- kind: mock.kind,
103
- states: mock.states,
104
- stateCount: Math.max(mock.states.length, 1),
105
- variants: posts.length,
106
- open: openCount(mock, posts),
107
- sessionId: mock.sessionId,
108
- createdAt: mock.createdAt,
109
- updatedAt: mock.updatedAt,
110
- thumbnail: thumbnail(mock, posts),
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 = {}) => ({