opencode-avatar 0.3.4 → 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,42 +493,39 @@ 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
- ipcRenderer.on('set-avatar', (event, avatarPath) => {
528
- loadAvatar('file://' + avatarPath);
527
+ ipcRenderer.on('set-avatar', (event, avatarDataUrl) => {
528
+ loadAvatar(avatarDataUrl);
529
529
  });
530
530
 
531
531
  // Fallback: load default avatar if no IPC message received
@@ -690,16 +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
- mainWindow.webContents.send("set-avatar", avatarPath);
695
+ const imageBuffer = fs.readFileSync(avatarPath);
696
+ const base64 = imageBuffer.toString("base64");
697
+ const dataUrl = `data:image/png;base64,${base64}`;
698
+ mainWindow.webContents.send("set-avatar", dataUrl);
695
699
  updateTrayIcon(avatarPath);
696
700
  }
697
701
  res.writeHead(200, { "Content-Type": "application/json" });
698
702
  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
- }
703
+ } catch (e) {}
703
704
  });
704
705
  } else if (req.method === "POST" && req.url === "/generate-avatar") {
705
706
  let body = "";
@@ -716,7 +717,10 @@ function startAvatarServer() {
716
717
  }
717
718
  const avatarPath = await generateAvatarForPrompt(prompt);
718
719
  if (mainWindow) {
719
- mainWindow.webContents.send("set-avatar", avatarPath);
720
+ const imageBuffer = fs.readFileSync(avatarPath);
721
+ const base64 = imageBuffer.toString("base64");
722
+ const dataUrl = `data:image/png;base64,${base64}`;
723
+ mainWindow.webContents.send("set-avatar", dataUrl);
720
724
  updateTrayIcon(avatarPath);
721
725
  }
722
726
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -776,9 +780,13 @@ function createWindow() {
776
780
  mainWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(HTML_CONTENT)}`);
777
781
  mainWindow.webContents.on("did-finish-load", () => {
778
782
  if (mainWindow) {
783
+ log("Window finished loading");
779
784
  const avatarPath = getAvatarPath();
780
785
  try {
781
- mainWindow.webContents.send("set-avatar", avatarPath);
786
+ const imageBuffer = fs.readFileSync(avatarPath);
787
+ const base64 = imageBuffer.toString("base64");
788
+ const dataUrl = `data:image/png;base64,${base64}`;
789
+ mainWindow.webContents.send("set-avatar", dataUrl);
782
790
  updateTrayIcon(avatarPath);
783
791
  setTimeout(() => {
784
792
  if (mainWindow && !mainWindow.isVisible()) {
@@ -786,9 +794,7 @@ function createWindow() {
786
794
  mainWindow.setAlwaysOnTop(true, "screen-saver");
787
795
  }
788
796
  }, 100);
789
- } catch (err) {
790
- const message = err instanceof Error ? err.message : String(err);
791
- }
797
+ } catch (error) {}
792
798
  }
793
799
  });
794
800
  mainWindow.webContents.on("did-fail-load", (event, errorCode, errorDescription) => {});
@@ -828,6 +834,7 @@ function createTray() {
828
834
  tray.on("click", () => mainWindow?.isVisible() ? mainWindow.hide() : mainWindow?.show());
829
835
  }
830
836
  function processTrayIcon(pngPath) {
837
+ log("Processing tray icon from: " + pngPath);
831
838
  let trayIcon = nativeImage.createFromPath(pngPath);
832
839
  if (!trayIcon.isEmpty()) {
833
840
  const size = trayIcon.getSize();
@@ -845,8 +852,10 @@ function processTrayIcon(pngPath) {
845
852
  }
846
853
  }
847
854
  trayIcon = nativeImage.createFromBitmap(bitmap, size);
855
+ log("Tray icon processed successfully");
848
856
  } else {
849
- trayIcon = nativeImage.createFromPath(path.join(AVATAR_DIR, "avatar.svg"));
857
+ log("Tray icon is empty, using fallback");
858
+ trayIcon = nativeImage.createFromDataURL("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==");
850
859
  }
851
860
  return trayIcon;
852
861
  }
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.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",