clay-server 4.1.0-beta.9 → 4.1.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 (111) hide show
  1. package/lib/context-usage.js +47 -0
  2. package/lib/issues-mcp-server.js +1 -1
  3. package/lib/issues-service.js +3 -1
  4. package/lib/loop-registry-occurrence.js +1 -0
  5. package/lib/notes.js +20 -3
  6. package/lib/os-users.js +6 -0
  7. package/lib/project-connection.js +14 -3
  8. package/lib/project-file-path.js +13 -0
  9. package/lib/project-file-watch.js +19 -8
  10. package/lib/project-filesystem.js +10 -16
  11. package/lib/project-http.js +53 -48
  12. package/lib/project-logs-mcp-server.js +8 -8
  13. package/lib/project-logs-schema.js +1 -1
  14. package/lib/project-logs-service.js +46 -13
  15. package/lib/project-logs-store.js +17 -15
  16. package/lib/project-logs.js +70 -0
  17. package/lib/project-loop.js +1 -0
  18. package/lib/project-notifications.js +1 -1
  19. package/lib/project-scheduled-result-store.js +131 -0
  20. package/lib/project-scheduled-task-execution.js +35 -2
  21. package/lib/project-session-notes.js +11 -4
  22. package/lib/project-sessions.js +7 -0
  23. package/lib/project-user-message.js +47 -0
  24. package/lib/project.js +53 -3
  25. package/lib/public/app.js +26 -9
  26. package/lib/public/css/avatar-imprints.css +1 -1
  27. package/lib/public/css/command-palette.css +20 -0
  28. package/lib/public/css/git-panel.css +13 -12
  29. package/lib/public/css/git-placard.css +4 -4
  30. package/lib/public/css/issues.css +78 -31
  31. package/lib/public/css/mobile-nav.css +7 -0
  32. package/lib/public/css/notifications-center.css +3 -3
  33. package/lib/public/css/project-logs.css +15 -0
  34. package/lib/public/css/sidebar.css +7 -0
  35. package/lib/public/css/sticky-notes.css +29 -0
  36. package/lib/public/modules/app-connection.js +9 -1
  37. package/lib/public/modules/app-favicon.js +10 -0
  38. package/lib/public/modules/app-messages.js +43 -12
  39. package/lib/public/modules/app-notifications.js +2 -0
  40. package/lib/public/modules/app-panels.js +77 -23
  41. package/lib/public/modules/app-projects.js +2 -0
  42. package/lib/public/modules/app-rate-limit.js +148 -61
  43. package/lib/public/modules/avatar-imprint.js +25 -70
  44. package/lib/public/modules/avatar.js +12 -4
  45. package/lib/public/modules/chat-render-runtime.js +20 -0
  46. package/lib/public/modules/clay-issue-links.js +23 -2
  47. package/lib/public/modules/clay-log-links.js +11 -1
  48. package/lib/public/modules/context-view-preference.js +143 -0
  49. package/lib/public/modules/default-vendor.js +116 -0
  50. package/lib/public/modules/filebrowser-tabs.js +1 -0
  51. package/lib/public/modules/filebrowser.js +45 -18
  52. package/lib/public/modules/issues-render.js +36 -10
  53. package/lib/public/modules/issues.js +95 -13
  54. package/lib/public/modules/markdown.js +3 -1
  55. package/lib/public/modules/pane-bridge.js +14 -8
  56. package/lib/public/modules/pane-issue-bridge.js +27 -0
  57. package/lib/public/modules/permission-hints.js +13 -0
  58. package/lib/public/modules/profile.js +9 -9
  59. package/lib/public/modules/project-activation.js +49 -0
  60. package/lib/public/modules/project-logs-render.js +13 -3
  61. package/lib/public/modules/project-logs.js +45 -15
  62. package/lib/public/modules/right-workbench.js +44 -0
  63. package/lib/public/modules/scheduled-result-client.js +33 -0
  64. package/lib/public/modules/scheduled-result-display.js +50 -0
  65. package/lib/public/modules/scheduled-result-notification.js +31 -0
  66. package/lib/public/modules/scheduled-task-presentation.js +15 -0
  67. package/lib/public/modules/scheduled-tasks.js +9 -21
  68. package/lib/public/modules/sidebar-mobile.js +51 -3
  69. package/lib/public/modules/sidebar-sessions.js +65 -5
  70. package/lib/public/modules/split-session-boundary.js +28 -0
  71. package/lib/public/modules/split-view.js +42 -7
  72. package/lib/public/modules/sticky-notes-browser.js +130 -18
  73. package/lib/public/modules/sticky-notes.js +2 -3
  74. package/lib/public/modules/terminal.js +4 -4
  75. package/lib/public/modules/thinking-lifecycle.js +4 -1
  76. package/lib/public/modules/tools.js +5 -1
  77. package/lib/scheduled-run-state.js +10 -2
  78. package/lib/scheduler.js +1 -0
  79. package/lib/sdk-bridge.js +97 -61
  80. package/lib/sdk-message-processor.js +44 -13
  81. package/lib/sdk-skill-discovery.js +6 -28
  82. package/lib/sdk-turn-queue.js +14 -0
  83. package/lib/server-context-view.js +53 -0
  84. package/lib/server-default-vendor.js +60 -0
  85. package/lib/server.js +23 -6
  86. package/lib/session-notes-mcp-server.js +3 -5
  87. package/lib/session-pair-factory.js +2 -2
  88. package/lib/session-title-generator.js +89 -0
  89. package/lib/session-title-policy.js +8 -2
  90. package/lib/sessions.js +26 -1
  91. package/lib/user-presence.js +6 -0
  92. package/lib/users-context-view-preferences.js +65 -0
  93. package/lib/users-default-vendor-preferences.js +80 -0
  94. package/lib/users.js +12 -0
  95. package/lib/ws-schema.js +11 -0
  96. package/lib/yoke/acp-agent-profiles.js +15 -2
  97. package/lib/yoke/adapters/acp.js +3 -2
  98. package/lib/yoke/adapters/antigravity.js +2 -2
  99. package/lib/yoke/adapters/claude-worker.js +11 -12
  100. package/lib/yoke/adapters/claude.js +75 -22
  101. package/lib/yoke/adapters/codex.js +216 -29
  102. package/lib/yoke/adapters/kiro.js +6 -5
  103. package/lib/yoke/claude-context-usage.js +57 -0
  104. package/lib/yoke/claude-message-correlation.js +31 -0
  105. package/lib/yoke/claude-model-switch.js +15 -0
  106. package/lib/yoke/index.js +5 -1
  107. package/lib/yoke/mcp-bridge-server.js +18 -5
  108. package/lib/yoke/session-skill-catalog.js +45 -0
  109. package/lib/yoke/skill-discovery.js +60 -20
  110. package/lib/yoke/vendor-registry.js +1 -1
  111. package/package.json +5 -7
@@ -15,6 +15,7 @@ import {
15
15
  startNewSession
16
16
  } from './sidebar-sessions.js';
17
17
  import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES, isExperimentalVendor } from './app-rendering.js';
18
+ import { defaultVendorUnavailable, resolvePreferredVendor, saveDefaultVendor } from './default-vendor.js';
18
19
  import {
19
20
  getCachedProjectList,
20
21
  getCachedCurrentSlug,
@@ -36,7 +37,7 @@ import { showHomeHub } from './app-home-hub.js';
36
37
  import { openHomeChat } from './home-mate-chat.js';
37
38
  import { openTerminal } from './terminal.js';
38
39
  import { requestKnowledgeList } from './mate-knowledge.js';
39
- import { loadRootDirectory } from './filebrowser.js';
40
+ import { loadRootDirectory, reopenFileViewer } from './filebrowser.js';
40
41
  import {
41
42
  prepareSidebarHierarchy,
42
43
  renderMobileDriverHierarchy,
@@ -91,6 +92,7 @@ export function openMobileSheet(type) {
91
92
  listEl.appendChild(fileTree);
92
93
  fileTree.classList.remove("hidden");
93
94
  }
95
+ reopenFileViewer();
94
96
  loadRootDirectory();
95
97
  } else if (type === "mate-knowledge") {
96
98
  titleEl.textContent = "Knowledge";
@@ -674,14 +676,17 @@ function renderMobileSessionsInto(container) {
674
676
  // the project's last-used vendor, plus a chevron that expands the full
675
677
  // vendor list. The list expands inline rather than as a fixed-position
676
678
  // popover because it lives inside a scrolling bottom sheet.
677
- var mobileDefaultVendor = resolveDefaultVendor();
679
+ var mobileDefaultVendor = resolvePreferredVendor(store.get('installedVendors') || []) || resolveDefaultVendor();
678
680
  var mobileInstalled = store.get('installedVendors') || [];
681
+ var mobileDefaultState = store.get('defaultVendorState') || {};
679
682
 
680
683
  var newRow = document.createElement("div");
681
684
  newRow.className = "mobile-session-new-row";
682
685
 
683
686
  var mainBtn = document.createElement("button");
684
687
  mainBtn.className = "mobile-session-new mobile-session-new-main";
688
+ mainBtn.title = "New " + (VENDOR_NAMES[mobileDefaultVendor] || mobileDefaultVendor) + " session";
689
+ if (defaultVendorUnavailable(mobileInstalled)) mainBtn.title += " (saved default is unavailable; using installed priority)";
685
690
  mainBtn.innerHTML = '<img src="' + (VENDOR_AVATARS[mobileDefaultVendor] || VENDOR_AVATARS.claude) +
686
691
  '" class="mobile-session-new-icon" alt=""><span>New ' + (VENDOR_NAMES[mobileDefaultVendor] || mobileDefaultVendor) + ' session</span>';
687
692
  mainBtn.addEventListener("click", function () {
@@ -703,12 +708,20 @@ function renderMobileSessionsInto(container) {
703
708
  });
704
709
  newRow.appendChild(chevronBtn);
705
710
 
711
+ if (defaultVendorUnavailable(mobileInstalled) && mobileDefaultState.preference) {
712
+ var mobileFallbackNote = document.createElement("div");
713
+ mobileFallbackNote.className = "mobile-session-new-preference-status mobile-session-new-fallback-note";
714
+ mobileFallbackNote.textContent = "Saved default " + (VENDOR_NAMES[mobileDefaultState.preference] || mobileDefaultState.preference) + " is unavailable. Using " + (VENDOR_NAMES[mobileDefaultVendor] || mobileDefaultVendor) + ".";
715
+ }
716
+
706
717
  // Every known vendor is listed. Uninstalled ones stay visible but disabled,
707
718
  // and tapping one opens the vendor's homepage.
708
719
  for (var vi = 0; vi < VENDOR_ORDER.length; vi++) {
709
720
  (function (vendor) {
710
721
  var isInstalled = mobileInstalled.indexOf(vendor) !== -1;
711
722
  var name = VENDOR_NAMES[vendor] || vendor;
723
+ var vendorRow = document.createElement("div");
724
+ vendorRow.className = "mobile-vendor-row";
712
725
  var vBtn = document.createElement("button");
713
726
  vBtn.className = "mobile-session-new mobile-session-new-vendor";
714
727
  if (!isInstalled) vBtn.classList.add("disabled");
@@ -725,11 +738,46 @@ function renderMobileSessionsInto(container) {
725
738
  startNewSession(vendor);
726
739
  closeMobileSheet();
727
740
  });
728
- vendorList.appendChild(vBtn);
741
+ vendorRow.appendChild(vBtn);
742
+ if (isInstalled) {
743
+ var defaultBtn = document.createElement("button");
744
+ defaultBtn.type = "button";
745
+ defaultBtn.className = "mobile-vendor-set-default";
746
+ defaultBtn.textContent = mobileDefaultState.saving ? "Saving…" : (vendor === mobileDefaultVendor && !defaultVendorUnavailable(mobileInstalled) ? "Default" : "Set default");
747
+ defaultBtn.disabled = mobileDefaultState.saving === true;
748
+ defaultBtn.setAttribute("aria-label", "Set " + name + " as default vendor");
749
+ defaultBtn.addEventListener("click", function (e) {
750
+ e.preventDefault();
751
+ e.stopPropagation();
752
+ saveDefaultVendor(vendor);
753
+ });
754
+ vendorRow.appendChild(defaultBtn);
755
+ }
756
+ vendorList.appendChild(vendorRow);
729
757
  })(VENDOR_ORDER[vi]);
730
758
  }
731
759
 
760
+ var automaticBtn = document.createElement("button");
761
+ automaticBtn.className = "mobile-session-new mobile-session-new-vendor mobile-session-new-automatic";
762
+ automaticBtn.type = "button";
763
+ automaticBtn.textContent = "Use automatic default";
764
+ automaticBtn.disabled = mobileDefaultState.saving === true;
765
+ automaticBtn.addEventListener("click", function (e) {
766
+ e.preventDefault();
767
+ e.stopPropagation();
768
+ saveDefaultVendor("");
769
+ });
770
+ vendorList.appendChild(automaticBtn);
771
+ if (mobileDefaultState.error) {
772
+ var mobileStatus = document.createElement("div");
773
+ mobileStatus.className = "mobile-session-new-preference-status";
774
+ mobileStatus.setAttribute("role", "alert");
775
+ mobileStatus.textContent = mobileDefaultState.error;
776
+ vendorList.appendChild(mobileStatus);
777
+ }
778
+
732
779
  container.appendChild(newRow);
780
+ if (mobileFallbackNote) container.appendChild(mobileFallbackNote);
733
781
  container.appendChild(vendorList);
734
782
 
735
783
  // Partition: loop sessions vs normal sessions (same logic as desktop renderSessionList)
@@ -15,6 +15,7 @@ import { refreshMobileChatSheet } from './sidebar-mobile.js';
15
15
  import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES, isExperimentalVendor } from './app-rendering.js';
16
16
  import { openGroup, separateGroup } from './split-view.js';
17
17
  import { groupedSessionIds } from './split-group-helpers.js';
18
+ import { defaultVendorUnavailable, resolvePreferredVendor, saveDefaultVendor } from './default-vendor.js';
18
19
  import { openPairDialog } from './split-pair-ui.js';
19
20
  import {
20
21
  hierarchyItemMatches,
@@ -41,6 +42,7 @@ var countdownContainer = null;
41
42
 
42
43
  // --- Session context menu ---
43
44
  var sessionCtxMenu = null;
45
+ var sessionCtxMenuAnchor = null;
44
46
  var sessionCtxSessionId = null;
45
47
  var draggedSessionId = null;
46
48
  var draggedSessionBookmarked = false;
@@ -48,6 +50,18 @@ var headerSearchOpen = false;
48
50
  var armedDeleteSessionId = null;
49
51
  var armedDeleteTimer = null;
50
52
 
53
+ store.subscribe(function (state, previous) {
54
+ if (state.defaultVendorState !== previous.defaultVendorState || state.installedVendors !== previous.installedVendors) {
55
+ var reopenNewMenu = !!(sessionCtxMenu && sessionCtxMenuAnchor);
56
+ if (reopenNewMenu) closeSessionCtxMenu();
57
+ if (getSessionListEl()) renderSessionList();
58
+ if (reopenNewMenu) {
59
+ var freshAnchor = getSessionListEl().querySelector(".session-top-action.split-chevron");
60
+ if (freshAnchor) showNewSessionMenu(freshAnchor, resolveDefaultVendor());
61
+ }
62
+ }
63
+ });
64
+
51
65
  function sendSessionBookmark(sessionId, bookmarked) {
52
66
  if (getWs() && store.get('connected')) {
53
67
  getWs().send(JSON.stringify({ type: "set_session_bookmark", sessionId: sessionId, bookmarked: !!bookmarked }));
@@ -317,10 +331,7 @@ function appendSessionCloseButton(el, session) {
317
331
  // installed-vendor priority (Claude, then Codex, then the remaining vendors).
318
332
  export function resolveDefaultVendor() {
319
333
  var installed = store.get('installedVendors') || [];
320
- for (var i = 0; i < VENDOR_ORDER.length; i++) {
321
- if (installed.indexOf(VENDOR_ORDER[i]) !== -1) return VENDOR_ORDER[i];
322
- }
323
- return "claude";
334
+ return resolvePreferredVendor(installed) || "claude";
324
335
  }
325
336
 
326
337
  // Single entry point for session creation from the sidebar. Optimistically
@@ -361,6 +372,7 @@ function renderSessionTopActions() {
361
372
  mainBtn.className = "session-top-action split-main";
362
373
  mainBtn.type = "button";
363
374
  mainBtn.title = "New " + (VENDOR_NAMES[defaultVendor] || defaultVendor) + " session";
375
+ if (defaultVendorUnavailable(store.get('installedVendors') || [])) mainBtn.title += " (saved default is unavailable; using installed priority)";
364
376
  mainBtn.innerHTML = '<img src="' + (VENDOR_AVATARS[defaultVendor] || VENDOR_AVATARS.claude) +
365
377
  '" class="session-top-action-icon" alt=""><span>New session</span>';
366
378
  mainBtn.addEventListener("click", function () {
@@ -394,6 +406,15 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
394
406
  var menu = document.createElement("div");
395
407
  menu.className = "session-ctx-menu session-new-menu";
396
408
  var installed = store.get('installedVendors') || [];
409
+ var defaultVendorState = store.get('defaultVendorState') || {};
410
+ sessionCtxMenuAnchor = anchorBtn;
411
+
412
+ if (defaultVendorUnavailable(installed) && defaultVendorState.preference) {
413
+ var fallbackNote = document.createElement("div");
414
+ fallbackNote.className = "session-new-preference-status session-new-fallback-note";
415
+ fallbackNote.textContent = "Saved default " + (VENDOR_NAMES[defaultVendorState.preference] || defaultVendorState.preference) + " is unavailable. Using " + (VENDOR_NAMES[defaultVendor] || defaultVendor) + ".";
416
+ menu.appendChild(fallbackNote);
417
+ }
397
418
 
398
419
  // Every known vendor is listed. Uninstalled ones stay visible but disabled,
399
420
  // and clicking one opens the vendor's homepage so the user can install it.
@@ -401,6 +422,8 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
401
422
  (function (vendor) {
402
423
  var isInstalled = installed.indexOf(vendor) !== -1;
403
424
  var name = VENDOR_NAMES[vendor] || vendor;
425
+ var row = document.createElement("div");
426
+ row.className = "session-new-vendor-row";
404
427
  var item = document.createElement("button");
405
428
  item.className = "session-ctx-item session-new-vendor";
406
429
  if (!isInstalled) item.classList.add("disabled");
@@ -423,10 +446,46 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
423
446
  }
424
447
  startNewSession(vendor);
425
448
  });
426
- menu.appendChild(item);
449
+ row.appendChild(item);
450
+ if (isInstalled) {
451
+ var defaultBtn = document.createElement("button");
452
+ defaultBtn.type = "button";
453
+ defaultBtn.className = "session-new-set-default";
454
+ defaultBtn.textContent = defaultVendorState.saving ? "Saving…" : (vendor === defaultVendor && !defaultVendorUnavailable(installed) ? "Default" : "Set default");
455
+ defaultBtn.disabled = defaultVendorState.saving === true;
456
+ defaultBtn.setAttribute("aria-label", "Set " + name + " as default vendor");
457
+ defaultBtn.title = "Use " + name + " for New session by default";
458
+ defaultBtn.addEventListener("click", function (e) {
459
+ e.preventDefault();
460
+ e.stopPropagation();
461
+ saveDefaultVendor(vendor);
462
+ });
463
+ row.appendChild(defaultBtn);
464
+ }
465
+ menu.appendChild(row);
427
466
  })(VENDOR_ORDER[i]);
428
467
  }
429
468
 
469
+ var automatic = document.createElement("button");
470
+ automatic.className = "session-ctx-item session-new-automatic";
471
+ automatic.type = "button";
472
+ automatic.textContent = "Use automatic default";
473
+ automatic.disabled = defaultVendorState.saving === true;
474
+ automatic.title = "Use Clay's installed-vendor priority";
475
+ automatic.addEventListener("click", function (e) {
476
+ e.stopPropagation();
477
+ closeSessionCtxMenu();
478
+ saveDefaultVendor("");
479
+ });
480
+ menu.appendChild(automatic);
481
+ if (defaultVendorState.error) {
482
+ var status = document.createElement("div");
483
+ status.className = "session-new-preference-status";
484
+ status.setAttribute("role", "alert");
485
+ status.textContent = defaultVendorState.error;
486
+ menu.appendChild(status);
487
+ }
488
+
430
489
  if (installed.length > 0) {
431
490
  var pairSep = document.createElement("div");
432
491
  pairSep.className = "session-ctx-sep";
@@ -660,6 +719,7 @@ export function getExpandedLoopRuns() {
660
719
  // --- Context menu ---
661
720
 
662
721
  function closeSessionCtxMenu() {
722
+ sessionCtxMenuAnchor = null;
663
723
  if (sessionCtxMenu) {
664
724
  sessionCtxMenu.remove();
665
725
  sessionCtxMenu = null;
@@ -0,0 +1,28 @@
1
+ import { store } from './store.js';
2
+ import { isProjectSessionReady } from './project-activation.js';
3
+
4
+ export function isCurrentProjectSessionReady(state, socket) {
5
+ return isProjectSessionReady(state, socket);
6
+ }
7
+
8
+ export function clearProjectSplitState() {
9
+ store.set({
10
+ splitPanes: null,
11
+ splitGroups: [],
12
+ splitDelegations: {},
13
+ splitGroupsProjectSlug: null,
14
+ sessionListProjectSlug: null,
15
+ activeSessionId: null,
16
+ sessionActivatedProjectSlug: null,
17
+ });
18
+ }
19
+
20
+ export function markProjectSessionListHydrated() {
21
+ var slug = store.get('activeProjectSlug');
22
+ if (slug) store.set({ sessionListProjectSlug: slug });
23
+ }
24
+
25
+ export function applyProjectSplitGroups(groups) {
26
+ var slug = store.get('activeProjectSlug');
27
+ store.set({ splitGroups: groups || [], splitGroupsProjectSlug: slug || null });
28
+ }
@@ -12,6 +12,9 @@ import { showConfirm } from './app-misc.js';
12
12
  import { syncPairChrome } from './split-pair-ui.js';
13
13
  import { presentMarkdownEdit } from './filebrowser.js';
14
14
  import { autoStartLoginIfNeeded } from './vendor-login.js';
15
+ import { isCurrentProjectSessionReady } from './split-session-boundary.js';
16
+ import { splitPanesMatchProject, hasCurrentSplitGroup, reconcileRestoredSplit } from './project-activation.js';
17
+ import { handlePaneIssueMessage } from './pane-issue-bridge.js';
15
18
 
16
19
  var host = null;
17
20
  var nativeApp = null;
@@ -85,10 +88,11 @@ function showDropOverlay() {
85
88
  dropOverlay.classList.add("visible");
86
89
  }
87
90
 
88
- function switchNativeSession(sessionId) {
91
+ function switchNativeSession(sessionId, expectedSlug) {
89
92
  store.set({ splitPanes: null });
90
93
  var ws = getWs();
91
- if (ws && ws.readyState === 1) {
94
+ var state = store.snap();
95
+ if ((!expectedSlug || expectedSlug === state.currentSlug) && isCurrentProjectSessionReady(state, ws)) {
92
96
  ws.send(JSON.stringify({ type: "switch_session", id: sessionId }));
93
97
  }
94
98
  }
@@ -99,7 +103,7 @@ function closePane(index) {
99
103
  if (split.groupId && getWs() && getWs().readyState === 1) {
100
104
  getWs().send(JSON.stringify({ type: "split_group_dissolve", id: split.groupId }));
101
105
  }
102
- switchNativeSession(split.panes[index === 0 ? 1 : 0].sessionId);
106
+ switchNativeSession(split.panes[index === 0 ? 1 : 0].sessionId, split.panes[index === 0 ? 1 : 0].slug);
103
107
  }
104
108
 
105
109
  function startPaneRename(header, titleEl, pane) {
@@ -288,6 +292,7 @@ function handlePaneMessage(event) {
288
292
  if (msg.message) autoStartLoginIfNeeded(msg.message);
289
293
  return;
290
294
  }
295
+ if (handlePaneIssueMessage(event, host)) return;
291
296
  if (msg.type !== "clay-pane-context") return;
292
297
  var frames = host.querySelectorAll(".split-pane-frame");
293
298
  for (var i = 0; i < frames.length; i++) {
@@ -372,7 +377,7 @@ export function openGroup(group) {
372
377
  var activeId = store.get('activeSessionId');
373
378
  if (activeId !== group.members[0] && activeId !== group.members[1]) {
374
379
  var ws = getWs();
375
- if (ws && ws.readyState === 1) {
380
+ if (isCurrentProjectSessionReady(store.snap(), ws)) {
376
381
  ws.send(JSON.stringify({ type: "switch_session", id: group.members[0] }));
377
382
  }
378
383
  }
@@ -384,7 +389,33 @@ export function openGroup(group) {
384
389
  // group member, reopen that group. Safe to call often -- no-ops unless a
385
390
  // group member is active natively with no split open.
386
391
  export function maybeRestoreSplitGroup() {
387
- if (store.get('paneMode') || store.get('splitPanes')) return;
392
+ var state = store.snap();
393
+ if (!isCurrentProjectSessionReady(state, getWs())) return;
394
+ var split = state.splitPanes;
395
+ if (split && !splitPanesMatchProject(split, state.currentSlug)) {
396
+ store.set({ splitPanes: null });
397
+ return;
398
+ }
399
+ if (state.paneMode) return;
400
+ if (split) {
401
+ var reconciliation = reconcileRestoredSplit(split, state.splitGroups);
402
+ if (reconciliation.action === "clear") {
403
+ store.set({ splitPanes: null });
404
+ return;
405
+ }
406
+ if (reconciliation.action === "rebuild") {
407
+ var rebuilt = reconciliation.group;
408
+ if (!sessionById(rebuilt.members[0]) || !sessionById(rebuilt.members[1])) {
409
+ store.set({ splitPanes: null });
410
+ return;
411
+ }
412
+ store.set({ splitPanes: {
413
+ groupId: rebuilt.id,
414
+ panes: [paneForSession(rebuilt.members[0]), paneForSession(rebuilt.members[1])],
415
+ } });
416
+ }
417
+ return;
418
+ }
388
419
  var activeId = store.get('activeSessionId');
389
420
  if (!activeId) return;
390
421
  var groups = store.get('splitGroups') || [];
@@ -505,12 +536,16 @@ export function initSplitView() {
505
536
  }
506
537
  }
507
538
  if (state.splitGroups !== prev.splitGroups) {
539
+ if (!isCurrentProjectSessionReady(state, getWs())) return;
508
540
  var split = state.splitPanes;
509
541
  if (!split || !split.panes) return;
542
+ if (!splitPanesMatchProject(split, state.currentSlug)) {
543
+ store.set({ splitPanes: null });
544
+ return;
545
+ }
510
546
  syncPairChrome(host, split);
511
547
  if (split.groupId) {
512
- var stillExists = state.splitGroups.some(function (group) { return group.id === split.groupId; });
513
- if (!stillExists) switchNativeSession(split.panes[0].sessionId);
548
+ if (!hasCurrentSplitGroup(split, state.splitGroups)) switchNativeSession(split.panes[0].sessionId, state.currentSlug);
514
549
  return;
515
550
  }
516
551
  var ids = [split.panes[0].sessionId, split.panes[1].sessionId];
@@ -4,9 +4,8 @@
4
4
  // the canvas over the conversation; this pane lists them, splits them into Open
5
5
  // and Closed, and is where a note is completed or brought back.
6
6
  //
7
- // Completion here is Close, never delete. A closed note leaves the active
8
- // canvas and moves to the Closed tab, where it stays readable and reversible.
9
- // There is deliberately no destructive control on this surface at all.
7
+ // Completion here is Close. Permanent deletion is available only for closed
8
+ // notes and always requires an explicit, accessible confirmation.
10
9
 
11
10
  import { store } from './store.js';
12
11
  import { iconHtml, refreshIcons } from './icons.js';
@@ -14,25 +13,18 @@ import { renderMiniMarkdown, getTitle } from './sticky-note-markdown.js';
14
13
  import { enhanceClayLogLinks } from './clay-log-links.js';
15
14
  import { send } from './sticky-notes-shared.js';
16
15
  import { listNoteData, focusNoteOnCanvas, hideNotes, showNotes } from './sticky-notes.js';
16
+ import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
17
17
 
18
18
  var panel = null;
19
19
  var gridEl = null;
20
20
  var tabsEl = null;
21
+ var deleteDialog = null;
22
+ var deleteTrigger = null;
23
+ var deleteRequestCounter = 0;
24
+ var deleteRequestTimer = null;
21
25
 
22
26
  // Registered by app.js so this module never imports the other right-pane tools
23
27
  // and cannot form an import cycle with them.
24
- var exclusiveClosers = [];
25
-
26
- export function registerExclusiveClosers(fns) {
27
- exclusiveClosers = (fns || []).filter(function (fn) { return typeof fn === "function"; });
28
- }
29
-
30
- function closeOtherRightTools() {
31
- for (var i = 0; i < exclusiveClosers.length; i++) {
32
- try { exclusiveClosers[i](); } catch (e) { /* a missing tool must not block opening */ }
33
- }
34
- }
35
-
36
28
  function isClosed(data) {
37
29
  if (!data) return false;
38
30
  if (data.state === "closed") return true;
@@ -74,11 +66,15 @@ function ensurePanel() {
74
66
  '</div>' +
75
67
  '</header>' +
76
68
  '<div id="notes-browser-tabs" class="notes-browser-tabs" role="tablist" aria-label="Sticky Notes state"></div>' +
77
- '<div id="notes-browser-grid" class="notes-browser-grid" role="list"></div>';
69
+ '<div id="notes-browser-grid" class="notes-browser-grid" role="list"></div>' +
70
+ '<div id="notes-delete-dialog" class="notes-delete-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="notes-delete-heading">' +
71
+ '<div class="notes-delete-dialog-card"><h2 id="notes-delete-heading">Delete this note permanently?</h2><p>This permanently removes the note. It cannot be restored.</p><p class="notes-delete-dialog-title"></p><p class="notes-delete-dialog-status" role="status"></p><div class="notes-delete-dialog-actions"><button type="button" data-action="cancel">Cancel</button><button type="button" class="danger" data-action="delete">Delete permanently</button></div></div>' +
72
+ '</div>';
78
73
  panels.appendChild(panel);
79
74
 
80
75
  gridEl = panel.querySelector("#notes-browser-grid");
81
76
  tabsEl = panel.querySelector("#notes-browser-tabs");
77
+ deleteDialog = panel.querySelector("#notes-delete-dialog");
82
78
 
83
79
  panel.querySelector("#notes-browser-close").addEventListener("click", closeNotesBrowser);
84
80
  panel.querySelector("#notes-browser-wide").addEventListener("click", function () {
@@ -87,6 +83,29 @@ function ensurePanel() {
87
83
  panel.querySelector("#notes-browser-fullscreen").addEventListener("click", function () {
88
84
  applyWindowState(store.get('notesBrowserWide'), !store.get('notesBrowserFullscreen'));
89
85
  });
86
+ deleteDialog.querySelector('[data-action="cancel"]').addEventListener("click", closeDeleteDialog);
87
+ deleteDialog.querySelector('[data-action="delete"]').addEventListener("click", confirmDelete);
88
+ deleteDialog.addEventListener("click", function (event) { if (event.target === deleteDialog) closeDeleteDialog(); });
89
+ deleteDialog.addEventListener("keydown", function (event) {
90
+ if (event.key === "Escape") {
91
+ event.preventDefault();
92
+ event.stopPropagation();
93
+ closeDeleteDialog(true);
94
+ return;
95
+ }
96
+ if (event.key !== "Tab") return;
97
+ var focusable = deleteDialog.querySelectorAll("button:not(:disabled), [href], input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex='-1'])");
98
+ if (!focusable.length) return;
99
+ var first = focusable[0];
100
+ var last = focusable[focusable.length - 1];
101
+ if (event.shiftKey && document.activeElement === first) {
102
+ event.preventDefault();
103
+ last.focus();
104
+ } else if (!event.shiftKey && document.activeElement === last) {
105
+ event.preventDefault();
106
+ first.focus();
107
+ }
108
+ });
90
109
  refreshIcons();
91
110
  }
92
111
 
@@ -177,7 +196,8 @@ function buildCard(data) {
177
196
  title.textContent = getTitle(data.text) || "Untitled";
178
197
  header.appendChild(title);
179
198
 
180
- // Exactly one lifecycle action per card, and it is never destructive.
199
+ // Closing and reopening are lifecycle actions; permanent deletion is offered
200
+ // separately below only because this card is known to be closed.
181
201
  var action = document.createElement("button");
182
202
  action.type = "button";
183
203
  action.className = "notes-browser-card-action";
@@ -211,6 +231,14 @@ function buildCard(data) {
211
231
  card.appendChild(body);
212
232
 
213
233
  if (closed) {
234
+ var deleteAction = document.createElement("button");
235
+ deleteAction.type = "button";
236
+ deleteAction.className = "notes-browser-card-action notes-browser-card-delete";
237
+ deleteAction.innerHTML = iconHtml("trash-2") + "<span>Delete permanently</span>";
238
+ deleteAction.title = "Permanently delete this closed note";
239
+ deleteAction.setAttribute("aria-label", "Delete permanently: " + title.textContent);
240
+ deleteAction.addEventListener("click", function (e) { e.stopPropagation(); openDeleteDialog(data, deleteAction); });
241
+ header.appendChild(deleteAction);
214
242
  var meta = document.createElement("p");
215
243
  meta.className = "notes-browser-card-meta";
216
244
  meta.textContent = closedMeta(data);
@@ -235,6 +263,77 @@ function buildCard(data) {
235
263
  return card;
236
264
  }
237
265
 
266
+ function nextDeleteRequestId() {
267
+ deleteRequestCounter++;
268
+ return "delete-note-" + Date.now() + "-" + deleteRequestCounter;
269
+ }
270
+
271
+ function accountKey() {
272
+ return store.get('myUserId') ? String(store.get('myUserId')) : "single";
273
+ }
274
+
275
+ function openDeleteDialog(data, trigger) {
276
+ if (!deleteDialog || !data || !isClosed(data)) return;
277
+ deleteTrigger = trigger || null;
278
+ store.set({ notesDeletePending: { noteId: data.id, requestId: null, projectSlug: store.get('currentSlug') || null, accountKey: accountKey() } });
279
+ deleteDialog.querySelector(".notes-delete-dialog-title").textContent = getTitle(data.text) || "Untitled";
280
+ deleteDialog.querySelector(".notes-delete-dialog-status").textContent = "";
281
+ deleteDialog.querySelector('[data-action="delete"]').disabled = false;
282
+ deleteDialog.querySelector('[data-action="cancel"]').disabled = false;
283
+ deleteDialog.classList.remove("hidden");
284
+ deleteDialog.querySelector('[data-action="cancel"]').focus();
285
+ }
286
+
287
+ function closeDeleteDialog(force) {
288
+ var pending = store.get('notesDeletePending');
289
+ if (!force && pending && pending.requestId) return;
290
+ if (deleteRequestTimer) { clearTimeout(deleteRequestTimer); deleteRequestTimer = null; }
291
+ if (deleteDialog) deleteDialog.classList.add("hidden");
292
+ store.set({ notesDeletePending: null });
293
+ if (deleteTrigger && deleteTrigger.isConnected) deleteTrigger.focus();
294
+ deleteTrigger = null;
295
+ }
296
+
297
+ function confirmDelete() {
298
+ var pending = store.get('notesDeletePending');
299
+ if (!pending || !deleteDialog) return;
300
+ if (!store.get('connected')) {
301
+ deleteDialog.querySelector(".notes-delete-dialog-status").textContent = "Sticky Notes are unavailable while disconnected.";
302
+ return;
303
+ }
304
+ var requestId = nextDeleteRequestId();
305
+ store.set({ notesDeletePending: Object.assign({}, pending, { requestId: requestId }) });
306
+ deleteDialog.querySelector('[data-action="delete"]').disabled = true;
307
+ deleteDialog.querySelector('[data-action="cancel"]').disabled = true;
308
+ deleteDialog.querySelector(".notes-delete-dialog-status").textContent = "Deleting permanently...";
309
+ send({ type: "note_delete_permanently", id: pending.noteId, requestId: requestId });
310
+ deleteRequestTimer = setTimeout(function () {
311
+ var active = store.get('notesDeletePending');
312
+ if (!deleteDialog || !active || active.requestId !== requestId) return;
313
+ store.set({ notesDeletePending: Object.assign({}, active, { requestId: null }) });
314
+ deleteDialog.querySelector('[data-action="delete"]').disabled = false;
315
+ deleteDialog.querySelector('[data-action="cancel"]').disabled = false;
316
+ deleteDialog.querySelector(".notes-delete-dialog-status").textContent = "The delete request timed out. Try again.";
317
+ deleteRequestTimer = null;
318
+ }, 10000);
319
+ }
320
+
321
+ export function handleNoteDeleteResult(msg) {
322
+ var pending = store.get('notesDeletePending');
323
+ if (!deleteDialog || !msg || msg.projectSlug !== store.get('currentSlug') ||
324
+ msg.requestId !== (pending && pending.requestId) || !pending || msg.noteId !== pending.noteId ||
325
+ msg.projectSlug !== pending.projectSlug || pending.accountKey !== accountKey()) return;
326
+ if (deleteRequestTimer) { clearTimeout(deleteRequestTimer); deleteRequestTimer = null; }
327
+ if (msg.ok) {
328
+ closeDeleteDialog(true);
329
+ return;
330
+ }
331
+ store.set({ notesDeletePending: Object.assign({}, pending, { requestId: null }) });
332
+ deleteDialog.querySelector('[data-action="delete"]').disabled = false;
333
+ deleteDialog.querySelector('[data-action="cancel"]').disabled = false;
334
+ deleteDialog.querySelector(".notes-delete-dialog-status").textContent = msg.error || "The note could not be deleted.";
335
+ }
336
+
238
337
  function emptyState(tab) {
239
338
  var empty = document.createElement("div");
240
339
  empty.className = "notes-browser-empty";
@@ -253,6 +352,11 @@ function emptyState(tab) {
253
352
  export function renderNotesBrowser() {
254
353
  if (!gridEl) return;
255
354
  var all = listNoteData();
355
+ var pending = store.get('notesDeletePending');
356
+ if (pending) {
357
+ var pendingCurrent = all.filter(function (item) { return item && item.id === pending.noteId; })[0];
358
+ if (!pendingCurrent || !isClosed(pendingCurrent)) closeDeleteDialog(true);
359
+ }
256
360
  var open = openNotes(all);
257
361
  var closed = closedNotes(all);
258
362
  renderTabs(open.length, closed.length);
@@ -283,7 +387,7 @@ export function renderNotesBrowser() {
283
387
  export function openNotesBrowser() {
284
388
  ensurePanel();
285
389
  if (!panel) return;
286
- closeOtherRightTools();
390
+ claimRightWorkbench("notes-browser");
287
391
  // The floating canvas would otherwise sit on top of the pane.
288
392
  hideNotes();
289
393
  panel.classList.remove("hidden");
@@ -295,6 +399,10 @@ export function openNotesBrowser() {
295
399
  }
296
400
 
297
401
  export function closeNotesBrowser() {
402
+ // Reset destructive UI state even when the panel was already closed. Right
403
+ // workbench/project transitions can race with panel visibility updates.
404
+ closeDeleteDialog(true);
405
+ releaseRightWorkbench("notes-browser");
298
406
  if (!store.get('notesBrowserOpen')) return;
299
407
  if (panel) panel.classList.add("hidden");
300
408
  // Fullscreen is always dropped on close so the next open is a bounded pane.
@@ -311,12 +419,16 @@ export function isNotesBrowserOpen() {
311
419
  }
312
420
 
313
421
  export function initNotesBrowser() {
422
+ registerRightWorkbench("notes-browser", closeNotesBrowser);
314
423
  document.addEventListener("keydown", function (e) {
315
424
  if (e.key !== "Escape") return;
425
+ if (deleteDialog && !deleteDialog.classList.contains("hidden")) return;
316
426
  if (!store.get('notesBrowserOpen')) return;
317
427
  closeNotesBrowser();
318
428
  });
319
429
  store.subscribe(function (state, previous) {
430
+ if (state.connected !== previous.connected || state.myUserId !== previous.myUserId ||
431
+ state.isMultiUserMode !== previous.isMultiUserMode) closeDeleteDialog(true);
320
432
  if (state.currentSlug === previous.currentSlug) return;
321
433
  // A different project starts from a closed, bounded, Open-tab pane.
322
434
  closeNotesBrowser();
@@ -424,13 +424,12 @@ export function handleNoteWritten(msg) {
424
424
  // an older daemon on the other end of the socket still must not corrupt state.
425
425
  export function handleNoteDeleted(msg) {
426
426
  var entry = notes.get(msg.id);
427
- if (!entry) return;
428
- entry.el.remove();
427
+ if (entry) entry.el.remove();
428
+ clearNoteTimers(msg.id);
429
429
  notes.delete(msg.id);
430
430
  updateBadge();
431
431
  updateSidebarBadge();
432
432
 
433
- clearNoteTimers(msg.id);
434
433
  refreshBrowser();
435
434
  }
436
435
 
@@ -1,11 +1,10 @@
1
1
  import { iconHtml, refreshIcons } from './icons.js';
2
2
  import { closeSidebar } from './sidebar.js';
3
- import { closeFileViewer } from './filebrowser.js';
3
+ import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
4
4
  import { copyToClipboard } from './utils.js';
5
5
  import { getTerminalTheme } from './theme.js';
6
6
  import { getTerminalFontFamily, getTerminalFontSize, onTerminalFontChange } from './terminal-prefs.js';
7
7
  import { createKeyToolbar } from './terminal-toolbar.js';
8
- import { closeIssues } from './issues.js';
9
8
 
10
9
  var ctx;
11
10
  var tabs = new Map(); // termId -> { id, title, exited, xterm, fitAddon, bodyEl }
@@ -136,6 +135,7 @@ function createMultiLineLinkProvider(xterm) {
136
135
  // --- Init ---
137
136
  export function initTerminal(_ctx) {
138
137
  ctx = _ctx;
138
+ registerRightWorkbench("terminal", closeTerminal);
139
139
 
140
140
  // Close panel button
141
141
  document.getElementById("terminal-close").addEventListener("click", function () {
@@ -193,8 +193,7 @@ export function openTerminal() {
193
193
  var container = ctx.terminalContainerEl;
194
194
 
195
195
  // Hide file viewer if open (also unwatches)
196
- closeFileViewer();
197
- closeIssues();
196
+ claimRightWorkbench("terminal");
198
197
 
199
198
  container.classList.remove("hidden");
200
199
  isOpen = true;
@@ -224,6 +223,7 @@ export function openTerminal() {
224
223
 
225
224
  // --- Close terminal panel (hide, detach, but keep PTYs alive) ---
226
225
  export function closeTerminal() {
226
+ releaseRightWorkbench("terminal");
227
227
  var container = ctx.terminalContainerEl;
228
228
  container.classList.remove("panel-fullscreen");
229
229
  container.classList.add("hidden");