clay-server 2.18.1-beta.1 → 2.19.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,137 +1,109 @@
1
1
  # Clay
2
2
 
3
- <p align="center">
4
- <img src="media/phone.gif" alt="Clay on phone" width="300">
5
- </p>
6
-
7
- <h3 align="center">Turn Claude Code into a team workspace. Any device, one command.</h3>
3
+ <h3 align="center">Claude Code in your browser. Bring your team, or build one.</h3>
8
4
 
9
5
  [![npm version](https://img.shields.io/npm/v/clay-server)](https://www.npmjs.com/package/clay-server) [![npm downloads](https://img.shields.io/npm/dw/clay-server)](https://www.npmjs.com/package/clay-server) [![GitHub stars](https://img.shields.io/github/stars/chadbyte/clay)](https://github.com/chadbyte/clay) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/chadbyte/clay/blob/main/LICENSE)
10
6
 
11
- Clay gives Claude Code a browser UI that runs on any device. Use it from your phone, run it on macOS, Windows, or Linux. Invite teammates, manage multiple projects from one sidebar, and get push notifications when Claude needs you. HTTPS and push notifications work out of the box with zero config. Built on the official Claude Agent SDK, not a terminal parser. Your machine is the server. No cloud relay in between, no extra network surface.
12
-
13
- ---
14
-
15
- ## Getting Started
16
-
17
- **Requirements:** Node.js 20+, Claude Code CLI (authenticated).
7
+ Everything Claude Code does, Clay does in your browser. Plus multi-session, file browser, scheduled agents, mobile notifications, and more. Invite your team to work together in the same session, or build an AI team from scratch. **Your machine is the server.** No cloud relay, no middleman.
18
8
 
19
9
  ```bash
20
10
  npx clay-server
11
+ # Scan the QR code to connect from any device
21
12
  ```
22
13
 
23
- On first run, it walks you through port and PIN setup.
24
- Scan the QR code to connect from your phone instantly.
25
-
26
- For remote access, use a VPN like Tailscale.
27
-
28
- <p align="center">
29
- <img src="media/start.gif" alt="Clay starting from CLI" width="600">
30
- </p>
31
-
32
14
  ---
33
15
 
34
- ## Multi-session, multi-project
16
+ ## What you get
35
17
 
36
- Add a project in the browser and an agent attaches to it.
37
- Run backend, frontend, and docs simultaneously. Switch between them in the sidebar.
18
+ ### Everything the CLI does
38
19
 
39
- The server runs as a background daemon. Sessions persist even after you close the terminal.
40
-
41
- Mermaid diagrams render as diagrams. Tables render as tables.
42
- Code blocks highlight 180+ languages.
43
- Browse project files in the file browser — changes reflect live while the agent works.
20
+ Your CLI sessions, your CLAUDE.md rules, your MCP servers. **All of it works in Clay as-is.** Pick up a CLI session in the browser, or continue a browser session in the CLI. Same SDK, same tools, same results.
44
21
 
45
22
  <p align="center">
46
23
  <img src="media/split.gif" alt="split-screen workflow" width="700">
47
24
  </p>
48
25
 
49
- ---
26
+ ### Everything the CLI doesn't
50
27
 
51
- ## Multi-user
28
+ **Multiple agents, multiple projects, at the same time.** Switch between them in the sidebar. Browse project files live while the agent works, with syntax highlighting for 180+ languages. Mermaid diagrams render as diagrams. Tables render as tables.
52
29
 
53
- Invite teammates and give them access to a project. They talk to Claude Code directly in the browser no terminal, no setup on their end.
30
+ **Schedule agents with cron**, or let them run autonomously with **Ralph Loop**. Your phone buzzes when Claude needs approval, finishes a task, or hits an error. Install as a **PWA for push notifications**. Close your laptop, sessions keep running.
54
31
 
55
- Add a CLAUDE.md and the AI operates within those rules: explains technical terms in plain language, escalates risky operations to seniors, summarizes changes in simple words.
32
+ <p align="center">
33
+ <img src="media/phone.gif" alt="Clay on phone" width="280">
34
+ </p>
56
35
 
57
- If someone gets stuck, join their session to unblock them in real time. Permissions can be separated per project and per session. Real-time presence shows who's where.
36
+ ### Bring your whole team
58
37
 
59
- ---
38
+ **One API key runs the whole workspace.** Invite teammates, set permissions per person, per project, per session. A designer reports a bug in plain language. A junior dev works with guardrails. If someone gets stuck, **jump into their session** to help in real time.
60
39
 
61
- ## Mates
40
+ Add a CLAUDE.md and the AI operates within those rules: explains technical terms simply, escalates risky operations to seniors, summarizes changes in plain words. Real-time presence shows who's where.
62
41
 
63
- Build your team, even if you're solo.
42
+ ### Build your team with Mates
64
43
 
65
- Mates are AI teammates you create through conversation. Interview them, give them a name, avatar, and role. A code reviewer, a marketing lead, a writing partner. They remember how you work together and carry context across sessions.
44
+ Not *"act like a design expert."* Mates are AI teammates shaped through real conversation, trained with your context, and built to hold their own perspective. Give them a name, avatar, expertise, and working style. **They don't flatter you. They push back.**
66
45
 
67
- They sit in your sidebar next to your human teammates. DM them, bring them into projects, let them work autonomously. In a multi-user workspace, your whole team, human and AI, works in one place.
46
+ They live in your sidebar next to your human teammates. @mention them in any project session when you need their take, DM them directly, or bring multiple into the same conversation. Each Mate builds persistent knowledge over time, remembering past decisions, project context, and how you work together.
68
47
 
69
- <!-- screenshot: sidebar with Mates visible -->
48
+ #### Debate before you decide
70
49
 
71
- ---
50
+ Let your Mates challenge each other. Set up a debate. Pick a moderator and panelists, give them a topic, and let them go. You can raise your hand to interject. When it wraps up, you get opposing perspectives from every angle.
72
51
 
73
- ## Mobile & notifications
52
+ "Should we rewrite this in Rust?" "Should we delay the launch to fix onboarding?" "Should we position this as enterprise-first or PLG?" Get real opposing perspectives before you commit.
74
53
 
75
- Phone, tablet, couch. All you need is a browser.
76
- Pick up a terminal session in the browser. Continue a browser session from another device.
77
- One QR code to connect. Install as a PWA for a native-like experience.
54
+ ### Your machine, your server
78
55
 
79
- When Claude asks for approval, your phone buzzes. You also get notified on completion or error. No need to keep the browser open.
56
+ Clay runs as a daemon on your machine. **No cloud relay, no intermediary service** between your browser and your code. Data flows directly to the Anthropic API, exactly as it does from the CLI.
80
57
 
81
- <p align="center">
82
- <img src="media/push-notification.jpg" alt="push notification" width="300">
83
- </p>
58
+ PIN authentication, per-project permissions, and HTTPS are built in. For remote access, use a VPN like Tailscale.
84
59
 
85
60
  ---
86
61
 
87
- ## Automation
62
+ ## How a bug gets fixed
88
63
 
89
- The scheduler kicks off agents at set times.
90
- Have it check open issues and submit PRs every morning at 8 AM.
91
- Or compile world news and email you a digest every day.
64
+ **Without Clay:**
65
+ Designer finds a bug writes up a ticket on Asana dev asks clarifying questions → PM prioritizes → dev opens terminal, fixes it → shares a preview → QA checks → deploy
66
+ <br>*7 steps. 3 people. 2 days.*
92
67
 
93
- Take it further with Ralph Loop, an autonomous coding loop built into Clay. The agent works, commits, and a judge evaluates. If it fails, a fresh session starts over with no memory of the previous attempt. Only the code carries over. Based on [Geoffrey Huntley's Ralph Wiggum technique](https://ghuntley.com/loop/).
68
+ **With Clay:**
69
+ Designer opens Clay in the browser, describes the bug in plain language → senior joins the same session, reviews the fix together → merge
70
+ <br>*2 steps. 2 people. Minutes. The designer never touched a terminal.*
94
71
 
95
72
  ---
96
73
 
97
- ## Security & Privacy
74
+ ## How Clay compares
98
75
 
99
- Your data flows directly from your machine to the Anthropic API, exactly as it does when you use the CLI. Clay adds a browser layer on top, not a middleman.
76
+ *As of March 2026.*
100
77
 
101
- HTTPS is enabled by default with a builtin certificate. PIN authentication and per-project/session permissions are built in. For local network use, this is sufficient. For remote access, we recommend a VPN like Tailscale.
78
+ | | CLI | Remote Control | Channels | **Clay** |
79
+ |---|---|---|---|---|
80
+ | Multi-user with roles | – | – | Platform-dependent | **Accounts + RBAC** |
81
+ | AI teammates (Mates + Debates) | – | – | – | **Yes** |
82
+ | Join teammate's session | – | – | – | **Yes** |
83
+ | Persistent daemon | – | Session-based | – | **Yes** |
84
+ | Native mobile app | – | **Yes** | **Platform app** | PWA |
85
+ | Official support | **Anthropic** | **Anthropic** | **Anthropic** | Community |
102
86
 
103
- ---
87
+ Clay is a community project, not affiliated with Anthropic. Official tools receive guaranteed support and updates.
104
88
 
105
- ## Why Clay?
89
+ ---
106
90
 
107
- *As of March 2026.*
91
+ ## Getting Started
108
92
 
109
- | | CLI + Remote Control | tmux | Desktop | Cowork | **Clay** |
110
- |---|---|---|---|---|---|
111
- | Multi-user | ❌ | ✅ | ❌ | ❌ | ✅ |
112
- | Mobile / PWA | ✅ | ➖ | ➖ | ➖ | ✅ |
113
- | Push notifications | 🟠 | ❌ | 🟠 | ❌ | ✅ |
114
- | GUI | 🟠 | ❌ | ✅ | ✅ | ✅ |
115
- | Scheduler (cron) | 🟠 | ❌ | ✅ | ✅ | ✅ |
116
- | Scheduler survives logout | ❌ | ➖ | 🟠 | 🟠 | ✅ |
117
- | Join teammate's session | ❌ | 🟠 | ❌ | ❌ | ✅ |
93
+ **Requirements:** Node.js 20+, Claude Code CLI (authenticated).
118
94
 
119
- ✅ Supported · 🟠 Partial / limited · ❌ Not supported · ➖ N/A
95
+ ```bash
96
+ npx clay-server
97
+ ```
120
98
 
121
- ---
99
+ On first run, it walks you through port and PIN setup.
100
+ Scan the QR code to connect from your phone instantly.
122
101
 
123
- ## Key Features
102
+ For remote access, use a VPN like Tailscale.
124
103
 
125
- * **Mates** - AI teammates with persistent identity, context, and memory. Create through an interview, DM them, collaborate across sessions.
126
- * **Multi-user** - Accounts, invitations, per-project/session permissions, real-time presence.
127
- * **Multi-agent** - Parallel agents per project, sidebar switching.
128
- * **Push notifications** - Approval, completion, error. Native-like PWA experience.
129
- * **Scheduler** - Cron-based automatic agent execution.
130
- * **Ralph Loop** - Autonomous coding loop. The agent works, a judge evaluates, and it iterates until it passes.
131
- * **File browser** - File exploration, syntax highlighting, live reload.
132
- * **Built-in terminal** - Multi-tab terminal, mobile keyboard support.
133
- * **Session search** - Full-text search across all conversation history.
134
- * **Session persistence** - Survives crashes, restarts, and network drops.
104
+ <p align="center">
105
+ <img src="media/start.gif" alt="Clay starting from CLI" width="600">
106
+ </p>
135
107
 
136
108
  ---
137
109
 
@@ -150,20 +122,11 @@ Yes. Pick up a CLI session in the browser, or continue a browser session in the
150
122
  Yes. If your project has a CLAUDE.md, it works in Clay as-is.
151
123
 
152
124
  **"Does each teammate need their own API key?"**
153
- No. Teammates share the Claude Code session logged in on the server. If needed, you can configure per-project environment variables to use different API keys.
125
+ No. Teammates share the Claude Code session logged in on the server. You can also assign different API keys per project for billing isolation.
154
126
 
155
127
  **"Does it work with MCP servers?"**
156
128
  Yes. MCP configurations from the CLI carry over as-is.
157
129
 
158
- **"What are Mates?"**
159
- AI teammates you create through a conversation. Each Mate has a name, avatar, personality, and persistent memory. They live in your sidebar and you can DM them or bring them into projects.
160
-
161
- **"How is a Mate different from Claude Projects?"**
162
- Claude Projects save prompts and files as context. A Mate is a teammate. It has its own identity formed through an interview, remembers how you work together across sessions, and exists alongside your human teammates in the workspace. You're not organizing prompts. You're building a team.
163
-
164
- **"Can I create multiple Mates?"**
165
- Yes. Create as many as you need. A code reviewer, a writing partner, a project manager. Each one is independent.
166
-
167
130
  ---
168
131
 
169
132
  ## HTTPS
package/lib/config.js CHANGED
@@ -335,4 +335,5 @@ module.exports = {
335
335
  syncClayrc: syncClayrc,
336
336
  removeFromClayrc: removeFromClayrc,
337
337
  chmodSafe: chmodSafe,
338
+ isDevMode: _devMode,
338
339
  };
package/lib/project.js CHANGED
@@ -3486,6 +3486,56 @@ function createProjectContext(opts) {
3486
3486
  return;
3487
3487
  }
3488
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
+
3489
3539
  if (msg.type !== "message") return;
3490
3540
  if (!msg.text && (!msg.images || msg.images.length === 0) && (!msg.pastes || msg.pastes.length === 0)) return;
3491
3541
 
@@ -3498,6 +3548,13 @@ function createProjectContext(opts) {
3498
3548
  sm.saveSessionFile(session);
3499
3549
  }
3500
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
+
3501
3558
  var userMsg = { type: "user_message", text: msg.text || "" };
3502
3559
  if (msg.images && msg.images.length > 0) {
3503
3560
  userMsg.imageCount = msg.images.length;
@@ -5873,6 +5930,14 @@ function createProjectContext(opts) {
5873
5930
  // Abort all active sessions and clean up mention sessions
5874
5931
  sm.sessions.forEach(function (session) {
5875
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
+ }
5876
5941
  if (session.abortController) {
5877
5942
  try { session.abortController.abort(); } catch (e) {}
5878
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
+