clay-server 2.26.0-beta.3 → 2.26.0-beta.5

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.
@@ -342,6 +342,7 @@
342
342
  gap: 6px;
343
343
  width: 80px;
344
344
  cursor: default;
345
+ position: relative;
345
346
  }
346
347
 
347
348
  .header-context-bar {
@@ -370,6 +371,191 @@
370
371
  white-space: nowrap;
371
372
  }
372
373
 
374
+ /* --- Context usage popover --- */
375
+ .context-usage-popover {
376
+ position: absolute;
377
+ top: calc(100% + 10px);
378
+ right: -12px;
379
+ width: 320px;
380
+ background: var(--bg-alt);
381
+ border: 1px solid var(--border);
382
+ border-radius: 10px;
383
+ padding: 16px;
384
+ font-size: 12px;
385
+ color: var(--text-secondary);
386
+ box-shadow: 0 4px 16px rgba(var(--shadow-rgb), 0.4);
387
+ z-index: 200;
388
+ animation: ctx-popover-in 0.12s ease-out;
389
+ line-height: 1.5;
390
+ }
391
+
392
+ .context-usage-popover.hidden { display: none; }
393
+
394
+ @keyframes ctx-popover-in {
395
+ from { opacity: 0; transform: translateY(-4px); }
396
+ to { opacity: 1; transform: translateY(0); }
397
+ }
398
+
399
+ /* Header */
400
+ .ctx-pop-header {
401
+ display: flex;
402
+ justify-content: space-between;
403
+ align-items: baseline;
404
+ margin-bottom: 12px;
405
+ }
406
+
407
+ .ctx-pop-model {
408
+ font-size: 12px;
409
+ color: var(--text-muted);
410
+ font-family: "Roboto Mono", monospace;
411
+ }
412
+
413
+ .ctx-pop-pct {
414
+ font-size: 22px;
415
+ font-weight: 600;
416
+ color: var(--text);
417
+ font-family: "Roboto Mono", monospace;
418
+ letter-spacing: -0.5px;
419
+ }
420
+
421
+ .ctx-pop-tokens {
422
+ font-size: 11px;
423
+ color: var(--text-muted);
424
+ font-family: "Roboto Mono", monospace;
425
+ margin-left: 6px;
426
+ font-weight: 400;
427
+ }
428
+
429
+ /* Stacked category bar */
430
+ .ctx-cat-bar {
431
+ display: flex;
432
+ height: 10px;
433
+ border-radius: 5px;
434
+ overflow: hidden;
435
+ background: var(--border);
436
+ margin-bottom: 12px;
437
+ gap: 1px;
438
+ }
439
+
440
+ .ctx-cat-bar > div {
441
+ height: 100%;
442
+ min-width: 3px;
443
+ transition: width 0.3s ease;
444
+ border-radius: 2px;
445
+ }
446
+
447
+ /* Category legend */
448
+ .ctx-cat-legend {
449
+ display: flex;
450
+ flex-direction: column;
451
+ gap: 1px;
452
+ margin-bottom: 4px;
453
+ }
454
+
455
+ .ctx-cat-item {
456
+ display: flex;
457
+ align-items: center;
458
+ justify-content: space-between;
459
+ width: 100%;
460
+ font-size: 12px;
461
+ padding: 3px 0;
462
+ border-radius: 4px;
463
+ transition: background 0.1s ease;
464
+ }
465
+
466
+ .ctx-cat-item:hover {
467
+ background: rgba(var(--overlay-rgb), 0.03);
468
+ }
469
+
470
+ .ctx-emoji {
471
+ display: inline-block;
472
+ filter: grayscale(1);
473
+ font-size: 12px;
474
+ transition: filter 0.15s ease;
475
+ }
476
+
477
+ .ctx-cat-item:hover .ctx-emoji,
478
+ .ctx-pop-row:hover .ctx-emoji,
479
+ .ctx-pop-note .ctx-emoji {
480
+ filter: grayscale(0);
481
+ }
482
+
483
+ .ctx-cat-name {
484
+ color: var(--text-secondary);
485
+ }
486
+
487
+ .ctx-cat-value {
488
+ font-family: "Roboto Mono", monospace;
489
+ color: var(--text-muted);
490
+ font-size: 11px;
491
+ font-variant-numeric: tabular-nums;
492
+ }
493
+
494
+ /* Sections */
495
+ .ctx-pop-divider {
496
+ border: none;
497
+ height: 1px;
498
+ background: var(--border-subtle);
499
+ margin: 10px 0;
500
+ }
501
+
502
+ .ctx-pop-section-label {
503
+ font-size: 11px;
504
+ text-transform: uppercase;
505
+ color: var(--text-muted);
506
+ letter-spacing: 0.8px;
507
+ margin-bottom: 6px;
508
+ font-weight: 500;
509
+ }
510
+
511
+ .ctx-pop-row {
512
+ display: flex;
513
+ justify-content: space-between;
514
+ align-items: center;
515
+ font-size: 12px;
516
+ padding: 3px 0;
517
+ border-radius: 4px;
518
+ transition: background 0.1s ease;
519
+ }
520
+
521
+ .ctx-pop-row:hover {
522
+ background: rgba(var(--overlay-rgb), 0.03);
523
+ }
524
+
525
+ .ctx-pop-row-label {
526
+ color: var(--text-secondary);
527
+ white-space: nowrap;
528
+ overflow: hidden;
529
+ text-overflow: ellipsis;
530
+ margin-right: 12px;
531
+ }
532
+
533
+ .ctx-pop-row-value {
534
+ font-family: "Roboto Mono", monospace;
535
+ color: var(--text-muted);
536
+ font-size: 11px;
537
+ flex-shrink: 0;
538
+ font-variant-numeric: tabular-nums;
539
+ }
540
+
541
+ .ctx-pop-note {
542
+ font-size: 11px;
543
+ color: var(--text-muted);
544
+ margin-top: 10px;
545
+ text-align: center;
546
+ padding: 4px 8px;
547
+ background: var(--border-subtle);
548
+ border-radius: 4px;
549
+ font-family: "Roboto Mono", monospace;
550
+ }
551
+
552
+ @media (max-width: 400px) {
553
+ .context-usage-popover {
554
+ width: 260px;
555
+ right: -8px;
556
+ }
557
+ }
558
+
373
559
  /* --- Shared pill style for rate limit & fast mode --- */
374
560
  .header-rate-limit,
375
561
  .header-fast-mode {
@@ -415,6 +415,13 @@
415
415
  <div id="input-wrapper">
416
416
  <div id="mention-menu"></div>
417
417
  <div id="slash-menu"></div>
418
+ <div id="context-sources-bar">
419
+ <div id="context-sources-chips"></div>
420
+ <button id="context-sources-add" type="button" title="Add context source"><i data-lucide="plus"></i><span>Context Sources</span></button>
421
+ <div id="context-sources-picker" class="hidden">
422
+ <div class="context-picker-section" id="context-picker-terminals"></div>
423
+ </div>
424
+ </div>
418
425
  <div id="suggestion-chips" class="hidden"></div>
419
426
  <div id="input-row">
420
427
  <div id="context-mini" class="hidden">
@@ -0,0 +1,226 @@
1
+ // Context Sources — attach terminal output (and future browser tabs) as context for Claude
2
+
3
+ var ctx = null;
4
+ var activeSourceIds = new Set();
5
+ var terminalList = []; // synced from terminal module's term_list
6
+
7
+ export function initContextSources(_ctx) {
8
+ ctx = _ctx;
9
+
10
+ var addBtn = document.getElementById("context-sources-add");
11
+ var picker = document.getElementById("context-sources-picker");
12
+
13
+ addBtn.addEventListener("click", function(e) {
14
+ e.stopPropagation();
15
+ if (picker.classList.contains("hidden")) {
16
+ renderPicker();
17
+ picker.classList.remove("hidden");
18
+ document.addEventListener("click", closePicker, true);
19
+ } else {
20
+ closePicker();
21
+ }
22
+ });
23
+
24
+ picker.addEventListener("click", function(e) {
25
+ e.stopPropagation();
26
+ });
27
+ }
28
+
29
+ function closePicker() {
30
+ var picker = document.getElementById("context-sources-picker");
31
+ picker.classList.add("hidden");
32
+ document.removeEventListener("click", closePicker, true);
33
+ }
34
+
35
+ // Restore state from server
36
+ export function handleContextSourcesState(msg) {
37
+ var saved = msg.active || [];
38
+ activeSourceIds = new Set(saved);
39
+ renderChips();
40
+ }
41
+
42
+ // Save active sources to server
43
+ function saveToServer() {
44
+ if (ctx && ctx.ws && ctx.connected) {
45
+ ctx.ws.send(JSON.stringify({
46
+ type: "context_sources_save",
47
+ active: Array.from(activeSourceIds)
48
+ }));
49
+ }
50
+ }
51
+
52
+ // Called when term_list arrives from server
53
+ export function updateTerminalList(terminals) {
54
+ terminalList = terminals || [];
55
+
56
+ // Remove active sources that no longer exist
57
+ var changed = false;
58
+ for (var id of activeSourceIds) {
59
+ if (id.startsWith("term:")) {
60
+ var termId = parseInt(id.split(":")[1], 10);
61
+ var found = false;
62
+ for (var i = 0; i < terminalList.length; i++) {
63
+ if (terminalList[i].id === termId) { found = true; break; }
64
+ }
65
+ if (!found) {
66
+ activeSourceIds.delete(id);
67
+ changed = true;
68
+ }
69
+ }
70
+ }
71
+
72
+ if (changed) saveToServer();
73
+ renderChips();
74
+
75
+ // If picker is open, re-render it
76
+ var picker = document.getElementById("context-sources-picker");
77
+ if (!picker.classList.contains("hidden")) {
78
+ renderPicker();
79
+ }
80
+ }
81
+
82
+ function toggleSource(sourceId) {
83
+ if (activeSourceIds.has(sourceId)) {
84
+ activeSourceIds.delete(sourceId);
85
+ } else {
86
+ activeSourceIds.add(sourceId);
87
+ }
88
+ saveToServer();
89
+ renderChips();
90
+ renderPicker();
91
+ }
92
+
93
+ function removeSource(sourceId) {
94
+ activeSourceIds.delete(sourceId);
95
+ saveToServer();
96
+ renderChips();
97
+
98
+ var picker = document.getElementById("context-sources-picker");
99
+ if (!picker.classList.contains("hidden")) {
100
+ renderPicker();
101
+ }
102
+ }
103
+
104
+ function renderChips() {
105
+ var container = document.getElementById("context-sources-chips");
106
+ container.innerHTML = "";
107
+
108
+ for (var id of activeSourceIds) {
109
+ var chip = document.createElement("div");
110
+ chip.className = "context-chip";
111
+
112
+ var label = getSourceLabel(id);
113
+ var iconName = getSourceIcon(id);
114
+
115
+ var labelEl = document.createElement("span");
116
+ labelEl.className = "context-chip-label";
117
+ labelEl.innerHTML =
118
+ '<i data-lucide="' + iconName + '"></i>' +
119
+ '<span>' + escapeHtml(label) + '</span>';
120
+ chip.appendChild(labelEl);
121
+
122
+ var removeBtn = document.createElement("button");
123
+ removeBtn.type = "button";
124
+ removeBtn.className = "context-chip-remove";
125
+ removeBtn.title = "Remove";
126
+ removeBtn.innerHTML = '<i data-lucide="minus"></i>';
127
+ removeBtn.setAttribute("data-source-id", id);
128
+ removeBtn.addEventListener("click", function(e) {
129
+ e.stopPropagation();
130
+ removeSource(this.getAttribute("data-source-id"));
131
+ if (typeof lucide !== "undefined") lucide.createIcons();
132
+ });
133
+
134
+ chip.appendChild(removeBtn);
135
+ container.appendChild(chip);
136
+ }
137
+
138
+ // Update add button label
139
+ var addBtn = document.getElementById("context-sources-add");
140
+ var labelSpan = addBtn.querySelector("span");
141
+ if (activeSourceIds.size > 0) {
142
+ labelSpan.textContent = "";
143
+ labelSpan.style.display = "none";
144
+ } else {
145
+ labelSpan.textContent = "Context Sources";
146
+ labelSpan.style.display = "";
147
+ }
148
+
149
+ if (typeof lucide !== "undefined") lucide.createIcons();
150
+ }
151
+
152
+ function renderPicker() {
153
+ var section = document.getElementById("context-picker-terminals");
154
+ section.innerHTML = "";
155
+
156
+ var sectionLabel = document.createElement("div");
157
+ sectionLabel.className = "context-picker-section-label";
158
+ sectionLabel.textContent = "Terminals";
159
+ section.appendChild(sectionLabel);
160
+
161
+ if (terminalList.length === 0) {
162
+ var empty = document.createElement("div");
163
+ empty.className = "context-picker-empty";
164
+ empty.textContent = "No terminals open";
165
+ section.appendChild(empty);
166
+ return;
167
+ }
168
+
169
+ for (var i = 0; i < terminalList.length; i++) {
170
+ var term = terminalList[i];
171
+ var sourceId = "term:" + term.id;
172
+ var isActive = activeSourceIds.has(sourceId);
173
+
174
+ var item = document.createElement("div");
175
+ item.className = "context-picker-item" + (isActive ? " active" : "");
176
+ item.setAttribute("data-source-id", sourceId);
177
+
178
+ item.innerHTML =
179
+ '<i data-lucide="square-terminal"></i>' +
180
+ '<span>' + escapeHtml(term.title || ("Terminal " + term.id)) + '</span>' +
181
+ '<i data-lucide="check" class="context-picker-check"></i>';
182
+
183
+ item.addEventListener("click", function() {
184
+ toggleSource(this.getAttribute("data-source-id"));
185
+ if (typeof lucide !== "undefined") lucide.createIcons();
186
+ });
187
+
188
+ section.appendChild(item);
189
+ }
190
+
191
+ if (typeof lucide !== "undefined") lucide.createIcons();
192
+ }
193
+
194
+ function getSourceLabel(id) {
195
+ if (id.startsWith("term:")) {
196
+ var termId = parseInt(id.split(":")[1], 10);
197
+ for (var i = 0; i < terminalList.length; i++) {
198
+ if (terminalList[i].id === termId) {
199
+ return terminalList[i].title || ("Terminal " + termId);
200
+ }
201
+ }
202
+ return "Terminal " + termId;
203
+ }
204
+ return id;
205
+ }
206
+
207
+ function getSourceIcon(id) {
208
+ if (id.startsWith("term:")) return "square-terminal";
209
+ return "circle";
210
+ }
211
+
212
+ // Get active source IDs (for use when sending messages)
213
+ export function getActiveSources() {
214
+ return Array.from(activeSourceIds);
215
+ }
216
+
217
+ // Check if any sources are active
218
+ export function hasActiveSources() {
219
+ return activeSourceIds.size > 0;
220
+ }
221
+
222
+ function escapeHtml(str) {
223
+ var div = document.createElement("div");
224
+ div.textContent = str;
225
+ return div.innerHTML;
226
+ }
package/lib/sdk-bridge.js CHANGED
@@ -188,6 +188,10 @@ function createSDKBridge(opts) {
188
188
  sm.sendAndRecord(session, obj);
189
189
  }
190
190
 
191
+ function sendToSession(session, obj) {
192
+ sm.sendToSession(session, obj);
193
+ }
194
+
191
195
  function processSDKMessage(session, parsed) {
192
196
  // Timing: log key SDK milestones relative to query start
193
197
  if (session._queryStartTs) {
@@ -435,6 +439,15 @@ function createSDKBridge(opts) {
435
439
  session.isProcessing = false;
436
440
  session.rateLimitResetsAt = null; // clear on success
437
441
  onProcessingChanged();
442
+ // Fetch rich context usage breakdown (fire-and-forget, non-blocking)
443
+ if (session.queryInstance && typeof session.queryInstance.getContextUsage === "function") {
444
+ session.queryInstance.getContextUsage().then(function(ctxUsage) {
445
+ session.lastContextUsage = ctxUsage;
446
+ sendToSession(session, { type: "context_usage", data: ctxUsage });
447
+ }).catch(function(e) {
448
+ console.error("[sdk-bridge] getContextUsage failed (non-fatal):", e.message || e);
449
+ });
450
+ }
438
451
  var lastStreamInput = session.lastStreamInputTokens || null;
439
452
  session.lastStreamInputTokens = null;
440
453
  sendAndRecord(session, {
@@ -1196,6 +1209,11 @@ function createSDKBridge(opts) {
1196
1209
  });
1197
1210
  break;
1198
1211
 
1212
+ case "context_usage":
1213
+ session.lastContextUsage = msg.data;
1214
+ sendToSession(session, { type: "context_usage", data: msg.data });
1215
+ break;
1216
+
1199
1217
  case "query_done":
1200
1218
  console.log("[sdk-bridge] IPC query_done received, pid=" + (worker.process ? worker.process.pid : "?"));
1201
1219
  // Mark that we received a proper IPC completion, so the exit
@@ -1698,6 +1716,7 @@ function createSDKBridge(opts) {
1698
1716
  for await (var msg of myQueryInstance) {
1699
1717
  processSDKMessage(session, msg);
1700
1718
  }
1719
+ // (getContextUsage moved to processSDKMessage result handler -- fire-and-forget)
1701
1720
  // Stream ended normally after a task stop — no "result" message was sent,
1702
1721
  // so the session is still marked as processing. Send interrupted feedback.
1703
1722
  if (session.isProcessing && session.taskStopRequested) {
package/lib/sdk-worker.js CHANGED
@@ -351,7 +351,19 @@ async function handleQueryStart(msg) {
351
351
  }
352
352
  sendToDaemon({ type: "sdk_event", event: event });
353
353
  }
354
- perf("all events streamed (counts=" + JSON.stringify(eventCounts) + "), sending query_done");
354
+ perf("all events streamed (counts=" + JSON.stringify(eventCounts) + "), fetching context usage");
355
+ // Fetch context usage breakdown before queryInstance is cleared
356
+ try {
357
+ if (queryInstance && typeof queryInstance.getContextUsage === "function") {
358
+ var ctxUsage = await queryInstance.getContextUsage();
359
+ sendToDaemon({ type: "context_usage", data: ctxUsage });
360
+ perf("context usage sent");
361
+ }
362
+ } catch (e) {
363
+ // Non-fatal: SDK may have already shut down
364
+ console.error("[sdk-worker] getContextUsage failed (non-fatal):", e.message);
365
+ }
366
+ perf("sending query_done");
355
367
  sendToDaemon({ type: "query_done" });
356
368
  } catch (err) {
357
369
  var errMsg = err.message || String(err);
package/lib/sessions.js CHANGED
@@ -366,7 +366,7 @@ function createSessionManager(opts) {
366
366
  }
367
367
  }
368
368
 
369
- _send({ type: "history_done", lastUsage: lastUsage, lastModelUsage: lastModelUsage, lastCost: lastCost, lastStreamInputTokens: lastStreamInputTokens });
369
+ _send({ type: "history_done", lastUsage: lastUsage, lastModelUsage: lastModelUsage, lastCost: lastCost, lastStreamInputTokens: lastStreamInputTokens, contextUsage: session.lastContextUsage || null });
370
370
  }
371
371
 
372
372
  function switchSession(localId, targetWs, transform) {
@@ -492,6 +492,20 @@ function createSessionManager(opts) {
492
492
  sessions.delete(localId);
493
493
  }
494
494
 
495
+ function doSendToSession(session, obj) {
496
+ // Send to active clients without recording to history/disk (ephemeral data)
497
+ if (sendEach) {
498
+ var data = JSON.stringify(obj);
499
+ sendEach(function (ws) {
500
+ if (ws._clayActiveSession === session.localId && ws.readyState === 1) {
501
+ ws.send(data);
502
+ }
503
+ });
504
+ } else if (session.localId === activeSessionId) {
505
+ send(obj);
506
+ }
507
+ }
508
+
495
509
  function doSendAndRecord(session, obj) {
496
510
  session.history.push(obj);
497
511
  appendToSessionFile(session, obj);
@@ -737,6 +751,7 @@ function createSessionManager(opts) {
737
751
  saveSessionFile: saveSessionFile,
738
752
  appendToSessionFile: appendToSessionFile,
739
753
  sendAndRecord: doSendAndRecord,
754
+ sendToSession: doSendToSession,
740
755
  findTurnBoundary: findTurnBoundary,
741
756
  replayHistory: replayHistory,
742
757
  searchSessions: searchSessions,
@@ -28,6 +28,7 @@ function createTerminalManager(opts) {
28
28
  pty: pty,
29
29
  scrollback: [],
30
30
  scrollbackSize: 0,
31
+ totalBytesWritten: 0,
31
32
  cols: cols || 80,
32
33
  rows: rows || 24,
33
34
  title: "Terminal " + id,
@@ -38,11 +39,13 @@ function createTerminalManager(opts) {
38
39
  };
39
40
 
40
41
  pty.onData(function (data) {
41
- // Buffer scrollback
42
- session.scrollback.push(data);
42
+ // Buffer scrollback with timestamps
43
+ var ts = Date.now();
44
+ session.scrollback.push({ ts: ts, data: data });
43
45
  session.scrollbackSize += data.length;
46
+ session.totalBytesWritten += data.length;
44
47
  while (session.scrollbackSize > SCROLLBACK_MAX && session.scrollback.length > 1) {
45
- session.scrollbackSize -= session.scrollback[0].length;
48
+ session.scrollbackSize -= session.scrollback[0].data.length;
46
49
  session.scrollback.shift();
47
50
  }
48
51
 
@@ -81,7 +84,7 @@ function createTerminalManager(opts) {
81
84
 
82
85
  // Replay scrollback only for newly attached clients
83
86
  if (!alreadySubscribed && session.scrollback.length > 0) {
84
- var replay = session.scrollback.join("");
87
+ var replay = session.scrollback.map(function(c) { return c.data; }).join("");
85
88
  sendTo(ws, { type: "term_output", id: id, data: replay });
86
89
  }
87
90
 
@@ -176,6 +179,18 @@ function createTerminalManager(opts) {
176
179
  return result;
177
180
  }
178
181
 
182
+ function getScrollback(id) {
183
+ var session = terminals.get(id);
184
+ if (!session) return null;
185
+ var content = session.scrollback.map(function(c) { return c.data; }).join("");
186
+ return {
187
+ content: content,
188
+ chunks: session.scrollback,
189
+ totalBytesWritten: session.totalBytesWritten,
190
+ bufferStart: session.totalBytesWritten - content.length
191
+ };
192
+ }
193
+
179
194
  function destroyAll() {
180
195
  for (var session of terminals.values()) {
181
196
  if (session.pty) {
@@ -196,6 +211,7 @@ function createTerminalManager(opts) {
196
211
  close: close,
197
212
  rename: rename,
198
213
  list: list,
214
+ getScrollback: getScrollback,
199
215
  destroyAll: destroyAll,
200
216
  };
201
217
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.26.0-beta.3",
3
+ "version": "2.26.0-beta.5",
4
4
  "description": "Self-hosted Claude Code in your browser. Multi-session, multi-user, push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",
@@ -36,7 +36,7 @@
36
36
  "homepage": "https://github.com/chadbyte/claude-relay#readme",
37
37
  "author": "Chad",
38
38
  "dependencies": {
39
- "@anthropic-ai/claude-agent-sdk": "^0.2.76",
39
+ "@anthropic-ai/claude-agent-sdk": "^0.2.92",
40
40
  "@lydell/node-pty": "^1.2.0-beta.3",
41
41
  "nodemailer": "^6.10.1",
42
42
  "qrcode-terminal": "^0.12.0",