| import { useEffect, useMemo, useRef } from 'react' |
| import useAppStore from '../store/appStore' |
|
|
| const KIND_COLOR = { |
| frontend: '#60a5fa', |
| backend: '#34d399', |
| install: '#f59e0b', |
| } |
|
|
| |
| |
| |
| const ERROR_RE = /\b(error|exception|traceback|enoent|cannot find module|syntaxerror|typeerror|referenceerror|fatal)\b|^✗|^\s*at\s+\S+\s*\(/i |
|
|
| |
| |
| 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) |
| } |
|
|
| |
| |
| |
| 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]) |
|
|
| |
| 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.` |
|
|
| |
| |
| const pins = activeFile ? [activeFile] : [] |
| setComposerDraft({ text, pinnedFiles: pins }) |
|
|
| |
| |
| 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> |
| ) |
| } |
|
|