opencode-avatar 0.3.4 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -194,6 +194,8 @@ The output shows:
194
194
  - Subsequent loads are instant (cached)
195
195
  - Reduce avatar size for faster generation
196
196
 
197
+ > **Warning:** If you have a lot of tools (like from an MCP), it will generate a lot of images per tool, which may impact performance.
198
+
197
199
  ## Development
198
200
 
199
201
  ### Building
package/dist/electron.js CHANGED
@@ -410,12 +410,10 @@ function getConfig() {
410
410
  const configPath = path.join(os.homedir(), ".config", "opencode", "opencode-avatar.json");
411
411
  const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
412
412
  if (!config.falKey) {
413
- console.warn("Warning: falKey not found in config file. Avatar generation will not work. Please set falKey in ~/.config/opencode/opencode-avatar.json");
414
413
  return { falKey: null, prompt: null };
415
414
  }
416
415
  return { falKey: config.falKey, prompt: config.prompt || null };
417
416
  } catch (error) {
418
- console.warn(`Warning: Failed to read config file: ${error.message}. Avatar generation will not work. Please ensure ~/.config/opencode/opencode-avatar.json exists and contains falKey.`);
419
417
  return { falKey: null, prompt: null };
420
418
  }
421
419
  }
@@ -490,42 +488,38 @@ var HTML_CONTENT = `<!DOCTYPE html>
490
488
 
491
489
  ctx.drawImage(srcImg, 0, 0);
492
490
 
493
- const topLeftPixel = ctx.getImageData(0, 0, 1, 1).data;
494
- const chromaR = topLeftPixel[0];
495
- const chromaG = topLeftPixel[1];
496
- const chromaB = topLeftPixel[2];
497
-
498
-
491
+ // Chroma keying: make background transparent
499
492
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
500
493
  const data = imageData.data;
494
+ const chromaR = data[0]; // first pixel R
495
+ const chromaG = data[1]; // first pixel G
496
+ const chromaB = data[2]; // first pixel B
501
497
  const tolerance = 30;
502
498
 
503
499
  for (let i = 0; i < data.length; i += 4) {
504
500
  const r = data[i];
505
501
  const g = data[i + 1];
506
502
  const b = data[i + 2];
507
-
508
- if (
509
- Math.abs(r - chromaR) <= tolerance &&
510
- Math.abs(g - chromaG) <= tolerance &&
511
- Math.abs(b - chromaB) <= tolerance
512
- ) {
513
- data[i + 3] = 0;
503
+ if (Math.abs(r - chromaR) <= tolerance &&
504
+ Math.abs(g - chromaG) <= tolerance &&
505
+ Math.abs(b - chromaB) <= tolerance) {
506
+ data[i + 3] = 0; // set alpha to 0
514
507
  }
515
508
  }
516
509
 
517
510
  ctx.putImageData(imageData, 0, 0);
511
+
518
512
  img.src = canvas.toDataURL('image/png');
519
513
  };
520
514
 
521
- srcImg.onerror = function(e) {
522
- console.error('Failed to load image:', e);
523
- img.src = 'avatar.svg';
524
- };
515
+ srcImg.onerror = function(e) {
516
+ console.error('Failed to load image:', e);
517
+ img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
518
+ };
525
519
  }
526
520
 
527
- ipcRenderer.on('set-avatar', (event, avatarPath) => {
528
- loadAvatar('file://' + avatarPath);
521
+ ipcRenderer.on('set-avatar', (event, avatarDataUrl) => {
522
+ loadAvatar(avatarDataUrl);
529
523
  });
530
524
 
531
525
  // Fallback: load default avatar if no IPC message received
@@ -691,15 +685,15 @@ function startAvatarServer() {
691
685
  try {
692
686
  const { avatarPath } = JSON.parse(body);
693
687
  if (mainWindow && avatarPath) {
694
- mainWindow.webContents.send("set-avatar", avatarPath);
688
+ const imageBuffer = fs.readFileSync(avatarPath);
689
+ const base64 = imageBuffer.toString("base64");
690
+ const dataUrl = `data:image/png;base64,${base64}`;
691
+ mainWindow.webContents.send("set-avatar", dataUrl);
695
692
  updateTrayIcon(avatarPath);
696
693
  }
697
694
  res.writeHead(200, { "Content-Type": "application/json" });
698
695
  res.end(JSON.stringify({ success: true }));
699
- } catch (e) {
700
- res.writeHead(400, { "Content-Type": "application/json" });
701
- res.end(JSON.stringify({ error: "Invalid JSON" }));
702
- }
696
+ } catch (e) {}
703
697
  });
704
698
  } else if (req.method === "POST" && req.url === "/generate-avatar") {
705
699
  let body = "";
@@ -716,7 +710,10 @@ function startAvatarServer() {
716
710
  }
717
711
  const avatarPath = await generateAvatarForPrompt(prompt);
718
712
  if (mainWindow) {
719
- mainWindow.webContents.send("set-avatar", avatarPath);
713
+ const imageBuffer = fs.readFileSync(avatarPath);
714
+ const base64 = imageBuffer.toString("base64");
715
+ const dataUrl = `data:image/png;base64,${base64}`;
716
+ mainWindow.webContents.send("set-avatar", dataUrl);
720
717
  updateTrayIcon(avatarPath);
721
718
  }
722
719
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -778,7 +775,10 @@ function createWindow() {
778
775
  if (mainWindow) {
779
776
  const avatarPath = getAvatarPath();
780
777
  try {
781
- mainWindow.webContents.send("set-avatar", avatarPath);
778
+ const imageBuffer = fs.readFileSync(avatarPath);
779
+ const base64 = imageBuffer.toString("base64");
780
+ const dataUrl = `data:image/png;base64,${base64}`;
781
+ mainWindow.webContents.send("set-avatar", dataUrl);
782
782
  updateTrayIcon(avatarPath);
783
783
  setTimeout(() => {
784
784
  if (mainWindow && !mainWindow.isVisible()) {
@@ -786,9 +786,7 @@ function createWindow() {
786
786
  mainWindow.setAlwaysOnTop(true, "screen-saver");
787
787
  }
788
788
  }, 100);
789
- } catch (err) {
790
- const message = err instanceof Error ? err.message : String(err);
791
- }
789
+ } catch (error) {}
792
790
  }
793
791
  });
794
792
  mainWindow.webContents.on("did-fail-load", (event, errorCode, errorDescription) => {});
@@ -846,7 +844,7 @@ function processTrayIcon(pngPath) {
846
844
  }
847
845
  trayIcon = nativeImage.createFromBitmap(bitmap, size);
848
846
  } else {
849
- trayIcon = nativeImage.createFromPath(path.join(AVATAR_DIR, "avatar.svg"));
847
+ trayIcon = nativeImage.createFromDataURL("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==");
850
848
  }
851
849
  return trayIcon;
852
850
  }
package/dist/index.js CHANGED
@@ -6,9 +6,9 @@ import * as http from "http";
6
6
  import * as fs from "fs";
7
7
  import * as os from "os";
8
8
  var __dirname = "/var/home/wizard/av";
9
- var AVATAR_DIR = __dirname;
9
+ var PLUGIN_DIR = __dirname;
10
+ var AVATAR_DIR = path.join(os.homedir(), ".config", "opencode");
10
11
  var DEFAULT_AVATAR = "avatar.png";
11
- var USER_AVATAR = path.join(os.homedir(), ".config", "opencode", "avatar.png");
12
12
  var THINKING_PROMPT = "thinking hard";
13
13
  var AVATAR_PORT = 47291;
14
14
  function getToolPrompt(toolName, toolDescription) {
@@ -93,18 +93,16 @@ function promptToFilename(prompt, toolName) {
93
93
  return "avatar_" + baseName.toLowerCase().replace(/[^a-z0-9\s]/g, "").replace(/\s+/g, "_").substring(0, 50) + ".png";
94
94
  }
95
95
  function getAvatarPath(prompt, toolName) {
96
+ const defaultAvatar = path.join(AVATAR_DIR, DEFAULT_AVATAR);
96
97
  if (!prompt) {
97
- if (fs.existsSync(USER_AVATAR)) {
98
- return USER_AVATAR;
99
- }
100
- return path.join(AVATAR_DIR, DEFAULT_AVATAR);
98
+ return defaultAvatar;
101
99
  }
102
100
  const filename = promptToFilename(prompt, toolName);
103
101
  const avatarPath = path.join(AVATAR_DIR, filename);
104
102
  if (fs.existsSync(avatarPath)) {
105
103
  return avatarPath;
106
104
  }
107
- return path.join(AVATAR_DIR, DEFAULT_AVATAR);
105
+ return defaultAvatar;
108
106
  }
109
107
  async function startElectron(avatarPath) {
110
108
  if (isShuttingDown) {
@@ -120,10 +118,10 @@ async function startElectron(avatarPath) {
120
118
  } catch (e) {}
121
119
  electronProcess = null;
122
120
  }
123
- const electronPath = path.join(AVATAR_DIR, "node_modules", ".bin", "electron");
124
- const electronEntry = path.join(AVATAR_DIR, "dist", "electron.js");
121
+ const electronPath = path.join(PLUGIN_DIR, "node_modules", ".bin", "electron");
122
+ const electronEntry = path.join(PLUGIN_DIR, "dist", "electron.js");
125
123
  const child = spawn(electronPath, [electronEntry, "--avatar", avatarPath, "--avatar-port", String(AVATAR_PORT)], {
126
- cwd: AVATAR_DIR,
124
+ cwd: PLUGIN_DIR,
127
125
  stdio: ["ignore", "pipe", "pipe"],
128
126
  detached: false
129
127
  });
@@ -166,9 +164,9 @@ process.on("SIGTERM", () => {
166
164
  process.on("uncaughtException", (err) => {
167
165
  shutdownElectron();
168
166
  });
169
- async function setAvatarViaHttp(prompt, toolName) {
167
+ async function setAvatarViaHttp(prompt, toolName, force) {
170
168
  const avatarPath = getAvatarPath(prompt, toolName);
171
- if (avatarPath === currentAvatar) {
169
+ if (!force && avatarPath === currentAvatar) {
172
170
  return;
173
171
  }
174
172
  currentAvatar = avatarPath;
@@ -236,10 +234,6 @@ var AvatarPlugin = async ({ client }) => {
236
234
  let data = "";
237
235
  res.on("data", (chunk) => data += chunk);
238
236
  res.on("end", () => {
239
- if (!showToasts) {
240
- isToolActive = false;
241
- isThinking = false;
242
- }
243
237
  if (res.statusCode === 200) {
244
238
  if (showToasts) {
245
239
  showInfoToast(`Avatar ready: ${prompt}`);
@@ -303,7 +297,7 @@ var AvatarPlugin = async ({ client }) => {
303
297
  isThinking = false;
304
298
  isToolActive = false;
305
299
  currentRequestId = null;
306
- await setAvatarViaHttp();
300
+ await setAvatarViaHttp(undefined, undefined, true);
307
301
  }
308
302
  }
309
303
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-avatar",
3
- "version": "0.3.4",
3
+ "version": "0.3.7",
4
4
  "description": "Dynamic desktop avatar plugin for OpenCode that reacts to your coding activities",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",