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 +55 -92
- package/lib/config.js +1 -0
- package/lib/project.js +65 -0
- 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 +28 -3
- package/lib/sessions.js +28 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,137 +1,109 @@
|
|
|
1
1
|
# Clay
|
|
2
2
|
|
|
3
|
-
<
|
|
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
|
[](https://www.npmjs.com/package/clay-server) [](https://www.npmjs.com/package/clay-server) [](https://github.com/chadbyte/clay) [](https://github.com/chadbyte/clay/blob/main/LICENSE)
|
|
10
6
|
|
|
11
|
-
|
|
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
|
-
##
|
|
16
|
+
## What you get
|
|
35
17
|
|
|
36
|
-
|
|
37
|
-
Run backend, frontend, and docs simultaneously. Switch between them in the sidebar.
|
|
18
|
+
### Everything the CLI does
|
|
38
19
|
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
+
<p align="center">
|
|
33
|
+
<img src="media/phone.gif" alt="Clay on phone" width="280">
|
|
34
|
+
</p>
|
|
56
35
|
|
|
57
|
-
|
|
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
|
-
|
|
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
|
|
42
|
+
### Build your team with Mates
|
|
64
43
|
|
|
65
|
-
Mates are AI teammates
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
62
|
+
## How a bug gets fixed
|
|
88
63
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
74
|
+
## How Clay compares
|
|
98
75
|
|
|
99
|
-
|
|
76
|
+
*As of March 2026.*
|
|
100
77
|
|
|
101
|
-
|
|
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
|
-
|
|
89
|
+
---
|
|
106
90
|
|
|
107
|
-
|
|
91
|
+
## Getting Started
|
|
108
92
|
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
+
For remote access, use a VPN like Tailscale.
|
|
124
103
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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.
|
|
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
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
|
-
|
|
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
|
+
}
|