opencode-avatar 0.3.3 → 0.3.6

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/dist/electron.js CHANGED
@@ -405,17 +405,20 @@ import * as fs from "fs";
405
405
  import * as os from "os";
406
406
  import { fileURLToPath } from "url";
407
407
  require_main().config();
408
+ var logFile = path.join(os.homedir(), "avatar.log");
409
+ function log(msg) {
410
+ fs.appendFileSync(logFile, new Date().toISOString() + ": " + msg + `
411
+ `);
412
+ }
408
413
  function getConfig() {
409
414
  try {
410
415
  const configPath = path.join(os.homedir(), ".config", "opencode", "opencode-avatar.json");
411
416
  const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
412
417
  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
418
  return { falKey: null, prompt: null };
415
419
  }
416
420
  return { falKey: config.falKey, prompt: config.prompt || null };
417
421
  } 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
422
  return { falKey: null, prompt: null };
420
423
  }
421
424
  }
@@ -490,38 +493,35 @@ var HTML_CONTENT = `<!DOCTYPE html>
490
493
 
491
494
  ctx.drawImage(srcImg, 0, 0);
492
495
 
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
-
496
+ // Chroma keying: make background transparent
499
497
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
500
498
  const data = imageData.data;
499
+ const chromaR = data[0]; // first pixel R
500
+ const chromaG = data[1]; // first pixel G
501
+ const chromaB = data[2]; // first pixel B
501
502
  const tolerance = 30;
502
503
 
503
504
  for (let i = 0; i < data.length; i += 4) {
504
505
  const r = data[i];
505
506
  const g = data[i + 1];
506
507
  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;
508
+ if (Math.abs(r - chromaR) <= tolerance &&
509
+ Math.abs(g - chromaG) <= tolerance &&
510
+ Math.abs(b - chromaB) <= tolerance) {
511
+ data[i + 3] = 0; // set alpha to 0
514
512
  }
515
513
  }
516
514
 
517
515
  ctx.putImageData(imageData, 0, 0);
516
+
518
517
  img.src = canvas.toDataURL('image/png');
519
518
  };
520
519
 
521
- srcImg.onerror = function(e) {
522
- console.error('Failed to load image:', e);
523
- img.src = 'avatar.svg';
524
- };
520
+ srcImg.onerror = function(e) {
521
+ console.error('Failed to load image:', e);
522
+ log('Renderer: Avatar load failed, using fallback transparent image');
523
+ img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
524
+ };
525
525
  }
526
526
 
527
527
  ipcRenderer.on('set-avatar', (event, avatarDataUrl) => {
@@ -690,18 +690,17 @@ function startAvatarServer() {
690
690
  req.on("end", () => {
691
691
  try {
692
692
  const { avatarPath } = JSON.parse(body);
693
+ log("Set-avatar request with path: " + avatarPath);
693
694
  if (mainWindow && avatarPath) {
694
695
  const imageBuffer = fs.readFileSync(avatarPath);
695
696
  const base64 = imageBuffer.toString("base64");
696
697
  const dataUrl = `data:image/png;base64,${base64}`;
697
698
  mainWindow.webContents.send("set-avatar", dataUrl);
699
+ updateTrayIcon(avatarPath);
698
700
  }
699
701
  res.writeHead(200, { "Content-Type": "application/json" });
700
702
  res.end(JSON.stringify({ success: true }));
701
- } catch (e) {
702
- res.writeHead(400, { "Content-Type": "application/json" });
703
- res.end(JSON.stringify({ error: "Invalid JSON" }));
704
- }
703
+ } catch (e) {}
705
704
  });
706
705
  } else if (req.method === "POST" && req.url === "/generate-avatar") {
707
706
  let body = "";
@@ -722,6 +721,7 @@ function startAvatarServer() {
722
721
  const base64 = imageBuffer.toString("base64");
723
722
  const dataUrl = `data:image/png;base64,${base64}`;
724
723
  mainWindow.webContents.send("set-avatar", dataUrl);
724
+ updateTrayIcon(avatarPath);
725
725
  }
726
726
  res.writeHead(200, { "Content-Type": "application/json" });
727
727
  res.end(JSON.stringify({ success: true, avatarPath }));
@@ -780,21 +780,21 @@ function createWindow() {
780
780
  mainWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(HTML_CONTENT)}`);
781
781
  mainWindow.webContents.on("did-finish-load", () => {
782
782
  if (mainWindow) {
783
+ log("Window finished loading");
783
784
  const avatarPath = getAvatarPath();
784
785
  try {
785
786
  const imageBuffer = fs.readFileSync(avatarPath);
786
787
  const base64 = imageBuffer.toString("base64");
787
788
  const dataUrl = `data:image/png;base64,${base64}`;
788
789
  mainWindow.webContents.send("set-avatar", dataUrl);
790
+ updateTrayIcon(avatarPath);
789
791
  setTimeout(() => {
790
792
  if (mainWindow && !mainWindow.isVisible()) {
791
793
  mainWindow.show();
792
794
  mainWindow.setAlwaysOnTop(true, "screen-saver");
793
795
  }
794
796
  }, 100);
795
- } catch (err) {
796
- const message = err instanceof Error ? err.message : String(err);
797
- }
797
+ } catch (error) {}
798
798
  }
799
799
  });
800
800
  mainWindow.webContents.on("did-fail-load", (event, errorCode, errorDescription) => {});
@@ -809,10 +809,7 @@ function createTray() {
809
809
  let trayIcon;
810
810
  try {
811
811
  const pngPath = path.join(AVATAR_DIR, "avatar.png");
812
- trayIcon = nativeImage.createFromPath(pngPath);
813
- if (trayIcon.isEmpty()) {
814
- trayIcon = nativeImage.createFromPath(path.join(AVATAR_DIR, "avatar.svg"));
815
- }
812
+ trayIcon = processTrayIcon(pngPath);
816
813
  } catch (e) {
817
814
  const message = e instanceof Error ? e.message : String(e);
818
815
  trayIcon = nativeImage.createFromPath(path.join(AVATAR_DIR, "avatar.svg"));
@@ -836,6 +833,38 @@ function createTray() {
836
833
  tray.setContextMenu(contextMenu);
837
834
  tray.on("click", () => mainWindow?.isVisible() ? mainWindow.hide() : mainWindow?.show());
838
835
  }
836
+ function processTrayIcon(pngPath) {
837
+ log("Processing tray icon from: " + pngPath);
838
+ let trayIcon = nativeImage.createFromPath(pngPath);
839
+ if (!trayIcon.isEmpty()) {
840
+ const size = trayIcon.getSize();
841
+ const bitmap = trayIcon.getBitmap();
842
+ const chromaR = bitmap[0];
843
+ const chromaG = bitmap[1];
844
+ const chromaB = bitmap[2];
845
+ const tolerance = 30;
846
+ for (let i = 0;i < bitmap.length; i += 4) {
847
+ const r = bitmap[i];
848
+ const g = bitmap[i + 1];
849
+ const b = bitmap[i + 2];
850
+ if (Math.abs(r - chromaR) <= tolerance && Math.abs(g - chromaG) <= tolerance && Math.abs(b - chromaB) <= tolerance) {
851
+ bitmap[i + 3] = 0;
852
+ }
853
+ }
854
+ trayIcon = nativeImage.createFromBitmap(bitmap, size);
855
+ log("Tray icon processed successfully");
856
+ } else {
857
+ log("Tray icon is empty, using fallback");
858
+ trayIcon = nativeImage.createFromDataURL("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==");
859
+ }
860
+ return trayIcon;
861
+ }
862
+ function updateTrayIcon(avatarPath) {
863
+ if (tray) {
864
+ const trayIcon = processTrayIcon(avatarPath);
865
+ tray.setImage(trayIcon);
866
+ }
867
+ }
839
868
  app.commandLine.appendSwitch("enable-transparent-visuals");
840
869
  app.whenReady().then(async () => {
841
870
  fs.mkdirSync(AVATAR_DIR, { recursive: true });
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.3",
3
+ "version": "0.3.6",
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",