Compare commits

..
Author SHA1 Message Date
0xtoshii 6ebc3eb825 md 2025-04-30 14:41:49 -07:00
0xtoshii 5953a212f0 changeset 2025-04-30 14:33:56 -07:00
0xtoshii 50496e4fd5 change default 2025-04-30 14:32:48 -07:00
0xtoshii acc12df665 create clinerules dir 2025-04-30 12:56:16 -07:00
17 changed files with 215 additions and 4470 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
create clinerules folder if its currently a file and creating new rule
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
updated drag and drop text to say "drop" instead of "drag"
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Minor UX improvement to drag and drop ux
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
add checkpoints after more messages
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Introduce UI library for future UI development
-1
View File
@@ -13,7 +13,6 @@ permissions:
jobs:
codespell:
if: false
name: Check for spelling errors
runs-on: ubuntu-latest
+2 -2
View File
@@ -70,8 +70,8 @@ jobs:
run: npm run format
# Build the extension before running tests
- name: Build Tests and Extension
run: npm run pretest
- name: Build Extension
run: npm run compile
- name: Unit Tests
run: npm run test:unit
+1
View File
@@ -12,6 +12,7 @@
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "--disable-workspace-trust", "${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "${defaultBuildTask}",
"postDebugTask": "stop",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
@@ -221,7 +221,14 @@ export const createRuleFile = async (isGlobal: boolean, filename: string, cwd: s
filePath = path.join(globalClineRulesFilePath, filename)
} else {
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
const hasError = await ensureLocalClinerulesDirExists(cwd)
if (hasError === true) {
return { filePath: null, fileExists: false }
}
await fs.mkdir(localClineRulesFilePath, { recursive: true })
filePath = path.join(localClineRulesFilePath, filename)
}
+45 -73
View File
@@ -927,7 +927,6 @@ export class Task {
let responseImages: string[] | undefined
if (response === "messageResponse") {
await this.say("user_feedback", text, images)
await this.saveCheckpoint()
responseText = text
responseImages = images
}
@@ -1327,7 +1326,6 @@ export class Task {
if (userFeedback) {
await this.say("user_feedback", userFeedback.text, userFeedback.images)
await this.saveCheckpoint()
return [
true,
formatResponse.toolResult(
@@ -1765,7 +1763,6 @@ export class Task {
if (text || images?.length) {
pushAdditionalToolFeedback(text, images)
await this.say("user_feedback", text, images)
await this.saveCheckpoint()
}
this.didRejectTool = true // Prevent further tool uses in this message
return false
@@ -1774,7 +1771,6 @@ export class Task {
if (text || images?.length) {
pushAdditionalToolFeedback(text, images)
await this.say("user_feedback", text, images)
await this.saveCheckpoint()
}
return true
}
@@ -1849,7 +1845,7 @@ export class Task {
if (!accessAllowed) {
await this.say("clineignore_error", relPath)
pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath)))
await this.saveCheckpoint()
break
}
@@ -1905,7 +1901,6 @@ export class Task {
)
await this.diffViewProvider.revertChanges()
await this.diffViewProvider.reset()
await this.saveCheckpoint()
break
}
} else if (content) {
@@ -1963,28 +1958,28 @@ export class Task {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError(block.name, "path"))
await this.diffViewProvider.reset()
await this.saveCheckpoint()
break
}
if (block.name === "replace_in_file" && !diff) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("replace_in_file", "diff"))
await this.diffViewProvider.reset()
await this.saveCheckpoint()
break
}
if (block.name === "write_to_file" && !content) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("write_to_file", "content"))
await this.diffViewProvider.reset()
await this.saveCheckpoint()
break
}
if (block.name === "new_rule" && !content) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("new_rule", "content"))
await this.diffViewProvider.reset()
await this.saveCheckpoint()
break
}
@@ -2043,7 +2038,6 @@ export class Task {
if (text || images?.length) {
pushAdditionalToolFeedback(text, images)
await this.say("user_feedback", text, images)
await this.saveCheckpoint()
}
this.didRejectTool = true
didApprove = false
@@ -2053,14 +2047,12 @@ export class Task {
if (text || images?.length) {
pushAdditionalToolFeedback(text, images)
await this.say("user_feedback", text, images)
await this.saveCheckpoint()
}
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
}
if (!didApprove) {
await this.diffViewProvider.revertChanges()
await this.saveCheckpoint()
break
}
}
@@ -2121,7 +2113,7 @@ export class Task {
await handleError("writing file", error)
await this.diffViewProvider.revertChanges()
await this.diffViewProvider.reset()
await this.saveCheckpoint()
break
}
}
@@ -2150,7 +2142,7 @@ export class Task {
if (!relPath) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("read_file", "path"))
await this.saveCheckpoint()
break
}
@@ -2158,7 +2150,7 @@ export class Task {
if (!accessAllowed) {
await this.say("clineignore_error", relPath)
pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath)))
await this.saveCheckpoint()
break
}
@@ -2181,7 +2173,6 @@ export class Task {
this.removeLastPartialMessageIfExistsWithType("say", "tool")
const didApprove = await askApproval("tool", completeMessage)
if (!didApprove) {
await this.saveCheckpoint()
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
break
}
@@ -2194,12 +2185,12 @@ export class Task {
await this.fileContextTracker.trackFileContext(relPath, "read_tool")
pushToolResult(content)
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("reading file", error)
await this.saveCheckpoint()
break
}
}
@@ -2230,7 +2221,7 @@ export class Task {
if (!relDirPath) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("list_files", "path"))
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -2263,18 +2254,17 @@ export class Task {
const didApprove = await askApproval("tool", completeMessage)
if (!didApprove) {
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
await this.saveCheckpoint()
break
}
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
}
pushToolResult(result)
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("listing files", error)
await this.saveCheckpoint()
break
}
}
@@ -2303,7 +2293,7 @@ export class Task {
if (!relDirPath) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("list_code_definition_names", "path"))
await this.saveCheckpoint()
break
}
@@ -2333,18 +2323,17 @@ export class Task {
const didApprove = await askApproval("tool", completeMessage)
if (!didApprove) {
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
await this.saveCheckpoint()
break
}
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
}
pushToolResult(result)
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("parsing source code definitions", error)
await this.saveCheckpoint()
break
}
}
@@ -2377,13 +2366,13 @@ export class Task {
if (!relDirPath) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("search_files", "path"))
await this.saveCheckpoint()
break
}
if (!regex) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("search_files", "regex"))
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -2415,18 +2404,17 @@ export class Task {
const didApprove = await askApproval("tool", completeMessage)
if (!didApprove) {
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
await this.saveCheckpoint()
break
}
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
}
pushToolResult(results)
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("searching files", error)
await this.saveCheckpoint()
break
}
}
@@ -2442,7 +2430,6 @@ export class Task {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "action"))
await this.browserSession.closeBrowser()
await this.saveCheckpoint()
}
break
}
@@ -2486,7 +2473,7 @@ export class Task {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "url"))
await this.browserSession.closeBrowser()
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -2502,7 +2489,6 @@ export class Task {
this.removeLastPartialMessageIfExistsWithType("say", "browser_action_launch")
const didApprove = await askApproval("browser_action_launch", url)
if (!didApprove) {
await this.saveCheckpoint()
break
}
}
@@ -2528,7 +2514,7 @@ export class Task {
await this.sayAndCreateMissingParamError("browser_action", "coordinate"),
)
await this.browserSession.closeBrowser()
await this.saveCheckpoint()
break // can't be within an inner switch
}
}
@@ -2537,7 +2523,7 @@ export class Task {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "text"))
await this.browserSession.closeBrowser()
await this.saveCheckpoint()
break
}
}
@@ -2586,7 +2572,7 @@ export class Task {
browserActionResult.screenshot ? [browserActionResult.screenshot] : [],
),
)
await this.saveCheckpoint()
break
case "close":
pushToolResult(
@@ -2594,7 +2580,7 @@ export class Task {
`The browser has been closed. You may now proceed to using other tools.`,
),
)
await this.saveCheckpoint()
break
}
@@ -2603,7 +2589,7 @@ export class Task {
} catch (error) {
await this.browserSession.closeBrowser() // if any error occurs, the browser session is terminated
await handleError("executing browser action", error)
await this.saveCheckpoint()
break
}
}
@@ -2631,7 +2617,7 @@ export class Task {
if (!command) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("execute_command", "command"))
await this.saveCheckpoint()
break
}
if (!requiresApprovalRaw) {
@@ -2639,7 +2625,7 @@ export class Task {
pushToolResult(
await this.sayAndCreateMissingParamError("execute_command", "requires_approval"),
)
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -2655,7 +2641,7 @@ export class Task {
pushToolResult(
formatResponse.toolError(formatResponse.clineIgnoreError(ignoredFileAttemptedToAccess)),
)
await this.saveCheckpoint()
break
}
@@ -2687,7 +2673,6 @@ export class Task {
`${this.shouldAutoApproveTool(block.name) && requiresApprovalPerLLM ? COMMAND_REQ_APP_STRING : ""}`, // ugly hack until we refactor combineCommandSequences
)
if (!didApprove) {
await this.saveCheckpoint()
break
}
}
@@ -2723,7 +2708,7 @@ export class Task {
}
} catch (error) {
await handleError("executing command", error)
await this.saveCheckpoint()
break
}
}
@@ -2753,13 +2738,13 @@ export class Task {
if (!server_name) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("use_mcp_tool", "server_name"))
await this.saveCheckpoint()
break
}
if (!tool_name) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("use_mcp_tool", "tool_name"))
await this.saveCheckpoint()
break
}
// arguments are optional, but if they are provided they must be valid JSON
@@ -2783,7 +2768,7 @@ export class Task {
formatResponse.invalidMcpToolArgumentError(server_name, tool_name),
),
)
await this.saveCheckpoint()
break
}
}
@@ -2810,7 +2795,6 @@ export class Task {
this.removeLastPartialMessageIfExistsWithType("say", "use_mcp_server")
const didApprove = await askApproval("use_mcp_server", completeMessage)
if (!didApprove) {
await this.saveCheckpoint()
break
}
}
@@ -2862,7 +2846,7 @@ export class Task {
}
} catch (error) {
await handleError("executing MCP tool", error)
await this.saveCheckpoint()
break
}
}
@@ -2890,13 +2874,13 @@ export class Task {
if (!server_name) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("access_mcp_resource", "server_name"))
await this.saveCheckpoint()
break
}
if (!uri) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("access_mcp_resource", "uri"))
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -2917,7 +2901,6 @@ export class Task {
this.removeLastPartialMessageIfExistsWithType("say", "use_mcp_server")
const didApprove = await askApproval("use_mcp_server", completeMessage)
if (!didApprove) {
await this.saveCheckpoint()
break
}
}
@@ -2937,12 +2920,12 @@ export class Task {
.join("\n\n") || "(Empty response)"
await this.say("mcp_server_response", resourceResultPretty)
pushToolResult(formatResponse.toolResult(resourceResultPretty))
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("accessing MCP resource", error)
await this.saveCheckpoint()
break
}
}
@@ -2961,7 +2944,7 @@ export class Task {
if (!question) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("ask_followup_question", "question"))
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -2998,12 +2981,12 @@ export class Task {
}
pushToolResult(formatResponse.toolResult(`<answer>\n${text}\n</answer>`, images))
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("asking question", error)
await this.saveCheckpoint()
break
}
}
@@ -3017,7 +3000,6 @@ export class Task {
if (!context) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("new_task", "context"))
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -3046,12 +3028,10 @@ export class Task {
formatResponse.toolResult(`The user has created a new task with the provided context.`),
)
}
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("creating new task", error)
await this.saveCheckpoint()
break
}
}
@@ -3065,7 +3045,6 @@ export class Task {
if (!context) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("condense", "context"))
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -3108,12 +3087,10 @@ export class Task {
await ensureTaskDirectoryExists(this.getContext(), this.taskId),
)
}
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("condensing context window", error)
await this.saveCheckpoint()
break
}
}
@@ -3174,7 +3151,6 @@ export class Task {
if (text || images?.length) {
telemetryService.captureOptionsIgnored(this.taskId, options.length, "plan")
await this.say("user_feedback", text ?? "", images)
await this.saveCheckpoint()
}
}
@@ -3321,14 +3297,12 @@ export class Task {
// complete command message
const didApprove = await askApproval("command", command)
if (!didApprove) {
await this.saveCheckpoint()
break
}
const [userRejected, execCommandResult] = await this.executeCommandTool(command!)
if (userRejected) {
this.didRejectTool = true
pushToolResult(execCommandResult)
await this.saveCheckpoint()
break
}
// user didn't reject, but the command may have output
@@ -3347,7 +3321,6 @@ export class Task {
break
}
await this.say("user_feedback", text ?? "", images)
await this.saveCheckpoint()
const toolResults: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = []
if (commandResult) {
@@ -3376,7 +3349,7 @@ export class Task {
}
} catch (error) {
await handleError("attempting completion", error)
await this.saveCheckpoint()
break
}
}
@@ -3478,6 +3451,9 @@ export class Task {
// Save checkpoint if this is the first API request
const isFirstRequest = this.clineMessages.filter((m) => m.say === "api_req_started").length === 0
if (isFirstRequest) {
await this.say("checkpoint_created") // no hash since we need to wait for CheckpointTracker to be initialized
}
// getting verbose details is an expensive operation, it uses globby to top-down build file structure of project which for large projects can take a few seconds
// for the best UX we show a placeholder api_req_started message with a loading spinner as this happens
@@ -3488,10 +3464,6 @@ export class Task {
}),
)
if (isFirstRequest) {
await this.say("checkpoint_created") // no hash since we need to wait for CheckpointTracker to be initialized
}
// use this opportunity to initialize the checkpoint tracker (can be expensive to initialize in the constructor)
// FIXME: right now we're letting users init checkpoints for old tasks, but this could be a problem if opening a task in the wrong workspace
// isNewTask &&
+65 -4047
View File
File diff suppressed because it is too large Load Diff
+2 -4
View File
@@ -15,13 +15,11 @@
},
"dependencies": {
"@floating-ui/react": "^0.27.4",
"@heroui/react": "^2.8.0-beta.2",
"@vscode/webview-ui-toolkit": "^1.4.0",
"debounce": "^2.1.1",
"dompurify": "^3.2.4",
"fast-deep-equal": "^3.1.3",
"firebase": "^11.3.0",
"framer-motion": "^12.7.4",
"fuse.js": "^7.0.0",
"fzf": "^0.5.2",
"mermaid": "^11.4.1",
@@ -44,7 +42,7 @@
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@tailwindcss/vite": "^4.1.4",
"@tailwindcss/vite": "^4.0.12",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
@@ -62,7 +60,7 @@
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"jsdom": "^26.0.0",
"tailwindcss": "^4.1.4",
"tailwindcss": "^4.0.12",
"typescript": "^5.7.3",
"typescript-eslint": "^8.18.2",
"vite": "^6.2.6",
+7 -5
View File
@@ -6,10 +6,10 @@ import HistoryView from "./components/history/HistoryView"
import SettingsView from "./components/settings/SettingsView"
import WelcomeView from "./components/welcome/WelcomeView"
import AccountView from "./components/account/AccountView"
import { useExtensionState } from "./context/ExtensionStateContext"
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
import { FirebaseAuthProvider } from "./context/FirebaseAuthContext"
import { vscode } from "./utils/vscode"
import McpView from "./components/mcp/configuration/McpConfigurationView"
import { Providers } from "./Providers"
const AppContent = () => {
const { didHydrateState, showWelcome, shouldShowAnnouncement, showMcp, mcpTab } = useExtensionState()
@@ -126,9 +126,11 @@ const AppContent = () => {
const App = () => {
return (
<Providers>
<AppContent />
</Providers>
<ExtensionStateContextProvider>
<FirebaseAuthProvider>
<AppContent />
</FirebaseAuthProvider>
</ExtensionStateContextProvider>
)
}
-15
View File
@@ -1,15 +0,0 @@
import type { ReactNode } from "react"
import { ExtensionStateContextProvider } from "./context/ExtensionStateContext"
import { FirebaseAuthProvider } from "./context/FirebaseAuthContext"
import { HeroUIProvider } from "@heroui/react"
export function Providers({ children }: { children: ReactNode }) {
return (
<ExtensionStateContextProvider>
<FirebaseAuthProvider>
<HeroUIProvider>{children}</HeroUIProvider>
</FirebaseAuthProvider>
</ExtensionStateContextProvider>
)
}
+81 -279
View File
@@ -237,7 +237,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
) => {
const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform } = useExtensionState()
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
const [isDraggingOver, setIsDraggingOver] = useState(false)
const [gitCommits, setGitCommits] = useState<GitCommit[]>([])
const [showSlashCommandsMenu, setShowSlashCommandsMenu] = useState(false)
@@ -267,10 +266,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const [menuPosition, setMenuPosition] = useState(0)
const [shownTooltipMode, setShownTooltipMode] = useState<ChatSettings["mode"] | null>(null)
const [pendingInsertions, setPendingInsertions] = useState<string[]>([])
const [showShiftDragTip, setShowShiftDragTip] = useState(false)
const shiftHoldTimerRef = useRef<NodeJS.Timeout | null>(null)
const [showUnsupportedFileError, setShowUnsupportedFileError] = useState(false)
const unsupportedFileTimerRef = useRef<NodeJS.Timeout | null>(null)
const [fileSearchResults, setFileSearchResults] = useState<SearchResult[]>([])
const [searchLoading, setSearchLoading] = useState(false)
@@ -1021,84 +1016,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}
}, [showModelSelector])
// Effect for Shift key hold detection
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Shift" && !event.repeat) {
// Start timer only if Shift is pressed and not already held down
if (shiftHoldTimerRef.current === null) {
shiftHoldTimerRef.current = setTimeout(() => {
setShowShiftDragTip(true)
}, 250) // 250ms delay
}
}
}
const handleKeyUp = (event: KeyboardEvent) => {
if (event.key === "Shift") {
// Clear timer and hide tip when Shift is released
if (shiftHoldTimerRef.current !== null) {
clearTimeout(shiftHoldTimerRef.current)
shiftHoldTimerRef.current = null
}
setShowShiftDragTip(false)
}
}
// Add listeners
window.addEventListener("keydown", handleKeyDown)
window.addEventListener("keyup", handleKeyUp)
// Cleanup listeners on component unmount
return () => {
window.removeEventListener("keydown", handleKeyDown)
window.removeEventListener("keyup", handleKeyUp)
// Clear any running timer on unmount
if (shiftHoldTimerRef.current !== null) {
clearTimeout(shiftHoldTimerRef.current)
}
}
}, []) // Empty dependency array ensures this runs only once on mount/unmount
// Function to show error message for unsupported files for drag and drop
const showUnsupportedFileErrorMessage = () => {
// Show error message for unsupported files
setShowUnsupportedFileError(true)
// Clear any existing timer
if (unsupportedFileTimerRef.current) {
clearTimeout(unsupportedFileTimerRef.current)
}
// Set timer to hide error after 3 seconds
unsupportedFileTimerRef.current = setTimeout(() => {
setShowUnsupportedFileError(false)
unsupportedFileTimerRef.current = null
}, 3000)
}
const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault()
setIsDraggingOver(true)
// Check if files are being dragged
if (e.dataTransfer.types.includes("Files")) {
// Check if any of the files are not images
const items = Array.from(e.dataTransfer.items)
const hasNonImageFile = items.some((item) => {
if (item.kind === "file") {
const type = item.type.split("/")[0]
return type !== "image"
}
return false
})
if (hasNonImageFile) {
showUnsupportedFileErrorMessage()
}
}
}
/**
* Handles the drag over event to allow dropping.
* Prevents the default behavior to enable drop.
@@ -1107,37 +1024,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
*/
const onDragOver = (e: React.DragEvent) => {
e.preventDefault()
// Ensure state remains true if dragging continues over the element
if (!isDraggingOver) {
setIsDraggingOver(true)
}
}
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault()
// Check if the related target is still within the drop zone; prevents flickering
const dropZone = e.currentTarget as HTMLElement
if (!dropZone.contains(e.relatedTarget as Node)) {
setIsDraggingOver(false)
// Don't clear the error message here, let it time out naturally
}
}
// Effect to detect when drag operation ends outside the component
useEffect(() => {
const handleGlobalDragEnd = () => {
// This will be triggered when the drag operation ends anywhere
setIsDraggingOver(false)
// Don't clear error message, let it time out naturally
}
document.addEventListener("dragend", handleGlobalDragEnd)
return () => {
document.removeEventListener("dragend", handleGlobalDragEnd)
}
}, [])
/**
* Handles the drop event for files and text.
* Processes dropped images and text, updating the state accordingly.
@@ -1146,14 +1034,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
*/
const onDrop = async (e: React.DragEvent) => {
e.preventDefault()
setIsDraggingOver(false) // Reset state on drop
// Clear any error message when something is actually dropped
setShowUnsupportedFileError(false)
if (unsupportedFileTimerRef.current) {
clearTimeout(unsupportedFileTimerRef.current)
unsupportedFileTimerRef.current = null
}
// --- 1. VSCode Explorer Drop Handling ---
let uris: string[] = []
@@ -1273,37 +1153,9 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
opacity: textAreaDisabled ? 0.5 : 1,
position: "relative",
display: "flex",
// Drag-over styles moved to DynamicTextArea
transition: "background-color 0.1s ease-in-out, border 0.1s ease-in-out",
}}
onDrop={onDrop}
onDragOver={onDragOver}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}>
{showUnsupportedFileError && (
<div
style={{
position: "absolute",
inset: "10px 15px",
backgroundColor: "rgba(var(--vscode-errorForeground-rgb), 0.1)",
border: "2px solid var(--vscode-errorForeground)",
borderRadius: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 10,
pointerEvents: "none",
}}>
<span
style={{
color: "var(--vscode-errorForeground)",
fontWeight: "bold",
fontSize: "12px",
}}>
Only image files are supported
</span>
</div>
)}
onDragOver={onDragOver}>
{showSlashCommandsMenu && (
<div ref={slashCommandsMenuContainerRef}>
<SlashCommandMenu
@@ -1398,7 +1250,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}
onHeightChange?.(height)
}}
placeholder={showUnsupportedFileError ? "" : placeholderText}
placeholder={placeholderText}
maxRows={10}
autoFocus={true}
style={{
@@ -1430,13 +1282,9 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
cursor: textAreaDisabled ? "not-allowed" : undefined,
flex: 1,
zIndex: 1,
outline:
isDraggingOver && !showUnsupportedFileError // Only show drag outline if not showing error
? "2px dashed var(--vscode-focusBorder)"
: isTextAreaFocused
? `1px solid ${chatSettings.mode === "plan" ? PLAN_MODE_COLOR : "var(--vscode-focusBorder)"}`
: "none",
outlineOffset: isDraggingOver && !showUnsupportedFileError ? "1px" : "0px", // Add offset for drag-over outline
outline: isTextAreaFocused
? `1px solid ${chatSettings.mode === "plan" ? PLAN_MODE_COLOR : "var(--vscode-focusBorder)"}`
: "none",
}}
onScroll={() => updateHighlights()}
/>
@@ -1498,129 +1346,83 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
</div>
<ControlsContainer>
{/* Always render both components, but control visibility with CSS */}
<div
style={{
position: "relative",
flex: 1,
minWidth: 0,
height: "28px", // Fixed height to prevent container shrinking
}}>
{/* ButtonGroup - always in DOM but visibility controlled */}
<ButtonGroup
style={{
opacity: showShiftDragTip ? 0 : 1,
pointerEvents: showShiftDragTip ? "none" : "auto",
position: "absolute",
top: 0,
left: 0,
right: 0,
transition: "opacity 0.3s ease-in-out",
transitionDelay: showShiftDragTip ? "0s" : "0.2s",
width: "100%",
height: "100%",
zIndex: showShiftDragTip ? 0 : 1,
}}>
<Tooltip tipText="Add Context" style={{ left: 0 }}>
<VSCodeButton
data-testid="context-button"
appearance="icon"
aria-label="Add Context"
disabled={textAreaDisabled}
onClick={handleContextButtonClick}
style={{ padding: "0px 0px", height: "20px" }}>
<ButtonContainer>
<span className="flex items-center" style={{ fontSize: "13px", marginBottom: 1 }}>
@
</span>
</ButtonContainer>
</VSCodeButton>
</Tooltip>
<ButtonGroup>
<Tooltip tipText="Add Context" style={{ left: 0 }}>
<VSCodeButton
data-testid="context-button"
appearance="icon"
aria-label="Add Context"
disabled={textAreaDisabled}
onClick={handleContextButtonClick}
style={{ padding: "0px 0px", height: "20px" }}>
<ButtonContainer>
<span className="flex items-center" style={{ fontSize: "13px", marginBottom: 1 }}>
@
</span>
{/* {showButtonText && <span style={{ fontSize: "10px" }}>Context</span>} */}
</ButtonContainer>
</VSCodeButton>
</Tooltip>
<Tooltip tipText="Add Images">
<VSCodeButton
data-testid="images-button"
appearance="icon"
aria-label="Add Images"
disabled={shouldDisableImages}
onClick={() => {
if (!shouldDisableImages) {
onSelectImages()
}
}}
style={{ padding: "0px 0px", height: "20px" }}>
<ButtonContainer>
<span
className="codicon codicon-device-camera flex items-center"
style={{ fontSize: "14px", marginBottom: -3 }}
/>
</ButtonContainer>
</VSCodeButton>
</Tooltip>
<ServersToggleModal />
<ClineRulesToggleModal />
<ModelContainer ref={modelSelectorRef}>
<ModelButtonWrapper ref={buttonRef}>
<ModelDisplayButton
role="button"
isActive={showModelSelector}
disabled={false}
title="Select Model / API Provider"
onClick={handleModelButtonClick}
tabIndex={0}>
<ModelButtonContent>{modelDisplayName}</ModelButtonContent>
</ModelDisplayButton>
</ModelButtonWrapper>
{showModelSelector && (
<ModelSelectorTooltip
arrowPosition={arrowPosition}
menuPosition={menuPosition}
style={{
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
}}>
<ApiOptions
showModelOptions={true}
apiErrorMessage={undefined}
modelIdErrorMessage={undefined}
isPopup={true}
saveImmediately={true} // Ensure popup saves immediately
/>
</ModelSelectorTooltip>
)}
</ModelContainer>
</ButtonGroup>
{/* Shift Tip - always in DOM but visibility controlled */}
<div
style={{
display: "flex",
justifyContent: "flex-start", // Left align horizontally
alignItems: "center", // Center vertically
height: "100%", // Fill the container height
padding: "4px 0", // Add padding to match button group height
boxSizing: "border-box", // Include padding in height calculation
opacity: showShiftDragTip ? 1 : 0,
pointerEvents: showShiftDragTip ? "auto" : "none",
position: "absolute",
top: 0,
left: 0,
right: 0,
transition: "opacity 0.3s ease-in-out",
transitionDelay: showShiftDragTip ? "0.2s" : "0s",
width: "100%",
zIndex: showShiftDragTip ? 1 : 0,
}}>
<span
style={{
fontSize: "10px",
color: "var(--vscode-descriptionForeground)",
whiteSpace: "nowrap",
}}>
Hold Shift to Drop Files
</span>
</div>
</div>
{/* Tooltip for Plan/Act toggle remains outside the conditional rendering */}
<Tooltip tipText="Add Images">
<VSCodeButton
data-testid="images-button"
appearance="icon"
aria-label="Add Images"
disabled={shouldDisableImages}
onClick={() => {
if (!shouldDisableImages) {
onSelectImages()
}
}}
style={{ padding: "0px 0px", height: "20px" }}>
<ButtonContainer>
<span
className="codicon codicon-device-camera flex items-center"
style={{ fontSize: "14px", marginBottom: -3 }}
/>
{/* {showButtonText && <span style={{ fontSize: "10px" }}>Images</span>} */}
</ButtonContainer>
</VSCodeButton>
</Tooltip>
<ServersToggleModal />
<ClineRulesToggleModal />
<ModelContainer ref={modelSelectorRef}>
<ModelButtonWrapper ref={buttonRef}>
<ModelDisplayButton
role="button"
isActive={showModelSelector}
disabled={false}
title="Select Model / API Provider"
onClick={handleModelButtonClick}
// onKeyDown={(e) => {
// if (e.key === "Enter" || e.key === " ") {
// e.preventDefault()
// handleModelButtonClick()
// }
// }}
tabIndex={0}>
<ModelButtonContent>{modelDisplayName}</ModelButtonContent>
</ModelDisplayButton>
</ModelButtonWrapper>
{showModelSelector && (
<ModelSelectorTooltip
arrowPosition={arrowPosition}
menuPosition={menuPosition}
style={{
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
}}>
<ApiOptions
showModelOptions={true}
apiErrorMessage={undefined}
modelIdErrorMessage={undefined}
isPopup={true}
saveImmediately={true} // Ensure popup saves immediately
/>
</ModelSelectorTooltip>
)}
</ModelContainer>
</ButtonGroup>
<Tooltip
style={{ zIndex: 1000 }}
visible={shownTooltipMode !== null}
-2
View File
@@ -4,8 +4,6 @@
/* @import "tailwindcss/preflight.css" layer(base); */
@import "tailwindcss/utilities.css" layer(utilities);
@config "../tailwind.config.js";
textarea:focus {
outline: 1.5px solid var(--vscode-focusBorder, #007fd4);
}
-22
View File
@@ -1,22 +0,0 @@
const { heroui } = require("@heroui/react")
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}", "./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
darkMode: "class",
plugins: [
heroui({
defaultTheme: "vscode",
themes: {
vscode: {
colors: {
background: "",
},
},
},
}),
],
}