-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Fix image drop from main editor #7910
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
aae1974
2f5757f
b238659
3675d3b
68e9e6e
f9079ea
4bdca1a
2a04f4e
4a86b55
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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"; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||
const dataUrl = `data:${fileType};base64,${Buffer.from( | ||||||
fileContents, | ||||||
).toString("base64")}`; | ||||||
return dataUrl; | ||||||
}); | ||||||
|
||||||
this.onWebviewOrCore("getIdeSettings", async (msg) => { | ||||||
return ide.getIdeSettings(); | ||||||
}); | ||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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 | ||||||
|
@@ -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( | ||||||
|
@@ -221,40 +222,23 @@ function TipTapEditorInner(props: TipTapEditorProps) { | |||||
if (e.shiftKey) { | ||||||
setShowDragOverMsg(false); | ||||||
} else { | ||||||
setTimeout(() => setShowDragOverMsg(false), 2000); | ||||||
setTimeout(() => { | ||||||
setShowDragOverMsg(false); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||
}, 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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||
}} | ||||||
> | ||||||
<div className="px-2.5 pb-1 pt-2"> | ||||||
|
@@ -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> | ||||||
); | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,7 @@ import { | |
getContextProviderDropdownOptions, | ||
getSlashCommandDropdownOptions, | ||
} from "./getSuggestion"; | ||
import { handleImageFile } from "./imageUtils"; | ||
import { handleImageFile, handleVSCodeResourceFromHtml } from "./imageUtils"; | ||
|
||
export function getPlaceholderText( | ||
placeholder: TipTapEditorProps["placeholder"], | ||
|
@@ -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(); | ||
|
||
|
@@ -147,6 +148,80 @@ export function createEditorConfig(options: { | |
const plugin = new Plugin({ | ||
props: { | ||
handleDOMEvents: { | ||
drop(view, event) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
// 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
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; | ||
|
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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