clay-server 5.1.0-beta.2 → 6.0.0-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/bin/cli.js +9 -91
- package/lib/daemon-settings.js +11 -0
- package/lib/daemon.js +8 -37
- package/lib/linear-store.js +35 -8
- package/lib/os-users.js +5 -16
- package/lib/project-connection.js +0 -7
- package/lib/project-filesystem.js +21 -39
- package/lib/project-mate-interaction.js +2 -2
- package/lib/project-server-settings.js +31 -0
- package/lib/project-sessions.js +57 -184
- package/lib/public/app.js +6 -37
- package/lib/public/css/input.css +3 -1
- package/lib/public/css/project-settings-modal.css +56 -0
- package/lib/public/css/server-settings-system.css +125 -0
- package/lib/public/css/session-github.css +25 -16
- package/lib/public/css/settings-ui.css +247 -22
- package/lib/public/css/shared-browser.css +2 -0
- package/lib/public/css/title-bar.css +5 -1
- package/lib/public/index.html +170 -446
- package/lib/public/modules/account-island.js +9 -7
- package/lib/public/modules/admin-settings-email.js +197 -0
- package/lib/public/modules/admin-settings-invitations.js +183 -0
- package/lib/public/modules/admin-settings-projects.js +198 -0
- package/lib/public/modules/admin-settings-ui.js +100 -0
- package/lib/public/modules/admin-settings-users.js +331 -0
- package/lib/public/modules/admin.js +19 -949
- package/lib/public/modules/app-dm.js +0 -2
- package/lib/public/modules/app-messages.js +13 -52
- package/lib/public/modules/app-rate-limit.js +70 -12
- package/lib/public/modules/context-sources.js +27 -18
- package/lib/public/modules/default-ai.js +14 -7
- package/lib/public/modules/header-tui-font.js +10 -20
- package/lib/public/modules/linear-settings.js +3 -2
- package/lib/public/modules/markdown.js +5 -6
- package/lib/public/modules/notification-settings.js +81 -0
- package/lib/public/modules/notifications.js +44 -28
- package/lib/public/modules/personal-settings-dialog.js +51 -0
- package/lib/public/modules/profile.js +15 -6
- package/lib/public/modules/project-settings-environment.js +106 -0
- package/lib/public/modules/project-settings.js +72 -628
- package/lib/public/modules/server-settings-controls.js +316 -0
- package/lib/public/modules/server-settings-environment.js +194 -0
- package/lib/public/modules/server-settings-navigation.js +39 -0
- package/lib/public/modules/server-settings-state.js +54 -0
- package/lib/public/modules/server-settings.js +116 -788
- package/lib/public/modules/server-storage-settings.js +84 -0
- package/lib/public/modules/session-github.js +2 -1
- package/lib/public/modules/session-presence-ui.js +1 -1
- package/lib/public/modules/settings-env-values.js +34 -0
- package/lib/public/modules/settings-select.js +121 -0
- package/lib/public/modules/shared-browser.js +10 -7
- package/lib/public/modules/stt-picker.js +14 -5
- package/lib/public/modules/terminal-prefs.js +22 -0
- package/lib/public/modules/theme.js +88 -52
- package/lib/public/modules/user-settings-account.js +318 -0
- package/lib/public/modules/user-settings-appearance.js +55 -0
- package/lib/public/modules/user-settings-editors.js +24 -0
- package/lib/public/modules/user-settings-integrations.js +104 -0
- package/lib/public/modules/user-settings-layout.js +80 -0
- package/lib/public/modules/user-settings-preferences.js +48 -0
- package/lib/public/modules/user-settings-profile.js +53 -0
- package/lib/public/modules/user-settings-themes.js +38 -0
- package/lib/public/modules/user-settings-voice.js +75 -0
- package/lib/public/modules/user-settings.js +53 -480
- package/lib/public/style.css +4 -0
- package/lib/sdk-message-processor.js +16 -4
- package/lib/sdk-quota-snapshot.js +37 -0
- package/lib/server-appearance-settings.js +71 -0
- package/lib/server-linear.js +14 -8
- package/lib/server-notification-settings.js +67 -0
- package/lib/server-settings.js +4 -0
- package/lib/server-storage.js +56 -0
- package/lib/server.js +10 -2
- package/lib/sessions.js +2 -3
- package/lib/startup-projects.js +40 -0
- package/lib/storage-usage.js +127 -0
- package/lib/users-preferences.js +0 -74
- package/lib/users.js +0 -4
- package/lib/ws-schema.js +2 -4
- package/lib/yoke/adapters/codex.js +8 -45
- package/lib/yoke/codex-model-catalog.js +40 -0
- package/lib/yoke/codex-rate-limits.js +26 -0
- package/package.json +3 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { initNotificationSettings, saveNotificationPreference, updatePushSettings } from './notification-settings.js';
|
|
1
2
|
import { initExtensionSettings } from './extension-settings.js';
|
|
2
3
|
import { copyToClipboard, showToast } from './utils.js';
|
|
3
4
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
@@ -193,9 +194,8 @@ export function initNotifications(_ctx) {
|
|
|
193
194
|
if (settingsUpdateCheck) {
|
|
194
195
|
settingsUpdateCheck.addEventListener("click", function (e) {
|
|
195
196
|
e.stopPropagation();
|
|
196
|
-
if (ctx.ws
|
|
197
|
-
|
|
198
|
-
}
|
|
197
|
+
if (!ctx.ws || ctx.ws.readyState !== 1) { showToast("Reconnect before checking for updates."); return; }
|
|
198
|
+
ctx.ws.send(JSON.stringify({ type: "check_update" }));
|
|
199
199
|
setUpdateBtn("Checking…", true, true);
|
|
200
200
|
});
|
|
201
201
|
}
|
|
@@ -222,10 +222,14 @@ export function initNotifications(_ctx) {
|
|
|
222
222
|
})();
|
|
223
223
|
var isStandalone = window.matchMedia("(display-mode:standalone)").matches || navigator.standalone;
|
|
224
224
|
|
|
225
|
+
window.addEventListener('clay:notification-preferences', function (event) {
|
|
226
|
+
notifAlertEnabled = event.detail.alert;
|
|
227
|
+
notifSoundEnabled = event.detail.sound;
|
|
228
|
+
});
|
|
225
229
|
// --- Browser notifications ---
|
|
226
230
|
notifPermission = ("Notification" in window) ? Notification.permission : "denied";
|
|
227
|
-
notifAlertEnabled =
|
|
228
|
-
notifSoundEnabled =
|
|
231
|
+
notifAlertEnabled = true;
|
|
232
|
+
notifSoundEnabled = true;
|
|
229
233
|
|
|
230
234
|
var notifBtn = $("notif-btn");
|
|
231
235
|
var notifMenu = $("notif-menu");
|
|
@@ -234,7 +238,7 @@ export function initNotifications(_ctx) {
|
|
|
234
238
|
|
|
235
239
|
if (notifAlertEnabled && "Notification" in window && Notification.permission === "denied") {
|
|
236
240
|
notifAlertEnabled = false;
|
|
237
|
-
|
|
241
|
+
|
|
238
242
|
}
|
|
239
243
|
notifToggleAlert.checked = notifAlertEnabled;
|
|
240
244
|
notifToggleSound.checked = notifSoundEnabled;
|
|
@@ -256,23 +260,24 @@ export function initNotifications(_ctx) {
|
|
|
256
260
|
|
|
257
261
|
notifToggleAlert.addEventListener("change", function () {
|
|
258
262
|
notifAlertEnabled = notifToggleAlert.checked;
|
|
259
|
-
|
|
263
|
+
saveNotificationPreference("alert", notifAlertEnabled);
|
|
260
264
|
notifBlockedHint.classList.add("hidden");
|
|
261
265
|
if (notifAlertEnabled && notifPermission !== "granted") {
|
|
262
266
|
if ("Notification" in window && Notification.permission === "denied") {
|
|
263
267
|
notifAlertEnabled = false;
|
|
264
268
|
notifToggleAlert.checked = false;
|
|
265
|
-
|
|
269
|
+
|
|
266
270
|
notifBlockedHint.classList.remove("hidden");
|
|
267
271
|
refreshIcons();
|
|
268
272
|
return;
|
|
269
273
|
}
|
|
274
|
+
if (!("Notification" in window)) { notifToggleAlert.checked = false; return; }
|
|
270
275
|
Notification.requestPermission().then(function (p) {
|
|
271
276
|
notifPermission = p;
|
|
272
277
|
if (p !== "granted") {
|
|
273
278
|
notifAlertEnabled = false;
|
|
274
279
|
notifToggleAlert.checked = false;
|
|
275
|
-
|
|
280
|
+
|
|
276
281
|
notifBlockedHint.classList.remove("hidden");
|
|
277
282
|
refreshIcons();
|
|
278
283
|
}
|
|
@@ -327,7 +332,7 @@ export function initNotifications(_ctx) {
|
|
|
327
332
|
|
|
328
333
|
notifToggleSound.addEventListener("change", function () {
|
|
329
334
|
notifSoundEnabled = notifToggleSound.checked;
|
|
330
|
-
|
|
335
|
+
saveNotificationPreference("sound", notifSoundEnabled);
|
|
331
336
|
});
|
|
332
337
|
|
|
333
338
|
// --- Push notifications toggle ---
|
|
@@ -376,14 +381,18 @@ export function initNotifications(_ctx) {
|
|
|
376
381
|
if (prevEndpoint && prevEndpoint !== sub.endpoint) {
|
|
377
382
|
payload.replaceEndpoint = prevEndpoint;
|
|
378
383
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
384
|
+
updatePushSettings(true, 'Registering push on this device…', true);
|
|
385
|
+
return fetch('/api/push-subscribe', {
|
|
386
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
387
|
+
credentials: 'same-origin', body: JSON.stringify(payload)
|
|
388
|
+
}).then(function (response) {
|
|
389
|
+
if (!response.ok) throw new Error('Registration failed');
|
|
390
|
+
updatePushSettings(true, 'Push enabled on this device.', false);
|
|
391
|
+
return true;
|
|
392
|
+
}).catch(function () {
|
|
393
|
+
updatePushSettings(false, 'Could not register push with Clay. Enable it again to retry.', false);
|
|
394
|
+
return false;
|
|
395
|
+
});
|
|
387
396
|
}
|
|
388
397
|
|
|
389
398
|
function subscribePush() {
|
|
@@ -398,8 +407,9 @@ export function initNotifications(_ctx) {
|
|
|
398
407
|
return reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key });
|
|
399
408
|
});
|
|
400
409
|
}).then(function (sub) {
|
|
401
|
-
sendPushSubscription(sub);
|
|
402
|
-
|
|
410
|
+
return sendPushSubscription(sub);
|
|
411
|
+
}).then(function (registered) {
|
|
412
|
+
if (!registered) return;
|
|
403
413
|
hideOnboarding();
|
|
404
414
|
localStorage.setItem("onboarding-dismissed", "1");
|
|
405
415
|
// Show a welcome notification so the user knows it works
|
|
@@ -411,18 +421,19 @@ export function initNotifications(_ctx) {
|
|
|
411
421
|
}).catch(function () {});
|
|
412
422
|
}).catch(function () {
|
|
413
423
|
notifTogglePush.checked = false;
|
|
414
|
-
|
|
424
|
+
updatePushSettings(false, "Push is off on this device. Enable it here to retry.");
|
|
415
425
|
notifBlockedHint.classList.remove("hidden");
|
|
416
426
|
refreshIcons();
|
|
417
427
|
});
|
|
418
428
|
}
|
|
419
429
|
|
|
420
430
|
function unsubscribePush() {
|
|
421
|
-
|
|
422
|
-
|
|
431
|
+
var subscription = window._pushSubscription;
|
|
432
|
+
updatePushSettings(false, 'Disabling push on this device…', true);
|
|
433
|
+
Promise.resolve(subscription ? subscription.unsubscribe() : true).then(function () {
|
|
423
434
|
window._pushSubscription = null;
|
|
424
|
-
|
|
425
|
-
|
|
435
|
+
updatePushSettings(false, 'Push is off on this device.', false);
|
|
436
|
+
}).catch(function () { notifTogglePush.checked = true; updatePushSettings(true, 'Could not disable push. Retry on this device.', false); });
|
|
426
437
|
}
|
|
427
438
|
|
|
428
439
|
notifTogglePush.addEventListener("change", function () {
|
|
@@ -434,6 +445,8 @@ export function initNotifications(_ctx) {
|
|
|
434
445
|
}
|
|
435
446
|
});
|
|
436
447
|
|
|
448
|
+
initNotificationSettings();
|
|
449
|
+
|
|
437
450
|
// --- Service Worker registration & push state sync ---
|
|
438
451
|
(function initServiceWorker() {
|
|
439
452
|
if (!("serviceWorker" in navigator)) return;
|
|
@@ -464,6 +477,7 @@ export function initNotifications(_ctx) {
|
|
|
464
477
|
|
|
465
478
|
return Promise.all([reg.pushManager.getSubscription(), vapidPromise]).then(function (results) {
|
|
466
479
|
var sub = results[0];
|
|
480
|
+
var hadSubscription = !!sub;
|
|
467
481
|
var serverKey = results[1];
|
|
468
482
|
|
|
469
483
|
// If subscription exists but VAPID key changed, unsubscribe and re-subscribe
|
|
@@ -479,9 +493,10 @@ export function initNotifications(_ctx) {
|
|
|
479
493
|
if (sub) {
|
|
480
494
|
window._pushSubscription = sub;
|
|
481
495
|
notifTogglePush.checked = true;
|
|
496
|
+
updatePushSettings(true);
|
|
482
497
|
sendPushSubscription(sub);
|
|
483
498
|
hideOnboarding();
|
|
484
|
-
} else if (serverKey &&
|
|
499
|
+
} else if (serverKey && hadSubscription) {
|
|
485
500
|
// Had push enabled but subscription is gone (VAPID key change), re-subscribe
|
|
486
501
|
var raw = atob(serverKey.replace(/-/g, "+").replace(/_/g, "/"));
|
|
487
502
|
var key = new Uint8Array(raw.length);
|
|
@@ -490,13 +505,14 @@ export function initNotifications(_ctx) {
|
|
|
490
505
|
.then(function (newSub) {
|
|
491
506
|
sendPushSubscription(newSub);
|
|
492
507
|
notifTogglePush.checked = true;
|
|
508
|
+
updatePushSettings(true);
|
|
493
509
|
}).catch(function () {
|
|
494
510
|
notifTogglePush.checked = false;
|
|
495
|
-
|
|
511
|
+
updatePushSettings(false, "Push is off on this device. Enable it here to retry.");
|
|
496
512
|
});
|
|
497
513
|
} else {
|
|
498
514
|
notifTogglePush.checked = false;
|
|
499
|
-
|
|
515
|
+
updatePushSettings(false, "Push is off on this device. Enable it here to retry.");
|
|
500
516
|
// Standalone (PWA) without push: redirect to setup for push onboarding
|
|
501
517
|
// Skip if setup was just completed (setup-done flag)
|
|
502
518
|
var isStandalone = window.matchMedia("(display-mode:standalone)").matches || navigator.standalone;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { confirmSettingsAction, isolateSettingsDialog, trapSettingsFocus } from './server-settings-state.js';
|
|
3
|
+
import { enhanceSettingsSelects } from './settings-select.js';
|
|
4
|
+
|
|
5
|
+
// Keep contextual connection forms transactional when opened from settings.
|
|
6
|
+
export function preparePersonalConnectionDialog(overlay, inline) {
|
|
7
|
+
var previous = document.activeElement;
|
|
8
|
+
var restore = inline ? function () {} : isolateSettingsDialog(overlay);
|
|
9
|
+
var dirty = false;
|
|
10
|
+
var dialog = overlay.querySelector('.email-setup-dialog');
|
|
11
|
+
dialog.setAttribute('role', inline ? 'region' : 'dialog');
|
|
12
|
+
if (!inline) dialog.setAttribute('aria-modal', 'true');
|
|
13
|
+
dialog.setAttribute('aria-label', 'Add personal mailbox');
|
|
14
|
+
overlay.querySelectorAll('.email-setup-field').forEach(function (field) {
|
|
15
|
+
var label = field.querySelector('label');
|
|
16
|
+
var input = field.querySelector('input,select');
|
|
17
|
+
if (label && input) label.htmlFor = input.id;
|
|
18
|
+
});
|
|
19
|
+
enhanceSettingsSelects(overlay);
|
|
20
|
+
function markDirty() { dirty = true; if (inline) store.set({personalEmailDirty:true}); }
|
|
21
|
+
overlay.addEventListener('input', markDirty);
|
|
22
|
+
overlay.addEventListener('change', markDirty);
|
|
23
|
+
var unsubscribe = inline ? store.subscribe(function (state, previousState) {
|
|
24
|
+
if (state.myUserId !== previousState.myUserId) close();
|
|
25
|
+
}) : function () {};
|
|
26
|
+
function cleanup() { observer.disconnect(); unsubscribe(); restore(); if (inline) store.set({personalEmailDirty:false}); }
|
|
27
|
+
function close() {
|
|
28
|
+
cleanup();overlay.remove();
|
|
29
|
+
if (previous && previous.isConnected) previous.focus();
|
|
30
|
+
}
|
|
31
|
+
function leave() {
|
|
32
|
+
if (dirty) confirmSettingsAction('Discard this unsaved mailbox connection?', 'Discard', close);
|
|
33
|
+
else close();
|
|
34
|
+
}
|
|
35
|
+
overlay.addEventListener('click', function (event) {
|
|
36
|
+
if (!inline && event.target === overlay || event.target.closest('#email-setup-cancel')) {
|
|
37
|
+
event.preventDefault();event.stopImmediatePropagation();leave();
|
|
38
|
+
}
|
|
39
|
+
}, true);
|
|
40
|
+
overlay.addEventListener('keydown', function (event) {
|
|
41
|
+
if (!inline || event.key === 'Escape') event.stopPropagation();
|
|
42
|
+
if (event.key === 'Escape' && !event.defaultPrevented) {event.preventDefault();leave();}
|
|
43
|
+
if (!inline) trapSettingsFocus(event, overlay);
|
|
44
|
+
});
|
|
45
|
+
var observer = new MutationObserver(function () {
|
|
46
|
+
if (!overlay.isConnected) {cleanup();if(previous && previous.isConnected)previous.focus();}
|
|
47
|
+
});
|
|
48
|
+
observer.observe(inline ? overlay.parentElement : document.body,{childList:true});
|
|
49
|
+
(overlay.querySelector('#email-setup-address') || overlay.querySelector('button')).focus();
|
|
50
|
+
return close;
|
|
51
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setChatLayout } from './theme.js';
|
|
1
2
|
// User profile module — profile popovers for people and Mates
|
|
2
3
|
// Stores profile server-side in ~/.clay/profile.json
|
|
3
4
|
// Generated identities use deterministic local pixel identicons.
|
|
@@ -7,7 +8,6 @@ import { avatarUrl, mateAvatarUrl, mateMarkUrl, userAvatarSeed } from './avatar.
|
|
|
7
8
|
import { store } from './store.js';
|
|
8
9
|
import { applyTerminalFont } from './terminal-prefs.js';
|
|
9
10
|
|
|
10
|
-
var ctx;
|
|
11
11
|
var profile = { name: '', lang: 'en-US', avatarStyle: 'imprint', avatarSeed: '', avatarColor: '#5857fc', avatarCustom: '' };
|
|
12
12
|
var profileUsername = '';
|
|
13
13
|
var popoverEl = null;
|
|
@@ -87,6 +87,16 @@ export function applyToIsland() {
|
|
|
87
87
|
document.dispatchEvent(new CustomEvent('clay:profile-updated'));
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
export async function saveProfileUpdates(updates) {
|
|
91
|
+
var next = Object.assign({}, profile, updates);
|
|
92
|
+
var response = await fetch('/api/profile', {method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(next)});
|
|
93
|
+
var result = await response.json();
|
|
94
|
+
if (!response.ok || result.error) throw new Error(result.error || 'Could not save your profile. Your draft is retained.');
|
|
95
|
+
profile = next;
|
|
96
|
+
applyToIsland();
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
90
100
|
export function updateProfileName(name) {
|
|
91
101
|
profile.name = String(name || '').trim();
|
|
92
102
|
applyToIsland();
|
|
@@ -528,8 +538,7 @@ function escapeAttr(str) {
|
|
|
528
538
|
}
|
|
529
539
|
|
|
530
540
|
// --- Init ---
|
|
531
|
-
export function initProfile(
|
|
532
|
-
ctx = _ctx;
|
|
541
|
+
export function initProfile() {
|
|
533
542
|
|
|
534
543
|
var island = document.getElementById('user-island');
|
|
535
544
|
if (!island) return;
|
|
@@ -556,13 +565,13 @@ export function initProfile(_ctx) {
|
|
|
556
565
|
// when the user opened the Settings modal (user-settings.js's
|
|
557
566
|
// populateAccount), so a hard refresh would drop persisted picks back
|
|
558
567
|
// to defaults until the modal was visited.
|
|
568
|
+
if(data.chatLayout)setChatLayout(data.chatLayout);
|
|
559
569
|
if (data.terminalFont && typeof data.terminalFont === "object") {
|
|
560
570
|
applyTerminalFont(data.terminalFont.family, data.terminalFont.size);
|
|
561
571
|
}
|
|
562
572
|
|
|
563
|
-
//
|
|
564
|
-
|
|
565
|
-
var matesOn = data.matesEnabled === true;
|
|
573
|
+
// Keep project Mate DM shortcuts available for legacy profiles too.
|
|
574
|
+
var matesOn = true;
|
|
566
575
|
store.set({ projectMateDmsEnabled: matesOn });
|
|
567
576
|
if (document.body) document.body.classList.toggle('project-mate-dms-disabled', !matesOn);
|
|
568
577
|
}).catch(function(err) {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { refreshIcons } from './icons.js';
|
|
2
|
+
import { store } from './store.js';
|
|
3
|
+
import { getWs } from './ws-ref.js';
|
|
4
|
+
import { parseEnvString, serializeEnvValue, looksLikeEnv } from './settings-env-values.js';
|
|
5
|
+
|
|
6
|
+
function state() { return store.get('projectEnvironmentEditor') || {}; }
|
|
7
|
+
function update(values) { store.set({ projectEnvironmentEditor: Object.assign({}, state(), values) }); }
|
|
8
|
+
function status(text) { document.getElementById('ps-env-save-status').textContent = text; }
|
|
9
|
+
function send(message) {
|
|
10
|
+
var ws = getWs();
|
|
11
|
+
if (!ws || ws.readyState !== 1) { status('Disconnected. Your draft is kept. Reconnect and try again.'); return false; }
|
|
12
|
+
ws.send(JSON.stringify(message)); return true;
|
|
13
|
+
}
|
|
14
|
+
export function projectEnvironmentDirty() { return state().slug === store.get("projectSettingsSlug") && !!(state().dirty || state().saving); }
|
|
15
|
+
export function openProjectEnvironment(slug) {
|
|
16
|
+
if (state().slug === slug && (state().loaded || state().loading || state().dirty)) return;
|
|
17
|
+
var cached = Object.assign({}, store.get('projectEnvironmentDrafts') || {});
|
|
18
|
+
if (state().slug && state().dirty) cached[state().slug] = state();
|
|
19
|
+
store.set({ projectEnvironmentDrafts: cached });
|
|
20
|
+
if (cached[slug]) { store.set({ projectEnvironmentEditor: cached[slug] }); render(); status('Unsaved changes'); return; }
|
|
21
|
+
update({ slug: slug, saved: '', rows: [], dirty: false, loaded: false, loading: false, editing: false, saving: false });
|
|
22
|
+
render(); status('Loading…');
|
|
23
|
+
var requestId = crypto.randomUUID();
|
|
24
|
+
update({ requestId: requestId });
|
|
25
|
+
if (send({ type: 'get_project_env', slug: slug, requestId: requestId })) update({ loading: true });
|
|
26
|
+
}
|
|
27
|
+
export function handleProjectEnv(msg) {
|
|
28
|
+
if (!state().loading || (msg.slug !== state().slug || msg.requestId !== state().requestId)) return;
|
|
29
|
+
update({ loading: false });
|
|
30
|
+
if (msg.error) { status(msg.error); return; }
|
|
31
|
+
update({ loaded: true, saved: msg.envrc || '', rows: parseEnvString(msg.envrc || '') });
|
|
32
|
+
render(); status('');
|
|
33
|
+
}
|
|
34
|
+
function changeRows(rows) { update({ rows: rows, dirty: true }); document.getElementById('ps-env-save').disabled = false; status('Unsaved changes'); }
|
|
35
|
+
function render() {
|
|
36
|
+
var editor = state();
|
|
37
|
+
var list = document.getElementById('ps-env-list'); list.replaceChildren();
|
|
38
|
+
(editor.rows || []).forEach(function (entry, index) {
|
|
39
|
+
var row = document.createElement('div'); row.className = 'ps-env-row';
|
|
40
|
+
var key = document.createElement('input'); key.className = 'ps-env-key'; key.value = entry.key; key.placeholder = 'KEY'; key.setAttribute('aria-label', 'Variable name');
|
|
41
|
+
var value = document.createElement('input'); value.className = 'ps-env-val'; value.type = 'password'; value.value = entry.value; value.placeholder = 'Value'; value.setAttribute('aria-label', 'Variable value');
|
|
42
|
+
[key, value].forEach(function (input) { input.autocomplete = 'off'; input.spellcheck = false; input.disabled = !!editor.saving; });
|
|
43
|
+
function changed() {
|
|
44
|
+
var rows = state().rows.slice(); rows[index] = { key: key.value, value: value.value }; changeRows(rows);
|
|
45
|
+
}
|
|
46
|
+
key.addEventListener('input', changed); value.addEventListener('input', changed);
|
|
47
|
+
key.addEventListener('paste', function (event) {
|
|
48
|
+
var text = event.clipboardData.getData('text');
|
|
49
|
+
if (!looksLikeEnv(text)) return;
|
|
50
|
+
event.preventDefault(); var rows = state().rows.slice();
|
|
51
|
+
rows.splice.apply(rows, [index, 1].concat(parseEnvString(text))); changeRows(rows); render();
|
|
52
|
+
});
|
|
53
|
+
var remove = document.createElement('button'); remove.className = 'ps-env-del'; remove.textContent = '×'; remove.setAttribute('aria-label', 'Remove variable'); remove.disabled = !!editor.saving;
|
|
54
|
+
remove.onclick = function () { var rows = state().rows.slice(); rows.splice(index, 1); changeRows(rows); render(); };
|
|
55
|
+
remove.innerHTML = '<i data-lucide="x"></i>';
|
|
56
|
+
var reveal = document.createElement('button'); reveal.type = 'button'; reveal.className = 'settings-btn-sm'; reveal.textContent = 'Show';
|
|
57
|
+
reveal.setAttribute('aria-label', 'Show variable value'); reveal.setAttribute('aria-pressed', 'false');
|
|
58
|
+
reveal.onclick = function () { var visible = value.type === 'password'; value.type = visible ? 'text' : 'password'; reveal.textContent = visible ? 'Hide' : 'Show'; reveal.setAttribute('aria-pressed', String(visible)); };
|
|
59
|
+
row.append(key, value, reveal, remove); list.appendChild(row);
|
|
60
|
+
});
|
|
61
|
+
document.getElementById('ps-env-add-btn').disabled = !editor.loaded || !!editor.saving;
|
|
62
|
+
document.getElementById('ps-env-save').disabled = !editor.loaded || !editor.dirty || !!editor.saving;
|
|
63
|
+
refreshIcons();
|
|
64
|
+
}
|
|
65
|
+
function clearCachedDraft() {
|
|
66
|
+
var cached = Object.assign({}, store.get('projectEnvironmentDrafts') || {}); delete cached[state().slug]; store.set({ projectEnvironmentDrafts: cached });
|
|
67
|
+
}
|
|
68
|
+
export function discardProjectEnvironment() {
|
|
69
|
+
clearCachedDraft();
|
|
70
|
+
update({ rows: parseEnvString(state().saved || ''), editing: false, dirty: false }); render(); status(''); document.getElementById('ps-env-add-btn').focus();
|
|
71
|
+
}
|
|
72
|
+
export function handleProjectEnvSaved(msg) {
|
|
73
|
+
if (!state().saving || (msg.slug !== state().slug || msg.requestId !== state().requestId)) return;
|
|
74
|
+
update({ saving: false });
|
|
75
|
+
if (!msg.ok) { render(); status(msg.error || 'Save failed. Your draft is kept.'); return; }
|
|
76
|
+
clearCachedDraft();
|
|
77
|
+
update({ saved: state().submitted, dirty: false, editing: false });
|
|
78
|
+
render(); document.getElementById('ps-env-add-btn').focus(); status('Saved. ' + (msg.timing || 'Applies to newly created coding-agent processes.'));
|
|
79
|
+
}
|
|
80
|
+
function save() {
|
|
81
|
+
var names = {};
|
|
82
|
+
var rows = state().rows || [];
|
|
83
|
+
for (var i = 0; i < rows.length; i++) {
|
|
84
|
+
var key = rows[i].key.trim();
|
|
85
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) { status('Enter a valid name for every variable.'); return; }
|
|
86
|
+
if (Object.prototype.hasOwnProperty.call(names, key)) { status('Each variable needs a unique name.'); return; }
|
|
87
|
+
names[key] = true;
|
|
88
|
+
}
|
|
89
|
+
var envrc = rows.map(function (row) { return 'export ' + row.key.trim() + '=' + serializeEnvValue(row.value); }).join('\n');
|
|
90
|
+
var requestId = crypto.randomUUID();
|
|
91
|
+
update({ requestId: requestId });
|
|
92
|
+
if (!send({ type: 'set_project_env', slug: state().slug, envrc: envrc, requestId: requestId })) return;
|
|
93
|
+
update({ submitted: envrc, saving: true }); render(); status('Saving…');
|
|
94
|
+
}
|
|
95
|
+
export function initProjectEnvironment() {
|
|
96
|
+
document.getElementById('ps-env-add-btn').onclick = function () { changeRows((state().rows || []).concat([{ key: '', value: '' }])); render(); document.querySelector('#ps-env-list .ps-env-row:last-child input').focus(); };
|
|
97
|
+
document.getElementById('ps-env-save').onclick = save;
|
|
98
|
+
store.subscribe(function (next, previous) {
|
|
99
|
+
if (next.myUserId !== previous.myUserId) {
|
|
100
|
+
store.set({ projectEnvironmentEditor: {}, projectEnvironmentDrafts: {} }); render(); status(''); return;
|
|
101
|
+
}
|
|
102
|
+
if (previous.connected && !next.connected && (state().saving || state().loading)) {
|
|
103
|
+
update({ saving: false, loading: false }); render(); status('Disconnected. Your draft is kept. Reconnect and try again.');
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|