// ***************************************************************************** // Copyright (C) 2025 EclipseSource GmbH. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { ChatResponsePartRenderer } from '@theia/ai-chat-ui/lib/browser/chat-response-part-renderer'; import { ResponseNode } from '@theia/ai-chat-ui/lib/browser/chat-tree-view'; import { ChatResponseContent, ToolCallChatResponseContent } from '@theia/ai-chat/lib/common'; import { codicon, LabelProvider } from '@theia/core/lib/browser'; import { URI } from '@theia/core/lib/common/uri'; import { inject, injectable } from '@theia/core/shared/inversify'; import * as React from '@theia/core/shared/react'; import { ReactNode } from '@theia/core/shared/react'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { ClaudeCodeToolCallChatResponseContent } from '../claude-code-tool-call-content'; import { CollapsibleToolRenderer } from './collapsible-tool-renderer'; import { nls } from '@theia/core'; interface GrepToolInput { pattern: string; path?: string; output_mode?: keyof typeof GREP_OUTPUT_MODES; glob?: string; type?: string; '-i'?: boolean; '-n'?: boolean; '-A'?: number; '-B'?: number; '-C'?: number; multiline?: boolean; head_limit?: number; } const GREP_OUTPUT_MODES = { 'content': nls.localize('theia/ai/claude-code/grepOutputModes/content', 'content'), 'files_with_matches': nls.localize('theia/ai/claude-code/grepOutputModes/filesWithMatches', 'files with matches'), 'count': nls.localize('theia/ai/claude-code/grepOutputModes/count', 'count') }; @injectable() export class GrepToolRenderer implements ChatResponsePartRenderer { @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; canHandle(response: ChatResponseContent): number { if (ClaudeCodeToolCallChatResponseContent.is(response) && response.name === 'Grep') { return 15; // Higher than default ToolCallPartRenderer (10) } return -1; } render(response: ToolCallChatResponseContent, parentNode: ResponseNode): ReactNode { try { const input = JSON.parse(response.arguments || '{}') as GrepToolInput; return ; } catch (error) { console.warn('Failed to parse Grep tool input:', error); return
{nls.localize('theia/ai/claude-code/failedToParseGrepToolData', 'Failed to parse Grep tool data')}
; } } } const GrepToolComponent: React.FC<{ input: GrepToolInput; workspaceService: WorkspaceService; labelProvider: LabelProvider; }> = ({ input, workspaceService, labelProvider }) => { const getSearchScope = (): string => { if (input.path) { return input.path.split('/').pop() || input.path; } return nls.localize('theia/ai/claude-code/project', 'project'); }; const getWorkspaceRelativePath = async (filePath: string): Promise => { try { const absoluteUri = new URI(filePath); const workspaceRelativePath = await workspaceService.getWorkspaceRelativePath(absoluteUri); return workspaceRelativePath || ''; } catch { return ''; } }; const [relativePath, setRelativePath] = React.useState(''); React.useEffect(() => { if (input.path) { getWorkspaceRelativePath(input.path).then(setRelativePath); } }, [input.path]); const getOptionsInfo = (): { label: string; count: number } => { const options = []; if (input['-i']) { options.push(nls.localize('theia/ai/claude-code/grepOptions/caseInsensitive', 'case-insensitive')); } if (input['-n']) { options.push(nls.localize('theia/ai/claude-code/grepOptions/lineNumbers', 'line numbers')); } if (input['-A']) { options.push(nls.localize('theia/ai/claude-code/grepOptions/linesAfter', '+{0} after'), input['-A']); } if (input['-B']) { options.push(nls.localize('theia/ai/claude-code/grepOptions/linesBefore', '+{0} before', input['-B'])); } if (input['-C']) { options.push(nls.localize('theia/ai/claude-code/grepOptions/linesContext', '±{0} context', input['-C'])); } if (input.multiline) { options.push(nls.localize('theia/ai/claude-code/grepOptions/multiLine', 'multiline')); } if (input.glob) { options.push(nls.localize('theia/ai/claude-code/grepOptions/glob', 'glob: {0}', input.glob)); } if (input.type) { options.push(nls.localize('theia/ai/claude-code/grepOptions/type', 'type: {0}', input.type)); } if (input.head_limit) { options.push(nls.localize('theia/ai/claude-code/grepOptions/headLimit', 'limit: {0}', input.head_limit)); } return { label: options.length > 0 ? options.join(', ') : '', count: options.length }; }; const optionsInfo = getOptionsInfo(); const compactHeader = ( <>
{nls.localize('theia/ai/claude-code/searching', 'Searching')} "{input.pattern}" {nls.localizeByDefault('in {0}', getSearchScope())} {relativePath && {relativePath}}
{input.output_mode && input.output_mode !== 'files_with_matches' && ( {GREP_OUTPUT_MODES[input.output_mode]} )} {optionsInfo.count > 0 && ( {optionsInfo.count > 1 ? nls.localize('theia/ai/claude-code/optionsCount', '{0} options', optionsInfo.count) : nls.localize('theia/ai/claude-code/oneOption', '1 option')} )}
); const expandedContent = (
{nls.localizeByDefault('Pattern')} "{input.pattern}"
{nls.localize('theia/ai/claude-code/searchPath', 'Search Path')} {input.path || nls.localize('theia/ai/claude-code/projectRoot', 'project root')}
{input.output_mode && (
{nls.localizeByDefault('Mode')} {GREP_OUTPUT_MODES[input.output_mode]}
)} {input.glob && (
{nls.localize('theia/ai/claude-code/fileFilter', 'File Filter')} {input.glob}
)} {input.type && (
{nls.localize('theia/ai/claude-code/fileType', 'File Type')} {input.type}
)} {optionsInfo.label && (
{nls.localizeByDefault('Options')} {optionsInfo.label}
)}
); return ( ); };