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:
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user