clay-server 2.27.0-beta.8 → 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 (72) 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-debate.js +19 -12
  10. package/lib/project-http.js +4 -2
  11. package/lib/project-loop.js +110 -48
  12. package/lib/project-mate-interaction.js +4 -0
  13. package/lib/project-notifications.js +210 -0
  14. package/lib/project-sessions.js +5 -2
  15. package/lib/project-user-message.js +2 -1
  16. package/lib/project.js +26 -2
  17. package/lib/public/app.js +1193 -8521
  18. package/lib/public/css/command-palette.css +14 -0
  19. package/lib/public/css/loop.css +301 -0
  20. package/lib/public/css/notifications-center.css +190 -0
  21. package/lib/public/css/rewind.css +6 -0
  22. package/lib/public/index.html +89 -35
  23. package/lib/public/modules/app-connection.js +160 -0
  24. package/lib/public/modules/app-cursors.js +473 -0
  25. package/lib/public/modules/app-debate-ui.js +389 -0
  26. package/lib/public/modules/app-dm.js +627 -0
  27. package/lib/public/modules/app-favicon.js +212 -0
  28. package/lib/public/modules/app-header.js +229 -0
  29. package/lib/public/modules/app-home-hub.js +600 -0
  30. package/lib/public/modules/app-loop-ui.js +589 -0
  31. package/lib/public/modules/app-loop-wizard.js +439 -0
  32. package/lib/public/modules/app-messages.js +1560 -0
  33. package/lib/public/modules/app-misc.js +299 -0
  34. package/lib/public/modules/app-notifications.js +372 -0
  35. package/lib/public/modules/app-panels.js +888 -0
  36. package/lib/public/modules/app-projects.js +798 -0
  37. package/lib/public/modules/app-rate-limit.js +451 -0
  38. package/lib/public/modules/app-rendering.js +597 -0
  39. package/lib/public/modules/app-skills-install.js +234 -0
  40. package/lib/public/modules/command-palette.js +27 -4
  41. package/lib/public/modules/input.js +31 -20
  42. package/lib/public/modules/scheduler-config.js +1532 -0
  43. package/lib/public/modules/scheduler-history.js +79 -0
  44. package/lib/public/modules/scheduler.js +33 -1554
  45. package/lib/public/modules/session-search.js +13 -1
  46. package/lib/public/modules/sidebar-mates.js +812 -0
  47. package/lib/public/modules/sidebar-mobile.js +1269 -0
  48. package/lib/public/modules/sidebar-projects.js +1449 -0
  49. package/lib/public/modules/sidebar-sessions.js +986 -0
  50. package/lib/public/modules/sidebar.js +232 -4591
  51. package/lib/public/modules/store.js +27 -0
  52. package/lib/public/modules/ws-ref.js +7 -0
  53. package/lib/public/style.css +1 -0
  54. package/lib/sdk-bridge.js +96 -717
  55. package/lib/sdk-message-processor.js +587 -0
  56. package/lib/sdk-message-queue.js +42 -0
  57. package/lib/sdk-skill-discovery.js +131 -0
  58. package/lib/server-admin.js +712 -0
  59. package/lib/server-auth.js +737 -0
  60. package/lib/server-dm.js +221 -0
  61. package/lib/server-mates.js +281 -0
  62. package/lib/server-palette.js +110 -0
  63. package/lib/server-settings.js +479 -0
  64. package/lib/server-skills.js +280 -0
  65. package/lib/server.js +246 -2755
  66. package/lib/sessions.js +11 -4
  67. package/lib/users-auth.js +146 -0
  68. package/lib/users-permissions.js +118 -0
  69. package/lib/users-preferences.js +210 -0
  70. package/lib/users.js +48 -398
  71. package/lib/ws-schema.js +498 -0
  72. package/package.json +1 -1
@@ -0,0 +1,798 @@
1
+ // app-projects.js - Project list, switching, add/remove project modals
2
+ // Extracted from app.js (PR-29)
3
+
4
+ import { escapeHtml } from './utils.js';
5
+ import { refreshIcons } from './icons.js';
6
+ import { parseEmojis } from './markdown.js';
7
+
8
+ var _ctx = null;
9
+
10
+ // --- Module-owned state ---
11
+ var cachedProjects = [];
12
+ var cachedProjectCount = 0;
13
+ var cachedRemovedProjects = [];
14
+ var pendingRemoveSlug = null;
15
+ var pendingRemoveName = null;
16
+
17
+ // Add-project modal state
18
+ var addProjectModal = null;
19
+ var addProjectInput = null;
20
+ var addProjectCreateInput = null;
21
+ var addProjectCloneInput = null;
22
+ var addProjectCloneProgress = null;
23
+ var addProjectSuggestions = null;
24
+ var addProjectError = null;
25
+ var addProjectOk = null;
26
+ var addProjectCancel = null;
27
+ var addProjectModeBtns = null;
28
+ var addProjectPanels = null;
29
+ var addProjectRemoved = null;
30
+ var addProjectDebounce = null;
31
+ var addProjectActiveIdx = -1;
32
+ var addProjectMode = "existing";
33
+
34
+ export function initProjects(ctx) {
35
+ _ctx = ctx;
36
+
37
+ // Init add-project modal DOM refs
38
+ addProjectModal = document.getElementById("add-project-modal");
39
+ addProjectInput = document.getElementById("add-project-input");
40
+ addProjectCreateInput = document.getElementById("add-project-create-input");
41
+ addProjectCloneInput = document.getElementById("add-project-clone-input");
42
+ addProjectCloneProgress = document.getElementById("add-project-clone-progress");
43
+ addProjectSuggestions = document.getElementById("add-project-suggestions");
44
+ addProjectError = document.getElementById("add-project-error");
45
+ addProjectOk = document.getElementById("add-project-ok");
46
+ addProjectCancel = document.getElementById("add-project-cancel");
47
+ addProjectModeBtns = addProjectModal.querySelectorAll(".add-project-mode-btn");
48
+ addProjectPanels = addProjectModal.querySelectorAll(".add-project-panel");
49
+ addProjectRemoved = document.getElementById("add-project-removed");
50
+
51
+ // Mode button click listeners
52
+ for (var mbi = 0; mbi < addProjectModeBtns.length; mbi++) {
53
+ addProjectModeBtns[mbi].addEventListener("click", function () {
54
+ if (this.disabled) return;
55
+ switchAddProjectMode(this.dataset.mode);
56
+ });
57
+ }
58
+
59
+ // Existing project input listeners
60
+ addProjectInput.addEventListener("focus", function () {
61
+ var val = addProjectInput.value;
62
+ if (val && addProjectSuggestions.children.length === 0) {
63
+ requestBrowseDir(val);
64
+ } else if (addProjectSuggestions.children.length > 0) {
65
+ addProjectSuggestions.classList.remove("hidden");
66
+ }
67
+ });
68
+
69
+ addProjectModal.querySelector(".confirm-dialog").addEventListener("click", function (e) {
70
+ if (e.target === addProjectInput || addProjectInput.contains(e.target)) return;
71
+ if (e.target === addProjectSuggestions || addProjectSuggestions.contains(e.target)) return;
72
+ addProjectSuggestions.classList.add("hidden");
73
+ addProjectActiveIdx = -1;
74
+ });
75
+
76
+ addProjectInput.addEventListener("input", function () {
77
+ var val = addProjectInput.value;
78
+ addProjectError.classList.add("hidden");
79
+ if (addProjectDebounce) clearTimeout(addProjectDebounce);
80
+ addProjectDebounce = setTimeout(function () {
81
+ requestBrowseDir(val);
82
+ }, 200);
83
+ });
84
+
85
+ addProjectInput.addEventListener("keydown", function (e) {
86
+ var items = addProjectSuggestions.querySelectorAll(".add-project-suggestion-item");
87
+
88
+ if (e.key === "ArrowDown") {
89
+ e.preventDefault();
90
+ if (items.length > 0) {
91
+ var next = addProjectActiveIdx < items.length - 1 ? addProjectActiveIdx + 1 : 0;
92
+ setActiveIdx(next);
93
+ }
94
+ return;
95
+ }
96
+
97
+ if (e.key === "ArrowUp") {
98
+ e.preventDefault();
99
+ if (items.length > 0) {
100
+ var prev = addProjectActiveIdx > 0 ? addProjectActiveIdx - 1 : items.length - 1;
101
+ setActiveIdx(prev);
102
+ }
103
+ return;
104
+ }
105
+
106
+ if (e.key === "Tab") {
107
+ e.preventDefault();
108
+ var target = addProjectActiveIdx >= 0 && addProjectActiveIdx < items.length
109
+ ? items[addProjectActiveIdx]
110
+ : items.length > 0 ? items[0] : null;
111
+ if (target) {
112
+ var p = target.dataset.path + "/";
113
+ addProjectInput.value = p;
114
+ addProjectError.classList.add("hidden");
115
+ requestBrowseDir(p);
116
+ }
117
+ return;
118
+ }
119
+
120
+ if (e.key === "Enter") {
121
+ e.preventDefault();
122
+ if (addProjectActiveIdx >= 0 && addProjectActiveIdx < items.length) {
123
+ var picked = items[addProjectActiveIdx].dataset.path + "/";
124
+ addProjectInput.value = picked;
125
+ addProjectError.classList.add("hidden");
126
+ requestBrowseDir(picked);
127
+ return;
128
+ }
129
+ submitAddProject();
130
+ return;
131
+ }
132
+
133
+ if (e.key === "Escape") {
134
+ e.preventDefault();
135
+ closeAddProjectModal();
136
+ return;
137
+ }
138
+ });
139
+
140
+ // Enter key on create/clone inputs
141
+ addProjectCreateInput.addEventListener("keydown", function (e) {
142
+ if (e.key === "Enter") { e.preventDefault(); submitAddProject(); }
143
+ if (e.key === "Escape") { e.preventDefault(); closeAddProjectModal(); }
144
+ });
145
+
146
+ addProjectCloneInput.addEventListener("keydown", function (e) {
147
+ if (e.key === "Enter") { e.preventDefault(); submitAddProject(); }
148
+ if (e.key === "Escape") { e.preventDefault(); closeAddProjectModal(); }
149
+ });
150
+
151
+ addProjectOk.addEventListener("click", function () { submitAddProject(); });
152
+ addProjectCancel.addEventListener("click", function () { closeAddProjectModal(); });
153
+
154
+ // Close on backdrop click
155
+ addProjectModal.querySelector(".confirm-backdrop").addEventListener("click", function () {
156
+ closeAddProjectModal();
157
+ });
158
+
159
+ // Project list add button
160
+ var projectListAddBtn = _ctx.$("project-list-add");
161
+ if (projectListAddBtn) {
162
+ projectListAddBtn.addEventListener("click", function () {
163
+ openAddProjectModal();
164
+ });
165
+ }
166
+ }
167
+
168
+ // --- State accessors ---
169
+
170
+ export function getCachedProjects() { return cachedProjects; }
171
+ export function setCachedProjects(v) { cachedProjects = v; }
172
+ export function getCachedProjectCount() { return cachedProjectCount; }
173
+ export function setCachedProjectCount(v) { cachedProjectCount = v; }
174
+ export function getCachedRemovedProjects() { return cachedRemovedProjects; }
175
+ export function setCachedRemovedProjects(v) { cachedRemovedProjects = v; }
176
+
177
+ // --- Functions ---
178
+
179
+ export function updateProjectList(msg) {
180
+ if (typeof msg.projectCount === "number") cachedProjectCount = msg.projectCount;
181
+ if (msg.projects) cachedProjects = msg.projects;
182
+ if (msg.removedProjects) cachedRemovedProjects = msg.removedProjects;
183
+ else if (msg.removedProjects === undefined) { /* keep cached */ }
184
+ else cachedRemovedProjects = [];
185
+ var count = cachedProjectCount || 0;
186
+ renderProjectList();
187
+ var projectHint = _ctx.$("project-hint");
188
+ if (count === 1 && projectHint) {
189
+ try {
190
+ if (!localStorage.getItem("clay-project-hint-dismissed")) {
191
+ projectHint.classList.remove("hidden");
192
+ }
193
+ } catch (e) {}
194
+ } else if (projectHint) {
195
+ projectHint.classList.add("hidden");
196
+ }
197
+ // Update topbar with server-wide presence
198
+ if (msg.serverUsers) {
199
+ var newOnlineIds = msg.serverUsers.map(function (u) { return u.id; });
200
+ var prevOnlineIds = _ctx.cachedOnlineIds || [];
201
+ _ctx.setCachedOnlineIds(newOnlineIds);
202
+ renderTopbarPresence(msg.serverUsers);
203
+ // Only re-render user strip if online IDs actually changed
204
+ if (!msg.allUsers && _ctx.cachedAllUsers.length > 0) {
205
+ var onlineChanged = newOnlineIds.length !== prevOnlineIds.length || newOnlineIds.some(function (id, i) { return id !== prevOnlineIds[i]; });
206
+ if (onlineChanged) {
207
+ _ctx.renderUserStrip(_ctx.cachedAllUsers, newOnlineIds, _ctx.myUserId, _ctx.cachedDmFavorites, _ctx.cachedDmConversations, _ctx.dmUnread, _ctx.dmRemovedUsers, _ctx.cachedMatesList);
208
+ }
209
+ }
210
+ }
211
+ // Update user strip (DM targets) in icon strip
212
+ if (msg.allUsers) {
213
+ _ctx.setCachedAllUsers(msg.allUsers);
214
+ if (msg.dmFavorites) _ctx.setCachedDmFavorites(msg.dmFavorites);
215
+ if (msg.dmConversations) _ctx.setCachedDmConversations(msg.dmConversations);
216
+ _ctx.renderUserStrip(msg.allUsers, _ctx.cachedOnlineIds, _ctx.myUserId, _ctx.cachedDmFavorites, _ctx.cachedDmConversations, _ctx.dmUnread, _ctx.dmRemovedUsers, _ctx.cachedMatesList);
217
+ if (document.body.classList.contains("mate-dm-active") || document.body.classList.contains("wide-view")) {
218
+ var refreshedMyUser = _ctx.cachedAllUsers.find(function (u) { return u.id === _ctx.myUserId; });
219
+ if (refreshedMyUser) {
220
+ document.body.dataset.myDisplayName = refreshedMyUser.displayName || refreshedMyUser.username || "";
221
+ document.body.dataset.myAvatarUrl = _ctx.userAvatarUrl(refreshedMyUser, 36);
222
+ try { localStorage.setItem("clay_my_user", JSON.stringify({ displayName: refreshedMyUser.displayName, username: refreshedMyUser.username, avatarStyle: refreshedMyUser.avatarStyle, avatarSeed: refreshedMyUser.avatarSeed, avatarCustom: refreshedMyUser.avatarCustom })); } catch(e) {}
223
+ }
224
+ }
225
+ // Render my avatar (always present, hidden behind user-island)
226
+ var meEl = document.getElementById("icon-strip-me");
227
+ if (meEl && !meEl.hasChildNodes()) {
228
+ var myUser = _ctx.cachedAllUsers.find(function (u) { return u.id === _ctx.myUserId; });
229
+ if (myUser) {
230
+ var meAvatar = document.createElement("img");
231
+ meAvatar.className = "icon-strip-me-avatar";
232
+ meAvatar.src = _ctx.userAvatarUrl(myUser, 34);
233
+ meEl.appendChild(meAvatar);
234
+ }
235
+ }
236
+ }
237
+ }
238
+
239
+ var _lastTopbarUserIds = [];
240
+ export function renderTopbarPresence(serverUsers) {
241
+ var countEl = document.getElementById("client-count");
242
+ if (!countEl) return;
243
+ if (serverUsers.length > 1) {
244
+ // Skip re-render if user list unchanged
245
+ var newIds = serverUsers.map(function (u) { return u.id; }).sort();
246
+ if (newIds.length === _lastTopbarUserIds.length && newIds.every(function (id, i) { return id === _lastTopbarUserIds[i]; })) return;
247
+ _lastTopbarUserIds = newIds;
248
+ countEl.innerHTML = "";
249
+ for (var cui = 0; cui < serverUsers.length; cui++) {
250
+ var cu = serverUsers[cui];
251
+ var cuImg = document.createElement("img");
252
+ cuImg.className = "client-avatar";
253
+ cuImg.src = _ctx.userAvatarUrl(cu, 24);
254
+ cuImg.alt = cu.displayName;
255
+ cuImg.dataset.tip = cu.displayName + " (@" + cu.username + ")";
256
+ if (cui > 0) cuImg.style.marginLeft = "-6px";
257
+ countEl.appendChild(cuImg);
258
+ }
259
+ countEl.classList.remove("hidden");
260
+ } else {
261
+ _lastTopbarUserIds = [];
262
+ countEl.classList.add("hidden");
263
+ }
264
+ }
265
+
266
+ export function renderProjectList() {
267
+ var iconStripProjects = cachedProjects.filter(function (p) {
268
+ return !p.isMate;
269
+ }).map(function (p) {
270
+ return {
271
+ slug: p.slug,
272
+ name: p.title || p.project,
273
+ icon: p.icon || null,
274
+ isProcessing: p.isProcessing,
275
+ onlineUsers: p.onlineUsers || [],
276
+ unread: p.unread || 0,
277
+ pendingPermissions: p.pendingPermissions || 0,
278
+ isWorktree: p.isWorktree || false,
279
+ parentSlug: p.parentSlug || null,
280
+ branch: p.branch || null,
281
+ worktreeAccessible: p.worktreeAccessible !== undefined ? p.worktreeAccessible : true,
282
+ };
283
+ });
284
+ var iconStripActiveSlug = (_ctx.mateProjectSlug && _ctx.savedMainSlug) ? _ctx.savedMainSlug : _ctx.currentSlug;
285
+ _ctx.renderIconStrip(iconStripProjects, iconStripActiveSlug);
286
+ // Update title bar project name and icon if it changed
287
+ if (!_ctx.mateProjectSlug) {
288
+ for (var pi = 0; pi < cachedProjects.length; pi++) {
289
+ if (cachedProjects[pi].slug === _ctx.currentSlug) {
290
+ var updatedName = cachedProjects[pi].title || cachedProjects[pi].project;
291
+ var tbName = document.getElementById("title-bar-project-name");
292
+ if (tbName && updatedName) tbName.textContent = updatedName;
293
+ var tbIcon = document.getElementById("title-bar-project-icon");
294
+ if (tbIcon) {
295
+ var pIcon = cachedProjects[pi].icon || null;
296
+ if (pIcon) {
297
+ tbIcon.textContent = pIcon;
298
+ parseEmojis(tbIcon);
299
+ tbIcon.classList.add("has-icon");
300
+ try { localStorage.setItem("clay-project-icon-" + (_ctx.currentSlug || "default"), pIcon); } catch (e) {}
301
+ } else {
302
+ tbIcon.textContent = "";
303
+ tbIcon.classList.remove("has-icon");
304
+ try { localStorage.removeItem("clay-project-icon-" + (_ctx.currentSlug || "default")); } catch (e) {}
305
+ }
306
+ }
307
+ break;
308
+ }
309
+ }
310
+ }
311
+ // Re-apply current socket status to the active icon's dot
312
+ var dot = _ctx.getStatusDot();
313
+ if (dot) {
314
+ if (_ctx.connected && _ctx.processing) { dot.classList.add("connected"); dot.classList.add("processing"); }
315
+ else if (_ctx.connected) { dot.classList.add("connected"); }
316
+ }
317
+ _ctx.updateCrossProjectBlink();
318
+ }
319
+
320
+ export function resetClientState() {
321
+ _ctx.closeSearch();
322
+ _ctx.messagesEl.innerHTML = "";
323
+ _ctx.setCurrentMsgEl(null);
324
+ _ctx.setCurrentFullText("");
325
+ _ctx.resetToolState();
326
+ _ctx.clearPendingImages();
327
+ _ctx.setActivityEl(null);
328
+ _ctx.setProcessing(false);
329
+ _ctx.setTurnCounter(0);
330
+ _ctx.setMessageUuidMap([]);
331
+ _ctx.setHistoryFrom(0);
332
+ _ctx.setHistoryTotal(0);
333
+ _ctx.setPrependAnchor(null);
334
+ _ctx.setLoadingMore(false);
335
+ _ctx.setIsUserScrolledUp(false);
336
+ _ctx.newMsgBtn.classList.add("hidden");
337
+ _ctx.setRewindMode(false);
338
+ _ctx.setActivity(null);
339
+ _ctx.setStatus("connected");
340
+ if (!_ctx.loopActive) _ctx.enableMainInput();
341
+ _ctx.resetUsage();
342
+ _ctx.resetTurnMetaCost();
343
+ _ctx.resetContext();
344
+ _ctx.resetRateLimitState();
345
+ if (_ctx.getHeaderContextEl()) { _ctx.getHeaderContextEl().remove(); _ctx.setHeaderContextEl(null); }
346
+ _ctx.hideSuggestionChips();
347
+ _ctx.closeSessionInfoPopover();
348
+ _ctx.stopUrgentBlink();
349
+ // Clear debate UI and state from previous session
350
+ _ctx.setDebateStickyState(null);
351
+ _ctx.resetDebateState();
352
+ var debateBadges = document.querySelectorAll(".debate-header-badge");
353
+ for (var dbi = 0; dbi < debateBadges.length; dbi++) debateBadges[dbi].remove();
354
+ _ctx.removeDebateBottomBar();
355
+ var handBar = document.getElementById("debate-hand-raise-bar");
356
+ if (handBar) handBar.remove();
357
+ var debateSticky = document.getElementById("debate-sticky");
358
+ if (debateSticky) { debateSticky.classList.add("hidden"); debateSticky.innerHTML = ""; }
359
+ var debateFloat = document.getElementById("debate-info-float");
360
+ if (debateFloat) { debateFloat.classList.add("hidden"); debateFloat.innerHTML = ""; }
361
+ }
362
+
363
+ export function switchProject(slug) {
364
+ if (!slug) return;
365
+ var wasDm = _ctx.dmMode;
366
+ var wasMate = _ctx.dmMode && _ctx.dmTargetUser && _ctx.dmTargetUser.isMate;
367
+ if (_ctx.dmMode) _ctx.exitDmMode(wasMate);
368
+ if (_ctx.isHomeHubVisible()) {
369
+ _ctx.hideHomeHub();
370
+ if (slug === _ctx.currentSlug) return;
371
+ }
372
+ if (slug === _ctx.currentSlug) {
373
+ if (wasDm && _ctx.getWs() && _ctx.getWs().readyState === 1) {
374
+ _ctx.getWs().send(JSON.stringify({ type: "switch_session", id: _ctx.activeSessionId }));
375
+ }
376
+ return;
377
+ }
378
+ _ctx.resetFileBrowser();
379
+ _ctx.closeArchive();
380
+ _ctx.hideMemory();
381
+ if (_ctx.isSchedulerOpen()) _ctx.closeScheduler();
382
+ _ctx.resetScheduler(slug);
383
+ _ctx.setCurrentSlug(slug);
384
+ _ctx.setBasePath("/p/" + slug + "/");
385
+ _ctx.setWsPath("/p/" + slug + "/ws");
386
+ if (document.documentElement.classList.contains("pwa-standalone")) {
387
+ history.replaceState(null, "", "/p/" + slug + "/");
388
+ } else {
389
+ history.pushState(null, "", "/p/" + slug + "/");
390
+ }
391
+ resetClientState();
392
+ _ctx.connect();
393
+ }
394
+
395
+ export function showUpdateAvailable(msg) {
396
+ var $ = _ctx.$;
397
+ var updatePillWrap = $("update-pill-wrap");
398
+ var updateVersion = $("update-version");
399
+ if (updatePillWrap && updateVersion && msg.version) {
400
+ updateVersion.textContent = "v" + msg.version;
401
+ updatePillWrap.classList.remove("hidden");
402
+ var updPill = $("update-pill");
403
+ var updResetBtn = $("update-now");
404
+ if (_ctx.isHeadlessMode) {
405
+ if (updPill) updPill.innerHTML = '<i data-lucide="arrow-up-circle"></i> <span id="update-version">v' + msg.version + '</span> available. Update manually';
406
+ if (updResetBtn) updResetBtn.style.display = "none";
407
+ } else {
408
+ if (updResetBtn) {
409
+ updResetBtn.innerHTML = '<i data-lucide="download"></i> Update now';
410
+ updResetBtn.disabled = false;
411
+ updResetBtn.style.display = "";
412
+ }
413
+ }
414
+ var updManualCmd = $("update-manual-cmd");
415
+ if (updManualCmd) {
416
+ var updTag = msg.version.indexOf("-beta") !== -1 ? "beta" : "latest";
417
+ updManualCmd.textContent = "npx clay-server@" + updTag;
418
+ }
419
+ refreshIcons();
420
+ }
421
+ var settingsUpdBtn = $("settings-update-check");
422
+ if (settingsUpdBtn && msg.version) {
423
+ settingsUpdBtn.innerHTML = "";
424
+ var ic = document.createElement("i");
425
+ ic.setAttribute("data-lucide", "arrow-up-circle");
426
+ settingsUpdBtn.appendChild(ic);
427
+ settingsUpdBtn.appendChild(document.createTextNode(" Update available (v" + msg.version + ")"));
428
+ settingsUpdBtn.classList.add("settings-btn-update-available");
429
+ settingsUpdBtn.disabled = false;
430
+ refreshIcons();
431
+ }
432
+ }
433
+
434
+ // --- Remove project ---
435
+
436
+ export function confirmRemoveProject(slug, name) {
437
+ pendingRemoveSlug = slug;
438
+ pendingRemoveName = name;
439
+ if (_ctx.getWs() && _ctx.getWs().readyState === 1) {
440
+ _ctx.getWs().send(JSON.stringify({ type: "remove_project_check", slug: slug }));
441
+ }
442
+ }
443
+
444
+ export function handleRemoveProjectCheckResult(msg) {
445
+ var slug = msg.slug || pendingRemoveSlug;
446
+ var name = msg.name || pendingRemoveName || slug;
447
+ if (!slug) return;
448
+
449
+ if (msg.count > 0) {
450
+ showRemoveProjectTaskDialog(slug, name, msg.count);
451
+ } else {
452
+ var isWt = slug.indexOf("--") !== -1;
453
+ var confirmMsg = isWt
454
+ ? 'Delete worktree "' + name + '"? The branch and working directory will be removed from disk.'
455
+ : 'Remove "' + name + '"? You can re-add it later.';
456
+ _ctx.showConfirm(confirmMsg, function () {
457
+ var iconEl = document.querySelector('.icon-strip-item[data-slug="' + slug + '"]');
458
+ if (iconEl) {
459
+ var rect = iconEl.getBoundingClientRect();
460
+ _ctx.spawnDustParticles(rect.left + rect.width / 2, rect.top + rect.height / 2);
461
+ }
462
+ setTimeout(function () {
463
+ if (_ctx.getWs() && _ctx.getWs().readyState === 1) {
464
+ _ctx.getWs().send(JSON.stringify({ type: "remove_project", slug: slug }));
465
+ }
466
+ }, 1000);
467
+ }, "Remove", true);
468
+ }
469
+ pendingRemoveSlug = null;
470
+ pendingRemoveName = null;
471
+ }
472
+
473
+ function showRemoveProjectTaskDialog(slug, name, taskCount) {
474
+ var otherProjects = cachedProjects.filter(function (p) { return p.slug !== slug; });
475
+
476
+ var modal = document.createElement("div");
477
+ modal.className = "remove-project-task-modal";
478
+ modal.innerHTML =
479
+ '<div class="remove-project-task-backdrop"></div>' +
480
+ '<div class="remove-project-task-dialog">' +
481
+ '<div class="remove-project-task-title">Remove project "' + (name || slug) + '"</div>' +
482
+ '<div class="remove-project-task-text">This project has <strong>' + taskCount + '</strong> task' + (taskCount > 1 ? 's' : '') + '/schedule' + (taskCount > 1 ? 's' : '') + '.</div>' +
483
+ '<div class="remove-project-task-options">' +
484
+ (otherProjects.length > 0
485
+ ? '<div class="remove-project-task-label">Move tasks to:</div>' +
486
+ '<select class="remove-project-task-select" id="rpt-move-target">' +
487
+ otherProjects.map(function (p) {
488
+ return '<option value="' + p.slug + '">' + (p.title || p.project || p.slug) + '</option>';
489
+ }).join("") +
490
+ '</select>' +
491
+ '<button class="remove-project-task-btn move" id="rpt-move-btn">Move &amp; Remove</button>'
492
+ : '') +
493
+ '<button class="remove-project-task-btn delete" id="rpt-delete-btn">Delete all &amp; Remove</button>' +
494
+ '<button class="remove-project-task-btn cancel" id="rpt-cancel-btn">Cancel</button>' +
495
+ '</div>' +
496
+ '</div>';
497
+
498
+ document.body.appendChild(modal);
499
+
500
+ var backdrop = modal.querySelector(".remove-project-task-backdrop");
501
+ var moveBtn = modal.querySelector("#rpt-move-btn");
502
+ var deleteBtn = modal.querySelector("#rpt-delete-btn");
503
+ var cancelBtn = modal.querySelector("#rpt-cancel-btn");
504
+ var selectEl = modal.querySelector("#rpt-move-target");
505
+
506
+ function close() { modal.remove(); }
507
+ backdrop.addEventListener("click", close);
508
+ cancelBtn.addEventListener("click", close);
509
+
510
+ if (moveBtn) {
511
+ moveBtn.addEventListener("click", function () {
512
+ var targetSlug = selectEl ? selectEl.value : null;
513
+ if (_ctx.getWs() && _ctx.getWs().readyState === 1 && targetSlug) {
514
+ _ctx.getWs().send(JSON.stringify({ type: "remove_project", slug: slug, moveTasksTo: targetSlug }));
515
+ }
516
+ close();
517
+ });
518
+ }
519
+
520
+ deleteBtn.addEventListener("click", function () {
521
+ if (_ctx.getWs() && _ctx.getWs().readyState === 1) {
522
+ _ctx.getWs().send(JSON.stringify({ type: "remove_project", slug: slug }));
523
+ }
524
+ close();
525
+ });
526
+ }
527
+
528
+ export function handleRemoveProjectResult(msg) {
529
+ if (msg.ok) {
530
+ if (msg.slug === _ctx.currentSlug) {
531
+ var isWorktree = msg.slug.indexOf("--") !== -1;
532
+ var parentSlug = isWorktree ? msg.slug.split("--")[0] : null;
533
+
534
+ _ctx.showToast(isWorktree ? "Worktree removed" : "Project removed", "success");
535
+
536
+ // Suppress disconnect overlay and reconnect by detaching the WS
537
+ var ws = _ctx.getWs();
538
+ if (ws) { ws.onclose = null; ws.onerror = null; ws.close(); _ctx.setWs(null); }
539
+ _ctx.cancelReconnect();
540
+ _ctx.setConnected(false);
541
+ _ctx.connectOverlay.classList.add("hidden");
542
+ if (!isWorktree) {
543
+ var removedProj = null;
544
+ for (var ri = 0; ri < cachedProjects.length; ri++) {
545
+ if (cachedProjects[ri].slug === msg.slug) { removedProj = cachedProjects[ri]; break; }
546
+ }
547
+ if (removedProj) {
548
+ cachedRemovedProjects.push({
549
+ path: removedProj.path || "",
550
+ title: removedProj.title || null,
551
+ icon: removedProj.icon || null,
552
+ removedAt: Date.now(),
553
+ });
554
+ }
555
+ }
556
+ cachedProjects = cachedProjects.filter(function (p) { return p.slug !== msg.slug; });
557
+ cachedProjectCount = cachedProjects.length;
558
+ _ctx.setCurrentSlug(null);
559
+ renderProjectList();
560
+ resetClientState();
561
+
562
+ if (parentSlug) {
563
+ switchProject(parentSlug);
564
+ } else {
565
+ _ctx.showHomeHub();
566
+ }
567
+ } else {
568
+ _ctx.showToast(msg.slug.indexOf("--") !== -1 ? "Worktree removed" : "Project removed", "success");
569
+ }
570
+ } else {
571
+ _ctx.showToast(msg.error || "Failed to remove project", "error");
572
+ }
573
+ }
574
+
575
+ // --- Add project modal ---
576
+
577
+ function switchAddProjectMode(mode) {
578
+ addProjectMode = mode;
579
+ for (var mi = 0; mi < addProjectModeBtns.length; mi++) {
580
+ var btn = addProjectModeBtns[mi];
581
+ if (btn.dataset.mode === mode) {
582
+ btn.classList.add("active");
583
+ } else {
584
+ btn.classList.remove("active");
585
+ }
586
+ }
587
+ for (var pi = 0; pi < addProjectPanels.length; pi++) {
588
+ var panel = addProjectPanels[pi];
589
+ if (panel.dataset.panel === mode) {
590
+ panel.classList.add("active");
591
+ } else {
592
+ panel.classList.remove("active");
593
+ }
594
+ }
595
+ addProjectError.classList.add("hidden");
596
+ addProjectCloneProgress.classList.add("hidden");
597
+ if (mode === "existing") {
598
+ addProjectOk.textContent = "Add";
599
+ } else if (mode === "create") {
600
+ addProjectOk.textContent = "Create";
601
+ } else if (mode === "clone") {
602
+ addProjectOk.textContent = "Clone";
603
+ }
604
+ setTimeout(function () {
605
+ if (mode === "existing") {
606
+ addProjectInput.focus();
607
+ } else if (mode === "create") {
608
+ addProjectCreateInput.focus();
609
+ } else if (mode === "clone") {
610
+ addProjectCloneInput.focus();
611
+ }
612
+ }, 50);
613
+ }
614
+
615
+ export function openAddProjectModal() {
616
+ addProjectModal.classList.remove("hidden");
617
+ addProjectInput.value = "/";
618
+ addProjectCreateInput.value = "";
619
+ addProjectCloneInput.value = "";
620
+ addProjectError.classList.add("hidden");
621
+ addProjectError.textContent = "";
622
+ addProjectCloneProgress.classList.add("hidden");
623
+ addProjectSuggestions.classList.add("hidden");
624
+ addProjectSuggestions.innerHTML = "";
625
+ addProjectActiveIdx = -1;
626
+ addProjectOk.disabled = false;
627
+ var existingBtn = addProjectModal.querySelector('.add-project-mode-btn[data-mode="existing"]');
628
+ if (_ctx.isOsUsers) {
629
+ // Default: disable existing directory for multi-user, but allow for admins
630
+ existingBtn.disabled = true;
631
+ switchAddProjectMode("create");
632
+ if (_ctx.checkAdminAccess) {
633
+ _ctx.checkAdminAccess().then(function (isAdmin) {
634
+ if (isAdmin) {
635
+ existingBtn.disabled = false;
636
+ }
637
+ });
638
+ }
639
+ } else {
640
+ existingBtn.disabled = false;
641
+ switchAddProjectMode("existing");
642
+ }
643
+ renderRemovedProjectsList();
644
+ }
645
+
646
+ function renderRemovedProjectsList() {
647
+ if (!addProjectRemoved) return;
648
+ addProjectRemoved.innerHTML = "";
649
+ if (!cachedRemovedProjects || cachedRemovedProjects.length === 0) {
650
+ addProjectRemoved.classList.add("hidden");
651
+ return;
652
+ }
653
+ addProjectRemoved.classList.remove("hidden");
654
+ for (var ri = 0; ri < cachedRemovedProjects.length; ri++) {
655
+ var rp = cachedRemovedProjects[ri];
656
+ var item = document.createElement("div");
657
+ item.className = "add-project-removed-item";
658
+ item.dataset.path = rp.path;
659
+ item.addEventListener("click", function () {
660
+ var p = this.dataset.path;
661
+ if (_ctx.getWs() && _ctx.getWs().readyState === 1) {
662
+ _ctx.getWs().send(JSON.stringify({ type: "add_project", path: p }));
663
+ }
664
+ closeAddProjectModal();
665
+ });
666
+ var iconEl = document.createElement("span");
667
+ iconEl.className = "add-project-removed-icon";
668
+ iconEl.textContent = rp.icon || "📁";
669
+ item.appendChild(iconEl);
670
+ var info = document.createElement("div");
671
+ info.className = "add-project-removed-info";
672
+ var nameEl = document.createElement("div");
673
+ nameEl.className = "add-project-removed-name";
674
+ nameEl.textContent = rp.title || rp.path.split("/").pop() || rp.path;
675
+ info.appendChild(nameEl);
676
+ var pathEl = document.createElement("div");
677
+ pathEl.className = "add-project-removed-path";
678
+ pathEl.textContent = rp.path;
679
+ info.appendChild(pathEl);
680
+ item.appendChild(info);
681
+ addProjectRemoved.appendChild(item);
682
+ }
683
+ try { parseEmojis(addProjectRemoved); } catch (e) {}
684
+ }
685
+
686
+ export function closeAddProjectModal() {
687
+ addProjectModal.classList.add("hidden");
688
+ addProjectInput.value = "";
689
+ addProjectCreateInput.value = "";
690
+ addProjectCloneInput.value = "";
691
+ addProjectSuggestions.classList.add("hidden");
692
+ addProjectSuggestions.innerHTML = "";
693
+ addProjectError.classList.add("hidden");
694
+ addProjectCloneProgress.classList.add("hidden");
695
+ addProjectActiveIdx = -1;
696
+ if (addProjectDebounce) { clearTimeout(addProjectDebounce); addProjectDebounce = null; }
697
+ }
698
+
699
+ function requestBrowseDir(val) {
700
+ if (!_ctx.getWs() || _ctx.getWs().readyState !== 1) return;
701
+ _ctx.getWs().send(JSON.stringify({ type: "browse_dir", path: val }));
702
+ }
703
+
704
+ export function handleBrowseDirResult(msg) {
705
+ addProjectSuggestions.innerHTML = "";
706
+ addProjectActiveIdx = -1;
707
+ if (msg.error) {
708
+ addProjectSuggestions.classList.add("hidden");
709
+ return;
710
+ }
711
+ var entries = msg.entries || [];
712
+ if (entries.length === 0) {
713
+ addProjectSuggestions.classList.add("hidden");
714
+ return;
715
+ }
716
+ for (var si = 0; si < entries.length; si++) {
717
+ var entry = entries[si];
718
+ var item = document.createElement("div");
719
+ item.className = "add-project-suggestion-item";
720
+ item.dataset.path = entry.path;
721
+ item.innerHTML = '<i data-lucide="folder"></i><span class="add-project-suggestion-name">' +
722
+ escapeHtml(entry.name) + '</span>';
723
+ item.addEventListener("click", function (e) {
724
+ var p = this.dataset.path + "/";
725
+ addProjectInput.value = p;
726
+ addProjectInput.focus();
727
+ addProjectError.classList.add("hidden");
728
+ requestBrowseDir(p);
729
+ });
730
+ addProjectSuggestions.appendChild(item);
731
+ }
732
+ addProjectSuggestions.classList.remove("hidden");
733
+ refreshIcons();
734
+ }
735
+
736
+ export function handleAddProjectResult(msg) {
737
+ addProjectCloneProgress.classList.add("hidden");
738
+ if (msg.ok) {
739
+ closeAddProjectModal();
740
+ if (msg.existing) {
741
+ _ctx.showToast("Project already registered", "info");
742
+ } else {
743
+ var toastMsg = addProjectMode === "create" ? "Project created" : addProjectMode === "clone" ? "Project cloned" : "Project added";
744
+ _ctx.showToast(toastMsg, "success");
745
+ if (msg.slug) {
746
+ switchProject(msg.slug);
747
+ }
748
+ }
749
+ } else {
750
+ addProjectError.textContent = msg.error || "Failed to add project";
751
+ addProjectError.classList.remove("hidden");
752
+ addProjectOk.disabled = false;
753
+ }
754
+ }
755
+
756
+ export function handleCloneProgress(msg) {
757
+ if (msg.status === "cloning") {
758
+ addProjectCloneProgress.classList.remove("hidden");
759
+ }
760
+ }
761
+
762
+ function setActiveIdx(idx) {
763
+ var items = addProjectSuggestions.querySelectorAll(".add-project-suggestion-item");
764
+ addProjectActiveIdx = idx;
765
+ for (var ai = 0; ai < items.length; ai++) {
766
+ if (ai === idx) {
767
+ items[ai].classList.add("active");
768
+ items[ai].scrollIntoView({ block: "nearest" });
769
+ } else {
770
+ items[ai].classList.remove("active");
771
+ }
772
+ }
773
+ }
774
+
775
+ function submitAddProject() {
776
+ addProjectError.classList.add("hidden");
777
+ addProjectOk.disabled = true;
778
+
779
+ if (addProjectMode === "existing") {
780
+ var val = addProjectInput.value.replace(/\/+$/, "");
781
+ if (!val) { addProjectOk.disabled = false; return; }
782
+ if (_ctx.getWs() && _ctx.getWs().readyState === 1) {
783
+ _ctx.getWs().send(JSON.stringify({ type: "add_project", path: val }));
784
+ }
785
+ } else if (addProjectMode === "create") {
786
+ var name = addProjectCreateInput.value.trim();
787
+ if (!name) { addProjectOk.disabled = false; return; }
788
+ if (_ctx.getWs() && _ctx.getWs().readyState === 1) {
789
+ _ctx.getWs().send(JSON.stringify({ type: "create_project", name: name }));
790
+ }
791
+ } else if (addProjectMode === "clone") {
792
+ var url = addProjectCloneInput.value.trim();
793
+ if (!url) { addProjectOk.disabled = false; return; }
794
+ if (_ctx.getWs() && _ctx.getWs().readyState === 1) {
795
+ _ctx.getWs().send(JSON.stringify({ type: "clone_project", url: url }));
796
+ }
797
+ }
798
+ }