claude-relay 2.1.2 → 2.2.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.
- package/README.md +31 -6
- package/bin/cli.js +7 -3
- package/lib/pages.js +3 -2
- package/lib/project.js +130 -39
- package/lib/public/app.js +381 -10
- package/lib/public/css/base.css +7 -0
- package/lib/public/css/filebrowser.css +149 -2
- package/lib/public/css/input.css +83 -10
- package/lib/public/css/menus.css +281 -1
- package/lib/public/css/messages.css +191 -0
- package/lib/public/css/sidebar.css +93 -1
- package/lib/public/index.html +90 -9
- package/lib/public/modules/filebrowser.js +33 -2
- package/lib/public/modules/input.js +98 -1
- package/lib/public/modules/notifications.js +19 -1
- package/lib/public/modules/qrcode.js +7 -1
- package/lib/public/modules/sidebar.js +233 -3
- package/lib/public/modules/terminal.js +484 -74
- package/lib/public/modules/tools.js +346 -2
- package/lib/public/sw.js +2 -5
- package/lib/push.js +16 -0
- package/lib/sdk-bridge.js +56 -6
- package/lib/server.js +4 -1
- package/lib/sessions.js +34 -0
- package/lib/terminal-manager.js +187 -0
- package/lib/terminal.js +3 -3
- package/lib/usage.js +90 -0
- package/package.json +1 -1
|
@@ -12,6 +12,8 @@ var planContent = null;
|
|
|
12
12
|
// --- Todo state ---
|
|
13
13
|
var todoItems = [];
|
|
14
14
|
var todoWidgetEl = null;
|
|
15
|
+
var todoWidgetVisible = true; // whether in-chat widget is in viewport
|
|
16
|
+
var todoObserver = null;
|
|
15
17
|
|
|
16
18
|
// --- Tool tracking ---
|
|
17
19
|
var tools = {};
|
|
@@ -232,6 +234,14 @@ function submitAskUserAnswer(container, toolId, questions, answers, multiSelecti
|
|
|
232
234
|
}
|
|
233
235
|
}
|
|
234
236
|
|
|
237
|
+
export function markAskUserAnswered(toolId) {
|
|
238
|
+
var container = document.querySelector('.ask-user-container[data-tool-id="' + toolId + '"]');
|
|
239
|
+
if (container && !container.classList.contains("answered")) {
|
|
240
|
+
container.classList.add("answered");
|
|
241
|
+
enableMainInput();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
235
245
|
// --- Permission request ---
|
|
236
246
|
function permissionInputSummary(toolName, input) {
|
|
237
247
|
if (!input || typeof input !== "object") return "";
|
|
@@ -250,6 +260,12 @@ export function renderPermissionRequest(requestId, toolName, toolInput, decision
|
|
|
250
260
|
ctx.finalizeAssistantBlock();
|
|
251
261
|
stopThinking();
|
|
252
262
|
|
|
263
|
+
// ExitPlanMode: render as plan confirmation instead of generic permission
|
|
264
|
+
if (toolName === "ExitPlanMode") {
|
|
265
|
+
renderPlanPermission(requestId);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
253
269
|
var container = document.createElement("div");
|
|
254
270
|
container.className = "permission-container";
|
|
255
271
|
container.dataset.requestId = requestId;
|
|
@@ -335,6 +351,54 @@ export function renderPermissionRequest(requestId, toolName, toolInput, decision
|
|
|
335
351
|
ctx.scrollToBottom();
|
|
336
352
|
}
|
|
337
353
|
|
|
354
|
+
function renderPlanPermission(requestId) {
|
|
355
|
+
var container = document.createElement("div");
|
|
356
|
+
container.className = "permission-container plan-permission";
|
|
357
|
+
container.dataset.requestId = requestId;
|
|
358
|
+
|
|
359
|
+
// Header
|
|
360
|
+
var header = document.createElement("div");
|
|
361
|
+
header.className = "permission-header plan-permission-header";
|
|
362
|
+
header.innerHTML =
|
|
363
|
+
'<span class="permission-icon">' + iconHtml("check-circle") + '</span>' +
|
|
364
|
+
'<span class="permission-title">Plan Approval</span>';
|
|
365
|
+
|
|
366
|
+
// Body (plan content already visible above, no need to repeat)
|
|
367
|
+
var body = document.createElement("div");
|
|
368
|
+
body.className = "permission-body";
|
|
369
|
+
|
|
370
|
+
// Actions
|
|
371
|
+
var actions = document.createElement("div");
|
|
372
|
+
actions.className = "permission-actions";
|
|
373
|
+
|
|
374
|
+
var approveBtn = document.createElement("button");
|
|
375
|
+
approveBtn.className = "permission-btn permission-allow";
|
|
376
|
+
approveBtn.textContent = "Approve Plan";
|
|
377
|
+
approveBtn.addEventListener("click", function () {
|
|
378
|
+
sendPermissionResponse(container, requestId, "allow");
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
var rejectBtn = document.createElement("button");
|
|
382
|
+
rejectBtn.className = "permission-btn permission-deny";
|
|
383
|
+
rejectBtn.textContent = "Reject Plan";
|
|
384
|
+
rejectBtn.addEventListener("click", function () {
|
|
385
|
+
sendPermissionResponse(container, requestId, "deny");
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
actions.appendChild(approveBtn);
|
|
389
|
+
actions.appendChild(rejectBtn);
|
|
390
|
+
|
|
391
|
+
container.appendChild(header);
|
|
392
|
+
container.appendChild(body);
|
|
393
|
+
container.appendChild(actions);
|
|
394
|
+
ctx.addToMessages(container);
|
|
395
|
+
|
|
396
|
+
pendingPermissions[requestId] = container;
|
|
397
|
+
refreshIcons();
|
|
398
|
+
ctx.setActivity(null);
|
|
399
|
+
ctx.scrollToBottom();
|
|
400
|
+
}
|
|
401
|
+
|
|
338
402
|
function sendPermissionResponse(container, requestId, decision) {
|
|
339
403
|
if (container.classList.contains("resolved")) return;
|
|
340
404
|
container.classList.add("resolved");
|
|
@@ -439,6 +503,7 @@ export function renderPlanCard(content) {
|
|
|
439
503
|
header.innerHTML =
|
|
440
504
|
'<span class="plan-card-icon">' + iconHtml("file-text") + '</span>' +
|
|
441
505
|
'<span class="plan-card-title">Implementation Plan</span>' +
|
|
506
|
+
'<button class="plan-card-copy" title="Copy plan">' + iconHtml("copy") + '</button>' +
|
|
442
507
|
'<span class="plan-card-chevron">' + iconHtml("chevron-down") + '</span>';
|
|
443
508
|
|
|
444
509
|
var body = document.createElement("div");
|
|
@@ -447,6 +512,21 @@ export function renderPlanCard(content) {
|
|
|
447
512
|
highlightCodeBlocks(body);
|
|
448
513
|
renderMermaidBlocks(body);
|
|
449
514
|
|
|
515
|
+
var copyBtn = header.querySelector(".plan-card-copy");
|
|
516
|
+
if (copyBtn) {
|
|
517
|
+
copyBtn.addEventListener("click", function (e) {
|
|
518
|
+
e.stopPropagation();
|
|
519
|
+
navigator.clipboard.writeText(content).then(function () {
|
|
520
|
+
copyBtn.innerHTML = iconHtml("check");
|
|
521
|
+
refreshIcons();
|
|
522
|
+
setTimeout(function () {
|
|
523
|
+
copyBtn.innerHTML = iconHtml("copy");
|
|
524
|
+
refreshIcons();
|
|
525
|
+
}, 1500);
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
450
530
|
header.addEventListener("click", function () {
|
|
451
531
|
el.classList.toggle("collapsed");
|
|
452
532
|
});
|
|
@@ -513,6 +593,9 @@ export function handleTaskUpdate(input) {
|
|
|
513
593
|
function renderTodoWidget() {
|
|
514
594
|
if (todoItems.length === 0) {
|
|
515
595
|
if (todoWidgetEl) { todoWidgetEl.remove(); todoWidgetEl = null; }
|
|
596
|
+
if (todoObserver) { todoObserver.disconnect(); todoObserver = null; }
|
|
597
|
+
todoWidgetVisible = true;
|
|
598
|
+
updateTodoSticky();
|
|
516
599
|
return;
|
|
517
600
|
}
|
|
518
601
|
|
|
@@ -549,11 +632,104 @@ function renderTodoWidget() {
|
|
|
549
632
|
|
|
550
633
|
if (isNew) {
|
|
551
634
|
ctx.addToMessages(todoWidgetEl);
|
|
635
|
+
setupTodoObserver();
|
|
552
636
|
}
|
|
637
|
+
updateTodoSticky();
|
|
553
638
|
refreshIcons();
|
|
554
639
|
ctx.scrollToBottom();
|
|
555
640
|
}
|
|
556
641
|
|
|
642
|
+
function setupTodoObserver() {
|
|
643
|
+
if (todoObserver) { todoObserver.disconnect(); todoObserver = null; }
|
|
644
|
+
if (!todoWidgetEl) return;
|
|
645
|
+
|
|
646
|
+
var messagesEl = document.getElementById("messages");
|
|
647
|
+
if (!messagesEl) return;
|
|
648
|
+
|
|
649
|
+
todoObserver = new IntersectionObserver(function (entries) {
|
|
650
|
+
todoWidgetVisible = entries[0].isIntersecting;
|
|
651
|
+
updateTodoStickyVisibility();
|
|
652
|
+
}, { root: messagesEl, threshold: 0 });
|
|
653
|
+
|
|
654
|
+
todoObserver.observe(todoWidgetEl);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function updateTodoStickyVisibility() {
|
|
658
|
+
var stickyEl = document.getElementById("todo-sticky");
|
|
659
|
+
if (!stickyEl) return;
|
|
660
|
+
|
|
661
|
+
if (todoWidgetVisible) {
|
|
662
|
+
stickyEl.classList.add("hidden");
|
|
663
|
+
} else {
|
|
664
|
+
// Only show if there are active (non-completed) tasks
|
|
665
|
+
var hasActive = false;
|
|
666
|
+
for (var i = 0; i < todoItems.length; i++) {
|
|
667
|
+
if (todoItems[i].status !== "completed") { hasActive = true; break; }
|
|
668
|
+
}
|
|
669
|
+
if (hasActive) {
|
|
670
|
+
stickyEl.classList.remove("hidden");
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function updateTodoSticky() {
|
|
676
|
+
var stickyEl = document.getElementById("todo-sticky");
|
|
677
|
+
if (!stickyEl) return;
|
|
678
|
+
|
|
679
|
+
// Hide if no active tasks (all completed or empty)
|
|
680
|
+
var hasActive = false;
|
|
681
|
+
for (var i = 0; i < todoItems.length; i++) {
|
|
682
|
+
if (todoItems[i].status !== "completed") { hasActive = true; break; }
|
|
683
|
+
}
|
|
684
|
+
if (!hasActive) {
|
|
685
|
+
stickyEl.classList.add("hidden");
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
var completed = 0;
|
|
690
|
+
for (var i = 0; i < todoItems.length; i++) {
|
|
691
|
+
if (todoItems[i].status === "completed") completed++;
|
|
692
|
+
}
|
|
693
|
+
var pct = Math.round(completed / todoItems.length * 100);
|
|
694
|
+
var wasCollapsed = stickyEl.classList.contains("collapsed");
|
|
695
|
+
|
|
696
|
+
var html = '<div class="todo-sticky-inner">' +
|
|
697
|
+
'<div class="todo-sticky-header">' +
|
|
698
|
+
'<span class="todo-sticky-icon">' + iconHtml("list-checks") + '</span>' +
|
|
699
|
+
'<span class="todo-sticky-title">Tasks</span>' +
|
|
700
|
+
'<span class="todo-sticky-count">' + completed + '/' + todoItems.length + '</span>' +
|
|
701
|
+
'<span class="todo-sticky-chevron">' + iconHtml("chevron-down") + '</span>' +
|
|
702
|
+
'</div>' +
|
|
703
|
+
'<div class="todo-sticky-progress"><div class="todo-sticky-progress-bar" style="width:' + pct + '%"></div></div>' +
|
|
704
|
+
'<div class="todo-sticky-items">';
|
|
705
|
+
|
|
706
|
+
for (var i = 0; i < todoItems.length; i++) {
|
|
707
|
+
var t = todoItems[i];
|
|
708
|
+
var statusClass = t.status === "completed" ? "completed" : t.status === "in_progress" ? "in-progress" : "pending";
|
|
709
|
+
html += '<div class="todo-sticky-item ' + statusClass + '">' +
|
|
710
|
+
'<span class="todo-sticky-item-icon">' + todoStatusIcon(t.status) + '</span>' +
|
|
711
|
+
'<span class="todo-sticky-item-text">' + escapeHtml(t.status === "in_progress" && t.activeForm ? t.activeForm : t.content) + '</span>' +
|
|
712
|
+
'</div>';
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
html += '</div></div>';
|
|
716
|
+
stickyEl.innerHTML = html;
|
|
717
|
+
|
|
718
|
+
// Only show sticky when in-chat widget is not visible in viewport
|
|
719
|
+
if (todoWidgetVisible) {
|
|
720
|
+
stickyEl.classList.add("hidden");
|
|
721
|
+
} else {
|
|
722
|
+
stickyEl.classList.remove("hidden");
|
|
723
|
+
}
|
|
724
|
+
if (wasCollapsed) stickyEl.classList.add("collapsed");
|
|
725
|
+
|
|
726
|
+
stickyEl.querySelector(".todo-sticky-header").addEventListener("click", function () {
|
|
727
|
+
stickyEl.classList.toggle("collapsed");
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
refreshIcons();
|
|
731
|
+
}
|
|
732
|
+
|
|
557
733
|
// --- Thinking ---
|
|
558
734
|
export function startThinking() {
|
|
559
735
|
ctx.finalizeAssistantBlock();
|
|
@@ -640,6 +816,164 @@ export function updateToolExecuting(id, name, input) {
|
|
|
640
816
|
ctx.scrollToBottom();
|
|
641
817
|
}
|
|
642
818
|
|
|
819
|
+
function buildUnifiedDiff(oldLines, newLines) {
|
|
820
|
+
var body = document.createElement("div");
|
|
821
|
+
body.className = "edit-diff-body";
|
|
822
|
+
|
|
823
|
+
var gutter = document.createElement("pre");
|
|
824
|
+
gutter.className = "edit-diff-gutter";
|
|
825
|
+
|
|
826
|
+
var content = document.createElement("pre");
|
|
827
|
+
content.className = "edit-diff-content";
|
|
828
|
+
|
|
829
|
+
var gutterLines = [];
|
|
830
|
+
|
|
831
|
+
for (var i = 0; i < oldLines.length; i++) {
|
|
832
|
+
gutterLines.push(String(i + 1));
|
|
833
|
+
var span = document.createElement("span");
|
|
834
|
+
span.className = "diff-del";
|
|
835
|
+
span.textContent = "- " + oldLines[i];
|
|
836
|
+
content.appendChild(span);
|
|
837
|
+
content.appendChild(document.createTextNode("\n"));
|
|
838
|
+
}
|
|
839
|
+
for (var i = 0; i < newLines.length; i++) {
|
|
840
|
+
gutterLines.push(String(i + 1));
|
|
841
|
+
var span = document.createElement("span");
|
|
842
|
+
span.className = "diff-add";
|
|
843
|
+
span.textContent = "+ " + newLines[i];
|
|
844
|
+
content.appendChild(span);
|
|
845
|
+
if (i < newLines.length - 1) content.appendChild(document.createTextNode("\n"));
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
gutter.textContent = gutterLines.join("\n");
|
|
849
|
+
body.appendChild(gutter);
|
|
850
|
+
body.appendChild(content);
|
|
851
|
+
return body;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function buildSplitDiff(oldLines, newLines) {
|
|
855
|
+
var body = document.createElement("div");
|
|
856
|
+
body.className = "edit-diff-body edit-diff-split";
|
|
857
|
+
|
|
858
|
+
var leftGutter = document.createElement("pre");
|
|
859
|
+
leftGutter.className = "edit-diff-gutter";
|
|
860
|
+
var leftContent = document.createElement("pre");
|
|
861
|
+
leftContent.className = "edit-diff-content edit-diff-side-old";
|
|
862
|
+
var rightGutter = document.createElement("pre");
|
|
863
|
+
rightGutter.className = "edit-diff-gutter";
|
|
864
|
+
var rightContent = document.createElement("pre");
|
|
865
|
+
rightContent.className = "edit-diff-content edit-diff-side-new";
|
|
866
|
+
|
|
867
|
+
var maxLen = Math.max(oldLines.length, newLines.length);
|
|
868
|
+
var leftNums = [];
|
|
869
|
+
var rightNums = [];
|
|
870
|
+
|
|
871
|
+
for (var i = 0; i < maxLen; i++) {
|
|
872
|
+
if (i < oldLines.length) {
|
|
873
|
+
leftNums.push(String(i + 1));
|
|
874
|
+
var span = document.createElement("span");
|
|
875
|
+
span.className = "diff-del";
|
|
876
|
+
span.textContent = oldLines[i];
|
|
877
|
+
leftContent.appendChild(span);
|
|
878
|
+
} else {
|
|
879
|
+
leftNums.push("");
|
|
880
|
+
leftContent.appendChild(document.createTextNode(""));
|
|
881
|
+
}
|
|
882
|
+
if (i < oldLines.length - 1 || (i >= oldLines.length && i < maxLen - 1)) {
|
|
883
|
+
leftContent.appendChild(document.createTextNode("\n"));
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (i < newLines.length) {
|
|
887
|
+
rightNums.push(String(i + 1));
|
|
888
|
+
var span = document.createElement("span");
|
|
889
|
+
span.className = "diff-add";
|
|
890
|
+
span.textContent = newLines[i];
|
|
891
|
+
rightContent.appendChild(span);
|
|
892
|
+
} else {
|
|
893
|
+
rightNums.push("");
|
|
894
|
+
rightContent.appendChild(document.createTextNode(""));
|
|
895
|
+
}
|
|
896
|
+
if (i < newLines.length - 1 || (i >= newLines.length && i < maxLen - 1)) {
|
|
897
|
+
rightContent.appendChild(document.createTextNode("\n"));
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
leftGutter.textContent = leftNums.join("\n");
|
|
902
|
+
rightGutter.textContent = rightNums.join("\n");
|
|
903
|
+
|
|
904
|
+
body.appendChild(leftGutter);
|
|
905
|
+
body.appendChild(leftContent);
|
|
906
|
+
body.appendChild(rightGutter);
|
|
907
|
+
body.appendChild(rightContent);
|
|
908
|
+
return body;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function renderEditDiff(oldStr, newStr, filePath) {
|
|
912
|
+
var wrapper = document.createElement("div");
|
|
913
|
+
wrapper.className = "edit-diff";
|
|
914
|
+
|
|
915
|
+
var oldLines = oldStr.split("\n");
|
|
916
|
+
var newLines = newStr.split("\n");
|
|
917
|
+
|
|
918
|
+
// Header with file path and split toggle (desktop only)
|
|
919
|
+
var header = document.createElement("div");
|
|
920
|
+
header.className = "edit-diff-header";
|
|
921
|
+
|
|
922
|
+
var pathSpan = document.createElement("span");
|
|
923
|
+
pathSpan.className = "edit-diff-path";
|
|
924
|
+
pathSpan.textContent = filePath || "";
|
|
925
|
+
header.appendChild(pathSpan);
|
|
926
|
+
|
|
927
|
+
var isMobile = "ontouchstart" in window;
|
|
928
|
+
var isSplit = false;
|
|
929
|
+
|
|
930
|
+
var unifiedBtn = document.createElement("button");
|
|
931
|
+
unifiedBtn.className = "edit-diff-toggle active";
|
|
932
|
+
unifiedBtn.innerHTML = iconHtml("list");
|
|
933
|
+
unifiedBtn.title = "Unified view";
|
|
934
|
+
|
|
935
|
+
var splitBtn = document.createElement("button");
|
|
936
|
+
splitBtn.className = "edit-diff-toggle";
|
|
937
|
+
splitBtn.innerHTML = iconHtml("columns-2");
|
|
938
|
+
splitBtn.title = "Split view";
|
|
939
|
+
|
|
940
|
+
var toggleWrap = document.createElement("span");
|
|
941
|
+
toggleWrap.className = "edit-diff-toggles";
|
|
942
|
+
if (isMobile) toggleWrap.style.display = "none";
|
|
943
|
+
toggleWrap.appendChild(unifiedBtn);
|
|
944
|
+
toggleWrap.appendChild(splitBtn);
|
|
945
|
+
header.appendChild(toggleWrap);
|
|
946
|
+
|
|
947
|
+
wrapper.appendChild(header);
|
|
948
|
+
|
|
949
|
+
var currentBody = buildUnifiedDiff(oldLines, newLines);
|
|
950
|
+
wrapper.appendChild(currentBody);
|
|
951
|
+
|
|
952
|
+
unifiedBtn.addEventListener("click", function () {
|
|
953
|
+
if (!isSplit) return;
|
|
954
|
+
isSplit = false;
|
|
955
|
+
unifiedBtn.classList.add("active");
|
|
956
|
+
splitBtn.classList.remove("active");
|
|
957
|
+
wrapper.removeChild(currentBody);
|
|
958
|
+
currentBody = buildUnifiedDiff(oldLines, newLines);
|
|
959
|
+
wrapper.appendChild(currentBody);
|
|
960
|
+
refreshIcons();
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
splitBtn.addEventListener("click", function () {
|
|
964
|
+
if (isSplit) return;
|
|
965
|
+
isSplit = true;
|
|
966
|
+
splitBtn.classList.add("active");
|
|
967
|
+
unifiedBtn.classList.remove("active");
|
|
968
|
+
wrapper.removeChild(currentBody);
|
|
969
|
+
currentBody = buildSplitDiff(oldLines, newLines);
|
|
970
|
+
wrapper.appendChild(currentBody);
|
|
971
|
+
refreshIcons();
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
return wrapper;
|
|
975
|
+
}
|
|
976
|
+
|
|
643
977
|
function isDiffContent(text) {
|
|
644
978
|
var lines = text.split("\n");
|
|
645
979
|
var diffMarkers = 0;
|
|
@@ -723,7 +1057,8 @@ export function updateToolResult(id, content, isError) {
|
|
|
723
1057
|
displayContent = displayContent.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").trim();
|
|
724
1058
|
if (displayContent.length > 10000) displayContent = displayContent.substring(0, 10000) + "\n... (truncated)";
|
|
725
1059
|
|
|
726
|
-
var
|
|
1060
|
+
var hasEditDiff = !isError && tool.name === "Edit" && tool.input && tool.input.old_string && tool.input.new_string;
|
|
1061
|
+
var expandByDefault = hasEditDiff || (!isError && tool.name === "Edit" && isDiffContent(displayContent));
|
|
727
1062
|
if (expandByDefault) {
|
|
728
1063
|
resultBlock.className = "tool-result-block";
|
|
729
1064
|
tool.el.classList.add("expanded");
|
|
@@ -731,7 +1066,9 @@ export function updateToolResult(id, content, isError) {
|
|
|
731
1066
|
resultBlock.className = "tool-result-block collapsed";
|
|
732
1067
|
}
|
|
733
1068
|
|
|
734
|
-
if (
|
|
1069
|
+
if (hasEditDiff) {
|
|
1070
|
+
resultBlock.appendChild(renderEditDiff(tool.input.old_string, tool.input.new_string, tool.input.file_path));
|
|
1071
|
+
} else if (!isError && isDiffContent(displayContent)) {
|
|
735
1072
|
resultBlock.appendChild(renderDiffPre(displayContent));
|
|
736
1073
|
} else if (!isError && tool.name === "Read" && tool.input && tool.input.file_path) {
|
|
737
1074
|
var parsed = parseLineNumberedContent(displayContent);
|
|
@@ -861,6 +1198,9 @@ export function restoreToolState(saved) {
|
|
|
861
1198
|
todoWidgetEl = saved.todoWidgetEl;
|
|
862
1199
|
inPlanMode = saved.inPlanMode;
|
|
863
1200
|
planContent = saved.planContent;
|
|
1201
|
+
if (todoWidgetEl) {
|
|
1202
|
+
setupTodoObserver();
|
|
1203
|
+
}
|
|
864
1204
|
}
|
|
865
1205
|
|
|
866
1206
|
export function resetToolState() {
|
|
@@ -870,7 +1210,11 @@ export function resetToolState() {
|
|
|
870
1210
|
planContent = null;
|
|
871
1211
|
todoItems = [];
|
|
872
1212
|
todoWidgetEl = null;
|
|
1213
|
+
todoWidgetVisible = true;
|
|
1214
|
+
if (todoObserver) { todoObserver.disconnect(); todoObserver = null; }
|
|
873
1215
|
pendingPermissions = {};
|
|
1216
|
+
var stickyEl = document.getElementById("todo-sticky");
|
|
1217
|
+
if (stickyEl) { stickyEl.classList.add("hidden"); stickyEl.innerHTML = ""; }
|
|
874
1218
|
}
|
|
875
1219
|
|
|
876
1220
|
export function initTools(_ctx) {
|
package/lib/public/sw.js
CHANGED
|
@@ -32,11 +32,8 @@ self.addEventListener("push", function (event) {
|
|
|
32
32
|
event.waitUntil(
|
|
33
33
|
self.clients.matchAll({ type: "window", includeUncontrolled: true }).then(function (clientList) {
|
|
34
34
|
// Skip notification if app is focused (user is already looking at it)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
for (var i = 0; i < clientList.length; i++) {
|
|
38
|
-
if (clientList[i].focused || clientList[i].visibilityState === "visible") return;
|
|
39
|
-
}
|
|
35
|
+
for (var i = 0; i < clientList.length; i++) {
|
|
36
|
+
if (clientList[i].focused || clientList[i].visibilityState === "visible") return;
|
|
40
37
|
}
|
|
41
38
|
return self.registration.showNotification(data.title || "Claude Relay", options);
|
|
42
39
|
}).catch(function () {})
|
package/lib/push.js
CHANGED
|
@@ -59,6 +59,22 @@ function initPush() {
|
|
|
59
59
|
|
|
60
60
|
save();
|
|
61
61
|
|
|
62
|
+
// Purge stale subscriptions on startup
|
|
63
|
+
var startupEndpoints = Array.from(subscriptions.keys());
|
|
64
|
+
for (var si = 0; si < startupEndpoints.length; si++) {
|
|
65
|
+
(function (ep) {
|
|
66
|
+
var sub = subscriptions.get(ep);
|
|
67
|
+
webpush.sendNotification(sub, JSON.stringify({ type: "test" }), { TTL: 0, vapidDetails: vapidDetails })
|
|
68
|
+
.then(function () {})
|
|
69
|
+
.catch(function (err) {
|
|
70
|
+
if (err.statusCode === 403 || err.statusCode === 410 || err.statusCode === 404) {
|
|
71
|
+
subscriptions.delete(ep);
|
|
72
|
+
save();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
})(startupEndpoints[si]);
|
|
76
|
+
}
|
|
77
|
+
|
|
62
78
|
function addSubscription(sub) {
|
|
63
79
|
if (!sub || !sub.endpoint) return;
|
|
64
80
|
// Store immediately, then validate async. Invalid subs get cleaned on first sendPush.
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -75,7 +75,7 @@ function createSDKBridge(opts) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
// Cache slash_commands from CLI init message
|
|
78
|
+
// Cache slash_commands and model from CLI init message
|
|
79
79
|
if (parsed.type === "system" && parsed.subtype === "init") {
|
|
80
80
|
if (parsed.skills) {
|
|
81
81
|
sm.skillNames = new Set(parsed.skills);
|
|
@@ -86,6 +86,10 @@ function createSDKBridge(opts) {
|
|
|
86
86
|
});
|
|
87
87
|
send({ type: "slash_commands", commands: sm.slashCommands });
|
|
88
88
|
}
|
|
89
|
+
if (parsed.model) {
|
|
90
|
+
sm.currentModel = parsed.model;
|
|
91
|
+
send({ type: "model_info", model: parsed.model, models: sm.availableModels || [] });
|
|
92
|
+
}
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
if (parsed.type === "stream_event" && parsed.event) {
|
|
@@ -214,6 +218,7 @@ function createSDKBridge(opts) {
|
|
|
214
218
|
type: "result",
|
|
215
219
|
cost: parsed.total_cost_usd,
|
|
216
220
|
duration: parsed.duration_ms,
|
|
221
|
+
usage: parsed.usage || null,
|
|
217
222
|
sessionId: parsed.session_id,
|
|
218
223
|
});
|
|
219
224
|
sendAndRecord(session, { type: "done", code: 0 });
|
|
@@ -232,6 +237,14 @@ function createSDKBridge(opts) {
|
|
|
232
237
|
session.streamedText = false;
|
|
233
238
|
sm.broadcastSessionList();
|
|
234
239
|
|
|
240
|
+
} else if (parsed.type === "system" && parsed.subtype === "status") {
|
|
241
|
+
if (parsed.status === "compacting") {
|
|
242
|
+
sendAndRecord(session, { type: "compacting", active: true });
|
|
243
|
+
} else if (session.compacting) {
|
|
244
|
+
sendAndRecord(session, { type: "compacting", active: false });
|
|
245
|
+
}
|
|
246
|
+
session.compacting = parsed.status === "compacting";
|
|
247
|
+
|
|
235
248
|
} else if (parsed.type && parsed.type !== "system" && parsed.type !== "user") {
|
|
236
249
|
}
|
|
237
250
|
}
|
|
@@ -336,13 +349,20 @@ function createSDKBridge(opts) {
|
|
|
336
349
|
async function getOrCreateRewindQuery(session) {
|
|
337
350
|
if (session.queryInstance) return { query: session.queryInstance, isTemp: false, cleanup: function() {} };
|
|
338
351
|
|
|
339
|
-
var sdk
|
|
352
|
+
var sdk;
|
|
353
|
+
try {
|
|
354
|
+
sdk = await getSDK();
|
|
355
|
+
} catch (e) {
|
|
356
|
+
send({ type: "error", text: "Failed to load Claude SDK: " + (e.message || e) });
|
|
357
|
+
throw e;
|
|
358
|
+
}
|
|
340
359
|
var mq = createMessageQueue();
|
|
341
360
|
|
|
342
361
|
var tempQuery = sdk.query({
|
|
343
362
|
prompt: mq,
|
|
344
363
|
options: {
|
|
345
364
|
cwd: cwd,
|
|
365
|
+
settingSources: ["user", "project", "local"],
|
|
346
366
|
enableFileCheckpointing: true,
|
|
347
367
|
resume: session.cliSessionId,
|
|
348
368
|
},
|
|
@@ -361,7 +381,13 @@ function createSDKBridge(opts) {
|
|
|
361
381
|
}
|
|
362
382
|
|
|
363
383
|
async function startQuery(session, text, images) {
|
|
364
|
-
var sdk
|
|
384
|
+
var sdk;
|
|
385
|
+
try {
|
|
386
|
+
sdk = await getSDK();
|
|
387
|
+
} catch (e) {
|
|
388
|
+
send({ type: "error", text: "Failed to load Claude SDK: " + (e.message || e) });
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
365
391
|
|
|
366
392
|
session.messageQueue = createMessageQueue();
|
|
367
393
|
session.blocks = {};
|
|
@@ -392,6 +418,7 @@ function createSDKBridge(opts) {
|
|
|
392
418
|
|
|
393
419
|
var queryOptions = {
|
|
394
420
|
cwd: cwd,
|
|
421
|
+
settingSources: ["user", "project", "local"],
|
|
395
422
|
includePartialMessages: true,
|
|
396
423
|
enableFileCheckpointing: true,
|
|
397
424
|
extraArgs: { "replay-user-messages": null },
|
|
@@ -480,7 +507,7 @@ function createSDKBridge(opts) {
|
|
|
480
507
|
return text;
|
|
481
508
|
}
|
|
482
509
|
|
|
483
|
-
// SDK warmup: grab slash_commands from SDK init
|
|
510
|
+
// SDK warmup: grab slash_commands, model, and available models from SDK init
|
|
484
511
|
async function warmup() {
|
|
485
512
|
try {
|
|
486
513
|
var sdk = await getSDK();
|
|
@@ -490,7 +517,7 @@ function createSDKBridge(opts) {
|
|
|
490
517
|
mq.end();
|
|
491
518
|
var stream = sdk.query({
|
|
492
519
|
prompt: mq,
|
|
493
|
-
options: { cwd: cwd, abortController: ac },
|
|
520
|
+
options: { cwd: cwd, settingSources: ["user", "project", "local"], abortController: ac },
|
|
494
521
|
});
|
|
495
522
|
for await (var msg of stream) {
|
|
496
523
|
if (msg.type === "system" && msg.subtype === "init") {
|
|
@@ -503,12 +530,34 @@ function createSDKBridge(opts) {
|
|
|
503
530
|
});
|
|
504
531
|
send({ type: "slash_commands", commands: sm.slashCommands });
|
|
505
532
|
}
|
|
533
|
+
if (msg.model) {
|
|
534
|
+
sm.currentModel = msg.model;
|
|
535
|
+
}
|
|
536
|
+
// Fetch available models before aborting
|
|
537
|
+
try {
|
|
538
|
+
var models = await stream.supportedModels();
|
|
539
|
+
sm.availableModels = models || [];
|
|
540
|
+
} catch (e) {}
|
|
541
|
+
send({ type: "model_info", model: sm.currentModel || "", models: sm.availableModels || [] });
|
|
506
542
|
ac.abort();
|
|
507
543
|
break;
|
|
508
544
|
}
|
|
509
545
|
}
|
|
510
546
|
} catch (e) {
|
|
511
|
-
|
|
547
|
+
if (e && e.name !== "AbortError" && !(e.message && e.message.indexOf("aborted") !== -1)) {
|
|
548
|
+
send({ type: "error", text: "Failed to load Claude SDK: " + (e.message || e) });
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function setModel(session, model) {
|
|
554
|
+
if (!session.queryInstance) return;
|
|
555
|
+
try {
|
|
556
|
+
await session.queryInstance.setModel(model);
|
|
557
|
+
sm.currentModel = model;
|
|
558
|
+
send({ type: "model_info", model: model, models: sm.availableModels || [] });
|
|
559
|
+
} catch (e) {
|
|
560
|
+
send({ type: "error", text: "Failed to switch model: " + (e.message || e) });
|
|
512
561
|
}
|
|
513
562
|
}
|
|
514
563
|
|
|
@@ -520,6 +569,7 @@ function createSDKBridge(opts) {
|
|
|
520
569
|
getOrCreateRewindQuery: getOrCreateRewindQuery,
|
|
521
570
|
startQuery: startQuery,
|
|
522
571
|
pushMessage: pushMessage,
|
|
572
|
+
setModel: setModel,
|
|
523
573
|
permissionPushTitle: permissionPushTitle,
|
|
524
574
|
permissionPushBody: permissionPushBody,
|
|
525
575
|
warmup: warmup,
|
package/lib/server.js
CHANGED
|
@@ -262,7 +262,10 @@ function createServer(opts) {
|
|
|
262
262
|
// Global info endpoint (auth required)
|
|
263
263
|
if (req.method === "GET" && req.url === "/info") {
|
|
264
264
|
if (!isAuthed(req, authToken)) {
|
|
265
|
-
res.writeHead(401, {
|
|
265
|
+
res.writeHead(401, {
|
|
266
|
+
"Content-Type": "application/json",
|
|
267
|
+
"Access-Control-Allow-Origin": "*",
|
|
268
|
+
});
|
|
266
269
|
res.end('{"error":"unauthorized"}');
|
|
267
270
|
return;
|
|
268
271
|
}
|
package/lib/sessions.js
CHANGED
|
@@ -181,6 +181,8 @@ function createSessionManager(opts) {
|
|
|
181
181
|
for (var i = fromIndex; i < total; i++) {
|
|
182
182
|
send(session.history[i]);
|
|
183
183
|
}
|
|
184
|
+
|
|
185
|
+
send({ type: "history_done" });
|
|
184
186
|
}
|
|
185
187
|
|
|
186
188
|
function switchSession(localId) {
|
|
@@ -281,6 +283,37 @@ function createSessionManager(opts) {
|
|
|
281
283
|
activeSessionId = lastSession.localId;
|
|
282
284
|
}
|
|
283
285
|
|
|
286
|
+
function searchSessions(query) {
|
|
287
|
+
if (!query) return [];
|
|
288
|
+
var q = query.toLowerCase();
|
|
289
|
+
var results = [];
|
|
290
|
+
sessions.forEach(function (session) {
|
|
291
|
+
var titleMatch = (session.title || "New Session").toLowerCase().indexOf(q) !== -1;
|
|
292
|
+
var contentMatch = false;
|
|
293
|
+
for (var i = 0; i < session.history.length; i++) {
|
|
294
|
+
var entry = session.history[i];
|
|
295
|
+
if ((entry.type === "delta" || entry.type === "user_message") && entry.text) {
|
|
296
|
+
if (entry.text.toLowerCase().indexOf(q) !== -1) {
|
|
297
|
+
contentMatch = true;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (titleMatch || contentMatch) {
|
|
303
|
+
results.push({
|
|
304
|
+
id: session.localId,
|
|
305
|
+
cliSessionId: session.cliSessionId || null,
|
|
306
|
+
title: session.title || "New Session",
|
|
307
|
+
active: session.localId === activeSessionId,
|
|
308
|
+
isProcessing: session.isProcessing,
|
|
309
|
+
lastActivity: session.lastActivity || session.createdAt || 0,
|
|
310
|
+
matchType: titleMatch && contentMatch ? "both" : titleMatch ? "title" : "content",
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
return results;
|
|
315
|
+
}
|
|
316
|
+
|
|
284
317
|
return {
|
|
285
318
|
get activeSessionId() { return activeSessionId; },
|
|
286
319
|
get nextLocalId() { return nextLocalId; },
|
|
@@ -301,6 +334,7 @@ function createSessionManager(opts) {
|
|
|
301
334
|
sendAndRecord: doSendAndRecord,
|
|
302
335
|
findTurnBoundary: findTurnBoundary,
|
|
303
336
|
replayHistory: replayHistory,
|
|
337
|
+
searchSessions: searchSessions,
|
|
304
338
|
};
|
|
305
339
|
}
|
|
306
340
|
|