mockpit 1.3.0 → 3.0.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 (77) hide show
  1. package/README.md +66 -202
  2. package/bin/demoData.js +5 -5
  3. package/bin/initDesign.js +6 -6
  4. package/bin/mockpit.js +687 -192
  5. package/bin/poster.js +377 -0
  6. package/dist/mcp/server.js +110 -19
  7. package/dist/server/answerAdditions.d.ts +7 -0
  8. package/dist/server/answerAdditions.js +76 -0
  9. package/dist/server/apiViews.d.ts +563 -41
  10. package/dist/server/apiViews.js +223 -74
  11. package/dist/server/app.d.ts +10 -2
  12. package/dist/server/app.js +3284 -782
  13. package/dist/server/appIcon.js +15 -5
  14. package/dist/server/appIconPng.js +3 -3
  15. package/dist/server/askNudges.js +1 -1
  16. package/dist/server/cdnInline.d.ts +8 -0
  17. package/dist/server/cdnInline.js +638 -0
  18. package/dist/server/codeWorker.js +2 -1
  19. package/dist/server/decisionList.d.ts +45 -0
  20. package/dist/server/decisionList.js +302 -0
  21. package/dist/server/decisions.d.ts +8 -8
  22. package/dist/server/decisions.js +18 -17
  23. package/dist/server/designGuide.d.ts +10 -6
  24. package/dist/server/designGuide.js +173 -78
  25. package/dist/server/embeds.d.ts +4 -1
  26. package/dist/server/embeds.js +57 -6
  27. package/dist/server/eventLog.d.ts +2 -2
  28. package/dist/server/eventLog.js +11 -11
  29. package/dist/server/events.d.ts +9 -6
  30. package/dist/server/feedbackBatch.d.ts +38 -16
  31. package/dist/server/feedbackBatch.js +121 -50
  32. package/dist/server/fills.d.ts +4 -0
  33. package/dist/server/fills.js +55 -1
  34. package/dist/server/guidance.d.ts +23 -0
  35. package/dist/server/guidance.js +71 -0
  36. package/dist/server/handoff.d.ts +4 -1
  37. package/dist/server/handoff.js +8 -2
  38. package/dist/server/icons.js +1 -1
  39. package/dist/server/index.js +49 -11
  40. package/dist/server/jump.d.ts +21 -0
  41. package/dist/server/jump.js +175 -0
  42. package/dist/server/knobs.d.ts +3 -3
  43. package/dist/server/knobs.js +6 -6
  44. package/dist/server/mcpHttp.d.ts +8 -1
  45. package/dist/server/mcpHttp.js +44 -7
  46. package/dist/server/mcpSpec.d.ts +2 -1
  47. package/dist/server/mcpSpec.js +172 -50
  48. package/dist/server/parts.js +9 -1
  49. package/dist/server/postSurfaces.js +3 -0
  50. package/dist/server/run.d.ts +1 -1
  51. package/dist/server/run.js +31 -17
  52. package/dist/server/runApi.d.ts +3 -3
  53. package/dist/server/runApi.js +34 -24
  54. package/dist/server/sqlStore.d.ts +117 -94
  55. package/dist/server/sqlStore.js +1089 -141
  56. package/dist/server/standings.d.ts +25 -0
  57. package/dist/server/standings.js +113 -0
  58. package/dist/server/surfacePage.d.ts +6 -9
  59. package/dist/server/surfacePage.js +361 -86
  60. package/dist/server/types.d.ts +252 -69
  61. package/dist/server/types.js +87 -69
  62. package/extensions/mockpit.js +245 -65
  63. package/guide/AGENT_SETUP.md +13 -1
  64. package/guide/packs/design-craft.md +34 -0
  65. package/guide/topics/asks.md +117 -69
  66. package/guide/topics/feedback.md +156 -119
  67. package/guide/topics/html.md +19 -32
  68. package/guide/topics/http.md +43 -40
  69. package/guide/topics/knobs.md +7 -10
  70. package/guide/topics/scripts.md +24 -35
  71. package/guide/topics/surfaces.md +50 -41
  72. package/opencode/mockpit.js +120 -0
  73. package/opencode/package.json +8 -0
  74. package/package.json +4 -6
  75. package/skills/launch-video/SKILL.md +12 -12
  76. package/skills/mockpit/SKILL.md +35 -23
  77. package/viewer/dist/index.html +9 -8
@@ -9,9 +9,8 @@
9
9
  // build their `kind` enums from it, so a kind can't be added to the model
10
10
  // without the MCP tier advertising it too (the gap that left `json`/`code`
11
11
  // publishable over CLI/REST but invisible to MCP). The per-kind FIELD schemas
12
- // in postSurfaces.ts and mcpSpec.ts are still hand-written; test/mcpSpec.test.ts
13
- // guards that every kind here round-trips through both the MCP schema and the
14
- // validator with its fields, so neither half can silently fall behind.
12
+ // in postSurfaces.ts and mcpSpec.ts are still hand-written: a kind added here
13
+ // needs its fields in both, or one half silently falls behind.
15
14
  export const SURFACE_KINDS = [
16
15
  "html",
17
16
  "diff",
@@ -51,11 +50,42 @@ export function isSurfaceKind(kind) {
51
50
  export function isSandboxedSurfaceKind(kind) {
52
51
  return isSurfaceKind(kind) && SURFACE_KIND_METADATA[kind].sandboxed;
53
52
  }
53
+ export const isDesignKind = (k) => k === "screen" || k === "component" || k === "idea";
54
+ export const variantStatus = (status) => status === "accepted" ? "inUse" : status;
54
55
  // Still in the running: what the user can choose between.
55
- export const isLive = (status) => status === "open" || status === "accepted";
56
+ export const isLive = (status) => status === "open" || status === "accepted" || status === "inUse";
56
57
  // The version a variant is resolved at when nothing names one: the version in
57
58
  // use when accepted, else its latest.
58
59
  export const inUseVersion = (post) => post.status === "accepted" ? (post.acceptedVersion ?? post.version) : post.version;
60
+ export const newestVisible = (post) => {
61
+ const hidden = post.hiddenVersions ?? [];
62
+ const visible = [post.version, ...post.history.map((h) => h.version)].filter((v) => !hidden.includes(v));
63
+ return visible.length ? Math.max(...visible) : post.version;
64
+ };
65
+ // What stands for one line of versions: the newest wins until the user presses Use
66
+ // this, and then only their pick does. A version they hid doesn't win.
67
+ export const currentVersion = (post) => post.status === "accepted" ? inUseVersion(post) : newestVisible(post);
68
+ // A newer version than the one in use, which asks before it takes over. Unset until
69
+ // the agent publishes past what the user picked or last chose to stay on.
70
+ export const changedTo = (post) => {
71
+ if (post.status !== "accepted")
72
+ return undefined;
73
+ const newest = newestVisible(post);
74
+ return newest > Math.max(inUseVersion(post), post.stayedVersion ?? 0) ? newest : undefined;
75
+ };
76
+ // The variant the agent would pick for a state, read off its questions. Parallel
77
+ // variants arrive within minutes of each other, so none of them is "the newest", and
78
+ // this is the only thing that says one of them leads.
79
+ export const recommendedVariant = (asks, state) => {
80
+ for (const a of asks) {
81
+ if (a.state !== undefined && a.state !== state)
82
+ continue;
83
+ const pick = a.options.find((o) => o.recommended && o.variant);
84
+ if (pick)
85
+ return pick.variant;
86
+ }
87
+ return undefined;
88
+ };
59
89
  // A variant as it stood at one of its versions (current or retained), or null
60
90
  // when that version is gone.
61
91
  export function postAtVersion(post, version) {
@@ -76,7 +106,7 @@ export function postAtVersion(post, version) {
76
106
  ...(old.author === undefined ? {} : { author: old.author }),
77
107
  };
78
108
  }
79
- // The variant's newest version not after mock version `n` — what the stage
109
+ // The variant's newest version not after design version `n` — what the stage
80
110
  // showed when a draft or reply was bound to n (the viewer's frameVersion).
81
111
  export function versionAt(post, n) {
82
112
  const versions = [post.version, ...post.history.map((h) => h.version)];
@@ -84,17 +114,30 @@ export function versionAt(post, n) {
84
114
  return fit.length ? Math.max(...fit) : Math.min(...versions);
85
115
  }
86
116
  // The state a version option names a version in: the ask's state, else the
87
- // mock's first (`ask` accepts version options without a state only while the
88
- // mock has one).
89
- export const optionState = (ask, mock) => ask.state ?? mock.states[0] ?? null;
117
+ // design's first (`ask` accepts version options without a state only while the
118
+ // design has one).
119
+ export const optionState = (ask, design) => ask.state ?? design.states[0] ?? null;
90
120
  // The viewer's write-in option on every ask. Reserved, so a delivered
91
121
  // `{id: "other", other: true}` choice never shadows an option the agent declared.
92
122
  export const OTHER_ID = "other";
123
+ // As far as a block's text is carried: the frame clips to it, the host and the
124
+ // server re-clip, so it also bounds what one comment can repeat back to an agent.
125
+ export const PARAGRAPH_TEXT_MAX = 600;
126
+ export const PARAGRAPHS_MAX = 500;
93
127
  // A one-tap verdict on a part (or, with part null, on the variant itself),
94
128
  // for a reviewer on a phone who would rather not type: like, dislike, or
95
129
  // "more like this". Rides as a part comment, so it travels in the one reply.
96
130
  export const REACTIONS = ["up", "down", "more"];
97
- export const sameScreen = (a, b) => a.mock === b.mock && a.state === b.state;
131
+ // The settings key prefix of a project's area order (`areaOrder:<project>`): a
132
+ // JSON array of area names, the order Home lists them in.
133
+ export const AREA_ORDER_SETTING = "areaOrder";
134
+ // The user's hand edits to a project's decision list (`decisions:<project>`).
135
+ export const DECISION_EDITS_SETTING = "decisions";
136
+ export const MAX_NOTES = 4000;
137
+ // Declared and not drawn yet: a screen or component the agent planned, which its next
138
+ // publish fills. An undeclared design with nothing in it is no plan, only empty.
139
+ export const isPlanned = (design, variants) => design.kind !== "idea" && variants === 0;
140
+ export const sameScreen = (a, b) => a.design === b.design && a.state === b.state;
98
141
  export const HISTORY_LIMIT = 20;
99
142
  // "user" is the reserved trust label for genuine human comments. A session
100
143
  // agent with that name could otherwise have its programmatic comments delivered
@@ -114,7 +157,7 @@ export function stripNul(s) {
114
157
  // budget the store evicts down to. One Durable Object holds the whole workspace, so
115
158
  // the budget sits well under its ~10 GB SQLite ceiling.
116
159
  export const MAX_ASSET_BYTES = 5 * 1024 * 1024;
117
- // What the user may attach to a write-in or a note: raster formats a model can
160
+ // What the user may attach to a write-in or a comment: raster formats a model can
118
161
  // read. SVG is active content, so it is never attachable.
119
162
  export const ATTACHABLE_IMAGE_TYPES = new Set([
120
163
  "image/png",
@@ -126,7 +169,7 @@ export const MAX_ASK_IMAGES = 6;
126
169
  export const MAX_WORKSPACE_ASSET_BYTES = 2 * 1024 * 1024 * 1024;
127
170
  // Short, unguessable id: 8 random bytes (64 bits) as 11 url-safe base64 chars.
128
171
  // These double as bearer capabilities: in publicRead mode `/s/:id` and
129
- // `/api/mocks/:id` are reachable without the workspace token, so the id IS the
172
+ // `/api/designs/:id` are reachable without the workspace token, so the id IS the
130
173
  // share secret and must resist enumeration. btoa is a global in both Node and
131
174
  // Workers.
132
175
  export const newId = () => {
@@ -158,7 +201,7 @@ export function normalizeSurfaceIds(surfaces) {
158
201
  // Fallback project for sessions that never declared one (bare `curl` publishes).
159
202
  export const DEFAULT_PROJECT = "workspace";
160
203
  export const DEFAULT_VARIANT = "default";
161
- // Stable, url-safe slug derived from a title. Empty input yields "mock" so a
204
+ // Stable, url-safe slug derived from a title. Empty input yields "design" so a
162
205
  // slug is never the empty string.
163
206
  export function slugify(input) {
164
207
  const slug = input
@@ -169,7 +212,7 @@ export function slugify(input) {
169
212
  .replace(/^-+|-+$/g, "")
170
213
  .slice(0, 60)
171
214
  .replace(/-+$/g, "");
172
- return slug || "mock";
215
+ return slug || "design";
173
216
  }
174
217
  // Last path segment of a working directory, for sessions that never declared a
175
218
  // project. Handles both separators so a Windows cwd resolves the same way.
@@ -186,68 +229,40 @@ export const htmlSurface = (html, kits) => ({
186
229
  ? { kits: kits.filter((k) => typeof k === "string") }
187
230
  : {}),
188
231
  });
189
- // An open ask is one no reply has answered yet: a write-in or a note alone
232
+ // An open ask is one no reply has answered yet: a write-in or a comment alone
190
233
  // answers it too ("Neither, because…").
191
- export const askAnswered = (a) => a.answer !== undefined || a.other !== undefined || a.note !== undefined;
192
- export const openAsks = (mock) => mock.asks.filter((a) => !askAnswered(a) && !a.dismissed);
193
- // How many built-in "Which one?" asks the viewer shows: one mock-wide when every
194
- // state needs one and the live names line up, else one per needing state. Same
195
- // rule as viewer/src/logic.ts builtinAsks, so Home, `read` and `pending` count
196
- // the question the user actually sees. Answering it archives the losers, which
197
- // is what makes it stop counting.
198
- export function builtinAskIds(mock, posts, mockId) {
199
- // Resolving closed the choice along with every other open question.
200
- if (mock.resolvedAt)
201
- return [];
202
- const live = (state) => posts
203
- .filter((p) => p.mock === mockId && p.state === state && isLive(p.status))
204
- .map((p) => p.variant);
205
- const needs = (state) => {
206
- const names = live(state);
207
- if (names.length < 2)
208
- return false;
209
- return !mock.asks.some((a) => (a.scope === "mock" || (a.scope === "state" && a.state === state)) &&
210
- names.every((n) => a.options.some((o) => o.variant === n)));
211
- };
212
- const states = mock.states.length ? mock.states : [null];
213
- const needing = states.filter(needs);
214
- if (!needing.length)
215
- return [];
216
- const key = (state) => [...live(state)].sort().join("\n");
217
- const first = key(needing[0]);
218
- return needing.length === states.length && states.every((st) => key(st) === first)
219
- ? [BUILTIN_ASK_ID]
220
- : needing.map((st) => (st === null ? BUILTIN_ASK_ID : `${BUILTIN_ASK_ID}:${st}`));
221
- }
222
- export const builtinAskCount = (mock, posts, mockId) => builtinAskIds(mock, posts, mockId).length;
223
- export const openCount = (mock, posts) => openAsks(mock).length + builtinAskCount(mock, posts, mock.id);
234
+ export const askAnswered = (a) => a.answer !== undefined || a.other !== undefined || a.comment !== undefined;
235
+ export const openAsks = (design) => design.asks.filter((a) => !askAnswered(a) && !a.dismissed);
236
+ // Questions an agent asked that wait on the user. The built-in "Which variant?"
237
+ // is not one: no agent is waiting on it, and being shown several designs owes
238
+ // nobody an answer.
239
+ export const openCount = (design) => openAsks(design).length;
224
240
  // How many of the open asks the user's unsent draft already speaks for (a
225
- // pick, a write-in or a note), so Home can show a started mock as "draft 2/4";
241
+ // pick, a write-in or a comment), so Home can show a started design as "draft 2/4";
226
242
  // null when there is no draft. A count only: the draft's content stays private.
227
- export function draftedCount(mock, posts) {
228
- const d = mock.draft;
243
+ export function draftedCount(design) {
244
+ const d = design.draft;
229
245
  if (!d)
230
246
  return null;
231
- const open = [...openAsks(mock).map((a) => a.id), ...builtinAskIds(mock, posts, mock.id)];
232
247
  const said = (id) => {
233
248
  const a = d.answers[id];
234
249
  const picked = Array.isArray(a) ? a.length > 0 : a !== undefined;
235
- return picked || d.others?.[id] !== undefined || d.notes?.[id] !== undefined;
250
+ return picked || d.others?.[id] !== undefined || d.askComments?.[id] !== undefined;
236
251
  };
237
- return open.filter(said).length;
252
+ return openAsks(design).filter((a) => said(a.id)).length;
238
253
  }
239
- // The viewer's built-in "Which one?" for variants no agent ask binds: `variant`
240
- // mock-wide, `variant:<state>` per state. Never stored; synthesized from the
254
+ // The viewer's built-in "Which variant?" for variants no agent ask binds: `variant`
255
+ // design-wide, `variant:<state>` per state. Never stored; synthesized from the
241
256
  // variants so a reply answering it validates, flips and reads like any ask. An
242
257
  // option's id is the variant name, so the viewer needs no id mapping.
243
258
  export const BUILTIN_ASK_ID = "variant";
244
- export function builtinAsk(mock, posts, id) {
259
+ export function builtinAsk(design, posts, id) {
245
260
  let state = null;
246
261
  if (id !== BUILTIN_ASK_ID) {
247
262
  if (!id.startsWith(`${BUILTIN_ASK_ID}:`))
248
263
  return undefined;
249
264
  state = id.slice(BUILTIN_ASK_ID.length + 1);
250
- if (!mock.states.includes(state))
265
+ if (!design.states.includes(state))
251
266
  return undefined;
252
267
  }
253
268
  const names = [
@@ -257,8 +272,8 @@ export function builtinAsk(mock, posts, id) {
257
272
  return undefined;
258
273
  return {
259
274
  id,
260
- text: "Which one?",
261
- scope: state === null ? "mock" : "state",
275
+ text: "Which variant?",
276
+ scope: state === null ? "design" : "state",
262
277
  ...(state === null ? {} : { state }),
263
278
  options: names.map((v) => ({ id: v, label: v, variant: v })),
264
279
  at: "",
@@ -266,9 +281,9 @@ export function builtinAsk(mock, posts, id) {
266
281
  }
267
282
  // Per-project rollup for the projects list. Pure, so every store agrees on
268
283
  // what "open" and "lastActiveAt" mean.
269
- export function summarizeProjects(mocks, sessions, posts = []) {
284
+ export function summarizeProjects(designs, sessions) {
270
285
  const byProject = new Map();
271
- for (const m of mocks) {
286
+ for (const m of designs) {
272
287
  const list = byProject.get(m.project);
273
288
  if (list)
274
289
  list.push(m);
@@ -288,12 +303,12 @@ export function summarizeProjects(mocks, sessions, posts = []) {
288
303
  return [...names]
289
304
  .map((name) => {
290
305
  const list = byProject.get(name) ?? [];
291
- const lastMock = list.reduce((max, m) => (m.updatedAt > max ? m.updatedAt : max), "");
306
+ const lastDesign = list.reduce((max, m) => (m.updatedAt > max ? m.updatedAt : max), "");
292
307
  return {
293
308
  name,
294
- mocks: list.length,
295
- open: list.reduce((n, m) => n + openCount(m, posts), 0),
296
- lastActiveAt: [lastMock, sessionActive.get(name) ?? ""].sort().pop() ?? "",
309
+ designs: list.length,
310
+ open: list.reduce((n, m) => n + openCount(m), 0),
311
+ lastActiveAt: [lastDesign, sessionActive.get(name) ?? ""].sort().pop() ?? "",
297
312
  sessions: sessionCounts.get(name) ?? 0,
298
313
  };
299
314
  })
@@ -306,7 +321,7 @@ export function surfacesByteLength(surfaces) {
306
321
  let n = 0;
307
322
  for (const p of surfaces) {
308
323
  if (p.kind === "html")
309
- n += p.html.length;
324
+ n += p.html.length + (p.poster?.length ?? 0);
310
325
  else if (p.kind === "diff") {
311
326
  n += p.patch?.length ?? 0;
312
327
  for (const f of p.files ?? [])
@@ -334,7 +349,8 @@ export function surfacesByteLength(surfaces) {
334
349
  }
335
350
  return n;
336
351
  }
337
- // Collect the asset ids an ordered surfaces list references (image surfaces).
352
+ // Collect the asset ids an ordered surfaces list references (image surfaces
353
+ // and html posters).
338
354
  // Used to keep referenced assets out of eviction's first wave. Assets embedded
339
355
  // by raw URL inside html markup are invisible here — touch-on-serve keeps those
340
356
  // warm instead.
@@ -342,12 +358,14 @@ export function collectAssetIds(surfaces, out) {
342
358
  for (const p of surfaces) {
343
359
  if (p.kind === "image")
344
360
  out.add(p.assetId);
361
+ else if (p.kind === "html" && p.poster)
362
+ out.add(p.poster);
345
363
  }
346
364
  }
347
365
  // The images a draft, a reply or a sent ask carries (all keep the user's
348
- // attachments in otherImages / noteImages).
366
+ // attachments in otherImages / askCommentImages).
349
367
  export function collectAttachedIds(holder, out) {
350
- for (const map of [holder?.otherImages, holder?.noteImages]) {
368
+ for (const map of [holder?.otherImages, holder?.askCommentImages]) {
351
369
  if (!map || typeof map !== "object")
352
370
  continue;
353
371
  for (const ids of Array.isArray(map) ? [map] : Object.values(map)) {