clay-server 5.2.0-beta.10 → 5.2.0-beta.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +3 -0
- 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 +2 -0
- 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
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Live appearance sync between the app shell and its pane iframes (split
|
|
2
|
+
// Driver/Worker panes and the contextual Mate window). Each iframe runs its own
|
|
3
|
+
// theme module and only reads the persisted preference at load, so the shell
|
|
4
|
+
// pushes the applied appearance down on every change and when a frame loads.
|
|
5
|
+
// Same-origin only, shell to child only; nothing is saved from either side.
|
|
6
|
+
|
|
7
|
+
import { store } from './store.js';
|
|
8
|
+
import { onThemeChange, getAppearanceSnapshot, applyExternalAppearance } from './theme.js';
|
|
9
|
+
|
|
10
|
+
var ID = /^[a-zA-Z0-9_-]{1,120}$/;
|
|
11
|
+
|
|
12
|
+
function paneFrames() {
|
|
13
|
+
return Array.prototype.filter.call(document.querySelectorAll('iframe'), function (frame) {
|
|
14
|
+
try { var url = new URL(frame.src, window.location.href); return url.origin === window.location.origin && url.searchParams.get('pane') === '1'; }
|
|
15
|
+
catch (error) { return false; }
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function push(frame) {
|
|
20
|
+
var snapshot = getAppearanceSnapshot();
|
|
21
|
+
try { if (frame.contentWindow) frame.contentWindow.postMessage({ type: 'clay-appearance', snapshot: snapshot }, window.location.origin); } catch (error) {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isId(value) { return typeof value === 'string' && ID.test(value); }
|
|
25
|
+
function isIdOrNull(value) { return value === null || isId(value); }
|
|
26
|
+
|
|
27
|
+
// Exact shape only: strings or null, never coerced values.
|
|
28
|
+
function validSnapshot(value) {
|
|
29
|
+
var pref = value && typeof value === 'object' ? value.preference : null;
|
|
30
|
+
if (!pref || typeof pref !== 'object' || !isId(value.themeId)) return false;
|
|
31
|
+
if (value.userId !== null && (typeof value.userId !== 'string' || !value.userId || value.userId.length > 200)) return false;
|
|
32
|
+
if (typeof pref.configured !== 'boolean') return false;
|
|
33
|
+
if (pref.mode !== null && pref.mode !== 'light' && pref.mode !== 'dark') return false;
|
|
34
|
+
return isIdOrNull(pref.theme) && isIdOrNull(pref.lightTheme) && isIdOrNull(pref.darkTheme);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function initPaneAppearanceBridge() {
|
|
38
|
+
if (store.get('paneMode')) {
|
|
39
|
+
if (window.parent === window) return;
|
|
40
|
+
window.addEventListener('message', function (event) {
|
|
41
|
+
if (event.origin !== window.location.origin || event.source !== window.parent) return;
|
|
42
|
+
var msg = event.data;
|
|
43
|
+
if (!msg || msg.type !== 'clay-appearance' || !validSnapshot(msg.snapshot)) return;
|
|
44
|
+
// Once this frame knows its owner the snapshot must name exactly that owner (null is a mismatch).
|
|
45
|
+
// Before identity is known, or in solo mode without one, any well-formed snapshot applies.
|
|
46
|
+
var owner = store.get('myUserId') || null;
|
|
47
|
+
if (owner !== null && msg.snapshot.userId !== owner) return;
|
|
48
|
+
applyExternalAppearance(msg.snapshot);
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
onThemeChange(function () { paneFrames().forEach(push); });
|
|
53
|
+
// Frames that rejected an early snapshot (identity not yet known here) get it again once it is.
|
|
54
|
+
store.subscribe(function (state, previous) { if (state.myUserId !== previous.myUserId) paneFrames().forEach(push); });
|
|
55
|
+
// A frame that finished loading may have been seeded before a pending save.
|
|
56
|
+
document.addEventListener('load', function (event) {
|
|
57
|
+
if (event.target && event.target.tagName === 'IFRAME' && paneFrames().indexOf(event.target) >= 0) push(event.target);
|
|
58
|
+
}, true);
|
|
59
|
+
}
|
|
@@ -135,9 +135,12 @@ export function uploadAvatarBlob(blob) {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
// --- Avatar position picker ---
|
|
138
|
-
|
|
138
|
+
// With a host element the editor mounts inline as a settings subview instead
|
|
139
|
+
// of a page-level overlay.
|
|
140
|
+
export function showAvatarPositioner(img, objectUrl, onDone, host) {
|
|
139
141
|
var outputSize = 256;
|
|
140
142
|
var viewSize = 220;
|
|
143
|
+
var priorFocus = document.activeElement;
|
|
141
144
|
|
|
142
145
|
// State
|
|
143
146
|
var scale = 1;
|
|
@@ -169,7 +172,7 @@ export function showAvatarPositioner(img, objectUrl, onDone) {
|
|
|
169
172
|
|
|
170
173
|
// Build overlay
|
|
171
174
|
var overlay = document.createElement('div');
|
|
172
|
-
overlay.className = 'avatar-positioner-overlay';
|
|
175
|
+
overlay.className = host ? 'avatar-positioner-inline settings-subview' : 'avatar-positioner-overlay';
|
|
173
176
|
|
|
174
177
|
var container = document.createElement('div');
|
|
175
178
|
container.className = 'avatar-positioner-container';
|
|
@@ -225,8 +228,10 @@ export function showAvatarPositioner(img, objectUrl, onDone) {
|
|
|
225
228
|
container.appendChild(btnRow);
|
|
226
229
|
|
|
227
230
|
overlay.appendChild(container);
|
|
228
|
-
|
|
231
|
+
if (host) { host.classList.add('has-subview'); host.appendChild(overlay); overlay.setAttribute('role', 'region'); overlay.setAttribute('aria-label', 'Position your avatar'); }
|
|
232
|
+
else document.body.appendChild(overlay);
|
|
229
233
|
updatePreview();
|
|
234
|
+
if (host) doneFocus();
|
|
230
235
|
|
|
231
236
|
// Drag
|
|
232
237
|
var dragging = false;
|
|
@@ -288,9 +293,12 @@ export function showAvatarPositioner(img, objectUrl, onDone) {
|
|
|
288
293
|
updatePreview();
|
|
289
294
|
}, { passive: false });
|
|
290
295
|
|
|
296
|
+
function doneFocus() { closeBtn.focus(); }
|
|
297
|
+
|
|
291
298
|
function cleanup(revoke) {
|
|
292
299
|
document.removeEventListener('keydown', onOverlayKeydown);
|
|
293
300
|
overlay.remove();
|
|
301
|
+
if (host) { host.classList.remove('has-subview'); if (priorFocus && priorFocus.isConnected) priorFocus.focus(); }
|
|
294
302
|
if (revoke) URL.revokeObjectURL(objectUrl);
|
|
295
303
|
}
|
|
296
304
|
|
|
@@ -81,6 +81,9 @@ function startMateCreation() {
|
|
|
81
81
|
maybeStartPendingCreation();
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// Entry for the zero-project welcome: the same Clay-guided flow the add button's Mate choice runs.
|
|
85
|
+
export function startClayMateCreation() { startMateCreation(); }
|
|
86
|
+
|
|
84
87
|
function maybeStartPendingCreation() {
|
|
85
88
|
var state = store.snap();
|
|
86
89
|
if (!state.projectMateCreationPendingStart || !state.connected || state.activeProjectSlug !== 'mate-' + state.projectMateCreationMateId) return;
|
|
@@ -89,11 +92,17 @@ function maybeStartPendingCreation() {
|
|
|
89
92
|
store.set({ projectMateCreationPendingStart: false, projectMateCreationAwaitingHistory: true, projectMateCreationRequestId: requestId });
|
|
90
93
|
}
|
|
91
94
|
|
|
92
|
-
function choiceButton(
|
|
95
|
+
function choiceButton(artwork, title, description) {
|
|
93
96
|
var button = document.createElement('button');
|
|
94
97
|
button.type = 'button';
|
|
95
98
|
button.className = 'project-create-choice-option';
|
|
96
|
-
|
|
99
|
+
var image = document.createElement('img');
|
|
100
|
+
image.src = '/welcome-art/' + artwork + '.png';
|
|
101
|
+
image.alt = '';
|
|
102
|
+
image.width = 1536;
|
|
103
|
+
image.height = 1024;
|
|
104
|
+
image.decoding = 'async';
|
|
105
|
+
button.appendChild(image);
|
|
97
106
|
var copy = document.createElement('span');
|
|
98
107
|
var heading = document.createElement('strong');
|
|
99
108
|
heading.textContent = title;
|
|
@@ -121,9 +130,9 @@ export function openProjectCreateChoice(opener, openProject) {
|
|
|
121
130
|
var title = document.createElement('h2');
|
|
122
131
|
title.id = 'project-create-choice-title';
|
|
123
132
|
title.textContent = 'What would you like to add?';
|
|
124
|
-
var mate = choiceButton('
|
|
133
|
+
var mate = choiceButton('mate-clay', 'Create a Mate', 'A specialist to work alongside you.');
|
|
125
134
|
mate.addEventListener('click', startMateCreation);
|
|
126
|
-
var project = choiceButton('
|
|
135
|
+
var project = choiceButton('project-clay', 'Start a project', 'Work on code or files together.');
|
|
127
136
|
project.addEventListener('click', function () {
|
|
128
137
|
closeChoice(false);
|
|
129
138
|
openProject();
|
|
@@ -27,12 +27,10 @@ function setSelectState(select, value, disabled) {
|
|
|
27
27
|
select.disabled = !!disabled;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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:
|
|
104
|
+
var active = { slug: slug || "", effectiveValue: value, isWorktree: !!(project && project.isWorktree), authorized: access.editable };
|
|
102
105
|
var pending = pendingState();
|
|
103
106
|
setVisibilityState(active, pending);
|
|
104
107
|
renderActiveState();
|
|
@@ -124,7 +127,7 @@ export function handleProjectSessionVisibilityDefault(msg) {
|
|
|
124
127
|
var active = activeState();
|
|
125
128
|
setVisibilityState(active, null);
|
|
126
129
|
var select = selectElement();
|
|
127
|
-
if (!select || !active || active.slug !== msg.slug) return;
|
|
130
|
+
if (!select || !active || active.slug !== msg.slug) { renderActiveState(); return; }
|
|
128
131
|
if (!msg.ok) {
|
|
129
132
|
active = Object.assign({}, active, { effectiveValue: pending.previous });
|
|
130
133
|
setVisibilityState(active, null);
|
|
@@ -31,7 +31,18 @@ export function handleProjectEnv(msg) {
|
|
|
31
31
|
update({ loaded: true, saved: msg.envrc || '', rows: parseEnvString(msg.envrc || '') });
|
|
32
32
|
render(); status('');
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
// A draft exists only while the rows differ from the saved variables; blank rows and edits that are
|
|
35
|
+
// reverted by hand leave nothing to save.
|
|
36
|
+
function rowsSignature(rows) {
|
|
37
|
+
return (rows || []).filter(function (row) { return row.key.trim() || row.value; }).map(function (row) { return row.key.trim() + '\u0000' + row.value; }).join('\n');
|
|
38
|
+
}
|
|
39
|
+
function changeRows(rows) {
|
|
40
|
+
var dirty = rowsSignature(rows) !== rowsSignature(parseEnvString(state().saved || ''));
|
|
41
|
+
update({ rows: rows, dirty: dirty });
|
|
42
|
+
document.getElementById('ps-env-save').disabled = !dirty || !!state().saving;
|
|
43
|
+
if (!dirty) clearCachedDraft();
|
|
44
|
+
status(dirty ? 'Unsaved changes' : '');
|
|
45
|
+
}
|
|
35
46
|
function render() {
|
|
36
47
|
var editor = state();
|
|
37
48
|
var list = document.getElementById('ps-env-list'); list.replaceChildren();
|
|
@@ -1,76 +1,64 @@
|
|
|
1
1
|
import { store } from './store.js';
|
|
2
2
|
import { getWs } from './ws-ref.js';
|
|
3
|
-
import { trapSettingsFocus, isolateSettingsDialog, confirmSettingsAction } from './server-settings-state.js';
|
|
4
3
|
import { initProjectEnvironment, openProjectEnvironment, projectEnvironmentDirty, discardProjectEnvironment } from './project-settings-environment.js';
|
|
5
4
|
import { enhanceSettingsSelects } from './settings-select.js';
|
|
6
|
-
// Project identity and
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
196
|
-
sections[j].classList.toggle("active", active2);
|
|
196
|
+
sections[j].classList.toggle("active", sections[j].dataset.section === name);
|
|
197
197
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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
|
-
|
|
334
|
-
|
|
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) {
|