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

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 (98) 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 +25 -17
  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 +79 -0
  24. package/lib/public/index.html +50 -63
  25. package/lib/public/modules/admin-settings-invitations.js +2 -2
  26. package/lib/public/modules/admin-settings-projects.js +2 -2
  27. package/lib/public/modules/admin-settings-ui.js +20 -4
  28. package/lib/public/modules/admin-settings-users.js +4 -4
  29. package/lib/public/modules/app-home-hub.js +2 -1
  30. package/lib/public/modules/app-messages.js +10 -2
  31. package/lib/public/modules/app-projects.js +1 -407
  32. package/lib/public/modules/app-rate-limit.js +12 -1
  33. package/lib/public/modules/clay-tool-presentation.js +1 -0
  34. package/lib/public/modules/contextual-clay.js +3 -1
  35. package/lib/public/modules/github-star.js +66 -0
  36. package/lib/public/modules/mate-loading-scene.js +17 -0
  37. package/lib/public/modules/profile.js +11 -3
  38. package/lib/public/modules/project-mate-creation-workspace.js +13 -4
  39. package/lib/public/modules/project-session-visibility-settings.js +19 -16
  40. package/lib/public/modules/project-settings.js +119 -129
  41. package/lib/public/modules/project-start-dialog.js +130 -0
  42. package/lib/public/modules/project-start-folder-state.js +78 -0
  43. package/lib/public/modules/project-start-folder-view.js +190 -0
  44. package/lib/public/modules/project-start-forms.js +66 -0
  45. package/lib/public/modules/project-start-path.js +60 -0
  46. package/lib/public/modules/project-start-state.js +54 -0
  47. package/lib/public/modules/project-start-submit.js +67 -0
  48. package/lib/public/modules/projects-hub-view.js +33 -14
  49. package/lib/public/modules/projects-hub.js +10 -6
  50. package/lib/public/modules/server-settings.js +36 -91
  51. package/lib/public/modules/settings-project-target.js +56 -0
  52. package/lib/public/modules/settings-select.js +1 -1
  53. package/lib/public/modules/settings-shell-registry.js +88 -0
  54. package/lib/public/modules/settings-shell-view.js +104 -0
  55. package/lib/public/modules/settings-shell.js +342 -0
  56. package/lib/public/modules/sidebar-mobile.js +8 -30
  57. package/lib/public/modules/sidebar-projects.js +2 -1
  58. package/lib/public/modules/theme.js +10 -2
  59. package/lib/public/modules/user-settings-layout.js +8 -33
  60. package/lib/public/modules/user-settings-profile.js +1 -1
  61. package/lib/public/modules/user-settings.js +18 -85
  62. package/lib/public/modules/vendor-usage-format.js +93 -0
  63. package/lib/public/modules/vendor-usage-list.js +45 -0
  64. package/lib/public/modules/vendor-usage-model.js +215 -0
  65. package/lib/public/modules/vendor-usage-overview.js +312 -0
  66. package/lib/public/style.css +10 -7
  67. package/lib/public/welcome-art/conversation-desk.png +0 -0
  68. package/lib/public/welcome-art/mate-clay.png +0 -0
  69. package/lib/public/welcome-art/project-clay.png +0 -0
  70. package/lib/sdk-bridge.js +6 -1
  71. package/lib/sdk-message-processor.js +9 -1
  72. package/lib/sdk-quota-snapshot.js +20 -2
  73. package/lib/server-github-stars.js +39 -0
  74. package/lib/server-global-ws.js +21 -17
  75. package/lib/server.js +4 -0
  76. package/lib/session-notes-mcp-server.js +26 -4
  77. package/lib/session-notes-writes.js +88 -0
  78. package/lib/session-visibility.js +20 -8
  79. package/lib/sessions.js +2 -0
  80. package/lib/vendor-usage-acp-readers.js +97 -0
  81. package/lib/vendor-usage-capabilities.js +88 -0
  82. package/lib/vendor-usage-http-readers.js +214 -0
  83. package/lib/vendor-usage-metrics.js +47 -0
  84. package/lib/vendor-usage-native-readers.js +106 -0
  85. package/lib/vendor-usage-overview.js +406 -0
  86. package/lib/vendor-usage-service.js +281 -0
  87. package/lib/ws-schema.js +4 -2
  88. package/lib/yoke/adapters/claude.js +12 -0
  89. package/lib/yoke/adapters/codex-query-contract.js +1 -0
  90. package/lib/yoke/adapters/codex.js +45 -12
  91. package/lib/yoke/claude-hook-installer.js +1 -0
  92. package/lib/yoke/codex-dynamic-tool-names.js +80 -0
  93. package/lib/yoke/codex-model-catalog.js +37 -3
  94. package/lib/yoke/muse-services.js +16 -3
  95. package/lib/yoke/providers/claude-model-descriptions.js +22 -2
  96. package/lib/yoke/vendor-registry.js +6 -2
  97. package/package.json +2 -1
  98. package/lib/public/modules/server-settings-navigation.js +0 -39
@@ -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);
@@ -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) {
@@ -0,0 +1,130 @@
1
+ // project-start-dialog.js - One fixed-size dialog for choosing how to start a project
2
+
3
+ import { store } from './store.js';
4
+ import { iconHtml, refreshIcons } from './icons.js';
5
+ import { openState, closeState, selectView } from './project-start-state.js';
6
+ import { folderViewHtml, bindFolderView, renderFolderView, startFolderBrowse, resetFolderState, handleBrowseDirResult } from './project-start-folder-view.js';
7
+ import { formViewHtml, bindFormView, renderFormView } from './project-start-forms.js';
8
+ import { submitFolder, handleAddProjectResult, handleCloneProgress } from './project-start-submit.js';
9
+
10
+ export { handleBrowseDirResult, handleAddProjectResult, handleCloneProgress };
11
+
12
+ var TITLES = { choices: 'Start a project', open: 'Open a folder', create: 'Create a project', clone: 'Clone a repository' };
13
+ var CHOICES = [
14
+ { view: 'open', icon: 'folder-open', title: 'Open an existing folder', text: 'Use a directory on the machine running Clay.' },
15
+ { view: 'create', icon: 'plus', title: 'Create a new project', text: 'Start in a new, empty project directory.' },
16
+ { view: 'clone', icon: 'git-branch', title: 'Clone a repository', text: 'Bring an existing Git repository into Clay.' },
17
+ ];
18
+ var shownGen = -1;
19
+ var opener = null;
20
+
21
+ function modal() { return document.getElementById('project-start-modal'); }
22
+ function body() { return document.getElementById('project-start-body'); }
23
+ function current() { return store.get('projectStart'); }
24
+
25
+ function choicesHtml() {
26
+ var html = '<div class="project-start-view" data-view="choices"><div class="project-start-scroll"><ul class="project-start-rows project-start-choices" role="list">';
27
+ for (var i = 0; i < CHOICES.length; i++) {
28
+ var c = CHOICES[i];
29
+ html += '<li><button type="button" class="project-start-row project-start-choice" data-choice="' + c.view + '">' +
30
+ iconHtml(c.icon, 'project-start-choice-icon') + '<span class="project-start-choice-text"><strong>' + c.title + '</strong><span>' + c.text + '</span></span>' +
31
+ iconHtml('chevron-right') + '</button></li>';
32
+ }
33
+ return html + '</ul></div></div>';
34
+ }
35
+
36
+ function focusFirst(view) {
37
+ var target = null;
38
+ if (view === 'choices') target = body().querySelector('[data-choice]');
39
+ else if (view === 'open') target = body().querySelector('[data-ps="list"]');
40
+ else target = body().querySelector('[data-ps="field"]');
41
+ if (!target) target = modal().querySelector('.project-start-dialog');
42
+ target.focus({ preventScroll: true });
43
+ }
44
+
45
+ function renderShell(view) {
46
+ var container = body();
47
+ document.getElementById('project-start-title').textContent = TITLES[view];
48
+ document.getElementById('project-start-back').hidden = view === 'choices';
49
+ document.getElementById('project-start-subtitle').hidden = view !== 'choices';
50
+ if (view === 'choices') container.innerHTML = choicesHtml();
51
+ else if (view === 'open') { container.innerHTML = folderViewHtml(); bindFolderView(submitFolder); }
52
+ else { container.innerHTML = formViewHtml(view); bindFormView(view); }
53
+ refreshIcons();
54
+ }
55
+
56
+ function render(state, previous) {
57
+ var overlay = modal();
58
+ if (!overlay) return;
59
+ if (state.view === null) {
60
+ if (!overlay.classList.contains('hidden')) hide();
61
+ return;
62
+ }
63
+ if (state.gen !== shownGen || previous.view === null) {
64
+ shownGen = state.gen;
65
+ overlay.classList.remove('hidden');
66
+ renderShell(state.view);
67
+ if (state.view === 'open') { resetFolderState(); startFolderBrowse(); }
68
+ if (state.view === 'create' || state.view === 'clone') renderFormView(state.view);
69
+ focusFirst(state.view);
70
+ return;
71
+ }
72
+ if (state.view === 'open') renderFolderView();
73
+ else if (state.view === 'create' || state.view === 'clone') renderFormView(state.view);
74
+ }
75
+
76
+ function hide() {
77
+ modal().classList.add('hidden');
78
+ body().innerHTML = '';
79
+ shownGen = -1;
80
+ resetFolderState();
81
+ var target = opener;
82
+ opener = null;
83
+ if (target && document.contains(target) && typeof target.focus === 'function') target.focus({ preventScroll: true });
84
+ }
85
+
86
+ export function openAddProjectModal(mode) {
87
+ if (current().view === null) opener = document.activeElement;
88
+ store.set({ projectStart: openState(current(), mode) });
89
+ }
90
+
91
+ export function closeAddProjectModal() {
92
+ if (current().view === null) return;
93
+ store.set({ projectStart: closeState(current()) });
94
+ }
95
+
96
+ function trapTab(event, dialog) {
97
+ var items = dialog.querySelectorAll('button:not([disabled]):not([hidden]), input:not([disabled]):not([hidden]), [tabindex="-1"][data-ps="list"]');
98
+ var focusable = [];
99
+ for (var i = 0; i < items.length; i++) if (items[i].offsetParent !== null && items[i].tabIndex >= 0) focusable.push(items[i]);
100
+ if (!focusable.length) { event.preventDefault(); dialog.focus(); return; }
101
+ var first = focusable[0];
102
+ var last = focusable[focusable.length - 1];
103
+ var active = document.activeElement;
104
+ if (event.shiftKey && (active === first || active === dialog)) { event.preventDefault(); last.focus(); }
105
+ else if (!event.shiftKey && active === last) { event.preventDefault(); first.focus(); }
106
+ }
107
+
108
+ export function initProjectStart() {
109
+ var overlay = modal();
110
+ var dialog = overlay.querySelector('.project-start-dialog');
111
+ store.subscribe(function (state, previous) {
112
+ if (state.projectStart !== previous.projectStart) render(state.projectStart, previous.projectStart);
113
+ else if (state.projectStartFolder !== previous.projectStartFolder && state.projectStart.view === 'open') renderFolderView();
114
+ });
115
+ overlay.querySelector('.project-start-backdrop').addEventListener('click', closeAddProjectModal);
116
+ document.getElementById('project-start-close').addEventListener('click', closeAddProjectModal);
117
+ document.getElementById('project-start-back').addEventListener('click', function () {
118
+ store.set({ projectStart: selectView(current(), 'choices') });
119
+ });
120
+ body().addEventListener('click', function (event) {
121
+ var choice = event.target.closest('[data-choice]');
122
+ if (choice) store.set({ projectStart: selectView(current(), choice.dataset.choice) });
123
+ });
124
+ dialog.addEventListener('keydown', function (event) {
125
+ if (event.key === 'Escape') { event.preventDefault(); closeAddProjectModal(); }
126
+ else if (event.key === 'Tab') trapTab(event, dialog);
127
+ });
128
+ var addButton = document.getElementById('project-list-add');
129
+ if (addButton) addButton.addEventListener('click', function () { openAddProjectModal(); });
130
+ }
@@ -0,0 +1,78 @@
1
+ // project-start-folder-state.js - Pure state transitions for the open-folder browser.
2
+ // A reply is applied only when its requestId matches the single in-flight request.
3
+
4
+ export function emptyFolderState() {
5
+ return {
6
+ status: "idle", dir: null, parent: null, entries: [], error: null, errorKind: null,
7
+ requestId: null, requestedPath: null, candidates: [], candidateIdx: 0, initial: false, pathOpen: false,
8
+ };
9
+ }
10
+
11
+ export function beginInitial(candidates, requestId) {
12
+ var next = emptyFolderState();
13
+ next.status = "loading";
14
+ next.candidates = candidates.slice();
15
+ next.initial = true;
16
+ next.requestId = requestId;
17
+ next.requestedPath = candidates[0] || "~";
18
+ return next;
19
+ }
20
+
21
+ export function beginNavigate(prev, path, requestId) {
22
+ return Object.assign({}, prev, {
23
+ status: "loading", requestId: requestId, requestedPath: path, error: null, errorKind: null, initial: false,
24
+ });
25
+ }
26
+
27
+ function errorKindOf(message) {
28
+ return /not permitted|permission|EACCES|EPERM/i.test(message || "") ? "denied" : "failed";
29
+ }
30
+
31
+ function cleanEntries(entries) {
32
+ var out = [];
33
+ for (var i = 0; i < (entries || []).length; i++) {
34
+ var e = entries[i];
35
+ if (e && typeof e.name === "string" && typeof e.path === "string") out.push({ name: e.name, path: e.path });
36
+ }
37
+ return out;
38
+ }
39
+
40
+ // Returns { state, retryPath }. retryPath is set when the initial candidate failed and another remains.
41
+ export function applyBrowseResult(prev, msg, nextRequestId) {
42
+ if (!msg || prev.status !== "loading" || !prev.requestId || msg.requestId !== prev.requestId) {
43
+ return { state: prev, retryPath: null };
44
+ }
45
+ var failed = !!msg.error;
46
+ var wrongTarget = !failed && (typeof msg.dir !== "string" || (msg.prefix || "") !== "");
47
+ if (failed || wrongTarget) {
48
+ var idx = prev.candidateIdx + 1;
49
+ if (prev.initial && idx < prev.candidates.length) {
50
+ var retry = Object.assign({}, prev, { candidateIdx: idx, requestId: nextRequestId, requestedPath: prev.candidates[idx] });
51
+ return { state: retry, retryPath: prev.candidates[idx] };
52
+ }
53
+ return {
54
+ state: Object.assign({}, prev, {
55
+ status: "error",
56
+ error: failed ? msg.error : "That folder was not found.",
57
+ errorKind: failed ? errorKindOf(msg.error) : "missing",
58
+ }),
59
+ retryPath: null,
60
+ };
61
+ }
62
+ return {
63
+ state: Object.assign({}, prev, {
64
+ status: "ready", dir: msg.dir, parent: typeof msg.parent === "string" ? msg.parent : null,
65
+ entries: cleanEntries(msg.entries), error: null, errorKind: null, initial: false,
66
+ }),
67
+ retryPath: null,
68
+ };
69
+ }
70
+
71
+ export function failLocally(prev, message) {
72
+ if (prev.status !== "loading") return prev;
73
+ return Object.assign({}, prev, { status: "error", error: message, errorKind: "failed" });
74
+ }
75
+
76
+ export function canOpenFolder(state) {
77
+ return state.status === "ready" && typeof state.dir === "string" && state.dir.length > 0;
78
+ }