From f15938ea4c84ef49d42ca3b0a778e7dd27dbd7e7 Mon Sep 17 00:00:00 2001 From: mastertyko <11311479+mastertyko@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:45:28 +0200 Subject: [PATCH] fix(pi-coding-agent): show full OAuth login URLs --- .../components/__tests__/login-dialog.test.ts | 24 ++++++++++++++ .../interactive/components/login-dialog.ts | 32 +++++++++++++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 packages/pi-coding-agent/src/modes/interactive/components/__tests__/login-dialog.test.ts diff --git a/packages/pi-coding-agent/src/modes/interactive/components/__tests__/login-dialog.test.ts b/packages/pi-coding-agent/src/modes/interactive/components/__tests__/login-dialog.test.ts new file mode 100644 index 000000000..1ce0469ff --- /dev/null +++ b/packages/pi-coding-agent/src/modes/interactive/components/__tests__/login-dialog.test.ts @@ -0,0 +1,24 @@ +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; +import { buildAuthUrlPresentation } from "../login-dialog.js"; + +describe("LoginDialogComponent", () => { + test("shows the full OAuth URL when the hyperlink label is truncated", () => { + const presentation = buildAuthUrlPresentation( + "https://auth.example.com/device?code=ABCD-1234&callback=oauth&state=needs-full-visibility", + 52, + ); + + assert.notEqual( + presentation.displayUrl, + "https://auth.example.com/device?code=ABCD-1234&callback=oauth&state=needs-full-visibility", + "narrow terminals should still truncate the hyperlink label", + ); + assert.ok(presentation.fullUrlLines.length > 1, "truncated URLs should expose wrapped full-url lines"); + assert.match(presentation.fullUrlLines[0] ?? "", /https:\/\/auth\.example\.com\/device\?code=ABCD-1234&/); + assert.match( + presentation.fullUrlLines[presentation.fullUrlLines.length - 1] ?? "", + /state=needs-full-visibility/, + ); + }); +}); diff --git a/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts b/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts index bf9e8b4ed..0a13465bb 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts @@ -7,6 +7,27 @@ import { theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; import { keyHint } from "./keybinding-hints.js"; +function wrapPlainText(text: string, width: number): string[] { + const lines: string[] = []; + const safeWidth = Math.max(1, width); + for (let idx = 0; idx < text.length; idx += safeWidth) { + lines.push(text.slice(idx, idx + safeWidth)); + } + return lines.length > 0 ? lines : [""]; +} + +export function buildAuthUrlPresentation(url: string, terminalColumns: number): { + displayUrl: string; + fullUrlLines: string[]; +} { + const maxUrlWidth = Math.max(20, terminalColumns - 4); + const displayUrl = truncateToWidth(url, maxUrlWidth); + return { + displayUrl, + fullUrlLines: displayUrl === url ? [] : wrapPlainText(url, maxUrlWidth), + }; +} + /** * Login dialog component - replaces editor during OAuth login flow. * @@ -124,14 +145,21 @@ export class LoginDialogComponent extends Container implements Focusable { // Truncate the visible URL text so it never wraps (which would break // the OSC 8 hyperlink). The full URL is still the link target. - const maxUrlWidth = Math.max(20, this.tui.terminal.columns - 4); - const displayUrl = truncateToWidth(url, maxUrlWidth); + const { displayUrl, fullUrlLines } = buildAuthUrlPresentation(url, this.tui.terminal.columns); const urlLink = `\x1b]8;;${url}\x07${theme.fg("accent", displayUrl)}\x1b]8;;\x07`; this.contentContainer.addChild(new Text(urlLink, 1, 0)); const clickHint = process.platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open"; this.contentContainer.addChild(new Text(theme.fg("dim", clickHint), 1, 0)); + if (fullUrlLines.length > 0) { + this.contentContainer.addChild(new Spacer(1)); + this.contentContainer.addChild(new Text(theme.fg("dim", "Full URL:"), 1, 0)); + for (const line of fullUrlLines) { + this.contentContainer.addChild(new Text(theme.fg("dim", line), 1, 0)); + } + } + if (instructions) { this.contentContainer.addChild(new Spacer(1)); this.contentContainer.addChild(new Text(theme.fg("warning", instructions), 1, 0));