clay-server 2.47.0-beta.6 → 3.0.0-beta.2
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/daemon.js +6 -27
- package/lib/project-connection.js +21 -8
- package/lib/project-session-pair.js +312 -0
- package/lib/project-session-spawn.js +8 -4
- package/lib/project-sessions.js +108 -10
- package/lib/project-user-message.js +18 -8
- package/lib/project.js +33 -0
- package/lib/public/app.js +20 -0
- package/lib/public/css/menus.css +83 -1
- package/lib/public/css/messages.css +14 -0
- package/lib/public/css/pane.css +420 -0
- package/lib/public/css/sidebar.css +40 -0
- package/lib/public/css/sticky-notes.css +2 -0
- package/lib/public/index.html +4 -0
- package/lib/public/modules/app-connection.js +6 -0
- package/lib/public/modules/app-favicon.js +5 -0
- package/lib/public/modules/app-header.js +101 -6
- package/lib/public/modules/app-messages.js +59 -6
- package/lib/public/modules/app-panels.js +49 -27
- package/lib/public/modules/app-rendering.js +11 -4
- package/lib/public/modules/pane-bridge.js +41 -0
- package/lib/public/modules/pane-session.js +18 -0
- package/lib/public/modules/sidebar-sessions.js +175 -1
- package/lib/public/modules/sidebar.js +2 -0
- package/lib/public/modules/split-group-helpers.js +18 -0
- package/lib/public/modules/split-pair-ui.js +324 -0
- package/lib/public/modules/split-view.js +499 -0
- package/lib/public/modules/sticky-notes.js +32 -91
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +111 -20
- package/lib/sdk-message-processor.js +2 -2
- package/lib/server.js +24 -4
- package/lib/session-hygiene.js +100 -0
- package/lib/session-pair-mcp-server.js +28 -0
- package/lib/session-split-groups.js +298 -0
- package/lib/sessions.js +163 -34
- package/lib/ws-request.js +14 -0
- package/lib/ws-schema.js +13 -0
- package/lib/yoke/adapters/claude.js +32 -0
- package/lib/yoke/adapters/codex.js +100 -77
- package/lib/yoke/adapters/kiro.js +34 -35
- package/lib/yoke/codex-app-server.js +56 -6
- package/lib/yoke/index.js +1 -0
- package/lib/yoke/skill-discovery.js +196 -0
- package/lib/yoke/vendor-registry.js +34 -0
- package/package.json +2 -2
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
// Two-pane iframe shell for the split-view spike.
|
|
2
|
+
|
|
3
|
+
import { store } from './store.js';
|
|
4
|
+
import { getWs } from './ws-ref.js';
|
|
5
|
+
import { getCachedSessions } from './sidebar-sessions.js';
|
|
6
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
7
|
+
import { detachTuiView } from './session-tui-view.js';
|
|
8
|
+
import { formatTokens } from './app-panels.js';
|
|
9
|
+
import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
|
|
10
|
+
import { groupedSessionIds, findSplitGroup } from './split-group-helpers.js';
|
|
11
|
+
import { showConfirm } from './app-misc.js';
|
|
12
|
+
import { syncPairChrome } from './split-pair-ui.js';
|
|
13
|
+
|
|
14
|
+
var host = null;
|
|
15
|
+
var nativeApp = null;
|
|
16
|
+
var mainPanelsEl = null;
|
|
17
|
+
var dropOverlay = null;
|
|
18
|
+
var ghostTitleEl = null;
|
|
19
|
+
var draggedSessionId = null;
|
|
20
|
+
var stickyNotesContainer = null;
|
|
21
|
+
var stickyNotesHome = null;
|
|
22
|
+
var stickyNotesAnchor = null;
|
|
23
|
+
function placeStickyNotesOverlay(splitActive) {
|
|
24
|
+
if (!stickyNotesContainer || !stickyNotesHome || !mainPanelsEl) return;
|
|
25
|
+
if (splitActive) {
|
|
26
|
+
// Notes are project-wide, so split view owns one canvas above both panes.
|
|
27
|
+
// Pane-mode CSS suppresses the duplicate canvas inside each iframe.
|
|
28
|
+
mainPanelsEl.appendChild(stickyNotesContainer);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (stickyNotesAnchor && stickyNotesAnchor.parentNode === stickyNotesHome) {
|
|
32
|
+
stickyNotesHome.insertBefore(stickyNotesContainer, stickyNotesAnchor);
|
|
33
|
+
} else {
|
|
34
|
+
stickyNotesHome.appendChild(stickyNotesContainer);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sessionById(sessionId) {
|
|
39
|
+
var sessions = getCachedSessions() || [];
|
|
40
|
+
for (var i = 0; i < sessions.length; i++) {
|
|
41
|
+
if (sessions[i].id === sessionId) return sessions[i];
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function paneForSession(sessionId) {
|
|
47
|
+
var session = sessionById(sessionId);
|
|
48
|
+
return {
|
|
49
|
+
slug: store.get('currentSlug'),
|
|
50
|
+
sessionId: sessionId,
|
|
51
|
+
title: (session && session.title) || ("Session " + sessionId),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function paneUrl(pane) {
|
|
56
|
+
return "/p/" + encodeURIComponent(pane.slug) + "/?pane=1&session=" + encodeURIComponent(pane.sessionId);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Arc-style drop preview: hovering one half folds the live app into the
|
|
60
|
+
// other half and shows a ghost pane where the dragged session will land.
|
|
61
|
+
function setPreviewSide(side) {
|
|
62
|
+
if (!mainPanelsEl) return;
|
|
63
|
+
mainPanelsEl.classList.toggle("split-preview-left", side === "left");
|
|
64
|
+
mainPanelsEl.classList.toggle("split-preview-right", side === "right");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hideDropOverlay() {
|
|
68
|
+
if (dropOverlay) dropOverlay.classList.remove("visible");
|
|
69
|
+
setPreviewSide(null);
|
|
70
|
+
if (mainPanelsEl) mainPanelsEl.classList.remove("split-drag-active");
|
|
71
|
+
draggedSessionId = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function showDropOverlay() {
|
|
75
|
+
if (!dropOverlay || store.get('splitPanes')) return;
|
|
76
|
+
var grouped = groupedSessionIds(store.get('splitGroups'));
|
|
77
|
+
if (grouped.has(store.get('activeSessionId'))) return;
|
|
78
|
+
if (ghostTitleEl) {
|
|
79
|
+
var session = sessionById(draggedSessionId);
|
|
80
|
+
ghostTitleEl.textContent = (session && session.title) || ("Session " + draggedSessionId);
|
|
81
|
+
}
|
|
82
|
+
if (mainPanelsEl) mainPanelsEl.classList.add("split-drag-active");
|
|
83
|
+
dropOverlay.classList.add("visible");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function switchNativeSession(sessionId) {
|
|
87
|
+
store.set({ splitPanes: null });
|
|
88
|
+
var ws = getWs();
|
|
89
|
+
if (ws && ws.readyState === 1) {
|
|
90
|
+
ws.send(JSON.stringify({ type: "switch_session", id: sessionId }));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function closePane(index) {
|
|
95
|
+
var split = store.get('splitPanes');
|
|
96
|
+
if (!split || !split.panes || split.panes.length !== 2) return;
|
|
97
|
+
if (split.groupId && getWs() && getWs().readyState === 1) {
|
|
98
|
+
getWs().send(JSON.stringify({ type: "split_group_dissolve", id: split.groupId }));
|
|
99
|
+
}
|
|
100
|
+
switchNativeSession(split.panes[index === 0 ? 1 : 0].sessionId);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function startPaneRename(header, titleEl, pane) {
|
|
104
|
+
if (header.querySelector(".split-pane-rename-input")) return;
|
|
105
|
+
var input = document.createElement("input");
|
|
106
|
+
input.type = "text";
|
|
107
|
+
input.className = "split-pane-rename-input";
|
|
108
|
+
input.value = pane.title;
|
|
109
|
+
titleEl.style.display = "none";
|
|
110
|
+
header.insertBefore(input, titleEl);
|
|
111
|
+
input.focus();
|
|
112
|
+
input.select();
|
|
113
|
+
|
|
114
|
+
var done = false;
|
|
115
|
+
function finish(commit) {
|
|
116
|
+
if (done) return;
|
|
117
|
+
done = true;
|
|
118
|
+
var newTitle = input.value.trim();
|
|
119
|
+
input.remove();
|
|
120
|
+
titleEl.style.display = "";
|
|
121
|
+
if (!commit || !newTitle || newTitle === pane.title) return;
|
|
122
|
+
pane.title = newTitle;
|
|
123
|
+
titleEl.textContent = newTitle;
|
|
124
|
+
var ws = getWs();
|
|
125
|
+
if (ws && ws.readyState === 1) {
|
|
126
|
+
ws.send(JSON.stringify({ type: "rename_session", id: pane.sessionId, title: newTitle }));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
input.addEventListener("keydown", function (event) {
|
|
131
|
+
if (event.key === "Enter") { event.preventDefault(); finish(true); }
|
|
132
|
+
if (event.key === "Escape") { event.preventDefault(); finish(false); }
|
|
133
|
+
});
|
|
134
|
+
input.addEventListener("blur", function () { finish(true); });
|
|
135
|
+
input.addEventListener("click", function (event) { event.stopPropagation(); });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Called on session_list so pane headers follow renames made elsewhere
|
|
139
|
+
// (sidebar, in-pane app). Panes being renamed inline are left alone.
|
|
140
|
+
export function syncPaneTitles() {
|
|
141
|
+
var split = store.get('splitPanes');
|
|
142
|
+
if (!split || !split.panes || !host) return;
|
|
143
|
+
var titleEls = host.querySelectorAll(".split-pane-title");
|
|
144
|
+
for (var i = 0; i < split.panes.length && i < titleEls.length; i++) {
|
|
145
|
+
var session = sessionById(split.panes[i].sessionId);
|
|
146
|
+
if (!session || !session.title) continue;
|
|
147
|
+
split.panes[i].title = session.title;
|
|
148
|
+
if (titleEls[i].style.display !== "none") titleEls[i].textContent = session.title;
|
|
149
|
+
}
|
|
150
|
+
var accessBtns = host.querySelectorAll(".split-pane-full-access");
|
|
151
|
+
for (var ai = 0; ai < split.panes.length && ai < accessBtns.length; ai++) {
|
|
152
|
+
updatePaneFullAccessButton(accessBtns[ai], sessionById(split.panes[ai].sessionId));
|
|
153
|
+
}
|
|
154
|
+
syncPairChrome(host, split);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function updatePaneFullAccessButton(button, session) {
|
|
158
|
+
if (!button) return;
|
|
159
|
+
var mode = session && (session.runtimeMode || session.mode || "gui");
|
|
160
|
+
var visible = !!session && !store.get('skipPermsEnabled') && mode === "gui";
|
|
161
|
+
var enabled = visible && session.permissionMode === "bypassPermissions";
|
|
162
|
+
button.classList.toggle("hidden", !visible);
|
|
163
|
+
button.classList.toggle("active", enabled);
|
|
164
|
+
button.setAttribute("aria-checked", enabled ? "true" : "false");
|
|
165
|
+
button.title = enabled ? "Skip Permissions is on — click to restore permission prompts" : "Skip Permissions is off — permission prompts are on";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function setPaneFullAccess(sessionId, enabled) {
|
|
169
|
+
var ws = getWs();
|
|
170
|
+
if (ws && ws.readyState === 1) {
|
|
171
|
+
ws.send(JSON.stringify({ type: "set_session_full_access", id: sessionId, enabled: enabled }));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function createPane(pane, index) {
|
|
176
|
+
var paneEl = document.createElement("section");
|
|
177
|
+
paneEl.className = "split-pane";
|
|
178
|
+
|
|
179
|
+
var header = document.createElement("header");
|
|
180
|
+
header.className = "split-pane-header";
|
|
181
|
+
|
|
182
|
+
var session = sessionById(pane.sessionId);
|
|
183
|
+
var vendor = (session && session.vendor) || "claude";
|
|
184
|
+
var vendorIcon = document.createElement("img");
|
|
185
|
+
vendorIcon.className = "split-pane-vendor";
|
|
186
|
+
vendorIcon.src = VENDOR_AVATARS[vendor] || VENDOR_AVATARS.claude;
|
|
187
|
+
vendorIcon.alt = "";
|
|
188
|
+
vendorIcon.title = VENDOR_NAMES[vendor] || vendor;
|
|
189
|
+
header.appendChild(vendorIcon);
|
|
190
|
+
|
|
191
|
+
var title = document.createElement("span");
|
|
192
|
+
title.className = "split-pane-title";
|
|
193
|
+
title.textContent = pane.title;
|
|
194
|
+
title.title = "Rename session";
|
|
195
|
+
title.addEventListener("click", function () { startPaneRename(header, title, pane); });
|
|
196
|
+
header.appendChild(title);
|
|
197
|
+
|
|
198
|
+
var frame = document.createElement("iframe");
|
|
199
|
+
frame.className = "split-pane-frame";
|
|
200
|
+
frame.src = paneUrl(pane);
|
|
201
|
+
frame.title = pane.title;
|
|
202
|
+
|
|
203
|
+
// Context-usage chip, fed by clay-pane-context messages from the pane
|
|
204
|
+
// iframe. Hidden until the pane reports a non-zero context percentage.
|
|
205
|
+
var ctxChip = document.createElement("button");
|
|
206
|
+
ctxChip.type = "button";
|
|
207
|
+
ctxChip.className = "split-pane-context";
|
|
208
|
+
ctxChip.title = "Context usage";
|
|
209
|
+
ctxChip.innerHTML = '<span class="split-pane-context-bar"><span class="split-pane-context-fill"></span></span><span class="split-pane-context-label"></span>';
|
|
210
|
+
ctxChip.addEventListener("click", function () {
|
|
211
|
+
if (frame.contentWindow) {
|
|
212
|
+
frame.contentWindow.postMessage({ type: "clay-pane-toggle-context" }, window.location.origin);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
header.appendChild(ctxChip);
|
|
216
|
+
|
|
217
|
+
var fullAccess = document.createElement("button");
|
|
218
|
+
fullAccess.type = "button";
|
|
219
|
+
fullAccess.className = "session-full-access split-pane-full-access hidden";
|
|
220
|
+
fullAccess.setAttribute("role", "switch");
|
|
221
|
+
fullAccess.innerHTML = '<span class="session-full-access-label">Skip Permissions</span><span class="session-full-access-track" aria-hidden="true"><span></span></span>';
|
|
222
|
+
updatePaneFullAccessButton(fullAccess, session);
|
|
223
|
+
fullAccess.addEventListener("click", function () {
|
|
224
|
+
var current = sessionById(pane.sessionId);
|
|
225
|
+
if (current && current.permissionMode === "bypassPermissions") {
|
|
226
|
+
setPaneFullAccess(pane.sessionId, false);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
showConfirm("Skip permission prompts for this session? Tool requests will be approved automatically.", function () {
|
|
230
|
+
setPaneFullAccess(pane.sessionId, true);
|
|
231
|
+
}, "Skip Permissions", true);
|
|
232
|
+
});
|
|
233
|
+
header.appendChild(fullAccess);
|
|
234
|
+
|
|
235
|
+
var close = document.createElement("button");
|
|
236
|
+
close.type = "button";
|
|
237
|
+
close.className = "split-pane-close";
|
|
238
|
+
close.title = "Close pane";
|
|
239
|
+
close.setAttribute("aria-label", "Close pane");
|
|
240
|
+
close.innerHTML = iconHtml("x");
|
|
241
|
+
close.addEventListener("click", function () { closePane(index); });
|
|
242
|
+
header.appendChild(close);
|
|
243
|
+
paneEl.appendChild(header);
|
|
244
|
+
paneEl.appendChild(frame);
|
|
245
|
+
return paneEl;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function updatePaneContextChip(paneEl, msg) {
|
|
249
|
+
if (!paneEl) return;
|
|
250
|
+
var chip = paneEl.querySelector(".split-pane-context");
|
|
251
|
+
if (!chip) return;
|
|
252
|
+
var pct = msg.pct || 0;
|
|
253
|
+
if (pct <= 0) return;
|
|
254
|
+
chip.classList.add("has-data");
|
|
255
|
+
var fill = chip.querySelector(".split-pane-context-fill");
|
|
256
|
+
var label = chip.querySelector(".split-pane-context-label");
|
|
257
|
+
fill.style.width = Math.min(100, pct).toFixed(1) + "%";
|
|
258
|
+
fill.className = "split-pane-context-fill" + (msg.cls || "");
|
|
259
|
+
label.textContent = pct.toFixed(0) + "%";
|
|
260
|
+
var tip = "Context " + pct.toFixed(0) + "% (" + formatTokens(msg.used || 0) + " / " + formatTokens(msg.win || 0) + " tokens)";
|
|
261
|
+
if (msg.cost) tip += " · $" + msg.cost.toFixed(4);
|
|
262
|
+
if (msg.model && msg.model !== "-") tip += " · " + msg.model;
|
|
263
|
+
chip.title = tip;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function handlePaneMessage(event) {
|
|
267
|
+
if (event.origin !== window.location.origin) return;
|
|
268
|
+
var msg = event.data;
|
|
269
|
+
if (!msg || msg.type !== "clay-pane-context" || !host) return;
|
|
270
|
+
var frames = host.querySelectorAll(".split-pane-frame");
|
|
271
|
+
for (var i = 0; i < frames.length; i++) {
|
|
272
|
+
if (frames[i].contentWindow === event.source) {
|
|
273
|
+
updatePaneContextChip(frames[i].closest(".split-pane"), msg);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
var renderedPaneKey = null;
|
|
280
|
+
|
|
281
|
+
function paneKey(panes) {
|
|
282
|
+
return panes.map(function (pane) { return pane.slug + "#" + pane.sessionId; }).join("|");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function renderSplit(split) {
|
|
286
|
+
if (!host || !nativeApp) return;
|
|
287
|
+
var panes = split && split.panes;
|
|
288
|
+
if (!panes || panes.length !== 2) {
|
|
289
|
+
placeStickyNotesOverlay(false);
|
|
290
|
+
host.innerHTML = "";
|
|
291
|
+
renderedPaneKey = null;
|
|
292
|
+
host.classList.remove("visible");
|
|
293
|
+
nativeApp.classList.remove("split-native-hidden");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
// splitPanes is replaced (same panes, new object) when the server confirms
|
|
297
|
+
// the groupId. Rebuilding then would reload both iframes, so skip DOM work
|
|
298
|
+
// when the rendered pane set is unchanged.
|
|
299
|
+
var key = paneKey(panes);
|
|
300
|
+
placeStickyNotesOverlay(true);
|
|
301
|
+
if (key === renderedPaneKey && host.classList.contains("visible")) return;
|
|
302
|
+
host.innerHTML = "";
|
|
303
|
+
renderedPaneKey = key;
|
|
304
|
+
nativeApp.classList.add("split-native-hidden");
|
|
305
|
+
host.classList.add("visible");
|
|
306
|
+
for (var i = 0; i < panes.length; i++) host.appendChild(createPane(panes[i], i));
|
|
307
|
+
syncPairChrome(host, split);
|
|
308
|
+
refreshIcons();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function openSplit(side, draggedId) {
|
|
312
|
+
var currentId = store.get('activeSessionId');
|
|
313
|
+
var grouped = groupedSessionIds(store.get('splitGroups'));
|
|
314
|
+
if (!currentId || !draggedId || currentId === draggedId || store.get('splitPanes')) {
|
|
315
|
+
hideDropOverlay();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (grouped.has(currentId) || grouped.has(draggedId)) {
|
|
319
|
+
hideDropOverlay();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
var current = paneForSession(currentId);
|
|
323
|
+
var dragged = paneForSession(draggedId);
|
|
324
|
+
var panes = side === "left" ? [dragged, current] : [current, dragged];
|
|
325
|
+
hideDropOverlay();
|
|
326
|
+
// The TUI host is position:fixed on document.body, so hiding #app does not
|
|
327
|
+
// hide it. Detach before showing panes; exiting the split re-attaches via
|
|
328
|
+
// the switch_session -> session_switched path.
|
|
329
|
+
detachTuiView();
|
|
330
|
+
store.set({ splitPanes: { groupId: null, panes: panes } });
|
|
331
|
+
var ws = getWs();
|
|
332
|
+
if (ws && ws.readyState === 1) {
|
|
333
|
+
ws.send(JSON.stringify({ type: "split_group_create", members: [panes[0].sessionId, panes[1].sessionId] }));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function openGroup(group) {
|
|
338
|
+
if (!group || !Array.isArray(group.members) || group.members.length !== 2) return false;
|
|
339
|
+
if (!sessionById(group.members[0]) || !sessionById(group.members[1])) return false;
|
|
340
|
+
detachTuiView();
|
|
341
|
+
store.set({
|
|
342
|
+
splitPanes: {
|
|
343
|
+
groupId: group.id,
|
|
344
|
+
panes: [paneForSession(group.members[0]), paneForSession(group.members[1])],
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
// Anchor the parent's active session (and server-side presence) to a
|
|
348
|
+
// member while the split is open, so a hard refresh restores into this
|
|
349
|
+
// group instead of whatever was viewed before it.
|
|
350
|
+
var activeId = store.get('activeSessionId');
|
|
351
|
+
if (activeId !== group.members[0] && activeId !== group.members[1]) {
|
|
352
|
+
var ws = getWs();
|
|
353
|
+
if (ws && ws.readyState === 1) {
|
|
354
|
+
ws.send(JSON.stringify({ type: "switch_session", id: group.members[0] }));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
dismissSplitOverlays();
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Reload survival: when the restored active session turns out to be a split
|
|
362
|
+
// group member, reopen that group. Safe to call often -- no-ops unless a
|
|
363
|
+
// group member is active natively with no split open.
|
|
364
|
+
export function maybeRestoreSplitGroup() {
|
|
365
|
+
if (store.get('paneMode') || store.get('splitPanes')) return;
|
|
366
|
+
var activeId = store.get('activeSessionId');
|
|
367
|
+
if (!activeId) return;
|
|
368
|
+
var groups = store.get('splitGroups') || [];
|
|
369
|
+
for (var i = 0; i < groups.length; i++) {
|
|
370
|
+
var members = groups[i].members || [];
|
|
371
|
+
if (members.indexOf(activeId) !== -1) {
|
|
372
|
+
openGroup(groups[i]);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function separateGroup(group) {
|
|
379
|
+
if (!group || !Array.isArray(group.members) || group.members.length !== 2) return;
|
|
380
|
+
var ws = getWs();
|
|
381
|
+
if (ws && ws.readyState === 1) {
|
|
382
|
+
ws.send(JSON.stringify({ type: "split_group_dissolve", id: group.id }));
|
|
383
|
+
}
|
|
384
|
+
var split = store.get('splitPanes');
|
|
385
|
+
if (split && split.groupId === group.id) switchNativeSession(group.members[0]);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function dismissSplitOverlays() {
|
|
389
|
+
hideDropOverlay();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function overlaySide(event) {
|
|
393
|
+
var rect = dropOverlay.getBoundingClientRect();
|
|
394
|
+
return (event.clientX - rect.left) < rect.width / 2 ? "left" : "right";
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function createDropOverlay(mainPanels) {
|
|
398
|
+
var overlay = document.createElement("div");
|
|
399
|
+
overlay.className = "split-drop-overlay";
|
|
400
|
+
|
|
401
|
+
var ghost = document.createElement("div");
|
|
402
|
+
ghost.className = "split-drop-ghost";
|
|
403
|
+
ghostTitleEl = document.createElement("span");
|
|
404
|
+
ghostTitleEl.className = "split-drop-ghost-title";
|
|
405
|
+
ghost.appendChild(ghostTitleEl);
|
|
406
|
+
overlay.appendChild(ghost);
|
|
407
|
+
|
|
408
|
+
overlay.addEventListener("dragover", function (event) {
|
|
409
|
+
event.preventDefault();
|
|
410
|
+
event.dataTransfer.dropEffect = "move";
|
|
411
|
+
setPreviewSide(overlaySide(event));
|
|
412
|
+
});
|
|
413
|
+
overlay.addEventListener("dragleave", function (event) {
|
|
414
|
+
if (!event.relatedTarget || !overlay.contains(event.relatedTarget)) setPreviewSide(null);
|
|
415
|
+
});
|
|
416
|
+
overlay.addEventListener("drop", function (event) {
|
|
417
|
+
event.preventDefault();
|
|
418
|
+
event.stopPropagation();
|
|
419
|
+
var side = overlaySide(event);
|
|
420
|
+
var droppedId = draggedSessionId || parseInt(event.dataTransfer.getData("text/plain"), 10);
|
|
421
|
+
openSplit(side, droppedId);
|
|
422
|
+
});
|
|
423
|
+
mainPanels.appendChild(overlay);
|
|
424
|
+
return overlay;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function handleSessionDragStart(event) {
|
|
428
|
+
if (store.get('splitPanes')) return;
|
|
429
|
+
var item = event.target.closest("[data-session-id][draggable='true']");
|
|
430
|
+
if (!item) return;
|
|
431
|
+
draggedSessionId = parseInt(item.dataset.sessionId, 10);
|
|
432
|
+
if (groupedSessionIds(store.get('splitGroups')).has(draggedSessionId)) {
|
|
433
|
+
draggedSessionId = null;
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (draggedSessionId) showDropOverlay();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function handleSidebarSessionClick(event) {
|
|
440
|
+
if (!store.get('splitPanes') || event.button !== 0) return;
|
|
441
|
+
if (event.target.closest(".session-close-btn, .session-more-btn")) return;
|
|
442
|
+
var item = event.target.closest(".session-item[data-session-id], .session-loop-child[data-session-id]");
|
|
443
|
+
if (!item) return;
|
|
444
|
+
var sessionId = parseInt(item.dataset.sessionId, 10);
|
|
445
|
+
if (!sessionId) return;
|
|
446
|
+
event.preventDefault();
|
|
447
|
+
event.stopImmediatePropagation();
|
|
448
|
+
switchNativeSession(sessionId);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function initSplitView() {
|
|
452
|
+
if (store.get('paneMode')) return;
|
|
453
|
+
var mainPanels = document.getElementById("main-panels");
|
|
454
|
+
nativeApp = document.getElementById("app");
|
|
455
|
+
if (!mainPanels || !nativeApp) return;
|
|
456
|
+
mainPanelsEl = mainPanels;
|
|
457
|
+
stickyNotesContainer = document.getElementById("sticky-notes-container");
|
|
458
|
+
if (stickyNotesContainer) {
|
|
459
|
+
stickyNotesHome = stickyNotesContainer.parentNode;
|
|
460
|
+
stickyNotesAnchor = stickyNotesContainer.nextSibling;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
host = document.createElement("div");
|
|
464
|
+
host.id = "split-host";
|
|
465
|
+
mainPanels.insertBefore(host, nativeApp.nextSibling);
|
|
466
|
+
dropOverlay = createDropOverlay(mainPanels);
|
|
467
|
+
|
|
468
|
+
window.addEventListener("message", handlePaneMessage);
|
|
469
|
+
document.addEventListener("dragstart", handleSessionDragStart);
|
|
470
|
+
document.addEventListener("dragend", hideDropOverlay);
|
|
471
|
+
document.addEventListener("drop", function (event) {
|
|
472
|
+
if (dropOverlay && !dropOverlay.contains(event.target)) hideDropOverlay();
|
|
473
|
+
});
|
|
474
|
+
document.addEventListener("click", handleSidebarSessionClick, true);
|
|
475
|
+
store.subscribe(function (state, prev) {
|
|
476
|
+
if (state.splitPanes !== prev.splitPanes) renderSplit(state.splitPanes);
|
|
477
|
+
// Switching to a session outside the open split (new_session, palette,
|
|
478
|
+
// notification click) closes the split UI; the group itself persists.
|
|
479
|
+
if (state.activeSessionId !== prev.activeSessionId && state.splitPanes && state.splitPanes.panes) {
|
|
480
|
+
var sp = state.splitPanes.panes;
|
|
481
|
+
if (state.activeSessionId !== sp[0].sessionId && state.activeSessionId !== sp[1].sessionId) {
|
|
482
|
+
store.set({ splitPanes: null });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (state.splitGroups !== prev.splitGroups) {
|
|
486
|
+
var split = state.splitPanes;
|
|
487
|
+
if (!split || !split.panes) return;
|
|
488
|
+
if (split.groupId) {
|
|
489
|
+
var stillExists = state.splitGroups.some(function (group) { return group.id === split.groupId; });
|
|
490
|
+
if (!stillExists) switchNativeSession(split.panes[0].sessionId);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
var ids = [split.panes[0].sessionId, split.panes[1].sessionId];
|
|
494
|
+
var confirmed = findSplitGroup(state.splitGroups, ids);
|
|
495
|
+
if (confirmed) store.set({ splitPanes: { groupId: confirmed.id, panes: split.panes } });
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
renderSplit(store.get('splitPanes'));
|
|
499
|
+
}
|
|
@@ -374,23 +374,25 @@ function renderNote(data) {
|
|
|
374
374
|
|
|
375
375
|
function setupDrag(noteEl, spacerEl, noteId) {
|
|
376
376
|
var dragging = false;
|
|
377
|
+
var pointerId = null;
|
|
377
378
|
var startX, startY, origX, origY;
|
|
378
379
|
|
|
379
|
-
spacerEl.addEventListener("
|
|
380
|
-
if (e.button !== 0) return;
|
|
380
|
+
spacerEl.addEventListener("pointerdown", function (e) {
|
|
381
|
+
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
381
382
|
e.preventDefault();
|
|
382
383
|
dragging = true;
|
|
384
|
+
pointerId = e.pointerId;
|
|
383
385
|
startX = e.clientX;
|
|
384
386
|
startY = e.clientY;
|
|
385
387
|
origX = parseInt(noteEl.style.left) || 0;
|
|
386
388
|
origY = parseInt(noteEl.style.top) || 0;
|
|
387
389
|
noteEl.classList.add("dragging");
|
|
388
|
-
document.
|
|
389
|
-
|
|
390
|
+
document.body.classList.add("sticky-note-interacting");
|
|
391
|
+
spacerEl.setPointerCapture(pointerId);
|
|
390
392
|
});
|
|
391
393
|
|
|
392
394
|
function onMove(e) {
|
|
393
|
-
if (!dragging) return;
|
|
395
|
+
if (!dragging || e.pointerId !== pointerId) return;
|
|
394
396
|
var dx = e.clientX - startX;
|
|
395
397
|
var dy = e.clientY - startY;
|
|
396
398
|
var c = clampPos(origX + dx, origY + dy, noteEl.offsetWidth, noteEl.offsetHeight);
|
|
@@ -398,80 +400,51 @@ function setupDrag(noteEl, spacerEl, noteId) {
|
|
|
398
400
|
noteEl.style.top = c.y + "px";
|
|
399
401
|
}
|
|
400
402
|
|
|
401
|
-
function onUp() {
|
|
402
|
-
if (!dragging) return;
|
|
403
|
+
function onUp(e) {
|
|
404
|
+
if (!dragging || (e && e.pointerId !== pointerId)) return;
|
|
403
405
|
dragging = false;
|
|
404
406
|
noteEl.classList.remove("dragging");
|
|
405
|
-
document.
|
|
406
|
-
|
|
407
|
+
document.body.classList.remove("sticky-note-interacting");
|
|
408
|
+
if (pointerId !== null && spacerEl.hasPointerCapture(pointerId)) spacerEl.releasePointerCapture(pointerId);
|
|
409
|
+
pointerId = null;
|
|
407
410
|
debouncedUpdate(noteId, {
|
|
408
411
|
x: parseInt(noteEl.style.left),
|
|
409
412
|
y: parseInt(noteEl.style.top),
|
|
410
413
|
}, 200);
|
|
411
414
|
}
|
|
412
415
|
|
|
413
|
-
|
|
414
|
-
spacerEl.addEventListener("
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
dragging = true;
|
|
418
|
-
startX = touch.clientX;
|
|
419
|
-
startY = touch.clientY;
|
|
420
|
-
origX = parseInt(noteEl.style.left) || 0;
|
|
421
|
-
origY = parseInt(noteEl.style.top) || 0;
|
|
422
|
-
noteEl.classList.add("dragging");
|
|
423
|
-
document.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
424
|
-
document.addEventListener("touchend", onTouchEnd);
|
|
425
|
-
}, { passive: true });
|
|
426
|
-
|
|
427
|
-
function onTouchMove(e) {
|
|
428
|
-
if (!dragging) return;
|
|
429
|
-
e.preventDefault();
|
|
430
|
-
var touch = e.touches[0];
|
|
431
|
-
var dx = touch.clientX - startX;
|
|
432
|
-
var dy = touch.clientY - startY;
|
|
433
|
-
var c = clampPos(origX + dx, origY + dy, noteEl.offsetWidth, noteEl.offsetHeight);
|
|
434
|
-
noteEl.style.left = c.x + "px";
|
|
435
|
-
noteEl.style.top = c.y + "px";
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function onTouchEnd() {
|
|
439
|
-
if (!dragging) return;
|
|
440
|
-
dragging = false;
|
|
441
|
-
noteEl.classList.remove("dragging");
|
|
442
|
-
document.removeEventListener("touchmove", onTouchMove);
|
|
443
|
-
document.removeEventListener("touchend", onTouchEnd);
|
|
444
|
-
debouncedUpdate(noteId, {
|
|
445
|
-
x: parseInt(noteEl.style.left),
|
|
446
|
-
y: parseInt(noteEl.style.top),
|
|
447
|
-
}, 200);
|
|
448
|
-
}
|
|
416
|
+
spacerEl.addEventListener("pointermove", onMove);
|
|
417
|
+
spacerEl.addEventListener("pointerup", onUp);
|
|
418
|
+
spacerEl.addEventListener("pointercancel", onUp);
|
|
419
|
+
spacerEl.addEventListener("lostpointercapture", onUp);
|
|
449
420
|
}
|
|
450
421
|
|
|
451
422
|
// --- Resize ---
|
|
452
423
|
|
|
453
424
|
function setupResize(noteEl, handle, noteId) {
|
|
454
425
|
var resizing = false;
|
|
426
|
+
var pointerId = null;
|
|
455
427
|
var startX, startY, origW, origH;
|
|
456
428
|
var MIN_W = 160;
|
|
457
429
|
var MIN_H = 80;
|
|
458
430
|
|
|
459
|
-
handle.addEventListener("
|
|
460
|
-
if (e.button !== 0) return;
|
|
431
|
+
handle.addEventListener("pointerdown", function (e) {
|
|
432
|
+
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
461
433
|
e.preventDefault();
|
|
462
434
|
e.stopPropagation();
|
|
463
435
|
resizing = true;
|
|
436
|
+
pointerId = e.pointerId;
|
|
464
437
|
startX = e.clientX;
|
|
465
438
|
startY = e.clientY;
|
|
466
439
|
origW = noteEl.offsetWidth;
|
|
467
440
|
origH = noteEl.offsetHeight;
|
|
468
441
|
noteEl.classList.add("resizing");
|
|
469
|
-
document.
|
|
470
|
-
|
|
442
|
+
document.body.classList.add("sticky-note-interacting");
|
|
443
|
+
handle.setPointerCapture(pointerId);
|
|
471
444
|
});
|
|
472
445
|
|
|
473
446
|
function onMove(e) {
|
|
474
|
-
if (!resizing) return;
|
|
447
|
+
if (!resizing || e.pointerId !== pointerId) return;
|
|
475
448
|
var rawW = Math.max(MIN_W, origW + (e.clientX - startX));
|
|
476
449
|
var rawH = Math.max(MIN_H, origH + (e.clientY - startY));
|
|
477
450
|
var cs = clampSize(parseInt(noteEl.style.left) || 0, parseInt(noteEl.style.top) || 0, rawW, rawH);
|
|
@@ -479,55 +452,23 @@ function setupResize(noteEl, handle, noteId) {
|
|
|
479
452
|
noteEl.style.height = Math.max(MIN_H, cs.h) + "px";
|
|
480
453
|
}
|
|
481
454
|
|
|
482
|
-
function onUp() {
|
|
483
|
-
if (!resizing) return;
|
|
455
|
+
function onUp(e) {
|
|
456
|
+
if (!resizing || (e && e.pointerId !== pointerId)) return;
|
|
484
457
|
resizing = false;
|
|
485
458
|
noteEl.classList.remove("resizing");
|
|
486
|
-
document.
|
|
487
|
-
|
|
459
|
+
document.body.classList.remove("sticky-note-interacting");
|
|
460
|
+
if (pointerId !== null && handle.hasPointerCapture(pointerId)) handle.releasePointerCapture(pointerId);
|
|
461
|
+
pointerId = null;
|
|
488
462
|
debouncedUpdate(noteId, {
|
|
489
463
|
w: noteEl.offsetWidth,
|
|
490
464
|
h: noteEl.offsetHeight,
|
|
491
465
|
}, 200);
|
|
492
466
|
}
|
|
493
467
|
|
|
494
|
-
|
|
495
|
-
handle.addEventListener("
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
var touch = e.touches[0];
|
|
499
|
-
resizing = true;
|
|
500
|
-
startX = touch.clientX;
|
|
501
|
-
startY = touch.clientY;
|
|
502
|
-
origW = noteEl.offsetWidth;
|
|
503
|
-
origH = noteEl.offsetHeight;
|
|
504
|
-
noteEl.classList.add("resizing");
|
|
505
|
-
document.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
506
|
-
document.addEventListener("touchend", onTouchEnd);
|
|
507
|
-
}, { passive: true });
|
|
508
|
-
|
|
509
|
-
function onTouchMove(e) {
|
|
510
|
-
if (!resizing) return;
|
|
511
|
-
e.preventDefault();
|
|
512
|
-
var touch = e.touches[0];
|
|
513
|
-
var rawW = Math.max(MIN_W, origW + (touch.clientX - startX));
|
|
514
|
-
var rawH = Math.max(MIN_H, origH + (touch.clientY - startY));
|
|
515
|
-
var cs = clampSize(parseInt(noteEl.style.left) || 0, parseInt(noteEl.style.top) || 0, rawW, rawH);
|
|
516
|
-
noteEl.style.width = Math.max(MIN_W, cs.w) + "px";
|
|
517
|
-
noteEl.style.height = Math.max(MIN_H, cs.h) + "px";
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function onTouchEnd() {
|
|
521
|
-
if (!resizing) return;
|
|
522
|
-
resizing = false;
|
|
523
|
-
noteEl.classList.remove("resizing");
|
|
524
|
-
document.removeEventListener("touchmove", onTouchMove);
|
|
525
|
-
document.removeEventListener("touchend", onTouchEnd);
|
|
526
|
-
debouncedUpdate(noteId, {
|
|
527
|
-
w: noteEl.offsetWidth,
|
|
528
|
-
h: noteEl.offsetHeight,
|
|
529
|
-
}, 200);
|
|
530
|
-
}
|
|
468
|
+
handle.addEventListener("pointermove", onMove);
|
|
469
|
+
handle.addEventListener("pointerup", onUp);
|
|
470
|
+
handle.addEventListener("pointercancel", onUp);
|
|
471
|
+
handle.addEventListener("lostpointercapture", onUp);
|
|
531
472
|
}
|
|
532
473
|
|
|
533
474
|
// --- Text edit (contenteditable) ---
|