flawed-avatar 0.2.1

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.
Files changed (152) hide show
  1. package/assets/animations/idle/Breathing Idle.fbx +0 -0
  2. package/assets/animations/idle/look away gesture.fbx +0 -0
  3. package/assets/animations/idle/weight shift.fbx +0 -0
  4. package/assets/animations/speaking/Agreeing.fbx +0 -0
  5. package/assets/animations/speaking/Talking (1).fbx +0 -0
  6. package/assets/animations/speaking/Talking (2).fbx +0 -0
  7. package/assets/animations/speaking/Talking (3).fbx +0 -0
  8. package/assets/animations/speaking/Talking.fbx +0 -0
  9. package/assets/animations/speaking/head nod yes.fbx +0 -0
  10. package/assets/animations/thinking/Thinking.fbx +0 -0
  11. package/assets/animations/thinking/thoughtful head shake.fbx +0 -0
  12. package/assets/animations/working/acknowledging.fbx +0 -0
  13. package/assets/animations/working/lengthy head nod.fbx +0 -0
  14. package/assets/icon.png +0 -0
  15. package/assets/models/CaptainLobster.vrm +0 -0
  16. package/assets/models/default-avatar.vrm +0 -0
  17. package/dist/chat-preload.cjs +87 -0
  18. package/dist/chat-renderer-bundle/chat-index.html +16 -0
  19. package/dist/chat-renderer-bundle/chat-renderer.js +355 -0
  20. package/dist/chat-renderer-bundle/styles/base.css +106 -0
  21. package/dist/chat-renderer-bundle/styles/chat.css +516 -0
  22. package/dist/chat-renderer-bundle/styles/components/button.css +221 -0
  23. package/dist/chat-renderer-bundle/styles/components/indicator.css +216 -0
  24. package/dist/chat-renderer-bundle/styles/components/input.css +139 -0
  25. package/dist/chat-renderer-bundle/styles/components/toast.css +204 -0
  26. package/dist/chat-renderer-bundle/styles/controls.css +279 -0
  27. package/dist/chat-renderer-bundle/styles/settings.css +310 -0
  28. package/dist/chat-renderer-bundle/styles/tokens.css +220 -0
  29. package/dist/chat-renderer-bundle/styles/utilities.css +349 -0
  30. package/dist/main/main/display-utils.d.ts +12 -0
  31. package/dist/main/main/display-utils.js +29 -0
  32. package/dist/main/main/gateway-client.d.ts +13 -0
  33. package/dist/main/main/gateway-client.js +265 -0
  34. package/dist/main/main/main.d.ts +1 -0
  35. package/dist/main/main/main.js +157 -0
  36. package/dist/main/main/persistence/chat-store.d.ts +8 -0
  37. package/dist/main/main/persistence/chat-store.js +110 -0
  38. package/dist/main/main/persistence/file-store.d.ts +17 -0
  39. package/dist/main/main/persistence/file-store.js +183 -0
  40. package/dist/main/main/persistence/index.d.ts +8 -0
  41. package/dist/main/main/persistence/index.js +8 -0
  42. package/dist/main/main/persistence/migrations.d.ts +23 -0
  43. package/dist/main/main/persistence/migrations.js +191 -0
  44. package/dist/main/main/persistence/settings-store.d.ts +32 -0
  45. package/dist/main/main/persistence/settings-store.js +174 -0
  46. package/dist/main/main/persistence/types.d.ts +72 -0
  47. package/dist/main/main/persistence/types.js +69 -0
  48. package/dist/main/main/settings-broadcast.d.ts +3 -0
  49. package/dist/main/main/settings-broadcast.js +9 -0
  50. package/dist/main/main/stdin-listener.d.ts +15 -0
  51. package/dist/main/main/stdin-listener.js +27 -0
  52. package/dist/main/main/tray.d.ts +3 -0
  53. package/dist/main/main/tray.js +59 -0
  54. package/dist/main/main/window-manager.d.ts +23 -0
  55. package/dist/main/main/window-manager.js +232 -0
  56. package/dist/main/main/window.d.ts +3 -0
  57. package/dist/main/main/window.js +528 -0
  58. package/dist/main/shared/config.d.ts +91 -0
  59. package/dist/main/shared/config.js +111 -0
  60. package/dist/main/shared/ipc-channels.d.ts +54 -0
  61. package/dist/main/shared/ipc-channels.js +68 -0
  62. package/dist/main/shared/types.d.ts +6 -0
  63. package/dist/main/shared/types.js +1 -0
  64. package/dist/preload.cjs +256 -0
  65. package/dist/renderer-bundle/index.html +63 -0
  66. package/dist/renderer-bundle/renderer.js +100734 -0
  67. package/dist/renderer-bundle/styles/base.css +106 -0
  68. package/dist/renderer-bundle/styles/chat.css +516 -0
  69. package/dist/renderer-bundle/styles/components/button.css +221 -0
  70. package/dist/renderer-bundle/styles/components/indicator.css +216 -0
  71. package/dist/renderer-bundle/styles/components/input.css +139 -0
  72. package/dist/renderer-bundle/styles/components/toast.css +204 -0
  73. package/dist/renderer-bundle/styles/controls.css +279 -0
  74. package/dist/renderer-bundle/styles/settings.css +310 -0
  75. package/dist/renderer-bundle/styles/tokens.css +220 -0
  76. package/dist/renderer-bundle/styles/utilities.css +349 -0
  77. package/index.ts +32 -0
  78. package/openclaw.plugin.json +22 -0
  79. package/package.json +45 -0
  80. package/src/electron-launcher.ts +63 -0
  81. package/src/main/chat-preload.cjs +87 -0
  82. package/src/main/display-utils.ts +39 -0
  83. package/src/main/gateway-client.ts +312 -0
  84. package/src/main/main.ts +169 -0
  85. package/src/main/persistence/chat-store.ts +143 -0
  86. package/src/main/persistence/file-store.ts +221 -0
  87. package/src/main/persistence/index.ts +69 -0
  88. package/src/main/persistence/migrations.ts +232 -0
  89. package/src/main/persistence/settings-store.ts +219 -0
  90. package/src/main/persistence/types.ts +107 -0
  91. package/src/main/preload.cjs +256 -0
  92. package/src/main/settings-broadcast.ts +13 -0
  93. package/src/main/settings-preload.cjs +153 -0
  94. package/src/main/stdin-listener.ts +34 -0
  95. package/src/main/tray.ts +65 -0
  96. package/src/main/window-manager.ts +298 -0
  97. package/src/main/window.ts +614 -0
  98. package/src/renderer/audio/audio-player.ts +161 -0
  99. package/src/renderer/audio/frequency-analyzer.ts +104 -0
  100. package/src/renderer/audio/index.ts +36 -0
  101. package/src/renderer/audio/kokoro-model-loader.ts +128 -0
  102. package/src/renderer/audio/kokoro-tts-service.ts +370 -0
  103. package/src/renderer/audio/lip-sync-profile.json +1 -0
  104. package/src/renderer/audio/phoneme-mapper.ts +120 -0
  105. package/src/renderer/audio/tts-controller.ts +344 -0
  106. package/src/renderer/audio/tts-service-factory.ts +75 -0
  107. package/src/renderer/audio/tts-service.ts +16 -0
  108. package/src/renderer/audio/types.ts +120 -0
  109. package/src/renderer/audio/web-speech-tts.ts +177 -0
  110. package/src/renderer/audio/wlipsync-analyzer.ts +145 -0
  111. package/src/renderer/avatar/animation-loader.ts +114 -0
  112. package/src/renderer/avatar/animator.ts +322 -0
  113. package/src/renderer/avatar/expressions.ts +165 -0
  114. package/src/renderer/avatar/eye-gaze.ts +255 -0
  115. package/src/renderer/avatar/eye-saccades.ts +133 -0
  116. package/src/renderer/avatar/hover-awareness.ts +125 -0
  117. package/src/renderer/avatar/ibl-enhancer.ts +163 -0
  118. package/src/renderer/avatar/lip-sync.ts +258 -0
  119. package/src/renderer/avatar/mixamo-retarget.ts +169 -0
  120. package/src/renderer/avatar/pixel-transparency.ts +65 -0
  121. package/src/renderer/avatar/scene.ts +70 -0
  122. package/src/renderer/avatar/spring-bones.ts +27 -0
  123. package/src/renderer/avatar/state-machine.ts +117 -0
  124. package/src/renderer/avatar/vrm-loader.ts +71 -0
  125. package/src/renderer/chat-window/chat-index.html +16 -0
  126. package/src/renderer/chat-window/chat-renderer.ts +28 -0
  127. package/src/renderer/index.html +63 -0
  128. package/src/renderer/renderer.ts +329 -0
  129. package/src/renderer/settings-window/settings-controls.ts +223 -0
  130. package/src/renderer/settings-window/settings-index.html +16 -0
  131. package/src/renderer/settings-window/settings-panel.ts +346 -0
  132. package/src/renderer/settings-window/settings-renderer.ts +5 -0
  133. package/src/renderer/styles/base.css +106 -0
  134. package/src/renderer/styles/chat.css +516 -0
  135. package/src/renderer/styles/components/button.css +221 -0
  136. package/src/renderer/styles/components/indicator.css +216 -0
  137. package/src/renderer/styles/components/input.css +139 -0
  138. package/src/renderer/styles/components/toast.css +204 -0
  139. package/src/renderer/styles/controls.css +279 -0
  140. package/src/renderer/styles/settings.css +310 -0
  141. package/src/renderer/styles/tokens.css +220 -0
  142. package/src/renderer/styles/utilities.css +349 -0
  143. package/src/renderer/types/avatar-bridge.d.ts +86 -0
  144. package/src/renderer/types/chat-bridge.d.ts +37 -0
  145. package/src/renderer/types/settings-bridge.d.ts +54 -0
  146. package/src/renderer/ui/chat-bubble.ts +435 -0
  147. package/src/renderer/ui/icons.ts +47 -0
  148. package/src/renderer/ui/typing-indicator.ts +41 -0
  149. package/src/service.ts +163 -0
  150. package/src/shared/config.ts +135 -0
  151. package/src/shared/ipc-channels.ts +81 -0
  152. package/src/shared/types.ts +7 -0
@@ -0,0 +1,153 @@
1
+ // Channel strings duplicated from src/shared/ipc-channels.ts (CJS cannot import ESM).
2
+ // Keep both files in sync when adding/renaming channels.
3
+ const { contextBridge, ipcRenderer } = require("electron");
4
+
5
+ const IPC = {
6
+ // Settings window lifecycle
7
+ CLOSE_SETTINGS: "settings:close",
8
+ PICK_VRM_FILE: "settings:pick-vrm-file",
9
+ // Full settings bundle
10
+ GET_SETTINGS: "avatar:get-settings",
11
+ // Opacity
12
+ SET_OPACITY: "avatar:set-opacity",
13
+ OPACITY_CHANGED: "avatar:opacity-changed",
14
+ // Scale
15
+ SET_SCALE: "avatar:set-scale",
16
+ SCALE_CHANGED: "avatar:scale-changed",
17
+ // Camera
18
+ SET_CAMERA_ZOOM: "avatar:set-camera-zoom",
19
+ SAVE_CAMERA_ZOOM: "avatar:save-camera-zoom",
20
+ // TTS
21
+ SET_TTS_ENABLED: "avatar:tts-set-enabled",
22
+ TTS_ENABLED_CHANGED: "avatar:tts-enabled-changed",
23
+ SET_TTS_ENGINE: "avatar:tts-set-engine",
24
+ TTS_ENGINE_CHANGED: "avatar:tts-engine-changed",
25
+ SET_TTS_VOICE: "avatar:tts-set-voice",
26
+ TTS_VOICE_CHANGED: "avatar:tts-voice-changed",
27
+ // Idle timeout
28
+ SET_IDLE_TIMEOUT: "chat:set-idle-timeout",
29
+ IDLE_TIMEOUT_CHANGED: "chat:idle-timeout-changed",
30
+ // Lighting
31
+ SET_LIGHTING_PROFILE: "avatar:set-lighting-profile",
32
+ LIGHTING_PROFILE_CHANGED: "avatar:lighting-profile-changed",
33
+ SET_LIGHTING_CUSTOM: "avatar:set-lighting-custom",
34
+ LIGHTING_CUSTOM_CHANGED: "avatar:lighting-custom-changed",
35
+ // VRM model
36
+ VRM_MODEL_CHANGED: "avatar:vrm-model-changed",
37
+ // Chat
38
+ CLEAR_CHAT_HISTORY: "chat:clear-history",
39
+ // Snap
40
+ SNAP_TO: "avatar:snap-to",
41
+ };
42
+
43
+ contextBridge.exposeInMainWorld("settingsBridge", {
44
+ // Getters (async invoke)
45
+ getSettings() {
46
+ return ipcRenderer.invoke(IPC.GET_SETTINGS);
47
+ },
48
+
49
+ // Setters (fire-and-forget send)
50
+ setOpacity(v) {
51
+ ipcRenderer.send(IPC.SET_OPACITY, v);
52
+ },
53
+
54
+ setScale(v) {
55
+ ipcRenderer.send(IPC.SET_SCALE, v);
56
+ },
57
+
58
+ setCameraZoom(v) {
59
+ ipcRenderer.send(IPC.SAVE_CAMERA_ZOOM, v);
60
+ },
61
+
62
+ setTtsEnabled(v) {
63
+ ipcRenderer.send(IPC.SET_TTS_ENABLED, v);
64
+ },
65
+
66
+ setTtsEngine(v) {
67
+ ipcRenderer.send(IPC.SET_TTS_ENGINE, v);
68
+ },
69
+
70
+ setTtsVoice(v) {
71
+ ipcRenderer.send(IPC.SET_TTS_VOICE, v);
72
+ },
73
+
74
+ setIdleTimeout(ms) {
75
+ ipcRenderer.send(IPC.SET_IDLE_TIMEOUT, ms);
76
+ },
77
+
78
+ setLightingProfile(profile) {
79
+ ipcRenderer.send(IPC.SET_LIGHTING_PROFILE, profile);
80
+ },
81
+
82
+ setLightingCustom(custom) {
83
+ ipcRenderer.send(IPC.SET_LIGHTING_CUSTOM, custom);
84
+ },
85
+
86
+ // Actions
87
+ pickVrmFile() {
88
+ return ipcRenderer.invoke(IPC.PICK_VRM_FILE);
89
+ },
90
+
91
+ snapTo(corner) {
92
+ ipcRenderer.send(IPC.SNAP_TO, corner);
93
+ },
94
+
95
+ clearChat() {
96
+ ipcRenderer.send(IPC.CLEAR_CHAT_HISTORY);
97
+ },
98
+
99
+ close() {
100
+ ipcRenderer.send(IPC.CLOSE_SETTINGS);
101
+ },
102
+
103
+ // Change listeners (main -> settings renderer)
104
+ onOpacityChanged(cb) {
105
+ ipcRenderer.removeAllListeners(IPC.OPACITY_CHANGED);
106
+ ipcRenderer.on(IPC.OPACITY_CHANGED, (_event, v) => cb(v));
107
+ },
108
+
109
+ onScaleChanged(cb) {
110
+ ipcRenderer.removeAllListeners(IPC.SCALE_CHANGED);
111
+ ipcRenderer.on(IPC.SCALE_CHANGED, (_event, v) => cb(v));
112
+ },
113
+
114
+ onCameraZoomChanged(cb) {
115
+ ipcRenderer.removeAllListeners(IPC.SET_CAMERA_ZOOM);
116
+ ipcRenderer.on(IPC.SET_CAMERA_ZOOM, (_event, v) => cb(v));
117
+ },
118
+
119
+ onTtsEnabledChanged(cb) {
120
+ ipcRenderer.removeAllListeners(IPC.TTS_ENABLED_CHANGED);
121
+ ipcRenderer.on(IPC.TTS_ENABLED_CHANGED, (_event, v) => cb(v));
122
+ },
123
+
124
+ onTtsEngineChanged(cb) {
125
+ ipcRenderer.removeAllListeners(IPC.TTS_ENGINE_CHANGED);
126
+ ipcRenderer.on(IPC.TTS_ENGINE_CHANGED, (_event, v) => cb(v));
127
+ },
128
+
129
+ onTtsVoiceChanged(cb) {
130
+ ipcRenderer.removeAllListeners(IPC.TTS_VOICE_CHANGED);
131
+ ipcRenderer.on(IPC.TTS_VOICE_CHANGED, (_event, v) => cb(v));
132
+ },
133
+
134
+ onIdleTimeoutChanged(cb) {
135
+ ipcRenderer.removeAllListeners(IPC.IDLE_TIMEOUT_CHANGED);
136
+ ipcRenderer.on(IPC.IDLE_TIMEOUT_CHANGED, (_event, ms) => cb(ms));
137
+ },
138
+
139
+ onLightingProfileChanged(cb) {
140
+ ipcRenderer.removeAllListeners(IPC.LIGHTING_PROFILE_CHANGED);
141
+ ipcRenderer.on(IPC.LIGHTING_PROFILE_CHANGED, (_event, v) => cb(v));
142
+ },
143
+
144
+ onLightingCustomChanged(cb) {
145
+ ipcRenderer.removeAllListeners(IPC.LIGHTING_CUSTOM_CHANGED);
146
+ ipcRenderer.on(IPC.LIGHTING_CUSTOM_CHANGED, (_event, v) => cb(v));
147
+ },
148
+
149
+ onVrmModelChanged(cb) {
150
+ ipcRenderer.removeAllListeners(IPC.VRM_MODEL_CHANGED);
151
+ ipcRenderer.on(IPC.VRM_MODEL_CHANGED, (_event, path) => cb(path));
152
+ },
153
+ });
@@ -0,0 +1,34 @@
1
+ import * as readline from "node:readline";
2
+
3
+ export type StdinCommand =
4
+ | { type: "show" }
5
+ | { type: "hide" }
6
+ | { type: "shutdown" }
7
+ | { type: "model-switch"; vrmPath: string };
8
+
9
+ /**
10
+ * Listen for newline-delimited JSON commands on stdin.
11
+ * Returns a cleanup function to stop listening.
12
+ */
13
+ export function createStdinListener(handler: (cmd: StdinCommand) => void): () => void {
14
+ const rl = readline.createInterface({ input: process.stdin });
15
+
16
+ rl.on("line", (line) => {
17
+ const trimmed = line.trim();
18
+ if (!trimmed) return;
19
+
20
+ try {
21
+ const parsed = JSON.parse(trimmed);
22
+ if (typeof parsed?.type === "string") {
23
+ if (parsed.type === "model-switch" && typeof parsed.vrmPath !== "string") return;
24
+ handler(parsed as StdinCommand);
25
+ }
26
+ } catch {
27
+ // Ignore malformed lines
28
+ }
29
+ });
30
+
31
+ return () => {
32
+ rl.close();
33
+ };
34
+ }
@@ -0,0 +1,65 @@
1
+ import { Tray, Menu, app } from "electron";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { showVrmPicker } from "./window.js";
5
+ import type { WindowManager } from "./window-manager.js";
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ let tray: Tray | null = null;
11
+
12
+ export function createTray(wm: WindowManager): Tray {
13
+ const iconPath = path.join(__dirname, "..", "..", "..", "assets", "icon.png");
14
+ tray = new Tray(iconPath);
15
+ tray.setToolTip("Flawed Avatar");
16
+
17
+ function rebuildMenu(): void {
18
+ const menu = Menu.buildFromTemplate([
19
+ {
20
+ label: wm.avatarWin.isVisible() ? "Hide Avatar" : "Show Avatar",
21
+ click() {
22
+ if (wm.avatarWin.isVisible()) {
23
+ wm.hideAll();
24
+ } else {
25
+ wm.showAvatar();
26
+ }
27
+ rebuildMenu();
28
+ },
29
+ },
30
+ {
31
+ label: "Change Avatar Model\u2026",
32
+ click() {
33
+ showVrmPicker(wm.avatarWin);
34
+ },
35
+ },
36
+ {
37
+ label: "Settings\u2026",
38
+ click() {
39
+ wm.showSettings();
40
+ },
41
+ },
42
+ { type: "separator" },
43
+ {
44
+ label: "Quit",
45
+ click() {
46
+ app.quit();
47
+ },
48
+ },
49
+ ]);
50
+ tray!.setContextMenu(menu);
51
+ }
52
+
53
+ rebuildMenu();
54
+
55
+ tray.on("click", () => {
56
+ if (wm.avatarWin.isVisible()) {
57
+ wm.hideAll();
58
+ } else {
59
+ wm.showAvatar();
60
+ }
61
+ rebuildMenu();
62
+ });
63
+
64
+ return tray;
65
+ }
@@ -0,0 +1,298 @@
1
+ import { BrowserWindow, ipcMain, screen } from "electron";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { createOverlayWindow } from "./window.js";
5
+ import { IPC } from "../shared/ipc-channels.js";
6
+ import {
7
+ WINDOW_HEIGHT,
8
+ CHAT_WINDOW_WIDTH,
9
+ CHAT_WINDOW_HEIGHT,
10
+ CHAT_WINDOW_GAP,
11
+ SETTINGS_WINDOW_WIDTH,
12
+ SETTINGS_WINDOW_HEIGHT,
13
+ } from "../shared/config.js";
14
+ import type { AgentState } from "../shared/types.js";
15
+ import { clampBoundsToWorkArea } from "./display-utils.js";
16
+ import { setSettingsBroadcastTarget } from "./settings-broadcast.js";
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = path.dirname(__filename);
20
+
21
+ export interface WindowManager {
22
+ avatarWin: BrowserWindow;
23
+ chatWin: BrowserWindow;
24
+ settingsWin: BrowserWindow | null;
25
+ chatVisible: boolean;
26
+ toggleChat(): void;
27
+ showChat(): void;
28
+ hideChat(): void;
29
+ repositionChat(): void;
30
+ showSettings(): void;
31
+ hideSettings(): void;
32
+ toggleSettings(): void;
33
+ hideAll(): void;
34
+ showAvatar(): void;
35
+ sendAgentState(state: AgentState): void;
36
+ sendToAvatar(channel: string, ...args: unknown[]): void;
37
+ sendToChat(channel: string, ...args: unknown[]): void;
38
+ sendToSettings(channel: string, ...args: unknown[]): void;
39
+ destroyAll(): void;
40
+ }
41
+
42
+ function computeChatPosition(
43
+ avatarX: number,
44
+ avatarY: number,
45
+ avatarWidth: number,
46
+ ): { x: number; y: number } {
47
+ const point = { x: avatarX + avatarWidth / 2, y: avatarY };
48
+ const display = screen.getDisplayNearestPoint(point);
49
+ const workArea = display.workArea;
50
+
51
+ const chatX = Math.round(
52
+ avatarX + avatarWidth / 2 - CHAT_WINDOW_WIDTH / 2,
53
+ );
54
+
55
+ // Try above avatar first
56
+ const aboveY = avatarY - CHAT_WINDOW_HEIGHT - CHAT_WINDOW_GAP;
57
+ if (aboveY >= workArea.y) {
58
+ return clampBoundsToWorkArea(chatX, aboveY, CHAT_WINDOW_WIDTH, CHAT_WINDOW_HEIGHT);
59
+ }
60
+
61
+ // Fall back to below avatar
62
+ const belowY = avatarY + WINDOW_HEIGHT + CHAT_WINDOW_GAP;
63
+ return clampBoundsToWorkArea(chatX, belowY, CHAT_WINDOW_WIDTH, CHAT_WINDOW_HEIGHT);
64
+ }
65
+
66
+ function createChatWindow(): BrowserWindow {
67
+ const chatWin = new BrowserWindow({
68
+ width: CHAT_WINDOW_WIDTH,
69
+ height: CHAT_WINDOW_HEIGHT,
70
+ transparent: true,
71
+ frame: false,
72
+ alwaysOnTop: true,
73
+ skipTaskbar: true,
74
+ resizable: false,
75
+ hasShadow: false,
76
+ show: false,
77
+ webPreferences: {
78
+ contextIsolation: true,
79
+ nodeIntegration: false,
80
+ preload: path.join(__dirname, "..", "..", "chat-preload.cjs"),
81
+ },
82
+ });
83
+
84
+ chatWin.loadFile(
85
+ path.join(__dirname, "..", "..", "chat-renderer-bundle", "chat-index.html"),
86
+ );
87
+
88
+ // Chat window should be interactive by default (not click-through)
89
+ // Only the transparent areas around the chat body will pass clicks through
90
+
91
+ return chatWin;
92
+ }
93
+
94
+ function createSettingsWindow(): BrowserWindow {
95
+ const settingsWin = new BrowserWindow({
96
+ width: SETTINGS_WINDOW_WIDTH,
97
+ height: SETTINGS_WINDOW_HEIGHT,
98
+ transparent: false,
99
+ frame: false,
100
+ alwaysOnTop: false,
101
+ skipTaskbar: false,
102
+ resizable: true,
103
+ minWidth: 320,
104
+ minHeight: 400,
105
+ show: false,
106
+ webPreferences: {
107
+ contextIsolation: true,
108
+ nodeIntegration: false,
109
+ preload: path.join(__dirname, "..", "..", "settings-preload.cjs"),
110
+ },
111
+ });
112
+
113
+ settingsWin.loadFile(
114
+ path.join(__dirname, "..", "..", "settings-renderer-bundle", "settings-index.html"),
115
+ );
116
+
117
+ return settingsWin;
118
+ }
119
+
120
+ export function createWindowManager(): WindowManager {
121
+ const avatarWin = createOverlayWindow();
122
+ let chatWin = createChatWindow();
123
+ let chatVisible = false;
124
+ let settingsWin: BrowserWindow | null = null;
125
+
126
+ // Handle chat window being closed externally
127
+ chatWin.on("closed", () => {
128
+ chatVisible = false;
129
+ chatWin = createChatWindow();
130
+ });
131
+
132
+ function repositionChat(): void {
133
+ if (chatWin.isDestroyed()) return;
134
+ const [ax, ay] = avatarWin.getPosition();
135
+ const [aw] = avatarWin.getSize();
136
+ const pos = computeChatPosition(ax, ay, aw);
137
+ chatWin.setPosition(pos.x, pos.y);
138
+ }
139
+
140
+ function showChat(): void {
141
+ if (chatVisible) return;
142
+ chatVisible = true;
143
+ repositionChat();
144
+ chatWin.showInactive();
145
+ chatWin.webContents.send(IPC.SHOW_CHAT_BUBBLE);
146
+ avatarWin.webContents.send(IPC.CHAT_VISIBILITY, true);
147
+ }
148
+
149
+ function hideChat(): void {
150
+ if (!chatVisible) return;
151
+ chatVisible = false;
152
+ if (!chatWin.isDestroyed()) chatWin.hide();
153
+ avatarWin.webContents.send(IPC.CHAT_VISIBILITY, false);
154
+ }
155
+
156
+ function toggleChat(): void {
157
+ if (chatVisible) {
158
+ hideChat();
159
+ } else {
160
+ showChat();
161
+ }
162
+ }
163
+
164
+ function showSettings(): void {
165
+ if (avatarWin.isDestroyed()) return;
166
+ if (!settingsWin || settingsWin.isDestroyed()) {
167
+ settingsWin = createSettingsWindow();
168
+ setSettingsBroadcastTarget(settingsWin);
169
+ settingsWin.on("closed", () => {
170
+ settingsWin = null;
171
+ setSettingsBroadcastTarget(null);
172
+ });
173
+ }
174
+ const [ax, ay] = avatarWin.getPosition();
175
+ const pos = clampBoundsToWorkArea(
176
+ ax - SETTINGS_WINDOW_WIDTH - 12,
177
+ ay,
178
+ SETTINGS_WINDOW_WIDTH,
179
+ SETTINGS_WINDOW_HEIGHT,
180
+ );
181
+ settingsWin.setPosition(pos.x, pos.y);
182
+ settingsWin.show();
183
+ settingsWin.focus();
184
+ }
185
+
186
+ function hideSettings(): void {
187
+ if (settingsWin && !settingsWin.isDestroyed()) settingsWin.hide();
188
+ }
189
+
190
+ function toggleSettings(): void {
191
+ if (settingsWin && !settingsWin.isDestroyed() && settingsWin.isVisible()) {
192
+ hideSettings();
193
+ } else {
194
+ showSettings();
195
+ }
196
+ }
197
+
198
+ function sendToSettings(channel: string, ...args: unknown[]): void {
199
+ if (settingsWin && !settingsWin.isDestroyed()) {
200
+ settingsWin.webContents.send(channel, ...args);
201
+ }
202
+ }
203
+
204
+ function hideAll(): void {
205
+ hideChat();
206
+ hideSettings();
207
+ avatarWin.hide();
208
+ }
209
+
210
+ function showAvatar(): void {
211
+ avatarWin.show();
212
+ }
213
+
214
+ function sendAgentState(state: AgentState): void {
215
+ avatarWin.webContents.send(IPC.AGENT_STATE, state);
216
+ if (!chatWin.isDestroyed()) {
217
+ chatWin.webContents.send(IPC.AGENT_STATE, state);
218
+ }
219
+ // Auto-show chat on non-idle states
220
+ if (state.phase !== "idle") {
221
+ showChat();
222
+ }
223
+ }
224
+
225
+ function sendToAvatar(channel: string, ...args: unknown[]): void {
226
+ if (!avatarWin.isDestroyed()) {
227
+ avatarWin.webContents.send(channel, ...args);
228
+ }
229
+ }
230
+
231
+ function sendToChat(channel: string, ...args: unknown[]): void {
232
+ if (!chatWin.isDestroyed()) {
233
+ chatWin.webContents.send(channel, ...args);
234
+ }
235
+ }
236
+
237
+ function destroyAll(): void {
238
+ if (settingsWin && !settingsWin.isDestroyed()) settingsWin.destroy();
239
+ if (!chatWin.isDestroyed()) chatWin.destroy();
240
+ if (!avatarWin.isDestroyed()) avatarWin.destroy();
241
+ }
242
+
243
+ // Reposition chat when avatar moves
244
+ avatarWin.on("moved", () => {
245
+ if (chatVisible) repositionChat();
246
+ });
247
+
248
+ // Toggle chat from avatar renderer
249
+ ipcMain.on(IPC.TOGGLE_CHAT, () => {
250
+ toggleChat();
251
+ });
252
+
253
+ // Chat window click-through toggle
254
+ ipcMain.on(IPC.SET_IGNORE_MOUSE_CHAT, (_event, ignore: unknown) => {
255
+ if (typeof ignore !== "boolean" || chatWin.isDestroyed()) return;
256
+ chatWin.setIgnoreMouseEvents(ignore, { forward: true });
257
+ });
258
+
259
+ // Chat content hidden (idle fade) → hide the BrowserWindow
260
+ ipcMain.on(IPC.CHAT_CONTENT_HIDDEN, () => {
261
+ hideChat();
262
+ });
263
+
264
+ // Chat content shown → ensure window is visible
265
+ ipcMain.on(IPC.CHAT_CONTENT_SHOWN, () => {
266
+ if (!chatVisible) showChat();
267
+ });
268
+
269
+ // Settings window open/close from renderer
270
+ ipcMain.on(IPC.OPEN_SETTINGS, () => {
271
+ showSettings();
272
+ });
273
+
274
+ ipcMain.on(IPC.CLOSE_SETTINGS, () => {
275
+ hideSettings();
276
+ });
277
+
278
+ return {
279
+ get avatarWin() { return avatarWin; },
280
+ get chatWin() { return chatWin; },
281
+ get settingsWin() { return settingsWin; },
282
+ get chatVisible() { return chatVisible; },
283
+ toggleChat,
284
+ showChat,
285
+ hideChat,
286
+ repositionChat,
287
+ showSettings,
288
+ hideSettings,
289
+ toggleSettings,
290
+ hideAll,
291
+ showAvatar,
292
+ sendAgentState,
293
+ sendToAvatar,
294
+ sendToChat,
295
+ sendToSettings,
296
+ destroyAll,
297
+ };
298
+ }