import { useVirtualizer } from '@tanstack/react-virtual'; import { useMemo, useRef, useState } from 'react'; import { useLogs } from '../../hooks/useTasks'; import type { LogLine } from '../../types'; import { formatWallClock } from '../../util/time'; import { Pill, type PillTone } from '../shared/Pill'; interface LogsTabProps { sessionId: string; } function levelTone(level: string | null): PillTone { switch (level) { case 'ERROR': case 'FATAL': return 'error'; case 'WARN': case 'WARNING': return 'warning'; case 'INFO': return 'info'; case 'DEBUG': case 'TRACE': return 'meta'; default: return 'neutral'; } } const LEVELS = ['ALL', 'ERROR', 'WARN', 'INFO', 'DEBUG'] as const; type LevelFilter = (typeof LEVELS)[number]; function matchesLevel(line: LogLine, filter: LevelFilter): boolean { if (filter === 'ALL') return true; if (line.level === null) return false; if (filter === 'WARN') return line.level === 'WARN' || line.level === 'WARNING'; if (filter === 'ERROR') return line.level === 'ERROR' || line.level === 'FATAL'; return line.level === filter; } /** Logs tab — structured view of a session's diagnostic log. Works for both * local sessions (whose dir holds `logs/kimi-code.log`) and imported bundles * (which additionally may carry the global log). */ export function LogsTab({ sessionId }: LogsTabProps) { const [which, setWhich] = useState<'session' | 'global'>('session'); const [level, setLevel] = useState('ALL'); const [search, setSearch] = useState(''); const { data, isLoading, error } = useLogs(sessionId, which); const parentRef = useRef(null); const lines = data?.lines ?? []; const filtered = useMemo(() => { const q = search.trim().toLowerCase(); return lines.filter((l) => { if (!matchesLevel(l, level)) return false; if (!q) return true; return l.raw.toLowerCase().includes(q); }); }, [lines, level, search]); const virt = useVirtualizer({ count: filtered.length, getScrollElement: () => parentRef.current, estimateSize: () => 24, overscan: 20, getItemKey: (i) => filtered[i]?.lineNo ?? i, }); const available = data?.available ?? { session: false, global: false }; return (
{ setWhich('session'); }} disabled={!available.session && !isLoading}> session { setWhich('global'); }} disabled={!available.global}> global
{ setSearch(e.target.value); }} className="w-64 border border-border bg-surface-0 px-2 py-1 font-mono text-[12px] text-fg-0 placeholder:text-fg-3 focus:border-border-strong focus:outline-none" /> {filtered.length} / {lines.length} {data?.truncated ? ' · tail' : ''}
{isLoading ? (
loading log…
) : error ? (
{error.message}
) : lines.length === 0 ? (
{which === 'global' && !available.global ? 'no global log in this bundle (export without --include-global-log)' : 'no log available for this session'}
) : (
{data?.truncated ? (
log is large — showing the most recent {lines.length} lines
) : null}
{virt.getVirtualItems().map((vi) => { const line = filtered[vi.index]; if (!line) return null; return (
); })}
)}
); } function SegBtn({ active, onClick, disabled, children }: { active: boolean; onClick: () => void; disabled?: boolean; children: import('react').ReactNode }) { return ( ); } function LogRow({ line }: { line: LogLine }) { const fieldKeys = Object.keys(line.fields); return (
{line.time ? formatWallClock(Date.parse(line.time)) : '—'} {line.level ? ( {line.level} ) : null} {line.message} {fieldKeys.length > 0 ? ( {fieldKeys.map((k) => ( {k}={line.fields[k]} ))} ) : null}
); }