Files
boocode/apps/web/src/components/FileViewerOverlay.tsx
indifferentketchup 8c200216eb 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.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 21:12:29 +00:00

244 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useRef, useState } from 'react';
import { Check, Copy, X, Paperclip } from 'lucide-react';
import { codeToHtml } from 'shiki';
import { sessionEvents } from '@/hooks/sessionEvents';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
interface Props {
path: string;
content: string;
lang: string | null;
onClose: () => void;
}
const SHIKI_THEME = 'github-dark';
function splitShikiLines(html: string): string[] {
const match = html.match(/<code[^>]*>([\s\S]*)<\/code>/);
if (!match) return [];
const inner = match[1]!;
const lines = inner.split(/(?=<span class="line">)/);
// Keep every line segment (including blank `<span class="line"></span>`
// entries) so lineHtmls.length matches the raw line count and line-number
// indexing stays in sync. The first element before the first line span is
// discarded (it's always empty or whitespace).
return lines.filter(l => l.startsWith('<span class="line">'));
}
function basename(path: string): string {
const parts = path.split('/');
return parts[parts.length - 1] ?? path;
}
export function FileViewerOverlay({ path, content, lang, onClose }: Props) {
const [copied, setCopied] = useState(false);
const [lineHtmls, setLineHtmls] = useState<string[] | null>(null);
const [selectedLines, setSelectedLines] = useState<Set<number>>(new Set());
const [showAttachPopover, setShowAttachPopover] = useState(false);
const draggingRef = useRef(false);
const dragStartRef = useRef<number | null>(null);
const overlayRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setSelectedLines(new Set());
setShowAttachPopover(false);
if (!lang) { setLineHtmls(null); return; }
let cancelled = false;
(async () => {
try {
const result = await codeToHtml(content, { lang, theme: SHIKI_THEME });
if (!cancelled) {
const lines = splitShikiLines(result);
setLineHtmls(lines.length > 0 ? lines : null);
}
} catch { if (!cancelled) setLineHtmls(null); }
})();
return () => { cancelled = true; };
}, [content, lang]);
const plainLines = content.split('\n');
const totalLines = lineHtmls ? lineHtmls.length : plainLines.length;
async function copyAll() {
try {
await navigator.clipboard.writeText(content);
setCopied(true);
setTimeout(() => setCopied(false), 1200);
} catch { /* ignore */ }
}
function handleLineMouseDown(lineNo: number, e: React.MouseEvent) {
if (e.shiftKey && dragStartRef.current !== null) {
const start = dragStartRef.current;
const min = Math.min(start, lineNo);
const max = Math.max(start, lineNo);
const next = new Set<number>();
for (let i = min; i <= max; i++) next.add(i);
setSelectedLines(next);
setShowAttachPopover(true);
return;
}
draggingRef.current = true;
dragStartRef.current = lineNo;
setSelectedLines(new Set([lineNo]));
setShowAttachPopover(false);
}
function handleLineMouseEnter(lineNo: number) {
if (!draggingRef.current || dragStartRef.current === null) return;
const start = dragStartRef.current;
const min = Math.min(start, lineNo);
const max = Math.max(start, lineNo);
const next = new Set<number>();
for (let i = min; i <= max; i++) next.add(i);
setSelectedLines(next);
}
const handleMouseUp = useCallback(() => {
if (draggingRef.current) {
draggingRef.current = false;
if (selectedLines.size > 0) setShowAttachPopover(true);
}
}, [selectedLines.size]);
useEffect(() => {
document.addEventListener('mouseup', handleMouseUp);
return () => document.removeEventListener('mouseup', handleMouseUp);
}, [handleMouseUp]);
useEffect(() => {
function handleClick(e: MouseEvent) {
if (overlayRef.current && !overlayRef.current.contains(e.target as Node)) {
onClose();
}
}
document.addEventListener('mousedown', handleClick);
return () => document.removeEventListener('mousedown', handleClick);
}, [onClose]);
useEffect(() => {
function handleKey(e: KeyboardEvent) {
if (e.key === 'Escape') onClose();
}
document.addEventListener('keydown', handleKey);
return () => document.removeEventListener('keydown', handleKey);
}, [onClose]);
function getSelectionRange(): { min: number; max: number } | null {
if (selectedLines.size === 0) return null;
let min = Infinity;
let max = -Infinity;
for (const n of selectedLines) {
if (n < min) min = n;
if (n > max) max = n;
}
return { min, max };
}
function handleAttach() {
const range = getSelectionRange();
if (!range) return;
const lines = content.split('\n').slice(range.min - 1, range.max);
sessionEvents.emit({
type: 'attach_chat_file',
attachment: {
kind: 'lines',
filename: path,
language: lang,
content: lines.join('\n'),
range: [range.min, range.max],
source: 'line-select',
},
});
setSelectedLines(new Set());
setShowAttachPopover(false);
}
const range = getSelectionRange();
const attachLabel = range
? range.min === range.max
? `Attach line ${range.min} to chat`
: `Attach lines ${range.min}${range.max} to chat`
: '';
return (
<div className="fixed inset-0 z-50 flex items-start justify-center pt-12 pb-12">
<div className="absolute inset-0 bg-black/40" />
<div
ref={overlayRef}
className="relative bg-background border rounded-lg shadow-xl flex flex-col w-[80vw] max-w-[1000px] max-h-[80vh] overflow-hidden"
>
<div className="flex items-center gap-2 px-4 py-2 border-b shrink-0">
<span className="text-sm font-medium truncate flex-1" title={path}>
{basename(path)}
</span>
<span className="text-xs text-muted-foreground truncate">{path}</span>
<button
type="button"
onClick={() => void copyAll()}
className="flex items-center gap-1 text-xs px-2 py-1 rounded hover:bg-muted"
>
{copied ? <Check size={12} /> : <Copy size={12} />}
{copied ? 'Copied' : 'Copy'}
</button>
<button
type="button"
onClick={onClose}
className="p-1 rounded hover:bg-muted"
aria-label="Close"
>
<X size={16} />
</button>
</div>
{/* Shiki-highlighted code lines are generated from source code files, not user content */}
<div className="flex-1 overflow-auto text-sm font-mono select-none">
{Array.from({ length: totalLines }, (_, i) => {
const lineNo = i + 1;
const isSelected = selectedLines.has(lineNo);
return (
<div
key={lineNo}
className={cn('flex', isSelected && 'bg-blue-500/10')}
onMouseDown={(e) => handleLineMouseDown(lineNo, e)}
onMouseEnter={() => handleLineMouseEnter(lineNo)}
>
<div
className="shrink-0 w-[3.5ch] text-right pr-2 text-xs text-muted-foreground select-none cursor-pointer hover:text-foreground"
style={{ fontVariantNumeric: 'tabular-nums' }}
>
{lineNo}
</div>
{lineHtmls ? (
<div
className="flex-1 min-w-0 text-xs leading-relaxed [&>.line]:!bg-transparent"
dangerouslySetInnerHTML={{ __html: lineHtmls[i] ?? '' }}
/>
) : (
<span className="flex-1 min-w-0 text-xs leading-relaxed whitespace-pre">
{plainLines[i] ?? ''}
</span>
)}
</div>
);
})}
</div>
{showAttachPopover && range && (
<div className="sticky bottom-0 border-t bg-background px-4 py-2 flex items-center gap-2">
<Paperclip size={14} className="text-muted-foreground" />
<span className="text-xs flex-1">{attachLabel}</span>
<Button size="sm" onClick={handleAttach}>
Attach
</Button>
<Button size="sm" variant="ghost" onClick={() => { setSelectedLines(new Set()); setShowAttachPopover(false); }}>
Cancel
</Button>
</div>
)}
</div>
</div>
);
}