mockpit 2.0.0 → 3.1.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 (90) hide show
  1. package/README.md +59 -223
  2. package/bin/demoData.js +2 -2
  3. package/bin/initDesign.js +17 -22
  4. package/bin/mockpit.js +626 -216
  5. package/bin/poster.js +434 -0
  6. package/dist/mcp/server.js +127 -29
  7. package/dist/server/answerAdditions.d.ts +7 -0
  8. package/dist/server/answerAdditions.js +76 -0
  9. package/dist/server/apiViews.d.ts +627 -385
  10. package/dist/server/apiViews.js +198 -81
  11. package/dist/server/app.d.ts +7 -2
  12. package/dist/server/app.js +3514 -984
  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 +640 -0
  18. package/dist/server/codeWorker.js +2 -2
  19. package/dist/server/contrast.d.ts +4 -0
  20. package/dist/server/contrast.js +157 -0
  21. package/dist/server/decisionList.d.ts +48 -0
  22. package/dist/server/decisionList.js +340 -0
  23. package/dist/server/decisions.d.ts +4 -4
  24. package/dist/server/decisions.js +9 -9
  25. package/dist/server/designGuide.d.ts +14 -7
  26. package/dist/server/designGuide.js +314 -88
  27. package/dist/server/embeds.d.ts +3 -1
  28. package/dist/server/embeds.js +31 -6
  29. package/dist/server/eventLog.d.ts +2 -2
  30. package/dist/server/eventLog.js +11 -11
  31. package/dist/server/events.d.ts +15 -6
  32. package/dist/server/feedbackBatch.d.ts +39 -13
  33. package/dist/server/feedbackBatch.js +129 -47
  34. package/dist/server/fills.d.ts +4 -0
  35. package/dist/server/fills.js +55 -1
  36. package/dist/server/guidance.d.ts +1 -1
  37. package/dist/server/guidance.js +2 -2
  38. package/dist/server/handoff.d.ts +4 -1
  39. package/dist/server/handoff.js +8 -2
  40. package/dist/server/icons.js +1 -1
  41. package/dist/server/index.js +39 -10
  42. package/dist/server/jump.d.ts +21 -0
  43. package/dist/server/jump.js +175 -0
  44. package/dist/server/kits.d.ts +2 -2
  45. package/dist/server/kits.js +54 -1
  46. package/dist/server/knobs.d.ts +3 -3
  47. package/dist/server/knobs.js +6 -6
  48. package/dist/server/mcpHttp.d.ts +7 -1
  49. package/dist/server/mcpHttp.js +29 -12
  50. package/dist/server/mcpSpec.d.ts +27 -27
  51. package/dist/server/mcpSpec.js +196 -56
  52. package/dist/server/parts.js +9 -1
  53. package/dist/server/run.d.ts +1 -1
  54. package/dist/server/run.js +47 -17
  55. package/dist/server/runApi.d.ts +3 -3
  56. package/dist/server/runApi.js +34 -21
  57. package/dist/server/sqlStore.d.ts +148 -103
  58. package/dist/server/sqlStore.js +1052 -181
  59. package/dist/server/standings.d.ts +25 -0
  60. package/dist/server/standings.js +113 -0
  61. package/dist/server/surfacePage.d.ts +8 -9
  62. package/dist/server/surfacePage.js +423 -100
  63. package/dist/server/system.d.ts +234 -0
  64. package/dist/server/system.js +1508 -0
  65. package/dist/server/systemChecks.d.ts +15 -0
  66. package/dist/server/systemChecks.js +224 -0
  67. package/dist/server/systemSpecimen.d.ts +2 -0
  68. package/dist/server/systemSpecimen.js +49 -0
  69. package/dist/server/themes.d.ts +9 -7
  70. package/dist/server/themes.js +43 -90
  71. package/dist/server/types.d.ts +266 -93
  72. package/dist/server/types.js +76 -79
  73. package/extensions/mockpit.js +89 -72
  74. package/guide/AGENT_SETUP.md +15 -1
  75. package/guide/packs/design-craft.md +2 -3
  76. package/guide/packs/foundations.md +35 -0
  77. package/guide/topics/asks.md +108 -63
  78. package/guide/topics/feedback.md +129 -120
  79. package/guide/topics/html.md +18 -25
  80. package/guide/topics/http.md +43 -41
  81. package/guide/topics/knobs.md +7 -10
  82. package/guide/topics/scripts.md +22 -33
  83. package/guide/topics/surfaces.md +39 -48
  84. package/guide/topics/system.md +157 -0
  85. package/opencode/mockpit.js +120 -0
  86. package/opencode/package.json +8 -0
  87. package/package.json +15 -14
  88. package/skills/launch-video/SKILL.md +11 -11
  89. package/skills/mockpit/SKILL.md +33 -26
  90. package/viewer/dist/index.html +10 -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) {
@@ -65,7 +94,7 @@ export function postAtVersion(post, version) {
65
94
  const old = post.history.find((h) => h.version === version);
66
95
  if (!old)
67
96
  return null;
68
- const { from: _f, prompt: _p, author: _a, ...rest } = post;
97
+ const { from: _f, prompt: _p, author: _a, contract: _c, system: _s, ...rest } = post;
69
98
  return {
70
99
  ...rest,
71
100
  version: old.version,
@@ -75,9 +104,11 @@ export function postAtVersion(post, version) {
75
104
  ...(old.from === undefined ? {} : { from: old.from }),
76
105
  ...(old.prompt === undefined ? {} : { prompt: old.prompt }),
77
106
  ...(old.author === undefined ? {} : { author: old.author }),
107
+ ...(old.contract === undefined ? {} : { contract: old.contract }),
108
+ ...(old.system === undefined ? {} : { system: old.system }),
78
109
  };
79
110
  }
80
- // The variant's newest version not after mock version `n` — what the stage
111
+ // The variant's newest version not after design version `n` — what the stage
81
112
  // showed when a draft or reply was bound to n (the viewer's frameVersion).
82
113
  export function versionAt(post, n) {
83
114
  const versions = [post.version, ...post.history.map((h) => h.version)];
@@ -85,12 +116,16 @@ export function versionAt(post, n) {
85
116
  return fit.length ? Math.max(...fit) : Math.min(...versions);
86
117
  }
87
118
  // 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;
119
+ // design's first (`ask` accepts version options without a state only while the
120
+ // design has one).
121
+ export const optionState = (ask, design) => ask.state ?? design.states[0] ?? null;
91
122
  // The viewer's write-in option on every ask. Reserved, so a delivered
92
123
  // `{id: "other", other: true}` choice never shadows an option the agent declared.
93
124
  export const OTHER_ID = "other";
125
+ // As far as a block's text is carried: the frame clips to it, the host and the
126
+ // server re-clip, so it also bounds what one comment can repeat back to an agent.
127
+ export const PARAGRAPH_TEXT_MAX = 600;
128
+ export const PARAGRAPHS_MAX = 500;
94
129
  // A one-tap verdict on a part (or, with part null, on the variant itself),
95
130
  // for a reviewer on a phone who would rather not type: like, dislike, or
96
131
  // "more like this". Rides as a part comment, so it travels in the one reply.
@@ -98,23 +133,13 @@ export const REACTIONS = ["up", "down", "more"];
98
133
  // The settings key prefix of a project's area order (`areaOrder:<project>`): a
99
134
  // JSON array of area names, the order Home lists them in.
100
135
  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;
136
+ // The user's hand edits to a project's decision list (`decisions:<project>`).
137
+ export const DECISION_EDITS_SETTING = "decisions";
138
+ export const MAX_NOTES = 4000;
139
+ // Declared and not drawn yet: a screen or component the agent planned, which its next
140
+ // publish fills. An undeclared design with nothing in it is no plan, only empty.
141
+ export const isPlanned = (design, variants) => design.kind !== "idea" && variants === 0;
142
+ export const sameScreen = (a, b) => a.design === b.design && a.state === b.state;
118
143
  export const HISTORY_LIMIT = 20;
119
144
  // "user" is the reserved trust label for genuine human comments. A session
120
145
  // agent with that name could otherwise have its programmatic comments delivered
@@ -134,7 +159,7 @@ export function stripNul(s) {
134
159
  // budget the store evicts down to. One Durable Object holds the whole workspace, so
135
160
  // the budget sits well under its ~10 GB SQLite ceiling.
136
161
  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
162
+ // What the user may attach to a write-in or a comment: raster formats a model can
138
163
  // read. SVG is active content, so it is never attachable.
139
164
  export const ATTACHABLE_IMAGE_TYPES = new Set([
140
165
  "image/png",
@@ -146,7 +171,7 @@ export const MAX_ASK_IMAGES = 6;
146
171
  export const MAX_WORKSPACE_ASSET_BYTES = 2 * 1024 * 1024 * 1024;
147
172
  // Short, unguessable id: 8 random bytes (64 bits) as 11 url-safe base64 chars.
148
173
  // 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
174
+ // `/api/designs/:id` are reachable without the workspace token, so the id IS the
150
175
  // share secret and must resist enumeration. btoa is a global in both Node and
151
176
  // Workers.
152
177
  export const newId = () => {
@@ -178,7 +203,7 @@ export function normalizeSurfaceIds(surfaces) {
178
203
  // Fallback project for sessions that never declared one (bare `curl` publishes).
179
204
  export const DEFAULT_PROJECT = "workspace";
180
205
  export const DEFAULT_VARIANT = "default";
181
- // Stable, url-safe slug derived from a title. Empty input yields "mock" so a
206
+ // Stable, url-safe slug derived from a title. Empty input yields "design" so a
182
207
  // slug is never the empty string.
183
208
  export function slugify(input) {
184
209
  const slug = input
@@ -189,7 +214,7 @@ export function slugify(input) {
189
214
  .replace(/^-+|-+$/g, "")
190
215
  .slice(0, 60)
191
216
  .replace(/-+$/g, "");
192
- return slug || "mock";
217
+ return slug || "design";
193
218
  }
194
219
  // Last path segment of a working directory, for sessions that never declared a
195
220
  // project. Handles both separators so a Windows cwd resolves the same way.
@@ -206,68 +231,40 @@ export const htmlSurface = (html, kits) => ({
206
231
  ? { kits: kits.filter((k) => typeof k === "string") }
207
232
  : {}),
208
233
  });
209
- // An open ask is one no reply has answered yet: a write-in or a note alone
234
+ // An open ask is one no reply has answered yet: a write-in or a comment alone
210
235
  // 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);
236
+ export const askAnswered = (a) => a.answer !== undefined || a.other !== undefined || a.comment !== undefined;
237
+ export const openAsks = (design) => design.asks.filter((a) => !askAnswered(a) && !a.dismissed);
238
+ // Questions an agent asked that wait on the user. The built-in "Which variant?"
239
+ // is not one: no agent is waiting on it, and being shown several designs owes
240
+ // nobody an answer.
241
+ export const openCount = (design) => openAsks(design).length;
244
242
  // 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";
243
+ // pick, a write-in or a comment), so Home can show a started design as "draft 2/4";
246
244
  // 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;
245
+ export function draftedCount(design) {
246
+ const d = design.draft;
249
247
  if (!d)
250
248
  return null;
251
- const open = [...openAsks(mock).map((a) => a.id), ...builtinAskIds(mock, posts, mock.id)];
252
249
  const said = (id) => {
253
250
  const a = d.answers[id];
254
251
  const picked = Array.isArray(a) ? a.length > 0 : a !== undefined;
255
- return picked || d.others?.[id] !== undefined || d.notes?.[id] !== undefined;
252
+ return picked || d.others?.[id] !== undefined || d.askComments?.[id] !== undefined;
256
253
  };
257
- return open.filter(said).length;
254
+ return openAsks(design).filter((a) => said(a.id)).length;
258
255
  }
259
256
  // 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
257
+ // design-wide, `variant:<state>` per state. Never stored; synthesized from the
261
258
  // variants so a reply answering it validates, flips and reads like any ask. An
262
259
  // option's id is the variant name, so the viewer needs no id mapping.
263
260
  export const BUILTIN_ASK_ID = "variant";
264
- export function builtinAsk(mock, posts, id) {
261
+ export function builtinAsk(design, posts, id) {
265
262
  let state = null;
266
263
  if (id !== BUILTIN_ASK_ID) {
267
264
  if (!id.startsWith(`${BUILTIN_ASK_ID}:`))
268
265
  return undefined;
269
266
  state = id.slice(BUILTIN_ASK_ID.length + 1);
270
- if (!mock.states.includes(state))
267
+ if (!design.states.includes(state))
271
268
  return undefined;
272
269
  }
273
270
  const names = [
@@ -278,7 +275,7 @@ export function builtinAsk(mock, posts, id) {
278
275
  return {
279
276
  id,
280
277
  text: "Which variant?",
281
- scope: state === null ? "mock" : "state",
278
+ scope: state === null ? "design" : "state",
282
279
  ...(state === null ? {} : { state }),
283
280
  options: names.map((v) => ({ id: v, label: v, variant: v })),
284
281
  at: "",
@@ -286,9 +283,9 @@ export function builtinAsk(mock, posts, id) {
286
283
  }
287
284
  // Per-project rollup for the projects list. Pure, so every store agrees on
288
285
  // what "open" and "lastActiveAt" mean.
289
- export function summarizeProjects(mocks, sessions, posts = []) {
286
+ export function summarizeProjects(designs, sessions) {
290
287
  const byProject = new Map();
291
- for (const m of mocks) {
288
+ for (const m of designs) {
292
289
  const list = byProject.get(m.project);
293
290
  if (list)
294
291
  list.push(m);
@@ -308,12 +305,12 @@ export function summarizeProjects(mocks, sessions, posts = []) {
308
305
  return [...names]
309
306
  .map((name) => {
310
307
  const list = byProject.get(name) ?? [];
311
- const lastMock = list.reduce((max, m) => (m.updatedAt > max ? m.updatedAt : max), "");
308
+ const lastDesign = list.reduce((max, m) => (m.updatedAt > max ? m.updatedAt : max), "");
312
309
  return {
313
310
  name,
314
- mocks: list.length,
315
- open: list.reduce((n, m) => n + openCount(m, posts), 0),
316
- lastActiveAt: [lastMock, sessionActive.get(name) ?? ""].sort().pop() ?? "",
311
+ designs: list.length,
312
+ open: list.reduce((n, m) => n + openCount(m), 0),
313
+ lastActiveAt: [lastDesign, sessionActive.get(name) ?? ""].sort().pop() ?? "",
317
314
  sessions: sessionCounts.get(name) ?? 0,
318
315
  };
319
316
  })
@@ -368,9 +365,9 @@ export function collectAssetIds(surfaces, out) {
368
365
  }
369
366
  }
370
367
  // The images a draft, a reply or a sent ask carries (all keep the user's
371
- // attachments in otherImages / noteImages).
368
+ // attachments in otherImages / askCommentImages).
372
369
  export function collectAttachedIds(holder, out) {
373
- for (const map of [holder?.otherImages, holder?.noteImages]) {
370
+ for (const map of [holder?.otherImages, holder?.askCommentImages]) {
374
371
  if (!map || typeof map !== "object")
375
372
  continue;
376
373
  for (const ids of Array.isArray(map) ? [map] : Object.values(map)) {