clay-server 2.31.0 → 2.32.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.
Files changed (74) hide show
  1. package/lib/browser-mcp-server.js +32 -44
  2. package/lib/debate-mcp-server.js +14 -31
  3. package/lib/mcp-local.js +31 -1
  4. package/lib/project-connection.js +4 -2
  5. package/lib/project-filesystem.js +47 -1
  6. package/lib/project-http.js +75 -8
  7. package/lib/project-mcp.js +4 -0
  8. package/lib/project-sessions.js +88 -51
  9. package/lib/project-user-message.js +12 -7
  10. package/lib/project.js +204 -90
  11. package/lib/public/app.js +123 -448
  12. package/lib/public/codex-avatar.png +0 -0
  13. package/lib/public/css/debate.css +3 -2
  14. package/lib/public/css/filebrowser.css +91 -1
  15. package/lib/public/css/icon-strip.css +21 -5
  16. package/lib/public/css/input.css +181 -100
  17. package/lib/public/css/mates.css +43 -0
  18. package/lib/public/css/mention.css +48 -4
  19. package/lib/public/css/menus.css +1 -1
  20. package/lib/public/css/messages.css +2 -0
  21. package/lib/public/css/notifications-center.css +19 -0
  22. package/lib/public/index.html +46 -24
  23. package/lib/public/modules/app-connection.js +138 -37
  24. package/lib/public/modules/app-cursors.js +18 -17
  25. package/lib/public/modules/app-debate-ui.js +9 -9
  26. package/lib/public/modules/app-dm.js +170 -131
  27. package/lib/public/modules/app-favicon.js +28 -26
  28. package/lib/public/modules/app-header.js +79 -68
  29. package/lib/public/modules/app-home-hub.js +55 -47
  30. package/lib/public/modules/app-loop-ui.js +34 -18
  31. package/lib/public/modules/app-loop-wizard.js +6 -6
  32. package/lib/public/modules/app-messages.js +195 -152
  33. package/lib/public/modules/app-misc.js +23 -12
  34. package/lib/public/modules/app-notifications.js +97 -3
  35. package/lib/public/modules/app-panels.js +203 -49
  36. package/lib/public/modules/app-projects.js +159 -150
  37. package/lib/public/modules/app-rate-limit.js +5 -4
  38. package/lib/public/modules/app-rendering.js +149 -101
  39. package/lib/public/modules/app-skills-install.js +4 -4
  40. package/lib/public/modules/context-sources.js +12 -41
  41. package/lib/public/modules/dom-refs.js +21 -0
  42. package/lib/public/modules/filebrowser.js +173 -2
  43. package/lib/public/modules/input.js +86 -0
  44. package/lib/public/modules/mate-sidebar.js +38 -0
  45. package/lib/public/modules/mention.js +24 -6
  46. package/lib/public/modules/scheduler.js +1 -1
  47. package/lib/public/modules/sidebar-mates.js +66 -34
  48. package/lib/public/modules/sidebar-mobile.js +34 -30
  49. package/lib/public/modules/sidebar-projects.js +60 -57
  50. package/lib/public/modules/sidebar-sessions.js +75 -69
  51. package/lib/public/modules/sidebar.js +12 -20
  52. package/lib/public/modules/skills.js +8 -9
  53. package/lib/public/modules/sticky-notes.js +1 -2
  54. package/lib/public/modules/store.js +9 -2
  55. package/lib/public/modules/stt.js +4 -1
  56. package/lib/public/modules/tools.js +14 -9
  57. package/lib/sdk-bridge.js +511 -1113
  58. package/lib/sdk-message-processor.js +123 -134
  59. package/lib/sdk-worker.js +4 -0
  60. package/lib/server-dm.js +1 -0
  61. package/lib/server.js +86 -1
  62. package/lib/sessions.js +47 -36
  63. package/lib/ws-schema.js +2 -0
  64. package/lib/yoke/adapters/claude-worker.js +559 -0
  65. package/lib/yoke/adapters/claude.js +1418 -0
  66. package/lib/yoke/adapters/codex.js +968 -0
  67. package/lib/yoke/adapters/gemini.js +668 -0
  68. package/lib/yoke/codex-app-server.js +307 -0
  69. package/lib/yoke/index.js +199 -0
  70. package/lib/yoke/instructions.js +62 -0
  71. package/lib/yoke/interface.js +92 -0
  72. package/lib/yoke/mcp-bridge-server.js +294 -0
  73. package/lib/yoke/package.json +7 -0
  74. package/package.json +3 -1
@@ -13,6 +13,9 @@ var _extRequestCallbacks = {};
13
13
 
14
14
  // Queue for extension messages that arrived before WS was ready
15
15
  var _pendingExtMessages = [];
16
+ // Cache last extension state so we can resend on WS reconnect (server restart)
17
+ var _lastTabListMsg = null;
18
+ var _lastMcpServersMsg = null;
16
19
 
17
20
  function sendOrQueue(msgObj) {
18
21
  var ws = getWs();
@@ -24,14 +27,20 @@ function sendOrQueue(msgObj) {
24
27
  }
25
28
 
26
29
  export function flushPendingExtMessages() {
27
- if (_pendingExtMessages.length === 0) return;
28
30
  var ws = getWs();
29
31
  if (!ws || ws.readyState !== 1) return;
30
- var queued = _pendingExtMessages.slice();
31
- _pendingExtMessages = [];
32
- for (var i = 0; i < queued.length; i++) {
33
- ws.send(JSON.stringify(queued[i]));
32
+ // Flush queued messages from before WS was ready
33
+ if (_pendingExtMessages.length > 0) {
34
+ var queued = _pendingExtMessages.slice();
35
+ _pendingExtMessages = [];
36
+ for (var i = 0; i < queued.length; i++) {
37
+ ws.send(JSON.stringify(queued[i]));
38
+ }
34
39
  }
40
+ // Resend cached extension state on every WS reconnect so the server
41
+ // re-registers _extensionWs and rebuilds MCP proxy servers
42
+ if (_lastTabListMsg) ws.send(JSON.stringify(_lastTabListMsg));
43
+ if (_lastMcpServersMsg) ws.send(JSON.stringify(_lastMcpServersMsg));
35
44
  }
36
45
 
37
46
  export function initMisc() {
@@ -121,11 +130,9 @@ export function initMisc() {
121
130
  if (msg.type === "clay_ext_tab_list") {
122
131
  setExtensionConnected(true);
123
132
  updateBrowserTabList(msg.tabs);
124
- // Also inform server about tab list (queue if WS not ready yet)
125
- sendOrQueue({
126
- type: "browser_tab_list",
127
- tabs: msg.tabs
128
- });
133
+ // Cache and send (or queue) - resent on WS reconnect via flushPendingExtMessages
134
+ _lastTabListMsg = { type: "browser_tab_list", tabs: msg.tabs };
135
+ sendOrQueue(_lastTabListMsg);
129
136
  }
130
137
  if (msg.type === "clay_ext_result") {
131
138
  handleExtensionResult(msg.requestId, msg.result);
@@ -135,12 +142,14 @@ export function initMisc() {
135
142
  }
136
143
 
137
144
  // MCP bridge: extension reports available MCP servers (queue if WS not ready yet)
145
+ // Cache for resend on WS reconnect (server restart loses _availableServers)
138
146
  if (msg.type === "mcp_servers_available") {
139
- sendOrQueue({
147
+ _lastMcpServersMsg = {
140
148
  type: "mcp_servers_available",
141
149
  servers: msg.servers,
142
150
  hostConnected: msg.hostConnected
143
- });
151
+ };
152
+ sendOrQueue(_lastMcpServersMsg);
144
153
  }
145
154
 
146
155
  // MCP bridge: tool result from extension (tool results should not be queued,
@@ -161,6 +170,7 @@ export function initMisc() {
161
170
 
162
171
  // Forward an MCP tool call from the server to the Chrome extension
163
172
  export function forwardMcpToolCall(msg) {
173
+ console.log("[mcp] forwarding to extension:", msg.callId, msg.server, msg.method);
164
174
  window.postMessage({
165
175
  source: "clay-page",
166
176
  payload: {
@@ -185,6 +195,7 @@ export function setHttpMcpServers(servers) {
185
195
  }
186
196
 
187
197
  export function handleMcpToolCallMessage(msg) {
198
+ console.log("[mcp] tool call received from server:", msg.callId, msg.server, msg.params && msg.params.name);
188
199
  var httpUrl = _httpMcpServers[msg.server];
189
200
  if (httpUrl) {
190
201
  // HTTP transport: call directly via fetch
@@ -15,6 +15,12 @@ var bannerContainer = null;
15
15
  var bellBtn = null;
16
16
  var badgeEl = null;
17
17
 
18
+ // --- Update available banner state ---
19
+ var pendingUpdateMsg = null;
20
+ var updateReshowTimer = null;
21
+ var updateDismissedAt = 0;
22
+ var UPDATE_RESHOW_INTERVAL = 60 * 60 * 1000; // 1 hour
23
+
18
24
  // ========================================================
19
25
  // Init
20
26
  // ========================================================
@@ -44,7 +50,10 @@ function showAllBanners() {
44
50
  // Clear existing banners first
45
51
  if (bannerContainer) bannerContainer.innerHTML = "";
46
52
 
47
- if (notifications.length === 0) {
53
+ // Re-add update banner if present
54
+ if (pendingUpdateMsg) showUpdateBanner(pendingUpdateMsg);
55
+
56
+ if (notifications.length === 0 && !pendingUpdateMsg) {
48
57
  showBanner({
49
58
  id: "_empty",
50
59
  type: "info",
@@ -237,7 +246,7 @@ export function handleNotificationCreated(msg) {
237
246
  var notif = msg.notification;
238
247
 
239
248
  // Auto-dismiss if it's for the session the user is currently viewing
240
- var activeSession = store.getState().activeSessionId || null;
249
+ var activeSession = store.get('activeSessionId') || null;
241
250
  console.log("[notif] created:", notif.type, "sessionId=" + notif.sessionId + "(" + typeof notif.sessionId + ")", "active=" + activeSession + "(" + typeof activeSession + ")", "match=" + (notif.sessionId == activeSession));
242
251
  if (notif.sessionId && String(notif.sessionId) === String(activeSession)) {
243
252
  dismissNotif(notif.id);
@@ -297,7 +306,7 @@ function navigateToNotification(notif) {
297
306
  return;
298
307
  }
299
308
 
300
- var currentSlug = store.getState().currentSlug || "";
309
+ var currentSlug = store.get('currentSlug') || "";
301
310
  var needsProjectSwitch = notif.slug && notif.slug !== currentSlug;
302
311
 
303
312
  if (needsProjectSwitch) {
@@ -314,6 +323,91 @@ function navigateToNotification(notif) {
314
323
  }
315
324
  }
316
325
 
326
+ // ========================================================
327
+ // Update available banner
328
+ // ========================================================
329
+
330
+ export function showUpdateBanner(msg) {
331
+ if (!msg || !msg.version) return;
332
+ pendingUpdateMsg = msg;
333
+ if (!bannerContainer) return;
334
+
335
+ // If user dismissed recently, skip until reshow timer fires
336
+ if (updateDismissedAt && (Date.now() - updateDismissedAt) < UPDATE_RESHOW_INTERVAL) return;
337
+
338
+ // Remove any existing update banner
339
+ var existing = bannerContainer.querySelector('[data-notif-id="_update"]');
340
+ if (existing) removeBanner(existing);
341
+
342
+ var isHeadless = store.get('isHeadlessMode');
343
+ var updTag = msg.version.indexOf("-beta") !== -1 ? "beta" : "latest";
344
+
345
+ var banner = document.createElement("div");
346
+ banner.className = "notif-banner notif-banner-update";
347
+ banner.setAttribute("data-notif-id", "_update");
348
+
349
+ var actionsHtml = '';
350
+ if (!isHeadless) {
351
+ actionsHtml =
352
+ '<div class="notif-banner-actions">' +
353
+ '<button class="notif-banner-update-now">Update now</button>' +
354
+ '</div>';
355
+ }
356
+
357
+ banner.innerHTML =
358
+ '<div class="notif-banner-icon"><img src="/icon-banded-76.png" width="32" height="32" alt="Clay" style="border-radius:8px"></div>' +
359
+ '<div class="notif-banner-body">' +
360
+ '<div class="notif-banner-project">CLAY</div>' +
361
+ '<div class="notif-banner-title">v' + escapeHtml(msg.version) + ' is available</div>' +
362
+ (isHeadless
363
+ ? '<div class="notif-banner-text">Run: npx clay-server@' + escapeHtml(updTag) + '</div>'
364
+ : '') +
365
+ actionsHtml +
366
+ '</div>' +
367
+ '<button class="notif-banner-close">' + iconHtml("x") + '</button>';
368
+
369
+ bannerContainer.appendChild(banner);
370
+ refreshIcons();
371
+
372
+ requestAnimationFrame(function () {
373
+ banner.classList.add("show");
374
+ });
375
+
376
+ // "Update now" button
377
+ var updateBtn = banner.querySelector(".notif-banner-update-now");
378
+ if (updateBtn) {
379
+ updateBtn.addEventListener("click", function (e) {
380
+ e.stopPropagation();
381
+ var ws = getWs();
382
+ if (ws && ws.readyState === 1) {
383
+ ws.send(JSON.stringify({ type: "update_now" }));
384
+ updateBtn.textContent = "Updating...";
385
+ updateBtn.disabled = true;
386
+ }
387
+ });
388
+ }
389
+
390
+ // Close button -> dismiss, re-show in 1 hour
391
+ var closeBtn = banner.querySelector(".notif-banner-close");
392
+ if (closeBtn) {
393
+ closeBtn.addEventListener("click", function (e) {
394
+ e.stopPropagation();
395
+ removeBanner(banner);
396
+ scheduleUpdateReshow();
397
+ });
398
+ }
399
+ }
400
+
401
+ function scheduleUpdateReshow() {
402
+ updateDismissedAt = Date.now();
403
+ if (updateReshowTimer) clearTimeout(updateReshowTimer);
404
+ updateReshowTimer = setTimeout(function () {
405
+ updateReshowTimer = null;
406
+ updateDismissedAt = 0;
407
+ if (pendingUpdateMsg) showUpdateBanner(pendingUpdateMsg);
408
+ }, UPDATE_RESHOW_INTERVAL);
409
+ }
410
+
317
411
  // ========================================================
318
412
  // Helpers
319
413
  // ========================================================
@@ -2,9 +2,10 @@
2
2
  // Extracted from app.js (PR-30)
3
3
 
4
4
  import { refreshIcons } from "./icons.js";
5
- import { escapeHtml } from "./utils.js";
5
+ import { escapeHtml, showToast } from "./utils.js";
6
6
  import { store } from './store.js';
7
7
  import { getWs } from './ws-ref.js';
8
+ import { VENDOR_NAMES } from './app-rendering.js';
8
9
 
9
10
  // --- Module-owned state (not in store) ---
10
11
  var sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
@@ -28,6 +29,12 @@ var configThinkingSection = null;
28
29
  var configThinkingBar = null;
29
30
  var configThinkingBudgetRow = null;
30
31
  var configThinkingBudgetInput = null;
32
+ var configApprovalSection = null;
33
+ var configApprovalBar = null;
34
+ var configSandboxSection = null;
35
+ var configSandboxBar = null;
36
+ var configWebsearchSection = null;
37
+ var configWebsearchBar = null;
31
38
 
32
39
  var usagePanel = null;
33
40
  var usagePanelClose = null;
@@ -78,10 +85,35 @@ var MODE_OPTIONS = [
78
85
  ];
79
86
  var MODE_FULL_AUTO = { value: "bypassPermissions", label: "Full auto" };
80
87
  var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
88
+ var EFFORT_LEVELS_BY_VENDOR = {
89
+ claude: ["low", "medium", "high", "xhigh", "max"],
90
+ codex: ["minimal", "low", "medium", "high", "xhigh"],
91
+ };
81
92
  var THINKING_OPTIONS = ["disabled", "adaptive", "budget"];
93
+ var CODEX_APPROVAL_OPTIONS = [
94
+ { value: "never", label: "Auto" },
95
+ { value: "on-failure", label: "On Fail" },
96
+ { value: "on-request", label: "Ask" },
97
+ ];
98
+ var CODEX_SANDBOX_OPTIONS = [
99
+ { value: "read-only", label: "Read Only" },
100
+ { value: "workspace-write", label: "Workspace" },
101
+ { value: "danger-full-access", label: "Full Access" },
102
+ ];
103
+ var CODEX_WEBSEARCH_OPTIONS = [
104
+ { value: "disabled", label: "Off" },
105
+ { value: "cached", label: "Cached" },
106
+ { value: "live", label: "Live" },
107
+ ];
82
108
  var KNOWN_CONTEXT_WINDOWS = {
83
109
  "opus-4-6": 1000000,
84
- "claude-sonnet-4": 1000000
110
+ "claude-sonnet-4": 1000000,
111
+ "gpt-5.4": 1048576,
112
+ "gpt-5.3": 1048576,
113
+ "gpt-5.2": 1048576,
114
+ "gpt-4.1": 1047576,
115
+ "o3": 200000,
116
+ "o4-mini": 200000,
85
117
  };
86
118
  // Categories to hide from the legend (noise, not actionable)
87
119
  var CTX_HIDDEN_CATS = { "Free space": 1, "Autocompact buffer": 1 };
@@ -98,7 +130,9 @@ function modelDisplayName(value, models) {
98
130
  if (!value) return "";
99
131
  if (models) {
100
132
  for (var i = 0; i < models.length; i++) {
101
- if (models[i].value === value && models[i].displayName) return models[i].displayName;
133
+ var m = models[i];
134
+ if (typeof m === "string") { if (m === value) return m; }
135
+ else if (m.value === value && m.displayName) return m.displayName;
102
136
  }
103
137
  }
104
138
  return value;
@@ -133,7 +167,7 @@ function isSonnetModel(model) {
133
167
  }
134
168
 
135
169
  function hasBeta(name) {
136
- var betas = store.getState().currentBetas;
170
+ var betas = store.get('currentBetas');
137
171
  for (var i = 0; i < betas.length; i++) {
138
172
  if (betas[i].indexOf(name) !== -1) return true;
139
173
  }
@@ -143,12 +177,13 @@ function hasBeta(name) {
143
177
  function rebuildModelList() {
144
178
  if (!configModelList) return;
145
179
  configModelList.innerHTML = "";
146
- var s = store.getState();
180
+ var s = store.snap();
147
181
  var list = s.currentModels.length > 0 ? s.currentModels : (s.currentModel ? [{ value: s.currentModel, displayName: s.currentModel }] : []);
148
182
  for (var i = 0; i < list.length; i++) {
149
183
  var item = list[i];
150
- var value = item.value || "";
151
- var label = item.displayName || value;
184
+ // Support both object { value, displayName } and plain string formats
185
+ var value = typeof item === "string" ? item : (item.value || "");
186
+ var label = typeof item === "string" ? item : (item.displayName || value);
152
187
  var btn = document.createElement("button");
153
188
  btn.className = "config-radio-item";
154
189
  if (value === s.currentModel) btn.classList.add("active");
@@ -171,14 +206,14 @@ function rebuildModeList() {
171
206
  if (!configModeList) return;
172
207
  configModeList.innerHTML = "";
173
208
  var options = MODE_OPTIONS.slice();
174
- if (store.getState().skipPermsEnabled) {
209
+ if (store.get('skipPermsEnabled')) {
175
210
  options.push(MODE_FULL_AUTO);
176
211
  }
177
212
  for (var i = 0; i < options.length; i++) {
178
213
  var opt = options[i];
179
214
  var btn = document.createElement("button");
180
215
  btn.className = "config-radio-item";
181
- if (opt.value === store.getState().currentMode) btn.classList.add("active");
216
+ if (opt.value === store.get('currentMode')) btn.classList.add("active");
182
217
  btn.dataset.mode = opt.value;
183
218
  btn.textContent = opt.label;
184
219
  btn.addEventListener("click", function () {
@@ -208,7 +243,7 @@ function rebuildEffortBar() {
208
243
  var level = levels[i];
209
244
  var btn = document.createElement("button");
210
245
  btn.className = "config-segment-btn";
211
- if (level === store.getState().currentEffort) btn.classList.add("active");
246
+ if (level === store.get('currentEffort')) btn.classList.add("active");
212
247
  btn.dataset.effort = level;
213
248
  btn.textContent = effortDisplayName(level);
214
249
  btn.addEventListener("click", function () {
@@ -227,7 +262,7 @@ function rebuildEffortBar() {
227
262
  function rebuildBetaSection() {
228
263
  if (!configBetaSection || !configBeta1mBtn) return;
229
264
  // Only show for Sonnet models
230
- if (!isSonnetModel(store.getState().currentModel)) {
265
+ if (!isSonnetModel(store.get('currentModel'))) {
231
266
  configBetaSection.style.display = "none";
232
267
  return;
233
268
  }
@@ -241,7 +276,7 @@ function rebuildThinkingSection() {
241
276
  if (!configThinkingBar || !configThinkingSection) return;
242
277
  configThinkingSection.style.display = "";
243
278
  configThinkingBar.innerHTML = "";
244
- var s = store.getState();
279
+ var s = store.snap();
245
280
  for (var i = 0; i < THINKING_OPTIONS.length; i++) {
246
281
  var opt = THINKING_OPTIONS[i];
247
282
  var btn = document.createElement("button");
@@ -253,7 +288,7 @@ function rebuildThinkingSection() {
253
288
  var thinking = this.dataset.thinking;
254
289
  var msg = { type: "set_thinking", thinking: thinking };
255
290
  if (thinking === "budget") {
256
- msg.budgetTokens = store.getState().currentThinkingBudget;
291
+ msg.budgetTokens = store.get('currentThinkingBudget');
257
292
  }
258
293
  var ws = getWs();
259
294
  if (ws && ws.readyState === 1) {
@@ -271,6 +306,47 @@ function rebuildThinkingSection() {
271
306
  }
272
307
  }
273
308
 
309
+ function buildSegmentedBar(barEl, options, currentValue, msgType, msgKey) {
310
+ if (!barEl) return;
311
+ barEl.innerHTML = "";
312
+ for (var i = 0; i < options.length; i++) {
313
+ var opt = options[i];
314
+ var btn = document.createElement("button");
315
+ btn.className = "config-segment-btn";
316
+ if (opt.value === currentValue) btn.classList.add("active");
317
+ btn.dataset.val = opt.value;
318
+ btn.textContent = opt.label;
319
+ btn.addEventListener("click", function () {
320
+ var val = this.dataset.val;
321
+ var msg = { type: msgType };
322
+ msg[msgKey] = val;
323
+ var ws = getWs();
324
+ if (ws && ws.readyState === 1) ws.send(JSON.stringify(msg));
325
+ configPopover.classList.add("hidden");
326
+ configChip.classList.remove("active");
327
+ });
328
+ barEl.appendChild(btn);
329
+ }
330
+ }
331
+
332
+ function rebuildCodexSections() {
333
+ var s = store.snap();
334
+ var isCodex = (s.currentVendor || "claude") === "codex";
335
+
336
+ if (configApprovalSection) {
337
+ configApprovalSection.style.display = isCodex ? "" : "none";
338
+ if (isCodex) buildSegmentedBar(configApprovalBar, CODEX_APPROVAL_OPTIONS, s.codexApproval || "on-failure", "set_codex_approval", "approval");
339
+ }
340
+ if (configSandboxSection) {
341
+ configSandboxSection.style.display = isCodex ? "" : "none";
342
+ if (isCodex) buildSegmentedBar(configSandboxBar, CODEX_SANDBOX_OPTIONS, s.codexSandbox || "workspace-write", "set_codex_sandbox", "sandbox");
343
+ }
344
+ if (configWebsearchSection) {
345
+ configWebsearchSection.style.display = isCodex ? "" : "none";
346
+ if (isCodex) buildSegmentedBar(configWebsearchBar, CODEX_WEBSEARCH_OPTIONS, s.codexWebSearch || "disabled", "set_codex_websearch", "webSearch");
347
+ }
348
+ }
349
+
274
350
  function escHtml(s) {
275
351
  var div = document.createElement("div");
276
352
  div.textContent = s;
@@ -301,6 +377,86 @@ export function initPanels() {
301
377
  configThinkingBar = $("config-thinking-bar");
302
378
  configThinkingBudgetRow = $("config-thinking-budget-row");
303
379
  configThinkingBudgetInput = $("config-thinking-budget");
380
+ configApprovalSection = $("config-approval-section");
381
+ configApprovalBar = $("config-approval-bar");
382
+ configSandboxSection = $("config-sandbox-section");
383
+ configSandboxBar = $("config-sandbox-bar");
384
+ configWebsearchSection = $("config-websearch-section");
385
+ configWebsearchBar = $("config-websearch-bar");
386
+
387
+ // --- Vendor toggle ---
388
+ var vendorToggleWrap = $("vendor-toggle-wrap");
389
+ var vendorBtnClaude = $("vendor-btn-claude");
390
+ var vendorBtnCodex = $("vendor-btn-codex");
391
+ var vendorBtns = { claude: vendorBtnClaude, codex: vendorBtnCodex };
392
+
393
+ function updateVendorToggle() {
394
+ var installed = store.get('installedVendors') || [];
395
+ var current = store.get('currentVendor') || "claude";
396
+
397
+ var vendors = Object.keys(vendorBtns);
398
+ for (var i = 0; i < vendors.length; i++) {
399
+ var v = vendors[i];
400
+ var btn = vendorBtns[v];
401
+ if (!btn) continue;
402
+ var isInstalled = installed.indexOf(v) !== -1;
403
+ btn.classList.toggle("active", v === current);
404
+ btn.classList.toggle("disabled", !isInstalled);
405
+ btn.title = isInstalled ? (VENDOR_NAMES[v] || v) : (VENDOR_NAMES[v] || v) + " is not installed";
406
+ }
407
+ }
408
+
409
+ function onVendorClick(vendor) {
410
+ if (vendor === (store.get('currentVendor') || "claude")) return;
411
+ var installed = store.get('installedVendors') || [];
412
+ if (installed.indexOf(vendor) === -1) return;
413
+ store.set({ currentVendor: vendor });
414
+ var ws = getWs();
415
+ if (ws) ws.send(JSON.stringify({ type: "set_vendor", vendor: vendor }));
416
+ }
417
+
418
+ if (vendorBtnClaude) vendorBtnClaude.addEventListener("click", function() { onVendorClick("claude"); });
419
+ if (vendorBtnCodex) vendorBtnCodex.addEventListener("click", function() { onVendorClick("codex"); });
420
+
421
+ // --- Reactive UI sync ---
422
+ store.subscribe(function (state, prev) {
423
+ // Vendor toggle state
424
+ if (state.availableVendors !== prev.availableVendors ||
425
+ state.installedVendors !== prev.installedVendors ||
426
+ state.currentVendor !== prev.currentVendor) {
427
+ updateVendorToggle();
428
+ }
429
+
430
+ // richContextUsage changed -> update popover + panel
431
+ if (state.richContextUsage !== prev.richContextUsage) {
432
+ if (state.richContextUsage) {
433
+ var hce = store.get('headerContextEl');
434
+ if (hce) hce.removeAttribute("data-tip");
435
+ if (state.ctxPopoverVisible) renderCtxPopover();
436
+ } else {
437
+ hideCtxPopover();
438
+ }
439
+ updateContextPanel();
440
+ }
441
+ // Vendor changed -> switch model list and current model to match
442
+ if (state.currentVendor !== prev.currentVendor && state.currentVendor) {
443
+ var ws = getWs();
444
+ if (ws) ws.send(JSON.stringify({ type: "get_vendor_models", vendor: state.currentVendor }));
445
+ }
446
+
447
+ // config chip
448
+ if (state.currentModel !== prev.currentModel ||
449
+ state.currentMode !== prev.currentMode ||
450
+ state.currentEffort !== prev.currentEffort ||
451
+ state.currentBetas !== prev.currentBetas ||
452
+ state.currentThinking !== prev.currentThinking ||
453
+ state.currentVendor !== prev.currentVendor ||
454
+ state.codexApproval !== prev.codexApproval ||
455
+ state.codexSandbox !== prev.codexSandbox ||
456
+ state.codexWebSearch !== prev.codexWebSearch) {
457
+ updateConfigChip();
458
+ }
459
+ });
304
460
 
305
461
  // Usage panel DOM refs
306
462
  usagePanel = $("usage-panel");
@@ -353,7 +509,7 @@ export function initPanels() {
353
509
  var val = parseInt(this.value, 10);
354
510
  if (isNaN(val) || val < 1024) val = 1024;
355
511
  if (val > 128000) val = 128000;
356
- store.setState({ currentThinkingBudget: val });
512
+ store.set({ currentThinkingBudget: val });
357
513
  this.value = val;
358
514
  var ws = getWs();
359
515
  if (ws && ws.readyState === 1) {
@@ -366,7 +522,7 @@ export function initPanels() {
366
522
  configBeta1mBtn.addEventListener("click", function (e) {
367
523
  e.stopPropagation();
368
524
  var active = hasBeta("context-1m");
369
- var betas = store.getState().currentBetas;
525
+ var betas = store.get('currentBetas');
370
526
  var newBetas;
371
527
  if (active) {
372
528
  // Remove context-1m beta
@@ -443,30 +599,27 @@ export function initPanels() {
443
599
  export function updateConfigChip() {
444
600
  if (!configChipWrap || !configChip) return;
445
601
  configChipWrap.classList.remove("hidden");
446
- var s = store.getState();
447
- var parts = [modelDisplayName(s.currentModel, s.currentModels)];
448
- parts.push(modeDisplayName(s.currentMode));
449
- // Only show effort if model supports it
450
- var modelSupportsEffort = getModelSupportsEffort();
451
- if (modelSupportsEffort) {
452
- parts.push(effortDisplayName(s.currentEffort));
453
- }
454
- if (s.currentThinking && s.currentThinking !== "adaptive") {
455
- parts.push(thinkingDisplayName(s.currentThinking));
456
- }
457
- if (hasBeta("context-1m")) {
458
- parts.push("1M");
459
- }
460
- configChipLabel.textContent = parts.join(" \u00b7 ");
602
+ var s = store.snap();
603
+ var vendor = s.currentVendor || "claude";
604
+ configChipLabel.textContent = modelDisplayName(s.currentModel, s.currentModels);
461
605
  rebuildModelList();
462
606
  rebuildModeList();
463
607
  rebuildEffortBar();
608
+
609
+ // Vendor-specific sections
610
+ var isClaude = vendor === "claude";
611
+ // MODE, THINKING, BETA are Claude-only
612
+ if (configModeList && configModeList.parentElement) configModeList.parentElement.style.display = isClaude ? "" : "none";
464
613
  rebuildThinkingSection();
465
- rebuildBetaSection();
614
+ if (configThinkingSection) configThinkingSection.style.display = isClaude ? "" : "none";
615
+ // BETA section deprecated (1M context is now standard)
616
+ if (configBetaSection) configBetaSection.style.display = "none";
617
+ // APPROVAL, SANDBOX, WEB SEARCH are Codex-only
618
+ rebuildCodexSections();
466
619
  }
467
620
 
468
621
  export function getModelSupportsEffort() {
469
- var s = store.getState();
622
+ var s = store.snap();
470
623
  if (!s.currentModels || s.currentModels.length === 0) return true; // assume yes if no info
471
624
  for (var i = 0; i < s.currentModels.length; i++) {
472
625
  if (s.currentModels[i].value === s.currentModel) {
@@ -478,17 +631,19 @@ export function getModelSupportsEffort() {
478
631
  }
479
632
 
480
633
  export function getModelEffortLevels() {
481
- var s = store.getState();
482
- if (!s.currentModels || s.currentModels.length === 0) return EFFORT_LEVELS;
634
+ var s = store.snap();
635
+ var vendor = s.currentVendor || "claude";
636
+ var defaultLevels = EFFORT_LEVELS_BY_VENDOR[vendor] || EFFORT_LEVELS;
637
+ if (!s.currentModels || s.currentModels.length === 0) return defaultLevels;
483
638
  for (var i = 0; i < s.currentModels.length; i++) {
484
639
  if (s.currentModels[i].value === s.currentModel) {
485
640
  if (s.currentModels[i].supportedEffortLevels && s.currentModels[i].supportedEffortLevels.length > 0) {
486
641
  return s.currentModels[i].supportedEffortLevels;
487
642
  }
488
- return EFFORT_LEVELS;
643
+ return defaultLevels;
489
644
  }
490
645
  }
491
- return EFFORT_LEVELS;
646
+ return defaultLevels;
492
647
  }
493
648
 
494
649
  // --- Usage panel ---
@@ -520,7 +675,7 @@ export function accumulateUsage(cost, usage) {
520
675
  sessionUsage.cacheWrite += usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0;
521
676
  }
522
677
  sessionUsage.turns++;
523
- if (!store.getState().replayingHistory) updateUsagePanel();
678
+ if (!store.get('replayingHistory')) updateUsagePanel();
524
679
  }
525
680
 
526
681
  export function resetUsage() {
@@ -628,21 +783,21 @@ export function updateContextPanel() {
628
783
  // Header bar
629
784
  if (pct > 0) {
630
785
  var statusArea = document.querySelector(".title-bar-content .status");
631
- var hCtxEl = store.getState().headerContextEl;
786
+ var hCtxEl = store.get('headerContextEl');
632
787
  if (statusArea && !hCtxEl) {
633
788
  hCtxEl = document.createElement("div");
634
789
  hCtxEl.className = "header-context";
635
790
  hCtxEl.innerHTML = '<div class="header-context-bar"><div class="header-context-fill"></div></div><span class="header-context-label"></span>';
636
791
  statusArea.insertBefore(hCtxEl, statusArea.firstChild);
637
792
  hCtxEl.addEventListener("mouseenter", function() {
638
- if (store.getState().richContextUsage) {
793
+ if (store.get('richContextUsage')) {
639
794
  showCtxPopover();
640
795
  }
641
796
  });
642
797
  hCtxEl.addEventListener("mouseleave", function() {
643
798
  ctxHoverTimer = setTimeout(hideCtxPopover, 120);
644
799
  });
645
- store.setState({ headerContextEl: hCtxEl });
800
+ store.set({ headerContextEl: hCtxEl });
646
801
  }
647
802
  if (hCtxEl) {
648
803
  var hFill = hCtxEl.querySelector(".header-context-fill");
@@ -651,7 +806,7 @@ export function updateContextPanel() {
651
806
  hFill.className = "header-context-fill" + cls;
652
807
  hLabel.textContent = pct.toFixed(0) + "%";
653
808
  // Use data-tip as fallback when rich data is not yet loaded
654
- if (store.getState().richContextUsage) {
809
+ if (store.get('richContextUsage')) {
655
810
  hCtxEl.removeAttribute("data-tip");
656
811
  } else {
657
812
  hCtxEl.dataset.tip = "Context window " + pct.toFixed(0) + "% used (" + formatTokens(used) + " / " + formatTokens(win) + " tokens)";
@@ -701,7 +856,7 @@ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens
701
856
  if (mu.maxOutputTokens) contextData.maxOutputTokens = mu.maxOutputTokens;
702
857
  }
703
858
  }
704
- if (!store.getState().replayingHistory) updateContextPanel();
859
+ if (!store.get('replayingHistory')) updateContextPanel();
705
860
  }
706
861
 
707
862
  // contextView: "off" | "mini" | "panel"
@@ -721,9 +876,8 @@ export function applyContextView(view) {
721
876
 
722
877
  export function resetContextData() {
723
878
  contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
724
- store.setState({ richContextUsage: null });
725
- hideCtxPopover();
726
- updateContextPanel();
879
+ store.set({ richContextUsage: null });
880
+ // hideCtxPopover + updateContextPanel handled by store subscriber
727
881
  }
728
882
 
729
883
  export function resetContext() {
@@ -770,24 +924,24 @@ export function ensureCtxPopover() {
770
924
  }
771
925
 
772
926
  export function showCtxPopover() {
773
- var s = store.getState();
927
+ var s = store.snap();
774
928
  if (!s.headerContextEl || !s.richContextUsage) return;
775
929
  if (ctxHoverTimer) { clearTimeout(ctxHoverTimer); ctxHoverTimer = null; }
776
930
  ensureCtxPopover();
777
931
  s.headerContextEl.appendChild(ctxPopoverEl);
778
932
  renderCtxPopover();
779
933
  ctxPopoverEl.classList.remove("hidden");
780
- store.setState({ ctxPopoverVisible: true });
934
+ store.set({ ctxPopoverVisible: true });
781
935
  }
782
936
 
783
937
  export function hideCtxPopover() {
784
938
  if (!ctxPopoverEl) return;
785
939
  ctxPopoverEl.classList.add("hidden");
786
- store.setState({ ctxPopoverVisible: false });
940
+ store.set({ ctxPopoverVisible: false });
787
941
  }
788
942
 
789
943
  export function renderCtxPopover() {
790
- var richContextUsage = store.getState().richContextUsage;
944
+ var richContextUsage = store.get('richContextUsage');
791
945
  if (!ctxPopoverEl || !richContextUsage) return;
792
946
  var d = richContextUsage;
793
947
  var cats = d.categories || [];