clay-server 5.2.0-beta.9 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -4
- package/lib/clay-product-control-tools.js +1 -0
- package/lib/clay-product-control.js +14 -11
- package/lib/clay-vendor-usage.js +44 -0
- package/lib/github-star-count.js +147 -0
- package/lib/notes-checklist.js +43 -1
- package/lib/notes.js +2 -1
- package/lib/project-session-notes.js +33 -8
- package/lib/project-sessions.js +63 -24
- package/lib/project-start-protocol.js +29 -0
- package/lib/project-vendor-login.js +2 -0
- package/lib/project.js +5 -1
- package/lib/public/app.js +27 -18
- package/lib/public/css/contextual-clay.css +1 -1
- package/lib/public/css/filebrowser.css +1 -1
- package/lib/public/css/mate-loading-scene.css +21 -0
- package/lib/public/css/overlays.css +64 -256
- package/lib/public/css/project-mate-creation.css +5 -6
- package/lib/public/css/project-start.css +108 -0
- package/lib/public/css/projects-hub.css +53 -14
- package/lib/public/css/settings-shell.css +103 -0
- package/lib/public/css/sidebar.css +7 -0
- package/lib/public/css/title-bar.css +83 -0
- package/lib/public/index.html +50 -63
- package/lib/public/modules/account-transport-settings.js +8 -2
- package/lib/public/modules/admin-settings-email.js +7 -2
- package/lib/public/modules/admin-settings-invitations.js +2 -2
- package/lib/public/modules/admin-settings-projects.js +2 -2
- package/lib/public/modules/admin-settings-ui.js +20 -4
- package/lib/public/modules/admin-settings-users.js +4 -4
- package/lib/public/modules/app-home-hub.js +2 -1
- package/lib/public/modules/app-messages.js +10 -2
- package/lib/public/modules/app-projects.js +1 -407
- package/lib/public/modules/app-rate-limit.js +12 -1
- package/lib/public/modules/clay-tool-presentation.js +1 -0
- package/lib/public/modules/contextual-clay.js +3 -1
- package/lib/public/modules/github-star.js +66 -0
- package/lib/public/modules/home-surface-boot.js +0 -1
- package/lib/public/modules/mate-loading-scene.js +17 -0
- package/lib/public/modules/pane-appearance-bridge.js +59 -0
- package/lib/public/modules/profile.js +11 -3
- package/lib/public/modules/project-mate-creation-workspace.js +13 -4
- package/lib/public/modules/project-session-visibility-settings.js +19 -16
- package/lib/public/modules/project-settings-environment.js +12 -1
- package/lib/public/modules/project-settings.js +119 -129
- package/lib/public/modules/project-start-dialog.js +130 -0
- package/lib/public/modules/project-start-folder-state.js +78 -0
- package/lib/public/modules/project-start-folder-view.js +190 -0
- package/lib/public/modules/project-start-forms.js +66 -0
- package/lib/public/modules/project-start-path.js +60 -0
- package/lib/public/modules/project-start-state.js +54 -0
- package/lib/public/modules/project-start-submit.js +67 -0
- package/lib/public/modules/projects-hub-view.js +32 -44
- package/lib/public/modules/projects-hub.js +39 -28
- package/lib/public/modules/server-settings.js +36 -91
- package/lib/public/modules/settings-project-target.js +56 -0
- package/lib/public/modules/settings-select.js +1 -1
- package/lib/public/modules/settings-shell-registry.js +88 -0
- package/lib/public/modules/settings-shell-view.js +104 -0
- package/lib/public/modules/settings-shell.js +342 -0
- package/lib/public/modules/sidebar-mobile.js +8 -30
- package/lib/public/modules/sidebar-projects.js +2 -1
- package/lib/public/modules/theme.js +31 -3
- package/lib/public/modules/user-settings-integrations.js +2 -1
- package/lib/public/modules/user-settings-layout.js +12 -34
- package/lib/public/modules/user-settings-profile.js +1 -1
- package/lib/public/modules/user-settings.js +18 -85
- package/lib/public/modules/vendor-usage-format.js +93 -0
- package/lib/public/modules/vendor-usage-list.js +45 -0
- package/lib/public/modules/vendor-usage-model.js +215 -0
- package/lib/public/modules/vendor-usage-overview.js +323 -0
- package/lib/public/style.css +10 -7
- package/lib/public/welcome-art/conversation-desk.png +0 -0
- package/lib/public/welcome-art/mate-clay.png +0 -0
- package/lib/public/welcome-art/project-clay.png +0 -0
- package/lib/sdk-bridge.js +6 -1
- package/lib/sdk-message-processor.js +9 -1
- package/lib/sdk-quota-snapshot.js +20 -2
- package/lib/server-github-stars.js +39 -0
- package/lib/server-global-ws.js +21 -17
- package/lib/server.js +11 -0
- package/lib/session-notes-mcp-server.js +26 -4
- package/lib/session-notes-writes.js +88 -0
- package/lib/session-visibility.js +20 -8
- package/lib/sessions.js +12 -2
- package/lib/vendor-usage-acp-readers.js +97 -0
- package/lib/vendor-usage-capabilities.js +88 -0
- package/lib/vendor-usage-http-readers.js +214 -0
- package/lib/vendor-usage-metrics.js +47 -0
- package/lib/vendor-usage-native-readers.js +106 -0
- package/lib/vendor-usage-overview.js +406 -0
- package/lib/vendor-usage-service.js +281 -0
- package/lib/ws-schema.js +4 -2
- package/lib/yoke/adapters/claude.js +12 -0
- package/lib/yoke/adapters/codex-query-contract.js +1 -0
- package/lib/yoke/adapters/codex.js +45 -12
- package/lib/yoke/claude-hook-installer.js +1 -0
- package/lib/yoke/codex-dynamic-tool-names.js +80 -0
- package/lib/yoke/codex-model-catalog.js +37 -3
- package/lib/yoke/muse-services.js +16 -3
- package/lib/yoke/providers/claude-model-descriptions.js +22 -2
- package/lib/yoke/vendor-registry.js +6 -2
- package/package.json +2 -1
- package/lib/public/modules/server-settings-navigation.js +0 -39
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { escapeHtml } from './utils.js';
|
|
2
1
|
import { iconHtml } from './icons.js';
|
|
3
2
|
|
|
4
3
|
export function ordinaryProjects(projects) {
|
|
@@ -7,55 +6,44 @@ export function ordinaryProjects(projects) {
|
|
|
7
6
|
});
|
|
8
7
|
}
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
// Zero ordinary projects (once the list is known): the welcome renders inside the normal app shell.
|
|
10
|
+
export function isShellWelcome(state) {
|
|
11
|
+
return !!state.projectListLoaded && ordinaryProjects(state.projectsHubList).length === 0;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function
|
|
15
|
-
return '<button type="button" class="projects-
|
|
16
|
-
'<span class="projects-action-icon">' + iconHtml(icon) + '</span><span><strong>' + title + '</strong><span>' + text + '</span></span>' + iconHtml('arrow-up-right') + '</button>';
|
|
14
|
+
function button(action, label, extra, disabled) {
|
|
15
|
+
return '<button type="button" class="projects-button ' + (extra || '') + '" data-projects-action="' + action + '"' + (disabled ? ' disabled' : '') + '>' + label + '</button>';
|
|
17
16
|
}
|
|
18
17
|
|
|
18
|
+
// Mate creation starts with the built-in Clay Mate, so the CTA waits until it is in the loaded Mate list.
|
|
19
|
+
export function clayMateReady(state) {
|
|
20
|
+
var mates = Array.isArray(state.cachedMatesList) ? state.cachedMatesList : [];
|
|
21
|
+
for (var i = 0; i < mates.length; i++) if (mates[i] && mates[i].builtinKey === 'clay') return true;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function welcomeCard(label, title, text, action, artwork) {
|
|
26
|
+
return '<div class="projects-intro-card"><div class="projects-intro-art"><img src="/welcome-art/' + artwork + '.png" width="1536" height="1024" alt="" decoding="async"></div>' +
|
|
27
|
+
'<span class="projects-intro-label">' + label + '</span><h2>' + title + '</h2><p>' + text + '</p><div class="projects-intro-action">' + action + '</div></div>';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The standalone project list no longer exists: the hub renders only the loading state
|
|
31
|
+
// and the zero-project welcome. Once ordinary projects are known the app opens a workspace.
|
|
19
32
|
export function projectsHubContent(state) {
|
|
20
33
|
var projects = ordinaryProjects(state.projectsHubList);
|
|
21
|
-
|
|
22
|
-
|
|
34
|
+
if (!state.projectListLoaded || projects.length) {
|
|
35
|
+
return '<main class="projects-main" aria-labelledby="projects-title"><div class="projects-loading" role="status"><h1 id="projects-title" tabindex="-1">Your workspace</h1><p>Loading your workspace…</p></div></main>';
|
|
36
|
+
}
|
|
23
37
|
var allowed = state.projectsAccessLoaded && (!state.permissions || state.permissions.createProject === true);
|
|
24
38
|
var available = allowed && state.connected && !state.pendingHomeProjectSlug;
|
|
25
|
-
var
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
'
|
|
32
|
-
|
|
33
|
-
'
|
|
34
|
-
|
|
35
|
-
'<div class="projects-welcome-actions">' + button('start', projects.length ? 'Explore your projects ' + iconHtml('arrow-right') : 'Set up your first project ' + iconHtml('arrow-right'), 'projects-button-primary') + '</div>' +
|
|
36
|
-
'<p class="projects-footnote">You can revisit this guide from Projects at any time.</p></section></main>';
|
|
37
|
-
}
|
|
38
|
-
html += '<div class="projects-heading"><div><div class="projects-eyebrow">YOUR WORKSPACE</div><h1 id="projects-title" tabindex="-1">' + (projects.length ? 'Projects' : 'Start with a project.') + '</h1>' +
|
|
39
|
-
'<p class="projects-lead">' + (projects.length ? 'Pick up where you left off, or begin something new.' : 'Choose where your work will live. You can add more projects later.') + '</p></div>' + button('guide', 'Getting started', 'projects-button-quiet') + '</div>';
|
|
40
|
-
if (state.pendingHomeProjectSlug) html += '<p class="projects-status" role="status">Opening your project…</p>';
|
|
41
|
-
if (projects.length) {
|
|
42
|
-
html += '<div class="projects-list" aria-label="Your projects">';
|
|
43
|
-
projects.forEach(function (project) {
|
|
44
|
-
html += '<button type="button" class="projects-list-item" data-project-slug="' + escapeHtml(project.slug) + '"' + (!state.connected || state.pendingHomeProjectSlug ? ' disabled' : '') + '>' +
|
|
45
|
-
'<span class="projects-action-icon">' + iconHtml(project.isWorktree ? 'git-branch' : 'folder') + '</span><span><strong>' + escapeHtml(project.title || project.project || project.slug) + '</strong>' +
|
|
46
|
-
'<span>' + escapeHtml(project.branch || project.path || project.slug) + '</span></span>' + iconHtml('arrow-right') + '</button>';
|
|
47
|
-
});
|
|
48
|
-
html += '</div><h2 class="projects-section-title">Add a project</h2>';
|
|
49
|
-
}
|
|
50
|
-
if (!state.projectsAccessLoaded) html += '<p class="projects-status" role="status">Checking project access…</p>';
|
|
51
|
-
else if (!allowed) html += '<p class="projects-status">Ask an administrator to create or share a project with you.</p>';
|
|
52
|
-
else {
|
|
53
|
-
html += '<div class="projects-setup-actions">' +
|
|
54
|
-
setupAction('existing', 'folder-open', 'Open a folder', 'Use a directory on the machine running Clay.', !available) +
|
|
55
|
-
setupAction('create', 'plus', 'Create a project', 'Start in a new, empty project directory.', !available) +
|
|
56
|
-
setupAction('clone', 'git-branch', 'Clone a repository', 'Bring an existing Git repository into Clay.', !available) + '</div>';
|
|
57
|
-
if (!projects.length) html += '<p class="projects-next-step"><span>UP NEXT</span> Choose your AI in the workspace, then tell your Driver what you want to build or fix.</p>';
|
|
58
|
-
}
|
|
59
|
-
if (!state.connected) html += '<p class="projects-status" role="status">Reconnecting… Project actions will be available when the connection returns.</p>';
|
|
60
|
-
return html + '</main><footer class="projects-footer">Your projects, conversations, and tools. One workspace.</footer>';
|
|
39
|
+
var mateReady = !!state.connected && clayMateReady(state);
|
|
40
|
+
return '<main class="projects-main" aria-labelledby="projects-title"><section class="projects-welcome"><div class="projects-eyebrow">WELCOME TO</div>' +
|
|
41
|
+
'<h1 id="projects-title" tabindex="-1"><span class="projects-wordmark" role="img" aria-label="Clay Studio"></span></h1>' +
|
|
42
|
+
'<p class="projects-lead">Where the minds work together</p>' +
|
|
43
|
+
'<div class="projects-intro-grid">' +
|
|
44
|
+
welcomeCard('01 / PROJECTS', 'Work together on a project.', 'Bring a codebase or a folder of files, and work through it together with Clay Studio.',
|
|
45
|
+
button('create-project', 'Start a project ' + iconHtml('arrow-right'), 'projects-button-primary', !available), 'project-clay') +
|
|
46
|
+
welcomeCard('02 / MATES', 'Create your own specialist.', 'Create a Mate with the expertise you need, and work together on your ideas and tasks.',
|
|
47
|
+
button('mate', 'Create a Mate ' + iconHtml('arrow-right'), 'projects-button-secondary', !mateReady), 'mate-clay') +
|
|
48
|
+
'</div></section></main>';
|
|
61
49
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { store } from './store.js';
|
|
2
2
|
import { getWs } from './ws-ref.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { switchProject } from './app-projects.js';
|
|
4
|
+
import { openAddProjectModal } from './project-start-dialog.js';
|
|
5
|
+
import { startClayMateCreation } from './project-mate-creation-workspace.js';
|
|
6
|
+
import { updateHomeSurfacePreference } from './home-surface.js';
|
|
7
|
+
import { projectsHubContent, isShellWelcome, clayMateReady, ordinaryProjects } from './projects-hub-view.js';
|
|
7
8
|
import { refreshIcons } from './icons.js';
|
|
8
9
|
|
|
9
10
|
function root() { return document.getElementById('projects-hub'); }
|
|
@@ -11,17 +12,18 @@ function root() { return document.getElementById('projects-hub'); }
|
|
|
11
12
|
function render() {
|
|
12
13
|
var element = root();
|
|
13
14
|
if (!element || !store.get('projectsHubVisible')) return;
|
|
15
|
+
document.body.classList.toggle('projects-shell', isShellWelcome(store.snap()));
|
|
14
16
|
var html = projectsHubContent(store.snap());
|
|
15
17
|
if (store.get('projectsHubMarkup') === html) return;
|
|
16
18
|
store.set({ projectsHubMarkup: html });
|
|
17
19
|
var active = element.contains(document.activeElement) ? document.activeElement : null;
|
|
18
|
-
var action = active && (active.dataset.projectsAction
|
|
20
|
+
var action = active && (active.dataset.projectsAction);
|
|
19
21
|
element.innerHTML = html;
|
|
20
22
|
refreshIcons();
|
|
21
23
|
if (action) {
|
|
22
24
|
var buttons = element.querySelectorAll('button, a');
|
|
23
25
|
for (var i = 0; i < buttons.length; i++) {
|
|
24
|
-
if (
|
|
26
|
+
if (buttons[i].dataset.projectsAction === action && !buttons[i].disabled) {
|
|
25
27
|
buttons[i].focus({ preventScroll: true });
|
|
26
28
|
break;
|
|
27
29
|
}
|
|
@@ -35,23 +37,39 @@ function focusHeading() {
|
|
|
35
37
|
if (title) title.focus({ preventScroll: true });
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
// The authorised project to open when no standalone list exists: the server-saved
|
|
41
|
+
// last project, then the current one, then the first ordinary project. Only
|
|
42
|
+
// projects present in the server-provided list qualify.
|
|
43
|
+
function workspaceFallback() {
|
|
44
|
+
var known = ordinaryProjects(store.get('projectsHubList')).map(function (project) { return project.slug; });
|
|
45
|
+
var choices = [store.get('homeSurfaceProjectSlug'), store.get('currentSlug'), known[0]];
|
|
46
|
+
for (var i = 0; i < choices.length; i++) if (choices[i] && known.indexOf(choices[i]) !== -1) return choices[i];
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Shows the loading or zero-project welcome; with projects present it opens a workspace instead.
|
|
38
51
|
export function showProjectsHub(fromHistory) {
|
|
52
|
+
if (store.get('projectListLoaded') && ordinaryProjects(store.get('projectsHubList')).length) {
|
|
53
|
+
var slug = workspaceFallback();
|
|
54
|
+
if (slug) {
|
|
55
|
+
hideProjectsHub();
|
|
56
|
+
if (fromHistory) history.replaceState(null, '', '/p/' + slug + '/');
|
|
57
|
+
switchProject(slug);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
39
61
|
store.set({ projectsHubVisible: true, pendingHomeProjectSlug: null });
|
|
40
62
|
document.body.classList.add('projects-active');
|
|
41
63
|
root().classList.remove('hidden');
|
|
42
|
-
|
|
43
|
-
if (!fromHistory && location.pathname !== '/projects') {
|
|
44
|
-
var method = document.documentElement.classList.contains('pwa-standalone') ? 'replaceState' : 'pushState';
|
|
45
|
-
history[method](null, '', '/projects');
|
|
46
|
-
}
|
|
64
|
+
if (location.pathname === '/projects' || location.pathname === '/projects/') history.replaceState(null, '', '/');
|
|
47
65
|
render();
|
|
48
66
|
focusHeading();
|
|
49
67
|
}
|
|
50
68
|
|
|
51
69
|
export function hideProjectsHub() {
|
|
52
70
|
if (!store.get('projectsHubVisible')) return;
|
|
53
|
-
store.set({ projectsHubVisible: false
|
|
54
|
-
document.body.classList.remove('projects-active');
|
|
71
|
+
store.set({ projectsHubVisible: false });
|
|
72
|
+
document.body.classList.remove('projects-active', 'projects-shell');
|
|
55
73
|
if (root()) root().classList.add('hidden');
|
|
56
74
|
}
|
|
57
75
|
|
|
@@ -61,25 +79,17 @@ export function completeProjectsOnboarding() {
|
|
|
61
79
|
}
|
|
62
80
|
|
|
63
81
|
function handleClick(event) {
|
|
64
|
-
var button = event.target.closest('[data-projects-action]
|
|
82
|
+
var button = event.target.closest('[data-projects-action]');
|
|
65
83
|
if (!button || button.disabled) return;
|
|
66
84
|
event.preventDefault();
|
|
67
85
|
var action = button.dataset.projectsAction;
|
|
68
|
-
if (action === 'settings') { openUserSettings(); return; }
|
|
69
|
-
if (action === 'guide') { store.set({ projectsShowGuide: true }); render(); focusHeading(); return; }
|
|
70
|
-
if (action === 'start') {
|
|
71
|
-
store.set({ projectsShowGuide: false });
|
|
72
|
-
updateHomeSurfacePreference({ onboardingStep: 'project' });
|
|
73
|
-
render(); focusHeading();
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
86
|
var ws = getWs();
|
|
77
87
|
if (!ws || ws.readyState !== 1) return;
|
|
78
|
-
if (
|
|
79
|
-
if (
|
|
88
|
+
if (action === 'mate') { if (clayMateReady(store.snap())) startClayMateCreation(); return; }
|
|
89
|
+
if (action === 'create-project') {
|
|
80
90
|
var permissions = store.get('permissions');
|
|
81
|
-
if (!store.get('projectsAccessLoaded') || (permissions && permissions.createProject !== true)) return;
|
|
82
|
-
openAddProjectModal(
|
|
91
|
+
if (!store.get('projectsAccessLoaded') || store.get('pendingHomeProjectSlug') || (permissions && permissions.createProject !== true)) return;
|
|
92
|
+
openAddProjectModal();
|
|
83
93
|
}
|
|
84
94
|
}
|
|
85
95
|
|
|
@@ -88,14 +98,15 @@ export function initProjectsHub() {
|
|
|
88
98
|
store.subscribe(function (state, previous) {
|
|
89
99
|
if (state.projectsHubRestoreRequested && !previous.projectsHubRestoreRequested) {
|
|
90
100
|
showProjectsHub(true);
|
|
91
|
-
if (location.pathname === '/') history.replaceState(null, '', '/projects');
|
|
92
101
|
store.set({ projectsHubRestoreRequested: false });
|
|
93
102
|
return;
|
|
94
103
|
}
|
|
95
104
|
if (!state.projectsHubVisible) return;
|
|
105
|
+
// A project already being opened keeps its own activation; otherwise known projects replace the hub.
|
|
106
|
+
if (state.projectListLoaded && !state.pendingHomeProjectSlug && ordinaryProjects(state.projectsHubList).length) { showProjectsHub(true); return; }
|
|
96
107
|
if (state.projectsHubList !== previous.projectsHubList || state.projectListLoaded !== previous.projectListLoaded ||
|
|
97
108
|
state.homeSurfaceLoaded !== previous.homeSurfaceLoaded || state.onboardingStep !== previous.onboardingStep ||
|
|
98
109
|
state.projectsAccessLoaded !== previous.projectsAccessLoaded || state.permissions !== previous.permissions ||
|
|
99
|
-
state.connected !== previous.connected || state.pendingHomeProjectSlug !== previous.pendingHomeProjectSlug) render();
|
|
110
|
+
state.connected !== previous.connected || state.cachedMatesList !== previous.cachedMatesList || state.pendingHomeProjectSlug !== previous.pendingHomeProjectSlug) render();
|
|
100
111
|
});
|
|
101
112
|
}
|
|
@@ -1,46 +1,28 @@
|
|
|
1
1
|
import { initStorageUsage, loadStorageUsage } from './server-storage-settings.js';
|
|
2
2
|
import { initAccountAuthSettings, loadAccountAuthSettings } from './account-auth-settings.js';
|
|
3
|
-
//
|
|
3
|
+
// Server pane of the unified settings shell. Page state survives navigation and closing.
|
|
4
4
|
import { store } from './store.js';
|
|
5
5
|
import { getWs } from './ws-ref.js';
|
|
6
6
|
import { refreshIcons } from './icons.js';
|
|
7
7
|
import { closeFileViewer } from './filebrowser.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { loadAdminSection } from './admin.js';
|
|
9
|
+
import { registerSettingsScope, isSettingsShellOpen, activeSettingsScope } from './settings-shell.js';
|
|
10
10
|
import { initNetworkSettings, loadNetworkSettings } from './server-network-settings.js';
|
|
11
11
|
import { initServerControls, setServerPinAvailability } from './server-settings-controls.js';
|
|
12
12
|
import { initServerEnvironment, loadSharedEnv } from './server-settings-environment.js';
|
|
13
|
-
import {
|
|
14
|
-
import { hasSettingsDrafts, confirmSettingsAction, trapSettingsFocus } from './server-settings-state.js';
|
|
13
|
+
import { hasSettingsDrafts } from './server-settings-state.js';
|
|
15
14
|
import { enhanceSettingsSelects } from './settings-select.js';
|
|
16
15
|
export { updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleServerSettingResult } from './server-settings-controls.js';
|
|
17
16
|
export { handleSharedEnv, handleSharedEnvSaved } from './server-settings-environment.js';
|
|
18
17
|
|
|
19
|
-
function isolateSettings(active) {
|
|
20
|
-
if (active) {
|
|
21
|
-
var entries = Array.from(document.body.children).filter(function (el) { return el !== root() && el.id !== 'confirm-modal' && el.tagName !== 'SCRIPT' && el.tagName !== 'STYLE'; }).map(function (el) { var entry = { el: el, inert: el.inert, hidden: el.getAttribute('aria-hidden') }; el.inert = true; el.setAttribute('aria-hidden', 'true'); return entry; });
|
|
22
|
-
store.set({ serverSettingsInert: entries });
|
|
23
|
-
} else {
|
|
24
|
-
(store.get('serverSettingsInert') || []).forEach(function (entry) { entry.el.inert = entry.inert; if (entry.hidden === null) entry.el.removeAttribute('aria-hidden'); else entry.el.setAttribute('aria-hidden', entry.hidden); });
|
|
25
|
-
store.set({ serverSettingsInert: [] });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
18
|
function root() { return document.getElementById('server-settings'); }
|
|
29
|
-
export function isSettingsOpen() { return
|
|
19
|
+
export function isSettingsOpen() { return isSettingsShellOpen() && activeSettingsScope() === 'server'; }
|
|
30
20
|
function send(type) { var ws = getWs(); if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: type })); }
|
|
31
21
|
|
|
32
|
-
function selectPage(id
|
|
22
|
+
function selectPage(id) {
|
|
33
23
|
if (!store.get('serverSettingsAuthorized')) return;
|
|
34
|
-
if (id === 'admin-invites') id = 'admin-users';
|
|
35
|
-
var nav = document.getElementById('settings-page-nav');
|
|
36
|
-
if (!nav.querySelector('[data-section="' + id + '"]')) id = 'overview';
|
|
37
24
|
store.set({ serverSettingsPage: id });
|
|
38
25
|
root().querySelectorAll('.server-settings-section').forEach(function (page) { page.classList.toggle('active', page.dataset.section === id); });
|
|
39
|
-
nav.querySelectorAll('button').forEach(function (button) {
|
|
40
|
-
button.classList.toggle('active', button.dataset.section === id);
|
|
41
|
-
if (button.dataset.section === id) button.setAttribute('aria-current', 'page'); else button.removeAttribute('aria-current');
|
|
42
|
-
});
|
|
43
|
-
document.getElementById('settings-nav-dropdown').value = id;
|
|
44
26
|
if (id === 'network') loadNetworkSettings();
|
|
45
27
|
if (id === 'security') loadAccountAuthSettings();
|
|
46
28
|
if (id === 'storage') loadStorageUsage();
|
|
@@ -49,85 +31,48 @@ function selectPage(id, focus) {
|
|
|
49
31
|
var body = document.getElementById(id + '-body');
|
|
50
32
|
if (id !== 'admin-smtp' || !(store.get('serverSettingsDrafts') || {}).smtp) loadAdminSection(id, body);
|
|
51
33
|
}
|
|
52
|
-
|
|
53
|
-
var heading = root().querySelector('.server-settings-section.active h2');
|
|
54
|
-
heading.tabIndex = -1; heading.focus();
|
|
55
|
-
}
|
|
34
|
+
enhanceSettingsSelects(root()); refreshIcons(root());
|
|
56
35
|
}
|
|
57
36
|
|
|
58
|
-
|
|
59
|
-
|
|
37
|
+
// The shell has already verified administrator (or solo-owner) access.
|
|
38
|
+
function enterServerSettings(ctx) {
|
|
60
39
|
closeFileViewer();
|
|
61
|
-
isolateSettings(true);
|
|
62
|
-
store.set({ serverSettingsReturnFocus: document.activeElement });
|
|
63
|
-
root().classList.remove('hidden');
|
|
64
|
-
document.getElementById('server-settings-btn').classList.add('active');
|
|
65
|
-
document.getElementById('server-settings-close').focus();
|
|
66
40
|
var status = document.getElementById('settings-access-status');
|
|
67
|
-
status.textContent = '
|
|
68
|
-
store.set({ serverSettingsAuthorized:
|
|
69
|
-
|
|
70
|
-
if (serverPinSection) serverPinSection.hidden = true;
|
|
41
|
+
status.textContent = '';
|
|
42
|
+
store.set({ serverSettingsAuthorized: true });
|
|
43
|
+
setServerPinAvailability(!ctx.access.multiUser);
|
|
71
44
|
root().querySelectorAll('.server-settings-section').forEach(function (page) { page.classList.remove('active'); });
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!isSettingsOpen()) return;
|
|
84
|
-
store.set({ serverSettingsAuthorized: true });
|
|
85
|
-
setServerPinAvailability(!data.multiUser);
|
|
86
|
-
status.textContent = '';
|
|
87
|
-
renderSettingsNavigation(!!data.multiUser, selectPage);
|
|
88
|
-
selectPage(store.get('serverSettingsPage') || 'overview', false);
|
|
89
|
-
if (!hasSettingsDrafts()) send('get_daemon_config');
|
|
90
|
-
setTimeout(function () {
|
|
91
|
-
if (isSettingsOpen() && !store.get('serverSettingsConfigLoaded')) status.textContent = 'Server configuration has not loaded. Reconnect, then reopen Settings to retry.';
|
|
92
|
-
}, 12000);
|
|
93
|
-
send('process_stats');
|
|
94
|
-
clearInterval(store.get('serverSettingsStatsTimer'));
|
|
95
|
-
store.set({ serverSettingsStatsTimer: setInterval(function () { send('process_stats'); }, 5000) });
|
|
96
|
-
document.getElementById('settings-project-slug').textContent = store.get('currentSlug') || '(none)';
|
|
97
|
-
document.getElementById('settings-ws-path').textContent = store.get('wsPath') || '/ws';
|
|
98
|
-
var pill = document.getElementById('skip-perms-pill');
|
|
99
|
-
document.getElementById('settings-skip-perms').textContent = pill && !pill.classList.contains('hidden') ? 'Skip' : 'Permission checks';
|
|
100
|
-
enhanceSettingsSelects(root()); refreshIcons(root());
|
|
101
|
-
} catch (error) { status.textContent = error.message; }
|
|
45
|
+
if (!hasSettingsDrafts()) send('get_daemon_config');
|
|
46
|
+
setTimeout(function () {
|
|
47
|
+
if (isSettingsOpen() && !store.get('serverSettingsConfigLoaded')) status.textContent = 'Server configuration has not loaded. Reconnect, then reopen Settings to retry.';
|
|
48
|
+
}, 12000);
|
|
49
|
+
send('process_stats');
|
|
50
|
+
clearInterval(store.get('serverSettingsStatsTimer'));
|
|
51
|
+
store.set({ serverSettingsStatsTimer: setInterval(function () { send('process_stats'); }, 5000) });
|
|
52
|
+
document.getElementById('settings-project-slug').textContent = store.get('currentSlug') || '(none)';
|
|
53
|
+
document.getElementById('settings-ws-path').textContent = store.get('wsPath') || '/ws';
|
|
54
|
+
var pill = document.getElementById('skip-perms-pill');
|
|
55
|
+
document.getElementById('settings-skip-perms').textContent = pill && !pill.classList.contains('hidden') ? 'Skip' : 'Permission checks';
|
|
102
56
|
}
|
|
103
57
|
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
root().classList.add('hidden');
|
|
108
|
-
document.getElementById('server-settings-btn').classList.remove('active');
|
|
109
|
-
clearInterval(store.get('serverSettingsStatsTimer'));
|
|
110
|
-
var previous = store.get('serverSettingsReturnFocus');
|
|
111
|
-
if (previous && previous.isConnected) previous.focus();
|
|
112
|
-
}
|
|
113
|
-
if (hasSettingsDrafts()) confirmSettingsAction('You have unsaved changes. Closing keeps these drafts in this tab, but reloading the page will lose them.', 'Close with drafts', close);
|
|
114
|
-
else close();
|
|
58
|
+
function leaveServerSettings() {
|
|
59
|
+
clearInterval(store.get('serverSettingsStatsTimer'));
|
|
60
|
+
store.set({ serverSettingsAuthorized: false });
|
|
115
61
|
}
|
|
116
62
|
|
|
117
63
|
export function initServerSettings() {
|
|
118
64
|
if (!root()) return;
|
|
119
65
|
initNetworkSettings(); initServerControls(); initServerEnvironment(); initStorageUsage(); initAccountAuthSettings();
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
trapSettingsFocus(event, root());
|
|
66
|
+
registerSettingsScope('server', {
|
|
67
|
+
pane: root(),
|
|
68
|
+
enter: enterServerSettings,
|
|
69
|
+
activate: selectPage,
|
|
70
|
+
leave: leaveServerSettings,
|
|
71
|
+
leavePrompt: function (kind) {
|
|
72
|
+
if (!hasSettingsDrafts() || kind === 'page' || kind === 'project') return null;
|
|
73
|
+
if (kind === 'close') return { message: 'You have unsaved changes. Closing keeps these drafts in this tab, but reloading the page will lose them.', action: 'Close with drafts' };
|
|
74
|
+
return { message: 'Your server settings drafts will remain in this tab while you visit another settings area.', action: 'Leave Server settings' };
|
|
75
|
+
}
|
|
131
76
|
});
|
|
132
77
|
new MutationObserver(function () { enhanceSettingsSelects(root()); }).observe(root(), { childList: true, subtree: true });
|
|
133
78
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Project selection for settings. The settings target is independent of the
|
|
2
|
+
// open workspace: choosing a project here never changes currentSlug or the
|
|
3
|
+
// workspace WebSocket. Pure helpers plus a generation guard for async replies.
|
|
4
|
+
|
|
5
|
+
export function settingsProjectLabel(project) {
|
|
6
|
+
return (project && (project.title || project.project || project.name || project.slug)) || '';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Mates and the Clay project have their own settings surfaces.
|
|
10
|
+
export function isSettingsProject(project, permissions) {
|
|
11
|
+
if (!project || !project.slug || project.isMate || /^mate-/.test(project.slug)) return false;
|
|
12
|
+
return !permissions || permissions.projectSettings !== false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Authorised projects for the signed-in user (the server-filtered project
|
|
16
|
+
// list), the open workspace first, the rest in their existing order.
|
|
17
|
+
export function listSettingsProjects(projects, currentSlug, permissions) {
|
|
18
|
+
var eligible = (Array.isArray(projects) ? projects : []).filter(function (project) { return isSettingsProject(project, permissions); });
|
|
19
|
+
var current = eligible.filter(function (project) { return project.slug === currentSlug; });
|
|
20
|
+
var others = eligible.filter(function (project) { return project.slug !== currentSlug; });
|
|
21
|
+
return current.concat(others).map(function (project, index) {
|
|
22
|
+
return { slug: project.slug, label: settingsProjectLabel(project), isCurrent: project.slug === currentSlug, project: project, first: index === 0 };
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// A requested slug must still be authorised; otherwise use the current
|
|
27
|
+
// workspace, then the first choice, then nothing.
|
|
28
|
+
export function resolveTargetSlug(requested, choices) {
|
|
29
|
+
var list = choices || [];
|
|
30
|
+
for (var i = 0; i < list.length; i++) if (list[i].slug === requested) return requested;
|
|
31
|
+
return list.length ? list[0].slug : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function findChoice(choices, slug) {
|
|
35
|
+
var list = choices || [];
|
|
36
|
+
for (var i = 0; i < list.length; i++) if (list[i].slug === slug) return list[i];
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function optionLabel(choice) {
|
|
41
|
+
return choice.isCurrent ? choice.label + ' (open workspace)' : choice.label;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Every settings open, scope change and project switch takes a new token.
|
|
45
|
+
// Async work captures the token and ignores its result once it is stale.
|
|
46
|
+
export function createTargetBinding() {
|
|
47
|
+
var generation = 0;
|
|
48
|
+
var slug = null;
|
|
49
|
+
return {
|
|
50
|
+
bind: function (next) { generation += 1; slug = next || null; return generation; },
|
|
51
|
+
invalidate: function () { generation += 1; slug = null; return generation; },
|
|
52
|
+
isCurrent: function (token, expectedSlug) { return token === generation && (expectedSlug === undefined || expectedSlug === slug); },
|
|
53
|
+
slug: function () { return slug; },
|
|
54
|
+
generation: function () { return generation; }
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -72,7 +72,7 @@ export function enhanceSettingsSelects(root) {
|
|
|
72
72
|
button.setAttribute('aria-expanded', 'true');
|
|
73
73
|
document.addEventListener('pointerdown', outside);
|
|
74
74
|
var bounds = button.getBoundingClientRect();
|
|
75
|
-
var boundary = wrap.closest('.server-settings-content, .us-modal-content, .us-content, .ps-content, .admin-modal');
|
|
75
|
+
var boundary = wrap.closest('.settings-shell-content, .server-settings-content, .us-modal-content, .us-content, .ps-content, .admin-modal');
|
|
76
76
|
var bottom = Math.min(window.innerHeight - 8, boundary ? boundary.getBoundingClientRect().bottom - 8 : window.innerHeight - 8);
|
|
77
77
|
var above = bounds.top - Math.max(8, boundary ? boundary.getBoundingClientRect().top + 8 : 8);
|
|
78
78
|
var below = bottom - bounds.bottom - 4;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Pure routing model for the unified settings shell: scopes, pages, legacy
|
|
2
|
+
// aliases and access-based fallbacks. No DOM or store access, so every entry
|
|
3
|
+
// point resolves to one exact { scope, page, slug } destination.
|
|
4
|
+
|
|
5
|
+
export var SETTINGS_SCOPES = [
|
|
6
|
+
{ id: 'personal', label: 'Personal', hint: 'Applies to your account on every device' },
|
|
7
|
+
{ id: 'project', label: 'Project', hint: 'Applies to the selected project' },
|
|
8
|
+
{ id: 'server', label: 'Server', hint: 'Applies to this Clay server for everyone' }
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export var PERSONAL_PAGES = [
|
|
12
|
+
{ id: 'us-account', label: 'Account' }, { id: 'us-appearance', label: 'Appearance' },
|
|
13
|
+
{ id: 'us-chat', label: 'Conversation' }, { id: 'us-notifications', label: 'Notifications' },
|
|
14
|
+
{ id: 'us-ai', label: 'AI preferences' }, { id: 'us-integrations', label: 'Integrations' }
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export var PROJECT_PAGES = [
|
|
18
|
+
{ id: 'profile', label: 'General' }, { id: 'access', label: 'Access' }, { id: 'environment', label: 'Environment' }
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export var SERVER_PAGES = [
|
|
22
|
+
{ id: 'overview', category: 'Service', label: 'Overview' },
|
|
23
|
+
{ id: 'network', category: 'Service', label: 'Network' },
|
|
24
|
+
{ id: 'storage', category: 'Service', label: 'Storage' },
|
|
25
|
+
{ id: 'security', category: 'Administration', label: 'Access & security' },
|
|
26
|
+
{ id: 'admin-users', category: 'Administration', label: 'Users', multiUser: true },
|
|
27
|
+
{ id: 'admin-projects', category: 'Administration', label: 'Project access', multiUser: true },
|
|
28
|
+
{ id: 'admin-smtp', category: 'Administration', label: 'Email delivery', multiUser: true },
|
|
29
|
+
{ id: 'environment', category: 'Agent configuration', label: 'Environment' },
|
|
30
|
+
{ id: 'operations', category: 'Maintenance', label: 'Operations' }
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
var PERSONAL_ALIASES = {
|
|
34
|
+
'us-mates': 'us-chat', 'us-experimental': 'us-chat', 'us-connections': 'us-integrations',
|
|
35
|
+
'us-email': 'us-integrations', 'us-logout': 'us-account'
|
|
36
|
+
};
|
|
37
|
+
var SERVER_ALIASES = { 'admin-invites': 'admin-users' };
|
|
38
|
+
|
|
39
|
+
export var DEFAULT_PERSONAL_PAGE = 'us-account';
|
|
40
|
+
|
|
41
|
+
// access: { server: boolean, multiUser: boolean, project: boolean }
|
|
42
|
+
export function defaultAccess() { return { server: false, multiUser: false, project: true }; }
|
|
43
|
+
|
|
44
|
+
// The authoritative role rule: a multi-user server needs an admin; a solo
|
|
45
|
+
// (single-user) server's owner has server access.
|
|
46
|
+
export function accessFromMe(me, permissions) {
|
|
47
|
+
var multiUser = !!(me && me.multiUser);
|
|
48
|
+
var admin = !!(me && me.user && me.user.role === 'admin');
|
|
49
|
+
return { server: !!me && (!multiUser || admin), multiUser: multiUser, project: !permissions || permissions.projectSettings !== false };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function pagesForScope(scope, access) {
|
|
53
|
+
access = access || defaultAccess();
|
|
54
|
+
if (scope === 'personal') return PERSONAL_PAGES.slice();
|
|
55
|
+
if (scope === 'project') return PROJECT_PAGES.slice();
|
|
56
|
+
if (scope === 'server') return SERVER_PAGES.filter(function (page) { return !page.multiUser || access.multiUser; });
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function scopeAvailable(scope, access) {
|
|
61
|
+
access = access || defaultAccess();
|
|
62
|
+
if (scope === 'personal') return true;
|
|
63
|
+
if (scope === 'project') return !!access.project;
|
|
64
|
+
if (scope === 'server') return !!access.server;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function resolvePage(scope, page, access) {
|
|
69
|
+
var alias = scope === 'personal' ? PERSONAL_ALIASES[page] : scope === 'server' ? SERVER_ALIASES[page] : null;
|
|
70
|
+
var id = alias || page;
|
|
71
|
+
var pages = pagesForScope(scope, access);
|
|
72
|
+
for (var i = 0; i < pages.length; i++) if (pages[i].id === id) return id;
|
|
73
|
+
return pages.length ? pages[0].id : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Unknown or unavailable scopes fall back to Personal, which is always allowed.
|
|
77
|
+
export function resolveRoute(route, access) {
|
|
78
|
+
route = route || {};
|
|
79
|
+
var scope = scopeAvailable(route.scope, access) ? route.scope : 'personal';
|
|
80
|
+
var requested = scope === route.scope ? route.page : null;
|
|
81
|
+
return {
|
|
82
|
+
scope: scope,
|
|
83
|
+
page: resolvePage(scope, requested, access),
|
|
84
|
+
slug: scope === 'project' ? (route.slug || null) : null,
|
|
85
|
+
requestedScope: route.scope || 'personal',
|
|
86
|
+
redirected: scope !== (route.scope || 'personal')
|
|
87
|
+
};
|
|
88
|
+
}
|