clay-server 2.18.0 → 2.19.0-beta.1

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/project.js CHANGED
@@ -438,11 +438,13 @@ function createProjectContext(opts) {
438
438
  craftingSessionId: null,
439
439
  startedAt: null,
440
440
  loopId: null,
441
+ loopFilesId: null,
441
442
  };
442
443
 
443
444
  function loopDir() {
444
- if (!loopState.loopId) return null;
445
- return path.join(cwd, ".claude", "loops", loopState.loopId);
445
+ var id = loopState.loopFilesId || loopState.loopId;
446
+ if (!id) return null;
447
+ return path.join(cwd, ".claude", "loops", id);
446
448
  }
447
449
 
448
450
  function generateLoopId() {
@@ -469,6 +471,7 @@ function createProjectContext(opts) {
469
471
  wizardData: loopState.wizardData,
470
472
  startedAt: loopState.startedAt,
471
473
  loopId: loopState.loopId,
474
+ loopFilesId: loopState.loopFilesId || null,
472
475
  };
473
476
  var tmpPath = _loopStatePath + ".tmp";
474
477
  fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
@@ -491,6 +494,7 @@ function createProjectContext(opts) {
491
494
  loopState.wizardData = data.wizardData || null;
492
495
  loopState.startedAt = data.startedAt || null;
493
496
  loopState.loopId = data.loopId || null;
497
+ loopState.loopFilesId = data.loopFilesId || null;
494
498
  // SDK sessions cannot survive daemon restart
495
499
  loopState.currentSessionId = null;
496
500
  loopState.judgeSessionId = null;
@@ -557,6 +561,7 @@ function createProjectContext(opts) {
557
561
  loopState.craftingSessionId = null;
558
562
  loopState.startedAt = null;
559
563
  loopState.loopId = null;
564
+ loopState.loopFilesId = null;
560
565
  saveLoopState();
561
566
  }
562
567
 
@@ -691,9 +696,10 @@ function createProjectContext(opts) {
691
696
  }
692
697
  // Set the loopId to the schedule's own id (not the linked task) so sidebar groups correctly
693
698
  loopState.loopId = record.id;
699
+ loopState.loopFilesId = loopFilesId;
694
700
  loopState.wizardData = null;
695
701
  activeRegistryId = record.id;
696
- console.log("[loop-registry] Auto-starting loop: " + record.name + " (" + loopId + ")");
702
+ console.log("[loop-registry] Auto-starting loop: " + record.name + " (" + loopState.loopId + ")");
697
703
  send({ type: "schedule_run_started", recordId: record.id });
698
704
  startLoop({ maxIterations: record.maxIterations, name: record.name });
699
705
  },
@@ -3473,12 +3479,63 @@ function createProjectContext(opts) {
3473
3479
  return;
3474
3480
  }
3475
3481
  loopState.loopId = rerunRec.id;
3482
+ loopState.loopFilesId = null;
3476
3483
  activeRegistryId = null; // not a scheduled trigger
3477
3484
  send({ type: "loop_rerun_started", recordId: rerunRec.id });
3478
3485
  startLoop();
3479
3486
  return;
3480
3487
  }
3481
3488
 
3489
+ // --- Schedule message for after rate limit resets ---
3490
+ if (msg.type === "schedule_message") {
3491
+ var schedSession = getSessionForWs(ws);
3492
+ if (!schedSession || !msg.text || !msg.resetsAt) return;
3493
+ // Cancel any existing scheduled message
3494
+ if (schedSession.scheduledMessage && schedSession.scheduledMessage.timer) {
3495
+ clearTimeout(schedSession.scheduledMessage.timer);
3496
+ }
3497
+ var schedDelay = Math.max(0, msg.resetsAt - Date.now()) + 3000;
3498
+ var schedEntry = {
3499
+ type: "scheduled_message_queued",
3500
+ text: msg.text,
3501
+ resetsAt: msg.resetsAt,
3502
+ scheduledAt: Date.now(),
3503
+ };
3504
+ sm.sendAndRecord(schedSession, schedEntry);
3505
+ schedSession.scheduledMessage = {
3506
+ text: msg.text,
3507
+ resetsAt: msg.resetsAt,
3508
+ timer: setTimeout(function () {
3509
+ schedSession.scheduledMessage = null;
3510
+ if (schedSession.destroying) return;
3511
+ console.log("[project] Scheduled message firing for session " + schedSession.localId);
3512
+ sm.sendAndRecord(schedSession, { type: "scheduled_message_sent" });
3513
+ // Send the message as if user typed it
3514
+ var schedUserMsg = { type: "user_message", text: msg.text };
3515
+ schedSession.history.push(schedUserMsg);
3516
+ sm.appendToSessionFile(schedSession, schedUserMsg);
3517
+ sendToSession(schedSession.localId, schedUserMsg);
3518
+ schedSession.isProcessing = true;
3519
+ onProcessingChanged();
3520
+ sendToSession(schedSession.localId, { type: "status", status: "processing" });
3521
+ sdk.startQuery(schedSession, msg.text, null, getLinuxUserForSession(schedSession));
3522
+ sm.broadcastSessionList();
3523
+ }, schedDelay),
3524
+ };
3525
+ return;
3526
+ }
3527
+
3528
+ if (msg.type === "cancel_scheduled_message") {
3529
+ var cancelSession = getSessionForWs(ws);
3530
+ if (!cancelSession) return;
3531
+ if (cancelSession.scheduledMessage && cancelSession.scheduledMessage.timer) {
3532
+ clearTimeout(cancelSession.scheduledMessage.timer);
3533
+ cancelSession.scheduledMessage = null;
3534
+ sm.sendAndRecord(cancelSession, { type: "scheduled_message_cancelled" });
3535
+ }
3536
+ return;
3537
+ }
3538
+
3482
3539
  if (msg.type !== "message") return;
3483
3540
  if (!msg.text && (!msg.images || msg.images.length === 0) && (!msg.pastes || msg.pastes.length === 0)) return;
3484
3541
 
@@ -3491,6 +3548,13 @@ function createProjectContext(opts) {
3491
3548
  sm.saveSessionFile(session);
3492
3549
  }
3493
3550
 
3551
+ // Cancel any pending scheduled message when user sends a regular message
3552
+ if (session.scheduledMessage && session.scheduledMessage.timer) {
3553
+ clearTimeout(session.scheduledMessage.timer);
3554
+ session.scheduledMessage = null;
3555
+ sm.sendAndRecord(session, { type: "scheduled_message_cancelled" });
3556
+ }
3557
+
3494
3558
  var userMsg = { type: "user_message", text: msg.text || "" };
3495
3559
  if (msg.images && msg.images.length > 0) {
3496
3560
  userMsg.imageCount = msg.images.length;
@@ -5866,6 +5930,14 @@ function createProjectContext(opts) {
5866
5930
  // Abort all active sessions and clean up mention sessions
5867
5931
  sm.sessions.forEach(function (session) {
5868
5932
  session.destroying = true;
5933
+ if (session.autoContinueTimer) {
5934
+ clearTimeout(session.autoContinueTimer);
5935
+ session.autoContinueTimer = null;
5936
+ }
5937
+ if (session.scheduledMessage && session.scheduledMessage.timer) {
5938
+ clearTimeout(session.scheduledMessage.timer);
5939
+ session.scheduledMessage = null;
5940
+ }
5869
5941
  if (session.abortController) {
5870
5942
  try { session.abortController.abort(); } catch (e) {}
5871
5943
  }
package/lib/public/app.js CHANGED
@@ -22,6 +22,7 @@ import { initAsciiLogo, startLogoAnimation, stopLogoAnimation } from './modules/
22
22
  import { initPlaybook, openPlaybook, getPlaybooks, getPlaybookForTip, isCompleted as isPlaybookCompleted } from './modules/playbook.js';
23
23
  import { initSTT } from './modules/stt.js';
24
24
  import { initProfile, getProfileLang } from './modules/profile.js';
25
+ import { initUserSettings } from './modules/user-settings.js';
25
26
  import { initAdmin, checkAdminAccess } from './modules/admin.js';
26
27
  import { initSessionSearch, toggleSearch, closeSearch, isSearchOpen, handleFindInSessionResults, onHistoryPrepended as onSessionSearchHistoryPrepended } from './modules/session-search.js';
27
28
  import { initTooltips, registerTooltip } from './modules/tooltip.js';
@@ -1605,6 +1606,8 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
1605
1606
  var connected = false;
1606
1607
  var wasConnected = false;
1607
1608
  var processing = false;
1609
+ var rateLimitResetsAt = null; // ms timestamp, set on rate_limit rejected
1610
+ var rateLimitResetTimer = null;
1608
1611
  // isComposing -> modules/input.js
1609
1612
  var reconnectTimer = null;
1610
1613
  var reconnectDelay = 1000;
@@ -3490,6 +3493,14 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
3490
3493
  popoverText = typeLabel + " limit exceeded";
3491
3494
  updateRateLimitIndicator(msg);
3492
3495
  startRateLimitCountdown(null, msg.resetsAt, null);
3496
+ // Track for schedule mode
3497
+ rateLimitResetsAt = msg.resetsAt;
3498
+ if (rateLimitResetTimer) clearTimeout(rateLimitResetTimer);
3499
+ rateLimitResetTimer = setTimeout(function () {
3500
+ rateLimitResetsAt = null;
3501
+ rateLimitResetTimer = null;
3502
+ exitScheduleMode();
3503
+ }, msg.resetsAt - Date.now() + 1000);
3493
3504
  } else {
3494
3505
  var pct = msg.utilization ? Math.round(msg.utilization * 100) : null;
3495
3506
  popoverText = typeLabel + " warning" + (pct ? " (" + pct + "% used)" : "");
@@ -3499,6 +3510,117 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
3499
3510
  showRateLimitPopover(popoverText, isRejected);
3500
3511
  }
3501
3512
 
3513
+ // --- Schedule mode (rate limit) ---
3514
+
3515
+ var scheduleModeActive = false;
3516
+
3517
+ function enterScheduleMode() {
3518
+ if (scheduleModeActive) return;
3519
+ scheduleModeActive = true;
3520
+ var inputRow = document.getElementById("input-row");
3521
+ if (inputRow) inputRow.classList.add("input-rate-limited");
3522
+ if (inputEl) {
3523
+ inputEl.dataset.originalPlaceholder = inputEl.placeholder;
3524
+ inputEl.placeholder = "Schedule message after limit resets...";
3525
+ }
3526
+ }
3527
+
3528
+ function exitScheduleMode() {
3529
+ if (!scheduleModeActive) return;
3530
+ scheduleModeActive = false;
3531
+ var inputRow = document.getElementById("input-row");
3532
+ if (inputRow) inputRow.classList.remove("input-rate-limited");
3533
+ if (inputEl && inputEl.dataset.originalPlaceholder) {
3534
+ inputEl.placeholder = inputEl.dataset.originalPlaceholder;
3535
+ delete inputEl.dataset.originalPlaceholder;
3536
+ }
3537
+ }
3538
+
3539
+ // --- Scheduled message in chat history ---
3540
+
3541
+ var scheduledMsgEl = null;
3542
+ var scheduledCountdownTimer = null;
3543
+
3544
+ function addScheduledMessageBubble(text, resetsAt) {
3545
+ removeScheduledMessageBubble();
3546
+ var wrap = document.createElement("div");
3547
+ wrap.className = "scheduled-msg-wrap";
3548
+ wrap.id = "scheduled-msg-bubble";
3549
+
3550
+ var bubble = document.createElement("div");
3551
+ bubble.className = "scheduled-msg-bubble";
3552
+
3553
+ var textEl = document.createElement("div");
3554
+ textEl.className = "scheduled-msg-text";
3555
+ textEl.textContent = text;
3556
+
3557
+ var metaEl = document.createElement("div");
3558
+ metaEl.className = "scheduled-msg-meta";
3559
+
3560
+ var clockIcon = document.createElement("span");
3561
+ clockIcon.className = "scheduled-msg-icon";
3562
+ clockIcon.innerHTML = iconHtml("clock");
3563
+ metaEl.appendChild(clockIcon);
3564
+
3565
+ var countdownEl = document.createElement("span");
3566
+ countdownEl.className = "scheduled-msg-countdown";
3567
+ metaEl.appendChild(countdownEl);
3568
+
3569
+ var cancelBtn = document.createElement("button");
3570
+ cancelBtn.className = "scheduled-msg-cancel";
3571
+ cancelBtn.title = "Cancel scheduled message";
3572
+ cancelBtn.innerHTML = iconHtml("x");
3573
+ cancelBtn.addEventListener("click", function () {
3574
+ if (ws && ws.readyState === 1) {
3575
+ ws.send(JSON.stringify({ type: "cancel_scheduled_message" }));
3576
+ }
3577
+ });
3578
+ metaEl.appendChild(cancelBtn);
3579
+
3580
+ bubble.appendChild(textEl);
3581
+ bubble.appendChild(metaEl);
3582
+ wrap.appendChild(bubble);
3583
+ messagesEl.appendChild(wrap);
3584
+ scheduledMsgEl = wrap;
3585
+ scrollToBottom();
3586
+
3587
+ // Start countdown
3588
+ function updateCountdown() {
3589
+ var remaining = resetsAt - Date.now();
3590
+ if (remaining <= 0) {
3591
+ countdownEl.textContent = "Sending...";
3592
+ if (scheduledCountdownTimer) { clearInterval(scheduledCountdownTimer); scheduledCountdownTimer = null; }
3593
+ return;
3594
+ }
3595
+ var hrs = Math.floor(remaining / 3600000);
3596
+ var mins = Math.floor((remaining % 3600000) / 60000);
3597
+ var secs = Math.floor((remaining % 60000) / 1000);
3598
+ var timeStr = "";
3599
+ if (hrs > 0) timeStr += hrs + "h ";
3600
+ if (mins > 0 || hrs > 0) timeStr += mins + "m ";
3601
+ timeStr += secs + "s";
3602
+
3603
+ var sendDate = new Date(resetsAt);
3604
+ var absTime = sendDate.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
3605
+ countdownEl.textContent = "Sends at " + absTime + " (" + timeStr + ")";
3606
+ }
3607
+ updateCountdown();
3608
+ scheduledCountdownTimer = setInterval(updateCountdown, 1000);
3609
+
3610
+ refreshIcons(wrap);
3611
+ }
3612
+
3613
+ function removeScheduledMessageBubble() {
3614
+ if (scheduledMsgEl) {
3615
+ scheduledMsgEl.remove();
3616
+ scheduledMsgEl = null;
3617
+ }
3618
+ if (scheduledCountdownTimer) {
3619
+ clearInterval(scheduledCountdownTimer);
3620
+ scheduledCountdownTimer = null;
3621
+ }
3622
+ }
3623
+
3502
3624
  // --- Fast Mode State ---
3503
3625
 
3504
3626
  var fastModeIndicatorEl = null;
@@ -4366,6 +4488,10 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
4366
4488
  if (isNotifAlertEnabled() && !window._pushSubscription) showDoneNotification();
4367
4489
  if (isNotifSoundEnabled()) playDoneSound();
4368
4490
  }
4491
+ // Enter schedule mode if rate limited
4492
+ if (rateLimitResetsAt && rateLimitResetsAt > Date.now() && msg.code === 1) {
4493
+ enterScheduleMode();
4494
+ }
4369
4495
  break;
4370
4496
 
4371
4497
  case "stderr":
@@ -4396,6 +4522,34 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
4396
4522
  handleRateLimitEvent(msg);
4397
4523
  break;
4398
4524
 
4525
+ case "scheduled_message_queued":
4526
+ addScheduledMessageBubble(msg.text, msg.resetsAt);
4527
+ exitScheduleMode();
4528
+ break;
4529
+
4530
+ case "scheduled_message_sent":
4531
+ removeScheduledMessageBubble();
4532
+ processing = true;
4533
+ setStatus("processing");
4534
+ break;
4535
+
4536
+ case "scheduled_message_cancelled":
4537
+ removeScheduledMessageBubble();
4538
+ // Re-enter schedule mode if still rate limited
4539
+ if (rateLimitResetsAt && rateLimitResetsAt > Date.now()) {
4540
+ enterScheduleMode();
4541
+ }
4542
+ break;
4543
+
4544
+ case "auto_continue_scheduled":
4545
+ // Scheduler auto-continue, just show info
4546
+ break;
4547
+
4548
+ case "auto_continue_fired":
4549
+ processing = true;
4550
+ setStatus("processing");
4551
+ break;
4552
+
4399
4553
  case "prompt_suggestion":
4400
4554
  showSuggestionChips(msg.suggestion);
4401
4555
  break;
@@ -5139,6 +5293,9 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
5139
5293
  getMateName: function () { return dmTargetUser ? (dmTargetUser.displayName || "Mate") : "Mate"; },
5140
5294
  getMateAvatarUrl: function () { return document.body.dataset.mateAvatarUrl || ""; },
5141
5295
  showMatePreThinking: function () { showMatePreThinking(); },
5296
+ isScheduleMode: function () { return scheduleModeActive; },
5297
+ getRateLimitResetsAt: function () { return rateLimitResetsAt; },
5298
+ exitScheduleMode: exitScheduleMode,
5142
5299
  });
5143
5300
 
5144
5301
  // --- @Mention module ---
@@ -5176,6 +5333,11 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
5176
5333
  basePath: basePath,
5177
5334
  });
5178
5335
 
5336
+ // --- User settings (full-screen overlay) ---
5337
+ initUserSettings({
5338
+ basePath: basePath,
5339
+ });
5340
+
5179
5341
  // --- Force PIN change overlay (for admin-created accounts with temp PIN) ---
5180
5342
  function showForceChangePinOverlay() {
5181
5343
  var ov = document.createElement("div");
@@ -7284,7 +7446,12 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
7284
7446
  btn.id = "cursor-share-toggle";
7285
7447
  btn.className = "cursor-share-btn";
7286
7448
  btn.innerHTML = '<i data-lucide="mouse-pointer-2"></i>';
7287
- actionsEl.appendChild(btn);
7449
+ var settingsBtn = document.getElementById("user-settings-btn");
7450
+ if (settingsBtn) {
7451
+ actionsEl.insertBefore(btn, settingsBtn);
7452
+ } else {
7453
+ actionsEl.appendChild(btn);
7454
+ }
7288
7455
 
7289
7456
  function updateToggleStyle() {
7290
7457
  if (cursorSharingEnabled) {
@@ -666,3 +666,84 @@
666
666
  pointer-events: none;
667
667
  opacity: 0.5;
668
668
  }
669
+
670
+ /* ==========================================================================
671
+ Rate Limit Schedule Mode
672
+ ========================================================================== */
673
+
674
+ /* Input area styling when rate limited */
675
+ #input-row.input-rate-limited {
676
+ border-color: var(--warning, #d97706);
677
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--warning, #d97706) 20%, transparent);
678
+ }
679
+
680
+ #input-row.input-rate-limited:focus-within {
681
+ border-color: var(--warning, #d97706);
682
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--warning, #d97706) 30%, transparent);
683
+ }
684
+
685
+ /* Scheduled message bubble in chat */
686
+ .scheduled-msg-wrap {
687
+ display: flex;
688
+ justify-content: flex-end;
689
+ padding: 4px 16px;
690
+ }
691
+
692
+ .scheduled-msg-bubble {
693
+ max-width: 75%;
694
+ padding: 10px 14px;
695
+ border-radius: 12px;
696
+ background: color-mix(in srgb, var(--warning, #d97706) 8%, var(--bg-alt));
697
+ border: 1px dashed color-mix(in srgb, var(--warning, #d97706) 40%, transparent);
698
+ }
699
+
700
+ .scheduled-msg-text {
701
+ font-size: 14px;
702
+ color: var(--text);
703
+ line-height: 1.5;
704
+ white-space: pre-wrap;
705
+ word-break: break-word;
706
+ }
707
+
708
+ .scheduled-msg-meta {
709
+ display: flex;
710
+ align-items: center;
711
+ gap: 6px;
712
+ margin-top: 8px;
713
+ font-size: 12px;
714
+ color: var(--warning, #d97706);
715
+ }
716
+
717
+ .scheduled-msg-icon .lucide {
718
+ width: 14px;
719
+ height: 14px;
720
+ }
721
+
722
+ .scheduled-msg-countdown {
723
+ flex: 1;
724
+ }
725
+
726
+ .scheduled-msg-cancel {
727
+ display: flex;
728
+ align-items: center;
729
+ justify-content: center;
730
+ width: 22px;
731
+ height: 22px;
732
+ border: none;
733
+ border-radius: 4px;
734
+ background: none;
735
+ color: var(--text-dimmer);
736
+ cursor: pointer;
737
+ padding: 0;
738
+ transition: background 0.15s, color 0.15s;
739
+ }
740
+
741
+ .scheduled-msg-cancel:hover {
742
+ background: rgba(var(--overlay-rgb), 0.08);
743
+ color: var(--error);
744
+ }
745
+
746
+ .scheduled-msg-cancel .lucide {
747
+ width: 14px;
748
+ height: 14px;
749
+ }
@@ -394,3 +394,4 @@
394
394
  border-color: #fff;
395
395
  box-shadow: 0 0 0 2px var(--accent);
396
396
  }
397
+
@@ -0,0 +1,321 @@
1
+ /* ==========================================================================
2
+ User Settings — Modal dialog with left nav + right content
3
+ ========================================================================== */
4
+
5
+ /* --- Overlay container --- */
6
+ #user-settings {
7
+ position: fixed;
8
+ inset: 0;
9
+ z-index: 600;
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ padding: 20px;
14
+ }
15
+
16
+ #user-settings.hidden {
17
+ display: none;
18
+ }
19
+
20
+ /* --- Modal box --- */
21
+ .us-modal {
22
+ position: relative;
23
+ background: var(--bg-alt);
24
+ border: 1px solid var(--border);
25
+ border-radius: 14px;
26
+ width: 680px;
27
+ max-width: 100%;
28
+ height: min(560px, calc(100vh - 40px));
29
+ display: flex;
30
+ flex-direction: column;
31
+ box-shadow: 0 8px 32px rgba(var(--shadow-rgb), 0.5);
32
+ animation: usModalIn 0.2s ease-out;
33
+ overflow: hidden;
34
+ }
35
+
36
+ @keyframes usModalIn {
37
+ from { opacity: 0; transform: scale(0.95); }
38
+ to { opacity: 1; transform: scale(1); }
39
+ }
40
+
41
+ /* --- Header --- */
42
+ .us-modal-header {
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: space-between;
46
+ padding: 16px 20px;
47
+ border-bottom: 1px solid var(--border-subtle);
48
+ flex-shrink: 0;
49
+ }
50
+
51
+ .us-modal-title {
52
+ font-size: 15px;
53
+ font-weight: 700;
54
+ color: var(--text);
55
+ }
56
+
57
+ .us-modal-close {
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: center;
61
+ width: 32px;
62
+ height: 32px;
63
+ border: none;
64
+ border-radius: 8px;
65
+ background: none;
66
+ color: var(--text-muted);
67
+ cursor: pointer;
68
+ transition: background 0.15s, color 0.15s;
69
+ }
70
+
71
+ .us-modal-close .lucide {
72
+ width: 18px;
73
+ height: 18px;
74
+ }
75
+
76
+ .us-modal-close:hover {
77
+ background: rgba(var(--overlay-rgb), 0.08);
78
+ color: var(--text);
79
+ }
80
+
81
+ /* --- Body: nav + content side by side --- */
82
+ .us-modal-body {
83
+ display: flex;
84
+ flex: 1 1 auto;
85
+ min-height: 0;
86
+ overflow: hidden;
87
+ }
88
+
89
+ /* --- Left nav --- */
90
+ .us-modal-nav {
91
+ width: 172px;
92
+ flex-shrink: 0;
93
+ padding: 12px 8px;
94
+ border-right: 1px solid var(--border-subtle);
95
+ background: var(--sidebar-bg);
96
+ overflow-y: auto;
97
+ }
98
+
99
+ .us-modal-nav-items {
100
+ display: flex;
101
+ flex-direction: column;
102
+ gap: 2px;
103
+ }
104
+
105
+ .us-nav-item {
106
+ display: flex;
107
+ align-items: center;
108
+ gap: 8px;
109
+ width: 100%;
110
+ padding: 7px 10px;
111
+ border: none;
112
+ background: none;
113
+ color: var(--text-muted);
114
+ font-family: inherit;
115
+ font-size: 13px;
116
+ font-weight: 500;
117
+ cursor: pointer;
118
+ border-radius: 6px;
119
+ transition: background 0.15s, color 0.15s;
120
+ text-align: left;
121
+ }
122
+
123
+ .us-nav-item:hover {
124
+ background: rgba(var(--overlay-rgb), 0.04);
125
+ color: var(--text-secondary);
126
+ }
127
+
128
+ .us-nav-item.active {
129
+ background: rgba(var(--overlay-rgb), 0.08);
130
+ color: var(--text);
131
+ }
132
+
133
+ .us-nav-separator {
134
+ height: 1px;
135
+ background: var(--border-subtle);
136
+ margin: 6px 10px;
137
+ }
138
+
139
+ .us-nav-danger {
140
+ color: var(--error);
141
+ }
142
+
143
+ .us-nav-danger:hover {
144
+ background: var(--error-12);
145
+ color: var(--error);
146
+ }
147
+
148
+ .us-nav-danger.active {
149
+ background: var(--error-12);
150
+ color: var(--error);
151
+ }
152
+
153
+ /* --- Right content --- */
154
+ .us-modal-content {
155
+ flex: 1 1 auto;
156
+ min-width: 0;
157
+ overflow-y: auto;
158
+ padding: 20px 24px;
159
+ }
160
+
161
+ /* --- Sections --- */
162
+ .us-section {
163
+ display: none;
164
+ }
165
+
166
+ .us-section.active {
167
+ display: block;
168
+ }
169
+
170
+ .us-section h2 {
171
+ font-size: 17px;
172
+ font-weight: 700;
173
+ color: var(--text);
174
+ margin: 0 0 16px;
175
+ }
176
+
177
+ .us-section h3 {
178
+ font-size: 13px;
179
+ font-weight: 600;
180
+ color: var(--text-secondary);
181
+ text-transform: uppercase;
182
+ letter-spacing: 0.04em;
183
+ margin: 20px 0 10px;
184
+ }
185
+
186
+ /* --- Text inputs --- */
187
+ .us-text-input {
188
+ width: 100%;
189
+ max-width: 320px;
190
+ padding: 8px 12px;
191
+ border: 1px solid var(--border);
192
+ border-radius: 6px;
193
+ background: var(--bg);
194
+ color: var(--text);
195
+ font-family: inherit;
196
+ font-size: 14px;
197
+ outline: none;
198
+ transition: border-color 0.15s;
199
+ margin-top: 4px;
200
+ }
201
+
202
+ .us-text-input:focus {
203
+ border-color: var(--accent);
204
+ }
205
+
206
+ .us-text-input::placeholder {
207
+ color: var(--text-dimmer);
208
+ }
209
+
210
+ /* --- Buttons --- */
211
+ .us-btn {
212
+ padding: 8px 20px;
213
+ border: none;
214
+ border-radius: 6px;
215
+ background: var(--accent);
216
+ color: #fff;
217
+ font-family: inherit;
218
+ font-size: 14px;
219
+ font-weight: 600;
220
+ cursor: pointer;
221
+ transition: opacity 0.15s;
222
+ }
223
+
224
+ .us-btn:hover {
225
+ opacity: 0.9;
226
+ }
227
+
228
+ .us-btn:disabled {
229
+ opacity: 0.4;
230
+ cursor: not-allowed;
231
+ }
232
+
233
+ .us-btn-danger {
234
+ background: var(--error);
235
+ }
236
+
237
+ /* --- PIN row --- */
238
+ .us-pin-row {
239
+ display: flex;
240
+ align-items: center;
241
+ gap: 10px;
242
+ margin-top: 4px;
243
+ }
244
+
245
+ .us-pin-input {
246
+ max-width: 140px;
247
+ font-family: "Roboto Mono", monospace;
248
+ letter-spacing: 4px;
249
+ text-align: center;
250
+ }
251
+
252
+ .us-pin-msg {
253
+ font-size: 13px;
254
+ margin-top: 8px;
255
+ }
256
+
257
+ .us-pin-msg.hidden {
258
+ display: none;
259
+ }
260
+
261
+ .us-pin-msg-ok {
262
+ color: #3fb950;
263
+ }
264
+
265
+ .us-pin-msg-err {
266
+ color: var(--error);
267
+ }
268
+
269
+ /* --- Active state on gear button --- */
270
+ #user-settings-btn.active {
271
+ background: var(--sidebar-hover);
272
+ color: var(--text);
273
+ }
274
+
275
+ /* --- Mobile nav dropdown (hidden on desktop) --- */
276
+ #user-settings .settings-nav-dropdown {
277
+ display: none;
278
+ }
279
+
280
+ /* ========== RESPONSIVE ========== */
281
+ @media (max-width: 580px) {
282
+ .us-modal {
283
+ width: 100%;
284
+ max-height: calc(100vh - 40px);
285
+ border-radius: 12px;
286
+ }
287
+
288
+ .us-modal-body {
289
+ flex-direction: column;
290
+ }
291
+
292
+ .us-modal-nav {
293
+ width: 100%;
294
+ border-right: none;
295
+ border-bottom: 1px solid var(--border-subtle);
296
+ padding: 8px 12px;
297
+ overflow-y: visible;
298
+ }
299
+
300
+ .us-modal-nav-items {
301
+ display: none;
302
+ }
303
+
304
+ #user-settings .settings-nav-dropdown {
305
+ display: block;
306
+ width: 100%;
307
+ padding: 8px 12px;
308
+ font-family: inherit;
309
+ font-size: 14px;
310
+ font-weight: 600;
311
+ color: var(--text);
312
+ background: var(--bg-alt);
313
+ border: 1px solid var(--border);
314
+ border-radius: 8px;
315
+ cursor: pointer;
316
+ }
317
+
318
+ .us-modal-content {
319
+ padding: 16px;
320
+ }
321
+ }
@@ -717,6 +717,7 @@
717
717
  </div>
718
718
  </div>
719
719
  <div class="user-island-actions">
720
+ <button id="user-settings-btn" title="User settings"><i data-lucide="settings"></i></button>
720
721
  </div>
721
722
  </div>
722
723
 
@@ -755,6 +756,67 @@
755
756
  </div>
756
757
  </div>
757
758
 
759
+ <!-- === User Settings (modal dialog) === -->
760
+ <div id="user-settings" class="hidden">
761
+ <div class="confirm-backdrop" id="user-settings-backdrop"></div>
762
+ <div class="us-modal">
763
+ <div class="us-modal-header">
764
+ <span class="us-modal-title">User Settings</span>
765
+ <button class="us-modal-close" id="user-settings-close" title="Close"><i data-lucide="x"></i></button>
766
+ </div>
767
+ <div class="us-modal-body">
768
+ <nav class="us-modal-nav">
769
+ <select id="user-settings-nav-dropdown" class="settings-nav-dropdown">
770
+ <option value="us-account" selected>Account</option>
771
+ </select>
772
+ <div class="us-modal-nav-items">
773
+ <button class="us-nav-item active" data-section="us-account"><span>Account</span></button>
774
+ <div class="us-nav-separator"></div>
775
+ <button class="us-nav-item us-nav-danger" data-section="us-logout"><span>Log Out</span></button>
776
+ </div>
777
+ </nav>
778
+ <div class="us-modal-content">
779
+
780
+ <!-- Account section -->
781
+ <div class="us-section" data-section="us-account">
782
+ <h2>Account</h2>
783
+ <div class="settings-card">
784
+ <div class="settings-field-row">
785
+ <div>
786
+ <div class="settings-label">Username</div>
787
+ <div class="settings-value" id="us-username">-</div>
788
+ </div>
789
+ </div>
790
+ </div>
791
+ <h3>Change PIN</h3>
792
+ <div class="settings-card">
793
+ <div class="settings-field">
794
+ <label class="settings-label">New PIN (6 digits)</label>
795
+ <div class="us-pin-row">
796
+ <input type="password" class="us-text-input us-pin-input" id="us-pin-input" maxlength="6" inputmode="numeric" pattern="[0-9]*" placeholder="000000" autocomplete="off">
797
+ <button class="us-btn" id="us-pin-save" disabled>Update PIN</button>
798
+ </div>
799
+ <div class="us-pin-msg hidden" id="us-pin-msg"></div>
800
+ </div>
801
+ </div>
802
+ </div>
803
+
804
+ <!-- Log out section -->
805
+ <div class="us-section" data-section="us-logout">
806
+ <h2>Log Out</h2>
807
+ <div class="settings-card settings-card-danger">
808
+ <div class="settings-field">
809
+ <p class="settings-hint" style="margin:0 0 12px;font-size:14px;color:var(--text-secondary)">You will be signed out and redirected to the login screen.</p>
810
+ <button class="us-btn us-btn-danger" id="us-logout-btn">Log Out</button>
811
+ </div>
812
+ </div>
813
+ </div>
814
+
815
+ </div>
816
+ </div>
817
+ </div>
818
+ </div>
819
+
758
820
  <!-- === Server Settings (full-screen overlay) === -->
759
821
  <div id="server-settings" class="hidden">
760
822
  <div class="server-settings-layout">
@@ -122,6 +122,22 @@ export function sendMessage() {
122
122
  }
123
123
 
124
124
  var pastes = pendingPastes.map(function (p) { return p.text; });
125
+
126
+ // Schedule mode: queue message for after rate limit resets
127
+ if (ctx.isScheduleMode && ctx.isScheduleMode()) {
128
+ var resetsAt = ctx.getRateLimitResetsAt ? ctx.getRateLimitResetsAt() : null;
129
+ if (resetsAt && resetsAt > Date.now()) {
130
+ ctx.ws.send(JSON.stringify({ type: "schedule_message", text: text || "", resetsAt: resetsAt }));
131
+ if (ctx.exitScheduleMode) ctx.exitScheduleMode();
132
+ ctx.inputEl.value = "";
133
+ sendInputSync();
134
+ clearPendingImages();
135
+ autoResize();
136
+ ctx.inputEl.focus();
137
+ return;
138
+ }
139
+ }
140
+
125
141
  ctx.addUserMessage(text, images.length > 0 ? images : null, pastes.length > 0 ? pastes : null);
126
142
 
127
143
  var payload = { type: "message", text: text || "" };
@@ -52,7 +52,7 @@ function saveProfile() {
52
52
  }).then(function(r) { return r.json(); });
53
53
  }
54
54
 
55
- function debouncedSave() {
55
+ export function debouncedSave() {
56
56
  if (saveTimer) clearTimeout(saveTimer);
57
57
  saveTimer = setTimeout(function() {
58
58
  saveProfile();
@@ -61,7 +61,7 @@ function debouncedSave() {
61
61
  }
62
62
 
63
63
  // --- DOM updates ---
64
- function applyToIsland() {
64
+ export function applyToIsland() {
65
65
  var avatarWrap = document.querySelector('.user-island-avatar');
66
66
  var nameEl = document.querySelector('.user-island-name');
67
67
  if (!avatarWrap || !nameEl) return;
@@ -0,0 +1,185 @@
1
+ // user-settings.js — Modal dialog for user settings
2
+ // Account management and logout
3
+
4
+ import { refreshIcons } from './icons.js';
5
+ import { showToast } from './utils.js';
6
+
7
+ var ctx = null;
8
+ var settingsEl = null;
9
+ var openBtn = null;
10
+ var closeBtn = null;
11
+ var backdrop = null;
12
+ var navItems = null;
13
+ var sections = null;
14
+
15
+
16
+ export function initUserSettings(appCtx) {
17
+ ctx = appCtx;
18
+ settingsEl = document.getElementById('user-settings');
19
+ openBtn = document.getElementById('user-settings-btn');
20
+ closeBtn = document.getElementById('user-settings-close');
21
+ backdrop = document.getElementById('user-settings-backdrop');
22
+
23
+ if (!settingsEl || !openBtn) return;
24
+
25
+ navItems = settingsEl.querySelectorAll('.us-nav-item');
26
+ sections = settingsEl.querySelectorAll('.us-section');
27
+
28
+ openBtn.addEventListener('click', function () {
29
+ openUserSettings();
30
+ });
31
+
32
+ if (closeBtn) {
33
+ closeBtn.addEventListener('click', function () {
34
+ closeUserSettings();
35
+ });
36
+ }
37
+
38
+ if (backdrop) {
39
+ backdrop.addEventListener('click', function () {
40
+ closeUserSettings();
41
+ });
42
+ }
43
+
44
+ document.addEventListener('keydown', function (e) {
45
+ if (e.key === 'Escape' && isUserSettingsOpen()) {
46
+ closeUserSettings();
47
+ }
48
+ });
49
+
50
+ for (var i = 0; i < navItems.length; i++) {
51
+ navItems[i].addEventListener('click', function () {
52
+ var section = this.dataset.section;
53
+ switchSection(section);
54
+ });
55
+ }
56
+
57
+ // Mobile nav dropdown
58
+ var navDropdown = document.getElementById('user-settings-nav-dropdown');
59
+ if (navDropdown) {
60
+ navDropdown.addEventListener('change', function () {
61
+ switchSection(this.value);
62
+ });
63
+ }
64
+
65
+ // PIN save button
66
+ var pinInput = document.getElementById('us-pin-input');
67
+ var pinSave = document.getElementById('us-pin-save');
68
+ if (pinInput && pinSave) {
69
+ pinInput.addEventListener('input', function () {
70
+ pinSave.disabled = !/^\d{6}$/.test(pinInput.value);
71
+ });
72
+ pinInput.addEventListener('keydown', stopProp);
73
+ pinInput.addEventListener('keyup', stopProp);
74
+ pinInput.addEventListener('keypress', stopProp);
75
+ pinSave.addEventListener('click', function () {
76
+ savePin(pinInput.value);
77
+ });
78
+ }
79
+
80
+ // Logout button
81
+ var logoutBtn = document.getElementById('us-logout-btn');
82
+ if (logoutBtn) {
83
+ logoutBtn.addEventListener('click', function () {
84
+ fetch('/auth/logout', { method: 'POST' }).then(function () {
85
+ window.location.reload();
86
+ }).catch(function () {
87
+ window.location.reload();
88
+ });
89
+ });
90
+ }
91
+ }
92
+
93
+ function openUserSettings() {
94
+ settingsEl.classList.remove('hidden');
95
+ openBtn.classList.add('active');
96
+ refreshIcons(settingsEl);
97
+ populateAccount();
98
+ switchSection('us-account');
99
+ }
100
+
101
+ export function closeUserSettings() {
102
+ settingsEl.classList.add('hidden');
103
+ openBtn.classList.remove('active');
104
+ }
105
+
106
+ export function isUserSettingsOpen() {
107
+ return settingsEl && !settingsEl.classList.contains('hidden');
108
+ }
109
+
110
+ function switchSection(sectionName) {
111
+ for (var i = 0; i < navItems.length; i++) {
112
+ navItems[i].classList.toggle('active', navItems[i].dataset.section === sectionName);
113
+ }
114
+ for (var j = 0; j < sections.length; j++) {
115
+ sections[j].classList.toggle('active', sections[j].dataset.section === sectionName);
116
+ }
117
+ var navDropdown = document.getElementById('user-settings-nav-dropdown');
118
+ if (navDropdown) navDropdown.value = sectionName;
119
+ }
120
+
121
+ function stopProp(e) {
122
+ e.stopPropagation();
123
+ }
124
+
125
+ // --- Account population ---
126
+
127
+ function populateAccount() {
128
+ fetch('/api/profile').then(function (r) {
129
+ if (!r.ok) return null;
130
+ return r.json();
131
+ }).then(function (data) {
132
+ if (!data) return;
133
+ var usernameEl = document.getElementById('us-username');
134
+ if (usernameEl && data.username) {
135
+ usernameEl.textContent = data.username;
136
+ }
137
+ // Hide account section in single-user mode (no username)
138
+ var accountNav = settingsEl.querySelector('[data-section="us-account"]');
139
+ if (accountNav && !data.username) {
140
+ accountNav.style.display = 'none';
141
+ }
142
+ }).catch(function () {});
143
+ }
144
+
145
+ function savePin(pin) {
146
+ var pinInput = document.getElementById('us-pin-input');
147
+ var pinSave = document.getElementById('us-pin-save');
148
+ var pinMsg = document.getElementById('us-pin-msg');
149
+
150
+ pinSave.disabled = true;
151
+ pinSave.textContent = 'Saving...';
152
+
153
+ fetch('/api/user/pin', {
154
+ method: 'PUT',
155
+ headers: { 'Content-Type': 'application/json' },
156
+ body: JSON.stringify({ pin: pin }),
157
+ }).then(function (r) { return r.json(); }).then(function (data) {
158
+ if (data.ok) {
159
+ pinInput.value = '';
160
+ pinSave.textContent = 'Update PIN';
161
+ if (pinMsg) {
162
+ pinMsg.textContent = 'PIN updated successfully.';
163
+ pinMsg.className = 'us-pin-msg us-pin-msg-ok';
164
+ pinMsg.classList.remove('hidden');
165
+ }
166
+ showToast('PIN updated');
167
+ } else {
168
+ pinSave.disabled = false;
169
+ pinSave.textContent = 'Update PIN';
170
+ if (pinMsg) {
171
+ pinMsg.textContent = data.error || 'Failed to update PIN.';
172
+ pinMsg.className = 'us-pin-msg us-pin-msg-err';
173
+ pinMsg.classList.remove('hidden');
174
+ }
175
+ }
176
+ }).catch(function () {
177
+ pinSave.disabled = false;
178
+ pinSave.textContent = 'Update PIN';
179
+ if (pinMsg) {
180
+ pinMsg.textContent = 'Network error.';
181
+ pinMsg.className = 'us-pin-msg us-pin-msg-err';
182
+ pinMsg.classList.remove('hidden');
183
+ }
184
+ });
185
+ }
@@ -21,6 +21,7 @@
21
21
  @import url("css/playbook.css");
22
22
  @import url("css/stt.css");
23
23
  @import url("css/profile.css");
24
+ @import url("css/user-settings.css");
24
25
  @import url("css/admin.css");
25
26
  @import url("css/session-search.css");
26
27
  @import url("css/tooltip.css");
package/lib/sdk-bridge.js CHANGED
@@ -337,6 +337,7 @@ function createSDKBridge(opts) {
337
337
  session.activeTaskToolIds = {};
338
338
  session.taskIdMap = {};
339
339
  session.isProcessing = false;
340
+ session.rateLimitResetsAt = null; // clear on success
340
341
  onProcessingChanged();
341
342
  var lastStreamInput = session.lastStreamInputTokens || null;
342
343
  session.lastStreamInputTokens = null;
@@ -467,6 +468,10 @@ function createSDKBridge(opts) {
467
468
  utilization: info.utilization || null,
468
469
  isUsingOverage: info.isUsingOverage || false,
469
470
  });
471
+ // Track rejection for auto-continue / scheduled message support
472
+ if (info.status === "rejected" && info.resetsAt) {
473
+ session.rateLimitResetsAt = info.resetsAt * 1000;
474
+ }
470
475
  }
471
476
 
472
477
  } else if (parsed.type === "prompt_suggestion") {
@@ -975,7 +980,29 @@ function createSDKBridge(opts) {
975
980
  sm.broadcastSessionList();
976
981
  }
977
982
  cleanupSessionWorker(session);
978
- if (session.onQueryComplete) {
983
+ // Auto-continue for scheduler sessions on rate limit
984
+ var workerDidScheduleAC = false;
985
+ if (session.rateLimitResetsAt && session.rateLimitResetsAt > Date.now()
986
+ && session.onQueryComplete && !session.destroying) {
987
+ var wacDelay = session.rateLimitResetsAt - Date.now() + 3000;
988
+ var wacResetsAt = session.rateLimitResetsAt;
989
+ session.rateLimitResetsAt = null;
990
+ session.rateLimitAutoContinuePending = true;
991
+ workerDidScheduleAC = true;
992
+ console.log("[sdk-bridge] Rate limited (worker), scheduling auto-continue in " + Math.round(wacDelay / 1000) + "s for session " + session.localId);
993
+ sendAndRecord(session, { type: "auto_continue_scheduled", resetsAt: wacResetsAt });
994
+ session.autoContinueTimer = setTimeout(function () {
995
+ session.autoContinueTimer = null;
996
+ session.rateLimitAutoContinuePending = false;
997
+ if (session.destroying) return;
998
+ console.log("[sdk-bridge] Auto-continue (worker) firing for session " + session.localId);
999
+ session.isProcessing = true;
1000
+ onProcessingChanged();
1001
+ sendAndRecord(session, { type: "auto_continue_fired" });
1002
+ startQuery(session, "continue", null, session.lastLinuxUser || null);
1003
+ }, wacDelay);
1004
+ }
1005
+ if (session.onQueryComplete && !workerDidScheduleAC) {
979
1006
  try { session.onQueryComplete(session); } catch (err) {
980
1007
  console.error("[sdk-bridge] onQueryComplete error:", err.message || err);
981
1008
  }
@@ -1345,8 +1372,32 @@ function createSDKBridge(opts) {
1345
1372
  session.pendingPermissions = {};
1346
1373
  session.pendingAskUser = {};
1347
1374
  session.pendingElicitations = {};
1375
+
1376
+ // Auto-continue for scheduler (Ralph Loop) sessions on rate limit
1377
+ var didScheduleAutoContinue = false;
1378
+ if (session.rateLimitResetsAt && session.rateLimitResetsAt > Date.now()
1379
+ && session.onQueryComplete && !session.destroying) {
1380
+ var acDelay = session.rateLimitResetsAt - Date.now() + 3000;
1381
+ var acResetsAt = session.rateLimitResetsAt;
1382
+ session.rateLimitResetsAt = null;
1383
+ session.rateLimitAutoContinuePending = true;
1384
+ didScheduleAutoContinue = true;
1385
+ console.log("[sdk-bridge] Rate limited, scheduling auto-continue in " + Math.round(acDelay / 1000) + "s for session " + session.localId);
1386
+ sendAndRecord(session, { type: "auto_continue_scheduled", resetsAt: acResetsAt });
1387
+ session.autoContinueTimer = setTimeout(function () {
1388
+ session.autoContinueTimer = null;
1389
+ session.rateLimitAutoContinuePending = false;
1390
+ if (session.destroying) return;
1391
+ console.log("[sdk-bridge] Auto-continue firing for session " + session.localId);
1392
+ session.isProcessing = true;
1393
+ onProcessingChanged();
1394
+ sendAndRecord(session, { type: "auto_continue_fired" });
1395
+ startQuery(session, "continue", null, session.lastLinuxUser || null);
1396
+ }, acDelay);
1397
+ }
1398
+
1348
1399
  // Ralph Loop: notify completion so loop orchestrator can proceed
1349
- if (session.onQueryComplete) {
1400
+ if (session.onQueryComplete && !didScheduleAutoContinue) {
1350
1401
  console.log("[sdk-bridge] Calling onQueryComplete for session " + session.localId + " (title: " + (session.title || "?") + ")");
1351
1402
  try {
1352
1403
  session.onQueryComplete(session);
@@ -1392,6 +1443,8 @@ function createSDKBridge(opts) {
1392
1443
  }
1393
1444
 
1394
1445
  async function startQuery(session, text, images, linuxUser) {
1446
+ // Remember linuxUser for auto-continue after rate limit
1447
+ session.lastLinuxUser = linuxUser || null;
1395
1448
  // OS-level isolation: delegate to worker process if linuxUser is set
1396
1449
  if (linuxUser) {
1397
1450
  return startQueryViaWorker(session, text, images, linuxUser);
package/lib/server.js CHANGED
@@ -958,6 +958,29 @@ function createServer(opts) {
958
958
  return;
959
959
  }
960
960
 
961
+ // Logout
962
+ if (req.method === "POST" && fullUrl === "/auth/logout") {
963
+ if (users.isMultiUser()) {
964
+ var cookies = parseCookies(req);
965
+ var token = cookies["relay_auth_user"];
966
+ if (token && multiUserTokens[token]) {
967
+ delete multiUserTokens[token];
968
+ saveTokens();
969
+ }
970
+ res.writeHead(200, {
971
+ "Set-Cookie": "relay_auth_user=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0" + (tlsOptions ? "; Secure" : ""),
972
+ "Content-Type": "application/json",
973
+ });
974
+ } else {
975
+ res.writeHead(200, {
976
+ "Set-Cookie": "relay_auth=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0" + (tlsOptions ? "; Secure" : ""),
977
+ "Content-Type": "application/json",
978
+ });
979
+ }
980
+ res.end('{"ok":true}');
981
+ return;
982
+ }
983
+
961
984
  // Invite page (magic link)
962
985
  if (req.method === "GET" && fullUrl.indexOf("/invite/") === 0) {
963
986
  var inviteCode = fullUrl.substring("/invite/".length);
package/lib/sessions.js CHANGED
@@ -628,28 +628,40 @@ function createSessionManager(opts) {
628
628
  }
629
629
 
630
630
  function migrateSessionTitles(getSDK, migrateCwd) {
631
- var toMigrate = [];
631
+ var candidates = [];
632
632
  sessions.forEach(function(s) {
633
633
  if (s.cliSessionId && s.title && s.title !== "New Session" && s.title !== "Resumed session") {
634
- toMigrate.push({ cliSessionId: s.cliSessionId, title: s.title });
634
+ candidates.push({ cliSessionId: s.cliSessionId, title: s.title });
635
635
  }
636
636
  });
637
- if (toMigrate.length === 0) return;
637
+ if (candidates.length === 0) return;
638
638
  getSDK().then(function(sdkMod) {
639
- var chain = Promise.resolve();
640
- for (var i = 0; i < toMigrate.length; i++) {
641
- (function(item) {
642
- chain = chain.then(function() {
643
- return sdkMod.renameSession(item.cliSessionId, item.title, { dir: migrateCwd }).catch(function(e) {
644
- console.error("[session] Migration failed for " + item.cliSessionId + ":", e.message);
639
+ return sdkMod.listSessions({ dir: migrateCwd }).then(function(sdkSessions) {
640
+ var sdkTitles = {};
641
+ for (var i = 0; i < sdkSessions.length; i++) {
642
+ if (sdkSessions[i].customTitle) {
643
+ sdkTitles[sdkSessions[i].sessionId] = sdkSessions[i].customTitle;
644
+ }
645
+ }
646
+ var toMigrate = candidates.filter(function(item) {
647
+ return sdkTitles[item.cliSessionId] !== item.title;
648
+ });
649
+ if (toMigrate.length === 0) return;
650
+ var chain = Promise.resolve();
651
+ for (var j = 0; j < toMigrate.length; j++) {
652
+ (function(item) {
653
+ chain = chain.then(function() {
654
+ return sdkMod.renameSession(item.cliSessionId, item.title, { dir: migrateCwd }).catch(function(e) {
655
+ console.error("[session] Migration failed for " + item.cliSessionId + ":", e.message);
656
+ });
645
657
  });
646
- });
647
- })(toMigrate[i]);
648
- }
649
- chain.then(function() {
650
- console.log("[session] Migrated " + toMigrate.length + " session title(s) to SDK format");
651
- }).catch(function(e) {
652
- console.error("[session] Migration chain failed:", e.message || e);
658
+ })(toMigrate[j]);
659
+ }
660
+ chain.then(function() {
661
+ console.log("[session] Migrated " + toMigrate.length + " session title(s) to SDK format");
662
+ }).catch(function(e) {
663
+ console.error("[session] Migration chain failed:", e.message || e);
664
+ });
653
665
  });
654
666
  }).catch(function() {});
655
667
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.18.0",
3
+ "version": "2.19.0-beta.1",
4
4
  "description": "Web UI for Claude Code. Any device. Push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",