@vibecodeqa/cli 0.35.6 → 0.35.8

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 +162 -43
  2. package/package.json +1 -1
package/dist/monitor.js CHANGED
@@ -10,7 +10,7 @@ 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
16
  import { loadHistory } from "./history.js";
@@ -68,9 +68,19 @@ function spark(values) {
68
68
  function ts() {
69
69
  return new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
70
70
  }
71
- function bar(score, width) {
72
- const filled = Math.round((score / 100) * width);
73
- 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.`;
74
84
  }
75
85
  // ── Scan via child process — UI never freezes ──
76
86
  function runScanProcess(cwd, skipTests) {
@@ -111,12 +121,6 @@ function ScorePanel({ state, height }) {
111
121
  const active = state.checks.filter((c) => !c.details.skipped && !c.details.comingSoon);
112
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] })] }));
113
123
  }
114
- function ChecksPanel({ checks, height }) {
115
- const active = checks.filter((c) => !c.details.skipped && !c.details.comingSoon);
116
- const pro = checks.filter((c) => c.details.comingSoon);
117
- const visibleLines = Math.max(1, height - 3);
118
- 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"] })] }));
119
- }
120
124
  function ActivityPanel({ log, height }) {
121
125
  const colors = {
122
126
  info: "gray", scan: "cyan", change: "yellow",
@@ -125,18 +129,6 @@ function ActivityPanel({ log, height }) {
125
129
  const visibleLines = Math.max(1, height - 3);
126
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)))] }));
127
131
  }
128
- function IssuesPanel({ checks, height }) {
129
- const issues = checks
130
- .flatMap((c) => c.issues.map((i) => ({ check: c.name, ...i })))
131
- .sort((a, b) => {
132
- const o = { error: 0, warning: 1, info: 2 };
133
- return (o[a.severity] ?? 2) - (o[b.severity] ?? 2);
134
- });
135
- const errors = issues.filter((i) => i.severity === "error").length;
136
- const warnings = issues.filter((i) => i.severity === "warning").length;
137
- const visibleLines = Math.max(1, height - 3);
138
- 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"] })] }));
139
- }
140
132
  function ConfigScreen({ monCfg, onSave, onClose, }) {
141
133
  const [cursor, setCursor] = useState(0);
142
134
  const [cfg, setCfg] = useState(JSON.parse(JSON.stringify(monCfg)));
@@ -274,13 +266,57 @@ function loadCachedScan(cwd) {
274
266
  return null;
275
267
  }
276
268
  }
269
+ // ── Check Detail View ──
270
+ function CheckDetail({ check, height, cursor, copied }) {
271
+ const bodyHeight = height - 5; // header + score + blank + footer margin
272
+ // Each issue takes 2-3 lines: header line + message (wraps if long)
273
+ // Estimate lines per issue for scroll calculation
274
+ const issueHeights = check.issues.map((iss) => {
275
+ const msgLen = iss.message.length;
276
+ return msgLen > 80 ? 3 : 2; // 2 lines base, 3 if message wraps
277
+ });
278
+ // Find scroll window that keeps cursor visible
279
+ let scrollStart = 0;
280
+ let linesUsed = 0;
281
+ // First, find how many items fit
282
+ const fits = [];
283
+ for (let i = 0; i < check.issues.length; i++) {
284
+ if (linesUsed + issueHeights[i] > bodyHeight)
285
+ break;
286
+ fits.push(i);
287
+ linesUsed += issueHeights[i];
288
+ }
289
+ const maxVisible = fits.length || 1;
290
+ // Adjust scroll so cursor is visible
291
+ if (cursor >= scrollStart + maxVisible)
292
+ scrollStart = cursor - maxVisible + 1;
293
+ if (cursor < scrollStart)
294
+ scrollStart = cursor;
295
+ scrollStart = Math.max(0, Math.min(scrollStart, check.issues.length - maxVisible));
296
+ // Collect visible items within height budget
297
+ const visible = [];
298
+ let usedLines = 0;
299
+ for (let i = scrollStart; i < check.issues.length; i++) {
300
+ if (usedLines + issueHeights[i] > bodyHeight)
301
+ break;
302
+ visible.push({ issue: check.issues[i], idx: i });
303
+ usedLines += issueHeights[i];
304
+ }
305
+ const remaining = check.issues.length - (scrollStart + visible.length);
306
+ return (_jsxs(Box, { flexDirection: "column", height: height, paddingX: 1, overflowY: "hidden", 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: [visible.map(({ issue: iss, idx }) => {
307
+ const sel = idx === cursor;
308
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [_jsxs(Text, { 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), iss.line ? `:${iss.line}` : "", " "] }), iss.rule && _jsxs(Text, { dimColor: true, children: ["(", iss.rule, ")"] })] }), _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: sel ? "white" : "gray", children: " " }), _jsx(Text, { color: sel ? "white" : undefined, children: iss.message })] })] }, idx));
309
+ }), remaining > 0 && _jsxs(Text, { dimColor: true, children: [" +", remaining, " more (\u2193 to scroll)"] })] }))] }));
310
+ }
277
311
  function MonitorApp({ cwd }) {
278
312
  const { exit } = useApp();
279
313
  const { stdout } = useStdout();
280
314
  const rows = stdout?.rows ?? 30;
281
315
  const cached = loadCachedScan(cwd);
282
316
  const [monCfg, setMonCfg] = useState(() => loadMonitorConfig(cwd));
283
- const [mode, setMode] = useState("monitor");
317
+ const [mode, setMode] = useState({ view: "dashboard" });
318
+ const [panel, setPanel] = useState("checks");
319
+ const [cursor, setCursor] = useState(0);
284
320
  const [state, setState] = useState(cached ?? {
285
321
  checks: [], score: 0, grade: "?", duration: 0,
286
322
  totalIssues: 0, scanning: true, scanCount: 0, scores: [],
@@ -288,6 +324,7 @@ function MonitorApp({ cwd }) {
288
324
  const [log, setLog] = useState([
289
325
  { time: ts(), text: cached ? `Loaded cached scan: ${cached.grade} ${cached.score}/100` : `Monitoring ${basename(cwd)}...`, type: cached ? "scan" : "info" },
290
326
  ]);
327
+ const [copied, setCopied] = useState(false);
291
328
  const scanningRef = useRef(false);
292
329
  const prevScoreRef = useRef(cached ? cached.score : null);
293
330
  const addLog = useCallback((text, type = "info") => {
@@ -295,6 +332,15 @@ function MonitorApp({ cwd }) {
295
332
  }, []);
296
333
  const workspace = detectWorkspace(cwd);
297
334
  const stack = detectStack(cwd, workspace);
335
+ // Derived lists for navigation
336
+ const activeChecks = state.checks.filter((c) => !c.details.skipped && !c.details.comingSoon);
337
+ const allIssues = state.checks
338
+ .flatMap((c) => c.issues.map((i) => ({ check: c.name, ...i })))
339
+ .sort((a, b) => {
340
+ const o = { error: 0, warning: 1, info: 2 };
341
+ return (o[a.severity] ?? 2) - (o[b.severity] ?? 2);
342
+ });
343
+ const currentList = panel === "checks" ? activeChecks : allIssues;
298
344
  const doScan = useCallback(async () => {
299
345
  if (scanningRef.current)
300
346
  return;
@@ -332,7 +378,6 @@ function MonitorApp({ cwd }) {
332
378
  prevScoreRef.current = result.score;
333
379
  scanningRef.current = false;
334
380
  }, [cwd, monCfg, addLog]);
335
- // Initial scan
336
381
  useEffect(() => { doScan(); }, [doScan]);
337
382
  // File watcher
338
383
  useEffect(() => {
@@ -355,38 +400,104 @@ function MonitorApp({ cwd }) {
355
400
  return () => { for (const w of watchers)
356
401
  w.close(); };
357
402
  }, [cwd, workspace, doScan, addLog, monCfg.debounceMs]);
358
- // Keyboard — all input handled, nothing echoed
403
+ // ── Keyboard — unified navigation ──
359
404
  useInput((input, key) => {
405
+ // Quit from anywhere
406
+ if (input === "q" || (key.ctrl && input === "c")) {
407
+ exit();
408
+ return;
409
+ }
410
+ // Esc: drill up or quit
360
411
  if (key.escape) {
361
- if (mode !== "monitor") {
362
- setMode("monitor");
412
+ if (mode.view !== "dashboard") {
413
+ setMode({ view: "dashboard" });
414
+ setCursor(0);
363
415
  return;
364
416
  }
365
417
  exit();
366
418
  return;
367
419
  }
368
- if (input === "q" || (key.ctrl && input === "c")) {
369
- exit();
420
+ // Global shortcuts
421
+ if (input === "r" && mode.view === "dashboard") {
422
+ doScan();
370
423
  return;
371
424
  }
372
- if (mode !== "monitor")
425
+ // View switching
426
+ if (input === "t") {
427
+ setMode({ view: "trends" });
428
+ setCursor(0);
373
429
  return;
374
- if (input === "r")
375
- doScan();
376
- if (input === "c")
377
- setMode("config");
378
- if (input === "t")
379
- setMode("trends");
430
+ }
431
+ if (input === "c" && mode.view !== "config") {
432
+ setMode({ view: "config" });
433
+ return;
434
+ }
435
+ // Dashboard navigation
436
+ if (mode.view === "dashboard") {
437
+ // Tab switches focused panel
438
+ if (key.tab) {
439
+ setPanel((p) => p === "checks" ? "issues" : "checks");
440
+ setCursor(0);
441
+ return;
442
+ }
443
+ // ↑↓ navigate within panel
444
+ if (key.upArrow)
445
+ setCursor((c) => Math.max(0, c - 1));
446
+ if (key.downArrow)
447
+ setCursor((c) => Math.min(currentList.length - 1, c + 1));
448
+ // Enter: drill into selected check
449
+ if (key.return && panel === "checks" && activeChecks[cursor]) {
450
+ setMode({ view: "check-detail", checkName: activeChecks[cursor].name });
451
+ setCursor(0);
452
+ return;
453
+ }
454
+ // Enter on issue: drill into that check
455
+ if (key.return && panel === "issues" && allIssues[cursor]) {
456
+ setMode({ view: "check-detail", checkName: allIssues[cursor].check });
457
+ setCursor(0);
458
+ return;
459
+ }
460
+ }
461
+ // Check detail: ↑↓ scroll, Enter copies fix prompt
462
+ if (mode.view === "check-detail") {
463
+ const check = state.checks.find((c) => c.name === mode.checkName);
464
+ if (check) {
465
+ if (key.upArrow)
466
+ setCursor((c) => Math.max(0, c - 1));
467
+ if (key.downArrow)
468
+ setCursor((c) => Math.min(check.issues.length - 1, c + 1));
469
+ if (key.return && check.issues[cursor]) {
470
+ const prompt = buildFixPrompt(check.name, check.issues[cursor]);
471
+ if (copyToClipboard(prompt)) {
472
+ setCopied(true);
473
+ addLog(`Copied fix prompt for ${check.name}:${check.issues[cursor].file || ""}`, "info");
474
+ setTimeout(() => setCopied(false), 2000);
475
+ }
476
+ }
477
+ }
478
+ }
479
+ // Trends: ↑↓ scroll
480
+ if (mode.view === "trends") {
481
+ if (key.upArrow)
482
+ setCursor((c) => Math.max(0, c - 1));
483
+ if (key.downArrow)
484
+ setCursor((c) => c + 1);
485
+ }
380
486
  });
381
487
  const proj = basename(cwd);
382
488
  const p = monCfg.panels;
383
- if (mode === "trends") {
384
- return (_jsx(Box, { flexDirection: "column", height: rows, children: _jsx(TrendsScreen, { cwd: cwd, height: rows, onClose: () => setMode("monitor") }) }));
489
+ // ── Render views ──
490
+ if (mode.view === "check-detail") {
491
+ const check = state.checks.find((c) => c.name === mode.checkName);
492
+ 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" }) })] }));
493
+ }
494
+ if (mode.view === "trends") {
495
+ return (_jsx(Box, { flexDirection: "column", height: rows, children: _jsx(TrendsScreen, { cwd: cwd, height: rows, onClose: () => setMode({ view: "dashboard" }) }) }));
385
496
  }
386
- if (mode === "config") {
387
- 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") }) }));
497
+ if (mode.view === "config") {
498
+ 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" }) }) }));
388
499
  }
389
- // Fixed panel heights — no reflow on content changes
500
+ // ── Dashboard ──
390
501
  const sidebarVisible = p.score || p.checks;
391
502
  const mainVisible = p.activity || p.issues;
392
503
  const bodyRows = rows - 4;
@@ -396,8 +507,16 @@ function MonitorApp({ cwd }) {
396
507
  const issuesH = p.issues ? bodyRows - activityH : 0;
397
508
  const errorCount = state.checks.reduce((s, c) => s + c.issues.filter((i) => i.severity === "error").length, 0);
398
509
  const warnCount = state.checks.reduce((s, c) => s + c.issues.filter((i) => i.severity === "warning").length, 0);
399
- const activeCount = state.checks.filter((c) => !c.details.skipped && !c.details.comingSoon).length;
400
- 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 t trends \u00B7 c settings" }), monCfg.alertBelow > 0 && (_jsxs(Text, { dimColor: true, children: ["alert <", monCfg.alertBelow, " \u00B7 drop \u2265", monCfg.alertDrop] }))] })] }));
510
+ 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) => {
511
+ const sel = panel === "checks" && i === cursor;
512
+ 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));
513
+ })] }))] })), 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) => {
514
+ const sel = panel === "issues" && i === cursor;
515
+ 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));
516
+ }), 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" }) })] }));
517
+ }
518
+ function Header({ proj, stack, workspace, state }) {
519
+ 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" })] })] }));
401
520
  }
402
521
  // ── Entry ──
403
522
  export async function startMonitor(cwd) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodeqa/cli",
3
- "version": "0.35.6",
3
+ "version": "0.35.8",
4
4
  "description": "Code health scanner for the AI coding era. 25 checks, zero config, full report.",
5
5
  "type": "module",
6
6
  "bin": {