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.
- package/lib/ask-user-mcp-server.js +5 -119
- package/lib/background-task-timing.js +105 -0
- package/lib/builtin-mates.js +6 -6
- package/lib/claude-hook-installer.js +15 -0
- package/lib/clay-history-mcp-server.js +62 -297
- package/lib/daemon.js +23 -1
- package/lib/debate-model-selection.js +119 -0
- package/lib/home-debate-tool-policy.js +164 -0
- package/lib/home-response-notifications.js +26 -0
- package/lib/knowledge-import.js +436 -0
- package/lib/knowledge-record-store.js +188 -0
- package/lib/knowledge-search.js +224 -0
- package/lib/mate-creation-mcp-server.js +55 -0
- package/lib/mate-knowledge-mcp-server.js +151 -0
- package/lib/mate-knowledge-migration.js +283 -0
- package/lib/mate-knowledge-service.js +488 -0
- package/lib/mate-knowledge-sync.js +482 -0
- package/lib/mate-ready-creation.js +95 -0
- package/lib/mates.js +2 -0
- package/lib/notes.js +2 -1
- package/lib/project-ask-user.js +64 -0
- package/lib/project-capsule-catalog.js +101 -0
- package/lib/project-connection.js +17 -0
- package/lib/project-debate-proposal.js +133 -17
- package/lib/project-debate.js +214 -110
- package/lib/project-delegated-follow-up.js +84 -0
- package/lib/project-delegated-session.js +53 -0
- package/lib/project-filesystem.js +7 -9
- package/lib/project-knowledge.js +23 -0
- package/lib/project-logs-comments.js +137 -0
- package/lib/project-logs-mcp-server.js +290 -0
- package/lib/project-logs-query.js +167 -0
- package/lib/project-logs-root.js +67 -0
- package/lib/project-logs-schema.js +259 -0
- package/lib/project-logs-service.js +357 -0
- package/lib/project-logs-snapshot.js +202 -0
- package/lib/project-logs-store.js +498 -0
- package/lib/project-logs-versioning.js +128 -0
- package/lib/project-logs.js +347 -0
- package/lib/project-mate-creation-proposal.js +120 -0
- package/lib/project-mate-interaction.js +14 -0
- package/lib/project-mate-knowledge.js +110 -0
- package/lib/project-memory.js +36 -7
- package/lib/project-models.js +157 -32
- package/lib/project-session-notes.js +2 -2
- package/lib/project-session-pair.js +8 -8
- package/lib/project-sessions.js +66 -51
- package/lib/project-vendor-login.js +414 -0
- package/lib/project-worker-proposal.js +25 -25
- package/lib/project-workspace-query.js +144 -0
- package/lib/project.js +433 -102
- package/lib/public/app.js +109 -48
- package/lib/public/css/avatar-imprints.css +0 -1
- package/lib/public/css/base.css +4 -0
- package/lib/public/css/capsule-ui.css +389 -0
- package/lib/public/css/command-palette.css +306 -6
- package/lib/public/css/home-capsule-library.css +243 -0
- package/lib/public/css/home-capsule-source.css +192 -0
- package/lib/public/css/home-debate-live.css +146 -0
- package/lib/public/css/home-debate-planning.css +125 -0
- package/lib/public/css/home-debates-archive.css +281 -0
- package/lib/public/css/home-hub.css +896 -405
- package/lib/public/css/home-mate-creation.css +34 -0
- package/lib/public/css/home-mate-model-picker.css +131 -0
- package/lib/public/css/home-mate-settings.css +344 -0
- package/lib/public/css/home-session-actions.css +265 -0
- package/lib/public/css/home-sidebar.css +667 -0
- package/lib/public/css/icon-strip.css +1 -2
- package/lib/public/css/input.css +79 -25
- package/lib/public/css/mates.css +7 -34
- package/lib/public/css/pane.css +1 -1
- package/lib/public/css/playbook.css +0 -80
- package/lib/public/css/project-logs.css +234 -0
- package/lib/public/css/scheduler.css +1 -1
- package/lib/public/css/sticky-notes.css +53 -117
- package/lib/public/css/tui-attention.css +0 -44
- package/lib/public/css/workspace-assignment.css +45 -0
- package/lib/public/fonts/source-serif-4/LICENSE.md +93 -0
- package/lib/public/fonts/source-serif-4/SourceSerif4Caption-Semibold.ttf.woff2 +0 -0
- package/lib/public/index.html +96 -74
- package/lib/public/modules/app-connection.js +100 -5
- package/lib/public/modules/app-dm.js +43 -160
- package/lib/public/modules/app-home-hub.js +328 -585
- package/lib/public/modules/app-message-cards.js +242 -0
- package/lib/public/modules/app-message-router.js +84 -0
- package/lib/public/modules/app-messages.js +124 -38
- package/lib/public/modules/app-notifications.js +8 -119
- package/lib/public/modules/app-panels.js +43 -2
- package/lib/public/modules/app-projects.js +9 -1
- package/lib/public/modules/app-rendering.js +21 -602
- package/lib/public/modules/avatar-imprint.js +11 -8
- package/lib/public/modules/background-tasks-ui.js +139 -16
- package/lib/public/modules/chat-bubble-renderer.js +152 -0
- package/lib/public/modules/chat-render-runtime.js +198 -0
- package/lib/public/modules/command-palette.js +225 -436
- package/lib/public/modules/dm-render.js +82 -0
- package/lib/public/modules/home-capsule-creation-intent.js +31 -0
- package/lib/public/modules/home-capsule-library.js +146 -0
- package/lib/public/modules/home-capsule-source.js +383 -0
- package/lib/public/modules/home-chat-identity.js +35 -0
- package/lib/public/modules/home-chat-scroll.js +77 -0
- package/lib/public/modules/home-chat-stream-state.js +34 -0
- package/lib/public/modules/home-composer-focus.js +25 -0
- package/lib/public/modules/home-conversations-sheet.js +179 -0
- package/lib/public/modules/home-debate-activity.js +48 -0
- package/lib/public/modules/home-debate-controls.js +295 -0
- package/lib/public/modules/home-debate-launch.js +41 -0
- package/lib/public/modules/home-debate-live.js +284 -0
- package/lib/public/modules/home-debate-models.js +73 -0
- package/lib/public/modules/home-debate-planning.js +335 -0
- package/lib/public/modules/home-debates-archive.js +279 -0
- package/lib/public/modules/home-dock-resize.js +74 -0
- package/lib/public/modules/home-dock.js +446 -0
- package/lib/public/modules/home-mate-chat.js +496 -0
- package/lib/public/modules/home-mate-creation.js +109 -0
- package/lib/public/modules/home-mate-model-picker.js +281 -0
- package/lib/public/modules/home-mate-selection.js +38 -0
- package/lib/public/modules/home-mate-settings-menu.js +131 -0
- package/lib/public/modules/home-mate-settings.js +383 -0
- package/lib/public/modules/home-session-actions.js +373 -0
- package/lib/public/modules/home-session-model-confirmation.js +13 -0
- package/lib/public/modules/home-shell.js +9 -0
- package/lib/public/modules/home-sidebar-chat-list.js +63 -0
- package/lib/public/modules/home-sidebar.js +165 -0
- package/lib/public/modules/home-sub-surface.js +19 -0
- package/lib/public/modules/home-surface-boot.js +68 -0
- package/lib/public/modules/home-surface.js +141 -0
- package/lib/public/modules/home-tools.js +271 -0
- package/lib/public/modules/markdown.js +50 -2
- package/lib/public/modules/mate-management.js +86 -0
- package/lib/public/modules/mate-sidebar.js +0 -8
- package/lib/public/modules/mate-wizard.js +0 -7
- package/lib/public/modules/mention.js +17 -19
- package/lib/public/modules/pane-bridge.js +9 -0
- package/lib/public/modules/profile.js +4 -5
- package/lib/public/modules/project-logs-ambient.js +238 -0
- package/lib/public/modules/project-logs-render.js +359 -0
- package/lib/public/modules/project-logs.js +328 -0
- package/lib/public/modules/project-settings.js +34 -28
- package/lib/public/modules/project-switcher.js +11 -5
- package/lib/public/modules/project-workspace-assignment.js +23 -0
- package/lib/public/modules/scheduler.js +17 -3
- package/lib/public/modules/search-clay-chat.js +402 -0
- package/lib/public/modules/server-settings.js +14 -12
- package/lib/public/modules/session-actions.js +1 -1
- package/lib/public/modules/sidebar-mates.js +56 -552
- package/lib/public/modules/sidebar-mobile.js +12 -73
- package/lib/public/modules/sidebar-presence.js +37 -0
- package/lib/public/modules/sidebar-sessions.js +25 -1
- package/lib/public/modules/split-pair-ui.js +13 -13
- package/lib/public/modules/split-view.js +7 -0
- package/lib/public/modules/sticky-notes.js +106 -53
- package/lib/public/modules/tool-input-composition.js +48 -0
- package/lib/public/modules/tool-llm-status.js +167 -0
- package/lib/public/modules/tool-palette.js +76 -4
- package/lib/public/modules/tool-renderer-advanced.js +285 -0
- package/lib/public/modules/tool-renderer-chart.js +158 -0
- package/lib/public/modules/tool-renderer-semantics.js +35 -0
- package/lib/public/modules/tool-renderer.js +464 -0
- package/lib/public/modules/tool-runtime.js +309 -0
- package/lib/public/modules/tool-ui-evaluator.js +97 -0
- package/lib/public/modules/tool-ui-tree.js +21 -0
- package/lib/public/modules/tui-attention.js +10 -0
- package/lib/public/modules/user-settings.js +16 -23
- package/lib/public/modules/vendor-login.js +287 -0
- package/lib/public/modules/worker-proposal.js +2 -2
- package/lib/public/modules/workspace-assignment-card.js +149 -0
- package/lib/public/modules/workspace-assignment-routing.js +4 -0
- package/lib/public/style.css +14 -2
- package/lib/runtime-env.js +65 -0
- package/lib/sdk-bridge.js +120 -32
- package/lib/sdk-message-processor.js +34 -13
- package/lib/server-home-capsule-creation.js +36 -0
- package/lib/server-home-chat-events.js +287 -0
- package/lib/server-home-chat.js +518 -0
- package/lib/server-home-clay-entry.js +65 -0
- package/lib/server-home-clay-session-links.js +26 -0
- package/lib/server-home-debate-planning.js +214 -0
- package/lib/server-home-debates.js +137 -0
- package/lib/server-home-mate-creation.js +261 -0
- package/lib/server-home-models.js +241 -0
- package/lib/server-home-preferences.js +61 -0
- package/lib/server-mates.js +37 -0
- package/lib/server-tools.js +377 -0
- package/lib/server.js +166 -59
- package/lib/session-pair-mcp-server.js +6 -6
- package/lib/sessions.js +22 -4
- package/lib/tool-capsule-source.js +142 -0
- package/lib/tool-control-mcp-server.js +126 -0
- package/lib/tool-llm.js +48 -0
- package/lib/tool-storage.js +77 -0
- package/lib/tool-ui-spec-advanced.js +82 -0
- package/lib/tool-ui-spec.js +261 -0
- package/lib/tools-registry.js +286 -0
- package/lib/users-home-dock-preferences.js +77 -0
- package/lib/users-home-surface-preferences.js +99 -0
- package/lib/users-preferences.js +4 -7
- package/lib/users.js +12 -0
- package/lib/whats-new-content.js +3 -3
- package/lib/workspace-assignment-service.js +420 -0
- package/lib/workspace-query-mcp-server.js +154 -0
- package/lib/workspace-query-service.js +492 -0
- package/lib/ws-schema.js +107 -12
- package/lib/yoke/adapters/acp.js +2 -1
- package/lib/yoke/adapters/antigravity.js +2 -1
- package/lib/yoke/adapters/claude-worker.js +11 -4
- package/lib/yoke/adapters/claude.js +141 -46
- package/lib/yoke/adapters/codex.js +79 -11
- package/lib/yoke/adapters/kiro.js +2 -1
- package/lib/yoke/claude-user-input.js +38 -0
- package/lib/yoke/codex-app-server.js +20 -2
- package/lib/yoke/codex-background-tasks.js +8 -2
- package/lib/yoke/codex-user-input.js +64 -0
- package/lib/yoke/complete-once.js +164 -0
- package/lib/yoke/index.js +5 -1
- package/lib/yoke/interface.js +19 -0
- package/lib/yoke/mcp-bridge-server.js +2 -6
- package/lib/yoke/user-input.js +254 -0
- package/package.json +4 -3
- package/lib/public/css/home-chat.css +0 -380
- package/lib/public/modules/home-chat.js +0 -438
- 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
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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
|
|
535
|
-
//
|
|
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({
|
|
539
|
-
if (document.body) document.body.classList.toggle('
|
|
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
|
+
}
|