diff --git a/.vscode/launch.json b/.vscode/launch.json index c4227314a16..dbcac057fed 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -18,7 +18,7 @@ ], "pauseForSourceMap": false, "outFiles": ["${workspaceFolder}/extensions/vscode/out/extension.js"], - "preLaunchTask": "vscode-extension:build-without-watch", + "preLaunchTask": "vscode-extension:build-with-packages", "env": { // "CONTROL_PLANE_ENV": "local", "CONTINUE_GLOBAL_DIR": "${workspaceFolder}/extensions/.continue-debug" diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ad157b57452..9b34de0f1a6 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -174,25 +174,6 @@ } ] }, - { - "label": "gui:build", - "type": "shell", - "command": "npm", - "options": { - "cwd": "${workspaceFolder}/gui", - "env": { - "NODE_OPTIONS": "--max-old-space-size=4096" - } - }, - "args": ["run", "build"], - "problemMatcher": ["$tsc"], - "presentation": { - "group": "build-tasks", - "panel": "shared", - "reveal": "silent", - "close": true - } - }, { "label": "binary:esbuild", "type": "shell", diff --git a/core/protocol/ide.ts b/core/protocol/ide.ts index 60a8985e9a9..d8b86ff960a 100644 --- a/core/protocol/ide.ts +++ b/core/protocol/ide.ts @@ -51,7 +51,6 @@ export type ToIdeFromWebviewOrCoreProtocol = { getPinnedFiles: [undefined, string[]]; showLines: [{ filepath: string; startLine: number; endLine: number }, void]; readRangeInFile: [{ filepath: string; range: Range }, string]; - readFileAsDataUrl: [{ filepath: string }, string]; getDiff: [{ includeUnstaged: boolean }, string[]]; getTerminalContents: [undefined, string]; getDebugLocals: [{ threadIndex: number }, string]; diff --git a/extensions/vscode/src/extension/VsCodeMessenger.ts b/extensions/vscode/src/extension/VsCodeMessenger.ts index acac0b3f623..3a1d281b59b 100644 --- a/extensions/vscode/src/extension/VsCodeMessenger.ts +++ b/extensions/vscode/src/extension/VsCodeMessenger.ts @@ -295,18 +295,6 @@ export class VsCodeMessenger { }); }); - this.onWebviewOrCore("readFileAsDataUrl", async (msg) => { - const { filepath } = msg.data; - const fileUri = vscode.Uri.file(filepath); - const fileContents = await vscode.workspace.fs.readFile(fileUri); - const fileType = - filepath.split(".").pop() === "png" ? "image/png" : "image/jpeg"; - const dataUrl = `data:${fileType};base64,${Buffer.from( - fileContents, - ).toString("base64")}`; - return dataUrl; - }); - this.onWebviewOrCore("getIdeSettings", async (msg) => { return ide.getIdeSettings(); }); diff --git a/gui/src/components/mainInput/TipTapEditor/TipTapEditor.tsx b/gui/src/components/mainInput/TipTapEditor/TipTapEditor.tsx index 6df190ef772..87c8384936d 100644 --- a/gui/src/components/mainInput/TipTapEditor/TipTapEditor.tsx +++ b/gui/src/components/mainInput/TipTapEditor/TipTapEditor.tsx @@ -57,13 +57,10 @@ function TipTapEditorInner(props: TipTapEditorProps) { const historyLength = useAppSelector((store) => store.session.history.length); const isInEdit = useAppSelector((store) => store.session.isInEdit); - const [showDragOverMsg, setShowDragOverMsg] = useState(false); - const { editor, onEnterRef } = createEditorConfig({ props, ideMessenger, dispatch, - setShowDragOverMsg, }); // Register the main editor with the provider @@ -140,6 +137,8 @@ function TipTapEditorInner(props: TipTapEditorProps) { } }, [isStreaming, props.isMainInput]); + const [showDragOverMsg, setShowDragOverMsg] = useState(false); + const [activeKey, setActiveKey] = useState(null); const insertCharacterWithWhitespace = useCallback( @@ -222,23 +221,40 @@ function TipTapEditorInner(props: TipTapEditorProps) { if (e.shiftKey) { setShowDragOverMsg(false); } else { - setTimeout(() => { - setShowDragOverMsg(false); - }, 2000); + setTimeout(() => setShowDragOverMsg(false), 2000); } } - setShowDragOverMsg(false); }} onDragEnter={() => { setShowDragOverMsg(true); }} - onDragEnd={() => { - setShowDragOverMsg(false); - }} onDrop={(event) => { - // Just hide the drag overlay - ProseMirror handles the actual drop setShowDragOverMsg(false); - // Let the event bubble to ProseMirror by not preventing default + if ( + !defaultModel || + !modelSupportsImages( + defaultModel.provider, + defaultModel.model, + defaultModel.title, + defaultModel.capabilities, + ) + ) { + return; + } + let file = event.dataTransfer.files[0]; + void handleImageFile(ideMessenger, file).then((result) => { + if (!editor) { + return; + } + if (result) { + const [_, dataUrl] = result; + const { schema } = editor.state; + const node = schema.nodes.image.create({ src: dataUrl }); + const tr = editor.state.tr.insert(0, node); + editor.view.dispatch(tr); + } + }); + event.preventDefault(); }} >
@@ -283,7 +299,9 @@ function TipTapEditorInner(props: TipTapEditorProps) { defaultModel?.model || "", defaultModel?.title, defaultModel?.capabilities, - ) && } + ) && ( + + )}
); diff --git a/gui/src/components/mainInput/TipTapEditor/components/DragOverlay.tsx b/gui/src/components/mainInput/TipTapEditor/components/DragOverlay.tsx index b3e935ea4ae..1417a822817 100644 --- a/gui/src/components/mainInput/TipTapEditor/components/DragOverlay.tsx +++ b/gui/src/components/mainInput/TipTapEditor/components/DragOverlay.tsx @@ -1,11 +1,34 @@ -import React from "react"; +import React, { useEffect } from "react"; import { HoverDiv, HoverTextDiv } from "./StyledComponents"; interface DragOverlayProps { show: boolean; + setShow: (show: boolean) => void; } -export const DragOverlay: React.FC = ({ show }) => { +export const DragOverlay: React.FC = ({ show, setShow }) => { + useEffect(() => { + const overListener = (event: DragEvent) => { + if (event.shiftKey) return; + setShow(true); + }; + window.addEventListener("dragover", overListener); + + const leaveListener = (event: DragEvent) => { + if (event.shiftKey) { + setShow(false); + } else { + setTimeout(() => setShow(false), 2000); + } + }; + window.addEventListener("dragleave", leaveListener); + + return () => { + window.removeEventListener("dragover", overListener); + window.removeEventListener("dragleave", leaveListener); + }; + }, []); + if (!show) return null; return ( diff --git a/gui/src/components/mainInput/TipTapEditor/components/StyledComponents.ts b/gui/src/components/mainInput/TipTapEditor/components/StyledComponents.ts index b47472ec8e9..3889a7ef99a 100644 --- a/gui/src/components/mainInput/TipTapEditor/components/StyledComponents.ts +++ b/gui/src/components/mainInput/TipTapEditor/components/StyledComponents.ts @@ -56,7 +56,6 @@ export const HoverDiv = styled.div` display: flex; align-items: center; justify-content: center; - pointer-events: none; `; export const HoverTextDiv = styled.div` @@ -69,5 +68,4 @@ export const HoverTextDiv = styled.div` display: flex; align-items: center; justify-content: center; - pointer-events: none; `; diff --git a/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts b/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts index a166ff33928..20ad8ba5179 100644 --- a/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts +++ b/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts @@ -27,7 +27,7 @@ import { getContextProviderDropdownOptions, getSlashCommandDropdownOptions, } from "./getSuggestion"; -import { handleImageFile, handleVSCodeResourceFromHtml } from "./imageUtils"; +import { handleImageFile } from "./imageUtils"; export function getPlaceholderText( placeholder: TipTapEditorProps["placeholder"], @@ -69,9 +69,8 @@ export function createEditorConfig(options: { props: TipTapEditorProps; ideMessenger: IIdeMessenger; dispatch: AppDispatch; - setShowDragOverMsg: (show: boolean) => void; }) { - const { props, ideMessenger, dispatch, setShowDragOverMsg } = options; + const { props, ideMessenger, dispatch } = options; const posthog = usePostHog(); @@ -148,80 +147,6 @@ export function createEditorConfig(options: { const plugin = new Plugin({ props: { handleDOMEvents: { - drop(view, event) { - // Hide drag overlay immediately when drop is handled - setShowDragOverMsg(false); - - // Get current model and check if it supports images - const model = defaultModelRef.current; - if ( - !model || - !modelSupportsImages( - model.provider, - model.model, - model.title, - model.capabilities, - ) - ) { - event.preventDefault(); - event.stopPropagation(); - return true; - } - - event.preventDefault(); - event.stopPropagation(); - - // Check if dataTransfer exists - if (!event.dataTransfer) { - return true; - } - - // Handle file drop first - if (event.dataTransfer.files.length > 0) { - const file = event.dataTransfer.files[0]; - void handleImageFile(ideMessenger, file).then((result) => { - if (result) { - const [_, dataUrl] = result; - const { schema } = view.state; - const node = schema.nodes.image.create({ - src: dataUrl, - }); - const tr = view.state.tr.insert(0, node); - view.dispatch(tr); - } - }); - return true; - } - - // Handle drop of HTML content (including VS Code resource URLs) - const html = event.dataTransfer.getData("text/html"); - if (html) { - void handleVSCodeResourceFromHtml(ideMessenger, html) - .then((dataUrl) => { - if (dataUrl) { - const { schema } = view.state; - const node = schema.nodes.image.create({ - src: dataUrl, - }); - const tr = view.state.tr.insert(0, node); - view.dispatch(tr); - } - }) - .catch((err) => - console.error( - "Failed to handle VS Code resource:", - err, - ), - ); - } - - return true; - }, - dragover(view, event) { - // Allow dragover for proper drop handling - event.preventDefault(); - return true; - }, paste(view, event) { const model = defaultModelRef.current; if (!model) return; diff --git a/gui/src/components/mainInput/TipTapEditor/utils/imageUtils.ts b/gui/src/components/mainInput/TipTapEditor/utils/imageUtils.ts index 027a444f37e..23de2dda130 100644 --- a/gui/src/components/mainInput/TipTapEditor/utils/imageUtils.ts +++ b/gui/src/components/mainInput/TipTapEditor/utils/imageUtils.ts @@ -2,159 +2,6 @@ import { IIdeMessenger } from "../../../../context/IdeMessenger"; const IMAGE_RESOLUTION = 1024; -/** - * Extracts the file path from a VS Code resource URL - * Example: "https://file+.vscode-resource.vscode-cdn.net/Users/path/to/file.jpg?version=123" - * Returns: "/Users/path/to/file.jpg" - */ -export function extractFilePathFromVSCodeResourceUrl( - url: string, -): string | null { - try { - const urlObj = new URL(url); - if (urlObj.hostname === "file+.vscode-resource.vscode-cdn.net") { - return decodeURIComponent(urlObj.pathname); - } - return null; - } catch (error) { - console.error("Error parsing VS Code resource URL:", error); - return null; - } -} - -/** - * Handles VS Code resource URLs by converting them to data URLs - * @param ideMessenger - The IDE messenger to communicate with VS Code - * @param vscodeResourceUrl - The VS Code resource URL - * @returns Promise with the data URL if successful, undefined otherwise - */ -export async function handleVSCodeResourceUrl( - ideMessenger: IIdeMessenger, - vscodeResourceUrl: string, -): Promise { - const filepath = extractFilePathFromVSCodeResourceUrl(vscodeResourceUrl); - if (!filepath) { - console.error( - "Could not extract file path from VS Code resource URL:", - vscodeResourceUrl, - ); - return undefined; - } - - console.log("Extracted filepath:", filepath); - - try { - console.log("Requesting readFileAsDataUrl for filepath:", filepath); - console.log("About to call ideMessenger.request..."); - - // Add a timeout wrapper to prevent hanging - const timeoutPromise = new Promise((_, reject) => - setTimeout( - () => reject(new Error("Request timeout after 10 seconds")), - 10000, - ), - ); - - const requestPromise = ideMessenger.request("readFileAsDataUrl", { - filepath, - }); - - console.log("Request promise created, waiting for response..."); - - const response = await Promise.race([requestPromise, timeoutPromise]); - - console.log("Got response from ideMessenger.request:", response); - console.log("Response type:", typeof response); - - // The response should be a WebviewSingleMessage which has status and content - if (response && typeof response === "object" && "status" in response) { - const typedResponse = response as { - status: string; - error?: string; - content?: string; - }; - if (typedResponse.status === "error") { - console.error("Error reading file as data URL:", typedResponse.error); - return undefined; - } - - if (typedResponse.status === "success" && typedResponse.content) { - const dataUrl = typedResponse.content; - console.log( - "Successfully got data URL for file, content length:", - dataUrl.length, - ); - return dataUrl; - } - } - - // If response is directly a string (shouldn't happen based on protocol but just in case) - if (typeof response === "string") { - console.log("Got direct string response, length:", response.length); - return response; - } - - console.error("Unexpected response format:", response); - return undefined; - } catch (error) { - console.error("Exception caught when reading file as data URL:", error); - console.error( - "Error stack:", - error instanceof Error ? error.stack : "No stack trace", - ); - return undefined; - } -} - -/** - * Extracts VS Code resource URL from HTML content - * @param html - HTML string that may contain VS Code resource URLs - * @returns The VS Code resource URL if found, null otherwise - */ -export function extractVSCodeResourceUrlFromHtml(html: string): string | null { - try { - // Create a temporary DOM element to parse the HTML - const tempDiv = document.createElement("div"); - tempDiv.innerHTML = html; - - // Look for img tags with VS Code resource URLs - const imgTags = tempDiv.querySelectorAll("img"); - for (const img of imgTags) { - const src = img.getAttribute("src"); - if (src && src.includes("file+.vscode-resource.vscode-cdn.net")) { - return src; - } - } - - return null; - } catch (error) { - console.error("Error parsing HTML for VS Code resource URL:", error); - return null; - } -} - -/** - * Handles HTML content that contains VS Code resource URLs and converts them to usable data URLs - * @param ideMessenger - The IDE messenger to communicate with VS Code - * @param html - HTML string containing VS Code resource URLs - * @returns Promise with the data URL if successful, undefined otherwise - */ -export async function handleVSCodeResourceFromHtml( - ideMessenger: IIdeMessenger, - html: string, -): Promise { - console.log("Processing HTML for VS Code resource URL:", html); - - const vscodeResourceUrl = extractVSCodeResourceUrlFromHtml(html); - if (!vscodeResourceUrl) { - console.log("No VS Code resource URL found in HTML"); - return undefined; - } - - console.log("Found VS Code resource URL:", vscodeResourceUrl); - return await handleVSCodeResourceUrl(ideMessenger, vscodeResourceUrl); -} - export function getDataUrlForFile( file: File, img: HTMLImageElement,