clay-server 4.1.0-beta.9 → 4.2.0-beta.1

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 (119) hide show
  1. package/lib/context-usage.js +47 -0
  2. package/lib/daemon-project-access.js +14 -0
  3. package/lib/daemon.js +7 -0
  4. package/lib/issues-mcp-server.js +1 -1
  5. package/lib/issues-service.js +3 -1
  6. package/lib/loop-registry-occurrence.js +1 -0
  7. package/lib/notes.js +20 -3
  8. package/lib/os-users.js +6 -0
  9. package/lib/project-connection.js +14 -3
  10. package/lib/project-file-path.js +13 -0
  11. package/lib/project-file-watch.js +19 -8
  12. package/lib/project-filesystem.js +10 -16
  13. package/lib/project-http.js +53 -48
  14. package/lib/project-issue-launch.js +3 -1
  15. package/lib/project-logs-mcp-server.js +8 -8
  16. package/lib/project-logs-schema.js +1 -1
  17. package/lib/project-logs-service.js +46 -13
  18. package/lib/project-logs-store.js +17 -15
  19. package/lib/project-logs.js +70 -0
  20. package/lib/project-loop.js +1 -0
  21. package/lib/project-notifications.js +1 -1
  22. package/lib/project-scheduled-result-store.js +131 -0
  23. package/lib/project-scheduled-task-execution.js +35 -2
  24. package/lib/project-session-notes.js +11 -4
  25. package/lib/project-sessions.js +39 -6
  26. package/lib/project-user-message.js +47 -0
  27. package/lib/project.js +57 -3
  28. package/lib/public/app.js +27 -9
  29. package/lib/public/css/avatar-imprints.css +1 -1
  30. package/lib/public/css/command-palette.css +57 -19
  31. package/lib/public/css/git-panel.css +13 -12
  32. package/lib/public/css/git-placard.css +4 -4
  33. package/lib/public/css/issues.css +79 -32
  34. package/lib/public/css/mobile-nav.css +7 -0
  35. package/lib/public/css/notifications-center.css +3 -3
  36. package/lib/public/css/project-logs.css +15 -0
  37. package/lib/public/css/sidebar.css +7 -0
  38. package/lib/public/css/sticky-notes.css +29 -0
  39. package/lib/public/index.html +10 -0
  40. package/lib/public/modules/app-connection.js +9 -1
  41. package/lib/public/modules/app-favicon.js +10 -0
  42. package/lib/public/modules/app-messages.js +50 -12
  43. package/lib/public/modules/app-notifications.js +2 -0
  44. package/lib/public/modules/app-panels.js +77 -23
  45. package/lib/public/modules/app-projects.js +2 -0
  46. package/lib/public/modules/app-rate-limit.js +148 -61
  47. package/lib/public/modules/avatar-imprint.js +25 -70
  48. package/lib/public/modules/avatar.js +12 -4
  49. package/lib/public/modules/chat-render-runtime.js +20 -0
  50. package/lib/public/modules/clay-issue-links.js +24 -2
  51. package/lib/public/modules/clay-log-links.js +12 -1
  52. package/lib/public/modules/context-view-preference.js +143 -0
  53. package/lib/public/modules/default-vendor.js +116 -0
  54. package/lib/public/modules/filebrowser-tabs.js +1 -0
  55. package/lib/public/modules/filebrowser.js +45 -18
  56. package/lib/public/modules/issues-render.js +36 -10
  57. package/lib/public/modules/issues.js +95 -13
  58. package/lib/public/modules/markdown.js +3 -1
  59. package/lib/public/modules/pane-bridge.js +14 -8
  60. package/lib/public/modules/pane-issue-bridge.js +27 -0
  61. package/lib/public/modules/permission-hints.js +13 -0
  62. package/lib/public/modules/profile.js +9 -9
  63. package/lib/public/modules/project-activation.js +49 -0
  64. package/lib/public/modules/project-logs-render.js +13 -3
  65. package/lib/public/modules/project-logs.js +45 -15
  66. package/lib/public/modules/project-session-visibility-settings.js +138 -0
  67. package/lib/public/modules/project-settings.js +5 -0
  68. package/lib/public/modules/right-workbench.js +44 -0
  69. package/lib/public/modules/scheduled-result-client.js +33 -0
  70. package/lib/public/modules/scheduled-result-display.js +50 -0
  71. package/lib/public/modules/scheduled-result-notification.js +31 -0
  72. package/lib/public/modules/scheduled-task-presentation.js +15 -0
  73. package/lib/public/modules/scheduled-tasks.js +9 -21
  74. package/lib/public/modules/sidebar-mobile.js +51 -3
  75. package/lib/public/modules/sidebar-projects.js +14 -3
  76. package/lib/public/modules/sidebar-sessions.js +65 -5
  77. package/lib/public/modules/split-session-boundary.js +28 -0
  78. package/lib/public/modules/split-view.js +42 -7
  79. package/lib/public/modules/sticky-notes-browser.js +130 -18
  80. package/lib/public/modules/sticky-notes.js +2 -3
  81. package/lib/public/modules/terminal.js +4 -4
  82. package/lib/public/modules/thinking-lifecycle.js +4 -1
  83. package/lib/public/modules/tools.js +5 -1
  84. package/lib/scheduled-run-state.js +10 -2
  85. package/lib/scheduler.js +1 -0
  86. package/lib/sdk-bridge.js +97 -61
  87. package/lib/sdk-message-processor.js +44 -13
  88. package/lib/sdk-skill-discovery.js +6 -28
  89. package/lib/sdk-turn-queue.js +14 -0
  90. package/lib/server-context-view.js +53 -0
  91. package/lib/server-default-vendor.js +60 -0
  92. package/lib/server.js +31 -7
  93. package/lib/session-notes-mcp-server.js +3 -5
  94. package/lib/session-pair-factory.js +2 -2
  95. package/lib/session-title-generator.js +89 -0
  96. package/lib/session-title-policy.js +8 -2
  97. package/lib/session-visibility.js +62 -1
  98. package/lib/sessions.js +28 -1
  99. package/lib/user-presence.js +6 -0
  100. package/lib/users-context-view-preferences.js +65 -0
  101. package/lib/users-default-vendor-preferences.js +80 -0
  102. package/lib/users.js +12 -0
  103. package/lib/ws-schema.js +13 -0
  104. package/lib/yoke/acp-agent-profiles.js +15 -2
  105. package/lib/yoke/adapters/acp.js +3 -2
  106. package/lib/yoke/adapters/antigravity.js +2 -2
  107. package/lib/yoke/adapters/claude-worker.js +11 -12
  108. package/lib/yoke/adapters/claude.js +75 -22
  109. package/lib/yoke/adapters/codex.js +216 -29
  110. package/lib/yoke/adapters/kiro.js +6 -5
  111. package/lib/yoke/claude-context-usage.js +57 -0
  112. package/lib/yoke/claude-message-correlation.js +31 -0
  113. package/lib/yoke/claude-model-switch.js +15 -0
  114. package/lib/yoke/index.js +5 -1
  115. package/lib/yoke/mcp-bridge-server.js +18 -5
  116. package/lib/yoke/session-skill-catalog.js +45 -0
  117. package/lib/yoke/skill-discovery.js +60 -20
  118. package/lib/yoke/vendor-registry.js +1 -1
  119. package/package.json +5 -7
@@ -0,0 +1,138 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { showToast } from './utils.js';
4
+
5
+ var requestSequence = 0;
6
+ var initialized = false;
7
+
8
+ function selectElement() {
9
+ return document.getElementById("ps-session-visibility-default");
10
+ }
11
+
12
+ function pendingState() {
13
+ return (store.get('projectSessionVisibilityState') || {}).pending || null;
14
+ }
15
+
16
+ function activeState() {
17
+ return (store.get('projectSessionVisibilityState') || {}).active || null;
18
+ }
19
+
20
+ function setVisibilityState(active, pending) {
21
+ store.set({ projectSessionVisibilityState: { active: active || null, pending: pending || null } });
22
+ }
23
+
24
+ function setSelectState(select, value, disabled) {
25
+ if (!select) return;
26
+ select.value = value;
27
+ select.disabled = !!disabled;
28
+ }
29
+
30
+ function isCurrentProject(active) {
31
+ return !!(active && active.slug && active.slug === store.get('currentSlug'));
32
+ }
33
+
34
+ function isReadOnly(active) {
35
+ return !active || !active.authorized || active.isWorktree || !isCurrentProject(active);
36
+ }
37
+
38
+ function renderActiveState() {
39
+ var active = activeState();
40
+ var pending = pendingState();
41
+ var select = selectElement();
42
+ var inheritedHint = document.getElementById("ps-session-visibility-default-inherited-hint");
43
+ var nonCurrentHint = document.getElementById("ps-session-visibility-default-non-current-hint");
44
+ if (inheritedHint) inheritedHint.classList.toggle("hidden", !(active && active.isWorktree));
45
+ if (nonCurrentHint) nonCurrentHint.classList.toggle("hidden", !(active && active.authorized && !active.isWorktree && !isCurrentProject(active)));
46
+ if (!select || !active) return;
47
+ setSelectState(select, active.effectiveValue, isReadOnly(active) || !!(pending && pending.slug === active.slug));
48
+ }
49
+
50
+ function isAuthorized(project) {
51
+ var users = store.get('cachedAllUsers') || [];
52
+ var userId = store.get('myUserId');
53
+ var currentUser = null;
54
+ for (var i = 0; i < users.length; i++) {
55
+ if (users[i].id === userId) { currentUser = users[i]; break; }
56
+ }
57
+ return !!(store.get('isMultiUserMode') && project &&
58
+ ((project.projectOwnerId && project.projectOwnerId === userId) || (currentUser && currentUser.role === "admin")) &&
59
+ (!store.get('permissions') || store.get('permissions').projectSettings !== false));
60
+ }
61
+
62
+ export function initProjectSessionVisibilitySettings() {
63
+ var select = selectElement();
64
+ if (!select || initialized) return;
65
+ initialized = true;
66
+ select.addEventListener("change", function () {
67
+ var ws = getWs();
68
+ var active = activeState();
69
+ var slug = active && active.slug || "";
70
+ var prior = active && active.effectiveValue || "private";
71
+ if (!slug || !ws || ws.readyState !== 1 || pendingState() || isReadOnly(active)) {
72
+ setSelectState(select, prior, isReadOnly(active) || !!pendingState());
73
+ return;
74
+ }
75
+ requestSequence += 1;
76
+ var requestId = "project-session-visibility-" + Date.now() + "-" + requestSequence;
77
+ setVisibilityState(active, { requestId: requestId, slug: slug, requested: select.value, previous: prior });
78
+ setSelectState(select, select.value, true);
79
+ ws.send(JSON.stringify({ type: "set_project_session_visibility_default", slug: slug, visibility: select.value, requestId: requestId }));
80
+ });
81
+
82
+ store.subscribe(function (state, previous) {
83
+ if (state.currentSlug !== previous.currentSlug) renderActiveState();
84
+ if (state.connected || previous.connected !== true) return;
85
+ var pending = pendingState();
86
+ if (!pending) return;
87
+ var current = selectElement();
88
+ var active = activeState();
89
+ if (current && active && active.slug === pending.slug) setSelectState(current, pending.previous, isReadOnly(active));
90
+ setVisibilityState(active, null);
91
+ renderActiveState();
92
+ });
93
+ }
94
+
95
+ export function populateProjectSessionVisibilitySettings(slug, project) {
96
+ var field = document.getElementById("ps-session-visibility-default-field");
97
+ var select = selectElement();
98
+ if (field) field.style.display = isAuthorized(project) ? "" : "none";
99
+ if (!select) return;
100
+ var value = project && project.sessionVisibilityDefault === "shared" ? "shared" : "private";
101
+ var active = { slug: slug || "", effectiveValue: value, isWorktree: !!(project && project.isWorktree), authorized: isAuthorized(project) };
102
+ var pending = pendingState();
103
+ setVisibilityState(active, pending);
104
+ renderActiveState();
105
+ }
106
+
107
+ export function refreshProjectSessionVisibilitySettings(projects) {
108
+ var select = selectElement();
109
+ if (!select) return;
110
+ var active = activeState();
111
+ var slug = active && active.slug || "";
112
+ var source = Array.isArray(projects) ? projects : [];
113
+ for (var i = 0; i < source.length; i++) {
114
+ if (source[i] && source[i].slug === slug) {
115
+ populateProjectSessionVisibilitySettings(slug, source[i]);
116
+ return;
117
+ }
118
+ }
119
+ }
120
+
121
+ export function handleProjectSessionVisibilityDefault(msg) {
122
+ var pending = pendingState();
123
+ if (!pending || msg.requestId !== pending.requestId || msg.slug !== pending.slug) return;
124
+ var active = activeState();
125
+ setVisibilityState(active, null);
126
+ var select = selectElement();
127
+ if (!select || !active || active.slug !== msg.slug) return;
128
+ if (!msg.ok) {
129
+ active = Object.assign({}, active, { effectiveValue: pending.previous });
130
+ setVisibilityState(active, null);
131
+ setSelectState(select, pending.previous, isReadOnly(active));
132
+ showToast(msg.error || "Failed to update new session visibility", "error");
133
+ return;
134
+ }
135
+ active = Object.assign({}, active, { effectiveValue: msg.visibility === "shared" ? "shared" : "private" });
136
+ setVisibilityState(active, null);
137
+ setSelectState(select, active.effectiveValue, isReadOnly(active));
138
+ }
@@ -4,6 +4,7 @@ import { showToast } from './utils.js';
4
4
  import { parseEmojis } from './markdown.js';
5
5
  import { closeFileViewer } from './filebrowser.js';
6
6
  import { renderModelList, renderModeList, renderEffortBar, renderThinkingBar, renderBetaCard } from './settings-defaults.js';
7
+ import { initProjectSessionVisibilitySettings, populateProjectSessionVisibilitySettings } from './project-session-visibility-settings.js';
7
8
 
8
9
  var ctx = null;
9
10
  var panelEl = null;
@@ -162,6 +163,8 @@ export function initProjectSettings(appCtx, emojiCategories) {
162
163
  hideTransferForm();
163
164
  });
164
165
  }
166
+
167
+ initProjectSessionVisibilitySettings();
165
168
  }
166
169
 
167
170
  // ===== Open / Close =====
@@ -246,6 +249,8 @@ function populateProfile() {
246
249
  // Icon
247
250
  updateIconPreview(currentProject ? currentProject.icon : null);
248
251
 
252
+ populateProjectSessionVisibilitySettings(currentSlug, currentProject);
253
+
249
254
  // Owner (only in multi-user mode)
250
255
  var ownerField = document.getElementById("ps-owner-field");
251
256
  if (ownerField) {
@@ -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
+ });
@@ -0,0 +1,33 @@
1
+ // Pure Scheduled Tasks result-news decisions. The production controller owns
2
+ // store, WebSocket, DOM, and peer-module calls directly.
3
+
4
+ export function scheduledResultStateTransition(state, msg) {
5
+ var patch = { scheduledResultUnread: Number(msg.unreadCount) || 0 };
6
+ var openRef = null;
7
+ if (msg.autoFocus && msg.newestUnreadRef && !state.scheduledResultAutoFocusHandled) {
8
+ patch.scheduledResultAutoFocusHandled = true;
9
+ patch.scheduledResultFocusRef = msg.newestUnreadRef;
10
+ openRef = msg.newestUnreadRef;
11
+ }
12
+ return { patch: patch, openRef: openRef };
13
+ }
14
+
15
+ export function scheduledResultCreatedTransition(state, msg) {
16
+ if (!msg || !msg.ref || (state.scheduledResultSeenRefs || {})[msg.ref]) return null;
17
+ var seen = Object.assign({}, state.scheduledResultSeenRefs || {});
18
+ seen[msg.ref] = true;
19
+ return { scheduledResultSeenRefs: seen, scheduledResultUnread: (state.scheduledResultUnread || 0) + 1 };
20
+ }
21
+
22
+ export function shouldAcknowledgeScheduledResult(entry, state, visibilityState) {
23
+ return !!(entry && entry.scheduledResult && visibilityState !== "hidden" && state.projectLogsOpen && state.projectLogsView === "detail" && state.projectLogsSelectedRef === entry.ref);
24
+ }
25
+
26
+ export function shouldFocusScheduledResult(entry, state) {
27
+ return !!(entry && state.scheduledResultFocusRef === entry.ref);
28
+ }
29
+
30
+ export function scheduledResultSessionId(state, msg) {
31
+ if (msg.requestId && msg.requestId !== state.scheduledResultSessionRequestId) return null;
32
+ return typeof msg.sessionId === "number" ? msg.sessionId : null;
33
+ }
@@ -0,0 +1,50 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { showToast } from './utils.js';
4
+ import { shouldAcknowledgeScheduledResult, shouldFocusScheduledResult } from './scheduled-result-client.js';
5
+
6
+ function sendRead(ref) {
7
+ var ws = getWs();
8
+ if (!ws || ws.readyState !== 1) return false;
9
+ ws.send(JSON.stringify({ type: "scheduled_task_result_mark_read", ref: ref }));
10
+ return true;
11
+ }
12
+
13
+ var openRequestCounter = 0;
14
+ export function openScheduledResultSession(entry) {
15
+ if (!entry || !entry.scheduledResult) return false;
16
+ openRequestCounter += 1;
17
+ var requestId = "open-scheduled-session-" + Date.now() + "-" + openRequestCounter;
18
+ store.set({ scheduledResultSessionRequestId: requestId });
19
+ var ws = getWs();
20
+ if (!ws || ws.readyState !== 1) { showToast("Reconnect to view this scheduled session.", "warn"); return false; }
21
+ ws.send(JSON.stringify({ type: "scheduled_task_result_open_session", requestId: requestId, ref: entry.ref })); return true;
22
+ }
23
+
24
+ function acknowledgeVisible() {
25
+ var ref = store.get('scheduledResultPendingReadRef');
26
+ if (!ref || document.visibilityState === "hidden") return;
27
+ store.set({ scheduledResultPendingReadRef: null });
28
+ var state = store.snap();
29
+ if (state.projectLogsOpen && state.projectLogsView === "detail" && state.projectLogsSelectedRef === ref) sendRead(ref);
30
+ }
31
+
32
+ export function clearPendingScheduledResultRead() {
33
+ store.set({ scheduledResultPendingReadRef: null });
34
+ }
35
+
36
+ export function handleScheduledResultDisplayed(entry) {
37
+ var state = store.snap();
38
+ if (shouldFocusScheduledResult(entry, state)) {
39
+ store.set({ scheduledResultFocusRef: null });
40
+ var heading = document.querySelector("#project-logs-detail .project-log-document h1");
41
+ if (heading) { heading.setAttribute("tabindex", "-1"); heading.focus(); }
42
+ }
43
+ if (shouldAcknowledgeScheduledResult(entry, store.snap(), document.visibilityState)) {
44
+ clearPendingScheduledResultRead(); sendRead(entry.ref);
45
+ } else if (entry && entry.scheduledResult && document.visibilityState === "hidden") store.set({ scheduledResultPendingReadRef: entry.ref });
46
+ }
47
+
48
+ export function initScheduledResultDisplay() {
49
+ document.addEventListener("visibilitychange", acknowledgeVisible);
50
+ }
@@ -0,0 +1,31 @@
1
+ import { refreshIcons, iconHtml } from './icons.js';
2
+ import { escapeHtml } from './utils.js';
3
+
4
+ function removeBanner(banner) {
5
+ if (!banner || !banner.parentNode) return;
6
+ banner.classList.remove("show");
7
+ setTimeout(function () { if (banner.parentNode) banner.remove(); }, 220);
8
+ }
9
+
10
+ export function showScheduledResultNotification(title, body, onOpen) {
11
+ var container = document.querySelector(".notif-banner-container");
12
+ if (!container) return false;
13
+ var banner = document.createElement("div");
14
+ banner.className = "notif-banner";
15
+ banner.innerHTML =
16
+ '<div class="notif-banner-icon">' + iconHtml("calendar-check") + '</div>' +
17
+ '<div class="notif-banner-body"><div class="notif-banner-project">SCHEDULED TASKS</div>' +
18
+ '<div class="notif-banner-title">' + escapeHtml(title || "Scheduled task finished") + '</div>' +
19
+ (body ? '<div class="notif-banner-text">' + escapeHtml(body) + '</div>' : '') +
20
+ '<div class="notif-banner-actions"><button class="notif-banner-result-open">View result</button></div></div>' +
21
+ '<button class="notif-banner-close" aria-label="Dismiss notification">' + iconHtml("x") + '</button>';
22
+ container.appendChild(banner);
23
+ refreshIcons(banner);
24
+ requestAnimationFrame(function () { banner.classList.add("show"); });
25
+ banner.querySelector(".notif-banner-result-open").addEventListener("click", function (event) {
26
+ event.stopPropagation(); removeBanner(banner); if (typeof onOpen === "function") onOpen();
27
+ });
28
+ banner.querySelector(".notif-banner-close").addEventListener("click", function (event) { event.stopPropagation(); removeBanner(banner); });
29
+ setTimeout(function () { removeBanner(banner); }, 8000);
30
+ return true;
31
+ }
@@ -0,0 +1,15 @@
1
+ export function scheduledTaskExecutionLabel(record) {
2
+ if (record.activeRun) {
3
+ if (record.activeRun.status === "needs-input") return "Needs input";
4
+ if (record.activeRun.status === "waiting-worker") return "Waiting for Worker";
5
+ if (record.activeRun.status === "reviewing") return "Driver reviewing";
6
+ return "Running";
7
+ }
8
+ if (!record.execution) return "Needs setup";
9
+ if (record.readiness && record.readiness.ok === false) return "Unavailable";
10
+ return record.enabled === false ? "Paused" : "Active";
11
+ }
12
+
13
+ export function scheduledTaskOpenRunLabel(record) {
14
+ return record.activeRun && record.activeRun.status === "needs-input" ? "Open blocked run" : "Open run";
15
+ }
@@ -2,16 +2,14 @@ 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';
12
9
  import { openDefaultAi } from './default-ai.js';
13
10
  import { reconcileScheduledTaskEdit } from './scheduled-task-edit-state.js';
14
11
  import { editorFields, fillEditor, readEditor, serverEditorData, validateEditor, handleEditorScheduleChange } from './scheduled-task-form.js';
12
+ import { scheduledTaskExecutionLabel, scheduledTaskOpenRunLabel } from './scheduled-task-presentation.js';
15
13
 
16
14
  export { isStrictCron, humanRecurrence } from './scheduled-task-recurrence.js';
17
15
 
@@ -56,18 +54,6 @@ function scheduleLabel(record) {
56
54
  return "No schedule";
57
55
  }
58
56
 
59
- function executionLabel(record) {
60
- if (record.activeRun) {
61
- if (record.activeRun.status === "needs-input") return "Needs input";
62
- if (record.activeRun.status === "waiting-worker") return "Waiting for Worker";
63
- if (record.activeRun.status === "reviewing") return "Driver reviewing";
64
- return "Running";
65
- }
66
- if (!record.execution) return "Needs setup";
67
- if (record.readiness && record.readiness.ok === false) return "Unavailable";
68
- return record.enabled === false ? "Paused" : "Active";
69
- }
70
-
71
57
  function ensurePanel() {
72
58
  if (panel) return;
73
59
  var host = document.getElementById("main-panels");
@@ -185,7 +171,7 @@ function buildRow(record) {
185
171
  row.className = "scheduled-task-row" + (record.enabled === false ? " paused" : "");
186
172
  row.dataset.taskId = record.id;
187
173
  row.setAttribute("role", "listitem");
188
- var stateLabel = executionLabel(record);
174
+ var stateLabel = scheduledTaskExecutionLabel(record);
189
175
  row.innerHTML =
190
176
  '<button class="scheduled-task-summary" type="button" data-action="expand" aria-expanded="' + (expandedId === record.id ? "true" : "false") + '">' +
191
177
  '<span class="scheduled-task-state-dot" aria-hidden="true"></span>' +
@@ -212,12 +198,12 @@ function buildRow(record) {
212
198
  detail.appendChild(instructions);
213
199
  var context = document.createElement("div");
214
200
  context.className = "scheduled-task-context";
215
- var executionState = executionLabel(record);
201
+ var executionState = scheduledTaskExecutionLabel(record);
216
202
  context.textContent = (record.projectTitle || store.get('currentSlug') || "Project") + " · " + executionState + (record.execution ? " · Driver and Split Worker" : " · Choose execution pair in Edit");
217
203
  detail.appendChild(context);
218
204
  var actions = document.createElement("div");
219
205
  actions.className = "scheduled-task-actions";
220
- actions.innerHTML = '<button type="button" data-action="edit">' + iconHtml("pencil") + 'Edit</button><button type="button" data-action="toggle">' + iconHtml(record.enabled === false ? "play" : "pause") + (record.enabled === false ? "Resume" : "Pause") + '</button><button type="button" data-action="run">' + iconHtml("zap") + 'Run now</button>' + (record.activeRun || record.latestRun ? '<button type="button" data-action="open-run">' + iconHtml("columns-2") + 'Open run</button>' : '') + '<button type="button" data-action="delete" class="danger">' + iconHtml("trash-2") + 'Delete</button>';
206
+ actions.innerHTML = '<button type="button" data-action="edit">' + iconHtml("pencil") + 'Edit</button><button type="button" data-action="toggle">' + iconHtml(record.enabled === false ? "play" : "pause") + (record.enabled === false ? "Resume" : "Pause") + '</button><button type="button" data-action="run">' + iconHtml("zap") + 'Run now</button>' + (record.activeRun || record.latestRun ? '<button type="button" data-action="open-run">' + iconHtml("columns-2") + scheduledTaskOpenRunLabel(record) + '</button>' : '') + '<button type="button" data-action="delete" class="danger">' + iconHtml("trash-2") + 'Delete</button>';
221
207
  detail.appendChild(actions);
222
208
  }
223
209
  row.appendChild(detail);
@@ -271,7 +257,7 @@ function handleClick(event) {
271
257
  var button = event.target.closest("button[data-action]");
272
258
  if (!button) return;
273
259
  var action = button.dataset.action;
274
- if (action === "default-ai") { openDefaultAi(); return; }
260
+ if (action === "default-ai") { event.stopPropagation(); openDefaultAi(); return; }
275
261
  if (action === "close") { closeScheduledTasks(); return; }
276
262
  if (action === "wide") { applyWindowState(!store.get('scheduledTasksWide'), store.get('scheduledTasksFullscreen')); return; }
277
263
  if (action === "fullscreen") { applyWindowState(store.get('scheduledTasksWide'), !store.get('scheduledTasksFullscreen')); return; }
@@ -331,7 +317,7 @@ export function openScheduledTasks() {
331
317
  if (!canOpen()) { closeScheduledTasks(); return; }
332
318
  ensurePanel();
333
319
  if (!panel) return;
334
- closeProjectLogs(); closeNotesBrowser(); closeFileViewer(); closeTerminal();
320
+ claimRightWorkbench("scheduled-tasks");
335
321
  panel.classList.remove("hidden");
336
322
  applyWindowState(store.get('scheduledTasksWide'), false);
337
323
  store.set({ scheduledTasksOpen: true });
@@ -341,6 +327,7 @@ export function openScheduledTasks() {
341
327
  }
342
328
 
343
329
  export function closeScheduledTasks() {
330
+ releaseRightWorkbench("scheduled-tasks");
344
331
  if (panel) panel.classList.add("hidden");
345
332
  setCreateMenu(false);
346
333
  applyWindowState(store.get('scheduledTasksWide'), false);
@@ -441,6 +428,7 @@ export function handleScheduledTaskSessionSwitched(msg) {
441
428
  }
442
429
 
443
430
  export function initScheduledTasks() {
431
+ registerRightWorkbench("scheduled-tasks", closeScheduledTasks);
444
432
  var button = document.getElementById("scheduler-btn");
445
433
  if (button) button.addEventListener("click", function () { if (isScheduledTasksOpen()) closeScheduledTasks(); else openScheduledTasks(); });
446
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)
@@ -1,7 +1,7 @@
1
1
  // sidebar-projects.js - Project icon strip, context menus, emoji picker, drag-and-drop, worktree modal
2
2
  // Extracted from sidebar.js (PR-36)
3
3
 
4
- import { escapeHtml } from './utils.js';
4
+ import { escapeHtml, showToast } from './utils.js';
5
5
  import { iconHtml, refreshIcons } from './icons.js';
6
6
  import { openProjectSettings } from './project-settings.js';
7
7
  import { triggerShare } from './qrcode.js';
@@ -415,6 +415,11 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
415
415
  closeProjectCtxMenu();
416
416
  if (closeUserCtxMenu) closeUserCtxMenu();
417
417
  closeEmojiPicker();
418
+ var projects = getCachedProjects();
419
+ var targetProject = null;
420
+ for (var projectIndex = 0; projectIndex < projects.length; projectIndex++) {
421
+ if (projects[projectIndex].slug === slug) { targetProject = projects[projectIndex]; break; }
422
+ }
418
423
 
419
424
  var menu = document.createElement("div");
420
425
  menu.className = "project-ctx-menu";
@@ -438,7 +443,13 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
438
443
  settingsItem.addEventListener("click", function (e) {
439
444
  e.stopPropagation();
440
445
  closeProjectCtxMenu();
441
- openProjectSettings(slug, { slug: slug, name: name, icon: icon, projectOwnerId: store.get('currentProjectOwnerId'), ownerLocked: store.get('ownerLocked') });
446
+ if (!targetProject) {
447
+ showToast("Project details are unavailable. Refresh and try again.", "error");
448
+ return;
449
+ }
450
+ openProjectSettings(slug, Object.assign({}, targetProject, {
451
+ name: targetProject.title || targetProject.project || name,
452
+ }));
442
453
  });
443
454
  menu.appendChild(settingsItem);
444
455
  }
@@ -460,7 +471,7 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
460
471
 
461
472
  // --- Manage Access ---
462
473
  if (store.get('isMultiUserMode')) {
463
- var isProjectOwner = store.get('myUserId') && store.get('currentProjectOwnerId') && store.get('myUserId') === store.get('currentProjectOwnerId');
474
+ var isProjectOwner = store.get('myUserId') && targetProject && targetProject.projectOwnerId && store.get('myUserId') === targetProject.projectOwnerId;
464
475
  var isAdmin = store.get('permissions') && store.get('permissions').projectSettings !== false;
465
476
  if (isProjectOwner || isAdmin) {
466
477
  var accessItem = document.createElement("button");