clay-server 2.26.0-beta.5 → 2.26.0-beta.7
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/browser-mcp-server.js +496 -0
- package/lib/project.js +340 -17
- package/lib/public/app.js +111 -1
- package/lib/public/css/input.css +16 -5
- package/lib/public/css/overlays.css +181 -0
- package/lib/public/css/rewind.css +79 -0
- package/lib/public/css/server-settings.css +1 -0
- package/lib/public/css/title-bar.css +3 -3
- package/lib/public/index.html +24 -1
- package/lib/public/modules/context-sources.js +116 -29
- package/lib/public/modules/notifications.js +109 -1
- package/lib/sdk-bridge.js +3 -0
- package/lib/server.js +42 -0
- package/package.json +1 -1
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
|
|
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
|
|
184
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
package/lib/public/css/input.css
CHANGED
|
@@ -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
|
========================================================================== */
|
|
@@ -682,11 +698,6 @@
|
|
|
682
698
|
flex-wrap: wrap;
|
|
683
699
|
gap: 4px;
|
|
684
700
|
padding: 4px 6px;
|
|
685
|
-
position: absolute;
|
|
686
|
-
bottom: 100%;
|
|
687
|
-
left: 0;
|
|
688
|
-
right: 0;
|
|
689
|
-
z-index: 5;
|
|
690
701
|
}
|
|
691
702
|
|
|
692
703
|
#suggestion-chips.hidden { display: none; }
|