clay-server 2.27.0-beta.9 → 2.27.0

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