clay-server 4.0.0-beta.2 → 4.0.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/lib/background-task-timing.js +105 -0
  2. package/lib/capsule-display-floor.js +51 -0
  3. package/lib/capsule-frame-server.js +255 -0
  4. package/lib/capsule-mate-turn.js +105 -0
  5. package/lib/capsule-pig-logic.js +300 -0
  6. package/lib/capsule-server-runtimes.js +39 -0
  7. package/lib/capsules/pig/display.js +198 -0
  8. package/lib/capsules/pig/manifest.json +9 -0
  9. package/lib/capsules/pig/ui.json +84 -0
  10. package/lib/config.js +25 -1
  11. package/lib/daemon-projects.js +13 -8
  12. package/lib/daemon.js +127 -14
  13. package/lib/git-cli.js +95 -1
  14. package/lib/knowledge-import.js +436 -0
  15. package/lib/knowledge-record-store.js +188 -0
  16. package/lib/knowledge-search.js +240 -0
  17. package/lib/mate-knowledge-mcp-server.js +151 -0
  18. package/lib/mate-knowledge-migration.js +283 -0
  19. package/lib/mate-knowledge-service.js +488 -0
  20. package/lib/mate-knowledge-sync.js +482 -0
  21. package/lib/migrate-single-user.js +2 -0
  22. package/lib/notes-lifecycle.js +149 -0
  23. package/lib/notes.js +60 -3
  24. package/lib/os-users.js +3 -1
  25. package/lib/project-capsule-catalog.js +9 -1
  26. package/lib/project-capsule-turn.js +96 -0
  27. package/lib/project-connection.js +14 -1
  28. package/lib/project-http.js +15 -1
  29. package/lib/project-knowledge.js +22 -0
  30. package/lib/project-log-context.js +172 -0
  31. package/lib/project-log-feedback-delivery.js +118 -0
  32. package/lib/project-logs-comments.js +137 -0
  33. package/lib/project-logs-context-state.js +42 -0
  34. package/lib/project-logs-mcp-server.js +328 -0
  35. package/lib/project-logs-query.js +185 -0
  36. package/lib/project-logs-root.js +75 -0
  37. package/lib/project-logs-schema.js +259 -0
  38. package/lib/project-logs-service.js +463 -0
  39. package/lib/project-logs-snapshot.js +205 -0
  40. package/lib/project-logs-store.js +495 -0
  41. package/lib/project-logs-versioning.js +132 -0
  42. package/lib/project-logs.js +400 -0
  43. package/lib/project-mate-interaction.js +14 -0
  44. package/lib/project-mate-knowledge.js +110 -0
  45. package/lib/project-memory.js +22 -0
  46. package/lib/project-message-delivery.js +70 -0
  47. package/lib/project-pair-lifecycle.js +460 -0
  48. package/lib/project-session-handoff.js +1 -1
  49. package/lib/project-session-notes.js +64 -13
  50. package/lib/project-session-pair.js +159 -146
  51. package/lib/project-session-spawn.js +1 -1
  52. package/lib/project-sessions.js +58 -13
  53. package/lib/project-user-message.js +53 -4
  54. package/lib/project-vendor-login.js +20 -3
  55. package/lib/project-worker-permission.js +430 -0
  56. package/lib/project-worker-proposal.js +208 -62
  57. package/lib/project.js +178 -6
  58. package/lib/public/app.js +58 -35
  59. package/lib/public/css/admin.css +2 -2
  60. package/lib/public/css/capsule-ui.css +38 -0
  61. package/lib/public/css/command-palette.css +19 -9
  62. package/lib/public/css/git-placard.css +154 -0
  63. package/lib/public/css/home-hub.css +4 -0
  64. package/lib/public/css/home-session-actions.css +66 -0
  65. package/lib/public/css/home-sidebar.css +66 -0
  66. package/lib/public/css/input.css +163 -28
  67. package/lib/public/css/mobile-nav.css +80 -0
  68. package/lib/public/css/notifications-center.css +67 -0
  69. package/lib/public/css/pane.css +52 -0
  70. package/lib/public/css/project-logs.css +198 -0
  71. package/lib/public/css/scheduler.css +1 -1
  72. package/lib/public/css/session-actions.css +16 -101
  73. package/lib/public/css/sidebar.css +103 -0
  74. package/lib/public/css/sticky-notes.css +237 -229
  75. package/lib/public/css/worker-proposal.css +34 -1
  76. package/lib/public/index.html +45 -11
  77. package/lib/public/modules/admin.js +8 -8
  78. package/lib/public/modules/app-connection.js +109 -4
  79. package/lib/public/modules/app-home-hub.js +19 -7
  80. package/lib/public/modules/app-message-cards.js +2 -1
  81. package/lib/public/modules/app-messages.js +84 -7
  82. package/lib/public/modules/app-misc.js +2 -14
  83. package/lib/public/modules/app-notifications.js +11 -7
  84. package/lib/public/modules/app-panels.js +46 -4
  85. package/lib/public/modules/app-projects.js +13 -8
  86. package/lib/public/modules/app-rendering.js +1 -0
  87. package/lib/public/modules/background-tasks-ui.js +139 -16
  88. package/lib/public/modules/capsule-preference.js +44 -0
  89. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  90. package/lib/public/modules/clay-log-links.js +70 -0
  91. package/lib/public/modules/command-palette.js +52 -15
  92. package/lib/public/modules/context-sources.js +0 -3
  93. package/lib/public/modules/git-agent-sessions.js +85 -0
  94. package/lib/public/modules/git-panel.js +111 -84
  95. package/lib/public/modules/git-placard.js +161 -0
  96. package/lib/public/modules/home-chat-empty-state.js +39 -0
  97. package/lib/public/modules/home-chat-identity.js +9 -0
  98. package/lib/public/modules/home-conversations-sheet.js +102 -48
  99. package/lib/public/modules/home-mate-chat.js +15 -35
  100. package/lib/public/modules/home-session-actions.js +6 -0
  101. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  102. package/lib/public/modules/home-sidebar.js +7 -0
  103. package/lib/public/modules/home-tool-frame.js +182 -0
  104. package/lib/public/modules/home-tools.js +138 -1
  105. package/lib/public/modules/input.js +38 -7
  106. package/lib/public/modules/markdown.js +2 -0
  107. package/lib/public/modules/mate-sidebar.js +0 -8
  108. package/lib/public/modules/message-delivery.js +97 -0
  109. package/lib/public/modules/paste-modal.js +84 -0
  110. package/lib/public/modules/project-logs-render.js +405 -0
  111. package/lib/public/modules/project-logs.js +461 -0
  112. package/lib/public/modules/project-removal-target.js +26 -0
  113. package/lib/public/modules/scheduler.js +17 -3
  114. package/lib/public/modules/session-actions.js +29 -105
  115. package/lib/public/modules/session-hierarchy.js +54 -0
  116. package/lib/public/modules/sidebar-mobile.js +40 -15
  117. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  118. package/lib/public/modules/sidebar-sessions.js +77 -11
  119. package/lib/public/modules/sidebar.js +17 -5
  120. package/lib/public/modules/split-group-helpers.js +8 -0
  121. package/lib/public/modules/split-view.js +5 -2
  122. package/lib/public/modules/sticky-note-markdown.js +5 -0
  123. package/lib/public/modules/sticky-notes-browser.js +326 -0
  124. package/lib/public/modules/sticky-notes-card.js +360 -0
  125. package/lib/public/modules/sticky-notes-editor.js +291 -0
  126. package/lib/public/modules/sticky-notes-shared.js +90 -0
  127. package/lib/public/modules/sticky-notes.js +170 -946
  128. package/lib/public/modules/tool-palette-order.js +143 -0
  129. package/lib/public/modules/tool-palette-overlays.js +159 -0
  130. package/lib/public/modules/tool-palette.js +21 -188
  131. package/lib/public/modules/tools.js +21 -1
  132. package/lib/public/modules/update-snooze.js +169 -0
  133. package/lib/public/modules/user-settings.js +4 -0
  134. package/lib/public/modules/worker-pane-lock.js +176 -0
  135. package/lib/public/modules/worker-proposal-state.js +16 -0
  136. package/lib/public/modules/worker-proposal.js +49 -12
  137. package/lib/public/style.css +3 -1
  138. package/lib/sdk-bridge.js +132 -46
  139. package/lib/sdk-message-processor.js +60 -14
  140. package/lib/server-experimental-settings.js +49 -0
  141. package/lib/server-global-ws.js +11 -0
  142. package/lib/server-home-chat-events.js +31 -1
  143. package/lib/server-home-chat.js +27 -4
  144. package/lib/server-home-clay-entry.js +1 -1
  145. package/lib/server-home-clay-session-links.js +34 -5
  146. package/lib/server-settings.js +5 -0
  147. package/lib/server-tools.js +127 -9
  148. package/lib/server.js +124 -18
  149. package/lib/session-driver-eligibility.js +38 -0
  150. package/lib/session-driver-orchestration.js +31 -0
  151. package/lib/session-hygiene.js +3 -0
  152. package/lib/session-notes-mcp-server.js +25 -5
  153. package/lib/session-pair-factory.js +247 -0
  154. package/lib/session-pair-mcp-server.js +46 -8
  155. package/lib/session-pair-prompts.js +79 -0
  156. package/lib/session-pair-turn-control.js +153 -0
  157. package/lib/session-provenance.js +119 -0
  158. package/lib/session-spawn-mcp-server.js +1 -1
  159. package/lib/session-split-groups.js +4 -1
  160. package/lib/session-title-policy.js +60 -0
  161. package/lib/session-visibility.js +39 -0
  162. package/lib/sessions.js +61 -11
  163. package/lib/tools-registry.js +89 -10
  164. package/lib/update-snooze.js +437 -0
  165. package/lib/users-experimental-preferences.js +30 -0
  166. package/lib/users-permissions.js +2 -2
  167. package/lib/users.js +22 -16
  168. package/lib/workspace-query-access.js +102 -0
  169. package/lib/workspace-query-service.js +24 -17
  170. package/lib/worktree.js +25 -38
  171. package/lib/ws-schema.js +38 -7
  172. package/lib/yoke/adapters/claude-worker.js +16 -6
  173. package/lib/yoke/adapters/claude.js +81 -4
  174. package/lib/yoke/adapters/codex.js +63 -15
  175. package/lib/yoke/codex-background-tasks.js +8 -2
  176. package/lib/yoke/vendor-registry.js +1 -1
  177. package/package.json +3 -3
@@ -16,6 +16,12 @@ import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES, isExperim
16
16
  import { openGroup, separateGroup } from './split-view.js';
17
17
  import { groupedSessionIds } from './split-group-helpers.js';
18
18
  import { openPairDialog } from './split-pair-ui.js';
19
+ import {
20
+ hierarchyItemMatches,
21
+ prepareSidebarHierarchy,
22
+ renderDesktopDriverHierarchy,
23
+ renderDesktopOrphanHierarchy
24
+ } from './sidebar-session-hierarchy.js';
19
25
 
20
26
 
21
27
  // --- Session state ---
@@ -49,8 +55,8 @@ function sendSessionBookmark(sessionId, bookmarked) {
49
55
  }
50
56
 
51
57
  function compareSessionListItems(a, b) {
52
- var aData = a && a.type === "session" ? a.data : a;
53
- var bData = b && b.type === "session" ? b.data : b;
58
+ var aData = a && (a.type === "session" || a.type === "driver-hierarchy") ? a.data : a;
59
+ var bData = b && (b.type === "session" || b.type === "driver-hierarchy") ? b.data : b;
54
60
  var aBookmarked = !!(aData && aData.bookmarked);
55
61
  var bBookmarked = !!(bData && bData.bookmarked);
56
62
  if (aBookmarked !== bBookmarked) return aBookmarked ? -1 : 1;
@@ -236,6 +242,14 @@ function collectItemSessionIds(item) {
236
242
  if (item.type === "split-group" && Array.isArray(item.members)) {
237
243
  return item.members.map(function (session) { return session.id; });
238
244
  }
245
+ if (item.type === "driver-hierarchy" && item.root) {
246
+ var hierarchyIds = [item.root.driver.id];
247
+ for (var j = 0; j < item.root.workers.length; j++) hierarchyIds.push(item.root.workers[j].id);
248
+ return hierarchyIds;
249
+ }
250
+ if (item.type === "orphan-workers" && Array.isArray(item.workers)) {
251
+ return item.workers.map(function (session) { return session.id; });
252
+ }
239
253
  return [];
240
254
  }
241
255
 
@@ -683,7 +697,7 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
683
697
 
684
698
  // Session visibility toggle (only the session owner can change)
685
699
  if (store.get('isMultiUserMode') && sessionData && sessionData.ownerId && sessionData.ownerId === store.get('myUserId')) {
686
- var currentVis = (sessionData && sessionData.sessionVisibility) || "shared";
700
+ var currentVis = (sessionData && sessionData.sessionVisibility) || "private";
687
701
  var isPrivate = currentVis === "private";
688
702
  var visItem = document.createElement("button");
689
703
  visItem.className = "session-ctx-item";
@@ -1246,10 +1260,12 @@ function renderLoopRun(parentGk, startedAtKey, sessions, isRalph) {
1246
1260
 
1247
1261
  // --- Session item rendering ---
1248
1262
 
1249
- function renderSessionItem(s) {
1263
+ function renderSessionItem(s, options) {
1264
+ var itemOptions = options || {};
1250
1265
  var el = document.createElement("div");
1251
1266
  var isMatch = searchMatchIds !== null && searchMatchIds.has(s.id);
1252
1267
  el.className = "session-item" + (s.active ? " active" : "") + (isMatch ? " search-match" : "");
1268
+ if (itemOptions.worker) el.classList.add("session-worker-item");
1253
1269
  el.dataset.sessionId = s.id;
1254
1270
 
1255
1271
  // Keep agent identity separate from the title so each row has a stable
@@ -1274,13 +1290,20 @@ function renderSessionItem(s) {
1274
1290
  if (s.loop && s.loop.source === "debate") {
1275
1291
  textHtml += '<span class="session-debate-icon" title="Debate">' + iconHtml("mic") + '</span>';
1276
1292
  }
1277
- if (store.get('isMultiUserMode') && s.sessionVisibility === "private") {
1278
- textHtml += '<span class="session-private-icon" title="Private session">' + iconHtml("lock") + '</span>';
1293
+ if (store.get('isMultiUserMode') && s.sessionVisibility === "shared") {
1294
+ textHtml += '<span class="session-shared-icon" title="Shared session" aria-label="Shared session">' + iconHtml("users") + '</span>';
1279
1295
  }
1280
1296
  textHtml += highlightMatch(s.title || "New Session", searchQuery);
1281
1297
  textSpan.innerHTML = textHtml;
1282
1298
  el.appendChild(textSpan);
1283
1299
 
1300
+ if (itemOptions.worker) {
1301
+ var workerLabel = document.createElement("span");
1302
+ workerLabel.className = "session-worker-generation" + (itemOptions.current ? " current" : "");
1303
+ workerLabel.textContent = itemOptions.current ? "Current" : (s.workerGeneration ? "Gen " + s.workerGeneration : "Worker");
1304
+ el.appendChild(workerLabel);
1305
+ }
1306
+
1284
1307
  var ageText = relativeTime(s.lastActivity || 0);
1285
1308
  if (ageText) {
1286
1309
  var age = document.createElement("span");
@@ -1325,7 +1348,7 @@ function renderSessionItem(s) {
1325
1348
 
1326
1349
  // Presence avatars (multi-user)
1327
1350
  renderPresenceAvatars(el, String(s.id));
1328
- setupSessionDragHandlers(el, s);
1351
+ if (!itemOptions.worker) setupSessionDragHandlers(el, s);
1329
1352
 
1330
1353
  return el;
1331
1354
  }
@@ -1376,8 +1399,29 @@ function renderSplitGroupItem(group, members) {
1376
1399
 
1377
1400
  // --- Main session list ---
1378
1401
 
1402
+ // Fingerprint of the last completed render: the whole session payload plus the
1403
+ // two store values this render also reads. Nothing is cherry-picked, so the
1404
+ // guard can only skip a frame that is byte-identical to what is already on
1405
+ // screen. A re-render triggered by other state (sessions === null) always
1406
+ // runs and resets the fingerprint afterwards.
1407
+ var lastRenderFingerprint = null;
1408
+
1409
+ function sessionRenderFingerprint(sessions) {
1410
+ try {
1411
+ return JSON.stringify([sessions, store.get('splitGroups') || [], store.get('splitPanes') || null]);
1412
+ } catch (e) {
1413
+ return null;
1414
+ }
1415
+ }
1416
+
1379
1417
  export function renderSessionList(sessions) {
1380
- if (sessions) cachedSessions = sessions;
1418
+ if (sessions) {
1419
+ // A reconnect and a streaming turn both re-send this list unchanged;
1420
+ // rebuilding the sidebar for an identical frame is what made it flicker.
1421
+ var incoming = sessionRenderFingerprint(sessions);
1422
+ if (incoming !== null && incoming === lastRenderFingerprint) return;
1423
+ cachedSessions = sessions;
1424
+ }
1381
1425
 
1382
1426
  // If mobile chat sheet is open, refresh it
1383
1427
  if (refreshMobileChatSheet) refreshMobileChatSheet();
@@ -1388,12 +1432,17 @@ export function renderSessionList(sessions) {
1388
1432
  // Group by loopId + date so all runs of the same task on the same day are merged
1389
1433
  var splitGroups = store.get('splitGroups') || [];
1390
1434
  var groupedIds = groupedSessionIds(splitGroups);
1435
+ var hierarchy = prepareSidebarHierarchy(cachedSessions);
1436
+ var hierarchyByDriver = hierarchy.byDriver;
1437
+ var hierarchySessionIds = hierarchy.sessionIds;
1438
+ hierarchySessionIds.forEach(function (id) { groupedIds.delete(id); });
1391
1439
  var sessionsById = new Map();
1392
1440
  for (var si = 0; si < cachedSessions.length; si++) sessionsById.set(cachedSessions[si].id, cachedSessions[si]);
1393
1441
  var loopGroups = {}; // groupKey -> [sessions]
1394
1442
  var normalSessions = [];
1395
1443
  for (var i = 0; i < cachedSessions.length; i++) {
1396
1444
  var s = cachedSessions[i];
1445
+ if (s.sessionRole === "worker") continue;
1397
1446
  if (groupedIds.has(s.id)) continue;
1398
1447
  if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph" && s.loop.source !== "debate") {
1399
1448
  // Task crafting sessions live in the scheduler calendar, not the main list (except debate)
@@ -1412,13 +1461,21 @@ export function renderSessionList(sessions) {
1412
1461
  // Build virtual items: normal sessions + one entry per loop group (using latest child's lastActivity)
1413
1462
  var items = [];
1414
1463
  for (var j = 0; j < normalSessions.length; j++) {
1415
- items.push({ type: "session", data: normalSessions[j], lastActivity: normalSessions[j].lastActivity || 0 });
1464
+ var driverRoot = hierarchyByDriver.get(normalSessions[j].id);
1465
+ if (driverRoot) items.push({ type: "driver-hierarchy", root: driverRoot, data: driverRoot.driver, lastActivity: driverRoot.lastActivity });
1466
+ else items.push({ type: "session", data: normalSessions[j], lastActivity: normalSessions[j].lastActivity || 0 });
1467
+ }
1468
+ if (hierarchy.orphans.length) {
1469
+ var orphanActivity = 0;
1470
+ for (var oi = 0; oi < hierarchy.orphans.length; oi++) orphanActivity = Math.max(orphanActivity, hierarchy.orphans[oi].lastActivity || 0);
1471
+ items.push({ type: "orphan-workers", workers: hierarchy.orphans, lastActivity: orphanActivity });
1416
1472
  }
1417
1473
  for (var sg = 0; sg < splitGroups.length; sg++) {
1418
1474
  var splitGroup = splitGroups[sg];
1419
1475
  var leftMember = sessionsById.get(splitGroup.members[0]);
1420
1476
  var rightMember = sessionsById.get(splitGroup.members[1]);
1421
1477
  if (!leftMember || !rightMember) continue;
1478
+ if (hierarchySessionIds.has(leftMember.id) || hierarchySessionIds.has(rightMember.id)) continue;
1422
1479
  items.push({
1423
1480
  type: "split-group",
1424
1481
  group: splitGroup,
@@ -1449,9 +1506,10 @@ export function renderSessionList(sessions) {
1449
1506
  if (item.type === "session" && item.data && !isSessionVisibleBySearch(item.data.id)) {
1450
1507
  continue;
1451
1508
  }
1509
+ if ((item.type === "driver-hierarchy" || item.type === "orphan-workers") && !hierarchyItemMatches(item, searchMatchIds)) continue;
1452
1510
  if (item.type === "split-group" && searchMatchIds !== null &&
1453
1511
  !searchMatchIds.has(item.members[0].id) && !searchMatchIds.has(item.members[1].id)) continue;
1454
- if (item.type === "session" && item.data && item.data.bookmarked) {
1512
+ if ((item.type === "session" || item.type === "driver-hierarchy") && item.data && item.data.bookmarked) {
1455
1513
  bookmarkedItems.push(item);
1456
1514
  } else {
1457
1515
  regularItems.push(item);
@@ -1468,7 +1526,8 @@ export function renderSessionList(sessions) {
1468
1526
  favoritesContainer.appendChild(emptyHint);
1469
1527
  }
1470
1528
  for (var bi = 0; bi < bookmarkedItems.length; bi++) {
1471
- favoritesContainer.appendChild(renderSessionItem(bookmarkedItems[bi].data));
1529
+ if (bookmarkedItems[bi].type === "driver-hierarchy") favoritesContainer.appendChild(renderDesktopDriverHierarchy(bookmarkedItems[bi].root, renderSessionItem, function () { renderSessionList(null); }, searchMatchIds));
1530
+ else favoritesContainer.appendChild(renderSessionItem(bookmarkedItems[bi].data));
1472
1531
  }
1473
1532
 
1474
1533
  var divider = document.createElement("div");
@@ -1508,6 +1567,10 @@ export function renderSessionList(sessions) {
1508
1567
  }
1509
1568
  } else if (item.type === "split-group") {
1510
1569
  regularContainer.appendChild(renderSplitGroupItem(item.group, item.members));
1570
+ } else if (item.type === "driver-hierarchy") {
1571
+ regularContainer.appendChild(renderDesktopDriverHierarchy(item.root, renderSessionItem, function () { renderSessionList(null); }, searchMatchIds));
1572
+ } else if (item.type === "orphan-workers") {
1573
+ regularContainer.appendChild(renderDesktopOrphanHierarchy(item.workers, renderSessionItem, function () { renderSessionList(null); }, searchMatchIds));
1511
1574
  } else {
1512
1575
  regularContainer.appendChild(renderSessionItem(item.data));
1513
1576
  }
@@ -1516,6 +1579,9 @@ export function renderSessionList(sessions) {
1516
1579
  refreshIcons();
1517
1580
  if (updatePageTitle) updatePageTitle();
1518
1581
  syncHeaderSearchUi();
1582
+ // Recorded from the sessions actually rendered, so a null-argument re-render
1583
+ // driven by other state refreshes the fingerprint rather than stranding it.
1584
+ lastRenderFingerprint = sessionRenderFingerprint(cachedSessions);
1519
1585
  }
1520
1586
 
1521
1587
  // --- Search results ---
@@ -1,4 +1,4 @@
1
- import { closeArchive } from './sticky-notes.js';
1
+ import { closeNotesBrowser } from './sticky-notes-browser.js';
2
2
  import { closeScheduler } from './scheduler.js';
3
3
  import { initSidebarSessions } from './sidebar-sessions.js';
4
4
  import { focusFileTree } from './filebrowser.js';
@@ -23,7 +23,7 @@ export function syncResizeHandles() {
23
23
  }
24
24
 
25
25
  export function dismissOverlayPanels() {
26
- closeArchive();
26
+ closeNotesBrowser();
27
27
  closeScheduler();
28
28
  }
29
29
 
@@ -119,7 +119,10 @@ export function initSidebar(_ctx) {
119
119
 
120
120
  // --- Panel switch (sessions / files / projects) ---
121
121
  var fileBrowserBtn = ctx.$("file-browser-btn");
122
- var gitBtn = ctx.$("git-sidebar-btn");
122
+ // Git has no tool-palette tile. The repository placard's More control is
123
+ // the entry point into the same panel, with the same lifecycle.
124
+ var gitPlacard = ctx.$("git-placard");
125
+ var gitPlacardMore = ctx.$("git-placard-more");
123
126
  var projectsPanel = ctx.$("sidebar-panel-projects");
124
127
  var sessionsPanel = ctx.$("sidebar-panel-sessions");
125
128
  var filesPanel = ctx.$("sidebar-panel-files");
@@ -216,14 +219,23 @@ export function initSidebar(_ctx) {
216
219
  });
217
220
  });
218
221
  }
219
- if (gitBtn) {
220
- gitBtn.addEventListener("click", function () {
222
+ if (gitPlacardMore) {
223
+ gitPlacardMore.addEventListener("click", function () {
221
224
  if (gitPanel && !gitPanel.classList.contains("hidden")) {
222
225
  hideGitPanel(function () { showSessionsPanel(); });
223
226
  }
224
227
  else showGitPanel();
225
228
  });
226
229
  }
230
+ if (gitPlacard && gitPlacardMore) {
231
+ // Clicking the block itself is a pointer convenience only; it forwards to
232
+ // the More control so every listener bound there runs exactly once, and
233
+ // the keyboard path stays the single focusable button.
234
+ gitPlacard.addEventListener("click", function (event) {
235
+ if (event.target.closest("#git-placard-more")) return;
236
+ gitPlacardMore.click();
237
+ });
238
+ }
227
239
  if (gitPanelClose) {
228
240
  gitPanelClose.addEventListener("click", function () {
229
241
  hideGitPanel(function () { showSessionsPanel(); });
@@ -16,3 +16,11 @@ export function findSplitGroup(groups, memberIds) {
16
16
  }
17
17
  return null;
18
18
  }
19
+
20
+ export function isConfiguredWorker(groups, sessionId) {
21
+ var list = groups || [];
22
+ for (var i = 0; i < list.length; i++) {
23
+ if (list[i].pair && list[i].pair.workerId === sessionId) return true;
24
+ }
25
+ return false;
26
+ }
@@ -7,7 +7,7 @@ import { iconHtml, refreshIcons } from './icons.js';
7
7
  import { detachTuiView } from './session-tui-view.js';
8
8
  import { formatTokens } from './app-panels.js';
9
9
  import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
10
- import { groupedSessionIds, findSplitGroup } from './split-group-helpers.js';
10
+ import { groupedSessionIds, findSplitGroup, isConfiguredWorker } from './split-group-helpers.js';
11
11
  import { showConfirm } from './app-misc.js';
12
12
  import { syncPairChrome } from './split-pair-ui.js';
13
13
  import { presentMarkdownEdit } from './filebrowser.js';
@@ -159,7 +159,10 @@ export function syncPaneTitles() {
159
159
  function updatePaneFullAccessButton(button, session) {
160
160
  if (!button) return;
161
161
  var mode = session && (session.runtimeMode || session.mode || "gui");
162
- var visible = !!session && !store.get('skipPermsEnabled') && mode === "gui";
162
+ var worker = !!session && isConfiguredWorker(store.get('splitGroups'), session.id);
163
+ // Permission policy belongs to the Driver. A configured Worker inherits it
164
+ // server-side and must not present a second, misleading control.
165
+ var visible = !!session && !worker && !store.get('skipPermsEnabled') && mode === "gui";
163
166
  var enabled = visible && session.permissionMode === "bypassPermissions";
164
167
  button.classList.toggle("hidden", !visible);
165
168
  button.classList.toggle("active", enabled);
@@ -58,6 +58,11 @@ function nodeToMarkdown(node) {
58
58
  return "\n" + inner;
59
59
  case "P": return "\n" + inner;
60
60
  case "A": return node.getAttribute("href") || inner;
61
+ case "BUTTON":
62
+ if (node.classList.contains("clayos-log-link") && node.dataset && node.dataset.logRef) {
63
+ return "`" + node.dataset.logRef + "`";
64
+ }
65
+ return inner;
61
66
  case "SPAN":
62
67
  if (node.classList.contains("sn-check")) {
63
68
  return node.classList.contains("checked") ? "- [x]" : "- [ ]";
@@ -0,0 +1,326 @@
1
+ // Sticky Notes browser: the bounded right workbench window over the board.
2
+ //
3
+ // This is the index/viewer surface, not the floating cards. The cards stay on
4
+ // the canvas over the conversation; this pane lists them, splits them into Open
5
+ // and Closed, and is where a note is completed or brought back.
6
+ //
7
+ // Completion here is Close, never delete. A closed note leaves the active
8
+ // canvas and moves to the Closed tab, where it stays readable and reversible.
9
+ // There is deliberately no destructive control on this surface at all.
10
+
11
+ import { store } from './store.js';
12
+ import { iconHtml, refreshIcons } from './icons.js';
13
+ import { renderMiniMarkdown, getTitle } from './sticky-note-markdown.js';
14
+ import { enhanceClayLogLinks } from './clay-log-links.js';
15
+ import { send } from './sticky-notes-shared.js';
16
+ import { listNoteData, focusNoteOnCanvas, hideNotes, showNotes } from './sticky-notes.js';
17
+
18
+ var panel = null;
19
+ var gridEl = null;
20
+ var tabsEl = null;
21
+
22
+ // Registered by app.js so this module never imports the other right-pane tools
23
+ // and cannot form an import cycle with them.
24
+ var exclusiveClosers = [];
25
+
26
+ export function registerExclusiveClosers(fns) {
27
+ exclusiveClosers = (fns || []).filter(function (fn) { return typeof fn === "function"; });
28
+ }
29
+
30
+ function closeOtherRightTools() {
31
+ for (var i = 0; i < exclusiveClosers.length; i++) {
32
+ try { exclusiveClosers[i](); } catch (e) { /* a missing tool must not block opening */ }
33
+ }
34
+ }
35
+
36
+ function isClosed(data) {
37
+ if (!data) return false;
38
+ if (data.state === "closed") return true;
39
+ if (data.state === "open") return false;
40
+ // Legacy notes predate the state field; `hidden` was the old spelling.
41
+ return data.hidden === true;
42
+ }
43
+
44
+ export function openNotes(all) {
45
+ return (all || []).filter(function (data) { return !isClosed(data); });
46
+ }
47
+
48
+ export function closedNotes(all) {
49
+ return (all || []).filter(isClosed);
50
+ }
51
+
52
+ function currentTab() {
53
+ return store.get('notesBrowserTab') === "closed" ? "closed" : "open";
54
+ }
55
+
56
+ // --- Panel ---------------------------------------------------------------
57
+
58
+ function ensurePanel() {
59
+ if (panel) return;
60
+ var panels = document.getElementById("main-panels");
61
+ if (!panels) return;
62
+ panel = document.createElement("section");
63
+ panel.id = "notes-browser";
64
+ panel.className = "notes-browser hidden";
65
+ panel.setAttribute("aria-label", "Sticky Notes");
66
+ panel.innerHTML =
67
+ '<header class="notes-browser-topbar">' +
68
+ '<span class="notes-browser-title">' + iconHtml("sticky-note") + 'Sticky Notes</span>' +
69
+ '<span class="notes-browser-subtitle">Open items on the board, and what has been closed</span>' +
70
+ '<div class="notes-browser-window-actions" aria-label="Sticky Notes window controls">' +
71
+ '<button id="notes-browser-wide" class="notes-browser-icon-btn" type="button" title="Widen panel" aria-label="Widen Sticky Notes panel" aria-pressed="false">' + iconHtml("chevrons-left-right") + '</button>' +
72
+ '<button id="notes-browser-fullscreen" class="notes-browser-icon-btn" type="button" title="Toggle fullscreen" aria-label="Toggle Sticky Notes fullscreen" aria-pressed="false">' + iconHtml("maximize-2") + '</button>' +
73
+ '<button id="notes-browser-close" class="notes-browser-icon-btn" type="button" title="Close Sticky Notes" aria-label="Close Sticky Notes">' + iconHtml("x") + '</button>' +
74
+ '</div>' +
75
+ '</header>' +
76
+ '<div id="notes-browser-tabs" class="notes-browser-tabs" role="tablist" aria-label="Sticky Notes state"></div>' +
77
+ '<div id="notes-browser-grid" class="notes-browser-grid" role="list"></div>';
78
+ panels.appendChild(panel);
79
+
80
+ gridEl = panel.querySelector("#notes-browser-grid");
81
+ tabsEl = panel.querySelector("#notes-browser-tabs");
82
+
83
+ panel.querySelector("#notes-browser-close").addEventListener("click", closeNotesBrowser);
84
+ panel.querySelector("#notes-browser-wide").addEventListener("click", function () {
85
+ applyWindowState(!store.get('notesBrowserWide'), store.get('notesBrowserFullscreen'));
86
+ });
87
+ panel.querySelector("#notes-browser-fullscreen").addEventListener("click", function () {
88
+ applyWindowState(store.get('notesBrowserWide'), !store.get('notesBrowserFullscreen'));
89
+ });
90
+ refreshIcons();
91
+ }
92
+
93
+ // Width and fullscreen are transient view state, so they live in the store.
94
+ // `panel-fullscreen` is the shared class the document viewer, terminal, and
95
+ // Logs already use, so the geometry is the established one.
96
+ function applyWindowState(wide, fullscreen) {
97
+ store.set({ notesBrowserWide: !!wide, notesBrowserFullscreen: !!fullscreen });
98
+ if (!panel) return;
99
+ panel.classList.toggle("notes-browser-wide", !!wide && !fullscreen);
100
+ panel.classList.toggle("panel-fullscreen", !!fullscreen);
101
+ var wideBtn = panel.querySelector("#notes-browser-wide");
102
+ var fullBtn = panel.querySelector("#notes-browser-fullscreen");
103
+ if (wideBtn) {
104
+ wideBtn.setAttribute("aria-pressed", wide ? "true" : "false");
105
+ wideBtn.disabled = !!fullscreen;
106
+ }
107
+ if (fullBtn) fullBtn.setAttribute("aria-pressed", fullscreen ? "true" : "false");
108
+ }
109
+
110
+ // --- Tabs ----------------------------------------------------------------
111
+
112
+ function renderTabs(openCount, closedCount) {
113
+ if (!tabsEl) return;
114
+ var active = currentTab();
115
+ tabsEl.innerHTML = "";
116
+ var specs = [
117
+ { key: "open", label: "Open", count: openCount },
118
+ { key: "closed", label: "Closed", count: closedCount },
119
+ ];
120
+ for (var i = 0; i < specs.length; i++) {
121
+ (function (spec) {
122
+ var tab = document.createElement("button");
123
+ tab.type = "button";
124
+ tab.className = "notes-browser-tab" + (spec.key === active ? " active" : "");
125
+ tab.setAttribute("role", "tab");
126
+ tab.setAttribute("aria-selected", spec.key === active ? "true" : "false");
127
+ tab.tabIndex = spec.key === active ? 0 : -1;
128
+ tab.dataset.tab = spec.key;
129
+ tab.innerHTML = '<span>' + spec.label + '</span><span class="notes-browser-tab-count">' + spec.count + '</span>';
130
+ tab.addEventListener("click", function () { selectTab(spec.key); });
131
+ tab.addEventListener("keydown", function (e) {
132
+ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
133
+ e.preventDefault();
134
+ selectTab(spec.key === "open" ? "closed" : "open");
135
+ var next = tabsEl.querySelector('.notes-browser-tab.active');
136
+ if (next) next.focus();
137
+ });
138
+ tabsEl.appendChild(tab);
139
+ })(specs[i]);
140
+ }
141
+ }
142
+
143
+ function selectTab(key) {
144
+ store.set({ notesBrowserTab: key === "closed" ? "closed" : "open" });
145
+ renderNotesBrowser();
146
+ }
147
+
148
+ // --- Cards ---------------------------------------------------------------
149
+
150
+ function closedMeta(data) {
151
+ var parts = [];
152
+ if (data.closedAt) {
153
+ var when = new Date(data.closedAt);
154
+ if (!isNaN(when.getTime())) parts.push("Closed " + when.toLocaleDateString());
155
+ } else {
156
+ parts.push("Closed");
157
+ }
158
+ var by = data.closedBy;
159
+ if (by && by.type === "user" && by.displayName) parts.push("by " + by.displayName);
160
+ else if (by && by.type === "session") parts.push("by an agent session");
161
+ return parts.join(" · ");
162
+ }
163
+
164
+ function buildCard(data) {
165
+ var card = document.createElement("article");
166
+ var closed = isClosed(data);
167
+ card.className = "notes-browser-card" + (closed ? " closed" : "");
168
+ card.setAttribute("role", "listitem");
169
+ card.dataset.color = data.color || "purple";
170
+ card.dataset.noteId = data.id;
171
+
172
+ var header = document.createElement("div");
173
+ header.className = "notes-browser-card-header";
174
+
175
+ var title = document.createElement("h3");
176
+ title.className = "notes-browser-card-title";
177
+ title.textContent = getTitle(data.text) || "Untitled";
178
+ header.appendChild(title);
179
+
180
+ // Exactly one lifecycle action per card, and it is never destructive.
181
+ var action = document.createElement("button");
182
+ action.type = "button";
183
+ action.className = "notes-browser-card-action";
184
+ if (closed) {
185
+ action.innerHTML = iconHtml("rotate-ccw") + "<span>Reopen</span>";
186
+ action.title = "Reopen this note and return it to the board";
187
+ action.setAttribute("aria-label", "Reopen note: " + title.textContent);
188
+ action.addEventListener("click", function (e) {
189
+ e.stopPropagation();
190
+ send({ type: "note_reopen", id: data.id });
191
+ });
192
+ } else {
193
+ action.innerHTML = iconHtml("check") + "<span>Close</span>";
194
+ action.title = "Close this note; it stays on record and can be reopened";
195
+ action.setAttribute("aria-label", "Close note: " + title.textContent);
196
+ action.addEventListener("click", function (e) {
197
+ e.stopPropagation();
198
+ send({ type: "note_close", id: data.id });
199
+ });
200
+ }
201
+ header.appendChild(action);
202
+ card.appendChild(header);
203
+
204
+ var body = document.createElement("div");
205
+ body.className = "notes-browser-card-body";
206
+ var bodyLines = (data.text || "").split("\n").slice(1).join("\n").trim();
207
+ if (bodyLines) {
208
+ body.innerHTML = renderMiniMarkdown("_\n" + bodyLines).replace('<div class="sn-title">_</div>', "");
209
+ enhanceClayLogLinks(body);
210
+ }
211
+ card.appendChild(body);
212
+
213
+ if (closed) {
214
+ var meta = document.createElement("p");
215
+ meta.className = "notes-browser-card-meta";
216
+ meta.textContent = closedMeta(data);
217
+ card.appendChild(meta);
218
+ }
219
+
220
+ var strip = document.createElement("div");
221
+ strip.className = "notes-browser-card-color";
222
+ card.appendChild(strip);
223
+
224
+ // An open note can be jumped to on the canvas. A closed note has no card on
225
+ // the canvas, so reopening is the only way back and the body is read-only.
226
+ if (!closed) {
227
+ card.tabIndex = 0;
228
+ card.addEventListener("click", function () { focusNoteOnCanvas(data.id); });
229
+ card.addEventListener("keydown", function (e) {
230
+ if (e.key !== "Enter" && e.key !== " ") return;
231
+ e.preventDefault();
232
+ focusNoteOnCanvas(data.id);
233
+ });
234
+ }
235
+ return card;
236
+ }
237
+
238
+ function emptyState(tab) {
239
+ var empty = document.createElement("div");
240
+ empty.className = "notes-browser-empty";
241
+ if (tab === "closed") {
242
+ empty.innerHTML = iconHtml("check") +
243
+ "<p>Nothing closed yet</p>" +
244
+ "<p class=\"notes-browser-empty-sub\">Closing a note takes it off the board without deleting it. Anything you close shows up here and can be reopened.</p>";
245
+ } else {
246
+ empty.innerHTML = iconHtml("sticky-note") +
247
+ "<p>Keep what matters across sessions</p>" +
248
+ "<p class=\"notes-browser-empty-sub\">Use sticky notes for checklists, goals, handoffs, and durable project knowledge. You and every Clay agent can find them in future sessions.</p>";
249
+ }
250
+ return empty;
251
+ }
252
+
253
+ export function renderNotesBrowser() {
254
+ if (!gridEl) return;
255
+ var all = listNoteData();
256
+ var open = openNotes(all);
257
+ var closed = closedNotes(all);
258
+ renderTabs(open.length, closed.length);
259
+
260
+ var tab = currentTab();
261
+ var shown = tab === "closed" ? closed : open;
262
+ // Newest first. The id is timestamp-prefixed, so it orders by creation.
263
+ shown = shown.slice().sort(function (a, b) {
264
+ var left = String(a.id || "");
265
+ var right = String(b.id || "");
266
+ return left < right ? 1 : left > right ? -1 : 0;
267
+ });
268
+
269
+ gridEl.innerHTML = "";
270
+ if (shown.length === 0) {
271
+ gridEl.appendChild(emptyState(tab));
272
+ refreshIcons();
273
+ return;
274
+ }
275
+ for (var i = 0; i < shown.length; i++) gridEl.appendChild(buildCard(shown[i]));
276
+ refreshIcons();
277
+ }
278
+
279
+ // --- Lifecycle -----------------------------------------------------------
280
+
281
+ // The conversation, composer, and title bar stay mounted and visible: the
282
+ // browser opens beside them in the single right workbench slot.
283
+ export function openNotesBrowser() {
284
+ ensurePanel();
285
+ if (!panel) return;
286
+ closeOtherRightTools();
287
+ // The floating canvas would otherwise sit on top of the pane.
288
+ hideNotes();
289
+ panel.classList.remove("hidden");
290
+ applyWindowState(store.get('notesBrowserWide'), false);
291
+ store.set({ notesBrowserOpen: true });
292
+ var sidebarBtn = document.getElementById("sticky-notes-sidebar-btn");
293
+ if (sidebarBtn) sidebarBtn.classList.add("active");
294
+ renderNotesBrowser();
295
+ }
296
+
297
+ export function closeNotesBrowser() {
298
+ if (!store.get('notesBrowserOpen')) return;
299
+ if (panel) panel.classList.add("hidden");
300
+ // Fullscreen is always dropped on close so the next open is a bounded pane.
301
+ applyWindowState(store.get('notesBrowserWide'), false);
302
+ store.set({ notesBrowserOpen: false });
303
+ var sidebarBtn = document.getElementById("sticky-notes-sidebar-btn");
304
+ if (sidebarBtn) sidebarBtn.classList.remove("active");
305
+ // The canvas comes back so the open notes are reachable again.
306
+ showNotes();
307
+ }
308
+
309
+ export function isNotesBrowserOpen() {
310
+ return !!store.get('notesBrowserOpen');
311
+ }
312
+
313
+ export function initNotesBrowser() {
314
+ document.addEventListener("keydown", function (e) {
315
+ if (e.key !== "Escape") return;
316
+ if (!store.get('notesBrowserOpen')) return;
317
+ closeNotesBrowser();
318
+ });
319
+ store.subscribe(function (state, previous) {
320
+ if (state.currentSlug === previous.currentSlug) return;
321
+ // A different project starts from a closed, bounded, Open-tab pane.
322
+ closeNotesBrowser();
323
+ applyWindowState(false, false);
324
+ store.set({ notesBrowserTab: "open" });
325
+ });
326
+ }