clay-server 2.39.0-beta.3 → 2.39.0-beta.4
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/lib/daemon.js +103 -0
- package/lib/project-sessions.js +36 -112
- package/lib/public/app.js +1 -3
- package/lib/public/index.html +0 -20
- package/lib/public/modules/app-messages.js +1 -5
- package/lib/public/modules/session-tui-view.js +82 -0
- package/lib/public/modules/sidebar-mobile.js +1 -11
- package/lib/public/modules/sidebar-sessions.js +5 -97
- package/lib/sessions.js +137 -1
- package/lib/users-preferences.js +4 -3
- package/lib/ws-schema.js +0 -3
- package/lib/yoke/adapters/claude-worker.js +19 -0
- package/lib/yoke/adapters/claude.js +18 -0
- package/package.json +1 -1
package/lib/daemon.js
CHANGED
|
@@ -1248,11 +1248,114 @@ var ipc = createIPCServer(socketPath(), function (msg) {
|
|
|
1248
1248
|
}
|
|
1249
1249
|
});
|
|
1250
1250
|
|
|
1251
|
+
// --- Cleanup stale SDK warmup CLI session files ---
|
|
1252
|
+
//
|
|
1253
|
+
// On adapter init the SDK is given a dummy "hi" prompt to discover models,
|
|
1254
|
+
// skills, and capabilities. Even though we abort before any response
|
|
1255
|
+
// arrives, the SDK creates a real CLI session jsonl on disk. Going forward
|
|
1256
|
+
// we delete those inline (see lib/yoke/adapters/claude.js and
|
|
1257
|
+
// claude-worker.js), but installs from before that fix can have a long
|
|
1258
|
+
// tail of "hi"-only sessions polluting the Resume CLI session dialog.
|
|
1259
|
+
//
|
|
1260
|
+
// On every daemon startup we scan REAL_HOME/.claude/projects/*/ and unlink
|
|
1261
|
+
// files that match a strict warmup signature so legitimate "hi" sessions
|
|
1262
|
+
// from real users are never touched. All checks must pass:
|
|
1263
|
+
//
|
|
1264
|
+
// 1. file size < 64 KB (real sessions grow past this fast;
|
|
1265
|
+
// threshold is generous because warmup files include the SDK's
|
|
1266
|
+
// skill_listing / deferred_tools attachments which can run into the
|
|
1267
|
+
// tens of KB on its own)
|
|
1268
|
+
// 2. mtime older than 60 seconds (avoid racing an in-flight warmup)
|
|
1269
|
+
// 3. exactly one user message, content text equals literal "hi"
|
|
1270
|
+
// 4. zero assistant messages (real users virtually always get a reply)
|
|
1271
|
+
//
|
|
1272
|
+
// Multi-user OS-isolated installs have per-user ~/.claude trees that the
|
|
1273
|
+
// daemon process can't traverse; the inline cleanup in claude-worker.js
|
|
1274
|
+
// covers those.
|
|
1275
|
+
function cleanupSdkWarmupSessions() {
|
|
1276
|
+
var projectsRoot = path.join(REAL_HOME, ".claude", "projects");
|
|
1277
|
+
var projectDirs;
|
|
1278
|
+
try { projectDirs = fs.readdirSync(projectsRoot, { withFileTypes: true }); }
|
|
1279
|
+
catch (e) { return; }
|
|
1280
|
+
|
|
1281
|
+
var deleted = 0;
|
|
1282
|
+
var nowMs = Date.now();
|
|
1283
|
+
var MAX_BYTES = 64 * 1024;
|
|
1284
|
+
var MIN_AGE_MS = 60 * 1000;
|
|
1285
|
+
|
|
1286
|
+
for (var di = 0; di < projectDirs.length; di++) {
|
|
1287
|
+
if (!projectDirs[di].isDirectory()) continue;
|
|
1288
|
+
var projDir = path.join(projectsRoot, projectDirs[di].name);
|
|
1289
|
+
var files;
|
|
1290
|
+
try { files = fs.readdirSync(projDir); } catch (e) { continue; }
|
|
1291
|
+
|
|
1292
|
+
for (var fi = 0; fi < files.length; fi++) {
|
|
1293
|
+
var f = files[fi];
|
|
1294
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
1295
|
+
var fp = path.join(projDir, f);
|
|
1296
|
+
|
|
1297
|
+
var st;
|
|
1298
|
+
try { st = fs.statSync(fp); } catch (e) { continue; }
|
|
1299
|
+
if (st.size >= MAX_BYTES) continue;
|
|
1300
|
+
if (nowMs - st.mtimeMs < MIN_AGE_MS) continue;
|
|
1301
|
+
|
|
1302
|
+
var raw;
|
|
1303
|
+
try { raw = fs.readFileSync(fp, "utf8"); } catch (e) { continue; }
|
|
1304
|
+
var lines = raw.split("\n");
|
|
1305
|
+
var userCount = 0;
|
|
1306
|
+
var assistantCount = 0;
|
|
1307
|
+
var userText = null;
|
|
1308
|
+
var parseOk = true;
|
|
1309
|
+
for (var li = 0; li < lines.length; li++) {
|
|
1310
|
+
var line = lines[li];
|
|
1311
|
+
if (!line) continue;
|
|
1312
|
+
var ev;
|
|
1313
|
+
try { ev = JSON.parse(line); } catch (e) { parseOk = false; break; }
|
|
1314
|
+
if (!ev || typeof ev !== "object") continue;
|
|
1315
|
+
if (ev.type === "user") {
|
|
1316
|
+
userCount++;
|
|
1317
|
+
if (userCount > 1) break;
|
|
1318
|
+
var msg = ev.message;
|
|
1319
|
+
var content = msg && msg.content;
|
|
1320
|
+
if (typeof content === "string") {
|
|
1321
|
+
userText = content;
|
|
1322
|
+
} else if (Array.isArray(content)) {
|
|
1323
|
+
var parts = [];
|
|
1324
|
+
for (var ci = 0; ci < content.length; ci++) {
|
|
1325
|
+
if (content[ci] && content[ci].type === "text" && typeof content[ci].text === "string") {
|
|
1326
|
+
parts.push(content[ci].text);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
userText = parts.join("");
|
|
1330
|
+
}
|
|
1331
|
+
} else if (ev.type === "assistant") {
|
|
1332
|
+
assistantCount++;
|
|
1333
|
+
if (assistantCount > 0) break;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
if (!parseOk) continue;
|
|
1337
|
+
if (userCount !== 1) continue;
|
|
1338
|
+
if (assistantCount !== 0) continue;
|
|
1339
|
+
if (userText !== "hi") continue;
|
|
1340
|
+
|
|
1341
|
+
try { fs.unlinkSync(fp); deleted++; } catch (e) {}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
if (deleted > 0) {
|
|
1346
|
+
console.log("[daemon] Removed " + deleted + " stale SDK warmup session file(s)");
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1251
1350
|
// --- Start listening (with retry for port-in-use during update handoff) ---
|
|
1252
1351
|
var listenRetries = 0;
|
|
1253
1352
|
var MAX_LISTEN_RETRIES = 15;
|
|
1254
1353
|
|
|
1255
1354
|
function startListening() {
|
|
1355
|
+
// One-shot cleanup of stale SDK warmup CLI session files from previous
|
|
1356
|
+
// daemon runs (best-effort, never fatal).
|
|
1357
|
+
try { cleanupSdkWarmupSessions(); } catch (e) {}
|
|
1358
|
+
|
|
1256
1359
|
relay.server.listen(config.port, listenHost, function () {
|
|
1257
1360
|
var protocol = tlsOptions ? "https" : "http";
|
|
1258
1361
|
console.log("[daemon] Listening on " + protocol + "://" + listenHost + ":" + config.port);
|
package/lib/project-sessions.js
CHANGED
|
@@ -265,12 +265,21 @@ function attachSessions(ctx) {
|
|
|
265
265
|
return null;
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
|
|
268
|
+
// Prepare a born-TUI session to be rendered via the SDK GUI chat for the
|
|
269
|
+
// current click. Reads the CLI jsonl transcript (cli-sessions.js),
|
|
270
|
+
// populates session.history, and tears
|
|
271
|
+
// down the PTY. The born-TUI marker (session.mode === 'tui') is kept on
|
|
272
|
+
// disk so that flipping claudeOpenMode back to 'tui' later restores the
|
|
273
|
+
// embedded-terminal experience instead of locking the session as GUI.
|
|
274
|
+
//
|
|
275
|
+
// Idempotent: if the PTY is already gone and history is populated, this
|
|
276
|
+
// is a no-op (avoids re-reading jsonl and re-writing the session file on
|
|
277
|
+
// every click while the user is reading the session in GUI mode).
|
|
278
|
+
function prepareTuiSessionForGuiView(session) {
|
|
273
279
|
if (!session || session.cliSessionId == null) return;
|
|
280
|
+
var alreadyHydrated = (typeof session.terminalId !== "number") &&
|
|
281
|
+
Array.isArray(session.history) && session.history.length > 0;
|
|
282
|
+
if (alreadyHydrated) return;
|
|
274
283
|
var cliSess;
|
|
275
284
|
try { cliSess = require("./cli-sessions"); } catch (e) { return; }
|
|
276
285
|
var history = null;
|
|
@@ -278,7 +287,6 @@ function attachSessions(ctx) {
|
|
|
278
287
|
if (Array.isArray(history)) {
|
|
279
288
|
session.history = history;
|
|
280
289
|
}
|
|
281
|
-
session.mode = "gui";
|
|
282
290
|
if (typeof session.terminalId === "number" && tm) {
|
|
283
291
|
try { tm.close(session.terminalId); } catch (e) {}
|
|
284
292
|
}
|
|
@@ -494,101 +502,10 @@ function attachSessions(ctx) {
|
|
|
494
502
|
return true;
|
|
495
503
|
}
|
|
496
504
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
// If Clay already has a persisted meta file for this cliSessionId, read
|
|
502
|
-
// its vendor so resumeSession doesn't silently default to the project's
|
|
503
|
-
// primary vendor (which would break codex sessions after server restart).
|
|
504
|
-
var persistedVendor = null;
|
|
505
|
-
try {
|
|
506
|
-
var _fsResume = require("fs");
|
|
507
|
-
var _pathResume = require("path");
|
|
508
|
-
var metaPath = _pathResume.join(sm.sessionsDir, msg.cliSessionId + ".jsonl");
|
|
509
|
-
if (_fsResume.existsSync(metaPath)) {
|
|
510
|
-
var firstLine = _fsResume.readFileSync(metaPath, "utf8").split("\n", 1)[0];
|
|
511
|
-
try {
|
|
512
|
-
var metaObj = JSON.parse(firstLine);
|
|
513
|
-
if (metaObj && metaObj.type === "meta" && metaObj.vendor) persistedVendor = metaObj.vendor;
|
|
514
|
-
} catch (e) {}
|
|
515
|
-
}
|
|
516
|
-
} catch (e) {}
|
|
517
|
-
|
|
518
|
-
// Try SDK for title first, then fall back to manual parsing
|
|
519
|
-
var titlePromise = adapter.getSessionInfo(msg.cliSessionId, { dir: cwd }).then(function(info) {
|
|
520
|
-
return (info && info.summary) ? info.summary.substring(0, 100) : null;
|
|
521
|
-
}).catch(function() { return null; });
|
|
522
|
-
|
|
523
|
-
Promise.all([
|
|
524
|
-
cliSess.readCliSessionHistory(cwd, msg.cliSessionId),
|
|
525
|
-
titlePromise
|
|
526
|
-
]).then(function(results) {
|
|
527
|
-
var history = results[0];
|
|
528
|
-
var sdkTitle = results[1];
|
|
529
|
-
var title = sdkTitle || "Resumed session";
|
|
530
|
-
if (!sdkTitle) {
|
|
531
|
-
for (var i = 0; i < history.length; i++) {
|
|
532
|
-
if (history[i].type === "user_message" && history[i].text) {
|
|
533
|
-
title = history[i].text.substring(0, 50);
|
|
534
|
-
break;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
var resumed = sm.resumeSession(msg.cliSessionId, { history: history, title: title, vendor: persistedVendor || undefined }, ws);
|
|
539
|
-
if (resumed) ws._clayActiveSession = resumed.localId;
|
|
540
|
-
}).catch(function() {
|
|
541
|
-
var resumed = sm.resumeSession(msg.cliSessionId, persistedVendor ? { vendor: persistedVendor } : undefined, ws);
|
|
542
|
-
if (resumed) ws._clayActiveSession = resumed.localId;
|
|
543
|
-
});
|
|
544
|
-
return true;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
if (msg.type === "list_cli_sessions") {
|
|
548
|
-
var _fs = require("fs");
|
|
549
|
-
// Collect session IDs already in relay (in-memory + persisted on disk)
|
|
550
|
-
var relayIds = {};
|
|
551
|
-
sm.sessions.forEach(function (s) {
|
|
552
|
-
if (s.cliSessionId) relayIds[s.cliSessionId] = true;
|
|
553
|
-
});
|
|
554
|
-
try {
|
|
555
|
-
var sessDir = sm.sessionsDir;
|
|
556
|
-
var diskFiles = _fs.readdirSync(sessDir);
|
|
557
|
-
for (var fi = 0; fi < diskFiles.length; fi++) {
|
|
558
|
-
if (diskFiles[fi].endsWith(".jsonl")) {
|
|
559
|
-
relayIds[diskFiles[fi].replace(".jsonl", "")] = true;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
} catch (e) {}
|
|
563
|
-
|
|
564
|
-
adapter.listSessions({ dir: cwd }).then(function(sdkSessions) {
|
|
565
|
-
var filtered = sdkSessions.filter(function(s) {
|
|
566
|
-
return !relayIds[s.sessionId];
|
|
567
|
-
}).map(function(s) {
|
|
568
|
-
return {
|
|
569
|
-
sessionId: s.sessionId,
|
|
570
|
-
firstPrompt: s.summary || s.firstPrompt || "",
|
|
571
|
-
model: null,
|
|
572
|
-
gitBranch: s.gitBranch || null,
|
|
573
|
-
startTime: s.createdAt ? new Date(s.createdAt).toISOString() : null,
|
|
574
|
-
lastActivity: s.lastModified ? new Date(s.lastModified).toISOString() : null,
|
|
575
|
-
};
|
|
576
|
-
});
|
|
577
|
-
sendTo(ws, { type: "cli_session_list", sessions: filtered });
|
|
578
|
-
}).catch(function() {
|
|
579
|
-
// Fallback to manual parsing if SDK fails
|
|
580
|
-
var cliSessions = require("./cli-sessions");
|
|
581
|
-
cliSessions.listCliSessions(cwd).then(function(sessions) {
|
|
582
|
-
var filtered = sessions.filter(function(s) {
|
|
583
|
-
return !relayIds[s.sessionId];
|
|
584
|
-
});
|
|
585
|
-
sendTo(ws, { type: "cli_session_list", sessions: filtered });
|
|
586
|
-
}).catch(function() {
|
|
587
|
-
sendTo(ws, { type: "cli_session_list", sessions: [] });
|
|
588
|
-
});
|
|
589
|
-
});
|
|
590
|
-
return true;
|
|
591
|
-
}
|
|
505
|
+
// Note: the old `resume_session` and `list_cli_sessions` WS handlers
|
|
506
|
+
// were removed when CLI sessions started being auto-adopted into the
|
|
507
|
+
// unified session list on server start (see sessions.js
|
|
508
|
+
// adoptOrphanedCliSessions). Click a session in the sidebar instead.
|
|
592
509
|
|
|
593
510
|
if (msg.type === "switch_session") {
|
|
594
511
|
if (msg.id && sm.sessions.has(msg.id)) {
|
|
@@ -598,21 +515,24 @@ function attachSessions(ctx) {
|
|
|
598
515
|
// - born-GUI viewed under TUI pref: spawn a transient PTY running
|
|
599
516
|
// `claude --resume <cliSessionId>`; the session record stays GUI
|
|
600
517
|
// so a later pref flip back to GUI just hides the runtime link.
|
|
601
|
-
// - born-TUI viewed under GUI pref:
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
//
|
|
518
|
+
// - born-TUI viewed under GUI pref: hydrate session.history from
|
|
519
|
+
// the jsonl transcript and tear down the PTY so the session
|
|
520
|
+
// renders via the SDK chat. The born-TUI marker stays on the
|
|
521
|
+
// record so a later pref flip back to TUI restores the
|
|
522
|
+
// embedded-terminal experience.
|
|
605
523
|
//
|
|
606
524
|
// runtimeMode / runtimeTerminalId are set on the session record so
|
|
607
525
|
// the session_switched and session_list broadcasts surface them to
|
|
608
526
|
// the client without sessions.js needing to know about the pref.
|
|
609
527
|
var xmTarget = sm.sessions.get(msg.id);
|
|
610
528
|
if (xmTarget && (xmTarget.vendor === "claude" || !xmTarget.vendor)) {
|
|
529
|
+
var xmPref = getClaudeOpenModeForWs(ws);
|
|
611
530
|
// Born-TUI session whose PTY is gone (typically after a daemon
|
|
612
|
-
// restart)
|
|
613
|
-
//
|
|
614
|
-
//
|
|
615
|
-
|
|
531
|
+
// restart) AND the user wants TUI rendering this click. Respawn
|
|
532
|
+
// via `claude --resume <cliSessionId>` so the rest of the pipeline
|
|
533
|
+
// has a live PTY to point at. Skipped when the user's pref is GUI
|
|
534
|
+
// - we'd just tear it back down two lines later.
|
|
535
|
+
if (xmTarget.mode === "tui" && xmTarget.cliSessionId && tm && xmPref === "tui" &&
|
|
616
536
|
(typeof xmTarget.terminalId !== "number" || !tm.has(xmTarget.terminalId))) {
|
|
617
537
|
var rsSid = xmTarget.cliSessionId;
|
|
618
538
|
var rsLocalId = xmTarget.localId;
|
|
@@ -632,11 +552,15 @@ function attachSessions(ctx) {
|
|
|
632
552
|
if (rsTerm) xmTarget.terminalId = rsTerm.id;
|
|
633
553
|
startTitleWatcher(xmTarget);
|
|
634
554
|
}
|
|
635
|
-
var xmPref = getClaudeOpenModeForWs(ws);
|
|
636
555
|
var xmRuntime = computeRuntimeMode(xmTarget, xmPref);
|
|
637
556
|
if (xmRuntime === "gui" && xmTarget.mode === "tui") {
|
|
638
|
-
|
|
639
|
-
|
|
557
|
+
// Born-TUI session under GUI pref. Hydrate history + drop PTY
|
|
558
|
+
// without flipping session.mode, so flipping back to TUI later
|
|
559
|
+
// restores the embedded-terminal rendering. runtimeMode is set
|
|
560
|
+
// to 'gui' explicitly so the client doesn't fall back to
|
|
561
|
+
// session.mode (which is still 'tui').
|
|
562
|
+
prepareTuiSessionForGuiView(xmTarget);
|
|
563
|
+
xmTarget.runtimeMode = "gui";
|
|
640
564
|
xmTarget.runtimeTerminalId = null;
|
|
641
565
|
} else if (xmRuntime === "tui" && xmTarget.mode === "gui" && xmTarget.cliSessionId) {
|
|
642
566
|
var xmRid = spawnRuntimeTuiPty(xmTarget, ws);
|
package/lib/public/app.js
CHANGED
|
@@ -5,7 +5,7 @@ import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, closeMermaidM
|
|
|
5
5
|
import { initSidebar, updatePageTitle, spawnDustParticles } from './modules/sidebar.js';
|
|
6
6
|
import {
|
|
7
7
|
renderSessionList, handleSearchResults, updateSessionPresence,
|
|
8
|
-
updateSessionBadge
|
|
8
|
+
updateSessionBadge
|
|
9
9
|
} from './modules/sidebar-sessions.js';
|
|
10
10
|
import {
|
|
11
11
|
renderIconStrip, getEmojiCategories, updateProjectBadge
|
|
@@ -99,7 +99,6 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
99
99
|
var hamburgerBtn = $("hamburger-btn");
|
|
100
100
|
var sidebarToggleBtn = $("sidebar-toggle-btn");
|
|
101
101
|
var sidebarExpandBtn = $("sidebar-expand-btn");
|
|
102
|
-
var resumeSessionBtn = $("resume-session-btn");
|
|
103
102
|
var imagePreviewBar = $("image-preview-bar");
|
|
104
103
|
var connectOverlay = $("connect-overlay");
|
|
105
104
|
|
|
@@ -378,7 +377,6 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
378
377
|
sidebarExpandBtn: sidebarExpandBtn,
|
|
379
378
|
hamburgerBtn: hamburgerBtn,
|
|
380
379
|
newSessionBtn: newSessionBtn,
|
|
381
|
-
resumeSessionBtn: resumeSessionBtn,
|
|
382
380
|
headerTitleEl: headerTitleEl,
|
|
383
381
|
showConfirm: showConfirm,
|
|
384
382
|
onFilesTabOpen: function () { loadRootDirectory(); },
|
package/lib/public/index.html
CHANGED
|
@@ -1556,26 +1556,6 @@
|
|
|
1556
1556
|
</div>
|
|
1557
1557
|
</div>
|
|
1558
1558
|
|
|
1559
|
-
<div id="resume-modal" class="hidden">
|
|
1560
|
-
<div class="confirm-backdrop"></div>
|
|
1561
|
-
<div class="confirm-dialog resume-picker-dialog">
|
|
1562
|
-
<div class="resume-modal-title">Resume CLI session</div>
|
|
1563
|
-
<div class="resume-picker-body">
|
|
1564
|
-
<div id="resume-picker-loading" class="resume-picker-loading">
|
|
1565
|
-
<div class="resume-picker-spinner"></div>
|
|
1566
|
-
<span>Loading sessions...</span>
|
|
1567
|
-
</div>
|
|
1568
|
-
<div id="resume-picker-empty" class="resume-picker-empty hidden">
|
|
1569
|
-
No CLI sessions found for this project.
|
|
1570
|
-
</div>
|
|
1571
|
-
<div id="resume-picker-list" class="resume-picker-list hidden"></div>
|
|
1572
|
-
</div>
|
|
1573
|
-
<div class="confirm-actions">
|
|
1574
|
-
<button class="confirm-btn confirm-cancel" id="resume-cancel">Cancel</button>
|
|
1575
|
-
</div>
|
|
1576
|
-
</div>
|
|
1577
|
-
</div>
|
|
1578
|
-
|
|
1579
1559
|
<div id="rewind-modal" class="hidden">
|
|
1580
1560
|
<div class="confirm-backdrop"></div>
|
|
1581
1561
|
<div class="confirm-dialog">
|
|
@@ -10,7 +10,7 @@ import { showToast } from './utils.js';
|
|
|
10
10
|
import { refreshIcons, iconHtml } from './icons.js';
|
|
11
11
|
import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks } from './markdown.js';
|
|
12
12
|
import { updatePageTitle } from './sidebar.js';
|
|
13
|
-
import { renderSessionList, updateSessionPresence,
|
|
13
|
+
import { renderSessionList, updateSessionPresence, handleSearchResults, updateSessionBadge } from './sidebar-sessions.js';
|
|
14
14
|
import { updateDmBadge, renderSidebarPresence, setMentionActive, renderUserStrip } from './sidebar-mates.js';
|
|
15
15
|
import { refreshMobileChatSheet } from './sidebar-mobile.js';
|
|
16
16
|
import { renderMateSessionList, handleMateSearchResults, updateMateSidebarProfile } from './mate-sidebar.js';
|
|
@@ -585,10 +585,6 @@ export function processMessage(msg) {
|
|
|
585
585
|
}
|
|
586
586
|
break;
|
|
587
587
|
|
|
588
|
-
case "cli_session_list":
|
|
589
|
-
populateCliSessionList(msg.sessions || []);
|
|
590
|
-
break;
|
|
591
|
-
|
|
592
588
|
case "session_switched":
|
|
593
589
|
hideHomeHub();
|
|
594
590
|
closeWhatsNewArticle();
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// the new session list; this view tears itself down via handleTermExited.
|
|
16
16
|
|
|
17
17
|
import { getWs } from './ws-ref.js';
|
|
18
|
+
import { store } from './store.js';
|
|
18
19
|
import { openArticle as openWhatsNewArticle } from './whats-new-article.js';
|
|
19
20
|
|
|
20
21
|
// Stable id of the canonical "Why TUI mode?" article in
|
|
@@ -164,10 +165,91 @@ function ensureHostEl() {
|
|
|
164
165
|
xtermContainerEl.style.position = "relative";
|
|
165
166
|
hostEl.appendChild(xtermContainerEl);
|
|
166
167
|
|
|
168
|
+
// Paste-image handling. xterm.js natively pastes text but ignores
|
|
169
|
+
// image/file clipboard payloads, so an image copied in Finder/Preview
|
|
170
|
+
// would silently drop on Cmd+V. We intercept in the capture phase
|
|
171
|
+
// (before xterm's hidden textarea handles the event), upload the
|
|
172
|
+
// image to /api/upload, and inject the returned absolute path into
|
|
173
|
+
// the PTY. Claude CLI accepts file paths as prompt input - the user
|
|
174
|
+
// can then submit with Enter.
|
|
175
|
+
//
|
|
176
|
+
// Capture phase + stopImmediatePropagation on image hits is what
|
|
177
|
+
// keeps xterm from ALSO pasting the file's text representation
|
|
178
|
+
// (Finder copies leave a "filename.png" string in text/plain).
|
|
179
|
+
// Text-only paste falls through so xterm's built-in text paste keeps
|
|
180
|
+
// working unchanged.
|
|
181
|
+
hostEl.addEventListener("paste", handleTuiPaste, true);
|
|
182
|
+
|
|
167
183
|
document.body.appendChild(hostEl);
|
|
168
184
|
return hostEl;
|
|
169
185
|
}
|
|
170
186
|
|
|
187
|
+
function handleTuiPaste(e) {
|
|
188
|
+
var cd = e.clipboardData;
|
|
189
|
+
if (!cd || currentTermId == null) return;
|
|
190
|
+
|
|
191
|
+
// Collect image blobs from both cd.files (Safari/iOS friendly) and
|
|
192
|
+
// cd.items. Plain-text-only paste is left untouched so xterm's built-in
|
|
193
|
+
// text paste keeps working.
|
|
194
|
+
var blobs = [];
|
|
195
|
+
if (cd.files && cd.files.length > 0) {
|
|
196
|
+
for (var i = 0; i < cd.files.length; i++) {
|
|
197
|
+
if (cd.files[i] && cd.files[i].type.indexOf("image/") === 0) blobs.push(cd.files[i]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (blobs.length === 0 && cd.items) {
|
|
201
|
+
for (var j = 0; j < cd.items.length; j++) {
|
|
202
|
+
if (cd.items[j] && cd.items[j].type && cd.items[j].type.indexOf("image/") === 0) {
|
|
203
|
+
var b = cd.items[j].getAsFile();
|
|
204
|
+
if (b) blobs.push(b);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (blobs.length === 0) return; // text or other - let xterm handle
|
|
209
|
+
|
|
210
|
+
// Stop xterm's textarea from also seeing this event and pasting the
|
|
211
|
+
// file's text representation (filename) alongside our injected path.
|
|
212
|
+
e.preventDefault();
|
|
213
|
+
e.stopImmediatePropagation();
|
|
214
|
+
for (var k = 0; k < blobs.length; k++) {
|
|
215
|
+
uploadAndInjectPath(blobs[k]);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function uploadAndInjectPath(blob) {
|
|
220
|
+
var ext = ".png";
|
|
221
|
+
if (blob.type === "image/jpeg") ext = ".jpg";
|
|
222
|
+
else if (blob.type === "image/gif") ext = ".gif";
|
|
223
|
+
else if (blob.type === "image/webp") ext = ".webp";
|
|
224
|
+
var name = blob.name || ("pasted-" + Date.now() + ext);
|
|
225
|
+
|
|
226
|
+
var reader = new FileReader();
|
|
227
|
+
reader.onload = function (ev) {
|
|
228
|
+
var dataUrl = ev.target.result;
|
|
229
|
+
var commaIdx = dataUrl.indexOf(",");
|
|
230
|
+
var b64 = commaIdx !== -1 ? dataUrl.substring(commaIdx + 1) : "";
|
|
231
|
+
var basePath = store.get("basePath") || "/";
|
|
232
|
+
var xhr = new XMLHttpRequest();
|
|
233
|
+
xhr.open("POST", basePath + "api/upload");
|
|
234
|
+
xhr.setRequestHeader("Content-Type", "application/json");
|
|
235
|
+
xhr.onload = function () {
|
|
236
|
+
if (xhr.status !== 200) return;
|
|
237
|
+
try {
|
|
238
|
+
var resp = JSON.parse(xhr.responseText);
|
|
239
|
+
if (!resp || !resp.path || currentTermId == null) return;
|
|
240
|
+
var ws = getWs();
|
|
241
|
+
if (!ws || ws.readyState !== 1) return;
|
|
242
|
+
// Wrap in double quotes if the path contains whitespace so the
|
|
243
|
+
// shell / claude CLI treats it as a single token.
|
|
244
|
+
var injected = /\s/.test(resp.path) ? '"' + resp.path + '"' : resp.path;
|
|
245
|
+
ws.send(JSON.stringify({ type: "term_input", id: currentTermId, data: injected }));
|
|
246
|
+
} catch (e) {}
|
|
247
|
+
};
|
|
248
|
+
xhr.send(JSON.stringify({ name: name, data: b64 }));
|
|
249
|
+
};
|
|
250
|
+
reader.readAsDataURL(blob);
|
|
251
|
+
}
|
|
252
|
+
|
|
171
253
|
function syncHostBounds() {
|
|
172
254
|
if (!hostEl) return;
|
|
173
255
|
var messagesEl = document.getElementById("messages");
|
|
@@ -11,8 +11,7 @@ import { getMateSessions } from './mate-sidebar.js';
|
|
|
11
11
|
import { openProjectSettings } from './project-settings.js';
|
|
12
12
|
import {
|
|
13
13
|
getCachedSessions,
|
|
14
|
-
getDateGroup
|
|
15
|
-
openResumePicker
|
|
14
|
+
getDateGroup
|
|
16
15
|
} from './sidebar-sessions.js';
|
|
17
16
|
import {
|
|
18
17
|
getCachedProjectList,
|
|
@@ -710,15 +709,6 @@ function renderMobileSessionsInto(container) {
|
|
|
710
709
|
});
|
|
711
710
|
container.appendChild(newBtn);
|
|
712
711
|
|
|
713
|
-
var importBtn = document.createElement("button");
|
|
714
|
-
importBtn.className = "mobile-session-new";
|
|
715
|
-
importBtn.innerHTML = '<i data-lucide="import" style="width:16px;height:16px"></i> Import session';
|
|
716
|
-
importBtn.addEventListener("click", function () {
|
|
717
|
-
closeMobileSheet();
|
|
718
|
-
setTimeout(function () { openResumePicker(); }, 250);
|
|
719
|
-
});
|
|
720
|
-
container.appendChild(importBtn);
|
|
721
|
-
|
|
722
712
|
// Partition: loop sessions vs normal sessions (same logic as desktop renderSessionList)
|
|
723
713
|
var sessions = getCachedSessions();
|
|
724
714
|
var loopGroups = {};
|
|
@@ -34,15 +34,10 @@ var sessionCtxMenu = null;
|
|
|
34
34
|
var sessionCtxSessionId = null;
|
|
35
35
|
var draggedSessionId = null;
|
|
36
36
|
var draggedSessionBookmarked = false;
|
|
37
|
-
var openResumePickerModal = function () {};
|
|
38
37
|
var headerSearchOpen = false;
|
|
39
38
|
var armedDeleteSessionId = null;
|
|
40
39
|
var armedDeleteTimer = null;
|
|
41
40
|
|
|
42
|
-
export function openResumePicker() {
|
|
43
|
-
openResumePickerModal();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
41
|
function sendSessionBookmark(sessionId, bookmarked) {
|
|
47
42
|
if (getWs() && store.get('connected')) {
|
|
48
43
|
getWs().send(JSON.stringify({ type: "set_session_bookmark", sessionId: sessionId, bookmarked: !!bookmarked }));
|
|
@@ -311,10 +306,13 @@ function openClaudeModeMenu(x, y) {
|
|
|
311
306
|
closeClaudeModeMenu();
|
|
312
307
|
var menu = document.createElement("div");
|
|
313
308
|
menu.className = "claude-mode-menu";
|
|
309
|
+
// CLI sessions (created by the `claude` CLI outside Clay) are now
|
|
310
|
+
// auto-adopted into the regular session list on server start, so no
|
|
311
|
+
// separate "Import CLI" entry is needed. Users see all sessions as one
|
|
312
|
+
// list and click renders per their claudeOpenMode pref.
|
|
314
313
|
var items = [
|
|
315
314
|
{ label: "Start as TUI", hint: "Real claude terminal", action: "new", mode: "tui" },
|
|
316
315
|
{ label: "Start as GUI", hint: "Clay chat UI", action: "new", mode: "gui" },
|
|
317
|
-
{ label: "Import CLI", hint: "Resume an existing claude session", action: "import" },
|
|
318
316
|
];
|
|
319
317
|
for (var i = 0; i < items.length; i++) {
|
|
320
318
|
(function (it) {
|
|
@@ -326,9 +324,7 @@ function openClaudeModeMenu(x, y) {
|
|
|
326
324
|
b.addEventListener("click", function (e) {
|
|
327
325
|
e.stopPropagation();
|
|
328
326
|
closeClaudeModeMenu();
|
|
329
|
-
if (
|
|
330
|
-
openResumePickerModal();
|
|
331
|
-
} else if (getWs() && store.get('connected')) {
|
|
327
|
+
if (getWs() && store.get('connected')) {
|
|
332
328
|
getWs().send(JSON.stringify({ type: "new_session", vendor: "claude", mode: it.mode }));
|
|
333
329
|
}
|
|
334
330
|
});
|
|
@@ -539,31 +535,6 @@ export function initSidebarSessions() {
|
|
|
539
535
|
}
|
|
540
536
|
|
|
541
537
|
// --- Resume session picker ---
|
|
542
|
-
var resumeModal = document.getElementById("resume-modal");
|
|
543
|
-
var resumeCancel = document.getElementById("resume-cancel");
|
|
544
|
-
var pickerLoading = document.getElementById("resume-picker-loading");
|
|
545
|
-
var pickerEmpty = document.getElementById("resume-picker-empty");
|
|
546
|
-
var pickerList = document.getElementById("resume-picker-list");
|
|
547
|
-
|
|
548
|
-
function openResumeModal() {
|
|
549
|
-
resumeModal.classList.remove("hidden");
|
|
550
|
-
pickerLoading.classList.remove("hidden");
|
|
551
|
-
pickerEmpty.classList.add("hidden");
|
|
552
|
-
pickerList.classList.add("hidden");
|
|
553
|
-
pickerList.innerHTML = "";
|
|
554
|
-
if (getWs() && store.get('connected')) {
|
|
555
|
-
getWs().send(JSON.stringify({ type: "list_cli_sessions" }));
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
openResumePickerModal = openResumeModal;
|
|
559
|
-
|
|
560
|
-
function closeResumeModal() {
|
|
561
|
-
resumeModal.classList.add("hidden");
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
resumeCancel.addEventListener("click", closeResumeModal);
|
|
565
|
-
resumeModal.querySelector(".confirm-backdrop").addEventListener("click", closeResumeModal);
|
|
566
|
-
|
|
567
538
|
// --- Schedule countdown timer ---
|
|
568
539
|
startCountdownTimer();
|
|
569
540
|
}
|
|
@@ -1450,66 +1421,3 @@ function relativeTime(isoString) {
|
|
|
1450
1421
|
return new Date(isoString).toLocaleDateString();
|
|
1451
1422
|
}
|
|
1452
1423
|
|
|
1453
|
-
export function populateCliSessionList(sessions) {
|
|
1454
|
-
var pickerLoading = document.getElementById("resume-picker-loading");
|
|
1455
|
-
var pickerEmpty = document.getElementById("resume-picker-empty");
|
|
1456
|
-
var pickerList = document.getElementById("resume-picker-list");
|
|
1457
|
-
if (!pickerLoading || !pickerList) return;
|
|
1458
|
-
|
|
1459
|
-
pickerLoading.classList.add("hidden");
|
|
1460
|
-
|
|
1461
|
-
if (!sessions || sessions.length === 0) {
|
|
1462
|
-
pickerEmpty.classList.remove("hidden");
|
|
1463
|
-
pickerList.classList.add("hidden");
|
|
1464
|
-
return;
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
pickerEmpty.classList.add("hidden");
|
|
1468
|
-
pickerList.classList.remove("hidden");
|
|
1469
|
-
pickerList.innerHTML = "";
|
|
1470
|
-
|
|
1471
|
-
for (var i = 0; i < sessions.length; i++) {
|
|
1472
|
-
var s = sessions[i];
|
|
1473
|
-
var item = document.createElement("div");
|
|
1474
|
-
item.className = "cli-session-item";
|
|
1475
|
-
|
|
1476
|
-
var title = document.createElement("div");
|
|
1477
|
-
title.className = "cli-session-title";
|
|
1478
|
-
title.textContent = s.firstPrompt || "Untitled session";
|
|
1479
|
-
item.appendChild(title);
|
|
1480
|
-
|
|
1481
|
-
var meta = document.createElement("div");
|
|
1482
|
-
meta.className = "cli-session-meta";
|
|
1483
|
-
if (s.lastActivity) {
|
|
1484
|
-
var time = document.createElement("span");
|
|
1485
|
-
time.textContent = relativeTime(s.lastActivity);
|
|
1486
|
-
meta.appendChild(time);
|
|
1487
|
-
}
|
|
1488
|
-
if (s.model) {
|
|
1489
|
-
var model = document.createElement("span");
|
|
1490
|
-
model.className = "badge";
|
|
1491
|
-
model.textContent = s.model;
|
|
1492
|
-
meta.appendChild(model);
|
|
1493
|
-
}
|
|
1494
|
-
if (s.gitBranch) {
|
|
1495
|
-
var branch = document.createElement("span");
|
|
1496
|
-
branch.className = "badge";
|
|
1497
|
-
branch.textContent = s.gitBranch;
|
|
1498
|
-
meta.appendChild(branch);
|
|
1499
|
-
}
|
|
1500
|
-
item.appendChild(meta);
|
|
1501
|
-
|
|
1502
|
-
(function (sessionId) {
|
|
1503
|
-
item.addEventListener("click", function () {
|
|
1504
|
-
if (getWs() && store.get('connected')) {
|
|
1505
|
-
getWs().send(JSON.stringify({ type: "resume_session", cliSessionId: sessionId }));
|
|
1506
|
-
}
|
|
1507
|
-
var modal = document.getElementById("resume-modal");
|
|
1508
|
-
if (modal) modal.classList.add("hidden");
|
|
1509
|
-
closeSidebar();
|
|
1510
|
-
});
|
|
1511
|
-
})(s.sessionId);
|
|
1512
|
-
|
|
1513
|
-
pickerList.appendChild(item);
|
|
1514
|
-
}
|
|
1515
|
-
}
|
package/lib/sessions.js
CHANGED
|
@@ -222,8 +222,144 @@ function createSessionManager(opts) {
|
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
//
|
|
225
|
+
// Adopt orphaned CLI sessions from ~/.claude/projects/<encoded-cwd>/ as
|
|
226
|
+
// Clay session records. After this runs the sidebar shows a single
|
|
227
|
+
// unified list of sessions regardless of whether they were born inside
|
|
228
|
+
// Clay or via the `claude` CLI directly. The user's claudeOpenMode pref
|
|
229
|
+
// decides how each click renders (TUI respawn vs GUI hydration) - both
|
|
230
|
+
// paths already exist for born-TUI sessions.
|
|
231
|
+
//
|
|
232
|
+
// Adopted records are saved with mode='tui' because they originated in
|
|
233
|
+
// the CLI, not via the SDK. The cross-mode click logic in
|
|
234
|
+
// project-sessions.js (prepareTuiSessionForGuiView + respawn) handles
|
|
235
|
+
// rendering them in either mode without further special-casing.
|
|
236
|
+
//
|
|
237
|
+
// Strict skip rules:
|
|
238
|
+
// - cliSessionId already known to Clay (avoids duplicate records)
|
|
239
|
+
// - File has zero user messages (incomplete / corrupted file)
|
|
240
|
+
// - Warmup shape: 1 user message that is literal "hi", 0 assistant
|
|
241
|
+
// messages (covered by daemon cleanup but defensive here too)
|
|
242
|
+
function adoptOrphanedCliSessions() {
|
|
243
|
+
var encodedCwd = utils.encodeCwd(cwd);
|
|
244
|
+
var cliDir = path.join(config.REAL_HOME, ".claude", "projects", encodedCwd);
|
|
245
|
+
var files;
|
|
246
|
+
try { files = fs.readdirSync(cliDir); } catch (e) { return; }
|
|
247
|
+
|
|
248
|
+
// Build set of cliSessionIds Clay already tracks
|
|
249
|
+
var knownCliIds = new Set();
|
|
250
|
+
sessions.forEach(function (s) {
|
|
251
|
+
if (s.cliSessionId) knownCliIds.add(s.cliSessionId);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
var MAX_READ = 64 * 1024; // first 64KB is enough for first user message
|
|
255
|
+
var adopted = 0;
|
|
256
|
+
|
|
257
|
+
for (var i = 0; i < files.length; i++) {
|
|
258
|
+
if (!files[i].endsWith(".jsonl")) continue;
|
|
259
|
+
var cliSid = files[i].slice(0, -".jsonl".length);
|
|
260
|
+
if (knownCliIds.has(cliSid)) continue;
|
|
261
|
+
|
|
262
|
+
var fp = path.join(cliDir, files[i]);
|
|
263
|
+
var raw;
|
|
264
|
+
try {
|
|
265
|
+
var stat = fs.statSync(fp);
|
|
266
|
+
var fd = fs.openSync(fp, "r");
|
|
267
|
+
var bytesToRead = Math.min(stat.size, MAX_READ);
|
|
268
|
+
var buf = Buffer.alloc(bytesToRead);
|
|
269
|
+
fs.readSync(fd, buf, 0, bytesToRead, 0);
|
|
270
|
+
fs.closeSync(fd);
|
|
271
|
+
raw = buf.toString("utf8");
|
|
272
|
+
} catch (e) { continue; }
|
|
273
|
+
|
|
274
|
+
var lines = raw.split("\n");
|
|
275
|
+
var userCount = 0;
|
|
276
|
+
var assistantCount = 0;
|
|
277
|
+
var firstUserText = null;
|
|
278
|
+
var createdAtIso = null;
|
|
279
|
+
for (var li = 0; li < lines.length; li++) {
|
|
280
|
+
var line = lines[li];
|
|
281
|
+
if (!line) continue;
|
|
282
|
+
var ev;
|
|
283
|
+
try { ev = JSON.parse(line); } catch (e) { continue; }
|
|
284
|
+
if (!ev || typeof ev !== "object") continue;
|
|
285
|
+
if (ev.type === "user" && ev.message && ev.message.role === "user") {
|
|
286
|
+
userCount++;
|
|
287
|
+
if (firstUserText == null) {
|
|
288
|
+
var c = ev.message.content;
|
|
289
|
+
if (typeof c === "string") {
|
|
290
|
+
firstUserText = c;
|
|
291
|
+
} else if (Array.isArray(c)) {
|
|
292
|
+
var parts = [];
|
|
293
|
+
for (var ci = 0; ci < c.length; ci++) {
|
|
294
|
+
if (c[ci] && c[ci].type === "text" && typeof c[ci].text === "string") parts.push(c[ci].text);
|
|
295
|
+
}
|
|
296
|
+
firstUserText = parts.join("");
|
|
297
|
+
}
|
|
298
|
+
if (ev.timestamp && !createdAtIso) createdAtIso = ev.timestamp;
|
|
299
|
+
}
|
|
300
|
+
} else if (ev.type === "assistant") {
|
|
301
|
+
assistantCount++;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (userCount === 0) continue;
|
|
306
|
+
// Defensive warmup skip (daemon also cleans these)
|
|
307
|
+
if (userCount === 1 && assistantCount === 0 && firstUserText === "hi") continue;
|
|
308
|
+
|
|
309
|
+
// Title: first user message trimmed/truncated. Fall back to a
|
|
310
|
+
// neutral label so the sidebar entry is identifiable.
|
|
311
|
+
var title = (firstUserText || "").trim().replace(/\s+/g, " ");
|
|
312
|
+
if (title.length > 60) title = title.slice(0, 57) + "...";
|
|
313
|
+
if (!title) title = "Imported CLI session";
|
|
314
|
+
|
|
315
|
+
var createdAt = Date.now();
|
|
316
|
+
if (createdAtIso) {
|
|
317
|
+
var t = Date.parse(createdAtIso);
|
|
318
|
+
if (!isNaN(t)) createdAt = t;
|
|
319
|
+
}
|
|
320
|
+
var lastActivity = createdAt;
|
|
321
|
+
try { lastActivity = fs.statSync(fp).mtimeMs; } catch (e) {}
|
|
322
|
+
|
|
323
|
+
var localId = nextLocalId++;
|
|
324
|
+
var session = {
|
|
325
|
+
localId: localId,
|
|
326
|
+
queryInstance: null,
|
|
327
|
+
messageQueue: null,
|
|
328
|
+
cliSessionId: cliSid,
|
|
329
|
+
blocks: {},
|
|
330
|
+
sentToolResults: {},
|
|
331
|
+
pendingPermissions: {},
|
|
332
|
+
pendingAskUser: {},
|
|
333
|
+
isProcessing: false,
|
|
334
|
+
title: title,
|
|
335
|
+
createdAt: createdAt,
|
|
336
|
+
lastActivity: lastActivity,
|
|
337
|
+
history: [],
|
|
338
|
+
messageUUIDs: [],
|
|
339
|
+
lastRewindUuid: null,
|
|
340
|
+
vendor: "claude",
|
|
341
|
+
mode: "tui",
|
|
342
|
+
terminalId: null,
|
|
343
|
+
runtimeMode: null,
|
|
344
|
+
runtimeTerminalId: null,
|
|
345
|
+
sessionVisibility: "shared",
|
|
346
|
+
bookmarked: false,
|
|
347
|
+
favoriteOrder: null,
|
|
348
|
+
};
|
|
349
|
+
sessions.set(localId, session);
|
|
350
|
+
try { saveSessionFile(session); } catch (e) {}
|
|
351
|
+
knownCliIds.add(cliSid);
|
|
352
|
+
adopted++;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (adopted > 0) {
|
|
356
|
+
console.log("[sessions] Adopted " + adopted + " CLI session(s) for " + cwd);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Load persisted sessions from disk, then adopt any orphan CLI sessions
|
|
226
361
|
loadSessions();
|
|
362
|
+
adoptOrphanedCliSessions();
|
|
227
363
|
|
|
228
364
|
function getActiveSession() {
|
|
229
365
|
return sessions.get(activeSessionId) || null;
|
package/lib/users-preferences.js
CHANGED
|
@@ -234,9 +234,10 @@ function attachPreferences(deps) {
|
|
|
234
234
|
// the cutover persist normally.
|
|
235
235
|
//
|
|
236
236
|
// The preference applies on the next session open. Currently displayed
|
|
237
|
-
// sessions are not re-rendered retroactively.
|
|
238
|
-
//
|
|
239
|
-
//
|
|
237
|
+
// sessions are not re-rendered retroactively. The cross-mode click
|
|
238
|
+
// logic in project-sessions.js handles both directions in-place
|
|
239
|
+
// (prepareTuiSessionForGuiView for tui->gui rendering, claude --resume
|
|
240
|
+
// PTY respawn for gui->tui rendering on a born-tui session).
|
|
240
241
|
|
|
241
242
|
// Single cutover instant for both default flip and one-time reset:
|
|
242
243
|
// 2026-06-15 00:00 in UTC+14 = 2026-06-14 10:00 UTC.
|
package/lib/ws-schema.js
CHANGED
|
@@ -23,11 +23,9 @@ var schema = {
|
|
|
23
23
|
"set_session_bookmark": { direction: "c2s", handler: "lib/project-sessions.js", description: "Bookmark or unbookmark a session in the sidebar" },
|
|
24
24
|
"reorder_session_bookmarks": { direction: "c2s", handler: "lib/project-sessions.js", description: "Reorder favorited sessions within the favorites area" },
|
|
25
25
|
"bulk_delete_sessions": { direction: "c2s", handler: "lib/project-sessions.js", description: "Delete a group of sessions at once" },
|
|
26
|
-
"resume_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Resume a CLI session by its CLI session ID" },
|
|
27
26
|
"set_session_visibility": { direction: "c2s", handler: "lib/project-sessions.js", description: "Show or hide a session in the sidebar" },
|
|
28
27
|
"search_sessions": { direction: "c2s", handler: "lib/project-sessions.js", description: "Search session titles" },
|
|
29
28
|
"search_session_content": { direction: "c2s", handler: "lib/project-sessions.js", description: "Full-text search within a session" },
|
|
30
|
-
"list_cli_sessions": { direction: "c2s", handler: "lib/project-sessions.js", description: "List active CLI sessions available for resume" },
|
|
31
29
|
"load_more_history": { direction: "c2s", handler: "lib/project-sessions.js", description: "Request older history entries for the current session" },
|
|
32
30
|
"fork_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Fork a session from a given message UUID" },
|
|
33
31
|
"input_sync": { direction: "c2s", handler: "lib/project-sessions.js", description: "Sync the current input field text to other clients" },
|
|
@@ -40,7 +38,6 @@ var schema = {
|
|
|
40
38
|
"session_unread": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Unread message count for a session" },
|
|
41
39
|
"search_results": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Session title search results" },
|
|
42
40
|
"search_content_results": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Full-text content search results" },
|
|
43
|
-
"cli_session_list": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "List of resumable CLI sessions" },
|
|
44
41
|
"fork_complete": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Fork succeeded, includes new session ID" },
|
|
45
42
|
"input_sync_broadcast": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Broadcast input text from another client" },
|
|
46
43
|
|
|
@@ -649,6 +649,11 @@ async function handleWarmup(msg) {
|
|
|
649
649
|
warmupOptions.abortController = ac;
|
|
650
650
|
applyWorkerClaudeBinary(warmupOptions);
|
|
651
651
|
|
|
652
|
+
// Capture session_id so we can delete the SDK-created CLI jsonl after
|
|
653
|
+
// abort. Without this every warmup leaves a one-line ("hi") session
|
|
654
|
+
// file in ~/.claude/projects/<encodedCwd>/ that pollutes the Resume
|
|
655
|
+
// CLI session list.
|
|
656
|
+
var warmupSessionId = null;
|
|
652
657
|
try {
|
|
653
658
|
var stream = sdk.query({
|
|
654
659
|
prompt: mq,
|
|
@@ -657,6 +662,7 @@ async function handleWarmup(msg) {
|
|
|
657
662
|
|
|
658
663
|
for await (var event of stream) {
|
|
659
664
|
if (event.type === "system" && event.subtype === "init") {
|
|
665
|
+
warmupSessionId = event.session_id || null;
|
|
660
666
|
var result = {
|
|
661
667
|
slashCommands: event.slash_commands || [],
|
|
662
668
|
model: event.model || "",
|
|
@@ -682,6 +688,19 @@ async function handleWarmup(msg) {
|
|
|
682
688
|
sendToDaemon({ type: "warmup_error", error: "Warmup failed: " + (e.message || e) });
|
|
683
689
|
}
|
|
684
690
|
}
|
|
691
|
+
|
|
692
|
+
// Delete the warmup CLI jsonl. Worker runs as the target OS user so
|
|
693
|
+
// os.homedir() resolves to that user's home, which is where the SDK
|
|
694
|
+
// wrote the file. Best-effort cleanup.
|
|
695
|
+
if (warmupSessionId) {
|
|
696
|
+
try {
|
|
697
|
+
var os = require("os");
|
|
698
|
+
var wmCwd = (warmupOptions && warmupOptions.cwd) || process.cwd();
|
|
699
|
+
var wmEncoded = wmCwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
700
|
+
var wmPath = path.join(os.homedir(), ".claude", "projects", wmEncoded, warmupSessionId + ".jsonl");
|
|
701
|
+
fs.unlinkSync(wmPath);
|
|
702
|
+
} catch (e) {}
|
|
703
|
+
}
|
|
685
704
|
}
|
|
686
705
|
|
|
687
706
|
// --- Cleanup ---
|
|
@@ -1079,11 +1079,18 @@ function createClaudeAdapter(opts) {
|
|
|
1079
1079
|
},
|
|
1080
1080
|
};
|
|
1081
1081
|
|
|
1082
|
+
// Capture the session_id from the system/init event so we can
|
|
1083
|
+
// delete the SDK-created jsonl file after we abort. The SDK starts
|
|
1084
|
+
// a real CLI session as soon as it receives our dummy "hi" prompt;
|
|
1085
|
+
// without cleanup these one-line ("hi") sessions pile up in the
|
|
1086
|
+
// Resume CLI session list.
|
|
1087
|
+
var warmupSessionId = null;
|
|
1082
1088
|
try {
|
|
1083
1089
|
var stream = sdk.query({ prompt: mq, options: warmupOptions });
|
|
1084
1090
|
|
|
1085
1091
|
for await (var msg of stream) {
|
|
1086
1092
|
if (msg.type === "system" && msg.subtype === "init") {
|
|
1093
|
+
warmupSessionId = msg.session_id || null;
|
|
1087
1094
|
result.skills = msg.skills || [];
|
|
1088
1095
|
result.defaultModel = msg.model || "";
|
|
1089
1096
|
result.slashCommands = msg.slash_commands || [];
|
|
@@ -1107,6 +1114,17 @@ function createClaudeAdapter(opts) {
|
|
|
1107
1114
|
}
|
|
1108
1115
|
}
|
|
1109
1116
|
|
|
1117
|
+
// Delete the warmup CLI jsonl file. Best-effort: file may not exist
|
|
1118
|
+
// (race conditions, alt SDK behavior, etc.) - silently skip.
|
|
1119
|
+
if (warmupSessionId) {
|
|
1120
|
+
try {
|
|
1121
|
+
var wmCwd = (initOpts && initOpts.cwd) || _cwd;
|
|
1122
|
+
var wmEncoded = wmCwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
1123
|
+
var wmPath = path.join(os.homedir(), ".claude", "projects", wmEncoded, warmupSessionId + ".jsonl");
|
|
1124
|
+
fs.unlinkSync(wmPath);
|
|
1125
|
+
} catch (e) {}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1110
1128
|
return result;
|
|
1111
1129
|
},
|
|
1112
1130
|
|
package/package.json
CHANGED