Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/components/Messages/ToolMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { GenericToolCall } from "../tools/GenericToolCall";
import { BashToolCall } from "../tools/BashToolCall";
import { FileEditToolCall } from "../tools/FileEditToolCall";
import { FileReadToolCall } from "../tools/FileReadToolCall";
import { FileSearchToolCall } from "../tools/FileSearchToolCall";
import { ProposePlanToolCall } from "../tools/ProposePlanToolCall";
import { TodoToolCall } from "../tools/TodoToolCall";
import { StatusSetToolCall } from "../tools/StatusSetToolCall";
Expand All @@ -13,6 +14,8 @@ import type {
BashToolResult,
FileReadToolArgs,
FileReadToolResult,
FileSearchToolArgs,
FileSearchToolResult,
FileEditInsertToolArgs,
FileEditInsertToolResult,
FileEditReplaceStringToolArgs,
Expand Down Expand Up @@ -45,6 +48,11 @@ function isFileReadTool(toolName: string, args: unknown): args is FileReadToolAr
return TOOL_DEFINITIONS.file_read.schema.safeParse(args).success;
}

function isFileSearchTool(toolName: string, args: unknown): args is FileSearchToolArgs {
if (toolName !== "file_search") return false;
return TOOL_DEFINITIONS.file_search.schema.safeParse(args).success;
}

function isFileEditReplaceStringTool(
toolName: string,
args: unknown
Expand Down Expand Up @@ -108,6 +116,18 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({ message, className, wo
);
}

if (isFileSearchTool(message.toolName, message.args)) {
return (
<div className={className}>
<FileSearchToolCall
args={message.args}
result={message.result as FileSearchToolResult | undefined}
status={message.status}
/>
</div>
);
}

if (isFileEditReplaceStringTool(message.toolName, message.args)) {
return (
<div className={className}>
Expand Down
153 changes: 153 additions & 0 deletions src/components/tools/FileSearchToolCall.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import React from "react";
import type { FileSearchToolArgs, FileSearchToolResult } from "@/types/tools";
import {
ToolContainer,
ToolHeader,
ExpandIcon,
StatusIndicator,
ToolDetails,
DetailSection,
DetailLabel,
DetailContent,
LoadingDots,
} from "./shared/ToolPrimitives";
import { useToolExpansion, getStatusDisplay, type ToolStatus } from "./shared/toolUtils";
import { TooltipWrapper, Tooltip } from "../Tooltip";

interface FileSearchToolCallProps {
args: FileSearchToolArgs;
result?: FileSearchToolResult;
status?: ToolStatus;
}

export const FileSearchToolCall: React.FC<FileSearchToolCallProps> = ({
args,
result,
status = "pending",
}) => {
const { expanded, toggleExpanded } = useToolExpansion();

const matchCount = result?.success ? result.matches.length : 0;
const totalMatches = result?.success ? result.total_matches : 0;
const hasMore = matchCount < totalMatches;

return (
<ToolContainer expanded={expanded}>
<ToolHeader onClick={toggleExpanded}>
<ExpandIcon expanded={expanded}>▶</ExpandIcon>
<TooltipWrapper inline>
<span>🔍</span>
<Tooltip>file_search</Tooltip>
</TooltipWrapper>
<span className="text-text font-monospace max-w-96 truncate">{args.file_path}</span>
{result && result.success && (
<span className="text-secondary ml-2 text-[10px] whitespace-nowrap">
{matchCount} {matchCount === 1 ? "match" : "matches"}
{hasMore && ` (showing first ${matchCount})`}
</span>
)}
<StatusIndicator status={status}>{getStatusDisplay(status)}</StatusIndicator>
</ToolHeader>

{expanded && (
<ToolDetails>
<DetailSection>
<div className="bg-code-bg flex flex-wrap gap-4 rounded px-2 py-1.5 text-[11px] leading-[1.4]">
<div className="flex gap-1.5">
<DetailLabel>Pattern:</DetailLabel>
<DetailContent>
<code className="text-code-string">&quot;{args.pattern}&quot;</code>
</DetailContent>
</div>
{args.context_lines !== undefined && (
<div className="flex gap-1.5">
<DetailLabel>Context:</DetailLabel>
<DetailContent>{args.context_lines} lines</DetailContent>
</div>
)}
</div>
</DetailSection>

{!result && (
<DetailSection>
<div className="text-secondary flex items-center gap-2">
<span>Searching</span>
<LoadingDots />
</div>
</DetailSection>
)}

{result && !result.success && (
<DetailSection>
<div className="border-error-border bg-error-bg rounded border px-3 py-2 text-sm">
<span className="text-error font-medium">Error: </span>
<span className="text-text">{result.error}</span>
</div>
</DetailSection>
)}

{result && result.success && (
<>
{matchCount === 0 ? (
<DetailSection>
<div className="text-secondary text-sm">No matches found</div>
</DetailSection>
) : (
result.matches.map((match, idx) => (
<DetailSection key={`match-${match.line_number}-${idx}`}>
<div className="bg-code-bg rounded">
{/* Context before */}
{match.context_before.length > 0 && (
<div className="text-code-comment border-code-border border-b px-3 py-1 font-mono text-xs">
{match.context_before.map((line, i) => (
<div key={`before-${i}`} className="opacity-60">
<span className="text-code-line-number mr-3 inline-block w-8 text-right">
{match.line_number - match.context_before.length + i}
</span>
{line}
</div>
))}
</div>
)}

{/* Matching line - highlighted */}
<div className="bg-code-highlight px-3 py-1 font-mono text-xs">
<span className="text-code-line-number mr-3 inline-block w-8 text-right font-bold">
{match.line_number}
</span>
<span className="text-code-keyword font-medium">{match.line_content}</span>
</div>

{/* Context after */}
{match.context_after.length > 0 && (
<div className="text-code-comment border-code-border border-t px-3 py-1 font-mono text-xs">
{match.context_after.map((line, i) => (
<div key={`after-${i}`} className="opacity-60">
<span className="text-code-line-number mr-3 inline-block w-8 text-right">
{match.line_number + i + 1}
</span>
{line}
</div>
))}
</div>
)}
</div>
</DetailSection>
))
)}

{hasMore && (
<DetailSection>
<div className="text-secondary text-xs">
Showing first {matchCount} of {totalMatches} matches. Increase max_results to
see more.
</div>
</DetailSection>
)}
</>
)}
</ToolDetails>
)}
</ToolContainer>
);
};
Loading
Loading