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
This commit is contained in:
roboomp
2026-07-21 20:33:16 +00:00
parent 9fd6e97113
commit 0d8a9d2f07
3 changed files with 41 additions and 1 deletions
+4
View File
@@ -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
@@ -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.
@@ -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<string, string[]> = {
const keyMap: Record<string, KeyId[]> = {
"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<string, () => 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<string>(),
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<string>();
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);