@hybridlabor-api/aos 4.17.0 → 4.18.1

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.
Files changed (88) hide show
  1. package/.claude/hooks/aos-bus.mjs +8 -4
  2. package/.claude/hooks/go-gate.mjs +1 -1
  3. package/.claude/hooks/go-token.mjs +17 -2
  4. package/.claude/hooks/memb-inject.mjs +61 -37
  5. package/.codex-plugin/plugin.json +1 -1
  6. package/.opencode/commands/bdb-aos-plan.md +1 -1
  7. package/README.md +1 -0
  8. package/THIRD_PARTY_NOTICES.md +2 -2
  9. package/bin/aos-acp.mjs +27 -1
  10. package/bin/aos-doctor.mjs +36 -1
  11. package/bin/aos-uninstall.mjs +16 -2
  12. package/bin/go-check.mjs +79 -0
  13. package/bin/guarded-patterns.json +106 -0
  14. package/commands/plan.md +1 -1
  15. package/docs/codenotch.md +44 -0
  16. package/docs/codex-gate-smoke.md +43 -0
  17. package/docs/delegation-routing.md +32 -0
  18. package/docs/go-check.md +60 -0
  19. package/docs/master-session-acp.md +2 -0
  20. package/docs/opencode-setup.md +18 -0
  21. package/installer.js +155 -70
  22. package/lib/codenotch.js +389 -0
  23. package/lib/retired-skills.js +101 -0
  24. package/mcps/mcsc/README.md +1 -1
  25. package/mcps/mcsc/packages/core/src/adapters/agy.js +3 -1
  26. package/mcps/mcsc/packages/core/src/adapters/codex.js +2 -1
  27. package/mcps/mcsc/packages/core/src/adapters/opencode.js +2 -1
  28. package/mcps/mcsc/packages/core/src/depth.js +16 -0
  29. package/mcps/mcsc/packages/mcp/server.js +15 -2
  30. package/package.json +2 -2
  31. package/plugin-commands.json +1 -2
  32. package/plugin.json +1 -4
  33. package/plugins/bdb-aos-codex/.codex-plugin/plugin.json +1 -1
  34. package/plugins/bdb-aos-codex/skills/plan/SKILL.md +1 -1
  35. package/scripts/codex-gate-smoke.mjs +73 -0
  36. package/skills/basic/master-session/SKILL.md +11 -0
  37. package/skills/global_config/agenttrail/SKILL.md +3 -1
  38. package/skills/global_config/agenttrail/bin/agenttrail.mjs +255 -117
  39. package/skills/global_config/agenttrail/bin/ensure.mjs +60 -40
  40. package/skills/global_config/agenttrail/bin/repoid.mjs +70 -0
  41. package/skills/global_config/agenttrail/public/index.html +9 -1
  42. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +1 -1
  43. package/skills/global_config/bdb-memb-mcp/SKILL.md +5 -4
  44. package/skills/global_config/bdb-visual-edit/SKILL.md +28 -32
  45. package/skills/global_config/bdb-visual-edit/references/vite-react-source-attr.md +2 -2
  46. package/skills/global_config/bdb-visual-edit/scripts/locate-source.mjs +135 -0
  47. package/skills/global_config/bdb-visual-edit/scripts/sanitize-element.mjs +30 -2
  48. package/skills/global_config/mcsc/SKILL.md +9 -1
  49. package/skills/global_config/plan-arbiter/SKILL.md +1 -1
  50. package/skills/global_config/plan-canvas/SKILL.md +42 -4
  51. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/README.md +1 -1
  52. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/render.js +2 -2
  53. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-client/geometry.js +76 -0
  54. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-client/index.js +596 -0
  55. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-client/model.js +192 -0
  56. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-client/toolbar.js +99 -0
  57. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-server.js +282 -0
  58. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotation-schema.js +210 -0
  59. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/route.js +10 -0
  60. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/sdk.js +6 -230
  61. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/server.js +45 -4
  62. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/sessions.js +60 -21
  63. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/trail-on-approve.js +103 -0
  64. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/ui.js +19 -7
  65. package/skills/global_config/plan-canvas/scripts/plan-canvas.js +118 -20
  66. package/skills/global_config/subagent-setup/SKILL.md +6 -0
  67. package/skills/global_config/subagent-setup/scripts/setup-subagents.mjs +20 -1
  68. package/skills/playbooks/pb-idea-to-launch/SKILL.md +2 -2
  69. package/skills/playbooks/pb-redesign-app/SKILL.md +3 -3
  70. package/skills/playbooks/pb-release-aos/SKILL.md +2 -2
  71. package/skills/playbooks/pb-ship/SKILL.md +2 -2
  72. package/skills/playbooks/pb-worktrees-land/SKILL.md +2 -2
  73. package/skills/global_config/bdb-visual-edit/scripts/pick-snippet.js +0 -27
  74. package/skills/global_config/visual-edit/README.md +0 -96
  75. package/skills/global_config/visual-edit/SKILL.md +0 -615
  76. package/skills/global_config/visual-plan/README.md +0 -93
  77. package/skills/global_config/visual-plan/SKILL.md +0 -544
  78. package/skills/global_config/visual-plan/references/canvas.md +0 -139
  79. package/skills/global_config/visual-plan/references/connection.md +0 -51
  80. package/skills/global_config/visual-plan/references/document-quality.md +0 -186
  81. package/skills/global_config/visual-plan/references/exemplar.md +0 -62
  82. package/skills/global_config/visual-plan/references/local-files.md +0 -99
  83. package/skills/global_config/visual-plan/references/wireframe.md +0 -319
  84. package/skills/global_config/visual-recap/README.md +0 -103
  85. package/skills/global_config/visual-recap/SKILL.md +0 -560
  86. package/skills/global_config/visual-recap/references/connection.md +0 -51
  87. package/skills/global_config/visual-recap/references/local-files.md +0 -99
  88. package/skills/global_config/visual-recap/references/wireframe.md +0 -319
@@ -0,0 +1,596 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Plan Canvas annotation layer (element, text, area, arrow, freehand, blur,
5
+ * comment). One module for both hosts: the sandboxed canvas iframe
6
+ * (transport 'postMessage') and a running localhost app (transport 'fetch').
7
+ * Own code, no third-party drawing library.
8
+ */
9
+
10
+ const geometry = require('./geometry');
11
+ const model = require('./model');
12
+ const { toolbarCss, toolbarHtml } = require('./toolbar');
13
+
14
+ const { rawUnits, toAnchorUnits, fromAnchorUnits, fitsAnchor } = geometry;
15
+ const { createDraft, draftAdd, draftUndo, draftRedo, draftClear, makeShape, buildItem, chunkItems, shapeSummary } = model;
16
+
17
+ // Runs in the browser. Free identifiers are the inlined pure helpers above.
18
+ function clientMain(CFG) {
19
+ const FETCH = CFG.transport === 'fetch';
20
+ if (!FETCH && window.parent === window) return;
21
+ if (window.__aosAnnotate) return;
22
+ window.__aosAnnotate = true;
23
+
24
+ const script = document.currentScript;
25
+ let endpoint = null;
26
+ let token = '';
27
+ if (FETCH && script) {
28
+ const key = script.getAttribute('data-session') || '';
29
+ token = script.getAttribute('data-token') || '';
30
+ let base = '';
31
+ try { base = new URL(script.src).origin; } catch (err) { base = ''; }
32
+ if (/^[a-f0-9]{12}$/.test(key) && token && base) endpoint = base + '/api/annotate/' + key;
33
+ }
34
+
35
+ const TOOL_IDS = ['element', 'text', 'rect', 'arrow', 'freehand', 'blur', 'comment'];
36
+ const TOOL_KEYS = { V: 'element', T: 'text', R: 'rect', A: 'arrow', F: 'freehand', B: 'blur', C: 'comment' };
37
+ const DRAW = ['rect', 'arrow', 'freehand', 'blur', 'comment'];
38
+ const COLOR_HEX = { red: '#e5484d', yellow: '#f5c518', blue: '#3b82f6', green: '#30a46c' };
39
+ const NS = 'http://www.w3.org/2000/svg';
40
+ const MAX_RAW = 5000;
41
+
42
+ let on = !FETCH;
43
+ let tool = 'element';
44
+ let color = 'red';
45
+ let stroke = 4;
46
+ let draft = createDraft();
47
+ let pending = null;
48
+ let live = null;
49
+ let queue = [];
50
+ let sending = false;
51
+ let statusTimer = null;
52
+ let frame = 0;
53
+
54
+ const post = msg => window.parent.postMessage(msg, '*');
55
+
56
+ // --- shadow-root UI ------------------------------------------------------
57
+ const host = document.createElement('div');
58
+ host.setAttribute('data-ecc-plan-canvas', 'ui');
59
+ host.setAttribute('data-version', CFG.version);
60
+ host.style.cssText = 'position:absolute;top:0;left:0;width:0;height:0;z-index:2147483647';
61
+ const root = host.attachShadow({ mode: 'open' });
62
+ const styleEl = document.createElement('style');
63
+ styleEl.textContent = CFG.css;
64
+ root.appendChild(styleEl);
65
+ root.append(...Array.from(new DOMParser().parseFromString(CFG.html, 'text/html').body.childNodes));
66
+ const attach = () => (document.body ? document.body.appendChild(host) : null);
67
+ if (document.body) attach();
68
+ else document.addEventListener('DOMContentLoaded', attach);
69
+
70
+ const $ = sel => root.querySelector(sel);
71
+ const ov = $('svg.ov');
72
+ const hl = $('.hl');
73
+ const selhint = $('.selhint');
74
+ const launch = $('.launch');
75
+ const bar = $('.bar');
76
+ const statusEl = $('.status');
77
+ const cardEl = $('.card');
78
+ const cardTitle = cardEl.querySelector('h4');
79
+ const cardSnippet = cardEl.querySelector('.snippet');
80
+ const noteEl = cardEl.querySelector('textarea');
81
+ const mini = $('.mini');
82
+ const miniList = mini.querySelector('ul');
83
+ const miniMsg = mini.querySelector('.msg');
84
+ const sendBtn = mini.querySelector('[data-act=send]');
85
+
86
+ const isOurs = el => el === host || host.contains(el);
87
+ const isTyping = t => Boolean(t && t.nodeType === 1 && (/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName) || t.isContentEditable));
88
+ const FORMISH = 'input,textarea,select,option,[contenteditable],script,style,noscript,[data-ecc-plan-canvas]';
89
+
90
+ function setStatus(msg) {
91
+ statusEl.textContent = msg;
92
+ clearTimeout(statusTimer);
93
+ if (msg) statusTimer = setTimeout(() => { statusEl.textContent = ''; }, 5000);
94
+ }
95
+
96
+ // --- selectors & context -------------------------------------------------
97
+ const esc = v => ((window.CSS && CSS.escape) ? CSS.escape(v) : v.replace(/[^a-zA-Z0-9_-]/g, '\\$&'));
98
+ function selectorFor(el) {
99
+ const parts = [];
100
+ let node = el;
101
+ for (let depth = 0; node && node.nodeType === 1 && depth < 6; depth++) {
102
+ if (node.id) { parts.unshift('#' + esc(node.id)); return parts.join(' > '); }
103
+ const tag = node.tagName.toLowerCase();
104
+ if (tag === 'body' || tag === 'html') { parts.unshift(tag); break; }
105
+ let nth = 1;
106
+ let sib = node;
107
+ while ((sib = sib.previousElementSibling)) if (sib.tagName === node.tagName) nth++;
108
+ parts.unshift(tag + ':nth-of-type(' + nth + ')');
109
+ node = node.parentElement;
110
+ }
111
+ return parts.join(' > ');
112
+ }
113
+ // Text nodes only: values of form controls and editable regions are never read.
114
+ function snippetFor(el) {
115
+ if (el.matches(FORMISH) || el.closest(FORMISH)) return '';
116
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, {
117
+ acceptNode: n => (n.parentElement && !n.parentElement.closest(FORMISH) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT)
118
+ });
119
+ let text = '';
120
+ for (let n = walker.nextNode(); n && text.length < 400; n = walker.nextNode()) text += ' ' + n.nodeValue;
121
+ return text.replace(/\s+/g, ' ').trim().slice(0, CAPS.snippet);
122
+ }
123
+ function isInteractive(el) {
124
+ for (let node = el; node && node.nodeType === 1; node = node.parentElement) {
125
+ if (/^(button|input|select|textarea|option|label|summary|a)$/i.test(node.tagName) || node.isContentEditable) return true;
126
+ }
127
+ return false;
128
+ }
129
+ // App mode: buttons, links and labels must be annotatable, so the element tool
130
+ // swallows their clicks; holding Alt lets a click through to the app.
131
+ const grabs = (e, el) => FETCH && !e.altKey && isInteractive(el);
132
+ function boxOf(el) {
133
+ const r = el.getBoundingClientRect();
134
+ return { left: r.left + window.scrollX, top: r.top + window.scrollY, width: r.width, height: r.height };
135
+ }
136
+ function anchorFor(el) {
137
+ return {
138
+ selector: selectorFor(el),
139
+ tag: el.tagName.toLowerCase(),
140
+ snippet: snippetFor(el),
141
+ classes: Array.from(el.classList || [])
142
+ };
143
+ }
144
+ function srcLocFor(el) {
145
+ const holder = FETCH ? el.closest('[data-aos-src]') : null;
146
+ return holder ? holder.getAttribute('data-aos-src') : null;
147
+ }
148
+ function elementAtClient(x, y) {
149
+ const hit = document.elementsFromPoint(x, y).find(el => !isOurs(el) && el !== document.documentElement && el !== document.body);
150
+ return hit || document.body;
151
+ }
152
+
153
+ // --- overlay rendering ---------------------------------------------------
154
+ function svgEl(name, attrs) {
155
+ const n = document.createElementNS(NS, name);
156
+ for (const k of Object.keys(attrs)) n.setAttribute(k, String(attrs[k]));
157
+ return n;
158
+ }
159
+ function drawShape(type, pts, colorName, sw, n) {
160
+ const col = COLOR_HEX[colorName] || COLOR_HEX.red;
161
+ const g = svgEl('g', { fill: 'none', stroke: col, 'stroke-width': sw, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' });
162
+ const a = pts[0];
163
+ const b = pts[pts.length - 1];
164
+ if (type === 'rect' || type === 'blur') {
165
+ const x = Math.min(a[0], b[0]);
166
+ const y = Math.min(a[1], b[1]);
167
+ const w = Math.abs(b[0] - a[0]);
168
+ const h = Math.abs(b[1] - a[1]);
169
+ if (type === 'blur') g.appendChild(svgEl('rect', { x, y, width: w, height: h, fill: '#111', 'fill-opacity': 0.92, stroke: 'none' }));
170
+ else g.appendChild(svgEl('rect', { x, y, width: w, height: h, fill: col, 'fill-opacity': 0.08, rx: 2 }));
171
+ } else if (type === 'arrow') {
172
+ const ang = Math.atan2(b[1] - a[1], b[0] - a[0]);
173
+ const len = 10 + sw * 2;
174
+ const wing = d => (b[0] - len * Math.cos(ang + d)) + ',' + (b[1] - len * Math.sin(ang + d));
175
+ g.appendChild(svgEl('line', { x1: a[0], y1: a[1], x2: b[0], y2: b[1] }));
176
+ g.appendChild(svgEl('polygon', { points: wing(-0.45) + ' ' + b[0] + ',' + b[1] + ' ' + wing(0.45), fill: col }));
177
+ } else if (type === 'freehand') {
178
+ g.appendChild(svgEl('polyline', { points: pts.map(p => p[0] + ',' + p[1]).join(' ') }));
179
+ } else if (type === 'comment') {
180
+ g.appendChild(svgEl('circle', { cx: a[0], cy: a[1], r: 11, fill: col, stroke: '#fff', 'stroke-width': 2 }));
181
+ const label = svgEl('text', { x: a[0], y: a[1] + 4, 'text-anchor': 'middle', 'font-size': 12, 'font-weight': 700, fill: '#fff', stroke: 'none' });
182
+ label.textContent = String(n);
183
+ g.appendChild(label);
184
+ }
185
+ return g;
186
+ }
187
+ function currentBox() {
188
+ return pending.el && pending.el.isConnected ? boxOf(pending.el) : pending.box;
189
+ }
190
+ function render() {
191
+ const de = document.documentElement;
192
+ ov.setAttribute('width', String(Math.max(de.scrollWidth, window.innerWidth)));
193
+ ov.setAttribute('height', String(Math.max(de.scrollHeight, window.innerHeight)));
194
+ ov.replaceChildren();
195
+ if (pending && draft.shapes.length) {
196
+ const box = currentBox();
197
+ draft.shapes.forEach((s, i) => ov.appendChild(drawShape(s.type, s.points.map(p => fromAnchorUnits(p, box)), s.color, s.s || 4, i + 1)));
198
+ }
199
+ if (live) ov.appendChild(drawShape(live.type, live.pts, color, stroke, draft.shapes.length + 1));
200
+ for (const b of root.querySelectorAll('.bar button[data-act]')) {
201
+ if (b.dataset.act === 'undo') b.disabled = !draft.shapes.length;
202
+ else if (b.dataset.act === 'redo') b.disabled = !draft.redo.length;
203
+ else if (b.dataset.act === 'delete') b.disabled = !pending;
204
+ }
205
+ }
206
+ const scheduleRender = () => {
207
+ if (frame) return;
208
+ frame = requestAnimationFrame(() => { frame = 0; render(); });
209
+ };
210
+
211
+ // --- state transitions ---------------------------------------------------
212
+ function syncUi() {
213
+ bar.hidden = !on;
214
+ launch.hidden = !(FETCH && !on);
215
+ ov.classList.toggle('draw', on && DRAW.includes(tool));
216
+ for (const b of root.querySelectorAll('[data-tool]')) b.setAttribute('aria-pressed', String(b.dataset.tool === tool));
217
+ for (const b of root.querySelectorAll('[data-color]')) b.setAttribute('aria-pressed', String(b.dataset.color === color));
218
+ for (const b of root.querySelectorAll('[data-stroke]')) b.setAttribute('aria-pressed', String(Number(b.dataset.stroke) === stroke));
219
+ }
220
+ function cancelAll() {
221
+ draftClear(draft);
222
+ pending = null;
223
+ live = null;
224
+ cardEl.hidden = true;
225
+ noteEl.value = '';
226
+ render();
227
+ }
228
+ function setOn(v) {
229
+ on = Boolean(v);
230
+ if (!on) { cancelAll(); hl.style.display = 'none'; selhint.style.display = 'none'; }
231
+ syncUi();
232
+ }
233
+ function setTool(t, fromChrome) {
234
+ if (!TOOL_IDS.includes(t)) return;
235
+ tool = t;
236
+ hl.style.display = 'none';
237
+ syncUi();
238
+ if (!FETCH && !fromChrome) post({ type: 'pc:tool', tool });
239
+ }
240
+ function focusTool() {
241
+ const b = root.querySelector('[data-tool][aria-pressed=true]');
242
+ if (b) b.focus();
243
+ }
244
+
245
+ // --- note card -----------------------------------------------------------
246
+ function showCard(label, x, y) {
247
+ cardTitle.textContent = label;
248
+ cardSnippet.textContent = pending.anchor.snippet || pending.anchor.selector;
249
+ if (cardEl.hidden) {
250
+ cardEl.hidden = false;
251
+ cardEl.style.left = Math.max(8, Math.min(x, window.scrollX + window.innerWidth - 316)) + 'px';
252
+ cardEl.style.top = y + 12 + 'px';
253
+ noteEl.focus();
254
+ }
255
+ }
256
+ function openElementCard(label, el, anchor, clientX, clientY) {
257
+ cancelAll();
258
+ pending = { fromShapes: false, el, box: boxOf(el), anchor, srcLoc: srcLocFor(el) };
259
+ showCard(label, clientX + window.scrollX, clientY + window.scrollY);
260
+ }
261
+ function queueCard(sendNow) {
262
+ if (!pending) return;
263
+ const text = noteEl.value.trim();
264
+ if (!text) { noteEl.focus(); return; }
265
+ if (pending.fromShapes && !draft.shapes.length) { setStatus('Nothing drawn: draw a shape first.'); return; }
266
+ const item = buildItem({
267
+ text,
268
+ anchor: pending.anchor,
269
+ shapes: draft.shapes,
270
+ box: currentBox(),
271
+ viewport: { w: window.innerWidth, h: window.innerHeight, dpr: window.devicePixelRatio },
272
+ target: FETCH ? { url: location.origin + location.pathname, srcLoc: pending.srcLoc } : null
273
+ });
274
+ if (!item) { setStatus('Could not build this annotation.'); return; }
275
+ cancelAll();
276
+ if (FETCH) {
277
+ queue.push(item);
278
+ renderMini();
279
+ if (sendNow) send();
280
+ } else {
281
+ post({ type: sendNow ? 'pc:queue-and-send' : 'pc:queue', item });
282
+ }
283
+ }
284
+ noteEl.addEventListener('keydown', e => {
285
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); queueCard(true); }
286
+ else if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); queueCard(false); }
287
+ else if (e.key === 'Escape') { e.preventDefault(); cancelAll(); }
288
+ e.stopPropagation();
289
+ });
290
+ noteEl.addEventListener('keyup', e => e.stopPropagation());
291
+ noteEl.addEventListener('keypress', e => e.stopPropagation());
292
+
293
+ // --- drawing -------------------------------------------------------------
294
+ const pagePt = e => [e.clientX + window.scrollX, e.clientY + window.scrollY];
295
+ function pickAnchor(allPts, refPage) {
296
+ let el = elementAtClient(refPage[0] - window.scrollX, refPage[1] - window.scrollY);
297
+ let box = boxOf(el);
298
+ while (!fitsAnchor(allPts, box) && el.parentElement) { el = el.parentElement; box = boxOf(el); }
299
+ return fitsAnchor(allPts, box) ? { el, box } : null;
300
+ }
301
+ function addDrawn(type, pts) {
302
+ if (pending && !pending.fromShapes) cancelAll();
303
+ if (!pending) {
304
+ const ref = type === 'arrow' ? pts[1] : [(pts[0][0] + pts[pts.length - 1][0]) / 2, (pts[0][1] + pts[pts.length - 1][1]) / 2];
305
+ const a = pickAnchor(pts, ref);
306
+ if (!a) { setStatus('Could not anchor this shape to an element.'); render(); return; }
307
+ pending = { fromShapes: true, el: a.el, box: a.box, anchor: anchorFor(a.el), srcLoc: srcLocFor(a.el) };
308
+ } else if (!fitsAnchor(pts, currentBox())) {
309
+ setStatus('Too far from the first shape: queue this note first, then draw it again.');
310
+ render();
311
+ return;
312
+ }
313
+ const box = currentBox();
314
+ const shape = makeShape(type, pts.map(p => toAnchorUnits(p, box)), color);
315
+ const why = draftAdd(draft, Object.assign(shape || {}, { s: stroke }));
316
+ if (why === 'max-shapes') setStatus('Limit of ' + CAPS.maxShapes + ' shapes per note: queue this note first.');
317
+ else if (why === 'max-points') setStatus('Drawing too detailed: queue this note first.');
318
+ else if (why) setStatus('Shape discarded.');
319
+ const last = pts[pts.length - 1];
320
+ showCard('Annotate drawing', last[0], last[1]);
321
+ render();
322
+ }
323
+ function endDraw() {
324
+ if (!live) return;
325
+ const { type, pts } = live;
326
+ live = null;
327
+ const a = pts[0];
328
+ const b = pts[pts.length - 1];
329
+ if (type === 'comment') return addDrawn(type, [a]);
330
+ if (type === 'freehand') {
331
+ let len = 0;
332
+ for (let i = 1; i < pts.length; i++) len += Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]);
333
+ if (len < 3) return render();
334
+ return addDrawn(type, pts);
335
+ }
336
+ if (Math.hypot(b[0] - a[0], b[1] - a[1]) < 4) return render();
337
+ return addDrawn(type, [a, b]);
338
+ }
339
+ ov.addEventListener('pointerdown', e => {
340
+ if (!on || !DRAW.includes(tool) || e.button !== 0 || live) return;
341
+ e.preventDefault();
342
+ try { ov.setPointerCapture(e.pointerId); } catch (err) { /* synthetic pointer */ }
343
+ live = { id: e.pointerId, type: tool, pts: [pagePt(e)] };
344
+ if (tool === 'comment') endDraw();
345
+ else scheduleRender();
346
+ });
347
+ ov.addEventListener('pointermove', e => {
348
+ if (!live || e.pointerId !== live.id) return;
349
+ const p = pagePt(e);
350
+ if (live.type === 'freehand') { if (live.pts.length < MAX_RAW) live.pts.push(p); }
351
+ else live.pts[1] = p;
352
+ scheduleRender();
353
+ });
354
+ ov.addEventListener('pointerup', e => { if (live && e.pointerId === live.id) endDraw(); });
355
+ ov.addEventListener('pointercancel', () => { live = null; render(); });
356
+
357
+ // --- element hover / click -----------------------------------------------
358
+ document.addEventListener('mousemove', e => {
359
+ if (!on || tool !== 'element' || pending) { hl.style.display = 'none'; return; }
360
+ const el = e.target;
361
+ if (!el || isOurs(el) || el === document.body || el === document.documentElement || (isInteractive(el) && !grabs(e, el))) {
362
+ hl.style.display = 'none';
363
+ return;
364
+ }
365
+ const rect = el.getBoundingClientRect();
366
+ hl.style.display = 'block';
367
+ hl.style.left = rect.left - 2 + 'px';
368
+ hl.style.top = rect.top - 2 + 'px';
369
+ hl.style.width = rect.width + 'px';
370
+ hl.style.height = rect.height + 'px';
371
+ }, true);
372
+
373
+ document.addEventListener('click', e => {
374
+ if (!on || tool !== 'element') return;
375
+ const el = e.target;
376
+ if (isOurs(el)) return;
377
+ if (pending) {
378
+ if (grabs(e, el)) { e.preventDefault(); e.stopPropagation(); }
379
+ if (!pending.fromShapes) cancelAll();
380
+ return;
381
+ }
382
+ if (isInteractive(el) && !grabs(e, el)) return;
383
+ const selection = window.getSelection();
384
+ if (selection && !selection.isCollapsed) return;
385
+ if (el === document.body || el === document.documentElement) return;
386
+ e.preventDefault();
387
+ e.stopPropagation();
388
+ hl.style.display = 'none';
389
+ openElementCard('Annotate <' + el.tagName.toLowerCase() + '>', el, anchorFor(el), e.clientX, e.clientY);
390
+ }, true);
391
+
392
+ ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'dblclick'].forEach(type => {
393
+ document.addEventListener(type, e => {
394
+ if (on && tool === 'element' && !isOurs(e.target) && grabs(e, e.target)) {
395
+ e.preventDefault();
396
+ e.stopPropagation();
397
+ }
398
+ }, true);
399
+ });
400
+
401
+ // --- text selection ------------------------------------------------------
402
+ document.addEventListener('mouseup', e => {
403
+ if (!on || (tool !== 'element' && tool !== 'text') || isOurs(e.target)) return;
404
+ setTimeout(() => {
405
+ const selection = window.getSelection();
406
+ if (!selection || selection.isCollapsed) { selhint.style.display = 'none'; return; }
407
+ const text = selection.toString().replace(/\s+/g, ' ').trim();
408
+ const range = selection.rangeCount ? selection.getRangeAt(0) : null;
409
+ if (!text || !range) return;
410
+ const node = range.commonAncestorContainer;
411
+ const el = node.nodeType === 1 ? node : node.parentElement;
412
+ if (el && (el.matches(FORMISH) || el.closest(FORMISH))) return;
413
+ const rect = range.getBoundingClientRect();
414
+ selhint.style.display = 'block';
415
+ selhint.style.left = Math.max(8, rect.left + window.scrollX) + 'px';
416
+ selhint.style.top = rect.top + window.scrollY - 36 + 'px';
417
+ selhint.onclick = () => {
418
+ selhint.style.display = 'none';
419
+ const target = el || document.body;
420
+ const anchor = { selector: selectorFor(target), tag: 'text', snippet: text.slice(0, CAPS.snippet), classes: Array.from(target.classList || []), textRange: { text: text.slice(0, CAPS.textRange) } };
421
+ openElementCard('Annotate selection', target, anchor, rect.left, rect.bottom);
422
+ };
423
+ }, 0);
424
+ }, true);
425
+ document.addEventListener('selectionchange', () => {
426
+ const selection = window.getSelection();
427
+ if (!selection || selection.isCollapsed) selhint.style.display = 'none';
428
+ });
429
+
430
+ // --- toolbar -------------------------------------------------------------
431
+ function undo() { if (draftUndo(draft)) render(); }
432
+ function redo() { if (draftRedo(draft)) render(); }
433
+ bar.addEventListener('click', e => {
434
+ const b = e.target.closest('button');
435
+ if (!b) return;
436
+ if (b.dataset.tool) setTool(b.dataset.tool);
437
+ else if (b.dataset.color) { color = b.dataset.color; syncUi(); }
438
+ else if (b.dataset.stroke) { stroke = Number(b.dataset.stroke) || 4; syncUi(); }
439
+ else if (b.dataset.act === 'undo') undo();
440
+ else if (b.dataset.act === 'redo') redo();
441
+ else if (b.dataset.act === 'delete') cancelAll();
442
+ else if (b.dataset.act === 'close') { if (FETCH) setOn(false); else post({ type: 'pc:toggle-mode' }); }
443
+ });
444
+ bar.addEventListener('keydown', e => {
445
+ const keys = ['ArrowLeft', 'ArrowRight', 'Home', 'End'];
446
+ if (!keys.includes(e.key)) return;
447
+ const list = Array.from(bar.querySelectorAll('button:not(:disabled)'));
448
+ const i = list.indexOf(e.composedPath()[0]);
449
+ if (i < 0) return;
450
+ e.preventDefault();
451
+ const next = e.key === 'Home' ? 0 : e.key === 'End' ? list.length - 1 : (i + (e.key === 'ArrowRight' ? 1 : -1) + list.length) % list.length;
452
+ list[next].focus();
453
+ });
454
+ cardEl.addEventListener('click', e => {
455
+ const b = e.target.closest('button');
456
+ if (!b) return;
457
+ if (b.dataset.act === 'cancel') cancelAll();
458
+ else if (b.dataset.act === 'queue') queueCard(false);
459
+ });
460
+ launch.addEventListener('click', () => { setOn(true); focusTool(); });
461
+
462
+ // --- app-mode mini queue and transport -------------------------------------
463
+ function setMiniMsg(msg) {
464
+ miniMsg.textContent = msg;
465
+ renderMini();
466
+ }
467
+ function renderMini() {
468
+ miniList.replaceChildren();
469
+ queue.forEach((item, i) => {
470
+ const li = document.createElement('li');
471
+ const label = document.createElement('span');
472
+ label.textContent = (item.shapes ? '[' + shapeSummary(item.shapes) + '] ' : '') + item.text;
473
+ const rm = document.createElement('button');
474
+ rm.type = 'button';
475
+ rm.textContent = '×';
476
+ rm.setAttribute('aria-label', 'Remove annotation ' + (i + 1));
477
+ rm.addEventListener('click', () => { queue = queue.filter(q => q !== item); renderMini(); });
478
+ li.append(label, rm);
479
+ miniList.appendChild(li);
480
+ });
481
+ sendBtn.disabled = sending || !queue.length;
482
+ mini.hidden = !FETCH || (!queue.length && !miniMsg.textContent);
483
+ }
484
+ async function send() {
485
+ if (sending || !queue.length) return;
486
+ if (!endpoint) { setMiniMsg('Missing data-session or data-token on the script tag.'); return; }
487
+ sending = true;
488
+ setMiniMsg('Sending...');
489
+ try {
490
+ for (const chunk of chunkItems(queue.slice())) {
491
+ const res = await fetch(endpoint, {
492
+ method: 'POST',
493
+ credentials: 'omit',
494
+ headers: { 'content-type': 'application/json', 'x-aos-annotate-token': token },
495
+ body: JSON.stringify({ items: chunk })
496
+ });
497
+ if (!res.ok) {
498
+ const expired = res.status === 401 || res.status === 403;
499
+ sending = false;
500
+ setMiniMsg(expired ? 'canvas link expired: run `aos-plan-canvas annotate <url>` again'
501
+ : res.status === 429 ? 'Too many annotations sent or queued: wait a moment and retry.'
502
+ : 'The canvas server refused this batch (HTTP ' + res.status + ').');
503
+ return;
504
+ }
505
+ queue = queue.filter(q => !chunk.includes(q));
506
+ }
507
+ sending = false;
508
+ setMiniMsg('Sent to the canvas.');
509
+ } catch (err) {
510
+ sending = false;
511
+ setMiniMsg('Could not reach the canvas server. Your annotations are still queued.');
512
+ }
513
+ }
514
+ sendBtn.addEventListener('click', send);
515
+
516
+ // --- keyboard ------------------------------------------------------------
517
+ document.addEventListener('keydown', e => {
518
+ const t = e.composedPath()[0];
519
+ const mod = e.metaKey || e.ctrlKey;
520
+ const k = (e.key || '').toLowerCase();
521
+ if (!FETCH && mod && k === 'i') { e.preventDefault(); post({ type: 'pc:toggle-mode' }); return; }
522
+ if (FETCH && e.altKey && e.shiftKey && e.code === 'KeyA') { e.preventDefault(); setOn(!on); if (on) focusTool(); return; }
523
+ if (!on || t === noteEl) return;
524
+ if (e.key === 'Escape') {
525
+ if (pending || live || draft.shapes.length) { e.preventDefault(); e.stopPropagation(); cancelAll(); }
526
+ else if (FETCH) setOn(false);
527
+ return;
528
+ }
529
+ if (isTyping(t)) return;
530
+ if (mod && k === 'z') { e.preventDefault(); if (e.shiftKey) redo(); else undo(); return; }
531
+ if (mod && k === 'y') { e.preventDefault(); redo(); return; }
532
+ if (e.key === 'Enter' && pending && !root.contains(t)) {
533
+ e.preventDefault();
534
+ if (noteEl.value.trim()) queueCard(mod); else noteEl.focus();
535
+ return;
536
+ }
537
+ if (mod || e.altKey) return;
538
+ if ((e.key === 'Delete' || e.key === 'Backspace') && pending) { e.preventDefault(); cancelAll(); return; }
539
+ const hit = TOOL_KEYS[e.key.length === 1 ? e.key.toUpperCase() : ''];
540
+ if (hit) { e.preventDefault(); setTool(hit); }
541
+ }, true);
542
+
543
+ // --- chrome bridge -------------------------------------------------------
544
+ if (!FETCH) {
545
+ window.addEventListener('message', e => {
546
+ if (e.source !== window.parent) return;
547
+ const msg = e.data;
548
+ if (!msg || typeof msg !== 'object') return;
549
+ if (msg.type === 'pc:set-mode') setOn(msg.annotate);
550
+ else if (msg.type === 'pc:restore-scroll') window.scrollTo(Number(msg.x) || 0, Number(msg.y) || 0);
551
+ else if (msg.type === 'pc:set-tool') setTool(msg.tool, true);
552
+ });
553
+ let scrollTimer = null;
554
+ window.addEventListener('scroll', () => {
555
+ if (scrollTimer) return;
556
+ scrollTimer = setTimeout(() => {
557
+ scrollTimer = null;
558
+ post({ type: 'pc:scroll', x: window.scrollX, y: window.scrollY });
559
+ }, 150);
560
+ }, { passive: true });
561
+ }
562
+ window.addEventListener('resize', scheduleRender);
563
+ if (typeof ResizeObserver === 'function') new ResizeObserver(scheduleRender).observe(document.documentElement);
564
+
565
+ syncUi();
566
+ render();
567
+ renderMini();
568
+ if (!FETCH) post({ type: 'pc:ready' });
569
+ }
570
+
571
+ function annotateClientJs({ transport = 'postMessage', version = '' } = {}) {
572
+ if (transport !== 'postMessage' && transport !== 'fetch') throw new Error(`unknown transport: ${transport}`);
573
+ const cfg = {
574
+ transport,
575
+ version: String(version).replace(/[^\w.+-]/g, '').slice(0, 40),
576
+ html: toolbarHtml(),
577
+ css: toolbarCss()
578
+ };
579
+ const consts = [
580
+ `const CAPS = ${JSON.stringify(model.CAPS)};`,
581
+ `const SHAPE_COLORS = ${JSON.stringify(model.SHAPE_COLORS)};`,
582
+ `const POINT_RULES = ${JSON.stringify(model.POINT_RULES)};`,
583
+ `const STRIP_RE = ${model.STRIP_RE};`,
584
+ `const CLASS_RE = ${model.CLASS_RE};`,
585
+ `const SRC_RE = ${model.SRC_RE};`,
586
+ `const TAG_RE = ${model.TAG_RE};`
587
+ ];
588
+ const fns = [
589
+ geometry.roundPoint, geometry.clampPoint, geometry.rawUnits, geometry.toAnchorUnits, geometry.fromAnchorUnits,
590
+ geometry.fitsAnchor, geometry.rdp, geometry.simplify, ...model.PURE_FUNCTIONS
591
+ ].map(fn => fn.toString());
592
+ const json = JSON.stringify(cfg).replace(/</g, '\\u003c');
593
+ return `'use strict';\n(() => {\n${consts.join('\n')}\n${fns.join('\n')}\n(${clientMain.toString()})(${json});\n})();\n`;
594
+ }
595
+
596
+ module.exports = { annotateClientJs };