clay-server 4.0.0-beta.8 → 4.0.0

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.
Files changed (185) hide show
  1. package/lib/capsule-display-floor.js +51 -0
  2. package/lib/capsule-frame-server.js +255 -0
  3. package/lib/capsule-mate-turn.js +105 -0
  4. package/lib/capsule-pig-logic.js +300 -0
  5. package/lib/capsule-server-runtimes.js +39 -0
  6. package/lib/capsules/pig/display.js +198 -0
  7. package/lib/capsules/pig/manifest.json +9 -0
  8. package/lib/capsules/pig/ui.json +84 -0
  9. package/lib/config.js +25 -1
  10. package/lib/daemon-projects.js +13 -8
  11. package/lib/daemon.js +127 -14
  12. package/lib/git-cli.js +95 -1
  13. package/lib/knowledge-import.js +436 -0
  14. package/lib/knowledge-record-store.js +188 -0
  15. package/lib/knowledge-search.js +240 -0
  16. package/lib/mate-knowledge-mcp-server.js +151 -0
  17. package/lib/mate-knowledge-migration.js +283 -0
  18. package/lib/mate-knowledge-service.js +488 -0
  19. package/lib/mate-knowledge-sync.js +482 -0
  20. package/lib/migrate-single-user.js +2 -0
  21. package/lib/notes-lifecycle.js +149 -0
  22. package/lib/notes.js +60 -3
  23. package/lib/os-users.js +3 -1
  24. package/lib/project-capsule-catalog.js +9 -1
  25. package/lib/project-capsule-turn.js +96 -0
  26. package/lib/project-connection.js +15 -1
  27. package/lib/project-file-history.js +223 -0
  28. package/lib/project-file-watch.js +77 -49
  29. package/lib/project-filesystem.js +31 -220
  30. package/lib/project-http.js +15 -1
  31. package/lib/project-knowledge.js +22 -0
  32. package/lib/project-log-context.js +172 -0
  33. package/lib/project-log-feedback-delivery.js +118 -0
  34. package/lib/project-logs-comments.js +137 -0
  35. package/lib/project-logs-context-state.js +42 -0
  36. package/lib/project-logs-mcp-server.js +328 -0
  37. package/lib/project-logs-query.js +185 -0
  38. package/lib/project-logs-root.js +75 -0
  39. package/lib/project-logs-schema.js +259 -0
  40. package/lib/project-logs-service.js +463 -0
  41. package/lib/project-logs-snapshot.js +205 -0
  42. package/lib/project-logs-store.js +495 -0
  43. package/lib/project-logs-versioning.js +132 -0
  44. package/lib/project-logs.js +400 -0
  45. package/lib/project-mate-interaction.js +14 -0
  46. package/lib/project-mate-knowledge.js +110 -0
  47. package/lib/project-memory.js +22 -0
  48. package/lib/project-message-delivery.js +70 -0
  49. package/lib/project-pair-lifecycle.js +460 -0
  50. package/lib/project-request-access.js +66 -0
  51. package/lib/project-session-handoff.js +1 -1
  52. package/lib/project-session-notes.js +64 -13
  53. package/lib/project-session-pair.js +159 -146
  54. package/lib/project-session-spawn.js +1 -1
  55. package/lib/project-sessions.js +58 -13
  56. package/lib/project-user-message.js +53 -4
  57. package/lib/project-worker-permission.js +430 -0
  58. package/lib/project-worker-proposal.js +208 -62
  59. package/lib/project.js +185 -6
  60. package/lib/public/app.js +63 -35
  61. package/lib/public/css/admin.css +2 -2
  62. package/lib/public/css/capsule-ui.css +38 -0
  63. package/lib/public/css/command-palette.css +19 -9
  64. package/lib/public/css/git-placard.css +154 -0
  65. package/lib/public/css/home-hub.css +4 -0
  66. package/lib/public/css/home-session-actions.css +66 -0
  67. package/lib/public/css/home-sidebar.css +66 -0
  68. package/lib/public/css/input.css +84 -3
  69. package/lib/public/css/messages.css +40 -1
  70. package/lib/public/css/mobile-nav.css +80 -0
  71. package/lib/public/css/notifications-center.css +67 -0
  72. package/lib/public/css/pane.css +97 -10
  73. package/lib/public/css/project-logs.css +198 -0
  74. package/lib/public/css/scheduler.css +1 -1
  75. package/lib/public/css/session-actions.css +16 -101
  76. package/lib/public/css/sidebar.css +103 -0
  77. package/lib/public/css/sticky-notes.css +237 -229
  78. package/lib/public/css/worker-proposal.css +36 -3
  79. package/lib/public/index.html +43 -11
  80. package/lib/public/modules/admin.js +8 -8
  81. package/lib/public/modules/app-connection.js +124 -6
  82. package/lib/public/modules/app-home-hub.js +12 -8
  83. package/lib/public/modules/app-message-cards.js +2 -1
  84. package/lib/public/modules/app-messages.js +92 -11
  85. package/lib/public/modules/app-misc.js +2 -14
  86. package/lib/public/modules/app-notifications.js +11 -7
  87. package/lib/public/modules/app-panels.js +3 -2
  88. package/lib/public/modules/app-projects.js +56 -18
  89. package/lib/public/modules/app-rendering.js +1 -0
  90. package/lib/public/modules/capsule-preference.js +44 -0
  91. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  92. package/lib/public/modules/clay-log-links.js +70 -0
  93. package/lib/public/modules/command-palette.js +52 -15
  94. package/lib/public/modules/context-sources.js +0 -3
  95. package/lib/public/modules/git-agent-sessions.js +85 -0
  96. package/lib/public/modules/git-panel.js +111 -84
  97. package/lib/public/modules/git-placard.js +161 -0
  98. package/lib/public/modules/home-chat-empty-state.js +39 -0
  99. package/lib/public/modules/home-chat-identity.js +9 -0
  100. package/lib/public/modules/home-conversations-sheet.js +102 -48
  101. package/lib/public/modules/home-mate-chat.js +15 -35
  102. package/lib/public/modules/home-session-actions.js +6 -0
  103. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  104. package/lib/public/modules/home-sidebar.js +7 -0
  105. package/lib/public/modules/home-surface-boot.js +7 -3
  106. package/lib/public/modules/home-tool-frame.js +182 -0
  107. package/lib/public/modules/home-tools.js +138 -1
  108. package/lib/public/modules/input.js +38 -7
  109. package/lib/public/modules/markdown.js +2 -0
  110. package/lib/public/modules/mate-sidebar.js +0 -8
  111. package/lib/public/modules/message-delivery.js +97 -0
  112. package/lib/public/modules/paste-modal.js +84 -0
  113. package/lib/public/modules/project-activation.js +45 -0
  114. package/lib/public/modules/project-logs-render.js +405 -0
  115. package/lib/public/modules/project-logs.js +461 -0
  116. package/lib/public/modules/project-removal-target.js +26 -0
  117. package/lib/public/modules/scheduler.js +17 -3
  118. package/lib/public/modules/session-actions.js +29 -105
  119. package/lib/public/modules/session-hierarchy.js +54 -0
  120. package/lib/public/modules/sidebar-mobile.js +40 -15
  121. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  122. package/lib/public/modules/sidebar-sessions.js +77 -11
  123. package/lib/public/modules/sidebar.js +17 -5
  124. package/lib/public/modules/split-group-helpers.js +8 -0
  125. package/lib/public/modules/split-pair-ui.js +15 -4
  126. package/lib/public/modules/split-view.js +5 -2
  127. package/lib/public/modules/split-worker-runtime.js +24 -0
  128. package/lib/public/modules/sticky-note-markdown.js +5 -0
  129. package/lib/public/modules/sticky-notes-browser.js +326 -0
  130. package/lib/public/modules/sticky-notes-card.js +360 -0
  131. package/lib/public/modules/sticky-notes-editor.js +291 -0
  132. package/lib/public/modules/sticky-notes-shared.js +90 -0
  133. package/lib/public/modules/sticky-notes.js +170 -946
  134. package/lib/public/modules/thinking-lifecycle.js +175 -0
  135. package/lib/public/modules/thinking-summary.js +28 -0
  136. package/lib/public/modules/thinking-view.js +75 -0
  137. package/lib/public/modules/tool-palette-order.js +143 -0
  138. package/lib/public/modules/tool-palette-overlays.js +159 -0
  139. package/lib/public/modules/tool-palette.js +21 -188
  140. package/lib/public/modules/tools.js +31 -123
  141. package/lib/public/modules/update-snooze.js +169 -0
  142. package/lib/public/modules/user-settings.js +4 -0
  143. package/lib/public/modules/worker-pane-lock.js +176 -0
  144. package/lib/public/modules/worker-proposal-state.js +16 -0
  145. package/lib/public/modules/worker-proposal.js +49 -20
  146. package/lib/public/style.css +3 -1
  147. package/lib/sdk-bridge.js +135 -46
  148. package/lib/sdk-message-processor.js +36 -19
  149. package/lib/server-experimental-settings.js +49 -0
  150. package/lib/server-global-ws.js +11 -0
  151. package/lib/server-home-chat-events.js +31 -1
  152. package/lib/server-home-chat.js +27 -4
  153. package/lib/server-home-clay-entry.js +1 -1
  154. package/lib/server-home-clay-session-links.js +34 -5
  155. package/lib/server-settings.js +5 -0
  156. package/lib/server-tools.js +127 -9
  157. package/lib/server.js +137 -22
  158. package/lib/session-driver-eligibility.js +38 -0
  159. package/lib/session-driver-orchestration.js +31 -0
  160. package/lib/session-hygiene.js +3 -0
  161. package/lib/session-notes-mcp-server.js +25 -5
  162. package/lib/session-pair-factory.js +247 -0
  163. package/lib/session-pair-mcp-server.js +46 -8
  164. package/lib/session-pair-prompts.js +79 -0
  165. package/lib/session-pair-turn-control.js +153 -0
  166. package/lib/session-provenance.js +119 -0
  167. package/lib/session-spawn-mcp-server.js +1 -1
  168. package/lib/session-split-groups.js +4 -1
  169. package/lib/session-title-policy.js +60 -0
  170. package/lib/session-visibility.js +41 -0
  171. package/lib/sessions.js +57 -10
  172. package/lib/tools-registry.js +89 -10
  173. package/lib/update-snooze.js +437 -0
  174. package/lib/users-experimental-preferences.js +30 -0
  175. package/lib/users-permissions.js +6 -6
  176. package/lib/users.js +22 -16
  177. package/lib/workspace-query-access.js +102 -0
  178. package/lib/workspace-query-service.js +24 -17
  179. package/lib/worktree.js +25 -38
  180. package/lib/ws-schema.js +38 -7
  181. package/lib/yoke/adapters/claude-worker.js +16 -6
  182. package/lib/yoke/adapters/claude.js +12 -0
  183. package/lib/yoke/adapters/codex.js +24 -14
  184. package/lib/yoke/vendor-registry.js +1 -1
  185. package/package.json +2 -2
@@ -31,7 +31,7 @@
31
31
  (function(){try{var k="clay-theme-vars",v=localStorage.getItem(k),r=document.documentElement;if(v){var o=JSON.parse(v),p;for(p in o)r.style.setProperty(p,o[p]);var vt=localStorage.getItem(k.replace("-vars","-variant"));if(vt==="light"){r.classList.add("light-theme");r.classList.remove("dark-theme")}else{r.classList.add("dark-theme");r.classList.remove("light-theme")}var m=document.querySelector('meta[name="theme-color"]');if(m&&o["--bg"])m.setAttribute("content",o["--bg"])}else{var sl=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches;if(sl){r.classList.add("light-theme");r.classList.remove("dark-theme")}}}catch(e){}})();
32
32
  </script>
33
33
  <script>if(window.navigator.standalone||window.matchMedia("(display-mode:standalone)").matches){document.documentElement.classList.add("pwa-standalone")}</script>
34
- <link rel="stylesheet" href="style.css?v=20260901-clay-chat6">
34
+ <link rel="stylesheet" href="style.css?v=20260908-log-links2">
35
35
  <style>
36
36
  @media(max-width:768px){
37
37
  /* User messages: vertical stack, avatar on top, right-aligned */
@@ -50,7 +50,7 @@
50
50
  }
51
51
  </style>
52
52
  </head>
53
- <body>
53
+ <body class="capsules-disabled">
54
54
  <div id="layout">
55
55
  <!-- === Top Bar (full width, forms reverse-ㄱ with icon strip) === -->
56
56
  <div id="top-bar">
@@ -137,6 +137,7 @@
137
137
  <nav class="home-sidebar-primary-actions" aria-label="Home actions">
138
138
  <button id="home-sidebar-new" class="home-sidebar-action home-sidebar-new" type="button" title="New Chat" aria-label="Start a new chat with the current Mate" disabled><span class="home-sidebar-action-icon" aria-hidden="true"><i data-lucide="message-square-plus"></i></span><span>New Chat</span></button>
139
139
  <button id="home-sidebar-debate" class="home-sidebar-action" type="button" title="Browse debates" aria-label="Browse debates" aria-pressed="false"><span class="home-sidebar-action-icon" aria-hidden="true"><i data-lucide="messages-square"></i></span><span>Debates</span></button>
140
+ <button id="home-scheduler-btn" class="home-sidebar-action" type="button" title="Scheduled Tasks" aria-label="Scheduled Tasks"><span class="home-sidebar-action-icon" aria-hidden="true"><i data-lucide="calendar-clock"></i></span><span>Scheduled Tasks</span></button>
140
141
  <button id="home-tools-btn" class="home-sidebar-action" type="button" title="Capsules" aria-label="Capsules" aria-expanded="false" aria-controls="home-tool-workbench"><span class="home-sidebar-action-icon" aria-hidden="true"><i data-lucide="box"></i></span><span id="home-tools-label">Capsules</span><span id="home-tools-activity" class="home-tools-activity" aria-hidden="true"></span></button>
141
142
  </nav>
142
143
  <section class="home-sidebar-section home-sidebar-mate-section" aria-labelledby="home-sidebar-mate-label">
@@ -246,6 +247,15 @@
246
247
  </div>
247
248
  <div id="session-actions"></div>
248
249
  <div id="session-actions-hidden" class="tool-palette-hidden-section hidden"></div>
250
+ <!-- Repository placard. Hidden until a real Git repository is
251
+ confirmed, so non-Git projects and pending detection show
252
+ nothing rather than an empty block. -->
253
+ <div id="git-placard" class="git-placard hidden" role="group">
254
+ <div id="git-placard-body" class="git-placard-body" aria-hidden="true"></div>
255
+ <button id="git-placard-more" class="git-placard-more" type="button" aria-label="Open Git panel for more repository detail" title="More Git detail">
256
+ <span>More</span><i data-lucide="chevron-right"></i>
257
+ </button>
258
+ </div>
249
259
  </div>
250
260
  <div id="sidebar-sessions-header">
251
261
  <div id="sessions-header-content">
@@ -412,9 +422,6 @@
412
422
  <div id="ralph-sticky" class="hidden"></div>
413
423
  <div id="debate-sticky" class="hidden"></div>
414
424
  <div class="status">
415
- <button id="header-session-actions-btn" type="button" aria-label="Session actions" aria-haspopup="menu" aria-expanded="false">
416
- <i data-lucide="bot"></i><span>Add Split Worker</span><i class="session-actions-trigger-chevron" data-lucide="chevron-down"></i>
417
- </button>
418
425
  <button type="button" id="header-tui-font-btn" class="header-tui-font-btn hidden" title="Terminal font" aria-label="Terminal font">
419
426
  <i data-lucide="type"></i>
420
427
  </button>
@@ -541,13 +548,15 @@
541
548
  <button id="schedule-btn" type="button" aria-label="Schedule message" title="Schedule message"><i data-lucide="clock"></i></button>
542
549
  <button id="ask-mate-btn" type="button" aria-label="Ask Mate" title="Ask a Mate for advice on this session"><i data-lucide="at-sign"></i></button>
543
550
  <div id="context-sources-btn-wrap" class="desktop-only">
544
- <button id="context-sources-add" type="button" title="Add context sources"><i data-lucide="library"></i><span class="ctx-label">Context</span></button>
551
+ <button id="context-sources-add" type="button" aria-label="Add context" title="Add context sources"><i data-lucide="library"></i></button>
545
552
  <div id="context-sources-picker" class="hidden">
546
553
  <div class="context-picker-section" id="context-picker-email"></div>
547
554
  <div class="context-picker-section" id="context-picker-terminals"></div>
548
555
  <div class="context-picker-section" id="context-picker-tabs"></div>
549
556
  </div>
550
557
  </div>
558
+ <button id="composer-add-worker-btn" type="button" class="composer-session-action desktop-only" aria-label="Add Split Worker" title="Add Split Worker"><i data-lucide="bot"></i></button>
559
+ <button id="composer-handoff-btn" type="button" class="composer-session-action desktop-only" aria-label="Continue in another agent" title="Continue in another agent"><i data-lucide="send"></i></button>
551
560
  </div>
552
561
  <div id="input-bottom-right">
553
562
  <div id="active-vendor-indicator" class="hidden" title="Session vendor">
@@ -998,6 +1007,7 @@
998
1007
  <option value="us-appearance">Appearance</option>
999
1008
  <option value="us-chat">Chat</option>
1000
1009
  <option value="us-mates">Mates</option>
1010
+ <option value="us-experimental">Experimental</option>
1001
1011
  <option value="us-email">Email</option>
1002
1012
  </select>
1003
1013
  <div class="us-modal-nav-items">
@@ -1005,6 +1015,7 @@
1005
1015
  <button class="us-nav-item" data-section="us-appearance"><span>Appearance</span></button>
1006
1016
  <button class="us-nav-item" data-section="us-chat"><span>Chat</span></button>
1007
1017
  <button class="us-nav-item" data-section="us-mates"><span>Mates</span></button>
1018
+ <button class="us-nav-item" data-section="us-experimental"><span>Experimental</span></button>
1008
1019
  <button class="us-nav-item" data-section="us-email"><span>Email</span></button>
1009
1020
  <div class="us-nav-separator"></div>
1010
1021
  <button class="us-nav-item us-nav-danger" data-section="us-logout"><span>Log Out</span></button>
@@ -1160,6 +1171,22 @@
1160
1171
  </div>
1161
1172
  </div>
1162
1173
 
1174
+ <!-- Experimental section -->
1175
+ <div class="us-section" data-section="us-experimental">
1176
+ <h2>Experimental</h2>
1177
+ <p class="settings-hint">Preview features may change or behave unexpectedly.</p>
1178
+ <div class="settings-card">
1179
+ <label class="settings-toggle-row">
1180
+ <div>
1181
+ <span class="settings-label">Capsules</span>
1182
+ <div class="settings-hint">Enable persistent mini-apps that you and your Mates can use.</div>
1183
+ </div>
1184
+ <input type="checkbox" id="us-capsules-enabled">
1185
+ <span class="toggle-track"><span class="toggle-thumb"></span></span>
1186
+ </label>
1187
+ </div>
1188
+ </div>
1189
+
1163
1190
  <!-- Email section -->
1164
1191
  <div class="us-section" data-section="us-email">
1165
1192
  <h2>Email Accounts</h2>
@@ -1516,17 +1543,22 @@
1516
1543
  </div>
1517
1544
  </div>
1518
1545
 
1519
- <div id="paste-modal" class="hidden">
1546
+ <div id="paste-modal" class="hidden" role="dialog" aria-modal="true" aria-labelledby="paste-modal-title">
1520
1547
  <div class="confirm-backdrop"></div>
1521
1548
  <div class="confirm-dialog paste-modal-dialog">
1522
1549
  <div class="paste-modal-header">
1523
- <span class="paste-modal-title">Pasted content</span>
1550
+ <span class="paste-modal-title" id="paste-modal-title">Pasted content</span>
1524
1551
  <div class="paste-modal-actions">
1525
- <button class="paste-modal-copy" title="Copy to clipboard"><i data-lucide="copy" style="width:15px;height:15px"></i></button>
1526
- <button class="paste-modal-close"><i data-lucide="x" style="width:16px;height:16px"></i></button>
1552
+ <button class="paste-modal-copy" type="button" title="Copy to clipboard" aria-label="Copy pasted content"><i data-lucide="copy" style="width:15px;height:15px"></i></button>
1553
+ <button class="paste-modal-close" type="button" aria-label="Close pasted content"><i data-lucide="x" style="width:16px;height:16px"></i></button>
1527
1554
  </div>
1528
1555
  </div>
1529
1556
  <pre class="paste-modal-body" id="paste-modal-body"></pre>
1557
+ <textarea class="paste-modal-editor" id="paste-modal-editor" aria-label="Pasted content"></textarea>
1558
+ <div class="paste-modal-footer" id="paste-modal-footer">
1559
+ <button class="paste-modal-cancel" type="button">Cancel</button>
1560
+ <button class="paste-modal-save" id="paste-modal-save" type="button">Save changes</button>
1561
+ </div>
1530
1562
  </div>
1531
1563
  </div>
1532
1564
 
@@ -2305,7 +2337,7 @@
2305
2337
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0/lib/addon-fit.min.js"></script>
2306
2338
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0/lib/addon-web-links.min.js"></script>
2307
2339
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0/lib/addon-webgl.min.js"></script>
2308
- <script type="module" src="app.js?v=20260902-markdown-boundary1"></script>
2340
+ <script type="module" src="app.js?v=20260908-log-links2"></script>
2309
2341
  <div id="pwa-install-modal" class="pwa-modal hidden">
2310
2342
  <div class="pwa-modal-backdrop"></div>
2311
2343
  <div class="pwa-modal-card">
@@ -2,15 +2,15 @@
2
2
  import { iconHtml, refreshIcons } from './icons.js';
3
3
  import { showToast, copyToClipboard, escapeHtml } from './utils.js';
4
4
 
5
- function showConfirmDialog(message, onConfirm) {
5
+ function showConfirmDialog(message, actionLabel, onConfirm) {
6
6
  var modal = document.createElement("div");
7
7
  modal.className = "admin-modal-overlay";
8
- var html = '<div class="admin-modal">' +
8
+ var html = '<div class="admin-modal" role="dialog" aria-modal="true" aria-label="Confirm action">' +
9
9
  '<div class="admin-modal-body" style="padding:20px 16px 16px">' +
10
10
  '<p class="admin-modal-desc" style="margin:0;font-size:14px;color:var(--text)">' + escapeHtml(message) + '</p>' +
11
11
  '</div>' +
12
12
  '<div class="admin-modal-footer">' +
13
- '<button class="admin-modal-save admin-modal-confirm-danger">Revoke</button>' +
13
+ '<button class="admin-modal-save admin-modal-confirm-danger">' + escapeHtml(actionLabel) + '</button>' +
14
14
  '<button class="admin-modal-cancel">Cancel</button>' +
15
15
  '</div></div>';
16
16
  modal.innerHTML = html;
@@ -198,9 +198,9 @@ function renderUsersTab(body) {
198
198
  var userId = this.dataset.userId;
199
199
  var user = cachedUsers.find(function (u) { return u.id === userId; });
200
200
  var name = user ? (user.displayName || user.username) : "this user";
201
- if (confirm("Remove " + name + "? This cannot be undone.")) {
201
+ showConfirmDialog("Remove " + name + "? This cannot be undone.", "Remove user", function () {
202
202
  removeUser(userId, body);
203
- }
203
+ });
204
204
  });
205
205
  }
206
206
  }
@@ -509,7 +509,7 @@ function renderInvitesTab(body) {
509
509
  revokeBtns[k].addEventListener("click", function () {
510
510
  var code = this.dataset.code;
511
511
  var btn = this;
512
- showConfirmDialog("Revoke this invite? The link will no longer work.", function () {
512
+ showConfirmDialog("Revoke this invite? The link will no longer work.", "Revoke", function () {
513
513
  btn.disabled = true;
514
514
  fetch("/api/admin/invites/" + encodeURIComponent(code), { method: "DELETE" })
515
515
  .then(function (r) { return r.json(); })
@@ -751,12 +751,12 @@ function renderSmtpTab(body, cfg) {
751
751
  var removeBtn = body.querySelector("#smtp-remove");
752
752
  if (removeBtn) {
753
753
  removeBtn.addEventListener("click", function () {
754
- if (confirm("Remove SMTP configuration? Users will need to use PIN login.")) {
754
+ showConfirmDialog("Remove SMTP configuration? Users will need to use PIN login.", "Remove SMTP", function () {
755
755
  apiPost("/api/admin/smtp", { host: "", user: "", pass: "", from: "" }).then(function () {
756
756
  showToast("SMTP configuration removed");
757
757
  loadSmtpTab(body);
758
758
  });
759
- }
759
+ });
760
760
  });
761
761
  }
762
762
  }
@@ -23,8 +23,80 @@ var connectTimeoutId = null;
23
23
  var connectOverlay = null;
24
24
  var hasConnectedOnce = false;
25
25
 
26
+ // The connect overlay is a full-viewport opaque panel. Revealing it for a
27
+ // reconnect that resolves in under a second reads as the whole screen
28
+ // blinking, so after the first successful connection it is only revealed once
29
+ // a disconnect has actually persisted. The status dot carries the state
30
+ // immediately and without covering anything.
31
+ var OVERLAY_GRACE_MS = 800;
32
+ var overlayGraceTimer = null;
33
+
34
+ // Keeps the socket from looking idle to an intermediary proxy. A proxy resets
35
+ // its upstream read timeout on data flowing back from the server, so this only
36
+ // protects the connection because the server answers with a pong: the project
37
+ // socket via project-connection.js and the slug-less socket via
38
+ // server-global-ws.js. Well under the common 60s proxy read timeout, and one
39
+ // timer per live socket.
40
+ var HEARTBEAT_MS = 25000;
41
+ var heartbeatTimer = null;
42
+ var heartbeatDeadlineTimer = null;
43
+ var disconnectedAt = 0;
44
+
45
+ function clearOverlayGrace() {
46
+ if (overlayGraceTimer) {
47
+ clearTimeout(overlayGraceTimer);
48
+ overlayGraceTimer = null;
49
+ }
50
+ }
51
+
52
+ function showOverlayNow() {
53
+ if (!connectOverlay) return;
54
+ if (hasConnectedOnce) {
55
+ var overlayMessage = document.getElementById("connect-overlay-msg");
56
+ if (overlayMessage) overlayMessage.textContent = "Reconnecting to server…";
57
+ }
58
+ connectOverlay.classList.remove("hidden");
59
+ }
60
+
61
+ export function stopHeartbeat() {
62
+ if (heartbeatTimer) {
63
+ clearInterval(heartbeatTimer);
64
+ heartbeatTimer = null;
65
+ }
66
+ if (heartbeatDeadlineTimer) {
67
+ clearTimeout(heartbeatDeadlineTimer);
68
+ heartbeatDeadlineTimer = null;
69
+ }
70
+ }
71
+
72
+ // Started once per live socket. The socket it was started for is captured, so
73
+ // a timer that outlives its socket stops itself instead of writing to a stale
74
+ // connection. The pong the server sends back keeps a proxy's upstream read
75
+ // timeout from expiring. A missing pong also identifies a half-open socket,
76
+ // which browsers can otherwise leave OPEN while silently losing sends.
77
+ export function startHeartbeat(socket) {
78
+ stopHeartbeat();
79
+ if (!socket) return;
80
+ heartbeatTimer = setInterval(function () {
81
+ if (!socket || socket.readyState !== 1 || getWs() !== socket) {
82
+ stopHeartbeat();
83
+ return;
84
+ }
85
+ try { socket.send(JSON.stringify({ type: "ping" })); } catch (e) {}
86
+ if (heartbeatDeadlineTimer) clearTimeout(heartbeatDeadlineTimer);
87
+ heartbeatDeadlineTimer = setTimeout(function () {
88
+ heartbeatDeadlineTimer = null;
89
+ if (getWs() === socket && socket.readyState === 1) connect();
90
+ }, 10000);
91
+ }, HEARTBEAT_MS);
92
+ }
93
+
26
94
  export function initConnection() {
27
95
  connectOverlay = document.getElementById("connect-overlay");
96
+ window.addEventListener("clay-message-delivery-timeout", function (event) {
97
+ var socket = event.detail && event.detail.socket;
98
+ if (!socket || getWs() === socket) connect();
99
+ });
28
100
 
29
101
  // --- Reactive UI sync for connected/processing state ---
30
102
  store.subscribe(function (state, prev) {
@@ -44,18 +116,30 @@ export function initConnection() {
44
116
  if (state.connected !== prev.connected) {
45
117
  var sendBtn = getSendBtn();
46
118
  if (state.connected) {
119
+ // Cancel a pending reveal and hide immediately: a reconnect that beat
120
+ // the grace delay must never flash the overlay at all.
121
+ clearOverlayGrace();
47
122
  hasConnectedOnce = true;
48
123
  if (sendBtn) sendBtn.disabled = false;
49
124
  if (connectOverlay) connectOverlay.classList.add("hidden");
50
125
  var updPill = document.getElementById("update-pill-wrap");
51
126
  if (updPill) updPill.classList.add("hidden");
52
127
  } else {
128
+ // The composer stays disabled while the socket is down because there
129
+ // is no outbound queue: every sender drops on readyState !== 1, so
130
+ // enabling it here would silently discard the message.
53
131
  if (sendBtn) sendBtn.disabled = true;
54
- if (hasConnectedOnce && connectOverlay) {
55
- var overlayMessage = document.getElementById("connect-overlay-msg");
56
- if (overlayMessage) overlayMessage.textContent = "Reconnecting to server…";
132
+ clearOverlayGrace();
133
+ if (!hasConnectedOnce) {
134
+ // First connection of the page: the startup overlay is the intended
135
+ // presentation and appears immediately.
136
+ showOverlayNow();
137
+ } else {
138
+ overlayGraceTimer = setTimeout(function () {
139
+ overlayGraceTimer = null;
140
+ if (!store.get('connected')) showOverlayNow();
141
+ }, OVERLAY_GRACE_MS);
57
142
  }
58
- if (connectOverlay) connectOverlay.classList.remove("hidden");
59
143
  }
60
144
  }
61
145
 
@@ -133,15 +217,28 @@ function onConnected() {
133
217
 
134
218
  export function connect() {
135
219
  var ws = getWs();
136
- if (ws) { ws.onclose = null; ws.close(); }
220
+ // Tear down the previous socket's heartbeat before a new socket exists, so
221
+ // two sockets can never both be pinging.
222
+ stopHeartbeat();
223
+ if (ws && ws.readyState === 1) setStatus("disconnected");
224
+ if (ws) {
225
+ ws.onopen = null;
226
+ ws.onmessage = null;
227
+ ws.onerror = null;
228
+ ws.onclose = null;
229
+ ws.close();
230
+ }
137
231
  if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
138
232
 
139
233
  var protocol = location.protocol === "https:" ? "wss:" : "ws:";
140
- var newWs = new WebSocket(protocol + "//" + location.host + store.get('wsPath'));
234
+ var socketPath = store.get('wsPath');
235
+ store.set({ socketPath: socketPath, activeProjectSlug: null, sessionActivatedProjectSlug: null });
236
+ var newWs = new WebSocket(protocol + "//" + location.host + socketPath);
141
237
  setWs(newWs);
142
238
 
143
239
  // If not connected within 3s, force retry
144
240
  connectTimeoutId = setTimeout(function () {
241
+ if (getWs() !== newWs) return;
145
242
  if (!store.get('connected')) {
146
243
  newWs.onclose = null;
147
244
  newWs.onerror = null;
@@ -151,8 +248,14 @@ export function connect() {
151
248
  }, 3000);
152
249
 
153
250
  newWs.onopen = function () {
251
+ if (getWs() !== newWs) return;
154
252
  if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
253
+ if (hasConnectedOnce && disconnectedAt) {
254
+ console.log("[clay] WebSocket reconnected after " + (Date.now() - disconnectedAt) + "ms");
255
+ }
256
+ disconnectedAt = 0;
155
257
  setStatus("connected");
258
+ startHeartbeat(newWs);
156
259
  reconnectDelay = 1000;
157
260
  if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
158
261
 
@@ -174,8 +277,18 @@ export function connect() {
174
277
  };
175
278
 
176
279
  newWs.onclose = function (e) {
280
+ if (getWs() !== newWs) return;
177
281
  if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
282
+ stopHeartbeat();
283
+ disconnectedAt = Date.now();
284
+ // Abnormal closes are the ones worth reporting: a clean close is either a
285
+ // deliberate navigation or a server shutdown the user already knows about.
286
+ if (e && !e.wasClean) {
287
+ console.warn("[clay] WebSocket closed abnormally: code=" + e.code +
288
+ " reason=" + (e.reason || "(none)") + " wasClean=false");
289
+ }
178
290
  closeDmUserPicker();
291
+ store.set({ activeProjectSlug: null, sessionActivatedProjectSlug: null });
179
292
  setStatus("disconnected");
180
293
  setActivity(null);
181
294
  scheduleReconnect();
@@ -184,6 +297,7 @@ export function connect() {
184
297
  newWs.onerror = function () {};
185
298
 
186
299
  newWs.onmessage = function (event) {
300
+ if (getWs() !== newWs) return;
187
301
  // Backup: if we're receiving messages, we're connected
188
302
  if (!store.get('connected')) {
189
303
  setStatus("connected");
@@ -194,6 +308,10 @@ export function connect() {
194
308
  blinkIO();
195
309
  var msg;
196
310
  try { msg = JSON.parse(event.data); } catch (e) { return; }
311
+ if (msg.type === "pong" && heartbeatDeadlineTimer) {
312
+ clearTimeout(heartbeatDeadlineTimer);
313
+ heartbeatDeadlineTimer = null;
314
+ }
197
315
  processMessage(msg);
198
316
  };
199
317
  }
@@ -2,7 +2,8 @@
2
2
 
3
3
  import { store } from './store.js';
4
4
  import { getWs } from './ws-ref.js';
5
- import { getCachedProjects } from './app-projects.js';
5
+ import { getCachedProjects, switchProject } from './app-projects.js';
6
+ import { chooseProjectActivationTarget } from './project-activation.js';
6
7
  import { mateAvatarUrl } from './avatar.js';
7
8
  import { exitDmMode } from './app-dm.js';
8
9
  import { closeHomeDock, initHomeDock, renderDock, requestHomeDockPreference } from './home-dock.js';
@@ -41,7 +42,14 @@ function syncHomePresentation() {
41
42
  function syncHomeCloseControl() {
42
43
  var close = document.getElementById("home-close-control");
43
44
  if (!close) return;
44
- close.classList.toggle("hidden", !store.get('currentSlug'));
45
+ close.classList.toggle("hidden", !getHomeReturnSlug());
46
+ }
47
+
48
+ function getHomeReturnSlug() {
49
+ return chooseProjectActivationTarget(
50
+ getCachedProjects(),
51
+ [store.get('currentSlug'), store.get('homeSurfaceProjectSlug')]
52
+ );
45
53
  }
46
54
 
47
55
  function getVisibleMates() {
@@ -390,13 +398,9 @@ export function hideHomeHub() {
390
398
  }
391
399
 
392
400
  export function minimizeHomeHub() {
393
- var slug = store.get('currentSlug');
401
+ var slug = getHomeReturnSlug();
394
402
  if (!slug || !homeHubVisible) return;
395
- rememberHomePrimarySurface("project");
396
- hideHomeHub();
397
- var route = "/p/" + slug + "/";
398
- if (document.documentElement.classList.contains("pwa-standalone")) history.replaceState(null, "", route);
399
- else history.pushState(null, "", route);
403
+ switchProject(slug);
400
404
  var projectInput = document.getElementById("input");
401
405
  if (projectInput && !projectInput.disabled) projectInput.focus({ preventScroll: true });
402
406
  }
@@ -6,7 +6,8 @@ import { escapeHtml, copyToClipboard } from './utils.js';
6
6
  import { renderMarkdown, highlightCodeBlocks } from './markdown.js';
7
7
  import { iconHtml, refreshIcons } from './icons.js';
8
8
  import { userAvatarUrl } from './avatar.js';
9
- import { showImageModal, showPasteModal } from './app-misc.js';
9
+ import { showImageModal } from './app-misc.js';
10
+ import { showPasteModal } from './paste-modal.js';
10
11
  import { addToMessages, scrollToBottom, forceScrollToBottom, getMsgTime, shouldGroupMessage, getTurnCounter, setTurnCounter, VENDOR_AVATARS } from './chat-render-runtime.js';
11
12
 
12
13
  export function addUserMessage(text, images, pastes, fromUserId, fromUserName, delegatedMeta) {