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.
Files changed (83) hide show
  1. package/bin/cli.js +9 -91
  2. package/lib/daemon-settings.js +11 -0
  3. package/lib/daemon.js +8 -37
  4. package/lib/linear-store.js +35 -8
  5. package/lib/os-users.js +5 -16
  6. package/lib/project-connection.js +0 -7
  7. package/lib/project-filesystem.js +21 -39
  8. package/lib/project-mate-interaction.js +2 -2
  9. package/lib/project-server-settings.js +31 -0
  10. package/lib/project-sessions.js +57 -184
  11. package/lib/public/app.js +6 -37
  12. package/lib/public/css/input.css +3 -1
  13. package/lib/public/css/project-settings-modal.css +56 -0
  14. package/lib/public/css/server-settings-system.css +125 -0
  15. package/lib/public/css/session-github.css +25 -16
  16. package/lib/public/css/settings-ui.css +247 -22
  17. package/lib/public/css/shared-browser.css +2 -0
  18. package/lib/public/css/title-bar.css +5 -1
  19. package/lib/public/index.html +170 -446
  20. package/lib/public/modules/account-island.js +9 -7
  21. package/lib/public/modules/admin-settings-email.js +197 -0
  22. package/lib/public/modules/admin-settings-invitations.js +183 -0
  23. package/lib/public/modules/admin-settings-projects.js +198 -0
  24. package/lib/public/modules/admin-settings-ui.js +100 -0
  25. package/lib/public/modules/admin-settings-users.js +331 -0
  26. package/lib/public/modules/admin.js +19 -949
  27. package/lib/public/modules/app-dm.js +0 -2
  28. package/lib/public/modules/app-messages.js +13 -52
  29. package/lib/public/modules/app-rate-limit.js +70 -12
  30. package/lib/public/modules/context-sources.js +27 -18
  31. package/lib/public/modules/default-ai.js +14 -7
  32. package/lib/public/modules/header-tui-font.js +10 -20
  33. package/lib/public/modules/linear-settings.js +3 -2
  34. package/lib/public/modules/markdown.js +5 -6
  35. package/lib/public/modules/notification-settings.js +81 -0
  36. package/lib/public/modules/notifications.js +44 -28
  37. package/lib/public/modules/personal-settings-dialog.js +51 -0
  38. package/lib/public/modules/profile.js +15 -6
  39. package/lib/public/modules/project-settings-environment.js +106 -0
  40. package/lib/public/modules/project-settings.js +72 -628
  41. package/lib/public/modules/server-settings-controls.js +316 -0
  42. package/lib/public/modules/server-settings-environment.js +194 -0
  43. package/lib/public/modules/server-settings-navigation.js +39 -0
  44. package/lib/public/modules/server-settings-state.js +54 -0
  45. package/lib/public/modules/server-settings.js +116 -788
  46. package/lib/public/modules/server-storage-settings.js +84 -0
  47. package/lib/public/modules/session-github.js +2 -1
  48. package/lib/public/modules/session-presence-ui.js +1 -1
  49. package/lib/public/modules/settings-env-values.js +34 -0
  50. package/lib/public/modules/settings-select.js +121 -0
  51. package/lib/public/modules/shared-browser.js +10 -7
  52. package/lib/public/modules/stt-picker.js +14 -5
  53. package/lib/public/modules/terminal-prefs.js +22 -0
  54. package/lib/public/modules/theme.js +88 -52
  55. package/lib/public/modules/user-settings-account.js +318 -0
  56. package/lib/public/modules/user-settings-appearance.js +55 -0
  57. package/lib/public/modules/user-settings-editors.js +24 -0
  58. package/lib/public/modules/user-settings-integrations.js +104 -0
  59. package/lib/public/modules/user-settings-layout.js +80 -0
  60. package/lib/public/modules/user-settings-preferences.js +48 -0
  61. package/lib/public/modules/user-settings-profile.js +53 -0
  62. package/lib/public/modules/user-settings-themes.js +38 -0
  63. package/lib/public/modules/user-settings-voice.js +75 -0
  64. package/lib/public/modules/user-settings.js +53 -480
  65. package/lib/public/style.css +4 -0
  66. package/lib/sdk-message-processor.js +16 -4
  67. package/lib/sdk-quota-snapshot.js +37 -0
  68. package/lib/server-appearance-settings.js +71 -0
  69. package/lib/server-linear.js +14 -8
  70. package/lib/server-notification-settings.js +67 -0
  71. package/lib/server-settings.js +4 -0
  72. package/lib/server-storage.js +56 -0
  73. package/lib/server.js +10 -2
  74. package/lib/sessions.js +2 -3
  75. package/lib/startup-projects.js +40 -0
  76. package/lib/storage-usage.js +127 -0
  77. package/lib/users-preferences.js +0 -74
  78. package/lib/users.js +0 -4
  79. package/lib/ws-schema.js +2 -4
  80. package/lib/yoke/adapters/codex.js +8 -45
  81. package/lib/yoke/codex-model-catalog.js +40 -0
  82. package/lib/yoke/codex-rate-limits.js +26 -0
  83. package/package.json +3 -3
@@ -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
- // Spawn a transient PTY for "view this Claude GUI session as TUI" (the
400
- // user's claudeOpenMode is 'tui'). The session itself stays a GUI session
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 so that flipping claudeOpenMode back to 'tui' later restores the
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
- var pref = getClaudeOpenModeForWs(ws);
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
- // Cold session: apply the viewer's pref.
503
+ // Retired terminal presentation preferences never affect an idle session.
516
504
  if (session.mode === "tui") {
517
- if (pref === "gui") {
518
- prepareTuiSessionForGuiView(session);
519
- session.runtimeMode = "gui";
520
- session.runtimeTerminalId = null;
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
- // Born-GUI: always GUI. We no longer auto-convert a GUI session to a
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
- if (requestedMode === "tui") {
718
- // TUI sessions own their cliSessionId up-front so we can launch
719
- // `claude --session-id <uuid>` and resume the same conversation
720
- // from external terminals (claude --resume <uuid>) and from the
721
- // jsonl watcher (~/.claude/projects/<cwd>/<uuid>.jsonl).
722
- //
723
- // Construction order matters: createSession() fires session_switched
724
- // synchronously, so we must populate terminalId on the record before
725
- // switching. Use createSessionRaw + switchSession to get the right
726
- // ordering and avoid an extra rebroadcast.
727
- sessionOpts.mode = "tui";
728
- sessionOpts.cliSessionId = crypto.randomUUID();
729
- sessionOpts.vendor = sessionOpts.vendor || "claude";
730
- // Per-session bypass-permissions: TUI shell command only. The flag is
731
- // persisted on the session so lazy-resume re-spawns the same way.
732
- if (msg.dangerouslySkipPermissions) sessionOpts.dangerouslySkipPermissions = true;
733
- newSess = sm.createSessionRaw(sessionOpts);
734
- if (tm) {
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
- var newSessionVendor = sessionOpts.vendor || sm.defaultVendor || "claude";
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; only cold sessions follow the clicker's
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); only cold sessions follow the viewer's pref. No PTY is spawned
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
- setUpdateChannel(newChannel);
1069
- setLatestVersion(null);
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: "error", message: "Admin access required" });
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
- var daemonConfig = opts.onGetDaemonConfig();
2164
- sendTo(ws, { type: "daemon_config", config: daemonConfig });
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
- if (msg.type === "set_keep_awake") {
2178
- if (typeof opts.onSetKeepAwake === "function") {
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
- if (msg.type === "set_inherit_groups") {
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
- // Per-user preference: when Clay opens a Claude session, render it as
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, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './modules/server-settings.js';
46
- import { initProjectSettings, handleInstructionsRead, handleInstructionsWrite, handleProjectEnv, handleProjectEnvSaved, isProjectSettingsOpen, handleProjectSharedEnv, handleProjectSharedEnvSaved, handleProjectOwnerChanged } from './modules/project-settings.js';
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: false,
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();
@@ -742,7 +742,9 @@
742
742
  }
743
743
 
744
744
  #input-wrapper {
745
- max-width: var(--content-width);
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); }