claude-relay 2.3.0 → 2.4.0

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.
Files changed (54) hide show
  1. package/README.md +21 -5
  2. package/bin/cli.js +214 -9
  3. package/lib/cli-sessions.js +270 -0
  4. package/lib/config.js +3 -2
  5. package/lib/daemon.js +45 -1
  6. package/lib/pages.js +8 -1
  7. package/lib/project.js +121 -12
  8. package/lib/public/app.js +411 -87
  9. package/lib/public/css/base.css +41 -7
  10. package/lib/public/css/diff.css +6 -6
  11. package/lib/public/css/filebrowser.css +62 -52
  12. package/lib/public/css/highlight.css +144 -0
  13. package/lib/public/css/input.css +11 -9
  14. package/lib/public/css/menus.css +82 -23
  15. package/lib/public/css/messages.css +183 -35
  16. package/lib/public/css/overlays.css +166 -50
  17. package/lib/public/css/rewind.css +17 -17
  18. package/lib/public/css/sidebar.css +210 -137
  19. package/lib/public/index.html +75 -42
  20. package/lib/public/modules/filebrowser.js +2 -1
  21. package/lib/public/modules/markdown.js +10 -10
  22. package/lib/public/modules/notifications.js +38 -1
  23. package/lib/public/modules/sidebar.js +109 -31
  24. package/lib/public/modules/terminal.js +84 -23
  25. package/lib/public/modules/theme.js +622 -0
  26. package/lib/public/modules/tools.js +247 -4
  27. package/lib/public/modules/utils.js +21 -5
  28. package/lib/public/style.css +1 -0
  29. package/lib/sdk-bridge.js +95 -0
  30. package/lib/server.js +45 -3
  31. package/lib/sessions.js +16 -3
  32. package/lib/themes/ayu-light.json +9 -0
  33. package/lib/themes/catppuccin-latte.json +9 -0
  34. package/lib/themes/catppuccin-mocha.json +9 -0
  35. package/lib/themes/claude-light.json +9 -0
  36. package/lib/themes/claude.json +9 -0
  37. package/lib/themes/dracula.json +9 -0
  38. package/lib/themes/everforest-light.json +9 -0
  39. package/lib/themes/everforest.json +9 -0
  40. package/lib/themes/github-light.json +9 -0
  41. package/lib/themes/gruvbox-dark.json +9 -0
  42. package/lib/themes/gruvbox-light.json +9 -0
  43. package/lib/themes/monokai.json +9 -0
  44. package/lib/themes/nord-light.json +9 -0
  45. package/lib/themes/nord.json +9 -0
  46. package/lib/themes/one-dark.json +9 -0
  47. package/lib/themes/one-light.json +9 -0
  48. package/lib/themes/rose-pine-dawn.json +9 -0
  49. package/lib/themes/rose-pine.json +9 -0
  50. package/lib/themes/solarized-dark.json +9 -0
  51. package/lib/themes/solarized-light.json +9 -0
  52. package/lib/themes/tokyo-night-light.json +9 -0
  53. package/lib/themes/tokyo-night.json +9 -0
  54. package/package.json +2 -1
@@ -13,9 +13,11 @@
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/gh/highlightjs/cdn-release@11/build/styles/github-dark-dimmed.min.css">
17
16
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/file-icons-js@1/css/style.css">
18
17
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5/css/xterm.min.css">
18
+ <script>
19
+ (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){}})();
20
+ </script>
19
21
  <link rel="stylesheet" href="style.css">
20
22
  </head>
21
23
  <body>
@@ -28,51 +30,56 @@
28
30
  <button id="sidebar-toggle-btn" title="Close sidebar"><i data-lucide="panel-left-close"></i></button>
29
31
  </div>
30
32
  <nav id="sidebar-nav">
31
- <div id="project-switcher">
32
- <button id="project-switcher-btn" type="button">
33
- <div id="project-switcher-label">
34
- <span class="ps-title">Projects</span>
35
- <div class="ps-current">
36
- <span id="project-switcher-name"></span>
37
- <span id="project-switcher-count" class="ps-count"></span>
38
- </div>
39
- </div>
40
- <i data-lucide="chevron-down" class="ps-chevron"></i>
41
- </button>
42
- <div id="project-dropdown" class="hidden">
43
- <div class="project-dropdown-header">Projects</div>
44
- <div id="project-dropdown-list"></div>
45
- <div class="project-dropdown-footer">
46
- <button id="project-dropdown-add" type="button"><i data-lucide="plus"></i> <span>Add project</span></button>
47
- </div>
33
+ <div id="project-list-section">
34
+ <div id="project-list-header">
35
+ <span class="project-list-title">Projects</span>
36
+ <button id="project-list-add" type="button" title="Add project"><i data-lucide="plus"></i></button>
48
37
  </div>
38
+ <div id="project-list"></div>
49
39
  </div>
50
40
  <div id="project-hint" class="hidden">
51
41
  <span class="project-hint-text">Run <code>npx claude-relay</code> in other directories to add more projects.</span>
52
42
  <button id="project-hint-dismiss" type="button" aria-label="Dismiss"><i data-lucide="x"></i></button>
53
43
  </div>
54
44
  </nav>
55
- <div id="sidebar-panel-sessions" class="sidebar-panel">
45
+ <div id="sidebar-tools">
46
+ <div class="sidebar-section-header">
47
+ <span class="sidebar-section-title">Tools</span>
48
+ </div>
56
49
  <div id="session-actions">
57
- <button id="new-session-btn"><i data-lucide="plus"></i> <span>New session</span></button>
58
- <button id="resume-session-btn"><i data-lucide="link"></i> <span>Resume with ID</span></button>
50
+ <button id="resume-session-btn" title="Resume a CLI session"><i data-lucide="terminal"></i> <span>Resume CLI</span></button>
59
51
  <button id="file-browser-btn"><i data-lucide="folder-tree"></i> <span>File browser</span></button>
60
52
  <button id="terminal-sidebar-btn"><i data-lucide="square-terminal"></i> <span>Terminal</span></button>
61
53
  </div>
62
- <div class="session-list-header">
63
- <span>Sessions</span>
64
- <button id="search-session-btn" type="button" title="Search sessions"><i data-lucide="search"></i></button>
54
+ </div>
55
+ <div id="sidebar-sessions-header">
56
+ <div id="sessions-header-content">
57
+ <div class="session-list-header">
58
+ <span>Sessions</span>
59
+ <div class="session-list-header-actions">
60
+ <button id="new-session-btn" type="button" title="New session"><i data-lucide="plus"></i></button>
61
+ <button id="search-session-btn" type="button" title="Search sessions"><i data-lucide="search"></i></button>
62
+ </div>
63
+ </div>
64
+ <div id="session-search" class="hidden">
65
+ <input id="session-search-input" type="text" placeholder="Search sessions..." autocomplete="off" spellcheck="false" />
66
+ <button id="session-search-clear" type="button" aria-label="Clear search"><i data-lucide="x"></i></button>
67
+ </div>
65
68
  </div>
66
- <div id="session-search" class="hidden">
67
- <input id="session-search-input" type="text" placeholder="Search sessions..." autocomplete="off" spellcheck="false" />
69
+ <div id="files-header-content" class="hidden">
70
+ <div class="session-list-header">
71
+ <span>File Browser</span>
72
+ <div class="session-list-header-actions">
73
+ <button id="file-panel-refresh" type="button" title="Refresh file tree"><i data-lucide="refresh-cw"></i></button>
74
+ <button id="file-panel-close" type="button" title="Close file browser"><i data-lucide="x"></i></button>
75
+ </div>
76
+ </div>
68
77
  </div>
78
+ </div>
79
+ <div id="sidebar-panel-sessions" class="sidebar-panel">
69
80
  <div id="session-list"></div>
70
81
  </div>
71
82
  <div id="sidebar-panel-files" class="sidebar-panel hidden">
72
- <div id="file-panel-header">
73
- <button id="file-panel-back" type="button"><i data-lucide="arrow-left"></i> <span>Sessions</span></button>
74
- <button id="file-panel-refresh" type="button" title="Refresh file tree"><i data-lucide="refresh-cw"></i></button>
75
- </div>
76
83
  <div id="file-tree"></div>
77
84
  </div>
78
85
  <div id="sidebar-footer">
@@ -82,8 +89,11 @@
82
89
  </button>
83
90
  <div id="sidebar-footer-menu" class="hidden">
84
91
  <a class="sidebar-menu-item" href="https://github.com/chadbyte/claude-relay" target="_blank" rel="noopener">
85
- <i data-lucide="github"></i> <span>GitHub</span>
92
+ <i data-lucide="star"></i> <span>Star on GitHub</span>
86
93
  </a>
94
+ <button class="sidebar-menu-item" id="footer-theme">
95
+ <i data-lucide="palette"></i> <span>Theme</span>
96
+ </button>
87
97
  <button class="sidebar-menu-item" id="footer-status">
88
98
  <i data-lucide="activity"></i> <span>Status</span>
89
99
  </button>
@@ -112,7 +122,8 @@
112
122
  <div id="header-left">
113
123
  <button id="sidebar-expand-btn" title="Open sidebar"><i data-lucide="panel-left-open"></i></button>
114
124
  <button id="hamburger-btn" aria-label="Toggle sidebar"><i data-lucide="menu"></i></button>
115
- <span class="project-name" id="project-name">Connecting...</span>
125
+ <span class="header-title" id="header-title">Connecting...</span>
126
+ <button id="header-rename-btn" type="button" title="Rename session"><i data-lucide="pencil"></i></button>
116
127
  </div>
117
128
  <div class="status">
118
129
  <div id="debug-menu-wrap" class="hidden">
@@ -139,7 +150,7 @@
139
150
  </div>
140
151
  </div>
141
152
  <div id="notif-menu-wrap">
142
- <button id="notif-btn" title="Notifications"><i data-lucide="sliders-horizontal"></i></button>
153
+ <button id="notif-btn" title="Notifications"><i data-lucide="bell"></i></button>
143
154
  <div id="notif-menu" class="hidden">
144
155
  <label class="notif-option" id="notif-push-row">
145
156
  <span><i data-lucide="smartphone" style="width:14px;height:14px"></i> Push notifications</span>
@@ -259,7 +270,7 @@
259
270
  <span class="context-mini-label" id="context-mini-label">0%</span>
260
271
  </div>
261
272
  <div id="image-preview-bar"></div>
262
- <textarea id="input" rows="1" placeholder="Message Claude Code..." enterkeyhint="send"></textarea>
273
+ <textarea id="input" rows="1" placeholder="Message Claude Code..." enterkeyhint="send" dir="auto"></textarea>
263
274
  <div id="input-bottom">
264
275
  <div id="attach-wrap">
265
276
  <button id="attach-btn" type="button" aria-label="Attach"><i data-lucide="plus"></i></button>
@@ -293,9 +304,9 @@
293
304
  <div id="terminal-container" class="hidden">
294
305
  <div class="terminal-header">
295
306
  <div id="terminal-tabs" class="terminal-tabs"></div>
307
+ <button class="terminal-new-tab-btn" id="terminal-new-tab" title="New tab"><i data-lucide="plus"></i></button>
296
308
  <div class="terminal-header-actions">
297
- <button class="file-viewer-btn" id="terminal-new-tab" title="New tab"><i data-lucide="plus"></i></button>
298
- <button class="file-viewer-btn" id="terminal-close" title="Close panel"><i data-lucide="x"></i></button>
309
+ <button class="file-viewer-btn" id="terminal-close" title="Hide panel"><i data-lucide="chevron-up"></i></button>
299
310
  </div>
300
311
  </div>
301
312
  <div id="terminal-toolbar" class="hidden">
@@ -369,16 +380,20 @@
369
380
 
370
381
  <div id="resume-modal" class="hidden">
371
382
  <div class="confirm-backdrop"></div>
372
- <div class="confirm-dialog">
383
+ <div class="confirm-dialog resume-picker-dialog">
373
384
  <div class="resume-modal-title">Resume CLI session</div>
374
- <div class="resume-modal-body">
375
- <p>Paste a session ID to continue a CLI conversation here.</p>
376
- <div class="resume-modal-hint">Run <code>claude conversation list</code> in your terminal to find session IDs.</div>
377
- <input type="text" id="resume-session-input" placeholder="Session ID" autocomplete="off" spellcheck="false">
385
+ <div class="resume-picker-body">
386
+ <div id="resume-picker-loading" class="resume-picker-loading">
387
+ <div class="resume-picker-spinner"></div>
388
+ <span>Loading sessions...</span>
389
+ </div>
390
+ <div id="resume-picker-empty" class="resume-picker-empty hidden">
391
+ No CLI sessions found for this project.
392
+ </div>
393
+ <div id="resume-picker-list" class="resume-picker-list hidden"></div>
378
394
  </div>
379
395
  <div class="confirm-actions">
380
396
  <button class="confirm-btn confirm-cancel" id="resume-cancel">Cancel</button>
381
- <button class="confirm-btn confirm-ok" id="resume-ok">Resume</button>
382
397
  </div>
383
398
  </div>
384
399
  </div>
@@ -414,6 +429,24 @@
414
429
  </div>
415
430
  </div>
416
431
 
432
+ <div id="add-project-modal" class="hidden">
433
+ <div class="confirm-backdrop"></div>
434
+ <div class="confirm-dialog add-project-dialog">
435
+ <div class="add-project-title">Add project</div>
436
+ <div class="add-project-body">
437
+ <div class="add-project-input-wrap">
438
+ <input type="text" id="add-project-input" placeholder="/" autocomplete="off" spellcheck="false">
439
+ <div id="add-project-suggestions" class="hidden"></div>
440
+ </div>
441
+ <div id="add-project-error" class="hidden"></div>
442
+ </div>
443
+ <div class="confirm-actions">
444
+ <button class="confirm-btn confirm-cancel" id="add-project-cancel">Cancel</button>
445
+ <button class="confirm-btn confirm-ok" id="add-project-ok">Add</button>
446
+ </div>
447
+ </div>
448
+ </div>
449
+
417
450
  <script src="https://cdn.jsdelivr.net/npm/marked@14/marked.min.js"></script>
418
451
  <script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
419
452
  <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/highlight.min.js"></script>
@@ -1,6 +1,6 @@
1
1
  import { iconHtml, refreshIcons } from './icons.js';
2
2
  import { escapeHtml, copyToClipboard } from './utils.js';
3
- import { renderMarkdown, highlightCodeBlocks } from './markdown.js';
3
+ import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks } from './markdown.js';
4
4
  import { closeSidebar } from './sidebar.js';
5
5
  import { renderUnifiedDiff, renderSplitDiff } from './diff.js';
6
6
 
@@ -123,6 +123,7 @@ function renderBody() {
123
123
  }
124
124
  }
125
125
  highlightCodeBlocks(bodyEl);
126
+ renderMermaidBlocks(bodyEl);
126
127
  renderBtn.classList.add("active");
127
128
  renderBtn.title = "Show raw";
128
129
  } else {
@@ -1,5 +1,6 @@
1
1
  import { copyToClipboard } from './utils.js';
2
2
  import { refreshIcons } from './icons.js';
3
+ import { getMermaidThemeVars } from './theme.js';
3
4
 
4
5
  // Initialize markdown parser
5
6
  marked.use({ gfm: true, breaks: false });
@@ -8,18 +9,17 @@ marked.use({ gfm: true, breaks: false });
8
9
  mermaid.initialize({
9
10
  startOnLoad: false,
10
11
  theme: "dark",
11
- themeVariables: {
12
- darkMode: true,
13
- background: "#1E1D1A",
14
- primaryColor: "#DA7756",
15
- primaryTextColor: "#E8E5DE",
16
- primaryBorderColor: "#3E3C37",
17
- lineColor: "#908B81",
18
- secondaryColor: "#35332F",
19
- tertiaryColor: "#2F2E2B"
20
- }
12
+ themeVariables: getMermaidThemeVars()
21
13
  });
22
14
 
15
+ export function updateMermaidTheme(vars) {
16
+ mermaid.initialize({
17
+ startOnLoad: false,
18
+ theme: "dark",
19
+ themeVariables: vars
20
+ });
21
+ }
22
+
23
23
  var mermaidIdCounter = 0;
24
24
 
25
25
  export function renderMarkdown(text) {
@@ -262,6 +262,15 @@ export function initNotifications(_ctx) {
262
262
  }
263
263
  });
264
264
 
265
+ // --- iOS Safari detection ---
266
+ var isIOSSafari = (function () {
267
+ var ua = navigator.userAgent;
268
+ var isIOS = /iPad|iPhone|iPod/.test(ua) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
269
+ var isSafari = isIOS && /Safari/.test(ua) && !/CriOS|FxiOS|OPiOS|EdgiOS/.test(ua);
270
+ return isSafari;
271
+ })();
272
+ var isStandalone = window.matchMedia("(display-mode:standalone)").matches || navigator.standalone;
273
+
265
274
  // --- Browser notifications ---
266
275
  notifPermission = ("Notification" in window) ? Notification.permission : "denied";
267
276
  notifAlertEnabled = localStorage.getItem("notif-alert") !== "0";
@@ -334,6 +343,7 @@ export function initNotifications(_ctx) {
334
343
  if (ua.indexOf("Firefox") !== -1) url = "about:preferences#privacy";
335
344
  else if (ua.indexOf("Edg/") !== -1) url = "edge://settings/content/notifications";
336
345
  else if (ua.indexOf("Arc") !== -1) url = "arc://settings/content/notifications";
346
+ else if (isIOSSafari) url = "Settings > Safari > Notifications";
337
347
  notifSettingsUrl.textContent = url;
338
348
  })();
339
349
 
@@ -375,7 +385,34 @@ export function initNotifications(_ctx) {
375
385
  var pushAvailable = ("serviceWorker" in navigator) &&
376
386
  (location.protocol === "https:" || location.hostname === "localhost");
377
387
 
378
- if (pushAvailable) {
388
+ // On iOS Safari (not in PWA mode), replace the push toggle with an info hint
389
+ if (isIOSSafari && !isStandalone) {
390
+ var infoRow = document.createElement("div");
391
+ infoRow.className = "notif-option notif-ios-info";
392
+ infoRow.style.display = "flex";
393
+ infoRow.innerHTML =
394
+ '<span><i data-lucide="smartphone" style="width:14px;height:14px"></i> Push notifications</span>' +
395
+ '<button class="notif-ios-info-btn" title="Info"><i data-lucide="info" style="width:14px;height:14px"></i></button>';
396
+ notifPushRow.parentNode.replaceChild(infoRow, notifPushRow);
397
+
398
+ var iosHint = document.createElement("div");
399
+ iosHint.id = "notif-ios-hint";
400
+ iosHint.className = "hidden";
401
+ iosHint.innerHTML =
402
+ 'To enable push notifications on iOS, tap <strong>Share</strong> ' +
403
+ '<i data-lucide="share" style="width:12px;height:12px;vertical-align:-2px"></i> ' +
404
+ 'then <strong>Add to Home Screen</strong>. ' +
405
+ 'Push notifications work inside the installed app.';
406
+ infoRow.parentNode.insertBefore(iosHint, infoRow.nextSibling);
407
+
408
+ infoRow.querySelector(".notif-ios-info-btn").addEventListener("click", function (e) {
409
+ e.preventDefault();
410
+ e.stopPropagation();
411
+ iosHint.classList.toggle("hidden");
412
+ refreshIcons();
413
+ });
414
+ refreshIcons();
415
+ } else if (pushAvailable) {
379
416
  notifPushRow.style.display = "flex";
380
417
  }
381
418
 
@@ -240,6 +240,9 @@ export function updatePageTitle() {
240
240
  var sessionTitle = "";
241
241
  var activeItem = ctx.sessionListEl.querySelector(".session-item.active .session-item-text");
242
242
  if (activeItem) sessionTitle = activeItem.textContent;
243
+ if (ctx.headerTitleEl) {
244
+ ctx.headerTitleEl.textContent = sessionTitle || ctx.projectName || "Claude Relay";
245
+ }
243
246
  if (ctx.projectName && sessionTitle) {
244
247
  document.title = sessionTitle + " - " + ctx.projectName;
245
248
  } else if (ctx.projectName) {
@@ -298,6 +301,7 @@ export function initSidebar(_ctx) {
298
301
  var searchBtn = ctx.$("search-session-btn");
299
302
  var searchBox = ctx.$("session-search");
300
303
  var searchInput = ctx.$("session-search-input");
304
+ var searchClear = ctx.$("session-search-clear");
301
305
 
302
306
  function openSearch() {
303
307
  searchBox.classList.remove("hidden");
@@ -326,6 +330,12 @@ export function initSidebar(_ctx) {
326
330
  }
327
331
  });
328
332
 
333
+ if (searchClear) {
334
+ searchClear.addEventListener("click", function () {
335
+ closeSearch();
336
+ });
337
+ }
338
+
329
339
  searchInput.addEventListener("input", function () {
330
340
  searchQuery = searchInput.value.trim();
331
341
  if (searchDebounce) clearTimeout(searchDebounce);
@@ -349,71 +359,139 @@ export function initSidebar(_ctx) {
349
359
  }
350
360
  });
351
361
 
352
- // --- Resume session modal ---
362
+ // --- Resume session picker ---
353
363
  var resumeModal = ctx.$("resume-modal");
354
- var resumeInput = ctx.$("resume-session-input");
355
- var resumeOk = ctx.$("resume-ok");
356
364
  var resumeCancel = ctx.$("resume-cancel");
365
+ var pickerLoading = ctx.$("resume-picker-loading");
366
+ var pickerEmpty = ctx.$("resume-picker-empty");
367
+ var pickerList = ctx.$("resume-picker-list");
357
368
 
358
369
  function openResumeModal() {
359
370
  resumeModal.classList.remove("hidden");
360
- resumeInput.value = "";
361
- setTimeout(function () { resumeInput.focus(); }, 50);
371
+ pickerLoading.classList.remove("hidden");
372
+ pickerEmpty.classList.add("hidden");
373
+ pickerList.classList.add("hidden");
374
+ pickerList.innerHTML = "";
375
+ if (ctx.ws && ctx.connected) {
376
+ ctx.ws.send(JSON.stringify({ type: "list_cli_sessions" }));
377
+ }
362
378
  }
363
379
 
364
380
  function closeResumeModal() {
365
381
  resumeModal.classList.add("hidden");
366
- resumeInput.value = "";
367
- }
368
-
369
- function submitResume() {
370
- var val = resumeInput.value.trim();
371
- if (!val) return;
372
- if (ctx.ws && ctx.connected) {
373
- ctx.ws.send(JSON.stringify({ type: "resume_session", cliSessionId: val }));
374
- }
375
- closeResumeModal();
376
- closeSidebar();
377
382
  }
378
383
 
379
384
  ctx.resumeSessionBtn.addEventListener("click", openResumeModal);
380
- resumeOk.addEventListener("click", submitResume);
381
385
  resumeCancel.addEventListener("click", closeResumeModal);
382
386
  resumeModal.querySelector(".confirm-backdrop").addEventListener("click", closeResumeModal);
383
387
 
384
- resumeInput.addEventListener("keydown", function (e) {
385
- if (e.key === "Enter") {
386
- e.preventDefault();
387
- submitResume();
388
- }
389
- if (e.key === "Escape") {
390
- e.preventDefault();
391
- closeResumeModal();
392
- }
393
- });
394
-
395
388
  // --- File browser panel switch ---
396
389
  var fileBrowserBtn = ctx.$("file-browser-btn");
397
390
  var sessionsPanel = ctx.$("sidebar-panel-sessions");
398
391
  var filesPanel = ctx.$("sidebar-panel-files");
399
- var filePanelBack = ctx.$("file-panel-back");
392
+ var sessionsHeaderContent = ctx.$("sessions-header-content");
393
+ var filesHeaderContent = ctx.$("files-header-content");
394
+ var filePanelClose = ctx.$("file-panel-close");
400
395
 
401
396
  function showFilesPanel() {
402
397
  sessionsPanel.classList.add("hidden");
403
398
  filesPanel.classList.remove("hidden");
399
+ if (sessionsHeaderContent) sessionsHeaderContent.classList.add("hidden");
400
+ if (filesHeaderContent) filesHeaderContent.classList.remove("hidden");
404
401
  if (ctx.onFilesTabOpen) ctx.onFilesTabOpen();
405
402
  }
406
403
 
407
404
  function showSessionsPanel() {
408
405
  filesPanel.classList.add("hidden");
409
406
  sessionsPanel.classList.remove("hidden");
407
+ if (filesHeaderContent) filesHeaderContent.classList.add("hidden");
408
+ if (sessionsHeaderContent) sessionsHeaderContent.classList.remove("hidden");
410
409
  }
411
410
 
412
411
  if (fileBrowserBtn) {
413
412
  fileBrowserBtn.addEventListener("click", showFilesPanel);
414
413
  }
415
- if (filePanelBack) {
416
- filePanelBack.addEventListener("click", showSessionsPanel);
414
+ if (filePanelClose) {
415
+ filePanelClose.addEventListener("click", showSessionsPanel);
416
+ }
417
+ }
418
+
419
+ // --- CLI session picker ---
420
+ function relativeTime(isoString) {
421
+ if (!isoString) return "";
422
+ var ms = Date.now() - new Date(isoString).getTime();
423
+ var sec = Math.floor(ms / 1000);
424
+ if (sec < 60) return "just now";
425
+ var min = Math.floor(sec / 60);
426
+ if (min < 60) return min + "m ago";
427
+ var hr = Math.floor(min / 60);
428
+ if (hr < 24) return hr + "h ago";
429
+ var days = Math.floor(hr / 24);
430
+ if (days < 30) return days + "d ago";
431
+ return new Date(isoString).toLocaleDateString();
432
+ }
433
+
434
+ export function populateCliSessionList(sessions) {
435
+ var pickerLoading = ctx.$("resume-picker-loading");
436
+ var pickerEmpty = ctx.$("resume-picker-empty");
437
+ var pickerList = ctx.$("resume-picker-list");
438
+ if (!pickerLoading || !pickerList) return;
439
+
440
+ pickerLoading.classList.add("hidden");
441
+
442
+ if (!sessions || sessions.length === 0) {
443
+ pickerEmpty.classList.remove("hidden");
444
+ pickerList.classList.add("hidden");
445
+ return;
446
+ }
447
+
448
+ pickerEmpty.classList.add("hidden");
449
+ pickerList.classList.remove("hidden");
450
+ pickerList.innerHTML = "";
451
+
452
+ for (var i = 0; i < sessions.length; i++) {
453
+ var s = sessions[i];
454
+ var item = document.createElement("div");
455
+ item.className = "cli-session-item";
456
+
457
+ var title = document.createElement("div");
458
+ title.className = "cli-session-title";
459
+ title.textContent = s.firstPrompt || "Untitled session";
460
+ item.appendChild(title);
461
+
462
+ var meta = document.createElement("div");
463
+ meta.className = "cli-session-meta";
464
+ if (s.lastActivity) {
465
+ var time = document.createElement("span");
466
+ time.textContent = relativeTime(s.lastActivity);
467
+ meta.appendChild(time);
468
+ }
469
+ if (s.model) {
470
+ var model = document.createElement("span");
471
+ model.className = "badge";
472
+ model.textContent = s.model;
473
+ meta.appendChild(model);
474
+ }
475
+ if (s.gitBranch) {
476
+ var branch = document.createElement("span");
477
+ branch.className = "badge";
478
+ branch.textContent = s.gitBranch;
479
+ meta.appendChild(branch);
480
+ }
481
+ item.appendChild(meta);
482
+
483
+ (function (sessionId) {
484
+ item.addEventListener("click", function () {
485
+ if (ctx.ws && ctx.connected) {
486
+ ctx.ws.send(JSON.stringify({ type: "resume_session", cliSessionId: sessionId }));
487
+ }
488
+ var modal = ctx.$("resume-modal");
489
+ if (modal) modal.classList.add("hidden");
490
+ closeSidebar();
491
+ });
492
+ })(s.sessionId);
493
+
494
+ pickerList.appendChild(item);
417
495
  }
418
496
  }
419
497
 
@@ -1,6 +1,8 @@
1
1
  import { iconHtml, refreshIcons } from './icons.js';
2
2
  import { closeSidebar } from './sidebar.js';
3
3
  import { closeFileViewer } from './filebrowser.js';
4
+ import { copyToClipboard } from './utils.js';
5
+ import { getTerminalTheme } from './theme.js';
4
6
 
5
7
  var ctx;
6
8
  var tabs = new Map(); // termId -> { id, title, exited, xterm, fitAddon, bodyEl }
@@ -11,6 +13,7 @@ var isTouchDevice = "ontouchstart" in window;
11
13
  var viewportHandler = null;
12
14
  var resizeObserver = null;
13
15
  var toolbarBound = false;
16
+ var termCtxMenu = null;
14
17
 
15
18
  // --- Init ---
16
19
  export function initTerminal(_ctx) {
@@ -195,28 +198,7 @@ function createXtermForTab(tab) {
195
198
  cursorBlink: true,
196
199
  fontSize: 13,
197
200
  fontFamily: "'SF Mono', Menlo, Monaco, 'Courier New', monospace",
198
- theme: {
199
- background: "#1a1a1a",
200
- foreground: "#d4d4d4",
201
- cursor: "#d4d4d4",
202
- selectionBackground: "rgba(255,255,255,0.2)",
203
- black: "#1a1a1a",
204
- red: "#f44747",
205
- green: "#6a9955",
206
- yellow: "#d7ba7d",
207
- blue: "#569cd6",
208
- magenta: "#c586c0",
209
- cyan: "#4ec9b0",
210
- white: "#d4d4d4",
211
- brightBlack: "#808080",
212
- brightRed: "#f44747",
213
- brightGreen: "#6a9955",
214
- brightYellow: "#d7ba7d",
215
- brightBlue: "#569cd6",
216
- brightMagenta: "#c586c0",
217
- brightCyan: "#4ec9b0",
218
- brightWhite: "#ffffff",
219
- },
201
+ theme: getTerminalTheme(),
220
202
  });
221
203
 
222
204
  var fitAddon = null;
@@ -239,6 +221,11 @@ function createXtermForTab(tab) {
239
221
  }
240
222
  });
241
223
 
224
+ // Right-click context menu
225
+ bodyEl.addEventListener("contextmenu", function (e) {
226
+ showTermCtxMenu(e, tab);
227
+ });
228
+
242
229
  tab.xterm = xterm;
243
230
  tab.fitAddon = fitAddon;
244
231
  tab.bodyEl = bodyEl;
@@ -319,7 +306,7 @@ function renderTabBar() {
319
306
 
320
307
  var closeBtn = document.createElement("button");
321
308
  closeBtn.className = "terminal-tab-close";
322
- closeBtn.innerHTML = '<i data-lucide="x" style="width:12px;height:12px"></i>';
309
+ closeBtn.innerHTML = '<i data-lucide="trash-2" style="width:12px;height:12px"></i>';
323
310
  closeBtn.addEventListener("click", function (e) {
324
311
  e.stopPropagation();
325
312
  closeTab(t.id);
@@ -527,6 +514,80 @@ export function resetTerminals() {
527
514
  renderTabBar();
528
515
  }
529
516
 
517
+ export function setTerminalTheme(xtermTheme) {
518
+ for (var tab of tabs.values()) {
519
+ if (tab.xterm) {
520
+ tab.xterm.options.theme = xtermTheme;
521
+ }
522
+ }
523
+ }
524
+
525
+ // --- Terminal context menu ---
526
+ function closeTermCtxMenu() {
527
+ if (termCtxMenu) {
528
+ termCtxMenu.remove();
529
+ termCtxMenu = null;
530
+ }
531
+ }
532
+
533
+ function showTermCtxMenu(e, tab) {
534
+ e.preventDefault();
535
+ e.stopPropagation();
536
+ closeTermCtxMenu();
537
+
538
+ var menu = document.createElement("div");
539
+ menu.className = "term-ctx-menu";
540
+
541
+ // Copy
542
+ var copyItem = document.createElement("button");
543
+ copyItem.className = "term-ctx-item";
544
+ copyItem.innerHTML = iconHtml("clipboard-copy") + " <span>Copy Terminal</span>";
545
+ copyItem.addEventListener("click", function (ev) {
546
+ ev.stopPropagation();
547
+ closeTermCtxMenu();
548
+ if (!tab.xterm) return;
549
+ tab.xterm.selectAll();
550
+ var text = tab.xterm.getSelection();
551
+ tab.xterm.clearSelection();
552
+ if (text) copyToClipboard(text);
553
+ });
554
+ menu.appendChild(copyItem);
555
+
556
+ // Clear
557
+ var clearItem = document.createElement("button");
558
+ clearItem.className = "term-ctx-item";
559
+ clearItem.innerHTML = iconHtml("trash-2") + " <span>Clear Terminal</span>";
560
+ clearItem.addEventListener("click", function (ev) {
561
+ ev.stopPropagation();
562
+ closeTermCtxMenu();
563
+ if (!tab.xterm) return;
564
+ tab.xterm.clear();
565
+ });
566
+ menu.appendChild(clearItem);
567
+
568
+ // Position at mouse cursor
569
+ menu.style.left = e.clientX + "px";
570
+ menu.style.top = e.clientY + "px";
571
+ document.body.appendChild(menu);
572
+
573
+ // Clamp to viewport
574
+ var rect = menu.getBoundingClientRect();
575
+ if (rect.right > window.innerWidth) {
576
+ menu.style.left = (window.innerWidth - rect.width - 4) + "px";
577
+ }
578
+ if (rect.bottom > window.innerHeight) {
579
+ menu.style.top = (window.innerHeight - rect.height - 4) + "px";
580
+ }
581
+
582
+ termCtxMenu = menu;
583
+ refreshIcons();
584
+
585
+ // Close on outside click (next tick to avoid immediate trigger)
586
+ setTimeout(function () {
587
+ document.addEventListener("click", closeTermCtxMenu, { once: true });
588
+ }, 0);
589
+ }
590
+
530
591
  // --- Mobile toolbar ---
531
592
  var KEY_MAP = {
532
593
  tab: "\t",