clay-server 4.0.0-beta.8 → 4.0.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 (185) hide show
  1. package/lib/capsule-display-floor.js +51 -0
  2. package/lib/capsule-frame-server.js +255 -0
  3. package/lib/capsule-mate-turn.js +105 -0
  4. package/lib/capsule-pig-logic.js +300 -0
  5. package/lib/capsule-server-runtimes.js +39 -0
  6. package/lib/capsules/pig/display.js +198 -0
  7. package/lib/capsules/pig/manifest.json +9 -0
  8. package/lib/capsules/pig/ui.json +84 -0
  9. package/lib/config.js +25 -1
  10. package/lib/daemon-projects.js +13 -8
  11. package/lib/daemon.js +127 -14
  12. package/lib/git-cli.js +95 -1
  13. package/lib/knowledge-import.js +436 -0
  14. package/lib/knowledge-record-store.js +188 -0
  15. package/lib/knowledge-search.js +240 -0
  16. package/lib/mate-knowledge-mcp-server.js +151 -0
  17. package/lib/mate-knowledge-migration.js +283 -0
  18. package/lib/mate-knowledge-service.js +488 -0
  19. package/lib/mate-knowledge-sync.js +482 -0
  20. package/lib/migrate-single-user.js +2 -0
  21. package/lib/notes-lifecycle.js +149 -0
  22. package/lib/notes.js +60 -3
  23. package/lib/os-users.js +3 -1
  24. package/lib/project-capsule-catalog.js +9 -1
  25. package/lib/project-capsule-turn.js +96 -0
  26. package/lib/project-connection.js +15 -1
  27. package/lib/project-file-history.js +223 -0
  28. package/lib/project-file-watch.js +77 -49
  29. package/lib/project-filesystem.js +31 -220
  30. package/lib/project-http.js +15 -1
  31. package/lib/project-knowledge.js +22 -0
  32. package/lib/project-log-context.js +172 -0
  33. package/lib/project-log-feedback-delivery.js +118 -0
  34. package/lib/project-logs-comments.js +137 -0
  35. package/lib/project-logs-context-state.js +42 -0
  36. package/lib/project-logs-mcp-server.js +328 -0
  37. package/lib/project-logs-query.js +185 -0
  38. package/lib/project-logs-root.js +75 -0
  39. package/lib/project-logs-schema.js +259 -0
  40. package/lib/project-logs-service.js +463 -0
  41. package/lib/project-logs-snapshot.js +205 -0
  42. package/lib/project-logs-store.js +495 -0
  43. package/lib/project-logs-versioning.js +132 -0
  44. package/lib/project-logs.js +400 -0
  45. package/lib/project-mate-interaction.js +14 -0
  46. package/lib/project-mate-knowledge.js +110 -0
  47. package/lib/project-memory.js +22 -0
  48. package/lib/project-message-delivery.js +70 -0
  49. package/lib/project-pair-lifecycle.js +460 -0
  50. package/lib/project-request-access.js +66 -0
  51. package/lib/project-session-handoff.js +1 -1
  52. package/lib/project-session-notes.js +64 -13
  53. package/lib/project-session-pair.js +159 -146
  54. package/lib/project-session-spawn.js +1 -1
  55. package/lib/project-sessions.js +58 -13
  56. package/lib/project-user-message.js +53 -4
  57. package/lib/project-worker-permission.js +430 -0
  58. package/lib/project-worker-proposal.js +208 -62
  59. package/lib/project.js +185 -6
  60. package/lib/public/app.js +63 -35
  61. package/lib/public/css/admin.css +2 -2
  62. package/lib/public/css/capsule-ui.css +38 -0
  63. package/lib/public/css/command-palette.css +19 -9
  64. package/lib/public/css/git-placard.css +154 -0
  65. package/lib/public/css/home-hub.css +4 -0
  66. package/lib/public/css/home-session-actions.css +66 -0
  67. package/lib/public/css/home-sidebar.css +66 -0
  68. package/lib/public/css/input.css +84 -3
  69. package/lib/public/css/messages.css +40 -1
  70. package/lib/public/css/mobile-nav.css +80 -0
  71. package/lib/public/css/notifications-center.css +67 -0
  72. package/lib/public/css/pane.css +97 -10
  73. package/lib/public/css/project-logs.css +198 -0
  74. package/lib/public/css/scheduler.css +1 -1
  75. package/lib/public/css/session-actions.css +16 -101
  76. package/lib/public/css/sidebar.css +103 -0
  77. package/lib/public/css/sticky-notes.css +237 -229
  78. package/lib/public/css/worker-proposal.css +36 -3
  79. package/lib/public/index.html +43 -11
  80. package/lib/public/modules/admin.js +8 -8
  81. package/lib/public/modules/app-connection.js +124 -6
  82. package/lib/public/modules/app-home-hub.js +12 -8
  83. package/lib/public/modules/app-message-cards.js +2 -1
  84. package/lib/public/modules/app-messages.js +92 -11
  85. package/lib/public/modules/app-misc.js +2 -14
  86. package/lib/public/modules/app-notifications.js +11 -7
  87. package/lib/public/modules/app-panels.js +3 -2
  88. package/lib/public/modules/app-projects.js +56 -18
  89. package/lib/public/modules/app-rendering.js +1 -0
  90. package/lib/public/modules/capsule-preference.js +44 -0
  91. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  92. package/lib/public/modules/clay-log-links.js +70 -0
  93. package/lib/public/modules/command-palette.js +52 -15
  94. package/lib/public/modules/context-sources.js +0 -3
  95. package/lib/public/modules/git-agent-sessions.js +85 -0
  96. package/lib/public/modules/git-panel.js +111 -84
  97. package/lib/public/modules/git-placard.js +161 -0
  98. package/lib/public/modules/home-chat-empty-state.js +39 -0
  99. package/lib/public/modules/home-chat-identity.js +9 -0
  100. package/lib/public/modules/home-conversations-sheet.js +102 -48
  101. package/lib/public/modules/home-mate-chat.js +15 -35
  102. package/lib/public/modules/home-session-actions.js +6 -0
  103. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  104. package/lib/public/modules/home-sidebar.js +7 -0
  105. package/lib/public/modules/home-surface-boot.js +7 -3
  106. package/lib/public/modules/home-tool-frame.js +182 -0
  107. package/lib/public/modules/home-tools.js +138 -1
  108. package/lib/public/modules/input.js +38 -7
  109. package/lib/public/modules/markdown.js +2 -0
  110. package/lib/public/modules/mate-sidebar.js +0 -8
  111. package/lib/public/modules/message-delivery.js +97 -0
  112. package/lib/public/modules/paste-modal.js +84 -0
  113. package/lib/public/modules/project-activation.js +45 -0
  114. package/lib/public/modules/project-logs-render.js +405 -0
  115. package/lib/public/modules/project-logs.js +461 -0
  116. package/lib/public/modules/project-removal-target.js +26 -0
  117. package/lib/public/modules/scheduler.js +17 -3
  118. package/lib/public/modules/session-actions.js +29 -105
  119. package/lib/public/modules/session-hierarchy.js +54 -0
  120. package/lib/public/modules/sidebar-mobile.js +40 -15
  121. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  122. package/lib/public/modules/sidebar-sessions.js +77 -11
  123. package/lib/public/modules/sidebar.js +17 -5
  124. package/lib/public/modules/split-group-helpers.js +8 -0
  125. package/lib/public/modules/split-pair-ui.js +15 -4
  126. package/lib/public/modules/split-view.js +5 -2
  127. package/lib/public/modules/split-worker-runtime.js +24 -0
  128. package/lib/public/modules/sticky-note-markdown.js +5 -0
  129. package/lib/public/modules/sticky-notes-browser.js +326 -0
  130. package/lib/public/modules/sticky-notes-card.js +360 -0
  131. package/lib/public/modules/sticky-notes-editor.js +291 -0
  132. package/lib/public/modules/sticky-notes-shared.js +90 -0
  133. package/lib/public/modules/sticky-notes.js +170 -946
  134. package/lib/public/modules/thinking-lifecycle.js +175 -0
  135. package/lib/public/modules/thinking-summary.js +28 -0
  136. package/lib/public/modules/thinking-view.js +75 -0
  137. package/lib/public/modules/tool-palette-order.js +143 -0
  138. package/lib/public/modules/tool-palette-overlays.js +159 -0
  139. package/lib/public/modules/tool-palette.js +21 -188
  140. package/lib/public/modules/tools.js +31 -123
  141. package/lib/public/modules/update-snooze.js +169 -0
  142. package/lib/public/modules/user-settings.js +4 -0
  143. package/lib/public/modules/worker-pane-lock.js +176 -0
  144. package/lib/public/modules/worker-proposal-state.js +16 -0
  145. package/lib/public/modules/worker-proposal.js +49 -20
  146. package/lib/public/style.css +3 -1
  147. package/lib/sdk-bridge.js +135 -46
  148. package/lib/sdk-message-processor.js +36 -19
  149. package/lib/server-experimental-settings.js +49 -0
  150. package/lib/server-global-ws.js +11 -0
  151. package/lib/server-home-chat-events.js +31 -1
  152. package/lib/server-home-chat.js +27 -4
  153. package/lib/server-home-clay-entry.js +1 -1
  154. package/lib/server-home-clay-session-links.js +34 -5
  155. package/lib/server-settings.js +5 -0
  156. package/lib/server-tools.js +127 -9
  157. package/lib/server.js +137 -22
  158. package/lib/session-driver-eligibility.js +38 -0
  159. package/lib/session-driver-orchestration.js +31 -0
  160. package/lib/session-hygiene.js +3 -0
  161. package/lib/session-notes-mcp-server.js +25 -5
  162. package/lib/session-pair-factory.js +247 -0
  163. package/lib/session-pair-mcp-server.js +46 -8
  164. package/lib/session-pair-prompts.js +79 -0
  165. package/lib/session-pair-turn-control.js +153 -0
  166. package/lib/session-provenance.js +119 -0
  167. package/lib/session-spawn-mcp-server.js +1 -1
  168. package/lib/session-split-groups.js +4 -1
  169. package/lib/session-title-policy.js +60 -0
  170. package/lib/session-visibility.js +41 -0
  171. package/lib/sessions.js +57 -10
  172. package/lib/tools-registry.js +89 -10
  173. package/lib/update-snooze.js +437 -0
  174. package/lib/users-experimental-preferences.js +30 -0
  175. package/lib/users-permissions.js +6 -6
  176. package/lib/users.js +22 -16
  177. package/lib/workspace-query-access.js +102 -0
  178. package/lib/workspace-query-service.js +24 -17
  179. package/lib/worktree.js +25 -38
  180. package/lib/ws-schema.js +38 -7
  181. package/lib/yoke/adapters/claude-worker.js +16 -6
  182. package/lib/yoke/adapters/claude.js +12 -0
  183. package/lib/yoke/adapters/codex.js +24 -14
  184. package/lib/yoke/vendor-registry.js +1 -1
  185. package/package.json +2 -2
@@ -0,0 +1,45 @@
1
+ // Pure project activation and Home-return selection helpers.
2
+
3
+ export function projectWsPath(slug) {
4
+ return slug ? "/p/" + slug + "/ws" : null;
5
+ }
6
+
7
+ export function chooseProjectActivationTarget(projects, candidates) {
8
+ var available = Array.isArray(projects) ? projects : [];
9
+ var preferred = Array.isArray(candidates) ? candidates : [];
10
+ for (var ci = 0; ci < preferred.length; ci++) {
11
+ if (!preferred[ci]) continue;
12
+ for (var pi = 0; pi < available.length; pi++) {
13
+ if (available[pi] && !available[pi].isMate && available[pi].slug === preferred[ci]) return preferred[ci];
14
+ }
15
+ }
16
+ for (var i = 0; i < available.length; i++) {
17
+ if (available[i] && !available[i].isMate && available[i].slug) return available[i].slug;
18
+ }
19
+ return null;
20
+ }
21
+
22
+ export function isProjectActivationPending(state, slug, socket) {
23
+ var target = projectWsPath(slug);
24
+ return !!target
25
+ && state.currentSlug === slug
26
+ && state.wsPath === target
27
+ && state.socketPath === target
28
+ && !!socket
29
+ && (socket.readyState === 0 || socket.readyState === 1);
30
+ }
31
+
32
+ export function isProjectActivated(state, slug, socket) {
33
+ return isProjectActivationPending(state, slug, socket)
34
+ && socket.readyState === 1
35
+ && state.connected === true
36
+ && state.activeProjectSlug === slug
37
+ && state.sessionActivatedProjectSlug === slug;
38
+ }
39
+
40
+ export function isProjectContextConnected(state, slug, socket) {
41
+ return isProjectActivationPending(state, slug, socket)
42
+ && socket.readyState === 1
43
+ && state.connected === true
44
+ && state.activeProjectSlug === slug;
45
+ }
@@ -0,0 +1,405 @@
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
+ export function renderContextFilter(container, selected, isWorktree, onChange) {
110
+ container.innerHTML = "";
111
+ var select = document.createElement("select");
112
+ select.id = "project-logs-context";
113
+ select.className = "project-logs-filter project-logs-context-filter";
114
+ select.setAttribute("aria-label", "Filter by change scope");
115
+ var options = isWorktree
116
+ ? [["current", "Current worktree"], ["project", "Project"], ["all", "All changes"]]
117
+ : [["project", "Project"], ["all", "All changes"]];
118
+ for (var i = 0; i < options.length; i++) {
119
+ var option = document.createElement("option");
120
+ option.value = options[i][0];
121
+ option.textContent = options[i][1];
122
+ select.appendChild(option);
123
+ }
124
+ select.value = selected || (isWorktree ? "current" : "project");
125
+ select.addEventListener("change", function () { onChange(select.value); });
126
+ container.appendChild(select);
127
+ }
128
+
129
+ function contextLabel(context) {
130
+ if (!context || !context.changeSetId) return "";
131
+ var label = context.branch || "Worktree change";
132
+ if (context.status === "merged") return label + " · Merged";
133
+ if (context.status === "archived") return label + " · Archived";
134
+ return label + " · Active";
135
+ }
136
+
137
+ // One ledger row. Reveals what happened without opening the record: category,
138
+ // priority, title, summary, author, time, revision and comment counts.
139
+ function renderRow(entry, onOpen) {
140
+ var row = document.createElement("button");
141
+ row.type = "button";
142
+ row.className = "project-log-row";
143
+ row.dataset.ref = entry.ref;
144
+ row.setAttribute("role", "listitem");
145
+
146
+ var head = document.createElement("div");
147
+ head.className = "project-log-row-head";
148
+
149
+ var chip = document.createElement("span");
150
+ chip.className = "project-log-chip";
151
+ chip.dataset.category = entry.category || entry.kind || "progress";
152
+ chip.textContent = categoryLabel(entry.category || entry.kind);
153
+ head.appendChild(chip);
154
+
155
+ if (entry.priority && entry.priority !== "normal") {
156
+ var flag = document.createElement("span");
157
+ flag.className = "project-log-priority";
158
+ flag.dataset.priority = entry.priority;
159
+ flag.textContent = PRIORITY_LABELS[entry.priority] || entry.priority;
160
+ head.appendChild(flag);
161
+ }
162
+
163
+ var title = document.createElement("span");
164
+ title.className = "project-log-row-title";
165
+ title.textContent = entry.title || "Untitled log";
166
+ head.appendChild(title);
167
+ row.appendChild(head);
168
+
169
+ if (entry.summary) {
170
+ var summary = document.createElement("p");
171
+ summary.className = "project-log-row-summary";
172
+ summary.textContent = entry.summary;
173
+ row.appendChild(summary);
174
+ }
175
+
176
+ var meta = document.createElement("div");
177
+ meta.className = "project-log-row-meta";
178
+ var parts = [authorLine(entry), relativeTime(entry.updatedAt || entry.createdAt)];
179
+ if (entry.revisions > 1) parts.push("v" + entry.revisions);
180
+ if (entry.commentCount > 0) {
181
+ parts.push(entry.commentCount + (entry.commentCount === 1 ? " comment" : " comments"));
182
+ }
183
+ var context = contextLabel(entry.context);
184
+ if (context) parts.push(context);
185
+ meta.textContent = parts.filter(Boolean).join(" · ");
186
+ row.appendChild(meta);
187
+
188
+ row.addEventListener("click", function () { onOpen(entry.ref); });
189
+ return row;
190
+ }
191
+
192
+ export function renderList(listEl, entries, onOpen, emptyState) {
193
+ listEl.innerHTML = "";
194
+ if (!entries.length) {
195
+ var empty = document.createElement("div");
196
+ empty.className = "project-logs-list-empty";
197
+ empty.textContent = emptyState || "No logs yet. This project's agent sessions record decisions and work here.";
198
+ listEl.appendChild(empty);
199
+ return;
200
+ }
201
+ for (var i = 0; i < entries.length; i++) {
202
+ listEl.appendChild(renderRow(entries[i], onOpen));
203
+ }
204
+ }
205
+
206
+ // --- Detail --------------------------------------------------------------
207
+
208
+ function renderComments(entry) {
209
+ var wrap = document.createElement("section");
210
+ wrap.className = "project-log-discussion";
211
+ var heading = document.createElement("h2");
212
+ var comments = entry.comments || [];
213
+ heading.textContent = comments.length
214
+ ? "Discussion (" + comments.length + ")"
215
+ : "Discussion";
216
+ wrap.appendChild(heading);
217
+
218
+ if (!comments.length) {
219
+ var none = document.createElement("p");
220
+ none.className = "project-log-discussion-empty";
221
+ none.textContent = "No comments yet.";
222
+ wrap.appendChild(none);
223
+ }
224
+ for (var i = 0; i < comments.length; i++) {
225
+ var comment = comments[i];
226
+ var item = document.createElement("article");
227
+ item.className = "project-log-comment";
228
+
229
+ var meta = document.createElement("p");
230
+ meta.className = "project-log-comment-meta";
231
+ meta.textContent = actorLabel(comment.author) + " · " + relativeTime(comment.at);
232
+ item.appendChild(meta);
233
+
234
+ var body = document.createElement("p");
235
+ body.className = "project-log-comment-body";
236
+ body.textContent = comment.body || "";
237
+ item.appendChild(body);
238
+
239
+ var status = comment.status || "pending";
240
+ var badge = document.createElement("span");
241
+ badge.className = "project-log-comment-status-badge";
242
+ badge.dataset.status = status;
243
+ badge.textContent = COMMENT_STATUS_LABELS[status] || status;
244
+ item.appendChild(badge);
245
+
246
+ // The Driver's decision sits under the comment it answers.
247
+ if (comment.review && comment.review.response) {
248
+ var response = document.createElement("div");
249
+ response.className = "project-log-comment-response";
250
+ var responseMeta = document.createElement("p");
251
+ responseMeta.className = "project-log-comment-response-meta";
252
+ var parts = ["Project Driver"];
253
+ if (comment.review.at) parts.push(relativeTime(comment.review.at));
254
+ if (comment.review.revision) parts.push("revision " + comment.review.revision);
255
+ responseMeta.textContent = parts.join(" · ");
256
+ var responseBody = document.createElement("p");
257
+ responseBody.className = "project-log-comment-response-body";
258
+ responseBody.textContent = comment.review.response;
259
+ response.appendChild(responseMeta);
260
+ response.appendChild(responseBody);
261
+ item.appendChild(response);
262
+ }
263
+
264
+ wrap.appendChild(item);
265
+ }
266
+ return wrap;
267
+ }
268
+
269
+ // A compact, read-only record of how the entry reached its current state.
270
+ // Metadata only: no bodies, and no revert control, because restoring a
271
+ // revision is the Project Driver's decision.
272
+ function renderHistory(entry) {
273
+ var history = entry.history || [];
274
+ if (history.length < 2) return null;
275
+ var wrap = document.createElement("section");
276
+ wrap.className = "project-log-history";
277
+ var heading = document.createElement("h2");
278
+ heading.textContent = "Version history (" + history.length + ")";
279
+ wrap.appendChild(heading);
280
+
281
+ var list = document.createElement("ol");
282
+ list.className = "project-log-history-list";
283
+ for (var i = history.length - 1; i >= 0; i--) {
284
+ var revision = history[i];
285
+ var row = document.createElement("li");
286
+ row.className = "project-log-history-row";
287
+
288
+ var label = document.createElement("span");
289
+ label.className = "project-log-history-label";
290
+ label.textContent = "v" + revision.revision;
291
+ row.appendChild(label);
292
+
293
+ var text = document.createElement("span");
294
+ text.className = "project-log-history-text";
295
+ var summary = [historyVerb(revision), relativeTime(revision.at)];
296
+ if (revision.changed && revision.changed.length) summary.push(revision.changed.join(", "));
297
+ text.textContent = summary.filter(Boolean).join(" · ");
298
+ row.appendChild(text);
299
+
300
+ if (revision.reason) {
301
+ var reason = document.createElement("span");
302
+ reason.className = "project-log-history-reason";
303
+ reason.textContent = revision.reason;
304
+ row.appendChild(reason);
305
+ }
306
+ list.appendChild(row);
307
+ }
308
+ wrap.appendChild(list);
309
+ return wrap;
310
+ }
311
+
312
+ function historyVerb(revision) {
313
+ if (revision.op === "create") return "Created";
314
+ if (revision.op === "incorporate") return "Incorporated a comment";
315
+ if (revision.op === "revert") {
316
+ return revision.revertedFrom ? "Reverted to v" + revision.revertedFrom : "Reverted";
317
+ }
318
+ if (revision.op === "link") return "Linked";
319
+ return "Revised";
320
+ }
321
+
322
+ // The entry, full pane. People may comment and the project owner may delete;
323
+ // there is no edit affordance because canonical authorship belongs to agents.
324
+ export function renderDetail(detailEl, entry, handlers) {
325
+ detailEl.innerHTML = "";
326
+ if (!entry) return;
327
+
328
+ var article = document.createElement("article");
329
+ article.className = "project-log-document";
330
+
331
+ var header = document.createElement("header");
332
+ header.innerHTML =
333
+ '<div class="project-log-doc-chips"></div>' +
334
+ '<h1></h1>' +
335
+ '<p class="project-log-doc-summary"></p>' +
336
+ '<p class="project-log-byline"></p>';
337
+ var chips = header.querySelector(".project-log-doc-chips");
338
+ var chip = document.createElement("span");
339
+ chip.className = "project-log-chip";
340
+ chip.dataset.category = entry.category || entry.kind || "progress";
341
+ chip.textContent = categoryLabel(entry.category || entry.kind);
342
+ chips.appendChild(chip);
343
+ if (entry.priority && entry.priority !== "normal") {
344
+ var flag = document.createElement("span");
345
+ flag.className = "project-log-priority";
346
+ flag.dataset.priority = entry.priority;
347
+ flag.textContent = PRIORITY_LABELS[entry.priority] || entry.priority;
348
+ chips.appendChild(flag);
349
+ }
350
+ var context = contextLabel(entry.context);
351
+ if (context) {
352
+ var contextChip = document.createElement("span");
353
+ contextChip.className = "project-log-context-chip";
354
+ contextChip.dataset.status = entry.context.status || "active";
355
+ contextChip.textContent = context;
356
+ chips.appendChild(contextChip);
357
+ }
358
+ setText(header, "h1", entry.title || "Untitled log");
359
+ setText(header, ".project-log-doc-summary", entry.summary || "");
360
+ var byline = [
361
+ "Revision " + (entry.revisions || 1),
362
+ authorLine(entry),
363
+ formatTime(entry.updatedAt || entry.createdAt),
364
+ ].filter(Boolean).join(" · ");
365
+ setText(header, ".project-log-byline", byline);
366
+ if (handlers && handlers.canDelete) {
367
+ var deleteButton = document.createElement("button");
368
+ deleteButton.type = "button";
369
+ deleteButton.className = "project-log-delete-button";
370
+ deleteButton.innerHTML = '<i data-lucide="trash-2"></i><span>Delete log</span>';
371
+ deleteButton.addEventListener("click", function () { handlers.onDelete(entry); });
372
+ header.appendChild(deleteButton);
373
+ }
374
+ article.appendChild(header);
375
+
376
+ var markdown = document.createElement("div");
377
+ markdown.className = "project-log-markdown md-content";
378
+ markdown.innerHTML = renderMarkdown(entry.body || "");
379
+ highlightCodeBlocks(markdown);
380
+ article.appendChild(markdown);
381
+
382
+ var history = renderHistory(entry);
383
+ if (history) article.appendChild(history);
384
+ article.appendChild(renderComments(entry));
385
+
386
+ var form = document.createElement("form");
387
+ form.className = "project-log-comment-form";
388
+ form.innerHTML =
389
+ '<label for="project-log-comment-input">Add a comment</label>' +
390
+ '<textarea id="project-log-comment-input" rows="3" required placeholder="Add context, a correction, or a question."></textarea>' +
391
+ '<p id="project-log-comment-status" class="project-log-comment-status" role="status"></p>' +
392
+ '<div class="project-log-comment-actions"><button class="primary" type="submit">Comment</button></div>';
393
+ form.addEventListener("submit", function (event) {
394
+ event.preventDefault();
395
+ var input = form.querySelector("#project-log-comment-input");
396
+ var status = form.querySelector("#project-log-comment-status");
397
+ var value = input.value.trim();
398
+ if (!value) { status.textContent = "A comment cannot be empty."; return; }
399
+ handlers.onComment(entry.ref, value, status, input);
400
+ });
401
+ article.appendChild(form);
402
+
403
+ detailEl.appendChild(article);
404
+ refreshIcons();
405
+ }