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
@@ -0,0 +1,342 @@
1
+ // Unified full-screen settings: one shell for Personal, Project and Server
2
+ // settings. Scope modules register a controller; the shell owns routing,
3
+ // access, project target binding, focus, close restoration and draft guards.
4
+ import { store } from './store.js';
5
+ import { refreshIcons } from './icons.js';
6
+ import { closeFileViewer } from './filebrowser.js';
7
+ import { holdNotesForSettings, releaseNotesForSettings } from './sticky-notes.js';
8
+ import { openNotesBrowser, closeNotesBrowser, isNotesBrowserOpen } from './sticky-notes-browser.js';
9
+ import { confirmSettingsAction, isolateSettingsDialog, trapSettingsFocus } from './server-settings-state.js';
10
+ import { showToast } from './utils.js';
11
+ import { enhanceSettingsSelects } from './settings-select.js';
12
+ import { accessFromMe, defaultAccess, pagesForScope, resolveRoute, scopeAvailable } from './settings-shell-registry.js';
13
+ import { listSettingsProjects, resolveTargetSlug, findChoice, createTargetBinding } from './settings-project-target.js';
14
+ import { setShellStatus, renderScopeTabs, renderPageNav, markActivePage, renderProjectSelect, showProjectPicker, syncProjectSelect } from './settings-shell-view.js';
15
+
16
+ var controllers = {};
17
+ var binding = createTargetBinding();
18
+ var root = null;
19
+ var lastChoices = '';
20
+ var OFFLINE = 'Offline. The project list and your project access may be out of date until the connection returns.';
21
+ var shell = { verified: false, pending: null, waiting: null, offline: false, open: false, scope: null, pages: {}, slug: null, access: defaultAccess(), returnFocus: null, restoreInert: null, notesReopen: false, token: 0, pending: null };
22
+ var OVERLAYS = '.admin-modal-overlay, #settings-confirm, .confirm-above-settings, .avatar-positioner-overlay, .email-setup-overlay, .settings-subview';
23
+
24
+ export function registerSettingsScope(id, controller) { controllers[id] = controller; }
25
+ export function isSettingsShellOpen() { return !!shell.open; }
26
+ export function activeSettingsScope() { return shell.scope; }
27
+ export function settingsTargetSlug() { return shell.slug; }
28
+ // Async replies compare their captured token with the live target.
29
+ export function settingsTargetToken() { return binding.generation(); }
30
+ export function isSettingsTargetCurrent(token, slug) { return shell.open && binding.isCurrent(token, slug); }
31
+
32
+ // Fail closed: until a fresh server answer is bound to this open, no
33
+ // administrator scope is offered, whatever an earlier session learned.
34
+ function provisionalAccess() {
35
+ var perms = store.get('permissions');
36
+ return Object.assign(defaultAccess(), { project: !perms || perms.projectSettings !== false });
37
+ }
38
+
39
+ // Reads only; the caller applies the answer after validating that it still
40
+ // belongs to the current open and signed-in user.
41
+ function fetchMe() {
42
+ return fetch('/api/me', { credentials: 'same-origin' }).then(function (response) {
43
+ if (!response.ok) throw new Error('Could not verify access.');
44
+ return response.json();
45
+ });
46
+ }
47
+
48
+ function setGears(open) {
49
+ ['server-settings-btn', 'user-settings-btn'].forEach(function (id) {
50
+ var button = document.getElementById(id);
51
+ if (!button) return;
52
+ button.classList.toggle('active', open);
53
+ button.setAttribute('aria-expanded', String(open));
54
+ });
55
+ }
56
+
57
+ function projectChoices() {
58
+ return listSettingsProjects(store.get('projectsHubList'), store.get('currentSlug'), store.get('permissions'));
59
+ }
60
+
61
+ function focusHeading(ctrl) {
62
+ var heading = ctrl && ctrl.pane.querySelector('.active h2');
63
+ if (heading) { heading.tabIndex = -1; heading.focus(); }
64
+ }
65
+
66
+ function setPaneVisibility(scope) {
67
+ Object.keys(controllers).forEach(function (id) { controllers[id].pane.classList.toggle('hidden', id !== scope); });
68
+ }
69
+
70
+ function controller() { return controllers[shell.scope]; }
71
+
72
+ // Ask before leaving drafts behind. kind: page | scope | project | close.
73
+ function guard(kind, proceed, pageId) {
74
+ var ctrl = controller();
75
+ var busy = ctrl && ctrl.busy && ctrl.busy(kind);
76
+ if (busy) { showToast(busy); return; }
77
+ var prompt = ctrl && ctrl.leavePrompt && ctrl.leavePrompt(kind, pageId);
78
+ if (!prompt) { proceed(); return; }
79
+ confirmSettingsAction(prompt.message, prompt.action, function () { if (prompt.apply) prompt.apply(); proceed(); });
80
+ }
81
+
82
+ function choicesSignature(choices, slug) {
83
+ return choices.map(function (choice) { return choice.slug + '|' + choice.label + '|' + choice.isCurrent; }).join('\n') + '#' + (slug || '');
84
+ }
85
+
86
+ function applyProject(ctrl, route, page) {
87
+ var requested = route.slug || shell.slug;
88
+ lastChoices = '';
89
+ // An unloaded list is never "no projects": wait for the authoritative list.
90
+ if (store.get('projectListLoaded') !== true) {
91
+ shell.waiting = { slug: requested };
92
+ binding.bind(null); shell.slug = null;
93
+ renderProjectSelect([], '', onProjectSelect, 'Loading projects…'); showProjectPicker(true);
94
+ renderPageNav(pagesForScope('project', shell.access), page, onPageSelect);
95
+ ctrl.setEmpty('Loading your projects…', 'loading');
96
+ var token = shell.token;
97
+ setTimeout(function () {
98
+ if (shell.open && token === shell.token && shell.waiting) {
99
+ ctrl.setEmpty('Your projects could not be loaded. Check the connection and reopen Settings to try again.', 'error');
100
+ setShellStatus('Your projects could not be loaded.', 'error');
101
+ }
102
+ }, 10000);
103
+ return;
104
+ }
105
+ shell.waiting = null;
106
+ var choices = projectChoices();
107
+ var slug = resolveTargetSlug(requested, choices);
108
+ var lost = !!requested && slug !== requested;
109
+ renderProjectSelect(choices, slug, onProjectSelect);
110
+ lastChoices = choicesSignature(choices, slug);
111
+ showProjectPicker(true);
112
+ binding.bind(slug);
113
+ shell.slug = slug;
114
+ if (!slug) {
115
+ ctrl.setEmpty('You do not have a project to configure yet. Open or create a project, then return here.', 'empty');
116
+ renderPageNav(pagesForScope('project', shell.access), page, onPageSelect);
117
+ return;
118
+ }
119
+ ctrl.setEmpty(null);
120
+ ctrl.retarget(slug, findChoice(choices, slug).project, binding.generation());
121
+ setShellStatus(lost ? 'That project is not available for your account. Showing ' + findChoice(choices, slug).label + '.' : '', lost ? 'error' : undefined);
122
+ }
123
+
124
+ function redirectNote(resolved) {
125
+ if (!resolved.redirected) return '';
126
+ return resolved.requestedScope === 'server' ? 'Server settings require an administrator. Your personal settings are shown.' : 'That settings area is not available for your account.';
127
+ }
128
+
129
+ function currentRoute() {
130
+ return { scope: shell.scope, page: shell.pages[shell.scope], slug: shell.slug || (shell.waiting && shell.waiting.slug) || null };
131
+ }
132
+
133
+ function applyRoute(route, focus) {
134
+ var resolved = resolveRoute(route, shell.access);
135
+ var ctrl = controllers[resolved.scope];
136
+ if (!ctrl) return;
137
+ if (shell.scope && shell.scope !== resolved.scope && controllers[shell.scope].leave) controllers[shell.scope].leave();
138
+ var entering = shell.scope !== resolved.scope;
139
+ shell.scope = resolved.scope;
140
+ binding.bind(null);
141
+ setPaneVisibility(resolved.scope);
142
+ renderScopeTabs(shell.access, resolved.scope, onScopeSelect);
143
+ setShellStatus(redirectNote(resolved), resolved.redirected ? 'error' : undefined);
144
+ var ctx = { access: shell.access, scope: resolved.scope };
145
+ if (entering && ctrl.enter) ctrl.enter(ctx);
146
+ if (resolved.scope === 'project') applyProject(ctrl, resolved, resolved.page);
147
+ else { showProjectPicker(false); shell.slug = null; shell.waiting = null; }
148
+ var page = resolved.page;
149
+ if (resolved.scope !== 'project' || shell.slug) renderPageNav(pagesForScope(resolved.scope, shell.access), page, onPageSelect);
150
+ shell.pages[resolved.scope] = page;
151
+ if (resolved.scope !== 'project' || shell.slug) ctrl.activate(page, ctx);
152
+ markActivePage(page);
153
+ store.set({ settingsLastRoute: { scope: resolved.scope, page: page, slug: shell.slug } });
154
+ enhanceSettingsSelects(root); refreshIcons(root);
155
+ if (focus) focusHeading(ctrl);
156
+ }
157
+
158
+ function onScopeSelect(scope) {
159
+ if (scope === shell.scope) return;
160
+ guard('scope', function () { applyRoute({ scope: scope, page: shell.pages[scope], slug: scope === 'project' ? currentRoute().slug : null }, true); });
161
+ }
162
+ function onPageSelect(page) {
163
+ if (page === shell.pages[shell.scope]) { focusHeading(controller()); return; }
164
+ guard('page', function () {
165
+ var ctrl = controller();
166
+ shell.pages[shell.scope] = page; ctrl.activate(page, { access: shell.access, scope: shell.scope });
167
+ markActivePage(page); store.set({ settingsLastRoute: { scope: shell.scope, page: page, slug: shell.slug } });
168
+ focusHeading(ctrl);
169
+ }, shell.pages[shell.scope]);
170
+ }
171
+ // A project switch retargets only the settings view, never the workspace.
172
+ function onProjectSelect(slug) {
173
+ syncProjectSelect(shell.slug);
174
+ if (!slug || slug === shell.slug) return;
175
+ guard('project', function () { applyRoute({ scope: 'project', page: shell.pages.project, slug: slug }, false); });
176
+ }
177
+
178
+ export function navigateSettings(route) {
179
+ // Until access is verified, remember the latest request and apply it afterwards.
180
+ if (!shell.verified) { shell.pending = route; return; }
181
+ var resolved = resolveRoute(route, shell.access);
182
+ if (resolved.scope === shell.scope && resolved.page === shell.pages[shell.scope] && (resolved.scope !== 'project' || !route.slug || route.slug === shell.slug)) {
183
+ setShellStatus(redirectNote(resolved), resolved.redirected ? 'error' : undefined);
184
+ focusHeading(controller()); return;
185
+ }
186
+ guard(resolved.scope === shell.scope ? (route.slug && route.slug !== shell.slug ? 'project' : 'page') : 'scope', function () { applyRoute(route, true); }, shell.pages[shell.scope]);
187
+ }
188
+
189
+ // Entry for every legacy opener. Returns once the exact destination is shown.
190
+ export function openSettings(route) {
191
+ if (!root) return Promise.resolve();
192
+ route = route || store.get('settingsLastRoute') || { scope: 'personal' };
193
+ if (shell.open) { navigateSettings(route); return Promise.resolve(); }
194
+ shell.open = true;
195
+ shell.token += 1;
196
+ var token = shell.token;
197
+ shell.returnFocus = document.activeElement;
198
+ shell.scope = null; shell.pages = {}; shell.slug = null;
199
+ closeFileViewer();
200
+ shell.notesReopen = isNotesBrowserOpen();
201
+ if (shell.notesReopen) closeNotesBrowser();
202
+ holdNotesForSettings();
203
+ root.classList.remove('hidden');
204
+ shell.restoreInert = isolateSettingsDialog(root);
205
+ setGears(true);
206
+ store.set({ settingsOpen: true });
207
+ var userAtOpen = store.get('myUserId');
208
+ shell.verified = false; shell.pending = null;
209
+ shell.access = provisionalAccess();
210
+ setShellStatus('');
211
+ if (route.scope !== 'server') applyRoute(route, true);
212
+ else { renderScopeTabs(shell.access, 'personal', onScopeSelect); setShellStatus('Checking administrator access…'); }
213
+ function current(me) {
214
+ // Stale after close/reopen, or a different signed-in user: ignore entirely.
215
+ return shell.open && token === shell.token && store.get('myUserId') === userAtOpen && (!me || !me.user || !userAtOpen || me.user.id === userAtOpen);
216
+ }
217
+ return fetchMe().then(function (me) {
218
+ if (!current(me)) return;
219
+ var access = accessFromMe(me, me.permissions || null);
220
+ store.set({ adminMe: me, permissions: me.permissions || null, projectsAccessLoaded: true });
221
+ var needsRoute = !shell.scope;
222
+ var changed = access.server !== shell.access.server || access.multiUser !== shell.access.multiUser || access.project !== shell.access.project;
223
+ shell.access = access;
224
+ shell.verified = true;
225
+ var pending = shell.pending; shell.pending = null;
226
+ if (pending) applyRoute(pending, true);
227
+ else if (needsRoute) applyRoute(route, true);
228
+ else if (changed) applyRoute(scopeAvailable(shell.scope, access) ? currentRoute() : { scope: 'personal' }, false);
229
+ }, function () {
230
+ if (!current()) return;
231
+ // Verification failed: Server stays unavailable and the view is rebuilt without it.
232
+ shell.access = Object.assign({}, shell.access, { server: false });
233
+ shell.verified = true;
234
+ var queued = shell.pending; shell.pending = null;
235
+ applyRoute(queued || (shell.scope && shell.scope !== 'server' ? currentRoute() : { scope: 'personal' }), false);
236
+ setShellStatus('Could not verify administrator access, so Server settings are unavailable. Close and reopen Settings to retry.', 'error');
237
+ });
238
+ }
239
+
240
+ function closePrompt() {
241
+ var withDrafts = Object.keys(controllers).filter(function (id) { return controllers[id].leavePrompt && controllers[id].leavePrompt('close'); });
242
+ if (!withDrafts.length) return null;
243
+ if (withDrafts.length === 1 && withDrafts[0] === shell.scope) return controllers[shell.scope].leavePrompt('close');
244
+ return { message: 'You have unsaved changes in another settings area. Closing keeps these drafts in this tab, but reloading the page will lose them.', action: 'Close with drafts' };
245
+ }
246
+
247
+ export function closeSettings(opts) {
248
+ opts = opts || {};
249
+ if (!shell.open) return;
250
+ if (opts.onlyScope && shell.scope !== opts.onlyScope) return;
251
+ function finish() {
252
+ shell.token += 1; binding.invalidate();
253
+ var ctrl = controller();
254
+ if (ctrl && ctrl.leave) ctrl.leave();
255
+ Object.keys(controllers).forEach(function (id) { if (controllers[id].close) controllers[id].close(); });
256
+ root.classList.add('hidden');
257
+ if (shell.restoreInert) { shell.restoreInert(); shell.restoreInert = null; }
258
+ setGears(false);
259
+ shell.open = false; shell.scope = null; shell.slug = null; shell.waiting = null; shell.offline = false; shell.verified = false; shell.pending = null;
260
+ store.set({ settingsOpen: false });
261
+ releaseNotesForSettings();
262
+ if (!opts.cleanup) {
263
+ if (shell.notesReopen) openNotesBrowser();
264
+ var focus = shell.returnFocus;
265
+ if (!focus || !focus.isConnected || focus === document.body) focus = document.getElementById('server-settings-btn');
266
+ if (focus) focus.focus();
267
+ }
268
+ shell.notesReopen = false; shell.returnFocus = null;
269
+ }
270
+ if (opts.cleanup || opts.force) { finish(); return; }
271
+ var ctrl = controller();
272
+ var busy = ctrl && ctrl.busy && ctrl.busy('close');
273
+ if (busy) { showToast(busy); return; }
274
+ var prompt = closePrompt();
275
+ if (!prompt) { finish(); return; }
276
+ confirmSettingsAction(prompt.message, prompt.action, function () { if (prompt.apply) prompt.apply(); finish(); });
277
+ }
278
+
279
+ // Authorisation, ordering and the open project can change while settings
280
+ // are open; an unavailable target is cleared rather than silently replaced.
281
+ function refreshLive() {
282
+ if (!shell.open || !shell.scope) return;
283
+ var perms = store.get('permissions');
284
+ var project = !perms || perms.projectSettings !== false;
285
+ if (project !== shell.access.project) {
286
+ shell.access = Object.assign({}, shell.access, { project: project });
287
+ if (shell.scope === 'project' && !project) { applyRoute({ scope: 'personal' }, false); setShellStatus('Project settings are no longer permitted for your account.', 'error'); return; }
288
+ renderScopeTabs(shell.access, shell.scope, onScopeSelect);
289
+ }
290
+ if (shell.scope !== 'project') return;
291
+ syncConnectivity();
292
+ if (shell.waiting && store.get('projectListLoaded') === true) {
293
+ var waiting = shell.waiting; shell.waiting = null;
294
+ applyRoute({ scope: 'project', page: shell.pages.project, slug: waiting.slug }, false);
295
+ return;
296
+ }
297
+ if (shell.waiting) return;
298
+ var choices = projectChoices();
299
+ var ctrl = controller();
300
+ var target = findChoice(choices, shell.slug);
301
+ var signature = choicesSignature(choices, target ? shell.slug : '');
302
+ if (signature !== lastChoices) { lastChoices = signature; renderProjectSelect(choices, target ? shell.slug : '', onProjectSelect); }
303
+ if (shell.slug && !target) {
304
+ var lostSlug = shell.slug;
305
+ binding.invalidate(); shell.slug = null;
306
+ ctrl.clearTarget(lostSlug);
307
+ ctrl.setEmpty('That project is no longer available for your account. Choose another project.', 'lost');
308
+ syncProjectSelect('');
309
+ setShellStatus('The selected project is no longer available. Unsaved edits for it were not applied.', 'error');
310
+ } else if (target) ctrl.refreshTarget(shell.slug, target.project);
311
+ }
312
+
313
+ function syncConnectivity() {
314
+ var offline = store.get('connected') === false;
315
+ if (offline && !shell.offline) { shell.offline = true; setShellStatus(OFFLINE, 'error'); }
316
+ else if (!offline && shell.offline) { shell.offline = false; setShellStatus(''); }
317
+ }
318
+
319
+ export function initSettingsShell() {
320
+ root = document.getElementById('settings-shell');
321
+ if (!root) return;
322
+ var content = document.getElementById('settings-shell-content');
323
+ ['user-settings', 'project-settings', 'server-settings'].forEach(function (id) {
324
+ var pane = document.getElementById(id);
325
+ if (pane) { pane.classList.add('settings-pane', 'hidden'); content.appendChild(pane); }
326
+ });
327
+ document.getElementById('settings-shell-close').onclick = function () { closeSettings(); };
328
+ var server = document.getElementById('server-settings-btn');
329
+ if (server) server.addEventListener('click', function () { if (shell.open) closeSettings(); else openSettings(); });
330
+ var personal = document.getElementById('user-settings-btn');
331
+ if (personal) personal.addEventListener('click', function () { openSettings({ scope: 'personal' }); });
332
+ document.addEventListener('keydown', function (event) {
333
+ if (shell.open && event.key === 'Tab' && !document.querySelector('#settings-confirm, .admin-modal-overlay')) trapSettingsFocus(event, root);
334
+ if (!shell.open || event.key !== 'Escape' || event.defaultPrevented) return;
335
+ if (store.get('speechPopup') || document.querySelector(OVERLAYS)) return;
336
+ event.preventDefault(); closeSettings();
337
+ });
338
+ store.subscribe(function (state, previous) {
339
+ if (state.myUserId !== previous.myUserId) { closeSettings({ cleanup: true }); return; }
340
+ if (state.projectsHubList !== previous.projectsHubList || state.currentSlug !== previous.currentSlug || state.permissions !== previous.permissions || state.projectListLoaded !== previous.projectListLoaded || state.connected !== previous.connected) refreshLive();
341
+ });
342
+ }
@@ -10,7 +10,7 @@ import { iconHtml, refreshIcons } from './icons.js';
10
10
  import { parseEmojis } from './markdown.js';
11
11
  import { getCurrentTheme, getChatLayout, setChatLayout } from './theme.js';
12
12
  import { openCommandPalette } from './command-palette.js';
13
- import { openProjectSettings } from './project-settings.js';
13
+ import { openSettings } from './settings-shell.js';
14
14
  import { getCachedSessions, openSessionContextMenu } from './sidebar-sessions.js';
15
15
  import { renderSessionPresence } from './session-presence-ui.js';
16
16
  import {
@@ -1033,6 +1033,7 @@ export function renderSheetTools(listEl) {
1033
1033
 
1034
1034
  function renderSheetSettings(listEl) {
1035
1035
  var items = [
1036
+ { icon: "user-cog", label: "Personal Settings", action: "personal-settings" },
1036
1037
  { icon: "folder-cog", label: "Project Settings", action: "project-settings" },
1037
1038
  { icon: "settings", label: "Server Settings", action: "server-settings" }
1038
1039
  ];
@@ -1044,35 +1045,12 @@ function renderSheetSettings(listEl) {
1044
1045
  btn.innerHTML = '<i data-lucide="' + item.icon + '"></i><span class="mobile-more-item-label">' + item.label + '</span>';
1045
1046
  btn.addEventListener("click", function () {
1046
1047
  closeMobileSheet();
1047
- if (item.action === "project-settings") {
1048
- setTimeout(function () {
1049
- // Find current project data
1050
- var proj = null;
1051
- for (var pi = 0; pi < getCachedProjectList().length; pi++) {
1052
- if (getCachedProjectList()[pi].slug === getCachedCurrentSlug()) {
1053
- proj = getCachedProjectList()[pi];
1054
- break;
1055
- }
1056
- }
1057
- // For mate projects, use mate display name instead of slug
1058
- if (proj && proj.isMate && getCachedMates().length > 0) {
1059
- var mateId = getCachedCurrentSlug().replace("mate-", "");
1060
- var _mates = getCachedMates();
1061
- for (var mi = 0; mi < _mates.length; mi++) {
1062
- var mp = _mates[mi].profile || {};
1063
- if (_mates[mi].id === mateId) {
1064
- proj = Object.assign({}, proj, { name: mp.displayName || _mates[mi].name || proj.name });
1065
- break;
1066
- }
1067
- }
1068
- }
1069
- if (proj && store.get('ownerLocked')) proj = Object.assign({}, proj, { ownerLocked: true });
1070
- openProjectSettings(getCachedCurrentSlug(), proj);
1071
- }, 250);
1072
- } else if (item.action === "server-settings") {
1073
- var settingsBtn = document.getElementById("server-settings-btn");
1074
- if (settingsBtn) setTimeout(function () { settingsBtn.click(); }, 250);
1075
- }
1048
+ // The shell resolves each scope to its exact page and falls back for
1049
+ // accounts that may not use it. Project settings target the open
1050
+ // workspace first.
1051
+ var route = item.action === "project-settings" ? { scope: "project", slug: getCachedCurrentSlug() }
1052
+ : item.action === "server-settings" ? { scope: "server" } : { scope: "personal" };
1053
+ setTimeout(function () { openSettings(route); }, 250);
1076
1054
  });
1077
1055
  listEl.appendChild(btn);
1078
1056
  })(items[i]);
@@ -10,7 +10,8 @@ import { store } from './store.js';
10
10
  import { getWs } from './ws-ref.js';
11
11
  import { closeSidebar } from './sidebar.js';
12
12
  import { showIconTooltip, hideIconTooltip, bindIconTooltip, closeUserCtxMenu, getCurrentDmUserId } from './sidebar-mates.js';
13
- import { switchProject, openAddProjectModal, getCachedProjects } from './app-projects.js';
13
+ import { switchProject, getCachedProjects } from './app-projects.js';
14
+ import { openAddProjectModal } from './project-start-dialog.js';
14
15
  import { showHomeHub } from './app-home-hub.js';
15
16
  import { getPendingTuiAttention } from './app-notifications.js';
16
17
  import { isExternalWorktree, externalWorktreeTooltip, appendExternalWorktreeBadge } from './worktree-location.js';
@@ -482,8 +482,13 @@ function updateToggleIcon() {
482
482
  // User settings toggle
483
483
  var usToggle = document.getElementById("us-theme-toggle");
484
484
  if (usToggle) usToggle.checked = isLight;
485
- // User island button is now a static "skins / themes" trigger; no
486
- // icon swap on mode change.
485
+ var quickToggle = document.getElementById("appearance-mode-toggle");
486
+ if (quickToggle) {
487
+ var label = isLight ? "Switch to dark mode" : "Switch to light mode";
488
+ quickToggle.setAttribute("aria-label", label);
489
+ quickToggle.title = label;
490
+ quickToggle.innerHTML = '<i data-lucide="' + (isLight ? "moon" : "sun") + '" class="cursor-share-icon" aria-hidden="true"></i>';
491
+ }
487
492
  refreshIcons();
488
493
  }
489
494
 
@@ -649,6 +654,9 @@ export function initTheme() {
649
654
  }
650
655
  }
651
656
 
657
+ var quickToggle = document.getElementById("appearance-mode-toggle");
658
+ if (quickToggle) quickToggle.addEventListener("click", toggleDarkMode);
659
+
652
660
  // Set initial toggle icon
653
661
  updateToggleIcon();
654
662
 
@@ -4,15 +4,9 @@ import { store } from './store.js';
4
4
  import { closeSpeechPicker } from './stt-picker.js';
5
5
  import { loadSpeechSettings } from './stt-settings.js';
6
6
  import { mountDefaultAi, unmountDefaultAi, requestDefaultAi } from './default-ai.js';
7
- import { confirmSettingsAction as showConfirmDialog } from './server-settings-state.js';
7
+ import { resolvePage } from './settings-shell-registry.js';
8
8
 
9
- export var USER_SETTINGS_PAGES = [
10
- ['us-account', 'Account'], ['us-appearance', 'Appearance'], ['us-chat', 'Conversation'],
11
- ['us-notifications', 'Notifications'], ['us-ai', 'AI preferences'], ['us-integrations', 'Integrations']
12
- ];
13
- export function resolveUserSettingsPage(id) {
14
- return ({'us-mates':'us-chat','us-experimental':'us-chat','us-connections':'us-integrations','us-email':'us-integrations','us-logout':'us-account'})[id] || id;
15
- }
9
+ export function resolveUserSettingsPage(id) { return resolvePage('personal', id); }
16
10
  function section(id) { return document.querySelector('#user-settings .us-section[data-section="' + id + '"]'); }
17
11
  function heading(target, text, copy) {
18
12
  var title = document.createElement('h3'); title.textContent = text; target.appendChild(title);
@@ -20,15 +14,6 @@ function heading(target, text, copy) {
20
14
  }
21
15
  export function buildUserSettingsLayout() {
22
16
  var root = document.getElementById('user-settings');
23
- root.querySelector('.us-modal').setAttribute('role','dialog');root.querySelector('.us-modal').setAttribute('aria-modal','true');root.querySelector('.us-modal').setAttribute('aria-label','User settings');
24
- var nav = root.querySelector('.us-modal-nav-items'); nav.replaceChildren();
25
- var dropdown = document.getElementById('user-settings-nav-dropdown'); dropdown.replaceChildren();
26
- USER_SETTINGS_PAGES.forEach(function (page) {
27
- var button = document.createElement('button'); button.className = 'us-nav-item'; button.dataset.section = page[0]; button.textContent = page[1]; button.type = 'button'; nav.appendChild(button);
28
- var option = document.createElement('option'); option.value = page[0]; option.textContent = page[1]; dropdown.appendChild(option);
29
- });
30
- dropdown.setAttribute('aria-label', 'Personal settings page');
31
- root.querySelector('.us-modal-nav').setAttribute('aria-label', 'Personal settings pages');
32
17
  var notifications=section('us-notifications');
33
18
  var push=document.getElementById('settings-notif-push').closest('label');
34
19
  heading(notifications,'This device','Browser permission and push delivery are configured separately on each browser.');notifications.appendChild(push);
@@ -54,36 +39,26 @@ export function buildUserSettingsLayout() {
54
39
  document.getElementById('us-logout-btn').closest('.settings-card').classList.remove('settings-card-danger');
55
40
  document.getElementById('us-pin-set-btn').parentElement.querySelector('.settings-hint').textContent = 'Change your personal account PIN. Server sign-in policy is managed separately by an administrator.';
56
41
  root.querySelectorAll('.layout-option-icon').forEach(function (el) {el.remove();});
57
- var back = document.createElement('button');back.type = 'button';back.className = 'settings-btn-sm us-back';back.textContent = '← Settings';root.querySelector('.us-modal-content').prepend(back);
58
- back.onclick = function () {root.classList.add('us-show-navigation');nav.querySelector('.active').focus();};
59
- nav.addEventListener('keydown', function (event) {
60
- if (['ArrowDown','ArrowUp','Home','End'].indexOf(event.key) < 0) return;
61
- var items = Array.from(nav.querySelectorAll('button'));var index = items.indexOf(document.activeElement);if(index < 0)return;
62
- event.preventDefault();index = event.key === 'Home' ? 0 : event.key === 'End' ? items.length-1 : (index+(event.key === 'ArrowDown'?1:-1)+items.length)%items.length;items[index].focus();
63
- });
64
42
  }
65
43
  export function loadUserSettingsExtras(page) {
66
- document.getElementById('user-settings').classList.remove('us-show-navigation');
67
44
  if (page === 'us-integrations') loadPersonalIntegrations();
68
45
  if (page === 'us-chat') loadSpeechSettings();
69
46
  if (page === 'us-ai') { mountDefaultAi(document.getElementById('us-default-ai')); requestDefaultAi(); var ws=getWs();if(ws && ws.readyState===1)ws.send(JSON.stringify({type:'get_claude_allow_list'})); }
70
47
  }
71
48
  export function finishUserSettingsExtras() {closeSpeechPicker();unmountDefaultAi(document.getElementById('us-default-ai'));}
72
- export function guardUserSettingsLeave(done, destination) {
73
- var active = document.querySelector('#user-settings .us-section.active');
74
- var page = active && active.dataset.section;
75
- if (destination && resolveUserSettingsPage(destination) === page) {done();return;}
76
- function leaving(id) {return !destination || page === id;}
49
+ // Returns the draft prompt for leaving one personal page (or every page when
50
+ // pageId is omitted); the shell shows it with the shared confirmation.
51
+ export function personalLeavePrompt(pageId) {
52
+ function leaving(id) {return !pageId || pageId === id;}
77
53
  var pin = document.getElementById('us-pin-form');
78
54
  var pinDirty = pin && !pin.classList.contains('hidden') && Array.from(pin.querySelectorAll('input')).some(function (input) {return !!input.value;});
79
55
  var key = document.getElementById('us-linear-key');
80
- if(store.get('speechPopup'))return;
81
56
  var dirty = leaving('us-integrations') && (store.get('personalEmailDirty') || store.get('personalLinearDirty') || key && key.value) ||
82
57
  leaving('us-chat') && store.get('userVoiceDirty') ||
83
58
  leaving('us-notifications') && store.get('notificationDraft') ||
84
59
  leaving('us-appearance') && (store.get('appearanceDraft') || store.get('userTerminalDirty')) ||
85
60
  leaving('us-account') && (store.get('userProfileDirty') || pinDirty) ||
86
61
  leaving('us-ai') && (store.get('defaultAiDraftDirty') || store.get('userAllowDirty'));
87
- if (!dirty) {done();return;}
88
- showConfirmDialog('Your unsaved changes will remain in this tab. Leave this editor and keep the draft?', 'Keep draft and leave', done);
62
+ if (!dirty) return null;
63
+ return { message: 'Your unsaved changes will remain in this tab. Leave this editor and keep the draft?', action: 'Keep draft and leave' };
89
64
  }
@@ -55,7 +55,7 @@ export function initPersonalProfile() {
55
55
  URL.revokeObjectURL(url); status.textContent = 'Uploading picture…';
56
56
  uploadAvatarBlob(blob).then(function () { status.textContent = 'Picture updated.'; populateAccount(); avatar.focus(); })
57
57
  .catch(function (error) { status.textContent = error.message; status.dataset.state = 'error'; });
58
- });
58
+ }, document.querySelector('#user-settings .us-section[data-section="us-account"]'));
59
59
  };
60
60
  image.onerror = function () { URL.revokeObjectURL(url); status.textContent = 'Could not read that image. Choose another file.'; };
61
61
  image.src = url; file.value = '';