mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
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>
182 lines
6.3 KiB
TypeScript
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 />
|
|
)
|
|
}
|
|
/>
|
|
)
|
|
}
|