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

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 (301) 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 +1 -239
  6. package/lib/claude-jsonl-watcher.js +1 -204
  7. package/lib/clay-mate-chat-receipts.js +26 -0
  8. package/lib/clay-mate-chat.js +87 -0
  9. package/lib/clay-mate-knowledge-input.js +26 -0
  10. package/lib/clay-mate-knowledge.js +95 -0
  11. package/lib/clay-mate-lifecycle.js +95 -0
  12. package/lib/clay-product-control-input.js +33 -0
  13. package/lib/clay-product-control-tools.js +51 -0
  14. package/lib/clay-product-control.js +237 -0
  15. package/lib/clay-project-lifecycle.js +154 -0
  16. package/lib/clay-project-record-input.js +44 -0
  17. package/lib/clay-project-records.js +83 -0
  18. package/lib/clay-scheduling-input.js +21 -0
  19. package/lib/clay-scheduling-receipts.js +26 -0
  20. package/lib/clay-scheduling.js +119 -0
  21. package/lib/clay-screen-controls.js +96 -0
  22. package/lib/clay-skill-install.js +1 -33
  23. package/lib/clay-workspace-controls.js +83 -0
  24. package/lib/clay-workspace-input.js +17 -0
  25. package/lib/clay-workspace-receipts.js +26 -0
  26. package/lib/cli-sessions.js +1 -300
  27. package/lib/codex-defaults.js +1 -28
  28. package/lib/context-usage.js +3 -2
  29. package/lib/daemon-project-access.js +11 -0
  30. package/lib/daemon-project-lifecycle-callbacks.js +15 -0
  31. package/lib/daemon-project-lifecycle.js +262 -0
  32. package/lib/daemon-projects.js +1 -1
  33. package/lib/daemon.js +78 -276
  34. package/lib/default-ai-runtime.js +4 -8
  35. package/lib/email-mcp-server.js +1 -1
  36. package/lib/issues-service.js +29 -15
  37. package/lib/kiro-defaults.js +1 -22
  38. package/lib/knowledge-db-import.js +5 -4
  39. package/lib/knowledge-db-service.js +9 -6
  40. package/lib/knowledge-document-index.js +2 -1
  41. package/lib/knowledge-product-operations.js +46 -0
  42. package/lib/mate-creation-mcp-server.js +2 -0
  43. package/lib/mate-knowledge-canonical-access.js +10 -0
  44. package/lib/mate-knowledge-service.js +7 -4
  45. package/lib/mate-lifecycle-service.js +94 -0
  46. package/lib/mate-message-delivery.js +43 -0
  47. package/lib/mate-ready-creation.js +9 -1
  48. package/lib/mate-schedule-input.js +29 -0
  49. package/lib/mate-schedule-lifecycle.js +151 -0
  50. package/lib/mate-scheduling.js +103 -0
  51. package/lib/mates.js +4 -3
  52. package/lib/mcp-proxy.js +2 -32
  53. package/lib/mcp-query-bridge.js +1 -1
  54. package/lib/mcp-source-adapters.js +1 -91
  55. package/lib/mcp-source-context.js +1 -42
  56. package/lib/mcp-source-discovery.js +1 -169
  57. package/lib/mcp-source-plugins.js +1 -68
  58. package/lib/mcp-source-restrictions.js +1 -47
  59. package/lib/mcp-tool-policy.js +1 -26
  60. package/lib/notes-checklist.js +50 -0
  61. package/lib/notes.js +49 -3
  62. package/lib/os-user-diagnostics.js +2 -4
  63. package/lib/os-users.js +24 -70
  64. package/lib/project-admin-overview.js +51 -0
  65. package/lib/project-ask-user.js +4 -2
  66. package/lib/project-connection.js +8 -7
  67. package/lib/project-delegated-follow-up.js +2 -1
  68. package/lib/project-directory-claim.js +65 -0
  69. package/lib/project-file-operations.js +61 -0
  70. package/lib/project-filesystem.js +13 -87
  71. package/lib/project-full-access.js +23 -2
  72. package/lib/project-http.js +4 -4
  73. package/lib/project-knowledge.js +32 -18
  74. package/lib/project-lifecycle-operations.js +98 -0
  75. package/lib/project-lifecycle-process-witness.js +32 -0
  76. package/lib/project-lifecycle-runtime.js +50 -0
  77. package/lib/project-linux-access.js +22 -0
  78. package/lib/project-logs-canonical-clay.js +27 -0
  79. package/lib/project-logs-clay-query.js +16 -0
  80. package/lib/project-logs-schema.js +4 -2
  81. package/lib/project-logs-service.js +8 -7
  82. package/lib/project-logs.js +1 -0
  83. package/lib/project-loop.js +1 -0
  84. package/lib/project-mate-chat-control.js +55 -0
  85. package/lib/project-mate-creation-proposal.js +18 -3
  86. package/lib/project-mcp.js +2 -2
  87. package/lib/project-models.js +14 -31
  88. package/lib/project-notifications.js +74 -16
  89. package/lib/project-ownership.js +77 -0
  90. package/lib/project-pair-result-delivery.js +14 -3
  91. package/lib/project-pair-usage.js +12 -13
  92. package/lib/project-pending-message-queue.js +4 -1
  93. package/lib/project-product-control.js +87 -0
  94. package/lib/project-product-session-queue.js +46 -0
  95. package/lib/project-push-access.js +28 -0
  96. package/lib/project-scheduled-message-queue.js +30 -0
  97. package/lib/project-scheduled-messages.js +63 -64
  98. package/lib/project-scheduled-task-control.js +83 -0
  99. package/lib/project-scheduled-tasks.js +13 -2
  100. package/lib/project-scheduling-access.js +10 -0
  101. package/lib/project-session-actions.js +65 -0
  102. package/lib/project-session-delete.js +8 -1
  103. package/lib/project-session-github.js +47 -12
  104. package/lib/project-session-history-actions.js +167 -0
  105. package/lib/project-session-pair.js +1 -1
  106. package/lib/project-session-runtime-actions.js +64 -0
  107. package/lib/project-session-spawn.js +2 -3
  108. package/lib/project-session-vendor.js +55 -0
  109. package/lib/project-sessions.js +82 -267
  110. package/lib/project-user-message.js +31 -1
  111. package/lib/project-vendor-login.js +6 -36
  112. package/lib/project-workspace-operations.js +126 -0
  113. package/lib/project-workspace-query.js +9 -2
  114. package/lib/project.js +95 -3
  115. package/lib/public/app.js +1 -0
  116. package/lib/public/css/clay-tool-activity.css +51 -0
  117. package/lib/public/css/home-mate-creation.css +5 -0
  118. package/lib/public/css/messages.css +6 -2
  119. package/lib/public/css/overlays.css +4 -2
  120. package/lib/public/css/project-admin-overview.css +13 -0
  121. package/lib/public/css/session-folders.css +11 -0
  122. package/lib/public/css/session-github.css +3 -0
  123. package/lib/public/css/sticky-notes.css +61 -1
  124. package/lib/public/index.html +1 -1
  125. package/lib/public/modules/admin-settings-projects.js +6 -2
  126. package/lib/public/modules/app-connection.js +8 -1
  127. package/lib/public/modules/app-favicon.js +11 -2
  128. package/lib/public/modules/app-header.js +4 -2
  129. package/lib/public/modules/app-loop-ui.js +2 -0
  130. package/lib/public/modules/app-message-router.js +1 -1
  131. package/lib/public/modules/app-messages.js +54 -8
  132. package/lib/public/modules/app-notifications.js +1 -2
  133. package/lib/public/modules/app-panels.js +61 -123
  134. package/lib/public/modules/app-projects.js +5 -0
  135. package/lib/public/modules/app-rate-limit.js +7 -27
  136. package/lib/public/modules/chat-render-runtime.js +3 -0
  137. package/lib/public/modules/clay-owner-navigation.js +27 -0
  138. package/lib/public/modules/clay-tool-activity.js +140 -0
  139. package/lib/public/modules/clay-tool-presentation.js +338 -0
  140. package/lib/public/modules/command-palette.js +1 -1
  141. package/lib/public/modules/default-ai.js +11 -3
  142. package/lib/public/modules/home-debate-planning.js +5 -5
  143. package/lib/public/modules/home-mate-chat.js +8 -6
  144. package/lib/public/modules/home-mate-creation.js +73 -1
  145. package/lib/public/modules/mcp-connection-view.js +1 -1
  146. package/lib/public/modules/permission-control.js +5 -4
  147. package/lib/public/modules/personal-settings-dialog.js +7 -1
  148. package/lib/public/modules/project-access-recovery.js +17 -0
  149. package/lib/public/modules/project-admin-overview.js +21 -0
  150. package/lib/public/modules/scheduler.js +2 -0
  151. package/lib/public/modules/session-create-form.js +10 -10
  152. package/lib/public/modules/session-folder-layout.js +1 -1
  153. package/lib/public/modules/session-folder-view-menu.js +9 -0
  154. package/lib/public/modules/session-github.js +65 -8
  155. package/lib/public/modules/settings-defaults.js +27 -28
  156. package/lib/public/modules/sidebar-session-folders-ui.js +1 -1
  157. package/lib/public/modules/sticky-note-markdown.js +72 -6
  158. package/lib/public/modules/sticky-notes-actions.js +30 -0
  159. package/lib/public/modules/sticky-notes-browser.js +9 -2
  160. package/lib/public/modules/sticky-notes-card.js +23 -7
  161. package/lib/public/modules/sticky-notes-checklist.js +71 -0
  162. package/lib/public/modules/sticky-notes-editor.js +108 -44
  163. package/lib/public/modules/sticky-notes-shared.js +50 -2
  164. package/lib/public/modules/sticky-notes-state.js +66 -0
  165. package/lib/public/modules/sticky-notes.js +34 -51
  166. package/lib/public/modules/thinking-lifecycle.js +33 -3
  167. package/lib/public/modules/thinking-view.js +23 -1
  168. package/lib/public/modules/tools.js +36 -7
  169. package/lib/public/modules/tui-grab.js +1 -2
  170. package/lib/public/modules/user-settings-appearance.js +10 -3
  171. package/lib/public/modules/user-settings-editors.js +1 -1
  172. package/lib/public/modules/user-settings-layout.js +11 -2
  173. package/lib/public/modules/user-settings-preferences.js +6 -2
  174. package/lib/public/modules/user-settings-profile.js +12 -2
  175. package/lib/public/modules/user-settings.js +3 -2
  176. package/lib/public/modules/vendor-login-browser.js +5 -3
  177. package/lib/public/modules/vendor-login.js +2 -2
  178. package/lib/public/modules/vendor-priority.js +15 -1
  179. package/lib/public/muse-avatar.svg +4 -0
  180. package/lib/public/style.css +2 -0
  181. package/lib/safe-bash-commands.js +3 -45
  182. package/lib/scheduled-task-records.js +3 -1
  183. package/lib/scheduler.js +2 -0
  184. package/lib/sdk-bridge.js +334 -403
  185. package/lib/sdk-foreground-admission.js +16 -0
  186. package/lib/sdk-mcp-descriptors.js +2 -37
  187. package/lib/sdk-message-processor.js +52 -17
  188. package/lib/sdk-quota-snapshot.js +1 -1
  189. package/lib/sdk-response-recovery.js +1 -0
  190. package/lib/sdk-worker.js +2 -677
  191. package/lib/server-admin.js +63 -16
  192. package/lib/server-dm.js +1 -1
  193. package/lib/server-global-ws.js +10 -20
  194. package/lib/server-home-chat-events.js +7 -3
  195. package/lib/server-home-chat.js +4 -16
  196. package/lib/server-home-mate-creation.js +1 -0
  197. package/lib/server-home-mcp-consent.js +3 -3
  198. package/lib/server-mates.js +25 -67
  199. package/lib/server-owner-ui.js +77 -0
  200. package/lib/server-path-access.js +38 -0
  201. package/lib/server-project-removal.js +22 -0
  202. package/lib/server.js +100 -30
  203. package/lib/session-config-transition.js +45 -0
  204. package/lib/session-folders.js +7 -2
  205. package/lib/session-fork-history.js +21 -0
  206. package/lib/session-github.js +9 -2
  207. package/lib/session-hygiene.js +3 -35
  208. package/lib/session-permission-mode.js +4 -3
  209. package/lib/session-skill-scope.js +11 -0
  210. package/lib/session-visibility.js +5 -2
  211. package/lib/sessions.js +27 -89
  212. package/lib/terminal-manager.js +52 -0
  213. package/lib/tool-llm.js +1 -1
  214. package/lib/tui-transcript-index.js +1 -132
  215. package/lib/users-permissions.js +2 -8
  216. package/lib/users.js +4 -0
  217. package/lib/vendor-login-browser.js +1 -11
  218. package/lib/workspace-query-access.js +4 -1
  219. package/lib/ws-schema.js +8 -2
  220. package/lib/yoke/acp-agent-profiles.js +5 -3
  221. package/lib/yoke/adapters/antigravity.js +3 -14
  222. package/lib/yoke/adapters/claude-query-contract.js +9 -0
  223. package/lib/yoke/adapters/claude.js +54 -50
  224. package/lib/yoke/adapters/codex-query-contract.js +24 -0
  225. package/lib/yoke/adapters/codex.js +53 -111
  226. package/lib/yoke/adapters/kiro.js +3 -1
  227. package/lib/yoke/adapters/muse-query-contract.js +7 -0
  228. package/lib/yoke/adapters/muse.js +122 -0
  229. package/lib/yoke/auth-events.js +17 -0
  230. package/lib/yoke/claude-hook-installer.js +267 -0
  231. package/lib/yoke/claude-jsonl-watcher.js +204 -0
  232. package/lib/yoke/claude-maintenance.js +55 -0
  233. package/lib/yoke/claude-permission-patterns.js +53 -0
  234. package/lib/yoke/claude-response-recovery.js +155 -0
  235. package/lib/yoke/claude-transcript-hygiene.js +34 -0
  236. package/lib/yoke/claude-worker-startup.js +28 -0
  237. package/lib/yoke/clay-skill-install.js +33 -0
  238. package/lib/yoke/cli-sessions.js +300 -0
  239. package/lib/yoke/codex-app-server.js +7 -12
  240. package/lib/yoke/codex-idle-recovery.js +81 -1
  241. package/lib/yoke/codex-recovery-admission.js +164 -0
  242. package/lib/yoke/configuration.js +13 -0
  243. package/lib/yoke/discovery.js +70 -0
  244. package/lib/yoke/errors.js +8 -0
  245. package/lib/yoke/host-environment.js +7 -0
  246. package/lib/yoke/index.js +9 -51
  247. package/lib/yoke/interface.js +116 -0
  248. package/lib/yoke/kiro-acp-server.js +5 -5
  249. package/lib/yoke/legacy-sessions.js +89 -0
  250. package/lib/yoke/login-protocol.js +25 -0
  251. package/lib/yoke/maintenance.js +12 -0
  252. package/lib/yoke/mcp-proxy.js +32 -0
  253. package/lib/yoke/mcp-source-adapters.js +91 -0
  254. package/lib/yoke/mcp-source-context.js +42 -0
  255. package/lib/yoke/mcp-source-discovery.js +169 -0
  256. package/lib/yoke/mcp-source-plugins.js +68 -0
  257. package/lib/yoke/mcp-source-restrictions.js +47 -0
  258. package/lib/yoke/mcp-tool-policy-normalize.js +28 -0
  259. package/lib/yoke/model-preferences.js +6 -0
  260. package/lib/yoke/muse-artifacts.js +36 -0
  261. package/lib/yoke/muse-controls.js +50 -0
  262. package/lib/yoke/muse-input.js +64 -0
  263. package/lib/yoke/muse-items.js +157 -0
  264. package/lib/yoke/muse-policy.js +52 -0
  265. package/lib/yoke/muse-query.js +428 -0
  266. package/lib/yoke/muse-runtime.js +211 -0
  267. package/lib/yoke/muse-services.js +79 -0
  268. package/lib/yoke/muse-skills.js +79 -0
  269. package/lib/yoke/muse-turns.js +71 -0
  270. package/lib/yoke/options/acp.js +2 -0
  271. package/lib/yoke/options/antigravity.js +2 -0
  272. package/lib/yoke/options/claude.js +21 -0
  273. package/lib/yoke/options/codex-defaults.js +29 -0
  274. package/lib/yoke/options/codex-settings.js +21 -0
  275. package/lib/yoke/options/codex.js +6 -0
  276. package/lib/yoke/options/kiro-defaults.js +22 -0
  277. package/lib/yoke/options/kiro.js +5 -0
  278. package/lib/yoke/options/muse.js +5 -0
  279. package/lib/yoke/provider-contract.js +5 -0
  280. package/lib/yoke/providers/antigravity-executable.js +8 -0
  281. package/lib/yoke/providers/claude-executable.js +40 -0
  282. package/lib/yoke/providers/claude-model-descriptions.js +14 -0
  283. package/lib/yoke/providers/claude-process.js +97 -0
  284. package/lib/yoke/providers/claude-provision.js +75 -0
  285. package/lib/yoke/providers/codex-errors.js +8 -0
  286. package/lib/yoke/providers/muse-executable.js +17 -0
  287. package/lib/yoke/provisioning.js +6 -0
  288. package/lib/yoke/query-options.js +10 -0
  289. package/lib/yoke/response-recovery.js +41 -0
  290. package/lib/yoke/sdk-mcp-descriptors.js +37 -0
  291. package/lib/yoke/sdk-server-factory.js +7 -0
  292. package/lib/yoke/session-history.js +8 -0
  293. package/lib/yoke/session-tool-transport.js +34 -13
  294. package/lib/yoke/settings.js +23 -0
  295. package/lib/yoke/source-locations.js +4 -0
  296. package/lib/yoke/title-query.js +18 -7
  297. package/lib/yoke/tui-transcript-index.js +132 -0
  298. package/lib/yoke/usage.js +48 -0
  299. package/lib/yoke/user-input.js +13 -0
  300. package/lib/yoke/vendor-registry.js +81 -1
  301. package/package.json +3 -2
@@ -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
+ }
@@ -8,7 +8,11 @@
8
8
  import { refreshIcons, iconHtml } from './icons.js';
9
9
  import { extractMarkdown, renderMiniMarkdown } from './sticky-note-markdown.js';
10
10
  import { enhanceClayLogLinks } from './clay-log-links.js';
11
- import { debouncedTextUpdate, syncTitle } from './sticky-notes-shared.js';
11
+ import { debouncedTextUpdate, syncTitle, getNoteUiState, setNoteUiState, subscribeNoteUiState } from './sticky-notes-shared.js';
12
+
13
+ import { renderTaskControls } from './sticky-notes-checklist.js';
14
+ import { requestChecklistMode } from './sticky-notes-actions.js';
15
+ import { mutateChecklist } from './sticky-notes-state.js';
12
16
 
13
17
  var formatToolbarEl = null;
14
18
 
@@ -145,51 +149,112 @@ function positionToolbarAtRange(toolbar, range) {
145
149
  });
146
150
  }
147
151
 
148
- export function setupTextEdit(textarea, rendered, noteId, mdBtn) {
152
+ export function setupTextEdit(textarea, rendered, noteId, mdBtn, checklistBtn, noteData) {
149
153
  var noteEl = textarea.closest(".sticky-note");
150
154
  var mdMode = false;
155
+ var disposed = false;
156
+ var ui = getNoteUiState(noteId, noteData);
157
+ var checklistMode = ui.checklistMode === true;
158
+ var checklistState = ui.checklistState || {};
159
+ var renderedDirty = false;
160
+ var sourceText = ui.text !== undefined ? ui.text : textarea.value;
161
+ textarea.value = sourceText;
162
+
163
+ function renderChecklist() {
164
+ var active = document.activeElement;
165
+ var focusKey = active && rendered.contains(active) && active.dataset ? active.dataset.checklistKey : undefined;
166
+ if (checklistMode) renderTaskControls(rendered, noteId);
167
+ else rendered.innerHTML = renderMiniMarkdown(sourceText, { checklistMode: checklistMode, checklistState: checklistState });
168
+ if (sourceText.trim()) enhanceClayLogLinks(rendered);
169
+ rendered.classList.toggle("is-empty", !checklistMode && !sourceText.trim());
170
+ rendered.contentEditable = checklistMode || getNoteUiState(noteId).pending.length ? "false" : "true";
171
+ if (focusKey !== undefined) {
172
+ var checks = rendered.querySelectorAll("[data-checklist-key]");
173
+ for (var i = 0; i < checks.length; i++) {
174
+ if (checks[i].dataset.checklistKey === focusKey) { checks[i].focus({ preventScroll: true }); break; }
175
+ }
176
+ }
177
+ }
151
178
 
152
179
  function saveRenderedMarkdown() {
153
- var markdown = extractMarkdown(rendered);
154
- textarea.value = markdown;
155
- debouncedTextUpdate(noteId, markdown);
156
- syncTitle(noteEl, markdown);
157
- rendered.classList.toggle("is-empty", !markdown.trim());
180
+ if (checklistMode) return;
181
+ renderedDirty = true;
182
+ sourceText = extractMarkdown(rendered);
183
+ textarea.value = sourceText;
184
+ setNoteUiState(noteId, { text: sourceText, draft: true });
185
+ debouncedTextUpdate(noteId, sourceText);
186
+ syncTitle(noteEl, sourceText);
187
+ rendered.classList.toggle("is-empty", !checklistMode && !sourceText.trim());
158
188
  }
159
189
 
160
190
  function toggleChecklistItem(check) {
161
- var checked = !check.classList.contains("checked");
162
- check.classList.toggle("checked", checked);
163
- check.textContent = checked ? "✓" : "☐";
164
- check.setAttribute("aria-checked", checked ? "true" : "false");
191
+ if (checklistMode && check.dataset.checklistIndex !== undefined) {
192
+ var index = parseInt(check.dataset.checklistIndex, 10);
193
+ var checked = !check.classList.contains("checked");
194
+ mutateChecklist(noteId, { checklistIndex: index, checklistKey: check.dataset.checklistKey || "", checklistSource: sourceText, checklistChecked: checked });
195
+ return;
196
+ }
197
+ var normalChecked = !check.classList.contains("checked");
198
+ check.classList.toggle("checked", normalChecked);
199
+ check.textContent = normalChecked ? "✓" : "☐";
200
+ check.setAttribute("aria-checked", normalChecked ? "true" : "false");
165
201
  saveRenderedMarkdown();
166
202
  }
167
203
 
168
- // MD button: toggle between contenteditable rendered view and raw textarea
204
+ var unsubscribeUiState = subscribeNoteUiState(function (all) {
205
+ var next = all[noteId];
206
+ if (!next) return;
207
+ var changed = next.checklistMode !== checklistMode ||
208
+ (next.text !== undefined && next.text !== sourceText) ||
209
+ next.pending.length !== ui.pending.length ||
210
+ next.checklistInputs !== ui.checklistInputs && !next.checklistInputs ||
211
+ JSON.stringify(next.checklistState || {}) !== JSON.stringify(checklistState || {});
212
+ ui = next;
213
+ checklistMode = next.checklistMode === true;
214
+ checklistState = next.checklistState || {};
215
+ if (next.text !== undefined && next.text !== sourceText) {
216
+ sourceText = next.text;
217
+ textarea.value = sourceText;
218
+ }
219
+ mdBtn.hidden = checklistMode;
220
+ mdBtn.disabled = checklistMode;
221
+ if (checklistMode && mdMode) {
222
+ mdMode = false; textarea.style.display = 'none'; rendered.style.display = ''; mdBtn.classList.remove('active');
223
+ }
224
+ checklistBtn.classList.toggle("active", checklistMode);
225
+ checklistBtn.title = checklistMode ? "Show note" : "Show checklist";
226
+ checklistBtn.setAttribute("aria-label", checklistBtn.title);
227
+ checklistBtn.setAttribute("aria-pressed", checklistMode ? "true" : "false");
228
+ var status = noteEl.querySelector(".sticky-note-save-status");
229
+ if (status) status.textContent = next.error || (next.pending && next.pending.length ? "Saving…" : "");
230
+ if (changed && !mdMode) { renderChecklist(); syncTitle(noteEl, sourceText); }
231
+ });
232
+ noteEl._stickyNoteDispose = function () {
233
+ disposed = true;
234
+ closeFormatToolbar();
235
+ unsubscribeUiState();
236
+ };
237
+
238
+ checklistBtn.addEventListener("click", function (e) {
239
+ e.stopPropagation();
240
+ requestChecklistMode(noteId);
241
+ });
242
+
169
243
  mdBtn.addEventListener("click", function (e) {
170
244
  e.stopPropagation();
245
+ if (checklistMode) return;
171
246
  mdMode = !mdMode;
172
247
  if (mdMode) {
173
- // Switch to raw markdown editing
174
- var md = extractMarkdown(rendered);
175
- textarea.value = md;
248
+ textarea.value = sourceText;
176
249
  textarea.style.display = "";
177
250
  rendered.style.display = "none";
178
251
  mdBtn.classList.add("active");
179
252
  textarea.focus();
180
253
  } else {
181
- // Switch back to contenteditable rendered view
182
- var md = textarea.value;
183
- debouncedTextUpdate(noteId, md);
184
- syncTitle(noteEl, md);
185
- if (md.trim()) {
186
- rendered.innerHTML = renderMiniMarkdown(md);
187
- enhanceClayLogLinks(rendered);
188
- rendered.classList.remove("is-empty");
189
- } else {
190
- rendered.innerHTML = "";
191
- rendered.classList.add("is-empty");
192
- }
254
+ sourceText = textarea.value;
255
+ if (getNoteUiState(noteId).draft) debouncedTextUpdate(noteId, sourceText);
256
+ syncTitle(noteEl, sourceText);
257
+ renderChecklist();
193
258
  textarea.style.display = "none";
194
259
  rendered.style.display = "";
195
260
  mdBtn.classList.remove("active");
@@ -197,11 +262,7 @@ export function setupTextEdit(textarea, rendered, noteId, mdBtn) {
197
262
  }
198
263
  });
199
264
 
200
- // Sync contenteditable changes to textarea (data store) and save
201
- rendered.addEventListener("input", function () {
202
- saveRenderedMarkdown();
203
- });
204
-
265
+ rendered.addEventListener("input", saveRenderedMarkdown);
205
266
  rendered.addEventListener("click", function (e) {
206
267
  var check = e.target.closest && e.target.closest(".sn-check");
207
268
  if (!check || !rendered.contains(check)) return;
@@ -210,27 +271,23 @@ export function setupTextEdit(textarea, rendered, noteId, mdBtn) {
210
271
  toggleChecklistItem(check);
211
272
  });
212
273
 
213
- // On blur, re-render to normalize HTML structure
214
274
  rendered.addEventListener("blur", function (e) {
215
- // Don't re-render if clicking format toolbar (it prevents default, but just in case)
275
+ if (checklistMode || !renderedDirty) return;
276
+ renderedDirty = false;
216
277
  if (e.relatedTarget && e.relatedTarget.closest && e.relatedTarget.closest(".sn-format-toolbar")) return;
217
278
  closeFormatToolbar();
218
279
  var md = extractMarkdown(rendered);
280
+ sourceText = md;
219
281
  textarea.value = md;
220
- if (md.trim()) {
221
- rendered.innerHTML = renderMiniMarkdown(md);
222
- enhanceClayLogLinks(rendered);
223
- rendered.classList.remove("is-empty");
224
- } else {
225
- rendered.innerHTML = "";
226
- rendered.classList.add("is-empty");
227
- }
282
+ setNoteUiState(noteId, { text: sourceText, draft: true });
283
+ renderChecklist();
228
284
  });
229
285
 
230
286
  // Show format toolbar when selecting text
231
287
  rendered.addEventListener("mouseup", function (e) {
232
288
  if (e.target.tagName === "A") return;
233
289
  setTimeout(function () {
290
+ if (disposed) return;
234
291
  var sel = window.getSelection();
235
292
  if (sel && !sel.isCollapsed && sel.toString().trim()) {
236
293
  showFormatToolbar(rendered);
@@ -260,6 +317,7 @@ export function setupTextEdit(textarea, rendered, noteId, mdBtn) {
260
317
  toggleChecklistItem(e.target);
261
318
  return;
262
319
  }
320
+ if (checklistMode) return;
263
321
  if (e.key === "Enter" && !e.shiftKey) {
264
322
  e.preventDefault();
265
323
  document.execCommand("insertLineBreak");
@@ -268,6 +326,7 @@ export function setupTextEdit(textarea, rendered, noteId, mdBtn) {
268
326
 
269
327
  // Paste as plain text to avoid importing HTML formatting
270
328
  rendered.addEventListener("paste", function (e) {
329
+ if (checklistMode) return;
271
330
  e.preventDefault();
272
331
  var text = (e.clipboardData || window.clipboardData).getData("text/plain");
273
332
  document.execCommand("insertText", false, text);
@@ -281,10 +340,15 @@ export function setupTextEdit(textarea, rendered, noteId, mdBtn) {
281
340
  // Sync textarea edits when in MD mode
282
341
  textarea.addEventListener("input", function () {
283
342
  if (!mdMode) return;
284
- debouncedTextUpdate(noteId, textarea.value);
285
- syncTitle(noteEl, textarea.value);
343
+ sourceText = textarea.value;
344
+ setNoteUiState(noteId, { text: sourceText, draft: true });
345
+ debouncedTextUpdate(noteId, sourceText);
346
+ syncTitle(noteEl, sourceText);
286
347
  });
287
348
 
349
+ mdBtn.hidden = checklistMode; mdBtn.disabled = checklistMode;
350
+ renderChecklist();
351
+
288
352
  textarea.addEventListener("mousedown", function (e) {
289
353
  e.stopPropagation();
290
354
  });
@@ -55,8 +55,51 @@ export function clampSize(x, y, w, h) {
55
55
 
56
56
  export function send(obj) {
57
57
  var ws = getWs();
58
- if (!ws || !store.get('connected')) return;
59
- ws.send(JSON.stringify(obj));
58
+ if (!ws || !store.get('connected')) return false;
59
+ try { ws.send(JSON.stringify(obj)); return true; } catch (error) { return false; }
60
+ }
61
+
62
+ export function getNoteUiState(id, fallback) {
63
+ var all = store.get("stickyNoteUiState") || {};
64
+ if (!all[id]) {
65
+ var initial = {
66
+ canonical: Object.assign({}, fallback),
67
+ canonicalRevision: 0,
68
+ draft: false,
69
+ text: fallback && fallback.text ? fallback.text : "",
70
+ checklistMode: fallback && fallback.checklistMode === true,
71
+ checklistState: fallback && fallback.checklistState ? fallback.checklistState : {},
72
+ pending: [],
73
+ };
74
+ var next = Object.assign({}, all);
75
+ next[id] = initial;
76
+ store.set({ stickyNoteUiState: next });
77
+ return initial;
78
+ }
79
+ return all[id];
80
+ }
81
+
82
+ export function setNoteUiState(id, changes) {
83
+ var all = store.get("stickyNoteUiState") || {};
84
+ var current = all[id] || {};
85
+ var next = Object.assign({}, all);
86
+ next[id] = Object.assign({}, current, changes || {});
87
+ store.set({ stickyNoteUiState: next });
88
+ return next[id];
89
+ }
90
+
91
+ export function deleteNoteUiState(id) {
92
+ var all = store.get("stickyNoteUiState") || {};
93
+ if (!all[id]) return;
94
+ var next = Object.assign({}, all);
95
+ delete next[id];
96
+ store.set({ stickyNoteUiState: next });
97
+ }
98
+
99
+ export function subscribeNoteUiState(listener) {
100
+ return store.subscribe(function (next, previous) {
101
+ if (next.stickyNoteUiState !== previous.stickyNoteUiState) listener(next.stickyNoteUiState || {});
102
+ });
60
103
  }
61
104
 
62
105
  export function debouncedUpdate(id, changes, delay) {
@@ -82,6 +125,11 @@ export function syncTitle(noteEl, text) {
82
125
 
83
126
 
84
127
  // Drop any pending writes for a note this client no longer has.
128
+ export function clearNoteTextTimer(id) {
129
+ clearTimeout(textTimers[id]);
130
+ delete textTimers[id];
131
+ }
132
+
85
133
  export function clearNoteTimers(id) {
86
134
  clearTimeout(updateTimers[id]);
87
135
  clearTimeout(textTimers[id]);
@@ -0,0 +1,66 @@
1
+ // Reconcile canonical broadcasts with optimistic checklist actions and text drafts.
2
+ import { store } from './store.js';
3
+ import { getNoteUiState, setNoteUiState, send } from './sticky-notes-shared.js';
4
+
5
+ function project(ui, pending, error) {
6
+ var canonical = ui.canonical || {};
7
+ var text = ui.draft ? ui.text : canonical.text || "";
8
+ var mode = canonical.checklistMode === true;
9
+ var state = Object.assign({}, canonical.checklistState || {});
10
+ pending.forEach(function (request) {
11
+ var changes = request.changes;
12
+ if (changes.text !== undefined) text = changes.text;
13
+ if (changes.checklistMode !== undefined) mode = changes.checklistMode;
14
+ if (changes.checklistKey) state[decodeURIComponent(changes.checklistKey)] = changes.checklistChecked;
15
+ });
16
+ return setNoteUiState(canonical.id, {
17
+ text: text,
18
+ checklistMode: mode, checklistState: state, pending: pending, error: error || '',
19
+ });
20
+ }
21
+
22
+ export function reconcileNote(data) {
23
+ var ui = getNoteUiState(data.id, data);
24
+ var draft = ui.draft && ui.text !== (data.text || '');
25
+ ui = setNoteUiState(data.id, { canonical: data, canonicalRevision: (ui.canonicalRevision || 0) + 1, draft: draft });
26
+ project(ui, ui.pending || [], ui.error);
27
+ }
28
+
29
+ export function mutateChecklist(id, changes) {
30
+ var ui = getNoteUiState(id);
31
+ var requestId = 'note-update-' + Date.now() + '-' + Math.random().toString(36).slice(2);
32
+ var pending = (ui.pending || []).concat([{ requestId: requestId, changes: changes, revision: ui.canonicalRevision, draftText: ui.text }]);
33
+ project(ui, pending, '');
34
+ if (!send(Object.assign({}, changes, { type: changes.humanClose ? 'note_close' : 'note_update', id: id, requestId: requestId }))) {
35
+ project(getNoteUiState(id), pending.slice(0, -1), 'Connection unavailable');
36
+ }
37
+ }
38
+
39
+ export function reconcileNoteResult(msg) {
40
+ var ui = getNoteUiState(msg.noteId);
41
+ var request = (ui.pending || []).find(function (item) { return item.requestId === msg.requestId; });
42
+ if (!request) return;
43
+ // The server broadcasts before acknowledgement. Use the returned note when
44
+ // that broadcast was absent, but never replace a newer remote canonical note.
45
+ if (msg.ok && msg.note && ui.canonicalRevision === request.revision) {
46
+ reconcileNote(msg.note);
47
+ ui = getNoteUiState(msg.noteId);
48
+ }
49
+ if (msg.ok && request.changes.checklistConvert && ui.text === request.draftText) {
50
+ ui = setNoteUiState(msg.noteId, { draft: false, text: ui.canonical.text || '' });
51
+ }
52
+ if (msg.ok && (request.changes.checklistEdit || request.changes.checklistConvert || request.changes.humanClose)) {
53
+ ui = setNoteUiState(msg.noteId, { checklistInputs: null });
54
+ }
55
+ var remaining = ui.pending.filter(function (item) { return item.requestId !== msg.requestId; });
56
+ project(ui, remaining, msg.ok ? '' : msg.error || 'Could not save checklist change');
57
+ return ui.canonical;
58
+ }
59
+
60
+ store.subscribe(function (next, previous) {
61
+ if (next.connected || next.connected === previous.connected) return;
62
+ var all = next.stickyNoteUiState || {};
63
+ Object.keys(all).forEach(function (id) {
64
+ if (all[id].pending && all[id].pending.length) project(all[id], [], 'Connection unavailable');
65
+ });
66
+ });