clay-server 2.39.0 → 2.40.0-beta.2
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/claude-hook-installer.js +71 -39
- package/lib/public/app.js +4 -0
- package/lib/public/css/menus.css +21 -0
- package/lib/public/css/mobile-nav.css +36 -0
- package/lib/public/css/user-settings.css +141 -29
- package/lib/public/index.html +7 -19
- package/lib/public/modules/app-messages.js +9 -2
- package/lib/public/modules/app-panels.js +39 -14
- package/lib/public/modules/header-tui-font.js +233 -0
- package/lib/public/modules/input.js +29 -3
- package/lib/public/modules/profile.js +9 -0
- package/lib/public/modules/session-tui-view.js +15 -0
- package/lib/public/modules/sidebar-mobile.js +25 -6
- package/lib/public/modules/user-settings.js +8 -65
- package/lib/yoke/adapters/codex.js +6 -5
- package/package.json +1 -1
|
@@ -24,8 +24,17 @@ var CLAY_HOOK_MARKER = "clay:tui-notify";
|
|
|
24
24
|
// Allow-patterns Clay manages in ~/.claude/settings.json `permissions.allow`.
|
|
25
25
|
// These mirror sdk-bridge.js `checkToolWhitelist` so TUI sessions get the
|
|
26
26
|
// same auto-approval convenience as SDK sessions. Conservative: only
|
|
27
|
-
// commands that stay safe even under Claude Code's
|
|
28
|
-
// commands like `ls && rm -rf /` would otherwise sneak past
|
|
27
|
+
// commands that stay safe even under Claude Code's wildcard matching
|
|
28
|
+
// (compound commands like `ls && rm -rf /` would otherwise sneak past
|
|
29
|
+
// `Bash(ls *)`).
|
|
30
|
+
//
|
|
31
|
+
// Pattern syntax note: claude 2.x uses space-wildcard form `Bash(cmd *)`
|
|
32
|
+
// for prefix matching. The older colon form `Bash(cmd:*)` is flagged as
|
|
33
|
+
// legacy in the CLI and no longer reliably matches argument-bearing
|
|
34
|
+
// commands - the TUI was still prompting for read-only invocations like
|
|
35
|
+
// `ls -la` even with the allow-list installed. Patterns below use the
|
|
36
|
+
// modern form. The bare command (e.g. `Bash(ls)`) is included alongside
|
|
37
|
+
// the wildcard so zero-arg invocations are also covered.
|
|
29
38
|
//
|
|
30
39
|
// User-authored entries are preserved -- on re-install we only strip
|
|
31
40
|
// patterns that appear in this constant list.
|
|
@@ -48,46 +57,48 @@ var CLAY_MANAGED_ALLOW = [
|
|
|
48
57
|
// Safe Bash commands. Match the curated set in sdk-bridge.js's
|
|
49
58
|
// safeBashCommands, restricted to ones whose pure read-only behavior
|
|
50
59
|
// doesn't depend on argument shape.
|
|
51
|
-
"Bash(ls
|
|
52
|
-
"Bash(file
|
|
53
|
-
"Bash(du
|
|
54
|
-
"Bash(basename
|
|
55
|
-
"Bash(grep
|
|
56
|
-
"Bash(fgrep
|
|
57
|
-
"Bash(which
|
|
58
|
-
"Bash(echo
|
|
59
|
-
"Bash(pwd
|
|
60
|
-
"Bash(date
|
|
61
|
-
"Bash(arch
|
|
62
|
-
"Bash(lsb_release
|
|
60
|
+
"Bash(ls)", "Bash(ls *)", "Bash(cat *)", "Bash(head *)", "Bash(tail *)", "Bash(wc *)",
|
|
61
|
+
"Bash(file *)", "Bash(stat *)", "Bash(find *)", "Bash(tree)", "Bash(tree *)",
|
|
62
|
+
"Bash(du *)", "Bash(df)", "Bash(df *)", "Bash(readlink *)", "Bash(realpath *)",
|
|
63
|
+
"Bash(basename *)", "Bash(dirname *)",
|
|
64
|
+
"Bash(grep *)", "Bash(rg *)", "Bash(ag *)", "Bash(ack *)",
|
|
65
|
+
"Bash(fgrep *)", "Bash(egrep *)",
|
|
66
|
+
"Bash(which *)", "Bash(type *)", "Bash(whereis *)",
|
|
67
|
+
"Bash(echo)", "Bash(echo *)", "Bash(printf *)", "Bash(env)", "Bash(env *)", "Bash(printenv)", "Bash(printenv *)",
|
|
68
|
+
"Bash(pwd)", "Bash(whoami)", "Bash(id)", "Bash(id *)", "Bash(groups)", "Bash(groups *)",
|
|
69
|
+
"Bash(date)", "Bash(date *)", "Bash(uname)", "Bash(uname *)", "Bash(hostname)", "Bash(uptime)",
|
|
70
|
+
"Bash(arch)", "Bash(nproc)", "Bash(free)", "Bash(free *)",
|
|
71
|
+
"Bash(lsb_release *)", "Bash(sw_vers)", "Bash(sw_vers *)", "Bash(locale)", "Bash(locale *)",
|
|
63
72
|
// Git read-only subcommands. Listed individually so write subcommands
|
|
64
73
|
// (commit, push, reset, etc.) still prompt.
|
|
65
|
-
"Bash(git status
|
|
66
|
-
"Bash(git
|
|
67
|
-
"Bash(git
|
|
68
|
-
"Bash(git
|
|
69
|
-
"Bash(git
|
|
74
|
+
"Bash(git status)", "Bash(git status *)", "Bash(git log)", "Bash(git log *)",
|
|
75
|
+
"Bash(git diff)", "Bash(git diff *)", "Bash(git show)", "Bash(git show *)",
|
|
76
|
+
"Bash(git branch)", "Bash(git branch *)", "Bash(git tag)", "Bash(git tag *)",
|
|
77
|
+
"Bash(git remote)", "Bash(git remote *)", "Bash(git config --get *)",
|
|
78
|
+
"Bash(git rev-parse *)", "Bash(git ls-files)", "Bash(git ls-files *)",
|
|
79
|
+
"Bash(git blame *)", "Bash(git describe)", "Bash(git describe *)",
|
|
70
80
|
// Package manager read-only subcommands
|
|
71
|
-
"Bash(npm list
|
|
72
|
-
"Bash(npm
|
|
81
|
+
"Bash(npm list)", "Bash(npm list *)", "Bash(npm ls)", "Bash(npm ls *)",
|
|
82
|
+
"Bash(npm view *)", "Bash(npm outdated)", "Bash(npm outdated *)",
|
|
83
|
+
"Bash(npm config get *)", "Bash(yarn list)", "Bash(yarn list *)", "Bash(pnpm list)", "Bash(pnpm list *)",
|
|
73
84
|
// Version checks
|
|
74
|
-
"Bash(node --version
|
|
75
|
-
"Bash(python3 --version
|
|
85
|
+
"Bash(node --version)", "Bash(npm --version)", "Bash(python --version)",
|
|
86
|
+
"Bash(python3 --version)", "Bash(go version)", "Bash(ruby --version)",
|
|
76
87
|
// Text processing (pure stdin/stdout)
|
|
77
|
-
"Bash(jq
|
|
78
|
-
"Bash(cut
|
|
79
|
-
"Bash(paste
|
|
80
|
-
"Bash(nl
|
|
88
|
+
"Bash(jq *)", "Bash(yq *)", "Bash(sort)", "Bash(sort *)", "Bash(uniq)", "Bash(uniq *)",
|
|
89
|
+
"Bash(cut *)", "Bash(tr *)", "Bash(awk *)", "Bash(sed *)",
|
|
90
|
+
"Bash(paste *)", "Bash(column *)", "Bash(rev)", "Bash(rev *)", "Bash(tac)", "Bash(tac *)",
|
|
91
|
+
"Bash(nl)", "Bash(nl *)", "Bash(fmt)", "Bash(fmt *)", "Bash(comm *)", "Bash(join *)",
|
|
81
92
|
// Comparison / hashing (read-only)
|
|
82
|
-
"Bash(diff
|
|
83
|
-
"Bash(sha1sum
|
|
84
|
-
"Bash(xxd
|
|
93
|
+
"Bash(diff *)", "Bash(cmp *)", "Bash(md5sum *)", "Bash(sha256sum *)",
|
|
94
|
+
"Bash(sha1sum *)", "Bash(shasum *)", "Bash(cksum *)", "Bash(base64)", "Bash(base64 *)",
|
|
95
|
+
"Bash(xxd *)", "Bash(od *)", "Bash(hexdump *)",
|
|
85
96
|
// Calendar / math
|
|
86
|
-
"Bash(cal
|
|
97
|
+
"Bash(cal)", "Bash(cal *)", "Bash(bc)", "Bash(bc *)", "Bash(expr *)", "Bash(factor *)", "Bash(seq *)",
|
|
87
98
|
// Process / network introspection (read-only)
|
|
88
|
-
"Bash(ps
|
|
89
|
-
"Bash(netstat
|
|
90
|
-
"Bash(dig
|
|
99
|
+
"Bash(ps)", "Bash(ps *)", "Bash(top)", "Bash(top *)", "Bash(htop)", "Bash(pgrep *)", "Bash(lsof)", "Bash(lsof *)",
|
|
100
|
+
"Bash(netstat)", "Bash(netstat *)", "Bash(ss)", "Bash(ss *)", "Bash(ifconfig)", "Bash(ifconfig *)", "Bash(ip *)",
|
|
101
|
+
"Bash(dig *)", "Bash(nslookup *)", "Bash(host *)",
|
|
91
102
|
];
|
|
92
103
|
|
|
93
104
|
function buildHookCommand(notifyUrl) {
|
|
@@ -186,11 +197,27 @@ function installNotificationHook(opts) {
|
|
|
186
197
|
return { installed: installed, errors: errors };
|
|
187
198
|
}
|
|
188
199
|
|
|
200
|
+
// Pattern shapes Clay used to install in older versions. Listed here so an
|
|
201
|
+
// upgrade can strip them from settings.json before re-installing the modern
|
|
202
|
+
// list. Without this, users who already had Clay running would end up with
|
|
203
|
+
// stale legacy-syntax entries next to the new ones - mostly harmless but
|
|
204
|
+
// noisy, and confusing if anyone reads the file.
|
|
205
|
+
function isLegacyClayPattern(p) {
|
|
206
|
+
if (typeof p !== "string") return false;
|
|
207
|
+
// Old colon-prefix Bash patterns: Bash(ls:*), Bash(git status:*), etc.
|
|
208
|
+
// Modern patterns use space-asterisk and never contain ":*", so this is
|
|
209
|
+
// a safe identifier for old Clay entries even though it can't tell apart
|
|
210
|
+
// user-authored colon patterns. The risk is judged acceptable because
|
|
211
|
+
// claude flags the colon form as legacy and is phasing it out anyway.
|
|
212
|
+
if (p.indexOf("Bash(") === 0 && p.indexOf(":*)") !== -1) return true;
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
189
216
|
// Merge Clay's managed allow-list into permissions.allow without disturbing
|
|
190
217
|
// the user's own entries. We identify "ours" by membership in
|
|
191
|
-
// CLAY_MANAGED_ALLOW
|
|
192
|
-
//
|
|
193
|
-
// survive unchanged
|
|
218
|
+
// CLAY_MANAGED_ALLOW (current) or isLegacyClayPattern (previous shape):
|
|
219
|
+
// re-install strips both, then re-inserts the current list. User-authored
|
|
220
|
+
// patterns survive unchanged.
|
|
194
221
|
function mergeAllowList(settingsPath, patterns) {
|
|
195
222
|
var data = readSettings(settingsPath);
|
|
196
223
|
if (!data.permissions || typeof data.permissions !== "object") data.permissions = {};
|
|
@@ -199,8 +226,13 @@ function mergeAllowList(settingsPath, patterns) {
|
|
|
199
226
|
var managedSet = {};
|
|
200
227
|
for (var i = 0; i < CLAY_MANAGED_ALLOW.length; i++) managedSet[CLAY_MANAGED_ALLOW[i]] = true;
|
|
201
228
|
|
|
202
|
-
// Strip prior Clay-managed entries
|
|
203
|
-
|
|
229
|
+
// Strip prior Clay-managed entries (current shape + legacy colon shape),
|
|
230
|
+
// then append the fresh list.
|
|
231
|
+
var preserved = allow.filter(function (p) {
|
|
232
|
+
if (managedSet[p]) return false;
|
|
233
|
+
if (isLegacyClayPattern(p)) return false;
|
|
234
|
+
return true;
|
|
235
|
+
});
|
|
204
236
|
var next = preserved.concat(patterns);
|
|
205
237
|
|
|
206
238
|
var before = JSON.stringify(allow);
|
package/lib/public/app.js
CHANGED
|
@@ -65,6 +65,7 @@ import { initLoopWizard, openRalphWizard as _loopOpenRalphWizard, closeRalphWiza
|
|
|
65
65
|
import { initAppNotifications, handleNotificationsState as _notifHandleState, handleNotificationCreated as _notifHandleCreated, handleNotificationDismissed as _notifHandleDismissed, handleNotificationDismissedAll as _notifHandleDismissedAll } from './modules/app-notifications.js';
|
|
66
66
|
import { initWhatsNew, handleWhatsNewState as _wnHandleState, handleWhatsNewSeenResult as _wnHandleSeenResult } from './modules/whats-new.js';
|
|
67
67
|
import { initWhatsNewArticle, openArticle as openWhatsNewArticle } from './modules/whats-new-article.js';
|
|
68
|
+
import { initHeaderTuiFont } from './modules/header-tui-font.js';
|
|
68
69
|
import { createStore, store } from './modules/store.js';
|
|
69
70
|
import { initPanels, updateConfigChip as _panUpdateConfigChip, getModelEffortLevels as _panGetModelEffortLevels, accumulateUsage as _panAccumulateUsage, updateUsagePanel as _panUpdateUsagePanel, resetUsage as _panResetUsage, toggleUsagePanel as _panToggleUsagePanel, formatTokens as _panFormatTokens, updateStatusPanel as _panUpdateStatusPanel, requestProcessStats as _panRequestProcessStats, toggleStatusPanel as _panToggleStatusPanel, accumulateContext as _panAccumulateContext, updateContextPanel as _panUpdateContextPanel, resetContext as _panResetContext, resetContextData as _panResetContextData, minimizeContext as _panMinimizeContext, expandContext as _panExpandContext, toggleContextPanel as _panToggleContextPanel, getContextView as _panGetContextView, renderCtxPopover as _panRenderCtxPopover, hideCtxPopover as _panHideCtxPopover, formatBytes as _panFormatBytes, formatUptime as _panFormatUptime, getModelSupportsEffort as _panGetModelSupportsEffort, getSessionUsage, setSessionUsage, getContextData, setContextData, setContextView as _panSetContextView, applyContextView as _panApplyContextView } from './modules/app-panels.js';
|
|
70
71
|
import { initProjects, updateProjectList as _projUpdateProjectList, renderProjectList as _projRenderProjectList, renderTopbarPresence as _projRenderTopbarPresence, switchProject as _projSwitchProject, resetClientState as _projResetClientState, confirmRemoveProject as _projConfirmRemoveProject, handleRemoveProjectCheckResult as _projHandleRemoveProjectCheckResult, handleRemoveProjectResult as _projHandleRemoveProjectResult, openAddProjectModal as _projOpenAddProjectModal, closeAddProjectModal as _projCloseAddProjectModal, handleBrowseDirResult as _projHandleBrowseDirResult, handleAddProjectResult as _projHandleAddProjectResult, handleCloneProgress as _projHandleCloneProgress, showUpdateAvailable as _projShowUpdateAvailable, getCachedProjects, setCachedProjects, getCachedProjectCount, getCachedRemovedProjects, setCachedRemovedProjects } from './modules/app-projects.js';
|
|
@@ -604,6 +605,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
604
605
|
// --- Panels module ---
|
|
605
606
|
initPanels();
|
|
606
607
|
|
|
608
|
+
// --- Header TUI font controls (visible only when TUI session active) ---
|
|
609
|
+
initHeaderTuiFont();
|
|
610
|
+
|
|
607
611
|
// --- Rendering module ---
|
|
608
612
|
initRendering();
|
|
609
613
|
|
package/lib/public/css/menus.css
CHANGED
|
@@ -544,6 +544,27 @@
|
|
|
544
544
|
.config-radio-item:hover { background: rgba(var(--overlay-rgb),0.05); color: var(--text); }
|
|
545
545
|
.config-radio-item.active { color: var(--accent); font-weight: 600; }
|
|
546
546
|
|
|
547
|
+
/* Locked picker items (Codex GUI sessions after the first message: model
|
|
548
|
+
is bound to the thread, picks would be silently ignored). Active model
|
|
549
|
+
stays slightly more legible so the user can still see which one's live. */
|
|
550
|
+
.config-radio-item.locked {
|
|
551
|
+
cursor: not-allowed;
|
|
552
|
+
opacity: 0.5;
|
|
553
|
+
}
|
|
554
|
+
.config-radio-item.locked:hover {
|
|
555
|
+
background: transparent;
|
|
556
|
+
color: var(--text-secondary);
|
|
557
|
+
}
|
|
558
|
+
.config-radio-item.locked.active {
|
|
559
|
+
opacity: 0.85;
|
|
560
|
+
}
|
|
561
|
+
.config-model-hint {
|
|
562
|
+
padding: 4px 14px 10px;
|
|
563
|
+
font-size: 11px;
|
|
564
|
+
color: var(--text-dimmer);
|
|
565
|
+
font-style: italic;
|
|
566
|
+
}
|
|
567
|
+
|
|
547
568
|
/* Segmented control for effort */
|
|
548
569
|
.config-segmented {
|
|
549
570
|
display: flex;
|
|
@@ -358,6 +358,42 @@
|
|
|
358
358
|
background: rgba(var(--overlay-rgb), 0.06);
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
+
/* Vendor-split row: Claude + Codex side-by-side. Used inside
|
|
362
|
+
renderMobileSessionsInto so users can pick the vendor on mobile, the
|
|
363
|
+
same way the desktop sidebar already does. */
|
|
364
|
+
.mobile-session-new-row {
|
|
365
|
+
display: flex;
|
|
366
|
+
gap: 6px;
|
|
367
|
+
padding: 6px 8px 8px;
|
|
368
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.mobile-session-new-row .mobile-session-new-vendor {
|
|
372
|
+
flex: 1 1 0;
|
|
373
|
+
width: auto;
|
|
374
|
+
margin-bottom: 0;
|
|
375
|
+
padding: 10px 12px;
|
|
376
|
+
border: 1px solid var(--border-subtle);
|
|
377
|
+
border-radius: 8px;
|
|
378
|
+
background: rgba(var(--overlay-rgb), 0.03);
|
|
379
|
+
color: var(--text);
|
|
380
|
+
justify-content: center;
|
|
381
|
+
gap: 8px;
|
|
382
|
+
}
|
|
383
|
+
.mobile-session-new-row .mobile-session-new-vendor:last-of-type {
|
|
384
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
385
|
+
}
|
|
386
|
+
.mobile-session-new-row .mobile-session-new-vendor:active {
|
|
387
|
+
background: rgba(var(--overlay-rgb), 0.1);
|
|
388
|
+
}
|
|
389
|
+
.mobile-session-new-icon {
|
|
390
|
+
width: 18px;
|
|
391
|
+
height: 18px;
|
|
392
|
+
flex-shrink: 0;
|
|
393
|
+
border-radius: 4px;
|
|
394
|
+
object-fit: cover;
|
|
395
|
+
}
|
|
396
|
+
|
|
361
397
|
/* --- Chat filter bar (horizontal scroll chips) --- */
|
|
362
398
|
.mobile-chat-filter-bar {
|
|
363
399
|
display: flex;
|
|
@@ -860,45 +860,157 @@
|
|
|
860
860
|
}
|
|
861
861
|
}
|
|
862
862
|
|
|
863
|
-
/*
|
|
864
|
-
|
|
863
|
+
/* Header TUI font icon button - single trigger that opens a popover
|
|
864
|
+
with the full font picker + size stepper. Visible only when a TUI
|
|
865
|
+
session is active. Matches the title-bar icon button tone used by
|
|
866
|
+
#terminal-toggle-btn / #find-in-session-btn / #debate-pdf-btn so
|
|
867
|
+
it blends into the toolbar instead of looking like a raw button. */
|
|
868
|
+
.header-tui-font-btn {
|
|
865
869
|
display: flex;
|
|
866
870
|
align-items: center;
|
|
867
|
-
|
|
868
|
-
|
|
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);
|
|
871
|
+
justify-content: center;
|
|
872
|
+
background: none;
|
|
873
|
+
border: 1px solid transparent;
|
|
875
874
|
border-radius: 8px;
|
|
876
|
-
|
|
875
|
+
color: var(--text-dimmer);
|
|
876
|
+
cursor: pointer;
|
|
877
|
+
padding: 4px;
|
|
878
|
+
transition: color 0.15s, background 0.15s, border-color 0.15s;
|
|
879
|
+
}
|
|
880
|
+
.header-tui-font-btn .lucide { width: 15px; height: 15px; }
|
|
881
|
+
.header-tui-font-btn:hover {
|
|
882
|
+
color: var(--text-secondary);
|
|
883
|
+
background: rgba(var(--overlay-rgb), 0.04);
|
|
884
|
+
border-color: var(--border);
|
|
885
|
+
}
|
|
886
|
+
.header-tui-font-btn.active {
|
|
877
887
|
color: var(--text);
|
|
878
|
-
|
|
888
|
+
background: rgba(var(--overlay-rgb), 0.06);
|
|
889
|
+
border-color: var(--border);
|
|
879
890
|
}
|
|
880
|
-
.
|
|
881
|
-
|
|
882
|
-
|
|
891
|
+
.header-tui-font-btn.hidden { display: none; }
|
|
892
|
+
|
|
893
|
+
/* Floating popover that holds the font menu + size row. Anchored
|
|
894
|
+
under the title-bar button via JS positioning. */
|
|
895
|
+
.header-tui-font-popover {
|
|
896
|
+
position: fixed;
|
|
897
|
+
min-width: 220px;
|
|
898
|
+
max-height: 70vh;
|
|
899
|
+
overflow-y: auto;
|
|
900
|
+
padding: 8px;
|
|
901
|
+
background: var(--bg-alt);
|
|
883
902
|
border: 1px solid var(--border);
|
|
884
|
-
border-radius:
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
903
|
+
border-radius: 10px;
|
|
904
|
+
box-shadow: 0 8px 28px rgba(var(--shadow-rgb), 0.45);
|
|
905
|
+
z-index: 200;
|
|
906
|
+
opacity: 0;
|
|
907
|
+
transform: translateY(-4px);
|
|
908
|
+
pointer-events: none;
|
|
909
|
+
transition: opacity 0.12s ease, transform 0.12s ease;
|
|
910
|
+
}
|
|
911
|
+
.header-tui-font-popover.visible {
|
|
912
|
+
opacity: 1;
|
|
913
|
+
transform: translateY(0);
|
|
914
|
+
pointer-events: auto;
|
|
915
|
+
}
|
|
916
|
+
.header-tui-font-popover-label {
|
|
917
|
+
padding: 4px 8px 6px;
|
|
918
|
+
font-size: 11px;
|
|
919
|
+
font-weight: 600;
|
|
920
|
+
color: var(--text-dimmer);
|
|
921
|
+
text-transform: uppercase;
|
|
922
|
+
letter-spacing: 0.04em;
|
|
889
923
|
}
|
|
890
|
-
.
|
|
891
|
-
|
|
892
|
-
|
|
924
|
+
.header-tui-font-popover-list {
|
|
925
|
+
display: flex;
|
|
926
|
+
flex-direction: column;
|
|
927
|
+
gap: 1px;
|
|
928
|
+
}
|
|
929
|
+
.header-tui-font-popover-item {
|
|
930
|
+
display: flex;
|
|
931
|
+
align-items: center;
|
|
932
|
+
gap: 8px;
|
|
933
|
+
width: 100%;
|
|
893
934
|
padding: 7px 10px;
|
|
894
|
-
border:
|
|
895
|
-
|
|
896
|
-
background: var(--input-bg);
|
|
935
|
+
border: none;
|
|
936
|
+
background: transparent;
|
|
897
937
|
color: var(--text);
|
|
898
938
|
font-size: 13px;
|
|
899
|
-
text-align:
|
|
939
|
+
text-align: left;
|
|
940
|
+
cursor: pointer;
|
|
941
|
+
border-radius: 6px;
|
|
942
|
+
}
|
|
943
|
+
.header-tui-font-popover-item:hover {
|
|
944
|
+
background: rgba(var(--overlay-rgb), 0.06);
|
|
945
|
+
}
|
|
946
|
+
.header-tui-font-popover-check {
|
|
947
|
+
display: inline-flex;
|
|
948
|
+
width: 14px;
|
|
949
|
+
height: 14px;
|
|
950
|
+
align-items: center;
|
|
951
|
+
justify-content: center;
|
|
952
|
+
color: var(--accent);
|
|
953
|
+
opacity: 0;
|
|
954
|
+
flex-shrink: 0;
|
|
955
|
+
}
|
|
956
|
+
.header-tui-font-popover-item.active .header-tui-font-popover-check { opacity: 1; }
|
|
957
|
+
.header-tui-font-popover-item-label {
|
|
958
|
+
flex: 1 1 auto;
|
|
959
|
+
overflow: hidden;
|
|
960
|
+
text-overflow: ellipsis;
|
|
961
|
+
white-space: nowrap;
|
|
962
|
+
}
|
|
963
|
+
.header-tui-font-popover-divider {
|
|
964
|
+
height: 1px;
|
|
965
|
+
background: var(--border-subtle);
|
|
966
|
+
margin: 6px 4px;
|
|
967
|
+
}
|
|
968
|
+
.header-tui-font-popover-size {
|
|
969
|
+
display: flex;
|
|
970
|
+
align-items: center;
|
|
971
|
+
justify-content: space-between;
|
|
972
|
+
gap: 12px;
|
|
973
|
+
padding: 4px 10px 4px;
|
|
900
974
|
}
|
|
901
|
-
.
|
|
975
|
+
.header-tui-font-popover-size-label {
|
|
902
976
|
font-size: 13px;
|
|
903
|
-
color: var(--text-
|
|
977
|
+
color: var(--text-secondary);
|
|
978
|
+
}
|
|
979
|
+
.header-tui-font-popover-size-stepper {
|
|
980
|
+
display: inline-flex;
|
|
981
|
+
align-items: center;
|
|
982
|
+
height: 26px;
|
|
983
|
+
border: 1px solid var(--border-subtle);
|
|
984
|
+
border-radius: 6px;
|
|
985
|
+
overflow: hidden;
|
|
986
|
+
}
|
|
987
|
+
.header-tui-font-popover-size-stepper button {
|
|
988
|
+
width: 26px;
|
|
989
|
+
height: 24px;
|
|
990
|
+
border: none;
|
|
991
|
+
background: transparent;
|
|
992
|
+
color: var(--text-secondary);
|
|
993
|
+
font-size: 14px;
|
|
994
|
+
font-weight: 600;
|
|
995
|
+
cursor: pointer;
|
|
996
|
+
display: inline-flex;
|
|
997
|
+
align-items: center;
|
|
998
|
+
justify-content: center;
|
|
999
|
+
padding: 0;
|
|
1000
|
+
}
|
|
1001
|
+
.header-tui-font-popover-size-stepper button:hover {
|
|
1002
|
+
background: rgba(var(--overlay-rgb), 0.06);
|
|
1003
|
+
color: var(--text);
|
|
1004
|
+
}
|
|
1005
|
+
.header-tui-font-popover-size-stepper button:disabled {
|
|
1006
|
+
opacity: 0.35;
|
|
1007
|
+
cursor: default;
|
|
1008
|
+
}
|
|
1009
|
+
.header-tui-font-popover-size-val {
|
|
1010
|
+
min-width: 28px;
|
|
1011
|
+
text-align: center;
|
|
1012
|
+
font-size: 12px;
|
|
1013
|
+
color: var(--text);
|
|
1014
|
+
font-variant-numeric: tabular-nums;
|
|
1015
|
+
user-select: none;
|
|
904
1016
|
}
|
package/lib/public/index.html
CHANGED
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
19
19
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
20
20
|
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;500;700&family=Nunito:wght@700;800&display=swap" rel="stylesheet">
|
|
21
|
+
<!-- Monospace web fonts used by the TUI font picker in the title bar.
|
|
22
|
+
SF Mono is Apple-system-only (no Google Fonts entry) and
|
|
23
|
+
ui-monospace is a system keyword - both rely on the user's OS. -->
|
|
24
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Fira+Code:wght@400;500;700&family=Cascadia+Code:wght@400;500;700&family=IBM+Plex+Mono:wght@400;500;700&family=Source+Code+Pro:wght@400;500;700&display=swap" rel="stylesheet">
|
|
21
25
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5/css/xterm.min.css">
|
|
22
26
|
<script>
|
|
23
27
|
(function(){try{var k="clay-theme-vars",v=localStorage.getItem(k),r=document.documentElement;if(v){var o=JSON.parse(v),p;for(p in o)r.style.setProperty(p,o[p]);var vt=localStorage.getItem(k.replace("-vars","-variant"));if(vt==="light"){r.classList.add("light-theme");r.classList.remove("dark-theme")}else{r.classList.add("dark-theme");r.classList.remove("light-theme")}var m=document.querySelector('meta[name="theme-color"]');if(m&&o["--bg"])m.setAttribute("content",o["--bg"])}else{var sl=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches;if(sl){r.classList.add("light-theme");r.classList.remove("dark-theme")}}}catch(e){}})();
|
|
@@ -367,6 +371,9 @@
|
|
|
367
371
|
<div id="ralph-sticky" class="hidden"></div>
|
|
368
372
|
<div id="debate-sticky" class="hidden"></div>
|
|
369
373
|
<div class="status">
|
|
374
|
+
<button type="button" id="header-tui-font-btn" class="header-tui-font-btn hidden" title="Terminal font" aria-label="Terminal font">
|
|
375
|
+
<i data-lucide="type"></i>
|
|
376
|
+
</button>
|
|
370
377
|
<button id="debate-pdf-btn" class="hidden" title="Export debate as PDF"><i data-lucide="download"></i></button>
|
|
371
378
|
<button id="find-in-session-btn" title="Search in session (Ctrl+F)"><i data-lucide="search"></i></button>
|
|
372
379
|
<button id="terminal-toggle-btn" title="Terminal"><i data-lucide="square-terminal"></i><span id="terminal-count" class="hidden"></span></button>
|
|
@@ -1001,25 +1008,6 @@
|
|
|
1001
1008
|
<!-- Appearance section -->
|
|
1002
1009
|
<div class="us-section" data-section="us-appearance">
|
|
1003
1010
|
<h2>Appearance</h2>
|
|
1004
|
-
<div class="settings-card">
|
|
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>
|
|
1021
|
-
</div>
|
|
1022
|
-
</div>
|
|
1023
1011
|
<div class="settings-card">
|
|
1024
1012
|
<div class="settings-label" style="margin-bottom:10px;">Chat layout</div>
|
|
1025
1013
|
<div class="layout-switcher" id="us-layout-switcher">
|
|
@@ -168,10 +168,17 @@ export function processMessage(msg) {
|
|
|
168
168
|
if (!store.get('sessionIsProcessing')) {
|
|
169
169
|
applyDeadSessionTodoCompaction();
|
|
170
170
|
}
|
|
171
|
-
//
|
|
171
|
+
// Show the locked vendor toggle only when history exists AND the
|
|
172
|
+
// vendor isn't already committed. With a committed vendor,
|
|
173
|
+
// session_switched has already hidden the toggle and shown the
|
|
174
|
+
// small #active-vendor-indicator; re-showing the locked toggle
|
|
175
|
+
// here would duplicate the avatar next to the indicator.
|
|
172
176
|
var _hTotal = store.get('historyTotal') || 0;
|
|
173
177
|
var _vtw2 = document.getElementById("vendor-toggle-wrap");
|
|
174
|
-
if (_vtw2 && _hTotal > 0
|
|
178
|
+
if (_vtw2 && _hTotal > 0 && !store.get('currentVendor')) {
|
|
179
|
+
_vtw2.classList.remove("hidden");
|
|
180
|
+
_vtw2.classList.add("locked");
|
|
181
|
+
}
|
|
175
182
|
// Restore cached rich context usage BEFORE updateContextPanel runs
|
|
176
183
|
if (msg.contextUsage) {
|
|
177
184
|
store.set({ richContextUsage: msg.contextUsage });
|
|
@@ -108,6 +108,7 @@ var CODEX_WEBSEARCH_OPTIONS = [
|
|
|
108
108
|
var KNOWN_CONTEXT_WINDOWS = {
|
|
109
109
|
"opus-4-6": 1000000,
|
|
110
110
|
"claude-sonnet-4": 1000000,
|
|
111
|
+
"gpt-5.5": 1048576,
|
|
111
112
|
"gpt-5.4": 1048576,
|
|
112
113
|
"gpt-5.3": 1048576,
|
|
113
114
|
"gpt-5.2": 1048576,
|
|
@@ -176,16 +177,27 @@ function hasBeta(name) {
|
|
|
176
177
|
|
|
177
178
|
function rebuildModelList() {
|
|
178
179
|
if (!configModelList) return;
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
// schemas and cache reuse.
|
|
180
|
+
// Picker visibility by vendor+mode:
|
|
181
|
+
// Claude TUI -> shown (Claude TUI accepts mid-thread model swaps).
|
|
182
|
+
// Claude GUI -> hidden (Agent SDK binds model at session creation;
|
|
183
|
+
// changing mid-thread breaks tool schemas and cache reuse).
|
|
184
|
+
// Codex GUI -> shown but locked after the first message (Codex protocol
|
|
185
|
+
// binds model at thread creation; sdk-bridge setModel
|
|
186
|
+
// already stores into sm.currentModel when there's no
|
|
187
|
+
// active queryInstance, so picks made before the first
|
|
188
|
+
// message do take effect on thread start).
|
|
183
189
|
var modelSection = configModelList.parentElement;
|
|
184
190
|
var s = store.snap();
|
|
185
|
-
var
|
|
191
|
+
var vendor = s.currentVendor || "claude";
|
|
192
|
+
var hideModelPicker = s.activeSessionMode === "gui" && vendor === "claude";
|
|
186
193
|
if (modelSection) modelSection.style.display = hideModelPicker ? "none" : "";
|
|
187
194
|
configModelList.innerHTML = "";
|
|
188
195
|
if (hideModelPicker) return;
|
|
196
|
+
|
|
197
|
+
var lockedForCodex = vendor === "codex"
|
|
198
|
+
&& s.activeSessionMode === "gui"
|
|
199
|
+
&& !!s.sessionHasHistory;
|
|
200
|
+
|
|
189
201
|
var list = s.currentModels.length > 0 ? s.currentModels : (s.currentModel ? [{ value: s.currentModel, displayName: s.currentModel }] : []);
|
|
190
202
|
for (var i = 0; i < list.length; i++) {
|
|
191
203
|
var item = list[i];
|
|
@@ -195,19 +207,32 @@ function rebuildModelList() {
|
|
|
195
207
|
var btn = document.createElement("button");
|
|
196
208
|
btn.className = "config-radio-item";
|
|
197
209
|
if (value === s.currentModel) btn.classList.add("active");
|
|
210
|
+
if (lockedForCodex) btn.classList.add("locked");
|
|
198
211
|
btn.dataset.model = value;
|
|
199
212
|
btn.textContent = label;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
213
|
+
if (lockedForCodex) {
|
|
214
|
+
btn.disabled = true;
|
|
215
|
+
btn.title = "Model is locked after the first message in a Codex session. Start a new session to change it.";
|
|
216
|
+
} else {
|
|
217
|
+
btn.addEventListener("click", function () {
|
|
218
|
+
var model = this.dataset.model;
|
|
219
|
+
var ws = getWs();
|
|
220
|
+
if (ws && ws.readyState === 1) {
|
|
221
|
+
ws.send(JSON.stringify({ type: "set_model", model: model }));
|
|
222
|
+
}
|
|
223
|
+
configPopover.classList.add("hidden");
|
|
224
|
+
configChip.classList.remove("active");
|
|
225
|
+
});
|
|
226
|
+
}
|
|
209
227
|
configModelList.appendChild(btn);
|
|
210
228
|
}
|
|
229
|
+
|
|
230
|
+
if (lockedForCodex) {
|
|
231
|
+
var hint = document.createElement("div");
|
|
232
|
+
hint.className = "config-model-hint";
|
|
233
|
+
hint.textContent = "Locked after first message — start a new session to change.";
|
|
234
|
+
configModelList.appendChild(hint);
|
|
235
|
+
}
|
|
211
236
|
}
|
|
212
237
|
|
|
213
238
|
function rebuildModeList() {
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// header-tui-font.js
|
|
2
|
+
//
|
|
3
|
+
// Single icon button in the title bar (visible only while a TUI session
|
|
4
|
+
// is active) that opens a popover with terminal font settings: family
|
|
5
|
+
// picker (with per-item font preview) + size stepper. Values live in
|
|
6
|
+
// terminal-prefs.js and persist server-side via
|
|
7
|
+
// PUT /api/user/terminal-font.
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
applyTerminalFont,
|
|
11
|
+
getTerminalFontFamily,
|
|
12
|
+
getTerminalFontSize,
|
|
13
|
+
onTerminalFontChange,
|
|
14
|
+
} from './terminal-prefs.js';
|
|
15
|
+
|
|
16
|
+
var FONT_OPTIONS = [
|
|
17
|
+
{ label: "SF Mono", family: "'SF Mono', Menlo, Monaco, 'Courier New', monospace" },
|
|
18
|
+
{ label: "JetBrains Mono", family: "'JetBrains Mono', 'SF Mono', Menlo, monospace" },
|
|
19
|
+
{ label: "Fira Code", family: "'Fira Code', 'SF Mono', Menlo, monospace" },
|
|
20
|
+
{ label: "Cascadia Code", family: "'Cascadia Code', 'SF Mono', Menlo, monospace" },
|
|
21
|
+
{ label: "IBM Plex Mono", family: "'IBM Plex Mono', 'SF Mono', Menlo, monospace" },
|
|
22
|
+
{ label: "Source Code Pro", family: "'Source Code Pro', 'SF Mono', Menlo, monospace" },
|
|
23
|
+
{ label: "Roboto Mono", family: "'Roboto Mono', 'SF Mono', Menlo, monospace" },
|
|
24
|
+
{ label: "System mono", family: "ui-monospace, monospace" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
var MIN_SIZE = 9;
|
|
28
|
+
var MAX_SIZE = 32;
|
|
29
|
+
|
|
30
|
+
var btnEl = null;
|
|
31
|
+
var popoverEl = null;
|
|
32
|
+
var sizeValEl = null;
|
|
33
|
+
var sizeDecEl = null;
|
|
34
|
+
var sizeIncEl = null;
|
|
35
|
+
var menuItemEls = [];
|
|
36
|
+
var popoverOpen = false;
|
|
37
|
+
var outsideHandler = null;
|
|
38
|
+
var keyHandler = null;
|
|
39
|
+
var initialized = false;
|
|
40
|
+
|
|
41
|
+
function escapeHtml(s) {
|
|
42
|
+
return String(s)
|
|
43
|
+
.replace(/&/g, '&')
|
|
44
|
+
.replace(/</g, '<')
|
|
45
|
+
.replace(/>/g, '>')
|
|
46
|
+
.replace(/"/g, '"');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function persistTermFont(family, size) {
|
|
50
|
+
fetch('/api/user/terminal-font', {
|
|
51
|
+
method: 'PUT',
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify({ family: family, size: size }),
|
|
54
|
+
}).catch(function () {});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function syncSize() {
|
|
58
|
+
if (!sizeValEl) return;
|
|
59
|
+
var sz = getTerminalFontSize();
|
|
60
|
+
sizeValEl.textContent = String(sz);
|
|
61
|
+
if (sizeDecEl) sizeDecEl.disabled = sz <= MIN_SIZE;
|
|
62
|
+
if (sizeIncEl) sizeIncEl.disabled = sz >= MAX_SIZE;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function syncActiveFamily() {
|
|
66
|
+
var fam = getTerminalFontFamily();
|
|
67
|
+
for (var i = 0; i < menuItemEls.length; i++) {
|
|
68
|
+
var el = menuItemEls[i];
|
|
69
|
+
el.classList.toggle('active', el.dataset.family === fam);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildPopover() {
|
|
74
|
+
if (popoverEl) return popoverEl;
|
|
75
|
+
popoverEl = document.createElement('div');
|
|
76
|
+
popoverEl.className = 'header-tui-font-popover';
|
|
77
|
+
|
|
78
|
+
var label = document.createElement('div');
|
|
79
|
+
label.className = 'header-tui-font-popover-label';
|
|
80
|
+
label.textContent = 'Font';
|
|
81
|
+
popoverEl.appendChild(label);
|
|
82
|
+
|
|
83
|
+
var list = document.createElement('div');
|
|
84
|
+
list.className = 'header-tui-font-popover-list';
|
|
85
|
+
for (var i = 0; i < FONT_OPTIONS.length; i++) {
|
|
86
|
+
var opt = FONT_OPTIONS[i];
|
|
87
|
+
var item = document.createElement('button');
|
|
88
|
+
item.type = 'button';
|
|
89
|
+
item.className = 'header-tui-font-popover-item';
|
|
90
|
+
item.dataset.family = opt.family;
|
|
91
|
+
item.style.fontFamily = opt.family;
|
|
92
|
+
item.innerHTML =
|
|
93
|
+
'<span class="header-tui-font-popover-check"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg></span>' +
|
|
94
|
+
'<span class="header-tui-font-popover-item-label">' + escapeHtml(opt.label) + '</span>';
|
|
95
|
+
(function (family) {
|
|
96
|
+
item.addEventListener('click', function (e) {
|
|
97
|
+
e.stopPropagation();
|
|
98
|
+
applyTerminalFont(family, undefined);
|
|
99
|
+
persistTermFont(family, undefined);
|
|
100
|
+
});
|
|
101
|
+
})(opt.family);
|
|
102
|
+
list.appendChild(item);
|
|
103
|
+
menuItemEls.push(item);
|
|
104
|
+
}
|
|
105
|
+
popoverEl.appendChild(list);
|
|
106
|
+
|
|
107
|
+
var divider = document.createElement('div');
|
|
108
|
+
divider.className = 'header-tui-font-popover-divider';
|
|
109
|
+
popoverEl.appendChild(divider);
|
|
110
|
+
|
|
111
|
+
var sizeRow = document.createElement('div');
|
|
112
|
+
sizeRow.className = 'header-tui-font-popover-size';
|
|
113
|
+
sizeRow.innerHTML =
|
|
114
|
+
'<span class="header-tui-font-popover-size-label">Size</span>' +
|
|
115
|
+
'<div class="header-tui-font-popover-size-stepper">' +
|
|
116
|
+
'<button type="button" data-step="-1" title="Smaller">−</button>' +
|
|
117
|
+
'<span class="header-tui-font-popover-size-val">13</span>' +
|
|
118
|
+
'<button type="button" data-step="1" title="Larger">+</button>' +
|
|
119
|
+
'</div>';
|
|
120
|
+
popoverEl.appendChild(sizeRow);
|
|
121
|
+
|
|
122
|
+
sizeValEl = sizeRow.querySelector('.header-tui-font-popover-size-val');
|
|
123
|
+
var stepBtns = sizeRow.querySelectorAll('button');
|
|
124
|
+
sizeDecEl = stepBtns[0];
|
|
125
|
+
sizeIncEl = stepBtns[1];
|
|
126
|
+
sizeDecEl.addEventListener('click', function (e) {
|
|
127
|
+
e.stopPropagation();
|
|
128
|
+
var next = Math.max(MIN_SIZE, getTerminalFontSize() - 1);
|
|
129
|
+
applyTerminalFont(undefined, next);
|
|
130
|
+
persistTermFont(undefined, next);
|
|
131
|
+
});
|
|
132
|
+
sizeIncEl.addEventListener('click', function (e) {
|
|
133
|
+
e.stopPropagation();
|
|
134
|
+
var next = Math.min(MAX_SIZE, getTerminalFontSize() + 1);
|
|
135
|
+
applyTerminalFont(undefined, next);
|
|
136
|
+
persistTermFont(undefined, next);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
document.body.appendChild(popoverEl);
|
|
140
|
+
return popoverEl;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function positionPopover() {
|
|
144
|
+
if (!popoverEl || !btnEl) return;
|
|
145
|
+
var r = btnEl.getBoundingClientRect();
|
|
146
|
+
popoverEl.style.left = '0px';
|
|
147
|
+
popoverEl.style.top = '0px';
|
|
148
|
+
var pw = popoverEl.offsetWidth || 220;
|
|
149
|
+
var ph = popoverEl.offsetHeight || 340;
|
|
150
|
+
var vw = window.innerWidth;
|
|
151
|
+
var vh = window.innerHeight;
|
|
152
|
+
var left = r.right - pw;
|
|
153
|
+
if (left < 8) left = 8;
|
|
154
|
+
if (left + pw > vw - 8) left = vw - pw - 8;
|
|
155
|
+
var top = r.bottom + 8;
|
|
156
|
+
if (top + ph > vh - 8) {
|
|
157
|
+
var flipped = r.top - ph - 8;
|
|
158
|
+
if (flipped >= 8) top = flipped;
|
|
159
|
+
else top = Math.max(8, vh - ph - 8);
|
|
160
|
+
}
|
|
161
|
+
popoverEl.style.left = left + 'px';
|
|
162
|
+
popoverEl.style.top = top + 'px';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function openPopover() {
|
|
166
|
+
if (popoverOpen) return;
|
|
167
|
+
buildPopover();
|
|
168
|
+
syncActiveFamily();
|
|
169
|
+
syncSize();
|
|
170
|
+
popoverEl.classList.add('visible');
|
|
171
|
+
positionPopover();
|
|
172
|
+
popoverOpen = true;
|
|
173
|
+
if (btnEl) btnEl.classList.add('active');
|
|
174
|
+
|
|
175
|
+
outsideHandler = function (ev) {
|
|
176
|
+
if (!popoverEl) return;
|
|
177
|
+
if (popoverEl.contains(ev.target)) return;
|
|
178
|
+
if (btnEl && btnEl.contains(ev.target)) return;
|
|
179
|
+
closePopover();
|
|
180
|
+
};
|
|
181
|
+
document.addEventListener('mousedown', outsideHandler, true);
|
|
182
|
+
|
|
183
|
+
keyHandler = function (ev) {
|
|
184
|
+
if (ev.key === 'Escape') closePopover();
|
|
185
|
+
};
|
|
186
|
+
document.addEventListener('keydown', keyHandler, true);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function closePopover() {
|
|
190
|
+
if (!popoverOpen) return;
|
|
191
|
+
if (popoverEl) popoverEl.classList.remove('visible');
|
|
192
|
+
if (btnEl) btnEl.classList.remove('active');
|
|
193
|
+
popoverOpen = false;
|
|
194
|
+
if (outsideHandler) {
|
|
195
|
+
document.removeEventListener('mousedown', outsideHandler, true);
|
|
196
|
+
outsideHandler = null;
|
|
197
|
+
}
|
|
198
|
+
if (keyHandler) {
|
|
199
|
+
document.removeEventListener('keydown', keyHandler, true);
|
|
200
|
+
keyHandler = null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function initHeaderTuiFont() {
|
|
205
|
+
if (initialized) return;
|
|
206
|
+
btnEl = document.getElementById('header-tui-font-btn');
|
|
207
|
+
if (!btnEl) return;
|
|
208
|
+
btnEl.addEventListener('click', function (e) {
|
|
209
|
+
e.stopPropagation();
|
|
210
|
+
if (popoverOpen) closePopover();
|
|
211
|
+
else openPopover();
|
|
212
|
+
});
|
|
213
|
+
// Reflect external changes back into the popover (re-renders only if
|
|
214
|
+
// it's currently open).
|
|
215
|
+
onTerminalFontChange(function () {
|
|
216
|
+
if (popoverOpen) {
|
|
217
|
+
syncActiveFamily();
|
|
218
|
+
syncSize();
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
initialized = true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function showHeaderTuiFont() {
|
|
225
|
+
if (!btnEl) return;
|
|
226
|
+
btnEl.classList.remove('hidden');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function hideHeaderTuiFont() {
|
|
230
|
+
closePopover();
|
|
231
|
+
if (!btnEl) return;
|
|
232
|
+
btnEl.classList.add('hidden');
|
|
233
|
+
}
|
|
@@ -260,10 +260,36 @@ export function sendMessage() {
|
|
|
260
260
|
if (_selVendor) payload.vendor = _selVendor;
|
|
261
261
|
ctx.ws.send(JSON.stringify(payload));
|
|
262
262
|
|
|
263
|
-
//
|
|
263
|
+
// First message commits the vendor and bumps the session into
|
|
264
|
+
// has-history state. The server won't re-fire session_switched after a
|
|
265
|
+
// message, so mirror the vendor-toggle/active-indicator swap locally:
|
|
266
|
+
// hide the picker, show the small avatar next to the config chip.
|
|
267
|
+
// Bumping sessionHasHistory drives in-session lock states (e.g. the
|
|
268
|
+
// Codex model picker that becomes informational once the thread is
|
|
269
|
+
// bound to a model).
|
|
270
|
+
var _committedVendor = store.get('currentVendor');
|
|
264
271
|
var _vtw2 = document.getElementById("vendor-toggle-wrap");
|
|
265
|
-
|
|
266
|
-
|
|
272
|
+
var _avi = document.getElementById("active-vendor-indicator");
|
|
273
|
+
var _avIcon = document.getElementById("active-vendor-icon");
|
|
274
|
+
if (_committedVendor) {
|
|
275
|
+
if (_vtw2) {
|
|
276
|
+
_vtw2.classList.add("hidden");
|
|
277
|
+
_vtw2.classList.remove("locked");
|
|
278
|
+
}
|
|
279
|
+
if (_avi && _avIcon) {
|
|
280
|
+
_avIcon.src = _committedVendor === "codex" ? "/codex-avatar.png" : "/claude-code-avatar.png";
|
|
281
|
+
_avIcon.alt = _committedVendor === "codex" ? "Codex" : "Claude";
|
|
282
|
+
_avi.title = (_committedVendor === "codex" ? "Codex" : "Claude") + " session";
|
|
283
|
+
_avi.classList.remove("hidden");
|
|
284
|
+
}
|
|
285
|
+
} else if (_vtw2) {
|
|
286
|
+
// No committed vendor (defensive — shouldn't happen because the
|
|
287
|
+
// input is otherwise gated on a vendor pick). Fall back to the
|
|
288
|
+
// locked toggle so the running vendor is still visible.
|
|
289
|
+
_vtw2.classList.remove("hidden");
|
|
290
|
+
_vtw2.classList.add("locked");
|
|
291
|
+
}
|
|
292
|
+
store.set({ vendorSelectionLocked: false, sessionHasHistory: true });
|
|
267
293
|
|
|
268
294
|
// Show pre-thinking dots before server responds
|
|
269
295
|
if (ctx.isMateDm && ctx.isMateDm()) {
|
|
@@ -6,6 +6,7 @@ import { iconHtml, refreshIcons } from './icons.js';
|
|
|
6
6
|
import { setSTTLang } from './stt.js';
|
|
7
7
|
import { avatarUrl, mateAvatarUrl, AVATAR_STYLES } from './avatar.js';
|
|
8
8
|
import { store } from './store.js';
|
|
9
|
+
import { applyTerminalFont } from './terminal-prefs.js';
|
|
9
10
|
|
|
10
11
|
var ctx;
|
|
11
12
|
var profile = { name: '', lang: 'en-US', avatarStyle: 'thumbs', avatarSeed: '', avatarColor: '#7c3aed', avatarCustom: '' };
|
|
@@ -579,6 +580,14 @@ export function initProfile(_ctx) {
|
|
|
579
580
|
setSTTLang(profile.lang);
|
|
580
581
|
}
|
|
581
582
|
|
|
583
|
+
// Hydrate terminal font prefs at boot. Previously this only happened
|
|
584
|
+
// when the user opened the Settings modal (user-settings.js's
|
|
585
|
+
// populateAccount), so a hard refresh would drop persisted picks back
|
|
586
|
+
// to defaults until the modal was visited.
|
|
587
|
+
if (data.terminalFont && typeof data.terminalFont === "object") {
|
|
588
|
+
applyTerminalFont(data.terminalFont.family, data.terminalFont.size);
|
|
589
|
+
}
|
|
590
|
+
|
|
582
591
|
// Apply Mates UI gate at boot so the sidebar avatars / DM picker
|
|
583
592
|
// entry / home-hub strip are hidden before the user opens settings.
|
|
584
593
|
// Default true: only flip the body class off when explicitly false.
|
|
@@ -18,6 +18,7 @@ import { getWs } from './ws-ref.js';
|
|
|
18
18
|
import { store } from './store.js';
|
|
19
19
|
import { getTerminalTheme } from './theme.js';
|
|
20
20
|
import { getTerminalFontFamily, getTerminalFontSize, onTerminalFontChange } from './terminal-prefs.js';
|
|
21
|
+
import { showHeaderTuiFont, hideHeaderTuiFont } from './header-tui-font.js';
|
|
21
22
|
import { openArticle as openWhatsNewArticle } from './whats-new-article.js';
|
|
22
23
|
|
|
23
24
|
// Stable id of the canonical "Why TUI mode?" article in
|
|
@@ -303,7 +304,10 @@ function fitNow() {
|
|
|
303
304
|
if (!xterm || !fitAddon || !hostEl) return;
|
|
304
305
|
syncHostBounds();
|
|
305
306
|
try {
|
|
307
|
+
var prevCols = xterm.cols;
|
|
308
|
+
var prevRows = xterm.rows;
|
|
306
309
|
fitAddon.fit();
|
|
310
|
+
var dimsChanged = (xterm.cols !== prevCols || xterm.rows !== prevRows);
|
|
307
311
|
// Inform the server so its PTY's idea of cols/rows matches what xterm
|
|
308
312
|
// just rendered. Without this, claude TUI redraws using stale dims.
|
|
309
313
|
if (currentTermId != null && getWs() && getWs().readyState === 1) {
|
|
@@ -314,6 +318,14 @@ function fitNow() {
|
|
|
314
318
|
rows: xterm.rows,
|
|
315
319
|
}));
|
|
316
320
|
}
|
|
321
|
+
// After cols/rows change, the previous frame in xterm's buffer is now
|
|
322
|
+
// reflowed/truncated against the new geometry. Claude TUI (ink) repaints
|
|
323
|
+
// using absolute cursor positioning and only writes cells it considers
|
|
324
|
+
// dirty - cells outside the old bounding box stay showing stale or empty
|
|
325
|
+
// content (the "right half is blank after window enlarges" symptom).
|
|
326
|
+
// Wipe the screen here so the next SIGWINCH-driven redraw lands on a
|
|
327
|
+
// clean canvas.
|
|
328
|
+
if (dimsChanged) xterm.write("\x1b[2J\x1b[H");
|
|
317
329
|
} catch (e) {}
|
|
318
330
|
}
|
|
319
331
|
|
|
@@ -377,6 +389,7 @@ export function attachTuiView(terminalId) {
|
|
|
377
389
|
if (currentTermId === terminalId && xterm) {
|
|
378
390
|
if (hostEl) hostEl.style.display = "flex";
|
|
379
391
|
hideGuiChrome(true);
|
|
392
|
+
showHeaderTuiFont();
|
|
380
393
|
fitNow();
|
|
381
394
|
try { xterm.focus(); } catch (e) {}
|
|
382
395
|
return;
|
|
@@ -388,6 +401,7 @@ export function attachTuiView(terminalId) {
|
|
|
388
401
|
if (!ensureHostEl()) return;
|
|
389
402
|
hostEl.style.display = "flex";
|
|
390
403
|
hideGuiChrome(true);
|
|
404
|
+
showHeaderTuiFont();
|
|
391
405
|
syncHostBounds();
|
|
392
406
|
|
|
393
407
|
currentTermId = terminalId;
|
|
@@ -437,6 +451,7 @@ export function detachTuiView() {
|
|
|
437
451
|
teardownXterm();
|
|
438
452
|
if (hostEl) hostEl.style.display = "none";
|
|
439
453
|
hideGuiChrome(false);
|
|
454
|
+
hideHeaderTuiFont();
|
|
440
455
|
}
|
|
441
456
|
|
|
442
457
|
// Route a term_output frame to the embedded xterm if it belongs to the
|
|
@@ -698,16 +698,35 @@ function renderMateMobileActions(container) {
|
|
|
698
698
|
|
|
699
699
|
// Helper: render sorted sessions into a container with date groups (with loop session grouping)
|
|
700
700
|
function renderMobileSessionsInto(container) {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
701
|
+
// Vendor-aware new-session row. Mirrors the desktop sidebar's two-button
|
|
702
|
+
// pattern (Claude defaults to TUI, Codex always GUI) so mobile users can
|
|
703
|
+
// pick the vendor instead of being silently routed to Claude TUI.
|
|
704
|
+
var newRow = document.createElement("div");
|
|
705
|
+
newRow.className = "mobile-session-new-row";
|
|
706
|
+
|
|
707
|
+
var claudeBtn = document.createElement("button");
|
|
708
|
+
claudeBtn.className = "mobile-session-new mobile-session-new-vendor";
|
|
709
|
+
claudeBtn.innerHTML = '<img src="/claude-code-avatar.png" class="mobile-session-new-icon" alt=""><span>Claude</span>';
|
|
710
|
+
claudeBtn.addEventListener("click", function () {
|
|
705
711
|
if (getWs() && store.get('connected')) {
|
|
706
|
-
getWs().send(JSON.stringify({ type: "new_session" }));
|
|
712
|
+
getWs().send(JSON.stringify({ type: "new_session", vendor: "claude", mode: "tui" }));
|
|
707
713
|
}
|
|
708
714
|
closeMobileSheet();
|
|
709
715
|
});
|
|
710
|
-
|
|
716
|
+
newRow.appendChild(claudeBtn);
|
|
717
|
+
|
|
718
|
+
var codexBtn = document.createElement("button");
|
|
719
|
+
codexBtn.className = "mobile-session-new mobile-session-new-vendor";
|
|
720
|
+
codexBtn.innerHTML = '<img src="/codex-avatar.png" class="mobile-session-new-icon" alt=""><span>Codex</span>';
|
|
721
|
+
codexBtn.addEventListener("click", function () {
|
|
722
|
+
if (getWs() && store.get('connected')) {
|
|
723
|
+
getWs().send(JSON.stringify({ type: "new_session", vendor: "codex" }));
|
|
724
|
+
}
|
|
725
|
+
closeMobileSheet();
|
|
726
|
+
});
|
|
727
|
+
newRow.appendChild(codexBtn);
|
|
728
|
+
|
|
729
|
+
container.appendChild(newRow);
|
|
711
730
|
|
|
712
731
|
// Partition: loop sessions vs normal sessions (same logic as desktop renderSessionList)
|
|
713
732
|
var sessions = getCachedSessions();
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { refreshIcons } from './icons.js';
|
|
5
5
|
import { showToast } from './utils.js';
|
|
6
6
|
import { getChatLayout, setChatLayout } from './theme.js';
|
|
7
|
-
import { applyTerminalFont
|
|
7
|
+
import { applyTerminalFont } from './terminal-prefs.js';
|
|
8
8
|
import { showEmailSetupModal, getEmailAccountListCache } from './context-sources.js';
|
|
9
9
|
import { setSTTLang } from './stt.js';
|
|
10
10
|
import { userAvatarUrl } from './avatar.js';
|
|
@@ -234,47 +234,10 @@ export function initUserSettings(appCtx) {
|
|
|
234
234
|
// Theme picker lives on the user island button now (palette icon).
|
|
235
235
|
// No appearance-section mount required.
|
|
236
236
|
|
|
237
|
-
// Terminal font
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
|
|
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
|
-
});
|
|
277
|
-
}
|
|
237
|
+
// Terminal font picker lives in the title bar (visible only while a
|
|
238
|
+
// TUI session is active). See lib/public/modules/header-tui-font.js.
|
|
239
|
+
// We still seed terminal-prefs from /api/profile below so the
|
|
240
|
+
// controls populate correctly when they appear.
|
|
278
241
|
|
|
279
242
|
// Layout switcher (Bubble / Channel)
|
|
280
243
|
var layoutSwitcher = document.getElementById('us-layout-switcher');
|
|
@@ -489,32 +452,12 @@ function populateAccount() {
|
|
|
489
452
|
// Hide account section in single-user mode (no username)
|
|
490
453
|
var accountNav = settingsEl.querySelector('[data-section="us-account"]');
|
|
491
454
|
if (accountNav) accountNav.style.display = data.username ? '' : 'none';
|
|
492
|
-
// Terminal font: seed in-memory prefs from server
|
|
455
|
+
// Terminal font: seed in-memory prefs from server. Header-mounted
|
|
456
|
+
// controls (header-tui-font.js) listen to applyTerminalFont and
|
|
457
|
+
// sync their UI - we don't touch any DOM here.
|
|
493
458
|
if (data.terminalFont && typeof data.terminalFont === "object") {
|
|
494
459
|
applyTerminalFont(data.terminalFont.family, data.terminalFont.size);
|
|
495
460
|
}
|
|
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());
|
|
518
461
|
// Auto-continue toggle
|
|
519
462
|
var acToggle = document.getElementById('us-auto-continue');
|
|
520
463
|
if (acToggle) acToggle.checked = !!data.autoContinueOnRateLimit;
|
|
@@ -586,7 +586,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
586
586
|
done: false,
|
|
587
587
|
aborted: false,
|
|
588
588
|
loopStarted: false,
|
|
589
|
-
model: queryOpts.model || "gpt-5.
|
|
589
|
+
model: queryOpts.model || "gpt-5.5",
|
|
590
590
|
// Track incremental text deltas
|
|
591
591
|
textBlocks: {}, // itemId -> true (text_start sent)
|
|
592
592
|
textLengths: {}, // itemId -> last sent length
|
|
@@ -831,7 +831,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
831
831
|
|
|
832
832
|
// Start or resume thread
|
|
833
833
|
var threadParams = {
|
|
834
|
-
model: queryOpts.model || "gpt-5.
|
|
834
|
+
model: queryOpts.model || "gpt-5.5",
|
|
835
835
|
sandbox: queryOpts.sandboxMode || "workspace-write",
|
|
836
836
|
approvalPolicy: queryOpts.approvalPolicy || "on-failure",
|
|
837
837
|
cwd: queryOpts.cwd,
|
|
@@ -1085,7 +1085,7 @@ function createCodexAdapter(opts) {
|
|
|
1085
1085
|
function buildReadyResponse(skillNames) {
|
|
1086
1086
|
return {
|
|
1087
1087
|
models: _cachedModels,
|
|
1088
|
-
defaultModel: "gpt-5.
|
|
1088
|
+
defaultModel: "gpt-5.5",
|
|
1089
1089
|
skills: skillNames || [],
|
|
1090
1090
|
slashCommands: skillNames || [],
|
|
1091
1091
|
fastModeState: null,
|
|
@@ -1297,9 +1297,10 @@ function createCodexAdapter(opts) {
|
|
|
1297
1297
|
throw createShutdownError();
|
|
1298
1298
|
}
|
|
1299
1299
|
|
|
1300
|
-
console.log("[codex] App-server initialized, models: gpt-5.4, gpt-5.4-mini, gpt-5.3-codex, gpt-5.3-codex-spark, gpt-5.2");
|
|
1300
|
+
console.log("[codex] App-server initialized, models: gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex, gpt-5.3-codex-spark, gpt-5.2");
|
|
1301
1301
|
|
|
1302
1302
|
_cachedModels = [
|
|
1303
|
+
"gpt-5.5",
|
|
1303
1304
|
"gpt-5.4",
|
|
1304
1305
|
"gpt-5.4-mini",
|
|
1305
1306
|
"gpt-5.3-codex",
|
|
@@ -1387,7 +1388,7 @@ function createCodexAdapter(opts) {
|
|
|
1387
1388
|
throw new Error("[yoke/codex] Adapter not initialized. Call init() first.");
|
|
1388
1389
|
}
|
|
1389
1390
|
|
|
1390
|
-
var model = queryOpts.model || "gpt-5.
|
|
1391
|
+
var model = queryOpts.model || "gpt-5.5";
|
|
1391
1392
|
var ac = queryOpts.abortController || new AbortController();
|
|
1392
1393
|
var activeEntry = {
|
|
1393
1394
|
abort: function() {
|
package/package.json
CHANGED