clay-server 2.27.0-beta.9 → 2.27.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/README.md +10 -0
- package/lib/daemon-projects.js +164 -0
- package/lib/daemon.js +13 -126
- package/lib/mates-identity.js +132 -0
- package/lib/mates-knowledge.js +113 -0
- package/lib/mates-prompts.js +398 -0
- package/lib/mates.js +40 -599
- package/lib/project-connection.js +2 -0
- package/lib/project-http.js +4 -2
- package/lib/project-loop.js +110 -48
- package/lib/project-mate-interaction.js +4 -0
- package/lib/project-notifications.js +210 -0
- package/lib/project-sessions.js +5 -2
- package/lib/project-user-message.js +2 -1
- package/lib/project.js +26 -2
- package/lib/public/app.js +1193 -8517
- package/lib/public/css/command-palette.css +14 -0
- package/lib/public/css/loop.css +301 -0
- package/lib/public/css/notifications-center.css +190 -0
- package/lib/public/css/rewind.css +6 -0
- package/lib/public/index.html +89 -35
- package/lib/public/modules/app-connection.js +160 -0
- package/lib/public/modules/app-cursors.js +473 -0
- package/lib/public/modules/app-debate-ui.js +389 -0
- package/lib/public/modules/app-dm.js +627 -0
- package/lib/public/modules/app-favicon.js +212 -0
- package/lib/public/modules/app-header.js +229 -0
- package/lib/public/modules/app-home-hub.js +600 -0
- package/lib/public/modules/app-loop-ui.js +589 -0
- package/lib/public/modules/app-loop-wizard.js +439 -0
- package/lib/public/modules/app-messages.js +1560 -0
- package/lib/public/modules/app-misc.js +299 -0
- package/lib/public/modules/app-notifications.js +372 -0
- package/lib/public/modules/app-panels.js +888 -0
- package/lib/public/modules/app-projects.js +798 -0
- package/lib/public/modules/app-rate-limit.js +451 -0
- package/lib/public/modules/app-rendering.js +597 -0
- package/lib/public/modules/app-skills-install.js +234 -0
- package/lib/public/modules/command-palette.js +27 -4
- package/lib/public/modules/input.js +31 -20
- package/lib/public/modules/scheduler-config.js +1532 -0
- package/lib/public/modules/scheduler-history.js +79 -0
- package/lib/public/modules/scheduler.js +33 -1554
- package/lib/public/modules/session-search.js +13 -1
- package/lib/public/modules/sidebar-mates.js +812 -0
- package/lib/public/modules/sidebar-mobile.js +1269 -0
- package/lib/public/modules/sidebar-projects.js +1449 -0
- package/lib/public/modules/sidebar-sessions.js +986 -0
- package/lib/public/modules/sidebar.js +232 -4591
- package/lib/public/modules/store.js +27 -0
- package/lib/public/modules/ws-ref.js +7 -0
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +96 -717
- package/lib/sdk-message-processor.js +587 -0
- package/lib/sdk-message-queue.js +42 -0
- package/lib/sdk-skill-discovery.js +131 -0
- package/lib/server-admin.js +712 -0
- package/lib/server-auth.js +737 -0
- package/lib/server-dm.js +221 -0
- package/lib/server-mates.js +281 -0
- package/lib/server-palette.js +110 -0
- package/lib/server-settings.js +479 -0
- package/lib/server-skills.js +280 -0
- package/lib/server.js +246 -2755
- package/lib/sessions.js +11 -4
- package/lib/users-auth.js +146 -0
- package/lib/users-permissions.js +118 -0
- package/lib/users-preferences.js +210 -0
- package/lib/users.js +48 -398
- package/lib/ws-schema.js +498 -0
- package/package.json +1 -1
|
@@ -1,4662 +1,303 @@
|
|
|
1
|
-
import { avatarUrl, userAvatarUrl, mateAvatarUrl } from './avatar.js';
|
|
2
|
-
import { escapeHtml, copyToClipboard } from './utils.js';
|
|
3
|
-
import { iconHtml, refreshIcons } from './icons.js';
|
|
4
|
-
import { openProjectSettings } from './project-settings.js';
|
|
5
|
-
import { triggerShare } from './qrcode.js';
|
|
6
|
-
import { parseEmojis } from './markdown.js';
|
|
7
|
-
import { getCurrentTheme, getChatLayout, setChatLayout } from './theme.js';
|
|
8
|
-
import { showMateProfilePopover } from './profile.js';
|
|
9
1
|
import { closeArchive } from './sticky-notes.js';
|
|
10
2
|
import { closeScheduler } from './scheduler.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
3
|
+
import { initSidebarSessions } from './sidebar-sessions.js';
|
|
4
|
+
import { initSidebarProjects, closeProjectCtxMenu } from './sidebar-projects.js';
|
|
5
|
+
import {
|
|
6
|
+
initSidebarMates,
|
|
7
|
+
showIconTooltip,
|
|
8
|
+
showIconTooltipHtml,
|
|
9
|
+
hideIconTooltip,
|
|
10
|
+
closeUserCtxMenu,
|
|
11
|
+
getCurrentDmUserId
|
|
12
|
+
} from './sidebar-mates.js';
|
|
13
|
+
import { initSidebarMobile } from './sidebar-mobile.js';
|
|
14
14
|
|
|
15
15
|
var ctx;
|
|
16
16
|
|
|
17
|
-
// --- Session search ---
|
|
18
|
-
var searchQuery = "";
|
|
19
|
-
var searchMatchIds = null; // null = no search, Set of matched session IDs
|
|
20
|
-
var searchDebounce = null;
|
|
21
|
-
var cachedSessions = [];
|
|
22
|
-
var expandedLoopGroups = new Set();
|
|
23
|
-
var expandedLoopRuns = new Set();
|
|
24
|
-
var expandedMobileLoopGroups = new Set();
|
|
25
|
-
var expandedMobileLoopRuns = new Set();
|
|
26
|
-
|
|
27
|
-
// --- Cached project data for mobile sheet ---
|
|
28
|
-
var cachedProjectList = [];
|
|
29
|
-
var cachedCurrentSlug = null;
|
|
30
|
-
var mobileChatSheetOpen = false; // track if chat sheet is showing
|
|
31
|
-
|
|
32
17
|
function dismissOverlayPanels() {
|
|
33
18
|
closeArchive();
|
|
34
19
|
closeScheduler();
|
|
35
20
|
}
|
|
36
21
|
|
|
37
|
-
|
|
38
|
-
var
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// --- Session context menu ---
|
|
45
|
-
var sessionCtxMenu = null;
|
|
46
|
-
var sessionCtxSessionId = null;
|
|
47
|
-
|
|
48
|
-
function closeSessionCtxMenu() {
|
|
49
|
-
if (sessionCtxMenu) {
|
|
50
|
-
sessionCtxMenu.remove();
|
|
51
|
-
sessionCtxMenu = null;
|
|
52
|
-
sessionCtxSessionId = null;
|
|
22
|
+
export function updatePageTitle() {
|
|
23
|
+
var sessionTitle = "";
|
|
24
|
+
var activeItem = ctx.sessionListEl.querySelector(".session-item.active .session-item-text");
|
|
25
|
+
if (activeItem) sessionTitle = activeItem.textContent;
|
|
26
|
+
if (ctx.headerTitleEl) {
|
|
27
|
+
ctx.headerTitleEl.textContent = sessionTitle || ctx.projectName || "Clay";
|
|
53
28
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
menu.className = "session-ctx-menu";
|
|
62
|
-
|
|
63
|
-
var renameItem = document.createElement("button");
|
|
64
|
-
renameItem.className = "session-ctx-item";
|
|
65
|
-
renameItem.innerHTML = iconHtml("pencil") + " <span>Rename</span>";
|
|
66
|
-
renameItem.addEventListener("click", function (e) {
|
|
67
|
-
e.stopPropagation();
|
|
68
|
-
closeSessionCtxMenu();
|
|
69
|
-
startInlineRename(sessionId, title);
|
|
70
|
-
});
|
|
71
|
-
menu.appendChild(renameItem);
|
|
72
|
-
|
|
73
|
-
// Session visibility toggle (only the session owner can change)
|
|
74
|
-
if (ctx.multiUser && sessionData && sessionData.ownerId && sessionData.ownerId === ctx.myUserId) {
|
|
75
|
-
var currentVis = (sessionData && sessionData.sessionVisibility) || "shared";
|
|
76
|
-
var isPrivate = currentVis === "private";
|
|
77
|
-
var visItem = document.createElement("button");
|
|
78
|
-
visItem.className = "session-ctx-item";
|
|
79
|
-
visItem.innerHTML = iconHtml(isPrivate ? "eye" : "eye-off") + " <span>" + (isPrivate ? "Make Shared" : "Make Private") + "</span>";
|
|
80
|
-
visItem.addEventListener("click", function (e) {
|
|
81
|
-
e.stopPropagation();
|
|
82
|
-
closeSessionCtxMenu();
|
|
83
|
-
var newVis = isPrivate ? "shared" : "private";
|
|
84
|
-
if (ctx.ws && ctx.connected) {
|
|
85
|
-
ctx.ws.send(JSON.stringify({ type: "set_session_visibility", sessionId: sessionId, visibility: newVis }));
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
menu.appendChild(visItem);
|
|
29
|
+
var tbProjectName = ctx.$("title-bar-project-name");
|
|
30
|
+
if (tbProjectName && ctx.projectName) {
|
|
31
|
+
tbProjectName.textContent = ctx.projectName;
|
|
32
|
+
} else if (tbProjectName && !tbProjectName.textContent) {
|
|
33
|
+
// Fallback: derive name from URL slug when projectName not yet available
|
|
34
|
+
var _m = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
|
|
35
|
+
if (_m) tbProjectName.textContent = _m[1];
|
|
89
36
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
e.stopPropagation();
|
|
97
|
-
closeSessionCtxMenu();
|
|
98
|
-
ctx.showConfirm('Delete "' + (title || "New Session") + '"? This session and its history will be permanently removed.', function () {
|
|
99
|
-
var ws = ctx.ws;
|
|
100
|
-
if (ws && ctx.connected) {
|
|
101
|
-
ws.send(JSON.stringify({ type: "delete_session", id: sessionId }));
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
menu.appendChild(deleteItem);
|
|
37
|
+
if (ctx.projectName && sessionTitle) {
|
|
38
|
+
document.title = sessionTitle + " - " + ctx.projectName;
|
|
39
|
+
} else if (ctx.projectName) {
|
|
40
|
+
document.title = ctx.projectName + " - Clay";
|
|
41
|
+
} else {
|
|
42
|
+
document.title = "Clay";
|
|
106
43
|
}
|
|
107
|
-
|
|
108
|
-
document.body.appendChild(menu);
|
|
109
|
-
sessionCtxMenu = menu;
|
|
110
|
-
refreshIcons();
|
|
111
|
-
|
|
112
|
-
// Position: fixed relative to the anchor button
|
|
113
|
-
requestAnimationFrame(function () {
|
|
114
|
-
var btnRect = anchorBtn.getBoundingClientRect();
|
|
115
|
-
menu.style.position = "fixed";
|
|
116
|
-
menu.style.top = (btnRect.bottom + 2) + "px";
|
|
117
|
-
menu.style.right = (window.innerWidth - btnRect.right) + "px";
|
|
118
|
-
menu.style.left = "auto";
|
|
119
|
-
// If menu overflows below viewport, flip up
|
|
120
|
-
var menuRect = menu.getBoundingClientRect();
|
|
121
|
-
if (menuRect.bottom > window.innerHeight - 8) {
|
|
122
|
-
menu.style.top = (btnRect.top - menuRect.height - 2) + "px";
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
44
|
}
|
|
126
45
|
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (!textSpan) return;
|
|
132
|
-
|
|
133
|
-
var input = document.createElement("input");
|
|
134
|
-
input.type = "text";
|
|
135
|
-
input.className = "session-rename-input";
|
|
136
|
-
input.value = currentTitle || "New Session";
|
|
137
|
-
|
|
138
|
-
var originalHtml = textSpan.innerHTML;
|
|
139
|
-
textSpan.innerHTML = "";
|
|
140
|
-
textSpan.appendChild(input);
|
|
141
|
-
input.focus();
|
|
142
|
-
input.select();
|
|
143
|
-
|
|
144
|
-
function commitRename() {
|
|
145
|
-
var newTitle = input.value.trim();
|
|
146
|
-
if (newTitle && newTitle !== currentTitle && ctx.ws && ctx.connected) {
|
|
147
|
-
ctx.ws.send(JSON.stringify({ type: "rename_session", id: sessionId, title: newTitle }));
|
|
148
|
-
}
|
|
149
|
-
// Restore text (server will send updated session_list)
|
|
150
|
-
textSpan.innerHTML = originalHtml;
|
|
151
|
-
if (newTitle && newTitle !== currentTitle) {
|
|
152
|
-
textSpan.textContent = newTitle;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
46
|
+
export function openSidebar() {
|
|
47
|
+
ctx.sidebar.classList.add("open");
|
|
48
|
+
ctx.sidebarOverlay.classList.add("visible");
|
|
49
|
+
}
|
|
155
50
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
});
|
|
160
|
-
input.addEventListener("blur", commitRename);
|
|
161
|
-
input.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
51
|
+
export function closeSidebar() {
|
|
52
|
+
ctx.sidebar.classList.remove("open");
|
|
53
|
+
ctx.sidebarOverlay.classList.remove("visible");
|
|
162
54
|
}
|
|
163
55
|
|
|
164
|
-
function
|
|
165
|
-
|
|
56
|
+
export function initSidebar(_ctx) {
|
|
57
|
+
ctx = _ctx;
|
|
166
58
|
|
|
167
|
-
|
|
168
|
-
|
|
59
|
+
// Initialize session sub-module with sidebar callbacks
|
|
60
|
+
ctx.closeSidebar = closeSidebar;
|
|
61
|
+
ctx.dismissOverlayPanels = dismissOverlayPanels;
|
|
62
|
+
ctx.updatePageTitle = updatePageTitle;
|
|
63
|
+
ctx.showIconTooltip = showIconTooltip;
|
|
64
|
+
ctx.showIconTooltipHtml = showIconTooltipHtml;
|
|
65
|
+
ctx.hideIconTooltip = hideIconTooltip;
|
|
66
|
+
ctx.closeUserCtxMenu = closeUserCtxMenu;
|
|
67
|
+
ctx.closeProjectCtxMenu = closeProjectCtxMenu;
|
|
68
|
+
ctx.getCurrentDmUserId = getCurrentDmUserId;
|
|
69
|
+
ctx.spawnDustParticles = spawnDustParticles;
|
|
70
|
+
initSidebarSessions(ctx);
|
|
71
|
+
initSidebarProjects(ctx);
|
|
72
|
+
initSidebarMates(ctx);
|
|
73
|
+
initSidebarMobile(ctx);
|
|
169
74
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
renameItem.innerHTML = iconHtml("pencil") + " <span>Rename</span>";
|
|
173
|
-
renameItem.addEventListener("click", function (e) {
|
|
174
|
-
e.stopPropagation();
|
|
175
|
-
closeSessionCtxMenu();
|
|
176
|
-
startLoopInlineRename(loopId, loopName);
|
|
75
|
+
ctx.hamburgerBtn.addEventListener("click", function () {
|
|
76
|
+
ctx.sidebar.classList.contains("open") ? closeSidebar() : openSidebar();
|
|
177
77
|
});
|
|
178
|
-
menu.appendChild(renameItem);
|
|
179
78
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (childCount > 1) msg += " and its " + childCount + " sessions";
|
|
189
|
-
msg += "? This cannot be undone.";
|
|
190
|
-
ctx.showConfirm(msg, function () {
|
|
191
|
-
if (ctx.ws && ctx.connected) {
|
|
192
|
-
ctx.ws.send(JSON.stringify({ type: "delete_loop_group", loopId: loopId }));
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
menu.appendChild(deleteItem);
|
|
79
|
+
ctx.sidebarOverlay.addEventListener("click", closeSidebar);
|
|
80
|
+
|
|
81
|
+
// --- Desktop sidebar collapse/expand ---
|
|
82
|
+
function toggleSidebarCollapse() {
|
|
83
|
+
var layout = ctx.$("layout");
|
|
84
|
+
var collapsed = layout.classList.toggle("sidebar-collapsed");
|
|
85
|
+
try { localStorage.setItem("sidebar-collapsed", collapsed ? "1" : ""); } catch (e) {}
|
|
86
|
+
setTimeout(function () { syncUserIslandWidth(); syncResizeHandle(); }, 210);
|
|
197
87
|
}
|
|
198
88
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
89
|
+
if (ctx.sidebarToggleBtn) ctx.sidebarToggleBtn.addEventListener("click", toggleSidebarCollapse);
|
|
90
|
+
if (ctx.sidebarExpandBtn) ctx.sidebarExpandBtn.addEventListener("click", toggleSidebarCollapse);
|
|
91
|
+
var mateSidebarToggle = document.getElementById("mate-sidebar-toggle-btn");
|
|
92
|
+
if (mateSidebarToggle) mateSidebarToggle.addEventListener("click", toggleSidebarCollapse);
|
|
202
93
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
menu.style.right = (window.innerWidth - btnRect.right) + "px";
|
|
208
|
-
menu.style.left = "auto";
|
|
209
|
-
var menuRect = menu.getBoundingClientRect();
|
|
210
|
-
if (menuRect.bottom > window.innerHeight - 8) {
|
|
211
|
-
menu.style.top = (btnRect.top - menuRect.height - 2) + "px";
|
|
94
|
+
// Restore collapsed state from localStorage
|
|
95
|
+
try {
|
|
96
|
+
if (localStorage.getItem("sidebar-collapsed") === "1") {
|
|
97
|
+
ctx.$("layout").classList.add("sidebar-collapsed");
|
|
212
98
|
}
|
|
213
|
-
})
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function startLoopInlineRename(loopId, currentName) {
|
|
217
|
-
var el = ctx.sessionListEl.querySelector('.session-loop-group[data-loop-id="' + loopId + '"]');
|
|
218
|
-
if (!el) return;
|
|
219
|
-
var textSpan = el.querySelector(".session-item-text");
|
|
220
|
-
if (!textSpan) return;
|
|
221
|
-
|
|
222
|
-
var input = document.createElement("input");
|
|
223
|
-
input.type = "text";
|
|
224
|
-
input.className = "session-rename-input";
|
|
225
|
-
input.value = currentName || "Ralph Loop";
|
|
226
|
-
|
|
227
|
-
var originalHtml = textSpan.innerHTML;
|
|
228
|
-
textSpan.innerHTML = "";
|
|
229
|
-
textSpan.appendChild(input);
|
|
230
|
-
input.focus();
|
|
231
|
-
input.select();
|
|
99
|
+
} catch (e) {}
|
|
232
100
|
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
textSpan.innerHTML = originalHtml;
|
|
239
|
-
if (newName && newName !== currentName) {
|
|
240
|
-
// Update text inline immediately
|
|
241
|
-
var nameNode = textSpan.querySelector(".session-loop-name");
|
|
242
|
-
if (nameNode) nameNode.textContent = newName;
|
|
101
|
+
ctx.newSessionBtn.addEventListener("click", function () {
|
|
102
|
+
if (ctx.ws && ctx.connected) {
|
|
103
|
+
ctx.ws.send(JSON.stringify({ type: "new_session" }));
|
|
104
|
+
closeSidebar();
|
|
243
105
|
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
input.addEventListener("keydown", function (e) {
|
|
247
|
-
if (e.key === "Enter") { e.preventDefault(); commitRename(); }
|
|
248
|
-
if (e.key === "Escape") { e.preventDefault(); textSpan.innerHTML = originalHtml; }
|
|
249
106
|
});
|
|
250
|
-
input.addEventListener("blur", commitRename);
|
|
251
|
-
input.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function getDateGroup(ts) {
|
|
255
|
-
var now = new Date();
|
|
256
|
-
var d = new Date(ts);
|
|
257
|
-
var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
258
|
-
var yesterday = new Date(today.getTime() - 86400000);
|
|
259
|
-
var weekAgo = new Date(today.getTime() - 7 * 86400000);
|
|
260
|
-
if (d >= today) return "Today";
|
|
261
|
-
if (d >= yesterday) return "Yesterday";
|
|
262
|
-
if (d >= weekAgo) return "This Week";
|
|
263
|
-
return "Older";
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function highlightMatch(text, query) {
|
|
267
|
-
if (!query) return escapeHtml(text);
|
|
268
|
-
var lower = text.toLowerCase();
|
|
269
|
-
var qLower = query.toLowerCase();
|
|
270
|
-
var idx = lower.indexOf(qLower);
|
|
271
|
-
if (idx === -1) return escapeHtml(text);
|
|
272
|
-
var before = text.substring(0, idx);
|
|
273
|
-
var match = text.substring(idx, idx + query.length);
|
|
274
|
-
var after = text.substring(idx + query.length);
|
|
275
|
-
return escapeHtml(before) + '<mark class="session-highlight">' + escapeHtml(match) + '</mark>' + escapeHtml(after);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function renderLoopChild(s) {
|
|
279
|
-
var el = document.createElement("div");
|
|
280
|
-
var isMatch = searchMatchIds !== null && searchMatchIds.has(s.id);
|
|
281
|
-
var dimmed = searchMatchIds !== null && !isMatch;
|
|
282
|
-
el.className = "session-loop-child" + (s.active ? " active" : "") + (isMatch ? " search-match" : "") + (dimmed ? " search-dimmed" : "");
|
|
283
|
-
el.dataset.sessionId = s.id;
|
|
284
107
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (s.loop) {
|
|
292
|
-
var isRalphChild = s.loop.source === "ralph";
|
|
293
|
-
var roleName = s.loop.role === "crafting" ? "Crafting" : s.loop.role === "judge" ? "Judge" : (isRalphChild ? "Coder" : "Run");
|
|
294
|
-
var iterSuffix = s.loop.role === "crafting" ? "" : " #" + s.loop.iteration;
|
|
295
|
-
var roleCls = s.loop.role === "crafting" ? " crafting" : (!isRalphChild ? " scheduled" : "");
|
|
296
|
-
textHtml += '<span class="session-loop-role-badge' + roleCls + '">' + roleName + iterSuffix + '</span>';
|
|
108
|
+
// --- New Ralph Loop button ---
|
|
109
|
+
var newRalphBtn = ctx.$("new-ralph-btn");
|
|
110
|
+
if (newRalphBtn) {
|
|
111
|
+
newRalphBtn.addEventListener("click", function () {
|
|
112
|
+
if (ctx.openRalphWizard) ctx.openRalphWizard();
|
|
113
|
+
});
|
|
297
114
|
}
|
|
298
|
-
textSpan.innerHTML = textHtml;
|
|
299
|
-
el.appendChild(textSpan);
|
|
300
|
-
|
|
301
|
-
el.addEventListener("click", (function (id) {
|
|
302
|
-
return function () {
|
|
303
|
-
if (ctx.ws && ctx.connected) {
|
|
304
|
-
ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
|
|
305
|
-
dismissOverlayPanels();
|
|
306
|
-
closeSidebar();
|
|
307
|
-
}
|
|
308
|
-
};
|
|
309
|
-
})(s.id));
|
|
310
|
-
|
|
311
|
-
return el;
|
|
312
|
-
}
|
|
313
115
|
|
|
314
|
-
|
|
315
|
-
var
|
|
116
|
+
// --- Panel switch (sessions / files / projects) ---
|
|
117
|
+
var fileBrowserBtn = ctx.$("file-browser-btn");
|
|
118
|
+
var projectsPanel = ctx.$("sidebar-panel-projects");
|
|
119
|
+
var sessionsPanel = ctx.$("sidebar-panel-sessions");
|
|
120
|
+
var filesPanel = ctx.$("sidebar-panel-files");
|
|
121
|
+
var sessionsHeaderContent = ctx.$("sessions-header-content");
|
|
122
|
+
var filesHeaderContent = ctx.$("files-header-content");
|
|
123
|
+
var filePanelClose = ctx.$("file-panel-close");
|
|
316
124
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (
|
|
322
|
-
|
|
125
|
+
function hideAllPanels() {
|
|
126
|
+
if (projectsPanel) projectsPanel.classList.add("hidden");
|
|
127
|
+
if (sessionsPanel) sessionsPanel.classList.add("hidden");
|
|
128
|
+
if (filesPanel) filesPanel.classList.add("hidden");
|
|
129
|
+
if (sessionsHeaderContent) sessionsHeaderContent.classList.add("hidden");
|
|
130
|
+
if (filesHeaderContent) filesHeaderContent.classList.add("hidden");
|
|
323
131
|
}
|
|
324
|
-
var runKeys = Object.keys(runMap);
|
|
325
132
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
var ai = (a.loop && a.loop.iteration) || 0;
|
|
330
|
-
var bi = (b.loop && b.loop.iteration) || 0;
|
|
331
|
-
if (ai !== bi) return ai - bi;
|
|
332
|
-
var ar = (a.loop && a.loop.role === "judge") ? 1 : 0;
|
|
333
|
-
var br = (b.loop && b.loop.role === "judge") ? 1 : 0;
|
|
334
|
-
return ar - br;
|
|
335
|
-
});
|
|
133
|
+
function showProjectsPanel() {
|
|
134
|
+
hideAllPanels();
|
|
135
|
+
if (projectsPanel) projectsPanel.classList.remove("hidden");
|
|
336
136
|
}
|
|
337
137
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
var hasActive = false;
|
|
343
|
-
var anyProcessing = false;
|
|
344
|
-
var latestSession = children[0];
|
|
345
|
-
for (var ci = 0; ci < children.length; ci++) {
|
|
346
|
-
if (children[ci].active) hasActive = true;
|
|
347
|
-
if (children[ci].isProcessing) anyProcessing = true;
|
|
348
|
-
if ((children[ci].lastActivity || 0) > (latestSession.lastActivity || 0)) {
|
|
349
|
-
latestSession = children[ci];
|
|
350
|
-
}
|
|
138
|
+
function showSessionsPanel() {
|
|
139
|
+
hideAllPanels();
|
|
140
|
+
if (sessionsPanel) sessionsPanel.classList.remove("hidden");
|
|
141
|
+
if (sessionsHeaderContent) sessionsHeaderContent.classList.remove("hidden");
|
|
351
142
|
}
|
|
352
143
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (children[j].loop && children[j].loop.role === "crafting") isCrafting = true;
|
|
144
|
+
function showFilesPanel() {
|
|
145
|
+
hideAllPanels();
|
|
146
|
+
if (filesPanel) filesPanel.classList.remove("hidden");
|
|
147
|
+
if (filesHeaderContent) filesHeaderContent.classList.remove("hidden");
|
|
148
|
+
if (ctx.onFilesTabOpen) ctx.onFilesTabOpen();
|
|
359
149
|
}
|
|
360
150
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
var wrapper = document.createElement("div");
|
|
364
|
-
wrapper.className = "session-loop-wrapper";
|
|
365
|
-
|
|
366
|
-
// Group header row
|
|
367
|
-
var el = document.createElement("div");
|
|
368
|
-
var groupClass = "session-loop-group" + (hasActive ? " active" : "") + (expanded ? " expanded" : "");
|
|
369
|
-
if (isDebate) groupClass += " debate";
|
|
370
|
-
else if (!isRalph) groupClass += " scheduled";
|
|
371
|
-
el.className = groupClass;
|
|
372
|
-
el.dataset.loopId = loopId;
|
|
373
|
-
|
|
374
|
-
var chevron = document.createElement("button");
|
|
375
|
-
chevron.className = "session-loop-chevron";
|
|
376
|
-
chevron.innerHTML = iconHtml("chevron-right");
|
|
377
|
-
chevron.addEventListener("click", (function (lid) {
|
|
378
|
-
return function (e) {
|
|
379
|
-
e.stopPropagation();
|
|
380
|
-
if (expandedLoopGroups.has(lid)) {
|
|
381
|
-
expandedLoopGroups.delete(lid);
|
|
382
|
-
} else {
|
|
383
|
-
expandedLoopGroups.add(lid);
|
|
384
|
-
}
|
|
385
|
-
renderSessionList(null);
|
|
386
|
-
};
|
|
387
|
-
})(gk));
|
|
388
|
-
el.appendChild(chevron);
|
|
389
|
-
|
|
390
|
-
var textSpan = document.createElement("span");
|
|
391
|
-
textSpan.className = "session-item-text";
|
|
392
|
-
var textHtml = "";
|
|
393
|
-
if (anyProcessing) {
|
|
394
|
-
textHtml += '<span class="session-processing"></span>';
|
|
151
|
+
if (fileBrowserBtn) {
|
|
152
|
+
fileBrowserBtn.addEventListener("click", showFilesPanel);
|
|
395
153
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
textHtml += '<span class="session-loop-icon' + iconClass + '">' + iconHtml(groupIcon) + '</span>';
|
|
399
|
-
textHtml += '<span class="session-loop-name">' + escapeHtml(loopName) + '</span>';
|
|
400
|
-
if (isCrafting && children.length === 1) {
|
|
401
|
-
textHtml += '<span class="session-loop-badge crafting">Crafting</span>';
|
|
402
|
-
} else {
|
|
403
|
-
var countLabel = runCount === 1 ? children.length : runCount + (runCount === 1 ? " run" : " runs");
|
|
404
|
-
var countClass = isDebate ? " debate" : (isRalph ? "" : " scheduled");
|
|
405
|
-
textHtml += '<span class="session-loop-count' + countClass + '">' + countLabel + '</span>';
|
|
154
|
+
if (filePanelClose) {
|
|
155
|
+
filePanelClose.addEventListener("click", showSessionsPanel);
|
|
406
156
|
}
|
|
407
|
-
textSpan.innerHTML = textHtml;
|
|
408
|
-
el.appendChild(textSpan);
|
|
409
|
-
|
|
410
|
-
// More button (ellipsis)
|
|
411
|
-
var moreBtn = document.createElement("button");
|
|
412
|
-
moreBtn.className = "session-more-btn";
|
|
413
|
-
moreBtn.innerHTML = iconHtml("ellipsis");
|
|
414
|
-
moreBtn.title = "More options";
|
|
415
|
-
moreBtn.addEventListener("click", (function (lid, name, count, btn) {
|
|
416
|
-
return function (e) {
|
|
417
|
-
e.stopPropagation();
|
|
418
|
-
showLoopCtxMenu(btn, lid, name, count);
|
|
419
|
-
};
|
|
420
|
-
})(loopId, loopName, children.length, moreBtn));
|
|
421
|
-
el.appendChild(moreBtn);
|
|
422
|
-
|
|
423
|
-
// Click row (not chevron/more) -> switch to latest session
|
|
424
|
-
el.addEventListener("click", (function (id) {
|
|
425
|
-
return function () {
|
|
426
|
-
if (ctx.ws && ctx.connected) {
|
|
427
|
-
ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
|
|
428
|
-
dismissOverlayPanels();
|
|
429
|
-
closeSidebar();
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
})(latestSession.id));
|
|
433
157
|
|
|
434
|
-
|
|
158
|
+
// --- User island width sync ---
|
|
159
|
+
var userIsland = document.getElementById("user-island");
|
|
160
|
+
var sidebarColumn = document.getElementById("sidebar-column");
|
|
435
161
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
var
|
|
439
|
-
|
|
162
|
+
function syncUserIslandWidth() {
|
|
163
|
+
if (!userIsland) return;
|
|
164
|
+
var mateSidebarColumn = document.getElementById("mate-sidebar-column");
|
|
165
|
+
var isMateDM = document.body.classList.contains("mate-dm-active");
|
|
166
|
+
var col = (isMateDM && mateSidebarColumn && !mateSidebarColumn.classList.contains("hidden")) ? mateSidebarColumn : sidebarColumn;
|
|
167
|
+
if (!col) return;
|
|
168
|
+
var rect = col.getBoundingClientRect();
|
|
169
|
+
userIsland.style.width = (rect.right - 8 - 8) + "px";
|
|
170
|
+
}
|
|
440
171
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
var singleRun = runMap[runKeys[0]];
|
|
444
|
-
for (var sk = 0; sk < singleRun.length; sk++) {
|
|
445
|
-
childContainer.appendChild(renderLoopChild(singleRun[sk]));
|
|
446
|
-
}
|
|
447
|
-
} else {
|
|
448
|
-
// Multiple runs: render each run as a collapsible sub-group
|
|
449
|
-
for (var rk = 0; rk < runKeys.length; rk++) {
|
|
450
|
-
childContainer.appendChild(renderLoopRun(gk, runKeys[rk], runMap[runKeys[rk]], isRalph));
|
|
451
|
-
}
|
|
452
|
-
}
|
|
172
|
+
// --- Sidebar resize handle ---
|
|
173
|
+
var resizeHandle = document.getElementById("sidebar-resize-handle");
|
|
453
174
|
|
|
454
|
-
|
|
175
|
+
function syncResizeHandle() {
|
|
176
|
+
if (!resizeHandle || !sidebarColumn) return;
|
|
177
|
+
var rect = sidebarColumn.getBoundingClientRect();
|
|
178
|
+
var parentRect = sidebarColumn.parentElement.getBoundingClientRect();
|
|
179
|
+
resizeHandle.style.left = (rect.right - parentRect.left) + "px";
|
|
455
180
|
}
|
|
456
181
|
|
|
457
|
-
|
|
458
|
-
|
|
182
|
+
if (resizeHandle && sidebarColumn) {
|
|
183
|
+
var dragging = false;
|
|
459
184
|
|
|
460
|
-
function
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
185
|
+
function onResizeMove(e) {
|
|
186
|
+
if (!dragging) return;
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
var clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
|
189
|
+
var iconStrip = document.getElementById("icon-strip");
|
|
190
|
+
var stripWidth = iconStrip ? iconStrip.offsetWidth : 72;
|
|
191
|
+
var newWidth = clientX - stripWidth;
|
|
192
|
+
if (newWidth < 192) newWidth = 192;
|
|
193
|
+
if (newWidth > 320) newWidth = 320;
|
|
194
|
+
sidebarColumn.style.width = newWidth + "px";
|
|
195
|
+
syncResizeHandle();
|
|
196
|
+
syncUserIslandWidth();
|
|
197
|
+
}
|
|
465
198
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
199
|
+
function onResizeEnd() {
|
|
200
|
+
if (!dragging) return;
|
|
201
|
+
dragging = false;
|
|
202
|
+
resizeHandle.classList.remove("dragging");
|
|
203
|
+
document.body.style.cursor = "";
|
|
204
|
+
document.body.style.userSelect = "";
|
|
205
|
+
document.removeEventListener("mousemove", onResizeMove);
|
|
206
|
+
document.removeEventListener("mouseup", onResizeEnd);
|
|
207
|
+
document.removeEventListener("touchmove", onResizeMove);
|
|
208
|
+
document.removeEventListener("touchend", onResizeEnd);
|
|
209
|
+
try { localStorage.setItem("sidebar-width", sidebarColumn.style.width); } catch (e) {}
|
|
474
210
|
}
|
|
475
|
-
}
|
|
476
211
|
|
|
477
|
-
|
|
478
|
-
|
|
212
|
+
function onResizeStart(e) {
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
dragging = true;
|
|
215
|
+
resizeHandle.classList.add("dragging");
|
|
216
|
+
document.body.style.cursor = "col-resize";
|
|
217
|
+
document.body.style.userSelect = "none";
|
|
218
|
+
document.addEventListener("mousemove", onResizeMove);
|
|
219
|
+
document.addEventListener("mouseup", onResizeEnd);
|
|
220
|
+
document.addEventListener("touchmove", onResizeMove, { passive: false });
|
|
221
|
+
document.addEventListener("touchend", onResizeEnd);
|
|
222
|
+
}
|
|
479
223
|
|
|
480
|
-
|
|
481
|
-
|
|
224
|
+
resizeHandle.addEventListener("mousedown", onResizeStart);
|
|
225
|
+
resizeHandle.addEventListener("touchstart", onResizeStart, { passive: false });
|
|
482
226
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
227
|
+
// Restore saved width (skip transition so user-island syncs immediately)
|
|
228
|
+
try {
|
|
229
|
+
var savedWidth = localStorage.getItem("sidebar-width");
|
|
230
|
+
if (savedWidth) {
|
|
231
|
+
var px = parseInt(savedWidth, 10);
|
|
232
|
+
if (px >= 192 && px <= 320) {
|
|
233
|
+
sidebarColumn.style.transition = "none";
|
|
234
|
+
sidebarColumn.style.width = px + "px";
|
|
235
|
+
sidebarColumn.offsetWidth; // force reflow
|
|
236
|
+
sidebarColumn.style.transition = "";
|
|
237
|
+
}
|
|
493
238
|
}
|
|
494
|
-
|
|
495
|
-
};
|
|
496
|
-
})(runGk));
|
|
497
|
-
el.appendChild(chevron);
|
|
239
|
+
} catch (e) {}
|
|
498
240
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
var textHtml = "";
|
|
502
|
-
if (anyProcessing) {
|
|
503
|
-
textHtml += '<span class="session-processing"></span>';
|
|
241
|
+
syncResizeHandle();
|
|
242
|
+
syncUserIslandWidth();
|
|
504
243
|
}
|
|
505
|
-
textHtml += '<span class="session-loop-run-time">' + escapeHtml(timeLabel) + '</span>';
|
|
506
|
-
textHtml += '<span class="session-loop-count' + (isRalph ? "" : " scheduled") + '">' + sessions.length + '</span>';
|
|
507
|
-
textSpan.innerHTML = textHtml;
|
|
508
|
-
el.appendChild(textSpan);
|
|
509
|
-
|
|
510
|
-
// Click row -> switch to latest session of this run
|
|
511
|
-
el.addEventListener("click", (function (id) {
|
|
512
|
-
return function () {
|
|
513
|
-
if (ctx.ws && ctx.connected) {
|
|
514
|
-
ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
|
|
515
|
-
dismissOverlayPanels();
|
|
516
|
-
closeSidebar();
|
|
517
|
-
}
|
|
518
|
-
};
|
|
519
|
-
})(latestSession.id));
|
|
520
244
|
|
|
521
|
-
|
|
245
|
+
// Initial sync even if no resize handle
|
|
246
|
+
syncUserIslandWidth();
|
|
522
247
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
248
|
+
// --- User island tooltip on hover (collapsed sidebar) ---
|
|
249
|
+
if (userIsland) {
|
|
250
|
+
var profileArea = userIsland.querySelector(".user-island-profile");
|
|
251
|
+
if (profileArea) {
|
|
252
|
+
profileArea.addEventListener("mouseenter", function () {
|
|
253
|
+
var layout = document.getElementById("layout");
|
|
254
|
+
if (!layout || !layout.classList.contains("sidebar-collapsed")) return;
|
|
255
|
+
var nameEl = userIsland.querySelector(".user-island-name");
|
|
256
|
+
var text = nameEl ? nameEl.textContent : "";
|
|
257
|
+
if (text) showIconTooltip(profileArea, text);
|
|
258
|
+
});
|
|
259
|
+
profileArea.addEventListener("mouseleave", function () {
|
|
260
|
+
hideIconTooltip();
|
|
261
|
+
});
|
|
528
262
|
}
|
|
529
|
-
wrapper.appendChild(childContainer);
|
|
530
263
|
}
|
|
531
264
|
|
|
532
|
-
return wrapper;
|
|
533
265
|
}
|
|
534
266
|
|
|
535
|
-
function
|
|
536
|
-
var
|
|
537
|
-
var
|
|
538
|
-
var
|
|
539
|
-
|
|
540
|
-
|
|
267
|
+
export function spawnDustParticles(cx, cy) {
|
|
268
|
+
var colors = ["#8B7355", "#A0522D", "#D2B48C", "#C4A882", "#9E9E9E", "#B8860B", "#BC8F8F"];
|
|
269
|
+
var count = 24;
|
|
270
|
+
var container = document.createElement("div");
|
|
271
|
+
container.style.position = "fixed";
|
|
272
|
+
container.style.top = "0";
|
|
273
|
+
container.style.left = "0";
|
|
274
|
+
container.style.width = "0";
|
|
275
|
+
container.style.height = "0";
|
|
276
|
+
container.style.pointerEvents = "none";
|
|
277
|
+
container.style.zIndex = "10000";
|
|
278
|
+
document.body.appendChild(container);
|
|
541
279
|
|
|
542
|
-
var
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
if (ctx.multiUser && s.sessionVisibility === "private") {
|
|
552
|
-
textHtml += '<span class="session-private-icon" title="Private session">' + iconHtml("lock") + '</span>';
|
|
553
|
-
}
|
|
554
|
-
textHtml += highlightMatch(s.title || "New Session", searchQuery);
|
|
555
|
-
textSpan.innerHTML = textHtml;
|
|
556
|
-
el.appendChild(textSpan);
|
|
557
|
-
|
|
558
|
-
// Right-click / long-press: context menu
|
|
559
|
-
el.addEventListener("contextmenu", (function(id, title, cliSid, anchor, sData) {
|
|
560
|
-
return function(e) {
|
|
561
|
-
e.preventDefault();
|
|
562
|
-
e.stopPropagation();
|
|
563
|
-
showSessionCtxMenu(anchor, id, title, cliSid, sData);
|
|
564
|
-
};
|
|
565
|
-
})(s.id, s.title, s.cliSessionId, el, s));
|
|
566
|
-
|
|
567
|
-
// Unread badge
|
|
568
|
-
var unreadBadge = document.createElement("span");
|
|
569
|
-
unreadBadge.className = "session-unread-badge";
|
|
570
|
-
unreadBadge.dataset.sessionId = s.id;
|
|
571
|
-
if (s.unread > 0) {
|
|
572
|
-
unreadBadge.textContent = s.unread > 99 ? "99+" : String(s.unread);
|
|
573
|
-
unreadBadge.classList.add("has-unread");
|
|
574
|
-
}
|
|
575
|
-
el.appendChild(unreadBadge);
|
|
576
|
-
|
|
577
|
-
el.addEventListener("click", (function (id) {
|
|
578
|
-
return function () {
|
|
579
|
-
if (ctx.ws && ctx.connected) {
|
|
580
|
-
var pendingQuery = searchQuery || "";
|
|
581
|
-
ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
|
|
582
|
-
dismissOverlayPanels();
|
|
583
|
-
closeSidebar();
|
|
584
|
-
if (pendingQuery) {
|
|
585
|
-
setTimeout(function () { openSessionSearch(pendingQuery); }, 400);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
};
|
|
589
|
-
})(s.id));
|
|
590
|
-
|
|
591
|
-
// Presence avatars (multi-user)
|
|
592
|
-
renderPresenceAvatars(el, String(s.id));
|
|
593
|
-
|
|
594
|
-
return el;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
export function renderSessionList(sessions) {
|
|
598
|
-
if (sessions) cachedSessions = sessions;
|
|
599
|
-
|
|
600
|
-
// If mobile chat sheet is open, refresh its session list
|
|
601
|
-
refreshMobileChatSheet();
|
|
602
|
-
|
|
603
|
-
ctx.sessionListEl.innerHTML = "";
|
|
604
|
-
|
|
605
|
-
// Partition: loop sessions vs normal sessions
|
|
606
|
-
// Group by loopId + date so all runs of the same task on the same day are merged
|
|
607
|
-
var loopGroups = {}; // groupKey -> [sessions]
|
|
608
|
-
var normalSessions = [];
|
|
609
|
-
for (var i = 0; i < cachedSessions.length; i++) {
|
|
610
|
-
var s = cachedSessions[i];
|
|
611
|
-
if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph" && s.loop.source !== "debate") {
|
|
612
|
-
// Task crafting sessions live in the scheduler calendar, not the main list (except debate)
|
|
613
|
-
continue;
|
|
614
|
-
} else if (s.loop && s.loop.loopId) {
|
|
615
|
-
var startedAt = s.loop.startedAt || 0;
|
|
616
|
-
var dateStr = startedAt ? new Date(startedAt).toISOString().slice(0, 10) : "unknown";
|
|
617
|
-
var groupKey = s.loop.loopId + ":" + dateStr;
|
|
618
|
-
if (!loopGroups[groupKey]) loopGroups[groupKey] = [];
|
|
619
|
-
loopGroups[groupKey].push(s);
|
|
620
|
-
} else {
|
|
621
|
-
normalSessions.push(s);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// Build virtual items: normal sessions + one entry per loop group (using latest child's lastActivity)
|
|
626
|
-
var items = [];
|
|
627
|
-
for (var j = 0; j < normalSessions.length; j++) {
|
|
628
|
-
items.push({ type: "session", data: normalSessions[j], lastActivity: normalSessions[j].lastActivity || 0 });
|
|
629
|
-
}
|
|
630
|
-
var groupKeys = Object.keys(loopGroups);
|
|
631
|
-
for (var k = 0; k < groupKeys.length; k++) {
|
|
632
|
-
var gk = groupKeys[k];
|
|
633
|
-
var children = loopGroups[gk];
|
|
634
|
-
var realLoopId = children[0].loop.loopId;
|
|
635
|
-
var maxActivity = 0;
|
|
636
|
-
for (var m = 0; m < children.length; m++) {
|
|
637
|
-
var act = children[m].lastActivity || 0;
|
|
638
|
-
if (act > maxActivity) maxActivity = act;
|
|
639
|
-
}
|
|
640
|
-
items.push({ type: "loop", loopId: realLoopId, groupKey: gk, children: children, lastActivity: maxActivity });
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// Sort by lastActivity descending
|
|
644
|
-
items.sort(function (a, b) {
|
|
645
|
-
return (b.lastActivity || 0) - (a.lastActivity || 0);
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
var currentGroup = "";
|
|
649
|
-
for (var n = 0; n < items.length; n++) {
|
|
650
|
-
var item = items[n];
|
|
651
|
-
var group = getDateGroup(item.lastActivity || 0);
|
|
652
|
-
if (group !== currentGroup) {
|
|
653
|
-
currentGroup = group;
|
|
654
|
-
var header = document.createElement("div");
|
|
655
|
-
header.className = "session-group-header";
|
|
656
|
-
header.textContent = group;
|
|
657
|
-
ctx.sessionListEl.appendChild(header);
|
|
658
|
-
}
|
|
659
|
-
if (item.type === "loop") {
|
|
660
|
-
ctx.sessionListEl.appendChild(renderLoopGroup(item.loopId, item.children, item.groupKey));
|
|
661
|
-
} else {
|
|
662
|
-
ctx.sessionListEl.appendChild(renderSessionItem(item.data));
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
refreshIcons();
|
|
666
|
-
updatePageTitle();
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
export function handleSearchResults(msg) {
|
|
670
|
-
if (msg.query !== searchQuery) return; // stale response
|
|
671
|
-
var ids = new Set();
|
|
672
|
-
for (var i = 0; i < msg.results.length; i++) {
|
|
673
|
-
ids.add(msg.results[i].id);
|
|
674
|
-
}
|
|
675
|
-
searchMatchIds = ids;
|
|
676
|
-
renderSessionList(null);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
export function updateSessionPresence(presence) {
|
|
680
|
-
sessionPresence = presence;
|
|
681
|
-
// Update presence avatars on existing session items without full re-render
|
|
682
|
-
var items = ctx.sessionListEl.querySelectorAll("[data-session-id]");
|
|
683
|
-
for (var i = 0; i < items.length; i++) {
|
|
684
|
-
renderPresenceAvatars(items[i], items[i].dataset.sessionId);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function presenceAvatarUrl(userOrStyle, seed) {
|
|
689
|
-
if (userOrStyle && typeof userOrStyle === "object") return userAvatarUrl(userOrStyle, 24);
|
|
690
|
-
return avatarUrl(userOrStyle || "thumbs", seed, 24);
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
function renderPresenceAvatars(el, sessionId) {
|
|
694
|
-
// Remove existing presence container
|
|
695
|
-
var existing = el.querySelector(".session-presence");
|
|
696
|
-
if (existing) existing.remove();
|
|
697
|
-
|
|
698
|
-
var users = sessionPresence[sessionId];
|
|
699
|
-
if (!users || users.length === 0) return;
|
|
700
|
-
|
|
701
|
-
var container = document.createElement("span");
|
|
702
|
-
container.className = "session-presence";
|
|
703
|
-
|
|
704
|
-
var max = 3;
|
|
705
|
-
var shown = users.length > max ? max : users.length;
|
|
706
|
-
for (var i = 0; i < shown; i++) {
|
|
707
|
-
var u = users[i];
|
|
708
|
-
var img = document.createElement("img");
|
|
709
|
-
img.className = "session-presence-avatar";
|
|
710
|
-
img.src = presenceAvatarUrl(u);
|
|
711
|
-
img.alt = u.displayName;
|
|
712
|
-
img.dataset.tip = u.displayName + (u.username ? " (@" + u.username + ")" : "");
|
|
713
|
-
if (i > 0) img.style.marginLeft = "-6px";
|
|
714
|
-
container.appendChild(img);
|
|
715
|
-
}
|
|
716
|
-
if (users.length > max) {
|
|
717
|
-
var more = document.createElement("span");
|
|
718
|
-
more.className = "session-presence-more";
|
|
719
|
-
more.textContent = "+" + (users.length - max);
|
|
720
|
-
container.appendChild(more);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// Insert before the more-btn
|
|
724
|
-
var moreBtn = el.querySelector(".session-more-btn");
|
|
725
|
-
if (moreBtn) {
|
|
726
|
-
el.insertBefore(container, moreBtn);
|
|
727
|
-
} else {
|
|
728
|
-
el.appendChild(container);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
export function updatePageTitle() {
|
|
733
|
-
var sessionTitle = "";
|
|
734
|
-
var activeItem = ctx.sessionListEl.querySelector(".session-item.active .session-item-text");
|
|
735
|
-
if (activeItem) sessionTitle = activeItem.textContent;
|
|
736
|
-
if (ctx.headerTitleEl) {
|
|
737
|
-
ctx.headerTitleEl.textContent = sessionTitle || ctx.projectName || "Clay";
|
|
738
|
-
}
|
|
739
|
-
var tbProjectName = ctx.$("title-bar-project-name");
|
|
740
|
-
if (tbProjectName && ctx.projectName) {
|
|
741
|
-
tbProjectName.textContent = ctx.projectName;
|
|
742
|
-
} else if (tbProjectName && !tbProjectName.textContent) {
|
|
743
|
-
// Fallback: derive name from URL slug when projectName not yet available
|
|
744
|
-
var _m = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
|
|
745
|
-
if (_m) tbProjectName.textContent = _m[1];
|
|
746
|
-
}
|
|
747
|
-
if (ctx.projectName && sessionTitle) {
|
|
748
|
-
document.title = sessionTitle + " - " + ctx.projectName;
|
|
749
|
-
} else if (ctx.projectName) {
|
|
750
|
-
document.title = ctx.projectName + " - Clay";
|
|
751
|
-
} else {
|
|
752
|
-
document.title = "Clay";
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
export function openSidebar() {
|
|
757
|
-
ctx.sidebar.classList.add("open");
|
|
758
|
-
ctx.sidebarOverlay.classList.add("visible");
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
export function closeSidebar() {
|
|
762
|
-
ctx.sidebar.classList.remove("open");
|
|
763
|
-
ctx.sidebarOverlay.classList.remove("visible");
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// --- Mobile sheet (fullscreen overlay for Projects / Sessions / Mate Profile) ---
|
|
767
|
-
|
|
768
|
-
var mobileSheetMateData = null;
|
|
769
|
-
|
|
770
|
-
export function setMobileSheetMateData(data) {
|
|
771
|
-
mobileSheetMateData = data;
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
export function openMobileSheet(type) {
|
|
775
|
-
var sheet = document.getElementById("mobile-sheet");
|
|
776
|
-
if (!sheet) return;
|
|
777
|
-
|
|
778
|
-
var titleEl = sheet.querySelector(".mobile-sheet-title");
|
|
779
|
-
var listEl = sheet.querySelector(".mobile-sheet-list");
|
|
780
|
-
if (!titleEl || !listEl) return;
|
|
781
|
-
|
|
782
|
-
// Return file tree to sidebar before clearing (prevents destroying it)
|
|
783
|
-
if (sheet.classList.contains("sheet-files")) {
|
|
784
|
-
var prevFileTree = document.getElementById("file-tree");
|
|
785
|
-
var prevPanel = document.getElementById("sidebar-panel-files");
|
|
786
|
-
if (prevFileTree && prevPanel) prevPanel.appendChild(prevFileTree);
|
|
787
|
-
}
|
|
788
|
-
// Return knowledge files to mate sidebar before clearing
|
|
789
|
-
if (sheet.classList.contains("sheet-knowledge")) {
|
|
790
|
-
var prevKnowledge = document.getElementById("mate-knowledge-files");
|
|
791
|
-
var prevKnowledgePanel = document.getElementById("mate-sidebar-knowledge");
|
|
792
|
-
if (prevKnowledge && prevKnowledgePanel) prevKnowledgePanel.appendChild(prevKnowledge);
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
listEl.innerHTML = "";
|
|
796
|
-
sheet.classList.remove("sheet-files", "sheet-knowledge");
|
|
797
|
-
|
|
798
|
-
if (type === "projects") {
|
|
799
|
-
titleEl.textContent = "Projects";
|
|
800
|
-
renderSheetProjects(listEl);
|
|
801
|
-
} else if (type === "sessions") {
|
|
802
|
-
titleEl.textContent = "Chat";
|
|
803
|
-
renderSheetSessions(listEl);
|
|
804
|
-
} else if (type === "files") {
|
|
805
|
-
titleEl.textContent = "Files";
|
|
806
|
-
sheet.classList.add("sheet-files");
|
|
807
|
-
var fileTree = document.getElementById("file-tree");
|
|
808
|
-
if (fileTree) {
|
|
809
|
-
listEl.appendChild(fileTree);
|
|
810
|
-
fileTree.classList.remove("hidden");
|
|
811
|
-
}
|
|
812
|
-
if (ctx.onFilesTabOpen) ctx.onFilesTabOpen();
|
|
813
|
-
} else if (type === "mate-knowledge") {
|
|
814
|
-
titleEl.textContent = "Knowledge";
|
|
815
|
-
sheet.classList.add("sheet-knowledge");
|
|
816
|
-
var knowledgeFiles = document.getElementById("mate-knowledge-files");
|
|
817
|
-
if (knowledgeFiles) {
|
|
818
|
-
listEl.appendChild(knowledgeFiles);
|
|
819
|
-
knowledgeFiles.classList.remove("hidden");
|
|
820
|
-
}
|
|
821
|
-
// Request knowledge list if not loaded
|
|
822
|
-
if (ctx.requestKnowledgeList) ctx.requestKnowledgeList();
|
|
823
|
-
} else if (type === "mate-profile") {
|
|
824
|
-
titleEl.textContent = "";
|
|
825
|
-
renderSheetMateProfile(listEl);
|
|
826
|
-
} else if (type === "search") {
|
|
827
|
-
titleEl.textContent = "Search";
|
|
828
|
-
renderSheetSearch(listEl);
|
|
829
|
-
} else if (type === "tools") {
|
|
830
|
-
titleEl.textContent = "Tools";
|
|
831
|
-
renderSheetTools(listEl);
|
|
832
|
-
} else if (type === "settings") {
|
|
833
|
-
titleEl.textContent = "Settings";
|
|
834
|
-
renderSheetSettings(listEl);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
sheet.classList.remove("hidden", "closing");
|
|
838
|
-
refreshIcons();
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
function closeMobileSheet() {
|
|
842
|
-
var sheet = document.getElementById("mobile-sheet");
|
|
843
|
-
if (!sheet || sheet.classList.contains("hidden")) return;
|
|
844
|
-
|
|
845
|
-
mobileChatSheetOpen = false;
|
|
846
|
-
|
|
847
|
-
// Return file tree to sidebar if it was moved
|
|
848
|
-
if (sheet.classList.contains("sheet-files")) {
|
|
849
|
-
var fileTree = document.getElementById("file-tree");
|
|
850
|
-
var sidebarFilesPanel = document.getElementById("sidebar-panel-files");
|
|
851
|
-
if (fileTree && sidebarFilesPanel) {
|
|
852
|
-
sidebarFilesPanel.appendChild(fileTree);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
// Return knowledge files to mate sidebar if moved
|
|
856
|
-
if (sheet.classList.contains("sheet-knowledge")) {
|
|
857
|
-
var knowledgeFiles = document.getElementById("mate-knowledge-files");
|
|
858
|
-
var knowledgePanel = document.getElementById("mate-sidebar-knowledge");
|
|
859
|
-
if (knowledgeFiles && knowledgePanel) {
|
|
860
|
-
knowledgePanel.appendChild(knowledgeFiles);
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
sheet.classList.add("closing");
|
|
865
|
-
setTimeout(function () {
|
|
866
|
-
sheet.classList.add("hidden");
|
|
867
|
-
sheet.classList.remove("closing", "sheet-files");
|
|
868
|
-
}, 230);
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
function renderSheetProjects(listEl) {
|
|
872
|
-
for (var i = 0; i < cachedProjectList.length; i++) {
|
|
873
|
-
(function (p) {
|
|
874
|
-
var el = document.createElement("button");
|
|
875
|
-
el.className = "mobile-project-item" + (p.slug === cachedCurrentSlug ? " active" : "");
|
|
876
|
-
|
|
877
|
-
var abbrev = document.createElement("span");
|
|
878
|
-
abbrev.className = "mobile-project-abbrev";
|
|
879
|
-
if (p.icon) {
|
|
880
|
-
abbrev.textContent = p.icon;
|
|
881
|
-
parseEmojis(abbrev);
|
|
882
|
-
} else {
|
|
883
|
-
abbrev.textContent = getProjectAbbrev(p.name);
|
|
884
|
-
}
|
|
885
|
-
el.appendChild(abbrev);
|
|
886
|
-
|
|
887
|
-
var name = document.createElement("span");
|
|
888
|
-
name.className = "mobile-project-name";
|
|
889
|
-
name.textContent = p.name;
|
|
890
|
-
el.appendChild(name);
|
|
891
|
-
|
|
892
|
-
if (p.isProcessing) {
|
|
893
|
-
var dot = document.createElement("span");
|
|
894
|
-
dot.className = "mobile-project-processing";
|
|
895
|
-
el.appendChild(dot);
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
if (p.unread > 0 && p.slug !== cachedCurrentSlug) {
|
|
899
|
-
var mBadge = document.createElement("span");
|
|
900
|
-
mBadge.className = "mobile-project-unread";
|
|
901
|
-
mBadge.textContent = p.unread > 99 ? "99+" : String(p.unread);
|
|
902
|
-
el.appendChild(mBadge);
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
el.addEventListener("click", function () {
|
|
906
|
-
if (ctx.switchProject) ctx.switchProject(p.slug);
|
|
907
|
-
closeMobileSheet();
|
|
908
|
-
});
|
|
909
|
-
|
|
910
|
-
listEl.appendChild(el);
|
|
911
|
-
})(cachedProjectList[i]);
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
function renderSheetSessions(listEl) {
|
|
916
|
-
// --- Context filter bar (horizontal scroll) ---
|
|
917
|
-
var filterBar = document.createElement("div");
|
|
918
|
-
filterBar.className = "mobile-chat-filter-bar";
|
|
919
|
-
|
|
920
|
-
// Current project chip (always first, pre-selected)
|
|
921
|
-
var currentProject = null;
|
|
922
|
-
for (var pi = 0; pi < cachedProjectList.length; pi++) {
|
|
923
|
-
if (cachedProjectList[pi].slug === cachedCurrentSlug) {
|
|
924
|
-
currentProject = cachedProjectList[pi];
|
|
925
|
-
break;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
// Build chips: projects first, then mates
|
|
930
|
-
var chips = [];
|
|
931
|
-
|
|
932
|
-
for (var ci = 0; ci < cachedProjectList.length; ci++) {
|
|
933
|
-
(function (p) {
|
|
934
|
-
var chip = document.createElement("button");
|
|
935
|
-
chip.className = "mobile-chat-chip";
|
|
936
|
-
var isDmActive = document.body.classList.contains("mate-dm-active");
|
|
937
|
-
if (p.slug === cachedCurrentSlug && !isDmActive) chip.classList.add("active");
|
|
938
|
-
chip.dataset.type = "project";
|
|
939
|
-
chip.dataset.slug = p.slug;
|
|
940
|
-
|
|
941
|
-
var abbrev = document.createElement("span");
|
|
942
|
-
abbrev.className = "mobile-chat-chip-icon";
|
|
943
|
-
if (p.icon) {
|
|
944
|
-
abbrev.textContent = p.icon;
|
|
945
|
-
parseEmojis(abbrev);
|
|
946
|
-
} else {
|
|
947
|
-
abbrev.textContent = getProjectAbbrev(p.name);
|
|
948
|
-
}
|
|
949
|
-
chip.appendChild(abbrev);
|
|
950
|
-
|
|
951
|
-
var label = document.createElement("span");
|
|
952
|
-
label.textContent = p.name;
|
|
953
|
-
chip.appendChild(label);
|
|
954
|
-
|
|
955
|
-
// Processing dot: same class as icon strip
|
|
956
|
-
var statusDot = document.createElement("span");
|
|
957
|
-
statusDot.className = "icon-strip-status";
|
|
958
|
-
if (p.isProcessing) statusDot.classList.add("processing");
|
|
959
|
-
chip.appendChild(statusDot);
|
|
960
|
-
|
|
961
|
-
if (p.unread > 0 && p.slug !== cachedCurrentSlug) {
|
|
962
|
-
var badge = document.createElement("span");
|
|
963
|
-
badge.className = "mobile-chat-chip-badge";
|
|
964
|
-
badge.textContent = p.unread > 99 ? "99+" : String(p.unread);
|
|
965
|
-
chip.appendChild(badge);
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
chips.push(chip);
|
|
969
|
-
})(cachedProjectList[ci]);
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
var favoriteChipMates = cachedMates.filter(function (m) {
|
|
973
|
-
if (cachedDmRemovedUsers[m.id]) return false;
|
|
974
|
-
if (cachedDmFavorites.indexOf(m.id) !== -1) return true;
|
|
975
|
-
if (cachedDmUnread[m.id] && cachedDmUnread[m.id] > 0) return true;
|
|
976
|
-
return false;
|
|
977
|
-
});
|
|
978
|
-
var sortedChipMates = favoriteChipMates.sort(function (a, b) {
|
|
979
|
-
var aBuiltin = a.builtinKey ? 1 : 0;
|
|
980
|
-
var bBuiltin = b.builtinKey ? 1 : 0;
|
|
981
|
-
if (aBuiltin !== bBuiltin) return bBuiltin - aBuiltin;
|
|
982
|
-
return (a.createdAt || 0) - (b.createdAt || 0);
|
|
983
|
-
});
|
|
984
|
-
for (var mi = 0; mi < sortedChipMates.length; mi++) {
|
|
985
|
-
(function (mate) {
|
|
986
|
-
var mp = mate.profile || {};
|
|
987
|
-
var chip = document.createElement("button");
|
|
988
|
-
chip.className = "mobile-chat-chip";
|
|
989
|
-
if (currentDmUserId === mate.id) chip.classList.add("active");
|
|
990
|
-
chip.dataset.type = "mate";
|
|
991
|
-
chip.dataset.mateId = mate.id;
|
|
992
|
-
|
|
993
|
-
var avatarEl = document.createElement("img");
|
|
994
|
-
avatarEl.className = "mobile-chat-chip-avatar";
|
|
995
|
-
avatarEl.src = mateAvatarUrl(mate, 20);
|
|
996
|
-
avatarEl.alt = mp.displayName || mate.name || "";
|
|
997
|
-
chip.appendChild(avatarEl);
|
|
998
|
-
|
|
999
|
-
var label = document.createElement("span");
|
|
1000
|
-
label.textContent = mp.displayName || mate.name || "Mate";
|
|
1001
|
-
chip.appendChild(label);
|
|
1002
|
-
|
|
1003
|
-
// Processing dot: same class as icon strip, same data source
|
|
1004
|
-
var mateSlug = "mate-" + mate.id;
|
|
1005
|
-
var mateProj = null;
|
|
1006
|
-
var allProjects = (ctx && ctx.projectList) || [];
|
|
1007
|
-
for (var pi = 0; pi < allProjects.length; pi++) {
|
|
1008
|
-
if (allProjects[pi].slug === mateSlug) { mateProj = allProjects[pi]; break; }
|
|
1009
|
-
}
|
|
1010
|
-
var statusDot = document.createElement("span");
|
|
1011
|
-
statusDot.className = "icon-strip-status";
|
|
1012
|
-
if (mateProj && mateProj.isProcessing) statusDot.classList.add("processing");
|
|
1013
|
-
chip.appendChild(statusDot);
|
|
1014
|
-
|
|
1015
|
-
var unreadCount = cachedDmUnread[mate.id] || 0;
|
|
1016
|
-
if (unreadCount > 0) {
|
|
1017
|
-
var badge = document.createElement("span");
|
|
1018
|
-
badge.className = "mobile-chat-chip-badge";
|
|
1019
|
-
badge.textContent = unreadCount > 99 ? "99+" : String(unreadCount);
|
|
1020
|
-
chip.appendChild(badge);
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
chips.push(chip);
|
|
1024
|
-
})(sortedChipMates[mi]);
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
for (var i = 0; i < chips.length; i++) {
|
|
1028
|
-
filterBar.appendChild(chips[i]);
|
|
1029
|
-
}
|
|
1030
|
-
listEl.appendChild(filterBar);
|
|
1031
|
-
|
|
1032
|
-
// --- Session list container ---
|
|
1033
|
-
var sessionListEl = document.createElement("div");
|
|
1034
|
-
sessionListEl.className = "mobile-chat-session-list";
|
|
1035
|
-
listEl.appendChild(sessionListEl);
|
|
1036
|
-
|
|
1037
|
-
// --- Render sessions for a context ---
|
|
1038
|
-
function renderSessionsForContext(type, slug, mateId) {
|
|
1039
|
-
sessionListEl.innerHTML = "";
|
|
1040
|
-
|
|
1041
|
-
if (type === "project") {
|
|
1042
|
-
renderMobileSessionsInto(sessionListEl);
|
|
1043
|
-
} else if (type === "mate") {
|
|
1044
|
-
// Mate DM: open the DM and show mate actions
|
|
1045
|
-
if (ctx.openDm) ctx.openDm(mateId);
|
|
1046
|
-
renderMateMobileActions(sessionListEl);
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
refreshIcons();
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
// --- Chip click handlers ---
|
|
1053
|
-
for (var j = 0; j < chips.length; j++) {
|
|
1054
|
-
(function (chip) {
|
|
1055
|
-
chip.addEventListener("click", function () {
|
|
1056
|
-
// Deactivate all chips
|
|
1057
|
-
for (var k = 0; k < chips.length; k++) {
|
|
1058
|
-
chips[k].classList.remove("active");
|
|
1059
|
-
}
|
|
1060
|
-
chip.classList.add("active");
|
|
1061
|
-
|
|
1062
|
-
var type = chip.dataset.type;
|
|
1063
|
-
if (type === "project") {
|
|
1064
|
-
var slug = chip.dataset.slug;
|
|
1065
|
-
var isDmNow = !!currentDmUserId;
|
|
1066
|
-
if (slug !== cachedCurrentSlug || isDmNow) {
|
|
1067
|
-
// Switch project (or exit DM back to same project)
|
|
1068
|
-
sessionListEl.innerHTML = "";
|
|
1069
|
-
if (slug !== cachedCurrentSlug) {
|
|
1070
|
-
var loading = document.createElement("div");
|
|
1071
|
-
loading.className = "mobile-chat-context-note";
|
|
1072
|
-
loading.textContent = "Loading sessions...";
|
|
1073
|
-
sessionListEl.appendChild(loading);
|
|
1074
|
-
}
|
|
1075
|
-
if (ctx.switchProject) ctx.switchProject(slug);
|
|
1076
|
-
if (!isDmNow || slug !== cachedCurrentSlug) {
|
|
1077
|
-
// renderSessionList will be called by WS, which calls refreshMobileChatSheet
|
|
1078
|
-
} else {
|
|
1079
|
-
// Exited DM, same project - render sessions now
|
|
1080
|
-
renderSessionsForContext("project", slug, null);
|
|
1081
|
-
}
|
|
1082
|
-
} else {
|
|
1083
|
-
renderSessionsForContext("project", slug, null);
|
|
1084
|
-
}
|
|
1085
|
-
} else if (type === "mate") {
|
|
1086
|
-
renderSessionsForContext("mate", null, chip.dataset.mateId);
|
|
1087
|
-
}
|
|
1088
|
-
});
|
|
1089
|
-
})(chips[j]);
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
// Track that chat sheet is open
|
|
1093
|
-
mobileChatSheetOpen = true;
|
|
1094
|
-
|
|
1095
|
-
// --- Initial render: show mate actions if DM active, otherwise project sessions ---
|
|
1096
|
-
if (currentDmUserId) {
|
|
1097
|
-
renderSessionsForContext("mate", null, currentDmUserId);
|
|
1098
|
-
} else {
|
|
1099
|
-
renderSessionsForContext("project", cachedCurrentSlug, null);
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
// Helper: create a mobile session item element
|
|
1104
|
-
function createMobileSessionItem(s) {
|
|
1105
|
-
var el = document.createElement("button");
|
|
1106
|
-
el.className = "mobile-session-item" + (s.active ? " active" : "");
|
|
1107
|
-
|
|
1108
|
-
// Processing dot (left side, before title)
|
|
1109
|
-
if (s.isProcessing) {
|
|
1110
|
-
var dot = document.createElement("span");
|
|
1111
|
-
dot.className = "mobile-session-processing";
|
|
1112
|
-
el.appendChild(dot);
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
var titleSpan = document.createElement("span");
|
|
1116
|
-
titleSpan.className = "mobile-session-title";
|
|
1117
|
-
titleSpan.textContent = s.title || "New Session";
|
|
1118
|
-
el.appendChild(titleSpan);
|
|
1119
|
-
|
|
1120
|
-
// Unread badge (right side)
|
|
1121
|
-
if (s.unread > 0 && !s.active) {
|
|
1122
|
-
var badge = document.createElement("span");
|
|
1123
|
-
badge.className = "mobile-session-unread";
|
|
1124
|
-
badge.textContent = s.unread > 99 ? "99+" : String(s.unread);
|
|
1125
|
-
el.appendChild(badge);
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
(function (id) {
|
|
1129
|
-
el.addEventListener("click", function () {
|
|
1130
|
-
if (ctx.ws && ctx.connected) {
|
|
1131
|
-
ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
|
|
1132
|
-
}
|
|
1133
|
-
dismissOverlayPanels();
|
|
1134
|
-
closeMobileSheet();
|
|
1135
|
-
});
|
|
1136
|
-
})(s.id);
|
|
1137
|
-
|
|
1138
|
-
return el;
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
// Helper: create a mobile loop child element (individual session inside a group)
|
|
1142
|
-
function createMobileLoopChild(s) {
|
|
1143
|
-
var el = document.createElement("button");
|
|
1144
|
-
el.className = "mobile-loop-child" + (s.active ? " active" : "");
|
|
1145
|
-
|
|
1146
|
-
if (s.isProcessing) {
|
|
1147
|
-
var dot = document.createElement("span");
|
|
1148
|
-
dot.className = "mobile-session-processing";
|
|
1149
|
-
el.appendChild(dot);
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
var textSpan = document.createElement("span");
|
|
1153
|
-
textSpan.className = "mobile-session-title";
|
|
1154
|
-
if (s.loop) {
|
|
1155
|
-
var isRalphChild = s.loop.source === "ralph";
|
|
1156
|
-
var roleName = s.loop.role === "crafting" ? "Crafting" : s.loop.role === "judge" ? "Judge" : (isRalphChild ? "Coder" : "Run");
|
|
1157
|
-
var iterSuffix = s.loop.role === "crafting" ? "" : " #" + s.loop.iteration;
|
|
1158
|
-
var roleCls = s.loop.role === "crafting" ? " crafting" : (!isRalphChild ? " scheduled" : "");
|
|
1159
|
-
var badge = document.createElement("span");
|
|
1160
|
-
badge.className = "mobile-loop-role-badge" + roleCls;
|
|
1161
|
-
badge.textContent = roleName + iterSuffix;
|
|
1162
|
-
textSpan.appendChild(badge);
|
|
1163
|
-
}
|
|
1164
|
-
el.appendChild(textSpan);
|
|
1165
|
-
|
|
1166
|
-
(function (id) {
|
|
1167
|
-
el.addEventListener("click", function () {
|
|
1168
|
-
if (ctx.ws && ctx.connected) {
|
|
1169
|
-
ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
|
|
1170
|
-
}
|
|
1171
|
-
dismissOverlayPanels();
|
|
1172
|
-
closeMobileSheet();
|
|
1173
|
-
});
|
|
1174
|
-
})(s.id);
|
|
1175
|
-
|
|
1176
|
-
return el;
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
// Helper: create a mobile loop run sub-group (collapsible time group)
|
|
1180
|
-
function createMobileLoopRun(parentGk, startedAtKey, sessions, isRalph) {
|
|
1181
|
-
var runGk = parentGk + ":" + startedAtKey;
|
|
1182
|
-
var expanded = expandedMobileLoopRuns.has(runGk);
|
|
1183
|
-
var startedAt = Number(startedAtKey);
|
|
1184
|
-
var timeLabel = startedAt ? new Date(startedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "Unknown";
|
|
1185
|
-
|
|
1186
|
-
var hasActive = false;
|
|
1187
|
-
var anyProcessing = false;
|
|
1188
|
-
var latestSession = sessions[0];
|
|
1189
|
-
for (var i = 0; i < sessions.length; i++) {
|
|
1190
|
-
if (sessions[i].active) hasActive = true;
|
|
1191
|
-
if (sessions[i].isProcessing) anyProcessing = true;
|
|
1192
|
-
if ((sessions[i].lastActivity || 0) > (latestSession.lastActivity || 0)) {
|
|
1193
|
-
latestSession = sessions[i];
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
var wrapper = document.createElement("div");
|
|
1198
|
-
wrapper.className = "mobile-loop-run-wrapper";
|
|
1199
|
-
|
|
1200
|
-
var header = document.createElement("button");
|
|
1201
|
-
header.className = "mobile-loop-run" + (hasActive ? " active" : "") + (expanded ? " expanded" : "") + (isRalph ? "" : " scheduled");
|
|
1202
|
-
|
|
1203
|
-
var chevron = document.createElement("span");
|
|
1204
|
-
chevron.className = "mobile-loop-chevron";
|
|
1205
|
-
chevron.innerHTML = iconHtml("chevron-right");
|
|
1206
|
-
header.appendChild(chevron);
|
|
1207
|
-
|
|
1208
|
-
var label = document.createElement("span");
|
|
1209
|
-
label.className = "mobile-loop-run-time";
|
|
1210
|
-
var labelHtml = "";
|
|
1211
|
-
if (anyProcessing) {
|
|
1212
|
-
labelHtml += '<span class="mobile-session-processing"></span> ';
|
|
1213
|
-
}
|
|
1214
|
-
labelHtml += escapeHtml(timeLabel);
|
|
1215
|
-
label.innerHTML = labelHtml;
|
|
1216
|
-
header.appendChild(label);
|
|
1217
|
-
|
|
1218
|
-
var countBadge = document.createElement("span");
|
|
1219
|
-
countBadge.className = "mobile-loop-count" + (isRalph ? "" : " scheduled");
|
|
1220
|
-
countBadge.textContent = String(sessions.length);
|
|
1221
|
-
header.appendChild(countBadge);
|
|
1222
|
-
|
|
1223
|
-
header.addEventListener("click", (function (rk) {
|
|
1224
|
-
return function (e) {
|
|
1225
|
-
e.stopPropagation();
|
|
1226
|
-
if (expandedMobileLoopRuns.has(rk)) {
|
|
1227
|
-
expandedMobileLoopRuns.delete(rk);
|
|
1228
|
-
} else {
|
|
1229
|
-
expandedMobileLoopRuns.add(rk);
|
|
1230
|
-
}
|
|
1231
|
-
refreshMobileChatSheet();
|
|
1232
|
-
};
|
|
1233
|
-
})(runGk));
|
|
1234
|
-
|
|
1235
|
-
wrapper.appendChild(header);
|
|
1236
|
-
|
|
1237
|
-
if (expanded) {
|
|
1238
|
-
var childContainer = document.createElement("div");
|
|
1239
|
-
childContainer.className = "mobile-loop-children";
|
|
1240
|
-
for (var k = 0; k < sessions.length; k++) {
|
|
1241
|
-
childContainer.appendChild(createMobileLoopChild(sessions[k]));
|
|
1242
|
-
}
|
|
1243
|
-
wrapper.appendChild(childContainer);
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
return wrapper;
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
// Helper: create a mobile loop group element (collapsible group header)
|
|
1250
|
-
function createMobileLoopGroup(loopId, children, groupKey) {
|
|
1251
|
-
var gk = groupKey || loopId;
|
|
1252
|
-
|
|
1253
|
-
// Sub-group children by startedAt (each run)
|
|
1254
|
-
var runMap = {};
|
|
1255
|
-
for (var i = 0; i < children.length; i++) {
|
|
1256
|
-
var runKey = String(children[i].loop && children[i].loop.startedAt || 0);
|
|
1257
|
-
if (!runMap[runKey]) runMap[runKey] = [];
|
|
1258
|
-
runMap[runKey].push(children[i]);
|
|
1259
|
-
}
|
|
1260
|
-
var runKeys = Object.keys(runMap);
|
|
1261
|
-
|
|
1262
|
-
// Sort each run's children by iteration then role
|
|
1263
|
-
for (var ri = 0; ri < runKeys.length; ri++) {
|
|
1264
|
-
runMap[runKeys[ri]].sort(function (a, b) {
|
|
1265
|
-
var ai = (a.loop && a.loop.iteration) || 0;
|
|
1266
|
-
var bi = (b.loop && b.loop.iteration) || 0;
|
|
1267
|
-
if (ai !== bi) return ai - bi;
|
|
1268
|
-
var ar = (a.loop && a.loop.role === "judge") ? 1 : 0;
|
|
1269
|
-
var br = (b.loop && b.loop.role === "judge") ? 1 : 0;
|
|
1270
|
-
return ar - br;
|
|
1271
|
-
});
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
// Sort runs by startedAt descending (newest first)
|
|
1275
|
-
runKeys.sort(function (a, b) { return Number(b) - Number(a); });
|
|
1276
|
-
|
|
1277
|
-
var expanded = expandedMobileLoopGroups.has(gk);
|
|
1278
|
-
var hasActive = false;
|
|
1279
|
-
var anyProcessing = false;
|
|
1280
|
-
var latestSession = children[0];
|
|
1281
|
-
for (var ci = 0; ci < children.length; ci++) {
|
|
1282
|
-
if (children[ci].active) hasActive = true;
|
|
1283
|
-
if (children[ci].isProcessing) anyProcessing = true;
|
|
1284
|
-
if ((children[ci].lastActivity || 0) > (latestSession.lastActivity || 0)) {
|
|
1285
|
-
latestSession = children[ci];
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
var loopName = (children[0].loop && children[0].loop.name) || "Ralph Loop";
|
|
1290
|
-
var isRalph = children[0].loop && children[0].loop.source === "ralph";
|
|
1291
|
-
var isCrafting = false;
|
|
1292
|
-
for (var j = 0; j < children.length; j++) {
|
|
1293
|
-
if (children[j].loop && children[j].loop.role === "crafting") isCrafting = true;
|
|
1294
|
-
}
|
|
1295
|
-
var runCount = runKeys.length;
|
|
1296
|
-
|
|
1297
|
-
var wrapper = document.createElement("div");
|
|
1298
|
-
wrapper.className = "mobile-loop-wrapper";
|
|
1299
|
-
|
|
1300
|
-
// Group header row
|
|
1301
|
-
var header = document.createElement("button");
|
|
1302
|
-
header.className = "mobile-loop-group" + (hasActive ? " active" : "") + (expanded ? " expanded" : "") + (isRalph ? "" : " scheduled");
|
|
1303
|
-
|
|
1304
|
-
var chevron = document.createElement("span");
|
|
1305
|
-
chevron.className = "mobile-loop-chevron";
|
|
1306
|
-
chevron.innerHTML = iconHtml("chevron-right");
|
|
1307
|
-
header.appendChild(chevron);
|
|
1308
|
-
|
|
1309
|
-
var iconSpan = document.createElement("span");
|
|
1310
|
-
var groupIcon = isRalph ? "repeat" : "calendar-clock";
|
|
1311
|
-
iconSpan.className = "mobile-loop-icon" + (isRalph ? "" : " scheduled");
|
|
1312
|
-
iconSpan.innerHTML = iconHtml(groupIcon);
|
|
1313
|
-
header.appendChild(iconSpan);
|
|
1314
|
-
|
|
1315
|
-
if (anyProcessing) {
|
|
1316
|
-
var dot = document.createElement("span");
|
|
1317
|
-
dot.className = "mobile-session-processing";
|
|
1318
|
-
header.appendChild(dot);
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
var nameSpan = document.createElement("span");
|
|
1322
|
-
nameSpan.className = "mobile-loop-name";
|
|
1323
|
-
nameSpan.textContent = loopName;
|
|
1324
|
-
header.appendChild(nameSpan);
|
|
1325
|
-
|
|
1326
|
-
if (isCrafting && children.length === 1) {
|
|
1327
|
-
var craftBadge = document.createElement("span");
|
|
1328
|
-
craftBadge.className = "mobile-loop-badge crafting";
|
|
1329
|
-
craftBadge.textContent = "Crafting";
|
|
1330
|
-
header.appendChild(craftBadge);
|
|
1331
|
-
} else {
|
|
1332
|
-
var countBadge = document.createElement("span");
|
|
1333
|
-
countBadge.className = "mobile-loop-count" + (isRalph ? "" : " scheduled");
|
|
1334
|
-
var countLabel = runCount === 1 ? String(children.length) : runCount + (runCount === 1 ? " run" : " runs");
|
|
1335
|
-
countBadge.textContent = countLabel;
|
|
1336
|
-
header.appendChild(countBadge);
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
// Chevron toggles expansion
|
|
1340
|
-
header.addEventListener("click", (function (lid) {
|
|
1341
|
-
return function (e) {
|
|
1342
|
-
e.stopPropagation();
|
|
1343
|
-
if (expandedMobileLoopGroups.has(lid)) {
|
|
1344
|
-
expandedMobileLoopGroups.delete(lid);
|
|
1345
|
-
} else {
|
|
1346
|
-
expandedMobileLoopGroups.add(lid);
|
|
1347
|
-
}
|
|
1348
|
-
refreshMobileChatSheet();
|
|
1349
|
-
};
|
|
1350
|
-
})(gk));
|
|
1351
|
-
|
|
1352
|
-
wrapper.appendChild(header);
|
|
1353
|
-
|
|
1354
|
-
// Expanded: show runs
|
|
1355
|
-
if (expanded) {
|
|
1356
|
-
var childContainer = document.createElement("div");
|
|
1357
|
-
childContainer.className = "mobile-loop-children";
|
|
1358
|
-
|
|
1359
|
-
if (runCount === 1) {
|
|
1360
|
-
var singleRun = runMap[runKeys[0]];
|
|
1361
|
-
for (var sk = 0; sk < singleRun.length; sk++) {
|
|
1362
|
-
childContainer.appendChild(createMobileLoopChild(singleRun[sk]));
|
|
1363
|
-
}
|
|
1364
|
-
} else {
|
|
1365
|
-
for (var rk = 0; rk < runKeys.length; rk++) {
|
|
1366
|
-
childContainer.appendChild(createMobileLoopRun(gk, runKeys[rk], runMap[runKeys[rk]], isRalph));
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
wrapper.appendChild(childContainer);
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
return wrapper;
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
function renderMateMobileActions(container) {
|
|
1377
|
-
var newSessionBtn = document.createElement("button");
|
|
1378
|
-
newSessionBtn.className = "mobile-session-new";
|
|
1379
|
-
newSessionBtn.innerHTML = '<i data-lucide="plus" style="width:16px;height:16px"></i> New session';
|
|
1380
|
-
newSessionBtn.addEventListener("click", function () {
|
|
1381
|
-
if (ctx.ws && ctx.connected) {
|
|
1382
|
-
ctx.ws.send(JSON.stringify({ type: "new_session" }));
|
|
1383
|
-
}
|
|
1384
|
-
closeMobileSheet();
|
|
1385
|
-
});
|
|
1386
|
-
container.appendChild(newSessionBtn);
|
|
1387
|
-
|
|
1388
|
-
var debateBtn = document.createElement("button");
|
|
1389
|
-
debateBtn.className = "mobile-session-new";
|
|
1390
|
-
debateBtn.innerHTML = '<i data-lucide="mic" style="width:16px;height:16px"></i> New debate';
|
|
1391
|
-
debateBtn.addEventListener("click", function () {
|
|
1392
|
-
closeMobileSheet();
|
|
1393
|
-
var targetBtn = document.getElementById("mate-debate-btn");
|
|
1394
|
-
if (targetBtn) setTimeout(function () { targetBtn.click(); }, 250);
|
|
1395
|
-
});
|
|
1396
|
-
container.appendChild(debateBtn);
|
|
1397
|
-
|
|
1398
|
-
// Render mate session list
|
|
1399
|
-
var mateSessions = getMateSessions();
|
|
1400
|
-
if (mateSessions.length > 0) {
|
|
1401
|
-
var sorted = mateSessions.slice().sort(function (a, b) {
|
|
1402
|
-
return (b.lastActivity || 0) - (a.lastActivity || 0);
|
|
1403
|
-
});
|
|
1404
|
-
|
|
1405
|
-
var currentGroup = "";
|
|
1406
|
-
for (var i = 0; i < sorted.length; i++) {
|
|
1407
|
-
var s = sorted[i];
|
|
1408
|
-
var group = getDateGroup(s.lastActivity || 0);
|
|
1409
|
-
if (group !== currentGroup) {
|
|
1410
|
-
currentGroup = group;
|
|
1411
|
-
var header = document.createElement("div");
|
|
1412
|
-
header.className = "mobile-sheet-group";
|
|
1413
|
-
header.textContent = group;
|
|
1414
|
-
container.appendChild(header);
|
|
1415
|
-
}
|
|
1416
|
-
var mateItem = createMobileSessionItem(s);
|
|
1417
|
-
container.appendChild(mateItem);
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
refreshIcons();
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
// Helper: render sorted sessions into a container with date groups (with loop session grouping)
|
|
1425
|
-
function renderMobileSessionsInto(container) {
|
|
1426
|
-
var newBtn = document.createElement("button");
|
|
1427
|
-
newBtn.className = "mobile-session-new";
|
|
1428
|
-
newBtn.innerHTML = '<i data-lucide="plus" style="width:16px;height:16px"></i> New session';
|
|
1429
|
-
newBtn.addEventListener("click", function () {
|
|
1430
|
-
if (ctx.ws && ctx.connected) {
|
|
1431
|
-
ctx.ws.send(JSON.stringify({ type: "new_session" }));
|
|
1432
|
-
}
|
|
1433
|
-
closeMobileSheet();
|
|
1434
|
-
});
|
|
1435
|
-
container.appendChild(newBtn);
|
|
1436
|
-
|
|
1437
|
-
// Partition: loop sessions vs normal sessions (same logic as desktop renderSessionList)
|
|
1438
|
-
var loopGroups = {};
|
|
1439
|
-
var normalSessions = [];
|
|
1440
|
-
for (var i = 0; i < cachedSessions.length; i++) {
|
|
1441
|
-
var s = cachedSessions[i];
|
|
1442
|
-
if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph" && s.loop.source !== "debate") {
|
|
1443
|
-
continue;
|
|
1444
|
-
} else if (s.loop && s.loop.loopId) {
|
|
1445
|
-
var startedAt = s.loop.startedAt || 0;
|
|
1446
|
-
var dateStr = startedAt ? new Date(startedAt).toISOString().slice(0, 10) : "unknown";
|
|
1447
|
-
var groupKey = s.loop.loopId + ":" + dateStr;
|
|
1448
|
-
if (!loopGroups[groupKey]) loopGroups[groupKey] = [];
|
|
1449
|
-
loopGroups[groupKey].push(s);
|
|
1450
|
-
} else {
|
|
1451
|
-
normalSessions.push(s);
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
// Build virtual items
|
|
1456
|
-
var items = [];
|
|
1457
|
-
for (var j = 0; j < normalSessions.length; j++) {
|
|
1458
|
-
items.push({ type: "session", data: normalSessions[j], lastActivity: normalSessions[j].lastActivity || 0 });
|
|
1459
|
-
}
|
|
1460
|
-
var groupKeys = Object.keys(loopGroups);
|
|
1461
|
-
for (var k = 0; k < groupKeys.length; k++) {
|
|
1462
|
-
var gk = groupKeys[k];
|
|
1463
|
-
var children = loopGroups[gk];
|
|
1464
|
-
var realLoopId = children[0].loop.loopId;
|
|
1465
|
-
var maxActivity = 0;
|
|
1466
|
-
for (var m = 0; m < children.length; m++) {
|
|
1467
|
-
var act = children[m].lastActivity || 0;
|
|
1468
|
-
if (act > maxActivity) maxActivity = act;
|
|
1469
|
-
}
|
|
1470
|
-
items.push({ type: "loop", loopId: realLoopId, groupKey: gk, children: children, lastActivity: maxActivity });
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
// Sort by lastActivity descending
|
|
1474
|
-
items.sort(function (a, b) {
|
|
1475
|
-
return (b.lastActivity || 0) - (a.lastActivity || 0);
|
|
1476
|
-
});
|
|
1477
|
-
|
|
1478
|
-
var currentGroup = "";
|
|
1479
|
-
for (var n = 0; n < items.length; n++) {
|
|
1480
|
-
var item = items[n];
|
|
1481
|
-
var group = getDateGroup(item.lastActivity || 0);
|
|
1482
|
-
if (group !== currentGroup) {
|
|
1483
|
-
currentGroup = group;
|
|
1484
|
-
var header = document.createElement("div");
|
|
1485
|
-
header.className = "mobile-sheet-group";
|
|
1486
|
-
header.textContent = group;
|
|
1487
|
-
container.appendChild(header);
|
|
1488
|
-
}
|
|
1489
|
-
if (item.type === "loop") {
|
|
1490
|
-
container.appendChild(createMobileLoopGroup(item.loopId, item.children, item.groupKey));
|
|
1491
|
-
} else {
|
|
1492
|
-
container.appendChild(createMobileSessionItem(item.data));
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
// Refresh mobile chat sheet when session data updates (called from renderSessionList)
|
|
1498
|
-
export function refreshMobileChatSheet() {
|
|
1499
|
-
if (!mobileChatSheetOpen) return;
|
|
1500
|
-
var sheet = document.getElementById("mobile-sheet");
|
|
1501
|
-
if (!sheet || sheet.classList.contains("hidden")) {
|
|
1502
|
-
mobileChatSheetOpen = false;
|
|
1503
|
-
return;
|
|
1504
|
-
}
|
|
1505
|
-
var sessionListEl = sheet.querySelector(".mobile-chat-session-list");
|
|
1506
|
-
if (!sessionListEl) return;
|
|
1507
|
-
|
|
1508
|
-
// Update chips: active state and processing dots
|
|
1509
|
-
var chips = sheet.querySelectorAll(".mobile-chat-chip");
|
|
1510
|
-
for (var i = 0; i < chips.length; i++) {
|
|
1511
|
-
var chip = chips[i];
|
|
1512
|
-
chip.classList.remove("active");
|
|
1513
|
-
|
|
1514
|
-
// Update active state
|
|
1515
|
-
var isDmActive = !!currentDmUserId;
|
|
1516
|
-
if (chip.dataset.type === "project" && chip.dataset.slug === cachedCurrentSlug && !isDmActive) {
|
|
1517
|
-
chip.classList.add("active");
|
|
1518
|
-
} else if (chip.dataset.type === "mate" && chip.dataset.mateId === currentDmUserId) {
|
|
1519
|
-
chip.classList.add("active");
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
// Update processing dot: same class as icon strip
|
|
1523
|
-
var statusDot = chip.querySelector(".icon-strip-status");
|
|
1524
|
-
if (statusDot) {
|
|
1525
|
-
var isProcessing = false;
|
|
1526
|
-
var allProjects = (ctx && ctx.projectList) || [];
|
|
1527
|
-
var lookupSlug = chip.dataset.type === "mate" ? ("mate-" + chip.dataset.mateId) : chip.dataset.slug;
|
|
1528
|
-
for (var pi = 0; pi < allProjects.length; pi++) {
|
|
1529
|
-
if (allProjects[pi].slug === lookupSlug && allProjects[pi].isProcessing) {
|
|
1530
|
-
isProcessing = true;
|
|
1531
|
-
break;
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
statusDot.classList.toggle("processing", isProcessing);
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
// Re-render sessions for current context
|
|
1539
|
-
sessionListEl.innerHTML = "";
|
|
1540
|
-
if (currentDmUserId) {
|
|
1541
|
-
renderMateMobileActions(sessionListEl);
|
|
1542
|
-
} else {
|
|
1543
|
-
renderMobileSessionsInto(sessionListEl);
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
refreshIcons();
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
function renderSheetMateProfile(listEl) {
|
|
1550
|
-
if (!mobileSheetMateData) return;
|
|
1551
|
-
var data = mobileSheetMateData;
|
|
1552
|
-
|
|
1553
|
-
// Profile header
|
|
1554
|
-
var header = document.createElement("div");
|
|
1555
|
-
header.className = "mate-profile-header";
|
|
1556
|
-
|
|
1557
|
-
var avatar = document.createElement("img");
|
|
1558
|
-
avatar.className = "mate-profile-avatar";
|
|
1559
|
-
avatar.src = data.avatarUrl || "";
|
|
1560
|
-
avatar.alt = data.displayName || "";
|
|
1561
|
-
header.appendChild(avatar);
|
|
1562
|
-
|
|
1563
|
-
var info = document.createElement("div");
|
|
1564
|
-
info.className = "mate-profile-info";
|
|
1565
|
-
var nameEl = document.createElement("div");
|
|
1566
|
-
nameEl.className = "mate-profile-name";
|
|
1567
|
-
nameEl.textContent = data.displayName || "";
|
|
1568
|
-
info.appendChild(nameEl);
|
|
1569
|
-
if (data.description) {
|
|
1570
|
-
var descEl = document.createElement("div");
|
|
1571
|
-
descEl.className = "mate-profile-desc";
|
|
1572
|
-
descEl.textContent = data.description;
|
|
1573
|
-
info.appendChild(descEl);
|
|
1574
|
-
}
|
|
1575
|
-
header.appendChild(info);
|
|
1576
|
-
listEl.appendChild(header);
|
|
1577
|
-
|
|
1578
|
-
// Action buttons
|
|
1579
|
-
var actions = [
|
|
1580
|
-
{ icon: "book-open", label: "Knowledge", btnId: "mate-knowledge-btn", countId: "mate-knowledge-count" },
|
|
1581
|
-
{ icon: "sticky-note", label: "Sticky Notes", btnId: "sticky-notes-toggle-btn", countId: "sticky-notes-sidebar-count" },
|
|
1582
|
-
{ icon: "puzzle", label: "Skills", btnId: "mate-skills-btn" },
|
|
1583
|
-
{ icon: "calendar", label: "Scheduled Tasks", btnId: "mate-scheduler-btn" }
|
|
1584
|
-
];
|
|
1585
|
-
|
|
1586
|
-
for (var i = 0; i < actions.length; i++) {
|
|
1587
|
-
(function (action) {
|
|
1588
|
-
var btn = document.createElement("button");
|
|
1589
|
-
btn.className = "mate-profile-action";
|
|
1590
|
-
var countHtml = "";
|
|
1591
|
-
if (action.countId) {
|
|
1592
|
-
var countEl = document.getElementById(action.countId);
|
|
1593
|
-
if (countEl && !countEl.classList.contains("hidden") && countEl.textContent) {
|
|
1594
|
-
countHtml = '<span class="mate-profile-action-count">' + escapeHtml(countEl.textContent) + '</span>';
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
btn.innerHTML = '<i data-lucide="' + action.icon + '"></i><span>' + action.label + '</span>' + countHtml;
|
|
1598
|
-
btn.addEventListener("click", function () {
|
|
1599
|
-
closeMobileSheet();
|
|
1600
|
-
var targetBtn = document.getElementById(action.btnId);
|
|
1601
|
-
if (targetBtn) {
|
|
1602
|
-
setTimeout(function () { targetBtn.click(); }, 250);
|
|
1603
|
-
}
|
|
1604
|
-
});
|
|
1605
|
-
listEl.appendChild(btn);
|
|
1606
|
-
})(actions[i]);
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
function renderSheetSearch(listEl) {
|
|
1611
|
-
// Search input at top
|
|
1612
|
-
var wrap = document.createElement("div");
|
|
1613
|
-
wrap.className = "mobile-search-input-wrap";
|
|
1614
|
-
var input = document.createElement("input");
|
|
1615
|
-
input.className = "mobile-search-input";
|
|
1616
|
-
input.type = "text";
|
|
1617
|
-
input.placeholder = "Search sessions, messages...";
|
|
1618
|
-
input.autocomplete = "off";
|
|
1619
|
-
input.spellcheck = false;
|
|
1620
|
-
wrap.appendChild(input);
|
|
1621
|
-
listEl.appendChild(wrap);
|
|
1622
|
-
|
|
1623
|
-
// Results container
|
|
1624
|
-
var resultsEl = document.createElement("div");
|
|
1625
|
-
resultsEl.style.padding = "0 8px";
|
|
1626
|
-
listEl.appendChild(resultsEl);
|
|
1627
|
-
|
|
1628
|
-
// Auto-focus
|
|
1629
|
-
setTimeout(function () { input.focus(); }, 300);
|
|
1630
|
-
|
|
1631
|
-
// Show all sessions initially
|
|
1632
|
-
renderSearchResults(resultsEl, "");
|
|
1633
|
-
|
|
1634
|
-
input.addEventListener("input", function () {
|
|
1635
|
-
var q = input.value.trim().toLowerCase();
|
|
1636
|
-
renderSearchResults(resultsEl, q);
|
|
1637
|
-
});
|
|
1638
|
-
input.addEventListener("keydown", function (e) { e.stopPropagation(); });
|
|
1639
|
-
input.addEventListener("keyup", function (e) { e.stopPropagation(); });
|
|
1640
|
-
input.addEventListener("keypress", function (e) { e.stopPropagation(); });
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
function renderSearchResults(container, query) {
|
|
1644
|
-
container.innerHTML = "";
|
|
1645
|
-
var sorted = cachedSessions.slice().sort(function (a, b) {
|
|
1646
|
-
return (b.lastActivity || 0) - (a.lastActivity || 0);
|
|
1647
|
-
});
|
|
1648
|
-
|
|
1649
|
-
var found = 0;
|
|
1650
|
-
for (var i = 0; i < sorted.length; i++) {
|
|
1651
|
-
var s = sorted[i];
|
|
1652
|
-
var title = s.title || "New Session";
|
|
1653
|
-
if (query && title.toLowerCase().indexOf(query) === -1) continue;
|
|
1654
|
-
found++;
|
|
1655
|
-
|
|
1656
|
-
var el = document.createElement("button");
|
|
1657
|
-
el.className = "mobile-session-item";
|
|
1658
|
-
if (s.active) el.classList.add("active");
|
|
1659
|
-
|
|
1660
|
-
var titleSpan = document.createElement("span");
|
|
1661
|
-
titleSpan.className = "mobile-session-title";
|
|
1662
|
-
titleSpan.textContent = title;
|
|
1663
|
-
el.appendChild(titleSpan);
|
|
1664
|
-
|
|
1665
|
-
if (s.isProcessing) {
|
|
1666
|
-
var dot = document.createElement("span");
|
|
1667
|
-
dot.className = "mobile-session-processing";
|
|
1668
|
-
el.appendChild(dot);
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
(function (id) {
|
|
1672
|
-
el.addEventListener("click", function () {
|
|
1673
|
-
if (ctx.ws && ctx.connected) {
|
|
1674
|
-
ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
|
|
1675
|
-
}
|
|
1676
|
-
dismissOverlayPanels();
|
|
1677
|
-
closeMobileSheet();
|
|
1678
|
-
});
|
|
1679
|
-
})(s.id);
|
|
1680
|
-
|
|
1681
|
-
container.appendChild(el);
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
if (found === 0 && query) {
|
|
1685
|
-
var empty = document.createElement("div");
|
|
1686
|
-
empty.className = "mobile-alert-empty";
|
|
1687
|
-
empty.textContent = 'No results for "' + query + '"';
|
|
1688
|
-
container.appendChild(empty);
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
function renderSheetTools(listEl) {
|
|
1693
|
-
var isMateDm = document.body.classList.contains("mate-dm-active");
|
|
1694
|
-
|
|
1695
|
-
var items = isMateDm ? [
|
|
1696
|
-
{ icon: "brain", label: "Memory", action: "mate-memory" },
|
|
1697
|
-
{ icon: "book-open", label: "Knowledge", action: "mate-knowledge" },
|
|
1698
|
-
{ icon: "sticky-note", label: "Sticky Notes", action: "mate-sticky" },
|
|
1699
|
-
{ icon: "puzzle", label: "Skills", action: "mate-skills" },
|
|
1700
|
-
{ icon: "calendar-clock", label: "Scheduled Tasks", action: "mate-scheduler" }
|
|
1701
|
-
] : [
|
|
1702
|
-
{ icon: "folder-tree", label: "Files", action: "files" },
|
|
1703
|
-
{ icon: "square-terminal", label: "Terminal", action: "terminal" },
|
|
1704
|
-
{ icon: "calendar-clock", label: "Scheduled Tasks", action: "scheduler" }
|
|
1705
|
-
];
|
|
1706
|
-
|
|
1707
|
-
for (var i = 0; i < items.length; i++) {
|
|
1708
|
-
(function (item) {
|
|
1709
|
-
var btn = document.createElement("button");
|
|
1710
|
-
btn.className = "mobile-more-item";
|
|
1711
|
-
btn.innerHTML = '<i data-lucide="' + item.icon + '"></i><span class="mobile-more-item-label">' + item.label + '</span>';
|
|
1712
|
-
btn.addEventListener("click", function () {
|
|
1713
|
-
closeMobileSheet();
|
|
1714
|
-
var targetId = null;
|
|
1715
|
-
if (item.action === "files") {
|
|
1716
|
-
setTimeout(function () { openMobileSheet("files"); }, 250);
|
|
1717
|
-
} else if (item.action === "terminal") {
|
|
1718
|
-
if (ctx.openTerminal) ctx.openTerminal();
|
|
1719
|
-
} else if (item.action === "scheduler") {
|
|
1720
|
-
targetId = "scheduler-btn";
|
|
1721
|
-
} else if (item.action === "mate-knowledge") {
|
|
1722
|
-
setTimeout(function () { openMobileSheet("mate-knowledge"); }, 250);
|
|
1723
|
-
return;
|
|
1724
|
-
} else if (item.action === "mate-sticky") {
|
|
1725
|
-
targetId = "mate-sticky-notes-btn";
|
|
1726
|
-
} else if (item.action === "mate-skills") {
|
|
1727
|
-
targetId = "mate-skills-btn";
|
|
1728
|
-
} else if (item.action === "mate-memory") {
|
|
1729
|
-
targetId = "mate-memory-btn";
|
|
1730
|
-
} else if (item.action === "mate-scheduler") {
|
|
1731
|
-
targetId = "mate-scheduler-btn";
|
|
1732
|
-
} else if (item.action === "mate-debate") {
|
|
1733
|
-
targetId = "mate-debate-btn";
|
|
1734
|
-
}
|
|
1735
|
-
if (targetId) {
|
|
1736
|
-
var targetBtn = document.getElementById(targetId);
|
|
1737
|
-
if (targetBtn) setTimeout(function () { targetBtn.click(); }, 250);
|
|
1738
|
-
}
|
|
1739
|
-
});
|
|
1740
|
-
listEl.appendChild(btn);
|
|
1741
|
-
})(items[i]);
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
function renderSheetSettings(listEl) {
|
|
1746
|
-
var items = [
|
|
1747
|
-
{ icon: "folder-cog", label: "Project Settings", action: "project-settings" },
|
|
1748
|
-
{ icon: "settings", label: "Server Settings", action: "server-settings" }
|
|
1749
|
-
];
|
|
1750
|
-
|
|
1751
|
-
for (var i = 0; i < items.length; i++) {
|
|
1752
|
-
(function (item) {
|
|
1753
|
-
var btn = document.createElement("button");
|
|
1754
|
-
btn.className = "mobile-more-item";
|
|
1755
|
-
btn.innerHTML = '<i data-lucide="' + item.icon + '"></i><span class="mobile-more-item-label">' + item.label + '</span>';
|
|
1756
|
-
btn.addEventListener("click", function () {
|
|
1757
|
-
closeMobileSheet();
|
|
1758
|
-
if (item.action === "project-settings") {
|
|
1759
|
-
setTimeout(function () {
|
|
1760
|
-
// Find current project data
|
|
1761
|
-
var proj = null;
|
|
1762
|
-
for (var pi = 0; pi < cachedAllProjects.length; pi++) {
|
|
1763
|
-
if (cachedAllProjects[pi].slug === cachedCurrentSlug) {
|
|
1764
|
-
proj = cachedAllProjects[pi];
|
|
1765
|
-
break;
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
// For mate projects, use mate display name instead of slug
|
|
1769
|
-
if (proj && proj.isMate && cachedMates.length > 0) {
|
|
1770
|
-
var mateId = cachedCurrentSlug.replace("mate-", "");
|
|
1771
|
-
for (var mi = 0; mi < cachedMates.length; mi++) {
|
|
1772
|
-
var mp = cachedMates[mi].profile || {};
|
|
1773
|
-
if (cachedMates[mi].id === mateId) {
|
|
1774
|
-
proj = Object.assign({}, proj, { name: mp.displayName || cachedMates[mi].name || proj.name });
|
|
1775
|
-
break;
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
openProjectSettings(cachedCurrentSlug, proj);
|
|
1780
|
-
}, 250);
|
|
1781
|
-
} else if (item.action === "server-settings") {
|
|
1782
|
-
var settingsBtn = document.getElementById("server-settings-btn");
|
|
1783
|
-
if (settingsBtn) setTimeout(function () { settingsBtn.click(); }, 250);
|
|
1784
|
-
}
|
|
1785
|
-
});
|
|
1786
|
-
listEl.appendChild(btn);
|
|
1787
|
-
})(items[i]);
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
// Dark/Light switch button
|
|
1791
|
-
var isDark = getCurrentTheme().variant === "dark";
|
|
1792
|
-
var themeBtn = document.createElement("button");
|
|
1793
|
-
themeBtn.className = "mobile-more-item";
|
|
1794
|
-
themeBtn.innerHTML = '<i data-lucide="' + (isDark ? "sun" : "moon") + '"></i><span class="mobile-more-item-label">Switch to ' + (isDark ? "Light" : "Dark") + '</span>';
|
|
1795
|
-
|
|
1796
|
-
themeBtn.addEventListener("click", function () {
|
|
1797
|
-
var themeToggle = document.getElementById("theme-toggle-check");
|
|
1798
|
-
if (themeToggle) themeToggle.click();
|
|
1799
|
-
// Update button text after a tick (theme applies async)
|
|
1800
|
-
setTimeout(function () {
|
|
1801
|
-
var nowDark = getCurrentTheme().variant === "dark";
|
|
1802
|
-
themeBtn.innerHTML = '<i data-lucide="' + (nowDark ? "sun" : "moon") + '"></i><span class="mobile-more-item-label">Switch to ' + (nowDark ? "Light" : "Dark") + '</span>';
|
|
1803
|
-
refreshIcons();
|
|
1804
|
-
}, 50);
|
|
1805
|
-
});
|
|
1806
|
-
|
|
1807
|
-
listEl.appendChild(themeBtn);
|
|
1808
|
-
|
|
1809
|
-
// Chat Layout switch button
|
|
1810
|
-
var currentLayout = getChatLayout();
|
|
1811
|
-
var isBubble = currentLayout === "bubble";
|
|
1812
|
-
var layoutBtn = document.createElement("button");
|
|
1813
|
-
layoutBtn.className = "mobile-more-item";
|
|
1814
|
-
layoutBtn.innerHTML = '<i data-lucide="' + (isBubble ? "monitor" : "message-circle") + '"></i>'
|
|
1815
|
-
+ '<span class="mobile-more-item-label">Switch to ' + (isBubble ? "Channel" : "Bubble") + '</span>';
|
|
1816
|
-
|
|
1817
|
-
layoutBtn.addEventListener("click", function () {
|
|
1818
|
-
var next = getChatLayout() === "bubble" ? "channel" : "bubble";
|
|
1819
|
-
setChatLayout(next);
|
|
1820
|
-
fetch('/api/user/chat-layout', {
|
|
1821
|
-
method: 'PUT',
|
|
1822
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1823
|
-
body: JSON.stringify({ layout: next })
|
|
1824
|
-
});
|
|
1825
|
-
closeMobileSheet();
|
|
1826
|
-
});
|
|
1827
|
-
|
|
1828
|
-
listEl.appendChild(layoutBtn);
|
|
1829
|
-
|
|
1830
|
-
// "Open as app" — only show if not already in PWA standalone mode
|
|
1831
|
-
if (!document.documentElement.classList.contains("pwa-standalone")) {
|
|
1832
|
-
var pwaBtn = document.createElement("button");
|
|
1833
|
-
pwaBtn.className = "mobile-more-item";
|
|
1834
|
-
pwaBtn.innerHTML = '<i data-lucide="smartphone"></i><span class="mobile-more-item-label">Open as app</span>';
|
|
1835
|
-
pwaBtn.addEventListener("click", function () {
|
|
1836
|
-
closeMobileSheet();
|
|
1837
|
-
// Trigger the existing PWA install modal
|
|
1838
|
-
var installPill = document.getElementById("pwa-install-pill");
|
|
1839
|
-
if (installPill) {
|
|
1840
|
-
setTimeout(function () { installPill.click(); }, 250);
|
|
1841
|
-
}
|
|
1842
|
-
});
|
|
1843
|
-
listEl.appendChild(pwaBtn);
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
export function initSidebar(_ctx) {
|
|
1848
|
-
ctx = _ctx;
|
|
1849
|
-
|
|
1850
|
-
document.addEventListener("click", function () { closeSessionCtxMenu(); });
|
|
1851
|
-
|
|
1852
|
-
ctx.hamburgerBtn.addEventListener("click", function () {
|
|
1853
|
-
ctx.sidebar.classList.contains("open") ? closeSidebar() : openSidebar();
|
|
1854
|
-
});
|
|
1855
|
-
|
|
1856
|
-
ctx.sidebarOverlay.addEventListener("click", closeSidebar);
|
|
1857
|
-
|
|
1858
|
-
// --- Desktop sidebar collapse/expand ---
|
|
1859
|
-
function toggleSidebarCollapse() {
|
|
1860
|
-
var layout = ctx.$("layout");
|
|
1861
|
-
var collapsed = layout.classList.toggle("sidebar-collapsed");
|
|
1862
|
-
try { localStorage.setItem("sidebar-collapsed", collapsed ? "1" : ""); } catch (e) {}
|
|
1863
|
-
setTimeout(function () { syncUserIslandWidth(); syncResizeHandle(); }, 210);
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
|
-
if (ctx.sidebarToggleBtn) ctx.sidebarToggleBtn.addEventListener("click", toggleSidebarCollapse);
|
|
1867
|
-
if (ctx.sidebarExpandBtn) ctx.sidebarExpandBtn.addEventListener("click", toggleSidebarCollapse);
|
|
1868
|
-
var mateSidebarToggle = document.getElementById("mate-sidebar-toggle-btn");
|
|
1869
|
-
if (mateSidebarToggle) mateSidebarToggle.addEventListener("click", toggleSidebarCollapse);
|
|
1870
|
-
|
|
1871
|
-
// Restore collapsed state from localStorage
|
|
1872
|
-
try {
|
|
1873
|
-
if (localStorage.getItem("sidebar-collapsed") === "1") {
|
|
1874
|
-
ctx.$("layout").classList.add("sidebar-collapsed");
|
|
1875
|
-
}
|
|
1876
|
-
} catch (e) {}
|
|
1877
|
-
|
|
1878
|
-
ctx.newSessionBtn.addEventListener("click", function () {
|
|
1879
|
-
if (ctx.ws && ctx.connected) {
|
|
1880
|
-
ctx.ws.send(JSON.stringify({ type: "new_session" }));
|
|
1881
|
-
closeSidebar();
|
|
1882
|
-
}
|
|
1883
|
-
});
|
|
1884
|
-
|
|
1885
|
-
// --- New Ralph Loop button ---
|
|
1886
|
-
var newRalphBtn = ctx.$("new-ralph-btn");
|
|
1887
|
-
if (newRalphBtn) {
|
|
1888
|
-
newRalphBtn.addEventListener("click", function () {
|
|
1889
|
-
if (ctx.openRalphWizard) ctx.openRalphWizard();
|
|
1890
|
-
});
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
// --- Session search ---
|
|
1894
|
-
var searchBtn = ctx.$("search-session-btn");
|
|
1895
|
-
var searchBox = ctx.$("session-search");
|
|
1896
|
-
var searchInput = ctx.$("session-search-input");
|
|
1897
|
-
var searchClear = ctx.$("session-search-clear");
|
|
1898
|
-
|
|
1899
|
-
function openSearch() {
|
|
1900
|
-
searchBox.classList.remove("hidden");
|
|
1901
|
-
searchBtn.classList.add("active");
|
|
1902
|
-
searchInput.value = "";
|
|
1903
|
-
searchQuery = "";
|
|
1904
|
-
setTimeout(function () { searchInput.focus(); }, 50);
|
|
1905
|
-
}
|
|
1906
|
-
|
|
1907
|
-
function closeSearch() {
|
|
1908
|
-
searchBox.classList.add("hidden");
|
|
1909
|
-
searchBtn.classList.remove("active");
|
|
1910
|
-
searchInput.value = "";
|
|
1911
|
-
searchQuery = "";
|
|
1912
|
-
searchMatchIds = null;
|
|
1913
|
-
if (searchDebounce) { clearTimeout(searchDebounce); searchDebounce = null; }
|
|
1914
|
-
renderSessionList(null);
|
|
1915
|
-
}
|
|
1916
|
-
|
|
1917
|
-
searchBtn.addEventListener("click", function () {
|
|
1918
|
-
if (searchBox.classList.contains("hidden")) {
|
|
1919
|
-
openSearch();
|
|
1920
|
-
} else {
|
|
1921
|
-
closeSearch();
|
|
1922
|
-
}
|
|
1923
|
-
});
|
|
1924
|
-
|
|
1925
|
-
if (searchClear) {
|
|
1926
|
-
searchClear.addEventListener("click", function () {
|
|
1927
|
-
closeSearch();
|
|
1928
|
-
});
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
searchInput.addEventListener("input", function () {
|
|
1932
|
-
searchQuery = searchInput.value.trim();
|
|
1933
|
-
if (searchDebounce) clearTimeout(searchDebounce);
|
|
1934
|
-
if (!searchQuery) {
|
|
1935
|
-
searchMatchIds = null;
|
|
1936
|
-
renderSessionList(null);
|
|
1937
|
-
return;
|
|
1938
|
-
}
|
|
1939
|
-
searchDebounce = setTimeout(function () {
|
|
1940
|
-
if (ctx.ws && ctx.connected) {
|
|
1941
|
-
ctx.ws.send(JSON.stringify({ type: "search_sessions", query: searchQuery }));
|
|
1942
|
-
}
|
|
1943
|
-
}, 200);
|
|
1944
|
-
});
|
|
1945
|
-
|
|
1946
|
-
searchInput.addEventListener("keydown", function (e) {
|
|
1947
|
-
if (e.key === "Escape") {
|
|
1948
|
-
e.preventDefault();
|
|
1949
|
-
closeSearch();
|
|
1950
|
-
}
|
|
1951
|
-
});
|
|
1952
|
-
|
|
1953
|
-
// --- Resume session picker ---
|
|
1954
|
-
var resumeModal = ctx.$("resume-modal");
|
|
1955
|
-
var resumeCancel = ctx.$("resume-cancel");
|
|
1956
|
-
var pickerLoading = ctx.$("resume-picker-loading");
|
|
1957
|
-
var pickerEmpty = ctx.$("resume-picker-empty");
|
|
1958
|
-
var pickerList = ctx.$("resume-picker-list");
|
|
1959
|
-
|
|
1960
|
-
function openResumeModal() {
|
|
1961
|
-
resumeModal.classList.remove("hidden");
|
|
1962
|
-
pickerLoading.classList.remove("hidden");
|
|
1963
|
-
pickerEmpty.classList.add("hidden");
|
|
1964
|
-
pickerList.classList.add("hidden");
|
|
1965
|
-
pickerList.innerHTML = "";
|
|
1966
|
-
if (ctx.ws && ctx.connected) {
|
|
1967
|
-
ctx.ws.send(JSON.stringify({ type: "list_cli_sessions" }));
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
|
-
|
|
1971
|
-
function closeResumeModal() {
|
|
1972
|
-
resumeModal.classList.add("hidden");
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
ctx.resumeSessionBtn.addEventListener("click", openResumeModal);
|
|
1976
|
-
resumeCancel.addEventListener("click", closeResumeModal);
|
|
1977
|
-
resumeModal.querySelector(".confirm-backdrop").addEventListener("click", closeResumeModal);
|
|
1978
|
-
|
|
1979
|
-
// --- Panel switch (sessions / files / projects) ---
|
|
1980
|
-
var fileBrowserBtn = ctx.$("file-browser-btn");
|
|
1981
|
-
var projectsPanel = ctx.$("sidebar-panel-projects");
|
|
1982
|
-
var sessionsPanel = ctx.$("sidebar-panel-sessions");
|
|
1983
|
-
var filesPanel = ctx.$("sidebar-panel-files");
|
|
1984
|
-
var sessionsHeaderContent = ctx.$("sessions-header-content");
|
|
1985
|
-
var filesHeaderContent = ctx.$("files-header-content");
|
|
1986
|
-
var filePanelClose = ctx.$("file-panel-close");
|
|
1987
|
-
|
|
1988
|
-
function hideAllPanels() {
|
|
1989
|
-
if (projectsPanel) projectsPanel.classList.add("hidden");
|
|
1990
|
-
if (sessionsPanel) sessionsPanel.classList.add("hidden");
|
|
1991
|
-
if (filesPanel) filesPanel.classList.add("hidden");
|
|
1992
|
-
if (sessionsHeaderContent) sessionsHeaderContent.classList.add("hidden");
|
|
1993
|
-
if (filesHeaderContent) filesHeaderContent.classList.add("hidden");
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
function showProjectsPanel() {
|
|
1997
|
-
hideAllPanels();
|
|
1998
|
-
if (projectsPanel) projectsPanel.classList.remove("hidden");
|
|
1999
|
-
}
|
|
2000
|
-
|
|
2001
|
-
function showSessionsPanel() {
|
|
2002
|
-
hideAllPanels();
|
|
2003
|
-
if (sessionsPanel) sessionsPanel.classList.remove("hidden");
|
|
2004
|
-
if (sessionsHeaderContent) sessionsHeaderContent.classList.remove("hidden");
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
|
-
function showFilesPanel() {
|
|
2008
|
-
hideAllPanels();
|
|
2009
|
-
if (filesPanel) filesPanel.classList.remove("hidden");
|
|
2010
|
-
if (filesHeaderContent) filesHeaderContent.classList.remove("hidden");
|
|
2011
|
-
if (ctx.onFilesTabOpen) ctx.onFilesTabOpen();
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
if (fileBrowserBtn) {
|
|
2015
|
-
fileBrowserBtn.addEventListener("click", showFilesPanel);
|
|
2016
|
-
}
|
|
2017
|
-
if (filePanelClose) {
|
|
2018
|
-
filePanelClose.addEventListener("click", showSessionsPanel);
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
// --- Mobile sheet close handlers ---
|
|
2022
|
-
var mobileSheet = document.getElementById("mobile-sheet");
|
|
2023
|
-
if (mobileSheet) {
|
|
2024
|
-
var sheetBackdrop = mobileSheet.querySelector(".mobile-sheet-backdrop");
|
|
2025
|
-
var sheetCloseBtn = mobileSheet.querySelector(".mobile-sheet-close");
|
|
2026
|
-
if (sheetBackdrop) sheetBackdrop.addEventListener("click", closeMobileSheet);
|
|
2027
|
-
if (sheetCloseBtn) sheetCloseBtn.addEventListener("click", closeMobileSheet);
|
|
2028
|
-
|
|
2029
|
-
// --- Drag to dismiss sheet ---
|
|
2030
|
-
var sheetHandle = mobileSheet.querySelector(".mobile-sheet-handle");
|
|
2031
|
-
var sheetContent = mobileSheet.querySelector(".mobile-sheet-content");
|
|
2032
|
-
if (sheetHandle && sheetContent) {
|
|
2033
|
-
var dragStartY = 0;
|
|
2034
|
-
var dragging = false;
|
|
2035
|
-
|
|
2036
|
-
sheetHandle.addEventListener("touchstart", function (e) {
|
|
2037
|
-
dragStartY = e.touches[0].clientY;
|
|
2038
|
-
dragging = true;
|
|
2039
|
-
sheetContent.style.transition = "none";
|
|
2040
|
-
}, { passive: true });
|
|
2041
|
-
|
|
2042
|
-
mobileSheet.addEventListener("touchmove", function (e) {
|
|
2043
|
-
if (!dragging) return;
|
|
2044
|
-
var deltaY = e.touches[0].clientY - dragStartY;
|
|
2045
|
-
if (deltaY < 0) deltaY = 0;
|
|
2046
|
-
sheetContent.style.transform = "translateY(" + deltaY + "px)";
|
|
2047
|
-
if (sheetBackdrop) {
|
|
2048
|
-
var opacity = Math.max(0, 1 - deltaY / (sheetContent.offsetHeight * 0.5));
|
|
2049
|
-
sheetBackdrop.style.opacity = opacity;
|
|
2050
|
-
}
|
|
2051
|
-
}, { passive: true });
|
|
2052
|
-
|
|
2053
|
-
mobileSheet.addEventListener("touchend", function () {
|
|
2054
|
-
if (!dragging) return;
|
|
2055
|
-
dragging = false;
|
|
2056
|
-
var currentY = parseFloat(sheetContent.style.transform.replace(/[^0-9.-]/g, "")) || 0;
|
|
2057
|
-
var threshold = sheetContent.offsetHeight * 0.3;
|
|
2058
|
-
|
|
2059
|
-
if (currentY > threshold) {
|
|
2060
|
-
sheetContent.style.transition = "transform 0.22s ease-in";
|
|
2061
|
-
sheetContent.style.transform = "translateY(100%)";
|
|
2062
|
-
if (sheetBackdrop) {
|
|
2063
|
-
sheetBackdrop.style.transition = "opacity 0.22s ease-in";
|
|
2064
|
-
sheetBackdrop.style.opacity = "0";
|
|
2065
|
-
}
|
|
2066
|
-
setTimeout(function () {
|
|
2067
|
-
sheetContent.style.transition = "";
|
|
2068
|
-
sheetContent.style.transform = "";
|
|
2069
|
-
if (sheetBackdrop) {
|
|
2070
|
-
sheetBackdrop.style.transition = "";
|
|
2071
|
-
sheetBackdrop.style.opacity = "";
|
|
2072
|
-
}
|
|
2073
|
-
// Close without animation since we already animated
|
|
2074
|
-
var sheet = document.getElementById("mobile-sheet");
|
|
2075
|
-
if (sheet) {
|
|
2076
|
-
if (sheet.classList.contains("sheet-files")) {
|
|
2077
|
-
var fileTree = document.getElementById("file-tree");
|
|
2078
|
-
var sidebarFilesPanel = document.getElementById("sidebar-panel-files");
|
|
2079
|
-
if (fileTree && sidebarFilesPanel) {
|
|
2080
|
-
sidebarFilesPanel.appendChild(fileTree);
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2083
|
-
sheet.classList.add("hidden");
|
|
2084
|
-
sheet.classList.remove("closing", "sheet-files");
|
|
2085
|
-
}
|
|
2086
|
-
}, 230);
|
|
2087
|
-
} else {
|
|
2088
|
-
sheetContent.style.transition = "transform 0.2s ease-out";
|
|
2089
|
-
sheetContent.style.transform = "translateY(0)";
|
|
2090
|
-
if (sheetBackdrop) {
|
|
2091
|
-
sheetBackdrop.style.transition = "opacity 0.2s ease-out";
|
|
2092
|
-
sheetBackdrop.style.opacity = "";
|
|
2093
|
-
}
|
|
2094
|
-
setTimeout(function () {
|
|
2095
|
-
sheetContent.style.transition = "";
|
|
2096
|
-
sheetContent.style.transform = "";
|
|
2097
|
-
if (sheetBackdrop) {
|
|
2098
|
-
sheetBackdrop.style.transition = "";
|
|
2099
|
-
sheetBackdrop.style.opacity = "";
|
|
2100
|
-
}
|
|
2101
|
-
}, 200);
|
|
2102
|
-
}
|
|
2103
|
-
}, { passive: true });
|
|
2104
|
-
}
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2107
|
-
// --- Mobile tab bar ---
|
|
2108
|
-
var mobileTabBar = document.getElementById("mobile-tab-bar");
|
|
2109
|
-
var mobileTabs = mobileTabBar ? mobileTabBar.querySelectorAll(".mobile-tab") : [];
|
|
2110
|
-
var mobileHomeBtn = document.getElementById("mobile-home-btn");
|
|
2111
|
-
|
|
2112
|
-
function setMobileTabActive(tabName) {
|
|
2113
|
-
for (var i = 0; i < mobileTabs.length; i++) {
|
|
2114
|
-
if (mobileTabs[i].dataset.tab === tabName) {
|
|
2115
|
-
mobileTabs[i].classList.add("active");
|
|
2116
|
-
} else {
|
|
2117
|
-
mobileTabs[i].classList.remove("active");
|
|
2118
|
-
}
|
|
2119
|
-
}
|
|
2120
|
-
if (mobileHomeBtn) {
|
|
2121
|
-
if (tabName === "home") {
|
|
2122
|
-
mobileHomeBtn.classList.add("active");
|
|
2123
|
-
} else {
|
|
2124
|
-
mobileHomeBtn.classList.remove("active");
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
|
-
}
|
|
2128
|
-
|
|
2129
|
-
for (var t = 0; t < mobileTabs.length; t++) {
|
|
2130
|
-
(function (tab) {
|
|
2131
|
-
tab.addEventListener("click", function () {
|
|
2132
|
-
var name = tab.dataset.tab;
|
|
2133
|
-
|
|
2134
|
-
if (name === "chat") {
|
|
2135
|
-
openMobileSheet("sessions");
|
|
2136
|
-
setMobileTabActive("chat");
|
|
2137
|
-
} else if (name === "search") {
|
|
2138
|
-
openCommandPalette();
|
|
2139
|
-
setMobileTabActive("search");
|
|
2140
|
-
} else if (name === "tools") {
|
|
2141
|
-
openMobileSheet("tools");
|
|
2142
|
-
setMobileTabActive("tools");
|
|
2143
|
-
} else if (name === "settings") {
|
|
2144
|
-
openMobileSheet("settings");
|
|
2145
|
-
setMobileTabActive("settings");
|
|
2146
|
-
}
|
|
2147
|
-
});
|
|
2148
|
-
})(mobileTabs[t]);
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
if (mobileHomeBtn) {
|
|
2152
|
-
mobileHomeBtn.addEventListener("click", function () {
|
|
2153
|
-
closeSidebar();
|
|
2154
|
-
setMobileTabActive("home");
|
|
2155
|
-
if (ctx.showHomeHub) ctx.showHomeHub();
|
|
2156
|
-
});
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
// --- User island width sync ---
|
|
2160
|
-
var userIsland = document.getElementById("user-island");
|
|
2161
|
-
var sidebarColumn = document.getElementById("sidebar-column");
|
|
2162
|
-
|
|
2163
|
-
function syncUserIslandWidth() {
|
|
2164
|
-
if (!userIsland) return;
|
|
2165
|
-
var mateSidebarColumn = document.getElementById("mate-sidebar-column");
|
|
2166
|
-
var isMateDM = document.body.classList.contains("mate-dm-active");
|
|
2167
|
-
var col = (isMateDM && mateSidebarColumn && !mateSidebarColumn.classList.contains("hidden")) ? mateSidebarColumn : sidebarColumn;
|
|
2168
|
-
if (!col) return;
|
|
2169
|
-
var rect = col.getBoundingClientRect();
|
|
2170
|
-
userIsland.style.width = (rect.right - 8 - 8) + "px";
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
// --- Sidebar resize handle ---
|
|
2174
|
-
var resizeHandle = document.getElementById("sidebar-resize-handle");
|
|
2175
|
-
|
|
2176
|
-
function syncResizeHandle() {
|
|
2177
|
-
if (!resizeHandle || !sidebarColumn) return;
|
|
2178
|
-
var rect = sidebarColumn.getBoundingClientRect();
|
|
2179
|
-
var parentRect = sidebarColumn.parentElement.getBoundingClientRect();
|
|
2180
|
-
resizeHandle.style.left = (rect.right - parentRect.left) + "px";
|
|
2181
|
-
}
|
|
2182
|
-
|
|
2183
|
-
if (resizeHandle && sidebarColumn) {
|
|
2184
|
-
var dragging = false;
|
|
2185
|
-
|
|
2186
|
-
function onResizeMove(e) {
|
|
2187
|
-
if (!dragging) return;
|
|
2188
|
-
e.preventDefault();
|
|
2189
|
-
var clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
|
2190
|
-
var iconStrip = document.getElementById("icon-strip");
|
|
2191
|
-
var stripWidth = iconStrip ? iconStrip.offsetWidth : 72;
|
|
2192
|
-
var newWidth = clientX - stripWidth;
|
|
2193
|
-
if (newWidth < 192) newWidth = 192;
|
|
2194
|
-
if (newWidth > 320) newWidth = 320;
|
|
2195
|
-
sidebarColumn.style.width = newWidth + "px";
|
|
2196
|
-
syncResizeHandle();
|
|
2197
|
-
syncUserIslandWidth();
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
function onResizeEnd() {
|
|
2201
|
-
if (!dragging) return;
|
|
2202
|
-
dragging = false;
|
|
2203
|
-
resizeHandle.classList.remove("dragging");
|
|
2204
|
-
document.body.style.cursor = "";
|
|
2205
|
-
document.body.style.userSelect = "";
|
|
2206
|
-
document.removeEventListener("mousemove", onResizeMove);
|
|
2207
|
-
document.removeEventListener("mouseup", onResizeEnd);
|
|
2208
|
-
document.removeEventListener("touchmove", onResizeMove);
|
|
2209
|
-
document.removeEventListener("touchend", onResizeEnd);
|
|
2210
|
-
try { localStorage.setItem("sidebar-width", sidebarColumn.style.width); } catch (e) {}
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
function onResizeStart(e) {
|
|
2214
|
-
e.preventDefault();
|
|
2215
|
-
dragging = true;
|
|
2216
|
-
resizeHandle.classList.add("dragging");
|
|
2217
|
-
document.body.style.cursor = "col-resize";
|
|
2218
|
-
document.body.style.userSelect = "none";
|
|
2219
|
-
document.addEventListener("mousemove", onResizeMove);
|
|
2220
|
-
document.addEventListener("mouseup", onResizeEnd);
|
|
2221
|
-
document.addEventListener("touchmove", onResizeMove, { passive: false });
|
|
2222
|
-
document.addEventListener("touchend", onResizeEnd);
|
|
2223
|
-
}
|
|
2224
|
-
|
|
2225
|
-
resizeHandle.addEventListener("mousedown", onResizeStart);
|
|
2226
|
-
resizeHandle.addEventListener("touchstart", onResizeStart, { passive: false });
|
|
2227
|
-
|
|
2228
|
-
// Restore saved width (skip transition so user-island syncs immediately)
|
|
2229
|
-
try {
|
|
2230
|
-
var savedWidth = localStorage.getItem("sidebar-width");
|
|
2231
|
-
if (savedWidth) {
|
|
2232
|
-
var px = parseInt(savedWidth, 10);
|
|
2233
|
-
if (px >= 192 && px <= 320) {
|
|
2234
|
-
sidebarColumn.style.transition = "none";
|
|
2235
|
-
sidebarColumn.style.width = px + "px";
|
|
2236
|
-
sidebarColumn.offsetWidth; // force reflow
|
|
2237
|
-
sidebarColumn.style.transition = "";
|
|
2238
|
-
}
|
|
2239
|
-
}
|
|
2240
|
-
} catch (e) {}
|
|
2241
|
-
|
|
2242
|
-
syncResizeHandle();
|
|
2243
|
-
syncUserIslandWidth();
|
|
2244
|
-
}
|
|
2245
|
-
|
|
2246
|
-
// Initial sync even if no resize handle
|
|
2247
|
-
syncUserIslandWidth();
|
|
2248
|
-
|
|
2249
|
-
// --- User island tooltip on hover (collapsed sidebar) ---
|
|
2250
|
-
if (userIsland) {
|
|
2251
|
-
var profileArea = userIsland.querySelector(".user-island-profile");
|
|
2252
|
-
if (profileArea) {
|
|
2253
|
-
profileArea.addEventListener("mouseenter", function () {
|
|
2254
|
-
var layout = document.getElementById("layout");
|
|
2255
|
-
if (!layout || !layout.classList.contains("sidebar-collapsed")) return;
|
|
2256
|
-
var nameEl = userIsland.querySelector(".user-island-name");
|
|
2257
|
-
var text = nameEl ? nameEl.textContent : "";
|
|
2258
|
-
if (text) showIconTooltip(profileArea, text);
|
|
2259
|
-
});
|
|
2260
|
-
profileArea.addEventListener("mouseleave", function () {
|
|
2261
|
-
hideIconTooltip();
|
|
2262
|
-
});
|
|
2263
|
-
}
|
|
2264
|
-
}
|
|
2265
|
-
|
|
2266
|
-
// --- Schedule countdown timer ---
|
|
2267
|
-
startCountdownTimer();
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
function startCountdownTimer() {
|
|
2271
|
-
if (countdownTimer) clearInterval(countdownTimer);
|
|
2272
|
-
countdownTimer = setInterval(updateCountdowns, 1000);
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
|
-
function updateCountdowns() {
|
|
2276
|
-
if (!ctx || !ctx.getUpcomingSchedules || !ctx.sessionListEl) return;
|
|
2277
|
-
var upcoming = ctx.getUpcomingSchedules(3 * 60 * 1000); // 3 minutes
|
|
2278
|
-
|
|
2279
|
-
// Remove stale container
|
|
2280
|
-
if (countdownContainer && !ctx.sessionListEl.contains(countdownContainer)) {
|
|
2281
|
-
countdownContainer = null;
|
|
2282
|
-
}
|
|
2283
|
-
|
|
2284
|
-
if (upcoming.length === 0) {
|
|
2285
|
-
if (countdownContainer) {
|
|
2286
|
-
countdownContainer.remove();
|
|
2287
|
-
countdownContainer = null;
|
|
2288
|
-
}
|
|
2289
|
-
return;
|
|
2290
|
-
}
|
|
2291
|
-
|
|
2292
|
-
if (!countdownContainer) {
|
|
2293
|
-
countdownContainer = document.createElement("div");
|
|
2294
|
-
countdownContainer.className = "session-countdown-group";
|
|
2295
|
-
ctx.sessionListEl.insertBefore(countdownContainer, ctx.sessionListEl.firstChild);
|
|
2296
|
-
}
|
|
2297
|
-
|
|
2298
|
-
var html = "";
|
|
2299
|
-
var now = Date.now();
|
|
2300
|
-
for (var i = 0; i < upcoming.length; i++) {
|
|
2301
|
-
var u = upcoming[i];
|
|
2302
|
-
var remaining = Math.max(0, Math.ceil((u.nextRunAt - now) / 1000));
|
|
2303
|
-
var min = Math.floor(remaining / 60);
|
|
2304
|
-
var sec = remaining % 60;
|
|
2305
|
-
var timeStr = min + ":" + (sec < 10 ? "0" : "") + sec;
|
|
2306
|
-
var colorStyle = u.color ? " style=\"border-left-color:" + u.color + "\"" : "";
|
|
2307
|
-
html += '<div class="session-countdown-item"' + colorStyle + '>';
|
|
2308
|
-
html += '<span class="session-countdown-name">' + escapeHtml(u.name) + '</span>';
|
|
2309
|
-
html += '<span class="session-countdown-badge">' + timeStr + '</span>';
|
|
2310
|
-
html += '</div>';
|
|
2311
|
-
}
|
|
2312
|
-
countdownContainer.innerHTML = html;
|
|
2313
|
-
}
|
|
2314
|
-
|
|
2315
|
-
// --- CLI session picker ---
|
|
2316
|
-
function relativeTime(isoString) {
|
|
2317
|
-
if (!isoString) return "";
|
|
2318
|
-
var ms = Date.now() - new Date(isoString).getTime();
|
|
2319
|
-
var sec = Math.floor(ms / 1000);
|
|
2320
|
-
if (sec < 60) return "just now";
|
|
2321
|
-
var min = Math.floor(sec / 60);
|
|
2322
|
-
if (min < 60) return min + "m ago";
|
|
2323
|
-
var hr = Math.floor(min / 60);
|
|
2324
|
-
if (hr < 24) return hr + "h ago";
|
|
2325
|
-
var days = Math.floor(hr / 24);
|
|
2326
|
-
if (days < 30) return days + "d ago";
|
|
2327
|
-
return new Date(isoString).toLocaleDateString();
|
|
2328
|
-
}
|
|
2329
|
-
|
|
2330
|
-
export function populateCliSessionList(sessions) {
|
|
2331
|
-
var pickerLoading = ctx.$("resume-picker-loading");
|
|
2332
|
-
var pickerEmpty = ctx.$("resume-picker-empty");
|
|
2333
|
-
var pickerList = ctx.$("resume-picker-list");
|
|
2334
|
-
if (!pickerLoading || !pickerList) return;
|
|
2335
|
-
|
|
2336
|
-
pickerLoading.classList.add("hidden");
|
|
2337
|
-
|
|
2338
|
-
if (!sessions || sessions.length === 0) {
|
|
2339
|
-
pickerEmpty.classList.remove("hidden");
|
|
2340
|
-
pickerList.classList.add("hidden");
|
|
2341
|
-
return;
|
|
2342
|
-
}
|
|
2343
|
-
|
|
2344
|
-
pickerEmpty.classList.add("hidden");
|
|
2345
|
-
pickerList.classList.remove("hidden");
|
|
2346
|
-
pickerList.innerHTML = "";
|
|
2347
|
-
|
|
2348
|
-
for (var i = 0; i < sessions.length; i++) {
|
|
2349
|
-
var s = sessions[i];
|
|
2350
|
-
var item = document.createElement("div");
|
|
2351
|
-
item.className = "cli-session-item";
|
|
2352
|
-
|
|
2353
|
-
var title = document.createElement("div");
|
|
2354
|
-
title.className = "cli-session-title";
|
|
2355
|
-
title.textContent = s.firstPrompt || "Untitled session";
|
|
2356
|
-
item.appendChild(title);
|
|
2357
|
-
|
|
2358
|
-
var meta = document.createElement("div");
|
|
2359
|
-
meta.className = "cli-session-meta";
|
|
2360
|
-
if (s.lastActivity) {
|
|
2361
|
-
var time = document.createElement("span");
|
|
2362
|
-
time.textContent = relativeTime(s.lastActivity);
|
|
2363
|
-
meta.appendChild(time);
|
|
2364
|
-
}
|
|
2365
|
-
if (s.model) {
|
|
2366
|
-
var model = document.createElement("span");
|
|
2367
|
-
model.className = "badge";
|
|
2368
|
-
model.textContent = s.model;
|
|
2369
|
-
meta.appendChild(model);
|
|
2370
|
-
}
|
|
2371
|
-
if (s.gitBranch) {
|
|
2372
|
-
var branch = document.createElement("span");
|
|
2373
|
-
branch.className = "badge";
|
|
2374
|
-
branch.textContent = s.gitBranch;
|
|
2375
|
-
meta.appendChild(branch);
|
|
2376
|
-
}
|
|
2377
|
-
item.appendChild(meta);
|
|
2378
|
-
|
|
2379
|
-
(function (sessionId) {
|
|
2380
|
-
item.addEventListener("click", function () {
|
|
2381
|
-
if (ctx.ws && ctx.connected) {
|
|
2382
|
-
ctx.ws.send(JSON.stringify({ type: "resume_session", cliSessionId: sessionId }));
|
|
2383
|
-
}
|
|
2384
|
-
var modal = ctx.$("resume-modal");
|
|
2385
|
-
if (modal) modal.classList.add("hidden");
|
|
2386
|
-
closeSidebar();
|
|
2387
|
-
});
|
|
2388
|
-
})(s.sessionId);
|
|
2389
|
-
|
|
2390
|
-
pickerList.appendChild(item);
|
|
2391
|
-
}
|
|
2392
|
-
}
|
|
2393
|
-
|
|
2394
|
-
// --- Icon Strip (Discord-style project icons) ---
|
|
2395
|
-
var iconStripTooltip = null;
|
|
2396
|
-
|
|
2397
|
-
function getProjectAbbrev(name) {
|
|
2398
|
-
if (!name) return "?";
|
|
2399
|
-
// Take first letter of each word, max 2 chars
|
|
2400
|
-
var words = name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/);
|
|
2401
|
-
if (words.length >= 2) {
|
|
2402
|
-
return (words[0][0] + words[1][0]).toUpperCase();
|
|
2403
|
-
}
|
|
2404
|
-
return name.substring(0, 2).toUpperCase();
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
|
-
function showIconTooltip(el, text) {
|
|
2408
|
-
hideIconTooltip();
|
|
2409
|
-
var tip = document.createElement("div");
|
|
2410
|
-
tip.className = "icon-strip-tooltip";
|
|
2411
|
-
tip.textContent = text;
|
|
2412
|
-
document.body.appendChild(tip);
|
|
2413
|
-
iconStripTooltip = tip;
|
|
2414
|
-
|
|
2415
|
-
requestAnimationFrame(function () {
|
|
2416
|
-
var rect = el.getBoundingClientRect();
|
|
2417
|
-
tip.style.top = (rect.top + rect.height / 2 - tip.offsetHeight / 2) + "px";
|
|
2418
|
-
tip.classList.add("visible");
|
|
2419
|
-
});
|
|
2420
|
-
}
|
|
2421
|
-
|
|
2422
|
-
function showIconTooltipHtml(el, html) {
|
|
2423
|
-
hideIconTooltip();
|
|
2424
|
-
var tip = document.createElement("div");
|
|
2425
|
-
tip.className = "icon-strip-tooltip";
|
|
2426
|
-
tip.style.whiteSpace = "normal";
|
|
2427
|
-
tip.style.maxWidth = "260px";
|
|
2428
|
-
tip.innerHTML = html;
|
|
2429
|
-
document.body.appendChild(tip);
|
|
2430
|
-
iconStripTooltip = tip;
|
|
2431
|
-
|
|
2432
|
-
requestAnimationFrame(function () {
|
|
2433
|
-
var rect = el.getBoundingClientRect();
|
|
2434
|
-
tip.style.top = (rect.top + rect.height / 2 - tip.offsetHeight / 2) + "px";
|
|
2435
|
-
tip.classList.add("visible");
|
|
2436
|
-
});
|
|
2437
|
-
}
|
|
2438
|
-
|
|
2439
|
-
function hideIconTooltip() {
|
|
2440
|
-
if (iconStripTooltip) {
|
|
2441
|
-
iconStripTooltip.remove();
|
|
2442
|
-
iconStripTooltip = null;
|
|
2443
|
-
}
|
|
2444
|
-
}
|
|
2445
|
-
|
|
2446
|
-
// --- DM user context menu ---
|
|
2447
|
-
var userCtxMenu = null;
|
|
2448
|
-
|
|
2449
|
-
function closeUserCtxMenu() {
|
|
2450
|
-
if (userCtxMenu) {
|
|
2451
|
-
userCtxMenu.remove();
|
|
2452
|
-
userCtxMenu = null;
|
|
2453
|
-
}
|
|
2454
|
-
document.removeEventListener("click", handleUserCtxOutsideClick, true);
|
|
2455
|
-
}
|
|
2456
|
-
|
|
2457
|
-
function showUserCtxMenu(anchorEl, user) {
|
|
2458
|
-
closeUserCtxMenu();
|
|
2459
|
-
closeProjectCtxMenu();
|
|
2460
|
-
|
|
2461
|
-
var menu = document.createElement("div");
|
|
2462
|
-
menu.className = "project-ctx-menu";
|
|
2463
|
-
|
|
2464
|
-
var removeItem = document.createElement("button");
|
|
2465
|
-
removeItem.className = "project-ctx-item project-ctx-delete";
|
|
2466
|
-
removeItem.innerHTML = iconHtml("user-minus") + " <span>Remove from favorites</span>";
|
|
2467
|
-
removeItem.addEventListener("click", function (e) {
|
|
2468
|
-
e.stopPropagation();
|
|
2469
|
-
// Spawn dust particles at the user icon position
|
|
2470
|
-
var iconRect = anchorEl.getBoundingClientRect();
|
|
2471
|
-
spawnDustParticles(iconRect.left + iconRect.width / 2, iconRect.top + iconRect.height / 2);
|
|
2472
|
-
closeUserCtxMenu();
|
|
2473
|
-
// Immediately mark as removed so strip re-render hides the icon,
|
|
2474
|
-
// even if the user was only visible via cachedDmConversations (not favorites)
|
|
2475
|
-
cachedDmRemovedUsers[user.id] = true;
|
|
2476
|
-
if (ctx.onDmRemoveUser) ctx.onDmRemoveUser(user.id);
|
|
2477
|
-
renderUserStrip(cachedAllUsers, cachedOnlineUserIds, cachedMyUserId, cachedDmFavorites, cachedDmConversations, cachedDmUnread, cachedDmRemovedUsers, cachedMates);
|
|
2478
|
-
if (ctx.sendWs) {
|
|
2479
|
-
ctx.sendWs({ type: "dm_remove_favorite", targetUserId: user.id });
|
|
2480
|
-
}
|
|
2481
|
-
});
|
|
2482
|
-
menu.appendChild(removeItem);
|
|
2483
|
-
|
|
2484
|
-
document.body.appendChild(menu);
|
|
2485
|
-
userCtxMenu = menu;
|
|
2486
|
-
refreshIcons();
|
|
2487
|
-
|
|
2488
|
-
requestAnimationFrame(function () {
|
|
2489
|
-
var rect = anchorEl.getBoundingClientRect();
|
|
2490
|
-
menu.style.position = "fixed";
|
|
2491
|
-
menu.style.left = (rect.right + 6) + "px";
|
|
2492
|
-
menu.style.top = rect.top + "px";
|
|
2493
|
-
var menuRect = menu.getBoundingClientRect();
|
|
2494
|
-
if (menuRect.right > window.innerWidth - 8) {
|
|
2495
|
-
menu.style.left = (rect.left - menuRect.width - 6) + "px";
|
|
2496
|
-
}
|
|
2497
|
-
if (menuRect.bottom > window.innerHeight - 8) {
|
|
2498
|
-
menu.style.top = (window.innerHeight - menuRect.height - 8) + "px";
|
|
2499
|
-
}
|
|
2500
|
-
});
|
|
2501
|
-
|
|
2502
|
-
// Close on outside click
|
|
2503
|
-
setTimeout(function () {
|
|
2504
|
-
document.addEventListener("click", handleUserCtxOutsideClick, true);
|
|
2505
|
-
}, 0);
|
|
2506
|
-
}
|
|
2507
|
-
|
|
2508
|
-
function handleUserCtxOutsideClick(e) {
|
|
2509
|
-
if (userCtxMenu && !userCtxMenu.contains(e.target)) {
|
|
2510
|
-
closeUserCtxMenu();
|
|
2511
|
-
}
|
|
2512
|
-
}
|
|
2513
|
-
|
|
2514
|
-
function showMateCtxMenu(anchorEl, mate) {
|
|
2515
|
-
// Primary mates cannot be edited or removed
|
|
2516
|
-
if (mate.primary) return;
|
|
2517
|
-
|
|
2518
|
-
closeUserCtxMenu();
|
|
2519
|
-
closeProjectCtxMenu();
|
|
2520
|
-
|
|
2521
|
-
var menu = document.createElement("div");
|
|
2522
|
-
menu.className = "project-ctx-menu";
|
|
2523
|
-
|
|
2524
|
-
// Edit Profile item
|
|
2525
|
-
var editItem = document.createElement("button");
|
|
2526
|
-
editItem.className = "project-ctx-item";
|
|
2527
|
-
editItem.innerHTML = iconHtml("edit-2") + " <span>Edit Profile</span>";
|
|
2528
|
-
editItem.addEventListener("click", function (e) {
|
|
2529
|
-
e.stopPropagation();
|
|
2530
|
-
closeUserCtxMenu();
|
|
2531
|
-
showMateProfilePopover(anchorEl, mate, function (updates) {
|
|
2532
|
-
if (ctx.sendWs) {
|
|
2533
|
-
ctx.sendWs({ type: "mate_update", mateId: mate.id, updates: updates });
|
|
2534
|
-
}
|
|
2535
|
-
});
|
|
2536
|
-
});
|
|
2537
|
-
menu.appendChild(editItem);
|
|
2538
|
-
|
|
2539
|
-
var removeItem = document.createElement("button");
|
|
2540
|
-
removeItem.className = "project-ctx-item";
|
|
2541
|
-
removeItem.innerHTML = iconHtml("star-off") + " <span>Remove from favorites</span>";
|
|
2542
|
-
removeItem.addEventListener("click", function (e) {
|
|
2543
|
-
e.stopPropagation();
|
|
2544
|
-
closeUserCtxMenu();
|
|
2545
|
-
// Spawn dust particles at the mate icon position
|
|
2546
|
-
var iconRect = anchorEl.getBoundingClientRect();
|
|
2547
|
-
spawnDustParticles(iconRect.left + iconRect.width / 2, iconRect.top + iconRect.height / 2);
|
|
2548
|
-
if (ctx.sendWs) {
|
|
2549
|
-
ctx.sendWs({ type: "dm_remove_favorite", targetUserId: mate.id });
|
|
2550
|
-
}
|
|
2551
|
-
});
|
|
2552
|
-
menu.appendChild(removeItem);
|
|
2553
|
-
|
|
2554
|
-
document.body.appendChild(menu);
|
|
2555
|
-
userCtxMenu = menu;
|
|
2556
|
-
refreshIcons();
|
|
2557
|
-
|
|
2558
|
-
requestAnimationFrame(function () {
|
|
2559
|
-
var rect = anchorEl.getBoundingClientRect();
|
|
2560
|
-
menu.style.position = "fixed";
|
|
2561
|
-
menu.style.left = (rect.right + 6) + "px";
|
|
2562
|
-
menu.style.top = rect.top + "px";
|
|
2563
|
-
var menuRect = menu.getBoundingClientRect();
|
|
2564
|
-
if (menuRect.right > window.innerWidth - 8) {
|
|
2565
|
-
menu.style.left = (rect.left - menuRect.width - 6) + "px";
|
|
2566
|
-
}
|
|
2567
|
-
if (menuRect.bottom > window.innerHeight - 8) {
|
|
2568
|
-
menu.style.top = (window.innerHeight - menuRect.height - 8) + "px";
|
|
2569
|
-
}
|
|
2570
|
-
});
|
|
2571
|
-
|
|
2572
|
-
setTimeout(function () {
|
|
2573
|
-
document.addEventListener("click", handleUserCtxOutsideClick, true);
|
|
2574
|
-
}, 0);
|
|
2575
|
-
}
|
|
2576
|
-
|
|
2577
|
-
// --- Project context menu ---
|
|
2578
|
-
var projectCtxMenu = null;
|
|
2579
|
-
|
|
2580
|
-
var EMOJI_CATEGORIES = [
|
|
2581
|
-
{ id: "frequent", icon: "🕐", label: "Frequent", emojis: [
|
|
2582
|
-
"😀","😎","🤓","🧠","💡","🔥","⚡","🚀",
|
|
2583
|
-
"🎯","🎮","🎨","🎵","📦","📁","📝","💻",
|
|
2584
|
-
"🖥️","⌨️","🔧","🛠️","⚙️","🧪","🔬","🧬",
|
|
2585
|
-
"🌍","🌱","🌊","🌸","🍀","🌈","☀️","🌙",
|
|
2586
|
-
"🐱","🐶","🐼","🦊","🦋","🐝","🐙","🦄",
|
|
2587
|
-
"🍕","🍔","☕","🍩","🍎","🍇","🧁","🍣",
|
|
2588
|
-
"❤️","💜","💙","💚","💛","🧡","🤍","🖤",
|
|
2589
|
-
"⭐","✨","💎","🏆","👑","🎪","🎭","🃏",
|
|
2590
|
-
]},
|
|
2591
|
-
{ id: "smileys", icon: "😀", label: "Smileys & People", emojis: [
|
|
2592
|
-
"😀","😃","😄","😁","😆","😅","🤣","😂",
|
|
2593
|
-
"🙂","😊","😇","🥰","😍","🤩","😘","😗",
|
|
2594
|
-
"😚","😙","🥲","😋","😛","😜","🤪","😝",
|
|
2595
|
-
"🤑","🤗","🤭","🫢","🤫","🤔","🫡","🤐",
|
|
2596
|
-
"🤨","😐","😑","😶","🫥","😏","😒","🙄",
|
|
2597
|
-
"😬","🤥","😌","😔","😪","🤤","😴","😷",
|
|
2598
|
-
"🤒","🤕","🤢","🤮","🥴","😵","🤯","🥳",
|
|
2599
|
-
"🥸","😎","🤓","🧐","😕","🫤","😟","🙁",
|
|
2600
|
-
"😮","😯","😲","😳","🥺","🥹","😦","😧",
|
|
2601
|
-
"😨","😰","😥","😢","😭","😱","😖","😣",
|
|
2602
|
-
"😞","😓","😩","😫","🥱","😤","😡","😠",
|
|
2603
|
-
"🤬","😈","👿","💀","☠️","💩","🤡","👹",
|
|
2604
|
-
"👺","👻","👽","👾","🤖","😺","😸","😹",
|
|
2605
|
-
"😻","😼","😽","🙀","😿","😾","🙈","🙉",
|
|
2606
|
-
"🙊","👋","🤚","🖐️","✋","🖖","🫱","🫲",
|
|
2607
|
-
"🫳","🫴","👌","🤌","🤏","✌️","🤞","🫰",
|
|
2608
|
-
"🤟","🤘","🤙","👈","👉","👆","🖕","👇",
|
|
2609
|
-
"☝️","🫵","👍","👎","✊","👊","🤛","🤜",
|
|
2610
|
-
"👏","🙌","🫶","👐","🤲","🤝","🙏","💪",
|
|
2611
|
-
]},
|
|
2612
|
-
{ id: "animals", icon: "🐻", label: "Animals & Nature", emojis: [
|
|
2613
|
-
"🐶","🐱","🐭","🐹","🐰","🦊","🐻","🐼",
|
|
2614
|
-
"🐻❄️","🐨","🐯","🦁","🐮","🐷","🐽","🐸",
|
|
2615
|
-
"🐵","🙈","🙉","🙊","🐒","🐔","🐧","🐦",
|
|
2616
|
-
"🐤","🐣","🐥","🦆","🦅","🦉","🦇","🐺",
|
|
2617
|
-
"🐗","🐴","🦄","🐝","🪱","🐛","🦋","🐌",
|
|
2618
|
-
"🐞","🐜","🪰","🪲","🪳","🦟","🦗","🕷️",
|
|
2619
|
-
"🦂","🐢","🐍","🦎","🦖","🦕","🐙","🦑",
|
|
2620
|
-
"🦐","🦞","🦀","🪸","🐡","🐠","🐟","🐬",
|
|
2621
|
-
"🐳","🐋","🦈","🐊","🐅","🐆","🦓","🫏",
|
|
2622
|
-
"🦍","🦧","🦣","🐘","🦛","🦏","🐪","🐫",
|
|
2623
|
-
"🦒","🦘","🦬","🐃","🐂","🐄","🐎","🐖",
|
|
2624
|
-
"🐏","🐑","🦙","🐐","🦌","🫎","🐕","🐩",
|
|
2625
|
-
"🦮","🐕🦺","🐈","🐈⬛","🪶","🐓","🦃","🦤",
|
|
2626
|
-
"🦚","🦜","🦢","🪿","🦩","🕊️","🐇","🦝",
|
|
2627
|
-
"🦨","🦡","🦫","🦦","🦥","🐁","🐀","🐿️",
|
|
2628
|
-
"🦔","🌵","🎄","🌲","🌳","🌴","🪵","🌱",
|
|
2629
|
-
"🌿","☘️","🍀","🎍","🪴","🎋","🍃","🍂",
|
|
2630
|
-
"🍁","🪺","🪹","🍄","🌾","💐","🌷","🌹",
|
|
2631
|
-
"🥀","🪻","🌺","🌸","🌼","🌻","🌞","🌝",
|
|
2632
|
-
"🌛","🌜","🌚","🌕","🌖","🌗","🌘","🌑",
|
|
2633
|
-
"🌒","🌓","🌔","🌙","🌎","🌍","🌏","🪐",
|
|
2634
|
-
"💫","⭐","🌟","✨","⚡","☄️","💥","🔥",
|
|
2635
|
-
"🌪️","🌈","☀️","🌤️","⛅","🌥️","☁️","🌦️",
|
|
2636
|
-
"🌧️","⛈️","🌩️","❄️","☃️","⛄","🌬️","💨",
|
|
2637
|
-
"💧","💦","🫧","☔","☂️","🌊","🌫️",
|
|
2638
|
-
]},
|
|
2639
|
-
{ id: "food", icon: "🍔", label: "Food & Drink", emojis: [
|
|
2640
|
-
"🍇","🍈","🍉","🍊","🍋","🍌","🍍","🥭",
|
|
2641
|
-
"🍎","🍏","🍐","🍑","🍒","🍓","🫐","🥝",
|
|
2642
|
-
"🍅","🫒","🥥","🥑","🍆","🥔","🥕","🌽",
|
|
2643
|
-
"🌶️","🫑","🥒","🥬","🥦","🧄","🧅","🥜",
|
|
2644
|
-
"🫘","🌰","🫚","🫛","🍞","🥐","🥖","🫓",
|
|
2645
|
-
"🥨","🥯","🥞","🧇","🧀","🍖","🍗","🥩",
|
|
2646
|
-
"🥓","🍔","🍟","🍕","🌭","🥪","🌮","🌯",
|
|
2647
|
-
"🫔","🥙","🧆","🥚","🍳","🥘","🍲","🫕",
|
|
2648
|
-
"🥣","🥗","🍿","🧈","🧂","🥫","🍱","🍘",
|
|
2649
|
-
"🍙","🍚","🍛","🍜","🍝","🍠","🍢","🍣",
|
|
2650
|
-
"🍤","🍥","🥮","🍡","🥟","🥠","🥡","🦀",
|
|
2651
|
-
"🦞","🦐","🦑","🦪","🍦","🍧","🍨","🍩",
|
|
2652
|
-
"🍪","🎂","🍰","🧁","🥧","🍫","🍬","🍭",
|
|
2653
|
-
"🍮","🍯","🍼","🥛","☕","🫖","🍵","🍶",
|
|
2654
|
-
"🍾","🍷","🍸","🍹","🍺","🍻","🥂","🥃",
|
|
2655
|
-
"🫗","🥤","🧋","🧃","🧉","🧊",
|
|
2656
|
-
]},
|
|
2657
|
-
{ id: "activity", icon: "⚽", label: "Activity", emojis: [
|
|
2658
|
-
"⚽","🏀","🏈","⚾","🥎","🎾","🏐","🏉",
|
|
2659
|
-
"🥏","🎱","🪀","🏓","🏸","🏒","🏑","🥍",
|
|
2660
|
-
"🏏","🪃","🥅","⛳","🪁","🛝","🏹","🎣",
|
|
2661
|
-
"🤿","🥊","🥋","🎽","🛹","🛼","🛷","⛸️",
|
|
2662
|
-
"🥌","🎿","⛷️","🏂","🪂","🏋️","🤸","🤺",
|
|
2663
|
-
"⛹️","🤾","🏌️","🏇","🧘","🏄","🏊","🤽",
|
|
2664
|
-
"🚣","🧗","🚵","🚴","🎪","🤹","🎭","🎨",
|
|
2665
|
-
"🎬","🎤","🎧","🎼","🎹","🥁","🪘","🎷",
|
|
2666
|
-
"🎺","🪗","🎸","🪕","🎻","🪈","🎲","♟️",
|
|
2667
|
-
"🎯","🎳","🎮","🕹️","🧩","🪩",
|
|
2668
|
-
]},
|
|
2669
|
-
{ id: "travel", icon: "🚗", label: "Travel & Places", emojis: [
|
|
2670
|
-
"🚗","🚕","🚙","🚌","🚎","🏎️","🚓","🚑",
|
|
2671
|
-
"🚒","🚐","🛻","🚚","🚛","🚜","🛵","🏍️",
|
|
2672
|
-
"🛺","🚲","🛴","🛹","🚏","🛣️","🛤️","⛽",
|
|
2673
|
-
"🛞","🚨","🚥","🚦","🛑","🚧","⚓","🛟",
|
|
2674
|
-
"⛵","🛶","🚤","🛳️","⛴️","🛥️","🚢","✈️",
|
|
2675
|
-
"🛩️","🛫","🛬","🪂","💺","🚁","🚟","🚠",
|
|
2676
|
-
"🚡","🛰️","🚀","🛸","🏠","🏡","🏘️","🏚️",
|
|
2677
|
-
"🏗️","🏭","🏢","🏬","🏣","🏤","🏥","🏦",
|
|
2678
|
-
"🏨","🏪","🏫","🏩","💒","🏛️","⛪","🕌",
|
|
2679
|
-
"🛕","🕍","⛩️","🕋","⛲","⛺","🌁","🌃",
|
|
2680
|
-
"🏙️","🌄","🌅","🌆","🌇","🌉","🗼","🗽",
|
|
2681
|
-
"🗻","🏕️","🎠","🎡","🎢","🏖️","🏝️","🏜️",
|
|
2682
|
-
"🌋","⛰️","🗺️","🧭","🏔️",
|
|
2683
|
-
]},
|
|
2684
|
-
{ id: "objects", icon: "💡", label: "Objects", emojis: [
|
|
2685
|
-
"⌚","📱","📲","💻","⌨️","🖥️","🖨️","🖱️",
|
|
2686
|
-
"🖲️","🕹️","🗜️","💽","💾","💿","📀","📼",
|
|
2687
|
-
"📷","📸","📹","🎥","📽️","🎞️","📞","☎️",
|
|
2688
|
-
"📟","📠","📺","📻","🎙️","🎚️","🎛️","🧭",
|
|
2689
|
-
"⏱️","⏲️","⏰","🕰️","⌛","⏳","📡","🔋",
|
|
2690
|
-
"🪫","🔌","💡","🔦","🕯️","🪔","🧯","🛢️",
|
|
2691
|
-
"🛍️","💰","💴","💵","💶","💷","🪙","💸",
|
|
2692
|
-
"💳","🧾","💹","✉️","📧","📨","📩","📤",
|
|
2693
|
-
"📥","📦","📫","📬","📭","📮","🗳️","✏️",
|
|
2694
|
-
"✒️","🖋️","🖊️","🖌️","🖍️","📝","💼","📁",
|
|
2695
|
-
"📂","🗂️","📅","📆","🗒️","🗓️","📇","📈",
|
|
2696
|
-
"📉","📊","📋","📌","📍","📎","🖇️","📏",
|
|
2697
|
-
"📐","✂️","🗃️","🗄️","🗑️","🔒","🔓","🔏",
|
|
2698
|
-
"🔐","🔑","🗝️","🔨","🪓","⛏️","⚒️","🛠️",
|
|
2699
|
-
"🗡️","⚔️","💣","🪃","🏹","🛡️","🪚","🔧",
|
|
2700
|
-
"🪛","🔩","⚙️","🗜️","⚖️","🦯","🔗","⛓️",
|
|
2701
|
-
"🪝","🧰","🧲","🪜","⚗️","🧪","🧫","🧬",
|
|
2702
|
-
"🔬","🔭","📡","💉","🩸","💊","🩹","🩼",
|
|
2703
|
-
"🩺","🩻","🚪","🛗","🪞","🪟","🛏️","🛋️",
|
|
2704
|
-
"🪑","🚽","🪠","🚿","🛁","🪤","🪒","🧴",
|
|
2705
|
-
"🧷","🧹","🧺","🧻","🪣","🧼","🫧","🪥",
|
|
2706
|
-
"🧽","🧯","🛒","🚬","⚰️","🪦","⚱️","🧿",
|
|
2707
|
-
"🪬","🗿","🪧","🪪",
|
|
2708
|
-
]},
|
|
2709
|
-
{ id: "symbols", icon: "❤️", label: "Symbols", emojis: [
|
|
2710
|
-
"❤️","🧡","💛","💚","💙","💜","🖤","🤍",
|
|
2711
|
-
"🤎","💔","❤️🔥","❤️🩹","❣️","💕","💞","💓",
|
|
2712
|
-
"💗","💖","💘","💝","💟","☮️","✝️","☪️",
|
|
2713
|
-
"🕉️","☸️","🪯","✡️","🔯","🕎","☯️","☦️",
|
|
2714
|
-
"🛐","⛎","♈","♉","♊","♋","♌","♍",
|
|
2715
|
-
"♎","♏","♐","♑","♒","♓","🆔","⚛️",
|
|
2716
|
-
"🉑","☢️","☣️","📴","📳","🈶","🈚","🈸",
|
|
2717
|
-
"🈺","🈷️","✴️","🆚","💮","🉐","㊙️","㊗️",
|
|
2718
|
-
"🈴","🈵","🈹","🈲","🅰️","🅱️","🆎","🆑",
|
|
2719
|
-
"🅾️","🆘","❌","⭕","🛑","⛔","📛","🚫",
|
|
2720
|
-
"💯","💢","♨️","🚷","🚯","🚳","🚱","🔞",
|
|
2721
|
-
"📵","🚭","❗","❕","❓","❔","‼️","⁉️",
|
|
2722
|
-
"🔅","🔆","〽️","⚠️","🚸","🔱","⚜️","🔰",
|
|
2723
|
-
"♻️","✅","🈯","💹","❇️","✳️","❎","🌐",
|
|
2724
|
-
"💠","Ⓜ️","🌀","💤","🏧","🚾","♿","🅿️",
|
|
2725
|
-
"🛗","🈳","🈂️","🛂","🛃","🛄","🛅","🚹",
|
|
2726
|
-
"🚺","🚼","⚧️","🚻","🚮","🎦","📶","🈁",
|
|
2727
|
-
"🔣","ℹ️","🔤","🔡","🔠","🆖","🆗","🆙",
|
|
2728
|
-
"🆒","🆕","🆓","0️⃣","1️⃣","2️⃣","3️⃣","4️⃣",
|
|
2729
|
-
"5️⃣","6️⃣","7️⃣","8️⃣","9️⃣","🔟","🔢","#️⃣",
|
|
2730
|
-
"*️⃣","⏏️","▶️","⏸️","⏯️","⏹️","⏺️","⏭️",
|
|
2731
|
-
"⏮️","⏩","⏪","⏫","⏬","◀️","🔼","🔽",
|
|
2732
|
-
"➡️","⬅️","⬆️","⬇️","↗️","↘️","↙️","↖️",
|
|
2733
|
-
"↕️","↔️","↩️","↪️","⤴️","⤵️","🔀","🔁",
|
|
2734
|
-
"🔂","🔄","🔃","🎵","🎶","✖️","➕","➖",
|
|
2735
|
-
"➗","🟰","♾️","💲","💱","™️","©️","®️",
|
|
2736
|
-
"〰️","➰","➿","🔚","🔙","🔛","🔝","🔜",
|
|
2737
|
-
"✔️","☑️","🔘","🔴","🟠","🟡","🟢","🔵",
|
|
2738
|
-
"🟣","⚫","⚪","🟤","🔺","🔻","🔸","🔹",
|
|
2739
|
-
"🔶","🔷","🔳","🔲","▪️","▫️","◾","◽",
|
|
2740
|
-
"◼️","◻️","🟥","🟧","🟨","🟩","🟦","🟪",
|
|
2741
|
-
"⬛","⬜","🟫","🔈","🔇","🔉","🔊","🔔",
|
|
2742
|
-
"🔕","📣","📢","👁️🗨️","💬","💭","🗯️","♠️",
|
|
2743
|
-
"♣️","♥️","♦️","🃏","🎴","🀄","🕐","🕑",
|
|
2744
|
-
"🕒","🕓","🕔","🕕","🕖","🕗","🕘","🕙","🕚","🕛",
|
|
2745
|
-
]},
|
|
2746
|
-
{ id: "flags", icon: "🏁", label: "Flags", emojis: [
|
|
2747
|
-
"🏁","🚩","🎌","🏴","🏳️","🏳️🌈","🏳️⚧️","🏴☠️",
|
|
2748
|
-
"🇦🇨","🇦🇩","🇦🇪","🇦🇫","🇦🇬","🇦🇮","🇦🇱","🇦🇲",
|
|
2749
|
-
"🇦🇴","🇦🇶","🇦🇷","🇦🇸","🇦🇹","🇦🇺","🇦🇼","🇦🇽",
|
|
2750
|
-
"🇦🇿","🇧🇦","🇧🇧","🇧🇩","🇧🇪","🇧🇫","🇧🇬","🇧🇭",
|
|
2751
|
-
"🇧🇮","🇧🇯","🇧🇱","🇧🇲","🇧🇳","🇧🇴","🇧🇶","🇧🇷",
|
|
2752
|
-
"🇧🇸","🇧🇹","🇧🇻","🇧🇼","🇧🇾","🇧🇿","🇨🇦","🇨🇨",
|
|
2753
|
-
"🇨🇩","🇨🇫","🇨🇬","🇨🇭","🇨🇮","🇨🇰","🇨🇱","🇨🇲",
|
|
2754
|
-
"🇨🇳","🇨🇴","🇨🇵","🇨🇷","🇨🇺","🇨🇻","🇨🇼","🇨🇽",
|
|
2755
|
-
"🇨🇾","🇨🇿","🇩🇪","🇩🇬","🇩🇯","🇩🇰","🇩🇲","🇩🇴",
|
|
2756
|
-
"🇩🇿","🇪🇦","🇪🇨","🇪🇪","🇪🇬","🇪🇭","🇪🇷","🇪🇸",
|
|
2757
|
-
"🇪🇹","🇪🇺","🇫🇮","🇫🇯","🇫🇰","🇫🇲","🇫🇴","🇫🇷",
|
|
2758
|
-
"🇬🇦","🇬🇧","🇬🇩","🇬🇪","🇬🇫","🇬🇬","🇬🇭","🇬🇮",
|
|
2759
|
-
"🇬🇱","🇬🇲","🇬🇳","🇬🇵","🇬🇶","🇬🇷","🇬🇸","🇬🇹",
|
|
2760
|
-
"🇬🇺","🇬🇼","🇬🇾","🇭🇰","🇭🇲","🇭🇳","🇭🇷","🇭🇹",
|
|
2761
|
-
"🇭🇺","🇮🇨","🇮🇩","🇮🇪","🇮🇱","🇮🇲","🇮🇳","🇮🇴",
|
|
2762
|
-
"🇮🇶","🇮🇷","🇮🇸","🇮🇹","🇯🇪","🇯🇲","🇯🇴","🇯🇵",
|
|
2763
|
-
"🇰🇪","🇰🇬","🇰🇭","🇰🇮","🇰🇲","🇰🇳","🇰🇵","🇰🇷",
|
|
2764
|
-
"🇰🇼","🇰🇾","🇰🇿","🇱🇦","🇱🇧","🇱🇨","🇱🇮","🇱🇰",
|
|
2765
|
-
"🇱🇷","🇱🇸","🇱🇹","🇱🇺","🇱🇻","🇱🇾","🇲🇦","🇲🇨",
|
|
2766
|
-
"🇲🇩","🇲🇪","🇲🇫","🇲🇬","🇲🇭","🇲🇰","🇲🇱","🇲🇲",
|
|
2767
|
-
"🇲🇳","🇲🇴","🇲🇵","🇲🇶","🇲🇷","🇲🇸","🇲🇹","🇲🇺",
|
|
2768
|
-
"🇲🇻","🇲🇼","🇲🇽","🇲🇾","🇲🇿","🇳🇦","🇳🇨","🇳🇪",
|
|
2769
|
-
"🇳🇫","🇳🇬","🇳🇮","🇳🇱","🇳🇴","🇳🇵","🇳🇷","🇳🇺",
|
|
2770
|
-
"🇳🇿","🇴🇲","🇵🇦","🇵🇪","🇵🇫","🇵🇬","🇵🇭","🇵🇰",
|
|
2771
|
-
"🇵🇱","🇵🇲","🇵🇳","🇵🇷","🇵🇸","🇵🇹","🇵🇼","🇵🇾",
|
|
2772
|
-
"🇶🇦","🇷🇪","🇷🇴","🇷🇸","🇷🇺","🇷🇼","🇸🇦","🇸🇧",
|
|
2773
|
-
"🇸🇨","🇸🇩","🇸🇪","🇸🇬","🇸🇭","🇸🇮","🇸🇯","🇸🇰",
|
|
2774
|
-
"🇸🇱","🇸🇲","🇸🇳","🇸🇴","🇸🇷","🇸🇸","🇸🇹","🇸🇻",
|
|
2775
|
-
"🇸🇽","🇸🇾","🇸🇿","🇹🇦","🇹🇨","🇹🇩","🇹🇫","🇹🇬",
|
|
2776
|
-
"🇹🇭","🇹🇯","🇹🇰","🇹🇱","🇹🇲","🇹🇳","🇹🇴","🇹🇷",
|
|
2777
|
-
"🇹🇹","🇹🇻","🇹🇼","🇹🇿","🇺🇦","🇺🇬","🇺🇲","🇺🇳",
|
|
2778
|
-
"🇺🇸","🇺🇾","🇺🇿","🇻🇦","🇻🇨","🇻🇪","🇻🇬","🇻🇮",
|
|
2779
|
-
"🇻🇳","🇻🇺","🇼🇫","🇼🇸","🇽🇰","🇾🇪","🇾🇹","🇿🇦",
|
|
2780
|
-
"🇿🇲","🇿🇼",
|
|
2781
|
-
]},
|
|
2782
|
-
];
|
|
2783
|
-
|
|
2784
|
-
// --- Project Access Popover ---
|
|
2785
|
-
var projectAccessPopover = null;
|
|
2786
|
-
|
|
2787
|
-
function closeProjectAccessPopover() {
|
|
2788
|
-
if (projectAccessPopover) {
|
|
2789
|
-
projectAccessPopover.remove();
|
|
2790
|
-
projectAccessPopover = null;
|
|
2791
|
-
document.removeEventListener("click", closeAccessOnOutside);
|
|
2792
|
-
document.removeEventListener("keydown", closeAccessOnEscape);
|
|
2793
|
-
}
|
|
2794
|
-
}
|
|
2795
|
-
|
|
2796
|
-
function closeAccessOnOutside(e) {
|
|
2797
|
-
if (projectAccessPopover && !projectAccessPopover.contains(e.target)) closeProjectAccessPopover();
|
|
2798
|
-
}
|
|
2799
|
-
function closeAccessOnEscape(e) {
|
|
2800
|
-
if (e.key === "Escape") closeProjectAccessPopover();
|
|
2801
|
-
}
|
|
2802
|
-
|
|
2803
|
-
function showProjectAccessPopover(anchorEl, slug) {
|
|
2804
|
-
closeProjectAccessPopover();
|
|
2805
|
-
|
|
2806
|
-
var popover = document.createElement("div");
|
|
2807
|
-
popover.className = "project-access-popover";
|
|
2808
|
-
popover.innerHTML = '<div class="project-access-loading">Loading...</div>';
|
|
2809
|
-
popover.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
2810
|
-
document.body.appendChild(popover);
|
|
2811
|
-
projectAccessPopover = popover;
|
|
2812
|
-
|
|
2813
|
-
// Position near anchor
|
|
2814
|
-
requestAnimationFrame(function () {
|
|
2815
|
-
var rect = anchorEl.getBoundingClientRect();
|
|
2816
|
-
popover.style.position = "fixed";
|
|
2817
|
-
popover.style.left = (rect.right + 8) + "px";
|
|
2818
|
-
popover.style.top = rect.top + "px";
|
|
2819
|
-
popover.style.zIndex = "9999";
|
|
2820
|
-
var popRect = popover.getBoundingClientRect();
|
|
2821
|
-
if (popRect.right > window.innerWidth - 8) {
|
|
2822
|
-
popover.style.left = (rect.left - popRect.width - 8) + "px";
|
|
2823
|
-
}
|
|
2824
|
-
if (popRect.bottom > window.innerHeight - 8) {
|
|
2825
|
-
popover.style.top = (window.innerHeight - popRect.height - 8) + "px";
|
|
2826
|
-
}
|
|
2827
|
-
});
|
|
2828
|
-
|
|
2829
|
-
setTimeout(function () {
|
|
2830
|
-
document.addEventListener("click", closeAccessOnOutside);
|
|
2831
|
-
document.addEventListener("keydown", closeAccessOnEscape);
|
|
2832
|
-
}, 0);
|
|
2833
|
-
|
|
2834
|
-
// Fetch access info and user list in parallel
|
|
2835
|
-
Promise.all([
|
|
2836
|
-
fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/access").then(function (r) { return r.json(); }),
|
|
2837
|
-
fetch("/api/admin/users").then(function (r) { return r.json(); }),
|
|
2838
|
-
]).then(function (results) {
|
|
2839
|
-
var access = results[0];
|
|
2840
|
-
var usersData = results[1];
|
|
2841
|
-
if (access.error || usersData.error) {
|
|
2842
|
-
popover.innerHTML = '<div class="project-access-loading">Failed to load</div>';
|
|
2843
|
-
return;
|
|
2844
|
-
}
|
|
2845
|
-
renderAccessPopover(popover, slug, access, usersData.users || []);
|
|
2846
|
-
}).catch(function () {
|
|
2847
|
-
popover.innerHTML = '<div class="project-access-loading">Failed to load</div>';
|
|
2848
|
-
});
|
|
2849
|
-
}
|
|
2850
|
-
|
|
2851
|
-
function renderAccessPopover(popover, slug, access, allUsers) {
|
|
2852
|
-
var visibility = access.visibility || "public";
|
|
2853
|
-
var allowedUsers = access.allowedUsers || [];
|
|
2854
|
-
var ownerId = access.ownerId;
|
|
2855
|
-
|
|
2856
|
-
// Filter out the owner from the user list (owner always has access)
|
|
2857
|
-
var selectableUsers = allUsers.filter(function (u) { return u.id !== ownerId; });
|
|
2858
|
-
|
|
2859
|
-
var html = '';
|
|
2860
|
-
html += '<div class="project-access-header">';
|
|
2861
|
-
html += '<span class="project-access-title">Project Access</span>';
|
|
2862
|
-
html += '<button class="project-access-close">×</button>';
|
|
2863
|
-
html += '</div>';
|
|
2864
|
-
|
|
2865
|
-
// Visibility toggle
|
|
2866
|
-
html += '<div class="project-access-section">';
|
|
2867
|
-
html += '<label class="project-access-label">Visibility</label>';
|
|
2868
|
-
html += '<div class="project-access-vis-row">';
|
|
2869
|
-
html += '<button class="project-access-vis-btn' + (visibility === "private" ? ' active' : '') + '" data-vis="private">';
|
|
2870
|
-
html += iconHtml("lock") + ' Private';
|
|
2871
|
-
html += '</button>';
|
|
2872
|
-
html += '<button class="project-access-vis-btn' + (visibility === "public" ? ' active' : '') + '" data-vis="public">';
|
|
2873
|
-
html += iconHtml("globe") + ' Public';
|
|
2874
|
-
html += '</button>';
|
|
2875
|
-
html += '</div>';
|
|
2876
|
-
html += '</div>';
|
|
2877
|
-
|
|
2878
|
-
// Allowed users (only when private)
|
|
2879
|
-
html += '<div class="project-access-section project-access-users-section"' + (visibility !== "private" ? ' style="display:none"' : '') + '>';
|
|
2880
|
-
html += '<label class="project-access-label">Allowed Users</label>';
|
|
2881
|
-
html += '<div class="project-access-user-list">';
|
|
2882
|
-
for (var i = 0; i < selectableUsers.length; i++) {
|
|
2883
|
-
var u = selectableUsers[i];
|
|
2884
|
-
var checked = allowedUsers.indexOf(u.id) !== -1 ? " checked" : "";
|
|
2885
|
-
html += '<label class="project-access-user-item">';
|
|
2886
|
-
html += '<input type="checkbox" data-uid="' + u.id + '"' + checked + '>';
|
|
2887
|
-
html += '<span>' + escapeHtml(u.displayName || u.username || u.id) + '</span>';
|
|
2888
|
-
html += '</label>';
|
|
2889
|
-
}
|
|
2890
|
-
if (selectableUsers.length === 0) {
|
|
2891
|
-
html += '<div class="project-access-empty">No other users</div>';
|
|
2892
|
-
}
|
|
2893
|
-
html += '</div>';
|
|
2894
|
-
html += '</div>';
|
|
2895
|
-
|
|
2896
|
-
popover.innerHTML = html;
|
|
2897
|
-
refreshIcons();
|
|
2898
|
-
|
|
2899
|
-
// Close button
|
|
2900
|
-
popover.querySelector(".project-access-close").addEventListener("click", function () {
|
|
2901
|
-
closeProjectAccessPopover();
|
|
2902
|
-
});
|
|
2903
|
-
|
|
2904
|
-
// Visibility toggle
|
|
2905
|
-
popover.querySelectorAll(".project-access-vis-btn").forEach(function (btn) {
|
|
2906
|
-
btn.addEventListener("click", function () {
|
|
2907
|
-
var newVis = btn.dataset.vis;
|
|
2908
|
-
popover.querySelectorAll(".project-access-vis-btn").forEach(function (b) { b.classList.remove("active"); });
|
|
2909
|
-
btn.classList.add("active");
|
|
2910
|
-
var usersSection = popover.querySelector(".project-access-users-section");
|
|
2911
|
-
if (usersSection) usersSection.style.display = newVis === "private" ? "" : "none";
|
|
2912
|
-
fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/visibility", {
|
|
2913
|
-
method: "PUT",
|
|
2914
|
-
headers: { "Content-Type": "application/json" },
|
|
2915
|
-
body: JSON.stringify({ visibility: newVis }),
|
|
2916
|
-
});
|
|
2917
|
-
});
|
|
2918
|
-
});
|
|
2919
|
-
|
|
2920
|
-
// User checkboxes
|
|
2921
|
-
popover.querySelectorAll('.project-access-user-item input[type="checkbox"]').forEach(function (cb) {
|
|
2922
|
-
cb.addEventListener("change", function () {
|
|
2923
|
-
var selected = [];
|
|
2924
|
-
popover.querySelectorAll('.project-access-user-item input[type="checkbox"]:checked').forEach(function (c) {
|
|
2925
|
-
selected.push(c.dataset.uid);
|
|
2926
|
-
});
|
|
2927
|
-
fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/users", {
|
|
2928
|
-
method: "PUT",
|
|
2929
|
-
headers: { "Content-Type": "application/json" },
|
|
2930
|
-
body: JSON.stringify({ allowedUsers: selected }),
|
|
2931
|
-
});
|
|
2932
|
-
});
|
|
2933
|
-
});
|
|
2934
|
-
}
|
|
2935
|
-
|
|
2936
|
-
function closeProjectCtxMenu() {
|
|
2937
|
-
if (projectCtxMenu) {
|
|
2938
|
-
projectCtxMenu.remove();
|
|
2939
|
-
projectCtxMenu = null;
|
|
2940
|
-
}
|
|
2941
|
-
}
|
|
2942
|
-
|
|
2943
|
-
function showIconCtxMenu(anchorEl, slug, name) {
|
|
2944
|
-
closeProjectCtxMenu();
|
|
2945
|
-
closeUserCtxMenu();
|
|
2946
|
-
closeEmojiPicker();
|
|
2947
|
-
|
|
2948
|
-
var menu = document.createElement("div");
|
|
2949
|
-
menu.className = "project-ctx-menu";
|
|
2950
|
-
|
|
2951
|
-
var isWorktree = slug.indexOf("--") !== -1;
|
|
2952
|
-
|
|
2953
|
-
if (isWorktree) {
|
|
2954
|
-
// Worktree context menu: only "Remove Worktree"
|
|
2955
|
-
var removeWtItem = document.createElement("button");
|
|
2956
|
-
removeWtItem.className = "project-ctx-item project-ctx-delete";
|
|
2957
|
-
removeWtItem.innerHTML = iconHtml("trash-2") + " <span>Remove Worktree</span>";
|
|
2958
|
-
removeWtItem.addEventListener("click", function (e) {
|
|
2959
|
-
e.stopPropagation();
|
|
2960
|
-
closeProjectCtxMenu();
|
|
2961
|
-
if (ctx.ws && ctx.connected) {
|
|
2962
|
-
ctx.ws.send(JSON.stringify({ type: "remove_project_check", slug: slug, name: name || slug }));
|
|
2963
|
-
}
|
|
2964
|
-
});
|
|
2965
|
-
menu.appendChild(removeWtItem);
|
|
2966
|
-
} else {
|
|
2967
|
-
// Regular project context menu
|
|
2968
|
-
var iconItem = document.createElement("button");
|
|
2969
|
-
iconItem.className = "project-ctx-item";
|
|
2970
|
-
iconItem.innerHTML = iconHtml("smile") + " <span>Set Icon</span>";
|
|
2971
|
-
iconItem.addEventListener("click", function (e) {
|
|
2972
|
-
e.stopPropagation();
|
|
2973
|
-
closeProjectCtxMenu();
|
|
2974
|
-
showEmojiPicker(slug, anchorEl);
|
|
2975
|
-
});
|
|
2976
|
-
menu.appendChild(iconItem);
|
|
2977
|
-
|
|
2978
|
-
// --- Add Worktree ---
|
|
2979
|
-
var wtItem = document.createElement("button");
|
|
2980
|
-
wtItem.className = "project-ctx-item";
|
|
2981
|
-
wtItem.innerHTML = iconHtml("git-branch") + " <span>Add Worktree</span>";
|
|
2982
|
-
wtItem.addEventListener("click", function (e) {
|
|
2983
|
-
e.stopPropagation();
|
|
2984
|
-
closeProjectCtxMenu();
|
|
2985
|
-
showWorktreeModal(slug, name || slug);
|
|
2986
|
-
});
|
|
2987
|
-
menu.appendChild(wtItem);
|
|
2988
|
-
// Remove Project intentionally omitted from right-click.
|
|
2989
|
-
// Destructive actions only live in the chevron menu.
|
|
2990
|
-
}
|
|
2991
|
-
|
|
2992
|
-
document.body.appendChild(menu);
|
|
2993
|
-
projectCtxMenu = menu;
|
|
2994
|
-
refreshIcons();
|
|
2995
|
-
|
|
2996
|
-
requestAnimationFrame(function () {
|
|
2997
|
-
var rect = anchorEl.getBoundingClientRect();
|
|
2998
|
-
menu.style.position = "fixed";
|
|
2999
|
-
menu.style.left = (rect.right + 6) + "px";
|
|
3000
|
-
menu.style.top = rect.top + "px";
|
|
3001
|
-
var menuRect = menu.getBoundingClientRect();
|
|
3002
|
-
if (menuRect.right > window.innerWidth - 8) {
|
|
3003
|
-
menu.style.left = (rect.left - menuRect.width - 6) + "px";
|
|
3004
|
-
}
|
|
3005
|
-
if (menuRect.bottom > window.innerHeight - 8) {
|
|
3006
|
-
menu.style.top = (window.innerHeight - menuRect.height - 8) + "px";
|
|
3007
|
-
}
|
|
3008
|
-
});
|
|
3009
|
-
}
|
|
3010
|
-
|
|
3011
|
-
function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
|
|
3012
|
-
closeProjectCtxMenu();
|
|
3013
|
-
closeUserCtxMenu();
|
|
3014
|
-
closeEmojiPicker();
|
|
3015
|
-
|
|
3016
|
-
var menu = document.createElement("div");
|
|
3017
|
-
menu.className = "project-ctx-menu";
|
|
3018
|
-
|
|
3019
|
-
// --- Set Icon ---
|
|
3020
|
-
var iconItem = document.createElement("button");
|
|
3021
|
-
iconItem.className = "project-ctx-item";
|
|
3022
|
-
iconItem.innerHTML = iconHtml("smile") + " <span>Set Icon</span>";
|
|
3023
|
-
iconItem.addEventListener("click", function (e) {
|
|
3024
|
-
e.stopPropagation();
|
|
3025
|
-
closeProjectCtxMenu();
|
|
3026
|
-
showEmojiPicker(slug, anchorEl);
|
|
3027
|
-
});
|
|
3028
|
-
menu.appendChild(iconItem);
|
|
3029
|
-
|
|
3030
|
-
// --- Project Settings ---
|
|
3031
|
-
if (!ctx.permissions || ctx.permissions.projectSettings !== false) {
|
|
3032
|
-
var settingsItem = document.createElement("button");
|
|
3033
|
-
settingsItem.className = "project-ctx-item";
|
|
3034
|
-
settingsItem.innerHTML = iconHtml("settings") + " <span>Project Settings</span>";
|
|
3035
|
-
settingsItem.addEventListener("click", function (e) {
|
|
3036
|
-
e.stopPropagation();
|
|
3037
|
-
closeProjectCtxMenu();
|
|
3038
|
-
openProjectSettings(slug, { slug: slug, name: name, icon: icon, projectOwnerId: ctx.projectOwnerId });
|
|
3039
|
-
});
|
|
3040
|
-
menu.appendChild(settingsItem);
|
|
3041
|
-
}
|
|
3042
|
-
|
|
3043
|
-
// --- Separator: collaboration ---
|
|
3044
|
-
var sep1 = document.createElement("div");
|
|
3045
|
-
sep1.className = "project-ctx-separator";
|
|
3046
|
-
menu.appendChild(sep1);
|
|
3047
|
-
|
|
3048
|
-
// --- Share ---
|
|
3049
|
-
var shareItem = document.createElement("button");
|
|
3050
|
-
shareItem.className = "project-ctx-item";
|
|
3051
|
-
shareItem.innerHTML = iconHtml("share") + " <span>Share</span>";
|
|
3052
|
-
shareItem.addEventListener("click", function (e) {
|
|
3053
|
-
e.stopPropagation();
|
|
3054
|
-
closeProjectCtxMenu();
|
|
3055
|
-
triggerShare();
|
|
3056
|
-
});
|
|
3057
|
-
menu.appendChild(shareItem);
|
|
3058
|
-
|
|
3059
|
-
// --- Manage Access (owner or admin, multi-user only) ---
|
|
3060
|
-
if (ctx.multiUser && slug.indexOf("--") === -1) {
|
|
3061
|
-
var isProjectOwner = ctx.myUserId && ctx.projectOwnerId && ctx.myUserId === ctx.projectOwnerId;
|
|
3062
|
-
var isAdmin = ctx.permissions && ctx.permissions.projectSettings !== false;
|
|
3063
|
-
if (isProjectOwner || isAdmin) {
|
|
3064
|
-
var accessItem = document.createElement("button");
|
|
3065
|
-
accessItem.className = "project-ctx-item";
|
|
3066
|
-
accessItem.innerHTML = iconHtml("users") + " <span>Manage Access</span>";
|
|
3067
|
-
accessItem.addEventListener("click", function (e) {
|
|
3068
|
-
e.stopPropagation();
|
|
3069
|
-
closeProjectCtxMenu();
|
|
3070
|
-
showProjectAccessPopover(anchorEl, slug);
|
|
3071
|
-
});
|
|
3072
|
-
menu.appendChild(accessItem);
|
|
3073
|
-
}
|
|
3074
|
-
}
|
|
3075
|
-
|
|
3076
|
-
// --- Separator: development ---
|
|
3077
|
-
var sep2 = document.createElement("div");
|
|
3078
|
-
sep2.className = "project-ctx-separator";
|
|
3079
|
-
menu.appendChild(sep2);
|
|
3080
|
-
|
|
3081
|
-
// --- Add Worktree ---
|
|
3082
|
-
var wtItem = document.createElement("button");
|
|
3083
|
-
wtItem.className = "project-ctx-item";
|
|
3084
|
-
wtItem.innerHTML = iconHtml("git-branch") + " <span>Add Worktree</span>";
|
|
3085
|
-
wtItem.addEventListener("click", function (e) {
|
|
3086
|
-
e.stopPropagation();
|
|
3087
|
-
closeProjectCtxMenu();
|
|
3088
|
-
showWorktreeModal(slug, name || slug);
|
|
3089
|
-
});
|
|
3090
|
-
menu.appendChild(wtItem);
|
|
3091
|
-
|
|
3092
|
-
if (!ctx.permissions || ctx.permissions.deleteProject !== false) {
|
|
3093
|
-
// --- Separator: danger zone ---
|
|
3094
|
-
var sep3 = document.createElement("div");
|
|
3095
|
-
sep3.className = "project-ctx-separator";
|
|
3096
|
-
menu.appendChild(sep3);
|
|
3097
|
-
|
|
3098
|
-
// --- Remove Project ---
|
|
3099
|
-
var deleteItem = document.createElement("button");
|
|
3100
|
-
deleteItem.className = "project-ctx-item project-ctx-delete";
|
|
3101
|
-
deleteItem.innerHTML = iconHtml("trash-2") + " <span>Remove Project</span>";
|
|
3102
|
-
deleteItem.addEventListener("click", function (e) {
|
|
3103
|
-
e.stopPropagation();
|
|
3104
|
-
closeProjectCtxMenu();
|
|
3105
|
-
if (ctx.ws && ctx.connected) {
|
|
3106
|
-
ctx.ws.send(JSON.stringify({ type: "remove_project_check", slug: slug, name: name }));
|
|
3107
|
-
}
|
|
3108
|
-
});
|
|
3109
|
-
menu.appendChild(deleteItem);
|
|
3110
|
-
}
|
|
3111
|
-
|
|
3112
|
-
document.body.appendChild(menu);
|
|
3113
|
-
projectCtxMenu = menu;
|
|
3114
|
-
refreshIcons();
|
|
3115
|
-
|
|
3116
|
-
// Position
|
|
3117
|
-
requestAnimationFrame(function () {
|
|
3118
|
-
var rect = anchorEl.getBoundingClientRect();
|
|
3119
|
-
menu.style.position = "fixed";
|
|
3120
|
-
if (position === "below") {
|
|
3121
|
-
// Chevron dropdown: directly below the anchor
|
|
3122
|
-
menu.style.left = rect.left + "px";
|
|
3123
|
-
menu.style.top = (rect.bottom + 4) + "px";
|
|
3124
|
-
} else {
|
|
3125
|
-
// Icon strip right-click: to the right of the anchor
|
|
3126
|
-
menu.style.left = (rect.right + 6) + "px";
|
|
3127
|
-
menu.style.top = rect.top + "px";
|
|
3128
|
-
}
|
|
3129
|
-
var menuRect = menu.getBoundingClientRect();
|
|
3130
|
-
if (menuRect.right > window.innerWidth - 8) {
|
|
3131
|
-
menu.style.left = (rect.left - menuRect.width - 6) + "px";
|
|
3132
|
-
}
|
|
3133
|
-
if (menuRect.bottom > window.innerHeight - 8) {
|
|
3134
|
-
menu.style.top = (window.innerHeight - menuRect.height - 8) + "px";
|
|
3135
|
-
}
|
|
3136
|
-
});
|
|
3137
|
-
}
|
|
3138
|
-
|
|
3139
|
-
// --- Emoji picker ---
|
|
3140
|
-
var emojiPickerEl = null;
|
|
3141
|
-
|
|
3142
|
-
function closeEmojiPicker() {
|
|
3143
|
-
if (emojiPickerEl) {
|
|
3144
|
-
emojiPickerEl.remove();
|
|
3145
|
-
emojiPickerEl = null;
|
|
3146
|
-
}
|
|
3147
|
-
}
|
|
3148
|
-
|
|
3149
|
-
function showEmojiPicker(slug, anchorEl) {
|
|
3150
|
-
closeEmojiPicker();
|
|
3151
|
-
|
|
3152
|
-
var picker = document.createElement("div");
|
|
3153
|
-
picker.className = "emoji-picker";
|
|
3154
|
-
picker.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
3155
|
-
|
|
3156
|
-
// --- Header ---
|
|
3157
|
-
var header = document.createElement("div");
|
|
3158
|
-
header.className = "emoji-picker-header";
|
|
3159
|
-
header.textContent = "Choose Icon";
|
|
3160
|
-
|
|
3161
|
-
var removeBtn = document.createElement("button");
|
|
3162
|
-
removeBtn.className = "emoji-picker-remove";
|
|
3163
|
-
removeBtn.textContent = "Remove";
|
|
3164
|
-
removeBtn.addEventListener("click", function (e) {
|
|
3165
|
-
e.stopPropagation();
|
|
3166
|
-
closeEmojiPicker();
|
|
3167
|
-
if (ctx.ws && ctx.connected) {
|
|
3168
|
-
ctx.ws.send(JSON.stringify({ type: "set_project_icon", slug: slug, icon: null }));
|
|
3169
|
-
}
|
|
3170
|
-
});
|
|
3171
|
-
header.appendChild(removeBtn);
|
|
3172
|
-
picker.appendChild(header);
|
|
3173
|
-
|
|
3174
|
-
// --- Category tabs ---
|
|
3175
|
-
var tabBar = document.createElement("div");
|
|
3176
|
-
tabBar.className = "emoji-picker-tabs";
|
|
3177
|
-
var tabBtns = [];
|
|
3178
|
-
|
|
3179
|
-
for (var t = 0; t < EMOJI_CATEGORIES.length; t++) {
|
|
3180
|
-
(function (cat, idx) {
|
|
3181
|
-
var tab = document.createElement("button");
|
|
3182
|
-
tab.className = "emoji-picker-tab" + (idx === 0 ? " active" : "");
|
|
3183
|
-
tab.textContent = cat.icon;
|
|
3184
|
-
tab.title = cat.label;
|
|
3185
|
-
tab.addEventListener("click", function (e) {
|
|
3186
|
-
e.stopPropagation();
|
|
3187
|
-
switchCategory(idx);
|
|
3188
|
-
});
|
|
3189
|
-
tabBar.appendChild(tab);
|
|
3190
|
-
tabBtns.push(tab);
|
|
3191
|
-
})(EMOJI_CATEGORIES[t], t);
|
|
3192
|
-
}
|
|
3193
|
-
parseEmojis(tabBar);
|
|
3194
|
-
picker.appendChild(tabBar);
|
|
3195
|
-
|
|
3196
|
-
// --- Scrollable grid area ---
|
|
3197
|
-
var scrollArea = document.createElement("div");
|
|
3198
|
-
scrollArea.className = "emoji-picker-scroll";
|
|
3199
|
-
|
|
3200
|
-
var grid = document.createElement("div");
|
|
3201
|
-
grid.className = "emoji-picker-grid";
|
|
3202
|
-
scrollArea.appendChild(grid);
|
|
3203
|
-
picker.appendChild(scrollArea);
|
|
3204
|
-
|
|
3205
|
-
function buildGrid(emojis) {
|
|
3206
|
-
grid.innerHTML = "";
|
|
3207
|
-
for (var i = 0; i < emojis.length; i++) {
|
|
3208
|
-
(function (emoji) {
|
|
3209
|
-
var btn = document.createElement("button");
|
|
3210
|
-
btn.className = "emoji-picker-item";
|
|
3211
|
-
btn.textContent = emoji;
|
|
3212
|
-
btn.addEventListener("click", function (e) {
|
|
3213
|
-
e.stopPropagation();
|
|
3214
|
-
closeEmojiPicker();
|
|
3215
|
-
if (ctx.ws && ctx.connected) {
|
|
3216
|
-
ctx.ws.send(JSON.stringify({ type: "set_project_icon", slug: slug, icon: emoji }));
|
|
3217
|
-
}
|
|
3218
|
-
});
|
|
3219
|
-
grid.appendChild(btn);
|
|
3220
|
-
})(emojis[i]);
|
|
3221
|
-
}
|
|
3222
|
-
parseEmojis(grid);
|
|
3223
|
-
scrollArea.scrollTop = 0;
|
|
3224
|
-
}
|
|
3225
|
-
|
|
3226
|
-
function switchCategory(idx) {
|
|
3227
|
-
for (var j = 0; j < tabBtns.length; j++) {
|
|
3228
|
-
tabBtns[j].classList.toggle("active", j === idx);
|
|
3229
|
-
}
|
|
3230
|
-
buildGrid(EMOJI_CATEGORIES[idx].emojis);
|
|
3231
|
-
}
|
|
3232
|
-
|
|
3233
|
-
// Start with first category (Frequent)
|
|
3234
|
-
buildGrid(EMOJI_CATEGORIES[0].emojis);
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
document.body.appendChild(picker);
|
|
3239
|
-
emojiPickerEl = picker;
|
|
3240
|
-
|
|
3241
|
-
// Position
|
|
3242
|
-
requestAnimationFrame(function () {
|
|
3243
|
-
var rect = anchorEl.getBoundingClientRect();
|
|
3244
|
-
picker.style.left = (rect.right + 6) + "px";
|
|
3245
|
-
picker.style.top = rect.top + "px";
|
|
3246
|
-
var pRect = picker.getBoundingClientRect();
|
|
3247
|
-
if (pRect.right > window.innerWidth - 8) {
|
|
3248
|
-
picker.style.left = (rect.left - pRect.width - 6) + "px";
|
|
3249
|
-
}
|
|
3250
|
-
if (pRect.bottom > window.innerHeight - 8) {
|
|
3251
|
-
picker.style.top = (window.innerHeight - pRect.height - 8) + "px";
|
|
3252
|
-
}
|
|
3253
|
-
});
|
|
3254
|
-
}
|
|
3255
|
-
|
|
3256
|
-
// --- Rename prompt ---
|
|
3257
|
-
function showProjectRename(slug, currentName) {
|
|
3258
|
-
var nameEl = document.getElementById("title-bar-project-name");
|
|
3259
|
-
if (!nameEl) return;
|
|
3260
|
-
|
|
3261
|
-
var input = document.createElement("input");
|
|
3262
|
-
input.type = "text";
|
|
3263
|
-
input.className = "project-rename-input";
|
|
3264
|
-
input.value = currentName || "";
|
|
3265
|
-
|
|
3266
|
-
var originalText = nameEl.textContent;
|
|
3267
|
-
nameEl.textContent = "";
|
|
3268
|
-
nameEl.appendChild(input);
|
|
3269
|
-
input.focus();
|
|
3270
|
-
input.select();
|
|
3271
|
-
|
|
3272
|
-
var committed = false;
|
|
3273
|
-
|
|
3274
|
-
function commitRename() {
|
|
3275
|
-
if (committed) return;
|
|
3276
|
-
committed = true;
|
|
3277
|
-
var newName = input.value.trim();
|
|
3278
|
-
if (newName && newName !== currentName && ctx.ws && ctx.connected) {
|
|
3279
|
-
ctx.ws.send(JSON.stringify({ type: "set_project_title", slug: slug, title: newName }));
|
|
3280
|
-
nameEl.textContent = newName;
|
|
3281
|
-
} else {
|
|
3282
|
-
nameEl.textContent = originalText;
|
|
3283
|
-
}
|
|
3284
|
-
}
|
|
3285
|
-
|
|
3286
|
-
input.addEventListener("keydown", function (e) {
|
|
3287
|
-
e.stopPropagation();
|
|
3288
|
-
if (e.key === "Enter") { e.preventDefault(); commitRename(); }
|
|
3289
|
-
if (e.key === "Escape") { e.preventDefault(); committed = true; nameEl.textContent = originalText; }
|
|
3290
|
-
});
|
|
3291
|
-
input.addEventListener("blur", commitRename);
|
|
3292
|
-
input.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
3293
|
-
}
|
|
3294
|
-
|
|
3295
|
-
// Click outside to close
|
|
3296
|
-
document.addEventListener("click", function () {
|
|
3297
|
-
closeProjectCtxMenu();
|
|
3298
|
-
closeEmojiPicker();
|
|
3299
|
-
});
|
|
3300
|
-
|
|
3301
|
-
// --- Drag-and-drop state ---
|
|
3302
|
-
var draggedSlug = null;
|
|
3303
|
-
var draggedEl = null;
|
|
3304
|
-
|
|
3305
|
-
function showTrashZone() {
|
|
3306
|
-
var addBtn = document.getElementById("icon-strip-add");
|
|
3307
|
-
if (!addBtn) return;
|
|
3308
|
-
addBtn.style.display = "none";
|
|
3309
|
-
|
|
3310
|
-
var existing = document.getElementById("icon-strip-trash");
|
|
3311
|
-
if (existing) existing.remove();
|
|
3312
|
-
|
|
3313
|
-
var trash = document.createElement("div");
|
|
3314
|
-
trash.id = "icon-strip-trash";
|
|
3315
|
-
trash.className = "icon-strip-trash";
|
|
3316
|
-
trash.innerHTML = iconHtml("trash-2");
|
|
3317
|
-
addBtn.parentNode.insertBefore(trash, addBtn.nextSibling);
|
|
3318
|
-
refreshIcons();
|
|
3319
|
-
|
|
3320
|
-
// Tooltip
|
|
3321
|
-
trash.addEventListener("mouseenter", function () { showIconTooltip(trash, "Remove project"); });
|
|
3322
|
-
trash.addEventListener("mouseleave", hideIconTooltip);
|
|
3323
|
-
|
|
3324
|
-
trash.addEventListener("dragover", function (e) {
|
|
3325
|
-
e.preventDefault();
|
|
3326
|
-
e.dataTransfer.dropEffect = "move";
|
|
3327
|
-
trash.classList.add("drag-hover");
|
|
3328
|
-
});
|
|
3329
|
-
trash.addEventListener("dragleave", function () {
|
|
3330
|
-
trash.classList.remove("drag-hover");
|
|
3331
|
-
});
|
|
3332
|
-
trash.addEventListener("drop", function (e) {
|
|
3333
|
-
e.preventDefault();
|
|
3334
|
-
trash.classList.remove("drag-hover");
|
|
3335
|
-
var slug = e.dataTransfer.getData("text/plain");
|
|
3336
|
-
if (slug && ctx.ws && ctx.connected) {
|
|
3337
|
-
ctx.ws.send(JSON.stringify({ type: "remove_project_check", slug: slug }));
|
|
3338
|
-
}
|
|
3339
|
-
});
|
|
3340
|
-
}
|
|
3341
|
-
|
|
3342
|
-
function hideTrashZone() {
|
|
3343
|
-
var trash = document.getElementById("icon-strip-trash");
|
|
3344
|
-
if (trash) trash.remove();
|
|
3345
|
-
var addBtn = document.getElementById("icon-strip-add");
|
|
3346
|
-
if (addBtn) addBtn.style.display = "";
|
|
3347
|
-
}
|
|
3348
|
-
|
|
3349
|
-
export function spawnDustParticles(cx, cy) {
|
|
3350
|
-
var colors = ["#8B7355", "#A0522D", "#D2B48C", "#C4A882", "#9E9E9E", "#B8860B", "#BC8F8F"];
|
|
3351
|
-
var count = 24;
|
|
3352
|
-
var container = document.createElement("div");
|
|
3353
|
-
container.style.position = "fixed";
|
|
3354
|
-
container.style.top = "0";
|
|
3355
|
-
container.style.left = "0";
|
|
3356
|
-
container.style.width = "0";
|
|
3357
|
-
container.style.height = "0";
|
|
3358
|
-
container.style.pointerEvents = "none";
|
|
3359
|
-
container.style.zIndex = "10000";
|
|
3360
|
-
document.body.appendChild(container);
|
|
3361
|
-
|
|
3362
|
-
for (var i = 0; i < count; i++) {
|
|
3363
|
-
var dot = document.createElement("div");
|
|
3364
|
-
dot.className = "dust-particle";
|
|
3365
|
-
var size = 3 + Math.random() * 5;
|
|
3366
|
-
var angle = Math.random() * Math.PI * 2;
|
|
3367
|
-
var dist = 30 + Math.random() * 60;
|
|
3368
|
-
var dx = Math.cos(angle) * dist;
|
|
3369
|
-
var dy = Math.sin(angle) * dist - 20; // bias upward
|
|
3370
|
-
var duration = 600 + Math.random() * 500;
|
|
3371
|
-
|
|
3372
|
-
dot.style.width = size + "px";
|
|
3373
|
-
dot.style.height = size + "px";
|
|
3374
|
-
dot.style.left = cx + "px";
|
|
3375
|
-
dot.style.top = cy + "px";
|
|
3376
|
-
dot.style.background = colors[Math.floor(Math.random() * colors.length)];
|
|
3377
|
-
dot.style.setProperty("--dust-x", dx + "px");
|
|
3378
|
-
dot.style.setProperty("--dust-y", dy + "px");
|
|
3379
|
-
dot.style.setProperty("--dust-duration", duration + "ms");
|
|
3380
|
-
|
|
3381
|
-
container.appendChild(dot);
|
|
3382
|
-
}
|
|
3383
|
-
|
|
3384
|
-
setTimeout(function () { container.remove(); }, 1200);
|
|
3385
|
-
}
|
|
3386
|
-
|
|
3387
|
-
function clearDragIndicators() {
|
|
3388
|
-
var items = document.querySelectorAll(".icon-strip-item.drag-over-above, .icon-strip-item.drag-over-below");
|
|
3389
|
-
for (var i = 0; i < items.length; i++) {
|
|
3390
|
-
items[i].classList.remove("drag-over-above", "drag-over-below");
|
|
3391
|
-
}
|
|
3392
|
-
}
|
|
3393
|
-
|
|
3394
|
-
function setupDragHandlers(el, slug) {
|
|
3395
|
-
el.setAttribute("draggable", "true");
|
|
3396
|
-
|
|
3397
|
-
el.addEventListener("dragstart", function (e) {
|
|
3398
|
-
draggedSlug = slug;
|
|
3399
|
-
draggedEl = el;
|
|
3400
|
-
e.dataTransfer.effectAllowed = "move";
|
|
3401
|
-
e.dataTransfer.setData("text/plain", slug);
|
|
3402
|
-
|
|
3403
|
-
// Custom drag image — just the 38px rounded icon, no pill/status
|
|
3404
|
-
var ghost = document.createElement("div");
|
|
3405
|
-
ghost.textContent = el.textContent.trim().split("\n")[0]; // abbreviation only
|
|
3406
|
-
ghost.style.cssText = "position:fixed;left:-200px;top:-200px;width:38px;height:38px;border-radius:12px;" +
|
|
3407
|
-
"background:var(--accent);color:#fff;display:flex;align-items:center;justify-content:center;" +
|
|
3408
|
-
"font-size:15px;font-weight:600;pointer-events:none;z-index:-1;";
|
|
3409
|
-
document.body.appendChild(ghost);
|
|
3410
|
-
e.dataTransfer.setDragImage(ghost, 19, 19);
|
|
3411
|
-
setTimeout(function () { ghost.remove(); }, 0);
|
|
3412
|
-
|
|
3413
|
-
setTimeout(function () { el.classList.add("dragging"); }, 0);
|
|
3414
|
-
hideIconTooltip();
|
|
3415
|
-
showTrashZone();
|
|
3416
|
-
});
|
|
3417
|
-
|
|
3418
|
-
el.addEventListener("dragover", function (e) {
|
|
3419
|
-
e.preventDefault();
|
|
3420
|
-
if (!draggedSlug || draggedSlug === slug) return;
|
|
3421
|
-
e.dataTransfer.dropEffect = "move";
|
|
3422
|
-
|
|
3423
|
-
clearDragIndicators();
|
|
3424
|
-
var rect = el.getBoundingClientRect();
|
|
3425
|
-
var midY = rect.top + rect.height / 2;
|
|
3426
|
-
if (e.clientY < midY) {
|
|
3427
|
-
el.classList.add("drag-over-above");
|
|
3428
|
-
} else {
|
|
3429
|
-
el.classList.add("drag-over-below");
|
|
3430
|
-
}
|
|
3431
|
-
});
|
|
3432
|
-
|
|
3433
|
-
el.addEventListener("dragleave", function () {
|
|
3434
|
-
el.classList.remove("drag-over-above", "drag-over-below");
|
|
3435
|
-
});
|
|
3436
|
-
|
|
3437
|
-
el.addEventListener("drop", function (e) {
|
|
3438
|
-
e.preventDefault();
|
|
3439
|
-
clearDragIndicators();
|
|
3440
|
-
if (!draggedSlug || draggedSlug === slug) return;
|
|
3441
|
-
|
|
3442
|
-
var rect = el.getBoundingClientRect();
|
|
3443
|
-
var midY = rect.top + rect.height / 2;
|
|
3444
|
-
var insertBefore = e.clientY < midY;
|
|
3445
|
-
|
|
3446
|
-
// Build new slug order
|
|
3447
|
-
var container = document.getElementById("icon-strip-projects");
|
|
3448
|
-
var items = container.querySelectorAll(".icon-strip-item");
|
|
3449
|
-
var slugs = [];
|
|
3450
|
-
for (var i = 0; i < items.length; i++) {
|
|
3451
|
-
if (items[i].dataset.slug !== draggedSlug) {
|
|
3452
|
-
slugs.push(items[i].dataset.slug);
|
|
3453
|
-
}
|
|
3454
|
-
}
|
|
3455
|
-
// Insert dragged slug at correct position
|
|
3456
|
-
var targetIdx = slugs.indexOf(slug);
|
|
3457
|
-
if (!insertBefore) targetIdx++;
|
|
3458
|
-
slugs.splice(targetIdx, 0, draggedSlug);
|
|
3459
|
-
|
|
3460
|
-
// Send reorder to server
|
|
3461
|
-
if (ctx.ws && ctx.connected) {
|
|
3462
|
-
ctx.ws.send(JSON.stringify({ type: "reorder_projects", slugs: slugs }));
|
|
3463
|
-
}
|
|
3464
|
-
});
|
|
3465
|
-
|
|
3466
|
-
el.addEventListener("dragend", function () {
|
|
3467
|
-
el.classList.remove("dragging");
|
|
3468
|
-
clearDragIndicators();
|
|
3469
|
-
draggedSlug = null;
|
|
3470
|
-
draggedEl = null;
|
|
3471
|
-
hideTrashZone();
|
|
3472
|
-
});
|
|
3473
|
-
}
|
|
3474
|
-
|
|
3475
|
-
export function renderSidebarPresence(onlineUsers) {
|
|
3476
|
-
var container = document.getElementById("sidebar-presence");
|
|
3477
|
-
if (!container) return;
|
|
3478
|
-
container.innerHTML = "";
|
|
3479
|
-
if (!onlineUsers || onlineUsers.length < 2) return;
|
|
3480
|
-
var maxShow = 4;
|
|
3481
|
-
for (var i = 0; i < Math.min(onlineUsers.length, maxShow); i++) {
|
|
3482
|
-
var ou = onlineUsers[i];
|
|
3483
|
-
var img = document.createElement("img");
|
|
3484
|
-
img.className = "sidebar-presence-avatar";
|
|
3485
|
-
img.src = presenceAvatarUrl(ou);
|
|
3486
|
-
img.alt = ou.displayName;
|
|
3487
|
-
img.dataset.tip = ou.displayName + " (@" + ou.username + ")";
|
|
3488
|
-
container.appendChild(img);
|
|
3489
|
-
}
|
|
3490
|
-
if (onlineUsers.length > maxShow) {
|
|
3491
|
-
var more = document.createElement("span");
|
|
3492
|
-
more.className = "sidebar-presence-more";
|
|
3493
|
-
more.textContent = "+" + (onlineUsers.length - maxShow);
|
|
3494
|
-
container.appendChild(more);
|
|
3495
|
-
}
|
|
3496
|
-
}
|
|
3497
|
-
|
|
3498
|
-
// --- Worktree folder collapse state (persisted in localStorage) ---
|
|
3499
|
-
var wtCollapsed = {};
|
|
3500
|
-
try {
|
|
3501
|
-
wtCollapsed = JSON.parse(localStorage.getItem("clay-wt-collapsed") || "{}");
|
|
3502
|
-
} catch (e) {}
|
|
3503
|
-
function setWtCollapsed(slug, collapsed) {
|
|
3504
|
-
wtCollapsed[slug] = collapsed;
|
|
3505
|
-
try { localStorage.setItem("clay-wt-collapsed", JSON.stringify(wtCollapsed)); } catch (e) {}
|
|
3506
|
-
}
|
|
3507
|
-
|
|
3508
|
-
// Group projects by parent/worktree relationship
|
|
3509
|
-
function groupProjects(projects) {
|
|
3510
|
-
var parents = [];
|
|
3511
|
-
var wtByParent = {};
|
|
3512
|
-
for (var i = 0; i < projects.length; i++) {
|
|
3513
|
-
var p = projects[i];
|
|
3514
|
-
if (p.isWorktree && p.parentSlug) {
|
|
3515
|
-
if (!wtByParent[p.parentSlug]) wtByParent[p.parentSlug] = [];
|
|
3516
|
-
wtByParent[p.parentSlug].push(p);
|
|
3517
|
-
} else {
|
|
3518
|
-
parents.push(p);
|
|
3519
|
-
}
|
|
3520
|
-
}
|
|
3521
|
-
return { parents: parents, wtByParent: wtByParent };
|
|
3522
|
-
}
|
|
3523
|
-
|
|
3524
|
-
// Create a standard icon-strip item element (shared between parent and worktree rendering)
|
|
3525
|
-
function createIconItem(p, currentSlug) {
|
|
3526
|
-
var el = document.createElement("a");
|
|
3527
|
-
var isActive = p.slug === currentSlug && !currentDmUserId;
|
|
3528
|
-
el.className = "icon-strip-item" + (isActive ? " active" : "");
|
|
3529
|
-
el.href = "/p/" + p.slug + "/";
|
|
3530
|
-
el.dataset.slug = p.slug;
|
|
3531
|
-
|
|
3532
|
-
if (p.icon) {
|
|
3533
|
-
var emojiSpan = document.createElement("span");
|
|
3534
|
-
emojiSpan.className = "project-emoji";
|
|
3535
|
-
emojiSpan.textContent = p.icon;
|
|
3536
|
-
parseEmojis(emojiSpan);
|
|
3537
|
-
el.appendChild(emojiSpan);
|
|
3538
|
-
} else {
|
|
3539
|
-
el.appendChild(document.createTextNode(getProjectAbbrev(p.name)));
|
|
3540
|
-
}
|
|
3541
|
-
|
|
3542
|
-
var pill = document.createElement("span");
|
|
3543
|
-
pill.className = "icon-strip-pill";
|
|
3544
|
-
el.appendChild(pill);
|
|
3545
|
-
|
|
3546
|
-
var statusDot = document.createElement("span");
|
|
3547
|
-
statusDot.className = "icon-strip-status";
|
|
3548
|
-
if (p.isProcessing) statusDot.classList.add("processing");
|
|
3549
|
-
el.appendChild(statusDot);
|
|
3550
|
-
|
|
3551
|
-
var projectBadge = document.createElement("span");
|
|
3552
|
-
projectBadge.className = "icon-strip-project-badge";
|
|
3553
|
-
if (p.unread > 0 && !isActive) {
|
|
3554
|
-
projectBadge.textContent = p.unread > 99 ? "99+" : String(p.unread);
|
|
3555
|
-
projectBadge.classList.add("has-unread");
|
|
3556
|
-
}
|
|
3557
|
-
el.appendChild(projectBadge);
|
|
3558
|
-
|
|
3559
|
-
// Pending permission shake for non-active projects
|
|
3560
|
-
if (p.pendingPermissions > 0 && !isActive) {
|
|
3561
|
-
el.classList.add("has-pending-perm");
|
|
3562
|
-
}
|
|
3563
|
-
|
|
3564
|
-
(function (name, elem) {
|
|
3565
|
-
elem.addEventListener("mouseenter", function () { showIconTooltip(elem, name); });
|
|
3566
|
-
elem.addEventListener("mouseleave", hideIconTooltip);
|
|
3567
|
-
})(p.name, el);
|
|
3568
|
-
|
|
3569
|
-
(function (slug) {
|
|
3570
|
-
el.addEventListener("click", function (e) {
|
|
3571
|
-
e.preventDefault();
|
|
3572
|
-
if (ctx.switchProject) ctx.switchProject(slug);
|
|
3573
|
-
});
|
|
3574
|
-
})(p.slug);
|
|
3575
|
-
|
|
3576
|
-
return el;
|
|
3577
|
-
}
|
|
3578
|
-
|
|
3579
|
-
// Worktree creation modal
|
|
3580
|
-
function showWorktreeModal(parentSlug, parentName) {
|
|
3581
|
-
// Remove existing modal if any
|
|
3582
|
-
var existing = document.getElementById("wt-modal-container");
|
|
3583
|
-
if (existing) existing.remove();
|
|
3584
|
-
|
|
3585
|
-
var container = document.createElement("div");
|
|
3586
|
-
container.id = "wt-modal-container";
|
|
3587
|
-
|
|
3588
|
-
var overlay = document.createElement("div");
|
|
3589
|
-
overlay.className = "wt-modal-overlay";
|
|
3590
|
-
container.appendChild(overlay);
|
|
3591
|
-
|
|
3592
|
-
var modal = document.createElement("div");
|
|
3593
|
-
modal.className = "wt-modal";
|
|
3594
|
-
|
|
3595
|
-
var title = document.createElement("div");
|
|
3596
|
-
title.className = "wt-modal-title";
|
|
3597
|
-
title.textContent = "Add Worktree \u2014 " + parentName;
|
|
3598
|
-
modal.appendChild(title);
|
|
3599
|
-
|
|
3600
|
-
var branchLabel = document.createElement("label");
|
|
3601
|
-
branchLabel.className = "wt-modal-label";
|
|
3602
|
-
branchLabel.textContent = "Branch name";
|
|
3603
|
-
modal.appendChild(branchLabel);
|
|
3604
|
-
|
|
3605
|
-
var branchInput = document.createElement("input");
|
|
3606
|
-
branchInput.type = "text";
|
|
3607
|
-
branchInput.className = "wt-modal-input";
|
|
3608
|
-
branchInput.placeholder = "feat/my-feature";
|
|
3609
|
-
branchInput.autocomplete = "off";
|
|
3610
|
-
branchInput.spellcheck = false;
|
|
3611
|
-
modal.appendChild(branchInput);
|
|
3612
|
-
|
|
3613
|
-
var baseLabel = document.createElement("label");
|
|
3614
|
-
baseLabel.className = "wt-modal-label";
|
|
3615
|
-
baseLabel.textContent = "Base branch";
|
|
3616
|
-
modal.appendChild(baseLabel);
|
|
3617
|
-
|
|
3618
|
-
var baseSelect = document.createElement("select");
|
|
3619
|
-
baseSelect.className = "wt-modal-input";
|
|
3620
|
-
// Add "main" as default while loading
|
|
3621
|
-
var defaultOpt = document.createElement("option");
|
|
3622
|
-
defaultOpt.value = "main";
|
|
3623
|
-
defaultOpt.textContent = "main";
|
|
3624
|
-
baseSelect.appendChild(defaultOpt);
|
|
3625
|
-
modal.appendChild(baseSelect);
|
|
3626
|
-
|
|
3627
|
-
// Fetch branches from target project via HTTP API
|
|
3628
|
-
fetch("/p/" + parentSlug + "/api/branches")
|
|
3629
|
-
.then(function (res) { return res.json(); })
|
|
3630
|
-
.then(function (data) {
|
|
3631
|
-
baseSelect.innerHTML = "";
|
|
3632
|
-
var branches = data.branches || ["main"];
|
|
3633
|
-
var defBranch = data.defaultBranch || "main";
|
|
3634
|
-
for (var i = 0; i < branches.length; i++) {
|
|
3635
|
-
var opt = document.createElement("option");
|
|
3636
|
-
opt.value = branches[i];
|
|
3637
|
-
opt.textContent = branches[i];
|
|
3638
|
-
if (branches[i] === defBranch) opt.selected = true;
|
|
3639
|
-
baseSelect.appendChild(opt);
|
|
3640
|
-
}
|
|
3641
|
-
})
|
|
3642
|
-
.catch(function () {});
|
|
3643
|
-
|
|
3644
|
-
var errorDiv = document.createElement("div");
|
|
3645
|
-
errorDiv.className = "wt-modal-error";
|
|
3646
|
-
modal.appendChild(errorDiv);
|
|
3647
|
-
|
|
3648
|
-
var actions = document.createElement("div");
|
|
3649
|
-
actions.className = "wt-modal-actions";
|
|
3650
|
-
|
|
3651
|
-
var cancelBtn = document.createElement("button");
|
|
3652
|
-
cancelBtn.className = "wt-modal-btn";
|
|
3653
|
-
cancelBtn.textContent = "Cancel";
|
|
3654
|
-
actions.appendChild(cancelBtn);
|
|
3655
|
-
|
|
3656
|
-
var createBtn = document.createElement("button");
|
|
3657
|
-
createBtn.className = "wt-modal-btn primary";
|
|
3658
|
-
createBtn.textContent = "Create";
|
|
3659
|
-
actions.appendChild(createBtn);
|
|
3660
|
-
|
|
3661
|
-
modal.appendChild(actions);
|
|
3662
|
-
container.appendChild(modal);
|
|
3663
|
-
document.body.appendChild(container);
|
|
3664
|
-
branchInput.focus();
|
|
3665
|
-
|
|
3666
|
-
function closeModal() { container.remove(); }
|
|
3667
|
-
|
|
3668
|
-
function doCreate() {
|
|
3669
|
-
var branch = branchInput.value.trim();
|
|
3670
|
-
var base = baseSelect.value.trim() || null;
|
|
3671
|
-
if (!branch) {
|
|
3672
|
-
errorDiv.textContent = "Branch name is required";
|
|
3673
|
-
errorDiv.classList.add("visible");
|
|
3674
|
-
return;
|
|
3675
|
-
}
|
|
3676
|
-
// Sanitize: replace slashes with dashes for directory name
|
|
3677
|
-
var dirName = branch.replace(/\//g, "-");
|
|
3678
|
-
createBtn.disabled = true;
|
|
3679
|
-
createBtn.textContent = "Creating...";
|
|
3680
|
-
errorDiv.classList.remove("visible");
|
|
3681
|
-
|
|
3682
|
-
if (ctx.ws && ctx.connected) {
|
|
3683
|
-
ctx.ws.send(JSON.stringify({
|
|
3684
|
-
type: "create_worktree",
|
|
3685
|
-
branch: branch,
|
|
3686
|
-
dirName: dirName,
|
|
3687
|
-
baseBranch: base
|
|
3688
|
-
}));
|
|
3689
|
-
}
|
|
3690
|
-
|
|
3691
|
-
// Listen for the result
|
|
3692
|
-
var handler = function (event) {
|
|
3693
|
-
var msg;
|
|
3694
|
-
try { msg = JSON.parse(event.data); } catch (e) { return; }
|
|
3695
|
-
if (msg.type === "create_worktree_result") {
|
|
3696
|
-
ctx.ws.removeEventListener("message", handler);
|
|
3697
|
-
if (msg.ok) {
|
|
3698
|
-
closeModal();
|
|
3699
|
-
if (msg.slug && ctx.switchProject) ctx.switchProject(msg.slug);
|
|
3700
|
-
} else {
|
|
3701
|
-
createBtn.disabled = false;
|
|
3702
|
-
createBtn.textContent = "Create";
|
|
3703
|
-
errorDiv.textContent = msg.error || "Failed to create worktree";
|
|
3704
|
-
errorDiv.classList.add("visible");
|
|
3705
|
-
}
|
|
3706
|
-
}
|
|
3707
|
-
};
|
|
3708
|
-
ctx.ws.addEventListener("message", handler);
|
|
3709
|
-
}
|
|
3710
|
-
|
|
3711
|
-
overlay.addEventListener("click", closeModal);
|
|
3712
|
-
cancelBtn.addEventListener("click", closeModal);
|
|
3713
|
-
createBtn.addEventListener("click", doCreate);
|
|
3714
|
-
branchInput.addEventListener("keydown", function (e) {
|
|
3715
|
-
if (e.key === "Enter") doCreate();
|
|
3716
|
-
if (e.key === "Escape") closeModal();
|
|
3717
|
-
});
|
|
3718
|
-
baseSelect.addEventListener("keydown", function (e) {
|
|
3719
|
-
if (e.key === "Enter") doCreate();
|
|
3720
|
-
if (e.key === "Escape") closeModal();
|
|
3721
|
-
});
|
|
3722
|
-
}
|
|
3723
|
-
|
|
3724
|
-
export function renderIconStrip(projects, currentSlug) {
|
|
3725
|
-
cachedProjectList = projects;
|
|
3726
|
-
cachedCurrentSlug = currentSlug;
|
|
3727
|
-
|
|
3728
|
-
var container = document.getElementById("icon-strip-projects");
|
|
3729
|
-
if (!container) return;
|
|
3730
|
-
container.innerHTML = "";
|
|
3731
|
-
|
|
3732
|
-
var grouped = groupProjects(projects);
|
|
3733
|
-
|
|
3734
|
-
for (var i = 0; i < grouped.parents.length; i++) {
|
|
3735
|
-
var p = grouped.parents[i];
|
|
3736
|
-
var worktrees = grouped.wtByParent[p.slug] || [];
|
|
3737
|
-
var hasWorktrees = worktrees.length > 0;
|
|
3738
|
-
|
|
3739
|
-
if (!hasWorktrees) {
|
|
3740
|
-
// Regular project, render as before
|
|
3741
|
-
var el = createIconItem(p, currentSlug);
|
|
3742
|
-
(function (slug, name, elem) {
|
|
3743
|
-
elem.addEventListener("contextmenu", function (e) {
|
|
3744
|
-
e.preventDefault();
|
|
3745
|
-
e.stopPropagation();
|
|
3746
|
-
showIconCtxMenu(elem, slug, name);
|
|
3747
|
-
});
|
|
3748
|
-
})(p.slug, p.name || p.slug, el);
|
|
3749
|
-
setupDragHandlers(el, p.slug);
|
|
3750
|
-
container.appendChild(el);
|
|
3751
|
-
continue;
|
|
3752
|
-
}
|
|
3753
|
-
|
|
3754
|
-
// Folder group for parent + worktrees
|
|
3755
|
-
var folder = document.createElement("div");
|
|
3756
|
-
folder.className = "icon-strip-group";
|
|
3757
|
-
folder.dataset.parentSlug = p.slug;
|
|
3758
|
-
if (wtCollapsed[p.slug]) folder.classList.add("collapsed");
|
|
3759
|
-
|
|
3760
|
-
// Bubble up worktree processing state to parent
|
|
3761
|
-
if (!p.isProcessing) {
|
|
3762
|
-
for (var wpi = 0; wpi < worktrees.length; wpi++) {
|
|
3763
|
-
if (worktrees[wpi].isProcessing) { p.isProcessing = true; break; }
|
|
3764
|
-
}
|
|
3765
|
-
}
|
|
3766
|
-
|
|
3767
|
-
// Parent icon as folder header
|
|
3768
|
-
var header = createIconItem(p, currentSlug);
|
|
3769
|
-
header.classList.add("folder-header");
|
|
3770
|
-
(function (slug, name, elem) {
|
|
3771
|
-
elem.addEventListener("contextmenu", function (e) {
|
|
3772
|
-
e.preventDefault();
|
|
3773
|
-
e.stopPropagation();
|
|
3774
|
-
showIconCtxMenu(elem, slug, name);
|
|
3775
|
-
});
|
|
3776
|
-
})(p.slug, p.name || p.slug, header);
|
|
3777
|
-
setupDragHandlers(header, p.slug);
|
|
3778
|
-
|
|
3779
|
-
// Chevron toggle
|
|
3780
|
-
var chevron = document.createElement("span");
|
|
3781
|
-
chevron.className = "icon-strip-group-chevron";
|
|
3782
|
-
chevron.innerHTML = '<i data-lucide="git-branch"></i>';
|
|
3783
|
-
(function (parentSlug, folderEl) {
|
|
3784
|
-
chevron.addEventListener("click", function (e) {
|
|
3785
|
-
e.preventDefault();
|
|
3786
|
-
e.stopPropagation();
|
|
3787
|
-
var nowCollapsed = folderEl.classList.toggle("collapsed");
|
|
3788
|
-
setWtCollapsed(parentSlug, nowCollapsed);
|
|
3789
|
-
});
|
|
3790
|
-
chevron.addEventListener("contextmenu", function (e) {
|
|
3791
|
-
e.preventDefault();
|
|
3792
|
-
e.stopPropagation();
|
|
3793
|
-
});
|
|
3794
|
-
})(p.slug, folder);
|
|
3795
|
-
chevron.setAttribute("data-tip", "Toggle worktrees");
|
|
3796
|
-
header.appendChild(chevron);
|
|
3797
|
-
folder.appendChild(header);
|
|
3798
|
-
|
|
3799
|
-
// Worktree items container
|
|
3800
|
-
var itemsContainer = document.createElement("div");
|
|
3801
|
-
itemsContainer.className = "icon-strip-group-items";
|
|
3802
|
-
|
|
3803
|
-
for (var wi = 0; wi < worktrees.length; wi++) {
|
|
3804
|
-
(function (wt) {
|
|
3805
|
-
var wtEl = document.createElement("a");
|
|
3806
|
-
var isWtActive = wt.slug === currentSlug && !currentDmUserId;
|
|
3807
|
-
var isAccessible = wt.worktreeAccessible !== false;
|
|
3808
|
-
wtEl.className = "icon-strip-wt-item" + (isWtActive ? " active" : "") + (!isAccessible ? " wt-disabled" : "");
|
|
3809
|
-
wtEl.href = "/p/" + wt.slug + "/";
|
|
3810
|
-
wtEl.dataset.slug = wt.slug;
|
|
3811
|
-
|
|
3812
|
-
var abbrev = document.createElement("span");
|
|
3813
|
-
abbrev.className = "wt-branch-abbrev";
|
|
3814
|
-
abbrev.textContent = getProjectAbbrev(wt.name);
|
|
3815
|
-
wtEl.appendChild(abbrev);
|
|
3816
|
-
|
|
3817
|
-
var wtStatus = document.createElement("span");
|
|
3818
|
-
wtStatus.className = "icon-strip-status";
|
|
3819
|
-
if (wt.isProcessing) wtStatus.classList.add("processing");
|
|
3820
|
-
wtEl.appendChild(wtStatus);
|
|
3821
|
-
|
|
3822
|
-
var tooltipText = wt.name;
|
|
3823
|
-
if (!isAccessible) {
|
|
3824
|
-
tooltipText += " (outside project path, cannot be accessed)";
|
|
3825
|
-
}
|
|
3826
|
-
(function (text, elem) {
|
|
3827
|
-
elem.addEventListener("mouseenter", function () { showIconTooltip(elem, text); });
|
|
3828
|
-
elem.addEventListener("mouseleave", hideIconTooltip);
|
|
3829
|
-
})(tooltipText, wtEl);
|
|
3830
|
-
|
|
3831
|
-
if (isAccessible) {
|
|
3832
|
-
(function (slug) {
|
|
3833
|
-
wtEl.addEventListener("click", function (e) {
|
|
3834
|
-
e.preventDefault();
|
|
3835
|
-
if (ctx.switchProject) ctx.switchProject(slug);
|
|
3836
|
-
});
|
|
3837
|
-
})(wt.slug);
|
|
3838
|
-
} else {
|
|
3839
|
-
wtEl.addEventListener("click", function (e) {
|
|
3840
|
-
e.preventDefault();
|
|
3841
|
-
});
|
|
3842
|
-
}
|
|
3843
|
-
|
|
3844
|
-
if (isAccessible) {
|
|
3845
|
-
(function (slug, name, elem) {
|
|
3846
|
-
elem.addEventListener("contextmenu", function (e) {
|
|
3847
|
-
e.preventDefault();
|
|
3848
|
-
e.stopPropagation();
|
|
3849
|
-
showIconCtxMenu(elem, slug, name);
|
|
3850
|
-
});
|
|
3851
|
-
})(wt.slug, wt.name, wtEl);
|
|
3852
|
-
} else {
|
|
3853
|
-
wtEl.addEventListener("contextmenu", function (e) {
|
|
3854
|
-
e.preventDefault();
|
|
3855
|
-
e.stopPropagation();
|
|
3856
|
-
});
|
|
3857
|
-
}
|
|
3858
|
-
|
|
3859
|
-
// Pending permission shake for worktree items
|
|
3860
|
-
if (wt.pendingPermissions > 0 && !isWtActive) {
|
|
3861
|
-
wtEl.classList.add("has-pending-perm");
|
|
3862
|
-
}
|
|
3863
|
-
|
|
3864
|
-
itemsContainer.appendChild(wtEl);
|
|
3865
|
-
})(worktrees[wi]);
|
|
3866
|
-
}
|
|
3867
|
-
|
|
3868
|
-
// Force expand if any worktree has pending permissions
|
|
3869
|
-
var hasWtPendingPerm = false;
|
|
3870
|
-
for (var wpi2 = 0; wpi2 < worktrees.length; wpi2++) {
|
|
3871
|
-
if (worktrees[wpi2].pendingPermissions > 0) { hasWtPendingPerm = true; break; }
|
|
3872
|
-
}
|
|
3873
|
-
if (hasWtPendingPerm) folder.classList.remove("collapsed");
|
|
3874
|
-
|
|
3875
|
-
// "+" button to add new worktree
|
|
3876
|
-
var addBtn = document.createElement("button");
|
|
3877
|
-
addBtn.className = "icon-strip-group-add";
|
|
3878
|
-
addBtn.textContent = "+";
|
|
3879
|
-
(function (parentSlug, parentName, btn) {
|
|
3880
|
-
btn.addEventListener("click", function (e) {
|
|
3881
|
-
e.preventDefault();
|
|
3882
|
-
e.stopPropagation();
|
|
3883
|
-
showWorktreeModal(parentSlug, parentName);
|
|
3884
|
-
});
|
|
3885
|
-
btn.addEventListener("mouseenter", function () { showIconTooltip(btn, "New worktree"); });
|
|
3886
|
-
btn.addEventListener("mouseleave", hideIconTooltip);
|
|
3887
|
-
})(p.slug, p.name, addBtn);
|
|
3888
|
-
itemsContainer.appendChild(addBtn);
|
|
3889
|
-
|
|
3890
|
-
folder.appendChild(itemsContainer);
|
|
3891
|
-
container.appendChild(folder);
|
|
3892
|
-
}
|
|
3893
|
-
|
|
3894
|
-
// Update home icon active state
|
|
3895
|
-
var homeIcon = document.querySelector(".icon-strip-home");
|
|
3896
|
-
if (homeIcon) {
|
|
3897
|
-
if ((!currentSlug || projects.length === 0) && !currentDmUserId) {
|
|
3898
|
-
homeIcon.classList.add("active");
|
|
3899
|
-
} else {
|
|
3900
|
-
homeIcon.classList.remove("active");
|
|
3901
|
-
}
|
|
3902
|
-
}
|
|
3903
|
-
|
|
3904
|
-
renderProjectList(projects, currentSlug);
|
|
3905
|
-
|
|
3906
|
-
// Render Lucide icons added dynamically (e.g. worktree git-branch icon)
|
|
3907
|
-
try { lucide.createIcons({ nodes: [container] }); } catch (e) {}
|
|
3908
|
-
}
|
|
3909
|
-
|
|
3910
|
-
function renderProjectList(projects, currentSlug) {
|
|
3911
|
-
var list = document.getElementById("project-list");
|
|
3912
|
-
if (!list) return;
|
|
3913
|
-
list.innerHTML = "";
|
|
3914
|
-
|
|
3915
|
-
var grouped = groupProjects(projects);
|
|
3916
|
-
|
|
3917
|
-
for (var i = 0; i < grouped.parents.length; i++) {
|
|
3918
|
-
var p = grouped.parents[i];
|
|
3919
|
-
var worktrees = grouped.wtByParent[p.slug] || [];
|
|
3920
|
-
|
|
3921
|
-
if (worktrees.length === 0) {
|
|
3922
|
-
// Regular project
|
|
3923
|
-
list.appendChild(createMobileProjectItem(p, currentSlug, false));
|
|
3924
|
-
continue;
|
|
3925
|
-
}
|
|
3926
|
-
|
|
3927
|
-
// Folder for parent + worktrees
|
|
3928
|
-
var folderDiv = document.createElement("div");
|
|
3929
|
-
folderDiv.className = "mobile-project-folder";
|
|
3930
|
-
if (wtCollapsed[p.slug]) folderDiv.classList.add("collapsed");
|
|
3931
|
-
|
|
3932
|
-
var headerEl = createMobileProjectItem(p, currentSlug, false);
|
|
3933
|
-
var chevron = document.createElement("span");
|
|
3934
|
-
chevron.className = "mobile-folder-chevron";
|
|
3935
|
-
chevron.innerHTML = "▼";
|
|
3936
|
-
(function (parentSlug, fDiv) {
|
|
3937
|
-
chevron.addEventListener("click", function (e) {
|
|
3938
|
-
e.preventDefault();
|
|
3939
|
-
e.stopPropagation();
|
|
3940
|
-
var nowCollapsed = fDiv.classList.toggle("collapsed");
|
|
3941
|
-
setWtCollapsed(parentSlug, nowCollapsed);
|
|
3942
|
-
});
|
|
3943
|
-
})(p.slug, folderDiv);
|
|
3944
|
-
headerEl.appendChild(chevron);
|
|
3945
|
-
folderDiv.appendChild(headerEl);
|
|
3946
|
-
|
|
3947
|
-
var wtList = document.createElement("div");
|
|
3948
|
-
wtList.className = "mobile-folder-items";
|
|
3949
|
-
for (var wi = 0; wi < worktrees.length; wi++) {
|
|
3950
|
-
var isAccessible = worktrees[wi].worktreeAccessible !== false;
|
|
3951
|
-
var wtItem = createMobileProjectItem(worktrees[wi], currentSlug, true);
|
|
3952
|
-
if (!isAccessible) wtItem.classList.add("wt-disabled");
|
|
3953
|
-
if (!isAccessible) {
|
|
3954
|
-
wtItem.addEventListener("click", function (e) { e.preventDefault(); e.stopPropagation(); });
|
|
3955
|
-
}
|
|
3956
|
-
wtList.appendChild(wtItem);
|
|
3957
|
-
}
|
|
3958
|
-
folderDiv.appendChild(wtList);
|
|
3959
|
-
list.appendChild(folderDiv);
|
|
3960
|
-
}
|
|
3961
|
-
}
|
|
3962
|
-
|
|
3963
|
-
function createMobileProjectItem(p, currentSlug, isWorktree) {
|
|
3964
|
-
var el = document.createElement("button");
|
|
3965
|
-
el.className = "mobile-project-item" + (p.slug === currentSlug ? " active" : "") + (isWorktree ? " wt-item" : "");
|
|
3966
|
-
|
|
3967
|
-
var abbrev = document.createElement("span");
|
|
3968
|
-
abbrev.className = "mobile-project-abbrev";
|
|
3969
|
-
if (p.icon) {
|
|
3970
|
-
abbrev.textContent = p.icon;
|
|
3971
|
-
parseEmojis(abbrev);
|
|
3972
|
-
} else {
|
|
3973
|
-
abbrev.textContent = getProjectAbbrev(p.name);
|
|
3974
|
-
}
|
|
3975
|
-
el.appendChild(abbrev);
|
|
3976
|
-
|
|
3977
|
-
var name = document.createElement("span");
|
|
3978
|
-
name.className = "mobile-project-name";
|
|
3979
|
-
name.textContent = p.name;
|
|
3980
|
-
el.appendChild(name);
|
|
3981
|
-
|
|
3982
|
-
if (p.isProcessing) {
|
|
3983
|
-
var dot = document.createElement("span");
|
|
3984
|
-
dot.className = "mobile-project-processing";
|
|
3985
|
-
el.appendChild(dot);
|
|
3986
|
-
}
|
|
3987
|
-
|
|
3988
|
-
el.addEventListener("click", function () {
|
|
3989
|
-
if (ctx.switchProject) ctx.switchProject(p.slug);
|
|
3990
|
-
closeSidebar();
|
|
3991
|
-
});
|
|
3992
|
-
|
|
3993
|
-
return el;
|
|
3994
|
-
}
|
|
3995
|
-
|
|
3996
|
-
export function getEmojiCategories() { return EMOJI_CATEGORIES; }
|
|
3997
|
-
|
|
3998
|
-
// --- User strip (DM targets) ---
|
|
3999
|
-
var cachedAllUsers = [];
|
|
4000
|
-
var cachedOnlineUserIds = [];
|
|
4001
|
-
var cachedDmFavorites = [];
|
|
4002
|
-
var cachedDmConversations = [];
|
|
4003
|
-
var cachedDmUnread = {};
|
|
4004
|
-
var cachedMyUserId = null;
|
|
4005
|
-
var currentDmUserId = null;
|
|
4006
|
-
var dmPickerOpen = false;
|
|
4007
|
-
|
|
4008
|
-
var cachedDmRemovedUsers = {};
|
|
4009
|
-
var cachedMates = [];
|
|
4010
|
-
|
|
4011
|
-
export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites, dmConversations, dmUnread, dmRemovedUsers, matesList) {
|
|
4012
|
-
cachedMates = matesList || cachedMates || [];
|
|
4013
|
-
cachedAllUsers = allUsers || [];
|
|
4014
|
-
cachedOnlineUserIds = onlineUserIds || [];
|
|
4015
|
-
cachedDmFavorites = dmFavorites || [];
|
|
4016
|
-
cachedDmConversations = dmConversations || [];
|
|
4017
|
-
cachedDmUnread = dmUnread || {};
|
|
4018
|
-
cachedDmRemovedUsers = dmRemovedUsers || {};
|
|
4019
|
-
cachedMyUserId = myUserId;
|
|
4020
|
-
var container = document.getElementById("icon-strip-users");
|
|
4021
|
-
if (!container) return;
|
|
4022
|
-
|
|
4023
|
-
// All other users
|
|
4024
|
-
var allOthers = cachedAllUsers.filter(function (u) { return u.id !== myUserId; });
|
|
4025
|
-
|
|
4026
|
-
// Hide section if no other users and no mates
|
|
4027
|
-
if (allOthers.length === 0 && cachedMates.length === 0) {
|
|
4028
|
-
container.innerHTML = "";
|
|
4029
|
-
container.classList.add("hidden");
|
|
4030
|
-
return;
|
|
4031
|
-
}
|
|
4032
|
-
|
|
4033
|
-
// Filter to show only: favorites + users with unread + users with DM conversations
|
|
4034
|
-
// But exclude users explicitly removed from favorites
|
|
4035
|
-
var others = allOthers.filter(function (u) {
|
|
4036
|
-
if (cachedDmRemovedUsers[u.id]) return false;
|
|
4037
|
-
if (cachedDmFavorites.indexOf(u.id) !== -1) return true;
|
|
4038
|
-
if (cachedDmUnread[u.id] && cachedDmUnread[u.id] > 0) return true;
|
|
4039
|
-
if (cachedDmConversations.indexOf(u.id) !== -1) return true;
|
|
4040
|
-
return false;
|
|
4041
|
-
});
|
|
4042
|
-
|
|
4043
|
-
container.classList.remove("hidden");
|
|
4044
|
-
container.innerHTML = "";
|
|
4045
|
-
|
|
4046
|
-
for (var i = 0; i < others.length; i++) {
|
|
4047
|
-
(function (u) {
|
|
4048
|
-
var el = document.createElement("div");
|
|
4049
|
-
el.className = "icon-strip-user";
|
|
4050
|
-
el.dataset.userId = u.id;
|
|
4051
|
-
if (u.id === currentDmUserId) el.classList.add("active");
|
|
4052
|
-
if (onlineUserIds.indexOf(u.id) !== -1) el.classList.add("online");
|
|
4053
|
-
|
|
4054
|
-
var pill = document.createElement("span");
|
|
4055
|
-
pill.className = "icon-strip-pill";
|
|
4056
|
-
el.appendChild(pill);
|
|
4057
|
-
|
|
4058
|
-
var avatar = document.createElement("img");
|
|
4059
|
-
avatar.className = "icon-strip-user-avatar";
|
|
4060
|
-
avatar.src = userAvatarUrl(u, 34);
|
|
4061
|
-
avatar.alt = u.displayName;
|
|
4062
|
-
el.appendChild(avatar);
|
|
4063
|
-
|
|
4064
|
-
var onlineDot = document.createElement("span");
|
|
4065
|
-
onlineDot.className = "icon-strip-user-online";
|
|
4066
|
-
el.appendChild(onlineDot);
|
|
4067
|
-
|
|
4068
|
-
var badge = document.createElement("span");
|
|
4069
|
-
badge.className = "icon-strip-user-badge";
|
|
4070
|
-
badge.dataset.userId = u.id;
|
|
4071
|
-
el.appendChild(badge);
|
|
4072
|
-
|
|
4073
|
-
// Tooltip
|
|
4074
|
-
el.addEventListener("mouseenter", function () { showIconTooltip(el, u.displayName); });
|
|
4075
|
-
el.addEventListener("mouseleave", hideIconTooltip);
|
|
4076
|
-
|
|
4077
|
-
// Click: open DM
|
|
4078
|
-
el.addEventListener("click", function () {
|
|
4079
|
-
if (ctx.openDm) ctx.openDm(u.id);
|
|
4080
|
-
});
|
|
4081
|
-
|
|
4082
|
-
// Right-click: show context menu
|
|
4083
|
-
el.addEventListener("contextmenu", function (e) {
|
|
4084
|
-
e.preventDefault();
|
|
4085
|
-
e.stopPropagation();
|
|
4086
|
-
showUserCtxMenu(el, u);
|
|
4087
|
-
});
|
|
4088
|
-
|
|
4089
|
-
container.appendChild(el);
|
|
4090
|
-
})(others[i]);
|
|
4091
|
-
}
|
|
4092
|
-
|
|
4093
|
-
// Build mate project status lookup from project list
|
|
4094
|
-
var mateProjectStatus = {};
|
|
4095
|
-
if (ctx && ctx.projectList) {
|
|
4096
|
-
var allProjects = ctx.projectList;
|
|
4097
|
-
for (var pi = 0; pi < allProjects.length; pi++) {
|
|
4098
|
-
if (allProjects[pi].isMate) {
|
|
4099
|
-
mateProjectStatus[allProjects[pi].slug] = allProjects[pi];
|
|
4100
|
-
}
|
|
4101
|
-
}
|
|
4102
|
-
}
|
|
4103
|
-
|
|
4104
|
-
// Render mates (only favorites, built-in first, then user-created)
|
|
4105
|
-
var favoriteMates = cachedMates.filter(function (m) {
|
|
4106
|
-
if (cachedDmRemovedUsers[m.id]) return false;
|
|
4107
|
-
if (cachedDmFavorites.indexOf(m.id) !== -1) return true;
|
|
4108
|
-
if (cachedDmUnread[m.id] && cachedDmUnread[m.id] > 0) return true;
|
|
4109
|
-
return false;
|
|
4110
|
-
});
|
|
4111
|
-
var sortedMates = favoriteMates.sort(function (a, b) {
|
|
4112
|
-
var aBuiltin = a.builtinKey ? 1 : 0;
|
|
4113
|
-
var bBuiltin = b.builtinKey ? 1 : 0;
|
|
4114
|
-
if (aBuiltin !== bBuiltin) return bBuiltin - aBuiltin;
|
|
4115
|
-
return (a.createdAt || 0) - (b.createdAt || 0);
|
|
4116
|
-
});
|
|
4117
|
-
for (var mi = 0; mi < sortedMates.length; mi++) {
|
|
4118
|
-
(function (mate) {
|
|
4119
|
-
var mp = mate.profile || {};
|
|
4120
|
-
var mateSlug = "mate-" + mate.id;
|
|
4121
|
-
var mateProj = mateProjectStatus[mateSlug] || {};
|
|
4122
|
-
var isActive = mate.id === currentDmUserId;
|
|
4123
|
-
var el = document.createElement("div");
|
|
4124
|
-
el.className = "icon-strip-user icon-strip-mate";
|
|
4125
|
-
el.dataset.userId = mate.id;
|
|
4126
|
-
el.dataset.mateSlug = mateSlug;
|
|
4127
|
-
if (isActive) el.classList.add("active");
|
|
4128
|
-
|
|
4129
|
-
// Pending permission shake
|
|
4130
|
-
if (mateProj.pendingPermissions > 0 && !isActive) {
|
|
4131
|
-
el.classList.add("has-pending-perm");
|
|
4132
|
-
}
|
|
4133
|
-
|
|
4134
|
-
var pill = document.createElement("span");
|
|
4135
|
-
pill.className = "icon-strip-pill";
|
|
4136
|
-
el.appendChild(pill);
|
|
4137
|
-
|
|
4138
|
-
var avatar = document.createElement("img");
|
|
4139
|
-
avatar.className = "icon-strip-user-avatar" + (mate.primary ? " icon-strip-primary-mate" : "");
|
|
4140
|
-
avatar.src = mateAvatarUrl(mate, 34);
|
|
4141
|
-
avatar.alt = mp.displayName || mate.name || "Mate";
|
|
4142
|
-
var mateColor = (mp.avatarColor) || mate.avatarColor || "#7c3aed";
|
|
4143
|
-
avatar.style.background = mateColor + "30";
|
|
4144
|
-
el.appendChild(avatar);
|
|
4145
|
-
|
|
4146
|
-
// Processing status dot (IO blink)
|
|
4147
|
-
var statusDot = document.createElement("span");
|
|
4148
|
-
statusDot.className = "icon-strip-status";
|
|
4149
|
-
if (mateProj.isProcessing) statusDot.classList.add("processing");
|
|
4150
|
-
el.appendChild(statusDot);
|
|
4151
|
-
|
|
4152
|
-
// Mate badge (bot icon)
|
|
4153
|
-
var mateBadge = document.createElement("span");
|
|
4154
|
-
mateBadge.className = "icon-strip-user-mate-badge";
|
|
4155
|
-
mateBadge.innerHTML = iconHtml("bot");
|
|
4156
|
-
el.appendChild(mateBadge);
|
|
4157
|
-
|
|
4158
|
-
var badge = document.createElement("span");
|
|
4159
|
-
badge.className = "icon-strip-user-badge";
|
|
4160
|
-
badge.dataset.userId = mate.id;
|
|
4161
|
-
el.appendChild(badge);
|
|
4162
|
-
|
|
4163
|
-
// Restore unread badge if cached
|
|
4164
|
-
var unreadCount = cachedDmUnread[mate.id] || 0;
|
|
4165
|
-
if (unreadCount > 0 && !isActive) {
|
|
4166
|
-
badge.textContent = unreadCount > 99 ? "99+" : String(unreadCount);
|
|
4167
|
-
badge.classList.add("has-unread");
|
|
4168
|
-
}
|
|
4169
|
-
|
|
4170
|
-
// Tooltip
|
|
4171
|
-
var displayName = mp.displayName || mate.name || "New Mate";
|
|
4172
|
-
el.addEventListener("mouseenter", function () {
|
|
4173
|
-
var html = '<div style="font-weight:600">' + escapeHtml(displayName);
|
|
4174
|
-
if (mate.primary) {
|
|
4175
|
-
html += ' <span style="font-size:10px;font-weight:600;color:#00b894;background:rgba(0,184,148,0.1);padding:1px 5px;border-radius:3px;margin-left:4px">SYSTEM</span>';
|
|
4176
|
-
}
|
|
4177
|
-
html += '</div>';
|
|
4178
|
-
if (mate.bio) {
|
|
4179
|
-
html += '<div style="font-weight:400;font-size:12px;color:var(--text-secondary);margin-top:2px">' + escapeHtml(mate.bio) + '</div>';
|
|
4180
|
-
}
|
|
4181
|
-
showIconTooltipHtml(el, html);
|
|
4182
|
-
});
|
|
4183
|
-
el.addEventListener("mouseleave", hideIconTooltip);
|
|
4184
|
-
|
|
4185
|
-
// Click: open DM with mate
|
|
4186
|
-
el.addEventListener("click", function () {
|
|
4187
|
-
if (ctx.openDm) ctx.openDm(mate.id);
|
|
4188
|
-
});
|
|
4189
|
-
|
|
4190
|
-
// Right-click: context menu for mate
|
|
4191
|
-
el.addEventListener("contextmenu", function (e) {
|
|
4192
|
-
e.preventDefault();
|
|
4193
|
-
e.stopPropagation();
|
|
4194
|
-
showMateCtxMenu(el, mate);
|
|
4195
|
-
});
|
|
4196
|
-
|
|
4197
|
-
container.appendChild(el);
|
|
4198
|
-
})(sortedMates[mi]);
|
|
4199
|
-
}
|
|
4200
|
-
|
|
4201
|
-
// Show container if we have mates even with no other users
|
|
4202
|
-
if (cachedMates.length > 0) {
|
|
4203
|
-
container.classList.remove("hidden");
|
|
4204
|
-
}
|
|
4205
|
-
|
|
4206
|
-
// Add user (+) button
|
|
4207
|
-
var addBtn = document.createElement("button");
|
|
4208
|
-
addBtn.className = "icon-strip-invite";
|
|
4209
|
-
addBtn.innerHTML = iconHtml("user-plus");
|
|
4210
|
-
addBtn.addEventListener("click", function (e) {
|
|
4211
|
-
e.stopPropagation();
|
|
4212
|
-
toggleDmUserPicker(addBtn);
|
|
4213
|
-
});
|
|
4214
|
-
addBtn.addEventListener("mouseenter", function () { showIconTooltip(addBtn, "Add user or create mate"); });
|
|
4215
|
-
addBtn.addEventListener("mouseleave", hideIconTooltip);
|
|
4216
|
-
container.appendChild(addBtn);
|
|
4217
|
-
refreshIcons();
|
|
4218
|
-
}
|
|
4219
|
-
|
|
4220
|
-
function toggleDmUserPicker(anchorEl) {
|
|
4221
|
-
if (dmPickerOpen) {
|
|
4222
|
-
closeDmUserPicker();
|
|
4223
|
-
return;
|
|
4224
|
-
}
|
|
4225
|
-
dmPickerOpen = true;
|
|
4226
|
-
|
|
4227
|
-
var picker = document.createElement("div");
|
|
4228
|
-
picker.className = "dm-user-picker";
|
|
4229
|
-
picker.id = "dm-user-picker";
|
|
4230
|
-
|
|
4231
|
-
// Search input
|
|
4232
|
-
var searchInput = document.createElement("input");
|
|
4233
|
-
searchInput.className = "dm-user-picker-search";
|
|
4234
|
-
searchInput.type = "text";
|
|
4235
|
-
searchInput.placeholder = "Search mates and users...";
|
|
4236
|
-
picker.appendChild(searchInput);
|
|
4237
|
-
|
|
4238
|
-
// User list element (appended later, after USERS label)
|
|
4239
|
-
var listEl = document.createElement("div");
|
|
4240
|
-
listEl.className = "dm-user-picker-list";
|
|
4241
|
-
|
|
4242
|
-
// Position the picker above the + button
|
|
4243
|
-
document.body.appendChild(picker);
|
|
4244
|
-
var rect = anchorEl.getBoundingClientRect();
|
|
4245
|
-
picker.style.left = (rect.right + 8) + "px";
|
|
4246
|
-
picker.style.bottom = (window.innerHeight - rect.bottom) + "px";
|
|
4247
|
-
|
|
4248
|
-
function renderPickerList(filter) {
|
|
4249
|
-
listEl.innerHTML = "";
|
|
4250
|
-
var allOthers = cachedAllUsers.filter(function (u) { return u.id !== cachedMyUserId; });
|
|
4251
|
-
// Exclude already-favorited users
|
|
4252
|
-
var available = allOthers.filter(function (u) {
|
|
4253
|
-
return cachedDmFavorites.indexOf(u.id) === -1;
|
|
4254
|
-
});
|
|
4255
|
-
if (filter) {
|
|
4256
|
-
var lf = filter.toLowerCase();
|
|
4257
|
-
available = available.filter(function (u) {
|
|
4258
|
-
return (u.displayName && u.displayName.toLowerCase().indexOf(lf) !== -1) ||
|
|
4259
|
-
(u.username && u.username.toLowerCase().indexOf(lf) !== -1);
|
|
4260
|
-
});
|
|
4261
|
-
}
|
|
4262
|
-
if (available.length === 0) {
|
|
4263
|
-
var emptyEl = document.createElement("div");
|
|
4264
|
-
emptyEl.className = "dm-user-picker-empty";
|
|
4265
|
-
emptyEl.textContent = filter ? "No users found" : "No more users to add";
|
|
4266
|
-
listEl.appendChild(emptyEl);
|
|
4267
|
-
return;
|
|
4268
|
-
}
|
|
4269
|
-
for (var i = 0; i < available.length; i++) {
|
|
4270
|
-
(function (u) {
|
|
4271
|
-
var item = document.createElement("div");
|
|
4272
|
-
item.className = "dm-user-picker-item";
|
|
4273
|
-
|
|
4274
|
-
var av = document.createElement("img");
|
|
4275
|
-
av.className = "dm-user-picker-avatar";
|
|
4276
|
-
av.src = userAvatarUrl(u, 28);
|
|
4277
|
-
av.alt = u.displayName;
|
|
4278
|
-
item.appendChild(av);
|
|
4279
|
-
|
|
4280
|
-
var name = document.createElement("span");
|
|
4281
|
-
name.className = "dm-user-picker-name";
|
|
4282
|
-
name.textContent = u.displayName;
|
|
4283
|
-
item.appendChild(name);
|
|
4284
|
-
|
|
4285
|
-
item.addEventListener("click", function () {
|
|
4286
|
-
if (ctx.sendWs) {
|
|
4287
|
-
ctx.sendWs({ type: "dm_add_favorite", targetUserId: u.id });
|
|
4288
|
-
}
|
|
4289
|
-
closeDmUserPicker();
|
|
4290
|
-
});
|
|
4291
|
-
|
|
4292
|
-
listEl.appendChild(item);
|
|
4293
|
-
})(available[i]);
|
|
4294
|
-
}
|
|
4295
|
-
}
|
|
4296
|
-
|
|
4297
|
-
// --- MATES section ---
|
|
4298
|
-
var matesSectionLabel = document.createElement("div");
|
|
4299
|
-
matesSectionLabel.className = "dm-user-picker-section";
|
|
4300
|
-
matesSectionLabel.textContent = "Mates";
|
|
4301
|
-
picker.appendChild(matesSectionLabel);
|
|
4302
|
-
|
|
4303
|
-
var matesListEl = document.createElement("div");
|
|
4304
|
-
matesListEl.className = "dm-user-picker-list dm-mates-list";
|
|
4305
|
-
picker.appendChild(matesListEl);
|
|
4306
|
-
|
|
4307
|
-
// Update scroll gradient hint
|
|
4308
|
-
function updateMatesScrollHint() {
|
|
4309
|
-
var isOverflow = matesListEl.scrollHeight > matesListEl.clientHeight + 2;
|
|
4310
|
-
if (!isOverflow) {
|
|
4311
|
-
matesListEl.classList.add("no-overflow");
|
|
4312
|
-
matesListEl.classList.remove("scrolled-bottom");
|
|
4313
|
-
return;
|
|
4314
|
-
}
|
|
4315
|
-
matesListEl.classList.remove("no-overflow");
|
|
4316
|
-
var atBottom = matesListEl.scrollTop + matesListEl.clientHeight >= matesListEl.scrollHeight - 4;
|
|
4317
|
-
if (atBottom) {
|
|
4318
|
-
matesListEl.classList.add("scrolled-bottom");
|
|
4319
|
-
} else {
|
|
4320
|
-
matesListEl.classList.remove("scrolled-bottom");
|
|
4321
|
-
}
|
|
4322
|
-
}
|
|
4323
|
-
matesListEl.addEventListener("scroll", updateMatesScrollHint);
|
|
4324
|
-
|
|
4325
|
-
function renderMatesList(filter) {
|
|
4326
|
-
matesListEl.innerHTML = "";
|
|
4327
|
-
var allMates = cachedMates || [];
|
|
4328
|
-
if (filter) {
|
|
4329
|
-
var lf = filter.toLowerCase();
|
|
4330
|
-
allMates = allMates.filter(function (m) {
|
|
4331
|
-
var name = (m.profile && m.profile.displayName) || m.name || "";
|
|
4332
|
-
return name.toLowerCase().indexOf(lf) !== -1;
|
|
4333
|
-
});
|
|
4334
|
-
}
|
|
4335
|
-
// Build unified list: installed builtins, deleted builtins, user-created
|
|
4336
|
-
var availBuiltins = (ctx.availableBuiltins && ctx.availableBuiltins()) || [];
|
|
4337
|
-
var entries = [];
|
|
4338
|
-
// 1. Installed builtin mates
|
|
4339
|
-
for (var si = 0; si < allMates.length; si++) {
|
|
4340
|
-
if (allMates[si].builtinKey) entries.push({ type: "mate", data: allMates[si] });
|
|
4341
|
-
}
|
|
4342
|
-
// 2. Deleted builtins (only when not filtering)
|
|
4343
|
-
if (!filter) {
|
|
4344
|
-
for (var di = 0; di < availBuiltins.length; di++) {
|
|
4345
|
-
entries.push({ type: "deleted", data: availBuiltins[di] });
|
|
4346
|
-
}
|
|
4347
|
-
}
|
|
4348
|
-
// 3. User-created mates
|
|
4349
|
-
var userMates = allMates.filter(function (m) { return !m.builtinKey; });
|
|
4350
|
-
userMates.sort(function (a, b) { return (a.createdAt || 0) - (b.createdAt || 0); });
|
|
4351
|
-
for (var ui = 0; ui < userMates.length; ui++) {
|
|
4352
|
-
entries.push({ type: "mate", data: userMates[ui] });
|
|
4353
|
-
}
|
|
4354
|
-
|
|
4355
|
-
for (var i = 0; i < entries.length; i++) {
|
|
4356
|
-
var entry = entries[i];
|
|
4357
|
-
if (entry.type === "deleted") {
|
|
4358
|
-
// Deleted builtin: show with "+ Add" button
|
|
4359
|
-
(function (b) {
|
|
4360
|
-
var bItem = document.createElement("div");
|
|
4361
|
-
bItem.className = "dm-user-picker-item dm-user-picker-builtin-item";
|
|
4362
|
-
bItem.style.opacity = "0.7";
|
|
4363
|
-
var bAv = document.createElement("img");
|
|
4364
|
-
bAv.className = "dm-user-picker-avatar";
|
|
4365
|
-
bAv.src = mateAvatarUrl({ avatarCustom: b.avatarCustom, avatarStyle: b.avatarStyle || "bottts", avatarSeed: b.displayName, id: b.key }, 28);
|
|
4366
|
-
bAv.alt = b.displayName;
|
|
4367
|
-
bItem.appendChild(bAv);
|
|
4368
|
-
var bNameWrap = document.createElement("div");
|
|
4369
|
-
bNameWrap.style.cssText = "flex:1;min-width:0;";
|
|
4370
|
-
var bName = document.createElement("span");
|
|
4371
|
-
bName.className = "dm-user-picker-name";
|
|
4372
|
-
bName.textContent = b.displayName;
|
|
4373
|
-
bNameWrap.appendChild(bName);
|
|
4374
|
-
var bBio = document.createElement("div");
|
|
4375
|
-
bBio.style.cssText = "font-size:11px;color:var(--text-dimmer);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
|
|
4376
|
-
bBio.textContent = b.bio || b.displayName;
|
|
4377
|
-
bNameWrap.appendChild(bBio);
|
|
4378
|
-
bItem.appendChild(bNameWrap);
|
|
4379
|
-
var bAddBtn = document.createElement("button");
|
|
4380
|
-
bAddBtn.style.cssText = "border:none;background:none;cursor:pointer;padding:2px 6px;color:var(--accent, #6366f1);font-size:12px;font-weight:600;white-space:nowrap;";
|
|
4381
|
-
bAddBtn.textContent = "+ Add";
|
|
4382
|
-
bAddBtn.title = "Re-add " + b.displayName;
|
|
4383
|
-
bAddBtn.addEventListener("click", function (e) {
|
|
4384
|
-
e.stopPropagation();
|
|
4385
|
-
if (ctx.sendWs) ctx.sendWs({ type: "mate_readd_builtin", builtinKey: b.key });
|
|
4386
|
-
closeDmUserPicker();
|
|
4387
|
-
});
|
|
4388
|
-
bItem.appendChild(bAddBtn);
|
|
4389
|
-
bItem.addEventListener("click", function () {
|
|
4390
|
-
if (ctx.sendWs) ctx.sendWs({ type: "mate_readd_builtin", builtinKey: b.key });
|
|
4391
|
-
closeDmUserPicker();
|
|
4392
|
-
});
|
|
4393
|
-
matesListEl.appendChild(bItem);
|
|
4394
|
-
})(entry.data);
|
|
4395
|
-
} else {
|
|
4396
|
-
// Normal mate
|
|
4397
|
-
(function (m) {
|
|
4398
|
-
var mp = m.profile || {};
|
|
4399
|
-
var isFav = cachedDmFavorites.indexOf(m.id) !== -1;
|
|
4400
|
-
var item = document.createElement("div");
|
|
4401
|
-
item.className = "dm-user-picker-item";
|
|
4402
|
-
if (isFav) item.classList.add("dm-picker-fav");
|
|
4403
|
-
var av = document.createElement("img");
|
|
4404
|
-
av.className = "dm-user-picker-avatar";
|
|
4405
|
-
av.src = mateAvatarUrl(m, 28);
|
|
4406
|
-
av.alt = mp.displayName || m.name || "Mate";
|
|
4407
|
-
item.appendChild(av);
|
|
4408
|
-
var nameWrap = document.createElement("div");
|
|
4409
|
-
nameWrap.style.cssText = "flex:1;min-width:0;";
|
|
4410
|
-
var name = document.createElement("span");
|
|
4411
|
-
name.className = "dm-user-picker-name";
|
|
4412
|
-
name.textContent = mp.displayName || m.name || "Mate";
|
|
4413
|
-
nameWrap.appendChild(name);
|
|
4414
|
-
if (m.bio) {
|
|
4415
|
-
var bio = document.createElement("div");
|
|
4416
|
-
bio.style.cssText = "font-size:11px;color:var(--text-dimmer);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
|
|
4417
|
-
bio.textContent = m.bio;
|
|
4418
|
-
nameWrap.appendChild(bio);
|
|
4419
|
-
}
|
|
4420
|
-
item.appendChild(nameWrap);
|
|
4421
|
-
// Delete button with inline confirm
|
|
4422
|
-
var delBtn = document.createElement("button");
|
|
4423
|
-
delBtn.className = "dm-picker-del-btn";
|
|
4424
|
-
delBtn.innerHTML = m.builtinKey ? iconHtml("minus-circle") : iconHtml("trash-2");
|
|
4425
|
-
delBtn.title = m.builtinKey ? "Remove mate" : "Delete mate";
|
|
4426
|
-
delBtn.addEventListener("click", function (e) {
|
|
4427
|
-
e.stopPropagation();
|
|
4428
|
-
var origHtml = item.innerHTML;
|
|
4429
|
-
item.innerHTML = "";
|
|
4430
|
-
item.style.justifyContent = "center";
|
|
4431
|
-
item.style.gap = "6px";
|
|
4432
|
-
var confirmMsg = document.createElement("span");
|
|
4433
|
-
confirmMsg.style.cssText = "font-size:12px;color:var(--text-dimmer);";
|
|
4434
|
-
confirmMsg.textContent = m.builtinKey ? "Remove? You can add back anytime." : "Delete permanently?";
|
|
4435
|
-
item.appendChild(confirmMsg);
|
|
4436
|
-
var yesBtn = document.createElement("button");
|
|
4437
|
-
yesBtn.style.cssText = "border:none;background:var(--danger,#e74c3c);color:#fff;padding:3px 10px;border-radius:4px;font-size:12px;cursor:pointer;";
|
|
4438
|
-
yesBtn.textContent = m.builtinKey ? "Remove" : "Delete";
|
|
4439
|
-
yesBtn.addEventListener("click", function (e2) {
|
|
4440
|
-
e2.stopPropagation();
|
|
4441
|
-
if (ctx.sendWs) ctx.sendWs({ type: "mate_delete", mateId: m.id });
|
|
4442
|
-
closeDmUserPicker();
|
|
4443
|
-
});
|
|
4444
|
-
item.appendChild(yesBtn);
|
|
4445
|
-
var noBtn = document.createElement("button");
|
|
4446
|
-
noBtn.style.cssText = "border:1px solid var(--border);background:none;color:var(--text);padding:3px 10px;border-radius:4px;font-size:12px;cursor:pointer;";
|
|
4447
|
-
noBtn.textContent = "Cancel";
|
|
4448
|
-
noBtn.addEventListener("click", function (e2) {
|
|
4449
|
-
e2.stopPropagation();
|
|
4450
|
-
item.innerHTML = origHtml;
|
|
4451
|
-
item.style.justifyContent = "";
|
|
4452
|
-
item.style.gap = "";
|
|
4453
|
-
refreshIcons();
|
|
4454
|
-
});
|
|
4455
|
-
item.appendChild(noBtn);
|
|
4456
|
-
});
|
|
4457
|
-
item.appendChild(delBtn);
|
|
4458
|
-
item.addEventListener("click", function () {
|
|
4459
|
-
if (ctx.openDm) ctx.openDm(m.id);
|
|
4460
|
-
if (!isFav && ctx.sendWs) ctx.sendWs({ type: "dm_add_favorite", targetUserId: m.id });
|
|
4461
|
-
closeDmUserPicker();
|
|
4462
|
-
});
|
|
4463
|
-
matesListEl.appendChild(item);
|
|
4464
|
-
})(entry.data);
|
|
4465
|
-
}
|
|
4466
|
-
}
|
|
4467
|
-
|
|
4468
|
-
if (entries.length === 0 && filter) {
|
|
4469
|
-
var emptyEl = document.createElement("div");
|
|
4470
|
-
emptyEl.className = "dm-user-picker-empty";
|
|
4471
|
-
emptyEl.textContent = "No mates found";
|
|
4472
|
-
matesListEl.appendChild(emptyEl);
|
|
4473
|
-
}
|
|
4474
|
-
refreshIcons();
|
|
4475
|
-
requestAnimationFrame(updateMatesScrollHint);
|
|
4476
|
-
}
|
|
4477
|
-
|
|
4478
|
-
// Create Mate option
|
|
4479
|
-
var createMateEl = document.createElement("div");
|
|
4480
|
-
createMateEl.className = "dm-user-picker-create-mate";
|
|
4481
|
-
var hasCustomMates = (cachedMates || []).some(function (m) { return !m.builtinKey; });
|
|
4482
|
-
var createMateLabel = hasCustomMates ? "Create a Mate" : "Create a Mate for what you're doing";
|
|
4483
|
-
createMateEl.innerHTML = iconHtml("bot") + " <span>" + createMateLabel + "</span>";
|
|
4484
|
-
createMateEl.addEventListener("click", function () {
|
|
4485
|
-
closeDmUserPicker();
|
|
4486
|
-
if (ctx.openMateWizard) ctx.openMateWizard();
|
|
4487
|
-
});
|
|
4488
|
-
picker.appendChild(createMateEl);
|
|
4489
|
-
|
|
4490
|
-
// Divider
|
|
4491
|
-
var divider = document.createElement("div");
|
|
4492
|
-
divider.style.borderTop = "1px solid var(--border, #333)";
|
|
4493
|
-
divider.style.margin = "4px 0";
|
|
4494
|
-
picker.appendChild(divider);
|
|
4495
|
-
|
|
4496
|
-
// Section label for users
|
|
4497
|
-
var sectionLabel = document.createElement("div");
|
|
4498
|
-
sectionLabel.className = "dm-user-picker-section";
|
|
4499
|
-
sectionLabel.textContent = "Users";
|
|
4500
|
-
picker.appendChild(sectionLabel);
|
|
4501
|
-
picker.appendChild(listEl);
|
|
4502
|
-
|
|
4503
|
-
renderMatesList("");
|
|
4504
|
-
renderPickerList("");
|
|
4505
|
-
searchInput.addEventListener("input", function () {
|
|
4506
|
-
var val = searchInput.value;
|
|
4507
|
-
renderMatesList(val);
|
|
4508
|
-
renderPickerList(val);
|
|
4509
|
-
});
|
|
4510
|
-
|
|
4511
|
-
// Focus search
|
|
4512
|
-
setTimeout(function () { searchInput.focus(); }, 50);
|
|
4513
|
-
|
|
4514
|
-
// Close on click outside
|
|
4515
|
-
function onDocClick(e) {
|
|
4516
|
-
if (!picker.contains(e.target) && e.target !== anchorEl && !anchorEl.contains(e.target)) {
|
|
4517
|
-
closeDmUserPicker();
|
|
4518
|
-
document.removeEventListener("click", onDocClick, true);
|
|
4519
|
-
}
|
|
4520
|
-
}
|
|
4521
|
-
setTimeout(function () {
|
|
4522
|
-
document.addEventListener("click", onDocClick, true);
|
|
4523
|
-
}, 10);
|
|
4524
|
-
picker._docClickHandler = onDocClick;
|
|
4525
|
-
}
|
|
4526
|
-
|
|
4527
|
-
export function closeDmUserPicker() {
|
|
4528
|
-
dmPickerOpen = false;
|
|
4529
|
-
var picker = document.getElementById("dm-user-picker");
|
|
4530
|
-
if (picker) {
|
|
4531
|
-
if (picker._docClickHandler) {
|
|
4532
|
-
document.removeEventListener("click", picker._docClickHandler, true);
|
|
4533
|
-
}
|
|
4534
|
-
picker.remove();
|
|
4535
|
-
}
|
|
4536
|
-
}
|
|
4537
|
-
|
|
4538
|
-
export function setCurrentDmUser(userId) {
|
|
4539
|
-
currentDmUserId = userId;
|
|
4540
|
-
// Update active state on user icons immediately
|
|
4541
|
-
var container = document.getElementById("icon-strip-users");
|
|
4542
|
-
if (!container) return;
|
|
4543
|
-
var items = container.querySelectorAll(".icon-strip-user");
|
|
4544
|
-
for (var i = 0; i < items.length; i++) {
|
|
4545
|
-
if (items[i].dataset.userId === userId) {
|
|
4546
|
-
items[i].classList.add("active");
|
|
4547
|
-
} else {
|
|
4548
|
-
items[i].classList.remove("active");
|
|
4549
|
-
}
|
|
4550
|
-
}
|
|
4551
|
-
}
|
|
4552
|
-
|
|
4553
|
-
export function updateDmBadge(userId, count) {
|
|
4554
|
-
var badge = document.querySelector('.icon-strip-user-badge[data-user-id="' + userId + '"]');
|
|
4555
|
-
if (!badge) return;
|
|
4556
|
-
if (count > 0) {
|
|
4557
|
-
badge.textContent = count > 99 ? "99+" : String(count);
|
|
4558
|
-
badge.classList.add("has-unread");
|
|
4559
|
-
} else {
|
|
4560
|
-
badge.textContent = "";
|
|
4561
|
-
badge.classList.remove("has-unread");
|
|
4562
|
-
}
|
|
4563
|
-
}
|
|
4564
|
-
|
|
4565
|
-
export function updateSessionBadge(sessionId, count) {
|
|
4566
|
-
var badge = document.querySelector('.session-unread-badge[data-session-id="' + sessionId + '"]');
|
|
4567
|
-
if (!badge) return;
|
|
4568
|
-
if (count > 0) {
|
|
4569
|
-
badge.textContent = count > 99 ? "99+" : String(count);
|
|
4570
|
-
badge.classList.add("has-unread");
|
|
4571
|
-
} else {
|
|
4572
|
-
badge.textContent = "";
|
|
4573
|
-
badge.classList.remove("has-unread");
|
|
4574
|
-
}
|
|
4575
|
-
}
|
|
4576
|
-
|
|
4577
|
-
export function updateProjectBadge(slug, count) {
|
|
4578
|
-
var icon = document.querySelector('.icon-strip-item[data-slug="' + slug + '"]');
|
|
4579
|
-
if (!icon) return;
|
|
4580
|
-
var badge = icon.querySelector(".icon-strip-project-badge");
|
|
4581
|
-
if (!badge) return;
|
|
4582
|
-
if (count > 0) {
|
|
4583
|
-
badge.textContent = count > 99 ? "99+" : String(count);
|
|
4584
|
-
badge.classList.add("has-unread");
|
|
4585
|
-
} else {
|
|
4586
|
-
badge.textContent = "";
|
|
4587
|
-
badge.classList.remove("has-unread");
|
|
4588
|
-
}
|
|
4589
|
-
}
|
|
4590
|
-
|
|
4591
|
-
export function initIconStrip(_ctx) {
|
|
4592
|
-
var addBtn = document.getElementById("icon-strip-add");
|
|
4593
|
-
if (addBtn) {
|
|
4594
|
-
addBtn.addEventListener("click", function () {
|
|
4595
|
-
if (_ctx.openAddProjectModal) {
|
|
4596
|
-
_ctx.openAddProjectModal();
|
|
4597
|
-
} else {
|
|
4598
|
-
var modal = _ctx.$("add-project-modal");
|
|
4599
|
-
if (modal) modal.classList.remove("hidden");
|
|
4600
|
-
}
|
|
4601
|
-
});
|
|
4602
|
-
addBtn.addEventListener("mouseenter", function () { showIconTooltip(addBtn, "Add project"); });
|
|
4603
|
-
addBtn.addEventListener("mouseleave", hideIconTooltip);
|
|
4604
|
-
}
|
|
280
|
+
for (var i = 0; i < count; i++) {
|
|
281
|
+
var dot = document.createElement("div");
|
|
282
|
+
dot.className = "dust-particle";
|
|
283
|
+
var size = 3 + Math.random() * 5;
|
|
284
|
+
var angle = Math.random() * Math.PI * 2;
|
|
285
|
+
var dist = 30 + Math.random() * 60;
|
|
286
|
+
var dx = Math.cos(angle) * dist;
|
|
287
|
+
var dy = Math.sin(angle) * dist - 20; // bias upward
|
|
288
|
+
var duration = 600 + Math.random() * 500;
|
|
4605
289
|
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
exploreBtn.addEventListener("mouseleave", hideIconTooltip);
|
|
4615
|
-
}
|
|
290
|
+
dot.style.width = size + "px";
|
|
291
|
+
dot.style.height = size + "px";
|
|
292
|
+
dot.style.left = cx + "px";
|
|
293
|
+
dot.style.top = cy + "px";
|
|
294
|
+
dot.style.background = colors[Math.floor(Math.random() * colors.length)];
|
|
295
|
+
dot.style.setProperty("--dust-x", dx + "px");
|
|
296
|
+
dot.style.setProperty("--dust-y", dy + "px");
|
|
297
|
+
dot.style.setProperty("--dust-duration", duration + "ms");
|
|
4616
298
|
|
|
4617
|
-
|
|
4618
|
-
var homeIcon = document.querySelector(".icon-strip-home");
|
|
4619
|
-
if (homeIcon) {
|
|
4620
|
-
homeIcon.addEventListener("mouseenter", function () { showIconTooltip(homeIcon, "Clay"); });
|
|
4621
|
-
homeIcon.addEventListener("mouseleave", hideIconTooltip);
|
|
4622
|
-
homeIcon.addEventListener("click", function (e) {
|
|
4623
|
-
e.preventDefault();
|
|
4624
|
-
if (_ctx.showHomeHub) _ctx.showHomeHub();
|
|
4625
|
-
});
|
|
4626
|
-
homeIcon.style.cursor = "pointer";
|
|
299
|
+
container.appendChild(dot);
|
|
4627
300
|
}
|
|
4628
301
|
|
|
4629
|
-
|
|
4630
|
-
var dropdownBtn = document.getElementById("title-bar-project-dropdown");
|
|
4631
|
-
if (dropdownBtn) {
|
|
4632
|
-
dropdownBtn.addEventListener("click", function (e) {
|
|
4633
|
-
e.stopPropagation();
|
|
4634
|
-
// Find current project info from cached list
|
|
4635
|
-
var current = null;
|
|
4636
|
-
for (var i = 0; i < cachedProjectList.length; i++) {
|
|
4637
|
-
if (cachedProjectList[i].slug === cachedCurrentSlug) {
|
|
4638
|
-
current = cachedProjectList[i];
|
|
4639
|
-
break;
|
|
4640
|
-
}
|
|
4641
|
-
}
|
|
4642
|
-
if (!current) return;
|
|
4643
|
-
|
|
4644
|
-
// Toggle open state
|
|
4645
|
-
if (projectCtxMenu) {
|
|
4646
|
-
closeProjectCtxMenu();
|
|
4647
|
-
dropdownBtn.classList.remove("open");
|
|
4648
|
-
return;
|
|
4649
|
-
}
|
|
4650
|
-
dropdownBtn.classList.add("open");
|
|
4651
|
-
showProjectCtxMenu(dropdownBtn, current.slug, current.name, current.icon, "below");
|
|
4652
|
-
// Remove open class when menu closes
|
|
4653
|
-
var observer = new MutationObserver(function () {
|
|
4654
|
-
if (!projectCtxMenu) {
|
|
4655
|
-
dropdownBtn.classList.remove("open");
|
|
4656
|
-
observer.disconnect();
|
|
4657
|
-
}
|
|
4658
|
-
});
|
|
4659
|
-
observer.observe(document.body, { childList: true });
|
|
4660
|
-
});
|
|
4661
|
-
}
|
|
302
|
+
setTimeout(function () { container.remove(); }, 1200);
|
|
4662
303
|
}
|