mockpit 2.0.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 (73) hide show
  1. package/README.md +60 -212
  2. package/bin/demoData.js +2 -2
  3. package/bin/initDesign.js +6 -6
  4. package/bin/mockpit.js +407 -187
  5. package/bin/poster.js +377 -0
  6. package/dist/mcp/server.js +80 -24
  7. package/dist/server/answerAdditions.d.ts +7 -0
  8. package/dist/server/answerAdditions.js +76 -0
  9. package/dist/server/apiViews.d.ts +289 -52
  10. package/dist/server/apiViews.js +189 -81
  11. package/dist/server/app.d.ts +7 -2
  12. package/dist/server/app.js +2845 -832
  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 +1 -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 +4 -4
  22. package/dist/server/decisions.js +9 -9
  23. package/dist/server/designGuide.d.ts +5 -5
  24. package/dist/server/designGuide.js +128 -67
  25. package/dist/server/embeds.d.ts +3 -1
  26. package/dist/server/embeds.js +31 -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 +35 -13
  31. package/dist/server/feedbackBatch.js +111 -47
  32. package/dist/server/fills.d.ts +4 -0
  33. package/dist/server/fills.js +55 -1
  34. package/dist/server/handoff.d.ts +4 -1
  35. package/dist/server/handoff.js +8 -2
  36. package/dist/server/icons.js +1 -1
  37. package/dist/server/index.js +39 -10
  38. package/dist/server/jump.d.ts +21 -0
  39. package/dist/server/jump.js +175 -0
  40. package/dist/server/knobs.d.ts +3 -3
  41. package/dist/server/knobs.js +6 -6
  42. package/dist/server/mcpHttp.d.ts +5 -1
  43. package/dist/server/mcpHttp.js +16 -11
  44. package/dist/server/mcpSpec.js +118 -46
  45. package/dist/server/parts.js +9 -1
  46. package/dist/server/run.d.ts +1 -1
  47. package/dist/server/run.js +25 -16
  48. package/dist/server/runApi.d.ts +3 -3
  49. package/dist/server/runApi.js +25 -19
  50. package/dist/server/sqlStore.d.ts +112 -96
  51. package/dist/server/sqlStore.js +906 -172
  52. package/dist/server/standings.d.ts +25 -0
  53. package/dist/server/standings.js +113 -0
  54. package/dist/server/surfacePage.d.ts +6 -9
  55. package/dist/server/surfacePage.js +361 -86
  56. package/dist/server/types.d.ts +226 -72
  57. package/dist/server/types.js +73 -78
  58. package/extensions/mockpit.js +89 -72
  59. package/guide/AGENT_SETUP.md +13 -1
  60. package/guide/packs/design-craft.md +2 -3
  61. package/guide/topics/asks.md +106 -62
  62. package/guide/topics/feedback.md +126 -119
  63. package/guide/topics/html.md +17 -24
  64. package/guide/topics/http.md +41 -41
  65. package/guide/topics/knobs.md +7 -10
  66. package/guide/topics/scripts.md +22 -33
  67. package/guide/topics/surfaces.md +39 -48
  68. package/opencode/mockpit.js +120 -0
  69. package/opencode/package.json +8 -0
  70. package/package.json +4 -3
  71. package/skills/launch-video/SKILL.md +11 -11
  72. package/skills/mockpit/SKILL.md +26 -22
  73. package/viewer/dist/index.html +9 -9
@@ -50,13 +50,42 @@ export function isSurfaceKind(kind) {
50
50
  export function isSandboxedSurfaceKind(kind) {
51
51
  return isSurfaceKind(kind) && SURFACE_KIND_METADATA[kind].sandboxed;
52
52
  }
53
- export const isMockKind = (k) => k === "page" || k === "component" || k === "sketch";
53
+ export const isDesignKind = (k) => k === "screen" || k === "component" || k === "idea";
54
54
  export const variantStatus = (status) => status === "accepted" ? "inUse" : status;
55
55
  // Still in the running: what the user can choose between.
56
56
  export const isLive = (status) => status === "open" || status === "accepted" || status === "inUse";
57
57
  // The version a variant is resolved at when nothing names one: the version in
58
58
  // use when accepted, else its latest.
59
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
+ };
60
89
  // A variant as it stood at one of its versions (current or retained), or null
61
90
  // when that version is gone.
62
91
  export function postAtVersion(post, version) {
@@ -77,7 +106,7 @@ export function postAtVersion(post, version) {
77
106
  ...(old.author === undefined ? {} : { author: old.author }),
78
107
  };
79
108
  }
80
- // 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
81
110
  // showed when a draft or reply was bound to n (the viewer's frameVersion).
82
111
  export function versionAt(post, n) {
83
112
  const versions = [post.version, ...post.history.map((h) => h.version)];
@@ -85,12 +114,16 @@ export function versionAt(post, n) {
85
114
  return fit.length ? Math.max(...fit) : Math.min(...versions);
86
115
  }
87
116
  // The state a version option names a version in: the ask's state, else the
88
- // mock's first (`ask` accepts version options without a state only while the
89
- // mock has one).
90
- 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;
91
120
  // The viewer's write-in option on every ask. Reserved, so a delivered
92
121
  // `{id: "other", other: true}` choice never shadows an option the agent declared.
93
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;
94
127
  // A one-tap verdict on a part (or, with part null, on the variant itself),
95
128
  // for a reviewer on a phone who would rather not type: like, dislike, or
96
129
  // "more like this". Rides as a part comment, so it travels in the one reply.
@@ -98,23 +131,13 @@ export const REACTIONS = ["up", "down", "more"];
98
131
  // The settings key prefix of a project's area order (`areaOrder:<project>`): a
99
132
  // JSON array of area names, the order Home lists them in.
100
133
  export const AREA_ORDER_SETTING = "areaOrder";
101
- // The state's own render, when a state has situations.
102
- export const NORMAL_SITUATION = "Normal";
103
- // The state a situation is stored as: what feedback, decisions and export name.
104
- export const situationKey = (state, situation) => `${state} · ${situation}`;
105
- // The state a situation belongs to, or the state itself.
106
- export function momentOf(situations, state) {
107
- if (state === null)
108
- return null;
109
- for (const [moment, names] of Object.entries(situations)) {
110
- if (names.some((n) => situationKey(moment, n) === state))
111
- return moment;
112
- }
113
- return state;
114
- }
115
- // The states that are moments of the mock, not situations of one.
116
- export const momentsOf = (states, situations) => states.filter((st) => momentOf(situations, st) === st);
117
- export const sameScreen = (a, b) => a.mock === b.mock && a.state === b.state;
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;
118
141
  export const HISTORY_LIMIT = 20;
119
142
  // "user" is the reserved trust label for genuine human comments. A session
120
143
  // agent with that name could otherwise have its programmatic comments delivered
@@ -134,7 +157,7 @@ export function stripNul(s) {
134
157
  // budget the store evicts down to. One Durable Object holds the whole workspace, so
135
158
  // the budget sits well under its ~10 GB SQLite ceiling.
136
159
  export const MAX_ASSET_BYTES = 5 * 1024 * 1024;
137
- // 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
138
161
  // read. SVG is active content, so it is never attachable.
139
162
  export const ATTACHABLE_IMAGE_TYPES = new Set([
140
163
  "image/png",
@@ -146,7 +169,7 @@ export const MAX_ASK_IMAGES = 6;
146
169
  export const MAX_WORKSPACE_ASSET_BYTES = 2 * 1024 * 1024 * 1024;
147
170
  // Short, unguessable id: 8 random bytes (64 bits) as 11 url-safe base64 chars.
148
171
  // These double as bearer capabilities: in publicRead mode `/s/:id` and
149
- // `/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
150
173
  // share secret and must resist enumeration. btoa is a global in both Node and
151
174
  // Workers.
152
175
  export const newId = () => {
@@ -178,7 +201,7 @@ export function normalizeSurfaceIds(surfaces) {
178
201
  // Fallback project for sessions that never declared one (bare `curl` publishes).
179
202
  export const DEFAULT_PROJECT = "workspace";
180
203
  export const DEFAULT_VARIANT = "default";
181
- // 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
182
205
  // slug is never the empty string.
183
206
  export function slugify(input) {
184
207
  const slug = input
@@ -189,7 +212,7 @@ export function slugify(input) {
189
212
  .replace(/^-+|-+$/g, "")
190
213
  .slice(0, 60)
191
214
  .replace(/-+$/g, "");
192
- return slug || "mock";
215
+ return slug || "design";
193
216
  }
194
217
  // Last path segment of a working directory, for sessions that never declared a
195
218
  // project. Handles both separators so a Windows cwd resolves the same way.
@@ -206,68 +229,40 @@ export const htmlSurface = (html, kits) => ({
206
229
  ? { kits: kits.filter((k) => typeof k === "string") }
207
230
  : {}),
208
231
  });
209
- // 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
210
233
  // answers it too ("Neither, because…").
211
- export const askAnswered = (a) => a.answer !== undefined || a.other !== undefined || a.note !== undefined;
212
- export const openAsks = (mock) => mock.asks.filter((a) => !askAnswered(a) && !a.dismissed);
213
- // How many built-in "Which variant?" questions the viewer shows: one mock-wide when every
214
- // state needs one and the live names line up, else one per needing state. Same
215
- // rule as viewer/src/logic.ts builtinAsks, so Home, `read` and `pending` count
216
- // the question the user actually sees. Answering it archives the losers, which
217
- // is what makes it stop counting.
218
- export function builtinAskIds(mock, posts, mockId) {
219
- // Closing the mock closed the choice along with every other open question.
220
- if (mock.closedAt)
221
- return [];
222
- const live = (state) => posts
223
- .filter((p) => p.mock === mockId && p.state === state && isLive(p.status))
224
- .map((p) => p.variant);
225
- const needs = (state) => {
226
- const names = live(state);
227
- if (names.length < 2)
228
- return false;
229
- return !mock.asks.some((a) => (a.scope === "mock" || (a.scope === "state" && a.state === state)) &&
230
- names.every((n) => a.options.some((o) => o.variant === n)));
231
- };
232
- const states = mock.states.length ? mock.states : [null];
233
- const needing = states.filter(needs);
234
- if (!needing.length)
235
- return [];
236
- const key = (state) => [...live(state)].sort().join("\n");
237
- const first = key(needing[0]);
238
- return needing.length === states.length && states.every((st) => key(st) === first)
239
- ? [BUILTIN_ASK_ID]
240
- : needing.map((st) => (st === null ? BUILTIN_ASK_ID : `${BUILTIN_ASK_ID}:${st}`));
241
- }
242
- export const builtinAskCount = (mock, posts, mockId) => builtinAskIds(mock, posts, mockId).length;
243
- 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;
244
240
  // How many of the open asks the user's unsent draft already speaks for (a
245
- // 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";
246
242
  // null when there is no draft. A count only: the draft's content stays private.
247
- export function draftedCount(mock, posts) {
248
- const d = mock.draft;
243
+ export function draftedCount(design) {
244
+ const d = design.draft;
249
245
  if (!d)
250
246
  return null;
251
- const open = [...openAsks(mock).map((a) => a.id), ...builtinAskIds(mock, posts, mock.id)];
252
247
  const said = (id) => {
253
248
  const a = d.answers[id];
254
249
  const picked = Array.isArray(a) ? a.length > 0 : a !== undefined;
255
- return picked || d.others?.[id] !== undefined || d.notes?.[id] !== undefined;
250
+ return picked || d.others?.[id] !== undefined || d.askComments?.[id] !== undefined;
256
251
  };
257
- return open.filter(said).length;
252
+ return openAsks(design).filter((a) => said(a.id)).length;
258
253
  }
259
254
  // The viewer's built-in "Which variant?" for variants no agent ask binds: `variant`
260
- // mock-wide, `variant:<state>` per state. Never stored; synthesized from the
255
+ // design-wide, `variant:<state>` per state. Never stored; synthesized from the
261
256
  // variants so a reply answering it validates, flips and reads like any ask. An
262
257
  // option's id is the variant name, so the viewer needs no id mapping.
263
258
  export const BUILTIN_ASK_ID = "variant";
264
- export function builtinAsk(mock, posts, id) {
259
+ export function builtinAsk(design, posts, id) {
265
260
  let state = null;
266
261
  if (id !== BUILTIN_ASK_ID) {
267
262
  if (!id.startsWith(`${BUILTIN_ASK_ID}:`))
268
263
  return undefined;
269
264
  state = id.slice(BUILTIN_ASK_ID.length + 1);
270
- if (!mock.states.includes(state))
265
+ if (!design.states.includes(state))
271
266
  return undefined;
272
267
  }
273
268
  const names = [
@@ -278,7 +273,7 @@ export function builtinAsk(mock, posts, id) {
278
273
  return {
279
274
  id,
280
275
  text: "Which variant?",
281
- scope: state === null ? "mock" : "state",
276
+ scope: state === null ? "design" : "state",
282
277
  ...(state === null ? {} : { state }),
283
278
  options: names.map((v) => ({ id: v, label: v, variant: v })),
284
279
  at: "",
@@ -286,9 +281,9 @@ export function builtinAsk(mock, posts, id) {
286
281
  }
287
282
  // Per-project rollup for the projects list. Pure, so every store agrees on
288
283
  // what "open" and "lastActiveAt" mean.
289
- export function summarizeProjects(mocks, sessions, posts = []) {
284
+ export function summarizeProjects(designs, sessions) {
290
285
  const byProject = new Map();
291
- for (const m of mocks) {
286
+ for (const m of designs) {
292
287
  const list = byProject.get(m.project);
293
288
  if (list)
294
289
  list.push(m);
@@ -308,12 +303,12 @@ export function summarizeProjects(mocks, sessions, posts = []) {
308
303
  return [...names]
309
304
  .map((name) => {
310
305
  const list = byProject.get(name) ?? [];
311
- 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), "");
312
307
  return {
313
308
  name,
314
- mocks: list.length,
315
- open: list.reduce((n, m) => n + openCount(m, posts), 0),
316
- 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() ?? "",
317
312
  sessions: sessionCounts.get(name) ?? 0,
318
313
  };
319
314
  })
@@ -368,9 +363,9 @@ export function collectAssetIds(surfaces, out) {
368
363
  }
369
364
  }
370
365
  // The images a draft, a reply or a sent ask carries (all keep the user's
371
- // attachments in otherImages / noteImages).
366
+ // attachments in otherImages / askCommentImages).
372
367
  export function collectAttachedIds(holder, out) {
373
- for (const map of [holder?.otherImages, holder?.noteImages]) {
368
+ for (const map of [holder?.otherImages, holder?.askCommentImages]) {
374
369
  if (!map || typeof map !== "object")
375
370
  continue;
376
371
  for (const ids of Array.isArray(map) ? [map] : Object.values(map)) {