mirror of
http://10.0.2.1:3031/sauer/claude-code.git
synced 2026-06-30 12:56:58 +10:00
262 lines
33 KiB
TypeScript
262 lines
33 KiB
TypeScript
|
|
import * as React from 'react';
|
||
|
|
import { MessageResponse } from '../../components/MessageResponse.js';
|
||
|
|
import { supportsHyperlinks } from '../../ink/supports-hyperlinks.js';
|
||
|
|
import { Link, Text } from '../../ink.js';
|
||
|
|
import { renderToolResultMessage as renderDefaultMCPToolResultMessage } from '../../tools/MCPTool/UI.js';
|
||
|
|
import type { MCPToolResult } from '../../utils/mcpValidation.js';
|
||
|
|
import { truncateToWidth } from '../format.js';
|
||
|
|
import { trackClaudeInChromeTabId } from './common.js';
|
||
|
|
export type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* All tool names from BROWSER_TOOLS in @ant/claude-for-chrome-mcp.
|
||
|
|
* Keep in sync with the package's BROWSER_TOOLS array.
|
||
|
|
*/
|
||
|
|
export type ChromeToolName = 'javascript_tool' | 'read_page' | 'find' | 'form_input' | 'computer' | 'navigate' | 'resize_window' | 'gif_creator' | 'upload_image' | 'get_page_text' | 'tabs_context_mcp' | 'tabs_create_mcp' | 'update_plan' | 'read_console_messages' | 'read_network_requests' | 'shortcuts_list' | 'shortcuts_execute';
|
||
|
|
const CHROME_EXTENSION_FOCUS_TAB_URL_BASE = 'https://clau.de/chrome/tab/';
|
||
|
|
function renderChromeToolUseMessage(input: Record<string, unknown>, toolName: ChromeToolName, verbose: boolean): React.ReactNode {
|
||
|
|
const tabId = input.tabId;
|
||
|
|
if (typeof tabId === 'number') {
|
||
|
|
trackClaudeInChromeTabId(tabId);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Build secondary info based on tool type and input
|
||
|
|
const secondaryInfo: string[] = [];
|
||
|
|
switch (toolName) {
|
||
|
|
case 'navigate':
|
||
|
|
if (typeof input.url === 'string') {
|
||
|
|
try {
|
||
|
|
const url = new URL(input.url);
|
||
|
|
secondaryInfo.push(url.hostname);
|
||
|
|
} catch {
|
||
|
|
secondaryInfo.push(truncateToWidth(input.url, 30));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case 'find':
|
||
|
|
if (typeof input.query === 'string') {
|
||
|
|
secondaryInfo.push(`pattern: ${truncateToWidth(input.query, 30)}`);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case 'computer':
|
||
|
|
if (typeof input.action === 'string') {
|
||
|
|
const action = input.action;
|
||
|
|
if (action === 'left_click' || action === 'right_click' || action === 'double_click' || action === 'middle_click') {
|
||
|
|
if (typeof input.ref === 'string') {
|
||
|
|
secondaryInfo.push(`${action} on ${input.ref}`);
|
||
|
|
} else if (Array.isArray(input.coordinate)) {
|
||
|
|
secondaryInfo.push(`${action} at (${input.coordinate.join(', ')})`);
|
||
|
|
} else {
|
||
|
|
secondaryInfo.push(action);
|
||
|
|
}
|
||
|
|
} else if (action === 'type' && typeof input.text === 'string') {
|
||
|
|
secondaryInfo.push(`type "${truncateToWidth(input.text, 15)}"`);
|
||
|
|
} else if (action === 'key' && typeof input.text === 'string') {
|
||
|
|
secondaryInfo.push(`key ${input.text}`);
|
||
|
|
} else if (action === 'scroll' && typeof input.scroll_direction === 'string') {
|
||
|
|
secondaryInfo.push(`scroll ${input.scroll_direction}`);
|
||
|
|
} else if (action === 'wait' && typeof input.duration === 'number') {
|
||
|
|
secondaryInfo.push(`wait ${input.duration}s`);
|
||
|
|
} else if (action === 'left_click_drag') {
|
||
|
|
secondaryInfo.push('drag');
|
||
|
|
} else {
|
||
|
|
secondaryInfo.push(action);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case 'gif_creator':
|
||
|
|
if (typeof input.action === 'string') {
|
||
|
|
secondaryInfo.push(`${input.action}`);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case 'resize_window':
|
||
|
|
if (typeof input.width === 'number' && typeof input.height === 'number') {
|
||
|
|
secondaryInfo.push(`${input.width}x${input.height}`);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case 'read_console_messages':
|
||
|
|
if (typeof input.pattern === 'string') {
|
||
|
|
secondaryInfo.push(`pattern: ${truncateToWidth(input.pattern, 20)}`);
|
||
|
|
}
|
||
|
|
if (input.onlyErrors === true) {
|
||
|
|
secondaryInfo.push('errors only');
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case 'read_network_requests':
|
||
|
|
if (typeof input.urlPattern === 'string') {
|
||
|
|
secondaryInfo.push(`pattern: ${truncateToWidth(input.urlPattern, 20)}`);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case 'shortcuts_execute':
|
||
|
|
if (typeof input.shortcutId === 'string') {
|
||
|
|
secondaryInfo.push(`shortcut_id: ${input.shortcutId}`);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case 'javascript_tool':
|
||
|
|
// In verbose mode, show the full code
|
||
|
|
if (verbose && typeof input.text === 'string') {
|
||
|
|
return input.text;
|
||
|
|
}
|
||
|
|
// In non-verbose mode, return empty string to preserve View Tab layout
|
||
|
|
return '';
|
||
|
|
case 'tabs_create_mcp':
|
||
|
|
case 'tabs_context_mcp':
|
||
|
|
case 'form_input':
|
||
|
|
case 'shortcuts_list':
|
||
|
|
case 'read_page':
|
||
|
|
case 'upload_image':
|
||
|
|
case 'get_page_text':
|
||
|
|
case 'update_plan':
|
||
|
|
// These tools don't have meaningful secondary info to show inline.
|
||
|
|
// Return empty string (not null) to ensure tool header still renders.
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
return secondaryInfo.join(', ') || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Renders a clickable "View Tab" link for Claude in Chrome MCP tools.
|
||
|
|
* Returns null if:
|
||
|
|
* - The tool is not a Claude in Chrome MCP tool
|
||
|
|
* - The input doesn't have a valid tabId
|
||
|
|
* - Hyperlinks are not supported
|
||
|
|
*/
|
||
|
|
function renderChromeViewTabLink(input: unknown): React.ReactNode {
|
||
|
|
if (!supportsHyperlinks()) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
if (typeof input !== 'object' || input === null || !('tabId' in input)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
const tabId = typeof input.tabId === 'number' ? input.tabId : typeof input.tabId === 'string' ? parseInt(input.tabId, 10) : NaN;
|
||
|
|
if (isNaN(tabId)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
const linkUrl = `${CHROME_EXTENSION_FOCUS_TAB_URL_BASE}${tabId}`;
|
||
|
|
return <Text>
|
||
|
|
{' '}
|
||
|
|
<Link url={linkUrl}>
|
||
|
|
<Text color="subtle">[View Tab]</Text>
|
||
|
|
</Link>
|
||
|
|
</Text>;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Custom tool result message rendering for claude-in-chrome tools.
|
||
|
|
* Shows a brief summary for successful results. Errors are handled by
|
||
|
|
* the default renderToolUseErrorMessage when is_error is set.
|
||
|
|
*/
|
||
|
|
export function renderChromeToolResultMessage(output: MCPToolResult, toolName: ChromeToolName, verbose: boolean): React.ReactNode {
|
||
|
|
if (verbose) {
|
||
|
|
return renderDefaultMCPToolResultMessage(output, [], {
|
||
|
|
verbose
|
||
|
|
});
|
||
|
|
}
|
||
|
|
let summary: string | null = null;
|
||
|
|
switch (toolName) {
|
||
|
|
case 'navigate':
|
||
|
|
summary = 'Navigation completed';
|
||
|
|
break;
|
||
|
|
case 'tabs_create_mcp':
|
||
|
|
summary = 'Tab created';
|
||
|
|
break;
|
||
|
|
case 'tabs_context_mcp':
|
||
|
|
summary = 'Tabs read';
|
||
|
|
break;
|
||
|
|
case 'form_input':
|
||
|
|
summary = 'Input completed';
|
||
|
|
break;
|
||
|
|
case 'computer':
|
||
|
|
summary = 'Action completed';
|
||
|
|
break;
|
||
|
|
case 'resize_window':
|
||
|
|
summary = 'Window resized';
|
||
|
|
break;
|
||
|
|
case 'find':
|
||
|
|
summary = 'Search completed';
|
||
|
|
break;
|
||
|
|
case 'gif_creator':
|
||
|
|
summary = 'GIF action completed';
|
||
|
|
break;
|
||
|
|
case 'read_console_messages':
|
||
|
|
summary = 'Console messages retrieved';
|
||
|
|
break;
|
||
|
|
case 'read_network_requests':
|
||
|
|
summary = 'Network requests retrieved';
|
||
|
|
break;
|
||
|
|
case 'shortcuts_list':
|
||
|
|
summary = 'Shortcuts retrieved';
|
||
|
|
break;
|
||
|
|
case 'shortcuts_execute':
|
||
|
|
summary = 'Shortcut executed';
|
||
|
|
break;
|
||
|
|
case 'javascript_tool':
|
||
|
|
summary = 'Script executed';
|
||
|
|
break;
|
||
|
|
case 'read_page':
|
||
|
|
summary = 'Page read';
|
||
|
|
break;
|
||
|
|
case 'upload_image':
|
||
|
|
summary = 'Image uploaded';
|
||
|
|
break;
|
||
|
|
case 'get_page_text':
|
||
|
|
summary = 'Page text retrieved';
|
||
|
|
break;
|
||
|
|
case 'update_plan':
|
||
|
|
summary = 'Plan updated';
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
if (summary) {
|
||
|
|
return <MessageResponse height={1}>
|
||
|
|
<Text dimColor>{summary}</Text>
|
||
|
|
</MessageResponse>;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns tool method overrides for Claude in Chrome MCP tools. Use this to customize
|
||
|
|
* rendering for chrome tools in a single spread operation.
|
||
|
|
*/
|
||
|
|
export function getClaudeInChromeMCPToolOverrides(toolName: string): {
|
||
|
|
userFacingName: (input?: Record<string, unknown>) => string;
|
||
|
|
renderToolUseMessage: (input: Record<string, unknown>, options: {
|
||
|
|
verbose: boolean;
|
||
|
|
}) => React.ReactNode;
|
||
|
|
renderToolUseTag: (input: Partial<Record<string, unknown>>) => React.ReactNode;
|
||
|
|
renderToolResultMessage: (output: string | MCPToolResult, progressMessagesForMessage: unknown[], options: {
|
||
|
|
verbose: boolean;
|
||
|
|
}) => React.ReactNode;
|
||
|
|
} {
|
||
|
|
return {
|
||
|
|
userFacingName(_input?: Record<string, unknown>) {
|
||
|
|
// Trim the _mcp postfix that show up in some of the tool names
|
||
|
|
const displayName = toolName.replace(/_mcp$/, '');
|
||
|
|
return `Claude in Chrome[${displayName}]`;
|
||
|
|
},
|
||
|
|
renderToolUseMessage(input: Record<string, unknown>, {
|
||
|
|
verbose
|
||
|
|
}: {
|
||
|
|
verbose: boolean;
|
||
|
|
}): React.ReactNode {
|
||
|
|
return renderChromeToolUseMessage(input, toolName as ChromeToolName, verbose);
|
||
|
|
},
|
||
|
|
renderToolUseTag(input: Partial<Record<string, unknown>>): React.ReactNode {
|
||
|
|
return renderChromeViewTabLink(input);
|
||
|
|
},
|
||
|
|
renderToolResultMessage(output: string | MCPToolResult, _progressMessagesForMessage: unknown[], {
|
||
|
|
verbose
|
||
|
|
}: {
|
||
|
|
verbose: boolean;
|
||
|
|
}): React.ReactNode {
|
||
|
|
if (!isMCPToolResult(output)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return renderChromeToolResultMessage(output, toolName as ChromeToolName, verbose);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
function isMCPToolResult(output: string | MCPToolResult): output is MCPToolResult {
|
||
|
|
return typeof output === 'object' && output !== null;
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIk1lc3NhZ2VSZXNwb25zZSIsInN1cHBvcnRzSHlwZXJsaW5rcyIsIkxpbmsiLCJUZXh0IiwicmVuZGVyVG9vbFJlc3VsdE1lc3NhZ2UiLCJyZW5kZXJEZWZhdWx0TUNQVG9vbFJlc3VsdE1lc3NhZ2UiLCJNQ1BUb29sUmVzdWx0IiwidHJ1bmNhdGVUb1dpZHRoIiwidHJhY2tDbGF1ZGVJbkNocm9tZVRhYklkIiwiVG9vbCIsIkNocm9tZVRvb2xOYW1lIiwiQ0hST01FX0VYVEVOU0lPTl9GT0NVU19UQUJfVVJMX0JBU0UiLCJyZW5kZXJDaHJvbWVUb29sVXNlTWVzc2FnZSIsImlucHV0IiwiUmVjb3JkIiwidG9vbE5hbWUiLCJ2ZXJib3NlIiwiUmVhY3ROb2RlIiwidGFiSWQiLCJzZWNvbmRhcnlJbmZvIiwidXJsIiwiVVJMIiwicHVzaCIsImhvc3RuYW1lIiwicXVlcnkiLCJhY3Rpb24iLCJyZWYiLCJBcnJheSIsImlzQXJyYXkiLCJjb29yZGluYXRlIiwiam9pbiIsInRleHQiLCJzY3JvbGxfZGlyZWN0aW9uIiwiZHVyYXRpb24iLCJ3aWR0aCIsImhlaWdodCIsInBhdHRlcm4iLCJvbmx5RXJyb3JzIiwidXJsUGF0dGVybiIsInNob3J0Y3V0SWQiLCJyZW5kZXJDaHJvbWVWaWV3VGFiTGluayIsInBhcnNlSW50IiwiTmFOIiwiaXNOYU4iLCJsaW5rVXJsIiwicmVuZGVyQ2hyb21lVG9vbFJlc3VsdE1lc3NhZ2UiLCJvdXRwdXQiLCJzdW1tYXJ5IiwiZ2V0Q2xhdWRlSW5DaHJvbWVNQ1BUb29sT3ZlcnJpZGVzIiwidXNlckZhY2luZ05hbWUiLCJyZW5kZXJUb29sVXNlTWVzc2FnZSIsIm9wdGlvbnMiLCJyZW5kZXJUb29sVXNlVGFnIiwiUGFydGlhbCIsInByb2dyZXNzTWVzc2FnZXNGb3JNZXNzYWdlIiwiX2lucHV0IiwiZGlzcGxheU5hbWUiLCJyZXBsYWNlIiwiX3Byb2dyZXNzTWVzc2FnZXNGb3JNZXNzYWdlIiwiaXNNQ1BUb29sUmVzdWx0Il0sInNvdXJjZXMiOlsidG9vbFJlbmRlcmluZy50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBNZXNzYWdlUmVzcG9uc2UgfSBmcm9tICcuLi8uLi9jb21wb25lbnRzL01lc3NhZ2VSZXNwb25zZS5qcydcbmltcG9ydCB7IHN1cHBvcnRzSHlwZXJsaW5rcyB9IGZyb20gJy4uLy4uL2luay9zdXBwb3J0cy1oeXBlcmxpbmtzLmpzJ1xuaW1wb3J0IHsgTGluaywgVGV4dCB9IGZyb20gJy4uLy4uL2luay5qcydcbmltcG9ydCB7IHJlbmRlclRvb2xSZXN1bHRNZXNzYWdlIGFzIHJlbmRlckRlZmF1bHRNQ1BUb29sUmVzdWx0TWVzc2FnZSB9IGZyb20gJy4uLy4uL3Rvb2xzL01DUFRvb2wvVUkuanMnXG5pbXBvcnQgdHlwZSB7IE1DUFRvb2xSZXN1bHQgfSBmcm9tICcuLi8uLi91dGlscy9tY3BWYWxpZGF0aW9uLmpzJ1xuaW1wb3J0IHsgdHJ1bmNhdGVUb1dpZHRoIH0gZnJvbSAnLi4vZm9ybWF0LmpzJ1xuaW1wb3J0IHsgdHJhY2tDbGF1ZGVJbkNocm9tZVRhYklkIH0gZnJvbSAnLi9jb21tb24uanMnXG5cbmV4cG9ydCB0eXBlIHsgVG9vbCB9IGZyb20gJ0Btb2RlbGNvbnRleHRwcm90b2NvbC9zZGsvdHlwZXMuanMnXG5cbi8qKlxuICogQWxsIHRvb2wgbmFtZXMgZnJvbSBCUk9XU0VSX1RPT0xTIGluIEBhbnQvY2xhdWRlLWZvci1jaHJvbWUtbWNwLlxuICogS2VlcCBpbiBzeW5jIHdpdGggdGhlIHBhY2thZ2UncyBCUk9XU0VSX1RPT0xTIGFycmF5LlxuICovXG5leHBvcnQgdHlwZSBDaHJvbWVUb29sTmFtZSA9XG4gIHwgJ2phdmFzY3JpcHRfdG9vbCdcbiAgfCAncmVhZF9wYWdlJ1xuICB8ICdmaW5kJ1xuICB8ICdmb3JtX2lucHV0J1xuICB8ICdjb21wdXRlcidcbiAgfCAnbmF2aWdhdGUnXG4gIHwgJ3Jlc2l6ZV93aW5kb3cnXG4gIHwgJ2dpZl9jcmVhdG9yJ1xuICB8ICd1cGxvYWRfaW1hZ2UnXG4gIHwgJ2dldF9wYWdlX3RleHQnXG4gIHwgJ3RhYnNfY29udGV4dF9tY3AnXG4gIHwgJ3RhYnNfY3JlYXRlX21jcCdcbiAgfCAndXBkYXRlX3BsYW4nXG4gIHwgJ3JlYWRfY29uc29sZV9tZXNzYWdlcydcbiAgfCAncmVhZF9uZXR3b3JrX3JlcXVlc3RzJ1xuICB8ICdzaG9ydGN1dHNfbGlzdCdcbiAgfCAnc2hvcnRjdXRzX2V4ZWN1dGUnXG5cbmNvbnN0IENIUk9NRV9FWFRFTlNJT05fRk9DVVNfVEFCX1VSTF9CQVNFID0gJ2h0dHBzOi8vY2xhdS5kZS9jaHJvbWUvdGFiLydcblxuZnVuY3Rpb24gcmVuZGVyQ2hyb21lVG9vbFVzZU1lc3NhZ2UoXG4gIGlucHV0OiBSZWNvcmQ8c3RyaW5nLCB1bmtub3duPixcbiAgdG9vbE5hbWU6IENocm9tZVRvb2xOYW1lLFxuICB2ZXJib3NlOiBib29sZWFuLFxuKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3QgdGFiSWQgPSBpbnB1dC50YWJJZFxuICBpZiAodHlwZW9mIHRhYklkID09PSAnbnVtYmVyJykge1xuICAgIHRyYWNrQ2xhdWRlSW5DaHJvbWVUYWJJZCh0YWJJZClcbiAgfVxuXG4gIC8vIEJ1aWxkIHNlY29uZGFyeSBpbmZvIGJhc2VkIG9uIHRvb2wgdHlwZSBhbmQgaW5wdXRcbiAgY29uc3Qgc2Vjb25kYXJ5SW5mbzogc3RyaW5nW10gPSBbXVxuXG4gIHN3aXRjaCAodG9vbE5hbWUpIHtcbiAgICBjYXNlICduYXZpZ2F0ZSc6XG4gICAgICBpZiAodHlwZW9mIGlucHV0LnVybCA9PT0gJ3N0cmluZycpIHtcbiAgICAgICAgdHJ5IHtcbiAgICAgICAgICBjb25zdCB1cmwgPSBuZXcgVVJMKGlucHV0LnVybClcbiAgICAgICAgICBzZWNvbmRhcnlJbmZvLnB1c2godXJsLmhvc3RuYW1lKVxuICAgICAgICB9IGNhdGNoIHtcbiAgICAgICAgICBzZWNvbmRhcnlJbmZvLnB1c2godHJ1bmNhdGVUb1dpZHRoKGlucHV0LnVybCwgMzApKVxuICAgICAgICB9XG4gICAgICB9XG4gICAgICBicmVha1xuXG4gICAgY2FzZSAnZmluZCc6XG4gICAgICBpZiAodHlwZW9mIGlucHV0LnF1ZXJ5ID09PSAnc3RyaW5nJykge1xuICAgICAgICBzZWNvbmRhcnlJbmZvLnB1c2goYHBhdHRlcm46ICR7dHJ1bmNhdGVUb1dpZHRoKGlucHV0LnF1ZXJ5LCAzMCl9YClcbiAgICAgIH1cbiAgICAgIGJyZWFrXG5cbiAgICBjYXNlICdjb21wdXRlcic6XG4gICAgICBpZiAodHlwZW9mIGlucHV0LmFjdGlvbiA9PT0gJ3N0cmluZycpIHtcbiAgICAgICAgY29uc3QgYWN0aW9
|