clay-server 4.0.0-beta.8 → 4.0.0

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 (185) hide show
  1. package/lib/capsule-display-floor.js +51 -0
  2. package/lib/capsule-frame-server.js +255 -0
  3. package/lib/capsule-mate-turn.js +105 -0
  4. package/lib/capsule-pig-logic.js +300 -0
  5. package/lib/capsule-server-runtimes.js +39 -0
  6. package/lib/capsules/pig/display.js +198 -0
  7. package/lib/capsules/pig/manifest.json +9 -0
  8. package/lib/capsules/pig/ui.json +84 -0
  9. package/lib/config.js +25 -1
  10. package/lib/daemon-projects.js +13 -8
  11. package/lib/daemon.js +127 -14
  12. package/lib/git-cli.js +95 -1
  13. package/lib/knowledge-import.js +436 -0
  14. package/lib/knowledge-record-store.js +188 -0
  15. package/lib/knowledge-search.js +240 -0
  16. package/lib/mate-knowledge-mcp-server.js +151 -0
  17. package/lib/mate-knowledge-migration.js +283 -0
  18. package/lib/mate-knowledge-service.js +488 -0
  19. package/lib/mate-knowledge-sync.js +482 -0
  20. package/lib/migrate-single-user.js +2 -0
  21. package/lib/notes-lifecycle.js +149 -0
  22. package/lib/notes.js +60 -3
  23. package/lib/os-users.js +3 -1
  24. package/lib/project-capsule-catalog.js +9 -1
  25. package/lib/project-capsule-turn.js +96 -0
  26. package/lib/project-connection.js +15 -1
  27. package/lib/project-file-history.js +223 -0
  28. package/lib/project-file-watch.js +77 -49
  29. package/lib/project-filesystem.js +31 -220
  30. package/lib/project-http.js +15 -1
  31. package/lib/project-knowledge.js +22 -0
  32. package/lib/project-log-context.js +172 -0
  33. package/lib/project-log-feedback-delivery.js +118 -0
  34. package/lib/project-logs-comments.js +137 -0
  35. package/lib/project-logs-context-state.js +42 -0
  36. package/lib/project-logs-mcp-server.js +328 -0
  37. package/lib/project-logs-query.js +185 -0
  38. package/lib/project-logs-root.js +75 -0
  39. package/lib/project-logs-schema.js +259 -0
  40. package/lib/project-logs-service.js +463 -0
  41. package/lib/project-logs-snapshot.js +205 -0
  42. package/lib/project-logs-store.js +495 -0
  43. package/lib/project-logs-versioning.js +132 -0
  44. package/lib/project-logs.js +400 -0
  45. package/lib/project-mate-interaction.js +14 -0
  46. package/lib/project-mate-knowledge.js +110 -0
  47. package/lib/project-memory.js +22 -0
  48. package/lib/project-message-delivery.js +70 -0
  49. package/lib/project-pair-lifecycle.js +460 -0
  50. package/lib/project-request-access.js +66 -0
  51. package/lib/project-session-handoff.js +1 -1
  52. package/lib/project-session-notes.js +64 -13
  53. package/lib/project-session-pair.js +159 -146
  54. package/lib/project-session-spawn.js +1 -1
  55. package/lib/project-sessions.js +58 -13
  56. package/lib/project-user-message.js +53 -4
  57. package/lib/project-worker-permission.js +430 -0
  58. package/lib/project-worker-proposal.js +208 -62
  59. package/lib/project.js +185 -6
  60. package/lib/public/app.js +63 -35
  61. package/lib/public/css/admin.css +2 -2
  62. package/lib/public/css/capsule-ui.css +38 -0
  63. package/lib/public/css/command-palette.css +19 -9
  64. package/lib/public/css/git-placard.css +154 -0
  65. package/lib/public/css/home-hub.css +4 -0
  66. package/lib/public/css/home-session-actions.css +66 -0
  67. package/lib/public/css/home-sidebar.css +66 -0
  68. package/lib/public/css/input.css +84 -3
  69. package/lib/public/css/messages.css +40 -1
  70. package/lib/public/css/mobile-nav.css +80 -0
  71. package/lib/public/css/notifications-center.css +67 -0
  72. package/lib/public/css/pane.css +97 -10
  73. package/lib/public/css/project-logs.css +198 -0
  74. package/lib/public/css/scheduler.css +1 -1
  75. package/lib/public/css/session-actions.css +16 -101
  76. package/lib/public/css/sidebar.css +103 -0
  77. package/lib/public/css/sticky-notes.css +237 -229
  78. package/lib/public/css/worker-proposal.css +36 -3
  79. package/lib/public/index.html +43 -11
  80. package/lib/public/modules/admin.js +8 -8
  81. package/lib/public/modules/app-connection.js +124 -6
  82. package/lib/public/modules/app-home-hub.js +12 -8
  83. package/lib/public/modules/app-message-cards.js +2 -1
  84. package/lib/public/modules/app-messages.js +92 -11
  85. package/lib/public/modules/app-misc.js +2 -14
  86. package/lib/public/modules/app-notifications.js +11 -7
  87. package/lib/public/modules/app-panels.js +3 -2
  88. package/lib/public/modules/app-projects.js +56 -18
  89. package/lib/public/modules/app-rendering.js +1 -0
  90. package/lib/public/modules/capsule-preference.js +44 -0
  91. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  92. package/lib/public/modules/clay-log-links.js +70 -0
  93. package/lib/public/modules/command-palette.js +52 -15
  94. package/lib/public/modules/context-sources.js +0 -3
  95. package/lib/public/modules/git-agent-sessions.js +85 -0
  96. package/lib/public/modules/git-panel.js +111 -84
  97. package/lib/public/modules/git-placard.js +161 -0
  98. package/lib/public/modules/home-chat-empty-state.js +39 -0
  99. package/lib/public/modules/home-chat-identity.js +9 -0
  100. package/lib/public/modules/home-conversations-sheet.js +102 -48
  101. package/lib/public/modules/home-mate-chat.js +15 -35
  102. package/lib/public/modules/home-session-actions.js +6 -0
  103. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  104. package/lib/public/modules/home-sidebar.js +7 -0
  105. package/lib/public/modules/home-surface-boot.js +7 -3
  106. package/lib/public/modules/home-tool-frame.js +182 -0
  107. package/lib/public/modules/home-tools.js +138 -1
  108. package/lib/public/modules/input.js +38 -7
  109. package/lib/public/modules/markdown.js +2 -0
  110. package/lib/public/modules/mate-sidebar.js +0 -8
  111. package/lib/public/modules/message-delivery.js +97 -0
  112. package/lib/public/modules/paste-modal.js +84 -0
  113. package/lib/public/modules/project-activation.js +45 -0
  114. package/lib/public/modules/project-logs-render.js +405 -0
  115. package/lib/public/modules/project-logs.js +461 -0
  116. package/lib/public/modules/project-removal-target.js +26 -0
  117. package/lib/public/modules/scheduler.js +17 -3
  118. package/lib/public/modules/session-actions.js +29 -105
  119. package/lib/public/modules/session-hierarchy.js +54 -0
  120. package/lib/public/modules/sidebar-mobile.js +40 -15
  121. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  122. package/lib/public/modules/sidebar-sessions.js +77 -11
  123. package/lib/public/modules/sidebar.js +17 -5
  124. package/lib/public/modules/split-group-helpers.js +8 -0
  125. package/lib/public/modules/split-pair-ui.js +15 -4
  126. package/lib/public/modules/split-view.js +5 -2
  127. package/lib/public/modules/split-worker-runtime.js +24 -0
  128. package/lib/public/modules/sticky-note-markdown.js +5 -0
  129. package/lib/public/modules/sticky-notes-browser.js +326 -0
  130. package/lib/public/modules/sticky-notes-card.js +360 -0
  131. package/lib/public/modules/sticky-notes-editor.js +291 -0
  132. package/lib/public/modules/sticky-notes-shared.js +90 -0
  133. package/lib/public/modules/sticky-notes.js +170 -946
  134. package/lib/public/modules/thinking-lifecycle.js +175 -0
  135. package/lib/public/modules/thinking-summary.js +28 -0
  136. package/lib/public/modules/thinking-view.js +75 -0
  137. package/lib/public/modules/tool-palette-order.js +143 -0
  138. package/lib/public/modules/tool-palette-overlays.js +159 -0
  139. package/lib/public/modules/tool-palette.js +21 -188
  140. package/lib/public/modules/tools.js +31 -123
  141. package/lib/public/modules/update-snooze.js +169 -0
  142. package/lib/public/modules/user-settings.js +4 -0
  143. package/lib/public/modules/worker-pane-lock.js +176 -0
  144. package/lib/public/modules/worker-proposal-state.js +16 -0
  145. package/lib/public/modules/worker-proposal.js +49 -20
  146. package/lib/public/style.css +3 -1
  147. package/lib/sdk-bridge.js +135 -46
  148. package/lib/sdk-message-processor.js +36 -19
  149. package/lib/server-experimental-settings.js +49 -0
  150. package/lib/server-global-ws.js +11 -0
  151. package/lib/server-home-chat-events.js +31 -1
  152. package/lib/server-home-chat.js +27 -4
  153. package/lib/server-home-clay-entry.js +1 -1
  154. package/lib/server-home-clay-session-links.js +34 -5
  155. package/lib/server-settings.js +5 -0
  156. package/lib/server-tools.js +127 -9
  157. package/lib/server.js +137 -22
  158. package/lib/session-driver-eligibility.js +38 -0
  159. package/lib/session-driver-orchestration.js +31 -0
  160. package/lib/session-hygiene.js +3 -0
  161. package/lib/session-notes-mcp-server.js +25 -5
  162. package/lib/session-pair-factory.js +247 -0
  163. package/lib/session-pair-mcp-server.js +46 -8
  164. package/lib/session-pair-prompts.js +79 -0
  165. package/lib/session-pair-turn-control.js +153 -0
  166. package/lib/session-provenance.js +119 -0
  167. package/lib/session-spawn-mcp-server.js +1 -1
  168. package/lib/session-split-groups.js +4 -1
  169. package/lib/session-title-policy.js +60 -0
  170. package/lib/session-visibility.js +41 -0
  171. package/lib/sessions.js +57 -10
  172. package/lib/tools-registry.js +89 -10
  173. package/lib/update-snooze.js +437 -0
  174. package/lib/users-experimental-preferences.js +30 -0
  175. package/lib/users-permissions.js +6 -6
  176. package/lib/users.js +22 -16
  177. package/lib/workspace-query-access.js +102 -0
  178. package/lib/workspace-query-service.js +24 -17
  179. package/lib/worktree.js +25 -38
  180. package/lib/ws-schema.js +38 -7
  181. package/lib/yoke/adapters/claude-worker.js +16 -6
  182. package/lib/yoke/adapters/claude.js +12 -0
  183. package/lib/yoke/adapters/codex.js +24 -14
  184. package/lib/yoke/vendor-registry.js +1 -1
  185. package/package.json +2 -2
@@ -0,0 +1,182 @@
1
+ // home-tool-frame.js - host side of the sandboxed rich Display frame.
2
+ //
3
+ // A rich element is additive Display: it renders in an iframe on a separate
4
+ // anonymous origin with sandbox="allow-scripts" and a frame CSP that allows
5
+ // no network at all. Its only channel is postMessage, and the only thing it
6
+ // can request is an act, which the host routes through the exact callback the
7
+ // floor buttons use. The floor never depends on the frame: if the frame fails
8
+ // to load or go ready, the host falls back to the floor automatically.
9
+
10
+ import { getWs } from './ws-ref.js';
11
+
12
+ var READY_TIMEOUT_MS = 6000;
13
+ var MAX_ACTION_LENGTH = 64;
14
+ var MAX_FRAME_HEIGHT = 4096;
15
+
16
+ var frames = Object.create(null);
17
+
18
+ // One listener for every frame. Messages are matched to a frame by the
19
+ // window they came from, never by origin text: a sandboxed frame without
20
+ // allow-same-origin reports the opaque origin "null", so identity comes from
21
+ // the contentWindow reference the host itself created.
22
+ var listenerAttached = false;
23
+
24
+ function ensureListener() {
25
+ if (listenerAttached) return;
26
+ listenerAttached = true;
27
+ window.addEventListener("message", function (event) {
28
+ var toolIds = Object.keys(frames);
29
+ var entry = null;
30
+ for (var i = 0; i < toolIds.length; i++) {
31
+ var candidate = frames[toolIds[i]];
32
+ if (candidate.iframe && candidate.iframe.contentWindow === event.source) {
33
+ entry = candidate;
34
+ break;
35
+ }
36
+ }
37
+ if (!entry) return;
38
+ var data = event.data;
39
+ if (!data || data.clayCapsuleFrame !== 1 || typeof data.type !== "string") return;
40
+ if (data.type === "ready") {
41
+ markReady(entry);
42
+ return;
43
+ }
44
+ if (data.type === "size") {
45
+ resizeFrame(entry, data.height);
46
+ return;
47
+ }
48
+ if (data.type === "act") {
49
+ // The frame asks; the host decides. Bounded validation here, then the
50
+ // same pipeline as a floor button click. Nothing else is accepted.
51
+ if (typeof data.actionId !== "string" || !data.actionId || data.actionId.length > MAX_ACTION_LENGTH) return;
52
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(data.actionId)) return;
53
+ var args = data.args && typeof data.args === "object" && !Array.isArray(data.args) ? data.args : {};
54
+ entry.onAct(data.actionId, args);
55
+ }
56
+ });
57
+ }
58
+
59
+ // The separate-origin frame cannot resize its own iframe. It reports the
60
+ // content height after its layout changes and the host applies that bounded
61
+ // value, allowing wrapping content to grow naturally as the capsule narrows.
62
+ function resizeFrame(entry, height) {
63
+ if (!Number.isFinite(height) || height <= 0 || height > MAX_FRAME_HEIGHT) return;
64
+ if (!entry.iframe) return;
65
+ entry.iframe.style.height = Math.ceil(height) + "px";
66
+ }
67
+
68
+ function markReady(entry) {
69
+ if (entry.ready) return;
70
+ entry.ready = true;
71
+ if (entry.readyTimer) {
72
+ clearTimeout(entry.readyTimer);
73
+ entry.readyTimer = null;
74
+ }
75
+ for (var i = 0; i < entry.queue.length; i++) {
76
+ postToFrame(entry, entry.queue[i]);
77
+ }
78
+ entry.queue.length = 0;
79
+ if (entry.onReady) entry.onReady();
80
+ }
81
+
82
+ function postToFrame(entry, payload) {
83
+ if (!entry.iframe || !entry.iframe.contentWindow) return;
84
+ // targetOrigin must be "*": a sandboxed frame without allow-same-origin has
85
+ // an opaque origin no origin string can name. Delivery is still exact,
86
+ // because the message goes to the one contentWindow this host created, and
87
+ // the payload is only the projection already chosen for this element.
88
+ entry.iframe.contentWindow.postMessage(payload, "*");
89
+ }
90
+
91
+ function failFrame(toolId, message) {
92
+ var entry = frames[toolId];
93
+ if (!entry) return;
94
+ disposeFrame(toolId);
95
+ if (entry.onUnavailable) entry.onUnavailable(message);
96
+ }
97
+
98
+ // Mounts the rich element for one Capsule. The frame URL is requested over
99
+ // the authenticated WebSocket and answered by handleToolFrameUrlState below.
100
+ export function mountRichDisplay(options) {
101
+ var toolId = options.toolId;
102
+ disposeFrame(toolId);
103
+ ensureListener();
104
+ frames[toolId] = {
105
+ toolId: toolId,
106
+ container: options.container,
107
+ onAct: options.onAct,
108
+ onReady: options.onReady || null,
109
+ onUnavailable: options.onUnavailable || null,
110
+ iframe: null,
111
+ ready: false,
112
+ queue: [],
113
+ readyTimer: null,
114
+ };
115
+ var ws = getWs();
116
+ if (!ws || ws.readyState !== 1) {
117
+ failFrame(toolId, "The connection to the server is not available.");
118
+ return;
119
+ }
120
+ ws.send(JSON.stringify({ type: "tool_frame_url", toolId: toolId }));
121
+ }
122
+
123
+ export function handleToolFrameUrlState(msg) {
124
+ if (!msg || !msg.toolId) return;
125
+ var entry = frames[msg.toolId];
126
+ if (!entry || entry.iframe) return;
127
+ if (msg.ok === false || !msg.frame || !msg.frame.port) {
128
+ failFrame(msg.toolId, msg.error || "The rich Display is unavailable.");
129
+ return;
130
+ }
131
+ var scheme = msg.frame.secure ? "https" : "http";
132
+ var url = scheme + "://" + window.location.hostname + ":" + msg.frame.port + msg.frame.path;
133
+ var iframe = document.createElement("iframe");
134
+ iframe.className = "home-tool-frame";
135
+ // allow-scripts only. No allow-same-origin, so the frame's origin is opaque
136
+ // and nothing of the host origin (DOM, cookies, storage) is reachable.
137
+ iframe.setAttribute("sandbox", "allow-scripts");
138
+ iframe.setAttribute("title", "Capsule rich display");
139
+ // Start collapsed so the frame can measure its content rather than merely
140
+ // echoing a fixed viewport height back to the host.
141
+ iframe.style.height = "1px";
142
+ iframe.src = url;
143
+ entry.iframe = iframe;
144
+ entry.container.appendChild(iframe);
145
+ entry.readyTimer = setTimeout(function () {
146
+ failFrame(msg.toolId, "The rich Display did not become ready.");
147
+ }, READY_TIMEOUT_MS);
148
+ }
149
+
150
+ export function pushFrameState(toolId, state) {
151
+ var entry = frames[toolId];
152
+ if (!entry) return;
153
+ var payload = { clayCapsuleFrame: 1, type: "state", state: state || {} };
154
+ if (!entry.ready) {
155
+ entry.queue.push(payload);
156
+ return;
157
+ }
158
+ postToFrame(entry, payload);
159
+ }
160
+
161
+ export function pushFrameEvent(toolId, event) {
162
+ var entry = frames[toolId];
163
+ if (!entry || !event) return;
164
+ var payload = { clayCapsuleFrame: 1, type: "event", event: event };
165
+ if (!entry.ready) {
166
+ entry.queue.push(payload);
167
+ return;
168
+ }
169
+ postToFrame(entry, payload);
170
+ }
171
+
172
+ export function hasFrame(toolId) {
173
+ return !!frames[toolId];
174
+ }
175
+
176
+ export function disposeFrame(toolId) {
177
+ var entry = frames[toolId];
178
+ if (!entry) return;
179
+ if (entry.readyTimer) clearTimeout(entry.readyTimer);
180
+ if (entry.iframe && entry.iframe.parentNode) entry.iframe.parentNode.removeChild(entry.iframe);
181
+ delete frames[toolId];
182
+ }
@@ -9,6 +9,8 @@ import { showToast } from './utils.js';
9
9
  import { createToolLlmStatus, initialToolLlmAlias } from './tool-llm-status.js';
10
10
  import { isToolModelAlias, shouldInjectToolLlmStatus } from './tool-ui-tree.js';
11
11
  import { mountCapsuleHostControls, disposeCapsuleHostControls, handleToolSourceState as applyToolSourceState, handleToolMateAccessState as applyToolMateAccessState } from './home-capsule-source.js';
12
+ import { mountRichDisplay, pushFrameState, pushFrameEvent, disposeFrame } from './home-tool-frame.js';
13
+ import { openHomeConversation } from './home-mate-chat.js';
12
14
 
13
15
  var definitions = Object.create(null);
14
16
  var runtimes = Object.create(null);
@@ -16,6 +18,8 @@ var registeredIds = Object.create(null);
16
18
  var errorSurfaces = Object.create(null);
17
19
  var displays = Object.create(null);
18
20
  var llmStatuses = Object.create(null);
21
+ var serverStates = Object.create(null);
22
+ var serverEventSeqs = Object.create(null);
19
23
 
20
24
  export function requestTools() {
21
25
  var ws = getWs();
@@ -29,6 +33,7 @@ function stopRuntime(toolId) {
29
33
  if (runtimes[toolId]) runtimes[toolId].stop();
30
34
  if (llmStatuses[toolId]) llmStatuses[toolId].dispose();
31
35
  if (displays[toolId]) disposeToolUi(displays[toolId]);
36
+ disposeFrame(toolId);
32
37
  delete runtimes[toolId];
33
38
  delete errorSurfaces[toolId];
34
39
  delete displays[toolId];
@@ -52,6 +57,133 @@ function renderRuntimeState(toolId, state) {
52
57
  }, display);
53
58
  }
54
59
 
60
+ // Server-runtime Capsules hold their Logic on the server, so the human's click
61
+ // travels the same act pipeline a Mate's clay_tool_act call travels. The
62
+ // browser only renders the state Logic hands back.
63
+ function sendServerControl(toolId, request) {
64
+ var ws = getWs();
65
+ if (!ws || ws.readyState !== 1) {
66
+ showRuntimeError(toolId, "The connection to the server is not available.");
67
+ return;
68
+ }
69
+ ws.send(JSON.stringify({
70
+ type: "tool_server_control",
71
+ toolId: toolId,
72
+ kind: request.kind,
73
+ actionId: request.actionId || null,
74
+ args: request.args || {},
75
+ }));
76
+ }
77
+
78
+ function renderServerState(toolId, state) {
79
+ var definition = definitions[toolId];
80
+ var display = displays[toolId];
81
+ if (!definition || !display) return;
82
+ renderToolUi(toolId, definition.uiTree, state || {}, function (actionName, args) {
83
+ sendServerControl(toolId, { kind: "act", actionId: actionName, args: args });
84
+ }, display);
85
+ }
86
+
87
+ // Applies a server-runtime Capsule state if it is not older than what is
88
+ // already rendered. States carry the Logic's monotonic eventSeq, so a slow
89
+ // snapshot response can never roll the Display back behind a pushed event.
90
+ function applyServerState(toolId, state) {
91
+ state = state || {};
92
+ // A successful read or event clears any stale refusal either way.
93
+ var surface = errorSurfaces[toolId];
94
+ if (surface) surface.classList.add("hidden");
95
+ var seq = Number.isInteger(state.eventSeq) ? state.eventSeq : null;
96
+ var lastSeq = serverEventSeqs[toolId];
97
+ if (seq !== null && lastSeq !== undefined && seq <= lastSeq) return false;
98
+ if (seq !== null) serverEventSeqs[toolId] = seq;
99
+ serverStates[toolId] = state;
100
+ renderServerState(toolId, state);
101
+ return true;
102
+ }
103
+
104
+ // Brief host-side attribution when the other seat acts: the Display flashes
105
+ // and the dock icon pulses, so a watching human can tell the change was the
106
+ // Mate operating the same interface. The meaning itself stays in state; this
107
+ // is only the host pointing at it.
108
+ function flashRemoteAct(toolId) {
109
+ pulseDockTool(toolId);
110
+ var display = displays[toolId];
111
+ if (!display) return;
112
+ display.classList.remove("capsule-remote-act");
113
+ void display.offsetWidth;
114
+ display.classList.add("capsule-remote-act");
115
+ setTimeout(function () { display.classList.remove("capsule-remote-act"); }, 900);
116
+ }
117
+
118
+ // A Capsule game session push. An explicit game start ("start") navigates the
119
+ // home board into the Mate's game session so starting a game visibly opens
120
+ // the table; ordinary turn engagements never yank the user away from the board.
121
+ export function handleCapsuleGameSession(msg) {
122
+ if (!msg || msg.kind !== "start" || !msg.mateId || !msg.sessionId) return;
123
+ openHomeConversation(msg.mateId, msg.sessionId);
124
+ }
125
+
126
+ export function handleToolServerState(msg) {
127
+ if (!msg || !msg.toolId) return;
128
+ if (msg.ok === false) {
129
+ showRuntimeError(msg.toolId, msg.error || "The Capsule action failed.");
130
+ return;
131
+ }
132
+ if (applyServerState(msg.toolId, msg.state)) pushFrameState(msg.toolId, msg.state || {});
133
+ }
134
+
135
+ // The push half of the shared-interface effect: Logic changed, no matter whose
136
+ // act changed it, so every open Display for this user re-renders. Events are
137
+ // applied one by one in seq order, so consecutive acts replay as steps rather
138
+ // than collapsing into a final state. The rich frame receives the full causal
139
+ // event so it can animate and attribute the transition.
140
+ export function handleToolServerEvent(msg) {
141
+ if (!msg || !msg.toolId || !msg.event || !msg.event.next) return;
142
+ if (!applyServerState(msg.toolId, msg.event.next)) return;
143
+ pushFrameEvent(msg.toolId, msg.event);
144
+ if (msg.event.actor && msg.event.actor !== "user") flashRemoteAct(msg.toolId);
145
+ }
146
+
147
+ // Mounts the additive rich element above the floor. The floor is rendered
148
+ // first and stays a click away behind the toggle, and any frame failure
149
+ // (no URL, load error, never ready) falls back to the floor silently: the
150
+ // rich element may enrich the Capsule, never gate it.
151
+ function mountServerFrame(toolId, root, floorEl) {
152
+ var frameWrap = document.createElement("div");
153
+ frameWrap.className = "home-tool-frame-wrap hidden";
154
+ frameWrap.dataset.capsuleRuntimeSurface = "true";
155
+ var toggle = document.createElement("button");
156
+ toggle.type = "button";
157
+ toggle.className = "home-tool-frame-toggle hidden";
158
+ toggle.dataset.capsuleRuntimeSurface = "true";
159
+ var showingFrame = false;
160
+ function setView(rich) {
161
+ showingFrame = rich;
162
+ frameWrap.classList.toggle("hidden", !rich);
163
+ floorEl.classList.toggle("hidden", rich);
164
+ toggle.textContent = rich ? "Standard controls" : "Rich view";
165
+ }
166
+ toggle.addEventListener("click", function () { setView(!showingFrame); });
167
+ root.appendChild(frameWrap);
168
+ root.appendChild(toggle);
169
+ mountRichDisplay({
170
+ toolId: toolId,
171
+ container: frameWrap,
172
+ onAct: function (actionId, args) {
173
+ sendServerControl(toolId, { kind: "act", actionId: actionId, args: args });
174
+ },
175
+ onReady: function () {
176
+ toggle.classList.remove("hidden");
177
+ setView(true);
178
+ },
179
+ onUnavailable: function () {
180
+ toggle.classList.add("hidden");
181
+ setView(false);
182
+ },
183
+ });
184
+ pushFrameState(toolId, serverStates[toolId] || {});
185
+ }
186
+
55
187
  function ensureRuntime(toolId) {
56
188
  if (runtimes[toolId]) return runtimes[toolId];
57
189
  var definition = definitions[toolId];
@@ -102,8 +234,11 @@ function mountTool(toolId, contentEl, chromeEl) {
102
234
  var serverDisplay = document.createElement("div");
103
235
  serverDisplay.className = "home-tool-display";
104
236
  serverDisplay.dataset.capsuleRuntimeSurface = "true";
237
+ if (definition.hasRichDisplay) mountServerFrame(toolId, root, serverDisplay);
105
238
  root.appendChild(serverDisplay);
106
- renderToolUi(toolId, definition.uiTree, {}, function () {}, serverDisplay);
239
+ displays[toolId] = serverDisplay;
240
+ renderServerState(toolId, serverStates[toolId] || {});
241
+ sendServerControl(toolId, { kind: "snapshot" });
107
242
  return;
108
243
  }
109
244
  var runtime = ensureRuntime(toolId);
@@ -189,6 +324,8 @@ export function handleToolMateAccessState(msg) {
189
324
  export function handleToolRemoved(msg) {
190
325
  stopRuntime(msg.toolId);
191
326
  delete definitions[msg.toolId];
327
+ delete serverStates[msg.toolId];
328
+ delete serverEventSeqs[msg.toolId];
192
329
  delete registeredIds[msg.toolId];
193
330
  unregisterDockTool(msg.toolId);
194
331
  syncStore();
@@ -3,11 +3,13 @@ import { setRewindMode, isRewindMode } from './rewind.js';
3
3
  import { renderPicker as renderContextPicker } from './context-sources.js';
4
4
  import { checkForMention, showMentionMenu, hideMentionMenu, isMentionMenuVisible, mentionMenuKeydown, setMentionAtIdx, parseMentionFromInput, clearMentionState, stickyReapplyMention, sendMention, sendUserMention, renderMentionUser, renderUserMention, removeMentionChip } from './mention.js';
5
5
  import { store } from './store.js';
6
+ import { sendAcknowledgedMessage } from './message-delivery.js';
6
7
  import { mateAvatarUrl } from './avatar.js';
7
8
  import { tuiIsActive, tuiSubmitText } from './session-tui-view.js';
8
9
  import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
9
10
  import { showToast } from './utils.js';
10
11
  import { isShellCommandMode, submitShellCommand } from './shell-command.js';
12
+ import { showPasteModal } from './paste-modal.js';
11
13
 
12
14
  var ctx;
13
15
 
@@ -315,7 +317,11 @@ export function sendMessage() {
315
317
  // Include selected vendor for session binding (server uses on first message)
316
318
  var _selVendor = store.get("currentVendor") || null;
317
319
  if (_selVendor) payload.vendor = _selVendor;
318
- ctx.ws.send(JSON.stringify(payload));
320
+ var clientMessageId = sendAcknowledgedMessage(payload);
321
+ var optimisticUsers = ctx.messagesEl.querySelectorAll(".msg-user");
322
+ if (optimisticUsers.length > 0) {
323
+ optimisticUsers[optimisticUsers.length - 1].dataset.clientMessageId = clientMessageId;
324
+ }
319
325
 
320
326
  // A turn blocked on an unanswered tool-permission request looks like a
321
327
  // black hole: the new message queues silently while the dots keep
@@ -474,6 +480,23 @@ function removePendingPaste(idx) {
474
480
  renderInputPreviews();
475
481
  }
476
482
 
483
+ function pastePreview(text) {
484
+ var preview = text.substring(0, 50).replace(/\n/g, " ");
485
+ if (text.length > 50) preview += "...";
486
+ return preview;
487
+ }
488
+
489
+ function editPendingPaste(idx) {
490
+ showPasteModal(pendingPastes[idx].text, function (text) {
491
+ if (!text) {
492
+ removePendingPaste(idx);
493
+ return;
494
+ }
495
+ pendingPastes[idx] = { text: text, preview: pastePreview(text) };
496
+ renderInputPreviews();
497
+ });
498
+ }
499
+
477
500
  function removePendingFile(idx) {
478
501
  pendingFiles.splice(idx, 1);
479
502
  renderInputPreviews();
@@ -557,6 +580,10 @@ function renderInputPreviews() {
557
580
  (function (idx) {
558
581
  var chip = document.createElement("div");
559
582
  chip.className = "pasted-chip";
583
+ var openBtn = document.createElement("button");
584
+ openBtn.type = "button";
585
+ openBtn.className = "pasted-chip-open";
586
+ openBtn.setAttribute("aria-label", "Edit pasted content");
560
587
  var preview = document.createElement("span");
561
588
  preview.className = "pasted-chip-preview";
562
589
  preview.textContent = pendingPastes[idx].preview;
@@ -564,14 +591,20 @@ function renderInputPreviews() {
564
591
  label.className = "pasted-chip-label";
565
592
  label.textContent = "PASTED";
566
593
  var removeBtn = document.createElement("button");
594
+ removeBtn.type = "button";
567
595
  removeBtn.className = "pasted-chip-remove";
596
+ removeBtn.setAttribute("aria-label", "Remove pasted content");
568
597
  removeBtn.innerHTML = iconHtml("x");
569
598
  removeBtn.addEventListener("click", function (e) {
570
599
  e.stopPropagation();
571
600
  removePendingPaste(idx);
572
601
  });
573
- chip.appendChild(preview);
574
- chip.appendChild(label);
602
+ openBtn.addEventListener("click", function () {
603
+ editPendingPaste(idx);
604
+ });
605
+ openBtn.appendChild(preview);
606
+ openBtn.appendChild(label);
607
+ chip.appendChild(openBtn);
575
608
  chip.appendChild(removeBtn);
576
609
  bar.appendChild(chip);
577
610
  })(j);
@@ -1026,7 +1059,7 @@ export function initInput(_ctx) {
1026
1059
  document.addEventListener("paste", function (e) {
1027
1060
  // Don't intercept paste when typing in modals or other non-chat inputs
1028
1061
  var target = e.target;
1029
- if (target && target.closest && target.closest(".sticky-note, #notes-archive, #ralph-wizard, .confirm-modal, .scheduler-detail, #debate-modal, .us-modal")) return;
1062
+ if (target && target.closest && target.closest(".sticky-note, #notes-archive, #ralph-wizard, .confirm-modal, #paste-modal, .scheduler-detail, #debate-modal, .us-modal")) return;
1030
1063
 
1031
1064
  var cd = e.clipboardData;
1032
1065
  if (!cd) return;
@@ -1080,9 +1113,7 @@ export function initInput(_ctx) {
1080
1113
  var pastedText = cd.getData("text/plain");
1081
1114
  if (pastedText && pastedText.length >= 500) {
1082
1115
  e.preventDefault();
1083
- var preview = pastedText.substring(0, 50).replace(/\n/g, " ");
1084
- if (pastedText.length > 50) preview += "...";
1085
- pendingPastes.push({ text: pastedText, preview: preview });
1116
+ pendingPastes.push({ text: pastedText, preview: pastePreview(pastedText) });
1086
1117
  renderInputPreviews();
1087
1118
  found = true;
1088
1119
  }
@@ -2,6 +2,7 @@ import { copyToClipboard, escapeHtml } from './utils.js';
2
2
  import { refreshIcons } from './icons.js';
3
3
  import { getMermaidThemeVars } from './theme.js';
4
4
  import { store } from './store.js';
5
+ import { enhanceClayLogLinks } from './clay-log-links.js';
5
6
 
6
7
  // Initialize markdown parser
7
8
  marked.use({ gfm: true, breaks: false });
@@ -89,6 +90,7 @@ export function enhanceClaySessionLinks(root) {
89
90
  fragment.appendChild(document.createTextNode(text.slice(offset)));
90
91
  nodes[i].parentNode.replaceChild(fragment, nodes[i]);
91
92
  }
93
+ enhanceClayLogLinks(root);
92
94
  }
93
95
 
94
96
  /**
@@ -117,14 +117,6 @@ export function initMateSidebar(wsGetter) {
117
117
  if (origBtn) origBtn.click();
118
118
  });
119
119
  }
120
- var mateSchedulerBtn = document.getElementById("mate-scheduler-btn");
121
- if (mateSchedulerBtn) {
122
- mateSchedulerBtn.addEventListener("click", function () {
123
- hideKnowledge();
124
- var origBtn = document.getElementById("scheduler-btn");
125
- if (origBtn) origBtn.click();
126
- });
127
- }
128
120
  }
129
121
 
130
122
  export function showMateSidebar(mateId, mateData) {
@@ -0,0 +1,97 @@
1
+ // Reliable delivery for ordinary project chat messages within a page lifetime.
2
+
3
+ import { store } from './store.js';
4
+ import { getWs } from './ws-ref.js';
5
+
6
+ var ACK_TIMEOUT_MS = 5000;
7
+ var ackTimers = {};
8
+
9
+ function createClientMessageId() {
10
+ if (window.crypto && typeof window.crypto.randomUUID === "function") {
11
+ return "cm-" + window.crypto.randomUUID();
12
+ }
13
+ return "cm-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2);
14
+ }
15
+
16
+ function pendingMessages() {
17
+ return store.get('pendingOutboundMessages') || [];
18
+ }
19
+
20
+ function replacePending(messages) {
21
+ store.set({ pendingOutboundMessages: messages });
22
+ }
23
+
24
+ function clearAckTimer(clientMessageId) {
25
+ if (!ackTimers[clientMessageId]) return;
26
+ clearTimeout(ackTimers[clientMessageId]);
27
+ delete ackTimers[clientMessageId];
28
+ }
29
+
30
+ function isPending(clientMessageId) {
31
+ var pending = pendingMessages();
32
+ for (var i = 0; i < pending.length; i++) {
33
+ if (pending[i].payload.clientMessageId === clientMessageId) return true;
34
+ }
35
+ return false;
36
+ }
37
+
38
+ function armAckTimer(clientMessageId, socket) {
39
+ clearAckTimer(clientMessageId);
40
+ ackTimers[clientMessageId] = setTimeout(function () {
41
+ delete ackTimers[clientMessageId];
42
+ if (!isPending(clientMessageId) || getWs() !== socket) return;
43
+ window.dispatchEvent(new CustomEvent("clay-message-delivery-timeout", {
44
+ detail: { socket: socket, clientMessageId: clientMessageId },
45
+ }));
46
+ }, ACK_TIMEOUT_MS);
47
+ }
48
+
49
+ function transmit(entry) {
50
+ var socket = getWs();
51
+ if (!socket || socket.readyState !== 1) return false;
52
+ try {
53
+ socket.send(JSON.stringify(entry.payload));
54
+ armAckTimer(entry.payload.clientMessageId, socket);
55
+ return true;
56
+ } catch (e) {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ export function sendAcknowledgedMessage(payload) {
62
+ var nextPayload = Object.assign({}, payload, { clientMessageId: createClientMessageId() });
63
+ var entry = {
64
+ projectSlug: store.get('currentSlug'),
65
+ sessionId: store.get('activeSessionId'),
66
+ payload: nextPayload,
67
+ };
68
+ replacePending(pendingMessages().concat([entry]));
69
+ if (!transmit(entry)) {
70
+ window.dispatchEvent(new CustomEvent("clay-message-delivery-timeout", {
71
+ detail: { socket: getWs(), clientMessageId: nextPayload.clientMessageId },
72
+ }));
73
+ }
74
+ return nextPayload.clientMessageId;
75
+ }
76
+
77
+ export function acknowledgeMessage(clientMessageId) {
78
+ if (typeof clientMessageId !== "string") return false;
79
+ var pending = pendingMessages();
80
+ var next = pending.filter(function (entry) {
81
+ return entry.payload.clientMessageId !== clientMessageId;
82
+ });
83
+ if (next.length === pending.length) return false;
84
+ clearAckTimer(clientMessageId);
85
+ replacePending(next);
86
+ return true;
87
+ }
88
+
89
+ export function replayPendingMessages(sessionId) {
90
+ var pending = pendingMessages();
91
+ var projectSlug = store.get('currentSlug');
92
+ for (var i = 0; i < pending.length; i++) {
93
+ if (pending[i].projectSlug === projectSlug && pending[i].sessionId === sessionId) {
94
+ transmit(pending[i]);
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,84 @@
1
+ // paste-modal.js - Shared read-only and composer-editable pasted-content dialog.
2
+
3
+ import { refreshIcons } from './icons.js';
4
+ import { copyToClipboard } from './utils.js';
5
+
6
+ var saveCallback = null;
7
+ var previousFocus = null;
8
+ var initialized = false;
9
+
10
+ function getElements() {
11
+ return {
12
+ modal: document.getElementById("paste-modal"),
13
+ title: document.getElementById("paste-modal-title"),
14
+ body: document.getElementById("paste-modal-body"),
15
+ editor: document.getElementById("paste-modal-editor"),
16
+ footer: document.getElementById("paste-modal-footer"),
17
+ save: document.getElementById("paste-modal-save")
18
+ };
19
+ }
20
+
21
+ function currentText(elements) {
22
+ if (saveCallback && elements.editor) return elements.editor.value;
23
+ return elements.body ? elements.body.textContent : "";
24
+ }
25
+
26
+ export function closePasteModal() {
27
+ var elements = getElements();
28
+ if (!elements.modal || elements.modal.classList.contains("hidden")) return;
29
+ elements.modal.classList.add("hidden");
30
+ elements.modal.classList.remove("paste-modal-editing");
31
+ saveCallback = null;
32
+ if (previousFocus && previousFocus.isConnected && previousFocus.focus) previousFocus.focus();
33
+ previousFocus = null;
34
+ }
35
+
36
+ export function showPasteModal(text, onSave) {
37
+ var elements = getElements();
38
+ if (!elements.modal || !elements.body || !elements.editor) return;
39
+ var editing = typeof onSave === "function";
40
+ previousFocus = document.activeElement;
41
+ saveCallback = editing ? onSave : null;
42
+ elements.body.textContent = text || "";
43
+ elements.editor.value = text || "";
44
+ elements.title.textContent = editing ? "Edit pasted content" : "Pasted content";
45
+ elements.modal.classList.toggle("paste-modal-editing", editing);
46
+ elements.modal.classList.remove("hidden");
47
+ refreshIcons(elements.modal);
48
+ requestAnimationFrame(function () {
49
+ if (editing) {
50
+ elements.editor.focus();
51
+ elements.editor.setSelectionRange(elements.editor.value.length, elements.editor.value.length);
52
+ } else {
53
+ var close = elements.modal.querySelector(".paste-modal-close");
54
+ if (close) close.focus();
55
+ }
56
+ });
57
+ }
58
+
59
+ export function initPasteModal() {
60
+ if (initialized) return;
61
+ var elements = getElements();
62
+ if (!elements.modal) return;
63
+ initialized = true;
64
+
65
+ elements.modal.querySelector(".confirm-backdrop").addEventListener("click", closePasteModal);
66
+ elements.modal.querySelector(".paste-modal-close").addEventListener("click", closePasteModal);
67
+ elements.modal.querySelector(".paste-modal-cancel").addEventListener("click", closePasteModal);
68
+ elements.modal.querySelector(".paste-modal-copy").addEventListener("click", function () {
69
+ copyToClipboard(currentText(elements), "Copied to clipboard");
70
+ });
71
+ elements.save.addEventListener("click", function () {
72
+ if (!saveCallback) return;
73
+ var callback = saveCallback;
74
+ var value = elements.editor.value;
75
+ closePasteModal();
76
+ callback(value);
77
+ });
78
+ elements.modal.addEventListener("keydown", function (event) {
79
+ if (event.key === "Escape") {
80
+ event.preventDefault();
81
+ closePasteModal();
82
+ }
83
+ });
84
+ }