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 +230 -8
- package/lib/public/index.html +22 -2
- package/lib/public/style.css +345 -8
- package/lib/server.js +25 -0
- package/lib/updater.js +1 -1
- package/package.json +1 -1
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
-
|
|
151
|
-
ws
|
|
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
|
|
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;
|
package/lib/public/index.html
CHANGED
|
@@ -19,8 +19,11 @@
|
|
|
19
19
|
<div id="layout">
|
|
20
20
|
<div id="sidebar">
|
|
21
21
|
<div id="sidebar-header">
|
|
22
|
-
<
|
|
23
|
-
|
|
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>
|
package/lib/public/style.css
CHANGED
|
@@ -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
|
-
|
|
91
|
-
|
|
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:
|
|
104
|
-
height:
|
|
140
|
+
width: 100%;
|
|
141
|
+
height: 36px;
|
|
105
142
|
border-radius: 8px;
|
|
106
|
-
border:
|
|
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
|
-
|
|
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:
|
|
118
|
-
height:
|
|
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