-
Notifications
You must be signed in to change notification settings - Fork 52
Add initial MCP version with SDK imports #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Caution Review failedThe pull request is closed. WalkthroughAdds MCP integration to AI Chat: new MCP SDK bundle and metadata, config/registry/tool-adapter/meta-tools, per‑turn ToolSurfaceProvider and ToolNameMap, AgentNodes and UI wiring for lane-based routing, tests, and GRD/build list updates. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant User
participant ChatView
participant AgentNode
participant ToolSurfaceProvider
participant MCPRegistry
participant ToolExecutor
participant ToolRegistry
User->>ChatView: submit message
ChatView->>AgentNode: createAgentNode(state)
AgentNode->>ToolSurfaceProvider: select(state, baseTools, opts)
ToolSurfaceProvider->>MCPRegistry: getStatus()
alt MCP disabled
ToolSurfaceProvider-->>AgentNode: return capped baseTools
else MCP enabled
ToolSurfaceProvider->>ToolRegistry: resolve MCP tools
ToolSurfaceProvider-->>AgentNode: return selected tools + selectedNames
end
AgentNode->>AgentNode: persist selectedNames in state.context
AgentNode->>ToolExecutor: execute with selected tools (apiKey forwarded)
ToolExecutor->>ToolRegistry: getRegisteredTool(name)
ToolExecutor-->>User: emit TOOL_RESULT / MODEL messages (uiLane set)
sequenceDiagram
autonumber
participant Settings as SettingsDialog
participant Panel as AIChatPanel
participant MCPConfig as MCPConfig
participant Registry as MCPRegistry
Settings->>MCPConfig: setMCPConfig(values)
MCPConfig-->>Settings: dispatch config-changed event
MCPConfig-->>Panel: window event
Panel->>MCPConfig: getMCPConfig()
alt enabled && autostart
Panel->>Registry: init()
Panel->>Registry: refresh()
Registry-->>Panel: status
else
Panel->>Registry: dispose()
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (2)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Codex Review: Here are some suggestions.
Reply with @codex fix comments to fix any unresolved comments.
About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you open a pull request for review, mark a draft as ready, or comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex fix this CI failure" or "@codex address that feedback".
| async execute(args: Record<string, unknown>): Promise<unknown> { | ||
| const sanitized = this.sanitize(args); | ||
| logger.info('Executing MCP tool', { name: this.name, serverId: this.serverId }); | ||
| return this.client.callTool(this.serverId, this.def.name, sanitized, { timeoutMs: 30000 }); | ||
| } | ||
|
|
||
| // Expose metadata for discovery/search | ||
| getServerId(): string { return this.serverId; } | ||
| getOriginalToolName(): string { return this.def.name; } | ||
|
|
||
| private sanitize(input: Record<string, unknown>): Record<string, unknown> { | ||
| const sensitive = ['token', 'api_key', 'password', 'secret', 'authorization']; | ||
| const out: Record<string, unknown> = {}; | ||
| for (const [k, v] of Object.entries(input || {})) { | ||
| if (typeof v === 'string' && sensitive.some(s => k.toLowerCase().includes(s))) { | ||
| out[k] = '[redacted]'; | ||
| } else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P1] Stop redacting tool arguments before executing MCP tools
In MCPToolAdapter.execute the code sanitizes the caller‑supplied args and then passes that sanitized object to client.callTool. Any parameter whose key contains token, api_key, password, etc. is replaced with '[redacted]', so tools that require those values will always receive placeholders instead of the real secrets and will fail to authenticate. The log line should use the sanitized copy, but the RPC call must be made with the original arguments.
Useful? React with 👍 / 👎.
| describe('connects to local Hacker News MCP server via SDK', function() { | ||
| this.timeout(30000); // Longer timeout for integration test | ||
|
|
||
| const HACKER_NEWS_SERVER: MCPServer = { | ||
| id: 'local-hn-sdk', | ||
| endpoint: 'http://localhost:5001/sse', | ||
| }; | ||
|
|
||
| it('connects and lists tools', async function() { | ||
| console.log('=== Testing Local Hacker News MCP Server with SDK ==='); | ||
|
|
||
| try { | ||
| // Step 1: Connect | ||
| console.log('Step 1: Connecting to MCP server via SDK...'); | ||
| await client.connect(HACKER_NEWS_SERVER); | ||
| assert.strictEqual(client.isConnected(HACKER_NEWS_SERVER.id), true, 'Should be connected'); | ||
| console.log('✓ Connection established via SDK'); | ||
|
|
||
| // Step 2: List tools | ||
| console.log('Step 2: Listing available tools via SDK...'); | ||
| const tools = await client.listTools(HACKER_NEWS_SERVER.id); | ||
| console.log(`✓ Retrieved ${tools.length} tools via SDK`); | ||
|
|
||
| // Verify tools structure | ||
| assert.ok(Array.isArray(tools), 'Tools should be an array'); | ||
| if (tools.length > 0) { | ||
| const tool = tools[0]; | ||
| assert.ok(tool.hasOwnProperty('name'), 'Tool should have name'); | ||
| assert.ok(tool.hasOwnProperty('description'), 'Tool should have description'); | ||
| assert.ok(tool.hasOwnProperty('inputSchema'), 'Tool should have inputSchema'); | ||
| console.log(`✓ Tool structure valid: ${tool.name}`); | ||
| } | ||
|
|
||
| // Step 3: Call a tool (if available) | ||
| if (tools.length > 0) { | ||
| const firstTool = tools[0]; | ||
| console.log(`Step 3: Testing tool call: ${firstTool.name}`); | ||
|
|
||
| try { | ||
| // Create minimal args for the tool | ||
| const args: Record<string, unknown> = {}; | ||
|
|
||
| // Add required parameters if the tool has them | ||
| if (firstTool.inputSchema && typeof firstTool.inputSchema === 'object') { | ||
| const schema = firstTool.inputSchema as any; | ||
| if (schema.required && Array.isArray(schema.required)) { | ||
| for (const reqParam of schema.required) { | ||
| if (reqParam === 'count' || reqParam.includes('count')) { | ||
| args[reqParam] = 3; // Use small count for testing | ||
| } else if (reqParam === 'item_id') { | ||
| args[reqParam] = 8863; // Classic HN item | ||
| } else if (reqParam === 'username') { | ||
| args[reqParam] = 'pg'; // Paul Graham | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| console.log(`Calling ${firstTool.name} with args:`, args); | ||
| const result = await client.callTool(HACKER_NEWS_SERVER.id, firstTool.name, args); | ||
|
|
||
| console.log('Tool call result via SDK:', typeof result); | ||
| assert.ok(result !== undefined, 'Tool call should return a result'); | ||
| console.log('✓ Tool call successful via SDK'); | ||
|
|
||
| } catch (err) { | ||
| console.error(`Tool call failed for ${firstTool.name}:`, err); | ||
| // Don't fail the test if tool call fails - might be expected with test data | ||
| console.log('Tool call failed, but connection and tools/list worked'); | ||
| } | ||
| } | ||
|
|
||
| console.log('✓ All SDK tests passed!'); | ||
|
|
||
| } catch (error) { | ||
| console.error('SDK test failed:', error); | ||
| throw error; | ||
| } finally { | ||
| // Cleanup | ||
| client.disconnect(HACKER_NEWS_SERVER.id); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P1] Remove integration test that depends on a local MCP server
The new MCPClientSDK.test.ts adds a test case that tries to connect to http://localhost:5001/sse and exercise real tool calls. This test is included in the unit‑test target and will run in CI where no MCP server is listening, causing the suite to hang or fail. Integration coverage belongs in a separate gated test; unit tests should not rely on external services.
Useful? React with 👍 / 👎.
| // Select tools based on MCP mode (all/router/meta) | ||
| const baseTools = getAgentToolsFromState(state); | ||
| const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 }); | ||
| // Persist selection in context so ToolExecutorNode can resolve the same set | ||
| if (!state.context) { (state as any).context = {}; } | ||
| (state.context as any).selectedToolNames = selection.selectedNames; | ||
| const tools = selection.tools; | ||
|
|
||
| // Convert ChatMessage[] to LLMMessage[] | ||
| const llmMessages = this.convertChatMessagesToLLMMessages(state.messages); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P1] Sanitize MCP tool names before sending them to LLM function calls
When registering MCP tools their original names (e.g. mcp:default:alpha) are forwarded directly in the tools array passed to llm.call. Providers such as OpenAI require function names to match ^[a-zA-Z0-9_-]{1,64}$; colons and other characters are rejected, so any call that includes an MCP tool will fail before the model executes. The tool names should be converted to a provider‑safe representation and a mapping kept so the executor can resolve the original tool.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 19
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts (1)
196-199: Avoid re-creating tool instances; cache or reuse registered instanceCreating a new instance on every lookup breaks stateful tools (e.g., MCP connections) and wastes resources. Prefer returning a cached/registered instance, falling back to factory once and caching it.
- static getToolInstance(name: string): Tool<any, any> | null { - const factory = this.toolFactories.get(name); - return factory ? factory() : null; - } + static getToolInstance(name: string): Tool<any, any> | null { + const existing = this.registeredTools.get(name); + if (existing) { + return existing; + } + const factory = this.toolFactories.get(name); + if (!factory) return null; + const instance = factory(); + this.registeredTools.set(name, instance); + return instance; + }front_end/panels/ai_chat/BUILD.gn (1)
296-312: Mock network calls and pin fixtures in MCPClientSDK tests
MCPClientSDK.test.ts invokes a real HTTP endpoint (http://localhost:5001/sseat line 58); replace this with ajest.mockor bundled in-repo fixture so tests run reliably in CI. ToolSurfaceProvider tests contain no external network calls and need no changes.front_end/panels/ai_chat/ui/AIChatPanel.ts (5)
78-95: Move CSS and MCP imports to top; dedupe MCPConfig import.Fixes import/first, import/order, import/no-duplicates.
-import chatViewStyles from './chatView.css.js'; +// (Move up near other imports at file top) +import chatViewStyles from './chatView.css.js'; @@ -// MCP integration -import { MCPRegistry } from '../mcp/MCPRegistry.js'; -import { getMCPConfig } from '../mcp/MCPConfig.js'; -import { onMCPConfigChange } from '../mcp/MCPConfig.js'; +// MCP integration +import { MCPRegistry } from '../mcp/MCPRegistry.js'; +import { getMCPConfig, onMCPConfigChange } from '../mcp/MCPConfig.js';
1790-1793: Fix event listener leak and call MCP unsubscribe on hide.
- removeEventListener with a fresh bind never removes the original listener.
- Store a single bound handler; use it for add/remove.
- Unsubscribe MCP listener.
override willHide(): void { - // Explicitly remove any event listeners to prevent memory leaks - this.#agentService.removeEventListener(AgentEvents.MESSAGES_CHANGED, this.#handleMessagesChanged.bind(this)); + // Explicitly remove listeners to prevent memory leaks + this.#agentService.removeEventListener(AgentEvents.MESSAGES_CHANGED, this.#boundMessagesChanged); + if (this.#mcpUnsubscribe) { + this.#mcpUnsubscribe(); + this.#mcpUnsubscribe = null; + } }Also add a single bound handler field and use it in initialize:
@@ #agentService = AgentService.getInstance(); + #boundMessagesChanged = this.#handleMessagesChanged.bind(this); @@ - // Remove any existing listeners to prevent duplicates - this.#agentService.removeEventListener(AgentEvents.MESSAGES_CHANGED, this.#handleMessagesChanged.bind(this)); + // Remove any existing listeners to prevent duplicates + this.#agentService.removeEventListener(AgentEvents.MESSAGES_CHANGED, this.#boundMessagesChanged); @@ - this.#agentService.addEventListener(AgentEvents.MESSAGES_CHANGED, this.#handleMessagesChanged.bind(this)); + this.#agentService.addEventListener(AgentEvents.MESSAGES_CHANGED, this.#boundMessagesChanged);
730-733: Do not install StorageMonitor in production; it logs secrets.Overriding localStorage and logging values/length/prefix risks secret leakage. Gate with an explicit debug flag.
- // Initialize storage monitoring for debugging - StorageMonitor.getInstance(); + // Initialize storage monitoring only when explicitly enabled for debugging + if (localStorage.getItem('ai_chat_debug_logging') === '1') { + StorageMonitor.getInstance(); + }
49-69: Redact sensitive values from StorageMonitor logs.Avoid logging any portion of secrets; keep only key and timestamp.
- logger.debug(`Value exists: ${!!value}`); - logger.debug(`Value length: ${value?.length || 0}`); - logger.debug(`Value preview: ${value?.substring(0, 50) + (value?.length > 50 ? '...' : '') || 'null'}`); + logger.debug(`Value exists: ${Boolean(value)}`); + logger.debug(`(value content redacted)`); @@ - if (key.includes('openrouter') || key.includes('ai_chat')) { + if (key.includes('openrouter') || key.includes('ai_chat')) { logger.debug(`=== LOCALSTORAGE REMOVE ===`); logger.debug(`Key: ${key}`); logger.debug(`Timestamp: ${new Date().toISOString()}`); }
1463-1467: Stop logging API key prefixes.Even partial prefixes can leak secrets in logs.
- logger.info('Retrieved API key:'); - logger.info('- Exists:', !!apiKey); - logger.info('- Length:', apiKey?.length || 0); - logger.info('- Prefix:', apiKey?.substring(0, 8) + '...' || 'none'); + logger.info('Retrieved API key:'); + logger.info('- Exists:', !!apiKey); + logger.info('- Length:', apiKey?.length || 0);front_end/panels/ai_chat/mcp/MCPRegistry.ts (1)
1-154: Add license header.The file is missing the required license header.
Add the appropriate license header at the beginning of the file. The exact format should match other files in the codebase.
♻️ Duplicate comments (3)
front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (1)
32-36: Don’t send redacted args to the MCP server (use redaction only for logs).You’re redacting secrets and then passing the redacted payload to callTool, which will break auth for tools that require these fields. Keep a sanitized copy for logging, but send the original args to the client.
async execute(args: Record<string, unknown>): Promise<unknown> { - const sanitized = this.sanitize(args); - logger.info('Executing MCP tool', { name: this.name, serverId: this.serverId }); - return this.client.callTool(this.serverId, this.def.name, sanitized, { timeoutMs: 30000 }); + const sanitized = this.sanitize(args); + logger.info('Executing MCP tool', { name: this.name, serverId: this.serverId, args: sanitized }); + return this.client.callTool(this.serverId, this.def.name, args, { timeoutMs: 30000 }); }front_end/panels/ai_chat/mcp/MCPClientSDK.test.ts (1)
53-55: Skip the local integration test in unit runs.This block requires a running local server and will fail/hang CI. Mark it skipped by default (enable later behind an env or separate suite).
-describe('connects to local Hacker News MCP server via SDK', function() { +describe.skip('connects to local Hacker News MCP server via SDK', function() { this.timeout(30000); // Longer timeout for integration testfront_end/panels/ai_chat/core/AgentNodes.ts (1)
150-157: Blocker: sanitize tool names for LLM function calls and reverse-map in executor (colons/dots will be rejected).You're passing raw tool.name values (e.g., mcp:server:tool and mcp.search) directly to providers. OpenAI/Anthropic/Google enforce function-name regex like ^[a-zA-Z0-9_-]{1,64}$, so “:” and “.” will 400 before a token is generated. Add a provider-safe mapping when building the tools array and persist a reverse map in state.context so ToolExecutorNode can resolve the original name. This also addresses the prior P1 review about this exact issue.
Apply these focused patches:
- Create selection + mapping, persist reverse map:
- const baseTools = getAgentToolsFromState(state); - const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 }); - // Persist selection in context so ToolExecutorNode can resolve the same set - if (!state.context) { (state as any).context = {}; } - (state.context as any).selectedToolNames = selection.selectedNames; - const tools = selection.tools; + const baseTools = getAgentToolsFromState(state); + const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 }); + // Persist selection and a reverse mapping from safe->original + if (!state.context) { (state as any).context = {}; } + (state.context as any).selectedToolNames = selection.selectedNames; + const toSafe = (n: string) => n.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64); + const safeMap: Record<string, string> = {}; + const safeTools = selection.tools.map(t => { + const safe = toSafe(t.name); + safeMap[safe] = t.name; + return { tool: t, safe }; + }); + (state.context as any).toolNameMap = safeMap; + const tools = selection.tools;
- Use safe names in tools payload to the provider:
- tools: tools.map(tool => ({ + tools: safeTools.map(({ tool, safe }) => ({ type: 'function', function: { - name: tool.name, + name: safe, description: tool.description, parameters: tool.schema, } })),
- Map back from safe name in ToolExecutorNode before lookup:
- // Get tool details from the ModelChatMessage - const toolName = lastMessage.toolName; + // Map back from provider-safe name to original registry name (if applicable) + const safeToOriginal = ((state.context as any)?.toolNameMap ?? {}) as Record<string, string>; + const toolName = safeToOriginal[lastMessage.toolName] ?? lastMessage.toolName;Also applies to: 176-185, 263-271, 464-471
🧹 Nitpick comments (53)
front_end/panels/ai_chat/ui/ChatView.ts (1)
1783-1789: Update remaining title attributes and add aria-label for “View Full Screen”
Two instances infront_end/panels/ai_chat/ui/ChatView.ts(lines 1786 and 1804) still usetitle="Open full report in document viewer". Replace both with:- title="Open full report in document viewer"> + title="Open full screen in document viewer" + aria-label="Open full screen in document viewer">front_end/third_party/mcp-sdk/ajv/.runkit_example.js (4)
16-17: Guard example invocations to avoid side effects on import.Wrap the demo calls so they only run when executed directly.
- test({foo: "abc", bar: 2}) - test({foo: 2, bar: 4}) +if (typeof require !== "undefined" && require.main === module) { + test({foo: "abc", bar: 2}) + test({foo: 2, bar: 4}) +}
1-2: Clarify module intent: ESM in front_end or mark as Node-only.If this ever gets included in front_end builds, prefer ESM import; otherwise, rename to .cjs or keep excluded.
ESM option:
-const Ajv = require("ajv") -const ajv = new Ajv({allErrors: true}) +import Ajv from "ajv" +const ajv = new Ajv({ allErrors: true })
21-22: Improve error readability.Multi-line errors are easier to scan.
- else console.log("Invalid: " + ajv.errorsText(validate.errors)) + else console.log("Invalid:\n" + ajv.errorsText(validate.errors, { separator: "\n" }))
1-23: Exclude dev-only RunKit example from production bundlesNo references to
.runkit_example.jswere found in GRD/GN manifests, but ensure it’s never picked up by your shipping build—either add it to your bundler/package exclusion list or relocate it under adocs/examplesfolder.front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts (2)
173-191: Overwriting factories/instances may leak resources; dispose old instances if presentWhen overwriting, gracefully dispose prior instance if it exposes a dispose() method.
static registerToolFactory(name: string, factory: () => Tool<any, any>): void { if (this.toolFactories.has(name)) { logger.warn(`Tool factory already registered for: ${name}. Overwriting.`); } - if (this.registeredTools.has(name)) { + if (this.registeredTools.has(name)) { logger.warn(`Tool instance already registered for: ${name}. Overwriting.`); + const oldInstance = this.registeredTools.get(name) as any; + if (oldInstance && typeof oldInstance.dispose === 'function') { + try { oldInstance.dispose(); } catch (e) { logger.warn('Error disposing previous tool instance:', e); } + } } this.toolFactories.set(name, factory);
313-317: Surface missing tools to aid diagnosisSilent drops make debugging hard. Log missing tool names once.
- private getToolInstances(): Array<Tool<any, any>> { - return this.config.tools - .map(toolName => ToolRegistry.getToolInstance(toolName)) - .filter((tool): tool is Tool<any, any> => tool !== null); - } + private getToolInstances(): Array<Tool<any, any>> { + const instances: Tool<any, any>[] = []; + for (const toolName of this.config.tools) { + const tool = ToolRegistry.getToolInstance(toolName); + if (!tool) { + logger.warn(`Tool not found: ${toolName}`); + continue; + } + instances.push(tool); + } + return instances; + }front_end/third_party/mcp-sdk/README.chromium (3)
1-9: Add provenance details and checksumsInclude tarball SHAs (and optionally commit/tag) to strengthen supply-chain traceability.
Name: Model Context Protocol SDK Short Name: mcp-sdk URL: https://github.com/modelcontextprotocol/typescript-sdk Version: 1.12.3 License: MIT License File: LICENSE Security Critical: no Shipped: yes +Source: npm (@modelcontextprotocol/sdk) +Sha256 (sdk tarball): <fill-in>
17-20: Harden update instructionsVerify tarballs and extract into explicit paths to avoid accidental tree pollution.
-To update this package: -wget -qO- https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.3.tgz | tar xzf - -wget -qO- https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz | tar xzf - +To update this package: +curl -fsSL https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.3.tgz -o /tmp/mcp-sdk.tgz +sha256sum /tmp/mcp-sdk.tgz # verify against the SHA above +mkdir -p front_end/third_party/mcp-sdk && tar -xzf /tmp/mcp-sdk.tgz -C front_end/third_party/mcp-sdk +curl -fsSL https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz -o /tmp/eventsource-parser.tgz +sha256sum /tmp/eventsource-parser.tgz # verify +tar -xzf /tmp/eventsource-parser.tgz -C front_end/third_party/mcp-sdk
21-24: Local modifications: add clarity on wrapper entrypoint and build targetsReference the exact entry (e.g., mcp-sdk.ts) and GN target that consumes it, to streamline future updates.
Local Modifications: - Created mcp-sdk.ts wrapper for DevTools integration - Removed Node.js-specific code paths -- Modified imports to use DevTools module system +- Modified imports to use DevTools module system +- Entry point consumed by GN target: //front_end/third_party/mcp-sdk:mcp_sdkfront_end/third_party/mcp-sdk/ajv/README.md (1)
1-208: CI: Exclude front_end/third_party/ from markdownlint and docs QA**
- Prevent noisy CI failures on this vendor README’s markdown nits and external image links.
- If this README is ever rendered in-product, ensure remote images aren’t fetched.
front_end/panels/ai_chat/mcp/MCPConfig.ts (6)
33-36: Validate allowlist shapeEnsure parsed JSON is string[]; otherwise ignore.
- if (raw) { - try { toolAllowlist = JSON.parse(raw); } catch { toolAllowlist = undefined; } - } + if (raw) { + try { + const parsed = JSON.parse(raw); + toolAllowlist = Array.isArray(parsed) && parsed.every(x => typeof x === 'string') ? parsed : undefined; + } catch { toolAllowlist = undefined; } + }
51-71: Support clearing fields (remove storage keys when undefined/null)Today undefined skips writes, making some values “sticky”. Explicitly remove keys so users can clear values in Settings.
- if (config.endpoint !== undefined) { - localStorage.setItem(KEYS.endpoint, config.endpoint); - } + if (config.endpoint !== undefined) { + config.endpoint ? localStorage.setItem(KEYS.endpoint, config.endpoint) + : localStorage.removeItem(KEYS.endpoint); + } - if (config.toolAllowlist) { + if (config.toolAllowlist) { localStorage.setItem(KEYS.allowlist, JSON.stringify(config.toolAllowlist)); } + if (config.toolAllowlist === undefined) { + localStorage.removeItem(KEYS.allowlist); + } - if (config.toolMode !== undefined) { - localStorage.setItem(KEYS.toolMode, config.toolMode); - } + if (config.toolMode !== undefined) { + localStorage.setItem(KEYS.toolMode, config.toolMode); + } else { + localStorage.removeItem(KEYS.toolMode); + }
83-87: Annotate local callback type to satisfy strict return-type lintKeeps eslint happy without changing behavior.
-export function onMCPConfigChange(handler: () => void): () => void { - const cb = () => handler(); +export function onMCPConfigChange(handler: () => void): () => void { + const cb: () => void = () => handler();
27-46: Guard against localStorage unavailabilitySome embeddings can throw on access (privacy settings). You already catch; consider returning full defaults to avoid partial shape.
Return a fully populated default object on error:
- logger.error('Failed to load MCP config', err); - return { enabled: false }; + logger.error('Failed to load MCP config', err); + return { enabled: false, toolMode: 'router', maxToolsPerTurn: 20, maxMcpPerTurn: 8, autostart: false };
48-77: Emit change event only when values actually change (optional)Debounce/throttle or diff old vs new to avoid redundant refresh work downstream.
1-96: Security follow-up: can we avoid storing tokens entirely?If UI needs a token, consider:
- OAuth + token kept in memory only
- Short-lived ephemeral sessions
- Using chrome.identity/devtools APIs
I can propose an end-to-end change removing token persistence from SettingsDialog and flowing tokens via in-memory singleton. Want a patch?
config/gni/devtools_grd_files.gni (1)
846-856: Ordering nit: keep third_party entries alphabetizedMinor: ensure mcp-sdk block stays alphabetically ordered with nearby third_party sections to reduce future conflicts.
front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts (1)
22-22: Fix import ordering to satisfy lint rulesPlace ../../mcp/MCPMetaTools.js with other local imports in sorted order (before ../../tools/* as per rule).
-import type { Tool } from '../../tools/Tools.js'; -import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js'; +import type { Tool } from '../../tools/Tools.js'; +import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js';If your lint requires grouping then alphabetical within group, move the MCP import to the top of the local (../../) block.
front_end/panels/ai_chat/core/ToolNameMapping.test.ts (1)
71-79: Lint cleanups: trailing space and assert helper.
- Remove trailing whitespace.
- Use assert.isOk per rulesdir/prefer-assert-is-ok.
- const result = await node.invoke(state); - + const result = await node.invoke(state); @@ - assert.ok((last as any).resultText); + assert.isOk((last as {resultText: unknown}).resultText);front_end/panels/ai_chat/core/AgentNodes.test.ts (2)
112-124: Type-safe extraction and preferred boolean assertion.- const resultText = (toolResultMessage as any).resultText; + const resultText = (toolResultMessage as {resultText: string}).resultText; @@ - assert.strictEqual(resultText.includes('test-session-123'), false); + assert.isFalse(resultText.includes('test-session-123'));
193-203: Avoid any; remove trailing spaces; ensure EOF newline.- const resultText = (toolResultMessage as any).resultText; - + const resultText = (toolResultMessage as {resultText: string}).resultText; +front_end/panels/ai_chat/ui/AIChatPanel.ts (3)
776-800: Add explicit return type; ensure safe re-init on config changes.
- Add ": void" to satisfy explicit-function-return-type.
- Keep try/catch; good.
- #setupMCPIntegration(): void { + #setupMCPIntegration(): void { const initAndRefresh = async () => { try { const mcpConfig = getMCPConfig(); // Only auto-connect if both enabled and autostart are true if (mcpConfig.enabled && mcpConfig.autostart) { await MCPRegistry.init(); await MCPRegistry.refresh(); const status = MCPRegistry.getStatus(); logger.info('MCP autostart completed', status); } } catch (err) { logger.error('Failed to initialize MCP', err); } }; void initAndRefresh(); // Subscribe to config changes this.#mcpUnsubscribe = onMCPConfigChange(() => { void initAndRefresh(); }); }
2149-2154: Guard MCP re-init with current config; avoid unnecessary work when disabled.- try { - await MCPRegistry.init(); - await MCPRegistry.refresh(); - } catch (err) { + try { + const cfg = getMCPConfig(); + if (cfg.enabled) { + await MCPRegistry.init(); + await MCPRegistry.refresh(); + } + } catch (err) { logger.error('Failed to reinitialize MCP after settings change', err); }
1989-2004: Prefer unknown over any in catch; format error safely.- } catch (error: any) { + } catch (error: unknown) { @@ - logger.error('Error in bookmark click handler', { error: error.message }); + const msg = error instanceof Error ? error.message : String(error); + logger.error('Error in bookmark click handler', { error: msg }); @@ - message: `Error bookmarking page: ${error.message}`, + message: `Error bookmarking page: ${msg}`,front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (3)
1-6: Fix license header and import order to satisfy lints.Add the standard license header and order value imports before type-only imports; third_party types before local types.
+// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. -import type { Tool } from '../tools/Tools.js'; -import { createLogger } from '../core/Logger.js'; -import type { MCPClient, MCPToolDef } from '../../../third_party/mcp-sdk/mcp-sdk.js'; +import { createLogger } from '../core/Logger.js'; +import type { MCPClient, MCPToolDef } from '../../../third_party/mcp-sdk/mcp-sdk.js'; +import type { Tool } from '../tools/Tools.js';
10-10: Remove explicit any in schema typing.Use unknown to appease eslint. Keep the fallback object.
- schema: any; + schema: unknown; @@ - const schema = def.inputSchema as any; + const schema = def.inputSchema as unknown; if (schema && typeof schema === 'object') { this.schema = schema; } else { this.schema = { type: 'object', properties: {} }; }Also applies to: 24-29
42-53: Broaden secret key redaction list (log-only).Consider also 'client_secret', 'access_token', 'refresh_token', 'set-cookie', 'cookie'. Keep redaction strictly for logs.
- const sensitive = ['token', 'api_key', 'password', 'secret', 'authorization']; + const sensitive = ['token', 'api_key', 'password', 'secret', 'authorization', 'client_secret', 'access_token', 'refresh_token', 'set-cookie', 'cookie'];front_end/panels/ai_chat/core/ToolSurfaceProvider.test.ts (4)
5-11: Fix import order to satisfy es-modules rules.Import MCP modules before core ToolSurfaceProvider/types as per lints.
-import { ToolSurfaceProvider } from './ToolSurfaceProvider.js'; -import type { AgentState } from './State.js'; -import type { Tool } from '../tools/Tools.js'; -import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js'; -import { MCPRegistry } from '../mcp/MCPRegistry.js'; -import { registerMCPMetaTools } from '../mcp/MCPMetaTools.js'; +import { registerMCPMetaTools } from '../mcp/MCPMetaTools.js'; +import { MCPRegistry } from '../mcp/MCPRegistry.js'; +import { ToolSurfaceProvider } from './ToolSurfaceProvider.js'; +import type { AgentState } from './State.js'; +import type { Tool } from '../tools/Tools.js'; +import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
95-102: Prefer assertion helpers for readability and lint compliance.Use include/lengthOf helpers.
- assert.ok(names.includes('core_tool_A')); - assert.ok(names.includes('core_tool_B')); + assert.include(names, 'core_tool_A'); + assert.include(names, 'core_tool_B'); const selectedMcp = names.filter(n => n.startsWith('mcp:default:')); - assert.strictEqual(selectedMcp.length, 2); + assert.lengthOf(selectedMcp, 2); // Alpha should be preferred due to query match - assert.ok(selectedMcp.includes('mcp:default:alpha')); + assert.include(selectedMcp, 'mcp:default:alpha');
129-135: Align assertions with chai helpers.- assert.ok(names.includes('core_tool_A')); - assert.ok(names.includes('mcp.search')); - assert.ok(names.includes('mcp.invoke')); + assert.include(names, 'core_tool_A'); + assert.include(names, 'mcp.search'); + assert.include(names, 'mcp.invoke');
61-70: Avoid global registry cross-test state.ToolRegistry is global; consider adding a test util to reset/teardown between tests to prevent leakage.
I can draft a ToolRegistry.resetForTests() helper and wire it in beforeEach/afterEach if you want.
Also applies to: 80-82
front_end/third_party/mcp-sdk/BUILD.gn (1)
9-45: LGTM on prebuilt packaging; ensure third-party licenses are shipped.The AJV, Zod, and eventsource-parser artifacts need corresponding LICENSE files in third_party and inclusion in GRD.
If not already done in this PR, please add/copy their LICENSE files and update devtools_grd_files.gni to include them.
front_end/panels/ai_chat/ui/SettingsDialog.ts (5)
2487-2510: Use braces and explicit return types per lint.Wrap single-line if and annotate return types on helpers.
- const formatTimestamp = (date: Date | undefined): string => { - if (!date) return ''; - return date.toLocaleString(); - }; + const formatTimestamp = (date: Date | undefined): string => { + if (!date) { + return ''; + } + return date.toLocaleString(); + };
2791-2796: Clear tool list without innerHTML.Use replaceChildren() to avoid innerHTML assignment.
- mcpToolsList.innerHTML = ''; + mcpToolsList.replaceChildren();
535-545: Remove unused catch param.Avoid ‘e’ unused var.
- } catch (e) { + } catch { // Fallback silently to storedProvider }
2579-2584: Add explicit return types to local helpers.Appease @typescript-eslint/explicit-function-return-type.
- const updateDisconnectButton = () => { + const updateDisconnectButton = (): void => { @@ - const updateBudgetControls = () => { + const updateBudgetControls = (): void => { @@ - const updateToolsList = () => { + const updateToolsList = (): void => {Also applies to: 2758-2768, 2791-2845
366-371: Prefer secure endpoint hint.For MCP endpoints, prefer wss:// over ws:// in hint text.
- mcpEndpointHint: 'HTTPS SSE or WebSocket endpoint (e.g., https://host/mcp or ws://localhost:9000)', + mcpEndpointHint: 'HTTPS SSE or secure WebSocket endpoint (e.g., https://host/mcp or wss://localhost:9000)',front_end/panels/ai_chat/core/AgentNodes.ts (4)
130-137: Trace the actually selected tools, not the base list.The observation input logs tools from getAgentToolsFromState(state), which can diverge from ToolSurfaceProvider.select. Either move selection earlier and log the selected set, or append an update to include selection.selectedNames for accuracy.
38-41: Replace console.log with logger. or guard under a debug flag (no-console violations).*There are many console.log statements; use logger.debug/info/warn or guard them behind an explicit debug flag to satisfy lint rules and avoid noisy prod logs.
Also applies to: 234-257, 485-567, 569-573, 579-606, 615-623, 671-681, 711-717, 747-749
154-156: Type safety for context fields and tool arrays (avoid any).Define selectedToolNames and toolNameMap on AgentState.context and type tools as Array<Tool<unknown, unknown>> to clear the no-explicit-any lint errors and prevent accidental shape drift.
I can add the minimal context typings in State.ts if you want.
Also applies to: 431-442, 444-445
5-14: Fix import ordering per ESLint import/order.Reorder imports so value imports precede type-only imports, and maintain group spacing as flagged by lint.
front_end/panels/ai_chat/mcp/MCPMetaTools.ts (2)
121-124: Lint cleanups: remove unused q; add curly braces to single-line ifs.Drop the unused q in execute() and wrap single-line ifs flagged by curly.
- const q = (args.query || '').toLowerCase(); + // no-op @@ - if (!key.startsWith('mcp:')) continue; - if (serverFilter) { - const serverId = key.split(':')[1] || ''; - if (!serverFilter.has(serverId)) continue; - } - const tool = ToolRegistry.getRegisteredTool(key); - if (!tool) continue; + if (!key.startsWith('mcp:')) { continue; } + if (serverFilter) { + const serverId = key.split(':')[1] || ''; + if (!serverFilter.has(serverId)) { continue; } + } + const tool = ToolRegistry.getRegisteredTool(key); + if (!tool) { continue; }Also applies to: 127-135
5-11: Import order/spacing per ESLint.Reorder so value imports precede type imports and keep group breaks.
front_end/panels/ai_chat/core/ToolSurfaceProvider.ts (4)
79-111: Avoid global pollution and console noise; gate debug helpers and logs.Attach debugToolSelection only in dev builds or behind an explicit flag, and replace console.log with logger.* to satisfy no-console.
Example:
-// DEBUG: Add a utility function to test MCP modes from console -(globalThis as any).debugToolSelection = { +// DEBUG (dev-only) +if (/* inject your dev flag */ false) { + (globalThis as any).debugToolSelection = { getCurrentMCPConfig: () => { - const cfg = getMCPConfig(); - console.log('Current MCP Config:', cfg); + const cfg = getMCPConfig(); + logger.debug('Current MCP Config:', cfg); return cfg; }, @@ -}; +}Also applies to: 118-128, 133-147, 149-161, 185-191, 194-215
192-195: Use ChatMessageEntity instead of string/any for user detection.Import ChatMessageEntity and compare against the enum; avoid (m as any).entity === 0.
+import { ChatMessageEntity } from '../ui/ChatView.js'; @@ - const lastUserMsg = [...state.messages].reverse().find(m => m.entity === 'user' || (m as any).entity === 0) as any; + const lastUserMsg = [...state.messages].reverse().find(m => m.entity === ChatMessageEntity.USER);
20-30: Type nits: prefer Array forms and drop unused selectedNames var.
- Use Array<Tool<unknown, unknown>> instead of Tool<any, any>[] to appease lint.
- Remove unused local selectedNames (you already return computed names).
Also applies to: 112-116, 129-132
5-12: Fix import ordering per ESLint import/order.Place MCPConfig before MCPRegistry per hints, and group type-only imports last.
front_end/panels/ai_chat/mcp/MCPRegistry.ts (5)
11-18: Fix interface formatting.The interface members should use semicolons instead of being implicitly terminated.
Apply this diff to fix the formatting:
export interface MCPRegistryStatus { enabled: boolean; - servers: Array<{ id: string; endpoint: string; connected: boolean; toolCount: number }>; + servers: Array<{ id: string; endpoint: string; connected: boolean; toolCount: number; }>; registeredToolNames: string[]; lastError?: string; lastErrorType?: 'connection' | 'authentication' | 'configuration' | 'network' | 'server_error' | 'unknown'; lastConnected?: Date; lastDisconnected?: Date; }
29-48: Remove trailing spaces.There are trailing spaces on lines 31 and 36 that should be removed.
Apply this diff to remove trailing spaces:
private categorizeError(error: unknown): 'connection' | 'authentication' | 'configuration' | 'network' | 'server_error' | 'unknown' { const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase(); - + if (message.includes('unauthorized') || message.includes('authentication') || message.includes('auth') || message.includes('token')) { return 'authentication'; } if (message.includes('network') || message.includes('timeout') || message.includes('connection reset') || message.includes('econnreset')) { - return 'network'; + return 'network'; }
87-125: Remove trailing spaces in refresh method.There are trailing spaces on lines 92 and 95 that should be removed.
Apply this diff to remove trailing spaces:
async refresh(): Promise<void> { const cfg = getMCPConfig(); if (!cfg.enabled || this.servers.length === 0) { return; } - + // Clear previously registered tools (ToolRegistry will overwrite on re-registration) this.registeredTools = []; - + const allow = new Set(cfg.toolAllowlist || []);
127-133: Improve error handling in dispose method.The empty catch block silently swallows errors. Consider at least logging disconnection failures for debugging purposes.
Apply this diff to improve error handling:
dispose(): void { for (const srv of this.servers) { - try { this.client.disconnect(srv.id); } catch {} + try { + this.client.disconnect(srv.id); + } catch (err) { + logger.warn('Failed to disconnect MCP server', { serverId: srv.id, err }); + } } this.lastDisconnected = new Date(); this.servers = []; }
55-85: Consider adding retry logic for connection failures.The init method attempts to connect once and logs the error if it fails. For better resilience, consider implementing retry logic with exponential backoff.
Would you like me to generate a retry mechanism with exponential backoff for the connection logic?
| const errorResultWithSession: ConfigurableAgentResult & { agentSession: any } = { | ||
| success: false, | ||
| error: 'Agent reached maximum iterations', | ||
| terminationReason: 'max_iterations', | ||
| // This is the problematic field that contains session data | ||
| intermediateSteps: [ | ||
| { entity: ChatMessageEntity.USER, text: 'test query' }, | ||
| { entity: ChatMessageEntity.AGENT_SESSION, agentSession: mockAgentSession, summary: 'test' } | ||
| ], | ||
| agentSession: mockAgentSession | ||
| }; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Replace any with a typed extension; avoid leaking agentSession types into LLM.
- const errorResultWithSession: ConfigurableAgentResult & { agentSession: any } = {
+ type AgentResultWithSession = ConfigurableAgentResult & { agentSession: unknown };
+ const errorResultWithSession: AgentResultWithSession = {
@@
- class MockConfigurableAgentTool extends ConfigurableAgentTool {
+ class MockConfigurableAgentTool extends ConfigurableAgentTool {
@@
- async execute(): Promise<ConfigurableAgentResult & { agentSession: any }> {
+ async execute(): Promise<AgentResultWithSession> {
return errorResultWithSession;
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const errorResultWithSession: ConfigurableAgentResult & { agentSession: any } = { | |
| success: false, | |
| error: 'Agent reached maximum iterations', | |
| terminationReason: 'max_iterations', | |
| // This is the problematic field that contains session data | |
| intermediateSteps: [ | |
| { entity: ChatMessageEntity.USER, text: 'test query' }, | |
| { entity: ChatMessageEntity.AGENT_SESSION, agentSession: mockAgentSession, summary: 'test' } | |
| ], | |
| agentSession: mockAgentSession | |
| }; | |
| // Define a reusable result type that carries an opaque session | |
| type AgentResultWithSession = ConfigurableAgentResult & { agentSession: unknown }; | |
| const errorResultWithSession: AgentResultWithSession = { | |
| success: false, | |
| error: 'Agent reached maximum iterations', | |
| terminationReason: 'max_iterations', | |
| // This is the problematic field that contains session data | |
| intermediateSteps: [ | |
| { entity: ChatMessageEntity.USER, text: 'test query' }, | |
| { entity: ChatMessageEntity.AGENT_SESSION, agentSession: mockAgentSession, summary: 'test' } | |
| ], | |
| agentSession: mockAgentSession | |
| }; | |
| class MockConfigurableAgentTool extends ConfigurableAgentTool { | |
| // Use the alias here instead of repeating an `any`-based type | |
| async execute(): Promise<AgentResultWithSession> { | |
| return errorResultWithSession; | |
| } | |
| } |
🧰 Tools
🪛 ESLint
[error] 45-45: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/AgentNodes.test.ts around lines 45-56, the test
uses "any" for agentSession and embeds full session objects in
intermediateSteps, leaking agentSession internals into LLM-facing step data; fix
by replacing "any" with a concrete typed extension: import/define an
AgentSession (or AgentSessionSummary) type and type the test value as
ConfigurableAgentResult & { agentSession: AgentSession | null }; change
intermediateSteps so AGENT_SESSION steps carry a minimal typed summary (e.g., {
entity: ChatMessageEntity.AGENT_SESSION; agentSession: AgentSessionSummary;
summary: string }) or remove the full session from intermediateSteps and only
store it on agentSession; update the mock to match the new types and ensure no
raw session internals are passed into LLM-facing fields.
| const successResultWithSession: ConfigurableAgentResult & { agentSession: any } = { | ||
| success: true, | ||
| output: 'Task completed successfully', | ||
| terminationReason: 'final_answer', | ||
| // This should not leak to LLM | ||
| intermediateSteps: [ | ||
| { entity: ChatMessageEntity.AGENT_SESSION, agentSession: mockAgentSession, summary: 'test' } | ||
| ], | ||
| agentSession: mockAgentSession | ||
| }; | ||
|
|
||
| class MockSuccessAgentTool extends ConfigurableAgentTool { | ||
| constructor() { | ||
| super({ | ||
| name: 'mock_success_agent', | ||
| description: 'Mock success agent for testing', | ||
| systemPrompt: 'Test prompt', | ||
| tools: [], | ||
| schema: { type: 'object', properties: {}, required: [] } | ||
| }); | ||
| } | ||
|
|
||
| async execute(): Promise<ConfigurableAgentResult & { agentSession: any }> { | ||
| return successResultWithSession; | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Mirror type cleanup for success-path tool result.
- const successResultWithSession: ConfigurableAgentResult & { agentSession: any } = {
+ type AgentResultWithSession = ConfigurableAgentResult & { agentSession: unknown };
+ const successResultWithSession: AgentResultWithSession = {
@@
- async execute(): Promise<ConfigurableAgentResult & { agentSession: any }> {
+ async execute(): Promise<AgentResultWithSession> {
return successResultWithSession;
}🧰 Tools
🪛 ESLint
[error] 140-140: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 162-162: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
| class RecordingTool implements Tool<Record<string, unknown>, unknown> { | ||
| public calls = 0; | ||
| constructor(public name: string) {} | ||
| description = 'records calls'; | ||
| schema = { type: 'object', properties: {} }; | ||
| async execute(_args: Record<string, unknown>): Promise<unknown> { | ||
| this.calls += 1; | ||
| return { ok: true, executed: this.name }; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Drop explicit public modifier; keep constructor param readonly.
Satisfies @typescript-eslint/explicit-member-accessibility and keeps intent.
-class RecordingTool implements Tool<Record<string, unknown>, unknown> {
- public calls = 0;
- constructor(public name: string) {}
+class RecordingTool implements Tool<Record<string, unknown>, unknown> {
+ calls = 0;
+ constructor(readonly name: string) {}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| class RecordingTool implements Tool<Record<string, unknown>, unknown> { | |
| public calls = 0; | |
| constructor(public name: string) {} | |
| description = 'records calls'; | |
| schema = { type: 'object', properties: {} }; | |
| async execute(_args: Record<string, unknown>): Promise<unknown> { | |
| this.calls += 1; | |
| return { ok: true, executed: this.name }; | |
| } | |
| } | |
| class RecordingTool implements Tool<Record<string, unknown>, unknown> { | |
| calls = 0; | |
| constructor(readonly name: string) {} | |
| description = 'records calls'; | |
| schema = { type: 'object', properties: {} }; | |
| async execute(_args: Record<string, unknown>): Promise<unknown> { | |
| this.calls += 1; | |
| return { ok: true, executed: this.name }; | |
| } | |
| } |
🧰 Tools
🪛 ESLint
[error] 13-13: Public accessibility modifier on class property calls.
(@typescript-eslint/explicit-member-accessibility)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/ToolNameMapping.test.ts around lines 12–21,
remove explicit "public" accessibility modifiers and make the constructor
parameter readonly: change "public calls = 0;" to "calls = 0;" and change
"constructor(public name: string) {}" to "constructor(readonly name: string)
{}", leaving description and schema as-is.
| beforeEach(() => { | ||
| // Mock localStorage in case anything touches it | ||
| mockLocalStorage = new Map(); | ||
| Object.defineProperty(window, 'localStorage', { | ||
| value: { | ||
| getItem: (key: string) => mockLocalStorage.get(key) || null, | ||
| setItem: (key: string, value: string) => mockLocalStorage.set(key, value), | ||
| removeItem: (key: string) => mockLocalStorage.delete(key), | ||
| clear: () => mockLocalStorage.clear(), | ||
| }, | ||
| writable: true, | ||
| }); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| mockLocalStorage.clear(); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Restore real localStorage after each test to avoid cross-test side effects.
Current code overrides window.localStorage but never restores it.
describe('AgentNodes sanitized tool name mapping', () => {
- let mockLocalStorage: Map<string, string>;
+ let mockLocal: Map<string, string>;
+ let realLocalStorage: Storage;
beforeEach(() => {
- // Mock localStorage in case anything touches it
- mockLocalStorage = new Map();
+ mockLocal = new Map();
+ realLocalStorage = window.localStorage;
Object.defineProperty(window, 'localStorage', {
value: {
- getItem: (key: string) => mockLocalStorage.get(key) || null,
- setItem: (key: string, value: string) => mockLocalStorage.set(key, value),
- removeItem: (key: string) => mockLocalStorage.delete(key),
- clear: () => mockLocalStorage.clear(),
+ getItem: (key: string) => mockLocal.get(key) || null,
+ setItem: (key: string, value: string) => mockLocal.set(key, value),
+ removeItem: (key: string) => mockLocal.delete(key),
+ clear: () => mockLocal.clear(),
},
writable: true,
});
});
afterEach(() => {
- mockLocalStorage.clear();
+ mockLocal.clear();
+ Object.defineProperty(window, 'localStorage', { value: realLocalStorage, writable: true });
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| beforeEach(() => { | |
| // Mock localStorage in case anything touches it | |
| mockLocalStorage = new Map(); | |
| Object.defineProperty(window, 'localStorage', { | |
| value: { | |
| getItem: (key: string) => mockLocalStorage.get(key) || null, | |
| setItem: (key: string, value: string) => mockLocalStorage.set(key, value), | |
| removeItem: (key: string) => mockLocalStorage.delete(key), | |
| clear: () => mockLocalStorage.clear(), | |
| }, | |
| writable: true, | |
| }); | |
| }); | |
| afterEach(() => { | |
| mockLocalStorage.clear(); | |
| }); | |
| describe('AgentNodes sanitized tool name mapping', () => { | |
| let mockLocal: Map<string, string>; | |
| let realLocalStorage: Storage; | |
| beforeEach(() => { | |
| mockLocal = new Map(); | |
| realLocalStorage = window.localStorage; | |
| Object.defineProperty(window, 'localStorage', { | |
| value: { | |
| getItem: (key: string) => mockLocal.get(key) || null, | |
| setItem: (key: string, value: string) => mockLocal.set(key, value), | |
| removeItem: (key: string) => mockLocal.delete(key), | |
| clear: () => mockLocal.clear(), | |
| }, | |
| writable: true, | |
| }); | |
| }); | |
| afterEach(() => { | |
| mockLocal.clear(); | |
| Object.defineProperty(window, 'localStorage', { value: realLocalStorage, writable: true }); | |
| }); | |
| // ...other tests... | |
| }); |
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/ToolNameMapping.test.ts around lines 26 to 42,
the test suite replaces window.localStorage but never restores the original,
causing cross-test side effects; capture the originalLocalStorage before
redefining window.localStorage in beforeEach, continue to set the mock there,
and then in afterEach restore window.localStorage = originalLocalStorage (and
clear/mockLocalStorage as needed) so the real browser/storage API is reinstated
for subsequent tests.
| const state: AgentState = { | ||
| messages: [ | ||
| { | ||
| entity: ChatMessageEntity.MODEL, | ||
| action: 'tool', | ||
| toolName: sanitizedName, | ||
| toolArgs: { x: 1 }, | ||
| toolCallId: 'call-1', | ||
| isFinalAnswer: false, | ||
| } as any | ||
| ], | ||
| agentType: 'deep-research' as any, | ||
| context: { | ||
| selectedToolNames: [sanitizedName], | ||
| selectedTools: [tool], | ||
| toolNameMap: { [sanitizedName]: originalName } | ||
| } | ||
| } as any; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Remove any casts; type the message and state precisely.
- const state: AgentState = {
+ const modelToolCall: ModelChatMessage = {
+ entity: ChatMessageEntity.MODEL,
+ action: 'tool',
+ toolName: sanitizedName,
+ toolArgs: { x: 1 } as Record<string, unknown>,
+ toolCallId: 'call-1',
+ isFinalAnswer: false,
+ };
+ const state = {
messages: [
- {
- entity: ChatMessageEntity.MODEL,
- action: 'tool',
- toolName: sanitizedName,
- toolArgs: { x: 1 },
- toolCallId: 'call-1',
- isFinalAnswer: false,
- } as any
+ modelToolCall,
],
agentType: 'deep-research' as any,
context: {
selectedToolNames: [sanitizedName],
selectedTools: [tool],
toolNameMap: { [sanitizedName]: originalName }
}
- } as any;
+ };Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 ESLint
[error] 59-59: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 61-61: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 67-67: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/ToolNameMapping.test.ts around lines 50–67,
remove all "as any" casts and construct the message and state with their real
types: import and use the exact message/interface types (e.g.,
ChatMessage/AgentMessage and AgentState), create a strongly typed message object
using the ChatMessageEntity enum for entity, a properly typed action union for
'tool', set toolName/toolArgs/toolCallId/isFinalAnswer with correct types,
assign agentType using the AgentType type, and build context with the exact
typed shape (selectedToolNames: string[], selectedTools: Tool[], toolNameMap:
Record<string,string>); replace each "as any" with these concrete typed objects
so the test is fully type-safe.
| getStatus(): MCPRegistryStatus { | ||
| return { | ||
| enabled: getMCPConfig().enabled, | ||
| servers: this.servers.map(s => ({ | ||
| id: s.id, | ||
| endpoint: s.endpoint, | ||
| connected: this.client.isConnected(s.id), | ||
| toolCount: 0, | ||
| })), | ||
| registeredToolNames: [...this.registeredTools], | ||
| lastError: this.lastError, | ||
| lastErrorType: this.lastErrorType, | ||
| lastConnected: this.lastConnected, | ||
| lastDisconnected: this.lastDisconnected, | ||
| }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
toolCount is always 0 in getStatus.
The toolCount field in the server status is hardcoded to 0, which doesn't provide accurate information about the number of tools available from each server.
Consider tracking the actual tool count per server. You could maintain a Map to store tool counts:
class RegistryImpl {
private client = new MCPClient();
private servers: MCPServer[] = [];
private registeredTools: string[] = [];
+ private toolCountPerServer = new Map<string, number>();
private lastError?: string;Then update it in the refresh method:
async refresh(): Promise<void> {
const cfg = getMCPConfig();
if (!cfg.enabled || this.servers.length === 0) {
return;
}
// Clear previously registered tools (ToolRegistry will overwrite on re-registration)
this.registeredTools = [];
+ this.toolCountPerServer.clear();
const allow = new Set(cfg.toolAllowlist || []);
for (const srv of this.servers) {
if (!this.client.isConnected(srv.id)) {
continue;
}
+ let registeredCount = 0;
let tools: MCPToolDef[] = [];
try {
tools = await this.client.listTools(srv.id);
} catch (err) {
this.setError(err);
logger.error('listTools failed', err);
continue;
}
for (const def of tools) {
const namespaced = `mcp:${srv.id}:${def.name}`;
if (allow.size > 0 && !allow.has(namespaced) && !allow.has(def.name)) {
continue;
}
try {
const factoryName = namespaced;
ToolRegistry.registerToolFactory(factoryName, () => new MCPToolAdapter(srv.id, this.client, def, namespaced));
this.registeredTools.push(factoryName);
+ registeredCount++;
} catch (err) {
logger.error('Failed to register MCP tool', { tool: def.name, err });
}
}
+ this.toolCountPerServer.set(srv.id, registeredCount);
}
}And use it in getStatus:
getStatus(): MCPRegistryStatus {
return {
enabled: getMCPConfig().enabled,
servers: this.servers.map(s => ({
id: s.id,
endpoint: s.endpoint,
connected: this.client.isConnected(s.id),
- toolCount: 0,
+ toolCount: this.toolCountPerServer.get(s.id) || 0,
})),
registeredToolNames: [...this.registeredTools],📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| getStatus(): MCPRegistryStatus { | |
| return { | |
| enabled: getMCPConfig().enabled, | |
| servers: this.servers.map(s => ({ | |
| id: s.id, | |
| endpoint: s.endpoint, | |
| connected: this.client.isConnected(s.id), | |
| toolCount: 0, | |
| })), | |
| registeredToolNames: [...this.registeredTools], | |
| lastError: this.lastError, | |
| lastErrorType: this.lastErrorType, | |
| lastConnected: this.lastConnected, | |
| lastDisconnected: this.lastDisconnected, | |
| }; | |
| } | |
| getStatus(): MCPRegistryStatus { | |
| return { | |
| enabled: getMCPConfig().enabled, | |
| servers: this.servers.map(s => ({ | |
| id: s.id, | |
| endpoint: s.endpoint, | |
| connected: this.client.isConnected(s.id), | |
| toolCount: this.toolCountPerServer.get(s.id) || 0, | |
| })), | |
| registeredToolNames: [...this.registeredTools], | |
| lastError: this.lastError, | |
| lastErrorType: this.lastErrorType, | |
| lastConnected: this.lastConnected, | |
| lastDisconnected: this.lastDisconnected, | |
| }; | |
| } |
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/mcp/MCPRegistry.ts around lines 135-150, toolCount
is hardcoded to 0; add a Map<string, number> (e.g., serverToolCounts) keyed by
server.id to track each server's tool count, initialize it in the class
constructor, update the map in the refresh method when you fetch or reconcile
tools for a given server (set to the fetched tools.length or 0 on
errors/disconnect), and change getStatus to read toolCount from
serverToolCounts.get(s.id) || 0 so the returned status reflects the actual
per-server tool counts.
| #mcpUnsubscribe: (() => void) | null = null; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Use unsubscribe handle or remove it.
Currently flagged as unused; wire it into lifecycle to prevent leaks.
- #mcpUnsubscribe: (() => void) | null = null;
+ #mcpUnsubscribe: (() => void) | null = null;Follow-up change in willHide below unsubscribes.
Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 ESLint
[error] 725-725: '#mcpUnsubscribe' is defined but never used.
(no-unused-private-class-members)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/ui/AIChatPanel.ts around lines 725-726, the private
field "#mcpUnsubscribe: (() => void) | null = null;" is declared but not used;
either remove it or wire it into the component lifecycle to avoid leaks. Fix by
assigning the unsubscribe function returned from whatever subscribe/observe call
creates the subscription to "#mcpUnsubscribe" at the point of subscription, and
in willHide (the lifecycle teardown) invoke "#mcpUnsubscribe()" if non-null then
set it to null so the subscription is cleaned up.
| MIT License | ||
|
|
||
| Copyright (c) 2024 Anthropic, PBC | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. No newline at end of file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Ensure license mapping into about:credits
Validate that the GN/GRD lists include this path so credits/legal pages pick it up.
🏁 Script executed:
#!/bin/bash
rg -nP 'mcp-sdk' config/gni devtools_grd_files.gni front_end/third_party/**/BUILD.gn -S -n || trueLength of output: 2399
Include mcp-sdk LICENSE in about:credits
Add "front_end/third_party/mcp-sdk/LICENSE" to both grd_files_release_sources and grd_files_debug_sources in config/gni/devtools_grd_files.gni so the license is picked up by about:credits.
🤖 Prompt for AI Agents
In front_end/third_party/mcp-sdk/LICENSE lines 1-21: the LICENSE file for
mcp-sdk is present but not referenced; add
"front_end/third_party/mcp-sdk/LICENSE" to both grd_files_release_sources and
grd_files_debug_sources arrays in config/gni/devtools_grd_files.gni so the
license is included in about:credits; update the two lists with the correct
comma-separated entry and maintain existing formatting/indentation.
| Dependencies: | ||
| - eventsource-parser@3.0.6 (MIT license) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Dependencies list appears incomplete
Ajv is bundled under mcp-sdk/ajv; list it (and any other bundled deps) to keep licensing accurate.
Dependencies:
-- eventsource-parser@3.0.6 (MIT license)
+- eventsource-parser@3.0.6 (MIT)
+- ajv@<version> (MIT)
+# Add any additional transitive/bundled deps present under this directory.🏁 Script executed:
#!/bin/bash
# Detect third-party modules actually present under mcp-sdk
fd -td . front_end/third_party/mcp-sdk -d 2
# Heuristic: scan dist for import/require references
rg -nP '(import|require)\(["'\'']([^"'\'']+)["'\'']\)' front_end/third_party/mcp-sdk -S -n -g '!**/README*' -g '!**/LICENSE*' -C0 | sed -E 's/.*[("\'\'']([^"'\''/]+).*/\1/' | sort -uLength of output: 974
🏁 Script executed:
#!/bin/bash
# List package.json files under mcp-sdk
package_files=$(fd package.json front_end/third_party/mcp-sdk -t f)
echo "package.json files found:"
echo "$package_files"
echo ""
# Extract name, version, license from each package.json
for f in $package_files; do
echo "File: $f"
awk '/"name"/ || /"version"/ || /"license"/' "$f"
echo ""
doneLength of output: 3019
Add missing bundled dependencies to README.chromium
Dependencies list in front_end/third_party/mcp-sdk/README.chromium currently only includes eventsource-parser@3.0.6. Please also list ajv@8.17.1 and zod@3.23.8 (both MIT) and keep a placeholder for any future bundled/transitive deps:
Dependencies:
- eventsource-parser@3.0.6 (MIT)
+- ajv@8.17.1 (MIT)
+- zod@3.23.8 (MIT)
# Add any additional transitive/bundled deps present under this directory.Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In front_end/third_party/mcp-sdk/README.chromium around lines 14 to 16, the
bundled dependencies list only mentions eventsource-parser@3.0.6; update the
Dependencies section to also include ajv@8.17.1 (MIT) and zod@3.23.8 (MIT), and
add a short placeholder line such as "Other bundled/transitive dependencies may
be listed here" for future additions; ensure each entry follows the same format
as the existing line (package@version (license)).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 26
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
front_end/panels/ai_chat/ui/ChatView.ts (1)
1801-1807: Avoid empty button text when isLastMessage is true.
📄 ${isLastMessage ? '' : 'View Full Screen'}yields an icon-only button (poor a11y) for last message. Keep a consistent text label and/or add aria-label.- 📄 ${isLastMessage ? '' : 'View Full Screen'} + 📄 View Full ScreenOptionally, also add:
- <button + <button + aria-label="View full screen"front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts (1)
639-644: Tools referenced but never registered: action_verification_agent will fail at runtime.The agent uses inspect_element and get_console_logs, but there are no ToolRegistry registrations for these tool ids in initializeConfiguredAgents().
Proposed fix (imports + registrations):
-import { NavigateURLTool, PerformActionTool, GetAccessibilityTreeTool, SearchContentTool, NavigateBackTool, NodeIDsToURLsTool, TakeScreenshotTool, ScrollPageTool } from '../../tools/Tools.js'; +import { NavigateURLTool, PerformActionTool, GetAccessibilityTreeTool, SearchContentTool, NavigateBackTool, NodeIDsToURLsTool, TakeScreenshotTool, ScrollPageTool, InspectElementTool, GetConsoleLogsTool } from '../../tools/Tools.js'; @@ ToolRegistry.registerToolFactory('take_screenshot', () => new TakeScreenshotTool()); + ToolRegistry.registerToolFactory('inspect_element', () => new InspectElementTool()); + ToolRegistry.registerToolFactory('get_console_logs', () => new GetConsoleLogsTool());front_end/panels/ai_chat/core/AgentNodes.ts (2)
172-186: Pass sanitized tool names to LLMUse the sanitized names when constructing the tools array for
llm.call.- tools: tools.map(tool => ({ + tools: sanitizedTools.map(({ sanitized, tool }) => ({ type: 'function', function: { - name: tool.name, + name: sanitized, description: tool.description, parameters: tool.schema, } })),
474-478: Don’t throw on missing tool; return structured TOOL_RESULT errorThrowing here skips result linking and breaks the chat flow. Emit a ToolResultMessage with
isError: true.- if (!selectedTool) { - throw new Error(`Tool ${toolName} not found`); - } + if (!selectedTool) { + const errorText = `Tool ${toolName} not found`; + const messages = [...state.messages, { + entity: ChatMessageEntity.TOOL_RESULT, + toolName, + resultText: errorText, + isError: true, + toolCallId, + error: errorText, + } as ToolResultMessage]; + return { ...state, messages, error: errorText }; + }front_end/panels/ai_chat/BUILD.gn (1)
296-312: Unittests likely need MCP SDK depMCPClientSDK.test.ts may import the MCP SDK transitively or directly. Add the bundle to unittests deps to avoid build flakiness.
ts_library("unittests") { testonly = true sources = [ "common/utils.test.ts", "mcp/MCPClientSDK.test.ts", "core/ToolSurfaceProvider.test.ts", "core/ToolNameMapping.test.ts", ] deps = [ ":ai_chat", "../../testing", "../../core/sdk:bundle", "../../generated:protocol", + "../../third_party/mcp-sdk:bundle", ] }
♻️ Duplicate comments (3)
front_end/panels/ai_chat/mcp/MCPClientSDK.test.ts (1)
53-134: Make the localhost integration test opt-in to avoid CI flakes.This block depends on a locally running MCP server; it will hang/fail in CI. Gate behind an env flag or skip by default.
Proposed change:
-describe('connects to local Hacker News MCP server via SDK', function() { +const runMcpE2E = process && process.env && process.env.MCP_E2E === '1'; +(runMcpE2E ? describe : describe.skip)('connects to local Hacker News MCP server via SDK', function() { this.timeout(30000); // Longer timeout for integration testAnd consider a shorter default timeout (e.g., 10s) with an AbortController inside connect if supported by the SDK.
front_end/panels/ai_chat/core/AgentNodes.ts (1)
150-167: Sanitize tool names for LLMs and persist mappingLLM function-calling requires
^[a-zA-Z0-9_-]{1,64}$. MCP tool names likemcp:default:alphawill be rejected. Sanitize names beforellm.call, persisttoolNameMap(sanitized → original), and expose sanitized names inselectedToolNamesfor the executor. Also use sanitized names in error handler.- // Select tools based on MCP mode (all/router/meta) - const baseTools = getAgentToolsFromState(state); - const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 }); - // Persist selection in context so ToolExecutorNode can resolve the same set - if (!state.context) { (state as any).context = {}; } - (state.context as any).selectedToolNames = selection.selectedNames; - const tools = selection.tools; + // Select tools based on MCP mode (all/router/meta) + const baseTools = getAgentToolsFromState(state); + const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 }); + // Persist selection and sanitized tool-name mapping for LLM compatibility + if (!state.context) { (state as any).context = {}; } + const shortHash = (s: string) => { let h = 0; for (let i = 0; i < s.length; i++) { h = (h * 31 + s.charCodeAt(i)) | 0; } return (h >>> 0).toString(36).slice(0, 6); }; + const toolNameMap: Record<string, string> = {}; + const sanitizedTools = selection.tools.map(tool => { + const original = tool.name; + let sanitized = original.replace(/[^a-zA-Z0-9_-]/g, '_'); + if (sanitized.length > 64) sanitized = sanitized.slice(0, 64); + if (toolNameMap[sanitized] && toolNameMap[sanitized] !== original) { + const suffix = shortHash(original); + const maxBase = 64 - (suffix.length + 1); + sanitized = `${sanitized.slice(0, maxBase)}_${suffix}`; + } + toolNameMap[sanitized] = original; + return { sanitized, tool }; + }); + (state.context as any).toolNameMap = toolNameMap; + (state.context as any).selectedToolNames = Object.keys(toolNameMap); + const tools = selection.tools; @@ - availableTools: tools.map(t => t.name) + availableTools: sanitizedTools.map(t => t.sanitized)front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (1)
32-36: Do not pass redacted args to MCP tools; only log sanitized copyCurrent code redacts secrets in
argsand sends the redacted object to the server, breaking auth. Log the sanitized copy but invoke with originalargs.async execute(args: Record<string, unknown>): Promise<unknown> { - const sanitized = this.sanitize(args); - logger.info('Executing MCP tool', { name: this.name, serverId: this.serverId }); - return this.client.callTool(this.serverId, this.def.name, sanitized, { timeoutMs: 30000 }); + const sanitized = this.sanitize(args); + logger.info('Executing MCP tool', { name: this.name, serverId: this.serverId, args: sanitized }); + return this.client.callTool(this.serverId, this.def.name, args, { timeoutMs: 30000 }); }
🧹 Nitpick comments (35)
front_end/third_party/mcp-sdk/LICENSE (1)
1-21: MIT text OK; add trailing newline at EOF for tooling hygiene.Some linters and license scanners expect a newline-terminated file.
Apply:
MIT License @@ SOFTWARE. +front_end/panels/ai_chat/ui/ChatView.ts (2)
1783-1789: Label change is fine; add an aria-label for accessibility.Button has an emoji and visible text; add an explicit aria-label to ensure a stable accessible name.
- <button - class="view-document-btn" + <button + class="view-document-btn" + aria-label="View full screen" @click=${() => this.#openInAIAssistantViewer(structuredResponse.markdownReport)} title="Open full report in document viewer"> 📄 View Full Screen </button>
1760-1811: Consider extracting the label to i18n/constants to avoid string drift.Two buttons now use “View Full Screen”. Centralize to a constant or i18n key so tests and UI copy stay consistent.
front_end/third_party/mcp-sdk/ajv/LICENSE (1)
1-22: Ajv MIT license included; add trailing newline if missing.Keep formatting consistent across third-party licenses.
SOFTWARE. +Ensure zod and eventsource-parser licenses are also present alongside their bundled code (see script in previous comment).
front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts (1)
196-204: Align instance retrieval with registration behavior.You eagerly instantiate and store tools at registration, but
getToolInstancereturns a fresh instance, whilegetRegisteredToolreturns the stored one. This can create multiple instances and state divergence.static getToolInstance(name: string): Tool<any, any> | null { - const factory = this.toolFactories.get(name); - return factory ? factory() : null; + const existing = this.registeredTools.get(name); + if (existing) return existing; + const factory = this.toolFactories.get(name); + if (!factory) return null; + const instance = factory(); + this.registeredTools.set(name, instance); + return instance; }If you intend ephemeral instances, rename one of the methods for clarity (e.g.,
createToolInstancevsgetRegisteredTool), and document lifecycle expectations.front_end/third_party/mcp-sdk/README.chromium (1)
17-24: Add provenance checksums and dependency license note.To make updates reproducible and pass third_party compliance, please include SHA256 checksums for the fetched tarballs and explicitly note where the dependency license is checked in-tree.
Suggested edit:
To update this package: -wget -qO- https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.3.tgz | tar xzf - -wget -qO- https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz | tar xzf - +wget -qO- https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.3.tgz | tar xzf - +wget -qO- https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz | tar xzf - + +Checksums (sha256): +- sdk-1.12.3.tgz: <fill-on-update> +- eventsource-parser-3.0.6.tgz: <fill-on-update> + +Notes: +- Ensure eventsource-parser’s MIT LICENSE is present at front_end/third_party/mcp-sdk/eventsource-parser/package/LICENSE (or add a copy under this directory).front_end/third_party/mcp-sdk/ajv/.runkit_example.js (1)
1-23: Exclude upstream RunKit example from bundles/lints.This dev-only example shouldn’t ship or trigger lint noise. Either exclude it from GN/GRD or hard-disable linters.
Suggested minimal header:
+/* eslint-disable @typescript-eslint/no-var-requires, no-console */ +// Upstream Ajv RunKit example; retained for completeness. Excluded from bundles. const Ajv = require("ajv")If possible, also exclude this path from any third_party bundling rules.
front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts (2)
98-100: Guard MCP meta-tools registration for idempotency and environments without SDK.If initializeConfiguredAgents() is called twice or the SDK isn’t available in some builds, registerMCPMetaTools() may throw or double-register.
Suggested defensive wrap:
- // Ensure MCP meta-tools are available regardless of mode; selection logic decides if they are surfaced - registerMCPMetaTools(); + // Ensure MCP meta-tools are available; selection logic decides surfacing + try { + // Optional: skip if already registered + // if (!ToolRegistry.hasTool || !ToolRegistry.hasTool('mcp.search')) { + registerMCPMetaTools(); + // } + } catch (e) { + console.warn('MCP meta-tools registration skipped:', e); + }
354-366: Nit: misleading comment.Comment says “For the action agent…” inside the research agent block; likely a copy/paste artifact.
- // For the action agent, we use the objective as the primary input, not the query field + // Prepare the user message for the research agentfront_end/third_party/mcp-sdk/ajv/README.md (1)
1-208: Silence linters for vendored docs rather than editing upstream content.markdownlint/LanguageTool warnings are expected for third_party. Prefer excluding front_end/third_party/** in lint configs or adding path-level suppressions in the tooling.
Action: update lint config to ignore third_party paths (no changes to this file).
front_end/panels/ai_chat/mcp/MCPClientSDK.test.ts (3)
35-42: Type-safe error assertions.Avoid relying on error.message without narrowing; keeps tests resilient to non-Error throws.
- } catch (error) { - assert.ok(error.message.includes('No connection for server')); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + assert.ok(msg.includes('No connection for server')); }Apply to both tests.
Also applies to: 44-51
24-26: Assert import (if not globally provided).If assert isn’t globally injected by the test runner, import from 'assert/strict' or chai to avoid runtime ReferenceError.
+import {strict as assert} from 'assert';If globals are guaranteed in this repo, ignore.
61-117: Reduce noisy console output in unit runs.Gate logs behind an env flag to keep CI output clean.
- console.log('=== Testing Local Hacker News MCP Server with SDK ==='); + if (process.env.VERBOSE_TESTS) console.log('=== Testing Local Hacker News MCP Server with SDK ==='); @@ - console.log('Step 1: Connecting to MCP server via SDK...'); + if (process.env.VERBOSE_TESTS) console.log('Step 1: Connecting to MCP server via SDK...'); @@ - console.log('✓ Connection established via SDK'); + if (process.env.VERBOSE_TESTS) console.log('✓ Connection established via SDK');(Repeat pattern for other logs.)
front_end/panels/ai_chat/core/ToolNameMapping.test.ts (1)
71-79: Address lint: preferassert.isOkand strip trailing spaces
- Replace
assert.ok(...)withassert.isOk(...).- Remove trailing whitespace on Line 71.
- const result = await node.invoke(state); - + const result = await node.invoke(state); @@ - assert.ok((last as any).resultText); + assert.isOk((last as any).resultText);front_end/third_party/mcp-sdk/BUILD.gn (1)
21-26: Avoid bundling both ESM and CJS zod buildsIncluding both
zod/lib/index.js(CJS) andzod/lib/index.mjs(ESM) can bloat the bundle or confuse resolution. Pick one variant used by the bundler/runtime.- "zod/lib/index.js", - "zod/lib/index.mjs", + # Prefer one module format (ESM shown here) + "zod/lib/index.mjs",front_end/panels/ai_chat/core/AgentNodes.ts (2)
83-84: Fix typo in log message"Coudnt" → "Couldn't".
- logger.warn('Coudnt find the answer'); + logger.warn("Couldn't find the answer");
379-392: Optional: ensure assistant tool-call messagecontentis empty string, notundefinedSome providers reject
undefined; empty string is safer. Verify with your LLM client’s contract.- llmMessages.push({ + llmMessages.push({ role: 'assistant', - content: undefined, + content: '', tool_calls: [{front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (1)
42-53: Optional: sanitize nested objects for loggingCurrent sanitization only redacts top-level fields. Consider recursive traversal.
- private sanitize(input: Record<string, unknown>): Record<string, unknown> { + private sanitize(input: Record<string, unknown>): Record<string, unknown> { const sensitive = ['token', 'api_key', 'password', 'secret', 'authorization']; - const out: Record<string, unknown> = {}; - for (const [k, v] of Object.entries(input || {})) { - if (typeof v === 'string' && sensitive.some(s => k.toLowerCase().includes(s))) { - out[k] = '[redacted]'; - } else { - out[k] = v; - } - } - return out; + const redact = (obj: any): any => { + if (!obj || typeof obj !== 'object') return obj; + const out: any = Array.isArray(obj) ? [] : {}; + for (const [k, v] of Object.entries(obj)) { + if (typeof v === 'string' && sensitive.some(s => k.toLowerCase().includes(s))) { + out[k] = '[redacted]'; + } else if (v && typeof v === 'object') { + out[k] = redact(v as any); + } else { + out[k] = v; + } + } + return out; + }; + return redact(input || {}); }front_end/panels/ai_chat/core/ToolSurfaceProvider.test.ts (4)
95-102: Preferassert.isOkand length helpersUse
assert.isOkandassert.lengthOfper lint rules.- assert.ok(names.includes('core_tool_A')); - assert.ok(names.includes('core_tool_B')); + assert.isOk(names.includes('core_tool_A')); + assert.isOk(names.includes('core_tool_B')); const selectedMcp = names.filter(n => n.startsWith('mcp:default:')); - assert.strictEqual(selectedMcp.length, 2); + assert.lengthOf(selectedMcp, 2); // Alpha should be preferred due to query match - assert.ok(selectedMcp.includes('mcp:default:alpha')); + assert.isOk(selectedMcp.includes('mcp:default:alpha'));
84-89: Use enum for message entity instead ofanyThis removes two
anyusages and improves readability.- const state: AgentState = { - messages: [{ entity: 0 as any, text: 'please run alpha operation' }], - context: {}, - selectedAgentType: 'deep-research' as any - } as any; + const state: AgentState = { + messages: [{ entity: ChatMessageEntity.USER as any, text: 'please run alpha operation' }], + context: {}, + selectedAgentType: 'deep-research' as any + } as any;
129-135: Preferassert.isOkoverassert.okAligns with project lint rule.
- assert.ok(names.includes('core_tool_A')); - assert.ok(names.includes('mcp.search')); - assert.ok(names.includes('mcp.invoke')); + assert.isOk(names.includes('core_tool_A')); + assert.isOk(names.includes('mcp.search')); + assert.isOk(names.includes('mcp.invoke'));
24-45: Consider adding a test-only reset for ToolRegistryWithout a clear reset API, registered tools can bleed across tests. Offer to add
ToolRegistry.resetForTests()to clear internal maps inbeforeEach/afterEach.front_end/panels/ai_chat/mcp/MCPConfig.ts (5)
83-87: Annotate the event listener callback return type to satisfy lint.Avoids the explicit-return-type error on Line 84.
- const cb = () => handler(); + const cb: () => void = () => handler();
1-4: Add the required license header.Build/lint will fail without it.
+// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import { createLogger } from '../core/Logger.js';
51-71: Allow clearing optional fields (endpoint/allowlist/etc.).Current writes only when value is defined; there’s no way to delete a persisted key. Consider treating null/empty-string as a delete.
- if (config.endpoint !== undefined) { - localStorage.setItem(KEYS.endpoint, config.endpoint); - } + if (config.endpoint !== undefined) { + config.endpoint ? localStorage.setItem(KEYS.endpoint, config.endpoint) + : localStorage.removeItem(KEYS.endpoint); + } @@ - if (config.toolAllowlist) { - localStorage.setItem(KEYS.allowlist, JSON.stringify(config.toolAllowlist)); - } + if (config.toolAllowlist !== undefined) { + config.toolAllowlist?.length + ? localStorage.setItem(KEYS.allowlist, JSON.stringify(config.toolAllowlist)) + : localStorage.removeItem(KEYS.allowlist); + }
33-36: Validate allowlist JSON shape.Defensive check avoids non-string array values.
- if (raw) { - try { toolAllowlist = JSON.parse(raw); } catch { toolAllowlist = undefined; } - } + if (raw) { + try { + const parsed = JSON.parse(raw); + toolAllowlist = Array.isArray(parsed) && parsed.every(x => typeof x === 'string') ? parsed : undefined; + } catch { + toolAllowlist = undefined; + } + }
54-56: Follow-up offer: secure storage abstraction.I can add a small SecureStore (session/in-memory today; pluggable for OS keychain/extension storage later) and wire token through it.
front_end/panels/ai_chat/mcp/MCPRegistry.ts (2)
12-18: Member delimiter style in interfaces.Project lint expects commas between members; update MCPRegistryStatus to use commas.
export interface MCPRegistryStatus { - enabled: boolean; - servers: Array<{ id: string; endpoint: string; connected: boolean; toolCount: number }>; - registeredToolNames: string[]; - lastError?: string; - lastErrorType?: 'connection' | 'authentication' | 'configuration' | 'network' | 'server_error' | 'unknown'; - lastConnected?: Date; - lastDisconnected?: Date; + enabled: boolean, + servers: Array<{ id: string; endpoint: string; connected: boolean; toolCount: number }>, + registeredToolNames: string[], + lastError?: string, + lastErrorType?: 'connection' | 'authentication' | 'configuration' | 'network' | 'server_error' | 'unknown', + lastConnected?: Date, + lastDisconnected?: Date, }
29-48: Minor: trim trailing spaces and standardize logger error shape.Nits but they do trip style checks; also prefer structured logs consistently: logger.error('MCP connect failed', { err }).
front_end/panels/ai_chat/mcp/MCPMetaTools.ts (3)
5-11: Reorder imports and avoid direct UI dependency at load time to reduce cycles.Defer AIChatPanel import inside rankWithLLM; group imports to satisfy lint.
-import type { Tool } from '../tools/Tools.js'; -import { createLogger } from '../core/Logger.js'; -import { MCPRegistry } from './MCPRegistry.js'; -import { MCPToolAdapter } from './MCPToolAdapter.js'; -import { LLMClient } from '../LLM/LLMClient.js'; -import { AIChatPanel } from '../ui/AIChatPanel.js'; +import { createLogger } from '../core/Logger.js'; +import type { Tool } from '../tools/Tools.js'; +import { MCPRegistry } from './MCPRegistry.js'; +import { MCPToolAdapter } from './MCPToolAdapter.js'; +import { LLMClient } from '../LLM/LLMClient.js';And later inside rankWithLLM:
- const { model, provider } = AIChatPanel.getNanoModelWithProvider(); + const { AIChatPanel } = await import('../ui/AIChatPanel.js'); + const { model, provider } = AIChatPanel.getNanoModelWithProvider();
100-114: Remove unused var and small scoring tidy.Drop q; tiny cleanup for readability.
- const q = (args.query || '').toLowerCase();
48-56: Use object shorthand for systemPrompt.Satisfies lint and reduces duplication.
- const response = await llm.call({ + const response = await llm.call({ provider, model, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userMessage } ], - systemPrompt: systemPrompt, + systemPrompt,front_end/panels/ai_chat/core/ToolSurfaceProvider.ts (2)
86-104: Make debug testMode mutate MCP config, not localStorage
testMode()writesai_chat_mcp_tool_modedirectly to localStorage which may not be read bygetMCPConfig(). UsesetMCPConfig()so behavior matches production.Apply:
- testMode: async (mode: 'all' | 'router' | 'meta') => { - const originalConfig = getMCPConfig(); - console.log(`Testing mode: ${mode}`); - // Temporarily set the mode - localStorage.setItem('ai_chat_mcp_tool_mode', mode); + testMode: async (mode: 'all' | 'router' | 'meta') => { + const originalConfig = getMCPConfig(); + console.log(`Testing mode: ${mode}`); + // Temporarily set the mode in MCP config + setMCPConfig({ ...originalConfig, toolMode: mode }); @@ - // Restore original mode - if (originalConfig.toolMode) { - localStorage.setItem('ai_chat_mcp_tool_mode', originalConfig.toolMode); - } + // Restore original mode + setMCPConfig(originalConfig);Also update imports:
- import { getMCPConfig } from '../mcp/MCPConfig.js'; + import { getMCPConfig, setMCPConfig } from '../mcp/MCPConfig.js';
118-128: Use the logger instead of console for structured debug outputYou already have
createLogger. Replaceconsole.log/errorwithlogger.debug/info/warn/errorfor consistency and log filtering.If you’d like, I can send a follow-up patch swapping these calls in one pass.
Also applies to: 139-147, 149-162, 186-190, 194-207, 209-214
front_end/panels/ai_chat/ui/SettingsDialog.ts (1)
434-434: Trim trailing space (lint error)Line 434 has a trailing space after the string literal.
- mcpMaxMcpPerTurn: 'Max MCP Tools Per Turn', + mcpMaxMcpPerTurn: 'Max MCP Tools Per Turn',
config/gni/devtools_grd_files.gni
Outdated
| "front_end/third_party/mcp-sdk/ajv/dist/ajv.js", | ||
| "front_end/third_party/mcp-sdk/zod/lib/index.js", | ||
| "front_end/third_party/mcp-sdk/zod/lib/index.mjs", | ||
| "front_end/third_party/mcp-sdk/eventsource-parser/package/dist/index.js", | ||
| "front_end/third_party/mcp-sdk/eventsource-parser/package/dist/stream.js", | ||
| "front_end/third_party/mcp-sdk/mcp-sdk.js", | ||
| "front_end/third_party/mcp-sdk/package/dist/client/index.js", | ||
| "front_end/third_party/mcp-sdk/package/dist/client/sse.js", | ||
| "front_end/third_party/mcp-sdk/package/dist/shared/protocol.js", | ||
| "front_end/third_party/mcp-sdk/package/dist/shared/transport.js", | ||
| "front_end/third_party/mcp-sdk/package/dist/types.js", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Vendored MCP SDK: verify licenses, trim duplicates, and keep list ordering tidy
- Please ensure front_end/third_party/mcp-sdk includes appropriate OSS metadata (LICENSE/README.chromium).
- Consider keeping the third_party block alphabetically ordered; mcp-sdk should come after marked.
- You list both zod/lib/index.js and zod/lib/index.mjs; if only one is referenced by the SDK, drop the other to save bytes.
Run this to verify quickly:
Expected:
- All files exist; each appears exactly once in the GRD file.
- At least one license/README.chromium-like file is present under front_end/third_party/mcp-sdk.
- Only one of zod/lib/index.(mjs|js) is referenced; if so, remove the unused one from the GRD.
- Optional: reorder the mcp-sdk block to maintain alphabetical order in the third_party section.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Confirm all newly added files exist and are not duplicated across lists.
gni=config/gni/devtools_grd_files.gni
new_files=(
"front_end/panels/ai_chat/core/ToolSurfaceProvider.js"
"front_end/panels/ai_chat/mcp/MCPConfig.js"
"front_end/panels/ai_chat/mcp/MCPRegistry.js"
"front_end/panels/ai_chat/mcp/MCPToolAdapter.js"
"front_end/panels/ai_chat/mcp/MCPMetaTools.js"
"front_end/panels/ai_chat/tools/LLMTracingWrapper.js"
"front_end/third_party/mcp-sdk/ajv/dist/ajv.js"
"front_end/third_party/mcp-sdk/zod/lib/index.js"
"front_end/third_party/mcp-sdk/zod/lib/index.mjs"
"front_end/third_party/mcp-sdk/eventsource-parser/package/dist/index.js"
"front_end/third_party/mcp-sdk/eventsource-parser/package/dist/stream.js"
"front_end/third_party/mcp-sdk/mcp-sdk.js"
"front_end/third_party/mcp-sdk/package/dist/client/index.js"
"front_end/third_party/mcp-sdk/package/dist/client/sse.js"
"front_end/third_party/mcp-sdk/package/dist/shared/protocol.js"
"front_end/third_party/mcp-sdk/package/dist/shared/transport.js"
"front_end/third_party/mcp-sdk/package/dist/types.js"
)
echo "Checking file existence..."
missing=0
for f in "${new_files[@]}"; do
if [ ! -f "$f" ]; then echo "MISSING: $f"; missing=1; fi
done
[ "$missing" -eq 0 ] && echo "All files exist."
echo "Checking duplicates across GRD lists..."
dups=0
for f in "${new_files[@]}"; do
c=$(rg -n --fixed-strings "$f" "$gni" | wc -l | tr -d ' ')
if [ "$c" -ne 1 ]; then echo "DUPLICATE/ABSENT in GRD lists: $f (count=$c)"; dups=1; fi
done
[ "$dups" -eq 0 ] && echo "No duplicates found in GRD lists."
# 2) License metadata in vendored SDK.
echo "Scanning for OSS metadata under mcp-sdk..."
fd -HI -t f '(LICENSE|COPYING|NOTICE|README\.chromium)$' front_end/third_party/mcp-sdk || true
# 3) Check if both zod entrypoints are actually referenced by the vendored SDK.
echo "Inspecting SDK imports for zod entrypoint usage..."
rg -n 'zod/lib/index\.(mjs|js)' front_end/third_party/mcp-sdk || true
# 4) Estimate total size impact of the vendored SDK block.
echo "Size of front_end/third_party/mcp-sdk:"
du -sh front_end/third_party/mcp-sdk || trueLength of output: 8107
config/gni/devtools_grd_files.gni: fix missing references & clean up vendored MCP SDK block
-
7 GRD entries reference non-existent files; either add these sources or remove their lines:
- front_end/panels/ai_chat/core/ToolSurfaceProvider.js
- front_end/panels/ai_chat/mcp/MCPConfig.js
- front_end/panels/ai_chat/mcp/MCPRegistry.js
- front_end/panels/ai_chat/mcp/MCPToolAdapter.js
- front_end/panels/ai_chat/mcp/MCPMetaTools.js
- front_end/panels/ai_chat/tools/LLMTracingWrapper.js
- front_end/third_party/mcp-sdk/mcp-sdk.js
-
Vendored mcp-sdk already includes OSS metadata (LICENSE, README.chromium).
-
Only
zod/lib/index.mjsis actually imported by the SDK; dropzod/lib/index.jsfrom the GRD to save bytes. -
Optional: reorder the mcp-sdk file list alphabetically within the third_party section.
| import { createToolExecutorNode } from './AgentNodes.js'; | ||
| import type { AgentState } from './State.js'; | ||
| import type { Tool } from '../tools/Tools.js'; | ||
| import { ChatMessageEntity } from '../ui/ChatView.js'; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix missing assertion import
assert is used but never imported; tests will throw ReferenceError. Add Chai assert import.
import { createToolExecutorNode } from './AgentNodes.js';
import type { AgentState } from './State.js';
import type { Tool } from '../tools/Tools.js';
import { ChatMessageEntity } from '../ui/ChatView.js';
+import { assert } from 'chai';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { createToolExecutorNode } from './AgentNodes.js'; | |
| import type { AgentState } from './State.js'; | |
| import type { Tool } from '../tools/Tools.js'; | |
| import { ChatMessageEntity } from '../ui/ChatView.js'; | |
| import { createToolExecutorNode } from './AgentNodes.js'; | |
| import type { AgentState } from './State.js'; | |
| import type { Tool } from '../tools/Tools.js'; | |
| import { ChatMessageEntity } from '../ui/ChatView.js'; | |
| import { assert } from 'chai'; |
🧰 Tools
🪛 ESLint
[error] 5-5: Incorrect same-namespace import: "./AgentNodes.js". Use "import * as Core from './core.js';" instead.
(rulesdir/es-modules-import)
[error] 6-6: There should be at least one empty line between import groups
(import/order)
[error] 7-7: ../tools/Tools.js type import should occur before import of ./AgentNodes.js
(import/order)
[error] 8-8: ../ui/ChatView.js import should occur before import of ./AgentNodes.js
(import/order)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/ToolNameMapping.test.ts around lines 5 to 9,
the test uses assert but never imports it; add an import for Chai's assert
(e.g., import { assert } from 'chai') at the top of the file so the tests no
longer throw a ReferenceError and assertions work correctly.
| import { ToolSurfaceProvider } from './ToolSurfaceProvider.js'; | ||
| import type { AgentState } from './State.js'; | ||
| import type { Tool } from '../tools/Tools.js'; | ||
| import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js'; | ||
| import { MCPRegistry } from '../mcp/MCPRegistry.js'; | ||
| import { registerMCPMetaTools } from '../mcp/MCPMetaTools.js'; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Import assertions and ChatMessageEntity for typed usage
Add Chai assert and use ChatMessageEntity.USER instead of 0 as any.
import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
import type { AgentState } from './State.js';
import type { Tool } from '../tools/Tools.js';
import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
import { MCPRegistry } from '../mcp/MCPRegistry.js';
import { registerMCPMetaTools } from '../mcp/MCPMetaTools.js';
+import { assert } from 'chai';
+import { ChatMessageEntity } from '../ui/ChatView.js';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { ToolSurfaceProvider } from './ToolSurfaceProvider.js'; | |
| import type { AgentState } from './State.js'; | |
| import type { Tool } from '../tools/Tools.js'; | |
| import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js'; | |
| import { MCPRegistry } from '../mcp/MCPRegistry.js'; | |
| import { registerMCPMetaTools } from '../mcp/MCPMetaTools.js'; | |
| import { ToolSurfaceProvider } from './ToolSurfaceProvider.js'; | |
| import type { AgentState } from './State.js'; | |
| import type { Tool } from '../tools/Tools.js'; | |
| import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js'; | |
| import { MCPRegistry } from '../mcp/MCPRegistry.js'; | |
| import { registerMCPMetaTools } from '../mcp/MCPMetaTools.js'; | |
| import { assert } from 'chai'; | |
| import { ChatMessageEntity } from '../ui/ChatView.js'; |
🧰 Tools
🪛 ESLint
[error] 5-5: Incorrect same-namespace import: "./ToolSurfaceProvider.js". Use "import * as Core from './core.js';" instead.
(rulesdir/es-modules-import)
[error] 5-5: ./ToolSurfaceProvider.js import should occur after import of ../mcp/MCPMetaTools.js
(import/order)
[error] 6-6: There should be at least one empty line between import groups
(import/order)
[error] 6-6: ./State.js type import should occur after import of ../mcp/MCPMetaTools.js
(import/order)
[error] 7-7: ../tools/Tools.js type import should occur after import of ../mcp/MCPMetaTools.js
(import/order)
[error] 9-9: ../mcp/MCPRegistry.js import should occur after import of ../mcp/MCPMetaTools.js
(import/order)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/ToolSurfaceProvider.test.ts around lines 5 to
11, the test currently lacks a Chai assert import and uses a raw numeric cast (0
as any) for the message role; import assert from 'chai' and import
ChatMessageEntity (the enum/constant) from its module (where ChatMessageEntity
is defined) and replace usages of 0 as any with ChatMessageEntity.USER so the
test uses typed role values and real assertions.
| function getAllMcpTools(): Tool<any, any>[] { | ||
| try { | ||
| const status = MCPRegistry.getStatus(); | ||
| console.log('[TOOL_SELECTION_DEBUG] MCPRegistry status:', { | ||
| enabled: status.enabled, | ||
| serverCount: status.servers.length, | ||
| servers: status.servers, | ||
| registeredToolNames: status.registeredToolNames, | ||
| lastError: status.lastError, | ||
| lastErrorType: status.lastErrorType | ||
| }); | ||
|
|
||
| const tools: Tool<any, any>[] = []; | ||
| for (const name of status.registeredToolNames) { | ||
| const tool = ToolRegistry.getRegisteredTool(name); | ||
| if (tool) { | ||
| tools.push(tool); | ||
| } else { | ||
| console.log('[TOOL_SELECTION_DEBUG] Tool registered but not found:', name); | ||
| } | ||
| } | ||
| console.log('[TOOL_SELECTION_DEBUG] getAllMcpTools result:', { | ||
| availableToolsCount: tools.length, | ||
| availableToolNames: tools.map(t => t.name) | ||
| }); | ||
| return tools; | ||
| } catch (error) { | ||
| console.error('[TOOL_SELECTION_DEBUG] Error in getAllMcpTools:', error); | ||
| return []; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Respect MCP tool allowlist when building the tool list
Currently, getAllMcpTools() returns all registered tools and ignores the saved allowlist. This can surface tools users explicitly disabled.
Apply:
function getAllMcpTools(): Tool<any, any>[] {
try {
const status = MCPRegistry.getStatus();
@@
- const tools: Tool<any, any>[] = [];
+ const tools: Tool<any, any>[] = [];
+ const cfg = getMCPConfig();
+ const allow = new Set(cfg.toolAllowlist || []);
@@
- for (const name of status.registeredToolNames) {
+ for (const name of status.registeredToolNames) {
const tool = ToolRegistry.getRegisteredTool(name);
- if (tool) {
- tools.push(tool);
+ if (tool) {
+ if (allow.size === 0 || allow.has(tool.name)) {
+ tools.push(tool);
+ }
} else {
console.log('[TOOL_SELECTION_DEBUG] Tool registered but not found:', name);
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function getAllMcpTools(): Tool<any, any>[] { | |
| try { | |
| const status = MCPRegistry.getStatus(); | |
| console.log('[TOOL_SELECTION_DEBUG] MCPRegistry status:', { | |
| enabled: status.enabled, | |
| serverCount: status.servers.length, | |
| servers: status.servers, | |
| registeredToolNames: status.registeredToolNames, | |
| lastError: status.lastError, | |
| lastErrorType: status.lastErrorType | |
| }); | |
| const tools: Tool<any, any>[] = []; | |
| for (const name of status.registeredToolNames) { | |
| const tool = ToolRegistry.getRegisteredTool(name); | |
| if (tool) { | |
| tools.push(tool); | |
| } else { | |
| console.log('[TOOL_SELECTION_DEBUG] Tool registered but not found:', name); | |
| } | |
| } | |
| console.log('[TOOL_SELECTION_DEBUG] getAllMcpTools result:', { | |
| availableToolsCount: tools.length, | |
| availableToolNames: tools.map(t => t.name) | |
| }); | |
| return tools; | |
| } catch (error) { | |
| console.error('[TOOL_SELECTION_DEBUG] Error in getAllMcpTools:', error); | |
| return []; | |
| } | |
| } | |
| function getAllMcpTools(): Tool<any, any>[] { | |
| try { | |
| const status = MCPRegistry.getStatus(); | |
| console.log('[TOOL_SELECTION_DEBUG] MCPRegistry status:', { | |
| enabled: status.enabled, | |
| serverCount: status.servers.length, | |
| servers: status.servers, | |
| registeredToolNames: status.registeredToolNames, | |
| lastError: status.lastError, | |
| lastErrorType: status.lastErrorType | |
| }); | |
| const tools: Tool<any, any>[] = []; | |
| const cfg = getMCPConfig(); | |
| const allow = new Set(cfg.toolAllowlist || []); | |
| for (const name of status.registeredToolNames) { | |
| const tool = ToolRegistry.getRegisteredTool(name); | |
| if (tool) { | |
| if (allow.size === 0 || allow.has(tool.name)) { | |
| tools.push(tool); | |
| } | |
| } else { | |
| console.log('[TOOL_SELECTION_DEBUG] Tool registered but not found:', name); | |
| } | |
| } | |
| console.log('[TOOL_SELECTION_DEBUG] getAllMcpTools result:', { | |
| availableToolsCount: tools.length, | |
| availableToolNames: tools.map(t => t.name) | |
| }); | |
| return tools; | |
| } catch (error) { | |
| console.error('[TOOL_SELECTION_DEBUG] Error in getAllMcpTools:', error); | |
| return []; | |
| } | |
| } |
| const updateDisconnectButton = () => { | ||
| const status = MCPRegistry.getStatus(); | ||
| const anyConnected = status.enabled && status.servers.some(s => s.connected); | ||
| mcpDisconnectContainer.style.display = anyConnected ? 'block' : 'none'; | ||
| }; | ||
| updateDisconnectButton(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add explicit return type (ESLint: explicit-function-return-type)
updateDisconnectButton should be annotated.
- const updateDisconnectButton = () => {
+ const updateDisconnectButton = (): void => {
const status = MCPRegistry.getStatus();
const anyConnected = status.enabled && status.servers.some(s => s.connected);
mcpDisconnectContainer.style.display = anyConnected ? 'block' : 'none';
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const updateDisconnectButton = () => { | |
| const status = MCPRegistry.getStatus(); | |
| const anyConnected = status.enabled && status.servers.some(s => s.connected); | |
| mcpDisconnectContainer.style.display = anyConnected ? 'block' : 'none'; | |
| }; | |
| updateDisconnectButton(); | |
| const updateDisconnectButton = (): void => { | |
| const status = MCPRegistry.getStatus(); | |
| const anyConnected = status.enabled && status.servers.some(s => s.connected); | |
| mcpDisconnectContainer.style.display = anyConnected ? 'block' : 'none'; | |
| }; | |
| updateDisconnectButton(); |
🧰 Tools
🪛 ESLint
[error] 2579-2579: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/ui/SettingsDialog.ts around lines 2579 to 2584, the
arrow function updateDisconnectButton lacks an explicit return type which
triggers the ESLint rule explicit-function-return-type; add an explicit void
return type by annotating the function as const updateDisconnectButton = ():
void => { ... } and keep the rest of the implementation and the
updateDisconnectButton() invocation unchanged.
| // Handle budget control changes | ||
| const updateBudgetControls = () => { | ||
| const maxTools = Math.max(1, Math.min(100, parseInt(mcpMaxToolsInput.value, 10) || 20)); | ||
| const maxMcp = Math.max(1, Math.min(50, parseInt(mcpMaxMcpInput.value, 10) || 8)); | ||
| setMCPConfig({ | ||
| ...getMCPConfig(), | ||
| maxToolsPerTurn: maxTools, | ||
| maxMcpPerTurn: maxMcp, | ||
| }); | ||
| onSettingsSaved(); | ||
| }; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add explicit return type and clamp logic remains unchanged
Annotate updateBudgetControls.
- const updateBudgetControls = () => {
+ const updateBudgetControls = (): void => {
const maxTools = Math.max(1, Math.min(100, parseInt(mcpMaxToolsInput.value, 10) || 20));
const maxMcp = Math.max(1, Math.min(50, parseInt(mcpMaxMcpInput.value, 10) || 8));
setMCPConfig({
...getMCPConfig(),
maxToolsPerTurn: maxTools,
maxMcpPerTurn: maxMcp,
});
onSettingsSaved();
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Handle budget control changes | |
| const updateBudgetControls = () => { | |
| const maxTools = Math.max(1, Math.min(100, parseInt(mcpMaxToolsInput.value, 10) || 20)); | |
| const maxMcp = Math.max(1, Math.min(50, parseInt(mcpMaxMcpInput.value, 10) || 8)); | |
| setMCPConfig({ | |
| ...getMCPConfig(), | |
| maxToolsPerTurn: maxTools, | |
| maxMcpPerTurn: maxMcp, | |
| }); | |
| onSettingsSaved(); | |
| }; | |
| // Handle budget control changes | |
| const updateBudgetControls = (): void => { | |
| const maxTools = Math.max(1, Math.min(100, parseInt(mcpMaxToolsInput.value, 10) || 20)); | |
| const maxMcp = Math.max(1, Math.min(50, parseInt(mcpMaxMcpInput.value, 10) || 8)); | |
| setMCPConfig({ | |
| ...getMCPConfig(), | |
| maxToolsPerTurn: maxTools, | |
| maxMcpPerTurn: maxMcp, | |
| }); | |
| onSettingsSaved(); | |
| }; |
🧰 Tools
🪛 ESLint
[error] 2758-2758: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/ui/SettingsDialog.ts around lines 2757 to 2768,
annotate the updateBudgetControls function with an explicit return type (void)
while leaving the existing clamp/parsing logic unchanged; update the function
signature to declare its return type and ensure any local variables keep their
current behavior, then rebuild/confirm type checks pass.
| const updateToolsList = () => { | ||
| const status = MCPRegistry.getStatus(); | ||
| mcpToolsList.innerHTML = ''; | ||
|
|
||
| if (!status.enabled || status.registeredToolNames.length === 0) { | ||
| const noToolsMessage = document.createElement('div'); | ||
| noToolsMessage.className = 'mcp-no-tools'; | ||
| noToolsMessage.textContent = i18nString(UIStrings.mcpNoTools); | ||
| mcpToolsList.appendChild(noToolsMessage); | ||
| return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use safe clearing and read fresh config for allowlist
- Avoid
innerHTML = ''for clearing. - Use the latest MCP config when computing the allowlist (the outer
currentMCPConfigmay be stale).
- const updateToolsList = () => {
+ const updateToolsList = (): void => {
const status = MCPRegistry.getStatus();
- mcpToolsList.innerHTML = '';
+ mcpToolsList.replaceChildren();
@@
- const currentAllowlist = new Set(currentMCPConfig.toolAllowlist || []);
+ const { toolAllowlist } = getMCPConfig();
+ const currentAllowlist = new Set(toolAllowlist || []);Also applies to: 2803-2814
🧰 Tools
🪛 ast-grep (0.38.6)
[warning] 2792-2792: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: mcpToolsList.innerHTML = ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html
(dom-content-modification)
🪛 ESLint
[error] 2791-2791: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
[error] 2794-2794: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
| # EventSource parser dependency | ||
| "eventsource-parser/package/dist/index.d.ts", | ||
| "eventsource-parser/package/dist/index.js", | ||
| "eventsource-parser/package/dist/stream.d.ts", | ||
| "eventsource-parser/package/dist/stream.js", | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Confirm eventsource-parser stream.js is actually needed in browser bundle
stream.js is typically Node-focused; if unused in DevTools, drop it to reduce risk and size.
🏁 Script executed:
#!/bin/bash
rg -nP "eventsource-parser/package/dist/(stream|index)\.js" -C1 --type=ts --type=jsLength of output: 85
🏁 Script executed:
#!/bin/bash
# Search for any direct imports or requires of eventsource-parser (with or without /package/dist),
# plus usage of createParser and stream() in TS/JS code
rg -nP "from ['\"]eventsource-parser(/package/dist)?['\"]" -C3 --type=ts --type=js
rg -nP "require\(['\"]eventsource-parser(/package/dist)?['\"]" -C3 --type=ts --type=js
rg -nP "createParser" -C3 --type=ts --type=js
rg -nP "stream\(" -C3 --type=ts --type=jsLength of output: 8137
Remove unused eventsource-parser stream files from BUILD.gn
front_end/third_party/mcp-sdk/BUILD.gn: drop "eventsource-parser/package/dist/stream.js" (and its .d.ts), as no front_end code imports or uses them.
🤖 Prompt for AI Agents
In front_end/third_party/mcp-sdk/BUILD.gn around lines 39 to 44, the BUILD file
lists eventsource-parser stream files that are unused; remove the entries
"eventsource-parser/package/dist/stream.js" and
"eventsource-parser/package/dist/stream.d.ts" from the file list so only the
actually imported files remain, then run a quick build or lint to ensure no
breakage.
|
Claude finished @tysonthomas9's task —— View job Comprehensive PR Review CompleteI've conducted a thorough review of this MCP integration PR. Here's my detailed feedback: 🔴 Critical Security Issues1. Token Storage Vulnerability (HIGH)File: localStorage.setItem(KEYS.token, config.token);Issue: Storing authentication tokens in 2. Tool Argument Redaction Bug (CRITICAL)File: const sanitized = this.sanitize(args);
return this.client.callTool(this.serverId, this.def.name, sanitized, { timeoutMs: 30000 });Issue: The code redacts secrets from arguments then sends the redacted version to MCP server, breaking authentication for any tool requiring API keys/tokens. 3. XSS Risk in Settings Dialog (MEDIUM)File: mcpStatusDetails.innerHTML = detailsHtml;Issue: Using 🔴 Critical Functionality Issues4. LLM Function Name Compatibility (BLOCKER)File: tools: tools.map(tool => ({
function: {
name: tool.name, // Names like "mcp:default:alpha" will be rejected by OpenAI
description: tool.description,
parameters: tool.schema,
}
}))Issue: MCP tool names containing colons/dots (e.g., 5. Integration Test CI Failure (HIGH)File: describe('connects to local Hacker News MCP server via SDK', function() {
const HACKER_NEWS_SERVER: MCPServer = {
endpoint: 'http://localhost:5001/sse',
};Issue: This integration test will fail/hang in CI where no local MCP server runs.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (4)
front_end/panels/ai_chat/ui/AIChatPanel.ts (4)
788-812: Add explicit return type and null-safe guards; optional debounce to avoid overlap.
- Type the inner async fn to satisfy explicit-function-return-type.
- Guard against undefined config using optional chaining.
Apply:
#setupMCPIntegration(): void { - const initAndRefresh = async () => { + const initAndRefresh = async (): Promise<void> => { try { - const mcpConfig = getMCPConfig(); + const mcpConfig = getMCPConfig(); // Only auto-connect if both enabled and autostart are true - if (mcpConfig.enabled && mcpConfig.autostart) { + if (mcpConfig?.enabled && mcpConfig?.autostart) { await MCPRegistry.init(); await MCPRegistry.refresh(); const status = MCPRegistry.getStatus(); logger.info('MCP autostart completed', status); } } catch (err) { logger.error('Failed to initialize MCP', err); } }; void initAndRefresh(); // Subscribe to config changes this.#mcpUnsubscribe = onMCPConfigChange(() => { void initAndRefresh(); }); }
2269-2274: Gate MCP re-init behind config.enabled.Avoids initializing when MCP is disabled in Settings.
Apply:
- try { - await MCPRegistry.init(); - await MCPRegistry.refresh(); - } catch (err) { + try { + const cfg = getMCPConfig(); + if (cfg?.enabled) { + await MCPRegistry.init(); + await MCPRegistry.refresh(); + } else { + logger.info('MCP disabled; skipping re-init after settings change'); + } + } catch (err) { logger.error('Failed to reinitialize MCP after settings change', err); - } + }
87-90: Move MCP imports to the top import block and de-duplicate (ESLint blockers).Currently after executable code (StorageMonitor) and duplicated from MCPConfig; violates import/first and import/no-duplicates.
Apply:
-// MCP integration -import { MCPRegistry } from '../mcp/MCPRegistry.js'; -import { getMCPConfig } from '../mcp/MCPConfig.js'; -import { onMCPConfigChange } from '../mcp/MCPConfig.js'; +// (moved near other imports at file top)And add alongside other imports at the top:
+// MCP integration +import { MCPRegistry } from '../mcp/MCPRegistry.js'; +import { getMCPConfig, onMCPConfigChange } from '../mcp/MCPConfig.js';
721-721: Use the unsubscribe handle and clear it on teardown.Prevents leaks when panel hides multiple times.
Apply:
override willHide(): void { @@ if (this.#boundOnChildAgentStarted) this.#agentService.removeEventListener(AgentEvents.CHILD_AGENT_STARTED, this.#boundOnChildAgentStarted); + if (this.#mcpUnsubscribe) { + this.#mcpUnsubscribe(); + this.#mcpUnsubscribe = null; + } }
🧹 Nitpick comments (2)
front_end/panels/ai_chat/BUILD.gn (2)
139-139: Ensure unittests bring in the MCP SDK bundle.If tests or MCP modules transitively import the SDK, ts_library("unittests") may need an explicit dep on ../../third_party/mcp-sdk:bundle to satisfy TypeScript/rollup resolution.
Apply if needed:
ts_library("unittests") { testonly = true @@ deps = [ ":ai_chat", "../../testing", "../../core/sdk:bundle", "../../generated:protocol", + "../../third_party/mcp-sdk:bundle", ] }
346-352: Tests added: consider SDK dep and visibility.
- If MCPClientSDK.test.ts or ToolSurfaceProvider tests import SDK types at runtime/compile-time, add ../../third_party/mcp-sdk:bundle to unittests deps (see earlier diff).
- Confirm test target visibility allows importing these new paths.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
config/gni/devtools_grd_files.gni(3 hunks)front_end/panels/ai_chat/BUILD.gn(6 hunks)front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts(1 hunks)front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts(2 hunks)front_end/panels/ai_chat/core/AgentNodes.ts(3 hunks)front_end/panels/ai_chat/ui/AIChatPanel.ts(5 hunks)front_end/panels/ai_chat/ui/ChatView.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- front_end/panels/ai_chat/ui/ChatView.ts
- front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts
- front_end/panels/ai_chat/core/AgentNodes.ts
- config/gni/devtools_grd_files.gni
🧰 Additional context used
🪛 ESLint
front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts
[error] 22-22: ../../mcp/MCPMetaTools.js import should occur before import of ../../tools/FetcherTool.js
(import/order)
🔇 Additional comments (5)
front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts (1)
98-99: WrapregisterMCPMetaToolsin try/catch// Ensure MCP meta-tools are available regardless of mode; selection logic decides if they are surfaced - registerMCPMetaTools(); + try { + registerMCPMetaTools(); // should be safe/no-op on repeated calls + } catch (err) { + console.warn('MCP meta-tools registration failed; continuing without MCP meta-tools.', err); + }Confirm that
registerMCPMetaToolsis idempotent and cannot throw under normal conditions to avoid duplicate factory registrations or fatal failures.front_end/panels/ai_chat/BUILD.gn (3)
58-58: ToolSurfaceProvider is correctly surfaced in build and metadata.Present in both devtools_module sources and _ai_chat_sources; will participate in GRD metadata. LGTM.
126-130: MCP sources added to module are consistent with metadata list.MCPConfig, MCPToolAdapter, MCPRegistry, MCPMetaTools are wired in; mirrors below in _ai_chat_sources. LGTM.
192-192: Metadata list mirrors new sources.ToolSurfaceProvider and MCP modules are included in _ai_chat_sources; JS outputs will be emitted for GRD checks. LGTM.
Also applies to: 259-263
front_end/panels/ai_chat/ui/AIChatPanel.ts (1)
752-754: Constructor hook for MCP setup — good placement.Runs after core wiring and model fetch; appropriate.
| import { WaitTool } from '../../tools/Tools.js'; | ||
| import { ThinkingTool } from '../../tools/ThinkingTool.js'; | ||
| import type { Tool } from '../../tools/Tools.js'; | ||
| import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix ESLint import/order violation for MCP import (must come before tools imports).
ESLint flags this new import placement. Move it above ../../tools/FetcherTool.js (i.e., to the top of the internal import block). Also avoid duplicate imports after reordering.
Apply within this hunk (remove misplaced import):
- import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js';Place this at the top of the import section (before any ../../tools/* imports):
import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js';
import { FetcherTool } from '../../tools/FetcherTool.js';
import { FinalizeWithCritiqueTool } from '../../tools/FinalizeWithCritiqueTool.js';
...Optional: If bundle size is a concern, consider lazy-loading MCP meta-tools (dynamic import) guarded by settings/allowlist in a follow-up, but that would require adjusting call sites to handle async.
🧰 Tools
🪛 ESLint
[error] 22-22: ../../mcp/MCPMetaTools.js import should occur before import of ../../tools/FetcherTool.js
(import/order)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts
around line 22, the import for registerMCPMetaTools is placed after tools
imports causing an ESLint import/order violation; move the registerMCPMetaTools
import to the top of the internal import block so it comes before any
../../tools/* imports, remove any duplicate imports that result from the
reorder, and ensure the tools (e.g., FetcherTool, FinalizeWithCritiqueTool,
etc.) remain imported after this MCP import; optionally consider converting the
MCP import to a dynamic import behind a feature flag in a follow-up if bundle
size is a concern.
|
Claude finished @tysonthomas9's task —— View job MCP Integration PR ReviewI've conducted a comprehensive review of this MCP integration PR. Here's my detailed feedback: Review Tasks:
🔴 Critical Security Issues1. Token Storage Vulnerability (HIGH)File: localStorage.setItem(KEYS.token, config.token);Issue: Storing authentication tokens in 2. Tool Argument Redaction Bug (CRITICAL)File: const sanitized = this.sanitize(args);
return this.client.callTool(this.serverId, this.def.name, sanitized, { timeoutMs: 30000 });Issue: The code redacts secrets from arguments then sends the redacted version to MCP server, breaking authentication for any tool requiring API keys/tokens. 3. XSS Risk in Settings Dialog (MEDIUM)File: mcpStatusDetails.innerHTML = detailsHtml;Issue: Using 🔴 Critical Functionality Issues4. LLM Function Name Compatibility (BLOCKER)File: tools: tools.map(tool => ({
function: {
name: tool.name, // Names like "mcp:default:alpha" will be rejected by OpenAI
description: tool.description,
parameters: tool.schema,
}
}))Issue: MCP tool names containing colons/dots (e.g., 5. Integration Test CI Failure (HIGH)File: describe('connects to local Hacker News MCP server via SDK', function() {
const HACKER_NEWS_SERVER: MCPServer = {
endpoint: 'http://localhost:5001/sse',
};Issue: This integration test will fail/hang in CI where no local MCP server runs.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
front_end/panels/ai_chat/ui/AIChatPanel.ts (2)
51-61: Stop logging secret values from localStorage (risk of secret leakage).
StorageMonitorlogs previews of values for keys that include credentials. Remove value previews, and avoid logging raw content.- logger.debug(`Value exists: ${!!value}`); - logger.debug(`Value length: ${value?.length || 0}`); - logger.debug(`Value preview: ${value?.substring(0, 50) + (value?.length > 50 ? '...' : '') || 'null'}`); + logger.debug(`Value exists: ${!!value}`); + logger.debug(`Value length: ${value?.length || 0}`); logger.debug(`Timestamp: ${new Date().toISOString()}`);Optional hardening (outside this hunk): initialize
StorageMonitoronly under a debug flag to avoid monkey‑patching in production. I can wire that if you want.Also applies to: 64-71
1507-1512: Don’t log API key prefixes.Leaking even partial secrets is risky and often forbidden by policy. Remove the prefix log.
- logger.info('Retrieved API key:'); - logger.info('- Exists:', !!apiKey); - logger.info('- Length:', apiKey?.length || 0); - logger.info('- Prefix:', apiKey?.substring(0, 8) + '...' || 'none'); + logger.info('Retrieved API key:'); + logger.info('- Exists:', !!apiKey); + logger.info('- Length:', apiKey?.length || 0);
♻️ Duplicate comments (12)
front_end/panels/ai_chat/ui/AIChatPanel.ts (4)
1956-1966: Use the MCP unsubscribe handle during teardown to prevent leaks.Invoke and clear
#mcpUnsubscribeinwillHide.override willHide(): void { // Explicitly remove any event listeners to prevent memory leaks if (this.#boundOnMessagesChanged) { this.#agentService.removeEventListener(AgentEvents.MESSAGES_CHANGED, this.#boundOnMessagesChanged); } if (this.#boundOnAgentSessionStarted) this.#agentService.removeEventListener(AgentEvents.AGENT_SESSION_STARTED, this.#boundOnAgentSessionStarted); if (this.#boundOnAgentToolStarted) this.#agentService.removeEventListener(AgentEvents.AGENT_TOOL_STARTED, this.#boundOnAgentToolStarted); if (this.#boundOnAgentToolCompleted) this.#agentService.removeEventListener(AgentEvents.AGENT_TOOL_COMPLETED, this.#boundOnAgentToolCompleted); if (this.#boundOnAgentSessionUpdated) this.#agentService.removeEventListener(AgentEvents.AGENT_SESSION_UPDATED, this.#boundOnAgentSessionUpdated); if (this.#boundOnChildAgentStarted) this.#agentService.removeEventListener(AgentEvents.CHILD_AGENT_STARTED, this.#boundOnChildAgentStarted); + if (this.#mcpUnsubscribe) { + this.#mcpUnsubscribe(); + this.#mcpUnsubscribe = null; + } }
88-91: Fix import order and combine duplicate MCP imports (ESLint: import/first, import/order, import/no-duplicates).Move MCP imports into the top import block (before chatView.css.js) and merge MCPConfig imports.
Apply within this block:
-// MCP integration -import { MCPRegistry } from '../mcp/MCPRegistry.js'; -import { getMCPConfig } from '../mcp/MCPConfig.js'; -import { onMCPConfigChange } from '../mcp/MCPConfig.js';Add near the main import group at file top (before
chatView.css.js):// MCP integration import { MCPRegistry } from '../mcp/MCPRegistry.js'; import { getMCPConfig, onMCPConfigChange } from '../mcp/MCPConfig.js';
789-813: Add explicit return type and nullish guards in MCP init helper.Annotate return type and guard config fields to satisfy lint and avoid undefined access.
- #setupMCPIntegration(): void { - const initAndRefresh = async () => { + #setupMCPIntegration(): void { + const initAndRefresh = async (): Promise<void> => { try { - const mcpConfig = getMCPConfig(); + const mcpConfig = getMCPConfig(); // Only auto-connect if both enabled and autostart are true - if (mcpConfig.enabled && mcpConfig.autostart) { + if (mcpConfig?.enabled && mcpConfig?.autostart) { await MCPRegistry.init(); await MCPRegistry.refresh(); const status = MCPRegistry.getStatus(); logger.info('MCP autostart completed', status); } - } catch (err) { - logger.error('Failed to initialize MCP', err); + } catch (err: unknown) { + logger.error('Failed to initialize MCP', err); } };
2331-2336: Gate MCP re-init behind config.enabled after settings change.Avoid initializing when MCP is disabled.
- // Re-initialize MCP based on latest settings - try { - await MCPRegistry.init(); - await MCPRegistry.refresh(); - } catch (err) { + // Re-initialize MCP based on latest settings + try { + const cfg = getMCPConfig(); + if (cfg?.enabled) { + await MCPRegistry.init(); + await MCPRegistry.refresh(); + } + } catch (err) { logger.error('Failed to reinitialize MCP after settings change', err); - } + }front_end/panels/ai_chat/ui/SettingsDialog.ts (6)
538-548: Remove unused catch parameter (ESLint: no-unused-vars).- } catch (e) { + } catch { // Fallback silently to storedProvider }
2653-2658: AnnotateupdateDisconnectButtonreturn type.- const updateDisconnectButton = () => { + const updateDisconnectButton = (): void => { const status = MCPRegistry.getStatus(); const anyConnected = status.enabled && status.servers.some(s => s.connected); mcpDisconnectContainer.style.display = anyConnected ? 'block' : 'none'; };
2831-2845: AnnotateupdateBudgetControlsreturn type.- const updateBudgetControls = () => { + const updateBudgetControls = (): void => { const maxTools = Math.max(1, Math.min(100, parseInt(mcpMaxToolsInput.value, 10) || 20)); const maxMcp = Math.max(1, Math.min(50, parseInt(mcpMaxMcpInput.value, 10) || 8)); setMCPConfig({ ...getMCPConfig(), maxToolsPerTurn: maxTools, maxMcpPerTurn: maxMcp, }); onSettingsSaved(); };
2561-2584: Add braces for single-line ifs and avoid HTML in error strings.Wrap the single-line ifs (curly rule) and return plain text messages from
formatMCPErrorto keep rendering safe.- const formatTimestamp = (date: Date | undefined): string => { - if (!date) return ''; - return date.toLocaleString(); - }; + const formatTimestamp = (date: Date | undefined): string => { + if (!date) { return ''; } + return date.toLocaleString(); + }; - const formatMCPError = (error: string, errorType?: string): string => { - if (!errorType) return error; - - switch (errorType) { + const formatMCPError = (error: string, errorType?: string): string => { + if (!errorType) { return error; } + switch (errorType) { case 'connection': - return `Connection failed: ${error}<br><span style="color: var(--color-text-secondary); font-size: 12px;">Check if the MCP server is running and the endpoint URL is correct.</span>`; + return `Connection failed: ${error}. Hint: Check if the MCP server is running and the endpoint URL is correct.`; case 'authentication': - return `Authentication failed: ${error}<br><span style="color: var(--color-text-secondary); font-size: 12px;">Verify your auth token is correct and has not expired.</span>`; + return `Authentication failed: ${error}. Hint: Verify your auth token is correct and has not expired.`; case 'configuration': - return `Configuration error: ${error}<br><span style="color: var(--color-text-secondary); font-size: 12px;">Check your endpoint URL format (should be ws:// or wss://).</span>`; + return `Configuration error: ${error}. Hint: Check your endpoint URL format (should be ws:// or wss://).`; case 'network': - return `Network error: ${error}<br><span style="color: var(--color-text-secondary); font-size: 12px;">Check your internet connection and firewall settings.</span>`; + return `Network error: ${error}. Hint: Check your internet connection and firewall settings.`; case 'server_error': - return `Server error: ${error}<br><span style="color: var(--color-text-secondary); font-size: 12px;">The MCP server encountered an internal error. Contact the server administrator.</span>`; + return `Server error: ${error}. Hint: The MCP server encountered an internal error. Contact the server administrator.`; default: return error; } };
2585-2624: Render MCP status without innerHTML (XSS risk) and add explicit return type.Build DOM nodes and use
replaceChildren. Also annotate return type.- const updateMCPStatus = () => { + const updateMCPStatus = (): void => { const status = MCPRegistry.getStatus(); if (!status.enabled) { mcpStatusDot.style.backgroundColor = 'var(--color-text-disabled)'; mcpStatusText.textContent = 'Disabled'; mcpStatusText.style.color = 'var(--color-text-disabled)'; - mcpStatusDetails.innerHTML = ''; + mcpStatusDetails.replaceChildren(); return; } const anyConnected = status.servers.some(s => s.connected); const toolCount = status.registeredToolNames.length; if (anyConnected) { mcpStatusDot.style.backgroundColor = 'var(--color-accent-green)'; mcpStatusText.textContent = `Connected (${toolCount} tools)`; mcpStatusText.style.color = 'var(--color-accent-green)'; - - let detailsHtml = ''; - if (status.lastConnected) { - detailsHtml += `Last connected: ${formatTimestamp(status.lastConnected)}<br>`; - } - if (status.lastError) { - detailsHtml += `<span style="color: var(--color-error-text)">${formatMCPError(status.lastError, status.lastErrorType)}</span>`; - } - mcpStatusDetails.innerHTML = detailsHtml; + const nodes: Node[] = []; + if (status.lastConnected) { + nodes.push(document.createTextNode(`Last connected: ${formatTimestamp(status.lastConnected)}`)); + nodes.push(document.createElement('br')); + } + if (status.lastError) { + const err = document.createElement('span'); + err.style.color = 'var(--color-error-text)'; + err.textContent = formatMCPError(status.lastError, status.lastErrorType); + nodes.push(err); + } + mcpStatusDetails.replaceChildren(...nodes); } else { mcpStatusDot.style.backgroundColor = 'var(--color-text-disabled)'; mcpStatusText.textContent = 'Not connected'; mcpStatusText.style.color = 'var(--color-text-disabled)'; - - let detailsHtml = ''; - if (status.lastDisconnected) { - detailsHtml += `Last disconnected: ${formatTimestamp(status.lastDisconnected)}<br>`; - } - if (status.lastError) { - detailsHtml += `<span style="color: var(--color-error-text)">${formatMCPError(status.lastError, status.lastErrorType)}</span>`; - } - mcpStatusDetails.innerHTML = detailsHtml; + const nodes: Node[] = []; + if (status.lastDisconnected) { + nodes.push(document.createTextNode(`Last disconnected: ${formatTimestamp(status.lastDisconnected)}`)); + nodes.push(document.createElement('br')); + } + if (status.lastError) { + const err = document.createElement('span'); + err.style.color = 'var(--color-error-text)'; + err.textContent = formatMCPError(status.lastError, status.lastErrorType); + nodes.push(err); + } + mcpStatusDetails.replaceChildren(...nodes); } };
2865-2915: Clear and rebuild tools list safely; use fresh config for allowlist.Avoid
innerHTML = ''; read allowlist from latest config each time.- const updateToolsList = () => { + const updateToolsList = (): void => { const status = MCPRegistry.getStatus(); - mcpToolsList.innerHTML = ''; + mcpToolsList.replaceChildren(); if (!status.enabled || status.registeredToolNames.length === 0) { const noToolsMessage = document.createElement('div'); noToolsMessage.className = 'mcp-no-tools'; noToolsMessage.textContent = i18nString(UIStrings.mcpNoTools); mcpToolsList.appendChild(noToolsMessage); return; } - const currentAllowlist = new Set(currentMCPConfig.toolAllowlist || []); + const { toolAllowlist } = getMCPConfig(); + const currentAllowlist = new Set(toolAllowlist || []);front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts (1)
22-22: Fix ESLint import/order for MCP import (place before tools).Move the MCP import above all
../../tools/*imports to satisfy import/order and avoid duplicates.- import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js'; +import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js'; import { FetcherTool } from '../../tools/FetcherTool.js'; import { FinalizeWithCritiqueTool } from '../../tools/FinalizeWithCritiqueTool.js'; import { SchemaBasedExtractorTool } from '../../tools/SchemaBasedExtractorTool.js';front_end/panels/ai_chat/core/AgentNodes.ts (1)
413-416: Executor must honor selected tools and sanitized names; removeanyto fix ESLint.Current map ignores
context.selectedToolsand sanitized aliases, and usesany. Prefer the persisted selection and map both original and sanitized names.-export function createToolExecutorNode(state: AgentState, provider: LLMProvider, modelName: string, miniModel?: string, nanoModel?: string): Runnable<AgentState, AgentState> { - const tools = getAgentToolsFromState(state); // Adjusted to use getAgentToolsFromState - const toolMap = new Map<string, ReturnType<typeof getTools>[number]>(); - (tools as any[]).forEach((tool: any) => toolMap.set(tool.name, tool)); +export function createToolExecutorNode( + state: AgentState, + provider: LLMProvider, + modelName: string, + miniModel?: string, + nanoModel?: string +): Runnable<AgentState, AgentState> { + type ToolT = ReturnType<typeof getTools>[number]; + const ctx = (state.context || {}) as { + selectedTools?: ToolT[]; + selectedToolNames?: string[]; + toolNameMap?: Record<string, string>; // sanitized -> original + }; + let tools: ToolT[] = Array.isArray(ctx.selectedTools) && ctx.selectedTools.length + ? ctx.selectedTools + : (getAgentToolsFromState(state) as ToolT[]); + const toolMap = new Map<string, ToolT>(); + const nameMap = ctx.toolNameMap || {}; + tools.forEach((tool) => { + toolMap.set(tool.name, tool); // original + // add sanitized alias(es) if mapped + for (const [sanitized, original] of Object.entries(nameMap)) { + if (original === tool.name) toolMap.set(sanitized, tool); + } + });
🧹 Nitpick comments (1)
front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts (1)
97-98: Make MCP registration resilient (lazy/guarded).Unconditional registration can throw in environments without MCP config. Wrap in a guard/try-catch or feature flag; consider lazy-loading later to keep init cheap.
- registerMCPMetaTools(); + try { + registerMCPMetaTools(); + } catch (e) { + // Non-fatal: MCP optional + console.warn('MCP meta-tools registration skipped:', e); + }Would you like a follow-up to convert this to a dynamic import behind a settings flag?
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
front_end/panels/ai_chat/BUILD.gn(6 hunks)front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts(1 hunks)front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts(2 hunks)front_end/panels/ai_chat/core/AgentNodes.ts(3 hunks)front_end/panels/ai_chat/ui/AIChatPanel.ts(5 hunks)front_end/panels/ai_chat/ui/SettingsDialog.ts(8 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts
- front_end/panels/ai_chat/BUILD.gn
🧰 Additional context used
🧬 Code graph analysis (2)
front_end/panels/ai_chat/ui/SettingsDialog.ts (1)
front_end/panels/ai_chat/ui/AIChatPanel.ts (9)
localStorage(817-840)currentProvider(878-938)currentProvider(2344-2403)error(1933-1946)logger(952-1045)logger(1336-1406)logger(1429-1456)logger(1563-1576)logger(1620-1623)
front_end/panels/ai_chat/core/AgentNodes.ts (1)
front_end/panels/ai_chat/core/GraphHelpers.ts (1)
getAgentToolsFromState(44-47)
🪛 ESLint
front_end/panels/ai_chat/ui/SettingsDialog.ts
[error] 437-437: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 546-546: 'e' is defined but never used.
(@typescript-eslint/no-unused-vars)
[error] 2562-2562: Expected { after 'if' condition.
(curly)
[error] 2567-2567: Expected { after 'if' condition.
(curly)
[error] 2568-2568: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 2585-2585: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
[error] 2600-2600: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 2613-2613: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 2653-2653: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
[error] 2832-2832: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
[error] 2865-2865: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
[error] 2868-2868: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 2900-2900: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts
[error] 22-22: ../../mcp/MCPMetaTools.js import should occur before import of ../../tools/FetcherTool.js
(import/order)
front_end/panels/ai_chat/core/AgentNodes.ts
[error] 13-13: ./ToolSurfaceProvider.js import should occur after type import of ../tracing/TracingProvider.js
(import/order)
[error] 415-415: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 415-415: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
front_end/panels/ai_chat/ui/AIChatPanel.ts
[error] 89-89: Import in body of module; reorder to top.
(import/first)
[error] 89-89: ../mcp/MCPRegistry.js import should occur before import of ./chatView.css.js
(import/order)
[error] 90-90: Import in body of module; reorder to top.
(import/first)
[error] 90-90: ../mcp/MCPConfig.js import should occur before import of ./chatView.css.js
(import/order)
[error] 90-90: '../mcp/MCPConfig.js' imported multiple times.
(import/no-duplicates)
[error] 91-91: Import in body of module; reorder to top.
(import/first)
[error] 91-91: ../mcp/MCPConfig.js import should occur before import of ./chatView.css.js
(import/order)
[error] 91-91: '../mcp/MCPConfig.js' imported multiple times.
(import/no-duplicates)
[error] 722-722: '#mcpUnsubscribe' is defined but never used.
(no-unused-private-class-members)
[error] 794-794: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
🪛 ast-grep (0.38.6)
front_end/panels/ai_chat/ui/SettingsDialog.ts
[warning] 2607-2607: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: mcpStatusDetails.innerHTML = detailsHtml
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html
(unsafe-html-content-assignment)
[warning] 2620-2620: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: mcpStatusDetails.innerHTML = detailsHtml
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html
(unsafe-html-content-assignment)
[warning] 2590-2590: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: mcpStatusDetails.innerHTML = ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html
(dom-content-modification)
[warning] 2607-2607: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: mcpStatusDetails.innerHTML = detailsHtml
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html
(dom-content-modification)
[warning] 2620-2620: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: mcpStatusDetails.innerHTML = detailsHtml
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html
(dom-content-modification)
[warning] 2866-2866: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: mcpToolsList.innerHTML = ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html
(dom-content-modification)
| // Select tools via ToolSurfaceProvider (router/meta/all) | ||
| const baseTools = getAgentToolsFromState(state); | ||
| const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 }); | ||
| const tools = selection.tools; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Per-turn tool selection added — persist selection and name map for executor.
The LLM is called with selection.tools, but the executor builds its map from base tools and doesn’t know sanitized names. Persist the turn’s tools and a sanitized→original name map in state.context so the executor can honor them.
- const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 });
- const tools = selection.tools;
+ const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 });
+ const tools = selection.tools;
+ // Make executor-aware of the exact tool set and sanitized name mapping
+ state.context = {
+ ...(state.context || {}),
+ selectedTools: tools,
+ toolNameMap: selection.nameMap || {}, // { sanitizedName: originalName }
+ };Nit: In tracing input above, record the actual selected tools instead of all agent tools.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Select tools via ToolSurfaceProvider (router/meta/all) | |
| const baseTools = getAgentToolsFromState(state); | |
| const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 }); | |
| const tools = selection.tools; | |
| // Select tools via ToolSurfaceProvider (router/meta/all) | |
| const baseTools = getAgentToolsFromState(state); | |
| const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 }); | |
| const tools = selection.tools; | |
| // Make executor-aware of the exact tool set and sanitized name mapping | |
| state.context = { | |
| ...(state.context || {}), | |
| selectedTools: tools, | |
| toolNameMap: selection.nameMap || {}, // { sanitizedName: originalName } | |
| }; |
🧰 Tools
🪛 ESLint
[error] 151-151: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/AgentNodes.ts around lines 147-151, the code
calls the LLM with selection.tools but the executor still builds its tool map
from baseTools and lacks the sanitized→original name mapping; update the agent
node to persist the per-turn selected tools and a sanitizedNameMap into
state.context (e.g. state.context.selectedTools and
state.context.sanitizedNameMap) immediately after ToolSurfaceProvider.select
resolves so the executor can read and build its map from state.context rather
than baseTools, and change any tracing/input payload to record the actual
selectedTools (selection.tools) instead of getAgentToolsFromState(state).
5684856 to
101249f
Compare
|
Claude finished @tysonthomas9's task —— View job MCP Integration PR ReviewI've conducted a comprehensive review of this MCP integration PR. Here's my detailed feedback: Review Tasks:
🔴 Critical Security Issues1. Token Storage Vulnerability (HIGH)File: localStorage.setItem(KEYS.token, config.token);Issue: Storing authentication tokens in 2. Tool Argument Redaction Bug (CRITICAL)File: const sanitized = this.sanitize(args);
return this.client.callTool(this.serverId, this.def.name, sanitized, { timeoutMs: 30000 });Issue: The code redacts secrets from arguments then sends the redacted version to MCP server, breaking authentication for any tool requiring API keys/tokens. 3. XSS Risk in Settings Dialog (MEDIUM)File: mcpStatusDetails.innerHTML = detailsHtml;Issue: Using 🔴 Critical Functionality Issues4. LLM Function Name Compatibility (BLOCKER)File: tools: tools.map(tool => ({
function: {
name: tool.name, // Names like "mcp:default:alpha" will be rejected by OpenAI
description: tool.description,
parameters: tool.schema,
}
}))Issue: MCP tool names containing colons/dots (e.g., 5. Integration Test CI Failure (HIGH)File: describe('connects to local Hacker News MCP server via SDK', function() {
const HACKER_NEWS_SERVER: MCPServer = {
endpoint: 'http://localhost:5001/sse',
};Issue: This integration test will fail/hang in CI where no local MCP server runs.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
front_end/panels/ai_chat/ui/ToolCallComponent.ts (1)
14-16: ESLint error: custom element defined in a disallowed folderThe rule requires custom elements to live in ui/components/ or a panel’s components/ sub-folder. Moving this file will unblock CI.
Suggested fix:
- Move to front_end/panels/ai_chat/ui/components/ToolCallComponent.ts and update imports/exports accordingly.
Temporary (not preferred) alternatives:
- Add a targeted override in the panel’s ESLint config for this path, or
- File-level suppression at top of file:
/* eslint-disable rulesdir/enforce-custom-element-definitions-location */front_end/panels/ai_chat/ui/message/MessageCombiner.ts (1)
46-56: Combine across agent-lane separators (don’t require immediate adjacency)Current logic only checks messages[i+1], so a tool_result separated by one or more agent-lane messages becomes orphaned. Scan forward for the next non-agent message (j) and, when you combine, advance the loop to that index (set i = j) so the tool_result and intermediate agent-lane messages are skipped.
File: front_end/panels/ai_chat/ui/message/MessageCombiner.ts (around lines 46–56).
front_end/panels/ai_chat/core/AgentNodes.ts (1)
261-273: Resolve sanitized tool name → original before lane detection.Parsed tool names from the model will be sanitized; registry holds originals.
- const regTool = ToolRegistry.getRegisteredTool(parsedAction.name as any); - const isAgentTool = !!regTool && (regTool instanceof ConfigurableAgentTool); + const nameMap: Record<string, string> = (state.context as any)?.toolNameMap || {}; + const originalName = nameMap[parsedAction.name] ?? parsedAction.name; + const regTool = ToolRegistry.getRegisteredTool(originalName as any); + const isAgentTool = regTool instanceof ConfigurableAgentTool;
♻️ Duplicate comments (34)
front_end/panels/ai_chat/mcp/MCPClientSDK.test.ts (4)
24-31: Prefer instanceOf/isFalse assertionsAlign with project assertion conventions.
- it('can be instantiated', () => { - assert.ok(client instanceof MCPClientSDK); - }); + it('can be instantiated', () => { + assert.instanceOf(client, MCPSDK.MCPClientSDK); + }); @@ - it('reports not connected initially', () => { - assert.strictEqual(client.isConnected('non-existent-server'), false); - }); + it('reports not connected initially', () => { + assert.isFalse(client.isConnected('non-existent-server')); + });
35-51: Tighten error assertions and avoid assert.okUse assert.include on message; remove redundant throw dance with assert.fail for clarity.
- it('throws error for missing server when listing tools', async () => { - try { - await client.listTools('non-existent-server'); - throw new Error('Should have thrown error'); - } catch (error) { - assert.ok(error.message.includes('No connection for server')); - } - }); + it('throws error for missing server when listing tools', async () => { + try { + await client.listTools('non-existent-server'); + assert.fail('Expected error'); + } catch (error) { + assert.include((error as Error).message, 'No connection for server'); + } + }); @@ - it('throws error for missing server when calling tools', async () => { + it('throws error for missing server when calling tools', async () => { try { await client.callTool('non-existent-server', 'test-tool', {}); - throw new Error('Should have thrown error'); + assert.fail('Expected error'); } catch (error) { - assert.ok(error.message.includes('No connection for server')); + assert.include((error as Error).message, 'No connection for server'); } });
5-7: Use a single namespaced import for SDK (fixes ESLint import rules)Consolidate value and type imports via namespace to satisfy rulesdir/es-modules-import and inline-type-imports.
-import { MCPClientSDK } from '../../../third_party/mcp-sdk/mcp-sdk.js'; -import type { MCPServer } from '../../../third_party/mcp-sdk/mcp-sdk.js'; +import * as MCPSDK from '../../../third_party/mcp-sdk/mcp-sdk.js';And update types/usages:
- let client: MCPClientSDK; + let client: MCPSDK.MCPClientSDK; @@ - client = new MCPClientSDK(); + client = new MCPSDK.MCPClientSDK(); @@ - const HACKER_NEWS_SERVER: MCPServer = { + const HACKER_NEWS_SERVER: MCPSDK.MCPServer = {
53-135: Remove or gate the localhost integration test; convert console.log to console.warnThis block depends on a local MCP server and will fail/hang in CI. Gate behind an env flag and fix console usage + boolean assertions.
-describe('connects to local Hacker News MCP server via SDK', function() { +const RUN_MCP_SDK_E2E = !!process.env.MCP_SDK_E2E; +(RUN_MCP_SDK_E2E ? describe : describe.skip)('connects to local Hacker News MCP server via SDK', function() { this.timeout(30000); // Longer timeout for integration test @@ - it('connects and lists tools', async function() { - console.log('=== Testing Local Hacker News MCP Server with SDK ==='); + it('connects and lists tools', async function() { + console.warn('=== Testing Local Hacker News MCP Server with SDK ==='); @@ - console.log('Step 1: Connecting to MCP server via SDK...'); + console.warn('Step 1: Connecting to MCP server via SDK...'); await client.connect(HACKER_NEWS_SERVER); - assert.strictEqual(client.isConnected(HACKER_NEWS_SERVER.id), true, 'Should be connected'); - console.log('✓ Connection established via SDK'); + assert.isTrue(client.isConnected(HACKER_NEWS_SERVER.id), 'Should be connected'); + console.warn('✓ Connection established via SDK'); @@ - console.log('Step 2: Listing available tools via SDK...'); + console.warn('Step 2: Listing available tools via SDK...'); const tools = await client.listTools(HACKER_NEWS_SERVER.id); - console.log(`✓ Retrieved ${tools.length} tools via SDK`); + console.warn(`✓ Retrieved ${tools.length} tools via SDK`); @@ - assert.ok(Array.isArray(tools), 'Tools should be an array'); + assert.isTrue(Array.isArray(tools), 'Tools should be an array'); if (tools.length > 0) { const tool = tools[0]; - assert.ok(tool.hasOwnProperty('name'), 'Tool should have name'); - assert.ok(tool.hasOwnProperty('description'), 'Tool should have description'); - assert.ok(tool.hasOwnProperty('inputSchema'), 'Tool should have inputSchema'); - console.log(`✓ Tool structure valid: ${tool.name}`); + assert.isTrue(Object.prototype.hasOwnProperty.call(tool, 'name'), 'Tool should have name'); + assert.isTrue(Object.prototype.hasOwnProperty.call(tool, 'description'), 'Tool should have description'); + assert.isTrue(Object.prototype.hasOwnProperty.call(tool, 'inputSchema'), 'Tool should have inputSchema'); + console.warn(`✓ Tool structure valid: ${tool.name}`); } @@ - console.log(`Step 3: Testing tool call: ${firstTool.name}`); + console.warn(`Step 3: Testing tool call: ${firstTool.name}`); @@ - const schema = firstTool.inputSchema as any; + const schema = firstTool.inputSchema as unknown; @@ - console.log(`Calling ${firstTool.name} with args:`, args); + console.warn(`Calling ${firstTool.name} with args:`, args); const result = await client.callTool(HACKER_NEWS_SERVER.id, firstTool.name, args); - - console.log('Tool call result via SDK:', typeof result); - assert.ok(result !== undefined, 'Tool call should return a result'); - console.log('✓ Tool call successful via SDK'); + console.warn('Tool call result via SDK:', typeof result); + assert.isDefined(result, 'Tool call should return a result'); + console.warn('✓ Tool call successful via SDK'); @@ - console.log('Tool call failed, but connection and tools/list worked'); + console.warn('Tool call failed, but connection and tools/list worked'); @@ - console.log('✓ All SDK tests passed!'); + console.warn('✓ All SDK tests passed!');Also add a final newline and strip trailing spaces.
front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts (1)
22-22: Fix import/order: place MCP import before tools importsESLint import/order flags this. Move registerMCPMetaTools import above any ../../tools/* imports.
-import { FetcherTool } from '../../tools/FetcherTool.js'; +import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js'; +import { FetcherTool } from '../../tools/FetcherTool.js'; import { FinalizeWithCritiqueTool } from '../../tools/FinalizeWithCritiqueTool.js'; @@ -import { ThinkingTool } from '../../tools/ThinkingTool.js'; -import type { Tool } from '../../tools/Tools.js'; -import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js'; +import { ThinkingTool } from '../../tools/ThinkingTool.js'; +import type { Tool } from '../../tools/Tools.js';front_end/panels/ai_chat/core/ToolNameMapping.test.ts (4)
12-21: Drop explicit public; make ctor param readonlyAligns with @typescript-eslint/explicit-member-accessibility and intent.
-class RecordingTool implements Tool<Record<string, unknown>, unknown> { - public calls = 0; - constructor(public name: string) {} +class RecordingTool implements Tool<Record<string, unknown>, unknown> { + calls = 0; + constructor(readonly name: string) {}
50-67: Remove any casts; type the message/state preciselyAvoid as any; construct a typed ModelChatMessage and AgentState.
- const state: AgentState = { - messages: [ - { - entity: ChatMessageEntity.MODEL, - action: 'tool', - toolName: sanitizedName, - toolArgs: { x: 1 }, - toolCallId: 'call-1', - isFinalAnswer: false, - } as any - ], - agentType: 'deep-research' as any, - context: { - selectedToolNames: [sanitizedName], - selectedTools: [tool], - toolNameMap: { [sanitizedName]: originalName } - } - } as any; + const modelCall = { + entity: ChatMessageEntity.MODEL, + action: 'tool' as const, + toolName: sanitizedName, + toolArgs: { x: 1 } as Record<string, unknown>, + toolCallId: 'call-1', + isFinalAnswer: false, + }; + const state: AgentState = { + messages: [modelCall], + context: { + selectedToolNames: [sanitizedName], + selectedTools: [tool], + toolNameMap: { [sanitizedName]: originalName }, + } as any, // if DevToolsContext type isn't exported here + };
23-42: Restore real localStorage after each testPrevents cross-test side effects.
-describe('AgentNodes sanitized tool name mapping', () => { - let mockLocalStorage: Map<string, string>; +describe('AgentNodes sanitized tool name mapping', () => { + let mockLocalStorage: Map<string, string>; + let realLocalStorage: Storage; @@ beforeEach(() => { @@ - Object.defineProperty(window, 'localStorage', { + realLocalStorage = window.localStorage; + Object.defineProperty(window, 'localStorage', { value: { @@ afterEach(() => { mockLocalStorage.clear(); + Object.defineProperty(window, 'localStorage', { value: realLocalStorage, writable: true }); });
5-9: Import assert and fix import grouping/orderassert is used but not imported; also group type imports before value imports and add a blank line between groups.
-import { createToolExecutorNode } from './AgentNodes.js'; -import type { AgentState } from './State.js'; -import type { Tool } from '../tools/Tools.js'; -import { ChatMessageEntity } from '../models/ChatTypes.js'; +import type { AgentState } from './State.js'; +import type { Tool } from '../tools/Tools.js'; +import { ChatMessageEntity } from '../models/ChatTypes.js'; +import { createToolExecutorNode } from './AgentNodes.js'; +import { assert } from 'chai';front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (1)
32-36: Do not pass redacted args to MCP; only redact for logging.Currently the sanitized args are sent to
client.callTool, breaking auth for tools that require secrets. Log the sanitized copy, but call with the originalargs. (Same concern previously raised.)- async execute(args: Record<string, unknown>): Promise<unknown> { - const sanitized = this.sanitize(args); - logger.info('Executing MCP tool', { name: this.name, serverId: this.serverId }); - return this.client.callTool(this.serverId, this.def.name, sanitized, { timeoutMs: 30000 }); - } + async execute(args: Record<string, unknown>): Promise<unknown> { + const redacted = this.sanitize(args); + logger.info('Executing MCP tool', { name: this.name, serverId: this.serverId, args: redacted }); + return this.client.callTool(this.serverId, this.def.name, args, { timeoutMs: 30000 }); + }front_end/panels/ai_chat/core/ToolSurfaceProvider.test.ts (1)
5-11: Import Chai assert and typed ChatMessageEntity; keep import order.Avoids globals and
0 as any.-import { ToolSurfaceProvider } from './ToolSurfaceProvider.js'; -import type { AgentState } from './State.js'; -import type { Tool } from '../tools/Tools.js'; -import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js'; -import { MCPRegistry } from '../mcp/MCPRegistry.js'; -import { registerMCPMetaTools } from '../mcp/MCPMetaTools.js'; +import { registerMCPMetaTools } from '../mcp/MCPMetaTools.js'; +import { MCPRegistry } from '../mcp/MCPRegistry.js'; +import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js'; +import { ToolSurfaceProvider } from './ToolSurfaceProvider.js'; +import type { AgentState } from './State.js'; +import type { Tool } from '../tools/Tools.js'; +import { assert } from 'chai'; +import { ChatMessageEntity } from '../ui/ChatView.js';front_end/panels/ai_chat/core/AgentNodes.test.ts (1)
45-56: Replaceanyfor agentSession with a typed alias; mirror in execute() return.- const errorResultWithSession: ConfigurableAgentResult & { agentSession: any } = { + type AgentResultWithSession = ConfigurableAgentResult & { agentSession: unknown }; + const errorResultWithSession: AgentResultWithSession = { @@ - async execute(): Promise<ConfigurableAgentResult & { agentSession: any }> { + async execute(): Promise<AgentResultWithSession> { return errorResultWithSession; }Also applies to: 69-71
front_end/panels/ai_chat/core/AgentNodes.ts (3)
169-181: Send provider‑safe tool names and keep mapping; avoid:in function names.Prevents OpenAI/Anthropic function-name rejections.
- tools: tools.map(tool => ({ + tools: tools.map(tool => { + const nameMap: Record<string, string> = (state.context as any)?.toolNameMap || {}; + const sanitized = Object.keys(nameMap).find(k => nameMap[k] === tool.name) + ?? tool.name.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64); + return ({ type: 'function', function: { - name: tool.name, + name: sanitized, description: tool.description, parameters: tool.schema, } - })), + }); + }),
147-154: Persist selected tool instances and name map for the executor.Store both the exact set and sanitized→original mapping.
- const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 }); - // Persist selection in context so ToolExecutorNode can resolve the same set - if (!state.context) { (state as any).context = {}; } - (state.context as any).selectedToolNames = selection.selectedNames; - const tools = selection.tools; + const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 }); + // Persist selection in context so ToolExecutorNode can resolve the same set + if (!state.context) { (state as any).context = {}; } + (state.context as any).selectedToolNames = selection.selectedNames; + (state.context as any).selectedTools = selection.tools; + (state.context as any).toolNameMap = selection.nameMap || {}; // { sanitized: original } + const tools = selection.tools;
421-436: Executor: prefer preselected tool instances; resolve sanitized names; key by both names.- const selectedNames: string[] | undefined = (state.context as any)?.selectedToolNames; - let tools: ReturnType<typeof getTools>; - if (selectedNames && selectedNames.length > 0) { - const resolved: any[] = []; - for (const name of selectedNames) { - const inst = ToolRegistry.getRegisteredTool(name as any); - if (inst) { resolved.push(inst as any); } - } - tools = resolved as any; - } else { - tools = getAgentToolsFromState(state) as any; - } - const toolMap = new Map<string, ReturnType<typeof getTools>[number]>(); - (tools as any[]).forEach((tool: any) => toolMap.set(tool.name, tool)); + const ctx = (state.context as any) || {}; + const selectedNames: string[] | undefined = ctx.selectedToolNames; + const nameMap: Record<string, string> = ctx.toolNameMap || {}; + let tools: ReturnType<typeof getTools>; + if (Array.isArray(ctx.selectedTools) && ctx.selectedTools.length > 0) { + tools = ctx.selectedTools as any; + } else if (selectedNames && selectedNames.length > 0) { + const resolved: any[] = []; + for (const name of selectedNames) { + const original = nameMap[name] || name; + const inst = ToolRegistry.getRegisteredTool(original as any); + if (inst) resolved.push(inst as any); + } + tools = resolved as any; + } else { + tools = getAgentToolsFromState(state) as any; + } + const toolMap = new Map<string, ReturnType<typeof getTools>[number]>(); + (tools as any[]).forEach((tool: any) => { + toolMap.set(tool.name, tool); // original + for (const [sanitized, original] of Object.entries(nameMap)) { + if (original === tool.name) toolMap.set(sanitized, tool); // alias + } + });front_end/panels/ai_chat/ui/AIChatPanel.ts (4)
88-91: Fix MCP import order and duplicate import (ESLint blockers).Move MCP imports into the main import group and combine the two MCPConfig imports.
-// MCP integration -import { MCPRegistry } from '../mcp/MCPRegistry.js'; -import { getMCPConfig } from '../mcp/MCPConfig.js'; -import { onMCPConfigChange } from '../mcp/MCPConfig.js'; +// (move to main import section with others at top) +// MCP integration +import { MCPRegistry } from '../mcp/MCPRegistry.js'; +import { getMCPConfig, onMCPConfigChange } from '../mcp/MCPConfig.js';
722-722: Use unsubscribe handle in teardown to prevent leaks; resolves no-unused-private-class-members.Store is set at Line 811 but never called; unsubscribe during willHide.
override willHide(): void { // Explicitly remove any event listeners to prevent memory leaks if (this.#boundOnMessagesChanged) { this.#agentService.removeEventListener(AgentEvents.MESSAGES_CHANGED, this.#boundOnMessagesChanged); } + if (this.#mcpUnsubscribe) { + this.#mcpUnsubscribe(); + this.#mcpUnsubscribe = null; + } if (this.#boundOnAgentSessionStarted) this.#agentService.removeEventListener(AgentEvents.AGENT_SESSION_STARTED, this.#boundOnAgentSessionStarted); if (this.#boundOnAgentToolStarted) this.#agentService.removeEventListener(AgentEvents.AGENT_TOOL_STARTED, this.#boundOnAgentToolStarted); if (this.#boundOnAgentToolCompleted) this.#agentService.removeEventListener(AgentEvents.AGENT_TOOL_COMPLETED, this.#boundOnAgentToolCompleted); if (this.#boundOnAgentSessionUpdated) this.#agentService.removeEventListener(AgentEvents.AGENT_SESSION_UPDATED, this.#boundOnAgentSessionUpdated); if (this.#boundOnChildAgentStarted) this.#agentService.removeEventListener(AgentEvents.CHILD_AGENT_STARTED, this.#boundOnChildAgentStarted); }Also applies to: 1956-1966
789-813: Add explicit return type and nullish guards in MCP init helper.Silences explicit-function-return-type and avoids undefined property reads.
#setupMCPIntegration(): void { - const initAndRefresh = async () => { + const initAndRefresh = async (): Promise<void> => { try { - const mcpConfig = getMCPConfig(); + const mcpConfig = getMCPConfig(); // Only auto-connect if both enabled and autostart are true - if (mcpConfig.enabled && mcpConfig.autostart) { + if (mcpConfig?.enabled && mcpConfig?.autostart) { await MCPRegistry.init(); await MCPRegistry.refresh(); const status = MCPRegistry.getStatus(); logger.info('MCP autostart completed', status); } } catch (err) { logger.error('Failed to initialize MCP', err); } };
2331-2336: Gate MCP re-init behind config.enabled to avoid unnecessary work when disabled.This also aligns with startup behavior.
- try { - await MCPRegistry.init(); - await MCPRegistry.refresh(); - } catch (err) { + try { + const cfg = getMCPConfig(); + if (cfg?.enabled) { + await MCPRegistry.init(); + await MCPRegistry.refresh(); + } + } catch (err) { logger.error('Failed to reinitialize MCP after settings change', err); - } + }front_end/panels/ai_chat/mcp/MCPConfig.ts (2)
1-1: Add Chromium BSD license header.+// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file.
38-41: Harden toolMode parsing and guard numeric parsing against NaN.- const toolMode = (localStorage.getItem(KEYS.toolMode) as MCPConfigData['toolMode']) || 'router'; - const maxToolsPerTurn = parseInt(localStorage.getItem(KEYS.maxToolsPerTurn) || '20', 10); - const maxMcpPerTurn = parseInt(localStorage.getItem(KEYS.maxMcpPerTurn) || '8', 10); + const rawMode = localStorage.getItem(KEYS.toolMode); + const toolMode: MCPConfigData['toolMode'] = + rawMode === 'all' || rawMode === 'router' || rawMode === 'meta' ? rawMode : 'router'; + const parsePositive = (v: string | null, fallback: number) => { + const n = Number.parseInt(v ?? '', 10); + return Number.isFinite(n) && n > 0 ? n : fallback; + }; + const maxToolsPerTurn = parsePositive(localStorage.getItem(KEYS.maxToolsPerTurn), 20); + const maxMcpPerTurn = parsePositive(localStorage.getItem(KEYS.maxMcpPerTurn), 8);front_end/panels/ai_chat/mcp/MCPRegistry.ts (2)
1-7: Fix license header and import ordering; use namespace import for third_party.+// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import * as MCPSDK from '../../../third_party/mcp-sdk/mcp-sdk.js'; import { createLogger } from '../core/Logger.js'; import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js'; -import type { MCPToolDef, MCPServer } from '../../../third_party/mcp-sdk/mcp-sdk.js'; -import { MCPClient } from '../../../third_party/mcp-sdk/mcp-sdk.js'; import { getMCPConfig } from './MCPConfig.js'; import { MCPToolAdapter } from './MCPToolAdapter.js'; + +type MCPToolDef = MCPSDK.MCPToolDef; +type MCPServer = MCPSDK.MCPServer;And update client construction:
- private client = new MCPClient(); + private client = new MCPSDK.MCPClient();
139-143: Report actual tool counts per server (not 0).connected: this.client.isConnected(s.id), - toolCount: 0, + toolCount: this.registeredTools.filter(name => name.startsWith(`mcp:${s.id}:`)).length,front_end/panels/ai_chat/mcp/MCPMetaTools.ts (3)
23-27: Schema: allow serverId string | string[] to match runtime.- serverId: { type: 'string', description: 'Filter by serverId (or array of ids)' }, + serverId: { + description: 'Filter by serverId (or array of ids)', + oneOf: [ + { type: 'string' }, + { type: 'array', items: { type: 'string' } } + ] + },And for invoke tool keep serverId: string as-is (execution requires a single server id).
Also applies to: 169-176
48-93: Provider-agnostic function-call parsing and preserve selection order.- const response = await llm.call({ + const response = await llm.call({ provider, model, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userMessage } ], systemPrompt: systemPrompt, // Use standard function-calling tool shape expected by providers tools: [{ type: 'function', function: { name: 'select_tools', description: 'Select the most relevant tools', parameters: { type: 'object', properties: { selected: { type: 'array', items: { type: 'string' }, description: 'Array of selected tool keys', maxItems: k }, rationale: { type: 'string', description: 'Brief explanation for selections' } }, required: ['selected'] } } }] }); - - if (!response.functionCall) { - throw new Error('No function call in LLM response'); - } - - const functionCall = response.functionCall; - const selectedKeys = (functionCall.arguments as any)?.selected || []; - - // Return candidates in the order selected by LLM, filtering to only selected keys - const selectedSet = new Set(selectedKeys); - return candidates.filter(c => selectedSet.has(c.key)); + const parsed = llm.parseResponse(response); + if (parsed.type !== 'tool_call' || parsed.name !== 'select_tools') { + throw new Error('LLM did not return select_tools function call'); + } + const rawArgs = parsed.args as unknown; + const argsObj = typeof rawArgs === 'string' ? JSON.parse(rawArgs) : rawArgs ?? {}; + const selectedKeys: string[] = Array.isArray((argsObj as any).selected) ? (argsObj as any).selected : []; + if (!selectedKeys.length) { + return this.rankHeuristic(candidates, query, k); + } + const byKey = new Map(candidates.map(c => [c.key, c])); + const ordered = selectedKeys.map(k2 => byKey.get(k2)).filter((x): x is NonNullable<typeof x> => !!x); + return ordered.slice(0, k);
151-155: Wraphybridcase declarations in a block to satisfy no-case-declarations.- case 'hybrid': - // For hybrid: use heuristic pre-filtering then LLM ranking on top candidates - const prefilterLimit = Math.min(candidates.length, k * 5); // 5x expansion for LLM to choose from - const prefiltered = this.rankHeuristic(candidates, args.query, prefilterLimit); - rankedTools = prefiltered.length <= k ? prefiltered : await this.rankWithLLM(prefiltered, args.query, k); - break; + case 'hybrid': { + // For hybrid: heuristic pre-filtering then LLM ranking on top candidates + const prefilterLimit = Math.min(candidates.length, k * 5); + const prefiltered = this.rankHeuristic(candidates, args.query, prefilterLimit); + rankedTools = prefiltered.length <= k ? prefiltered : await this.rankWithLLM(prefiltered, args.query, k); + break; + }front_end/panels/ai_chat/core/ToolSurfaceProvider.ts (2)
32-56: Respect MCP tool allowlist when building the MCP tool list.
getAllMcpTools()ignores the saved allowlist and can surface disabled tools. Filter bycfg.toolAllowlist(empty set means “allow all”).function getAllMcpTools(): Tool<any, any>[] { try { const status = MCPRegistry.getStatus(); - console.log('[TOOL_SELECTION_DEBUG] MCPRegistry status:', { + logger.debug('[TOOL_SELECTION_DEBUG] MCPRegistry status:', { enabled: status.enabled, serverCount: status.servers.length, servers: status.servers, registeredToolNames: status.registeredToolNames, lastError: status.lastError, lastErrorType: status.lastErrorType }); - - const tools: Tool<any, any>[] = []; + const cfg = getMCPConfig(); + const allow = new Set(cfg.toolAllowlist || []); + const tools: Tool<any, any>[] = []; for (const name of status.registeredToolNames) { const tool = ToolRegistry.getRegisteredTool(name); - if (tool) { - tools.push(tool); + if (tool) { + if (allow.size === 0 || allow.has(tool.name)) { + tools.push(tool); + } } else { - console.log('[TOOL_SELECTION_DEBUG] Tool registered but not found:', name); + logger.warn('[TOOL_SELECTION_DEBUG] Tool registered but not found:', name); } } - console.log('[TOOL_SELECTION_DEBUG] getAllMcpTools result:', { + logger.debug('[TOOL_SELECTION_DEBUG] getAllMcpTools result:', { availableToolsCount: tools.length, availableToolNames: tools.map(t => t.name) }); return tools; } catch (error) { - console.error('[TOOL_SELECTION_DEBUG] Error in getAllMcpTools:', error); + logger.error('[TOOL_SELECTION_DEBUG] Error in getAllMcpTools:', error); return []; } }
114-117: Honor config budgets by default; let opts override.Defaults should come from MCP config when
optsis not passed.- const { maxToolsPerTurn = 20, maxMcpPerTurn = 8 } = opts || {}; const cfg = getMCPConfig(); + const maxToolsPerTurn = opts?.maxToolsPerTurn ?? cfg.maxToolsPerTurn ?? 20; + const maxMcpPerTurn = opts?.maxMcpPerTurn ?? cfg.maxMcpPerTurn ?? 8; const mode = cfg.toolMode || 'router';front_end/panels/ai_chat/ui/SettingsDialog.ts (6)
538-548: Remove unused catch parameter.Silences
@typescript-eslint/no-unused-vars.- } catch (e) { + } catch { // Fallback silently to storedProvider }
2653-2658: Add explicit return type.Satisfies
explicit-function-return-type.- const updateDisconnectButton = () => { + const updateDisconnectButton = (): void => { const status = MCPRegistry.getStatus(); const anyConnected = status.enabled && status.servers.some(s => s.connected); mcpDisconnectContainer.style.display = anyConnected ? 'block' : 'none'; };
2831-2845: AnnotateupdateBudgetControlsreturn type.Avoids ESLint error; logic unchanged.
- const updateBudgetControls = () => { + const updateBudgetControls = (): void => { const maxTools = Math.max(1, Math.min(100, parseInt(mcpMaxToolsInput.value, 10) || 20)); const maxMcp = Math.max(1, Math.min(50, parseInt(mcpMaxMcpInput.value, 10) || 8)); setMCPConfig({ ...getMCPConfig(), maxToolsPerTurn: maxTools, maxMcpPerTurn: maxMcp, }); onSettingsSaved(); };
2561-2583: Add braces (curly) and make error formatters return plain text (no HTML).Preps for safe DOM rendering and fixes lint.
- const formatTimestamp = (date: Date | undefined): string => { - if (!date) return ''; - return date.toLocaleString(); - }; + const formatTimestamp = (date: Date | undefined): string => { + if (!date) { return ''; } + return date.toLocaleString(); + }; - const formatMCPError = (error: string, errorType?: string): string => { - if (!errorType) return error; - - switch (errorType) { + const formatMCPError = (error: string, errorType?: string): string => { + if (!errorType) { return error; } + switch (errorType) { case 'connection': - return `Connection failed: ${error}<br><span style="color: var(--color-text-secondary); font-size: 12px;">Check if the MCP server is running and the endpoint URL is correct.</span>`; + return `Connection failed: ${error}. Hint: Check if the MCP server is running and the endpoint URL is correct.`; case 'authentication': - return `Authentication failed: ${error}<br><span style="color: var(--color-text-secondary); font-size: 12px;">Verify your auth token is correct and has not expired.</span>`; + return `Authentication failed: ${error}. Hint: Verify your auth token is correct and has not expired.`; case 'configuration': - return `Configuration error: ${error}<br><span style="color: var(--color-text-secondary); font-size: 12px;">Check your endpoint URL format (should be ws:// or wss://).</span>`; + return `Configuration error: ${error}. Hint: Check your endpoint URL format (should be ws:// or wss://).`; case 'network': - return `Network error: ${error}<br><span style="color: var(--color-text-secondary); font-size: 12px;">Check your internet connection and firewall settings.</span>`; + return `Network error: ${error}. Hint: Check your internet connection and firewall settings.`; case 'server_error': - return `Server error: ${error}<br><span style="color: var(--color-text-secondary); font-size: 12px;">The MCP server encountered an internal error. Contact the server administrator.</span>`; + return `Server error: ${error}. Hint: The MCP server encountered an internal error. Contact the server administrator.`; default: return error; } };
2585-2624: Replace innerHTML with safe DOM construction in MCP status (XSS risk).
status.lastErrormay contain untrusted content. Build nodes and set textContent; also annotate return type.- const updateMCPStatus = () => { + const updateMCPStatus = (): void => { const status = MCPRegistry.getStatus(); if (!status.enabled) { mcpStatusDot.style.backgroundColor = 'var(--color-text-disabled)'; mcpStatusText.textContent = 'Disabled'; mcpStatusText.style.color = 'var(--color-text-disabled)'; - mcpStatusDetails.innerHTML = ''; + mcpStatusDetails.replaceChildren(); return; } const anyConnected = status.servers.some(s => s.connected); const toolCount = status.registeredToolNames.length; if (anyConnected) { mcpStatusDot.style.backgroundColor = 'var(--color-accent-green)'; mcpStatusText.textContent = `Connected (${toolCount} tools)`; mcpStatusText.style.color = 'var(--color-accent-green)'; - - let detailsHtml = ''; - if (status.lastConnected) { - detailsHtml += `Last connected: ${formatTimestamp(status.lastConnected)}<br>`; - } - if (status.lastError) { - detailsHtml += `<span style="color: var(--color-error-text)">${formatMCPError(status.lastError, status.lastErrorType)}</span>`; - } - mcpStatusDetails.innerHTML = detailsHtml; + const nodes: HTMLElement[] = []; + if (status.lastConnected) { + const line = document.createElement('div'); + line.textContent = `Last connected: ${formatTimestamp(status.lastConnected)}`; + nodes.push(line); + } + if (status.lastError) { + const line = document.createElement('div'); + line.textContent = formatMCPError(status.lastError, status.lastErrorType); + line.style.color = 'var(--color-error-text)'; + nodes.push(line); + } + mcpStatusDetails.replaceChildren(...nodes); } else { mcpStatusDot.style.backgroundColor = 'var(--color-text-disabled)'; mcpStatusText.textContent = 'Not connected'; mcpStatusText.style.color = 'var(--color-text-disabled)'; - - let detailsHtml = ''; - if (status.lastDisconnected) { - detailsHtml += `Last disconnected: ${formatTimestamp(status.lastDisconnected)}<br>`; - } - if (status.lastError) { - detailsHtml += `<span style="color: var(--color-error-text)">${formatMCPError(status.lastError, status.lastErrorType)}</span>`; - } - mcpStatusDetails.innerHTML = detailsHtml; + const nodes: HTMLElement[] = []; + if (status.lastDisconnected) { + const line = document.createElement('div'); + line.textContent = `Last disconnected: ${formatTimestamp(status.lastDisconnected)}`; + nodes.push(line); + } + if (status.lastError) { + const line = document.createElement('div'); + line.textContent = formatMCPError(status.lastError, status.lastErrorType); + line.style.color = 'var(--color-error-text)'; + nodes.push(line); + } + mcpStatusDetails.replaceChildren(...nodes); } };
2865-2915: Avoid innerHTML when updating tools list; read fresh allowlist.Prevents XSS and stale-config issues.
- const updateToolsList = () => { + const updateToolsList = (): void => { const status = MCPRegistry.getStatus(); - mcpToolsList.innerHTML = ''; + mcpToolsList.replaceChildren(); @@ - const currentAllowlist = new Set(currentMCPConfig.toolAllowlist || []); + const { toolAllowlist } = getMCPConfig(); + const currentAllowlist = new Set(toolAllowlist || []);
🧹 Nitpick comments (28)
front_end/panels/ai_chat/ui/ToolCallComponent.ts (1)
193-202: Make the toggle accessible (keyboard + semantics)The clickable div lacks role/keyboard handling. Add semantics and key support; reflect expanded state.
Example (outside this hunk):
// Template <div class="tool-header" role="button" tabindex="0" aria-expanded=${this.isExpanded} @click=${this.toggleExpanded} @keydown=${(e: KeyboardEvent) => (e.key === 'Enter' || e.key === ' ') && (e.preventDefault(), this.toggleExpanded())} >front_end/third_party/mcp-sdk/ajv/README.md (1)
1-208: Avoid editing vendored README; silence markdownlint for third_party/markdownlint warnings (MD045, MD001, MD040, MD034) stem from upstream content. Prefer excluding third_party from lint rather than modifying vendor files.
Apply either approach:
Option A (.markdownlintignore at repo root)
+front_end/third_party/**Option B (markdownlint config)
{ "ignores": [ + "front_end/third_party/**" ] }front_end/panels/ai_chat/mcp/MCPClientSDK.test.ts (1)
1-136: Remove debug console logs and use clearer boolean assertions in MCPClientSDK.test.ts
- Remove or gate debug console.log calls (integration-only) to avoid noisy CI output. Affected lines in front_end/panels/ai_chat/mcp/MCPClientSDK.test.ts: 62, 66, 69, 72, 74, 83, 89, 111, 114, 116, 121, 125.
- Replace boolean strictEqual checks with clearer assertions: line 29 -> assert.ok(!client.isConnected('non-existent-server')); line 68 -> assert.ok(client.isConnected(HACKER_NEWS_SERVER.id), 'Should be connected').
front_end/panels/ai_chat/core/ToolNameMapping.test.ts (1)
75-80: Prefer strict typed checks; avoid(last as any)and assert.okTighten assertions.
- const last = result.messages[result.messages.length - 1]; - assert.strictEqual(last.entity, ChatMessageEntity.TOOL_RESULT); - assert.strictEqual((last as any).toolName, sanitizedName); - assert.ok((last as any).resultText); + const last = result.messages[result.messages.length - 1]!; + assert.strictEqual(last.entity, ChatMessageEntity.TOOL_RESULT); + assert.strictEqual((last as { toolName: string }).toolName, sanitizedName); + assert.isOk((last as { resultText: string }).resultText);front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (3)
1-3: Add license header + fix import order (value imports before type-only).Also aligns with ESLint import/order.
+// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + -import type { Tool } from '../tools/Tools.js'; -import { createLogger } from '../core/Logger.js'; -import type { MCPClient, MCPToolDef } from '../../../third_party/mcp-sdk/mcp-sdk.js'; +import { createLogger } from '../core/Logger.js'; +import type { Tool } from '../tools/Tools.js'; +import type { MCPClient, MCPToolDef } from '../../../third_party/mcp-sdk/mcp-sdk.js';
10-11: Removeanyfrom schema typing; keep a safe JSON Schema shape.Prevents
no-explicit-anyand clarifies intent.- schema: any; + schema: Record<string, unknown>; - const schema = def.inputSchema as any; + const schema = def.inputSchema as Record<string, unknown> | undefined; if (schema && typeof schema === 'object') { this.schema = schema; } else { this.schema = { type: 'object', properties: {} }; }Also applies to: 24-29
42-53: Deep‑redact sensitive fields (nested objects/arrays), case‑insensitive.Covers headers/structured inputs, not just top-level strings.
- private sanitize(input: Record<string, unknown>): Record<string, unknown> { - const sensitive = ['token', 'api_key', 'password', 'secret', 'authorization']; - const out: Record<string, unknown> = {}; - for (const [k, v] of Object.entries(input || {})) { - if (typeof v === 'string' && sensitive.some(s => k.toLowerCase().includes(s))) { - out[k] = '[redacted]'; - } else { - out[k] = v; - } - } - return out; - } + private sanitize(input: unknown): unknown { + const sensitive = ['token', 'api_key', 'apikey', 'password', 'secret', 'authorization', 'auth']; + const redact = (key: string, val: unknown): unknown => { + if (sensitive.some(s => key.toLowerCase().includes(s))) return '[redacted]'; + if (Array.isArray(val)) return val.map(v => this.sanitize(v)); + if (val && typeof val === 'object') return this.sanitize(val); + return val; + }; + if (!input || typeof input !== 'object') return input; + const out: Record<string, unknown> = Array.isArray(input) ? {} : {}; + const obj = input as Record<string, unknown>; + const res: Record<string, unknown> = {}; + for (const [k, v] of Object.entries(obj)) res[k] = redact(k, v); + return res; + }front_end/panels/ai_chat/core/ToolSurfaceProvider.test.ts (2)
85-89: Use typed role constant instead of numeric cast.- const state: AgentState = { - messages: [{ entity: 0 as any, text: 'please run alpha operation' }], + const state: AgentState = { + messages: [{ entity: ChatMessageEntity.USER, text: 'please run alpha operation' }], context: {}, selectedAgentType: 'deep-research' as any } as any;
95-102: Preferassert.isOk,assert.lengthOf,assert.isFalse(per repo rules).- assert.ok(names.includes('core_tool_A')); - assert.ok(names.includes('core_tool_B')); + assert.isOk(names.includes('core_tool_A')); + assert.isOk(names.includes('core_tool_B')); - assert.strictEqual(selectedMcp.length, 2); + assert.lengthOf(selectedMcp, 2); - assert.ok(selectedMcp.includes('mcp:default:alpha')); + assert.isOk(selectedMcp.includes('mcp:default:alpha')); @@ - assert.ok(names.includes('core_tool_A')); - assert.ok(names.includes('mcp.search')); - assert.ok(names.includes('mcp.invoke')); + assert.isOk(names.includes('core_tool_A')); + assert.isOk(names.includes('mcp.search')); + assert.isOk(names.includes('mcp.invoke')); // Should NOT include raw MCP tool(s) in meta mode assert.isFalse(names.includes('mcp:default:delta'));Also applies to: 129-135
front_end/panels/ai_chat/core/AgentNodes.test.ts (3)
5-10: Import Chai assert and types; drop custom global assert declarations.Keeps tests typed and avoids ad-hoc globals.
-import { createToolExecutorNode } from './AgentNodes.js'; -import { ConfigurableAgentTool } from '../agent_framework/ConfigurableAgentTool.js'; -import { ChatMessageEntity } from '../ui/ChatView.js'; -import type { AgentState } from './State.js'; -import type { ConfigurableAgentResult } from '../agent_framework/ConfigurableAgentTool.js'; +import { createToolExecutorNode } from './AgentNodes.js'; +import { ConfigurableAgentTool } from '../agent_framework/ConfigurableAgentTool.js'; +import { ChatMessageEntity } from '../ui/ChatView.js'; +import type { AgentState } from './State.js'; +import type { ConfigurableAgentResult } from '../agent_framework/ConfigurableAgentTool.js'; +import type { ToolResultMessage } from '../models/ChatTypes.js'; +import type { LLMProvider } from '../LLM/LLMTypes.js'; +import { assert } from 'chai';
108-114: Avoid(any)for result message; cast to ToolResultMessage.- const toolResultMessage = result.messages[result.messages.length - 1]; + const toolResultMessage = result.messages[result.messages.length - 1] as ToolResultMessage; @@ - const resultText = (toolResultMessage as any).resultText; + const resultText = toolResultMessage.resultText; @@ - const toolResultMessage = result.messages[result.messages.length - 1]; - const resultText = (toolResultMessage as any).resultText; + const toolResultMessage = result.messages[result.messages.length - 1] as ToolResultMessage; + const resultText = toolResultMessage.resultText;Also applies to: 192-197
123-124: Useassert.isFalsedirectly.- assert.strictEqual(resultText.includes('test-session-123'), false); + assert.isFalse(resultText.includes('test-session-123'));front_end/panels/ai_chat/ui/message/MessageCombiner.ts (2)
32-36: Removeanycast; checkuiLanevia in-operator.- // Skip agent-lane items from chat feed - if ((msg as any).uiLane === 'agent') { + // Skip agent-lane items from chat feed + if ('uiLane' in msg && msg.uiLane === 'agent') { continue; }
49-55: Removeanycast in next-item check.- next && (next as any).uiLane !== 'agent' && next.entity === 'tool_result' && + next && (!('uiLane' in next) || next.uiLane !== 'agent') && next.entity === 'tool_result' &&front_end/panels/ai_chat/core/AgentNodes.ts (2)
10-13: Deduplicate/merge LLM type imports and fix import order (types before values).-import type { LLMMessage } from '../LLM/LLMTypes.js'; -import type { LLMProvider } from '../LLM/LLMTypes.js'; -import { createSystemPromptAsync, getAgentToolsFromState } from './GraphHelpers.js'; -import { ToolSurfaceProvider } from './ToolSurfaceProvider.js'; +import type { LLMMessage, LLMProvider } from '../LLM/LLMTypes.js'; +import { createSystemPromptAsync, getAgentToolsFromState } from './GraphHelpers.js';Then move ToolSurfaceProvider below the TracingProvider type import:
import { createLogger } from './Logger.js'; @@ import type { TracingProvider } from '../tracing/TracingProvider.js'; +import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
129-133: Trace actual per-turn selected tools instead of all agent tools.- tools: getAgentToolsFromState(state).map(t => t.name), + tools: (state.context?.selectedTools ?? getAgentToolsFromState(state)).map(t => t.name),front_end/panels/ai_chat/ui/ChatView.ts (3)
373-376: Avoidanyin lane filter; use a minimal typed cast.- // Lane filter: hide agent-lane items from the main chat feed - if ((message as any).uiLane === 'agent') { + // Lane filter: hide agent-lane items from the main chat feed + if ((message as { uiLane?: 'agent' | 'chat' }).uiLane === 'agent') { return html``; }
495-503: Lit attribute bindings: drop quotes around interpolations.- <span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}">●</span> + <span class="tool-status-marker ${status}" title=${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}>●</span> @@ - <span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}">●</span> + <span class="tool-status-marker ${status}" title=${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}>●</span>
944-962: Prefer state-driven toggling over direct DOM mutation.Keep expanded state in component state (e.g., Set of expanded toolCallIds) and render conditionally instead of querying/modifying DOM. This avoids desync with Lit re-renders and improves testability.
Happy to sketch a minimal Set-based approach keyed by toolCallId if you want it in this PR.
front_end/panels/ai_chat/mcp/MCPMetaTools.ts (1)
5-11: Reorder imports to satisfy import/order (values before types).-import type { Tool } from '../tools/Tools.js'; -import { createLogger } from '../core/Logger.js'; +import { createLogger } from '../core/Logger.js'; import { MCPRegistry } from './MCPRegistry.js'; import { MCPToolAdapter } from './MCPToolAdapter.js'; import { LLMClient } from '../LLM/LLMClient.js'; import { AIChatPanel } from '../ui/AIChatPanel.js'; +import type { Tool } from '../tools/Tools.js';front_end/panels/ai_chat/core/ToolSurfaceProvider.ts (7)
164-177: Meta mode should also respect the allowlist.If users restricted tools, reflect that for meta-tools as well.
- const metaTools = [search, invoke].filter(Boolean) as Tool<any, any>[]; + const { toolAllowlist = [] } = getMCPConfig(); + const allow = new Set(toolAllowlist); + const metaTools = [search, invoke] + .filter(Boolean) + .filter(t => allow.size === 0 || allow.has((t as Tool<any, any>).name)) as Tool<any, any>[];
5-11: Fix import order to satisfy import/order.Place value imports first, keep MCPConfig before MCPRegistry, and move type-only imports after values.
-import type { AgentState } from './State.js'; -import { createLogger } from './Logger.js'; -import type { Tool } from '../tools/Tools.js'; -import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js'; -import { MCPRegistry } from '../mcp/MCPRegistry.js'; -import { getMCPConfig } from '../mcp/MCPConfig.js'; -import { MCPToolAdapter } from '../mcp/MCPToolAdapter.js'; +import { createLogger } from './Logger.js'; +import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js'; +import { getMCPConfig } from '../mcp/MCPConfig.js'; +import { MCPRegistry } from '../mcp/MCPRegistry.js'; +import { MCPToolAdapter } from '../mcp/MCPToolAdapter.js'; +import type { Tool } from '../tools/Tools.js'; +import type { AgentState } from './State.js';
20-30: Use Array and avoid explicit any in generics.Aligns with ESLint rules and improves typing.
-function uniqByName(tools: Tool<any, any>[]): Tool<any, any>[] { - const seen = new Set<string>(); - const out: Tool<any, any>[] = []; +function uniqByName(tools: Array<Tool<unknown, unknown>>): Array<Tool<unknown, unknown>> { + const seen = new Set<string>(); + const out: Array<Tool<unknown, unknown>> = [];
32-34: Apply Array and logger usage consistently in this function.Cleans up types and removes raw console usage.
-function getAllMcpTools(): Tool<any, any>[] { +function getAllMcpTools(): Array<Tool<unknown, unknown>> { @@ - const tools: Tool<any, any>[] = []; + const tools: Array<Tool<unknown, unknown>> = []; @@ - return []; + return [];Also applies to: 44-46, 57-62
64-76: Add braces to single-line ifs and tighten types.Satisfies
curlyand avoidsanyin Tool type.-function scoreTool(query: string, agentType: string | null | undefined, tool: Tool<any, any>): number { +function scoreTool(query: string, agentType: string | null | undefined, tool: Tool<unknown, unknown>): number { const q = (query || '').toLowerCase(); const a = (agentType || '').toLowerCase(); const name = (tool.name || '').toLowerCase(); const desc = (tool.description || '').toLowerCase(); let score = 0; - if (q && name.includes(q)) score += 10; - if (q && desc.includes(q)) score += 3; - if (a && name.includes(a)) score += 2; - if (a && desc.includes(a)) score += 1; + if (q && name.includes(q)) { score += 10; } + if (q && desc.includes(q)) { score += 3; } + if (a && name.includes(a)) { score += 2; } + if (a && desc.includes(a)) { score += 1; } // Prefer MCP tools only slightly lower than strong name matches - if (tool instanceof MCPToolAdapter) score += 0.5; + if (tool instanceof MCPToolAdapter) { score += 0.5; } return score; }
79-110: Avoidanyon globalThis and use logger instead of console.Keeps dev debug hooks while satisfying lint rules.
-// DEBUG: Add a utility function to test MCP modes from console -(globalThis as any).debugToolSelection = { +// DEBUG: Add a utility function to test MCP modes from console +(globalThis as unknown as { debugToolSelection: unknown }).debugToolSelection = { getCurrentMCPConfig: () => { const cfg = getMCPConfig(); - console.log('Current MCP Config:', cfg); + logger.debug('Current MCP Config:', cfg); return cfg; }, testMode: async (mode: 'all' | 'router' | 'meta') => { - const originalConfig = getMCPConfig(); - console.log(`Testing mode: ${mode}`); + const originalConfig = getMCPConfig(); + logger.debug(`Testing mode: ${mode}`); // Temporarily set the mode localStorage.setItem('ai_chat_mcp_tool_mode', mode); @@ - const result = await ToolSurfaceProvider.select(mockState, mockBaseTools); + const result = await ToolSurfaceProvider.select(mockState, mockBaseTools); // Restore original mode - if (originalConfig.toolMode) { - localStorage.setItem('ai_chat_mcp_tool_mode', originalConfig.toolMode); - } - console.log(`Mode ${mode} result:`, result); + if (originalConfig.toolMode) { + localStorage.setItem('ai_chat_mcp_tool_mode', originalConfig.toolMode); + } else { + localStorage.removeItem('ai_chat_mcp_tool_mode'); + } + logger.debug(`Mode ${mode} result:`, result); return result; }, getMCPRegistryStatus: () => { const status = MCPRegistry.getStatus(); - console.log('MCP Registry Status:', status); + logger.debug('MCP Registry Status:', status); return status; } };
112-118: Replace raw console logging with the project logger; remove unused localselectedNames.Eliminates lint errors and keeps consistent logging.
selectedNamesvar is unused.export const ToolSurfaceProvider = { - async select(state: AgentState, baseTools: Tool<any, any>[], opts?: ToolSelectionOptions): Promise<{ tools: Tool<any, any>[]; selectedNames: string[] }> { + async select( + state: AgentState, + baseTools: Array<Tool<unknown, unknown>>, + opts?: ToolSelectionOptions + ): Promise<{ tools: Array<Tool<unknown, unknown>>; selectedNames: string[] }> { @@ - console.log('[TOOL_SELECTION_DEBUG] ToolSurfaceProvider.select called with:', { + logger.debug('[TOOL_SELECTION_DEBUG] ToolSurfaceProvider.select called with:', { @@ - let resultTools: Tool<any, any>[] = uniqByName([...baseTools]); - const selectedNames: string[] = []; + let resultTools: Array<Tool<unknown, unknown>> = uniqByName([...baseTools]); @@ - console.log('[TOOL_SELECTION_DEBUG] Base tools provided:', { + logger.debug('[TOOL_SELECTION_DEBUG] Base tools provided:', { @@ - console.log('[TOOL_SELECTION_DEBUG] MCP disabled, returning core tools only'); + logger.debug('[TOOL_SELECTION_DEBUG] MCP disabled, returning core tools only'); const uniq = uniqByName(resultTools).slice(0, maxToolsPerTurn); - console.log('[TOOL_SELECTION_DEBUG] Final result (MCP disabled):', { + logger.debug('[TOOL_SELECTION_DEBUG] Final result (MCP disabled):', { @@ - console.log('[TOOL_SELECTION_DEBUG] Using ALL mode'); + logger.debug('[TOOL_SELECTION_DEBUG] Using ALL mode'); const mcpTools = getAllMcpTools(); - console.log('[TOOL_SELECTION_DEBUG] MCP tools found:', { + logger.debug('[TOOL_SELECTION_DEBUG] MCP tools found:', { @@ - console.log('[TOOL_SELECTION_DEBUG] Final result (ALL mode):', { + logger.debug('[TOOL_SELECTION_DEBUG] Final result (ALL mode):', { @@ - console.log('[TOOL_SELECTION_DEBUG] Using META mode'); + logger.debug('[TOOL_SELECTION_DEBUG] Using META mode'); @@ - console.log('[TOOL_SELECTION_DEBUG] Meta tools found:', { + logger.debug('[TOOL_SELECTION_DEBUG] Meta tools found:', { @@ - console.log('[TOOL_SELECTION_DEBUG] Final result (META mode):', { + logger.debug('[TOOL_SELECTION_DEBUG] Final result (META mode):', { @@ - console.log('[TOOL_SELECTION_DEBUG] Using ROUTER mode'); + logger.debug('[TOOL_SELECTION_DEBUG] Using ROUTER mode'); @@ - console.log('[TOOL_SELECTION_DEBUG] MCP tools available for scoring:', { + logger.debug('[TOOL_SELECTION_DEBUG] MCP tools available for scoring:', { @@ - const lastUserMsg = [...state.messages].reverse().find(m => m.entity === 'user' || (m as any).entity === 0) as any; + const lastUserMsg = [...(state.messages ?? [])] + .reverse() + .find((m: any) => m.entity === 'user' || m.entity === 0) as any; const queryText = lastUserMsg?.text || ''; - console.log('[TOOL_SELECTION_DEBUG] Query text for scoring:', queryText); + logger.debug('[TOOL_SELECTION_DEBUG] Query text for scoring:', queryText); @@ - console.log('[TOOL_SELECTION_DEBUG] Top scored MCP tools:', { + logger.debug('[TOOL_SELECTION_DEBUG] Top scored MCP tools:', { @@ - console.log('[TOOL_SELECTION_DEBUG] Final result (ROUTER mode):', { + logger.debug('[TOOL_SELECTION_DEBUG] Final result (ROUTER mode):', {Also applies to: 129-147, 149-162, 164-181, 184-201, 208-215
front_end/panels/ai_chat/ui/SettingsDialog.ts (1)
1306-1310: Localize advanced hint through i18n or keep as-is?This hard-coded English string mixes with i18n strings. Consider moving it to UIStrings for consistency.
| // Create ToolExecutorNode | ||
| const toolExecutorNode = createToolExecutorNode(stateWithMockTool); | ||
|
|
||
| // Execute the node | ||
| const result = await toolExecutorNode.invoke(stateWithMockTool); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
createToolExecutorNode signature updated — pass provider and model.
Current test calls will fail to compile.
- const toolExecutorNode = createToolExecutorNode(stateWithMockTool);
+ const toolExecutorNode = createToolExecutorNode(
+ stateWithMockTool,
+ 'test' as unknown as LLMProvider,
+ 'test-model'
+ );
@@
- const toolExecutorNode = createToolExecutorNode(stateWithMockTool);
+ const toolExecutorNode = createToolExecutorNode(
+ stateWithMockTool,
+ 'test' as unknown as LLMProvider,
+ 'test-model'
+ );Also applies to: 189-191
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/AgentNodes.test.ts around lines 99-104 (and
similarly at 189-191), the test calls createToolExecutorNode with a single
argument but the function signature now requires provider and model parameters;
update the test to pass the provider and model when creating the node (e.g.,
extract or mock provider and model from stateWithMockTool or create small mock
values) so the call matches the new signature and the test compiles.
| const q = (args.query || '').toLowerCase(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Remove unused variable and add braces for single-line ifs (curly).
- const q = (args.query || '').toLowerCase();
+ // no-op
@@
- if (!key.startsWith('mcp:')) continue;
+ if (!key.startsWith('mcp:')) { continue; }
@@
- if (serverFilter) {
+ if (serverFilter) {
const serverId = key.split(':')[1] || '';
- if (!serverFilter.has(serverId)) continue;
+ if (!serverFilter.has(serverId)) { continue; }
}
@@
- if (!tool) continue;
+ if (!tool) { continue; }Also applies to: 127-134
🧰 Tools
🪛 ESLint
[error] 121-121: 'q' is assigned a value but never used.
(@typescript-eslint/no-unused-vars)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/mcp/MCPMetaTools.ts around lines 121-122 (and
similarly 127-134), remove the unused variable declaration "const q =
(args.query || '').toLowerCase();" and update any single-line if statements to
use braces; if code later referenced the lowercased query, inline the value
where needed as (args.query || '').toLowerCase() or create a used variable with
a descriptive name, otherwise delete the unused declaration, and add { ... }
around all single-line if bodies in the indicated ranges to enforce consistent
curly-brace style.
|
Claude finished @tysonthomas9's task —— View job MCP Integration PR ReviewI've conducted a comprehensive review of this MCP integration PR. Here's my detailed feedback: Review Tasks:
🔴 Critical Security Issues1. Token Storage Vulnerability (HIGH)File: localStorage.setItem(KEYS.token, config.token);Issue: Storing authentication tokens in 2. Tool Argument Redaction Bug (CRITICAL)File: const sanitized = this.sanitize(args);
return this.client.callTool(this.serverId, this.def.name, sanitized, { timeoutMs: 30000 });Issue: The code redacts secrets from arguments then sends the redacted version to MCP server, breaking authentication for any tool requiring API keys/tokens. 3. XSS Risk in Settings Dialog (MEDIUM)File: mcpStatusDetails.innerHTML = detailsHtml;Issue: Using 🔴 Critical Functionality Issues4. LLM Function Name Compatibility (BLOCKER)File: tools: tools.map(tool => ({
function: {
name: tool.name, // Names like "mcp:default:alpha" will be rejected by OpenAI
description: tool.description,
parameters: tool.schema,
}
}))Issue: MCP tool names containing colons/dots (e.g., 5. Integration Test CI Failure (HIGH)File: describe('connects to local Hacker News MCP server via SDK', function() {
const HACKER_NEWS_SERVER: MCPServer = {
endpoint: 'http://localhost:5001/sse',
};Issue: This integration test will fail/hang in CI where no local MCP server runs.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
front_end/panels/ai_chat/ui/__tests__/SettingsDialogOpenRouterCache.test.ts (1)
35-45: Restore mocked globals to avoid cross-test leakage.Capture originals and restore in
afterEachfor bothLLMClient.fetchOpenRouterModelsandwindow.localStorageto keep tests isolated.Apply:
@@ // Mock LLMClient.fetchOpenRouterModels - fetchOpenRouterModelsCalls = []; - LLMClient.fetchOpenRouterModels = async (apiKey: string) => { + fetchOpenRouterModelsCalls = []; + const originalFetchOpenRouterModels = LLMClient.fetchOpenRouterModels; + LLMClient.fetchOpenRouterModels = async (apiKey: string) => { fetchOpenRouterModelsCalls.push({ apiKey, timestamp: mockCurrentTime }); return [ { id: 'openai/gpt-4', name: 'GPT-4' }, { id: 'anthropic/claude-3-sonnet', name: 'Claude 3 Sonnet' }, { id: 'meta-llama/llama-2-70b-chat', name: 'Llama 2 70B' }, ]; }; @@ - // Restore original Date.now - Date.now = originalDateNow; - mockLocalStorage.clear(); - fetchOpenRouterModelsCalls = []; + // Restore originals to avoid leakage across tests + Date.now = originalDateNow; + fetchOpenRouterModelsCalls = []; + try { LLMClient.fetchOpenRouterModels = originalFetchOpenRouterModels; } catch {} + try { + // Restore localStorage to a clean stubbed object + Object.defineProperty(window, 'localStorage', { + value: { getItem: () => null, setItem: () => {}, removeItem: () => {}, clear: () => {} }, + writable: true, + }); + } catch {}
♻️ Duplicate comments (7)
front_end/panels/ai_chat/ui/AIChatPanel.ts (4)
88-91: Move and dedupe MCP imports (lint: import/first, import/order, no-duplicates, no-unused-vars)Place MCP imports with the main import group and combine duplicates. Also remove the unused
getMCPConfigif you gate below (see next comment).-// MCP integration -import { MCPRegistry } from '../mcp/MCPRegistry.js'; -import { getMCPConfig } from '../mcp/MCPConfig.js'; -import { onMCPConfigChange } from '../mcp/MCPConfig.js'; +// MCP integration +import { MCPRegistry } from '../mcp/MCPRegistry.js'; +import { getMCPConfig, onMCPConfigChange } from '../mcp/MCPConfig.js';Then move this block up with the other imports (before
chatViewStyles).
722-722: Unsubscribe on teardown to prevent leaks (and satisfy no-unused-private-class-members)
#mcpUnsubscribeis assigned but never invoked. Call it inwillHide().override willHide(): void { // Explicitly remove any event listeners to prevent memory leaks + if (this.#mcpUnsubscribe) { + this.#mcpUnsubscribe(); + this.#mcpUnsubscribe = null; + }
789-809: Gate MCP init/refresh behind config and add explicit return typeAvoid unconditional connect; respect user settings (e.g.,
enabled,autostart). Also add the missing return type for the inner async function (lint).- #setupMCPIntegration(): void { - const initAndRefresh = async () => { + #setupMCPIntegration(): void { + const initAndRefresh = async (): Promise<void> => { try { - // Always attempt to connect to MCP on startup - await MCPRegistry.init(); - await MCPRegistry.refresh(); + const cfg = getMCPConfig?.(); + if (cfg?.enabled && cfg?.autostart) { + await MCPRegistry.init(); + await MCPRegistry.refresh(); + } const status = MCPRegistry.getStatus(); logger.info('MCP auto-connect completed', status); } catch (err) { logger.error('Failed to initialize MCP', err); } }; void initAndRefresh(); // Subscribe to config changes this.#mcpUnsubscribe = onMCPConfigChange(() => { void initAndRefresh(); }); }
2329-2334: Also gate MCP re-init after settings changesSkip MCP init/refresh when disabled.
- try { - await MCPRegistry.init(); - await MCPRegistry.refresh(); - } catch (err) { + try { + const cfg = getMCPConfig?.(); + if (cfg?.enabled) { + await MCPRegistry.init(); + await MCPRegistry.refresh(); + } + } catch (err) { logger.error('Failed to reinitialize MCP after settings change', err); - } + }front_end/panels/ai_chat/ui/SettingsDialog.ts (3)
2658-2663: Add explicit return type (ESLint: explicit-function-return-type)
2816-2827: Add explicit return type and clamp logic remains unchanged
2850-2859: Use safe clearing and read fresh config for allowlist
🧹 Nitpick comments (14)
front_end/panels/ai_chat/ui/message/MessageList.ts (1)
68-68: Use reserved-space variable for bottom padding to avoid input overlay overlapHard-coding
padding-bottom: 12pxcan let the floating input bar cover the last message. Prefer the same variable used in chatView.css so spacing stays consistent.- padding-bottom: 12px; + padding-bottom: var(--ai-input-bar-reserved-space, 12px);front_end/panels/ai_chat/ui/__tests__/LiveAgentSessionComponent.test.ts (1)
79-89: Assert computed display, not inline styleChecking
element.style.displayis brittle (it only reflects inline styles). UsegetComputedStylefor robustness.- // Default expanded - assert.strictEqual(timeline.style.display, 'block'); + // Default expanded + assert.strictEqual(getComputedStyle(timeline).display, 'block'); @@ - assert.strictEqual((el.shadowRoot!.querySelector('.timeline-items') as HTMLElement).style.display, 'none'); + assert.strictEqual(getComputedStyle(el.shadowRoot!.querySelector('.timeline-items') as HTMLElement).display, 'none'); @@ - assert.strictEqual((el.shadowRoot!.querySelector('.timeline-items') as HTMLElement).style.display, 'block'); + assert.strictEqual(getComputedStyle(el.shadowRoot!.querySelector('.timeline-items') as HTMLElement).display, 'block');front_end/panels/ai_chat/ui/chatView.css (3)
14-15: Surface the reserved-space variableGood addition. Consider documenting expected input bar heights and where this variable should be overridden (e.g., via container or theme) to avoid drift.
46-53: z-index of overlay vs timeline/status markersThe input bar is
z-index: 5, but some UI markers use far higher z-indices, which can pop above the bar. If intentional, ignore; otherwise raise the input bar's z-index or reduce local marker z-indices to avoid visual overlap near the bottom.
103-107: Avoid magic number: align expanded padding with the variableUse the same variable as the legacy container to keep spacing consistent across views.
-.chat-view-container.expanded-view { - padding-bottom: 150px; /* tune if input height changes */ -} +.chat-view-container.expanded-view { + padding-bottom: var(--ai-input-bar-reserved-space); +}front_end/panels/ai_chat/ui/AIChatPanel.ts (1)
2086-2088: Remove unused HelpDialog import or fallback to dialog when pop-up blocking occursSince Help now opens docs in a new tab,
HelpDialogis likely unused. Remove the import to satisfy lint. Alternatively, catch failures and open the dialog as a fallback. Example (import removal):- import { HelpDialog } from './HelpDialog.js';front_end/panels/ai_chat/ui/ChatView.ts (1)
945-963: Avoid direct DOM toggling for state; store expand/collapse in component stateDirectly modifying
style.displayis fragile. Track expansion in class state and render conditionally so the state survives re-renders.I can provide a patch using
#expandedToolCalls = new Set<string>()keyed bytoolCallId.front_end/panels/ai_chat/ui/__tests__/ChatViewAgentSessions.test.ts (2)
95-111: Top-level promotion test: great coverage; tiny flake guardYou already added
doubleRaf(). Consider asserting with a small retry loop if this still flakes in CI.
413-424: Remove unused variable
beforeisn’t used.- // Capture initial state, then toggle once - const before = (sroot.querySelector('.timeline-items') as HTMLElement).style.display; + // Toggle onceconfig/gni/devtools_grd_files.gni (2)
869-871: Keep third_party entries alphabetically ordered (place marked before mcp-sdk).Current order has mcp-sdk before marked. Swap to retain strict alpha ordering within the third_party block.
Apply this diff:
- "front_end/third_party/mcp-sdk/mcp-sdk.js", - "front_end/third_party/marked/marked.js", + "front_end/third_party/marked/marked.js", + "front_end/third_party/mcp-sdk/mcp-sdk.js",
2212-2221: Trim duplicate Zod entrypoint — keep index.mjsfront_end/third_party/mcp-sdk/package/dist/types.js imports "../../zod/lib/index.mjs"; remove the unused index.js entry from the vendored lists to avoid bundling an unused entrypoint.
Locations: config/gni/devtools_grd_files.gni (lines ~2220–2221), front_end/third_party/mcp-sdk/BUILD.gn (lines ~23–24).
- "front_end/third_party/mcp-sdk/zod/lib/index.js", "front_end/third_party/mcp-sdk/zod/lib/index.mjs",front_end/panels/ai_chat/ui/__tests__/SettingsDialogOpenRouterCache.test.ts (1)
115-141: Optional: exercise the real auto-refresh path once integration lands.When the dialog implements auto-refresh, add an integration-style test that opens the selector (emits
model-selector-focus) and asserts a single fetch when cache is stale.front_end/panels/ai_chat/ui/model_selector/ModelSelector.ts (1)
86-90: Optional a11y polish for the trigger.Consider adding
aria-haspopup="listbox"andaria-expanded=${this.#open}to the trigger button for better accessibility.- <button class="model-select-trigger" @click=${this.#toggle} ?disabled=${this.#disabled}> + <button class="model-select-trigger" + @click=${this.#toggle} + ?disabled=${this.#disabled} + aria-haspopup="listbox" + aria-expanded=${String(this.#open)}>front_end/panels/ai_chat/ui/SettingsDialog.ts (1)
448-456: Consider replacinganytypes with proper interfacesThe static model selector references use
anytypes. Consider defining proper interfaces for type safety:- static #openaiMiniModelSelect: any | null = null; - static #openaiNanoModelSelect: any | null = null; + static #openaiMiniModelSelect: HTMLElement | null = null; + static #openaiNanoModelSelect: HTMLElement | null = null;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
config/gni/devtools_grd_files.gni(5 hunks)front_end/panels/ai_chat/ui/AIChatPanel.ts(6 hunks)front_end/panels/ai_chat/ui/ChatView.ts(7 hunks)front_end/panels/ai_chat/ui/HelpDialog.ts(2 hunks)front_end/panels/ai_chat/ui/SettingsDialog.ts(16 hunks)front_end/panels/ai_chat/ui/__tests__/ChatViewAgentSessions.test.ts(7 hunks)front_end/panels/ai_chat/ui/__tests__/LiveAgentSessionComponent.test.ts(1 hunks)front_end/panels/ai_chat/ui/__tests__/SettingsDialogOpenRouterCache.test.ts(3 hunks)front_end/panels/ai_chat/ui/chatView.css(5 hunks)front_end/panels/ai_chat/ui/input/InputBar.ts(1 hunks)front_end/panels/ai_chat/ui/message/MessageList.ts(1 hunks)front_end/panels/ai_chat/ui/message/StructuredResponseRender.ts(1 hunks)front_end/panels/ai_chat/ui/message/__tests__/MessageCombiner.test.ts(1 hunks)front_end/panels/ai_chat/ui/model_selector/ModelSelector.ts(3 hunks)
✅ Files skipped from review due to trivial changes (1)
- front_end/panels/ai_chat/ui/message/StructuredResponseRender.ts
🧰 Additional context used
🧬 Code graph analysis (6)
front_end/panels/ai_chat/ui/message/__tests__/MessageCombiner.test.ts (2)
front_end/panels/ai_chat/models/ChatTypes.ts (1)
ChatMessage(74-75)front_end/panels/ai_chat/ui/message/MessageCombiner.ts (1)
combineMessages(26-103)
front_end/panels/ai_chat/ui/model_selector/ModelSelector.ts (1)
front_end/panels/ai_chat/ui/AIChatPanel.ts (1)
ModelOption(96-100)
front_end/panels/ai_chat/ui/__tests__/SettingsDialogOpenRouterCache.test.ts (1)
front_end/panels/ai_chat/ui/AIChatPanel.ts (4)
window(1047-1094)apiKey(1166-1197)apiKey(1239-1258)apiKey(1578-1612)
front_end/panels/ai_chat/ui/__tests__/ChatViewAgentSessions.test.ts (4)
front_end/testing/DOMHelpers.ts (1)
doubleRaf(257-259)front_end/panels/ai_chat/ui/ChatView.ts (1)
session(315-328)front_end/panels/ai_chat/core/AgentService.ts (1)
session(660-700)front_end/panels/ai_chat/ui/LiveAgentSessionComponent.ts (2)
session(35-35)session(36-36)
front_end/panels/ai_chat/ui/SettingsDialog.ts (3)
front_end/panels/ai_chat/ui/AIChatPanel.ts (11)
localStorage(814-837)currentProvider(875-935)currentProvider(2342-2401)SettingsDialog(2093-2107)ModelOption(96-100)error(1930-1943)logger(949-1042)logger(1333-1403)logger(1426-1453)logger(1560-1573)logger(1617-1620)front_end/panels/ai_chat/ui/input/InputBar.ts (2)
modelOptions(35-35)selectedModel(36-36)front_end/panels/ai_chat/ui/model_selector/ModelSelector.ts (2)
ModelOption(10-10)ModelOption(62-66)
front_end/panels/ai_chat/ui/ChatView.ts (3)
front_end/panels/ai_chat/ui/AIChatPanel.ts (8)
data(1529-1540)data(2006-2041)event(1671-1678)event(1683-1687)event(1692-1696)event(1701-1705)event(1710-1714)event(1719-1723)front_end/panels/ai_chat/ui/markdown/MarkdownRenderers.ts (1)
renderMarkdown(63-81)front_end/panels/ai_chat/core/AgentService.ts (1)
event(621-657)
🪛 ESLint
front_end/panels/ai_chat/ui/message/__tests__/MessageCombiner.test.ts
[error] 40-40: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 42-42: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 43-43: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 44-44: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 50-50: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 51-51: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 57-57: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 59-59: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 61-61: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
front_end/panels/ai_chat/ui/model_selector/ModelSelector.ts
[error] 42-42: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
[error] 44-44: Expected { after 'if' condition.
(curly)
front_end/panels/ai_chat/ui/__tests__/ChatViewAgentSessions.test.ts
[error] 320-320: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 321-321: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 353-353: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 354-354: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 413-413: 'before' is assigned a value but never used.
(@typescript-eslint/no-unused-vars)
[error] 418-418: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
front_end/panels/ai_chat/ui/AIChatPanel.ts
[error] 89-89: Import in body of module; reorder to top.
(import/first)
[error] 89-89: ../mcp/MCPRegistry.js import should occur before import of ./chatView.css.js
(import/order)
[error] 90-90: Import in body of module; reorder to top.
(import/first)
[error] 90-90: ../mcp/MCPConfig.js import should occur before import of ./chatView.css.js
(import/order)
[error] 90-90: 'getMCPConfig' is defined but never used.
(@typescript-eslint/no-unused-vars)
[error] 90-90: '../mcp/MCPConfig.js' imported multiple times.
(import/no-duplicates)
[error] 91-91: Import in body of module; reorder to top.
(import/first)
[error] 91-91: ../mcp/MCPConfig.js import should occur before import of ./chatView.css.js
(import/order)
[error] 91-91: '../mcp/MCPConfig.js' imported multiple times.
(import/no-duplicates)
[error] 722-722: '#mcpUnsubscribe' is defined but never used.
(no-unused-private-class-members)
[error] 794-794: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
front_end/panels/ai_chat/ui/SettingsDialog.ts
[error] 399-399: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 431-431: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 448-448: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 449-449: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 450-450: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 451-451: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 452-452: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 453-453: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 454-454: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 455-455: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 540-540: 'e' is defined but never used.
(@typescript-eslint/no-unused-vars)
[error] 932-932: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
[error] 932-932: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 938-938: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 940-940: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 954-954: Prefer template literals over imperative DOM API calls
(rulesdir/no-imperative-dom-api)
[error] 959-959: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 961-961: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 2567-2567: Expected { after 'if' condition.
(curly)
[error] 2572-2572: Expected { after 'if' condition.
(curly)
[error] 2573-2573: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 2590-2590: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
[error] 2605-2605: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 2618-2618: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 2658-2658: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
[error] 2817-2817: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
[error] 2850-2850: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
[error] 2853-2853: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 2885-2885: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 3836-3836: Prefer template literals over imperative DOM API calls
(rulesdir/no-imperative-dom-api)
[error] 3850-3850: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
front_end/panels/ai_chat/ui/ChatView.ts
[error] 375-375: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 496-496: When interpolating a value as an attribute in Lit you do not need double quotes around it.
(rulesdir/lit-no-attribute-quotes)
[error] 503-503: When interpolating a value as an attribute in Lit you do not need double quotes around it.
(rulesdir/lit-no-attribute-quotes)
[error] 953-953: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
🪛 ast-grep (0.38.6)
front_end/panels/ai_chat/ui/SettingsDialog.ts
[warning] 2612-2612: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: mcpStatusDetails.innerHTML = detailsHtml
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html
(unsafe-html-content-assignment)
[warning] 2625-2625: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: mcpStatusDetails.innerHTML = detailsHtml
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html
(unsafe-html-content-assignment)
[warning] 2595-2595: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: mcpStatusDetails.innerHTML = ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html
(dom-content-modification)
[warning] 2612-2612: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: mcpStatusDetails.innerHTML = detailsHtml
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html
(dom-content-modification)
[warning] 2625-2625: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: mcpStatusDetails.innerHTML = detailsHtml
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html
(dom-content-modification)
[warning] 2851-2851: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: mcpToolsList.innerHTML = ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html
(dom-content-modification)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: claude-review
🔇 Additional comments (20)
front_end/panels/ai_chat/ui/HelpDialog.ts (1)
217-217: Confirm updated i18n key usage across localesSwitched from
alphaVersionWarningtobetaVersionWarning. Ensure translations and message catalogs are regenerated so this key resolves in all locales.front_end/panels/ai_chat/ui/chatView.css (2)
64-64: Overflow hidden: confirm focus/scroll into viewWith
.chat-view-container { overflow: hidden; }, ensure focusing elements near the bottom (e.g., via keyboard) still scrolls the inner list correctly and isn't clipped.
556-556: Spacing increase: verify it doesn’t push content under the overlayRaising
.message-actions-rowmargin-bottom to 28px can interact with the overlayed input bar. Validate last-message visibility on small viewports.front_end/panels/ai_chat/ui/ChatView.ts (1)
374-377: Lane filter looks goodHiding
uiLane === 'agent'in the main feed aligns with the lane-based design.front_end/panels/ai_chat/ui/__tests__/ChatViewAgentSessions.test.ts (3)
4-4: Good addition of doubleRaf for render-settleThis de-flakes timing around ScheduledRender.
260-265: Nested child verification is solidAsserting via nested shadow DOM is the right approach here.
320-355: Lane flags in tests improve clarityMarking agent-managed items with
uiLane: 'agent'matches production routing.config/gni/devtools_grd_files.gni (1)
1383-1383: LGTM: new unbundled helper entry.
front_end/panels/ai_assistance/components/ScrollPinHelper.jsentry looks fine.front_end/panels/ai_chat/ui/__tests__/SettingsDialogOpenRouterCache.test.ts (2)
90-92: Use of window.localStorage.getItem is correct in the mocked environment.Aligned with the localStorage shim added in beforeEach; good.
148-150: Consistent localStorage API usage for API key read.Good switch to
window.localStorage.getItem(...); matches the mock and production access pattern.front_end/panels/ai_chat/ui/input/InputBar.ts (1)
95-101: PreferAbove binding matches new ModelSelector API; verify UX polarity.
.preferAbove=${!this.#centered}wires dropdown to open above when not centered. Confirm this matches intended layout (e.g., centered layout may need above to avoid overflow).front_end/panels/ai_chat/ui/model_selector/ModelSelector.ts (3)
22-35: Public flags for positioning/searchability look good.Encapsulated via getters/setters; render reacts correctly.
68-69: Searchability threshold with override is reasonable.
#isSearchable()usingforceSearchable || options.length >= 20is clear and safe.
91-93: Dropdown positioning respects preferAbove.Class toggle will let CSS control placement; ensure styles define
.model-dropdown.above|.below.front_end/panels/ai_chat/ui/SettingsDialog.ts (5)
11-12: LGTM: MCP integration imports are correctly structuredThe new MCP imports (
getMCPConfig,setMCPConfig,isMCPEnabled,MCPRegistry) follow proper module organization and provide the necessary abstractions for MCP functionality integration.
355-436: Comprehensive UI strings for MCP integrationThe addition of MCP-related UI strings is well-structured and covers all necessary user-facing elements including configuration options, tool management, and help text. The internationalization approach is consistent with the existing codebase.
2501-2927: MCP section implementation is comprehensive but hidden by designThe MCP integration section includes all necessary functionality - configuration inputs, status monitoring, tool management, and budget controls. The decision to hide it by default (line 2505:
mcpSection.style.display = 'none') aligns with the auto-connect approach mentioned in comments.The implementation covers:
- Configuration management with proper validation
- Real-time status updates with connection monitoring
- Tool discovery and per-tool allowlist management
- Advanced budget controls for tool selection
3825-3871: Model selector component integration looks solidThe
createModelSelectorhelper function properly integrates the newai-model-selectorcomponent while maintaining backward compatibility. The API bridge (lines 3857-3863) that exposes a.valueproperty similar to native<select>elements is a good design choice for existing code compatibility.
3797-3823: Static method for OAuth integration is well-implementedThe
updateOpenRouterModelsstatic method provides a clean interface for updating model options after OAuth authentication. The event-driven approach with custom events and localStorage caching is appropriate for this use case.front_end/panels/ai_chat/ui/message/__tests__/MessageCombiner.test.ts (1)
42-44: Replace explicit any casts with a narrow test-local ChatMessage type and use ChatMessageEntity constants.eslint couldn't be run in the sandbox: "eslint: command not found". Run eslint locally and confirm the lint errors clear after applying the diff.
File: front_end/panels/ai_chat/ui/message/tests/MessageCombiner.test.ts (lines 42-44, 50-51)
- { entity: ChatMessageEntity.MODEL, action: 'tool', toolName: 'fetch', toolCallId, isFinalAnswer: false, uiLane: 'agent' } as any, - { entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":true}', isError: false, uiLane: 'agent' } as any, - { entity: ChatMessageEntity.AGENT_SESSION, agentSession: { sessionId: 's1', agentName: 'agent', status: 'running', startTime: new Date(), messages: [], nestedSessions: [] } } as any, + { entity: ChatMessageEntity.MODEL, action: 'tool', toolName: 'fetch', toolCallId, isFinalAnswer: false, uiLane: 'agent' } as ChatMessage, + { entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":true}', isError: false, uiLane: 'agent' } as ChatMessage, + { entity: ChatMessageEntity.AGENT_SESSION, agentSession: { sessionId: 's1', agentName: 'agent', status: 'running', startTime: new Date(), messages: [], nestedSessions: [] } } as ChatMessage,- assert.strictEqual((combined[0] as any).entity, 'user'); - assert.strictEqual((combined[1] as any).entity, 'agent_session'); + assert.strictEqual((combined[0] as ChatMessage).entity, ChatMessageEntity.USER); + assert.strictEqual((combined[1] as ChatMessage).entity, ChatMessageEntity.AGENT_SESSION);Define the local type once (outside the changed range):
// Replace the current alias: // type ChatMessage = any; type ChatMessage = { entity: string; [k: string]: unknown };
| return html` | ||
| <!-- Reasoning (if any) displayed above the timeline --> | ||
| ${toolReasoning ? html` | ||
| <div class="message-text reasoning-text" style="margin-bottom: 8px;"> | ||
| ${renderMarkdown(toolReasoning, this.#markdownRenderer)} | ||
| </div> | ||
| ` : Lit.nothing} | ||
| <!-- Timeline Tool Execution --> | ||
| <div class="agent-execution-timeline single-tool"> | ||
| <!-- Tool Header --> | ||
| <div class="agent-header"> | ||
| <div class="agent-marker"></div> | ||
| <div class="agent-title">${descriptionData.action}</div> | ||
| <div class="agent-divider"></div> | ||
| <button class="tool-toggle" @click=${(e: Event) => this.#toggleToolResult(e)}> | ||
| <span class="toggle-icon">▼</span> | ||
| </button> | ||
| </div> | ||
| // <div class="timeline-items" style="display: none;"> | ||
| // <div class="timeline-item"> | ||
| // <div class="tool-line"> | ||
| // ${descriptionData.isMultiLine ? html` | ||
| // <div class="tool-summary"> | ||
| // <span class="tool-description"> | ||
| // <span class="tool-description-indicator">└─</span> | ||
| // <div>${(descriptionData.content as Array<{key: string, value: string}>)[0]?.value || 'multiple parameters'}</div> | ||
| // </span> | ||
| // <span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}">●</span> | ||
| // </div> | ||
| // ` : html` | ||
| // <span class="tool-description"> | ||
| // <span class="tool-description-indicator">└─</span> | ||
| // <div>${descriptionData.content}</div> | ||
| // </span> | ||
| // <span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}">●</span> | ||
| // `} | ||
| // </div> | ||
| <div class="timeline-items" style="display: none;"> | ||
| <div class="timeline-item"> | ||
| <div class="tool-line"> | ||
| ${descriptionData.isMultiLine ? html` | ||
| <div class="tool-summary"> | ||
| <span class="tool-description"> | ||
| <span class="tool-description-indicator">└─</span> | ||
| <div>${(descriptionData.content as Array<{key: string, value: string}>)[0]?.value || 'multiple parameters'}</div> | ||
| </span> | ||
| <span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}">●</span> | ||
| </div> | ||
| ` : html` | ||
| <span class="tool-description"> | ||
| <span class="tool-description-indicator">└─</span> | ||
| <div>${descriptionData.content}</div> | ||
| </span> | ||
| <span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}">●</span> | ||
| `} | ||
| </div> | ||
| // <!-- Result Block - Integrated within timeline item --> | ||
| // ${isCombined && resultText ? html` | ||
| // <div class="tool-result-integrated ${status}"> | ||
| // Response: | ||
| // ${this.#formatJsonWithSyntaxHighlighting(resultText)} | ||
| // </div> | ||
| // ` : Lit.nothing} | ||
| // </div> | ||
| // </div> | ||
| <!-- Result Block - Integrated within timeline item --> | ||
| ${isCombined && resultText ? html` | ||
| <div class="tool-result-integrated ${status}"> | ||
| Response: | ||
| ${this.#formatJsonWithSyntaxHighlighting(resultText)} | ||
| </div> | ||
| ` : Lit.nothing} | ||
| </div> | ||
| </div> | ||
| // <!-- Loading spinner for running tools --> | ||
| // ${status === 'running' ? html` | ||
| // <div class="tool-loading"> | ||
| // <svg class="loading-spinner" width="16" height="16" viewBox="0 0 16 16"> | ||
| // <circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="30 12" stroke-linecap="round"> | ||
| // <animateTransform | ||
| // attributeName="transform" | ||
| // attributeType="XML" | ||
| // type="rotate" | ||
| // from="0 8 8" | ||
| // to="360 8 8" | ||
| // dur="1s" | ||
| // repeatCount="indefinite" /> | ||
| // </circle> | ||
| // </svg> | ||
| // </div> | ||
| // ` : Lit.nothing} | ||
|
|
||
| // <!-- Error messages --> | ||
| // ${modelMessage.error ? html`<div class="message-error tool-error-message">Model Error: ${modelMessage.error}</div>` : Lit.nothing} | ||
| // </div> | ||
| // `; | ||
| <!-- Loading spinner for running tools --> | ||
| ${status === 'running' ? html` | ||
| <div class="tool-loading"> | ||
| <svg class="loading-spinner" width="16" height="16" viewBox="0 0 16 16"> | ||
| <circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="30 12" stroke-linecap="round"> | ||
| <animateTransform | ||
| attributeName="transform" | ||
| attributeType="XML" | ||
| type="rotate" | ||
| from="0 8 8" | ||
| to="360 8 8" | ||
| dur="1s" | ||
| repeatCount="indefinite" /> | ||
| </circle> | ||
| </svg> | ||
| </div> | ||
| ` : Lit.nothing} | ||
| <!-- Error messages --> | ||
| ${modelMessage.error ? html`<div class="message-error tool-error-message">Model Error: ${modelMessage.error}</div>` : Lit.nothing} | ||
| </div> | ||
| `; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fix Lit attribute interpolation and persist toggle state
- Lit rule: remove quotes around interpolated
titlevalues. - Optional: toggled state is DOM-driven and will reset on rerender. Persist by tracking expanded tool-call IDs.
- <span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}">●</span>
+ <span class="tool-status-marker ${status}" title=${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}>●</span>
@@
- <span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}">●</span>
+ <span class="tool-status-marker ${status}" title=${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}>●</span>If you want persistence, I can propose a small Set<string> keyed by toolCallId to track expanded items.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return html` | |
| <!-- Reasoning (if any) displayed above the timeline --> | |
| ${toolReasoning ? html` | |
| <div class="message-text reasoning-text" style="margin-bottom: 8px;"> | |
| ${renderMarkdown(toolReasoning, this.#markdownRenderer)} | |
| </div> | |
| ` : Lit.nothing} | |
| <!-- Timeline Tool Execution --> | |
| <div class="agent-execution-timeline single-tool"> | |
| <!-- Tool Header --> | |
| <div class="agent-header"> | |
| <div class="agent-marker"></div> | |
| <div class="agent-title">${descriptionData.action}</div> | |
| <div class="agent-divider"></div> | |
| <button class="tool-toggle" @click=${(e: Event) => this.#toggleToolResult(e)}> | |
| <span class="toggle-icon">▼</span> | |
| </button> | |
| </div> | |
| // <div class="timeline-items" style="display: none;"> | |
| // <div class="timeline-item"> | |
| // <div class="tool-line"> | |
| // ${descriptionData.isMultiLine ? html` | |
| // <div class="tool-summary"> | |
| // <span class="tool-description"> | |
| // <span class="tool-description-indicator">└─</span> | |
| // <div>${(descriptionData.content as Array<{key: string, value: string}>)[0]?.value || 'multiple parameters'}</div> | |
| // </span> | |
| // <span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}">●</span> | |
| // </div> | |
| // ` : html` | |
| // <span class="tool-description"> | |
| // <span class="tool-description-indicator">└─</span> | |
| // <div>${descriptionData.content}</div> | |
| // </span> | |
| // <span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}">●</span> | |
| // `} | |
| // </div> | |
| <div class="timeline-items" style="display: none;"> | |
| <div class="timeline-item"> | |
| <div class="tool-line"> | |
| ${descriptionData.isMultiLine ? html` | |
| <div class="tool-summary"> | |
| <span class="tool-description"> | |
| <span class="tool-description-indicator">└─</span> | |
| <div>${(descriptionData.content as Array<{key: string, value: string}>)[0]?.value || 'multiple parameters'}</div> | |
| </span> | |
| <span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}">●</span> | |
| </div> | |
| ` : html` | |
| <span class="tool-description"> | |
| <span class="tool-description-indicator">└─</span> | |
| <div>${descriptionData.content}</div> | |
| </span> | |
| <span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}">●</span> | |
| `} | |
| </div> | |
| // <!-- Result Block - Integrated within timeline item --> | |
| // ${isCombined && resultText ? html` | |
| // <div class="tool-result-integrated ${status}"> | |
| // Response: | |
| // ${this.#formatJsonWithSyntaxHighlighting(resultText)} | |
| // </div> | |
| // ` : Lit.nothing} | |
| // </div> | |
| // </div> | |
| <!-- Result Block - Integrated within timeline item --> | |
| ${isCombined && resultText ? html` | |
| <div class="tool-result-integrated ${status}"> | |
| Response: | |
| ${this.#formatJsonWithSyntaxHighlighting(resultText)} | |
| </div> | |
| ` : Lit.nothing} | |
| </div> | |
| </div> | |
| // <!-- Loading spinner for running tools --> | |
| // ${status === 'running' ? html` | |
| // <div class="tool-loading"> | |
| // <svg class="loading-spinner" width="16" height="16" viewBox="0 0 16 16"> | |
| // <circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="30 12" stroke-linecap="round"> | |
| // <animateTransform | |
| // attributeName="transform" | |
| // attributeType="XML" | |
| // type="rotate" | |
| // from="0 8 8" | |
| // to="360 8 8" | |
| // dur="1s" | |
| // repeatCount="indefinite" /> | |
| // </circle> | |
| // </svg> | |
| // </div> | |
| // ` : Lit.nothing} | |
| // <!-- Error messages --> | |
| // ${modelMessage.error ? html`<div class="message-error tool-error-message">Model Error: ${modelMessage.error}</div>` : Lit.nothing} | |
| // </div> | |
| // `; | |
| <!-- Loading spinner for running tools --> | |
| ${status === 'running' ? html` | |
| <div class="tool-loading"> | |
| <svg class="loading-spinner" width="16" height="16" viewBox="0 0 16 16"> | |
| <circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="30 12" stroke-linecap="round"> | |
| <animateTransform | |
| attributeName="transform" | |
| attributeType="XML" | |
| type="rotate" | |
| from="0 8 8" | |
| to="360 8 8" | |
| dur="1s" | |
| repeatCount="indefinite" /> | |
| </circle> | |
| </svg> | |
| </div> | |
| ` : Lit.nothing} | |
| <!-- Error messages --> | |
| ${modelMessage.error ? html`<div class="message-error tool-error-message">Model Error: ${modelMessage.error}</div>` : Lit.nothing} | |
| </div> | |
| `; | |
| } | |
| return html` | |
| <!-- Reasoning (if any) displayed above the timeline --> | |
| ${toolReasoning ? html` | |
| <div class="message-text reasoning-text" style="margin-bottom: 8px;"> | |
| ${renderMarkdown(toolReasoning, this.#markdownRenderer)} | |
| </div> | |
| ` : Lit.nothing} | |
| <!-- Timeline Tool Execution --> | |
| <div class="agent-execution-timeline single-tool"> | |
| <!-- Tool Header --> | |
| <div class="agent-header"> | |
| <div class="agent-marker"></div> | |
| <div class="agent-title">${descriptionData.action}</div> | |
| <div class="agent-divider"></div> | |
| <button class="tool-toggle" @click=${(e: Event) => this.#toggleToolResult(e)}> | |
| <span class="toggle-icon">▼</span> | |
| </button> | |
| </div> | |
| <div class="timeline-items" style="display: none;"> | |
| <div class="timeline-item"> | |
| <div class="tool-line"> | |
| ${descriptionData.isMultiLine ? html` | |
| <div class="tool-summary"> | |
| <span class="tool-description"> | |
| <span class="tool-description-indicator">└─</span> | |
| <div>${(descriptionData.content as Array<{key: string, value: string}>)[0]?.value || 'multiple parameters'}</div> | |
| </span> | |
| <span class="tool-status-marker ${status}" title=${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}>●</span> | |
| </div> | |
| ` : html` | |
| <span class="tool-description"> | |
| <span class="tool-description-indicator">└─</span> | |
| <div>${descriptionData.content}</div> | |
| </span> | |
| <span class="tool-status-marker ${status}" title=${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}>●</span> | |
| `} | |
| </div> | |
| <!-- Result Block - Integrated within timeline item --> | |
| ${isCombined && resultText ? html` | |
| <div class="tool-result-integrated ${status}"> | |
| Response: | |
| ${this.#formatJsonWithSyntaxHighlighting(resultText)} | |
| </div> | |
| ` : Lit.nothing} | |
| </div> | |
| </div> | |
| <!-- Loading spinner for running tools --> | |
| ${status === 'running' ? html` | |
| <div class="tool-loading"> | |
| <svg class="loading-spinner" width="16" height="16" viewBox="0 0 16 16"> | |
| <circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="30 12" stroke-linecap="round"> | |
| <animateTransform | |
| attributeName="transform" | |
| attributeType="XML" | |
| type="rotate" | |
| from="0 8 8" | |
| to="360 8 8" | |
| dur="1s" | |
| repeatCount="indefinite" /> | |
| </circle> | |
| </svg> | |
| </div> | |
| ` : Lit.nothing} | |
| <!-- Error messages --> | |
| ${modelMessage.error ? html`<div class="message-error tool-error-message">Model Error: ${modelMessage.error}</div>` : Lit.nothing} | |
| </div> | |
| `; | |
| } |
🧰 Tools
🪛 ESLint
[error] 496-496: When interpolating a value as an attribute in Lit you do not need double quotes around it.
(rulesdir/lit-no-attribute-quotes)
[error] 503-503: When interpolating a value as an attribute in Lit you do not need double quotes around it.
(rulesdir/lit-no-attribute-quotes)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/ui/ChatView.ts around lines 467-539, the Lit
template is incorrectly interpolating attribute values with quotes (e.g.
title="${...}") which violates Lit rules, and the tool-result toggle is
DOM-driven so it will reset on rerender; remove the quotes so attributes use
unquoted binding (title=${...}) and replace the DOM-only toggle with a
component-level state: add a private Set<string> (e.g. #expandedToolCalls) keyed
by toolCallId, update the toggle handler to add/remove the toolCallId from the
Set, and drive the timeline visibility/result rendering from that Set (use
conditional rendering or set a class/style based on Set.has(toolCallId)) so the
expanded/collapsed state persists across rerenders.
| it('hides agent-lane tool_result even if it arrives before model tool-call', () => { | ||
| const toolCallId = 'tc-outoforder'; | ||
| const messages: ChatMessage[] = [ | ||
| { entity: ChatMessageEntity.USER, text: 'go' } as any, | ||
| // Agent-managed tool result first | ||
| { entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":1}', isError: false, isFromConfigurableAgent: true } as any, | ||
| { entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":1}', isError: false, uiLane: 'agent' } as any, | ||
| // Model tool call later | ||
| { entity: ChatMessageEntity.MODEL, action: 'tool', toolName: 'fetch', toolCallId, isFinalAnswer: false } as any, | ||
| ]; | ||
|
|
||
| const combined = combineMessages(messages); | ||
| // Expect: user only (both the agent-managed result and matching model call removed) | ||
| // Expect: user only (both the agent-lane result and matching model call removed) | ||
| assert.lengthOf(combined, 1); | ||
| assert.strictEqual((combined[0] as any).entity, 'user'); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Test expectation conflicts with current combineMessages logic; model tool-call won’t be removed.
combineMessages only skips items with uiLane === 'agent'. A later model tool-call without uiLane is still emitted, even if an agent-lane tool_result with the same toolCallId appeared earlier. Result: this test will fail against the current implementation.
Choose one:
- Option A (adjust test to current behavior).
- // Expect: user only (both the agent-lane result and matching model call removed)
- assert.lengthOf(combined, 1);
- assert.strictEqual((combined[0] as any).entity, 'user');
+ // Expect: user + model tool-call (agent-lane result hidden, model call still visible)
+ assert.lengthOf(combined, 2);
+ assert.strictEqual((combined[0] as ChatMessage).entity, ChatMessageEntity.USER);
+ assert.strictEqual((combined[1] as ChatMessage).entity, ChatMessageEntity.MODEL);
+ assert.strictEqual((combined[1] as { action: string }).action, 'tool');- Option B (preferred if product intent is to hide the model call once an agent handled it): update MessageCombiner to drop model tool-calls whose toolCallId was already seen in an agent-lane tool_result.
Apply this to front_end/panels/ai_chat/ui/message/MessageCombiner.ts:
export function combineMessages(messages: ChatMessage[]): CombinedMessage[] {
const result: CombinedMessage[] = [];
+ // Pre-scan for agent-lane tool-results so we can hide later model tool-calls they fulfilled.
+ const agentLaneToolResultCallIds = new Set<string>();
+ for (const m of messages) {
+ if ((m as any).uiLane === 'agent' && m.entity === 'tool_result' && (m as ToolResultMessage).toolCallId) {
+ agentLaneToolResultCallIds.add((m as ToolResultMessage).toolCallId!);
+ }
+ }
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
@@
- if (msg.entity === 'model' && (msg as ModelChatMessage).action === 'tool') {
+ if (msg.entity === 'model' && (msg as ModelChatMessage).action === 'tool') {
const modelMsg = msg as ModelChatMessage;
+ // If an agent-lane tool_result already exists for this call, hide the model call.
+ if (modelMsg.toolCallId && agentLaneToolResultCallIds.has(modelMsg.toolCallId)) {
+ continue;
+ }
const next = messages[i + 1];Also remove explicit any in this test segment:
- { entity: ChatMessageEntity.USER, text: 'go' } as any,
+ { entity: ChatMessageEntity.USER, text: 'go' } as ChatMessage,
- { entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":1}', isError: false, uiLane: 'agent' } as any,
+ { entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":1}', isError: false, uiLane: 'agent' } as ChatMessage,
- { entity: ChatMessageEntity.MODEL, action: 'tool', toolName: 'fetch', toolCallId, isFinalAnswer: false } as any,
+ { entity: ChatMessageEntity.MODEL, action: 'tool', toolName: 'fetch', toolCallId, isFinalAnswer: false } as ChatMessage,🏁 Script executed:
#!/usr/bin/env bash
# 1) Lint the test file to ensure no-explicit-any violations are resolved.
eslint front_end/panels/ai_chat/ui/message/__tests__/MessageCombiner.test.ts
# 2) Grep combineMessages for the new agentLaneToolResultCallIds check (if Option B applied).
rg -n "agentLaneToolResultCallIds|uiLane === 'agent'" front_end/panels/ai_chat/ui/message/MessageCombiner.ts -C3
# 3) Run just this spec (adjust to your test runner).
pnpm -w test front_end/panels/ai_chat/ui/message/__tests__/MessageCombiner.test.tsLength of output: 266
Test expectation conflicts with combineMessages — update test or make MessageCombiner hide model tool-calls already fulfilled by agent-lane results
combineMessages currently only skips items with uiLane === 'agent' and does not drop later model tool-calls that share the same toolCallId; the spec will fail against the current implementation.
Choose one:
- Option A (adjust test to current behavior).
- // Expect: user only (both the agent-lane result and matching model call removed)
- assert.lengthOf(combined, 1);
- assert.strictEqual((combined[0] as any).entity, 'user');
+ // Expect: user + model tool-call (agent-lane result hidden, model call still visible)
+ assert.lengthOf(combined, 2);
+ assert.strictEqual((combined[0] as ChatMessage).entity, ChatMessageEntity.USER);
+ assert.strictEqual((combined[1] as ChatMessage).entity, ChatMessageEntity.MODEL);
+ assert.strictEqual((combined[1] as { action: string }).action, 'tool');- Option B (preferred if product intent is to hide the model call once an agent handled it): update MessageCombiner to drop model tool-calls whose toolCallId was already seen in an agent-lane tool_result.
Apply to front_end/panels/ai_chat/ui/message/MessageCombiner.ts:
export function combineMessages(messages: ChatMessage[]): CombinedMessage[] {
const result: CombinedMessage[] = [];
+ // Pre-scan for agent-lane tool-results so we can hide later model tool-calls they fulfilled.
+ const agentLaneToolResultCallIds = new Set<string>();
+ for (const m of messages) {
+ if ((m as any).uiLane === 'agent' && m.entity === 'tool_result' && (m as ToolResultMessage).toolCallId) {
+ agentLaneToolResultCallIds.add((m as ToolResultMessage).toolCallId!);
+ }
+ }
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
@@
- if (msg.entity === 'model' && (msg as ModelChatMessage).action === 'tool') {
+ if (msg.entity === 'model' && (msg as ModelChatMessage).action === 'tool') {
+ // If an agent-lane tool_result already exists for this call, hide the model call.
+ const modelMsg = msg as ModelChatMessage;
+ if (modelMsg.toolCallId && agentLaneToolResultCallIds.has(modelMsg.toolCallId)) {
+ continue;
+ }
+ // (rest unchanged)
+ const next = messages[i + 1];Also remove explicit any in the test segment:
- { entity: ChatMessageEntity.USER, text: 'go' } as any,
- { entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":1}', isError: false, uiLane: 'agent' } as any,
- { entity: ChatMessageEntity.MODEL, action: 'tool', toolName: 'fetch', toolCallId, isFinalAnswer: false } as any,
+ { entity: ChatMessageEntity.USER, text: 'go' } as ChatMessage,
+ { entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":1}', isError: false, uiLane: 'agent' } as ChatMessage,
+ { entity: ChatMessageEntity.MODEL, action: 'tool', toolName: 'fetch', toolCallId, isFinalAnswer: false } as ChatMessage,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('hides agent-lane tool_result even if it arrives before model tool-call', () => { | |
| const toolCallId = 'tc-outoforder'; | |
| const messages: ChatMessage[] = [ | |
| { entity: ChatMessageEntity.USER, text: 'go' } as any, | |
| // Agent-managed tool result first | |
| { entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":1}', isError: false, isFromConfigurableAgent: true } as any, | |
| { entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":1}', isError: false, uiLane: 'agent' } as any, | |
| // Model tool call later | |
| { entity: ChatMessageEntity.MODEL, action: 'tool', toolName: 'fetch', toolCallId, isFinalAnswer: false } as any, | |
| ]; | |
| const combined = combineMessages(messages); | |
| // Expect: user only (both the agent-managed result and matching model call removed) | |
| // Expect: user only (both the agent-lane result and matching model call removed) | |
| assert.lengthOf(combined, 1); | |
| assert.strictEqual((combined[0] as any).entity, 'user'); | |
| }); | |
| it('hides agent-lane tool_result even if it arrives before model tool-call', () => { | |
| const toolCallId = 'tc-outoforder'; | |
| const messages: ChatMessage[] = [ | |
| { entity: ChatMessageEntity.USER, text: 'go' } as ChatMessage, | |
| // Agent-managed tool result first | |
| { entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":1}', isError: false, uiLane: 'agent' } as ChatMessage, | |
| // Model tool call later | |
| { entity: ChatMessageEntity.MODEL, action: 'tool', toolName: 'fetch', toolCallId, isFinalAnswer: false } as ChatMessage, | |
| ]; | |
| const combined = combineMessages(messages); | |
| // Expect: user only (both the agent-lane result and matching model call removed) | |
| assert.lengthOf(combined, 1); | |
| assert.strictEqual((combined[0] as ChatMessage).entity, ChatMessageEntity.USER); | |
| }); |
🧰 Tools
🪛 ESLint
[error] 57-57: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 59-59: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 61-61: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 67-67: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
| #toggle = (e: Event) => { | ||
| e.preventDefault(); | ||
| if (this.#disabled) return; | ||
| const wasOpen = this.#open; | ||
| this.#open = !this.#open; | ||
| this.#render(); | ||
| if (!wasOpen && this.#open) { | ||
| // Notify host that the selector opened (used to lazily refresh models) | ||
| this.dispatchEvent(new CustomEvent('model-selector-focus', {bubbles: true})); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix ESLint issues on #toggle (explicit return type + curly).
Lints complain about missing return type and single-line if. Add : void and braces.
Apply:
- #toggle = (e: Event) => {
+ #toggle = (e: Event): void => {
e.preventDefault();
- if (this.#disabled) return;
+ if (this.#disabled) { return; }
const wasOpen = this.#open;
this.#open = !this.#open;
this.#render();
if (!wasOpen && this.#open) {
// Notify host that the selector opened (used to lazily refresh models)
this.dispatchEvent(new CustomEvent('model-selector-focus', {bubbles: true}));
}
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| #toggle = (e: Event) => { | |
| e.preventDefault(); | |
| if (this.#disabled) return; | |
| const wasOpen = this.#open; | |
| this.#open = !this.#open; | |
| this.#render(); | |
| if (!wasOpen && this.#open) { | |
| // Notify host that the selector opened (used to lazily refresh models) | |
| this.dispatchEvent(new CustomEvent('model-selector-focus', {bubbles: true})); | |
| } | |
| }; | |
| #toggle = (e: Event): void => { | |
| e.preventDefault(); | |
| if (this.#disabled) { return; } | |
| const wasOpen = this.#open; | |
| this.#open = !this.#open; | |
| this.#render(); | |
| if (!wasOpen && this.#open) { | |
| // Notify host that the selector opened (used to lazily refresh models) | |
| this.dispatchEvent(new CustomEvent('model-selector-focus', {bubbles: true})); | |
| } | |
| }; |
🧰 Tools
🪛 ESLint
[error] 42-42: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
[error] 44-44: Expected { after 'if' condition.
(curly)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/ui/model_selector/ModelSelector.ts around lines 42
to 52, the private method #toggle is missing an explicit return type and uses a
single-line if without braces which breaks ESLint rules; update the method
signature to include an explicit return type of void and add curly braces around
the single-line if (this.#disabled) return; so it becomes a block with braces to
satisfy the linter, keeping the rest of the logic unchanged.
|
Claude finished @tysonthomas9's task —— View job I'll analyze this and get back to you. |
|
Claude finished @tysonthomas9's task —— View job Code Review: MCP Integration PR #44I've conducted a comprehensive code review of the MCP (Model Context Protocol) integration. Here's my analysis: Code Quality & Best Practices ⭐Strengths:
Areas for improvement:
Test Coverage ✅Good coverage found:
Missing tests:
Potential Bugs
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
front_end/panels/ai_chat/tools/Tools.ts (4)
55-61: Unify API key flow: prefer ctx.apiKey; remove AgentService coupling in toolsYou added apiKey to LLMContext (nice), but confirmWith404LLM still pulls credentials from AgentService, bypassing the provided context and the per‑turn flow. Gate on ctx.apiKey instead to keep a single source of truth.
@@ export interface LLMContext { - apiKey?: string; + apiKey?: string; provider: LLMProvider; model: string; @@ private async confirmWith404LLM(url: string, title: string, content: string, ctx?: LLMContext): Promise<boolean> { try { - const agentService = AgentService.getInstance(); - const apiKey = agentService.getApiKey(); - - if (!apiKey) { + // Use per-call context key to avoid global coupling + if (!ctx?.apiKey) { logger.warn('No API key available for 404 confirmation'); return false; }Also applies to: 805-814
661-669: Fix broken interpolation in navigation logTemplate string is in single quotes; ${url} won’t interpolate.
- logger.info('Initiating navigation to: ${url}'); + logger.info(`Initiating navigation to: ${url}`);
1123-1155: Sanitize selector to prevent JS injection in evaluate()Directly embedding selector can break the expression or enable injection. Use JSON.stringify and refer to a local variable.
- expression: `(() => { - const element = document.querySelector("${selector}"); + expression: `(() => { + const sel = ${JSON.stringify(selector)}; + const element = document.querySelector(sel); if (!element) { return { success: false, - message: "Element not found with selector: ${selector}" + message: "Element not found with selector: " + sel }; }
1199-1206: Sanitize query and numbers in SearchContentTool evaluate()Embed query via JSON.stringify to avoid breaking the script; keep limit numeric.
- expression: `(() => { - const query = "${query}"; - const limit = ${limit}; + expression: `(() => { + const query = ${JSON.stringify(query)}; + const limit = ${Number(limit)};Also applies to: 1268-1276
front_end/panels/ai_chat/agent_framework/AgentRunner.ts (1)
645-653: LLMClient doesn't accept per-call apiKey — model calls will not pick up per-run keys
- LLMClient.call's request type has no apiKey; providers are instantiated in initialize() with providerConfig.apiKey and looked up from LLMProviderRegistry. (front_end/panels/ai_chat/LLM/LLMClient.ts — initialize ≈ line 68; call ≈ line 123)
- AgentRunner invokes llm.call(...) without an apiKey (front_end/panels/ai_chat/agent_framework/AgentRunner.ts — lines 645–653 and 1370–1394), so per‑turn keys passed to tools won’t affect the main model call.
- Actionable fixes: either ensure LLMClient.initialize(...) is called per run with the run's provider apiKey before calling llm.call, or add apiKey to LLMCallRequest and update LLMClient.call + provider selection/creation to honor per-call apiKey.
front_end/panels/ai_chat/core/AgentService.ts (1)
255-268: Prevent secret/API-key leakage to logs & tracing
- AgentService.ts: remove storing API keys in state/context — lines ~200–203 ((this.#state as any).context.apiKey = ...) and ~396–397 (injecting apiKey into local
statebefore graph invoke). Don’t stash secrets in any object that may be passed to tracing/graph.- AgentService.ts: remove/replace debug warnings that emit un‑sanitized state/context. Locations: ~320–346 (createTrace + logger.debug) and ~380–420 (console.warns). Log only non‑secret primitives (traceId, messageCount, booleans).
- Tracing calls: ensure tracingProvider.createTrace / createObservation never serialize state/context or tool args containing secrets. Sanitize payloads before sending (strip keys: apiKey, api_key, token, secret, password, authorization, etc.).
- Other files logging secret metadata: ui/AIChatPanel.ts (1051–1056, 1504–1509) and LLM/OpenRouterProvider.ts (627–633) — stop logging API key length/prefix (log existence only).
- Action: add/reuse a central sanitizer (evaluation/utils/SanitizationUtils exists) and run tests/assertions to confirm no secret keys are present in any trace/log payloads.
front_end/panels/ai_chat/BUILD.gn (1)
348-364: Tests not wired into GN: add missing test files.These two tests won’t run in CI without being listed.
sources = [ @@ "agent_framework/__tests__/AgentRunner.run.flows.test.ts", + "core/__tests__/AgentNodesSanitize.test.ts", + "core/ToolNameMapping.test.ts", "mcp/MCPClientSDK.test.ts", "core/ToolSurfaceProvider.test.ts", ]front_end/panels/ai_chat/core/AgentNodes.ts (1)
384-419: Remove non-null assertions in tool-call replay.Narrow types instead of using ! to satisfy @typescript-eslint/no-non-null-assertion.
- } else if ('action' in msg && msg.action === 'tool' && 'toolName' in msg && 'toolArgs' in msg && 'toolCallId' in msg) { - // Tool call message - convert from ModelChatMessage structure - const fnName = originalToSanitized?.[msg.toolName!] || ToolNameMap.getSanitized(msg.toolName!); + } else if ('action' in msg && msg.action === 'tool' && typeof (msg as any).toolName === 'string' && typeof (msg as any).toolCallId === 'string') { + // Tool call message - convert from ModelChatMessage structure + const { toolName, toolArgs, toolCallId } = + msg as ModelChatMessage & { toolName: string; toolArgs: unknown; toolCallId: string }; + const fnName = originalToSanitized?.[toolName] || ToolNameMap.getSanitized(toolName); llmMessages.push({ role: 'assistant', content: undefined, tool_calls: [{ - id: msg.toolCallId!, + id: toolCallId, type: 'function' as const, function: { name: fnName, - arguments: JSON.stringify(msg.toolArgs), + arguments: JSON.stringify(toolArgs), } }], });
♻️ Duplicate comments (10)
front_end/panels/ai_chat/mcp/MCPConfig.ts (2)
1-4: Add Chromium BSD license header (repo rule)This file is missing the standard header.
+// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import { createLogger } from '../core/Logger.js';
38-41: Validate toolMode and harden numeric parsing (avoid NaN bleed-through)Don’t trust persisted strings; fence toolMode and guard parseInt.
- const toolMode = (localStorage.getItem(KEYS.toolMode) as MCPConfigData['toolMode']) || 'router'; - const maxToolsPerTurn = parseInt(localStorage.getItem(KEYS.maxToolsPerTurn) || '20', 10); - const maxMcpPerTurn = parseInt(localStorage.getItem(KEYS.maxMcpPerTurn) || '8', 10); + const rawMode = localStorage.getItem(KEYS.toolMode); + const toolMode: MCPConfigData['toolMode'] = + rawMode === 'all' || rawMode === 'router' || rawMode === 'meta' ? rawMode : 'router'; + const parseIntSafe = (v: string|null, d: number) => { + const n = Number.parseInt(v ?? '', 10); + return Number.isFinite(n) ? n : d; + }; + const maxToolsPerTurn = parseIntSafe(localStorage.getItem(KEYS.maxToolsPerTurn), 20); + const maxMcpPerTurn = parseIntSafe(localStorage.getItem(KEYS.maxMcpPerTurn), 8);front_end/panels/ai_chat/core/ToolNameMapping.test.ts (2)
15-18: Droppublic; make ctor param readonly.-class RecordingTool implements Tool<Record<string, unknown>, unknown> { - public calls = 0; - constructor(public name: string) {} +class RecordingTool implements Tool<Record<string, unknown>, unknown> { + calls = 0; + constructor(readonly name: string) {}
26-45: Restore real localStorage after each test.-describe('AgentNodes sanitized tool name mapping', () => { - let mockLocalStorage: Map<string, string>; +describe('AgentNodes sanitized tool name mapping', () => { + let mockLocalStorage: Map<string, string>; + let realLocalStorage: Storage; @@ - Object.defineProperty(window, 'localStorage', { + realLocalStorage = window.localStorage; + Object.defineProperty(window, 'localStorage', { value: { @@ writable: true, }); }); afterEach(() => { mockLocalStorage.clear(); + Object.defineProperty(window, 'localStorage', { value: realLocalStorage, writable: true }); });front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (1)
1-4: Add license header and fix import order.+// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + -import type { Tool } from '../tools/Tools.js'; -import { createLogger } from '../core/Logger.js'; -import type { MCPClient, MCPToolDef } from '../../../third_party/mcp-sdk/mcp-sdk.js'; +import { createLogger } from '../core/Logger.js'; +import type { MCPClient, MCPToolDef } from '../../../third_party/mcp-sdk/mcp-sdk.js'; +import type { Tool } from '../tools/Tools.js';front_end/panels/ai_chat/core/AgentNodes.ts (1)
154-159: Persist per‑turn selected tool instances and a name map.Executor can then honor exactly the same set and resolve sanitized↔original lookups.
- (state.context as any).selectedToolNames = selection.selectedNames; - const tools = selection.tools; + (state.context as any).selectedToolNames = selection.selectedNames; + const tools = selection.tools; + (state.context as any).selectedTools = tools; + (state.context as any).toolNameMap = Object.fromEntries( + (tools as any[]).map(t => [ToolNameMap.getSanitized(t.name), t.name]) + );front_end/panels/ai_chat/mcp/MCPRegistry.ts (3)
21-29: Use namespace client type to satisfy import rule.Construct MCPClient from the namespace.
- private client = new MCPClient(); + private client = new MCPSDK.MCPClient();
140-155: Report actual tool counts per server.Replace hardcoded 0 with a filtered count of registered MCP tools.
getStatus(): MCPRegistryStatus { return { enabled: getMCPConfig().enabled, servers: this.servers.map(s => ({ id: s.id, endpoint: s.endpoint, connected: this.client.isConnected(s.id), - toolCount: 0, + toolCount: this.registeredTools.filter(name => name.startsWith(`mcp:${s.id}:`)).length, })),
1-8: Add license header and fix third_party import per lint (namespace import + type aliases).Resolves check-license-header, es-modules-import, import/order, and inline-type-imports.
+// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + -import { createLogger } from '../core/Logger.js'; -import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js'; -import * as ToolNameMap from '../core/ToolNameMap.js'; -import type { MCPToolDef, MCPServer } from '../../../third_party/mcp-sdk/mcp-sdk.js'; -import { MCPClient } from '../../../third_party/mcp-sdk/mcp-sdk.js'; +import * as MCPSDK from '../../../third_party/mcp-sdk/mcp-sdk.js'; + +import { createLogger } from '../core/Logger.js'; +import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js'; +import * as ToolNameMap from '../core/ToolNameMap.js'; +type MCPToolDef = MCPSDK.MCPToolDef; +type MCPServer = MCPSDK.MCPServer;front_end/panels/ai_chat/ui/SettingsDialog.ts (1)
2874-2925: Avoid innerHTML for clearing and read fresh allowlist.Prevents XSS flags and stale config use.
- const updateToolsList = () => { + const updateToolsList = (): void => { const status = MCPRegistry.getStatus(); - mcpToolsList.innerHTML = ''; + mcpToolsList.replaceChildren(); @@ - const currentAllowlist = new Set(currentMCPConfig.toolAllowlist || []); + const { toolAllowlist } = getMCPConfig(); + const currentAllowlist = new Set(toolAllowlist || []);
🧹 Nitpick comments (24)
front_end/panels/ai_chat/tools/Tools.ts (2)
892-899: Use appropriate log levels in NavigateBackToolRegular control‑flow logs should not use error level.
- logger.error('navigate_back', args); + logger.info('navigate_back', args); @@ - logger.error('Navigation completed, document ready state is complete'); + logger.info('Navigation completed, document ready state is complete'); @@ - logger.error('Still waiting for navigation to complete...'); + logger.debug('Still waiting for navigation to complete...'); @@ - if (!isNavigationComplete) { - logger.error('Navigation timed out after waiting for document ready state'); - } + if (!isNavigationComplete) { + logger.warn('Navigation timed out after waiting for document ready state'); + }Also applies to: 954-969
1458-1492: Optional: pass apiKey from ctx to LLMClient call sitesSeveral tool LLM calls (e.g., WaitTool) ignore ctx.apiKey. If LLMClient supports per‑call keys, pass it to avoid reliance on globals.
Would you like me to scan LLMClient for an apiKey parameter and wire it through these calls?
front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts (2)
18-29: Use semicolons in interface members (lint fix)ESLint flags member-delimiter-style; switch commas to semicolons.
export interface CallCtx { - apiKey?: string, - provider?: LLMProvider, - model?: string, - miniModel?: string, - nanoModel?: string, - mainModel?: string, - getVisionCapability?: (modelName: string) => Promise<boolean> | boolean, - overrideSessionId?: string, - overrideParentSessionId?: string, - overrideTraceId?: string, + apiKey?: string; + provider?: LLMProvider; + model?: string; + miniModel?: string; + nanoModel?: string; + mainModel?: string; + getVisionCapability?: (modelName: string) => Promise<boolean> | boolean; + overrideSessionId?: string; + overrideParentSessionId?: string; + overrideTraceId?: string; }
400-409: Type the context parameter to CallCtxAvoid unknown and redundant cast.
- async execute(args: ConfigurableAgentArgs, _ctx?: unknown): Promise<ConfigurableAgentResult & { agentSession: AgentSession }> { + async execute(args: ConfigurableAgentArgs, _ctx?: CallCtx): Promise<ConfigurableAgentResult & { agentSession: AgentSession }> { @@ - const callCtx = (_ctx || {}) as CallCtx; + const callCtx: CallCtx = _ctx ?? {};front_end/panels/ai_chat/core/ToolNameMap.ts (1)
13-18: Add curly braces for single-line if statements (lint fix)Satisfy curly rule and keep style consistent.
function sanitize(original: string): string { let name = original.replace(/[^a-zA-Z0-9_-]/g, '_'); - if (!name) name = 'tool'; - if (name.length > 64) name = name.slice(0, 64); + if (!name) { name = 'tool'; } + if (name.length > 64) { name = name.slice(0, 64); } return name; } @@ export function addMapping(original: string): string { const existing = originalToSanitized.get(original); - if (existing) return existing; + if (existing) { return existing; }Also applies to: 35-41
front_end/panels/ai_chat/mcp/MCPConfig.ts (1)
91-95: Annotate local callback type to satisfy explicit-return-type lintESLint flags the inline arrow; add a type annotation.
-export function onMCPConfigChange(handler: () => void): () => void { - const cb = () => handler(); +export function onMCPConfigChange(handler: () => void): () => void { + const cb: () => void = () => handler(); window.addEventListener('ai_chat_mcp_config_changed', cb); return () => window.removeEventListener('ai_chat_mcp_config_changed', cb); }front_end/panels/ai_chat/core/__tests__/AgentNodesSanitize.test.ts (2)
70-73: Satisfy lint: preferassert.isOk.- assert.ok(capturedTools && capturedTools.length === 1, 'tools should be passed'); + assert.isOk(capturedTools && capturedTools.length === 1, 'tools should be passed');
15-38: Reduceanyin stubs for better type-safety.Optional: type
originalGetInstanceandcapturedToolsprecisely to quiet mostno-explicit-anyhits.Also applies to: 41-41, 45-58
front_end/panels/ai_chat/core/ToolNameMapping.test.ts (1)
53-64: Reduceanyin stubs; remove unused casts.Optional cleanups to satisfy most lint errors on this file.
Also applies to: 71-82
front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (1)
32-36: Make timeout configurable (optional).Expose timeout via ctor param or config; default 30s.
- return this.client.callTool(this.serverId, this.def.name, args, { timeoutMs: 30000 }); + const timeoutMs = 30_000; + return this.client.callTool(this.serverId, this.def.name, args, { timeoutMs });front_end/panels/ai_chat/BUILD.gn (1)
17-60: Keep lists sorted for minimal diffs (optional).Consider alphabetical order in sources for maintainability.
Also applies to: 152-266
front_end/panels/ai_chat/core/AgentNodes.ts (5)
36-37: Remove extra blank line.ESLint: no-multiple-empty-lines.
- - +
151-159: Type the tools list; avoid any and trailing whitespace.Use ReturnType and drop unnecessary casts.
- const baseTools = BaseOrchestratorAgent.getAgentTools(state.selectedAgentType ?? '') as any; - const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 }); + const baseTools: ReturnType<typeof getTools> = + BaseOrchestratorAgent.getAgentTools(state.selectedAgentType ?? '') as ReturnType<typeof getTools>; + const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 }); // Persist selection in context so ToolExecutorNode can resolve the same set if (!state.context) { (state as any).context = {}; } (state.context as any).selectedToolNames = selection.selectedNames; - const tools = selection.tools; - + const tools: ReturnType<typeof getTools> = selection.tools as ReturnType<typeof getTools>; +
444-460: Alias toolMap entries by sanitized names too (makes lookups robust).Optional but future-proof if any caller accidentally uses sanitized names.
- (tools as any[]).forEach((tool: any) => toolMap.set(tool.name, tool)); + (tools as any[]).forEach((tool: any) => { + toolMap.set(tool.name, tool); // original + const sanitized = ToolNameMap.getSanitized(tool.name); + if (sanitized !== tool.name) toolMap.set(sanitized, tool); // alias + });
157-157: Trim trailing whitespace.Stylistic lint error.
253-253: Trim trailing whitespace.Stylistic lint error.
front_end/panels/ai_chat/mcp/MCPRegistry.ts (2)
56-64: Clear ToolNameMap on init is good; also clear on dispose to avoid stale mappings.Add ToolNameMap.clear() and reset registeredTools.
dispose(): void { for (const srv of this.servers) { try { this.client.disconnect(srv.id); } catch {} } this.lastDisconnected = new Date(); this.servers = []; + this.registeredTools = []; + try { ToolNameMap.clear(); } catch {} }
11-19: Inline object type in interface triggers member-delimiter-style; extract a named type.Prevents the “Expected a comma” lint.
-export interface MCPRegistryStatus { +export interface MCPServerStatus { + id: string; + endpoint: string; + connected: boolean; + toolCount: number; +} + +export interface MCPRegistryStatus { enabled: boolean; - servers: Array<{ id: string; endpoint: string; connected: boolean; toolCount: number }>; + servers: Array<MCPServerStatus>; registeredToolNames: string[]; lastError?: string;front_end/panels/ai_chat/ui/SettingsDialog.ts (6)
2584-2651: Harden status rendering and add return type.Avoid innerHTML (already done), but annotate function and keep safe node building.
- const updateMCPStatus = () => { + const updateMCPStatus = (): void => {
2682-2687: Annotate updateDisconnectButton return type.Satisfies explicit-function-return-type.
- const updateDisconnectButton = () => { + const updateDisconnectButton = (): void => {
2840-2854: Annotate budget controls handler.Satisfies explicit-function-return-type.
- const updateBudgetControls = () => { + const updateBudgetControls = (): void => {
446-456: Replace any with typed custom element for model selectors.Improves type-safety and removes no-explicit-any errors.
-export class SettingsDialog { +interface AIModelSelectorElement extends HTMLElement { + options: ModelOption[]; + selected: string; + value: string; + forceSearchable?: boolean; +} + +export class SettingsDialog { // Variables to store direct references to model selectors - static #openaiMiniModelSelect: any | null = null; - static #openaiNanoModelSelect: any | null = null; - static #litellmMiniModelSelect: any | null = null; - static #litellmNanoModelSelect: any | null = null; - static #groqMiniModelSelect: any | null = null; - static #groqNanoModelSelect: any | null = null; - static #openrouterMiniModelSelect: any | null = null; - static #openrouterNanoModelSelect: any | null = null; + static #openaiMiniModelSelect: AIModelSelectorElement | null = null; + static #openaiNanoModelSelect: AIModelSelectorElement | null = null; + static #litellmMiniModelSelect: AIModelSelectorElement | null = null; + static #litellmNanoModelSelect: AIModelSelectorElement | null = null; + static #groqMiniModelSelect: AIModelSelectorElement | null = null; + static #groqNanoModelSelect: AIModelSelectorElement | null = null; + static #openrouterMiniModelSelect: AIModelSelectorElement | null = null; + static #openrouterNanoModelSelect: AIModelSelectorElement | null = null;
923-955: Type the selector element in helpers and drop any.Satisfies no-explicit-any and explicit return type in helper.
- function refreshModelSelectOptions(select: any, models: ModelOption[], currentValue: string, defaultLabel: string) { + function refreshModelSelectOptions(select: HTMLSelectElement | AIModelSelectorElement, models: ModelOption[], currentValue: string, defaultLabel: string): void { @@ - if (select && select.tagName && select.tagName.toLowerCase() === 'ai-model-selector') { + if ((select as HTMLElement)?.tagName?.toLowerCase() === 'ai-model-selector') { const previousValue = select.value || select.selected || ''; const opts = [{ value: '', label: defaultLabel }, ...models]; - select.options = opts; + (select as AIModelSelectorElement).options = opts; if (previousValue && opts.some((o: any) => o.value === previousValue)) { - select.value = previousValue; + (select as AIModelSelectorElement).value = previousValue; } else if (currentValue && opts.some((o: any) => o.value === currentValue)) { - select.value = currentValue; + (select as AIModelSelectorElement).value = currentValue; } else { - select.value = ''; + (select as AIModelSelectorElement).value = ''; } return; }- const selectorEl = document.createElement('ai-model-selector') as any; + const selectorEl = document.createElement('ai-model-selector') as AIModelSelectorElement;Also applies to: 3874-3895
3073-3080: Fix UI grammar in disclaimer text.User-facing copy nit.
- <strong>Beta Version:</strong> This is an beta version of the Browser Operator - AI Assistant feature. + <strong>Beta Version:</strong> This is a beta version of the Browser Operator - AI Assistant feature. @@ - <strong>Provider Support:</strong> We currently support OpenAI, Groq and OpenRouter providers directly. And we support LiteLLM as a proxy to access 100+ other models. + <strong>Provider Support:</strong> We currently support OpenAI, Groq, and OpenRouter directly, and LiteLLM as a proxy to access 100+ other models.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
config/gni/devtools_grd_files.gni(5 hunks)front_end/panels/ai_chat/BUILD.gn(7 hunks)front_end/panels/ai_chat/agent_framework/AgentRunner.ts(1 hunks)front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts(3 hunks)front_end/panels/ai_chat/core/AgentNodes.ts(12 hunks)front_end/panels/ai_chat/core/AgentService.ts(2 hunks)front_end/panels/ai_chat/core/ToolNameMap.ts(1 hunks)front_end/panels/ai_chat/core/ToolNameMapping.test.ts(1 hunks)front_end/panels/ai_chat/core/__tests__/AgentNodesSanitize.test.ts(1 hunks)front_end/panels/ai_chat/mcp/MCPConfig.ts(1 hunks)front_end/panels/ai_chat/mcp/MCPRegistry.ts(1 hunks)front_end/panels/ai_chat/mcp/MCPToolAdapter.ts(1 hunks)front_end/panels/ai_chat/tools/Tools.ts(1 hunks)front_end/panels/ai_chat/ui/SettingsDialog.ts(17 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- config/gni/devtools_grd_files.gni
🧰 Additional context used
🧬 Code graph analysis (10)
front_end/panels/ai_chat/core/__tests__/AgentNodesSanitize.test.ts (3)
front_end/panels/ai_chat/core/State.ts (1)
AgentState(28-35)front_end/panels/ai_chat/LLM/LLMClient.ts (1)
LLMClient(49-385)front_end/panels/ai_chat/core/AgentNodes.ts (1)
createAgentNode(25-442)
front_end/panels/ai_chat/agent_framework/AgentRunner.ts (1)
front_end/panels/ai_chat/tracing/configure-langfuse.js (1)
config(17-26)
front_end/panels/ai_chat/core/AgentService.ts (1)
front_end/panels/ai_chat/ui/AIChatPanel.ts (3)
apiKey(1166-1197)apiKey(1239-1258)apiKey(1578-1612)
front_end/panels/ai_chat/mcp/MCPRegistry.ts (4)
front_end/panels/ai_chat/core/Logger.ts (1)
createLogger(316-318)front_end/panels/ai_chat/mcp/MCPConfig.ts (1)
getMCPConfig(27-46)front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts (1)
ToolRegistry(180-227)front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (1)
MCPToolAdapter(7-54)
front_end/panels/ai_chat/mcp/MCPConfig.ts (1)
front_end/panels/ai_chat/core/Logger.ts (1)
createLogger(316-318)
front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts (1)
front_end/panels/ai_chat/ui/AIChatPanel.ts (8)
logger(949-1042)logger(1333-1403)logger(1426-1453)logger(1560-1573)logger(1617-1620)apiKey(1166-1197)apiKey(1239-1258)apiKey(1578-1612)
front_end/panels/ai_chat/ui/SettingsDialog.ts (4)
front_end/panels/ai_chat/ui/AIChatPanel.ts (9)
localStorage(814-837)ModelOption(96-100)model(1780-1793)error(1930-1943)logger(949-1042)logger(1333-1403)logger(1426-1453)logger(1560-1573)logger(1617-1620)front_end/panels/ai_chat/ui/model_selector/ModelSelector.ts (2)
ModelOption(10-10)ModelOption(62-66)front_end/panels/ai_chat/mcp/MCPConfig.ts (3)
getMCPConfig(27-46)isMCPEnabled(87-89)setMCPConfig(48-85)front_end/panels/ai_chat/mcp/MCPRegistry.ts (1)
MCPRegistry(162-162)
front_end/panels/ai_chat/core/AgentNodes.ts (4)
front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts (2)
ToolRegistry(180-227)ConfigurableAgentTool(297-513)front_end/panels/ai_chat/models/ChatTypes.ts (2)
ChatMessage(74-75)ToolResultMessage(53-63)front_end/panels/ai_chat/LLM/LLMTypes.ts (1)
LLMMessage(176-189)front_end/panels/ai_chat/tools/Tools.ts (1)
getTools(4164-4217)
front_end/panels/ai_chat/core/ToolNameMapping.test.ts (4)
front_end/panels/ai_chat/tools/Tools.ts (1)
Tool(40-49)front_end/panels/ai_chat/LLM/LLMClient.ts (1)
LLMClient(49-385)front_end/panels/ai_chat/core/State.ts (1)
AgentState(28-35)front_end/panels/ai_chat/core/AgentNodes.ts (1)
createAgentNode(25-442)
front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (2)
front_end/panels/ai_chat/core/Logger.ts (1)
createLogger(316-318)front_end/panels/ai_chat/tools/Tools.ts (1)
Tool(40-49)
🪛 ESLint
front_end/panels/ai_chat/core/__tests__/AgentNodesSanitize.test.ts
[error] 7-7: ../../models/ChatTypes.js import should occur before import of ../AgentNodes.js
(import/order)
[error] 9-9: ../../LLM/LLMClient.js import should occur before import of ../AgentNodes.js
(import/order)
[error] 15-15: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 20-20: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 20-20: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 31-31: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 36-36: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 37-37: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 41-41: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 45-45: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 46-46: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 47-47: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 49-49: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 54-54: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 61-61: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 64-64: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 66-66: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 70-70: Use assert.isOk(e) or assert(e) instead of assert.ok(e)
(rulesdir/prefer-assert-is-ok)
[error] 71-71: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 75-75: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
front_end/panels/ai_chat/core/AgentService.ts
[error] 201-201: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 202-202: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 397-397: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
front_end/panels/ai_chat/core/ToolNameMap.ts
[error] 15-15: Expected { after 'if' condition.
(curly)
[error] 16-16: Expected { after 'if' condition.
(curly)
[error] 37-37: Expected { after 'if' condition.
(curly)
front_end/panels/ai_chat/mcp/MCPRegistry.ts
[error] 1-163: Missing license header
(rulesdir/check-license-header)
[error] 2-2: ../agent_framework/ConfigurableAgentTool.js import should occur before import of ../core/Logger.js
(import/order)
[error] 3-3: Incorrect cross-namespace import: "../core/ToolNameMap.js". Use "import * as Namespace from '../namespace/namespace.js';" instead.
(rulesdir/es-modules-import)
[error] 4-4: ../../../third_party/mcp-sdk/mcp-sdk.js type import should occur before import of ../core/Logger.js
(import/order)
[error] 4-4: Type imports must be imported in the same import statement as values, using the type keyword
(rulesdir/inline-type-imports)
[error] 5-5: Incorrect cross-namespace import: "../../../third_party/mcp-sdk/mcp-sdk.js". Use "import * as Namespace from '../namespace/namespace.js';" instead. If the third_party dependency does not expose a single entrypoint, update es_modules_import.js to make it exempt.
(rulesdir/es-modules-import)
[error] 5-5: There should be at least one empty line between import groups
(import/order)
[error] 5-5: ../../../third_party/mcp-sdk/mcp-sdk.js import should occur before import of ../core/Logger.js
(import/order)
[error] 13-13: Expected a comma.
(@stylistic/member-delimiter-style)
[error] 13-13: Expected a comma.
(@stylistic/member-delimiter-style)
[error] 13-13: Expected a comma.
(@stylistic/member-delimiter-style)
[error] 32-32: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 37-37: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 95-95: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 98-98: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
front_end/panels/ai_chat/mcp/MCPConfig.ts
[error] 1-104: Missing license header
(rulesdir/check-license-header)
[error] 92-92: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts
[error] 19-19: Expected a semicolon.
(@stylistic/member-delimiter-style)
front_end/panels/ai_chat/ui/SettingsDialog.ts
[error] 399-399: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 431-431: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 448-448: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 449-449: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 450-450: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 451-451: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 452-452: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 453-453: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 454-454: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 455-455: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 923-923: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
[error] 923-923: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 929-929: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 931-931: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 945-945: Prefer template literals over imperative DOM API calls
(rulesdir/no-imperative-dom-api)
[error] 950-950: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 952-952: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 2562-2562: Expected { after 'if' condition.
(curly)
[error] 2567-2567: Expected { after 'if' condition.
(curly)
[error] 2584-2584: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
[error] 2599-2599: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 2627-2627: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 2682-2682: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
[error] 2841-2841: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
[error] 2874-2874: Missing return type on function.
(@typescript-eslint/explicit-function-return-type)
[error] 2877-2877: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 2909-2909: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 3860-3860: Prefer template literals over imperative DOM API calls
(rulesdir/no-imperative-dom-api)
[error] 3874-3874: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
front_end/panels/ai_chat/core/AgentNodes.ts
[error] 7-7: There should be no empty line within import group
(import/order)
[error] 10-10: '../LLM/LLMTypes.js' imported multiple times.
(import/no-duplicates)
[error] 11-11: There should be at least one empty line between import groups
(import/order)
[error] 11-11: '../LLM/LLMTypes.js' imported multiple times.
(import/no-duplicates)
[error] 12-12: ./GraphHelpers.js import should occur after type import of ../tracing/TracingProvider.js
(import/order)
[error] 13-13: ./BaseOrchestratorAgent.js import should occur after type import of ../tracing/TracingProvider.js
(import/order)
[error] 14-14: ./ToolSurfaceProvider.js import should occur after type import of ../tracing/TracingProvider.js
(import/order)
[error] 15-15: ./Logger.js import should occur after type import of ../tracing/TracingProvider.js
(import/order)
[error] 16-16: ./State.js type import should occur after type import of ../tracing/TracingProvider.js
(import/order)
[error] 17-17: ./Types.js type import should occur after type import of ../tracing/TracingProvider.js
(import/order)
[error] 18-18: There should be at least one empty line between import groups
(import/order)
[error] 18-18: ./AgentErrorHandler.js import should occur after type import of ../tracing/TracingProvider.js
(import/order)
[error] 19-19: There should be at least one empty line between import groups
(import/order)
[error] 20-20: There should be at least one empty line between import groups
(import/order)
[error] 20-20: ./ToolNameMap.js import should occur after type import of ../tracing/TracingProvider.js
(import/order)
[error] 36-37: More than 1 blank line not allowed.
(@stylistic/no-multiple-empty-lines)
[error] 151-151: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 154-154: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 155-155: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 157-157: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 253-253: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
[error] 284-284: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 405-405: Forbidden non-null assertion.
(@typescript-eslint/no-non-null-assertion)
[error] 405-405: Forbidden non-null assertion.
(@typescript-eslint/no-non-null-assertion)
[error] 410-410: Forbidden non-null assertion.
(@typescript-eslint/no-non-null-assertion)
[error] 446-446: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 449-449: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 451-451: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 452-452: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 454-454: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 459-459: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 459-459: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 593-593: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 594-594: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 594-594: Trailing spaces not allowed.
(@stylistic/no-trailing-spaces)
front_end/panels/ai_chat/core/ToolNameMapping.test.ts
[error] 5-5: Incorrect same-namespace import: "./AgentNodes.js". Use "import * as Core from './core.js';" instead.
(rulesdir/es-modules-import)
[error] 5-5: 'createToolExecutorNode' is defined but never used.
(@typescript-eslint/no-unused-vars)
[error] 6-6: There should be at least one empty line between import groups
(import/order)
[error] 7-7: ../tools/Tools.js type import should occur before import of ./AgentNodes.js
(import/order)
[error] 8-8: There should be at least one empty line between import groups
(import/order)
[error] 8-8: ../models/ChatTypes.js import should occur before import of ./AgentNodes.js
(import/order)
[error] 9-9: Incorrect same-namespace import: "./ToolSurfaceProvider.js". Use "import * as Core from './core.js';" instead.
(rulesdir/es-modules-import)
[error] 9-9: There should be at least one empty line between import groups
(import/order)
[error] 11-11: ../LLM/LLMClient.js import should occur before import of ./AgentNodes.js
(import/order)
[error] 16-16: Public accessibility modifier on class property calls.
(@typescript-eslint/explicit-member-accessibility)
[error] 53-53: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 56-56: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 57-57: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 58-58: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 66-66: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 67-67: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 69-69: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 73-73: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 75-75: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 80-80: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 81-81: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
front_end/panels/ai_chat/mcp/MCPToolAdapter.ts
[error] 1-55: Missing license header
(rulesdir/check-license-header)
[error] 2-2: ../core/Logger.js import should occur before type import of ../tools/Tools.js
(import/order)
[error] 3-3: ../../../third_party/mcp-sdk/mcp-sdk.js type import should occur before type import of ../tools/Tools.js
(import/order)
[error] 10-10: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 24-24: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🪛 ast-grep (0.38.6)
front_end/panels/ai_chat/ui/SettingsDialog.ts
[warning] 2875-2875: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: mcpToolsList.innerHTML = ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html
(dom-content-modification)
🔇 Additional comments (7)
front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts (1)
199-200: Good: clearer registration log with interpolationSwitch to template literal improves diagnostics.
front_end/panels/ai_chat/mcp/MCPConfig.ts (1)
31-36: Good: token now uses sessionStorage (reduced XSS blast radius)Moving tokens off localStorage is a solid improvement.
Also applies to: 54-64
front_end/panels/ai_chat/agent_framework/AgentRunner.ts (1)
976-989: Good: pass apiKey through tool execution contextThis aligns tool calls with the per‑turn credential flow.
front_end/panels/ai_chat/core/AgentService.ts (1)
201-202: Fix ESLint: no-explicit-any.These casts trigger @typescript-eslint/no-explicit-any. The previous refactor eliminates them.
Also applies to: 397-397
front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (1)
32-36: Good: redaction only for logs; raw args sent to MCP.front_end/panels/ai_chat/core/AgentNodes.ts (2)
197-205: Good: LLM tool names are sanitized for provider constraints.Using ToolNameMap.getSanitized(tool.name) prevents OpenAI-style name rejections.
248-266: Trace uses original tool name; resolve looks correct.You resolve parsedAction.name via ToolNameMap.resolveOriginal before use — prevents executor lookup failures.
| import { createAgentNode } from '../AgentNodes.js'; | ||
| import type { AgentState } from '../State.js'; | ||
| import { ChatMessageEntity } from '../../models/ChatTypes.js'; | ||
| import { ToolSurfaceProvider } from '../ToolSurfaceProvider.js'; | ||
| import { LLMClient } from '../../LLM/LLMClient.js'; | ||
| import '../../agent_framework/ConfigurableAgentTool.js'; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Import assert and fix import order.
Add chai’s assert and satisfy import-order rules.
-import { createAgentNode } from '../AgentNodes.js';
-import type { AgentState } from '../State.js';
-import { ChatMessageEntity } from '../../models/ChatTypes.js';
-import { ToolSurfaceProvider } from '../ToolSurfaceProvider.js';
-import { LLMClient } from '../../LLM/LLMClient.js';
-import '../../agent_framework/ConfigurableAgentTool.js';
+import { ChatMessageEntity } from '../../models/ChatTypes.js';
+import { LLMClient } from '../../LLM/LLMClient.js';
+import { createAgentNode } from '../AgentNodes.js';
+import type { AgentState } from '../State.js';
+import { ToolSurfaceProvider } from '../ToolSurfaceProvider.js';
+import '../../agent_framework/ConfigurableAgentTool.js';
+import { assert } from 'chai';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { createAgentNode } from '../AgentNodes.js'; | |
| import type { AgentState } from '../State.js'; | |
| import { ChatMessageEntity } from '../../models/ChatTypes.js'; | |
| import { ToolSurfaceProvider } from '../ToolSurfaceProvider.js'; | |
| import { LLMClient } from '../../LLM/LLMClient.js'; | |
| import '../../agent_framework/ConfigurableAgentTool.js'; | |
| import { ChatMessageEntity } from '../../models/ChatTypes.js'; | |
| import { LLMClient } from '../../LLM/LLMClient.js'; | |
| import { createAgentNode } from '../AgentNodes.js'; | |
| import type { AgentState } from '../State.js'; | |
| import { ToolSurfaceProvider } from '../ToolSurfaceProvider.js'; | |
| import '../../agent_framework/ConfigurableAgentTool.js'; | |
| import { assert } from 'chai'; |
🧰 Tools
🪛 ESLint
[error] 7-7: ../../models/ChatTypes.js import should occur before import of ../AgentNodes.js
(import/order)
[error] 9-9: ../../LLM/LLMClient.js import should occur before import of ../AgentNodes.js
(import/order)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/__tests__/AgentNodesSanitize.test.ts around
lines 5 to 11, the test is missing chai's assert import and the current imports
violate the project's import-order rule; add "import { assert } from 'chai';"
and reorder the existing imports to follow the repository's import-order
convention (typically external packages first, then absolute/internal modules,
then relative paths), placing the chai import with other external imports so
linting passes.
| messages: [ { entity: ChatMessageEntity.USER, text: 'run' } as any ], | ||
| agentType: 'web_task', | ||
| context: {}, | ||
| } as any; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Use the correct field: selectedAgentType (not agentType). Remove any.
- const state: AgentState = {
- messages: [ { entity: ChatMessageEntity.USER, text: 'run' } as any ],
- agentType: 'web_task',
- context: {},
- } as any;
+ const state: AgentState = {
+ messages: [ { entity: ChatMessageEntity.USER, text: 'run' } ],
+ selectedAgentType: 'web_task',
+ context: {},
+ };📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| messages: [ { entity: ChatMessageEntity.USER, text: 'run' } as any ], | |
| agentType: 'web_task', | |
| context: {}, | |
| } as any; | |
| const state: AgentState = { | |
| messages: [ { entity: ChatMessageEntity.USER, text: 'run' } ], | |
| selectedAgentType: 'web_task', | |
| context: {}, | |
| }; |
🧰 Tools
🪛 ESLint
[error] 61-61: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 64-64: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/__tests__/AgentNodesSanitize.test.ts around
lines 61-64, the test object uses the wrong field name (agentType) and applies
unsafe any casts; change the field to selectedAgentType: 'web_task' and remove
the trailing "as any" casts, instead construct the object with the correct shape
or import/annotate the appropriate test type so the messages array and root
object are properly typed without using any.
| import type { getTools } from '../tools/Tools.js'; | ||
| import { ChatMessageEntity, type ModelChatMessage, type ToolResultMessage, type ChatMessage, type AgentSessionMessage } from '../models/ChatTypes.js'; | ||
| import { ConfigurableAgentTool } from '../agent_framework/ConfigurableAgentTool.js'; | ||
| import { ConfigurableAgentTool, ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js'; | ||
|
|
||
| import { LLMClient } from '../LLM/LLMClient.js'; | ||
| import type { LLMMessage } from '../LLM/LLMTypes.js'; | ||
| import type { LLMProvider } from '../LLM/LLMTypes.js'; | ||
| import { createSystemPromptAsync, getAgentToolsFromState } from './GraphHelpers.js'; | ||
| import { createSystemPromptAsync } from './GraphHelpers.js'; | ||
| import * as BaseOrchestratorAgent from './BaseOrchestratorAgent.js'; | ||
| import { ToolSurfaceProvider } from './ToolSurfaceProvider.js'; | ||
| import { createLogger } from './Logger.js'; | ||
| import type { AgentState } from './State.js'; | ||
| import type { Runnable } from './Types.js'; | ||
| import { AgentErrorHandler } from './AgentErrorHandler.js'; | ||
| import { createTracingProvider, withTracingContext } from '../tracing/TracingConfig.js'; | ||
| import * as ToolNameMap from './ToolNameMap.js'; | ||
| import type { TracingProvider } from '../tracing/TracingProvider.js'; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fix import ordering and duplicate imports (blocks lint/build).
- Group type-only imports first, then value imports.
- Combine duplicate imports from '../LLM/LLMTypes.js'.
-import type { getTools } from '../tools/Tools.js';
-import { ChatMessageEntity, type ModelChatMessage, type ToolResultMessage, type ChatMessage, type AgentSessionMessage } from '../models/ChatTypes.js';
-import { ConfigurableAgentTool, ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
-
-import { LLMClient } from '../LLM/LLMClient.js';
-import type { LLMMessage } from '../LLM/LLMTypes.js';
-import type { LLMProvider } from '../LLM/LLMTypes.js';
-import { createSystemPromptAsync } from './GraphHelpers.js';
-import * as BaseOrchestratorAgent from './BaseOrchestratorAgent.js';
-import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
-import { createLogger } from './Logger.js';
-import type { AgentState } from './State.js';
-import type { Runnable } from './Types.js';
-import { AgentErrorHandler } from './AgentErrorHandler.js';
-import { createTracingProvider, withTracingContext } from '../tracing/TracingConfig.js';
-import * as ToolNameMap from './ToolNameMap.js';
-import type { TracingProvider } from '../tracing/TracingProvider.js';
+import type { getTools } from '../tools/Tools.js';
+import type { LLMMessage, LLMProvider } from '../LLM/LLMTypes.js';
+import type { AgentState } from './State.js';
+import type { Runnable } from './Types.js';
+import type { TracingProvider } from '../tracing/TracingProvider.js';
+
+import { ChatMessageEntity, type ModelChatMessage, type ToolResultMessage, type ChatMessage, type AgentSessionMessage } from '../models/ChatTypes.js';
+import { ConfigurableAgentTool, ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
+import { LLMClient } from '../LLM/LLMClient.js';
+import { createSystemPromptAsync } from './GraphHelpers.js';
+import * as BaseOrchestratorAgent from './BaseOrchestratorAgent.js';
+import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
+import { createLogger } from './Logger.js';
+import { AgentErrorHandler } from './AgentErrorHandler.js';
+import { createTracingProvider, withTracingContext } from '../tracing/TracingConfig.js';
+import * as ToolNameMap from './ToolNameMap.js';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import type { getTools } from '../tools/Tools.js'; | |
| import { ChatMessageEntity, type ModelChatMessage, type ToolResultMessage, type ChatMessage, type AgentSessionMessage } from '../models/ChatTypes.js'; | |
| import { ConfigurableAgentTool } from '../agent_framework/ConfigurableAgentTool.js'; | |
| import { ConfigurableAgentTool, ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js'; | |
| import { LLMClient } from '../LLM/LLMClient.js'; | |
| import type { LLMMessage } from '../LLM/LLMTypes.js'; | |
| import type { LLMProvider } from '../LLM/LLMTypes.js'; | |
| import { createSystemPromptAsync, getAgentToolsFromState } from './GraphHelpers.js'; | |
| import { createSystemPromptAsync } from './GraphHelpers.js'; | |
| import * as BaseOrchestratorAgent from './BaseOrchestratorAgent.js'; | |
| import { ToolSurfaceProvider } from './ToolSurfaceProvider.js'; | |
| import { createLogger } from './Logger.js'; | |
| import type { AgentState } from './State.js'; | |
| import type { Runnable } from './Types.js'; | |
| import { AgentErrorHandler } from './AgentErrorHandler.js'; | |
| import { createTracingProvider, withTracingContext } from '../tracing/TracingConfig.js'; | |
| import * as ToolNameMap from './ToolNameMap.js'; | |
| import type { TracingProvider } from '../tracing/TracingProvider.js'; | |
| import type { getTools } from '../tools/Tools.js'; | |
| import type { LLMMessage, LLMProvider } from '../LLM/LLMTypes.js'; | |
| import type { AgentState } from './State.js'; | |
| import type { Runnable } from './Types.js'; | |
| import type { TracingProvider } from '../tracing/TracingProvider.js'; | |
| import { ChatMessageEntity, type ModelChatMessage, type ToolResultMessage, type ChatMessage, type AgentSessionMessage } from '../models/ChatTypes.js'; | |
| import { ConfigurableAgentTool, ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js'; | |
| import { LLMClient } from '../LLM/LLMClient.js'; | |
| import { createSystemPromptAsync } from './GraphHelpers.js'; | |
| import * as BaseOrchestratorAgent from './BaseOrchestratorAgent.js'; | |
| import { ToolSurfaceProvider } from './ToolSurfaceProvider.js'; | |
| import { createLogger } from './Logger.js'; | |
| import { AgentErrorHandler } from './AgentErrorHandler.js'; | |
| import { createTracingProvider, withTracingContext } from '../tracing/TracingConfig.js'; | |
| import * as ToolNameMap from './ToolNameMap.js'; |
🧰 Tools
🪛 ESLint
[error] 5-5: ../tools/Tools.js type import should occur after type import of ../LLM/LLMTypes.js
(import/order)
[error] 6-6: ../models/ChatTypes.js import should occur after type import of ../LLM/LLMTypes.js
(import/order)
[error] 7-7: There should be no empty line within import group
(import/order)
[error] 10-10: '../LLM/LLMTypes.js' imported multiple times.
(import/no-duplicates)
[error] 11-11: There should be at least one empty line between import groups
(import/order)
[error] 11-11: '../LLM/LLMTypes.js' imported multiple times.
(import/no-duplicates)
[error] 12-12: ./GraphHelpers.js import should occur after type import of ../tracing/TracingProvider.js
(import/order)
[error] 13-13: ./BaseOrchestratorAgent.js import should occur after type import of ../tracing/TracingProvider.js
(import/order)
[error] 14-14: ./ToolSurfaceProvider.js import should occur after type import of ../tracing/TracingProvider.js
(import/order)
[error] 15-15: ./Logger.js import should occur after type import of ../tracing/TracingProvider.js
(import/order)
[error] 16-16: ./State.js type import should occur after type import of ../tracing/TracingProvider.js
(import/order)
[error] 17-17: ./Types.js type import should occur after type import of ../tracing/TracingProvider.js
(import/order)
[error] 18-18: There should be at least one empty line between import groups
(import/order)
[error] 18-18: ./AgentErrorHandler.js import should occur after type import of ../tracing/TracingProvider.js
(import/order)
[error] 19-19: There should be at least one empty line between import groups
(import/order)
[error] 20-20: There should be at least one empty line between import groups
(import/order)
[error] 20-20: ./ToolNameMap.js import should occur after type import of ../tracing/TracingProvider.js
(import/order)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/AgentNodes.ts around lines 5–22, imports are
mis-ordered and LLMTypes is imported twice; group all type-only imports first
(using "import type" for types like getTools, ModelChatMessage,
ToolResultMessage, ChatMessage, AgentSessionMessage, LLMMessage, LLMProvider,
AgentState, Runnable, TracingProvider), then list value imports
(ChatMessageEntity, ConfigurableAgentTool, ToolRegistry, LLMClient,
createSystemPromptAsync, BaseOrchestratorAgent, ToolSurfaceProvider,
createLogger, AgentErrorHandler, createTracingProvider, withTracingContext,
ToolNameMap), and combine the two separate imports from '../LLM/LLMTypes.js'
into a single "import type" statement to remove duplication and satisfy the
linter/build.
| // Stash apiKey in state context for downstream tools that need it | ||
| if (!this.#state.context) { (this.#state as any).context = {}; } | ||
| (this.#state.context as any).apiKey = apiKey || ''; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not persist API key in AgentService state.
Persisting secrets in long-lived state risks leakage via future logs/tracing/serialization. Inject per-run only (you already do this later).
Apply this diff to drop the persistent stash:
- // Stash apiKey in state context for downstream tools that need it
- if (!this.#state.context) { (this.#state as any).context = {}; }
- (this.#state.context as any).apiKey = apiKey || '';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Stash apiKey in state context for downstream tools that need it | |
| if (!this.#state.context) { (this.#state as any).context = {}; } | |
| (this.#state.context as any).apiKey = apiKey || ''; |
🧰 Tools
🪛 ESLint
[error] 201-201: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 202-202: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/AgentService.ts around lines 200 to 203, the
code persists the API key into this.#state.context which stores secrets in
long‑lived state; remove the lines that set this.#state.context and do not stash
apiKey on the instance. Instead, pass the apiKey into the per-run call/site that
needs it (or attach it to the transient run context already used later), and
audit any downstream usages to accept an injected per-run apiKey parameter
rather than reading from this.#state.context.
| // Inject API key into context for tool execution paths (ConfigurableAgentTool) | ||
| try { (state as any).context.apiKey = this.#apiKey || ''; } catch {} | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Type-safe, per-run secret injection (no any).
Remove any and keep the key in a dedicated secrets bag on context.
Apply:
- // Inject API key into context for tool execution paths (ConfigurableAgentTool)
- try { (state as any).context.apiKey = this.#apiKey || ''; } catch {}
+ // Inject API key for tool execution; keep out of logs/serialization
+ type CtxWithSecrets = AgentState['context'] & { secrets?: { apiKey?: string } };
+ const ctx = state.context as CtxWithSecrets;
+ ctx.secrets = { ...(ctx.secrets ?? {}), apiKey: this.#apiKey ?? undefined };Follow-up: extend DevToolsContext to include secrets?: { apiKey?: string } so casts can be removed entirely.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Inject API key into context for tool execution paths (ConfigurableAgentTool) | |
| try { (state as any).context.apiKey = this.#apiKey || ''; } catch {} | |
| // Inject API key for tool execution; keep out of logs/serialization | |
| type CtxWithSecrets = AgentState['context'] & { secrets?: { apiKey?: string } }; | |
| const ctx = state.context as CtxWithSecrets; | |
| ctx.secrets = { ...(ctx.secrets ?? {}), apiKey: this.#apiKey ?? undefined }; |
🧰 Tools
🪛 ESLint
[error] 397-397: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/AgentService.ts around lines 396-398, replace
the unsafe (state as any).context.apiKey injection by placing the key into a
dedicated secrets bag on context: ensure state.context exists, ensure
state.context.secrets is an object (create if missing), and set
state.context.secrets.apiKey = this.#apiKey || ''. Remove the any cast so access
is type-checked; update call sites/types as needed and as a follow-up extend
DevToolsContext to declare secrets?: { apiKey?: string } so the cast can be
eliminated entirely.
| import { createAgentNode, createToolExecutorNode } from './AgentNodes.js'; | ||
| import type { AgentState } from './State.js'; | ||
| import type { Tool } from '../tools/Tools.js'; | ||
| import { ChatMessageEntity } from '../models/ChatTypes.js'; | ||
| import { ToolSurfaceProvider } from './ToolSurfaceProvider.js'; | ||
| import '../agent_framework/ConfigurableAgentTool.js'; | ||
| import { LLMClient } from '../LLM/LLMClient.js'; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Trim imports, add assert, and fix order.
-import { createAgentNode, createToolExecutorNode } from './AgentNodes.js';
+import { createAgentNode } from './AgentNodes.js';
import type { AgentState } from './State.js';
import type { Tool } from '../tools/Tools.js';
import { ChatMessageEntity } from '../models/ChatTypes.js';
import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
import '../agent_framework/ConfigurableAgentTool.js';
import { LLMClient } from '../LLM/LLMClient.js';
+import { assert } from 'chai';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { createAgentNode, createToolExecutorNode } from './AgentNodes.js'; | |
| import type { AgentState } from './State.js'; | |
| import type { Tool } from '../tools/Tools.js'; | |
| import { ChatMessageEntity } from '../models/ChatTypes.js'; | |
| import { ToolSurfaceProvider } from './ToolSurfaceProvider.js'; | |
| import '../agent_framework/ConfigurableAgentTool.js'; | |
| import { LLMClient } from '../LLM/LLMClient.js'; | |
| import { createAgentNode } from './AgentNodes.js'; | |
| import type { AgentState } from './State.js'; | |
| import type { Tool } from '../tools/Tools.js'; | |
| import { ChatMessageEntity } from '../models/ChatTypes.js'; | |
| import { ToolSurfaceProvider } from './ToolSurfaceProvider.js'; | |
| import '../agent_framework/ConfigurableAgentTool.js'; | |
| import { LLMClient } from '../LLM/LLMClient.js'; | |
| import { assert } from 'chai'; |
🧰 Tools
🪛 ESLint
[error] 5-5: Incorrect same-namespace import: "./AgentNodes.js". Use "import * as Core from './core.js';" instead.
(rulesdir/es-modules-import)
[error] 5-5: 'createToolExecutorNode' is defined but never used.
(@typescript-eslint/no-unused-vars)
[error] 6-6: There should be at least one empty line between import groups
(import/order)
[error] 7-7: ../tools/Tools.js type import should occur before import of ./AgentNodes.js
(import/order)
[error] 8-8: There should be at least one empty line between import groups
(import/order)
[error] 8-8: ../models/ChatTypes.js import should occur before import of ./AgentNodes.js
(import/order)
[error] 9-9: Incorrect same-namespace import: "./ToolSurfaceProvider.js". Use "import * as Core from './core.js';" instead.
(rulesdir/es-modules-import)
[error] 9-9: There should be at least one empty line between import groups
(import/order)
[error] 11-11: ../LLM/LLMClient.js import should occur before import of ./AgentNodes.js
(import/order)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/ToolNameMapping.test.ts around lines 5 to 12,
trim unused imports, add a missing assert import, and reorder imports for
clarity: remove imports that aren’t referenced in the test (e.g.,
ToolSurfaceProvider, ConfigurableAgentTool, LLMClient if they are unused), keep
only the required imports (createAgentNode, createToolExecutorNode, AgentState
type, Tool type, ChatMessageEntity), add an import for assert (import assert
from 'assert';) and reorder so external modules/types are grouped before local
module imports for readability.
| const initial: AgentState = { | ||
| messages: [ { entity: ChatMessageEntity.USER, text: 'go' } as any ], | ||
| agentType: 'deep-research' as any, | ||
| context: {}, | ||
| } as any; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Use selectedAgentType and tighten types; remove any.
- const initial: AgentState = {
- messages: [ { entity: ChatMessageEntity.USER, text: 'go' } as any ],
- agentType: 'deep-research' as any,
- context: {},
- } as any;
+ const initial: AgentState = {
+ messages: [ { entity: ChatMessageEntity.USER, text: 'go' } ],
+ selectedAgentType: 'deep-research',
+ context: {},
+ };📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const initial: AgentState = { | |
| messages: [ { entity: ChatMessageEntity.USER, text: 'go' } as any ], | |
| agentType: 'deep-research' as any, | |
| context: {}, | |
| } as any; | |
| const initial: AgentState = { | |
| messages: [ { entity: ChatMessageEntity.USER, text: 'go' } ], | |
| selectedAgentType: 'deep-research', | |
| context: {}, | |
| }; |
🧰 Tools
🪛 ESLint
[error] 66-66: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 67-67: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 69-69: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/ToolNameMapping.test.ts around lines 65 to 69,
the test uses broad `any` casts and the legacy `agentType` field; replace
`agentType` with the correct `selectedAgentType` property and tighten types by
using the actual AgentState and ChatMessage types (or imports) instead of `as
any`. Ensure the messages array uses the proper ChatMessage/ChatMessageEntity
type for the user message and that the AgentState object is typed explicitly
(remove all `as any`), updating imports if necessary so the test compiles with
strict typing.
| export class MCPToolAdapter implements Tool<Record<string, unknown>, unknown> { | ||
| name: string; | ||
| description: string; | ||
| schema: any; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Remove any from schema; match Tool interface shape.
- schema: any;
+ schema: { type: string; properties: Record<string, unknown>; required?: string[] };
@@
- const schema = def.inputSchema as any;
- if (schema && typeof schema === 'object') {
- this.schema = schema;
+ const schema = def.inputSchema as Partial<{ type: string; properties: Record<string, unknown>; required?: string[] }> | undefined;
+ if (schema && typeof schema === 'object' && schema.type === 'object' && schema.properties) {
+ this.schema = { type: 'object', properties: schema.properties, required: schema.required };
} else {
this.schema = { type: 'object', properties: {} };
}Also applies to: 24-29
🧰 Tools
🪛 ESLint
[error] 10-10: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/mcp/MCPToolAdapter.ts around lines 10 and also
24-29, the schema property is typed as any and other related types do not match
the Tool interface; replace any with the exact shape from the Tool interface (or
import the Tool type and use it) so the class fields and method signatures
conform to Tool. Update the file to import or declare the Tool type, type schema
and any related properties/method params to Tool (or Tool['schema'] /
appropriate subtype) and adjust usages to satisfy the interface (add missing
fields/return types) so the adapter implements the Tool interface without any
anys.
| // ---- MCP Integration Section ---- | ||
| const mcpSection = document.createElement('div'); | ||
| mcpSection.className = 'settings-section mcp-section'; | ||
| // Hide MCP UI: auto-connect is always on; settings are not user-configurable | ||
| mcpSection.style.display = 'none'; | ||
| contentDiv.appendChild(mcpSection); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MCP section is intentionally hidden but toggleAdvancedSections reveals it.
Guard against re-showing hidden MCP UI.
- mcpSection.style.display = 'none';
+ mcpSection.style.display = 'none';
+ mcpSection.dataset.hiddenByDefault = 'true';- const mcpSectionEl = contentDiv.querySelector('.mcp-section') as HTMLElement | null;
- if (mcpSectionEl) { mcpSectionEl.style.display = display; }
+ const mcpSectionEl = contentDiv.querySelector('.mcp-section') as HTMLElement | null;
+ if (mcpSectionEl && mcpSectionEl.dataset.hiddenByDefault !== 'true') {
+ mcpSectionEl.style.display = display;
+ }Also applies to: 3274-3276
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/ui/SettingsDialog.ts around lines 2496-2501 and
3274-3276 the MCP section is hidden via style.display = 'none' but
toggleAdvancedSections can re-show it; mark the MCP container as permanently
locked (e.g., set a data attribute or CSS class like data-locked="true" or
.locked-mcp) and update the toggleAdvancedSections logic to skip/showing any
section with that marker so the MCP UI remains hidden even when advanced
sections are toggled.
| const formatTimestamp = (date: Date | undefined): string => { | ||
| if (!date) return ''; | ||
| return date.toLocaleString(); | ||
| }; | ||
|
|
||
| const formatMCPError = (error: string, errorType?: string): {message: string, hint?: string} => { | ||
| if (!errorType) return {message: error}; | ||
| switch (errorType) { | ||
| case 'connection': | ||
| return {message: `Connection failed: ${error}`, hint: 'Check if the MCP server is running and the endpoint URL is correct.'}; | ||
| case 'authentication': | ||
| return {message: `Authentication failed: ${error}`, hint: 'Verify your auth token is correct and has not expired.'}; | ||
| case 'configuration': | ||
| return {message: `Configuration error: ${error}`, hint: 'Check your endpoint URL format (should be ws:// or wss://).'}; | ||
| case 'network': | ||
| return {message: `Network error: ${error}`, hint: 'Check your internet connection and firewall settings.'}; | ||
| case 'server_error': | ||
| return {message: `Server error: ${error}`, hint: 'The MCP server encountered an internal error. Contact the server administrator.'}; | ||
| default: | ||
| return {message: error}; | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add braces and explicit return types for helpers (curly/explicit-function-return-type).
Also return structured error text without HTML.
- const formatTimestamp = (date: Date | undefined): string => {
- if (!date) return '';
- return date.toLocaleString();
- };
+ const formatTimestamp = (date: Date | undefined): string => {
+ if (!date) { return ''; }
+ return date.toLocaleString();
+ };
- const formatMCPError = (error: string, errorType?: string): {message: string, hint?: string} => {
- if (!errorType) return {message: error};
+ const formatMCPError = (error: string, errorType?: string): {message: string, hint?: string} => {
+ if (!errorType) { return {message: error}; }
switch (errorType) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const formatTimestamp = (date: Date | undefined): string => { | |
| if (!date) return ''; | |
| return date.toLocaleString(); | |
| }; | |
| const formatMCPError = (error: string, errorType?: string): {message: string, hint?: string} => { | |
| if (!errorType) return {message: error}; | |
| switch (errorType) { | |
| case 'connection': | |
| return {message: `Connection failed: ${error}`, hint: 'Check if the MCP server is running and the endpoint URL is correct.'}; | |
| case 'authentication': | |
| return {message: `Authentication failed: ${error}`, hint: 'Verify your auth token is correct and has not expired.'}; | |
| case 'configuration': | |
| return {message: `Configuration error: ${error}`, hint: 'Check your endpoint URL format (should be ws:// or wss://).'}; | |
| case 'network': | |
| return {message: `Network error: ${error}`, hint: 'Check your internet connection and firewall settings.'}; | |
| case 'server_error': | |
| return {message: `Server error: ${error}`, hint: 'The MCP server encountered an internal error. Contact the server administrator.'}; | |
| default: | |
| return {message: error}; | |
| } | |
| }; | |
| const formatTimestamp = (date: Date | undefined): string => { | |
| if (!date) { return ''; } | |
| return date.toLocaleString(); | |
| }; | |
| const formatMCPError = (error: string, errorType?: string): {message: string, hint?: string} => { | |
| if (!errorType) { return {message: error}; } | |
| switch (errorType) { | |
| case 'connection': | |
| return {message: `Connection failed: ${error}`, hint: 'Check if the MCP server is running and the endpoint URL is correct.'}; | |
| case 'authentication': | |
| return {message: `Authentication failed: ${error}`, hint: 'Verify your auth token is correct and has not expired.'}; | |
| case 'configuration': | |
| return {message: `Configuration error: ${error}`, hint: 'Check your endpoint URL format (should be ws:// or wss://).'}; | |
| case 'network': | |
| return {message: `Network error: ${error}`, hint: 'Check your internet connection and firewall settings.'}; | |
| case 'server_error': | |
| return {message: `Server error: ${error}`, hint: 'The MCP server encountered an internal error. Contact the server administrator.'}; | |
| default: | |
| return {message: error}; | |
| } | |
| }; |
🧰 Tools
🪛 ESLint
[error] 2562-2562: Expected { after 'if' condition.
(curly)
[error] 2567-2567: Expected { after 'if' condition.
(curly)
Summary by CodeRabbit
New Features
UI
Tests
Chores