clay-server 4.1.0-beta.13 → 4.1.0-beta.15

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 (38) hide show
  1. package/lib/issues-mcp-server.js +1 -1
  2. package/lib/project-file-path.js +13 -0
  3. package/lib/project-file-watch.js +19 -8
  4. package/lib/project-filesystem.js +10 -16
  5. package/lib/project-http.js +19 -11
  6. package/lib/project-logs-mcp-server.js +8 -8
  7. package/lib/project.js +3 -2
  8. package/lib/public/app.js +15 -9
  9. package/lib/public/css/git-panel.css +13 -12
  10. package/lib/public/css/git-placard.css +4 -4
  11. package/lib/public/css/mobile-nav.css +7 -0
  12. package/lib/public/css/sidebar.css +7 -0
  13. package/lib/public/modules/app-connection.js +3 -0
  14. package/lib/public/modules/app-messages.js +7 -5
  15. package/lib/public/modules/app-panels.js +47 -12
  16. package/lib/public/modules/app-rate-limit.js +148 -61
  17. package/lib/public/modules/default-vendor.js +116 -0
  18. package/lib/public/modules/filebrowser-tabs.js +1 -0
  19. package/lib/public/modules/filebrowser.js +45 -18
  20. package/lib/public/modules/issues.js +4 -6
  21. package/lib/public/modules/project-logs.js +4 -9
  22. package/lib/public/modules/right-workbench.js +44 -0
  23. package/lib/public/modules/scheduled-tasks.js +4 -5
  24. package/lib/public/modules/sidebar-mobile.js +51 -3
  25. package/lib/public/modules/sidebar-sessions.js +65 -5
  26. package/lib/public/modules/sticky-notes-browser.js +4 -13
  27. package/lib/public/modules/terminal.js +4 -4
  28. package/lib/sdk-message-processor.js +11 -2
  29. package/lib/server-default-vendor.js +60 -0
  30. package/lib/server.js +4 -0
  31. package/lib/session-notes-mcp-server.js +2 -4
  32. package/lib/users-default-vendor-preferences.js +80 -0
  33. package/lib/users.js +6 -0
  34. package/lib/ws-schema.js +3 -0
  35. package/lib/yoke/acp-agent-profiles.js +15 -2
  36. package/lib/yoke/adapters/codex.js +56 -13
  37. package/lib/yoke/vendor-registry.js +1 -1
  38. package/package.json +1 -1
@@ -2,15 +2,14 @@ import { iconHtml, refreshIcons } from './icons.js';
2
2
  import { escapeHtml, copyToClipboard } from './utils.js';
3
3
  import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, exportMarkdownAsPdf } from './markdown.js';
4
4
  import { closeSidebar } from './sidebar.js';
5
- import { closeTerminal } from './terminal.js';
5
+ import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench, isRightWorkbenchCurrent } from './right-workbench.js';
6
6
  import { renderUnifiedDiff, renderSplitDiff } from './diff.js';
7
7
  import { initFileIcons, getFileIconSvg, getFolderIconSvg } from './fileicons.js';
8
8
  import { copyMarkdownFormatting } from './rich-clipboard.js';
9
9
  import { animateMarkdownChange, beginMarkdownPresentation, cancelMarkdownFollow, isFollowingMarkdown } from './markdown-live-edit.js';
10
10
  import { store } from './store.js';
11
11
  import { enterMarkdownSlides, exitMarkdownSlides, handleMarkdownSlideKey, syncMarkdownSlidesButton, toggleMarkdownSlideLevelMenu } from './markdown-slides.js';
12
- import { initFileViewerTabs, openFileViewerTab, previewFileViewerTab, updateFileViewerTab, closeFileViewerTab, focusedFileViewerTab, clearFileViewerTabs } from './filebrowser-tabs.js';
13
- import { closeIssues } from './issues.js';
12
+ import { initFileViewerTabs, openFileViewerTab, previewFileViewerTab, updateFileViewerTab, closeFileViewerTab, focusedFileViewerTab, focusedFileViewerTabData, clearFileViewerTabs } from './filebrowser-tabs.js';
14
13
  import { initFileBrowserContextMenu, downloadProjectFile } from './filebrowser-context-menu.js';
15
14
  import { registerClayFileLinkOpener } from './clay-file-links.js';
16
15
 
@@ -41,10 +40,13 @@ export function initFileBrowser(_ctx) {
41
40
  if (mainPanels && ctx.fileViewerEl) mainPanels.appendChild(ctx.fileViewerEl);
42
41
  initFileViewerTabs({
43
42
  onFocus: function(path) {
43
+ claimRightWorkbench("file-viewer");
44
+ ctx.fileViewerEl.classList.remove("hidden");
44
45
  if (path !== currentFilePath) requestFileContent(path);
45
46
  },
46
47
  onEmpty: function() { teardownFileViewer(); },
47
48
  });
49
+ registerRightWorkbench("file-viewer", teardownFileViewer);
48
50
 
49
51
  // Load material file icons in background
50
52
  initFileIcons().then(function () {
@@ -149,7 +151,7 @@ export function initFileBrowser(_ctx) {
149
151
 
150
152
  // Close button
151
153
  document.getElementById("file-viewer-close").addEventListener("click", function () {
152
- closeFileViewerTab(focusedFileViewerTab());
154
+ closeFileViewer();
153
155
  });
154
156
 
155
157
  // Full-viewport presentation toggle
@@ -395,6 +397,7 @@ function kbCollapseOrAscend() {
395
397
  function kbActivate() {
396
398
  if (!_kbFocused) return;
397
399
  if (!isDirRow(_kbFocused) && _kbFocused.dataset.path) {
400
+ claimRightWorkbench("file-viewer");
398
401
  openFileViewerTab(_kbFocused.dataset.path);
399
402
  }
400
403
  _kbFocused.click();
@@ -477,10 +480,7 @@ function ensureRenderedMarkdown() {
477
480
  }
478
481
 
479
482
  export function closeFileViewer() {
480
- if (focusedFileViewerTab()) {
481
- closeFileViewerTab(focusedFileViewerTab());
482
- return;
483
- }
483
+ releaseRightWorkbench("file-viewer");
484
484
  teardownFileViewer();
485
485
  }
486
486
 
@@ -512,6 +512,7 @@ export function setFileViewerFullscreen(enabled) {
512
512
  }
513
513
 
514
514
  export function resetFileBrowser() {
515
+ releaseRightWorkbench("file-viewer");
515
516
  clearFileViewerTabs();
516
517
  teardownFileViewer();
517
518
  // Clear all cached state
@@ -552,12 +553,13 @@ export function openFile(filePath, opts) {
552
553
  if (!state.connected) return false;
553
554
  if (opts && opts.projectSlug && opts.projectSlug !== state.currentSlug) return false;
554
555
  if (opts && opts.sessionId && String(opts.sessionId) !== String(state.activeSessionId)) return false;
555
- closeIssues();
556
+ var workbenchRevision = claimRightWorkbench("file-viewer");
556
557
  if (followedFileChanged(filePath) || (opts && opts.diff && isFollowingMarkdown(filePath))) {
557
558
  cancelMarkdownFollow();
558
559
  }
559
560
  pendingRenderedOpen = !!(opts && opts.rendered);
560
561
  store.set({ pendingFileNavigation: opts && opts.line ? { path: filePath, line: opts.line, column: opts.column || null, requestId: null } : null });
562
+ store.set({ fileViewerOpenRevision: workbenchRevision });
561
563
  openFileViewerTab(filePath);
562
564
  if (opts && opts.diff) {
563
565
  pendingOpenMode = { type: "diff", oldStr: opts.diff.oldStr, newStr: opts.diff.newStr };
@@ -567,9 +569,28 @@ export function openFile(filePath, opts) {
567
569
  requestFileContent(filePath);
568
570
  }
569
571
 
572
+ export function reopenFileViewer() {
573
+ var workbenchRevision = claimRightWorkbench("file-viewer");
574
+ store.set({ fileViewerOpenRevision: workbenchRevision });
575
+ if (!focusedFileViewerTab()) return false;
576
+ ctx.fileViewerEl.classList.remove("hidden");
577
+ var tab = focusedFileViewerTabData();
578
+ if (currentFilePath === focusedFileViewerTab() && currentContent != null) {
579
+ sendWatch(currentFilePath);
580
+ return true;
581
+ }
582
+ if (tab && tab.content != null) {
583
+ showFileContent({ path: focusedFileViewerTab(), content: tab.content, size: tab.content.length });
584
+ return true;
585
+ }
586
+ requestFileContent(focusedFileViewerTab());
587
+ return true;
588
+ }
589
+
570
590
  export function openWorkingTreeDiff(diff) {
571
591
  if (!diff || !diff.path) return;
572
- closeIssues();
592
+ var workbenchRevision = claimRightWorkbench("file-viewer");
593
+ store.set({ fileViewerOpenRevision: workbenchRevision });
573
594
  openFileViewerTab(diff.path);
574
595
  pendingRenderedOpen = false;
575
596
  pendingOpenMode = diff.binary ? null : {
@@ -588,6 +609,8 @@ export function openWorkingTreeDiff(diff) {
588
609
 
589
610
  export function presentMarkdownEdit(msg) {
590
611
  if (!msg || !beginMarkdownPresentation(msg.path)) return;
612
+ var workbenchRevision = claimRightWorkbench("file-viewer");
613
+ store.set({ fileViewerOpenRevision: workbenchRevision });
591
614
  openFileViewerTab(msg.path, { content: msg.content });
592
615
  pendingOpenMode = null;
593
616
  pendingRenderedOpen = true;
@@ -687,7 +710,7 @@ function requestDirectory(dirPath) {
687
710
  function requestFileContent(filePath) {
688
711
  var requestId = "file-read-" + Date.now() + "-" + Math.random().toString(36).slice(2);
689
712
  var state = store.snap();
690
- store.set({ fileReadRequest: { requestId: requestId, path: filePath, projectSlug: state.currentSlug || null, sessionId: state.activeSessionId != null ? String(state.activeSessionId) : null, accountId: state.myUserId || null } });
713
+ store.set({ fileReadRequest: { requestId: requestId, path: filePath, projectSlug: state.currentSlug || null, sessionId: state.activeSessionId != null ? String(state.activeSessionId) : null, accountId: state.myUserId || null, workbenchRevision: store.get('rightWorkbenchRevision') || 0 } });
691
714
  var pending = store.get('pendingFileNavigation');
692
715
  if (pending && pending.path === filePath) store.set({ pendingFileNavigation: Object.assign({}, pending, { requestId: requestId }) });
693
716
  if (ctx.ws && ctx.connected) {
@@ -811,6 +834,7 @@ function renderFilteredTree(container, tree, depth, query) {
811
834
  (function (rowEl, childEl, folderName) {
812
835
  rowEl.addEventListener("click", function (e) {
813
836
  e.stopPropagation();
837
+ claimRightWorkbench("file-viewer");
814
838
  var isExpanded = rowEl.classList.contains("expanded");
815
839
  rowEl.classList.toggle("expanded");
816
840
  childEl.classList.toggle("hidden", isExpanded);
@@ -833,6 +857,7 @@ function renderFilteredTree(container, tree, depth, query) {
833
857
  (function (filePath, rowEl) {
834
858
  rowEl.addEventListener("click", function (e) {
835
859
  e.stopPropagation();
860
+ claimRightWorkbench("file-viewer");
836
861
  if (followedFileChanged(filePath)) cancelMarkdownFollow();
837
862
  var prev = ctx.fileTreeEl.querySelector(".file-tree-item.active");
838
863
  if (prev) prev.classList.remove("active");
@@ -843,7 +868,7 @@ function renderFilteredTree(container, tree, depth, query) {
843
868
  });
844
869
  rowEl.addEventListener("dblclick", function (e) {
845
870
  e.stopPropagation();
846
- openFileViewerTab(filePath);
871
+ openFile(filePath);
847
872
  });
848
873
  })(entry.path, row);
849
874
 
@@ -973,7 +998,7 @@ export function handleFsRead(msg) {
973
998
  if ((request.projectSlug || null) !== (msg.projectSlug || null)) return;
974
999
  if ((request.sessionId || null) !== (msg.sessionId == null ? null : String(msg.sessionId))) return;
975
1000
  if ((request.accountId || null) !== (msg.accountId || null)) return;
976
- showFileContent(msg);
1001
+ showFileContent(msg, request.workbenchRevision);
977
1002
  }
978
1003
 
979
1004
  // --- Tree rendering ---
@@ -1040,6 +1065,7 @@ function renderEntries(container, entries, depth) {
1040
1065
  (function (dirPath, childEl, rowEl, folderName) {
1041
1066
  rowEl.addEventListener("click", function (e) {
1042
1067
  e.stopPropagation();
1068
+ claimRightWorkbench("file-viewer");
1043
1069
  var isExpanded = rowEl.classList.contains("expanded");
1044
1070
  if (isExpanded) {
1045
1071
  rowEl.classList.remove("expanded");
@@ -1079,6 +1105,7 @@ function renderEntries(container, entries, depth) {
1079
1105
  (function (filePath, rowEl) {
1080
1106
  rowEl.addEventListener("click", function (e) {
1081
1107
  e.stopPropagation();
1108
+ claimRightWorkbench("file-viewer");
1082
1109
  if (followedFileChanged(filePath)) cancelMarkdownFollow();
1083
1110
  // Mark active
1084
1111
  var prev = ctx.fileTreeEl.querySelector(".file-tree-item.active");
@@ -1093,7 +1120,7 @@ function renderEntries(container, entries, depth) {
1093
1120
  });
1094
1121
  rowEl.addEventListener("dblclick", function (e) {
1095
1122
  e.stopPropagation();
1096
- openFileViewerTab(filePath);
1123
+ openFile(filePath);
1097
1124
  });
1098
1125
  })(entry.path, row);
1099
1126
 
@@ -1105,8 +1132,9 @@ function renderEntries(container, entries, depth) {
1105
1132
 
1106
1133
  // --- File viewer ---
1107
1134
 
1108
- function showFileContent(msg) {
1135
+ function showFileContent(msg, expectedRevision) {
1109
1136
  if (!updateFileViewerTab(msg.path, { content: msg.content })) return;
1137
+ if (expectedRevision != null && !isRightWorkbenchCurrent("file-viewer", expectedRevision)) return;
1110
1138
  var pathEl = document.getElementById("file-viewer-path");
1111
1139
  var bodyEl = document.getElementById("file-viewer-body");
1112
1140
  var renderBtn = document.getElementById("file-viewer-render");
@@ -1183,7 +1211,6 @@ function showFileContent(msg) {
1183
1211
  }
1184
1212
  }
1185
1213
 
1186
- closeTerminal();
1187
1214
  ctx.fileViewerEl.classList.remove("hidden");
1188
1215
  sendWatch(msg.path);
1189
1216
  refreshIcons();
@@ -1291,7 +1318,7 @@ function pathsReferToSameFile(left, right) {
1291
1318
  }
1292
1319
 
1293
1320
  export function handleFileChanged(msg) {
1294
- if (!msg.path || msg.path !== currentFilePath) return;
1321
+ if (!msg.path || !pathsReferToSameFile(msg.path, currentFilePath)) return;
1295
1322
  if (ctx.fileViewerEl.classList.contains("hidden")) return;
1296
1323
  if (historyVisible || inlineDiffActive) return;
1297
1324
  if (msg.content === currentContent) return;
@@ -1299,7 +1326,7 @@ export function handleFileChanged(msg) {
1299
1326
  var bodyEl = document.getElementById("file-viewer-body");
1300
1327
  var scrollPos = bodyEl ? bodyEl.scrollTop : 0;
1301
1328
  pendingRefresh = true;
1302
- showFileContent(msg);
1329
+ showFileContent(msg.path === currentFilePath ? msg : Object.assign({}, msg, { path: currentFilePath }));
1303
1330
  if (bodyEl) bodyEl.scrollTop = scrollPos;
1304
1331
  }
1305
1332
 
@@ -1,11 +1,7 @@
1
1
  import { store } from './store.js';
2
2
  import { getWs } from './ws-ref.js';
3
3
  import { showToast } from './utils.js';
4
- import { closeProjectLogs } from './project-logs.js';
5
- import { closeScheduledTasks } from './scheduled-tasks.js';
6
- import { closeNotesBrowser } from './sticky-notes-browser.js';
7
- import { closeFileViewer } from './filebrowser.js';
8
- import { closeTerminal } from './terminal.js';
4
+ import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
9
5
  import { refreshIcons } from './icons.js';
10
6
  import { renderIssueList, renderIssueDetail, renderIssueForm, renderIssueHistory, statusLabel } from './issues-render.js';
11
7
  function panel() { return document.getElementById('issues-panel'); }
@@ -81,6 +77,7 @@ function confirmDelete() {
81
77
  store.set({ issuesMutationRequest: request('issue_delete', { ref: entry.ref, expectedRevision: entry.revision }, 'delete') });
82
78
  }
83
79
  export function closeIssues() {
80
+ releaseRightWorkbench("issues");
84
81
  bumpViewGeneration();
85
82
  if (panel()) panel().classList.add('hidden');
86
83
  store.set({ issuesOpen: false });
@@ -91,7 +88,7 @@ export function openIssues(ref) {
91
88
  if (!panel() || store.get('isMate')) return;
92
89
  panel().querySelector('.issues-notice').textContent = '';
93
90
  bumpViewGeneration();
94
- closeProjectLogs(); closeScheduledTasks(); closeNotesBrowser(); closeFileViewer(); closeTerminal();
91
+ claimRightWorkbench("issues");
95
92
  panel().classList.remove('hidden');
96
93
  store.set({ issuesOpen: true });
97
94
  var button = document.getElementById('issues-btn');
@@ -102,6 +99,7 @@ export function openIssues(ref) {
102
99
  export function initIssues() {
103
100
  var host = document.getElementById('main-panels');
104
101
  if (!host || panel()) return;
102
+ registerRightWorkbench("issues", closeIssues);
105
103
  var el = document.createElement('section');
106
104
  el.id = 'issues-panel'; el.className = 'hidden'; el.setAttribute('aria-label', 'Project Issues');
107
105
  el.innerHTML = '<header class="issues-topbar"><button class="issues-icon-btn" data-issues-back aria-label="Back to issues" title="Back to issue list"><i data-lucide="arrow-left"></i></button><span class="issues-title"><i data-lucide="circle-dot"></i>Issues</span><span class="issues-subtitle">Manage issues through Driver chat.</span><div class="issues-window-actions"><button class="issues-icon-btn" data-issues-wide aria-label="Widen Issues" title="Widen Issues" aria-pressed="false"><i data-lucide="chevrons-left-right"></i></button><button class="issues-icon-btn" data-issues-full aria-label="Toggle Issues fullscreen" title="Toggle fullscreen" aria-pressed="false"><i data-lucide="maximize-2"></i></button><button class="issues-icon-btn" data-issues-close aria-label="Close Issues" title="Close Issues"><i data-lucide="x"></i></button></div></header><div class="issues-filters"><label class="issues-search"><i data-lucide="search"></i><input name="query" type="search" placeholder="Search issues" aria-label="Search issues"></label><select name="filter-status" aria-label="Filter status"><option value="">All statuses</option><option value="open">Open</option><option value="in_progress">In progress</option><option value="resolved">Resolved</option><option value="closed">Closed</option></select><select name="filter-type" aria-label="Filter type"><option value="">All types</option><option>bug</option><option>feature</option><option>plan</option></select></div><p class="issues-notice" role="status"></p><main class="issues-content" role="region" aria-live="polite"></main><div class="issues-delete-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="issues-delete-heading"><div class="issues-delete-card"><h2 id="issues-delete-heading">Delete this issue?</h2><p>This removes the issue from the board but preserves its history.</p><p class="issues-delete-title"></p><p class="issues-delete-status" role="status"></p><div><button type="button" data-issue-delete-cancel>Cancel</button><button type="button" data-issue-delete-confirm>Delete issue</button></div></div></div>';
@@ -16,10 +16,7 @@ import { showToast } from './utils.js';
16
16
  import { showScheduledResultNotification } from './scheduled-result-notification.js';
17
17
  import * as scheduledResultClient from './scheduled-result-client.js';
18
18
  import { clearPendingScheduledResultRead, handleScheduledResultDisplayed, initScheduledResultDisplay, openScheduledResultSession } from './scheduled-result-display.js';
19
- import { closeScheduledTasks } from './scheduled-tasks.js';
20
- import { closeNotesBrowser } from './sticky-notes-browser.js';
21
- import { closeFileViewer } from './filebrowser.js';
22
- import { closeTerminal } from './terminal.js';
19
+ import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
23
20
  import { renderFilter, renderContextFilter, renderList, renderDetail } from './project-logs-render.js';
24
21
 
25
22
  var panel = null;
@@ -285,6 +282,7 @@ function applyContextFilter(contextMode) {
285
282
  // --- Lifecycle -----------------------------------------------------------
286
283
 
287
284
  export function initProjectLogs() {
285
+ registerRightWorkbench("project-logs", closeProjectLogs);
288
286
  var button = document.getElementById("project-logs-btn");
289
287
  if (!button) return;
290
288
  button.addEventListener("click", function () {
@@ -331,11 +329,7 @@ export function initProjectLogs() {
331
329
  export function openProjectLogs() {
332
330
  ensurePanel();
333
331
  if (!panel) return;
334
- closeScheduledTasks();
335
- closeNotesBrowser();
336
- // Claim the single right workbench slot.
337
- try { closeFileViewer(); } catch (e) {}
338
- try { closeTerminal(); } catch (e) {}
332
+ claimRightWorkbench("project-logs");
339
333
  panel.classList.remove("hidden");
340
334
  applyWindowState(store.get('projectLogsWide'), false);
341
335
  var button = document.getElementById("project-logs-btn");
@@ -355,6 +349,7 @@ export function openProjectLog(ref) {
355
349
  }
356
350
 
357
351
  export function closeProjectLogs() {
352
+ releaseRightWorkbench("project-logs");
358
353
  if (!store.get('projectLogsOpen')) return;
359
354
  if (panel) {
360
355
  panel.classList.add("hidden");
@@ -0,0 +1,44 @@
1
+ // Single arbitration point for the mutually exclusive right-side workbench.
2
+
3
+ import { store } from './store.js';
4
+
5
+ var closers = {};
6
+
7
+ export function registerRightWorkbench(name, close) {
8
+ if (!name || typeof close !== "function") return;
9
+ closers[name] = close;
10
+ }
11
+
12
+ export function claimRightWorkbench(name) {
13
+ var previousRevision = store.get("rightWorkbenchRevision") || 0;
14
+ var nextRevision = previousRevision + 1;
15
+ storeSet(name, nextRevision);
16
+ var keys = Object.keys(closers);
17
+ for (var i = 0; i < keys.length; i++) {
18
+ if (keys[i] === name) continue;
19
+ try { closers[keys[i]](); } catch (error) { /* one panel must not block another */ }
20
+ }
21
+ return nextRevision;
22
+ }
23
+
24
+ function storeSet(name, value) {
25
+ store.set({ rightWorkbenchOwner: name, rightWorkbenchRevision: value });
26
+ }
27
+
28
+ export function releaseRightWorkbench(name) {
29
+ if (store.get("rightWorkbenchOwner") !== name) return;
30
+ storeSet(null, (store.get("rightWorkbenchRevision") || 0) + 1);
31
+ }
32
+
33
+ export function isRightWorkbenchCurrent(name, expectedRevision) {
34
+ return store.get("rightWorkbenchOwner") === name && store.get("rightWorkbenchRevision") === expectedRevision;
35
+ }
36
+
37
+ export function resetRightWorkbench() {
38
+ storeSet(null, (store.get("rightWorkbenchRevision") || 0) + 1);
39
+ }
40
+
41
+ store.subscribe(function (state, previous) {
42
+ if (state.currentSlug === previous.currentSlug && state.myUserId === previous.myUserId) return;
43
+ resetRightWorkbench();
44
+ });
@@ -2,10 +2,7 @@ import { store } from './store.js';
2
2
  import { getWs } from './ws-ref.js';
3
3
  import { refreshIcons, iconHtml } from './icons.js';
4
4
  import { showToast } from './utils.js';
5
- import { closeProjectLogs } from './project-logs.js';
6
- import { closeNotesBrowser } from './sticky-notes-browser.js';
7
- import { closeFileViewer } from './filebrowser.js';
8
- import { closeTerminal } from './terminal.js';
5
+ import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
9
6
  import { humanRecurrence } from './scheduled-task-recurrence.js';
10
7
  import { humanScheduleRule } from './scheduled-task-schedule-rule.js';
11
8
  import { readExecutionRuntime, renderExecutionRuntime } from './scheduled-task-engine.js';
@@ -320,7 +317,7 @@ export function openScheduledTasks() {
320
317
  if (!canOpen()) { closeScheduledTasks(); return; }
321
318
  ensurePanel();
322
319
  if (!panel) return;
323
- closeProjectLogs(); closeNotesBrowser(); closeFileViewer(); closeTerminal();
320
+ claimRightWorkbench("scheduled-tasks");
324
321
  panel.classList.remove("hidden");
325
322
  applyWindowState(store.get('scheduledTasksWide'), false);
326
323
  store.set({ scheduledTasksOpen: true });
@@ -330,6 +327,7 @@ export function openScheduledTasks() {
330
327
  }
331
328
 
332
329
  export function closeScheduledTasks() {
330
+ releaseRightWorkbench("scheduled-tasks");
333
331
  if (panel) panel.classList.add("hidden");
334
332
  setCreateMenu(false);
335
333
  applyWindowState(store.get('scheduledTasksWide'), false);
@@ -430,6 +428,7 @@ export function handleScheduledTaskSessionSwitched(msg) {
430
428
  }
431
429
 
432
430
  export function initScheduledTasks() {
431
+ registerRightWorkbench("scheduled-tasks", closeScheduledTasks);
433
432
  var button = document.getElementById("scheduler-btn");
434
433
  if (button) button.addEventListener("click", function () { if (isScheduledTasksOpen()) closeScheduledTasks(); else openScheduledTasks(); });
435
434
  document.addEventListener("click", handleDocumentClick);
@@ -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;