Unverified 提交 76cff36c authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Fix uploading files to codebase in local agent mode (#2210)

<!-- CURSOR_SUMMARY --> > [!NOTE] > Fixes upload-to-codebase in local-agent by resolving attachment IDs to real file bytes and adds e2e coverage. > > - **Core fix**: New `file_upload_utils.resolveFileUploadContent` maps `DYAD_ATTACHMENT_X` IDs to uploaded file contents via `FileUploadsState`. > - **Tool update**: `write_file` now writes resolved bytes (supports binary) instead of raw ID strings. > - **Stream handling**: Clear stale uploads for the chat at stream start in `chat_stream_handlers`. > - **Tests**: Playwright test, fixture, and snapshot verify uploading `logo.png` to `assets/uploaded-file.png` and content parity. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 220b0eaae361fee015e5146b9fffd217e7bf7626. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Fixes uploading files to the codebase in local-agent mode by resolving attachment IDs (e.g., DYAD_ATTACHMENT_0) to real file bytes before writing. Adds e2e coverage to prevent regressions. - **Bug Fixes** - Added resolver to map upload IDs to file content using FileUploadsState. - Updated write_file to use the resolver and write the resolved bytes. - Cleared file uploads state at chat start to avoid stale IDs from previous requests. - Added Playwright test and fixture that upload logo.png and verify the written file and snapshot. <sup>Written for commit 220b0eaae361fee015e5146b9fffd217e7bf7626. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. -->
上级 c26f65a1
import type { LocalAgentFixture } from "../../../../testing/fake-llm-server/localAgentTypes";
/**
* Test fixture for file upload to codebase in local-agent mode.
* The AI receives a file upload ID (DYAD_ATTACHMENT_0) and uses the write_file tool
* to write the uploaded file to the codebase. The file_upload_utils should resolve
* the attachment ID to the actual file content.
*/
export const fixture: LocalAgentFixture = {
description: "Upload file to codebase using write_file tool",
turns: [
{
text: "I'll upload your file to the codebase.",
toolCalls: [
{
name: "write_file",
args: {
path: "assets/uploaded-file.png",
content: "DYAD_ATTACHMENT_0",
description: "Upload file to codebase",
},
},
],
},
{
text: "I've successfully uploaded your file to assets/uploaded-file.png in the codebase.",
},
],
};
import path from "path";
import fs from "fs";
import { expect } from "@playwright/test";
import { testSkipIfWindows } from "./helpers/test_helper";
/**
* Test for file upload to codebase in local-agent mode.
*
* This tests that when a file is uploaded with "upload to codebase" mode,
* the local agent's write_file tool correctly resolves the file ID
* (e.g., DYAD_ATTACHMENT_0) to the actual uploaded file content.
*/
testSkipIfWindows("local-agent - upload file to codebase", async ({ po }) => {
await po.setUpDyadPro({ localAgent: true });
await po.importApp("minimal");
await po.selectLocalAgentMode();
// Open auxiliary actions menu
await po
.getChatInputContainer()
.getByTestId("auxiliary-actions-menu")
.click();
// Hover over "Attach files" to open submenu
await po.page.getByRole("menuitem", { name: "Attach files" }).hover();
// Set up file chooser listener BEFORE clicking the menu item
const fileChooserPromise = po.page.waitForEvent("filechooser");
// Click the menu item to trigger the file picker
await po.page.getByText("Upload file to codebase").click();
// Handle the file chooser dialog
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles("e2e-tests/fixtures/images/logo.png");
// Send prompt that triggers the upload-to-codebase fixture
await po.sendPrompt("tc=local-agent/upload-to-codebase");
// Verify the file was written to the codebase
const appPath = await po.getCurrentAppPath();
const filePath = path.join(appPath, "assets", "uploaded-file.png");
// The file should exist
expect(fs.existsSync(filePath)).toBe(true);
// The file contents should match the original uploaded file
const expectedContents = fs.readFileSync(
"e2e-tests/fixtures/images/logo.png",
"base64",
);
const actualContents = fs.readFileSync(filePath, "base64");
expect(actualContents).toBe(expectedContents);
// Snapshot the messages
await po.snapshotMessages();
});
- paragraph: /Generate an AI_RULES\.md file for this app\. Describe the tech stack in 5-\d+ bullet points and describe clear rules about what libraries to use for what\./
- img
- text: file1.txt
- button "Edit":
- img
- img
- text: file1.txt
- paragraph: More EOM
- button:
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- img
- paragraph: tc=local-agent/upload-to-codebase
- paragraph: "Attachments:"
- paragraph: "File to upload to codebase: logo.png (file id: DYAD_ATTACHMENT_0)"
- paragraph: I'll upload your file to the codebase.
- img
- text: uploaded-file.png
- button "Edit":
- img
- img
- text: "assets/uploaded-file.png Summary: Upload file to codebase"
- paragraph: I've successfully uploaded your file to assets/uploaded-file.png in the codebase.
- button:
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- img
- button "Undo":
- img
- button "Retry":
- img
\ No newline at end of file
......@@ -226,6 +226,8 @@ export function registerChatStreamHandlers() {
let attachmentPaths: string[] = [];
try {
const fileUploadsState = FileUploadsState.getInstance();
// Clear any stale state from previous requests for this chat
fileUploadsState.clear(req.chatId);
let dyadRequestId: string | undefined;
// Create an AbortController for this stream
const abortController = new AbortController();
......@@ -1455,8 +1457,6 @@ ${problemReport.problems
error: `Sorry, there was an error processing your request: ${error}`,
});
// Clean up file uploads state on error
FileUploadsState.getInstance().clear(req.chatId);
return "error";
} finally {
// Clean up the abort controller
......@@ -1504,11 +1504,6 @@ ${problemReport.problems
updatedFiles: false,
} satisfies ChatResponseEnd);
// Clean up uploads state for this chat
try {
FileUploadsState.getInstance().clear(chatId);
} catch {}
return true;
});
}
......
/**
* Shared utility for resolving file upload IDs to actual file content.
* When users upload files with "upload-to-codebase" attachment type,
* the LLM receives a file ID (e.g., DYAD_ATTACHMENT_0) instead of the raw content.
* This utility replaces those IDs with the actual file content when writing files.
*/
import fs from "node:fs";
import log from "electron-log";
import {
FileUploadsState,
FileUploadInfo,
} from "@/ipc/utils/file_uploads_state";
const readFile = fs.promises.readFile;
const logger = log.scope("file_upload_utils");
export interface ResolveFileUploadResult {
/** The resolved content (either the original content or the uploaded file's content) */
content: string | Buffer;
/** Whether a file upload ID was replaced */
wasReplaced: boolean;
/** Info about the file that was replaced (if any) */
fileInfo?: FileUploadInfo;
}
/**
* Resolves file upload IDs in content to actual file content.
*
* If the content (stripped of whitespace) exactly matches a file upload ID
* (e.g., "DYAD_ATTACHMENT_0"), this function reads the actual uploaded file
* and returns its content. Otherwise, returns the original content unchanged.
*
* @param content - The content to check for file upload IDs
* @param chatId - The chat ID to look up file uploads for
* @returns The resolved content and metadata about the replacement
*/
export async function resolveFileUploadContent(
content: string,
chatId: number,
): Promise<ResolveFileUploadResult> {
const fileUploadsState = FileUploadsState.getInstance();
const fileUploadsMap = fileUploadsState.getFileUploadsForChat(chatId);
if (fileUploadsMap.size === 0) {
return { content, wasReplaced: false };
}
const trimmedContent = content.trim();
const fileInfo = fileUploadsMap.get(trimmedContent);
if (!fileInfo) {
return { content, wasReplaced: false };
}
try {
const fileContent = await readFile(fileInfo.filePath);
logger.log(
`Replaced file ID ${trimmedContent} with content from ${fileInfo.originalName}`,
);
return {
content: fileContent,
wasReplaced: true,
fileInfo,
};
} catch (error) {
logger.error(
`Failed to read uploaded file ${fileInfo.originalName}:`,
error,
);
throw new Error(
`Failed to read uploaded file: ${fileInfo.originalName}. ${error}`,
);
}
}
......@@ -9,6 +9,7 @@ import {
isServerFunction,
isSharedServerModule,
} from "../../../../../../supabase_admin/supabase_utils";
import { resolveFileUploadContent } from "./file_upload_utils";
const logger = log.scope("write_file");
......@@ -47,12 +48,16 @@ export const writeFileTool: ToolDefinition<z.infer<typeof writeFileSchema>> = {
ctx.isSharedModulesChanged = true;
}
// Resolve file upload IDs to actual content
const resolved = await resolveFileUploadContent(args.content, ctx.chatId);
const contentToWrite = resolved.content;
// Ensure directory exists
const dirPath = path.dirname(fullFilePath);
fs.mkdirSync(dirPath, { recursive: true });
// Write file content
fs.writeFileSync(fullFilePath, args.content);
fs.writeFileSync(fullFilePath, contentToWrite);
logger.log(`Successfully wrote file: ${fullFilePath}`);
// Deploy Supabase function if applicable
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论