From 792ecd6064b73a042ae33fa58e2a5a36fc6c8e80 Mon Sep 17 00:00:00 2001 From: Gerard-Devlin Date: Thu, 11 Jun 2026 16:02:15 +0800 Subject: [PATCH] refactor: remove PDF sidecar extraction and related logic from project attachments --- .../src-tauri/src/anthropic_proxy/messages.rs | 54 ++++++- apps/desktop/src-tauri/src/claude.rs | 3 + apps/desktop/src-tauri/src/uv.rs | 101 ++++++++++++- .../__tests__/lib/project-attachments.test.ts | 59 ++------ .../components/claude-chat/chat-composer.tsx | 84 +++-------- .../desktop/src/components/project-wizard.tsx | 4 +- .../template-gallery/template-preview.tsx | 4 +- apps/desktop/src/lib/pdf-text-extractor.ts | 133 ------------------ apps/desktop/src/lib/project-attachments.ts | 34 ++--- 9 files changed, 186 insertions(+), 290 deletions(-) delete mode 100644 apps/desktop/src/lib/pdf-text-extractor.ts diff --git a/apps/desktop/src-tauri/src/anthropic_proxy/messages.rs b/apps/desktop/src-tauri/src/anthropic_proxy/messages.rs index 46f9204..3cd10cc 100644 --- a/apps/desktop/src-tauri/src/anthropic_proxy/messages.rs +++ b/apps/desktop/src-tauri/src/anthropic_proxy/messages.rs @@ -43,12 +43,15 @@ pub(super) fn anthropic_to_openai_request( .filter_map(anthropic_tool_to_openai_tool) .collect::>(); if !converted.is_empty() { - let tool_choice = openai_tool_choice(request.get("tool_choice")); let mut converted = converted; - if tool_choice == Value::String("required".to_string()) { + let requested_tool_choice = openai_tool_choice(request.get("tool_choice")); + let tool_choice = if requested_tool_choice.is_object() { + requested_tool_choice + } else { append_exit_tool(&mut converted); append_exit_tool_reminder(&mut body); - } + Value::String("required".to_string()) + }; body["tools"] = Value::Array(converted); body["tool_choice"] = tool_choice; } @@ -530,13 +533,13 @@ fn append_exit_tool(tools: &mut Vec) { "type": "function", "function": { "name": EXIT_TOOL_NAME, - "description": "Use this when tool mode is active but no remaining tool call is needed. The response field is forwarded to the user as the final answer.", + "description": "Use this when tool mode is active and no remaining tool call is needed. This is the valid way to exit tool mode with a final answer.", "parameters": { "type": "object", "properties": { "response": { "type": "string", - "description": "Final response to show the user." + "description": "Final response to show the user exactly as written." } }, "required": ["response"] @@ -554,7 +557,7 @@ fn append_exit_tool_reminder(body: &mut Value) { }; messages.push(json!({ "role": "system", - "content": "Tool mode is active. Use the most suitable tool when it helps complete the task. If no available tool is appropriate or the task is complete, call ExitTool with the final response instead of inventing another tool call.", + "content": "Tool mode is active. The user expects you to proactively execute the most suitable tool to help complete the task. Before invoking a tool, carefully evaluate whether it matches the current task. If no available tool is appropriate, or the task is complete, call ExitTool with the final response instead of inventing another tool call.", })); } @@ -818,6 +821,45 @@ mod tests { assert_eq!(converted["tool_choice"], "required"); } + #[test] + fn enters_tool_mode_when_tools_are_available() { + let request = json!({ + "messages": [{ "role": "user", "content": "use the right tool" }], + "tools": [{ + "name": "Skill", + "description": "Load a skill", + "input_schema": { "type": "object" } + }] + }); + + let converted = anthropic_to_openai_request(&request, &credential()).unwrap(); + let tool_names = converted["tools"] + .as_array() + .unwrap() + .iter() + .filter_map(|tool| { + tool.pointer("/function/name") + .and_then(|value| value.as_str()) + }) + .collect::>(); + let reminder = converted["messages"] + .as_array() + .unwrap() + .iter() + .find(|message| { + message.get("role").and_then(|value| value.as_str()) == Some("system") + && message + .get("content") + .and_then(|value| value.as_str()) + .is_some_and(|content| content.contains("Tool mode is active")) + }); + + assert_eq!(converted["tool_choice"], "required"); + assert!(tool_names.contains(&"Skill")); + assert!(tool_names.contains(&EXIT_TOOL_NAME)); + assert!(reminder.is_some()); + } + #[test] fn converts_exit_tool_response_to_final_text() { let request = json!({ "model": "claude-sonnet-4" }); diff --git a/apps/desktop/src-tauri/src/claude.rs b/apps/desktop/src-tauri/src/claude.rs index 9cd1ccb..1ae18d7 100644 --- a/apps/desktop/src-tauri/src/claude.rs +++ b/apps/desktop/src-tauri/src/claude.rs @@ -1560,6 +1560,9 @@ fn create_command( let venv_dir = std::path::Path::new(cwd).join(".venv"); if venv_dir.exists() { cmd.env("VIRTUAL_ENV", &venv_dir); + cmd.env("UV_PROJECT_ENVIRONMENT", &venv_dir); + cmd.env("PYTHONNOUSERSITE", "1"); + cmd.env("PIP_REQUIRE_VIRTUALENV", "true"); #[cfg(not(target_os = "windows"))] let venv_bin = venv_dir.join("bin"); #[cfg(target_os = "windows")] diff --git a/apps/desktop/src-tauri/src/uv.rs b/apps/desktop/src-tauri/src/uv.rs index 174f73a..9d52761 100644 --- a/apps/desktop/src-tauri/src/uv.rs +++ b/apps/desktop/src-tauri/src/uv.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use tauri::{Emitter, WebviewWindow}; use tokio::io::{AsyncBufReadExt, BufReader}; @@ -113,6 +113,28 @@ fn venv_python(venv_dir: &std::path::Path) -> PathBuf { } } +fn venv_pip(venv_dir: &std::path::Path) -> PathBuf { + #[cfg(not(target_os = "windows"))] + { + venv_bin_dir(venv_dir).join("pip") + } + #[cfg(target_os = "windows")] + { + venv_bin_dir(venv_dir).join("pip.exe") + } +} + +fn venv_pip_shim(venv_dir: &std::path::Path) -> PathBuf { + #[cfg(not(target_os = "windows"))] + { + venv_bin_dir(venv_dir).join("pip") + } + #[cfg(target_os = "windows")] + { + venv_bin_dir(venv_dir).join("pip.cmd") + } +} + fn path_with_venv(venv_dir: &std::path::Path) -> String { let bin = venv_bin_dir(venv_dir); let current = std::env::var("PATH").unwrap_or_default(); @@ -123,6 +145,73 @@ fn path_with_venv(venv_dir: &std::path::Path) -> String { format!("{}{}{}", bin.to_string_lossy(), sep, current) } +fn write_pip_shim(venv_dir: &Path) -> Result<(), String> { + let uv_bin = find_uv_binary().unwrap_or_else(|_| "uv".to_string()); + let shim_path = venv_pip_shim(venv_dir); + + #[cfg(target_os = "windows")] + { + let content = format!( + "@echo off\r\nset \"VIRTUAL_ENV={}\"\r\n\"{}\" pip %*\r\n", + venv_dir.to_string_lossy(), + uv_bin + ); + std::fs::write(&shim_path, &content) + .map_err(|e| format!("Failed to create pip shim: {}", e))?; + let pip3_path = venv_bin_dir(venv_dir).join("pip3.cmd"); + let _ = std::fs::write(pip3_path, content); + } + + #[cfg(not(target_os = "windows"))] + { + let content = format!( + "#!/bin/sh\nVIRTUAL_ENV=\"{}\" exec \"{}\" pip \"$@\"\n", + venv_dir.to_string_lossy(), + uv_bin + ); + std::fs::write(&shim_path, content) + .map_err(|e| format!("Failed to create pip shim: {}", e))?; + use std::os::unix::fs::PermissionsExt; + let mut perms = std::fs::metadata(&shim_path) + .map_err(|e| format!("Failed to stat pip shim: {}", e))? + .permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(&shim_path, perms) + .map_err(|e| format!("Failed to mark pip shim executable: {}", e))?; + } + + Ok(()) +} + +async fn ensure_venv_pip(venv_dir: &Path) -> Result<(), String> { + if venv_pip(venv_dir).exists() || venv_pip_shim(venv_dir).exists() { + return Ok(()); + } + + let python = venv_python(venv_dir); + if !python.exists() { + return Err(format!( + "Project .venv is missing Python at {}", + python.display() + )); + } + + let mut ensure_cmd = tokio::process::Command::new(&python); + ensure_cmd.args(["-m", "ensurepip", "--upgrade"]); + ensure_cmd.env("VIRTUAL_ENV", venv_dir); + ensure_cmd.env("PATH", path_with_venv(venv_dir)); + ensure_cmd.env("PYTHONNOUSERSITE", "1"); + #[cfg(target_os = "windows")] + { + ensure_cmd.creation_flags(CREATE_NO_WINDOW); + } + + match ensure_cmd.output().await { + Ok(output) if output.status.success() && venv_pip(venv_dir).exists() => Ok(()), + _ => write_pip_shim(venv_dir), + } +} + // ─── Tauri Commands ─── #[tauri::command] @@ -287,6 +376,7 @@ pub async fn setup_project_venv(project_path: String) -> Result Result/.venv let mut venv_cmd = tokio::process::Command::new(&uv_bin); - venv_cmd.args(["venv", &venv_dir.to_string_lossy()]); + let venv_arg = venv_dir.to_string_lossy().to_string(); + venv_cmd.args(["venv", "--seed", venv_arg.as_str()]); venv_cmd.current_dir(project); #[cfg(target_os = "windows")] { @@ -316,6 +407,7 @@ pub async fn setup_project_venv(project_path: String) -> Result ({ copyFileToProject: vi.fn(), - join: vi.fn(), - createPdfTextSidecar: vi.fn(), - isPdfPath: vi.fn((path: string) => path.toLowerCase().endsWith(".pdf")), })); vi.mock("@/lib/tauri/fs", () => ({ copyFileToProject: mocks.copyFileToProject, - join: mocks.join, -})); - -vi.mock("@/lib/pdf-text-extractor", () => ({ - createPdfTextSidecar: mocks.createPdfTextSidecar, - isPdfPath: mocks.isPdfPath, })); import { buildReferenceFilesSection, - importReferenceFilesWithSidecars, + importReferenceFiles, } from "@/lib/project-attachments"; describe("project attachment helpers", () => { beforeEach(() => { mocks.copyFileToProject.mockReset(); - mocks.join.mockReset(); - mocks.createPdfTextSidecar.mockReset(); - mocks.isPdfPath.mockClear(); }); - it("imports PDFs and creates extracted text sidecars", async () => { + it("imports PDFs without creating extracted text files", async () => { mocks.copyFileToProject.mockResolvedValueOnce("attachments/paper.pdf"); - mocks.join.mockResolvedValueOnce("C:/project/attachments/paper.pdf"); - mocks.createPdfTextSidecar.mockResolvedValueOnce({ - sidecarRelativePath: "attachments/paper.pdf.txt", - }); - const files = await importReferenceFilesWithSidecars("C:/project", [ + const files = await importReferenceFiles("C:/project", [ "C:/source/paper.pdf", ]); @@ -46,50 +30,23 @@ describe("project attachment helpers", () => { "C:/source/paper.pdf", "attachments/paper.pdf", ); - expect(mocks.createPdfTextSidecar).toHaveBeenCalledWith( - "C:/project", - "attachments/paper.pdf", - "C:/project/attachments/paper.pdf", - ); expect(files).toEqual([ { relativePath: "attachments/paper.pdf", - sidecarRelativePath: "attachments/paper.pdf.txt", }, ]); }); - it("keeps imported PDF references when sidecar extraction fails", async () => { - mocks.copyFileToProject.mockResolvedValueOnce("attachments/paper.pdf"); - mocks.join.mockResolvedValueOnce("C:/project/attachments/paper.pdf"); - mocks.createPdfTextSidecar.mockRejectedValueOnce( - new Error("cannot extract"), - ); - - const files = await importReferenceFilesWithSidecars("C:/project", [ - "C:/source/paper.pdf", - ]); - - expect(files[0]).toMatchObject({ - relativePath: "attachments/paper.pdf", - sidecarError: "cannot extract", - }); - }); - - it("builds a prompt section that points models at PDF sidecars", () => { + it("builds a prompt section that keeps PDF references as PDFs", () => { const section = buildReferenceFilesSection([ - { - relativePath: "attachments/paper.pdf", - sidecarRelativePath: "attachments/paper.pdf.txt", - }, + { relativePath: "attachments/paper.pdf" }, { relativePath: "attachments/data.csv" }, ]); expect(section).toContain("### Reference Files"); - expect(section).toContain( - "`attachments/paper.pdf` (extracted text: `attachments/paper.pdf.txt`)", - ); + expect(section).toContain("`attachments/paper.pdf` (PDF)"); expect(section).toContain("`attachments/data.csv`"); - expect(section).toContain("For PDFs, read the extracted text sidecar"); + expect(section).not.toContain("extracted text"); + expect(section).not.toContain(".pdf.txt"); }); }); diff --git a/apps/desktop/src/components/claude-chat/chat-composer.tsx b/apps/desktop/src/components/claude-chat/chat-composer.tsx index e5bf297..e9bf98b 100644 --- a/apps/desktop/src/components/claude-chat/chat-composer.tsx +++ b/apps/desktop/src/components/claude-chat/chat-composer.tsx @@ -48,7 +48,6 @@ import { } from "@/stores/claude-setup-store"; import { useDocumentStore, type ProjectFile } from "@/stores/document-store"; import { getUniqueTargetName } from "@/lib/tauri/fs"; -import { createPdfTextSidecar, isPdfPath } from "@/lib/pdf-text-extractor"; import { getProviderDisplayName, getProviderIconSrc, @@ -141,6 +140,10 @@ function cleanupTemporaryPinnedContext(context: PinnedContext) { void cleanupTemporaryFilePaths([context.filePath]); } +function isPdfPath(path: string) { + return path.toLowerCase().endsWith(".pdf"); +} + function getFileIcon(file: ProjectFile) { if (file.type === "image") return ; @@ -598,33 +601,7 @@ export const ChatComposer: FC<{ isOpen?: boolean }> = ({ isOpen }) => { }, [slashQuery !== null, projectRoot]); const buildPinnedContextForFile = useCallback( - async ( - file: ProjectFile, - ): Promise<{ context: PinnedContext; createdSidecar: boolean }> => { - if (projectRoot && file.type === "pdf") { - try { - const sidecar = await createPdfTextSidecar( - projectRoot, - file.relativePath, - file.absolutePath, - ); - - return { - context: { - label: `@${file.relativePath}`, - filePath: sidecar.sidecarRelativePath, - selectedText: sidecar.contextText, - }, - createdSidecar: true, - }; - } catch (err) { - log.error("Failed to extract PDF text", { - path: file.relativePath, - error: String(err), - }); - } - } - + async (file: ProjectFile): Promise => { const isTextFile = file.type === "tex" || file.type === "bib" || @@ -632,17 +609,14 @@ export const ChatComposer: FC<{ isOpen?: boolean }> = ({ isOpen }) => { file.type === "other"; return { - context: { - label: `@${file.relativePath}`, - filePath: file.relativePath, - selectedText: isTextFile - ? (file.content ?? "") - : `[Referenced file: ${file.relativePath} (${file.type} file)]`, - }, - createdSidecar: false, + label: `@${file.relativePath}`, + filePath: file.relativePath, + selectedText: isTextFile + ? (file.content ?? "") + : `[Referenced file: ${file.relativePath} (${file.type} file)]`, }; }, - [projectRoot], + [], ); const selectMention = useCallback( @@ -660,16 +634,13 @@ export const ChatComposer: FC<{ isOpen?: boolean }> = ({ isOpen }) => { setMentionQuery(null); // Pin the whole file as context - const { context, createdSidecar } = await buildPinnedContextForFile(file); - if (createdSidecar) { - await refreshFiles(); - } + const context = await buildPinnedContextForFile(file); setPinnedContexts((prev) => [...prev, context]); // Refocus textarea setTimeout(() => textarea.focus(), 0); }, - [buildPinnedContextForFile, input, refreshFiles], + [buildPinnedContextForFile, input], ); const selectSlashCommand = useCallback((command: SlashCommand) => { @@ -712,7 +683,6 @@ export const ChatComposer: FC<{ isOpen?: boolean }> = ({ isOpen }) => { // Pin each file as context const storeFiles = useDocumentStore.getState().files; const newContexts: PinnedContext[] = []; - let createdPdfSidecar = false; for (const relativePath of importedPaths) { const imported = storeFiles.find( @@ -720,10 +690,7 @@ export const ChatComposer: FC<{ isOpen?: boolean }> = ({ isOpen }) => { ); if (imported) { - const { context, createdSidecar } = - await buildPinnedContextForFile(imported); - createdPdfSidecar ||= createdSidecar; - newContexts.push(context); + newContexts.push(await buildPinnedContextForFile(imported)); } else { // File imported but type might be filtered out — still pin as reference newContexts.push({ @@ -735,10 +702,6 @@ export const ChatComposer: FC<{ isOpen?: boolean }> = ({ isOpen }) => { } if (newContexts.length > 0) { - if (createdPdfSidecar) { - await refreshFiles(); - } - setPinnedContexts((prev) => { // Deduplicate by label const existingLabels = new Set(prev.map((c) => c.label)); @@ -876,25 +839,10 @@ export const ChatComposer: FC<{ isOpen?: boolean }> = ({ isOpen }) => { const buffer = await file.arrayBuffer(); await writeFile(fullPath, new Uint8Array(buffer)); - let contextFilePath = uniqueName; let content: string; if (isPdfPath(uniqueName) || file.type === "application/pdf") { - try { - const sidecar = await createPdfTextSidecar( - projectRoot, - uniqueName, - fullPath, - ); - contextFilePath = sidecar.sidecarRelativePath; - content = sidecar.contextText; - } catch (err) { - log.error("Failed to extract pasted PDF text", { - fileName: uniqueName, - error: String(err), - }); - content = `[Attached file: ${uniqueName} (${file.type})]`; - } + content = `[Attached file: ${uniqueName} (PDF)]`; } else { // Determine if it's a text file const isText = file.type.startsWith("text/"); @@ -905,7 +853,7 @@ export const ChatComposer: FC<{ isOpen?: boolean }> = ({ isOpen }) => { newContexts.push({ label: `@${uniqueName}`, - filePath: contextFilePath, + filePath: uniqueName, selectedText: content, }); } catch (err) { diff --git a/apps/desktop/src/components/project-wizard.tsx b/apps/desktop/src/components/project-wizard.tsx index 934aa7e..cb81853 100644 --- a/apps/desktop/src/components/project-wizard.tsx +++ b/apps/desktop/src/components/project-wizard.tsx @@ -32,7 +32,7 @@ import { TemplateGallery } from "@/components/template-gallery"; import { DEFAULT_CLAUDE_MD } from "@/lib/default-claude-md"; import { buildReferenceFilesSection, - importReferenceFilesWithSidecars, + importReferenceFiles, } from "@/lib/project-attachments"; import { getProjectNameError, normalizeProjectName } from "@/lib/project-name"; @@ -247,7 +247,7 @@ function ScratchForm({ onBack }: { onBack: () => void }) { const referenceFiles = attachments.length > 0 - ? await importReferenceFilesWithSidecars(projectPath, attachments) + ? await importReferenceFiles(projectPath, attachments) : []; if (purpose.trim()) { diff --git a/apps/desktop/src/components/template-gallery/template-preview.tsx b/apps/desktop/src/components/template-gallery/template-preview.tsx index 49546a6..7b347a2 100644 --- a/apps/desktop/src/components/template-gallery/template-preview.tsx +++ b/apps/desktop/src/components/template-gallery/template-preview.tsx @@ -45,7 +45,7 @@ import type { PageSize } from "@/lib/mupdf/types"; import { createLogger } from "@/lib/debug/logger"; import { buildReferenceFilesSection, - importReferenceFilesWithSidecars, + importReferenceFiles, } from "@/lib/project-attachments"; import { getProjectNameError, normalizeProjectName } from "@/lib/project-name"; @@ -411,7 +411,7 @@ export function TemplatePreview() { const referenceFiles = attachments.length > 0 - ? await importReferenceFilesWithSidecars(projectPath, attachments) + ? await importReferenceFiles(projectPath, attachments) : []; if (purpose.trim()) { diff --git a/apps/desktop/src/lib/pdf-text-extractor.ts b/apps/desktop/src/lib/pdf-text-extractor.ts deleted file mode 100644 index fa096b6..0000000 --- a/apps/desktop/src/lib/pdf-text-extractor.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { join } from "@tauri-apps/api/path"; -import { readFile, writeTextFile } from "@tauri-apps/plugin-fs"; -import { getMupdfClient } from "@/lib/mupdf/mupdf-client"; -import type { StructuredTextData } from "@/lib/mupdf/types"; - -const PDF_CONTEXT_CHAR_LIMIT = 80_000; - -export interface PdfTextSidecar { - pageCount: number; - sidecarRelativePath: string; - sidecarAbsolutePath: string; - sidecarContent: string; - contextText: string; -} - -export function isPdfPath(path: string): boolean { - return path.toLowerCase().endsWith(".pdf"); -} - -function structuredTextToPlainText(data: StructuredTextData): string { - const lines: string[] = []; - - for (const block of data.blocks || []) { - if (block.type !== "text") continue; - - let addedBlockLine = false; - for (const line of block.lines || []) { - const text = line.text?.trimEnd(); - if (!text) continue; - - lines.push(text); - addedBlockLine = true; - } - - if (addedBlockLine) { - lines.push(""); - } - } - - return lines.join("\n").trim(); -} - -async function extractPdfText(absolutePath: string): Promise<{ - pageCount: number; - pages: string[]; -}> { - const bytes = await readFile(absolutePath); - const buffer = new Uint8Array(bytes).buffer; - const client = getMupdfClient(); - const docId = await client.openDocument(buffer, "application/pdf"); - - try { - const pageCount = await client.countPages(docId); - const pages: string[] = []; - - for (let pageIndex = 0; pageIndex < pageCount; pageIndex++) { - const structuredText = await client.getPageText(docId, pageIndex); - pages.push(structuredTextToPlainText(structuredText)); - } - - return { pageCount, pages }; - } finally { - await client.closeDocument(docId).catch(() => {}); - } -} - -function buildSidecarContent( - pdfRelativePath: string, - pageCount: number, - pages: string[], -): string { - const body = pages - .map((text, index) => { - const pageText = text.trim() || "[No extractable text on this page]"; - return `## Page ${index + 1}\n\n${pageText}`; - }) - .join("\n\n---\n\n"); - - return [ - `# Extracted PDF Text: ${pdfRelativePath}`, - "", - `Pages: ${pageCount}`, - "", - body, - "", - ].join("\n"); -} - -function buildContextText( - pdfRelativePath: string, - sidecarRelativePath: string, - pageCount: number, - sidecarContent: string, -): string { - const truncated = - sidecarContent.length > PDF_CONTEXT_CHAR_LIMIT - ? `${sidecarContent.slice(0, PDF_CONTEXT_CHAR_LIMIT)}\n\n[Truncated for chat context. Full extracted text is available at ${sidecarRelativePath}.]` - : sidecarContent; - - return [ - `[PDF attachment: ${pdfRelativePath}]`, - `[ClaudePrism extracted ${pageCount} page(s) with built-in MuPDF.]`, - `[Use this extracted text first. If you need more, read ${sidecarRelativePath}; do not rely on the raw PDF reader unless Poppler is installed.]`, - "", - truncated, - ].join("\n"); -} - -export async function createPdfTextSidecar( - projectRoot: string, - pdfRelativePath: string, - pdfAbsolutePath: string, -): Promise { - const { pageCount, pages } = await extractPdfText(pdfAbsolutePath); - const sidecarRelativePath = `${pdfRelativePath}.txt`; - const sidecarAbsolutePath = await join(projectRoot, sidecarRelativePath); - const sidecarContent = buildSidecarContent(pdfRelativePath, pageCount, pages); - - await writeTextFile(sidecarAbsolutePath, sidecarContent); - - return { - pageCount, - sidecarRelativePath, - sidecarAbsolutePath, - sidecarContent, - contextText: buildContextText( - pdfRelativePath, - sidecarRelativePath, - pageCount, - sidecarContent, - ), - }; -} diff --git a/apps/desktop/src/lib/project-attachments.ts b/apps/desktop/src/lib/project-attachments.ts index 4beaa65..2c0e650 100644 --- a/apps/desktop/src/lib/project-attachments.ts +++ b/apps/desktop/src/lib/project-attachments.ts @@ -1,17 +1,18 @@ -import { createPdfTextSidecar, isPdfPath } from "@/lib/pdf-text-extractor"; -import { copyFileToProject, join } from "@/lib/tauri/fs"; +import { copyFileToProject } from "@/lib/tauri/fs"; export interface ImportedReferenceFile { relativePath: string; - sidecarRelativePath?: string; - sidecarError?: string; } function baseName(path: string): string { return path.split(/[/\\]/).pop() || path; } -export async function importReferenceFilesWithSidecars( +function isPdfPath(path: string): boolean { + return path.toLowerCase().endsWith(".pdf"); +} + +export async function importReferenceFiles( projectRoot: string, sourcePaths: string[], targetFolder = "attachments", @@ -26,22 +27,6 @@ export async function importReferenceFilesWithSidecars( targetName, ); const reference: ImportedReferenceFile = { relativePath }; - - if (isPdfPath(relativePath)) { - try { - const absolutePath = await join(projectRoot, relativePath); - const sidecar = await createPdfTextSidecar( - projectRoot, - relativePath, - absolutePath, - ); - reference.sidecarRelativePath = sidecar.sidecarRelativePath; - } catch (err) { - reference.sidecarError = - err instanceof Error ? err.message : String(err); - } - } - imported.push(reference); } @@ -54,11 +39,8 @@ export function buildReferenceFilesSection( if (references.length === 0) return ""; const lines = references.map((reference) => { - if (reference.sidecarRelativePath) { - return `- \`${reference.relativePath}\` (extracted text: \`${reference.sidecarRelativePath}\`)`; - } if (isPdfPath(reference.relativePath)) { - return `- \`${reference.relativePath}\` (PDF; extracted text sidecar was not generated)`; + return `- \`${reference.relativePath}\` (PDF)`; } return `- \`${reference.relativePath}\``; }); @@ -68,7 +50,7 @@ export function buildReferenceFilesSection( "### Reference Files", lines.join("\n"), "", - "Please review them and incorporate relevant information. For PDFs, read the extracted text sidecar when one is listed instead of relying on the raw PDF file.", + "Please review them and incorporate relevant information.", "", ].join("\n"); }