claude-relay 2.4.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +75 -32
- package/lib/daemon.js +106 -37
- package/lib/project.js +9 -1
- package/lib/public/app.js +18 -0
- package/lib/public/css/filebrowser.css +7 -2
- package/lib/public/css/overlays.css +19 -2
- package/lib/public/css/sidebar.css +15 -8
- package/lib/public/index.html +2 -3
- package/lib/public/modules/filebrowser.js +28 -11
- package/lib/public/modules/fileicons.js +172 -0
- package/lib/public/modules/notifications.js +14 -1
- package/lib/public/modules/theme.js +2 -2
- package/lib/server.js +1 -1
- package/lib/sessions.js +41 -2
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -41,6 +41,7 @@ var removePath = null;
|
|
|
41
41
|
var listMode = false;
|
|
42
42
|
var dangerouslySkipPermissions = false;
|
|
43
43
|
var headlessMode = false;
|
|
44
|
+
var watchMode = false;
|
|
44
45
|
|
|
45
46
|
for (var i = 0; i < args.length; i++) {
|
|
46
47
|
if (args[i] === "-p" || args[i] === "--port") {
|
|
@@ -56,6 +57,8 @@ for (var i = 0; i < args.length; i++) {
|
|
|
56
57
|
skipUpdate = true;
|
|
57
58
|
} else if (args[i] === "--dev") {
|
|
58
59
|
// Already handled above for CLAUDE_RELAY_HOME, just skip
|
|
60
|
+
} else if (args[i] === "--watch" || args[i] === "-w") {
|
|
61
|
+
watchMode = true;
|
|
59
62
|
} else if (args[i] === "--debug") {
|
|
60
63
|
debugMode = true;
|
|
61
64
|
} else if (args[i] === "-y" || args[i] === "--yes") {
|
|
@@ -406,9 +409,13 @@ async function restartDaemonFromConfig() {
|
|
|
406
409
|
newConfig.pid = child.pid;
|
|
407
410
|
saveConfig(newConfig);
|
|
408
411
|
|
|
409
|
-
// Wait and verify
|
|
410
|
-
|
|
411
|
-
var
|
|
412
|
+
// Wait and verify (retry up to 5 seconds)
|
|
413
|
+
var alive = false;
|
|
414
|
+
for (var rc = 0; rc < 10; rc++) {
|
|
415
|
+
await new Promise(function (resolve) { setTimeout(resolve, 500); });
|
|
416
|
+
alive = await isDaemonAliveAsync(newConfig);
|
|
417
|
+
if (alive) break;
|
|
418
|
+
}
|
|
412
419
|
if (!alive) {
|
|
413
420
|
log(a.red + " Restart failed. Check logs: " + a.reset + logFile);
|
|
414
421
|
process.exit(1);
|
|
@@ -1266,11 +1273,13 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
|
|
|
1266
1273
|
config.pid = child.pid;
|
|
1267
1274
|
saveConfig(config);
|
|
1268
1275
|
|
|
1269
|
-
// Wait for daemon to start
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1276
|
+
// Wait for daemon to start (retry up to 5 seconds)
|
|
1277
|
+
var alive = false;
|
|
1278
|
+
for (var attempt = 0; attempt < 10; attempt++) {
|
|
1279
|
+
await new Promise(function (resolve) { setTimeout(resolve, 500); });
|
|
1280
|
+
alive = await isDaemonAliveAsync(config);
|
|
1281
|
+
if (alive) break;
|
|
1282
|
+
}
|
|
1274
1283
|
if (!alive) {
|
|
1275
1284
|
log(a.red + "Failed to start daemon. Check logs:" + a.reset);
|
|
1276
1285
|
log(a.dim + logFile + a.reset);
|
|
@@ -1363,6 +1372,13 @@ async function devMode(pin, keepAwake, existingPinHash) {
|
|
|
1363
1372
|
intentionalKill = false;
|
|
1364
1373
|
return;
|
|
1365
1374
|
}
|
|
1375
|
+
// Exit code 120 = update restart — respawn daemon with current dev code
|
|
1376
|
+
if (code === 120) {
|
|
1377
|
+
console.log("\x1b[36m[dev]\x1b[0m Update restart — respawning daemon...");
|
|
1378
|
+
console.log("");
|
|
1379
|
+
setTimeout(spawnDaemon, 500);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1366
1382
|
// Unexpected exit — auto restart
|
|
1367
1383
|
console.log("\x1b[33m[dev] Daemon exited (code " + code + "), restarting...\x1b[0m");
|
|
1368
1384
|
setTimeout(spawnDaemon, 500);
|
|
@@ -1382,37 +1398,46 @@ async function devMode(pin, keepAwake, existingPinHash) {
|
|
|
1382
1398
|
}
|
|
1383
1399
|
|
|
1384
1400
|
console.log("\x1b[36m[dev]\x1b[0m Starting relay on port " + port + "...");
|
|
1385
|
-
|
|
1401
|
+
if (watchMode) {
|
|
1402
|
+
console.log("\x1b[36m[dev]\x1b[0m Watching lib/ for changes (excluding lib/public/)");
|
|
1403
|
+
}
|
|
1386
1404
|
console.log("");
|
|
1387
1405
|
|
|
1388
1406
|
spawnDaemon();
|
|
1389
1407
|
|
|
1390
1408
|
// Wait for daemon to be ready, then show CLI menu
|
|
1391
|
-
await new Promise(function (resolve) { setTimeout(resolve, 1000); });
|
|
1392
1409
|
config.pid = child ? child.pid : null;
|
|
1393
1410
|
saveConfig(config);
|
|
1394
1411
|
|
|
1395
|
-
var daemonReady =
|
|
1412
|
+
var daemonReady = false;
|
|
1413
|
+
for (var da = 0; da < 10; da++) {
|
|
1414
|
+
await new Promise(function (resolve) { setTimeout(resolve, 500); });
|
|
1415
|
+
daemonReady = await isDaemonAliveAsync(config);
|
|
1416
|
+
if (daemonReady) break;
|
|
1417
|
+
}
|
|
1396
1418
|
if (daemonReady) {
|
|
1397
1419
|
showServerStarted(config, ip);
|
|
1398
1420
|
}
|
|
1399
1421
|
|
|
1400
|
-
// Watch lib/ for server-side file changes
|
|
1401
|
-
var watcher =
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1422
|
+
// Watch lib/ for server-side file changes (only with --watch)
|
|
1423
|
+
var watcher = null;
|
|
1424
|
+
if (watchMode) {
|
|
1425
|
+
watcher = fs.watch(libDir, { recursive: true }, function (eventType, filename) {
|
|
1426
|
+
if (!filename) return;
|
|
1427
|
+
// Skip client-side files — they're served from disk
|
|
1428
|
+
if (filename.startsWith("public" + path.sep) || filename.startsWith("public/")) return;
|
|
1429
|
+
// Skip non-JS files
|
|
1430
|
+
if (!filename.endsWith(".js")) return;
|
|
1431
|
+
|
|
1432
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1433
|
+
debounceTimer = setTimeout(function () {
|
|
1434
|
+
console.log("\x1b[36m[dev]\x1b[0m File changed: lib/" + filename);
|
|
1435
|
+
console.log("\x1b[36m[dev]\x1b[0m Restarting...");
|
|
1436
|
+
console.log("");
|
|
1437
|
+
restartDaemon();
|
|
1438
|
+
}, 300);
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1416
1441
|
|
|
1417
1442
|
// Clean exit on Ctrl+C
|
|
1418
1443
|
var shuttingDown = false;
|
|
@@ -1420,7 +1445,7 @@ async function devMode(pin, keepAwake, existingPinHash) {
|
|
|
1420
1445
|
if (shuttingDown) return;
|
|
1421
1446
|
shuttingDown = true;
|
|
1422
1447
|
console.log("\n\x1b[36m[dev]\x1b[0m Shutting down...");
|
|
1423
|
-
watcher.close();
|
|
1448
|
+
if (watcher) watcher.close();
|
|
1424
1449
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1425
1450
|
intentionalKill = true;
|
|
1426
1451
|
if (child) {
|
|
@@ -1498,9 +1523,12 @@ async function restartDaemonWithTLS(config, callback) {
|
|
|
1498
1523
|
newConfig.pid = child.pid;
|
|
1499
1524
|
saveConfig(newConfig);
|
|
1500
1525
|
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1526
|
+
var alive = false;
|
|
1527
|
+
for (var ra = 0; ra < 10; ra++) {
|
|
1528
|
+
await new Promise(function (resolve) { setTimeout(resolve, 500); });
|
|
1529
|
+
alive = await isDaemonAliveAsync(newConfig);
|
|
1530
|
+
if (alive) break;
|
|
1531
|
+
}
|
|
1504
1532
|
if (!alive) {
|
|
1505
1533
|
log(sym.warn + " " + a.yellow + "Failed to restart with HTTPS, falling back to HTTP..." + a.reset);
|
|
1506
1534
|
// Re-fork without TLS so the server is at least running
|
|
@@ -1519,7 +1547,11 @@ async function restartDaemonWithTLS(config, callback) {
|
|
|
1519
1547
|
fs.closeSync(logFd2);
|
|
1520
1548
|
newConfig.pid = child2.pid;
|
|
1521
1549
|
saveConfig(newConfig);
|
|
1522
|
-
|
|
1550
|
+
for (var rb = 0; rb < 10; rb++) {
|
|
1551
|
+
await new Promise(function (resolve) { setTimeout(resolve, 500); });
|
|
1552
|
+
var retryAlive = await isDaemonAliveAsync(newConfig);
|
|
1553
|
+
if (retryAlive) break;
|
|
1554
|
+
}
|
|
1523
1555
|
startDaemonWatcher();
|
|
1524
1556
|
callback(newConfig);
|
|
1525
1557
|
return;
|
|
@@ -2204,6 +2236,17 @@ var currentVersion = require("../package.json").version;
|
|
|
2204
2236
|
}
|
|
2205
2237
|
|
|
2206
2238
|
if (alive) {
|
|
2239
|
+
// Headless mode — daemon already running, just report and exit
|
|
2240
|
+
if (headlessMode) {
|
|
2241
|
+
var protocol = config.tls ? "https" : "http";
|
|
2242
|
+
var ip = getLocalIP();
|
|
2243
|
+
var url = protocol + "://" + ip + ":" + config.port;
|
|
2244
|
+
console.log(" " + sym.done + " Daemon already running (PID " + config.pid + ")");
|
|
2245
|
+
console.log(" " + sym.done + " " + url);
|
|
2246
|
+
process.exit(0);
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2207
2250
|
// Daemon is running — auto-add cwd if needed, then show menu
|
|
2208
2251
|
var ip = getLocalIP();
|
|
2209
2252
|
|
package/lib/daemon.js
CHANGED
|
@@ -234,13 +234,103 @@ var ipc = createIPCServer(socketPath(), function (msg) {
|
|
|
234
234
|
gracefulShutdown();
|
|
235
235
|
return { ok: true };
|
|
236
236
|
|
|
237
|
+
case "update": {
|
|
238
|
+
console.log("[daemon] Update & restart requested via IPC");
|
|
239
|
+
|
|
240
|
+
// Dev mode (config.debug): just exit with code 120, cli.js dev watcher respawns daemon
|
|
241
|
+
if (config.debug) {
|
|
242
|
+
console.log("[daemon] Dev mode — restarting via dev watcher");
|
|
243
|
+
updateHandoff = true;
|
|
244
|
+
setTimeout(function () { gracefulShutdown(); }, 100);
|
|
245
|
+
return { ok: true };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Production: fetch latest via npx, then spawn updated daemon
|
|
249
|
+
var { execSync: execSyncUpd, spawn: spawnUpd } = require("child_process");
|
|
250
|
+
var updDaemonScript;
|
|
251
|
+
try {
|
|
252
|
+
// npx downloads the package and puts a bin symlink; `which` prints its path
|
|
253
|
+
var binPath = execSyncUpd(
|
|
254
|
+
"npx --yes --package=claude-relay@latest -- which claude-relay",
|
|
255
|
+
{ stdio: ["ignore", "pipe", "pipe"], timeout: 120000, encoding: "utf8" }
|
|
256
|
+
).trim();
|
|
257
|
+
// Resolve symlink to get the actual package directory
|
|
258
|
+
var realBin = fs.realpathSync(binPath);
|
|
259
|
+
updDaemonScript = path.join(path.dirname(realBin), "..", "lib", "daemon.js");
|
|
260
|
+
updDaemonScript = path.resolve(updDaemonScript);
|
|
261
|
+
console.log("[daemon] Resolved updated daemon:", updDaemonScript);
|
|
262
|
+
} catch (updErr) {
|
|
263
|
+
console.log("[daemon] npx resolve failed:", updErr.message);
|
|
264
|
+
// Fallback: restart with current code
|
|
265
|
+
updDaemonScript = path.join(__dirname, "daemon.js");
|
|
266
|
+
}
|
|
267
|
+
// Spawn new daemon process — it will retry if port is still in use
|
|
268
|
+
var { logPath: updLogPath, configPath: updConfigPath } = require("./config");
|
|
269
|
+
var updLogFd = fs.openSync(updLogPath(), "a");
|
|
270
|
+
var updChild = spawnUpd(process.execPath, [updDaemonScript], {
|
|
271
|
+
detached: true,
|
|
272
|
+
windowsHide: true,
|
|
273
|
+
stdio: ["ignore", updLogFd, updLogFd],
|
|
274
|
+
env: Object.assign({}, process.env, {
|
|
275
|
+
CLAUDE_RELAY_CONFIG: updConfigPath(),
|
|
276
|
+
}),
|
|
277
|
+
});
|
|
278
|
+
updChild.unref();
|
|
279
|
+
fs.closeSync(updLogFd);
|
|
280
|
+
config.pid = updChild.pid;
|
|
281
|
+
saveConfig(config);
|
|
282
|
+
console.log("[daemon] Spawned new daemon (PID " + updChild.pid + "), shutting down...");
|
|
283
|
+
updateHandoff = true;
|
|
284
|
+
setTimeout(function () { gracefulShutdown(); }, 100);
|
|
285
|
+
return { ok: true };
|
|
286
|
+
}
|
|
287
|
+
|
|
237
288
|
default:
|
|
238
289
|
return { ok: false, error: "unknown command: " + msg.cmd };
|
|
239
290
|
}
|
|
240
291
|
});
|
|
241
292
|
|
|
242
|
-
// --- Start listening ---
|
|
293
|
+
// --- Start listening (with retry for port-in-use during update handoff) ---
|
|
294
|
+
var listenRetries = 0;
|
|
295
|
+
var MAX_LISTEN_RETRIES = 15;
|
|
296
|
+
|
|
297
|
+
function startListening() {
|
|
298
|
+
relay.server.listen(config.port, function () {
|
|
299
|
+
var protocol = tlsOptions ? "https" : "http";
|
|
300
|
+
console.log("[daemon] Listening on " + protocol + "://0.0.0.0:" + config.port);
|
|
301
|
+
console.log("[daemon] PID:", process.pid);
|
|
302
|
+
console.log("[daemon] Projects:", config.projects.length);
|
|
303
|
+
|
|
304
|
+
// Update PID in config
|
|
305
|
+
config.pid = process.pid;
|
|
306
|
+
saveConfig(config);
|
|
307
|
+
|
|
308
|
+
// Check for crash info from a previous crash and notify clients
|
|
309
|
+
var crashInfo = readCrashInfo();
|
|
310
|
+
if (crashInfo) {
|
|
311
|
+
console.log("[daemon] Recovered from crash at", new Date(crashInfo.time).toISOString());
|
|
312
|
+
console.log("[daemon] Crash reason:", crashInfo.reason);
|
|
313
|
+
// Delay notification so clients have time to reconnect
|
|
314
|
+
setTimeout(function () {
|
|
315
|
+
relay.broadcastAll({
|
|
316
|
+
type: "toast",
|
|
317
|
+
level: "warn",
|
|
318
|
+
message: "Server recovered from a crash and was automatically restarted.",
|
|
319
|
+
detail: crashInfo.reason || null,
|
|
320
|
+
});
|
|
321
|
+
}, 3000);
|
|
322
|
+
clearCrashInfo();
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
243
327
|
relay.server.on("error", function (err) {
|
|
328
|
+
if (err.code === "EADDRINUSE" && listenRetries < MAX_LISTEN_RETRIES) {
|
|
329
|
+
listenRetries++;
|
|
330
|
+
console.log("[daemon] Port " + config.port + " in use, retrying (" + listenRetries + "/" + MAX_LISTEN_RETRIES + ")...");
|
|
331
|
+
setTimeout(startListening, 1000);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
244
334
|
console.error("[daemon] Server error:", err.message);
|
|
245
335
|
writeCrashInfo({
|
|
246
336
|
reason: "Server error: " + err.message,
|
|
@@ -250,33 +340,7 @@ relay.server.on("error", function (err) {
|
|
|
250
340
|
process.exit(1);
|
|
251
341
|
});
|
|
252
342
|
|
|
253
|
-
|
|
254
|
-
var protocol = tlsOptions ? "https" : "http";
|
|
255
|
-
console.log("[daemon] Listening on " + protocol + "://0.0.0.0:" + config.port);
|
|
256
|
-
console.log("[daemon] PID:", process.pid);
|
|
257
|
-
console.log("[daemon] Projects:", config.projects.length);
|
|
258
|
-
|
|
259
|
-
// Update PID in config
|
|
260
|
-
config.pid = process.pid;
|
|
261
|
-
saveConfig(config);
|
|
262
|
-
|
|
263
|
-
// Check for crash info from a previous crash and notify clients
|
|
264
|
-
var crashInfo = readCrashInfo();
|
|
265
|
-
if (crashInfo) {
|
|
266
|
-
console.log("[daemon] Recovered from crash at", new Date(crashInfo.time).toISOString());
|
|
267
|
-
console.log("[daemon] Crash reason:", crashInfo.reason);
|
|
268
|
-
// Delay notification so clients have time to reconnect
|
|
269
|
-
setTimeout(function () {
|
|
270
|
-
relay.broadcastAll({
|
|
271
|
-
type: "toast",
|
|
272
|
-
level: "warn",
|
|
273
|
-
message: "Server recovered from a crash and was automatically restarted.",
|
|
274
|
-
detail: crashInfo.reason || null,
|
|
275
|
-
});
|
|
276
|
-
}, 3000);
|
|
277
|
-
clearCrashInfo();
|
|
278
|
-
}
|
|
279
|
-
});
|
|
343
|
+
startListening();
|
|
280
344
|
|
|
281
345
|
// --- HTTP onboarding server (only when TLS is active) ---
|
|
282
346
|
if (relay.onboardingServer) {
|
|
@@ -300,8 +364,11 @@ if (config.keepAwake && process.platform === "darwin") {
|
|
|
300
364
|
}
|
|
301
365
|
|
|
302
366
|
// --- Graceful shutdown ---
|
|
367
|
+
var updateHandoff = false; // true when shutting down for update (new daemon already spawned)
|
|
368
|
+
|
|
303
369
|
function gracefulShutdown() {
|
|
304
370
|
console.log("[daemon] Shutting down...");
|
|
371
|
+
var exitCode = updateHandoff ? 120 : 0; // 120 = update handoff, don't auto-restart
|
|
305
372
|
|
|
306
373
|
if (caffeinateProc) {
|
|
307
374
|
try { caffeinateProc.kill(); } catch (e) {}
|
|
@@ -309,14 +376,16 @@ function gracefulShutdown() {
|
|
|
309
376
|
|
|
310
377
|
ipc.close();
|
|
311
378
|
|
|
312
|
-
// Remove PID from config
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
379
|
+
// Remove PID from config (skip if update handoff — new daemon PID is already saved)
|
|
380
|
+
if (!updateHandoff) {
|
|
381
|
+
try {
|
|
382
|
+
var c = loadConfig();
|
|
383
|
+
if (c && c.pid === process.pid) {
|
|
384
|
+
delete c.pid;
|
|
385
|
+
saveConfig(c);
|
|
386
|
+
}
|
|
387
|
+
} catch (e) {}
|
|
388
|
+
}
|
|
320
389
|
|
|
321
390
|
relay.destroyAll();
|
|
322
391
|
|
|
@@ -326,7 +395,7 @@ function gracefulShutdown() {
|
|
|
326
395
|
|
|
327
396
|
relay.server.close(function () {
|
|
328
397
|
console.log("[daemon] Server closed");
|
|
329
|
-
process.exit(
|
|
398
|
+
process.exit(exitCode);
|
|
330
399
|
});
|
|
331
400
|
|
|
332
401
|
// Force exit after 5 seconds
|
package/lib/project.js
CHANGED
|
@@ -346,7 +346,7 @@ function createProjectContext(opts) {
|
|
|
346
346
|
if (s.cliSessionId) relayIds[s.cliSessionId] = true;
|
|
347
347
|
});
|
|
348
348
|
try {
|
|
349
|
-
var sessDir =
|
|
349
|
+
var sessDir = sm.sessionsDir;
|
|
350
350
|
var diskFiles = _fs.readdirSync(sessDir);
|
|
351
351
|
for (var fi = 0; fi < diskFiles.length; fi++) {
|
|
352
352
|
if (diskFiles[fi].endsWith(".jsonl")) {
|
|
@@ -406,6 +406,14 @@ function createProjectContext(opts) {
|
|
|
406
406
|
return;
|
|
407
407
|
}
|
|
408
408
|
|
|
409
|
+
if (msg.type === "update_now") {
|
|
410
|
+
send({ type: "update_started", version: latestVersion || "" });
|
|
411
|
+
var _ipc = require("./ipc");
|
|
412
|
+
var _config = require("./config");
|
|
413
|
+
_ipc.sendIPCCommand(_config.socketPath(), { cmd: "update" });
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
409
417
|
if (msg.type === "process_stats") {
|
|
410
418
|
var sessionCount = sm.sessions.size;
|
|
411
419
|
var processingCount = 0;
|
package/lib/public/app.js
CHANGED
|
@@ -1355,6 +1355,12 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
|
|
|
1355
1355
|
if (updateBanner && updateVersion && msg.version) {
|
|
1356
1356
|
updateVersion.textContent = "v" + msg.version;
|
|
1357
1357
|
updateBanner.classList.remove("hidden");
|
|
1358
|
+
// Reset button state (may be stuck on "Updating..." after restart)
|
|
1359
|
+
var updResetBtn = $("update-now");
|
|
1360
|
+
if (updResetBtn) {
|
|
1361
|
+
updResetBtn.textContent = "Update now";
|
|
1362
|
+
updResetBtn.disabled = false;
|
|
1363
|
+
}
|
|
1358
1364
|
refreshIcons();
|
|
1359
1365
|
}
|
|
1360
1366
|
// Show badge on footer update check item
|
|
@@ -1373,6 +1379,18 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
|
|
|
1373
1379
|
}
|
|
1374
1380
|
break;
|
|
1375
1381
|
|
|
1382
|
+
case "update_started":
|
|
1383
|
+
var updNowBtn = $("update-now");
|
|
1384
|
+
if (updNowBtn) {
|
|
1385
|
+
updNowBtn.textContent = "Updating...";
|
|
1386
|
+
updNowBtn.disabled = true;
|
|
1387
|
+
}
|
|
1388
|
+
// Block the entire screen with the connect overlay
|
|
1389
|
+
connectStatusEl.textContent = "Updating" + (msg.version ? " to v" + msg.version : "") + "...";
|
|
1390
|
+
connectOverlay.classList.remove("hidden");
|
|
1391
|
+
startPixelAnim();
|
|
1392
|
+
break;
|
|
1393
|
+
|
|
1376
1394
|
case "slash_commands":
|
|
1377
1395
|
var reserved = new Set(builtinCommands.map(function (c) { return c.name; }));
|
|
1378
1396
|
slashCommands = (msg.commands || []).filter(function (name) {
|
|
@@ -74,9 +74,14 @@
|
|
|
74
74
|
align-items: center;
|
|
75
75
|
justify-content: center;
|
|
76
76
|
width: 16px;
|
|
77
|
+
height: 16px;
|
|
77
78
|
flex-shrink: 0;
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
line-height: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.file-tree-icon svg {
|
|
83
|
+
width: 16px;
|
|
84
|
+
height: 16px;
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
.file-tree-chevron {
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
.update-banner-text .lucide { width: 13px; height: 13px; color: var(--success); }
|
|
92
92
|
.update-banner-text strong { color: var(--text); font-weight: 600; }
|
|
93
93
|
|
|
94
|
-
#update-
|
|
94
|
+
#update-now {
|
|
95
95
|
background: var(--success-15);
|
|
96
96
|
border: 1px solid var(--success-25);
|
|
97
97
|
border-radius: 6px;
|
|
@@ -105,7 +105,24 @@
|
|
|
105
105
|
transition: background 0.15s;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
#update-
|
|
108
|
+
#update-now:hover { background: var(--success-25); }
|
|
109
|
+
#update-now:disabled { opacity: 0.6; cursor: default; }
|
|
110
|
+
|
|
111
|
+
#update-how {
|
|
112
|
+
background: transparent;
|
|
113
|
+
border: 1px solid var(--text-dimmer);
|
|
114
|
+
border-radius: 6px;
|
|
115
|
+
color: var(--text-dimmer);
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
font-family: inherit;
|
|
118
|
+
font-size: 11px;
|
|
119
|
+
font-weight: 600;
|
|
120
|
+
padding: 3px 7px;
|
|
121
|
+
flex-shrink: 0;
|
|
122
|
+
transition: background 0.15s, color 0.15s;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#update-how:hover { background: rgba(var(--overlay-rgb), 0.05); color: var(--text-secondary); }
|
|
109
126
|
|
|
110
127
|
#update-popover {
|
|
111
128
|
display: none;
|
|
@@ -432,13 +432,15 @@
|
|
|
432
432
|
display: flex;
|
|
433
433
|
align-items: center;
|
|
434
434
|
gap: 4px;
|
|
435
|
-
padding:
|
|
435
|
+
padding: 0 36px 0 12px;
|
|
436
|
+
height: 34px;
|
|
436
437
|
border-radius: 10px;
|
|
437
438
|
cursor: pointer;
|
|
438
439
|
font-size: 13px;
|
|
439
440
|
color: var(--text-secondary);
|
|
440
441
|
margin-bottom: 0;
|
|
441
|
-
transition: background 0.
|
|
442
|
+
transition: background 0.35s ease, color 0.35s ease;
|
|
443
|
+
contain: size layout;
|
|
442
444
|
}
|
|
443
445
|
|
|
444
446
|
.session-item-text {
|
|
@@ -471,7 +473,11 @@
|
|
|
471
473
|
}
|
|
472
474
|
|
|
473
475
|
.session-more-btn {
|
|
474
|
-
|
|
476
|
+
position: absolute;
|
|
477
|
+
right: 8px;
|
|
478
|
+
top: 50%;
|
|
479
|
+
transform: translateY(-50%);
|
|
480
|
+
display: flex;
|
|
475
481
|
width: 24px;
|
|
476
482
|
height: 24px;
|
|
477
483
|
border-radius: 6px;
|
|
@@ -479,15 +485,16 @@
|
|
|
479
485
|
background: transparent;
|
|
480
486
|
color: var(--text-muted);
|
|
481
487
|
cursor: pointer;
|
|
482
|
-
flex-shrink: 0;
|
|
483
488
|
align-items: center;
|
|
484
489
|
justify-content: center;
|
|
485
490
|
padding: 0;
|
|
486
|
-
|
|
491
|
+
opacity: 0;
|
|
492
|
+
transition: opacity 0.35s ease, color 0.15s, background 0.15s;
|
|
487
493
|
}
|
|
488
494
|
|
|
489
|
-
.session-more-btn .lucide
|
|
490
|
-
.session-
|
|
495
|
+
.session-more-btn .lucide,
|
|
496
|
+
.session-more-btn svg { width: 14px; height: 14px; display: block; }
|
|
497
|
+
.session-item:hover .session-more-btn { opacity: 1; }
|
|
491
498
|
.session-more-btn:hover { color: var(--text); background: rgba(var(--overlay-rgb), 0.06); }
|
|
492
499
|
|
|
493
500
|
/* --- Session context menu --- */
|
|
@@ -540,7 +547,7 @@
|
|
|
540
547
|
}
|
|
541
548
|
|
|
542
549
|
@media (hover: none) {
|
|
543
|
-
.session-more-btn {
|
|
550
|
+
.session-more-btn { opacity: 1; }
|
|
544
551
|
.msg-copy-hint { opacity: 1; }
|
|
545
552
|
.msg-user[data-uuid] .bubble::after { display: none; }
|
|
546
553
|
.mermaid-diagram::after { display: none; }
|
package/lib/public/index.html
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
14
14
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
15
15
|
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400;14..32,500;14..32,600&family=Styrene+A+Web:wght@400;500&display=swap" rel="stylesheet">
|
|
16
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/file-icons-js@1/css/style.css">
|
|
17
16
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5/css/xterm.min.css">
|
|
18
17
|
<script>
|
|
19
18
|
(function(){try{var k="claude-relay-theme-vars",v=localStorage.getItem(k);if(!v)return;var o=JSON.parse(v),r=document.documentElement,p;for(p in o)r.style.setProperty(p,o[p]);var vt=localStorage.getItem(k.replace("-vars","-variant"));if(vt==="light"){r.classList.add("light-theme");r.classList.remove("dark-theme")}else{r.classList.add("dark-theme");r.classList.remove("light-theme")}var m=document.querySelector('meta[name="theme-color"]');if(m&&o["--bg"])m.setAttribute("content",o["--bg"])}catch(e){}})();
|
|
@@ -107,7 +106,8 @@
|
|
|
107
106
|
<div id="app">
|
|
108
107
|
<div id="update-banner" class="hidden">
|
|
109
108
|
<span class="update-banner-text"><i data-lucide="arrow-up-circle"></i> A new version of Claude Relay is available: <strong id="update-version"></strong></span>
|
|
110
|
-
<button id="update-
|
|
109
|
+
<button id="update-now">Update now</button>
|
|
110
|
+
<button id="update-how" title="Manual update instructions">?</button>
|
|
111
111
|
<button id="update-banner-close" aria-label="Dismiss"><i data-lucide="x"></i></button>
|
|
112
112
|
</div>
|
|
113
113
|
<div id="onboarding-banner" class="hidden">
|
|
@@ -453,7 +453,6 @@
|
|
|
453
453
|
<script src="https://cdn.jsdelivr.net/npm/lucide@0.468.0/dist/umd/lucide.min.js"></script>
|
|
454
454
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
455
455
|
<script src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js"></script>
|
|
456
|
-
<script src="https://cdn.jsdelivr.net/npm/file-icons-js@1/index.js"></script>
|
|
457
456
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5/lib/xterm.min.js"></script>
|
|
458
457
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0/lib/addon-fit.min.js"></script>
|
|
459
458
|
<script type="module" src="app.js"></script>
|
|
@@ -3,6 +3,7 @@ import { escapeHtml, copyToClipboard } from './utils.js';
|
|
|
3
3
|
import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks } from './markdown.js';
|
|
4
4
|
import { closeSidebar } from './sidebar.js';
|
|
5
5
|
import { renderUnifiedDiff, renderSplitDiff } from './diff.js';
|
|
6
|
+
import { initFileIcons, getFileIconSvg, getFolderIconSvg } from './fileicons.js';
|
|
6
7
|
|
|
7
8
|
var ctx;
|
|
8
9
|
var treeData = {}; // path -> { loaded, children }
|
|
@@ -24,6 +25,14 @@ var pendingFileAt = null; // callback for pending file-at
|
|
|
24
25
|
export function initFileBrowser(_ctx) {
|
|
25
26
|
ctx = _ctx;
|
|
26
27
|
|
|
28
|
+
// Load material file icons in background
|
|
29
|
+
initFileIcons().then(function () {
|
|
30
|
+
// Re-render tree if already loaded, so file icons appear
|
|
31
|
+
if (treeData["."] && treeData["."].loaded) {
|
|
32
|
+
renderTree();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
27
36
|
// Close button
|
|
28
37
|
document.getElementById("file-viewer-close").addEventListener("click", function () {
|
|
29
38
|
closeFileViewer();
|
|
@@ -329,14 +338,21 @@ function renderEntries(container, entries, depth) {
|
|
|
329
338
|
if (entry.type === "dir") {
|
|
330
339
|
row.innerHTML =
|
|
331
340
|
'<span class="file-tree-chevron">' + iconHtml("chevron-right") + '</span>' +
|
|
332
|
-
|
|
341
|
+
'<span class="file-tree-icon file-tree-folder-icon"></span>' +
|
|
333
342
|
'<span class="file-tree-name">' + escapeHtml(entry.name) + '</span>';
|
|
334
343
|
|
|
344
|
+
// Async-load folder icon SVG
|
|
345
|
+
(function (iconEl, name) {
|
|
346
|
+
getFolderIconSvg(name, false, function (svg) {
|
|
347
|
+
iconEl.innerHTML = svg;
|
|
348
|
+
});
|
|
349
|
+
})(row.querySelector(".file-tree-folder-icon"), entry.name);
|
|
350
|
+
|
|
335
351
|
var childContainer = document.createElement("div");
|
|
336
352
|
childContainer.className = "file-tree-children hidden";
|
|
337
353
|
childContainer.dataset.parentPath = entry.path;
|
|
338
354
|
|
|
339
|
-
(function (dirPath, childEl, rowEl) {
|
|
355
|
+
(function (dirPath, childEl, rowEl, folderName) {
|
|
340
356
|
rowEl.addEventListener("click", function (e) {
|
|
341
357
|
e.stopPropagation();
|
|
342
358
|
var isExpanded = rowEl.classList.contains("expanded");
|
|
@@ -356,16 +372,23 @@ function renderEntries(container, entries, depth) {
|
|
|
356
372
|
refreshIcons();
|
|
357
373
|
}
|
|
358
374
|
}
|
|
375
|
+
// Swap folder icon open/closed
|
|
376
|
+
var folderIconEl = rowEl.querySelector(".file-tree-folder-icon");
|
|
377
|
+
if (folderIconEl) {
|
|
378
|
+
getFolderIconSvg(folderName, !isExpanded, function (svg) {
|
|
379
|
+
folderIconEl.innerHTML = svg;
|
|
380
|
+
});
|
|
381
|
+
}
|
|
359
382
|
});
|
|
360
|
-
})(entry.path, childContainer, row);
|
|
383
|
+
})(entry.path, childContainer, row, entry.name);
|
|
361
384
|
|
|
362
385
|
container.appendChild(row);
|
|
363
386
|
container.appendChild(childContainer);
|
|
364
387
|
} else {
|
|
365
|
-
var
|
|
388
|
+
var fileSvg = getFileIconSvg(entry.name);
|
|
366
389
|
row.innerHTML =
|
|
367
390
|
'<span class="file-tree-spacer"></span>' +
|
|
368
|
-
'<span class="file-tree-icon
|
|
391
|
+
'<span class="file-tree-icon">' + fileSvg + '</span>' +
|
|
369
392
|
'<span class="file-tree-name">' + escapeHtml(entry.name) + '</span>';
|
|
370
393
|
|
|
371
394
|
(function (filePath, rowEl) {
|
|
@@ -388,12 +411,6 @@ function renderEntries(container, entries, depth) {
|
|
|
388
411
|
}
|
|
389
412
|
}
|
|
390
413
|
|
|
391
|
-
function getFileIconClass(name) {
|
|
392
|
-
if (typeof FileIcons !== "undefined") {
|
|
393
|
-
return FileIcons.getClassWithColor(name) || "default-icon";
|
|
394
|
-
}
|
|
395
|
-
return "default-icon";
|
|
396
|
-
}
|
|
397
414
|
|
|
398
415
|
// --- File viewer ---
|
|
399
416
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Material Icon Theme file & folder icons
|
|
2
|
+
// Files: material-file-icons via esm.sh (dynamic import, ~475KB lazy-loaded)
|
|
3
|
+
// Folders: material-icon-theme SVGs via jsdelivr CDN (fetched & cached)
|
|
4
|
+
|
|
5
|
+
var materialIcons = null;
|
|
6
|
+
var folderSvgCache = {};
|
|
7
|
+
var loadPromise = null;
|
|
8
|
+
|
|
9
|
+
var FOLDER_CDN = "https://cdn.jsdelivr.net/npm/material-icon-theme@5/icons/";
|
|
10
|
+
|
|
11
|
+
var FOLDER_MAP = {
|
|
12
|
+
"src": "folder-src",
|
|
13
|
+
"lib": "folder-lib",
|
|
14
|
+
"dist": "folder-dist",
|
|
15
|
+
"build": "folder-dist",
|
|
16
|
+
"out": "folder-dist",
|
|
17
|
+
"output": "folder-dist",
|
|
18
|
+
"node_modules": "folder-node",
|
|
19
|
+
"test": "folder-test",
|
|
20
|
+
"tests": "folder-test",
|
|
21
|
+
"__tests__": "folder-test",
|
|
22
|
+
"spec": "folder-test",
|
|
23
|
+
"docs": "folder-docs",
|
|
24
|
+
"doc": "folder-docs",
|
|
25
|
+
"config": "folder-config",
|
|
26
|
+
".config": "folder-config",
|
|
27
|
+
"public": "folder-public",
|
|
28
|
+
"static": "folder-public",
|
|
29
|
+
"assets": "folder-images",
|
|
30
|
+
"images": "folder-images",
|
|
31
|
+
"img": "folder-images",
|
|
32
|
+
"icons": "folder-images",
|
|
33
|
+
"media": "folder-images",
|
|
34
|
+
"components": "folder-components",
|
|
35
|
+
"hooks": "folder-hook",
|
|
36
|
+
"utils": "folder-utils",
|
|
37
|
+
"util": "folder-utils",
|
|
38
|
+
"helpers": "folder-helper",
|
|
39
|
+
"helper": "folder-helper",
|
|
40
|
+
"api": "folder-api",
|
|
41
|
+
"routes": "folder-routes",
|
|
42
|
+
"router": "folder-routes",
|
|
43
|
+
"pages": "folder-layout",
|
|
44
|
+
"views": "folder-views",
|
|
45
|
+
"styles": "folder-css",
|
|
46
|
+
"css": "folder-css",
|
|
47
|
+
"scss": "folder-sass",
|
|
48
|
+
"sass": "folder-sass",
|
|
49
|
+
"scripts": "folder-scripts",
|
|
50
|
+
"bin": "folder-scripts",
|
|
51
|
+
".git": "folder-git",
|
|
52
|
+
".github": "folder-github",
|
|
53
|
+
".vscode": "folder-vscode",
|
|
54
|
+
"docker": "folder-docker",
|
|
55
|
+
".docker": "folder-docker",
|
|
56
|
+
"database": "folder-database",
|
|
57
|
+
"db": "folder-database",
|
|
58
|
+
"server": "folder-server",
|
|
59
|
+
"client": "folder-client",
|
|
60
|
+
"types": "folder-typescript",
|
|
61
|
+
"typings": "folder-typescript",
|
|
62
|
+
"@types": "folder-typescript",
|
|
63
|
+
"modules": "folder-node",
|
|
64
|
+
"packages": "folder-packages",
|
|
65
|
+
"vendor": "folder-lib",
|
|
66
|
+
"env": "folder-environment",
|
|
67
|
+
".env": "folder-environment",
|
|
68
|
+
"mock": "folder-mock",
|
|
69
|
+
"mocks": "folder-mock",
|
|
70
|
+
"__mocks__": "folder-mock",
|
|
71
|
+
"middleware": "folder-middleware",
|
|
72
|
+
"i18n": "folder-i18n",
|
|
73
|
+
"locale": "folder-i18n",
|
|
74
|
+
"locales": "folder-i18n",
|
|
75
|
+
"fonts": "folder-font",
|
|
76
|
+
"font": "folder-font",
|
|
77
|
+
"logs": "folder-log",
|
|
78
|
+
"log": "folder-log",
|
|
79
|
+
"tmp": "folder-temp",
|
|
80
|
+
"temp": "folder-temp",
|
|
81
|
+
"templates": "folder-template",
|
|
82
|
+
"template": "folder-template",
|
|
83
|
+
"ref": "folder-resource",
|
|
84
|
+
"resources": "folder-resource",
|
|
85
|
+
"res": "folder-resource",
|
|
86
|
+
"themes": "folder-theme",
|
|
87
|
+
"theme": "folder-theme",
|
|
88
|
+
"plugins": "folder-plugin",
|
|
89
|
+
"plugin": "folder-plugin",
|
|
90
|
+
"context": "folder-context",
|
|
91
|
+
"contexts": "folder-context",
|
|
92
|
+
"redux": "folder-redux-store",
|
|
93
|
+
"store": "folder-redux-store",
|
|
94
|
+
"stores": "folder-redux-store",
|
|
95
|
+
"controllers": "folder-controller",
|
|
96
|
+
"controller": "folder-controller",
|
|
97
|
+
"models": "folder-database",
|
|
98
|
+
"model": "folder-database",
|
|
99
|
+
"services": "folder-server",
|
|
100
|
+
"service": "folder-server",
|
|
101
|
+
".claude": "folder-config"
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// --- Public API ---
|
|
105
|
+
|
|
106
|
+
export function initFileIcons() {
|
|
107
|
+
if (loadPromise) return loadPromise;
|
|
108
|
+
loadPromise = import("https://esm.sh/material-file-icons@2").then(function (mod) {
|
|
109
|
+
materialIcons = mod;
|
|
110
|
+
}).catch(function (err) {
|
|
111
|
+
console.warn("[fileicons] Failed to load material-file-icons:", err);
|
|
112
|
+
});
|
|
113
|
+
// Pre-fetch default folder icons
|
|
114
|
+
fetchFolderSvg("folder.svg");
|
|
115
|
+
fetchFolderSvg("folder-open.svg");
|
|
116
|
+
return loadPromise;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function getFileIconSvg(filename) {
|
|
120
|
+
if (!materialIcons) return "";
|
|
121
|
+
try {
|
|
122
|
+
var icon = materialIcons.getIcon(filename);
|
|
123
|
+
return icon && icon.svg ? icon.svg : "";
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return "";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function getFolderIconSvg(folderName, isOpen, callback) {
|
|
130
|
+
var lower = folderName.toLowerCase();
|
|
131
|
+
var base = FOLDER_MAP[lower] || "folder";
|
|
132
|
+
var svgName = isOpen ? (base + "-open.svg") : (base + ".svg");
|
|
133
|
+
|
|
134
|
+
// Cache hit
|
|
135
|
+
if (folderSvgCache[svgName]) {
|
|
136
|
+
callback(folderSvgCache[svgName]);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fetchFolderSvg(svgName).then(function (svg) {
|
|
141
|
+
if (svg) {
|
|
142
|
+
callback(svg);
|
|
143
|
+
} else {
|
|
144
|
+
// Fallback to default folder icon
|
|
145
|
+
var fallback = isOpen ? "folder-open.svg" : "folder.svg";
|
|
146
|
+
if (folderSvgCache[fallback]) {
|
|
147
|
+
callback(folderSvgCache[fallback]);
|
|
148
|
+
} else {
|
|
149
|
+
fetchFolderSvg(fallback).then(function (fb) {
|
|
150
|
+
callback(fb || "");
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --- Internal ---
|
|
158
|
+
|
|
159
|
+
function fetchFolderSvg(svgName) {
|
|
160
|
+
if (folderSvgCache[svgName]) {
|
|
161
|
+
return Promise.resolve(folderSvgCache[svgName]);
|
|
162
|
+
}
|
|
163
|
+
return fetch(FOLDER_CDN + svgName).then(function (res) {
|
|
164
|
+
if (!res.ok) return null;
|
|
165
|
+
return res.text();
|
|
166
|
+
}).then(function (text) {
|
|
167
|
+
if (text) folderSvgCache[svgName] = text;
|
|
168
|
+
return text;
|
|
169
|
+
}).catch(function () {
|
|
170
|
+
return null;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
@@ -85,9 +85,10 @@ export function initNotifications(_ctx) {
|
|
|
85
85
|
var banner = $("update-banner");
|
|
86
86
|
var closeBtn = $("update-banner-close");
|
|
87
87
|
var howBtn = $("update-how");
|
|
88
|
+
var updateNowBtn = $("update-now");
|
|
88
89
|
if (!banner) return;
|
|
89
90
|
|
|
90
|
-
// Build popover
|
|
91
|
+
// Build popover (manual update instructions)
|
|
91
92
|
var popover = document.createElement("div");
|
|
92
93
|
popover.id = "update-popover";
|
|
93
94
|
popover.innerHTML =
|
|
@@ -113,6 +114,18 @@ export function initNotifications(_ctx) {
|
|
|
113
114
|
});
|
|
114
115
|
});
|
|
115
116
|
|
|
117
|
+
// "Update now" button — trigger server-side update + restart
|
|
118
|
+
if (updateNowBtn) {
|
|
119
|
+
updateNowBtn.addEventListener("click", function () {
|
|
120
|
+
if (ctx.ws && ctx.connected) {
|
|
121
|
+
ctx.ws.send(JSON.stringify({ type: "update_now" }));
|
|
122
|
+
updateNowBtn.textContent = "Updating...";
|
|
123
|
+
updateNowBtn.disabled = true;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// "?" button — toggle manual instructions popover
|
|
116
129
|
howBtn.addEventListener("click", function (e) {
|
|
117
130
|
e.stopPropagation();
|
|
118
131
|
popover.classList.toggle("visible");
|
|
@@ -151,8 +151,8 @@ function computeVars(theme) {
|
|
|
151
151
|
"--success": b.base0B,
|
|
152
152
|
"--warning": b.base0A,
|
|
153
153
|
"--sidebar-bg": isLight ? darken(b.base00, 0.02) : darken(b.base00, 0.10),
|
|
154
|
-
"--sidebar-hover": mixColors(b.base00, b.base01, 0.5),
|
|
155
|
-
"--sidebar-active": mixColors(b.base01, b.base02, 0.5),
|
|
154
|
+
"--sidebar-hover": isLight ? darken(b.base00, 0.06) : mixColors(b.base00, b.base01, 0.5),
|
|
155
|
+
"--sidebar-active": isLight ? darken(b.base01, 0.05) : mixColors(b.base01, b.base02, 0.5),
|
|
156
156
|
"--accent-8": hexToRgba(b.base09, 0.08),
|
|
157
157
|
"--accent-12": hexToRgba(b.base09, 0.12),
|
|
158
158
|
"--accent-15": hexToRgba(b.base09, 0.15),
|
package/lib/server.js
CHANGED
|
@@ -90,7 +90,7 @@ function serveStatic(urlPath, res) {
|
|
|
90
90
|
var content = fs.readFileSync(filePath);
|
|
91
91
|
var ext = path.extname(filePath);
|
|
92
92
|
var mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
93
|
-
res.writeHead(200, { "Content-Type": mime + "; charset=utf-8" });
|
|
93
|
+
res.writeHead(200, { "Content-Type": mime + "; charset=utf-8", "Cache-Control": "no-cache" });
|
|
94
94
|
res.end(content);
|
|
95
95
|
return true;
|
|
96
96
|
} catch (e) {
|
package/lib/sessions.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
+
const config = require("./config");
|
|
3
4
|
|
|
4
5
|
function createSessionManager(opts) {
|
|
5
6
|
var cwd = opts.cwd;
|
|
@@ -13,10 +14,47 @@ function createSessionManager(opts) {
|
|
|
13
14
|
var slashCommands = null; // shared across sessions
|
|
14
15
|
var skillNames = null; // Claude-only skills to filter from slash menu
|
|
15
16
|
|
|
16
|
-
// --- Session persistence ---
|
|
17
|
-
var
|
|
17
|
+
// --- Session persistence (centralized in ~/.claude-relay/sessions/{encoded-cwd}/) ---
|
|
18
|
+
var encodedCwd = cwd.replace(/\//g, "-");
|
|
19
|
+
var sessionsDir = path.join(config.CONFIG_DIR, "sessions", encodedCwd);
|
|
18
20
|
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
19
21
|
|
|
22
|
+
// Auto-migrate from old per-project location ({cwd}/.claude-relay/sessions/)
|
|
23
|
+
var oldSessionsDir = path.join(cwd, ".claude-relay", "sessions");
|
|
24
|
+
try {
|
|
25
|
+
var oldFiles = fs.readdirSync(oldSessionsDir);
|
|
26
|
+
var migrated = 0;
|
|
27
|
+
for (var mi = 0; mi < oldFiles.length; mi++) {
|
|
28
|
+
if (!oldFiles[mi].endsWith(".jsonl")) continue;
|
|
29
|
+
var oldFilePath = path.join(oldSessionsDir, oldFiles[mi]);
|
|
30
|
+
var newFilePath = path.join(sessionsDir, oldFiles[mi]);
|
|
31
|
+
if (fs.existsSync(newFilePath)) continue;
|
|
32
|
+
try {
|
|
33
|
+
fs.renameSync(oldFilePath, newFilePath);
|
|
34
|
+
migrated++;
|
|
35
|
+
} catch (renameErr) {
|
|
36
|
+
try {
|
|
37
|
+
fs.copyFileSync(oldFilePath, newFilePath);
|
|
38
|
+
fs.unlinkSync(oldFilePath);
|
|
39
|
+
migrated++;
|
|
40
|
+
} catch (copyErr) {}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (migrated > 0) {
|
|
44
|
+
console.log("[sessions] Migrated " + migrated + " session(s) to " + sessionsDir);
|
|
45
|
+
}
|
|
46
|
+
// Clean up old directory if empty
|
|
47
|
+
try {
|
|
48
|
+
if (fs.readdirSync(oldSessionsDir).length === 0) {
|
|
49
|
+
fs.rmdirSync(oldSessionsDir);
|
|
50
|
+
var parentDir = path.join(cwd, ".claude-relay");
|
|
51
|
+
if (fs.readdirSync(parentDir).length === 0) fs.rmdirSync(parentDir);
|
|
52
|
+
}
|
|
53
|
+
} catch (e) {}
|
|
54
|
+
} catch (e) {
|
|
55
|
+
// Old directory doesn't exist — that's fine
|
|
56
|
+
}
|
|
57
|
+
|
|
20
58
|
function sessionFilePath(cliSessionId) {
|
|
21
59
|
return path.join(sessionsDir, cliSessionId + ".jsonl");
|
|
22
60
|
}
|
|
@@ -344,6 +382,7 @@ function createSessionManager(opts) {
|
|
|
344
382
|
get skillNames() { return skillNames; },
|
|
345
383
|
set skillNames(v) { skillNames = v; },
|
|
346
384
|
sessions: sessions,
|
|
385
|
+
sessionsDir: sessionsDir,
|
|
347
386
|
HISTORY_PAGE_SIZE: HISTORY_PAGE_SIZE,
|
|
348
387
|
getActiveSession: getActiveSession,
|
|
349
388
|
createSession: createSession,
|