clay-server 5.2.0-beta.9 → 5.2.1-beta.1
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 +30 -18
- package/lib/public/css/command-palette.css +12 -2
- package/lib/public/css/contextual-clay.css +1 -1
- package/lib/public/css/filebrowser.css +4 -5
- package/lib/public/css/mate-loading-scene.css +21 -0
- package/lib/public/css/messages.css +30 -11
- package/lib/public/css/overlays.css +67 -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/right-workbench.css +28 -0
- package/lib/public/css/settings-shell.css +103 -0
- package/lib/public/css/sidebar.css +7 -0
- package/lib/public/css/title-bar.css +99 -0
- package/lib/public/index.html +54 -66
- 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/pair-result-status.js +89 -39
- 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/topbar-workbench-controls.js +102 -0
- 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 +13 -10
- 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
|
@@ -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 {
|
|
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
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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,
|
|
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';
|
|
@@ -222,6 +222,7 @@ var customSet = {}; // ids that came from ~/.clay/themes/
|
|
|
222
222
|
var themesLoaded = false;
|
|
223
223
|
var currentThemeId = DEFAULT_DARK_THEME_ID;
|
|
224
224
|
var changeCallbacks = [];
|
|
225
|
+
var externalThemeId = null; // set by the parent shell for pane iframes
|
|
225
226
|
var STORAGE_KEY = "clay-theme";
|
|
226
227
|
var MODE_KEY = "clay-mode"; // "light" | "dark" | null (system)
|
|
227
228
|
var SKIN_KEY = "clay-skin"; // theme id within current variant pair
|
|
@@ -274,6 +275,23 @@ export function onThemeChange(fn) {
|
|
|
274
275
|
changeCallbacks.push(fn);
|
|
275
276
|
}
|
|
276
277
|
|
|
278
|
+
// The appearance this frame currently shows, for the parent shell to hand to
|
|
279
|
+
// its pane iframes. Never persisted from here.
|
|
280
|
+
export function getAppearanceSnapshot() {
|
|
281
|
+
var pref = store.get('appearanceDraft') || store.get('appearancePreference') || {};
|
|
282
|
+
return { themeId: currentThemeId, userId: store.get('myUserId') || null,
|
|
283
|
+
preference: { configured: !!pref.configured || !!pref.mode || !!pref.theme, theme: pref.theme || null, mode: pref.mode || null,
|
|
284
|
+
lightTheme: pref.lightTheme || null, darkTheme: pref.darkTheme || null } };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// A pane iframe mirrors the parent's live appearance. The server remains the
|
|
288
|
+
// only persisted source, so this applies the view without saving anything.
|
|
289
|
+
export function applyExternalAppearance(snapshot) {
|
|
290
|
+
externalThemeId = snapshot.themeId;
|
|
291
|
+
store.set({ appearancePreference: snapshot.preference, appearanceDraft: null });
|
|
292
|
+
applyTheme(snapshot.themeId);
|
|
293
|
+
}
|
|
294
|
+
|
|
277
295
|
export function getThemes() {
|
|
278
296
|
// Return a copy
|
|
279
297
|
var all = {};
|
|
@@ -367,7 +385,7 @@ function loadThemes() {
|
|
|
367
385
|
|
|
368
386
|
// Always apply the current theme now that real data is loaded
|
|
369
387
|
// (before this, only defaultExactVars was used as fallback)
|
|
370
|
-
applyTheme(currentThemeId);
|
|
388
|
+
applyTheme(externalThemeId && getTheme(externalThemeId) ? externalThemeId : currentThemeId);
|
|
371
389
|
}).catch(function () {
|
|
372
390
|
// API unavailable — keep the embedded Clay Studio pair.
|
|
373
391
|
themes[DEFAULT_DARK_THEME_ID] = defaultDarkFallback;
|
|
@@ -482,8 +500,13 @@ function updateToggleIcon() {
|
|
|
482
500
|
// User settings toggle
|
|
483
501
|
var usToggle = document.getElementById("us-theme-toggle");
|
|
484
502
|
if (usToggle) usToggle.checked = isLight;
|
|
485
|
-
|
|
486
|
-
|
|
503
|
+
var quickToggle = document.getElementById("appearance-mode-toggle");
|
|
504
|
+
if (quickToggle) {
|
|
505
|
+
var label = isLight ? "Switch to dark mode" : "Switch to light mode";
|
|
506
|
+
quickToggle.setAttribute("aria-label", label);
|
|
507
|
+
quickToggle.title = label;
|
|
508
|
+
quickToggle.innerHTML = '<i data-lucide="' + (isLight ? "moon" : "sun") + '" class="cursor-share-icon" aria-hidden="true"></i>';
|
|
509
|
+
}
|
|
487
510
|
refreshIcons();
|
|
488
511
|
}
|
|
489
512
|
|
|
@@ -649,6 +672,9 @@ export function initTheme() {
|
|
|
649
672
|
}
|
|
650
673
|
}
|
|
651
674
|
|
|
675
|
+
var quickToggle = document.getElementById("appearance-mode-toggle");
|
|
676
|
+
if (quickToggle) quickToggle.addEventListener("click", toggleDarkMode);
|
|
677
|
+
|
|
652
678
|
// Set initial toggle icon
|
|
653
679
|
updateToggleIcon();
|
|
654
680
|
|
|
@@ -711,6 +737,8 @@ async function loadAppearance() {
|
|
|
711
737
|
if (!response.ok) throw new Error('Appearance preferences unavailable.');
|
|
712
738
|
var data = await response.json();
|
|
713
739
|
if(store.get('appearanceSaveRequest'))return;
|
|
740
|
+
// A pane that already mirrors its parent's live view is newer than this GET, which may predate a pending save.
|
|
741
|
+
if(externalThemeId)return;
|
|
714
742
|
if (data.configured) {store.set({appearancePreference:data});applyTheme(themeIdForMode(data.mode || getSystemMode()));}
|
|
715
743
|
else {
|
|
716
744
|
var legacy = null;try {legacy = localStorage.getItem(STORAGE_KEY);}catch(error){}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Placement of the Browser, Terminal and sticky-note visibility buttons.
|
|
2
|
+
// Desktop keeps them in the top bar beside Notifications and Settings; where the
|
|
3
|
+
// top bar is hidden (mobile) the very same nodes return to the title bar status
|
|
4
|
+
// area. Nodes, IDs, handlers, state and permissions are never recreated.
|
|
5
|
+
var MOBILE = '(max-width: 768px)';
|
|
6
|
+
var IDS = ['sticky-notes-toggle-btn', 'shared-browser-toggle-btn', 'terminal-toggle-btn'];
|
|
7
|
+
var DESKTOP_ORDER = ['shared-browser-toggle-btn', 'terminal-toggle-btn', 'sticky-notes-toggle-btn'];
|
|
8
|
+
|
|
9
|
+
function place(mobile) {
|
|
10
|
+
var status = document.querySelector('#main-column > .title-bar-content .status');
|
|
11
|
+
var actions = document.querySelector('#top-bar .top-bar-actions');
|
|
12
|
+
var anchor = document.getElementById('notif-center-btn');
|
|
13
|
+
if (!status || !actions) return;
|
|
14
|
+
var order = mobile ? IDS : DESKTOP_ORDER;
|
|
15
|
+
for (var i = 0; i < order.length; i++) {
|
|
16
|
+
var node = document.getElementById(order[i]);
|
|
17
|
+
if (!node) continue;
|
|
18
|
+
if (mobile) status.appendChild(node);
|
|
19
|
+
else actions.insertBefore(node, anchor);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The search trigger is centred in the bar. It keeps that place and width when
|
|
24
|
+
// the free gap between the left group and the actions allows, otherwise it
|
|
25
|
+
// collapses to an icon-only trigger (same node, ID, handler and label) inside
|
|
26
|
+
// the gap. If even the icon cannot fit, the per-vendor usage chips fold into the
|
|
27
|
+
// existing all-vendors chip, then the active-session usage link folds, before
|
|
28
|
+
// anything else is touched. Action buttons are never hidden or clipped.
|
|
29
|
+
var MIN_FULL = 150, ICON = 24, MARGIN = 12;
|
|
30
|
+
|
|
31
|
+
// Sizes seen right after the last applied layout. Observer wake-ups that find the
|
|
32
|
+
// same sizes (including the ones our own class changes cause) do no work at all.
|
|
33
|
+
var applied = null;
|
|
34
|
+
|
|
35
|
+
function sizes(bar, left, actions) {
|
|
36
|
+
return Math.round(bar.offsetWidth) + '|' + Math.round(left.offsetWidth) + '|' + Math.round(actions.offsetWidth);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function layoutTopbar(force) {
|
|
40
|
+
var bar = document.getElementById('top-bar');
|
|
41
|
+
var left = bar && bar.querySelector('.top-bar-left-pills');
|
|
42
|
+
var actions = bar && bar.querySelector('.top-bar-actions');
|
|
43
|
+
var search = document.getElementById('cmd-palette-btn');
|
|
44
|
+
if (!bar || !left || !actions || !search) return;
|
|
45
|
+
if (!bar.offsetWidth) return;
|
|
46
|
+
if (!force && applied === sizes(bar, left, actions)) return;
|
|
47
|
+
if (getComputedStyle(search).display === 'none') return;
|
|
48
|
+
var levels = [[], ['usage-compact'], ['usage-compact', 'usage-compact-link']];
|
|
49
|
+
var result = null;
|
|
50
|
+
for (var i = 0; i < levels.length && !result; i++) {
|
|
51
|
+
bar.classList.remove('usage-compact', 'usage-compact-link', 'search-icon');
|
|
52
|
+
levels[i].forEach(function (name) { bar.classList.add(name); });
|
|
53
|
+
bar.style.removeProperty('--top-bar-search-width'); bar.style.removeProperty('--top-bar-search-left');
|
|
54
|
+
var width = bar.getBoundingClientRect().width, center = width / 2;
|
|
55
|
+
var barLeft = bar.getBoundingClientRect().left;
|
|
56
|
+
var leftEdge = left.getBoundingClientRect().right - barLeft, actionsEdge = actions.getBoundingClientRect().left - barLeft;
|
|
57
|
+
var half = Math.min(center - leftEdge, actionsEdge - center) - MARGIN;
|
|
58
|
+
var target = Math.min(520, width * 0.4);
|
|
59
|
+
if (2 * half >= MIN_FULL) result = { mode: 'full', width: Math.min(target, 2 * half), left: null, level: i };
|
|
60
|
+
else if (actionsEdge - leftEdge >= ICON + 2 * MARGIN) {
|
|
61
|
+
// Stay centred in the bar when that is inside the gap.
|
|
62
|
+
var lo = leftEdge + MARGIN + ICON / 2, hi = actionsEdge - MARGIN - ICON / 2;
|
|
63
|
+
result = { mode: 'icon', width: ICON, left: Math.min(Math.max(center, lo), hi), level: i };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!result) result = { mode: 'icon', width: ICON, left: Math.max(left.getBoundingClientRect().right - bar.getBoundingClientRect().left + MARGIN + ICON / 2, 0), level: levels.length - 1 };
|
|
67
|
+
bar.classList.toggle('search-icon', result.mode === 'icon');
|
|
68
|
+
if (result.mode === 'full') bar.style.setProperty('--top-bar-search-width', Math.round(result.width) + 'px');
|
|
69
|
+
else bar.style.setProperty('--top-bar-search-left', Math.round(result.left) + 'px');
|
|
70
|
+
applied = sizes(bar, left, actions);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function observeTopbarLayout() {
|
|
74
|
+
var bar = document.getElementById('top-bar');
|
|
75
|
+
if (!bar || typeof ResizeObserver === 'undefined') return;
|
|
76
|
+
var queued = false;
|
|
77
|
+
var run = function (force) { if (queued) return; queued = true; requestAnimationFrame(function () { queued = false; layoutTopbar(force === true); }); };
|
|
78
|
+
var observer = new ResizeObserver(function () { run(false); });
|
|
79
|
+
observer.observe(bar);
|
|
80
|
+
['.top-bar-left-pills', '.top-bar-actions'].forEach(function (selector) { var node = bar.querySelector(selector); if (node) observer.observe(node); });
|
|
81
|
+
// Content swaps that leave the applied size unchanged (a folded usage chip changing)
|
|
82
|
+
// still need a fresh decision; plain class toggles are caught by the size observers.
|
|
83
|
+
new MutationObserver(function () { run(true); }).observe(bar.querySelector('.top-bar-actions'), { childList: true, subtree: true, characterData: true });
|
|
84
|
+
layoutTopbar(true);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Mirror "title bar replaced by another view" on the top bar so CSS needs no body-level :has().
|
|
88
|
+
function mirrorTitleBarHidden() {
|
|
89
|
+
var title = document.querySelector('#main-column > .title-bar-content'), bar = document.getElementById('top-bar');
|
|
90
|
+
if (!title || !bar) return;
|
|
91
|
+
var sync = function () { bar.classList.toggle('title-bar-hidden', title.classList.contains('hidden')); };
|
|
92
|
+
sync();
|
|
93
|
+
new MutationObserver(sync).observe(title, { attributes: true, attributeFilter: ['class'] });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function initTopbarWorkbenchControls() {
|
|
97
|
+
var query = window.matchMedia(MOBILE);
|
|
98
|
+
place(query.matches);
|
|
99
|
+
query.addEventListener('change', function () { place(query.matches); });
|
|
100
|
+
mirrorTitleBarHidden();
|
|
101
|
+
observeTopbarLayout();
|
|
102
|
+
}
|
|
@@ -54,7 +54,8 @@ export function initPersonalIntegrations() {
|
|
|
54
54
|
if (!store.get('personalLinearDirty')) { action(); return; }
|
|
55
55
|
confirmSettingsAction('Discard this unsaved connection draft?', 'Discard', action);
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
// Only an open editor holds a draft; autofill or restore into the hidden form is not an edit.
|
|
58
|
+
form.oninput = function () { if (!store.get('personalLinearEditing')) return; store.set({personalLinearDirty: !!key.value || name.value !== store.get('personalLinearOriginalName')}); };
|
|
58
59
|
async function save(disconnect) {
|
|
59
60
|
if (store.get('personalLinearSaving')) return;
|
|
60
61
|
var actor = store.get('myUserId'), generation = store.get('personalLinearGeneration');
|