Skip to content

Commit c580391

Browse files
authored
Audit cleanup: tsconfig, structure, security, UX (#13)
- tsconfig: noUnusedLocals, noUnusedParameters, noImplicitOverride, noFallthroughCasesInSwitch, forceConsistentCasingInFileNames - crypto.randomBytes for webview nonces (was Math.random) - Extract webview JS to media/{runOutput,settings,snapshot}.js plus a shared media/webview-utils.js for escapeHtml/formatDuration. Helpers are deduplicated; PANEL_JS template-string constants are gone - New src/panels/webviewHtml.ts: renderWebviewHtml + URI helpers with optional data injection (window.AgentDevice.data) - extension.ts split from 533 LOC -> 34. New wiring modules under src/wiring/: services, languageFeatures, views, runEvents, commands - Discriminated-union types for all three webviews' incoming messages - Drop two non-null assertions; replace with explicit guards - Webview state persisted via vscode.setState: RunOutputPanel keeps expanded set + last run; SnapshotInspectorPanel keeps refs + query - DeviceCatalog gains an AbortController; dispose() aborts in-flight adb/emulator/agent-device subprocesses - CliRunner.binPath now string | (() => string). ReplayRunner + DeviceCatalog take provider functions for cliPath/sessionName/sdk so settings changes apply on the next spawn (no reload required; reload prompt removed) - ElementRefCompletionProvider: drop dead "00" prefix on sortText - package.json: add onView/onCommand activation events for sidebar views and palette commands; new agentDevice.refreshSnapshot and clearSnapshot commands wired to the Snapshot Inspector view title; Cmd+Shift+Enter / Ctrl+Shift+Enter keybinding for runScript on .ad
1 parent 15ac05c commit c580391

21 files changed

Lines changed: 1465 additions & 1233 deletions

media/runOutput.js

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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+
})();

media/settings.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
(function () {
2+
const vscode = acquireVsCodeApi();
3+
const root = document.getElementById('root');
4+
const fields = (window.AgentDevice && window.AgentDevice.data && window.AgentDevice.data.fields) || [];
5+
let snapshot = [];
6+
let hasWorkspace = false;
7+
let scope = (vscode.getState() && vscode.getState().scope) || 'user';
8+
9+
window.addEventListener('message', function (e) {
10+
if (e.data && e.data.type === 'snapshot') {
11+
snapshot = e.data.fields;
12+
hasWorkspace = !!e.data.hasWorkspace;
13+
if (!hasWorkspace && scope === 'workspace') scope = 'user';
14+
render();
15+
}
16+
});
17+
18+
function render() {
19+
const scopeBar =
20+
'<div class="scope" role="tablist">' +
21+
scopeBtn('user', 'User') +
22+
scopeBtn('workspace', 'Workspace', !hasWorkspace) +
23+
'</div>';
24+
25+
const fieldHtml = fields.map(renderField).join('');
26+
const footer =
27+
'<div class="footer">' +
28+
'<button class="link-btn" id="open-native">' +
29+
'Open in Settings UI →' +
30+
'</button>' +
31+
'</div>';
32+
root.innerHTML = scopeBar + '<div class="fields">' + fieldHtml + '</div>' + footer;
33+
bind();
34+
}
35+
36+
function scopeBtn(value, label, disabled) {
37+
const selected = scope === value;
38+
return '<button class="scope-btn" data-scope="' + value + '"' +
39+
(disabled ? ' disabled' : '') +
40+
' aria-selected="' + selected + '" role="tab">' + esc(label) + '</button>';
41+
}
42+
43+
function renderField(field) {
44+
const snap = snapshot.find(function (s) { return s.key === field.key; }) || {};
45+
const scopeValue = scope === 'user' ? snap.userValue : snap.workspaceValue;
46+
const otherValue = scope === 'user' ? snap.workspaceValue : snap.userValue;
47+
const explicitlySet = scopeValue !== undefined;
48+
49+
const sourceLabel = explicitlySet
50+
? '<span class="field-source from-' + scope + '">' + scope + '</span>'
51+
: (otherValue !== undefined
52+
? '<span class="field-source">inherited from ' + (scope === 'user' ? 'workspace' : 'user') + '</span>'
53+
: '<span class="field-source">default</span>');
54+
55+
if (field.type === 'boolean') {
56+
const effective = explicitlySet
57+
? !!scopeValue
58+
: (otherValue !== undefined ? !!otherValue : !!snap.defaultValue);
59+
return '<div class="field">' +
60+
'<div class="field-label">' + esc(field.label) + '</div>' +
61+
'<div class="field-checkbox">' +
62+
'<input type="checkbox" id="f-' + esc(field.key) + '"' + (effective ? ' checked' : '') + ' />' +
63+
'<label for="f-' + esc(field.key) + '">' + esc(field.hint) + '</label>' +
64+
'</div>' +
65+
sourceLabel +
66+
'</div>';
67+
}
68+
69+
const stringValue = typeof scopeValue === 'string' ? scopeValue : '';
70+
const placeholder = (typeof otherValue === 'string' && otherValue) || field.placeholder || '';
71+
const resetBtn = explicitlySet
72+
? '<button class="action" data-reset="' + esc(field.key) + '" title="Reset this scope">↺</button>'
73+
: '';
74+
return '<div class="field">' +
75+
'<div class="field-label">' + esc(field.label) + '</div>' +
76+
'<div class="field-row">' +
77+
'<input type="text" id="f-' + esc(field.key) + '" value="' + esc(stringValue) + '" placeholder="' + esc(placeholder) + '" />' +
78+
resetBtn +
79+
'</div>' +
80+
'<div class="field-hint">' + esc(field.hint) + '</div>' +
81+
sourceLabel +
82+
'</div>';
83+
}
84+
85+
function bind() {
86+
document.querySelectorAll('.scope-btn').forEach(function (btn) {
87+
btn.addEventListener('click', function () {
88+
const next = btn.getAttribute('data-scope');
89+
if (!next || btn.disabled) return;
90+
scope = next;
91+
vscode.setState({ scope: scope });
92+
render();
93+
});
94+
});
95+
96+
fields.forEach(function (field) {
97+
const el = document.getElementById('f-' + field.key);
98+
if (!el) return;
99+
if (field.type === 'boolean') {
100+
el.addEventListener('change', function () {
101+
vscode.postMessage({
102+
type: 'set',
103+
key: field.key,
104+
value: el.checked,
105+
scope: scope,
106+
});
107+
});
108+
} else {
109+
el.addEventListener('change', function () {
110+
vscode.postMessage({
111+
type: 'set',
112+
key: field.key,
113+
value: el.value,
114+
scope: scope,
115+
});
116+
});
117+
}
118+
});
119+
120+
document.querySelectorAll('[data-reset]').forEach(function (btn) {
121+
btn.addEventListener('click', function () {
122+
const key = btn.getAttribute('data-reset');
123+
if (!key) return;
124+
vscode.postMessage({ type: 'reset-field', key: key, scope: scope });
125+
});
126+
});
127+
128+
const native = document.getElementById('open-native');
129+
if (native) native.addEventListener('click', function () { vscode.postMessage({ type: 'open-native' }); });
130+
}
131+
132+
const esc = window.AgentDevice.escapeHtml;
133+
134+
vscode.postMessage({ type: 'ready' });
135+
})();

0 commit comments

Comments
 (0)