clay-server 5.2.0-beta.4 → 5.2.0-beta.5

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 (171) hide show
  1. package/lib/account-auth-socket.js +1 -0
  2. package/lib/builtin-mates.js +17 -14
  3. package/lib/canonical-clay-record-authority.js +15 -0
  4. package/lib/canonical-clay-scheduling-authority.js +19 -0
  5. package/lib/claude-hook-installer.js +28 -0
  6. package/lib/clay-mate-chat-receipts.js +26 -0
  7. package/lib/clay-mate-chat.js +87 -0
  8. package/lib/clay-mate-knowledge-input.js +26 -0
  9. package/lib/clay-mate-knowledge.js +95 -0
  10. package/lib/clay-mate-lifecycle.js +95 -0
  11. package/lib/clay-product-control-input.js +33 -0
  12. package/lib/clay-product-control-tools.js +51 -0
  13. package/lib/clay-product-control.js +237 -0
  14. package/lib/clay-project-lifecycle.js +154 -0
  15. package/lib/clay-project-record-input.js +44 -0
  16. package/lib/clay-project-records.js +83 -0
  17. package/lib/clay-scheduling-input.js +21 -0
  18. package/lib/clay-scheduling-receipts.js +26 -0
  19. package/lib/clay-scheduling.js +119 -0
  20. package/lib/clay-screen-controls.js +96 -0
  21. package/lib/clay-workspace-controls.js +83 -0
  22. package/lib/clay-workspace-input.js +17 -0
  23. package/lib/clay-workspace-receipts.js +26 -0
  24. package/lib/daemon-project-access.js +11 -0
  25. package/lib/daemon-project-lifecycle-callbacks.js +15 -0
  26. package/lib/daemon-project-lifecycle.js +262 -0
  27. package/lib/daemon-projects.js +1 -1
  28. package/lib/daemon.js +73 -221
  29. package/lib/issues-service.js +29 -15
  30. package/lib/knowledge-db-import.js +5 -4
  31. package/lib/knowledge-db-service.js +9 -6
  32. package/lib/knowledge-document-index.js +2 -1
  33. package/lib/knowledge-product-operations.js +46 -0
  34. package/lib/mate-creation-mcp-server.js +2 -0
  35. package/lib/mate-knowledge-canonical-access.js +10 -0
  36. package/lib/mate-knowledge-service.js +7 -4
  37. package/lib/mate-lifecycle-service.js +94 -0
  38. package/lib/mate-message-delivery.js +43 -0
  39. package/lib/mate-ready-creation.js +9 -1
  40. package/lib/mate-schedule-input.js +29 -0
  41. package/lib/mate-schedule-lifecycle.js +151 -0
  42. package/lib/mate-scheduling.js +103 -0
  43. package/lib/mates.js +4 -3
  44. package/lib/notes-checklist.js +50 -0
  45. package/lib/notes.js +49 -3
  46. package/lib/os-users.js +20 -5
  47. package/lib/project-admin-overview.js +51 -0
  48. package/lib/project-ask-user.js +4 -2
  49. package/lib/project-delegated-follow-up.js +2 -1
  50. package/lib/project-directory-claim.js +65 -0
  51. package/lib/project-file-operations.js +61 -0
  52. package/lib/project-filesystem.js +13 -87
  53. package/lib/project-full-access.js +23 -2
  54. package/lib/project-knowledge.js +32 -18
  55. package/lib/project-lifecycle-operations.js +98 -0
  56. package/lib/project-lifecycle-process-witness.js +32 -0
  57. package/lib/project-lifecycle-runtime.js +50 -0
  58. package/lib/project-linux-access.js +22 -0
  59. package/lib/project-logs-canonical-clay.js +27 -0
  60. package/lib/project-logs-clay-query.js +16 -0
  61. package/lib/project-logs-schema.js +4 -2
  62. package/lib/project-logs-service.js +8 -7
  63. package/lib/project-logs.js +1 -0
  64. package/lib/project-loop.js +1 -0
  65. package/lib/project-mate-chat-control.js +55 -0
  66. package/lib/project-mate-creation-proposal.js +18 -3
  67. package/lib/project-models.js +14 -31
  68. package/lib/project-notifications.js +74 -16
  69. package/lib/project-ownership.js +77 -0
  70. package/lib/project-pair-result-delivery.js +14 -3
  71. package/lib/project-pending-message-queue.js +1 -0
  72. package/lib/project-product-control.js +87 -0
  73. package/lib/project-product-session-queue.js +46 -0
  74. package/lib/project-push-access.js +28 -0
  75. package/lib/project-scheduled-message-queue.js +30 -0
  76. package/lib/project-scheduled-messages.js +63 -64
  77. package/lib/project-scheduled-task-control.js +83 -0
  78. package/lib/project-scheduled-tasks.js +13 -2
  79. package/lib/project-scheduling-access.js +10 -0
  80. package/lib/project-session-actions.js +65 -0
  81. package/lib/project-session-delete.js +8 -1
  82. package/lib/project-session-github.js +47 -12
  83. package/lib/project-session-history-actions.js +165 -0
  84. package/lib/project-session-runtime-actions.js +63 -0
  85. package/lib/project-session-vendor.js +54 -0
  86. package/lib/project-sessions.js +64 -241
  87. package/lib/project-user-message.js +30 -1
  88. package/lib/project-workspace-operations.js +126 -0
  89. package/lib/project-workspace-query.js +9 -2
  90. package/lib/project.js +95 -3
  91. package/lib/public/css/clay-tool-activity.css +51 -0
  92. package/lib/public/css/home-mate-creation.css +5 -0
  93. package/lib/public/css/overlays.css +4 -2
  94. package/lib/public/css/project-admin-overview.css +13 -0
  95. package/lib/public/css/session-folders.css +11 -0
  96. package/lib/public/css/session-github.css +3 -0
  97. package/lib/public/css/sticky-notes.css +61 -1
  98. package/lib/public/index.html +1 -1
  99. package/lib/public/modules/admin-settings-projects.js +6 -2
  100. package/lib/public/modules/app-connection.js +8 -1
  101. package/lib/public/modules/app-message-router.js +1 -1
  102. package/lib/public/modules/app-messages.js +18 -3
  103. package/lib/public/modules/app-projects.js +5 -0
  104. package/lib/public/modules/clay-owner-navigation.js +27 -0
  105. package/lib/public/modules/clay-tool-activity.js +140 -0
  106. package/lib/public/modules/clay-tool-presentation.js +338 -0
  107. package/lib/public/modules/command-palette.js +1 -1
  108. package/lib/public/modules/default-ai.js +11 -3
  109. package/lib/public/modules/home-debate-planning.js +5 -5
  110. package/lib/public/modules/home-mate-chat.js +8 -6
  111. package/lib/public/modules/home-mate-creation.js +73 -1
  112. package/lib/public/modules/personal-settings-dialog.js +7 -1
  113. package/lib/public/modules/project-access-recovery.js +17 -0
  114. package/lib/public/modules/project-admin-overview.js +21 -0
  115. package/lib/public/modules/session-folder-layout.js +1 -1
  116. package/lib/public/modules/session-folder-view-menu.js +9 -0
  117. package/lib/public/modules/session-github.js +65 -8
  118. package/lib/public/modules/sidebar-session-folders-ui.js +1 -1
  119. package/lib/public/modules/sticky-note-markdown.js +72 -6
  120. package/lib/public/modules/sticky-notes-actions.js +30 -0
  121. package/lib/public/modules/sticky-notes-browser.js +9 -2
  122. package/lib/public/modules/sticky-notes-card.js +23 -7
  123. package/lib/public/modules/sticky-notes-checklist.js +71 -0
  124. package/lib/public/modules/sticky-notes-editor.js +108 -44
  125. package/lib/public/modules/sticky-notes-shared.js +50 -2
  126. package/lib/public/modules/sticky-notes-state.js +66 -0
  127. package/lib/public/modules/sticky-notes.js +34 -51
  128. package/lib/public/modules/thinking-lifecycle.js +33 -3
  129. package/lib/public/modules/thinking-view.js +23 -1
  130. package/lib/public/modules/tools.js +34 -6
  131. package/lib/public/modules/user-settings-appearance.js +10 -3
  132. package/lib/public/modules/user-settings-editors.js +1 -1
  133. package/lib/public/modules/user-settings-layout.js +11 -2
  134. package/lib/public/modules/user-settings-preferences.js +6 -2
  135. package/lib/public/modules/user-settings-profile.js +12 -2
  136. package/lib/public/modules/user-settings.js +3 -2
  137. package/lib/public/style.css +2 -0
  138. package/lib/scheduled-task-records.js +3 -1
  139. package/lib/scheduler.js +2 -0
  140. package/lib/sdk-bridge.js +220 -21
  141. package/lib/sdk-message-processor.js +2 -0
  142. package/lib/sdk-response-recovery.js +155 -0
  143. package/lib/server-admin.js +63 -16
  144. package/lib/server-dm.js +1 -1
  145. package/lib/server-global-ws.js +10 -20
  146. package/lib/server-home-chat-events.js +7 -3
  147. package/lib/server-home-chat.js +4 -16
  148. package/lib/server-home-mate-creation.js +1 -0
  149. package/lib/server-home-mcp-consent.js +3 -3
  150. package/lib/server-mates.js +25 -67
  151. package/lib/server-owner-ui.js +77 -0
  152. package/lib/server-path-access.js +38 -0
  153. package/lib/server-project-removal.js +22 -0
  154. package/lib/server.js +99 -29
  155. package/lib/session-config-transition.js +45 -0
  156. package/lib/session-folders.js +7 -2
  157. package/lib/session-github.js +9 -2
  158. package/lib/session-visibility.js +5 -2
  159. package/lib/sessions.js +10 -0
  160. package/lib/terminal-manager.js +52 -0
  161. package/lib/users-permissions.js +2 -8
  162. package/lib/users.js +4 -0
  163. package/lib/workspace-query-access.js +4 -1
  164. package/lib/ws-schema.js +8 -2
  165. package/lib/yoke/adapters/claude.js +47 -8
  166. package/lib/yoke/adapters/codex.js +52 -111
  167. package/lib/yoke/claude-worker-startup.js +28 -0
  168. package/lib/yoke/codex-idle-recovery.js +81 -1
  169. package/lib/yoke/codex-recovery-admission.js +164 -0
  170. package/lib/yoke/session-tool-transport.js +3 -0
  171. package/package.json +1 -1
@@ -0,0 +1,21 @@
1
+ // This page never opens a project socket or loads project content modules.
2
+ var tabs = Array.from(document.querySelectorAll('[role="tab"]'));
3
+ function select(tab) {
4
+ tabs.forEach(function (item) {
5
+ var active = item === tab;
6
+ item.setAttribute("aria-selected", String(active));
7
+ item.tabIndex = active ? 0 : -1;
8
+ document.getElementById(item.getAttribute("aria-controls")).hidden = !active;
9
+ });
10
+ }
11
+ tabs.forEach(function (tab, index) {
12
+ tab.addEventListener("click", function () { select(tab); });
13
+ tab.addEventListener("keydown", function (event) {
14
+ if (["ArrowLeft", "ArrowRight", "Home", "End"].indexOf(event.key) === -1) return;
15
+ event.preventDefault();
16
+ var target = event.key === "Home" ? tabs[0] : event.key === "End" ? tabs[tabs.length - 1] : tabs[(index + (event.key === "ArrowRight" ? 1 : tabs.length - 1)) % tabs.length];
17
+ select(target);
18
+ target.focus();
19
+ });
20
+ });
21
+ select(tabs[0]);
@@ -9,7 +9,7 @@ export var FAVORITES_ID = "favorites";
9
9
  export var UNFILED_ID = "unfiled";
10
10
 
11
11
  export function defaultFolderState() {
12
- return { folders: [], assignments: {}, favorites: [], orders: {}, collapsed: {}, view: { sort: "activity", direction: "desc" } };
12
+ return { folders: [], assignments: {}, favorites: [], orders: {}, collapsed: {}, view: { sort: "activity", direction: "desc", showDateHeadings: true } };
13
13
  }
14
14
 
15
15
  export function sortUnits(units, view, order) {
@@ -78,6 +78,15 @@ export function paintViewMenu() {
78
78
  note.textContent = "Drag sessions to arrange them. Favorites keep their own order.";
79
79
  menu.appendChild(note);
80
80
  }
81
+ var dateToggle = document.createElement("button");
82
+ dateToggle.type = "button";
83
+ dateToggle.className = "session-folder-view-toggle";
84
+ dateToggle.dataset.focusKey = "showDateHeadings";
85
+ dateToggle.setAttribute("role", "switch");
86
+ dateToggle.setAttribute("aria-checked", String(view.showDateHeadings !== false));
87
+ dateToggle.innerHTML = '<span>Show date headings</span><span class="session-folder-view-toggle-track" aria-hidden="true"><span class="session-folder-view-toggle-thumb"></span></span>';
88
+ dateToggle.addEventListener("click", function () { sendViewPatch({ showDateHeadings: view.showDateHeadings === false }); });
89
+ menu.appendChild(dateToggle);
81
90
  if (focusKey) {
82
91
  var target = menu.querySelector('[data-focus-key="' + focusKey + '"]') || menu.querySelector(".session-folder-radio.selected");
83
92
  if (target) target.focus();
@@ -46,6 +46,28 @@ function compactGithubWork(links, sessionRow) {
46
46
  var latestPr = links.find(function (link) { return link.kind === 'pr'; });
47
47
  return [latestLinear, latestIssue, latestPr].filter(Boolean);
48
48
  }
49
+ function personalScope() {
50
+ return String(store.get('currentSlug') || '') + '\u0000' + String(store.get('myUserId') || 'default');
51
+ }
52
+ function ensurePersonalScope() {
53
+ var scope = personalScope();
54
+ if (store.get('githubPersonalReviewScope') !== scope) store.set({ githubPersonalReviewScope: scope, githubPersonalReview: {} });
55
+ return scope;
56
+ }
57
+ function sessionIdentifier(session) {
58
+ return String(session && (session._githubSessionId || session.id || session.localId) || '');
59
+ }
60
+ function personalReviewState(session, link) {
61
+ var scope = ensurePersonalScope();
62
+ var key = scope + '\u0000' + (sessionIdentifier(session) || String(link._githubSessionId || '')) + '\u0000' + String(link.url || '').toLowerCase();
63
+ return (store.get('githubPersonalReview') || {})[key] || link.personalReviewState || '';
64
+ }
65
+ function prReviewStatus(link) {
66
+ if (link.review === 'CHANGES_REQUESTED') return { icon: 'message-square-warning', label: 'Changes requested', className: 'github-review-changes' };
67
+ if (link.personalReviewState === 'requested') return { icon: 'user-round-check', label: 'Review requested from you', className: 'github-review-requested' };
68
+ if (link.review === 'APPROVED') return { icon: 'badge-check', label: 'Approved', className: 'github-review-approved' };
69
+ return null;
70
+ }
49
71
  export function githubWorkMarkup(session, detailed, expanded, sessionRow) {
50
72
  var links = orderedGithubWork(session);
51
73
  if (!links.length) return '';
@@ -56,13 +78,17 @@ export function githubWorkMarkup(session, detailed, expanded, sessionRow) {
56
78
  return anchor(link.url, description, LINEAR_MARK + '<span class="linear-work-id">' + escapeHtml(link.identifier) + '</span><span class="linear-work-title">' + escapeHtml(link.title) + '</span><span class="linear-work-state"><span class="linear-work-state-full">' + escapeHtml(link.stateName) + '</span><span class="linear-work-state-compact" aria-hidden="true">' + escapeHtml(compactLinearState(link.stateName)) + '</span></span>', 'session-github-link session-linear-link');
57
79
  }
58
80
  var state = ['open', 'closed', 'merged', 'draft'].indexOf(link.state) >= 0 ? link.state : 'unknown';
59
- var icon = link.kind === 'pr' ? (state === 'merged' ? 'git-merge' : state === 'closed' ? 'git-pull-request-closed' : state === 'draft' ? 'git-pull-request-draft' : 'git-pull-request') : (state === 'closed' ? 'circle-check' : 'circle-dot');
60
- var description = link.repository + ' · ' + label(link) + ' · ' + link.title + ' · ' + state + (link.checkedAt ? ' · Checked ' + new Date(link.checkedAt).toLocaleString() : '') + (link.stale ? ' (last known status; refresh unavailable)' : '');
61
- var html = anchor(link.url, description, iconHtml('github', 'github-work-brand') + iconHtml(icon, 'github-work-status') + '<span><span class="github-work-kind">' + (link.kind === 'pr' ? 'PR ' : 'Issue ') + '</span>#' + escapeHtml(String(link.number)) + '</span>', 'session-github-link github-kind-' + (link.kind === 'pr' ? 'pr' : 'issue') + ' github-state-' + state);
81
+ var reviewLink = Object.assign({}, link, { personalReviewState: personalReviewState(session, link) });
82
+ var reviewStatus = link.kind === 'pr' && state === 'open' ? prReviewStatus(reviewLink) : null;
83
+ var icon = link.kind === 'pr' ? (state === 'merged' ? 'git-merge' : state === 'closed' ? 'git-pull-request-closed' : state === 'draft' ? 'git-pull-request-draft' : reviewStatus ? reviewStatus.icon : 'git-pull-request') : (state === 'closed' ? 'circle-check' : 'circle-dot');
84
+ var statusLabel = reviewStatus ? reviewStatus.label : (state === 'merged' ? 'Merged' : state === 'closed' ? 'Closed' : state === 'draft' ? 'Draft' : state === 'open' ? 'Open' : 'Unknown');
85
+ var description = link.repository + ' · ' + label(link) + ' · ' + link.title + ' · ' + statusLabel + (link.checkedAt ? ' · Checked ' + new Date(link.checkedAt).toLocaleString() : '') + (link.stale ? ' (last known status; refresh unavailable)' : '');
86
+ var statusMarkup = '<span class="github-work-status' + (reviewStatus ? ' ' + reviewStatus.className : '') + '" title="' + escapeHtml(statusLabel) + '" aria-label="' + escapeHtml(statusLabel) + '">' + iconHtml(icon) + '</span>';
87
+ var html = anchor(link.url, description, iconHtml('github', 'github-work-brand') + statusMarkup + '<span><span class="github-work-kind">' + (link.kind === 'pr' ? 'PR ' : 'Issue ') + '</span>#' + escapeHtml(String(link.number)) + '</span>', 'session-github-link github-kind-' + (link.kind === 'pr' ? 'pr' : 'issue') + ' github-state-' + state);
62
88
  if (expanded) html = '<span class="github-work-entry">' + html + '<span class="github-work-entry-title">' + escapeHtml(link.title || '') + '</span><span class="github-work-entry-meta">' + escapeHtml(link.repository + ' · ' + state + (link.stale ? ' · Last known status' : '')) + '</span></span>';
63
89
  if (detailed && link.kind === 'pr') {
64
90
  var actions = anchor(link.url + '/files', 'View PR changes on GitHub', 'Changes', 'github-work-action') +
65
- anchor(link.url + '#pullrequestreview', 'View PR reviews on GitHub', escapeHtml((link.review || 'Reviews').replace(/_/g, ' ').toLowerCase()), 'github-work-action');
91
+ anchor(link.url + '#pullrequestreview', 'View PR reviews on GitHub', 'Reviews', 'github-work-action');
66
92
  var checks = '';
67
93
  (link.checks || []).forEach(function (check) {
68
94
  if (/^https:\/\//i.test(check.url)) checks += anchor(check.url, check.name + ': ' + check.state, '<span class="github-work-check-name">' + escapeHtml(check.name) + '</span><span class="github-work-check-state">' + escapeHtml(check.state.toLowerCase()) + '</span>', 'github-work-action github-work-check');
@@ -76,6 +102,28 @@ export function githubWorkMarkup(session, detailed, expanded, sessionRow) {
76
102
  return html;
77
103
  }).join('') + (!detailed && !expanded && links.length > displayed.length ? '<button type="button" class="github-work-more" aria-label="Show all ' + links.length + ' linked ' + (links.some(function (item) { return item.provider === 'linear'; }) ? 'work items' : 'GitHub items') + '" aria-haspopup="dialog" aria-expanded="false" data-work-session="' + escapeHtml(String(session.id || session.localId || '')) + '" data-work-primary="' + escapeHtml(JSON.stringify(session.workPrimary || {})) + '" data-github-links="' + escapeHtml(JSON.stringify(safeLinks(session))) + '">+' + (links.length - displayed.length) + '</button>' : '') + '</span>';
78
104
  }
105
+ export function applyGithubPersonalReviewStatus(message) {
106
+ var state = store.snap();
107
+ if (!message || (message.projectSlug && message.projectSlug !== state.currentSlug) || (message.accountId && message.accountId !== state.myUserId)) return;
108
+ var refresh = state.githubWorkRefresh || {};
109
+ if (message.requestId && refresh.requestId && message.requestId !== refresh.requestId) return;
110
+ var updates = message.personalReview || {};
111
+ var scope = ensurePersonalScope();
112
+ var personal = Object.assign({}, store.get('githubPersonalReview') || {});
113
+ var sessionId = String(message.sessionId);
114
+ Object.keys(updates).forEach(function (url) {
115
+ var status = updates[url];
116
+ if (status === 'requested' || status === 'clear') personal[scope + '\u0000' + sessionId + '\u0000' + url.toLowerCase()] = status;
117
+ });
118
+ store.set({ githubPersonalReview: personal });
119
+ var sessions = store.get('githubWorkSessions') || [];
120
+ sessions.forEach(function (session) {
121
+ if (String(session.localId) !== sessionId && String(session.id) !== sessionId) return;
122
+ document.querySelectorAll('[data-github-session-id="' + sessionIdentifier(session) + '"] .session-work-links-slot').forEach(function (slot) { slot.innerHTML = githubWorkMarkup(session, false, false, true); });
123
+ });
124
+ syncGithubSessions(sessions);
125
+ refreshIcons();
126
+ }
79
127
  export function appendGithubWork(row, session, title) {
80
128
  var html = githubWorkMarkup(session, false, false, true);
81
129
  bindGithubWorkPopover();
@@ -94,6 +142,7 @@ export function appendGithubWork(row, session, title) {
94
142
  }
95
143
  export function syncGithubSessions(sessions) {
96
144
  bindGithubWorkPopover();
145
+ ensurePersonalScope();
97
146
  store.set({ githubWorkSessions: sessions || [], githubWorkProject: store.get('currentSlug') });
98
147
  var active = (sessions || []).find(function (session) { return session.active; });
99
148
  var title = document.getElementById('header-title');
@@ -132,15 +181,16 @@ export function refreshGithubWork() {
132
181
  if (previous.key === key && Date.now() - previous.at < 60000) return;
133
182
  var ws = getWs();
134
183
  if (!ws || ws.readyState !== 1) return;
135
- store.set({ githubWorkRefresh: { key: key, at: Date.now() } });
136
- ws.send(JSON.stringify({ type: 'session_github_refresh', sessionIds: ids }));
184
+ var requestId = 'github-refresh-' + Date.now() + '-' + Math.random().toString(36).slice(2);
185
+ store.set({ githubWorkRefresh: { key: key, at: Date.now(), requestId: requestId } });
186
+ ws.send(JSON.stringify({ type: 'session_github_refresh', sessionIds: ids, requestId: requestId, projectSlug: store.get('currentSlug'), accountId: store.get('myUserId') || null }));
137
187
  var linearIds = ids.filter(function (id) { return (store.get('githubWorkSessions') || []).some(function (item) { return item.id === id && (item.linearLinks || []).length; }); });
138
188
  if (linearIds.length) ws.send(JSON.stringify({ type: 'session_linear_refresh', sessionIds: linearIds }));
139
189
  }
140
190
 
141
191
  export function resetGithubWork() {
142
192
  closeGithubWorkPopover();
143
- store.set({ githubWorkSessions: [], githubWorkProject: null, githubWorkRefresh: null, workPrimaryPending: null });
193
+ store.set({ githubWorkSessions: [], githubWorkProject: null, githubWorkRefresh: null, githubPersonalReview: {}, workPrimaryPending: null });
144
194
  var header = document.getElementById('header-github-links');
145
195
  if (header) header.remove();
146
196
  }
@@ -218,7 +268,8 @@ function bindGithubWorkPopover() {
218
268
  popup.dataset.sessionId = button.dataset.workSession || '';
219
269
  var primary;
220
270
  try { primary = JSON.parse(button.dataset.workPrimary || '{}'); } catch (error) { primary = {}; }
221
- popup.innerHTML = '<strong>' + workTitle + '</strong>' + primaryWorkPopover(links, primary, popup.dataset.sessionId, githubWorkMarkup);
271
+ var popupLinks = links.map(function (link) { return Object.assign({}, link, { _githubSessionId: popup.dataset.sessionId }); });
272
+ popup.innerHTML = '<strong>' + workTitle + '</strong>' + primaryWorkPopover(popupLinks, primary, popup.dataset.sessionId, githubWorkMarkup);
222
273
  document.body.appendChild(popup);
223
274
  mountGithubWorkPopover(popup, button);
224
275
  button.setAttribute('aria-expanded', 'true');
@@ -238,3 +289,9 @@ function bindGithubWorkPopover() {
238
289
  if (state.currentSlug !== previous.currentSlug || state.activeSessionId !== previous.activeSessionId || state.dmMode !== previous.dmMode) closeGithubWorkPopover();
239
290
  });
240
291
  }
292
+ store.subscribe(function (state, previous) {
293
+ if (state.currentSlug !== previous.currentSlug || state.myUserId !== previous.myUserId) {
294
+ var scope = personalScope();
295
+ if (state.githubPersonalReviewScope !== scope) store.set({ githubPersonalReviewScope: scope, githubPersonalReview: {} });
296
+ }
297
+ });
@@ -353,7 +353,7 @@ function renderSection(section, ctx, kind) {
353
353
  body.appendChild(empty);
354
354
  }
355
355
  var view = currentFolderState().view;
356
- var dateGroups = isFavorites ? null : groupUnitsByDate(section.units, view.sort, view.direction);
356
+ var dateGroups = isFavorites || view.showDateHeadings === false ? null : groupUnitsByDate(section.units, view.sort, view.direction);
357
357
  if (dateGroups) {
358
358
  for (var g = 0; g < dateGroups.length; g++) {
359
359
  var ids = groupRootSessionIds(dateGroups[g].units);
@@ -6,7 +6,32 @@ export function getTitle(text) {
6
6
  return idx === -1 ? text : text.substring(0, idx);
7
7
  }
8
8
 
9
- function formatInline(text) {
9
+ function checklistFence(line) {
10
+ var match = line.match(/^\s*(\x60{3,}|~{3,})/);
11
+ return match ? match[1].charAt(0) : null;
12
+ }
13
+
14
+ export function getChecklistItems(text) {
15
+ var lines = (text || "").split("\n").slice(1);
16
+ var items = [];
17
+ var fence = null;
18
+ var occurrences = Object.create(null);
19
+ for (var i = 0; i < lines.length; i++) {
20
+ var line = lines[i];
21
+ var nextFence = checklistFence(line);
22
+ if (nextFence) {
23
+ fence = fence === nextFence ? null : (fence || nextFence);
24
+ continue;
25
+ }
26
+ if (fence || !line.trim() || /^\s*#{1,6}\s/.test(line)) continue;
27
+ var occurrence = occurrences[line] || 0;
28
+ occurrences[line] = occurrence + 1;
29
+ items.push({ index: i, text: line, key: line + "\u0000" + occurrence });
30
+ }
31
+ return items;
32
+ }
33
+
34
+ function formatInline(text, preserveChecks) {
10
35
  var escaped = text
11
36
  .replace(/&/g, "&amp;")
12
37
  .replace(/</g, "&lt;")
@@ -17,18 +42,59 @@ function formatInline(text) {
17
42
  .replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, "<em>$1</em>")
18
43
  .replace(/`([^`]+)`/g, "<code>$1</code>")
19
44
  .replace(/~~(.+?)~~/g, "<del>$1</del>")
20
- .replace(/^(?:-\s*)?\[[xX]\]/gm, '<span class="sn-check checked" role="checkbox" aria-checked="true" tabindex="0" contenteditable="false">✓</span>')
21
- .replace(/^(?:-\s*)?\[ \]/gm, '<span class="sn-check" role="checkbox" aria-checked="false" tabindex="0" contenteditable="false">☐</span>')
45
+ .replace(preserveChecks ? /(?!x)x^/ : /^(?:-\s*)?\[[xX]\]/gm, '<span class="sn-check checked" role="checkbox" aria-checked="true" tabindex="0" contenteditable="false">✓</span>')
46
+ .replace(preserveChecks ? /(?!x)x^/ : /^(?:-\s*)?\[ \]/gm, '<span class="sn-check" role="checkbox" aria-checked="false" tabindex="0" contenteditable="false">☐</span>')
22
47
  .replace(/\n/g, "<br>");
23
48
  }
24
49
 
25
- export function renderMiniMarkdown(text) {
50
+ export function renderMiniMarkdown(text, options) {
26
51
  if (!text) return "";
52
+ options = options || {};
27
53
  var lines = text.split("\n");
28
54
  var title = lines[0];
29
55
  var body = lines.slice(1).join("\n");
30
- var html = '<div class="sn-title">' + formatInline(title) + "</div>";
31
- if (body.trim()) html += formatInline(body);
56
+ var html = '<div class="sn-title">' + formatInline(title, options.checklistMode) + "</div>";
57
+ if (body.trim()) {
58
+ if (options.checklistMode) {
59
+ var checklistLines = body.split("\n");
60
+ var checklistHtml = [];
61
+ var inCode = false;
62
+ var fenceChar = null;
63
+ var occurrences = Object.create(null);
64
+ for (var i = 0; i < checklistLines.length; i++) {
65
+ var line = checklistLines[i];
66
+ var lineFence = checklistFence(line);
67
+ if (lineFence) {
68
+ fenceChar = fenceChar === lineFence ? null : (fenceChar || lineFence);
69
+ inCode = !!fenceChar;
70
+ checklistHtml.push(formatInline(line, true));
71
+ if (i < checklistLines.length - 1) checklistHtml.push("<br>");
72
+ continue;
73
+ }
74
+ var task = !inCode && line.trim() && !/^\s*#{1,6}\s/.test(line);
75
+ if (task) {
76
+ var markerMatch = line.match(/^\s*(?:[-*+]\s+)?\[([ xX])\]\s*(.*)$/);
77
+ var checked = markerMatch ? markerMatch[1].toLowerCase() === "x" : false;
78
+ var content = markerMatch ? markerMatch[2] : line;
79
+ var occurrence = occurrences[line] || 0;
80
+ occurrences[line] = occurrence + 1;
81
+ var checklistKey = line + "\u0000" + occurrence;
82
+ var stored = options.checklistState && options.checklistState[checklistKey];
83
+ if (stored === undefined && Array.isArray(options.checklistState)) stored = options.checklistState[i];
84
+ if (stored !== undefined) {
85
+ checked = stored === true;
86
+ }
87
+ checklistHtml.push('<span class="sn-check' + (checked ? ' checked' : '') + '" data-checklist-index="' + i + '" data-checklist-key="' + encodeURIComponent(checklistKey) + '" role="checkbox" aria-checked="' + (checked ? 'true' : 'false') + '" tabindex="0" contenteditable="false">' + (checked ? "✓" : "☐") + "</span>" + formatInline(content));
88
+ } else {
89
+ checklistHtml.push(formatInline(line, !!inCode));
90
+ }
91
+ if (i < checklistLines.length - 1) checklistHtml.push("<br>");
92
+ }
93
+ html += checklistHtml.join("");
94
+ } else {
95
+ html += formatInline(body);
96
+ }
97
+ }
32
98
  return html;
33
99
  }
34
100
 
@@ -0,0 +1,30 @@
1
+ import { getNoteUiState, clearNoteTextTimer, setNoteUiState } from './sticky-notes-shared.js';
2
+ import { checklistDraftText } from './sticky-notes-checklist.js';
3
+ import { mutateChecklist } from './sticky-notes-state.js';
4
+ import { confirmSettingsAction } from './server-settings-state.js';
5
+
6
+ export function requestChecklistMode(id) {
7
+ var ui = getNoteUiState(id);
8
+ if (ui.pending.length) return;
9
+ if (ui.checklistMode) { mutateChecklist(id, { checklistMode: false }); return; }
10
+ var text = ui.text;
11
+ var canonical = ui.canonical;
12
+ function convert() {
13
+ var current = getNoteUiState(id);
14
+ if (current.text !== text || current.canonical.text !== canonical.text || current.canonical.updatedAt !== canonical.updatedAt) {
15
+ setNoteUiState(id, { error: 'The note changed. Review its contents and try again.' }); return;
16
+ }
17
+ clearNoteTextTimer(id);
18
+ mutateChecklist(id, { checklistConvert: true, checklistSource: canonical.text, checklistVersion: canonical.updatedAt });
19
+ }
20
+ if (text.trim() || (canonical.text || '').trim()) {
21
+ confirmSettingsAction('Switching to a checklist will delete all existing contents and completion marks. Continue with an empty checklist?', 'Delete contents and create checklist', convert);
22
+ } else convert();
23
+ }
24
+
25
+ export function closeHumanNote(id, fallback) {
26
+ var ui = getNoteUiState(id, fallback);
27
+ if (ui.pending.length) { setNoteUiState(id, { error: 'Wait for the current save before closing.' }); return; }
28
+ clearNoteTextTimer(id);
29
+ mutateChecklist(id, { humanClose: true, expectedText: ui.canonical.text, expectedVersion: ui.canonical.updatedAt, draftText: ui.checklistMode ? checklistDraftText(ui) : ui.text });
30
+ }
@@ -11,7 +11,8 @@ import { store } from './store.js';
11
11
  import { iconHtml, refreshIcons } from './icons.js';
12
12
  import { renderMiniMarkdown, getTitle } from './sticky-note-markdown.js';
13
13
  import { enhanceClayLogLinks } from './clay-log-links.js';
14
- import { send } from './sticky-notes-shared.js';
14
+ import { closeHumanNote } from './sticky-notes-actions.js';
15
+ import { send, getNoteUiState } from './sticky-notes-shared.js';
15
16
  import { listNoteData, focusNoteOnCanvas, hideNotes, showNotes } from './sticky-notes.js';
16
17
  import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
17
18
 
@@ -215,7 +216,7 @@ function buildCard(data) {
215
216
  action.setAttribute("aria-label", "Close note: " + title.textContent);
216
217
  action.addEventListener("click", function (e) {
217
218
  e.stopPropagation();
218
- send({ type: "note_close", id: data.id });
219
+ closeHumanNote(data.id, data);
219
220
  });
220
221
  }
221
222
  header.appendChild(action);
@@ -229,6 +230,11 @@ function buildCard(data) {
229
230
  enhanceClayLogLinks(body);
230
231
  }
231
232
  card.appendChild(body);
233
+ var ui = getNoteUiState(data.id, data);
234
+ if (ui.error || ui.pending.length) {
235
+ var status = document.createElement('p'); status.className = 'settings-status'; status.setAttribute('role', 'status');
236
+ status.textContent = ui.error || 'Saving…'; card.appendChild(status);
237
+ }
232
238
 
233
239
  if (closed) {
234
240
  var deleteAction = document.createElement("button");
@@ -427,6 +433,7 @@ export function initNotesBrowser() {
427
433
  closeNotesBrowser();
428
434
  });
429
435
  store.subscribe(function (state, previous) {
436
+ if (state.notesBrowserOpen && state.stickyNoteUiState !== previous.stickyNoteUiState) renderNotesBrowser();
430
437
  if (state.connected !== previous.connected || state.myUserId !== previous.myUserId ||
431
438
  state.isMultiUserMode !== previous.isMultiUserMode) closeDeleteDialog(true);
432
439
  if (state.currentSlug === previous.currentSlug) return;
@@ -17,6 +17,7 @@ import {
17
17
  clampSize,
18
18
  debouncedUpdate,
19
19
  } from './sticky-notes-shared.js';
20
+ import { closeHumanNote } from './sticky-notes-actions.js';
20
21
  import { setupTextEdit } from './sticky-notes-editor.js';
21
22
 
22
23
  var colorPickerEl = null;
@@ -74,7 +75,7 @@ export function renderNote(data) {
74
75
  addBtn.innerHTML = iconHtml("plus");
75
76
  addBtn.addEventListener("click", function (e) {
76
77
  e.stopPropagation();
77
- createNote();
78
+ send({ type: "note_create", x: 60, y: 60, color: "purple", opacity: 0.64 });
78
79
  });
79
80
  header.appendChild(addBtn);
80
81
 
@@ -90,6 +91,21 @@ export function renderNote(data) {
90
91
  mdBtn.innerHTML = "<span class='sn-md-label'>MD</span>";
91
92
  header.appendChild(mdBtn);
92
93
 
94
+ var checklistBtn = document.createElement("button");
95
+ checklistBtn.className = "sticky-note-btn sticky-note-checklist-btn";
96
+ checklistBtn.title = data.checklistMode ? "Show note" : "Show checklist";
97
+ checklistBtn.setAttribute("aria-label", checklistBtn.title);
98
+ checklistBtn.setAttribute("aria-pressed", data.checklistMode === true ? "true" : "false");
99
+ checklistBtn.innerHTML = iconHtml("list-check");
100
+ if (data.checklistMode) checklistBtn.classList.add("active");
101
+ header.appendChild(checklistBtn);
102
+
103
+ var saveStatus = document.createElement("span");
104
+ saveStatus.className = "sticky-note-save-status";
105
+ saveStatus.setAttribute("role", "status");
106
+ saveStatus.setAttribute("aria-live", "polite");
107
+ header.appendChild(saveStatus);
108
+
93
109
  var opacityWrap = document.createElement("div");
94
110
  opacityWrap.className = "sticky-note-opacity";
95
111
  var opacitySlider = document.createElement("input");
@@ -139,11 +155,12 @@ export function renderNote(data) {
139
155
  rendered.contentEditable = "true";
140
156
  rendered.spellcheck = true;
141
157
  if (data.text && data.text.trim()) {
142
- rendered.innerHTML = renderMiniMarkdown(data.text);
158
+ rendered.innerHTML = renderMiniMarkdown(data.text, { checklistMode: data.checklistMode === true, checklistState: data.checklistState || [] });
143
159
  enhanceClayLogLinks(rendered);
144
160
  } else {
145
161
  rendered.classList.add("is-empty");
146
162
  }
163
+ rendered.contentEditable = data.checklistMode === true ? "false" : "true";
147
164
  body.appendChild(rendered);
148
165
 
149
166
  el.appendChild(body);
@@ -156,7 +173,7 @@ export function renderNote(data) {
156
173
  // --- Event handlers ---
157
174
  setupDrag(el, spacer, data.id);
158
175
  setupResize(el, resizeHandle, data.id);
159
- setupTextEdit(textarea, rendered, data.id, mdBtn);
176
+ setupTextEdit(textarea, rendered, data.id, mdBtn, checklistBtn, data);
160
177
  setupColorPicker(colorBtn, el, data.id);
161
178
  setupMinimize(minBtn, el, data.id);
162
179
  setupClose(closeBtn, el, data.id);
@@ -336,14 +353,13 @@ function setupMinimize(btn, noteEl, noteId) {
336
353
 
337
354
  // --- Close note ---
338
355
  //
339
- // Completing a note closes it. The record is kept and stays reachable in the
340
- // browser's Closed tab; nothing is ever deleted from this control.
356
+ // The human close transaction retains nonempty notes in Closed and removes
357
+ // truly empty notes after checking the canonical source and visible draft.
341
358
 
342
359
  function setupClose(btn, noteEl, noteId) {
343
360
  btn.addEventListener("click", function (e) {
344
361
  e.stopPropagation();
345
- noteEl.classList.add("hidden");
346
- send({ type: "note_close", id: noteId });
362
+ closeHumanNote(noteId);
347
363
  });
348
364
  }
349
365
 
@@ -0,0 +1,71 @@
1
+ // Plain task controls use Markdown only as the internal persistence format.
2
+ import { iconHtml, refreshIcons } from './icons.js';
3
+ import { getChecklistItems } from './sticky-note-markdown.js';
4
+ import { getNoteUiState, setNoteUiState } from './sticky-notes-shared.js';
5
+ import { mutateChecklist } from './sticky-notes-state.js';
6
+
7
+ export function checklistDraftText(ui) {
8
+ var fields = ui.checklistInputs || {};
9
+ var lines = (ui.text || '').split('\n');
10
+ if (fields.title !== undefined) lines[0] = fields.title;
11
+ getChecklistItems(ui.text).forEach(function (item) {
12
+ if (fields[item.key] !== undefined) {
13
+ var marker = item.text.match(/^\s*(?:[-*+]\s+)?\[([ xX])\]/);
14
+ var checked = ui.checklistState[item.key];
15
+ if (checked === undefined) checked = marker && marker[1].toLowerCase() === 'x';
16
+ lines[item.index + 1] = '- [' + (checked ? 'x' : ' ') + '] ' + fields[item.key];
17
+ }
18
+ });
19
+ if (fields.entry && fields.entry.trim()) {
20
+ if (!lines[0].trim()) lines[0] = 'Checklist';
21
+ lines.push('- [ ] ' + fields.entry.trim());
22
+ }
23
+ return lines.join('\n');
24
+ }
25
+
26
+ export function renderTaskControls(root, id) {
27
+ var ui = getNoteUiState(id);
28
+ var fields = ui.checklistInputs || {};
29
+ var busy = ui.pending.length > 0;
30
+ root.replaceChildren();
31
+ var form = document.createElement('form'); form.className = 'sn-task-form';
32
+ function input(label, value, key) {
33
+ var control = document.createElement('input'); control.type = 'text'; control.value = fields[key] !== undefined ? fields[key] : value;
34
+ control.setAttribute('aria-label', label); control.placeholder = label; control.maxLength = 400;
35
+ control.disabled = busy;
36
+ control.oninput = function () {
37
+ var current = getNoteUiState(id);
38
+ var draft = Object.assign({}, current.checklistInputs || {}); draft[key] = control.value;
39
+ setNoteUiState(id, { checklistInputs: draft });
40
+ };
41
+ return control;
42
+ }
43
+ var title = input('Checklist title', (ui.text || '').split('\n')[0], 'title'); title.className = 'sn-task-title'; form.appendChild(title);
44
+ getChecklistItems(ui.text).forEach(function (item, position) {
45
+ var row = document.createElement('div'); row.className = 'sn-task-row';
46
+ var marker = item.text.match(/^\s*(?:[-*+]\s+)?\[([ xX])\]\s*(.*)$/);
47
+ var check = document.createElement('button'); check.type = 'button'; check.className = 'sn-check';
48
+ var checked = ui.checklistState[item.key];
49
+ if (checked === undefined) checked = marker && marker[1].toLowerCase() === 'x';
50
+ check.classList.toggle('checked', !!checked); check.textContent = checked ? '✓' : '☐';
51
+ check.setAttribute('role', 'checkbox'); check.setAttribute('aria-checked', checked ? 'true' : 'false'); check.setAttribute('aria-label', 'Complete task ' + (position + 1));
52
+ check.dataset.checklistIndex = item.index; check.dataset.checklistKey = encodeURIComponent(item.key);
53
+ check.disabled = busy;
54
+ var label = input('Task ' + (position + 1), marker ? marker[2] : item.text, item.key);
55
+ var remove = document.createElement('button'); remove.type = 'button'; remove.className = 'sn-task-remove'; remove.innerHTML = iconHtml('x'); remove.setAttribute('aria-label', 'Remove task ' + (position + 1)); remove.disabled = busy;
56
+ remove.onclick = function () {
57
+ var current = getNoteUiState(id); var lines = checklistDraftText(current).split('\n'); lines.splice(item.index + 1, 1);
58
+ save(lines.join('\n'));
59
+ };
60
+ row.append(check, label, remove); form.appendChild(row);
61
+ });
62
+ var entry = input('New task', '', 'entry'); entry.className = 'sn-task-entry'; form.appendChild(entry);
63
+ var saveButton = document.createElement('button'); saveButton.type = 'submit'; saveButton.className = 'sn-task-save'; saveButton.textContent = 'Save tasks'; saveButton.disabled = busy; form.appendChild(saveButton);
64
+ function save(text) {
65
+ var current = getNoteUiState(id);
66
+ if (current.pending.length) return;
67
+ mutateChecklist(id, { checklistEdit: true, checklistSource: current.canonical.text, checklistVersion: current.canonical.updatedAt, text: text });
68
+ }
69
+ form.onsubmit = function (event) { event.preventDefault(); save(checklistDraftText(getNoteUiState(id))); };
70
+ root.appendChild(form); refreshIcons(root);
71
+ }