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.
- package/README.md +66 -202
- package/bin/demoData.js +5 -5
- package/bin/initDesign.js +6 -6
- package/bin/mockpit.js +687 -192
- package/bin/poster.js +377 -0
- package/dist/mcp/server.js +110 -19
- package/dist/server/answerAdditions.d.ts +7 -0
- package/dist/server/answerAdditions.js +76 -0
- package/dist/server/apiViews.d.ts +563 -41
- package/dist/server/apiViews.js +223 -74
- package/dist/server/app.d.ts +10 -2
- package/dist/server/app.js +3284 -782
- 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 +2 -1
- package/dist/server/decisionList.d.ts +45 -0
- package/dist/server/decisionList.js +302 -0
- package/dist/server/decisions.d.ts +8 -8
- package/dist/server/decisions.js +18 -17
- package/dist/server/designGuide.d.ts +10 -6
- package/dist/server/designGuide.js +173 -78
- package/dist/server/embeds.d.ts +4 -1
- package/dist/server/embeds.js +57 -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 +38 -16
- package/dist/server/feedbackBatch.js +121 -50
- package/dist/server/fills.d.ts +4 -0
- package/dist/server/fills.js +55 -1
- package/dist/server/guidance.d.ts +23 -0
- package/dist/server/guidance.js +71 -0
- 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 +49 -11
- 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 +8 -1
- package/dist/server/mcpHttp.js +44 -7
- package/dist/server/mcpSpec.d.ts +2 -1
- package/dist/server/mcpSpec.js +172 -50
- package/dist/server/parts.js +9 -1
- package/dist/server/postSurfaces.js +3 -0
- package/dist/server/run.d.ts +1 -1
- package/dist/server/run.js +31 -17
- package/dist/server/runApi.d.ts +3 -3
- package/dist/server/runApi.js +34 -24
- package/dist/server/sqlStore.d.ts +117 -94
- package/dist/server/sqlStore.js +1089 -141
- 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 +252 -69
- package/dist/server/types.js +87 -69
- package/extensions/mockpit.js +245 -65
- package/guide/AGENT_SETUP.md +13 -1
- package/guide/packs/design-craft.md +34 -0
- package/guide/topics/asks.md +117 -69
- package/guide/topics/feedback.md +156 -119
- package/guide/topics/html.md +19 -32
- package/guide/topics/http.md +43 -40
- package/guide/topics/knobs.md +7 -10
- package/guide/topics/scripts.md +24 -35
- package/guide/topics/surfaces.md +50 -41
- package/opencode/mockpit.js +120 -0
- package/opencode/package.json +8 -0
- package/package.json +4 -6
- package/skills/launch-video/SKILL.md +12 -12
- package/skills/mockpit/SKILL.md +35 -23
- package/viewer/dist/index.html +9 -8
package/dist/server/types.js
CHANGED
|
@@ -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
|
|
13
|
-
//
|
|
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
|
|
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
|
-
//
|
|
88
|
-
//
|
|
89
|
-
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;
|
|
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
|
-
|
|
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
|
|
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/
|
|
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 "
|
|
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 || "
|
|
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
|
|
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.
|
|
192
|
-
export const openAsks = (
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
|
|
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
|
|
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(
|
|
228
|
-
const d =
|
|
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.
|
|
250
|
+
return picked || d.others?.[id] !== undefined || d.askComments?.[id] !== undefined;
|
|
236
251
|
};
|
|
237
|
-
return
|
|
252
|
+
return openAsks(design).filter((a) => said(a.id)).length;
|
|
238
253
|
}
|
|
239
|
-
// The viewer's built-in "Which
|
|
240
|
-
//
|
|
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(
|
|
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 (!
|
|
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
|
|
261
|
-
scope: state === null ? "
|
|
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(
|
|
284
|
+
export function summarizeProjects(designs, sessions) {
|
|
270
285
|
const byProject = new Map();
|
|
271
|
-
for (const m of
|
|
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
|
|
306
|
+
const lastDesign = list.reduce((max, m) => (m.updatedAt > max ? m.updatedAt : max), "");
|
|
292
307
|
return {
|
|
293
308
|
name,
|
|
294
|
-
|
|
295
|
-
open: list.reduce((n, m) => n + openCount(m
|
|
296
|
-
lastActiveAt: [
|
|
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 /
|
|
366
|
+
// attachments in otherImages / askCommentImages).
|
|
349
367
|
export function collectAttachedIds(holder, out) {
|
|
350
|
-
for (const map of [holder?.otherImages, holder?.
|
|
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)) {
|