@echomem/mcp 1.4.7 → 1.4.9
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/README.md +35 -9
- package/assets/canonical-scorer/README.md +18 -0
- package/assets/canonical-scorer/analyze-10-problems.mjs +857 -0
- package/assets/canonical-scorer/build-session-waste-dashboard.mjs +1628 -0
- package/assets/canonical-scorer/golden_anchors.mjs +83 -0
- package/assets/canonical-scorer/optimizable_detail.mjs +633 -0
- package/assets/hud/claude.svg +1 -0
- package/assets/hud/codex.svg +1 -0
- package/assets/hud/session-viewer.html +35 -0
- package/dist/city/chaos-to-clarity-pencil.html +582 -0
- package/dist/city/echo-ai-city-only.html +1126 -109
- package/dist/city/echo-ai-city-only.template.html +1126 -109
- package/dist/city/echo-face-cutout.png +0 -0
- package/dist/city/pencil-pie-generator.html +883 -0
- package/dist/city/pencil-webgl-landscape.html +1239 -0
- package/dist/city/spatial-fan-story.html +479 -0
- package/dist/codex-session-files.js +283 -0
- package/dist/codex-sync.js +7 -2
- package/dist/context-analysis/canonical-golden.js +47 -0
- package/dist/context-analysis/claude-native-canonical.js +1193 -0
- package/dist/context-analysis/vendored-canonical.js +793 -0
- package/dist/context-analysis/workspace-report.js +1838 -0
- package/dist/context-metrics/calculate.js +56 -0
- package/dist/context-metrics/model-limits.js +26 -0
- package/dist/context-metrics/types.js +1 -0
- package/dist/forensics-10-problems.js +7 -6
- package/dist/forensics.js +863 -132
- package/dist/hud/adapters.js +8 -4
- package/dist/hud/autostart.js +66 -0
- package/dist/hud/cli.js +31 -0
- package/dist/hud/electron-main.js +182 -19
- package/dist/hud/metric.js +13 -4
- package/dist/hud/monitor.js +171 -84
- package/dist/hud/preload.cjs +3 -0
- package/dist/hud/server.js +321 -4
- package/dist/hud/web.js +880 -270
- package/dist/index.js +122 -24
- package/dist/local-data-paths.js +87 -0
- package/dist/migrate.js +55 -29
- package/dist/report.js +101 -40
- package/dist/setup-page.js +4257 -245
- package/dist/setup-preview.js +245 -0
- package/dist/setup.js +786 -75
- package/dist/v1-contract.js +20 -2
- package/package.json +6 -4
- package/templates/echomem-recall.md +2 -2
package/dist/hud/adapters.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { resolveClaudeProjectsDir, resolveCodexSessionsDir } from "../local-data-paths.js";
|
|
3
4
|
import { homePath, newestFile, readJsonl, walkFiles } from "./fs.js";
|
|
4
5
|
import { bumpTurn, newMetricState, recordEdit, recordRead, recordTool, scoreMetric, shellRead, } from "./metric.js";
|
|
5
6
|
export const adapters = {
|
|
@@ -31,8 +32,8 @@ export function adapterList(mode) {
|
|
|
31
32
|
return [adapters[mode]];
|
|
32
33
|
}
|
|
33
34
|
function listCodex() {
|
|
34
|
-
const root =
|
|
35
|
-
return walkFiles(root, (file) => /^rollout-.*\.jsonl$/.test(path.basename(file)));
|
|
35
|
+
const root = resolveCodexSessionsDir();
|
|
36
|
+
return root ? walkFiles(root, (file) => /^rollout-.*\.jsonl$/.test(path.basename(file))) : [];
|
|
36
37
|
}
|
|
37
38
|
function findActiveCodex() {
|
|
38
39
|
return newestFile(listCodex());
|
|
@@ -121,10 +122,13 @@ function scoreCodex(file) {
|
|
|
121
122
|
return score;
|
|
122
123
|
}
|
|
123
124
|
function listClaudeCode() {
|
|
124
|
-
|
|
125
|
+
const root = resolveClaudeProjectsDir();
|
|
126
|
+
return root ? walkFiles(root, (file) => file.endsWith(".jsonl")) : [];
|
|
125
127
|
}
|
|
126
128
|
function findActiveClaudeCode() {
|
|
127
|
-
const
|
|
129
|
+
const projects = resolveClaudeProjectsDir();
|
|
130
|
+
const cacheRoot = projects ? path.join(path.dirname(projects), "echo-ctx") : null;
|
|
131
|
+
const cache = cacheRoot ? newestFile(walkFiles(cacheRoot, (file) => file.endsWith(".json"))) : null;
|
|
128
132
|
if (cache)
|
|
129
133
|
return cache;
|
|
130
134
|
return newestFile(listClaudeCode());
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
// Show-after-restart for the HUD via a macOS LaunchAgent. We ship a CLI, not an .app bundle, so this is
|
|
6
|
+
// the standard way to make the overlay persist across quit/reboot. Shared by the CLI (`echomem-hud
|
|
7
|
+
// autostart`) and the Electron context menu so both write an identical, robust registration.
|
|
8
|
+
const LABEL = "com.echomem.hud";
|
|
9
|
+
export function autostartPlistPath() {
|
|
10
|
+
return path.join(os.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
11
|
+
}
|
|
12
|
+
export function autostartSupported() {
|
|
13
|
+
return process.platform === "darwin";
|
|
14
|
+
}
|
|
15
|
+
export function isAutostartEnabled() {
|
|
16
|
+
return autostartSupported() && fs.existsSync(autostartPlistPath());
|
|
17
|
+
}
|
|
18
|
+
function escapeXml(value) {
|
|
19
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
20
|
+
}
|
|
21
|
+
function launchctlUid() {
|
|
22
|
+
return typeof process.getuid === "function" ? process.getuid() : 501;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Register the LaunchAgent. `programArgs` is the full ProgramArguments array — the caller supplies the
|
|
26
|
+
* exact launch command (electron binary + electron-main.js + flags) so it works regardless of whether
|
|
27
|
+
* node is on launchd's PATH. Loads immediately as well as at future logins.
|
|
28
|
+
*/
|
|
29
|
+
export function enableAutostart(programArgs) {
|
|
30
|
+
if (!autostartSupported())
|
|
31
|
+
return { ok: false, message: "autostart is currently macOS-only." };
|
|
32
|
+
const plist = autostartPlistPath();
|
|
33
|
+
const argsXml = programArgs.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
|
|
34
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
35
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
36
|
+
<plist version="1.0"><dict>
|
|
37
|
+
<key>Label</key><string>${LABEL}</string>
|
|
38
|
+
<key>ProgramArguments</key><array>
|
|
39
|
+
${argsXml}
|
|
40
|
+
</array>
|
|
41
|
+
<key>RunAtLoad</key><true/>
|
|
42
|
+
<key>StandardOutPath</key><string>/tmp/echomem-hud.log</string>
|
|
43
|
+
<key>StandardErrorPath</key><string>/tmp/echomem-hud.log</string>
|
|
44
|
+
</dict></plist>
|
|
45
|
+
`;
|
|
46
|
+
fs.mkdirSync(path.dirname(plist), { recursive: true });
|
|
47
|
+
fs.writeFileSync(plist, xml);
|
|
48
|
+
const uid = launchctlUid();
|
|
49
|
+
spawnSync("launchctl", ["bootout", `gui/${uid}/${LABEL}`], { stdio: "ignore" }); // replace stale reg
|
|
50
|
+
const boot = spawnSync("launchctl", ["bootstrap", `gui/${uid}`, plist], { encoding: "utf8" });
|
|
51
|
+
return boot.status === 0
|
|
52
|
+
? { ok: true, message: `EchoMem HUD will show after restart. Off anytime: echomem-hud autostart off` }
|
|
53
|
+
: { ok: true, message: `Wrote ${plist} — shows after your next restart.` };
|
|
54
|
+
}
|
|
55
|
+
export function disableAutostart() {
|
|
56
|
+
if (!autostartSupported())
|
|
57
|
+
return;
|
|
58
|
+
const uid = launchctlUid();
|
|
59
|
+
spawnSync("launchctl", ["bootout", `gui/${uid}/${LABEL}`], { stdio: "ignore" });
|
|
60
|
+
try {
|
|
61
|
+
fs.rmSync(autostartPlistPath(), { force: true });
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* already gone */
|
|
65
|
+
}
|
|
66
|
+
}
|
package/dist/hud/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { adapterList } from "./adapters.js";
|
|
6
|
+
import { autostartPlistPath, autostartSupported, disableAutostart, enableAutostart, isAutostartEnabled } from "./autostart.js";
|
|
6
7
|
import { statSignature } from "./fs.js";
|
|
7
8
|
import { installHooks } from "./hooks.js";
|
|
8
9
|
import { HudMonitor } from "./monitor.js";
|
|
@@ -23,6 +24,8 @@ try {
|
|
|
23
24
|
await cmdApp(flags);
|
|
24
25
|
else if (command === "install-hooks")
|
|
25
26
|
await cmdInstallHooks(flags);
|
|
27
|
+
else if (command === "autostart")
|
|
28
|
+
cmdAutostart(argv[1] || "status", flags);
|
|
26
29
|
else if (command === "status")
|
|
27
30
|
await cmdStatus(flags);
|
|
28
31
|
else if (command === "report")
|
|
@@ -79,6 +82,33 @@ async function cmdApp(flags) {
|
|
|
79
82
|
child.unref();
|
|
80
83
|
console.log("EchoMem HUD app launched.");
|
|
81
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* `echomem-hud autostart on|off|status` — show the HUD after restart via a macOS LaunchAgent, so
|
|
87
|
+
* closing/rebooting doesn't lose it (we ship a CLI, not an .app, so this is the standard mechanism).
|
|
88
|
+
* The registration runs the electron binary directly on electron-main.js — no dependency on node
|
|
89
|
+
* being on launchd's PATH — matching what the in-app "Show after restart" toggle writes.
|
|
90
|
+
*/
|
|
91
|
+
function cmdAutostart(action, flags) {
|
|
92
|
+
if (!autostartSupported()) {
|
|
93
|
+
console.log("autostart is currently macOS-only (LaunchAgent).");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (action === "on") {
|
|
97
|
+
const require = createRequire(import.meta.url);
|
|
98
|
+
const electronPath = require("electron");
|
|
99
|
+
const mainPath = fileURLToPath(new URL("./electron-main.js", import.meta.url));
|
|
100
|
+
const client = typeof flags.client === "string" ? flags.client : "auto";
|
|
101
|
+
const result = enableAutostart([electronPath, mainPath, "--client", client]);
|
|
102
|
+
console.log(`✅ ${result.message}`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (action === "off") {
|
|
106
|
+
disableAutostart();
|
|
107
|
+
console.log("✅ Show after restart disabled — the HUD will not relaunch after restart.");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
console.log(isAutostartEnabled() ? `Autostart: ON (${autostartPlistPath()})` : "Autostart: OFF. Enable with: echomem-hud autostart on");
|
|
111
|
+
}
|
|
82
112
|
async function cmdInstallHooks(flags) {
|
|
83
113
|
const paths = installHooks(parseMode(flags.client));
|
|
84
114
|
console.log(`Installed EchoMem HUD hook support:\n${paths.map((p) => `- ${p}`).join("\n")}`);
|
|
@@ -136,6 +166,7 @@ Usage:
|
|
|
136
166
|
echomem-hud serve [--client codex|claude-code|claude-desktop|both|auto] [--port 17377]
|
|
137
167
|
echomem-hud app [--client codex|claude-code|claude-desktop|both|auto]
|
|
138
168
|
echomem-hud install-hooks [--client codex|claude-code|both]
|
|
169
|
+
echomem-hud autostart on|off|status (show the HUD after restart — macOS)
|
|
139
170
|
echomem-hud status
|
|
140
171
|
echomem-hud report [--client codex|claude-code|claude-desktop|auto] [--limit 40] [--json]
|
|
141
172
|
`);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { app, BrowserWindow, ipcMain, Menu, screen, shell } from "electron";
|
|
6
|
+
import { app, BrowserWindow, dialog, ipcMain, Menu, nativeImage, screen, shell } from "electron";
|
|
7
|
+
import { disableAutostart, enableAutostart, isAutostartEnabled } from "./autostart.js";
|
|
6
8
|
import { createHudServer } from "./server.js";
|
|
7
9
|
const flags = parseFlags(process.argv.slice(2));
|
|
8
10
|
const mode = parseMode(flags.client);
|
|
@@ -10,8 +12,16 @@ const port = typeof flags.port === "string" ? Number(flags.port) || 17377 : 1737
|
|
|
10
12
|
const COLLAPSED_WIDTH = 360;
|
|
11
13
|
const COLLAPSED_HEIGHT = 112;
|
|
12
14
|
const EXPANDED_HEIGHT = 460;
|
|
15
|
+
// Mini mode: a small always-on pill (status dot + tokens) for users who find the full bubble too big.
|
|
16
|
+
const MINI_WIDTH = 190;
|
|
17
|
+
const MINI_HEIGHT = 48;
|
|
18
|
+
const AGENT_VISIBILITY_POLL_MS = 900;
|
|
19
|
+
let miniPref = false;
|
|
20
|
+
let onlyShowWhenAgentOpen = false;
|
|
21
|
+
let manuallyHidden = false;
|
|
13
22
|
let hudServer = null;
|
|
14
23
|
let mainWindow = null;
|
|
24
|
+
let visibilityTimer = null;
|
|
15
25
|
const dragState = new Map();
|
|
16
26
|
// Single-instance: a second `echomem-hud app` brings the existing HUD back instead of opening another.
|
|
17
27
|
const gotSingleInstanceLock = app.requestSingleInstanceLock();
|
|
@@ -28,8 +38,23 @@ app.whenReady().then(async () => {
|
|
|
28
38
|
if (!win)
|
|
29
39
|
return;
|
|
30
40
|
const bounds = win.getBounds();
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
// Closing returns to the user's preferred resting size (mini pill or the full bubble).
|
|
42
|
+
const width = open ? COLLAPSED_WIDTH : miniPref ? MINI_WIDTH : COLLAPSED_WIDTH;
|
|
43
|
+
const height = open ? clampExpandedHeight(measured && measured > 200 ? measured : EXPANDED_HEIGHT) : miniPref ? MINI_HEIGHT : COLLAPSED_HEIGHT;
|
|
44
|
+
win.setBounds({ ...bounds, width, height });
|
|
45
|
+
writeBounds(win);
|
|
46
|
+
});
|
|
47
|
+
ipcMain.on("hud:set-mini", (event, payload) => {
|
|
48
|
+
miniPref = typeof payload === "object" ? Boolean(payload.mini) : Boolean(payload);
|
|
49
|
+
const win = BrowserWindow.fromWebContents(event.sender);
|
|
50
|
+
if (!win)
|
|
51
|
+
return;
|
|
52
|
+
const bounds = win.getBounds();
|
|
53
|
+
win.setBounds({
|
|
54
|
+
...bounds,
|
|
55
|
+
width: miniPref ? MINI_WIDTH : COLLAPSED_WIDTH,
|
|
56
|
+
height: miniPref ? MINI_HEIGHT : COLLAPSED_HEIGHT,
|
|
57
|
+
});
|
|
33
58
|
writeBounds(win);
|
|
34
59
|
});
|
|
35
60
|
ipcMain.on("hud:drag-start", (event, payload) => {
|
|
@@ -99,10 +124,12 @@ app.on("window-all-closed", async () => {
|
|
|
99
124
|
});
|
|
100
125
|
function createWindow(url) {
|
|
101
126
|
const savedBounds = readBounds();
|
|
127
|
+
miniPref = Boolean(savedBounds?.mini);
|
|
128
|
+
onlyShowWhenAgentOpen = Boolean(savedBounds?.onlyShowWhenAgentOpen);
|
|
102
129
|
const bounds = savedBounds ? normalizedBounds(savedBounds) : defaultBounds();
|
|
103
130
|
const win = new BrowserWindow({
|
|
104
|
-
width: COLLAPSED_WIDTH,
|
|
105
|
-
height: COLLAPSED_HEIGHT,
|
|
131
|
+
width: miniPref ? MINI_WIDTH : COLLAPSED_WIDTH,
|
|
132
|
+
height: miniPref ? MINI_HEIGHT : COLLAPSED_HEIGHT,
|
|
106
133
|
x: bounds.x,
|
|
107
134
|
y: bounds.y,
|
|
108
135
|
frame: false,
|
|
@@ -126,17 +153,30 @@ function createWindow(url) {
|
|
|
126
153
|
}
|
|
127
154
|
win.on("moved", () => writeBounds(win));
|
|
128
155
|
mainWindow = win;
|
|
156
|
+
const webContentsId = win.webContents.id;
|
|
129
157
|
win.on("closed", () => {
|
|
130
|
-
dragState.delete(
|
|
158
|
+
dragState.delete(webContentsId);
|
|
159
|
+
stopAgentVisibilityLoop();
|
|
131
160
|
if (mainWindow === win)
|
|
132
161
|
mainWindow = null;
|
|
133
162
|
});
|
|
134
163
|
win.webContents.on("context-menu", () => {
|
|
135
164
|
const menu = Menu.buildFromTemplate([
|
|
136
|
-
{ label: "Hide for now", click: () => {
|
|
137
|
-
|
|
165
|
+
{ label: "Hide for now", click: () => { hideForNow(win); } },
|
|
166
|
+
{
|
|
167
|
+
label: "Only show when Codex/Claude is open",
|
|
168
|
+
type: "checkbox",
|
|
169
|
+
checked: onlyShowWhenAgentOpen,
|
|
170
|
+
click: (item) => { setOnlyShowWhenAgentOpen(item.checked, win); },
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
label: "Show after restart",
|
|
174
|
+
type: "checkbox",
|
|
175
|
+
checked: isAutostartEnabled(),
|
|
176
|
+
click: (item) => { setAutostart(item.checked); },
|
|
177
|
+
},
|
|
138
178
|
{ type: "separator" },
|
|
139
|
-
{ label: "Quit EchoMem HUD", click: () =>
|
|
179
|
+
{ label: "Quit EchoMem HUD", click: () => quitWithAutostartPrompt(win) },
|
|
140
180
|
]);
|
|
141
181
|
menu.popup({ window: win });
|
|
142
182
|
});
|
|
@@ -145,10 +185,11 @@ function createWindow(url) {
|
|
|
145
185
|
if (shown || win.isDestroyed())
|
|
146
186
|
return;
|
|
147
187
|
shown = true;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
188
|
+
if (onlyShowWhenAgentOpen)
|
|
189
|
+
syncAgentVisibility(win);
|
|
190
|
+
else
|
|
191
|
+
showHudWindow(win, true);
|
|
192
|
+
startAgentVisibilityLoop(win);
|
|
152
193
|
writeBounds(win);
|
|
153
194
|
};
|
|
154
195
|
win.once("ready-to-show", reveal);
|
|
@@ -158,6 +199,31 @@ function createWindow(url) {
|
|
|
158
199
|
win.webContents.openDevTools({ mode: "detach" });
|
|
159
200
|
win.loadURL(url);
|
|
160
201
|
}
|
|
202
|
+
// Show-after-restart registration that runs THIS electron binary on electron-main.js directly — no node
|
|
203
|
+
// PATH dependency. Mirrors what `echomem-hud autostart on` writes, so CLI and menu stay in sync.
|
|
204
|
+
function setAutostart(on) {
|
|
205
|
+
if (on)
|
|
206
|
+
enableAutostart([process.execPath, fileURLPath("./electron-main.js"), "--client", String(mode), "--port", String(port)]);
|
|
207
|
+
else
|
|
208
|
+
disableAutostart();
|
|
209
|
+
}
|
|
210
|
+
// Quitting is the moment the HUD truly disappears (unlike "Hide for now"), so if it won't come back on
|
|
211
|
+
// its own, that's exactly when to offer show-after-restart — per the user's "ask me when it goes away".
|
|
212
|
+
function quitWithAutostartPrompt(win) {
|
|
213
|
+
if (process.platform !== "darwin" || isAutostartEnabled())
|
|
214
|
+
return void app.quit();
|
|
215
|
+
const choice = showRecoveryPrompt({
|
|
216
|
+
buttons: ["Show after restart & quit", "Just quit", "Cancel"],
|
|
217
|
+
cancelId: 2,
|
|
218
|
+
message: "Bring EchoMem back automatically?",
|
|
219
|
+
detail: "EchoMem has no app icon to relaunch from. Turn on Show after restart and it will be waiting after your computer restarts. You can also tell your coding agent to run `echomem-hud app`.",
|
|
220
|
+
});
|
|
221
|
+
if (choice === 2)
|
|
222
|
+
return;
|
|
223
|
+
if (choice === 0)
|
|
224
|
+
setAutostart(true);
|
|
225
|
+
app.quit();
|
|
226
|
+
}
|
|
161
227
|
function clampExpandedHeight(value) {
|
|
162
228
|
const workArea = screen.getPrimaryDisplay().workArea;
|
|
163
229
|
const max = Math.max(COLLAPSED_HEIGHT, workArea.height - 48);
|
|
@@ -166,11 +232,11 @@ function clampExpandedHeight(value) {
|
|
|
166
232
|
function showExistingWindow() {
|
|
167
233
|
if (!mainWindow || mainWindow.isDestroyed())
|
|
168
234
|
return;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
235
|
+
manuallyHidden = false;
|
|
236
|
+
if (onlyShowWhenAgentOpen)
|
|
237
|
+
syncAgentVisibility(mainWindow);
|
|
238
|
+
else
|
|
239
|
+
showHudWindow(mainWindow, true);
|
|
174
240
|
}
|
|
175
241
|
async function hasCompatibleHudState(baseUrl, requestedMode) {
|
|
176
242
|
const signal = AbortSignal.timeout(750);
|
|
@@ -244,7 +310,104 @@ function clamp(value, min, max) {
|
|
|
244
310
|
function writeBounds(win) {
|
|
245
311
|
const b = win.getBounds();
|
|
246
312
|
fs.mkdirSync(path.dirname(boundsPath()), { recursive: true });
|
|
247
|
-
fs.writeFileSync(boundsPath(), JSON.stringify(b, null, 2));
|
|
313
|
+
fs.writeFileSync(boundsPath(), JSON.stringify({ ...b, mini: miniPref, onlyShowWhenAgentOpen }, null, 2));
|
|
314
|
+
}
|
|
315
|
+
function hideForNow(win) {
|
|
316
|
+
if (win.isDestroyed())
|
|
317
|
+
return;
|
|
318
|
+
if (process.platform === "darwin" && !isAutostartEnabled()) {
|
|
319
|
+
const choice = showRecoveryPrompt({
|
|
320
|
+
buttons: ["Show after restart & hide", "Hide for now", "Cancel"],
|
|
321
|
+
cancelId: 2,
|
|
322
|
+
message: "Hide EchoMem for now?",
|
|
323
|
+
detail: "It will stay hidden until you restart it. Turn on Show after restart to bring it back after your computer restarts. Before then, you can ask your coding agent to run `echomem-hud app`.",
|
|
324
|
+
});
|
|
325
|
+
if (choice === 2)
|
|
326
|
+
return;
|
|
327
|
+
if (choice === 0)
|
|
328
|
+
setAutostart(true);
|
|
329
|
+
}
|
|
330
|
+
manuallyHidden = true;
|
|
331
|
+
win.hide();
|
|
332
|
+
}
|
|
333
|
+
function showRecoveryPrompt(options) {
|
|
334
|
+
app.focus({ steal: true });
|
|
335
|
+
const cancelId = typeof options.cancelId === "number" ? options.cancelId : options.buttons.length - 1;
|
|
336
|
+
const { cancelId: _cancelId, ...dialogOptions } = options;
|
|
337
|
+
return dialog.showMessageBoxSync({
|
|
338
|
+
type: "question",
|
|
339
|
+
defaultId: 0,
|
|
340
|
+
cancelId,
|
|
341
|
+
icon: nativeImage.createFromPath(fileURLPath("../../assets/hud/echo-face-cutout.png")),
|
|
342
|
+
...dialogOptions,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
function setOnlyShowWhenAgentOpen(next, win) {
|
|
346
|
+
onlyShowWhenAgentOpen = next;
|
|
347
|
+
manuallyHidden = false;
|
|
348
|
+
writeBounds(win);
|
|
349
|
+
if (next)
|
|
350
|
+
syncAgentVisibility(win);
|
|
351
|
+
else
|
|
352
|
+
showHudWindow(win, true);
|
|
353
|
+
}
|
|
354
|
+
function startAgentVisibilityLoop(win) {
|
|
355
|
+
stopAgentVisibilityLoop();
|
|
356
|
+
visibilityTimer = setInterval(() => syncAgentVisibility(win), AGENT_VISIBILITY_POLL_MS);
|
|
357
|
+
}
|
|
358
|
+
function stopAgentVisibilityLoop() {
|
|
359
|
+
if (!visibilityTimer)
|
|
360
|
+
return;
|
|
361
|
+
clearInterval(visibilityTimer);
|
|
362
|
+
visibilityTimer = null;
|
|
363
|
+
}
|
|
364
|
+
function syncAgentVisibility(win) {
|
|
365
|
+
if (win.isDestroyed() || !onlyShowWhenAgentOpen)
|
|
366
|
+
return;
|
|
367
|
+
const frontmost = frontmostAppKind();
|
|
368
|
+
if (frontmost === "agent") {
|
|
369
|
+
if (!manuallyHidden && !win.isVisible())
|
|
370
|
+
showHudWindow(win, false);
|
|
371
|
+
else if (!manuallyHidden)
|
|
372
|
+
win.moveTop();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
// Leave the HUD alone while the user is interacting with the HUD/menu itself, or when macOS does
|
|
376
|
+
// not report a frontmost process. The next concrete app detection will settle visibility.
|
|
377
|
+
if (frontmost === "self" || frontmost === "unknown")
|
|
378
|
+
return;
|
|
379
|
+
if (win.isVisible())
|
|
380
|
+
win.hide();
|
|
381
|
+
}
|
|
382
|
+
function showHudWindow(win, focus) {
|
|
383
|
+
if (win.isDestroyed())
|
|
384
|
+
return;
|
|
385
|
+
if (focus) {
|
|
386
|
+
app.focus({ steal: true });
|
|
387
|
+
win.show();
|
|
388
|
+
win.focus();
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
win.showInactive();
|
|
392
|
+
}
|
|
393
|
+
win.moveTop();
|
|
394
|
+
}
|
|
395
|
+
function frontmostAppKind() {
|
|
396
|
+
if (process.platform !== "darwin")
|
|
397
|
+
return "unknown";
|
|
398
|
+
try {
|
|
399
|
+
const name = execFileSync("osascript", ["-e", 'tell application "System Events" to get name of first application process whose frontmost is true'], { encoding: "utf8", timeout: 500, stdio: ["ignore", "pipe", "ignore"] }).trim().toLowerCase();
|
|
400
|
+
if (!name)
|
|
401
|
+
return "unknown";
|
|
402
|
+
if (name.includes("codex") || name.includes("claude"))
|
|
403
|
+
return "agent";
|
|
404
|
+
if (name.includes("echomem") || name.includes("electron") || name === app.getName().toLowerCase())
|
|
405
|
+
return "self";
|
|
406
|
+
return "other";
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return "unknown";
|
|
410
|
+
}
|
|
248
411
|
}
|
|
249
412
|
function dragPoint(payload) {
|
|
250
413
|
const startX = typeof payload.screenX === "number" && Number.isFinite(payload.screenX) ? payload.screenX : null;
|
package/dist/hud/metric.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { calculateContextMetrics } from "../context-metrics/calculate.js";
|
|
1
2
|
export const BUCKETS = {
|
|
2
3
|
rangeRedundant: "range_redundant",
|
|
3
4
|
staleRead: "stale_read",
|
|
@@ -72,16 +73,24 @@ export function scoreMetric(params) {
|
|
|
72
73
|
const ct = Number(params.ctTokens) || 0;
|
|
73
74
|
const modelWindow = Number(params.modelContextWindow) || 0;
|
|
74
75
|
const pollutionTok = Object.values(params.state.buckets).reduce((sum, bucket) => sum + bucket.tokens, 0);
|
|
75
|
-
const
|
|
76
|
+
const contextMetrics = calculateContextMetrics({
|
|
77
|
+
latestInputTokens: ct,
|
|
78
|
+
modelContextLimitTokens: modelWindow || null,
|
|
79
|
+
currentResidentWasteTokens: pollutionTok,
|
|
80
|
+
});
|
|
81
|
+
const pollution = Math.min(0.95, contextMetrics.noisePct);
|
|
76
82
|
const pollutionPct = Math.round(pollution * 100);
|
|
77
|
-
const saturationPct =
|
|
83
|
+
const saturationPct = contextMetrics.contextFullnessPct !== undefined
|
|
84
|
+
? Math.round(contextMetrics.contextFullnessPct * 100)
|
|
85
|
+
: null;
|
|
78
86
|
return {
|
|
79
87
|
client: params.client,
|
|
80
88
|
sourcePath: params.sourcePath,
|
|
81
89
|
turn: params.state.turn,
|
|
82
90
|
reads: params.state.reads,
|
|
83
91
|
redundantCount: params.state.redundantCount,
|
|
84
|
-
usefulPct: Math.round((1
|
|
92
|
+
usefulPct: Math.round(Math.max(0, Math.min(1, contextMetrics.usefulPct)) * 100),
|
|
93
|
+
healthScorePct: contextMetrics.healthScorePct,
|
|
85
94
|
pollutionPct,
|
|
86
95
|
pollutionTok,
|
|
87
96
|
ctTokens: ct,
|
|
@@ -134,7 +143,7 @@ export function formatTokens(tokens) {
|
|
|
134
143
|
}
|
|
135
144
|
export function formatGlance(score) {
|
|
136
145
|
const dot = score.color === "amber" ? "◑" : "●";
|
|
137
|
-
return `${dot} ${score.
|
|
146
|
+
return `${dot} ${score.healthScorePct}% score · ${formatTokens(score.ctTokens)}`;
|
|
138
147
|
}
|
|
139
148
|
function estimateReadTokens(start, end) {
|
|
140
149
|
const boundedEnd = Math.min(end, start + 4000);
|