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.
- package/lib/project-mate-interaction.js +760 -0
- package/lib/project-memory.js +677 -0
- package/lib/project.js +192 -1371
- package/lib/public/app.js +192 -13
- package/lib/public/css/input.css +173 -59
- package/lib/public/css/menus.css +7 -0
- package/lib/public/css/sidebar.css +10 -0
- package/lib/public/css/title-bar.css +186 -0
- package/lib/public/index.html +7 -0
- package/lib/public/modules/context-sources.js +226 -0
- package/lib/sdk-bridge.js +19 -0
- package/lib/sdk-worker.js +13 -1
- package/lib/sessions.js +16 -1
- package/lib/terminal-manager.js +20 -4
- package/package.json +2 -2
|
@@ -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 {
|
package/lib/public/index.html
CHANGED
|
@@ -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) + "),
|
|
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,
|
package/lib/terminal-manager.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
"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.
|
|
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",
|