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
@@ -1,10 +1,10 @@
1
1
  // User profile module — profile popovers for people and Mates
2
2
  // Stores profile server-side in ~/.clay/profile.json
3
- // Generated identities use deterministic Clay Imprints.
3
+ // Generated identities use deterministic local pixel identicons.
4
4
 
5
5
  import { iconHtml, refreshIcons } from './icons.js';
6
6
  import { setSTTLang } from './stt.js';
7
- import { avatarUrl, mateAvatarUrl, mateMarkUrl } from './avatar.js';
7
+ import { avatarUrl, mateAvatarUrl, mateMarkUrl, userAvatarSeed } from './avatar.js';
8
8
  import { store } from './store.js';
9
9
  import { applyTerminalFont } from './terminal-prefs.js';
10
10
 
@@ -15,7 +15,7 @@ var popoverEl = null;
15
15
  var saveTimer = null;
16
16
 
17
17
  function getAvatarSeed() {
18
- return profile.avatarSeed || 'anonymous';
18
+ return profile.avatarSeed || String(profileUsername || 'anonymous').substring(0, 30);
19
19
  }
20
20
 
21
21
  // --- API ---
@@ -307,14 +307,14 @@ function showPopover() {
307
307
  html += '<input type="text" class="profile-field-input" id="profile-name-input" value="' + escapeAttr(displayName) + '" placeholder="Enter your name..." maxlength="50" spellcheck="false" autocomplete="off">';
308
308
  html += '</div>';
309
309
 
310
- // Clay Imprint or custom photo
310
+ // Pixel identicon or custom photo
311
311
  html += '<div class="profile-field">';
312
312
  html += '<label class="profile-field-label">Identity</label>';
313
313
  html += '<div class="profile-imprint-panel">';
314
- html += '<div class="profile-imprint-preview"><img src="' + avatarUrl(currentStyle, seed, 72) + '" alt="Generated Clay Imprint"><span><strong>Clay Imprint</strong><small>Unique to this identity</small></span></div>';
314
+ html += '<div class="profile-imprint-preview"><img src="' + avatarUrl(currentStyle, seed, 72) + '" alt="Generated pixel avatar"><span><strong>Pixel avatar</strong><small>Unique to this identity</small></span></div>';
315
315
  html += '<div class="profile-imprint-actions">';
316
- html += '<button class="profile-use-imprint' + (!profile.avatarCustom ? ' is-active' : '') + '" type="button">Use imprint</button>';
317
- html += '<button class="profile-regenerate-imprint" type="button" title="Generate another imprint">' + iconHtml('shuffle') + ' New imprint</button>';
316
+ html += '<button class="profile-use-imprint' + (!profile.avatarCustom ? ' is-active' : '') + '" type="button">Use pixel avatar</button>';
317
+ html += '<button class="profile-regenerate-imprint" type="button" title="Generate another pixel avatar">' + iconHtml('shuffle') + ' New avatar</button>';
318
318
  html += '<button class="profile-avatar-upload' + (profile.avatarCustom ? ' is-active' : '') + '" type="button">Upload photo</button>';
319
319
  html += '</div>';
320
320
  html += '</div>';
@@ -511,9 +511,9 @@ export function initProfile(_ctx) {
511
511
  if (data.avatarCustom) profile.avatarCustom = data.avatarCustom;
512
512
  if (data.username) profileUsername = data.username;
513
513
 
514
- // Auto-generate seed if none exists
514
+ // Persist a stable account identity only when the server has not saved a seed.
515
515
  if (!profile.avatarSeed) {
516
- profile.avatarSeed = Math.random().toString(36).substring(2, 10);
516
+ profile.avatarSeed = userAvatarSeed(data);
517
517
  saveProfile();
518
518
  }
519
519
 
@@ -43,3 +43,52 @@ export function isProjectContextConnected(state, slug, socket) {
43
43
  && state.connected === true
44
44
  && state.activeProjectSlug === slug;
45
45
  }
46
+
47
+ export function isProjectSessionReady(state, socket) {
48
+ var target = projectWsPath(state.currentSlug);
49
+ return !!socket
50
+ && socket.readyState === 1
51
+ && state.connected === true
52
+ && !!state.currentSlug
53
+ && state.wsPath === target
54
+ && state.socketPath === target
55
+ && state.activeProjectSlug === state.currentSlug
56
+ && state.sessionActivatedProjectSlug === state.currentSlug
57
+ && state.sessionListProjectSlug === state.currentSlug
58
+ && state.splitGroupsProjectSlug === state.currentSlug;
59
+ }
60
+
61
+ export function splitPanesMatchProject(split, slug) {
62
+ if (!split || !Array.isArray(split.panes) || split.panes.length !== 2 || !slug) return false;
63
+ return split.panes[0].slug === slug && split.panes[1].slug === slug;
64
+ }
65
+
66
+ export function hasCurrentSplitGroup(split, groups) {
67
+ if (!split || !split.groupId) return true;
68
+ var members = split.panes && split.panes.map(function (pane) { return pane.sessionId; });
69
+ if (!members || members.length !== 2) return false;
70
+ var available = Array.isArray(groups) ? groups : [];
71
+ for (var i = 0; i < available.length; i++) {
72
+ var group = available[i];
73
+ if (group.id !== split.groupId || !Array.isArray(group.members) || group.members.length !== 2) continue;
74
+ if (group.members.indexOf(members[0]) !== -1 && group.members.indexOf(members[1]) !== -1) return true;
75
+ }
76
+ return false;
77
+ }
78
+
79
+ export function reconcileRestoredSplit(split, groups) {
80
+ if (!split || !split.groupId) return { action: "none" };
81
+ var available = Array.isArray(groups) ? groups : [];
82
+ for (var i = 0; i < available.length; i++) {
83
+ var group = available[i];
84
+ if (group.id !== split.groupId) continue;
85
+ var oldMembers = split.panes && split.panes.map(function (pane) { return pane.sessionId; });
86
+ var members = Array.isArray(group.members) ? group.members : [];
87
+ if (oldMembers && oldMembers.length === 2 && members.length === 2
88
+ && members.indexOf(oldMembers[0]) !== -1 && members.indexOf(oldMembers[1]) !== -1) {
89
+ return { action: "keep", group: group };
90
+ }
91
+ return { action: "rebuild", group: group };
92
+ }
93
+ return { action: "clear" };
94
+ }
@@ -43,8 +43,9 @@ function actorLabel(actor) {
43
43
  // object (userId, sessionKey, vendor), but a session-authored entry must not
44
44
  // read as if the human owner wrote it: the Project Driver did. The owner is
45
45
  // only the account the session ran under.
46
- function authorLine(entry) {
46
+ export function authorLine(entry) {
47
47
  var actor = (entry && (entry.updatedBy || entry.createdBy)) || {};
48
+ if (actor.type === "system") return actor.displayName || "Scheduled Tasks";
48
49
  if (actor.type === "session") {
49
50
  return actor.vendor ? "Project Driver (" + vendorLabel(actor.vendor) + ")" : "Project Driver";
50
51
  }
@@ -62,7 +63,7 @@ function formatTime(value) {
62
63
  catch (e) { return ""; }
63
64
  }
64
65
 
65
- function relativeTime(value) {
66
+ export function relativeTime(value) {
66
67
  if (!value) return "";
67
68
  var delta = Date.now() - value;
68
69
  if (delta < 0) return formatTime(value);
@@ -363,7 +364,7 @@ export function renderDetail(detailEl, entry, handlers) {
363
364
  formatTime(entry.updatedAt || entry.createdAt),
364
365
  ].filter(Boolean).join(" · ");
365
366
  setText(header, ".project-log-byline", byline);
366
- if (handlers && handlers.canDelete) {
367
+ if (handlers && handlers.canDelete && !entry.scheduledResult) {
367
368
  var deleteButton = document.createElement("button");
368
369
  deleteButton.type = "button";
369
370
  deleteButton.className = "project-log-delete-button";
@@ -373,6 +374,15 @@ export function renderDetail(detailEl, entry, handlers) {
373
374
  }
374
375
  article.appendChild(header);
375
376
 
377
+ if (entry.scheduledResult && entry.scheduledResult.driverOriginId && handlers && handlers.onOpenSession) {
378
+ var sessionButton = document.createElement("button");
379
+ sessionButton.type = "button";
380
+ sessionButton.className = "project-log-session-button";
381
+ sessionButton.innerHTML = '<i data-lucide="external-link"></i><span>View session</span>';
382
+ sessionButton.addEventListener("click", function () { handlers.onOpenSession(entry); });
383
+ article.appendChild(sessionButton);
384
+ }
385
+
376
386
  var markdown = document.createElement("div");
377
387
  markdown.className = "project-log-markdown md-content";
378
388
  markdown.innerHTML = renderMarkdown(entry.body || "");
@@ -1,5 +1,4 @@
1
1
  // Project Logs — the project's ledger of agent-authored decisions and work.
2
- //
3
2
  // A bounded right-side workbench pane, like the document viewer and terminal.
4
3
  // Inside it there is one navigation stack rather than a master/detail split:
5
4
  // the list fills the pane, opening an entry replaces it with the full-pane
@@ -14,10 +13,10 @@ import { store } from './store.js';
14
13
  import { getWs } from './ws-ref.js';
15
14
  import { refreshIcons } from './icons.js';
16
15
  import { showToast } from './utils.js';
17
- import { closeScheduledTasks } from './scheduled-tasks.js';
18
- import { closeNotesBrowser } from './sticky-notes-browser.js';
19
- import { closeFileViewer } from './filebrowser.js';
20
- import { closeTerminal } from './terminal.js';
16
+ import { showScheduledResultNotification } from './scheduled-result-notification.js';
17
+ import * as scheduledResultClient from './scheduled-result-client.js';
18
+ import { clearPendingScheduledResultRead, handleScheduledResultDisplayed, initScheduledResultDisplay, openScheduledResultSession } from './scheduled-result-display.js';
19
+ import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
21
20
  import { renderFilter, renderContextFilter, renderList, renderDetail } from './project-logs-render.js';
22
21
 
23
22
  var panel = null;
@@ -35,7 +34,7 @@ var requestCounter = 0;
35
34
  function syncUnreadBadge() {
36
35
  var button = document.getElementById("project-logs-btn");
37
36
  if (!button) return;
38
- var unread = store.get('projectLogsUnread') || 0;
37
+ var unread = (store.get('projectLogsUnread') || 0) + (store.get('scheduledResultUnread') || 0);
39
38
  button.classList.toggle("project-logs-unread", unread > 0);
40
39
  var badge = document.getElementById("project-logs-count");
41
40
  if (!badge) return;
@@ -164,10 +163,10 @@ function applyWindowState(wide, fullscreen) {
164
163
  }
165
164
 
166
165
  // --- Navigation stack ----------------------------------------------------
167
-
168
166
  function showList() {
169
167
  if (!panel) return;
170
168
  store.set({ projectLogsView: "list", projectLogsSelectedRef: null });
169
+ clearPendingScheduledResultRead();
171
170
  detailEl.classList.add("hidden");
172
171
  listEl.classList.remove("hidden");
173
172
  panel.querySelector("#project-logs-toolbar").classList.remove("hidden");
@@ -188,8 +187,10 @@ function showDetail(entry) {
188
187
  renderDetail(detailEl, entry, {
189
188
  onComment: submitComment,
190
189
  onDelete: openDeleteDialog,
190
+ onOpenSession: openScheduledResultSession,
191
191
  canDelete: store.get('projectLogsCanDelete') === true,
192
192
  });
193
+ handleScheduledResultDisplayed(entry);
193
194
  }
194
195
 
195
196
  function openDeleteDialog(entry) {
@@ -230,7 +231,6 @@ function confirmDelete() {
230
231
  }
231
232
 
232
233
  // --- Requests ------------------------------------------------------------
233
-
234
234
  function requestList() {
235
235
  var requestId = nextRequestId("list-logs");
236
236
  store.set({ projectLogsListRequestId: requestId });
@@ -282,14 +282,17 @@ function applyContextFilter(contextMode) {
282
282
  // --- Lifecycle -----------------------------------------------------------
283
283
 
284
284
  export function initProjectLogs() {
285
+ registerRightWorkbench("project-logs", closeProjectLogs);
285
286
  var button = document.getElementById("project-logs-btn");
286
287
  if (!button) return;
287
288
  button.addEventListener("click", function () {
288
289
  if (store.get('projectLogsOpen')) closeProjectLogs();
289
290
  else openProjectLogs();
290
291
  });
292
+ initScheduledResultDisplay();
291
293
  syncUnreadBadge();
292
294
  store.subscribe(function (state, previous) {
295
+ if (state.connected === false && previous.connected === true) store.set({ scheduledResultAutoFocusHandled: false });
293
296
  if (state.currentSlug === previous.currentSlug) return;
294
297
  closeProjectLogs();
295
298
  applyWindowState(false, false);
@@ -309,7 +312,13 @@ export function initProjectLogs() {
309
312
  projectLogsDeleteRequestId: null,
310
313
  projectLogsCanDelete: false,
311
314
  projectLogsUnread: 0,
312
- projectLogsSeenRevisions: {}
315
+ projectLogsSeenRevisions: {},
316
+ scheduledResultUnread: 0,
317
+ scheduledResultAutoFocusHandled: false,
318
+ scheduledResultFocusRef: null,
319
+ scheduledResultPendingReadRef: null,
320
+ scheduledResultSeenRefs: {},
321
+ scheduledResultSessionRequestId: null,
313
322
  });
314
323
  syncUnreadBadge();
315
324
  });
@@ -320,11 +329,7 @@ export function initProjectLogs() {
320
329
  export function openProjectLogs() {
321
330
  ensurePanel();
322
331
  if (!panel) return;
323
- closeScheduledTasks();
324
- closeNotesBrowser();
325
- // Claim the single right workbench slot.
326
- try { closeFileViewer(); } catch (e) {}
327
- try { closeTerminal(); } catch (e) {}
332
+ claimRightWorkbench("project-logs");
328
333
  panel.classList.remove("hidden");
329
334
  applyWindowState(store.get('projectLogsWide'), false);
330
335
  var button = document.getElementById("project-logs-btn");
@@ -344,6 +349,7 @@ export function openProjectLog(ref) {
344
349
  }
345
350
 
346
351
  export function closeProjectLogs() {
352
+ releaseRightWorkbench("project-logs");
347
353
  if (!store.get('projectLogsOpen')) return;
348
354
  if (panel) {
349
355
  panel.classList.add("hidden");
@@ -384,11 +390,35 @@ export function handleProjectLogsState(msg) {
384
390
 
385
391
  export function handleProjectLogEntry(msg) {
386
392
  if (msg.requestId && msg.requestId !== store.get('projectLogsReadRequestId')) return;
387
- if (!msg.entry) return;
393
+ if (!msg.entry || !store.get('projectLogsOpen')) return;
388
394
  store.set({ projectLogsCanDelete: msg.canDelete === true });
389
395
  showDetail(msg.entry);
390
396
  }
391
397
 
398
+ export function handleScheduledTaskResultsState(msg) {
399
+ var transition = scheduledResultClient.scheduledResultStateTransition(store.snap(), msg);
400
+ store.set(transition.patch); syncUnreadBadge();
401
+ if (!transition.openRef) return false;
402
+ var opened = openProjectLog(transition.openRef);
403
+ if (!opened) store.set({ scheduledResultFocusRef: null });
404
+ return opened;
405
+ }
406
+
407
+ export function handleScheduledTaskResultCreated(msg) {
408
+ var patch = scheduledResultClient.scheduledResultCreatedTransition(store.snap(), msg);
409
+ if (!patch) return false;
410
+ store.set(patch); syncUnreadBadge();
411
+ showScheduledResultNotification(msg.title || "Scheduled task finished", msg.summary || "", function () { openProjectLog(msg.ref); });
412
+ return true;
413
+ }
414
+
415
+ export function handleScheduledTaskResultSession(msg) {
416
+ var sessionId = scheduledResultClient.scheduledResultSessionId(store.snap(), msg);
417
+ if (sessionId === null) return false;
418
+ store.set({ scheduledResultSessionRequestId: null });
419
+ return send({ type: "switch_session", id: sessionId });
420
+ }
421
+
392
422
  export function handleProjectLogCommented(msg) {
393
423
  if (msg.requestId && msg.requestId !== store.get('projectLogsCommentRequestId')) return;
394
424
  if (!msg.entry) return;
@@ -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);