From 0d8a9d2f0778ead503ff6e2943970cf1f99ee7d7 Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 21 Jul 2026 20:33:16 +0000 Subject: [PATCH] fix(auth): routed smart paste into focused prompts - Intercepted the configured smart-paste shortcut while a paste-capable modal owns focus. - Added regression coverage for Ctrl+V API-key entry and documented the fix. Fixes #6057 --- packages/coding-agent/CHANGELOG.md | 4 +++ .../src/modes/controllers/input-controller.ts | 11 ++++++++ .../test/input-controller-keybindings.test.ts | 27 ++++++++++++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 6f32dc885..09aadf33e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## [Unreleased] +### Fixed + +- Fixed `Ctrl+V` clipboard paste being dropped while API-key and other modal prompts own focus ([#6057](https://github.com/can1357/oh-my-pi/issues/6057)). + ## [17.0.5] - 2026-07-18 diff --git a/packages/coding-agent/src/modes/controllers/input-controller.ts b/packages/coding-agent/src/modes/controllers/input-controller.ts index 2ba96cfbc..b9f00b90e 100644 --- a/packages/coding-agent/src/modes/controllers/input-controller.ts +++ b/packages/coding-agent/src/modes/controllers/input-controller.ts @@ -172,6 +172,7 @@ export class InputController { #enhancedPaste?: EnhancedPasteController; #focusedLeftTapListenerInstalled = false; + #focusedPasteListenerInstalled = false; #btwBranchListenerInstalled = false; #btwCopyListenerInstalled = false; // Tap counter for the double-← gesture; reset whenever a quiet gap @@ -265,6 +266,16 @@ export class InputController { return { consume: true }; }); } + if (!this.#focusedPasteListenerInstalled) { + this.#focusedPasteListenerInstalled = true; + this.ctx.ui.addInputListener(data => { + const focused = this.ctx.ui.getFocused(); + if (!focused || focused === this.ctx.editor || !hasPasteText(focused)) return undefined; + if (!this.ctx.keybindings.matches(data, "app.clipboard.pasteImage")) return undefined; + void this.handleImagePaste(); + return { consume: true }; + }); + } this.ctx.editor.onEscape = () => { // Side-channel panels are the topmost view. Esc dismisses them before // touching loop mode, maintenance, or the underlying main turn. diff --git a/packages/coding-agent/test/input-controller-keybindings.test.ts b/packages/coding-agent/test/input-controller-keybindings.test.ts index d5520bf7d..c1286ca70 100644 --- a/packages/coding-agent/test/input-controller-keybindings.test.ts +++ b/packages/coding-agent/test/input-controller-keybindings.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, type Mock, vi } from "bun:test"; import type { ImageContent } from "@oh-my-pi/pi-ai"; import { InputController } from "@oh-my-pi/pi-coding-agent/modes/controllers/input-controller"; import type { InteractiveModeContext } from "@oh-my-pi/pi-coding-agent/modes/types"; +import { type KeyId, matchesKey } from "@oh-my-pi/pi-tui"; import manualContinuePrompt from "../src/prompts/system/manual-continue.md" with { type: "text" }; type FakeEditor = { @@ -55,11 +56,12 @@ function registeredInputListeners(addInputListener: Mock<(listener: InputListene async function createContext() { let editorText = ""; - const keyMap: Record = { + const keyMap: Record = { "app.display.reset": ["ctrl+l"], "app.model.selectTemporary": ["ctrl+y"], "app.model.select": ["alt+m"], "app.retry": ["alt+r"], + "app.clipboard.pasteImage": ["ctrl+v"], }; const customHandlers = new Map void>(); const setActionKeys = vi.fn(); @@ -149,6 +151,9 @@ async function createContext() { getKeys(action: string) { return keyMap[action] ? [...keyMap[action]] : []; }, + matches(data: string, action: string) { + return keyMap[action]?.some(key => matchesKey(data, key)) ?? false; + }, } as InteractiveModeContext["keybindings"], locallySubmittedUserSignatures: new Set(), isKnownSlashCommand: () => false, @@ -414,6 +419,26 @@ describe("InputController keybinding setup", () => { expect(spies.handleBtwBranchKey).not.toHaveBeenCalled(); }); + it("routes the smart-paste shortcut to a focused login input", async () => { + const { promise: pasted, resolve: resolvePaste } = Promise.withResolvers(); + const focusedPasteText = vi.fn((text: string) => { + resolvePaste(text); + }); + const { InputController, ctx, setFocused, spies } = await createContext(); + setFocused({ pasteText: focusedPasteText }); + const controller = new InputController(ctx, { + readImage: async () => null, + readText: async () => "sk-test-key", + }); + + controller.setupKeyHandlers(); + const result = dispatchInput(registeredInputListeners(spies.addInputListener), "\x16"); + + expect(result).toEqual({ consume: true }); + expect(await pasted).toBe("sk-test-key"); + expect(focusedPasteText).toHaveBeenCalledWith("sk-test-key"); + }); + it("routes c to copy a copyable /btw panel when the editor is empty", async () => { const { InputController, ctx, spies } = await createContext(); (ctx.canCopyBtw as unknown as { mockReturnValue(value: boolean): void }).mockReturnValue(true);