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.
- package/README.md +60 -212
- package/bin/demoData.js +2 -2
- package/bin/initDesign.js +6 -6
- package/bin/mockpit.js +407 -187
- package/bin/poster.js +377 -0
- package/dist/mcp/server.js +80 -24
- package/dist/server/answerAdditions.d.ts +7 -0
- package/dist/server/answerAdditions.js +76 -0
- package/dist/server/apiViews.d.ts +289 -52
- package/dist/server/apiViews.js +189 -81
- package/dist/server/app.d.ts +7 -2
- package/dist/server/app.js +2845 -832
- package/dist/server/appIcon.js +15 -5
- package/dist/server/appIconPng.js +3 -3
- package/dist/server/askNudges.js +1 -1
- package/dist/server/cdnInline.d.ts +8 -0
- package/dist/server/cdnInline.js +638 -0
- package/dist/server/codeWorker.js +1 -1
- package/dist/server/decisionList.d.ts +45 -0
- package/dist/server/decisionList.js +302 -0
- package/dist/server/decisions.d.ts +4 -4
- package/dist/server/decisions.js +9 -9
- package/dist/server/designGuide.d.ts +5 -5
- package/dist/server/designGuide.js +128 -67
- package/dist/server/embeds.d.ts +3 -1
- package/dist/server/embeds.js +31 -6
- package/dist/server/eventLog.d.ts +2 -2
- package/dist/server/eventLog.js +11 -11
- package/dist/server/events.d.ts +9 -6
- package/dist/server/feedbackBatch.d.ts +35 -13
- package/dist/server/feedbackBatch.js +111 -47
- package/dist/server/fills.d.ts +4 -0
- package/dist/server/fills.js +55 -1
- package/dist/server/handoff.d.ts +4 -1
- package/dist/server/handoff.js +8 -2
- package/dist/server/icons.js +1 -1
- package/dist/server/index.js +39 -10
- package/dist/server/jump.d.ts +21 -0
- package/dist/server/jump.js +175 -0
- package/dist/server/knobs.d.ts +3 -3
- package/dist/server/knobs.js +6 -6
- package/dist/server/mcpHttp.d.ts +5 -1
- package/dist/server/mcpHttp.js +16 -11
- package/dist/server/mcpSpec.js +118 -46
- package/dist/server/parts.js +9 -1
- package/dist/server/run.d.ts +1 -1
- package/dist/server/run.js +25 -16
- package/dist/server/runApi.d.ts +3 -3
- package/dist/server/runApi.js +25 -19
- package/dist/server/sqlStore.d.ts +112 -96
- package/dist/server/sqlStore.js +906 -172
- package/dist/server/standings.d.ts +25 -0
- package/dist/server/standings.js +113 -0
- package/dist/server/surfacePage.d.ts +6 -9
- package/dist/server/surfacePage.js +361 -86
- package/dist/server/types.d.ts +226 -72
- package/dist/server/types.js +73 -78
- package/extensions/mockpit.js +89 -72
- package/guide/AGENT_SETUP.md +13 -1
- package/guide/packs/design-craft.md +2 -3
- package/guide/topics/asks.md +106 -62
- package/guide/topics/feedback.md +126 -119
- package/guide/topics/html.md +17 -24
- package/guide/topics/http.md +41 -41
- package/guide/topics/knobs.md +7 -10
- package/guide/topics/scripts.md +22 -33
- package/guide/topics/surfaces.md +39 -48
- package/opencode/mockpit.js +120 -0
- package/opencode/package.json +8 -0
- package/package.json +4 -3
- package/skills/launch-video/SKILL.md +11 -11
- package/skills/mockpit/SKILL.md +26 -22
- package/viewer/dist/index.html +9 -9
package/dist/server/types.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
//
|
|
89
|
-
//
|
|
90
|
-
export const optionState = (ask,
|
|
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
|
|
102
|
-
export const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
//
|
|
106
|
-
export
|
|
107
|
-
|
|
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
|
|
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/
|
|
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 "
|
|
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 || "
|
|
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
|
|
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.
|
|
212
|
-
export const openAsks = (
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
|
|
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
|
|
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(
|
|
248
|
-
const d =
|
|
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.
|
|
250
|
+
return picked || d.others?.[id] !== undefined || d.askComments?.[id] !== undefined;
|
|
256
251
|
};
|
|
257
|
-
return
|
|
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
|
-
//
|
|
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(
|
|
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 (!
|
|
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 ? "
|
|
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(
|
|
284
|
+
export function summarizeProjects(designs, sessions) {
|
|
290
285
|
const byProject = new Map();
|
|
291
|
-
for (const m of
|
|
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
|
|
306
|
+
const lastDesign = list.reduce((max, m) => (m.updatedAt > max ? m.updatedAt : max), "");
|
|
312
307
|
return {
|
|
313
308
|
name,
|
|
314
|
-
|
|
315
|
-
open: list.reduce((n, m) => n + openCount(m
|
|
316
|
-
lastActiveAt: [
|
|
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 /
|
|
366
|
+
// attachments in otherImages / askCommentImages).
|
|
372
367
|
export function collectAttachedIds(holder, out) {
|
|
373
|
-
for (const map of [holder?.otherImages, holder?.
|
|
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)) {
|