Files
claude-code/src/components/QuickOpenDialog.tsx
claude-code-best a14d3dc8f0 fix(types): clean type fixes across 92 files
Apply proper TypeScript type corrections without any unsafe casts:
- Fix unknown/never/{} types from decompilation
- Correct function signatures and parameter types
- Add missing type declarations and interfaces
- Fix Ink component prop types
- Update API client/provider type annotations

Test files with mock data casts are included as-is (acceptable pattern).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 23:45:56 +08:00

182 lines
6.3 KiB
TypeScript

import * as path from 'path'
import * as React from 'react'
import { useEffect, useRef, useState } from 'react'
import { useRegisterOverlay } from '../context/overlayContext.js'
import { generateFileSuggestions } from '../hooks/fileSuggestions.js'
import { useTerminalSize } from '../hooks/useTerminalSize.js'
import { Text } from '@anthropic/ink'
import { logEvent } from '../services/analytics/index.js'
import { getCwd } from '../utils/cwd.js'
import { openFileInExternalEditor } from '../utils/editor.js'
import { truncatePathMiddle, truncateToWidth } from '../utils/format.js'
import { highlightMatch } from '../utils/highlightMatch.js'
import { readFileInRange } from '../utils/readFileInRange.js'
import { FuzzyPicker, LoadingState } from '@anthropic/ink'
type Props = {
onDone: () => void
onInsert: (text: string) => void
}
const VISIBLE_RESULTS = 8
const PREVIEW_LINES = 20
/**
* Quick Open dialog (ctrl+shift+p / cmd+shift+p).
* Fuzzy file finder with a syntax-highlighted preview of the focused file.
*/
export function QuickOpenDialog({ onDone, onInsert }: Props): React.ReactNode {
useRegisterOverlay('quick-open')
const { columns, rows } = useTerminalSize()
// Chrome (title + search + hints + pane border + gaps) eats ~14 rows.
// Shrink the list on short terminals so the dialog doesn't clip.
const visibleResults = Math.min(VISIBLE_RESULTS, Math.max(4, rows - 14))
const [results, setResults] = useState<string[]>([])
const [query, setQuery] = useState('')
const [focusedPath, setFocusedPath] = useState<string | undefined>(undefined)
const [preview, setPreview] = useState<{
path: string
content: string
} | null>(null)
const queryGenRef = useRef(0)
useEffect(() => () => void queryGenRef.current++, [])
const previewOnRight = columns >= 120
// Side preview sits in a fixed-height row alongside the list (visibleCount
// rows), so overflowing that height garbles the layout — cap to fit, minus
// one for the path header line.
const effectivePreviewLines = previewOnRight
? VISIBLE_RESULTS - 1
: PREVIEW_LINES
// A generation counter invalidates stale results if the user types faster
// than the index can respond.
const handleQueryChange = (q: string) => {
setQuery(q)
const gen = ++queryGenRef.current
if (!q.trim()) {
// generateFileSuggestions('') returns raw readdir() of cwd (designed for
// @-mentions). For Quick Open that's just noise — show the empty state.
setResults([])
return
}
void generateFileSuggestions(q, true).then(items => {
if (gen !== queryGenRef.current) return
// Filter out directory entries — they come back with a trailing path.sep
// from getTopLevelPaths() and would cause readFileInRange to throw EISDIR,
// leaving the preview pane stuck on "Loading preview…".
// Normalize separators to '/' so truncatePathMiddle (which uses
// lastIndexOf('/')) can find the filename on Windows too.
const paths = items
.filter(i => i.id.startsWith('file-'))
.map(i => i.displayText)
.filter(p => !p.endsWith(path.sep))
.map(p => p.split(path.sep).join('/'))
setResults(paths)
})
}
// Load a short preview of the focused file. Each navigation aborts the
// previous read so holding ↓ doesn't pile up whole-file reads and so a
// slow early read can't overwrite a faster later one. The stale preview
// stays visible until the new one arrives — renderPreview overlays a dim
// loading indicator rather than blanking the pane.
useEffect(() => {
if (!focusedPath) {
// No results — clear so the empty-state renders instead of a stale
// preview from a previous query.
setPreview(null)
return
}
const controller = new AbortController()
const absolute = path.resolve(getCwd(), focusedPath)
void readFileInRange(
absolute,
0,
effectivePreviewLines,
undefined,
controller.signal,
)
.then(r => {
if (controller.signal.aborted) return
setPreview({ path: focusedPath, content: r.content })
})
.catch(() => {
if (controller.signal.aborted) return
setPreview({ path: focusedPath, content: '(preview unavailable)' })
})
return () => controller.abort()
}, [focusedPath, effectivePreviewLines])
const maxPathWidth = previewOnRight
? Math.max(20, Math.floor((columns - 10) * 0.4))
: Math.max(20, columns - 8)
const previewWidth = previewOnRight
? Math.max(40, columns - maxPathWidth - 14)
: columns - 6
const handleOpen = (p: string) => {
const opened = openFileInExternalEditor(path.resolve(getCwd(), p))
logEvent('tengu_quick_open_select', {
result_count: results.length,
opened_editor: opened,
})
onDone()
}
const handleInsert = (p: string, mention: boolean) => {
onInsert(mention ? `@${p} ` : `${p} `)
logEvent('tengu_quick_open_insert', {
result_count: results.length,
mention,
})
onDone()
}
return (
<FuzzyPicker
title="Quick Open"
placeholder="Type to search files…"
items={results}
getKey={p => p}
visibleCount={visibleResults}
direction="up"
previewPosition={previewOnRight ? 'right' : 'bottom'}
onQueryChange={handleQueryChange}
onFocus={p => setFocusedPath(p)}
onSelect={handleOpen}
onTab={{ action: 'mention', handler: p => handleInsert(p, true) }}
onShiftTab={{
action: 'insert path',
handler: p => handleInsert(p, false),
}}
onCancel={onDone}
emptyMessage={q => (q ? 'No matching files' : 'Start typing to search…')}
selectAction="open in editor"
renderItem={(p, isFocused) => (
<Text color={isFocused ? 'suggestion' : undefined}>
{truncatePathMiddle(p, maxPathWidth)}
</Text>
)}
renderPreview={p =>
preview ? (
<>
<Text dimColor>
{truncatePathMiddle(p, previewWidth)}
{preview.path !== p ? ' · loading…' : ''}
</Text>
{preview.content.split('\n').map((line, i) => (
<Text key={i}>
{highlightMatch(truncateToWidth(line, previewWidth), query)}
</Text>
))}
</>
) : (
<LoadingState message="Loading preview…" dimColor />
)
}
/>
)
}