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.
- package/lib/capsule-display-floor.js +51 -0
- package/lib/capsule-frame-server.js +255 -0
- package/lib/capsule-mate-turn.js +105 -0
- package/lib/capsule-pig-logic.js +300 -0
- package/lib/capsule-server-runtimes.js +39 -0
- package/lib/capsules/pig/display.js +198 -0
- package/lib/capsules/pig/manifest.json +9 -0
- package/lib/capsules/pig/ui.json +84 -0
- package/lib/config.js +25 -1
- package/lib/daemon-projects.js +13 -8
- package/lib/daemon.js +127 -14
- package/lib/git-cli.js +95 -1
- package/lib/knowledge-import.js +436 -0
- package/lib/knowledge-record-store.js +188 -0
- package/lib/knowledge-search.js +240 -0
- package/lib/mate-knowledge-mcp-server.js +151 -0
- package/lib/mate-knowledge-migration.js +283 -0
- package/lib/mate-knowledge-service.js +488 -0
- package/lib/mate-knowledge-sync.js +482 -0
- package/lib/migrate-single-user.js +2 -0
- package/lib/notes-lifecycle.js +149 -0
- package/lib/notes.js +60 -3
- package/lib/os-users.js +3 -1
- package/lib/project-capsule-catalog.js +9 -1
- package/lib/project-capsule-turn.js +96 -0
- package/lib/project-connection.js +15 -1
- package/lib/project-file-history.js +223 -0
- package/lib/project-file-watch.js +77 -49
- package/lib/project-filesystem.js +31 -220
- package/lib/project-http.js +15 -1
- package/lib/project-knowledge.js +22 -0
- package/lib/project-log-context.js +172 -0
- package/lib/project-log-feedback-delivery.js +118 -0
- package/lib/project-logs-comments.js +137 -0
- package/lib/project-logs-context-state.js +42 -0
- package/lib/project-logs-mcp-server.js +328 -0
- package/lib/project-logs-query.js +185 -0
- package/lib/project-logs-root.js +75 -0
- package/lib/project-logs-schema.js +259 -0
- package/lib/project-logs-service.js +463 -0
- package/lib/project-logs-snapshot.js +205 -0
- package/lib/project-logs-store.js +495 -0
- package/lib/project-logs-versioning.js +132 -0
- package/lib/project-logs.js +400 -0
- package/lib/project-mate-interaction.js +14 -0
- package/lib/project-mate-knowledge.js +110 -0
- package/lib/project-memory.js +22 -0
- package/lib/project-message-delivery.js +70 -0
- package/lib/project-pair-lifecycle.js +460 -0
- package/lib/project-request-access.js +66 -0
- package/lib/project-session-handoff.js +1 -1
- package/lib/project-session-notes.js +64 -13
- package/lib/project-session-pair.js +159 -146
- package/lib/project-session-spawn.js +1 -1
- package/lib/project-sessions.js +58 -13
- package/lib/project-user-message.js +53 -4
- package/lib/project-worker-permission.js +430 -0
- package/lib/project-worker-proposal.js +208 -62
- package/lib/project.js +185 -6
- package/lib/public/app.js +63 -35
- package/lib/public/css/admin.css +2 -2
- package/lib/public/css/capsule-ui.css +38 -0
- package/lib/public/css/command-palette.css +19 -9
- package/lib/public/css/git-placard.css +154 -0
- package/lib/public/css/home-hub.css +4 -0
- package/lib/public/css/home-session-actions.css +66 -0
- package/lib/public/css/home-sidebar.css +66 -0
- package/lib/public/css/input.css +84 -3
- package/lib/public/css/messages.css +40 -1
- package/lib/public/css/mobile-nav.css +80 -0
- package/lib/public/css/notifications-center.css +67 -0
- package/lib/public/css/pane.css +97 -10
- package/lib/public/css/project-logs.css +198 -0
- package/lib/public/css/scheduler.css +1 -1
- package/lib/public/css/session-actions.css +16 -101
- package/lib/public/css/sidebar.css +103 -0
- package/lib/public/css/sticky-notes.css +237 -229
- package/lib/public/css/worker-proposal.css +36 -3
- package/lib/public/index.html +43 -11
- package/lib/public/modules/admin.js +8 -8
- package/lib/public/modules/app-connection.js +124 -6
- package/lib/public/modules/app-home-hub.js +12 -8
- package/lib/public/modules/app-message-cards.js +2 -1
- package/lib/public/modules/app-messages.js +92 -11
- package/lib/public/modules/app-misc.js +2 -14
- package/lib/public/modules/app-notifications.js +11 -7
- package/lib/public/modules/app-panels.js +3 -2
- package/lib/public/modules/app-projects.js +56 -18
- package/lib/public/modules/app-rendering.js +1 -0
- package/lib/public/modules/capsule-preference.js +44 -0
- package/lib/public/modules/chat-bubble-renderer.js +1 -1
- package/lib/public/modules/clay-log-links.js +70 -0
- package/lib/public/modules/command-palette.js +52 -15
- package/lib/public/modules/context-sources.js +0 -3
- package/lib/public/modules/git-agent-sessions.js +85 -0
- package/lib/public/modules/git-panel.js +111 -84
- package/lib/public/modules/git-placard.js +161 -0
- package/lib/public/modules/home-chat-empty-state.js +39 -0
- package/lib/public/modules/home-chat-identity.js +9 -0
- package/lib/public/modules/home-conversations-sheet.js +102 -48
- package/lib/public/modules/home-mate-chat.js +15 -35
- package/lib/public/modules/home-session-actions.js +6 -0
- package/lib/public/modules/home-sidebar-chat-list.js +101 -41
- package/lib/public/modules/home-sidebar.js +7 -0
- package/lib/public/modules/home-surface-boot.js +7 -3
- package/lib/public/modules/home-tool-frame.js +182 -0
- package/lib/public/modules/home-tools.js +138 -1
- package/lib/public/modules/input.js +38 -7
- package/lib/public/modules/markdown.js +2 -0
- package/lib/public/modules/mate-sidebar.js +0 -8
- package/lib/public/modules/message-delivery.js +97 -0
- package/lib/public/modules/paste-modal.js +84 -0
- package/lib/public/modules/project-activation.js +45 -0
- package/lib/public/modules/project-logs-render.js +405 -0
- package/lib/public/modules/project-logs.js +461 -0
- package/lib/public/modules/project-removal-target.js +26 -0
- package/lib/public/modules/scheduler.js +17 -3
- package/lib/public/modules/session-actions.js +29 -105
- package/lib/public/modules/session-hierarchy.js +54 -0
- package/lib/public/modules/sidebar-mobile.js +40 -15
- package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
- package/lib/public/modules/sidebar-sessions.js +77 -11
- package/lib/public/modules/sidebar.js +17 -5
- package/lib/public/modules/split-group-helpers.js +8 -0
- package/lib/public/modules/split-pair-ui.js +15 -4
- package/lib/public/modules/split-view.js +5 -2
- package/lib/public/modules/split-worker-runtime.js +24 -0
- package/lib/public/modules/sticky-note-markdown.js +5 -0
- package/lib/public/modules/sticky-notes-browser.js +326 -0
- package/lib/public/modules/sticky-notes-card.js +360 -0
- package/lib/public/modules/sticky-notes-editor.js +291 -0
- package/lib/public/modules/sticky-notes-shared.js +90 -0
- package/lib/public/modules/sticky-notes.js +170 -946
- package/lib/public/modules/thinking-lifecycle.js +175 -0
- package/lib/public/modules/thinking-summary.js +28 -0
- package/lib/public/modules/thinking-view.js +75 -0
- package/lib/public/modules/tool-palette-order.js +143 -0
- package/lib/public/modules/tool-palette-overlays.js +159 -0
- package/lib/public/modules/tool-palette.js +21 -188
- package/lib/public/modules/tools.js +31 -123
- package/lib/public/modules/update-snooze.js +169 -0
- package/lib/public/modules/user-settings.js +4 -0
- package/lib/public/modules/worker-pane-lock.js +176 -0
- package/lib/public/modules/worker-proposal-state.js +16 -0
- package/lib/public/modules/worker-proposal.js +49 -20
- package/lib/public/style.css +3 -1
- package/lib/sdk-bridge.js +135 -46
- package/lib/sdk-message-processor.js +36 -19
- package/lib/server-experimental-settings.js +49 -0
- package/lib/server-global-ws.js +11 -0
- package/lib/server-home-chat-events.js +31 -1
- package/lib/server-home-chat.js +27 -4
- package/lib/server-home-clay-entry.js +1 -1
- package/lib/server-home-clay-session-links.js +34 -5
- package/lib/server-settings.js +5 -0
- package/lib/server-tools.js +127 -9
- package/lib/server.js +137 -22
- package/lib/session-driver-eligibility.js +38 -0
- package/lib/session-driver-orchestration.js +31 -0
- package/lib/session-hygiene.js +3 -0
- package/lib/session-notes-mcp-server.js +25 -5
- package/lib/session-pair-factory.js +247 -0
- package/lib/session-pair-mcp-server.js +46 -8
- package/lib/session-pair-prompts.js +79 -0
- package/lib/session-pair-turn-control.js +153 -0
- package/lib/session-provenance.js +119 -0
- package/lib/session-spawn-mcp-server.js +1 -1
- package/lib/session-split-groups.js +4 -1
- package/lib/session-title-policy.js +60 -0
- package/lib/session-visibility.js +41 -0
- package/lib/sessions.js +57 -10
- package/lib/tools-registry.js +89 -10
- package/lib/update-snooze.js +437 -0
- package/lib/users-experimental-preferences.js +30 -0
- package/lib/users-permissions.js +6 -6
- package/lib/users.js +22 -16
- package/lib/workspace-query-access.js +102 -0
- package/lib/workspace-query-service.js +24 -17
- package/lib/worktree.js +25 -38
- package/lib/ws-schema.js +38 -7
- package/lib/yoke/adapters/claude-worker.js +16 -6
- package/lib/yoke/adapters/claude.js +12 -0
- package/lib/yoke/adapters/codex.js +24 -14
- package/lib/yoke/vendor-registry.js +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
574
|
-
|
|
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
|
-
|
|
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
|
+
}
|