clay-server 2.40.0-beta.1 → 2.40.0-beta.3
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/css/menus.css +21 -0
- package/lib/public/css/mobile-nav.css +36 -0
- package/lib/public/css/user-settings.css +26 -2
- package/lib/public/modules/app-messages.js +9 -2
- package/lib/public/modules/app-panels.js +38 -14
- package/lib/public/modules/input.js +29 -3
- package/lib/public/modules/profile.js +9 -0
- package/lib/public/modules/session-tui-view.js +11 -0
- package/lib/public/modules/sidebar-mobile.js +25 -6
- package/lib/public/modules/terminal.js +42 -0
- 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/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;
|
|
@@ -862,8 +862,32 @@
|
|
|
862
862
|
|
|
863
863
|
/* Header TUI font icon button - single trigger that opens a popover
|
|
864
864
|
with the full font picker + size stepper. Visible only when a TUI
|
|
865
|
-
session is active.
|
|
866
|
-
|
|
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 {
|
|
869
|
+
display: flex;
|
|
870
|
+
align-items: center;
|
|
871
|
+
justify-content: center;
|
|
872
|
+
background: none;
|
|
873
|
+
border: 1px solid transparent;
|
|
874
|
+
border-radius: 8px;
|
|
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 {
|
|
887
|
+
color: var(--text);
|
|
888
|
+
background: rgba(var(--overlay-rgb), 0.06);
|
|
889
|
+
border-color: var(--border);
|
|
890
|
+
}
|
|
867
891
|
.header-tui-font-btn.hidden { display: none; }
|
|
868
892
|
|
|
869
893
|
/* Floating popover that holds the font menu + size row. Anchored
|
|
@@ -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 });
|
|
@@ -177,16 +177,27 @@ function hasBeta(name) {
|
|
|
177
177
|
|
|
178
178
|
function rebuildModelList() {
|
|
179
179
|
if (!configModelList) return;
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
// 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).
|
|
184
189
|
var modelSection = configModelList.parentElement;
|
|
185
190
|
var s = store.snap();
|
|
186
|
-
var
|
|
191
|
+
var vendor = s.currentVendor || "claude";
|
|
192
|
+
var hideModelPicker = s.activeSessionMode === "gui" && vendor === "claude";
|
|
187
193
|
if (modelSection) modelSection.style.display = hideModelPicker ? "none" : "";
|
|
188
194
|
configModelList.innerHTML = "";
|
|
189
195
|
if (hideModelPicker) return;
|
|
196
|
+
|
|
197
|
+
var lockedForCodex = vendor === "codex"
|
|
198
|
+
&& s.activeSessionMode === "gui"
|
|
199
|
+
&& !!s.sessionHasHistory;
|
|
200
|
+
|
|
190
201
|
var list = s.currentModels.length > 0 ? s.currentModels : (s.currentModel ? [{ value: s.currentModel, displayName: s.currentModel }] : []);
|
|
191
202
|
for (var i = 0; i < list.length; i++) {
|
|
192
203
|
var item = list[i];
|
|
@@ -196,19 +207,32 @@ function rebuildModelList() {
|
|
|
196
207
|
var btn = document.createElement("button");
|
|
197
208
|
btn.className = "config-radio-item";
|
|
198
209
|
if (value === s.currentModel) btn.classList.add("active");
|
|
210
|
+
if (lockedForCodex) btn.classList.add("locked");
|
|
199
211
|
btn.dataset.model = value;
|
|
200
212
|
btn.textContent = label;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
+
}
|
|
210
227
|
configModelList.appendChild(btn);
|
|
211
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
|
+
}
|
|
212
236
|
}
|
|
213
237
|
|
|
214
238
|
function rebuildModeList() {
|
|
@@ -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.
|
|
@@ -304,7 +304,10 @@ function fitNow() {
|
|
|
304
304
|
if (!xterm || !fitAddon || !hostEl) return;
|
|
305
305
|
syncHostBounds();
|
|
306
306
|
try {
|
|
307
|
+
var prevCols = xterm.cols;
|
|
308
|
+
var prevRows = xterm.rows;
|
|
307
309
|
fitAddon.fit();
|
|
310
|
+
var dimsChanged = (xterm.cols !== prevCols || xterm.rows !== prevRows);
|
|
308
311
|
// Inform the server so its PTY's idea of cols/rows matches what xterm
|
|
309
312
|
// just rendered. Without this, claude TUI redraws using stale dims.
|
|
310
313
|
if (currentTermId != null && getWs() && getWs().readyState === 1) {
|
|
@@ -315,6 +318,14 @@ function fitNow() {
|
|
|
315
318
|
rows: xterm.rows,
|
|
316
319
|
}));
|
|
317
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");
|
|
318
329
|
} catch (e) {}
|
|
319
330
|
}
|
|
320
331
|
|
|
@@ -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();
|
|
@@ -440,6 +440,29 @@ function createXtermForTab(tab) {
|
|
|
440
440
|
tab.bodyEl = bodyEl;
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
+
// Rebuild a tab's WebGL glyph atlas, then force a full redraw.
|
|
444
|
+
//
|
|
445
|
+
// The WebGL renderer caches rasterized glyphs in a texture atlas keyed on
|
|
446
|
+
// the font (family + cell metrics). When the font changes or cell size
|
|
447
|
+
// shifts on resize, the atlas keeps glyphs rasterized from the old state,
|
|
448
|
+
// so text renders garbled until a full repaint rebuilds it. That repaint
|
|
449
|
+
// is exactly what "select all" triggers, which is why selecting everything
|
|
450
|
+
// appears to "fix" the corruption. clearTextureAtlas() forces the rebuild
|
|
451
|
+
// so glyphs re-rasterize at the new font/metrics immediately. Deferred one
|
|
452
|
+
// frame so any preceding fit() has settled. No-op on tabs that fell back to
|
|
453
|
+
// the DOM renderer. Mirrors the resize handler in session-tui-view.js.
|
|
454
|
+
function rebuildTabGlyphAtlas(tab) {
|
|
455
|
+
if (!tab) return;
|
|
456
|
+
requestAnimationFrame(function () {
|
|
457
|
+
if (tab._webglAddon && typeof tab._webglAddon.clearTextureAtlas === "function") {
|
|
458
|
+
try { tab._webglAddon.clearTextureAtlas(); } catch (e) {}
|
|
459
|
+
}
|
|
460
|
+
if (tab.xterm) {
|
|
461
|
+
try { tab.xterm.refresh(0, tab.xterm.rows - 1); } catch (e) {}
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
443
466
|
// --- Fit active terminal ---
|
|
444
467
|
var fitRafId = null;
|
|
445
468
|
|
|
@@ -462,6 +485,9 @@ function fitTerminal() {
|
|
|
462
485
|
}));
|
|
463
486
|
}
|
|
464
487
|
} catch (e) {}
|
|
488
|
+
// Cell metrics shift on resize; rebuild the WebGL glyph atlas so glyphs
|
|
489
|
+
// don't render against stale cell dimensions.
|
|
490
|
+
rebuildTabGlyphAtlas(tab);
|
|
465
491
|
});
|
|
466
492
|
}
|
|
467
493
|
|
|
@@ -818,7 +844,23 @@ function applyFontToAllTabs(family, size) {
|
|
|
818
844
|
if (family) tab.xterm.options.fontFamily = family;
|
|
819
845
|
if (size) tab.xterm.options.fontSize = size;
|
|
820
846
|
if (tab.fitAddon) tab.fitAddon.fit();
|
|
847
|
+
// A font-size change shifts cell metrics, so fit() recomputes
|
|
848
|
+
// cols/rows. Notify the PTY of the new size - otherwise the running
|
|
849
|
+
// program keeps drawing to the old dimensions and the output looks
|
|
850
|
+
// corrupted. (Font-family-only changes leave cols/rows unchanged,
|
|
851
|
+
// making this a harmless no-op.) Mirrors fitTerminal()'s resize msg.
|
|
852
|
+
if (ctx.ws && ctx.connected) {
|
|
853
|
+
ctx.ws.send(JSON.stringify({
|
|
854
|
+
type: "term_resize",
|
|
855
|
+
id: tab.id,
|
|
856
|
+
cols: tab.xterm.cols,
|
|
857
|
+
rows: tab.xterm.rows,
|
|
858
|
+
}));
|
|
859
|
+
}
|
|
821
860
|
} catch (e) {}
|
|
861
|
+
// Changing the font invalidates the WebGL glyph atlas; rebuild it so
|
|
862
|
+
// the new font renders cleanly instead of stale, garbled cached glyphs.
|
|
863
|
+
rebuildTabGlyphAtlas(tab);
|
|
822
864
|
}
|
|
823
865
|
}
|
|
824
866
|
onTerminalFontChange(applyFontToAllTabs);
|
package/package.json
CHANGED