Skip to content
Open
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";
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Sep 22, 2025

Choose a reason for hiding this comment

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

Non-PNG images are all forced to image/jpeg; gif/webp/svg (and others) will be mislabeled, causing incorrect data URLs.

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

<comment>Non-PNG images are all forced to image/jpeg; gif/webp/svg (and others) will be mislabeled, causing incorrect data URLs.</comment>

<file context>
@@ -287,6 +287,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>
Fix with Cubic

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Sep 22, 2025

Choose a reason for hiding this comment

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

Extension comparison is case-sensitive; uppercase extensions (e.g., .PNG) are misclassified, producing the wrong MIME type.

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

<comment>Extension comparison is case-sensitive; uppercase extensions (e.g., .PNG) are misclassified, producing the wrong MIME type.</comment>

<file context>
@@ -287,6 +287,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);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Sep 22, 2025

Choose a reason for hiding this comment

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

Unconditional hide overrides delayed/conditional logic in onDragLeave, making the delay and Shift-key behavior ineffective.

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

<comment>Unconditional hide overrides delayed/conditional logic in onDragLeave, making the delay and Shift-key behavior ineffective.</comment>

<file context>
@@ -221,40 +222,23 @@ function TipTapEditorInner(props: TipTapEditorProps) {
           } else {
-            setTimeout(() =&gt; setShowDragOverMsg(false), 2000);
+            setTimeout(() =&gt; {
+              setShowDragOverMsg(false);
+            }, 2000);
           }
</file context>
Fix with Cubic

}, 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 Sep 22, 2025

Choose a reason for hiding this comment

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

onDrop does not prevent default, which can cause the browser to navigate/open the dropped file when dropping outside the editor area. Add event.preventDefault() to avoid data loss risk.

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

<comment>onDrop does not prevent default, which can cause the browser to navigate/open the dropped file when dropping outside the editor area. Add event.preventDefault() to avoid data loss risk.</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) {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Sep 22, 2025

Choose a reason for hiding this comment

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

Drop handler prevents default and returns true even when nothing is handled, likely blocking normal text/URL drops. Consider only preventing default/returning true when an image is actually processed, otherwise fall through.

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

<comment>Drop handler prevents default and returns true even when nothing is handled, likely blocking normal text/URL drops. Consider only preventing default/returning true when an image is actually processed, otherwise fall through.</comment>

<file context>
@@ -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);
</file context>
Fix with Cubic

// 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 Sep 22, 2025

Choose a reason for hiding this comment

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

Image from HTML drop is inserted at position 0 instead of the intended drop/caret position.

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

<comment>Image from HTML drop is inserted at position 0 instead of the intended drop/caret position.</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