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
package/lib/project-sessions.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
var serverSettingsMessages = require("./project-server-settings");
|
|
1
2
|
var fs = require("fs");
|
|
2
3
|
var path = require("path");
|
|
3
|
-
var crypto = require("crypto");
|
|
4
4
|
var { execFileSync } = require("child_process");
|
|
5
5
|
var { CODEX_DEFAULTS, getCodexConfig } = require("./codex-defaults");
|
|
6
6
|
var updateSnooze = require("./update-snooze");
|
|
@@ -217,16 +217,6 @@ function attachSessions(ctx) {
|
|
|
217
217
|
return opts.canAccessProjectSlug(userId, targetSlug) === true;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
// Resolve the active user's Claude open-mode preference ('gui' or 'tui').
|
|
221
|
-
// Multi-user mode reads per-user storage; single-user mode falls back to
|
|
222
|
-
// the daemon-level default ('gui').
|
|
223
|
-
function getClaudeOpenModeForWs(ws) {
|
|
224
|
-
if (!usersModule || typeof usersModule.getClaudeOpenMode !== "function") return "gui";
|
|
225
|
-
var uid = ws && ws._clayUser ? ws._clayUser.id : null;
|
|
226
|
-
if (!uid) return "gui";
|
|
227
|
-
try { return usersModule.getClaudeOpenMode(uid) || "gui"; } catch (e) { return "gui"; }
|
|
228
|
-
}
|
|
229
|
-
|
|
230
220
|
// Resolve the home directory where Claude Code writes JSONL for a
|
|
231
221
|
// given session. In OS-isolation mode each Clay user runs as a real
|
|
232
222
|
// Linux account so JSONL lives under /home/clay-name; for single-user
|
|
@@ -396,8 +386,8 @@ function attachSessions(ctx) {
|
|
|
396
386
|
};
|
|
397
387
|
}
|
|
398
388
|
|
|
399
|
-
//
|
|
400
|
-
//
|
|
389
|
+
// Resume a legacy terminal session without rewriting its saved origin.
|
|
390
|
+
// The session itself keeps its original mode
|
|
401
391
|
// on disk; we only attach a runtime terminal so xterm can render
|
|
402
392
|
// `claude --resume <cliSessionId>`. When the PTY dies the runtime link
|
|
403
393
|
// clears and the next click can re-attach without converting the session.
|
|
@@ -441,8 +431,7 @@ function attachSessions(ctx) {
|
|
|
441
431
|
// current click. Reads the CLI jsonl transcript (cli-sessions.js),
|
|
442
432
|
// populates session.history, and tears
|
|
443
433
|
// down the PTY. The born-TUI marker (session.mode === 'tui') is kept on
|
|
444
|
-
// disk
|
|
445
|
-
// embedded-terminal experience instead of locking the session as GUI.
|
|
434
|
+
// disk to preserve the session's origin and compatibility metadata.
|
|
446
435
|
//
|
|
447
436
|
// Idempotent: if the PTY is already gone and history is populated, this
|
|
448
437
|
// is a no-op (avoids re-reading jsonl and re-writing the session file on
|
|
@@ -486,8 +475,7 @@ function attachSessions(ctx) {
|
|
|
486
475
|
function resolveSessionForView(session, ws) {
|
|
487
476
|
if (!session) return;
|
|
488
477
|
if (session.vendor && !vendorSupportsTui(session.vendor)) { session.tuiSuspended = false; return; }
|
|
489
|
-
|
|
490
|
-
// A LIVE runtime always wins over the viewer's claudeOpenMode pref:
|
|
478
|
+
// Preserve a live runtime across this feature's retirement:
|
|
491
479
|
// another user (or this user in another tab) may be in the session right
|
|
492
480
|
// now, so we join it in whatever mode it is actually running and never
|
|
493
481
|
// convert or kill it. "tui stays tui, gui stays gui."
|
|
@@ -512,23 +500,14 @@ function attachSessions(ctx) {
|
|
|
512
500
|
session.tuiSuspended = false;
|
|
513
501
|
return;
|
|
514
502
|
}
|
|
515
|
-
//
|
|
503
|
+
// Retired terminal presentation preferences never affect an idle session.
|
|
516
504
|
if (session.mode === "tui") {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
session.tuiSuspended = false;
|
|
522
|
-
} else {
|
|
523
|
-
prepareTuiSessionForGuiView(session);
|
|
524
|
-
session.runtimeMode = null;
|
|
525
|
-
session.runtimeTerminalId = null;
|
|
526
|
-
session.tuiSuspended = true;
|
|
527
|
-
}
|
|
505
|
+
prepareTuiSessionForGuiView(session);
|
|
506
|
+
session.runtimeMode = "gui";
|
|
507
|
+
session.runtimeTerminalId = null;
|
|
508
|
+
session.tuiSuspended = false;
|
|
528
509
|
} else {
|
|
529
|
-
//
|
|
530
|
-
// `claude --resume` terminal on a pref=tui click - that hijacked shared
|
|
531
|
-
// GUI sessions in multi-user. Such sessions render as their SDK chat.
|
|
510
|
+
// Chat sessions always retain their SDK presentation.
|
|
532
511
|
session.runtimeMode = null;
|
|
533
512
|
session.tuiSuspended = false;
|
|
534
513
|
}
|
|
@@ -593,6 +572,7 @@ function attachSessions(ctx) {
|
|
|
593
572
|
}
|
|
594
573
|
|
|
595
574
|
function handleSessionsMessage(ws, msg) {
|
|
575
|
+
if (serverSettingsMessages.handleServerSettings({ opts: opts, usersModule: usersModule, sendTo: sendTo, send: send }, ws, msg)) return true;
|
|
596
576
|
if (contextUsage.handleMessage(ws, msg)) return true;
|
|
597
577
|
|
|
598
578
|
if (msg.type === "push_subscribe") {
|
|
@@ -700,95 +680,27 @@ function attachSessions(ctx) {
|
|
|
700
680
|
sessionOpts.sessionVisibility = visibilityChoice.visibility;
|
|
701
681
|
if (msg.vendor) sessionOpts.vendor = msg.vendor;
|
|
702
682
|
if (isMate && msg._mateDefaultsToken === mateDefaultsToken) sessionOpts.model = msg.model;
|
|
703
|
-
// Mode resolution: vendors without a TUI session mode are always GUI.
|
|
704
|
-
// TUI-capable sessions honor the explicit msg.mode if provided, otherwise
|
|
705
|
-
// fall back to the user's claudeOpenMode preference. This is what
|
|
706
|
-
// makes the sidebar's "Claude" icon button create the right kind of
|
|
707
|
-
// session without the client needing to know the preference.
|
|
708
|
-
var requestedMode;
|
|
709
|
-
if (msg.vendor && !vendorSupportsTui(msg.vendor)) {
|
|
710
|
-
requestedMode = "gui";
|
|
711
|
-
} else if (msg.mode === "tui" || msg.mode === "gui") {
|
|
712
|
-
requestedMode = msg.mode;
|
|
713
|
-
} else {
|
|
714
|
-
requestedMode = getClaudeOpenModeForWs(ws);
|
|
715
|
-
}
|
|
716
683
|
var newSess;
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
sessionOpts.
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
var tuiSid = newSess.cliSessionId;
|
|
736
|
-
var tuiLocalId = newSess.localId;
|
|
737
|
-
var tuiSkip = newSess.dangerouslySkipPermissions ? " --dangerously-skip-permissions" : "";
|
|
738
|
-
var tuiCmd = "claude --session-id " + tuiSid + tuiSkip + "; exit\n";
|
|
739
|
-
var tuiTerm = tm.create(80, 24, getOsUserInfoForWs(ws), ws, {
|
|
740
|
-
initialInput: tuiCmd,
|
|
741
|
-
kind: "tui-session",
|
|
742
|
-
title: "claude " + tuiSid.slice(0, 8),
|
|
743
|
-
onExit: function (termSession) {
|
|
744
|
-
var s = sm.sessions.get(tuiLocalId);
|
|
745
|
-
if (!s) return;
|
|
746
|
-
if (termSession && termSession.reclaimed) {
|
|
747
|
-
// Reclaimed (idle sweep or explicit Close), not a real /exit:
|
|
748
|
-
// keep the session (its jsonl transcript stays on disk and
|
|
749
|
-
// lazy-resume can re-spawn claude). Just drop the dead PTY link.
|
|
750
|
-
s.terminalId = null;
|
|
751
|
-
try { sm.saveSessionFile(s); } catch (e) {}
|
|
752
|
-
try { sm.broadcastSessionList(); } catch (e) {}
|
|
753
|
-
} else {
|
|
754
|
-
try { sm.deleteSessionQuiet(tuiLocalId); } catch (e) {}
|
|
755
|
-
try { sm.broadcastSessionList(); } catch (e) {}
|
|
756
|
-
}
|
|
757
|
-
},
|
|
758
|
-
onData: makeTuiActivityHook(tuiLocalId),
|
|
759
|
-
});
|
|
760
|
-
if (tuiTerm) {
|
|
761
|
-
newSess.terminalId = tuiTerm.id;
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
// Persist immediately so the session reappears in the sidebar
|
|
765
|
-
// after a daemon restart (the SDK path saves on stream events,
|
|
766
|
-
// but TUI sessions never produce any so this is our only chance).
|
|
767
|
-
try { sm.saveSessionFile(newSess); } catch (e) {}
|
|
768
|
-
startTitleWatcher(newSess);
|
|
769
|
-
sm.switchSession(newSess.localId, ws);
|
|
684
|
+
var newSessionVendor = sessionOpts.vendor || sm.defaultVendor || "claude";
|
|
685
|
+
var newSessionEffort = isMate && msg._mateDefaultsToken === mateDefaultsToken
|
|
686
|
+
? (msg.effort || "")
|
|
687
|
+
: ((sm.currentEffortByVendor && sm.currentEffortByVendor[newSessionVendor]) || sm.currentEffort || "medium");
|
|
688
|
+
sessionOpts.effort = yoke.clampEffort(newSessionVendor, newSessionEffort) || null;
|
|
689
|
+
// Reuse an existing blank GUI session instead of stacking another
|
|
690
|
+
// one. A vendor-less blank is acceptable: nothing has happened in it,
|
|
691
|
+
// so stamping the requested vendor is equivalent to a fresh create.
|
|
692
|
+
var reusable = msg.forceNew === true ? null : sm.findReusableBlankSession({
|
|
693
|
+
vendor: sessionOpts.vendor || null,
|
|
694
|
+
ownerId: sessionOpts.ownerId || null,
|
|
695
|
+
});
|
|
696
|
+
if (reusable) {
|
|
697
|
+
if (sessionOpts.vendor && reusable.vendor !== sessionOpts.vendor) reusable.vendor = sessionOpts.vendor;
|
|
698
|
+
if (!reusable.effort) reusable.effort = sessionOpts.effort;
|
|
699
|
+
// Reusable blanks already exist; do not retroactively change them.
|
|
700
|
+
sm.switchSession(reusable.localId, ws);
|
|
701
|
+
newSess = reusable;
|
|
770
702
|
} else {
|
|
771
|
-
|
|
772
|
-
var newSessionEffort = isMate && msg._mateDefaultsToken === mateDefaultsToken
|
|
773
|
-
? (msg.effort || "")
|
|
774
|
-
: ((sm.currentEffortByVendor && sm.currentEffortByVendor[newSessionVendor]) || sm.currentEffort || "medium");
|
|
775
|
-
sessionOpts.effort = yoke.clampEffort(newSessionVendor, newSessionEffort) || null;
|
|
776
|
-
// Reuse an existing blank GUI session instead of stacking another
|
|
777
|
-
// one. A vendor-less blank is acceptable: nothing has happened in it,
|
|
778
|
-
// so stamping the requested vendor is equivalent to a fresh create.
|
|
779
|
-
var reusable = msg.forceNew === true ? null : sm.findReusableBlankSession({
|
|
780
|
-
vendor: sessionOpts.vendor || null,
|
|
781
|
-
ownerId: sessionOpts.ownerId || null,
|
|
782
|
-
});
|
|
783
|
-
if (reusable) {
|
|
784
|
-
if (sessionOpts.vendor && reusable.vendor !== sessionOpts.vendor) reusable.vendor = sessionOpts.vendor;
|
|
785
|
-
if (!reusable.effort) reusable.effort = sessionOpts.effort;
|
|
786
|
-
// Reusable blanks already exist; do not retroactively change them.
|
|
787
|
-
sm.switchSession(reusable.localId, ws);
|
|
788
|
-
newSess = reusable;
|
|
789
|
-
} else {
|
|
790
|
-
newSess = sm.createSession(sessionOpts, ws);
|
|
791
|
-
}
|
|
703
|
+
newSess = sm.createSession(sessionOpts, ws);
|
|
792
704
|
}
|
|
793
705
|
ws._clayActiveSession = newSess.localId;
|
|
794
706
|
var folderPlaced = msg.folderId === undefined ? null : sessionFolders.placeNewSession(ws, newSess, msg.folderId, msg.folderSlug);
|
|
@@ -881,14 +793,11 @@ function attachSessions(ctx) {
|
|
|
881
793
|
// resolveSessionForView sets runtimeMode / runtimeTerminalId /
|
|
882
794
|
// tuiSuspended (and hydrates the transcript) before sm.switchSession
|
|
883
795
|
// broadcasts session_switched. A live runtime keeps its actual mode
|
|
884
|
-
// for every viewer;
|
|
885
|
-
// claudeOpenMode pref. Nothing is spawned here.
|
|
796
|
+
// for every viewer; idle sessions open in chat. Nothing is spawned here.
|
|
886
797
|
var xmTarget = sm.sessions.get(msg.id);
|
|
887
798
|
if (xmTarget && (!xmTarget.vendor || vendorSupportsTui(xmTarget.vendor))) {
|
|
888
799
|
// Single source of truth: live runtime wins (tui stays tui, gui stays
|
|
889
|
-
// gui);
|
|
890
|
-
// on switch - born-TUI resumes lazily via the Resume bar
|
|
891
|
-
// (resume_tui_session), and born-GUI never auto-converts to a terminal.
|
|
800
|
+
// gui); idle legacy sessions hydrate their transcript into chat.
|
|
892
801
|
resolveSessionForView(xmTarget, ws);
|
|
893
802
|
}
|
|
894
803
|
var switchTargetSess = sm.sessions.get(msg.id);
|
|
@@ -1063,12 +972,17 @@ function attachSessions(ctx) {
|
|
|
1063
972
|
}
|
|
1064
973
|
|
|
1065
974
|
if (msg.type === "set_update_channel") {
|
|
1066
|
-
if (!updateSnooze.isUpdateAdmin(ws, usersModule)) return true;
|
|
975
|
+
if (!updateSnooze.isUpdateAdmin(ws, usersModule)) { sendTo(ws, { type: "set_update_channel_result", ok: false, error: "Administrator access required." }); return true; }
|
|
1067
976
|
var newChannel = msg.channel === "beta" ? "beta" : "stable";
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
if (typeof opts.onSetUpdateChannel === "function") {
|
|
977
|
+
try {
|
|
978
|
+
if (typeof opts.onSetUpdateChannel !== "function") throw new Error("Release channel cannot be saved in this runtime.");
|
|
1071
979
|
opts.onSetUpdateChannel(newChannel);
|
|
980
|
+
setUpdateChannel(newChannel);
|
|
981
|
+
setLatestVersion(null);
|
|
982
|
+
sendTo(ws, { type: "set_update_channel_result", ok: true, timing: "Future update checks use this channel." });
|
|
983
|
+
} catch (error) {
|
|
984
|
+
sendTo(ws, { type: "set_update_channel_result", ok: false, error: error.message });
|
|
985
|
+
return true;
|
|
1072
986
|
}
|
|
1073
987
|
// Re-fetch with new channel and broadcast to admin clients, honouring
|
|
1074
988
|
// any snooze each of them has on the version that comes back.
|
|
@@ -1095,7 +1009,7 @@ function attachSessions(ctx) {
|
|
|
1095
1009
|
} else {
|
|
1096
1010
|
sendTo(ws, { type: "up_to_date", version: currentVersion });
|
|
1097
1011
|
}
|
|
1098
|
-
}).catch(function () {});
|
|
1012
|
+
}).catch(function () { sendTo(ws, { type: "update_check_failed", error: "Could not check for updates. Retry when the server can reach the release service." }); });
|
|
1099
1013
|
return true;
|
|
1100
1014
|
}
|
|
1101
1015
|
|
|
@@ -2150,9 +2064,9 @@ function attachSessions(ctx) {
|
|
|
2150
2064
|
if (msg.type === "get_daemon_config" || msg.type === "set_pin" || msg.type === "set_keep_awake" ||
|
|
2151
2065
|
msg.type === "set_auto_continue" || msg.type === "set_inherit_groups" || msg.type === "set_image_retention" || msg.type === "shutdown_server" || msg.type === "restart_server") {
|
|
2152
2066
|
if (usersModule.isMultiUser()) {
|
|
2153
|
-
var _wsUser = ws._clayUser;
|
|
2067
|
+
var _wsUser = ws._clayUser && usersModule.findUserById(ws._clayUser.id);
|
|
2154
2068
|
if (!_wsUser || _wsUser.role !== "admin") {
|
|
2155
|
-
sendTo(ws, { type: "
|
|
2069
|
+
sendTo(ws, { type: msg.type + "_result", ok: false, error: "Administrator access required." });
|
|
2156
2070
|
return true;
|
|
2157
2071
|
}
|
|
2158
2072
|
}
|
|
@@ -2160,28 +2074,15 @@ function attachSessions(ctx) {
|
|
|
2160
2074
|
|
|
2161
2075
|
if (msg.type === "get_daemon_config") {
|
|
2162
2076
|
if (typeof opts.onGetDaemonConfig === "function") {
|
|
2163
|
-
|
|
2164
|
-
sendTo(ws, { type: "daemon_config", config:
|
|
2165
|
-
}
|
|
2077
|
+
try { sendTo(ws, { type: "daemon_config", config: opts.onGetDaemonConfig() }); }
|
|
2078
|
+
catch (error) { sendTo(ws, { type: "daemon_config", config: { error: 'Server configuration is unavailable. Close and reopen Settings to retry.' } }); }
|
|
2079
|
+
} else { sendTo(ws, { type: "daemon_config", config: { error: 'Server configuration is unavailable in this runtime.' } }); }
|
|
2166
2080
|
return true;
|
|
2167
2081
|
}
|
|
2168
2082
|
|
|
2169
|
-
if (msg.type === "set_pin") {
|
|
2170
|
-
if (typeof opts.onSetPin === "function") {
|
|
2171
|
-
var pinResult = opts.onSetPin(msg.pin || null);
|
|
2172
|
-
sendTo(ws, { type: "set_pin_result", ok: pinResult.ok, pinEnabled: pinResult.pinEnabled });
|
|
2173
|
-
}
|
|
2174
|
-
return true;
|
|
2175
|
-
}
|
|
2176
2083
|
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
var kaResult = opts.onSetKeepAwake(msg.value);
|
|
2180
|
-
sendTo(ws, { type: "set_keep_awake_result", ok: kaResult.ok, keepAwake: kaResult.keepAwake });
|
|
2181
|
-
send({ type: "keep_awake_changed", keepAwake: kaResult.keepAwake });
|
|
2182
|
-
}
|
|
2183
|
-
return true;
|
|
2184
|
-
}
|
|
2084
|
+
|
|
2085
|
+
|
|
2185
2086
|
|
|
2186
2087
|
if (msg.type === "set_auto_continue") {
|
|
2187
2088
|
if (typeof opts.onSetAutoContinue === "function") {
|
|
@@ -2192,14 +2093,7 @@ function attachSessions(ctx) {
|
|
|
2192
2093
|
return true;
|
|
2193
2094
|
}
|
|
2194
2095
|
|
|
2195
|
-
|
|
2196
|
-
if (typeof opts.onSetInheritGroups === "function") {
|
|
2197
|
-
var igResult = opts.onSetInheritGroups(msg.value);
|
|
2198
|
-
sendTo(ws, { type: "set_inherit_groups_result", ok: igResult.ok, inheritGroups: igResult.inheritGroups });
|
|
2199
|
-
send({ type: "inherit_groups_changed", inheritGroups: igResult.inheritGroups });
|
|
2200
|
-
}
|
|
2201
|
-
return true;
|
|
2202
|
-
}
|
|
2096
|
+
|
|
2203
2097
|
|
|
2204
2098
|
if (msg.type === "get_claude_allow_list") {
|
|
2205
2099
|
var galUid = ws._clayUser ? ws._clayUser.id : null;
|
|
@@ -2230,6 +2124,7 @@ function attachSessions(ctx) {
|
|
|
2230
2124
|
// Re-install settings.json with the updated list so the new patterns
|
|
2231
2125
|
// take effect on the next `claude` invocation. Resolve this user's
|
|
2232
2126
|
// home (OS-mode: getent passwd; single-user: os.homedir()).
|
|
2127
|
+
var salEffective = false;
|
|
2233
2128
|
try {
|
|
2234
2129
|
var salInstaller = require("./claude-hook-installer");
|
|
2235
2130
|
var salHome = null;
|
|
@@ -2241,38 +2136,16 @@ function attachSessions(ctx) {
|
|
|
2241
2136
|
}
|
|
2242
2137
|
if (!salHome) salHome = require("os").homedir();
|
|
2243
2138
|
var salMerged = (salInstaller.CLAY_MANAGED_ALLOW || []).concat(salResult.claudeUserAllowList);
|
|
2244
|
-
salInstaller.installAllowList({ homeDirs: [salHome], patterns: salMerged });
|
|
2139
|
+
var salInstalled = salInstaller.installAllowList({ homeDirs: [salHome], patterns: salMerged });
|
|
2140
|
+
salEffective = !salInstalled.errors.length;
|
|
2245
2141
|
} catch (e) {}
|
|
2246
|
-
sendTo(ws, { type: "set_claude_user_allow_list_result", ok: true, patterns: salResult.claudeUserAllowList });
|
|
2142
|
+
sendTo(ws, { type: "set_claude_user_allow_list_result", ok: true, effective: salEffective, patterns: salResult.claudeUserAllowList });
|
|
2247
2143
|
return true;
|
|
2248
2144
|
}
|
|
2249
2145
|
|
|
2146
|
+
// Old clients receive an explicit refusal; stored legacy values are ignored.
|
|
2250
2147
|
if (msg.type === "set_claude_open_mode") {
|
|
2251
|
-
|
|
2252
|
-
// the SDK-driven custom chat ("gui") or as an embedded `claude` TUI
|
|
2253
|
-
// ("tui"). Applies to the next session open; currently displayed
|
|
2254
|
-
// sessions are not re-rendered retroactively.
|
|
2255
|
-
var comUserId = ws._clayUser ? ws._clayUser.id : null;
|
|
2256
|
-
if (!comUserId) {
|
|
2257
|
-
sendTo(ws, { type: "set_claude_open_mode_result", ok: false, error: "no_user" });
|
|
2258
|
-
return true;
|
|
2259
|
-
}
|
|
2260
|
-
var comResult = usersModule.setClaudeOpenMode(comUserId, msg.value);
|
|
2261
|
-
if (comResult && comResult.ok) {
|
|
2262
|
-
sendTo(ws, { type: "set_claude_open_mode_result", ok: true, claudeOpenMode: comResult.claudeOpenMode });
|
|
2263
|
-
// Echo as a "changed" broadcast for this user's other tabs/devices.
|
|
2264
|
-
sendTo(ws, { type: "claude_open_mode_changed", claudeOpenMode: comResult.claudeOpenMode });
|
|
2265
|
-
} else {
|
|
2266
|
-
sendTo(ws, { type: "set_claude_open_mode_result", ok: false, error: (comResult && comResult.error) || "unknown" });
|
|
2267
|
-
}
|
|
2268
|
-
return true;
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
|
-
if (msg.type === "set_image_retention") {
|
|
2272
|
-
if (typeof opts.onSetImageRetention === "function") {
|
|
2273
|
-
var irResult = opts.onSetImageRetention(msg.days);
|
|
2274
|
-
sendTo(ws, { type: "set_image_retention_result", ok: irResult.ok, days: irResult.days });
|
|
2275
|
-
}
|
|
2148
|
+
sendTo(ws, { type: "set_claude_open_mode_result", ok: false, error: "Terminal presentation has been removed. Claude opens in Clay chat." });
|
|
2276
2149
|
return true;
|
|
2277
2150
|
}
|
|
2278
2151
|
|
package/lib/public/app.js
CHANGED
|
@@ -42,8 +42,8 @@ import { initStickyNotes, handleNotesList, handleNoteCreated, handleNoteUpdated,
|
|
|
42
42
|
import { initNotesBrowser, openNotesBrowser, closeNotesBrowser, isNotesBrowserOpen, renderNotesBrowser } from './modules/sticky-notes-browser.js';
|
|
43
43
|
import { initTheme, getThemeColor, getComputedVar, onThemeChange, getCurrentTheme, getChatLayout } from './modules/theme.js';
|
|
44
44
|
import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionResolved, markPermissionCancelled, renderElicitationRequest, markElicitationResolved, renderPlanBanner, renderPlanCard, handleTodoWrite, handleTaskCreate, handleTaskUpdate, startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, addTurnMeta, resetTurnMetaCost, enableMainInput, getTools, getPlanContent, setPlanContent, isPlanFilePath, getTodoTools, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, updateSubagentProgress, initSubagentStop, closeToolGroup, removeToolFromGroup } from './modules/tools.js';
|
|
45
|
-
import { initServerSettings, updateSettingsStats, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved
|
|
46
|
-
import { initProjectSettings
|
|
45
|
+
import { initServerSettings, updateSettingsStats, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved } from './modules/server-settings.js';
|
|
46
|
+
import { initProjectSettings } from './modules/project-settings.js';
|
|
47
47
|
import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modules/skills.js';
|
|
48
48
|
import { initMcp } from './modules/mcp-ui.js';
|
|
49
49
|
import { initScheduler, resetScheduler, handleLoopRegistryUpdated, handleScheduleRunStarted, handleScheduleRunFinished, handleLoopScheduled, openSchedulerToTab, isSchedulerOpen, closeScheduler, enterCraftingMode, exitCraftingMode, handleLoopRegistryFiles, getUpcomingSchedules } from './modules/scheduler.js';
|
|
@@ -554,7 +554,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
554
554
|
cachedMatesList: [],
|
|
555
555
|
cachedDmFavorites: [],
|
|
556
556
|
cachedAvailableBuiltins: [],
|
|
557
|
-
projectMateDmsEnabled:
|
|
557
|
+
projectMateDmsEnabled: true,
|
|
558
558
|
returningFromMateDm: false,
|
|
559
559
|
pendingMateInterview: null,
|
|
560
560
|
pendingTermCommand: null,
|
|
@@ -602,10 +602,6 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
602
602
|
vendorInfo: {},
|
|
603
603
|
// Rate-limit data is projected only for the displayed session/vendor.
|
|
604
604
|
rateLimitState: {},
|
|
605
|
-
// How Claude sessions open: "gui" (default) or "tui". The server sends
|
|
606
|
-
// claude_open_mode_changed on connect; this seeds it beforehand so the
|
|
607
|
-
// new-session menu doesn't flash the TUI-only entry.
|
|
608
|
-
claudeOpenMode: "gui",
|
|
609
605
|
currentMode: "default",
|
|
610
606
|
effectivePermissionMode: null,
|
|
611
607
|
permissionCapabilities: { auto: false, mcpOverride: false },
|
|
@@ -1133,9 +1129,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
1133
1129
|
|
|
1134
1130
|
// --- Admin (multi-user mode) ---
|
|
1135
1131
|
// isMultiUserMode, isHeadlessMode, myUserId -> store
|
|
1136
|
-
initAdmin(
|
|
1137
|
-
get projectList() { return getCachedProjects(); },
|
|
1138
|
-
});
|
|
1132
|
+
initAdmin();
|
|
1139
1133
|
fetch("/api/me").then(function (r) { return r.json(); }).then(function (d) {
|
|
1140
1134
|
if (d.multiUser) {
|
|
1141
1135
|
store.set({ isMultiUserMode: true });
|
|
@@ -1217,35 +1211,10 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
1217
1211
|
});
|
|
1218
1212
|
|
|
1219
1213
|
// --- Server Settings ---
|
|
1220
|
-
initServerSettings(
|
|
1221
|
-
get ws() { return _getWsRef(); },
|
|
1222
|
-
get projectName() { return store.get('projectName'); },
|
|
1223
|
-
get currentSlug() { return store.get('currentSlug'); },
|
|
1224
|
-
wsPath: wsPath,
|
|
1225
|
-
get currentModels() { return store.get('currentModels'); },
|
|
1226
|
-
set currentModels(v) { store.set({ currentModels: v }); },
|
|
1227
|
-
get currentModel() { return store.get('currentModel'); },
|
|
1228
|
-
get currentMode() { return store.get('currentMode'); },
|
|
1229
|
-
get currentEffort() { return store.get('currentEffort'); },
|
|
1230
|
-
get currentBetas() { return store.get('currentBetas'); },
|
|
1231
|
-
get currentThinking() { return store.get('currentThinking'); },
|
|
1232
|
-
get currentThinkingBudget() { return store.get('currentThinkingBudget'); },
|
|
1233
|
-
setContextView: function (v) { _panSetContextView(v); },
|
|
1234
|
-
applyContextView: function (v) { _panApplyContextView(v); },
|
|
1235
|
-
});
|
|
1214
|
+
initServerSettings();
|
|
1236
1215
|
|
|
1237
1216
|
// --- Project Settings ---
|
|
1238
|
-
initProjectSettings(
|
|
1239
|
-
get ws() { return _getWsRef(); },
|
|
1240
|
-
get connected() { return store.get('connected'); },
|
|
1241
|
-
get currentModels() { return store.get('currentModels'); },
|
|
1242
|
-
get currentModel() { return store.get('currentModel'); },
|
|
1243
|
-
get currentMode() { return store.get('currentMode'); },
|
|
1244
|
-
get currentEffort() { return store.get('currentEffort'); },
|
|
1245
|
-
get currentBetas() { return store.get('currentBetas'); },
|
|
1246
|
-
get currentThinking() { return store.get('currentThinking'); },
|
|
1247
|
-
get currentThinkingBudget() { return store.get('currentThinkingBudget'); },
|
|
1248
|
-
}, getEmojiCategories());
|
|
1217
|
+
initProjectSettings(getEmojiCategories());
|
|
1249
1218
|
|
|
1250
1219
|
// --- QR code ---
|
|
1251
1220
|
initQrCode();
|
package/lib/public/css/input.css
CHANGED
|
@@ -742,7 +742,9 @@
|
|
|
742
742
|
}
|
|
743
743
|
|
|
744
744
|
#input-wrapper {
|
|
745
|
-
|
|
745
|
+
/* Writing follows the pane width independently of transcript layout.
|
|
746
|
+
Mate workspaces provide their own bounded composer override. */
|
|
747
|
+
max-width: 100%;
|
|
746
748
|
margin: 0 auto;
|
|
747
749
|
position: relative;
|
|
748
750
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/* Project settings shares the focused Mate dialog shell and compact settings interior. */
|
|
2
|
+
#project-settings {
|
|
3
|
+
position: fixed; inset: 0; z-index: 9000; display: grid; place-items: center;
|
|
4
|
+
width: auto; padding: 24px; border: 0; background: rgba(0, 0, 0, .38); backdrop-filter: blur(2px);
|
|
5
|
+
}
|
|
6
|
+
#project-settings.hidden { display: none; }
|
|
7
|
+
#project-settings .ps-modal {
|
|
8
|
+
display: flex; flex-direction: column; width: min(920px, 100%); height: min(720px, calc(100dvh - 48px));
|
|
9
|
+
border: 1px solid var(--border); border-radius: 14px; background: var(--bg); overflow: hidden;
|
|
10
|
+
box-shadow: 0 16px 48px rgba(0, 0, 0, .18);
|
|
11
|
+
}
|
|
12
|
+
#project-settings .ps-modal-header { display: flex; align-items: center; justify-content: space-between; min-height: 72px; padding: 14px 18px 13px 22px; border-bottom: 1px solid var(--border-subtle); }
|
|
13
|
+
#project-settings .ps-modal-header h2 { margin: 0; font-size: 16px; line-height: 22px; }
|
|
14
|
+
#project-settings #ps-nav-title { display: block; font-size: 12px; color: var(--text-muted); max-width: 65vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
15
|
+
#project-settings #project-settings-close { width: 32px; height: 32px; padding: 0; display: grid; place-items: center; }
|
|
16
|
+
#project-settings #project-settings-close svg { width: 16px; height: 16px; }
|
|
17
|
+
#project-settings .project-settings-layout { flex: 1; min-height: 0; height: auto; justify-content: initial; }
|
|
18
|
+
#project-settings .project-settings-layout::after { display: none; }
|
|
19
|
+
#project-settings .project-settings-nav { width: 180px; justify-content: initial; border-right: 1px solid var(--border-subtle); }
|
|
20
|
+
#project-settings .project-settings-nav-inner { width: 100%; box-sizing: border-box; padding: 14px 8px; }
|
|
21
|
+
#project-settings .settings-nav-item { height: 32px; min-height: 32px; border-left: 0; }
|
|
22
|
+
#project-settings .project-settings-content { max-width: none; }
|
|
23
|
+
#project-settings .project-settings-content-inner { padding: 24px 28px; }
|
|
24
|
+
#project-settings .ps-section h2 { margin: 0 0 20px; font-size: 20px; }
|
|
25
|
+
#project-settings .settings-card { background: none; border: 0; border-radius: 0; padding: 0; box-shadow: none; }
|
|
26
|
+
#project-settings .settings-field + .settings-field { border-top: 1px solid var(--border-subtle); padding-top: 20px; margin-top: 20px; }
|
|
27
|
+
#project-settings .settings-nav-dropdown { display: none; }
|
|
28
|
+
#project-settings :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
29
|
+
@media (max-width: 768px) {
|
|
30
|
+
#project-settings { padding: 12px; }
|
|
31
|
+
#project-settings .ps-modal { height: min(720px, calc(100dvh - 24px)); }
|
|
32
|
+
#project-settings .project-settings-layout { flex-direction: column; }
|
|
33
|
+
#project-settings .project-settings-nav { width: 100%; flex-shrink: 0; overflow: visible; border-right: 0; border-bottom: 1px solid var(--border-subtle); }
|
|
34
|
+
#project-settings .project-settings-nav-inner { padding: 8px 16px; }
|
|
35
|
+
#project-settings .server-settings-nav-items { display: none; }
|
|
36
|
+
#project-settings .settings-nav-dropdown { display: block; width: 100%; height: 36px; margin: 0; }
|
|
37
|
+
#project-settings .project-settings-content-inner { padding: 20px 16px; }
|
|
38
|
+
}
|
|
39
|
+
#project-settings [hidden], #project-settings .hidden { display: none !important; }
|
|
40
|
+
@media (prefers-reduced-motion: reduce) {
|
|
41
|
+
#project-settings *, #project-settings *::before, #project-settings *::after { transition: none !important; animation: none !important; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#project-settings .ps-project-identity { display: flex; align-items: flex-start; gap: 20px; padding: 8px 0 24px; }
|
|
45
|
+
#project-settings .ps-project-mark { flex: 0 0 64px; text-align: center; }
|
|
46
|
+
#project-settings .ps-project-icon { position: relative; width: 64px; height: 64px; display: grid; place-items: center; border: 1px solid var(--border); border-radius: 12px; background: var(--bg-alt); color: var(--text); cursor: pointer; }
|
|
47
|
+
#project-settings .ps-project-icon > svg { position: absolute; width: 12px; height: 12px; right: 4px; bottom: 4px; padding: 2px; border-radius: 4px; background: var(--bg); }
|
|
48
|
+
#project-settings .ps-icon-reset { padding: 6px 0; border: 0; background: none; color: var(--text-muted); font: inherit; font-size: 11px; cursor: pointer; }
|
|
49
|
+
#project-settings .ps-project-identity-copy { flex: 1; min-width: 0; }
|
|
50
|
+
#project-settings .ps-name-row { justify-content: space-between; gap: 12px; }
|
|
51
|
+
#project-settings #ps-project-name { font-size: 16px; font-weight: 600; overflow-wrap: anywhere; }
|
|
52
|
+
#project-settings .ps-project-location { border-top: 1px solid var(--border-subtle); padding-top: 20px; }
|
|
53
|
+
#project-settings .ps-project-location h3 { margin: 0 0 8px; }
|
|
54
|
+
#project-settings .ps-project-path { font-size: 13px; font-family: var(--font-mono, monospace); overflow-wrap: anywhere; margin: 0 0 8px; color: var(--text); user-select: text; }
|
|
55
|
+
|
|
56
|
+
#project-settings .ps-project-icon .ps-icon-preview svg { width: 28px; height: 28px; color: var(--text-muted); }
|