@vibecodeqa/cli 0.35.6 → 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 +132 -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,27 @@ function loadCachedScan(cwd) {
274
266
  return null;
275
267
  }
276
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
+ }
277
281
  function MonitorApp({ cwd }) {
278
282
  const { exit } = useApp();
279
283
  const { stdout } = useStdout();
280
284
  const rows = stdout?.rows ?? 30;
281
285
  const cached = loadCachedScan(cwd);
282
286
  const [monCfg, setMonCfg] = useState(() => loadMonitorConfig(cwd));
283
- const [mode, setMode] = useState("monitor");
287
+ const [mode, setMode] = useState({ view: "dashboard" });
288
+ const [panel, setPanel] = useState("checks");
289
+ const [cursor, setCursor] = useState(0);
284
290
  const [state, setState] = useState(cached ?? {
285
291
  checks: [], score: 0, grade: "?", duration: 0,
286
292
  totalIssues: 0, scanning: true, scanCount: 0, scores: [],
@@ -288,6 +294,7 @@ function MonitorApp({ cwd }) {
288
294
  const [log, setLog] = useState([
289
295
  { time: ts(), text: cached ? `Loaded cached scan: ${cached.grade} ${cached.score}/100` : `Monitoring ${basename(cwd)}...`, type: cached ? "scan" : "info" },
290
296
  ]);
297
+ const [copied, setCopied] = useState(false);
291
298
  const scanningRef = useRef(false);
292
299
  const prevScoreRef = useRef(cached ? cached.score : null);
293
300
  const addLog = useCallback((text, type = "info") => {
@@ -295,6 +302,15 @@ function MonitorApp({ cwd }) {
295
302
  }, []);
296
303
  const workspace = detectWorkspace(cwd);
297
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;
298
314
  const doScan = useCallback(async () => {
299
315
  if (scanningRef.current)
300
316
  return;
@@ -332,7 +348,6 @@ function MonitorApp({ cwd }) {
332
348
  prevScoreRef.current = result.score;
333
349
  scanningRef.current = false;
334
350
  }, [cwd, monCfg, addLog]);
335
- // Initial scan
336
351
  useEffect(() => { doScan(); }, [doScan]);
337
352
  // File watcher
338
353
  useEffect(() => {
@@ -355,38 +370,104 @@ function MonitorApp({ cwd }) {
355
370
  return () => { for (const w of watchers)
356
371
  w.close(); };
357
372
  }, [cwd, workspace, doScan, addLog, monCfg.debounceMs]);
358
- // Keyboard — all input handled, nothing echoed
373
+ // ── Keyboard — unified navigation ──
359
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
360
381
  if (key.escape) {
361
- if (mode !== "monitor") {
362
- setMode("monitor");
382
+ if (mode.view !== "dashboard") {
383
+ setMode({ view: "dashboard" });
384
+ setCursor(0);
363
385
  return;
364
386
  }
365
387
  exit();
366
388
  return;
367
389
  }
368
- if (input === "q" || (key.ctrl && input === "c")) {
369
- exit();
390
+ // Global shortcuts
391
+ if (input === "r" && mode.view === "dashboard") {
392
+ doScan();
370
393
  return;
371
394
  }
372
- if (mode !== "monitor")
395
+ // View switching
396
+ if (input === "t") {
397
+ setMode({ view: "trends" });
398
+ setCursor(0);
373
399
  return;
374
- if (input === "r")
375
- doScan();
376
- if (input === "c")
377
- setMode("config");
378
- if (input === "t")
379
- setMode("trends");
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
+ }
380
456
  });
381
457
  const proj = basename(cwd);
382
458
  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") }) }));
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" }) })] }));
385
463
  }
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") }) }));
464
+ if (mode.view === "trends") {
465
+ return (_jsx(Box, { flexDirection: "column", height: rows, children: _jsx(TrendsScreen, { cwd: cwd, height: rows, onClose: () => setMode({ view: "dashboard" }) }) }));
388
466
  }
389
- // Fixed panel heights — no reflow on content changes
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 ──
390
471
  const sidebarVisible = p.score || p.checks;
391
472
  const mainVisible = p.activity || p.issues;
392
473
  const bodyRows = rows - 4;
@@ -396,8 +477,16 @@ function MonitorApp({ cwd }) {
396
477
  const issuesH = p.issues ? bodyRows - activityH : 0;
397
478
  const errorCount = state.checks.reduce((s, c) => s + c.issues.filter((i) => i.severity === "error").length, 0);
398
479
  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] }))] })] }));
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" })] })] }));
401
490
  }
402
491
  // ── Entry ──
403
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.6",
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": {