clay-server 2.17.0-beta.4 → 2.17.0-beta.6

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/bin/cli.js CHANGED
@@ -2251,7 +2251,7 @@ function showSetupGuide(config, ip, goBack) {
2251
2251
  // mkcert: use HTTP onboarding server for CA install flow
2252
2252
  var setupUrl;
2253
2253
  if (config.builtinCert) {
2254
- setupUrl = toClayStudioUrl(setupIP, config.port, "https") + "?playbook=push-notifications";
2254
+ setupUrl = toClayStudioUrl(setupIP, config.port, "https") + "/pwa";
2255
2255
  } else if (config.tls) {
2256
2256
  setupUrl = "http://" + setupIP + ":" + (config.port + 1) + "/setup" + setupQuery;
2257
2257
  } else {
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:#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
@@ -1646,11 +1646,11 @@ function createProjectContext(opts) {
1646
1646
  var switchTarget = sm.sessions.get(msg.id);
1647
1647
  if (!usersModule.canAccessSession(ws._clayUser.id, switchTarget, { visibility: "public" })) return;
1648
1648
  ws._clayActiveSession = msg.id;
1649
- sm.switchSession(msg.id, ws);
1649
+ sm.switchSession(msg.id, ws, hydrateImageRefs);
1650
1650
  broadcastPresence();
1651
1651
  } else {
1652
1652
  ws._clayActiveSession = msg.id;
1653
- sm.switchSession(msg.id, ws);
1653
+ sm.switchSession(msg.id, ws, hydrateImageRefs);
1654
1654
  }
1655
1655
  }
1656
1656
  return;
@@ -2005,7 +2005,7 @@ function createProjectContext(opts) {
2005
2005
  onProcessingChanged();
2006
2006
 
2007
2007
  sm.saveSessionFile(session);
2008
- sm.switchSession(session.localId, ws);
2008
+ sm.switchSession(session.localId, ws, hydrateImageRefs);
2009
2009
  sm.sendAndRecord(session, { type: "rewind_complete", mode: mode });
2010
2010
  sm.broadcastSessionList();
2011
2011
  } catch (err) {
@@ -3162,7 +3162,7 @@ function createProjectContext(opts) {
3162
3162
  judgeCraftSession.ralphCraftingMode = true;
3163
3163
  judgeCraftSession.loop = { active: true, iteration: 0, role: "crafting", loopId: newLoopId, name: craftName, source: recordSource, startedAt: loopState.startedAt };
3164
3164
  sm.saveSessionFile(judgeCraftSession);
3165
- sm.switchSession(judgeCraftSession.localId);
3165
+ sm.switchSession(judgeCraftSession.localId, null, hydrateImageRefs);
3166
3166
  loopState.craftingSessionId = judgeCraftSession.localId;
3167
3167
 
3168
3168
  loopRegistry.updateRecord(newLoopId, { craftingSessionId: judgeCraftSession.localId });
@@ -3206,7 +3206,7 @@ function createProjectContext(opts) {
3206
3206
  craftingSession.ralphCraftingMode = true;
3207
3207
  craftingSession.loop = { active: true, iteration: 0, role: "crafting", loopId: newLoopId, name: craftName, source: recordSource, startedAt: loopState.startedAt };
3208
3208
  sm.saveSessionFile(craftingSession);
3209
- sm.switchSession(craftingSession.localId);
3209
+ sm.switchSession(craftingSession.localId, null, hydrateImageRefs);
3210
3210
  loopState.craftingSessionId = craftingSession.localId;
3211
3211
 
3212
3212
  // Store crafting session ID in the registry record
package/lib/public/app.js CHANGED
@@ -6766,10 +6766,10 @@ import { initLongPress } from './modules/longpress.js';
6766
6766
  modal.querySelector(".pwa-modal-backdrop").addEventListener("click", closeModal);
6767
6767
 
6768
6768
  confirmBtn.addEventListener("click", function () {
6769
- // Builtin cert (*.d.clay.studio): open push notification guide directly
6769
+ // Builtin cert (*.d.clay.studio): open PWA setup guide
6770
6770
  if (location.hostname.endsWith(".d.clay.studio")) {
6771
6771
  closeModal();
6772
- openPlaybook("push-notifications");
6772
+ location.href = "/pwa";
6773
6773
  return;
6774
6774
  }
6775
6775
  // mkcert / other: redirect to onboarding setup page
@@ -44,36 +44,37 @@ 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
 
68
- @media (max-width: 768px) {
69
- .top-bar-install-btn {
70
- top: auto;
71
- bottom: 0;
72
- height: 32px;
73
- transform: none;
74
- }
75
- }
76
-
77
78
  /* PWA install modal */
78
79
  .pwa-modal {
79
80
  position: fixed;
@@ -15,10 +15,6 @@
15
15
 
16
16
  /* --- Update banner --- */
17
17
  .top-bar-update {
18
- position: absolute;
19
- left: 10px;
20
- top: 0;
21
- bottom: 0;
22
18
  display: flex;
23
19
  align-items: center;
24
20
  }
@@ -29,15 +29,17 @@
29
29
  <div id="layout">
30
30
  <!-- === Top Bar (full width, forms reverse-ㄱ with icon strip) === -->
31
31
  <div id="top-bar">
32
- <button id="pwa-install-pill" class="top-bar-install-btn hidden" title="Install app"><i data-lucide="download"></i></button>
33
- <button id="cmd-palette-btn" class="cmd-palette-searchbar" title="Command palette"><i data-lucide="search"></i><span class="cmd-palette-searchbar-text">Search sessions, projects, and commands</span><kbd class="cmd-palette-searchbar-kbd"></kbd></button>
34
- <div id="update-pill-wrap" class="top-bar-update hidden">
35
- <button id="update-pill" class="top-bar-update-btn"><i data-lucide="arrow-up-circle"></i> <span id="update-version"></span> is available. Update now</button>
32
+ <div class="top-bar-left-pills">
33
+ <button id="pwa-install-pill" class="top-bar-install-btn hidden" title="Open as app"><i data-lucide="download"></i> Open as app</button>
34
+ <div id="update-pill-wrap" class="top-bar-update hidden">
35
+ <button id="update-pill" class="top-bar-update-btn"><i data-lucide="arrow-up-circle"></i> <span id="update-version"></span> is available. Update now</button>
36
36
  <div id="update-popover" class="top-bar-popover">
37
37
  <div class="popover-row"><button id="update-now" class="popover-action popover-action-primary"><i data-lucide="download"></i> Update now</button></div>
38
38
  <div class="popover-row"><div class="popover-label">Or run manually:</div><div class="popover-cmd"><code id="update-manual-cmd">npx clay-server@latest</code><button class="popover-copy" title="Copy"><i data-lucide="copy"></i></button></div></div>
39
39
  </div>
40
40
  </div>
41
+ </div>
42
+ <button id="cmd-palette-btn" class="cmd-palette-searchbar" title="Command palette"><i data-lucide="search"></i><span class="cmd-palette-searchbar-text">Search sessions, projects, and commands</span><kbd class="cmd-palette-searchbar-kbd"></kbd></button>
41
43
  <div class="top-bar-actions">
42
44
  <!-- Pill badges -->
43
45
  <div id="skip-perms-pill" class="top-bar-pill pill-error hidden"><i data-lucide="shield-off"></i> <span>Skip perms</span></div>
@@ -1329,6 +1329,22 @@ function renderSheetSettings(listEl) {
1329
1329
  });
1330
1330
 
1331
1331
  listEl.appendChild(themeBtn);
1332
+
1333
+ // "Open as app" — only show if not already in PWA standalone mode
1334
+ if (!document.documentElement.classList.contains("pwa-standalone")) {
1335
+ var pwaBtn = document.createElement("button");
1336
+ pwaBtn.className = "mobile-more-item";
1337
+ pwaBtn.innerHTML = '<i data-lucide="smartphone"></i><span class="mobile-more-item-label">Open as app</span>';
1338
+ pwaBtn.addEventListener("click", function () {
1339
+ closeMobileSheet();
1340
+ // Trigger the existing PWA install modal
1341
+ var installPill = document.getElementById("pwa-install-pill");
1342
+ if (installPill) {
1343
+ setTimeout(function () { installPill.click(); }, 250);
1344
+ }
1345
+ });
1346
+ listEl.appendChild(pwaBtn);
1347
+ }
1332
1348
  }
1333
1349
 
1334
1350
  export function initSidebar(_ctx) {
package/lib/server.js CHANGED
@@ -1017,6 +1017,19 @@ function createServer(opts) {
1017
1017
  return;
1018
1018
  }
1019
1019
 
1020
+ // PWA install guide (builtin cert mode, no CA step needed)
1021
+ if (fullUrl === "/pwa" && req.method === "GET") {
1022
+ var host = req.headers.host || "localhost";
1023
+ var hostname = host.split(":")[0];
1024
+ var protocol = tlsOptions ? "https" : "http";
1025
+ var pwaUrl = protocol + "://" + hostname + ":" + portNum;
1026
+ res.writeHead(200, {
1027
+ "Content-Type": "text/html; charset=utf-8",
1028
+ });
1029
+ res.end(setupPageHtml(pwaUrl, pwaUrl, false, true));
1030
+ return;
1031
+ }
1032
+
1020
1033
  // Global push endpoints (used by setup page)
1021
1034
  if (req.method === "GET" && fullUrl === "/api/vapid-public-key" && pushModule) {
1022
1035
  res.writeHead(200, { "Content-Type": "application/json" });
package/lib/sessions.js CHANGED
@@ -315,7 +315,7 @@ function createSessionManager(opts) {
315
315
  return 0;
316
316
  }
317
317
 
318
- function replayHistory(session, fromIndex, targetWs) {
318
+ function replayHistory(session, fromIndex, targetWs, transform) {
319
319
  var _send = (targetWs && sendTo) ? function (obj) { sendTo(targetWs, obj); } : send;
320
320
  var total = session.history.length;
321
321
  if (typeof fromIndex !== "number") {
@@ -329,7 +329,7 @@ function createSessionManager(opts) {
329
329
  _send({ type: "history_meta", total: total, from: fromIndex });
330
330
 
331
331
  for (var i = fromIndex; i < total; i++) {
332
- _send(session.history[i]);
332
+ _send(transform ? transform(session.history[i]) : session.history[i]);
333
333
  }
334
334
 
335
335
  // Find the last result message in the full history for accurate context data
@@ -351,7 +351,7 @@ function createSessionManager(opts) {
351
351
  _send({ type: "history_done", lastUsage: lastUsage, lastModelUsage: lastModelUsage, lastCost: lastCost, lastStreamInputTokens: lastStreamInputTokens });
352
352
  }
353
353
 
354
- function switchSession(localId, targetWs) {
354
+ function switchSession(localId, targetWs, transform) {
355
355
  var session = sessions.get(localId);
356
356
  if (!session) return;
357
357
 
@@ -374,7 +374,7 @@ function createSessionManager(opts) {
374
374
 
375
375
  _send({ type: "session_switched", id: localId, cliSessionId: session.cliSessionId || null, loop: session.loop || null });
376
376
  broadcastSessionList();
377
- replayHistory(session, undefined, targetWs);
377
+ replayHistory(session, undefined, targetWs, transform);
378
378
 
379
379
  if (session.isProcessing) {
380
380
  _send({ type: "status", status: "processing" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.17.0-beta.4",
3
+ "version": "2.17.0-beta.6",
4
4
  "description": "Web UI for Claude Code. Any device. Push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",
@@ -1,10 +0,0 @@
1
- {
2
- "name": "Clay Light",
3
- "author": "Clay",
4
- "variant": "light",
5
- "base00": "F3EBE7", "base01": "EBE1DC", "base02": "D8CCC6", "base03": "A09590",
6
- "base04": "786D67", "base05": "504541", "base06": "332925", "base07": "1A1412",
7
- "base08": "C83520", "base09": "F74728", "base0A": "C08520", "base0B": "008F6B",
8
- "base0C": "1C8575", "base0D": "3560B0", "base0E": "8C4E8E", "base0F": "A57C45",
9
- "accent2": "2A26E5"
10
- }
@@ -1,10 +0,0 @@
1
- {
2
- "name": "Clay Dark",
3
- "author": "Clay",
4
- "variant": "dark",
5
- "base00": "1F1B1B", "base01": "2A2525", "base02": "352F2F", "base03": "7D7370",
6
- "base04": "A09590", "base05": "C2BAB4", "base06": "E5DED8", "base07": "FFFFFF",
7
- "base08": "F74728", "base09": "FE7150", "base0A": "E5A040", "base0B": "09E5A3",
8
- "base0C": "4EC9B0", "base0D": "6BA0E5", "base0E": "D085CC", "base0F": "D09558",
9
- "accent2": "5857FC"
10
- }