Files
boocode/apps/web/src/components/panes/terminal/SearchBar.tsx
indifferentketchup abe1a311d0 refactor: codebase audit cleanup — dead code, dedup, module splits
Multi-agent audit + aggressive cleanup across server/web/coder/booterm,
delivered behind a DEFER discipline so none of the in-flight files were
touched. Removes dead code/deps/columns, dedups server + coder helpers,
and splits the oversized modules (tools.ts, opencode-server.ts,
sentinel-summaries, turn.ts, TerminalPane.tsx) behind stable contracts.
Adds 78 parity/unit tests (server 587, coder 323); fixes two latent bugs
(ChatPane queue keys, FileViewerOverlay blank-line parity).

Intended tag: v2.7.12-audit-cleanup.
2026-06-02 21:12:29 +00:00

164 lines
4.2 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import type { SearchAddon } from '@xterm/addon-search';
import { ChevronDown, ChevronUp, X } from 'lucide-react';
import { type TermTheme } from './theme';
// ============================================================
// SearchBar — kept from v1.10.4
// ============================================================
interface SearchBarProps {
searchRef: React.MutableRefObject<SearchAddon | null>;
theme: TermTheme;
onClose: () => void;
}
export function SearchBar({ searchRef, theme, onClose }: SearchBarProps) {
const [q, setQ] = useState('');
const [counts, setCounts] = useState<{ idx: number; total: number }>({ idx: -1, total: 0 });
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
useEffect(() => {
const addon = searchRef.current;
if (!addon) return;
const sub = addon.onDidChangeResults(({ resultIndex, resultCount }) => {
setCounts({ idx: resultIndex, total: resultCount });
});
return () => sub.dispose();
}, [searchRef]);
useEffect(() => {
const addon = searchRef.current;
if (!addon) return;
if (q.length === 0) {
addon.clearDecorations?.();
setCounts({ idx: -1, total: 0 });
return;
}
addon.findNext(q, {
incremental: true,
decorations: {
matchBackground: theme.selectionBackground,
matchOverviewRuler: theme.cursor,
activeMatchBackground: theme.cursor,
activeMatchColorOverviewRuler: theme.cursor,
},
});
}, [q, searchRef, theme]);
function findNext(): void {
if (!q) return;
searchRef.current?.findNext(q);
}
function findPrev(): void {
if (!q) return;
searchRef.current?.findPrevious(q);
}
function onKey(ev: React.KeyboardEvent<HTMLInputElement>): void {
if (ev.key === 'Escape') {
ev.preventDefault();
onClose();
return;
}
if (ev.key === 'Enter') {
ev.preventDefault();
if (ev.shiftKey) findPrev();
else findNext();
}
}
return (
<div
style={{
position: 'absolute',
top: 8,
right: 8,
background: '#1a1d24',
border: '1px solid #2a2d34',
borderRadius: 8,
padding: 4,
display: 'flex',
alignItems: 'center',
gap: 4,
boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
zIndex: 40,
}}
>
<input
ref={inputRef}
value={q}
onChange={(ev) => setQ(ev.target.value)}
onKeyDown={onKey}
placeholder="Search…"
style={{
background: 'transparent',
border: 0,
outline: 'none',
color: '#d6deeb',
padding: '8px 8px',
fontSize: 13,
width: 160,
minHeight: 36,
}}
/>
{q.length > 0 && (
<span
style={{
fontSize: 11,
color: counts.total === 0 ? '#ef5350' : '#575656',
minWidth: 56,
textAlign: 'right',
padding: '0 4px',
whiteSpace: 'nowrap',
}}
>
{counts.total === 0
? 'No match'
: counts.idx === -1
? `${counts.total}+`
: `${counts.idx + 1} of ${counts.total}`}
</span>
)}
<button
type="button"
onClick={findPrev}
aria-label="Previous match"
title="Previous (Shift+Enter)"
style={iconBtnStyle}
>
<ChevronUp size={16} />
</button>
<button
type="button"
onClick={findNext}
aria-label="Next match"
title="Next (Enter)"
style={iconBtnStyle}
>
<ChevronDown size={16} />
</button>
<button
type="button"
onClick={onClose}
aria-label="Close search"
title="Close (Esc)"
style={iconBtnStyle}
>
<X size={16} />
</button>
</div>
);
}
const iconBtnStyle: React.CSSProperties = {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 44,
height: 44,
background: 'transparent',
border: 0,
color: '#d6deeb',
cursor: 'pointer',
borderRadius: 6,
};