refactor(coding-agent): removed automatic file attachment for non-image paste paths

- Removed `onPasteFilePath` handler to prevent automatic background file attachment when pasting paths.
- Updated `CustomEditor` to treat non-image paths as literal text input.
- Cleaned up unused file system utilities and paste path resolution logic.
This commit is contained in:
can1357
2026-06-25 12:57:54 +02:00
parent c03a78917a
commit 57e9848c8c
4 changed files with 13 additions and 124 deletions
@@ -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<string>();
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);
});
});
@@ -318,8 +318,6 @@ export class CustomEditor extends Editor {
onPasteImage?: () => Promise<boolean>;
/** Called when a bracketed paste contains one or more image-file paths. */
onPasteImagePath?: (path: string) => void | Promise<void>;
/** Called when a bracketed paste contains one or more non-image file paths. */
onPasteFilePath?: (path: string) => void | Promise<void>;
/** 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);
@@ -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 `<attachment>\n${content}\n</attachment>`;
}
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<void> {
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<void> {
try {
const image = await loadImageInput({
@@ -1462,29 +1414,6 @@ export class InputController {
this.ctx.ui.requestRender();
}
async #attachExistingFileAsLocal(sourcePath: string): Promise<string> {
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
@@ -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");
});
});