Repository navigation
fix(web): attach a picture pasted into the message box #152
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
Changes from all commits
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 |
|---|---|---|
|
|
@@ -49,6 +49,7 @@ import { | |
| type DispatchRoom, | ||
| } from "./dispatch-card.tsx"; | ||
| import { earlierDocumentRole } from "./earlier-document.ts"; | ||
| import { pastedFiles } from "./pasted-files.ts"; | ||
| import { ArtifactPane, LIVE_TAB } from "./artifact-pane.tsx"; | ||
| import "./persistent-workspace.css"; | ||
| import { ProductApiError, type ProjectArtifact } from "@corbits-builder/client"; | ||
|
|
@@ -1285,6 +1286,14 @@ export function StageWorkspace({ | |
| ); | ||
| } | ||
| } else { | ||
| // Files can be attached while the hub offers kinds it reads, the provider is set and nothing is | ||
| // mid-send; the drop zone, the paperclip and a paste all follow this. | ||
| const attaching = | ||
| limits !== undefined && | ||
| limits.mimeTypes.length > 0 && | ||
| !sending && | ||
| !approvalBusy && | ||
| !providerMissing; | ||
| dock = ( | ||
| <div | ||
| className="composer" | ||
|
|
@@ -1342,13 +1351,7 @@ export function StageWorkspace({ | |
| ) : null} | ||
| <AttachmentDropZone | ||
| onFiles={addFiles} | ||
| disabled={ | ||
| limits === undefined || | ||
| limits.mimeTypes.length === 0 || | ||
| sending || | ||
| approvalBusy || | ||
| providerMissing | ||
| } | ||
| disabled={!attaching} | ||
| label={wording.conversation.dropFiles} | ||
| > | ||
| <Dictated | ||
|
|
@@ -1391,6 +1394,15 @@ export function StageWorkspace({ | |
| )} | ||
| <PromptInputTextarea | ||
| ref={composerRef} | ||
| // A picture on the clipboard attaches like a chosen file, while attaching is | ||
| // offered at all; text, and anything else, pastes as the browser would. | ||
| onPaste={(event) => { | ||
| if (!attaching) return; | ||
| const files = pastedFiles(event.clipboardData); | ||
| if (files.length === 0) return; | ||
| event.preventDefault(); | ||
|
Comment on lines
+1402
to
+1403
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. P2 · Mixed text-and-image clipboard drops the text Whenever the clipboard carries any file, preventDefault() runs, even if it also carries text. Office apps and some browsers put a picture next to the text when you copy, so a paste that should insert text attaches an image and loses the text. Copy cells from Excel or text from Word, which also puts an image/png on the clipboard, then paste into the message box. Resolve this comment. Triage will not finish while it is open. |
||
| addFiles(files); | ||
|
Comment on lines
+1402
to
+1404
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. P2 · Paste is silently swallowed while limits are still loading The handler calls preventDefault() before addFiles. addFiles returns early when limits is undefined, so the paste is cancelled and nothing appears: no attachment and no message. This is the same silent failure the PR is meant to fix. Paste an image before the attachment limits have loaded. Resolve this comment. Triage will not finish while it is open. |
||
| }} | ||
|
Comment on lines
+1399
to
+1405
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. P2 · Paste attaches files while attaching is turned off When the provider is missing or no MIME types are allowed, the drop zone is disabled and the paperclip is hidden. The new paste handler checks neither condition, so pasting still attaches files. Paste a screenshot when providerMissing is true or limits.mimeTypes is empty. Resolve this comment. Triage will not finish while it is open. |
||
| placeholder={ | ||
| providerMissing | ||
| ? wording.conversation.enableAiFirst | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { expect, test } from "bun:test"; | ||
| import { pastedFiles, pastedName } from "./pasted-files.ts"; | ||
|
|
||
| const at = new Date("2026-11-04T10:52:07.000Z"); | ||
| const picture = () => | ||
| new File([new Uint8Array([137, 80, 78, 71])], "image.png", { | ||
| type: "image/png", | ||
| }); | ||
| const clipboard = (files: File[], text = "") => ({ | ||
| files, | ||
| getData: (type: string) => (type === "text/plain" ? text : ""), | ||
| }); | ||
|
|
||
| test("a clipboard picture gets a stamped name; a named file keeps its own", () => { | ||
| const report = new File(["hi"], "report.pdf", { type: "application/pdf" }); | ||
| const [first, second] = pastedFiles(clipboard([picture(), report]), at); | ||
| expect(first?.name).toMatch(/^pasted-2026-11-04-10-52-07-\d+\.png$/); | ||
| expect(first?.type).toBe("image/png"); | ||
| expect(second?.name).toBe("report.pdf"); | ||
| }); | ||
|
|
||
| test("every pasted picture gets its own name, even within one second", () => { | ||
| const names = [ | ||
| ...pastedFiles(clipboard([picture(), picture()]), at), | ||
| ...pastedFiles(clipboard([picture()]), at), | ||
| ].map((file) => file.name); | ||
| expect(new Set(names).size).toBe(3); | ||
| }); | ||
|
|
||
| test("a paste that carries text is left to the browser, and an empty one yields nothing", () => { | ||
| expect(pastedFiles(clipboard([picture()], "A1\tB1"), at)).toEqual([]); | ||
| expect(pastedFiles(clipboard([]), at)).toEqual([]); | ||
| expect(pastedFiles(null, at)).toEqual([]); | ||
| expect(pastedName("application/octet-stream", at, 7)).toBe( | ||
| "pasted-2026-11-04-10-52-07-7", | ||
| ); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| /** | ||
| * Files pasted into the message box, named so each can be attached: a picture from the clipboard | ||
| * arrives as "image.png" every time, which the attachment list would refuse as a duplicate. | ||
| */ | ||
| const GENERIC_NAMES = new Set([ | ||
| "", | ||
| "image.png", | ||
| "image.jpg", | ||
| "image.jpeg", | ||
| "image.gif", | ||
| "image.webp", | ||
| ]); | ||
|
|
||
| const EXTENSIONS: Readonly<Record<string, string>> = { | ||
| "image/png": "png", | ||
| "image/jpeg": "jpg", | ||
| "image/gif": "gif", | ||
| "image/webp": "webp", | ||
| "application/pdf": "pdf", | ||
| "text/plain": "txt", | ||
| "text/markdown": "md", | ||
| "text/csv": "csv", | ||
| "application/json": "json", | ||
| }; | ||
|
|
||
| // Counts up for the page's life, so two pastes in one second still get different names. | ||
| let pasted = 0; | ||
|
|
||
| /** "pasted-2026-11-04-10-52-07-3.png": the time for the person, the count so no two collide. */ | ||
| export function pastedName(type: string, at: Date, serial: number): string { | ||
| const stamp = at | ||
| .toISOString() | ||
| .slice(0, 19) | ||
| .replace("T", "-") | ||
| .replaceAll(":", "-"); | ||
|
Comment on lines
+31
to
+35
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. P2 · Pasted names are only unique to the second, so close pastes collide as duplicates The name is built from the timestamp cut down to whole seconds. Two pastes of a clipboard picture within the same second get the same Press Ctrl+V twice quickly with a screenshot on the clipboard, or paste two images and paste again a second later. Resolve this comment. Triage will not finish while it is open. |
||
| const extension = EXTENSIONS[type]; | ||
|
Comment on lines
+32
to
+36
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. P3 · Name stamp uses UTC instead of the person's local time toISOString() always gives UTC. A stamp meant to show "the time for the person" is wrong by their UTC offset; outside UTC it can even show a different date from the one they pasted on. Paste a screenshot from a browser set to any time zone other than UTC. Resolve this comment. Triage will not finish while it is open. |
||
| const base = `pasted-${stamp}-${String(serial)}`; | ||
| return extension === undefined ? base : `${base}.${extension}`; | ||
| } | ||
|
|
||
| /** | ||
| * The files on the clipboard, each with a name of its own, or none when the paste carries text: | ||
| * an office app copies a picture of the cells beside the text, and the text is what was meant. | ||
| */ | ||
| export function pastedFiles( | ||
| clipboard: { files: ArrayLike<File>; getData(type: string): string } | null, | ||
| at: Date = new Date(), | ||
| ): File[] { | ||
| if (clipboard === null) return []; | ||
| if (clipboard.getData("text/plain").trim() !== "") return []; | ||
| return Array.from(clipboard.files, (file) => { | ||
|
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. P2 · Any text on the clipboard throws away the pasted files If the clipboard has any non-blank text/plain, the code skips every file. Copying a file in a file manager (Finder, Explorer) or using "Copy Image" in some browsers puts the file name or image URL on the clipboard as text/plain next to the file. The file is then silently dropped: the paste inserts a name or URL instead of attaching anything, which is the silent failure #151 is meant to fix. Copy report.pdf in macOS Finder and paste it into the message box: the clipboard holds the file plus text/plain "report.pdf". Resolve this comment. Triage will not finish while it is open. |
||
| if (!GENERIC_NAMES.has(file.name.toLowerCase())) return file; | ||
| pasted += 1; | ||
| return new File([file], pastedName(file.type, at, pasted), { | ||
| type: file.type, | ||
| }); | ||
| }); | ||
| } | ||
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.
P2 · A copied file that also puts text on the clipboard is not attached
The handler leaves the paste alone whenever
pastedFilesreturns nothing.pastedFiles(in pasted-files.ts, not part of this diff) returns nothing whenever the clipboard carries any non-blank text/plain. When you copy a file in a file manager, the browser usually also puts the file name on the clipboard as text/plain. So pasting a real file inserts its name instead of attaching it, which goes against the PR's goal of attaching any readable file.Copy a PDF or image file in macOS Finder and paste it into the message box in Chrome.
Resolve this comment. Triage will not finish while it is open.