clay-server 2.16.0 → 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.
@@ -0,0 +1,47 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDgjCCAwigAwIBAgISBmtBLGUclrEfhwU9evzgyDCQMAoGCCqGSM49BAMDMDIx
3
+ CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
4
+ ODAeFw0yNjAzMjMwOTQ0MDJaFw0yNjA2MjEwOTQ0MDFaMBoxGDAWBgNVBAMMDyou
5
+ ZC5jbGF5LnN0dWRpbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEhAD1htqRg8
6
+ n7evflBZ1X7UaCeqBGcvG/MNtlAKd1VVfVGFuanyUjksV9++R1EuKLhEPM3loL/3
7
+ Gz8+XEewGw6jggIUMIICEDAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYB
8
+ BQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU8Anzwuqwtujb+h3o/CnbJu5Z
9
+ +W8wHwYDVR0jBBgwFoAUjw0TovYuftFQbDMYOF1ZjiNykcowMgYIKwYBBQUHAQEE
10
+ JjAkMCIGCCsGAQUFBzAChhZodHRwOi8vZTguaS5sZW5jci5vcmcvMBoGA1UdEQQT
11
+ MBGCDyouZC5jbGF5LnN0dWRpbzATBgNVHSAEDDAKMAgGBmeBDAECATAtBgNVHR8E
12
+ JjAkMCKgIKAehhxodHRwOi8vZTguYy5sZW5jci5vcmcvMTcuY3JsMIIBBQYKKwYB
13
+ BAHWeQIEAgSB9gSB8wDxAHcAFoMtq/CpJQ8P8DqlRf/Iv8gj0IdL9gQpJ/jnHzMT
14
+ 9foAAAGdGkoIlgAABAMASDBGAiEAhJFJwEIag1Bzt0WtYgMzLdJn/k+Is2RukdDo
15
+ G5sXpyMCIQCQOX9nOoaVIXxF1KXiavbAY5QIyJRuvK7Fn6WeL58YQQB2AMs49xWJ
16
+ fIShRF9bwd37yW7ymlnNRwppBYWwyxTDFFjnAAABnRpKCJgAAAQDAEcwRQIhAI98
17
+ cmflulGQJMfD10jbstVwodGpzl5licg6FTxcYachAiBZV1cZnPfasTzcteXyjCuz
18
+ c1wayYAtch+0soAWvBKZRzAKBggqhkjOPQQDAwNoADBlAjAwJO4ti4AJJTtMxYsr
19
+ Jf5052oDrD2POtoiPksruQVVacsq0T/9VYVX+X2vElCrxFwCMQDcSToBRWGpv/G3
20
+ JBpbEAB1qhk1Z9lYPQKH6gRvtp35XJWY0PucGRWgQUrXuZcxGlA=
21
+ -----END CERTIFICATE-----
22
+ -----BEGIN CERTIFICATE-----
23
+ MIIEVjCCAj6gAwIBAgIQY5WTY8JOcIJxWRi/w9ftVjANBgkqhkiG9w0BAQsFADBP
24
+ MQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFy
25
+ Y2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTAeFw0yNDAzMTMwMDAwMDBa
26
+ Fw0yNzAzMTIyMzU5NTlaMDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBF
27
+ bmNyeXB0MQswCQYDVQQDEwJFODB2MBAGByqGSM49AgEGBSuBBAAiA2IABNFl8l7c
28
+ S7QMApzSsvru6WyrOq44ofTUOTIzxULUzDMMNMchIJBwXOhiLxxxs0LXeb5GDcHb
29
+ R6EToMffgSZjO9SNHfY9gjMy9vQr5/WWOrQTZxh7az6NSNnq3u2ubT6HTKOB+DCB
30
+ 9TAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMB
31
+ MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFI8NE6L2Ln7RUGwzGDhdWY4j
32
+ cpHKMB8GA1UdIwQYMBaAFHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEB
33
+ BCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzATBgNVHSAE
34
+ DDAKMAgGBmeBDAECATAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8veDEuYy5sZW5j
35
+ ci5vcmcvMA0GCSqGSIb3DQEBCwUAA4ICAQBnE0hGINKsCYWi0Xx1ygxD5qihEjZ0
36
+ RI3tTZz1wuATH3ZwYPIp97kWEayanD1j0cDhIYzy4CkDo2jB8D5t0a6zZWzlr98d
37
+ AQFNh8uKJkIHdLShy+nUyeZxc5bNeMp1Lu0gSzE4McqfmNMvIpeiwWSYO9w82Ob8
38
+ otvXcO2JUYi3svHIWRm3+707DUbL51XMcY2iZdlCq4Wa9nbuk3WTU4gr6LY8MzVA
39
+ aDQG2+4U3eJ6qUF10bBnR1uuVyDYs9RhrwucRVnfuDj29CMLTsplM5f5wSV5hUpm
40
+ Uwp/vV7M4w4aGunt74koX71n4EdagCsL/Yk5+mAQU0+tue0JOfAV/R6t1k+Xk9s2
41
+ HMQFeoxppfzAVC04FdG9M+AC2JWxmFSt6BCuh3CEey3fE52Qrj9YM75rtvIjsm/1
42
+ Hl+u//Wqxnu1ZQ4jpa+VpuZiGOlWrqSP9eogdOhCGisnyewWJwRQOqK16wiGyZeR
43
+ xs/Bekw65vwSIaVkBruPiTfMOo0Zh4gVa8/qJgMbJbyrwwG97z/PRgmLKCDl8z3d
44
+ tA0Z7qq7fta0Gl24uyuB05dqI5J1LvAzKuWdIjT1tP8qCoxSE/xpix8hX2dt3h+/
45
+ jujUgFPFZ0EVZ0xSyBNRF3MboGZnYXFUxpNjTWPKpagDHJQmqrAcDmWJnMsFY3jS
46
+ u1igv3OefnWjSQ==
47
+ -----END CERTIFICATE-----
@@ -0,0 +1,5 @@
1
+ -----BEGIN PRIVATE KEY-----
2
+ MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgtwBpQgcr1N5kEPEv
3
+ Rz5JCBMpDQHD8U44HlzB9bLvQAihRANCAARIQA9YbakYPJ+3r35QWdV+1GgnqgRn
4
+ LxvzDbZQCndVVX1Rhbmp8lI5LFffvkdRLii4RDzN5aC/9xs/PlxHsBsO
5
+ -----END PRIVATE KEY-----
package/lib/daemon.js CHANGED
@@ -60,10 +60,26 @@ if (config.osUsers) {
60
60
  // --- TLS ---
61
61
  var tlsOptions = null;
62
62
  if (config.tls) {
63
+ // 1. Check builtin cert (shipped with package)
64
+ var builtinKeyPath = path.join(__dirname, "certs", "privkey.pem");
65
+ var builtinCertPath = path.join(__dirname, "certs", "fullchain.pem");
66
+
67
+ // 2. User cert (mkcert, etc.)
63
68
  var os = require("os");
64
69
  var certDir = path.join(process.env.CLAY_HOME || process.env.CLAUDE_RELAY_HOME || path.join(os.homedir(), ".clay"), "certs");
65
- var keyPath = path.join(certDir, "key.pem");
66
- var certPath = path.join(certDir, "cert.pem");
70
+ var userKeyPath = path.join(certDir, "key.pem");
71
+ var userCertPath = path.join(certDir, "cert.pem");
72
+
73
+ var keyPath, certPath;
74
+ if (config.builtinCert !== false && fs.existsSync(builtinKeyPath) && fs.existsSync(builtinCertPath)) {
75
+ keyPath = builtinKeyPath;
76
+ certPath = builtinCertPath;
77
+ config.builtinCert = true;
78
+ } else {
79
+ keyPath = userKeyPath;
80
+ certPath = userCertPath;
81
+ }
82
+
67
83
  try {
68
84
  tlsOptions = {
69
85
  key: fs.readFileSync(keyPath),
@@ -78,7 +94,7 @@ var caRoot = null;
78
94
  try {
79
95
  var { execSync } = require("child_process");
80
96
  caRoot = path.join(
81
- execSync("mkcert -CAROOT", { encoding: "utf8" }).trim(),
97
+ execSync("mkcert -CAROOT", { encoding: "utf8", stdio: "pipe" }).trim(),
82
98
  "rootCA.pem"
83
99
  );
84
100
  if (!fs.existsSync(caRoot)) caRoot = null;
@@ -119,6 +135,7 @@ var listenHost = config.host || "0.0.0.0";
119
135
  var relay = createServer({
120
136
  tlsOptions: tlsOptions,
121
137
  caPath: caRoot,
138
+ builtinCert: config.builtinCert || false,
122
139
  pinHash: config.pinHash || null,
123
140
  port: config.port,
124
141
  debug: config.debug || false,
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:#2F2E2B;color:#E8E5DE;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}
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:#DA7756;font-size:22px;margin:0 0 4px;text-align:center}
55
- .subtitle{text-align:center;color:#908B81;font-size:13px;margin-bottom:28px}
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:#3E3C37;transition:background 0.3s}
60
- .steps-bar .pip.done{background:#57AB5A}
61
- .steps-bar .pip.active{background:#DA7756}
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:#DA7756;font-weight:600;margin-bottom:8px}
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:#908B81;margin-bottom:20px}
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:rgba(218,119,86,0.15);color:#DA7756;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px;flex-shrink:0;margin-top:1px}
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:#6D6860;margin-top:4px}
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:#DA7756;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}
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 #3E3C37;color:#E8E5DE}
80
- .btn.outline:hover{border-color:#6D6860}
81
- .btn.success{background:#57AB5A}
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:rgba(87,171,90,0.1);color:#57AB5A;border:1px solid rgba(87,171,90,0.15)}
89
- .check-status.warn{background:rgba(218,119,86,0.06);border:1px solid rgba(218,119,86,0.15);color:#DA7756}
90
- .check-status.pending{background:rgba(144,139,129,0.06);border:1px solid rgba(144,139,129,0.15);color:#908B81}
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:#908B81;margin-bottom:24px}
99
+ .done-desc{font-size:14px;color:var(--s-muted);margin-bottom:24px}
98
100
 
99
- .skip-link{display:block;text-align:center;color:#6D6860;font-size:13px;text-decoration:none;margin-top:12px;cursor:pointer;border:none;background:none;font-family:inherit}
100
- .skip-link:hover{color:#908B81}
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;
package/lib/public/app.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { avatarUrl, userAvatarUrl, mateAvatarUrl } from './modules/avatar.js';
1
2
  import { showToast, copyToClipboard, escapeHtml } from './modules/utils.js';
2
3
  import { refreshIcons, iconHtml, randomThinkingVerb } from './modules/icons.js';
3
4
  import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, closeMermaidModal, parseEmojis } from './modules/markdown.js';
@@ -7,7 +8,7 @@ import { initMateKnowledge, requestKnowledgeList, renderKnowledgeList, handleKno
7
8
  import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid, addRewindButton } from './modules/rewind.js';
8
9
  import { initNotifications, showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './modules/notifications.js';
9
10
  import { initInput, clearPendingImages, handleInputSync, autoResize, builtinCommands, sendMessage } from './modules/input.js';
10
- import { initQrCode } from './modules/qrcode.js';
11
+ import { initQrCode, triggerShare } from './modules/qrcode.js';
11
12
  import { initFileBrowser, loadRootDirectory, refreshTree, handleFsList, handleFsRead, handleDirChanged, refreshIfOpen, handleFileChanged, handleFileHistory, handleGitDiff, handleFileAt, getPendingNavigate, closeFileViewer, resetFileBrowser } from './modules/filebrowser.js';
12
13
  import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermExited, handleTermClosed, sendTerminalCommand } from './modules/terminal.js';
13
14
  import { initStickyNotes, handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted, openArchive, closeArchive, isArchiveOpen, hideNotes, showNotes, isNotesVisible } from './modules/sticky-notes.js';
@@ -651,20 +652,20 @@ import { initLongPress } from './modules/longpress.js';
651
652
  document.body.classList.add("mate-dm-active");
652
653
  // Build mate avatar URL for DM bubble injection
653
654
  var mp = targetUser.profile || {};
654
- var mateAvatarUrl = "https://api.dicebear.com/9.x/" + (mp.avatarStyle || targetUser.avatarStyle || "bottts") + "/svg?seed=" + encodeURIComponent(mp.avatarSeed || targetUser.avatarSeed || targetUser.id) + "&size=36";
655
+ var mateAvUrlDm = mateAvatarUrl(targetUser, 36);
655
656
  var myUser = cachedAllUsers.find(function (u) { return u.id === myUserId; });
656
657
  if (!myUser) {
657
658
  try { var cached = JSON.parse(localStorage.getItem("clay_my_user") || "null"); if (cached) myUser = cached; } catch(e) {}
658
659
  }
659
- var myAvatarUrl = "https://api.dicebear.com/9.x/" + ((myUser && myUser.avatarStyle) || "thumbs") + "/svg?seed=" + encodeURIComponent((myUser && (myUser.avatarSeed || myUser.username)) || myUserId) + "&size=36";
660
+ var myAvatarUrl = userAvatarUrl(myUser || { id: myUserId }, 36);
660
661
  var myDisplayName = (myUser && myUser.displayName) || "";
661
- document.body.dataset.mateAvatarUrl = mateAvatarUrl;
662
+ document.body.dataset.mateAvatarUrl = mateAvUrlDm;
662
663
  document.body.dataset.mateName = mp.displayName || targetUser.displayName || targetUser.name || "";
663
664
  document.body.dataset.myAvatarUrl = myAvatarUrl;
664
665
  document.body.dataset.myDisplayName = myDisplayName;
665
666
  // Cache my info for restore after hard refresh
666
667
  if (myUser) {
667
- try { localStorage.setItem("clay_my_user", JSON.stringify({ displayName: myUser.displayName, avatarStyle: myUser.avatarStyle, avatarSeed: myUser.avatarSeed, username: myUser.username })); } catch(e) {}
668
+ try { localStorage.setItem("clay_my_user", JSON.stringify({ displayName: myUser.displayName, avatarStyle: myUser.avatarStyle, avatarSeed: myUser.avatarSeed, avatarCustom: myUser.avatarCustom, username: myUser.username })); } catch(e) {}
668
669
  }
669
670
  var titleBarContent = document.querySelector(".title-bar-content");
670
671
  if (titleBarContent) {
@@ -677,7 +678,7 @@ import { initLongPress } from './modules/longpress.js';
677
678
  var mateMobileAvatar = document.getElementById("mate-mobile-avatar");
678
679
  var mateMobileName = document.getElementById("mate-mobile-name");
679
680
  var mateMobileStatus = document.getElementById("mate-mobile-status");
680
- if (mateMobileAvatar) mateMobileAvatar.src = mateAvatarUrl;
681
+ if (mateMobileAvatar) mateMobileAvatar.src = mateAvUrlDm;
681
682
  if (mateMobileName) mateMobileName.textContent = (mp.displayName || targetUser.displayName || targetUser.name || "");
682
683
  if (mateMobileStatus) mateMobileStatus.textContent = "online";
683
684
  mateMobileTitle.classList.remove("hidden");
@@ -686,7 +687,7 @@ import { initLongPress } from './modules/longpress.js';
686
687
  id: targetUser.id,
687
688
  displayName: mp.displayName || targetUser.displayName || targetUser.name || "",
688
689
  description: mp.description || targetUser.description || "",
689
- avatarUrl: mateAvatarUrl,
690
+ avatarUrl: mateAvUrlDm,
690
691
  color: mateColor
691
692
  });
692
693
  }
@@ -723,7 +724,7 @@ import { initLongPress } from './modules/longpress.js';
723
724
  } else {
724
725
  if (dmHeaderBar) dmHeaderBar.style.display = "";
725
726
  if (dmAvatar) {
726
- dmAvatar.src = "https://api.dicebear.com/9.x/" + (targetUser.avatarStyle || "thumbs") + "/svg?seed=" + encodeURIComponent(targetUser.avatarSeed || targetUser.username) + "&size=28";
727
+ dmAvatar.src = userAvatarUrl(targetUser, 28);
727
728
  }
728
729
  if (dmName) dmName.textContent = targetUser.displayName;
729
730
  if (dmHeaderBar && targetUser.avatarColor) {
@@ -1165,11 +1166,9 @@ import { initLongPress } from './modules/longpress.js';
1165
1166
  avatar.className = "dm-msg-avatar";
1166
1167
  if (isMe) {
1167
1168
  var myUser = cachedAllUsers.find(function (u) { return u.id === myUserId; });
1168
- var myStyle = myUser ? myUser.avatarStyle : "thumbs";
1169
- var mySeed = myUser ? (myUser.avatarSeed || myUser.username) : myUserId;
1170
- avatar.src = "https://api.dicebear.com/9.x/" + (myStyle || "thumbs") + "/svg?seed=" + encodeURIComponent(mySeed) + "&size=36";
1169
+ avatar.src = userAvatarUrl(myUser || { id: myUserId }, 36);
1171
1170
  } else if (dmTargetUser) {
1172
- avatar.src = "https://api.dicebear.com/9.x/" + (dmTargetUser.avatarStyle || "thumbs") + "/svg?seed=" + encodeURIComponent(dmTargetUser.avatarSeed || dmTargetUser.username) + "&size=36";
1171
+ avatar.src = userAvatarUrl(dmTargetUser, 36);
1173
1172
  }
1174
1173
  div.appendChild(avatar);
1175
1174
 
@@ -1224,7 +1223,7 @@ import { initLongPress } from './modules/longpress.js';
1224
1223
 
1225
1224
  var avatar = document.createElement("img");
1226
1225
  avatar.className = "dm-msg-avatar";
1227
- avatar.src = "https://api.dicebear.com/9.x/" + (dmTargetUser.avatarStyle || "thumbs") + "/svg?seed=" + encodeURIComponent(dmTargetUser.avatarSeed || dmTargetUser.username) + "&size=36";
1226
+ avatar.src = userAvatarUrl(dmTargetUser, 36);
1228
1227
  div.appendChild(avatar);
1229
1228
 
1230
1229
  var dots = document.createElement("div");
@@ -1272,10 +1271,10 @@ import { initLongPress } from './modules/longpress.js';
1272
1271
  avatarWrap.className = "home-hub-mate-avatar-wrap";
1273
1272
 
1274
1273
  var mp = mate.profile || {};
1275
- var avatarUrl = "https://api.dicebear.com/9.x/" + (mp.avatarStyle || mate.avatarStyle || "bottts") + "/svg?seed=" + encodeURIComponent(mp.avatarSeed || mate.avatarSeed || mate.id) + "&size=48";
1274
+ var mateAvUrl = mateAvatarUrl(mate, 48);
1276
1275
  var avatar = document.createElement("img");
1277
1276
  avatar.className = "home-hub-mate-avatar";
1278
- avatar.src = avatarUrl;
1277
+ avatar.src = mateAvUrl;
1279
1278
  avatar.alt = mp.displayName || mate.displayName || mate.name || "";
1280
1279
  avatarWrap.appendChild(avatar);
1281
1280
 
@@ -1407,7 +1406,7 @@ import { initLongPress } from './modules/longpress.js';
1407
1406
  var refreshedMyUser = cachedAllUsers.find(function (u) { return u.id === myUserId; });
1408
1407
  if (refreshedMyUser) {
1409
1408
  document.body.dataset.myDisplayName = refreshedMyUser.displayName || "";
1410
- document.body.dataset.myAvatarUrl = "https://api.dicebear.com/9.x/" + (refreshedMyUser.avatarStyle || "thumbs") + "/svg?seed=" + encodeURIComponent(refreshedMyUser.avatarSeed || refreshedMyUser.username) + "&size=36";
1409
+ document.body.dataset.myAvatarUrl = userAvatarUrl(refreshedMyUser, 36);
1411
1410
  }
1412
1411
  }
1413
1412
  // Render my avatar (always present, hidden behind user-island)
@@ -1417,7 +1416,7 @@ import { initLongPress } from './modules/longpress.js';
1417
1416
  if (myUser) {
1418
1417
  var meAvatar = document.createElement("img");
1419
1418
  meAvatar.className = "icon-strip-me-avatar";
1420
- meAvatar.src = "https://api.dicebear.com/9.x/" + (myUser.avatarStyle || "thumbs") + "/svg?seed=" + encodeURIComponent(myUser.avatarSeed || myUser.username) + "&size=34";
1419
+ meAvatar.src = userAvatarUrl(myUser, 34);
1421
1420
  meEl.appendChild(meAvatar);
1422
1421
  }
1423
1422
  }
@@ -1433,7 +1432,7 @@ import { initLongPress } from './modules/longpress.js';
1433
1432
  var cu = serverUsers[cui];
1434
1433
  var cuImg = document.createElement("img");
1435
1434
  cuImg.className = "client-avatar";
1436
- cuImg.src = "https://api.dicebear.com/9.x/" + (cu.avatarStyle || "thumbs") + "/svg?seed=" + encodeURIComponent(cu.avatarSeed || cu.username) + "&size=24";
1435
+ cuImg.src = userAvatarUrl(cu, 24);
1437
1436
  cuImg.alt = cu.displayName;
1438
1437
  cuImg.dataset.tip = cu.displayName + " (@" + cu.username + ")";
1439
1438
  if (cui > 0) cuImg.style.marginLeft = "-6px";
@@ -4618,7 +4617,7 @@ import { initLongPress } from './modules/longpress.js';
4618
4617
  updateMateSidebarProfile(msg.mate);
4619
4618
  }
4620
4619
  // Update DM header if currently chatting with this mate
4621
- if (dmMode && currentDmTarget === msg.mate.id) {
4620
+ if (dmMode && dmTargetUser && dmTargetUser.id === msg.mate.id) {
4622
4621
  var updatedName = (msg.mate.profile && msg.mate.profile.displayName) || msg.mate.name;
4623
4622
  if (updatedName) {
4624
4623
  var dmHeaderName = document.getElementById("dm-header-name");
@@ -5169,6 +5168,8 @@ import { initLongPress } from './modules/longpress.js';
5169
5168
 
5170
5169
  // --- QR code ---
5171
5170
  initQrCode();
5171
+ var sharePill = document.getElementById("share-pill");
5172
+ if (sharePill) sharePill.addEventListener("click", triggerShare);
5172
5173
 
5173
5174
  // --- File browser ---
5174
5175
  initFileBrowser({
@@ -5192,6 +5193,21 @@ import { initLongPress } from './modules/longpress.js';
5192
5193
  // --- Playbook Engine ---
5193
5194
  initPlaybook();
5194
5195
 
5196
+ // Auto-open playbook from URL param (e.g. ?playbook=push-notifications)
5197
+ (function () {
5198
+ var params = new URLSearchParams(window.location.search);
5199
+ var pbId = params.get("playbook");
5200
+ if (pbId) {
5201
+ // Small delay to ensure DOM and playbook registry are ready
5202
+ setTimeout(function () { openPlaybook(pbId); }, 300);
5203
+ // Clean up URL
5204
+ params.delete("playbook");
5205
+ var clean = params.toString();
5206
+ var newUrl = window.location.pathname + (clean ? "?" + clean : "") + window.location.hash;
5207
+ window.history.replaceState(null, "", newUrl);
5208
+ }
5209
+ })();
5210
+
5195
5211
  // --- In-session search (Cmd+F / Ctrl+F) ---
5196
5212
  initSessionSearch({
5197
5213
  messagesEl: messagesEl,
@@ -6751,7 +6767,13 @@ import { initLongPress } from './modules/longpress.js';
6751
6767
  modal.querySelector(".pwa-modal-backdrop").addEventListener("click", closeModal);
6752
6768
 
6753
6769
  confirmBtn.addEventListener("click", function () {
6754
- // Redirect to setup page
6770
+ // Builtin cert (*.d.clay.studio): open PWA setup guide
6771
+ if (location.hostname.endsWith(".d.clay.studio")) {
6772
+ closeModal();
6773
+ location.href = "/pwa";
6774
+ return;
6775
+ }
6776
+ // mkcert / other: redirect to onboarding setup page
6755
6777
  var port = parseInt(location.port, 10);
6756
6778
  var setupUrl;
6757
6779
  if (!port) {
@@ -6834,7 +6856,7 @@ import { initLongPress } from './modules/longpress.js';
6834
6856
  return userColorMap[userId];
6835
6857
  }
6836
6858
 
6837
- function createCursorElement(userId, displayName, color, avatarStyle, avatarSeed) {
6859
+ function createCursorElement(userId, displayName, color, avatarStyle, avatarSeed, avatarCustom) {
6838
6860
  var wrapper = document.createElement("div");
6839
6861
  wrapper.className = "remote-cursor";
6840
6862
  wrapper.dataset.userId = userId;
@@ -6870,9 +6892,7 @@ import { initLongPress } from './modules/longpress.js';
6870
6892
  // Avatar
6871
6893
  var avatarImg = document.createElement("img");
6872
6894
  avatarImg.className = "remote-cursor-avatar";
6873
- var style = avatarStyle || "thumbs";
6874
- var seed = avatarSeed || userId;
6875
- avatarImg.src = "https://api.dicebear.com/9.x/" + style + "/svg?seed=" + encodeURIComponent(seed) + "&size=16";
6895
+ avatarImg.src = avatarCustom ? avatarCustom : avatarUrl(avatarStyle || "thumbs", avatarSeed || userId, 16);
6876
6896
  avatarImg.style.cssText = "width:14px;height:14px;border-radius:50%;background:#fff;flex-shrink:0;";
6877
6897
  tag.appendChild(avatarImg);
6878
6898
 
@@ -7044,7 +7064,7 @@ import { initLongPress } from './modules/longpress.js';
7044
7064
  var entry = remoteCursors[userId];
7045
7065
  if (!entry) {
7046
7066
  var color = getCursorColor(userId);
7047
- var el = createCursorElement(userId, msg.displayName, color, msg.avatarStyle, msg.avatarSeed);
7067
+ var el = createCursorElement(userId, msg.displayName, color, msg.avatarStyle, msg.avatarSeed, msg.avatarCustom);
7048
7068
  messagesEl.appendChild(el);
7049
7069
  var indicator = createOffscreenIndicator(userId, msg.displayName, color);
7050
7070
  messagesEl.appendChild(indicator);
@@ -320,7 +320,7 @@
320
320
  right: 0;
321
321
  transform: none;
322
322
  width: 100%;
323
- max-height: 85vh;
323
+ max-height: 85dvh;
324
324
  border-radius: 16px 16px 0 0;
325
325
  border: none;
326
326
  border-top: 1px solid var(--border-subtle);
@@ -1,3 +1,15 @@
1
+ /* === Avatar anti-aliasing === */
2
+ .mate-sidebar-avatar,
3
+ .mate-collapsed-avatar,
4
+ .dm-bubble-avatar,
5
+ .dm-bubble-avatar-me,
6
+ .home-hub-mate-avatar {
7
+ image-rendering: -webkit-optimize-contrast;
8
+ image-rendering: smooth;
9
+ -webkit-backface-visibility: hidden;
10
+ backface-visibility: hidden;
11
+ }
12
+
1
13
  /* === Mate Chat Title Bar === */
2
14
 
3
15
  .title-bar-content.mate-dm-active {
@@ -151,20 +163,7 @@
151
163
  font-size: 48px;
152
164
  line-height: 1;
153
165
  }
154
- .mate-intro-experimental {
155
- display: inline-block;
156
- font-size: 10px;
157
- font-weight: 700;
158
- letter-spacing: 0.05em;
159
- text-transform: uppercase;
160
- color: #f59e0b;
161
- background: rgba(245, 158, 11, 0.12);
162
- border: 1px solid rgba(245, 158, 11, 0.25);
163
- border-radius: 10px;
164
- padding: 2px 7px;
165
- vertical-align: middle;
166
- margin-right: 4px;
167
- }
166
+
168
167
  .mate-intro-title {
169
168
  font-size: 22px;
170
169
  font-weight: 700;
@@ -244,6 +244,25 @@
244
244
  margin-top: 2px;
245
245
  font-weight: 400;
246
246
  }
247
+ .qr-share-btn {
248
+ display: inline-flex;
249
+ align-items: center;
250
+ gap: 6px;
251
+ margin-top: 12px;
252
+ padding: 8px 16px;
253
+ border: 1px solid #ddd;
254
+ border-radius: 8px;
255
+ background: #fff;
256
+ color: #333;
257
+ font-family: inherit;
258
+ font-size: 13px;
259
+ font-weight: 500;
260
+ cursor: pointer;
261
+ transition: background 0.15s;
262
+ }
263
+ .qr-share-btn:hover { background: #f5f5f5; }
264
+ .qr-share-btn .lucide { width: 14px; height: 14px; }
265
+ .qr-share-btn.hidden { display: none; }
247
266
 
248
267
  /* --- Notification menu --- */
249
268
  #notif-menu-wrap {
@@ -44,35 +44,61 @@ button.top-bar-pill.pill-success:hover { background: color-mix(in srgb, var(--su
44
44
  button.top-bar-pill.pill-accent:hover { background: color-mix(in srgb, var(--accent) 20%, transparent); }
45
45
 
46
46
  /* PWA install button — left side of top bar, icon only */
47
- .top-bar-install-btn {
47
+ .top-bar-left-pills {
48
+ display: flex;
49
+ align-items: center;
50
+ gap: 6px;
48
51
  position: absolute;
49
52
  left: 10px;
50
- top: 50%;
51
- transform: translateY(-50%);
52
- display: flex;
53
+ top: 0;
54
+ bottom: 0;
55
+ }
56
+ .top-bar-install-btn {
57
+ display: inline-flex;
53
58
  align-items: center;
54
- justify-content: center;
55
- background: none;
56
- border: none;
57
- border-radius: 6px;
59
+ gap: 4px;
60
+ background: color-mix(in srgb, var(--accent) 12%, transparent);
58
61
  color: var(--accent);
62
+ border: none;
63
+ border-radius: 10px;
64
+ padding: 2px 10px;
65
+ font-family: inherit;
66
+ font-size: 11px;
67
+ font-weight: 600;
59
68
  cursor: pointer;
60
- padding: 3px;
61
- transition: color 0.15s, background 0.15s;
69
+ white-space: nowrap;
70
+ line-height: 1;
71
+ transition: background 0.15s;
62
72
  }
63
- .top-bar-install-btn .lucide { width: 14px; height: 14px; }
64
- .top-bar-install-btn:hover { background: color-mix(in srgb, var(--accent) 12%, transparent); }
73
+ .top-bar-install-btn .lucide { width: 12px; height: 12px; }
74
+ .top-bar-install-btn:hover { background: color-mix(in srgb, var(--accent) 20%, transparent); }
65
75
  .top-bar-install-btn.hidden { display: none; }
66
76
  .pwa-standalone .top-bar-install-btn { display: none !important; }
67
77
 
78
+ /* Share button — desktop only, same style as install pill */
79
+ .top-bar-share-btn {
80
+ display: inline-flex;
81
+ align-items: center;
82
+ gap: 4px;
83
+ background: color-mix(in srgb, var(--accent) 12%, transparent);
84
+ color: var(--accent);
85
+ border: none;
86
+ border-radius: 10px;
87
+ padding: 2px 10px;
88
+ font-family: inherit;
89
+ font-size: 11px;
90
+ font-weight: 600;
91
+ cursor: pointer;
92
+ white-space: nowrap;
93
+ line-height: 1;
94
+ transition: background 0.15s;
95
+ }
96
+ .top-bar-share-btn .lucide { width: 12px; height: 12px; }
97
+ .top-bar-share-btn:hover { background: color-mix(in srgb, var(--accent) 20%, transparent); }
68
98
  @media (max-width: 768px) {
69
- .top-bar-install-btn {
70
- top: auto;
71
- bottom: 0;
72
- height: 32px;
73
- transform: none;
74
- }
99
+ .top-bar-share-btn { display: none; }
75
100
  }
101
+ .pwa-standalone .top-bar-share-btn { display: none !important; }
76
102
 
77
103
  /* PWA install modal */
78
104
  .pwa-modal {