clay-server 5.1.0-beta.2 → 5.1.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 (101) hide show
  1. package/bin/cli.js +9 -91
  2. package/lib/builtin-mates.js +7 -7
  3. package/lib/daemon-settings.js +11 -0
  4. package/lib/daemon.js +8 -37
  5. package/lib/linear-store.js +35 -8
  6. package/lib/mate-identity-file.js +39 -0
  7. package/lib/mate-knowledge-sync.js +1 -1
  8. package/lib/mate-ready-creation.js +2 -1
  9. package/lib/mates-identity.js +4 -4
  10. package/lib/mates-prompts.js +4 -3
  11. package/lib/mates.js +12 -11
  12. package/lib/os-users.js +5 -16
  13. package/lib/project-connection.js +0 -7
  14. package/lib/project-debate.js +3 -2
  15. package/lib/project-filesystem.js +21 -39
  16. package/lib/project-mate-interaction.js +7 -6
  17. package/lib/project-server-settings.js +31 -0
  18. package/lib/project-sessions.js +57 -184
  19. package/lib/project.js +3 -3
  20. package/lib/public/app.js +6 -37
  21. package/lib/public/css/input.css +3 -1
  22. package/lib/public/css/menus.css +3 -0
  23. package/lib/public/css/overlays.css +7 -0
  24. package/lib/public/css/project-settings-modal.css +56 -0
  25. package/lib/public/css/server-settings-system.css +125 -0
  26. package/lib/public/css/session-github.css +25 -16
  27. package/lib/public/css/settings-ui.css +247 -22
  28. package/lib/public/css/shared-browser.css +7 -3
  29. package/lib/public/css/title-bar.css +5 -1
  30. package/lib/public/index.html +173 -447
  31. package/lib/public/modules/account-island.js +9 -7
  32. package/lib/public/modules/admin-settings-email.js +197 -0
  33. package/lib/public/modules/admin-settings-invitations.js +183 -0
  34. package/lib/public/modules/admin-settings-projects.js +198 -0
  35. package/lib/public/modules/admin-settings-ui.js +100 -0
  36. package/lib/public/modules/admin-settings-users.js +331 -0
  37. package/lib/public/modules/admin.js +19 -949
  38. package/lib/public/modules/app-dm.js +0 -2
  39. package/lib/public/modules/app-messages.js +13 -52
  40. package/lib/public/modules/app-rate-limit.js +70 -12
  41. package/lib/public/modules/avatar.js +6 -1
  42. package/lib/public/modules/context-sources.js +27 -18
  43. package/lib/public/modules/default-ai.js +14 -7
  44. package/lib/public/modules/header-tui-font.js +10 -20
  45. package/lib/public/modules/home-mate-avatar-editor.js +2 -2
  46. package/lib/public/modules/linear-settings.js +3 -2
  47. package/lib/public/modules/markdown.js +5 -6
  48. package/lib/public/modules/notification-settings.js +81 -0
  49. package/lib/public/modules/notifications.js +44 -28
  50. package/lib/public/modules/personal-settings-dialog.js +51 -0
  51. package/lib/public/modules/profile.js +15 -6
  52. package/lib/public/modules/project-settings-environment.js +106 -0
  53. package/lib/public/modules/project-settings.js +72 -628
  54. package/lib/public/modules/server-settings-controls.js +316 -0
  55. package/lib/public/modules/server-settings-environment.js +194 -0
  56. package/lib/public/modules/server-settings-navigation.js +39 -0
  57. package/lib/public/modules/server-settings-state.js +54 -0
  58. package/lib/public/modules/server-settings.js +116 -788
  59. package/lib/public/modules/server-storage-settings.js +84 -0
  60. package/lib/public/modules/session-github.js +2 -1
  61. package/lib/public/modules/session-presence-ui.js +1 -1
  62. package/lib/public/modules/settings-env-values.js +34 -0
  63. package/lib/public/modules/settings-select.js +121 -0
  64. package/lib/public/modules/shared-browser.js +22 -12
  65. package/lib/public/modules/stt-picker.js +14 -5
  66. package/lib/public/modules/terminal-prefs.js +22 -0
  67. package/lib/public/modules/theme.js +88 -52
  68. package/lib/public/modules/user-settings-account.js +318 -0
  69. package/lib/public/modules/user-settings-appearance.js +55 -0
  70. package/lib/public/modules/user-settings-editors.js +24 -0
  71. package/lib/public/modules/user-settings-integrations.js +104 -0
  72. package/lib/public/modules/user-settings-layout.js +80 -0
  73. package/lib/public/modules/user-settings-preferences.js +48 -0
  74. package/lib/public/modules/user-settings-profile.js +53 -0
  75. package/lib/public/modules/user-settings-themes.js +38 -0
  76. package/lib/public/modules/user-settings-voice.js +75 -0
  77. package/lib/public/modules/user-settings.js +53 -480
  78. package/lib/public/style.css +4 -0
  79. package/lib/sdk-message-processor.js +16 -4
  80. package/lib/sdk-quota-snapshot.js +37 -0
  81. package/lib/server-appearance-settings.js +71 -0
  82. package/lib/server-linear.js +14 -8
  83. package/lib/server-mate-instructions.js +2 -2
  84. package/lib/server-mates.js +2 -1
  85. package/lib/server-notification-settings.js +67 -0
  86. package/lib/server-settings.js +4 -0
  87. package/lib/server-storage.js +57 -0
  88. package/lib/server.js +10 -2
  89. package/lib/sessions.js +3 -4
  90. package/lib/startup-projects.js +40 -0
  91. package/lib/storage-native-walk.js +85 -0
  92. package/lib/storage-usage.js +144 -0
  93. package/lib/users-preferences.js +0 -74
  94. package/lib/users.js +0 -4
  95. package/lib/ws-schema.js +2 -4
  96. package/lib/yoke/acp-process-manager.js +2 -2
  97. package/lib/yoke/adapters/codex.js +12 -52
  98. package/lib/yoke/codex-app-server.js +2 -2
  99. package/lib/yoke/codex-model-catalog.js +52 -0
  100. package/lib/yoke/codex-rate-limits.js +26 -0
  101. package/package.json +3 -3
@@ -1,32 +1,50 @@
1
- // project-settings.js — Project settings panel (profile, defaults, instructions, env)
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { trapSettingsFocus, isolateSettingsDialog, confirmSettingsAction } from './server-settings-state.js';
4
+ import { initProjectEnvironment, openProjectEnvironment, projectEnvironmentDirty, discardProjectEnvironment } from './project-settings-environment.js';
5
+ import { enhanceSettingsSelects } from './settings-select.js';
6
+ // Project identity and access in a focused settings dialog.
2
7
  import { refreshIcons } from './icons.js';
3
8
  import { showToast } from './utils.js';
4
9
  import { parseEmojis } from './markdown.js';
5
- import { closeFileViewer } from './filebrowser.js';
6
10
  import { holdNotesForSettings, releaseNotesForSettings } from './sticky-notes.js';
7
11
  import { openNotesBrowser, closeNotesBrowser, isNotesBrowserOpen } from './sticky-notes-browser.js';
8
- import { renderModelList, renderModeList, renderEffortBar, renderThinkingBar, renderBetaCard } from './settings-defaults.js';
9
12
  import { initProjectSessionVisibilitySettings, populateProjectSessionVisibilitySettings } from './project-session-visibility-settings.js';
10
13
 
11
- var ctx = null;
12
14
  var panelEl = null;
13
15
  var navItems = null;
14
16
  var sections = null;
15
- var currentSlug = null;
16
- var currentProject = null; // { slug, name, icon }
17
17
  var notesBrowserReopen = false;
18
+ var restoreBackground = null;
19
+ var returnFocus = null;
20
+ function settingsSlug() { return store.get("projectSettingsSlug"); }
21
+ function settingsProject() { return store.get("projectSettingsProject"); }
18
22
 
19
23
  // Emoji categories (reuse from sidebar)
20
24
  var EMOJI_CATEGORIES = null;
21
25
 
22
26
 
23
- // ===== Init =====
24
- export function initProjectSettings(appCtx, emojiCategories) {
25
- ctx = appCtx;
27
+ export function initProjectSettings(emojiCategories) {
26
28
  EMOJI_CATEGORIES = emojiCategories;
27
29
  panelEl = document.getElementById("project-settings");
28
30
  if (!panelEl) return;
29
31
 
32
+ document.body.appendChild(panelEl);
33
+ var destinations = [{id: 'profile', label: 'General'}, {id: 'access', label: 'Access'}, {id: 'environment', label: 'Environment'}];
34
+ var nav = panelEl.querySelector('.server-settings-nav-items');
35
+ var dropdown = document.getElementById('ps-nav-dropdown');
36
+ destinations.forEach(function (page) {
37
+ var button = document.createElement('button');
38
+ button.className = 'settings-nav-item'; button.dataset.section = page.id; button.textContent = page.label;
39
+ nav.appendChild(button);
40
+ var option = document.createElement('option'); option.value = page.id; option.textContent = page.label; dropdown.appendChild(option);
41
+ });
42
+ initProjectEnvironment();
43
+ panelEl.addEventListener('click', function (event) { if (event.target === panelEl) closeProjectSettings(); });
44
+ panelEl.addEventListener('keydown', function (event) {
45
+ if (event.key === 'Escape' && !event.defaultPrevented) { event.preventDefault(); event.stopPropagation(); closeProjectSettings(); }
46
+ trapSettingsFocus(event, panelEl);
47
+ });
30
48
  navItems = panelEl.querySelectorAll(".settings-nav-item");
31
49
  sections = panelEl.querySelectorAll(".ps-section");
32
50
 
@@ -53,13 +71,6 @@ export function initProjectSettings(appCtx, emojiCategories) {
53
71
  });
54
72
  }
55
73
 
56
- // ESC key
57
- document.addEventListener("keydown", function (e) {
58
- if (e.key === "Escape" && panelEl && !panelEl.classList.contains("hidden")) {
59
- closeProjectSettings();
60
- }
61
- });
62
-
63
74
  // Profile: rename
64
75
  var renameBtn = document.getElementById("ps-rename-btn");
65
76
  var renameForm = document.getElementById("ps-rename-form");
@@ -70,7 +81,7 @@ export function initProjectSettings(appCtx, emojiCategories) {
70
81
  if (renameBtn) {
71
82
  renameBtn.addEventListener("click", function () {
72
83
  renameForm.classList.remove("hidden");
73
- renameInput.value = currentProject ? currentProject.name || "" : "";
84
+ renameInput.value = settingsProject() ? settingsProject().name || "" : "";
74
85
  renameBtn.classList.add("hidden");
75
86
  renameInput.focus();
76
87
  renameInput.select();
@@ -100,81 +111,24 @@ export function initProjectSettings(appCtx, emojiCategories) {
100
111
  }
101
112
  if (iconRemoveBtn) {
102
113
  iconRemoveBtn.addEventListener("click", function () {
103
- if (ctx.ws && ctx.connected) {
104
- ctx.ws.send(JSON.stringify({ type: "set_project_icon", slug: currentSlug, icon: null }));
114
+ if (getWs() && store.get('connected')) {
115
+ getWs().send(JSON.stringify({ type: "set_project_icon", slug: settingsSlug(), icon: null }));
105
116
  }
106
117
  updateIconPreview(null);
107
118
  });
108
119
  }
109
120
 
110
- // Instructions: save
111
- var instrSave = document.getElementById("ps-instructions-save");
112
- if (instrSave) {
113
- instrSave.addEventListener("click", function () { saveInstructions(); });
114
- }
115
-
116
- // Environment: add button
117
- var envAddBtn = document.getElementById("ps-env-add-btn");
118
- if (envAddBtn) {
119
- envAddBtn.addEventListener("click", function () {
120
- addEnvRow("", "", true);
121
- autoSaveEnv();
122
- });
123
- }
124
-
125
- // Environment: tab switching
126
- var envTabs = panelEl.querySelectorAll(".ps-env-tab");
127
- var envTabContents = panelEl.querySelectorAll(".ps-env-tab-content");
128
- for (var ti = 0; ti < envTabs.length; ti++) {
129
- envTabs[ti].addEventListener("click", function () {
130
- var tab = this.dataset.tab;
131
- for (var a = 0; a < envTabs.length; a++) {
132
- envTabs[a].classList.toggle("active", envTabs[a].dataset.tab === tab);
133
- }
134
- for (var b = 0; b < envTabContents.length; b++) {
135
- envTabContents[b].classList.toggle("active", envTabContents[b].dataset.tab === tab);
136
- }
137
- if (tab === "shared") loadSharedEnv();
138
- });
139
- }
140
-
141
- // Environment: shared env add button
142
- var sharedEnvAddBtn = document.getElementById("ps-shared-env-add-btn");
143
- if (sharedEnvAddBtn) {
144
- sharedEnvAddBtn.addEventListener("click", function () {
145
- addSharedEnvRow("", "", true);
146
- autoSaveSharedEnv();
147
- });
148
- }
149
-
150
- // Owner: transfer
151
- var transferBtn = document.getElementById("ps-transfer-btn");
152
- if (transferBtn) {
153
- transferBtn.addEventListener("click", function () {
154
- showTransferForm();
155
- });
156
- }
157
- var transferSave = document.getElementById("ps-transfer-save");
158
- if (transferSave) {
159
- transferSave.addEventListener("click", function () {
160
- commitTransfer();
161
- });
162
- }
163
- var transferCancel = document.getElementById("ps-transfer-cancel");
164
- if (transferCancel) {
165
- transferCancel.addEventListener("click", function () {
166
- hideTransferForm();
167
- });
168
- }
169
-
170
121
  initProjectSessionVisibilitySettings();
122
+ store.subscribe(function (next, previous) { if (next.myUserId !== previous.myUserId) closeProjectSettings({ cleanup: true }); });
171
123
  }
172
124
 
173
- // ===== Open / Close =====
174
125
  export function openProjectSettings(slug, project) {
175
126
  if (!panelEl) return;
176
- currentSlug = slug;
177
- currentProject = project;
127
+ returnFocus = document.activeElement;
128
+ if (!returnFocus || returnFocus === document.body) returnFocus = document.getElementById("title-bar-project-dropdown");
129
+ store.set({ projectSettingsSlug: slug, projectSettingsProject: project });
130
+ var env = store.get("projectEnvironmentEditor");
131
+ if (env && !env.dirty && !env.saving) store.set({ projectEnvironmentEditor: Object.assign({}, env, { loaded: false, loading: false }) });
178
132
 
179
133
  // Set nav title
180
134
  var navTitle = document.getElementById("ps-nav-title");
@@ -186,8 +140,6 @@ export function openProjectSettings(slug, project) {
186
140
  // Populate profile
187
141
  populateProfile();
188
142
 
189
- // Close file viewer if open (prevent split-screen)
190
- closeFileViewer();
191
143
 
192
144
  // Sticky notes must not sit over settings; remember what to restore.
193
145
  if (panelEl.classList.contains("hidden")) {
@@ -198,6 +150,9 @@ export function openProjectSettings(slug, project) {
198
150
 
199
151
  // Show panel
200
152
  panelEl.classList.remove("hidden");
153
+ if (!restoreBackground) restoreBackground = isolateSettingsDialog(panelEl);
154
+ document.getElementById("project-settings-close").focus();
155
+ enhanceSettingsSelects(panelEl);
201
156
  refreshIcons();
202
157
  }
203
158
 
@@ -205,9 +160,20 @@ export function openProjectSettings(slug, project) {
205
160
  // not reopened over whatever screen comes next.
206
161
  export function closeProjectSettings(opts) {
207
162
  if (!panelEl) return;
163
+ if (!(opts && opts.cleanup) && (store.get("projectEnvironmentEditor") || {}).saving) { showToast("Wait for the environment save to finish."); return; }
164
+ var rename = document.getElementById("ps-rename-form");
165
+ var renameDirty = rename && !rename.classList.contains("hidden") && document.getElementById("ps-rename-input").value.trim() !== (settingsProject().name || "");
166
+ if (!(opts && (opts.cleanup || opts.confirmed)) && (projectEnvironmentDirty() || renameDirty)) {
167
+ confirmSettingsAction('Your changes have not been saved. Discard them and close?', 'Discard changes', function () {
168
+ discardProjectEnvironment(); closeProjectSettings({ confirmed: true });
169
+ });
170
+ return;
171
+ }
208
172
  var wasOpen = !panelEl.classList.contains("hidden");
209
173
  panelEl.classList.add("hidden");
210
174
  closePsEmojiPicker();
175
+ if (restoreBackground) { restoreBackground(); restoreBackground = null; }
176
+ if (returnFocus && returnFocus.isConnected && !(opts && opts.cleanup)) returnFocus.focus();
211
177
  if (wasOpen) {
212
178
  releaseNotesForSettings();
213
179
  if (notesBrowserReopen && !(opts && opts.cleanup)) openNotesBrowser();
@@ -219,11 +185,11 @@ export function isProjectSettingsOpen() {
219
185
  return panelEl && !panelEl.classList.contains("hidden");
220
186
  }
221
187
 
222
- // ===== Section switching =====
223
188
  function switchSection(name) {
224
189
  for (var i = 0; i < navItems.length; i++) {
225
190
  var active = navItems[i].dataset.section === name;
226
191
  navItems[i].classList.toggle("active", active);
192
+ navItems[i].setAttribute("aria-current", active ? "page" : "false");
227
193
  }
228
194
  for (var j = 0; j < sections.length; j++) {
229
195
  var active2 = sections[j].dataset.section === name;
@@ -236,27 +202,12 @@ function switchSection(name) {
236
202
  psNavDropdown.value = name;
237
203
  }
238
204
 
239
- // Lazy-load section data
240
- if (name === "defaults") populateDefaults();
241
- if (name === "instructions") loadInstructions();
242
- if (name === "environment") {
243
- // Reset tabs to "project" tab
244
- var envTabs = panelEl.querySelectorAll(".ps-env-tab");
245
- var envTabContents = panelEl.querySelectorAll(".ps-env-tab-content");
246
- for (var t = 0; t < envTabs.length; t++) {
247
- envTabs[t].classList.toggle("active", envTabs[t].dataset.tab === "project");
248
- }
249
- for (var u = 0; u < envTabContents.length; u++) {
250
- envTabContents[u].classList.toggle("active", envTabContents[u].dataset.tab === "project");
251
- }
252
- loadEnvironment();
253
- }
205
+ if (name === "environment") openProjectEnvironment(settingsSlug());
254
206
  }
255
207
 
256
- // ===== Profile =====
257
208
  function populateProfile() {
258
209
  var nameEl = document.getElementById("ps-project-name");
259
- if (nameEl) nameEl.textContent = currentProject ? currentProject.name || "-" : "-";
210
+ if (nameEl) nameEl.textContent = settingsProject() ? settingsProject().name || "-" : "-";
260
211
 
261
212
  // Reset rename form
262
213
  var renameForm = document.getElementById("ps-rename-form");
@@ -265,74 +216,28 @@ function populateProfile() {
265
216
  if (renameBtn) renameBtn.classList.remove("hidden");
266
217
 
267
218
  // Icon
268
- updateIconPreview(currentProject ? currentProject.icon : null);
269
-
270
- populateProjectSessionVisibilitySettings(currentSlug, currentProject);
271
-
272
- // Owner (only in multi-user mode)
273
- var ownerField = document.getElementById("ps-owner-field");
274
- if (ownerField) {
275
- var ownerId = currentProject ? currentProject.projectOwnerId : null;
276
- var isOwnerLocked = currentProject ? currentProject.ownerLocked : false;
277
- var isMultiUser = ctx.multiUser;
278
- if (isMultiUser) {
279
- ownerField.style.display = "";
280
- var ownerNameEl = document.getElementById("ps-owner-name");
281
- var transferBtn = document.getElementById("ps-transfer-btn");
282
- var ownerLockedHint = document.getElementById("ps-owner-locked-hint");
283
- if (transferBtn) transferBtn.style.display = "none";
284
- if (ownerLockedHint) {
285
- if (isOwnerLocked) { ownerLockedHint.classList.remove("hidden"); } else { ownerLockedHint.classList.add("hidden"); }
286
- }
287
- // Fetch user list (only succeeds for admin)
288
- fetch("/api/admin/users").then(function (r) {
289
- if (!r.ok) throw new Error("not admin");
290
- return r.json();
291
- }).then(function (data) {
292
- var users = data.users || [];
293
- // Show owner name
294
- if (ownerId) {
295
- var owner = null;
296
- for (var i = 0; i < users.length; i++) {
297
- if (users[i].id === ownerId) { owner = users[i]; break; }
298
- }
299
- if (ownerNameEl) ownerNameEl.textContent = owner ? (owner.displayName || owner.username) : ownerId;
300
- } else {
301
- if (ownerNameEl) ownerNameEl.textContent = "Not set";
302
- }
303
- // Admin can transfer unless ownership is locked (home directory projects)
304
- if (transferBtn && !isOwnerLocked) transferBtn.style.display = "";
305
- }).catch(function () {
306
- // Not admin, show owner name from limited info
307
- if (ownerId) {
308
- if (ownerNameEl) ownerNameEl.textContent = ownerId;
309
- // Project owner can also transfer unless locked
310
- if (!isOwnerLocked && ctx.myUserId && ctx.myUserId === ownerId && transferBtn) {
311
- transferBtn.style.display = "";
312
- }
313
- } else {
314
- if (ownerNameEl) ownerNameEl.textContent = "Not set";
315
- }
316
- });
317
- hideTransferForm();
318
- } else {
319
- ownerField.style.display = "none";
320
- }
321
- }
219
+ updateIconPreview(settingsProject() ? settingsProject().icon : null);
220
+
221
+ populateProjectSessionVisibilitySettings(settingsSlug(), settingsProject());
222
+ document.getElementById("ps-access-note").textContent = store.get("isMultiUserMode") ? "Choose who can see new conversations in this project. Existing conversations keep their visibility." : "This server is in single-user mode. Project membership and shared session defaults are available in multi-user mode.";
223
+
224
+ var project = settingsProject() || {};
225
+ document.getElementById('ps-project-path').textContent = project.path || (settingsSlug() === store.get('currentSlug') ? store.get('cwd') : '') || 'Folder information unavailable';
322
226
  }
323
227
 
324
228
  function commitRename() {
325
229
  var renameInput = document.getElementById("ps-rename-input");
326
230
  var nameEl = document.getElementById("ps-project-name");
327
231
  var newName = renameInput ? renameInput.value.trim() : "";
328
- if (newName && ctx.ws && ctx.connected) {
329
- ctx.ws.send(JSON.stringify({ type: "set_project_title", slug: currentSlug, title: newName }));
232
+ if (newName && getWs() && store.get('connected')) {
233
+ getWs().send(JSON.stringify({ type: "set_project_title", slug: settingsSlug(), title: newName }));
330
234
  if (nameEl) nameEl.textContent = newName;
331
- if (currentProject) currentProject.name = newName;
235
+ if (settingsProject()) settingsProject().name = newName;
332
236
  var navTitle = document.getElementById("ps-nav-title");
333
237
  if (navTitle) navTitle.textContent = newName;
334
238
  }
335
- cancelRename();
239
+ if (newName && getWs() && store.get("connected")) cancelRename();
240
+ else showToast("Enter a name and connect to the server before saving.");
336
241
  }
337
242
 
338
243
  function cancelRename() {
@@ -342,62 +247,19 @@ function cancelRename() {
342
247
  if (renameBtn) renameBtn.classList.remove("hidden");
343
248
  }
344
249
 
345
- // ===== Owner transfer =====
346
- function showTransferForm() {
347
- var form = document.getElementById("ps-transfer-form");
348
- var btn = document.getElementById("ps-transfer-btn");
349
- var select = document.getElementById("ps-transfer-select");
350
- if (!form || !select) return;
351
-
352
- // Fetch user list and populate select
353
- select.innerHTML = '<option value="">Loading...</option>';
354
- fetch("/api/admin/users").then(function (r) { return r.json(); }).then(function (data) {
355
- var users = data.users || [];
356
- select.innerHTML = "";
357
- for (var i = 0; i < users.length; i++) {
358
- var u = users[i];
359
- var opt = document.createElement("option");
360
- opt.value = u.id;
361
- opt.textContent = (u.displayName || u.username) + (u.linuxUser ? " (" + u.linuxUser + ")" : "");
362
- if (currentProject && u.id === currentProject.projectOwnerId) opt.selected = true;
363
- select.appendChild(opt);
364
- }
365
- }).catch(function () {
366
- select.innerHTML = '<option value="">Failed to load users</option>';
367
- });
368
-
369
- form.classList.remove("hidden");
370
- if (btn) btn.classList.add("hidden");
371
- }
372
-
373
- function hideTransferForm() {
374
- var form = document.getElementById("ps-transfer-form");
375
- var btn = document.getElementById("ps-transfer-btn");
376
- if (form) form.classList.add("hidden");
377
- if (btn) btn.classList.remove("hidden");
378
- }
379
-
380
- function commitTransfer() {
381
- var select = document.getElementById("ps-transfer-select");
382
- var userId = select ? select.value : "";
383
- if (!userId || !ctx.ws || !ctx.connected) return;
384
- ctx.ws.send(JSON.stringify({ type: "transfer_project_owner", slug: currentSlug, userId: userId }));
385
- hideTransferForm();
386
- }
387
-
388
250
  function updateIconPreview(icon) {
389
251
  var preview = document.getElementById("ps-icon-preview");
390
252
  var removeBtn = document.getElementById("ps-icon-remove-btn");
391
253
  if (preview) {
392
254
  preview.textContent = icon || "";
393
255
  if (icon) parseEmojis(preview);
256
+ else { preview.innerHTML = '<i data-lucide="folder"></i>'; refreshIcons(); }
394
257
  }
395
258
  if (removeBtn) {
396
259
  removeBtn.classList.toggle("hidden", !icon);
397
260
  }
398
261
  }
399
262
 
400
- // ===== Emoji picker (inline in settings) =====
401
263
  var psEmojiPickerEl = null;
402
264
 
403
265
  function closePsEmojiPicker() {
@@ -468,8 +330,8 @@ function showPsEmojiPicker() {
468
330
  btn.addEventListener("click", function (e) {
469
331
  e.stopPropagation();
470
332
  closePsEmojiPicker();
471
- if (ctx.ws && ctx.connected) {
472
- ctx.ws.send(JSON.stringify({ type: "set_project_icon", slug: currentSlug, icon: emoji }));
333
+ if (getWs() && store.get('connected')) {
334
+ getWs().send(JSON.stringify({ type: "set_project_icon", slug: settingsSlug(), icon: emoji }));
473
335
  }
474
336
  updateIconPreview(emoji);
475
337
  });
@@ -494,431 +356,13 @@ function showPsEmojiPicker() {
494
356
  psEmojiPickerEl = picker;
495
357
  }
496
358
 
497
- // ===== Defaults =====
498
- function psSendMsg(type, data) {
499
- var ws = ctx.ws;
500
- if (ws && ws.readyState === 1) {
501
- var msg = Object.assign({ type: type }, data);
502
- ws.send(JSON.stringify(msg));
503
- }
504
- }
505
-
506
- function psDefaultsOpts() {
507
- return {
508
- models: ctx.currentModels || [],
509
- currentModel: ctx.currentModel || "",
510
- currentMode: ctx.currentMode || "default",
511
- currentEffort: ctx.currentEffort || "medium",
512
- currentThinking: ctx.currentThinking || "adaptive",
513
- currentThinkingBudget: ctx.currentThinkingBudget || 10000,
514
- currentBetas: ctx.currentBetas || [],
515
- sendMsg: psSendMsg,
516
- modelMsgType: "set_project_default_model",
517
- modeMsgType: "set_project_default_mode",
518
- effortMsgType: "set_project_default_effort",
519
- onModelSelect: function (model) {
520
- renderBetaCard("ps", Object.assign({}, psDefaultsOpts(), { overrideModel: model }));
521
- },
522
- };
523
- }
524
-
525
- function populateDefaults() {
526
- var opts = psDefaultsOpts();
527
- renderModelList("ps", opts);
528
- renderBetaCard("ps", opts);
529
- renderModeList("ps", opts);
530
- renderEffortBar("ps", opts);
531
- renderThinkingBar("ps", opts);
532
- }
533
-
534
- // ===== Instructions (CLAUDE.md) =====
535
- function loadInstructions() {
536
- var editor = document.getElementById("ps-instructions-editor");
537
- var status = document.getElementById("ps-instructions-status");
538
- var saveStatus = document.getElementById("ps-instructions-save-status");
539
- if (saveStatus) saveStatus.textContent = "";
540
-
541
- if (status) status.textContent = "Loading...";
542
-
543
- if (ctx.ws && ctx.connected) {
544
- ctx.ws.send(JSON.stringify({ type: "fs_read", path: "CLAUDE.md" }));
545
- }
546
- }
547
-
548
- export function handleInstructionsRead(msg) {
549
- var editor = document.getElementById("ps-instructions-editor");
550
- var status = document.getElementById("ps-instructions-status");
551
- if (!editor) return;
552
-
553
- if (msg.error) {
554
- editor.value = "";
555
- if (status) status.textContent = "No CLAUDE.md file found. Save to create one.";
556
- } else {
557
- editor.value = msg.content || "";
558
- if (status) status.textContent = "";
559
- }
560
- }
561
-
562
- function saveInstructions() {
563
- var editor = document.getElementById("ps-instructions-editor");
564
- var saveStatus = document.getElementById("ps-instructions-save-status");
565
- if (!editor) return;
566
-
567
- if (ctx.ws && ctx.connected) {
568
- ctx.ws.send(JSON.stringify({ type: "fs_write", path: "CLAUDE.md", content: editor.value }));
569
- if (saveStatus) saveStatus.textContent = "Saving...";
570
- }
571
- }
572
-
573
- export function handleInstructionsWrite(msg) {
574
- var saveStatus = document.getElementById("ps-instructions-save-status");
575
- if (!saveStatus) return;
576
- if (msg.ok) {
577
- saveStatus.textContent = "Saved";
578
- setTimeout(function () { saveStatus.textContent = ""; }, 2000);
579
- } else {
580
- saveStatus.textContent = "Error: " + (msg.error || "Failed to save");
581
- }
582
- }
583
-
584
- // ===== Environment (key-value list) =====
585
- var envSaveTimer = null;
586
- var envStatusTimer = null;
587
-
588
- function showEnvSaveStatus(elementId, text, duration) {
589
- var saveStatus = document.getElementById(elementId);
590
- if (!saveStatus) return;
591
- if (envStatusTimer) clearTimeout(envStatusTimer);
592
- saveStatus.textContent = text;
593
- envStatusTimer = setTimeout(function () { saveStatus.textContent = ""; }, duration);
594
- }
595
-
596
- function loadEnvironment() {
597
- var saveStatus = document.getElementById("ps-env-save-status");
598
- if (saveStatus) saveStatus.textContent = "";
599
-
600
- if (ctx.ws && ctx.connected) {
601
- ctx.ws.send(JSON.stringify({ type: "get_project_env", slug: currentSlug }));
602
- }
603
- }
604
-
605
- export function handleProjectEnv(msg) {
606
- // Parse envrc string into key-value pairs
607
- var list = document.getElementById("ps-env-list");
608
- if (!list) return;
609
- list.innerHTML = "";
610
-
611
- var pairs = parseEnvString(msg.envrc || "");
612
- for (var i = 0; i < pairs.length; i++) {
613
- addEnvRow(pairs[i].key, pairs[i].value, false);
614
- }
615
- refreshIcons();
616
- }
617
-
618
- // Check if text looks like env format: first line starts with a valid VAR_NAME=
619
- export function looksLikeEnv(text) {
620
- var first = text.split("\n")[0].trim();
621
- if (first.indexOf("export ") === 0) first = first.substring(7);
622
- return /^[A-Za-z_][A-Za-z0-9_]*=/.test(first);
623
- }
624
-
625
- export function parseEnvString(str) {
626
- var pairs = [];
627
- if (!str) return pairs;
628
- var lines = str.split("\n");
629
- for (var i = 0; i < lines.length; i++) {
630
- var line = lines[i].trim();
631
- if (!line || line.charAt(0) === "#") continue;
632
- // Strip leading "export "
633
- if (line.indexOf("export ") === 0) line = line.substring(7);
634
- var eq = line.indexOf("=");
635
- if (eq === -1) continue;
636
- var key = line.substring(0, eq).trim();
637
- var val = line.substring(eq + 1).trim();
638
- // Saved values are JSON-quoted so shell metacharacters remain data.
639
- if (val.charAt(0) === '"' && val.charAt(val.length - 1) === '"') {
640
- try { val = JSON.parse(val); } catch (e) { val = val.substring(1, val.length - 1); }
641
- } else if (val.charAt(0) === "'" && val.charAt(val.length - 1) === "'") {
642
- val = val.substring(1, val.length - 1);
643
- }
644
- if (key) pairs.push({ key: key, value: val });
645
- }
646
- return pairs;
647
- }
648
-
649
- export function serializeEnvValue(value) {
650
- return JSON.stringify(value == null ? "" : String(value));
651
- }
652
-
653
- function buildEnvString() {
654
- var list = document.getElementById("ps-env-list");
655
- if (!list) return "";
656
- var rows = list.querySelectorAll(".ps-env-row");
657
- var lines = [];
658
- for (var i = 0; i < rows.length; i++) {
659
- var keyInput = rows[i].querySelector(".ps-env-key");
660
- var valInput = rows[i].querySelector(".ps-env-val");
661
- var key = keyInput ? keyInput.value.trim() : "";
662
- var val = valInput ? valInput.value : "";
663
- if (key) lines.push("export " + key + "=" + serializeEnvValue(val));
664
- }
665
- return lines.join("\n");
666
- }
667
-
668
- function addEnvRow(key, value, focus) {
669
- var list = document.getElementById("ps-env-list");
670
- if (!list) return;
671
-
672
- var row = document.createElement("div");
673
- row.className = "ps-env-row";
674
-
675
- var keyInput = document.createElement("input");
676
- keyInput.type = "text";
677
- keyInput.className = "ps-env-key";
678
- keyInput.placeholder = "KEY";
679
- keyInput.value = key;
680
- keyInput.spellcheck = false;
681
- keyInput.autocomplete = "off";
682
-
683
- var valInput = document.createElement("input");
684
- valInput.type = "text";
685
- valInput.className = "ps-env-val";
686
- valInput.placeholder = "value";
687
- valInput.value = value;
688
- valInput.spellcheck = false;
689
- valInput.autocomplete = "off";
690
-
691
- var delBtn = document.createElement("button");
692
- delBtn.className = "ps-env-del";
693
- delBtn.title = "Remove";
694
- delBtn.innerHTML = '<i data-lucide="x"></i>';
695
-
696
- delBtn.addEventListener("click", function () {
697
- row.remove();
698
- autoSaveEnv();
699
- });
700
-
701
- // Auto-save on change
702
- keyInput.addEventListener("input", function () { autoSaveEnv(); });
703
- valInput.addEventListener("input", function () { autoSaveEnv(); });
704
-
705
- // Paste detection: if pasting KEY=VALUE content into key field, parse it
706
- keyInput.addEventListener("paste", function (e) {
707
- var text = (e.clipboardData || window.clipboardData).getData("text");
708
- if (text && looksLikeEnv(text)) {
709
- e.preventDefault();
710
- var pairs = parseEnvString(text);
711
- if (pairs.length > 0) {
712
- // Fill current row with first pair
713
- keyInput.value = pairs[0].key;
714
- valInput.value = pairs[0].value;
715
- // Add remaining as new rows
716
- for (var p = 1; p < pairs.length; p++) {
717
- addEnvRow(pairs[p].key, pairs[p].value, false);
718
- }
719
- autoSaveEnv();
720
- }
721
- }
722
- });
723
-
724
- // Also handle paste into value field
725
- valInput.addEventListener("paste", function (e) {
726
- var text = (e.clipboardData || window.clipboardData).getData("text");
727
- if (text && text.indexOf("\n") !== -1 && text.indexOf("=") !== -1) {
728
- e.preventDefault();
729
- var pairs = parseEnvString(text);
730
- if (pairs.length > 0) {
731
- keyInput.value = pairs[0].key;
732
- valInput.value = pairs[0].value;
733
- for (var p = 1; p < pairs.length; p++) {
734
- addEnvRow(pairs[p].key, pairs[p].value, false);
735
- }
736
- autoSaveEnv();
737
- }
738
- }
739
- });
740
-
741
- row.appendChild(keyInput);
742
- row.appendChild(valInput);
743
- row.appendChild(delBtn);
744
- list.appendChild(row);
745
- refreshIcons();
746
-
747
- if (focus) keyInput.focus();
748
- }
749
-
750
- function autoSaveEnv() {
751
- if (envSaveTimer) clearTimeout(envSaveTimer);
752
- envSaveTimer = setTimeout(function () {
753
- var envrc = buildEnvString();
754
- if (ctx.ws && ctx.connected) {
755
- ctx.ws.send(JSON.stringify({ type: "set_project_env", slug: currentSlug, envrc: envrc }));
756
- showEnvSaveStatus("ps-env-save-status", "Saving...", 2000);
757
- }
758
- }, 800);
759
- }
760
-
761
- export function handleProjectEnvSaved(msg) {
762
- if (msg.ok) {
763
- showEnvSaveStatus("ps-env-save-status", msg.timing || "Saved", 5000);
764
- } else {
765
- showEnvSaveStatus("ps-env-save-status", "Error: " + (msg.error || "Failed to save"), 5000);
766
- }
767
- }
768
-
769
- // ===== Shared Environment (via tabs) =====
770
- var sharedEnvSaveTimer = null;
771
- var sharedEnvStatusTimer = null;
772
-
773
- function showSharedEnvSaveStatus(text, duration) {
774
- var saveStatus = document.getElementById("ps-shared-env-save-status");
775
- if (!saveStatus) return;
776
- if (sharedEnvStatusTimer) clearTimeout(sharedEnvStatusTimer);
777
- saveStatus.textContent = text;
778
- sharedEnvStatusTimer = setTimeout(function () { saveStatus.textContent = ""; }, duration);
779
- }
780
-
781
- function loadSharedEnv() {
782
- var saveStatus = document.getElementById("ps-shared-env-save-status");
783
- if (saveStatus) saveStatus.textContent = "";
784
-
785
- if (ctx.ws && ctx.connected) {
786
- ctx.ws.send(JSON.stringify({ type: "get_shared_env" }));
787
- }
788
- }
789
-
790
- export function handleProjectSharedEnv(msg) {
791
- var list = document.getElementById("ps-shared-env-list");
792
- if (!list) return;
793
- list.innerHTML = "";
794
-
795
- var pairs = parseEnvString(msg.envrc || "");
796
- for (var i = 0; i < pairs.length; i++) {
797
- addSharedEnvRow(pairs[i].key, pairs[i].value, false);
798
- }
799
- refreshIcons();
800
- }
801
-
802
- export function handleProjectSharedEnvSaved(msg) {
803
- if (msg.ok) {
804
- showSharedEnvSaveStatus(msg.timing || "Saved", 5000);
805
- } else {
806
- showSharedEnvSaveStatus("Error: " + (msg.error || "Failed to save"), 5000);
807
- }
808
- }
809
-
810
- function buildSharedEnvString() {
811
- var list = document.getElementById("ps-shared-env-list");
812
- if (!list) return "";
813
- var rows = list.querySelectorAll(".ps-env-row");
814
- var lines = [];
815
- for (var i = 0; i < rows.length; i++) {
816
- var keyInput = rows[i].querySelector(".ps-env-key");
817
- var valInput = rows[i].querySelector(".ps-env-val");
818
- var key = keyInput ? keyInput.value.trim() : "";
819
- var val = valInput ? valInput.value : "";
820
- if (key) lines.push("export " + key + "=" + serializeEnvValue(val));
821
- }
822
- return lines.join("\n");
823
- }
824
-
825
- function addSharedEnvRow(key, value, focus) {
826
- var list = document.getElementById("ps-shared-env-list");
827
- if (!list) return;
828
-
829
- var row = document.createElement("div");
830
- row.className = "ps-env-row";
831
-
832
- var keyInput = document.createElement("input");
833
- keyInput.type = "text";
834
- keyInput.className = "ps-env-key";
835
- keyInput.placeholder = "KEY";
836
- keyInput.value = key;
837
- keyInput.spellcheck = false;
838
- keyInput.autocomplete = "off";
839
-
840
- var valInput = document.createElement("input");
841
- valInput.type = "text";
842
- valInput.className = "ps-env-val";
843
- valInput.placeholder = "value";
844
- valInput.value = value;
845
- valInput.spellcheck = false;
846
- valInput.autocomplete = "off";
847
-
848
- var delBtn = document.createElement("button");
849
- delBtn.className = "ps-env-del";
850
- delBtn.title = "Remove";
851
- delBtn.innerHTML = '<i data-lucide="x"></i>';
852
-
853
- delBtn.addEventListener("click", function () {
854
- row.remove();
855
- autoSaveSharedEnv();
856
- });
857
-
858
- keyInput.addEventListener("input", function () { autoSaveSharedEnv(); });
859
- valInput.addEventListener("input", function () { autoSaveSharedEnv(); });
860
-
861
- // Paste detection
862
- keyInput.addEventListener("paste", function (e) {
863
- var text = (e.clipboardData || window.clipboardData).getData("text");
864
- if (text && looksLikeEnv(text)) {
865
- e.preventDefault();
866
- var pairs = parseEnvString(text);
867
- if (pairs.length > 0) {
868
- keyInput.value = pairs[0].key;
869
- valInput.value = pairs[0].value;
870
- for (var p = 1; p < pairs.length; p++) {
871
- addSharedEnvRow(pairs[p].key, pairs[p].value, false);
872
- }
873
- autoSaveSharedEnv();
874
- }
875
- }
876
- });
877
-
878
- valInput.addEventListener("paste", function (e) {
879
- var text = (e.clipboardData || window.clipboardData).getData("text");
880
- if (text && text.indexOf("\n") !== -1 && text.indexOf("=") !== -1) {
881
- e.preventDefault();
882
- var pairs = parseEnvString(text);
883
- if (pairs.length > 0) {
884
- keyInput.value = pairs[0].key;
885
- valInput.value = pairs[0].value;
886
- for (var p = 1; p < pairs.length; p++) {
887
- addSharedEnvRow(pairs[p].key, pairs[p].value, false);
888
- }
889
- autoSaveSharedEnv();
890
- }
891
- }
892
- });
893
-
894
- row.appendChild(keyInput);
895
- row.appendChild(valInput);
896
- row.appendChild(delBtn);
897
- list.appendChild(row);
898
- refreshIcons();
899
-
900
- if (focus) keyInput.focus();
901
- }
902
-
903
- function autoSaveSharedEnv() {
904
- if (sharedEnvSaveTimer) clearTimeout(sharedEnvSaveTimer);
905
- sharedEnvSaveTimer = setTimeout(function () {
906
- var envrc = buildSharedEnvString();
907
- if (ctx.ws && ctx.connected) {
908
- ctx.ws.send(JSON.stringify({ type: "set_shared_env", envrc: envrc }));
909
- showSharedEnvSaveStatus("Saving...", 2000);
910
- }
911
- }, 800);
912
- }
913
-
914
- // ===== Update from external events =====
915
359
  export function updateProjectSettingsIcon(icon) {
916
- if (currentProject) currentProject.icon = icon;
360
+ if (settingsProject()) settingsProject().icon = icon;
917
361
  updateIconPreview(icon);
918
362
  }
919
363
 
920
364
  export function updateProjectSettingsName(name) {
921
- if (currentProject) currentProject.name = name;
365
+ if (settingsProject()) settingsProject().name = name;
922
366
  var nameEl = document.getElementById("ps-project-name");
923
367
  if (nameEl) nameEl.textContent = name || "-";
924
368
  var navTitle = document.getElementById("ps-nav-title");
@@ -926,8 +370,8 @@ export function updateProjectSettingsName(name) {
926
370
  }
927
371
 
928
372
  export function handleProjectOwnerChanged(msg) {
929
- if (currentProject) {
930
- currentProject.projectOwnerId = msg.ownerId;
373
+ if (settingsProject()) {
374
+ settingsProject().projectOwnerId = msg.ownerId;
931
375
  }
932
376
  var ownerNameEl = document.getElementById("ps-owner-name");
933
377
  if (ownerNameEl) ownerNameEl.textContent = msg.ownerName || msg.ownerId || "Not set";