|
10 | 10 | configured: true, |
11 | 11 | }; |
12 | 12 |
|
| 13 | + // ── Event delegation (fixes onclick CSP block) ─────────────────────────── |
| 14 | + document.addEventListener('click', function (e) { |
| 15 | + const el = e.target.closest('[data-action]'); |
| 16 | + if (!el) return; |
| 17 | + const action = el.dataset.action; |
| 18 | + switch (action) { |
| 19 | + case 'switchTab': switchTab(el.dataset.tab); break; |
| 20 | + case 'sendChat': sendChat(); break; |
| 21 | + case 'sendAsRun': sendAsRun(); break; |
| 22 | + case 'startRun': startRun(); break; |
| 23 | + case 'selectRun': selectRun(el.dataset.id); break; |
| 24 | + case 'cancelRun': cancelRun(el.dataset.id); break; |
| 25 | + case 'applyFiles': applyFiles(el.dataset.id); break; |
| 26 | + case 'openBrowser': openInBrowser(el.dataset.id); break; |
| 27 | + case 'configure': configure(); break; |
| 28 | + case 'copyCode': copyCode(parseInt(el.dataset.idx)); break; |
| 29 | + case 'insertCode': insertCode(parseInt(el.dataset.idx)); break; |
| 30 | + } |
| 31 | + }); |
| 32 | + |
| 33 | + // ── Keyboard events ─────────────────────────────────────────────────────── |
| 34 | + document.addEventListener('keydown', function (e) { |
| 35 | + const target = e.target; |
| 36 | + if (target.id === 'chatInput') { |
| 37 | + if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChat(); } |
| 38 | + } |
| 39 | + if (target.id === 'objective') { |
| 40 | + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); startRun(); } |
| 41 | + } |
| 42 | + }); |
| 43 | + |
| 44 | + // ── Messages from extension host ───────────────────────────────────────── |
13 | 45 | window.addEventListener('message', ({ data }) => { |
14 | 46 | switch (data.type) { |
15 | 47 | case 'init': |
|
39 | 71 | break; |
40 | 72 | case 'triggerNewRun': |
41 | 73 | switchTab('runs'); |
42 | | - setTimeout(() => { const el = document.getElementById('objective'); if (el) { if (data.selection) el.value = data.selection; el.focus(); } }, 100); |
| 74 | + setTimeout(function () { |
| 75 | + const el = document.getElementById('objective'); |
| 76 | + if (el) { if (data.selection) el.value = data.selection; el.focus(); } |
| 77 | + }, 100); |
43 | 78 | break; |
44 | 79 | case 'runStarting': |
45 | 80 | state.streaming = [{ cls: 'dim', text: '\u27F3 Starting run...' }]; |
|
49 | 84 | case 'runStarted': |
50 | 85 | state.activeRunId = data.runId; |
51 | 86 | state.selectedRunId = data.runId; |
52 | | - state.streaming = [{ cls: 'ok', text: '\u2713 Run started: ' + data.runId.slice(0,8) + '...' }]; |
| 87 | + state.streaming = [{ cls: 'ok', text: '\u2713 Run started: ' + data.runId.slice(0, 8) + '...' }]; |
53 | 88 | renderRuns(); |
54 | 89 | break; |
55 | 90 | case 'runError': |
|
75 | 110 | } |
76 | 111 | }); |
77 | 112 |
|
| 113 | + // ── Tab switching ───────────────────────────────────────────────────────── |
78 | 114 | function switchTab(tab) { |
79 | 115 | state.tab = tab; |
80 | 116 | document.getElementById('chat-panel').classList.toggle('hidden', tab !== 'chat'); |
81 | 117 | document.getElementById('runs-panel').classList.toggle('hidden', tab !== 'runs'); |
82 | 118 | document.getElementById('tab-chat').classList.toggle('active', tab === 'chat'); |
83 | 119 | document.getElementById('tab-runs').classList.toggle('active', tab === 'runs'); |
84 | 120 | } |
85 | | - window.switchTab = switchTab; |
86 | 121 |
|
| 122 | + // ── Chat ───────────────────────────────────────────────────────────────── |
87 | 123 | function sendChat() { |
88 | 124 | const input = document.getElementById('chatInput'); |
| 125 | + if (!input) return; |
89 | 126 | const msg = input.value.trim(); |
90 | 127 | if (!msg || state.thinking) return; |
91 | 128 | state.messages.push({ role: 'user', text: msg }); |
92 | 129 | renderMessages(); |
93 | 130 | input.value = ''; |
94 | 131 | vscode.postMessage({ type: 'chat', message: msg, context: state.chatContext || '' }); |
95 | | - state.chatContext = null; |
96 | 132 | } |
97 | | - window.sendChat = sendChat; |
98 | 133 |
|
99 | 134 | function sendAsRun() { |
100 | 135 | const input = document.getElementById('chatInput'); |
| 136 | + if (!input) return; |
101 | 137 | const msg = input.value.trim(); |
102 | 138 | if (!msg) return; |
103 | 139 | input.value = ''; |
104 | 140 | switchTab('runs'); |
105 | | - setTimeout(() => { |
| 141 | + setTimeout(function () { |
106 | 142 | const obj = document.getElementById('objective'); |
107 | 143 | if (obj) { obj.value = msg; startRun(); } |
108 | 144 | }, 100); |
109 | 145 | } |
110 | | - window.sendAsRun = sendAsRun; |
111 | 146 |
|
112 | 147 | function renderMessages() { |
113 | 148 | const el = document.getElementById('messages'); |
|
116 | 151 | el.innerHTML = '<div class="empty">Ask anything about your code...<br><span style="font-size:11px;opacity:0.6">Enter to send \u2022 Shift+Enter for newline<br>\u26A1 "Run as Agent" to start a multi-agent task</span></div>'; |
117 | 152 | return; |
118 | 153 | } |
119 | | - let html = state.messages.map((m, i) => { |
| 154 | + let html = state.messages.map(function (m, i) { |
120 | 155 | if (m.role === 'user') { |
121 | 156 | return '<div class="msg user"><div class="bubble user-bubble">' + escHtml(m.text) + '</div></div>'; |
122 | 157 | } |
123 | 158 | let h = '<div class="msg assistant"><div class="bubble asst-bubble">' + escHtml(m.text) + '</div>'; |
124 | 159 | if (m.hasCode && m.code) { |
125 | | - h += '<div class="code-block"><div class="code-header"><span class="code-lang">' + escHtml(m.lang || 'code') + '</span>'; |
126 | | - h += '<div class="code-actions"><button class="cbtn" onclick="copyCode(' + i + ')">Copy</button><button class="cbtn" onclick="insertCode(' + i + ')">Insert</button></div></div>'; |
| 160 | + h += '<div class="code-block">'; |
| 161 | + h += '<div class="code-header"><span class="code-lang">' + escHtml(m.lang || 'code') + '</span>'; |
| 162 | + h += '<div class="code-actions">'; |
| 163 | + h += '<button class="cbtn" data-action="copyCode" data-idx="' + i + '">Copy</button>'; |
| 164 | + h += '<button class="cbtn" data-action="insertCode" data-idx="' + i + '">Insert</button>'; |
| 165 | + h += '</div></div>'; |
127 | 166 | h += '<code>' + escHtml(m.code) + '</code></div>'; |
128 | 167 | } |
129 | 168 | h += '</div>'; |
|
140 | 179 | const m = state.messages[i]; |
141 | 180 | if (m && m.code) vscode.postMessage({ type: 'copyToClipboard', text: m.code }); |
142 | 181 | } |
143 | | - window.copyCode = copyCode; |
144 | 182 |
|
145 | 183 | function insertCode(i) { |
146 | 184 | const m = state.messages[i]; |
147 | 185 | if (m && m.code) vscode.postMessage({ type: 'insertCode', code: m.code }); |
148 | 186 | } |
149 | | - window.insertCode = insertCode; |
150 | | - |
151 | | - document.getElementById('chatInput').addEventListener('keydown', function(e) { |
152 | | - if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChat(); } |
153 | | - }); |
154 | 187 |
|
| 188 | + // ── Runs ────────────────────────────────────────────────────────────────── |
155 | 189 | function renderRuns() { |
156 | 190 | const el = document.getElementById('runs-content'); |
157 | 191 | if (!el) return; |
158 | 192 | updatePlanBadge(); |
159 | 193 | if (!state.configured) { |
160 | | - el.innerHTML = '<div class="setup"><p>Connect your CesaFlow account to start running AI agents.</p><button class="run-btn" onclick="configure()">\u2699 Configure API Key</button></div>'; |
| 194 | + el.innerHTML = '<div class="setup"><p>Connect your CesaFlow account to start running AI agents.</p><button class="run-btn" data-action="configure">\u2699 Configure API Key</button></div>'; |
161 | 195 | return; |
162 | 196 | } |
163 | 197 | let html = '<div class="section-title">New Run</div><div class="run-form">'; |
164 | | - html += '<textarea id="objective" class="run-input" placeholder="Describe your coding task..." rows="3" onkeydown="handleRunKeydown(event)"></textarea>'; |
165 | | - html += '<button class="run-btn" onclick="startRun()">\u25B6 Start Run</button></div>'; |
| 198 | + html += '<textarea id="objective" class="run-input" placeholder="Describe your coding task..." rows="3"></textarea>'; |
| 199 | + html += '<button class="run-btn" data-action="startRun">\u25B6 Start Run</button></div>'; |
166 | 200 | if (state.streaming.length > 0) { |
167 | | - html += '<hr class="divider"><div class="section-title">Output</div><div class="stream-box" id="streamBox">'; |
168 | | - state.streaming.slice(-80).forEach(function(l) { html += '<div class="stream-line ' + l.cls + '">' + escHtml(l.text) + '</div>'; }); |
| 201 | + html += '<hr class="divider"><div class="section-title">Output</div>'; |
| 202 | + html += '<div class="stream-box" id="streamBox">'; |
| 203 | + state.streaming.slice(-80).forEach(function (l) { |
| 204 | + html += '<div class="stream-line ' + l.cls + '">' + escHtml(l.text) + '</div>'; |
| 205 | + }); |
169 | 206 | html += '</div>'; |
170 | 207 | } |
171 | | - if (state.selectedRunId && state.runDetail) { html += '<hr class="divider">' + renderDetailHtml(); } |
| 208 | + if (state.selectedRunId && state.runDetail) { |
| 209 | + html += '<hr class="divider">' + renderDetailHtml(); |
| 210 | + } |
172 | 211 | html += '<hr class="divider"><div class="section-title">Recent Runs</div>'; |
173 | | - if (state.runs.length === 0) { html += '<div class="empty">No runs yet</div>'; } |
174 | | - else { |
175 | | - state.runs.forEach(function(run) { |
| 212 | + if (state.runs.length === 0) { |
| 213 | + html += '<div class="empty">No runs yet</div>'; |
| 214 | + } else { |
| 215 | + state.runs.forEach(function (run) { |
176 | 216 | const obj = (run.task_objective || run.objective || 'Untitled').slice(0, 52); |
177 | 217 | const status = run.status || 'pending'; |
178 | 218 | const sel = run.run_id === state.selectedRunId; |
179 | | - html += '<div class="run-item' + (sel ? ' active' : '') + '" onclick="selectRun(\'' + run.run_id + '\')">'; |
180 | | - html += '<div class="run-dot ' + status + '"></div><span class="run-obj">' + escHtml(obj) + '</span><span class="run-status">' + status + '</span></div>'; |
| 219 | + html += '<div class="run-item' + (sel ? ' active' : '') + '" data-action="selectRun" data-id="' + escAttr(run.run_id) + '">'; |
| 220 | + html += '<div class="run-dot ' + status + '"></div>'; |
| 221 | + html += '<span class="run-obj">' + escHtml(obj) + '</span>'; |
| 222 | + html += '<span class="run-status">' + status + '</span>'; |
| 223 | + html += '</div>'; |
181 | 224 | }); |
182 | 225 | } |
183 | 226 | el.innerHTML = html; |
184 | 227 | scrollStream(); |
185 | 228 | } |
186 | 229 |
|
187 | 230 | function renderDetailHtml() { |
188 | | - const run = state.runDetail; if (!run) return ''; |
| 231 | + const run = state.runDetail; |
| 232 | + if (!run) return ''; |
189 | 233 | const files = (state.runWorkspace || {}).files || []; |
190 | 234 | const isRunning = run.status === 'running' || run.status === 'pending'; |
191 | 235 | let h = '<div class="section-title">Run Detail</div><div class="detail">'; |
192 | 236 | h += '<div style="font-size:13px;font-weight:600;margin-bottom:8px">' + escHtml((run.task_objective || '').slice(0, 60)) + '</div>'; |
193 | | - (run.nodes || []).forEach(function(n) { |
| 237 | + (run.nodes || []).forEach(function (n) { |
194 | 238 | const c = n.status === 'completed' ? '#4ec994' : n.status === 'failed' ? '#f48771' : n.status === 'running' ? '#7c9ef8' : '#888'; |
195 | | - h += '<div class="agent-row"><div class="agent-dot" style="background:' + c + '"></div><span>' + escHtml(n.agent_name) + '</span><span style="margin-left:auto;font-size:10px;opacity:0.6">' + n.status + '</span></div>'; |
| 239 | + h += '<div class="agent-row"><div class="agent-dot" style="background:' + c + '"></div>'; |
| 240 | + h += '<span>' + escHtml(n.agent_name) + '</span>'; |
| 241 | + h += '<span style="margin-left:auto;font-size:10px;opacity:0.6">' + n.status + '</span></div>'; |
196 | 242 | }); |
197 | 243 | if (files.length > 0) { |
198 | 244 | h += '<div style="margin-top:6px">'; |
199 | | - files.slice(0, 8).forEach(function(f) { h += '<span class="file-chip">' + escHtml(f) + '</span>'; }); |
| 245 | + files.slice(0, 8).forEach(function (f) { h += '<span class="file-chip">' + escHtml(f) + '</span>'; }); |
200 | 246 | if (files.length > 8) h += '<span class="file-chip">+' + (files.length - 8) + ' more</span>'; |
201 | 247 | h += '</div>'; |
202 | 248 | } |
203 | 249 | h += '<div class="detail-actions">'; |
204 | | - if (isRunning) { h += '<button class="btn-sm danger" onclick="cancelRun(\'' + run.run_id + '\')">\u25A0 Cancel</button>'; } |
205 | | - else { |
206 | | - if (files.length > 0) h += '<button class="btn-sm primary" onclick="applyFiles(\'' + run.run_id + '\')">\u2B07 Apply Files</button>'; |
207 | | - h += '<button class="btn-sm" onclick="openInBrowser(\'' + run.run_id + '\')">\u2197 Open</button>'; |
| 250 | + if (isRunning) { |
| 251 | + h += '<button class="btn-sm danger" data-action="cancelRun" data-id="' + escAttr(run.run_id) + '">\u25A0 Cancel</button>'; |
| 252 | + } else { |
| 253 | + if (files.length > 0) { |
| 254 | + h += '<button class="btn-sm primary" data-action="applyFiles" data-id="' + escAttr(run.run_id) + '">\u2B07 Apply Files</button>'; |
| 255 | + } |
| 256 | + h += '<button class="btn-sm" data-action="openBrowser" data-id="' + escAttr(run.run_id) + '">\u2197 Open</button>'; |
208 | 257 | } |
209 | 258 | h += '</div></div>'; |
210 | 259 | return h; |
211 | 260 | } |
212 | 261 |
|
| 262 | + function renderStreaming() { |
| 263 | + const b = document.getElementById('streamBox'); |
| 264 | + if (!b) { renderRuns(); return; } |
| 265 | + b.innerHTML = state.streaming.slice(-80).map(function (l) { |
| 266 | + return '<div class="stream-line ' + l.cls + '">' + escHtml(l.text) + '</div>'; |
| 267 | + }).join(''); |
| 268 | + scrollStream(); |
| 269 | + } |
| 270 | + |
213 | 271 | function handleWsEvent(ev) { |
214 | 272 | const t = ev.type; |
215 | 273 | if (t === 'token_chunk') { |
216 | 274 | const last = state.streaming[state.streaming.length - 1]; |
217 | 275 | if (last && last.streaming) { last.text += ev.chunk || ''; } |
218 | | - else { state.streaming.push({ cls: 'agent', text: '[' + (ev.agent || '?') + '] ', streaming: true }); state.streaming[state.streaming.length-1].text += ev.chunk || ''; } |
219 | | - renderStreaming(); return; |
| 276 | + else { |
| 277 | + state.streaming.push({ cls: 'agent', text: '[' + (ev.agent || '?') + '] ', streaming: true }); |
| 278 | + state.streaming[state.streaming.length - 1].text += ev.chunk || ''; |
| 279 | + } |
| 280 | + renderStreaming(); |
| 281 | + return; |
| 282 | + } |
| 283 | + if (t === 'node_completed') { |
| 284 | + const l = state.streaming[state.streaming.length - 1]; |
| 285 | + if (l && l.streaming) { l.streaming = false; state.streaming.push({ cls: 'dim', text: '' }); } |
220 | 286 | } |
221 | | - if (t === 'node_completed') { const l = state.streaming[state.streaming.length-1]; if (l && l.streaming) { l.streaming = false; state.streaming.push({ cls: 'dim', text: '' }); } } |
222 | 287 | if (t === 'file_written') state.streaming.push({ cls: 'file', text: '\uD83D\uDCC4 ' + ev.path }); |
223 | 288 | else if (t === 'node_started') state.streaming.push({ cls: 'agent', text: '\u25B6 ' + (ev.agent || '') + ' agent starting...' }); |
224 | 289 | else if (t === 'run_completed') { state.streaming.push({ cls: 'ok', text: '\uD83C\uDF89 Run completed!' }); state.activeRunId = null; } |
|
227 | 292 | renderStreaming(); |
228 | 293 | } |
229 | 294 |
|
230 | | - function renderStreaming() { const b = document.getElementById('streamBox'); if (!b) { renderRuns(); return; } b.innerHTML = state.streaming.slice(-80).map(function(l) { return '<div class="stream-line ' + l.cls + '">' + escHtml(l.text) + '</div>'; }).join(''); scrollStream(); } |
231 | | - function updatePlanBadge() { const b = document.getElementById('planBadge'); if (!b) return; b.textContent = (state.me && state.me.organization) ? state.me.organization.plan : 'free'; } |
232 | | - function startRun() { const ta = document.getElementById('objective'); if (!ta) return; const obj = ta.value.trim(); if (!obj) return; state.streaming = []; ta.value = ''; vscode.postMessage({ type: 'startRun', objective: obj }); } |
233 | | - function handleRunKeydown(e) { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); startRun(); } } |
234 | | - function selectRun(id) { state.selectedRunId = id; state.runDetail = null; state.runWorkspace = null; renderRuns(); vscode.postMessage({ type: 'selectRun', runId: id }); } |
| 295 | + // ── Actions ─────────────────────────────────────────────────────────────── |
| 296 | + function updatePlanBadge() { |
| 297 | + const b = document.getElementById('planBadge'); |
| 298 | + if (!b) return; |
| 299 | + b.textContent = (state.me && state.me.organization) ? state.me.organization.plan : 'free'; |
| 300 | + } |
| 301 | + function startRun() { |
| 302 | + const ta = document.getElementById('objective'); |
| 303 | + if (!ta) return; |
| 304 | + const obj = ta.value.trim(); |
| 305 | + if (!obj) return; |
| 306 | + state.streaming = []; |
| 307 | + ta.value = ''; |
| 308 | + vscode.postMessage({ type: 'startRun', objective: obj }); |
| 309 | + } |
| 310 | + function selectRun(id) { |
| 311 | + state.selectedRunId = id; |
| 312 | + state.runDetail = null; |
| 313 | + state.runWorkspace = null; |
| 314 | + renderRuns(); |
| 315 | + vscode.postMessage({ type: 'selectRun', runId: id }); |
| 316 | + } |
235 | 317 | function cancelRun(id) { vscode.postMessage({ type: 'cancelRun', runId: id }); } |
236 | 318 | function applyFiles(id) { vscode.postMessage({ type: 'applyFiles', runId: id }); } |
237 | | - function openInBrowser(id) { const base = state.serverUrl ? state.serverUrl.replace(':8001', ':3000') : 'http://localhost:3000'; vscode.postMessage({ type: 'openBrowser', url: base + '/dashboard/runs/' + id }); } |
| 319 | + function openInBrowser(id) { |
| 320 | + const base = state.serverUrl ? state.serverUrl.replace(':8001', ':3000') : 'http://localhost:3000'; |
| 321 | + vscode.postMessage({ type: 'openBrowser', url: base + '/dashboard/runs/' + id }); |
| 322 | + } |
238 | 323 | function configure() { vscode.postMessage({ type: 'configure' }); } |
239 | 324 | function scrollStream() { const b = document.getElementById('streamBox'); if (b) b.scrollTop = b.scrollHeight; } |
240 | | - function escHtml(s) { return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); } |
241 | | - |
242 | | - window.startRun = startRun; |
243 | | - window.handleRunKeydown = handleRunKeydown; |
244 | | - window.selectRun = selectRun; |
245 | | - window.cancelRun = cancelRun; |
246 | | - window.applyFiles = applyFiles; |
247 | | - window.openInBrowser = openInBrowser; |
248 | | - window.configure = configure; |
| 325 | + function escHtml(s) { return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); } |
| 326 | + function escAttr(s) { return String(s || '').replace(/"/g, '"').replace(/'/g, '''); } |
249 | 327 |
|
250 | 328 | vscode.postMessage({ type: 'ready' }); |
251 | 329 | }()); |
0 commit comments