Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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