clay-server 4.0.0-beta.1 → 4.0.0-beta.11
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/background-task-timing.js +105 -0
- package/lib/config.js +25 -1
- package/lib/daemon.js +22 -0
- package/lib/git-cli.js +94 -1
- package/lib/knowledge-import.js +436 -0
- package/lib/knowledge-record-store.js +188 -0
- package/lib/knowledge-search.js +240 -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/notes-lifecycle.js +149 -0
- package/lib/notes.js +60 -3
- package/lib/project-connection.js +17 -0
- package/lib/project-http.js +15 -1
- package/lib/project-knowledge.js +22 -0
- package/lib/project-logs-comments.js +137 -0
- package/lib/project-logs-mcp-server.js +314 -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 +348 -0
- package/lib/project-mate-interaction.js +14 -0
- package/lib/project-mate-knowledge.js +110 -0
- package/lib/project-memory.js +22 -0
- package/lib/project-pair-lifecycle.js +461 -0
- package/lib/project-session-notes.js +64 -13
- package/lib/project-session-pair.js +124 -111
- package/lib/project-sessions.js +46 -6
- package/lib/project-user-message.js +41 -4
- package/lib/project-vendor-login.js +414 -0
- package/lib/project-worker-permission.js +429 -0
- package/lib/project-worker-proposal.js +16 -1
- package/lib/project.js +164 -4
- package/lib/public/app.js +49 -20
- package/lib/public/css/git-placard.css +154 -0
- package/lib/public/css/input.css +79 -25
- package/lib/public/css/notifications-center.css +67 -0
- package/lib/public/css/pane.css +52 -0
- package/lib/public/css/project-logs.css +234 -0
- package/lib/public/css/scheduler.css +1 -1
- package/lib/public/css/sticky-notes.css +231 -226
- package/lib/public/index.html +12 -0
- package/lib/public/modules/app-connection.js +90 -4
- package/lib/public/modules/app-messages.js +54 -19
- package/lib/public/modules/app-notifications.js +15 -124
- package/lib/public/modules/app-panels.js +43 -2
- package/lib/public/modules/app-projects.js +2 -2
- package/lib/public/modules/background-tasks-ui.js +139 -16
- package/lib/public/modules/git-agent-sessions.js +85 -0
- package/lib/public/modules/git-panel.js +111 -84
- package/lib/public/modules/git-placard.js +161 -0
- package/lib/public/modules/home-sidebar.js +7 -0
- package/lib/public/modules/mate-sidebar.js +0 -8
- package/lib/public/modules/pane-bridge.js +9 -0
- 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 +329 -0
- package/lib/public/modules/scheduler.js +17 -3
- package/lib/public/modules/sidebar-mobile.js +5 -9
- package/lib/public/modules/sidebar-sessions.js +25 -1
- package/lib/public/modules/sidebar.js +17 -5
- package/lib/public/modules/split-view.js +7 -0
- package/lib/public/modules/sticky-notes-browser.js +324 -0
- package/lib/public/modules/sticky-notes-card.js +358 -0
- package/lib/public/modules/sticky-notes-editor.js +288 -0
- package/lib/public/modules/sticky-notes-shared.js +90 -0
- package/lib/public/modules/sticky-notes.js +168 -946
- package/lib/public/modules/tool-palette-order.js +143 -0
- package/lib/public/modules/tool-palette-overlays.js +159 -0
- package/lib/public/modules/tool-palette.js +21 -188
- package/lib/public/modules/tui-attention.js +10 -0
- package/lib/public/modules/update-snooze.js +169 -0
- package/lib/public/modules/vendor-login.js +287 -0
- package/lib/public/modules/worker-pane-lock.js +174 -0
- package/lib/public/style.css +2 -0
- package/lib/sdk-bridge.js +77 -10
- package/lib/sdk-message-processor.js +24 -7
- package/lib/server.js +61 -6
- package/lib/session-driver-eligibility.js +183 -0
- package/lib/session-notes-mcp-server.js +25 -5
- package/lib/session-pair-factory.js +233 -0
- package/lib/session-pair-mcp-server.js +45 -5
- package/lib/session-pair-prompts.js +60 -0
- package/lib/sessions.js +4 -1
- package/lib/tools-registry.js +11 -1
- package/lib/update-snooze.js +437 -0
- package/lib/workspace-query-access.js +102 -0
- package/lib/workspace-query-service.js +24 -17
- package/lib/ws-schema.js +28 -3
- package/lib/yoke/adapters/claude.js +69 -4
- package/lib/yoke/adapters/codex.js +42 -5
- package/lib/yoke/codex-app-server.js +20 -2
- package/lib/yoke/codex-background-tasks.js +8 -2
- package/package.json +3 -3
|
@@ -125,8 +125,8 @@ export function initScheduler(_ctx) {
|
|
|
125
125
|
function ensurePanel() {
|
|
126
126
|
if (panel) return;
|
|
127
127
|
|
|
128
|
-
var
|
|
129
|
-
if (!
|
|
128
|
+
var hostEl = document.getElementById("main-area");
|
|
129
|
+
if (!hostEl) return;
|
|
130
130
|
|
|
131
131
|
panel = document.createElement("div");
|
|
132
132
|
panel.id = "scheduler-panel";
|
|
@@ -256,7 +256,7 @@ function ensurePanel() {
|
|
|
256
256
|
bodyRow.appendChild(content);
|
|
257
257
|
panel.appendChild(bodyRow);
|
|
258
258
|
|
|
259
|
-
|
|
259
|
+
hostEl.appendChild(panel);
|
|
260
260
|
|
|
261
261
|
// --- Close button (in top bar) ---
|
|
262
262
|
panel.querySelector("#scheduler-panel-close").addEventListener("click", function () {
|
|
@@ -383,6 +383,8 @@ function openScheduler() {
|
|
|
383
383
|
|
|
384
384
|
var sidebarBtn = document.getElementById("scheduler-btn");
|
|
385
385
|
if (sidebarBtn) sidebarBtn.classList.add("active");
|
|
386
|
+
var homeBtn = document.getElementById("home-scheduler-btn");
|
|
387
|
+
if (homeBtn) homeBtn.classList.add("active");
|
|
386
388
|
|
|
387
389
|
// Persist scheduler state in URL hash
|
|
388
390
|
if (location.hash !== "#scheduler") {
|
|
@@ -423,6 +425,8 @@ export function closeScheduler() {
|
|
|
423
425
|
// Un-mark sidebar button
|
|
424
426
|
var sidebarBtn = document.getElementById("scheduler-btn");
|
|
425
427
|
if (sidebarBtn) sidebarBtn.classList.remove("active");
|
|
428
|
+
var homeBtn = document.getElementById("home-scheduler-btn");
|
|
429
|
+
if (homeBtn) homeBtn.classList.remove("active");
|
|
426
430
|
|
|
427
431
|
// Remove scheduler hash from URL
|
|
428
432
|
if (location.hash === "#scheduler") {
|
|
@@ -1470,6 +1474,16 @@ export function openSchedulerToTab(tab) {
|
|
|
1470
1474
|
}
|
|
1471
1475
|
}
|
|
1472
1476
|
|
|
1477
|
+
export function openHomeScheduler() {
|
|
1478
|
+
if (!ctx) return;
|
|
1479
|
+
ctx.requireClayRalph(function () {
|
|
1480
|
+
showAllProjects = true;
|
|
1481
|
+
openSchedulerToTab("calendar");
|
|
1482
|
+
renderSidebar();
|
|
1483
|
+
render();
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1473
1487
|
export function isSchedulerOpen() {
|
|
1474
1488
|
return panelOpen;
|
|
1475
1489
|
}
|
|
@@ -887,8 +887,7 @@ function renderSheetMateProfile(listEl) {
|
|
|
887
887
|
var actions = [
|
|
888
888
|
{ icon: "book-open", label: "Knowledge", btnId: "mate-knowledge-btn", countId: "mate-knowledge-count" },
|
|
889
889
|
{ icon: "sticky-note", label: "Sticky Notes", btnId: "sticky-notes-sidebar-btn", countId: "sticky-notes-sidebar-count" },
|
|
890
|
-
{ icon: "puzzle", label: "Skills", btnId: "mate-skills-btn" }
|
|
891
|
-
{ icon: "calendar", label: "Scheduled Tasks", btnId: "mate-scheduler-btn" }
|
|
890
|
+
{ icon: "puzzle", label: "Skills", btnId: "mate-skills-btn" }
|
|
892
891
|
];
|
|
893
892
|
|
|
894
893
|
for (var i = 0; i < actions.length; i++) {
|
|
@@ -1005,12 +1004,11 @@ function renderSheetTools(listEl) {
|
|
|
1005
1004
|
{ icon: "brain", label: "Memory", action: "mate-memory" },
|
|
1006
1005
|
{ icon: "book-open", label: "Knowledge", action: "mate-knowledge" },
|
|
1007
1006
|
{ icon: "sticky-note", label: "Sticky Notes", action: "mate-sticky" },
|
|
1008
|
-
{ icon: "puzzle", label: "Skills", action: "mate-skills" }
|
|
1009
|
-
{ icon: "calendar-clock", label: "Scheduled Tasks", action: "mate-scheduler" }
|
|
1007
|
+
{ icon: "puzzle", label: "Skills", action: "mate-skills" }
|
|
1010
1008
|
] : [
|
|
1011
1009
|
{ icon: "folder-tree", label: "Files", action: "files" },
|
|
1012
1010
|
{ icon: "square-terminal", label: "Terminal", action: "terminal" },
|
|
1013
|
-
{ icon: "
|
|
1011
|
+
{ icon: "notebook-tabs", label: "Logs", action: "project-logs" }
|
|
1014
1012
|
];
|
|
1015
1013
|
|
|
1016
1014
|
for (var i = 0; i < items.length; i++) {
|
|
@@ -1025,8 +1023,8 @@ function renderSheetTools(listEl) {
|
|
|
1025
1023
|
setTimeout(function () { openMobileSheet("files"); }, 250);
|
|
1026
1024
|
} else if (item.action === "terminal") {
|
|
1027
1025
|
openTerminal();
|
|
1028
|
-
} else if (item.action === "
|
|
1029
|
-
targetId = "
|
|
1026
|
+
} else if (item.action === "project-logs") {
|
|
1027
|
+
targetId = "project-logs-btn";
|
|
1030
1028
|
} else if (item.action === "mate-knowledge") {
|
|
1031
1029
|
setTimeout(function () { openMobileSheet("mate-knowledge"); }, 250);
|
|
1032
1030
|
return;
|
|
@@ -1036,8 +1034,6 @@ function renderSheetTools(listEl) {
|
|
|
1036
1034
|
targetId = "mate-skills-btn";
|
|
1037
1035
|
} else if (item.action === "mate-memory") {
|
|
1038
1036
|
targetId = "mate-memory-btn";
|
|
1039
|
-
} else if (item.action === "mate-scheduler") {
|
|
1040
|
-
targetId = "mate-scheduler-btn";
|
|
1041
1037
|
} else if (item.action === "mate-debate") {
|
|
1042
1038
|
targetId = "mate-debate-btn";
|
|
1043
1039
|
}
|
|
@@ -1376,8 +1376,29 @@ function renderSplitGroupItem(group, members) {
|
|
|
1376
1376
|
|
|
1377
1377
|
// --- Main session list ---
|
|
1378
1378
|
|
|
1379
|
+
// Fingerprint of the last completed render: the whole session payload plus the
|
|
1380
|
+
// two store values this render also reads. Nothing is cherry-picked, so the
|
|
1381
|
+
// guard can only skip a frame that is byte-identical to what is already on
|
|
1382
|
+
// screen. A re-render triggered by other state (sessions === null) always
|
|
1383
|
+
// runs and resets the fingerprint afterwards.
|
|
1384
|
+
var lastRenderFingerprint = null;
|
|
1385
|
+
|
|
1386
|
+
function sessionRenderFingerprint(sessions) {
|
|
1387
|
+
try {
|
|
1388
|
+
return JSON.stringify([sessions, store.get('splitGroups') || [], store.get('splitPanes') || null]);
|
|
1389
|
+
} catch (e) {
|
|
1390
|
+
return null;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1379
1394
|
export function renderSessionList(sessions) {
|
|
1380
|
-
if (sessions)
|
|
1395
|
+
if (sessions) {
|
|
1396
|
+
// A reconnect and a streaming turn both re-send this list unchanged;
|
|
1397
|
+
// rebuilding the sidebar for an identical frame is what made it flicker.
|
|
1398
|
+
var incoming = sessionRenderFingerprint(sessions);
|
|
1399
|
+
if (incoming !== null && incoming === lastRenderFingerprint) return;
|
|
1400
|
+
cachedSessions = sessions;
|
|
1401
|
+
}
|
|
1381
1402
|
|
|
1382
1403
|
// If mobile chat sheet is open, refresh it
|
|
1383
1404
|
if (refreshMobileChatSheet) refreshMobileChatSheet();
|
|
@@ -1516,6 +1537,9 @@ export function renderSessionList(sessions) {
|
|
|
1516
1537
|
refreshIcons();
|
|
1517
1538
|
if (updatePageTitle) updatePageTitle();
|
|
1518
1539
|
syncHeaderSearchUi();
|
|
1540
|
+
// Recorded from the sessions actually rendered, so a null-argument re-render
|
|
1541
|
+
// driven by other state refreshes the fingerprint rather than stranding it.
|
|
1542
|
+
lastRenderFingerprint = sessionRenderFingerprint(cachedSessions);
|
|
1519
1543
|
}
|
|
1520
1544
|
|
|
1521
1545
|
// --- Search results ---
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { closeNotesBrowser } from './sticky-notes-browser.js';
|
|
2
2
|
import { closeScheduler } from './scheduler.js';
|
|
3
3
|
import { initSidebarSessions } from './sidebar-sessions.js';
|
|
4
4
|
import { focusFileTree } from './filebrowser.js';
|
|
@@ -23,7 +23,7 @@ export function syncResizeHandles() {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function dismissOverlayPanels() {
|
|
26
|
-
|
|
26
|
+
closeNotesBrowser();
|
|
27
27
|
closeScheduler();
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -119,7 +119,10 @@ export function initSidebar(_ctx) {
|
|
|
119
119
|
|
|
120
120
|
// --- Panel switch (sessions / files / projects) ---
|
|
121
121
|
var fileBrowserBtn = ctx.$("file-browser-btn");
|
|
122
|
-
|
|
122
|
+
// Git has no tool-palette tile. The repository placard's More control is
|
|
123
|
+
// the entry point into the same panel, with the same lifecycle.
|
|
124
|
+
var gitPlacard = ctx.$("git-placard");
|
|
125
|
+
var gitPlacardMore = ctx.$("git-placard-more");
|
|
123
126
|
var projectsPanel = ctx.$("sidebar-panel-projects");
|
|
124
127
|
var sessionsPanel = ctx.$("sidebar-panel-sessions");
|
|
125
128
|
var filesPanel = ctx.$("sidebar-panel-files");
|
|
@@ -216,14 +219,23 @@ export function initSidebar(_ctx) {
|
|
|
216
219
|
});
|
|
217
220
|
});
|
|
218
221
|
}
|
|
219
|
-
if (
|
|
220
|
-
|
|
222
|
+
if (gitPlacardMore) {
|
|
223
|
+
gitPlacardMore.addEventListener("click", function () {
|
|
221
224
|
if (gitPanel && !gitPanel.classList.contains("hidden")) {
|
|
222
225
|
hideGitPanel(function () { showSessionsPanel(); });
|
|
223
226
|
}
|
|
224
227
|
else showGitPanel();
|
|
225
228
|
});
|
|
226
229
|
}
|
|
230
|
+
if (gitPlacard && gitPlacardMore) {
|
|
231
|
+
// Clicking the block itself is a pointer convenience only; it forwards to
|
|
232
|
+
// the More control so every listener bound there runs exactly once, and
|
|
233
|
+
// the keyboard path stays the single focusable button.
|
|
234
|
+
gitPlacard.addEventListener("click", function (event) {
|
|
235
|
+
if (event.target.closest("#git-placard-more")) return;
|
|
236
|
+
gitPlacardMore.click();
|
|
237
|
+
});
|
|
238
|
+
}
|
|
227
239
|
if (gitPanelClose) {
|
|
228
240
|
gitPanelClose.addEventListener("click", function () {
|
|
229
241
|
hideGitPanel(function () { showSessionsPanel(); });
|
|
@@ -11,6 +11,7 @@ import { groupedSessionIds, findSplitGroup } from './split-group-helpers.js';
|
|
|
11
11
|
import { showConfirm } from './app-misc.js';
|
|
12
12
|
import { syncPairChrome } from './split-pair-ui.js';
|
|
13
13
|
import { presentMarkdownEdit } from './filebrowser.js';
|
|
14
|
+
import { autoStartLoginIfNeeded } from './vendor-login.js';
|
|
14
15
|
|
|
15
16
|
var host = null;
|
|
16
17
|
var nativeApp = null;
|
|
@@ -276,6 +277,12 @@ function handlePaneMessage(event) {
|
|
|
276
277
|
if (present) presentMarkdownEdit(present);
|
|
277
278
|
return;
|
|
278
279
|
}
|
|
280
|
+
// Both panes can report auth_required for their own sessions. The shell runs
|
|
281
|
+
// one login flow for the project; repeat events are no-ops there.
|
|
282
|
+
if (msg.type === "clay-pane-auth-required") {
|
|
283
|
+
if (msg.message) autoStartLoginIfNeeded(msg.message);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
279
286
|
if (msg.type !== "clay-pane-context") return;
|
|
280
287
|
var frames = host.querySelectorAll(".split-pane-frame");
|
|
281
288
|
for (var i = 0; i < frames.length; i++) {
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// Sticky Notes browser: the bounded right workbench window over the board.
|
|
2
|
+
//
|
|
3
|
+
// This is the index/viewer surface, not the floating cards. The cards stay on
|
|
4
|
+
// the canvas over the conversation; this pane lists them, splits them into Open
|
|
5
|
+
// and Closed, and is where a note is completed or brought back.
|
|
6
|
+
//
|
|
7
|
+
// Completion here is Close, never delete. A closed note leaves the active
|
|
8
|
+
// canvas and moves to the Closed tab, where it stays readable and reversible.
|
|
9
|
+
// There is deliberately no destructive control on this surface at all.
|
|
10
|
+
|
|
11
|
+
import { store } from './store.js';
|
|
12
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
13
|
+
import { renderMiniMarkdown, getTitle } from './sticky-note-markdown.js';
|
|
14
|
+
import { send } from './sticky-notes-shared.js';
|
|
15
|
+
import { listNoteData, focusNoteOnCanvas, hideNotes, showNotes } from './sticky-notes.js';
|
|
16
|
+
|
|
17
|
+
var panel = null;
|
|
18
|
+
var gridEl = null;
|
|
19
|
+
var tabsEl = null;
|
|
20
|
+
|
|
21
|
+
// Registered by app.js so this module never imports the other right-pane tools
|
|
22
|
+
// and cannot form an import cycle with them.
|
|
23
|
+
var exclusiveClosers = [];
|
|
24
|
+
|
|
25
|
+
export function registerExclusiveClosers(fns) {
|
|
26
|
+
exclusiveClosers = (fns || []).filter(function (fn) { return typeof fn === "function"; });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function closeOtherRightTools() {
|
|
30
|
+
for (var i = 0; i < exclusiveClosers.length; i++) {
|
|
31
|
+
try { exclusiveClosers[i](); } catch (e) { /* a missing tool must not block opening */ }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isClosed(data) {
|
|
36
|
+
if (!data) return false;
|
|
37
|
+
if (data.state === "closed") return true;
|
|
38
|
+
if (data.state === "open") return false;
|
|
39
|
+
// Legacy notes predate the state field; `hidden` was the old spelling.
|
|
40
|
+
return data.hidden === true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function openNotes(all) {
|
|
44
|
+
return (all || []).filter(function (data) { return !isClosed(data); });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function closedNotes(all) {
|
|
48
|
+
return (all || []).filter(isClosed);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function currentTab() {
|
|
52
|
+
return store.get('notesBrowserTab') === "closed" ? "closed" : "open";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- Panel ---------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
function ensurePanel() {
|
|
58
|
+
if (panel) return;
|
|
59
|
+
var panels = document.getElementById("main-panels");
|
|
60
|
+
if (!panels) return;
|
|
61
|
+
panel = document.createElement("section");
|
|
62
|
+
panel.id = "notes-browser";
|
|
63
|
+
panel.className = "notes-browser hidden";
|
|
64
|
+
panel.setAttribute("aria-label", "Sticky Notes");
|
|
65
|
+
panel.innerHTML =
|
|
66
|
+
'<header class="notes-browser-topbar">' +
|
|
67
|
+
'<span class="notes-browser-title">' + iconHtml("sticky-note") + 'Sticky Notes</span>' +
|
|
68
|
+
'<span class="notes-browser-subtitle">Open items on the board, and what has been closed</span>' +
|
|
69
|
+
'<div class="notes-browser-window-actions" aria-label="Sticky Notes window controls">' +
|
|
70
|
+
'<button id="notes-browser-wide" class="notes-browser-icon-btn" type="button" title="Widen panel" aria-label="Widen Sticky Notes panel" aria-pressed="false">' + iconHtml("chevrons-left-right") + '</button>' +
|
|
71
|
+
'<button id="notes-browser-fullscreen" class="notes-browser-icon-btn" type="button" title="Toggle fullscreen" aria-label="Toggle Sticky Notes fullscreen" aria-pressed="false">' + iconHtml("maximize-2") + '</button>' +
|
|
72
|
+
'<button id="notes-browser-close" class="notes-browser-icon-btn" type="button" title="Close Sticky Notes" aria-label="Close Sticky Notes">' + iconHtml("x") + '</button>' +
|
|
73
|
+
'</div>' +
|
|
74
|
+
'</header>' +
|
|
75
|
+
'<div id="notes-browser-tabs" class="notes-browser-tabs" role="tablist" aria-label="Sticky Notes state"></div>' +
|
|
76
|
+
'<div id="notes-browser-grid" class="notes-browser-grid" role="list"></div>';
|
|
77
|
+
panels.appendChild(panel);
|
|
78
|
+
|
|
79
|
+
gridEl = panel.querySelector("#notes-browser-grid");
|
|
80
|
+
tabsEl = panel.querySelector("#notes-browser-tabs");
|
|
81
|
+
|
|
82
|
+
panel.querySelector("#notes-browser-close").addEventListener("click", closeNotesBrowser);
|
|
83
|
+
panel.querySelector("#notes-browser-wide").addEventListener("click", function () {
|
|
84
|
+
applyWindowState(!store.get('notesBrowserWide'), store.get('notesBrowserFullscreen'));
|
|
85
|
+
});
|
|
86
|
+
panel.querySelector("#notes-browser-fullscreen").addEventListener("click", function () {
|
|
87
|
+
applyWindowState(store.get('notesBrowserWide'), !store.get('notesBrowserFullscreen'));
|
|
88
|
+
});
|
|
89
|
+
refreshIcons();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Width and fullscreen are transient view state, so they live in the store.
|
|
93
|
+
// `panel-fullscreen` is the shared class the document viewer, terminal, and
|
|
94
|
+
// Logs already use, so the geometry is the established one.
|
|
95
|
+
function applyWindowState(wide, fullscreen) {
|
|
96
|
+
store.set({ notesBrowserWide: !!wide, notesBrowserFullscreen: !!fullscreen });
|
|
97
|
+
if (!panel) return;
|
|
98
|
+
panel.classList.toggle("notes-browser-wide", !!wide && !fullscreen);
|
|
99
|
+
panel.classList.toggle("panel-fullscreen", !!fullscreen);
|
|
100
|
+
var wideBtn = panel.querySelector("#notes-browser-wide");
|
|
101
|
+
var fullBtn = panel.querySelector("#notes-browser-fullscreen");
|
|
102
|
+
if (wideBtn) {
|
|
103
|
+
wideBtn.setAttribute("aria-pressed", wide ? "true" : "false");
|
|
104
|
+
wideBtn.disabled = !!fullscreen;
|
|
105
|
+
}
|
|
106
|
+
if (fullBtn) fullBtn.setAttribute("aria-pressed", fullscreen ? "true" : "false");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --- Tabs ----------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
function renderTabs(openCount, closedCount) {
|
|
112
|
+
if (!tabsEl) return;
|
|
113
|
+
var active = currentTab();
|
|
114
|
+
tabsEl.innerHTML = "";
|
|
115
|
+
var specs = [
|
|
116
|
+
{ key: "open", label: "Open", count: openCount },
|
|
117
|
+
{ key: "closed", label: "Closed", count: closedCount },
|
|
118
|
+
];
|
|
119
|
+
for (var i = 0; i < specs.length; i++) {
|
|
120
|
+
(function (spec) {
|
|
121
|
+
var tab = document.createElement("button");
|
|
122
|
+
tab.type = "button";
|
|
123
|
+
tab.className = "notes-browser-tab" + (spec.key === active ? " active" : "");
|
|
124
|
+
tab.setAttribute("role", "tab");
|
|
125
|
+
tab.setAttribute("aria-selected", spec.key === active ? "true" : "false");
|
|
126
|
+
tab.tabIndex = spec.key === active ? 0 : -1;
|
|
127
|
+
tab.dataset.tab = spec.key;
|
|
128
|
+
tab.innerHTML = '<span>' + spec.label + '</span><span class="notes-browser-tab-count">' + spec.count + '</span>';
|
|
129
|
+
tab.addEventListener("click", function () { selectTab(spec.key); });
|
|
130
|
+
tab.addEventListener("keydown", function (e) {
|
|
131
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
selectTab(spec.key === "open" ? "closed" : "open");
|
|
134
|
+
var next = tabsEl.querySelector('.notes-browser-tab.active');
|
|
135
|
+
if (next) next.focus();
|
|
136
|
+
});
|
|
137
|
+
tabsEl.appendChild(tab);
|
|
138
|
+
})(specs[i]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function selectTab(key) {
|
|
143
|
+
store.set({ notesBrowserTab: key === "closed" ? "closed" : "open" });
|
|
144
|
+
renderNotesBrowser();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --- Cards ---------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
function closedMeta(data) {
|
|
150
|
+
var parts = [];
|
|
151
|
+
if (data.closedAt) {
|
|
152
|
+
var when = new Date(data.closedAt);
|
|
153
|
+
if (!isNaN(when.getTime())) parts.push("Closed " + when.toLocaleDateString());
|
|
154
|
+
} else {
|
|
155
|
+
parts.push("Closed");
|
|
156
|
+
}
|
|
157
|
+
var by = data.closedBy;
|
|
158
|
+
if (by && by.type === "user" && by.displayName) parts.push("by " + by.displayName);
|
|
159
|
+
else if (by && by.type === "session") parts.push("by an agent session");
|
|
160
|
+
return parts.join(" · ");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function buildCard(data) {
|
|
164
|
+
var card = document.createElement("article");
|
|
165
|
+
var closed = isClosed(data);
|
|
166
|
+
card.className = "notes-browser-card" + (closed ? " closed" : "");
|
|
167
|
+
card.setAttribute("role", "listitem");
|
|
168
|
+
card.dataset.color = data.color || "purple";
|
|
169
|
+
card.dataset.noteId = data.id;
|
|
170
|
+
|
|
171
|
+
var header = document.createElement("div");
|
|
172
|
+
header.className = "notes-browser-card-header";
|
|
173
|
+
|
|
174
|
+
var title = document.createElement("h3");
|
|
175
|
+
title.className = "notes-browser-card-title";
|
|
176
|
+
title.textContent = getTitle(data.text) || "Untitled";
|
|
177
|
+
header.appendChild(title);
|
|
178
|
+
|
|
179
|
+
// Exactly one lifecycle action per card, and it is never destructive.
|
|
180
|
+
var action = document.createElement("button");
|
|
181
|
+
action.type = "button";
|
|
182
|
+
action.className = "notes-browser-card-action";
|
|
183
|
+
if (closed) {
|
|
184
|
+
action.innerHTML = iconHtml("rotate-ccw") + "<span>Reopen</span>";
|
|
185
|
+
action.title = "Reopen this note and return it to the board";
|
|
186
|
+
action.setAttribute("aria-label", "Reopen note: " + title.textContent);
|
|
187
|
+
action.addEventListener("click", function (e) {
|
|
188
|
+
e.stopPropagation();
|
|
189
|
+
send({ type: "note_reopen", id: data.id });
|
|
190
|
+
});
|
|
191
|
+
} else {
|
|
192
|
+
action.innerHTML = iconHtml("check") + "<span>Close</span>";
|
|
193
|
+
action.title = "Close this note; it stays on record and can be reopened";
|
|
194
|
+
action.setAttribute("aria-label", "Close note: " + title.textContent);
|
|
195
|
+
action.addEventListener("click", function (e) {
|
|
196
|
+
e.stopPropagation();
|
|
197
|
+
send({ type: "note_close", id: data.id });
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
header.appendChild(action);
|
|
201
|
+
card.appendChild(header);
|
|
202
|
+
|
|
203
|
+
var body = document.createElement("div");
|
|
204
|
+
body.className = "notes-browser-card-body";
|
|
205
|
+
var bodyLines = (data.text || "").split("\n").slice(1).join("\n").trim();
|
|
206
|
+
if (bodyLines) {
|
|
207
|
+
body.innerHTML = renderMiniMarkdown("_\n" + bodyLines).replace('<div class="sn-title">_</div>', "");
|
|
208
|
+
}
|
|
209
|
+
card.appendChild(body);
|
|
210
|
+
|
|
211
|
+
if (closed) {
|
|
212
|
+
var meta = document.createElement("p");
|
|
213
|
+
meta.className = "notes-browser-card-meta";
|
|
214
|
+
meta.textContent = closedMeta(data);
|
|
215
|
+
card.appendChild(meta);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
var strip = document.createElement("div");
|
|
219
|
+
strip.className = "notes-browser-card-color";
|
|
220
|
+
card.appendChild(strip);
|
|
221
|
+
|
|
222
|
+
// An open note can be jumped to on the canvas. A closed note has no card on
|
|
223
|
+
// the canvas, so reopening is the only way back and the body is read-only.
|
|
224
|
+
if (!closed) {
|
|
225
|
+
card.tabIndex = 0;
|
|
226
|
+
card.addEventListener("click", function () { focusNoteOnCanvas(data.id); });
|
|
227
|
+
card.addEventListener("keydown", function (e) {
|
|
228
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
229
|
+
e.preventDefault();
|
|
230
|
+
focusNoteOnCanvas(data.id);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return card;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function emptyState(tab) {
|
|
237
|
+
var empty = document.createElement("div");
|
|
238
|
+
empty.className = "notes-browser-empty";
|
|
239
|
+
if (tab === "closed") {
|
|
240
|
+
empty.innerHTML = iconHtml("check") +
|
|
241
|
+
"<p>Nothing closed yet</p>" +
|
|
242
|
+
"<p class=\"notes-browser-empty-sub\">Closing a note takes it off the board without deleting it. Anything you close shows up here and can be reopened.</p>";
|
|
243
|
+
} else {
|
|
244
|
+
empty.innerHTML = iconHtml("sticky-note") +
|
|
245
|
+
"<p>Keep what matters across sessions</p>" +
|
|
246
|
+
"<p class=\"notes-browser-empty-sub\">Use sticky notes for checklists, goals, handoffs, and durable project knowledge. You and every Clay agent can find them in future sessions.</p>";
|
|
247
|
+
}
|
|
248
|
+
return empty;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function renderNotesBrowser() {
|
|
252
|
+
if (!gridEl) return;
|
|
253
|
+
var all = listNoteData();
|
|
254
|
+
var open = openNotes(all);
|
|
255
|
+
var closed = closedNotes(all);
|
|
256
|
+
renderTabs(open.length, closed.length);
|
|
257
|
+
|
|
258
|
+
var tab = currentTab();
|
|
259
|
+
var shown = tab === "closed" ? closed : open;
|
|
260
|
+
// Newest first. The id is timestamp-prefixed, so it orders by creation.
|
|
261
|
+
shown = shown.slice().sort(function (a, b) {
|
|
262
|
+
var left = String(a.id || "");
|
|
263
|
+
var right = String(b.id || "");
|
|
264
|
+
return left < right ? 1 : left > right ? -1 : 0;
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
gridEl.innerHTML = "";
|
|
268
|
+
if (shown.length === 0) {
|
|
269
|
+
gridEl.appendChild(emptyState(tab));
|
|
270
|
+
refreshIcons();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
for (var i = 0; i < shown.length; i++) gridEl.appendChild(buildCard(shown[i]));
|
|
274
|
+
refreshIcons();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// --- Lifecycle -----------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
// The conversation, composer, and title bar stay mounted and visible: the
|
|
280
|
+
// browser opens beside them in the single right workbench slot.
|
|
281
|
+
export function openNotesBrowser() {
|
|
282
|
+
ensurePanel();
|
|
283
|
+
if (!panel) return;
|
|
284
|
+
closeOtherRightTools();
|
|
285
|
+
// The floating canvas would otherwise sit on top of the pane.
|
|
286
|
+
hideNotes();
|
|
287
|
+
panel.classList.remove("hidden");
|
|
288
|
+
applyWindowState(store.get('notesBrowserWide'), false);
|
|
289
|
+
store.set({ notesBrowserOpen: true });
|
|
290
|
+
var sidebarBtn = document.getElementById("sticky-notes-sidebar-btn");
|
|
291
|
+
if (sidebarBtn) sidebarBtn.classList.add("active");
|
|
292
|
+
renderNotesBrowser();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function closeNotesBrowser() {
|
|
296
|
+
if (!store.get('notesBrowserOpen')) return;
|
|
297
|
+
if (panel) panel.classList.add("hidden");
|
|
298
|
+
// Fullscreen is always dropped on close so the next open is a bounded pane.
|
|
299
|
+
applyWindowState(store.get('notesBrowserWide'), false);
|
|
300
|
+
store.set({ notesBrowserOpen: false });
|
|
301
|
+
var sidebarBtn = document.getElementById("sticky-notes-sidebar-btn");
|
|
302
|
+
if (sidebarBtn) sidebarBtn.classList.remove("active");
|
|
303
|
+
// The canvas comes back so the open notes are reachable again.
|
|
304
|
+
showNotes();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function isNotesBrowserOpen() {
|
|
308
|
+
return !!store.get('notesBrowserOpen');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function initNotesBrowser() {
|
|
312
|
+
document.addEventListener("keydown", function (e) {
|
|
313
|
+
if (e.key !== "Escape") return;
|
|
314
|
+
if (!store.get('notesBrowserOpen')) return;
|
|
315
|
+
closeNotesBrowser();
|
|
316
|
+
});
|
|
317
|
+
store.subscribe(function (state, previous) {
|
|
318
|
+
if (state.currentSlug === previous.currentSlug) return;
|
|
319
|
+
// A different project starts from a closed, bounded, Open-tab pane.
|
|
320
|
+
closeNotesBrowser();
|
|
321
|
+
applyWindowState(false, false);
|
|
322
|
+
store.set({ notesBrowserTab: "open" });
|
|
323
|
+
});
|
|
324
|
+
}
|