clay-server 2.26.0-beta.5 → 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.
package/lib/project.js CHANGED
@@ -161,8 +161,13 @@ function createProjectContext(opts) {
161
161
  var worktreeMeta = opts.worktreeMeta || null; // { parentSlug, branch, accessible }
162
162
  var isMate = opts.isMate || false;
163
163
  var onCreateWorktree = opts.onCreateWorktree || null;
164
+ var serverPort = opts.port || 2633;
165
+ var serverTls = opts.tls || false;
164
166
  var latestVersion = null;
165
167
 
168
+ // Browser MCP server runs in-process via createSdkMcpServer (no child process spawn).
169
+ // Do NOT write to .claude-local/settings.json -- the SDK reads that too, causing duplicate spawns.
170
+
166
171
  // --- Chat image storage ---
167
172
  var _imgConfig = require("./config");
168
173
  var _imgUtils = require("./utils");
@@ -172,7 +177,18 @@ function createProjectContext(opts) {
172
177
 
173
178
  // Convert imageRefs in history entries to images with URLs for the client
174
179
  function hydrateImageRefs(entry) {
175
- if (!entry || !entry.imageRefs) return entry;
180
+ if (!entry) return entry;
181
+ // Hydrate context_preview: convert screenshotFile to screenshotUrl
182
+ if (entry.type === "context_preview" && entry.tab && entry.tab.screenshotFile) {
183
+ var hydrated = {};
184
+ for (var k in entry) hydrated[k] = entry[k];
185
+ hydrated.tab = {};
186
+ for (var tk in entry.tab) hydrated.tab[tk] = entry.tab[tk];
187
+ hydrated.tab.screenshotUrl = "/p/" + slug + "/images/" + entry.tab.screenshotFile;
188
+ delete hydrated.tab.screenshotFile;
189
+ return hydrated;
190
+ }
191
+ if (!entry.imageRefs) return entry;
176
192
  if (entry.type !== "user_message" && entry.type !== "mention_user") return entry;
177
193
  var images = [];
178
194
  for (var ri = 0; ri < entry.imageRefs.length; ri++) {
@@ -180,8 +196,8 @@ function createProjectContext(opts) {
180
196
  images.push({ mediaType: ref.mediaType, url: "/p/" + slug + "/images/" + ref.file });
181
197
  }
182
198
  var hydrated = {};
183
- for (var k in entry) {
184
- if (k !== "imageRefs") hydrated[k] = entry[k];
199
+ for (var k2 in entry) {
200
+ if (k2 !== "imageRefs") hydrated[k2] = entry[k2];
185
201
  }
186
202
  hydrated.images = images;
187
203
  return hydrated;
@@ -275,6 +291,60 @@ function createProjectContext(opts) {
275
291
  // --- Per-project clients ---
276
292
  var clients = new Set();
277
293
 
294
+ // --- Browser extension state ---
295
+ var _browserTabList = {}; // tabId -> { id, url, title, favIconUrl }
296
+ var _extensionWs = null; // WebSocket of the client with the Chrome extension
297
+ var _extToken = crypto.randomUUID(); // Auth token for MCP server bridge
298
+ var pendingExtensionRequests = {}; // requestId -> { resolve, timer }
299
+
300
+ function sendExtensionCommand(ws, command, args, timeout) {
301
+ return new Promise(function(resolve) {
302
+ var requestId = crypto.randomUUID();
303
+ var ms = timeout || 3000;
304
+ var timer = setTimeout(function() {
305
+ delete pendingExtensionRequests[requestId];
306
+ resolve(null);
307
+ }, ms);
308
+ pendingExtensionRequests[requestId] = { resolve: resolve, timer: timer };
309
+ sendTo(ws, {
310
+ type: "extension_command",
311
+ command: command,
312
+ args: args,
313
+ requestId: requestId
314
+ });
315
+ });
316
+ }
317
+
318
+ // Send extension command via the tracked extension client (for MCP bridge)
319
+ function sendExtensionCommandAny(command, args, timeout) {
320
+ if (!_extensionWs || _extensionWs.readyState !== 1) {
321
+ return Promise.reject(new Error("Browser extension not connected"));
322
+ }
323
+ return sendExtensionCommand(_extensionWs, command, args, timeout);
324
+ }
325
+
326
+ function requestTabContext(ws, tabId) {
327
+ // Try inject first (best-effort), then request all data in parallel.
328
+ // Even if inject fails (CSP etc.), page text and screenshot still work.
329
+ return sendExtensionCommand(ws, "tab_inject", { tabId: tabId }).then(function() {}, function() {}).then(function() {
330
+ return Promise.all([
331
+ sendExtensionCommand(ws, "tab_console", { tabId: tabId }),
332
+ sendExtensionCommand(ws, "tab_network", { tabId: tabId }),
333
+ sendExtensionCommand(ws, "tab_page_text", { tabId: tabId }),
334
+ sendExtensionCommand(ws, "tab_screenshot", { tabId: tabId })
335
+ ]);
336
+ }).then(function(results) {
337
+ return {
338
+ console: results[0],
339
+ network: results[1],
340
+ pageText: results[2],
341
+ screenshot: results[3]
342
+ };
343
+ }).catch(function() {
344
+ return null;
345
+ });
346
+ }
347
+
278
348
  function send(obj) {
279
349
  var data = JSON.stringify(obj);
280
350
  for (var ws of clients) {
@@ -487,6 +557,45 @@ function createProjectContext(opts) {
487
557
  mateDisplayName: opts.mateDisplayName || "",
488
558
  isMate: isMate,
489
559
  dangerouslySkipPermissions: dangerouslySkipPermissions,
560
+ mcpServers: isMate ? undefined : (function () {
561
+ try {
562
+ var browserMcp = require("./browser-mcp-server");
563
+ var mcpConfig = browserMcp.create(sendExtensionCommandAny, function () {
564
+ return Object.values(_browserTabList || {});
565
+ }, {
566
+ watchTab: function (tabId) {
567
+ var key = "tab:" + tabId;
568
+ var active = loadContextSources(slug);
569
+ if (active.indexOf(key) === -1) {
570
+ active.push(key);
571
+ saveContextSources(slug, active);
572
+ var msg = JSON.stringify({ type: "context_sources_state", active: active });
573
+ for (var c of clients) { if (c.readyState === 1) c.send(msg); }
574
+ }
575
+ return active;
576
+ },
577
+ unwatchTab: function (tabId) {
578
+ var key = "tab:" + tabId;
579
+ var active = loadContextSources(slug);
580
+ var idx = active.indexOf(key);
581
+ if (idx !== -1) {
582
+ active.splice(idx, 1);
583
+ saveContextSources(slug, active);
584
+ var msg = JSON.stringify({ type: "context_sources_state", active: active });
585
+ for (var c of clients) { if (c.readyState === 1) c.send(msg); }
586
+ }
587
+ return active;
588
+ },
589
+ });
590
+ if (!mcpConfig) return undefined;
591
+ var servers = {};
592
+ servers[mcpConfig.name || "clay-browser"] = mcpConfig;
593
+ return servers;
594
+ } catch (e) {
595
+ console.error("[project] Failed to create browser MCP server:", e.message);
596
+ return undefined;
597
+ }
598
+ })(),
490
599
  onProcessingChanged: onProcessingChanged,
491
600
  onTurnDone: isMate ? function (session, preview) { digestDmTurn(session, preview); } : null,
492
601
  scheduleMessage: function (session, text, resetsAt) {
@@ -1263,7 +1372,9 @@ function createProjectContext(opts) {
1263
1372
  }
1264
1373
  sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1265
1374
  sendTo(ws, { type: "term_list", terminals: tm.list() });
1266
- sendTo(ws, { type: "context_sources_state", active: loadContextSources(slug) });
1375
+ // Restore context sources (keep tab: sources — validated against _browserTabList at query time)
1376
+ var restoredSources = loadContextSources(slug);
1377
+ sendTo(ws, { type: "context_sources_state", active: restoredSources });
1267
1378
  sendTo(ws, { type: "notes_list", notes: nm.list() });
1268
1379
  sendTo(ws, { type: "loop_registry_updated", records: getHubSchedules() });
1269
1380
 
@@ -3372,6 +3483,27 @@ function createProjectContext(opts) {
3372
3483
  return;
3373
3484
  }
3374
3485
 
3486
+ // --- Browser Extension ---
3487
+ if (msg.type === "browser_tab_list") {
3488
+ _extensionWs = ws; // Track which client has the extension
3489
+ var tabs = msg.tabs || [];
3490
+ _browserTabList = {};
3491
+ for (var bti = 0; bti < tabs.length; bti++) {
3492
+ _browserTabList[tabs[bti].id] = tabs[bti];
3493
+ }
3494
+ return;
3495
+ }
3496
+
3497
+ if (msg.type === "extension_result") {
3498
+ var pending = pendingExtensionRequests[msg.requestId];
3499
+ if (pending) {
3500
+ clearTimeout(pending.timer);
3501
+ pending.resolve(msg.result);
3502
+ delete pendingExtensionRequests[msg.requestId];
3503
+ }
3504
+ return;
3505
+ }
3506
+
3375
3507
  // --- Scheduled tasks permission gate ---
3376
3508
  if (msg.type === "loop_start" || msg.type === "loop_stop" || msg.type === "loop_registry_files" ||
3377
3509
  msg.type === "loop_registry_list" || msg.type === "loop_registry_update" || msg.type === "loop_registry_rename" ||
@@ -3915,23 +4047,176 @@ function createProjectContext(opts) {
3915
4047
  }
3916
4048
  }
3917
4049
 
3918
- if (!session.isProcessing) {
3919
- session.isProcessing = true;
3920
- onProcessingChanged();
3921
- session.sentToolResults = {};
3922
- sendToSession(session.localId, { type: "status", status: "processing" });
3923
- if (!session.queryInstance && (!session.worker || session.messageQueue !== "worker")) {
3924
- // No active query (or worker idle between queries): start a new query
3925
- session._queryStartTs = Date.now();
3926
- console.log("[PERF] project.js: startQuery called, localId=" + session.localId + " t=0ms");
3927
- sdk.startQuery(session, fullText, msg.images, getLinuxUserForSession(session));
4050
+ // Collect browser tab context (async: requires round-trip to client extension)
4051
+ var tabSources = ctxSources.filter(function(id) {
4052
+ if (!id.startsWith("tab:")) return false;
4053
+ // Only include tabs that currently exist in the browser
4054
+ var tid = parseInt(id.split(":")[1], 10);
4055
+ return !!_browserTabList[tid];
4056
+ });
4057
+
4058
+ function dispatchToSdk(finalText) {
4059
+ if (!session.isProcessing) {
4060
+ session.isProcessing = true;
4061
+ onProcessingChanged();
4062
+ session.sentToolResults = {};
4063
+ sendToSession(session.localId, { type: "status", status: "processing" });
4064
+ if (!session.queryInstance && (!session.worker || session.messageQueue !== "worker")) {
4065
+ // No active query (or worker idle between queries): start a new query
4066
+ session._queryStartTs = Date.now();
4067
+ console.log("[PERF] project.js: startQuery called, localId=" + session.localId + " t=0ms");
4068
+ sdk.startQuery(session, finalText, msg.images, getLinuxUserForSession(session));
4069
+ } else {
4070
+ sdk.pushMessage(session, finalText, msg.images);
4071
+ }
3928
4072
  } else {
3929
- sdk.pushMessage(session, fullText, msg.images);
4073
+ sdk.pushMessage(session, finalText, msg.images);
3930
4074
  }
4075
+ sm.broadcastSessionList();
4076
+ }
4077
+
4078
+ if (tabSources.length > 0) {
4079
+ // Request tab context from all active browser tab sources
4080
+ var tabPromises = tabSources.map(function(srcId) {
4081
+ var tabId = parseInt(srcId.split(":")[1], 10);
4082
+ return requestTabContext(ws, tabId);
4083
+ });
4084
+ Promise.all(tabPromises).then(function(results) {
4085
+ var tabContextParts = [];
4086
+ var screenshotImages = [];
4087
+
4088
+ for (var ti = 0; ti < results.length; ti++) {
4089
+ if (!results[ti]) continue;
4090
+ var tabId2 = parseInt(tabSources[ti].split(":")[1], 10);
4091
+ var tabInfo = _browserTabList[tabId2];
4092
+ var tabLabel = tabInfo ? (tabInfo.title || tabInfo.url || "Tab " + tabId2) : "Tab " + tabId2;
4093
+ var r = results[ti];
4094
+ var parts = [];
4095
+
4096
+ // Console logs
4097
+ if (r.console && r.console.logs) {
4098
+ try {
4099
+ var logs = typeof r.console.logs === "string" ? JSON.parse(r.console.logs) : r.console.logs;
4100
+ if (logs && logs.length > 0) {
4101
+ var logLines = [];
4102
+ var logSlice = logs.slice(-50);
4103
+ for (var li = 0; li < logSlice.length; li++) {
4104
+ var entry = logSlice[li];
4105
+ var ts = entry.ts ? new Date(entry.ts).toTimeString().slice(0, 8) : "";
4106
+ var lvl = (entry.level || "log").toUpperCase();
4107
+ logLines.push("[" + ts + " " + lvl + "] " + (entry.text || ""));
4108
+ }
4109
+ parts.push("Console:\n" + logLines.join("\n"));
4110
+ }
4111
+ } catch (e) {
4112
+ // ignore parse errors
4113
+ }
4114
+ }
4115
+
4116
+ // Network requests
4117
+ if (r.network && r.network.network) {
4118
+ try {
4119
+ var netLog = typeof r.network.network === "string" ? JSON.parse(r.network.network) : r.network.network;
4120
+ if (netLog && netLog.length > 0) {
4121
+ var netLines = [];
4122
+ var netSlice = netLog.slice(-30);
4123
+ for (var ni = 0; ni < netSlice.length; ni++) {
4124
+ var req = netSlice[ni];
4125
+ var line = (req.method || "GET") + " " + (req.url || "") + " " + (req.status || 0) + " " + (req.duration || 0) + "ms";
4126
+ if (req.error) line += " [" + req.error + "]";
4127
+ netLines.push(line);
4128
+ }
4129
+ parts.push("Network (last " + netSlice.length + " requests):\n" + netLines.join("\n"));
4130
+ }
4131
+ } catch (e) {
4132
+ // ignore parse errors
4133
+ }
4134
+ }
4135
+
4136
+ // Page text (from tab_page_text command)
4137
+ if (r.pageText && (r.pageText.text || r.pageText.value)) {
4138
+ var pageContent = r.pageText.text || r.pageText.value;
4139
+ if (pageContent.length > 0) {
4140
+ if (pageContent.length > 32768) {
4141
+ pageContent = pageContent.substring(0, 32768) + "\n... (truncated)";
4142
+ }
4143
+ parts.push("Page text:\n" + pageContent);
4144
+ }
4145
+ }
4146
+
4147
+ // Screenshot — save to disk and add to images for SDK
4148
+ if (r.screenshot && r.screenshot.image) {
4149
+ try {
4150
+ var screenshotData = r.screenshot.image;
4151
+ var screenshotName = saveImageFile("image/png", screenshotData, getLinuxUserForSession(session));
4152
+ if (screenshotName) {
4153
+ var screenshotPath = path.join(imagesDir, screenshotName);
4154
+ // Add to images array for SDK multimodal
4155
+ screenshotImages.push({
4156
+ mediaType: "image/png",
4157
+ data: screenshotData,
4158
+ file: screenshotName,
4159
+ tabTitle: tabLabel,
4160
+ tabUrl: tabInfo ? tabInfo.url : ""
4161
+ });
4162
+ parts.push("[Screenshot saved: " + screenshotPath + "]");
4163
+ }
4164
+ } catch (e) {
4165
+ // ignore screenshot save errors
4166
+ }
4167
+ }
4168
+
4169
+ if (r.console && r.console.error) {
4170
+ parts.push("(Console error: " + r.console.error + ")");
4171
+ }
4172
+ if (r.network && r.network.error) {
4173
+ parts.push("(Network error: " + r.network.error + ")");
4174
+ }
4175
+
4176
+ if (parts.length > 0) {
4177
+ tabContextParts.push("[Browser tab: " + tabLabel + "]\n" + parts.join("\n\n"));
4178
+ }
4179
+ }
4180
+
4181
+ if (tabContextParts.length > 0) {
4182
+ fullText = tabContextParts.join("\n\n---\n\n") + "\n\n" + fullText;
4183
+ }
4184
+
4185
+ // If screenshots were captured, send context preview cards and add to SDK images
4186
+ if (screenshotImages.length > 0) {
4187
+ if (!msg.images) msg.images = [];
4188
+ for (var si = 0; si < screenshotImages.length; si++) {
4189
+ var ss = screenshotImages[si];
4190
+ // Save context_preview to history so it restores on session load
4191
+ var previewEntry = {
4192
+ type: "context_preview",
4193
+ tab: {
4194
+ title: ss.tabTitle || "",
4195
+ url: ss.tabUrl || "",
4196
+ screenshotFile: ss.file
4197
+ }
4198
+ };
4199
+ session.history.push(previewEntry);
4200
+ // Send context card to all clients
4201
+ sendToSession(session.localId, {
4202
+ type: "context_preview",
4203
+ tab: {
4204
+ title: ss.tabTitle || "",
4205
+ url: ss.tabUrl || "",
4206
+ screenshotUrl: "/p/" + slug + "/images/" + ss.file
4207
+ }
4208
+ });
4209
+ // Add to SDK images for multimodal
4210
+ msg.images.push({ mediaType: ss.mediaType, data: ss.data });
4211
+ }
4212
+ sm.saveSessionFile(session);
4213
+ }
4214
+
4215
+ dispatchToSdk(fullText);
4216
+ });
3931
4217
  } else {
3932
- sdk.pushMessage(session, fullText, msg.images);
4218
+ dispatchToSdk(fullText);
3933
4219
  }
3934
- sm.broadcastSessionList();
3935
4220
  }
3936
4221
 
3937
4222
  // --- Shared helpers ---
@@ -4061,6 +4346,44 @@ function createProjectContext(opts) {
4061
4346
 
4062
4347
  // --- Handle project-scoped HTTP requests ---
4063
4348
  function handleHTTP(req, res, urlPath) {
4349
+ // Browser MCP extension bridge: forward commands to Chrome extension
4350
+ if (req.method === "POST" && urlPath === "/ext-command") {
4351
+ parseJsonBody(req).then(function (body) {
4352
+ // Validate auth token
4353
+ if (!body.token || body.token !== _extToken) {
4354
+ res.writeHead(403, { "Content-Type": "application/json" });
4355
+ res.end('{"error":"Invalid token"}');
4356
+ return;
4357
+ }
4358
+ var command = body.command;
4359
+ var args = body.args || {};
4360
+ var timeout = Math.min(body.timeout || 5000, 30000); // max 30s
4361
+
4362
+ // Special command: list_tabs (no extension round-trip needed)
4363
+ if (command === "list_tabs") {
4364
+ var tabArr = [];
4365
+ for (var tid in _browserTabList) {
4366
+ tabArr.push(_browserTabList[tid]);
4367
+ }
4368
+ res.writeHead(200, { "Content-Type": "application/json" });
4369
+ res.end(JSON.stringify({ result: { tabs: tabArr } }));
4370
+ return;
4371
+ }
4372
+
4373
+ sendExtensionCommandAny(command, args, timeout).then(function (result) {
4374
+ res.writeHead(200, { "Content-Type": "application/json" });
4375
+ res.end(JSON.stringify({ result: result || {} }));
4376
+ }).catch(function (err) {
4377
+ res.writeHead(200, { "Content-Type": "application/json" });
4378
+ res.end(JSON.stringify({ error: err.message || "Extension command failed" }));
4379
+ });
4380
+ }).catch(function () {
4381
+ res.writeHead(400, { "Content-Type": "application/json" });
4382
+ res.end('{"error":"Invalid JSON body"}');
4383
+ });
4384
+ return true;
4385
+ }
4386
+
4064
4387
  // Serve chat images
4065
4388
  if (req.method === "GET" && urlPath.indexOf("/images/") === 0) {
4066
4389
  var imgName = path.basename(urlPath);
package/lib/public/app.js CHANGED
@@ -12,7 +12,7 @@ import { initInput, clearPendingImages, handleInputSync, autoResize, builtinComm
12
12
  import { initQrCode, triggerShare } from './modules/qrcode.js';
13
13
  import { initFileBrowser, loadRootDirectory, refreshTree, handleFsList, handleFsRead, handleDirChanged, refreshIfOpen, handleFileChanged, handleFileHistory, handleGitDiff, handleFileAt, getPendingNavigate, closeFileViewer, resetFileBrowser } from './modules/filebrowser.js';
14
14
  import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermResized, handleTermExited, handleTermClosed, sendTerminalCommand } from './modules/terminal.js';
15
- import { initContextSources, updateTerminalList, handleContextSourcesState, getActiveSources, hasActiveSources } from './modules/context-sources.js';
15
+ import { initContextSources, updateTerminalList, updateBrowserTabList, handleContextSourcesState, getActiveSources, hasActiveSources } from './modules/context-sources.js';
16
16
  import { initStickyNotes, handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted, openArchive, closeArchive, isArchiveOpen, hideNotes, showNotes, isNotesVisible } from './modules/sticky-notes.js';
17
17
  import { initTheme, getThemeColor, getComputedVar, onThemeChange, getCurrentTheme } from './modules/theme.js';
18
18
  import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionResolved, markPermissionCancelled, renderElicitationRequest, markElicitationResolved, renderPlanBanner, renderPlanCard, handleTodoWrite, handleTaskCreate, handleTaskUpdate, startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, addTurnMeta, resetTurnMetaCost, enableMainInput, getTools, getPlanContent, setPlanContent, isPlanFilePath, getTodoTools, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, updateSubagentProgress, initSubagentStop, closeToolGroup, removeToolFromGroup } from './modules/tools.js';
@@ -4588,6 +4588,59 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4588
4588
  }
4589
4589
  break;
4590
4590
 
4591
+ case "context_preview":
4592
+ // Show a Context Card with tab screenshot between user message and assistant response
4593
+ if (msg.tab) {
4594
+ var card = document.createElement("div");
4595
+ card.className = "context-card";
4596
+
4597
+ // Header
4598
+ var header = document.createElement("div");
4599
+ header.className = "context-card-header";
4600
+ var icon = document.createElement("span");
4601
+ icon.className = "context-card-icon";
4602
+ icon.textContent = "\uD83D\uDC41";
4603
+ header.appendChild(icon);
4604
+ var label = document.createElement("span");
4605
+ label.textContent = "Viewing tab";
4606
+ header.appendChild(label);
4607
+ card.appendChild(header);
4608
+
4609
+ // Screenshot
4610
+ if (msg.tab.screenshotUrl) {
4611
+ var img = document.createElement("img");
4612
+ img.className = "context-card-screenshot";
4613
+ img.src = msg.tab.screenshotUrl;
4614
+ img.loading = "lazy";
4615
+ img.addEventListener("click", function () { showImageModal(this.src); });
4616
+ card.appendChild(img);
4617
+ }
4618
+
4619
+ // Meta: title + domain
4620
+ var tabTitle = msg.tab.title || "";
4621
+ var tabDomain = "";
4622
+ try { tabDomain = new URL(msg.tab.url).hostname; } catch (e) {}
4623
+ if (tabTitle || tabDomain) {
4624
+ var meta = document.createElement("div");
4625
+ meta.className = "context-card-meta";
4626
+ var titleEl = document.createElement("span");
4627
+ titleEl.className = "context-card-title";
4628
+ titleEl.textContent = tabTitle;
4629
+ meta.appendChild(titleEl);
4630
+ if (tabDomain) {
4631
+ var domainEl = document.createElement("span");
4632
+ domainEl.className = "context-card-domain";
4633
+ domainEl.textContent = tabDomain;
4634
+ meta.appendChild(domainEl);
4635
+ }
4636
+ card.appendChild(meta);
4637
+ }
4638
+
4639
+ messagesEl.appendChild(card);
4640
+ scrollToBottom();
4641
+ }
4642
+ break;
4643
+
4591
4644
  case "status":
4592
4645
  if (msg.status === "processing") {
4593
4646
  setStatus("processing");
@@ -4972,6 +5025,10 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4972
5025
  handleContextSourcesState(msg);
4973
5026
  break;
4974
5027
 
5028
+ case "extension_command":
5029
+ sendExtensionCommand(msg.command, msg.args, msg.requestId);
5030
+ break;
5031
+
4975
5032
  case "term_created":
4976
5033
  handleTermCreated(msg);
4977
5034
  if (pendingTermCommand) {
@@ -5942,6 +5999,59 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5942
5999
  get connected() { return connected; },
5943
6000
  });
5944
6001
 
6002
+ // --- Chrome Extension Bridge ---
6003
+ var _extRequestCallbacks = {}; // requestId -> callback function
6004
+
6005
+ function sendExtensionCommand(command, args, requestId) {
6006
+ window.postMessage({
6007
+ source: "clay-page",
6008
+ payload: {
6009
+ type: "clay_ext_command",
6010
+ command: command,
6011
+ args: args,
6012
+ requestId: requestId
6013
+ }
6014
+ }, "*");
6015
+ }
6016
+
6017
+ function handleExtensionResult(requestId, result) {
6018
+ // Check local callback first (for server-initiated requests)
6019
+ var cb = _extRequestCallbacks[requestId];
6020
+ if (cb) {
6021
+ delete _extRequestCallbacks[requestId];
6022
+ cb(result);
6023
+ return;
6024
+ }
6025
+ // Forward to server
6026
+ if (ws && ws.readyState === 1) {
6027
+ ws.send(JSON.stringify({
6028
+ type: "extension_result",
6029
+ requestId: requestId,
6030
+ result: result
6031
+ }));
6032
+ }
6033
+ }
6034
+
6035
+ window.addEventListener("message", function(event) {
6036
+ if (event.source !== window) return;
6037
+ if (!event.data || event.data.source !== "clay-chrome-extension") return;
6038
+ var msg = event.data.payload;
6039
+
6040
+ if (msg.type === "clay_ext_tab_list") {
6041
+ updateBrowserTabList(msg.tabs);
6042
+ // Also inform server about tab list
6043
+ if (ws && ws.readyState === 1) {
6044
+ ws.send(JSON.stringify({
6045
+ type: "browser_tab_list",
6046
+ tabs: msg.tabs
6047
+ }));
6048
+ }
6049
+ }
6050
+ if (msg.type === "clay_ext_result") {
6051
+ handleExtensionResult(msg.requestId, msg.result);
6052
+ }
6053
+ });
6054
+
5945
6055
  // --- Playbook Engine ---
5946
6056
  initPlaybook();
5947
6057
 
@@ -328,6 +328,12 @@
328
328
  white-space: nowrap;
329
329
  border: 1px solid var(--border);
330
330
  transition: border-color 0.15s;
331
+ animation: chipIn 0.3s ease-out;
332
+ }
333
+
334
+ @keyframes chipIn {
335
+ from { opacity: 0; transform: translateY(6px) scale(0.95); }
336
+ to { opacity: 1; transform: translateY(0) scale(1); }
331
337
  }
332
338
 
333
339
  .context-chip-label {
@@ -368,6 +374,8 @@
368
374
  bottom: calc(100% + 4px);
369
375
  left: 0;
370
376
  min-width: 200px;
377
+ max-height: 320px;
378
+ overflow-y: auto;
371
379
  background: var(--sidebar-bg);
372
380
  border: 1px solid var(--border);
373
381
  border-radius: 10px;
@@ -430,6 +438,14 @@
430
438
  text-align: center;
431
439
  }
432
440
 
441
+ .context-picker-favicon {
442
+ width: 14px;
443
+ height: 14px;
444
+ border-radius: 2px;
445
+ flex-shrink: 0;
446
+ object-fit: contain;
447
+ }
448
+
433
449
  /* ==========================================================================
434
450
  Input Area — Claude-style unified container
435
451
  ========================================================================== */