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.
Files changed (71) hide show
  1. package/README.md +10 -0
  2. package/lib/daemon-projects.js +164 -0
  3. package/lib/daemon.js +13 -126
  4. package/lib/mates-identity.js +132 -0
  5. package/lib/mates-knowledge.js +113 -0
  6. package/lib/mates-prompts.js +398 -0
  7. package/lib/mates.js +40 -599
  8. package/lib/project-connection.js +2 -0
  9. package/lib/project-http.js +4 -2
  10. package/lib/project-loop.js +110 -48
  11. package/lib/project-mate-interaction.js +4 -0
  12. package/lib/project-notifications.js +210 -0
  13. package/lib/project-sessions.js +5 -2
  14. package/lib/project-user-message.js +2 -1
  15. package/lib/project.js +26 -2
  16. package/lib/public/app.js +1193 -8517
  17. package/lib/public/css/command-palette.css +14 -0
  18. package/lib/public/css/loop.css +301 -0
  19. package/lib/public/css/notifications-center.css +190 -0
  20. package/lib/public/css/rewind.css +6 -0
  21. package/lib/public/index.html +89 -35
  22. package/lib/public/modules/app-connection.js +160 -0
  23. package/lib/public/modules/app-cursors.js +473 -0
  24. package/lib/public/modules/app-debate-ui.js +389 -0
  25. package/lib/public/modules/app-dm.js +627 -0
  26. package/lib/public/modules/app-favicon.js +212 -0
  27. package/lib/public/modules/app-header.js +229 -0
  28. package/lib/public/modules/app-home-hub.js +600 -0
  29. package/lib/public/modules/app-loop-ui.js +589 -0
  30. package/lib/public/modules/app-loop-wizard.js +439 -0
  31. package/lib/public/modules/app-messages.js +1560 -0
  32. package/lib/public/modules/app-misc.js +299 -0
  33. package/lib/public/modules/app-notifications.js +372 -0
  34. package/lib/public/modules/app-panels.js +888 -0
  35. package/lib/public/modules/app-projects.js +798 -0
  36. package/lib/public/modules/app-rate-limit.js +451 -0
  37. package/lib/public/modules/app-rendering.js +597 -0
  38. package/lib/public/modules/app-skills-install.js +234 -0
  39. package/lib/public/modules/command-palette.js +27 -4
  40. package/lib/public/modules/input.js +31 -20
  41. package/lib/public/modules/scheduler-config.js +1532 -0
  42. package/lib/public/modules/scheduler-history.js +79 -0
  43. package/lib/public/modules/scheduler.js +33 -1554
  44. package/lib/public/modules/session-search.js +13 -1
  45. package/lib/public/modules/sidebar-mates.js +812 -0
  46. package/lib/public/modules/sidebar-mobile.js +1269 -0
  47. package/lib/public/modules/sidebar-projects.js +1449 -0
  48. package/lib/public/modules/sidebar-sessions.js +986 -0
  49. package/lib/public/modules/sidebar.js +232 -4591
  50. package/lib/public/modules/store.js +27 -0
  51. package/lib/public/modules/ws-ref.js +7 -0
  52. package/lib/public/style.css +1 -0
  53. package/lib/sdk-bridge.js +96 -717
  54. package/lib/sdk-message-processor.js +587 -0
  55. package/lib/sdk-message-queue.js +42 -0
  56. package/lib/sdk-skill-discovery.js +131 -0
  57. package/lib/server-admin.js +712 -0
  58. package/lib/server-auth.js +737 -0
  59. package/lib/server-dm.js +221 -0
  60. package/lib/server-mates.js +281 -0
  61. package/lib/server-palette.js +110 -0
  62. package/lib/server-settings.js +479 -0
  63. package/lib/server-skills.js +280 -0
  64. package/lib/server.js +246 -2755
  65. package/lib/sessions.js +11 -4
  66. package/lib/users-auth.js +146 -0
  67. package/lib/users-permissions.js +118 -0
  68. package/lib/users-preferences.js +210 -0
  69. package/lib/users.js +48 -398
  70. package/lib/ws-schema.js +498 -0
  71. package/package.json +1 -1
@@ -0,0 +1,1269 @@
1
+ // sidebar-mobile.js - Mobile sheet overlays, tab bar, and mobile-specific rendering
2
+ // Extracted from sidebar.js (PR-38)
3
+
4
+ import { mateAvatarUrl } from './avatar.js';
5
+ import { escapeHtml } from './utils.js';
6
+ import { iconHtml, refreshIcons } from './icons.js';
7
+ import { parseEmojis } from './markdown.js';
8
+ import { getCurrentTheme, getChatLayout, setChatLayout } from './theme.js';
9
+ import { openCommandPalette } from './command-palette.js';
10
+ import { getMateSessions } from './mate-sidebar.js';
11
+ import { openProjectSettings } from './project-settings.js';
12
+ import {
13
+ getCachedSessions,
14
+ getDateGroup
15
+ } from './sidebar-sessions.js';
16
+ import {
17
+ getCachedProjectList,
18
+ getCachedCurrentSlug,
19
+ getProjectAbbrev
20
+ } from './sidebar-projects.js';
21
+ import {
22
+ getCurrentDmUserId,
23
+ getCachedMates,
24
+ getCachedDmFavorites,
25
+ getCachedDmUnread,
26
+ getCachedDmRemovedUsers
27
+ } from './sidebar-mates.js';
28
+
29
+ var _ctx = null;
30
+
31
+ // --- Mobile state ---
32
+ var mobileChatSheetOpen = false;
33
+ var mobileSheetMateData = null;
34
+ var expandedMobileLoopGroups = new Set();
35
+ var expandedMobileLoopRuns = new Set();
36
+
37
+ export function setMobileSheetMateData(data) {
38
+ mobileSheetMateData = data;
39
+ }
40
+
41
+ export function openMobileSheet(type) {
42
+ var sheet = document.getElementById("mobile-sheet");
43
+ if (!sheet) return;
44
+
45
+ var titleEl = sheet.querySelector(".mobile-sheet-title");
46
+ var listEl = sheet.querySelector(".mobile-sheet-list");
47
+ if (!titleEl || !listEl) return;
48
+
49
+ // Return file tree to sidebar before clearing (prevents destroying it)
50
+ if (sheet.classList.contains("sheet-files")) {
51
+ var prevFileTree = document.getElementById("file-tree");
52
+ var prevPanel = document.getElementById("sidebar-panel-files");
53
+ if (prevFileTree && prevPanel) prevPanel.appendChild(prevFileTree);
54
+ }
55
+ // Return knowledge files to mate sidebar before clearing
56
+ if (sheet.classList.contains("sheet-knowledge")) {
57
+ var prevKnowledge = document.getElementById("mate-knowledge-files");
58
+ var prevKnowledgePanel = document.getElementById("mate-sidebar-knowledge");
59
+ if (prevKnowledge && prevKnowledgePanel) prevKnowledgePanel.appendChild(prevKnowledge);
60
+ }
61
+
62
+ listEl.innerHTML = "";
63
+ sheet.classList.remove("sheet-files", "sheet-knowledge");
64
+
65
+ if (type === "projects") {
66
+ titleEl.textContent = "Projects";
67
+ renderSheetProjects(listEl);
68
+ } else if (type === "sessions") {
69
+ titleEl.textContent = "Chat";
70
+ renderSheetSessions(listEl);
71
+ } else if (type === "files") {
72
+ titleEl.textContent = "Files";
73
+ sheet.classList.add("sheet-files");
74
+ var fileTree = document.getElementById("file-tree");
75
+ if (fileTree) {
76
+ listEl.appendChild(fileTree);
77
+ fileTree.classList.remove("hidden");
78
+ }
79
+ if (_ctx.onFilesTabOpen) _ctx.onFilesTabOpen();
80
+ } else if (type === "mate-knowledge") {
81
+ titleEl.textContent = "Knowledge";
82
+ sheet.classList.add("sheet-knowledge");
83
+ var knowledgeFiles = document.getElementById("mate-knowledge-files");
84
+ if (knowledgeFiles) {
85
+ listEl.appendChild(knowledgeFiles);
86
+ knowledgeFiles.classList.remove("hidden");
87
+ }
88
+ // Request knowledge list if not loaded
89
+ if (_ctx.requestKnowledgeList) _ctx.requestKnowledgeList();
90
+ } else if (type === "mate-profile") {
91
+ titleEl.textContent = "";
92
+ renderSheetMateProfile(listEl);
93
+ } else if (type === "search") {
94
+ titleEl.textContent = "Search";
95
+ renderSheetSearch(listEl);
96
+ } else if (type === "tools") {
97
+ titleEl.textContent = "Tools";
98
+ renderSheetTools(listEl);
99
+ } else if (type === "settings") {
100
+ titleEl.textContent = "Settings";
101
+ renderSheetSettings(listEl);
102
+ }
103
+
104
+ sheet.classList.remove("hidden", "closing");
105
+ refreshIcons();
106
+ }
107
+
108
+ function closeMobileSheet() {
109
+ var sheet = document.getElementById("mobile-sheet");
110
+ if (!sheet || sheet.classList.contains("hidden")) return;
111
+
112
+ mobileChatSheetOpen = false;
113
+
114
+ // Return file tree to sidebar if it was moved
115
+ if (sheet.classList.contains("sheet-files")) {
116
+ var fileTree = document.getElementById("file-tree");
117
+ var sidebarFilesPanel = document.getElementById("sidebar-panel-files");
118
+ if (fileTree && sidebarFilesPanel) {
119
+ sidebarFilesPanel.appendChild(fileTree);
120
+ }
121
+ }
122
+ // Return knowledge files to mate sidebar if moved
123
+ if (sheet.classList.contains("sheet-knowledge")) {
124
+ var knowledgeFiles = document.getElementById("mate-knowledge-files");
125
+ var knowledgePanel = document.getElementById("mate-sidebar-knowledge");
126
+ if (knowledgeFiles && knowledgePanel) {
127
+ knowledgePanel.appendChild(knowledgeFiles);
128
+ }
129
+ }
130
+
131
+ sheet.classList.add("closing");
132
+ setTimeout(function () {
133
+ sheet.classList.add("hidden");
134
+ sheet.classList.remove("closing", "sheet-files");
135
+ }, 230);
136
+ }
137
+
138
+ function renderSheetProjects(listEl) {
139
+ for (var i = 0; i < getCachedProjectList().length; i++) {
140
+ (function (p) {
141
+ var el = document.createElement("button");
142
+ el.className = "mobile-project-item" + (p.slug === getCachedCurrentSlug() ? " active" : "");
143
+
144
+ var abbrev = document.createElement("span");
145
+ abbrev.className = "mobile-project-abbrev";
146
+ if (p.icon) {
147
+ abbrev.textContent = p.icon;
148
+ parseEmojis(abbrev);
149
+ } else {
150
+ abbrev.textContent = getProjectAbbrev(p.name);
151
+ }
152
+ el.appendChild(abbrev);
153
+
154
+ var name = document.createElement("span");
155
+ name.className = "mobile-project-name";
156
+ name.textContent = p.name;
157
+ el.appendChild(name);
158
+
159
+ if (p.isProcessing) {
160
+ var dot = document.createElement("span");
161
+ dot.className = "mobile-project-processing";
162
+ el.appendChild(dot);
163
+ }
164
+
165
+ if (p.unread > 0 && p.slug !== getCachedCurrentSlug()) {
166
+ var mBadge = document.createElement("span");
167
+ mBadge.className = "mobile-project-unread";
168
+ mBadge.textContent = p.unread > 99 ? "99+" : String(p.unread);
169
+ el.appendChild(mBadge);
170
+ }
171
+
172
+ el.addEventListener("click", function () {
173
+ if (_ctx.switchProject) _ctx.switchProject(p.slug);
174
+ closeMobileSheet();
175
+ });
176
+
177
+ listEl.appendChild(el);
178
+ })(getCachedProjectList()[i]);
179
+ }
180
+ }
181
+
182
+ function renderSheetSessions(listEl) {
183
+ // --- Context filter bar (horizontal scroll) ---
184
+ var filterBar = document.createElement("div");
185
+ filterBar.className = "mobile-chat-filter-bar";
186
+
187
+ // Current project chip (always first, pre-selected)
188
+ var currentProject = null;
189
+ for (var pi = 0; pi < getCachedProjectList().length; pi++) {
190
+ if (getCachedProjectList()[pi].slug === getCachedCurrentSlug()) {
191
+ currentProject = getCachedProjectList()[pi];
192
+ break;
193
+ }
194
+ }
195
+
196
+ // Build chips: projects first, then mates
197
+ var chips = [];
198
+
199
+ for (var ci = 0; ci < getCachedProjectList().length; ci++) {
200
+ (function (p) {
201
+ var chip = document.createElement("button");
202
+ chip.className = "mobile-chat-chip";
203
+ var isDmActive = document.body.classList.contains("mate-dm-active");
204
+ if (p.slug === getCachedCurrentSlug() && !isDmActive) chip.classList.add("active");
205
+ chip.dataset.type = "project";
206
+ chip.dataset.slug = p.slug;
207
+
208
+ var abbrev = document.createElement("span");
209
+ abbrev.className = "mobile-chat-chip-icon";
210
+ if (p.icon) {
211
+ abbrev.textContent = p.icon;
212
+ parseEmojis(abbrev);
213
+ } else {
214
+ abbrev.textContent = getProjectAbbrev(p.name);
215
+ }
216
+ chip.appendChild(abbrev);
217
+
218
+ var label = document.createElement("span");
219
+ label.textContent = p.name;
220
+ chip.appendChild(label);
221
+
222
+ // Processing dot: same class as icon strip
223
+ var statusDot = document.createElement("span");
224
+ statusDot.className = "icon-strip-status";
225
+ if (p.isProcessing) statusDot.classList.add("processing");
226
+ chip.appendChild(statusDot);
227
+
228
+ if (p.unread > 0 && p.slug !== getCachedCurrentSlug()) {
229
+ var badge = document.createElement("span");
230
+ badge.className = "mobile-chat-chip-badge";
231
+ badge.textContent = p.unread > 99 ? "99+" : String(p.unread);
232
+ chip.appendChild(badge);
233
+ }
234
+
235
+ chips.push(chip);
236
+ })(getCachedProjectList()[ci]);
237
+ }
238
+
239
+ var favoriteChipMates = getCachedMates().filter(function (m) {
240
+ if (getCachedDmRemovedUsers()[m.id]) return false;
241
+ if (getCachedDmFavorites().indexOf(m.id) !== -1) return true;
242
+ if (getCachedDmUnread()[m.id] && getCachedDmUnread()[m.id] > 0) return true;
243
+ return false;
244
+ });
245
+ var sortedChipMates = favoriteChipMates.sort(function (a, b) {
246
+ var aBuiltin = a.builtinKey ? 1 : 0;
247
+ var bBuiltin = b.builtinKey ? 1 : 0;
248
+ if (aBuiltin !== bBuiltin) return bBuiltin - aBuiltin;
249
+ return (a.createdAt || 0) - (b.createdAt || 0);
250
+ });
251
+ for (var mi = 0; mi < sortedChipMates.length; mi++) {
252
+ (function (mate) {
253
+ var mp = mate.profile || {};
254
+ var chip = document.createElement("button");
255
+ chip.className = "mobile-chat-chip";
256
+ if (getCurrentDmUserId() === mate.id) chip.classList.add("active");
257
+ chip.dataset.type = "mate";
258
+ chip.dataset.mateId = mate.id;
259
+
260
+ var avatarEl = document.createElement("img");
261
+ avatarEl.className = "mobile-chat-chip-avatar";
262
+ avatarEl.src = mateAvatarUrl(mate, 20);
263
+ avatarEl.alt = mp.displayName || mate.name || "";
264
+ chip.appendChild(avatarEl);
265
+
266
+ var label = document.createElement("span");
267
+ label.textContent = mp.displayName || mate.name || "Mate";
268
+ chip.appendChild(label);
269
+
270
+ // Processing dot: same class as icon strip, same data source
271
+ var mateSlug = "mate-" + mate.id;
272
+ var mateProj = null;
273
+ var allProjects = (_ctx && _ctx.projectList) || [];
274
+ for (var pi = 0; pi < allProjects.length; pi++) {
275
+ if (allProjects[pi].slug === mateSlug) { mateProj = allProjects[pi]; break; }
276
+ }
277
+ var statusDot = document.createElement("span");
278
+ statusDot.className = "icon-strip-status";
279
+ if (mateProj && mateProj.isProcessing) statusDot.classList.add("processing");
280
+ chip.appendChild(statusDot);
281
+
282
+ var unreadCount = getCachedDmUnread()[mate.id] || 0;
283
+ if (unreadCount > 0) {
284
+ var badge = document.createElement("span");
285
+ badge.className = "mobile-chat-chip-badge";
286
+ badge.textContent = unreadCount > 99 ? "99+" : String(unreadCount);
287
+ chip.appendChild(badge);
288
+ }
289
+
290
+ chips.push(chip);
291
+ })(sortedChipMates[mi]);
292
+ }
293
+
294
+ for (var i = 0; i < chips.length; i++) {
295
+ filterBar.appendChild(chips[i]);
296
+ }
297
+ listEl.appendChild(filterBar);
298
+
299
+ // --- Session list container ---
300
+ var sessionListEl = document.createElement("div");
301
+ sessionListEl.className = "mobile-chat-session-list";
302
+ listEl.appendChild(sessionListEl);
303
+
304
+ // --- Render sessions for a context ---
305
+ function renderSessionsForContext(type, slug, mateId) {
306
+ sessionListEl.innerHTML = "";
307
+
308
+ if (type === "project") {
309
+ renderMobileSessionsInto(sessionListEl);
310
+ } else if (type === "mate") {
311
+ // Mate DM: open the DM and show mate actions
312
+ if (_ctx.openDm) _ctx.openDm(mateId);
313
+ renderMateMobileActions(sessionListEl);
314
+ }
315
+
316
+ refreshIcons();
317
+ }
318
+
319
+ // --- Chip click handlers ---
320
+ for (var j = 0; j < chips.length; j++) {
321
+ (function (chip) {
322
+ chip.addEventListener("click", function () {
323
+ // Deactivate all chips
324
+ for (var k = 0; k < chips.length; k++) {
325
+ chips[k].classList.remove("active");
326
+ }
327
+ chip.classList.add("active");
328
+
329
+ var type = chip.dataset.type;
330
+ if (type === "project") {
331
+ var slug = chip.dataset.slug;
332
+ var isDmNow = !!getCurrentDmUserId();
333
+ if (slug !== getCachedCurrentSlug() || isDmNow) {
334
+ // Switch project (or exit DM back to same project)
335
+ sessionListEl.innerHTML = "";
336
+ if (slug !== getCachedCurrentSlug()) {
337
+ var loading = document.createElement("div");
338
+ loading.className = "mobile-chat-context-note";
339
+ loading.textContent = "Loading sessions...";
340
+ sessionListEl.appendChild(loading);
341
+ }
342
+ if (_ctx.switchProject) _ctx.switchProject(slug);
343
+ if (!isDmNow || slug !== getCachedCurrentSlug()) {
344
+ // renderSessionList will be called by WS, which calls refreshMobileChatSheet
345
+ } else {
346
+ // Exited DM, same project - render sessions now
347
+ renderSessionsForContext("project", slug, null);
348
+ }
349
+ } else {
350
+ renderSessionsForContext("project", slug, null);
351
+ }
352
+ } else if (type === "mate") {
353
+ renderSessionsForContext("mate", null, chip.dataset.mateId);
354
+ }
355
+ });
356
+ })(chips[j]);
357
+ }
358
+
359
+ // Track that chat sheet is open
360
+ mobileChatSheetOpen = true;
361
+
362
+ // --- Initial render: show mate actions if DM active, otherwise project sessions ---
363
+ if (getCurrentDmUserId()) {
364
+ renderSessionsForContext("mate", null, getCurrentDmUserId());
365
+ } else {
366
+ renderSessionsForContext("project", getCachedCurrentSlug(), null);
367
+ }
368
+ }
369
+
370
+ // Helper: create a mobile session item element
371
+ function createMobileSessionItem(s) {
372
+ var el = document.createElement("button");
373
+ el.className = "mobile-session-item" + (s.active ? " active" : "");
374
+
375
+ // Processing dot (left side, before title)
376
+ if (s.isProcessing) {
377
+ var dot = document.createElement("span");
378
+ dot.className = "mobile-session-processing";
379
+ el.appendChild(dot);
380
+ }
381
+
382
+ var titleSpan = document.createElement("span");
383
+ titleSpan.className = "mobile-session-title";
384
+ titleSpan.textContent = s.title || "New Session";
385
+ el.appendChild(titleSpan);
386
+
387
+ // Unread badge (right side)
388
+ if (s.unread > 0 && !s.active) {
389
+ var badge = document.createElement("span");
390
+ badge.className = "mobile-session-unread";
391
+ badge.textContent = s.unread > 99 ? "99+" : String(s.unread);
392
+ el.appendChild(badge);
393
+ }
394
+
395
+ (function (id) {
396
+ el.addEventListener("click", function () {
397
+ if (_ctx.ws && _ctx.connected) {
398
+ _ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
399
+ }
400
+ if (_ctx.dismissOverlayPanels) _ctx.dismissOverlayPanels();
401
+ closeMobileSheet();
402
+ });
403
+ })(s.id);
404
+
405
+ return el;
406
+ }
407
+
408
+ // Helper: create a mobile loop child element (individual session inside a group)
409
+ function createMobileLoopChild(s) {
410
+ var el = document.createElement("button");
411
+ el.className = "mobile-loop-child" + (s.active ? " active" : "");
412
+
413
+ if (s.isProcessing) {
414
+ var dot = document.createElement("span");
415
+ dot.className = "mobile-session-processing";
416
+ el.appendChild(dot);
417
+ }
418
+
419
+ var textSpan = document.createElement("span");
420
+ textSpan.className = "mobile-session-title";
421
+ if (s.loop) {
422
+ var isRalphChild = s.loop.source === "ralph";
423
+ var roleName = s.loop.role === "crafting" ? "Crafting" : s.loop.role === "judge" ? "Judge" : (isRalphChild ? "Coder" : "Run");
424
+ var iterSuffix = s.loop.role === "crafting" ? "" : " #" + s.loop.iteration;
425
+ var roleCls = s.loop.role === "crafting" ? " crafting" : (!isRalphChild ? " scheduled" : "");
426
+ var badge = document.createElement("span");
427
+ badge.className = "mobile-loop-role-badge" + roleCls;
428
+ badge.textContent = roleName + iterSuffix;
429
+ textSpan.appendChild(badge);
430
+ }
431
+ el.appendChild(textSpan);
432
+
433
+ (function (id) {
434
+ el.addEventListener("click", function () {
435
+ if (_ctx.ws && _ctx.connected) {
436
+ _ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
437
+ }
438
+ if (_ctx.dismissOverlayPanels) _ctx.dismissOverlayPanels();
439
+ closeMobileSheet();
440
+ });
441
+ })(s.id);
442
+
443
+ return el;
444
+ }
445
+
446
+ // Helper: create a mobile loop run sub-group (collapsible time group)
447
+ function createMobileLoopRun(parentGk, startedAtKey, sessions, isRalph) {
448
+ var runGk = parentGk + ":" + startedAtKey;
449
+ var expanded = expandedMobileLoopRuns.has(runGk);
450
+ var startedAt = Number(startedAtKey);
451
+ var timeLabel = startedAt ? new Date(startedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "Unknown";
452
+
453
+ var hasActive = false;
454
+ var anyProcessing = false;
455
+ var latestSession = sessions[0];
456
+ for (var i = 0; i < sessions.length; i++) {
457
+ if (sessions[i].active) hasActive = true;
458
+ if (sessions[i].isProcessing) anyProcessing = true;
459
+ if ((sessions[i].lastActivity || 0) > (latestSession.lastActivity || 0)) {
460
+ latestSession = sessions[i];
461
+ }
462
+ }
463
+
464
+ var wrapper = document.createElement("div");
465
+ wrapper.className = "mobile-loop-run-wrapper";
466
+
467
+ var header = document.createElement("button");
468
+ header.className = "mobile-loop-run" + (hasActive ? " active" : "") + (expanded ? " expanded" : "") + (isRalph ? "" : " scheduled");
469
+
470
+ var chevron = document.createElement("span");
471
+ chevron.className = "mobile-loop-chevron";
472
+ chevron.innerHTML = iconHtml("chevron-right");
473
+ header.appendChild(chevron);
474
+
475
+ var label = document.createElement("span");
476
+ label.className = "mobile-loop-run-time";
477
+ var labelHtml = "";
478
+ if (anyProcessing) {
479
+ labelHtml += '<span class="mobile-session-processing"></span> ';
480
+ }
481
+ labelHtml += escapeHtml(timeLabel);
482
+ label.innerHTML = labelHtml;
483
+ header.appendChild(label);
484
+
485
+ var countBadge = document.createElement("span");
486
+ countBadge.className = "mobile-loop-count" + (isRalph ? "" : " scheduled");
487
+ countBadge.textContent = String(sessions.length);
488
+ header.appendChild(countBadge);
489
+
490
+ header.addEventListener("click", (function (rk) {
491
+ return function (e) {
492
+ e.stopPropagation();
493
+ if (expandedMobileLoopRuns.has(rk)) {
494
+ expandedMobileLoopRuns.delete(rk);
495
+ } else {
496
+ expandedMobileLoopRuns.add(rk);
497
+ }
498
+ refreshMobileChatSheet();
499
+ };
500
+ })(runGk));
501
+
502
+ wrapper.appendChild(header);
503
+
504
+ if (expanded) {
505
+ var childContainer = document.createElement("div");
506
+ childContainer.className = "mobile-loop-children";
507
+ for (var k = 0; k < sessions.length; k++) {
508
+ childContainer.appendChild(createMobileLoopChild(sessions[k]));
509
+ }
510
+ wrapper.appendChild(childContainer);
511
+ }
512
+
513
+ return wrapper;
514
+ }
515
+
516
+ // Helper: create a mobile loop group element (collapsible group header)
517
+ function createMobileLoopGroup(loopId, children, groupKey) {
518
+ var gk = groupKey || loopId;
519
+
520
+ // Sub-group children by startedAt (each run)
521
+ var runMap = {};
522
+ for (var i = 0; i < children.length; i++) {
523
+ var runKey = String(children[i].loop && children[i].loop.startedAt || 0);
524
+ if (!runMap[runKey]) runMap[runKey] = [];
525
+ runMap[runKey].push(children[i]);
526
+ }
527
+ var runKeys = Object.keys(runMap);
528
+
529
+ // Sort each run's children by iteration then role
530
+ for (var ri = 0; ri < runKeys.length; ri++) {
531
+ runMap[runKeys[ri]].sort(function (a, b) {
532
+ var ai = (a.loop && a.loop.iteration) || 0;
533
+ var bi = (b.loop && b.loop.iteration) || 0;
534
+ if (ai !== bi) return ai - bi;
535
+ var ar = (a.loop && a.loop.role === "judge") ? 1 : 0;
536
+ var br = (b.loop && b.loop.role === "judge") ? 1 : 0;
537
+ return ar - br;
538
+ });
539
+ }
540
+
541
+ // Sort runs by startedAt descending (newest first)
542
+ runKeys.sort(function (a, b) { return Number(b) - Number(a); });
543
+
544
+ var expanded = expandedMobileLoopGroups.has(gk);
545
+ var hasActive = false;
546
+ var anyProcessing = false;
547
+ var latestSession = children[0];
548
+ for (var ci = 0; ci < children.length; ci++) {
549
+ if (children[ci].active) hasActive = true;
550
+ if (children[ci].isProcessing) anyProcessing = true;
551
+ if ((children[ci].lastActivity || 0) > (latestSession.lastActivity || 0)) {
552
+ latestSession = children[ci];
553
+ }
554
+ }
555
+
556
+ var loopName = (children[0].loop && children[0].loop.name) || "Loop";
557
+ var isRalph = children[0].loop && children[0].loop.source === "ralph";
558
+ var isCrafting = false;
559
+ for (var j = 0; j < children.length; j++) {
560
+ if (children[j].loop && children[j].loop.role === "crafting") isCrafting = true;
561
+ }
562
+ var runCount = runKeys.length;
563
+
564
+ var wrapper = document.createElement("div");
565
+ wrapper.className = "mobile-loop-wrapper";
566
+
567
+ // Group header row
568
+ var header = document.createElement("button");
569
+ header.className = "mobile-loop-group" + (hasActive ? " active" : "") + (expanded ? " expanded" : "") + (isRalph ? "" : " scheduled");
570
+
571
+ var chevron = document.createElement("span");
572
+ chevron.className = "mobile-loop-chevron";
573
+ chevron.innerHTML = iconHtml("chevron-right");
574
+ header.appendChild(chevron);
575
+
576
+ var iconSpan = document.createElement("span");
577
+ var groupIcon = isRalph ? "repeat" : "calendar-clock";
578
+ iconSpan.className = "mobile-loop-icon" + (isRalph ? "" : " scheduled");
579
+ iconSpan.innerHTML = iconHtml(groupIcon);
580
+ header.appendChild(iconSpan);
581
+
582
+ if (anyProcessing) {
583
+ var dot = document.createElement("span");
584
+ dot.className = "mobile-session-processing";
585
+ header.appendChild(dot);
586
+ }
587
+
588
+ var nameSpan = document.createElement("span");
589
+ nameSpan.className = "mobile-loop-name";
590
+ nameSpan.textContent = loopName;
591
+ header.appendChild(nameSpan);
592
+
593
+ if (isCrafting && children.length === 1) {
594
+ var craftBadge = document.createElement("span");
595
+ craftBadge.className = "mobile-loop-badge crafting";
596
+ craftBadge.textContent = "Crafting";
597
+ header.appendChild(craftBadge);
598
+ } else {
599
+ var countBadge = document.createElement("span");
600
+ countBadge.className = "mobile-loop-count" + (isRalph ? "" : " scheduled");
601
+ var countLabel = runCount === 1 ? String(children.length) : runCount + (runCount === 1 ? " run" : " runs");
602
+ countBadge.textContent = countLabel;
603
+ header.appendChild(countBadge);
604
+ }
605
+
606
+ // Chevron toggles expansion
607
+ header.addEventListener("click", (function (lid) {
608
+ return function (e) {
609
+ e.stopPropagation();
610
+ if (expandedMobileLoopGroups.has(lid)) {
611
+ expandedMobileLoopGroups.delete(lid);
612
+ } else {
613
+ expandedMobileLoopGroups.add(lid);
614
+ }
615
+ refreshMobileChatSheet();
616
+ };
617
+ })(gk));
618
+
619
+ wrapper.appendChild(header);
620
+
621
+ // Expanded: show runs
622
+ if (expanded) {
623
+ var childContainer = document.createElement("div");
624
+ childContainer.className = "mobile-loop-children";
625
+
626
+ if (runCount === 1) {
627
+ var singleRun = runMap[runKeys[0]];
628
+ for (var sk = 0; sk < singleRun.length; sk++) {
629
+ childContainer.appendChild(createMobileLoopChild(singleRun[sk]));
630
+ }
631
+ } else {
632
+ for (var rk = 0; rk < runKeys.length; rk++) {
633
+ childContainer.appendChild(createMobileLoopRun(gk, runKeys[rk], runMap[runKeys[rk]], isRalph));
634
+ }
635
+ }
636
+
637
+ wrapper.appendChild(childContainer);
638
+ }
639
+
640
+ return wrapper;
641
+ }
642
+
643
+ function renderMateMobileActions(container) {
644
+ var newSessionBtn = document.createElement("button");
645
+ newSessionBtn.className = "mobile-session-new";
646
+ newSessionBtn.innerHTML = '<i data-lucide="plus" style="width:16px;height:16px"></i> New session';
647
+ newSessionBtn.addEventListener("click", function () {
648
+ if (_ctx.ws && _ctx.connected) {
649
+ _ctx.ws.send(JSON.stringify({ type: "new_session" }));
650
+ }
651
+ closeMobileSheet();
652
+ });
653
+ container.appendChild(newSessionBtn);
654
+
655
+ var debateBtn = document.createElement("button");
656
+ debateBtn.className = "mobile-session-new";
657
+ debateBtn.innerHTML = '<i data-lucide="mic" style="width:16px;height:16px"></i> New debate';
658
+ debateBtn.addEventListener("click", function () {
659
+ closeMobileSheet();
660
+ var targetBtn = document.getElementById("mate-debate-btn");
661
+ if (targetBtn) setTimeout(function () { targetBtn.click(); }, 250);
662
+ });
663
+ container.appendChild(debateBtn);
664
+
665
+ // Render mate session list
666
+ var mateSessions = getMateSessions();
667
+ if (mateSessions.length > 0) {
668
+ var sorted = mateSessions.slice().sort(function (a, b) {
669
+ return (b.lastActivity || 0) - (a.lastActivity || 0);
670
+ });
671
+
672
+ var currentGroup = "";
673
+ for (var i = 0; i < sorted.length; i++) {
674
+ var s = sorted[i];
675
+ var group = getDateGroup(s.lastActivity || 0);
676
+ if (group !== currentGroup) {
677
+ currentGroup = group;
678
+ var header = document.createElement("div");
679
+ header.className = "mobile-sheet-group";
680
+ header.textContent = group;
681
+ container.appendChild(header);
682
+ }
683
+ var mateItem = createMobileSessionItem(s);
684
+ container.appendChild(mateItem);
685
+ }
686
+ }
687
+
688
+ refreshIcons();
689
+ }
690
+
691
+ // Helper: render sorted sessions into a container with date groups (with loop session grouping)
692
+ function renderMobileSessionsInto(container) {
693
+ var newBtn = document.createElement("button");
694
+ newBtn.className = "mobile-session-new";
695
+ newBtn.innerHTML = '<i data-lucide="plus" style="width:16px;height:16px"></i> New session';
696
+ newBtn.addEventListener("click", function () {
697
+ if (_ctx.ws && _ctx.connected) {
698
+ _ctx.ws.send(JSON.stringify({ type: "new_session" }));
699
+ }
700
+ closeMobileSheet();
701
+ });
702
+ container.appendChild(newBtn);
703
+
704
+ var importBtn = document.createElement("button");
705
+ importBtn.className = "mobile-session-new";
706
+ importBtn.innerHTML = '<i data-lucide="import" style="width:16px;height:16px"></i> Import session';
707
+ importBtn.addEventListener("click", function () {
708
+ closeMobileSheet();
709
+ var targetBtn = document.getElementById("resume-session-btn");
710
+ if (targetBtn) setTimeout(function () { targetBtn.click(); }, 250);
711
+ });
712
+ container.appendChild(importBtn);
713
+
714
+ // Partition: loop sessions vs normal sessions (same logic as desktop renderSessionList)
715
+ var sessions = getCachedSessions();
716
+ var loopGroups = {};
717
+ var normalSessions = [];
718
+ for (var i = 0; i < sessions.length; i++) {
719
+ var s = sessions[i];
720
+ if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph" && s.loop.source !== "debate") {
721
+ continue;
722
+ } else if (s.loop && s.loop.loopId) {
723
+ var startedAt = s.loop.startedAt || 0;
724
+ var dateStr = startedAt ? new Date(startedAt).toISOString().slice(0, 10) : "unknown";
725
+ var groupKey = s.loop.loopId + ":" + dateStr;
726
+ if (!loopGroups[groupKey]) loopGroups[groupKey] = [];
727
+ loopGroups[groupKey].push(s);
728
+ } else {
729
+ normalSessions.push(s);
730
+ }
731
+ }
732
+
733
+ // Build virtual items
734
+ var items = [];
735
+ for (var j = 0; j < normalSessions.length; j++) {
736
+ items.push({ type: "session", data: normalSessions[j], lastActivity: normalSessions[j].lastActivity || 0 });
737
+ }
738
+ var groupKeys = Object.keys(loopGroups);
739
+ for (var k = 0; k < groupKeys.length; k++) {
740
+ var gk = groupKeys[k];
741
+ var children = loopGroups[gk];
742
+ var realLoopId = children[0].loop.loopId;
743
+ var maxActivity = 0;
744
+ for (var m = 0; m < children.length; m++) {
745
+ var act = children[m].lastActivity || 0;
746
+ if (act > maxActivity) maxActivity = act;
747
+ }
748
+ items.push({ type: "loop", loopId: realLoopId, groupKey: gk, children: children, lastActivity: maxActivity });
749
+ }
750
+
751
+ // Sort by lastActivity descending
752
+ items.sort(function (a, b) {
753
+ return (b.lastActivity || 0) - (a.lastActivity || 0);
754
+ });
755
+
756
+ var currentGroup = "";
757
+ for (var n = 0; n < items.length; n++) {
758
+ var item = items[n];
759
+ var group = getDateGroup(item.lastActivity || 0);
760
+ if (group !== currentGroup) {
761
+ currentGroup = group;
762
+ var header = document.createElement("div");
763
+ header.className = "mobile-sheet-group";
764
+ header.textContent = group;
765
+ container.appendChild(header);
766
+ }
767
+ if (item.type === "loop") {
768
+ container.appendChild(createMobileLoopGroup(item.loopId, item.children, item.groupKey));
769
+ } else {
770
+ container.appendChild(createMobileSessionItem(item.data));
771
+ }
772
+ }
773
+ }
774
+
775
+ // Refresh mobile chat sheet when session data updates (called from renderSessionList)
776
+ export function refreshMobileChatSheet() {
777
+ if (!mobileChatSheetOpen) return;
778
+ var sheet = document.getElementById("mobile-sheet");
779
+ if (!sheet || sheet.classList.contains("hidden")) {
780
+ mobileChatSheetOpen = false;
781
+ return;
782
+ }
783
+ var sessionListEl = sheet.querySelector(".mobile-chat-session-list");
784
+ if (!sessionListEl) return;
785
+
786
+ // Update chips: active state and processing dots
787
+ var chips = sheet.querySelectorAll(".mobile-chat-chip");
788
+ for (var i = 0; i < chips.length; i++) {
789
+ var chip = chips[i];
790
+ chip.classList.remove("active");
791
+
792
+ // Update active state
793
+ var isDmActive = !!getCurrentDmUserId();
794
+ if (chip.dataset.type === "project" && chip.dataset.slug === getCachedCurrentSlug() && !isDmActive) {
795
+ chip.classList.add("active");
796
+ } else if (chip.dataset.type === "mate" && chip.dataset.mateId === getCurrentDmUserId()) {
797
+ chip.classList.add("active");
798
+ }
799
+
800
+ // Update processing dot: same class as icon strip
801
+ var statusDot = chip.querySelector(".icon-strip-status");
802
+ if (statusDot) {
803
+ var isProcessing = false;
804
+ var allProjects = (_ctx && _ctx.projectList) || [];
805
+ var lookupSlug = chip.dataset.type === "mate" ? ("mate-" + chip.dataset.mateId) : chip.dataset.slug;
806
+ for (var pi = 0; pi < allProjects.length; pi++) {
807
+ if (allProjects[pi].slug === lookupSlug && allProjects[pi].isProcessing) {
808
+ isProcessing = true;
809
+ break;
810
+ }
811
+ }
812
+ statusDot.classList.toggle("processing", isProcessing);
813
+ }
814
+ }
815
+
816
+ // Re-render sessions for current context
817
+ sessionListEl.innerHTML = "";
818
+ if (getCurrentDmUserId()) {
819
+ renderMateMobileActions(sessionListEl);
820
+ } else {
821
+ renderMobileSessionsInto(sessionListEl);
822
+ }
823
+
824
+ refreshIcons();
825
+ }
826
+
827
+ function renderSheetMateProfile(listEl) {
828
+ if (!mobileSheetMateData) return;
829
+ var data = mobileSheetMateData;
830
+
831
+ // Profile header
832
+ var header = document.createElement("div");
833
+ header.className = "mate-profile-header";
834
+
835
+ var avatar = document.createElement("img");
836
+ avatar.className = "mate-profile-avatar";
837
+ avatar.src = data.avatarUrl || "";
838
+ avatar.alt = data.displayName || "";
839
+ header.appendChild(avatar);
840
+
841
+ var info = document.createElement("div");
842
+ info.className = "mate-profile-info";
843
+ var nameEl = document.createElement("div");
844
+ nameEl.className = "mate-profile-name";
845
+ nameEl.textContent = data.displayName || "";
846
+ info.appendChild(nameEl);
847
+ if (data.description) {
848
+ var descEl = document.createElement("div");
849
+ descEl.className = "mate-profile-desc";
850
+ descEl.textContent = data.description;
851
+ info.appendChild(descEl);
852
+ }
853
+ header.appendChild(info);
854
+ listEl.appendChild(header);
855
+
856
+ // Action buttons
857
+ var actions = [
858
+ { icon: "book-open", label: "Knowledge", btnId: "mate-knowledge-btn", countId: "mate-knowledge-count" },
859
+ { icon: "sticky-note", label: "Sticky Notes", btnId: "sticky-notes-toggle-btn", countId: "sticky-notes-sidebar-count" },
860
+ { icon: "puzzle", label: "Skills", btnId: "mate-skills-btn" },
861
+ { icon: "calendar", label: "Scheduled Tasks", btnId: "mate-scheduler-btn" }
862
+ ];
863
+
864
+ for (var i = 0; i < actions.length; i++) {
865
+ (function (action) {
866
+ var btn = document.createElement("button");
867
+ btn.className = "mate-profile-action";
868
+ var countHtml = "";
869
+ if (action.countId) {
870
+ var countEl = document.getElementById(action.countId);
871
+ if (countEl && !countEl.classList.contains("hidden") && countEl.textContent) {
872
+ countHtml = '<span class="mate-profile-action-count">' + escapeHtml(countEl.textContent) + '</span>';
873
+ }
874
+ }
875
+ btn.innerHTML = '<i data-lucide="' + action.icon + '"></i><span>' + action.label + '</span>' + countHtml;
876
+ btn.addEventListener("click", function () {
877
+ closeMobileSheet();
878
+ var targetBtn = document.getElementById(action.btnId);
879
+ if (targetBtn) {
880
+ setTimeout(function () { targetBtn.click(); }, 250);
881
+ }
882
+ });
883
+ listEl.appendChild(btn);
884
+ })(actions[i]);
885
+ }
886
+ }
887
+
888
+ function renderSheetSearch(listEl) {
889
+ // Search input at top
890
+ var wrap = document.createElement("div");
891
+ wrap.className = "mobile-search-input-wrap";
892
+ var input = document.createElement("input");
893
+ input.className = "mobile-search-input";
894
+ input.type = "text";
895
+ input.placeholder = "Search sessions, messages...";
896
+ input.autocomplete = "off";
897
+ input.spellcheck = false;
898
+ wrap.appendChild(input);
899
+ listEl.appendChild(wrap);
900
+
901
+ // Results container
902
+ var resultsEl = document.createElement("div");
903
+ resultsEl.style.padding = "0 8px";
904
+ listEl.appendChild(resultsEl);
905
+
906
+ // Auto-focus
907
+ setTimeout(function () { input.focus(); }, 300);
908
+
909
+ // Show all sessions initially
910
+ renderSearchResults(resultsEl, "");
911
+
912
+ input.addEventListener("input", function () {
913
+ var q = input.value.trim().toLowerCase();
914
+ renderSearchResults(resultsEl, q);
915
+ });
916
+ input.addEventListener("keydown", function (e) { e.stopPropagation(); });
917
+ input.addEventListener("keyup", function (e) { e.stopPropagation(); });
918
+ input.addEventListener("keypress", function (e) { e.stopPropagation(); });
919
+ }
920
+
921
+ function renderSearchResults(container, query) {
922
+ container.innerHTML = "";
923
+ var sorted = getCachedSessions().slice().sort(function (a, b) {
924
+ return (b.lastActivity || 0) - (a.lastActivity || 0);
925
+ });
926
+
927
+ var found = 0;
928
+ for (var i = 0; i < sorted.length; i++) {
929
+ var s = sorted[i];
930
+ var title = s.title || "New Session";
931
+ if (query && title.toLowerCase().indexOf(query) === -1) continue;
932
+ found++;
933
+
934
+ var el = document.createElement("button");
935
+ el.className = "mobile-session-item";
936
+ if (s.active) el.classList.add("active");
937
+
938
+ var titleSpan = document.createElement("span");
939
+ titleSpan.className = "mobile-session-title";
940
+ titleSpan.textContent = title;
941
+ el.appendChild(titleSpan);
942
+
943
+ if (s.isProcessing) {
944
+ var dot = document.createElement("span");
945
+ dot.className = "mobile-session-processing";
946
+ el.appendChild(dot);
947
+ }
948
+
949
+ (function (id) {
950
+ el.addEventListener("click", function () {
951
+ if (_ctx.ws && _ctx.connected) {
952
+ _ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
953
+ }
954
+ if (_ctx.dismissOverlayPanels) _ctx.dismissOverlayPanels();
955
+ closeMobileSheet();
956
+ });
957
+ })(s.id);
958
+
959
+ container.appendChild(el);
960
+ }
961
+
962
+ if (found === 0 && query) {
963
+ var empty = document.createElement("div");
964
+ empty.className = "mobile-alert-empty";
965
+ empty.textContent = 'No results for "' + query + '"';
966
+ container.appendChild(empty);
967
+ }
968
+ }
969
+
970
+ function renderSheetTools(listEl) {
971
+ var isMateDm = document.body.classList.contains("mate-dm-active");
972
+
973
+ var items = isMateDm ? [
974
+ { icon: "brain", label: "Memory", action: "mate-memory" },
975
+ { icon: "book-open", label: "Knowledge", action: "mate-knowledge" },
976
+ { icon: "sticky-note", label: "Sticky Notes", action: "mate-sticky" },
977
+ { icon: "puzzle", label: "Skills", action: "mate-skills" },
978
+ { icon: "calendar-clock", label: "Scheduled Tasks", action: "mate-scheduler" }
979
+ ] : [
980
+ { icon: "folder-tree", label: "Files", action: "files" },
981
+ { icon: "square-terminal", label: "Terminal", action: "terminal" },
982
+ { icon: "calendar-clock", label: "Scheduled Tasks", action: "scheduler" }
983
+ ];
984
+
985
+ for (var i = 0; i < items.length; i++) {
986
+ (function (item) {
987
+ var btn = document.createElement("button");
988
+ btn.className = "mobile-more-item";
989
+ btn.innerHTML = '<i data-lucide="' + item.icon + '"></i><span class="mobile-more-item-label">' + item.label + '</span>';
990
+ btn.addEventListener("click", function () {
991
+ closeMobileSheet();
992
+ var targetId = null;
993
+ if (item.action === "files") {
994
+ setTimeout(function () { openMobileSheet("files"); }, 250);
995
+ } else if (item.action === "terminal") {
996
+ if (_ctx.openTerminal) _ctx.openTerminal();
997
+ } else if (item.action === "scheduler") {
998
+ targetId = "scheduler-btn";
999
+ } else if (item.action === "mate-knowledge") {
1000
+ setTimeout(function () { openMobileSheet("mate-knowledge"); }, 250);
1001
+ return;
1002
+ } else if (item.action === "mate-sticky") {
1003
+ targetId = "mate-sticky-notes-btn";
1004
+ } else if (item.action === "mate-skills") {
1005
+ targetId = "mate-skills-btn";
1006
+ } else if (item.action === "mate-memory") {
1007
+ targetId = "mate-memory-btn";
1008
+ } else if (item.action === "mate-scheduler") {
1009
+ targetId = "mate-scheduler-btn";
1010
+ } else if (item.action === "mate-debate") {
1011
+ targetId = "mate-debate-btn";
1012
+ }
1013
+ if (targetId) {
1014
+ var targetBtn = document.getElementById(targetId);
1015
+ if (targetBtn) setTimeout(function () { targetBtn.click(); }, 250);
1016
+ }
1017
+ });
1018
+ listEl.appendChild(btn);
1019
+ })(items[i]);
1020
+ }
1021
+ }
1022
+
1023
+ function renderSheetSettings(listEl) {
1024
+ var items = [
1025
+ { icon: "folder-cog", label: "Project Settings", action: "project-settings" },
1026
+ { icon: "settings", label: "Server Settings", action: "server-settings" }
1027
+ ];
1028
+
1029
+ for (var i = 0; i < items.length; i++) {
1030
+ (function (item) {
1031
+ var btn = document.createElement("button");
1032
+ btn.className = "mobile-more-item";
1033
+ btn.innerHTML = '<i data-lucide="' + item.icon + '"></i><span class="mobile-more-item-label">' + item.label + '</span>';
1034
+ btn.addEventListener("click", function () {
1035
+ closeMobileSheet();
1036
+ if (item.action === "project-settings") {
1037
+ setTimeout(function () {
1038
+ // Find current project data
1039
+ var proj = null;
1040
+ for (var pi = 0; pi < getCachedProjectList().length; pi++) {
1041
+ if (getCachedProjectList()[pi].slug === getCachedCurrentSlug()) {
1042
+ proj = getCachedProjectList()[pi];
1043
+ break;
1044
+ }
1045
+ }
1046
+ // For mate projects, use mate display name instead of slug
1047
+ if (proj && proj.isMate && getCachedMates().length > 0) {
1048
+ var mateId = getCachedCurrentSlug().replace("mate-", "");
1049
+ var _mates = getCachedMates();
1050
+ for (var mi = 0; mi < _mates.length; mi++) {
1051
+ var mp = _mates[mi].profile || {};
1052
+ if (_mates[mi].id === mateId) {
1053
+ proj = Object.assign({}, proj, { name: mp.displayName || _mates[mi].name || proj.name });
1054
+ break;
1055
+ }
1056
+ }
1057
+ }
1058
+ openProjectSettings(getCachedCurrentSlug(), proj);
1059
+ }, 250);
1060
+ } else if (item.action === "server-settings") {
1061
+ var settingsBtn = document.getElementById("server-settings-btn");
1062
+ if (settingsBtn) setTimeout(function () { settingsBtn.click(); }, 250);
1063
+ }
1064
+ });
1065
+ listEl.appendChild(btn);
1066
+ })(items[i]);
1067
+ }
1068
+
1069
+ // Dark/Light switch button
1070
+ var isDark = getCurrentTheme().variant === "dark";
1071
+ var themeBtn = document.createElement("button");
1072
+ themeBtn.className = "mobile-more-item";
1073
+ themeBtn.innerHTML = '<i data-lucide="' + (isDark ? "sun" : "moon") + '"></i><span class="mobile-more-item-label">Switch to ' + (isDark ? "Light" : "Dark") + '</span>';
1074
+
1075
+ themeBtn.addEventListener("click", function () {
1076
+ var themeToggle = document.getElementById("theme-toggle-check");
1077
+ if (themeToggle) themeToggle.click();
1078
+ // Update button text after a tick (theme applies async)
1079
+ setTimeout(function () {
1080
+ var nowDark = getCurrentTheme().variant === "dark";
1081
+ themeBtn.innerHTML = '<i data-lucide="' + (nowDark ? "sun" : "moon") + '"></i><span class="mobile-more-item-label">Switch to ' + (nowDark ? "Light" : "Dark") + '</span>';
1082
+ refreshIcons();
1083
+ }, 50);
1084
+ });
1085
+
1086
+ listEl.appendChild(themeBtn);
1087
+
1088
+ // Chat Layout switch button
1089
+ var currentLayout = getChatLayout();
1090
+ var isBubble = currentLayout === "bubble";
1091
+ var layoutBtn = document.createElement("button");
1092
+ layoutBtn.className = "mobile-more-item";
1093
+ layoutBtn.innerHTML = '<i data-lucide="' + (isBubble ? "monitor" : "message-circle") + '"></i>'
1094
+ + '<span class="mobile-more-item-label">Switch to ' + (isBubble ? "Channel" : "Bubble") + '</span>';
1095
+
1096
+ layoutBtn.addEventListener("click", function () {
1097
+ var next = getChatLayout() === "bubble" ? "channel" : "bubble";
1098
+ setChatLayout(next);
1099
+ fetch('/api/user/chat-layout', {
1100
+ method: 'PUT',
1101
+ headers: { 'Content-Type': 'application/json' },
1102
+ body: JSON.stringify({ layout: next })
1103
+ });
1104
+ closeMobileSheet();
1105
+ });
1106
+
1107
+ listEl.appendChild(layoutBtn);
1108
+
1109
+ // "Open as app" -- only show if not already in PWA standalone mode
1110
+ if (!document.documentElement.classList.contains("pwa-standalone")) {
1111
+ var pwaBtn = document.createElement("button");
1112
+ pwaBtn.className = "mobile-more-item";
1113
+ pwaBtn.innerHTML = '<i data-lucide="smartphone"></i><span class="mobile-more-item-label">Open as app</span>';
1114
+ pwaBtn.addEventListener("click", function () {
1115
+ closeMobileSheet();
1116
+ // Trigger the existing PWA install modal
1117
+ var installPill = document.getElementById("pwa-install-pill");
1118
+ if (installPill) {
1119
+ setTimeout(function () { installPill.click(); }, 250);
1120
+ }
1121
+ });
1122
+ listEl.appendChild(pwaBtn);
1123
+ }
1124
+ }
1125
+
1126
+ export function initSidebarMobile(ctx) {
1127
+ _ctx = ctx;
1128
+
1129
+ // Put refreshMobileChatSheet on ctx for external callers
1130
+ ctx.refreshMobileChatSheet = refreshMobileChatSheet;
1131
+
1132
+ // --- Mobile sheet close handlers ---
1133
+ var mobileSheet = document.getElementById("mobile-sheet");
1134
+ if (mobileSheet) {
1135
+ var sheetBackdrop = mobileSheet.querySelector(".mobile-sheet-backdrop");
1136
+ var sheetCloseBtn = mobileSheet.querySelector(".mobile-sheet-close");
1137
+ if (sheetBackdrop) sheetBackdrop.addEventListener("click", closeMobileSheet);
1138
+ if (sheetCloseBtn) sheetCloseBtn.addEventListener("click", closeMobileSheet);
1139
+
1140
+ // --- Drag to dismiss sheet ---
1141
+ var sheetHandle = mobileSheet.querySelector(".mobile-sheet-handle");
1142
+ var sheetContent = mobileSheet.querySelector(".mobile-sheet-content");
1143
+ if (sheetHandle && sheetContent) {
1144
+ var dragStartY = 0;
1145
+ var dragging = false;
1146
+
1147
+ sheetHandle.addEventListener("touchstart", function (e) {
1148
+ dragStartY = e.touches[0].clientY;
1149
+ dragging = true;
1150
+ sheetContent.style.transition = "none";
1151
+ }, { passive: true });
1152
+
1153
+ mobileSheet.addEventListener("touchmove", function (e) {
1154
+ if (!dragging) return;
1155
+ var deltaY = e.touches[0].clientY - dragStartY;
1156
+ if (deltaY < 0) deltaY = 0;
1157
+ sheetContent.style.transform = "translateY(" + deltaY + "px)";
1158
+ if (sheetBackdrop) {
1159
+ var opacity = Math.max(0, 1 - deltaY / (sheetContent.offsetHeight * 0.5));
1160
+ sheetBackdrop.style.opacity = opacity;
1161
+ }
1162
+ }, { passive: true });
1163
+
1164
+ mobileSheet.addEventListener("touchend", function () {
1165
+ if (!dragging) return;
1166
+ dragging = false;
1167
+ var currentY = parseFloat(sheetContent.style.transform.replace(/[^0-9.-]/g, "")) || 0;
1168
+ var threshold = sheetContent.offsetHeight * 0.3;
1169
+
1170
+ if (currentY > threshold) {
1171
+ sheetContent.style.transition = "transform 0.22s ease-in";
1172
+ sheetContent.style.transform = "translateY(100%)";
1173
+ if (sheetBackdrop) {
1174
+ sheetBackdrop.style.transition = "opacity 0.22s ease-in";
1175
+ sheetBackdrop.style.opacity = "0";
1176
+ }
1177
+ setTimeout(function () {
1178
+ sheetContent.style.transition = "";
1179
+ sheetContent.style.transform = "";
1180
+ if (sheetBackdrop) {
1181
+ sheetBackdrop.style.transition = "";
1182
+ sheetBackdrop.style.opacity = "";
1183
+ }
1184
+ // Close without animation since we already animated
1185
+ var sheet = document.getElementById("mobile-sheet");
1186
+ if (sheet) {
1187
+ if (sheet.classList.contains("sheet-files")) {
1188
+ var fileTree = document.getElementById("file-tree");
1189
+ var sidebarFilesPanel = document.getElementById("sidebar-panel-files");
1190
+ if (fileTree && sidebarFilesPanel) {
1191
+ sidebarFilesPanel.appendChild(fileTree);
1192
+ }
1193
+ }
1194
+ sheet.classList.add("hidden");
1195
+ sheet.classList.remove("closing", "sheet-files");
1196
+ }
1197
+ }, 230);
1198
+ } else {
1199
+ sheetContent.style.transition = "transform 0.2s ease-out";
1200
+ sheetContent.style.transform = "translateY(0)";
1201
+ if (sheetBackdrop) {
1202
+ sheetBackdrop.style.transition = "opacity 0.2s ease-out";
1203
+ sheetBackdrop.style.opacity = "";
1204
+ }
1205
+ setTimeout(function () {
1206
+ sheetContent.style.transition = "";
1207
+ sheetContent.style.transform = "";
1208
+ if (sheetBackdrop) {
1209
+ sheetBackdrop.style.transition = "";
1210
+ sheetBackdrop.style.opacity = "";
1211
+ }
1212
+ }, 200);
1213
+ }
1214
+ }, { passive: true });
1215
+ }
1216
+ }
1217
+
1218
+ // --- Mobile tab bar ---
1219
+ var mobileTabBar = document.getElementById("mobile-tab-bar");
1220
+ var mobileTabs = mobileTabBar ? mobileTabBar.querySelectorAll(".mobile-tab") : [];
1221
+ var mobileHomeBtn = document.getElementById("mobile-home-btn");
1222
+
1223
+ function setMobileTabActive(tabName) {
1224
+ for (var i = 0; i < mobileTabs.length; i++) {
1225
+ if (mobileTabs[i].dataset.tab === tabName) {
1226
+ mobileTabs[i].classList.add("active");
1227
+ } else {
1228
+ mobileTabs[i].classList.remove("active");
1229
+ }
1230
+ }
1231
+ if (mobileHomeBtn) {
1232
+ if (tabName === "home") {
1233
+ mobileHomeBtn.classList.add("active");
1234
+ } else {
1235
+ mobileHomeBtn.classList.remove("active");
1236
+ }
1237
+ }
1238
+ }
1239
+
1240
+ for (var t = 0; t < mobileTabs.length; t++) {
1241
+ (function (tab) {
1242
+ tab.addEventListener("click", function () {
1243
+ var name = tab.dataset.tab;
1244
+
1245
+ if (name === "chat") {
1246
+ openMobileSheet("sessions");
1247
+ setMobileTabActive("chat");
1248
+ } else if (name === "search") {
1249
+ openCommandPalette();
1250
+ setMobileTabActive("search");
1251
+ } else if (name === "tools") {
1252
+ openMobileSheet("tools");
1253
+ setMobileTabActive("tools");
1254
+ } else if (name === "settings") {
1255
+ openMobileSheet("settings");
1256
+ setMobileTabActive("settings");
1257
+ }
1258
+ });
1259
+ })(mobileTabs[t]);
1260
+ }
1261
+
1262
+ if (mobileHomeBtn) {
1263
+ mobileHomeBtn.addEventListener("click", function () {
1264
+ if (_ctx.closeSidebar) _ctx.closeSidebar();
1265
+ setMobileTabActive("home");
1266
+ if (_ctx.showHomeHub) _ctx.showHomeHub();
1267
+ });
1268
+ }
1269
+ }