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.
- package/README.md +59 -223
- package/bin/demoData.js +2 -2
- package/bin/initDesign.js +17 -22
- package/bin/mockpit.js +626 -216
- package/bin/poster.js +434 -0
- package/dist/mcp/server.js +127 -29
- package/dist/server/answerAdditions.d.ts +7 -0
- package/dist/server/answerAdditions.js +76 -0
- package/dist/server/apiViews.d.ts +627 -385
- package/dist/server/apiViews.js +198 -81
- package/dist/server/app.d.ts +7 -2
- package/dist/server/app.js +3514 -984
- 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 +640 -0
- package/dist/server/codeWorker.js +2 -2
- package/dist/server/contrast.d.ts +4 -0
- package/dist/server/contrast.js +157 -0
- package/dist/server/decisionList.d.ts +48 -0
- package/dist/server/decisionList.js +340 -0
- package/dist/server/decisions.d.ts +4 -4
- package/dist/server/decisions.js +9 -9
- package/dist/server/designGuide.d.ts +14 -7
- package/dist/server/designGuide.js +314 -88
- 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 +15 -6
- package/dist/server/feedbackBatch.d.ts +39 -13
- package/dist/server/feedbackBatch.js +129 -47
- package/dist/server/fills.d.ts +4 -0
- package/dist/server/fills.js +55 -1
- package/dist/server/guidance.d.ts +1 -1
- package/dist/server/guidance.js +2 -2
- 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/kits.d.ts +2 -2
- package/dist/server/kits.js +54 -1
- package/dist/server/knobs.d.ts +3 -3
- package/dist/server/knobs.js +6 -6
- package/dist/server/mcpHttp.d.ts +7 -1
- package/dist/server/mcpHttp.js +29 -12
- package/dist/server/mcpSpec.d.ts +27 -27
- package/dist/server/mcpSpec.js +196 -56
- package/dist/server/parts.js +9 -1
- package/dist/server/run.d.ts +1 -1
- package/dist/server/run.js +47 -17
- package/dist/server/runApi.d.ts +3 -3
- package/dist/server/runApi.js +34 -21
- package/dist/server/sqlStore.d.ts +148 -103
- package/dist/server/sqlStore.js +1052 -181
- package/dist/server/standings.d.ts +25 -0
- package/dist/server/standings.js +113 -0
- package/dist/server/surfacePage.d.ts +8 -9
- package/dist/server/surfacePage.js +423 -100
- package/dist/server/system.d.ts +234 -0
- package/dist/server/system.js +1508 -0
- package/dist/server/systemChecks.d.ts +15 -0
- package/dist/server/systemChecks.js +224 -0
- package/dist/server/systemSpecimen.d.ts +2 -0
- package/dist/server/systemSpecimen.js +49 -0
- package/dist/server/themes.d.ts +9 -7
- package/dist/server/themes.js +43 -90
- package/dist/server/types.d.ts +266 -93
- package/dist/server/types.js +76 -79
- package/extensions/mockpit.js +89 -72
- package/guide/AGENT_SETUP.md +15 -1
- package/guide/packs/design-craft.md +2 -3
- package/guide/packs/foundations.md +35 -0
- package/guide/topics/asks.md +108 -63
- package/guide/topics/feedback.md +129 -120
- package/guide/topics/html.md +18 -25
- package/guide/topics/http.md +43 -41
- package/guide/topics/knobs.md +7 -10
- package/guide/topics/scripts.md +22 -33
- package/guide/topics/surfaces.md +39 -48
- package/guide/topics/system.md +157 -0
- package/opencode/mockpit.js +120 -0
- package/opencode/package.json +8 -0
- package/package.json +15 -14
- package/skills/launch-video/SKILL.md +11 -11
- package/skills/mockpit/SKILL.md +33 -26
- package/viewer/dist/index.html +10 -9
package/dist/server/guidance.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
// reads is the one that wins.
|
|
6
6
|
//
|
|
7
7
|
// Runtime-agnostic (no node imports): the Worker builds the brief too.
|
|
8
|
-
export const GUIDE_PACKS = ["design-craft"];
|
|
8
|
+
export const GUIDE_PACKS = ["design-craft", "foundations"];
|
|
9
9
|
const isGuidePack = (id) => GUIDE_PACKS.includes(id);
|
|
10
|
-
// `rules:<project>`
|
|
10
|
+
// `rules:<project>` is a project setting and moves with a project
|
|
11
11
|
// rename. The user's own rules are not a project, so their key has no colon:
|
|
12
12
|
// a project named "user" can't collide with it.
|
|
13
13
|
export const RULES_SETTING = "rules";
|
package/dist/server/handoff.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Comment, Session } from "./types.ts";
|
|
2
2
|
export declare const HANDOFF_GRACE_MS: number;
|
|
3
|
-
export declare const HANDOFF_NOTE = "Sent to an earlier agent session in this project that has gone quiet; it is yours now. Read the
|
|
3
|
+
export declare const HANDOFF_NOTE = "Sent to an earlier agent session in this project that has gone quiet; it is yours now. Read the design before acting.";
|
|
4
|
+
export declare const ROUTED_NOTE = "The agent that asked this wasn't listening, so the user sent it to you; it is yours now. Read the design before acting.";
|
|
5
|
+
export declare const OWNER_BUSY_MS: number;
|
|
4
6
|
export declare const ownerActiveAt: (s: Session) => string;
|
|
7
|
+
export declare const sessionName: (s: Session) => string;
|
|
5
8
|
export declare function supersedes(taker: Session, owner: Session, listening: boolean): boolean;
|
|
6
9
|
export declare function handoffReady(comment: Comment, owner: Session, taker: Session, now: number): boolean;
|
package/dist/server/handoff.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Orphaned replies: a Send lands on the session that owns the
|
|
1
|
+
// Orphaned replies: a Send lands on the session that owns the design, and when
|
|
2
2
|
// that agent restarts into a new session (a new conversation, /clear, a crash)
|
|
3
3
|
// nobody would ever read it. The rule below decides when a newer session may
|
|
4
4
|
// take such a reply instead. Runtime-agnostic (no node imports).
|
|
@@ -20,8 +20,14 @@
|
|
|
20
20
|
// it got before the taker existed goes at once — the user answered, then
|
|
21
21
|
// restarted. A keyed owner always gets the grace: its conversation may resume.
|
|
22
22
|
export const HANDOFF_GRACE_MS = 2 * 60_000;
|
|
23
|
-
export const HANDOFF_NOTE = "Sent to an earlier agent session in this project that has gone quiet; it is yours now. Read the
|
|
23
|
+
export const HANDOFF_NOTE = "Sent to an earlier agent session in this project that has gone quiet; it is yours now. Read the design before acting.";
|
|
24
|
+
export const ROUTED_NOTE = "The agent that asked this wasn't listening, so the user sent it to you; it is yours now. Read the design before acting.";
|
|
25
|
+
// A waiter stops between a wake and the end of that turn, so an owner that called
|
|
26
|
+
// this recently is working, not gone, and keeps its replies. Same span as "Agent working".
|
|
27
|
+
export const OWNER_BUSY_MS = 5 * 60_000;
|
|
24
28
|
export const ownerActiveAt = (s) => s.agentActiveAt ?? s.lastActiveAt;
|
|
29
|
+
// All the user can recognise a session by: most have no title, and ids mean nothing.
|
|
30
|
+
export const sessionName = (s) => s.cwd?.split(/[\\/]/).filter(Boolean).pop() ?? s.agent;
|
|
25
31
|
export function supersedes(taker, owner, listening) {
|
|
26
32
|
return (owner.id !== taker.id &&
|
|
27
33
|
owner.project !== null &&
|
package/dist/server/icons.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// imports): it runs inside renderHtmlPage on the Worker too.
|
|
5
5
|
// Served with the server rather than installed per project: lucide (ISC) is
|
|
6
6
|
// what agents reach for by default, mage (Apache-2.0) is what `mockpit init`
|
|
7
|
-
// used to upload as a sprite, so
|
|
7
|
+
// used to upload as a sprite, so designs written against it keep rendering.
|
|
8
8
|
export const BUNDLED_ICON_PREFIXES = ["lucide", "mage"];
|
|
9
9
|
const NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
10
10
|
export const isIconName = (s) => typeof s === "string" && NAME_RE.test(s);
|
package/dist/server/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { serve } from "@hono/node-server";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { compress } from "hono/compress";
|
|
2
4
|
import { createHash } from "node:crypto";
|
|
3
5
|
import { readdir, readFile } from "node:fs/promises";
|
|
4
|
-
import { homedir } from "node:os";
|
|
6
|
+
import { homedir, networkInterfaces } from "node:os";
|
|
5
7
|
import { basename, dirname, join } from "node:path";
|
|
6
8
|
import { fileURLToPath } from "node:url";
|
|
7
9
|
import { createApp } from "./app.js";
|
|
@@ -40,6 +42,10 @@ for (const name of (await readdir(serverDir)).sort()) {
|
|
|
40
42
|
renderHash.update(await readFile(join(serverDir, name)));
|
|
41
43
|
}
|
|
42
44
|
renderHash.update(await readFile(join(root, "package-lock.json")).catch(() => pkgJson));
|
|
45
|
+
// Browsers keep a version's document for a year, so a version changed outside
|
|
46
|
+
// the API (a direct store edit) stays stale in them; a new salt on restart
|
|
47
|
+
// re-fetches every document without touching code.
|
|
48
|
+
renderHash.update(process.env.MOCKPIT_RENDER_SALT ?? "");
|
|
43
49
|
const renderRev = renderHash.digest("hex").slice(0, 10);
|
|
44
50
|
const pr = process.env.MOCKPIT_PUBLIC_READ;
|
|
45
51
|
const publicRead = pr === "session" || pr === "full" ? pr : undefined;
|
|
@@ -48,7 +54,9 @@ const publicRead = pr === "session" || pr === "full" ? pr : undefined;
|
|
|
48
54
|
// default lives in ~/.mockpit/, a user-owned dir that survives reinstalls and
|
|
49
55
|
// is writable however the package was installed.
|
|
50
56
|
const dbPath = process.env.MOCKPIT_DB ?? join(homedir(), ".mockpit", "mockpit.db");
|
|
51
|
-
|
|
57
|
+
// Far past the Durable Object's budget: one lab of inlined pages can be 70 MB,
|
|
58
|
+
// and a design that doesn't fit is re-read and re-parsed on every request.
|
|
59
|
+
const store = new SqlStore(createSqliteStorage(dbPath), { postCacheBytes: 256 * 1024 * 1024 });
|
|
52
60
|
console.log(`mockpit store: SQLite at ${dbPath}`);
|
|
53
61
|
// The event log is on by default, next to the database, so a session can be
|
|
54
62
|
// diagnosed after the fact without having planned to (`scripts/inspect.ts`).
|
|
@@ -64,6 +72,27 @@ if (logPath)
|
|
|
64
72
|
// database never picks up the real ones.
|
|
65
73
|
const rulesPath = dbPath === ":memory:" ? undefined : join(dirname(dbPath), "rules.md");
|
|
66
74
|
const userRules = rulesPath ? () => readFile(rulesPath, "utf8").catch(() => null) : undefined;
|
|
75
|
+
const port = Number(process.env.PORT ?? 8228);
|
|
76
|
+
// MOCKPIT_HOST (or `serve --host`) restricts the listener to one address.
|
|
77
|
+
// Unset keeps the previous behaviour — node's default, every interface — because
|
|
78
|
+
// that is what a container or a LAN-shared instance needs. Set it to 127.0.0.1
|
|
79
|
+
// when the server shares a host with anything you don't want reaching it; that
|
|
80
|
+
// is stronger than the token, which is a single shared secret by design.
|
|
81
|
+
const hostname = process.env.MOCKPIT_HOST || undefined;
|
|
82
|
+
// The addresses a phone might open this server at. Read when asked, since a laptop
|
|
83
|
+
// changes networks while it runs.
|
|
84
|
+
const networkHosts = () => {
|
|
85
|
+
if (hostname === "127.0.0.1" || hostname === "::1" || hostname === "localhost") {
|
|
86
|
+
return { localOnly: true, hosts: [] };
|
|
87
|
+
}
|
|
88
|
+
if (hostname && hostname !== "0.0.0.0" && hostname !== "::") {
|
|
89
|
+
return { localOnly: false, hosts: [`${hostname}:${port}`] };
|
|
90
|
+
}
|
|
91
|
+
const hosts = Object.values(networkInterfaces())
|
|
92
|
+
.flat()
|
|
93
|
+
.flatMap((a) => (a && a.family === "IPv4" && !a.internal ? [`${a.address}:${port}`] : []));
|
|
94
|
+
return { localOnly: false, hosts: hosts.slice(0, 3) };
|
|
95
|
+
};
|
|
67
96
|
const app = createApp({
|
|
68
97
|
store,
|
|
69
98
|
viewerHtml,
|
|
@@ -80,16 +109,16 @@ const app = createApp({
|
|
|
80
109
|
renderRev,
|
|
81
110
|
// `run` scripts execute in QuickJS inside a worker thread per run.
|
|
82
111
|
executor: createNodeExecutor(),
|
|
112
|
+
networkHosts,
|
|
83
113
|
log: logPath ? createFileLog(logPath) : undefined,
|
|
84
114
|
});
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
serve({ fetch: app.fetch, port, hostname }, (info) => {
|
|
115
|
+
// Cloudflare compresses at its edge; a Node server reached over a network
|
|
116
|
+
// (a tailnet, a LAN) would otherwise send the viewer and every list uncompressed.
|
|
117
|
+
// Hono's filter leaves SSE streams alone.
|
|
118
|
+
const outer = new Hono();
|
|
119
|
+
outer.use(compress());
|
|
120
|
+
outer.all("*", (c) => app.fetch(c.req.raw, c.env));
|
|
121
|
+
serve({ fetch: outer.fetch, port, hostname }, (info) => {
|
|
93
122
|
// Report the address actually bound. Printing "localhost" unconditionally hid
|
|
94
123
|
// the fact that the default listens on every interface.
|
|
95
124
|
const shown = hostname ?? "localhost";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type JumpRows, type DesignKind } from "./types.ts";
|
|
2
|
+
export type JumpMatch = "title" | "state" | "variant" | "area" | "question" | "description";
|
|
3
|
+
export interface JumpHit {
|
|
4
|
+
project: string;
|
|
5
|
+
id: string | null;
|
|
6
|
+
slug: string | null;
|
|
7
|
+
title: string;
|
|
8
|
+
kind: DesignKind | null;
|
|
9
|
+
width: number | null;
|
|
10
|
+
area: string | null;
|
|
11
|
+
match: JumpMatch;
|
|
12
|
+
text: string | null;
|
|
13
|
+
state: string | null;
|
|
14
|
+
variant: string | null;
|
|
15
|
+
archived: boolean;
|
|
16
|
+
open: number;
|
|
17
|
+
designArchived: boolean;
|
|
18
|
+
inUse: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function jumpHits(rows: JumpRows, query: string, project: string | null): JumpHit[];
|
|
21
|
+
export declare function recentHits(rows: JumpRows, ids: string[]): JumpHit[];
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Search to jump: find a design by anything the user might remember it by, its
|
|
2
|
+
// title, a state, a variant's name or note, a question, or its area. It reads
|
|
3
|
+
// what `listJumpRows` hands it, which never includes surfaces, so a big
|
|
4
|
+
// workspace costs its metadata and not its pages. This project comes first, then
|
|
5
|
+
// the others. Runtime-agnostic (no node imports).
|
|
6
|
+
import { isLive, openAsks } from "./types.js";
|
|
7
|
+
const MAX_HERE = 20;
|
|
8
|
+
const MAX_ELSEWHERE = 12;
|
|
9
|
+
// A design whose variants all carry the word shouldn't push every other design off the list.
|
|
10
|
+
const MAX_PER_DESIGN = 3;
|
|
11
|
+
const SNIPPET = 90;
|
|
12
|
+
// Precision before recall: what a design is called beats what was once said about it.
|
|
13
|
+
const RANK = {
|
|
14
|
+
exact: 0,
|
|
15
|
+
prefix: 1,
|
|
16
|
+
word: 2,
|
|
17
|
+
title: 3,
|
|
18
|
+
state: 4,
|
|
19
|
+
variant: 5,
|
|
20
|
+
area: 6,
|
|
21
|
+
question: 7,
|
|
22
|
+
description: 8,
|
|
23
|
+
};
|
|
24
|
+
// "Cafe" finds "Café", and "food" finds "Food".
|
|
25
|
+
const fold = (s) => s.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
|
|
26
|
+
function clip(text, term) {
|
|
27
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
28
|
+
if (flat.length <= SNIPPET)
|
|
29
|
+
return flat;
|
|
30
|
+
const at = Math.max(0, fold(flat).indexOf(term));
|
|
31
|
+
const start = at < SNIPPET / 3 ? 0 : at - Math.floor(SNIPPET / 3);
|
|
32
|
+
const cut = flat.slice(start, start + SNIPPET).trim();
|
|
33
|
+
return `${start > 0 ? "…" : ""}${cut}${start + SNIPPET < flat.length ? "…" : ""}`;
|
|
34
|
+
}
|
|
35
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
36
|
+
function allHits(rows, terms, whole) {
|
|
37
|
+
const byDesign = new Map();
|
|
38
|
+
for (const v of rows.variants) {
|
|
39
|
+
const list = byDesign.get(v.designId);
|
|
40
|
+
if (list)
|
|
41
|
+
list.push(v);
|
|
42
|
+
else
|
|
43
|
+
byDesign.set(v.designId, [v]);
|
|
44
|
+
}
|
|
45
|
+
const has = (text) => {
|
|
46
|
+
const f = fold(text);
|
|
47
|
+
return terms.every((t) => f.includes(t));
|
|
48
|
+
};
|
|
49
|
+
const out = [];
|
|
50
|
+
const areasSeen = new Set();
|
|
51
|
+
for (const m of rows.designs) {
|
|
52
|
+
const variants = byDesign.get(m.id) ?? [];
|
|
53
|
+
const base = {
|
|
54
|
+
project: m.project,
|
|
55
|
+
id: m.id,
|
|
56
|
+
slug: m.slug,
|
|
57
|
+
title: m.title,
|
|
58
|
+
kind: m.kind,
|
|
59
|
+
width: m.widths[0] ?? null,
|
|
60
|
+
area: m.area,
|
|
61
|
+
open: openAsks(m).length,
|
|
62
|
+
designArchived: !!m.archivedAt,
|
|
63
|
+
inUse: variants.some((v) => v.status === "accepted"),
|
|
64
|
+
text: null,
|
|
65
|
+
state: null,
|
|
66
|
+
variant: null,
|
|
67
|
+
archived: false,
|
|
68
|
+
};
|
|
69
|
+
const mine = [];
|
|
70
|
+
const add = (rank, match, extra = {}) => mine.push({ hit: { ...base, match, ...extra }, rank, updatedAt: m.updatedAt });
|
|
71
|
+
const title = fold(m.title);
|
|
72
|
+
if (title === whole)
|
|
73
|
+
add(RANK.exact, "title");
|
|
74
|
+
else if (title.startsWith(whole))
|
|
75
|
+
add(RANK.prefix, "title");
|
|
76
|
+
else if (terms.every((t) => new RegExp(`(^|[^\\p{L}\\p{N}])${escapeRe(t)}`, "u").test(title)))
|
|
77
|
+
add(RANK.word, "title");
|
|
78
|
+
else if (has(m.title) || has(m.slug))
|
|
79
|
+
add(RANK.title, "title");
|
|
80
|
+
for (const state of m.states) {
|
|
81
|
+
if (has(state))
|
|
82
|
+
add(RANK.state, "state", { text: state, state });
|
|
83
|
+
}
|
|
84
|
+
const namesSeen = new Set();
|
|
85
|
+
for (const v of variants) {
|
|
86
|
+
const gone = !isLive(v.status);
|
|
87
|
+
const tail = gone ? 0.5 : 0;
|
|
88
|
+
// A variant named "default" says nothing the design's title doesn't.
|
|
89
|
+
if (v.variant !== "default" && !namesSeen.has(v.variant) && has(v.variant)) {
|
|
90
|
+
namesSeen.add(v.variant);
|
|
91
|
+
add(RANK.variant + tail, "variant", {
|
|
92
|
+
text: v.variant,
|
|
93
|
+
state: v.state,
|
|
94
|
+
variant: v.variant,
|
|
95
|
+
archived: gone,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (v.description && has(v.description)) {
|
|
99
|
+
add(RANK.description + tail, "description", {
|
|
100
|
+
text: clip(v.description, terms[0]),
|
|
101
|
+
state: v.state,
|
|
102
|
+
variant: v.variant,
|
|
103
|
+
archived: gone,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const ask of m.asks) {
|
|
108
|
+
if (has(ask.text)) {
|
|
109
|
+
add(RANK.question, "question", {
|
|
110
|
+
text: clip(ask.text, terms[0]),
|
|
111
|
+
state: ask.state ?? null,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
mine.sort((a, b) => a.rank - b.rank);
|
|
116
|
+
out.push(...mine.slice(0, MAX_PER_DESIGN));
|
|
117
|
+
if (m.area && has(m.area) && !areasSeen.has(`${m.project}\n${m.area}`)) {
|
|
118
|
+
areasSeen.add(`${m.project}\n${m.area}`);
|
|
119
|
+
out.push({
|
|
120
|
+
hit: {
|
|
121
|
+
...base,
|
|
122
|
+
id: null,
|
|
123
|
+
slug: null,
|
|
124
|
+
title: m.area,
|
|
125
|
+
kind: null,
|
|
126
|
+
width: null,
|
|
127
|
+
match: "area",
|
|
128
|
+
open: 0,
|
|
129
|
+
designArchived: false,
|
|
130
|
+
inUse: false,
|
|
131
|
+
},
|
|
132
|
+
rank: RANK.area,
|
|
133
|
+
updatedAt: m.updatedAt,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
export function jumpHits(rows, query, project) {
|
|
140
|
+
const whole = fold(query).trim().replace(/\s+/g, " ");
|
|
141
|
+
const terms = whole.split(" ").filter(Boolean);
|
|
142
|
+
if (!terms.length)
|
|
143
|
+
return [];
|
|
144
|
+
const order = (a, b) => a.rank - b.rank || b.updatedAt.localeCompare(a.updatedAt);
|
|
145
|
+
const all = allHits(rows, terms, whole);
|
|
146
|
+
const here = all.filter((h) => h.hit.project === project).sort(order);
|
|
147
|
+
const elsewhere = all.filter((h) => h.hit.project !== project).sort(order);
|
|
148
|
+
return [...here.slice(0, MAX_HERE), ...elsewhere.slice(0, MAX_ELSEWHERE)].map((h) => h.hit);
|
|
149
|
+
}
|
|
150
|
+
// The designs this browser opened last, in that order, as they are called now: a
|
|
151
|
+
// renamed one follows, and a deleted one drops out.
|
|
152
|
+
export function recentHits(rows, ids) {
|
|
153
|
+
const wanted = new Map(ids.map((id, i) => [id, i]));
|
|
154
|
+
const inUse = new Set(rows.variants.filter((v) => v.status === "accepted").map((v) => v.designId));
|
|
155
|
+
return rows.designs
|
|
156
|
+
.filter((m) => wanted.has(m.id))
|
|
157
|
+
.sort((a, b) => wanted.get(a.id) - wanted.get(b.id))
|
|
158
|
+
.map((m) => ({
|
|
159
|
+
project: m.project,
|
|
160
|
+
id: m.id,
|
|
161
|
+
slug: m.slug,
|
|
162
|
+
title: m.title,
|
|
163
|
+
kind: m.kind,
|
|
164
|
+
width: m.widths[0] ?? null,
|
|
165
|
+
area: m.area,
|
|
166
|
+
match: "title",
|
|
167
|
+
text: null,
|
|
168
|
+
state: null,
|
|
169
|
+
variant: null,
|
|
170
|
+
archived: false,
|
|
171
|
+
open: openAsks(m).length,
|
|
172
|
+
designArchived: !!m.archivedAt,
|
|
173
|
+
inUse: inUse.has(m.id),
|
|
174
|
+
}));
|
|
175
|
+
}
|
package/dist/server/kits.d.ts
CHANGED
|
@@ -14,18 +14,18 @@ export declare const KITS: Kit[];
|
|
|
14
14
|
export declare const isKnownKit: (id: unknown) => id is string;
|
|
15
15
|
export declare const KIT_IDS: string[];
|
|
16
16
|
export declare const kitSummaries: (projectKits?: readonly ProjectKit[]) => ({
|
|
17
|
-
href?: string | undefined;
|
|
18
17
|
id: string;
|
|
19
18
|
source: "bundled";
|
|
20
19
|
label: string;
|
|
21
20
|
summary: string;
|
|
22
21
|
classes: string;
|
|
22
|
+
href?: string | undefined;
|
|
23
23
|
} | {
|
|
24
|
-
source: "project";
|
|
25
24
|
id: string;
|
|
26
25
|
href: string;
|
|
27
26
|
script?: string;
|
|
28
27
|
doc: string;
|
|
28
|
+
source: "project";
|
|
29
29
|
})[];
|
|
30
30
|
export declare const RESERVED_KIT_IDS: string[];
|
|
31
31
|
export declare const PROJECT_KIT_DOC_MAX = 1200;
|
package/dist/server/kits.js
CHANGED
|
@@ -165,6 +165,52 @@ const BUILTIN_CSS = `
|
|
|
165
165
|
const BASECOAT_THEME_CSS = `
|
|
166
166
|
:root{--radius:var(--border-radius-md);--background:var(--color-background-primary);--foreground:var(--color-text-primary);--card:var(--color-background-primary);--card-foreground:var(--color-text-primary);--popover:var(--color-background-primary);--popover-foreground:var(--color-text-primary);--primary:var(--color-text-primary);--primary-foreground:var(--color-background-primary);--secondary:var(--color-background-secondary);--secondary-foreground:var(--color-text-primary);--muted:var(--color-background-secondary);--muted-foreground:var(--color-text-secondary);--accent:var(--color-background-secondary);--accent-foreground:var(--color-text-primary);--destructive:var(--color-text-danger);--border:var(--color-border-secondary);--input:var(--color-border-secondary);--ring:var(--color-border-info)}
|
|
167
167
|
`;
|
|
168
|
+
// layout: composition primitives that say how boxes relate, never how they look. Every
|
|
169
|
+
// gap is `--space`, which falls back to `--layout-space`: the base step of the
|
|
170
|
+
// project's space scale when its system has one (system.ts), else 1rem. Each takes
|
|
171
|
+
// its measure from a custom property on the element (`style="--space: var(--space-l)"`),
|
|
172
|
+
// so a page sets a step without a new class. Those properties don't inherit, so a
|
|
173
|
+
// layout nested in another starts from the defaults, not its parent's measure; what
|
|
174
|
+
// a primitive's children read, it passes down as its own `--l-*`.
|
|
175
|
+
//
|
|
176
|
+
// A cover fills the screen, but a frame is sized from its content, so 100vh grows with
|
|
177
|
+
// the cover it sizes: the cap ends that, and border-box keeps the padding inside.
|
|
178
|
+
const LAYOUT_CSS = `
|
|
179
|
+
@property --space{syntax:"*";inherits:false}
|
|
180
|
+
@property --measure{syntax:"*";inherits:false}
|
|
181
|
+
@property --side{syntax:"*";inherits:false}
|
|
182
|
+
@property --threshold{syntax:"*";inherits:false}
|
|
183
|
+
@property --min{syntax:"*";inherits:false}
|
|
184
|
+
@property --item{syntax:"*";inherits:false}
|
|
185
|
+
.l-stack,.l-cluster,.l-sidebar,.l-switcher,.l-grid,.l-reel{gap:var(--space,var(--layout-space,1rem))}
|
|
186
|
+
.l-stack{display:flex;flex-direction:column}
|
|
187
|
+
.l-stack>*{margin-block:0}
|
|
188
|
+
.l-box{padding:var(--space,var(--layout-space,1rem))}
|
|
189
|
+
.l-center{box-sizing:content-box;max-inline-size:var(--measure,65ch);margin-inline:auto;padding-inline:var(--gutter,0)}
|
|
190
|
+
.l-cluster{display:flex;flex-wrap:wrap;justify-content:var(--justify,flex-start);align-items:var(--align,center)}
|
|
191
|
+
.l-sidebar{display:flex;flex-wrap:wrap;align-items:var(--align,stretch);--l-side:var(--side,16rem)}
|
|
192
|
+
.l-sidebar>*{flex-grow:1}
|
|
193
|
+
.l-sidebar>:first-child{flex-basis:var(--l-side)}
|
|
194
|
+
.l-sidebar>:last-child{flex-basis:0;flex-grow:999;min-inline-size:var(--content-min,55%)}
|
|
195
|
+
.l-sidebar.end>:first-child{flex-basis:0;flex-grow:999;min-inline-size:var(--content-min,55%)}
|
|
196
|
+
.l-sidebar.end>:last-child{flex-basis:var(--l-side);flex-grow:1;min-inline-size:0}
|
|
197
|
+
.l-switcher{display:flex;flex-wrap:wrap;--l-threshold:var(--threshold,32rem)}
|
|
198
|
+
.l-switcher>*{flex-grow:1;flex-basis:calc((var(--l-threshold) - 100%) * 999)}
|
|
199
|
+
.l-cover{display:flex;flex-direction:column;box-sizing:border-box;min-block-size:var(--min-height,min(100vh,50rem));--l-space:var(--space,var(--layout-space,1rem));padding:var(--l-space)}
|
|
200
|
+
.l-cover>*{margin-block:var(--l-space)}
|
|
201
|
+
.l-cover>:first-child{margin-block-start:0}
|
|
202
|
+
.l-cover>:last-child{margin-block-end:0}
|
|
203
|
+
.l-cover>.main{margin-block:auto}
|
|
204
|
+
.l-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(var(--min,14rem),100%),1fr))}
|
|
205
|
+
.l-frame{aspect-ratio:var(--ratio,16 / 9);overflow:hidden;display:flex;align-items:center;justify-content:center}
|
|
206
|
+
.l-frame>img,.l-frame>video{inline-size:100%;block-size:100%;object-fit:cover}
|
|
207
|
+
.l-reel{display:flex;overflow-x:auto;overflow-y:hidden;scroll-snap-type:x proximity;padding-block-end:var(--scroll-room,0);--l-item:var(--item,auto)}
|
|
208
|
+
.l-reel>*{flex:0 0 var(--l-item);scroll-snap-align:start}
|
|
209
|
+
.l-imposter{position:absolute;inset-block-start:50%;inset-inline-start:50%;translate:-50% -50%;max-inline-size:calc(100% - 2 * var(--margin,0px));max-block-size:calc(100% - 2 * var(--margin,0px));overflow:auto}
|
|
210
|
+
.l-imposter.fixed{position:fixed}
|
|
211
|
+
.l-icon{inline-size:.75em;block-size:.75em;inline-size:1cap;block-size:1cap;flex:none}
|
|
212
|
+
.l-with-icon{display:inline-flex;align-items:baseline;gap:var(--icon-space,.5ch)}
|
|
213
|
+
`;
|
|
168
214
|
// Pinned to a major so fixes arrive without a server release. 1.x moved
|
|
169
215
|
// variants from `.btn-primary` classes to `data-variant`.
|
|
170
216
|
const BASECOAT_CDN = "https://cdn.jsdelivr.net/npm/basecoat-css@1/dist";
|
|
@@ -172,7 +218,7 @@ export const KITS = [
|
|
|
172
218
|
{
|
|
173
219
|
id: "builtin",
|
|
174
220
|
label: "Builtin",
|
|
175
|
-
summary: "shadcn-shaped components (CSS only) for projects with no
|
|
221
|
+
summary: "shadcn-shaped components (CSS only) for projects with no components of their own",
|
|
176
222
|
classes: "btn · card · input · label · badge · tabs · dialog · table",
|
|
177
223
|
css: BUILTIN_CSS,
|
|
178
224
|
},
|
|
@@ -202,6 +248,13 @@ export const KITS = [
|
|
|
202
248
|
// also drives dropdown-menu, select and popover markup.
|
|
203
249
|
script: `${BASECOAT_CDN}/js/all.min.js`,
|
|
204
250
|
},
|
|
251
|
+
{
|
|
252
|
+
id: "layout",
|
|
253
|
+
label: "Layout",
|
|
254
|
+
summary: 'composition primitives on the space scale; set a step with style="--space: var(--space-l)"',
|
|
255
|
+
classes: "l-stack · l-box · l-center (--measure) · l-cluster · l-sidebar[.end] (--side) · l-switcher (--threshold) · l-cover (>.main) · l-grid (--min) · l-frame (--ratio) · l-reel (--item) · l-imposter[.fixed] · l-icon · l-with-icon",
|
|
256
|
+
css: LAYOUT_CSS,
|
|
257
|
+
},
|
|
205
258
|
];
|
|
206
259
|
const KIT_BY_ID = new Map(KITS.map((k) => [k.id, k]));
|
|
207
260
|
export const isKnownKit = (id) => typeof id === "string" && KIT_BY_ID.has(id);
|
package/dist/server/knobs.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExplicitKnob, KnobConfig, Knobs, KnobValue,
|
|
1
|
+
import type { ExplicitKnob, KnobConfig, Knobs, KnobValue, Design, Post } from "./types.ts";
|
|
2
2
|
export type Checked<T> = {
|
|
3
3
|
ok: true;
|
|
4
4
|
value: T;
|
|
@@ -10,10 +10,10 @@ export declare function explicitKnob(config: KnobConfig): ExplicitKnob;
|
|
|
10
10
|
export declare function checkKnobConfig(path: string, raw: unknown): Checked<KnobConfig>;
|
|
11
11
|
export declare function checkKnobs(raw: unknown): Checked<Knobs>;
|
|
12
12
|
export declare function checkKnobValue(path: string, config: KnobConfig, value: unknown): Checked<KnobValue>;
|
|
13
|
-
export declare function knobFor(path: string,
|
|
13
|
+
export declare function knobFor(path: string, design: Pick<Design, "knobs">, posts: (Pick<Post, "knobs"> & {
|
|
14
14
|
surfaces?: Post["surfaces"];
|
|
15
15
|
})[]): KnobConfig | undefined;
|
|
16
|
-
export declare function checkKnobValues(raw: unknown,
|
|
16
|
+
export declare function checkKnobValues(raw: unknown, design: Pick<Design, "knobs">, posts: (Pick<Post, "knobs"> & {
|
|
17
17
|
surfaces?: Post["surfaces"];
|
|
18
18
|
})[], what: string): Checked<Record<string, KnobValue>>;
|
|
19
19
|
export declare function discreteChoices(config: KnobConfig): number | null;
|
package/dist/server/knobs.js
CHANGED
|
@@ -293,12 +293,12 @@ export function checkKnobValue(path, config, value) {
|
|
|
293
293
|
}
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
|
-
// The declared knob for a path: the
|
|
296
|
+
// The declared knob for a path: the design's own, else a variant's override,
|
|
297
297
|
// else the knob a variant's slot implies (fills.ts), so an option can `set` a
|
|
298
298
|
// slot's fill without the agent declaring anything.
|
|
299
|
-
export function knobFor(path,
|
|
300
|
-
if (Object.hasOwn(
|
|
301
|
-
return
|
|
299
|
+
export function knobFor(path, design, posts) {
|
|
300
|
+
if (Object.hasOwn(design.knobs, path))
|
|
301
|
+
return design.knobs[path];
|
|
302
302
|
for (const p of posts)
|
|
303
303
|
if (p.knobs && Object.hasOwn(p.knobs, path))
|
|
304
304
|
return p.knobs[path];
|
|
@@ -311,7 +311,7 @@ export function knobFor(path, mock, posts) {
|
|
|
311
311
|
}
|
|
312
312
|
return undefined;
|
|
313
313
|
}
|
|
314
|
-
export function checkKnobValues(raw,
|
|
314
|
+
export function checkKnobValues(raw, design, posts, what) {
|
|
315
315
|
if (raw === undefined || raw === null)
|
|
316
316
|
return { ok: true, value: {} };
|
|
317
317
|
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
@@ -319,7 +319,7 @@ export function checkKnobValues(raw, mock, posts, what) {
|
|
|
319
319
|
}
|
|
320
320
|
const out = {};
|
|
321
321
|
for (const [path, value] of Object.entries(raw)) {
|
|
322
|
-
const config = knobFor(path,
|
|
322
|
+
const config = knobFor(path, design, posts);
|
|
323
323
|
if (config === undefined)
|
|
324
324
|
return { ok: false, error: `${what}: no knob "${path}" is declared` };
|
|
325
325
|
const checked = checkKnobValue(path, config, value);
|
package/dist/server/mcpHttp.d.ts
CHANGED
|
@@ -11,13 +11,16 @@ export interface McpFlows {
|
|
|
11
11
|
tree: Flow<[query: {
|
|
12
12
|
project?: string;
|
|
13
13
|
}]>;
|
|
14
|
+
decisions: Flow<[query: {
|
|
15
|
+
project?: string;
|
|
16
|
+
}]>;
|
|
14
17
|
get: Flow<[ref: unknown, query: {
|
|
15
18
|
project?: unknown;
|
|
16
19
|
body?: boolean;
|
|
17
20
|
history?: boolean;
|
|
18
21
|
}]>;
|
|
19
22
|
ask: Flow<[ref: unknown, body: any, ctx: FlowContext]>;
|
|
20
|
-
|
|
23
|
+
exportDesign: Flow<[
|
|
21
24
|
ref: unknown,
|
|
22
25
|
query: {
|
|
23
26
|
project?: unknown;
|
|
@@ -27,12 +30,15 @@ export interface McpFlows {
|
|
|
27
30
|
ctx: FlowContext
|
|
28
31
|
]>;
|
|
29
32
|
say: Flow<[ref: unknown, body: any, ctx: FlowContext]>;
|
|
33
|
+
move: Flow<[ref: unknown, body: any, ctx: FlowContext]>;
|
|
30
34
|
feedback: Flow<[query: {
|
|
31
35
|
session?: unknown;
|
|
32
36
|
project?: unknown;
|
|
33
37
|
}, ctx: FlowContext]>;
|
|
34
38
|
edit: Flow<[ref: unknown, body: any, ctx: FlowContext]>;
|
|
35
39
|
editProject: Flow<[name: unknown, body: any, ctx: FlowContext]>;
|
|
40
|
+
system: Flow<[project: unknown, body: any, ctx: FlowContext]>;
|
|
41
|
+
exportSystem: Flow<[project: unknown]>;
|
|
36
42
|
}
|
|
37
43
|
export interface McpDeps {
|
|
38
44
|
store: Store;
|
package/dist/server/mcpHttp.js
CHANGED
|
@@ -12,16 +12,16 @@ export function unwrap(result) {
|
|
|
12
12
|
return result.body;
|
|
13
13
|
}
|
|
14
14
|
// `edit` is one verb with two shapes: `rename` or `areaOrder` edits the
|
|
15
|
-
// project, anything else edits the
|
|
15
|
+
// project, anything else edits the design. Shared by both MCP transports' HTTP
|
|
16
16
|
// side and `run`.
|
|
17
17
|
export function editCall(flows, args, ctx, project) {
|
|
18
18
|
if (args.rename === undefined && args.areaOrder === undefined) {
|
|
19
|
-
return flows.edit(args.
|
|
19
|
+
return flows.edit(args.design, args, ctx);
|
|
20
20
|
}
|
|
21
|
-
if (args.
|
|
21
|
+
if (args.design !== undefined) {
|
|
22
22
|
return Promise.resolve({
|
|
23
23
|
status: 400,
|
|
24
|
-
body: { error: '"rename" and "areaOrder" edit the project; edit the
|
|
24
|
+
body: { error: '"rename" and "areaOrder" edit the project; edit the design without them' },
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
if (!project) {
|
|
@@ -63,14 +63,17 @@ export function registerMcp(app, deps) {
|
|
|
63
63
|
case "publish":
|
|
64
64
|
return unwrap(await flows.publish(args, ctx));
|
|
65
65
|
case "ask":
|
|
66
|
-
return unwrap(await flows.ask(args.
|
|
66
|
+
return unwrap(await flows.ask(args.design, args, ctx));
|
|
67
67
|
case "read":
|
|
68
|
-
if (args.
|
|
68
|
+
if (args.decisions === true) {
|
|
69
|
+
return unwrap(await flows.decisions({ project: args.project }));
|
|
70
|
+
}
|
|
71
|
+
if (args.design === undefined && args.tree === true) {
|
|
69
72
|
return unwrap(await flows.tree({ project: args.project }));
|
|
70
73
|
}
|
|
71
|
-
if (args.
|
|
74
|
+
if (args.design === undefined)
|
|
72
75
|
return unwrap(await flows.list({ project: args.project }));
|
|
73
|
-
return unwrap(await flows.get(args.
|
|
76
|
+
return unwrap(await flows.get(args.design, {
|
|
74
77
|
project: args.project,
|
|
75
78
|
body: args.body === true,
|
|
76
79
|
history: args.history === true,
|
|
@@ -78,11 +81,25 @@ export function registerMcp(app, deps) {
|
|
|
78
81
|
case "feedback":
|
|
79
82
|
return unwrap(await flows.feedback({ session: args.session, project: args.project }, ctx));
|
|
80
83
|
case "say":
|
|
81
|
-
return unwrap(await flows.say(args.
|
|
84
|
+
return unwrap(await flows.say(args.design, args, ctx));
|
|
85
|
+
case "move":
|
|
86
|
+
return unwrap(await flows.move(args.design, args, ctx));
|
|
82
87
|
case "export":
|
|
83
|
-
|
|
88
|
+
if (args.design === undefined) {
|
|
89
|
+
return unwrap(await flows.exportSystem(await projectOf(args)));
|
|
90
|
+
}
|
|
91
|
+
return unwrap(await flows.exportDesign(args.design, args, ctx));
|
|
84
92
|
case "edit":
|
|
85
93
|
return unwrap(await editCall(flows, args, ctx, await namedProject(args)));
|
|
94
|
+
case "system": {
|
|
95
|
+
const writing = Object.keys(args).some((k) => !["session", "project", "agent"].includes(k));
|
|
96
|
+
if (!writing)
|
|
97
|
+
return unwrap(await flows.system(await projectOf(args), args, ctx));
|
|
98
|
+
const project = await namedProject(args);
|
|
99
|
+
if (!project)
|
|
100
|
+
throw new Error("system writes need a project: pass project, or call from a session that has one");
|
|
101
|
+
return unwrap(await flows.system(project, args, ctx));
|
|
102
|
+
}
|
|
86
103
|
case "upload": {
|
|
87
104
|
if (typeof args.data !== "string" || args.data.length === 0) {
|
|
88
105
|
throw new Error("upload needs base64 `data`");
|
|
@@ -110,7 +127,7 @@ export function registerMcp(app, deps) {
|
|
|
110
127
|
return await deps.guide(await projectOf(args));
|
|
111
128
|
if (!isGuideTopic(args.topic))
|
|
112
129
|
throw new Error(unknownTopicMessage(String(args.topic)));
|
|
113
|
-
return await deps.guide("", args.topic);
|
|
130
|
+
return await deps.guide(args.topic === "system" ? await projectOf(args) : "", args.topic);
|
|
114
131
|
}
|
|
115
132
|
default:
|
|
116
133
|
throw new Error(`unknown tool: ${name}`);
|
|
@@ -119,7 +136,7 @@ export function registerMcp(app, deps) {
|
|
|
119
136
|
app.post("/mcp", async (c) => {
|
|
120
137
|
const rpc = (id, result) => c.json({ jsonrpc: "2.0", id, result });
|
|
121
138
|
// `?mode=code` is the opt-in codemode catalog for clients without a
|
|
122
|
-
// harness of their own: one `run` tool instead of the
|
|
139
|
+
// harness of their own: one `run` tool instead of the design tools.
|
|
123
140
|
const code = c.req.query("mode") === "code";
|
|
124
141
|
const rpcError = (id, code, message, status = 200) => c.json({ jsonrpc: "2.0", id, error: { code, message } }, status);
|
|
125
142
|
let msg;
|