clay-server 5.2.0-beta.10 → 5.2.0-beta.12

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 (104) hide show
  1. package/README.md +0 -4
  2. package/lib/clay-product-control-tools.js +1 -0
  3. package/lib/clay-product-control.js +14 -11
  4. package/lib/clay-vendor-usage.js +44 -0
  5. package/lib/github-star-count.js +147 -0
  6. package/lib/notes-checklist.js +43 -1
  7. package/lib/notes.js +2 -1
  8. package/lib/project-session-notes.js +33 -8
  9. package/lib/project-sessions.js +63 -24
  10. package/lib/project-start-protocol.js +29 -0
  11. package/lib/project-vendor-login.js +2 -0
  12. package/lib/project.js +3 -0
  13. package/lib/public/app.js +27 -18
  14. package/lib/public/css/contextual-clay.css +1 -1
  15. package/lib/public/css/filebrowser.css +1 -1
  16. package/lib/public/css/mate-loading-scene.css +21 -0
  17. package/lib/public/css/overlays.css +64 -256
  18. package/lib/public/css/project-mate-creation.css +5 -6
  19. package/lib/public/css/project-start.css +108 -0
  20. package/lib/public/css/projects-hub.css +53 -14
  21. package/lib/public/css/settings-shell.css +103 -0
  22. package/lib/public/css/sidebar.css +7 -0
  23. package/lib/public/css/title-bar.css +83 -0
  24. package/lib/public/index.html +50 -63
  25. package/lib/public/modules/account-transport-settings.js +8 -2
  26. package/lib/public/modules/admin-settings-email.js +7 -2
  27. package/lib/public/modules/admin-settings-invitations.js +2 -2
  28. package/lib/public/modules/admin-settings-projects.js +2 -2
  29. package/lib/public/modules/admin-settings-ui.js +20 -4
  30. package/lib/public/modules/admin-settings-users.js +4 -4
  31. package/lib/public/modules/app-home-hub.js +2 -1
  32. package/lib/public/modules/app-messages.js +10 -2
  33. package/lib/public/modules/app-projects.js +1 -407
  34. package/lib/public/modules/app-rate-limit.js +12 -1
  35. package/lib/public/modules/clay-tool-presentation.js +1 -0
  36. package/lib/public/modules/contextual-clay.js +3 -1
  37. package/lib/public/modules/github-star.js +66 -0
  38. package/lib/public/modules/home-surface-boot.js +0 -1
  39. package/lib/public/modules/mate-loading-scene.js +17 -0
  40. package/lib/public/modules/pane-appearance-bridge.js +59 -0
  41. package/lib/public/modules/profile.js +11 -3
  42. package/lib/public/modules/project-mate-creation-workspace.js +13 -4
  43. package/lib/public/modules/project-session-visibility-settings.js +19 -16
  44. package/lib/public/modules/project-settings-environment.js +12 -1
  45. package/lib/public/modules/project-settings.js +119 -129
  46. package/lib/public/modules/project-start-dialog.js +130 -0
  47. package/lib/public/modules/project-start-folder-state.js +78 -0
  48. package/lib/public/modules/project-start-folder-view.js +190 -0
  49. package/lib/public/modules/project-start-forms.js +66 -0
  50. package/lib/public/modules/project-start-path.js +60 -0
  51. package/lib/public/modules/project-start-state.js +54 -0
  52. package/lib/public/modules/project-start-submit.js +67 -0
  53. package/lib/public/modules/projects-hub-view.js +32 -44
  54. package/lib/public/modules/projects-hub.js +39 -28
  55. package/lib/public/modules/server-settings.js +36 -91
  56. package/lib/public/modules/settings-project-target.js +56 -0
  57. package/lib/public/modules/settings-select.js +1 -1
  58. package/lib/public/modules/settings-shell-registry.js +88 -0
  59. package/lib/public/modules/settings-shell-view.js +104 -0
  60. package/lib/public/modules/settings-shell.js +342 -0
  61. package/lib/public/modules/sidebar-mobile.js +8 -30
  62. package/lib/public/modules/sidebar-projects.js +2 -1
  63. package/lib/public/modules/theme.js +31 -3
  64. package/lib/public/modules/user-settings-integrations.js +2 -1
  65. package/lib/public/modules/user-settings-layout.js +12 -34
  66. package/lib/public/modules/user-settings-profile.js +1 -1
  67. package/lib/public/modules/user-settings.js +18 -85
  68. package/lib/public/modules/vendor-usage-format.js +93 -0
  69. package/lib/public/modules/vendor-usage-list.js +45 -0
  70. package/lib/public/modules/vendor-usage-model.js +215 -0
  71. package/lib/public/modules/vendor-usage-overview.js +323 -0
  72. package/lib/public/style.css +10 -7
  73. package/lib/public/welcome-art/conversation-desk.png +0 -0
  74. package/lib/public/welcome-art/mate-clay.png +0 -0
  75. package/lib/public/welcome-art/project-clay.png +0 -0
  76. package/lib/sdk-bridge.js +6 -1
  77. package/lib/sdk-message-processor.js +9 -1
  78. package/lib/sdk-quota-snapshot.js +20 -2
  79. package/lib/server-github-stars.js +39 -0
  80. package/lib/server-global-ws.js +21 -17
  81. package/lib/server.js +11 -0
  82. package/lib/session-notes-mcp-server.js +26 -4
  83. package/lib/session-notes-writes.js +88 -0
  84. package/lib/session-visibility.js +20 -8
  85. package/lib/sessions.js +2 -0
  86. package/lib/vendor-usage-acp-readers.js +97 -0
  87. package/lib/vendor-usage-capabilities.js +88 -0
  88. package/lib/vendor-usage-http-readers.js +214 -0
  89. package/lib/vendor-usage-metrics.js +47 -0
  90. package/lib/vendor-usage-native-readers.js +106 -0
  91. package/lib/vendor-usage-overview.js +406 -0
  92. package/lib/vendor-usage-service.js +281 -0
  93. package/lib/ws-schema.js +4 -2
  94. package/lib/yoke/adapters/claude.js +12 -0
  95. package/lib/yoke/adapters/codex-query-contract.js +1 -0
  96. package/lib/yoke/adapters/codex.js +45 -12
  97. package/lib/yoke/claude-hook-installer.js +1 -0
  98. package/lib/yoke/codex-dynamic-tool-names.js +80 -0
  99. package/lib/yoke/codex-model-catalog.js +37 -3
  100. package/lib/yoke/muse-services.js +16 -3
  101. package/lib/yoke/providers/claude-model-descriptions.js +22 -2
  102. package/lib/yoke/vendor-registry.js +6 -2
  103. package/package.json +2 -1
  104. package/lib/public/modules/server-settings-navigation.js +0 -39
@@ -0,0 +1,59 @@
1
+ // Live appearance sync between the app shell and its pane iframes (split
2
+ // Driver/Worker panes and the contextual Mate window). Each iframe runs its own
3
+ // theme module and only reads the persisted preference at load, so the shell
4
+ // pushes the applied appearance down on every change and when a frame loads.
5
+ // Same-origin only, shell to child only; nothing is saved from either side.
6
+
7
+ import { store } from './store.js';
8
+ import { onThemeChange, getAppearanceSnapshot, applyExternalAppearance } from './theme.js';
9
+
10
+ var ID = /^[a-zA-Z0-9_-]{1,120}$/;
11
+
12
+ function paneFrames() {
13
+ return Array.prototype.filter.call(document.querySelectorAll('iframe'), function (frame) {
14
+ try { var url = new URL(frame.src, window.location.href); return url.origin === window.location.origin && url.searchParams.get('pane') === '1'; }
15
+ catch (error) { return false; }
16
+ });
17
+ }
18
+
19
+ function push(frame) {
20
+ var snapshot = getAppearanceSnapshot();
21
+ try { if (frame.contentWindow) frame.contentWindow.postMessage({ type: 'clay-appearance', snapshot: snapshot }, window.location.origin); } catch (error) {}
22
+ }
23
+
24
+ function isId(value) { return typeof value === 'string' && ID.test(value); }
25
+ function isIdOrNull(value) { return value === null || isId(value); }
26
+
27
+ // Exact shape only: strings or null, never coerced values.
28
+ function validSnapshot(value) {
29
+ var pref = value && typeof value === 'object' ? value.preference : null;
30
+ if (!pref || typeof pref !== 'object' || !isId(value.themeId)) return false;
31
+ if (value.userId !== null && (typeof value.userId !== 'string' || !value.userId || value.userId.length > 200)) return false;
32
+ if (typeof pref.configured !== 'boolean') return false;
33
+ if (pref.mode !== null && pref.mode !== 'light' && pref.mode !== 'dark') return false;
34
+ return isIdOrNull(pref.theme) && isIdOrNull(pref.lightTheme) && isIdOrNull(pref.darkTheme);
35
+ }
36
+
37
+ export function initPaneAppearanceBridge() {
38
+ if (store.get('paneMode')) {
39
+ if (window.parent === window) return;
40
+ window.addEventListener('message', function (event) {
41
+ if (event.origin !== window.location.origin || event.source !== window.parent) return;
42
+ var msg = event.data;
43
+ if (!msg || msg.type !== 'clay-appearance' || !validSnapshot(msg.snapshot)) return;
44
+ // Once this frame knows its owner the snapshot must name exactly that owner (null is a mismatch).
45
+ // Before identity is known, or in solo mode without one, any well-formed snapshot applies.
46
+ var owner = store.get('myUserId') || null;
47
+ if (owner !== null && msg.snapshot.userId !== owner) return;
48
+ applyExternalAppearance(msg.snapshot);
49
+ });
50
+ return;
51
+ }
52
+ onThemeChange(function () { paneFrames().forEach(push); });
53
+ // Frames that rejected an early snapshot (identity not yet known here) get it again once it is.
54
+ store.subscribe(function (state, previous) { if (state.myUserId !== previous.myUserId) paneFrames().forEach(push); });
55
+ // A frame that finished loading may have been seeded before a pending save.
56
+ document.addEventListener('load', function (event) {
57
+ if (event.target && event.target.tagName === 'IFRAME' && paneFrames().indexOf(event.target) >= 0) push(event.target);
58
+ }, true);
59
+ }
@@ -135,9 +135,12 @@ export function uploadAvatarBlob(blob) {
135
135
  }
136
136
 
137
137
  // --- Avatar position picker ---
138
- export function showAvatarPositioner(img, objectUrl, onDone) {
138
+ // With a host element the editor mounts inline as a settings subview instead
139
+ // of a page-level overlay.
140
+ export function showAvatarPositioner(img, objectUrl, onDone, host) {
139
141
  var outputSize = 256;
140
142
  var viewSize = 220;
143
+ var priorFocus = document.activeElement;
141
144
 
142
145
  // State
143
146
  var scale = 1;
@@ -169,7 +172,7 @@ export function showAvatarPositioner(img, objectUrl, onDone) {
169
172
 
170
173
  // Build overlay
171
174
  var overlay = document.createElement('div');
172
- overlay.className = 'avatar-positioner-overlay';
175
+ overlay.className = host ? 'avatar-positioner-inline settings-subview' : 'avatar-positioner-overlay';
173
176
 
174
177
  var container = document.createElement('div');
175
178
  container.className = 'avatar-positioner-container';
@@ -225,8 +228,10 @@ export function showAvatarPositioner(img, objectUrl, onDone) {
225
228
  container.appendChild(btnRow);
226
229
 
227
230
  overlay.appendChild(container);
228
- document.body.appendChild(overlay);
231
+ if (host) { host.classList.add('has-subview'); host.appendChild(overlay); overlay.setAttribute('role', 'region'); overlay.setAttribute('aria-label', 'Position your avatar'); }
232
+ else document.body.appendChild(overlay);
229
233
  updatePreview();
234
+ if (host) doneFocus();
230
235
 
231
236
  // Drag
232
237
  var dragging = false;
@@ -288,9 +293,12 @@ export function showAvatarPositioner(img, objectUrl, onDone) {
288
293
  updatePreview();
289
294
  }, { passive: false });
290
295
 
296
+ function doneFocus() { closeBtn.focus(); }
297
+
291
298
  function cleanup(revoke) {
292
299
  document.removeEventListener('keydown', onOverlayKeydown);
293
300
  overlay.remove();
301
+ if (host) { host.classList.remove('has-subview'); if (priorFocus && priorFocus.isConnected) priorFocus.focus(); }
294
302
  if (revoke) URL.revokeObjectURL(objectUrl);
295
303
  }
296
304
 
@@ -81,6 +81,9 @@ function startMateCreation() {
81
81
  maybeStartPendingCreation();
82
82
  }
83
83
 
84
+ // Entry for the zero-project welcome: the same Clay-guided flow the add button's Mate choice runs.
85
+ export function startClayMateCreation() { startMateCreation(); }
86
+
84
87
  function maybeStartPendingCreation() {
85
88
  var state = store.snap();
86
89
  if (!state.projectMateCreationPendingStart || !state.connected || state.activeProjectSlug !== 'mate-' + state.projectMateCreationMateId) return;
@@ -89,11 +92,17 @@ function maybeStartPendingCreation() {
89
92
  store.set({ projectMateCreationPendingStart: false, projectMateCreationAwaitingHistory: true, projectMateCreationRequestId: requestId });
90
93
  }
91
94
 
92
- function choiceButton(icon, title, description) {
95
+ function choiceButton(artwork, title, description) {
93
96
  var button = document.createElement('button');
94
97
  button.type = 'button';
95
98
  button.className = 'project-create-choice-option';
96
- button.innerHTML = iconHtml(icon);
99
+ var image = document.createElement('img');
100
+ image.src = '/welcome-art/' + artwork + '.png';
101
+ image.alt = '';
102
+ image.width = 1536;
103
+ image.height = 1024;
104
+ image.decoding = 'async';
105
+ button.appendChild(image);
97
106
  var copy = document.createElement('span');
98
107
  var heading = document.createElement('strong');
99
108
  heading.textContent = title;
@@ -121,9 +130,9 @@ export function openProjectCreateChoice(opener, openProject) {
121
130
  var title = document.createElement('h2');
122
131
  title.id = 'project-create-choice-title';
123
132
  title.textContent = 'What would you like to add?';
124
- var mate = choiceButton('users', 'Mate', 'Shape a new collaborator with Clay');
133
+ var mate = choiceButton('mate-clay', 'Create a Mate', 'A specialist to work alongside you.');
125
134
  mate.addEventListener('click', startMateCreation);
126
- var project = choiceButton('folder-plus', 'Project', 'Add a local workspace');
135
+ var project = choiceButton('project-clay', 'Start a project', 'Work on code or files together.');
127
136
  project.addEventListener('click', function () {
128
137
  closeChoice(false);
129
138
  openProject();
@@ -27,12 +27,10 @@ function setSelectState(select, value, disabled) {
27
27
  select.disabled = !!disabled;
28
28
  }
29
29
 
30
- function isCurrentProject(active) {
31
- return !!(active && active.slug && active.slug === store.get('currentSlug'));
32
- }
33
-
30
+ // Editable for any authorised selected project: the server re-authorises the
31
+ // exact slug (owner, permission, access, worktree) and echoes it back.
34
32
  function isReadOnly(active) {
35
- return !active || !active.authorized || active.isWorktree || !isCurrentProject(active);
33
+ return !active || !active.authorized || active.isWorktree;
36
34
  }
37
35
 
38
36
  function renderActiveState() {
@@ -40,23 +38,26 @@ function renderActiveState() {
40
38
  var pending = pendingState();
41
39
  var select = selectElement();
42
40
  var inheritedHint = document.getElementById("ps-session-visibility-default-inherited-hint");
43
- var nonCurrentHint = document.getElementById("ps-session-visibility-default-non-current-hint");
44
41
  if (inheritedHint) inheritedHint.classList.toggle("hidden", !(active && active.isWorktree));
45
- if (nonCurrentHint) nonCurrentHint.classList.toggle("hidden", !(active && active.authorized && !active.isWorktree && !isCurrentProject(active)));
46
42
  if (!select || !active) return;
47
- setSelectState(select, active.effectiveValue, isReadOnly(active) || !!(pending && pending.slug === active.slug));
43
+ // One request at a time: a pending change for any project holds the control.
44
+ setSelectState(select, active.effectiveValue, isReadOnly(active) || !!pending);
48
45
  }
49
46
 
50
- function isAuthorized(project) {
47
+ // The server requires the project owner plus the projectSettings permission.
48
+ // An administrator who is not the owner may see the value but not change it.
49
+ function visibilityAccess(project) {
51
50
  var users = store.get('cachedAllUsers') || [];
52
51
  var userId = store.get('myUserId');
53
52
  var currentUser = null;
54
53
  for (var i = 0; i < users.length; i++) {
55
54
  if (users[i].id === userId) { currentUser = users[i]; break; }
56
55
  }
57
- return !!(store.get('isMultiUserMode') && project &&
58
- ((project.projectOwnerId && project.projectOwnerId === userId) || (currentUser && currentUser.role === "admin")) &&
59
- (!store.get('permissions') || store.get('permissions').projectSettings !== false));
56
+ var permitted = !store.get('permissions') || store.get('permissions').projectSettings !== false;
57
+ var owner = !!(project && project.projectOwnerId && project.projectOwnerId === userId);
58
+ var admin = !!(currentUser && currentUser.role === "admin");
59
+ var multi = !!(store.get('isMultiUserMode') && project && permitted);
60
+ return { visible: multi && (owner || admin), editable: multi && owner };
60
61
  }
61
62
 
62
63
  export function initProjectSessionVisibilitySettings() {
@@ -80,7 +81,6 @@ export function initProjectSessionVisibilitySettings() {
80
81
  });
81
82
 
82
83
  store.subscribe(function (state, previous) {
83
- if (state.currentSlug !== previous.currentSlug) renderActiveState();
84
84
  if (state.connected || previous.connected !== true) return;
85
85
  var pending = pendingState();
86
86
  if (!pending) return;
@@ -95,10 +95,13 @@ export function initProjectSessionVisibilitySettings() {
95
95
  export function populateProjectSessionVisibilitySettings(slug, project) {
96
96
  var field = document.getElementById("ps-session-visibility-default-field");
97
97
  var select = selectElement();
98
- if (field) field.style.display = isAuthorized(project) ? "" : "none";
98
+ var access = visibilityAccess(project);
99
+ if (field) field.style.display = access.visible ? "" : "none";
100
+ var ownerHint = document.getElementById("ps-session-visibility-default-owner-hint");
101
+ if (ownerHint) ownerHint.classList.toggle("hidden", !(access.visible && !access.editable));
99
102
  if (!select) return;
100
103
  var value = project && project.sessionVisibilityDefault === "shared" ? "shared" : "private";
101
- var active = { slug: slug || "", effectiveValue: value, isWorktree: !!(project && project.isWorktree), authorized: isAuthorized(project) };
104
+ var active = { slug: slug || "", effectiveValue: value, isWorktree: !!(project && project.isWorktree), authorized: access.editable };
102
105
  var pending = pendingState();
103
106
  setVisibilityState(active, pending);
104
107
  renderActiveState();
@@ -124,7 +127,7 @@ export function handleProjectSessionVisibilityDefault(msg) {
124
127
  var active = activeState();
125
128
  setVisibilityState(active, null);
126
129
  var select = selectElement();
127
- if (!select || !active || active.slug !== msg.slug) return;
130
+ if (!select || !active || active.slug !== msg.slug) { renderActiveState(); return; }
128
131
  if (!msg.ok) {
129
132
  active = Object.assign({}, active, { effectiveValue: pending.previous });
130
133
  setVisibilityState(active, null);
@@ -31,7 +31,18 @@ export function handleProjectEnv(msg) {
31
31
  update({ loaded: true, saved: msg.envrc || '', rows: parseEnvString(msg.envrc || '') });
32
32
  render(); status('');
33
33
  }
34
- function changeRows(rows) { update({ rows: rows, dirty: true }); document.getElementById('ps-env-save').disabled = false; status('Unsaved changes'); }
34
+ // A draft exists only while the rows differ from the saved variables; blank rows and edits that are
35
+ // reverted by hand leave nothing to save.
36
+ function rowsSignature(rows) {
37
+ return (rows || []).filter(function (row) { return row.key.trim() || row.value; }).map(function (row) { return row.key.trim() + '\u0000' + row.value; }).join('\n');
38
+ }
39
+ function changeRows(rows) {
40
+ var dirty = rowsSignature(rows) !== rowsSignature(parseEnvString(state().saved || ''));
41
+ update({ rows: rows, dirty: dirty });
42
+ document.getElementById('ps-env-save').disabled = !dirty || !!state().saving;
43
+ if (!dirty) clearCachedDraft();
44
+ status(dirty ? 'Unsaved changes' : '');
45
+ }
35
46
  function render() {
36
47
  var editor = state();
37
48
  var list = document.getElementById('ps-env-list'); list.replaceChildren();
@@ -1,76 +1,64 @@
1
1
  import { store } from './store.js';
2
2
  import { getWs } from './ws-ref.js';
3
- import { trapSettingsFocus, isolateSettingsDialog, confirmSettingsAction } from './server-settings-state.js';
4
3
  import { initProjectEnvironment, openProjectEnvironment, projectEnvironmentDirty, discardProjectEnvironment } from './project-settings-environment.js';
5
4
  import { enhanceSettingsSelects } from './settings-select.js';
6
- // Project identity and access in a focused settings dialog.
5
+ // Project identity, access and environment: the Project pane of the unified
6
+ // settings shell. The shell owns the selected target; this pane only renders
7
+ // and edits the project it was last bound to.
7
8
  import { refreshIcons } from './icons.js';
8
9
  import { showToast } from './utils.js';
9
10
  import { parseEmojis } from './markdown.js';
10
- import { holdNotesForSettings, releaseNotesForSettings } from './sticky-notes.js';
11
- import { openNotesBrowser, closeNotesBrowser, isNotesBrowserOpen } from './sticky-notes-browser.js';
11
+ import { registerSettingsScope, openSettings, closeSettings, isSettingsShellOpen, activeSettingsScope } from './settings-shell.js';
12
12
  import { initProjectSessionVisibilitySettings, populateProjectSessionVisibilitySettings } from './project-session-visibility-settings.js';
13
13
 
14
14
  var panelEl = null;
15
- var navItems = null;
16
15
  var sections = null;
17
- var notesBrowserReopen = false;
18
- var restoreBackground = null;
19
- var returnFocus = null;
20
16
  function settingsSlug() { return store.get("projectSettingsSlug"); }
21
17
  function settingsProject() { return store.get("projectSettingsProject"); }
22
18
 
23
19
  // Emoji categories (reuse from sidebar)
24
20
  var EMOJI_CATEGORIES = null;
25
21
 
22
+ function renameDirty() {
23
+ var rename = document.getElementById("ps-rename-form");
24
+ var project = settingsProject();
25
+ return !!(rename && project && !rename.classList.contains("hidden") && document.getElementById("ps-rename-input").value.trim() !== (project.name || ""));
26
+ }
27
+
28
+ function discardDrafts() {
29
+ discardProjectEnvironment();
30
+ cancelRename();
31
+ }
32
+
33
+ var controller = {
34
+ enter: function () {},
35
+ activate: function (page) { switchSection(page); },
36
+ leave: function () { closePsEmojiPicker(); },
37
+ busy: function (kind) { return kind !== "page" && (store.get("projectEnvironmentEditor") || {}).saving ? "Wait for the environment save to finish." : ""; },
38
+ leavePrompt: function (kind) {
39
+ var env = projectEnvironmentDirty();
40
+ var rename = renameDirty();
41
+ if (!env && !rename) return null;
42
+ if (kind === "close") return { message: "Your changes have not been saved. Discard them and close?", action: "Discard changes", apply: discardDrafts };
43
+ if (kind === "page") return null;
44
+ if (kind === "project" && rename) return { message: "Your unsaved project name change will be discarded when you switch projects. An unsaved environment draft stays in this tab.", action: "Discard name and switch", apply: cancelRename };
45
+ return { message: "Your unsaved project changes will remain in this tab. Leave this editor and keep the draft?", action: "Keep draft and leave" };
46
+ },
47
+ retarget: bindProjectSettings,
48
+ refreshTarget: refreshProjectSettings,
49
+ clearTarget: clearProjectSettings,
50
+ setEmpty: setPaneEmpty
51
+ };
26
52
 
27
53
  export function initProjectSettings(emojiCategories) {
28
54
  EMOJI_CATEGORIES = emojiCategories;
29
55
  panelEl = document.getElementById("project-settings");
30
56
  if (!panelEl) return;
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
- });
57
+ controller.pane = panelEl;
58
+ registerSettingsScope("project", controller);
42
59
  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
- });
48
- navItems = panelEl.querySelectorAll(".settings-nav-item");
49
60
  sections = panelEl.querySelectorAll(".ps-section");
50
61
 
51
- // Nav clicks
52
- for (var i = 0; i < navItems.length; i++) {
53
- navItems[i].addEventListener("click", function () {
54
- switchSection(this.dataset.section);
55
- });
56
- }
57
-
58
- // Mobile dropdown nav
59
- var psNavDropdown = document.getElementById("ps-nav-dropdown");
60
- if (psNavDropdown) {
61
- psNavDropdown.addEventListener("change", function () {
62
- switchSection(psNavDropdown.value);
63
- });
64
- }
65
-
66
- // Close button
67
- var closeBtn = document.getElementById("project-settings-close");
68
- if (closeBtn) {
69
- closeBtn.addEventListener("click", function () {
70
- closeProjectSettings();
71
- });
72
- }
73
-
74
62
  // Profile: rename
75
63
  var renameBtn = document.getElementById("ps-rename-btn");
76
64
  var renameForm = document.getElementById("ps-rename-form");
@@ -111,98 +99,105 @@ export function initProjectSettings(emojiCategories) {
111
99
  }
112
100
  if (iconRemoveBtn) {
113
101
  iconRemoveBtn.addEventListener("click", function () {
114
- if (getWs() && store.get('connected')) {
115
- getWs().send(JSON.stringify({ type: "set_project_icon", slug: settingsSlug(), icon: null }));
116
- }
117
- updateIconPreview(null);
102
+ var slug = settingsSlug();
103
+ var previous = { icon: settingsProject() ? settingsProject().icon || null : null };
104
+ if (slug && sendEdit("set_project_icon", { slug: slug, icon: null }, previous)) updateIconPreview(null);
118
105
  });
119
106
  }
120
107
 
121
108
  initProjectSessionVisibilitySettings();
122
- store.subscribe(function (next, previous) { if (next.myUserId !== previous.myUserId) closeProjectSettings({ cleanup: true }); });
123
109
  }
124
110
 
125
- export function openProjectSettings(slug, project) {
126
- if (!panelEl) return;
127
- returnFocus = document.activeElement;
128
- if (!returnFocus || returnFocus === document.body) returnFocus = document.getElementById("title-bar-project-dropdown");
129
- store.set({ projectSettingsSlug: slug, projectSettingsProject: project });
111
+ // Entry for the project context menu and mobile sheet: show this exact
112
+ // project's settings without changing the open workspace.
113
+ export function openProjectSettings(slug) {
114
+ return openSettings({ scope: "project", page: "profile", slug: slug });
115
+ }
116
+
117
+ // Bind the pane to one project. Called by the shell for every target change.
118
+ function bindProjectSettings(slug, project) {
119
+ var same = settingsSlug() === slug;
120
+ store.set({ projectSettingsSlug: slug, projectSettingsProject: Object.assign({}, project, { name: project.title || project.project || project.name || slug }) });
130
121
  var env = store.get("projectEnvironmentEditor");
131
122
  if (env && !env.dirty && !env.saving) store.set({ projectEnvironmentEditor: Object.assign({}, env, { loaded: false, loading: false }) });
123
+ if (same) refreshProjectSettings(slug, project);
124
+ else { closePsEmojiPicker(); populateProfile(); }
125
+ var active = panelEl.querySelector(".ps-section.active");
126
+ if (active && active.dataset.section === "environment") openProjectEnvironment(slug);
127
+ }
132
128
 
133
- // Set nav title
134
- var navTitle = document.getElementById("ps-nav-title");
135
- if (navTitle) navTitle.textContent = project.name || slug;
136
-
137
- // Reset to first section
138
- switchSection("profile");
139
-
140
- // Populate profile
141
- populateProfile();
129
+ function refreshProjectSettings(slug, project) {
130
+ if (settingsSlug() !== slug) return;
131
+ var name = project.title || project.project || project.name || slug;
132
+ store.set({ projectSettingsProject: Object.assign({}, project, { name: name }) });
133
+ var nameEl = document.getElementById("ps-project-name");
134
+ if (nameEl) nameEl.textContent = name || "-";
135
+ updateIconPreview(project.icon || null);
136
+ populateProjectSessionVisibilitySettings(slug, settingsProject());
137
+ document.getElementById('ps-project-path').textContent = project.path || (slug === store.get('currentSlug') ? store.get('cwd') : '') || 'Folder information unavailable';
138
+ }
142
139
 
140
+ // The selected project is gone or no longer authorised.
141
+ function clearProjectSettings(slug) {
142
+ var env = store.get("projectEnvironmentEditor") || {};
143
+ var drafts = Object.assign({}, store.get("projectEnvironmentDrafts") || {});
144
+ delete drafts[slug];
145
+ store.set({ projectSettingsSlug: null, projectSettingsProject: null, projectEnvironmentDrafts: drafts });
146
+ if (env.slug === slug) { store.set({ projectEnvironmentEditor: {} }); }
147
+ closePsEmojiPicker();
148
+ cancelRename();
149
+ }
143
150
 
144
- // Sticky notes must not sit over settings; remember what to restore.
145
- if (panelEl.classList.contains("hidden")) {
146
- notesBrowserReopen = isNotesBrowserOpen();
147
- if (notesBrowserReopen) closeNotesBrowser();
148
- holdNotesForSettings();
151
+ var EMPTY_HEADINGS = { loading: "Loading projects", empty: "No project selected", error: "Projects unavailable", lost: "Project unavailable" };
152
+ function setPaneEmpty(text, kind) {
153
+ var empty = document.getElementById("ps-empty");
154
+ if (empty) {
155
+ empty.hidden = !text;
156
+ document.getElementById("ps-empty-text").textContent = text || "";
157
+ empty.querySelector("h2").textContent = EMPTY_HEADINGS[kind] || EMPTY_HEADINGS.empty;
149
158
  }
159
+ panelEl.classList.toggle("ps-no-target", !!text);
160
+ }
150
161
 
151
- // Show panel
152
- panelEl.classList.remove("hidden");
153
- if (!restoreBackground) restoreBackground = isolateSettingsDialog(panelEl);
154
- document.getElementById("project-settings-close").focus();
155
- enhanceSettingsSelects(panelEl);
156
- refreshIcons();
162
+ // Rename and icon edits are bound to the exact project, request and (for the
163
+ // reply) the slug the server echoes; a reply for another project never
164
+ // changes the view.
165
+ var pendingEdits = {};
166
+ var editSequence = 0;
167
+ function sendEdit(type, fields, previous) {
168
+ var ws = getWs();
169
+ if (!ws || !store.get("connected")) return false;
170
+ editSequence += 1;
171
+ var requestId = "project-settings-" + Date.now() + "-" + editSequence;
172
+ pendingEdits[requestId] = { slug: fields.slug, type: type, previous: previous };
173
+ ws.send(JSON.stringify(Object.assign({ type: type, requestId: requestId }, fields)));
174
+ return true;
175
+ }
176
+
177
+ export function handleProjectSettingsEditResult(msg) {
178
+ var pending = msg && msg.requestId && pendingEdits[msg.requestId];
179
+ if (!pending) return;
180
+ delete pendingEdits[msg.requestId];
181
+ if (msg.slug !== pending.slug || msg.ok || settingsSlug() !== pending.slug) return;
182
+ if (pending.type === "set_project_title") updateProjectSettingsName(pending.previous.name);
183
+ else updateProjectSettingsIcon(pending.previous.icon);
157
184
  }
158
185
 
159
- // `cleanup` is for teardown while leaving the project: the notes workbench is
160
- // not reopened over whatever screen comes next.
161
186
  export function closeProjectSettings(opts) {
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
- }
172
- var wasOpen = !panelEl.classList.contains("hidden");
173
- panelEl.classList.add("hidden");
174
- closePsEmojiPicker();
175
- if (restoreBackground) { restoreBackground(); restoreBackground = null; }
176
- if (returnFocus && returnFocus.isConnected && !(opts && opts.cleanup)) returnFocus.focus();
177
- if (wasOpen) {
178
- releaseNotesForSettings();
179
- if (notesBrowserReopen && !(opts && opts.cleanup)) openNotesBrowser();
180
- notesBrowserReopen = false;
181
- }
187
+ closeSettings({ cleanup: !!(opts && opts.cleanup), onlyScope: "project" });
182
188
  }
183
189
 
184
190
  export function isProjectSettingsOpen() {
185
- return panelEl && !panelEl.classList.contains("hidden");
191
+ return isSettingsShellOpen() && activeSettingsScope() === "project";
186
192
  }
187
193
 
188
194
  function switchSection(name) {
189
- for (var i = 0; i < navItems.length; i++) {
190
- var active = navItems[i].dataset.section === name;
191
- navItems[i].classList.toggle("active", active);
192
- navItems[i].setAttribute("aria-current", active ? "page" : "false");
193
- }
194
195
  for (var j = 0; j < sections.length; j++) {
195
- var active2 = sections[j].dataset.section === name;
196
- sections[j].classList.toggle("active", active2);
196
+ sections[j].classList.toggle("active", sections[j].dataset.section === name);
197
197
  }
198
-
199
- // Sync mobile dropdown
200
- var psNavDropdown = document.getElementById("ps-nav-dropdown");
201
- if (psNavDropdown && psNavDropdown.value !== name) {
202
- psNavDropdown.value = name;
203
- }
204
-
205
- if (name === "environment") openProjectEnvironment(settingsSlug());
198
+ if (name === "environment" && settingsSlug()) openProjectEnvironment(settingsSlug());
199
+ enhanceSettingsSelects(panelEl);
200
+ refreshIcons();
206
201
  }
207
202
 
208
203
  function populateProfile() {
@@ -229,15 +224,13 @@ function commitRename() {
229
224
  var renameInput = document.getElementById("ps-rename-input");
230
225
  var nameEl = document.getElementById("ps-project-name");
231
226
  var newName = renameInput ? renameInput.value.trim() : "";
232
- if (newName && getWs() && store.get('connected')) {
233
- getWs().send(JSON.stringify({ type: "set_project_title", slug: settingsSlug(), title: newName }));
227
+ var slug = settingsSlug();
228
+ var previous = { name: settingsProject() ? settingsProject().name : "" };
229
+ if (newName && slug && sendEdit("set_project_title", { slug: slug, title: newName }, previous)) {
234
230
  if (nameEl) nameEl.textContent = newName;
235
231
  if (settingsProject()) settingsProject().name = newName;
236
- var navTitle = document.getElementById("ps-nav-title");
237
- if (navTitle) navTitle.textContent = newName;
238
- }
239
- if (newName && getWs() && store.get("connected")) cancelRename();
240
- else showToast("Enter a name and connect to the server before saving.");
232
+ cancelRename();
233
+ } else showToast("Enter a name and connect to the server before saving.");
241
234
  }
242
235
 
243
236
  function cancelRename() {
@@ -330,10 +323,9 @@ function showPsEmojiPicker() {
330
323
  btn.addEventListener("click", function (e) {
331
324
  e.stopPropagation();
332
325
  closePsEmojiPicker();
333
- if (getWs() && store.get('connected')) {
334
- getWs().send(JSON.stringify({ type: "set_project_icon", slug: settingsSlug(), icon: emoji }));
335
- }
336
- updateIconPreview(emoji);
326
+ var slug = settingsSlug();
327
+ var previous = { icon: settingsProject() ? settingsProject().icon || null : null };
328
+ if (slug && sendEdit("set_project_icon", { slug: slug, icon: emoji }, previous)) updateIconPreview(emoji);
337
329
  });
338
330
  grid.appendChild(btn);
339
331
  })(emojis[i]);
@@ -365,8 +357,6 @@ export function updateProjectSettingsName(name) {
365
357
  if (settingsProject()) settingsProject().name = name;
366
358
  var nameEl = document.getElementById("ps-project-name");
367
359
  if (nameEl) nameEl.textContent = name || "-";
368
- var navTitle = document.getElementById("ps-nav-title");
369
- if (navTitle) navTitle.textContent = name || "-";
370
360
  }
371
361
 
372
362
  export function handleProjectOwnerChanged(msg) {