claude-relay 1.2.0 → 1.2.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/public/app.js CHANGED
@@ -15,6 +15,8 @@
15
15
  var sessionListEl = $("session-list");
16
16
  var newSessionBtn = $("new-session-btn");
17
17
  var hamburgerBtn = $("hamburger-btn");
18
+ var sidebarToggleBtn = $("sidebar-toggle-btn");
19
+ var sidebarExpandBtn = $("sidebar-expand-btn");
18
20
  var imagePreviewBar = $("image-preview-bar");
19
21
  var connectOverlay = $("connect-overlay");
20
22
  var connectVerbEl = $("connect-verb");
@@ -121,6 +123,32 @@
121
123
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
122
124
  }
123
125
 
126
+ // --- Confirm modal ---
127
+ var confirmModal = $("confirm-modal");
128
+ var confirmText = $("confirm-text");
129
+ var confirmOk = $("confirm-ok");
130
+ var confirmCancel = $("confirm-cancel");
131
+ var confirmCallback = null;
132
+
133
+ function showConfirm(text, onConfirm) {
134
+ confirmText.textContent = text;
135
+ confirmCallback = onConfirm;
136
+ confirmModal.classList.remove("hidden");
137
+ }
138
+
139
+ function hideConfirm() {
140
+ confirmModal.classList.add("hidden");
141
+ confirmCallback = null;
142
+ }
143
+
144
+ confirmOk.addEventListener("click", function () {
145
+ if (confirmCallback) confirmCallback();
146
+ hideConfirm();
147
+ });
148
+
149
+ confirmCancel.addEventListener("click", hideConfirm);
150
+ confirmModal.querySelector(".confirm-backdrop").addEventListener("click", hideConfirm);
151
+
124
152
  // --- Sidebar ---
125
153
  function renderSessionList(sessions) {
126
154
  sessionListEl.innerHTML = "";
@@ -144,14 +172,16 @@
144
172
  deleteBtn.className = "session-delete-btn";
145
173
  deleteBtn.innerHTML = iconHtml("trash-2");
146
174
  deleteBtn.title = "Delete session";
147
- deleteBtn.addEventListener("click", (function(id) {
175
+ deleteBtn.addEventListener("click", (function(id, title) {
148
176
  return function(e) {
149
177
  e.stopPropagation();
150
- if (ws && connected) {
151
- ws.send(JSON.stringify({ type: "delete_session", id: id }));
152
- }
178
+ showConfirm('Delete "' + (title || "New Session") + '"? This session and its history will be permanently removed.', function () {
179
+ if (ws && connected) {
180
+ ws.send(JSON.stringify({ type: "delete_session", id: id }));
181
+ }
182
+ });
153
183
  };
154
- })(s.id));
184
+ })(s.id, s.title));
155
185
  el.appendChild(deleteBtn);
156
186
 
157
187
  el.addEventListener("click", (function (id) {
@@ -198,6 +228,25 @@
198
228
 
199
229
  sidebarOverlay.addEventListener("click", closeSidebar);
200
230
 
231
+ // --- Desktop sidebar collapse/expand ---
232
+ function toggleSidebarCollapse() {
233
+ var layout = $("layout");
234
+ var collapsed = layout.classList.toggle("sidebar-collapsed");
235
+ try { localStorage.setItem("sidebar-collapsed", collapsed ? "1" : ""); } catch (e) {}
236
+ }
237
+
238
+ sidebarToggleBtn.addEventListener("click", toggleSidebarCollapse);
239
+ sidebarExpandBtn.addEventListener("click", toggleSidebarCollapse);
240
+
241
+ // Restore collapsed state from localStorage
242
+ (function () {
243
+ try {
244
+ if (localStorage.getItem("sidebar-collapsed") === "1") {
245
+ $("layout").classList.add("sidebar-collapsed");
246
+ }
247
+ } catch (e) {}
248
+ })();
249
+
201
250
  newSessionBtn.addEventListener("click", function () {
202
251
  if (ws && connected) {
203
252
  ws.send(JSON.stringify({ type: "new_session" }));
@@ -713,14 +762,14 @@
713
762
 
714
763
  var allowBtn = document.createElement("button");
715
764
  allowBtn.className = "permission-btn permission-allow";
716
- allowBtn.textContent = "Allow";
765
+ allowBtn.textContent = "Allow Once";
717
766
  allowBtn.addEventListener("click", function () {
718
767
  sendPermissionResponse(container, requestId, "allow");
719
768
  });
720
769
 
721
770
  var allowAlwaysBtn = document.createElement("button");
722
771
  allowAlwaysBtn.className = "permission-btn permission-allow-session";
723
- allowAlwaysBtn.textContent = "Allow for Session";
772
+ allowAlwaysBtn.textContent = "Always Allow";
724
773
  allowAlwaysBtn.addEventListener("click", function () {
725
774
  sendPermissionResponse(container, requestId, "allow_always");
726
775
  });
@@ -1115,6 +1164,7 @@
1115
1164
  el.dataset.toolId = id;
1116
1165
  el.innerHTML =
1117
1166
  '<div class="tool-header">' +
1167
+ '<span class="tool-chevron">' + iconHtml("chevron-right") + '</span>' +
1118
1168
  '<span class="tool-bullet"></span>' +
1119
1169
  '<span class="tool-name"></span>' +
1120
1170
  '<span class="tool-desc"></span>' +
@@ -1186,6 +1236,63 @@
1186
1236
  return pre;
1187
1237
  }
1188
1238
 
1239
+ function getLanguageFromPath(filePath) {
1240
+ if (!filePath) return null;
1241
+ var parts = filePath.split("/");
1242
+ var filename = parts[parts.length - 1].toLowerCase();
1243
+ var dotIdx = filename.lastIndexOf(".");
1244
+ if (dotIdx === -1 || dotIdx === filename.length - 1) return null;
1245
+ var ext = filename.substring(dotIdx + 1);
1246
+ var map = {
1247
+ js: "javascript", jsx: "javascript", mjs: "javascript", cjs: "javascript",
1248
+ ts: "typescript", tsx: "typescript", mts: "typescript",
1249
+ py: "python", rb: "ruby", rs: "rust", go: "go",
1250
+ java: "java", kt: "kotlin", kts: "kotlin",
1251
+ cs: "csharp", cpp: "cpp", cc: "cpp", c: "c", h: "c", hpp: "cpp",
1252
+ css: "css", scss: "scss", less: "less",
1253
+ html: "xml", htm: "xml", xml: "xml", svg: "xml",
1254
+ json: "json", yaml: "yaml", yml: "yaml",
1255
+ md: "markdown", sh: "bash", bash: "bash", zsh: "bash",
1256
+ sql: "sql", swift: "swift", php: "php",
1257
+ toml: "ini", ini: "ini", conf: "ini",
1258
+ lua: "lua", r: "r", pl: "perl",
1259
+ ex: "elixir", exs: "elixir",
1260
+ erl: "erlang", hs: "haskell",
1261
+ graphql: "graphql", gql: "graphql",
1262
+ };
1263
+ return map[ext] || null;
1264
+ }
1265
+
1266
+ function parseLineNumberedContent(text) {
1267
+ var lines = text.split("\n");
1268
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
1269
+ lines.pop();
1270
+ }
1271
+ if (lines.length === 0) return null;
1272
+
1273
+ var pattern = /^\s*(\d+)[→\t](.*)$/;
1274
+ var checkCount = Math.min(lines.length, 5);
1275
+ var matchCount = 0;
1276
+ for (var i = 0; i < checkCount; i++) {
1277
+ if (pattern.test(lines[i])) matchCount++;
1278
+ }
1279
+ if (matchCount < Math.ceil(checkCount * 0.6)) return null;
1280
+
1281
+ var numbers = [];
1282
+ var code = [];
1283
+ for (var i = 0; i < lines.length; i++) {
1284
+ var m = lines[i].match(pattern);
1285
+ if (m) {
1286
+ numbers.push(m[1]);
1287
+ code.push(m[2]);
1288
+ } else {
1289
+ numbers.push("");
1290
+ code.push(lines[i]);
1291
+ }
1292
+ }
1293
+ return { numbers: numbers, code: code };
1294
+ }
1295
+
1189
1296
  function updateToolResult(id, content, isError) {
1190
1297
  var tool = tools[id];
1191
1298
  if (!tool) return;
@@ -1196,12 +1303,64 @@
1196
1303
  }
1197
1304
 
1198
1305
  var resultBlock = document.createElement("div");
1199
- resultBlock.className = "tool-result-block";
1200
1306
  var displayContent = content || "(no output)";
1307
+ displayContent = displayContent.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").trim();
1201
1308
  if (displayContent.length > 10000) displayContent = displayContent.substring(0, 10000) + "\n... (truncated)";
1202
1309
 
1310
+ var expandByDefault = !isError && tool.name === "Edit" && isDiffContent(displayContent);
1311
+ if (expandByDefault) {
1312
+ resultBlock.className = "tool-result-block";
1313
+ tool.el.classList.add("expanded");
1314
+ } else {
1315
+ resultBlock.className = "tool-result-block collapsed";
1316
+ }
1317
+
1203
1318
  if (!isError && isDiffContent(displayContent)) {
1204
1319
  resultBlock.appendChild(renderDiffPre(displayContent));
1320
+ } else if (!isError && tool.name === "Read" && tool.input && tool.input.file_path) {
1321
+ var parsed = parseLineNumberedContent(displayContent);
1322
+ if (parsed) {
1323
+ var lang = getLanguageFromPath(tool.input.file_path);
1324
+ var viewer = document.createElement("div");
1325
+ viewer.className = "code-viewer";
1326
+
1327
+ var gutter = document.createElement("pre");
1328
+ gutter.className = "code-gutter";
1329
+ gutter.textContent = parsed.numbers.join("\n");
1330
+
1331
+ var codeBlock = document.createElement("pre");
1332
+ codeBlock.className = "code-content";
1333
+ var codeText = parsed.code.join("\n");
1334
+
1335
+ if (lang) {
1336
+ try {
1337
+ var highlighted = hljs.highlight(codeText, { language: lang });
1338
+ var codeEl = document.createElement("code");
1339
+ codeEl.className = "hljs language-" + lang;
1340
+ codeEl.innerHTML = highlighted.value;
1341
+ codeBlock.appendChild(codeEl);
1342
+ } catch (e) {
1343
+ codeBlock.textContent = codeText;
1344
+ }
1345
+ } else {
1346
+ codeBlock.textContent = codeText;
1347
+ }
1348
+
1349
+ viewer.appendChild(gutter);
1350
+ viewer.appendChild(codeBlock);
1351
+
1352
+ // Sync vertical scroll between gutter and code
1353
+ viewer.addEventListener("scroll", function () {
1354
+ gutter.scrollTop = viewer.scrollTop;
1355
+ codeBlock.scrollTop = viewer.scrollTop;
1356
+ });
1357
+
1358
+ resultBlock.appendChild(viewer);
1359
+ } else {
1360
+ var pre = document.createElement("pre");
1361
+ pre.textContent = displayContent;
1362
+ resultBlock.appendChild(pre);
1363
+ }
1205
1364
  } else {
1206
1365
  var pre = document.createElement("pre");
1207
1366
  if (isError) pre.className = "is-error";
@@ -1212,6 +1371,7 @@
1212
1371
 
1213
1372
  tool.el.querySelector(".tool-header").addEventListener("click", function () {
1214
1373
  resultBlock.classList.toggle("collapsed");
1374
+ tool.el.classList.toggle("expanded");
1215
1375
  });
1216
1376
 
1217
1377
  markToolDone(id, isError);
@@ -1318,6 +1478,16 @@
1318
1478
  updatePageTitle();
1319
1479
  break;
1320
1480
 
1481
+ case "update_available":
1482
+ var updateBanner = $("update-banner");
1483
+ var updateVersion = $("update-version");
1484
+ if (updateBanner && updateVersion && msg.version) {
1485
+ updateVersion.textContent = "v" + msg.version;
1486
+ updateBanner.classList.remove("hidden");
1487
+ refreshIcons();
1488
+ }
1489
+ break;
1490
+
1321
1491
  case "slash_commands":
1322
1492
  slashCommands = (msg.commands || []).map(function (name) {
1323
1493
  return { name: name, desc: "Skill" };
@@ -1815,6 +1985,58 @@
1815
1985
  window.visualViewport.addEventListener("scroll", onViewportChange);
1816
1986
  }
1817
1987
 
1988
+ // --- Update banner ---
1989
+ (function () {
1990
+ var banner = $("update-banner");
1991
+ var closeBtn = $("update-banner-close");
1992
+ var howBtn = $("update-how");
1993
+ if (!banner) return;
1994
+
1995
+ // Build popover
1996
+ var popover = document.createElement("div");
1997
+ popover.id = "update-popover";
1998
+ popover.innerHTML =
1999
+ '<div class="popover-label">Run in your terminal:</div>' +
2000
+ '<div class="popover-cmd">' +
2001
+ '<code>npx claude-relay@latest</code>' +
2002
+ '<button class="popover-copy" title="Copy">' + iconHtml("copy") + '</button>' +
2003
+ '</div>';
2004
+ banner.appendChild(popover);
2005
+ refreshIcons();
2006
+
2007
+ var copyBtn = popover.querySelector(".popover-copy");
2008
+ copyBtn.addEventListener("click", function () {
2009
+ navigator.clipboard.writeText("npx claude-relay@latest").then(function () {
2010
+ copyBtn.classList.add("copied");
2011
+ copyBtn.innerHTML = iconHtml("check");
2012
+ refreshIcons();
2013
+ setTimeout(function () {
2014
+ copyBtn.classList.remove("copied");
2015
+ copyBtn.innerHTML = iconHtml("copy");
2016
+ refreshIcons();
2017
+ }, 1500);
2018
+ });
2019
+ });
2020
+
2021
+ howBtn.addEventListener("click", function (e) {
2022
+ e.stopPropagation();
2023
+ popover.classList.toggle("visible");
2024
+ });
2025
+
2026
+ document.addEventListener("click", function (e) {
2027
+ if (!popover.contains(e.target) && e.target !== howBtn) {
2028
+ popover.classList.remove("visible");
2029
+ }
2030
+ });
2031
+
2032
+ if (closeBtn) {
2033
+ closeBtn.addEventListener("click", function () {
2034
+ banner.classList.add("hidden");
2035
+ popover.classList.remove("visible");
2036
+ });
2037
+ }
2038
+ })();
2039
+
1818
2040
  // --- HTTPS banner / auto-redirect ---
1819
2041
  (function () {
1820
2042
  if (location.protocol === "https:") return;
@@ -19,8 +19,11 @@
19
19
  <div id="layout">
20
20
  <div id="sidebar">
21
21
  <div id="sidebar-header">
22
- <span class="sidebar-title">Sessions</span>
23
- <button id="new-session-btn" title="New session"><i data-lucide="plus"></i></button>
22
+ <div class="sidebar-header-top">
23
+ <button id="sidebar-toggle-btn" title="Close sidebar"><i data-lucide="panel-left-close"></i></button>
24
+ <span class="sidebar-title">Sessions</span>
25
+ </div>
26
+ <button id="new-session-btn" title="New session"><i data-lucide="plus"></i> New Session</button>
24
27
  </div>
25
28
  <div id="session-list"></div>
26
29
  <div id="sidebar-footer">
@@ -31,12 +34,18 @@
31
34
  </div>
32
35
  <div id="sidebar-overlay"></div>
33
36
  <div id="app">
37
+ <div id="update-banner" class="hidden">
38
+ <span class="update-banner-text"><i data-lucide="arrow-up-circle"></i> A new version of Claude Relay is available: <strong id="update-version"></strong></span>
39
+ <button id="update-how" title="How to update">How to update</button>
40
+ <button id="update-banner-close" aria-label="Dismiss"><i data-lucide="x"></i></button>
41
+ </div>
34
42
  <div id="https-banner" class="hidden">
35
43
  <span class="https-banner-text"><i data-lucide="shield"></i> Your connection is not encrypted. <a id="https-banner-link" href="/setup">Set up HTTPS</a></span>
36
44
  <button id="https-banner-close" aria-label="Dismiss"><i data-lucide="x"></i></button>
37
45
  </div>
38
46
  <div id="header">
39
47
  <div id="header-left">
48
+ <button id="sidebar-expand-btn" title="Open sidebar"><i data-lucide="panel-left-open"></i></button>
40
49
  <button id="hamburger-btn" aria-label="Toggle sidebar"><i data-lucide="menu"></i></button>
41
50
  <span class="project-name" id="project-name">Connecting...</span>
42
51
  </div>
@@ -68,6 +77,17 @@
68
77
  </div>
69
78
  </div>
70
79
 
80
+ <div id="confirm-modal" class="hidden">
81
+ <div class="confirm-backdrop"></div>
82
+ <div class="confirm-dialog">
83
+ <div class="confirm-text" id="confirm-text"></div>
84
+ <div class="confirm-actions">
85
+ <button class="confirm-btn confirm-cancel" id="confirm-cancel">Cancel</button>
86
+ <button class="confirm-btn confirm-delete" id="confirm-ok">Delete</button>
87
+ </div>
88
+ </div>
89
+ </div>
90
+
71
91
  <script src="https://cdn.jsdelivr.net/npm/marked@14/marked.min.js"></script>
72
92
  <script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
73
93
  <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/highlight.min.js"></script>
@@ -82,13 +82,50 @@ html, body {
82
82
  display: flex;
83
83
  flex-direction: column;
84
84
  overflow: hidden;
85
+ transition: width 0.2s ease;
86
+ }
87
+
88
+ #layout.sidebar-collapsed #sidebar {
89
+ width: 0;
90
+ border: none;
85
91
  }
86
92
 
87
93
  #sidebar-header {
94
+ display: flex;
95
+ flex-direction: column;
96
+ gap: 12px;
97
+ padding: calc(var(--safe-top) + 16px) 16px 12px;
98
+ }
99
+
100
+ .sidebar-header-top {
88
101
  display: flex;
89
102
  align-items: center;
90
- justify-content: space-between;
91
- padding: 16px 16px 12px;
103
+ gap: 10px;
104
+ }
105
+
106
+ #sidebar-toggle-btn {
107
+ width: 30px;
108
+ height: 30px;
109
+ border-radius: 8px;
110
+ border: none;
111
+ background: transparent;
112
+ color: var(--text-secondary);
113
+ cursor: pointer;
114
+ display: flex;
115
+ align-items: center;
116
+ justify-content: center;
117
+ transition: background 0.15s, color 0.15s;
118
+ flex-shrink: 0;
119
+ }
120
+
121
+ #sidebar-toggle-btn .lucide {
122
+ width: 18px;
123
+ height: 18px;
124
+ }
125
+
126
+ #sidebar-toggle-btn:hover {
127
+ background: var(--sidebar-hover);
128
+ color: var(--text);
92
129
  }
93
130
 
94
131
  .sidebar-title {
@@ -100,27 +137,35 @@ html, body {
100
137
  }
101
138
 
102
139
  #new-session-btn {
103
- width: 30px;
104
- height: 30px;
140
+ width: 100%;
141
+ height: 36px;
105
142
  border-radius: 8px;
106
- border: none;
143
+ border: 1px solid var(--border-subtle);
107
144
  background: transparent;
108
145
  color: var(--text-secondary);
109
146
  cursor: pointer;
110
147
  display: flex;
111
148
  align-items: center;
112
149
  justify-content: center;
113
- transition: background 0.15s, color 0.15s;
150
+ gap: 6px;
151
+ font-family: inherit;
152
+ font-size: 13px;
153
+ font-weight: 500;
154
+ transition: background 0.15s, color 0.15s, border-color 0.15s;
155
+ white-space: nowrap;
156
+ overflow: hidden;
114
157
  }
115
158
 
116
159
  #new-session-btn .lucide {
117
- width: 18px;
118
- height: 18px;
160
+ width: 16px;
161
+ height: 16px;
162
+ flex-shrink: 0;
119
163
  }
120
164
 
121
165
  #new-session-btn:hover {
122
166
  background: var(--sidebar-hover);
123
167
  color: var(--text);
168
+ border-color: var(--border);
124
169
  }
125
170
 
126
171
  #session-list {
@@ -240,6 +285,34 @@ html, body {
240
285
 
241
286
  #sidebar-overlay.visible { display: block; }
242
287
 
288
+ /* --- Sidebar expand (desktop collapsed) --- */
289
+ #sidebar-expand-btn {
290
+ display: none;
291
+ background: none;
292
+ border: none;
293
+ color: var(--text-secondary);
294
+ cursor: pointer;
295
+ padding: 4px;
296
+ margin-right: 10px;
297
+ flex-shrink: 0;
298
+ align-items: center;
299
+ justify-content: center;
300
+ transition: color 0.15s;
301
+ }
302
+
303
+ #sidebar-expand-btn .lucide {
304
+ width: 20px;
305
+ height: 20px;
306
+ }
307
+
308
+ #sidebar-expand-btn:hover {
309
+ color: var(--text);
310
+ }
311
+
312
+ #layout.sidebar-collapsed #sidebar-expand-btn {
313
+ display: flex;
314
+ }
315
+
243
316
  /* --- Hamburger --- */
244
317
  #hamburger-btn {
245
318
  display: none;
@@ -315,6 +388,189 @@ html, body {
315
388
  #https-banner-close .lucide { width: 14px; height: 14px; }
316
389
  #https-banner-close:hover { color: var(--text-secondary); }
317
390
 
391
+ /* --- Update banner --- */
392
+ #update-banner {
393
+ flex-shrink: 0;
394
+ display: flex;
395
+ align-items: center;
396
+ justify-content: center;
397
+ gap: 8px;
398
+ padding: 8px 16px;
399
+ background: rgba(87, 171, 90, 0.08);
400
+ border-bottom: 1px solid rgba(87, 171, 90, 0.15);
401
+ font-size: 12px;
402
+ color: var(--text-secondary);
403
+ position: relative;
404
+ }
405
+
406
+ #update-banner.hidden { display: none; }
407
+
408
+ .update-banner-text {
409
+ display: flex;
410
+ align-items: center;
411
+ gap: 6px;
412
+ }
413
+
414
+ .update-banner-text .lucide { width: 13px; height: 13px; color: var(--success); }
415
+ .update-banner-text strong { color: var(--text); font-weight: 600; }
416
+
417
+ #update-how {
418
+ background: rgba(87, 171, 90, 0.15);
419
+ border: 1px solid rgba(87, 171, 90, 0.25);
420
+ border-radius: 6px;
421
+ color: var(--success);
422
+ cursor: pointer;
423
+ font-family: inherit;
424
+ font-size: 11px;
425
+ font-weight: 500;
426
+ padding: 3px 10px;
427
+ flex-shrink: 0;
428
+ transition: background 0.15s;
429
+ }
430
+
431
+ #update-how:hover { background: rgba(87, 171, 90, 0.25); }
432
+
433
+ #update-popover {
434
+ display: none;
435
+ position: absolute;
436
+ top: 100%;
437
+ left: 50%;
438
+ transform: translateX(-50%);
439
+ background: var(--code-bg);
440
+ border: 1px solid var(--border);
441
+ border-radius: 10px;
442
+ padding: 12px 16px;
443
+ z-index: 200;
444
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
445
+ white-space: nowrap;
446
+ }
447
+
448
+ #update-popover.visible { display: block; }
449
+
450
+ #update-popover .popover-label {
451
+ font-size: 12px;
452
+ color: var(--text-muted);
453
+ margin-bottom: 6px;
454
+ }
455
+
456
+ #update-popover .popover-cmd {
457
+ display: flex;
458
+ align-items: center;
459
+ gap: 8px;
460
+ }
461
+
462
+ #update-popover .popover-cmd code {
463
+ font-family: "SF Mono", Menlo, Monaco, monospace;
464
+ font-size: 13px;
465
+ color: var(--text);
466
+ background: rgba(255, 255, 255, 0.06);
467
+ padding: 6px 12px;
468
+ border-radius: 6px;
469
+ user-select: all;
470
+ -webkit-user-select: all;
471
+ }
472
+
473
+ #update-popover .popover-copy {
474
+ background: none;
475
+ border: 1px solid var(--border);
476
+ border-radius: 6px;
477
+ color: var(--text-muted);
478
+ cursor: pointer;
479
+ padding: 5px 7px;
480
+ display: flex;
481
+ align-items: center;
482
+ justify-content: center;
483
+ transition: color 0.15s, border-color 0.15s;
484
+ }
485
+
486
+ #update-popover .popover-copy .lucide { width: 14px; height: 14px; }
487
+ #update-popover .popover-copy:hover { color: var(--text); border-color: var(--text-dimmer); }
488
+ #update-popover .popover-copy.copied { color: var(--success); border-color: var(--success); }
489
+
490
+ #update-banner-close {
491
+ background: none;
492
+ border: none;
493
+ color: var(--text-dimmer);
494
+ cursor: pointer;
495
+ padding: 2px;
496
+ display: flex;
497
+ align-items: center;
498
+ flex-shrink: 0;
499
+ }
500
+
501
+ #update-banner-close .lucide { width: 14px; height: 14px; }
502
+ #update-banner-close:hover { color: var(--text-secondary); }
503
+
504
+ /* --- Confirm modal --- */
505
+ #confirm-modal {
506
+ position: fixed;
507
+ inset: 0;
508
+ z-index: 300;
509
+ display: flex;
510
+ align-items: center;
511
+ justify-content: center;
512
+ }
513
+
514
+ #confirm-modal.hidden { display: none; }
515
+
516
+ .confirm-backdrop {
517
+ position: absolute;
518
+ inset: 0;
519
+ background: rgba(0, 0, 0, 0.5);
520
+ backdrop-filter: blur(2px);
521
+ -webkit-backdrop-filter: blur(2px);
522
+ }
523
+
524
+ .confirm-dialog {
525
+ position: relative;
526
+ background: var(--bg-alt);
527
+ border: 1px solid var(--border);
528
+ border-radius: 14px;
529
+ padding: 20px 24px;
530
+ max-width: 320px;
531
+ width: 90%;
532
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
533
+ }
534
+
535
+ .confirm-text {
536
+ font-size: 14px;
537
+ color: var(--text);
538
+ line-height: 1.5;
539
+ margin-bottom: 18px;
540
+ }
541
+
542
+ .confirm-actions {
543
+ display: flex;
544
+ gap: 8px;
545
+ justify-content: flex-end;
546
+ }
547
+
548
+ .confirm-btn {
549
+ padding: 7px 16px;
550
+ border-radius: 8px;
551
+ border: none;
552
+ font-size: 13px;
553
+ font-weight: 500;
554
+ font-family: inherit;
555
+ cursor: pointer;
556
+ transition: background 0.15s, opacity 0.15s;
557
+ }
558
+
559
+ .confirm-cancel {
560
+ background: var(--input-bg);
561
+ color: var(--text-secondary);
562
+ border: 1px solid var(--border);
563
+ }
564
+
565
+ .confirm-cancel:hover { background: var(--sidebar-hover); }
566
+
567
+ .confirm-delete {
568
+ background: var(--error);
569
+ color: #fff;
570
+ }
571
+
572
+ .confirm-delete:hover { opacity: 0.85; }
573
+
318
574
  /* --- Connect overlay --- */
319
575
  #connect-overlay {
320
576
  position: absolute;
@@ -754,6 +1010,22 @@ html, body {
754
1010
  transition: background 0.15s;
755
1011
  }
756
1012
 
1013
+ .tool-chevron {
1014
+ color: var(--text-dimmer);
1015
+ transition: transform 0.2s;
1016
+ display: inline-flex;
1017
+ flex-shrink: 0;
1018
+ }
1019
+
1020
+ .tool-chevron .lucide {
1021
+ width: 14px;
1022
+ height: 14px;
1023
+ }
1024
+
1025
+ .tool-item.expanded .tool-chevron {
1026
+ transform: rotate(90deg);
1027
+ }
1028
+
757
1029
  .tool-header:hover {
758
1030
  background: rgba(255, 255, 255, 0.05);
759
1031
  }
@@ -885,6 +1157,58 @@ html, body {
885
1157
  .diff-content .diff-file-header { color: #8B949E; font-weight: 600; }
886
1158
  .diff-content .diff-ctx { color: var(--text-muted); }
887
1159
 
1160
+ /* --- Code viewer (Read tool) --- */
1161
+ .code-viewer {
1162
+ display: flex;
1163
+ max-height: 300px;
1164
+ overflow-y: auto;
1165
+ overflow-x: hidden;
1166
+ }
1167
+
1168
+ .code-viewer pre {
1169
+ margin: 0;
1170
+ max-height: none;
1171
+ overflow-y: visible;
1172
+ font-family: "SF Mono", Menlo, Monaco, monospace;
1173
+ font-size: 12px;
1174
+ line-height: 1.55;
1175
+ white-space: pre;
1176
+ word-break: normal;
1177
+ }
1178
+
1179
+ .code-gutter {
1180
+ flex-shrink: 0;
1181
+ padding: 12px 12px 12px 14px;
1182
+ text-align: right;
1183
+ color: var(--text-dimmer);
1184
+ user-select: none;
1185
+ -webkit-user-select: none;
1186
+ border-right: 1px solid var(--border-subtle);
1187
+ min-width: 48px;
1188
+ }
1189
+
1190
+ .code-content {
1191
+ flex: 1;
1192
+ padding: 12px 14px;
1193
+ overflow-x: auto;
1194
+ min-width: 0;
1195
+ color: var(--text-muted);
1196
+ }
1197
+
1198
+ .code-content code {
1199
+ font-family: inherit;
1200
+ font-size: inherit;
1201
+ line-height: inherit;
1202
+ background: none;
1203
+ padding: 0;
1204
+ border-radius: 0;
1205
+ }
1206
+
1207
+ .code-content .hljs {
1208
+ background: transparent;
1209
+ padding: 0;
1210
+ }
1211
+
888
1212
  /* ==========================================================================
889
1213
  Plan Mode
890
1214
  ========================================================================== */
@@ -1587,18 +1911,31 @@ html, body {
1587
1911
  z-index: 100;
1588
1912
  transform: translateX(-100%);
1589
1913
  transition: transform 0.25s ease;
1914
+ width: 260px;
1590
1915
  }
1591
1916
 
1592
1917
  #sidebar.open {
1593
1918
  transform: translateX(0);
1594
1919
  }
1595
1920
 
1921
+ /* On mobile, sidebar-collapsed should not affect sidebar (hamburger controls it) */
1922
+ #layout.sidebar-collapsed #sidebar {
1923
+ width: 260px;
1924
+ border-right: 1px solid var(--border-subtle);
1925
+ }
1926
+
1596
1927
  #hamburger-btn {
1597
1928
  display: flex;
1598
1929
  align-items: center;
1599
1930
  justify-content: center;
1600
1931
  }
1601
1932
 
1933
+ #sidebar-toggle-btn,
1934
+ #sidebar-expand-btn,
1935
+ #layout.sidebar-collapsed #sidebar-expand-btn {
1936
+ display: none;
1937
+ }
1938
+
1602
1939
  .msg-user .bubble {
1603
1940
  max-width: 90%;
1604
1941
  }
package/lib/server.js CHANGED
@@ -3,6 +3,7 @@ const crypto = require("crypto");
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
5
  const { WebSocketServer } = require("ws");
6
+ const { fetchLatestVersion, isNewer } = require("./updater");
6
7
 
7
8
  // SDK loaded dynamically (ESM module)
8
9
  var sdkModule = null;
@@ -234,6 +235,17 @@ function setupPageHtml(httpsUrl) {
234
235
  function createServer(cwd, tlsOptions, caPath, pin, mainPort) {
235
236
  var authToken = pin ? generateAuthToken(pin) : null;
236
237
  const project = path.basename(cwd);
238
+ const currentVersion = require("../package.json").version;
239
+ let latestVersion = null;
240
+
241
+ // Check for updates in background
242
+ fetchLatestVersion().then(function(v) {
243
+ if (v && isNewer(v, currentVersion)) {
244
+ latestVersion = v;
245
+ // Notify already-connected clients
246
+ send({ type: "update_available", version: v });
247
+ }
248
+ });
237
249
 
238
250
  // --- Multi-session state ---
239
251
  let nextLocalId = 1;
@@ -382,6 +394,7 @@ function createServer(cwd, tlsOptions, caPath, pin, mainPort) {
382
394
  sentToolResults: {},
383
395
  pendingPermissions: {},
384
396
  pendingAskUser: {},
397
+ allowedTools: {},
385
398
  isProcessing: false,
386
399
  title: "",
387
400
  createdAt: Date.now(),
@@ -627,6 +640,11 @@ function createServer(cwd, tlsOptions, caPath, pin, mainPort) {
627
640
  });
628
641
  }
629
642
 
643
+ // Auto-approve if tool was previously allowed for session
644
+ if (session.allowedTools && session.allowedTools[toolName]) {
645
+ return Promise.resolve({ behavior: "allow", updatedInput: input });
646
+ }
647
+
630
648
  // Regular tool permission request: send to client and wait
631
649
  return new Promise(function(resolve) {
632
650
  var requestId = crypto.randomUUID();
@@ -895,6 +913,9 @@ function createServer(cwd, tlsOptions, caPath, pin, mainPort) {
895
913
 
896
914
  // Send cached state to this client only
897
915
  sendTo(ws, { type: "info", cwd: cwd, project: project });
916
+ if (latestVersion) {
917
+ sendTo(ws, { type: "update_available", version: latestVersion });
918
+ }
898
919
  if (slashCommands) {
899
920
  sendTo(ws, { type: "slash_commands", commands: slashCommands });
900
921
  }
@@ -1005,6 +1026,10 @@ function createServer(cwd, tlsOptions, caPath, pin, mainPort) {
1005
1026
  delete session.pendingPermissions[requestId];
1006
1027
 
1007
1028
  if (decision === "allow" || decision === "allow_always") {
1029
+ if (decision === "allow_always") {
1030
+ if (!session.allowedTools) session.allowedTools = {};
1031
+ session.allowedTools[pending.toolName] = true;
1032
+ }
1008
1033
  pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
1009
1034
  } else {
1010
1035
  pending.resolve({ behavior: "deny", message: "User denied permission" });
package/lib/updater.js CHANGED
@@ -93,4 +93,4 @@ async function checkAndUpdate(currentVersion, skipUpdate) {
93
93
  return false;
94
94
  }
95
95
 
96
- module.exports = { checkAndUpdate };
96
+ module.exports = { checkAndUpdate, fetchLatestVersion, isNewer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-relay",
3
- "version": "1.2.0",
3
+ "version": "1.2.4",
4
4
  "description": "Access Claude Code on your machine, from anywhere. One command, no config.",
5
5
  "bin": {
6
6
  "claude-relay": "./bin/cli.js"