Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
45 changes: 44 additions & 1 deletion bun.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@
"crc-32": "^1.2.2",
"diff": "^8.0.2",
"disposablestack": "^1.1.7",
"ignore": "^7.0.5",
"jsonc-parser": "^3.3.1",
"lru-cache": "^11.2.2",
"markdown-it": "^14.1.0",
"mermaid": "^11.12.0",
"minimatch": "^10.0.3",
"minimist": "^1.2.8",
"posthog-js": "^1.276.0",
"react": "^18.2.0",
Expand Down Expand Up @@ -83,6 +85,7 @@
"@types/jest": "^30.0.0",
"@types/katex": "^0.16.7",
"@types/markdown-it": "^14.1.2",
"@types/minimatch": "^6.0.0",
"@types/minimist": "^1.2.5",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
Expand Down
21 changes: 21 additions & 0 deletions src/components/Messages/ToolMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import { GenericToolCall } from "../tools/GenericToolCall";
import { BashToolCall } from "../tools/BashToolCall";
import { FileEditToolCall } from "../tools/FileEditToolCall";
import { FileReadToolCall } from "../tools/FileReadToolCall";
import { FileListToolCall } from "../tools/FileListToolCall";

import { ProposePlanToolCall } from "../tools/ProposePlanToolCall";
import { TodoToolCall } from "../tools/TodoToolCall";
import type {
BashToolArgs,
BashToolResult,
FileReadToolArgs,
FileReadToolResult,
FileListToolArgs,
FileListToolResult,
FileEditInsertToolArgs,
FileEditInsertToolResult,
FileEditReplaceStringToolArgs,
Expand Down Expand Up @@ -42,6 +46,11 @@ function isFileReadTool(toolName: string, args: unknown): args is FileReadToolAr
return TOOL_DEFINITIONS.file_read.schema.safeParse(args).success;
}

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

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

if (isFileListTool(message.toolName, message.args)) {
return (
<div className={className}>
<FileListToolCall
args={message.args}
result={message.result as FileListToolResult | undefined}
status={message.status}
/>
</div>
);
}

if (isFileEditReplaceStringTool(message.toolName, message.args)) {
return (
<div className={className}>
Expand Down
7 changes: 2 additions & 5 deletions src/components/tools/BashToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
LoadingDots,
} from "./shared/ToolPrimitives";
import { useToolExpansion, getStatusDisplay, type ToolStatus } from "./shared/toolUtils";
import { TooltipWrapper, Tooltip } from "../Tooltip";
import { ToolIcon } from "./shared/ToolIcon";

// Bash-specific styled components

Expand Down Expand Up @@ -123,10 +123,7 @@ export const BashToolCall: React.FC<BashToolCallProps> = ({
<ToolContainer expanded={expanded}>
<ToolHeader onClick={toggleExpanded}>
<ExpandIcon expanded={expanded}>▶</ExpandIcon>
<TooltipWrapper inline>
<span>🔧</span>
<Tooltip>bash</Tooltip>
</TooltipWrapper>
<ToolIcon emoji="🔧" toolName="bash" />
<ScriptPreview>{args.script}</ScriptPreview>
<TimeoutInfo status={isPending ? status : undefined}>
timeout: {args.timeout_secs ?? BASH_DEFAULT_TIMEOUT_SECS}s
Expand Down
7 changes: 2 additions & 5 deletions src/components/tools/FileEditToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
HeaderButton,
} from "./shared/ToolPrimitives";
import { useToolExpansion, getStatusDisplay, type ToolStatus } from "./shared/toolUtils";
import { TooltipWrapper, Tooltip } from "../Tooltip";
import { ToolIcon } from "./shared/ToolIcon";

// File edit specific styled components

Expand Down Expand Up @@ -290,10 +290,7 @@ export const FileEditToolCall: React.FC<FileEditToolCallProps> = ({
<StyledToolHeader>
<LeftContent onClick={toggleExpanded}>
<ExpandIcon expanded={expanded}>▶</ExpandIcon>
<TooltipWrapper inline>
<span>✏️</span>
<Tooltip>{toolName}</Tooltip>
</TooltipWrapper>
<ToolIcon emoji="✏️" toolName={toolName} />
<FilePath>{filePath}</FilePath>
</LeftContent>
{!(result && result.success && result.diff) && (
Expand Down
163 changes: 163 additions & 0 deletions src/components/tools/FileListToolCall.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import React from "react";
import styled from "@emotion/styled";
import type { FileListToolArgs, FileListToolResult } from "@/types/tools";
import {
ToolContainer,
ToolHeader,
ExpandIcon,
StatusIndicator,
ToolDetails,
DetailSection,
DetailLabel,
LoadingDots,
} from "./shared/ToolPrimitives";
import { useToolExpansion, getStatusDisplay, ToolStatus } from "./shared/toolUtils";
import { ToolIcon } from "./shared/ToolIcon";

// FileList-specific styled components

const PathText = styled.span`
color: var(--color-text);
font-family: var(--font-monospace);
font-weight: 500;
`;

const ParamsText = styled.span`
color: var(--color-text-secondary);
font-size: 10px;
margin-left: 8px;
`;

const CountBadge = styled.span`
color: var(--color-text-secondary);
font-size: 10px;
margin-left: 8px;
`;

const ErrorMessage = styled.div`
color: #f44336;
font-size: 11px;
padding: 6px 8px;
background: rgba(244, 67, 54, 0.1);
border-radius: 3px;
border-left: 2px solid #f44336;
line-height: 1.5;
white-space: pre-wrap;
`;

const ErrorHint = styled.div`
color: var(--color-text-secondary);
font-size: 10px;
margin-top: 6px;
font-style: italic;
`;

const OutputBlock = styled.pre`
margin: 0;
padding: 8px 12px;
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
font-size: 11px;
line-height: 1.6;
white-space: pre;
overflow-x: auto;
font-family: var(--font-monospace);
color: var(--color-text);
`;

const EmptyMessage = styled.div`
color: var(--color-text-secondary);
font-style: italic;
text-align: center;
padding: 16px;
`;

interface FileListToolCallProps {
args: FileListToolArgs;
result?: FileListToolResult;
status: ToolStatus;
}

export const FileListToolCall: React.FC<FileListToolCallProps> = ({ args, result, status }) => {
const { expanded, toggleExpanded } = useToolExpansion(false);
const isError = status === "failed" || (result && !result.success);
const isComplete = status === "completed";
const isPending = status === "pending" || status === "executing";

// Build parameter summary
const params: string[] = [];
if (args.max_depth !== undefined && args.max_depth !== 1) {
params.push(`depth: ${args.max_depth}`);
}
if (args.pattern) {
params.push(`pattern: ${args.pattern}`);
}
if (args.gitignore === false) {
params.push("gitignore: off");
}
if (args.max_entries) {
params.push(`max: ${args.max_entries}`);
}

const paramStr = params.length > 0 ? `(${params.join(", ")})` : "";

// Ensure path ends with / to indicate it's a directory
const displayPath = args.path.endsWith("/") ? args.path : `${args.path}/`;

// Convert our status to shared ToolStatus type
const toolStatus = isError ? "failed" : isPending ? "executing" : "completed";

return (
<ToolContainer expanded={expanded}>
<ToolHeader onClick={toggleExpanded}>
<ExpandIcon expanded={expanded}>▶</ExpandIcon>
<ToolIcon emoji="📖" toolName="file_list" />
<PathText>{displayPath}</PathText>
{paramStr && <ParamsText>{paramStr}</ParamsText>}
{isComplete && result && result.success && (
<CountBadge>{result.total_count} entries</CountBadge>
)}
<StatusIndicator status={toolStatus}>{getStatusDisplay(toolStatus)}</StatusIndicator>
</ToolHeader>

{expanded && (
<ToolDetails>
{/* Pending state */}
{isPending && (
<DetailSection>
Listing directory
<LoadingDots />
</DetailSection>
)}

{/* Error state */}
{isError && result && !result.success && (
<DetailSection>
<DetailLabel>Error</DetailLabel>
<ErrorMessage>
{result.error}
{result.total_found !== undefined && (
<ErrorHint>
Found {result.total_found}+ entries (limit: {result.limit_requested})
</ErrorHint>
)}
</ErrorMessage>
</DetailSection>
)}

{/* Success state */}
{isComplete && result && result.success && (
<DetailSection>
<DetailLabel>Contents ({result.total_count} entries)</DetailLabel>
{result.output === "(empty directory)" ? (
<EmptyMessage>Empty directory</EmptyMessage>
) : (
<OutputBlock>{result.output}</OutputBlock>
)}
</DetailSection>
)}
</ToolDetails>
)}
</ToolContainer>
);
};
7 changes: 2 additions & 5 deletions src/components/tools/FileReadToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
LoadingDots,
} from "./shared/ToolPrimitives";
import { useToolExpansion, getStatusDisplay, type ToolStatus } from "./shared/toolUtils";
import { TooltipWrapper, Tooltip } from "../Tooltip";
import { ToolIcon } from "./shared/ToolIcon";

// FileRead-specific styled components

Expand Down Expand Up @@ -165,10 +165,7 @@ export const FileReadToolCall: React.FC<FileReadToolCallProps> = ({
<ToolContainer expanded={expanded}>
<ToolHeader onClick={toggleExpanded}>
<ExpandIcon expanded={expanded}>▶</ExpandIcon>
<TooltipWrapper inline>
<span>📖</span>
<Tooltip>file_read</Tooltip>
</TooltipWrapper>
<ToolIcon emoji="📖" toolName="file_read" />
<FilePathText>{filePath}</FilePathText>
{result && result.success && parsedContent && (
<MetadataText>
Expand Down
3 changes: 2 additions & 1 deletion src/components/tools/ProposePlanToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ToolDetails,
} from "./shared/ToolPrimitives";
import { useToolExpansion, getStatusDisplay, type ToolStatus } from "./shared/toolUtils";
import { ToolIcon } from "./shared/ToolIcon";
import { MarkdownRenderer } from "../Messages/MarkdownRenderer";
import { formatKeybind, KEYBINDS } from "@/utils/ui/keybinds";
import { useStartHere } from "@/hooks/useStartHere";
Expand Down Expand Up @@ -285,7 +286,7 @@ export const ProposePlanToolCall: React.FC<ProposePlanToolCallProps> = ({
<ToolContainer expanded={expanded}>
<ToolHeader onClick={toggleExpanded}>
<ExpandIcon expanded={expanded}>▶</ExpandIcon>
<ToolName>propose_plan</ToolName>
<ToolIcon emoji="📋" toolName="propose_plan" />
<StatusIndicator status={status}>{statusDisplay}</StatusIndicator>
</ToolHeader>

Expand Down
7 changes: 2 additions & 5 deletions src/components/tools/TodoToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
ToolDetails,
} from "./shared/ToolPrimitives";
import { useToolExpansion, getStatusDisplay, type ToolStatus } from "./shared/toolUtils";
import { TooltipWrapper, Tooltip } from "../Tooltip";
import { ToolIcon } from "./shared/ToolIcon";
import { TodoList } from "../TodoList";

interface TodoToolCallProps {
Expand All @@ -29,10 +29,7 @@ export const TodoToolCall: React.FC<TodoToolCallProps> = ({
<ToolContainer expanded={expanded}>
<ToolHeader onClick={toggleExpanded}>
<ExpandIcon expanded={expanded}>▶</ExpandIcon>
<TooltipWrapper inline>
<span>📋</span>
<Tooltip>todo_write</Tooltip>
</TooltipWrapper>
<ToolIcon emoji="📋" toolName="todo_write" />
<StatusIndicator status={status}>{statusDisplay}</StatusIndicator>
</ToolHeader>

Expand Down
21 changes: 21 additions & 0 deletions src/components/tools/shared/ToolIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from "react";
import { TooltipWrapper, Tooltip } from "../../Tooltip";

interface ToolIconProps {
emoji: string;
toolName: string;
}

/**
* Shared component for displaying tool emoji with tooltip showing the full tool name.
* Used consistently across all tool components in ToolHeader.
*/
export const ToolIcon: React.FC<ToolIconProps> = ({ emoji, toolName }) => {
return (
<TooltipWrapper inline>
<span>{emoji}</span>
<Tooltip>{toolName}</Tooltip>
</TooltipWrapper>
);
};

5 changes: 5 additions & 0 deletions src/constants/toolLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@ export const BASH_MAX_LINE_BYTES = 1024; // 1KB per line
export const BASH_MAX_TOTAL_BYTES = 16 * 1024; // 16KB total output to show agent
export const BASH_MAX_FILE_BYTES = 100 * 1024; // 100KB max to save to temp file

export const FILE_LIST_DEFAULT_DEPTH = 1; // Non-recursive by default
export const FILE_LIST_MAX_DEPTH = 10; // Allow deep traversal when needed
export const FILE_LIST_DEFAULT_MAX_ENTRIES = 64; // Reasonable default
export const FILE_LIST_HARD_MAX_ENTRIES = 128; // Absolute limit (prevent context overload)

export const MAX_TODOS = 7; // Maximum number of TODO items in a list
13 changes: 13 additions & 0 deletions src/services/tools/fileCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,16 @@ export function validatePathInCwd(filePath: string, cwd: string): { error: strin

return null;
}

/**
* Format a file size in bytes to a human-readable string.
* Uses KB for sizes >= 1KB, MB for sizes >= 1MB, otherwise bytes.
*
* @param bytes - File size in bytes
* @returns Formatted size string (e.g., "1.5KB", "2.3MB", "512B")
*/
export function formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
}
Loading
Loading