Skip to content
Closed
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
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 @@ -298,6 +298,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";
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uppercase image extensions (e.g. foo.PNG) hit this branch and get labeled image/jpeg even though the bytes are PNG, producing a mismatched data URL that can fail to render; normalize the extension before comparing.

Prompt for AI agents
Address the following comment on extensions/vscode/src/extension/VsCodeMessenger.ts at line 306:

<comment>Uppercase image extensions (e.g. foo.PNG) hit this branch and get labeled image/jpeg even though the bytes are PNG, producing a mismatched data URL that can fail to render; normalize the extension before comparing.</comment>

<file context>
@@ -298,6 +298,18 @@ export class VsCodeMessenger {
+      const fileUri = vscode.Uri.file(filepath);
+      const fileContents = await vscode.workspace.fs.readFile(fileUri);
+      const fileType =
+        filepath.split(&quot;.&quot;).pop() === &quot;png&quot; ? &quot;image/png&quot; : &quot;image/jpeg&quot;;
+      const dataUrl = `data:${fileType};base64,${Buffer.from(
+        fileContents,
</file context>
Suggested change
filepath.split(".").pop() === "png" ? "image/png" : "image/jpeg";
filepath.split(".").pop()?.toLowerCase() === "png" ? "image/png" : "image/jpeg";
Fix with Cubic

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
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping a file on InputBoxDiv areas outside the ProseMirror view (like the toolbar) now navigates the browser to that file because this handler no longer calls event.preventDefault(), and the new drop plugin in utils/editorConfig.ts only runs for drops that originate on the editor DOM. Please continue to cancel the default action here to keep the page from navigating away.

Prompt for AI agents
Address the following comment on gui/src/components/mainInput/TipTapEditor/TipTapEditor.tsx at line 241:

<comment>Dropping a file on InputBoxDiv areas outside the ProseMirror view (like the toolbar) now navigates the browser to that file because this handler no longer calls event.preventDefault(), and the new drop plugin in utils/editorConfig.ts only runs for drops that originate on the editor DOM. Please continue to cancel the default action here to keep the page from navigating away.</comment>

<file context>
@@ -221,40 +222,23 @@ function TipTapEditorInner(props: TipTapEditorProps) {
-          }
-        });
-        event.preventDefault();
+        // Let the event bubble to ProseMirror by not preventing default
       }}
     &gt;
</file context>
Suggested change
// Let the event bubble to ProseMirror by not preventing default
event.preventDefault();
Fix with Cubic

}}
>
<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);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The drop handler always inserts the image at document position 0, so every dropped image appears at the top instead of the drop point. Please compute the drop insertion position (e.g., using the drop selection or view.posAtCoords) before inserting.

Prompt for AI agents
Address the following comment on gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts at line 189:

<comment>The drop handler always inserts the image at document position 0, so every dropped image appears at the top instead of the drop point. Please compute the drop insertion position (e.g., using the drop selection or view.posAtCoords) before inserting.</comment>

<file context>
@@ -147,6 +148,80 @@ export function createEditorConfig(options: {
+                        const node = schema.nodes.image.create({
+                          src: dataUrl,
+                        });
+                        const tr = view.state.tr.insert(0, node);
+                        view.dispatch(tr);
+                      }
</file context>
Fix with Cubic

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