fix(auth): routed setup copy shortcut through prompt

Paste-code provider prompts now preserve the Alt+C authentication URL retry while their input owns TUI focus.\n\nFixes #2908
This commit is contained in:
roboomp
2026-06-17 23:30:14 +00:00
parent ba3b1529c1
commit 507e0516d1
3 changed files with 65 additions and 7 deletions
+4 -1
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed provider setup sign-in URLs to attempt clipboard/OSC 52 copy and expose an Alt+C retry shortcut, so authentication is not blocked when TUI selection is unavailable ([#2908](https://github.com/can1357/oh-my-pi/issues/2908)).
## [16.0.5] - 2026-06-17
### Added
@@ -23,7 +27,6 @@
### Fixed
- Fixed provider setup sign-in URLs to attempt clipboard/OSC 52 copy and expose an Alt+C retry shortcut, so authentication is not blocked when TUI selection is unavailable ([#2908](https://github.com/can1357/oh-my-pi/issues/2908)).
- Fixed Matplotlib figure display to emit PNG output immediately when `display(fig)` is called, even if the figure is closed before the end-of-cell flush
- Fixed persisted tool-result image payloads in `details.images` to externalize and resolve through the session blob store, so generated-image details survive resume without stale blob refs or truncation
- Fixed duplicate Matplotlib image output by skipping the automatic end-of-request figure flush for figures that were already displayed through `display(fig)`
@@ -1,7 +1,14 @@
import type { AuthStorage } from "@oh-my-pi/pi-ai";
import { PASTE_CODE_LOGIN_PROVIDERS } from "@oh-my-pi/pi-ai";
import type { OAuthProvider } from "@oh-my-pi/pi-ai/oauth/types";
import { Input, matchesKey, type SgrMouseEvent, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
import {
type Component,
type Focusable,
Input,
matchesKey,
type SgrMouseEvent,
wrapTextWithAnsi,
} from "@oh-my-pi/pi-tui";
import { getAgentDbPath } from "@oh-my-pi/pi-utils";
import { copyToClipboard } from "../../../utils/clipboard";
import { OAuthSelectorComponent } from "../../components/oauth-selector";
@@ -16,10 +23,48 @@ function loginCopyHint(): string {
return theme.fg("dim", "(clipboard copy attempted; Alt+C retries)");
}
class CopyablePromptInput implements Component, Focusable {
#input: Input;
#onCopy: () => void;
constructor(input: Input, onCopy: () => void) {
this.#input = input;
this.#onCopy = onCopy;
}
get focused(): boolean {
return this.#input.focused;
}
set focused(value: boolean) {
this.#input.focused = value;
}
setUseTerminalCursor(useTerminalCursor: boolean): void {
this.#input.setUseTerminalCursor(useTerminalCursor);
}
render(width: number): readonly string[] {
return this.#input.render(width);
}
handleInput(data: string): void {
if (matchesKey(data, "alt+c")) {
this.#onCopy();
return;
}
this.#input.handleInput(data);
}
invalidate(): void {
this.#input.invalidate();
}
}
interface PromptState {
message: string;
placeholder?: string;
input: Input;
input: CopyablePromptInput;
}
/**
@@ -211,9 +256,12 @@ export class SignInTab implements SetupTab {
#showPrompt(prompt: { message: string; placeholder?: string }): Promise<string> {
this.#resolvePrompt("");
const input = new Input();
const focusInput = new CopyablePromptInput(input, () => {
void this.#copyAuthUrl();
});
const pending = Promise.withResolvers<string>();
this.#promptResolve = pending.resolve;
this.#prompt = { message: prompt.message, placeholder: prompt.placeholder, input };
this.#prompt = { message: prompt.message, placeholder: prompt.placeholder, input: focusInput };
input.onSubmit = value => {
this.#resolvePrompt(value);
};
@@ -221,7 +269,7 @@ export class SignInTab implements SetupTab {
this.#loginAbort?.abort();
this.#resolvePrompt("");
};
this.host.setFocus(input);
this.host.setFocus(focusInput);
this.host.requestRender();
return pending.promise;
}
@@ -5,6 +5,7 @@ import { SignInTab } from "@oh-my-pi/pi-coding-agent/modes/setup-wizard/scenes/s
import type { SetupSceneHost } from "@oh-my-pi/pi-coding-agent/modes/setup-wizard/scenes/types";
import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import * as clipboard from "@oh-my-pi/pi-coding-agent/utils/clipboard";
import type { Component } from "@oh-my-pi/pi-tui";
beforeAll(async () => {
await initTheme();
@@ -19,6 +20,7 @@ describe("SignInTab", () => {
const url = `https://example.com/oauth/authorize?client_id=omp&redirect_uri=http%3A%2F%2Flocalhost%3A45454%2Fcallback&state=${"a".repeat(96)}`;
const loginGate = Promise.withResolvers<void>();
const copySpy = vi.spyOn(clipboard, "copyToClipboard").mockResolvedValue(undefined);
let focusTarget: Component | undefined;
const openedUrls: string[] = [];
const authStorage = {
@@ -47,7 +49,9 @@ describe("SignInTab", () => {
},
requestRender(): void {},
finish(): void {},
setFocus(): void {},
setFocus(component: Component | null): void {
focusTarget = component ?? undefined;
},
restoreFocus(): void {},
} as unknown as SetupSceneHost;
@@ -64,7 +68,10 @@ describe("SignInTab", () => {
expect(compact).not.toContain("…");
expect(rendered.join("\n")).toContain(`\x1b]8;;${url}\x07Open login URL\x1b]8;;\x07`);
expect(openedUrls).toEqual([url]);
expect(copySpy).toHaveBeenCalledWith(url);
expect(focusTarget).toBeDefined();
focusTarget?.handleInput?.("\x1bc");
expect(copySpy).toHaveBeenCalledTimes(2);
expect(copySpy).toHaveBeenLastCalledWith(url);
// On a ~24-row terminal the wizard body ends up ~8 rows; the OSC8
// link, a plain URL row, and the focused input must survive that clip.