clay-server 3.8.1 → 4.0.0-beta.10

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 (222) hide show
  1. package/lib/ask-user-mcp-server.js +5 -119
  2. package/lib/background-task-timing.js +105 -0
  3. package/lib/builtin-mates.js +6 -6
  4. package/lib/claude-hook-installer.js +15 -0
  5. package/lib/clay-history-mcp-server.js +62 -297
  6. package/lib/daemon.js +23 -1
  7. package/lib/debate-model-selection.js +119 -0
  8. package/lib/home-debate-tool-policy.js +164 -0
  9. package/lib/home-response-notifications.js +26 -0
  10. package/lib/knowledge-import.js +436 -0
  11. package/lib/knowledge-record-store.js +188 -0
  12. package/lib/knowledge-search.js +224 -0
  13. package/lib/mate-creation-mcp-server.js +55 -0
  14. package/lib/mate-knowledge-mcp-server.js +151 -0
  15. package/lib/mate-knowledge-migration.js +283 -0
  16. package/lib/mate-knowledge-service.js +488 -0
  17. package/lib/mate-knowledge-sync.js +482 -0
  18. package/lib/mate-ready-creation.js +95 -0
  19. package/lib/mates.js +2 -0
  20. package/lib/notes.js +2 -1
  21. package/lib/project-ask-user.js +64 -0
  22. package/lib/project-capsule-catalog.js +101 -0
  23. package/lib/project-connection.js +17 -0
  24. package/lib/project-debate-proposal.js +133 -17
  25. package/lib/project-debate.js +214 -110
  26. package/lib/project-delegated-follow-up.js +84 -0
  27. package/lib/project-delegated-session.js +53 -0
  28. package/lib/project-filesystem.js +7 -9
  29. package/lib/project-knowledge.js +23 -0
  30. package/lib/project-logs-comments.js +137 -0
  31. package/lib/project-logs-mcp-server.js +290 -0
  32. package/lib/project-logs-query.js +167 -0
  33. package/lib/project-logs-root.js +67 -0
  34. package/lib/project-logs-schema.js +259 -0
  35. package/lib/project-logs-service.js +357 -0
  36. package/lib/project-logs-snapshot.js +202 -0
  37. package/lib/project-logs-store.js +498 -0
  38. package/lib/project-logs-versioning.js +128 -0
  39. package/lib/project-logs.js +347 -0
  40. package/lib/project-mate-creation-proposal.js +120 -0
  41. package/lib/project-mate-interaction.js +14 -0
  42. package/lib/project-mate-knowledge.js +110 -0
  43. package/lib/project-memory.js +36 -7
  44. package/lib/project-models.js +157 -32
  45. package/lib/project-session-notes.js +2 -2
  46. package/lib/project-session-pair.js +8 -8
  47. package/lib/project-sessions.js +66 -51
  48. package/lib/project-vendor-login.js +414 -0
  49. package/lib/project-worker-proposal.js +25 -25
  50. package/lib/project-workspace-query.js +144 -0
  51. package/lib/project.js +433 -102
  52. package/lib/public/app.js +109 -48
  53. package/lib/public/css/avatar-imprints.css +0 -1
  54. package/lib/public/css/base.css +4 -0
  55. package/lib/public/css/capsule-ui.css +389 -0
  56. package/lib/public/css/command-palette.css +306 -6
  57. package/lib/public/css/home-capsule-library.css +243 -0
  58. package/lib/public/css/home-capsule-source.css +192 -0
  59. package/lib/public/css/home-debate-live.css +146 -0
  60. package/lib/public/css/home-debate-planning.css +125 -0
  61. package/lib/public/css/home-debates-archive.css +281 -0
  62. package/lib/public/css/home-hub.css +896 -405
  63. package/lib/public/css/home-mate-creation.css +34 -0
  64. package/lib/public/css/home-mate-model-picker.css +131 -0
  65. package/lib/public/css/home-mate-settings.css +344 -0
  66. package/lib/public/css/home-session-actions.css +265 -0
  67. package/lib/public/css/home-sidebar.css +667 -0
  68. package/lib/public/css/icon-strip.css +1 -2
  69. package/lib/public/css/input.css +79 -25
  70. package/lib/public/css/mates.css +7 -34
  71. package/lib/public/css/pane.css +1 -1
  72. package/lib/public/css/playbook.css +0 -80
  73. package/lib/public/css/project-logs.css +234 -0
  74. package/lib/public/css/scheduler.css +1 -1
  75. package/lib/public/css/sticky-notes.css +53 -117
  76. package/lib/public/css/tui-attention.css +0 -44
  77. package/lib/public/css/workspace-assignment.css +45 -0
  78. package/lib/public/fonts/source-serif-4/LICENSE.md +93 -0
  79. package/lib/public/fonts/source-serif-4/SourceSerif4Caption-Semibold.ttf.woff2 +0 -0
  80. package/lib/public/index.html +96 -74
  81. package/lib/public/modules/app-connection.js +100 -5
  82. package/lib/public/modules/app-dm.js +43 -160
  83. package/lib/public/modules/app-home-hub.js +328 -585
  84. package/lib/public/modules/app-message-cards.js +242 -0
  85. package/lib/public/modules/app-message-router.js +84 -0
  86. package/lib/public/modules/app-messages.js +124 -38
  87. package/lib/public/modules/app-notifications.js +8 -119
  88. package/lib/public/modules/app-panels.js +43 -2
  89. package/lib/public/modules/app-projects.js +9 -1
  90. package/lib/public/modules/app-rendering.js +21 -602
  91. package/lib/public/modules/avatar-imprint.js +11 -8
  92. package/lib/public/modules/background-tasks-ui.js +139 -16
  93. package/lib/public/modules/chat-bubble-renderer.js +152 -0
  94. package/lib/public/modules/chat-render-runtime.js +198 -0
  95. package/lib/public/modules/command-palette.js +225 -436
  96. package/lib/public/modules/dm-render.js +82 -0
  97. package/lib/public/modules/home-capsule-creation-intent.js +31 -0
  98. package/lib/public/modules/home-capsule-library.js +146 -0
  99. package/lib/public/modules/home-capsule-source.js +383 -0
  100. package/lib/public/modules/home-chat-identity.js +35 -0
  101. package/lib/public/modules/home-chat-scroll.js +77 -0
  102. package/lib/public/modules/home-chat-stream-state.js +34 -0
  103. package/lib/public/modules/home-composer-focus.js +25 -0
  104. package/lib/public/modules/home-conversations-sheet.js +179 -0
  105. package/lib/public/modules/home-debate-activity.js +48 -0
  106. package/lib/public/modules/home-debate-controls.js +295 -0
  107. package/lib/public/modules/home-debate-launch.js +41 -0
  108. package/lib/public/modules/home-debate-live.js +284 -0
  109. package/lib/public/modules/home-debate-models.js +73 -0
  110. package/lib/public/modules/home-debate-planning.js +335 -0
  111. package/lib/public/modules/home-debates-archive.js +279 -0
  112. package/lib/public/modules/home-dock-resize.js +74 -0
  113. package/lib/public/modules/home-dock.js +446 -0
  114. package/lib/public/modules/home-mate-chat.js +496 -0
  115. package/lib/public/modules/home-mate-creation.js +109 -0
  116. package/lib/public/modules/home-mate-model-picker.js +281 -0
  117. package/lib/public/modules/home-mate-selection.js +38 -0
  118. package/lib/public/modules/home-mate-settings-menu.js +131 -0
  119. package/lib/public/modules/home-mate-settings.js +383 -0
  120. package/lib/public/modules/home-session-actions.js +373 -0
  121. package/lib/public/modules/home-session-model-confirmation.js +13 -0
  122. package/lib/public/modules/home-shell.js +9 -0
  123. package/lib/public/modules/home-sidebar-chat-list.js +63 -0
  124. package/lib/public/modules/home-sidebar.js +165 -0
  125. package/lib/public/modules/home-sub-surface.js +19 -0
  126. package/lib/public/modules/home-surface-boot.js +68 -0
  127. package/lib/public/modules/home-surface.js +141 -0
  128. package/lib/public/modules/home-tools.js +271 -0
  129. package/lib/public/modules/markdown.js +50 -2
  130. package/lib/public/modules/mate-management.js +86 -0
  131. package/lib/public/modules/mate-sidebar.js +0 -8
  132. package/lib/public/modules/mate-wizard.js +0 -7
  133. package/lib/public/modules/mention.js +17 -19
  134. package/lib/public/modules/pane-bridge.js +9 -0
  135. package/lib/public/modules/profile.js +4 -5
  136. package/lib/public/modules/project-logs-ambient.js +238 -0
  137. package/lib/public/modules/project-logs-render.js +359 -0
  138. package/lib/public/modules/project-logs.js +328 -0
  139. package/lib/public/modules/project-settings.js +34 -28
  140. package/lib/public/modules/project-switcher.js +11 -5
  141. package/lib/public/modules/project-workspace-assignment.js +23 -0
  142. package/lib/public/modules/scheduler.js +17 -3
  143. package/lib/public/modules/search-clay-chat.js +402 -0
  144. package/lib/public/modules/server-settings.js +14 -12
  145. package/lib/public/modules/session-actions.js +1 -1
  146. package/lib/public/modules/sidebar-mates.js +56 -552
  147. package/lib/public/modules/sidebar-mobile.js +12 -73
  148. package/lib/public/modules/sidebar-presence.js +37 -0
  149. package/lib/public/modules/sidebar-sessions.js +25 -1
  150. package/lib/public/modules/split-pair-ui.js +13 -13
  151. package/lib/public/modules/split-view.js +7 -0
  152. package/lib/public/modules/sticky-notes.js +106 -53
  153. package/lib/public/modules/tool-input-composition.js +48 -0
  154. package/lib/public/modules/tool-llm-status.js +167 -0
  155. package/lib/public/modules/tool-palette.js +76 -4
  156. package/lib/public/modules/tool-renderer-advanced.js +285 -0
  157. package/lib/public/modules/tool-renderer-chart.js +158 -0
  158. package/lib/public/modules/tool-renderer-semantics.js +35 -0
  159. package/lib/public/modules/tool-renderer.js +464 -0
  160. package/lib/public/modules/tool-runtime.js +309 -0
  161. package/lib/public/modules/tool-ui-evaluator.js +97 -0
  162. package/lib/public/modules/tool-ui-tree.js +21 -0
  163. package/lib/public/modules/tui-attention.js +10 -0
  164. package/lib/public/modules/user-settings.js +16 -23
  165. package/lib/public/modules/vendor-login.js +287 -0
  166. package/lib/public/modules/worker-proposal.js +2 -2
  167. package/lib/public/modules/workspace-assignment-card.js +149 -0
  168. package/lib/public/modules/workspace-assignment-routing.js +4 -0
  169. package/lib/public/style.css +14 -2
  170. package/lib/runtime-env.js +65 -0
  171. package/lib/sdk-bridge.js +120 -32
  172. package/lib/sdk-message-processor.js +34 -13
  173. package/lib/server-home-capsule-creation.js +36 -0
  174. package/lib/server-home-chat-events.js +287 -0
  175. package/lib/server-home-chat.js +518 -0
  176. package/lib/server-home-clay-entry.js +65 -0
  177. package/lib/server-home-clay-session-links.js +26 -0
  178. package/lib/server-home-debate-planning.js +214 -0
  179. package/lib/server-home-debates.js +137 -0
  180. package/lib/server-home-mate-creation.js +261 -0
  181. package/lib/server-home-models.js +241 -0
  182. package/lib/server-home-preferences.js +61 -0
  183. package/lib/server-mates.js +37 -0
  184. package/lib/server-tools.js +377 -0
  185. package/lib/server.js +166 -59
  186. package/lib/session-pair-mcp-server.js +6 -6
  187. package/lib/sessions.js +22 -4
  188. package/lib/tool-capsule-source.js +142 -0
  189. package/lib/tool-control-mcp-server.js +126 -0
  190. package/lib/tool-llm.js +48 -0
  191. package/lib/tool-storage.js +77 -0
  192. package/lib/tool-ui-spec-advanced.js +82 -0
  193. package/lib/tool-ui-spec.js +261 -0
  194. package/lib/tools-registry.js +286 -0
  195. package/lib/users-home-dock-preferences.js +77 -0
  196. package/lib/users-home-surface-preferences.js +99 -0
  197. package/lib/users-preferences.js +4 -7
  198. package/lib/users.js +12 -0
  199. package/lib/whats-new-content.js +3 -3
  200. package/lib/workspace-assignment-service.js +420 -0
  201. package/lib/workspace-query-mcp-server.js +154 -0
  202. package/lib/workspace-query-service.js +492 -0
  203. package/lib/ws-schema.js +107 -12
  204. package/lib/yoke/adapters/acp.js +2 -1
  205. package/lib/yoke/adapters/antigravity.js +2 -1
  206. package/lib/yoke/adapters/claude-worker.js +11 -4
  207. package/lib/yoke/adapters/claude.js +141 -46
  208. package/lib/yoke/adapters/codex.js +79 -11
  209. package/lib/yoke/adapters/kiro.js +2 -1
  210. package/lib/yoke/claude-user-input.js +38 -0
  211. package/lib/yoke/codex-app-server.js +20 -2
  212. package/lib/yoke/codex-background-tasks.js +8 -2
  213. package/lib/yoke/codex-user-input.js +64 -0
  214. package/lib/yoke/complete-once.js +164 -0
  215. package/lib/yoke/index.js +5 -1
  216. package/lib/yoke/interface.js +19 -0
  217. package/lib/yoke/mcp-bridge-server.js +2 -6
  218. package/lib/yoke/user-input.js +254 -0
  219. package/package.json +4 -3
  220. package/lib/public/css/home-chat.css +0 -380
  221. package/lib/public/modules/home-chat.js +0 -438
  222. package/lib/server-clay-home.js +0 -245
@@ -102,27 +102,25 @@ function buildMentionCandidates() {
102
102
  candidates.push(PLAIN_MENTION_CANDIDATES[pi]);
103
103
  }
104
104
 
105
- // Mates: only when the Mates UI is enabled
106
- if (store.get('matesEnabled') !== false) {
107
- var mates = ctx.matesList ? ctx.matesList() : [];
108
- for (var mi = 0; mi < mates.length; mi++) {
109
- var m = mates[mi];
110
- if (m.status === "interviewing") continue;
111
- candidates.push({
112
- kind: "mate",
113
- id: m.id,
114
- name: (m.profile && m.profile.displayName) || m.name || "Mate",
115
- color: (m.profile && m.profile.avatarColor) || "#5857fc",
116
- avatarSrc: mateAvatarUrl(m, 24),
117
- vendor: m.vendor || "claude",
118
- bio: m.bio || (m.profile && m.profile.bio) || "",
119
- primary: !!m.primary,
120
- raw: m,
121
- });
122
- }
105
+ // Mate mentions are a project collaboration capability, not a Mate DM.
106
+ var mates = ctx.matesList ? ctx.matesList() : [];
107
+ for (var mi = 0; mi < mates.length; mi++) {
108
+ var m = mates[mi];
109
+ if (m.status === "interviewing") continue;
110
+ candidates.push({
111
+ kind: "mate",
112
+ id: m.id,
113
+ name: (m.profile && m.profile.displayName) || m.name || "Mate",
114
+ color: (m.profile && m.profile.avatarColor) || "#5857fc",
115
+ avatarSrc: mateAvatarUrl(m, 24),
116
+ vendor: m.vendor || "claude",
117
+ bio: m.bio || (m.profile && m.profile.bio) || "",
118
+ primary: !!m.primary,
119
+ raw: m,
120
+ });
123
121
  }
124
122
 
125
- // Users: every other signed-in user (always available, even if Mates UI is off).
123
+ // Users: every other signed-in user.
126
124
  // We use the cached user list pushed by the server in projects_updated.
127
125
  var allUsers = ctx.allUsers ? ctx.allUsers() : [];
128
126
  var myId = ctx.myUserId ? ctx.myUserId() : null;
@@ -23,6 +23,15 @@ export function reportPaneContext(data) {
23
23
  }, window.location.origin);
24
24
  }
25
25
 
26
+ // A pane has no banner surface and no login modal of its own, so both panes
27
+ // hitting auth_required would otherwise race to start their own login flow.
28
+ // Hand the event to the parent shell, which owns the single flow.
29
+ export function forwardPaneAuthRequired(message) {
30
+ if (!store.get('paneMode') || window.parent === window) return false;
31
+ window.parent.postMessage({ type: "clay-pane-auth-required", message: message }, window.location.origin);
32
+ return true;
33
+ }
34
+
26
35
  export function forwardPaneMarkdownPresentation(message) {
27
36
  if (!store.get('paneMode') || window.parent === window) return false;
28
37
  window.parent.postMessage({ type: "clay-pane-present-markdown", message: message }, window.location.origin);
@@ -531,12 +531,11 @@ export function initProfile(_ctx) {
531
531
  applyTerminalFont(data.terminalFont.family, data.terminalFont.size);
532
532
  }
533
533
 
534
- // Apply Mates UI gate at boot so the sidebar avatars / DM picker
535
- // entry / home-hub strip are hidden before the user opens settings.
536
- // Default OFF: Mates is opt-in, so only enable when explicitly true.
534
+ // Apply the project-only Mate DM shortcut preference at boot. Home and
535
+ // the rest of the Mate capability surface remain available.
537
536
  var matesOn = data.matesEnabled === true;
538
- store.set({ matesEnabled: matesOn });
539
- if (document.body) document.body.classList.toggle('mates-disabled', !matesOn);
537
+ store.set({ projectMateDmsEnabled: matesOn });
538
+ if (document.body) document.body.classList.toggle('project-mate-dms-disabled', !matesOn);
540
539
  }).catch(function(err) {
541
540
  console.warn('[Profile] Failed to load:', err);
542
541
  });
@@ -0,0 +1,238 @@
1
+ // Ambient discovery for Project Logs.
2
+ //
3
+ // The ledger accumulates alongside the work, so it should be reachable without
4
+ // being hidden and noticeable without interrupting. Two affordances:
5
+ //
6
+ // - a slim handle on the right workbench edge that previews the pane on
7
+ // hover or focus, and pins it open on any meaningful interaction
8
+ // - a restrained unread marker on the toolbar button and the handle when the
9
+ // Project Driver revises the ledger
10
+ //
11
+ // A preview is explicitly not an open: it closes itself when the pointer
12
+ // leaves and never survives an Escape. Nothing here opens the pane on its own,
13
+ // and an update never opens anything at all.
14
+
15
+ import { store } from './store.js';
16
+
17
+ var handle = null;
18
+ var closeTimer = null;
19
+ var hoverCapable = false;
20
+
21
+ // Long enough to cross the gap from handle to panel without losing the
22
+ // preview, short enough not to feel stuck.
23
+ var CLOSE_DELAY_MS = 420;
24
+
25
+ // Callbacks supplied by project-logs.js so this module never talks to the
26
+ // WebSocket or decides navigation.
27
+ var hooks = { reveal: null, hide: null, pin: null, isOpen: null, isPinned: null };
28
+
29
+ function prefersReducedMotion() {
30
+ try {
31
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
32
+ } catch (e) {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ // Hover preview is only offered to a device that genuinely hovers. Touch keeps
38
+ // the explicit toolbar and navigation behaviour with no hover dependency.
39
+ function detectHoverCapable() {
40
+ try {
41
+ return window.matchMedia("(hover: hover) and (pointer: fine)").matches;
42
+ } catch (e) {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ function cancelClose() {
48
+ if (closeTimer) {
49
+ clearTimeout(closeTimer);
50
+ closeTimer = null;
51
+ }
52
+ }
53
+
54
+ function scheduleClose() {
55
+ cancelClose();
56
+ closeTimer = setTimeout(function () {
57
+ closeTimer = null;
58
+ // A pinned pane is a real open and is never closed by the pointer leaving.
59
+ if (store.get('projectLogsPinned')) return;
60
+ if (!store.get('projectLogsPreview')) return;
61
+ hidePreview();
62
+ }, CLOSE_DELAY_MS);
63
+ }
64
+
65
+ export function showPreview() {
66
+ if (!hooks.reveal) return;
67
+ if (store.get('projectLogsPinned') || store.get('projectLogsOpen')) return;
68
+ cancelClose();
69
+ if (store.get('projectLogsPreview')) return;
70
+ store.set({ projectLogsPreview: true });
71
+ hooks.reveal();
72
+ syncHandle();
73
+ }
74
+
75
+ export function hidePreview() {
76
+ cancelClose();
77
+ if (!store.get('projectLogsPreview')) return;
78
+ store.set({ projectLogsPreview: false });
79
+ if (hooks.hide) hooks.hide();
80
+ syncHandle();
81
+ }
82
+
83
+ // Any meaningful interaction turns a preview into a real open.
84
+ export function pinFromPreview() {
85
+ cancelClose();
86
+ if (!hooks.pin) return;
87
+ store.set({ projectLogsPreview: false });
88
+ hooks.pin();
89
+ syncHandle();
90
+ }
91
+
92
+ // The handle only exists while the pane is closed; a visible pane is its own
93
+ // affordance.
94
+ function syncHandle() {
95
+ if (!handle) return;
96
+ var open = !!(store.get('projectLogsOpen') || store.get('projectLogsPreview'));
97
+ handle.classList.toggle("hidden", open);
98
+ var unread = store.get('projectLogsUnread') || 0;
99
+ handle.classList.toggle("has-unread", unread > 0);
100
+ handle.setAttribute("aria-label", unread > 0
101
+ ? "Open Project Logs, " + unread + (unread === 1 ? " new entry" : " new entries")
102
+ : "Open Project Logs");
103
+ handle.title = handle.getAttribute("aria-label");
104
+ }
105
+
106
+ // The toolbar button carries the same unread state, so the cue is visible
107
+ // wherever the user is looking.
108
+ function syncToolbar() {
109
+ var button = document.getElementById("project-logs-btn");
110
+ if (!button) return;
111
+ var unread = store.get('projectLogsUnread') || 0;
112
+ button.classList.toggle("project-logs-unread", unread > 0);
113
+ var badge = document.getElementById("project-logs-count");
114
+ if (badge) {
115
+ badge.textContent = unread > 0 ? String(unread) : "";
116
+ badge.classList.toggle("hidden", unread < 1);
117
+ }
118
+ }
119
+
120
+ export function syncAmbient() {
121
+ syncHandle();
122
+ syncToolbar();
123
+ }
124
+
125
+ // A canonical revision arrived. Mark it, never open anything.
126
+ //
127
+ // Deduped by the server-derived reference plus revision number, so a replayed
128
+ // or duplicated frame cannot inflate the count or re-trigger the flourish.
129
+ export function noteCanonicalUpdate(msg) {
130
+ if (!msg || !msg.ref || !msg.revision) return false;
131
+ var seen = store.get('projectLogsSeenRevisions') || {};
132
+ var key = msg.ref;
133
+ if (seen[key] >= msg.revision) return false;
134
+ var next = Object.assign({}, seen);
135
+ next[key] = msg.revision;
136
+
137
+ // An update while the pane is open is already visible, so it is acknowledged
138
+ // rather than counted.
139
+ var visible = !!(store.get('projectLogsOpen') || store.get('projectLogsPinned'));
140
+ store.set({
141
+ projectLogsSeenRevisions: next,
142
+ projectLogsUnread: visible ? 0 : (store.get('projectLogsUnread') || 0) + 1,
143
+ });
144
+ syncAmbient();
145
+ if (!visible) flourish();
146
+ return true;
147
+ }
148
+
149
+ // A single restrained pulse on the two ambient surfaces. Never the whole
150
+ // screen, and nothing at all when the user asked for reduced motion.
151
+ function flourish() {
152
+ if (prefersReducedMotion()) return;
153
+ var targets = [handle, document.getElementById("project-logs-btn")];
154
+ for (var i = 0; i < targets.length; i++) {
155
+ var target = targets[i];
156
+ if (!target) continue;
157
+ target.classList.remove("project-logs-pulse");
158
+ // Force a reflow so the animation restarts rather than being ignored.
159
+ void target.offsetWidth;
160
+ target.classList.add("project-logs-pulse");
161
+ }
162
+ }
163
+
164
+ export function acknowledgeUpdates() {
165
+ if (!store.get('projectLogsUnread')) {
166
+ syncAmbient();
167
+ return;
168
+ }
169
+ store.set({ projectLogsUnread: 0 });
170
+ var targets = [handle, document.getElementById("project-logs-btn")];
171
+ for (var i = 0; i < targets.length; i++) {
172
+ if (targets[i]) targets[i].classList.remove("project-logs-pulse");
173
+ }
174
+ syncAmbient();
175
+ }
176
+
177
+ export function resetAmbient() {
178
+ cancelClose();
179
+ store.set({
180
+ projectLogsUnread: 0,
181
+ projectLogsSeenRevisions: {},
182
+ projectLogsPreview: false,
183
+ projectLogsPinned: false,
184
+ });
185
+ syncAmbient();
186
+ }
187
+
188
+ // Called by project-logs.js when the pane itself is entered or left, so moving
189
+ // from the handle into the panel keeps the preview alive.
190
+ export function bindPanelHover(panel) {
191
+ if (!hoverCapable || !panel) return;
192
+ panel.addEventListener("pointerenter", cancelClose);
193
+ panel.addEventListener("pointerleave", function () {
194
+ if (store.get('projectLogsPreview')) scheduleClose();
195
+ });
196
+ }
197
+
198
+ export function initProjectLogsAmbient(callbacks) {
199
+ hooks = callbacks || hooks;
200
+ hoverCapable = detectHoverCapable();
201
+
202
+ var main = document.getElementById("main-panels");
203
+ if (!main) return;
204
+
205
+ handle = document.createElement("button");
206
+ handle.id = "project-logs-handle";
207
+ handle.type = "button";
208
+ handle.className = "project-logs-handle";
209
+ handle.setAttribute("aria-label", "Open Project Logs");
210
+ handle.title = "Open Project Logs";
211
+ handle.innerHTML = '<span class="project-logs-handle-notch" aria-hidden="true"></span>';
212
+ main.appendChild(handle);
213
+
214
+ // Click and keyboard both pin, so the handle is a real control and not a
215
+ // hover-only affordance.
216
+ handle.addEventListener("click", function () {
217
+ if (store.get('projectLogsOpen')) return;
218
+ pinFromPreview();
219
+ });
220
+ handle.addEventListener("focus", function () {
221
+ if (!store.get('projectLogsOpen')) showPreview();
222
+ });
223
+ handle.addEventListener("blur", function () {
224
+ if (store.get('projectLogsPreview') && !store.get('projectLogsPinned')) scheduleClose();
225
+ });
226
+
227
+ if (hoverCapable) {
228
+ handle.addEventListener("pointerenter", showPreview);
229
+ handle.addEventListener("pointerleave", scheduleClose);
230
+ }
231
+
232
+ document.addEventListener("keydown", function (event) {
233
+ if (event.key !== "Escape") return;
234
+ if (store.get('projectLogsPreview')) hidePreview();
235
+ });
236
+
237
+ syncAmbient();
238
+ }
@@ -0,0 +1,359 @@
1
+ // Project Logs rendering — ledger rows, entry detail, and discussion.
2
+ //
3
+ // Split from project-logs.js so both stay well inside the module size limit.
4
+ // This module owns markup only: it never talks to the WebSocket and never
5
+ // decides navigation. Callers pass in the handlers they want wired.
6
+
7
+ import { refreshIcons } from './icons.js';
8
+ import { renderMarkdown, highlightCodeBlocks } from './markdown.js';
9
+
10
+ // Priority is a stable enum, so its labels are known ahead of time.
11
+ var PRIORITY_LABELS = { normal: "Normal", important: "Important", urgent: "Urgent" };
12
+
13
+ // A comment is a proposal the Project Driver reviews, so its state is shown
14
+ // plainly rather than implying the ledger changed.
15
+ var COMMENT_STATUS_LABELS = {
16
+ "pending": "Awaiting Project Driver review",
17
+ "clarification-needed": "Project Driver asked a question",
18
+ "incorporated": "Incorporated",
19
+ "declined": "Declined",
20
+ };
21
+
22
+ // Categories are each project's own evolving vocabulary, so there is no label
23
+ // table: any kebab-case label is rendered readably by de-slugging it. Every
24
+ // category reaches the DOM through textContent, never markup, so an arbitrary
25
+ // label is displayed safely whatever it contains.
26
+ export function categoryLabel(value) {
27
+ if (typeof value !== "string" || !value) return "Record";
28
+ var words = value.split("-");
29
+ var out = [];
30
+ for (var i = 0; i < words.length; i++) {
31
+ if (!words[i]) continue;
32
+ out.push(i === 0 ? words[i].charAt(0).toUpperCase() + words[i].slice(1) : words[i]);
33
+ }
34
+ return out.length ? out.join(" ") : "Record";
35
+ }
36
+
37
+ function actorLabel(actor) {
38
+ var source = actor || {};
39
+ return source.displayName || source.userName || source.userId || source.sessionKey || "Unknown author";
40
+ }
41
+
42
+ // Who authored a canonical record. The backend keeps full blame on the author
43
+ // object (userId, sessionKey, vendor), but a session-authored entry must not
44
+ // read as if the human owner wrote it: the Project Driver did. The owner is
45
+ // only the account the session ran under.
46
+ function authorLine(entry) {
47
+ var actor = (entry && (entry.updatedBy || entry.createdBy)) || {};
48
+ if (actor.type === "session") {
49
+ return actor.vendor ? "Project Driver (" + vendorLabel(actor.vendor) + ")" : "Project Driver";
50
+ }
51
+ return actorLabel(actor);
52
+ }
53
+
54
+ function vendorLabel(vendor) {
55
+ if (typeof vendor !== "string" || !vendor) return "";
56
+ return vendor.charAt(0).toUpperCase() + vendor.slice(1);
57
+ }
58
+
59
+ function formatTime(value) {
60
+ if (!value) return "";
61
+ try { return new Date(value).toLocaleString(); }
62
+ catch (e) { return ""; }
63
+ }
64
+
65
+ function relativeTime(value) {
66
+ if (!value) return "";
67
+ var delta = Date.now() - value;
68
+ if (delta < 0) return formatTime(value);
69
+ var minutes = Math.floor(delta / 60000);
70
+ if (minutes < 1) return "just now";
71
+ if (minutes < 60) return minutes + "m ago";
72
+ var hours = Math.floor(minutes / 60);
73
+ if (hours < 24) return hours + "h ago";
74
+ var days = Math.floor(hours / 24);
75
+ if (days < 30) return days + "d ago";
76
+ return formatTime(value);
77
+ }
78
+
79
+ function setText(parent, selector, value) {
80
+ var el = parent.querySelector(selector);
81
+ if (el) el.textContent = value;
82
+ }
83
+
84
+ // --- List ----------------------------------------------------------------
85
+
86
+ // The filter control. A compact select, not a dashboard: it narrows the
87
+ // ledger without becoming a surface of its own.
88
+ export function renderFilter(container, categories, selected, onChange) {
89
+ container.innerHTML = "";
90
+ var select = document.createElement("select");
91
+ select.id = "project-logs-category";
92
+ select.className = "project-logs-filter";
93
+ select.setAttribute("aria-label", "Filter by category");
94
+ var all = document.createElement("option");
95
+ all.value = "";
96
+ all.textContent = "All categories";
97
+ select.appendChild(all);
98
+ for (var i = 0; i < categories.length; i++) {
99
+ var option = document.createElement("option");
100
+ option.value = categories[i];
101
+ option.textContent = categoryLabel(categories[i]);
102
+ select.appendChild(option);
103
+ }
104
+ select.value = selected || "";
105
+ select.addEventListener("change", function () { onChange(select.value); });
106
+ container.appendChild(select);
107
+ }
108
+
109
+ // One ledger row. Reveals what happened without opening the record: category,
110
+ // priority, title, summary, author, time, revision and comment counts.
111
+ function renderRow(entry, onOpen) {
112
+ var row = document.createElement("button");
113
+ row.type = "button";
114
+ row.className = "project-log-row";
115
+ row.dataset.ref = entry.ref;
116
+ row.setAttribute("role", "listitem");
117
+
118
+ var head = document.createElement("div");
119
+ head.className = "project-log-row-head";
120
+
121
+ var chip = document.createElement("span");
122
+ chip.className = "project-log-chip";
123
+ chip.dataset.category = entry.category || entry.kind || "progress";
124
+ chip.textContent = categoryLabel(entry.category || entry.kind);
125
+ head.appendChild(chip);
126
+
127
+ if (entry.priority && entry.priority !== "normal") {
128
+ var flag = document.createElement("span");
129
+ flag.className = "project-log-priority";
130
+ flag.dataset.priority = entry.priority;
131
+ flag.textContent = PRIORITY_LABELS[entry.priority] || entry.priority;
132
+ head.appendChild(flag);
133
+ }
134
+
135
+ var title = document.createElement("span");
136
+ title.className = "project-log-row-title";
137
+ title.textContent = entry.title || "Untitled log";
138
+ head.appendChild(title);
139
+ row.appendChild(head);
140
+
141
+ if (entry.summary) {
142
+ var summary = document.createElement("p");
143
+ summary.className = "project-log-row-summary";
144
+ summary.textContent = entry.summary;
145
+ row.appendChild(summary);
146
+ }
147
+
148
+ var meta = document.createElement("div");
149
+ meta.className = "project-log-row-meta";
150
+ var parts = [authorLine(entry), relativeTime(entry.updatedAt || entry.createdAt)];
151
+ if (entry.revisions > 1) parts.push("v" + entry.revisions);
152
+ if (entry.commentCount > 0) {
153
+ parts.push(entry.commentCount + (entry.commentCount === 1 ? " comment" : " comments"));
154
+ }
155
+ meta.textContent = parts.filter(Boolean).join(" · ");
156
+ row.appendChild(meta);
157
+
158
+ row.addEventListener("click", function () { onOpen(entry.ref); });
159
+ return row;
160
+ }
161
+
162
+ export function renderList(listEl, entries, onOpen) {
163
+ listEl.innerHTML = "";
164
+ if (!entries.length) {
165
+ var empty = document.createElement("div");
166
+ empty.className = "project-logs-list-empty";
167
+ empty.textContent = "No logs yet. This project's agent sessions record decisions and work here.";
168
+ listEl.appendChild(empty);
169
+ return;
170
+ }
171
+ for (var i = 0; i < entries.length; i++) {
172
+ listEl.appendChild(renderRow(entries[i], onOpen));
173
+ }
174
+ }
175
+
176
+ // --- Detail --------------------------------------------------------------
177
+
178
+ function renderComments(entry) {
179
+ var wrap = document.createElement("section");
180
+ wrap.className = "project-log-discussion";
181
+ var heading = document.createElement("h2");
182
+ var comments = entry.comments || [];
183
+ heading.textContent = comments.length
184
+ ? "Discussion (" + comments.length + ")"
185
+ : "Discussion";
186
+ wrap.appendChild(heading);
187
+
188
+ if (!comments.length) {
189
+ var none = document.createElement("p");
190
+ none.className = "project-log-discussion-empty";
191
+ none.textContent = "No comments yet.";
192
+ wrap.appendChild(none);
193
+ }
194
+ for (var i = 0; i < comments.length; i++) {
195
+ var comment = comments[i];
196
+ var item = document.createElement("article");
197
+ item.className = "project-log-comment";
198
+
199
+ var meta = document.createElement("p");
200
+ meta.className = "project-log-comment-meta";
201
+ meta.textContent = actorLabel(comment.author) + " · " + relativeTime(comment.at);
202
+ item.appendChild(meta);
203
+
204
+ var body = document.createElement("p");
205
+ body.className = "project-log-comment-body";
206
+ body.textContent = comment.body || "";
207
+ item.appendChild(body);
208
+
209
+ var status = comment.status || "pending";
210
+ var badge = document.createElement("span");
211
+ badge.className = "project-log-comment-status-badge";
212
+ badge.dataset.status = status;
213
+ badge.textContent = COMMENT_STATUS_LABELS[status] || status;
214
+ item.appendChild(badge);
215
+
216
+ // The Driver's decision sits under the comment it answers.
217
+ if (comment.review && comment.review.response) {
218
+ var response = document.createElement("div");
219
+ response.className = "project-log-comment-response";
220
+ var responseMeta = document.createElement("p");
221
+ responseMeta.className = "project-log-comment-response-meta";
222
+ var parts = ["Project Driver"];
223
+ if (comment.review.at) parts.push(relativeTime(comment.review.at));
224
+ if (comment.review.revision) parts.push("revision " + comment.review.revision);
225
+ responseMeta.textContent = parts.join(" · ");
226
+ var responseBody = document.createElement("p");
227
+ responseBody.className = "project-log-comment-response-body";
228
+ responseBody.textContent = comment.review.response;
229
+ response.appendChild(responseMeta);
230
+ response.appendChild(responseBody);
231
+ item.appendChild(response);
232
+ }
233
+
234
+ wrap.appendChild(item);
235
+ }
236
+ return wrap;
237
+ }
238
+
239
+ // A compact, read-only record of how the entry reached its current state.
240
+ // Metadata only: no bodies, and no revert control, because restoring a
241
+ // revision is the Project Driver's decision.
242
+ function renderHistory(entry) {
243
+ var history = entry.history || [];
244
+ if (history.length < 2) return null;
245
+ var wrap = document.createElement("section");
246
+ wrap.className = "project-log-history";
247
+ var heading = document.createElement("h2");
248
+ heading.textContent = "Version history (" + history.length + ")";
249
+ wrap.appendChild(heading);
250
+
251
+ var list = document.createElement("ol");
252
+ list.className = "project-log-history-list";
253
+ for (var i = history.length - 1; i >= 0; i--) {
254
+ var revision = history[i];
255
+ var row = document.createElement("li");
256
+ row.className = "project-log-history-row";
257
+
258
+ var label = document.createElement("span");
259
+ label.className = "project-log-history-label";
260
+ label.textContent = "v" + revision.revision;
261
+ row.appendChild(label);
262
+
263
+ var text = document.createElement("span");
264
+ text.className = "project-log-history-text";
265
+ var summary = [historyVerb(revision), relativeTime(revision.at)];
266
+ if (revision.changed && revision.changed.length) summary.push(revision.changed.join(", "));
267
+ text.textContent = summary.filter(Boolean).join(" · ");
268
+ row.appendChild(text);
269
+
270
+ if (revision.reason) {
271
+ var reason = document.createElement("span");
272
+ reason.className = "project-log-history-reason";
273
+ reason.textContent = revision.reason;
274
+ row.appendChild(reason);
275
+ }
276
+ list.appendChild(row);
277
+ }
278
+ wrap.appendChild(list);
279
+ return wrap;
280
+ }
281
+
282
+ function historyVerb(revision) {
283
+ if (revision.op === "create") return "Created";
284
+ if (revision.op === "incorporate") return "Incorporated a comment";
285
+ if (revision.op === "revert") {
286
+ return revision.revertedFrom ? "Reverted to v" + revision.revertedFrom : "Reverted";
287
+ }
288
+ if (revision.op === "link") return "Linked";
289
+ return "Revised";
290
+ }
291
+
292
+ // The entry, full pane. People read here and may only comment: there is no
293
+ // edit affordance because canonical authorship belongs to agent sessions.
294
+ export function renderDetail(detailEl, entry, handlers) {
295
+ detailEl.innerHTML = "";
296
+ if (!entry) return;
297
+
298
+ var article = document.createElement("article");
299
+ article.className = "project-log-document";
300
+
301
+ var header = document.createElement("header");
302
+ header.innerHTML =
303
+ '<div class="project-log-doc-chips"></div>' +
304
+ '<h1></h1>' +
305
+ '<p class="project-log-doc-summary"></p>' +
306
+ '<p class="project-log-byline"></p>';
307
+ var chips = header.querySelector(".project-log-doc-chips");
308
+ var chip = document.createElement("span");
309
+ chip.className = "project-log-chip";
310
+ chip.dataset.category = entry.category || entry.kind || "progress";
311
+ chip.textContent = categoryLabel(entry.category || entry.kind);
312
+ chips.appendChild(chip);
313
+ if (entry.priority && entry.priority !== "normal") {
314
+ var flag = document.createElement("span");
315
+ flag.className = "project-log-priority";
316
+ flag.dataset.priority = entry.priority;
317
+ flag.textContent = PRIORITY_LABELS[entry.priority] || entry.priority;
318
+ chips.appendChild(flag);
319
+ }
320
+ setText(header, "h1", entry.title || "Untitled log");
321
+ setText(header, ".project-log-doc-summary", entry.summary || "");
322
+ var byline = [
323
+ "Revision " + (entry.revisions || 1),
324
+ authorLine(entry),
325
+ formatTime(entry.updatedAt || entry.createdAt),
326
+ ].filter(Boolean).join(" · ");
327
+ setText(header, ".project-log-byline", byline);
328
+ article.appendChild(header);
329
+
330
+ var markdown = document.createElement("div");
331
+ markdown.className = "project-log-markdown md-content";
332
+ markdown.innerHTML = renderMarkdown(entry.body || "");
333
+ highlightCodeBlocks(markdown);
334
+ article.appendChild(markdown);
335
+
336
+ var history = renderHistory(entry);
337
+ if (history) article.appendChild(history);
338
+ article.appendChild(renderComments(entry));
339
+
340
+ var form = document.createElement("form");
341
+ form.className = "project-log-comment-form";
342
+ form.innerHTML =
343
+ '<label for="project-log-comment-input">Add a comment</label>' +
344
+ '<textarea id="project-log-comment-input" rows="3" required placeholder="Add context, a correction, or a question."></textarea>' +
345
+ '<p id="project-log-comment-status" class="project-log-comment-status" role="status"></p>' +
346
+ '<div class="project-log-comment-actions"><button class="primary" type="submit">Comment</button></div>';
347
+ form.addEventListener("submit", function (event) {
348
+ event.preventDefault();
349
+ var input = form.querySelector("#project-log-comment-input");
350
+ var status = form.querySelector("#project-log-comment-status");
351
+ var value = input.value.trim();
352
+ if (!value) { status.textContent = "A comment cannot be empty."; return; }
353
+ handlers.onComment(entry.ref, value, status, input);
354
+ });
355
+ article.appendChild(form);
356
+
357
+ detailEl.appendChild(article);
358
+ refreshIcons();
359
+ }