@hybridlabor-api/aos 4.0.2 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/.claude/agents/database-reviewer.md +109 -0
  2. package/.claude/agents/go-build-resolver.md +112 -0
  3. package/.claude/agents/opensource-forker.md +216 -0
  4. package/.claude/agents/opensource-sanitizer.md +206 -0
  5. package/.claude/agents/security-reviewer.md +126 -0
  6. package/.claude/agents/silent-failure-hunter.md +68 -0
  7. package/CLAUDE.md +15 -0
  8. package/README.md +0 -13
  9. package/THIRD_PARTY_NOTICES.md +133 -0
  10. package/package.json +4 -2
  11. package/skills/basic/bdbmediastorm/SKILL.md +7 -1
  12. package/skills/basic/startcycle/SKILL.md +2 -0
  13. package/skills/bdbrainstorm/SKILL.md +1 -0
  14. package/skills/global_config/plan-canvas/SKILL.md +233 -0
  15. package/skills/global_config/plan-canvas/scripts/lib/loopback-guard.js +59 -0
  16. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/markdown.js +301 -0
  17. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/sdk.js +239 -0
  18. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/server.js +636 -0
  19. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/sessions.js +271 -0
  20. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/ui.js +630 -0
  21. package/skills/global_config/plan-canvas/scripts/plan-canvas.js +419 -0
  22. package/.agents/skills/firecrawl/SKILL.md +0 -149
  23. package/.agents/skills/firecrawl/rules/install.md +0 -82
  24. package/.agents/skills/firecrawl/rules/security.md +0 -26
  25. package/.agents/skills/firecrawl-agent/SKILL.md +0 -58
  26. package/.agents/skills/firecrawl-build/SKILL.md +0 -39
  27. package/.agents/skills/firecrawl-build-interact/SKILL.md +0 -68
  28. package/.agents/skills/firecrawl-build-onboarding/SKILL.md +0 -103
  29. package/.agents/skills/firecrawl-build-onboarding/references/auth-flow.md +0 -39
  30. package/.agents/skills/firecrawl-build-onboarding/references/project-setup.md +0 -20
  31. package/.agents/skills/firecrawl-build-onboarding/references/sdk-installation.md +0 -17
  32. package/.agents/skills/firecrawl-build-scrape/SKILL.md +0 -69
  33. package/.agents/skills/firecrawl-build-search/SKILL.md +0 -69
  34. package/.agents/skills/firecrawl-crawl/SKILL.md +0 -59
  35. package/.agents/skills/firecrawl-download/SKILL.md +0 -70
  36. package/.agents/skills/firecrawl-interact/SKILL.md +0 -84
  37. package/.agents/skills/firecrawl-map/SKILL.md +0 -51
  38. package/.agents/skills/firecrawl-scrape/SKILL.md +0 -69
  39. package/.agents/skills/firecrawl-search/SKILL.md +0 -60
  40. package/docs/sessions/AUDIT-HANDOVER-2026-08-28.md +0 -169
  41. package/docs/sessions/BDB_REMOTEOS_MCP_HANDOVER.md +0 -130
  42. package/docs/sessions/SESSION-HANDOVER-v3.13.md +0 -249
  43. package/mcps/RhinoMCP/cc-plugin/.claude/settings.json +0 -10
  44. package/mcps/after-effects-mcp/build/index.js +0 -840
  45. package/mcps/after-effects-mcp/build/scripts/applyEffect.jsx +0 -153
  46. package/mcps/after-effects-mcp/build/scripts/applyEffectTemplate.jsx +0 -218
  47. package/mcps/after-effects-mcp/build/scripts/createComposition.jsx +0 -71
  48. package/mcps/after-effects-mcp/build/scripts/createShapeLayer.jsx +0 -147
  49. package/mcps/after-effects-mcp/build/scripts/createSolidLayer.jsx +0 -114
  50. package/mcps/after-effects-mcp/build/scripts/createTextLayer.jsx +0 -115
  51. package/mcps/after-effects-mcp/build/scripts/getLayerInfo.jsx +0 -192
  52. package/mcps/after-effects-mcp/build/scripts/getProjectInfo.jsx +0 -90
  53. package/mcps/after-effects-mcp/build/scripts/listCompositions.jsx +0 -50
  54. package/mcps/after-effects-mcp/build/scripts/mcp-bridge-auto.jsx +0 -1773
  55. package/mcps/after-effects-mcp/build/scripts/setLayerProperties.jsx +0 -160
  56. package/mcps/bdb-remoteos-mcp/queue.db +0 -0
  57. package/mcps/bdb-remoteos-mcp/src/bdb_remoteos_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
  58. package/mcps/bdb-remoteos-mcp/src/bdb_remoteos_mcp/__pycache__/incus_client.cpython-312.pyc +0 -0
  59. package/mcps/bdb-remoteos-mcp/src/bdb_remoteos_mcp/__pycache__/main.cpython-312.pyc +0 -0
  60. package/mcps/bdb-remoteos-mcp/src/bdb_remoteos_mcp/__pycache__/queue.cpython-312.pyc +0 -0
  61. package/mcps/bdb-remoteos-mcp/src/bdb_remoteos_mcp/__pycache__/schemas.cpython-312.pyc +0 -0
  62. package/mcps/bdb-remoteos-mcp/src/bdb_remoteos_mcp/__pycache__/server.cpython-312.pyc +0 -0
  63. package/mcps/bdb-remoteos-mcp/src/bdb_remoteos_mcp/__pycache__/webhook.cpython-312.pyc +0 -0
  64. package/mcps/bdb-remoteos-mcp/tests/__pycache__/__init__.cpython-312.pyc +0 -0
  65. package/mcps/bdb-remoteos-mcp/tests/__pycache__/mock_incus.cpython-312.pyc +0 -0
  66. package/mcps/bdb-remoteos-mcp/tests/__pycache__/test_mcp_server.cpython-312-pytest-9.1.1.pyc +0 -0
  67. package/mcps/bdb-remoteos-mcp/tests/__pycache__/test_security_redteam.cpython-312-pytest-9.1.1.pyc +0 -0
  68. package/mcps/bdb-remoteos-mcp/tests/__pycache__/test_webhook.cpython-312-pytest-9.1.1.pyc +0 -0
  69. package/mcps/computer-use-mcp/dist/client.d.ts +0 -150
  70. package/mcps/computer-use-mcp/dist/client.js +0 -136
  71. package/mcps/computer-use-mcp/dist/entrypoint.d.ts +0 -16
  72. package/mcps/computer-use-mcp/dist/entrypoint.js +0 -26
  73. package/mcps/computer-use-mcp/dist/native.d.ts +0 -212
  74. package/mcps/computer-use-mcp/dist/native.js +0 -50
  75. package/mcps/computer-use-mcp/dist/server.d.ts +0 -32
  76. package/mcps/computer-use-mcp/dist/server.js +0 -342
  77. package/mcps/computer-use-mcp/dist/session.d.ts +0 -101
  78. package/mcps/computer-use-mcp/dist/session.js +0 -2372
  79. package/skills/bdbsaastraining/scripts/__pycache__/build_profile.cpython-314.pyc +0 -0
@@ -0,0 +1,630 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Plan Canvas browser chrome: the editor shell that frames an artifact,
5
+ * plus the rendered-markdown artifact template.
6
+ *
7
+ * Visual language mirrors the ECC web dashboard (scripts/dashboard-web.js):
8
+ * same design tokens, dark-first with a light theme, accent→pink brand
9
+ * gradient. Everything is served inline — no CDNs, no external assets.
10
+ *
11
+ * Source: affaan-m/ECC — MIT, see THIRD_PARTY_NOTICES.md
12
+ */
13
+
14
+ const path = require('path');
15
+
16
+ const { escapeHtml } = require('./markdown');
17
+
18
+ // Pinned Mermaid ESM build, loaded in the browser only when an artifact
19
+ // actually contains a diagram. Override with a local/vendored URL (e.g. an
20
+ // air-gapped mirror) via AOS_PLAN_CANVAS_MERMAID_URL. If the fetch fails, the
21
+ // diagram source stays visible as a styled code block — nothing breaks.
22
+ const DEFAULT_MERMAID_URL = 'https://cdn.jsdelivr.net/npm/mermaid@11.4.1/dist/mermaid.esm.min.mjs';
23
+
24
+ function mermaidUrl(env = process.env) {
25
+ const override = env.AOS_PLAN_CANVAS_MERMAID_URL;
26
+ return override && String(override).trim() ? String(override).trim() : DEFAULT_MERMAID_URL;
27
+ }
28
+
29
+ // Browser module that renders `<pre class="mermaid">` blocks, themed to match
30
+ // the ECC canvas. Kept import-only so a CDN failure degrades gracefully.
31
+ function mermaidLoaderScript(url) {
32
+ return `<script type="module">
33
+ try {
34
+ const mermaid = (await import(${JSON.stringify(url)})).default;
35
+ mermaid.initialize({
36
+ startOnLoad: false,
37
+ securityLevel: 'strict',
38
+ theme: 'dark',
39
+ fontFamily: "-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif",
40
+ themeVariables: {
41
+ primaryColor: '#13161e', primaryBorderColor: '#6885e8', primaryTextColor: '#dfe2e9',
42
+ lineColor: '#80859a', secondaryColor: '#191d2a', tertiaryColor: '#101218',
43
+ background: '#080a0e', mainBkg: '#13161e', clusterBkg: '#0d0f14'
44
+ }
45
+ });
46
+ await mermaid.run({ querySelector: '.mermaid' });
47
+ } catch (err) {
48
+ document.querySelectorAll('.mermaid').forEach(el => el.classList.add('mermaid-unrendered'));
49
+ console.warn('Mermaid render skipped:', err && err.message);
50
+ }
51
+ </script>`;
52
+ }
53
+
54
+ // Design tokens shared by the chrome and the markdown artifact template.
55
+ const TOKENS_CSS = `
56
+ :root{
57
+ --bg:#080a0e; --bg2:#0d0f14; --bg3:#13161e; --bg4:#191d2a;
58
+ --surface:#101218; --surface-hover:#171a24; --border:#1d2130; --border-light:#272c3e;
59
+ --text:#dfe2e9; --text2:#80859a; --text3:#4c5168;
60
+ --accent:#6885e8; --accent-glow:rgba(104,133,232,0.15); --accent-dim:#3d5ab8;
61
+ --green:#4acb8a; --green-glow:rgba(74,203,138,0.15);
62
+ --orange:#eca85a; --orange-glow:rgba(236,168,90,0.15);
63
+ --pink:#e26a9e; --pink-glow:rgba(226,106,158,0.15);
64
+ --red:#e86060; --red-glow:rgba(232,96,96,0.15);
65
+ --teal:#4acbbe; --teal-glow:rgba(74,203,190,0.15);
66
+ --radius:8px; --radius-sm:5px;
67
+ --font:-apple-system,BlinkMacSystemFont,'SF Pro Display','Inter','Segoe UI',Roboto,sans-serif;
68
+ --mono:'SF Mono','Fira Code','JetBrains Mono','Cascadia Code',monospace;
69
+ --shadow:0 1px 2px rgba(0,0,0,0.4);
70
+ --shadow-lg:0 8px 32px rgba(0,0,0,0.6);
71
+ }
72
+ [data-theme="light"]{
73
+ --bg:#f4f5f7; --bg2:#ffffff; --bg3:#eaecef; --bg4:#dfe2e6;
74
+ --surface:#ffffff; --surface-hover:#f4f5f7; --border:#cdd1d9; --border-light:#dde1e8;
75
+ --text:#181b23; --text2:#585e6e; --text3:#9197a8;
76
+ --accent:#4560d0; --accent-glow:rgba(69,96,208,0.08); --accent-dim:#2f44a0;
77
+ --green:#16a34a; --green-glow:rgba(22,163,74,0.08);
78
+ --orange:#d97706; --orange-glow:rgba(217,119,6,0.08);
79
+ --pink:#c73877; --pink-glow:rgba(199,56,119,0.08);
80
+ --red:#dc2626; --red-glow:rgba(220,38,38,0.08);
81
+ --teal:#0d9488; --teal-glow:rgba(13,148,136,0.08);
82
+ --shadow:0 1px 2px rgba(0,0,0,0.04);
83
+ --shadow-lg:0 8px 32px rgba(0,0,0,0.08);
84
+ }
85
+ `;
86
+
87
+ function canvasCss() {
88
+ return `${TOKENS_CSS}
89
+ *{margin:0;padding:0;box-sizing:border-box}
90
+ html,body{height:100%}
91
+ body{font-family:var(--font);background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased;line-height:1.4;overflow:hidden}
92
+ ::selection{background:var(--accent);color:#fff}
93
+ ::-webkit-scrollbar{width:8px;height:8px}
94
+ ::-webkit-scrollbar-track{background:transparent}
95
+ ::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px}
96
+ button{font-family:var(--font)}
97
+
98
+ .bar{display:flex;align-items:center;gap:12px;height:52px;padding:0 16px;background:color-mix(in srgb,var(--bg2) 88%,transparent);border-bottom:1px solid var(--border);backdrop-filter:blur(16px)}
99
+ .brand{display:flex;align-items:center;gap:9px;min-width:0}
100
+ .brand .logo{width:26px;height:26px;flex:none;background:linear-gradient(135deg,var(--accent),var(--pink));border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;color:#fff}
101
+ .brand .name{font-size:13.5px;font-weight:600;white-space:nowrap}
102
+ .brand .file{font-size:11.5px;color:var(--text2);font-family:var(--mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:34vw}
103
+ .bar .spacer{flex:1}
104
+
105
+ .presence{display:flex;align-items:center;gap:6px;font-size:11px;font-weight:500;color:var(--text2);background:var(--bg3);border:1px solid var(--border);border-radius:99px;padding:3px 10px 3px 8px;white-space:nowrap}
106
+ .presence .dot{width:7px;height:7px;border-radius:99px;background:var(--text3)}
107
+ .presence[data-state="listening"] .dot{background:var(--green);box-shadow:0 0 0 3px var(--green-glow);animation:pulse 2s infinite}
108
+ .presence[data-state="thinking"] .dot,.presence[data-state="typing"] .dot{background:var(--accent);box-shadow:0 0 0 3px var(--accent-glow);animation:pulse 1.2s infinite}
109
+ .presence[data-state="queued"] .dot{background:var(--orange);box-shadow:0 0 0 3px var(--orange-glow)}
110
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.45}}
111
+
112
+ .toggle{display:flex;align-items:center;gap:7px;font-size:11.5px;color:var(--text2);cursor:pointer;user-select:none}
113
+ .toggle .track{width:30px;height:17px;border-radius:99px;background:var(--bg4);border:1px solid var(--border);position:relative;transition:background .15s}
114
+ .toggle .knob{position:absolute;top:1px;left:1px;width:13px;height:13px;border-radius:99px;background:var(--text2);transition:transform .15s,background .15s}
115
+ .toggle[aria-pressed="true"] .track{background:var(--accent);border-color:var(--accent-dim)}
116
+ .toggle[aria-pressed="true"] .knob{transform:translateX(13px);background:#fff}
117
+
118
+ .icon-btn{height:28px;padding:0 10px;border-radius:6px;border:1px solid var(--border);background:var(--bg3);color:var(--text2);cursor:pointer;font-size:11.5px;display:flex;align-items:center;gap:5px;transition:all .12s}
119
+ .icon-btn:hover{border-color:var(--border-light);color:var(--text);background:var(--bg4)}
120
+ .icon-btn.danger:hover{border-color:var(--red);color:var(--red);background:var(--red-glow)}
121
+
122
+ .layout{display:flex;height:calc(100% - 52px)}
123
+ .frame{flex:1;min-width:0;position:relative;background:var(--bg2)}
124
+ .frame iframe{width:100%;height:100%;border:0;background:#fff}
125
+ [data-theme] .frame iframe{background:var(--bg2)}
126
+
127
+ .panel{width:340px;flex:none;display:flex;flex-direction:column;border-left:1px solid var(--border);background:var(--bg2)}
128
+ .panel h2{font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--text3);padding:12px 14px 8px}
129
+
130
+ .verdict{display:flex;gap:8px;padding:0 14px 12px;border-bottom:1px solid var(--border)}
131
+ .verdict button{flex:1;height:30px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;transition:all .12s}
132
+ .verdict .approve{border:1px solid var(--green);background:var(--green-glow);color:var(--green)}
133
+ .verdict .approve:hover{background:var(--green);color:#fff}
134
+ .verdict .changes{border:1px solid var(--orange);background:var(--orange-glow);color:var(--orange)}
135
+ .verdict .changes:hover{background:var(--orange);color:#fff}
136
+
137
+ .chat{flex:1;overflow-y:auto;padding:10px 14px;display:flex;flex-direction:column;gap:8px}
138
+ .msg{max-width:92%;padding:7px 10px;border-radius:10px;font-size:12.5px;white-space:pre-wrap;word-break:break-word}
139
+ .msg.user{align-self:flex-end;background:var(--accent-glow);border:1px solid color-mix(in srgb,var(--accent) 35%,transparent);color:var(--text);border-bottom-right-radius:3px}
140
+ .msg.agent{align-self:flex-start;background:var(--bg3);border:1px solid var(--border);color:var(--text);border-bottom-left-radius:3px}
141
+ .msg .meta{display:block;font-size:9.5px;color:var(--text3);margin-top:3px}
142
+ .msg.kind-annotation{border-left:2px solid var(--teal)}
143
+ .msg.kind-verdict{border-left:2px solid var(--green)}
144
+ .chat .empty{color:var(--text3);font-size:12px;text-align:center;margin-top:24px;line-height:1.6}
145
+
146
+ /* iMessage-style activity bubble: dots while the agent thinks or types. */
147
+ .typing{align-self:flex-start;display:none;align-items:center;gap:8px;background:var(--bg3);border:1px solid var(--border);border-bottom-left-radius:3px;border-radius:10px;padding:9px 12px}
148
+ .typing.show{display:flex}
149
+ .typing .dots{display:flex;align-items:center;gap:3px}
150
+ .typing .dots i{width:6px;height:6px;border-radius:99px;background:var(--text2);animation:typing-bounce 1.4s infinite ease-in-out both}
151
+ .typing .dots i:nth-child(1){animation-delay:-.32s}
152
+ .typing .dots i:nth-child(2){animation-delay:-.16s}
153
+ .typing .label{font-size:11px;color:var(--text3)}
154
+ @keyframes typing-bounce{0%,80%,100%{transform:translateY(0);opacity:.4}40%{transform:translateY(-4px);opacity:1}}
155
+ @media (prefers-reduced-motion:reduce){
156
+ .typing .dots i{animation:none;opacity:.7}
157
+ .presence .dot{animation:none}
158
+ }
159
+ /* A queued message nobody is listening for gets an explicit, honest note. */
160
+ .stalled{align-self:flex-start;display:none;gap:8px;background:var(--orange-glow);border:1px solid color-mix(in srgb,var(--orange) 35%,transparent);border-radius:10px;padding:8px 11px;font-size:11.5px;color:var(--text2);line-height:1.5}
161
+ .stalled.show{display:flex}
162
+
163
+ .queue{padding:8px 14px 0;display:flex;flex-direction:column;gap:6px;max-height:180px;overflow-y:auto}
164
+ .pill{display:flex;align-items:flex-start;gap:8px;background:var(--bg3);border:1px solid var(--border);border-left:2px solid var(--teal);border-radius:6px;padding:6px 8px;font-size:11.5px}
165
+ .pill.kind-chat{border-left-color:var(--accent)}
166
+ .pill.kind-verdict{border-left-color:var(--green)}
167
+ .pill .where{color:var(--teal);font-family:var(--mono);font-size:10px;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
168
+ .pill .body{flex:1;min-width:0;color:var(--text2)}
169
+ .pill .txt{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)}
170
+ .pill button{border:none;background:none;color:var(--text3);cursor:pointer;font-size:13px;line-height:1;padding:1px}
171
+ .pill button:hover{color:var(--red)}
172
+
173
+ .composer{padding:10px 14px 14px;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:8px}
174
+ .composer .hint{font-size:10px;color:var(--text3)}
175
+ .composer textarea{width:100%;min-height:60px;max-height:160px;resize:vertical;background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:8px 10px;color:var(--text);font-size:12.5px;font-family:var(--font);outline:none;transition:all .15s}
176
+ .composer textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow)}
177
+ .composer .row{display:flex;gap:8px;align-items:center}
178
+ .composer .send{flex:1;height:32px;border:none;border-radius:6px;background:var(--accent);color:#fff;font-size:12.5px;font-weight:600;cursor:pointer;transition:all .12s}
179
+ .composer .send:hover{background:var(--accent-dim)}
180
+ .composer .send:disabled{opacity:.5;cursor:default}
181
+ .composer .status{font-size:10.5px;color:var(--text3)}
182
+
183
+ .overlay{position:absolute;inset:0;display:none;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bg) 80%,transparent);backdrop-filter:blur(6px);z-index:50}
184
+ .overlay.show{display:flex}
185
+ .overlay .card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-lg);padding:26px 32px;text-align:center;max-width:340px}
186
+ .overlay .card h3{font-size:14px;margin-bottom:6px}
187
+ .overlay .card p{font-size:12px;color:var(--text2);line-height:1.5}
188
+ `;
189
+ }
190
+
191
+ // Client logic for the chrome page (runs in the top window).
192
+ function canvasClientJs() {
193
+ return `'use strict';
194
+ (() => {
195
+ const boot = JSON.parse(document.getElementById('pc-session').textContent);
196
+ const key = boot.key;
197
+ const $ = id => document.getElementById(id);
198
+ const frame = $('artifact');
199
+ const chatLog = $('chatLog');
200
+ const queueEl = $('queue');
201
+ const input = $('chatInput');
202
+ const sendBtn = $('send');
203
+ const statusEl = $('sendStatus');
204
+ const presence = $('presence');
205
+ const QKEY = 'ecc-plan-canvas:queue:' + key;
206
+ let queue = [];
207
+ let lastScroll = { x: 0, y: 0 };
208
+ let ended = boot.status === 'ended';
209
+ let sending = false;
210
+
211
+ try { queue = JSON.parse(sessionStorage.getItem(QKEY) || '[]'); } catch { queue = []; }
212
+
213
+ // --- theme ---------------------------------------------------------
214
+ const themeKey = 'ecc-plan-canvas:theme';
215
+ function applyTheme(t) {
216
+ if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
217
+ else document.documentElement.removeAttribute('data-theme');
218
+ $('themeBtn').textContent = t === 'light' ? '\\u263E dark' : '\\u2600 light';
219
+ }
220
+ // Storage access throws outright when the browser blocks site data for this
221
+ // origin (loopback is a common trigger). Unguarded, that killed the whole
222
+ // client IIFE here, before the send button and Enter handlers bound below:
223
+ // every control rendered and stayed inert. sessionStorage is already guarded
224
+ // above and below; match it. See affaan-m/ECC#2702.
225
+ function readTheme() {
226
+ try { return localStorage.getItem(themeKey); } catch { return null; }
227
+ }
228
+ function writeTheme(v) {
229
+ try { localStorage.setItem(themeKey, v); } catch { /* site data blocked */ }
230
+ }
231
+ let theme = readTheme() || 'dark';
232
+ applyTheme(theme);
233
+ $('themeBtn').addEventListener('click', () => {
234
+ theme = theme === 'light' ? 'dark' : 'light';
235
+ writeTheme(theme);
236
+ applyTheme(theme);
237
+ });
238
+
239
+ // --- annotate mode -------------------------------------------------
240
+ let annotate = true;
241
+ function setAnnotate(on) {
242
+ annotate = on;
243
+ $('annotate').setAttribute('aria-pressed', String(on));
244
+ postToFrame({ type: 'pc:set-mode', annotate: on });
245
+ }
246
+ $('annotate').addEventListener('click', () => setAnnotate(!annotate));
247
+ document.addEventListener('keydown', e => {
248
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'i') {
249
+ e.preventDefault();
250
+ setAnnotate(!annotate);
251
+ }
252
+ }, true);
253
+
254
+ // --- iframe bridge --------------------------------------------------
255
+ function postToFrame(msg) {
256
+ if (frame.contentWindow) frame.contentWindow.postMessage(msg, '*');
257
+ }
258
+ window.addEventListener('message', e => {
259
+ if (e.source !== frame.contentWindow) return;
260
+ const msg = e.data || {};
261
+ if (msg.type === 'pc:queue' && msg.item) addToQueue(msg.item);
262
+ else if (msg.type === 'pc:queue-and-send' && msg.item) { addToQueue(msg.item); send(); }
263
+ else if (msg.type === 'pc:scroll') lastScroll = { x: msg.x || 0, y: msg.y || 0 };
264
+ else if (msg.type === 'pc:toggle-mode') setAnnotate(!annotate);
265
+ else if (msg.type === 'pc:ready') {
266
+ postToFrame({ type: 'pc:set-mode', annotate });
267
+ postToFrame({ type: 'pc:restore-scroll', x: lastScroll.x, y: lastScroll.y });
268
+ }
269
+ });
270
+
271
+ // --- queue ----------------------------------------------------------
272
+ function persistQueue() { try { sessionStorage.setItem(QKEY, JSON.stringify(queue)); } catch { /* full */ } }
273
+ function addToQueue(item) { queue.push(item); persistQueue(); renderQueue(); }
274
+ function renderQueue() {
275
+ queueEl.innerHTML = '';
276
+ queue.forEach((item, i) => {
277
+ const pill = document.createElement('div');
278
+ pill.className = 'pill kind-' + item.kind;
279
+ const body = document.createElement('span');
280
+ body.className = 'body';
281
+ if (item.anchor) {
282
+ const where = document.createElement('span');
283
+ where.className = 'where';
284
+ where.textContent = item.anchor.snippet || item.anchor.selector;
285
+ body.appendChild(where);
286
+ }
287
+ const txt = document.createElement('span');
288
+ txt.className = 'txt';
289
+ txt.textContent = item.kind === 'verdict' ? (item.verdict === 'approve' ? 'Approve plan' : 'Request changes') + (item.text ? ': ' + item.text : '') : item.text;
290
+ body.appendChild(txt);
291
+ const rm = document.createElement('button');
292
+ rm.textContent = '\\u00D7';
293
+ rm.title = 'Remove';
294
+ rm.addEventListener('click', () => { queue.splice(i, 1); persistQueue(); renderQueue(); });
295
+ pill.append(body, rm);
296
+ queueEl.appendChild(pill);
297
+ });
298
+ }
299
+ renderQueue();
300
+
301
+ // --- activity indicators ---------------------------------------------
302
+ // Built once and re-appended on every chat render so the animation never
303
+ // restarts mid-thought.
304
+ const typingEl = document.createElement('div');
305
+ typingEl.className = 'typing';
306
+ typingEl.setAttribute('role', 'status');
307
+ typingEl.setAttribute('aria-live', 'polite');
308
+ const dots = document.createElement('span');
309
+ dots.className = 'dots';
310
+ dots.append(document.createElement('i'), document.createElement('i'), document.createElement('i'));
311
+ const typingLabel = document.createElement('span');
312
+ typingLabel.className = 'label';
313
+ typingEl.append(dots, typingLabel);
314
+
315
+ const stalledEl = document.createElement('div');
316
+ stalledEl.className = 'stalled';
317
+ stalledEl.setAttribute('role', 'status');
318
+
319
+ const TYPING_LABELS = { thinking: 'agent is thinking\\u2026', typing: 'agent is typing\\u2026' };
320
+
321
+ function renderActivity(state) {
322
+ const typingText = TYPING_LABELS[state];
323
+ typingEl.classList.toggle('show', Boolean(typingText));
324
+ if (typingText) typingLabel.textContent = typingText;
325
+ const stalled = state === 'queued';
326
+ stalledEl.classList.toggle('show', stalled);
327
+ if (stalled) {
328
+ stalledEl.textContent =
329
+ 'Delivered to the queue. Your agent is not listening right now, so it picks this up the moment it checks in.';
330
+ }
331
+ if (typingText || stalled) scrollToEnd();
332
+ }
333
+
334
+ // --- chat -----------------------------------------------------------
335
+ function atBottom() {
336
+ return chatLog.scrollHeight - chatLog.scrollTop - chatLog.clientHeight < 40;
337
+ }
338
+ function scrollToEnd() { chatLog.scrollTop = chatLog.scrollHeight; }
339
+
340
+ function renderChat(entries) {
341
+ const pinned = atBottom();
342
+ chatLog.innerHTML = '';
343
+ if (!entries.length) {
344
+ const empty = document.createElement('div');
345
+ empty.className = 'empty';
346
+ empty.textContent = 'Click anything in the plan to annotate it, or type below. Feedback goes straight to your agent.';
347
+ chatLog.appendChild(empty);
348
+ } else {
349
+ for (const entry of entries) {
350
+ const div = document.createElement('div');
351
+ div.className = 'msg ' + (entry.role === 'agent' ? 'agent' : 'user') + ' kind-' + (entry.kind || 'chat');
352
+ div.textContent = entry.text;
353
+ const meta = document.createElement('span');
354
+ meta.className = 'meta';
355
+ meta.textContent = (entry.role === 'agent' ? 'agent' : 'you') + ' \\u00B7 ' + new Date(entry.at).toLocaleTimeString();
356
+ div.appendChild(meta);
357
+ chatLog.appendChild(div);
358
+ }
359
+ }
360
+ // The indicators live at the tail of the log, so they survive re-render.
361
+ chatLog.appendChild(typingEl);
362
+ chatLog.appendChild(stalledEl);
363
+ if (pinned) scrollToEnd();
364
+ }
365
+ renderChat(boot.chat || []);
366
+
367
+ // --- send -----------------------------------------------------------
368
+ async function send(extraItems) {
369
+ if (ended || sending) return;
370
+ const items = queue.slice();
371
+ if (extraItems) items.push(...extraItems);
372
+ const text = input.value.trim();
373
+ if (text) items.push({ kind: 'chat', text });
374
+ if (!items.length) {
375
+ statusEl.textContent = 'Nothing to send yet - annotate the plan or type a message.';
376
+ return;
377
+ }
378
+ sending = true;
379
+ sendBtn.disabled = true;
380
+ statusEl.textContent = 'Sending\\u2026';
381
+ try {
382
+ const res = await fetch('/api/session/' + key + '/feedback', {
383
+ method: 'POST',
384
+ headers: { 'content-type': 'application/json' },
385
+ body: JSON.stringify({ items })
386
+ });
387
+ if (!res.ok) throw new Error('HTTP ' + res.status);
388
+ const body = await res.json().catch(() => ({}));
389
+ queue = [];
390
+ persistQueue();
391
+ renderQueue();
392
+ input.value = '';
393
+ // Say what actually happened: a parked agent takes the batch on the
394
+ // spot, otherwise it sits in the queue until the agent checks in.
395
+ statusEl.textContent = body.presence === 'thinking' || body.presence === 'typing'
396
+ ? 'Delivered. Your agent has it.'
397
+ : 'Queued. Your agent picks this up the moment it checks in.';
398
+ if (body.presence) applyPresence(body.presence);
399
+ } catch (err) {
400
+ statusEl.textContent = 'Send failed (' + err.message + ') - is the canvas server still running?';
401
+ } finally {
402
+ sending = false;
403
+ sendBtn.disabled = ended;
404
+ }
405
+ }
406
+ sendBtn.addEventListener('click', () => send());
407
+ input.addEventListener('keydown', e => {
408
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
409
+ });
410
+ $('approve').addEventListener('click', () => send([{ kind: 'verdict', verdict: 'approve' }]));
411
+ $('changes').addEventListener('click', () => send([{ kind: 'verdict', verdict: 'request-changes' }]));
412
+
413
+ // --- session controls ------------------------------------------------
414
+ $('reloadBtn').addEventListener('click', reloadArtifact);
415
+ $('endBtn').addEventListener('click', async () => {
416
+ if (!window.confirm('End this review session?')) return;
417
+ try { await fetch('/api/session/' + key + '/end', { method: 'POST' }); } catch { /* server gone */ }
418
+ });
419
+ function reloadArtifact() {
420
+ const base = frame.getAttribute('data-artifact-src');
421
+ frame.src = base + '?t=' + Date.now();
422
+ }
423
+ function markEnded(endedBy) {
424
+ ended = true;
425
+ sendBtn.disabled = true;
426
+ input.disabled = true;
427
+ renderActivity('ended');
428
+ presence.setAttribute('data-state', 'ended');
429
+ presence.querySelector('.label').textContent = 'session ended';
430
+ $('endedOverlay').classList.add('show');
431
+ $('endedWho').textContent = endedBy === 'agent'
432
+ ? 'Your agent closed this review.'
433
+ : 'You ended this review. Head back to your agent session.';
434
+ }
435
+ if (ended) markEnded(boot.endedBy);
436
+
437
+ // --- server events ----------------------------------------------------
438
+ const PRESENCE_LABELS = {
439
+ waiting: 'agent not connected',
440
+ listening: 'agent listening',
441
+ thinking: 'agent is thinking\\u2026',
442
+ typing: 'agent is typing\\u2026',
443
+ queued: 'queued for your agent'
444
+ };
445
+ function applyPresence(state) {
446
+ if (ended) return;
447
+ presence.setAttribute('data-state', state);
448
+ presence.querySelector('.label').textContent = PRESENCE_LABELS[state] || state;
449
+ renderActivity(state);
450
+ }
451
+ function connectEvents() {
452
+ const es = new EventSource('/events/' + key);
453
+ es.addEventListener('chat-sync', e => renderChat(JSON.parse(e.data).chat || []));
454
+ es.addEventListener('presence', e => applyPresence(JSON.parse(e.data).state));
455
+ es.addEventListener('reload', reloadArtifact);
456
+ es.addEventListener('ended', e => { markEnded(JSON.parse(e.data).endedBy); es.close(); });
457
+ es.onerror = () => {
458
+ if (ended) return;
459
+ renderActivity('offline');
460
+ presence.setAttribute('data-state', 'waiting');
461
+ presence.querySelector('.label').textContent = 'canvas server offline';
462
+ };
463
+ }
464
+ connectEvents();
465
+ })();`;
466
+ }
467
+
468
+ // The chrome page: header bar, artifact iframe, conversation rail.
469
+ function renderCanvasHtml(session, { clientPath = '/client.js', cssPath = '/canvas.css' } = {}) {
470
+ const name = path.basename(session.file);
471
+ const bootstrap = JSON.stringify({
472
+ key: session.key,
473
+ file: session.file,
474
+ status: session.status,
475
+ endedBy: session.endedBy || null,
476
+ chat: session.chat
477
+ }).replace(/</g, '\\u003c');
478
+ const artifactSrc = `/artifact/${session.key}/`;
479
+ return `<!DOCTYPE html>
480
+ <html lang="en">
481
+ <head>
482
+ <meta charset="utf-8">
483
+ <meta name="viewport" content="width=device-width, initial-scale=1">
484
+ <title>${escapeHtml(name)} · Plan Canvas</title>
485
+ <link rel="stylesheet" href="${cssPath}">
486
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><defs><linearGradient id='g' x1='0' y1='0' x2='1' y2='1'><stop offset='0' stop-color='%236885e8'/><stop offset='1' stop-color='%23e26a9e'/></linearGradient></defs><rect width='100' height='100' rx='22' fill='url(%23g)'/><text x='50' y='68' font-size='52' font-weight='700' font-family='sans-serif' fill='white' text-anchor='middle'>E</text></svg>">
487
+ </head>
488
+ <body>
489
+ <script id="pc-session" type="application/json">${bootstrap}</script>
490
+ <header class="bar">
491
+ <div class="brand">
492
+ <div class="logo">E</div>
493
+ <span class="name">Plan Canvas</span>
494
+ <span class="file" title="${escapeHtml(session.file)}">${escapeHtml(name)}</span>
495
+ </div>
496
+ <div class="spacer"></div>
497
+ <div id="presence" class="presence" data-state="waiting"><span class="dot"></span><span class="label">agent not connected</span></div>
498
+ <div id="annotate" class="toggle" role="switch" aria-pressed="true" title="Toggle annotate mode (Cmd/Ctrl+I)">
499
+ <span>Annotate</span><span class="track"><span class="knob"></span></span>
500
+ </div>
501
+ <button id="themeBtn" class="icon-btn" type="button">light</button>
502
+ <button id="reloadBtn" class="icon-btn" type="button" title="Reload artifact">Reload</button>
503
+ <button id="endBtn" class="icon-btn danger" type="button">End session</button>
504
+ </header>
505
+ <div class="layout">
506
+ <main class="frame">
507
+ <iframe id="artifact" title="Artifact under review" src="${artifactSrc}" data-artifact-src="${artifactSrc}" sandbox="allow-scripts allow-forms allow-popups"></iframe>
508
+ <div id="endedOverlay" class="overlay"><div class="card"><h3>Session ended</h3><p id="endedWho"></p></div></div>
509
+ </main>
510
+ <aside class="panel">
511
+ <h2>Plan verdict</h2>
512
+ <div class="verdict">
513
+ <button id="approve" class="approve" type="button">Approve plan</button>
514
+ <button id="changes" class="changes" type="button">Request changes</button>
515
+ </div>
516
+ <h2>Conversation</h2>
517
+ <div id="chatLog" class="chat"></div>
518
+ <div id="queue" class="queue"></div>
519
+ <div class="composer">
520
+ <textarea id="chatInput" placeholder="Message your agent&#10;Enter to send &middot; Shift+Enter for a new line"></textarea>
521
+ <div class="row">
522
+ <button id="send" class="send" type="button">Send to agent</button>
523
+ </div>
524
+ <div id="sendStatus" class="status"></div>
525
+ <div class="hint">Annotations queue up here until you send them together.</div>
526
+ </div>
527
+ </aside>
528
+ </div>
529
+ <script src="${clientPath}"></script>
530
+ </body>
531
+ </html>`;
532
+ }
533
+
534
+ // ECC-styled document template for rendered markdown plan artifacts.
535
+ function renderMarkdownArtifactHtml(bodyHtml, { title, sdkSrc }) {
536
+ const hasMermaid = bodyHtml.includes('class="mermaid"');
537
+ return `<!DOCTYPE html>
538
+ <html lang="en">
539
+ <head>
540
+ <meta charset="utf-8">
541
+ <meta name="viewport" content="width=device-width, initial-scale=1">
542
+ <title>${escapeHtml(title)}</title>
543
+ <style>
544
+ ${TOKENS_CSS}
545
+ *{margin:0;padding:0;box-sizing:border-box}
546
+ body{font-family:var(--font);background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased;line-height:1.65;font-size:14.5px}
547
+ .doc{max-width:860px;margin:0 auto;padding:44px 36px 90px}
548
+ h1,h2,h3,h4,h5,h6{line-height:1.25;margin:1.6em 0 .55em;letter-spacing:-.01em}
549
+ h1{font-size:26px;margin-top:.3em;padding-bottom:.45em;border-bottom:1px solid var(--border)}
550
+ h1:after{content:'';display:block;width:56px;height:3px;margin-top:14px;border-radius:2px;background:linear-gradient(90deg,var(--accent),var(--pink))}
551
+ h2{font-size:19px;padding-bottom:.3em;border-bottom:1px solid var(--border)}
552
+ h3{font-size:15.5px}
553
+ h4,h5,h6{font-size:13.5px;color:var(--text2);text-transform:uppercase;letter-spacing:.05em}
554
+ p,ul,ol,blockquote,table,pre{margin-bottom:.9em}
555
+ ul,ol{padding-left:1.5em}
556
+ li{margin:.25em 0}
557
+ li.task{list-style:none;margin-left:-1.3em}
558
+ li.task input{margin-right:.5em;accent-color:var(--accent)}
559
+ a{color:var(--accent);text-decoration:none;border-bottom:1px solid var(--accent-glow)}
560
+ a:hover{border-bottom-color:var(--accent)}
561
+ code{font-family:var(--mono);font-size:.88em;background:var(--bg3);border:1px solid var(--border);border-radius:4px;padding:.12em .38em}
562
+ pre{background:var(--bg3);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;overflow-x:auto}
563
+ pre code{background:none;border:none;padding:0;font-size:12.5px;line-height:1.55}
564
+ blockquote{border-left:3px solid var(--accent);background:var(--accent-glow);border-radius:0 var(--radius-sm) var(--radius-sm) 0;padding:8px 14px;color:var(--text2)}
565
+ table{width:100%;border-collapse:collapse;font-size:13px;display:block;overflow-x:auto}
566
+ th,td{text-align:left;padding:7px 12px;border:1px solid var(--border)}
567
+ th{background:var(--bg3);font-weight:600;font-size:11.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--text2);white-space:nowrap}
568
+ tbody tr:hover{background:var(--surface-hover)}
569
+ hr{border:none;border-top:1px solid var(--border);margin:1.6em 0}
570
+ img{max-width:100%;border-radius:var(--radius-sm)}
571
+ pre.mermaid{font-family:var(--mono);font-size:12.5px;line-height:1.55;white-space:pre-wrap}
572
+ pre.mermaid[data-processed]{background:transparent;border:none;padding:4px 0;text-align:center;overflow-x:auto}
573
+ pre.mermaid[data-processed] svg{max-width:100%;height:auto}
574
+ pre.mermaid.mermaid-unrendered:before{content:'diagram source (renderer unavailable)';display:block;font-family:var(--font);font-size:10.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--text3);margin-bottom:6px}
575
+ </style>
576
+ </head>
577
+ <body>
578
+ <article class="doc">
579
+ ${bodyHtml}
580
+ </article>
581
+ ${hasMermaid ? mermaidLoaderScript(mermaidUrl()) : ''}
582
+ <script src="${sdkSrc}"></script>
583
+ </body>
584
+ </html>`;
585
+ }
586
+
587
+ // Landing page listing sessions (GET /).
588
+ function renderSessionListHtml(sessions) {
589
+ const rows = sessions.map(s => {
590
+ const status = s.status === 'ended' ? `ended by ${escapeHtml(s.endedBy || 'agent')}` : s.status;
591
+ const link = s.status === 'ended'
592
+ ? escapeHtml(path.basename(s.file))
593
+ : `<a href="/canvas/${escapeHtml(s.key)}">${escapeHtml(path.basename(s.file))}</a>`;
594
+ return `<tr><td>${link}</td><td class="mono">${escapeHtml(s.file)}</td><td><span class="badge ${escapeHtml(s.status)}">${status}</span></td></tr>`;
595
+ }).join('\n');
596
+ return `<!DOCTYPE html>
597
+ <html lang="en">
598
+ <head>
599
+ <meta charset="utf-8">
600
+ <title>Plan Canvas · sessions</title>
601
+ <style>
602
+ ${TOKENS_CSS}
603
+ *{margin:0;padding:0;box-sizing:border-box}
604
+ body{font-family:var(--font);background:var(--bg);color:var(--text);padding:40px;line-height:1.5}
605
+ .logo{width:30px;height:30px;background:linear-gradient(135deg,var(--accent),var(--pink));border-radius:7px;display:inline-flex;align-items:center;justify-content:center;font-weight:700;color:#fff;margin-right:10px;vertical-align:middle}
606
+ h1{font-size:18px;display:inline-block;vertical-align:middle}
607
+ table{margin-top:24px;border-collapse:collapse;width:100%;max-width:900px;font-size:13px}
608
+ th,td{text-align:left;padding:8px 12px;border-bottom:1px solid var(--border)}
609
+ th{color:var(--text3);font-size:11px;text-transform:uppercase;letter-spacing:.05em}
610
+ a{color:var(--accent);text-decoration:none}
611
+ .mono{font-family:var(--mono);font-size:11.5px;color:var(--text2)}
612
+ .badge{font-size:11px;padding:2px 8px;border-radius:99px;background:var(--bg3);border:1px solid var(--border);color:var(--text2)}
613
+ .badge.open,.badge.feedback{color:var(--green);border-color:var(--green);background:var(--green-glow)}
614
+ .empty{margin-top:24px;color:var(--text3);font-size:13px}
615
+ </style>
616
+ </head>
617
+ <body>
618
+ <span class="logo">E</span><h1>Plan Canvas sessions</h1>
619
+ ${sessions.length ? `<table><thead><tr><th>Artifact</th><th>Path</th><th>Status</th></tr></thead><tbody>${rows}</tbody></table>` : '<p class="empty">No sessions yet. Ask your agent to open a plan with the plan-canvas skill.</p>'}
620
+ </body>
621
+ </html>`;
622
+ }
623
+
624
+ module.exports = {
625
+ canvasCss,
626
+ canvasClientJs,
627
+ renderCanvasHtml,
628
+ renderMarkdownArtifactHtml,
629
+ renderSessionListHtml
630
+ };