diff --git a/packages/coding-agent/docs/keybindings.md b/packages/coding-agent/docs/keybindings.md index eb25606b3..440c130ec 100644 --- a/packages/coding-agent/docs/keybindings.md +++ b/packages/coding-agent/docs/keybindings.md @@ -125,7 +125,7 @@ In fullscreen mode, these actions control the transcript and take precedence ove | `app.exit` | `ctrl+d` | Exit (when editor empty) | | `app.suspend` | `ctrl+z` (None on Windows) | Suspend to background | | `app.editor.external` | `ctrl+g` | Open in external editor (`externalEditor`, `$VISUAL`, `$EDITOR`, Notepad on Windows, or `nano` elsewhere) | -| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on Windows and WSL) | Paste image or text from clipboard | +| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on Windows and WSL) | Paste files on macOS, images, or text from clipboard | On native Windows, `app.suspend` has no default because Windows terminals do not support Unix job control. If you assign it manually, Pi shows a status message instead of suspending. WSL uses the normal `ctrl+z` and `fg` behavior. diff --git a/packages/coding-agent/src/core/keybindings.ts b/packages/coding-agent/src/core/keybindings.ts index 9136b07b9..f67cbe5e3 100644 --- a/packages/coding-agent/src/core/keybindings.ts +++ b/packages/coding-agent/src/core/keybindings.ts @@ -141,7 +141,7 @@ export const KEYBINDINGS = { }, "app.clipboard.pasteImage": { defaultKeys: windowsKeybindings ? "alt+v" : "ctrl+v", - description: "Paste image from clipboard (text fallback)", + description: "Paste files on macOS, images, or text from clipboard", }, "app.session.new": { defaultKeys: [], description: "Start a new session" }, "app.session.tree": { defaultKeys: [], description: "Open session tree" }, diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 01e254b29..607eec41e 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -117,7 +117,7 @@ import type { TruncationResult } from "../../core/tools/truncate.ts"; import { hasTrustRequiringProjectResources, ProjectTrustStore } from "../../core/trust-manager.ts"; import { getUsageCostBreakdown } from "../../core/usage-totals.ts"; import { getChangelogPath, getNewEntries, normalizeChangelogLinks, parseChangelog } from "../../utils/changelog.ts"; -import { copyToClipboard, readClipboardText } from "../../utils/clipboard.ts"; +import { copyToClipboard, readClipboardFilePaths, readClipboardText } from "../../utils/clipboard.ts"; import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.ts"; import { parseGitUrl } from "../../utils/git.ts"; import { getCwdRelativePath } from "../../utils/paths.ts"; @@ -200,6 +200,10 @@ interface Expandable { setExpanded(expanded: boolean): void; } +interface ActiveEditor extends EditorComponent { + getCursor?(): { line: number; col: number }; +} + interface WorkingStatusEditor extends EditorComponent { readonly embedWorkingStatus: boolean; setWorkingStatusIndicator(indicator: StatusIndicator | undefined): void; @@ -428,7 +432,7 @@ export class InteractiveMode { private pendingMessagesContainer: Container; private statusContainer: Container; private defaultEditor: CustomEditor; - private editor: EditorComponent; + private editor: ActiveEditor; private editorComponentFactory: EditorFactory | undefined; private autocompleteProvider: AutocompleteProvider | undefined; private autocompleteProviderWrappers: AutocompleteProviderFactory[] = []; @@ -994,7 +998,7 @@ export class InteractiveMode { rawKeyHint("!!", "to run bash (no context)"), hint("app.message.followUp", "to queue follow-up"), hint("app.message.dequeue", "to edit all queued messages"), - hint("app.clipboard.pasteImage", "to paste image (with text fallback)"), + hint("app.clipboard.pasteImage", "to paste files on macOS, images, or text"), rawKeyHint("drop files", "to attach"), ].join("\n"); const compactInstructions = () => @@ -3017,8 +3021,8 @@ export class InteractiveMode { } }; - // Handle clipboard paste (triggered on Ctrl+V). Images are attached by path; - // otherwise, paste plain text from the system clipboard. + // Handle clipboard paste (triggered on Ctrl+V). Copied files use their original paths, + // images are attached via temporary files, and plain text is the final fallback. this.defaultEditor.onPasteImage = () => { void this.handleClipboardPaste(); }; @@ -3040,6 +3044,23 @@ export class InteractiveMode { private async handleClipboardPaste(): Promise { try { + const filePaths = await readClipboardFilePaths(); + if (filePaths) { + if (filePaths.some((filePath) => /\p{Cc}/u.test(filePath))) { + throw new Error("Clipboard file path contains control characters"); + } + const paths = this.isBashMode ? filePaths.map(quoteIfNeeded).join(" ") : filePaths.join("\n"); + const cursor = this.editor.getCursor?.(); + const currentLine = cursor ? (this.editor.getText().split("\n")[cursor.line] ?? "") : ""; + const characterBeforeCursor = cursor && cursor.col > 0 ? currentLine[cursor.col - 1] : ""; + const characterAfterCursor = cursor ? currentLine[cursor.col] : ""; + const leadingSpace = characterBeforeCursor && !/\s/.test(characterBeforeCursor) ? " " : ""; + const trailingSpace = characterAfterCursor && !/\s/.test(characterAfterCursor) ? " " : ""; + this.editor.insertTextAtCursor?.(`${leadingSpace}${paths}${trailingSpace}`); + this.ui.requestRender(); + return; + } + const image = await readClipboardImage(); if (image) { const tmpDir = os.tmpdir(); @@ -3058,8 +3079,8 @@ export class InteractiveMode { this.editor.insertTextAtCursor?.(text); this.ui.requestRender(); } - } catch { - // Silently ignore clipboard errors (may not have permission, etc.) + } catch (error) { + this.showError(`Failed to paste from clipboard: ${error instanceof Error ? error.message : String(error)}`); } } @@ -6636,7 +6657,7 @@ export class InteractiveMode { | \`${copyMessage}\` | Copy selection or last assistant message | | \`${followUp}\` | Queue follow-up message | | \`${dequeue}\` | Restore queued messages | -| \`${pasteImage}\` | Paste image or text from clipboard | +| \`${pasteImage}\` | Paste files on macOS, images, or text from clipboard | | \`/\` | Slash commands | | \`!\` | Run bash command | | \`!!\` | Run bash command (excluded from context) | diff --git a/packages/coding-agent/src/utils/clipboard.ts b/packages/coding-agent/src/utils/clipboard.ts index e14111794..926d9f9f9 100644 --- a/packages/coding-agent/src/utils/clipboard.ts +++ b/packages/coding-agent/src/utils/clipboard.ts @@ -71,6 +71,12 @@ export async function readClipboardText(): Promise { } } +/** Read file paths, such as Finder file copies, from the native clipboard. */ +export async function readClipboardFilePaths(): Promise { + const paths = await getNativeClipboard()?.getFilePaths?.(); + return paths?.length ? paths : null; +} + export async function copyToClipboard(text: string): Promise { const p = platform(); const env = process.env; diff --git a/packages/coding-agent/test/clipboard-image-native-errors.test.ts b/packages/coding-agent/test/clipboard-image-native-errors.test.ts index ef64da4dc..1ef9bd949 100644 --- a/packages/coding-agent/test/clipboard-image-native-errors.test.ts +++ b/packages/coding-agent/test/clipboard-image-native-errors.test.ts @@ -11,7 +11,11 @@ vi.mock("@earendil-works/pi-tui", async (importOriginal) => ({ }, }), })); -vi.mock("../src/utils/clipboard.ts", () => ({ copyToClipboard: vi.fn(), readClipboardText })); +vi.mock("../src/utils/clipboard.ts", () => ({ + copyToClipboard: vi.fn(), + readClipboardFilePaths: vi.fn(async () => null), + readClipboardText, +})); vi.mock("../src/utils/clipboard-command.ts", () => ({ runClipboardCommand: vi.fn(async () => undefined) })); afterEach(() => vi.unstubAllEnvs()); @@ -19,6 +23,7 @@ test("native image errors abort paste without reading text or changing the edito vi.stubEnv("TERMUX_VERSION", ""); const context = { editor: { insertTextAtCursor: vi.fn() }, + showError: vi.fn(), ui: { requestRender: vi.fn() }, }; const prototype = InteractiveMode.prototype as unknown as { @@ -28,4 +33,7 @@ test("native image errors abort paste without reading text or changing the edito expect(readClipboardText).not.toHaveBeenCalled(); expect(context.editor.insertTextAtCursor).not.toHaveBeenCalled(); expect(context.ui.requestRender).not.toHaveBeenCalled(); + expect(context.showError).toHaveBeenCalledExactlyOnceWith( + "Failed to paste from clipboard: Native clipboard operation failed", + ); }); diff --git a/packages/coding-agent/test/clipboard-paste-file-paths.test.ts b/packages/coding-agent/test/clipboard-paste-file-paths.test.ts new file mode 100644 index 000000000..fb141fb20 --- /dev/null +++ b/packages/coding-agent/test/clipboard-paste-file-paths.test.ts @@ -0,0 +1,141 @@ +import { beforeEach, expect, test, vi } from "vitest"; +import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts"; + +const mocks = vi.hoisted(() => ({ + readClipboardFilePaths: vi.fn<() => Promise>(), + readClipboardImage: vi.fn<() => Promise<{ bytes: Uint8Array; mimeType: string } | null>>(), + readClipboardText: vi.fn<() => Promise>(), +})); + +vi.mock("../src/utils/clipboard.ts", () => ({ + copyToClipboard: vi.fn(), + readClipboardFilePaths: mocks.readClipboardFilePaths, + readClipboardText: mocks.readClipboardText, +})); +vi.mock("../src/utils/clipboard-image.ts", () => ({ + extensionForImageMimeType: () => "png", + readClipboardImage: mocks.readClipboardImage, +})); + +beforeEach(() => vi.resetAllMocks()); + +test("Finder file paths take precedence over their icon image", async () => { + // Regression test for #9999. + const filePaths = ["/tmp/screenshot.png", "/tmp/My Photos/photo.png"]; + mocks.readClipboardFilePaths.mockResolvedValue(filePaths); + mocks.readClipboardImage.mockResolvedValue({ + bytes: new Uint8Array([0x89, 0x50, 0x4e, 0x47]), + mimeType: "image/png", + }); + mocks.readClipboardText.mockResolvedValue(null); + const insertTextAtCursor = vi.fn<(text: string) => void>(); + const context = { + editor: { insertTextAtCursor }, + showError: vi.fn(), + ui: { requestRender: vi.fn() }, + }; + const prototype = InteractiveMode.prototype as unknown as { + handleClipboardPaste(this: typeof context): Promise; + }; + + await prototype.handleClipboardPaste.call(context); + expect(insertTextAtCursor).toHaveBeenCalledExactlyOnceWith(filePaths.join("\n")); + expect(mocks.readClipboardImage).not.toHaveBeenCalled(); +}); + +test("clipboard file paths containing terminal control characters are rejected", async () => { + const insertTextAtCursor = vi.fn<(text: string) => void>(); + const showError = vi.fn<(message: string) => void>(); + mocks.readClipboardFilePaths.mockResolvedValue(["/tmp/photo\x1b]0;unsafe\x07.png"]); + const context = { + editor: { insertTextAtCursor }, + showError, + ui: { requestRender: vi.fn() }, + }; + const prototype = InteractiveMode.prototype as unknown as { + handleClipboardPaste(this: typeof context): Promise; + }; + + await prototype.handleClipboardPaste.call(context); + + expect(insertTextAtCursor).not.toHaveBeenCalled(); + expect(mocks.readClipboardImage).not.toHaveBeenCalled(); + expect(showError).toHaveBeenCalledExactlyOnceWith( + "Failed to paste from clipboard: Clipboard file path contains control characters", + ); +}); + +test("native file-path errors are shown without falling through to the icon image", async () => { + const showError = vi.fn<(message: string) => void>(); + mocks.readClipboardFilePaths.mockRejectedValue(new Error("Native clipboard file read failed")); + const context = { + editor: { insertTextAtCursor: vi.fn() }, + showError, + ui: { requestRender: vi.fn() }, + }; + const prototype = InteractiveMode.prototype as unknown as { + handleClipboardPaste(this: typeof context): Promise; + }; + + await prototype.handleClipboardPaste.call(context); + + expect(context.editor.insertTextAtCursor).not.toHaveBeenCalled(); + expect(mocks.readClipboardImage).not.toHaveBeenCalled(); + expect(showError).toHaveBeenCalledExactlyOnceWith( + "Failed to paste from clipboard: Native clipboard file read failed", + ); +}); + +test.each([ + ["punctuation", "Review:", 7], + ["Unicode text", "確認", 2], +])("clipboard file paths are separated from preceding %s", async (_description, editorText, cursorCol) => { + const insertTextAtCursor = vi.fn<(text: string) => void>(); + mocks.readClipboardFilePaths.mockResolvedValue(["/tmp/photo.png"]); + const context = { + editor: { + getCursor: () => ({ line: 0, col: cursorCol }), + getText: () => editorText, + insertTextAtCursor, + }, + showError: vi.fn(), + ui: { requestRender: vi.fn() }, + }; + const prototype = InteractiveMode.prototype as unknown as { + handleClipboardPaste(this: typeof context): Promise; + }; + + await prototype.handleClipboardPaste.call(context); + + expect(insertTextAtCursor).toHaveBeenCalledExactlyOnceWith(" /tmp/photo.png"); + expect(mocks.readClipboardImage).not.toHaveBeenCalled(); +}); + +test("bash mode shell-quotes file paths and inserts them as arguments", async () => { + const insertTextAtCursor = vi.fn<(text: string) => void>(); + mocks.readClipboardFilePaths.mockResolvedValue([ + "/tmp/My Photos/photo.png", + "/tmp/$(touch hacked).png", + "/tmp/plain.png", + ]); + const context = { + editor: { + getCursor: () => ({ line: 0, col: 3 }), + getText: () => "catDEST", + insertTextAtCursor, + }, + isBashMode: true, + showError: vi.fn(), + ui: { requestRender: vi.fn() }, + }; + const prototype = InteractiveMode.prototype as unknown as { + handleClipboardPaste(this: typeof context): Promise; + }; + + await prototype.handleClipboardPaste.call(context); + + expect(insertTextAtCursor).toHaveBeenCalledExactlyOnceWith( + " '/tmp/My Photos/photo.png' '/tmp/$(touch hacked).png' /tmp/plain.png ", + ); + expect(mocks.readClipboardImage).not.toHaveBeenCalled(); +}); diff --git a/packages/coding-agent/test/interactive-tui.test.ts b/packages/coding-agent/test/interactive-tui.test.ts index 63ce7d35a..b9b18f8b2 100644 --- a/packages/coding-agent/test/interactive-tui.test.ts +++ b/packages/coding-agent/test/interactive-tui.test.ts @@ -21,6 +21,7 @@ import { initTheme } from "../src/modes/interactive/theme/theme.ts"; const clipboardMocks = vi.hoisted(() => ({ copyToClipboard: vi.fn<(text: string) => Promise>(), + readClipboardFilePaths: vi.fn<() => Promise>(), readClipboardText: vi.fn<() => Promise>(), })); diff --git a/packages/tui/native/clipboard.h b/packages/tui/native/clipboard.h index 0987b4ed6..4e7d0a70f 100644 --- a/packages/tui/native/clipboard.h +++ b/packages/tui/native/clipboard.h @@ -7,8 +7,17 @@ #include #endif -typedef enum { CLIPBOARD_TEXT, CLIPBOARD_IMAGE, CLIPBOARD_WRITE } clipboard_operation; -typedef enum { CLIPBOARD_UNAVAILABLE, CLIPBOARD_EMPTY, CLIPBOARD_UTF8, CLIPBOARD_LATIN1, CLIPBOARD_UTF16, CLIPBOARD_BUFFER } clipboard_format; +typedef enum { CLIPBOARD_TEXT, CLIPBOARD_IMAGE, CLIPBOARD_WRITE, CLIPBOARD_FILES } clipboard_operation; +// CLIPBOARD_PATHS stores concatenated, NUL-terminated UTF-8 paths in clipboard_job.data. +typedef enum { + CLIPBOARD_UNAVAILABLE, + CLIPBOARD_EMPTY, + CLIPBOARD_UTF8, + CLIPBOARD_LATIN1, + CLIPBOARD_UTF16, + CLIPBOARD_BUFFER, + CLIPBOARD_PATHS +} clipboard_format; typedef struct { napi_async_work work; @@ -63,6 +72,33 @@ static void PI_NAPI_CALL execute_clipboard_work(napi_env env, void* data) { clipboard_execute(data); } +static int create_clipboard_paths_value( + napi_env env, + const clipboard_job* job, + napi_create_string_utf8_fn create_string, + napi_value* result +) { + napi_create_array_fn create_array = (napi_create_array_fn)node_symbol("napi_create_array"); + napi_set_element_fn set_element = (napi_set_element_fn)node_symbol("napi_set_element"); + if (!create_array || !set_element || create_array(env, result) != 0) return 1; + + const char* cursor = job->data; + const char* end = cursor + job->length; + for (uint32_t index = 0; cursor < end; index++) { + const char* terminator = cursor; + while (terminator < end && *terminator != '\0') terminator++; + if (terminator == end) return 1; + + napi_value item = 0; + if (create_string(env, cursor, (size_t)(terminator - cursor), &item) != 0 || + set_element(env, *result, index, item) != 0) { + return 1; + } + cursor = terminator + 1; + } + return 0; +} + static void PI_NAPI_CALL complete_clipboard_work(napi_env env, int status, void* data) { clipboard_job* job = data; napi_create_string_utf8_fn create_string = (napi_create_string_utf8_fn)node_symbol("napi_create_string_utf8"); @@ -71,6 +107,8 @@ static void PI_NAPI_CALL complete_clipboard_work(napi_env env, int status, void* if (!job->error && job->operation != CLIPBOARD_WRITE) { if (job->format == CLIPBOARD_EMPTY) { result = null_value(env); + } else if (job->format == CLIPBOARD_PATHS) { + status = create_clipboard_paths_value(env, job, create_string, &result); } else if (job->format == CLIPBOARD_BUFFER) { napi_create_buffer_copy_fn create_buffer = (napi_create_buffer_copy_fn)node_symbol("napi_create_buffer_copy"); status = create_buffer(env, job->length, job->data, 0, &result); diff --git a/packages/tui/native/darwin/README.md b/packages/tui/native/darwin/README.md index 2eb5c722e..74c5c5911 100644 --- a/packages/tui/native/darwin/README.md +++ b/packages/tui/native/darwin/README.md @@ -1,6 +1,6 @@ # Darwin native prebuilds -Provides modifier-key state and asynchronous text/image clipboard access using AppKit. +Provides modifier-key state and asynchronous text, image, and file-path clipboard access using AppKit. ## Building diff --git a/packages/tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node b/packages/tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node index f453d0c06..3294b6bff 100755 Binary files a/packages/tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node and b/packages/tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node differ diff --git a/packages/tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node b/packages/tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node index 2f65289b2..014383575 100755 Binary files a/packages/tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node and b/packages/tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node differ diff --git a/packages/tui/native/darwin/src/darwin-platform.m b/packages/tui/native/darwin/src/darwin-platform.m index 38823357f..5cdd8f912 100644 --- a/packages/tui/native/darwin/src/darwin-platform.m +++ b/packages/tui/native/darwin/src/darwin-platform.m @@ -66,6 +66,40 @@ static void clipboard_execute(clipboard_job* job) { } else { clipboard_copy(job, text.UTF8String, [text lengthOfBytesUsingEncoding:NSUTF8StringEncoding], CLIPBOARD_UTF8); } + } else if (job->operation == CLIPBOARD_FILES) { + NSArray* urls = [pasteboard readObjectsForClasses:@[[NSURL class]] + options:@{NSPasteboardURLReadingFileURLsOnlyKey: @YES}]; + size_t total = 0; + for (NSURL* url in urls) { + const char* path = url.fileSystemRepresentation; + if (!path) continue; + size_t length = strlen(path); + if (length == SIZE_MAX || total > SIZE_MAX - length - 1) { + job->error = "Clipboard file paths are too large"; + return; + } + total += length + 1; + } + if (total == 0) { + job->format = CLIPBOARD_EMPTY; + return; + } + char* paths = clipboard_alloc(total); + if (!paths) { + job->error = "Out of memory"; + return; + } + char* cursor = paths; + for (NSURL* url in urls) { + const char* path = url.fileSystemRepresentation; + if (!path) continue; + size_t length = strlen(path); + memcpy(cursor, path, length + 1); + cursor += length + 1; + } + job->data = paths; + job->length = total; + job->format = CLIPBOARD_PATHS; } else { if (![pasteboard availableTypeFromArray:@[ NSPasteboardTypePNG, NSPasteboardTypeTIFF ]]) { job->format = CLIPBOARD_EMPTY; @@ -84,10 +118,15 @@ static void clipboard_execute(clipboard_job* job) { } } +static napi_value PI_NAPI_CALL get_clipboard_file_paths(napi_env env, napi_callback_info info) { + return queue_clipboard(env, info, CLIPBOARD_FILES); +} + PI_NAPI_EXPORT napi_value napi_register_module_v1(napi_env env, napi_value exports) { set_function_export(env, exports, "isModifierPressed", is_modifier_pressed); set_function_export(env, exports, "getText", get_clipboard_text); set_function_export(env, exports, "setText", set_clipboard_text); set_function_export(env, exports, "getImage", get_clipboard_image); + set_function_export(env, exports, "getFilePaths", get_clipboard_file_paths); return exports; } diff --git a/packages/tui/native/napi.h b/packages/tui/native/napi.h index 93c985c67..816a8202e 100644 --- a/packages/tui/native/napi.h +++ b/packages/tui/native/napi.h @@ -26,6 +26,7 @@ typedef void* napi_deferred; typedef void (PI_NAPI_CALL *napi_async_execute_callback)(napi_env, void*); typedef void (PI_NAPI_CALL *napi_async_complete_callback)(napi_env, int, void*); typedef int (PI_NAPI_CALL *napi_create_async_work_fn)(napi_env, napi_value, napi_value, napi_async_execute_callback, napi_async_complete_callback, void*, napi_async_work*); +typedef int (PI_NAPI_CALL *napi_create_array_fn)(napi_env, napi_value*); typedef int (PI_NAPI_CALL *napi_async_work_fn)(napi_env, napi_async_work); typedef int (PI_NAPI_CALL *napi_create_promise_fn)(napi_env, napi_deferred*, napi_value*); typedef int (PI_NAPI_CALL *napi_settle_deferred_fn)(napi_env, napi_deferred, napi_value); @@ -41,6 +42,7 @@ typedef int (PI_NAPI_CALL *napi_get_value_fn)(napi_env, napi_value*); typedef int (PI_NAPI_CALL *napi_get_value_string_utf8_fn)(napi_env, napi_value, char*, size_t, size_t*); typedef int (PI_NAPI_CALL *napi_get_value_string_utf16_fn)(napi_env, napi_value, uint16_t*, size_t, size_t*); typedef int (PI_NAPI_CALL *napi_set_named_property_fn)(napi_env, napi_value, const char*, napi_value); +typedef int (PI_NAPI_CALL *napi_set_element_fn)(napi_env, napi_value, uint32_t, napi_value); typedef int (PI_NAPI_CALL *napi_throw_error_fn)(napi_env, const char*, const char*); static void* node_symbol(const char* name) { diff --git a/packages/tui/src/native-platform.ts b/packages/tui/src/native-platform.ts index f5e82c035..3a6580a44 100644 --- a/packages/tui/src/native-platform.ts +++ b/packages/tui/src/native-platform.ts @@ -11,6 +11,8 @@ export interface NativeClipboard { getText(): Promise; /** Undefined means unavailable, null means no image; transfer failures reject. */ getImage(): Promise; + /** POSIX paths of file URLs on the clipboard. Undefined means unsupported, null means no files. */ + getFilePaths?(): Promise; /** Linux uses command-line tools to retain clipboard ownership instead. */ setText?(text: string): Promise; }