clay-server 2.27.0-beta.11 → 2.27.0-beta.13

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.
@@ -0,0 +1,907 @@
1
+ // app-panels.js - Config chip, usage panel, status panel, context panel
2
+ // Extracted from app.js (PR-30)
3
+
4
+ import { refreshIcons } from "./icons.js";
5
+ import { escapeHtml } from "./utils.js";
6
+
7
+ var _ctx = null;
8
+
9
+ // --- Module-owned state ---
10
+ var currentModels = [];
11
+ var currentModel = "";
12
+ var currentMode = "default";
13
+ var currentEffort = "medium";
14
+ var currentBetas = [];
15
+ var currentThinking = "adaptive";
16
+ var currentThinkingBudget = 10000;
17
+ var sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
18
+ var contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
19
+ var headerContextEl = null;
20
+ var richContextUsage = null;
21
+ var ctxPopoverEl = null;
22
+ var ctxPopoverVisible = false;
23
+ var ctxHoverTimer = null;
24
+ var statusRefreshTimer = null;
25
+
26
+ // --- DOM refs ---
27
+ var configChipWrap = null;
28
+ var configChip = null;
29
+ var configChipLabel = null;
30
+ var configPopover = null;
31
+ var configModelList = null;
32
+ var configModeList = null;
33
+ var configEffortSection = null;
34
+ var configEffortBar = null;
35
+ var configBetaSection = null;
36
+ var configBeta1mBtn = null;
37
+ var configThinkingSection = null;
38
+ var configThinkingBar = null;
39
+ var configThinkingBudgetRow = null;
40
+ var configThinkingBudgetInput = null;
41
+
42
+ var usagePanel = null;
43
+ var usagePanelClose = null;
44
+ var usageCostEl = null;
45
+ var usageInputEl = null;
46
+ var usageOutputEl = null;
47
+ var usageCacheReadEl = null;
48
+ var usageCacheWriteEl = null;
49
+ var usageTurnsEl = null;
50
+
51
+ var statusPanel = null;
52
+ var statusPanelClose = null;
53
+ var statusPidEl = null;
54
+ var statusUptimeEl = null;
55
+ var statusRssEl = null;
56
+ var statusHeapUsedEl = null;
57
+ var statusHeapTotalEl = null;
58
+ var statusExternalEl = null;
59
+ var statusSessionsEl = null;
60
+ var statusProcessingEl = null;
61
+ var statusClientsEl = null;
62
+ var statusTerminalsEl = null;
63
+
64
+ var contextPanel = null;
65
+ var contextPanelClose = null;
66
+ var contextPanelMinimize = null;
67
+ var contextBarFill = null;
68
+ var contextBarPct = null;
69
+ var contextUsedEl = null;
70
+ var contextWindowEl = null;
71
+ var contextMaxOutputEl = null;
72
+ var contextInputEl = null;
73
+ var contextOutputEl = null;
74
+ var contextCacheReadEl = null;
75
+ var contextCacheWriteEl = null;
76
+ var contextModelEl = null;
77
+ var contextCostEl = null;
78
+ var contextTurnsEl = null;
79
+ var contextMini = null;
80
+ var contextMiniFill = null;
81
+ var contextMiniLabel = null;
82
+
83
+ // --- Constants ---
84
+ var MODE_OPTIONS = [
85
+ { value: "default", label: "Default" },
86
+ { value: "plan", label: "Plan" },
87
+ { value: "acceptEdits", label: "Auto-accept edits" },
88
+ ];
89
+ var MODE_FULL_AUTO = { value: "bypassPermissions", label: "Full auto" };
90
+ var EFFORT_LEVELS = ["low", "medium", "high", "max"];
91
+ var THINKING_OPTIONS = ["disabled", "adaptive", "budget"];
92
+ var KNOWN_CONTEXT_WINDOWS = {
93
+ "opus-4-6": 1000000,
94
+ "claude-sonnet-4": 1000000
95
+ };
96
+ // Categories to hide from the legend (noise, not actionable)
97
+ var CTX_HIDDEN_CATS = { "Free space": 1, "Autocompact buffer": 1 };
98
+
99
+ // --- State accessors ---
100
+ export function getCurrentModel() { return currentModel; }
101
+ export function setCurrentModel(v) { currentModel = v; }
102
+ export function getCurrentModels() { return currentModels; }
103
+ export function setCurrentModels(v) { currentModels = v; }
104
+ export function getCurrentMode() { return currentMode; }
105
+ export function setCurrentMode(v) { currentMode = v; }
106
+ export function getCurrentEffort() { return currentEffort; }
107
+ export function setCurrentEffort(v) { currentEffort = v; }
108
+ export function getCurrentBetas() { return currentBetas; }
109
+ export function setCurrentBetas(v) { currentBetas = v; }
110
+ export function getCurrentThinking() { return currentThinking; }
111
+ export function setCurrentThinking(v) { currentThinking = v; }
112
+ export function getCurrentThinkingBudget() { return currentThinkingBudget; }
113
+ export function setCurrentThinkingBudget(v) { currentThinkingBudget = v; }
114
+ export function getSessionUsage() { return sessionUsage; }
115
+ export function setSessionUsage(v) { sessionUsage = v; }
116
+ export function getContextData() { return contextData; }
117
+ export function setContextData(v) { contextData = v; }
118
+ export function getHeaderContextEl() { return headerContextEl; }
119
+ export function setHeaderContextEl(v) { headerContextEl = v; }
120
+ export function getRichContextUsage() { return richContextUsage; }
121
+ export function setRichContextUsage(v) { richContextUsage = v; }
122
+ export function getCtxPopoverVisible() { return ctxPopoverVisible; }
123
+
124
+ // --- Internal helpers ---
125
+
126
+ function modelDisplayName(value, models) {
127
+ if (!value) return "";
128
+ if (models) {
129
+ for (var i = 0; i < models.length; i++) {
130
+ if (models[i].value === value && models[i].displayName) return models[i].displayName;
131
+ }
132
+ }
133
+ return value;
134
+ }
135
+
136
+ function modeDisplayName(value) {
137
+ for (var i = 0; i < MODE_OPTIONS.length; i++) {
138
+ if (MODE_OPTIONS[i].value === value) return MODE_OPTIONS[i].label;
139
+ }
140
+ if (value === "bypassPermissions") return "Full auto";
141
+ if (value === "dontAsk") return "Don\u2019t ask";
142
+ return value;
143
+ }
144
+
145
+ function effortDisplayName(value) {
146
+ if (!value) return "";
147
+ return value.charAt(0).toUpperCase() + value.slice(1);
148
+ }
149
+
150
+ function thinkingDisplayName(value) {
151
+ if (value === "disabled") return "Off";
152
+ if (value === "adaptive") return "Adaptive";
153
+ if (value === "budget") return "Budget";
154
+ return value || "Adaptive";
155
+ }
156
+
157
+ function isSonnetModel(model) {
158
+ if (!model) return false;
159
+ var lower = model.toLowerCase();
160
+ return lower.indexOf("sonnet") !== -1;
161
+ }
162
+
163
+ function hasBeta(name) {
164
+ for (var i = 0; i < currentBetas.length; i++) {
165
+ if (currentBetas[i].indexOf(name) !== -1) return true;
166
+ }
167
+ return false;
168
+ }
169
+
170
+ function rebuildModelList() {
171
+ if (!configModelList) return;
172
+ configModelList.innerHTML = "";
173
+ var list = currentModels.length > 0 ? currentModels : (currentModel ? [{ value: currentModel, displayName: currentModel }] : []);
174
+ for (var i = 0; i < list.length; i++) {
175
+ var item = list[i];
176
+ var value = item.value || "";
177
+ var label = item.displayName || value;
178
+ var btn = document.createElement("button");
179
+ btn.className = "config-radio-item";
180
+ if (value === currentModel) btn.classList.add("active");
181
+ btn.dataset.model = value;
182
+ btn.textContent = label;
183
+ btn.addEventListener("click", function () {
184
+ var model = this.dataset.model;
185
+ var ws = _ctx.getWs();
186
+ if (ws && ws.readyState === 1) {
187
+ ws.send(JSON.stringify({ type: "set_model", model: model }));
188
+ }
189
+ configPopover.classList.add("hidden");
190
+ configChip.classList.remove("active");
191
+ });
192
+ configModelList.appendChild(btn);
193
+ }
194
+ }
195
+
196
+ function rebuildModeList() {
197
+ if (!configModeList) return;
198
+ configModeList.innerHTML = "";
199
+ var options = MODE_OPTIONS.slice();
200
+ if (_ctx.skipPermsEnabled) {
201
+ options.push(MODE_FULL_AUTO);
202
+ }
203
+ for (var i = 0; i < options.length; i++) {
204
+ var opt = options[i];
205
+ var btn = document.createElement("button");
206
+ btn.className = "config-radio-item";
207
+ if (opt.value === currentMode) btn.classList.add("active");
208
+ btn.dataset.mode = opt.value;
209
+ btn.textContent = opt.label;
210
+ btn.addEventListener("click", function () {
211
+ var mode = this.dataset.mode;
212
+ var ws = _ctx.getWs();
213
+ if (ws && ws.readyState === 1) {
214
+ ws.send(JSON.stringify({ type: "set_permission_mode", mode: mode }));
215
+ }
216
+ configPopover.classList.add("hidden");
217
+ configChip.classList.remove("active");
218
+ });
219
+ configModeList.appendChild(btn);
220
+ }
221
+ }
222
+
223
+ function rebuildEffortBar() {
224
+ if (!configEffortBar || !configEffortSection) return;
225
+ var supportsEffort = getModelSupportsEffort();
226
+ if (!supportsEffort) {
227
+ configEffortSection.style.display = "none";
228
+ return;
229
+ }
230
+ configEffortSection.style.display = "";
231
+ configEffortBar.innerHTML = "";
232
+ var levels = getModelEffortLevels();
233
+ for (var i = 0; i < levels.length; i++) {
234
+ var level = levels[i];
235
+ var btn = document.createElement("button");
236
+ btn.className = "config-segment-btn";
237
+ if (level === currentEffort) btn.classList.add("active");
238
+ btn.dataset.effort = level;
239
+ btn.textContent = effortDisplayName(level);
240
+ btn.addEventListener("click", function () {
241
+ var effort = this.dataset.effort;
242
+ var ws = _ctx.getWs();
243
+ if (ws && ws.readyState === 1) {
244
+ ws.send(JSON.stringify({ type: "set_effort", effort: effort }));
245
+ }
246
+ configPopover.classList.add("hidden");
247
+ configChip.classList.remove("active");
248
+ });
249
+ configEffortBar.appendChild(btn);
250
+ }
251
+ }
252
+
253
+ function rebuildBetaSection() {
254
+ if (!configBetaSection || !configBeta1mBtn) return;
255
+ // Only show for Sonnet models
256
+ if (!isSonnetModel(currentModel)) {
257
+ configBetaSection.style.display = "none";
258
+ return;
259
+ }
260
+ configBetaSection.style.display = "";
261
+ var active = hasBeta("context-1m");
262
+ configBeta1mBtn.classList.toggle("active", active);
263
+ configBeta1mBtn.setAttribute("aria-checked", active ? "true" : "false");
264
+ }
265
+
266
+ function rebuildThinkingSection() {
267
+ if (!configThinkingBar || !configThinkingSection) return;
268
+ configThinkingSection.style.display = "";
269
+ configThinkingBar.innerHTML = "";
270
+ for (var i = 0; i < THINKING_OPTIONS.length; i++) {
271
+ var opt = THINKING_OPTIONS[i];
272
+ var btn = document.createElement("button");
273
+ btn.className = "config-segment-btn";
274
+ if (opt === currentThinking) btn.classList.add("active");
275
+ btn.dataset.thinking = opt;
276
+ btn.textContent = thinkingDisplayName(opt);
277
+ btn.addEventListener("click", function () {
278
+ var thinking = this.dataset.thinking;
279
+ var msg = { type: "set_thinking", thinking: thinking };
280
+ if (thinking === "budget") {
281
+ msg.budgetTokens = currentThinkingBudget;
282
+ }
283
+ var ws = _ctx.getWs();
284
+ if (ws && ws.readyState === 1) {
285
+ ws.send(JSON.stringify(msg));
286
+ }
287
+ });
288
+ configThinkingBar.appendChild(btn);
289
+ }
290
+ // Show/hide budget input
291
+ if (configThinkingBudgetRow) {
292
+ configThinkingBudgetRow.style.display = currentThinking === "budget" ? "" : "none";
293
+ }
294
+ if (configThinkingBudgetInput) {
295
+ configThinkingBudgetInput.value = currentThinkingBudget;
296
+ }
297
+ }
298
+
299
+ function escHtml(s) {
300
+ var div = document.createElement("div");
301
+ div.textContent = s;
302
+ return div.innerHTML;
303
+ }
304
+
305
+ function em(emoji) {
306
+ return '<span class="ctx-emoji">' + emoji + '</span>';
307
+ }
308
+
309
+ // --- Exported functions ---
310
+
311
+ export function initPanels(ctx) {
312
+ _ctx = ctx;
313
+ var $ = _ctx.$;
314
+
315
+ // Config chip DOM refs
316
+ configChipWrap = $("config-chip-wrap");
317
+ configChip = $("config-chip");
318
+ configChipLabel = $("config-chip-label");
319
+ configPopover = $("config-popover");
320
+ configModelList = $("config-model-list");
321
+ configModeList = $("config-mode-list");
322
+ configEffortSection = $("config-effort-section");
323
+ configEffortBar = $("config-effort-bar");
324
+ configBetaSection = $("config-beta-section");
325
+ configBeta1mBtn = $("config-beta-1m");
326
+ configThinkingSection = $("config-thinking-section");
327
+ configThinkingBar = $("config-thinking-bar");
328
+ configThinkingBudgetRow = $("config-thinking-budget-row");
329
+ configThinkingBudgetInput = $("config-thinking-budget");
330
+
331
+ // Usage panel DOM refs
332
+ usagePanel = $("usage-panel");
333
+ usagePanelClose = $("usage-panel-close");
334
+ usageCostEl = $("usage-cost");
335
+ usageInputEl = $("usage-input");
336
+ usageOutputEl = $("usage-output");
337
+ usageCacheReadEl = $("usage-cache-read");
338
+ usageCacheWriteEl = $("usage-cache-write");
339
+ usageTurnsEl = $("usage-turns");
340
+
341
+ // Status panel DOM refs
342
+ statusPanel = $("status-panel");
343
+ statusPanelClose = $("status-panel-close");
344
+ statusPidEl = $("status-pid");
345
+ statusUptimeEl = $("status-uptime");
346
+ statusRssEl = $("status-rss");
347
+ statusHeapUsedEl = $("status-heap-used");
348
+ statusHeapTotalEl = $("status-heap-total");
349
+ statusExternalEl = $("status-external");
350
+ statusSessionsEl = $("status-sessions");
351
+ statusProcessingEl = $("status-processing");
352
+ statusClientsEl = $("status-clients");
353
+ statusTerminalsEl = $("status-terminals");
354
+
355
+ // Context panel DOM refs
356
+ contextPanel = $("context-panel");
357
+ contextPanelClose = $("context-panel-close");
358
+ contextPanelMinimize = $("context-panel-minimize");
359
+ contextBarFill = $("context-bar-fill");
360
+ contextBarPct = $("context-bar-pct");
361
+ contextUsedEl = $("context-used");
362
+ contextWindowEl = $("context-window");
363
+ contextMaxOutputEl = $("context-max-output");
364
+ contextInputEl = $("context-input");
365
+ contextOutputEl = $("context-output");
366
+ contextCacheReadEl = $("context-cache-read");
367
+ contextCacheWriteEl = $("context-cache-write");
368
+ contextModelEl = $("context-model");
369
+ contextCostEl = $("context-cost");
370
+ contextTurnsEl = $("context-turns");
371
+ contextMini = $("context-mini");
372
+ contextMiniFill = $("context-mini-fill");
373
+ contextMiniLabel = $("context-mini-label");
374
+
375
+ // --- Event listeners ---
376
+
377
+ if (configThinkingBudgetInput) {
378
+ configThinkingBudgetInput.addEventListener("change", function () {
379
+ var val = parseInt(this.value, 10);
380
+ if (isNaN(val) || val < 1024) val = 1024;
381
+ if (val > 128000) val = 128000;
382
+ currentThinkingBudget = val;
383
+ this.value = val;
384
+ var ws = _ctx.getWs();
385
+ if (ws && ws.readyState === 1) {
386
+ ws.send(JSON.stringify({ type: "set_thinking", thinking: "budget", budgetTokens: val }));
387
+ }
388
+ });
389
+ }
390
+
391
+ if (configBeta1mBtn) {
392
+ configBeta1mBtn.addEventListener("click", function (e) {
393
+ e.stopPropagation();
394
+ var active = hasBeta("context-1m");
395
+ var newBetas;
396
+ if (active) {
397
+ // Remove context-1m beta
398
+ newBetas = [];
399
+ for (var i = 0; i < currentBetas.length; i++) {
400
+ if (currentBetas[i].indexOf("context-1m") === -1) {
401
+ newBetas.push(currentBetas[i]);
402
+ }
403
+ }
404
+ } else {
405
+ // Add context-1m beta
406
+ newBetas = currentBetas.slice();
407
+ newBetas.push("context-1m-2025-08-07");
408
+ }
409
+ var ws = _ctx.getWs();
410
+ if (ws && ws.readyState === 1) {
411
+ ws.send(JSON.stringify({ type: "set_betas", betas: newBetas }));
412
+ }
413
+ });
414
+ }
415
+
416
+ if (configChip) {
417
+ configChip.addEventListener("click", function (e) {
418
+ e.stopPropagation();
419
+ var wasHidden = configPopover.classList.toggle("hidden");
420
+ configChip.classList.toggle("active", !wasHidden);
421
+ });
422
+ }
423
+
424
+ document.addEventListener("click", function (e) {
425
+ if (configPopover && configChip && !configPopover.contains(e.target) && e.target !== configChip) {
426
+ configPopover.classList.add("hidden");
427
+ configChip.classList.remove("active");
428
+ }
429
+ });
430
+
431
+ if (usagePanelClose) {
432
+ usagePanelClose.addEventListener("click", function () {
433
+ usagePanel.classList.add("hidden");
434
+ });
435
+ }
436
+
437
+ if (statusPanelClose) {
438
+ statusPanelClose.addEventListener("click", function () {
439
+ statusPanel.classList.add("hidden");
440
+ if (statusRefreshTimer) {
441
+ clearInterval(statusRefreshTimer);
442
+ statusRefreshTimer = null;
443
+ }
444
+ });
445
+ }
446
+
447
+ if (contextPanelClose) {
448
+ contextPanelClose.addEventListener("click", function () {
449
+ setContextView("off");
450
+ applyContextView("off");
451
+ });
452
+ }
453
+
454
+ if (contextPanelMinimize) {
455
+ contextPanelMinimize.addEventListener("click", minimizeContext);
456
+ }
457
+
458
+ if (contextMini) {
459
+ contextMini.addEventListener("click", expandContext);
460
+ }
461
+
462
+ // Restore context view on load
463
+ applyContextView(getContextView());
464
+ }
465
+
466
+ // --- Config chip ---
467
+
468
+ export function updateConfigChip() {
469
+ if (!configChipWrap || !configChip) return;
470
+ configChipWrap.classList.remove("hidden");
471
+ var parts = [modelDisplayName(currentModel, currentModels)];
472
+ parts.push(modeDisplayName(currentMode));
473
+ // Only show effort if model supports it
474
+ var modelSupportsEffort = getModelSupportsEffort();
475
+ if (modelSupportsEffort) {
476
+ parts.push(effortDisplayName(currentEffort));
477
+ }
478
+ if (currentThinking && currentThinking !== "adaptive") {
479
+ parts.push(thinkingDisplayName(currentThinking));
480
+ }
481
+ if (hasBeta("context-1m")) {
482
+ parts.push("1M");
483
+ }
484
+ configChipLabel.textContent = parts.join(" \u00b7 ");
485
+ rebuildModelList();
486
+ rebuildModeList();
487
+ rebuildEffortBar();
488
+ rebuildThinkingSection();
489
+ rebuildBetaSection();
490
+ }
491
+
492
+ export function getModelSupportsEffort() {
493
+ if (!currentModels || currentModels.length === 0) return true; // assume yes if no info
494
+ for (var i = 0; i < currentModels.length; i++) {
495
+ if (currentModels[i].value === currentModel) {
496
+ if (currentModels[i].supportsEffort === false) return false;
497
+ return true;
498
+ }
499
+ }
500
+ return true;
501
+ }
502
+
503
+ export function getModelEffortLevels() {
504
+ if (!currentModels || currentModels.length === 0) return EFFORT_LEVELS;
505
+ for (var i = 0; i < currentModels.length; i++) {
506
+ if (currentModels[i].value === currentModel) {
507
+ if (currentModels[i].supportedEffortLevels && currentModels[i].supportedEffortLevels.length > 0) {
508
+ return currentModels[i].supportedEffortLevels;
509
+ }
510
+ return EFFORT_LEVELS;
511
+ }
512
+ }
513
+ return EFFORT_LEVELS;
514
+ }
515
+
516
+ // --- Usage panel ---
517
+
518
+ export function formatTokens(n) {
519
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + "M";
520
+ if (n >= 1000) return (n / 1000).toFixed(1) + "K";
521
+ return String(n);
522
+ }
523
+
524
+ export function updateUsagePanel() {
525
+ if (!usageCostEl) return;
526
+ usageCostEl.textContent = "$" + sessionUsage.cost.toFixed(4);
527
+ usageInputEl.textContent = formatTokens(sessionUsage.input);
528
+ usageOutputEl.textContent = formatTokens(sessionUsage.output);
529
+ usageCacheReadEl.textContent = formatTokens(sessionUsage.cacheRead);
530
+ usageCacheWriteEl.textContent = formatTokens(sessionUsage.cacheWrite);
531
+ usageTurnsEl.textContent = String(sessionUsage.turns);
532
+ }
533
+
534
+ export function accumulateUsage(cost, usage) {
535
+ // cost is the SDK's total_cost_usd -- a cumulative running total, not a delta.
536
+ // Assign directly instead of summing to avoid overcounting.
537
+ if (cost != null) sessionUsage.cost = cost;
538
+ if (usage) {
539
+ sessionUsage.input += usage.input_tokens || usage.inputTokens || 0;
540
+ sessionUsage.output += usage.output_tokens || usage.outputTokens || 0;
541
+ sessionUsage.cacheRead += usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0;
542
+ sessionUsage.cacheWrite += usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0;
543
+ }
544
+ sessionUsage.turns++;
545
+ if (!_ctx.getReplayingHistory()) updateUsagePanel();
546
+ }
547
+
548
+ export function resetUsage() {
549
+ sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
550
+ updateUsagePanel();
551
+ if (usagePanel) usagePanel.classList.add("hidden");
552
+ }
553
+
554
+ export function toggleUsagePanel() {
555
+ if (!usagePanel) return;
556
+ usagePanel.classList.toggle("hidden");
557
+ refreshIcons();
558
+ }
559
+
560
+ // --- Status panel ---
561
+
562
+ export function formatBytes(n) {
563
+ if (n >= 1073741824) return (n / 1073741824).toFixed(1) + " GB";
564
+ if (n >= 1048576) return (n / 1048576).toFixed(1) + " MB";
565
+ if (n >= 1024) return (n / 1024).toFixed(1) + " KB";
566
+ return n + " B";
567
+ }
568
+
569
+ export function formatUptime(seconds) {
570
+ var d = Math.floor(seconds / 86400);
571
+ var h = Math.floor((seconds % 86400) / 3600);
572
+ var m = Math.floor((seconds % 3600) / 60);
573
+ var s = Math.floor(seconds % 60);
574
+ if (d > 0) return d + "d " + h + "h " + m + "m";
575
+ if (h > 0) return h + "h " + m + "m " + s + "s";
576
+ return m + "m " + s + "s";
577
+ }
578
+
579
+ export function updateStatusPanel(data) {
580
+ if (!statusPidEl) return;
581
+ statusPidEl.textContent = String(data.pid);
582
+ statusUptimeEl.textContent = formatUptime(data.uptime);
583
+ statusRssEl.textContent = formatBytes(data.memory.rss);
584
+ statusHeapUsedEl.textContent = formatBytes(data.memory.heapUsed);
585
+ statusHeapTotalEl.textContent = formatBytes(data.memory.heapTotal);
586
+ statusExternalEl.textContent = formatBytes(data.memory.external);
587
+ statusSessionsEl.textContent = String(data.sessions);
588
+ statusProcessingEl.textContent = String(data.processing);
589
+ statusClientsEl.textContent = String(data.clients);
590
+ statusTerminalsEl.textContent = String(data.terminals);
591
+ }
592
+
593
+ export function requestProcessStats() {
594
+ var ws = _ctx.getWs();
595
+ if (ws && ws.readyState === 1) {
596
+ ws.send(JSON.stringify({ type: "process_stats" }));
597
+ }
598
+ }
599
+
600
+ export function toggleStatusPanel() {
601
+ if (!statusPanel) return;
602
+ var opening = statusPanel.classList.contains("hidden");
603
+ statusPanel.classList.toggle("hidden");
604
+ if (opening) {
605
+ requestProcessStats();
606
+ statusRefreshTimer = setInterval(requestProcessStats, 5000);
607
+ } else {
608
+ if (statusRefreshTimer) {
609
+ clearInterval(statusRefreshTimer);
610
+ statusRefreshTimer = null;
611
+ }
612
+ }
613
+ refreshIcons();
614
+ }
615
+
616
+ // --- Context panel ---
617
+
618
+ export function resolveContextWindow(model, sdkValue) {
619
+ if (sdkValue) return sdkValue;
620
+ var lc = (model || "").toLowerCase();
621
+ for (var key in KNOWN_CONTEXT_WINDOWS) {
622
+ if (lc.includes(key)) return KNOWN_CONTEXT_WINDOWS[key];
623
+ }
624
+ return 200000;
625
+ }
626
+
627
+ export function contextPctClass(pct) {
628
+ return pct >= 85 ? " danger" : pct >= 60 ? " warn" : "";
629
+ }
630
+
631
+ export function updateContextPanel() {
632
+ if (!contextUsedEl) return;
633
+ // Context window usage = input tokens only (includes cache read/write)
634
+ var used = contextData.input;
635
+ var win = contextData.contextWindow;
636
+ var pct = win > 0 ? Math.min(100, (used / win) * 100) : 0;
637
+ var cls = contextPctClass(pct);
638
+ // Panel bar
639
+ contextBarFill.style.width = pct.toFixed(1) + "%";
640
+ contextBarFill.className = "context-bar-fill" + cls;
641
+ contextBarPct.textContent = pct.toFixed(0) + "%";
642
+ // Mini bar
643
+ if (contextMiniFill) {
644
+ contextMiniFill.style.width = pct.toFixed(1) + "%";
645
+ contextMiniFill.className = "context-mini-fill" + cls;
646
+ }
647
+ if (contextMiniLabel) {
648
+ contextMiniLabel.textContent = (win > 0 ? formatTokens(used) + "/" + formatTokens(win) : "0%");
649
+ }
650
+ // Header bar
651
+ if (pct > 0) {
652
+ var statusArea = document.querySelector(".title-bar-content .status");
653
+ if (statusArea && !headerContextEl) {
654
+ headerContextEl = document.createElement("div");
655
+ headerContextEl.className = "header-context";
656
+ headerContextEl.innerHTML = '<div class="header-context-bar"><div class="header-context-fill"></div></div><span class="header-context-label"></span>';
657
+ statusArea.insertBefore(headerContextEl, statusArea.firstChild);
658
+ headerContextEl.addEventListener("mouseenter", function() {
659
+ if (richContextUsage) {
660
+ showCtxPopover();
661
+ }
662
+ });
663
+ headerContextEl.addEventListener("mouseleave", function() {
664
+ ctxHoverTimer = setTimeout(hideCtxPopover, 120);
665
+ });
666
+ }
667
+ if (headerContextEl) {
668
+ var hFill = headerContextEl.querySelector(".header-context-fill");
669
+ var hLabel = headerContextEl.querySelector(".header-context-label");
670
+ hFill.style.width = pct.toFixed(1) + "%";
671
+ hFill.className = "header-context-fill" + cls;
672
+ hLabel.textContent = pct.toFixed(0) + "%";
673
+ // Use data-tip as fallback when rich data is not yet loaded
674
+ if (richContextUsage) {
675
+ headerContextEl.removeAttribute("data-tip");
676
+ } else {
677
+ headerContextEl.dataset.tip = "Context window " + pct.toFixed(0) + "% used (" + formatTokens(used) + " / " + formatTokens(win) + " tokens)";
678
+ }
679
+ }
680
+ }
681
+ contextUsedEl.textContent = formatTokens(used);
682
+ contextWindowEl.textContent = win > 0 ? formatTokens(win) : "-";
683
+ contextMaxOutputEl.textContent = contextData.maxOutputTokens > 0 ? formatTokens(contextData.maxOutputTokens) : "-";
684
+ contextInputEl.textContent = formatTokens(contextData.input);
685
+ contextOutputEl.textContent = formatTokens(contextData.output);
686
+ contextCacheReadEl.textContent = formatTokens(contextData.cacheRead);
687
+ contextCacheWriteEl.textContent = formatTokens(contextData.cacheWrite);
688
+ contextModelEl.textContent = contextData.model;
689
+ contextCostEl.textContent = "$" + contextData.cost.toFixed(4);
690
+ contextTurnsEl.textContent = String(contextData.turns);
691
+ }
692
+
693
+ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens) {
694
+ // cost is the SDK's total_cost_usd -- a cumulative running total, not a delta.
695
+ if (cost != null) contextData.cost = cost;
696
+ // Use latest turn values (not cumulative) since each turn's input_tokens
697
+ // already includes the full conversation context up to that point
698
+ if (usage) {
699
+ // Prefer per-call input_tokens from the last stream message_start event
700
+ // when available -- result.usage.input_tokens sums all API calls in a turn,
701
+ // inflating context usage when tools are involved.
702
+ // Falls back to the summed value for setups that don't emit message_start.
703
+ if (lastStreamInputTokens) {
704
+ contextData.input = lastStreamInputTokens;
705
+ } else {
706
+ contextData.input = (usage.input_tokens || usage.inputTokens || 0)
707
+ + (usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0);
708
+ }
709
+ contextData.output = usage.output_tokens || usage.outputTokens || 0;
710
+ contextData.cacheRead = usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0;
711
+ contextData.cacheWrite = usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0;
712
+ }
713
+ contextData.turns++;
714
+ if (modelUsage) {
715
+ var models = Object.keys(modelUsage);
716
+ if (models.length > 0) {
717
+ var m = models[0];
718
+ var mu = modelUsage[m];
719
+ contextData.model = m;
720
+ contextData.contextWindow = resolveContextWindow(m, mu.contextWindow);
721
+ if (mu.maxOutputTokens) contextData.maxOutputTokens = mu.maxOutputTokens;
722
+ }
723
+ }
724
+ if (!_ctx.getReplayingHistory()) updateContextPanel();
725
+ }
726
+
727
+ // contextView: "off" | "mini" | "panel"
728
+ export function getContextView() {
729
+ try { return localStorage.getItem("clay-context-view") || "off"; } catch (e) { return "off"; }
730
+ }
731
+
732
+ export function setContextView(v) {
733
+ try { localStorage.setItem("clay-context-view", v); } catch (e) {}
734
+ }
735
+
736
+ export function applyContextView(view) {
737
+ if (contextPanel) contextPanel.classList.toggle("hidden", view !== "panel");
738
+ if (contextMini) contextMini.classList.toggle("hidden", view !== "mini");
739
+ if (view === "panel") refreshIcons();
740
+ }
741
+
742
+ export function resetContextData() {
743
+ contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
744
+ richContextUsage = null;
745
+ hideCtxPopover();
746
+ updateContextPanel();
747
+ }
748
+
749
+ export function resetContext() {
750
+ resetContextData();
751
+ // Keep view state, just reset data
752
+ applyContextView(getContextView());
753
+ }
754
+
755
+ export function minimizeContext() {
756
+ setContextView("mini");
757
+ applyContextView("mini");
758
+ }
759
+
760
+ export function expandContext() {
761
+ setContextView("panel");
762
+ applyContextView("panel");
763
+ }
764
+
765
+ export function toggleContextPanel() {
766
+ if (!contextPanel) return;
767
+ var view = getContextView();
768
+ if (view === "panel") {
769
+ setContextView("mini");
770
+ applyContextView("mini");
771
+ } else {
772
+ setContextView("panel");
773
+ applyContextView("panel");
774
+ }
775
+ }
776
+
777
+ // --- Rich context usage popover ---
778
+
779
+ export function ensureCtxPopover() {
780
+ if (ctxPopoverEl) return;
781
+ ctxPopoverEl = document.createElement("div");
782
+ ctxPopoverEl.className = "context-usage-popover hidden";
783
+ // Keep popover open when hovering over it
784
+ ctxPopoverEl.addEventListener("mouseenter", function() {
785
+ if (ctxHoverTimer) { clearTimeout(ctxHoverTimer); ctxHoverTimer = null; }
786
+ });
787
+ ctxPopoverEl.addEventListener("mouseleave", function() {
788
+ hideCtxPopover();
789
+ });
790
+ }
791
+
792
+ export function showCtxPopover() {
793
+ if (!headerContextEl || !richContextUsage) return;
794
+ if (ctxHoverTimer) { clearTimeout(ctxHoverTimer); ctxHoverTimer = null; }
795
+ ensureCtxPopover();
796
+ headerContextEl.appendChild(ctxPopoverEl);
797
+ renderCtxPopover();
798
+ ctxPopoverEl.classList.remove("hidden");
799
+ ctxPopoverVisible = true;
800
+ }
801
+
802
+ export function hideCtxPopover() {
803
+ if (!ctxPopoverEl) return;
804
+ ctxPopoverEl.classList.add("hidden");
805
+ ctxPopoverVisible = false;
806
+ }
807
+
808
+ export function renderCtxPopover() {
809
+ if (!ctxPopoverEl || !richContextUsage) return;
810
+ var d = richContextUsage;
811
+ var cats = d.categories || [];
812
+ var total = d.totalTokens || 0;
813
+ var max = d.maxTokens || 0;
814
+ var pct = d.percentage != null ? d.percentage : (max > 0 ? (total / max) * 100 : 0);
815
+
816
+ var html = "";
817
+
818
+ // Header
819
+ html += '<div class="ctx-pop-header">';
820
+ html += '<span class="ctx-pop-model">' + escHtml(d.model || contextData.model || "-") + '</span>';
821
+ html += '<span class="ctx-pop-pct">' + pct.toFixed(0) + '%';
822
+ html += '<span class="ctx-pop-tokens">' + formatTokens(total) + ' / ' + formatTokens(max) + '</span>';
823
+ html += '</span>';
824
+ html += '</div>';
825
+
826
+ // Category emoji map
827
+ var CTX_EMOJI = {
828
+ "System prompt": "\ud83d\udcdc", "System tools": "\ud83d\udee0\ufe0f",
829
+ "Memory files": "\ud83d\udcc1", "Skills": "\u26a1", "Messages": "\ud83d\udcac",
830
+ "MCP tools": "\ud83d\udd0c", "Agents": "\ud83e\udd16", "Deferred tools": "\ud83d\udce6"
831
+ };
832
+
833
+ // Stacked bar
834
+ if (cats.length > 0 && max > 0) {
835
+ html += '<div class="ctx-cat-bar">';
836
+ for (var i = 0; i < cats.length; i++) {
837
+ var cat = cats[i];
838
+ if (cat.isDeferred || !cat.tokens || CTX_HIDDEN_CATS[cat.name]) continue;
839
+ var w = Math.max(0.3, (cat.tokens / max) * 100);
840
+ html += '<div style="width:' + w.toFixed(2) + '%;background:' + escHtml(cat.color) + '"></div>';
841
+ }
842
+ html += '</div>';
843
+
844
+ // Legend
845
+ html += '<div class="ctx-cat-legend">';
846
+ for (var j = 0; j < cats.length; j++) {
847
+ var c = cats[j];
848
+ if (c.isDeferred || !c.tokens || CTX_HIDDEN_CATS[c.name]) continue;
849
+ var emoji = CTX_EMOJI[c.name] || "\ud83d\udcca";
850
+ html += '<div class="ctx-cat-item">';
851
+ html += '<span class="ctx-cat-name">' + em(emoji) + ' ' + escHtml(c.name) + '</span>';
852
+ html += '<span class="ctx-cat-value">' + formatTokens(c.tokens) + '</span>';
853
+ html += '</div>';
854
+ }
855
+ html += '</div>';
856
+ }
857
+
858
+ // Message breakdown
859
+ var mb = d.messageBreakdown;
860
+ if (mb) {
861
+ html += '<div class="ctx-pop-divider"></div>';
862
+ html += '<div class="ctx-pop-section-label">' + em("\ud83d\udcac") + ' Messages</div>';
863
+ if (mb.userMessageTokens) {
864
+ html += '<div class="ctx-pop-row"><span class="ctx-pop-row-label">' + em("\ud83d\udc64") + ' User</span><span class="ctx-pop-row-value">' + formatTokens(mb.userMessageTokens) + '</span></div>';
865
+ }
866
+ if (mb.assistantMessageTokens) {
867
+ html += '<div class="ctx-pop-row"><span class="ctx-pop-row-label">' + em("\ud83e\udd16") + ' Assistant</span><span class="ctx-pop-row-value">' + formatTokens(mb.assistantMessageTokens) + '</span></div>';
868
+ }
869
+ if (mb.toolCallTokens) {
870
+ html += '<div class="ctx-pop-row"><span class="ctx-pop-row-label">' + em("\ud83d\udee0\ufe0f") + ' Tool calls</span><span class="ctx-pop-row-value">' + formatTokens(mb.toolCallTokens) + '</span></div>';
871
+ }
872
+ if (mb.toolResultTokens) {
873
+ html += '<div class="ctx-pop-row"><span class="ctx-pop-row-label">' + em("\ud83d\udccb") + ' Tool results</span><span class="ctx-pop-row-value">' + formatTokens(mb.toolResultTokens) + '</span></div>';
874
+ }
875
+ if (mb.attachmentTokens) {
876
+ html += '<div class="ctx-pop-row"><span class="ctx-pop-row-label">' + em("\ud83d\udcce") + ' Attachments</span><span class="ctx-pop-row-value">' + formatTokens(mb.attachmentTokens) + '</span></div>';
877
+ }
878
+ }
879
+
880
+ // Memory files
881
+ var mf = d.memoryFiles;
882
+ if (mf && mf.length > 0) {
883
+ html += '<div class="ctx-pop-divider"></div>';
884
+ html += '<div class="ctx-pop-section-label">' + em("\ud83d\udcc1") + ' Memory Files</div>';
885
+ var baseCount = {};
886
+ for (var mc = 0; mc < mf.length; mc++) {
887
+ var bn = mf[mc].path.split("/").pop() || mf[mc].path;
888
+ baseCount[bn] = (baseCount[bn] || 0) + 1;
889
+ }
890
+ for (var mi = 0; mi < mf.length; mi++) {
891
+ var fpath = mf[mi].path;
892
+ var fname = fpath.split("/").pop() || fpath;
893
+ if (baseCount[fname] > 1) {
894
+ var parts = fpath.split("/");
895
+ fname = parts.length >= 2 ? parts[parts.length - 2] + "/" + fname : fpath;
896
+ }
897
+ html += '<div class="ctx-pop-row"><span class="ctx-pop-row-label">' + em("\ud83d\udcc4") + ' ' + escHtml(fname) + '</span><span class="ctx-pop-row-value">' + formatTokens(mf[mi].tokens) + '</span></div>';
898
+ }
899
+ }
900
+
901
+ // Auto-compact note
902
+ if (d.isAutoCompactEnabled && d.autoCompactThreshold) {
903
+ html += '<div class="ctx-pop-note">' + em("\u267b\ufe0f") + ' Auto-compact at ' + formatTokens(d.autoCompactThreshold) + '</div>';
904
+ }
905
+
906
+ ctxPopoverEl.innerHTML = html;
907
+ }