clay-server 2.17.0-beta.1 → 2.17.0-beta.10
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 +20 -7
- package/bin/cli.js +71 -25
- package/lib/daemon.js +1 -1
- package/lib/pages.js +22 -20
- package/lib/project.js +9 -5
- package/lib/public/app.js +25 -26
- package/lib/public/css/command-palette.css +1 -1
- package/lib/public/css/mates.css +13 -14
- package/lib/public/css/menus.css +19 -0
- package/lib/public/css/overlays.css +44 -18
- package/lib/public/css/profile.css +128 -0
- package/lib/public/css/title-bar.css +0 -4
- package/lib/public/index.html +9 -5
- package/lib/public/modules/avatar.js +36 -0
- package/lib/public/modules/command-palette.js +4 -7
- package/lib/public/modules/mate-sidebar.js +5 -8
- package/lib/public/modules/profile.js +351 -24
- package/lib/public/modules/qrcode.js +23 -3
- package/lib/public/modules/sidebar.js +26 -9
- package/lib/public/sw.js +4 -1
- package/lib/server.js +224 -3
- package/lib/sessions.js +4 -4
- package/package.json +1 -1
- package/lib/themes/clay-light.json +0 -10
- package/lib/themes/clay.json +0 -10
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
[](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
10
|
|
|
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. 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.
|
|
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
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -98,7 +98,7 @@ Take it further with Ralph Loop, an autonomous coding loop built into Clay. The
|
|
|
98
98
|
|
|
99
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.
|
|
100
100
|
|
|
101
|
-
PIN authentication
|
|
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.
|
|
102
102
|
|
|
103
103
|
---
|
|
104
104
|
|
|
@@ -166,20 +166,32 @@ Yes. Create as many as you need. A code reviewer, a writing partner, a project m
|
|
|
166
166
|
|
|
167
167
|
---
|
|
168
168
|
|
|
169
|
-
## HTTPS
|
|
169
|
+
## HTTPS
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
HTTPS is enabled by default using a builtin wildcard certificate for `*.d.clay.studio`. No setup required. Available from `v2.17.0-beta.2`. Your browser connects to a URL like:
|
|
172
172
|
|
|
173
|
-
|
|
173
|
+
```
|
|
174
|
+
https://192-168-1-50.d.clay.studio:2633
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The domain resolves to your local IP. All traffic stays on your network. See [clay-dns](clay-dns/) for details on how this works.
|
|
178
|
+
|
|
179
|
+
Push notifications require HTTPS, so they work out of the box with this setup. Install Clay as a PWA on your device to receive them.
|
|
180
|
+
|
|
181
|
+
<details>
|
|
182
|
+
<summary><strong>Alternative: local certificate with mkcert</strong></summary>
|
|
183
|
+
|
|
184
|
+
If you prefer to use a locally generated certificate (e.g. air-gapped environments where DNS is unavailable):
|
|
174
185
|
|
|
175
186
|
```bash
|
|
176
187
|
brew install mkcert
|
|
177
188
|
mkcert -install
|
|
189
|
+
npx clay-server --local-cert
|
|
178
190
|
```
|
|
179
191
|
|
|
180
|
-
|
|
192
|
+
This generates a self-signed certificate trusted by your machine. The setup wizard will guide you through installing the CA on other devices.
|
|
181
193
|
|
|
182
|
-
|
|
194
|
+
</details>
|
|
183
195
|
|
|
184
196
|
---
|
|
185
197
|
|
|
@@ -192,6 +204,7 @@ npx clay-server --yes # Skip interactive prompts (use defaults)
|
|
|
192
204
|
npx clay-server -y --pin 123456
|
|
193
205
|
# Non-interactive + PIN (for scripts/CI)
|
|
194
206
|
npx clay-server --no-https # Disable HTTPS
|
|
207
|
+
npx clay-server --local-cert # Use local certificate (mkcert) instead of builtin
|
|
195
208
|
npx clay-server --no-update # Skip update check
|
|
196
209
|
npx clay-server --debug # Enable debug panel
|
|
197
210
|
npx clay-server --add . # Add current directory to running daemon
|
package/bin/cli.js
CHANGED
|
@@ -53,6 +53,7 @@ var args = process.argv.slice(2);
|
|
|
53
53
|
var port = _isDev ? 2635 : 2633;
|
|
54
54
|
var useHttps = true;
|
|
55
55
|
var forceMkcert = false;
|
|
56
|
+
var forceBuiltin = false;
|
|
56
57
|
var skipUpdate = false;
|
|
57
58
|
var debugMode = false;
|
|
58
59
|
var autoYes = false;
|
|
@@ -84,6 +85,8 @@ for (var i = 0; i < args.length; i++) {
|
|
|
84
85
|
useHttps = false;
|
|
85
86
|
} else if (args[i] === "--local-cert") {
|
|
86
87
|
forceMkcert = true;
|
|
88
|
+
} else if (args[i] === "--builtin-cert") {
|
|
89
|
+
forceBuiltin = true;
|
|
87
90
|
} else if (args[i] === "--no-update" || args[i] === "--skip-update") {
|
|
88
91
|
skipUpdate = true;
|
|
89
92
|
} else if (args[i] === "--dev") {
|
|
@@ -128,7 +131,8 @@ for (var i = 0; i < args.length; i++) {
|
|
|
128
131
|
console.log(" -p, --port <port> Port to listen on (default: 2633)");
|
|
129
132
|
console.log(" --host <address> Address to bind to (default: 0.0.0.0)");
|
|
130
133
|
console.log(" --no-https Disable HTTPS (enabled by default)");
|
|
131
|
-
console.log(" --local-cert Use local certificate (mkcert)
|
|
134
|
+
console.log(" --local-cert Use local certificate (mkcert), suppress migration notice");
|
|
135
|
+
console.log(" --builtin-cert Use builtin certificate even if mkcert is installed");
|
|
132
136
|
console.log(" --no-update Skip auto-update check on startup");
|
|
133
137
|
console.log(" --debug Enable debug panel in the web UI");
|
|
134
138
|
console.log(" -y, --yes Skip interactive prompts (accept defaults)");
|
|
@@ -599,10 +603,11 @@ function toClayStudioUrl(ip, port, protocol) {
|
|
|
599
603
|
}
|
|
600
604
|
|
|
601
605
|
function ensureCerts(ip) {
|
|
602
|
-
//
|
|
603
|
-
if (
|
|
606
|
+
// --builtin-cert: skip mkcert entirely, go straight to builtin
|
|
607
|
+
if (forceBuiltin) {
|
|
604
608
|
var builtin = getBuiltinCert();
|
|
605
609
|
if (builtin) return builtin;
|
|
610
|
+
return null;
|
|
606
611
|
}
|
|
607
612
|
|
|
608
613
|
var homeDir = os.homedir();
|
|
@@ -619,24 +624,30 @@ function ensureCerts(ip) {
|
|
|
619
624
|
fs.copyFileSync(legacyCert, certPath);
|
|
620
625
|
}
|
|
621
626
|
|
|
627
|
+
var mkcertInstalled = hasMkcert();
|
|
628
|
+
|
|
622
629
|
var caRoot = null;
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
+
if (mkcertInstalled) {
|
|
631
|
+
try {
|
|
632
|
+
caRoot = path.join(
|
|
633
|
+
execSync("mkcert -CAROOT", { encoding: "utf8" }).trim(),
|
|
634
|
+
"rootCA.pem"
|
|
635
|
+
);
|
|
636
|
+
if (!fs.existsSync(caRoot)) caRoot = null;
|
|
637
|
+
} catch (e) {}
|
|
638
|
+
}
|
|
630
639
|
|
|
631
640
|
// Collect all IPv4 addresses (Tailscale + LAN)
|
|
632
641
|
var allIPs = getAllIPs();
|
|
633
642
|
|
|
634
643
|
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
|
635
644
|
var needRegen = false;
|
|
645
|
+
var isMkcertCert = false;
|
|
636
646
|
try {
|
|
637
647
|
var certText = execFileSync("openssl", ["x509", "-in", certPath, "-text", "-noout"], { encoding: "utf8" });
|
|
638
648
|
// If cert is from an external CA (e.g. Tailscale/Let's Encrypt), never regenerate
|
|
639
649
|
if (certText.indexOf("mkcert") === -1) return { key: keyPath, cert: certPath, caRoot: caRoot };
|
|
650
|
+
isMkcertCert = true;
|
|
640
651
|
for (var i = 0; i < allIPs.length; i++) {
|
|
641
652
|
if (certText.indexOf(allIPs[i]) === -1) {
|
|
642
653
|
needRegen = true;
|
|
@@ -644,24 +655,41 @@ function ensureCerts(ip) {
|
|
|
644
655
|
}
|
|
645
656
|
}
|
|
646
657
|
} catch (e) { needRegen = true; }
|
|
647
|
-
|
|
658
|
+
// mkcert cert but mkcert uninstalled: CA is gone, cert is untrusted. Skip it.
|
|
659
|
+
if (isMkcertCert && !mkcertInstalled) needRegen = true;
|
|
660
|
+
if (!needRegen) {
|
|
661
|
+
return { key: keyPath, cert: certPath, caRoot: caRoot, mkcertDetected: mkcertInstalled && !forceMkcert };
|
|
662
|
+
}
|
|
648
663
|
}
|
|
649
664
|
|
|
650
|
-
|
|
665
|
+
// mkcert installed: generate local cert (legacy behavior)
|
|
666
|
+
if (mkcertInstalled) {
|
|
667
|
+
fs.mkdirSync(certDir, { recursive: true });
|
|
668
|
+
|
|
669
|
+
var domains = ["localhost", "127.0.0.1", "::1"];
|
|
670
|
+
for (var i = 0; i < allIPs.length; i++) {
|
|
671
|
+
if (domains.indexOf(allIPs[i]) === -1) domains.push(allIPs[i]);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
try {
|
|
675
|
+
var mkcertArgs = ["-key-file", keyPath, "-cert-file", certPath].concat(domains);
|
|
676
|
+
execFileSync("mkcert", mkcertArgs, { stdio: "pipe" });
|
|
677
|
+
} catch (err) {
|
|
678
|
+
// mkcert generation failed, fall through to builtin
|
|
679
|
+
}
|
|
651
680
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
681
|
+
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
|
682
|
+
return { key: keyPath, cert: certPath, caRoot: caRoot, mkcertDetected: !forceMkcert };
|
|
683
|
+
}
|
|
655
684
|
}
|
|
656
685
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
return null;
|
|
686
|
+
// Fallback: builtin cert (unless --local-cert forces mkcert-only)
|
|
687
|
+
if (!forceMkcert) {
|
|
688
|
+
var builtin = getBuiltinCert();
|
|
689
|
+
if (builtin) return builtin;
|
|
662
690
|
}
|
|
663
691
|
|
|
664
|
-
return
|
|
692
|
+
return null;
|
|
665
693
|
}
|
|
666
694
|
|
|
667
695
|
// --- Logo ---
|
|
@@ -1436,12 +1464,14 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
|
1436
1464
|
var ip = getLocalIP();
|
|
1437
1465
|
var hasTls = false;
|
|
1438
1466
|
var hasBuiltinCert = false;
|
|
1467
|
+
var mkcertDetected = false;
|
|
1439
1468
|
|
|
1440
1469
|
if (useHttps) {
|
|
1441
1470
|
var certPaths = ensureCerts(ip);
|
|
1442
1471
|
if (certPaths) {
|
|
1443
1472
|
hasTls = true;
|
|
1444
1473
|
if (certPaths.builtin) hasBuiltinCert = true;
|
|
1474
|
+
if (certPaths.mkcertDetected) mkcertDetected = true;
|
|
1445
1475
|
} else {
|
|
1446
1476
|
log(sym.warn + " " + a.yellow + "HTTPS unavailable" + a.reset + a.dim + " · mkcert not installed" + a.reset);
|
|
1447
1477
|
}
|
|
@@ -1516,6 +1546,7 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
|
1516
1546
|
pinHash: mode === "multi" && cliPin ? generateAuthToken(cliPin) : null,
|
|
1517
1547
|
tls: hasTls,
|
|
1518
1548
|
builtinCert: hasBuiltinCert,
|
|
1549
|
+
mkcertDetected: mkcertDetected,
|
|
1519
1550
|
debug: debugMode,
|
|
1520
1551
|
keepAwake: keepAwake,
|
|
1521
1552
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
@@ -1595,6 +1626,8 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
|
1595
1626
|
: protocol + "://" + ip + ":" + config.port;
|
|
1596
1627
|
console.log(" " + sym.done + " Daemon started (PID " + config.pid + ")");
|
|
1597
1628
|
console.log(" " + sym.done + " " + url);
|
|
1629
|
+
if (config.builtinCert) console.log(" " + sym.done + " d.clay.studio provides HTTPS certificates only. Your traffic never leaves your network.");
|
|
1630
|
+
if (config.mkcertDetected) console.log(" " + sym.warn + " Clay now ships with a builtin HTTPS certificate. To use it, pass --builtin-cert or uninstall mkcert.");
|
|
1598
1631
|
console.log(" " + sym.done + " Headless mode — exiting CLI");
|
|
1599
1632
|
process.exit(0);
|
|
1600
1633
|
return;
|
|
@@ -1611,12 +1644,14 @@ async function devMode(mode, keepAwake, existingPinHash) {
|
|
|
1611
1644
|
var ip = getLocalIP();
|
|
1612
1645
|
var hasTls = false;
|
|
1613
1646
|
var hasBuiltinCert = false;
|
|
1647
|
+
var mkcertDetected = false;
|
|
1614
1648
|
|
|
1615
1649
|
if (useHttps) {
|
|
1616
1650
|
var certPaths = ensureCerts(ip);
|
|
1617
1651
|
if (certPaths) {
|
|
1618
1652
|
hasTls = true;
|
|
1619
1653
|
if (certPaths.builtin) hasBuiltinCert = true;
|
|
1654
|
+
if (certPaths.mkcertDetected) mkcertDetected = true;
|
|
1620
1655
|
}
|
|
1621
1656
|
}
|
|
1622
1657
|
|
|
@@ -1680,6 +1715,7 @@ async function devMode(mode, keepAwake, existingPinHash) {
|
|
|
1680
1715
|
pinHash: existingPinHash || null,
|
|
1681
1716
|
tls: hasTls,
|
|
1682
1717
|
builtinCert: hasBuiltinCert,
|
|
1718
|
+
mkcertDetected: mkcertDetected,
|
|
1683
1719
|
debug: true,
|
|
1684
1720
|
keepAwake: keepAwake || false,
|
|
1685
1721
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
@@ -1829,6 +1865,7 @@ async function restartDaemonWithTLS(config, callback) {
|
|
|
1829
1865
|
return;
|
|
1830
1866
|
}
|
|
1831
1867
|
var hasBuiltinCert = !!(certPaths && certPaths.builtin);
|
|
1868
|
+
var mkcertDetected = !!(certPaths && certPaths.mkcertDetected);
|
|
1832
1869
|
|
|
1833
1870
|
// Shut down old daemon
|
|
1834
1871
|
stopDaemonWatcher();
|
|
@@ -1853,6 +1890,7 @@ async function restartDaemonWithTLS(config, callback) {
|
|
|
1853
1890
|
pinHash: config.pinHash || null,
|
|
1854
1891
|
tls: true,
|
|
1855
1892
|
builtinCert: hasBuiltinCert,
|
|
1893
|
+
mkcertDetected: mkcertDetected,
|
|
1856
1894
|
debug: config.debug || false,
|
|
1857
1895
|
keepAwake: config.keepAwake || false,
|
|
1858
1896
|
dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
|
|
@@ -1951,6 +1989,7 @@ function showMainMenu(config, ip) {
|
|
|
1951
1989
|
function afterQr() {
|
|
1952
1990
|
// Status line
|
|
1953
1991
|
log(" " + a.dim + "clay" + a.reset + " " + a.dim + "v" + currentVersion + a.reset + a.dim + " — " + url + a.reset);
|
|
1992
|
+
if (config.builtinCert) log(" " + a.dim + "d.clay.studio provides HTTPS certificates only. Your traffic never leaves your network." + a.reset);
|
|
1954
1993
|
var parts = [];
|
|
1955
1994
|
parts.push(a.bold + projs.length + a.reset + a.dim + (projs.length === 1 ? " project" : " projects"));
|
|
1956
1995
|
parts.push(a.reset + a.bold + totalSessions + a.reset + a.dim + (totalSessions === 1 ? " session" : " sessions"));
|
|
@@ -1961,6 +2000,13 @@ function showMainMenu(config, ip) {
|
|
|
1961
2000
|
log(" Press " + a.bold + "o" + a.reset + " to open in browser");
|
|
1962
2001
|
log("");
|
|
1963
2002
|
|
|
2003
|
+
if (config.mkcertDetected) {
|
|
2004
|
+
log(" " + sym.warn + " " + a.yellow + "Clay now ships with a builtin HTTPS certificate." + a.reset);
|
|
2005
|
+
log(" " + a.dim + "No more CA setup on each device." + a.reset);
|
|
2006
|
+
log(" " + a.dim + "To use it, pass --builtin-cert or uninstall mkcert." + a.reset);
|
|
2007
|
+
log("");
|
|
2008
|
+
}
|
|
2009
|
+
|
|
1964
2010
|
showMenuItems();
|
|
1965
2011
|
}
|
|
1966
2012
|
|
|
@@ -2030,9 +2076,8 @@ function showMainMenu(config, ip) {
|
|
|
2030
2076
|
}
|
|
2031
2077
|
}, {
|
|
2032
2078
|
hint: [
|
|
2033
|
-
"claude-relay has been renamed to clay-server · npx clay-server",
|
|
2034
2079
|
"Run npx clay-server in other directories to add more projects.",
|
|
2035
|
-
"★ github.com/chadbyte/
|
|
2080
|
+
"★ github.com/chadbyte/clay — Press s to star the repo",
|
|
2036
2081
|
],
|
|
2037
2082
|
keys: [
|
|
2038
2083
|
{ key: "o", onKey: function () {
|
|
@@ -2040,7 +2085,7 @@ function showMainMenu(config, ip) {
|
|
|
2040
2085
|
showMainMenu(config, ip);
|
|
2041
2086
|
}},
|
|
2042
2087
|
{ key: "s", onKey: function () {
|
|
2043
|
-
openUrl("https://github.com/chadbyte/
|
|
2088
|
+
openUrl("https://github.com/chadbyte/clay");
|
|
2044
2089
|
showMainMenu(config, ip);
|
|
2045
2090
|
}},
|
|
2046
2091
|
],
|
|
@@ -2206,7 +2251,7 @@ function showSetupGuide(config, ip, goBack) {
|
|
|
2206
2251
|
// mkcert: use HTTP onboarding server for CA install flow
|
|
2207
2252
|
var setupUrl;
|
|
2208
2253
|
if (config.builtinCert) {
|
|
2209
|
-
setupUrl = toClayStudioUrl(setupIP, config.port, "https") + "
|
|
2254
|
+
setupUrl = toClayStudioUrl(setupIP, config.port, "https") + "/pwa";
|
|
2210
2255
|
} else if (config.tls) {
|
|
2211
2256
|
setupUrl = "http://" + setupIP + ":" + (config.port + 1) + "/setup" + setupQuery;
|
|
2212
2257
|
} else {
|
|
@@ -2725,6 +2770,7 @@ var currentVersion = require("../package.json").version;
|
|
|
2725
2770
|
: protocol + "://" + ip + ":" + config.port;
|
|
2726
2771
|
console.log(" " + sym.done + " Daemon already running (PID " + config.pid + ")");
|
|
2727
2772
|
console.log(" " + sym.done + " " + url);
|
|
2773
|
+
if (config.builtinCert) console.log(" " + sym.done + " d.clay.studio provides HTTPS certificates only. Your traffic never leaves your network.");
|
|
2728
2774
|
process.exit(0);
|
|
2729
2775
|
return;
|
|
2730
2776
|
}
|
package/lib/daemon.js
CHANGED
|
@@ -94,7 +94,7 @@ var caRoot = null;
|
|
|
94
94
|
try {
|
|
95
95
|
var { execSync } = require("child_process");
|
|
96
96
|
caRoot = path.join(
|
|
97
|
-
execSync("mkcert -CAROOT", { encoding: "utf8" }).trim(),
|
|
97
|
+
execSync("mkcert -CAROOT", { encoding: "utf8", stdio: "pipe" }).trim(),
|
|
98
98
|
"rootCA.pem"
|
|
99
99
|
);
|
|
100
100
|
if (!fs.existsSync(caRoot)) caRoot = null;
|
package/lib/pages.js
CHANGED
|
@@ -48,56 +48,58 @@ function setupPageHtml(httpsUrl, httpUrl, hasCert, lanMode) {
|
|
|
48
48
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
49
49
|
<title>Setup - Clay</title>
|
|
50
50
|
<style>
|
|
51
|
+
:root{--s-bg:#282a36;--s-text:#f8f8f2;--s-accent:#ffb86c;--s-muted:#6272a4;--s-border:#44475a;--s-dimmer:#6272a4;--s-success:#50fa7b;--s-accent-15:rgba(255,184,108,0.15);--s-success-10:rgba(80,250,123,0.1);--s-success-15:rgba(80,250,123,0.15);--s-accent-06:rgba(255,184,108,0.06);--s-muted-06:rgba(98,114,164,0.06);--s-muted-15:rgba(98,114,164,0.15)}
|
|
52
|
+
@media(prefers-color-scheme:light){:root{--s-bg:#FAFAFA;--s-text:#5C6166;--s-accent:#FA8D3E;--s-muted:#A0A6AC;--s-border:#D2D4D8;--s-dimmer:#8A9199;--s-success:#6CBF49;--s-accent-15:rgba(250,141,62,0.15);--s-success-10:rgba(108,191,73,0.1);--s-success-15:rgba(108,191,73,0.15);--s-accent-06:rgba(250,141,62,0.06);--s-muted-06:rgba(160,166,172,0.06);--s-muted-15:rgba(160,166,172,0.15)}}
|
|
51
53
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
52
|
-
body{background
|
|
54
|
+
body{background:var(--s-bg);color:var(--s-text);font-family:system-ui,-apple-system,sans-serif;min-height:100dvh;display:flex;justify-content:center;padding:env(safe-area-inset-top,0) 20px 40px}
|
|
53
55
|
.c{max-width:480px;width:100%;padding-top:40px}
|
|
54
|
-
h1{color
|
|
55
|
-
.subtitle{text-align:center;color
|
|
56
|
+
h1{color:var(--s-accent);font-size:22px;margin:0 0 4px;text-align:center}
|
|
57
|
+
.subtitle{text-align:center;color:var(--s-muted);font-size:13px;margin-bottom:28px}
|
|
56
58
|
|
|
57
59
|
/* Steps indicator */
|
|
58
60
|
.steps-bar{display:flex;gap:6px;margin-bottom:32px}
|
|
59
|
-
.steps-bar .pip{flex:1;height:3px;border-radius:2px;background
|
|
60
|
-
.steps-bar .pip.done{background
|
|
61
|
-
.steps-bar .pip.active{background
|
|
61
|
+
.steps-bar .pip{flex:1;height:3px;border-radius:2px;background:var(--s-border);transition:background 0.3s}
|
|
62
|
+
.steps-bar .pip.done{background:var(--s-success)}
|
|
63
|
+
.steps-bar .pip.active{background:var(--s-accent)}
|
|
62
64
|
|
|
63
65
|
/* Step card */
|
|
64
66
|
.step-card{display:none;animation:fadeIn 0.25s ease}
|
|
65
67
|
.step-card.active{display:block}
|
|
66
68
|
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
|
67
69
|
|
|
68
|
-
.step-label{font-size:11px;text-transform:uppercase;letter-spacing:1px;color
|
|
70
|
+
.step-label{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--s-accent);font-weight:600;margin-bottom:8px}
|
|
69
71
|
.step-title{font-size:18px;font-weight:600;margin-bottom:6px}
|
|
70
|
-
.step-desc{font-size:14px;line-height:1.6;color
|
|
72
|
+
.step-desc{font-size:14px;line-height:1.6;color:var(--s-muted);margin-bottom:20px}
|
|
71
73
|
|
|
72
74
|
.instruction{display:flex;gap:12px;margin-bottom:16px}
|
|
73
|
-
.inst-num{width:24px;height:24px;border-radius:50%;background:
|
|
75
|
+
.inst-num{width:24px;height:24px;border-radius:50%;background:var(--s-accent-15);color:var(--s-accent);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px;flex-shrink:0;margin-top:1px}
|
|
74
76
|
.inst-text{font-size:14px;line-height:1.6}
|
|
75
|
-
.inst-text .note{font-size:12px;color
|
|
77
|
+
.inst-text .note{font-size:12px;color:var(--s-dimmer);margin-top:4px}
|
|
76
78
|
|
|
77
|
-
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;background
|
|
79
|
+
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;background:var(--s-accent);color:#fff;text-decoration:none;padding:12px 24px;border-radius:12px;font-weight:600;font-size:14px;margin:4px 0;border:none;cursor:pointer;font-family:inherit;transition:opacity 0.15s}
|
|
78
80
|
.btn:hover{opacity:0.9}
|
|
79
|
-
.btn.outline{background:transparent;border:1.5px solid
|
|
80
|
-
.btn.outline:hover{border-color
|
|
81
|
-
.btn.success{background
|
|
81
|
+
.btn.outline{background:transparent;border:1.5px solid var(--s-border);color:var(--s-text)}
|
|
82
|
+
.btn.outline:hover{border-color:var(--s-dimmer)}
|
|
83
|
+
.btn.success{background:var(--s-success)}
|
|
82
84
|
.btn:disabled{opacity:0.4;cursor:default}
|
|
83
85
|
|
|
84
86
|
.btn-row{display:flex;gap:8px;margin-top:20px}
|
|
85
87
|
.btn-row .btn{flex:1}
|
|
86
88
|
|
|
87
89
|
.check-status{display:flex;align-items:center;gap:8px;padding:12px 16px;border-radius:10px;font-size:13px;margin:16px 0}
|
|
88
|
-
.check-status.ok{background:
|
|
89
|
-
.check-status.warn{background:
|
|
90
|
-
.check-status.pending{background:
|
|
90
|
+
.check-status.ok{background:var(--s-success-10);color:var(--s-success);border:1px solid var(--s-success-15)}
|
|
91
|
+
.check-status.warn{background:var(--s-accent-06);border:1px solid var(--s-accent-15);color:var(--s-accent)}
|
|
92
|
+
.check-status.pending{background:var(--s-muted-06);border:1px solid var(--s-muted-15);color:var(--s-muted)}
|
|
91
93
|
|
|
92
94
|
.platform-ios,.platform-android,.platform-desktop{display:none}
|
|
93
95
|
|
|
94
96
|
.done-card{text-align:center;padding:40px 0}
|
|
95
97
|
.done-icon{font-size:48px;margin-bottom:16px}
|
|
96
98
|
.done-title{font-size:20px;font-weight:600;margin-bottom:8px}
|
|
97
|
-
.done-desc{font-size:14px;color
|
|
99
|
+
.done-desc{font-size:14px;color:var(--s-muted);margin-bottom:24px}
|
|
98
100
|
|
|
99
|
-
.skip-link{display:block;text-align:center;color
|
|
100
|
-
.skip-link:hover{color
|
|
101
|
+
.skip-link{display:block;text-align:center;color:var(--s-dimmer);font-size:13px;text-decoration:none;margin-top:12px;cursor:pointer;border:none;background:none;font-family:inherit}
|
|
102
|
+
.skip-link:hover{color:var(--s-muted)}
|
|
101
103
|
</style></head><body>
|
|
102
104
|
<div class="c">
|
|
103
105
|
<h1>Clay</h1>
|
package/lib/project.js
CHANGED
|
@@ -246,6 +246,7 @@ function createProjectContext(opts) {
|
|
|
246
246
|
username: u.username,
|
|
247
247
|
avatarStyle: p.avatarStyle || "thumbs",
|
|
248
248
|
avatarSeed: p.avatarSeed || u.username,
|
|
249
|
+
avatarCustom: p.avatarCustom || "",
|
|
249
250
|
});
|
|
250
251
|
}
|
|
251
252
|
msg.users = userList;
|
|
@@ -1646,11 +1647,11 @@ function createProjectContext(opts) {
|
|
|
1646
1647
|
var switchTarget = sm.sessions.get(msg.id);
|
|
1647
1648
|
if (!usersModule.canAccessSession(ws._clayUser.id, switchTarget, { visibility: "public" })) return;
|
|
1648
1649
|
ws._clayActiveSession = msg.id;
|
|
1649
|
-
sm.switchSession(msg.id, ws);
|
|
1650
|
+
sm.switchSession(msg.id, ws, hydrateImageRefs);
|
|
1650
1651
|
broadcastPresence();
|
|
1651
1652
|
} else {
|
|
1652
1653
|
ws._clayActiveSession = msg.id;
|
|
1653
|
-
sm.switchSession(msg.id, ws);
|
|
1654
|
+
sm.switchSession(msg.id, ws, hydrateImageRefs);
|
|
1654
1655
|
}
|
|
1655
1656
|
}
|
|
1656
1657
|
return;
|
|
@@ -2005,7 +2006,7 @@ function createProjectContext(opts) {
|
|
|
2005
2006
|
onProcessingChanged();
|
|
2006
2007
|
|
|
2007
2008
|
sm.saveSessionFile(session);
|
|
2008
|
-
sm.switchSession(session.localId, ws);
|
|
2009
|
+
sm.switchSession(session.localId, ws, hydrateImageRefs);
|
|
2009
2010
|
sm.sendAndRecord(session, { type: "rewind_complete", mode: mode });
|
|
2010
2011
|
sm.broadcastSessionList();
|
|
2011
2012
|
} catch (err) {
|
|
@@ -2076,6 +2077,7 @@ function createProjectContext(opts) {
|
|
|
2076
2077
|
displayName: p.name || u.displayName || u.username,
|
|
2077
2078
|
avatarStyle: p.avatarStyle || "thumbs",
|
|
2078
2079
|
avatarSeed: p.avatarSeed || u.username,
|
|
2080
|
+
avatarCustom: p.avatarCustom || "",
|
|
2079
2081
|
};
|
|
2080
2082
|
if (msg.type === "cursor_move") {
|
|
2081
2083
|
cursorMsg.turn = msg.turn;
|
|
@@ -3162,7 +3164,7 @@ function createProjectContext(opts) {
|
|
|
3162
3164
|
judgeCraftSession.ralphCraftingMode = true;
|
|
3163
3165
|
judgeCraftSession.loop = { active: true, iteration: 0, role: "crafting", loopId: newLoopId, name: craftName, source: recordSource, startedAt: loopState.startedAt };
|
|
3164
3166
|
sm.saveSessionFile(judgeCraftSession);
|
|
3165
|
-
sm.switchSession(judgeCraftSession.localId);
|
|
3167
|
+
sm.switchSession(judgeCraftSession.localId, null, hydrateImageRefs);
|
|
3166
3168
|
loopState.craftingSessionId = judgeCraftSession.localId;
|
|
3167
3169
|
|
|
3168
3170
|
loopRegistry.updateRecord(newLoopId, { craftingSessionId: judgeCraftSession.localId });
|
|
@@ -3206,7 +3208,7 @@ function createProjectContext(opts) {
|
|
|
3206
3208
|
craftingSession.ralphCraftingMode = true;
|
|
3207
3209
|
craftingSession.loop = { active: true, iteration: 0, role: "crafting", loopId: newLoopId, name: craftName, source: recordSource, startedAt: loopState.startedAt };
|
|
3208
3210
|
sm.saveSessionFile(craftingSession);
|
|
3209
|
-
sm.switchSession(craftingSession.localId);
|
|
3211
|
+
sm.switchSession(craftingSession.localId, null, hydrateImageRefs);
|
|
3210
3212
|
loopState.craftingSessionId = craftingSession.localId;
|
|
3211
3213
|
|
|
3212
3214
|
// Store crafting session ID in the registry record
|
|
@@ -3504,6 +3506,7 @@ function createProjectContext(opts) {
|
|
|
3504
3506
|
username: u.username,
|
|
3505
3507
|
avatarStyle: p.avatarStyle || "thumbs",
|
|
3506
3508
|
avatarSeed: p.avatarSeed || u.username,
|
|
3509
|
+
avatarCustom: p.avatarCustom || "",
|
|
3507
3510
|
});
|
|
3508
3511
|
}
|
|
3509
3512
|
send({ type: "session_presence", presence: presence });
|
|
@@ -4124,6 +4127,7 @@ function createProjectContext(opts) {
|
|
|
4124
4127
|
username: u.username,
|
|
4125
4128
|
avatarStyle: p.avatarStyle || "thumbs",
|
|
4126
4129
|
avatarSeed: p.avatarSeed || u.username,
|
|
4130
|
+
avatarCustom: p.avatarCustom || "",
|
|
4127
4131
|
});
|
|
4128
4132
|
}
|
|
4129
4133
|
status.onlineUsers = onlineUsers;
|