diff --git a/apps/desktop/src/components/claude-chat/chat-composer.tsx b/apps/desktop/src/components/claude-chat/chat-composer.tsx index 97a923c..e5bf297 100644 --- a/apps/desktop/src/components/claude-chat/chat-composer.tsx +++ b/apps/desktop/src/components/claude-chat/chat-composer.tsx @@ -32,7 +32,7 @@ import { ListEndIcon, } from "lucide-react"; import { getCurrentWebview } from "@tauri-apps/api/webview"; -import { writeFile, mkdir, exists } from "@tauri-apps/plugin-fs"; +import { writeFile, mkdir, exists, remove } from "@tauri-apps/plugin-fs"; import { join, tempDir } from "@tauri-apps/api/path"; import { invoke } from "@tauri-apps/api/core"; import { @@ -87,6 +87,7 @@ interface PinnedContext { filePath: string; selectedText: string; imageDataUrl?: string; // thumbnail for captured images + isTemporary?: boolean; } function pastedFileExtension(file: File) { @@ -113,6 +114,33 @@ function readFileAsDataUrl(file: File) { }); } +function temporaryFilePaths(contexts: PinnedContext[]) { + return contexts + .filter((context) => context.isTemporary) + .map((context) => context.filePath); +} + +async function cleanupTemporaryFilePaths(paths: string[] | undefined) { + if (!paths?.length) return; + await Promise.all( + paths.map(async (path) => { + try { + await remove(path); + } catch (err) { + log.warn("Failed to remove temporary pasted file", { + path, + error: String(err), + }); + } + }), + ); +} + +function cleanupTemporaryPinnedContext(context: PinnedContext) { + if (!context.isTemporary) return; + void cleanupTemporaryFilePaths([context.filePath]); +} + function getFileIcon(file: ProjectFile) { if (file.type === "image") return ; @@ -813,6 +841,7 @@ export const ChatComposer: FC<{ isOpen?: boolean }> = ({ isOpen }) => { "Use this image file as visual context for the user's message.", ].join("\n"), imageDataUrl: await readFileAsDataUrl(file), + isTemporary: true, }); } catch (err) { log.error("Failed to save pasted image", { @@ -946,6 +975,7 @@ export const ChatComposer: FC<{ isOpen?: boolean }> = ({ isOpen }) => { label: combinedLabel, filePath: pinnedContexts[0].filePath, selectedText: combinedText, + temporaryFilePaths: temporaryFilePaths(pinnedContexts), }; } @@ -960,7 +990,8 @@ export const ChatComposer: FC<{ isOpen?: boolean }> = ({ isOpen }) => { if (textareaRef.current) { textareaRef.current.style.height = "auto"; } - // Clear pinned contexts after send + // Clear pinned contexts after send. Temporary files are removed by the + // completion event once the provider has finished with them. setPinnedContexts([]); }, [ activeTabId, @@ -1655,9 +1686,12 @@ export const ChatComposer: FC<{ isOpen?: boolean }> = ({ isOpen }) => { type="button" aria-label="Remove queued guidance" className="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-red-500/10 hover:text-red-600 dark:hover:bg-red-500/15 dark:hover:text-red-400" - onClick={() => - removeQueuedGuidance(activeTabId, guidance.id) - } + onClick={() => { + void cleanupTemporaryFilePaths( + guidance.contextOverride?.temporaryFilePaths, + ); + removeQueuedGuidance(activeTabId, guidance.id); + }} > @@ -1683,11 +1717,12 @@ export const ChatComposer: FC<{ isOpen?: boolean }> = ({ isOpen }) => { />