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.
@@ -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 expandByDefault = !isError && tool.name === "Edit" && isDiffContent(displayContent);
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 (!isError && isDiffContent(displayContent)) {
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
- // But always show permission requests, questions, and errors regardless
36
- if (data.type !== "permission_request" && data.type !== "ask_user" && data.type !== "error") {
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 = await getSDK();
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 = await getSDK();
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 message, then abort
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
- // Expected: AbortError after we abort
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, { "Content-Type": "application/json" });
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