Skip to content
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
],
"pauseForSourceMap": false,
"outFiles": ["${workspaceFolder}/extensions/vscode/out/extension.js"],
"preLaunchTask": "vscode-extension:build-with-packages",
"preLaunchTask": "vscode-extension:build-without-watch",
"env": {
// "CONTROL_PLANE_ENV": "local",
"CONTINUE_GLOBAL_DIR": "${workspaceFolder}/extensions/.continue-debug"
Expand Down
19 changes: 19 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,25 @@
}
]
},
{
"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",
Expand Down
1 change: 1 addition & 0 deletions core/protocol/ide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ 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];
Expand Down
12 changes: 12 additions & 0 deletions extensions/vscode/src/extension/VsCodeMessenger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,18 @@ 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();
});
Expand Down
44 changes: 13 additions & 31 deletions gui/src/components/mainInput/TipTapEditor/TipTapEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,13 @@ 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
Expand Down Expand Up @@ -137,8 +140,6 @@ function TipTapEditorInner(props: TipTapEditorProps) {
}
}, [isStreaming, props.isMainInput]);

const [showDragOverMsg, setShowDragOverMsg] = useState(false);

const [activeKey, setActiveKey] = useState<string | null>(null);

const insertCharacterWithWhitespace = useCallback(
Expand Down Expand Up @@ -221,40 +222,23 @@ 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);
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();
// Let the event bubble to ProseMirror by not preventing default
}}
>
<div className="px-2.5 pb-1 pt-2">
Expand Down Expand Up @@ -299,9 +283,7 @@ function TipTapEditorInner(props: TipTapEditorProps) {
defaultModel?.model || "",
defaultModel?.title,
defaultModel?.capabilities,
) && (
<DragOverlay show={showDragOverMsg} setShow={setShowDragOverMsg} />
)}
) && <DragOverlay show={showDragOverMsg} />}
<div id={TIPPY_DIV_ID} className="fixed z-50" />
</InputBoxDiv>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,11 @@
import React, { useEffect } from "react";
import React from "react";
import { HoverDiv, HoverTextDiv } from "./StyledComponents";

interface DragOverlayProps {
show: boolean;
setShow: (show: boolean) => void;
}

export const DragOverlay: React.FC<DragOverlayProps> = ({ 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);
};
}, []);

export const DragOverlay: React.FC<DragOverlayProps> = ({ show }) => {
if (!show) return null;

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const HoverDiv = styled.div`
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
`;

export const HoverTextDiv = styled.div`
Expand All @@ -68,4 +69,5 @@ export const HoverTextDiv = styled.div`
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
`;
79 changes: 77 additions & 2 deletions gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
getContextProviderDropdownOptions,
getSlashCommandDropdownOptions,
} from "./getSuggestion";
import { handleImageFile } from "./imageUtils";
import { handleImageFile, handleVSCodeResourceFromHtml } from "./imageUtils";

export function getPlaceholderText(
placeholder: TipTapEditorProps["placeholder"],
Expand Down Expand Up @@ -69,8 +69,9 @@ export function createEditorConfig(options: {
props: TipTapEditorProps;
ideMessenger: IIdeMessenger;
dispatch: AppDispatch;
setShowDragOverMsg: (show: boolean) => void;
}) {
const { props, ideMessenger, dispatch } = options;
const { props, ideMessenger, dispatch, setShowDragOverMsg } = options;

const posthog = usePostHog();

Expand Down Expand Up @@ -147,6 +148,80 @@ 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;
Expand Down
Loading
Loading