@agentprojectcontext/apx 1.15.5 → 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.
- package/package.json +40 -5
- package/src/cli/commands/log.js +113 -0
- package/src/cli/commands/overlay.js +253 -0
- package/src/cli/commands/sys.js +88 -16
- package/src/cli/index.js +23 -1
- package/src/cli/terminal-chat/renderer.js +71 -56
- package/src/cli-ts/commands/agent.ts +173 -0
- package/src/cli-ts/commands/chat.ts +119 -0
- package/src/cli-ts/commands/daemon.ts +112 -0
- package/src/cli-ts/commands/exec.ts +109 -0
- package/src/cli-ts/commands/mcp.ts +235 -0
- package/src/cli-ts/commands/session.ts +224 -0
- package/src/cli-ts/commands/status.ts +61 -0
- package/src/cli-ts/http.ts +36 -0
- package/src/cli-ts/index.ts +73 -0
- package/src/cli-ts/ui.ts +107 -0
- package/src/core/logging.js +81 -0
- package/src/daemon/api.js +58 -0
- package/src/daemon/engines/anthropic.js +60 -1
- package/src/daemon/engines/index.js +2 -1
- package/src/daemon/engines/ollama.js +70 -3
- package/src/daemon/index.js +58 -0
- package/src/daemon/overlay-ws.js +40 -0
- package/src/daemon/plugins/index.js +2 -1
- package/src/daemon/plugins/overlay.js +177 -0
- package/src/daemon/plugins/telegram.js +15 -3
- package/src/daemon/super-agent.js +102 -19
- package/src/daemon/transcription.js +262 -59
- package/src/daemon/wakeup.js +14 -19
- package/src/daemon/whisper-server.py +57 -6
- package/src/overlay/index.html +44 -0
- package/src/overlay/main.js +480 -0
- package/src/overlay/package.json +3 -0
- package/src/overlay/preload.js +34 -0
- package/src/overlay/renderer.js +371 -0
- package/src/overlay/style.css +250 -0
- package/src/tui/_shims/cli-error.ts +6 -0
- package/src/tui/_shims/cli-logo.ts +18 -0
- package/src/tui/_shims/cli-ui.ts +1 -0
- package/src/tui/_shims/config-console-state.ts +7 -0
- package/src/tui/_shims/core-any.ts +30 -0
- package/src/tui/_shims/core-binary.ts +13 -0
- package/src/tui/_shims/core-flag.ts +3 -0
- package/src/tui/_shims/core-log.ts +14 -0
- package/src/tui/_shims/lsp-language.ts +1 -0
- package/src/tui/_shims/opencode-any.ts +135 -0
- package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
- package/src/tui/_shims/plugin-tui.ts +13 -0
- package/src/tui/_shims/provider-provider.ts +10 -0
- package/src/tui/_shims/session-retry.ts +1 -0
- package/src/tui/_shims/session-schema.ts +15 -0
- package/src/tui/_shims/session-session.ts +3 -0
- package/src/tui/_shims/snapshot.ts +4 -0
- package/src/tui/_shims/tool-any.ts +18 -0
- package/src/tui/_shims/util-error.ts +7 -0
- package/src/tui/_shims/util-filesystem.ts +79 -0
- package/src/tui/_shims/util-format.ts +7 -0
- package/src/tui/_shims/util-iife.ts +3 -0
- package/src/tui/_shims/util-locale.ts +10 -0
- package/src/tui/_shims/util-process.ts +38 -0
- package/src/tui/app.tsx +783 -0
- package/src/tui/asset/charge.wav +0 -0
- package/src/tui/asset/pulse-a.wav +0 -0
- package/src/tui/asset/pulse-b.wav +0 -0
- package/src/tui/asset/pulse-c.wav +0 -0
- package/src/tui/attach.ts +100 -0
- package/src/tui/component/bg-pulse-render.ts +436 -0
- package/src/tui/component/bg-pulse.tsx +99 -0
- package/src/tui/component/border.tsx +21 -0
- package/src/tui/component/dialog-agent.tsx +31 -0
- package/src/tui/component/dialog-console-org.tsx +103 -0
- package/src/tui/component/dialog-mcp.tsx +85 -0
- package/src/tui/component/dialog-model.tsx +175 -0
- package/src/tui/component/dialog-provider.tsx +456 -0
- package/src/tui/component/dialog-retry-action.tsx +160 -0
- package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
- package/src/tui/component/dialog-session-list.tsx +323 -0
- package/src/tui/component/dialog-session-rename.tsx +31 -0
- package/src/tui/component/dialog-skill.tsx +36 -0
- package/src/tui/component/dialog-stash.tsx +87 -0
- package/src/tui/component/dialog-status.tsx +168 -0
- package/src/tui/component/dialog-tag.tsx +44 -0
- package/src/tui/component/dialog-theme-list.tsx +50 -0
- package/src/tui/component/dialog-variant.tsx +39 -0
- package/src/tui/component/dialog-workspace-create.tsx +302 -0
- package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
- package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
- package/src/tui/component/error-component.tsx +92 -0
- package/src/tui/component/logo.tsx +896 -0
- package/src/tui/component/plugin-route-missing.tsx +14 -0
- package/src/tui/component/prompt/autocomplete.tsx +869 -0
- package/src/tui/component/prompt/cwd.ts +0 -0
- package/src/tui/component/prompt/frecency.tsx +90 -0
- package/src/tui/component/prompt/history.tsx +108 -0
- package/src/tui/component/prompt/index.tsx +1809 -0
- package/src/tui/component/prompt/part.ts +16 -0
- package/src/tui/component/prompt/stash.tsx +101 -0
- package/src/tui/component/prompt/traits.ts +35 -0
- package/src/tui/component/spinner.tsx +24 -0
- package/src/tui/component/startup-loading.tsx +63 -0
- package/src/tui/component/todo-item.tsx +32 -0
- package/src/tui/component/use-connected.tsx +9 -0
- package/src/tui/component/workspace-label.tsx +19 -0
- package/src/tui/config/cwd.ts +5 -0
- package/src/tui/config/keybind.ts +432 -0
- package/src/tui/config/tui-migrate.ts +154 -0
- package/src/tui/config/tui-schema.ts +34 -0
- package/src/tui/config/tui.ts +46 -0
- package/src/tui/context/aggregate-failures.ts +34 -0
- package/src/tui/context/args.tsx +15 -0
- package/src/tui/context/command-palette.tsx +163 -0
- package/src/tui/context/directory.ts +15 -0
- package/src/tui/context/editor-zed.ts +283 -0
- package/src/tui/context/editor.ts +468 -0
- package/src/tui/context/event-apx.ts +22 -0
- package/src/tui/context/event.ts +6 -0
- package/src/tui/context/exit.tsx +60 -0
- package/src/tui/context/helper.tsx +25 -0
- package/src/tui/context/kv.tsx +81 -0
- package/src/tui/context/local.tsx +608 -0
- package/src/tui/context/path-format.tsx +39 -0
- package/src/tui/context/project-apx.tsx +48 -0
- package/src/tui/context/project.tsx +7 -0
- package/src/tui/context/prompt.tsx +18 -0
- package/src/tui/context/route.tsx +52 -0
- package/src/tui/context/sdk-apx.tsx +185 -0
- package/src/tui/context/sdk.tsx +6 -0
- package/src/tui/context/sync-apx.tsx +178 -0
- package/src/tui/context/sync-v2.tsx +16 -0
- package/src/tui/context/sync.tsx +118 -0
- package/src/tui/context/theme/aura.json +69 -0
- package/src/tui/context/theme/ayu.json +80 -0
- package/src/tui/context/theme/carbonfox.json +248 -0
- package/src/tui/context/theme/catppuccin-frappe.json +230 -0
- package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
- package/src/tui/context/theme/catppuccin.json +112 -0
- package/src/tui/context/theme/cobalt2.json +225 -0
- package/src/tui/context/theme/cursor.json +249 -0
- package/src/tui/context/theme/dracula.json +219 -0
- package/src/tui/context/theme/everforest.json +241 -0
- package/src/tui/context/theme/flexoki.json +237 -0
- package/src/tui/context/theme/github.json +233 -0
- package/src/tui/context/theme/gruvbox.json +242 -0
- package/src/tui/context/theme/kanagawa.json +77 -0
- package/src/tui/context/theme/lucent-orng.json +234 -0
- package/src/tui/context/theme/material.json +235 -0
- package/src/tui/context/theme/matrix.json +77 -0
- package/src/tui/context/theme/mercury.json +252 -0
- package/src/tui/context/theme/monokai.json +221 -0
- package/src/tui/context/theme/nightowl.json +221 -0
- package/src/tui/context/theme/nord.json +223 -0
- package/src/tui/context/theme/one-dark.json +84 -0
- package/src/tui/context/theme/opencode.json +245 -0
- package/src/tui/context/theme/orng.json +249 -0
- package/src/tui/context/theme/osaka-jade.json +93 -0
- package/src/tui/context/theme/palenight.json +222 -0
- package/src/tui/context/theme/rosepine.json +234 -0
- package/src/tui/context/theme/solarized.json +223 -0
- package/src/tui/context/theme/synthwave84.json +226 -0
- package/src/tui/context/theme/tokyonight.json +243 -0
- package/src/tui/context/theme/vercel.json +245 -0
- package/src/tui/context/theme/vesper.json +218 -0
- package/src/tui/context/theme/zenburn.json +223 -0
- package/src/tui/context/theme.tsx +1247 -0
- package/src/tui/context/tui-config.tsx +9 -0
- package/src/tui/event.ts +16 -0
- package/src/tui/feature-plugins/home/footer.tsx +94 -0
- package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
- package/src/tui/feature-plugins/home/tips.tsx +59 -0
- package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
- package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
- package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
- package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
- package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
- package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
- package/src/tui/feature-plugins/system/plugins.tsx +269 -0
- package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
- package/src/tui/feature-plugins/system/which-key.tsx +608 -0
- package/src/tui/keymap.tsx +166 -0
- package/src/tui/layer.ts +6 -0
- package/src/tui/plugin/api.tsx +381 -0
- package/src/tui/plugin/command-shim.ts +109 -0
- package/src/tui/plugin/internal.ts +33 -0
- package/src/tui/plugin/runtime.ts +1069 -0
- package/src/tui/plugin/slots.tsx +60 -0
- package/src/tui/routes/home.tsx +96 -0
- package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
- package/src/tui/routes/session/dialog-message.tsx +108 -0
- package/src/tui/routes/session/dialog-subagent.tsx +26 -0
- package/src/tui/routes/session/dialog-timeline.tsx +47 -0
- package/src/tui/routes/session/footer.tsx +91 -0
- package/src/tui/routes/session/index.tsx +188 -0
- package/src/tui/routes/session/permission.tsx +722 -0
- package/src/tui/routes/session/question.tsx +490 -0
- package/src/tui/routes/session/sidebar.tsx +102 -0
- package/src/tui/routes/session/subagent-footer.tsx +133 -0
- package/src/tui/run.ts +84 -0
- package/src/tui/thread.ts +261 -0
- package/src/tui/tsconfig.json +40 -0
- package/src/tui/ui/dialog-alert.tsx +66 -0
- package/src/tui/ui/dialog-confirm.tsx +108 -0
- package/src/tui/ui/dialog-export-options.tsx +217 -0
- package/src/tui/ui/dialog-help.tsx +40 -0
- package/src/tui/ui/dialog-prompt.tsx +101 -0
- package/src/tui/ui/dialog-select.tsx +553 -0
- package/src/tui/ui/dialog.tsx +211 -0
- package/src/tui/ui/link.tsx +34 -0
- package/src/tui/ui/spinner.ts +368 -0
- package/src/tui/ui/toast.tsx +111 -0
- package/src/tui/util/clipboard.ts +217 -0
- package/src/tui/util/editor.ts +37 -0
- package/src/tui/util/model.ts +23 -0
- package/src/tui/util/provider-origin.ts +7 -0
- package/src/tui/util/revert-diff.ts +18 -0
- package/src/tui/util/scroll.ts +25 -0
- package/src/tui/util/selection.ts +65 -0
- package/src/tui/util/signal.ts +41 -0
- package/src/tui/util/sound.ts +156 -0
- package/src/tui/util/transcript.ts +112 -0
- package/src/tui/validate-session.ts +29 -0
- package/src/tui/win32.ts +130 -0
- 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,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
|
+
});
|