clay-server 3.8.1 → 4.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/lib/ask-user-mcp-server.js +5 -119
  2. package/lib/background-task-timing.js +105 -0
  3. package/lib/builtin-mates.js +6 -6
  4. package/lib/claude-hook-installer.js +15 -0
  5. package/lib/clay-history-mcp-server.js +62 -297
  6. package/lib/daemon.js +23 -1
  7. package/lib/debate-model-selection.js +119 -0
  8. package/lib/home-debate-tool-policy.js +164 -0
  9. package/lib/home-response-notifications.js +26 -0
  10. package/lib/knowledge-import.js +436 -0
  11. package/lib/knowledge-record-store.js +188 -0
  12. package/lib/knowledge-search.js +224 -0
  13. package/lib/mate-creation-mcp-server.js +55 -0
  14. package/lib/mate-knowledge-mcp-server.js +151 -0
  15. package/lib/mate-knowledge-migration.js +283 -0
  16. package/lib/mate-knowledge-service.js +488 -0
  17. package/lib/mate-knowledge-sync.js +482 -0
  18. package/lib/mate-ready-creation.js +95 -0
  19. package/lib/mates.js +2 -0
  20. package/lib/notes.js +2 -1
  21. package/lib/project-ask-user.js +64 -0
  22. package/lib/project-capsule-catalog.js +101 -0
  23. package/lib/project-connection.js +17 -0
  24. package/lib/project-debate-proposal.js +133 -17
  25. package/lib/project-debate.js +214 -110
  26. package/lib/project-delegated-follow-up.js +84 -0
  27. package/lib/project-delegated-session.js +53 -0
  28. package/lib/project-filesystem.js +7 -9
  29. package/lib/project-knowledge.js +23 -0
  30. package/lib/project-logs-comments.js +137 -0
  31. package/lib/project-logs-mcp-server.js +290 -0
  32. package/lib/project-logs-query.js +167 -0
  33. package/lib/project-logs-root.js +67 -0
  34. package/lib/project-logs-schema.js +259 -0
  35. package/lib/project-logs-service.js +357 -0
  36. package/lib/project-logs-snapshot.js +202 -0
  37. package/lib/project-logs-store.js +498 -0
  38. package/lib/project-logs-versioning.js +128 -0
  39. package/lib/project-logs.js +347 -0
  40. package/lib/project-mate-creation-proposal.js +120 -0
  41. package/lib/project-mate-interaction.js +14 -0
  42. package/lib/project-mate-knowledge.js +110 -0
  43. package/lib/project-memory.js +36 -7
  44. package/lib/project-models.js +157 -32
  45. package/lib/project-session-notes.js +2 -2
  46. package/lib/project-session-pair.js +8 -8
  47. package/lib/project-sessions.js +66 -51
  48. package/lib/project-vendor-login.js +414 -0
  49. package/lib/project-worker-proposal.js +25 -25
  50. package/lib/project-workspace-query.js +144 -0
  51. package/lib/project.js +433 -102
  52. package/lib/public/app.js +109 -48
  53. package/lib/public/css/avatar-imprints.css +0 -1
  54. package/lib/public/css/base.css +4 -0
  55. package/lib/public/css/capsule-ui.css +389 -0
  56. package/lib/public/css/command-palette.css +306 -6
  57. package/lib/public/css/home-capsule-library.css +243 -0
  58. package/lib/public/css/home-capsule-source.css +192 -0
  59. package/lib/public/css/home-debate-live.css +146 -0
  60. package/lib/public/css/home-debate-planning.css +125 -0
  61. package/lib/public/css/home-debates-archive.css +281 -0
  62. package/lib/public/css/home-hub.css +896 -405
  63. package/lib/public/css/home-mate-creation.css +34 -0
  64. package/lib/public/css/home-mate-model-picker.css +131 -0
  65. package/lib/public/css/home-mate-settings.css +344 -0
  66. package/lib/public/css/home-session-actions.css +265 -0
  67. package/lib/public/css/home-sidebar.css +667 -0
  68. package/lib/public/css/icon-strip.css +1 -2
  69. package/lib/public/css/input.css +79 -25
  70. package/lib/public/css/mates.css +7 -34
  71. package/lib/public/css/pane.css +1 -1
  72. package/lib/public/css/playbook.css +0 -80
  73. package/lib/public/css/project-logs.css +234 -0
  74. package/lib/public/css/scheduler.css +1 -1
  75. package/lib/public/css/sticky-notes.css +53 -117
  76. package/lib/public/css/tui-attention.css +0 -44
  77. package/lib/public/css/workspace-assignment.css +45 -0
  78. package/lib/public/fonts/source-serif-4/LICENSE.md +93 -0
  79. package/lib/public/fonts/source-serif-4/SourceSerif4Caption-Semibold.ttf.woff2 +0 -0
  80. package/lib/public/index.html +96 -74
  81. package/lib/public/modules/app-connection.js +100 -5
  82. package/lib/public/modules/app-dm.js +43 -160
  83. package/lib/public/modules/app-home-hub.js +328 -585
  84. package/lib/public/modules/app-message-cards.js +242 -0
  85. package/lib/public/modules/app-message-router.js +84 -0
  86. package/lib/public/modules/app-messages.js +124 -38
  87. package/lib/public/modules/app-notifications.js +8 -119
  88. package/lib/public/modules/app-panels.js +43 -2
  89. package/lib/public/modules/app-projects.js +9 -1
  90. package/lib/public/modules/app-rendering.js +21 -602
  91. package/lib/public/modules/avatar-imprint.js +11 -8
  92. package/lib/public/modules/background-tasks-ui.js +139 -16
  93. package/lib/public/modules/chat-bubble-renderer.js +152 -0
  94. package/lib/public/modules/chat-render-runtime.js +198 -0
  95. package/lib/public/modules/command-palette.js +225 -436
  96. package/lib/public/modules/dm-render.js +82 -0
  97. package/lib/public/modules/home-capsule-creation-intent.js +31 -0
  98. package/lib/public/modules/home-capsule-library.js +146 -0
  99. package/lib/public/modules/home-capsule-source.js +383 -0
  100. package/lib/public/modules/home-chat-identity.js +35 -0
  101. package/lib/public/modules/home-chat-scroll.js +77 -0
  102. package/lib/public/modules/home-chat-stream-state.js +34 -0
  103. package/lib/public/modules/home-composer-focus.js +25 -0
  104. package/lib/public/modules/home-conversations-sheet.js +179 -0
  105. package/lib/public/modules/home-debate-activity.js +48 -0
  106. package/lib/public/modules/home-debate-controls.js +295 -0
  107. package/lib/public/modules/home-debate-launch.js +41 -0
  108. package/lib/public/modules/home-debate-live.js +284 -0
  109. package/lib/public/modules/home-debate-models.js +73 -0
  110. package/lib/public/modules/home-debate-planning.js +335 -0
  111. package/lib/public/modules/home-debates-archive.js +279 -0
  112. package/lib/public/modules/home-dock-resize.js +74 -0
  113. package/lib/public/modules/home-dock.js +446 -0
  114. package/lib/public/modules/home-mate-chat.js +496 -0
  115. package/lib/public/modules/home-mate-creation.js +109 -0
  116. package/lib/public/modules/home-mate-model-picker.js +281 -0
  117. package/lib/public/modules/home-mate-selection.js +38 -0
  118. package/lib/public/modules/home-mate-settings-menu.js +131 -0
  119. package/lib/public/modules/home-mate-settings.js +383 -0
  120. package/lib/public/modules/home-session-actions.js +373 -0
  121. package/lib/public/modules/home-session-model-confirmation.js +13 -0
  122. package/lib/public/modules/home-shell.js +9 -0
  123. package/lib/public/modules/home-sidebar-chat-list.js +63 -0
  124. package/lib/public/modules/home-sidebar.js +165 -0
  125. package/lib/public/modules/home-sub-surface.js +19 -0
  126. package/lib/public/modules/home-surface-boot.js +68 -0
  127. package/lib/public/modules/home-surface.js +141 -0
  128. package/lib/public/modules/home-tools.js +271 -0
  129. package/lib/public/modules/markdown.js +50 -2
  130. package/lib/public/modules/mate-management.js +86 -0
  131. package/lib/public/modules/mate-sidebar.js +0 -8
  132. package/lib/public/modules/mate-wizard.js +0 -7
  133. package/lib/public/modules/mention.js +17 -19
  134. package/lib/public/modules/pane-bridge.js +9 -0
  135. package/lib/public/modules/profile.js +4 -5
  136. package/lib/public/modules/project-logs-ambient.js +238 -0
  137. package/lib/public/modules/project-logs-render.js +359 -0
  138. package/lib/public/modules/project-logs.js +328 -0
  139. package/lib/public/modules/project-settings.js +34 -28
  140. package/lib/public/modules/project-switcher.js +11 -5
  141. package/lib/public/modules/project-workspace-assignment.js +23 -0
  142. package/lib/public/modules/scheduler.js +17 -3
  143. package/lib/public/modules/search-clay-chat.js +402 -0
  144. package/lib/public/modules/server-settings.js +14 -12
  145. package/lib/public/modules/session-actions.js +1 -1
  146. package/lib/public/modules/sidebar-mates.js +56 -552
  147. package/lib/public/modules/sidebar-mobile.js +12 -73
  148. package/lib/public/modules/sidebar-presence.js +37 -0
  149. package/lib/public/modules/sidebar-sessions.js +25 -1
  150. package/lib/public/modules/split-pair-ui.js +13 -13
  151. package/lib/public/modules/split-view.js +7 -0
  152. package/lib/public/modules/sticky-notes.js +106 -53
  153. package/lib/public/modules/tool-input-composition.js +48 -0
  154. package/lib/public/modules/tool-llm-status.js +167 -0
  155. package/lib/public/modules/tool-palette.js +76 -4
  156. package/lib/public/modules/tool-renderer-advanced.js +285 -0
  157. package/lib/public/modules/tool-renderer-chart.js +158 -0
  158. package/lib/public/modules/tool-renderer-semantics.js +35 -0
  159. package/lib/public/modules/tool-renderer.js +464 -0
  160. package/lib/public/modules/tool-runtime.js +309 -0
  161. package/lib/public/modules/tool-ui-evaluator.js +97 -0
  162. package/lib/public/modules/tool-ui-tree.js +21 -0
  163. package/lib/public/modules/tui-attention.js +10 -0
  164. package/lib/public/modules/user-settings.js +16 -23
  165. package/lib/public/modules/vendor-login.js +287 -0
  166. package/lib/public/modules/worker-proposal.js +2 -2
  167. package/lib/public/modules/workspace-assignment-card.js +149 -0
  168. package/lib/public/modules/workspace-assignment-routing.js +4 -0
  169. package/lib/public/style.css +14 -2
  170. package/lib/runtime-env.js +65 -0
  171. package/lib/sdk-bridge.js +120 -32
  172. package/lib/sdk-message-processor.js +34 -13
  173. package/lib/server-home-capsule-creation.js +36 -0
  174. package/lib/server-home-chat-events.js +287 -0
  175. package/lib/server-home-chat.js +518 -0
  176. package/lib/server-home-clay-entry.js +65 -0
  177. package/lib/server-home-clay-session-links.js +26 -0
  178. package/lib/server-home-debate-planning.js +214 -0
  179. package/lib/server-home-debates.js +137 -0
  180. package/lib/server-home-mate-creation.js +261 -0
  181. package/lib/server-home-models.js +241 -0
  182. package/lib/server-home-preferences.js +61 -0
  183. package/lib/server-mates.js +37 -0
  184. package/lib/server-tools.js +377 -0
  185. package/lib/server.js +166 -59
  186. package/lib/session-pair-mcp-server.js +6 -6
  187. package/lib/sessions.js +22 -4
  188. package/lib/tool-capsule-source.js +142 -0
  189. package/lib/tool-control-mcp-server.js +126 -0
  190. package/lib/tool-llm.js +48 -0
  191. package/lib/tool-storage.js +77 -0
  192. package/lib/tool-ui-spec-advanced.js +82 -0
  193. package/lib/tool-ui-spec.js +261 -0
  194. package/lib/tools-registry.js +286 -0
  195. package/lib/users-home-dock-preferences.js +77 -0
  196. package/lib/users-home-surface-preferences.js +99 -0
  197. package/lib/users-preferences.js +4 -7
  198. package/lib/users.js +12 -0
  199. package/lib/whats-new-content.js +3 -3
  200. package/lib/workspace-assignment-service.js +420 -0
  201. package/lib/workspace-query-mcp-server.js +154 -0
  202. package/lib/workspace-query-service.js +492 -0
  203. package/lib/ws-schema.js +107 -12
  204. package/lib/yoke/adapters/acp.js +2 -1
  205. package/lib/yoke/adapters/antigravity.js +2 -1
  206. package/lib/yoke/adapters/claude-worker.js +11 -4
  207. package/lib/yoke/adapters/claude.js +141 -46
  208. package/lib/yoke/adapters/codex.js +79 -11
  209. package/lib/yoke/adapters/kiro.js +2 -1
  210. package/lib/yoke/claude-user-input.js +38 -0
  211. package/lib/yoke/codex-app-server.js +20 -2
  212. package/lib/yoke/codex-background-tasks.js +8 -2
  213. package/lib/yoke/codex-user-input.js +64 -0
  214. package/lib/yoke/complete-once.js +164 -0
  215. package/lib/yoke/index.js +5 -1
  216. package/lib/yoke/interface.js +19 -0
  217. package/lib/yoke/mcp-bridge-server.js +2 -6
  218. package/lib/yoke/user-input.js +254 -0
  219. package/package.json +4 -3
  220. package/lib/public/css/home-chat.css +0 -380
  221. package/lib/public/modules/home-chat.js +0 -438
  222. package/lib/server-clay-home.js +0 -245
@@ -0,0 +1,167 @@
1
+ // Compact provider/model status for LLM-enabled Capsule surfaces.
2
+
3
+ import { getWs } from './ws-ref.js';
4
+
5
+ var surfaces = [];
6
+ var requestCounter = 0;
7
+ var activeRequests = Object.create(null);
8
+ var states = Object.create(null);
9
+ var subscribers = [];
10
+
11
+ export var TOOL_LLM_ALIASES = Object.freeze(["fast", "standard", "deep"]);
12
+
13
+ function safeAlias(alias) {
14
+ return alias === "fast" || alias === "deep" ? alias : "standard";
15
+ }
16
+
17
+ function isAlias(alias) {
18
+ return alias === "fast" || alias === "standard" || alias === "deep";
19
+ }
20
+
21
+ export function initialToolLlmAlias(manifest) {
22
+ return manifest && isAlias(manifest.modelAlias) ? manifest.modelAlias : null;
23
+ }
24
+
25
+ export function toolLlmDisplayValue(modelState) {
26
+ var ready = modelState && modelState.status === "ready" && modelState.vendorName && modelState.modelName;
27
+ return ready ? modelState.vendorName + " · " + modelState.modelName : "No model configured";
28
+ }
29
+
30
+ export function toolLlmShouldRetry(modelState) {
31
+ return !!modelState && modelState.status === "error";
32
+ }
33
+
34
+ function currentSurfaces() {
35
+ surfaces = surfaces.filter(function (surface) { return !!surface.root; });
36
+ return surfaces;
37
+ }
38
+
39
+ function notifyAlias(alias) {
40
+ var current = subscribers.slice();
41
+ for (var i = 0; i < current.length; i++) current[i](alias, getToolLlmConfiguration(alias));
42
+ }
43
+
44
+ export function getToolLlmConfiguration(alias) {
45
+ alias = safeAlias(alias);
46
+ return Object.assign({ status: "loading", vendorName: "", modelName: "", error: "" }, states[alias] || {});
47
+ }
48
+
49
+ export function subscribeToolLlmConfigurations(listener) {
50
+ subscribers.push(listener);
51
+ return function () {
52
+ var index = subscribers.indexOf(listener);
53
+ if (index !== -1) subscribers.splice(index, 1);
54
+ };
55
+ }
56
+
57
+ function renderSurface(surface) {
58
+ if (!surface.alias) {
59
+ surface.value.textContent = "Selected when Capsule runs";
60
+ surface.value.title = "The concrete model is selected when this Capsule requests an LLM capability.";
61
+ surface.error.classList.add("hidden");
62
+ surface.retry.classList.add("hidden");
63
+ return;
64
+ }
65
+ var state = states[surface.alias] || { status: "loading", vendorName: "", modelName: "", error: "" };
66
+ var ready = state.status === "ready" && state.vendorName && state.modelName;
67
+ surface.value.textContent = toolLlmDisplayValue(state);
68
+ surface.value.title = ready ? "Capsule " + surface.alias + " model: " + state.vendorName + " · " + state.modelName : "No configured Capsule model";
69
+ surface.error.textContent = state.error || "";
70
+ surface.error.classList.toggle("hidden", !state.error);
71
+ surface.retry.classList.toggle("hidden", !toolLlmShouldRetry(state));
72
+ surface.retry.disabled = false;
73
+ surface.retry.textContent = "Retry";
74
+ }
75
+
76
+ function renderAlias(alias) {
77
+ var current = currentSurfaces();
78
+ for (var i = 0; i < current.length; i++) {
79
+ if (current[i].alias === alias) renderSurface(current[i]);
80
+ }
81
+ }
82
+
83
+ export function requestToolLlmConfiguration(alias) {
84
+ alias = safeAlias(alias);
85
+ var ws = getWs();
86
+ if (!ws || ws.readyState !== 1) {
87
+ activeRequests[alias] = null;
88
+ states[alias] = { status: "error", vendorName: "", modelName: "", error: "Connect to Clay to check the Capsule model." };
89
+ renderAlias(alias);
90
+ notifyAlias(alias);
91
+ return;
92
+ }
93
+ requestCounter++;
94
+ activeRequests[alias] = "tool-llm-config-" + requestCounter;
95
+ states[alias] = { status: "loading", vendorName: "", modelName: "", error: "" };
96
+ renderAlias(alias);
97
+ notifyAlias(alias);
98
+ ws.send(JSON.stringify({ type: "tool_llm_config_get", requestId: activeRequests[alias], alias: alias }));
99
+ }
100
+
101
+ export function requestAllToolLlmConfigurations() {
102
+ for (var i = 0; i < TOOL_LLM_ALIASES.length; i++) requestToolLlmConfiguration(TOOL_LLM_ALIASES[i]);
103
+ }
104
+
105
+ export function handleToolLlmConfigState(msg) {
106
+ if (!isAlias(msg.alias)) return;
107
+ var alias = msg.alias;
108
+ if (msg.requestId && msg.requestId !== activeRequests[alias]) return;
109
+ activeRequests[alias] = null;
110
+ states[alias] = {
111
+ status: msg.status === "ready" ? "ready" : "error",
112
+ vendorName: msg.vendorName || msg.vendor || "",
113
+ modelName: msg.modelName || msg.model || "",
114
+ error: msg.error || "",
115
+ };
116
+ renderAlias(alias);
117
+ notifyAlias(alias);
118
+ }
119
+
120
+ export function createToolLlmStatus(alias) {
121
+ alias = isAlias(alias) ? alias : null;
122
+ var root = document.createElement("section");
123
+ root.className = "tool-llm-status";
124
+ root.setAttribute("aria-label", "Capsule model status");
125
+ var identity = document.createElement("div");
126
+ identity.className = "tool-llm-identity";
127
+ var label = document.createElement("span");
128
+ label.className = "tool-llm-label";
129
+ label.textContent = "Model";
130
+ var value = document.createElement("span");
131
+ value.className = "tool-llm-value";
132
+ value.setAttribute("aria-live", "polite");
133
+ identity.appendChild(label);
134
+ identity.appendChild(value);
135
+ var actions = document.createElement("div");
136
+ actions.className = "tool-llm-actions";
137
+ var retry = document.createElement("button");
138
+ retry.type = "button";
139
+ retry.className = "tool-llm-action hidden";
140
+ retry.addEventListener("click", function () {
141
+ if (surface.alias) requestToolLlmConfiguration(surface.alias);
142
+ });
143
+ var error = document.createElement("p");
144
+ error.className = "tool-llm-error hidden";
145
+ actions.appendChild(retry);
146
+ root.appendChild(identity);
147
+ root.appendChild(actions);
148
+ root.appendChild(error);
149
+ var surface = { root: root, alias: alias, value: value, retry: retry, error: error };
150
+ surfaces.push(surface);
151
+ renderSurface(surface);
152
+ if (alias) requestToolLlmConfiguration(alias);
153
+ return {
154
+ element: root,
155
+ setAlias: function (nextAlias) {
156
+ if (!isAlias(nextAlias)) return;
157
+ surface.alias = nextAlias;
158
+ renderSurface(surface);
159
+ requestToolLlmConfiguration(nextAlias);
160
+ },
161
+ dispose: function () {
162
+ surface.root = null;
163
+ var index = surfaces.indexOf(surface);
164
+ if (index !== -1) surfaces.splice(index, 1);
165
+ },
166
+ };
167
+ }
@@ -23,7 +23,7 @@ var SESSION_TOOLS = [
23
23
  { id: "file-browser-btn", icon: "folder-tree", label: "File browser" },
24
24
  { id: "terminal-sidebar-btn", icon: "square-terminal", label: "Terminal", countId: "terminal-sidebar-count" },
25
25
  { id: "sticky-notes-sidebar-btn", icon: "sticky-note", label: "Sticky Notes", countId: "sticky-notes-sidebar-count" },
26
- { id: "scheduler-btn", icon: "calendar-clock", label: "Scheduled Tasks" },
26
+ { id: "project-logs-btn", icon: "notebook-tabs", label: "Logs", countId: "project-logs-count" },
27
27
  { id: "loop-tool-btn", icon: "repeat", label: "Loop" },
28
28
  { id: "git-sidebar-btn", icon: "git-branch", label: "Git", countId: "git-sidebar-count" },
29
29
  { id: "mcp-btn", icon: "cable", label: "MCP Servers", countId: "mcp-sidebar-count" },
@@ -34,7 +34,6 @@ var MATE_TOOLS = [
34
34
  { id: "mate-memory-btn", icon: "brain", label: "Memory", countId: "mate-memory-count" },
35
35
  { id: "mate-knowledge-btn", icon: "book-open", label: "Knowledge", countId: "mate-knowledge-count" },
36
36
  { id: "mate-sticky-notes-btn", icon: "sticky-note", label: "Sticky Notes" },
37
- { id: "mate-scheduler-btn", icon: "calendar-clock", label: "Scheduled Tasks" },
38
37
  { id: "mate-debate-btn", icon: "mic", label: "Debate" },
39
38
  { id: "mate-mcp-btn", icon: "cable", label: "MCP Servers", countId: "mate-mcp-sidebar-count" },
40
39
  { id: "mate-skills-btn", icon: "puzzle", label: "Skills" },
@@ -53,10 +52,75 @@ var PALETTES = {
53
52
  },
54
53
  };
55
54
 
55
+ // Tools that were replaced in the session palette. A saved preference naming
56
+ // the old id would otherwise silently drop out (its element no longer exists)
57
+ // and the replacement would append to the end of the palette instead of
58
+ // inheriting the slot the user had chosen.
59
+ var LEGACY_SESSION_TOOL_IDS = {
60
+ "scheduler-btn": "project-logs-btn",
61
+ };
62
+
56
63
  var _saveTimers = {};
57
64
  var _draggingEl = null;
58
65
  var _draggingPaletteName = null;
59
66
 
67
+ function indexOfId(list, id) {
68
+ for (var i = 0; i < list.length; i++) {
69
+ if (list[i] === id) return i;
70
+ }
71
+ return -1;
72
+ }
73
+
74
+ // Rewrite a saved preference so a replaced tool keeps its slot.
75
+ //
76
+ // If the user already has an explicit preference for the replacement, that
77
+ // choice wins and the legacy id is simply dropped: their position, and whether
78
+ // they hid it, are never overridden. Otherwise the legacy id is replaced in
79
+ // place, at the exact index it occupied, in whichever list it appeared in.
80
+ //
81
+ // Exported for tests; returns new arrays and never mutates the input.
82
+ export function normalizeToolPreferences(name, prefs) {
83
+ var order = (prefs && prefs.order) || [];
84
+ var hidden = (prefs && prefs.hidden) || [];
85
+ var result = { order: order.slice(), hidden: hidden.slice(), migrated: false };
86
+ if (name !== "session") return result;
87
+
88
+ var legacyIds = Object.keys(LEGACY_SESSION_TOOL_IDS);
89
+ for (var i = 0; i < legacyIds.length; i++) {
90
+ var legacyId = legacyIds[i];
91
+ var replacementId = LEGACY_SESSION_TOOL_IDS[legacyId];
92
+ var inOrder = indexOfId(result.order, legacyId) !== -1;
93
+ var inHidden = indexOfId(result.hidden, legacyId) !== -1;
94
+ if (!inOrder && !inHidden) continue;
95
+
96
+ var explicit = indexOfId(result.order, replacementId) !== -1
97
+ || indexOfId(result.hidden, replacementId) !== -1;
98
+ result.order = rewriteList(result.order, legacyId, replacementId, explicit);
99
+ result.hidden = rewriteList(result.hidden, legacyId, replacementId, explicit);
100
+ result.migrated = true;
101
+ }
102
+ return result;
103
+ }
104
+
105
+ // Drop the legacy id when the replacement is already chosen explicitly;
106
+ // otherwise swap it in place. Any duplicate of the replacement introduced by
107
+ // the swap is removed, keeping the first occurrence.
108
+ function rewriteList(list, legacyId, replacementId, dropLegacy) {
109
+ var out = [];
110
+ var seen = {};
111
+ for (var i = 0; i < list.length; i++) {
112
+ var id = list[i];
113
+ if (id === legacyId) {
114
+ if (dropLegacy) continue;
115
+ id = replacementId;
116
+ }
117
+ if (seen[id]) continue;
118
+ seen[id] = true;
119
+ out.push(id);
120
+ }
121
+ return out;
122
+ }
123
+
60
124
  export function initToolPalettes() {
61
125
  for (var name in PALETTES) {
62
126
  buildPalette(name);
@@ -495,11 +559,13 @@ function applyPreferences(name, prefs) {
495
559
  var hiddenGrid = hiddenSection ? hiddenSection.querySelector('.tool-palette-hidden-grid') : null;
496
560
  if (!active || !hiddenGrid) return;
497
561
 
562
+ var normalized = normalizeToolPreferences(name, prefs);
563
+
498
564
  var hiddenSet = {};
499
- var hiddenList = prefs.hidden || [];
565
+ var hiddenList = normalized.hidden;
500
566
  for (var i = 0; i < hiddenList.length; i++) hiddenSet[hiddenList[i]] = true;
501
567
 
502
- var orderList = prefs.order || [];
568
+ var orderList = normalized.order;
503
569
  // Any tools not in the stored order are appended in registry order so
504
570
  // newly-added tools surface automatically for existing users.
505
571
  var placed = {};
@@ -525,6 +591,12 @@ function applyPreferences(name, prefs) {
525
591
  hiddenGrid.appendChild(hbtn);
526
592
  }
527
593
  }
594
+
595
+ // Write the rewritten preference back once, through the normal save path, so
596
+ // the migration survives reloads and reaches the user's other devices. The
597
+ // save reads the DOM this function just arranged, so it records exactly what
598
+ // is on screen.
599
+ if (normalized.migrated) queueSave(name);
528
600
  }
529
601
 
530
602
  function queueSave(name) {
@@ -0,0 +1,285 @@
1
+ // Host-owned advanced controls for the safe Capsule UI vocabulary.
2
+
3
+ import { collectionView, normalizeOptions, resolveProps, resolveValue, valueAtPath } from './tool-ui-evaluator.js';
4
+ import { renderChartNode } from './tool-renderer-chart.js';
5
+
6
+ var dialogOpeners = new WeakMap();
7
+
8
+ function nextGroupToken(context, prefix, authoredId) {
9
+ if (authoredId) return authoredId;
10
+ if (!Number.isInteger(context.autoControlIndex) || context.autoControlIndex < 1) context.autoControlIndex = 1;
11
+ return prefix + "-" + context.autoControlIndex++;
12
+ }
13
+
14
+ function emit(node, props, context, extra) {
15
+ var args = Object.assign({}, resolveValue(props.args || {}, context.state, context.item), extra || {});
16
+ if (node.id) args.controlId = node.id;
17
+ context.emit(node.action, args);
18
+ }
19
+
20
+ function appendRenderedChildren(target, children, context, renderNode) {
21
+ for (var i = 0; i < children.length; i++) {
22
+ var rendered = renderNode(children[i], context);
23
+ if (rendered) target.appendChild(rendered);
24
+ }
25
+ }
26
+
27
+ function renderForm(node, props, context, renderNode) {
28
+ var form = document.createElement("form");
29
+ form.className = "tool-form";
30
+ if (props.label) form.setAttribute("aria-label", String(props.label));
31
+ appendRenderedChildren(form, node.children || [], context, renderNode);
32
+ var error = document.createElement("div");
33
+ error.className = "tool-form-error hidden";
34
+ error.setAttribute("role", "alert");
35
+ form.appendChild(error);
36
+ form.addEventListener("submit", function (event) {
37
+ event.preventDefault();
38
+ var invalid = form.querySelector(":invalid");
39
+ if (invalid) {
40
+ error.textContent = invalid.validationMessage || "Check the highlighted fields.";
41
+ error.classList.remove("hidden");
42
+ invalid.focus();
43
+ return;
44
+ }
45
+ error.classList.add("hidden");
46
+ emit(node, props, context, {});
47
+ });
48
+ var submit = document.createElement("button");
49
+ submit.type = "submit";
50
+ submit.className = "tool-button tool-button--variant-primary";
51
+ submit.dataset.toolControlId = node.id || "form-submit-" + context.autoControlIndex++;
52
+ submit.textContent = String(props.submitLabel || "Submit");
53
+ form.appendChild(submit);
54
+ return form;
55
+ }
56
+
57
+ function renderTabs(node, props, context, renderNode) {
58
+ var wrapper = document.createElement("div");
59
+ wrapper.className = "tool-tabs";
60
+ var list = document.createElement("div");
61
+ list.className = "tool-tab-list";
62
+ list.setAttribute("role", "tablist");
63
+ list.setAttribute("aria-label", String(props.label || "Sections"));
64
+ var panels = document.createElement("div");
65
+ var selected = valueAtPath(context.state, node.bind, context.item);
66
+ var groupToken = nextGroupToken(context, "tabs", node.id);
67
+ var children = node.children || [];
68
+ var dynamic = props.options ? normalizeOptions(props.options) : null;
69
+ var tabs = dynamic ? dynamic.map(function (option) { return { props: option, children: [] }; }) : children;
70
+ var buttons = [];
71
+ for (var i = 0; i < tabs.length; i++) {
72
+ var tab = tabs[i];
73
+ var tabProps = resolveProps(tab.props || {}, context.state, context.item);
74
+ var active = selected === tabProps.value || (selected === undefined && i === 0);
75
+ var button = document.createElement("button");
76
+ button.type = "button";
77
+ button.className = "tool-tab";
78
+ button.textContent = String(tabProps.label);
79
+ button.setAttribute("role", "tab");
80
+ button.setAttribute("aria-selected", active ? "true" : "false");
81
+ button.tabIndex = active ? 0 : -1;
82
+ button.disabled = tabProps.disabled === true;
83
+ var panelId = "tool-tab-panel-" + context.toolId + "-" + groupToken + "-" + i;
84
+ var tabId = "tool-tab-" + context.toolId + "-" + groupToken + "-" + i;
85
+ button.id = tabId;
86
+ button.setAttribute("aria-controls", panelId);
87
+ button.dataset.tabIndex = String(i);
88
+ button.addEventListener("click", function (event) {
89
+ var index = Number(event.currentTarget.dataset.tabIndex);
90
+ emit(node, props, context, { value: resolveProps(tabs[index].props || {}, context.state, context.item).value });
91
+ });
92
+ buttons.push(button);
93
+ list.appendChild(button);
94
+ if (active) {
95
+ var panel = document.createElement("div");
96
+ panel.id = panelId;
97
+ panel.className = "tool-tab-panel";
98
+ panel.setAttribute("role", "tabpanel");
99
+ panel.setAttribute("aria-labelledby", tabId);
100
+ appendRenderedChildren(panel, tab.children || [], context, renderNode);
101
+ panels.appendChild(panel);
102
+ }
103
+ }
104
+ list.addEventListener("keydown", function (event) {
105
+ if (["ArrowLeft", "ArrowRight", "Home", "End"].indexOf(event.key) === -1) return;
106
+ event.preventDefault();
107
+ var enabled = buttons.filter(function (button) { return !button.disabled; });
108
+ if (!enabled.length) return;
109
+ var index = enabled.indexOf(document.activeElement);
110
+ var next = event.key === "Home" ? 0 : event.key === "End" ? enabled.length - 1 : (index + (event.key === "ArrowRight" ? 1 : -1) + enabled.length) % enabled.length;
111
+ enabled[next].focus();
112
+ });
113
+ wrapper.appendChild(list);
114
+ wrapper.appendChild(panels);
115
+ return wrapper;
116
+ }
117
+
118
+ function renderDialog(node, props, context, renderNode) {
119
+ var placeholder = document.createElement("span");
120
+ var dialogToken = nextGroupToken(context, "dialog", node.id);
121
+ if (props.open !== true) {
122
+ var openerId = dialogOpeners.get(context.container);
123
+ dialogOpeners.delete(context.container);
124
+ var returnTimer = openerId ? setTimeout(function () {
125
+ var controls = context.container.querySelectorAll("[data-tool-control-id]");
126
+ for (var i = 0; i < controls.length; i++) if (controls[i].dataset.toolControlId === openerId) controls[i].focus({ preventScroll: true });
127
+ }, 0) : null;
128
+ if (returnTimer) context.disposers.push(function () { clearTimeout(returnTimer); });
129
+ return placeholder;
130
+ }
131
+ if (context.activeControlId) dialogOpeners.set(context.container, context.activeControlId);
132
+ var overlay = document.createElement("div");
133
+ overlay.className = "tool-dialog-overlay";
134
+ var dialog = document.createElement("section");
135
+ dialog.className = "tool-dialog";
136
+ dialog.setAttribute("role", "dialog");
137
+ dialog.setAttribute("aria-modal", "true");
138
+ var title = document.createElement("h3");
139
+ var titleId = "tool-dialog-title-" + context.toolId + "-" + dialogToken;
140
+ title.id = titleId;
141
+ title.className = "tool-dialog-title";
142
+ title.textContent = String(props.label);
143
+ dialog.appendChild(title);
144
+ dialog.setAttribute("aria-labelledby", titleId);
145
+ if (props.description) {
146
+ var description = document.createElement("p");
147
+ var descriptionId = "tool-dialog-description-" + context.toolId + "-" + dialogToken;
148
+ description.id = descriptionId;
149
+ description.className = "tool-dialog-description";
150
+ description.textContent = String(props.description);
151
+ dialog.appendChild(description);
152
+ dialog.setAttribute("aria-describedby", descriptionId);
153
+ }
154
+ var close = document.createElement("button");
155
+ close.type = "button";
156
+ close.className = "tool-dialog-close";
157
+ close.textContent = "Close";
158
+ close.setAttribute("aria-label", "Close " + String(props.label));
159
+ close.addEventListener("click", function () { context.emit(props.closeAction, {}); });
160
+ dialog.appendChild(close);
161
+ appendRenderedChildren(dialog, node.children || [], context, renderNode);
162
+ overlay.appendChild(dialog);
163
+ overlay.addEventListener("mousedown", function (event) { if (event.target === overlay) context.emit(props.closeAction, {}); });
164
+ overlay.addEventListener("keydown", function (event) {
165
+ if (event.key === "Escape") { event.preventDefault(); context.emit(props.closeAction, {}); return; }
166
+ if (event.key !== "Tab") return;
167
+ var focusable = dialog.querySelectorAll("button:not(:disabled),input:not(:disabled),select:not(:disabled),textarea:not(:disabled),[tabindex]:not([tabindex='-1'])");
168
+ if (!focusable.length) return;
169
+ var first = focusable[0];
170
+ var last = focusable[focusable.length - 1];
171
+ if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
172
+ else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
173
+ });
174
+ var focusTimer = setTimeout(function () { if (context.container.contains(overlay)) close.focus(); }, 0);
175
+ context.disposers.push(function () { clearTimeout(focusTimer); });
176
+ return overlay;
177
+ }
178
+
179
+ function renderMenu(node, props, context) {
180
+ var wrapper = document.createElement("div");
181
+ wrapper.className = "tool-menu";
182
+ var trigger = document.createElement("button");
183
+ trigger.type = "button";
184
+ trigger.className = "tool-button tool-button--variant-ghost";
185
+ trigger.dataset.toolControlId = node.id || "menu-trigger-" + context.autoControlIndex++;
186
+ trigger.textContent = String(props.triggerLabel || props.label);
187
+ trigger.setAttribute("aria-haspopup", "menu");
188
+ trigger.setAttribute("aria-expanded", "false");
189
+ var menu = document.createElement("div");
190
+ menu.className = "tool-menu-popup hidden";
191
+ menu.setAttribute("role", "menu");
192
+ menu.setAttribute("aria-label", String(props.label));
193
+ var dynamic = props.options ? normalizeOptions(props.options) : null;
194
+ var items = dynamic ? dynamic.map(function (option) { return { action: node.action, props: { label: option.label, disabled: option.disabled, args: { value: option.value } } }; }) : node.children || [];
195
+ for (var i = 0; i < items.length; i++) {
196
+ var item = document.createElement("button");
197
+ item.type = "button";
198
+ item.className = "tool-menu-item";
199
+ item.setAttribute("role", "menuitem");
200
+ var itemProps = resolveProps(items[i].props || {}, context.state, context.item);
201
+ item.textContent = String(itemProps.label);
202
+ item.disabled = itemProps.disabled === true;
203
+ item.dataset.menuIndex = String(i);
204
+ item.addEventListener("click", function (event) {
205
+ var selected = items[Number(event.currentTarget.dataset.menuIndex)];
206
+ closeMenu(true);
207
+ emit(selected, resolveProps(selected.props || {}, context.state, context.item), context, {});
208
+ });
209
+ menu.appendChild(item);
210
+ }
211
+ function closeMenu(focus) { menu.classList.add("hidden"); trigger.setAttribute("aria-expanded", "false"); if (focus) trigger.focus(); }
212
+ function openMenu() { menu.classList.remove("hidden"); trigger.setAttribute("aria-expanded", "true"); var first = menu.querySelector("[role='menuitem']:not(:disabled)"); if (first) first.focus(); }
213
+ trigger.addEventListener("click", function () { if (menu.classList.contains("hidden")) openMenu(); else closeMenu(false); });
214
+ trigger.addEventListener("keydown", function (event) { if (event.key === "ArrowDown") { event.preventDefault(); openMenu(); } });
215
+ menu.addEventListener("keydown", function (event) {
216
+ var controls = Array.prototype.slice.call(menu.querySelectorAll("[role='menuitem']:not(:disabled)"));
217
+ if (event.key === "Escape") { event.preventDefault(); closeMenu(true); return; }
218
+ if (["ArrowDown", "ArrowUp", "Home", "End"].indexOf(event.key) === -1) return;
219
+ event.preventDefault();
220
+ if (!controls.length) return;
221
+ var current = controls.indexOf(document.activeElement);
222
+ var next = event.key === "Home" ? 0 : event.key === "End" ? controls.length - 1 : (current + (event.key === "ArrowDown" ? 1 : -1) + controls.length) % controls.length;
223
+ controls[next].focus();
224
+ });
225
+ var outside = function (event) { if (!wrapper.contains(event.target)) closeMenu(false); };
226
+ document.addEventListener("mousedown", outside);
227
+ context.disposers.push(function () { document.removeEventListener("mousedown", outside); });
228
+ wrapper.appendChild(trigger);
229
+ wrapper.appendChild(menu);
230
+ return wrapper;
231
+ }
232
+
233
+ function renderPagination(node, props, context) {
234
+ var nav = document.createElement("nav");
235
+ nav.className = "tool-pagination";
236
+ nav.setAttribute("aria-label", String(props.label || "Pagination"));
237
+ var page = Math.max(1, Number(valueAtPath(context.state, node.bind, context.item)) || 1);
238
+ var total = Math.max(0, Number(props.total) || 0);
239
+ var pageSize = Math.max(1, Math.min(100, Number(props.pageSize) || 20));
240
+ var pages = Math.max(1, Math.ceil(total / pageSize));
241
+ function appendPageButton(target, label, current) {
242
+ var button = document.createElement("button");
243
+ button.type = "button";
244
+ button.className = "tool-button tool-button--variant-ghost";
245
+ button.textContent = label;
246
+ button.disabled = target < 1 || target > pages;
247
+ if (current) button.setAttribute("aria-current", "page");
248
+ button.addEventListener("click", function () { context.emit(props.pageAction, { value: target, pageSize: pageSize }); });
249
+ nav.appendChild(button);
250
+ }
251
+ appendPageButton(page - 1, "Previous", false);
252
+ var first = Math.max(1, Math.min(pages - 4, page - 2));
253
+ var last = Math.min(pages, first + 4);
254
+ for (var pi = first; pi <= last; pi++) appendPageButton(pi, String(pi), pi === page);
255
+ var status = document.createElement("span");
256
+ status.textContent = "Page " + Math.min(page, pages) + " of " + pages;
257
+ status.setAttribute("aria-live", "polite");
258
+ nav.appendChild(status);
259
+ appendPageButton(page + 1, "Next", false);
260
+ if (Array.isArray(props.pageSizes) && props.pageSizeAction) {
261
+ var size = document.createElement("select");
262
+ size.className = "tool-select tool-pagination-size";
263
+ size.setAttribute("aria-label", "Items per page");
264
+ props.pageSizes.slice(0, 10).forEach(function (value) {
265
+ var option = document.createElement("option");
266
+ option.value = String(value);
267
+ option.textContent = String(value) + " per page";
268
+ option.selected = value === pageSize;
269
+ size.appendChild(option);
270
+ });
271
+ size.addEventListener("change", function () { context.emit(props.pageSizeAction, { value: Number(size.value) }); });
272
+ nav.appendChild(size);
273
+ }
274
+ return nav;
275
+ }
276
+
277
+ export function renderAdvancedNode(node, props, context, renderNode) {
278
+ if (node.type === "form") return renderForm(node, props, context, renderNode);
279
+ if (node.type === "tabs") return renderTabs(node, props, context, renderNode);
280
+ if (node.type === "dialog") return renderDialog(node, props, context, renderNode);
281
+ if (node.type === "menu") return renderMenu(node, props, context);
282
+ if (node.type === "pagination") return renderPagination(node, props, context);
283
+ if (node.type === "chart") return renderChartNode(node, props, context);
284
+ return null;
285
+ }