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
|
@@ -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;
|
|
@@ -7,7 +8,7 @@ export interface VariantRef {
|
|
|
7
8
|
export interface InUseRef extends VariantRef {
|
|
8
9
|
version: number;
|
|
9
10
|
}
|
|
10
|
-
export interface
|
|
11
|
+
export interface FeedbackMessage {
|
|
11
12
|
seq: number;
|
|
12
13
|
text: string;
|
|
13
14
|
state: string | null;
|
|
@@ -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,20 @@ 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
|
-
|
|
51
|
-
|
|
77
|
+
messages: FeedbackMessage[];
|
|
78
|
+
inUse: InUseRef[];
|
|
52
79
|
archived: VariantRef[];
|
|
53
|
-
dropped: VariantRef[];
|
|
54
80
|
handoff?: string;
|
|
55
|
-
|
|
56
|
-
dismissed?: {
|
|
57
|
-
ask: string;
|
|
58
|
-
text: string;
|
|
59
|
-
}[];
|
|
81
|
+
edited?: DesignEdit;
|
|
60
82
|
}
|
|
61
83
|
export declare function reactionLine(c: PartComment & {
|
|
62
84
|
reaction: Reaction;
|
|
63
85
|
}): string;
|
|
64
|
-
export declare function groupFeedback(comments: Comment[],
|
|
86
|
+
export declare function groupFeedback(comments: Comment[], designs: Map<string, Design>, posts: Map<string, Post[]>, base?: string): FeedbackBatch[];
|
|
65
87
|
export declare function buildFeedbackBatches(store: Store, comments: Comment[], base: string): Promise<FeedbackBatch[]>;
|
|
66
88
|
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,8 +62,11 @@ 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
|
|
16
|
-
const
|
|
65
|
+
// A design published without variant names has only "default": naming it is noise.
|
|
66
|
+
const named = c.variant && c.variant !== DEFAULT_VARIANT ? c.variant : null;
|
|
67
|
+
// A version is written as agents write it, dark@2.
|
|
68
|
+
const at = c.version ? `@${c.version}` : "";
|
|
69
|
+
const variant = named ? `${named}${at}` : at ? `${DEFAULT_VARIANT}${at}` : null;
|
|
17
70
|
const where = [
|
|
18
71
|
c.part !== null && variant ? `variant ${variant}` : "",
|
|
19
72
|
c.state ? `state ${c.state}` : "",
|
|
@@ -28,21 +81,32 @@ export function reactionLine(c) {
|
|
|
28
81
|
return `${REACTION_LEAD[c.reaction]} ${target}${tail}${note}`;
|
|
29
82
|
}
|
|
30
83
|
const isReaction = (c) => !!c.reaction;
|
|
31
|
-
function answered(reply,
|
|
84
|
+
function answered(reply, design, posts, base) {
|
|
32
85
|
const others = reply.others ?? {};
|
|
33
|
-
const
|
|
86
|
+
const comments = reply.askComments ?? {};
|
|
34
87
|
const refs = (ids) => (ids ?? []).map((id) => ({ id, url: `${base}/a/${encodeURIComponent(id)}` }));
|
|
88
|
+
const standings = reply.standings ?? {};
|
|
35
89
|
const askIds = new Set([
|
|
36
90
|
...Object.keys(reply.answers),
|
|
37
91
|
...Object.keys(others),
|
|
38
|
-
...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),
|
|
39
96
|
]);
|
|
40
97
|
const out = [];
|
|
41
98
|
for (const askId of askIds) {
|
|
42
|
-
const ask =
|
|
99
|
+
const ask = design?.asks.find((a) => a.id === askId) ?? (design && builtinAsk(design, posts, askId));
|
|
43
100
|
const answer = reply.answers[askId];
|
|
44
101
|
const ids = answer === undefined ? [] : Array.isArray(answer) ? answer : [answer];
|
|
45
|
-
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
|
+
});
|
|
46
110
|
const otherImages = refs(reply.otherImages?.[askId]);
|
|
47
111
|
if (others[askId] !== undefined)
|
|
48
112
|
chosen.push({
|
|
@@ -51,77 +115,84 @@ function answered(reply, mock, posts, base) {
|
|
|
51
115
|
other: true,
|
|
52
116
|
...(otherImages.length ? { images: otherImages } : {}),
|
|
53
117
|
});
|
|
54
|
-
const
|
|
118
|
+
const commentImages = refs(reply.askCommentImages?.[askId]);
|
|
119
|
+
const st = standings[askId];
|
|
55
120
|
out.push({
|
|
56
121
|
ask: askId,
|
|
57
122
|
text: ask?.text ?? "",
|
|
58
123
|
chosen,
|
|
59
|
-
...(
|
|
60
|
-
...(
|
|
124
|
+
...(ask && st ? standingView({ ...ask, answer }, st) : {}),
|
|
125
|
+
...(comments[askId] !== undefined ? { comment: comments[askId] } : {}),
|
|
126
|
+
...(commentImages.length ? { commentImages } : {}),
|
|
61
127
|
});
|
|
62
128
|
}
|
|
63
129
|
return out;
|
|
64
130
|
}
|
|
65
131
|
// Comments arrive in seq order; keep that order both between batches and inside
|
|
66
|
-
// one. A
|
|
132
|
+
// one. A design gets a fresh batch whenever a second reply arrives for it, so a
|
|
67
133
|
// batch carries at most one reply and nothing is merged away. A comment whose
|
|
68
|
-
//
|
|
134
|
+
// design is unknown still gets a batch — feedback is never dropped for want of
|
|
69
135
|
// context. `base` (origin + base path) makes image URLs absolute.
|
|
70
|
-
export function groupFeedback(comments,
|
|
136
|
+
export function groupFeedback(comments, designs, posts, base = "") {
|
|
71
137
|
const batches = [];
|
|
72
138
|
const open = new Map();
|
|
73
139
|
for (const c of comments) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
+
const key = c.designId ?? "";
|
|
145
|
+
const design = c.designId ? designs.get(c.designId) : undefined;
|
|
146
|
+
const designPosts = (c.designId && posts.get(c.designId)) || [];
|
|
77
147
|
let batch = open.get(key);
|
|
78
|
-
|
|
148
|
+
// An edit says where the design is now, so it joins whatever batch is open.
|
|
149
|
+
const edit = c.kind === "reply" ? c.payload?.edited : undefined;
|
|
150
|
+
if (!batch || (c.kind === "reply" && !edit && batch.reply)) {
|
|
79
151
|
batch = {
|
|
80
|
-
|
|
81
|
-
project:
|
|
82
|
-
|
|
83
|
-
title:
|
|
152
|
+
designId: c.designId,
|
|
153
|
+
project: design?.project ?? null,
|
|
154
|
+
design: design?.slug ?? null,
|
|
155
|
+
title: design?.title ?? null,
|
|
84
156
|
reply: null,
|
|
85
|
-
|
|
86
|
-
|
|
157
|
+
messages: [],
|
|
158
|
+
inUse: [],
|
|
87
159
|
archived: [],
|
|
88
|
-
dropped: [],
|
|
89
160
|
};
|
|
90
161
|
open.set(key, batch);
|
|
91
162
|
batches.push(batch);
|
|
92
163
|
}
|
|
93
|
-
if (
|
|
94
|
-
batch.
|
|
95
|
-
batch.dismissed = c.payload.resolved.dismissed.map((id) => ({
|
|
96
|
-
ask: id,
|
|
97
|
-
text: (mock?.asks.find((a) => a.id === id) ?? (mock && builtinAsk(mock, mockPosts, id)))
|
|
98
|
-
?.text ?? "",
|
|
99
|
-
}));
|
|
164
|
+
if (edit) {
|
|
165
|
+
batch.edited = { ...batch.edited, ...edit };
|
|
100
166
|
continue;
|
|
101
167
|
}
|
|
102
168
|
if (c.kind === "reply" && c.payload) {
|
|
103
|
-
const { others: _others,
|
|
169
|
+
const { others: _others, askComments: _askComments, otherImages: _otherImages, askCommentImages: _askCommentImages, routed, more: asked, standings: _standings, update: wanted, ...payload } = c.payload;
|
|
170
|
+
const more = asked && design ? moreAnswers(design, asked, c.payload.standings?.[asked.ask]) : undefined;
|
|
171
|
+
const update = wanted && design ? updateRequest(design, wanted) : undefined;
|
|
172
|
+
if (routed)
|
|
173
|
+
batch.handoff = ROUTED_NOTE;
|
|
104
174
|
const reactions = (payload.comments ?? []).filter(isReaction).map(reactionLine);
|
|
105
175
|
batch.reply = {
|
|
106
176
|
...payload,
|
|
107
177
|
comments: (payload.comments ?? []).filter((x) => !x.reaction),
|
|
108
178
|
seq: c.seq,
|
|
109
179
|
at: c.createdAt,
|
|
110
|
-
asks: answered(c.payload,
|
|
180
|
+
asks: answered(c.payload, design, designPosts, base),
|
|
111
181
|
...(reactions.length ? { reactions } : {}),
|
|
182
|
+
...(more ? { more } : {}),
|
|
183
|
+
...(update ? { update } : {}),
|
|
112
184
|
};
|
|
113
|
-
const decisions =
|
|
185
|
+
const decisions = design ? decisionsFor(design, designPosts, c.payload) : null;
|
|
114
186
|
if (decisions)
|
|
115
187
|
batch.reply.decisions = decisions;
|
|
116
|
-
batch.
|
|
188
|
+
batch.inUse = designPosts
|
|
117
189
|
.filter((p) => p.status === "accepted")
|
|
118
190
|
.map((p) => ({ ...ref(p), version: inUseVersion(p) }));
|
|
119
|
-
batch.archived =
|
|
120
|
-
batch.dropped = mockPosts.filter((p) => p.status === "dropped").map(ref);
|
|
191
|
+
batch.archived = designPosts.filter((p) => p.status === "archived").map(ref);
|
|
121
192
|
continue;
|
|
122
193
|
}
|
|
123
|
-
const post = c.postId ?
|
|
124
|
-
batch.
|
|
194
|
+
const post = c.postId ? designPosts.find((p) => p.id === c.postId) : undefined;
|
|
195
|
+
batch.messages.push({
|
|
125
196
|
seq: c.seq,
|
|
126
197
|
text: c.text,
|
|
127
198
|
state: post?.state ?? null,
|
|
@@ -135,18 +206,18 @@ export function groupFeedback(comments, mocks, posts, base = "") {
|
|
|
135
206
|
return batches;
|
|
136
207
|
}
|
|
137
208
|
export async function buildFeedbackBatches(store, comments, base) {
|
|
138
|
-
const
|
|
209
|
+
const designs = new Map();
|
|
139
210
|
const posts = new Map();
|
|
140
211
|
for (const c of comments) {
|
|
141
|
-
if (!c.
|
|
212
|
+
if (!c.designId || designs.has(c.designId))
|
|
142
213
|
continue;
|
|
143
|
-
const
|
|
144
|
-
if (!
|
|
214
|
+
const design = await store.getDesign(c.designId);
|
|
215
|
+
if (!design)
|
|
145
216
|
continue;
|
|
146
|
-
|
|
147
|
-
posts.set(c.
|
|
217
|
+
designs.set(c.designId, design);
|
|
218
|
+
posts.set(c.designId, await store.listPosts({ designId: c.designId }));
|
|
148
219
|
}
|
|
149
|
-
return groupFeedback(comments,
|
|
220
|
+
return groupFeedback(comments, designs, posts, base);
|
|
150
221
|
}
|
|
151
222
|
// Every image a list of batches carries, in order — what the MCP tiers attach
|
|
152
223
|
// as image content so the model sees what the user attached.
|
|
@@ -157,8 +228,8 @@ export function feedbackImages(batches) {
|
|
|
157
228
|
for (const c of a.chosen)
|
|
158
229
|
if ("other" in c && c.images)
|
|
159
230
|
out.push(...c.images);
|
|
160
|
-
if (a.
|
|
161
|
-
out.push(...a.
|
|
231
|
+
if (a.commentImages)
|
|
232
|
+
out.push(...a.commentImages);
|
|
162
233
|
}
|
|
163
234
|
}
|
|
164
235
|
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.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const GUIDE_PACKS: readonly ["design-craft"];
|
|
2
|
+
export type GuidePack = (typeof GUIDE_PACKS)[number];
|
|
3
|
+
export declare const RULES_SETTING = "rules";
|
|
4
|
+
export declare const USER_RULES_SETTING = "userRules";
|
|
5
|
+
export declare const MAX_RULES_CHARS = 6000;
|
|
6
|
+
export interface RulesFile {
|
|
7
|
+
packs: string[];
|
|
8
|
+
body: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function parseRulesFile(text: string): RulesFile;
|
|
11
|
+
export interface GuidanceInput {
|
|
12
|
+
packs: Partial<Record<GuidePack, string>>;
|
|
13
|
+
user: string | null;
|
|
14
|
+
project: string | null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The guidance line and sections for the brief, or "" when only core is on:
|
|
18
|
+
* a workspace that never wrote a rules file gets the brief it always had.
|
|
19
|
+
*/
|
|
20
|
+
export declare function guidanceSections(input: GuidanceInput): {
|
|
21
|
+
line: string;
|
|
22
|
+
body: string;
|
|
23
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Guidance layers on top of the core brief: packs that ship with mockpit (off
|
|
2
|
+
// until a rules file names them), the user's own rules and the project's.
|
|
3
|
+
// Higher wins: project, then the user's, then packs, then core. The brief
|
|
4
|
+
// prints them in that order from the bottom up, so the last word an agent
|
|
5
|
+
// reads is the one that wins.
|
|
6
|
+
//
|
|
7
|
+
// Runtime-agnostic (no node imports): the Worker builds the brief too.
|
|
8
|
+
export const GUIDE_PACKS = ["design-craft"];
|
|
9
|
+
const isGuidePack = (id) => GUIDE_PACKS.includes(id);
|
|
10
|
+
// `rules:<project>` sits beside `design:<project>` and moves with a project
|
|
11
|
+
// rename. The user's own rules are not a project, so their key has no colon:
|
|
12
|
+
// a project named "user" can't collide with it.
|
|
13
|
+
export const RULES_SETTING = "rules";
|
|
14
|
+
export const USER_RULES_SETTING = "userRules";
|
|
15
|
+
// A rules file is read whole into every brief, so it stays a page, not a book.
|
|
16
|
+
export const MAX_RULES_CHARS = 6_000;
|
|
17
|
+
// `---\npacks: [design-craft]\n---` (or `packs: a, b`, or a `- a` list), then
|
|
18
|
+
// markdown. Anything else in the frontmatter is ignored rather than refused:
|
|
19
|
+
// the file is the user's, and a stray key must not cost them their rules.
|
|
20
|
+
export function parseRulesFile(text) {
|
|
21
|
+
const src = text.replace(/\r\n?/g, "\n");
|
|
22
|
+
const m = /^---\n([\s\S]*?)\n---[ \t]*(?:\n|$)/.exec(src);
|
|
23
|
+
if (!m)
|
|
24
|
+
return { packs: [], body: src.trim() };
|
|
25
|
+
const packs = [];
|
|
26
|
+
const lines = m[1].split("\n");
|
|
27
|
+
for (let i = 0; i < lines.length; i++) {
|
|
28
|
+
const kv = /^packs\s*:\s*(.*)$/.exec(lines[i].trim());
|
|
29
|
+
if (!kv)
|
|
30
|
+
continue;
|
|
31
|
+
const inline = kv[1].trim().replace(/^\[|\]$/g, "");
|
|
32
|
+
if (inline)
|
|
33
|
+
packs.push(...inline.split(","));
|
|
34
|
+
for (let j = i + 1; j < lines.length && /^\s*-\s+/.test(lines[j]); j++) {
|
|
35
|
+
packs.push(lines[j].replace(/^\s*-\s+/, ""));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const names = packs.map((p) => p.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
39
|
+
return { packs: [...new Set(names)], body: src.slice(m[0].length).trim() };
|
|
40
|
+
}
|
|
41
|
+
// The layer files' own `## Rules` / `## Suggestions` sit one level under the
|
|
42
|
+
// layer's heading in the brief.
|
|
43
|
+
const demote = (md) => md.replace(/^(#{1,5}) /gm, "#$1 ");
|
|
44
|
+
/**
|
|
45
|
+
* The guidance line and sections for the brief, or "" when only core is on:
|
|
46
|
+
* a workspace that never wrote a rules file gets the brief it always had.
|
|
47
|
+
*/
|
|
48
|
+
export function guidanceSections(input) {
|
|
49
|
+
const user = input.user ? parseRulesFile(input.user) : null;
|
|
50
|
+
const project = input.project ? parseRulesFile(input.project) : null;
|
|
51
|
+
const named = [...new Set([...(user?.packs ?? []), ...(project?.packs ?? [])])];
|
|
52
|
+
const on = named.filter((p) => isGuidePack(p) && !!input.packs[p]);
|
|
53
|
+
const unknown = named.filter((p) => !on.includes(p));
|
|
54
|
+
const layers = [
|
|
55
|
+
...on.map((p) => ({ label: p, heading: `Pack: ${p}`, text: input.packs[p].trim() })),
|
|
56
|
+
...(user?.body ? [{ label: "your rules", heading: "Your rules", text: user.body }] : []),
|
|
57
|
+
...(project?.body
|
|
58
|
+
? [{ label: "project", heading: "This project's rules", text: project.body }]
|
|
59
|
+
: []),
|
|
60
|
+
];
|
|
61
|
+
if (layers.length === 0 && unknown.length === 0)
|
|
62
|
+
return { line: "", body: "" };
|
|
63
|
+
const missing = unknown.length
|
|
64
|
+
? ` (unknown pack ${unknown.map((p) => `"${p}"`).join(", ")}; packs: ${GUIDE_PACKS.join(", ")})`
|
|
65
|
+
: "";
|
|
66
|
+
const line = `Guidance: ${["core", ...layers.map((l) => l.label)].join(" · ")}${missing}. ` +
|
|
67
|
+
"Higher wins: project, then yours, then packs, then core; the user's own words beat " +
|
|
68
|
+
"all of them. **Rules** are always followed; **Suggestions** are aimed for.";
|
|
69
|
+
const body = layers.map((l) => `## ${l.heading}\n\n${demote(l.text)}`).join("\n\n");
|
|
70
|
+
return { line, body };
|
|
71
|
+
}
|
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);
|