-
Notifications
You must be signed in to change notification settings - Fork 2.9k
t3code/support markdown chat attachments #3927
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
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 |
|---|---|---|
|
|
@@ -123,6 +123,11 @@ import { | |
| } from "../../lib/contextWindow"; | ||
| import { formatProviderSkillDisplayName } from "../../providerSkillPresentation"; | ||
| import { searchProviderSkills } from "../../providerSkillSearch"; | ||
| import { projectEnvironment } from "~/state/projects"; | ||
| import { useAtomCommand } from "~/state/use-atom-command"; | ||
| import { resolvePathLinkTarget } from "~/terminal-links"; | ||
|
|
||
| const TEXT_ATTACHMENT_MAX_BYTES = 1024 * 1024; | ||
| import { useMediaQuery } from "../../hooks/useMediaQuery"; | ||
| import type { ReviewCommentContext } from "../../reviewCommentContext"; | ||
|
|
||
|
|
@@ -613,6 +618,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | |
| // Store subscriptions (prompt / images / terminal contexts) | ||
| // ------------------------------------------------------------------ | ||
| const composerDraft = useComposerThreadDraft(composerDraftTarget); | ||
| const writeProjectFile = useAtomCommand(projectEnvironment.writeFile, { reportFailure: false }); | ||
| const prompt = composerDraft.prompt; | ||
| const composerImages = composerDraft.images; | ||
| const composerTerminalContexts = composerDraft.terminalContexts; | ||
|
|
@@ -1758,14 +1764,46 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | |
| }; | ||
|
|
||
| // ------------------------------------------------------------------ | ||
| // Callbacks: images | ||
| // Callbacks: attachments | ||
| // ------------------------------------------------------------------ | ||
| const addComposerImages = (files: File[]) => { | ||
| const addComposerTextAttachment = async (file: File): Promise<string | null> => { | ||
| if (!gitCwd) return `Could not resolve the workspace path for '${file.name}'.`; | ||
| if (file.size > TEXT_ATTACHMENT_MAX_BYTES) { | ||
| return `'${file.name}' exceeds the 1 MB text attachment limit.`; | ||
| } | ||
| const safeName = file.name.replace(/[^a-zA-Z0-9._-]+/g, "-") || "context.md"; | ||
| const relativePath = `.t3/attachments/${randomUUID()}/${safeName}`; | ||
| const result = await file | ||
| .arrayBuffer() | ||
| .then((buffer) => { | ||
| const bytes = new Uint8Array(buffer); | ||
| if (bytes.includes(0)) return null; | ||
| const contents = new TextDecoder("utf-8", { fatal: true }).decode(bytes); | ||
| return writeProjectFile({ | ||
| environmentId, | ||
| input: { cwd: gitCwd, relativePath, contents }, | ||
| }); | ||
| }) | ||
| .catch(() => null); | ||
| if (result === null) return `'${file.name}' is not a supported text file.`; | ||
| if (result._tag === "Failure") return `Could not attach '${file.name}'.`; | ||
|
|
||
| const currentPrompt = promptRef.current; | ||
| const separator = currentPrompt.length > 0 && !/\s$/.test(currentPrompt) ? " " : ""; | ||
| applyPromptReplacement( | ||
| currentPrompt.length, | ||
| currentPrompt.length, | ||
| `${separator}${serializeComposerFileLink(resolvePathLinkTarget(relativePath, gitCwd))} `, | ||
|
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.
When the first message is sent in New worktree mode, Useful? React with 👍 / 👎. |
||
| ); | ||
|
Contributor
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. Parallel drops corrupt promptHigh Severity Each text attachment appends using Reviewed by Cursor Bugbot for commit f044e40. Configure here. |
||
| return null; | ||
| }; | ||
|
|
||
| const addComposerAttachments = async (files: File[]) => { | ||
| if (!activeThreadId || files.length === 0) return; | ||
| if (pendingUserInputs.length > 0) { | ||
| toastManager.add({ | ||
| type: "error", | ||
| title: "Attach images after answering plan questions.", | ||
| title: "Attach files after answering plan questions.", | ||
| }); | ||
| return; | ||
| } | ||
|
|
@@ -1774,7 +1812,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | |
| let error: string | null = null; | ||
| for (const file of files) { | ||
| if (!file.type.startsWith("image/")) { | ||
| error = `Unsupported file type for '${file.name}'. Please attach image files only.`; | ||
| error = (await addComposerTextAttachment(file)) ?? error; | ||
| continue; | ||
| } | ||
| if (file.size > PROVIDER_SEND_TURN_MAX_IMAGE_BYTES) { | ||
|
|
@@ -1818,7 +1856,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | |
| const imageFiles = files.filter((file) => file.type.startsWith("image/")); | ||
| if (imageFiles.length === 0) return; | ||
| event.preventDefault(); | ||
| addComposerImages(imageFiles); | ||
|
Contributor
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. Mixed batch errors overwrittenMedium Severity In a mixed drop, text failures use Reviewed by Cursor Bugbot for commit f044e40. Configure here.
Contributor
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 limit skips text filesLow Severity When the per-message image cap is reached, Reviewed by Cursor Bugbot for commit f044e40. Configure here. |
||
| void addComposerAttachments(imageFiles); | ||
| }; | ||
|
|
||
| const onComposerDragEnter = (event: React.DragEvent<HTMLDivElement>) => { | ||
|
|
@@ -1852,7 +1890,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | |
| dragDepthRef.current = 0; | ||
| setIsDragOverComposer(false); | ||
| const files = Array.from(event.dataTransfer.files); | ||
| addComposerImages(files); | ||
| void addComposerAttachments(files); | ||
|
Contributor
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. Send races text attachmentMedium Severity When text files are dropped or pasted, their asynchronous processing to write the file and add its link to the prompt is not awaited. This allows the composer's prompt to be submitted prematurely, resulting in messages sent without the intended attachment link and potentially leaving orphaned attachment files. Reviewed by Cursor Bugbot for commit f044e40. Configure here. 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.
For dropped markdown/text files this handler now fire-and-forgets an async path: Useful? React with 👍 / 👎. |
||
| focusComposer(); | ||
| }; | ||
| const handleInterruptPrimaryAction = useCallback(() => { | ||
|
|
||


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.
This writes every dropped text/markdown attachment under
.t3/attachmentsinside the user's project, but the code does not ensure that.t3/is ignored for arbitrary workspaces. In projects that have not already added that ignore rule, attaching a file creates untracked files that show up in git status and can be accidentally committed along with the user's changes.Useful? React with 👍 / 👎.