diff --git a/.changeset/delete-file-cli-display.md b/.changeset/delete-file-cli-display.md new file mode 100644 index 00000000000..aa92dc91754 --- /dev/null +++ b/.changeset/delete-file-cli-display.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Add proper display for deleteFile tool in CLI instead of showing "Unknown tool: deleteFile" diff --git a/cli/src/ui/messages/extension/__tests__/ExtensionMessageRow.test.tsx b/cli/src/ui/messages/extension/__tests__/ExtensionMessageRow.test.tsx index 19b2cce937b..00e4feaa882 100644 --- a/cli/src/ui/messages/extension/__tests__/ExtensionMessageRow.test.tsx +++ b/cli/src/ui/messages/extension/__tests__/ExtensionMessageRow.test.tsx @@ -414,6 +414,52 @@ describe("ExtensionMessageRow", () => { expect(lastFrame()).toBeDefined() expect(lastFrame()).not.toContain("Unknown message type") }) + + it("should handle 'ask' deleteFile tool messages for single file", () => { + const message: ExtensionChatMessage = { + ts: Date.now(), + type: "ask", + ask: "tool", + text: JSON.stringify({ + tool: "deleteFile", + path: "src/test.ts", + }), + } + + const { lastFrame } = render() + + expect(lastFrame()).toBeDefined() + expect(lastFrame()).not.toContain("Unknown tool") + expect(lastFrame()).toContain("Delete") + expect(lastFrame()).toContain("src/test.ts") + }) + + it("should handle 'ask' deleteFile tool messages for directory with stats", () => { + const message: ExtensionChatMessage = { + ts: Date.now(), + type: "ask", + ask: "tool", + text: JSON.stringify({ + tool: "deleteFile", + path: "src/components", + stats: { + files: 5, + directories: 2, + size: 1024, + isComplete: true, + }, + }), + } + + const { lastFrame } = render() + + expect(lastFrame()).toBeDefined() + expect(lastFrame()).not.toContain("Unknown tool") + expect(lastFrame()).toContain("Delete") + expect(lastFrame()).toContain("src/components") + expect(lastFrame()).toContain("5 files") + expect(lastFrame()).toContain("2 dirs") + }) }) describe("MCP Server Messages", () => { diff --git a/cli/src/ui/messages/extension/__tests__/utils.test.ts b/cli/src/ui/messages/extension/__tests__/utils.test.ts index 46534c3cd6d..ab7b4b6a852 100644 --- a/cli/src/ui/messages/extension/__tests__/utils.test.ts +++ b/cli/src/ui/messages/extension/__tests__/utils.test.ts @@ -7,6 +7,7 @@ import { isMcpServerData, parseMcpServerData, formatUnknownMessageContent, + getToolIcon, } from "../utils.js" import type { ExtensionChatMessage } from "../../../../types/messages.js" @@ -492,3 +493,29 @@ describe("formatUnknownMessageContent", () => { }) }) }) + +describe("getToolIcon", () => { + it("should return 🗑️ for deleteFile tool", () => { + expect(getToolIcon("deleteFile")).toBe("🗑️") + }) + + it("should return ± for editedExistingFile tool", () => { + expect(getToolIcon("editedExistingFile")).toBe("±") + }) + + it("should return ± for appliedDiff tool", () => { + expect(getToolIcon("appliedDiff")).toBe("±") + }) + + it("should return 📄 for newFileCreated tool", () => { + expect(getToolIcon("newFileCreated")).toBe("📄") + }) + + it("should return 📝 for readFile tool", () => { + expect(getToolIcon("readFile")).toBe("📝") + }) + + it("should return ⚙ for unknown tools", () => { + expect(getToolIcon("unknownTool")).toBe("⚙") + }) +}) diff --git a/cli/src/ui/messages/extension/tools/ToolDeleteFileMessage.tsx b/cli/src/ui/messages/extension/tools/ToolDeleteFileMessage.tsx new file mode 100644 index 00000000000..17c916d4b81 --- /dev/null +++ b/cli/src/ui/messages/extension/tools/ToolDeleteFileMessage.tsx @@ -0,0 +1,67 @@ +import React, { useMemo } from "react" +import { Box, Text } from "ink" +import type { ToolMessageProps } from "../types.js" +import { formatFilePath, getToolIcon } from "../utils.js" +import { useTheme } from "../../../../state/hooks/useTheme.js" + +/** + * Display file or directory deletion + * Uses compact format: 🗑️ Delete(filename) or 🗑️ Delete(dirname) ⎿ X files, Y dirs + */ +export const ToolDeleteFileMessage: React.FC = ({ toolData }) => { + const theme = useTheme() + + // Format stats summary for directory deletion + const statsSummary = useMemo(() => { + if (!toolData.stats) { + return null + } + + const { files, directories, isComplete } = toolData.stats + + if (!isComplete) { + return "scanning..." + } + + const parts: string[] = [] + if (files > 0 || directories === 0) { + parts.push(`${files} ${files === 1 ? "file" : "files"}`) + } + if (directories > 0) { + parts.push(`${directories} ${directories === 1 ? "dir" : "dirs"}`) + } + + return parts.length > 0 ? `⎿ ${parts.join(", ")}` : null + }, [toolData.stats]) + + const icon = getToolIcon("deleteFile") + + return ( + + {/* Compact header: 🗑️ Delete(filename) ⎿ X files, Y dirs */} + + + {icon} Delete( + + + {formatFilePath(toolData.path || "")} + + + ) + + {toolData.isOutsideWorkspace && ( + + {" "} + ⚠ + + )} + {statsSummary && ( + + {" "} + {statsSummary} + + )} + + + ) +} diff --git a/cli/src/ui/messages/extension/tools/ToolRouter.tsx b/cli/src/ui/messages/extension/tools/ToolRouter.tsx index abeb8ae7f99..20c69d5522f 100644 --- a/cli/src/ui/messages/extension/tools/ToolRouter.tsx +++ b/cli/src/ui/messages/extension/tools/ToolRouter.tsx @@ -20,6 +20,7 @@ import { ToolFinishTaskMessage, ToolFetchInstructionsMessage, ToolRunSlashCommandMessage, + ToolDeleteFileMessage, } from "./index.js" /** @@ -82,6 +83,9 @@ export const ToolRouter: React.FC = ({ message, toolData }) => case "runSlashCommand": return + case "deleteFile": + return + default: return ( diff --git a/cli/src/ui/messages/extension/tools/__tests__/ToolDeleteFileMessage.test.tsx b/cli/src/ui/messages/extension/tools/__tests__/ToolDeleteFileMessage.test.tsx new file mode 100644 index 00000000000..d6eb2b54f22 --- /dev/null +++ b/cli/src/ui/messages/extension/tools/__tests__/ToolDeleteFileMessage.test.tsx @@ -0,0 +1,276 @@ +/** + * Tests for ToolDeleteFileMessage component + * + * This test suite verifies that the ToolDeleteFileMessage component correctly displays + * file and directory deletion information. + */ + +import { describe, it, expect, vi } from "vitest" +import { render } from "ink-testing-library" +import { ToolDeleteFileMessage } from "../ToolDeleteFileMessage.js" +import type { ExtensionChatMessage } from "../../../../../types/messages.js" +import type { ToolData } from "../../types.js" + +// Mock the logs service +vi.mock("../../../../../services/logs.js", () => ({ + logs: { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }, +})) + +describe("ToolDeleteFileMessage", () => { + const createMessage = (): ExtensionChatMessage => ({ + ts: Date.now(), + type: "ask", + ask: "tool", + text: "", + }) + + describe("Single file deletion", () => { + it("should display file path for single file deletion", () => { + const toolData: ToolData = { + tool: "deleteFile", + path: "src/test.ts", + } + + const { lastFrame } = render() + + expect(lastFrame()).toBeDefined() + expect(lastFrame()).toContain("src/test.ts") + }) + + it("should display delete icon (🗑️) for file deletion", () => { + const toolData: ToolData = { + tool: "deleteFile", + path: "test.ts", + } + + const { lastFrame } = render() + + expect(lastFrame()).toContain("🗑️") + }) + + it("should display Delete action label", () => { + const toolData: ToolData = { + tool: "deleteFile", + path: "test.ts", + } + + const { lastFrame } = render() + + expect(lastFrame()).toContain("Delete") + }) + + it("should format path by removing leading ./", () => { + const toolData: ToolData = { + tool: "deleteFile", + path: "./src/test.ts", + } + + const { lastFrame } = render() + + expect(lastFrame()).toContain("src/test.ts") + expect(lastFrame()).not.toContain("./src/test.ts") + }) + }) + + describe("Directory deletion", () => { + it("should display directory path for directory deletion", () => { + const toolData: ToolData = { + tool: "deleteFile", + path: "src/components", + stats: { + files: 5, + directories: 2, + size: 1024, + isComplete: true, + }, + } + + const { lastFrame } = render() + + expect(lastFrame()).toBeDefined() + expect(lastFrame()).toContain("src/components") + }) + + it("should display file count for directory deletion", () => { + const toolData: ToolData = { + tool: "deleteFile", + path: "src/components", + stats: { + files: 5, + directories: 2, + size: 1024, + isComplete: true, + }, + } + + const { lastFrame } = render() + + expect(lastFrame()).toContain("5 files") + }) + + it("should display directory count for directory deletion", () => { + const toolData: ToolData = { + tool: "deleteFile", + path: "src/components", + stats: { + files: 5, + directories: 2, + size: 1024, + isComplete: true, + }, + } + + const { lastFrame } = render() + + expect(lastFrame()).toContain("2 dirs") + }) + + it("should display singular 'file' when count is 1", () => { + const toolData: ToolData = { + tool: "deleteFile", + path: "src/components", + stats: { + files: 1, + directories: 0, + size: 512, + isComplete: true, + }, + } + + const { lastFrame } = render() + + expect(lastFrame()).toContain("1 file") + expect(lastFrame()).not.toContain("1 files") + }) + + it("should display singular 'dir' when count is 1", () => { + const toolData: ToolData = { + tool: "deleteFile", + path: "src/components", + stats: { + files: 0, + directories: 1, + size: 0, + isComplete: true, + }, + } + + const { lastFrame } = render() + + expect(lastFrame()).toContain("1 dir") + expect(lastFrame()).not.toContain("1 dirs") + }) + + it("should show scanning indicator when isComplete is false", () => { + const toolData: ToolData = { + tool: "deleteFile", + path: "src/components", + stats: { + files: 0, + directories: 0, + size: 0, + isComplete: false, + }, + } + + const { lastFrame } = render() + + expect(lastFrame()).toContain("scanning") + }) + + it("should not show scanning indicator when isComplete is true", () => { + const toolData: ToolData = { + tool: "deleteFile", + path: "src/components", + stats: { + files: 5, + directories: 2, + size: 1024, + isComplete: true, + }, + } + + const { lastFrame } = render() + + expect(lastFrame()).not.toContain("scanning") + }) + }) + + describe("Warning indicators", () => { + it("should display outside workspace warning when isOutsideWorkspace is true", () => { + const toolData: ToolData = { + tool: "deleteFile", + path: "/etc/config", + isOutsideWorkspace: true, + } + + const { lastFrame } = render() + + expect(lastFrame()).toContain("⚠") + }) + + it("should not display outside workspace warning when isOutsideWorkspace is false", () => { + const toolData: ToolData = { + tool: "deleteFile", + path: "src/test.ts", + isOutsideWorkspace: false, + } + + const { lastFrame } = render() + + // Should not contain warning symbol (but may contain other content) + const frame = lastFrame() || "" + // Count occurrences of ⚠ - should be 0 + const warningCount = (frame.match(/⚠/g) || []).length + expect(warningCount).toBe(0) + }) + }) + + describe("Edge cases", () => { + it("should handle empty path gracefully", () => { + const toolData: ToolData = { + tool: "deleteFile", + path: "", + } + + const { lastFrame } = render() + + expect(lastFrame()).toBeDefined() + expect(lastFrame()).toContain("Delete") + }) + + it("should handle undefined path gracefully", () => { + const toolData: ToolData = { + tool: "deleteFile", + } + + const { lastFrame } = render() + + expect(lastFrame()).toBeDefined() + expect(lastFrame()).toContain("Delete") + }) + + it("should handle zero stats for directory", () => { + const toolData: ToolData = { + tool: "deleteFile", + path: "empty-dir", + stats: { + files: 0, + directories: 0, + size: 0, + isComplete: true, + }, + } + + const { lastFrame } = render() + + expect(lastFrame()).toBeDefined() + expect(lastFrame()).toContain("empty-dir") + }) + }) +}) diff --git a/cli/src/ui/messages/extension/tools/index.ts b/cli/src/ui/messages/extension/tools/index.ts index 900ba2800e1..e6278be55e9 100644 --- a/cli/src/ui/messages/extension/tools/index.ts +++ b/cli/src/ui/messages/extension/tools/index.ts @@ -15,3 +15,4 @@ export { ToolNewTaskMessage } from "./ToolNewTaskMessage.js" export { ToolFinishTaskMessage } from "./ToolFinishTaskMessage.js" export { ToolFetchInstructionsMessage } from "./ToolFetchInstructionsMessage.js" export { ToolRunSlashCommandMessage } from "./ToolRunSlashCommandMessage.js" +export { ToolDeleteFileMessage } from "./ToolDeleteFileMessage.js" diff --git a/cli/src/ui/messages/extension/types.ts b/cli/src/ui/messages/extension/types.ts index ac7b0d33502..11d5867a46f 100644 --- a/cli/src/ui/messages/extension/types.ts +++ b/cli/src/ui/messages/extension/types.ts @@ -23,6 +23,16 @@ export interface BatchDiffItem { diff?: string } +/** + * Directory deletion stats structure + */ +export interface DeleteFileStats { + files: number + directories: number + size: number + isComplete: boolean +} + /** * Parsed tool data structure */ @@ -49,6 +59,7 @@ export interface ToolData { additionalFileCount?: number fastApplyResult?: unknown diffStats?: { added: number; removed: number } + stats?: DeleteFileStats } /** diff --git a/cli/src/ui/messages/extension/utils.ts b/cli/src/ui/messages/extension/utils.ts index f785c5f5f1f..cd5a7322f22 100644 --- a/cli/src/ui/messages/extension/utils.ts +++ b/cli/src/ui/messages/extension/utils.ts @@ -200,6 +200,8 @@ export function getToolIcon(tool: string): string { return "📖" case "runSlashCommand": return "▶" + case "deleteFile": + return "🗑️" default: return "⚙" }