fix: use relative PNG paths for idle state, transparent SVG for active

This commit is contained in:
2026-06-18 15:50:18 +08:00
parent ef79e67f0a
commit 4811fef4d2
6 changed files with 74 additions and 34 deletions
@@ -2,13 +2,22 @@
"Actions": [ "Actions": [
{ {
"UUID": "com.streamdock.ai-monitor.codex", "UUID": "com.streamdock.ai-monitor.codex",
"Icon": "static/img/codex-icon.svg", "Icon": "static/img/codex.png",
"Name": "Codex Desktop", "Name": "Codex Desktop",
"States": [ "States": [
{ "Image": "static/img/codex-icon.svg", "TitleAlignment": "center", "FontSize": "11" } {
"Image": "static/img/codex.png",
"TitleAlignment": "center",
"FontSize": "11"
}
],
"Settings": {
"pollInterval": 1000
},
"Controllers": [
"Keypad",
"Information"
], ],
"Settings": { "pollInterval": 1000 },
"Controllers": ["Keypad", "Information"],
"UserTitleEnabled": true, "UserTitleEnabled": true,
"SupportedInMultiActions": true, "SupportedInMultiActions": true,
"Tooltip": "Codex Desktop 工作流状态监控", "Tooltip": "Codex Desktop 工作流状态监控",
@@ -16,13 +25,22 @@
}, },
{ {
"UUID": "com.streamdock.ai-monitor.claude", "UUID": "com.streamdock.ai-monitor.claude",
"Icon": "static/img/claude-icon.svg", "Icon": "static/img/claude.png",
"Name": "Claude Desktop", "Name": "Claude Desktop",
"States": [ "States": [
{ "Image": "static/img/claude-icon.svg", "TitleAlignment": "center", "FontSize": "11" } {
"Image": "static/img/claude.png",
"TitleAlignment": "center",
"FontSize": "11"
}
],
"Settings": {
"pollInterval": 1000
},
"Controllers": [
"Keypad",
"Information"
], ],
"Settings": { "pollInterval": 1000 },
"Controllers": ["Keypad", "Information"],
"UserTitleEnabled": true, "UserTitleEnabled": true,
"SupportedInMultiActions": true, "SupportedInMultiActions": true,
"Tooltip": "Claude Desktop 工作流状态监控", "Tooltip": "Claude Desktop 工作流状态监控",
@@ -30,13 +48,22 @@
}, },
{ {
"UUID": "com.streamdock.ai-monitor.opencode", "UUID": "com.streamdock.ai-monitor.opencode",
"Icon": "static/img/opencode-icon.svg", "Icon": "static/img/opencode.png",
"Name": "opencode", "Name": "opencode",
"States": [ "States": [
{ "Image": "static/img/opencode-icon.svg", "TitleAlignment": "center", "FontSize": "11" } {
"Image": "static/img/opencode.png",
"TitleAlignment": "center",
"FontSize": "11"
}
],
"Settings": {
"pollInterval": 1000
},
"Controllers": [
"Keypad",
"Information"
], ],
"Settings": { "pollInterval": 1000 },
"Controllers": ["Keypad", "Information"],
"UserTitleEnabled": true, "UserTitleEnabled": true,
"SupportedInMultiActions": true, "SupportedInMultiActions": true,
"Tooltip": "opencode 工作流状态监控", "Tooltip": "opencode 工作流状态监控",
@@ -46,18 +73,28 @@
"SDKVersion": 1, "SDKVersion": 1,
"Author": "StreamDock", "Author": "StreamDock",
"Name": "AI Monitor", "Name": "AI Monitor",
"Icon": "static/img/codex-icon.svg", "Icon": "static/img/ai.png",
"Category": "AI Monitor", "Category": "AI Monitor",
"CategoryIcon": "static/img/codex-icon.svg", "CategoryIcon": "static/img/ai.png",
"CodePathWin": "plugin/index.js", "CodePathWin": "plugin/index.js",
"CodePathMac": "plugin/index.js", "CodePathMac": "plugin/index.js",
"Description": "监控 Codex / Claude / opencode AI 工作流状态", "Description": "监控 Codex / Claude / opencode AI 工作流状态",
"Version": "2.0.0", "Version": "2.0.0",
"URL": "http://10.10.10.14:18003/v6ole/streamdock-ai-monitor", "URL": "http://10.10.10.14:18003/v6ole/streamdock-ai-monitor",
"OS": [ "OS": [
{ "Platform": "windows", "MinimumVersion": "7" }, {
{ "Platform": "mac", "MinimumVersion": "10.11" } "Platform": "windows",
"MinimumVersion": "7"
},
{
"Platform": "mac",
"MinimumVersion": "10.11"
}
], ],
"Software": { "MinimumVersion": "3.10.188.226" }, "Software": {
"Nodejs": { "Version": "20" } "MinimumVersion": "3.10.188.226"
},
"Nodejs": {
"Version": "20"
}
} }
@@ -13,7 +13,7 @@ var AGENTS = {
codex: { codex: {
label: 'Codex', label: 'Codex',
scanDir: path.join(HOME, '.codex', 'sessions'), scanDir: path.join(HOME, '.codex', 'sessions'),
logoFile: 'codex-icon.svg', logoFile: 'codex.png',
normalize: function(obj) { normalize: function(obj) {
var t = obj.type || '', pt = (obj.payload || {}).type || '', it = ((obj.payload || {}).item || {}).type || ''; var t = obj.type || '', pt = (obj.payload || {}).type || '', it = ((obj.payload || {}).item || {}).type || '';
if (t === 'event_msg') { if (t === 'event_msg') {
@@ -40,7 +40,7 @@ var AGENTS = {
claude: { claude: {
label: 'Claude', label: 'Claude',
scanDir: path.join(HOME, '.claude', 'projects'), scanDir: path.join(HOME, '.claude', 'projects'),
logoFile: 'claude-icon.svg', logoFile: 'claude.png',
normalize: function(obj) { normalize: function(obj) {
var t = obj.type || ''; var t = obj.type || '';
if (t === 'user') return 'prompt'; if (t === 'user') return 'prompt';
@@ -61,7 +61,7 @@ var AGENTS = {
opencode: { opencode: {
label: 'opencode', label: 'opencode',
scanDir: path.join(HOME, '.opencode', 'sessions'), scanDir: path.join(HOME, '.opencode', 'sessions'),
logoFile: 'opencode-icon.svg', logoFile: 'opencode.png',
normalize: function(obj) { normalize: function(obj) {
var t = obj.type || ''; var t = obj.type || '';
if (t === 'user') return 'prompt'; if (t === 'user') return 'prompt';
@@ -75,13 +75,6 @@ var AGENTS = {
} }
}; };
// 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 // Shared display machinery
var STATUS = { var STATUS = {
@@ -100,15 +93,26 @@ function makeSvg(fill, label, blink, breathe) {
var anim = ''; var anim = '';
if (blink) anim = '<animate attributeName="opacity" values="1;0.2;1" dur="0.8s" repeatCount="indefinite"/>'; if (blink) anim = '<animate attributeName="opacity" values="1;0.2;1" dur="0.8s" repeatCount="indefinite"/>';
else if (breathe) anim = '<animate attributeName="opacity" values="1;0.4;1" dur="2s" repeatCount="indefinite"/>'; else if (breathe) anim = '<animate attributeName="opacity" values="1;0.4;1" dur="2s" repeatCount="indefinite"/>';
return '<svg width="144" height="144" xmlns="http://www.w3.org/2000/svg"><rect width="144" height="144" rx="20" fill="none"/><circle cx="72" cy="58" r="32" fill="' + fill + '">' + anim + '</circle><text x="72" y="118" font-family="Arial" font-weight="bold" font-size="13" fill="white" text-anchor="middle">' + label + '</text></svg>'; return '<svg width="144" height="144" xmlns="http://www.w3.org/2000/svg"><rect width="144" height="144" rx="20" fill="#0d0d1a"/><circle cx="72" cy="58" r="32" fill="' + fill + '">' + anim + '</circle><text x="72" y="118" font-family="Arial" font-weight="bold" font-size="13" fill="white" text-anchor="middle">' + label + '</text></svg>';
} }
function setDisplay(ctx, state) { function setDisplay(ctx, state) {
var d = STATUS[state] || STATUS['idle']; var d = STATUS[state] || STATUS['idle'];
var ss = states[ctx]; var ss = states[ctx];
plugin.setTitle(ctx, d.title); if (state === 'idle' && ss && ss.agent) {
if (state === 'idle' && ss && ss.logoUri) { plugin.setTitle(ctx, ''); plugin.setImage(ctx, ss.logoUri); } // Use relative PNG path for idle
else plugin.setImage(ctx, 'data:image/svg+xml;charset=utf-8,' + makeSvg(d.fill, d.title, d.blink, d.breathe)); plugin.setTitle(ctx, '');
plugin.setImage(ctx, 'static/img/' + ss.agent.logoFile);
} else {
plugin.setTitle(ctx, d.title);
var svg = '<svg width="144" height="144" xmlns="http://www.w3.org/2000/svg"><rect width="144" height="144" rx="20" fill="none"/>';
var anim = '';
if (d.blink) anim = '<animate attributeName="opacity" values="1;0.2;1" dur="0.8s" repeatCount="indefinite"/>';
else if (d.breathe) anim = '<animate attributeName="opacity" values="1;0.4;1" dur="2s" repeatCount="indefinite"/>';
svg += '<circle cx="72" cy="58" r="32" fill="' + d.fill + '">' + anim + '</circle>';
svg += '<text x="72" y="118" font-family="Arial" font-weight="bold" font-size="13" fill="white" text-anchor="middle">' + d.title + '</text></svg>';
plugin.setImage(ctx, 'data:image/svg+xml;charset=utf-8,' + svg);
}
} }
function setState(ctx, state) { function setState(ctx, state) {
@@ -174,8 +178,7 @@ function makeActionFn(agentKey) {
default: { pollInterval: POLL_INTERVAL }, default: { pollInterval: POLL_INTERVAL },
_willAppear: function(data) { _willAppear: function(data) {
var ctx = data.context; var ctx = data.context;
var logoUri = LOGOS[agentKey] || ''; states[ctx] = { agent: agent, currentState: 'idle', lastActivity: Date.now(), offsets: {}, timer: null };
states[ctx] = { agent: agent, currentState: 'idle', lastActivity: Date.now(), offsets: {}, timer: null, logoUri: logoUri };
setDisplay(ctx, 'idle'); setDisplay(ctx, 'idle');
poll(ctx); poll(ctx);
states[ctx].timer = setInterval(function() { poll(ctx); }, POLL_INTERVAL); states[ctx].timer = setInterval(function() { poll(ctx); }, POLL_INTERVAL);
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB