diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4c116ea94..b910c8daa 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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)` diff --git a/packages/coding-agent/src/modes/setup-wizard/scenes/sign-in.ts b/packages/coding-agent/src/modes/setup-wizard/scenes/sign-in.ts index 8338c0199..c0896c4da 100644 --- a/packages/coding-agent/src/modes/setup-wizard/scenes/sign-in.ts +++ b/packages/coding-agent/src/modes/setup-wizard/scenes/sign-in.ts @@ -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 { this.#resolvePrompt(""); const input = new Input(); + const focusInput = new CopyablePromptInput(input, () => { + void this.#copyAuthUrl(); + }); const pending = Promise.withResolvers(); 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; } diff --git a/packages/coding-agent/test/setup-wizard-sign-in.test.ts b/packages/coding-agent/test/setup-wizard-sign-in.test.ts index cac8440ef..3d875bf9f 100644 --- a/packages/coding-agent/test/setup-wizard-sign-in.test.ts +++ b/packages/coding-agent/test/setup-wizard-sign-in.test.ts @@ -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(); 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.