@hybridlabor-api/aos 4.17.0 → 4.18.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/.claude/hooks/aos-bus.mjs +8 -4
- package/.claude/hooks/go-gate.mjs +1 -1
- package/.claude/hooks/go-token.mjs +17 -2
- package/.claude/hooks/memb-inject.mjs +61 -37
- package/.codex-plugin/plugin.json +1 -1
- package/.opencode/commands/bdb-aos-plan.md +1 -1
- package/README.md +1 -0
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/bin/aos-acp.mjs +27 -1
- package/bin/aos-doctor.mjs +36 -1
- package/bin/aos-uninstall.mjs +16 -2
- package/bin/go-check.mjs +79 -0
- package/bin/guarded-patterns.json +106 -0
- package/commands/plan.md +1 -1
- package/docs/codenotch.md +44 -0
- package/docs/codex-gate-smoke.md +43 -0
- package/docs/delegation-routing.md +32 -0
- package/docs/go-check.md +60 -0
- package/docs/master-session-acp.md +2 -0
- package/docs/opencode-setup.md +18 -0
- package/installer.js +129 -70
- package/lib/codenotch.js +389 -0
- package/lib/retired-skills.js +101 -0
- package/mcps/mcsc/README.md +1 -1
- package/mcps/mcsc/packages/core/src/adapters/agy.js +3 -1
- package/mcps/mcsc/packages/core/src/adapters/codex.js +2 -1
- package/mcps/mcsc/packages/core/src/adapters/opencode.js +2 -1
- package/mcps/mcsc/packages/core/src/depth.js +16 -0
- package/mcps/mcsc/packages/mcp/server.js +15 -2
- package/package.json +2 -2
- package/plugin-commands.json +1 -2
- package/plugin.json +1 -4
- package/plugins/bdb-aos-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/bdb-aos-codex/skills/plan/SKILL.md +1 -1
- package/scripts/codex-gate-smoke.mjs +73 -0
- package/skills/basic/master-session/SKILL.md +11 -0
- package/skills/global_config/agenttrail/SKILL.md +3 -1
- package/skills/global_config/agenttrail/bin/agenttrail.mjs +255 -117
- package/skills/global_config/agenttrail/bin/ensure.mjs +60 -40
- package/skills/global_config/agenttrail/bin/repoid.mjs +70 -0
- package/skills/global_config/agenttrail/public/index.html +9 -1
- package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +1 -1
- package/skills/global_config/bdb-memb-mcp/SKILL.md +5 -4
- package/skills/global_config/bdb-visual-edit/SKILL.md +28 -32
- package/skills/global_config/bdb-visual-edit/references/vite-react-source-attr.md +2 -2
- package/skills/global_config/bdb-visual-edit/scripts/locate-source.mjs +135 -0
- package/skills/global_config/bdb-visual-edit/scripts/sanitize-element.mjs +30 -2
- package/skills/global_config/mcsc/SKILL.md +9 -1
- package/skills/global_config/plan-arbiter/SKILL.md +1 -1
- package/skills/global_config/plan-canvas/SKILL.md +42 -4
- package/skills/global_config/plan-canvas/scripts/lib/plan-builder/README.md +1 -1
- package/skills/global_config/plan-canvas/scripts/lib/plan-builder/render.js +2 -2
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-client/geometry.js +76 -0
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-client/index.js +596 -0
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-client/model.js +192 -0
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-client/toolbar.js +99 -0
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-server.js +282 -0
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotation-schema.js +210 -0
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/route.js +10 -0
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/sdk.js +6 -230
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/server.js +45 -4
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/sessions.js +60 -21
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/trail-on-approve.js +103 -0
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/ui.js +19 -7
- package/skills/global_config/plan-canvas/scripts/plan-canvas.js +118 -20
- package/skills/global_config/subagent-setup/SKILL.md +6 -0
- package/skills/global_config/subagent-setup/scripts/setup-subagents.mjs +20 -1
- package/skills/playbooks/pb-idea-to-launch/SKILL.md +2 -2
- package/skills/playbooks/pb-redesign-app/SKILL.md +3 -3
- package/skills/playbooks/pb-release-aos/SKILL.md +2 -2
- package/skills/playbooks/pb-ship/SKILL.md +2 -2
- package/skills/playbooks/pb-worktrees-land/SKILL.md +2 -2
- package/skills/global_config/bdb-visual-edit/scripts/pick-snippet.js +0 -27
- package/skills/global_config/visual-edit/README.md +0 -96
- package/skills/global_config/visual-edit/SKILL.md +0 -615
- package/skills/global_config/visual-plan/README.md +0 -93
- package/skills/global_config/visual-plan/SKILL.md +0 -544
- package/skills/global_config/visual-plan/references/canvas.md +0 -139
- package/skills/global_config/visual-plan/references/connection.md +0 -51
- package/skills/global_config/visual-plan/references/document-quality.md +0 -186
- package/skills/global_config/visual-plan/references/exemplar.md +0 -62
- package/skills/global_config/visual-plan/references/local-files.md +0 -99
- package/skills/global_config/visual-plan/references/wireframe.md +0 -319
- package/skills/global_config/visual-recap/README.md +0 -103
- package/skills/global_config/visual-recap/SKILL.md +0 -560
- package/skills/global_config/visual-recap/references/connection.md +0 -51
- package/skills/global_config/visual-recap/references/local-files.md +0 -99
- package/skills/global_config/visual-recap/references/wireframe.md +0 -319
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plan Canvas annotation schema: whitelist normaliser for browser-submitted
|
|
5
|
+
* annotations. Output is rebuilt field by field; nothing from the input object
|
|
6
|
+
* is copied through, unknown keys are dropped, types are never coerced.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const MAX_TEXT = 4000;
|
|
12
|
+
const MAX_SHAPES = 16;
|
|
13
|
+
const MAX_POINTS_TOTAL = 2048;
|
|
14
|
+
const MAX_FREEHAND_POINTS = 512;
|
|
15
|
+
const MAX_CLASSES = 12;
|
|
16
|
+
const MAX_CLASS_SCAN = 200;
|
|
17
|
+
const BBOX_LIMIT = 100000;
|
|
18
|
+
const POINT_MIN = -4;
|
|
19
|
+
const POINT_MAX = 5;
|
|
20
|
+
|
|
21
|
+
const SHAPE_POINTS = {
|
|
22
|
+
element: [0, 0],
|
|
23
|
+
text: [0, 0],
|
|
24
|
+
rect: [2, 2],
|
|
25
|
+
arrow: [2, 2],
|
|
26
|
+
freehand: [2, MAX_FREEHAND_POINTS],
|
|
27
|
+
blur: [2, 2],
|
|
28
|
+
comment: [1, 1]
|
|
29
|
+
};
|
|
30
|
+
const COLORS = new Set(['red', 'yellow', 'blue', 'green']);
|
|
31
|
+
const TAG_RE = /^[A-Za-z][A-Za-z0-9-]*$/;
|
|
32
|
+
const TAG_MAX = 60;
|
|
33
|
+
const SEGMENT_RE = /^(?:[a-z][a-z0-9-]{0,30}:nth-of-type\([1-9]\d{0,3}\)|html|body|#[A-Za-z0-9_-]{1,64})$/;
|
|
34
|
+
const MAX_SEGMENTS = 12;
|
|
35
|
+
// Copy of bdb-visual-edit's sanitize-element.mjs allowlist; tests keep them in step.
|
|
36
|
+
const SRC_EXTS = new Set(['.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro', '.html', '.mdx']);
|
|
37
|
+
const SRC_SKIP_DIRS = new Set(['node_modules', 'dist', 'build']);
|
|
38
|
+
const CONFIG_NAME_RE = /\.config\.[^/]*$/i;
|
|
39
|
+
const CLASS_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
40
|
+
const SRC_RE = /^[\w@.-]+(?:\/[\w@.-]+)*\.[A-Za-z0-9]{1,8}:\d{1,6}(?::\d{1,5})?$/;
|
|
41
|
+
const ORIGIN_RE = /^http:\/\/(127\.0\.0\.1|localhost|\[::1\]):(\d{1,5})$/i;
|
|
42
|
+
const LOOPBACK_URL_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);
|
|
43
|
+
const CONTROL_RE = /[\u0000-\u001f\u007f-\u009f---]/g;
|
|
44
|
+
const WHITESPACE_CONTROL_RE = /[\t\n\r]+/g;
|
|
45
|
+
|
|
46
|
+
const own = (obj, key) => (obj !== null && typeof obj === 'object' && Object.hasOwn(obj, key) ? obj[key] : undefined);
|
|
47
|
+
const isNum = value => typeof value === 'number' && Number.isFinite(value);
|
|
48
|
+
|
|
49
|
+
// Tabs and line breaks become one space so multi-line comments stay readable;
|
|
50
|
+
// every other control, bidi and zero-width character is removed.
|
|
51
|
+
function cleanText(value, max = MAX_TEXT) {
|
|
52
|
+
if (typeof value !== 'string') return '';
|
|
53
|
+
let out = value.replace(WHITESPACE_CONTROL_RE, ' ').replace(CONTROL_RE, '').slice(0, max);
|
|
54
|
+
if (/[\ud800-\udbff]$/.test(out)) out = out.slice(0, -1);
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Returns the canonical `http://host:port` for a loopback app origin, else null.
|
|
59
|
+
function normalizeOrigin(value) {
|
|
60
|
+
if (typeof value !== 'string') return null;
|
|
61
|
+
const match = ORIGIN_RE.exec(value);
|
|
62
|
+
if (!match) return null;
|
|
63
|
+
const port = Number(match[2]);
|
|
64
|
+
if (port < 1024 || port > 65535) return null;
|
|
65
|
+
return `http://${match[1].toLowerCase()}:${port}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function cleanSrcLoc(value) {
|
|
69
|
+
if (typeof value !== 'string' || value.length > 240 || !SRC_RE.test(value)) return null;
|
|
70
|
+
const file = value.slice(0, value.search(/:\d/));
|
|
71
|
+
if (file.split('/').some(s => s.startsWith('.') || SRC_SKIP_DIRS.has(s))) return null;
|
|
72
|
+
if (!SRC_EXTS.has(path.extname(file)) || CONFIG_NAME_RE.test(file)) return null;
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function cleanAppUrl(value, boundOrigin) {
|
|
77
|
+
const fallback = boundOrigin || null;
|
|
78
|
+
if (typeof value !== 'string' || value.length > 2000) return fallback;
|
|
79
|
+
try {
|
|
80
|
+
const url = new URL(value);
|
|
81
|
+
if (url.protocol !== 'http:' || !LOOPBACK_URL_HOSTS.has(url.hostname) || url.port === '') return fallback;
|
|
82
|
+
if (!boundOrigin || normalizeOrigin(url.origin) !== boundOrigin) return fallback;
|
|
83
|
+
const clean = `${url.origin}${url.pathname}`;
|
|
84
|
+
return clean.length <= 300 ? clean : fallback;
|
|
85
|
+
} catch {
|
|
86
|
+
return fallback;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function cleanTarget(raw, origin, boundOrigin) {
|
|
91
|
+
if (origin !== 'app') return { origin: 'canvas', url: null };
|
|
92
|
+
const target = { origin: 'app', url: cleanAppUrl(own(own(raw, 'target'), 'url'), boundOrigin) };
|
|
93
|
+
const srcLoc = cleanSrcLoc(own(own(raw, 'target'), 'srcLoc'));
|
|
94
|
+
if (srcLoc) target.srcLoc = srcLoc;
|
|
95
|
+
return target;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function cleanClasses(value) {
|
|
99
|
+
if (!Array.isArray(value)) return [];
|
|
100
|
+
const out = [];
|
|
101
|
+
for (let i = 0; i < Math.min(value.length, MAX_CLASS_SCAN) && out.length < MAX_CLASSES; i++) {
|
|
102
|
+
const c = value[i];
|
|
103
|
+
if (typeof c === 'string' && CLASS_RE.test(c) && !out.includes(c)) out.push(c);
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Only the tag:nth-of-type grammar the client generates passes; free text is blanked.
|
|
109
|
+
function cleanSelector(value) {
|
|
110
|
+
if (typeof value !== 'string' || value.length > 600) return '';
|
|
111
|
+
const parts = value.split(' > ');
|
|
112
|
+
return parts.length <= MAX_SEGMENTS && parts.every(p => SEGMENT_RE.test(p)) ? value : '';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function cleanAnchor(raw) {
|
|
116
|
+
const selector = own(raw, 'selector');
|
|
117
|
+
if (typeof selector !== 'string') return null;
|
|
118
|
+
const tag = own(raw, 'tag');
|
|
119
|
+
const anchor = {
|
|
120
|
+
selector: cleanSelector(selector),
|
|
121
|
+
tag: typeof tag === 'string' && TAG_RE.test(tag) ? tag.slice(0, TAG_MAX) : '',
|
|
122
|
+
snippet: cleanText(own(raw, 'snippet'), 400)
|
|
123
|
+
};
|
|
124
|
+
const classes = cleanClasses(own(raw, 'classes'));
|
|
125
|
+
if (classes.length) anchor.classes = classes;
|
|
126
|
+
const range = own(raw, 'textRange');
|
|
127
|
+
if (range !== null && typeof range === 'object') {
|
|
128
|
+
anchor.textRange = { text: cleanText(own(range, 'text'), 1000) };
|
|
129
|
+
}
|
|
130
|
+
return anchor;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const roundTo = (n, digits) => Number(n.toFixed(digits));
|
|
134
|
+
const clampPoint = n => roundTo(Math.min(POINT_MAX, Math.max(POINT_MIN, n)), 4);
|
|
135
|
+
|
|
136
|
+
// Any malformed shape rejects the whole item: a half-drawn annotation would
|
|
137
|
+
// mislead the agent about what the human marked.
|
|
138
|
+
function cleanShapes(value) {
|
|
139
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > MAX_SHAPES) return null;
|
|
140
|
+
const shapes = [];
|
|
141
|
+
let total = 0;
|
|
142
|
+
for (const rawShape of value) {
|
|
143
|
+
const type = own(rawShape, 'type');
|
|
144
|
+
const limits = typeof type === 'string' && Object.hasOwn(SHAPE_POINTS, type) ? SHAPE_POINTS[type] : null;
|
|
145
|
+
if (!limits) return null;
|
|
146
|
+
const shape = { type };
|
|
147
|
+
if (limits[1] > 0) {
|
|
148
|
+
const points = own(rawShape, 'points');
|
|
149
|
+
if (!Array.isArray(points) || points.length < limits[0] || points.length > limits[1]) return null;
|
|
150
|
+
total += points.length;
|
|
151
|
+
if (total > MAX_POINTS_TOTAL) return null;
|
|
152
|
+
shape.points = [];
|
|
153
|
+
for (const p of points) {
|
|
154
|
+
if (!Array.isArray(p) || p.length !== 2 || !isNum(p[0]) || !isNum(p[1])) return null;
|
|
155
|
+
shape.points.push([clampPoint(p[0]), clampPoint(p[1])]);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const color = own(rawShape, 'color');
|
|
159
|
+
if (typeof color === 'string' && COLORS.has(color)) shape.color = color;
|
|
160
|
+
shapes.push(shape);
|
|
161
|
+
}
|
|
162
|
+
return shapes;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function cleanViewport(raw) {
|
|
166
|
+
const w = own(raw, 'w');
|
|
167
|
+
const h = own(raw, 'h');
|
|
168
|
+
const dpr = own(raw, 'dpr');
|
|
169
|
+
if (!isNum(w) || !isNum(h) || !isNum(dpr)) return null;
|
|
170
|
+
const out = { w: Math.round(w), h: Math.round(h), dpr: roundTo(dpr, 2) };
|
|
171
|
+
if (out.w < 1 || out.w > BBOX_LIMIT || out.h < 1 || out.h > BBOX_LIMIT || out.dpr < 0.5 || out.dpr > 8) return null;
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function cleanPage(raw) {
|
|
176
|
+
const out = {};
|
|
177
|
+
for (const k of ['x', 'y', 'w', 'h']) {
|
|
178
|
+
const n = own(raw, k);
|
|
179
|
+
if (!isNum(n)) return null;
|
|
180
|
+
out[k] = Math.round(Math.min(BBOX_LIMIT, Math.max(0, n)));
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @param raw untrusted annotation object
|
|
187
|
+
* @param origin 'canvas' | 'app', set by the endpoint, never by the body
|
|
188
|
+
* @param boundOrigin the token's bound app origin (app only)
|
|
189
|
+
* @returns the validated item fields (without id/at/kind) or null
|
|
190
|
+
*/
|
|
191
|
+
function normalizeAnnotation(raw, { origin = 'canvas', boundOrigin = null } = {}) {
|
|
192
|
+
if (raw === null || typeof raw !== 'object') return null;
|
|
193
|
+
const text = cleanText(own(raw, 'text'));
|
|
194
|
+
if (!text) return null;
|
|
195
|
+
const anchor = cleanAnchor(own(raw, 'anchor'));
|
|
196
|
+
if (!anchor) return null;
|
|
197
|
+
const fields = { text, anchor, target: cleanTarget(raw, origin === 'app' ? 'app' : 'canvas', boundOrigin) };
|
|
198
|
+
if (own(raw, 'shapes') !== undefined) {
|
|
199
|
+
const shapes = cleanShapes(own(raw, 'shapes'));
|
|
200
|
+
if (!shapes) return null;
|
|
201
|
+
fields.shapes = shapes;
|
|
202
|
+
}
|
|
203
|
+
const viewport = cleanViewport(own(raw, 'viewport'));
|
|
204
|
+
if (viewport) fields.viewport = viewport;
|
|
205
|
+
const page = cleanPage(own(raw, 'page'));
|
|
206
|
+
if (page) fields.page = page;
|
|
207
|
+
return fields;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = { cleanSrcLoc, cleanText, normalizeAnnotation, normalizeOrigin };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function routeFor(item, { planHasComponents = false } = {}) {
|
|
4
|
+
if (!item || typeof item !== 'object') return 'artifact';
|
|
5
|
+
if (item.kind === 'annotation' && item.target && item.target.origin === 'app') return 'visual-edit';
|
|
6
|
+
if (item.kind === 'verdict' && item.verdict === 'approve' && planHasComponents) return 'build';
|
|
7
|
+
return 'artifact';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
module.exports = { routeFor };
|
|
@@ -1,239 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Plan Canvas artifact SDK
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* script can only talk to the chrome via postMessage. It renders all of its
|
|
8
|
-
* own UI inside a shadow root so it never annotates itself and never leaks
|
|
9
|
-
* styles into the artifact.
|
|
10
|
-
*
|
|
11
|
-
* Source: affaan-m/ECC — MIT, see THIRD_PARTY_NOTICES.md
|
|
4
|
+
* Plan Canvas artifact SDK: the script injected into the reviewed artifact.
|
|
5
|
+
* The layer itself lives in ./annotate-client (shared with /annotate.js).
|
|
6
|
+
* Source: affaan-m/ECC (MIT), see THIRD_PARTY_NOTICES.md
|
|
12
7
|
*/
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
return `'use strict';
|
|
16
|
-
(() => {
|
|
17
|
-
if (window.parent === window) return; // only meaningful inside the canvas
|
|
18
|
-
if (window.__eccPlanCanvasSdk) return;
|
|
19
|
-
window.__eccPlanCanvasSdk = true;
|
|
20
|
-
|
|
21
|
-
let annotate = true;
|
|
22
|
-
let card = null;
|
|
23
|
-
|
|
24
|
-
const post = msg => window.parent.postMessage(msg, '*');
|
|
25
|
-
|
|
26
|
-
// --- shadow-root UI host --------------------------------------------
|
|
27
|
-
const host = document.createElement('div');
|
|
28
|
-
host.setAttribute('data-ecc-plan-canvas', 'ui');
|
|
29
|
-
host.style.cssText = 'position:absolute;top:0;left:0;width:0;height:0;z-index:2147483647';
|
|
30
|
-
const root = host.attachShadow({ mode: 'open' });
|
|
31
|
-
root.innerHTML = \`
|
|
32
|
-
<style>
|
|
33
|
-
:host{all:initial}
|
|
34
|
-
.hl{position:fixed;pointer-events:none;border:1.5px solid #6885e8;background:rgba(104,133,232,0.12);border-radius:4px;display:none;z-index:2147483646;transition:all .06s ease-out}
|
|
35
|
-
.selhint{position:absolute;display:none;z-index:2147483647;background:#101218;color:#dfe2e9;border:1px solid #272c3e;border-radius:6px;padding:4px 10px;font:600 11.5px -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;cursor:pointer;box-shadow:0 8px 32px rgba(0,0,0,0.6)}
|
|
36
|
-
.selhint:hover{border-color:#6885e8}
|
|
37
|
-
.card{position:absolute;display:none;z-index:2147483647;width:300px;background:#101218;border:1px solid #272c3e;border-radius:8px;box-shadow:0 8px 32px rgba(0,0,0,0.6);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#dfe2e9}
|
|
38
|
-
.card h4{margin:0;padding:10px 12px 0;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:#80859a}
|
|
39
|
-
.card .snippet{padding:4px 12px 0;font:10.5px 'SF Mono','Fira Code',monospace;color:#4acbbe;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
40
|
-
.card textarea{display:block;width:calc(100% - 24px);margin:8px 12px;min-height:56px;resize:vertical;background:#13161e;border:1px solid #1d2130;border-radius:6px;color:#dfe2e9;font:12.5px -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;padding:7px 9px;outline:none;box-sizing:border-box}
|
|
41
|
-
.card textarea:focus{border-color:#6885e8}
|
|
42
|
-
.card .row{display:flex;justify-content:flex-end;gap:8px;padding:0 12px 12px}
|
|
43
|
-
.card button{border-radius:6px;font:600 11.5px -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;padding:5px 12px;cursor:pointer}
|
|
44
|
-
.card .cancel{background:none;border:1px solid #1d2130;color:#80859a}
|
|
45
|
-
.card .cancel:hover{color:#dfe2e9;border-color:#272c3e}
|
|
46
|
-
.card .queue{background:#6885e8;border:1px solid #6885e8;color:#fff}
|
|
47
|
-
.card .queue:hover{background:#3d5ab8}
|
|
48
|
-
.card .keys{padding:0 12px 10px;font-size:9.5px;color:#4c5168}
|
|
49
|
-
</style>
|
|
50
|
-
<div class="hl"></div>
|
|
51
|
-
<button class="selhint" type="button">Annotate selection</button>
|
|
52
|
-
<div class="card">
|
|
53
|
-
<h4></h4>
|
|
54
|
-
<div class="snippet"></div>
|
|
55
|
-
<textarea placeholder="What should change here?"></textarea>
|
|
56
|
-
<div class="row">
|
|
57
|
-
<button class="cancel" type="button">Cancel</button>
|
|
58
|
-
<button class="queue" type="button">Queue</button>
|
|
59
|
-
</div>
|
|
60
|
-
<div class="keys">Enter to queue · Cmd/Ctrl+Enter to queue & send</div>
|
|
61
|
-
</div>\`;
|
|
62
|
-
const attach = () => document.body ? document.body.appendChild(host) : null;
|
|
63
|
-
if (document.body) attach();
|
|
64
|
-
else document.addEventListener('DOMContentLoaded', attach);
|
|
65
|
-
|
|
66
|
-
const hl = root.querySelector('.hl');
|
|
67
|
-
const selhint = root.querySelector('.selhint');
|
|
68
|
-
const cardEl = root.querySelector('.card');
|
|
69
|
-
const cardTitle = cardEl.querySelector('h4');
|
|
70
|
-
const cardSnippet = cardEl.querySelector('.snippet');
|
|
71
|
-
const cardText = cardEl.querySelector('textarea');
|
|
72
|
-
|
|
73
|
-
// --- selectors & context ---------------------------------------------
|
|
74
|
-
const esc = v => (window.CSS && CSS.escape) ? CSS.escape(v) : v.replace(/[^a-zA-Z0-9_-]/g, '\\\\$&');
|
|
75
|
-
function selectorFor(el) {
|
|
76
|
-
const parts = [];
|
|
77
|
-
let node = el;
|
|
78
|
-
for (let depth = 0; node && node.nodeType === 1 && depth < 6; depth++) {
|
|
79
|
-
if (node.id) { parts.unshift('#' + esc(node.id)); return parts.join(' > '); }
|
|
80
|
-
const tag = node.tagName.toLowerCase();
|
|
81
|
-
if (tag === 'body' || tag === 'html') { parts.unshift(tag); break; }
|
|
82
|
-
let nth = 1;
|
|
83
|
-
let sib = node;
|
|
84
|
-
while ((sib = sib.previousElementSibling)) if (sib.tagName === node.tagName) nth++;
|
|
85
|
-
parts.unshift(tag + ':nth-of-type(' + nth + ')');
|
|
86
|
-
node = node.parentElement;
|
|
87
|
-
}
|
|
88
|
-
return parts.join(' > ');
|
|
89
|
-
}
|
|
90
|
-
function snippetFor(el) {
|
|
91
|
-
return (el.innerText || el.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 200);
|
|
92
|
-
}
|
|
93
|
-
const INTERACTIVE = new Set(['button', 'input', 'select', 'textarea', 'option', 'label', 'summary', 'a']);
|
|
94
|
-
function isInteractive(el) {
|
|
95
|
-
let node = el;
|
|
96
|
-
while (node && node.nodeType === 1) {
|
|
97
|
-
if (INTERACTIVE.has(node.tagName.toLowerCase()) || node.isContentEditable) return true;
|
|
98
|
-
node = node.parentElement;
|
|
99
|
-
}
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
const isOurs = el => el === host || host.contains(el);
|
|
103
|
-
|
|
104
|
-
// --- annotation card ---------------------------------------------------
|
|
105
|
-
function openCard(target) {
|
|
106
|
-
card = target;
|
|
107
|
-
cardTitle.textContent = target.kindLabel;
|
|
108
|
-
cardSnippet.textContent = target.anchor.snippet || target.anchor.selector;
|
|
109
|
-
cardText.value = '';
|
|
110
|
-
cardEl.style.display = 'block';
|
|
111
|
-
const x = Math.min(target.x, window.innerWidth - 320) + window.scrollX;
|
|
112
|
-
const y = target.y + 12 + window.scrollY;
|
|
113
|
-
cardEl.style.left = Math.max(8, x) + 'px';
|
|
114
|
-
cardEl.style.top = y + 'px';
|
|
115
|
-
cardText.focus();
|
|
116
|
-
}
|
|
117
|
-
function closeCard() {
|
|
118
|
-
card = null;
|
|
119
|
-
cardEl.style.display = 'none';
|
|
120
|
-
}
|
|
121
|
-
function queueCard(sendNow) {
|
|
122
|
-
if (!card) return;
|
|
123
|
-
const text = cardText.value.trim();
|
|
124
|
-
if (!text) { cardText.focus(); return; }
|
|
125
|
-
post({
|
|
126
|
-
type: sendNow ? 'pc:queue-and-send' : 'pc:queue',
|
|
127
|
-
item: { kind: 'annotation', text, anchor: card.anchor }
|
|
128
|
-
});
|
|
129
|
-
closeCard();
|
|
130
|
-
}
|
|
131
|
-
cardEl.querySelector('.cancel').addEventListener('click', closeCard);
|
|
132
|
-
cardEl.querySelector('.queue').addEventListener('click', () => queueCard(false));
|
|
133
|
-
cardText.addEventListener('keydown', e => {
|
|
134
|
-
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); queueCard(true); }
|
|
135
|
-
else if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); queueCard(false); }
|
|
136
|
-
else if (e.key === 'Escape') closeCard();
|
|
137
|
-
});
|
|
9
|
+
const { annotateClientJs } = require('./annotate-client');
|
|
138
10
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (!annotate || card) { hl.style.display = 'none'; return; }
|
|
142
|
-
const el = e.target;
|
|
143
|
-
if (!el || isOurs(el) || el === document.body || el === document.documentElement || isInteractive(el)) {
|
|
144
|
-
hl.style.display = 'none';
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
const rect = el.getBoundingClientRect();
|
|
148
|
-
hl.style.display = 'block';
|
|
149
|
-
hl.style.left = rect.left - 2 + 'px';
|
|
150
|
-
hl.style.top = rect.top - 2 + 'px';
|
|
151
|
-
hl.style.width = rect.width + 'px';
|
|
152
|
-
hl.style.height = rect.height + 'px';
|
|
153
|
-
}, true);
|
|
154
|
-
|
|
155
|
-
document.addEventListener('click', e => {
|
|
156
|
-
if (!annotate) return;
|
|
157
|
-
const el = e.target;
|
|
158
|
-
if (isOurs(el)) return;
|
|
159
|
-
if (card) { if (!cardEl.contains(e.composedPath()[0])) closeCard(); return; }
|
|
160
|
-
if (isInteractive(el)) return; // let controls behave natively
|
|
161
|
-
const selection = window.getSelection();
|
|
162
|
-
if (selection && !selection.isCollapsed) return; // handled by selection flow
|
|
163
|
-
if (el === document.body || el === document.documentElement) return;
|
|
164
|
-
e.preventDefault();
|
|
165
|
-
e.stopPropagation();
|
|
166
|
-
hl.style.display = 'none';
|
|
167
|
-
openCard({
|
|
168
|
-
kindLabel: 'Annotate <' + el.tagName.toLowerCase() + '>',
|
|
169
|
-
anchor: { selector: selectorFor(el), tag: el.tagName.toLowerCase(), snippet: snippetFor(el) },
|
|
170
|
-
x: e.clientX,
|
|
171
|
-
y: e.clientY
|
|
172
|
-
});
|
|
173
|
-
}, true);
|
|
174
|
-
|
|
175
|
-
// --- text selection -------------------------------------------------------
|
|
176
|
-
document.addEventListener('mouseup', e => {
|
|
177
|
-
if (!annotate || card || isOurs(e.target)) return;
|
|
178
|
-
setTimeout(() => {
|
|
179
|
-
const selection = window.getSelection();
|
|
180
|
-
const text = selection ? String(selection).replace(/\\s+/g, ' ').trim() : '';
|
|
181
|
-
if (!text || !selection.rangeCount) { selhint.style.display = 'none'; return; }
|
|
182
|
-
const rect = selection.getRangeAt(0).getBoundingClientRect();
|
|
183
|
-
selhint.style.display = 'block';
|
|
184
|
-
selhint.style.left = rect.left + window.scrollX + 'px';
|
|
185
|
-
selhint.style.top = rect.bottom + 6 + window.scrollY + 'px';
|
|
186
|
-
selhint.onclick = () => {
|
|
187
|
-
selhint.style.display = 'none';
|
|
188
|
-
const anchorNode = selection.anchorNode;
|
|
189
|
-
const el = anchorNode && anchorNode.nodeType === 1 ? anchorNode : anchorNode && anchorNode.parentElement;
|
|
190
|
-
openCard({
|
|
191
|
-
kindLabel: 'Annotate selection',
|
|
192
|
-
anchor: {
|
|
193
|
-
selector: el ? selectorFor(el) : 'body',
|
|
194
|
-
tag: 'text',
|
|
195
|
-
snippet: text.slice(0, 200),
|
|
196
|
-
textRange: { text: text.slice(0, 1000) }
|
|
197
|
-
},
|
|
198
|
-
x: rect.left,
|
|
199
|
-
y: rect.bottom
|
|
200
|
-
});
|
|
201
|
-
};
|
|
202
|
-
}, 0);
|
|
203
|
-
}, true);
|
|
204
|
-
document.addEventListener('selectionchange', () => {
|
|
205
|
-
const selection = window.getSelection();
|
|
206
|
-
if (!selection || selection.isCollapsed) selhint.style.display = 'none';
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// --- chrome bridge ---------------------------------------------------------
|
|
210
|
-
window.addEventListener('message', e => {
|
|
211
|
-
const msg = e.data || {};
|
|
212
|
-
if (msg.type === 'pc:set-mode') {
|
|
213
|
-
annotate = Boolean(msg.annotate);
|
|
214
|
-
if (!annotate) { hl.style.display = 'none'; selhint.style.display = 'none'; closeCard(); }
|
|
215
|
-
} else if (msg.type === 'pc:restore-scroll') {
|
|
216
|
-
window.scrollTo(msg.x || 0, msg.y || 0);
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
document.addEventListener('keydown', e => {
|
|
220
|
-
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'i') {
|
|
221
|
-
e.preventDefault();
|
|
222
|
-
post({ type: 'pc:toggle-mode' });
|
|
223
|
-
} else if (e.key === 'Escape' && card) closeCard();
|
|
224
|
-
}, true);
|
|
225
|
-
|
|
226
|
-
let scrollTimer = null;
|
|
227
|
-
window.addEventListener('scroll', () => {
|
|
228
|
-
if (scrollTimer) return;
|
|
229
|
-
scrollTimer = setTimeout(() => {
|
|
230
|
-
scrollTimer = null;
|
|
231
|
-
post({ type: 'pc:scroll', x: window.scrollX, y: window.scrollY });
|
|
232
|
-
}, 150);
|
|
233
|
-
}, { passive: true });
|
|
234
|
-
|
|
235
|
-
post({ type: 'pc:ready' });
|
|
236
|
-
})();`;
|
|
11
|
+
function artifactSdkJs() {
|
|
12
|
+
return annotateClientJs({ transport: 'postMessage' });
|
|
237
13
|
}
|
|
238
14
|
|
|
239
15
|
module.exports = { artifactSdkJs };
|
|
@@ -17,8 +17,10 @@ const http = require('http');
|
|
|
17
17
|
const path = require('path');
|
|
18
18
|
|
|
19
19
|
const { buildAllowedHostnames, isAllowedFetchSite, isAllowedHostHeader, isAllowedOrigin } = require('../loopback-guard');
|
|
20
|
+
const { createAnnotateHandler } = require('./annotate-server');
|
|
20
21
|
const { renderMarkdown } = require('./markdown');
|
|
21
22
|
const { artifactSdkJs } = require('./sdk');
|
|
23
|
+
const { ensureTrailOnApprove } = require('./trail-on-approve');
|
|
22
24
|
const {
|
|
23
25
|
canvasCss,
|
|
24
26
|
canvasClientJs,
|
|
@@ -165,11 +167,23 @@ function createPlanCanvasServer({
|
|
|
165
167
|
typingExpiryMs = DEFAULT_TYPING_EXPIRY_MS,
|
|
166
168
|
presenceSweepMs = DEFAULT_PRESENCE_SWEEP_MS,
|
|
167
169
|
onIdleShutdown = null,
|
|
170
|
+
now = Date.now,
|
|
168
171
|
log = () => {}
|
|
169
172
|
} = {}) {
|
|
170
173
|
if (!store) throw new Error('createPlanCanvasServer requires a session store');
|
|
171
174
|
|
|
172
175
|
const allowedHostnames = buildAllowedHostnames(host);
|
|
176
|
+
const annotate = createAnnotateHandler({
|
|
177
|
+
store,
|
|
178
|
+
version,
|
|
179
|
+
now,
|
|
180
|
+
log,
|
|
181
|
+
onQueued: key => {
|
|
182
|
+
wake.emit(`wake:${key}`);
|
|
183
|
+
broadcast(key, 'chat-sync', { chat: store.get(key).chat });
|
|
184
|
+
broadcastPresence(key);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
173
187
|
const wake = new EventEmitter();
|
|
174
188
|
wake.setMaxListeners(0);
|
|
175
189
|
const sseClients = new Map(); // key -> Set<res>
|
|
@@ -334,6 +348,7 @@ function createPlanCanvasServer({
|
|
|
334
348
|
if (!artifactPath) {
|
|
335
349
|
return sendJson(res, 403, { error: 'artifact path is outside the workspace' });
|
|
336
350
|
}
|
|
351
|
+
const resumed = Boolean(store.findByFile(artifactPath));
|
|
337
352
|
const { session, refused } = store.open(artifactPath, { reopen: Boolean(body.reopen) });
|
|
338
353
|
if (refused) {
|
|
339
354
|
return sendJson(res, 409, {
|
|
@@ -348,7 +363,9 @@ function createPlanCanvasServer({
|
|
|
348
363
|
status: 'open',
|
|
349
364
|
key: session.key,
|
|
350
365
|
file: session.file,
|
|
351
|
-
url: `/canvas/${session.key}
|
|
366
|
+
url: `/canvas/${session.key}`,
|
|
367
|
+
resumed,
|
|
368
|
+
viewers: sseClients.get(session.key)?.size || 0
|
|
352
369
|
});
|
|
353
370
|
}
|
|
354
371
|
|
|
@@ -437,16 +454,32 @@ function createPlanCanvasServer({
|
|
|
437
454
|
return sendJson(res, 200, { status: 'ended', endedBy: 'agent' });
|
|
438
455
|
}
|
|
439
456
|
|
|
440
|
-
const sessionMatch = pathname.match(/^\/api\/session\/([a-f0-9]{12})\/(feedback|end|reply|typing)$/);
|
|
457
|
+
const sessionMatch = pathname.match(/^\/api\/session\/([a-f0-9]{12})\/(feedback|end|reply|typing|resume)$/);
|
|
441
458
|
if (sessionMatch && req.method === 'POST') {
|
|
442
459
|
const [, key, action] = sessionMatch;
|
|
443
460
|
const session = store.get(key);
|
|
444
461
|
if (!session) return sendJson(res, 404, { error: 'unknown session' });
|
|
445
462
|
|
|
463
|
+
if (action === 'resume') {
|
|
464
|
+
if (!fs.existsSync(session.file)) return sendJson(res, 404, { error: 'artifact file no longer exists' });
|
|
465
|
+
if (!confinedArtifactPath(session.file, workspaceRoot)) {
|
|
466
|
+
return sendJson(res, 403, { error: 'artifact path is outside the workspace' });
|
|
467
|
+
}
|
|
468
|
+
store.open(session.file, { reopen: true });
|
|
469
|
+
watchSession(session);
|
|
470
|
+
broadcastPresence(key);
|
|
471
|
+
res.writeHead(303, { location: `/canvas/${key}` });
|
|
472
|
+
return res.end();
|
|
473
|
+
}
|
|
474
|
+
|
|
446
475
|
if (action === 'feedback') {
|
|
447
476
|
const body = await readJsonBody(req);
|
|
448
477
|
const result = store.queueFeedback(key, body.items, { endSession: Boolean(body.endSession) });
|
|
449
478
|
if (!result) return sendJson(res, 409, { error: 'session already ended' });
|
|
479
|
+
// Record the trail outcome before waking a parked await so it can report it.
|
|
480
|
+
if (result.accepted.some(i => i.kind === 'verdict' && i.verdict === 'approve')) {
|
|
481
|
+
ensureTrailOnApprove({ file: session.file, key, log, onOutcome: outcome => store.setTrailOutcome(key, outcome) });
|
|
482
|
+
}
|
|
450
483
|
wake.emit(`wake:${key}`);
|
|
451
484
|
broadcast(key, 'chat-sync', { chat: store.get(key).chat });
|
|
452
485
|
if (body.endSession) broadcast(key, 'ended', { endedBy: 'user' });
|
|
@@ -584,11 +617,19 @@ function createPlanCanvasServer({
|
|
|
584
617
|
if (!isAllowedHostHeader(req.headers.host, allowedHostnames)) {
|
|
585
618
|
return sendJson(res, 403, { error: 'forbidden host header' });
|
|
586
619
|
}
|
|
620
|
+
let url;
|
|
621
|
+
try {
|
|
622
|
+
url = new URL(req.url, `http://${req.headers.host}`);
|
|
623
|
+
} catch {
|
|
624
|
+
return sendJson(res, 400, { error: 'bad request target' });
|
|
625
|
+
}
|
|
626
|
+
const { pathname } = url;
|
|
627
|
+
// Annotate routes are called from a dev app on another loopback origin, so
|
|
628
|
+
// they do their own Origin/token checks instead of the same-origin gate.
|
|
629
|
+
if (annotate.handles(pathname)) return annotate.handle(req, res, url);
|
|
587
630
|
if (!isAllowedOrigin(req.headers.origin, allowedHostnames, boundPort)) {
|
|
588
631
|
return sendJson(res, 403, { error: 'forbidden origin' });
|
|
589
632
|
}
|
|
590
|
-
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
591
|
-
const { pathname } = url;
|
|
592
633
|
// The artifact iframe is sandboxed without allow-same-origin, so its script
|
|
593
634
|
// and asset loads arrive as Sec-Fetch-Site: cross-site. Only those read-only
|
|
594
635
|
// static paths may skip the fetch-site check; everything else keeps it.
|