@agentprojectcontext/apx 1.15.6 → 1.16.0

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 (221) hide show
  1. package/package.json +40 -5
  2. package/src/cli/commands/log.js +113 -0
  3. package/src/cli/commands/overlay.js +253 -0
  4. package/src/cli/commands/sys.js +88 -16
  5. package/src/cli/index.js +23 -1
  6. package/src/cli/terminal-chat/renderer.js +71 -56
  7. package/src/cli-ts/commands/agent.ts +173 -0
  8. package/src/cli-ts/commands/chat.ts +119 -0
  9. package/src/cli-ts/commands/daemon.ts +112 -0
  10. package/src/cli-ts/commands/exec.ts +109 -0
  11. package/src/cli-ts/commands/mcp.ts +235 -0
  12. package/src/cli-ts/commands/session.ts +224 -0
  13. package/src/cli-ts/commands/status.ts +61 -0
  14. package/src/cli-ts/http.ts +36 -0
  15. package/src/cli-ts/index.ts +73 -0
  16. package/src/cli-ts/ui.ts +107 -0
  17. package/src/core/logging.js +81 -0
  18. package/src/daemon/api.js +58 -0
  19. package/src/daemon/engines/anthropic.js +60 -1
  20. package/src/daemon/engines/index.js +2 -1
  21. package/src/daemon/engines/ollama.js +70 -3
  22. package/src/daemon/index.js +58 -0
  23. package/src/daemon/overlay-ws.js +40 -0
  24. package/src/daemon/plugins/index.js +2 -1
  25. package/src/daemon/plugins/overlay.js +177 -0
  26. package/src/daemon/plugins/telegram.js +15 -3
  27. package/src/daemon/super-agent.js +102 -19
  28. package/src/daemon/transcription.js +262 -59
  29. package/src/daemon/whisper-server.py +57 -6
  30. package/src/overlay/index.html +44 -0
  31. package/src/overlay/main.js +480 -0
  32. package/src/overlay/package.json +3 -0
  33. package/src/overlay/preload.js +34 -0
  34. package/src/overlay/renderer.js +371 -0
  35. package/src/overlay/style.css +250 -0
  36. package/src/tui/_shims/cli-error.ts +6 -0
  37. package/src/tui/_shims/cli-logo.ts +18 -0
  38. package/src/tui/_shims/cli-ui.ts +1 -0
  39. package/src/tui/_shims/config-console-state.ts +7 -0
  40. package/src/tui/_shims/core-any.ts +30 -0
  41. package/src/tui/_shims/core-binary.ts +13 -0
  42. package/src/tui/_shims/core-flag.ts +3 -0
  43. package/src/tui/_shims/core-log.ts +14 -0
  44. package/src/tui/_shims/lsp-language.ts +1 -0
  45. package/src/tui/_shims/opencode-any.ts +135 -0
  46. package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
  47. package/src/tui/_shims/plugin-tui.ts +13 -0
  48. package/src/tui/_shims/provider-provider.ts +10 -0
  49. package/src/tui/_shims/session-retry.ts +1 -0
  50. package/src/tui/_shims/session-schema.ts +15 -0
  51. package/src/tui/_shims/session-session.ts +3 -0
  52. package/src/tui/_shims/snapshot.ts +4 -0
  53. package/src/tui/_shims/tool-any.ts +18 -0
  54. package/src/tui/_shims/util-error.ts +7 -0
  55. package/src/tui/_shims/util-filesystem.ts +79 -0
  56. package/src/tui/_shims/util-format.ts +7 -0
  57. package/src/tui/_shims/util-iife.ts +3 -0
  58. package/src/tui/_shims/util-locale.ts +10 -0
  59. package/src/tui/_shims/util-process.ts +38 -0
  60. package/src/tui/app.tsx +783 -0
  61. package/src/tui/asset/charge.wav +0 -0
  62. package/src/tui/asset/pulse-a.wav +0 -0
  63. package/src/tui/asset/pulse-b.wav +0 -0
  64. package/src/tui/asset/pulse-c.wav +0 -0
  65. package/src/tui/attach.ts +100 -0
  66. package/src/tui/component/bg-pulse-render.ts +436 -0
  67. package/src/tui/component/bg-pulse.tsx +99 -0
  68. package/src/tui/component/border.tsx +21 -0
  69. package/src/tui/component/dialog-agent.tsx +31 -0
  70. package/src/tui/component/dialog-console-org.tsx +103 -0
  71. package/src/tui/component/dialog-mcp.tsx +85 -0
  72. package/src/tui/component/dialog-model.tsx +175 -0
  73. package/src/tui/component/dialog-provider.tsx +456 -0
  74. package/src/tui/component/dialog-retry-action.tsx +160 -0
  75. package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
  76. package/src/tui/component/dialog-session-list.tsx +323 -0
  77. package/src/tui/component/dialog-session-rename.tsx +31 -0
  78. package/src/tui/component/dialog-skill.tsx +36 -0
  79. package/src/tui/component/dialog-stash.tsx +87 -0
  80. package/src/tui/component/dialog-status.tsx +168 -0
  81. package/src/tui/component/dialog-tag.tsx +44 -0
  82. package/src/tui/component/dialog-theme-list.tsx +50 -0
  83. package/src/tui/component/dialog-variant.tsx +39 -0
  84. package/src/tui/component/dialog-workspace-create.tsx +302 -0
  85. package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
  86. package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
  87. package/src/tui/component/error-component.tsx +92 -0
  88. package/src/tui/component/logo.tsx +896 -0
  89. package/src/tui/component/plugin-route-missing.tsx +14 -0
  90. package/src/tui/component/prompt/autocomplete.tsx +869 -0
  91. package/src/tui/component/prompt/cwd.ts +0 -0
  92. package/src/tui/component/prompt/frecency.tsx +90 -0
  93. package/src/tui/component/prompt/history.tsx +108 -0
  94. package/src/tui/component/prompt/index.tsx +1809 -0
  95. package/src/tui/component/prompt/part.ts +16 -0
  96. package/src/tui/component/prompt/stash.tsx +101 -0
  97. package/src/tui/component/prompt/traits.ts +35 -0
  98. package/src/tui/component/spinner.tsx +24 -0
  99. package/src/tui/component/startup-loading.tsx +63 -0
  100. package/src/tui/component/todo-item.tsx +32 -0
  101. package/src/tui/component/use-connected.tsx +9 -0
  102. package/src/tui/component/workspace-label.tsx +19 -0
  103. package/src/tui/config/cwd.ts +5 -0
  104. package/src/tui/config/keybind.ts +432 -0
  105. package/src/tui/config/tui-migrate.ts +154 -0
  106. package/src/tui/config/tui-schema.ts +34 -0
  107. package/src/tui/config/tui.ts +46 -0
  108. package/src/tui/context/aggregate-failures.ts +34 -0
  109. package/src/tui/context/args.tsx +15 -0
  110. package/src/tui/context/command-palette.tsx +163 -0
  111. package/src/tui/context/directory.ts +15 -0
  112. package/src/tui/context/editor-zed.ts +283 -0
  113. package/src/tui/context/editor.ts +468 -0
  114. package/src/tui/context/event-apx.ts +22 -0
  115. package/src/tui/context/event.ts +6 -0
  116. package/src/tui/context/exit.tsx +60 -0
  117. package/src/tui/context/helper.tsx +25 -0
  118. package/src/tui/context/kv.tsx +81 -0
  119. package/src/tui/context/local.tsx +608 -0
  120. package/src/tui/context/path-format.tsx +39 -0
  121. package/src/tui/context/project-apx.tsx +48 -0
  122. package/src/tui/context/project.tsx +7 -0
  123. package/src/tui/context/prompt.tsx +18 -0
  124. package/src/tui/context/route.tsx +52 -0
  125. package/src/tui/context/sdk-apx.tsx +185 -0
  126. package/src/tui/context/sdk.tsx +6 -0
  127. package/src/tui/context/sync-apx.tsx +178 -0
  128. package/src/tui/context/sync-v2.tsx +16 -0
  129. package/src/tui/context/sync.tsx +118 -0
  130. package/src/tui/context/theme/aura.json +69 -0
  131. package/src/tui/context/theme/ayu.json +80 -0
  132. package/src/tui/context/theme/carbonfox.json +248 -0
  133. package/src/tui/context/theme/catppuccin-frappe.json +230 -0
  134. package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
  135. package/src/tui/context/theme/catppuccin.json +112 -0
  136. package/src/tui/context/theme/cobalt2.json +225 -0
  137. package/src/tui/context/theme/cursor.json +249 -0
  138. package/src/tui/context/theme/dracula.json +219 -0
  139. package/src/tui/context/theme/everforest.json +241 -0
  140. package/src/tui/context/theme/flexoki.json +237 -0
  141. package/src/tui/context/theme/github.json +233 -0
  142. package/src/tui/context/theme/gruvbox.json +242 -0
  143. package/src/tui/context/theme/kanagawa.json +77 -0
  144. package/src/tui/context/theme/lucent-orng.json +234 -0
  145. package/src/tui/context/theme/material.json +235 -0
  146. package/src/tui/context/theme/matrix.json +77 -0
  147. package/src/tui/context/theme/mercury.json +252 -0
  148. package/src/tui/context/theme/monokai.json +221 -0
  149. package/src/tui/context/theme/nightowl.json +221 -0
  150. package/src/tui/context/theme/nord.json +223 -0
  151. package/src/tui/context/theme/one-dark.json +84 -0
  152. package/src/tui/context/theme/opencode.json +245 -0
  153. package/src/tui/context/theme/orng.json +249 -0
  154. package/src/tui/context/theme/osaka-jade.json +93 -0
  155. package/src/tui/context/theme/palenight.json +222 -0
  156. package/src/tui/context/theme/rosepine.json +234 -0
  157. package/src/tui/context/theme/solarized.json +223 -0
  158. package/src/tui/context/theme/synthwave84.json +226 -0
  159. package/src/tui/context/theme/tokyonight.json +243 -0
  160. package/src/tui/context/theme/vercel.json +245 -0
  161. package/src/tui/context/theme/vesper.json +218 -0
  162. package/src/tui/context/theme/zenburn.json +223 -0
  163. package/src/tui/context/theme.tsx +1247 -0
  164. package/src/tui/context/tui-config.tsx +9 -0
  165. package/src/tui/event.ts +16 -0
  166. package/src/tui/feature-plugins/home/footer.tsx +94 -0
  167. package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
  168. package/src/tui/feature-plugins/home/tips.tsx +59 -0
  169. package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
  170. package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
  171. package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
  172. package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
  173. package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
  174. package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
  175. package/src/tui/feature-plugins/system/plugins.tsx +269 -0
  176. package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
  177. package/src/tui/feature-plugins/system/which-key.tsx +608 -0
  178. package/src/tui/keymap.tsx +166 -0
  179. package/src/tui/layer.ts +6 -0
  180. package/src/tui/plugin/api.tsx +381 -0
  181. package/src/tui/plugin/command-shim.ts +109 -0
  182. package/src/tui/plugin/internal.ts +33 -0
  183. package/src/tui/plugin/runtime.ts +1069 -0
  184. package/src/tui/plugin/slots.tsx +60 -0
  185. package/src/tui/routes/home.tsx +96 -0
  186. package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
  187. package/src/tui/routes/session/dialog-message.tsx +108 -0
  188. package/src/tui/routes/session/dialog-subagent.tsx +26 -0
  189. package/src/tui/routes/session/dialog-timeline.tsx +47 -0
  190. package/src/tui/routes/session/footer.tsx +91 -0
  191. package/src/tui/routes/session/index.tsx +188 -0
  192. package/src/tui/routes/session/permission.tsx +722 -0
  193. package/src/tui/routes/session/question.tsx +490 -0
  194. package/src/tui/routes/session/sidebar.tsx +102 -0
  195. package/src/tui/routes/session/subagent-footer.tsx +133 -0
  196. package/src/tui/run.ts +84 -0
  197. package/src/tui/thread.ts +261 -0
  198. package/src/tui/tsconfig.json +40 -0
  199. package/src/tui/ui/dialog-alert.tsx +66 -0
  200. package/src/tui/ui/dialog-confirm.tsx +108 -0
  201. package/src/tui/ui/dialog-export-options.tsx +217 -0
  202. package/src/tui/ui/dialog-help.tsx +40 -0
  203. package/src/tui/ui/dialog-prompt.tsx +101 -0
  204. package/src/tui/ui/dialog-select.tsx +553 -0
  205. package/src/tui/ui/dialog.tsx +211 -0
  206. package/src/tui/ui/link.tsx +34 -0
  207. package/src/tui/ui/spinner.ts +368 -0
  208. package/src/tui/ui/toast.tsx +111 -0
  209. package/src/tui/util/clipboard.ts +217 -0
  210. package/src/tui/util/editor.ts +37 -0
  211. package/src/tui/util/model.ts +23 -0
  212. package/src/tui/util/provider-origin.ts +7 -0
  213. package/src/tui/util/revert-diff.ts +18 -0
  214. package/src/tui/util/scroll.ts +25 -0
  215. package/src/tui/util/selection.ts +65 -0
  216. package/src/tui/util/signal.ts +41 -0
  217. package/src/tui/util/sound.ts +156 -0
  218. package/src/tui/util/transcript.ts +112 -0
  219. package/src/tui/validate-session.ts +29 -0
  220. package/src/tui/win32.ts +130 -0
  221. package/src/tui/worker.ts +104 -0
@@ -0,0 +1,480 @@
1
+ // APX Overlay — Electron main process.
2
+ // Provides: system tray icon, configurable global shortcut, transparent
3
+ // floating chat window, WebSocket connection to APX daemon.
4
+ //
5
+ // Default shortcut: Cmd+Shift+\ (Mac) / Ctrl+Shift+\ (Win/Linux).
6
+ // Override in ~/.apx/config.json: "overlay": { "shortcut": "CommandOrControl+Shift+Space" }
7
+ //
8
+ // Launch via: electron src/overlay/main.js [--port 7430] [--shortcut <accel>]
9
+ // Or via: apx overlay start
10
+
11
+ "use strict";
12
+ const { app, BrowserWindow, Tray, globalShortcut, ipcMain, nativeImage, screen, Menu } = require("electron");
13
+ const path = require("path");
14
+ const os = require("os");
15
+ const fs = require("fs");
16
+ const http = require("http");
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Config from CLI args or env
20
+ // ---------------------------------------------------------------------------
21
+
22
+ const args = process.argv.slice(2);
23
+ function getArg(name) {
24
+ const i = args.indexOf(name);
25
+ return i !== -1 ? args[i + 1] : null;
26
+ }
27
+
28
+ const DAEMON_PORT = parseInt(getArg("--port") || process.env.APX_PORT || "7430", 10);
29
+ const DAEMON_HOST = getArg("--host") || process.env.APX_HOST || "127.0.0.1";
30
+ const WHISPER_PORT = 18765;
31
+ const TOKEN_PATH = path.join(os.homedir(), ".apx", "daemon.token");
32
+ const CONFIG_PATH = path.join(os.homedir(), ".apx", "config.json");
33
+
34
+ // Default shortcut: Cmd/Ctrl + Shift + \ (backslash — rarely used by other apps)
35
+ // User can override via config overlay.shortcut or --shortcut CLI arg.
36
+ const DEFAULT_SHORTCUT = "CommandOrControl+G";
37
+
38
+ function readApxConfig() {
39
+ try { return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8")); } catch { return {}; }
40
+ }
41
+
42
+ function getShortcut() {
43
+ const fromArg = getArg("--shortcut");
44
+ if (fromArg) return fromArg;
45
+ const cfg = readApxConfig();
46
+ return cfg?.overlay?.shortcut || DEFAULT_SHORTCUT;
47
+ }
48
+
49
+ function readToken() {
50
+ try { return fs.readFileSync(TOKEN_PATH, "utf8").trim(); } catch { return ""; }
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Window size + position helpers
55
+ // ---------------------------------------------------------------------------
56
+
57
+ const WIN_W = 420;
58
+ const WIN_H = 560;
59
+
60
+ function getWindowPosition() {
61
+ const display = screen.getPrimaryDisplay();
62
+ const { workArea } = display;
63
+ const isMac = process.platform === "darwin";
64
+ if (isMac) {
65
+ // Top-right, below menu bar (workArea.y already accounts for it)
66
+ return { x: workArea.x + workArea.width - WIN_W - 12, y: workArea.y + 8 };
67
+ }
68
+ // Windows/Linux: bottom-right, above taskbar
69
+ return {
70
+ x: workArea.x + workArea.width - WIN_W - 12,
71
+ y: workArea.y + workArea.height - WIN_H - 12,
72
+ };
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Globals
77
+ // ---------------------------------------------------------------------------
78
+
79
+ let mainWindow = null;
80
+ let tray = null;
81
+ let wsConn = null; // WebSocket to daemon
82
+ let isRecording = false;
83
+ let overlayVisible = false;
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // App lifecycle
87
+ // ---------------------------------------------------------------------------
88
+
89
+ // On macOS, don't show in the dock — it's a tray-only utility
90
+ if (process.platform === "darwin") app.dock?.hide();
91
+
92
+ app.whenReady().then(() => {
93
+ console.log(`overlay: starting — daemon ${DAEMON_HOST}:${DAEMON_PORT} — pid ${process.pid}`);
94
+ try { createTray(); console.log("overlay: tray created"); }
95
+ catch (e) { console.error("overlay: createTray failed:", e.message); }
96
+ try { createWindow(); console.log("overlay: window created"); }
97
+ catch (e) { console.error("overlay: createWindow failed:", e.message); }
98
+ try { registerShortcut(); }
99
+ catch (e) { console.error("overlay: registerShortcut failed:", e.message); }
100
+ connectDaemon();
101
+ });
102
+
103
+ process.on("uncaughtException", (e) => {
104
+ console.error("overlay: uncaught exception:", e.stack || e.message);
105
+ });
106
+
107
+ app.on("window-all-closed", (e) => {
108
+ // Prevent app from quitting when window closes — keep in tray
109
+ e.preventDefault?.();
110
+ });
111
+
112
+ app.on("will-quit", () => {
113
+ globalShortcut.unregisterAll();
114
+ });
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Tray
118
+ // ---------------------------------------------------------------------------
119
+
120
+ // Minimal valid 16x16 transparent PNG (base64).
121
+ // macOS requires a real PNG for Tray — raw RGBA buffers are not accepted.
122
+ // We use setTitle() to display the visible symbol in the menu bar.
123
+ const ICON_TRANSPARENT_PNG =
124
+ "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCS" +
125
+ "VQICAgIfAhkiAAAAAlwSFlzAAALEwAACxMBAJqcGAAAABl0RVh0U29" +
126
+ "mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAAOSURBVDiNY2Bg" +
127
+ "YPgPAAEEAQABZQMuAAAAAElFTkSuQmCC";
128
+
129
+ // Red dot PNG for recording state (16x16 solid red circle).
130
+ const ICON_RED_PNG =
131
+ "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAASUlEQVQ4" +
132
+ "y2NgYGD4z0ABYBpFkKoFVAMDA8N/CiygWgMDA8N/qgygWgMDA8N/og" +
133
+ "ygSgMDA8N/igygWgMDA8N/BgYGBgCZJCULAAAAAElFTkSuQmCC";
134
+
135
+ function buildTrayIcon(recording) {
136
+ const b64 = recording ? ICON_RED_PNG : ICON_TRANSPARENT_PNG;
137
+ const img = nativeImage.createFromDataURL(`data:image/png;base64,${b64}`);
138
+ // Template image: macOS auto-adapts colour to dark/light menu bar
139
+ if (process.platform === "darwin") img.setTemplateImage(!recording);
140
+ return img;
141
+ }
142
+
143
+ function createTray() {
144
+ const icon = buildTrayIcon(false);
145
+ tray = new Tray(icon);
146
+
147
+ // On macOS, setTitle shows text right in the menu bar (most visible approach)
148
+ if (process.platform === "darwin") tray.setTitle(" ◉");
149
+
150
+ tray.setToolTip("APX Voice Overlay — click to toggle, right-click for menu");
151
+
152
+ const contextMenu = Menu.buildFromTemplate([
153
+ { label: "Show / Hide", click: toggleWindow },
154
+ { label: "Start Recording", click: startRecording },
155
+ { type: "separator" },
156
+ { label: "Quit APX Overlay", click: () => app.exit(0) },
157
+ ]);
158
+ tray.setContextMenu(contextMenu);
159
+ tray.on("click", toggleWindow);
160
+ }
161
+
162
+ function updateTrayRecording(rec) {
163
+ if (!tray) return;
164
+ tray.setImage(buildTrayIcon(rec));
165
+ if (process.platform === "darwin") tray.setTitle(rec ? " ⏺" : " ◉");
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // Window
170
+ // ---------------------------------------------------------------------------
171
+
172
+ function createWindow() {
173
+ const pos = getWindowPosition();
174
+ mainWindow = new BrowserWindow({
175
+ width: WIN_W,
176
+ height: WIN_H,
177
+ x: pos.x,
178
+ y: pos.y,
179
+ frame: false,
180
+ transparent: true,
181
+ alwaysOnTop: true,
182
+ skipTaskbar: true,
183
+ resizable: false,
184
+ hasShadow: false,
185
+ show: false,
186
+ focusable: true,
187
+ webPreferences: {
188
+ preload: path.join(__dirname, "preload.js"),
189
+ contextIsolation: true,
190
+ nodeIntegration: false,
191
+ // Allow getUserMedia for microphone access
192
+ allowRunningInsecureContent: false,
193
+ },
194
+ });
195
+
196
+ mainWindow.loadFile(path.join(__dirname, "index.html"));
197
+
198
+ mainWindow.on("blur", () => {
199
+ // Don't auto-hide while recording or streaming
200
+ if (!isRecording) {
201
+ setTimeout(() => {
202
+ if (mainWindow && !mainWindow.isFocused() && !isRecording) {
203
+ // Keep visible — user might be reading the response
204
+ }
205
+ }, 200);
206
+ }
207
+ });
208
+
209
+ mainWindow.on("closed", () => { mainWindow = null; });
210
+
211
+ // ESC key handled in renderer via preload
212
+ }
213
+
214
+ function toggleWindow() {
215
+ if (!mainWindow) { createWindow(); return; }
216
+ if (overlayVisible) {
217
+ hideOverlay();
218
+ } else {
219
+ showOverlay();
220
+ }
221
+ }
222
+
223
+ function showOverlay() {
224
+ if (!mainWindow) createWindow();
225
+ const pos = getWindowPosition();
226
+ mainWindow.setPosition(pos.x, pos.y);
227
+ mainWindow.show();
228
+ mainWindow.focus();
229
+ overlayVisible = true;
230
+ }
231
+
232
+ function hideOverlay() {
233
+ if (mainWindow) mainWindow.hide();
234
+ overlayVisible = false;
235
+ if (isRecording) stopRecording();
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // Global shortcut: Cmd/Ctrl+Shift+Space toggles recording
240
+ // ---------------------------------------------------------------------------
241
+
242
+ function registerShortcut() {
243
+ const shortcut = getShortcut();
244
+ const ok = globalShortcut.register(shortcut, () => {
245
+ if (!overlayVisible) {
246
+ showOverlay();
247
+ // Auto-start recording when opening via shortcut
248
+ setTimeout(startRecording, 150);
249
+ } else if (isRecording) {
250
+ stopRecording();
251
+ } else {
252
+ startRecording();
253
+ }
254
+ });
255
+ if (!ok) {
256
+ console.error(`overlay: failed to register shortcut "${shortcut}". Try a different shortcut in ~/.apx/config.json: overlay.shortcut`);
257
+ } else {
258
+ console.log(`overlay: shortcut registered: ${shortcut}`);
259
+ }
260
+ }
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // Recording control
264
+ // ---------------------------------------------------------------------------
265
+
266
+ function startRecording() {
267
+ if (isRecording) return;
268
+ isRecording = true;
269
+ updateTrayRecording(true);
270
+ mainWindow?.webContents.send("recording-start");
271
+ }
272
+
273
+ function stopRecording() {
274
+ if (!isRecording) return;
275
+ isRecording = false;
276
+ updateTrayRecording(false);
277
+ mainWindow?.webContents.send("recording-stop");
278
+ }
279
+
280
+ // ---------------------------------------------------------------------------
281
+ // IPC handlers (renderer → main)
282
+ // ---------------------------------------------------------------------------
283
+
284
+ // Renderer sends audio chunk for transcription
285
+ ipcMain.handle("transcribe-chunk", async (_event, { buffer, format, language }) => {
286
+ try {
287
+ console.log(`overlay: transcribe chunk — ${buffer.byteLength}b ${format}`);
288
+ const result = await transcribeChunk(Buffer.from(buffer), format || "webm", language || "auto");
289
+ if (result?.ok) console.log(`overlay: transcribed → "${(result.text || "").slice(0, 80)}"`);
290
+ else console.error("overlay: transcription error:", result?.error);
291
+ return result;
292
+ } catch (e) {
293
+ console.error("overlay: transcribeChunk exception:", e.message);
294
+ return { ok: false, error: e.message };
295
+ }
296
+ });
297
+
298
+ // Renderer sends final transcribed text to daemon
299
+ ipcMain.handle("send-message", async (_event, { text, previousMessages }) => {
300
+ console.log(`overlay: send-message → "${text.slice(0, 80)}"`);
301
+ return sendMessageToDaemon(text, previousMessages || []);
302
+ });
303
+
304
+ // Renderer requests cancel
305
+ ipcMain.handle("cancel", async () => {
306
+ if (wsConn && wsConn.readyState === 1) {
307
+ wsConn.send(JSON.stringify({ type: "cancel" }));
308
+ }
309
+ stopRecording();
310
+ });
311
+
312
+ // Renderer requests close/hide
313
+ ipcMain.handle("close-overlay", async () => {
314
+ hideOverlay();
315
+ });
316
+
317
+ // Renderer queries the configured shortcut for display
318
+ ipcMain.handle("get-shortcut", () => getShortcut());
319
+
320
+ // Check if the whisper server is running and the model is loaded
321
+ ipcMain.handle("check-whisper-ready", () => {
322
+ return new Promise((resolve) => {
323
+ const options = {
324
+ hostname: "127.0.0.1",
325
+ port: WHISPER_PORT,
326
+ path: "/health",
327
+ method: "GET",
328
+ };
329
+ const req = http.request(options, (res) => {
330
+ let data = "";
331
+ res.on("data", (c) => data += c);
332
+ res.on("end", () => {
333
+ try {
334
+ const json = JSON.parse(data);
335
+ resolve({ ready: json.ok && json.loaded === true });
336
+ } catch {
337
+ resolve({ ready: false });
338
+ }
339
+ });
340
+ });
341
+ req.on("error", () => resolve({ ready: false }));
342
+ req.setTimeout(800, () => { req.destroy(); resolve({ ready: false }); });
343
+ req.end();
344
+ });
345
+ });
346
+
347
+ // Renderer requests recording toggle (ESC cancels, shortcut toggles)
348
+ ipcMain.handle("toggle-recording", async () => {
349
+ if (isRecording) stopRecording(); else startRecording();
350
+ });
351
+
352
+ // ---------------------------------------------------------------------------
353
+ // Whisper chunk transcription — proxied through the daemon (auto-starts whisper server)
354
+ // ---------------------------------------------------------------------------
355
+
356
+ function transcribeChunk(buf, format, language) {
357
+ return new Promise((resolve, reject) => {
358
+ const token = readToken();
359
+ const options = {
360
+ hostname: DAEMON_HOST,
361
+ port: DAEMON_PORT,
362
+ path: "/transcribe/chunk",
363
+ method: "POST",
364
+ headers: {
365
+ "Content-Type": "application/octet-stream",
366
+ "Content-Length": buf.length,
367
+ "X-Audio-Format": format,
368
+ "X-Language": language,
369
+ // Overlay is real-time → local whisper only. Never fall back to OpenAI.
370
+ "X-Provider": "local",
371
+ ...(token ? { "Authorization": `Bearer ${token}` } : {}),
372
+ },
373
+ };
374
+ const req = http.request(options, (res) => {
375
+ let data = "";
376
+ res.on("data", (c) => data += c);
377
+ res.on("end", () => {
378
+ try { resolve(JSON.parse(data)); }
379
+ catch { reject(new Error("bad json from daemon")); }
380
+ });
381
+ });
382
+ req.on("error", reject);
383
+ req.setTimeout(30000, () => { req.destroy(); reject(new Error("transcription timeout")); });
384
+ req.write(buf);
385
+ req.end();
386
+ });
387
+ }
388
+
389
+ // ---------------------------------------------------------------------------
390
+ // Daemon communication
391
+ // ---------------------------------------------------------------------------
392
+
393
+ function connectDaemon() {
394
+ // Lazy-load ws from the APX node_modules (co-located)
395
+ let WS;
396
+ try {
397
+ WS = require("ws");
398
+ } catch {
399
+ console.warn("overlay: 'ws' module not found — WebSocket disabled. Install with: npm install ws");
400
+ return;
401
+ }
402
+
403
+ const token = readToken();
404
+ const url = `ws://${DAEMON_HOST}:${DAEMON_PORT}/overlay/ws`;
405
+
406
+ function connect() {
407
+ try {
408
+ wsConn = new WS(url, {
409
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
410
+ });
411
+
412
+ wsConn.on("open", () => {
413
+ console.log("overlay: connected to daemon");
414
+ resetReconnectDelay();
415
+ mainWindow?.webContents.send("daemon-connected");
416
+ });
417
+
418
+ wsConn.on("message", (raw) => {
419
+ let msg;
420
+ try { msg = JSON.parse(raw.toString()); } catch { return; }
421
+ // Forward all daemon events to the renderer
422
+ mainWindow?.webContents.send("daemon-event", msg);
423
+ });
424
+
425
+ wsConn.on("close", () => {
426
+ wsConn = null;
427
+ mainWindow?.webContents.send("daemon-disconnected");
428
+ scheduleReconnect();
429
+ });
430
+
431
+ wsConn.on("error", (e) => {
432
+ console.warn("overlay ws error:", e.message);
433
+ });
434
+ } catch (e) {
435
+ console.warn("overlay: connect failed —", e.message);
436
+ scheduleReconnect();
437
+ }
438
+ }
439
+
440
+ // Exponential backoff with cap: 1s → 2s → 4s → … → 30s. Resets to 1s
441
+ // after a successful open() (see below).
442
+ let reconnectDelay = 1000;
443
+ function scheduleReconnect() {
444
+ const delay = reconnectDelay;
445
+ reconnectDelay = Math.min(reconnectDelay * 2, 30000);
446
+ setTimeout(connect, delay);
447
+ }
448
+ function resetReconnectDelay() { reconnectDelay = 1000; }
449
+
450
+ connect();
451
+ }
452
+
453
+ async function sendMessageToDaemon(text, previousMessages) {
454
+ const token = readToken();
455
+ return new Promise((resolve, reject) => {
456
+ const body = JSON.stringify({ text, previousMessages });
457
+ const options = {
458
+ hostname: DAEMON_HOST,
459
+ port: DAEMON_PORT,
460
+ path: "/overlay/message",
461
+ method: "POST",
462
+ headers: {
463
+ "Content-Type": "application/json",
464
+ "Content-Length": Buffer.byteLength(body),
465
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
466
+ },
467
+ };
468
+ const req = http.request(options, (res) => {
469
+ let data = "";
470
+ res.on("data", (c) => data += c);
471
+ res.on("end", () => {
472
+ try { resolve(JSON.parse(data)); }
473
+ catch { resolve({ ok: true }); }
474
+ });
475
+ });
476
+ req.on("error", reject);
477
+ req.write(body);
478
+ req.end();
479
+ });
480
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,34 @@
1
+ // Preload — context bridge between Electron main and renderer.
2
+ "use strict";
3
+ const { contextBridge, ipcRenderer } = require("electron");
4
+
5
+ contextBridge.exposeInMainWorld("apx", {
6
+ // Recording control
7
+ onRecordingStart: (fn) => ipcRenderer.on("recording-start", fn),
8
+ onRecordingStop: (fn) => ipcRenderer.on("recording-stop", fn),
9
+ toggleRecording: () => ipcRenderer.invoke("toggle-recording"),
10
+ cancel: () => ipcRenderer.invoke("cancel"),
11
+ close: () => ipcRenderer.invoke("close-overlay"),
12
+
13
+ // Transcription
14
+ transcribeChunk: (buffer, format, language) =>
15
+ ipcRenderer.invoke("transcribe-chunk", { buffer, format, language }),
16
+
17
+ // Check if the whisper model is loaded (false = still loading)
18
+ checkWhisperReady: () => ipcRenderer.invoke("check-whisper-ready"),
19
+
20
+ // Send final text to daemon
21
+ sendMessage: (text, previousMessages) =>
22
+ ipcRenderer.invoke("send-message", { text, previousMessages }),
23
+
24
+ // Daemon events (tokens, tools, done, error)
25
+ onDaemonEvent: (fn) => ipcRenderer.on("daemon-event", (_e, msg) => fn(msg)),
26
+ onDaemonConnected: (fn) => ipcRenderer.on("daemon-connected", fn),
27
+ onDaemonDisconnected: (fn) => ipcRenderer.on("daemon-disconnected", fn),
28
+
29
+ // Platform info
30
+ platform: process.platform,
31
+
32
+ // Query configured shortcut for display in hint bar
33
+ getShortcut: () => ipcRenderer.invoke("get-shortcut"),
34
+ });