fix(core): emit auth URL as OSC 8 hyperlink instead of hard-wrapping

When the Qwen OAuth device flow falls back to the non-interactive
message (--no-browser or non-TUI contexts), the auth URL was hard-wrapped
at character boundaries to fit a fixed-width ASCII box. This broke the
URL across multiple lines, making it impossible to click or copy-paste
as a single link — especially over SSH.

Add supportsOsc8Hyperlinks() to detect terminal OSC 8 support (iTerm2,
WezTerm, Kitty, VS Code, Windows Terminal, VTE, Alacritty, etc.) and
osc8Hyperlink() to wrap URLs in OSC 8 escape sequences. When supported,
the URL is emitted as a single clickable hyperlink; otherwise the
existing ASCII box with hard-wrapping is preserved as a fallback.

Fixes #6428

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
DennisYu07 2026-07-07 15:27:22 +08:00
parent 7e0e79b6bc
commit 8aecb37211
2 changed files with 246 additions and 6 deletions

View file

@ -18,6 +18,7 @@ import {
qwenOAuth2Events,
QwenOAuth2Event,
QwenOAuth2Client,
showFallbackMessage,
type DeviceAuthorizationResponse,
type DeviceTokenResponse,
type ErrorData,
@ -2387,3 +2388,172 @@ describe('Constants and Configuration', () => {
);
});
});
describe('showFallbackMessage', () => {
const ORIGINAL_ENV = { ...process.env };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let stderrWriteSpy: any;
let originalIsTTY: boolean;
beforeEach(() => {
originalIsTTY = process.stderr.isTTY ?? false;
stderrWriteSpy = vi
.spyOn(process.stderr, 'write')
.mockImplementation(() => true);
// Reset env
process.env = { ...ORIGINAL_ENV };
});
afterEach(() => {
stderrWriteSpy.mockRestore();
process.env = ORIGINAL_ENV;
// Restore isTTY
if (originalIsTTY) {
Object.defineProperty(process.stderr, 'isTTY', {
value: true,
configurable: true,
});
} else {
Object.defineProperty(process.stderr, 'isTTY', {
value: false,
configurable: true,
});
}
});
function getOutput(): string {
return stderrWriteSpy.mock.calls.map((c: [string]) => c[0]).join('');
}
it('emits OSC 8 hyperlink when terminal supports it (iTerm.app)', () => {
Object.defineProperty(process.stderr, 'isTTY', {
value: true,
configurable: true,
});
process.env['TERM_PROGRAM'] = 'iTerm.app';
showFallbackMessage('https://chat.qwen.ai/device?code=ABC123');
const output = getOutput();
expect(output).toContain(
'\x1b]8;;https://chat.qwen.ai/device?code=ABC123\x07',
);
expect(output).toContain('\x1b]8;;\x07');
// URL appears as a single line within the OSC 8 envelope
expect(output).toContain(
'https://chat.qwen.ai/device?code=ABC123\x1b]8;;\x07',
);
});
it('hard-wraps URL when terminal does not support OSC 8 (non-TTY)', () => {
Object.defineProperty(process.stderr, 'isTTY', {
value: false,
configurable: true,
});
delete process.env['TERM_PROGRAM'];
delete process.env['FORCE_HYPERLINK'];
showFallbackMessage('https://chat.qwen.ai/device?code=ABC123');
const output = getOutput();
expect(output).not.toContain('\x1b]8;;');
// URL is present as plain text
expect(output).toContain('https://chat.qwen.ai/device?code=ABC123');
});
it('does not emit OSC 8 when QWEN_DISABLE_HYPERLINKS=1', () => {
Object.defineProperty(process.stderr, 'isTTY', {
value: true,
configurable: true,
});
process.env['TERM_PROGRAM'] = 'iTerm.app';
process.env['QWEN_DISABLE_HYPERLINKS'] = '1';
showFallbackMessage('https://chat.qwen.ai/device?code=ABC123');
const output = getOutput();
expect(output).not.toContain('\x1b]8;;');
});
it('respects FORCE_HYPERLINK=1 even in tmux', () => {
Object.defineProperty(process.stderr, 'isTTY', {
value: true,
configurable: true,
});
process.env['TMUX'] = '/tmp/tmux-1000/default,1234,0';
process.env['FORCE_HYPERLINK'] = '1';
showFallbackMessage('https://chat.qwen.ai/device?code=ABC123');
const output = getOutput();
expect(output).toContain('\x1b]8;;');
});
it('refuses OSC 8 in tmux without FORCE_HYPERLINK', () => {
Object.defineProperty(process.stderr, 'isTTY', {
value: true,
configurable: true,
});
process.env['TMUX'] = '/tmp/tmux-1000/default,1234,0';
delete process.env['FORCE_HYPERLINK'];
showFallbackMessage('https://chat.qwen.ai/device?code=ABC123');
const output = getOutput();
expect(output).not.toContain('\x1b]8;;');
});
it('detects Windows Terminal via WT_SESSION', () => {
Object.defineProperty(process.stderr, 'isTTY', {
value: true,
configurable: true,
});
process.env['WT_SESSION'] = 'xxx';
showFallbackMessage('https://chat.qwen.ai/device?code=ABC123');
const output = getOutput();
expect(output).toContain('\x1b]8;;');
});
it('detects Kitty via TERM=xterm-kitty', () => {
Object.defineProperty(process.stderr, 'isTTY', {
value: true,
configurable: true,
});
process.env['TERM'] = 'xterm-kitty';
showFallbackMessage('https://chat.qwen.ai/device?code=ABC123');
const output = getOutput();
expect(output).toContain('\x1b]8;;');
});
it('detects VS Code via TERM_PROGRAM=vscode', () => {
Object.defineProperty(process.stderr, 'isTTY', {
value: true,
configurable: true,
});
process.env['TERM_PROGRAM'] = 'vscode';
showFallbackMessage('https://chat.qwen.ai/device?code=ABC123');
const output = getOutput();
expect(output).toContain('\x1b]8;;');
});
it('still renders the ASCII box with title and instructions', () => {
Object.defineProperty(process.stderr, 'isTTY', {
value: true,
configurable: true,
});
process.env['TERM_PROGRAM'] = 'iTerm.app';
showFallbackMessage('https://chat.qwen.ai/device?code=ABC123');
const output = getOutput();
expect(output).toContain('Qwen OAuth Device Authorization');
expect(output).toContain('Please visit the following URL');
expect(output).toContain('Waiting for authorization');
});
});

View file

@ -701,16 +701,81 @@ export async function getQwenOAuthClient(
}
}
/**
* Check whether stderr's terminal supports OSC 8 hyperlinks. A minimal but
* safe subset of the detection logic in `packages/cli/src/ui/utils/osc8.ts`
* enough to cover the common terminals (iTerm2, WezTerm, Kitty, VS Code,
* Windows Terminal, GNOME Terminal/VTE) while refusing for multiplexers
* (tmux/screen) and non-TTY contexts unless `FORCE_HYPERLINK` is set.
*/
function supportsOsc8Hyperlinks(): boolean {
const env = process.env;
if (env['QWEN_DISABLE_HYPERLINKS'] === '1') return false;
if (!process.stderr?.isTTY) return false;
const force = env['FORCE_HYPERLINK'];
if (force !== undefined) return force !== '0';
if (env['CI']) return false;
if (env['TMUX'] || env['STY']) return false;
if (env['WT_SESSION']) return true;
if (env['KITTY_WINDOW_ID'] || env['TERM'] === 'xterm-kitty') return true;
if (env['DOMTERM']) return true;
if (env['GHOSTTY_RESOURCES_DIR'] || env['TERM'] === 'xterm-ghostty') {
return true;
}
if (env['TERMINAL_EMULATOR'] === 'JetBrains-JediTerm') return true;
if (env['TERM_PROGRAM']) {
switch (env['TERM_PROGRAM']) {
case 'iTerm.app':
case 'WezTerm':
case 'vscode':
case 'ghostty':
case 'mintty':
return true;
default:
break;
}
}
if (env['VTE_VERSION']) {
const v = parseInt(env['VTE_VERSION'], 10);
if (Number.isFinite(v) && v >= 5000 && v !== 5000) return true;
}
if (
env['TERM'] === 'alacritty' ||
env['ALACRITTY_LOG'] !== undefined ||
env['ALACRITTY_WINDOW_ID'] !== undefined
) {
return true;
}
return false;
}
/**
* Wrap a URL in an OSC 8 hyperlink escape sequence. BEL (\x07) terminates
* the OSC more broadly supported than ST (ESC \\). Control characters
* are stripped from the URL to prevent breaking the OSC envelope.
*/
function osc8Hyperlink(url: string): string {
// eslint-disable-next-line no-control-regex
const safeUrl = url.replace(/[\x00-\x1f\x7f\x80-\x9f]/g, '');
return `\x1b]8;;${safeUrl}\x07${safeUrl}\x1b]8;;\x07`;
}
/**
* Displays a formatted box with OAuth device authorization URL.
* Uses process.stderr.write() to ensure the auth URL is always visible to users,
* especially in non-interactive mode. Using stderr prevents corruption of
* structured JSON output (which goes to stdout) and follows the standard Unix
* convention of user-facing messages to stderr.
*
* When the terminal supports OSC 8 hyperlinks, the URL is emitted as a single
* clickable link instead of being hard-wrapped across multiple lines. This is
* especially important over SSH, where wrapped URLs are impossible to
* copy-paste as a single link.
*/
function showFallbackMessage(verificationUriComplete: string): void {
export function showFallbackMessage(verificationUriComplete: string): void {
const title = 'Qwen OAuth Device Authorization';
const url = verificationUriComplete;
const useOsc8 = supportsOsc8Hyperlinks();
const minWidth = 70;
const maxWidth = 80;
const boxWidth = Math.min(Math.max(title.length + 4, minWidth), maxWidth);
@ -786,11 +851,16 @@ function showFallbackMessage(verificationUriComplete: string): void {
process.stderr.write(emptyLine + '\n');
// Write URL
for (const line of urlLines) {
process.stderr.write(
'| ' + line + ' '.repeat(contentWidth - line.length) + ' |\n',
);
// Write URL — as a single OSC 8 clickable hyperlink when supported,
// or hard-wrapped across lines as a fallback.
if (useOsc8) {
process.stderr.write('| ' + osc8Hyperlink(url) + ' |\n');
} else {
for (const line of urlLines) {
process.stderr.write(
'| ' + line + ' '.repeat(contentWidth - line.length) + ' |\n',
);
}
}
process.stderr.write(emptyLine + '\n');