Skip to content

Commit 9ba8f21

Browse files
nrjdalalclaude
andcommitted
fix: resolve all P1/P2 review issues in interactive picker
P1: Use resolveSymlinkPath() for preview file resolution P1: Remove stdin listener before async highlight to prevent race P1: Handle Ctrl-C in preview mode P2: Track preview state for correct resize handling P2: ANSI-aware line truncation to prevent color bleed Fix: full-width background on cursor line in preview Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent aa3d497 commit 9ba8f21

1 file changed

Lines changed: 50 additions & 9 deletions

File tree

bin/utils/interactive-picker.ts

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,23 @@ export type TreeEntry = {
7474

7575
const stripAnsi = (s: string) => s.replace(/\x1B\[\d+(?:;\d+)*m/g, "")
7676

77+
function truncateAnsi(s: string, maxWidth: number): string {
78+
let visible = 0
79+
let i = 0
80+
while (i < s.length && visible < maxWidth) {
81+
if (s[i] === "\x1B" && s[i + 1] === "[") {
82+
const end = s.indexOf("m", i)
83+
if (end !== -1) {
84+
i = end + 1
85+
continue
86+
}
87+
}
88+
visible++
89+
i++
90+
}
91+
return s.slice(0, i) + "\x1B[0m"
92+
}
93+
7794
type TreeNode = {
7895
name: string
7996
path: string
@@ -372,7 +389,12 @@ export function interactivePicker(
372389
resolve([])
373390
process.exit(0)
374391
}
375-
const onResize = () => render()
392+
let inPreview = false
393+
let previewRenderer: (() => void) | null = null
394+
const onResize = () => {
395+
if (inPreview && previewRenderer) previewRenderer()
396+
else render()
397+
}
376398
process.on("exit", onExit)
377399
process.on("SIGINT", onSigint)
378400
stream.on("resize", onResize)
@@ -490,10 +512,12 @@ export function interactivePicker(
490512
}
491513

492514
async function showPreview(node: TreeNode) {
493-
const filePath = path.join(
494-
basePath!,
495-
node.type === "symlink" ? node.linkTarget.replace(/\/$/, "") : node.path,
496-
)
515+
// Remove stdin listener immediately to prevent race during async highlight
516+
stdin.removeListener("data", onKey)
517+
518+
const resolvedPath =
519+
node.type === "symlink" ? resolveSymlinkPath(node.path, node.linkTarget) : node.path
520+
const filePath = path.join(basePath!, resolvedPath)
497521
let content: string
498522
try {
499523
const stat = fs.statSync(filePath)
@@ -552,11 +576,13 @@ export function interactivePicker(
552576
const lineIdx = previewScrollOffset + i
553577
const isCursorLine = lineIdx === previewCursor
554578
const lineNum = dim(` ${String(lineIdx + 1).padStart(lineNumWidth)} `)
555-
const lineContent = lines[lineIdx].slice(0, cols - lineNumWidth - 5)
579+
const lineContent = truncateAnsi(lines[lineIdx], cols - lineNumWidth - 5)
556580
let line = `${lineNum}${lineContent}`
557581
if (isCursorLine) {
558-
const pad = Math.max(0, cols - stripAnsi(line).length)
559-
line = `\x1B[48;5;236m${line}${" ".repeat(pad)}\x1B[49m`
582+
// Strip trailing resets so bg extends fully
583+
const cleaned = line.replace(/\x1B\[0m/g, "")
584+
const pad = Math.max(0, cols - stripAnsi(cleaned).length)
585+
line = `\x1B[48;5;236m${cleaned}${" ".repeat(pad)}\x1B[0m`
560586
}
561587
out += line + "\n"
562588
}
@@ -582,7 +608,21 @@ export function interactivePicker(
582608
function onPreviewKey(buf: Buffer) {
583609
const key = buf.toString()
584610

611+
// Ctrl-C in preview
612+
if (key === "\x03") {
613+
inPreview = false
614+
previewRenderer = null
615+
stdin.removeListener("data", onPreviewKey)
616+
cleanup()
617+
process.removeListener("exit", onExit)
618+
process.removeListener("SIGINT", onSigint)
619+
resolve([])
620+
return
621+
}
622+
585623
if (key === "\x1B" || key === "q" || key === "Q" || key === "\r") {
624+
inPreview = false
625+
previewRenderer = null
586626
stdin.removeListener("data", onPreviewKey)
587627
stdin.on("data", onKey)
588628
render()
@@ -599,7 +639,8 @@ export function interactivePicker(
599639
renderPreview()
600640
}
601641

602-
stdin.removeListener("data", onKey)
642+
inPreview = true
643+
previewRenderer = renderPreview
603644
stdin.on("data", onPreviewKey)
604645
renderPreview()
605646
}

0 commit comments

Comments
 (0)