Tesseract / studio /src /components /LogsPanel.jsx
rkbosamia's picture
Tesseract major version upgrade - v2
9c49532
Raw
History Blame Contribute Delete
5.62 kB
import { useEffect, useMemo, useRef } from 'react'
import useAppStore from '../store/appStore'
const KIND_COLOR = {
frontend: '#60a5fa',
backend: '#34d399',
install: '#f59e0b',
}
// Heuristic: lines that look like real errors / stack traces. Intentionally
// broad — false positives just mean the user gets a "Fix" affordance on a
// harmless line, which they can ignore. False negatives are worse.
const ERROR_RE = /\b(error|exception|traceback|enoent|cannot find module|syntaxerror|typeerror|referenceerror|fatal)\b|^✗|^\s*at\s+\S+\s*\(/i
// Common false positives we'd rather not light up: log lines that explicitly
// report success/no-error, and the literal word "error" in non-error context.
const NOT_ERROR_RE = /\b(no errors?|0 errors?|error[-_]free)\b/i
function isErrorLine(line) {
if (!line) return false
if (NOT_ERROR_RE.test(line)) return false
return ERROR_RE.test(line)
}
// Bundle a window of context around an error line — a few lines before, the
// hit, a few after. The agent has `read_logs` if it needs more, but this
// gives it enough to start without an extra tool roundtrip.
function buildContext(logs, idx, before = 6, after = 4) {
const start = Math.max(0, idx - before)
const end = Math.min(logs.length, idx + after + 1)
return logs.slice(start, end)
.map((e) => `[${e.kind}] ${e.line}`)
.join('\n')
}
function FixPill({ onClick, disabled }) {
return (
<button
onClick={(e) => { e.stopPropagation(); onClick() }}
disabled={disabled}
className="opacity-0 group-hover:opacity-100 transition-opacity shrink-0 ml-2 px-2 py-0.5 text-[10px] font-semibold rounded border mono disabled:opacity-30"
style={{
background: 'rgba(239, 68, 68, 0.12)',
borderColor: 'rgba(239, 68, 68, 0.4)',
color: '#fca5a5',
userSelect: 'none',
}}
title="Send this error to the AI and have it fix the root cause"
>
✦ Fix with AI
</button>
)
}
export default function LogsPanel() {
const logs = useAppStore((s) => s.runtime.logs)
const mode = useAppStore((s) => s.mode)
const setMode = useAppStore((s) => s.setMode)
const setComposerDraft = useAppStore((s) => s.setComposerDraft)
const activeFile = useAppStore((s) => s.project.activeFile)
const showToast = useAppStore((s) => s.showToast)
const isStreaming = useAppStore((s) => s.isStreaming)
const bottomRef = useRef(null)
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'instant' })
}, [logs.length])
// Memoize the error flags so we don't re-regex on every render of every row.
const errorMask = useMemo(
() => logs.map((e) => isErrorLine(e.line)),
[logs],
)
function fixThis(idx) {
const ctx = buildContext(logs, idx)
const entry = logs[idx]
const kindLabel = entry?.kind === 'install' ? 'install step'
: entry?.kind === 'backend' ? 'backend dev server'
: entry?.kind === 'frontend' ? 'frontend dev server'
: 'dev server'
const text =
`The ${kindLabel} just emitted an error. Here's the log window around it:
\`\`\`
${ctx}
\`\`\`
Find the file(s) involved, read them, and fix the root cause. Use \`read_logs\` if you need broader context.`
// Auto-pin the active file if there is one — it's usually what the user
// was just editing and likely the source of the regression.
const pins = activeFile ? [activeFile] : []
setComposerDraft({ text, pinnedFiles: pins })
// If we're in CODE mode, flip to VIBE so the composer is visible and the
// draft actually lands somewhere the user can see.
if (mode !== 'vibe') setMode('vibe')
showToast('Sent to chat — review and press Enter')
}
return (
<div
style={{
height: '100%',
overflowY: 'auto',
background: 'var(--code-bg)',
padding: '8px 12px',
fontFamily: 'ui-monospace, "JetBrains Mono", monospace',
fontSize: 11.5,
color: 'var(--code-text)',
lineHeight: 1.7,
}}
>
{logs.length === 0 ? (
<div style={{ color: 'var(--text-faint)', paddingTop: 4 }}>
No logs yet. Click Run to start the dev servers.
</div>
) : (
logs.map((entry, i) => {
const isError = errorMask[i]
return (
<div
key={i}
className={isError ? 'group' : ''}
style={{
display: 'flex',
gap: 8,
alignItems: 'flex-start',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
background: isError ? 'rgba(239, 68, 68, 0.06)' : 'transparent',
margin: isError ? '0 -12px' : 0,
padding: isError ? '0 12px' : 0,
borderLeft: isError ? '2px solid rgba(239, 68, 68, 0.4)' : '2px solid transparent',
}}
>
<span style={{ color: KIND_COLOR[entry.kind] ?? '#6b7280', flexShrink: 0, minWidth: 72, userSelect: 'none' }}>
[{entry.kind}]
</span>
<span style={{ color: 'var(--code-text)', flex: 1, minWidth: 0 }}>
{entry.line}
</span>
{isError && (
<FixPill
onClick={() => fixThis(i)}
disabled={isStreaming}
/>
)}
</div>
)
})
)}
<div ref={bottomRef} />
</div>
)
}