diff --git a/com.streamdock.codex.monitor.sdPlugin/manifest.json b/com.streamdock.codex.monitor.sdPlugin/manifest.json index f413bbb..95569ad 100644 --- a/com.streamdock.codex.monitor.sdPlugin/manifest.json +++ b/com.streamdock.codex.monitor.sdPlugin/manifest.json @@ -13,7 +13,7 @@ ], "Settings": { "pollInterval": 1000, - "showHealth": true + "agent": "codex" }, "Controllers": ["Keypad", "Information"], "UserTitleEnabled": true, diff --git a/com.streamdock.codex.monitor.sdPlugin/plugin/index.js b/com.streamdock.codex.monitor.sdPlugin/plugin/index.js index 88887f1..5b082c4 100644 --- a/com.streamdock.codex.monitor.sdPlugin/plugin/index.js +++ b/com.streamdock.codex.monitor.sdPlugin/plugin/index.js @@ -3,52 +3,88 @@ const fs = require('fs'); const path = require('path'); const os = require('os'); -const plugin = new Plugins('codex-monitor'); +const plugin = new Plugins('ai-monitor'); const timers = {}; const sessionOffsets = {}; -const healthOffsets = {}; - -// ─── 直接调试日志 ────────────────────────────────────── const DEBUG_LOG = path.join(__dirname, '..', 'debug.log'); -function dlog(msg) { - try { fs.appendFileSync(DEBUG_LOG, '[' + new Date().toISOString() + '] ' + msg + '\n'); } catch (_) {} -} -dlog('=== Plugin started v6 ==='); +function dlog(msg) { try { fs.appendFileSync(DEBUG_LOG, '[' + new Date().toISOString() + '] ' + msg + '\n'); } catch (_) {} } +dlog('=== AI Monitor v7 multi-agent ==='); -// ─── 路径配置 ─────────────────────────────────────────── const HOME = os.homedir(); -const HEALTH_MARKER = 'app_server_connection.state_changed'; const DEFAULT_POLL = 1000; const DONE_CUE_MS = 800; const COOLDOWN_MS = 300; const IDLE_TIMEOUT_MS = 3000; -// ─── 简单递归文件查找 ────────────────────────────────── -function findFiles(rootDir, suffix, maxDepth) { - var results = []; - maxDepth = maxDepth || 16; - if (!fs.existsSync(rootDir)) return results; - (function walk(dir, depth) { - if (depth > maxDepth) return; - var names; - try { names = fs.readdirSync(dir); } catch (_) { return; } - for (var i = 0; i < names.length; i++) { - var name = names[i]; - var full = path.join(dir, name); - var st; - try { st = fs.statSync(full); } catch (_) { continue; } - if (st.isDirectory()) walk(full, depth + 1); - else if (st.isFile() && name.endsWith(suffix)) results.push(full); +var AGENTS = { + codex: { + label: 'Codex', + scanDir: path.join(HOME, '.codex', 'sessions'), + fileSuffix: '.jsonl', + normalize: function(obj) { + var t = obj.type || ''; + var pt = (obj.payload || {}).type || ''; + var it = ((obj.payload || {}).item || {}).type || ''; + if (t === 'event_msg') { + if (pt === 'task_started') return 'thinking'; + if (pt === 'task_complete') return 'done'; + if (pt === 'turn_aborted') return 'error'; + if (pt === 'agent_message') return 'typing'; + if (pt === 'user_message') return 'prompt'; + if (pt === 'web_search_end') return 'working'; + if (pt === 'agent_reasoning') return 'thinking'; + return null; + } + if (t === 'response_item') { + var nt = pt || it; + if (nt === 'function_call' || nt === 'custom_tool_call' || nt === 'function_call_output' || nt === 'custom_tool_call_output') return 'working'; + if (nt === 'web_search_call') return 'working'; + if (nt === 'reasoning') return 'thinking'; + if (nt === 'message') return 'typing'; + return null; + } + return null; + } + }, + claude: { + label: 'Claude', + scanDir: path.join(HOME, '.claude', 'projects'), + fileSuffix: '.jsonl', + normalize: function(obj) { + var t = obj.type || ''; + if (t === 'user') return 'prompt'; + if (t === 'assistant') { + var content = (obj.message || {}).content || []; + for (var i = 0; i < content.length; i++) { + var ct = content[i].type || ''; + if (ct === 'tool_use') return 'working'; + if (ct === 'thinking') return 'thinking'; } - })(rootDir, 0); - return results; -} - -// ─── 状态映射 ─────────────────────────────────────────── -var STATE = { - IDLE: 'idle', PROMPT: 'prompt', THINK: 'thinking', WORK: 'working', - TYPING: 'typing', DONE: 'done', ERROR: 'error', WAITING: 'waiting', - RECONNECTING: 'reconnecting', RECOVERED: 'recovered' + for (var j = 0; j < content.length; j++) { + if ((content[j].type || '') === 'text') return 'typing'; + } + return 'typing'; + } + if (t === 'system') { if (obj.subtype === 'turn_duration') return 'done'; return null; } + if (t === 'error' || t === 'api_error') return 'error'; + return null; + } + }, + opencode: { + label: 'opencode', + scanDir: path.join(HOME, '.opencode', 'sessions'), + fileSuffix: '.jsonl', + normalize: function(obj) { + var t = obj.type || ''; + if (t === 'user') return 'prompt'; + if (t === 'assistant') return 'typing'; + if (t === 'tool' || t === 'tool_call') return 'working'; + if (t === 'thinking' || t === 'reasoning') return 'thinking'; + if (t === 'done' || t === 'complete') return 'done'; + if (t === 'error') return 'error'; + return null; + } + } }; var STATUS_DISPLAY = {}; @@ -63,258 +99,147 @@ STATUS_DISPLAY['waiting'] = { title: 'Waiting', fill: '#ef4444', blink: true, br STATUS_DISPLAY['reconnecting'] = { title: 'Reconnect', fill: '#ef4444', blink: true, breathe: false }; STATUS_DISPLAY['recovered'] = { title: 'Online', fill: '#22c55e', blink: false, breathe: false }; -// 优先级: ERROR > WORK > TYPING > THINK > PROMPT > DONE > IDLE -var STATE_PRIORITY = ['error', 'waiting', 'reconnecting', 'working', 'typing', 'thinking', 'prompt', 'done', 'idle']; +var STATE_PRIORITY = ['error','waiting','reconnecting','working','typing','thinking','prompt','done','idle']; +var currentCtx = null, currentState = 'idle', lastHwSet = 0, lastActivityTime = 0; +var animationHandle = null, currentAgent = null; function makeSvg(fill, label, blink, breathe) { - var anim = ''; - if (blink) anim = ''; - else if (breathe) anim = ''; - return '' + anim + '' + label + ''; + var anim = ''; + if (blink) anim = ''; + else if (breathe) anim = ''; + return '' + anim + '' + label + ''; } function setDisplay(context, state) { - var d = STATUS_DISPLAY[state] || STATUS_DISPLAY['idle']; - plugin.setTitle(context, d.title); - plugin.setImage(context, 'data:image/svg+xml;charset=utf-8,' + makeSvg(d.fill, d.title, d.blink, d.breathe)); + var d = STATUS_DISPLAY[state] || STATUS_DISPLAY['idle']; + plugin.setTitle(context, d.title); + plugin.setImage(context, 'data:image/svg+xml;charset=utf-8,' + makeSvg(d.fill, d.title, d.blink, d.breathe)); } -// ─── 状态管理 ────────────────────────────────────────── -var currentCtx = null, currentState = 'idle', lastHwSet = 0, lastActivityTime = 0; -var animationHandle = null; - function animateTick() { - if (!currentCtx) return; - var d = STATUS_DISPLAY[currentState] || STATUS_DISPLAY['idle']; - if (d.blink || d.breathe) { - var now = Date.now(); - var period = d.blink ? 800 : 2000; - var phase = (now % period) / period; - var alpha = d.blink ? (phase < 0.5 ? 1 : 0.2) : 0.4 + 0.6 * Math.sin(phase * Math.PI); - var r = parseInt(d.fill.slice(1,3), 16), g = parseInt(d.fill.slice(3,5), 16), b = parseInt(d.fill.slice(5,7), 16); - var color = 'rgb(' + Math.round(16+(r-16)*alpha) + ',' + Math.round(16+(g-16)*alpha) + ',' + Math.round(16+(b-16)*alpha) + ')'; - var svg = '' + d.title + ''; - plugin.setImage(currentCtx, 'data:image/svg+xml;charset=utf-8,' + svg); - } + if (!currentCtx) return; + var d = STATUS_DISPLAY[currentState] || STATUS_DISPLAY['idle']; + if (d.blink || d.breathe) { + var now = Date.now(); var period = d.blink ? 800 : 2000; var phase = (now % period) / period; + var alpha = d.blink ? (phase < 0.5 ? 1 : 0.2) : 0.4 + 0.6 * Math.sin(phase * Math.PI); + var _r = parseInt(d.fill.slice(1,3),16), _g = parseInt(d.fill.slice(3,5),16), _b = parseInt(d.fill.slice(5,7),16); + var color = 'rgb(' + Math.round(16+(_r-16)*alpha) + ',' + Math.round(16+(_g-16)*alpha) + ',' + Math.round(16+(_b-16)*alpha) + ')'; + var svg = '' + d.title + ''; + plugin.setImage(currentCtx, 'data:image/svg+xml;charset=utf-8,' + svg); + } } function setState(context, state) { - if (state === currentState && context === currentCtx && Date.now() - lastHwSet < COOLDOWN_MS) return; - if (state === 'done') { - setDisplay(context, 'done'); - currentState = 'done'; - setTimeout(function () { - if (currentState === 'done') { currentState = 'idle'; setDisplay(context, 'idle'); } - }, DONE_CUE_MS); - } else if (state === 'recovered') { - currentState = 'idle'; setDisplay(context, 'idle'); - } else { - currentState = state; setDisplay(context, state); + if (state === currentState && context === currentCtx && Date.now() - lastHwSet < COOLDOWN_MS) return; + if (state === 'done') { + setDisplay(context, 'done'); currentState = 'done'; + setTimeout(function() { if (currentState === 'done') { currentState = 'idle'; setDisplay(context, 'idle'); } }, DONE_CUE_MS); + } else if (state === 'recovered') { currentState = 'idle'; setDisplay(context, 'idle'); } + else { currentState = state; setDisplay(context, state); } + lastHwSet = Date.now(); dlog('State: ' + state + ' [' + (currentAgent ? currentAgent.label : '?') + ']'); +} + +function findFiles(rootDir, suffix, maxDepth) { + var results = []; maxDepth = maxDepth || 16; + if (!fs.existsSync(rootDir)) return results; + (function walk(dir, depth) { + if (depth > maxDepth) return; + var names; try { names = fs.readdirSync(dir); } catch (_) { return; } + for (var i = 0; i < names.length; i++) { + var full = path.join(dir, names[i]); + var st; try { st = fs.statSync(full); } catch (_) { continue; } + if (st.isDirectory()) walk(full, depth + 1); + else if (st.isFile() && names[i].endsWith(suffix)) results.push(full); } - lastHwSet = Date.now(); - dlog('State: ' + state); + })(rootDir, 0); + return results; } -// ─── Session JSONL 解析 ──────────────────────────────── -function normalizeEvent(obj) { - var type = obj.type || ''; - var pt = (obj.payload || {}).type || ''; - var it = ((obj.payload || {}).item || {}).type || ''; - if (type === 'event_msg') { - if (pt === 'task_started') return 'thinking'; - if (pt === 'task_complete') return 'done'; - if (pt === 'turn_aborted') return 'error'; - if (pt === 'agent_message') return 'typing'; - if (pt === 'user_message') return 'prompt'; - if (pt === 'web_search_end') return 'working'; - if (pt === 'agent_reasoning') return 'thinking'; - return null; +function latestFiles(rootDir, suffix, limit) { + try { + var files = findFiles(rootDir, suffix); + files.sort(function(a,b) { return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs; }); + return files.slice(0, limit || 3); + } catch (_) { return []; } +} + +function tailFile(filePath, agent, context) { + var detected = false; + try { + var stat = fs.statSync(filePath); + var off = sessionOffsets[filePath] || 0; + if (stat.size < off) { sessionOffsets[filePath] = 0; return false; } + if (stat.size === off) return false; + var fd = fs.openSync(filePath, 'r'); + var grow = Math.min(stat.size - off, 65536); + var buf = Buffer.alloc(grow); + fs.readSync(fd, buf, 0, grow, off); + fs.closeSync(fd); + sessionOffsets[filePath] = stat.size; + var lines = buf.toString('utf-8').split('\n').filter(function(l) { return l.trim(); }); + var bestPri = STATE_PRIORITY.length, bestState = null; + for (var i = 0; i < lines.length; i++) { + try { + var obj = JSON.parse(lines[i]); + var s = agent.normalize(obj); + if (s) { var pri = STATE_PRIORITY.indexOf(s); if (pri >= 0 && pri < bestPri) { bestPri = pri; bestState = s; } } + } catch (_) {} } - if (type === 'response_item') { - var nt = pt || it; - if (nt === 'function_call' || nt === 'custom_tool_call' || nt === 'function_call_output' || nt === 'custom_tool_call_output') return 'working'; - if (nt === 'web_search_call') return 'working'; - if (nt === 'reasoning') return 'thinking'; - if (nt === 'message') return 'typing'; - return null; - } - return null; + if (bestState) { dlog('Detected: ' + bestState + ' from ' + path.basename(filePath)); setState(context, bestState); detected = true; } + } catch (_) { delete sessionOffsets[filePath]; } + return detected; } -// ─── Session tails ───────────────────────────────────── -function latestSessionFiles() { - try { - var dir = path.join(HOME, '.codex', 'sessions'); - var files = findFiles(dir, '.jsonl'); - files.sort(function (a, b) { return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs; }); - return files.slice(0, 3); - } catch (_) { return []; } -} - -function tailSession(filePath, context) { - var detected = false; - try { - var stat = fs.statSync(filePath); - var off = sessionOffsets[filePath] || 0; - if (stat.size < off) { sessionOffsets[filePath] = 0; return false; } - if (stat.size === off) return false; - var fd = fs.openSync(filePath, 'r'); - var grow = Math.min(stat.size - off, 65536); - var buf = Buffer.alloc(grow); - fs.readSync(fd, buf, 0, grow, off); - fs.closeSync(fd); - sessionOffsets[filePath] = stat.size; - var lines = buf.toString('utf-8').split('\n').filter(function (l) { return l.trim(); }); - var bestPri = STATE_PRIORITY.length, bestState = null; - for (var i = 0; i < lines.length; i++) { - try { - var obj = JSON.parse(lines[i]); - var s = normalizeEvent(obj); - if (s) { - var pri = STATE_PRIORITY.indexOf(s); - if (pri >= 0 && pri < bestPri) { bestPri = pri; bestState = s; } - } - } catch (_) {} - } - if (bestState) { - dlog('Detected: ' + bestState + ' from ' + path.basename(filePath)); - setState(context, bestState); - detected = true; - } - } catch (_) { delete sessionOffsets[filePath]; } - return detected; -} - -// ─── Health monitoring ───────────────────────────────── -var healthState = { reconnecting: false }; - -function parseHealthLine(line) { - if (line.indexOf(HEALTH_MARKER) === -1) return null; - var fieldRe = /([A-Za-z][A-Za-z0-9_]*)=("[^"]*"|\S+)/g; - var fields = {}, m; - while ((m = fieldRe.exec(line)) !== null) { - var val = m[2]; - if (val.charAt(0) === '"' && val.charAt(val.length - 1) === '"') val = val.slice(1, -1); - fields[m[1]] = val; - } - var next = fields.next || ''; - var prev = fields.previous || ''; - var attempt = parseInt(fields.reconnectAttempt || '0', 10); - var scheduled = (fields.reconnectTimerScheduled || '').toLowerCase() === 'true'; - var err = fields.connectionError || ''; - var cause = fields.cause || ''; - if (next === 'connected' && healthState.reconnecting) { healthState.reconnecting = false; return 'recovered'; } - if ((next === 'connecting' || next === 'disconnected') && (attempt > 0 || scheduled || (err && err.toLowerCase() !== 'null') || (prev === 'connected' && cause !== 'stop_process' && cause !== 'shutdown'))) { - healthState.reconnecting = true; return 'reconnecting'; - } - return null; -} - -function latestHealthLogs() { - try { - var dir = process.platform === 'win32' ? path.join(HOME, 'AppData', 'Local', 'Codex', 'Logs') : path.join(HOME, 'Library', 'Logs', 'com.openai.codex'); - var files = findFiles(dir, '.log'); - files.sort(function (a, b) { return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs; }); - return files.slice(0, 3); - } catch (_) { return []; } -} - -function tailHealth(filePath, context) { - try { - var stat = fs.statSync(filePath); - var off = healthOffsets[filePath] || 0; - if (stat.size < off) { healthOffsets[filePath] = 0; return; } - if (stat.size === off) return; - var fd = fs.openSync(filePath, 'r'); - var grow = Math.min(stat.size - off, 65536); - var buf = Buffer.alloc(grow); - fs.readSync(fd, buf, 0, grow, off); - fs.closeSync(fd); - healthOffsets[filePath] = stat.size; - var lines = buf.toString('utf-8').split('\n').filter(function (l) { return l.trim(); }); - var lastHealth = null; - for (var i = 0; i < lines.length; i++) { - var s = parseHealthLine(lines[i]); - if (s) lastHealth = s; - } - if (lastHealth) { dlog('Health: ' + lastHealth); setState(context, lastHealth); } - } catch (_) { delete healthOffsets[filePath]; } -} - -// ─── 主轮询 ──────────────────────────────────────────── var pollCount = 0; -function poll(context, interval, showHealth) { - pollCount++; - if (pollCount % 10 === 1) dlog('Poll #' + pollCount); - - var detected = false; - var files = latestSessionFiles(); - for (var i = 0; i < files.length; i++) { - if (tailSession(files[i], context)) detected = true; - } - - if (detected) lastActivityTime = Date.now(); - - // Idle timeout: return to IDLE if nothing happened for IDLE_TIMEOUT_MS - if (currentState !== 'idle' && currentState !== 'done' && Date.now() - lastActivityTime > IDLE_TIMEOUT_MS) { - dlog('Idle timeout - returning to IDLE'); - setState(context, 'idle'); - } - - if (showHealth) { - var hfiles = latestHealthLogs(); - for (var j = 0; j < hfiles.length; j++) tailHealth(hfiles[j], context); - } +function poll(context, interval, agent) { + pollCount++; + if (pollCount % 10 === 1) dlog('Poll #' + pollCount + ' [' + (agent ? agent.label : '?') + ']'); + if (!agent) return; + var detected = false; + var files = latestFiles(agent.scanDir, agent.fileSuffix); + for (var i = 0; i < files.length; i++) { if (tailFile(files[i], agent, context)) detected = true; } + if (detected) lastActivityTime = Date.now(); + if (currentState !== 'idle' && currentState !== 'done' && Date.now() - lastActivityTime > IDLE_TIMEOUT_MS) { + dlog('Idle timeout - returning to IDLE'); setState(context, 'idle'); + } } -// ─── Action 定义 ─────────────────────────────────────── plugin.monitor = new Actions({ - default: { pollInterval: DEFAULT_POLL, showHealth: true }, - - _willAppear: function (data) { - var ctx = data.context; - dlog('WillAppear: ' + ctx); - currentCtx = ctx; - currentState = 'idle'; - lastHwSet = 0; - lastActivityTime = Date.now(); - setDisplay(ctx, 'idle'); - - var s = plugin.monitor.data[ctx]; - var interval = s.pollInterval || DEFAULT_POLL; - var showHealth = s.showHealth !== false; - poll(ctx, interval, showHealth); - timers[ctx] = setInterval(function () { poll(ctx, interval, showHealth); }, interval); - if (!animationHandle) animationHandle = setInterval(animateTick, 200); - }, - - _willDisappear: function (data) { - var ctx = data.context; - dlog('WillDisappear: ' + ctx); - if (timers[ctx]) { clearInterval(timers[ctx]); delete timers[ctx]; } - if (currentCtx === ctx) { currentCtx = null; currentState = 'idle'; } - if (Object.keys(timers).length === 0 && animationHandle) { clearInterval(animationHandle); animationHandle = null; } - }, - - _didReceiveSettings: function (data) { - var ctx = data.context; - if (timers[ctx]) clearInterval(timers[ctx]); - var s = plugin.monitor.data[ctx]; - var interval = s.pollInterval || DEFAULT_POLL; - var showHealth = s.showHealth !== false; - poll(ctx, interval, showHealth); - timers[ctx] = setInterval(function () { poll(ctx, interval, showHealth); }, interval); - }, - - keyUp: function (data) { - var s = plugin.monitor.data[data.context]; - poll(data.context, s.pollInterval || DEFAULT_POLL, s.showHealth !== false); - }, - - sendToPlugin: function (data) { - if (data.payload && data.payload.command === 'refresh') { - var s = plugin.monitor.data[data.context]; - poll(data.context, s.pollInterval || DEFAULT_POLL, s.showHealth !== false); - } + default: { pollInterval: DEFAULT_POLL, agent: 'codex' }, + _willAppear: function(data) { + var ctx = data.context, s = plugin.monitor.data[ctx], agentKey = s.agent || 'codex', agent = AGENTS[agentKey]; + if (!agent) { agent = AGENTS['codex']; dlog('Unknown agent: ' + agentKey + ', falling back to codex'); } + currentAgent = agent; dlog('WillAppear: ' + ctx + ' agent=' + agentKey); + currentCtx = ctx; currentState = 'idle'; lastHwSet = 0; lastActivityTime = Date.now(); + setDisplay(ctx, 'idle'); + var interval = s.pollInterval || DEFAULT_POLL; + poll(ctx, interval, agent); + timers[ctx] = setInterval(function() { poll(ctx, interval, agent); }, interval); + if (!animationHandle) animationHandle = setInterval(animateTick, 200); + }, + _willDisappear: function(data) { + var ctx = data.context; + if (timers[ctx]) { clearInterval(timers[ctx]); delete timers[ctx]; } + if (currentCtx === ctx) { currentCtx = null; currentAgent = null; currentState = 'idle'; } + if (Object.keys(timers).length === 0 && animationHandle) { clearInterval(animationHandle); animationHandle = null; } + }, + _didReceiveSettings: function(data) { + var ctx = data.context; + if (timers[ctx]) clearInterval(timers[ctx]); + var s = plugin.monitor.data[ctx], agentKey = s.agent || 'codex', agent = AGENTS[agentKey]; + if (!agent) agent = AGENTS['codex']; currentAgent = agent; + var interval = s.pollInterval || DEFAULT_POLL; + poll(ctx, interval, agent); + timers[ctx] = setInterval(function() { poll(ctx, interval, agent); }, interval); + }, + keyUp: function(data) { + var s = plugin.monitor.data[data.context], agent = AGENTS[s.agent || 'codex'] || AGENTS['codex']; + poll(data.context, s.pollInterval || DEFAULT_POLL, agent); + }, + sendToPlugin: function(data) { + if (data.payload && data.payload.command === 'refresh') { + var s = plugin.monitor.data[data.context], agent = AGENTS[s.agent || 'codex'] || AGENTS['codex']; + poll(data.context, s.pollInterval || DEFAULT_POLL, agent); } -}); + } +}); \ No newline at end of file diff --git a/com.streamdock.codex.monitor.sdPlugin/propertyInspector/codex-monitor/index.html b/com.streamdock.codex.monitor.sdPlugin/propertyInspector/codex-monitor/index.html index d5a395e..3e59c1d 100644 --- a/com.streamdock.codex.monitor.sdPlugin/propertyInspector/codex-monitor/index.html +++ b/com.streamdock.codex.monitor.sdPlugin/propertyInspector/codex-monitor/index.html @@ -105,6 +105,14 @@ +
+ + +
diff --git a/com.streamdock.codex.monitor.sdPlugin/propertyInspector/codex-monitor/index.js b/com.streamdock.codex.monitor.sdPlugin/propertyInspector/codex-monitor/index.js index dbf5bf8..7d602c2 100644 --- a/com.streamdock.codex.monitor.sdPlugin/propertyInspector/codex-monitor/index.js +++ b/com.streamdock.codex.monitor.sdPlugin/propertyInspector/codex-monitor/index.js @@ -1,6 +1,7 @@ (function () { 'use strict'; + const agentSelect = document.getElementById('agentSelect'); const pollSlider = document.getElementById('pollInterval'); const pollVal = document.getElementById('pollVal'); const showHealthToggle = document.getElementById('showHealth'); @@ -11,6 +12,7 @@ $settings = $settings || {}; function applySettings(settings) { + if (settings.agent) { agentSelect.value = settings.agent; } if (settings.pollInterval !== undefined) { pollSlider.value = settings.pollInterval; pollVal.textContent = settings.pollInterval + 'ms'; @@ -20,6 +22,10 @@ } } + agentSelect.addEventListener('change', function () { + $settings.agent = this.value; + }); + pollSlider.addEventListener('input', function () { const val = parseInt(this.value, 10); pollVal.textContent = val + 'ms';