flightbox 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/paths.js ADDED
@@ -0,0 +1,26 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ export function flightboxHome() {
5
+ return process.env.FLIGHTBOX_HOME ?? path.join(os.homedir(), '.flightbox');
6
+ }
7
+ export function rawLogDir() {
8
+ return path.join(flightboxHome(), 'raw');
9
+ }
10
+ export function dbPath() {
11
+ return path.join(flightboxHome(), 'db.sqlite');
12
+ }
13
+ export function claudeHome() {
14
+ return process.env.FLIGHTBOX_CLAUDE_HOME ?? path.join(os.homedir(), '.claude');
15
+ }
16
+ export function claudeProjectsDir() {
17
+ return path.join(claudeHome(), 'projects');
18
+ }
19
+ export function claudeSettingsPath() {
20
+ return path.join(claudeHome(), 'settings.json');
21
+ }
22
+ // Static web assets are emitted by Vite to dist/web, a sibling of this compiled file
23
+ // (dist/paths.js). Resolve relative to the module so it works from any CWD and via npx.
24
+ export function webDistDir() {
25
+ return path.join(path.dirname(fileURLToPath(import.meta.url)), 'web');
26
+ }
@@ -0,0 +1,94 @@
1
+ import fs from 'node:fs';
2
+ import http from 'node:http';
3
+ import { webDistDir } from '../paths.js';
4
+ import { sessionListDto, sessionDetailDto, claimsDto } from './store-api.js';
5
+ import { resolveStaticFile } from './static.js';
6
+ function sendJson(res, status, body) {
7
+ const json = JSON.stringify(body);
8
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
9
+ res.end(json);
10
+ }
11
+ /** Decode a raw path segment into a session id, or null if invalid.
12
+ * Invalid means: URIError on decode, empty string, or contains a GLOB
13
+ * metacharacter (* ? [) that would widen the store's prefix query.
14
+ */
15
+ function decodeId(raw) {
16
+ let decoded;
17
+ try {
18
+ decoded = decodeURIComponent(raw);
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ if (decoded === '' || /[*?[]/.test(decoded))
24
+ return null;
25
+ return decoded;
26
+ }
27
+ export function createServer(store, distDir = webDistDir()) {
28
+ return http.createServer((req, res) => {
29
+ if (req.method !== 'GET') {
30
+ sendJson(res, 405, { error: 'method not allowed' });
31
+ return;
32
+ }
33
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1');
34
+ const parts = url.pathname.split('/').filter(Boolean); // e.g. ['api','sessions','abc','claims']
35
+ if (parts[0] === 'api' && parts[1] === 'sessions') {
36
+ if (parts.length === 2) {
37
+ sendJson(res, 200, sessionListDto(store));
38
+ return;
39
+ }
40
+ const id = decodeId(parts[2]);
41
+ if (id === null) {
42
+ sendJson(res, 404, { error: 'session not found' });
43
+ return;
44
+ }
45
+ if (parts.length === 3) {
46
+ const detail = sessionDetailDto(store, id);
47
+ detail ? sendJson(res, 200, detail) : sendJson(res, 404, { error: 'session not found' });
48
+ return;
49
+ }
50
+ if (parts.length === 4 && parts[3] === 'claims') {
51
+ const claims = claimsDto(store, id);
52
+ claims ? sendJson(res, 200, claims) : sendJson(res, 404, { error: 'session not found' });
53
+ return;
54
+ }
55
+ }
56
+ // Any unmatched /api/* route is a JSON 404 — never fall through to the SPA.
57
+ if (parts[0] === 'api') {
58
+ sendJson(res, 404, { error: 'not found' });
59
+ return;
60
+ }
61
+ // Non-API GET: serve the built SPA (static asset or client-route fallback).
62
+ const resolved = resolveStaticFile(distDir, url.pathname);
63
+ if (resolved) {
64
+ res.writeHead(200, { 'content-type': resolved.contentType });
65
+ fs.createReadStream(resolved.filePath).pipe(res);
66
+ return;
67
+ }
68
+ sendJson(res, 404, { error: 'not found' });
69
+ });
70
+ }
71
+ export function startServer(store, portStart = 51789, distDir = webDistDir()) {
72
+ return new Promise((resolve, reject) => {
73
+ const server = createServer(store, distDir);
74
+ let port = portStart;
75
+ const tryListen = () => {
76
+ server.listen(port, '127.0.0.1');
77
+ };
78
+ server.on('listening', () => {
79
+ const addr = server.address();
80
+ const boundPort = typeof addr === 'object' && addr ? addr.port : port;
81
+ resolve({ server, port: boundPort, url: `http://127.0.0.1:${boundPort}` });
82
+ });
83
+ server.on('error', (err) => {
84
+ if (err.code === 'EADDRINUSE' && port !== 0 && port < portStart + 50) {
85
+ port += 1;
86
+ server.close(() => tryListen());
87
+ }
88
+ else {
89
+ reject(err);
90
+ }
91
+ });
92
+ tryListen();
93
+ });
94
+ }
@@ -0,0 +1,35 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const TYPES = {
4
+ '.html': 'text/html; charset=utf-8',
5
+ '.js': 'text/javascript; charset=utf-8',
6
+ '.mjs': 'text/javascript; charset=utf-8',
7
+ '.css': 'text/css; charset=utf-8',
8
+ '.json': 'application/json; charset=utf-8',
9
+ '.svg': 'image/svg+xml',
10
+ '.png': 'image/png',
11
+ '.ico': 'image/x-icon',
12
+ '.woff2': 'font/woff2',
13
+ };
14
+ export function contentTypeFor(filePath) {
15
+ return TYPES[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream';
16
+ }
17
+ /** Resolve a URL path to a file inside distDir. Traversal-safe. Client routes
18
+ * (no file extension, no match) fall back to index.html so the SPA can route. */
19
+ export function resolveStaticFile(distDir, urlPath) {
20
+ const root = path.resolve(distDir);
21
+ const rel = urlPath === '/' ? 'index.html' : urlPath.replace(/^\/+/, '');
22
+ const candidate = path.resolve(root, rel);
23
+ if (candidate !== root && !candidate.startsWith(root + path.sep))
24
+ return null; // traversal
25
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
26
+ return { filePath: candidate, contentType: contentTypeFor(candidate) };
27
+ }
28
+ // No extension → treat as a client-side route; serve index.html.
29
+ if (path.extname(candidate) === '') {
30
+ const index = path.join(root, 'index.html');
31
+ if (fs.existsSync(index))
32
+ return { filePath: index, contentType: contentTypeFor(index) };
33
+ }
34
+ return null;
35
+ }
@@ -0,0 +1,52 @@
1
+ function durationMs(start, end) {
2
+ if (!start || !end)
3
+ return null;
4
+ const ms = new Date(end).getTime() - new Date(start).getTime();
5
+ return Number.isNaN(ms) || ms < 0 ? null : ms;
6
+ }
7
+ function discrepancy(store, id) {
8
+ if (store.hookEventCount(id) === 0)
9
+ return false;
10
+ return store.claimsForSession(id).some((c) => c.status !== 'succeeded');
11
+ }
12
+ export function sessionListDto(store) {
13
+ return store.listSessions().map((s) => ({
14
+ id: s.id,
15
+ project: s.project_dir,
16
+ startedAt: s.started_at,
17
+ endedAt: s.ended_at,
18
+ durationMs: durationMs(s.started_at, s.ended_at),
19
+ tokens: s.total_tokens,
20
+ fileCount: store.fileTouchCount(s.id),
21
+ hasDiscrepancy: discrepancy(store, s.id),
22
+ }));
23
+ }
24
+ export function sessionDetailDto(store, id) {
25
+ const s = store.findSession(id);
26
+ if (!s)
27
+ return null;
28
+ const events = store.eventsForSession(s.id);
29
+ return {
30
+ id: s.id,
31
+ project: s.project_dir,
32
+ model: s.model,
33
+ startedAt: s.started_at,
34
+ endedAt: s.ended_at,
35
+ durationMs: durationMs(s.started_at, s.ended_at),
36
+ tokens: store.sessionTokens(s.id),
37
+ fileCount: store.fileTouchCount(s.id),
38
+ commandCount: events.filter((e) => e.type === 'command').length,
39
+ subagentCount: events.filter((e) => e.type === 'subagent_spawn').length,
40
+ events: events.map((e) => ({ ts: e.ts, type: e.type, toolName: e.toolName, detail: e.detail, sidechain: e.sidechain })),
41
+ };
42
+ }
43
+ export function claimsDto(store, id) {
44
+ const s = store.findSession(id);
45
+ if (!s)
46
+ return null;
47
+ return {
48
+ sessionId: s.id,
49
+ hooksPresent: store.hookEventCount(s.id) > 0,
50
+ files: store.claimsForSession(s.id).map(({ path, status }) => ({ path, status })),
51
+ };
52
+ }
package/dist/store.js ADDED
@@ -0,0 +1,159 @@
1
+ import Database from 'better-sqlite3';
2
+ import { dbPath } from './paths.js';
3
+ const SCHEMA = `
4
+ CREATE TABLE IF NOT EXISTS sessions (
5
+ id TEXT PRIMARY KEY,
6
+ project_dir TEXT,
7
+ started_at TEXT,
8
+ ended_at TEXT,
9
+ model TEXT
10
+ );
11
+ CREATE TABLE IF NOT EXISTS events (
12
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
13
+ session_id TEXT NOT NULL,
14
+ ts TEXT NOT NULL,
15
+ type TEXT NOT NULL,
16
+ tool_name TEXT,
17
+ detail TEXT NOT NULL,
18
+ source TEXT NOT NULL,
19
+ sidechain INTEGER NOT NULL DEFAULT 0,
20
+ uniq_key TEXT NOT NULL UNIQUE
21
+ );
22
+ CREATE INDEX IF NOT EXISTS idx_events_session_ts ON events(session_id, ts);
23
+ CREATE TABLE IF NOT EXISTS file_touches (
24
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
25
+ session_id TEXT NOT NULL,
26
+ path TEXT NOT NULL,
27
+ action TEXT NOT NULL,
28
+ ts TEXT NOT NULL,
29
+ uniq_key TEXT NOT NULL UNIQUE
30
+ );
31
+ CREATE TABLE IF NOT EXISTS token_usage (
32
+ message_uuid TEXT PRIMARY KEY,
33
+ session_id TEXT NOT NULL,
34
+ model TEXT,
35
+ ts TEXT NOT NULL,
36
+ input_tokens INTEGER NOT NULL DEFAULT 0,
37
+ output_tokens INTEGER NOT NULL DEFAULT 0,
38
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
39
+ cache_creation_tokens INTEGER NOT NULL DEFAULT 0
40
+ );
41
+ CREATE TABLE IF NOT EXISTS tool_outcomes (
42
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
43
+ session_id TEXT NOT NULL,
44
+ path TEXT,
45
+ tool_name TEXT NOT NULL,
46
+ success INTEGER,
47
+ raw_response TEXT NOT NULL,
48
+ ts TEXT NOT NULL,
49
+ uniq_key TEXT NOT NULL UNIQUE
50
+ );
51
+ CREATE INDEX IF NOT EXISTS idx_outcomes_session_path ON tool_outcomes(session_id, path);
52
+ `;
53
+ const LIST_SQL = `
54
+ SELECT s.*,
55
+ (SELECT COUNT(*) FROM events e WHERE e.session_id = s.id) AS event_count,
56
+ COALESCE((SELECT SUM(input_tokens + output_tokens) FROM token_usage t WHERE t.session_id = s.id), 0) AS total_tokens
57
+ FROM sessions s
58
+ `;
59
+ export function openStore(file = dbPath()) {
60
+ const db = new Database(file);
61
+ db.pragma('journal_mode = WAL');
62
+ db.exec(SCHEMA);
63
+ const upsert = db.prepare(`
64
+ INSERT INTO sessions (id, project_dir, started_at, ended_at, model)
65
+ VALUES (@id, @projectDir, @startedAt, @endedAt, @model)
66
+ ON CONFLICT(id) DO UPDATE SET
67
+ project_dir = COALESCE(excluded.project_dir, project_dir),
68
+ started_at = COALESCE(MIN(excluded.started_at, started_at), excluded.started_at, started_at),
69
+ ended_at = COALESCE(MAX(excluded.ended_at, ended_at), excluded.ended_at, ended_at),
70
+ model = COALESCE(excluded.model, model)
71
+ `);
72
+ const insEvent = db.prepare(`
73
+ INSERT OR IGNORE INTO events (session_id, ts, type, tool_name, detail, source, sidechain, uniq_key)
74
+ VALUES (@sessionId, @ts, @type, @toolName, @detail, @source, @sidechain, @uniqKey)
75
+ `);
76
+ const insTouch = db.prepare(`
77
+ INSERT OR IGNORE INTO file_touches (session_id, path, action, ts, uniq_key)
78
+ VALUES (@sessionId, @path, @action, @ts, @uniqKey)
79
+ `);
80
+ const insUsage = db.prepare(`
81
+ INSERT OR IGNORE INTO token_usage
82
+ (message_uuid, session_id, model, ts, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens)
83
+ VALUES
84
+ (@messageUuid, @sessionId, @model, @ts, @inputTokens, @outputTokens, @cacheReadTokens, @cacheCreationTokens)
85
+ `);
86
+ const insOutcome = db.prepare(`
87
+ INSERT OR IGNORE INTO tool_outcomes (session_id, path, tool_name, success, raw_response, ts, uniq_key)
88
+ VALUES (@sessionId, @path, @toolName, @success, @rawResponse, @ts, @uniqKey)
89
+ `);
90
+ const claimsSql = db.prepare(`
91
+ SELECT ft.path AS path,
92
+ MAX(CASE WHEN o.success = 1 THEN 1 ELSE 0 END) AS any_success,
93
+ MAX(CASE WHEN o.success = 0 THEN 1 ELSE 0 END) AS any_failure
94
+ FROM file_touches ft
95
+ LEFT JOIN tool_outcomes o
96
+ ON o.session_id = ft.session_id AND o.path = ft.path
97
+ WHERE ft.session_id = ? AND ft.action IN ('edit', 'write')
98
+ GROUP BY ft.path
99
+ ORDER BY ft.path
100
+ `);
101
+ const listSql = db.prepare(`${LIST_SQL} ORDER BY s.started_at DESC`);
102
+ const findSessionSql = db.prepare(`${LIST_SQL} WHERE s.id GLOB ? || '*' LIMIT 1`);
103
+ const eventsForSessionSql = db.prepare('SELECT * FROM events WHERE session_id = ? ORDER BY ts, id');
104
+ const hookEventCountSql = db.prepare("SELECT COUNT(*) AS n FROM events WHERE session_id = ? AND source = 'hook'");
105
+ const fileTouchCountSql = db.prepare('SELECT COUNT(DISTINCT path) AS n FROM file_touches WHERE session_id = ?');
106
+ const sessionTokensSql = db.prepare(`
107
+ SELECT COALESCE(SUM(input_tokens),0) AS input, COALESCE(SUM(output_tokens),0) AS output,
108
+ COALESCE(SUM(cache_read_tokens),0) AS cacheRead, COALESCE(SUM(cache_creation_tokens),0) AS cacheCreation
109
+ FROM token_usage WHERE session_id = ?
110
+ `);
111
+ const statsByDaySql = db.prepare(`
112
+ SELECT substr(ts, 1, 10) AS day, SUM(input_tokens + output_tokens) AS tokens
113
+ FROM token_usage GROUP BY day ORDER BY day DESC LIMIT 14
114
+ `);
115
+ const statsByProjectSql = db.prepare(`
116
+ SELECT COALESCE(s.project_dir, '(unknown)') AS project, SUM(t.input_tokens + t.output_tokens) AS tokens
117
+ FROM token_usage t LEFT JOIN sessions s ON s.id = t.session_id
118
+ GROUP BY project ORDER BY tokens DESC
119
+ `);
120
+ const rowToEvent = (r) => ({
121
+ sessionId: r.session_id,
122
+ ts: r.ts,
123
+ type: r.type,
124
+ toolName: r.tool_name,
125
+ detail: r.detail,
126
+ source: r.source,
127
+ sidechain: !!r.sidechain,
128
+ uniqKey: r.uniq_key,
129
+ });
130
+ return {
131
+ upsertSession: (p) => upsert.run({
132
+ id: p.id,
133
+ projectDir: p.projectDir ?? null,
134
+ startedAt: p.startedAt ?? null,
135
+ endedAt: p.endedAt ?? null,
136
+ model: p.model ?? null,
137
+ }),
138
+ insertEvent: (e) => insEvent.run({ ...e, sidechain: e.sidechain ? 1 : 0 }),
139
+ insertFileTouch: (t) => insTouch.run(t),
140
+ insertTokenUsage: (u) => insUsage.run(u),
141
+ listSessions: () => listSql.all(),
142
+ findSession: (idPrefix) => findSessionSql.get(idPrefix),
143
+ eventsForSession: (sessionId) => eventsForSessionSql.all(sessionId).map(rowToEvent),
144
+ hookEventCount: (sessionId) => hookEventCountSql.get(sessionId).n,
145
+ fileTouchCount: (sessionId) => fileTouchCountSql.get(sessionId).n,
146
+ sessionTokens: (sessionId) => {
147
+ const r = sessionTokensSql.get(sessionId);
148
+ return { input: r.input, output: r.output, cacheRead: r.cacheRead, cacheCreation: r.cacheCreation };
149
+ },
150
+ statsByDay: () => statsByDaySql.all(),
151
+ statsByProject: () => statsByProjectSql.all(),
152
+ insertToolOutcome: (o) => insOutcome.run({ ...o, success: o.success === null ? null : o.success ? 1 : 0 }),
153
+ claimsForSession: (sessionId) => claimsSql.all(sessionId).map((r) => ({
154
+ path: r.path,
155
+ status: r.any_success ? 'succeeded' : r.any_failure ? 'failed' : 'attempted',
156
+ })),
157
+ close: () => db.close(),
158
+ };
159
+ }
@@ -0,0 +1 @@
1
+ export const VERSION = '0.1.0';
@@ -0,0 +1 @@
1
+ :root{--bg: #07090b;--bg-2: #0b0f12;--panel: #0e1317;--panel-2: #121a20;--line: rgba(120, 200, 220, .09);--line-strong: rgba(255, 168, 76, .22);--amber: #ff9a2e;--amber-soft: #ffb768;--amber-glow: rgba(255, 154, 46, .35);--cyan: #4fe0c4;--cyan-glow: rgba(79, 224, 196, .3);--red: #ff5d5d;--red-glow: rgba(255, 93, 93, .3);--violet: #a98bff;--text: #e7eef2;--muted: #8497a1;--faint: #56676f;--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;--hud: "Chakra Petch", system-ui, sans-serif;--radius: 4px}*{box-sizing:border-box}html,body{margin:0;padding:0}body{background:var(--bg);color:var(--text);font-family:var(--hud);font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased;min-height:100vh;position:relative;overflow-x:hidden}body:before{content:"";position:fixed;top:0;right:0;bottom:0;left:0;z-index:-2;background:radial-gradient(1200px 600px at 78% -10%,rgba(255,154,46,.12),transparent 60%),radial-gradient(900px 500px at 8% 0%,rgba(79,224,196,.06),transparent 55%),linear-gradient(var(--line) 1px,transparent 1px),linear-gradient(90deg,var(--line) 1px,transparent 1px),var(--bg);background-size:100% 100%,100% 100%,44px 44px,44px 44px,100% 100%;background-position:center}body:after{content:"";position:fixed;top:0;right:0;bottom:0;left:0;z-index:-1;pointer-events:none;background-image:repeating-linear-gradient(to bottom,#0000 0,#0000 2px,#00000029 3px);opacity:.4;mix-blend-mode:multiply}.cockpit-header{position:sticky;top:0;z-index:10;display:flex;align-items:center;gap:18px;padding:14px clamp(16px,4vw,42px);background:linear-gradient(180deg,#07090bf5,#07090bd1);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);border-bottom:1px solid var(--line-strong)}.brand{display:flex;align-items:baseline;gap:12px;text-decoration:none;color:var(--text)}.brand .mark{font-family:var(--hud);font-weight:700;font-size:21px;letter-spacing:.34em;text-transform:uppercase;color:var(--text);text-shadow:0 0 18px rgba(255,154,46,.18)}.brand .mark b{color:var(--amber);font-weight:700}.brand .sub{font-family:var(--mono);font-size:10px;letter-spacing:.28em;text-transform:uppercase;color:var(--faint)}.rec{margin-left:auto;display:inline-flex;align-items:center;gap:8px;font-family:var(--mono);font-size:11px;letter-spacing:.22em;color:var(--muted)}.rec .led{width:9px;height:9px;border-radius:50%;background:var(--red);box-shadow:0 0 10px 1px var(--red-glow);animation:blink 1.6s steps(1,end) infinite}@keyframes blink{0%,55%{opacity:1}56%,to{opacity:.25}}.shell{max-width:1120px;margin:0 auto;padding:clamp(20px,4vw,40px) clamp(16px,4vw,42px) 80px}.section-label{font-family:var(--mono);font-size:11px;letter-spacing:.26em;text-transform:uppercase;color:var(--faint);margin:0 0 14px;display:flex;align-items:center;gap:10px}.section-label:after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--line-strong),transparent)}.state{font-family:var(--mono);letter-spacing:.12em;color:var(--muted);padding:28px 4px}.state.error{color:var(--red)}.back{display:inline-flex;align-items:center;gap:8px;font-family:var(--mono);font-size:12px;letter-spacing:.14em;color:var(--muted);text-decoration:none;margin-bottom:22px;transition:color .15s}.back:hover{color:var(--amber)}.flightlog{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:13px}.flightlog thead th{text-align:left;font-family:var(--hud);font-weight:600;font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--faint);padding:0 16px 12px;border-bottom:1px solid var(--line-strong)}.flightlog tbody tr{border-bottom:1px solid var(--line);transition:background .14s;animation:rise .5s cubic-bezier(.2,.7,.2,1) both;animation-delay:calc(var(--i, 0) * 34ms)}.flightlog tbody tr:hover{background:#ff9a2e0d}.flightlog tbody td{padding:13px 16px;color:var(--text);vertical-align:middle}.flightlog td.id a{color:var(--amber);text-decoration:none;letter-spacing:.08em;border-bottom:1px solid transparent;transition:border-color .14s,text-shadow .14s}.flightlog tr:hover td.id a{border-color:var(--amber);text-shadow:0 0 12px var(--amber-glow)}.flightlog td.project{color:var(--text);font-family:var(--hud);letter-spacing:.02em}.flightlog td.dim{color:var(--muted)}.flightlog td.num{color:var(--cyan);text-align:right}.flightlog td.badge{text-align:center;width:42px}.warn-led{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;color:var(--amber);background:#ff9a2e1f;box-shadow:0 0 12px var(--amber-glow),inset 0 0 0 1px var(--line-strong);font-size:12px}.summary-head{margin-bottom:26px}.summary-head .project-name{font-family:var(--hud);font-weight:600;font-size:clamp(22px,3.4vw,34px);letter-spacing:.01em;margin:4px 0 6px;color:var(--text);word-break:break-word}.summary-head .sid{font-family:var(--mono);font-size:12px;letter-spacing:.14em;color:var(--faint)}.gauges{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px;margin-bottom:34px}.gauge{position:relative;background:linear-gradient(180deg,var(--panel-2),var(--panel));border:1px solid var(--line);border-radius:var(--radius);padding:14px 15px 15px;overflow:hidden;animation:rise .55s cubic-bezier(.2,.7,.2,1) both;animation-delay:calc(var(--i, 0) * 55ms)}.gauge:before{content:"";position:absolute;top:0;left:0;width:26px;height:1px;background:var(--amber);box-shadow:0 0 8px var(--amber-glow)}.gauge .label{font-family:var(--hud);font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--faint);margin-bottom:9px}.gauge .value{font-family:var(--mono);font-weight:500;font-size:22px;letter-spacing:.01em;color:var(--text);line-height:1.15;word-break:break-word}.gauge .value.amber{color:var(--amber-soft)}.gauge .value.cyan{color:var(--cyan)}.gauge .value small{display:block;font-size:11px;color:var(--muted);letter-spacing:.06em;margin-top:4px}.panel{background:linear-gradient(180deg,#121a2080,#0e131780);border:1px solid var(--line);border-radius:var(--radius);padding:20px clamp(14px,2.5vw,24px);margin-bottom:22px}.controls{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:18px}.controls label{display:flex;flex-direction:column;gap:6px;font-family:var(--hud);font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--faint)}.controls select,.controls input{font-family:var(--mono);font-size:13px;color:var(--text);background:var(--bg-2);border:1px solid var(--line-strong);border-radius:var(--radius);padding:9px 12px;min-width:190px;outline:none;transition:border-color .15s,box-shadow .15s}.controls select:focus,.controls input:focus{border-color:var(--amber);box-shadow:0 0 0 1px var(--amber),0 0 16px var(--amber-glow)}.controls input::placeholder{color:var(--faint)}.feed{list-style:none;margin:0;padding:0;position:relative}.feed:before{content:"";position:absolute;left:128px;top:4px;bottom:4px;width:1px;background:linear-gradient(180deg,transparent,var(--line-strong),transparent)}.event{display:grid;grid-template-columns:120px 1fr;gap:16px;align-items:baseline;padding:8px 0;animation:fade .4s ease both;animation-delay:calc(var(--i, 0) * 16ms)}.event time{font-family:var(--mono);font-size:12px;color:var(--faint);text-align:right;padding-right:4px}.event .body{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;position:relative}.event .body:before{content:"";position:absolute;left:-20px;top:8px;width:7px;height:7px;border-radius:50%;background:var(--faint);box-shadow:0 0 0 3px var(--bg)}.event.sidechain{padding-left:26px;opacity:.82}.event.sidechain .body:before{background:var(--violet);box-shadow:0 0 8px #a98bff66,0 0 0 3px var(--bg)}.chip{font-family:var(--mono);font-size:10px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;padding:2px 8px;border-radius:3px;border:1px solid currentColor;white-space:nowrap}.chip.command{color:var(--amber)}.chip.file_touch{color:var(--cyan)}.chip.subagent_spawn{color:var(--violet)}.chip.tool_call{color:var(--muted)}.chip.session_start,.chip.session_end{color:var(--faint)}.event .tool{font-family:var(--mono);font-size:12px;color:var(--amber-soft);background:#ff9a2e14;padding:1px 6px;border-radius:3px}.event .detail{font-family:var(--mono);font-size:13px;color:var(--text);word-break:break-word}.feed-empty{font-family:var(--mono);color:var(--muted);padding:10px 0}.caution{display:flex;align-items:center;gap:12px;font-family:var(--mono);font-size:12.5px;color:var(--amber-soft);background:repeating-linear-gradient(45deg,#ff9a2e17 0,#ff9a2e17 10px,#ff9a2e08 10px,#ff9a2e08 20px);border:1px solid var(--line-strong);border-left:3px solid var(--amber);border-radius:var(--radius);padding:11px 14px;margin-bottom:16px}.claims{list-style:none;margin:0;padding:0}.claim{display:flex;align-items:center;gap:14px;padding:10px 4px;border-bottom:1px solid var(--line)}.claim:last-child{border-bottom:none}.status-led{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:50%;font-size:13px;flex-shrink:0;border:1px solid currentColor}.status-led.succeeded{color:var(--cyan);box-shadow:0 0 12px var(--cyan-glow)}.status-led.failed{color:var(--red);box-shadow:0 0 12px var(--red-glow)}.status-led.attempted{color:var(--amber);box-shadow:0 0 12px var(--amber-glow)}.claim .path{font-family:var(--mono);font-size:13px;color:var(--text);word-break:break-all}.claims-ok{font-family:var(--mono);font-size:13px;color:var(--cyan);letter-spacing:.04em;margin:14px 0 0}.claims-empty{font-family:var(--mono);color:var(--muted)}@keyframes rise{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}@keyframes fade{0%{opacity:0}to{opacity:1}}@media(max-width:620px){.feed:before{display:none}.event{grid-template-columns:1fr;gap:4px}.event time{text-align:left}.event .body:before{display:none}.flightlog td.project{max-width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}