From 13643165f1490420ecdd9853c5277445a4d27617 Mon Sep 17 00:00:00 2001 From: Nick Chomey Date: Thu, 9 Oct 2025 14:30:25 +0000 Subject: [PATCH 01/12] add context bar with button to remove files, @ menu option to add all open files/editors --- pnpm-lock.yaml | 16 +- src/package.json | 2 +- .../src/components/chat/ChatTextArea.tsx | 250 +++++++++--------- webview-ui/src/components/chat/ChatView.tsx | 44 ++- .../src/components/chat/ContextMenu.tsx | 4 + .../src/components/chat/ContextPillsBar.tsx | 67 +++++ .../chat/__tests__/ChatTextArea.spec.tsx | 60 +++++ .../chat/__tests__/ContextPillsBar.spec.tsx | 100 +++++++ webview-ui/src/index.css | 60 +++++ webview-ui/src/utils/context-mentions.ts | 9 + 10 files changed, 470 insertions(+), 142 deletions(-) create mode 100644 webview-ui/src/components/chat/ContextPillsBar.tsx create mode 100644 webview-ui/src/components/chat/__tests__/ContextPillsBar.spec.tsx diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d09c6adacc..c08a3311be7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2942,10 +2942,6 @@ packages: resolution: {integrity: sha512-t3yaEOuGu9NlIZ+hIeGbBjFtZT7j2cb2tg0fuaJKeGotchRjjLfrBA9Kwf8quhpP1EUuxModQg04q/mBwyg8uA==} engines: {node: '>=6.9.0'} - '@babel/runtime@7.28.3': - resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.28.4': resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} @@ -20055,8 +20051,6 @@ snapshots: '@babel/runtime@7.27.4': {} - '@babel/runtime@7.28.3': {} - '@babel/runtime@7.28.4': {} '@babel/template@7.27.2': @@ -20630,7 +20624,7 @@ snapshots: '@babel/preset-env': 7.28.3(@babel/core@7.28.3) '@babel/preset-react': 7.27.1(@babel/core@7.28.3) '@babel/preset-typescript': 7.27.1(@babel/core@7.28.3) - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 '@babel/runtime-corejs3': 7.28.3 '@babel/traverse': 7.28.3 '@docusaurus/logger': 3.8.1 @@ -35456,7 +35450,7 @@ snapshots: react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@6.0.0(react@19.1.1))(webpack@5.101.3(esbuild@0.25.9)): dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.1.1)' webpack: 5.101.3(esbuild@0.25.9) @@ -35511,13 +35505,13 @@ snapshots: react-router-config@5.1.1(react-router@5.3.4(react@19.1.1))(react@19.1.1): dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 react: 19.1.1 react-router: 5.3.4(react@19.1.1) react-router-dom@5.3.4(react@19.1.1): dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 history: 4.10.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -35528,7 +35522,7 @@ snapshots: react-router@5.3.4(react@19.1.1): dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 history: 4.10.1 hoist-non-react-statics: 3.3.2 loose-envify: 1.4.0 diff --git a/src/package.json b/src/package.json index fb7b04b525a..eb8e1ad9fce 100644 --- a/src/package.json +++ b/src/package.json @@ -3,7 +3,7 @@ "displayName": "%extension.displayName%", "description": "%extension.description%", "publisher": "kilocode", - "version": "4.101.0", + "version": "4.102.0", "icon": "assets/icons/logo-outline-black.png", "galleryBanner": { "color": "#FFFFFF", diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 834bdbe7b45..d1c19cd30d7 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -18,7 +18,7 @@ import { shouldShowContextMenu, SearchResult, } from "@src/utils/context-mentions" -import { convertToMentionPath } from "@/utils/path-mentions" +import { convertToMentionPath, escapeSpaces } from "@/utils/path-mentions" import { DropdownOptionType, Button, StandardTooltip } from "@/components/ui" // kilocode_change import Thumbnails from "../common/Thumbnails" @@ -62,6 +62,7 @@ interface ChatTextAreaProps { selectedImages: string[] setSelectedImages: React.Dispatch> onSend: () => void + onMentionAdd?: (mention: string) => void onSelectImages: () => void shouldDisableImages: boolean onHeightChange?: (height: number) => void @@ -84,6 +85,7 @@ export const ChatTextArea = forwardRef( selectedImages, setSelectedImages, onSend, + onMentionAdd, onSelectImages, shouldDisableImages, onHeightChange, @@ -240,7 +242,6 @@ export const ChatTextArea = forwardRef( const [searchQuery, setSearchQuery] = useState("") const textAreaRef = useRef(null) const [isMouseDownOnMenu, setIsMouseDownOnMenu] = useState(false) - const highlightLayerRef = useRef(null) const [selectedMenuIndex, setSelectedMenuIndex] = useState(-1) const [selectedType, setSelectedType] = useState(null) const [justDeletedSpaceAfterMention, setJustDeletedSpaceAfterMention] = useState(false) @@ -311,17 +312,30 @@ export const ChatTextArea = forwardRef( { type: ContextMenuOptionType.Terminal, value: "terminal" }, ...gitCommits, ...openedTabs - .filter((tab) => tab.path) - .map((tab) => ({ - type: ContextMenuOptionType.OpenedFile, - value: "/" + tab.path, - })), + .filter((tab) => tab.path && tab.path.trim() !== "" && tab.path.trim() !== "/") + .map((tab) => { + // tab.path is already a relative path (e.g., "src/file.ts") + // Format it directly as a mention path with @/ prefix + // Escape spaces in the path if present + const relativePath = tab.path || ""; + const pathWithEscapedSpaces = relativePath.includes(" ") ? escapeSpaces(relativePath) : relativePath; + const mentionPath = `@/${pathWithEscapedSpaces}`; + return { + type: ContextMenuOptionType.OpenedFile, + value: mentionPath, + }; + }), ...filePaths - .map((file) => "/" + file) - .filter((path) => !openedTabs.some((tab) => tab.path && "/" + tab.path === path)) // Filter out paths that are already in openedTabs - .map((path) => ({ - type: path.endsWith("/") ? ContextMenuOptionType.Folder : ContextMenuOptionType.File, - value: path, + .filter((file) => file && file.trim() !== "" && file.trim() !== "/") + .map((file) => `@/${file}`) + .filter((mentionPath) => mentionPath !== "@/") // Exclude root + .filter((mentionPath) => { + // Deduplicate with openedTabs + return !openedTabs.some((tab) => tab.path && `@/${tab.path}` === mentionPath); + }) + .map((mentionPath) => ({ + type: mentionPath.endsWith("/") ? ContextMenuOptionType.Folder : ContextMenuOptionType.File, + value: mentionPath, })), ] }, [filePaths, gitCommits, openedTabs]) @@ -383,7 +397,29 @@ export const ChatTextArea = forwardRef( return } - if ( + if (type === ContextMenuOptionType.AllOpenEditors) { + // Add all open files as context pills + if (openedTabs && openedTabs.length > 0) { + for (const tab of openedTabs) { + // Skip tabs with invalid paths + if (!tab.path || tab.path.trim() === "" || tab.path.trim() === "/") { + continue; + } + + // tab.path is already a relative path (e.g., "src/file.ts") + // Format it directly as a mention path with @/ prefix + // Escape spaces in the path if present + const pathWithEscapedSpaces = tab.path.includes(" ") ? escapeSpaces(tab.path) : tab.path; + const mentionPath = `@/${pathWithEscapedSpaces}`; + + if (onMentionAdd) { + onMentionAdd(mentionPath); + } + } + } + setShowContextMenu(false); + setSelectedType(null); + } else if ( type === ContextMenuOptionType.File || type === ContextMenuOptionType.Folder || type === ContextMenuOptionType.Git @@ -399,43 +435,72 @@ export const ChatTextArea = forwardRef( setShowContextMenu(false) setSelectedType(null) - if (textAreaRef.current) { - let insertValue = value || "" - - if (type === ContextMenuOptionType.URL) { - insertValue = value || "" - } else if (type === ContextMenuOptionType.File || type === ContextMenuOptionType.Folder) { - insertValue = value || "" - } else if (type === ContextMenuOptionType.Problems) { - insertValue = "problems" - } else if (type === ContextMenuOptionType.Terminal) { - insertValue = "terminal" - } else if (type === ContextMenuOptionType.Git) { - insertValue = value || "" - } + // Determine the mention value + let insertValue = value || "" + + if (type === ContextMenuOptionType.URL) { + insertValue = value || ""; + } else if (type === ContextMenuOptionType.File || type === ContextMenuOptionType.Folder) { + insertValue = value || ""; + } else if (type === ContextMenuOptionType.Problems) { + insertValue = "problems"; + } else if (type === ContextMenuOptionType.Terminal) { + insertValue = "terminal"; + } else if (type === ContextMenuOptionType.Git) { + insertValue = value || ""; + } - const { newValue, mentionIndex } = insertMention( - textAreaRef.current.value, - cursorPosition, - insertValue, - ) + // If onMentionAdd callback is provided, use it (new behavior - add to pills) + if (onMentionAdd) { + // Remove the @ character from textarea + if (textAreaRef.current) { + const beforeCursor = textAreaRef.current.value.slice(0, cursorPosition); + const afterCursor = textAreaRef.current.value.slice(cursorPosition); + const lastAtIndex = beforeCursor.lastIndexOf("@"); - setInputValue(newValue) - const newCursorPosition = newValue.indexOf(" ", mentionIndex + insertValue.length) + 1 - setCursorPosition(newCursorPosition) - setIntendedCursorPosition(newCursorPosition) + if (lastAtIndex !== -1) { + const newValue = beforeCursor.slice(0, lastAtIndex) + afterCursor; + setInputValue(newValue); + setCursorPosition(lastAtIndex); + setIntendedCursorPosition(lastAtIndex); + } + } + + // Add to context pills + onMentionAdd(insertValue); - // Scroll to cursor. + // Focus textarea setTimeout(() => { if (textAreaRef.current) { - textAreaRef.current.blur() - textAreaRef.current.focus() + textAreaRef.current.focus(); } - }, 0) + }, 0); + } else { + // Old behavior - insert into textarea (for backwards compatibility) + if (textAreaRef.current) { + const { newValue, mentionIndex } = insertMention( + textAreaRef.current.value, + cursorPosition, + insertValue, + ) + + setInputValue(newValue); + const newCursorPosition = newValue.indexOf(" ", mentionIndex + insertValue.length) + 1; + setCursorPosition(newCursorPosition); + setIntendedCursorPosition(newCursorPosition) + + // Scroll to cursor. + setTimeout(() => { + if (textAreaRef.current) { + textAreaRef.current.blur(); + textAreaRef.current.focus(); + } + }, 0); + } } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [setInputValue, cursorPosition], + [setInputValue, cursorPosition, onMentionAdd], ) // kilocode_change start: pull slash commands from Cline @@ -890,47 +955,7 @@ export const ChatTextArea = forwardRef( setIsMouseDownOnMenu(true) }, []) - const updateHighlights = useCallback(() => { - if (!textAreaRef.current || !highlightLayerRef.current) return - - // kilocode_change start: pull slash commands from Cline - let processedText = textAreaRef.current.value - - processedText = processedText - .replace(/\n$/, "\n\n") - .replace(/[<>&]/g, (c) => ({ "<": "<", ">": ">", "&": "&" })[c] || c) - .replace(mentionRegexGlobal, '$&') - - // check for highlighting /slash-commands - if (/^\s*\//.test(processedText)) { - const slashIndex = processedText.indexOf("/") - - // end of command is end of text or first whitespace - const spaceIndex = processedText.indexOf(" ", slashIndex) - const endIndex = spaceIndex > -1 ? spaceIndex : processedText.length - - // extract and validate the exact command text - const commandText = processedText.substring(slashIndex + 1, endIndex) - const isValidCommand = validateSlashCommand(commandText, customModes) - - if (isValidCommand) { - const fullCommand = processedText.substring(slashIndex, endIndex) // includes slash - - const highlighted = `${fullCommand}` - processedText = - processedText.substring(0, slashIndex) + highlighted + processedText.substring(endIndex) - } - } - // kilocode_change end - - highlightLayerRef.current.innerHTML = processedText - highlightLayerRef.current.scrollTop = textAreaRef.current.scrollTop - highlightLayerRef.current.scrollLeft = textAreaRef.current.scrollLeft - }, [customModes]) - - useLayoutEffect(() => { - updateHighlights() - }, [inputValue, updateHighlights]) + // Highlight overlay removed - mentions are now shown in ContextPillsBar instead const updateCursorPosition = useCallback(() => { if (textAreaRef.current) { @@ -961,7 +986,17 @@ export const ChatTextArea = forwardRef( const lines = text.split(/\r?\n/).filter((line) => line.trim() !== "") if (lines.length > 0) { - // Process each line as a separate file path + // Check if Shift key is pressed for context attachment + if (e.shiftKey && onMentionAdd) { + // Add files to context pills instead of textarea + for (const line of lines) { + const mentionText = convertToMentionPath(line, cwd || ""); + onMentionAdd(mentionText); + } + return; + } + + // Process each line as a separate file path (original behavior) let newValue = inputValue.slice(0, cursorPosition) let totalLength = 0 @@ -969,7 +1004,7 @@ export const ChatTextArea = forwardRef( for (let i = 0; i < lines.length; i++) { const line = lines[i] // Convert each path to a mention-friendly format - const mentionText = convertToMentionPath(line, cwd) + const mentionText = convertToMentionPath(line, cwd || "") newValue += mentionText totalLength += mentionText.length @@ -1058,6 +1093,7 @@ export const ChatTextArea = forwardRef( setCursorPosition, setIntendedCursorPosition, shouldDisableImages, + onMentionAdd, setSelectedImages, t, selectedImages.length, // kilocode_change - added selectedImages.length @@ -1321,35 +1357,6 @@ export const ChatTextArea = forwardRef( "overflow-hidden", "rounded", )}> -
{ if (typeof ref === "function") { @@ -1360,10 +1367,7 @@ export const ChatTextArea = forwardRef( textAreaRef.current = el }} value={inputValue} - onChange={(e) => { - handleInputChange(e) - updateHighlights() - }} + onChange={handleInputChange} onFocus={() => setIsFocused(true)} onKeyDown={handleKeyDown} onKeyUp={handleKeyUp} @@ -1414,7 +1418,6 @@ export const ChatTextArea = forwardRef( "scrollbar-hide", "pb-16", // kilocode_change: Increased padding to prevent overlap with control bar )} - onScroll={() => updateHighlights()} /> {/* kilocode_change {Transparent overlay at bottom of textArea to avoid text overlap } */}
( className={cn("chat-text-area", "relative", "flex", "flex-col", "outline-none")} onDrop={handleDrop} onDragOver={(e) => { - // Only allowed to drop images/files on shift key pressed. - if (!e.shiftKey) { - setIsDraggingOver(false) - return - } - + // Allow drag over to show visual feedback, but only accept with Shift key e.preventDefault() - setIsDraggingOver(true) e.dataTransfer.dropEffect = "copy" + + // Set dragging state based on Shift key + setIsDraggingOver(e.shiftKey) }} onDragLeave={(e) => { e.preventDefault() diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 5fc426d0c9e..77f7390374e 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -51,6 +51,7 @@ import Announcement from "./Announcement" import BrowserSessionRow from "./BrowserSessionRow" import ChatRow from "./ChatRow" import { ChatTextArea } from "./ChatTextArea" +import { ContextPillsBar } from "./ContextPillsBar" // import TaskHeader from "./TaskHeader"// kilocode_change import KiloTaskHeader from "../kilocode/KiloTaskHeader" // kilocode_change import AutoApproveMenu from "./AutoApproveMenu" @@ -188,6 +189,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction(null) const [sendingDisabled, setSendingDisabled] = useState(false) const [selectedImages, setSelectedImages] = useState([]) + const [contextMentions, setContextMentions] = useState([]) // we need to hold on to the ask because useEffect > lastMessage will always let us know when an ask comes in and handle it, but by the time handleMessage is called, the last message might not be the ask anymore (it could be a say that followed) const [clineAsk, setClineAsk] = useState(undefined) @@ -611,6 +613,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + // Validate mention before adding - skip empty or invalid mentions + const trimmed = mention?.trim() || ""; + if (!trimmed || trimmed === "@" || trimmed === "@/") { + return; + } + // Check for duplicates before adding + setContextMentions((prev) => { + if (prev.includes(mention)) { + return prev; + } + return [...prev, mention]; + }); + }, []); + + const handleRemoveMention = useCallback((mention: string) => { + setContextMentions((prev) => prev.filter((m) => m !== mention)); + }, []) + /** * Handles sending messages to the extension * @param text - The message text to send @@ -628,7 +650,17 @@ const ChatViewComponent: React.ForwardRefRenderFunction { text = text.trim() - if (text || images.length > 0) { + // Inject context mentions into the text + // Filter out invalid mentions and ensure proper formatting + const validMentions = contextMentions + .filter((m) => m && m.trim() !== "" && m.trim() !== "@" && m.trim() !== "@/") + .map((m) => m.trim()) + .map((m) => m.startsWith("@") ? m : `@${m}`); + + const mentionsText = validMentions.join(" "); + const fullText = mentionsText ? `${mentionsText} ${text}` : text; + + if (fullText || images.length > 0) { if (sendingDisabled) { try { console.log("queueMessage", text, images) @@ -648,7 +680,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction + handleSendMessage(inputValue, selectedImages)} + onMentionAdd={handleAddMention} onSelectImages={selectImages} shouldDisableImages={shouldDisableImages} onHeightChange={() => { diff --git a/webview-ui/src/components/chat/ContextMenu.tsx b/webview-ui/src/components/chat/ContextMenu.tsx index f29be07a29d..8d3e5b4301e 100644 --- a/webview-ui/src/components/chat/ContextMenu.tsx +++ b/webview-ui/src/components/chat/ContextMenu.tsx @@ -150,6 +150,8 @@ const ContextMenu: React.FC = ({ case ContextMenuOptionType.Image: return Add Image // kilocode_change end + case ContextMenuOptionType.AllOpenEditors: + return All Open Editors case ContextMenuOptionType.Git: if (option.value) { return ( @@ -236,6 +238,8 @@ const ContextMenu: React.FC = ({ case ContextMenuOptionType.Image: return "device-camera" // kilocode_change end + case ContextMenuOptionType.AllOpenEditors: + return "window" case ContextMenuOptionType.Git: return "git-commit" case ContextMenuOptionType.NoResults: diff --git a/webview-ui/src/components/chat/ContextPillsBar.tsx b/webview-ui/src/components/chat/ContextPillsBar.tsx new file mode 100644 index 00000000000..6e3f30ee064 --- /dev/null +++ b/webview-ui/src/components/chat/ContextPillsBar.tsx @@ -0,0 +1,67 @@ +import { X } from "lucide-react" +import { StandardTooltip } from "@/components/ui" + +interface ContextPillsBarProps { + mentions: string[] + onRemove: (mention: string) => void +} + +export const ContextPillsBar = ({ mentions, onRemove }: ContextPillsBarProps) => { + if (mentions.length === 0) return null + + const getDisplayName = (path: string) => { + // Remove @ prefix if present + const cleanPath = path.startsWith("@") ? path.slice(1) : path + + // Handle special mentions + if (["problems", "terminal", "git-changes"].includes(cleanPath)) { + return cleanPath + } + + // Handle git commit hashes + if (/^[a-f0-9]{7,40}$/i.test(cleanPath)) { + return cleanPath.substring(0, 7) + } + + // Handle URLs + if (cleanPath.startsWith("http://") || cleanPath.startsWith("https://")) { + try { + const url = new URL(cleanPath) + return url.hostname + } catch { + return cleanPath + } + } + + // Get basename for file paths + const parts = cleanPath.split("/") + return parts[parts.length - 1] || cleanPath + } + + return ( +
+ {mentions.map((mention, index) => { + const displayName = getDisplayName(mention) + // For hover text, show the path without the @ prefix and with proper formatting + const cleanPath = mention.startsWith("@") ? mention.slice(1) : mention + // Ensure the path starts with / for display, but don't add an extra one + const displayPath = cleanPath.startsWith("/") ? cleanPath : `/${cleanPath}` + + return ( + +
+ {displayName} + +
+
+ ) + })} +
+ ) +} \ No newline at end of file diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx index 81baea06207..b94604ef0a1 100644 --- a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx @@ -506,6 +506,66 @@ describe("ChatTextArea", () => { expect(setInputValue).not.toHaveBeenCalled() }) + it("should add files to context pills when Shift is pressed during drag and drop", () => { + const onMentionAdd = vi.fn() + + const { container } = render( + , + ) + + // Create a mock dataTransfer object with text data containing multiple file paths + const dataTransfer = { + getData: vi.fn().mockReturnValue("/Users/test/project/file1.js\n/Users/test/project/file2.js"), + files: [], + } + + // Create a custom drop event with Shift key + const dropEvent = new Event('drop', { bubbles: true, cancelable: true }) as any + dropEvent.dataTransfer = dataTransfer + dropEvent.preventDefault = vi.fn() + dropEvent.shiftKey = true + + // Simulate drop event with Shift key pressed + container.querySelector(".chat-text-area")!.dispatchEvent(dropEvent) + + // Verify onMentionAdd was called for each file path + expect(onMentionAdd).toHaveBeenCalledTimes(2) + expect(onMentionAdd).toHaveBeenCalledWith("@/file1.js") + expect(onMentionAdd).toHaveBeenCalledWith("@/file2.js") + + // Verify setInputValue was NOT called (files should go to context pills, not textarea) + expect(defaultProps.setInputValue).not.toHaveBeenCalled() + }) + + it("should add files to textarea when Shift is NOT pressed during drag and drop", () => { + const onMentionAdd = vi.fn() + + const { container } = render( + , + ) + + // Create a mock dataTransfer object with text data containing multiple file paths + const dataTransfer = { + getData: vi.fn().mockReturnValue("/Users/test/project/file1.js\n/Users/test/project/file2.js"), + files: [], + } + + // Create a custom drop event without Shift key + const dropEvent = new Event('drop', { bubbles: true, cancelable: true }) as any + dropEvent.dataTransfer = dataTransfer + dropEvent.preventDefault = vi.fn() + dropEvent.shiftKey = false + + // Simulate drop event without Shift key + container.querySelector(".chat-text-area")!.dispatchEvent(dropEvent) + + // Verify onMentionAdd was NOT called + expect(onMentionAdd).not.toHaveBeenCalled() + + // Verify setInputValue was called with the correct value (original behavior) + expect(defaultProps.setInputValue).toHaveBeenCalledWith("@/file1.js @/file2.js Initial text") + }) + describe("prompt history navigation", () => { const mockClineMessages = [ { type: "say", say: "user_feedback", text: "First prompt", ts: 1000 }, diff --git a/webview-ui/src/components/chat/__tests__/ContextPillsBar.spec.tsx b/webview-ui/src/components/chat/__tests__/ContextPillsBar.spec.tsx new file mode 100644 index 00000000000..b81b127b985 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/ContextPillsBar.spec.tsx @@ -0,0 +1,100 @@ +import { describe, it, expect, vi } from "vitest" +import { render, screen } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { ContextPillsBar } from "../ContextPillsBar" + +// Mock StandardTooltip to simplify testing +vi.mock("@/components/ui", () => ({ + StandardTooltip: ({ children }: any) => <>{children}, +})) + +describe("ContextPillsBar", () => { + it("should render nothing when mentions array is empty", () => { + const { container } = render() + expect(container.firstChild).toBeNull() + }) + + it("should render pills for each mention", () => { + const mentions = ["/src/App.tsx", "/src/utils/helper.ts", "/README.md"] + render() + + expect(screen.getByText("App.tsx")).toBeInTheDocument() + expect(screen.getByText("helper.ts")).toBeInTheDocument() + expect(screen.getByText("README.md")).toBeInTheDocument() + }) + + it("should display only filename for file paths", () => { + const mentions = ["/path/to/deeply/nested/file.tsx"] + render() + + expect(screen.getByText("file.tsx")).toBeInTheDocument() + expect(screen.queryByText("/path/to/deeply/nested/file.tsx")).not.toBeInTheDocument() + }) + + it("should display special mentions correctly", () => { + const mentions = ["problems", "terminal", "git-changes"] + render() + + expect(screen.getByText("problems")).toBeInTheDocument() + expect(screen.getByText("terminal")).toBeInTheDocument() + expect(screen.getByText("git-changes")).toBeInTheDocument() + }) + + it("should display shortened git commit hash", () => { + const mentions = ["a1b2c3d4e5f6789"] + render() + + expect(screen.getByText("a1b2c3d")).toBeInTheDocument() + }) + + it("should display hostname for URLs", () => { + const mentions = ["https://example.com/path/to/page"] + render() + + expect(screen.getByText("example.com")).toBeInTheDocument() + }) + + it("should call onRemove when remove button is clicked", async () => { + const onRemove = vi.fn() + const mentions = ["/src/App.tsx", "/src/utils/helper.ts"] + const user = userEvent.setup() + + render() + + const removeButtons = screen.getAllByRole("button") + await user.click(removeButtons[0]) + + expect(onRemove).toHaveBeenCalledWith("/src/App.tsx") + }) + + it("should render remove button for each pill", () => { + const mentions = ["/src/App.tsx", "/src/utils/helper.ts", "/README.md"] + render() + + const removeButtons = screen.getAllByRole("button") + expect(removeButtons).toHaveLength(3) + }) + + it("should have correct aria-label for remove buttons", () => { + const mentions = ["/src/App.tsx"] + render() + + const removeButton = screen.getByLabelText("Remove App.tsx") + expect(removeButton).toBeInTheDocument() + }) + + it("should handle mentions with @ prefix", () => { + const mentions = ["@/src/App.tsx"] + render() + + expect(screen.getByText("App.tsx")).toBeInTheDocument() + }) + + it("should handle multiple pills with same filename", () => { + const mentions = ["/src/components/App.tsx", "/tests/App.tsx"] + render() + + const pills = screen.getAllByText("App.tsx") + expect(pills).toHaveLength(2) + }) +}) \ No newline at end of file diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index 6d019872b71..670e88605b0 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -398,6 +398,66 @@ vscode-dropdown::part(listbox) { border-radius: 3px; box-shadow: 0 0 0 0.5px color-mix(in srgb, var(--vscode-badge-foreground) 30%, transparent); } +/* Context Pills Bar */ +.context-pills-bar { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 8px 12px; + background-color: var(--vscode-editor-background); + border-bottom: 1px solid color-mix(in srgb, var(--vscode-panel-border) 50%, transparent); + min-height: 40px; + align-items: center; +} + +.context-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 4px 4px 8px; + background-color: color-mix(in srgb, var(--vscode-badge-background) 85%, transparent); + border: 1px solid color-mix(in srgb, var(--vscode-badge-background) 40%, transparent); + border-radius: 4px; + font-size: calc(var(--vscode-font-size) * 0.9); + color: var(--vscode-badge-foreground); + transition: all 0.15s ease; +} + +.context-pill:hover { + background-color: var(--vscode-badge-background); + border-color: color-mix(in srgb, var(--vscode-badge-background) 60%, transparent); +} + +.context-pill-name { + white-space: nowrap; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; +} + +.context-pill-remove { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 2px; + background: transparent; + border: none; + border-radius: 2px; + cursor: pointer; + opacity: 0.6; + transition: all 0.1s ease; + color: currentColor; +} + +.context-pill-remove:hover { + opacity: 1; + background-color: color-mix(in srgb, var(--vscode-badge-foreground) 15%, transparent); +} + +.context-pill-remove:active { + transform: scale(0.9); +} + /** * vscrui Overrides / Hacks diff --git a/webview-ui/src/utils/context-mentions.ts b/webview-ui/src/utils/context-mentions.ts index c9b273a8ff4..f93c2348386 100644 --- a/webview-ui/src/utils/context-mentions.ts +++ b/webview-ui/src/utils/context-mentions.ts @@ -110,6 +110,7 @@ export enum ContextMenuOptionType { Image = "image", // kilocode_change Command = "command", // Add command type SectionHeader = "sectionHeader", // Add section header type + AllOpenEditors = "allOpenEditors", // Add all open editors type } export interface ContextMenuQueryItem { @@ -254,6 +255,7 @@ export function getContextMenuOptions( { type: ContextMenuOptionType.Problems }, { type: ContextMenuOptionType.Terminal }, { type: ContextMenuOptionType.URL }, + { type: ContextMenuOptionType.AllOpenEditors }, { type: ContextMenuOptionType.Folder }, { type: ContextMenuOptionType.File }, { type: ContextMenuOptionType.Image }, // kilocode_change @@ -284,6 +286,13 @@ export function getContextMenuOptions( if (query.startsWith("http")) { suggestions.push({ type: ContextMenuOptionType.URL, value: query }) } + if ("all open editors".startsWith(lowerQuery) || "opened".startsWith(lowerQuery)) { + suggestions.push({ + type: ContextMenuOptionType.AllOpenEditors, + label: "All Open Editors", + description: "Add all open files to context", + }) + } // Add exact SHA matches to suggestions if (/^[a-f0-9]{7,40}$/i.test(lowerQuery)) { From d1f0ef21b0a7a48389b92577ce68318d35b04d49 Mon Sep 17 00:00:00 2001 From: nickchomey <88559987+nickchomey@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:47:30 -0600 Subject: [PATCH 02/12] Update webview-ui/src/components/chat/ChatTextArea.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- webview-ui/src/components/chat/ChatTextArea.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index d1c19cd30d7..fb45df12e76 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -955,7 +955,13 @@ export const ChatTextArea = forwardRef( setIsMouseDownOnMenu(true) }, []) - // Highlight overlay removed - mentions are now shown in ContextPillsBar instead + // The visual highlight overlay for mentions within the textarea was removed. + // Previously, mentions were visually highlighted directly in the textarea as users typed. + // This functionality was removed to simplify the input UI and improve maintainability. + // Mentions are now visually represented in the ContextPillsBar component above the textarea, + // providing a clearer and more accessible way to view and manage mentions in the message. + // If visual highlighting in the textarea is needed in the future, refer to previous implementations + // prior to this change for guidance. const updateCursorPosition = useCallback(() => { if (textAreaRef.current) { From 926f2cf38bcac1640d5913931e071349c5107704 Mon Sep 17 00:00:00 2001 From: nickchomey <88559987+nickchomey@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:49:06 -0600 Subject: [PATCH 03/12] create isValidMention utility function Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- webview-ui/src/components/chat/ChatView.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 77f7390374e..78a4944495c 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -622,11 +622,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + const trimmed = mention?.trim() || ""; + return !!trimmed && trimmed !== "@" && trimmed !== "@/"; + }; + const handleAddMention = useCallback((mention: string) => { // Validate mention before adding - skip empty or invalid mentions - const trimmed = mention?.trim() || ""; - if (!trimmed || trimmed === "@" || trimmed === "@/") { - return; + if (!isValidMention(mention)) { } // Check for duplicates before adding setContextMentions((prev) => { @@ -651,11 +655,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction m && m.trim() !== "" && m.trim() !== "@" && m.trim() !== "@/") - .map((m) => m.trim()) - .map((m) => m.startsWith("@") ? m : `@${m}`); + .filter(isValidMention) + .map((m) => m.trim()) + .map((m) => m.startsWith("@") ? m : `@${m}`); const mentionsText = validMentions.join(" "); const fullText = mentionsText ? `${mentionsText} ${text}` : text; From ad718697eff14fcbfd1ed92f1f9f6f2993b2bc92 Mon Sep 17 00:00:00 2001 From: nickchomey <88559987+nickchomey@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:49:06 -0600 Subject: [PATCH 04/12] fix test --- webview-ui/src/utils/__tests__/context-mentions.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webview-ui/src/utils/__tests__/context-mentions.spec.ts b/webview-ui/src/utils/__tests__/context-mentions.spec.ts index 34b198d3e46..e590fcacf1a 100644 --- a/webview-ui/src/utils/__tests__/context-mentions.spec.ts +++ b/webview-ui/src/utils/__tests__/context-mentions.spec.ts @@ -238,11 +238,12 @@ describe("getContextMenuOptions", () => { it("should return all option types for empty query", () => { const result = getContextMenuOptions("", null, []) - expect(result).toHaveLength(7) // kilocode_change: added image option + expect(result).toHaveLength(8) // kilocode_change: added image and allOpenEditors options expect(result.map((item) => item.type)).toEqual([ ContextMenuOptionType.Problems, ContextMenuOptionType.Terminal, ContextMenuOptionType.URL, + ContextMenuOptionType.AllOpenEditors, ContextMenuOptionType.Folder, ContextMenuOptionType.File, ContextMenuOptionType.Image, // kilocode_change From 01d63fa755acf0d45d158a417ab7873167653f07 Mon Sep 17 00:00:00 2001 From: nickchomey <88559987+nickchomey@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:49:06 -0600 Subject: [PATCH 05/12] fix other tests --- packages/build/src/__tests__/index.test.ts | 2 +- src/extension.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/build/src/__tests__/index.test.ts b/packages/build/src/__tests__/index.test.ts index 638b3fd0104..eda70fac1c9 100644 --- a/packages/build/src/__tests__/index.test.ts +++ b/packages/build/src/__tests__/index.test.ts @@ -148,7 +148,7 @@ describe("generatePackageJson", () => { { command: "roo-code-nightly.plusButtonClicked", title: "%command.newTask.title%", - icon: "$(edit)", + icon: "$(add)", }, { command: "roo-code-nightly.openInNewTab", diff --git a/src/extension.ts b/src/extension.ts index 7b7fbbf4a8e..fb4654a62b5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -170,6 +170,13 @@ export async function activate(context: vscode.ExtensionContext) { `[authStateChangedHandler] remoteControlEnabled(false) failed: ${error instanceof Error ? error.message : String(error)}`, ) } + try { + await BridgeOrchestrator.disconnect() + } catch (error) { + cloudLogger( + `[authStateChangedHandler] BridgeOrchestrator.disconnect() failed: ${error instanceof Error ? error.message : String(error)}`, + ) + } } } From c473e4035947a292b77b1a75b863ad2ddec38c4d Mon Sep 17 00:00:00 2001 From: nickchomey <88559987+nickchomey@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:49:06 -0600 Subject: [PATCH 06/12] fix missing early return --- webview-ui/src/components/chat/ChatView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 78a4944495c..8292c2f1e2e 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -631,6 +631,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { // Validate mention before adding - skip empty or invalid mentions if (!isValidMention(mention)) { + return; // kilocode_change: return early if mention is invalid } // Check for duplicates before adding setContextMentions((prev) => { From 18e3f4d09cc81355bc9f797f7749ee53666fe564 Mon Sep 17 00:00:00 2001 From: nickchomey <88559987+nickchomey@users.noreply.github.com> Date: Thu, 9 Oct 2025 12:38:19 -0600 Subject: [PATCH 07/12] dont call trim twice Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- webview-ui/src/components/chat/ChatTextArea.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index fb45df12e76..c1bf53d77b6 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -312,7 +312,11 @@ export const ChatTextArea = forwardRef( { type: ContextMenuOptionType.Terminal, value: "terminal" }, ...gitCommits, ...openedTabs - .filter((tab) => tab.path && tab.path.trim() !== "" && tab.path.trim() !== "/") + .filter((tab) => { + if (!tab.path) return false; + const trimmedPath = tab.path.trim(); + return trimmedPath !== "" && trimmedPath !== "/"; + }) .map((tab) => { // tab.path is already a relative path (e.g., "src/file.ts") // Format it directly as a mention path with @/ prefix From 5de9c8b500ce4b68dbf7307bb9c61cf81ef88b13 Mon Sep 17 00:00:00 2001 From: nickchomey <88559987+nickchomey@users.noreply.github.com> Date: Thu, 9 Oct 2025 12:38:44 -0600 Subject: [PATCH 08/12] dont call trim twice Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- webview-ui/src/components/chat/ChatTextArea.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index c1bf53d77b6..f7f611ec1a7 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -406,7 +406,8 @@ export const ChatTextArea = forwardRef( if (openedTabs && openedTabs.length > 0) { for (const tab of openedTabs) { // Skip tabs with invalid paths - if (!tab.path || tab.path.trim() === "" || tab.path.trim() === "/") { + const trimmedPath = tab.path ? tab.path.trim() : ""; + if (!tab.path || trimmedPath === "" || trimmedPath === "/") { continue; } From a1cef8183cbef1900cfe9f8f22c583f0d76e7cef Mon Sep 17 00:00:00 2001 From: nickchomey <88559987+nickchomey@users.noreply.github.com> Date: Thu, 9 Oct 2025 12:41:00 -0600 Subject: [PATCH 09/12] dont call trim twice Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- webview-ui/src/components/chat/ChatTextArea.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index f7f611ec1a7..7631f3d1888 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -330,7 +330,11 @@ export const ChatTextArea = forwardRef( }; }), ...filePaths - .filter((file) => file && file.trim() !== "" && file.trim() !== "/") + .filter((file) => { + if (!file) return false; + const trimmed = file.trim(); + return trimmed !== "" && trimmed !== "/"; + }) .map((file) => `@/${file}`) .filter((mentionPath) => mentionPath !== "@/") // Exclude root .filter((mentionPath) => { From 3bd1a63a1683e45a32c8a201c6d5d54ad02e1d27 Mon Sep 17 00:00:00 2001 From: nickchomey <88559987+nickchomey@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:49:06 -0600 Subject: [PATCH 10/12] extract isvalidmention from component --- webview-ui/src/components/chat/ChatView.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 8292c2f1e2e..8eb7e4019f9 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -83,6 +83,13 @@ export const MAX_IMAGES_PER_MESSAGE = 20 // This is the Anthropic limit. const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0 + +// Utility function to validate mentions +const isValidMention = (mention: string): boolean => { + const trimmed = mention?.trim() || ""; + return !!trimmed && trimmed !== "@" && trimmed !== "@/"; +}; + const ChatViewComponent: React.ForwardRefRenderFunction = ( { isHidden, showAnnouncement, hideAnnouncement }, ref, @@ -622,12 +629,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - const trimmed = mention?.trim() || ""; - return !!trimmed && trimmed !== "@" && trimmed !== "@/"; - }; - const handleAddMention = useCallback((mention: string) => { // Validate mention before adding - skip empty or invalid mentions if (!isValidMention(mention)) { From 2a874e1edf304d81892781ca0c53fcc3c75f04cc Mon Sep 17 00:00:00 2001 From: Nick Chomey Date: Thu, 9 Oct 2025 18:53:04 +0000 Subject: [PATCH 11/12] move close x button to left side so can more rapidly remove pills --- webview-ui/src/components/chat/ContextPillsBar.tsx | 2 +- webview-ui/src/index.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webview-ui/src/components/chat/ContextPillsBar.tsx b/webview-ui/src/components/chat/ContextPillsBar.tsx index 6e3f30ee064..a472e5542f6 100644 --- a/webview-ui/src/components/chat/ContextPillsBar.tsx +++ b/webview-ui/src/components/chat/ContextPillsBar.tsx @@ -50,7 +50,6 @@ export const ContextPillsBar = ({ mentions, onRemove }: ContextPillsBarProps) => return (
- {displayName} + {displayName}
) diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index 670e88605b0..2423e819691 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -414,7 +414,7 @@ vscode-dropdown::part(listbox) { display: inline-flex; align-items: center; gap: 4px; - padding: 4px 4px 4px 8px; + padding: 4px 8px 4px 4px; background-color: color-mix(in srgb, var(--vscode-badge-background) 85%, transparent); border: 1px solid color-mix(in srgb, var(--vscode-badge-background) 40%, transparent); border-radius: 4px; From b60c3058243cc8374cf80bcda3d83392589414d8 Mon Sep 17 00:00:00 2001 From: Nick Chomey Date: Thu, 9 Oct 2025 22:14:47 +0000 Subject: [PATCH 12/12] add right click to add to context from file explore - supports multiselect --- packages/types/src/vscode.ts | 1 + src/activate/registerCommands.ts | 81 +++++++++++++++++++++ src/package.json | 15 +++- src/package.nls.json | 1 + src/shared/ExtensionMessage.ts | 1 + webview-ui/src/components/chat/ChatView.tsx | 10 +++ 6 files changed, 107 insertions(+), 2 deletions(-) diff --git a/packages/types/src/vscode.ts b/packages/types/src/vscode.ts index ed8af699d28..0854b193c14 100644 --- a/packages/types/src/vscode.ts +++ b/packages/types/src/vscode.ts @@ -63,6 +63,7 @@ export const commandIds = [ "handleExternalUri", // kilocode_change - for JetBrains plugin URL forwarding "focusPanel", "toggleAutoApprove", + "addFileToContext", // kilocode_change ] as const export type CommandId = (typeof commandIds)[number] diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index 78d41ea2c3b..75e89dfbfe0 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -1,4 +1,5 @@ import * as vscode from "vscode" +import * as path from "path" // kilocode_change import delay from "delay" import type { CommandId } from "@roo-code/types" @@ -289,6 +290,86 @@ const getCommandsMap = ({ context, outputChannel }: RegisterCommandOptions): Rec action: "toggleAutoApprove", }) }, + // kilocode_change: Add files/folders to context from Explorer context menu + addFileToContext: async (...args: any[]) => { + // Helper to extract URIs from many possible shapes passed by VS Code + const tryExtractUris = (val: any): vscode.Uri[] => { + const out: vscode.Uri[] = [] + if (!val) return out + // Direct Uri + if (val instanceof vscode.Uri) { + out.push(val) + return out + } + // Arrays + if (Array.isArray(val)) { + for (const item of val) out.push(...tryExtractUris(item)) + return out + } + // Explorer items + if (val.resourceUri instanceof vscode.Uri) out.push(val.resourceUri) + if (val.uri instanceof vscode.Uri) out.push(val.uri) + if (val.resource instanceof vscode.Uri) out.push(val.resource) + return out + } + + const uris: vscode.Uri[] = [] + // VS Code commonly passes (resource, allSelectedResources) for Explorer multi-select + if (args.length > 0) uris.push(...tryExtractUris(args[0])) + if (args.length > 1) uris.push(...tryExtractUris(args[1])) + + // Fallback: active editor if nothing extracted + if (uris.length === 0) { + const activeEditor = vscode.window.activeTextEditor + if (activeEditor?.document?.uri) { + uris.push(activeEditor.document.uri) + } + } + + if (uris.length === 0) { + vscode.window.showInformationMessage("No file or folder selected.") + return + } + + // Convert to mention strings for context pills (use workspace-relative with leading '@/') + const workspaceFolders = vscode.workspace.workspaceFolders ?? [] + const asMention = async (uri: vscode.Uri) => { + const folder = workspaceFolders.find((f) => uri.fsPath.startsWith(f.uri.fsPath)) + let isFolder = false + try { + const stat = await vscode.workspace.fs.stat(uri) + isFolder = stat.type === vscode.FileType.Directory + } catch { + // ignore stat errors; fall back to heuristics + isFolder = uri.path.endsWith("/") || uri.fsPath.endsWith(path.sep) + } + if (folder) { + // Compute relative path and normalize + let rel = uri.fsPath.substring(folder.uri.fsPath.length).replace(/\\/g, "/") + // Remove any leading slashes from rel to avoid double slashes + rel = rel.replace(/^\/+/, "") + return `@/${rel}${isFolder ? "/" : ""}` + } + // Fall back to basename with '@/' + const base = path.basename(uri.fsPath) + return `@/${base}${isFolder ? "/" : ""}` + } + + const mentionList = await Promise.all(uris.map(asMention)) + const mentions = Array.from(new Set(mentionList)) + + const visibleProvider = getVisibleProviderOrLog(outputChannel) + if (!visibleProvider) return + + // Add mentions directly to pills bar in the webview + await visibleProvider.postMessageToWebview({ + type: "action", + action: "addContextMentions", + values: { mentions }, + }) + + vscode.window.setStatusBarMessage(`Added ${mentions.length} item(s) to Kilo context`, 2000) + }, }) export const openClineInNewTab = async ({ context, outputChannel }: Omit) => { diff --git a/src/package.json b/src/package.json index eb8e1ad9fce..84805f9f5a9 100644 --- a/src/package.json +++ b/src/package.json @@ -3,7 +3,7 @@ "displayName": "%extension.displayName%", "description": "%extension.description%", "publisher": "kilocode", - "version": "4.102.0", + "version": "4.101.0", "icon": "assets/icons/logo-outline-black.png", "galleryBanner": { "color": "#FFFFFF", @@ -292,9 +292,20 @@ "command": "kilo-code.ghost.goToPreviousSuggestion", "title": "%ghost.commands.goToPreviousSuggestion%", "category": "%configuration.title%" + }, + { + "command": "kilo-code.addFileToContext", + "title": "%command.addFileToContext.title%", + "category": "%configuration.title%" } ], "menus": { + "explorer/context": [ + { + "command": "kilo-code.addFileToContext", + "group": "navigation@9" + } + ], "editor/context": [ { "submenu": "kilo-code.contextMenu", @@ -764,4 +775,4 @@ "vitest": "^3.2.3", "zod-to-ts": "^1.2.0" } -} +} \ No newline at end of file diff --git a/src/package.nls.json b/src/package.nls.json index f2d39854afb..de985671503 100644 --- a/src/package.nls.json +++ b/src/package.nls.json @@ -19,6 +19,7 @@ "command.fixCode.title": "Fix Code", "command.improveCode.title": "Improve Code", "command.addToContext.title": "Add To Context", + "command.addFileToContext.title": "Add File(s) to Context", "command.focusInput.title": "Focus Input Field", "command.setCustomStoragePath.title": "Set Custom Storage Path", "command.importSettings.title": "Import Settings", diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 2bbc4161fa2..ff73fdfe7aa 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -174,6 +174,7 @@ export interface ExtensionMessage { | "focusInput" | "switchTab" | "focusChatInput" // kilocode_change + | "addContextMentions" // kilocode_change: add mentions directly to pills bar | "toggleAutoApprove" invoke?: "newChat" | "sendMessage" | "primaryButtonClick" | "secondaryButtonClick" | "setChatBoxMessage" state?: ExtensionState diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 8eb7e4019f9..ef7ef615bad 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -875,6 +875,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction handleAddMention(m)); + } + break; + // kilocode_change end } break case "selectedImages": @@ -929,6 +938,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction