clay-server 5.0.0-beta.2 → 5.0.0-beta.4
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 +9 -0
- package/lib/new-session-default.js +45 -0
- package/lib/project-connection.js +4 -2
- package/lib/project-session-creation.js +107 -0
- package/lib/project-session-delete.js +21 -3
- package/lib/project-session-folder-delete.js +169 -0
- package/lib/project-session-folders.js +272 -0
- package/lib/project-sessions.js +46 -26
- package/lib/project.js +2 -0
- package/lib/public/app.js +12 -0
- package/lib/public/css/filebrowser.css +3 -3
- package/lib/public/css/fonts.css +6 -0
- package/lib/public/css/home-hub.css +52 -0
- package/lib/public/css/mates.css +1 -1
- package/lib/public/css/mobile-nav.css +0 -128
- package/lib/public/css/session-folders.css +402 -0
- package/lib/public/css/sidebar.css +28 -545
- package/lib/public/index.html +13 -22
- package/lib/public/modules/app-favicon.js +5 -4
- package/lib/public/modules/app-home-hub.js +60 -7
- package/lib/public/modules/app-messages.js +13 -0
- package/lib/public/modules/home-project-empty-access.js +44 -0
- package/lib/public/modules/input.js +3 -0
- package/lib/public/modules/project-activation.js +11 -0
- package/lib/public/modules/project-switcher.js +4 -4
- package/lib/public/modules/session-create-form.js +428 -0
- package/lib/public/modules/session-create.js +340 -0
- package/lib/public/modules/session-date-group-header.js +57 -0
- package/lib/public/modules/session-date-groups.js +84 -0
- package/lib/public/modules/session-folder-collapse.js +192 -0
- package/lib/public/modules/session-folder-context.js +215 -0
- package/lib/public/modules/session-folder-delete.js +339 -0
- package/lib/public/modules/session-folder-dialogs.js +173 -0
- package/lib/public/modules/session-folder-dnd.js +214 -0
- package/lib/public/modules/session-folder-draft.js +184 -0
- package/lib/public/modules/session-folder-layout.js +133 -0
- package/lib/public/modules/session-folder-toolbar.js +185 -0
- package/lib/public/modules/session-folder-view-menu.js +187 -0
- package/lib/public/modules/session-folders.js +97 -0
- package/lib/public/modules/session-list-search.js +108 -0
- package/lib/public/modules/session-presence-ui.js +44 -0
- package/lib/public/modules/sidebar-creation-effect.js +130 -0
- package/lib/public/modules/sidebar-mobile.js +39 -182
- package/lib/public/modules/sidebar-session-folders-ui.js +394 -0
- package/lib/public/modules/sidebar-sessions.js +142 -718
- package/lib/public/modules/sidebar.js +30 -7
- package/lib/public/modules/terminal.js +2 -2
- package/lib/public/modules/tool-palette.js +37 -219
- package/lib/public/style.css +1 -0
- package/lib/server.js +4 -0
- package/lib/session-folders.js +414 -0
- package/lib/sessions.js +7 -5
- package/lib/users-session-folder-preferences.js +69 -0
- package/lib/users.js +6 -0
- package/lib/ws-schema.js +11 -0
- package/package.json +1 -1
- package/lib/public/modules/session-creation-cta.js +0 -46
- package/lib/public/modules/tool-palette-overlays.js +0 -159
|
@@ -2,7 +2,6 @@ import { appendGithubWork } from './session-github.js';
|
|
|
2
2
|
// sidebar-sessions.js - Session list, search, presence, countdown, CLI picker
|
|
3
3
|
// Extracted from sidebar.js (PR-35)
|
|
4
4
|
|
|
5
|
-
import { avatarUrl, userAvatarUrl } from './avatar.js';
|
|
6
5
|
import { escapeHtml } from './utils.js';
|
|
7
6
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
8
7
|
import { openSearch as openSessionSearch } from './session-search.js';
|
|
@@ -13,15 +12,21 @@ import { dismissOverlayPanels, closeSidebar, updatePageTitle, spawnDustParticles
|
|
|
13
12
|
import { showConfirm } from './app-misc.js';
|
|
14
13
|
import { getUpcomingSchedules } from './scheduler.js';
|
|
15
14
|
import { refreshMobileChatSheet } from './sidebar-mobile.js';
|
|
16
|
-
import { VENDOR_AVATARS, VENDOR_NAMES
|
|
15
|
+
import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
|
|
17
16
|
import { openGroup, separateGroup } from './split-view.js';
|
|
18
17
|
import { groupedSessionIds } from './split-group-helpers.js';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
import { resolvePreferredVendor } from './default-vendor.js';
|
|
19
|
+
import {
|
|
20
|
+
canOrganizeSession, computeLayout, createStarButton, isFavoriteSession, openMoveToFolder, renderFavoritesSection,
|
|
21
|
+
renderOrdinarySections, setupFolderDrag, toggleFavorite
|
|
22
|
+
} from './sidebar-session-folders-ui.js';
|
|
23
|
+
import { renderFolderToolbar, captureFolderInputs, folderToolbarNeedsRender } from './session-folder-toolbar.js';
|
|
24
|
+
import { captureFolderCollapseAnimations } from './session-folder-collapse.js';
|
|
25
|
+
import { getListSearchQuery, getListSearchMatchIds, applyListSearchResults, filterItemsBySearch } from './session-list-search.js';
|
|
26
|
+
import { expectCreatedSession } from './session-create.js';
|
|
27
|
+
import { describeSessionDelete } from './session-delete-scope.js';
|
|
28
|
+
import { renderSessionPresence, setSessionPresence } from './session-presence-ui.js';
|
|
23
29
|
import {
|
|
24
|
-
hierarchyItemMatches,
|
|
25
30
|
prepareSidebarHierarchy,
|
|
26
31
|
renderDesktopDriverHierarchy,
|
|
27
32
|
renderDesktopOrphanHierarchy
|
|
@@ -30,15 +35,9 @@ import {
|
|
|
30
35
|
|
|
31
36
|
// --- Session state ---
|
|
32
37
|
var cachedSessions = [];
|
|
33
|
-
var searchQuery = "";
|
|
34
|
-
var searchMatchIds = null; // null = no search, Set of matched session IDs
|
|
35
|
-
var searchDebounce = null;
|
|
36
38
|
var expandedLoopGroups = new Set();
|
|
37
39
|
var expandedLoopRuns = new Set();
|
|
38
40
|
|
|
39
|
-
// --- Session presence (multi-user: who is viewing which session) ---
|
|
40
|
-
var sessionPresence = {}; // { sessionId: [{ id, displayName, avatarStyle, avatarSeed }] }
|
|
41
|
-
|
|
42
41
|
// --- Countdown timer for upcoming schedules ---
|
|
43
42
|
var countdownTimer = null;
|
|
44
43
|
var countdownContainer = null;
|
|
@@ -47,146 +46,17 @@ var countdownContainer = null;
|
|
|
47
46
|
var sessionCtxMenu = null;
|
|
48
47
|
var sessionCtxMenuAnchor = null;
|
|
49
48
|
var sessionCtxSessionId = null;
|
|
50
|
-
var draggedSessionId = null;
|
|
51
|
-
var draggedSessionBookmarked = false;
|
|
52
|
-
var headerSearchOpen = false;
|
|
53
49
|
var armedDeleteSessionId = null;
|
|
54
50
|
var armedDeleteTimer = null;
|
|
55
51
|
|
|
56
52
|
store.subscribe(function (state, previous) {
|
|
57
|
-
if (state.
|
|
58
|
-
var reopenNewMenu = !!(sessionCtxMenu && sessionCtxMenuAnchor);
|
|
59
|
-
var sessionList = getSessionListEl();
|
|
60
|
-
if (reopenNewMenu) closeSessionCtxMenu();
|
|
61
|
-
if (sessionList) renderSessionList();
|
|
62
|
-
if (reopenNewMenu && sessionList) {
|
|
63
|
-
var freshAnchor = sessionList.querySelector(".session-top-action.split-provider");
|
|
64
|
-
if (freshAnchor) showNewSessionMenu(freshAnchor, resolveDefaultVendor());
|
|
65
|
-
}
|
|
66
|
-
}
|
|
53
|
+
if ((state.sessionFolders !== previous.sessionFolders || folderToolbarNeedsRender(state, previous)) && getSessionListEl()) renderSessionList(null);
|
|
67
54
|
});
|
|
68
55
|
|
|
69
|
-
function sendSessionBookmark(sessionId, bookmarked) {
|
|
70
|
-
if (getWs() && store.get('connected')) {
|
|
71
|
-
getWs().send(JSON.stringify({ type: "set_session_bookmark", sessionId: sessionId, bookmarked: !!bookmarked }));
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function compareSessionListItems(a, b) {
|
|
76
|
-
var aData = a && (a.type === "session" || a.type === "driver-hierarchy") ? a.data : a;
|
|
77
|
-
var bData = b && (b.type === "session" || b.type === "driver-hierarchy") ? b.data : b;
|
|
78
|
-
var aBookmarked = !!(aData && aData.bookmarked);
|
|
79
|
-
var bBookmarked = !!(bData && bData.bookmarked);
|
|
80
|
-
if (aBookmarked !== bBookmarked) return aBookmarked ? -1 : 1;
|
|
81
|
-
if (aBookmarked && bBookmarked) {
|
|
82
|
-
var ao = aData && typeof aData.favoriteOrder === "number" ? aData.favoriteOrder : Number.MAX_SAFE_INTEGER;
|
|
83
|
-
var bo = bData && typeof bData.favoriteOrder === "number" ? bData.favoriteOrder : Number.MAX_SAFE_INTEGER;
|
|
84
|
-
if (ao !== bo) return ao - bo;
|
|
85
|
-
}
|
|
86
|
-
return (b.lastActivity || 0) - (a.lastActivity || 0);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function clearSessionDragIndicators() {
|
|
90
|
-
var listEl = getSessionListEl();
|
|
91
|
-
if (!listEl) return;
|
|
92
|
-
var active = listEl.querySelectorAll(".session-favorites-divider.drag-hover, .session-regular-drop.drag-hover, .session-item.dragging");
|
|
93
|
-
for (var i = 0; i < active.length; i++) {
|
|
94
|
-
active[i].classList.remove("drag-hover", "dragging");
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function setupSessionDragHandlers(el, session) {
|
|
99
|
-
el.setAttribute("draggable", "true");
|
|
100
|
-
|
|
101
|
-
el.addEventListener("dragstart", function (e) {
|
|
102
|
-
draggedSessionId = session.id;
|
|
103
|
-
draggedSessionBookmarked = !!session.bookmarked;
|
|
104
|
-
e.dataTransfer.effectAllowed = "move";
|
|
105
|
-
e.dataTransfer.setData("text/plain", String(session.id));
|
|
106
|
-
|
|
107
|
-
var ghost = document.createElement("div");
|
|
108
|
-
ghost.textContent = session.title || "New Session";
|
|
109
|
-
ghost.style.cssText = "position:fixed;left:-200px;top:-200px;max-width:220px;padding:8px 12px;border-radius:10px;" +
|
|
110
|
-
"background:var(--sidebar-active);color:var(--text);font-size:13px;font-weight:600;pointer-events:none;z-index:-1;";
|
|
111
|
-
document.body.appendChild(ghost);
|
|
112
|
-
e.dataTransfer.setDragImage(ghost, 18, 18);
|
|
113
|
-
setTimeout(function () { ghost.remove(); }, 0);
|
|
114
|
-
|
|
115
|
-
setTimeout(function () { el.classList.add("dragging"); }, 0);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
el.addEventListener("dragend", function () {
|
|
119
|
-
clearSessionDragIndicators();
|
|
120
|
-
draggedSessionId = null;
|
|
121
|
-
draggedSessionBookmarked = false;
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
if (session.bookmarked) {
|
|
125
|
-
el.addEventListener("dragover", function (e) {
|
|
126
|
-
if (!draggedSessionId || draggedSessionId === session.id) return;
|
|
127
|
-
e.preventDefault();
|
|
128
|
-
e.dataTransfer.dropEffect = "move";
|
|
129
|
-
var rect = el.getBoundingClientRect();
|
|
130
|
-
var insertBefore = e.clientY < rect.top + rect.height / 2;
|
|
131
|
-
el.classList.remove("drag-over-above", "drag-over-below");
|
|
132
|
-
el.classList.add(insertBefore ? "drag-over-above" : "drag-over-below");
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
el.addEventListener("dragleave", function () {
|
|
136
|
-
el.classList.remove("drag-over-above", "drag-over-below");
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
el.addEventListener("drop", function (e) {
|
|
140
|
-
if (!draggedSessionId || draggedSessionId === session.id) return;
|
|
141
|
-
e.preventDefault();
|
|
142
|
-
var rect = el.getBoundingClientRect();
|
|
143
|
-
var insertBefore = e.clientY < rect.top + rect.height / 2;
|
|
144
|
-
el.classList.remove("drag-over-above", "drag-over-below");
|
|
145
|
-
if (draggedSessionBookmarked) {
|
|
146
|
-
if (getWs() && store.get('connected')) {
|
|
147
|
-
getWs().send(JSON.stringify({
|
|
148
|
-
type: "reorder_session_bookmarks",
|
|
149
|
-
sourceId: draggedSessionId,
|
|
150
|
-
targetId: session.id,
|
|
151
|
-
insertBefore: insertBefore,
|
|
152
|
-
}));
|
|
153
|
-
}
|
|
154
|
-
} else {
|
|
155
|
-
sendSessionBookmark(draggedSessionId, true);
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function setupBookmarkDropTarget(el, bookmarked) {
|
|
162
|
-
el.addEventListener("dragover", function (e) {
|
|
163
|
-
if (!draggedSessionId) return;
|
|
164
|
-
e.preventDefault();
|
|
165
|
-
e.dataTransfer.dropEffect = "move";
|
|
166
|
-
el.classList.add("drag-hover");
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
el.addEventListener("dragleave", function () {
|
|
170
|
-
el.classList.remove("drag-hover");
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
el.addEventListener("drop", function (e) {
|
|
174
|
-
if (!draggedSessionId) return;
|
|
175
|
-
e.preventDefault();
|
|
176
|
-
el.classList.remove("drag-hover");
|
|
177
|
-
if (draggedSessionBookmarked !== !!bookmarked) {
|
|
178
|
-
sendSessionBookmark(draggedSessionId, !!bookmarked);
|
|
179
|
-
}
|
|
180
|
-
clearSessionDragIndicators();
|
|
181
|
-
draggedSessionId = null;
|
|
182
|
-
draggedSessionBookmarked = false;
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
56
|
function spawnSessionDeleteParticles(sessionId) {
|
|
187
57
|
if (!spawnDustParticles) return;
|
|
188
58
|
setTimeout(function () {
|
|
189
|
-
var el = getSessionListEl().querySelector('[data-session-id="' + sessionId + '"]');
|
|
59
|
+
var el = visibleSessionRow(sessionId) || getSessionListEl().querySelector('[data-session-id="' + sessionId + '"]');
|
|
190
60
|
if (!el) return;
|
|
191
61
|
var rect = el.getBoundingClientRect();
|
|
192
62
|
spawnDustParticles(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
|
@@ -209,14 +79,15 @@ function clearArmedSessionDelete() {
|
|
|
209
79
|
armedDeleteTimer = null;
|
|
210
80
|
}
|
|
211
81
|
if (armedDeleteSessionId !== null) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
82
|
+
// A session can appear twice (its folder and Favorites): reset every copy.
|
|
83
|
+
var prevBtns = getSessionListEl() ? getSessionListEl().querySelectorAll('.session-close-btn[data-session-id="' + armedDeleteSessionId + '"]') : [];
|
|
84
|
+
for (var pb = 0; pb < prevBtns.length; pb++) {
|
|
85
|
+
prevBtns[pb].classList.remove("armed");
|
|
86
|
+
prevBtns[pb].innerHTML = iconHtml("x");
|
|
87
|
+
prevBtns[pb].title = "Delete session";
|
|
88
|
+
prevBtns[pb].setAttribute("aria-label", "Delete session");
|
|
219
89
|
}
|
|
90
|
+
if (prevBtns.length) refreshIcons();
|
|
220
91
|
}
|
|
221
92
|
armedDeleteSessionId = null;
|
|
222
93
|
}
|
|
@@ -242,70 +113,6 @@ function deleteSessionImmediately(session) {
|
|
|
242
113
|
}
|
|
243
114
|
}
|
|
244
115
|
|
|
245
|
-
function collectItemSessionIds(item) {
|
|
246
|
-
if (!item) return [];
|
|
247
|
-
if (item.type === "session" && item.data && typeof item.data.id === "number") {
|
|
248
|
-
if (!isSessionVisibleBySearch(item.data.id)) return [];
|
|
249
|
-
return [item.data.id];
|
|
250
|
-
}
|
|
251
|
-
if (item.type === "loop" && Array.isArray(item.children)) {
|
|
252
|
-
var ids = [];
|
|
253
|
-
for (var i = 0; i < item.children.length; i++) {
|
|
254
|
-
if (typeof item.children[i].id === "number" && isSessionVisibleBySearch(item.children[i].id)) {
|
|
255
|
-
ids.push(item.children[i].id);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return ids;
|
|
259
|
-
}
|
|
260
|
-
if (item.type === "split-group" && Array.isArray(item.members)) {
|
|
261
|
-
return item.members.map(function (session) { return session.id; });
|
|
262
|
-
}
|
|
263
|
-
if (item.type === "driver-hierarchy" && item.root) {
|
|
264
|
-
var hierarchyIds = [item.root.driver.id];
|
|
265
|
-
for (var j = 0; j < item.root.workers.length; j++) hierarchyIds.push(item.root.workers[j].id);
|
|
266
|
-
return hierarchyIds;
|
|
267
|
-
}
|
|
268
|
-
if (item.type === "orphan-workers" && Array.isArray(item.workers)) {
|
|
269
|
-
return item.workers.map(function (session) { return session.id; });
|
|
270
|
-
}
|
|
271
|
-
return [];
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function confirmDeleteSessionGroup(groupLabel, sessionIds) {
|
|
275
|
-
if (!Array.isArray(sessionIds) || sessionIds.length === 0) return;
|
|
276
|
-
showConfirm(describeGroupDelete(groupLabel, sessionIds, cachedSessions).message, function () {
|
|
277
|
-
var ws = getWs();
|
|
278
|
-
if (ws && store.get('connected')) {
|
|
279
|
-
ws.send(JSON.stringify({ type: "bulk_delete_sessions", sessionIds: sessionIds }));
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function createSessionGroupHeader(group, sessionIds) {
|
|
285
|
-
var header = document.createElement("div");
|
|
286
|
-
header.className = "session-group-header";
|
|
287
|
-
|
|
288
|
-
var label = document.createElement("span");
|
|
289
|
-
label.className = "session-group-header-label";
|
|
290
|
-
label.textContent = group;
|
|
291
|
-
header.appendChild(label);
|
|
292
|
-
|
|
293
|
-
if ((!store.get('permissions') || store.get('permissions').sessionDelete !== false) && Array.isArray(sessionIds) && sessionIds.length > 0) {
|
|
294
|
-
var clearBtn = document.createElement("button");
|
|
295
|
-
clearBtn.className = "session-group-clear-btn";
|
|
296
|
-
clearBtn.type = "button";
|
|
297
|
-
clearBtn.textContent = "Clear";
|
|
298
|
-
clearBtn.addEventListener("click", function (e) {
|
|
299
|
-
e.preventDefault();
|
|
300
|
-
e.stopPropagation();
|
|
301
|
-
confirmDeleteSessionGroup(group, sessionIds);
|
|
302
|
-
});
|
|
303
|
-
header.appendChild(clearBtn);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return header;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
116
|
function appendSessionCloseButton(el, session) {
|
|
310
117
|
if (store.get('permissions') && store.get('permissions').sessionDelete === false) return;
|
|
311
118
|
|
|
@@ -348,13 +155,22 @@ export function resolveDefaultVendor() {
|
|
|
348
155
|
// Otherwise vendorSelectionLocked from the previous session can reject the
|
|
349
156
|
// new config_state and leave a Kiro/Codex session rendered as Claude.
|
|
350
157
|
export function startNewSession(vendor, extra) {
|
|
351
|
-
if (!getWs() || !store.get('connected')) return;
|
|
158
|
+
if (!getWs() || !store.get('connected')) return false;
|
|
352
159
|
var payload = { type: "new_session", vendor: vendor };
|
|
353
160
|
if (extra) {
|
|
354
161
|
var keys = Object.keys(extra);
|
|
355
162
|
for (var i = 0; i < keys.length; i++) payload[keys[i]] = extra[keys[i]];
|
|
356
163
|
}
|
|
357
|
-
|
|
164
|
+
try {
|
|
165
|
+
getWs().send(JSON.stringify(payload));
|
|
166
|
+
} catch (e) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
// The destination (Unfiled unless a folder was chosen) is expanded and the
|
|
170
|
+
// new session revealed once it becomes active.
|
|
171
|
+
// A creation that carries a request id is revealed from its acknowledgement,
|
|
172
|
+
// which knows where the session was really filed.
|
|
173
|
+
if (!(extra && extra.requestId)) expectCreatedSession(extra && extra.folderId ? extra.folderId : "unfiled");
|
|
358
174
|
store.set({
|
|
359
175
|
lastVendor: vendor,
|
|
360
176
|
currentVendor: vendor,
|
|
@@ -363,273 +179,9 @@ export function startNewSession(vendor, extra) {
|
|
|
363
179
|
vendorSelectionLocked: true,
|
|
364
180
|
sessionVendorBound: true,
|
|
365
181
|
});
|
|
182
|
+
return true;
|
|
366
183
|
}
|
|
367
184
|
|
|
368
|
-
export function renderSessionTopActions() {
|
|
369
|
-
var wrap = document.createElement("div");
|
|
370
|
-
wrap.className = "session-top-actions";
|
|
371
|
-
|
|
372
|
-
var defaultVendor = resolveDefaultVendor();
|
|
373
|
-
var vendorName = VENDOR_NAMES[defaultVendor] || defaultVendor;
|
|
374
|
-
var createTitle = "Create a " + vendorName + " session";
|
|
375
|
-
if (defaultVendorUnavailable(store.get('installedVendors') || [])) createTitle += " (saved default is unavailable; using installed priority)";
|
|
376
|
-
var controls;
|
|
377
|
-
controls = renderSessionCreationCta({
|
|
378
|
-
rowClass: "session-top-action-split",
|
|
379
|
-
createClass: "session-top-action split-main",
|
|
380
|
-
providerClass: "session-top-action split-provider",
|
|
381
|
-
iconClass: "session-top-action-icon",
|
|
382
|
-
vendorKey: defaultVendor,
|
|
383
|
-
vendorName: vendorName,
|
|
384
|
-
vendorAvatar: VENDOR_AVATARS[defaultVendor] || VENDOR_AVATARS.claude,
|
|
385
|
-
createTitle: createTitle,
|
|
386
|
-
providerTitle: "Switch AI provider (current: " + vendorName + ")",
|
|
387
|
-
menuId: "session-new-menu",
|
|
388
|
-
onCreate: function () { startNewSession(defaultVendor); },
|
|
389
|
-
onProvider: function (e) {
|
|
390
|
-
e.preventDefault();
|
|
391
|
-
e.stopPropagation();
|
|
392
|
-
if (sessionCtxMenu) { closeSessionCtxMenu(); return; }
|
|
393
|
-
showNewSessionMenu(controls.providerButton, defaultVendor);
|
|
394
|
-
},
|
|
395
|
-
});
|
|
396
|
-
wrap.appendChild(controls.element);
|
|
397
|
-
return wrap;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Provider disclosure anchored to the grouped provider control. Reuses the
|
|
401
|
-
// session-ctx-menu element/var so the global document click handler closes it.
|
|
402
|
-
function showNewSessionMenu(anchorBtn, defaultVendor) {
|
|
403
|
-
closeSessionCtxMenu();
|
|
404
|
-
|
|
405
|
-
var menu = document.createElement("div");
|
|
406
|
-
menu.className = "session-ctx-menu session-new-menu";
|
|
407
|
-
menu.id = "session-new-menu";
|
|
408
|
-
var installed = store.get('installedVendors') || [];
|
|
409
|
-
var defaultVendorState = store.get('defaultVendorState') || {};
|
|
410
|
-
sessionCtxMenuAnchor = anchorBtn;
|
|
411
|
-
anchorBtn.setAttribute("aria-expanded", "true");
|
|
412
|
-
|
|
413
|
-
if (defaultVendorUnavailable(installed) && defaultVendorState.preference) {
|
|
414
|
-
var fallbackNote = document.createElement("div");
|
|
415
|
-
fallbackNote.className = "session-new-preference-status session-new-fallback-note";
|
|
416
|
-
fallbackNote.textContent = "Saved default " + (VENDOR_NAMES[defaultVendorState.preference] || defaultVendorState.preference) + " is unavailable. Using " + (VENDOR_NAMES[defaultVendor] || defaultVendor) + ".";
|
|
417
|
-
menu.appendChild(fallbackNote);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Every known vendor is listed. Uninstalled ones stay visible but disabled,
|
|
421
|
-
// and clicking one opens the vendor's homepage so the user can install it.
|
|
422
|
-
for (var i = 0; i < VENDOR_ORDER.length; i++) {
|
|
423
|
-
(function (vendor) {
|
|
424
|
-
var isInstalled = installed.indexOf(vendor) !== -1;
|
|
425
|
-
var name = VENDOR_NAMES[vendor] || vendor;
|
|
426
|
-
var row = document.createElement("div");
|
|
427
|
-
row.className = "session-new-vendor-row";
|
|
428
|
-
var item = document.createElement("button");
|
|
429
|
-
item.className = "session-ctx-item session-new-vendor";
|
|
430
|
-
if (!isInstalled) item.classList.add("disabled");
|
|
431
|
-
if (vendor === defaultVendor) item.classList.add("active");
|
|
432
|
-
item.type = "button";
|
|
433
|
-
item.innerHTML = '<img src="' + VENDOR_AVATARS[vendor] + '" class="session-top-action-icon" alt="">' +
|
|
434
|
-
'<span class="session-new-vendor-name">' + (isInstalled ? "Create with " : "Learn about ") + name + '</span>' +
|
|
435
|
-
(isExperimentalVendor(vendor) ? '<span class="vendor-experimental-badge" aria-label="Experimental integration" title="Experimental integration; not yet validated through direct use testing">🧪</span>' : '') +
|
|
436
|
-
(isInstalled ? "" : '<span class="session-new-vendor-note">Not installed</span>' + iconHtml("external-link"));
|
|
437
|
-
item.title = isInstalled
|
|
438
|
-
? "New " + name + " session"
|
|
439
|
-
: name + " is not installed - open " + VENDOR_HOMEPAGES[vendor];
|
|
440
|
-
if (isExperimentalVendor(vendor)) item.title += " · Experimental integration; not yet validated through direct use testing";
|
|
441
|
-
item.addEventListener("click", function (e) {
|
|
442
|
-
e.stopPropagation();
|
|
443
|
-
closeSessionCtxMenu();
|
|
444
|
-
if (!isInstalled) {
|
|
445
|
-
window.open(VENDOR_HOMEPAGES[vendor], "_blank", "noopener");
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
startNewSession(vendor);
|
|
449
|
-
});
|
|
450
|
-
row.appendChild(item);
|
|
451
|
-
if (isInstalled) {
|
|
452
|
-
var defaultBtn = document.createElement("button");
|
|
453
|
-
defaultBtn.type = "button";
|
|
454
|
-
defaultBtn.className = "session-new-set-default";
|
|
455
|
-
defaultBtn.textContent = defaultVendorState.saving ? "Saving…" : (vendor === defaultVendor && !defaultVendorUnavailable(installed) ? "Default" : "Set default");
|
|
456
|
-
defaultBtn.disabled = defaultVendorState.saving === true;
|
|
457
|
-
defaultBtn.setAttribute("aria-label", "Set " + name + " as default vendor");
|
|
458
|
-
defaultBtn.title = "Use " + name + " for New session by default";
|
|
459
|
-
defaultBtn.addEventListener("click", function (e) {
|
|
460
|
-
e.preventDefault();
|
|
461
|
-
e.stopPropagation();
|
|
462
|
-
saveDefaultVendor(vendor);
|
|
463
|
-
});
|
|
464
|
-
row.appendChild(defaultBtn);
|
|
465
|
-
}
|
|
466
|
-
menu.appendChild(row);
|
|
467
|
-
})(VENDOR_ORDER[i]);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
var automatic = document.createElement("button");
|
|
471
|
-
automatic.className = "session-ctx-item session-new-automatic";
|
|
472
|
-
automatic.type = "button";
|
|
473
|
-
automatic.textContent = "Use automatic default";
|
|
474
|
-
automatic.disabled = defaultVendorState.saving === true;
|
|
475
|
-
automatic.title = "Use Clay's installed-vendor priority";
|
|
476
|
-
automatic.addEventListener("click", function (e) {
|
|
477
|
-
e.stopPropagation();
|
|
478
|
-
closeSessionCtxMenu();
|
|
479
|
-
saveDefaultVendor("");
|
|
480
|
-
});
|
|
481
|
-
menu.appendChild(automatic);
|
|
482
|
-
if (defaultVendorState.error) {
|
|
483
|
-
var status = document.createElement("div");
|
|
484
|
-
status.className = "session-new-preference-status";
|
|
485
|
-
status.setAttribute("role", "alert");
|
|
486
|
-
status.textContent = defaultVendorState.error;
|
|
487
|
-
menu.appendChild(status);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (installed.length > 0) {
|
|
491
|
-
var pairSep = document.createElement("div");
|
|
492
|
-
pairSep.className = "session-ctx-sep";
|
|
493
|
-
menu.appendChild(pairSep);
|
|
494
|
-
var pairItem = document.createElement("button");
|
|
495
|
-
pairItem.className = "session-ctx-item";
|
|
496
|
-
pairItem.type = "button";
|
|
497
|
-
pairItem.innerHTML = iconHtml("panel-left-close") + " <span>Pair session…</span>";
|
|
498
|
-
pairItem.title = "Create a Driver and Worker in a split view";
|
|
499
|
-
pairItem.addEventListener("click", function (e) {
|
|
500
|
-
e.stopPropagation();
|
|
501
|
-
closeSessionCtxMenu();
|
|
502
|
-
openPairDialog();
|
|
503
|
-
});
|
|
504
|
-
menu.appendChild(pairItem);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Claude-only launch mode: a bare TUI shell with permission prompts off.
|
|
508
|
-
// Hidden in GUI mode (the default) because the item only ever produces a
|
|
509
|
-
// terminal session, which contradicts what the rest of the menu creates.
|
|
510
|
-
if (installed.indexOf("claude") !== -1 && store.get('claudeOpenMode') === "tui") {
|
|
511
|
-
var sep = document.createElement("div");
|
|
512
|
-
sep.className = "session-ctx-sep";
|
|
513
|
-
menu.appendChild(sep);
|
|
514
|
-
|
|
515
|
-
var skipItem = document.createElement("button");
|
|
516
|
-
skipItem.className = "session-ctx-item";
|
|
517
|
-
skipItem.type = "button";
|
|
518
|
-
skipItem.innerHTML = iconHtml("shield-off") + " <span>Skip Permissions (TUI)</span>";
|
|
519
|
-
skipItem.title = "Start a terminal session with --dangerously-skip-permissions";
|
|
520
|
-
skipItem.addEventListener("click", function (e) {
|
|
521
|
-
e.stopPropagation();
|
|
522
|
-
closeSessionCtxMenu();
|
|
523
|
-
startNewSession("claude", { mode: "tui", dangerouslySkipPermissions: true });
|
|
524
|
-
});
|
|
525
|
-
menu.appendChild(skipItem);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
document.body.appendChild(menu);
|
|
529
|
-
sessionCtxMenu = menu;
|
|
530
|
-
menu.addEventListener("keydown", function (e) {
|
|
531
|
-
if (e.key !== "Escape") return;
|
|
532
|
-
e.preventDefault();
|
|
533
|
-
e.stopPropagation();
|
|
534
|
-
closeSessionCtxMenu(true);
|
|
535
|
-
});
|
|
536
|
-
refreshIcons();
|
|
537
|
-
|
|
538
|
-
requestAnimationFrame(function () {
|
|
539
|
-
var btnRect = anchorBtn.getBoundingClientRect();
|
|
540
|
-
menu.style.position = "fixed";
|
|
541
|
-
menu.style.top = (btnRect.bottom + 2) + "px";
|
|
542
|
-
menu.style.left = btnRect.left + "px";
|
|
543
|
-
menu.style.right = "auto";
|
|
544
|
-
var menuRect = menu.getBoundingClientRect();
|
|
545
|
-
if (menuRect.right > window.innerWidth - 8) {
|
|
546
|
-
menu.style.left = "auto";
|
|
547
|
-
menu.style.right = (window.innerWidth - btnRect.right) + "px";
|
|
548
|
-
}
|
|
549
|
-
if (menuRect.bottom > window.innerHeight - 8) {
|
|
550
|
-
menu.style.top = (btnRect.top - menuRect.height - 2) + "px";
|
|
551
|
-
}
|
|
552
|
-
var firstMenuItem = menu.querySelector("button:not(:disabled)");
|
|
553
|
-
if (firstMenuItem) firstMenuItem.focus();
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
function runSessionSearch(query) {
|
|
558
|
-
var normalizedQuery = query || "";
|
|
559
|
-
var trimmedQuery = normalizedQuery.trim();
|
|
560
|
-
searchQuery = normalizedQuery;
|
|
561
|
-
if (searchDebounce) {
|
|
562
|
-
clearTimeout(searchDebounce);
|
|
563
|
-
searchDebounce = null;
|
|
564
|
-
}
|
|
565
|
-
if (!trimmedQuery) {
|
|
566
|
-
searchMatchIds = null;
|
|
567
|
-
renderSessionList(null);
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
searchDebounce = setTimeout(function () {
|
|
571
|
-
if (getWs() && store.get('connected')) {
|
|
572
|
-
getWs().send(JSON.stringify({ type: "search_sessions", query: searchQuery }));
|
|
573
|
-
}
|
|
574
|
-
}, 200);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
function syncHeaderSearchUi() {
|
|
578
|
-
var searchInline = document.getElementById("session-header-search-inline");
|
|
579
|
-
var searchInput = document.getElementById("session-header-search-input");
|
|
580
|
-
var searchClear = document.getElementById("session-header-search-clear");
|
|
581
|
-
var searchBtn = document.getElementById("session-header-search-btn");
|
|
582
|
-
var filterCount = document.getElementById("session-filter-count");
|
|
583
|
-
var isOpen = headerSearchOpen || !!searchQuery;
|
|
584
|
-
if (!searchInline || !searchInput || !searchClear || !searchBtn || !filterCount) return;
|
|
585
|
-
searchInline.classList.toggle("hidden", !isOpen);
|
|
586
|
-
searchBtn.classList.toggle("active", isOpen);
|
|
587
|
-
if (searchInput.value !== searchQuery) {
|
|
588
|
-
searchInput.value = searchQuery;
|
|
589
|
-
}
|
|
590
|
-
searchClear.classList.toggle("hidden", !searchQuery);
|
|
591
|
-
if (!searchQuery || searchMatchIds === null) {
|
|
592
|
-
filterCount.classList.add("hidden");
|
|
593
|
-
filterCount.textContent = "";
|
|
594
|
-
} else {
|
|
595
|
-
filterCount.classList.remove("hidden");
|
|
596
|
-
filterCount.textContent = String(searchMatchIds.size);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
function openHeaderSearch() {
|
|
601
|
-
headerSearchOpen = true;
|
|
602
|
-
syncHeaderSearchUi();
|
|
603
|
-
var searchInput = document.getElementById("session-header-search-input");
|
|
604
|
-
if (searchInput) {
|
|
605
|
-
requestAnimationFrame(function () {
|
|
606
|
-
searchInput.focus();
|
|
607
|
-
searchInput.select();
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
function closeHeaderSearch() {
|
|
613
|
-
headerSearchOpen = false;
|
|
614
|
-
syncHeaderSearchUi();
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
function clearSessionSearch(shouldBlur, input, shouldClose) {
|
|
618
|
-
if (searchDebounce) {
|
|
619
|
-
clearTimeout(searchDebounce);
|
|
620
|
-
searchDebounce = null;
|
|
621
|
-
}
|
|
622
|
-
searchQuery = "";
|
|
623
|
-
searchMatchIds = null;
|
|
624
|
-
if (shouldClose) {
|
|
625
|
-
headerSearchOpen = false;
|
|
626
|
-
}
|
|
627
|
-
syncHeaderSearchUi();
|
|
628
|
-
renderSessionList(null);
|
|
629
|
-
if (shouldBlur && input) {
|
|
630
|
-
input.blur();
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
185
|
|
|
634
186
|
export function initSidebarSessions() {
|
|
635
187
|
|
|
@@ -638,63 +190,12 @@ export function initSidebarSessions() {
|
|
|
638
190
|
clearArmedSessionDelete();
|
|
639
191
|
});
|
|
640
192
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
var searchClear = document.getElementById("session-header-search-clear");
|
|
644
|
-
var searchInline = document.getElementById("session-header-search-inline");
|
|
645
|
-
|
|
646
|
-
if (searchBtn && searchInput && searchClear && searchInline) {
|
|
647
|
-
searchBtn.addEventListener("click", function () {
|
|
648
|
-
if (!headerSearchOpen && !searchQuery) {
|
|
649
|
-
openHeaderSearch();
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
if (!searchQuery) {
|
|
653
|
-
closeHeaderSearch();
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
searchInput.focus();
|
|
657
|
-
searchInput.select();
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
searchInput.addEventListener("input", function () {
|
|
661
|
-
runSessionSearch(searchInput.value);
|
|
662
|
-
syncHeaderSearchUi();
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
searchInput.addEventListener("keydown", function (e) {
|
|
666
|
-
if (e.key === "Escape") {
|
|
667
|
-
e.preventDefault();
|
|
668
|
-
if (searchInput.value.trim()) {
|
|
669
|
-
clearSessionSearch(false, searchInput, false);
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
clearSessionSearch(true, searchInput, true);
|
|
673
|
-
}
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
searchInput.addEventListener("blur", function () {
|
|
677
|
-
setTimeout(function () {
|
|
678
|
-
if (!searchQuery && document.activeElement !== searchBtn && document.activeElement !== searchClear) {
|
|
679
|
-
closeHeaderSearch();
|
|
680
|
-
}
|
|
681
|
-
}, 0);
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
searchClear.addEventListener("click", function () {
|
|
685
|
-
clearSessionSearch(false, searchInput, false);
|
|
686
|
-
searchInput.focus();
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
syncHeaderSearchUi();
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// The "New session" split button renders from lastVendor + installedVendors,
|
|
693
|
-
// so a late-arriving vendor detection or a vendor switch has to repaint it.
|
|
193
|
+
// Folder form, folder state and the creation reveal repaint through their own
|
|
194
|
+
// subscriptions; only structural changes to the open form need a list render.
|
|
694
195
|
store.subscribe(function (state, prev) {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
196
|
+
var a = state.sessionCreate;
|
|
197
|
+
var b = prev.sessionCreate;
|
|
198
|
+
if ((!a !== !b || (a && b && a.folderId !== b.folderId)) && getSessionListEl()) renderSessionList(null);
|
|
698
199
|
if (state.splitPanes !== prev.splitPanes) renderSessionList(null);
|
|
699
200
|
});
|
|
700
201
|
|
|
@@ -710,11 +211,11 @@ export function getCachedSessions() {
|
|
|
710
211
|
}
|
|
711
212
|
|
|
712
213
|
export function getSearchQuery() {
|
|
713
|
-
return
|
|
214
|
+
return getListSearchQuery();
|
|
714
215
|
}
|
|
715
216
|
|
|
716
217
|
export function getSearchMatchIds() {
|
|
717
|
-
return
|
|
218
|
+
return getListSearchMatchIds();
|
|
718
219
|
}
|
|
719
220
|
|
|
720
221
|
export function getExpandedLoopGroups() {
|
|
@@ -746,15 +247,28 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
|
|
|
746
247
|
var menu = document.createElement("div");
|
|
747
248
|
menu.className = "session-ctx-menu";
|
|
748
249
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
250
|
+
if (canOrganizeSession(sessionData)) {
|
|
251
|
+
var isFavorite = isFavoriteSession(sessionData);
|
|
252
|
+
var bookmarkItem = document.createElement("button");
|
|
253
|
+
bookmarkItem.className = "session-ctx-item";
|
|
254
|
+
bookmarkItem.innerHTML = iconHtml(isFavorite ? "star-off" : "star") + " <span>" + (isFavorite ? "Remove from Favorites" : "Add to Favorites") + "</span>";
|
|
255
|
+
bookmarkItem.addEventListener("click", function (e) {
|
|
256
|
+
e.stopPropagation();
|
|
257
|
+
closeSessionCtxMenu();
|
|
258
|
+
toggleFavorite(sessionData);
|
|
259
|
+
});
|
|
260
|
+
menu.appendChild(bookmarkItem);
|
|
261
|
+
|
|
262
|
+
var moveItem = document.createElement("button");
|
|
263
|
+
moveItem.className = "session-ctx-item";
|
|
264
|
+
moveItem.innerHTML = iconHtml("folder-input") + " <span>Move to folder</span>";
|
|
265
|
+
moveItem.addEventListener("click", function (e) {
|
|
266
|
+
e.stopPropagation();
|
|
267
|
+
closeSessionCtxMenu();
|
|
268
|
+
openMoveToFolder(sessionData);
|
|
269
|
+
});
|
|
270
|
+
menu.appendChild(moveItem);
|
|
271
|
+
}
|
|
758
272
|
|
|
759
273
|
var renameItem = document.createElement("button");
|
|
760
274
|
renameItem.className = "session-ctx-item";
|
|
@@ -816,6 +330,10 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
|
|
|
816
330
|
});
|
|
817
331
|
}
|
|
818
332
|
|
|
333
|
+
export function openSessionContextMenu(anchor, session) {
|
|
334
|
+
showSessionCtxMenu(anchor, session.id, session.title, session.cliSessionId, session);
|
|
335
|
+
}
|
|
336
|
+
|
|
819
337
|
function showSplitGroupCtxMenu(anchor, group) {
|
|
820
338
|
closeSessionCtxMenu();
|
|
821
339
|
var menu = document.createElement("div");
|
|
@@ -909,8 +427,16 @@ function showLoopCtxMenu(anchorBtn, loopId, loopName, childCount) {
|
|
|
909
427
|
|
|
910
428
|
// --- Inline rename ---
|
|
911
429
|
|
|
430
|
+
// A session can be on screen twice (its folder and Favorites); work on the copy
|
|
431
|
+
// the user can actually see.
|
|
432
|
+
function visibleSessionRow(sessionId) {
|
|
433
|
+
var rows = getSessionListEl().querySelectorAll('.session-item[data-session-id="' + sessionId + '"]');
|
|
434
|
+
for (var i = 0; i < rows.length; i++) if (rows[i].getClientRects().length) return rows[i];
|
|
435
|
+
return rows[0] || null;
|
|
436
|
+
}
|
|
437
|
+
|
|
912
438
|
function startInlineRename(sessionId, currentTitle) {
|
|
913
|
-
var el =
|
|
439
|
+
var el = visibleSessionRow(sessionId);
|
|
914
440
|
if (!el) return;
|
|
915
441
|
var textSpan = el.querySelector(".session-item-text");
|
|
916
442
|
if (!textSpan) return;
|
|
@@ -1018,19 +544,7 @@ function startLoopInlineRename(loopId, currentName) {
|
|
|
1018
544
|
input.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
1019
545
|
}
|
|
1020
546
|
|
|
1021
|
-
// ---
|
|
1022
|
-
|
|
1023
|
-
export function getDateGroup(ts) {
|
|
1024
|
-
var now = new Date();
|
|
1025
|
-
var d = new Date(ts);
|
|
1026
|
-
var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1027
|
-
var yesterday = new Date(today.getTime() - 86400000);
|
|
1028
|
-
var weekAgo = new Date(today.getTime() - 7 * 86400000);
|
|
1029
|
-
if (d >= today) return "Today";
|
|
1030
|
-
if (d >= yesterday) return "Yesterday";
|
|
1031
|
-
if (d >= weekAgo) return "This Week";
|
|
1032
|
-
return "Older";
|
|
1033
|
-
}
|
|
547
|
+
// --- Highlighting ---
|
|
1034
548
|
|
|
1035
549
|
export function highlightMatch(text, query) {
|
|
1036
550
|
if (!query) return escapeHtml(text);
|
|
@@ -1045,15 +559,15 @@ export function highlightMatch(text, query) {
|
|
|
1045
559
|
}
|
|
1046
560
|
|
|
1047
561
|
function isSessionVisibleBySearch(sessionId) {
|
|
1048
|
-
if (
|
|
1049
|
-
return
|
|
562
|
+
if (getListSearchMatchIds() === null) return true;
|
|
563
|
+
return getListSearchMatchIds().has(sessionId);
|
|
1050
564
|
}
|
|
1051
565
|
|
|
1052
566
|
// --- Loop child / run / group rendering ---
|
|
1053
567
|
|
|
1054
568
|
function renderLoopChild(s) {
|
|
1055
569
|
var el = document.createElement("div");
|
|
1056
|
-
var isMatch =
|
|
570
|
+
var isMatch = getListSearchMatchIds() !== null && getListSearchMatchIds().has(s.id);
|
|
1057
571
|
el.className = "session-loop-child" + (s.active ? " active" : "") + (isMatch ? " search-match" : "");
|
|
1058
572
|
el.dataset.sessionId = s.id;
|
|
1059
573
|
|
|
@@ -1092,7 +606,7 @@ function renderLoopChild(s) {
|
|
|
1092
606
|
|
|
1093
607
|
function renderLoopGroup(loopId, children, groupKey) {
|
|
1094
608
|
var visibleChildren = children;
|
|
1095
|
-
if (
|
|
609
|
+
if (getListSearchMatchIds() !== null) {
|
|
1096
610
|
visibleChildren = [];
|
|
1097
611
|
for (var vi = 0; vi < children.length; vi++) {
|
|
1098
612
|
if (isSessionVisibleBySearch(children[vi].id)) {
|
|
@@ -1335,7 +849,7 @@ function renderLoopRun(parentGk, startedAtKey, sessions, isRalph) {
|
|
|
1335
849
|
function renderSessionItem(s, options) {
|
|
1336
850
|
var itemOptions = options || {};
|
|
1337
851
|
var el = document.createElement("div");
|
|
1338
|
-
var isMatch =
|
|
852
|
+
var isMatch = getListSearchMatchIds() !== null && getListSearchMatchIds().has(s.id);
|
|
1339
853
|
el.className = "session-item" + (s.active ? " active" : "") + (isMatch ? " search-match" : "");
|
|
1340
854
|
if (itemOptions.worker) el.classList.add("session-worker-item");
|
|
1341
855
|
el.dataset.sessionId = s.id;
|
|
@@ -1365,7 +879,7 @@ function renderSessionItem(s, options) {
|
|
|
1365
879
|
if (store.get('isMultiUserMode') && s.sessionVisibility === "shared") {
|
|
1366
880
|
textHtml += '<span class="session-shared-icon" title="Shared session" aria-label="Shared session">' + iconHtml("users") + '</span>';
|
|
1367
881
|
}
|
|
1368
|
-
textHtml += highlightMatch(s.title || "New Session",
|
|
882
|
+
textHtml += '<span class="session-item-title">' + highlightMatch(s.title || "New Session", getListSearchQuery()) + '</span>';
|
|
1369
883
|
textSpan.innerHTML = textHtml;
|
|
1370
884
|
el.appendChild(textSpan);
|
|
1371
885
|
appendGithubWork(el, s, textSpan);
|
|
@@ -1403,12 +917,19 @@ function renderSessionItem(s, options) {
|
|
|
1403
917
|
unreadBadge.classList.add("has-unread");
|
|
1404
918
|
}
|
|
1405
919
|
el.appendChild(unreadBadge);
|
|
1406
|
-
|
|
920
|
+
var actions = document.createElement("span");
|
|
921
|
+
actions.className = "session-row-actions";
|
|
922
|
+
if (!itemOptions.worker && canOrganizeSession(s)) {
|
|
923
|
+
actions.appendChild(createStarButton(s));
|
|
924
|
+
if (isFavoriteSession(s)) el.classList.add("is-favorite");
|
|
925
|
+
}
|
|
926
|
+
appendSessionCloseButton(actions, s);
|
|
927
|
+
if (actions.childNodes.length) el.appendChild(actions);
|
|
1407
928
|
|
|
1408
929
|
el.addEventListener("click", (function (id) {
|
|
1409
930
|
return function () {
|
|
1410
931
|
if (getWs() && store.get('connected')) {
|
|
1411
|
-
var pendingQuery =
|
|
932
|
+
var pendingQuery = getListSearchQuery() || "";
|
|
1412
933
|
getWs().send(JSON.stringify({ type: "switch_session", id: id }));
|
|
1413
934
|
dismissOverlayPanels();
|
|
1414
935
|
closeSidebar();
|
|
@@ -1420,8 +941,8 @@ function renderSessionItem(s, options) {
|
|
|
1420
941
|
})(s.id));
|
|
1421
942
|
|
|
1422
943
|
// Presence avatars (multi-user)
|
|
1423
|
-
|
|
1424
|
-
if (!itemOptions.worker)
|
|
944
|
+
renderSessionPresence(el, String(s.id));
|
|
945
|
+
if (!itemOptions.worker) setupFolderDrag(el, s);
|
|
1425
946
|
|
|
1426
947
|
return el;
|
|
1427
948
|
}
|
|
@@ -1450,7 +971,7 @@ function renderSplitGroupItem(group, members) {
|
|
|
1450
971
|
}
|
|
1451
972
|
if (splitProcessing) html += '<span class="session-processing"></span>';
|
|
1452
973
|
if (splitBackgroundTaskCount) html += '<span class="session-background-task-count">' + splitBackgroundTaskCount + '</span>';
|
|
1453
|
-
html += '<span class="split-group-name">' + highlightMatch(group.name || "Split",
|
|
974
|
+
html += '<span class="split-group-name">' + highlightMatch(group.name || "Split", getListSearchQuery()) + "</span>";
|
|
1454
975
|
text.innerHTML = html;
|
|
1455
976
|
el.appendChild(text);
|
|
1456
977
|
|
|
@@ -1502,6 +1023,11 @@ export function renderSessionList(sessions) {
|
|
|
1502
1023
|
cachedSessions = sessions;
|
|
1503
1024
|
}
|
|
1504
1025
|
|
|
1026
|
+
// Remember the inline folder draft before either surface is rebuilt: the mobile
|
|
1027
|
+
// refresh below builds a fresh row whose queued focus must not be re-captured.
|
|
1028
|
+
captureFolderInputs();
|
|
1029
|
+
captureFolderCollapseAnimations(document);
|
|
1030
|
+
|
|
1505
1031
|
// If mobile chat sheet is open, refresh it
|
|
1506
1032
|
if (refreshMobileChatSheet) refreshMobileChatSheet();
|
|
1507
1033
|
|
|
@@ -1584,94 +1110,38 @@ export function renderSessionList(sessions) {
|
|
|
1584
1110
|
items.push({ type: "loop", loopId: realLoopId, groupKey: gk, children: children, lastActivity: maxActivity });
|
|
1585
1111
|
}
|
|
1586
1112
|
|
|
1587
|
-
|
|
1588
|
-
items
|
|
1589
|
-
|
|
1590
|
-
var
|
|
1591
|
-
var
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
}
|
|
1605
|
-
if ((item.type === "session" || item.type === "driver-hierarchy") && item.data && item.data.bookmarked) {
|
|
1606
|
-
bookmarkedItems.push(item);
|
|
1607
|
-
} else {
|
|
1608
|
-
regularItems.push(item);
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
var favoritesContainer = document.createElement("div");
|
|
1613
|
-
favoritesContainer.className = "session-favorites-section";
|
|
1614
|
-
setupBookmarkDropTarget(favoritesContainer, true);
|
|
1615
|
-
if (bookmarkedItems.length === 0) {
|
|
1616
|
-
var emptyHint = document.createElement("div");
|
|
1617
|
-
emptyHint.className = "session-favorites-empty";
|
|
1618
|
-
emptyHint.textContent = "Drag and drop sessions here to add favorites.";
|
|
1619
|
-
favoritesContainer.appendChild(emptyHint);
|
|
1620
|
-
}
|
|
1621
|
-
for (var bi = 0; bi < bookmarkedItems.length; bi++) {
|
|
1622
|
-
if (bookmarkedItems[bi].type === "driver-hierarchy") favoritesContainer.appendChild(renderDesktopDriverHierarchy(bookmarkedItems[bi].root, renderSessionItem, function () { renderSessionList(null); }, searchMatchIds));
|
|
1623
|
-
else favoritesContainer.appendChild(renderSessionItem(bookmarkedItems[bi].data));
|
|
1624
|
-
}
|
|
1113
|
+
var matchIds = getListSearchMatchIds();
|
|
1114
|
+
var visibleItems = filterItemsBySearch(items, matchIds);
|
|
1115
|
+
|
|
1116
|
+
var rerender = function () { renderSessionList(null); };
|
|
1117
|
+
var layout = computeLayout(visibleItems, matchIds !== null, "desktop");
|
|
1118
|
+
var folderCtx = {
|
|
1119
|
+
surface: "desktop",
|
|
1120
|
+
searching: matchIds !== null,
|
|
1121
|
+
sessions: cachedSessions,
|
|
1122
|
+
renderItem: function (it) {
|
|
1123
|
+
if (it.type === "loop") return renderLoopGroup(it.loopId, it.children, it.groupKey) || document.createElement("div");
|
|
1124
|
+
if (it.type === "split-group") return renderSplitGroupItem(it.group, it.members);
|
|
1125
|
+
if (it.type === "driver-hierarchy") return renderDesktopDriverHierarchy(it.root, renderSessionItem, rerender, matchIds);
|
|
1126
|
+
if (it.type === "orphan-workers") return renderDesktopOrphanHierarchy(it.workers, renderSessionItem, rerender, matchIds);
|
|
1127
|
+
return renderSessionItem(it.data);
|
|
1128
|
+
},
|
|
1129
|
+
};
|
|
1625
1130
|
|
|
1626
|
-
|
|
1627
|
-
|
|
1131
|
+
// The folder toolbar sits above one continuous list: Favorites first, then
|
|
1132
|
+
// custom folders, then Unfiled. Each header has its own New session button.
|
|
1133
|
+
var topActions = document.createElement("div");
|
|
1134
|
+
topActions.className = "session-list-sticky-top";
|
|
1135
|
+
topActions.appendChild(renderFolderToolbar("desktop"));
|
|
1136
|
+
getSessionListEl().appendChild(topActions);
|
|
1628
1137
|
|
|
1629
1138
|
var regularContainer = document.createElement("div");
|
|
1630
1139
|
regularContainer.className = "session-regular-drop";
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
stickyTop.className = "session-list-sticky-top";
|
|
1634
|
-
stickyTop.appendChild(favoritesContainer);
|
|
1635
|
-
stickyTop.appendChild(divider);
|
|
1636
|
-
stickyTop.appendChild(renderSessionTopActions());
|
|
1637
|
-
getSessionListEl().appendChild(stickyTop);
|
|
1638
|
-
|
|
1639
|
-
var currentGroup = "";
|
|
1640
|
-
var currentGroupIds = [];
|
|
1641
|
-
for (var ri = 0; ri < regularItems.length; ri++) {
|
|
1642
|
-
var item = regularItems[ri];
|
|
1643
|
-
var group = getDateGroup(item.lastActivity || 0);
|
|
1644
|
-
if (group !== currentGroup) {
|
|
1645
|
-
currentGroup = group;
|
|
1646
|
-
currentGroupIds = [];
|
|
1647
|
-
for (var gi = ri; gi < regularItems.length; gi++) {
|
|
1648
|
-
if (getDateGroup(regularItems[gi].lastActivity || 0) !== group) break;
|
|
1649
|
-
var groupIds = collectItemSessionIds(regularItems[gi]);
|
|
1650
|
-
for (var gj = 0; gj < groupIds.length; gj++) currentGroupIds.push(groupIds[gj]);
|
|
1651
|
-
}
|
|
1652
|
-
if (group !== "Today") {
|
|
1653
|
-
regularContainer.appendChild(createSessionGroupHeader(group, currentGroupIds));
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
if (item.type === "loop") {
|
|
1657
|
-
var loopEl = renderLoopGroup(item.loopId, item.children, item.groupKey);
|
|
1658
|
-
if (loopEl) {
|
|
1659
|
-
regularContainer.appendChild(loopEl);
|
|
1660
|
-
}
|
|
1661
|
-
} else if (item.type === "split-group") {
|
|
1662
|
-
regularContainer.appendChild(renderSplitGroupItem(item.group, item.members));
|
|
1663
|
-
} else if (item.type === "driver-hierarchy") {
|
|
1664
|
-
regularContainer.appendChild(renderDesktopDriverHierarchy(item.root, renderSessionItem, function () { renderSessionList(null); }, searchMatchIds));
|
|
1665
|
-
} else if (item.type === "orphan-workers") {
|
|
1666
|
-
regularContainer.appendChild(renderDesktopOrphanHierarchy(item.workers, renderSessionItem, function () { renderSessionList(null); }, searchMatchIds));
|
|
1667
|
-
} else {
|
|
1668
|
-
regularContainer.appendChild(renderSessionItem(item.data));
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1140
|
+
regularContainer.appendChild(renderFavoritesSection(layout, folderCtx));
|
|
1141
|
+
renderOrdinarySections(layout, folderCtx, regularContainer);
|
|
1671
1142
|
getSessionListEl().appendChild(regularContainer);
|
|
1672
1143
|
refreshIcons();
|
|
1673
1144
|
if (updatePageTitle) updatePageTitle();
|
|
1674
|
-
syncHeaderSearchUi();
|
|
1675
1145
|
// Recorded from the sessions actually rendered, so a null-argument re-render
|
|
1676
1146
|
// driven by other state refreshes the fingerprint rather than stranding it.
|
|
1677
1147
|
lastRenderFingerprint = sessionRenderFingerprint(cachedSessions);
|
|
@@ -1680,68 +1150,19 @@ export function renderSessionList(sessions) {
|
|
|
1680
1150
|
// --- Search results ---
|
|
1681
1151
|
|
|
1682
1152
|
export function handleSearchResults(msg) {
|
|
1683
|
-
|
|
1684
|
-
var ids = new Set();
|
|
1685
|
-
for (var i = 0; i < msg.results.length; i++) {
|
|
1686
|
-
ids.add(msg.results[i].id);
|
|
1687
|
-
}
|
|
1688
|
-
searchMatchIds = ids;
|
|
1689
|
-
renderSessionList(null);
|
|
1153
|
+
applyListSearchResults(msg);
|
|
1690
1154
|
}
|
|
1691
1155
|
|
|
1692
1156
|
// --- Session presence ---
|
|
1693
1157
|
|
|
1694
1158
|
export function updateSessionPresence(presence) {
|
|
1695
|
-
|
|
1159
|
+
setSessionPresence(presence);
|
|
1696
1160
|
// Update presence avatars on existing session items without full re-render
|
|
1697
|
-
var items = getSessionListEl().querySelectorAll("[data-session-id]");
|
|
1161
|
+
var items = getSessionListEl().querySelectorAll(".session-item[data-session-id]");
|
|
1698
1162
|
for (var i = 0; i < items.length; i++) {
|
|
1699
|
-
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
function presenceAvatarUrl(userOrStyle, seed) {
|
|
1704
|
-
if (userOrStyle && typeof userOrStyle === "object") return userAvatarUrl(userOrStyle, 24);
|
|
1705
|
-
return avatarUrl(userOrStyle || "imprint", seed, 24);
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
function renderPresenceAvatars(el, sessionId) {
|
|
1709
|
-
// Remove existing presence container
|
|
1710
|
-
var existing = el.querySelector(".session-presence");
|
|
1711
|
-
if (existing) existing.remove();
|
|
1712
|
-
|
|
1713
|
-
var users = sessionPresence[sessionId];
|
|
1714
|
-
if (!users || users.length === 0) return;
|
|
1715
|
-
|
|
1716
|
-
var container = document.createElement("span");
|
|
1717
|
-
container.className = "session-presence";
|
|
1718
|
-
|
|
1719
|
-
var max = 3;
|
|
1720
|
-
var shown = users.length > max ? max : users.length;
|
|
1721
|
-
for (var i = 0; i < shown; i++) {
|
|
1722
|
-
var u = users[i];
|
|
1723
|
-
var img = document.createElement("img");
|
|
1724
|
-
img.className = "session-presence-avatar";
|
|
1725
|
-
img.src = presenceAvatarUrl(u);
|
|
1726
|
-
img.alt = u.displayName;
|
|
1727
|
-
img.dataset.tip = u.displayName + (u.username ? " (@" + u.username + ")" : "");
|
|
1728
|
-
if (i > 0) img.style.marginLeft = "-6px";
|
|
1729
|
-
container.appendChild(img);
|
|
1730
|
-
}
|
|
1731
|
-
if (users.length > max) {
|
|
1732
|
-
var more = document.createElement("span");
|
|
1733
|
-
more.className = "session-presence-more";
|
|
1734
|
-
more.textContent = "+" + (users.length - max);
|
|
1735
|
-
container.appendChild(more);
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
// Insert before the more-btn
|
|
1739
|
-
var moreBtn = el.querySelector(".session-more-btn");
|
|
1740
|
-
if (moreBtn) {
|
|
1741
|
-
el.insertBefore(container, moreBtn);
|
|
1742
|
-
} else {
|
|
1743
|
-
el.appendChild(container);
|
|
1163
|
+
renderSessionPresence(items[i], items[i].dataset.sessionId);
|
|
1744
1164
|
}
|
|
1165
|
+
refreshMobileChatSheet();
|
|
1745
1166
|
}
|
|
1746
1167
|
|
|
1747
1168
|
// --- Session badge ---
|
|
@@ -1750,24 +1171,27 @@ export function updateSessionBadge(sessionId, count) {
|
|
|
1750
1171
|
for (var i = 0; i < cachedSessions.length; i++) {
|
|
1751
1172
|
if (cachedSessions[i].id === sessionId) cachedSessions[i].unread = count;
|
|
1752
1173
|
}
|
|
1753
|
-
|
|
1174
|
+
// Every copy of the row (a favorite shows twice) is updated.
|
|
1175
|
+
var badges = document.querySelectorAll('.session-unread-badge[data-session-id="' + sessionId + '"]');
|
|
1754
1176
|
var groups = store.get('splitGroups') || [];
|
|
1755
1177
|
for (var gi = 0; gi < groups.length; gi++) {
|
|
1756
1178
|
if (groups[gi].members.indexOf(sessionId) === -1) continue;
|
|
1757
|
-
|
|
1179
|
+
badges = document.querySelectorAll('.session-unread-badge[data-split-group-id="' + groups[gi].id + '"]');
|
|
1758
1180
|
count = 0;
|
|
1759
1181
|
for (var mi = 0; mi < cachedSessions.length; mi++) {
|
|
1760
1182
|
if (groups[gi].members.indexOf(cachedSessions[mi].id) !== -1) count += cachedSessions[mi].unread || 0;
|
|
1761
1183
|
}
|
|
1762
1184
|
break;
|
|
1763
1185
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1186
|
+
for (var bi = 0; bi < badges.length; bi++) {
|
|
1187
|
+
var badge = badges[bi];
|
|
1188
|
+
if (count > 0) {
|
|
1189
|
+
badge.textContent = count > 99 ? "99+" : String(count);
|
|
1190
|
+
badge.classList.add("has-unread");
|
|
1191
|
+
} else {
|
|
1192
|
+
badge.textContent = "";
|
|
1193
|
+
badge.classList.remove("has-unread");
|
|
1194
|
+
}
|
|
1771
1195
|
}
|
|
1772
1196
|
}
|
|
1773
1197
|
|