clay-server 2.26.0-beta.4 → 2.26.0-beta.6

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.
@@ -23,8 +23,8 @@
23
23
  display: inline-flex;
24
24
  align-items: center;
25
25
  gap: 4px;
26
- background: color-mix(in srgb, var(--success) 12%, transparent);
27
- color: var(--success);
26
+ background: color-mix(in srgb, #3b82f6 12%, transparent);
27
+ color: #3b82f6;
28
28
  border: none;
29
29
  border-radius: 10px;
30
30
  padding: 2px 10px;
@@ -38,7 +38,7 @@
38
38
  }
39
39
 
40
40
  .top-bar-update-btn .lucide { width: 12px; height: 12px; }
41
- .top-bar-update-btn:hover { background: color-mix(in srgb, var(--success) 20%, transparent); }
41
+ .top-bar-update-btn:hover { background: color-mix(in srgb, #3b82f6 20%, transparent); }
42
42
 
43
43
  /* --- Top bar actions (right-aligned group) --- */
44
44
  .top-bar-actions {
@@ -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 {
@@ -52,6 +52,28 @@
52
52
  <div id="top-bar">
53
53
  <div class="top-bar-left-pills">
54
54
  <button id="pwa-install-pill" class="top-bar-install-btn hidden" title="Open as app"><i data-lucide="download"></i> Open as app</button>
55
+ <div id="ext-pill-wrap" class="ext-pill-wrap">
56
+ <button id="ext-pill" class="top-bar-ext-btn" title="Chrome Extension"><i data-lucide="puzzle"></i> Extension</button>
57
+ <div id="ext-popover" class="ext-popover">
58
+ <div class="ext-popover-header">
59
+ <div class="ext-popover-title">Clay for Chrome <span class="ext-experimental">Experimental</span></div>
60
+ <div class="ext-popover-sub">v0.1.0 &middot; <a href="https://github.com/chadbyte/clay-chrome" target="_blank" rel="noopener">GitHub</a></div>
61
+ </div>
62
+ <div class="ext-popover-connected hidden" id="ext-connected-banner"><i data-lucide="check-circle"></i> Extension connected</div>
63
+ <div class="ext-popover-desc" id="ext-popover-desc">Connect your browser to Clay. Gives Claude visibility into open tabs, console, network, DOM, and screenshots.</div>
64
+ <button class="ext-popover-download" id="ext-download-btn"><i data-lucide="download"></i> Download Extension (.zip)</button>
65
+ <div class="ext-popover-status hidden" id="ext-download-status"></div>
66
+ <div class="ext-popover-divider" id="ext-popover-divider"></div>
67
+ <div class="ext-popover-guide-title" id="ext-popover-guide-title">Installation</div>
68
+ <div class="ext-popover-steps">
69
+ <div class="ext-popover-step"><span class="ext-snum">1</span> Download above, then unzip to a permanent folder.</div>
70
+ <div class="ext-popover-step"><span class="ext-snum">2</span> Open <code id="ext-copy-url" class="ext-popover-code" title="Click to copy">chrome://extensions</code></div>
71
+ <div class="ext-popover-step"><span class="ext-snum">3</span> Enable <strong>Developer mode</strong> (top-right toggle).</div>
72
+ <div class="ext-popover-step"><span class="ext-snum">4</span> <strong>Load unpacked</strong> > select the unzipped folder.</div>
73
+ <div class="ext-popover-step"><span class="ext-snum">5</span> Refresh this page. Done!</div>
74
+ </div>
75
+ </div>
76
+ </div>
55
77
  <button id="share-pill" class="top-bar-share-btn" title="Share"><i data-lucide="qr-code"></i> Share</button>
56
78
  <div id="update-pill-wrap" class="top-bar-update hidden">
57
79
  <button id="update-pill" class="top-bar-update-btn"><i data-lucide="arrow-up-circle"></i> <span id="update-version"></span> is available. Update now</button>
@@ -420,6 +442,7 @@
420
442
  <button id="context-sources-add" type="button" title="Add context source"><i data-lucide="plus"></i><span>Context Sources</span></button>
421
443
  <div id="context-sources-picker" class="hidden">
422
444
  <div class="context-picker-section" id="context-picker-terminals"></div>
445
+ <div class="context-picker-section" id="context-picker-tabs"></div>
423
446
  </div>
424
447
  </div>
425
448
  <div id="suggestion-chips" class="hidden"></div>
@@ -1,8 +1,9 @@
1
- // Context Sources — attach terminal output (and future browser tabs) as context for Claude
1
+ // Context Sources — attach terminal output and browser tabs as context for Claude
2
2
 
3
3
  var ctx = null;
4
4
  var activeSourceIds = new Set();
5
5
  var terminalList = []; // synced from terminal module's term_list
6
+ var browserTabList = []; // synced from Chrome extension via postMessage
6
7
 
7
8
  export function initContextSources(_ctx) {
8
9
  ctx = _ctx;
@@ -79,6 +80,36 @@ export function updateTerminalList(terminals) {
79
80
  }
80
81
  }
81
82
 
83
+ // Called when Chrome extension sends tab list via postMessage
84
+ export function updateBrowserTabList(tabs) {
85
+ browserTabList = tabs || [];
86
+
87
+ // Remove active tab sources that no longer exist
88
+ var changed = false;
89
+ for (var id of activeSourceIds) {
90
+ if (id.startsWith("tab:")) {
91
+ var tabId = parseInt(id.split(":")[1], 10);
92
+ var found = false;
93
+ for (var i = 0; i < browserTabList.length; i++) {
94
+ if (browserTabList[i].id === tabId) { found = true; break; }
95
+ }
96
+ if (!found) {
97
+ activeSourceIds.delete(id);
98
+ changed = true;
99
+ }
100
+ }
101
+ }
102
+
103
+ if (changed) saveToServer();
104
+ renderChips();
105
+
106
+ // If picker is open, re-render it
107
+ var picker = document.getElementById("context-sources-picker");
108
+ if (!picker.classList.contains("hidden")) {
109
+ renderPicker();
110
+ }
111
+ }
112
+
82
113
  function toggleSource(sourceId) {
83
114
  if (activeSourceIds.has(sourceId)) {
84
115
  activeSourceIds.delete(sourceId);
@@ -150,42 +181,87 @@ function renderChips() {
150
181
  }
151
182
 
152
183
  function renderPicker() {
153
- var section = document.getElementById("context-picker-terminals");
154
- section.innerHTML = "";
184
+ // --- Terminals section ---
185
+ var termSection = document.getElementById("context-picker-terminals");
186
+ termSection.innerHTML = "";
155
187
 
156
- var sectionLabel = document.createElement("div");
157
- sectionLabel.className = "context-picker-section-label";
158
- sectionLabel.textContent = "Terminals";
159
- section.appendChild(sectionLabel);
188
+ var termLabel = document.createElement("div");
189
+ termLabel.className = "context-picker-section-label";
190
+ termLabel.textContent = "Terminals";
191
+ termSection.appendChild(termLabel);
160
192
 
161
193
  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
- }
194
+ var termEmpty = document.createElement("div");
195
+ termEmpty.className = "context-picker-empty";
196
+ termEmpty.textContent = "No terminals open";
197
+ termSection.appendChild(termEmpty);
198
+ } else {
199
+ for (var i = 0; i < terminalList.length; i++) {
200
+ var term = terminalList[i];
201
+ var termSourceId = "term:" + term.id;
202
+ var termActive = activeSourceIds.has(termSourceId);
168
203
 
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);
204
+ var termItem = document.createElement("div");
205
+ termItem.className = "context-picker-item" + (termActive ? " active" : "");
206
+ termItem.setAttribute("data-source-id", termSourceId);
173
207
 
174
- var item = document.createElement("div");
175
- item.className = "context-picker-item" + (isActive ? " active" : "");
176
- item.setAttribute("data-source-id", sourceId);
208
+ termItem.innerHTML =
209
+ '<i data-lucide="square-terminal"></i>' +
210
+ '<span>' + escapeHtml(term.title || ("Terminal " + term.id)) + '</span>' +
211
+ '<i data-lucide="check" class="context-picker-check"></i>';
177
212
 
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>';
213
+ termItem.addEventListener("click", function() {
214
+ toggleSource(this.getAttribute("data-source-id"));
215
+ if (typeof lucide !== "undefined") lucide.createIcons();
216
+ });
182
217
 
183
- item.addEventListener("click", function() {
184
- toggleSource(this.getAttribute("data-source-id"));
185
- if (typeof lucide !== "undefined") lucide.createIcons();
186
- });
218
+ termSection.appendChild(termItem);
219
+ }
220
+ }
221
+
222
+ // --- Browser Tabs section ---
223
+ var tabSection = document.getElementById("context-picker-tabs");
224
+ tabSection.innerHTML = "";
225
+
226
+ if (browserTabList.length > 0) {
227
+ var tabLabel = document.createElement("div");
228
+ tabLabel.className = "context-picker-section-label";
229
+ tabLabel.textContent = "Browser Tabs";
230
+ tabSection.appendChild(tabLabel);
231
+
232
+ for (var j = 0; j < browserTabList.length; j++) {
233
+ var tab = browserTabList[j];
234
+ var tabSourceId = "tab:" + tab.id;
235
+ var tabActive = activeSourceIds.has(tabSourceId);
236
+
237
+ var tabItem = document.createElement("div");
238
+ tabItem.className = "context-picker-item" + (tabActive ? " active" : "");
239
+ tabItem.setAttribute("data-source-id", tabSourceId);
240
+
241
+ var tabTitle = tab.title || tab.url || "Tab";
242
+ // Truncate long URLs for display
243
+ var tabDisplay = tabTitle.length > 50 ? tabTitle.slice(0, 47) + "..." : tabTitle;
244
+
245
+ var faviconHtml = "";
246
+ if (tab.favIconUrl) {
247
+ faviconHtml = '<img src="' + escapeHtml(tab.favIconUrl) + '" class="context-picker-favicon" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'\'">' +
248
+ '<i data-lucide="globe" style="display:none"></i>';
249
+ } else {
250
+ faviconHtml = '<i data-lucide="globe"></i>';
251
+ }
252
+
253
+ tabItem.innerHTML =
254
+ faviconHtml +
255
+ '<span title="' + escapeHtml(tab.url || "") + '">' + escapeHtml(tabDisplay) + '</span>' +
256
+ '<i data-lucide="check" class="context-picker-check"></i>';
257
+
258
+ tabItem.addEventListener("click", function() {
259
+ toggleSource(this.getAttribute("data-source-id"));
260
+ if (typeof lucide !== "undefined") lucide.createIcons();
261
+ });
187
262
 
188
- section.appendChild(item);
263
+ tabSection.appendChild(tabItem);
264
+ }
189
265
  }
190
266
 
191
267
  if (typeof lucide !== "undefined") lucide.createIcons();
@@ -201,11 +277,22 @@ function getSourceLabel(id) {
201
277
  }
202
278
  return "Terminal " + termId;
203
279
  }
280
+ if (id.startsWith("tab:")) {
281
+ var tabId = parseInt(id.split(":")[1], 10);
282
+ for (var j = 0; j < browserTabList.length; j++) {
283
+ if (browserTabList[j].id === tabId) {
284
+ var title = browserTabList[j].title || browserTabList[j].url || "";
285
+ return title.length > 30 ? title.slice(0, 27) + "..." : title;
286
+ }
287
+ }
288
+ return "Tab " + tabId;
289
+ }
204
290
  return id;
205
291
  }
206
292
 
207
293
  function getSourceIcon(id) {
208
294
  if (id.startsWith("term:")) return "square-terminal";
295
+ if (id.startsWith("tab:")) return "globe";
209
296
  return "circle";
210
297
  }
211
298
 
@@ -1,4 +1,4 @@
1
- import { copyToClipboard } from './utils.js';
1
+ import { copyToClipboard, showToast } from './utils.js';
2
2
  import { iconHtml, refreshIcons } from './icons.js';
3
3
 
4
4
  var ctx;
@@ -155,6 +155,114 @@ export function initNotifications(_ctx) {
155
155
  });
156
156
  })();
157
157
 
158
+ // --- Extension pill popover ---
159
+ (function () {
160
+ var extPillWrap = $("ext-pill-wrap");
161
+ var extPillBtn = $("ext-pill");
162
+ var extPopover = $("ext-popover");
163
+ var extDownloadBtn = $("ext-download-btn");
164
+ var extDownloadStatus = $("ext-download-status");
165
+ var extCopyUrl = $("ext-copy-url");
166
+ if (!extPillWrap || !extPillBtn || !extPopover) return;
167
+
168
+ // Detect extension connection via postMessage from content.js
169
+ var extConnected = false;
170
+ var connectedBanner = $("ext-connected-banner");
171
+ var extDesc = $("ext-popover-desc");
172
+ var extDivider = $("ext-popover-divider");
173
+ var extGuideTitle = $("ext-popover-guide-title");
174
+ var extSteps = extPopover.querySelector(".ext-popover-steps");
175
+
176
+ function setExtConnected() {
177
+ if (extConnected) return;
178
+ extConnected = true;
179
+ extPillBtn.classList.add("ext-connected");
180
+ extPillBtn.innerHTML = '<i data-lucide="check"></i> Extension';
181
+ refreshIcons(extPillBtn);
182
+ if (connectedBanner) connectedBanner.classList.remove("hidden");
183
+ if (extDownloadBtn) extDownloadBtn.style.display = "none";
184
+ if (extDownloadStatus) extDownloadStatus.classList.add("hidden");
185
+ if (extDesc) extDesc.style.display = "none";
186
+ if (extDivider) extDivider.style.display = "none";
187
+ if (extGuideTitle) extGuideTitle.style.display = "none";
188
+ if (extSteps) extSteps.style.display = "none";
189
+ }
190
+
191
+ window.addEventListener("message", function (event) {
192
+ if (event.source !== window) return;
193
+ if (!event.data || event.data.source !== "clay-chrome-extension") return;
194
+ setExtConnected();
195
+ });
196
+
197
+ // Toggle popover
198
+ extPillBtn.addEventListener("click", function (e) {
199
+ e.stopPropagation();
200
+ extPopover.classList.toggle("visible");
201
+ refreshIcons(extPopover);
202
+ });
203
+
204
+ document.addEventListener("click", function (e) {
205
+ if (!extPopover.contains(e.target) && e.target !== extPillBtn && !extPillBtn.contains(e.target)) {
206
+ extPopover.classList.remove("visible");
207
+ }
208
+ });
209
+
210
+ // Download button
211
+ if (extDownloadBtn) {
212
+ extDownloadBtn.addEventListener("click", function (e) {
213
+ e.stopPropagation();
214
+ extDownloadBtn.disabled = true;
215
+ extDownloadBtn.innerHTML = iconHtml("loader") + " Downloading...";
216
+ refreshIcons(extDownloadBtn);
217
+ var loaderIcon = extDownloadBtn.querySelector(".lucide");
218
+ if (loaderIcon) loaderIcon.style.animation = "spin 1s linear infinite";
219
+ if (extDownloadStatus) {
220
+ extDownloadStatus.classList.remove("hidden");
221
+ extDownloadStatus.textContent = "Fetching from GitHub...";
222
+ extDownloadStatus.style.color = "";
223
+ }
224
+ fetch("/api/extension/download").then(function (resp) {
225
+ if (!resp.ok) throw new Error("Download failed (" + resp.status + ")");
226
+ return resp.blob();
227
+ }).then(function (blob) {
228
+ var url = URL.createObjectURL(blob);
229
+ var a = document.createElement("a");
230
+ a.href = url;
231
+ a.download = "clay-chrome-extension.zip";
232
+ document.body.appendChild(a);
233
+ a.click();
234
+ document.body.removeChild(a);
235
+ URL.revokeObjectURL(url);
236
+ if (extDownloadStatus) {
237
+ extDownloadStatus.textContent = "Download complete!";
238
+ extDownloadStatus.style.color = "var(--accent)";
239
+ }
240
+ showToast("Extension downloaded");
241
+ }).catch(function (err) {
242
+ if (extDownloadStatus) {
243
+ extDownloadStatus.textContent = "Failed: " + err.message;
244
+ extDownloadStatus.style.color = "var(--danger, #e53935)";
245
+ }
246
+ showToast("Download failed");
247
+ }).finally(function () {
248
+ extDownloadBtn.disabled = false;
249
+ extDownloadBtn.innerHTML = iconHtml("download") + " Download Extension (.zip)";
250
+ refreshIcons(extDownloadBtn);
251
+ });
252
+ });
253
+ }
254
+
255
+ // Copy chrome://extensions URL
256
+ if (extCopyUrl) {
257
+ extCopyUrl.addEventListener("click", function (e) {
258
+ e.stopPropagation();
259
+ copyToClipboard("chrome://extensions").then(function () {
260
+ showToast("Copied chrome://extensions");
261
+ });
262
+ });
263
+ }
264
+ })();
265
+
158
266
  // --- Settings: Check for updates ---
159
267
  (function () {
160
268
  var settingsUpdateCheck = $("settings-update-check");
package/lib/sdk-bridge.js CHANGED
@@ -134,6 +134,7 @@ function createSDKBridge(opts) {
134
134
  var mateDisplayName = opts.mateDisplayName || "";
135
135
  var isMate = opts.isMate || (slug.indexOf("mate-") === 0);
136
136
  var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
137
+ var mcpServers = opts.mcpServers || null;
137
138
  var onProcessingChanged = opts.onProcessingChanged || function () {};
138
139
  var onTurnDone = opts.onTurnDone || null;
139
140
 
@@ -188,6 +189,10 @@ function createSDKBridge(opts) {
188
189
  sm.sendAndRecord(session, obj);
189
190
  }
190
191
 
192
+ function sendToSession(session, obj) {
193
+ sm.sendToSession(session, obj);
194
+ }
195
+
191
196
  function processSDKMessage(session, parsed) {
192
197
  // Timing: log key SDK milestones relative to query start
193
198
  if (session._queryStartTs) {
@@ -435,6 +440,15 @@ function createSDKBridge(opts) {
435
440
  session.isProcessing = false;
436
441
  session.rateLimitResetsAt = null; // clear on success
437
442
  onProcessingChanged();
443
+ // Fetch rich context usage breakdown (fire-and-forget, non-blocking)
444
+ if (session.queryInstance && typeof session.queryInstance.getContextUsage === "function") {
445
+ session.queryInstance.getContextUsage().then(function(ctxUsage) {
446
+ session.lastContextUsage = ctxUsage;
447
+ sendToSession(session, { type: "context_usage", data: ctxUsage });
448
+ }).catch(function(e) {
449
+ console.error("[sdk-bridge] getContextUsage failed (non-fatal):", e.message || e);
450
+ });
451
+ }
438
452
  var lastStreamInput = session.lastStreamInputTokens || null;
439
453
  session.lastStreamInputTokens = null;
440
454
  sendAndRecord(session, {
@@ -1115,6 +1129,7 @@ function createSDKBridge(opts) {
1115
1129
  agentProgressSummaries: true,
1116
1130
  };
1117
1131
 
1132
+ if (mcpServers) queryOptions.mcpServers = mcpServers;
1118
1133
  if (sm.currentModel) queryOptions.model = sm.currentModel;
1119
1134
  if (sm.currentEffort) queryOptions.effort = sm.currentEffort;
1120
1135
  if (sm.currentBetas && sm.currentBetas.length > 0) queryOptions.betas = sm.currentBetas;
@@ -1196,6 +1211,11 @@ function createSDKBridge(opts) {
1196
1211
  });
1197
1212
  break;
1198
1213
 
1214
+ case "context_usage":
1215
+ session.lastContextUsage = msg.data;
1216
+ sendToSession(session, { type: "context_usage", data: msg.data });
1217
+ break;
1218
+
1199
1219
  case "query_done":
1200
1220
  console.log("[sdk-bridge] IPC query_done received, pid=" + (worker.process ? worker.process.pid : "?"));
1201
1221
  // Mark that we received a proper IPC completion, so the exit
@@ -1698,6 +1718,7 @@ function createSDKBridge(opts) {
1698
1718
  for await (var msg of myQueryInstance) {
1699
1719
  processSDKMessage(session, msg);
1700
1720
  }
1721
+ // (getContextUsage moved to processSDKMessage result handler -- fire-and-forget)
1701
1722
  // Stream ended normally after a task stop — no "result" message was sent,
1702
1723
  // so the session is still marked as processing. Send interrupted feedback.
1703
1724
  if (session.isProcessing && session.taskStopRequested) {
@@ -1921,6 +1942,7 @@ function createSDKBridge(opts) {
1921
1942
  abortController: session.abortController,
1922
1943
  promptSuggestions: true,
1923
1944
  agentProgressSummaries: true,
1945
+ mcpServers: mcpServers || undefined,
1924
1946
  canUseTool: function(toolName, input, toolOpts) {
1925
1947
  return handleCanUseTool(session, toolName, input, toolOpts);
1926
1948
  },
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);