mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
fix(terminal): detect PR links with ANSI resets (#8216)
* fix(terminal): detect PR links with ANSI resets (#8126) * fix(terminal): safely normalize PR link styling --------- Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createTerminalGitHubPRLinkDetector } from './terminal-github-pr-link-detector'
|
||||
|
||||
const issue8126Url = 'https://github.com/owner/repo/pull/10'
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
@@ -18,6 +20,65 @@ describe('createTerminalGitHubPRLinkDetector', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('detects issue 8126 Claude Code PR links with attached ANSI reset', () => {
|
||||
const observe = createTerminalGitHubPRLinkDetector()
|
||||
|
||||
expect(observe(`${issue8126Url}\x1b[22m\n`)).toEqual([
|
||||
{
|
||||
url: issue8126Url,
|
||||
slug: { owner: 'owner', repo: 'repo' },
|
||||
number: 10
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('strips an ANSI reset split across PTY chunks', () => {
|
||||
const observe = createTerminalGitHubPRLinkDetector()
|
||||
|
||||
expect(observe(`${issue8126Url}\x1b`)).toEqual([])
|
||||
expect(observe('[22m\n')).toEqual([
|
||||
{
|
||||
url: issue8126Url,
|
||||
slug: { owner: 'owner', repo: 'repo' },
|
||||
number: 10
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('rejects PR URLs corrupted by cursor movement', () => {
|
||||
const observe = createTerminalGitHubPRLinkDetector()
|
||||
|
||||
expect(observe('https://github.com/owne\x1b[1Cr/repo/pull/10\n')).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects PR URLs fused across terminal rows', () => {
|
||||
for (const cursorMove of ['\x1b[1A', '\x1b[1B']) {
|
||||
const observe = createTerminalGitHubPRLinkDetector()
|
||||
|
||||
expect(observe(`https://github.com/owner/repo/pull/${cursorMove}10\n`)).toEqual([])
|
||||
}
|
||||
})
|
||||
|
||||
it('does not fuse screen-editing controls into PR URLs', () => {
|
||||
for (const screenEdit of ['\x08', '\x0b', '\x0c', '\x1bD', '\x1b[2J', '\x1b[2K', '\x1b[1S']) {
|
||||
const observe = createTerminalGitHubPRLinkDetector()
|
||||
|
||||
expect(observe(`https://github.com/owner/repo/pull/1${screenEdit}0\n`)).toEqual([])
|
||||
}
|
||||
})
|
||||
|
||||
it('deduplicates styled and plain instances', () => {
|
||||
const observe = createTerminalGitHubPRLinkDetector()
|
||||
|
||||
expect(observe(`${issue8126Url}\x1b[22m\n${issue8126Url}\n`)).toEqual([
|
||||
{
|
||||
url: issue8126Url,
|
||||
slug: { owner: 'owner', repo: 'repo' },
|
||||
number: 10
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('waits for a boundary when the URL is split across PTY chunks', () => {
|
||||
const observe = createTerminalGitHubPRLinkDetector()
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-control-regex -- ANSI SGR sequences are raw PTY input. */
|
||||
/**
|
||||
* Chunk-boundary-safe GitHub PR URL scan over PTY output.
|
||||
*
|
||||
@@ -11,6 +12,9 @@ import type { RepoSlug } from './github-links'
|
||||
import { parseGitHubIssueOrPRLink } from './github-links'
|
||||
|
||||
const GITHUB_PR_PATH_MARKER = '/pull/'
|
||||
const TERMINAL_SGR_PATTERN = /\x1b\[[0-?]*[ -/]*m/g
|
||||
const TERMINAL_CURSOR_CONTROL_PATTERN = /[\x08\x0b\x0c]/g
|
||||
const TERMINAL_CONTROL_GUARD = '\ufffd'
|
||||
const HTTP_SCHEME_PREFIXES = ['https://', 'http://'] as const
|
||||
const TRAILING_TERMINAL_PUNCTUATION_RE = /[),.;\]}]+$/
|
||||
const MAX_CARRY_LENGTH = 512
|
||||
@@ -27,6 +31,9 @@ function trimTerminalUrl(candidate: string): string {
|
||||
}
|
||||
|
||||
function parseTerminalGitHubPRUrl(candidate: string): TerminalGitHubPRLink | null {
|
||||
if (candidate.includes('\x1b') || candidate.includes(TERMINAL_CONTROL_GUARD)) {
|
||||
return null
|
||||
}
|
||||
const url = trimTerminalUrl(candidate)
|
||||
const parsed = parseGitHubIssueOrPRLink(url)
|
||||
if (!parsed || parsed.type !== 'pr') {
|
||||
@@ -128,12 +135,19 @@ export function createTerminalGitHubPRLinkDetector(): (data: string) => Terminal
|
||||
const seenUrls = new Set<string>()
|
||||
|
||||
return (data: string): TerminalGitHubPRLink[] => {
|
||||
const combined = carry ? carry + data : data
|
||||
const rawCombined = carry ? carry + data : data
|
||||
|
||||
if (!combined.includes(GITHUB_PR_PATH_MARKER)) {
|
||||
carry = getPotentialGitHubPRCarry(combined)
|
||||
// Why: PTY output is a hot path; avoid multi-pass ANSI normalization for
|
||||
// chunks that cannot contain a GitHub pull-request URL.
|
||||
if (!rawCombined.includes(GITHUB_PR_PATH_MARKER)) {
|
||||
carry = getPotentialGitHubPRCarry(rawCombined)
|
||||
return []
|
||||
}
|
||||
// Why: SGR styling has no screen width, so removing it is safe. Cursor
|
||||
// controls get a guard; other escape sequences remain URL-invalid.
|
||||
const combined = rawCombined
|
||||
.replace(TERMINAL_SGR_PATTERN, '')
|
||||
.replace(TERMINAL_CURSOR_CONTROL_PATTERN, TERMINAL_CONTROL_GUARD)
|
||||
|
||||
const links: TerminalGitHubPRLink[] = []
|
||||
// Why: PTY data may echo a huge pasted line. Scan URL candidates directly
|
||||
@@ -154,7 +168,7 @@ export function createTerminalGitHubPRLinkDetector(): (data: string) => Terminal
|
||||
links.push(parsed)
|
||||
}
|
||||
|
||||
carry = getPotentialGitHubPRCarry(combined)
|
||||
carry = getPotentialGitHubPRCarry(rawCombined)
|
||||
return links
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user