@echomem/mcp 1.4.7 → 1.4.8
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 +14 -8
- 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/echo-ai-city-only.html +31 -13
- package/dist/city/echo-ai-city-only.template.html +31 -13
- package/dist/city/echo-face-cutout.png +0 -0
- 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/monitor.js +36 -68
- package/dist/hud/preload.cjs +3 -0
- package/dist/hud/server.js +321 -4
- package/dist/hud/web.js +633 -69
- package/dist/index.js +116 -22
- package/dist/migrate.js +18 -0
- package/dist/setup-page.js +978 -60
- package/dist/setup.js +355 -42
- package/dist/v1-contract.js +20 -2
- package/package.json +3 -2
|
@@ -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/monitor.js
CHANGED
|
@@ -6,7 +6,6 @@ import { adapterList, adapters } from "./adapters.js";
|
|
|
6
6
|
import { homePath, newestFile, walkFiles } from "./fs.js";
|
|
7
7
|
const LIVE_WINDOW_MS = 45_000;
|
|
8
8
|
const ONGOING_WINDOW_MS = 300_000; // a thread counts as "ongoing" if its log was written in the last 5 min
|
|
9
|
-
const FOCUS_GRACE_MS = 8_000; // keep the last-known frontmost client this long when detection momentarily misses
|
|
10
9
|
const RECENT_WINDOW_MS = 12 * 60 * 60 * 1000; // a session is a "recent tab" if its log was touched in the last 12h
|
|
11
10
|
const RECENT_CAP = 16; // most-recent N sessions kept as switchable tabs
|
|
12
11
|
export class HudMonitor extends EventEmitter {
|
|
@@ -19,7 +18,6 @@ export class HudMonitor extends EventEmitter {
|
|
|
19
18
|
labelCache = new Map();
|
|
20
19
|
missing = [];
|
|
21
20
|
frontmostCheckedAt = 0;
|
|
22
|
-
frontmostSeenAt = 0;
|
|
23
21
|
frontmostClient = null;
|
|
24
22
|
lastActiveClient = null;
|
|
25
23
|
threadCounts = {};
|
|
@@ -65,17 +63,13 @@ export class HudMonitor extends EventEmitter {
|
|
|
65
63
|
lastActiveMs,
|
|
66
64
|
};
|
|
67
65
|
});
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return a.focused ? -1 : 1;
|
|
72
|
-
if (a.live !== b.live)
|
|
73
|
-
return a.live ? -1 : 1;
|
|
74
|
-
return a.lastActiveMs - b.lastActiveMs;
|
|
75
|
-
});
|
|
66
|
+
// The user's foreground agent is the primary truth. Background agents can keep writing logs, but
|
|
67
|
+
// they should never steal the main HUD away from Codex/Claude while that app is what the user opened.
|
|
68
|
+
sessions.sort((a, b) => hudSelectionRank(a) - hudSelectionRank(b) || a.lastActiveMs - b.lastActiveMs);
|
|
76
69
|
// A user-pinned tab overrides auto-selection: it becomes the primary and never flips away.
|
|
77
70
|
const pinned = this.pinnedView(now);
|
|
78
71
|
const ordered = pinned ? [pinned, ...sessions.filter((s) => s.sourcePath !== pinned.sourcePath)] : sessions;
|
|
72
|
+
const pinnedId = pinned ? this.pinnedId : null;
|
|
79
73
|
return {
|
|
80
74
|
mode: this.mode,
|
|
81
75
|
active: ordered[0] || null,
|
|
@@ -83,6 +77,7 @@ export class HudMonitor extends EventEmitter {
|
|
|
83
77
|
sessions: ordered,
|
|
84
78
|
threadCounts: this.threadCounts,
|
|
85
79
|
recentSessions: this.recentSessions,
|
|
80
|
+
pinnedId,
|
|
86
81
|
missing: this.missing,
|
|
87
82
|
updatedAt: new Date().toISOString(),
|
|
88
83
|
};
|
|
@@ -139,13 +134,6 @@ export class HudMonitor extends EventEmitter {
|
|
|
139
134
|
if (detected) {
|
|
140
135
|
// Positive detection wins immediately (real focus switch, e.g. Codex → Claude Desktop).
|
|
141
136
|
this.frontmostClient = detected;
|
|
142
|
-
this.frontmostSeenAt = now;
|
|
143
|
-
}
|
|
144
|
-
else if (now - this.frontmostSeenAt > FOCUS_GRACE_MS) {
|
|
145
|
-
// Detection missed (osascript timeout, or a non-agent app like a browser is front). Hold the
|
|
146
|
-
// last-known focus for a grace window so the active slot doesn't flip to a background client
|
|
147
|
-
// just because it's writing — then release once the grace expires.
|
|
148
|
-
this.frontmostClient = null;
|
|
149
137
|
}
|
|
150
138
|
}
|
|
151
139
|
return this.frontmostClient && this.scores.has(this.frontmostClient) ? this.frontmostClient : null;
|
|
@@ -213,60 +201,15 @@ export class HudMonitor extends EventEmitter {
|
|
|
213
201
|
return title;
|
|
214
202
|
}
|
|
215
203
|
else {
|
|
216
|
-
const
|
|
217
|
-
if (
|
|
218
|
-
return
|
|
204
|
+
const cached = this.claudeTitleCache.get(file);
|
|
205
|
+
if (cached !== undefined)
|
|
206
|
+
return cached;
|
|
207
|
+
const title = claudeDisplayTitle(file, client);
|
|
208
|
+
this.claudeTitleCache.set(file, title);
|
|
209
|
+
return title;
|
|
219
210
|
}
|
|
220
211
|
return sessionLabel(file, client); // repo/cwd basename fallback
|
|
221
212
|
}
|
|
222
|
-
// Claude has no title index, so derive one from the thread's first real user message (cached per file
|
|
223
|
-
// — the first message never changes). Distinguishes two threads in the same repo.
|
|
224
|
-
claudeTitle(file) {
|
|
225
|
-
const cached = this.claudeTitleCache.get(file);
|
|
226
|
-
if (cached !== undefined)
|
|
227
|
-
return cached;
|
|
228
|
-
let title = "";
|
|
229
|
-
try {
|
|
230
|
-
const fd = fs.openSync(file, "r");
|
|
231
|
-
try {
|
|
232
|
-
const buf = Buffer.alloc(64 * 1024);
|
|
233
|
-
const bytes = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
234
|
-
for (const line of buf.toString("utf8", 0, bytes).split("\n")) {
|
|
235
|
-
if (!line.includes('"user"'))
|
|
236
|
-
continue;
|
|
237
|
-
let obj;
|
|
238
|
-
try {
|
|
239
|
-
obj = JSON.parse(line);
|
|
240
|
-
}
|
|
241
|
-
catch {
|
|
242
|
-
continue; // partial/last line
|
|
243
|
-
}
|
|
244
|
-
if (!isRecord(obj) || obj.type !== "user")
|
|
245
|
-
continue;
|
|
246
|
-
const message = isRecord(obj.message) ? obj.message : null;
|
|
247
|
-
const content = message ? message.content : undefined;
|
|
248
|
-
let body = "";
|
|
249
|
-
if (typeof content === "string")
|
|
250
|
-
body = content;
|
|
251
|
-
else if (Array.isArray(content))
|
|
252
|
-
body = content.map((b) => (isRecord(b) && typeof b.text === "string" ? b.text : "")).join(" ");
|
|
253
|
-
body = body.replace(/\s+/g, " ").trim();
|
|
254
|
-
if (!body || body.startsWith("<") || /^#\s*claudeMd\b/i.test(body) || body.startsWith("Caveat:"))
|
|
255
|
-
continue;
|
|
256
|
-
title = body.length > 48 ? `${body.slice(0, 47)}…` : body;
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
finally {
|
|
261
|
-
fs.closeSync(fd);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
catch {
|
|
265
|
-
/* unreadable → falls back to repo label */
|
|
266
|
-
}
|
|
267
|
-
this.claudeTitleCache.set(file, title);
|
|
268
|
-
return title;
|
|
269
|
-
}
|
|
270
213
|
// Codex writes a lightweight index of every thread (id + human title). Load it (mtime-cached) so tabs
|
|
271
214
|
// show real titles like "Verify file state" instead of a UUID.
|
|
272
215
|
loadCodexTitles() {
|
|
@@ -360,6 +303,18 @@ export class HudMonitor extends EventEmitter {
|
|
|
360
303
|
this.emit("state", this.snapshot());
|
|
361
304
|
}
|
|
362
305
|
}
|
|
306
|
+
export function hudSelectionRank(session) {
|
|
307
|
+
if (session.focused)
|
|
308
|
+
return 0;
|
|
309
|
+
if (session.live)
|
|
310
|
+
return 1;
|
|
311
|
+
return 2;
|
|
312
|
+
}
|
|
313
|
+
export function claudeDisplayTitle(file, client) {
|
|
314
|
+
const label = sessionLabel(file, client);
|
|
315
|
+
const stamp = sessionTimeLabel(resolveClaudeTranscript(file) ?? file);
|
|
316
|
+
return label === defaultLabel(client) ? stamp : `${label} · ${stamp}`;
|
|
317
|
+
}
|
|
363
318
|
function detectFrontmostClient() {
|
|
364
319
|
if (process.platform !== "darwin")
|
|
365
320
|
return null;
|
|
@@ -400,6 +355,19 @@ function sessionLabel(file, client) {
|
|
|
400
355
|
const cwd = peekCwd(target);
|
|
401
356
|
return cwd ? path.basename(cwd) : defaultLabel(client);
|
|
402
357
|
}
|
|
358
|
+
function sessionTimeLabel(file) {
|
|
359
|
+
try {
|
|
360
|
+
const date = new Date(fs.statSync(file).mtimeMs);
|
|
361
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
362
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
363
|
+
const hour = String(date.getHours()).padStart(2, "0");
|
|
364
|
+
const minute = String(date.getMinutes()).padStart(2, "0");
|
|
365
|
+
return `${month}/${day} ${hour}:${minute}`;
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
return sessionIdFromPath(file).slice(0, 8) || "session";
|
|
369
|
+
}
|
|
370
|
+
}
|
|
403
371
|
// The Claude Code active source is the echo-ctx cache (<sessionId>.json), whose mtime only bumps when
|
|
404
372
|
// the statusline re-renders — stale during a long turn. The transcript grows every tool call, so it's
|
|
405
373
|
// the true liveness signal. Take the fresher of the two. Cached per cache-file (paths are stable).
|
package/dist/hud/preload.cjs
CHANGED
|
@@ -7,6 +7,9 @@ electron_1.contextBridge.exposeInMainWorld("echomemHud", {
|
|
|
7
7
|
setOpen(open, height) {
|
|
8
8
|
electron_1.ipcRenderer.send("hud:set-open", { open, height });
|
|
9
9
|
},
|
|
10
|
+
setMini(mini) {
|
|
11
|
+
electron_1.ipcRenderer.send("hud:set-mini", { mini });
|
|
12
|
+
},
|
|
10
13
|
startDrag(screenX, screenY) {
|
|
11
14
|
electron_1.ipcRenderer.send("hud:drag-start", { screenX, screenY });
|
|
12
15
|
},
|