clay-server 2.39.0-beta.3 → 2.39.0-beta.5

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 CHANGED
@@ -728,6 +728,19 @@ var relay = createServer({
728
728
  console.log("[daemon] Chat layout:", val, "(web)");
729
729
  return { ok: true, chatLayout: val };
730
730
  },
731
+ onSetTerminalFont: function (family, size) {
732
+ var DEFAULT_FAMILY = "'SF Mono', Menlo, Monaco, 'Courier New', monospace";
733
+ var current = config.terminalFont || { family: DEFAULT_FAMILY, size: 13 };
734
+ var nextFamily = (typeof family === "string" && family.trim()) ? family.trim().slice(0, 200) : current.family;
735
+ var nextSize = current.size;
736
+ if (typeof size === "number" && size >= 9 && size <= 32) {
737
+ nextSize = Math.round(size);
738
+ }
739
+ config.terminalFont = { family: nextFamily, size: nextSize };
740
+ saveConfig(config);
741
+ console.log("[daemon] Terminal font:", nextFamily, nextSize);
742
+ return config.terminalFont;
743
+ },
731
744
  onSetMateOnboarded: function () {
732
745
  config.mateOnboardingShown = true;
733
746
  saveConfig(config);
@@ -1248,11 +1261,114 @@ var ipc = createIPCServer(socketPath(), function (msg) {
1248
1261
  }
1249
1262
  });
1250
1263
 
1264
+ // --- Cleanup stale SDK warmup CLI session files ---
1265
+ //
1266
+ // On adapter init the SDK is given a dummy "hi" prompt to discover models,
1267
+ // skills, and capabilities. Even though we abort before any response
1268
+ // arrives, the SDK creates a real CLI session jsonl on disk. Going forward
1269
+ // we delete those inline (see lib/yoke/adapters/claude.js and
1270
+ // claude-worker.js), but installs from before that fix can have a long
1271
+ // tail of "hi"-only sessions polluting the Resume CLI session dialog.
1272
+ //
1273
+ // On every daemon startup we scan REAL_HOME/.claude/projects/*/ and unlink
1274
+ // files that match a strict warmup signature so legitimate "hi" sessions
1275
+ // from real users are never touched. All checks must pass:
1276
+ //
1277
+ // 1. file size < 64 KB (real sessions grow past this fast;
1278
+ // threshold is generous because warmup files include the SDK's
1279
+ // skill_listing / deferred_tools attachments which can run into the
1280
+ // tens of KB on its own)
1281
+ // 2. mtime older than 60 seconds (avoid racing an in-flight warmup)
1282
+ // 3. exactly one user message, content text equals literal "hi"
1283
+ // 4. zero assistant messages (real users virtually always get a reply)
1284
+ //
1285
+ // Multi-user OS-isolated installs have per-user ~/.claude trees that the
1286
+ // daemon process can't traverse; the inline cleanup in claude-worker.js
1287
+ // covers those.
1288
+ function cleanupSdkWarmupSessions() {
1289
+ var projectsRoot = path.join(REAL_HOME, ".claude", "projects");
1290
+ var projectDirs;
1291
+ try { projectDirs = fs.readdirSync(projectsRoot, { withFileTypes: true }); }
1292
+ catch (e) { return; }
1293
+
1294
+ var deleted = 0;
1295
+ var nowMs = Date.now();
1296
+ var MAX_BYTES = 64 * 1024;
1297
+ var MIN_AGE_MS = 60 * 1000;
1298
+
1299
+ for (var di = 0; di < projectDirs.length; di++) {
1300
+ if (!projectDirs[di].isDirectory()) continue;
1301
+ var projDir = path.join(projectsRoot, projectDirs[di].name);
1302
+ var files;
1303
+ try { files = fs.readdirSync(projDir); } catch (e) { continue; }
1304
+
1305
+ for (var fi = 0; fi < files.length; fi++) {
1306
+ var f = files[fi];
1307
+ if (!f.endsWith(".jsonl")) continue;
1308
+ var fp = path.join(projDir, f);
1309
+
1310
+ var st;
1311
+ try { st = fs.statSync(fp); } catch (e) { continue; }
1312
+ if (st.size >= MAX_BYTES) continue;
1313
+ if (nowMs - st.mtimeMs < MIN_AGE_MS) continue;
1314
+
1315
+ var raw;
1316
+ try { raw = fs.readFileSync(fp, "utf8"); } catch (e) { continue; }
1317
+ var lines = raw.split("\n");
1318
+ var userCount = 0;
1319
+ var assistantCount = 0;
1320
+ var userText = null;
1321
+ var parseOk = true;
1322
+ for (var li = 0; li < lines.length; li++) {
1323
+ var line = lines[li];
1324
+ if (!line) continue;
1325
+ var ev;
1326
+ try { ev = JSON.parse(line); } catch (e) { parseOk = false; break; }
1327
+ if (!ev || typeof ev !== "object") continue;
1328
+ if (ev.type === "user") {
1329
+ userCount++;
1330
+ if (userCount > 1) break;
1331
+ var msg = ev.message;
1332
+ var content = msg && msg.content;
1333
+ if (typeof content === "string") {
1334
+ userText = content;
1335
+ } else if (Array.isArray(content)) {
1336
+ var parts = [];
1337
+ for (var ci = 0; ci < content.length; ci++) {
1338
+ if (content[ci] && content[ci].type === "text" && typeof content[ci].text === "string") {
1339
+ parts.push(content[ci].text);
1340
+ }
1341
+ }
1342
+ userText = parts.join("");
1343
+ }
1344
+ } else if (ev.type === "assistant") {
1345
+ assistantCount++;
1346
+ if (assistantCount > 0) break;
1347
+ }
1348
+ }
1349
+ if (!parseOk) continue;
1350
+ if (userCount !== 1) continue;
1351
+ if (assistantCount !== 0) continue;
1352
+ if (userText !== "hi") continue;
1353
+
1354
+ try { fs.unlinkSync(fp); deleted++; } catch (e) {}
1355
+ }
1356
+ }
1357
+
1358
+ if (deleted > 0) {
1359
+ console.log("[daemon] Removed " + deleted + " stale SDK warmup session file(s)");
1360
+ }
1361
+ }
1362
+
1251
1363
  // --- Start listening (with retry for port-in-use during update handoff) ---
1252
1364
  var listenRetries = 0;
1253
1365
  var MAX_LISTEN_RETRIES = 15;
1254
1366
 
1255
1367
  function startListening() {
1368
+ // One-shot cleanup of stale SDK warmup CLI session files from previous
1369
+ // daemon runs (best-effort, never fatal).
1370
+ try { cleanupSdkWarmupSessions(); } catch (e) {}
1371
+
1256
1372
  relay.server.listen(config.port, listenHost, function () {
1257
1373
  var protocol = tlsOptions ? "https" : "http";
1258
1374
  console.log("[daemon] Listening on " + protocol + "://" + listenHost + ":" + config.port);
@@ -265,12 +265,21 @@ function attachSessions(ctx) {
265
265
  return null;
266
266
  }
267
267
 
268
- // In-place conversion of a TUI session to GUI when the user's preference
269
- // is 'gui'. Reads the jsonl transcript through the existing import path
270
- // (same code Import CLI uses), populates session.history, flips the mode
271
- // to 'gui', and kills the PTY. Subsequent clicks render via the SDK chat.
272
- function convertTuiSessionToGui(session) {
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
- if (msg.type === "resume_session") {
498
- if (!msg.cliSessionId) return true;
499
- var cliSess = require("./cli-sessions");
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: in-place convert via the
602
- // Import CLI code path (jsonl history -> session.history,
603
- // session.mode -> 'gui', kill PTY). This is destructive but
604
- // matches "TUI->GUI is what Import CLI was always for."
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). Respawn via `claude --resume <cliSessionId>` before
613
- // the cross-mode logic runs - the rest of the pipeline assumes
614
- // terminalId points at a live PTY for TUI sessions.
615
- if (xmTarget.mode === "tui" && xmTarget.cliSessionId && tm &&
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
- convertTuiSessionToGui(xmTarget);
639
- xmTarget.runtimeMode = null;
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, populateCliSessionList
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(); },
@@ -1119,35 +1119,6 @@
1119
1119
  margin-left: 12px;
1120
1120
  }
1121
1121
 
1122
- /* TUI session indicator: a tiny black "screen" with a bold green prompt
1123
- glyph, meant to read as a miniature terminal (CRT-style cursor) at
1124
- sidebar scale. Aligned to the middle of the text x-height so it sits
1125
- visually centered with the session title rather than dropping below. */
1126
- .session-tui-icon {
1127
- display: inline-flex;
1128
- align-items: center;
1129
- justify-content: center;
1130
- width: 14px;
1131
- height: 14px;
1132
- border-radius: 3px;
1133
- background: #000;
1134
- margin-right: 6px;
1135
- vertical-align: middle;
1136
- flex-shrink: 0;
1137
- /* Nudge upward a hair so the icon optical-centers with lowercase glyphs
1138
- (lucide icons render slightly below the geometric center). */
1139
- position: relative;
1140
- top: -1px;
1141
- }
1142
- .session-tui-icon svg,
1143
- .session-tui-icon .lucide {
1144
- width: 10px;
1145
- height: 10px;
1146
- color: #50fa7b;
1147
- stroke: #50fa7b;
1148
- stroke-width: 3;
1149
- }
1150
-
1151
1122
  .session-top-action:hover {
1152
1123
  background: var(--sidebar-hover);
1153
1124
  color: var(--text);
@@ -1,3 +1,13 @@
1
+ /* Subtle 1px divider against the sidebar so the embedded terminal
2
+ doesn't visually bleed into the session list. The host is
3
+ position:fixed and anchored to #messages' bounding rect, so its
4
+ left edge sits exactly on the sidebar boundary. Color matches
5
+ #main-area's existing border-left so the line reads as one
6
+ continuous edge with the rest of the chrome. */
7
+ #tui-session-host {
8
+ border-left: 1px solid var(--border-subtle);
9
+ }
10
+
1
11
  /* Policy notice that sits above the embedded xterm in fullscreen TUI
2
12
  sessions, explaining why this mode exists (post-2026-06-15 Agent SDK
3
13
  billing split). Thin so it doesn't eat terminal real estate. */
@@ -859,3 +859,46 @@
859
859
  align-items: stretch;
860
860
  }
861
861
  }
862
+
863
+ /* Terminal font picker row */
864
+ .us-term-font-row {
865
+ display: flex;
866
+ align-items: center;
867
+ gap: 8px;
868
+ flex-wrap: wrap;
869
+ }
870
+ .us-term-font-family {
871
+ flex: 1 1 auto;
872
+ min-width: 180px;
873
+ padding: 7px 10px;
874
+ border: 1px solid var(--border);
875
+ border-radius: 8px;
876
+ background: var(--input-bg);
877
+ color: var(--text);
878
+ font-size: 13px;
879
+ }
880
+ .us-term-font-custom {
881
+ flex: 1 1 100%;
882
+ padding: 7px 10px;
883
+ border: 1px solid var(--border);
884
+ border-radius: 8px;
885
+ background: var(--input-bg);
886
+ color: var(--text);
887
+ font-size: 13px;
888
+ font-family: "Roboto Mono", monospace;
889
+ }
890
+ .us-term-font-custom.hidden { display: none; }
891
+ .us-term-font-size {
892
+ width: 64px;
893
+ padding: 7px 10px;
894
+ border: 1px solid var(--border);
895
+ border-radius: 8px;
896
+ background: var(--input-bg);
897
+ color: var(--text);
898
+ font-size: 13px;
899
+ text-align: center;
900
+ }
901
+ .us-term-font-unit {
902
+ font-size: 13px;
903
+ color: var(--text-dimmer);
904
+ }
@@ -879,7 +879,7 @@
879
879
  </div>
880
880
  </div>
881
881
  <div class="user-island-actions">
882
- <button id="user-theme-toggle-btn" title="Switch theme" aria-label="Switch theme"><i data-lucide="moon"></i></button>
882
+ <button id="user-theme-toggle-btn" title="Themes" aria-label="Themes"><i data-lucide="palette"></i></button>
883
883
  <button id="user-settings-btn" title="User settings"><i data-lucide="settings"></i></button>
884
884
  </div>
885
885
  </div>
@@ -1002,18 +1002,22 @@
1002
1002
  <div class="us-section" data-section="us-appearance">
1003
1003
  <h2>Appearance</h2>
1004
1004
  <div class="settings-card">
1005
- <div class="settings-label" style="margin-bottom:10px;">Theme</div>
1006
- <div class="layout-switcher" id="us-theme-switcher">
1007
- <button class="layout-option" data-theme="light">
1008
- <span class="layout-option-icon">☀️</span>
1009
- <span class="layout-option-label">Light</span>
1010
- <span class="layout-option-desc">For people who open curtains. Bright, clean, productive.</span>
1011
- </button>
1012
- <button class="layout-option" data-theme="dark">
1013
- <span class="layout-option-icon">🌙</span>
1014
- <span class="layout-option-label">Dark</span>
1015
- <span class="layout-option-desc">For those who thrive after sunset. Your screen, your cave.</span>
1016
- </button>
1005
+ <div class="settings-label" style="margin-bottom:10px;">Terminal font</div>
1006
+ <div class="us-term-font-row">
1007
+ <select id="us-term-font-family" class="us-term-font-family">
1008
+ <option value="'SF Mono', Menlo, Monaco, 'Courier New', monospace">SF Mono / Menlo (default)</option>
1009
+ <option value="'JetBrains Mono', 'SF Mono', Menlo, monospace">JetBrains Mono</option>
1010
+ <option value="'Fira Code', 'SF Mono', Menlo, monospace">Fira Code</option>
1011
+ <option value="'Cascadia Code', 'SF Mono', Menlo, monospace">Cascadia Code</option>
1012
+ <option value="'IBM Plex Mono', 'SF Mono', Menlo, monospace">IBM Plex Mono</option>
1013
+ <option value="'Source Code Pro', 'SF Mono', Menlo, monospace">Source Code Pro</option>
1014
+ <option value="'Roboto Mono', 'SF Mono', Menlo, monospace">Roboto Mono</option>
1015
+ <option value="ui-monospace, monospace">System monospace</option>
1016
+ <option value="__custom__">Custom...</option>
1017
+ </select>
1018
+ <input type="text" id="us-term-font-family-custom" class="us-term-font-custom hidden" placeholder="e.g. 'My Font', monospace" maxlength="200">
1019
+ <input type="number" id="us-term-font-size" class="us-term-font-size" min="9" max="32" step="1">
1020
+ <span class="us-term-font-unit">px</span>
1017
1021
  </div>
1018
1022
  </div>
1019
1023
  <div class="settings-card">
@@ -1556,26 +1560,6 @@
1556
1560
  </div>
1557
1561
  </div>
1558
1562
 
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
1563
  <div id="rewind-modal" class="hidden">
1580
1564
  <div class="confirm-backdrop"></div>
1581
1565
  <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, populateCliSessionList, handleSearchResults, updateSessionBadge } from './sidebar-sessions.js';
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();