Files
scarf/design/static-site/ui-kit/Logs.jsx
T
Alan Wizemann 8a2d89654b feat(design): adopt ScarfDesign system across Mac UI
Add a typed design-system package (Packages/ScarfDesign) with rust-tone
color tokens, type scale, spacing/radius tokens, ScarfPageHeader and
component primitives (ScarfCard, ScarfBadge, ScarfTextField,
ScarfSectionHeader, ScarfDivider, four button styles). Both Mac and iOS
targets `import ScarfDesign`.

Sidebar redesigned per design/static-site/ui-kit/Sidebar.jsx — glassy
translucent background, 224 px width, app-icon header with server pill,
custom tokenized rows with rust accent-tint when active, footer with
live Hermes-running indicator (wired to ServerLiveStatusRegistry).

14 mockup-backed feature screens redesigned: Settings, Dashboard,
Sessions, Memory, Chat (visual sweep), Activity, Cron, Insights,
MCPServers, Health, Logs, Tools (full); Projects light-touch.
Non-mockup features inherit rust through AccentColor.colorset repoint.

Mac AppIcon.appiconset replaced with the rust set. AccentColor.colorset
repointed to BrandRust hex (light + dark variants).

Visual sweep: every multi-button page-header / action-bar cluster now
wraps in .fixedSize(horizontal: true, vertical: false) so labels can't
wrap letter-by-letter at narrow widths (regression seen on the MCP
detail pane with 4 buttons).

Follow-ups landed:
- Sidebar Hermes-running probe wired to per-window
  ServerLiveStatusRegistry (no more placeholder green).
- Sessions: today filter predicate (isDateInToday(startedAt)); pill
  count reflects real count. Starred stays a no-op pending an upstream
  pinned/starred field on HermesSession.
- Dashboard: Recent activity column rendered alongside Recent sessions
  in a ViewThatFits 2-col grid. Populated from
  HermesDataService.fetchRecentToolCalls(limit:) flattened to
  ActivityEntry. ActivityEntry gains a public memberwise init.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:27:54 +02:00

124 lines
6.2 KiB
React

// Logs — streaming monospace surface. Filter pills + a fake live tail.
const LOG_LINES = [
{ ts: '09:42:18.124', level: 'info', source: 'gateway', msg: 'POST /v1/messages → 200 (1.2s, 482 tokens out)' },
{ ts: '09:42:18.066', level: 'debug', source: 'tool', msg: 'tool_call read_file path=src/App.jsx (8.2KB)' },
{ ts: '09:42:17.880', level: 'info', source: 'agent', msg: 'turn 14 started — personality=Forge model=claude-sonnet-4.5' },
{ ts: '09:42:15.341', level: 'warn', source: 'mcp', msg: 'github: rate-limit warning 4500/5000 used this hour' },
{ ts: '09:42:11.012', level: 'info', source: 'tool', msg: 'tool_call execute cmd="npm test -- --watch=false" status=ok 14.2s' },
{ ts: '09:42:01.508', level: 'error', source: 'tool', msg: 'tool_call execute denied: command "rm -rf node_modules" matches deny rule "rm -rf"' },
{ ts: '09:41:58.211', level: 'info', source: 'agent', msg: 'user message received (1.4KB)' },
{ ts: '09:41:42.004', level: 'debug', source: 'memory', msg: 'AGENTS.md hash unchanged (4f02…ab19), skipping reload' },
{ ts: '09:41:30.882', level: 'info', source: 'cron', msg: 'incident-triage finished ok (4.2s)' },
{ ts: '09:41:26.108', level: 'info', source: 'cron', msg: 'incident-triage started' },
{ ts: '09:41:18.443', level: 'info', source: 'mcp', msg: 'linear: tools/list 9 tools (142ms)' },
{ ts: '09:40:54.221', level: 'warn', source: 'gateway', msg: 'approval pending: tool_call execute cmd="git push origin main" (12s)' },
{ ts: '09:40:42.001', level: 'info', source: 'agent', msg: 'turn 13 ended — 2.1s, 7 tool calls, $0.0042' },
{ ts: '09:40:21.778', level: 'debug', source: 'tool', msg: 'tool_call list_files path=ui_kits/scarf-mac (24 entries)' },
{ ts: '09:40:18.422', level: 'error', source: 'mcp', msg: 'notion: TLS handshake failed (timeout 5s) — backing off 30s' },
{ ts: '09:40:02.114', level: 'info', source: 'agent', msg: 'session resumed (idle 14m)' },
];
const LEVEL_TONES = {
debug: '#7C7263', info: 'var(--blue-500)', warn: 'var(--amber-500)', error: 'var(--red-500)',
};
function Logs() {
const [level, setLevel] = React.useState(['info', 'warn', 'error']);
const [source, setSource] = React.useState('all');
const [search, setSearch] = React.useState('');
const [follow, setFollow] = React.useState(true);
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
const sources = ['all', 'agent', 'tool', 'gateway', 'mcp', 'cron', 'memory'];
const filtered = LOG_LINES.filter(l => {
if (!level.includes(l.level)) return false;
if (source !== 'all' && l.source !== source) return false;
if (search && !l.msg.toLowerCase().includes(search.toLowerCase())) return false;
return true;
});
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Logs"
subtitle="Live tail across the gateway, agent, tools, MCP servers, and cron"
actions={<>
<Btn icon="download">Export</Btn>
<Btn icon={follow ? 'pause' : 'play'} onClick={() => setFollow(!follow)}>
{follow ? 'Pause' : 'Follow'}
</Btn>
</>} />
{/* Toolbar */}
<div style={{
padding: '12px 24px', borderBottom: '0.5px solid var(--border)',
background: 'var(--bg-card)', display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap',
}}>
<TextInput value={search} onChange={setSearch} leftIcon="search" placeholder="Filter messages…" mono width={280} />
<div style={{ display: 'flex', gap: 4 }}>
{['debug', 'info', 'warn', 'error'].map(lv => {
const on = level.includes(lv);
return (
<button key={lv} onClick={() => setLevel(on ? level.filter(x => x !== lv) : [...level, lv])} style={{
padding: '4px 10px', borderRadius: 6, border: '0.5px solid var(--border)',
background: on ? 'var(--bg-tertiary)' : 'var(--bg-card)',
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
color: on ? LEVEL_TONES[lv] : 'var(--fg-faint)',
textTransform: 'uppercase', cursor: 'pointer', letterSpacing: '0.04em',
}}>{lv}</button>
);
})}
</div>
<div style={{ display: 'flex', gap: 4, marginLeft: 'auto' }}>
{sources.map(s => (
<button key={s} onClick={() => setSource(s)} style={{
padding: '4px 10px', borderRadius: 6, border: 'none',
background: source === s ? 'var(--accent-tint)' : 'transparent',
color: source === s ? 'var(--accent-active)' : 'var(--fg-muted)',
fontFamily: 'var(--font-mono)', fontSize: 11, cursor: 'pointer',
}}>{s}</button>
))}
</div>
</div>
{/* Tail */}
<div style={{
flex: 1, overflowY: 'auto', background: '#1F1B16', color: '#E8E1D2',
fontFamily: 'var(--font-mono)', fontSize: 12, lineHeight: 1.7,
padding: '12px 0',
}}>
{filtered.map((l, i) => <LogRow key={i} l={l} />)}
{follow && (
<div style={{ padding: '6px 24px', display: 'flex', alignItems: 'center', gap: 8,
color: '#A89B82', fontSize: 11 }}>
<span style={{ width: 6, height: 6, borderRadius: 3, background: 'var(--green-500)',
animation: 'pulse 1.4s ease-in-out infinite' }}></span>
following 4 lines/sec
</div>
)}
</div>
<style>{`
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
`}</style>
</div>
);
}
function LogRow({ l }) {
return (
<div style={{
display: 'flex', gap: 14, padding: '1px 24px', alignItems: 'baseline',
}}>
<span style={{ color: '#7C7263', fontSize: 11, width: 100, flexShrink: 0 }}>{l.ts}</span>
<span style={{ color: LEVEL_TONES[l.level], width: 50, flexShrink: 0,
textTransform: 'uppercase', fontSize: 10, fontWeight: 700, letterSpacing: '0.04em' }}>{l.level}</span>
<span style={{ color: '#A89B82', width: 70, flexShrink: 0 }}>{l.source}</span>
<span style={{ color: '#E8E1D2', flex: 1 }}>{l.msg}</span>
</div>
);
}
window.Logs = Logs;