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 +59 -30
- package/dist/index.js +11 -17
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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 (
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
124
|
-
const electronEntry = path.join(
|
|
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:
|
|
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
|
};
|