clay-server 2.39.0-beta.4 → 2.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/daemon.js +13 -0
- package/lib/public/css/sidebar.css +0 -29
- package/lib/public/css/tui-attention.css +10 -0
- package/lib/public/css/user-settings.css +43 -0
- package/lib/public/index.html +17 -13
- package/lib/public/modules/session-tui-view.js +93 -42
- package/lib/public/modules/sidebar-sessions.js +0 -3
- package/lib/public/modules/terminal-prefs.js +53 -0
- package/lib/public/modules/terminal.js +18 -2
- package/lib/public/modules/theme.js +89 -20
- package/lib/public/modules/tui-attention.js +35 -10
- package/lib/public/modules/user-settings.js +73 -25
- package/lib/server-settings.js +55 -0
- package/lib/users-preferences.js +45 -0
- package/lib/users.js +4 -0
- package/package.json +1 -1
package/lib/daemon.js
CHANGED
|
@@ -728,6 +728,19 @@ var relay = createServer({
|
|
|
728
728
|
console.log("[daemon] Chat layout:", val, "(web)");
|
|
729
729
|
return { ok: true, chatLayout: val };
|
|
730
730
|
},
|
|
731
|
+
onSetTerminalFont: function (family, size) {
|
|
732
|
+
var DEFAULT_FAMILY = "'SF Mono', Menlo, Monaco, 'Courier New', monospace";
|
|
733
|
+
var current = config.terminalFont || { family: DEFAULT_FAMILY, size: 13 };
|
|
734
|
+
var nextFamily = (typeof family === "string" && family.trim()) ? family.trim().slice(0, 200) : current.family;
|
|
735
|
+
var nextSize = current.size;
|
|
736
|
+
if (typeof size === "number" && size >= 9 && size <= 32) {
|
|
737
|
+
nextSize = Math.round(size);
|
|
738
|
+
}
|
|
739
|
+
config.terminalFont = { family: nextFamily, size: nextSize };
|
|
740
|
+
saveConfig(config);
|
|
741
|
+
console.log("[daemon] Terminal font:", nextFamily, nextSize);
|
|
742
|
+
return config.terminalFont;
|
|
743
|
+
},
|
|
731
744
|
onSetMateOnboarded: function () {
|
|
732
745
|
config.mateOnboardingShown = true;
|
|
733
746
|
saveConfig(config);
|
|
@@ -1119,35 +1119,6 @@
|
|
|
1119
1119
|
margin-left: 12px;
|
|
1120
1120
|
}
|
|
1121
1121
|
|
|
1122
|
-
/* TUI session indicator: a tiny black "screen" with a bold green prompt
|
|
1123
|
-
glyph, meant to read as a miniature terminal (CRT-style cursor) at
|
|
1124
|
-
sidebar scale. Aligned to the middle of the text x-height so it sits
|
|
1125
|
-
visually centered with the session title rather than dropping below. */
|
|
1126
|
-
.session-tui-icon {
|
|
1127
|
-
display: inline-flex;
|
|
1128
|
-
align-items: center;
|
|
1129
|
-
justify-content: center;
|
|
1130
|
-
width: 14px;
|
|
1131
|
-
height: 14px;
|
|
1132
|
-
border-radius: 3px;
|
|
1133
|
-
background: #000;
|
|
1134
|
-
margin-right: 6px;
|
|
1135
|
-
vertical-align: middle;
|
|
1136
|
-
flex-shrink: 0;
|
|
1137
|
-
/* Nudge upward a hair so the icon optical-centers with lowercase glyphs
|
|
1138
|
-
(lucide icons render slightly below the geometric center). */
|
|
1139
|
-
position: relative;
|
|
1140
|
-
top: -1px;
|
|
1141
|
-
}
|
|
1142
|
-
.session-tui-icon svg,
|
|
1143
|
-
.session-tui-icon .lucide {
|
|
1144
|
-
width: 10px;
|
|
1145
|
-
height: 10px;
|
|
1146
|
-
color: #50fa7b;
|
|
1147
|
-
stroke: #50fa7b;
|
|
1148
|
-
stroke-width: 3;
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
1122
|
.session-top-action:hover {
|
|
1152
1123
|
background: var(--sidebar-hover);
|
|
1153
1124
|
color: var(--text);
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/* Subtle 1px divider against the sidebar so the embedded terminal
|
|
2
|
+
doesn't visually bleed into the session list. The host is
|
|
3
|
+
position:fixed and anchored to #messages' bounding rect, so its
|
|
4
|
+
left edge sits exactly on the sidebar boundary. Color matches
|
|
5
|
+
#main-area's existing border-left so the line reads as one
|
|
6
|
+
continuous edge with the rest of the chrome. */
|
|
7
|
+
#tui-session-host {
|
|
8
|
+
border-left: 1px solid var(--border-subtle);
|
|
9
|
+
}
|
|
10
|
+
|
|
1
11
|
/* Policy notice that sits above the embedded xterm in fullscreen TUI
|
|
2
12
|
sessions, explaining why this mode exists (post-2026-06-15 Agent SDK
|
|
3
13
|
billing split). Thin so it doesn't eat terminal real estate. */
|
|
@@ -859,3 +859,46 @@
|
|
|
859
859
|
align-items: stretch;
|
|
860
860
|
}
|
|
861
861
|
}
|
|
862
|
+
|
|
863
|
+
/* Terminal font picker row */
|
|
864
|
+
.us-term-font-row {
|
|
865
|
+
display: flex;
|
|
866
|
+
align-items: center;
|
|
867
|
+
gap: 8px;
|
|
868
|
+
flex-wrap: wrap;
|
|
869
|
+
}
|
|
870
|
+
.us-term-font-family {
|
|
871
|
+
flex: 1 1 auto;
|
|
872
|
+
min-width: 180px;
|
|
873
|
+
padding: 7px 10px;
|
|
874
|
+
border: 1px solid var(--border);
|
|
875
|
+
border-radius: 8px;
|
|
876
|
+
background: var(--input-bg);
|
|
877
|
+
color: var(--text);
|
|
878
|
+
font-size: 13px;
|
|
879
|
+
}
|
|
880
|
+
.us-term-font-custom {
|
|
881
|
+
flex: 1 1 100%;
|
|
882
|
+
padding: 7px 10px;
|
|
883
|
+
border: 1px solid var(--border);
|
|
884
|
+
border-radius: 8px;
|
|
885
|
+
background: var(--input-bg);
|
|
886
|
+
color: var(--text);
|
|
887
|
+
font-size: 13px;
|
|
888
|
+
font-family: "Roboto Mono", monospace;
|
|
889
|
+
}
|
|
890
|
+
.us-term-font-custom.hidden { display: none; }
|
|
891
|
+
.us-term-font-size {
|
|
892
|
+
width: 64px;
|
|
893
|
+
padding: 7px 10px;
|
|
894
|
+
border: 1px solid var(--border);
|
|
895
|
+
border-radius: 8px;
|
|
896
|
+
background: var(--input-bg);
|
|
897
|
+
color: var(--text);
|
|
898
|
+
font-size: 13px;
|
|
899
|
+
text-align: center;
|
|
900
|
+
}
|
|
901
|
+
.us-term-font-unit {
|
|
902
|
+
font-size: 13px;
|
|
903
|
+
color: var(--text-dimmer);
|
|
904
|
+
}
|
package/lib/public/index.html
CHANGED
|
@@ -879,7 +879,7 @@
|
|
|
879
879
|
</div>
|
|
880
880
|
</div>
|
|
881
881
|
<div class="user-island-actions">
|
|
882
|
-
<button id="user-theme-toggle-btn" title="
|
|
882
|
+
<button id="user-theme-toggle-btn" title="Themes" aria-label="Themes"><i data-lucide="palette"></i></button>
|
|
883
883
|
<button id="user-settings-btn" title="User settings"><i data-lucide="settings"></i></button>
|
|
884
884
|
</div>
|
|
885
885
|
</div>
|
|
@@ -1002,18 +1002,22 @@
|
|
|
1002
1002
|
<div class="us-section" data-section="us-appearance">
|
|
1003
1003
|
<h2>Appearance</h2>
|
|
1004
1004
|
<div class="settings-card">
|
|
1005
|
-
<div class="settings-label" style="margin-bottom:10px;">
|
|
1006
|
-
<div class="
|
|
1007
|
-
<
|
|
1008
|
-
<
|
|
1009
|
-
<
|
|
1010
|
-
<
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
<
|
|
1014
|
-
<
|
|
1015
|
-
<
|
|
1016
|
-
|
|
1005
|
+
<div class="settings-label" style="margin-bottom:10px;">Terminal font</div>
|
|
1006
|
+
<div class="us-term-font-row">
|
|
1007
|
+
<select id="us-term-font-family" class="us-term-font-family">
|
|
1008
|
+
<option value="'SF Mono', Menlo, Monaco, 'Courier New', monospace">SF Mono / Menlo (default)</option>
|
|
1009
|
+
<option value="'JetBrains Mono', 'SF Mono', Menlo, monospace">JetBrains Mono</option>
|
|
1010
|
+
<option value="'Fira Code', 'SF Mono', Menlo, monospace">Fira Code</option>
|
|
1011
|
+
<option value="'Cascadia Code', 'SF Mono', Menlo, monospace">Cascadia Code</option>
|
|
1012
|
+
<option value="'IBM Plex Mono', 'SF Mono', Menlo, monospace">IBM Plex Mono</option>
|
|
1013
|
+
<option value="'Source Code Pro', 'SF Mono', Menlo, monospace">Source Code Pro</option>
|
|
1014
|
+
<option value="'Roboto Mono', 'SF Mono', Menlo, monospace">Roboto Mono</option>
|
|
1015
|
+
<option value="ui-monospace, monospace">System monospace</option>
|
|
1016
|
+
<option value="__custom__">Custom...</option>
|
|
1017
|
+
</select>
|
|
1018
|
+
<input type="text" id="us-term-font-family-custom" class="us-term-font-custom hidden" placeholder="e.g. 'My Font', monospace" maxlength="200">
|
|
1019
|
+
<input type="number" id="us-term-font-size" class="us-term-font-size" min="9" max="32" step="1">
|
|
1020
|
+
<span class="us-term-font-unit">px</span>
|
|
1017
1021
|
</div>
|
|
1018
1022
|
</div>
|
|
1019
1023
|
<div class="settings-card">
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
import { getWs } from './ws-ref.js';
|
|
18
18
|
import { store } from './store.js';
|
|
19
|
+
import { getTerminalTheme } from './theme.js';
|
|
20
|
+
import { getTerminalFontFamily, getTerminalFontSize, onTerminalFontChange } from './terminal-prefs.js';
|
|
19
21
|
import { openArticle as openWhatsNewArticle } from './whats-new-article.js';
|
|
20
22
|
|
|
21
23
|
// Stable id of the canonical "Why TUI mode?" article in
|
|
@@ -23,33 +25,9 @@ import { openArticle as openWhatsNewArticle } from './whats-new-article.js';
|
|
|
23
25
|
// opens that article in the blog viewer instead of a one-off modal.
|
|
24
26
|
var TUI_POLICY_ARTICLE_ID = "2026-06-tui-default";
|
|
25
27
|
|
|
26
|
-
// Claude TUI
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// session view so `claude` renders consistently across themes.
|
|
30
|
-
var TUI_TERMINAL_THEME = {
|
|
31
|
-
background: "#000000",
|
|
32
|
-
foreground: "#e5e5e5",
|
|
33
|
-
cursor: "#e5e5e5",
|
|
34
|
-
cursorAccent: "#000000",
|
|
35
|
-
selectionBackground: "#3a3a3a",
|
|
36
|
-
black: "#000000",
|
|
37
|
-
red: "#cd3131",
|
|
38
|
-
green: "#0dbc79",
|
|
39
|
-
yellow: "#e5e510",
|
|
40
|
-
blue: "#2472c8",
|
|
41
|
-
magenta: "#bc3fbc",
|
|
42
|
-
cyan: "#11a8cd",
|
|
43
|
-
white: "#e5e5e5",
|
|
44
|
-
brightBlack: "#666666",
|
|
45
|
-
brightRed: "#f14c4c",
|
|
46
|
-
brightGreen: "#23d18b",
|
|
47
|
-
brightYellow: "#f5f543",
|
|
48
|
-
brightBlue: "#3b8eea",
|
|
49
|
-
brightMagenta: "#d670d6",
|
|
50
|
-
brightCyan: "#29b8db",
|
|
51
|
-
brightWhite: "#ffffff",
|
|
52
|
-
};
|
|
28
|
+
// Claude TUI session terminal colors follow Clay's active theme via
|
|
29
|
+
// getTerminalTheme(). Live theme switches are wired through
|
|
30
|
+
// setTuiSessionTheme() below, which theme.js calls from applyTheme.
|
|
53
31
|
|
|
54
32
|
var hostEl = null; // container div mounted over #messages
|
|
55
33
|
var xtermContainerEl = null;
|
|
@@ -165,25 +143,73 @@ function ensureHostEl() {
|
|
|
165
143
|
xtermContainerEl.style.position = "relative";
|
|
166
144
|
hostEl.appendChild(xtermContainerEl);
|
|
167
145
|
|
|
168
|
-
// Paste-image handling.
|
|
169
|
-
// image/file clipboard payloads, so an image copied in Finder/Preview
|
|
170
|
-
// would silently drop on Cmd+V. We intercept in the capture phase
|
|
171
|
-
// (before xterm's hidden textarea handles the event), upload the
|
|
172
|
-
// image to /api/upload, and inject the returned absolute path into
|
|
173
|
-
// the PTY. Claude CLI accepts file paths as prompt input - the user
|
|
174
|
-
// can then submit with Enter.
|
|
146
|
+
// Paste-image handling. Two paths cover the common platforms:
|
|
175
147
|
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
148
|
+
// 1. `paste` event in capture phase - covers Cmd+V on macOS and
|
|
149
|
+
// Ctrl+V on Win/Linux, where the browser fires a paste event
|
|
150
|
+
// with clipboardData populated.
|
|
151
|
+
// 2. `keydown` for Ctrl+V (no Meta) in capture phase - macOS does
|
|
152
|
+
// not fire `paste` for Ctrl+V (Ctrl isn't the OS paste modifier
|
|
153
|
+
// there), so the keystroke would otherwise reach claude CLI
|
|
154
|
+
// directly. We read the system clipboard via the async
|
|
155
|
+
// Clipboard API instead.
|
|
156
|
+
//
|
|
157
|
+
// In both paths an image hit is uploaded to /api/upload (same
|
|
158
|
+
// endpoint the GUI input uses) and the returned absolute path is
|
|
159
|
+
// injected as text into the PTY; claude CLI accepts file paths as
|
|
160
|
+
// prompt input. Capture phase + stopImmediatePropagation keep xterm
|
|
161
|
+
// from also processing the event (which would paste the filename
|
|
162
|
+
// text alongside the path).
|
|
181
163
|
hostEl.addEventListener("paste", handleTuiPaste, true);
|
|
164
|
+
hostEl.addEventListener("keydown", handleTuiCtrlV, true);
|
|
182
165
|
|
|
183
166
|
document.body.appendChild(hostEl);
|
|
184
167
|
return hostEl;
|
|
185
168
|
}
|
|
186
169
|
|
|
170
|
+
function handleTuiCtrlV(e) {
|
|
171
|
+
// Only handle plain Ctrl+V (no Meta/Alt/Shift). Cmd+V on macOS is
|
|
172
|
+
// covered by the paste event handler. Without this intercept on macOS
|
|
173
|
+
// a Ctrl+V keystroke would just be forwarded to claude CLI's own
|
|
174
|
+
// paste path, which can't read image data out of the system
|
|
175
|
+
// clipboard reliably ("no image found in clipboard").
|
|
176
|
+
if (e.key !== "v" || !e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return;
|
|
177
|
+
if (currentTermId == null) return;
|
|
178
|
+
if (!navigator.clipboard || typeof navigator.clipboard.read !== "function") return;
|
|
179
|
+
|
|
180
|
+
// Stop the keystroke from also reaching xterm / the PTY. We take full
|
|
181
|
+
// ownership of paste semantics: image -> upload + inject path, text
|
|
182
|
+
// -> forward via term_input, nothing -> no-op.
|
|
183
|
+
e.preventDefault();
|
|
184
|
+
e.stopImmediatePropagation();
|
|
185
|
+
|
|
186
|
+
navigator.clipboard.read().then(function (items) {
|
|
187
|
+
for (var i = 0; i < items.length; i++) {
|
|
188
|
+
var imgType = null;
|
|
189
|
+
for (var t = 0; t < items[i].types.length; t++) {
|
|
190
|
+
if (items[i].types[t].indexOf("image/") === 0) { imgType = items[i].types[t]; break; }
|
|
191
|
+
}
|
|
192
|
+
if (imgType) {
|
|
193
|
+
items[i].getType(imgType).then(uploadAndInjectPath).catch(function () {});
|
|
194
|
+
return; // handle the first image found
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// No image - fall back to text paste behavior.
|
|
198
|
+
if (typeof navigator.clipboard.readText === "function") {
|
|
199
|
+
navigator.clipboard.readText().then(function (text) {
|
|
200
|
+
if (!text || currentTermId == null) return;
|
|
201
|
+
var ws = getWs();
|
|
202
|
+
if (!ws || ws.readyState !== 1) return;
|
|
203
|
+
ws.send(JSON.stringify({ type: "term_input", id: currentTermId, data: text }));
|
|
204
|
+
}).catch(function () {});
|
|
205
|
+
}
|
|
206
|
+
}).catch(function () {
|
|
207
|
+
// Permission denied or unsupported - silently fail. xterm received
|
|
208
|
+
// no keystroke either because we already stopped propagation, so
|
|
209
|
+
// the user can press the key again to retry.
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
187
213
|
function handleTuiPaste(e) {
|
|
188
214
|
var cd = e.clipboardData;
|
|
189
215
|
if (!cd || currentTermId == null) return;
|
|
@@ -293,14 +319,14 @@ function fitNow() {
|
|
|
293
319
|
|
|
294
320
|
function createXterm() {
|
|
295
321
|
if (typeof Terminal === "undefined") return null;
|
|
296
|
-
var theme =
|
|
322
|
+
var theme = getTerminalTheme();
|
|
297
323
|
// Match the host background to the xterm theme so any sub-cell gap
|
|
298
324
|
// between the last rendered row and the host's bottom blends in.
|
|
299
325
|
if (hostEl) hostEl.style.background = theme.background;
|
|
300
326
|
var term = new Terminal({
|
|
301
327
|
cursorBlink: true,
|
|
302
|
-
fontSize:
|
|
303
|
-
fontFamily:
|
|
328
|
+
fontSize: getTerminalFontSize(),
|
|
329
|
+
fontFamily: getTerminalFontFamily(),
|
|
304
330
|
theme: theme,
|
|
305
331
|
scrollback: 5000,
|
|
306
332
|
});
|
|
@@ -450,3 +476,28 @@ export function tuiHandleTermClosed(msg) {
|
|
|
450
476
|
export function getActiveTuiTerminalId() {
|
|
451
477
|
return currentTermId;
|
|
452
478
|
}
|
|
479
|
+
|
|
480
|
+
// Live theme update. Called by theme.js applyTheme() whenever the user
|
|
481
|
+
// switches themes - rewrites xterm colors in place and re-syncs the
|
|
482
|
+
// host background so transitions are seamless without re-mounting the
|
|
483
|
+
// PTY.
|
|
484
|
+
export function setTuiSessionTheme(xtermTheme) {
|
|
485
|
+
if (xterm) {
|
|
486
|
+
try { xterm.options.theme = xtermTheme; } catch (e) {}
|
|
487
|
+
}
|
|
488
|
+
if (hostEl && xtermTheme && xtermTheme.background) {
|
|
489
|
+
hostEl.style.background = xtermTheme.background;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Live font update. Cell metrics shift with font size, so we refit
|
|
494
|
+
// after applying and let the existing resize debounce notify the PTY
|
|
495
|
+
// of the new cols/rows.
|
|
496
|
+
onTerminalFontChange(function (family, size) {
|
|
497
|
+
if (!xterm) return;
|
|
498
|
+
try {
|
|
499
|
+
if (family) xterm.options.fontFamily = family;
|
|
500
|
+
if (size) xterm.options.fontSize = size;
|
|
501
|
+
} catch (e) {}
|
|
502
|
+
scheduleResize();
|
|
503
|
+
});
|
|
@@ -1099,9 +1099,6 @@ function renderSessionItem(s) {
|
|
|
1099
1099
|
if (store.get('isMultiUserMode') && s.sessionVisibility === "private") {
|
|
1100
1100
|
textHtml += '<span class="session-private-icon" title="Private session">' + iconHtml("lock") + '</span>';
|
|
1101
1101
|
}
|
|
1102
|
-
if (s.mode === "tui") {
|
|
1103
|
-
textHtml += '<span class="session-tui-icon" title="Claude Code terminal session">' + iconHtml("terminal") + '</span>';
|
|
1104
|
-
}
|
|
1105
1102
|
textHtml += highlightMatch(s.title || "New Session", searchQuery);
|
|
1106
1103
|
textSpan.innerHTML = textHtml;
|
|
1107
1104
|
el.appendChild(textSpan);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// terminal-prefs.js
|
|
2
|
+
//
|
|
3
|
+
// Single source of truth for terminal font preferences (family + size).
|
|
4
|
+
// Every xterm in Clay (bottom shell panel, Claude TUI session view, TUI
|
|
5
|
+
// attention modal) reads from here on create and re-applies on the
|
|
6
|
+
// `font-change` event so live updates are seamless.
|
|
7
|
+
//
|
|
8
|
+
// Persistence lives server-side under the user's profile. This module
|
|
9
|
+
// keeps a synchronized in-memory copy that the rest of the client reads
|
|
10
|
+
// without round-trips.
|
|
11
|
+
|
|
12
|
+
var DEFAULT_FAMILY = "'SF Mono', Menlo, Monaco, 'Courier New', monospace";
|
|
13
|
+
var DEFAULT_SIZE = 13;
|
|
14
|
+
|
|
15
|
+
var currentFamily = DEFAULT_FAMILY;
|
|
16
|
+
var currentSize = DEFAULT_SIZE;
|
|
17
|
+
var listeners = [];
|
|
18
|
+
|
|
19
|
+
export function getTerminalFontFamily() {
|
|
20
|
+
return currentFamily || DEFAULT_FAMILY;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getTerminalFontSize() {
|
|
24
|
+
var n = Number(currentSize);
|
|
25
|
+
return (n >= 9 && n <= 32) ? n : DEFAULT_SIZE;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getDefaultTerminalFontFamily() {
|
|
29
|
+
return DEFAULT_FAMILY;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Set the in-memory values and notify subscribers. Pass null/undefined
|
|
33
|
+
// for any field that should stay unchanged. Persisting to the server
|
|
34
|
+
// is the caller's responsibility - this only updates local UI.
|
|
35
|
+
export function applyTerminalFont(family, size) {
|
|
36
|
+
var changed = false;
|
|
37
|
+
if (typeof family === "string" && family.trim() && family !== currentFamily) {
|
|
38
|
+
currentFamily = family;
|
|
39
|
+
changed = true;
|
|
40
|
+
}
|
|
41
|
+
if (typeof size === "number" && size >= 9 && size <= 32 && size !== currentSize) {
|
|
42
|
+
currentSize = Math.round(size);
|
|
43
|
+
changed = true;
|
|
44
|
+
}
|
|
45
|
+
if (!changed) return;
|
|
46
|
+
for (var i = 0; i < listeners.length; i++) {
|
|
47
|
+
try { listeners[i](currentFamily, currentSize); } catch (e) {}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function onTerminalFontChange(fn) {
|
|
52
|
+
if (typeof fn === "function") listeners.push(fn);
|
|
53
|
+
}
|
|
@@ -3,6 +3,7 @@ import { closeSidebar } from './sidebar.js';
|
|
|
3
3
|
import { closeFileViewer } from './filebrowser.js';
|
|
4
4
|
import { copyToClipboard } from './utils.js';
|
|
5
5
|
import { getTerminalTheme } from './theme.js';
|
|
6
|
+
import { getTerminalFontFamily, getTerminalFontSize, onTerminalFontChange } from './terminal-prefs.js';
|
|
6
7
|
|
|
7
8
|
var ctx;
|
|
8
9
|
var tabs = new Map(); // termId -> { id, title, exited, xterm, fitAddon, bodyEl }
|
|
@@ -347,8 +348,8 @@ function createXtermForTab(tab) {
|
|
|
347
348
|
|
|
348
349
|
var xterm = new Terminal({
|
|
349
350
|
cursorBlink: true,
|
|
350
|
-
fontSize:
|
|
351
|
-
fontFamily:
|
|
351
|
+
fontSize: getTerminalFontSize(),
|
|
352
|
+
fontFamily: getTerminalFontFamily(),
|
|
352
353
|
theme: getTerminalTheme(),
|
|
353
354
|
});
|
|
354
355
|
|
|
@@ -807,6 +808,21 @@ export function setTerminalTheme(xtermTheme) {
|
|
|
807
808
|
}
|
|
808
809
|
}
|
|
809
810
|
|
|
811
|
+
// Live font update: applies family/size to every open shell terminal
|
|
812
|
+
// and refits each (cell dimensions change with font size, so cols/rows
|
|
813
|
+
// shift too). Hooked via onTerminalFontChange below.
|
|
814
|
+
function applyFontToAllTabs(family, size) {
|
|
815
|
+
for (var tab of tabs.values()) {
|
|
816
|
+
if (!tab.xterm) continue;
|
|
817
|
+
try {
|
|
818
|
+
if (family) tab.xterm.options.fontFamily = family;
|
|
819
|
+
if (size) tab.xterm.options.fontSize = size;
|
|
820
|
+
if (tab.fitAddon) tab.fitAddon.fit();
|
|
821
|
+
} catch (e) {}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
onTerminalFontChange(applyFontToAllTabs);
|
|
825
|
+
|
|
810
826
|
// --- Terminal context menu ---
|
|
811
827
|
function closeTermCtxMenu() {
|
|
812
828
|
if (termCtxMenu) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
2
2
|
import { setTerminalTheme } from './terminal.js';
|
|
3
|
+
import { setTuiSessionTheme } from './session-tui-view.js';
|
|
4
|
+
import { setTuiAttentionTheme } from './tui-attention.js';
|
|
3
5
|
import { updateMermaidTheme } from './markdown.js';
|
|
4
6
|
|
|
5
7
|
// --- Color utilities ---
|
|
@@ -352,6 +354,8 @@ export function applyTheme(themeId, fromPicker) {
|
|
|
352
354
|
|
|
353
355
|
var termTheme = computeTerminalTheme(theme);
|
|
354
356
|
try { setTerminalTheme(termTheme); } catch (e) {}
|
|
357
|
+
try { setTuiSessionTheme(termTheme); } catch (e) {}
|
|
358
|
+
try { setTuiAttentionTheme(termTheme); } catch (e) {}
|
|
355
359
|
|
|
356
360
|
var mermaidVars = computeMermaidVars(theme);
|
|
357
361
|
try { updateMermaidTheme(mermaidVars); } catch (e) {}
|
|
@@ -538,15 +542,8 @@ function updateToggleIcon() {
|
|
|
538
542
|
// User settings toggle
|
|
539
543
|
var usToggle = document.getElementById("us-theme-toggle");
|
|
540
544
|
if (usToggle) usToggle.checked = isLight;
|
|
541
|
-
// User island
|
|
542
|
-
|
|
543
|
-
if (islandToggle) {
|
|
544
|
-
islandToggle.classList.remove("theme-toggle-light", "theme-toggle-dark");
|
|
545
|
-
islandToggle.classList.add(isLight ? "theme-toggle-light" : "theme-toggle-dark");
|
|
546
|
-
islandToggle.innerHTML = iconHtml(isLight ? "moon" : "sun");
|
|
547
|
-
islandToggle.title = isLight ? "Switch to dark mode" : "Switch to light mode";
|
|
548
|
-
islandToggle.setAttribute("aria-label", islandToggle.title);
|
|
549
|
-
}
|
|
545
|
+
// User island button is now a static "skins / themes" trigger; no
|
|
546
|
+
// icon swap on mode change.
|
|
550
547
|
refreshIcons();
|
|
551
548
|
}
|
|
552
549
|
|
|
@@ -749,24 +746,96 @@ export function initTheme() {
|
|
|
749
746
|
var islandToggle = document.getElementById("user-theme-toggle-btn");
|
|
750
747
|
if (islandToggle && !islandToggle._clayThemeBound) {
|
|
751
748
|
islandToggle._clayThemeBound = true;
|
|
752
|
-
|
|
753
|
-
|
|
749
|
+
// Lock down static skin icon (independent of mode) and open the
|
|
750
|
+
// theme picker popover anchored to the button.
|
|
751
|
+
islandToggle.innerHTML = iconHtml("palette");
|
|
752
|
+
islandToggle.title = "Themes";
|
|
753
|
+
islandToggle.setAttribute("aria-label", "Themes");
|
|
754
|
+
refreshIcons();
|
|
755
|
+
islandToggle.addEventListener("click", function (e) {
|
|
756
|
+
e.stopPropagation();
|
|
757
|
+
toggleIslandThemePicker(islandToggle);
|
|
754
758
|
});
|
|
755
759
|
}
|
|
756
760
|
}
|
|
757
761
|
|
|
758
|
-
// ---
|
|
759
|
-
|
|
760
|
-
|
|
762
|
+
// --- Floating popover anchored to the user island button ---
|
|
763
|
+
//
|
|
764
|
+
// Mounts the picker (built by createThemePicker) into document.body as a
|
|
765
|
+
// fixed-position overlay aligned under the anchor's right edge. Click
|
|
766
|
+
// outside or Escape closes it. The picker DOM is shared with any other
|
|
767
|
+
// embed point (settings page); we just reparent and reposition.
|
|
768
|
+
|
|
769
|
+
var islandPickerOpen = false;
|
|
770
|
+
var islandPickerOutsideHandler = null;
|
|
771
|
+
var islandPickerKeyHandler = null;
|
|
772
|
+
|
|
773
|
+
function positionPickerNearAnchor(anchorEl) {
|
|
774
|
+
if (!pickerEl || !anchorEl) return;
|
|
775
|
+
var rect = anchorEl.getBoundingClientRect();
|
|
776
|
+
var picker = pickerEl;
|
|
777
|
+
// Render hidden first to measure
|
|
778
|
+
picker.style.left = "0px";
|
|
779
|
+
picker.style.top = "0px";
|
|
780
|
+
var pw = picker.offsetWidth || 240;
|
|
781
|
+
var ph = picker.offsetHeight || 280;
|
|
782
|
+
var vw = window.innerWidth;
|
|
783
|
+
var vh = window.innerHeight;
|
|
784
|
+
// Right-align to anchor, drop below by 8px. Flip upward if not enough
|
|
785
|
+
// room below. Clamp into viewport on both axes.
|
|
786
|
+
var left = rect.right - pw;
|
|
787
|
+
if (left < 8) left = 8;
|
|
788
|
+
if (left + pw > vw - 8) left = vw - pw - 8;
|
|
789
|
+
var top = rect.bottom + 8;
|
|
790
|
+
if (top + ph > vh - 8) {
|
|
791
|
+
var flipped = rect.top - ph - 8;
|
|
792
|
+
if (flipped >= 8) top = flipped;
|
|
793
|
+
else top = Math.max(8, vh - ph - 8);
|
|
794
|
+
}
|
|
795
|
+
picker.style.left = left + "px";
|
|
796
|
+
picker.style.top = top + "px";
|
|
797
|
+
}
|
|
761
798
|
|
|
762
|
-
|
|
763
|
-
|
|
799
|
+
function closeIslandThemePicker() {
|
|
800
|
+
if (!islandPickerOpen) return;
|
|
801
|
+
islandPickerOpen = false;
|
|
802
|
+
if (pickerEl) pickerEl.classList.remove("visible");
|
|
803
|
+
if (islandPickerOutsideHandler) {
|
|
804
|
+
document.removeEventListener("mousedown", islandPickerOutsideHandler, true);
|
|
805
|
+
islandPickerOutsideHandler = null;
|
|
806
|
+
}
|
|
807
|
+
if (islandPickerKeyHandler) {
|
|
808
|
+
document.removeEventListener("keydown", islandPickerKeyHandler, true);
|
|
809
|
+
islandPickerKeyHandler = null;
|
|
764
810
|
}
|
|
811
|
+
}
|
|
765
812
|
|
|
766
|
-
|
|
767
|
-
if (
|
|
768
|
-
|
|
769
|
-
|
|
813
|
+
export function toggleIslandThemePicker(anchorEl) {
|
|
814
|
+
if (islandPickerOpen) {
|
|
815
|
+
closeIslandThemePicker();
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if (!pickerEl) createThemePicker();
|
|
819
|
+
if (pickerEl.parentNode !== document.body) {
|
|
820
|
+
document.body.appendChild(pickerEl);
|
|
770
821
|
}
|
|
822
|
+
positionPickerNearAnchor(anchorEl);
|
|
771
823
|
pickerEl.classList.add("visible");
|
|
824
|
+
islandPickerOpen = true;
|
|
825
|
+
|
|
826
|
+
// Click anywhere outside picker (and not the anchor) closes it. Use
|
|
827
|
+
// capture phase + mousedown so the click that opened the picker
|
|
828
|
+
// doesn't immediately close it.
|
|
829
|
+
islandPickerOutsideHandler = function (ev) {
|
|
830
|
+
if (!pickerEl) return;
|
|
831
|
+
if (pickerEl.contains(ev.target)) return;
|
|
832
|
+
if (anchorEl && anchorEl.contains(ev.target)) return;
|
|
833
|
+
closeIslandThemePicker();
|
|
834
|
+
};
|
|
835
|
+
document.addEventListener("mousedown", islandPickerOutsideHandler, true);
|
|
836
|
+
|
|
837
|
+
islandPickerKeyHandler = function (ev) {
|
|
838
|
+
if (ev.key === "Escape") closeIslandThemePicker();
|
|
839
|
+
};
|
|
840
|
+
document.addEventListener("keydown", islandPickerKeyHandler, true);
|
|
772
841
|
}
|
|
@@ -13,13 +13,12 @@
|
|
|
13
13
|
// term_attach / term_input / term_output through that connection only.
|
|
14
14
|
// The main WS is untouched.
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
};
|
|
16
|
+
import { getTerminalTheme } from './theme.js';
|
|
17
|
+
import { getTerminalFontFamily, getTerminalFontSize, onTerminalFontChange } from './terminal-prefs.js';
|
|
18
|
+
|
|
19
|
+
// TUI attention modal xterm follows Clay's active theme via
|
|
20
|
+
// getTerminalTheme() and live-updates through setTuiAttentionTheme()
|
|
21
|
+
// which theme.js calls from applyTheme.
|
|
23
22
|
|
|
24
23
|
var modalEl = null;
|
|
25
24
|
var modalXterm = null;
|
|
@@ -152,9 +151,9 @@ export function openTuiModal(terminalId, sourceSlug, info) {
|
|
|
152
151
|
bodyEl.innerHTML = "";
|
|
153
152
|
modalXterm = new Terminal({
|
|
154
153
|
cursorBlink: true,
|
|
155
|
-
fontSize:
|
|
156
|
-
fontFamily:
|
|
157
|
-
theme:
|
|
154
|
+
fontSize: getTerminalFontSize(),
|
|
155
|
+
fontFamily: getTerminalFontFamily(),
|
|
156
|
+
theme: getTerminalTheme(),
|
|
158
157
|
scrollback: 5000,
|
|
159
158
|
});
|
|
160
159
|
if (typeof FitAddon !== "undefined") {
|
|
@@ -260,3 +259,29 @@ export function tuiModalHandleTermOutput() { return false; }
|
|
|
260
259
|
export function tuiModalHandleTermResized() { return false; }
|
|
261
260
|
export function tuiModalHandleTermExited() { return false; }
|
|
262
261
|
export function tuiModalHandleTermClosed() { return false; }
|
|
262
|
+
|
|
263
|
+
// Live theme update for the attention modal. Called by theme.js when
|
|
264
|
+
// the user switches themes. Also retints the surrounding modal chrome
|
|
265
|
+
// so the frame doesn't stay black when a light theme is active.
|
|
266
|
+
export function setTuiAttentionTheme(xtermTheme) {
|
|
267
|
+
if (modalXterm) {
|
|
268
|
+
try { modalXterm.options.theme = xtermTheme; } catch (e) {}
|
|
269
|
+
}
|
|
270
|
+
if (modalEl && xtermTheme && xtermTheme.background) {
|
|
271
|
+
var bodyEl = modalEl.querySelector(".tui-modal-body");
|
|
272
|
+
if (bodyEl) bodyEl.style.background = xtermTheme.background;
|
|
273
|
+
var frameEl = modalEl.querySelector(".tui-modal");
|
|
274
|
+
if (frameEl) frameEl.style.background = xtermTheme.background;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Live font update for the attention modal. Refit after applying so
|
|
279
|
+
// cell metrics + PTY cols/rows stay in sync.
|
|
280
|
+
onTerminalFontChange(function (family, size) {
|
|
281
|
+
if (!modalXterm) return;
|
|
282
|
+
try {
|
|
283
|
+
if (family) modalXterm.options.fontFamily = family;
|
|
284
|
+
if (size) modalXterm.options.fontSize = size;
|
|
285
|
+
} catch (e) {}
|
|
286
|
+
scheduleModalResize();
|
|
287
|
+
});
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
import { refreshIcons } from './icons.js';
|
|
5
5
|
import { showToast } from './utils.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getChatLayout, setChatLayout } from './theme.js';
|
|
7
|
+
import { applyTerminalFont, getTerminalFontFamily, getTerminalFontSize } from './terminal-prefs.js';
|
|
7
8
|
import { showEmailSetupModal, getEmailAccountListCache } from './context-sources.js';
|
|
8
9
|
import { setSTTLang } from './stt.js';
|
|
9
10
|
import { userAvatarUrl } from './avatar.js';
|
|
@@ -230,21 +231,49 @@ export function initUserSettings(appCtx) {
|
|
|
230
231
|
});
|
|
231
232
|
}
|
|
232
233
|
|
|
233
|
-
// Theme
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
234
|
+
// Theme picker lives on the user island button now (palette icon).
|
|
235
|
+
// No appearance-section mount required.
|
|
236
|
+
|
|
237
|
+
// Terminal font: family select + optional custom input + size number.
|
|
238
|
+
// Saves to server on change and immediately applies to every open
|
|
239
|
+
// xterm via applyTerminalFont (terminal-prefs.js fanout).
|
|
240
|
+
var tfSelect = document.getElementById('us-term-font-family');
|
|
241
|
+
var tfCustom = document.getElementById('us-term-font-family-custom');
|
|
242
|
+
var tfSize = document.getElementById('us-term-font-size');
|
|
243
|
+
|
|
244
|
+
function persistTermFont(family, size) {
|
|
245
|
+
fetch('/api/user/terminal-font', {
|
|
246
|
+
method: 'PUT',
|
|
247
|
+
headers: { 'Content-Type': 'application/json' },
|
|
248
|
+
body: JSON.stringify({ family: family, size: size }),
|
|
249
|
+
}).catch(function () {});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (tfSelect && tfCustom && tfSize) {
|
|
253
|
+
tfSelect.addEventListener('change', function () {
|
|
254
|
+
if (tfSelect.value === '__custom__') {
|
|
255
|
+
tfCustom.classList.remove('hidden');
|
|
256
|
+
tfCustom.focus();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
tfCustom.classList.add('hidden');
|
|
260
|
+
applyTerminalFont(tfSelect.value, undefined);
|
|
261
|
+
persistTermFont(tfSelect.value, undefined);
|
|
262
|
+
});
|
|
263
|
+
tfCustom.addEventListener('change', function () {
|
|
264
|
+
var v = tfCustom.value.trim();
|
|
265
|
+
if (!v) return;
|
|
266
|
+
applyTerminalFont(v, undefined);
|
|
267
|
+
persistTermFont(v, undefined);
|
|
268
|
+
});
|
|
269
|
+
tfSize.addEventListener('change', function () {
|
|
270
|
+
var n = Number(tfSize.value);
|
|
271
|
+
if (!isFinite(n)) return;
|
|
272
|
+
n = Math.max(9, Math.min(32, Math.round(n)));
|
|
273
|
+
tfSize.value = String(n);
|
|
274
|
+
applyTerminalFont(undefined, n);
|
|
275
|
+
persistTermFont(undefined, n);
|
|
276
|
+
});
|
|
248
277
|
}
|
|
249
278
|
|
|
250
279
|
// Layout switcher (Bubble / Channel)
|
|
@@ -460,18 +489,37 @@ function populateAccount() {
|
|
|
460
489
|
// Hide account section in single-user mode (no username)
|
|
461
490
|
var accountNav = settingsEl.querySelector('[data-section="us-account"]');
|
|
462
491
|
if (accountNav) accountNav.style.display = data.username ? '' : 'none';
|
|
492
|
+
// Terminal font: seed in-memory prefs from server, populate inputs.
|
|
493
|
+
if (data.terminalFont && typeof data.terminalFont === "object") {
|
|
494
|
+
applyTerminalFont(data.terminalFont.family, data.terminalFont.size);
|
|
495
|
+
}
|
|
496
|
+
var tfSelectEl = document.getElementById('us-term-font-family');
|
|
497
|
+
var tfCustomEl = document.getElementById('us-term-font-family-custom');
|
|
498
|
+
var tfSizeEl = document.getElementById('us-term-font-size');
|
|
499
|
+
if (tfSelectEl && tfCustomEl) {
|
|
500
|
+
var fam = getTerminalFontFamily();
|
|
501
|
+
var matched = false;
|
|
502
|
+
for (var oi = 0; oi < tfSelectEl.options.length; oi++) {
|
|
503
|
+
if (tfSelectEl.options[oi].value === fam) {
|
|
504
|
+
tfSelectEl.value = fam;
|
|
505
|
+
matched = true;
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (!matched) {
|
|
510
|
+
tfSelectEl.value = '__custom__';
|
|
511
|
+
tfCustomEl.value = fam;
|
|
512
|
+
tfCustomEl.classList.remove('hidden');
|
|
513
|
+
} else {
|
|
514
|
+
tfCustomEl.classList.add('hidden');
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (tfSizeEl) tfSizeEl.value = String(getTerminalFontSize());
|
|
463
518
|
// Auto-continue toggle
|
|
464
519
|
var acToggle = document.getElementById('us-auto-continue');
|
|
465
520
|
if (acToggle) acToggle.checked = !!data.autoContinueOnRateLimit;
|
|
466
|
-
// Theme
|
|
467
|
-
|
|
468
|
-
if (tSwitcher) {
|
|
469
|
-
var currentMode = (getCurrentTheme() && getCurrentTheme().variant) || 'dark';
|
|
470
|
-
var tBtns = tSwitcher.querySelectorAll('.layout-option');
|
|
471
|
-
for (var ti = 0; ti < tBtns.length; ti++) {
|
|
472
|
-
tBtns[ti].classList.toggle('selected', tBtns[ti].dataset.theme === currentMode);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
521
|
+
// Theme picker self-syncs active selection via theme.js's applyTheme
|
|
522
|
+
// change callbacks (updatePickerActive). Nothing to do here.
|
|
475
523
|
// Layout switcher: sync from server response
|
|
476
524
|
// Sync mate onboarding state from server
|
|
477
525
|
if (data.mateOnboardingShown) {
|
package/lib/server-settings.js
CHANGED
|
@@ -30,6 +30,7 @@ function attachSettings(ctx) {
|
|
|
30
30
|
profile.chatLayout = mu.chatLayout || "channel";
|
|
31
31
|
profile.mateOnboardingShown = !!mu.mateOnboardingShown;
|
|
32
32
|
profile.matesEnabled = mu.matesEnabled !== false;
|
|
33
|
+
try { profile.terminalFont = users.getTerminalFont(mu.id); } catch (e) {}
|
|
33
34
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
34
35
|
res.end(JSON.stringify(profile));
|
|
35
36
|
return true;
|
|
@@ -52,6 +53,14 @@ function attachSettings(ctx) {
|
|
|
52
53
|
profile.chatLayout = dc.chatLayout || "channel";
|
|
53
54
|
profile.mateOnboardingShown = !!dc.mateOnboardingShown;
|
|
54
55
|
profile.matesEnabled = dc.matesEnabled !== false;
|
|
56
|
+
if (dc.terminalFont && typeof dc.terminalFont === "object") {
|
|
57
|
+
profile.terminalFont = {
|
|
58
|
+
family: dc.terminalFont.family || "'SF Mono', Menlo, Monaco, 'Courier New', monospace",
|
|
59
|
+
size: typeof dc.terminalFont.size === "number" ? dc.terminalFont.size : 13,
|
|
60
|
+
};
|
|
61
|
+
} else {
|
|
62
|
+
profile.terminalFont = { family: "'SF Mono', Menlo, Monaco, 'Courier New', monospace", size: 13 };
|
|
63
|
+
}
|
|
55
64
|
}
|
|
56
65
|
// Check if custom avatar file exists
|
|
57
66
|
try {
|
|
@@ -514,6 +523,52 @@ function attachSettings(ctx) {
|
|
|
514
523
|
return true;
|
|
515
524
|
}
|
|
516
525
|
|
|
526
|
+
// PUT /api/user/terminal-font
|
|
527
|
+
// Body: { family?: string, size?: number }
|
|
528
|
+
// Per-user terminal font preferences shared across every xterm in
|
|
529
|
+
// the Clay UI (bottom panel, Claude TUI session view, attention
|
|
530
|
+
// modal). Single-user mode persists into daemon config; multi-user
|
|
531
|
+
// mode persists into users.json.
|
|
532
|
+
if (req.method === "PUT" && fullUrl === "/api/user/terminal-font") {
|
|
533
|
+
var isMultiUserTF = users.isMultiUser();
|
|
534
|
+
var muTF = getMultiUserFromReq(req);
|
|
535
|
+
var bodyTF = "";
|
|
536
|
+
req.on("data", function (chunk) { bodyTF += chunk; });
|
|
537
|
+
req.on("end", function () {
|
|
538
|
+
try {
|
|
539
|
+
var dataTF = JSON.parse(bodyTF || "{}");
|
|
540
|
+
if (isMultiUserTF) {
|
|
541
|
+
if (!muTF) {
|
|
542
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
543
|
+
res.end('{"error":"unauthorized"}');
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
var resultTF = users.setTerminalFont(muTF.id, dataTF.family, dataTF.size);
|
|
547
|
+
if (resultTF.error) {
|
|
548
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
549
|
+
res.end(JSON.stringify({ error: resultTF.error }));
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
553
|
+
res.end(JSON.stringify({ ok: true, terminalFont: resultTF.terminalFont }));
|
|
554
|
+
} else {
|
|
555
|
+
if (typeof opts.onSetTerminalFont === "function") {
|
|
556
|
+
var saved = opts.onSetTerminalFont(dataTF.family, dataTF.size);
|
|
557
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
558
|
+
res.end(JSON.stringify({ ok: true, terminalFont: saved }));
|
|
559
|
+
} else {
|
|
560
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
561
|
+
res.end('{"error":"single-user font update unsupported"}');
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
} catch (e) {
|
|
565
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
566
|
+
res.end('{"error":"Invalid request"}');
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
|
|
517
572
|
// POST /api/user/mate-onboarded
|
|
518
573
|
if (req.method === "POST" && fullUrl === "/api/user/mate-onboarded") {
|
|
519
574
|
var isMultiUser = users.isMultiUser();
|
package/lib/users-preferences.js
CHANGED
|
@@ -405,6 +405,49 @@ function attachPreferences(deps) {
|
|
|
405
405
|
return { error: "User not found" };
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
// --- Terminal font preferences ---
|
|
409
|
+
//
|
|
410
|
+
// Per-user font family + size for every xterm in Clay (bottom panel
|
|
411
|
+
// shell, Claude TUI session view, TUI attention modal). Stored as a
|
|
412
|
+
// single object so the two values stay together.
|
|
413
|
+
|
|
414
|
+
var DEFAULT_TERM_FONT_FAMILY = "'SF Mono', Menlo, Monaco, 'Courier New', monospace";
|
|
415
|
+
var DEFAULT_TERM_FONT_SIZE = 13;
|
|
416
|
+
var MIN_TERM_FONT_SIZE = 9;
|
|
417
|
+
var MAX_TERM_FONT_SIZE = 32;
|
|
418
|
+
|
|
419
|
+
function getTerminalFont(userId) {
|
|
420
|
+
var data = loadUsers();
|
|
421
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
422
|
+
if (data.users[i].id === userId) {
|
|
423
|
+
var tf = data.users[i].terminalFont || {};
|
|
424
|
+
return {
|
|
425
|
+
family: (typeof tf.family === "string" && tf.family.trim()) ? tf.family : DEFAULT_TERM_FONT_FAMILY,
|
|
426
|
+
size: (typeof tf.size === "number" && tf.size >= MIN_TERM_FONT_SIZE && tf.size <= MAX_TERM_FONT_SIZE) ? tf.size : DEFAULT_TERM_FONT_SIZE,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return { family: DEFAULT_TERM_FONT_FAMILY, size: DEFAULT_TERM_FONT_SIZE };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function setTerminalFont(userId, family, size) {
|
|
434
|
+
var data = loadUsers();
|
|
435
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
436
|
+
if (data.users[i].id === userId) {
|
|
437
|
+
var current = data.users[i].terminalFont || {};
|
|
438
|
+
var nextFamily = (typeof family === "string" && family.trim()) ? family.trim().slice(0, 200) : current.family;
|
|
439
|
+
var nextSize = current.size;
|
|
440
|
+
if (typeof size === "number" && size >= MIN_TERM_FONT_SIZE && size <= MAX_TERM_FONT_SIZE) {
|
|
441
|
+
nextSize = Math.round(size);
|
|
442
|
+
}
|
|
443
|
+
data.users[i].terminalFont = { family: nextFamily, size: nextSize };
|
|
444
|
+
saveUsers(data);
|
|
445
|
+
return { ok: true, terminalFont: data.users[i].terminalFont };
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return { error: "User not found" };
|
|
449
|
+
}
|
|
450
|
+
|
|
408
451
|
// --- What's New seen ids ---
|
|
409
452
|
//
|
|
410
453
|
// Per-user list of dismissed "What's New" entry ids. The whats-new
|
|
@@ -477,6 +520,8 @@ function attachPreferences(deps) {
|
|
|
477
520
|
setToolPalette: setToolPalette,
|
|
478
521
|
getWhatsNewSeenIds: getWhatsNewSeenIds,
|
|
479
522
|
markWhatsNewSeen: markWhatsNewSeen,
|
|
523
|
+
getTerminalFont: getTerminalFont,
|
|
524
|
+
setTerminalFont: setTerminalFont,
|
|
480
525
|
setMateOnboarded: setMateOnboarded,
|
|
481
526
|
};
|
|
482
527
|
}
|
package/lib/users.js
CHANGED
|
@@ -426,6 +426,8 @@ var getToolPalettes = preferences.getToolPalettes;
|
|
|
426
426
|
var setToolPalette = preferences.setToolPalette;
|
|
427
427
|
var getWhatsNewSeenIds = preferences.getWhatsNewSeenIds;
|
|
428
428
|
var markWhatsNewSeen = preferences.markWhatsNewSeen;
|
|
429
|
+
var getTerminalFont = preferences.getTerminalFont;
|
|
430
|
+
var setTerminalFont = preferences.setTerminalFont;
|
|
429
431
|
var setMateOnboarded = preferences.setMateOnboarded;
|
|
430
432
|
|
|
431
433
|
module.exports = {
|
|
@@ -493,6 +495,8 @@ module.exports = {
|
|
|
493
495
|
setToolPalette: setToolPalette,
|
|
494
496
|
getWhatsNewSeenIds: getWhatsNewSeenIds,
|
|
495
497
|
markWhatsNewSeen: markWhatsNewSeen,
|
|
498
|
+
getTerminalFont: getTerminalFont,
|
|
499
|
+
setTerminalFont: setTerminalFont,
|
|
496
500
|
getDeletedBuiltinKeys: getDeletedBuiltinKeys,
|
|
497
501
|
addDeletedBuiltinKey: addDeletedBuiltinKey,
|
|
498
502
|
removeDeletedBuiltinKey: removeDeletedBuiltinKey,
|
package/package.json
CHANGED