clay-server 2.26.0-beta.17 → 2.26.0-beta.19

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.
@@ -111,7 +111,7 @@ function create(sendCommand, getTabList, contextOps) {
111
111
  // --- browser_screenshot ---
112
112
  tools.push(tool(
113
113
  "browser_screenshot",
114
- "Capture a screenshot of a browser tab (full viewport or a specific element)",
114
+ "Capture a screenshot of a browser tab. Skip if the tab is already attached as a context source (data is auto-injected).",
115
115
  buildShape({
116
116
  tabId: { type: "number", description: "Tab ID" },
117
117
  selector: { type: "string", description: "CSS selector to capture a specific element (optional)" },
@@ -134,7 +134,7 @@ function create(sendCommand, getTabList, contextOps) {
134
134
  // --- browser_console ---
135
135
  tools.push(tool(
136
136
  "browser_console",
137
- "Read captured console logs from a tab (log, warn, error, info)",
137
+ "Read captured console logs from a tab. Skip if the tab is already a context source (data is auto-injected).",
138
138
  buildShape({
139
139
  tabId: { type: "number", description: "Tab ID" },
140
140
  }, ["tabId"]),
@@ -157,7 +157,7 @@ function create(sendCommand, getTabList, contextOps) {
157
157
  // --- browser_network ---
158
158
  tools.push(tool(
159
159
  "browser_network",
160
- "Read captured network requests (fetch/XHR) from a tab",
160
+ "Read captured network requests (fetch/XHR) from a tab. Skip if the tab is already a context source.",
161
161
  buildShape({
162
162
  tabId: { type: "number", description: "Tab ID" },
163
163
  }, ["tabId"]),
@@ -181,7 +181,7 @@ function create(sendCommand, getTabList, contextOps) {
181
181
  // --- browser_read_page ---
182
182
  tools.push(tool(
183
183
  "browser_read_page",
184
- "Read page text content (innerText). Optionally read only a specific element.",
184
+ "Read page text content (innerText). Skip if the tab is already a context source (text is auto-injected). Use for tabs NOT in context sources, or to read a specific element via selector.",
185
185
  buildShape({
186
186
  tabId: { type: "number", description: "Tab ID" },
187
187
  selector: { type: "string", description: "CSS selector to read specific element (optional)" },
@@ -0,0 +1,94 @@
1
+ // Debate MCP Server for Clay (in-process SDK version)
2
+ // Provides the propose_debate tool so mates can propose debates
3
+ // via the SDK tool system instead of writing files to disk.
4
+ //
5
+ // Usage:
6
+ // var debateMcp = require("./debate-mcp-server");
7
+ // var mcpConfig = debateMcp.create(onPropose);
8
+ // // Pass mcpConfig to sdk-bridge opts.mcpServers
9
+
10
+ var z;
11
+ try { z = require("zod"); } catch (e) { z = null; }
12
+
13
+ function buildShape(props, required) {
14
+ if (!z) return {};
15
+ var shape = {};
16
+ var keys = Object.keys(props);
17
+ for (var i = 0; i < keys.length; i++) {
18
+ var k = keys[i];
19
+ var p = props[k];
20
+ var field;
21
+ if (p.type === "number") field = z.number();
22
+ else if (p.type === "boolean") field = z.boolean();
23
+ else if (p.enum) field = z.enum(p.enum);
24
+ else field = z.string();
25
+ if (p.description) field = field.describe(p.description);
26
+ if (!required || required.indexOf(k) === -1) field = field.optional();
27
+ shape[k] = field;
28
+ }
29
+ return shape;
30
+ }
31
+
32
+ // onPropose(briefData) -> Promise<{action: "start"|"cancel"}>
33
+ // The returned Promise blocks the tool until the user approves or cancels.
34
+ function create(onPropose) {
35
+ var sdk;
36
+ try { sdk = require("@anthropic-ai/claude-agent-sdk"); } catch (e) {
37
+ console.error("[debate-mcp] Failed to load SDK:", e.message);
38
+ return null;
39
+ }
40
+
41
+ var createSdkMcpServer = sdk.createSdkMcpServer;
42
+ var tool = sdk.tool;
43
+ if (!createSdkMcpServer || !tool) {
44
+ console.error("[debate-mcp] SDK missing createSdkMcpServer or tool helper");
45
+ return null;
46
+ }
47
+
48
+ var tools = [];
49
+
50
+ tools.push(tool(
51
+ "propose_debate",
52
+ "Propose a structured debate among Clay Mates. The user will see an inline approval card. The tool blocks until the user approves or cancels.",
53
+ buildShape({
54
+ topic: { type: "string", description: "The debate topic" },
55
+ format: { type: "string", description: "Debate format, e.g. free_discussion (default)" },
56
+ context: { type: "string", description: "Key context from the conversation that panelists should know" },
57
+ specialRequests: { type: "string", description: "Special instructions for the debate, or empty" },
58
+ panelists: { type: "string", description: "JSON array of panelist objects: [{\"mateId\": \"<UUID>\", \"role\": \"perspective\", \"brief\": \"guidance\"}]" },
59
+ }, ["topic", "panelists"]),
60
+ function (args) {
61
+ var panelists;
62
+ try {
63
+ panelists = JSON.parse(args.panelists);
64
+ } catch (e) {
65
+ return Promise.resolve({
66
+ content: [{ type: "text", text: "Error: panelists must be a valid JSON array. Got: " + (args.panelists || "").substring(0, 100) }],
67
+ });
68
+ }
69
+
70
+ var briefData = {
71
+ topic: args.topic || "Untitled debate",
72
+ format: args.format || "free_discussion",
73
+ context: args.context || "",
74
+ specialRequests: args.specialRequests || null,
75
+ panelists: panelists,
76
+ };
77
+
78
+ return onPropose(briefData).then(function (result) {
79
+ if (result && result.action === "start") {
80
+ return { content: [{ type: "text", text: "Debate approved and started. Topic: " + briefData.topic }] };
81
+ }
82
+ return { content: [{ type: "text", text: "Debate proposal was cancelled by the user." }] };
83
+ });
84
+ }
85
+ ));
86
+
87
+ return createSdkMcpServer({
88
+ name: "clay-debate",
89
+ version: "1.0.0",
90
+ tools: tools,
91
+ });
92
+ }
93
+
94
+ module.exports = { create: create };
package/lib/mates.js CHANGED
@@ -633,35 +633,23 @@ var DEBATE_AWARENESS_SECTION =
633
633
  "\n\n" + DEBATE_AWARENESS_MARKER + "\n" +
634
634
  "## Proposing Debates\n\n" +
635
635
  "**This section is managed by the system and cannot be removed.**\n\n" +
636
- "When the user suggests that a topic would benefit from a multi-perspective debate " +
637
- "(e.g., \"let's debate this\", \"I want to hear different viewpoints\"), you can propose " +
638
- "a structured debate by writing a brief file.\n\n" +
636
+ "When the user suggests a debate, you MUST use the `propose_debate` tool. " +
637
+ "NEVER write debate files to disk. NEVER mkdir for debates. NEVER use Write/Bash for debate setup. " +
638
+ "The ONLY way to propose a debate is the `propose_debate` tool.\n\n" +
639
639
  "**How to propose a debate:**\n" +
640
- "1. Generate a unique ID: `debate_` followed by the current timestamp in milliseconds\n" +
641
- "2. Write the brief as JSON to: `.clay/debates/<debate_id>/brief.json` (relative to the project root)\n" +
642
- "3. The system will detect the file and show the user an inline card with your proposal\n" +
643
- "4. The user can then approve or cancel the debate\n\n" +
644
- "**Brief JSON schema:**\n" +
645
- "```json\n" +
646
- "{\n" +
647
- " \"topic\": \"The refined debate topic\",\n" +
648
- " \"format\": \"free_discussion\",\n" +
649
- " \"context\": \"Key context from the conversation that panelists should know\",\n" +
650
- " \"specialRequests\": \"Any special instructions, or null\",\n" +
651
- " \"panelists\": [\n" +
652
- " {\n" +
653
- " \"mateId\": \"<mate UUID from the team roster above>\",\n" +
654
- " \"role\": \"The perspective or stance this panelist should take\",\n" +
655
- " \"brief\": \"Specific guidance for this panelist\"\n" +
656
- " }\n" +
657
- " ]\n" +
658
- "}\n" +
659
- "```\n\n" +
640
+ "Call the `propose_debate` tool with these parameters:\n" +
641
+ "- `topic` (required): The refined debate topic\n" +
642
+ "- `format`: Debate format, default \"free_discussion\"\n" +
643
+ "- `context`: Key context from the conversation that panelists should know\n" +
644
+ "- `specialRequests`: Any special instructions\n" +
645
+ "- `panelists` (required): A JSON string array of panelist objects:\n" +
646
+ " `[{\"mateId\": \"<mate UUID from team roster>\", \"role\": \"perspective\", \"brief\": \"guidance\"}]`\n\n" +
647
+ "The user will see an inline approval card. The tool blocks until they approve or cancel.\n\n" +
660
648
  "**Rules:**\n" +
661
649
  "- Choose 2-4 panelists from the team roster. Pick mates whose expertise fits the topic.\n" +
662
650
  "- Do NOT include yourself as a panelist. You will moderate the debate.\n" +
663
651
  "- Only propose a debate when the user explicitly asks for one.\n" +
664
- "- Make sure the directory exists before writing (use mkdir -p or equivalent).\n";
652
+ "- Do NOT write files to disk for debate proposals. Always use the propose_debate tool.\n";
665
653
 
666
654
  function enforceDebateAwareness(filePath) {
667
655
  if (!fs.existsSync(filePath)) return false;
@@ -14,6 +14,13 @@ var matesModule = require("./mates");
14
14
  */
15
15
  function attachDebate(ctx) {
16
16
 
17
+ // For mate projects, enforce latest debate awareness prompt in CLAUDE.md
18
+ // so mates use the propose_debate MCP tool instead of writing files.
19
+ if (ctx.isMate) {
20
+ var _debateClaudeMdPath = path.join(ctx.cwd, "CLAUDE.md");
21
+ try { matesModule.enforceDebateAwareness(_debateClaudeMdPath); } catch (e) {}
22
+ }
23
+
17
24
  // --- Helpers shared with other modules ---
18
25
 
19
26
  function escapeRegex(str) {
@@ -270,9 +277,9 @@ function attachDebate(ctx) {
270
277
  var mateCtx = debate.mateCtx || matesModule.buildMateCtx(null);
271
278
  debate.nameMap = buildDebateNameMap(debate.panelists, mateCtx);
272
279
 
273
- // If debate was started from DM (no setupSessionId), go to reviewing phase
274
- if (!debate.setupSessionId) {
275
- console.log("[debate] Brief picked up from DM, entering review phase. Topic:", debate.topic);
280
+ // quickStart from DM or no setupSessionId: show brief card for user approval
281
+ if (!debate.setupSessionId || debate.quickStart) {
282
+ console.log("[debate] Brief picked up, entering review phase. Topic:", debate.topic);
276
283
  debate.phase = "reviewing";
277
284
  persistDebateState(session);
278
285
 
@@ -323,85 +330,18 @@ function attachDebate(ctx) {
323
330
  // --- Restore debate on reconnect ---
324
331
 
325
332
  function restoreDebateState(ws) {
326
- var userId = ws._clayUser ? ws._clayUser.id : null;
327
- var mateCtx = matesModule.buildMateCtx(userId);
328
-
333
+ // On server restart, SDK sessions are lost so debates cannot continue.
334
+ // Clear stale debate state instead of restoring dead UI.
329
335
  ctx.sm.sessions.forEach(function (session) {
330
- // Already restored
331
- if (session._debate) return;
332
-
333
- // Has persisted debate state?
334
- if (!session.debateState) return;
335
-
336
- var phase = session.debateState.phase;
337
- if (phase !== "preparing" && phase !== "reviewing" && phase !== "live") return;
338
-
339
- // Restore _debate from persisted state (pass userId for correct mateCtx)
340
- var debate = restoreDebateFromState(session, userId);
341
- if (!debate) return;
342
-
343
- // Update mateCtx with the connected user's context
344
- debate.mateCtx = mateCtx;
345
- debate.nameMap = buildDebateNameMap(debate.panelists, mateCtx);
346
-
347
- var moderatorProfile = ctx.getMateProfile(mateCtx, debate.moderatorId);
348
-
349
- if (phase === "preparing") {
350
- var briefPath = debate.briefPath;
351
- if (!briefPath && debate.debateId) {
352
- briefPath = path.join(ctx.cwd, ".clay", "debates", debate.debateId, "brief.json");
353
- }
354
- if (!briefPath) return;
355
-
356
- console.log("[debate] Restoring debate (preparing). topic:", debate.topic, "briefPath:", briefPath);
357
- startDebateBriefWatcher(session, debate, briefPath);
358
-
359
- ctx.sendTo(ws, {
360
- type: "debate_preparing",
361
- topic: debate.topic,
362
- moderatorId: debate.moderatorId,
363
- moderatorName: moderatorProfile.name,
364
- setupSessionId: debate.setupSessionId,
365
- panelists: debate.panelists.map(function (p) {
366
- var prof = ctx.getMateProfile(mateCtx, p.mateId);
367
- return { mateId: p.mateId, name: prof.name };
368
- }),
369
- });
370
- } else if (phase === "reviewing") {
371
- console.log("[debate] Restoring debate (reviewing). topic:", debate.topic);
372
- ctx.sendTo(ws, {
373
- type: "debate_brief_ready",
374
- debateId: debate.debateId,
375
- topic: debate.topic,
376
- format: debate.format || "free_discussion",
377
- context: debate.context || "",
378
- specialRequests: debate.specialRequests || null,
379
- moderatorId: debate.moderatorId,
380
- moderatorName: moderatorProfile.name,
381
- panelists: debate.panelists.map(function (p) {
382
- var prof = ctx.getMateProfile(mateCtx, p.mateId);
383
- return { mateId: p.mateId, name: prof.name, role: p.role || "", brief: p.brief || "" };
384
- }),
385
- });
386
- } else if (phase === "live") {
387
- console.log("[debate] Restoring debate (live). topic:", debate.topic, "awaitingConclude:", debate.awaitingConcludeConfirm);
388
- // Debate was live when server restarted. It can't resume AI turns,
389
- // but we can show the sticky and let user see history.
390
- ctx.sendTo(ws, {
391
- type: "debate_started",
392
- topic: debate.topic,
393
- format: debate.format,
394
- round: debate.round,
395
- moderatorId: debate.moderatorId,
396
- moderatorName: moderatorProfile.name,
397
- panelists: debate.panelists.map(function (p) {
398
- var prof = ctx.getMateProfile(mateCtx, p.mateId);
399
- return { mateId: p.mateId, name: prof.name, role: p.role, avatarColor: prof.avatarColor, avatarStyle: prof.avatarStyle, avatarSeed: prof.avatarSeed };
400
- }),
401
- });
402
- // If moderator had concluded, re-send conclude confirm so client shows End/Continue UI
403
- if (debate.awaitingConcludeConfirm) {
404
- ctx.sendTo(ws, { type: "debate_conclude_confirm", topic: debate.topic, round: debate.round });
336
+ if (session._debate) {
337
+ delete session._debate;
338
+ }
339
+ if (session.debateState) {
340
+ var phase = session.debateState.phase;
341
+ if (phase === "preparing" || phase === "reviewing" || phase === "live") {
342
+ console.log("[debate] Clearing stale debate state:", session.debateState.topic);
343
+ session.debateState = null;
344
+ ctx.sm.saveSessionFile(session);
405
345
  }
406
346
  }
407
347
  });
@@ -1741,6 +1681,48 @@ function attachDebate(ctx) {
1741
1681
  })();
1742
1682
  }
1743
1683
 
1684
+ // --- MCP-based debate proposal approval ---
1685
+
1686
+ function handleMcpDebateApproval(session, briefData, mateId, ws) {
1687
+ if (session._debate && (session._debate.phase === "preparing" || session._debate.phase === "reviewing" || session._debate.phase === "live")) {
1688
+ console.warn("[debate] Cannot start MCP debate: another debate is active on this session");
1689
+ return;
1690
+ }
1691
+
1692
+ var userId = ws && ws._clayUser ? ws._clayUser.id : (session.ownerId || ctx.projectOwnerId || null);
1693
+ var mateCtx = matesModule.buildMateCtx(userId);
1694
+ var debateId = "debate_" + Date.now();
1695
+
1696
+ var debate = {
1697
+ phase: "reviewing",
1698
+ topic: briefData.topic || "Untitled debate",
1699
+ format: briefData.format || "free_discussion",
1700
+ context: briefData.context || "",
1701
+ specialRequests: briefData.specialRequests || null,
1702
+ moderatorId: mateId,
1703
+ panelists: (briefData.panelists || []).map(function (p) {
1704
+ return { mateId: p.mateId, role: p.role || "", brief: p.brief || "" };
1705
+ }),
1706
+ mateCtx: mateCtx,
1707
+ moderatorSession: null,
1708
+ panelistSessions: {},
1709
+ nameMap: null,
1710
+ turnInProgress: false,
1711
+ pendingComment: null,
1712
+ round: 1,
1713
+ history: [],
1714
+ setupSessionId: null,
1715
+ debateId: debateId,
1716
+ briefPath: null,
1717
+ ownerId: userId,
1718
+ };
1719
+ debate.nameMap = buildDebateNameMap(debate.panelists, mateCtx);
1720
+ session._debate = debate;
1721
+
1722
+ console.log("[debate] MCP debate approved. Topic:", debate.topic, "debateId:", debateId);
1723
+ startDebateLive(session);
1724
+ }
1725
+
1744
1726
  // --- Public API ---
1745
1727
 
1746
1728
  return {
@@ -1753,6 +1735,7 @@ function attachDebate(ctx) {
1753
1735
  handleDebateUserFloorResponse: handleDebateUserFloorResponse,
1754
1736
  restoreDebateState: restoreDebateState,
1755
1737
  checkForDmDebateBrief: checkForDmDebateBrief,
1738
+ handleMcpDebateApproval: handleMcpDebateApproval,
1756
1739
  };
1757
1740
  }
1758
1741
 
@@ -569,11 +569,10 @@ function attachMateInteraction(ctx) {
569
569
  onDone: mentionCallbacks.onDone,
570
570
  onError: mentionCallbacks.onError,
571
571
  canUseTool: function (toolName, input, toolOpts) {
572
- var autoAllow = { Read: true, Glob: true, Grep: true, WebFetch: true, WebSearch: true };
573
- if (autoAllow[toolName]) {
574
- return Promise.resolve({ behavior: "allow", updatedInput: input });
575
- }
576
- // Route through the project session's permission system
572
+ // Use the shared whitelist from sdk-bridge (read-only tools + safe bash commands)
573
+ var whitelisted = sdk.checkToolWhitelist(toolName, input);
574
+ if (whitelisted) return Promise.resolve(whitelisted);
575
+ // Not whitelisted: route through the project session's permission system
577
576
  return new Promise(function (resolve) {
578
577
  var requestId = crypto.randomUUID();
579
578
  session.pendingPermissions[requestId] = {
package/lib/project.js CHANGED
@@ -293,6 +293,7 @@ function createProjectContext(opts) {
293
293
 
294
294
  // --- Browser extension state ---
295
295
  var _browserTabList = {}; // tabId -> { id, url, title, favIconUrl }
296
+ var _pendingDebateProposals = {}; // proposalId -> { resolve, briefData }
296
297
  var _extensionWs = null; // WebSocket of the client with the Chrome extension
297
298
  var _extToken = crypto.randomUUID(); // Auth token for MCP server bridge
298
299
  var pendingExtensionRequests = {}; // requestId -> { resolve, timer }
@@ -557,47 +558,72 @@ function createProjectContext(opts) {
557
558
  mateDisplayName: opts.mateDisplayName || "",
558
559
  isMate: isMate,
559
560
  dangerouslySkipPermissions: dangerouslySkipPermissions,
560
- mcpServers: isMate ? undefined : (function () {
561
+ mcpServers: (function () {
562
+ var servers = {};
563
+
564
+ // Debate MCP server (available to both mates and main project)
561
565
  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
- },
566
+ var debateMcp = require("./debate-mcp-server");
567
+ var debateMcpConfig = debateMcp.create(function onPropose(briefData) {
568
+ return new Promise(function (resolve) {
569
+ var proposalId = "dp_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
570
+ briefData.proposalId = proposalId;
571
+ _pendingDebateProposals[proposalId] = {
572
+ resolve: resolve,
573
+ briefData: briefData,
574
+ };
575
+ // The SDK sends tool_executing with briefData as input.
576
+ // Client renders the debate brief card when it sees propose_debate.
577
+ });
589
578
  });
590
- if (!mcpConfig) return undefined;
591
- var servers = {};
592
- servers[mcpConfig.name || "clay-browser"] = mcpConfig;
593
- return servers;
579
+ if (debateMcpConfig) servers[debateMcpConfig.name || "clay-debate"] = debateMcpConfig;
594
580
  } catch (e) {
595
- console.error("[project] Failed to create browser MCP server:", e.message);
596
- return undefined;
581
+ console.error("[project] Failed to create debate MCP server:", e.message);
597
582
  }
583
+
584
+ // Browser MCP server (main project only, not mates)
585
+ if (!isMate) {
586
+ try {
587
+ var browserMcp = require("./browser-mcp-server");
588
+ var mcpConfig = browserMcp.create(sendExtensionCommandAny, function () {
589
+ return Object.values(_browserTabList || {});
590
+ }, {
591
+ watchTab: function (tabId) {
592
+ var key = "tab:" + tabId;
593
+ var active = loadContextSources(slug);
594
+ if (active.indexOf(key) === -1) {
595
+ active.push(key);
596
+ saveContextSources(slug, active);
597
+ var _msg = JSON.stringify({ type: "context_sources_state", active: active });
598
+ for (var c of clients) { if (c.readyState === 1) c.send(_msg); }
599
+ }
600
+ return active;
601
+ },
602
+ unwatchTab: function (tabId) {
603
+ var key = "tab:" + tabId;
604
+ var active = loadContextSources(slug);
605
+ var idx = active.indexOf(key);
606
+ if (idx !== -1) {
607
+ active.splice(idx, 1);
608
+ saveContextSources(slug, active);
609
+ var _msg = JSON.stringify({ type: "context_sources_state", active: active });
610
+ for (var c of clients) { if (c.readyState === 1) c.send(_msg); }
611
+ }
612
+ return active;
613
+ },
614
+ });
615
+ if (mcpConfig) servers[mcpConfig.name || "clay-browser"] = mcpConfig;
616
+ } catch (e) {
617
+ console.error("[project] Failed to create browser MCP server:", e.message);
618
+ }
619
+ }
620
+
621
+ return Object.keys(servers).length > 0 ? servers : undefined;
598
622
  })(),
599
623
  onProcessingChanged: onProcessingChanged,
600
- onTurnDone: isMate ? function (session, preview) { digestDmTurn(session, preview); } : null,
624
+ onTurnDone: isMate ? function (session, preview) {
625
+ digestDmTurn(session, preview);
626
+ } : null,
601
627
  scheduleMessage: function (session, text, resetsAt) {
602
628
  scheduleMessage(session, text, resetsAt);
603
629
  },
@@ -1689,6 +1715,28 @@ function createProjectContext(opts) {
1689
1715
  handleDebateConfirmBrief(ws);
1690
1716
  return;
1691
1717
  }
1718
+ if (msg.type === "debate_proposal_response") {
1719
+ // Match the most recent pending proposal (proposalId may not be
1720
+ // available on the client since it's not part of the tool input)
1721
+ var _dpKeys = Object.keys(_pendingDebateProposals);
1722
+ if (_dpKeys.length === 0) return;
1723
+ var _dpKey = msg.proposalId || _dpKeys[_dpKeys.length - 1];
1724
+ var pending = _pendingDebateProposals[_dpKey];
1725
+ if (!pending) return;
1726
+ delete _pendingDebateProposals[_dpKey];
1727
+ if (msg.action === "start") {
1728
+ // Set up debate state on the session, then transition to live
1729
+ var _dpSession = getSessionForWs(ws);
1730
+ if (_dpSession) {
1731
+ var _dpMateId = isMate ? path.basename(cwd) : null;
1732
+ handleMcpDebateApproval(_dpSession, pending.briefData, _dpMateId, ws);
1733
+ }
1734
+ pending.resolve({ action: "start" });
1735
+ } else {
1736
+ pending.resolve({ action: "cancel" });
1737
+ }
1738
+ return;
1739
+ }
1692
1740
  if (msg.type === "debate_user_floor_response") {
1693
1741
  handleDebateUserFloorResponse(ws, msg);
1694
1742
  return;
@@ -4188,7 +4236,8 @@ function createProjectContext(opts) {
4188
4236
  data: screenshotData,
4189
4237
  file: screenshotName,
4190
4238
  tabTitle: tabLabel,
4191
- tabUrl: tabInfo ? tabInfo.url : ""
4239
+ tabUrl: tabInfo ? tabInfo.url : "",
4240
+ tabFavIconUrl: tabInfo ? tabInfo.favIconUrl : ""
4192
4241
  });
4193
4242
  parts.push("[Screenshot saved: " + screenshotPath + "]");
4194
4243
  }
@@ -4210,7 +4259,8 @@ function createProjectContext(opts) {
4210
4259
  }
4211
4260
 
4212
4261
  if (tabContextParts.length > 0) {
4213
- fullText = tabContextParts.join("\n\n---\n\n") + "\n\n" + fullText;
4262
+ fullText = "[The following browser tab data is automatically attached as context sources. Do NOT call browser_read_page, browser_console, browser_network, or browser_screenshot for these tabs — the data is already here.]\n\n" +
4263
+ tabContextParts.join("\n\n---\n\n") + "\n\n" + fullText;
4214
4264
  }
4215
4265
 
4216
4266
  // If screenshots were captured, send context preview cards and add to SDK images
@@ -4224,6 +4274,7 @@ function createProjectContext(opts) {
4224
4274
  tab: {
4225
4275
  title: ss.tabTitle || "",
4226
4276
  url: ss.tabUrl || "",
4277
+ favIconUrl: ss.tabFavIconUrl || "",
4227
4278
  screenshotFile: ss.file
4228
4279
  }
4229
4280
  };
@@ -4234,6 +4285,7 @@ function createProjectContext(opts) {
4234
4285
  tab: {
4235
4286
  title: ss.tabTitle || "",
4236
4287
  url: ss.tabUrl || "",
4288
+ favIconUrl: ss.tabFavIconUrl || "",
4237
4289
  screenshotUrl: "/p/" + slug + "/images/" + ss.file
4238
4290
  }
4239
4291
  });
@@ -4307,6 +4359,8 @@ function createProjectContext(opts) {
4307
4359
  var _debate = attachDebate({
4308
4360
  cwd: cwd,
4309
4361
  slug: slug,
4362
+ isMate: isMate,
4363
+ projectOwnerId: projectOwnerId,
4310
4364
  send: send,
4311
4365
  sendTo: sendTo,
4312
4366
  sendToSession: sendToSession,
@@ -4331,6 +4385,7 @@ function createProjectContext(opts) {
4331
4385
  var handleDebateUserFloorResponse = _debate.handleDebateUserFloorResponse;
4332
4386
  var restoreDebateState = _debate.restoreDebateState;
4333
4387
  var checkForDmDebateBrief = _debate.checkForDmDebateBrief;
4388
+ var handleMcpDebateApproval = _debate.handleMcpDebateApproval;
4334
4389
 
4335
4390
  // --- Session presence (who is viewing which session) ---
4336
4391
  function broadcastPresence() {
package/lib/public/app.js CHANGED
@@ -32,7 +32,7 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
32
32
  import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } from './modules/command-palette.js';
33
33
  import { initLongPress } from './modules/longpress.js';
34
34
  import { initMention, handleMentionStart, handleMentionStream, handleMentionDone, handleMentionError, handleMentionActivity, renderMentionUser, renderMentionResponse } from './modules/mention.js';
35
- import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateResumed, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, handleDebateEnded, handleDebateError, renderDebateStarted, renderDebateTurnDone, renderDebateEnded, renderDebateCommentInjected, renderDebateUserResume, openDebateModal, closeDebateModal, handleDebateBriefReady, renderDebateBriefReady, isDebateActive, resetDebateState, exportDebateAsPdf } from './modules/debate.js';
35
+ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateResumed, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, handleDebateEnded, handleDebateError, renderDebateStarted, renderDebateTurnDone, renderDebateEnded, renderDebateCommentInjected, renderDebateUserResume, openDebateModal, closeDebateModal, handleDebateBriefReady, renderDebateBriefReady, isDebateActive, resetDebateState, exportDebateAsPdf, renderMcpDebateProposal } from './modules/debate.js';
36
36
 
37
37
  // --- Base path for multi-project routing ---
38
38
  var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
@@ -4388,6 +4388,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4388
4388
  if (dhBar) dhBar.remove();
4389
4389
  var dbBadges = document.querySelectorAll(".debate-header-badge");
4390
4390
  for (var dbi = 0; dbi < dbBadges.length; dbi++) dbBadges[dbi].remove();
4391
+ // Clean up ended mode banner if debate is not active on this session
4392
+ if (debateEndedMode) exitDebateEndedMode();
4391
4393
  }
4392
4394
  scrollToBottom();
4393
4395
  // Scroll to tool element if navigating from file edit history
@@ -4741,7 +4743,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4741
4743
  header.className = "context-card-header";
4742
4744
  var icon = document.createElement("span");
4743
4745
  icon.className = "context-card-icon";
4744
- icon.textContent = "\uD83D\uDC41";
4746
+ icon.innerHTML = iconHtml("globe");
4745
4747
  header.appendChild(icon);
4746
4748
  var label = document.createElement("span");
4747
4749
  label.textContent = "Viewing tab";
@@ -4765,6 +4767,15 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4765
4767
  if (tabTitle || tabDomain) {
4766
4768
  var meta = document.createElement("div");
4767
4769
  meta.className = "context-card-meta";
4770
+ if (msg.tab.favIconUrl) {
4771
+ var fav = document.createElement("img");
4772
+ fav.className = "context-card-favicon";
4773
+ fav.src = msg.tab.favIconUrl;
4774
+ fav.width = 14;
4775
+ fav.height = 14;
4776
+ fav.onerror = function () { this.style.display = "none"; };
4777
+ meta.appendChild(fav);
4778
+ }
4768
4779
  var titleEl = document.createElement("span");
4769
4780
  titleEl.className = "context-card-title";
4770
4781
  titleEl.textContent = tabTitle;
@@ -4838,6 +4849,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4838
4849
  }
4839
4850
  renderPlanBanner("exit");
4840
4851
  getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
4852
+ } else if (msg.name === "propose_debate" || (msg.name && msg.name.indexOf("propose_debate") !== -1)) {
4853
+ getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
4841
4854
  } else if (getTodoTools()[msg.name]) {
4842
4855
  getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
4843
4856
  } else {
@@ -4846,7 +4859,18 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4846
4859
  break;
4847
4860
 
4848
4861
  case "tool_executing":
4849
- if (msg.name === "AskUserQuestion" && msg.input && msg.input.questions) {
4862
+ if ((msg.name === "propose_debate" || (msg.name && msg.name.indexOf("propose_debate") !== -1)) && msg.input) {
4863
+ var _dpTool = getTools()[msg.id];
4864
+ if (_dpTool) {
4865
+ if (_dpTool.el) _dpTool.el.style.display = "none";
4866
+ _dpTool.done = true;
4867
+ _dpTool.hidden = true;
4868
+ removeToolFromGroup(msg.id);
4869
+ }
4870
+ finalizeAssistantBlock();
4871
+ renderMcpDebateProposal(msg.id, msg.input);
4872
+ startUrgentBlink();
4873
+ } else if (msg.name === "AskUserQuestion" && msg.input && msg.input.questions) {
4850
4874
  var askTool = getTools()[msg.id];
4851
4875
  if (askTool) {
4852
4876
  if (askTool.el) askTool.el.style.display = "none";
@@ -5902,7 +5926,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5902
5926
  // --- Debate module ---
5903
5927
  initDebate({
5904
5928
  get ws() { return ws; },
5929
+ sendWs: function (obj) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj)); },
5905
5930
  messagesEl: messagesEl,
5931
+ addToMessages: function (el) { addToMessages(el); },
5906
5932
  scrollToBottom: scrollToBottom,
5907
5933
  addCopyHandler: addCopyHandler,
5908
5934
  matesList: function () { return cachedMatesList || []; },
@@ -873,7 +873,8 @@
873
873
  .terminal-tab.active { color: var(--text); border-color: var(--accent); }
874
874
  .terminal-tab.exited { opacity: 0.5; }
875
875
 
876
- .terminal-tab-close {
876
+
877
+ .terminal-tab-more {
877
878
  display: none;
878
879
  align-items: center;
879
880
  justify-content: center;
@@ -888,9 +889,44 @@
888
889
  line-height: 1;
889
890
  }
890
891
 
891
- .terminal-tab:hover .terminal-tab-close,
892
- .terminal-tab.active .terminal-tab-close { display: flex; }
893
- .terminal-tab-close:hover { background: rgba(var(--overlay-rgb),0.1); color: var(--text); }
892
+ .terminal-tab:hover .terminal-tab-more,
893
+ .terminal-tab.active .terminal-tab-more { display: flex; }
894
+ .terminal-tab-more:hover { background: rgba(var(--overlay-rgb),0.1); color: var(--text); }
895
+
896
+ .terminal-tab-ctx {
897
+ position: fixed;
898
+ z-index: 9999;
899
+ min-width: 120px;
900
+ background: var(--bg-elevated, var(--bg));
901
+ border: 1px solid var(--border);
902
+ border-radius: 6px;
903
+ padding: 4px;
904
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
905
+ }
906
+
907
+ .terminal-tab-ctx-item {
908
+ display: flex;
909
+ align-items: center;
910
+ gap: 8px;
911
+ width: 100%;
912
+ padding: 6px 10px;
913
+ border: none;
914
+ border-radius: 4px;
915
+ background: transparent;
916
+ color: var(--text-secondary);
917
+ font-size: 12px;
918
+ cursor: pointer;
919
+ white-space: nowrap;
920
+ }
921
+
922
+ .terminal-tab-ctx-item:hover {
923
+ background: rgba(var(--overlay-rgb),0.08);
924
+ color: var(--text);
925
+ }
926
+
927
+ .terminal-tab-ctx-danger:hover {
928
+ color: #e74c3c;
929
+ }
894
930
 
895
931
  .terminal-tab-label { cursor: default; }
896
932
 
@@ -971,6 +1007,7 @@
971
1007
 
972
1008
  .terminal-tab-body .xterm {
973
1009
  height: 100%;
1010
+ overflow: hidden;
974
1011
  }
975
1012
 
976
1013
  /* --- Terminal key toolbar (mobile) --- */
@@ -197,8 +197,13 @@
197
197
  }
198
198
 
199
199
  .context-card-header .context-card-icon {
200
- font-size: 13px;
201
200
  opacity: 0.7;
201
+ display: inline-flex;
202
+ align-items: center;
203
+ }
204
+ .context-card-header .context-card-icon .lucide {
205
+ width: 14px;
206
+ height: 14px;
202
207
  }
203
208
 
204
209
  .context-card-screenshot {
@@ -226,10 +231,16 @@
226
231
 
227
232
  .context-card-meta {
228
233
  display: flex;
229
- justify-content: space-between;
230
- align-items: baseline;
234
+ align-items: center;
231
235
  margin-top: 8px;
232
- gap: 12px;
236
+ gap: 6px;
237
+ }
238
+ .context-card-favicon {
239
+ width: 14px;
240
+ height: 14px;
241
+ border-radius: 2px;
242
+ flex-shrink: 0;
243
+ object-fit: contain;
233
244
  }
234
245
 
235
246
  .context-card-title {
@@ -240,6 +251,8 @@
240
251
  text-overflow: ellipsis;
241
252
  flex: 1;
242
253
  min-width: 0;
254
+ flex: 1;
255
+ min-width: 0;
243
256
  }
244
257
 
245
258
  .context-card-domain {
@@ -2040,6 +2040,7 @@
2040
2040
  <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5/lib/xterm.min.js"></script>
2041
2041
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0/lib/addon-fit.min.js"></script>
2042
2042
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0/lib/addon-web-links.min.js"></script>
2043
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0/lib/addon-webgl.min.js"></script>
2043
2044
  <script type="module" src="app.js"></script>
2044
2045
  <div id="pwa-install-modal" class="pwa-modal hidden">
2045
2046
  <div class="pwa-modal-backdrop"></div>
@@ -690,8 +690,8 @@ export function renderDebateEnded(entry) {
690
690
  ctx.messagesEl.appendChild(statusLine);
691
691
  refreshIcons();
692
692
 
693
- // Show ended mode with native input area
694
- if (ctx.showDebateEndedMode) ctx.showDebateEndedMode(entry);
693
+ // During history replay, don't activate ended mode banner.
694
+ // The debate is already over; status line in messages is sufficient.
695
695
  }
696
696
 
697
697
  export function renderDebateCommentInjected(entry) {
@@ -1301,3 +1301,117 @@ function renderDebateBriefCard(msg, resolved) {
1301
1301
  refreshIcons();
1302
1302
  ctx.scrollToBottom();
1303
1303
  }
1304
+
1305
+ // --- MCP-based debate proposal card ---
1306
+ // Renders an inline card from the propose_debate MCP tool input.
1307
+ // The card reuses the same visual structure as renderDebateBriefCard
1308
+ // but sends debate_proposal_response instead of debate_confirm_brief.
1309
+
1310
+ export function renderMcpDebateProposal(toolId, input) {
1311
+ var panelists = [];
1312
+ try {
1313
+ panelists = typeof input.panelists === "string" ? JSON.parse(input.panelists) : (input.panelists || []);
1314
+ } catch (e) {
1315
+ panelists = [];
1316
+ }
1317
+
1318
+ var el = document.createElement("div");
1319
+ el.className = "debate-brief-card";
1320
+
1321
+ // Header
1322
+ var header = document.createElement("div");
1323
+ header.className = "debate-brief-card-header";
1324
+ header.innerHTML =
1325
+ '<span class="debate-brief-card-icon">' + iconHtml("message-circle") + '</span>' +
1326
+ '<span class="debate-brief-card-title">Debate Proposal</span>' +
1327
+ '<span class="debate-brief-card-chevron">' + iconHtml("chevron-down") + '</span>';
1328
+
1329
+ // Body
1330
+ var body = document.createElement("div");
1331
+ body.className = "debate-brief-card-body";
1332
+
1333
+ var topicHtml = '<div class="debate-brief-topic">' + escapeHtml(input.topic || "Untitled") + '</div>';
1334
+
1335
+ if (input.context) {
1336
+ topicHtml += '<div class="debate-brief-context">' + escapeHtml(input.context) + '</div>';
1337
+ }
1338
+
1339
+ // Resolve mate names from matesList
1340
+ var mates = ctx.matesList ? ctx.matesList() : [];
1341
+ var mateMap = {};
1342
+ for (var mi = 0; mi < mates.length; mi++) {
1343
+ mateMap[mates[mi].id] = mates[mi];
1344
+ }
1345
+
1346
+ topicHtml += '<div class="debate-brief-panelists-label">' + iconHtml("users") + ' <strong>Panelists:</strong></div>';
1347
+ topicHtml += '<div class="debate-brief-panelists">';
1348
+ for (var i = 0; i < panelists.length; i++) {
1349
+ var p = panelists[i];
1350
+ var mate = mateMap[p.mateId];
1351
+ var mateName = mate ? (mate.displayName || mate.name || p.mateId) : p.mateId;
1352
+ var avatarSrc = mate ? mateAvatarUrl(mate, 24) : "";
1353
+ topicHtml += '<div class="debate-brief-panelist" style="display:flex;align-items:center;gap:8px;">';
1354
+ if (avatarSrc) {
1355
+ topicHtml += '<img src="' + escapeHtml(avatarSrc) + '" width="24" height="24" style="border-radius:50%;flex-shrink:0;">';
1356
+ }
1357
+ topicHtml += '<span class="debate-brief-panelist-name">' + escapeHtml(mateName) + '</span>';
1358
+ if (p.role) {
1359
+ topicHtml += '<span class="debate-brief-panelist-role">' + escapeHtml(p.role) + '</span>';
1360
+ }
1361
+ topicHtml += '</div>';
1362
+ }
1363
+ topicHtml += '</div>';
1364
+
1365
+ if (input.specialRequests) {
1366
+ topicHtml += '<div class="debate-brief-special">' +
1367
+ iconHtml("info") + ' ' + escapeHtml(input.specialRequests) +
1368
+ '</div>';
1369
+ }
1370
+
1371
+ body.innerHTML = topicHtml;
1372
+
1373
+ // Actions
1374
+ var actions = document.createElement("div");
1375
+ actions.className = "debate-brief-actions";
1376
+
1377
+ var startBtn = document.createElement("button");
1378
+ startBtn.className = "debate-brief-start-btn";
1379
+ startBtn.innerHTML = iconHtml("play") + " Start Debate";
1380
+
1381
+ var cancelBtn = document.createElement("button");
1382
+ cancelBtn.className = "debate-brief-cancel-btn";
1383
+ cancelBtn.textContent = "Cancel";
1384
+
1385
+ startBtn.addEventListener("click", function () {
1386
+ if (ctx.sendWs) {
1387
+ ctx.sendWs({ type: "debate_proposal_response", proposalId: input.proposalId, action: "start" });
1388
+ }
1389
+ el.classList.add("resolved");
1390
+ actions.innerHTML = '<span class="debate-brief-resolved-label">' + iconHtml("check") + ' Starting debate...</span>';
1391
+ refreshIcons();
1392
+ });
1393
+
1394
+ cancelBtn.addEventListener("click", function () {
1395
+ if (ctx.sendWs) {
1396
+ ctx.sendWs({ type: "debate_proposal_response", proposalId: input.proposalId, action: "cancel" });
1397
+ }
1398
+ el.classList.add("resolved");
1399
+ actions.innerHTML = '<span class="debate-brief-resolved-label debate-brief-cancelled">' + iconHtml("x") + ' Cancelled</span>';
1400
+ refreshIcons();
1401
+ });
1402
+
1403
+ actions.appendChild(startBtn);
1404
+ actions.appendChild(cancelBtn);
1405
+
1406
+ // Collapse toggle
1407
+ header.addEventListener("click", function () {
1408
+ el.classList.toggle("collapsed");
1409
+ });
1410
+
1411
+ el.appendChild(header);
1412
+ el.appendChild(body);
1413
+ el.appendChild(actions);
1414
+ ctx.addToMessages(el);
1415
+ refreshIcons();
1416
+ ctx.scrollToBottom();
1417
+ }
@@ -352,6 +352,16 @@ function createXtermForTab(tab) {
352
352
 
353
353
  xterm.open(bodyEl);
354
354
 
355
+ // WebGL addon: pixel-perfect rendering (eliminates gaps in block characters)
356
+ // Must be loaded after xterm.open() so the rendering context is available.
357
+ if (typeof WebglAddon !== "undefined") {
358
+ try {
359
+ xterm.loadAddon(new WebglAddon.WebglAddon());
360
+ } catch (e) {
361
+ // WebGL not available, fall back to DOM renderer
362
+ }
363
+ }
364
+
355
365
  // Route input to server
356
366
  xterm.onData(function (data) {
357
367
  if (ctx.ws && ctx.connected) {
@@ -481,14 +491,14 @@ function renderTabBar() {
481
491
  startRenameTab(t, label);
482
492
  });
483
493
 
484
- var closeBtn = document.createElement("button");
485
- closeBtn.className = "terminal-tab-close";
486
- closeBtn.innerHTML = '<i data-lucide="trash-2" style="width:12px;height:12px"></i>';
487
- closeBtn.addEventListener("click", function (e) {
494
+ var moreBtn = document.createElement("button");
495
+ moreBtn.className = "terminal-tab-more";
496
+ moreBtn.innerHTML = '<i data-lucide="ellipsis" style="width:12px;height:12px"></i>';
497
+ moreBtn.addEventListener("click", function (e) {
488
498
  e.stopPropagation();
489
- closeTab(t.id);
499
+ showTabContextMenu(e, t, label);
490
500
  });
491
- el.appendChild(closeBtn);
501
+ el.appendChild(moreBtn);
492
502
 
493
503
  el.addEventListener("click", function () {
494
504
  if (t.id !== activeTabId) {
@@ -504,6 +514,52 @@ function renderTabBar() {
504
514
  refreshIcons();
505
515
  }
506
516
 
517
+ // --- Tab context menu (three-dot) ---
518
+ function showTabContextMenu(e, tab, labelEl) {
519
+ var existing = document.querySelector(".terminal-tab-ctx");
520
+ if (existing) existing.remove();
521
+
522
+ var menu = document.createElement("div");
523
+ menu.className = "terminal-tab-ctx";
524
+
525
+ var renameItem = document.createElement("button");
526
+ renameItem.className = "terminal-tab-ctx-item";
527
+ renameItem.innerHTML = '<i data-lucide="pencil" style="width:13px;height:13px"></i> Rename';
528
+ renameItem.addEventListener("click", function () {
529
+ menu.remove();
530
+ startRenameTab(tab, labelEl);
531
+ });
532
+ menu.appendChild(renameItem);
533
+
534
+ var closeItem = document.createElement("button");
535
+ closeItem.className = "terminal-tab-ctx-item terminal-tab-ctx-danger";
536
+ closeItem.innerHTML = '<i data-lucide="trash-2" style="width:13px;height:13px"></i> Close';
537
+ closeItem.addEventListener("click", function () {
538
+ menu.remove();
539
+ closeTab(tab.id);
540
+ });
541
+ menu.appendChild(closeItem);
542
+
543
+ document.body.appendChild(menu);
544
+ refreshIcons();
545
+
546
+ // Position near the button
547
+ var rect = e.currentTarget.getBoundingClientRect();
548
+ menu.style.top = (rect.bottom + 4) + "px";
549
+ menu.style.left = rect.left + "px";
550
+
551
+ // Dismiss on outside click
552
+ function dismiss(ev) {
553
+ if (!menu.contains(ev.target)) {
554
+ menu.remove();
555
+ document.removeEventListener("mousedown", dismiss, true);
556
+ }
557
+ }
558
+ setTimeout(function () {
559
+ document.addEventListener("mousedown", dismiss, true);
560
+ }, 0);
561
+ }
562
+
507
563
  // --- Rename tab inline ---
508
564
  function startRenameTab(tab, labelEl) {
509
565
  var input = document.createElement("input");
package/lib/sdk-bridge.js CHANGED
@@ -1567,24 +1567,15 @@ function createSDKBridge(opts) {
1567
1567
 
1568
1568
  // --- SDK query lifecycle ---
1569
1569
 
1570
- function handleCanUseTool(session, toolName, input, opts) {
1571
- // Ralph Loop execution: auto-approve all tools, deny interactive ones.
1572
- // Crafting sessions are interactive — user and Claude collaborate to build PROMPT.md / JUDGE.md.
1573
- if (session.loop && session.loop.active && session.loop.role !== "crafting") {
1574
- if (toolName === "AskUserQuestion") {
1575
- return Promise.resolve({ behavior: "deny", message: "Autonomous mode. Make your own decision." });
1576
- }
1577
- if (toolName === "EnterPlanMode") {
1578
- return Promise.resolve({ behavior: "deny", message: "Do not enter plan mode. Execute directly." });
1579
- }
1580
- return Promise.resolve({ behavior: "allow", updatedInput: input });
1581
- }
1582
-
1570
+ // Check if a tool should be auto-approved based on whitelist rules.
1571
+ // Returns { behavior: "allow", updatedInput } if whitelisted, or null if not.
1572
+ // Shared by handleCanUseTool and mate mention canUseTool handlers.
1573
+ function checkToolWhitelist(toolName, input) {
1583
1574
  // Auto-approve read-only tools for ALL sessions.
1584
1575
  // These tools only inspect files and fetch data — no side effects.
1585
1576
  var readOnlyTools = { Read: true, Glob: true, Grep: true, WebFetch: true, WebSearch: true };
1586
1577
  if (readOnlyTools[toolName]) {
1587
- return Promise.resolve({ behavior: "allow", updatedInput: input });
1578
+ return { behavior: "allow", updatedInput: input };
1588
1579
  }
1589
1580
 
1590
1581
  // Auto-approve safe browser MCP tools.
@@ -1595,10 +1586,17 @@ function createSDKBridge(opts) {
1595
1586
  if (toolName.indexOf("mcp__") === 0 && toolName.indexOf("__browser_") !== -1) {
1596
1587
  var mcpToolName = toolName.substring(toolName.lastIndexOf("__") + 2);
1597
1588
  if (safeBrowserTools[mcpToolName]) {
1598
- return Promise.resolve({ behavior: "allow", updatedInput: input });
1589
+ return { behavior: "allow", updatedInput: input };
1599
1590
  }
1600
1591
  }
1601
1592
 
1593
+ // Auto-approve debate MCP tools (propose_debate).
1594
+ // These are user-facing tools that show inline approval cards,
1595
+ // so the permission prompt is redundant.
1596
+ if (toolName.indexOf("mcp__clay-debate__") === 0) {
1597
+ return { behavior: "allow", updatedInput: input };
1598
+ }
1599
+
1602
1600
  // Auto-approve safe Bash commands (read-only, non-destructive)
1603
1601
  // Applies to ALL sessions (mates and regular projects alike).
1604
1602
  // These are purely read-only commands that cannot modify files, install
@@ -1663,10 +1661,32 @@ function createSDKBridge(opts) {
1663
1661
  if (!safeBashCommands[firstWord]) { allSafe = false; break; }
1664
1662
  }
1665
1663
  if (allSafe) {
1666
- return Promise.resolve({ behavior: "allow", updatedInput: input });
1664
+ return { behavior: "allow", updatedInput: input };
1667
1665
  }
1668
1666
  }
1669
1667
 
1668
+ return null; // Not whitelisted
1669
+ }
1670
+
1671
+ function handleCanUseTool(session, toolName, input, opts) {
1672
+ // Ralph Loop execution: auto-approve all tools, deny interactive ones.
1673
+ // Crafting sessions are interactive — user and Claude collaborate to build PROMPT.md / JUDGE.md.
1674
+ if (session.loop && session.loop.active && session.loop.role !== "crafting") {
1675
+ if (toolName === "AskUserQuestion") {
1676
+ return Promise.resolve({ behavior: "deny", message: "Autonomous mode. Make your own decision." });
1677
+ }
1678
+ if (toolName === "EnterPlanMode") {
1679
+ return Promise.resolve({ behavior: "deny", message: "Do not enter plan mode. Execute directly." });
1680
+ }
1681
+ return Promise.resolve({ behavior: "allow", updatedInput: input });
1682
+ }
1683
+
1684
+ // Check shared whitelist (read-only tools, safe browser tools, safe bash commands)
1685
+ var whitelisted = checkToolWhitelist(toolName, input);
1686
+ if (whitelisted) {
1687
+ return Promise.resolve(whitelisted);
1688
+ }
1689
+
1670
1690
  // AskUserQuestion: wait for user answers via WebSocket
1671
1691
  if (toolName === "AskUserQuestion") {
1672
1692
  return new Promise(function(resolve) {
@@ -2529,6 +2549,7 @@ function createSDKBridge(opts) {
2529
2549
  return {
2530
2550
  createMessageQueue: createMessageQueue,
2531
2551
  processSDKMessage: processSDKMessage,
2552
+ checkToolWhitelist: checkToolWhitelist,
2532
2553
  handleCanUseTool: handleCanUseTool,
2533
2554
  handleElicitation: handleElicitation,
2534
2555
  processQueryStream: processQueryStream,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.26.0-beta.17",
3
+ "version": "2.26.0-beta.19",
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",