const { Plugins, Actions, log } = require('./utils/plugin'); const fs = require('fs'); const path = require('path'); const os = require('os'); const plugin = new Plugins('ai-monitor'); const HOME = os.homedir(); const POLL_INTERVAL = 1000; const IDLE_TIMEOUT_MS = 3000; // Agent configs var AGENTS = { codex: { label: 'Codex', scanDir: path.join(HOME, '.codex', 'sessions'), logoFile: 'codex-icon.svg', normalize: function(obj) { var t = obj.type || '', pt = (obj.payload || {}).type || '', 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'), logoFile: 'claude-icon.svg', 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'; } 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; } return null; } }, opencode: { label: 'opencode', scanDir: path.join(HOME, '.opencode', 'sessions'), logoFile: 'opencode-icon.svg', 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; } } }; // Load logos as data URIs var LOGOS = {}; function loadLogo(filename) { try { return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(fs.readFileSync(path.join(__dirname, '..', 'static', 'img', filename), 'utf-8')); } catch (_) { return ''; } } Object.keys(AGENTS).forEach(function(k) { LOGOS[k] = loadLogo(AGENTS[k].logoFile); }); // Shared display machinery var STATUS = { idle: { title: 'Ready', fill: '#22c55e', blink: false, breathe: false }, prompt: { title: 'Prompt', fill: '#3b82f6', blink: false, breathe: false }, thinking: { title: 'Thinking', fill: '#f59e0b', blink: false, breathe: true }, working: { title: 'Working', fill: '#f59e0b', blink: true, breathe: false }, typing: { title: 'Typing', fill: '#f59e0b', blink: false, breathe: true }, done: { title: 'Done', fill: '#22c55e', blink: true, breathe: false }, error: { title: 'Error', fill: '#ef4444', blink: false, breathe: false }, }; var PRIORITY = ['error','working','typing','thinking','prompt','done','idle']; var states = {}; // per-context state: { agent, currentState, lastActivity, offsets, timer, logoUri } function makeSvg(fill, label, blink, breathe) { var anim = ''; if (blink) anim = ''; else if (breathe) anim = ''; return '' + anim + '' + label + ''; } function setDisplay(ctx, state) { var d = STATUS[state] || STATUS['idle']; var ss = states[ctx]; plugin.setTitle(ctx, d.title); if (state === 'idle' && ss && ss.logoUri) plugin.setImage(ctx, ss.logoUri); else plugin.setImage(ctx, 'data:image/svg+xml;charset=utf-8,' + makeSvg(d.fill, d.title, d.blink, d.breathe)); } function setState(ctx, state) { var ss = states[ctx]; if (!ss) return; if (state === ss.currentState) return; if (state === 'done') { setDisplay(ctx, 'done'); ss.currentState = 'done'; setTimeout(function() { if (states[ctx] && states[ctx].currentState === 'done') { states[ctx].currentState = 'idle'; setDisplay(ctx, 'idle'); } }, 800); } else { ss.currentState = state; setDisplay(ctx, state); } } function findFiles(rootDir, suffix) { var results = []; if (!fs.existsSync(rootDir)) return results; (function walk(dir, depth) { if (depth > 16) 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); } })(rootDir, 0); return results; } function tailFile(filePath, ctx, agent) { var ss = states[ctx]; if (!ss) return false; var detected = false; try { var stat = fs.statSync(filePath); var off = ss.offsets[filePath] || 0; if (stat.size < off) { ss.offsets[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); ss.offsets[filePath] = stat.size; var lines = buf.toString('utf-8').split('\n').filter(function(l) { return l.trim(); }); var bestPri = 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 = PRIORITY.indexOf(s); if (pri >= 0 && pri < bestPri) { bestPri = pri; bestState = s; } } } catch (_) {} } if (bestState) { setState(ctx, bestState); detected = true; } } catch (_) { delete ss.offsets[filePath]; } return detected; } function poll(ctx) { var ss = states[ctx]; if (!ss || !ss.agent) return; var detected = false; var files = findFiles(ss.agent.scanDir, '.jsonl'); files.sort(function(a,b) { return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs; }); for (var i = 0; i < Math.min(files.length, 3); i++) { if (tailFile(files[i], ctx, ss.agent)) detected = true; } if (detected) ss.lastActivity = Date.now(); if (ss.currentState !== 'idle' && ss.currentState !== 'done' && Date.now() - ss.lastActivity > IDLE_TIMEOUT_MS) { setState(ctx, 'idle'); } } function makeActionFn(agentKey) { var agent = AGENTS[agentKey]; return { default: { pollInterval: POLL_INTERVAL }, _willAppear: function(data) { var ctx = data.context; var logoUri = LOGOS[agentKey] || ''; states[ctx] = { agent: agent, currentState: 'idle', lastActivity: Date.now(), offsets: {}, timer: null, logoUri: logoUri }; setDisplay(ctx, 'idle'); poll(ctx); states[ctx].timer = setInterval(function() { poll(ctx); }, POLL_INTERVAL); }, _willDisappear: function(data) { var ctx = data.context; if (states[ctx] && states[ctx].timer) { clearInterval(states[ctx].timer); } delete states[ctx]; }, keyUp: function(data) { poll(data.context); }, }; } // Register each agent as a separate action plugin.codex = new Actions(makeActionFn('codex')); plugin.claude = new Actions(makeActionFn('claude')); plugin.opencode = new Actions(makeActionFn('opencode'));