|
| 1 | +(function () { |
| 2 | + const vscode = acquireVsCodeApi(); |
| 3 | + const root = document.getElementById('root'); |
| 4 | + const persisted = vscode.getState() || {}; |
| 5 | + let state = persisted.state || { kind: 'list', files: [] }; |
| 6 | + let elapsedTimer = null; |
| 7 | + const expanded = new Set(persisted.expanded || []); |
| 8 | + |
| 9 | + window.addEventListener('message', function (e) { |
| 10 | + if (e.data && e.data.type === 'state') { |
| 11 | + state = e.data.state; |
| 12 | + vscode.setState({ state: state, expanded: Array.from(expanded) }); |
| 13 | + render(); |
| 14 | + } |
| 15 | + }); |
| 16 | + |
| 17 | + function render() { |
| 18 | + if (elapsedTimer) { clearInterval(elapsedTimer); elapsedTimer = null; } |
| 19 | + if (state.kind === 'list') { |
| 20 | + renderList(); |
| 21 | + } else { |
| 22 | + renderRun(); |
| 23 | + } |
| 24 | + bind(); |
| 25 | + } |
| 26 | + |
| 27 | + function renderList() { |
| 28 | + const hasFiles = state.files && state.files.length > 0; |
| 29 | + const header = |
| 30 | + '<div class="list-header">' + |
| 31 | + '<span class="list-title">' + |
| 32 | + (hasFiles |
| 33 | + ? state.files.length + ' .ad ' + (state.files.length === 1 ? 'script' : 'scripts') |
| 34 | + : 'Agent Device') + |
| 35 | + '</span>' + |
| 36 | + '<button id="new-file" class="ghost" title="New from template">' + |
| 37 | + '<i class="codicon codicon-new-file"></i>New' + |
| 38 | + '</button>' + |
| 39 | + '</div>'; |
| 40 | + |
| 41 | + let body; |
| 42 | + if (!hasFiles) { |
| 43 | + body = |
| 44 | + '<div class="empty-state">' + |
| 45 | + '<i class="codicon codicon-file-code empty-icon"></i>' + |
| 46 | + '<h3>No .ad scripts yet</h3>' + |
| 47 | + '<p>Create one from a template, or start from an empty file.</p>' + |
| 48 | + '<button id="new-file-empty">' + |
| 49 | + '<i class="codicon codicon-new-file"></i>Create from template' + |
| 50 | + '</button>' + |
| 51 | + '</div>'; |
| 52 | + } else { |
| 53 | + body = '<ol class="files">' + |
| 54 | + state.files.map(function (file) { |
| 55 | + return '<li class="file" data-uri="' + esc(file.uri) + '" title="Run">' + |
| 56 | + '<i class="codicon codicon-file-code"></i>' + |
| 57 | + '<span class="file-path">' + esc(file.relativePath) + '</span>' + |
| 58 | + '<i class="codicon codicon-play file-run-hint" title="Run"></i>' + |
| 59 | + '</li>'; |
| 60 | + }).join('') + |
| 61 | + '</ol>'; |
| 62 | + } |
| 63 | + root.innerHTML = header + body; |
| 64 | + } |
| 65 | + |
| 66 | + function renderRun() { |
| 67 | + const back = '<div class="actions"><button id="back" class="ghost"><i class="codicon codicon-arrow-left"></i>Back to list</button></div>'; |
| 68 | + const summary = renderSummary(); |
| 69 | + let actions = ''; |
| 70 | + if (state.status === 'running') { |
| 71 | + actions = '<div class="actions"><button id="stop"><i class="codicon codicon-debug-stop"></i>Stop</button></div>'; |
| 72 | + } else if (state.status !== 'idle') { |
| 73 | + const buttons = ['<button id="run">Re-run</button>']; |
| 74 | + if (state.reportAvailable) { |
| 75 | + buttons.push('<button id="open-report" class="ghost"><i class="codicon codicon-file-symlink-file"></i>View report</button>'); |
| 76 | + } |
| 77 | + actions = '<div class="actions">' + buttons.join('') + '</div>'; |
| 78 | + } |
| 79 | + const list = state.steps && state.steps.length |
| 80 | + ? '<ol class="steps">' + state.steps.map(renderStep).join('') + '</ol>' |
| 81 | + : '<div class="placeholder">No steps to show.</div>'; |
| 82 | + root.innerHTML = back + summary + actions + list; |
| 83 | + |
| 84 | + if (state.status === 'running' && state.startedAt) { |
| 85 | + const startedAt = state.startedAt; |
| 86 | + elapsedTimer = setInterval(function () { |
| 87 | + const el = document.getElementById('elapsed'); |
| 88 | + if (el) el.textContent = formatDuration(Date.now() - startedAt); |
| 89 | + const running = document.querySelector('li.step.running'); |
| 90 | + if (running) { |
| 91 | + const stepStartedAtAttr = running.getAttribute('data-started-at'); |
| 92 | + if (stepStartedAtAttr) { |
| 93 | + const stepStartedAt = Number(stepStartedAtAttr); |
| 94 | + const dur = running.querySelector('.step-duration'); |
| 95 | + if (dur) dur.textContent = formatDuration(Date.now() - stepStartedAt); |
| 96 | + } |
| 97 | + } |
| 98 | + }, 100); |
| 99 | + } |
| 100 | + } |
| 101 | + |
| 102 | + function renderSummary() { |
| 103 | + const status = statusHtml(state.status); |
| 104 | + const name = state.scriptName ? '<span class="script-name" id="reveal">' + esc(state.scriptName) + '</span>' : ''; |
| 105 | + const dur = state.status === 'running' |
| 106 | + ? '<span class="duration" id="elapsed">0ms</span>' |
| 107 | + : (state.durationMs != null ? '<span class="duration">' + formatDuration(state.durationMs) + '</span>' : ''); |
| 108 | + return '<div class="summary">' + status + name + dur + '</div>'; |
| 109 | + } |
| 110 | + |
| 111 | + function statusHtml(status) { |
| 112 | + if (status === 'running') return '<span class="status running"><i class="codicon codicon-loading codicon-modifier-spin"></i>Running</span>'; |
| 113 | + if (status === 'success') return '<span class="status success"><i class="codicon codicon-pass-filled"></i>Pass</span>'; |
| 114 | + if (status === 'failure') return '<span class="status failure"><i class="codicon codicon-error"></i>Fail</span>'; |
| 115 | + if (status === 'cancelled') return '<span class="status cancelled"><i class="codicon codicon-circle-slash"></i>Cancelled</span>'; |
| 116 | + return ''; |
| 117 | + } |
| 118 | + |
| 119 | + function stepIconHtml(state) { |
| 120 | + if (state === 'pending') return '<span class="step-icon pending"><i class="codicon codicon-circle-large-outline"></i></span>'; |
| 121 | + if (state === 'running') return '<span class="step-icon running"><i class="codicon codicon-loading codicon-modifier-spin"></i></span>'; |
| 122 | + if (state === 'passed') return '<span class="step-icon passed"><i class="codicon codicon-pass-filled"></i></span>'; |
| 123 | + if (state === 'failed') return '<span class="step-icon failed"><i class="codicon codicon-error"></i></span>'; |
| 124 | + if (state === 'skipped') return '<span class="step-icon skipped"><i class="codicon codicon-circle-slash"></i></span>'; |
| 125 | + return ''; |
| 126 | + } |
| 127 | + |
| 128 | + function renderStep(step) { |
| 129 | + const isExpanded = expanded.has(step.index); |
| 130 | + const dur = step.durationMs != null ? formatDuration(step.durationMs) : ''; |
| 131 | + const startedAt = step.state === 'running' && step.startedAt ? step.startedAt : ''; |
| 132 | + const header = |
| 133 | + '<div class="step-header" data-step="' + step.index + '">' + |
| 134 | + stepIconHtml(step.state) + |
| 135 | + '<span class="step-line" data-line="' + step.lineNumber + '">L' + step.lineNumber + '</span>' + |
| 136 | + '<span class="step-text">' + esc(step.display) + '</span>' + |
| 137 | + '<span class="step-duration">' + dur + '</span>' + |
| 138 | + '</div>'; |
| 139 | + const body = isExpanded ? renderStepBody(step) : ''; |
| 140 | + return '<li class="step ' + step.state + (isExpanded ? ' expanded' : '') + '"' + |
| 141 | + (startedAt ? ' data-started-at="' + startedAt + '"' : '') + '>' + header + body + '</li>'; |
| 142 | + } |
| 143 | + |
| 144 | + function renderStepBody(step) { |
| 145 | + const parts = []; |
| 146 | + if (step.errorMessage) { |
| 147 | + parts.push(renderOutputBlock('Error', step.errorMessage, 'error')); |
| 148 | + } |
| 149 | + if (step.stderr && step.stderr.trim()) { |
| 150 | + parts.push(renderOutputBlock('stderr', step.stderr.trim(), '')); |
| 151 | + } |
| 152 | + if (step.stdout && step.stdout.trim()) { |
| 153 | + parts.push(renderOutputBlock('stdout', step.stdout.trim(), '')); |
| 154 | + } |
| 155 | + if (parts.length === 0) { |
| 156 | + const placeholder = step.state === 'running' ? 'in progress…' : 'no output'; |
| 157 | + parts.push('<div class="step-body"><div class="label-row"><span class="label">' + placeholder + '</span></div></div>'); |
| 158 | + } |
| 159 | + return parts.join(''); |
| 160 | + } |
| 161 | + |
| 162 | + function renderOutputBlock(label, text, extraClass) { |
| 163 | + return '<div class="step-body ' + extraClass + '">' + |
| 164 | + '<div class="label-row">' + |
| 165 | + '<span class="label">' + esc(label) + '</span>' + |
| 166 | + '<button class="copy-btn" type="button" title="Copy"><i class="codicon codicon-clippy"></i><span class="copy-label">Copy</span></button>' + |
| 167 | + '</div>' + |
| 168 | + '<pre>' + esc(text) + '</pre>' + |
| 169 | + '</div>'; |
| 170 | + } |
| 171 | + |
| 172 | + function bind() { |
| 173 | + const runBtn = document.getElementById('run'); |
| 174 | + if (runBtn) runBtn.addEventListener('click', function () { vscode.postMessage({ type: 'run' }); }); |
| 175 | + |
| 176 | + const stopBtn = document.getElementById('stop'); |
| 177 | + if (stopBtn) stopBtn.addEventListener('click', function () { vscode.postMessage({ type: 'cancel' }); }); |
| 178 | + |
| 179 | + const reportBtn = document.getElementById('open-report'); |
| 180 | + if (reportBtn) reportBtn.addEventListener('click', function () { vscode.postMessage({ type: 'open-report' }); }); |
| 181 | + |
| 182 | + const backBtn = document.getElementById('back'); |
| 183 | + if (backBtn) backBtn.addEventListener('click', function () { vscode.postMessage({ type: 'show-list' }); }); |
| 184 | + |
| 185 | + const newBtn = document.getElementById('new-file'); |
| 186 | + if (newBtn) newBtn.addEventListener('click', function () { vscode.postMessage({ type: 'new-file' }); }); |
| 187 | + const newBtnEmpty = document.getElementById('new-file-empty'); |
| 188 | + if (newBtnEmpty) newBtnEmpty.addEventListener('click', function () { vscode.postMessage({ type: 'new-file' }); }); |
| 189 | + |
| 190 | + const reveal = document.getElementById('reveal'); |
| 191 | + if (reveal) reveal.addEventListener('click', function () { vscode.postMessage({ type: 'reveal-script' }); }); |
| 192 | + |
| 193 | + const fileRows = document.querySelectorAll('li.file'); |
| 194 | + fileRows.forEach(function (row) { |
| 195 | + row.addEventListener('click', function () { |
| 196 | + const uri = row.getAttribute('data-uri'); |
| 197 | + if (uri) vscode.postMessage({ type: 'run-file', uri: uri }); |
| 198 | + }); |
| 199 | + }); |
| 200 | + |
| 201 | + const expandableHeaders = document.querySelectorAll('li.step.passed > .step-header, li.step.failed > .step-header, li.step.running > .step-header'); |
| 202 | + expandableHeaders.forEach(function (header) { |
| 203 | + header.addEventListener('click', function (e) { |
| 204 | + if (e.target && e.target.classList && e.target.classList.contains('step-line')) { |
| 205 | + return; |
| 206 | + } |
| 207 | + const idx = Number(header.getAttribute('data-step')); |
| 208 | + if (expanded.has(idx)) expanded.delete(idx); else expanded.add(idx); |
| 209 | + vscode.setState({ state: state, expanded: Array.from(expanded) }); |
| 210 | + render(); |
| 211 | + }); |
| 212 | + }); |
| 213 | + |
| 214 | + const lineLinks = document.querySelectorAll('.step-line'); |
| 215 | + lineLinks.forEach(function (link) { |
| 216 | + link.addEventListener('click', function (e) { |
| 217 | + e.stopPropagation(); |
| 218 | + const lineNumber = Number(link.getAttribute('data-line')); |
| 219 | + vscode.postMessage({ type: 'reveal-line', lineNumber: lineNumber }); |
| 220 | + }); |
| 221 | + }); |
| 222 | + |
| 223 | + const copyButtons = document.querySelectorAll('.copy-btn'); |
| 224 | + copyButtons.forEach(function (btn) { |
| 225 | + btn.addEventListener('click', function (e) { |
| 226 | + e.stopPropagation(); |
| 227 | + const body = btn.closest('.step-body'); |
| 228 | + const pre = body && body.querySelector('pre'); |
| 229 | + if (!pre) return; |
| 230 | + const text = pre.textContent || ''; |
| 231 | + Promise.resolve(navigator.clipboard.writeText(text)) |
| 232 | + .then(function () { flashCopied(btn, 'Copied'); }) |
| 233 | + .catch(function () { flashCopied(btn, 'Failed'); }); |
| 234 | + }); |
| 235 | + }); |
| 236 | + } |
| 237 | + |
| 238 | + function flashCopied(btn, label) { |
| 239 | + const span = btn.querySelector('.copy-label'); |
| 240 | + if (!span) return; |
| 241 | + btn.classList.add('copied'); |
| 242 | + span.textContent = label; |
| 243 | + setTimeout(function () { |
| 244 | + btn.classList.remove('copied'); |
| 245 | + span.textContent = 'Copy'; |
| 246 | + }, 1200); |
| 247 | + } |
| 248 | + |
| 249 | + const esc = window.AgentDevice.escapeHtml; |
| 250 | + const formatDuration = window.AgentDevice.formatDuration; |
| 251 | + |
| 252 | + vscode.postMessage({ type: 'ready' }); |
| 253 | +})(); |
0 commit comments