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 +75 -3
- package/lib/public/app.js +168 -1
- package/lib/public/css/input.css +81 -0
- package/lib/public/css/profile.css +1 -0
- package/lib/public/css/user-settings.css +321 -0
- package/lib/public/index.html +62 -0
- package/lib/public/modules/input.js +16 -0
- package/lib/public/modules/profile.js +2 -2
- package/lib/public/modules/user-settings.js +185 -0
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +55 -2
- package/lib/server.js +23 -0
- package/lib/sessions.js +28 -16
- package/package.json +1 -1
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
|
-
|
|
445
|
-
|
|
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
|
-
|
|
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) {
|
package/lib/public/css/input.css
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
package/lib/public/index.html
CHANGED
|
@@ -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
|
+
}
|
package/lib/public/style.css
CHANGED
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
|
-
|
|
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
|
|
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
|
-
|
|
634
|
+
candidates.push({ cliSessionId: s.cliSessionId, title: s.title });
|
|
635
635
|
}
|
|
636
636
|
});
|
|
637
|
-
if (
|
|
637
|
+
if (candidates.length === 0) return;
|
|
638
638
|
getSDK().then(function(sdkMod) {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
(
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
}
|