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/embeds.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Composition: `<mockpit-embed slug="…" variant="…" version="…">` tags in any
|
|
2
|
-
//
|
|
2
|
+
// design's html are expanded server-side by inlining the referenced variant
|
|
3
3
|
// version's first html surface body, recursively, so a component can be built
|
|
4
4
|
// from components. A tag without `version` follows the version in use when the
|
|
5
5
|
// variant it resolves to is accepted, else its latest; one without `variant`
|
|
@@ -47,7 +47,19 @@ export function parseEmbedTags(html) {
|
|
|
47
47
|
return tags;
|
|
48
48
|
}
|
|
49
49
|
export const hasEmbedTags = (html) => /<mockpit-embed\b/i.test(html);
|
|
50
|
-
//
|
|
50
|
+
// Whether a post's first html surface embeds anything, cached per surfaces array
|
|
51
|
+
// (a stored post never mutates it): a design's view asks on every read.
|
|
52
|
+
const embedsBySurfaces = new WeakMap();
|
|
53
|
+
export function firstHtmlEmbeds(surfaces) {
|
|
54
|
+
const hit = embedsBySurfaces.get(surfaces);
|
|
55
|
+
if (hit !== undefined)
|
|
56
|
+
return hit;
|
|
57
|
+
const html = surfaces.find((x) => x.kind === "html");
|
|
58
|
+
const found = !!html && html.kind === "html" && hasEmbedTags(html.html);
|
|
59
|
+
embedsBySurfaces.set(surfaces, found);
|
|
60
|
+
return found;
|
|
61
|
+
}
|
|
62
|
+
// The html with every reference to design `from` pointed at `to`: the slug of
|
|
51
63
|
// each embed tag naming it, and each data-goto naming it ("from" or
|
|
52
64
|
// "from:<state>"). What a slug rename writes into every stored version, so
|
|
53
65
|
// each version still renders the same component it did.
|
|
@@ -64,12 +76,12 @@ export function renameSlugRefs(html, from, to) {
|
|
|
64
76
|
})
|
|
65
77
|
.replace(/(\sdata-goto\s*=\s*)(?:"([^"]*)"|'([^']*)')/gi, (m, key, dq, sq) => {
|
|
66
78
|
const value = dq ?? sq ?? "";
|
|
67
|
-
// "next" and "prev" are directions, never a
|
|
79
|
+
// "next" and "prev" are directions, never a design.
|
|
68
80
|
if (value === "next" || value === "prev")
|
|
69
81
|
return m;
|
|
70
82
|
const colon = value.indexOf(":");
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
83
|
+
const design = (colon < 0 ? value : value.slice(0, colon)).trim();
|
|
84
|
+
if (design !== from)
|
|
73
85
|
return m;
|
|
74
86
|
const q = dq === undefined ? "'" : '"';
|
|
75
87
|
return `${key}${q}${to}${colon < 0 ? "" : value.slice(colon)}${q}`;
|
|
@@ -77,7 +89,7 @@ export function renameSlugRefs(html, from, to) {
|
|
|
77
89
|
}
|
|
78
90
|
const escapeAttr = (s) => s.replace(/&/g, "&").replace(/"/g, """);
|
|
79
91
|
// Replace each tag with its resolved body, expanding that body's own tags in
|
|
80
|
-
// turn. `stack` is the chain of slugs being expanded (the root
|
|
92
|
+
// turn. `stack` is the chain of slugs being expanded (the root design's first),
|
|
81
93
|
// so a component that reaches back up renders a marked placeholder instead of
|
|
82
94
|
// recursing. An unresolvable embed renders as an empty marked-up placeholder
|
|
83
95
|
// rather than vanishing, so a broken reference is visible in review instead of
|
|
@@ -130,6 +142,19 @@ export async function expandHtml(html, resolve, stack) {
|
|
|
130
142
|
const out = html.replace(EMBED_TAG, (raw) => bodies[i++] ?? raw);
|
|
131
143
|
return { html: out, used, stamps, cycle };
|
|
132
144
|
}
|
|
145
|
+
const STYLE_BLOCK = /<style\b[^>]*>[\s\S]*?<\/style\s*>/gi;
|
|
146
|
+
// Components built on one kit each carry its stylesheet. A repeat would re-declare
|
|
147
|
+
// the kit's rules after what an earlier component set over them and undo it, so
|
|
148
|
+
// only the first copy stays.
|
|
149
|
+
export function dropRepeatedStyles(html) {
|
|
150
|
+
const seen = new Set();
|
|
151
|
+
return html.replace(STYLE_BLOCK, (block) => {
|
|
152
|
+
if (seen.has(block))
|
|
153
|
+
return "";
|
|
154
|
+
seen.add(block);
|
|
155
|
+
return block;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
133
158
|
// A short, stable key for what a document's embeds resolved to: part of the
|
|
134
159
|
// render cache key and of the frame URL, so a component's new version reloads
|
|
135
160
|
// every page that shows it and a stale document is never served as current.
|
|
@@ -7,7 +7,7 @@ export interface LogEntry {
|
|
|
7
7
|
ms: number;
|
|
8
8
|
session?: string;
|
|
9
9
|
project?: string;
|
|
10
|
-
|
|
10
|
+
design?: string;
|
|
11
11
|
state?: string | null;
|
|
12
12
|
variant?: string;
|
|
13
13
|
version?: number;
|
|
@@ -21,7 +21,7 @@ export interface LogEntry {
|
|
|
21
21
|
}
|
|
22
22
|
export type LogSink = (entry: LogEntry) => void;
|
|
23
23
|
export declare function clientOf(header: string | undefined, viewer: boolean, mcp: boolean): string;
|
|
24
|
-
export declare function fromRequest(query: URLSearchParams, body: unknown,
|
|
24
|
+
export declare function fromRequest(query: URLSearchParams, body: unknown, pathDesign: string | undefined): Partial<LogEntry>;
|
|
25
25
|
export declare function fromResponse(body: unknown): Partial<LogEntry>;
|
|
26
26
|
export declare function writeLog(sink: LogSink | undefined, entry: LogEntry): void;
|
|
27
27
|
export declare function logMiddleware(sink: LogSink): MiddlewareHandler;
|
package/dist/server/eventLog.js
CHANGED
|
@@ -18,12 +18,12 @@ const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
|
18
18
|
const text = (v) => typeof v === "string" && v ? v.slice(0, 200) : undefined;
|
|
19
19
|
// What the request itself names, for calls whose response doesn't (errors,
|
|
20
20
|
// feedback reads).
|
|
21
|
-
export function fromRequest(query, body,
|
|
21
|
+
export function fromRequest(query, body, pathDesign) {
|
|
22
22
|
const b = isObj(body) ? body : {};
|
|
23
23
|
return prune({
|
|
24
24
|
session: text(query.get("session")) ?? text(b.session),
|
|
25
25
|
project: text(query.get("project")) ?? text(b.project),
|
|
26
|
-
|
|
26
|
+
design: pathDesign ?? text(b.design),
|
|
27
27
|
variant: text(b.variant),
|
|
28
28
|
viewing: text(query.get("viewing")),
|
|
29
29
|
});
|
|
@@ -32,7 +32,7 @@ export function fromRequest(query, body, pathMock) {
|
|
|
32
32
|
export function fromResponse(body) {
|
|
33
33
|
if (!isObj(body))
|
|
34
34
|
return {};
|
|
35
|
-
const
|
|
35
|
+
const design = isObj(body.design) ? body.design : undefined;
|
|
36
36
|
const post = isObj(body.post) ? body.post : undefined;
|
|
37
37
|
// POST /api/sessions answers with the bare session (201 new, 200 reclaimed by key).
|
|
38
38
|
const session = typeof body.agentSeq === "number" ? body : undefined;
|
|
@@ -41,10 +41,10 @@ export function fromResponse(body) {
|
|
|
41
41
|
return prune({
|
|
42
42
|
session: text(body.sessionId) ?? text(post?.sessionId) ?? text(session?.id),
|
|
43
43
|
key: text(session?.key),
|
|
44
|
-
project: text(
|
|
45
|
-
|
|
44
|
+
project: text(design?.project) ?? text(body.project) ?? text(session?.project),
|
|
45
|
+
design: text(design?.slug) ??
|
|
46
46
|
text(body.slug) ??
|
|
47
|
-
(typeof body.
|
|
47
|
+
(typeof body.design === "string" ? text(body.design) : undefined),
|
|
48
48
|
state: post ? (post.state ?? null) : undefined,
|
|
49
49
|
variant: text(post?.variant),
|
|
50
50
|
version: typeof post?.version === "number" ? post.version : undefined,
|
|
@@ -74,11 +74,11 @@ function deliveredSeqs(feedback) {
|
|
|
74
74
|
function pendingLines(pending) {
|
|
75
75
|
const rows = Array.isArray(pending) ? pending : isObj(pending) ? [pending] : [];
|
|
76
76
|
return rows.flatMap((p) => {
|
|
77
|
-
if (!isObj(p) || typeof p.
|
|
77
|
+
if (!isObj(p) || typeof p.design !== "string")
|
|
78
78
|
return [];
|
|
79
79
|
if (isObj(p.draft))
|
|
80
|
-
return [`${p.
|
|
81
|
-
return p.viewerOpen ? [`${p.
|
|
80
|
+
return [`${p.design} ${p.draft.answered}/${p.draft.of} answered`];
|
|
81
|
+
return p.viewerOpen ? [`${p.design} open`] : [];
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
84
|
function prune(entry) {
|
|
@@ -144,8 +144,8 @@ export function logMiddleware(sink) {
|
|
|
144
144
|
Object.assign(entry, fromMcpResult(resBody));
|
|
145
145
|
}
|
|
146
146
|
else {
|
|
147
|
-
const
|
|
148
|
-
Object.assign(entry, fromRequest(url.searchParams, reqBody,
|
|
147
|
+
const pathDesign = /^\/api\/designs\/([^/]+)/.exec(c.req.path)?.[1];
|
|
148
|
+
Object.assign(entry, fromRequest(url.searchParams, reqBody, pathDesign));
|
|
149
149
|
Object.assign(entry, fromResponse(resBody));
|
|
150
150
|
}
|
|
151
151
|
writeLog(sink, entry);
|
package/dist/server/events.d.ts
CHANGED
|
@@ -3,33 +3,33 @@ export type FeedEvent = {
|
|
|
3
3
|
type: "session-created" | "session-updated" | "session-deleted";
|
|
4
4
|
id: string;
|
|
5
5
|
} | {
|
|
6
|
-
type: "
|
|
6
|
+
type: "design-created" | "design-updated";
|
|
7
7
|
id: string;
|
|
8
8
|
project: string;
|
|
9
9
|
} | {
|
|
10
|
-
type: "
|
|
10
|
+
type: "design-deleted";
|
|
11
11
|
id: string;
|
|
12
12
|
project: string;
|
|
13
13
|
} | {
|
|
14
14
|
type: "draft-updated";
|
|
15
|
-
|
|
15
|
+
designId: string;
|
|
16
16
|
} | {
|
|
17
17
|
type: "post-created" | "post-updated";
|
|
18
18
|
id: string;
|
|
19
|
-
|
|
19
|
+
designId: string;
|
|
20
20
|
sessionId: string;
|
|
21
21
|
version: number;
|
|
22
22
|
by: "agent" | "user";
|
|
23
23
|
} | {
|
|
24
24
|
type: "post-deleted";
|
|
25
25
|
id: string;
|
|
26
|
-
|
|
26
|
+
designId: string;
|
|
27
27
|
sessionId: string;
|
|
28
28
|
} | {
|
|
29
29
|
type: "comment-created";
|
|
30
30
|
id: string;
|
|
31
31
|
sessionId: string;
|
|
32
|
-
|
|
32
|
+
designId: string | null;
|
|
33
33
|
postId: string | null;
|
|
34
34
|
seq: number;
|
|
35
35
|
} | {
|
|
@@ -40,6 +40,15 @@ export type FeedEvent = {
|
|
|
40
40
|
type: "comment-delivered";
|
|
41
41
|
sessionId: string;
|
|
42
42
|
seq: number;
|
|
43
|
+
} | {
|
|
44
|
+
type: "agent-called";
|
|
45
|
+
sessionId: string;
|
|
46
|
+
} | {
|
|
47
|
+
type: "system-changed";
|
|
48
|
+
project: string;
|
|
49
|
+
rev: number;
|
|
50
|
+
summary: string;
|
|
51
|
+
by: "user" | "agent";
|
|
43
52
|
} | {
|
|
44
53
|
type: "theme-changed";
|
|
45
54
|
mode: Mode;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Decisions } from "./decisions.ts";
|
|
2
|
-
import { type
|
|
2
|
+
import { type StandingView } from "./standings.ts";
|
|
3
|
+
import { type AskOption, type Comment, type CommentAnchor, type Design, type DesignEdit, OTHER_ID, type PartComment, type Post, type Reaction, type Reply, type Store } from "./types.ts";
|
|
3
4
|
export interface VariantRef {
|
|
4
5
|
state: string | null;
|
|
5
6
|
variant: string;
|
|
@@ -27,14 +28,40 @@ export interface OtherChoice {
|
|
|
27
28
|
other: true;
|
|
28
29
|
images?: ImageRef[];
|
|
29
30
|
}
|
|
30
|
-
export interface AnsweredAsk {
|
|
31
|
+
export interface AnsweredAsk extends StandingView {
|
|
31
32
|
ask: string;
|
|
32
33
|
text: string;
|
|
33
34
|
chosen: (AskOption | OtherChoice)[];
|
|
35
|
+
comment?: string;
|
|
36
|
+
commentImages?: ImageRef[];
|
|
37
|
+
}
|
|
38
|
+
export interface MoreAnswers {
|
|
39
|
+
ask: string;
|
|
40
|
+
text: string;
|
|
41
|
+
existing: string[];
|
|
34
42
|
note?: string;
|
|
35
|
-
|
|
43
|
+
like?: string;
|
|
44
|
+
kept?: string[];
|
|
45
|
+
hidden?: {
|
|
46
|
+
label: string;
|
|
47
|
+
why?: string;
|
|
48
|
+
}[];
|
|
49
|
+
answered: {
|
|
50
|
+
ask: string;
|
|
51
|
+
text: string;
|
|
52
|
+
chosen: string[];
|
|
53
|
+
comment?: string;
|
|
54
|
+
}[];
|
|
36
55
|
}
|
|
37
|
-
export interface
|
|
56
|
+
export interface UpdateRequest {
|
|
57
|
+
ask: string;
|
|
58
|
+
text: string;
|
|
59
|
+
kept: AskOption;
|
|
60
|
+
inUse: AskOption[];
|
|
61
|
+
}
|
|
62
|
+
export interface FeedbackReply extends Omit<Reply, "others" | "askComments" | "otherImages" | "askCommentImages" | "more" | "standings" | "update"> {
|
|
63
|
+
more?: MoreAnswers;
|
|
64
|
+
update?: UpdateRequest;
|
|
38
65
|
seq: number;
|
|
39
66
|
at: string;
|
|
40
67
|
asks: AnsweredAsk[];
|
|
@@ -42,25 +69,24 @@ export interface FeedbackReply extends Omit<Reply, "others" | "notes" | "otherIm
|
|
|
42
69
|
decisions?: Decisions;
|
|
43
70
|
}
|
|
44
71
|
export interface FeedbackBatch {
|
|
45
|
-
|
|
72
|
+
designId: string | null;
|
|
46
73
|
project: string | null;
|
|
47
|
-
|
|
74
|
+
design: string | null;
|
|
48
75
|
title: string | null;
|
|
49
76
|
reply: FeedbackReply | null;
|
|
50
77
|
messages: FeedbackMessage[];
|
|
51
78
|
inUse: InUseRef[];
|
|
52
79
|
archived: VariantRef[];
|
|
53
80
|
handoff?: string;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
edited?: MockEdit;
|
|
81
|
+
edited?: DesignEdit;
|
|
82
|
+
system?: {
|
|
83
|
+
rev: number;
|
|
84
|
+
changes: string[];
|
|
85
|
+
};
|
|
60
86
|
}
|
|
61
87
|
export declare function reactionLine(c: PartComment & {
|
|
62
88
|
reaction: Reaction;
|
|
63
89
|
}): string;
|
|
64
|
-
export declare function groupFeedback(comments: Comment[],
|
|
90
|
+
export declare function groupFeedback(comments: Comment[], designs: Map<string, Design>, posts: Map<string, Post[]>, base?: string): FeedbackBatch[];
|
|
65
91
|
export declare function buildFeedbackBatches(store: Store, comments: Comment[], base: string): Promise<FeedbackBatch[]>;
|
|
66
92
|
export declare function feedbackImages(batches: FeedbackBatch[]): ImageRef[];
|
|
@@ -1,10 +1,60 @@
|
|
|
1
|
-
// The agent-facing shape of feedback: one batch per
|
|
1
|
+
// The agent-facing shape of feedback: one batch per design, in delivery order.
|
|
2
2
|
// The `feedback` verb, the watch long-poll and the write piggyback all return this,
|
|
3
|
-
// so an agent reads one grouped object instead of re-deriving which
|
|
3
|
+
// so an agent reads one grouped object instead of re-deriving which design a flat
|
|
4
4
|
// comment list belongs to. Runtime-agnostic (no node imports).
|
|
5
5
|
import { decisionsFor } from "./decisions.js";
|
|
6
|
+
import { ROUTED_NOTE } from "./handoff.js";
|
|
7
|
+
import { standingOf, standingView } from "./standings.js";
|
|
6
8
|
import { builtinAsk, inUseVersion, DEFAULT_VARIANT, OTHER_ID, } from "./types.js";
|
|
7
9
|
const ref = (p) => ({ state: p.state, variant: p.variant });
|
|
10
|
+
function moreAnswers(design, more, sent) {
|
|
11
|
+
const ask = design.asks.find((a) => a.id === more.ask);
|
|
12
|
+
if (!ask)
|
|
13
|
+
return undefined;
|
|
14
|
+
const st = sent ?? standingOf(design, ask.id);
|
|
15
|
+
const view = standingView(ask, st);
|
|
16
|
+
const like = more.like ? ask.options.find((o) => o.id === more.like)?.label : undefined;
|
|
17
|
+
const labels = (a) => {
|
|
18
|
+
const ids = a.answer === undefined ? [] : Array.isArray(a.answer) ? a.answer : [a.answer];
|
|
19
|
+
return [
|
|
20
|
+
...ids.map((id) => a.options.find((o) => o.id === id)?.label ?? id),
|
|
21
|
+
...(a.other === undefined ? [] : [a.other]),
|
|
22
|
+
];
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
ask: ask.id,
|
|
26
|
+
text: ask.text,
|
|
27
|
+
// Hidden ones are out: asked again, they'd only fold away.
|
|
28
|
+
existing: ask.options.filter((o) => !st.hidden.some((h) => h.id === o.id)).map((o) => o.label),
|
|
29
|
+
...(more.note ? { note: more.note } : {}),
|
|
30
|
+
...(like ? { like } : {}),
|
|
31
|
+
...(view?.kept ? { kept: view.kept.map((k) => k.label) } : {}),
|
|
32
|
+
...(view?.hidden
|
|
33
|
+
? { hidden: view.hidden.map((h) => ({ label: h.label, ...(h.why ? { why: h.why } : {}) })) }
|
|
34
|
+
: {}),
|
|
35
|
+
answered: design.asks
|
|
36
|
+
.filter((a) => a.id !== ask.id && labels(a).length > 0)
|
|
37
|
+
.map((a) => ({
|
|
38
|
+
ask: a.id,
|
|
39
|
+
text: a.text,
|
|
40
|
+
chosen: labels(a),
|
|
41
|
+
...(a.comment ? { comment: a.comment } : {}),
|
|
42
|
+
})),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function updateRequest(design, wanted) {
|
|
46
|
+
const ask = design.asks.find((a) => a.id === wanted.ask);
|
|
47
|
+
const kept = ask?.options.find((o) => o.id === wanted.option);
|
|
48
|
+
if (!ask || !kept)
|
|
49
|
+
return undefined;
|
|
50
|
+
const ids = ask.answer === undefined ? [] : [ask.answer].flat();
|
|
51
|
+
return {
|
|
52
|
+
ask: ask.id,
|
|
53
|
+
text: ask.text,
|
|
54
|
+
kept,
|
|
55
|
+
inUse: ask.options.filter((o) => ids.includes(o.id)),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
8
58
|
const REACTION_LEAD = {
|
|
9
59
|
up: "👍",
|
|
10
60
|
down: "👎",
|
|
@@ -12,7 +62,7 @@ const REACTION_LEAD = {
|
|
|
12
62
|
};
|
|
13
63
|
// `👍 part "hero" (variant B, state Mobile)`, `👎 variant B`, `more like this: the page`.
|
|
14
64
|
export function reactionLine(c) {
|
|
15
|
-
// A
|
|
65
|
+
// A design published without variant names has only "default": naming it is noise.
|
|
16
66
|
const named = c.variant && c.variant !== DEFAULT_VARIANT ? c.variant : null;
|
|
17
67
|
// A version is written as agents write it, dark@2.
|
|
18
68
|
const at = c.version ? `@${c.version}` : "";
|
|
@@ -31,21 +81,32 @@ export function reactionLine(c) {
|
|
|
31
81
|
return `${REACTION_LEAD[c.reaction]} ${target}${tail}${note}`;
|
|
32
82
|
}
|
|
33
83
|
const isReaction = (c) => !!c.reaction;
|
|
34
|
-
function answered(reply,
|
|
84
|
+
function answered(reply, design, posts, base) {
|
|
35
85
|
const others = reply.others ?? {};
|
|
36
|
-
const
|
|
86
|
+
const comments = reply.askComments ?? {};
|
|
37
87
|
const refs = (ids) => (ids ?? []).map((id) => ({ id, url: `${base}/a/${encodeURIComponent(id)}` }));
|
|
88
|
+
const standings = reply.standings ?? {};
|
|
38
89
|
const askIds = new Set([
|
|
39
90
|
...Object.keys(reply.answers),
|
|
40
91
|
...Object.keys(others),
|
|
41
|
-
...Object.keys(
|
|
92
|
+
...Object.keys(comments),
|
|
93
|
+
// A question whose answers the user only kept or hid is still news: more answers
|
|
94
|
+
// should follow the kept ones and avoid the hidden.
|
|
95
|
+
...Object.keys(standings).filter((id) => id !== reply.more?.ask),
|
|
42
96
|
]);
|
|
43
97
|
const out = [];
|
|
44
98
|
for (const askId of askIds) {
|
|
45
|
-
const ask =
|
|
99
|
+
const ask = design?.asks.find((a) => a.id === askId) ?? (design && builtinAsk(design, posts, askId));
|
|
46
100
|
const answer = reply.answers[askId];
|
|
47
101
|
const ids = answer === undefined ? [] : Array.isArray(answer) ? answer : [answer];
|
|
48
|
-
const chosen = ids.map((id) =>
|
|
102
|
+
const chosen = ids.map((id) => {
|
|
103
|
+
const option = ask?.options.find((o) => o.id === id);
|
|
104
|
+
if (!option)
|
|
105
|
+
return { id, label: id };
|
|
106
|
+
// What it brought isn't in any version yet: an agent folding it in needs it here.
|
|
107
|
+
const brought = design?.answerAdditions.find((a) => Object.entries(option.set ?? {}).some(([k, v]) => a.knob === k && a.value === v));
|
|
108
|
+
return brought ? { ...option, brought } : option;
|
|
109
|
+
});
|
|
49
110
|
const otherImages = refs(reply.otherImages?.[askId]);
|
|
50
111
|
if (others[askId] !== undefined)
|
|
51
112
|
chosen.push({
|
|
@@ -54,38 +115,60 @@ function answered(reply, mock, posts, base) {
|
|
|
54
115
|
other: true,
|
|
55
116
|
...(otherImages.length ? { images: otherImages } : {}),
|
|
56
117
|
});
|
|
57
|
-
const
|
|
118
|
+
const commentImages = refs(reply.askCommentImages?.[askId]);
|
|
119
|
+
const st = standings[askId];
|
|
58
120
|
out.push({
|
|
59
121
|
ask: askId,
|
|
60
122
|
text: ask?.text ?? "",
|
|
61
123
|
chosen,
|
|
62
|
-
...(
|
|
63
|
-
...(
|
|
124
|
+
...(ask && st ? standingView({ ...ask, answer }, st) : {}),
|
|
125
|
+
...(comments[askId] !== undefined ? { comment: comments[askId] } : {}),
|
|
126
|
+
...(commentImages.length ? { commentImages } : {}),
|
|
64
127
|
});
|
|
65
128
|
}
|
|
66
129
|
return out;
|
|
67
130
|
}
|
|
68
131
|
// Comments arrive in seq order; keep that order both between batches and inside
|
|
69
|
-
// one. A
|
|
132
|
+
// one. A design gets a fresh batch whenever a second reply arrives for it, so a
|
|
70
133
|
// batch carries at most one reply and nothing is merged away. A comment whose
|
|
71
|
-
//
|
|
134
|
+
// design is unknown still gets a batch — feedback is never dropped for want of
|
|
72
135
|
// context. `base` (origin + base path) makes image URLs absolute.
|
|
73
|
-
export function groupFeedback(comments,
|
|
136
|
+
export function groupFeedback(comments, designs, posts, base = "") {
|
|
74
137
|
const batches = [];
|
|
75
138
|
const open = new Map();
|
|
76
139
|
for (const c of comments) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
140
|
+
// Stored from when a close wrote to this stream. Ending something tells the agent
|
|
141
|
+
// nothing, so one still ahead of the cursor stays unsaid.
|
|
142
|
+
if (c.kind === "reply" && c.payload?.archived)
|
|
143
|
+
continue;
|
|
144
|
+
// The user's hand on the system page: one project batch each, never merged.
|
|
145
|
+
const decided = c.kind === "reply" && !c.designId ? c.payload?.system : undefined;
|
|
146
|
+
if (decided) {
|
|
147
|
+
batches.push({
|
|
148
|
+
designId: null,
|
|
149
|
+
project: decided.project,
|
|
150
|
+
design: null,
|
|
151
|
+
title: null,
|
|
152
|
+
reply: null,
|
|
153
|
+
messages: [],
|
|
154
|
+
inUse: [],
|
|
155
|
+
archived: [],
|
|
156
|
+
system: { rev: decided.rev, changes: decided.changes },
|
|
157
|
+
});
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const key = c.designId ?? "";
|
|
161
|
+
const design = c.designId ? designs.get(c.designId) : undefined;
|
|
162
|
+
const designPosts = (c.designId && posts.get(c.designId)) || [];
|
|
80
163
|
let batch = open.get(key);
|
|
81
|
-
// An edit says where the
|
|
164
|
+
// An edit says where the design is now, so it joins whatever batch is open.
|
|
82
165
|
const edit = c.kind === "reply" ? c.payload?.edited : undefined;
|
|
83
|
-
if (!batch || (c.kind === "reply" && !edit &&
|
|
166
|
+
if (!batch || (c.kind === "reply" && !edit && batch.reply)) {
|
|
84
167
|
batch = {
|
|
85
|
-
|
|
86
|
-
project:
|
|
87
|
-
|
|
88
|
-
title:
|
|
168
|
+
designId: c.designId,
|
|
169
|
+
project: design?.project ?? null,
|
|
170
|
+
design: design?.slug ?? null,
|
|
171
|
+
title: design?.title ?? null,
|
|
89
172
|
reply: null,
|
|
90
173
|
messages: [],
|
|
91
174
|
inUse: [],
|
|
@@ -98,36 +181,35 @@ export function groupFeedback(comments, mocks, posts, base = "") {
|
|
|
98
181
|
batch.edited = { ...batch.edited, ...edit };
|
|
99
182
|
continue;
|
|
100
183
|
}
|
|
101
|
-
if (c.kind === "reply" && c.payload?.closed) {
|
|
102
|
-
batch.closed = true;
|
|
103
|
-
batch.dismissed = c.payload.closed.dismissed.map((id) => ({
|
|
104
|
-
ask: id,
|
|
105
|
-
text: (mock?.asks.find((a) => a.id === id) ?? (mock && builtinAsk(mock, mockPosts, id)))
|
|
106
|
-
?.text ?? "",
|
|
107
|
-
}));
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
184
|
if (c.kind === "reply" && c.payload) {
|
|
111
|
-
const { others: _others,
|
|
185
|
+
const { others: _others, askComments: _askComments, otherImages: _otherImages, askCommentImages: _askCommentImages, routed, more: asked, standings: _standings, update: wanted, system: decidedHere, ...payload } = c.payload;
|
|
186
|
+
if (decidedHere)
|
|
187
|
+
batch.system = { rev: decidedHere.rev, changes: decidedHere.changes };
|
|
188
|
+
const more = asked && design ? moreAnswers(design, asked, c.payload.standings?.[asked.ask]) : undefined;
|
|
189
|
+
const update = wanted && design ? updateRequest(design, wanted) : undefined;
|
|
190
|
+
if (routed)
|
|
191
|
+
batch.handoff = ROUTED_NOTE;
|
|
112
192
|
const reactions = (payload.comments ?? []).filter(isReaction).map(reactionLine);
|
|
113
193
|
batch.reply = {
|
|
114
194
|
...payload,
|
|
115
195
|
comments: (payload.comments ?? []).filter((x) => !x.reaction),
|
|
116
196
|
seq: c.seq,
|
|
117
197
|
at: c.createdAt,
|
|
118
|
-
asks: answered(c.payload,
|
|
198
|
+
asks: answered(c.payload, design, designPosts, base),
|
|
119
199
|
...(reactions.length ? { reactions } : {}),
|
|
200
|
+
...(more ? { more } : {}),
|
|
201
|
+
...(update ? { update } : {}),
|
|
120
202
|
};
|
|
121
|
-
const decisions =
|
|
203
|
+
const decisions = design ? decisionsFor(design, designPosts, c.payload) : null;
|
|
122
204
|
if (decisions)
|
|
123
205
|
batch.reply.decisions = decisions;
|
|
124
|
-
batch.inUse =
|
|
206
|
+
batch.inUse = designPosts
|
|
125
207
|
.filter((p) => p.status === "accepted")
|
|
126
208
|
.map((p) => ({ ...ref(p), version: inUseVersion(p) }));
|
|
127
|
-
batch.archived =
|
|
209
|
+
batch.archived = designPosts.filter((p) => p.status === "archived").map(ref);
|
|
128
210
|
continue;
|
|
129
211
|
}
|
|
130
|
-
const post = c.postId ?
|
|
212
|
+
const post = c.postId ? designPosts.find((p) => p.id === c.postId) : undefined;
|
|
131
213
|
batch.messages.push({
|
|
132
214
|
seq: c.seq,
|
|
133
215
|
text: c.text,
|
|
@@ -142,18 +224,18 @@ export function groupFeedback(comments, mocks, posts, base = "") {
|
|
|
142
224
|
return batches;
|
|
143
225
|
}
|
|
144
226
|
export async function buildFeedbackBatches(store, comments, base) {
|
|
145
|
-
const
|
|
227
|
+
const designs = new Map();
|
|
146
228
|
const posts = new Map();
|
|
147
229
|
for (const c of comments) {
|
|
148
|
-
if (!c.
|
|
230
|
+
if (!c.designId || designs.has(c.designId))
|
|
149
231
|
continue;
|
|
150
|
-
const
|
|
151
|
-
if (!
|
|
232
|
+
const design = await store.getDesign(c.designId);
|
|
233
|
+
if (!design)
|
|
152
234
|
continue;
|
|
153
|
-
|
|
154
|
-
posts.set(c.
|
|
235
|
+
designs.set(c.designId, design);
|
|
236
|
+
posts.set(c.designId, await store.listPosts({ designId: c.designId }));
|
|
155
237
|
}
|
|
156
|
-
return groupFeedback(comments,
|
|
238
|
+
return groupFeedback(comments, designs, posts, base);
|
|
157
239
|
}
|
|
158
240
|
// Every image a list of batches carries, in order — what the MCP tiers attach
|
|
159
241
|
// as image content so the model sees what the user attached.
|
|
@@ -164,8 +246,8 @@ export function feedbackImages(batches) {
|
|
|
164
246
|
for (const c of a.chosen)
|
|
165
247
|
if ("other" in c && c.images)
|
|
166
248
|
out.push(...c.images);
|
|
167
|
-
if (a.
|
|
168
|
-
out.push(...a.
|
|
249
|
+
if (a.commentImages)
|
|
250
|
+
out.push(...a.commentImages);
|
|
169
251
|
}
|
|
170
252
|
}
|
|
171
253
|
return out;
|
package/dist/server/fills.d.ts
CHANGED
|
@@ -26,9 +26,13 @@ export interface FillScan {
|
|
|
26
26
|
}
|
|
27
27
|
export declare function scanFills(html: string): FillScan;
|
|
28
28
|
export declare function slotKnobs(slots: SlotInfo[]): Knobs;
|
|
29
|
+
export declare const cssString: (s: string) => string;
|
|
29
30
|
export declare function slotCss(slots: SlotInfo[]): string;
|
|
30
31
|
export declare function composeFills(html: string, chosen: Record<string, string>): string;
|
|
31
32
|
export declare function slotsInSurfaces(surfaces: Surface[]): SlotInfo[];
|
|
33
|
+
export declare const AS_IS_FILL = "as-is";
|
|
34
|
+
export declare function partOccurrences(html: string, part: string): number;
|
|
35
|
+
export declare function addFill(html: string, part: string, fill: string, markup: string): string;
|
|
32
36
|
export type FillSplice = {
|
|
33
37
|
ok: true;
|
|
34
38
|
surfaces: Surface[];
|
package/dist/server/fills.js
CHANGED
|
@@ -165,7 +165,7 @@ export function slotKnobs(slots) {
|
|
|
165
165
|
}
|
|
166
166
|
return out;
|
|
167
167
|
}
|
|
168
|
-
const cssString = (s) => `"${s.replace(/[\\"]/g, (c) => `\\${c}`).replace(/[\n\r<>]/g, (c) => `\\${c.charCodeAt(0).toString(16)} `)}"`;
|
|
168
|
+
export const cssString = (s) => `"${s.replace(/[\\"]/g, (c) => `\\${c}`).replace(/[\n\r<>]/g, (c) => `\\${c.charCodeAt(0).toString(16)} `)}"`;
|
|
169
169
|
// Shows only the chosen fill of each slot, keyed on the `data-k-<part>`
|
|
170
170
|
// attribute the knob runtime keeps on <html>. The chosen fill is never styled:
|
|
171
171
|
// only the others are hidden, so a fill keeps whatever display the agent gave it.
|
|
@@ -254,6 +254,60 @@ function markFill(html, part, fill) {
|
|
|
254
254
|
const at = m.index + m[0].indexOf("<") + 1 + m[1].length;
|
|
255
255
|
return html.slice(0, at) + add + html.slice(at);
|
|
256
256
|
}
|
|
257
|
+
// The name a plain part's own markup goes by once an answer adds a fill to it.
|
|
258
|
+
export const AS_IS_FILL = "as-is";
|
|
259
|
+
// How many elements declare this part, fills counted once each. A fill can only be
|
|
260
|
+
// added to a part that is one thing or already has fills.
|
|
261
|
+
export function partOccurrences(html, part) {
|
|
262
|
+
if (scanAll(html).ranges.some((r) => r.part === part))
|
|
263
|
+
return 1;
|
|
264
|
+
let n = 0;
|
|
265
|
+
walkOpenTags(html, (attrs) => {
|
|
266
|
+
if (attrs["data-part"] === part)
|
|
267
|
+
n++;
|
|
268
|
+
});
|
|
269
|
+
return n;
|
|
270
|
+
}
|
|
271
|
+
function walkOpenTags(html, visit) {
|
|
272
|
+
TOKEN.lastIndex = 0;
|
|
273
|
+
let m;
|
|
274
|
+
while ((m = TOKEN.exec(html))) {
|
|
275
|
+
if (m[0].startsWith("<!--") || m[1])
|
|
276
|
+
continue;
|
|
277
|
+
const tag = m[2].toLowerCase();
|
|
278
|
+
const end = TOKEN.lastIndex;
|
|
279
|
+
visit(attributes(m[3] ?? ""), m.index + 1 + m[2].length);
|
|
280
|
+
TOKEN.lastIndex = end;
|
|
281
|
+
if (RAW.has(tag)) {
|
|
282
|
+
const endTag = new RegExp(`</${tag}\\s*>`, "gi");
|
|
283
|
+
endTag.lastIndex = end;
|
|
284
|
+
const e = endTag.exec(html);
|
|
285
|
+
TOKEN.lastIndex = e ? endTag.lastIndex : html.length;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Adds a fill at render time, leaving the html as it was when it can't: a version
|
|
290
|
+
// without this part has nothing to fill, and one with its own fill of this name
|
|
291
|
+
// already says what that value means.
|
|
292
|
+
export function addFill(html, part, fill, markup) {
|
|
293
|
+
const ranges = scanAll(html).ranges.filter((r) => r.part === part);
|
|
294
|
+
if (ranges.some((r) => r.fill === fill))
|
|
295
|
+
return html;
|
|
296
|
+
let base = html;
|
|
297
|
+
if (ranges.length === 0) {
|
|
298
|
+
const at = [];
|
|
299
|
+
walkOpenTags(html, (attrs, tagNameEnd) => {
|
|
300
|
+
if (attrs["data-part"] === part)
|
|
301
|
+
at.push(tagNameEnd);
|
|
302
|
+
});
|
|
303
|
+
if (at.length !== 1)
|
|
304
|
+
return html;
|
|
305
|
+
base = `${html.slice(0, at[0])} data-fill="${AS_IS_FILL}" data-fill-default${html.slice(at[0])}`;
|
|
306
|
+
}
|
|
307
|
+
const spliced = spliceFills([{ kind: "html", html: base }], { [`${part}#${fill}`]: markup });
|
|
308
|
+
const surface = spliced.ok ? spliced.surfaces[0] : undefined;
|
|
309
|
+
return surface?.kind === "html" ? surface.html : html;
|
|
310
|
+
}
|
|
257
311
|
// Replace one fill of a slot, or add it after the slot's last fill, keyed
|
|
258
312
|
// "part#fill". Like spliceParts, every target is located in the original
|
|
259
313
|
// markup and overlapping edits are refused.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const GUIDE_PACKS: readonly ["design-craft"];
|
|
1
|
+
export declare const GUIDE_PACKS: readonly ["design-craft", "foundations"];
|
|
2
2
|
export type GuidePack = (typeof GUIDE_PACKS)[number];
|
|
3
3
|
export declare const RULES_SETTING = "rules";
|
|
4
4
|
export declare const USER_RULES_SETTING = "userRules";
|