clay-server 4.0.0-beta.2 → 4.0.0-beta.21

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 (177) hide show
  1. package/lib/background-task-timing.js +105 -0
  2. package/lib/capsule-display-floor.js +51 -0
  3. package/lib/capsule-frame-server.js +255 -0
  4. package/lib/capsule-mate-turn.js +105 -0
  5. package/lib/capsule-pig-logic.js +300 -0
  6. package/lib/capsule-server-runtimes.js +39 -0
  7. package/lib/capsules/pig/display.js +198 -0
  8. package/lib/capsules/pig/manifest.json +9 -0
  9. package/lib/capsules/pig/ui.json +84 -0
  10. package/lib/config.js +25 -1
  11. package/lib/daemon-projects.js +13 -8
  12. package/lib/daemon.js +127 -14
  13. package/lib/git-cli.js +95 -1
  14. package/lib/knowledge-import.js +436 -0
  15. package/lib/knowledge-record-store.js +188 -0
  16. package/lib/knowledge-search.js +240 -0
  17. package/lib/mate-knowledge-mcp-server.js +151 -0
  18. package/lib/mate-knowledge-migration.js +283 -0
  19. package/lib/mate-knowledge-service.js +488 -0
  20. package/lib/mate-knowledge-sync.js +482 -0
  21. package/lib/migrate-single-user.js +2 -0
  22. package/lib/notes-lifecycle.js +149 -0
  23. package/lib/notes.js +60 -3
  24. package/lib/os-users.js +3 -1
  25. package/lib/project-capsule-catalog.js +9 -1
  26. package/lib/project-capsule-turn.js +96 -0
  27. package/lib/project-connection.js +14 -1
  28. package/lib/project-http.js +15 -1
  29. package/lib/project-knowledge.js +22 -0
  30. package/lib/project-log-context.js +172 -0
  31. package/lib/project-log-feedback-delivery.js +118 -0
  32. package/lib/project-logs-comments.js +137 -0
  33. package/lib/project-logs-context-state.js +42 -0
  34. package/lib/project-logs-mcp-server.js +328 -0
  35. package/lib/project-logs-query.js +185 -0
  36. package/lib/project-logs-root.js +75 -0
  37. package/lib/project-logs-schema.js +259 -0
  38. package/lib/project-logs-service.js +463 -0
  39. package/lib/project-logs-snapshot.js +205 -0
  40. package/lib/project-logs-store.js +495 -0
  41. package/lib/project-logs-versioning.js +132 -0
  42. package/lib/project-logs.js +400 -0
  43. package/lib/project-mate-interaction.js +14 -0
  44. package/lib/project-mate-knowledge.js +110 -0
  45. package/lib/project-memory.js +22 -0
  46. package/lib/project-message-delivery.js +70 -0
  47. package/lib/project-pair-lifecycle.js +460 -0
  48. package/lib/project-session-handoff.js +1 -1
  49. package/lib/project-session-notes.js +64 -13
  50. package/lib/project-session-pair.js +159 -146
  51. package/lib/project-session-spawn.js +1 -1
  52. package/lib/project-sessions.js +58 -13
  53. package/lib/project-user-message.js +53 -4
  54. package/lib/project-vendor-login.js +20 -3
  55. package/lib/project-worker-permission.js +430 -0
  56. package/lib/project-worker-proposal.js +208 -62
  57. package/lib/project.js +178 -6
  58. package/lib/public/app.js +58 -35
  59. package/lib/public/css/admin.css +2 -2
  60. package/lib/public/css/capsule-ui.css +38 -0
  61. package/lib/public/css/command-palette.css +19 -9
  62. package/lib/public/css/git-placard.css +154 -0
  63. package/lib/public/css/home-hub.css +4 -0
  64. package/lib/public/css/home-session-actions.css +66 -0
  65. package/lib/public/css/home-sidebar.css +66 -0
  66. package/lib/public/css/input.css +163 -28
  67. package/lib/public/css/mobile-nav.css +80 -0
  68. package/lib/public/css/notifications-center.css +67 -0
  69. package/lib/public/css/pane.css +52 -0
  70. package/lib/public/css/project-logs.css +198 -0
  71. package/lib/public/css/scheduler.css +1 -1
  72. package/lib/public/css/session-actions.css +16 -101
  73. package/lib/public/css/sidebar.css +103 -0
  74. package/lib/public/css/sticky-notes.css +237 -229
  75. package/lib/public/css/worker-proposal.css +34 -1
  76. package/lib/public/index.html +45 -11
  77. package/lib/public/modules/admin.js +8 -8
  78. package/lib/public/modules/app-connection.js +109 -4
  79. package/lib/public/modules/app-home-hub.js +19 -7
  80. package/lib/public/modules/app-message-cards.js +2 -1
  81. package/lib/public/modules/app-messages.js +84 -7
  82. package/lib/public/modules/app-misc.js +2 -14
  83. package/lib/public/modules/app-notifications.js +11 -7
  84. package/lib/public/modules/app-panels.js +46 -4
  85. package/lib/public/modules/app-projects.js +13 -8
  86. package/lib/public/modules/app-rendering.js +1 -0
  87. package/lib/public/modules/background-tasks-ui.js +139 -16
  88. package/lib/public/modules/capsule-preference.js +44 -0
  89. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  90. package/lib/public/modules/clay-log-links.js +70 -0
  91. package/lib/public/modules/command-palette.js +52 -15
  92. package/lib/public/modules/context-sources.js +0 -3
  93. package/lib/public/modules/git-agent-sessions.js +85 -0
  94. package/lib/public/modules/git-panel.js +111 -84
  95. package/lib/public/modules/git-placard.js +161 -0
  96. package/lib/public/modules/home-chat-empty-state.js +39 -0
  97. package/lib/public/modules/home-chat-identity.js +9 -0
  98. package/lib/public/modules/home-conversations-sheet.js +102 -48
  99. package/lib/public/modules/home-mate-chat.js +15 -35
  100. package/lib/public/modules/home-session-actions.js +6 -0
  101. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  102. package/lib/public/modules/home-sidebar.js +7 -0
  103. package/lib/public/modules/home-tool-frame.js +182 -0
  104. package/lib/public/modules/home-tools.js +138 -1
  105. package/lib/public/modules/input.js +38 -7
  106. package/lib/public/modules/markdown.js +2 -0
  107. package/lib/public/modules/mate-sidebar.js +0 -8
  108. package/lib/public/modules/message-delivery.js +97 -0
  109. package/lib/public/modules/paste-modal.js +84 -0
  110. package/lib/public/modules/project-logs-render.js +405 -0
  111. package/lib/public/modules/project-logs.js +461 -0
  112. package/lib/public/modules/project-removal-target.js +26 -0
  113. package/lib/public/modules/scheduler.js +17 -3
  114. package/lib/public/modules/session-actions.js +29 -105
  115. package/lib/public/modules/session-hierarchy.js +54 -0
  116. package/lib/public/modules/sidebar-mobile.js +40 -15
  117. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  118. package/lib/public/modules/sidebar-sessions.js +77 -11
  119. package/lib/public/modules/sidebar.js +17 -5
  120. package/lib/public/modules/split-group-helpers.js +8 -0
  121. package/lib/public/modules/split-view.js +5 -2
  122. package/lib/public/modules/sticky-note-markdown.js +5 -0
  123. package/lib/public/modules/sticky-notes-browser.js +326 -0
  124. package/lib/public/modules/sticky-notes-card.js +360 -0
  125. package/lib/public/modules/sticky-notes-editor.js +291 -0
  126. package/lib/public/modules/sticky-notes-shared.js +90 -0
  127. package/lib/public/modules/sticky-notes.js +170 -946
  128. package/lib/public/modules/tool-palette-order.js +143 -0
  129. package/lib/public/modules/tool-palette-overlays.js +159 -0
  130. package/lib/public/modules/tool-palette.js +21 -188
  131. package/lib/public/modules/tools.js +21 -1
  132. package/lib/public/modules/update-snooze.js +169 -0
  133. package/lib/public/modules/user-settings.js +4 -0
  134. package/lib/public/modules/worker-pane-lock.js +176 -0
  135. package/lib/public/modules/worker-proposal-state.js +16 -0
  136. package/lib/public/modules/worker-proposal.js +49 -12
  137. package/lib/public/style.css +3 -1
  138. package/lib/sdk-bridge.js +132 -46
  139. package/lib/sdk-message-processor.js +60 -14
  140. package/lib/server-experimental-settings.js +49 -0
  141. package/lib/server-global-ws.js +11 -0
  142. package/lib/server-home-chat-events.js +31 -1
  143. package/lib/server-home-chat.js +27 -4
  144. package/lib/server-home-clay-entry.js +1 -1
  145. package/lib/server-home-clay-session-links.js +34 -5
  146. package/lib/server-settings.js +5 -0
  147. package/lib/server-tools.js +127 -9
  148. package/lib/server.js +124 -18
  149. package/lib/session-driver-eligibility.js +38 -0
  150. package/lib/session-driver-orchestration.js +31 -0
  151. package/lib/session-hygiene.js +3 -0
  152. package/lib/session-notes-mcp-server.js +25 -5
  153. package/lib/session-pair-factory.js +247 -0
  154. package/lib/session-pair-mcp-server.js +46 -8
  155. package/lib/session-pair-prompts.js +79 -0
  156. package/lib/session-pair-turn-control.js +153 -0
  157. package/lib/session-provenance.js +119 -0
  158. package/lib/session-spawn-mcp-server.js +1 -1
  159. package/lib/session-split-groups.js +4 -1
  160. package/lib/session-title-policy.js +60 -0
  161. package/lib/session-visibility.js +39 -0
  162. package/lib/sessions.js +61 -11
  163. package/lib/tools-registry.js +89 -10
  164. package/lib/update-snooze.js +437 -0
  165. package/lib/users-experimental-preferences.js +30 -0
  166. package/lib/users-permissions.js +2 -2
  167. package/lib/users.js +22 -16
  168. package/lib/workspace-query-access.js +102 -0
  169. package/lib/workspace-query-service.js +24 -17
  170. package/lib/worktree.js +25 -38
  171. package/lib/ws-schema.js +38 -7
  172. package/lib/yoke/adapters/claude-worker.js +16 -6
  173. package/lib/yoke/adapters/claude.js +81 -4
  174. package/lib/yoke/adapters/codex.js +63 -15
  175. package/lib/yoke/codex-background-tasks.js +8 -2
  176. package/lib/yoke/vendor-registry.js +1 -1
  177. package/package.json +3 -3
@@ -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>
@@ -501,12 +508,14 @@
501
508
  <div class="usage-section-label">Session Tokens</div>
502
509
  <div class="usage-row"><span class="usage-label">Input</span><span id="context-input" class="usage-value">0</span></div>
503
510
  <div class="usage-row"><span class="usage-label">Output</span><span id="context-output" class="usage-value">0</span></div>
511
+ <div class="usage-row hidden" id="context-thinking-row" title="Extended thinking tokens, already counted inside Output"><span class="usage-label">Thinking</span><span id="context-thinking" class="usage-value">0</span></div>
504
512
  <div class="usage-row"><span class="usage-label">Cache read</span><span id="context-cache-read" class="usage-value">0</span></div>
505
513
  <div class="usage-row"><span class="usage-label">Cache write</span><span id="context-cache-write" class="usage-value">0</span></div>
506
514
  <div class="usage-divider"></div>
507
515
  <div class="usage-section-label">Model</div>
508
516
  <div class="usage-row"><span class="usage-label">Name</span><span id="context-model" class="usage-value">-</span></div>
509
517
  <div class="usage-row"><span class="usage-label">Cost</span><span id="context-cost" class="usage-value">$0.0000</span></div>
518
+ <div class="usage-row hidden" id="context-cost-basis-row" title="Which price table this cost was computed from"><span class="usage-label">Priced at</span><span id="context-cost-basis" class="usage-value">-</span></div>
510
519
  <div class="usage-row"><span class="usage-label">Turns</span><span id="context-turns" class="usage-value">0</span></div>
511
520
  </div>
512
521
  </div>
@@ -539,13 +548,15 @@
539
548
  <button id="schedule-btn" type="button" aria-label="Schedule message" title="Schedule message"><i data-lucide="clock"></i></button>
540
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>
541
550
  <div id="context-sources-btn-wrap" class="desktop-only">
542
- <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>
543
552
  <div id="context-sources-picker" class="hidden">
544
553
  <div class="context-picker-section" id="context-picker-email"></div>
545
554
  <div class="context-picker-section" id="context-picker-terminals"></div>
546
555
  <div class="context-picker-section" id="context-picker-tabs"></div>
547
556
  </div>
548
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>
549
560
  </div>
550
561
  <div id="input-bottom-right">
551
562
  <div id="active-vendor-indicator" class="hidden" title="Session vendor">
@@ -996,6 +1007,7 @@
996
1007
  <option value="us-appearance">Appearance</option>
997
1008
  <option value="us-chat">Chat</option>
998
1009
  <option value="us-mates">Mates</option>
1010
+ <option value="us-experimental">Experimental</option>
999
1011
  <option value="us-email">Email</option>
1000
1012
  </select>
1001
1013
  <div class="us-modal-nav-items">
@@ -1003,6 +1015,7 @@
1003
1015
  <button class="us-nav-item" data-section="us-appearance"><span>Appearance</span></button>
1004
1016
  <button class="us-nav-item" data-section="us-chat"><span>Chat</span></button>
1005
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>
1006
1019
  <button class="us-nav-item" data-section="us-email"><span>Email</span></button>
1007
1020
  <div class="us-nav-separator"></div>
1008
1021
  <button class="us-nav-item us-nav-danger" data-section="us-logout"><span>Log Out</span></button>
@@ -1158,6 +1171,22 @@
1158
1171
  </div>
1159
1172
  </div>
1160
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
+
1161
1190
  <!-- Email section -->
1162
1191
  <div class="us-section" data-section="us-email">
1163
1192
  <h2>Email Accounts</h2>
@@ -1514,17 +1543,22 @@
1514
1543
  </div>
1515
1544
  </div>
1516
1545
 
1517
- <div id="paste-modal" class="hidden">
1546
+ <div id="paste-modal" class="hidden" role="dialog" aria-modal="true" aria-labelledby="paste-modal-title">
1518
1547
  <div class="confirm-backdrop"></div>
1519
1548
  <div class="confirm-dialog paste-modal-dialog">
1520
1549
  <div class="paste-modal-header">
1521
- <span class="paste-modal-title">Pasted content</span>
1550
+ <span class="paste-modal-title" id="paste-modal-title">Pasted content</span>
1522
1551
  <div class="paste-modal-actions">
1523
- <button class="paste-modal-copy" title="Copy to clipboard"><i data-lucide="copy" style="width:15px;height:15px"></i></button>
1524
- <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>
1525
1554
  </div>
1526
1555
  </div>
1527
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>
1528
1562
  </div>
1529
1563
  </div>
1530
1564
 
@@ -2303,7 +2337,7 @@
2303
2337
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0/lib/addon-fit.min.js"></script>
2304
2338
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0/lib/addon-web-links.min.js"></script>
2305
2339
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0/lib/addon-webgl.min.js"></script>
2306
- <script type="module" src="app.js?v=20260902-markdown-boundary1"></script>
2340
+ <script type="module" src="app.js?v=20260908-log-links2"></script>
2307
2341
  <div id="pwa-install-modal" class="pwa-modal hidden">
2308
2342
  <div class="pwa-modal-backdrop"></div>
2309
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,6 +217,10 @@ function onConnected() {
133
217
 
134
218
  export function connect() {
135
219
  var ws = getWs();
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");
136
224
  if (ws) { ws.onclose = null; ws.close(); }
137
225
  if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
138
226
 
@@ -152,7 +240,12 @@ export function connect() {
152
240
 
153
241
  newWs.onopen = function () {
154
242
  if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
243
+ if (hasConnectedOnce && disconnectedAt) {
244
+ console.log("[clay] WebSocket reconnected after " + (Date.now() - disconnectedAt) + "ms");
245
+ }
246
+ disconnectedAt = 0;
155
247
  setStatus("connected");
248
+ startHeartbeat(newWs);
156
249
  reconnectDelay = 1000;
157
250
  if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
158
251
 
@@ -175,6 +268,14 @@ export function connect() {
175
268
 
176
269
  newWs.onclose = function (e) {
177
270
  if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
271
+ stopHeartbeat();
272
+ disconnectedAt = Date.now();
273
+ // Abnormal closes are the ones worth reporting: a clean close is either a
274
+ // deliberate navigation or a server shutdown the user already knows about.
275
+ if (e && !e.wasClean) {
276
+ console.warn("[clay] WebSocket closed abnormally: code=" + e.code +
277
+ " reason=" + (e.reason || "(none)") + " wasClean=false");
278
+ }
178
279
  closeDmUserPicker();
179
280
  setStatus("disconnected");
180
281
  setActivity(null);
@@ -194,6 +295,10 @@ export function connect() {
194
295
  blinkIO();
195
296
  var msg;
196
297
  try { msg = JSON.parse(event.data); } catch (e) { return; }
298
+ if (msg.type === "pong" && heartbeatDeadlineTimer) {
299
+ clearTimeout(heartbeatDeadlineTimer);
300
+ heartbeatDeadlineTimer = null;
301
+ }
197
302
  processMessage(msg);
198
303
  };
199
304
  }
@@ -2,7 +2,7 @@
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
6
  import { mateAvatarUrl } from './avatar.js';
7
7
  import { exitDmMode } from './app-dm.js';
8
8
  import { closeHomeDock, initHomeDock, renderDock, requestHomeDockPreference } from './home-dock.js';
@@ -41,7 +41,22 @@ function syncHomePresentation() {
41
41
  function syncHomeCloseControl() {
42
42
  var close = document.getElementById("home-close-control");
43
43
  if (!close) return;
44
- close.classList.toggle("hidden", !store.get('currentSlug'));
44
+ close.classList.toggle("hidden", !getHomeReturnSlug());
45
+ }
46
+
47
+ function getHomeReturnSlug() {
48
+ var projects = getCachedProjects() || [];
49
+ var candidates = [store.get('currentSlug'), store.get('homeSurfaceProjectSlug')];
50
+ for (var ci = 0; ci < candidates.length; ci++) {
51
+ if (!candidates[ci]) continue;
52
+ for (var pi = 0; pi < projects.length; pi++) {
53
+ if (projects[pi] && !projects[pi].isMate && projects[pi].slug === candidates[ci]) return candidates[ci];
54
+ }
55
+ }
56
+ for (var i = 0; i < projects.length; i++) {
57
+ if (projects[i] && !projects[i].isMate && projects[i].slug) return projects[i].slug;
58
+ }
59
+ return null;
45
60
  }
46
61
 
47
62
  function getVisibleMates() {
@@ -390,13 +405,10 @@ export function hideHomeHub() {
390
405
  }
391
406
 
392
407
  export function minimizeHomeHub() {
393
- var slug = store.get('currentSlug');
408
+ var slug = getHomeReturnSlug();
394
409
  if (!slug || !homeHubVisible) return;
395
410
  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);
411
+ switchProject(slug);
400
412
  var projectInput = document.getElementById("input");
401
413
  if (projectInput && !projectInput.disabled) projectInput.focus({ preventScroll: true });
402
414
  }
@@ -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) {
@@ -3,6 +3,7 @@
3
3
  // All dependencies are direct imports; no context injection needed.
4
4
 
5
5
  import { store } from './store.js';
6
+ import { acknowledgeMessage, replayPendingMessages } from './message-delivery.js';
6
7
  import { getWs } from './ws-ref.js';
7
8
 
8
9
  // --- Leaf module imports ---
@@ -15,7 +16,7 @@ import { updateDmBadge, setMentionActive, renderUserStrip } from './sidebar-mate
15
16
  import { renderSidebarPresence } from './sidebar-presence.js';
16
17
  import { refreshMobileChatSheet } from './sidebar-mobile.js';
17
18
  import { renderMateSessionList, handleMateSearchResults, updateMateSidebarProfile } from './mate-sidebar.js';
18
- import { openHomeChat, handleHomeMateHistory, handleHomeMateDelta, handleHomeMateDone, handleHomeMateError, handleHomeMateSessionsState } from './home-mate-chat.js';
19
+ import { openHomeChat, handleHomeMateHistory, handleHomeMateDelta, handleHomeMateSegment, handleHomeMateDone, handleHomeMateError, handleHomeMateSessionsState } from './home-mate-chat.js';
19
20
  import { handleHomeSurfaceState } from './home-surface.js';
20
21
  import { handleHomeMateMemoryState, handleHomeMateKnowledgeState } from './home-mate-settings.js';
21
22
  import { renderKnowledgeList, handleKnowledgeContent } from './mate-knowledge.js';
@@ -41,6 +42,7 @@ import { tuiModalHandleTermOutput, tuiModalHandleTermResized, tuiModalHandleTerm
41
42
  import { updateTerminalList, handleContextSourcesState, updateEmailAccountList, updateEmailUnreadCounts, handleEmailTestResult, handleEmailAddResult, handleEmailRemoveResult } from './context-sources.js';
42
43
  import { refreshEmailSettings } from './user-settings.js';
43
44
  import { handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted, handleNoteWritten } from './sticky-notes.js';
45
+ import { handleProjectLogsState, handleProjectLogEntry, handleProjectLogCommented, handleProjectLogCommentReviewed, handleProjectLogDeleted, handleProjectLogUpdated, handleProjectLogsError } from './project-logs.js';
44
46
  import { handleSkillInstalled, handleSkillUninstalled } from './skills.js';
45
47
  import { showRewindModal, onRewindComplete, setRewindMode, onRewindError, clearPendingRewindUuid, addRewindButton } from './rewind.js';
46
48
  import { checkAdminAccess } from './admin.js';
@@ -64,8 +66,9 @@ import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateCont
64
66
  import { updateProjectList, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress } from './app-projects.js';
65
67
  import { updateHistorySentinel, prependOlderHistory } from './app-header.js';
66
68
  import { hideHomeHub, showHomeHub } from './app-home-hub.js';
67
- import { handleToolsState, handleToolInstalled, handleToolRemoved, handleToolStorageResult, handleToolLlmResult, handleToolsError, handleToolControlRequest } from './home-tools.js';
69
+ import { handleToolsState, handleToolInstalled, handleToolRemoved, handleToolStorageResult, handleToolLlmResult, handleToolsError, handleToolControlRequest, handleToolServerState, handleToolServerEvent, handleCapsuleGameSession } from './home-tools.js';
68
70
  import { handleHomeDockState } from './home-dock.js';
71
+ import { handleToolFrameUrlState } from './home-tool-frame.js';
69
72
  import { enterDmMode, exitDmMode, handleMateCreatedInApp, updateMateIconStatus, appendDmMessage, showDmTypingIndicator, buildMateInterviewPrompt } from './app-dm.js';
70
73
  import { handleRateLimitEvent, updateRateLimitUsage, addScheduledMessageBubble, removeScheduledMessageBubble, handleFastModeState } from './app-rate-limit.js';
71
74
  import { handleRemoteCursorMove, handleRemoteCursorLeave, handleRemoteSelection, clearRemoteCursors } from './app-cursors.js';
@@ -73,6 +76,7 @@ import { showLoopBanner, updateLoopBanner, updateLoopInputVisibility, showRalphA
73
76
  import { showDebateSticky, showDebateConcludeConfirm, showDebateUserFloor, exitDebateFloorMode, exitDebateConcludeMode, exitDebateEndedMode, updateDebateRound, renderDebateUserFloorDone } from './app-debate-ui.js';
74
77
  import { handleSkillInstallWs } from './app-skills-install.js';
75
78
  import { handleNotificationsState, handleNotificationCreated, handleNotificationDismissed, handleNotificationDismissedAll, showUpdateBanner } from './app-notifications.js';
79
+ import { handleUpdateSnoozed } from './update-snooze.js';
76
80
  import { autoStartLoginIfNeeded, handleVendorLoginReady, handleVendorLoginState, handleVendorLoginError, handleAuthRefreshed } from './vendor-login.js';
77
81
  import { handleDebatePreparing, handleDebateBriefReady, renderDebateBriefReady, handleDebateStarted, renderDebateStarted, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, renderDebateCommentInjected, handleDebateResumed, handleDebateEnded, renderDebateEnded, handleDebateError, isDebateActive, renderMcpDebateProposal, renderDebateUserResume } from './debate.js';
78
82
  import { handleMentionStart, handleMentionActivity, handleMentionStream, handleMentionDone, handleMentionError, renderMentionUser, renderMentionResponse, renderUserMention } from './mention.js';
@@ -160,6 +164,20 @@ export function processMessage(msg) {
160
164
  }
161
165
 
162
166
  switch (msg.type) {
167
+ case "message_ack":
168
+ var acknowledgedEl = msg.clientMessageId
169
+ ? messagesEl.querySelector('[data-client-message-id="' + msg.clientMessageId + '"]')
170
+ : null;
171
+ acknowledgeMessage(msg.clientMessageId);
172
+ if (!acknowledgedEl && msg.message) {
173
+ addUserMessage(msg.message.text, msg.message.images || null, msg.message.pastes || null, msg.message.from, msg.message.fromName);
174
+ var acknowledgedUsers = messagesEl.querySelectorAll(".msg-user");
175
+ if (acknowledgedUsers.length > 0) {
176
+ acknowledgedUsers[acknowledgedUsers.length - 1].dataset.clientMessageId = msg.clientMessageId;
177
+ }
178
+ }
179
+ break;
180
+
163
181
  case "history_meta":
164
182
  store.set({ historyFrom: msg.from, historyTotal: msg.total, replayingHistory: true });
165
183
  updateHistorySentinel();
@@ -330,7 +348,9 @@ export function processMessage(msg) {
330
348
  break;
331
349
 
332
350
  case "update_available":
333
- // In multi-user mode, only show update UI to admins
351
+ // The server already refuses to send this frame to anyone who is not
352
+ // admin-grade, so this check is defence in depth for the UI, not the
353
+ // access control.
334
354
  if (store.get('isMultiUserMode')) {
335
355
  checkAdminAccess().then(function (isAdmin) {
336
356
  if (!isAdmin) return;
@@ -343,6 +363,10 @@ export function processMessage(msg) {
343
363
  }
344
364
  break;
345
365
 
366
+ case "update_snoozed":
367
+ handleUpdateSnoozed(msg);
368
+ break;
369
+
346
370
  case "up_to_date":
347
371
  var utdBtn = document.getElementById("settings-update-check");
348
372
  if (utdBtn) {
@@ -619,6 +643,26 @@ export function processMessage(msg) {
619
643
  handleToolControlRequest(msg);
620
644
  break;
621
645
 
646
+ case "tool_server_state":
647
+ handleToolServerState(msg);
648
+ break;
649
+
650
+ case "tool_server_event":
651
+ handleToolServerEvent(msg);
652
+ break;
653
+
654
+ case "tool_frame_url_state":
655
+ handleToolFrameUrlState(msg);
656
+ break;
657
+
658
+ case "capsule_game_session":
659
+ handleCapsuleGameSession(msg);
660
+ break;
661
+
662
+ case "home_mate_segment":
663
+ handleHomeMateSegment(msg);
664
+ break;
665
+
622
666
  case "input_sync":
623
667
  if (!store.get('dmMode')) handleInputSync(msg.text);
624
668
  break;
@@ -737,6 +781,7 @@ export function processMessage(msg) {
737
781
  ? msg.runtimeTerminalId
738
782
  : (typeof msg.terminalId === "number" ? msg.terminalId : null);
739
783
  store.set({ activeSessionId: msg.id, cliSessionId: msg.cliSessionId || null, currentModel: msg.model || "", currentEffort: msg.effort || store.get('currentEffort'), vendorCapabilities: msg.capabilities || {}, sessionIsProcessing: !!msg.isProcessing, activeSessionMode: _effectiveMode, activeTerminalId: _effectiveTerminalId, sessionHasHistory: !!msg.hasHistory, sessionVendorBound: !!msg.vendor || !!msg.hasHistory, sessionFullAccess: msg.permissionMode === "bypassPermissions", currentMode: msg.permissionMode || store.get('currentMode') });
784
+ replayPendingMessages(msg.id);
740
785
  // TUI sessions swap the chat UI for an embedded xterm running
741
786
  // `claude` inside a real PTY. Mount or tear down before the rest of
742
787
  // the chat-side bookkeeping runs so we don't waste work on hidden DOM.
@@ -858,6 +903,10 @@ export function processMessage(msg) {
858
903
  } else {
859
904
  addUserMessage(msg.text, msg.images || null, msg.pastes || null, msg.from, msg.fromName, msg.delegated ? msg : null);
860
905
  }
906
+ if (msg.clientMessageId) {
907
+ var replayedUsers = messagesEl.querySelectorAll(".msg-user");
908
+ if (replayedUsers.length > 0) replayedUsers[replayedUsers.length - 1].dataset.clientMessageId = msg.clientMessageId;
909
+ }
861
910
  // Split Worker pane: a delegated instruction starts (or replays into) a
862
911
  // driven turn; surface the composer notice. Cleared on "done".
863
912
  if (msg.delegated) showWorkerDelegationNotice(msg.delegatedByTitle || null);
@@ -1300,10 +1349,10 @@ export function processMessage(msg) {
1300
1349
  appendDelta((msg.text || "Authentication required.") + "\n");
1301
1350
  setStatus("connected");
1302
1351
  if (!store.get('loopActive')) enableMainInput();
1303
- // Open the login flow for this vendor. The server keeps exactly one
1304
- // login terminal per vendor, so repeat auth_required events (other
1305
- // sessions, split panes, a 401 retry burst) do not stack up.
1306
- autoStartLoginIfNeeded(msg);
1352
+ // Open the login flow only for a live auth failure. Older Clay
1353
+ // versions recorded this transient event in session history; replaying
1354
+ // one must not reopen a login terminal after the user signed in.
1355
+ if (!store.get('replayingHistory')) autoStartLoginIfNeeded(msg);
1307
1356
  break;
1308
1357
 
1309
1358
  case "vendor_login_ready":
@@ -1567,6 +1616,34 @@ export function processMessage(msg) {
1567
1616
  handleNoteDeleted(msg);
1568
1617
  break;
1569
1618
 
1619
+ case "project_logs_state":
1620
+ handleProjectLogsState(msg);
1621
+ break;
1622
+
1623
+ case "project_log_entry":
1624
+ handleProjectLogEntry(msg);
1625
+ break;
1626
+
1627
+ case "project_log_commented":
1628
+ handleProjectLogCommented(msg);
1629
+ break;
1630
+
1631
+ case "project_log_comment_reviewed":
1632
+ handleProjectLogCommentReviewed(msg);
1633
+ break;
1634
+
1635
+ case "project_log_deleted":
1636
+ handleProjectLogDeleted(msg);
1637
+ break;
1638
+
1639
+ case "project_log_updated":
1640
+ handleProjectLogUpdated(msg);
1641
+ break;
1642
+
1643
+ case "project_logs_error":
1644
+ handleProjectLogsError(msg);
1645
+ break;
1646
+
1570
1647
  case "process_stats":
1571
1648
  updateStatusPanel(msg);
1572
1649
  updateSettingsStats(msg);