diff --git a/packages/coding-agent/src/modes/components/custom-editor.test.ts b/packages/coding-agent/src/modes/components/custom-editor.test.ts index 13a3b4a5b..e56121d96 100644 --- a/packages/coding-agent/src/modes/components/custom-editor.test.ts +++ b/packages/coding-agent/src/modes/components/custom-editor.test.ts @@ -93,15 +93,17 @@ describe("CustomEditor bracketed path paste", () => { expect(extractBracketedImagePastePaths(bracketedPaste("/tmp/report.csv"))).toBeUndefined(); }); - it("routes non-image path pastes through the file-path hook", async () => { + it("inserts non-image path pastes as literal text instead of attaching them", () => { const { editor } = makeEditor(); - const pasted = Promise.withResolvers(); - editor.onPasteFilePath = path => pasted.resolve(path); + let imagePathCalls = 0; + editor.onPasteImagePath = () => { + imagePathCalls++; + }; editor.handleInput(bracketedPaste("/tmp/report.csv")); - expect(await pasted.promise).toBe("/tmp/report.csv"); - expect(editor.getText()).toBe(""); + expect(editor.getText()).toBe("/tmp/report.csv"); + expect(imagePathCalls).toBe(0); }); }); diff --git a/packages/coding-agent/src/modes/components/custom-editor.ts b/packages/coding-agent/src/modes/components/custom-editor.ts index 1e7beca30..9c9235139 100644 --- a/packages/coding-agent/src/modes/components/custom-editor.ts +++ b/packages/coding-agent/src/modes/components/custom-editor.ts @@ -318,8 +318,6 @@ export class CustomEditor extends Editor { onPasteImage?: () => Promise; /** Called when a bracketed paste contains one or more image-file paths. */ onPasteImagePath?: (path: string) => void | Promise; - /** Called when a bracketed paste contains one or more non-image file paths. */ - onPasteFilePath?: (path: string) => void | Promise; /** Called when the configured raw text-paste shortcut is pressed. */ onPasteTextRaw?: () => void; /** Called when the configured dequeue shortcut is pressed. */ @@ -505,20 +503,12 @@ export class CustomEditor extends Editor { return; } - const pastedPaths = extractBracketedPastePaths(data); - if (pastedPaths) { - const canHandlePaths = pastedPaths.every(path => - isImagePath(path) ? this.onPasteImagePath !== undefined : this.onPasteFilePath !== undefined, - ); - if (canHandlePaths) { - void (async () => { - for (const path of pastedPaths) { - if (isImagePath(path)) await this.onPasteImagePath?.(path); - else await this.onPasteFilePath?.(path); - } - })(); - return; - } + const pastedImagePaths = extractBracketedImagePastePaths(data); + if (pastedImagePaths && this.onPasteImagePath) { + void (async () => { + for (const path of pastedImagePaths) await this.onPasteImagePath?.(path); + })(); + return; } const parsedKey = parseKey(data); diff --git a/packages/coding-agent/src/modes/controllers/input-controller.ts b/packages/coding-agent/src/modes/controllers/input-controller.ts index bf8a31b8e..063fb0a1f 100644 --- a/packages/coding-agent/src/modes/controllers/input-controller.ts +++ b/packages/coding-agent/src/modes/controllers/input-controller.ts @@ -1,6 +1,5 @@ import * as fs from "node:fs/promises"; import * as path from "node:path"; -import { fileURLToPath } from "node:url"; import type { ImageContent } from "@oh-my-pi/pi-ai"; import { type AutocompleteProvider, matchesKey, type SlashCommand } from "@oh-my-pi/pi-tui"; import { $env, isEnoent, logger, sanitizeText } from "@oh-my-pi/pi-utils"; @@ -20,7 +19,6 @@ import { isTinyTitleLocalModelKey } from "../../tiny/models"; import { isLowSignalTitleInput } from "../../tiny/text"; import { tinyTitleClient } from "../../tiny/title-client"; import type { TinyTitleProgressEvent } from "../../tiny/title-protocol"; -import { resolveReadPath } from "../../tools/path-utils"; import { shortenPath, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils"; import { copyToClipboard, readImageFromClipboard, readTextFromClipboard } from "../../utils/clipboard"; import { EnhancedPasteController } from "../../utils/enhanced-paste"; @@ -104,20 +102,6 @@ function wrapPasteInAttachmentBlock(content: string): string { return `\n${content}\n`; } -const FILE_URI_REGEX = /^file:\/\//i; - -function pastedFileAttachmentExtension(sourcePath: string): string { - const ext = path.extname(sourcePath); - const bareExt = ext.slice(1); - if (!bareExt || bareExt.length > 32 || !/^[a-z0-9][a-z0-9._-]*$/i.test(bareExt)) return ""; - return ext; -} - -function resolvePastedFilePath(filePath: string, cwd: string): string { - if (FILE_URI_REGEX.test(filePath)) return fileURLToPath(filePath); - return resolveReadPath(filePath, cwd); -} - const TINY_TITLE_PROGRESS_DONE_TTL_MS = 3_000; // A cached model fires its file-load events in a short burst and then goes silent // while onnxruntime builds the session; a genuine download keeps streaming progress @@ -392,7 +376,6 @@ export class InputController { ); this.ctx.editor.onPasteImage = () => this.handleImagePaste(); this.ctx.editor.onPasteImagePath = path => this.handleImagePathPaste(path); - this.ctx.editor.onPasteFilePath = path => this.handleFilePathPaste(path); this.ctx.editor.setActionKeys( "app.clipboard.pasteTextRaw", this.ctx.keybindings.getKeys("app.clipboard.pasteTextRaw"), @@ -1261,37 +1244,6 @@ export class InputController { } } - async handleFilePathPaste(filePath: string): Promise { - try { - const resolvedPath = resolvePastedFilePath(filePath, this.ctx.sessionManager.getCwd()); - const stat = await Bun.file(resolvedPath).stat(); - if (!stat.isFile()) { - this.ctx.editor.pasteText(filePath); - this.ctx.ui.requestRender(); - this.ctx.showStatus("Pasted path is not a file"); - return; - } - - const reference = await this.#attachExistingFileAsLocal(resolvedPath); - this.ctx.editor.insertText(`${reference} `); - this.ctx.ui.requestRender(); - this.ctx.showStatus(`Attached file as ${reference}`); - } catch (error) { - if (isEnoent(error)) { - this.ctx.editor.pasteText(filePath); - this.ctx.ui.requestRender(); - this.ctx.showStatus("Pasted file path was not found"); - return; - } - logger.warn("failed to attach pasted file path", { - error: error instanceof Error ? error.message : String(error), - }); - this.ctx.editor.pasteText(filePath); - this.ctx.ui.requestRender(); - this.ctx.showError("Failed to attach pasted file path — pasted path inline instead"); - } - } - async handleImagePathPaste(path: string): Promise { try { const image = await loadImageInput({ @@ -1462,29 +1414,6 @@ export class InputController { this.ctx.ui.requestRender(); } - async #attachExistingFileAsLocal(sourcePath: string): Promise { - const localRoot = resolveLocalRoot({ - getArtifactsDir: () => this.ctx.sessionManager.getArtifactsDir(), - getSessionId: () => this.ctx.sessionManager.getSessionId(), - }); - await fs.mkdir(localRoot, { recursive: true }); - const ext = pastedFileAttachmentExtension(sourcePath); - let name: string; - let filePath: string; - do { - this.#attachmentCounter++; - name = `attachment-${this.#attachmentCounter}${ext}`; - filePath = path.join(localRoot, name); - } while (await Bun.file(filePath).exists()); - - try { - await fs.link(sourcePath, filePath); - } catch { - await fs.copyFile(sourcePath, filePath); - } - return `local://${name}`; - } - /** * Save a large paste to the session's `local://` store and insert a clean `local://attachment-N` * reference into the editor so the agent can `read` it on demand — instead of inlining the text or diff --git a/packages/coding-agent/test/input-controller-large-paste.test.ts b/packages/coding-agent/test/input-controller-large-paste.test.ts index d20e80af5..355607c75 100644 --- a/packages/coding-agent/test/input-controller-large-paste.test.ts +++ b/packages/coding-agent/test/input-controller-large-paste.test.ts @@ -149,36 +149,4 @@ describe("InputController.presentLargePasteMenu file attachment", () => { expect(await Bun.file(path.join(dir, "local", "attachment-1")).text()).toBe("previous"); expect(await Bun.file(path.join(dir, "local", "attachment-2")).text()).toBe("fresh"); }); - - it("attaches an existing pasted file as a local:// reference with its extension", async () => { - dir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-file-paste-test-")); - const sourceDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-file-paste-source-")); - const sourcePath = path.join(sourceDir, "report.csv"); - await Bun.write(sourcePath, "name,value\nalpha,1\n"); - const { controller, spies } = createContext({ artifactsDir: dir }); - - try { - await controller.handleFilePathPaste(sourcePath); - } finally { - await fs.rm(sourceDir, { recursive: true, force: true }); - } - - expect(spies.insertText).toHaveBeenCalledWith("local://attachment-1.csv "); - expect(spies.pasteText).not.toHaveBeenCalled(); - expect(spies.requestRender).toHaveBeenCalled(); - expect(spies.showStatus).toHaveBeenCalledWith("Attached file as local://attachment-1.csv"); - expect(await Bun.file(path.join(dir, "local", "attachment-1.csv")).text()).toBe("name,value\nalpha,1\n"); - }); - - it("falls back to inline text when the pasted file path does not exist", async () => { - dir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-file-paste-test-")); - const missing = path.join(dir, "missing.txt"); - const { controller, spies } = createContext({ artifactsDir: dir }); - - await controller.handleFilePathPaste(missing); - - expect(spies.insertText).not.toHaveBeenCalled(); - expect(spies.pasteText).toHaveBeenCalledWith(missing); - expect(spies.showStatus).toHaveBeenCalledWith("Pasted file path was not found"); - }); });