clay-server 5.2.0-beta.11 → 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/lib/public/app.js +2 -1
- package/lib/public/css/title-bar.css +4 -0
- package/lib/public/modules/account-transport-settings.js +8 -2
- package/lib/public/modules/admin-settings-email.js +7 -2
- package/lib/public/modules/home-surface-boot.js +0 -1
- package/lib/public/modules/pane-appearance-bridge.js +59 -0
- package/lib/public/modules/project-settings-environment.js +12 -1
- package/lib/public/modules/projects-hub-view.js +15 -46
- package/lib/public/modules/projects-hub.js +31 -24
- package/lib/public/modules/theme.js +21 -1
- package/lib/public/modules/user-settings-integrations.js +2 -1
- package/lib/public/modules/user-settings-layout.js +4 -1
- package/lib/public/modules/vendor-usage-overview.js +11 -0
- package/lib/public/style.css +1 -1
- package/lib/server.js +7 -0
- package/package.json +1 -1
package/lib/public/app.js
CHANGED
|
@@ -64,6 +64,7 @@ import { initProjectLogs, closeProjectLogs } from './modules/project-logs.js';
|
|
|
64
64
|
import { initProjectSwitcher } from './modules/project-switcher.js';
|
|
65
65
|
import { initSplitView } from './modules/split-view.js';
|
|
66
66
|
import { initPaneBridge } from './modules/pane-bridge.js';
|
|
67
|
+
import { initPaneAppearanceBridge } from './modules/pane-appearance-bridge.js';
|
|
67
68
|
import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
68
69
|
import { initSessionSearch, toggleSearch, closeSearch, isSearchOpen, handleFindInSessionResults, onHistoryPrepended as onSessionSearchHistoryPrepended } from './modules/session-search.js';
|
|
69
70
|
import { initTooltips, registerTooltip } from './modules/tooltip.js';
|
|
@@ -427,7 +428,6 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
427
428
|
projectListLoaded: false,
|
|
428
429
|
projectsAccessLoaded: false,
|
|
429
430
|
projectsHubRestoreRequested: false,
|
|
430
|
-
projectsShowGuide: false,
|
|
431
431
|
projectStart: initialProjectStart(),
|
|
432
432
|
projectStartFolder: emptyFolderState(),
|
|
433
433
|
onboardingStep: 'welcome',
|
|
@@ -709,6 +709,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
709
709
|
|
|
710
710
|
// --- Theme (module) ---
|
|
711
711
|
initTheme();
|
|
712
|
+
initPaneAppearanceBridge();
|
|
712
713
|
|
|
713
714
|
// --- Tooltips ---
|
|
714
715
|
initTooltips();
|
|
@@ -93,6 +93,10 @@
|
|
|
93
93
|
right: 10px;
|
|
94
94
|
top: 50%;
|
|
95
95
|
transform: translateY(-50%);
|
|
96
|
+
/* The transform makes this group its own stacking context, so its popovers
|
|
97
|
+
are ordered by this value. Sit above chat layers but below contextual Clay
|
|
98
|
+
(160), the mobile overlays and modals. */
|
|
99
|
+
z-index: 150;
|
|
96
100
|
display: flex;
|
|
97
101
|
align-items: center;
|
|
98
102
|
gap: 2px;
|
|
@@ -33,8 +33,14 @@ export function initAccountTransportSettings() {
|
|
|
33
33
|
'<div class="settings-form-actions"><button class="settings-btn-sm">Save connection settings</button></div></form><p class="settings-hint" role="status"></p>';
|
|
34
34
|
anchor.after(section);
|
|
35
35
|
section.querySelectorAll('[name=origin],[name=proxies]').forEach(function (input) { input.oninput = function () {
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// A draft is the form differing from the saved values, so a manual revert clears it.
|
|
37
|
+
var saved = store.get('accountTransport') || {};
|
|
38
|
+
var origin = section.querySelector('[name=origin]').value.trim();
|
|
39
|
+
var proxies = section.querySelector('[name=proxies]').value.split(/\s+/).filter(Boolean);
|
|
40
|
+
var dirty = origin !== (saved.origin || '') || proxies.join('\n') !== (saved.trustedProxies || []).join('\n');
|
|
41
|
+
var state = Object.assign({}, saved, { dirty: dirty, error: '' });
|
|
42
|
+
if (dirty) setSettingsDraft('accountTransport', true); else clearSettingsDraft('accountTransport');
|
|
43
|
+
store.set({ accountTransport: state }); render(state);
|
|
38
44
|
}; });
|
|
39
45
|
section.querySelector('form').onsubmit = async function (event) {
|
|
40
46
|
event.preventDefault(); var password = section.querySelector('[name=password]');
|
|
@@ -96,8 +96,13 @@ function renderSmtpTab(body, cfg) {
|
|
|
96
96
|
|
|
97
97
|
body.innerHTML = html;
|
|
98
98
|
clearSettingsDraft('smtp');
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
// The draft is the form differing from what was rendered, so reverting a field clears it.
|
|
100
|
+
var fields = Array.from(body.querySelectorAll('input, select, textarea'));
|
|
101
|
+
function fieldValue(field) { return field.type === 'checkbox' ? field.checked : field.value; }
|
|
102
|
+
var baseline = fields.map(fieldValue);
|
|
103
|
+
function syncDraft() { setSettingsDraft('smtp', fields.some(function (field, index) { return fieldValue(field) !== baseline[index]; })); }
|
|
104
|
+
body.oninput = syncDraft;
|
|
105
|
+
body.onchange = syncDraft;
|
|
101
106
|
refreshIcons(body);
|
|
102
107
|
|
|
103
108
|
var errorEl = body.querySelector("#smtp-error");
|
|
@@ -11,7 +11,6 @@ function explicitProjectPath(pathname) {
|
|
|
11
11
|
export function resolveHomeBootDestination(options) {
|
|
12
12
|
if (!options) return "wait";
|
|
13
13
|
if (options.paneMode || explicitProjectPath(options.pathname)) return "project";
|
|
14
|
-
if (/^\/projects\/?$/.test(options.pathname || "")) return "projects";
|
|
15
14
|
// The server-selected project embedded in the root shell is authoritative.
|
|
16
15
|
// Home is entered only through an explicit in-app action; reloading `/`
|
|
17
16
|
// returns to the project workspace even if Home was the last open surface.
|
|
@@ -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
|
+
}
|
|
@@ -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,4 +1,3 @@
|
|
|
1
|
-
import { escapeHtml } from './utils.js';
|
|
2
1
|
import { iconHtml } from './icons.js';
|
|
3
2
|
|
|
4
3
|
export function ordinaryProjects(projects) {
|
|
@@ -28,53 +27,23 @@ function welcomeCard(label, title, text, action, artwork) {
|
|
|
28
27
|
'<span class="projects-intro-label">' + label + '</span><h2>' + title + '</h2><p>' + text + '</p><div class="projects-intro-action">' + action + '</div></div>';
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
'<span class="projects-action-icon">' + iconHtml(icon) + '</span><span><strong>' + title + '</strong><span>' + text + '</span></span>' + iconHtml('arrow-up-right') + '</button>';
|
|
34
|
-
}
|
|
35
|
-
|
|
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.
|
|
36
32
|
export function projectsHubContent(state) {
|
|
37
33
|
var projects = ordinaryProjects(state.projectsHubList);
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
}
|
|
40
37
|
var allowed = state.projectsAccessLoaded && (!state.permissions || state.permissions.createProject === true);
|
|
41
38
|
var available = allowed && state.connected && !state.pendingHomeProjectSlug;
|
|
42
|
-
var
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
'
|
|
51
|
-
|
|
52
|
-
welcomeCard('01 / PROJECTS', 'Work together on a project.', 'Bring a codebase or a folder of files, and work through it together with Clay Studio.',
|
|
53
|
-
button(projects.length ? 'start' : 'create-project', projects.length ? 'Explore your projects ' + iconHtml('arrow-right') : 'Start a project ' + iconHtml('arrow-right'), 'projects-button-primary', !projects.length && !available), 'project-clay') +
|
|
54
|
-
welcomeCard('02 / MATES', 'Create your own specialist.', 'Create a Mate with the expertise you need, and work together on your ideas and tasks.',
|
|
55
|
-
button('mate', 'Create a Mate ' + iconHtml('arrow-right'), 'projects-button-secondary', !mateReady), 'mate-clay') +
|
|
56
|
-
'</div><p class="projects-footnote">You can revisit this guide from Projects at any time.</p></section></main>';
|
|
57
|
-
}
|
|
58
|
-
html += '<div class="projects-heading"><div><div class="projects-eyebrow">YOUR WORKSPACE</div><h1 id="projects-title" tabindex="-1">Projects</h1>' +
|
|
59
|
-
'<p class="projects-lead">Pick up where you left off, or begin something new.</p></div>' + button('guide', 'Getting started', 'projects-button-quiet') + '</div>';
|
|
60
|
-
if (state.pendingHomeProjectSlug) html += '<p class="projects-status" role="status">Opening your project…</p>';
|
|
61
|
-
if (projects.length) {
|
|
62
|
-
html += '<div class="projects-list" aria-label="Your projects">';
|
|
63
|
-
projects.forEach(function (project) {
|
|
64
|
-
html += '<button type="button" class="projects-list-item" data-project-slug="' + escapeHtml(project.slug) + '"' + (!state.connected || state.pendingHomeProjectSlug ? ' disabled' : '') + '>' +
|
|
65
|
-
'<span class="projects-action-icon">' + iconHtml(project.isWorktree ? 'git-branch' : 'folder') + '</span><span><strong>' + escapeHtml(project.title || project.project || project.slug) + '</strong>' +
|
|
66
|
-
'<span>' + escapeHtml(project.branch || project.path || project.slug) + '</span></span>' + iconHtml('arrow-right') + '</button>';
|
|
67
|
-
});
|
|
68
|
-
html += '</div><h2 class="projects-section-title">Add a project</h2>';
|
|
69
|
-
}
|
|
70
|
-
if (!state.projectsAccessLoaded) html += '<p class="projects-status" role="status">Checking project access…</p>';
|
|
71
|
-
else if (!allowed) html += '<p class="projects-status">Ask an administrator to create or share a project with you.</p>';
|
|
72
|
-
else {
|
|
73
|
-
html += '<div class="projects-setup-actions">' +
|
|
74
|
-
setupAction('existing', 'folder-open', 'Open a folder', 'Use a directory on the machine running Clay.', !available) +
|
|
75
|
-
setupAction('create', 'plus', 'Create a project', 'Start in a new, empty project directory.', !available) +
|
|
76
|
-
setupAction('clone', 'git-branch', 'Clone a repository', 'Bring an existing Git repository into Clay.', !available) + '</div>';
|
|
77
|
-
}
|
|
78
|
-
if (!state.connected) html += '<p class="projects-status" role="status">Reconnecting… Project actions will be available when the connection returns.</p>';
|
|
79
|
-
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>';
|
|
80
49
|
}
|
|
@@ -3,9 +3,8 @@ import { getWs } from './ws-ref.js';
|
|
|
3
3
|
import { switchProject } from './app-projects.js';
|
|
4
4
|
import { openAddProjectModal } from './project-start-dialog.js';
|
|
5
5
|
import { startClayMateCreation } from './project-mate-creation-workspace.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { projectsHubContent, isShellWelcome, clayMateReady } from './projects-hub-view.js';
|
|
6
|
+
import { updateHomeSurfacePreference } from './home-surface.js';
|
|
7
|
+
import { projectsHubContent, isShellWelcome, clayMateReady, ordinaryProjects } from './projects-hub-view.js';
|
|
9
8
|
import { refreshIcons } from './icons.js';
|
|
10
9
|
|
|
11
10
|
function root() { return document.getElementById('projects-hub'); }
|
|
@@ -18,13 +17,13 @@ function render() {
|
|
|
18
17
|
if (store.get('projectsHubMarkup') === html) return;
|
|
19
18
|
store.set({ projectsHubMarkup: html });
|
|
20
19
|
var active = element.contains(document.activeElement) ? document.activeElement : null;
|
|
21
|
-
var action = active && (active.dataset.projectsAction
|
|
20
|
+
var action = active && (active.dataset.projectsAction);
|
|
22
21
|
element.innerHTML = html;
|
|
23
22
|
refreshIcons();
|
|
24
23
|
if (action) {
|
|
25
24
|
var buttons = element.querySelectorAll('button, a');
|
|
26
25
|
for (var i = 0; i < buttons.length; i++) {
|
|
27
|
-
if (
|
|
26
|
+
if (buttons[i].dataset.projectsAction === action && !buttons[i].disabled) {
|
|
28
27
|
buttons[i].focus({ preventScroll: true });
|
|
29
28
|
break;
|
|
30
29
|
}
|
|
@@ -38,22 +37,38 @@ function focusHeading() {
|
|
|
38
37
|
if (title) title.focus({ preventScroll: true });
|
|
39
38
|
}
|
|
40
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.
|
|
41
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
|
+
}
|
|
42
61
|
store.set({ projectsHubVisible: true, pendingHomeProjectSlug: null });
|
|
43
62
|
document.body.classList.add('projects-active');
|
|
44
63
|
root().classList.remove('hidden');
|
|
45
|
-
|
|
46
|
-
if (!fromHistory && location.pathname !== '/projects') {
|
|
47
|
-
var method = document.documentElement.classList.contains('pwa-standalone') ? 'replaceState' : 'pushState';
|
|
48
|
-
history[method](null, '', '/projects');
|
|
49
|
-
}
|
|
64
|
+
if (location.pathname === '/projects' || location.pathname === '/projects/') history.replaceState(null, '', '/');
|
|
50
65
|
render();
|
|
51
66
|
focusHeading();
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
export function hideProjectsHub() {
|
|
55
70
|
if (!store.get('projectsHubVisible')) return;
|
|
56
|
-
store.set({ projectsHubVisible: false
|
|
71
|
+
store.set({ projectsHubVisible: false });
|
|
57
72
|
document.body.classList.remove('projects-active', 'projects-shell');
|
|
58
73
|
if (root()) root().classList.add('hidden');
|
|
59
74
|
}
|
|
@@ -64,26 +79,17 @@ export function completeProjectsOnboarding() {
|
|
|
64
79
|
}
|
|
65
80
|
|
|
66
81
|
function handleClick(event) {
|
|
67
|
-
var button = event.target.closest('[data-projects-action]
|
|
82
|
+
var button = event.target.closest('[data-projects-action]');
|
|
68
83
|
if (!button || button.disabled) return;
|
|
69
84
|
event.preventDefault();
|
|
70
85
|
var action = button.dataset.projectsAction;
|
|
71
|
-
if (action === 'settings') { openUserSettings(); return; }
|
|
72
|
-
if (action === 'guide') { store.set({ projectsShowGuide: true }); render(); focusHeading(); return; }
|
|
73
|
-
if (action === 'start') {
|
|
74
|
-
store.set({ projectsShowGuide: false });
|
|
75
|
-
updateHomeSurfacePreference({ onboardingStep: 'project' });
|
|
76
|
-
render(); focusHeading();
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
86
|
var ws = getWs();
|
|
80
87
|
if (!ws || ws.readyState !== 1) return;
|
|
81
88
|
if (action === 'mate') { if (clayMateReady(store.snap())) startClayMateCreation(); return; }
|
|
82
|
-
if (
|
|
83
|
-
if (action === 'create-project' || button.dataset.projectsMode) {
|
|
89
|
+
if (action === 'create-project') {
|
|
84
90
|
var permissions = store.get('permissions');
|
|
85
91
|
if (!store.get('projectsAccessLoaded') || store.get('pendingHomeProjectSlug') || (permissions && permissions.createProject !== true)) return;
|
|
86
|
-
openAddProjectModal(
|
|
92
|
+
openAddProjectModal();
|
|
87
93
|
}
|
|
88
94
|
}
|
|
89
95
|
|
|
@@ -92,11 +98,12 @@ export function initProjectsHub() {
|
|
|
92
98
|
store.subscribe(function (state, previous) {
|
|
93
99
|
if (state.projectsHubRestoreRequested && !previous.projectsHubRestoreRequested) {
|
|
94
100
|
showProjectsHub(true);
|
|
95
|
-
if (location.pathname === '/') history.replaceState(null, '', '/projects');
|
|
96
101
|
store.set({ projectsHubRestoreRequested: false });
|
|
97
102
|
return;
|
|
98
103
|
}
|
|
99
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; }
|
|
100
107
|
if (state.projectsHubList !== previous.projectsHubList || state.projectListLoaded !== previous.projectListLoaded ||
|
|
101
108
|
state.homeSurfaceLoaded !== previous.homeSurfaceLoaded || state.onboardingStep !== previous.onboardingStep ||
|
|
102
109
|
state.projectsAccessLoaded !== previous.projectsAccessLoaded || state.permissions !== previous.permissions ||
|
|
@@ -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;
|
|
@@ -719,6 +737,8 @@ async function loadAppearance() {
|
|
|
719
737
|
if (!response.ok) throw new Error('Appearance preferences unavailable.');
|
|
720
738
|
var data = await response.json();
|
|
721
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;
|
|
722
742
|
if (data.configured) {store.set({appearancePreference:data});applyTheme(themeIdForMode(data.mode || getSystemMode()));}
|
|
723
743
|
else {
|
|
724
744
|
var legacy = null;try {legacy = localStorage.getItem(STORAGE_KEY);}catch(error){}
|
|
@@ -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');
|
|
@@ -52,8 +52,11 @@ export function personalLeavePrompt(pageId) {
|
|
|
52
52
|
function leaving(id) {return !pageId || pageId === id;}
|
|
53
53
|
var pin = document.getElementById('us-pin-form');
|
|
54
54
|
var pinDirty = pin && !pin.classList.contains('hidden') && Array.from(pin.querySelectorAll('input')).some(function (input) {return !!input.value;});
|
|
55
|
+
// A value left in the hidden key field (browser autofill or form restore) is
|
|
56
|
+
// not an edit; only an open connection editor can hold a draft key.
|
|
55
57
|
var key = document.getElementById('us-linear-key');
|
|
56
|
-
var
|
|
58
|
+
var keyDraft = key && key.value && store.get('personalLinearEditing') && !(key.form && key.form.hidden);
|
|
59
|
+
var dirty = leaving('us-integrations') && (store.get('personalEmailDirty') || store.get('personalLinearDirty') || keyDraft) ||
|
|
57
60
|
leaving('us-chat') && store.get('userVoiceDirty') ||
|
|
58
61
|
leaving('us-notifications') && store.get('notificationDraft') ||
|
|
59
62
|
leaving('us-appearance') && (store.get('appearanceDraft') || store.get('userTerminalDirty')) ||
|
|
@@ -17,6 +17,7 @@ var records = {};
|
|
|
17
17
|
var tickTimer = null;
|
|
18
18
|
var outsideHandler = null;
|
|
19
19
|
var keyHandler = null;
|
|
20
|
+
var blurHandler = null;
|
|
20
21
|
var requestCounter = 0;
|
|
21
22
|
var pendingRequestId = null;
|
|
22
23
|
|
|
@@ -135,6 +136,8 @@ function placePopover(pop) {
|
|
|
135
136
|
if (shift) pop.style.marginRight = (-shift) + "px";
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
function shellWindow() { return typeof window !== "undefined" ? window : null; }
|
|
140
|
+
|
|
138
141
|
function bindDismissal(open) {
|
|
139
142
|
if (open && !outsideHandler) {
|
|
140
143
|
outsideHandler = function (event) {
|
|
@@ -143,13 +146,21 @@ function bindDismissal(open) {
|
|
|
143
146
|
keyHandler = function (event) {
|
|
144
147
|
if (event.key === "Escape") { closePopover(true); }
|
|
145
148
|
};
|
|
149
|
+
// A click inside a split pane or contextual iframe never reaches this
|
|
150
|
+
// document; focus moving into the frame blurs the shell window instead.
|
|
151
|
+
blurHandler = function () {
|
|
152
|
+
if (document.activeElement && document.activeElement.tagName === "IFRAME") closePopover(false);
|
|
153
|
+
};
|
|
146
154
|
document.addEventListener("pointerdown", outsideHandler, true);
|
|
147
155
|
document.addEventListener("keydown", keyHandler, true);
|
|
156
|
+
if (shellWindow()) shellWindow().addEventListener("blur", blurHandler);
|
|
148
157
|
} else if (!open && outsideHandler) {
|
|
149
158
|
document.removeEventListener("pointerdown", outsideHandler, true);
|
|
150
159
|
document.removeEventListener("keydown", keyHandler, true);
|
|
160
|
+
if (blurHandler && shellWindow()) shellWindow().removeEventListener("blur", blurHandler);
|
|
151
161
|
outsideHandler = null;
|
|
152
162
|
keyHandler = null;
|
|
163
|
+
blurHandler = null;
|
|
153
164
|
}
|
|
154
165
|
}
|
|
155
166
|
|
package/lib/public/style.css
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
@import url("css/linear.css");
|
|
7
7
|
@import url("css/base.css?v=20260828-type1");
|
|
8
8
|
@import url("css/icon-strip.css?v=20260828-surfaces4");
|
|
9
|
-
@import url("css/title-bar.css?v=
|
|
9
|
+
@import url("css/title-bar.css?v=20261018-actions-layer");
|
|
10
10
|
@import url("css/clay-workspace.css?v=20261010-tint");
|
|
11
11
|
@import url("css/pane.css");
|
|
12
12
|
@import url("css/worker-proposal.css");
|
package/lib/server.js
CHANGED
|
@@ -970,6 +970,13 @@ function createServer(opts) {
|
|
|
970
970
|
res.end(auth.getAuthPage());
|
|
971
971
|
return;
|
|
972
972
|
}
|
|
973
|
+
// The standalone project list was removed. A legacy /projects link resolves
|
|
974
|
+
// through the same server-backed workspace selection as the root path.
|
|
975
|
+
if (fullUrl !== "/") {
|
|
976
|
+
res.writeHead(302, { "Location": "/", "Cache-Control": "no-store" });
|
|
977
|
+
res.end();
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
973
980
|
if (projects.size > 0) {
|
|
974
981
|
var targetSlug = null;
|
|
975
982
|
var reqUser = users.isMultiUser() ? getMultiUserFromReq(req) : null;
|
package/package.json
CHANGED