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
@@ -2,10 +2,10 @@
2
2
  // Extracted from app.js (PR-34)
3
3
 
4
4
  import { refreshIcons, iconHtml } from './icons.js';
5
- import { escapeHtml, copyToClipboard } from './utils.js';
6
5
  import { getWs } from './ws-ref.js';
7
6
  import { updateBrowserTabList } from './context-sources.js';
8
7
  import { setExtensionConnected } from './mcp-ui.js';
8
+ import { initPasteModal } from './paste-modal.js';
9
9
 
10
10
  // --- Module-owned state ---
11
11
  var confirmCallback = null;
@@ -44,6 +44,7 @@ export function flushPendingExtMessages() {
44
44
  }
45
45
 
46
46
  export function initMisc() {
47
+ initPasteModal();
47
48
  // --- Confirm modal listeners ---
48
49
  var confirmModal = document.getElementById("confirm-modal");
49
50
  var confirmOk = document.getElementById("confirm-ok");
@@ -258,19 +259,6 @@ export function closeImageModal() {
258
259
  if (modal) modal.classList.add("hidden");
259
260
  }
260
261
 
261
- export function showPasteModal(text) {
262
- var modal = document.getElementById("paste-modal");
263
- var body = document.getElementById("paste-modal-body");
264
- if (!modal || !body) return;
265
- body.textContent = text;
266
- modal.classList.remove("hidden");
267
- }
268
-
269
- export function closePasteModal() {
270
- var modal = document.getElementById("paste-modal");
271
- if (modal) modal.classList.add("hidden");
272
- }
273
-
274
262
  export function showConfirm(text, onConfirm, okLabel, destructive) {
275
263
  var confirmText = document.getElementById("confirm-text");
276
264
  var confirmOk = document.getElementById("confirm-ok");
@@ -14,6 +14,7 @@ import { openTuiModal } from './tui-attention.js';
14
14
  import { requestVendorLogin } from './vendor-login.js';
15
15
  import { startUrgentBlink, stopUrgentBlink } from './app-favicon.js';
16
16
  import { playDoneSound, isNotifSoundEnabled } from './notifications.js';
17
+ import { attachSnoozeControl, setSnoozeBannerRemover } from './update-snooze.js';
17
18
  var notifications = [];
18
19
  var unreadCount = 0;
19
20
  var bannerContainer = null;
@@ -56,6 +57,10 @@ export function initAppNotifications() {
56
57
  bellBtn = document.getElementById("notif-center-btn");
57
58
  badgeEl = document.getElementById("notif-center-badge");
58
59
 
60
+ // Banner teardown lives here (exit animation, clear-all sync), so the
61
+ // snooze module borrows it rather than importing this module back.
62
+ setSnoozeBannerRemover(removeBanner);
63
+
59
64
  // Split panes are secondary clients inside the parent shell. They keep
60
65
  // notification state in sync, but the parent owns the single visible
61
66
  // banner surface for the whole split view.
@@ -708,13 +713,11 @@ export function showUpdateBanner(msg) {
708
713
  banner.setAttribute("data-notif-id", "_update");
709
714
  banner.setAttribute("data-update-version", msg.version);
710
715
 
711
- var actionsHtml = '';
712
- if (!isHeadless) {
713
- actionsHtml =
714
- '<div class="notif-banner-actions">' +
715
- '<button class="notif-banner-update-now">Update now</button>' +
716
- '</div>';
717
- }
716
+ // Headless installs update from the shell, so they get no install button —
717
+ // but they still get the action row, because Snooze applies either way.
718
+ var actionsHtml = '<div class="notif-banner-actions">' +
719
+ (isHeadless ? '' : '<button class="notif-banner-update-now">Update now</button>') +
720
+ '</div>';
718
721
 
719
722
  banner.innerHTML =
720
723
  '<div class="notif-banner-icon"><img src="/clay-studio-symbol.png" width="32" height="32" alt="Clay Studio"></div>' +
@@ -729,6 +732,7 @@ export function showUpdateBanner(msg) {
729
732
  '<button class="notif-banner-close">' + iconHtml("x") + '</button>';
730
733
 
731
734
  bannerContainer.appendChild(banner);
735
+ attachSnoozeControl(banner.querySelector(".notif-banner-actions"));
732
736
  refreshIcons();
733
737
 
734
738
  requestAnimationFrame(function () {
@@ -10,7 +10,7 @@ import { setupModelPicker, renderModelPicker, requestVendorModels, prepareModelP
10
10
 
11
11
  // --- Module-owned state (not in store) ---
12
12
  var sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
13
- var contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
13
+ var contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0, thinking: null, costBasis: null };
14
14
  var ctxPopoverEl = null;
15
15
  var ctxHoverTimer = null;
16
16
  var statusRefreshTimer = null;
@@ -72,6 +72,10 @@ var contextCacheReadEl = null;
72
72
  var contextCacheWriteEl = null;
73
73
  var contextModelEl = null;
74
74
  var contextCostEl = null;
75
+ var contextThinkingEl = null;
76
+ var contextThinkingRow = null;
77
+ var contextCostBasisEl = null;
78
+ var contextCostBasisRow = null;
75
79
  var contextTurnsEl = null;
76
80
  var contextMini = null;
77
81
  var contextMiniFill = null;
@@ -87,7 +91,7 @@ var MODE_FULL_AUTO = { value: "bypassPermissions", label: "Full auto" };
87
91
  var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
88
92
  var EFFORT_LEVELS_BY_VENDOR = {
89
93
  claude: ["low", "medium", "high", "xhigh", "max"],
90
- codex: ["minimal", "low", "medium", "high", "xhigh"],
94
+ codex: ["low", "medium", "high", "xhigh", "max"],
91
95
  antigravity: [],
92
96
  opencode: [],
93
97
  kiro: ["low", "medium", "high", "xhigh", "max"],
@@ -111,7 +115,8 @@ var CODEX_WEBSEARCH_OPTIONS = [
111
115
  var KNOWN_CONTEXT_WINDOWS = {
112
116
  "opus-4-6": 1000000,
113
117
  "claude-sonnet-4": 1000000,
114
- "gpt-5.6": 272000,
118
+ "gpt-6-astra": 1050000,
119
+ "gpt-5.6": 1050000,
115
120
  "gpt-5.5": 1048576,
116
121
  "gpt-5.4": 1048576,
117
122
  "gpt-5.3": 1048576,
@@ -449,6 +454,10 @@ export function initPanels() {
449
454
  contextCacheWriteEl = $("context-cache-write");
450
455
  contextModelEl = $("context-model");
451
456
  contextCostEl = $("context-cost");
457
+ contextThinkingEl = $("context-thinking");
458
+ contextThinkingRow = $("context-thinking-row");
459
+ contextCostBasisEl = $("context-cost-basis");
460
+ contextCostBasisRow = $("context-cost-basis-row");
452
461
  contextTurnsEl = $("context-turns");
453
462
  contextMini = $("context-mini");
454
463
  contextMiniFill = $("context-mini-fill");
@@ -776,6 +785,16 @@ export function updateContextPanel() {
776
785
  contextCacheWriteEl.textContent = formatTokens(contextData.cacheWrite);
777
786
  contextModelEl.textContent = contextData.model;
778
787
  contextCostEl.textContent = "$" + contextData.cost.toFixed(4);
788
+ // Optional SDK fields: shown only once the CLI has actually reported them.
789
+ if (contextThinkingRow && contextThinkingEl) {
790
+ var hasThinking = typeof contextData.thinking === "number";
791
+ contextThinkingRow.classList.toggle("hidden", !hasThinking);
792
+ if (hasThinking) contextThinkingEl.textContent = formatTokens(contextData.thinking);
793
+ }
794
+ if (contextCostBasisRow && contextCostBasisEl) {
795
+ contextCostBasisRow.classList.toggle("hidden", !contextData.costBasis);
796
+ if (contextData.costBasis) contextCostBasisEl.textContent = contextData.costBasis;
797
+ }
779
798
  contextTurnsEl.textContent = String(contextData.turns);
780
799
  }
781
800
 
@@ -811,6 +830,29 @@ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens
811
830
  contextData.model = displayModel;
812
831
  contextData.contextWindow = resolveContextWindow(displayModel, mu.contextWindow);
813
832
  if (mu.maxOutputTokens) contextData.maxOutputTokens = mu.maxOutputTokens;
833
+
834
+ // thinkingTokens and costBasis are per-model and optional: absent on
835
+ // older CLI builds and, for a resumed session, on the turns that ran
836
+ // before the field existed. Leave them null in that case so the rows
837
+ // stay hidden rather than claiming a confident zero.
838
+ var thinkingTotal = null;
839
+ var basis = null;
840
+ var basisMixed = false;
841
+ for (var mi = 0; mi < models.length; mi++) {
842
+ var entry = modelUsage[models[mi]];
843
+ if (!entry) continue;
844
+ if (typeof entry.thinkingTokens === "number") {
845
+ thinkingTotal = (thinkingTotal || 0) + entry.thinkingTokens;
846
+ }
847
+ if (entry.costBasis) {
848
+ if (basis === null) basis = entry.costBasis;
849
+ else if (basis !== entry.costBasis) basisMixed = true;
850
+ }
851
+ }
852
+ contextData.thinking = thinkingTotal;
853
+ // Several models in one query can be priced differently; say so rather
854
+ // than reporting whichever happened to come first.
855
+ contextData.costBasis = basisMixed ? "mixed" : basis;
814
856
  }
815
857
  }
816
858
  if (!store.get('replayingHistory')) updateContextPanel();
@@ -832,7 +874,7 @@ export function applyContextView(view) {
832
874
  }
833
875
 
834
876
  export function resetContextData() {
835
- contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
877
+ contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0, thinking: null, costBasis: null };
836
878
  store.set({ richContextUsage: null });
837
879
  // hideCtxPopover + updateContextPanel handled by store subscriber
838
880
  }
@@ -19,7 +19,7 @@ import { exitDmMode } from './app-dm.js';
19
19
  import { isHomeHubVisible, hideHomeHub, showHomeHub } from './app-home-hub.js';
20
20
  import { closeArticle as closeWhatsNewArticle } from './whats-new-article.js';
21
21
  import { resetFileBrowser } from './filebrowser.js';
22
- import { closeArchive } from './sticky-notes.js';
22
+ import { closeNotesBrowser } from './sticky-notes-browser.js';
23
23
  import { hideMemory } from './mate-memory.js';
24
24
  import { isSchedulerOpen, closeScheduler, resetScheduler } from './scheduler.js';
25
25
  import { connect, cancelReconnect, setStatus } from './app-connection.js';
@@ -34,6 +34,8 @@ import { resetRateLimitState } from './app-rate-limit.js';
34
34
  import { closeSessionInfoPopover } from './app-header.js';
35
35
  import { resetDebateState } from './debate.js';
36
36
  import { removeDebateBottomBar } from './app-debate-ui.js';
37
+ import { closeProjectSettings } from './project-settings.js';
38
+ import { chooseProjectAfterRemoval } from './project-removal-target.js';
37
39
 
38
40
  // --- Module-owned state ---
39
41
  var cachedProjects = [];
@@ -415,13 +417,15 @@ export function switchProject(slug) {
415
417
  if (!slug) return;
416
418
  document.cookie = "clay_last_project=" + encodeURIComponent(slug) + "; Path=/; SameSite=Strict; Max-Age=31536000";
417
419
  var st = store.snap();
420
+ var targetWsPath = "/p/" + slug + "/ws";
421
+ var alreadyInProject = slug === st.currentSlug && st.wsPath === targetWsPath;
418
422
  var wasDm = st.dmMode;
419
423
  var wasMate = st.dmMode && st.dmTargetUser && st.dmTargetUser.isMate;
420
424
  if (st.dmMode) exitDmMode(wasMate);
421
425
  closeWhatsNewArticle();
422
426
  if (isHomeHubVisible()) {
423
427
  hideHomeHub();
424
- if (slug === store.get('currentSlug')) {
428
+ if (alreadyInProject) {
425
429
  if (document.documentElement.classList.contains("pwa-standalone")) {
426
430
  history.replaceState(null, "", "/p/" + slug + "/");
427
431
  } else {
@@ -430,7 +434,7 @@ export function switchProject(slug) {
430
434
  return;
431
435
  }
432
436
  }
433
- if (slug === store.get('currentSlug')) {
437
+ if (alreadyInProject) {
434
438
  var ws = getWs();
435
439
  if (wasDm && ws && ws.readyState === 1) {
436
440
  ws.send(JSON.stringify({ type: "switch_session", id: store.get('activeSessionId') }));
@@ -438,13 +442,13 @@ export function switchProject(slug) {
438
442
  return;
439
443
  }
440
444
  resetFileBrowser();
441
- closeArchive();
445
+ closeNotesBrowser();
442
446
  hideMemory();
443
447
  if (isSchedulerOpen()) closeScheduler();
444
448
  resetScheduler(slug);
445
449
  store.set({ currentSlug: slug });
446
450
  store.set({ basePath: "/p/" + slug + "/" });
447
- store.set({ wsPath: "/p/" + slug + "/ws" });
451
+ store.set({ wsPath: targetWsPath });
448
452
  if (document.documentElement.classList.contains("pwa-standalone")) {
449
453
  history.replaceState(null, "", "/p/" + slug + "/");
450
454
  } else {
@@ -572,9 +576,10 @@ export function handleRemoveProjectResult(msg) {
572
576
  var currentSlug = store.get('currentSlug');
573
577
  if (msg.slug === currentSlug) {
574
578
  var isWorktree = msg.slug.indexOf("--") !== -1;
575
- var parentSlug = isWorktree ? msg.slug.split("--")[0] : null;
579
+ var targetSlug = chooseProjectAfterRemoval(cachedProjects, msg.slug);
576
580
 
577
581
  showToast(isWorktree ? "Worktree removed" : "Project removed", "success");
582
+ closeProjectSettings();
578
583
 
579
584
  // Suppress disconnect overlay and reconnect by detaching the WS
580
585
  var ws = getWs();
@@ -602,8 +607,8 @@ export function handleRemoveProjectResult(msg) {
602
607
  renderProjectList();
603
608
  resetClientState();
604
609
 
605
- if (parentSlug) {
606
- switchProject(parentSlug);
610
+ if (targetSlug) {
611
+ switchProject(targetSlug);
607
612
  } else {
608
613
  showHomeHub();
609
614
  }
@@ -111,6 +111,7 @@ export function finalizeAssistantBlock() {
111
111
 
112
112
  export function showClaudePreThinking() {
113
113
  if (getChatLayout() !== "channel") return;
114
+ removeMatePreThinking();
114
115
  var vendor = store.get('currentVendor') || "claude";
115
116
  var vendorAvatar = VENDOR_AVATARS[vendor] || VENDOR_AVATARS.claude;
116
117
  var vendorName = VENDOR_NAMES[vendor] || VENDOR_NAMES.claude;
@@ -1,55 +1,178 @@
1
+ // background-tasks-ui.js - Background task status line above the composer.
2
+ //
3
+ // Collapsed it reads as a quiet status line, not a button demanding attention:
4
+ // a muted activity indicator, a count, and a chevron. Expanding lists each task
5
+ // with its elapsed time and a Stop control.
6
+ //
7
+ // Elapsed time comes from `started_at` (epoch ms) which the server stamps in
8
+ // lib/background-task-timing.js. The ticker only runs while the list is
9
+ // expanded, so a collapsed bar costs nothing.
10
+
1
11
  import { store } from './store.js';
2
12
  import { getWs } from './ws-ref.js';
3
13
  import { escapeHtml } from './utils.js';
14
+ import { iconHtml, refreshIcons } from './icons.js';
4
15
 
5
16
  var expanded = false;
17
+ var elapsedTimer = null;
18
+
19
+ // Lucide icon per vendor-neutral task type (see normalizeBackgroundTasks in
20
+ // yoke/adapters/claude.js for where these types are assigned).
21
+ var TASK_TYPE_ICONS = { shell: "terminal", agent: "sparkles", other: "circle-dot" };
6
22
 
7
23
  export function initBackgroundTasksUi() {
8
- store.subscribe(function(state, prev) {
24
+ store.subscribe(function (state, prev) {
9
25
  if (state.activeBackgroundTasks !== prev.activeBackgroundTasks) renderBackgroundTasks();
10
26
  });
11
27
  renderBackgroundTasks();
12
28
  }
13
29
 
30
+ // "45s", "2m 14s", "1h 03m" - short enough to sit in a row without wrapping.
31
+ export function formatElapsed(startedAt, now) {
32
+ if (typeof startedAt !== "number" || !isFinite(startedAt) || startedAt <= 0) return "";
33
+ var current = typeof now === "number" ? now : Date.now();
34
+ var totalSeconds = Math.floor((current - startedAt) / 1000);
35
+ if (totalSeconds < 0) totalSeconds = 0;
36
+ if (totalSeconds < 60) return totalSeconds + "s";
37
+ var minutes = Math.floor(totalSeconds / 60);
38
+ var seconds = totalSeconds % 60;
39
+ if (minutes < 60) return minutes + "m " + pad(seconds) + "s";
40
+ var hours = Math.floor(minutes / 60);
41
+ return hours + "h " + pad(minutes % 60) + "m";
42
+ }
43
+
44
+ function pad(value) {
45
+ return value < 10 ? "0" + value : String(value);
46
+ }
47
+
48
+ function stopElapsedTimer() {
49
+ if (!elapsedTimer) return;
50
+ clearInterval(elapsedTimer);
51
+ elapsedTimer = null;
52
+ }
53
+
54
+ // Ticks at most once a second and only touches the time cells, so expanding
55
+ // the list never re-renders (and never steals focus from) the rows.
56
+ function startElapsedTimer() {
57
+ stopElapsedTimer();
58
+ elapsedTimer = setInterval(updateElapsedCells, 1000);
59
+ }
60
+
61
+ function updateElapsedCells() {
62
+ var bar = document.getElementById('background-tasks-bar');
63
+ if (!bar || !expanded) {
64
+ stopElapsedTimer();
65
+ return;
66
+ }
67
+ var now = Date.now();
68
+ var cells = bar.querySelectorAll('.background-task-elapsed');
69
+ for (var i = 0; i < cells.length; i++) {
70
+ var startedAt = Number(cells[i].dataset.startedAt);
71
+ cells[i].textContent = formatElapsed(startedAt, now);
72
+ }
73
+ }
74
+
75
+ function activityDotsHtml() {
76
+ return '<span class="background-tasks-activity" aria-hidden="true"><i></i><i></i><i></i></span>';
77
+ }
78
+
79
+ // Ambient tasks are the CLI's own housekeeping. The SDK asks hosts to keep
80
+ // them out of activity indicators; Clay still lists them so the user can see
81
+ // what runs on their behalf. Absent means false (Codex has no ambient concept,
82
+ // and older CLI builds report only user work).
83
+ export function isAmbientTask(task) {
84
+ return !!(task && task.ambient === true);
85
+ }
86
+
87
+ export function splitTasks(tasks) {
88
+ var user = [];
89
+ var ambient = [];
90
+ var list = Array.isArray(tasks) ? tasks : [];
91
+ for (var i = 0; i < list.length; i++) {
92
+ if (!list[i]) continue;
93
+ if (isAmbientTask(list[i])) ambient.push(list[i]);
94
+ else user.push(list[i]);
95
+ }
96
+ return { user: user, ambient: ambient };
97
+ }
98
+
99
+ function taskRowHtml(task, now) {
100
+ var taskType = task.task_type || 'other';
101
+ var icon = TASK_TYPE_ICONS[taskType] || TASK_TYPE_ICONS.other;
102
+ var description = task.description || 'Background task';
103
+ var startedAt = typeof task.started_at === 'number' ? task.started_at : 0;
104
+ var elapsed = formatElapsed(startedAt, now);
105
+ return '<div class="background-task-row' + (isAmbientTask(task) ? ' ambient' : '') + '">' +
106
+ '<span class="background-task-icon" title="' + escapeHtml(taskType) + '">' + iconHtml(icon) + '</span>' +
107
+ '<span class="background-task-description" title="' + escapeHtml(description) + '">' +
108
+ escapeHtml(description) + '</span>' +
109
+ '<span class="background-task-meta">' +
110
+ '<span class="background-task-type">' + escapeHtml(taskType) + '</span>' +
111
+ '<span class="background-task-elapsed" data-started-at="' + startedAt + '">' + escapeHtml(elapsed) + '</span>' +
112
+ '</span>' +
113
+ '<button type="button" class="background-task-stop" data-task-id="' + escapeHtml(task.task_id || '') + '" ' +
114
+ 'aria-label="Stop ' + escapeHtml(description) + '">Stop</button>' +
115
+ '</div>';
116
+ }
117
+
14
118
  function renderBackgroundTasks() {
15
119
  var tasks = store.get('activeBackgroundTasks') || [];
120
+ var split = splitTasks(tasks);
16
121
  var existing = document.getElementById('background-tasks-bar');
17
- if (tasks.length === 0) {
122
+ // Ambient-only means nothing the user asked for is running, so the composer
123
+ // stays quiet: no bar, no dots, no count.
124
+ if (split.user.length === 0) {
18
125
  if (existing) existing.remove();
19
126
  expanded = false;
127
+ stopElapsedTimer();
20
128
  return;
21
129
  }
22
130
  var inputArea = document.getElementById('input-area');
23
131
  if (!inputArea || !inputArea.parentNode) return;
132
+
24
133
  var bar = existing || document.createElement('div');
25
134
  bar.id = 'background-tasks-bar';
26
- bar.className = 'background-tasks-bar';
27
- var taskLabel = tasks.length === 1 ? 'background task' : 'background tasks';
28
- var html = '<button type="button" class="background-tasks-toggle" aria-expanded="' + expanded + '">' +
29
- '<span aria-hidden="true">⏳</span><span class="background-tasks-chip">' + tasks.length + '</span>' +
30
- '<span>' + tasks.length + ' ' + taskLabel + '</span></button>';
135
+ bar.className = 'background-tasks-bar' + (expanded ? ' expanded' : '');
136
+
137
+ var taskLabel = split.user.length === 1 ? 'background task' : 'background tasks';
138
+ var summary = split.user.length + ' ' + taskLabel;
139
+ var html = '<button type="button" class="background-tasks-toggle" aria-expanded="' + expanded + '"' +
140
+ ' aria-controls="background-tasks-list" aria-label="' + summary + ', ' +
141
+ (expanded ? 'collapse' : 'expand') + ' details">' +
142
+ activityDotsHtml() +
143
+ '<span class="background-tasks-summary">' + summary + '</span>' +
144
+ '<span class="background-tasks-chevron" aria-hidden="true">' + iconHtml('chevron-right') + '</span>' +
145
+ '</button>';
146
+
31
147
  if (expanded) {
32
- html += '<div class="background-tasks-list">';
33
- for (var i = 0; i < tasks.length; i++) {
34
- var task = tasks[i];
35
- html += '<div class="background-task-row"><span class="background-task-description" title="' + escapeHtml(task.description || '') + '">' +
36
- escapeHtml(task.description || 'Background task') + '</span><span class="background-task-type">' +
37
- escapeHtml(task.task_type || 'other') + '</span><button type="button" class="background-task-stop" data-task-id="' +
38
- escapeHtml(task.task_id || '') + '">Stop</button></div>';
148
+ var now = Date.now();
149
+ html += '<div class="background-tasks-list" id="background-tasks-list">';
150
+ for (var i = 0; i < split.user.length; i++) html += taskRowHtml(split.user[i], now);
151
+ // Housekeeping is shown, not hidden: the user can see what Clay runs on
152
+ // its own and stop a runaway one. Kept visually subordinate to real work.
153
+ if (split.ambient.length > 0) {
154
+ html += '<div class="background-tasks-section-label">Housekeeping</div>';
155
+ for (var a = 0; a < split.ambient.length; a++) html += taskRowHtml(split.ambient[a], now);
39
156
  }
40
157
  html += '</div>';
41
158
  }
42
159
  bar.innerHTML = html;
43
160
  if (!existing) inputArea.parentNode.insertBefore(bar, inputArea);
44
- bar.querySelector('.background-tasks-toggle').addEventListener('click', function() {
161
+ refreshIcons();
162
+
163
+ bar.querySelector('.background-tasks-toggle').addEventListener('click', function () {
45
164
  expanded = !expanded;
46
165
  renderBackgroundTasks();
47
166
  });
167
+
48
168
  var stopButtons = bar.querySelectorAll('.background-task-stop');
49
169
  for (var j = 0; j < stopButtons.length; j++) {
50
- stopButtons[j].addEventListener('click', function() {
170
+ stopButtons[j].addEventListener('click', function () {
51
171
  var ws = getWs();
52
172
  if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'stop_task', taskId: this.dataset.taskId }));
53
173
  });
54
174
  }
175
+
176
+ if (expanded) startElapsedTimer();
177
+ else stopElapsedTimer();
55
178
  }
@@ -0,0 +1,44 @@
1
+ import { store } from './store.js';
2
+ import { requestTools } from './home-tools.js';
3
+ import { closeHomeDock } from './home-dock.js';
4
+ import { showToast } from './utils.js';
5
+
6
+ function applyCapsulesEnabled(enabled, refreshTools) {
7
+ var want = enabled === true;
8
+ store.set({ capsulesEnabled: want, capsulesPreferenceLoaded: true });
9
+ if (document.body) document.body.classList.toggle('capsules-disabled', !want);
10
+ if (!want && store.get('dockOpen') === true) closeHomeDock();
11
+ if (refreshTools !== false) requestTools();
12
+ }
13
+
14
+ export function initCapsulePreference() {
15
+ var toggle = document.getElementById('us-capsules-enabled');
16
+ if (!toggle) return;
17
+ toggle.addEventListener('change', function () {
18
+ var previous = store.get('capsulesEnabled') === true;
19
+ var want = this.checked === true;
20
+ applyCapsulesEnabled(want, false);
21
+ fetch('/api/user/capsules-enabled', {
22
+ method: 'PUT',
23
+ headers: { 'Content-Type': 'application/json' },
24
+ body: JSON.stringify({ enabled: want }),
25
+ }).then(function (response) {
26
+ if (!response.ok) throw new Error('Could not save the Capsules preference.');
27
+ return response.json();
28
+ }).then(function (data) {
29
+ applyCapsulesEnabled(data.capsulesEnabled === true, true);
30
+ showToast(data.capsulesEnabled ? 'Experimental Capsules enabled' : 'Experimental Capsules disabled');
31
+ }).catch(function () {
32
+ toggle.checked = previous;
33
+ applyCapsulesEnabled(previous, true);
34
+ showToast('Could not save the Capsules preference.', 'error');
35
+ });
36
+ });
37
+ }
38
+
39
+ export function syncCapsulePreference(enabled) {
40
+ var want = enabled === true;
41
+ var toggle = document.getElementById('us-capsules-enabled');
42
+ if (toggle) toggle.checked = want;
43
+ applyCapsulesEnabled(want, true);
44
+ }
@@ -105,7 +105,7 @@ export function addAssistantCopyHandler(row, rawText) {
105
105
  }
106
106
 
107
107
  row.addEventListener("click", function (event) {
108
- if (event.target.closest("a, pre, code")) return;
108
+ if (event.target.closest("a, button, pre, code")) return;
109
109
  var selection = window.getSelection();
110
110
  if (selection && selection.toString().length > 0) return;
111
111
  if (!primed) {
@@ -0,0 +1,70 @@
1
+ export function parseClayLogReferences(text) {
2
+ var pattern = /\[clayos\/(log:[A-Za-z0-9_-]{24})(?:\s+[\u2014-]\s+([^\]\n]{1,80}))?\]|(^|[^A-Za-z0-9_/:])(log:[A-Za-z0-9_-]{24})(?![A-Za-z0-9_-])/g;
3
+ var results = [];
4
+ var match;
5
+ while ((match = pattern.exec(text || ""))) {
6
+ results.push({
7
+ start: match.index,
8
+ end: pattern.lastIndex,
9
+ prefix: match[3] || "",
10
+ ref: match[1] || match[4],
11
+ label: match[2] ? match[2].trim() : "",
12
+ });
13
+ }
14
+ return results;
15
+ }
16
+
17
+ export function isExactClayLogReference(text) {
18
+ return /^log:[A-Za-z0-9_-]{24}$/.test((text || "").trim());
19
+ }
20
+
21
+ function createLogLink(ref, labelText) {
22
+ var button = document.createElement("button");
23
+ button.type = "button";
24
+ button.className = "clayos-log-link";
25
+ button.contentEditable = "false";
26
+ button.dataset.logRef = ref;
27
+ button.setAttribute("aria-label", "Open Project Log" + (labelText ? " " + labelText : ""));
28
+ var label = document.createElement("span");
29
+ label.textContent = "Log";
30
+ button.appendChild(label);
31
+ if (labelText) {
32
+ var meta = document.createElement("small");
33
+ meta.textContent = labelText;
34
+ button.appendChild(meta);
35
+ }
36
+ return button;
37
+ }
38
+
39
+ export function enhanceClayLogLinks(root) {
40
+ var codeNodes = root.querySelectorAll("code");
41
+ for (var codeIndex = 0; codeIndex < codeNodes.length; codeIndex++) {
42
+ var code = codeNodes[codeIndex];
43
+ var codeText = (code.textContent || "").trim();
44
+ if (code.closest("pre") || !isExactClayLogReference(codeText)) continue;
45
+ code.parentNode.replaceChild(createLogLink(codeText, ""), code);
46
+ }
47
+
48
+ var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
49
+ var nodes = [];
50
+ var node;
51
+ while ((node = walker.nextNode())) {
52
+ if (!node.parentElement || node.parentElement.closest("code, pre, a, button")) continue;
53
+ if (parseClayLogReferences(node.nodeValue).length) nodes.push(node);
54
+ }
55
+ for (var i = 0; i < nodes.length; i++) {
56
+ var text = nodes[i].nodeValue || "";
57
+ var matches = parseClayLogReferences(text);
58
+ var fragment = document.createDocumentFragment();
59
+ var offset = 0;
60
+ for (var j = 0; j < matches.length; j++) {
61
+ var match = matches[j];
62
+ fragment.appendChild(document.createTextNode(text.slice(offset, match.start)));
63
+ if (match.prefix) fragment.appendChild(document.createTextNode(match.prefix));
64
+ fragment.appendChild(createLogLink(match.ref, match.label));
65
+ offset = match.end;
66
+ }
67
+ fragment.appendChild(document.createTextNode(text.slice(offset)));
68
+ nodes[i].parentNode.replaceChild(fragment, nodes[i]);
69
+ }
70
+ }