@vibecodeqa/cli 0.35.5 → 0.35.7

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.
Files changed (2) hide show
  1. package/dist/monitor.js +183 -39
  2. package/package.json +1 -1
package/dist/monitor.js CHANGED
@@ -10,9 +10,10 @@ import { useState, useEffect, useCallback, useRef } from "react";
10
10
  import { render, Box, Text, useApp, useInput, useStdout } from "ink";
11
11
  import { resolve, join, basename } from "node:path";
12
12
  import { watch, existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
13
- import { execFile } from "node:child_process";
13
+ import { execFile, execSync } from "node:child_process";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { detectStack, detectWorkspace } from "./detect.js";
16
+ import { loadHistory } from "./history.js";
16
17
  const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8")).version;
17
18
  const CLI_PATH = fileURLToPath(new URL("./cli.js", import.meta.url));
18
19
  const DEFAULTS = {
@@ -67,9 +68,19 @@ function spark(values) {
67
68
  function ts() {
68
69
  return new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
69
70
  }
70
- function bar(score, width) {
71
- const filled = Math.round((score / 100) * width);
72
- return "".repeat(filled) + "".repeat(width - filled);
71
+ function copyToClipboard(text) {
72
+ try {
73
+ const cmd = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip -selection clipboard";
74
+ execSync(cmd, { input: text, stdio: ["pipe", "pipe", "pipe"] });
75
+ return true;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ function buildFixPrompt(checkName, issue) {
82
+ const loc = issue.file ? `${issue.file}${issue.line ? `:${issue.line}` : ""}` : "";
83
+ return `Fix this ${issue.severity} in ${loc || "the project"}:\n${issue.message}${issue.rule ? ` (${issue.rule})` : ""}\nCheck: ${checkName}\n\nAnalyze the code, explain the issue, and provide the fix.`;
73
84
  }
74
85
  // ── Scan via child process — UI never freezes ──
75
86
  function runScanProcess(cwd, skipTests) {
@@ -110,12 +121,6 @@ function ScorePanel({ state, height }) {
110
121
  const active = state.checks.filter((c) => !c.details.skipped && !c.details.comingSoon);
111
122
  return (_jsxs(Box, { flexDirection: "column", width: 24, height: height, borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, color: "magenta", children: " \u25C8 Score" }), _jsx(Box, { justifyContent: "center", marginY: 1, children: _jsx(Text, { color: gc(state.grade), bold: true, children: state.scanning ? " scanning..." : ` ${state.grade} ${state.score}/100` }) }), _jsxs(Text, { dimColor: true, children: [" ", active.length, " checks \u00B7 ", state.totalIssues, " issues"] }), _jsxs(Text, { dimColor: true, children: [" ", state.duration, "ms \u00B7 scan #", state.scanCount] }), s && _jsxs(Text, { color: "cyan", children: [" ", s] })] }));
112
123
  }
113
- function ChecksPanel({ checks, height }) {
114
- const active = checks.filter((c) => !c.details.skipped && !c.details.comingSoon);
115
- const pro = checks.filter((c) => c.details.comingSoon);
116
- const visibleLines = Math.max(1, height - 3);
117
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, width: 24, height: height, overflowY: "hidden", children: [_jsx(Text, { bold: true, color: "magenta", children: " \u25C8 Checks" }), active.slice(0, visibleLines).map((c) => (_jsxs(Text, { children: [_jsxs(Text, { color: gc(c.grade), children: [" ", c.grade === "A" ? "●" : c.grade === "B" ? "◐" : "○", " "] }), _jsx(Text, { children: c.name.slice(0, 13).padEnd(13) }), _jsxs(Text, { dimColor: true, children: [" ", bar(c.score, 4), " "] }), _jsx(Text, { color: gc(c.grade), children: String(c.score).padStart(3) })] }, c.name))), active.length > visibleLines && _jsxs(Text, { dimColor: true, children: [" +", active.length - visibleLines, " more"] }), pro.length > 0 && _jsxs(Text, { color: "magenta", children: [" \u25C6 ", pro.length, " Pro"] })] }));
118
- }
119
124
  function ActivityPanel({ log, height }) {
120
125
  const colors = {
121
126
  info: "gray", scan: "cyan", change: "yellow",
@@ -124,18 +129,6 @@ function ActivityPanel({ log, height }) {
124
129
  const visibleLines = Math.max(1, height - 3);
125
130
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, height: height, overflowY: "hidden", children: [_jsx(Text, { bold: true, color: "magenta", children: " \u25C8 Activity" }), log.slice(-visibleLines).map((entry, i) => (_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { dimColor: true, children: [entry.time, " "] }), _jsx(Text, { color: colors[entry.type], children: entry.text })] }, i)))] }));
126
131
  }
127
- function IssuesPanel({ checks, height }) {
128
- const issues = checks
129
- .flatMap((c) => c.issues.map((i) => ({ check: c.name, ...i })))
130
- .sort((a, b) => {
131
- const o = { error: 0, warning: 1, info: 2 };
132
- return (o[a.severity] ?? 2) - (o[b.severity] ?? 2);
133
- });
134
- const errors = issues.filter((i) => i.severity === "error").length;
135
- const warnings = issues.filter((i) => i.severity === "warning").length;
136
- const visibleLines = Math.max(1, height - 3);
137
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, height: height, overflowY: "hidden", children: [_jsxs(Text, { bold: true, color: "magenta", children: [" ", "\u25C8 Issues (", issues.length, ")", errors > 0 && _jsxs(Text, { color: "red", children: [" ", errors, "E"] }), warnings > 0 && _jsxs(Text, { color: "yellow", children: [" ", warnings, "W"] })] }), issues.slice(0, visibleLines).map((iss, i) => (_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: sc(iss.severity), bold: true, children: [iss.severity[0].toUpperCase(), " "] }), _jsxs(Text, { dimColor: true, children: [(iss.check || "").slice(0, 11).padEnd(11), " "] }), iss.file && _jsxs(Text, { color: "cyan", children: [basename(String(iss.file)).slice(0, 18).padEnd(18), " "] }), _jsx(Text, { children: iss.message.slice(0, 45) })] }, i))), issues.length > visibleLines && _jsxs(Text, { dimColor: true, children: [" +", issues.length - visibleLines, " more"] })] }));
138
- }
139
132
  function ConfigScreen({ monCfg, onSave, onClose, }) {
140
133
  const [cursor, setCursor] = useState(0);
141
134
  const [cfg, setCfg] = useState(JSON.parse(JSON.stringify(monCfg)));
@@ -199,6 +192,55 @@ function ConfigScreen({ monCfg, onSave, onClose, }) {
199
192
  return (_jsxs(Text, { children: [_jsxs(Text, { color: selected ? "white" : "gray", children: [prefix, opt.label.padEnd(28)] }), _jsxs(Text, { color: "cyan", bold: true, children: ["\u25C0 ", String(opt.value).padStart(5), " \u25B6"] })] }, opt.key));
200
193
  }), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { dimColor: true, children: " \u2191\u2193 navigate \u00B7 Space toggle \u00B7 \u2190\u2192 adjust \u00B7 Shift+\u2190\u2192 \u00D710" }), _jsx(Text, { dimColor: true, children: " s save & close \u00B7 Esc cancel" })] }));
201
194
  }
195
+ // ── Trends Screen ──
196
+ function sparkFull(values, width) {
197
+ if (values.length < 2)
198
+ return "";
199
+ const bars = " ▁▂▃▄▅▆▇█";
200
+ const min = Math.min(...values);
201
+ const max = Math.max(...values);
202
+ const range = max - min || 1;
203
+ // Resample to fit width
204
+ const sampled = [];
205
+ for (let i = 0; i < width; i++) {
206
+ const idx = Math.round((i / (width - 1)) * (values.length - 1));
207
+ sampled.push(values[idx]);
208
+ }
209
+ return sampled.map((v) => bars[Math.round(((v - min) / range) * 8)]).join("");
210
+ }
211
+ function TrendsScreen({ cwd, height, onClose }) {
212
+ const historyDir = join(cwd, ".vibe-check", "history");
213
+ const history = loadHistory(historyDir);
214
+ useInput((input, key) => {
215
+ if (key.escape || input === "t")
216
+ onClose();
217
+ });
218
+ if (history.length < 2) {
219
+ return (_jsxs(Box, { flexDirection: "column", height: height, paddingX: 1, children: [_jsx(Text, { bold: true, color: "magenta", children: " \u25C8 Trends" }), _jsx(Text, { dimColor: true, children: " Need at least 2 scans. Run the scanner a few more times." }), _jsx(Text, { dimColor: true, children: " Esc to go back" })] }));
220
+ }
221
+ const latest = history[history.length - 1];
222
+ const first = history[0];
223
+ const overallDelta = latest.score - first.score;
224
+ const scores = history.map((h) => h.score);
225
+ const chartWidth = 40;
226
+ // Get all check names from latest
227
+ const checkNames = [...latest.checkScores.keys()];
228
+ // Build per-check trends
229
+ const checkTrends = checkNames
230
+ .map((name) => {
231
+ const values = history.map((h) => h.checkScores.get(name) ?? 0).filter((v) => v > 0);
232
+ if (values.length < 2)
233
+ return null;
234
+ const current = values[values.length - 1];
235
+ const prev = values[0];
236
+ const delta = current - prev;
237
+ return { name, current, delta, spark: sparkFull(values, 20) };
238
+ })
239
+ .filter(Boolean)
240
+ .sort((a, b) => a.delta - b.delta);
241
+ const visibleChecks = Math.max(1, height - 10);
242
+ return (_jsxs(Box, { flexDirection: "column", height: height, paddingX: 1, children: [_jsxs(Text, { bold: true, color: "magenta", children: [" \u25C8 Trends \u2014 ", history.length, " scans"] }), _jsxs(Text, { dimColor: true, children: [" ", first.timestamp.split("T")[0], " \u2192 ", latest.timestamp.split("T")[0]] }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: " Overall Score" }), _jsxs(Text, { children: [_jsxs(Text, { color: "cyan", children: [" ", sparkFull(scores, chartWidth), " "] }), _jsxs(Text, { color: gc(latest.score >= 90 ? "A" : latest.score >= 75 ? "B" : "C"), bold: true, children: [" ", latest.score] }), _jsxs(Text, { color: overallDelta >= 0 ? "green" : "red", children: [" ", overallDelta >= 0 ? "+" : "", overallDelta] })] }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: " Per-Check (first \u2192 latest)" }), checkTrends.slice(0, visibleChecks).map((t) => (_jsxs(Text, { children: [_jsxs(Text, { children: [" ", t.name.slice(0, 14).padEnd(14), " "] }), _jsxs(Text, { color: "cyan", children: [t.spark, " "] }), _jsxs(Text, { color: gc(t.current >= 90 ? "A" : t.current >= 75 ? "B" : "C"), children: [String(t.current).padStart(3), " "] }), _jsxs(Text, { color: t.delta >= 0 ? "green" : "red", children: [t.delta >= 0 ? "+" : "", t.delta] })] }, t.name))), checkTrends.length > visibleChecks && _jsxs(Text, { dimColor: true, children: [" +", checkTrends.length - visibleChecks, " more"] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: " Esc back to dashboard" }) })] }));
243
+ }
202
244
  // ── Main App ──
203
245
  /** Load last scan from .vibe-check/report.json for instant display on startup. */
204
246
  function loadCachedScan(cwd) {
@@ -224,13 +266,27 @@ function loadCachedScan(cwd) {
224
266
  return null;
225
267
  }
226
268
  }
269
+ // ── Check Detail View ──
270
+ function CheckDetail({ check, height, cursor, copied }) {
271
+ const visibleIssues = Math.max(1, height - 6);
272
+ // Scroll window: keep cursor visible
273
+ const scrollStart = Math.max(0, Math.min(cursor - Math.floor(visibleIssues / 2), check.issues.length - visibleIssues));
274
+ const visibleSlice = check.issues.slice(scrollStart, scrollStart + visibleIssues);
275
+ return (_jsxs(Box, { flexDirection: "column", height: height, paddingX: 1, children: [_jsxs(Text, { bold: true, color: "magenta", children: [" \u25C8 ", check.name] }), _jsxs(Text, { children: [_jsxs(Text, { color: gc(check.grade), bold: true, children: [" ", check.grade, " ", check.score, "/100"] }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", check.issues.length, " issues \u00B7 ", check.duration, "ms"] }), copied && _jsx(Text, { color: "green", bold: true, children: " \u2713 Copied!" })] }), _jsx(Text, { children: " " }), check.issues.length === 0 ? (_jsx(Text, { color: "green", children: " No issues found." })) : (_jsxs(_Fragment, { children: [visibleSlice.map((iss, i) => {
276
+ const idx = scrollStart + i;
277
+ const sel = idx === cursor;
278
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: sel ? "white" : "gray", children: sel ? "▸" : " " }), _jsxs(Text, { color: sc(iss.severity), bold: true, children: [iss.severity[0].toUpperCase(), " "] }), iss.file && _jsxs(Text, { color: "cyan", children: [String(iss.file).slice(0, 28).padEnd(28), " "] }), iss.line && _jsx(Text, { dimColor: true, children: String(iss.line).padEnd(5) }), _jsx(Text, { children: iss.message.slice(0, 55) }), iss.rule && _jsxs(Text, { dimColor: true, children: [" (", iss.rule, ")"] })] }, idx));
279
+ }), check.issues.length > scrollStart + visibleIssues && _jsxs(Text, { dimColor: true, children: [" +", check.issues.length - scrollStart - visibleIssues, " more"] })] }))] }));
280
+ }
227
281
  function MonitorApp({ cwd }) {
228
282
  const { exit } = useApp();
229
283
  const { stdout } = useStdout();
230
284
  const rows = stdout?.rows ?? 30;
231
285
  const cached = loadCachedScan(cwd);
232
286
  const [monCfg, setMonCfg] = useState(() => loadMonitorConfig(cwd));
233
- const [mode, setMode] = useState("monitor");
287
+ const [mode, setMode] = useState({ view: "dashboard" });
288
+ const [panel, setPanel] = useState("checks");
289
+ const [cursor, setCursor] = useState(0);
234
290
  const [state, setState] = useState(cached ?? {
235
291
  checks: [], score: 0, grade: "?", duration: 0,
236
292
  totalIssues: 0, scanning: true, scanCount: 0, scores: [],
@@ -238,6 +294,7 @@ function MonitorApp({ cwd }) {
238
294
  const [log, setLog] = useState([
239
295
  { time: ts(), text: cached ? `Loaded cached scan: ${cached.grade} ${cached.score}/100` : `Monitoring ${basename(cwd)}...`, type: cached ? "scan" : "info" },
240
296
  ]);
297
+ const [copied, setCopied] = useState(false);
241
298
  const scanningRef = useRef(false);
242
299
  const prevScoreRef = useRef(cached ? cached.score : null);
243
300
  const addLog = useCallback((text, type = "info") => {
@@ -245,6 +302,15 @@ function MonitorApp({ cwd }) {
245
302
  }, []);
246
303
  const workspace = detectWorkspace(cwd);
247
304
  const stack = detectStack(cwd, workspace);
305
+ // Derived lists for navigation
306
+ const activeChecks = state.checks.filter((c) => !c.details.skipped && !c.details.comingSoon);
307
+ const allIssues = state.checks
308
+ .flatMap((c) => c.issues.map((i) => ({ check: c.name, ...i })))
309
+ .sort((a, b) => {
310
+ const o = { error: 0, warning: 1, info: 2 };
311
+ return (o[a.severity] ?? 2) - (o[b.severity] ?? 2);
312
+ });
313
+ const currentList = panel === "checks" ? activeChecks : allIssues;
248
314
  const doScan = useCallback(async () => {
249
315
  if (scanningRef.current)
250
316
  return;
@@ -282,7 +348,6 @@ function MonitorApp({ cwd }) {
282
348
  prevScoreRef.current = result.score;
283
349
  scanningRef.current = false;
284
350
  }, [cwd, monCfg, addLog]);
285
- // Initial scan
286
351
  useEffect(() => { doScan(); }, [doScan]);
287
352
  // File watcher
288
353
  useEffect(() => {
@@ -305,33 +370,104 @@ function MonitorApp({ cwd }) {
305
370
  return () => { for (const w of watchers)
306
371
  w.close(); };
307
372
  }, [cwd, workspace, doScan, addLog, monCfg.debounceMs]);
308
- // Keyboard — all input handled, nothing echoed
373
+ // ── Keyboard — unified navigation ──
309
374
  useInput((input, key) => {
375
+ // Quit from anywhere
376
+ if (input === "q" || (key.ctrl && input === "c")) {
377
+ exit();
378
+ return;
379
+ }
380
+ // Esc: drill up or quit
310
381
  if (key.escape) {
311
- if (mode === "config") {
312
- setMode("monitor");
382
+ if (mode.view !== "dashboard") {
383
+ setMode({ view: "dashboard" });
384
+ setCursor(0);
313
385
  return;
314
386
  }
315
387
  exit();
316
388
  return;
317
389
  }
318
- if (input === "q" || (key.ctrl && input === "c")) {
319
- exit();
390
+ // Global shortcuts
391
+ if (input === "r" && mode.view === "dashboard") {
392
+ doScan();
320
393
  return;
321
394
  }
322
- if (mode !== "monitor")
395
+ // View switching
396
+ if (input === "t") {
397
+ setMode({ view: "trends" });
398
+ setCursor(0);
323
399
  return;
324
- if (input === "r")
325
- doScan();
326
- if (input === "c")
327
- setMode("config");
400
+ }
401
+ if (input === "c" && mode.view !== "config") {
402
+ setMode({ view: "config" });
403
+ return;
404
+ }
405
+ // Dashboard navigation
406
+ if (mode.view === "dashboard") {
407
+ // Tab switches focused panel
408
+ if (key.tab) {
409
+ setPanel((p) => p === "checks" ? "issues" : "checks");
410
+ setCursor(0);
411
+ return;
412
+ }
413
+ // ↑↓ navigate within panel
414
+ if (key.upArrow)
415
+ setCursor((c) => Math.max(0, c - 1));
416
+ if (key.downArrow)
417
+ setCursor((c) => Math.min(currentList.length - 1, c + 1));
418
+ // Enter: drill into selected check
419
+ if (key.return && panel === "checks" && activeChecks[cursor]) {
420
+ setMode({ view: "check-detail", checkName: activeChecks[cursor].name });
421
+ setCursor(0);
422
+ return;
423
+ }
424
+ // Enter on issue: drill into that check
425
+ if (key.return && panel === "issues" && allIssues[cursor]) {
426
+ setMode({ view: "check-detail", checkName: allIssues[cursor].check });
427
+ setCursor(0);
428
+ return;
429
+ }
430
+ }
431
+ // Check detail: ↑↓ scroll, Enter copies fix prompt
432
+ if (mode.view === "check-detail") {
433
+ const check = state.checks.find((c) => c.name === mode.checkName);
434
+ if (check) {
435
+ if (key.upArrow)
436
+ setCursor((c) => Math.max(0, c - 1));
437
+ if (key.downArrow)
438
+ setCursor((c) => Math.min(check.issues.length - 1, c + 1));
439
+ if (key.return && check.issues[cursor]) {
440
+ const prompt = buildFixPrompt(check.name, check.issues[cursor]);
441
+ if (copyToClipboard(prompt)) {
442
+ setCopied(true);
443
+ addLog(`Copied fix prompt for ${check.name}:${check.issues[cursor].file || ""}`, "info");
444
+ setTimeout(() => setCopied(false), 2000);
445
+ }
446
+ }
447
+ }
448
+ }
449
+ // Trends: ↑↓ scroll
450
+ if (mode.view === "trends") {
451
+ if (key.upArrow)
452
+ setCursor((c) => Math.max(0, c - 1));
453
+ if (key.downArrow)
454
+ setCursor((c) => c + 1);
455
+ }
328
456
  });
329
457
  const proj = basename(cwd);
330
458
  const p = monCfg.panels;
331
- if (mode === "config") {
332
- return (_jsx(Box, { flexDirection: "column", height: rows, justifyContent: "center", alignItems: "center", children: _jsx(ConfigScreen, { monCfg: monCfg, onSave: (cfg) => { setMonCfg(cfg); saveMonitorConfig(cwd, cfg); addLog("Settings saved", "info"); }, onClose: () => setMode("monitor") }) }));
459
+ // ── Render views ──
460
+ if (mode.view === "check-detail") {
461
+ const check = state.checks.find((c) => c.name === mode.checkName);
462
+ return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Header, { proj: proj, stack: stack, workspace: workspace, state: state }), check ? _jsx(CheckDetail, { check: check, height: rows - 3, cursor: cursor, copied: copied }) : _jsx(Text, { dimColor: true, children: " Check not found" }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "Esc back \u00B7 \u2191\u2193 select \u00B7 Enter copy fix prompt \u00B7 q quit" }) })] }));
333
463
  }
334
- // Fixed panel heights — no reflow on content changes
464
+ if (mode.view === "trends") {
465
+ return (_jsx(Box, { flexDirection: "column", height: rows, children: _jsx(TrendsScreen, { cwd: cwd, height: rows, onClose: () => setMode({ view: "dashboard" }) }) }));
466
+ }
467
+ if (mode.view === "config") {
468
+ return (_jsx(Box, { flexDirection: "column", height: rows, justifyContent: "center", alignItems: "center", children: _jsx(ConfigScreen, { monCfg: monCfg, onSave: (cfg) => { setMonCfg(cfg); saveMonitorConfig(cwd, cfg); addLog("Settings saved", "info"); }, onClose: () => setMode({ view: "dashboard" }) }) }));
469
+ }
470
+ // ── Dashboard ──
335
471
  const sidebarVisible = p.score || p.checks;
336
472
  const mainVisible = p.activity || p.issues;
337
473
  const bodyRows = rows - 4;
@@ -341,8 +477,16 @@ function MonitorApp({ cwd }) {
341
477
  const issuesH = p.issues ? bodyRows - activityH : 0;
342
478
  const errorCount = state.checks.reduce((s, c) => s + c.issues.filter((i) => i.severity === "error").length, 0);
343
479
  const warnCount = state.checks.reduce((s, c) => s + c.issues.filter((i) => i.severity === "warning").length, 0);
344
- const activeCount = state.checks.filter((c) => !c.details.skipped && !c.details.comingSoon).length;
345
- return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Box, { paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(Text, { color: "magenta", bold: true, children: "vcqa monitor" }), _jsxs(Text, { dimColor: true, children: [" v", VERSION] })] }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: proj }), _jsxs(Text, { dimColor: true, children: [" ", stack.language, "/", stack.framework, workspace.isMonorepo ? ` · ${workspace.tool}` : ""] })] }), _jsxs(Text, { children: [state.score > 0 && _jsxs(Text, { color: gc(state.grade), bold: true, children: [state.grade, " ", state.score, " "] }), _jsx(Text, { dimColor: true, children: state.scanning ? "⟳ scanning" : "● watching" })] })] }), _jsx(Box, { paddingX: 1, gap: 2, height: 1, children: state.scanCount > 0 && (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "E " }), _jsx(Text, { color: "red", bold: true, children: errorCount })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "W " }), _jsx(Text, { color: "yellow", bold: true, children: warnCount })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "checks " }), _jsx(Text, { bold: true, children: activeCount })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "scan " }), _jsxs(Text, { children: [state.duration, "ms"] })] }), state.scores.length >= 2 && _jsx(Text, { color: "cyan", children: spark(state.scores) })] })) }), _jsxs(Box, { height: bodyRows, children: [sidebarVisible && (_jsxs(Box, { flexDirection: "column", width: 26, children: [p.score && _jsx(ScorePanel, { state: state, height: scoreH }), p.checks && _jsx(ChecksPanel, { checks: state.checks, height: checksH })] })), mainVisible && (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [p.activity && _jsx(ActivityPanel, { log: log, height: activityH }), p.issues && _jsx(IssuesPanel, { checks: state.checks, height: issuesH })] })), !sidebarVisible && !mainVisible && (_jsx(Box, { height: bodyRows, justifyContent: "center", alignItems: "center", children: _jsx(Text, { dimColor: true, children: "All panels hidden. Press c to configure." }) }))] }), _jsxs(Box, { paddingX: 1, justifyContent: "space-between", children: [_jsx(Text, { dimColor: true, children: "q/Esc quit \u00B7 r rescan \u00B7 c settings" }), monCfg.alertBelow > 0 && (_jsxs(Text, { dimColor: true, children: ["alert <", monCfg.alertBelow, " \u00B7 drop \u2265", monCfg.alertDrop] }))] })] }));
480
+ return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Header, { proj: proj, stack: stack, workspace: workspace, state: state }), _jsx(Box, { paddingX: 1, gap: 2, height: 1, children: state.scanCount > 0 && (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "E " }), _jsx(Text, { color: "red", bold: true, children: errorCount })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "W " }), _jsx(Text, { color: "yellow", bold: true, children: warnCount })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "checks " }), _jsx(Text, { bold: true, children: activeChecks.length })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "scan " }), _jsxs(Text, { children: [state.duration, "ms"] })] }), state.scores.length >= 2 && _jsx(Text, { color: "cyan", children: spark(state.scores) })] })) }), _jsxs(Box, { height: bodyRows, children: [sidebarVisible && (_jsxs(Box, { flexDirection: "column", width: 26, children: [p.score && _jsx(ScorePanel, { state: state, height: scoreH }), p.checks && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: panel === "checks" ? "magenta" : "gray", paddingX: 1, width: 24, height: checksH, overflowY: "hidden", children: [_jsxs(Text, { bold: true, color: "magenta", children: [" \u25C8 Checks ", panel === "checks" && _jsx(Text, { dimColor: true, children: "\u25C4" })] }), activeChecks.slice(0, checksH - 3).map((c, i) => {
481
+ const sel = panel === "checks" && i === cursor;
482
+ return (_jsxs(Text, { children: [_jsxs(Text, { color: sel ? "white" : gc(c.grade), children: [sel ? "▸" : " ", c.grade === "A" ? "●" : c.grade === "B" ? "◐" : "○", " "] }), _jsx(Text, { bold: sel, children: c.name.slice(0, 13).padEnd(13) }), _jsx(Text, { color: gc(c.grade), children: String(c.score).padStart(3) })] }, c.name));
483
+ })] }))] })), mainVisible && (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [p.activity && _jsx(ActivityPanel, { log: log, height: activityH }), p.issues && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: panel === "issues" ? "magenta" : "gray", paddingX: 1, height: issuesH, overflowY: "hidden", children: [_jsxs(Text, { bold: true, color: "magenta", children: [" ", "\u25C8 Issues (", allIssues.length, ") ", panel === "issues" && _jsx(Text, { dimColor: true, children: "\u25C4" })] }), allIssues.slice(0, issuesH - 3).map((iss, i) => {
484
+ const sel = panel === "issues" && i === cursor;
485
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: sel ? "white" : "gray", children: sel ? "▸" : " " }), _jsxs(Text, { color: sc(iss.severity), bold: true, children: [iss.severity[0].toUpperCase(), " "] }), _jsxs(Text, { dimColor: true, children: [(iss.check || "").slice(0, 11).padEnd(11), " "] }), iss.file && _jsxs(Text, { color: "cyan", children: [basename(String(iss.file)).slice(0, 18).padEnd(18), " "] }), _jsx(Text, { children: iss.message.slice(0, 40) })] }, i));
486
+ }), allIssues.length > issuesH - 3 && _jsxs(Text, { dimColor: true, children: [" +", allIssues.length - (issuesH - 3), " more"] })] }))] })), !sidebarVisible && !mainVisible && (_jsx(Box, { height: bodyRows, justifyContent: "center", alignItems: "center", children: _jsx(Text, { dimColor: true, children: "All panels hidden. Press c to configure." }) }))] }), _jsx(Box, { paddingX: 1, justifyContent: "space-between", children: _jsx(Text, { dimColor: true, children: "Tab panel \u00B7 \u2191\u2193 select \u00B7 Enter drill \u00B7 Esc back \u00B7 r scan \u00B7 t trends \u00B7 c config \u00B7 q quit" }) })] }));
487
+ }
488
+ function Header({ proj, stack, workspace, state }) {
489
+ return (_jsxs(Box, { paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(Text, { color: "magenta", bold: true, children: "vcqa monitor" }), _jsxs(Text, { dimColor: true, children: [" v", VERSION] })] }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: proj }), _jsxs(Text, { dimColor: true, children: [" ", stack.language, "/", stack.framework, workspace.isMonorepo ? ` · ${workspace.tool}` : ""] })] }), _jsxs(Text, { children: [state.score > 0 && _jsxs(Text, { color: gc(state.grade), bold: true, children: [state.grade, " ", state.score, " "] }), _jsx(Text, { dimColor: true, children: state.scanning ? "⟳ scanning" : "● watching" })] })] }));
346
490
  }
347
491
  // ── Entry ──
348
492
  export async function startMonitor(cwd) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodeqa/cli",
3
- "version": "0.35.5",
3
+ "version": "0.35.7",
4
4
  "description": "Code health scanner for the AI coding era. 25 checks, zero config, full report.",
5
5
  "type": "module",
6
6
  "bin": {