diff --git a/packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx b/packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx index 19d4b3cac89..1e5315a2646 100644 --- a/packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx +++ b/packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx @@ -18,8 +18,8 @@ const RenderInlineInternal: React.FC = ({ text: rawText, defaultColor, }) => { - const text = stripUnsafeCharacters(rawText); - const ansiText = parseMarkdownToANSI(text, defaultColor); + const sanitizedText = stripUnsafeCharacters(rawText); + const ansiText = parseMarkdownToANSI(sanitizedText, defaultColor); return {ansiText}; }; diff --git a/packages/cli/src/ui/utils/markdownParsingUtils.test.ts b/packages/cli/src/ui/utils/markdownParsingUtils.test.ts index 5728f886dc8..270044bf177 100644 --- a/packages/cli/src/ui/utils/markdownParsingUtils.test.ts +++ b/packages/cli/src/ui/utils/markdownParsingUtils.test.ts @@ -142,7 +142,7 @@ describe('parsingUtils', () => { const input = 'Visit https://google.com now'; const output = parseMarkdownToANSI(input); expect(output).toBe( - `${primary('Visit ')}${link('https://google.com')}${primary(' now')}`, + `${primary('Visit ')}\x1b]8;;https://google.com\x1b\\${link('https://google.com')}\x1b]8;;\x1b\\${primary(' now')}`, ); }); @@ -269,5 +269,51 @@ describe('parsingUtils', () => { expect(output).toBe(primary('It costs $5.99 total')); }); }); + + describe('OSC 8 terminal hyperlinks for bare URLs', () => { + const OSC = '\x1b]8;;'; + const ST = '\x1b\\'; + + it('wraps bare https URLs in an OSC 8 hyperlink sequence', () => { + const url = + 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=x'; + const result = parseMarkdownToANSI(url); + // Must begin with the OSC 8 opening sequence containing the full URL + expect(result).toContain(`${OSC}${url}${ST}`); + // Must close with the OSC 8 terminator + expect(result).toContain(`${OSC}${ST}`); + // The full URL string must appear as visible text + expect(result).toContain(url); + }); + + it('does not drop OAuth query parameters', () => { + const url = + 'https://example.com/auth?redirect_uri=http%3A%2F%2F127.0.0.1%3A1234%2Foauth2callback&response_type=code'; + const result = parseMarkdownToANSI(url); + expect(result).toContain('response_type=code'); + expect(result).toContain('redirect_uri=http%3A%2F%2F127.0.0.1'); + }); + + it('updates the existing bare-URL test to expect OSC 8 wrapping', () => { + const url = 'https://google.com'; + const result = parseMarkdownToANSI(`Visit ${url} now`); + expect(result).toContain(`${OSC}${url}${ST}`); + }); + + it('does not emit OSC 8 when chalk.level is 0 (NO_COLOR / non-TTY)', () => { + const originalLevel = chalk.level; + try { + chalk.level = 0; + const url = 'https://example.com/path?q=1'; + const result = parseMarkdownToANSI(url); + // Must not contain the OSC 8 opening sequence + expect(result).not.toContain('\x1b]8;;'); + // The URL text itself must still be present + expect(result).toContain(url); + } finally { + chalk.level = originalLevel; + } + }); + }); }); }); diff --git a/packages/cli/src/ui/utils/markdownParsingUtils.ts b/packages/cli/src/ui/utils/markdownParsingUtils.ts index 841809f08c7..dd3b63952d5 100644 --- a/packages/cli/src/ui/utils/markdownParsingUtils.ts +++ b/packages/cli/src/ui/utils/markdownParsingUtils.ts @@ -236,7 +236,16 @@ export const parseMarkdownToANSI = ( ), ); } else if (fullMatch.match(/^https?:\/\//)) { - styledPart = ansiColorize(fullMatch, theme.text.link); + const colored = ansiColorize(fullMatch, theme.text.link); + // Wrap in an OSC 8 terminal hyperlink so the full URL remains the + // clickable target even when the terminal visually word-wraps the text. + // OSC 8 is an escape sequence: only emit it when chalk is outputting + // ANSI codes. chalk.level === 0 when NO_COLOR is set, stdout is + // non-TTY, or FORCE_COLOR/TERM=dumb suppresses output. + styledPart = + chalk.level > 0 + ? `\x1b]8;;${fullMatch}\x1b\\${colored}\x1b]8;;\x1b\\` + : colored; } } catch (e) { debugLogger.warn('Error parsing inline markdown part:', fullMatch, e); diff --git a/packages/core/src/code_assist/oauth2.ts b/packages/core/src/code_assist/oauth2.ts index 01b0d0f04c8..671733ffad5 100644 --- a/packages/core/src/code_assist/oauth2.ts +++ b/packages/core/src/code_assist/oauth2.ts @@ -451,9 +451,11 @@ async function authWithUserCode(client: OAuth2Client): Promise { code_challenge: codeVerifier.codeChallenge, state, }); + const osc8Link = (url: string) => + `\x1b]8;;${url}\x1b\\${url}\x1b]8;;\x1b\\`; writeToStdout( 'Please visit the following URL to authorize the application:\n\n' + - authUrl + + osc8Link(authUrl) + '\n\n', );