reviw 0.16.1 → 0.16.3

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/README.ja.md CHANGED
@@ -286,8 +286,8 @@ npx reviw .artifacts/<feature>/REPORT.md
286
286
  Playwrightを使用したブラウザ自動化ツールキット。
287
287
 
288
288
  **機能:**
289
- - PythonとNode.js Playwrightスクリプト
290
- - サーバーライフサイクル管理(`scripts/with_server.py`)
289
+ - TypeScript Playwright Test (`@playwright/test`)
290
+ - webServerサポート付きPlaywright設定
291
291
  - スクリーンショットと動画キャプチャ
292
292
  - コンソールログとネットワークリクエスト監視
293
293
  - 高度なデバッグ用CDP統合
package/README.md CHANGED
@@ -318,8 +318,8 @@ npx reviw .artifacts/<feature>/REPORT.md
318
318
  Browser automation toolkit using Playwright.
319
319
 
320
320
  **Features:**
321
- - Python and Node.js Playwright scripts
322
- - Server lifecycle management (`scripts/with_server.py`)
321
+ - TypeScript Playwright Test (`@playwright/test`)
322
+ - Playwright configuration with webServer support
323
323
  - Screenshot and video capture
324
324
  - Console log and network request monitoring
325
325
  - CDP integration for advanced debugging
package/cli.cjs CHANGED
@@ -825,9 +825,10 @@ function serializeForScript(value) {
825
825
  .replace(/\$\{/g, "\\${");
826
826
  }
827
827
 
828
- function diffHtmlTemplate(diffData) {
828
+ function diffHtmlTemplate(diffData, history = []) {
829
829
  const { rows, projectRoot, relativePath } = diffData;
830
830
  const serialized = serializeForScript(rows);
831
+ const historyJson = serializeForScript(history);
831
832
  const fileCount = rows.filter((r) => r.type === "file").length;
832
833
 
833
834
  return `<!doctype html>
@@ -1235,6 +1236,221 @@ function diffHtmlTemplate(diffData) {
1235
1236
  }
1236
1237
  .no-diff h2 { font-size: 20px; margin: 0 0 8px; color: var(--text); }
1237
1238
  .no-diff p { font-size: 14px; margin: 0; }
1239
+
1240
+ /* History Panel - Push layout */
1241
+ body { transition: margin-right 0.25s ease; }
1242
+ body.history-open { margin-right: 320px; }
1243
+ body.history-open header { right: 320px; }
1244
+ header { transition: right 0.25s ease; right: 0; }
1245
+
1246
+ .history-toggle {
1247
+ background: var(--selected-bg);
1248
+ color: var(--text);
1249
+ border: 1px solid var(--border);
1250
+ border-radius: 6px;
1251
+ padding: 6px 8px;
1252
+ font-size: 14px;
1253
+ cursor: pointer;
1254
+ width: 34px;
1255
+ height: 34px;
1256
+ display: flex;
1257
+ align-items: center;
1258
+ justify-content: center;
1259
+ }
1260
+ .history-toggle:hover { background: var(--border); }
1261
+ .history-panel {
1262
+ position: fixed;
1263
+ top: 0;
1264
+ right: 0;
1265
+ width: 320px;
1266
+ height: 100vh;
1267
+ background: var(--panel);
1268
+ border-left: 1px solid var(--border);
1269
+ z-index: 90;
1270
+ transform: translateX(100%);
1271
+ transition: transform 0.25s ease;
1272
+ display: flex;
1273
+ flex-direction: column;
1274
+ }
1275
+ .history-panel.open { transform: translateX(0); }
1276
+ .history-panel-header {
1277
+ padding: 16px;
1278
+ border-bottom: 1px solid var(--border);
1279
+ display: flex;
1280
+ justify-content: space-between;
1281
+ align-items: center;
1282
+ }
1283
+ .history-panel-header h3 { margin: 0; font-size: 14px; font-weight: 600; }
1284
+ .history-panel-close {
1285
+ background: transparent;
1286
+ border: none;
1287
+ color: var(--muted);
1288
+ cursor: pointer;
1289
+ font-size: 18px;
1290
+ padding: 4px;
1291
+ }
1292
+ .history-panel-close:hover { color: var(--text); }
1293
+ .history-panel-body {
1294
+ flex: 1;
1295
+ overflow-y: auto;
1296
+ padding: 12px;
1297
+ }
1298
+ .history-empty {
1299
+ color: var(--muted);
1300
+ font-size: 13px;
1301
+ text-align: center;
1302
+ padding: 40px 20px;
1303
+ }
1304
+ .history-date-group {
1305
+ margin-bottom: 16px;
1306
+ }
1307
+ .history-date {
1308
+ font-size: 11px;
1309
+ font-weight: 600;
1310
+ color: var(--muted);
1311
+ margin-bottom: 8px;
1312
+ text-transform: uppercase;
1313
+ }
1314
+ .history-item {
1315
+ background: var(--bg);
1316
+ border: 1px solid var(--border);
1317
+ border-radius: 6px;
1318
+ margin-bottom: 8px;
1319
+ overflow: hidden;
1320
+ }
1321
+ .history-item-header {
1322
+ display: flex;
1323
+ justify-content: space-between;
1324
+ align-items: center;
1325
+ padding: 8px 10px;
1326
+ background: var(--selected-bg);
1327
+ cursor: pointer;
1328
+ }
1329
+ .history-item-header:hover { background: var(--border); }
1330
+ .history-item-file {
1331
+ font-size: 12px;
1332
+ font-weight: 600;
1333
+ color: var(--text);
1334
+ white-space: nowrap;
1335
+ overflow: hidden;
1336
+ text-overflow: ellipsis;
1337
+ max-width: 180px;
1338
+ }
1339
+ .history-item-time {
1340
+ font-size: 10px;
1341
+ color: var(--muted);
1342
+ }
1343
+ .history-item-body {
1344
+ display: none;
1345
+ padding: 10px;
1346
+ font-size: 12px;
1347
+ border-top: 1px solid var(--border);
1348
+ }
1349
+ .history-item.expanded .history-item-body { display: block; }
1350
+ .history-summary {
1351
+ color: var(--text);
1352
+ margin-bottom: 8px;
1353
+ padding-bottom: 8px;
1354
+ border-bottom: 1px solid var(--border);
1355
+ }
1356
+ .history-summary-label {
1357
+ font-size: 10px;
1358
+ font-weight: 600;
1359
+ color: var(--muted);
1360
+ margin-bottom: 4px;
1361
+ }
1362
+ .history-summary-text {
1363
+ white-space: pre-wrap;
1364
+ line-height: 1.4;
1365
+ }
1366
+ .history-comments-label {
1367
+ font-size: 10px;
1368
+ font-weight: 600;
1369
+ color: var(--muted);
1370
+ margin-bottom: 6px;
1371
+ }
1372
+ .history-comment {
1373
+ padding: 6px 0;
1374
+ border-bottom: 1px solid var(--border);
1375
+ }
1376
+ .history-comment:last-child { border-bottom: none; }
1377
+ .history-comment-line {
1378
+ font-size: 10px;
1379
+ color: var(--accent);
1380
+ font-weight: 600;
1381
+ margin-bottom: 2px;
1382
+ }
1383
+ .history-comment-quote {
1384
+ background: rgba(0, 0, 0, 0.3);
1385
+ border-left: 2px solid var(--accent);
1386
+ padding: 4px 8px;
1387
+ margin: 4px 0;
1388
+ font-family: 'SF Mono', Monaco, Consolas, monospace;
1389
+ font-size: 11px;
1390
+ color: var(--muted);
1391
+ white-space: pre-wrap;
1392
+ word-break: break-all;
1393
+ max-height: 80px;
1394
+ overflow-y: auto;
1395
+ }
1396
+ .history-comment-text {
1397
+ color: var(--text);
1398
+ line-height: 1.4;
1399
+ white-space: pre-wrap;
1400
+ }
1401
+ .history-badge {
1402
+ display: inline-block;
1403
+ background: var(--accent);
1404
+ color: var(--text-inverse);
1405
+ font-size: 10px;
1406
+ padding: 2px 6px;
1407
+ border-radius: 10px;
1408
+ margin-left: 6px;
1409
+ }
1410
+
1411
+ /* Past comment indicator on lines */
1412
+ .diff-line[data-has-history]::before {
1413
+ content: '💬';
1414
+ position: absolute;
1415
+ left: 4px;
1416
+ font-size: 10px;
1417
+ opacity: 0.6;
1418
+ }
1419
+ .diff-line { position: relative; }
1420
+ .past-comment-overlay {
1421
+ background: var(--selected-bg);
1422
+ border-left: 3px solid var(--muted);
1423
+ margin: 0;
1424
+ padding: 8px 12px;
1425
+ font-size: 11px;
1426
+ color: var(--muted);
1427
+ display: none;
1428
+ }
1429
+ .past-comment-overlay.visible { display: block; }
1430
+ .past-comment-header {
1431
+ display: flex;
1432
+ justify-content: space-between;
1433
+ align-items: center;
1434
+ margin-bottom: 4px;
1435
+ }
1436
+ .past-comment-date {
1437
+ font-size: 10px;
1438
+ color: var(--muted);
1439
+ }
1440
+ .past-comment-toggle {
1441
+ background: transparent;
1442
+ border: none;
1443
+ color: var(--muted);
1444
+ cursor: pointer;
1445
+ font-size: 10px;
1446
+ padding: 2px 4px;
1447
+ }
1448
+ .past-comment-toggle:hover { color: var(--text); }
1449
+ .past-comment-text {
1450
+ color: var(--text);
1451
+ white-space: pre-wrap;
1452
+ line-height: 1.4;
1453
+ }
1238
1454
  </style>
1239
1455
  </head>
1240
1456
  <body>
@@ -1245,11 +1461,23 @@ function diffHtmlTemplate(diffData) {
1245
1461
  <span class="pill">Comments <strong id="comment-count">0</strong></span>
1246
1462
  </div>
1247
1463
  <div class="actions">
1464
+ <button class="history-toggle" id="history-toggle" title="Review History">☰</button>
1248
1465
  <button class="theme-toggle" id="theme-toggle" title="Toggle theme"><span id="theme-icon">🌙</span></button>
1249
1466
  <button id="send-and-exit">Submit & Exit</button>
1250
1467
  </div>
1251
1468
  </header>
1252
1469
 
1470
+ <!-- History Panel -->
1471
+ <aside class="history-panel" id="history-panel">
1472
+ <div class="history-panel-header">
1473
+ <h3>📜 Review History</h3>
1474
+ <button class="history-panel-close" id="history-panel-close">✕</button>
1475
+ </div>
1476
+ <div class="history-panel-body" id="history-panel-body">
1477
+ <div class="history-empty">No review history yet.</div>
1478
+ </div>
1479
+ </aside>
1480
+
1253
1481
  <div class="wrap">
1254
1482
  ${rows.length === 0 ? '<div class="no-diff"><h2>No changes</h2><p>Working tree is clean</p></div>' : '<div class="diff-container" id="diff-container"></div>'}
1255
1483
  </div>
@@ -1283,11 +1511,11 @@ function diffHtmlTemplate(diffData) {
1283
1511
  <label for="global-comment">Overall comment (optional)</label>
1284
1512
  <textarea id="global-comment" placeholder="Add a summary or overall feedback..."></textarea>
1285
1513
  <div class="modal-checkboxes">
1286
- <label><input type="checkbox" id="prompt-subagents" checked /> All implementation, verification, and report creation will be done by the sub-agents.</label>
1287
- <label><input type="checkbox" id="prompt-reviw" checked /> Open in REVIW next time.</label>
1288
- <label><input type="checkbox" id="prompt-screenshots" checked /> Update all screenshots and videos.</label>
1289
- <label><input type="checkbox" id="prompt-user-feedback-todo" checked /> Add the user's feedback to the Todo list, and do not check it off without the user's approval.</label>
1290
- <label><input type="checkbox" id="prompt-deep-dive" checked /> Before implementing, deeply probe the user's request. If using Claude Code, start with AskUserQuestion and EnterPlanMode; otherwise achieve the same depth through interactive questions and planning, even if the UI differs.</label>
1514
+ <label><input type="checkbox" id="prompt-subagents" checked /> 🤖 Delegate to sub-agents (implement, verify, report)</label>
1515
+ <label><input type="checkbox" id="prompt-reviw" checked /> 👁️ Open in REVIW next time</label>
1516
+ <label><input type="checkbox" id="prompt-screenshots" checked /> 📸 Update all screenshots/videos</label>
1517
+ <label><input type="checkbox" id="prompt-user-feedback-todo" checked /> Add feedback to Todo (require approval)</label>
1518
+ <label><input type="checkbox" id="prompt-deep-dive" checked /> 🔍 Probe requirements before implementing</label>
1291
1519
  </div>
1292
1520
  <div class="modal-actions">
1293
1521
  <button id="modal-cancel">Cancel</button>
@@ -1298,8 +1526,9 @@ function diffHtmlTemplate(diffData) {
1298
1526
 
1299
1527
  <script>
1300
1528
  const DATA = ${serialized};
1301
- const FILE_NAME = ${serializeForScript(title)};
1529
+ const FILE_NAME = ${serializeForScript(relativePath)};
1302
1530
  const MODE = 'diff';
1531
+ const HISTORY_DATA = ${historyJson};
1303
1532
 
1304
1533
  // Theme
1305
1534
  (function initTheme() {
@@ -1319,6 +1548,166 @@ function diffHtmlTemplate(diffData) {
1319
1548
  });
1320
1549
  })();
1321
1550
 
1551
+ // --- History Management ---
1552
+ // History is now server-side (file-based), HISTORY_DATA is provided by server
1553
+
1554
+ function loadHistory() {
1555
+ // Return server-provided history data
1556
+ return Array.isArray(HISTORY_DATA) ? HISTORY_DATA : [];
1557
+ }
1558
+
1559
+ // saveToHistory is handled server-side via /exit endpoint
1560
+ function saveToHistory(payload) {
1561
+ // No-op on client - server saves history when receiving /exit
1562
+ }
1563
+
1564
+ function formatDate(isoString) {
1565
+ const d = new Date(isoString);
1566
+ return d.toLocaleDateString('ja-JP', { year: 'numeric', month: '2-digit', day: '2-digit' });
1567
+ }
1568
+
1569
+ function formatTime(isoString) {
1570
+ const d = new Date(isoString);
1571
+ return d.toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit' });
1572
+ }
1573
+
1574
+ function getBasename(filepath) {
1575
+ return filepath.split('/').pop() || filepath;
1576
+ }
1577
+
1578
+ function renderHistoryPanel() {
1579
+ const body = document.getElementById('history-panel-body');
1580
+ const history = loadHistory();
1581
+ if (history.length === 0) {
1582
+ body.innerHTML = '<div class="history-empty">No review history yet.</div>';
1583
+ return;
1584
+ }
1585
+
1586
+ const grouped = {};
1587
+ history.forEach((item, idx) => {
1588
+ const date = formatDate(item.submittedAt);
1589
+ if (!grouped[date]) grouped[date] = [];
1590
+ grouped[date].push({ ...item, _idx: idx });
1591
+ });
1592
+
1593
+ let html = '';
1594
+ for (const date of Object.keys(grouped)) {
1595
+ html += \`<div class="history-date-group">
1596
+ <div class="history-date">\${date}</div>\`;
1597
+ for (const item of grouped[date]) {
1598
+ const commentCount = item.comments?.length || 0;
1599
+ html += \`<div class="history-item" data-idx="\${item._idx}">
1600
+ <div class="history-item-header">
1601
+ <span class="history-item-file">\${getBasename(item.file)}</span>
1602
+ <span class="history-item-time">\${formatTime(item.submittedAt)}<span class="history-badge">\${commentCount}</span></span>
1603
+ </div>
1604
+ <div class="history-item-body">\`;
1605
+ if (item.summary) {
1606
+ html += \`<div class="history-summary">
1607
+ <div class="history-summary-label">Summary</div>
1608
+ <div class="history-summary-text">\${escapeHtmlForHistory(item.summary)}</div>
1609
+ </div>\`;
1610
+ }
1611
+ if (commentCount > 0) {
1612
+ html += \`<div class="history-comments-label">Line Comments (\${commentCount})</div>\`;
1613
+ for (const c of item.comments) {
1614
+ const lineLabel = c.line ? \`L\${c.line}\${c.lineEnd ? '-' + c.lineEnd : ''}\` : (c.row != null ? \`L\${c.row}\` : '');
1615
+ const text = c.comment || c.text || '';
1616
+ // Support both direct content and context.content structures
1617
+ const content = c.content || c.context?.content || '';
1618
+ html += \`<div class="history-comment">
1619
+ <div class="history-comment-line">\${lineLabel}</div>\`;
1620
+ if (content) {
1621
+ html += \`<div class="history-comment-quote">\${escapeHtmlForHistory(content)}</div>\`;
1622
+ }
1623
+ html += \`<div class="history-comment-text">\${escapeHtmlForHistory(text)}</div>
1624
+ </div>\`;
1625
+ }
1626
+ }
1627
+ html += \`</div></div>\`;
1628
+ }
1629
+ html += \`</div>\`;
1630
+ }
1631
+ body.innerHTML = html;
1632
+
1633
+ body.querySelectorAll('.history-item-header').forEach(header => {
1634
+ header.addEventListener('click', () => {
1635
+ header.parentElement.classList.toggle('expanded');
1636
+ });
1637
+ });
1638
+ }
1639
+
1640
+ function escapeHtmlForHistory(s) {
1641
+ if (!s) return '';
1642
+ return String(s).replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c] || c));
1643
+ }
1644
+
1645
+ function getHistoryForFile(filename) {
1646
+ const history = loadHistory();
1647
+ return history.filter(h => h.file === filename || getBasename(h.file) === getBasename(filename));
1648
+ }
1649
+
1650
+ function renderPastCommentsOnLines() {
1651
+ const fileHistory = getHistoryForFile(FILE_NAME);
1652
+ if (fileHistory.length === 0) return;
1653
+
1654
+ const lineComments = {};
1655
+ fileHistory.forEach(h => {
1656
+ if (!h.comments) return;
1657
+ h.comments.forEach(c => {
1658
+ const line = c.line || c.row;
1659
+ if (!line) return;
1660
+ if (!lineComments[line]) lineComments[line] = [];
1661
+ lineComments[line].push({
1662
+ date: formatDate(h.submittedAt),
1663
+ text: c.comment || c.text || ''
1664
+ });
1665
+ });
1666
+ });
1667
+
1668
+ Object.entries(lineComments).forEach(([line, comments]) => {
1669
+ const lineEl = document.querySelector('[data-row="' + line + '"]');
1670
+ if (lineEl && !lineEl.dataset.hasHistory) {
1671
+ lineEl.dataset.hasHistory = 'true';
1672
+ lineEl.title = comments.length + ' past comment(s) - click to view in History panel';
1673
+ }
1674
+ });
1675
+ }
1676
+
1677
+ // History Panel Toggle
1678
+ (function initHistoryPanel() {
1679
+ const toggle = document.getElementById('history-toggle');
1680
+ const panel = document.getElementById('history-panel');
1681
+ const closeBtn = document.getElementById('history-panel-close');
1682
+
1683
+ function openPanel() {
1684
+ panel.classList.add('open');
1685
+ document.body.classList.add('history-open');
1686
+ renderHistoryPanel();
1687
+ }
1688
+
1689
+ function closePanel() {
1690
+ panel.classList.remove('open');
1691
+ document.body.classList.remove('history-open');
1692
+ }
1693
+
1694
+ toggle?.addEventListener('click', () => {
1695
+ if (panel.classList.contains('open')) {
1696
+ closePanel();
1697
+ } else {
1698
+ openPanel();
1699
+ }
1700
+ });
1701
+
1702
+ closeBtn?.addEventListener('click', closePanel);
1703
+
1704
+ document.addEventListener('keydown', (e) => {
1705
+ if (e.key === 'Escape' && panel.classList.contains('open')) {
1706
+ closePanel();
1707
+ }
1708
+ });
1709
+ })();
1710
+
1322
1711
  const container = document.getElementById('diff-container');
1323
1712
  const card = document.getElementById('comment-card');
1324
1713
  const commentInput = document.getElementById('comment-input');
@@ -1628,7 +2017,18 @@ function diffHtmlTemplate(diffData) {
1628
2017
  document.getElementById('clear-comment').addEventListener('click', clearCurrent);
1629
2018
  document.getElementById('close-card').addEventListener('click', closeCard);
1630
2019
  document.addEventListener('keydown', e => {
1631
- if (e.key === 'Escape') closeCard();
2020
+ if (e.key === 'Escape') {
2021
+ // Don't close card if any fullscreen overlay is open
2022
+ const imageOverlay = document.getElementById('image-fullscreen');
2023
+ const videoOverlay = document.getElementById('video-fullscreen');
2024
+ const mermaidOverlay = document.getElementById('mermaid-fullscreen');
2025
+ if (imageOverlay?.classList.contains('visible') ||
2026
+ videoOverlay?.classList.contains('visible') ||
2027
+ mermaidOverlay?.classList.contains('visible')) {
2028
+ return; // Let the fullscreen handlers handle ESC
2029
+ }
2030
+ closeCard();
2031
+ }
1632
2032
  if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') saveCurrent();
1633
2033
  });
1634
2034
 
@@ -1657,13 +2057,13 @@ function diffHtmlTemplate(diffData) {
1657
2057
  const modalSummary = document.getElementById('modal-summary');
1658
2058
  const globalCommentInput = document.getElementById('global-comment');
1659
2059
 
1660
- // Prompt checkboxes
2060
+ // Prompt checkboxes - text is the strong enforcement prompt for YAML output
1661
2061
  const promptCheckboxes = [
1662
- { id: 'prompt-subagents', text: 'All implementation, verification, and report creation will be done by the sub-agents.' },
1663
- { id: 'prompt-reviw', text: 'Open in REVIW next time.' },
1664
- { id: 'prompt-screenshots', text: 'Update all screenshots and videos.' },
1665
- { id: 'prompt-user-feedback-todo', text: "Add the user's feedback to the Todo list, and do not check it off without the user's approval." },
1666
- { id: 'prompt-deep-dive', text: "Before implementing, deeply probe the user's request. If using Claude Code, start with AskUserQuestion and EnterPlanMode; otherwise achieve the same depth through interactive questions and planning, even if the UI differs." }
2062
+ { id: 'prompt-subagents', text: 'MANDATORY: You MUST delegate ALL implementation, verification, and report creation to sub-agents. Direct execution on the main thread is PROHIBITED.' },
2063
+ { id: 'prompt-reviw', text: 'REQUIRED: Before reporting completion, you MUST open the result in REVIW for user review. Skipping this step is NOT allowed.' },
2064
+ { id: 'prompt-screenshots', text: 'MANDATORY: You MUST update ALL screenshots and videos as evidence. Reports without visual proof are REJECTED.' },
2065
+ { id: 'prompt-user-feedback-todo', text: "STRICT RULE: Add ALL user feedback to the Todo list. You are FORBIDDEN from marking any item complete without explicit user approval." },
2066
+ { id: 'prompt-deep-dive', text: "REQUIRED: Before ANY implementation, you MUST deeply probe the user's requirements using AskUserQuestion and EnterPlanMode. Starting implementation without thorough requirement analysis is PROHIBITED." }
1667
2067
  ];
1668
2068
  const PROMPT_STORAGE_KEY = 'reviw-prompt-prefs';
1669
2069
 
@@ -1710,7 +2110,7 @@ function diffHtmlTemplate(diffData) {
1710
2110
  }
1711
2111
 
1712
2112
  function payload(reason) {
1713
- const data = { file: FILE_NAME, mode: MODE, reason, at: new Date().toISOString(), comments: Object.values(comments) };
2113
+ const data = { file: FILE_NAME, mode: MODE, submittedBy: reason, submittedAt: new Date().toISOString(), comments: Object.values(comments) };
1714
2114
  if (globalComment.trim()) data.summary = globalComment.trim();
1715
2115
  const prompts = getSelectedPrompts();
1716
2116
  if (prompts.length > 0) data.prompts = prompts;
@@ -1720,7 +2120,9 @@ function diffHtmlTemplate(diffData) {
1720
2120
  if (sent) return;
1721
2121
  sent = true;
1722
2122
  clearStorage();
1723
- navigator.sendBeacon('/exit', new Blob([JSON.stringify(payload(reason))], { type: 'application/json' }));
2123
+ const p = payload(reason);
2124
+ saveToHistory(p);
2125
+ navigator.sendBeacon('/exit', new Blob([JSON.stringify(p)], { type: 'application/json' }));
1724
2126
  }
1725
2127
  function showSubmitModal() {
1726
2128
  const count = Object.keys(comments).length;
@@ -1783,6 +2185,7 @@ function diffHtmlTemplate(diffData) {
1783
2185
 
1784
2186
  renderDiff();
1785
2187
  refreshList();
2188
+ renderPastCommentsOnLines();
1786
2189
 
1787
2190
  // Recovery
1788
2191
  (function checkRecovery() {
@@ -1802,11 +2205,12 @@ function diffHtmlTemplate(diffData) {
1802
2205
  }
1803
2206
 
1804
2207
  // --- HTML template ---------------------------------------------------------
1805
- function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHtml, reviwQuestions = []) {
2208
+ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHtml, reviwQuestions = [], history = []) {
1806
2209
  const serialized = serializeForScript(dataRows);
1807
2210
  const modeJson = serializeForScript(mode);
1808
2211
  const titleJson = serializeForScript(relativePath); // Use relativePath as file identifier
1809
2212
  const questionsJson = serializeForScript(reviwQuestions || []);
2213
+ const historyJson = serializeForScript(history);
1810
2214
  const hasPreview = !!previewHtml;
1811
2215
  return `<!doctype html>
1812
2216
  <html lang="ja">
@@ -1979,7 +2383,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
1979
2383
  position: sticky;
1980
2384
  top: 0;
1981
2385
  z-index: 3;
1982
- background: var(--panel-solid);
2386
+ background: var(--panel-solid) !important;
1983
2387
  color: var(--muted);
1984
2388
  font-size: 12px;
1985
2389
  text-align: center;
@@ -1989,6 +2393,9 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
1989
2393
  white-space: nowrap;
1990
2394
  transition: background 200ms ease;
1991
2395
  }
2396
+ thead th:not(.selected) {
2397
+ background: var(--panel-solid) !important;
2398
+ }
1992
2399
  thead th:first-child,
1993
2400
  tbody th {
1994
2401
  width: 28px;
@@ -2093,7 +2500,9 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
2093
2500
  tr:nth-child(even) td:not(.selected):not(.has-comment) { background: var(--row-even); }
2094
2501
  td:hover:not(.selected) { background: var(--hover-bg); box-shadow: inset 0 0 0 1px rgba(96,165,250,0.25); }
2095
2502
  td.has-comment { background: rgba(34,197,94,0.12); box-shadow: inset 0 0 0 1px rgba(34,197,94,0.35); }
2096
- td.selected, th.selected, thead th.selected { background: rgba(99,102,241,0.22) !important; box-shadow: inset 0 0 0 1px rgba(99,102,241,0.45); }
2503
+ td.selected, tbody th.selected { background: rgba(99,102,241,0.22) !important; box-shadow: inset 0 0 0 1px rgba(99,102,241,0.45); }
2504
+ thead th.selected { background: #c7d2fe !important; box-shadow: inset 0 0 0 1px rgba(99,102,241,0.45); }
2505
+ [data-theme="dark"] thead th.selected { background: #3730a3 !important; }
2097
2506
  body.dragging { user-select: none; cursor: crosshair; }
2098
2507
  body.dragging td, body.dragging tbody th { cursor: crosshair; }
2099
2508
  tbody th { cursor: pointer; }
@@ -2255,6 +2664,19 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
2255
2664
  max-height: none;
2256
2665
  overflow: visible;
2257
2666
  }
2667
+ /* Ensure thead is opaque in md-right to prevent content showing through */
2668
+ .md-right thead th {
2669
+ background: var(--panel-solid) !important;
2670
+ }
2671
+ .md-right thead th.selected {
2672
+ background: #c7d2fe !important;
2673
+ }
2674
+ [data-theme="dark"] .md-right thead th {
2675
+ background: var(--panel-solid) !important;
2676
+ }
2677
+ [data-theme="dark"] .md-right thead th.selected {
2678
+ background: #3730a3 !important;
2679
+ }
2258
2680
  .md-preview h1, .md-preview h2, .md-preview h3, .md-preview h4 {
2259
2681
  margin: 0.4em 0 0.2em;
2260
2682
  }
@@ -3090,6 +3512,169 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
3090
3512
  font-family: monospace;
3091
3513
  }
3092
3514
  .mermaid-error-toast.visible { display: block; }
3515
+
3516
+ /* History Panel - Push layout */
3517
+ body { transition: margin-right 0.25s ease; }
3518
+ body.history-open { margin-right: 320px; }
3519
+ body.history-open header { right: 320px; }
3520
+ header { transition: right 0.25s ease; right: 0; }
3521
+
3522
+ .history-toggle {
3523
+ background: var(--selected-bg);
3524
+ color: var(--text);
3525
+ border: 1px solid var(--border);
3526
+ border-radius: 6px;
3527
+ padding: 6px 8px;
3528
+ font-size: 14px;
3529
+ cursor: pointer;
3530
+ width: 34px;
3531
+ height: 34px;
3532
+ display: flex;
3533
+ align-items: center;
3534
+ justify-content: center;
3535
+ }
3536
+ .history-toggle:hover { background: var(--border); }
3537
+ .history-panel {
3538
+ position: fixed;
3539
+ top: 0;
3540
+ right: 0;
3541
+ width: 320px;
3542
+ height: 100vh;
3543
+ background: var(--panel-solid);
3544
+ border-left: 1px solid var(--border);
3545
+ z-index: 90;
3546
+ transform: translateX(100%);
3547
+ transition: transform 0.25s ease;
3548
+ display: flex;
3549
+ flex-direction: column;
3550
+ }
3551
+ .history-panel.open { transform: translateX(0); }
3552
+ .history-panel-header {
3553
+ padding: 16px;
3554
+ border-bottom: 1px solid var(--border);
3555
+ display: flex;
3556
+ justify-content: space-between;
3557
+ align-items: center;
3558
+ }
3559
+ .history-panel-header h3 { margin: 0; font-size: 14px; font-weight: 600; }
3560
+ .history-panel-close {
3561
+ background: transparent;
3562
+ border: none;
3563
+ color: var(--muted);
3564
+ cursor: pointer;
3565
+ font-size: 18px;
3566
+ padding: 4px;
3567
+ }
3568
+ .history-panel-close:hover { color: var(--text); }
3569
+ .history-panel-body {
3570
+ flex: 1;
3571
+ overflow-y: auto;
3572
+ padding: 12px;
3573
+ }
3574
+ .history-empty {
3575
+ color: var(--muted);
3576
+ font-size: 13px;
3577
+ text-align: center;
3578
+ padding: 40px 20px;
3579
+ }
3580
+ .history-date-group { margin-bottom: 16px; }
3581
+ .history-date {
3582
+ font-size: 11px;
3583
+ font-weight: 600;
3584
+ color: var(--muted);
3585
+ margin-bottom: 8px;
3586
+ text-transform: uppercase;
3587
+ }
3588
+ .history-item {
3589
+ background: var(--bg);
3590
+ border: 1px solid var(--border);
3591
+ border-radius: 6px;
3592
+ margin-bottom: 8px;
3593
+ overflow: hidden;
3594
+ }
3595
+ .history-item-header {
3596
+ display: flex;
3597
+ justify-content: space-between;
3598
+ align-items: center;
3599
+ padding: 8px 10px;
3600
+ background: var(--selected-bg);
3601
+ cursor: pointer;
3602
+ }
3603
+ .history-item-header:hover { background: var(--hover-bg); }
3604
+ .history-item-file {
3605
+ font-size: 12px;
3606
+ font-weight: 600;
3607
+ color: var(--text);
3608
+ white-space: nowrap;
3609
+ overflow: hidden;
3610
+ text-overflow: ellipsis;
3611
+ max-width: 180px;
3612
+ }
3613
+ .history-item-time { font-size: 10px; color: var(--muted); }
3614
+ .history-item-body {
3615
+ display: none;
3616
+ padding: 10px;
3617
+ font-size: 12px;
3618
+ border-top: 1px solid var(--border);
3619
+ }
3620
+ .history-item.expanded .history-item-body { display: block; }
3621
+ .history-summary {
3622
+ color: var(--text);
3623
+ margin-bottom: 8px;
3624
+ padding-bottom: 8px;
3625
+ border-bottom: 1px solid var(--border);
3626
+ }
3627
+ .history-summary-label {
3628
+ font-size: 10px;
3629
+ font-weight: 600;
3630
+ color: var(--muted);
3631
+ margin-bottom: 4px;
3632
+ }
3633
+ .history-summary-text { white-space: pre-wrap; line-height: 1.4; }
3634
+ .history-comments-label {
3635
+ font-size: 10px;
3636
+ font-weight: 600;
3637
+ color: var(--muted);
3638
+ margin-bottom: 6px;
3639
+ }
3640
+ .history-comment {
3641
+ padding: 6px 0;
3642
+ border-bottom: 1px solid var(--border);
3643
+ }
3644
+ .history-comment:last-child { border-bottom: none; }
3645
+ .history-comment-line {
3646
+ font-size: 10px;
3647
+ color: var(--accent);
3648
+ font-weight: 600;
3649
+ margin-bottom: 2px;
3650
+ }
3651
+ .history-comment-quote {
3652
+ background: rgba(0, 0, 0, 0.3);
3653
+ border-left: 2px solid var(--accent);
3654
+ padding: 4px 8px;
3655
+ margin: 4px 0;
3656
+ font-family: 'SF Mono', Monaco, Consolas, monospace;
3657
+ font-size: 11px;
3658
+ color: var(--muted);
3659
+ white-space: pre-wrap;
3660
+ word-break: break-all;
3661
+ max-height: 80px;
3662
+ overflow-y: auto;
3663
+ }
3664
+ .history-comment-text {
3665
+ color: var(--text);
3666
+ line-height: 1.4;
3667
+ white-space: pre-wrap;
3668
+ }
3669
+ .history-badge {
3670
+ display: inline-block;
3671
+ background: var(--accent);
3672
+ color: var(--text-inverse);
3673
+ font-size: 10px;
3674
+ padding: 2px 6px;
3675
+ border-radius: 10px;
3676
+ margin-left: 6px;
3677
+ }
3093
3678
  </style>
3094
3679
  <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
3095
3680
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/styles/github-dark.min.css" id="hljs-theme-dark">
@@ -3104,6 +3689,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
3104
3689
  <span class="pill">Comments <strong id="comment-count">0</strong></span>
3105
3690
  </div>
3106
3691
  <div class="actions">
3692
+ <button class="history-toggle" id="history-toggle" title="Review History">☰</button>
3107
3693
  <button class="theme-toggle" id="theme-toggle" title="Toggle theme" aria-label="Toggle theme">
3108
3694
  <span id="theme-icon">🌙</span>
3109
3695
  </button>
@@ -3111,6 +3697,17 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
3111
3697
  </div>
3112
3698
  </header>
3113
3699
 
3700
+ <!-- History Panel -->
3701
+ <aside class="history-panel" id="history-panel">
3702
+ <div class="history-panel-header">
3703
+ <h3>📜 Review History</h3>
3704
+ <button class="history-panel-close" id="history-panel-close">✕</button>
3705
+ </div>
3706
+ <div class="history-panel-body" id="history-panel-body">
3707
+ <div class="history-empty">No review history yet.</div>
3708
+ </div>
3709
+ </aside>
3710
+
3114
3711
  <div class="wrap">
3115
3712
  ${
3116
3713
  hasPreview && mode === "markdown"
@@ -3216,11 +3813,11 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
3216
3813
  <label for="global-comment">Overall comment (optional)</label>
3217
3814
  <textarea id="global-comment" placeholder="Add a summary or overall feedback..."></textarea>
3218
3815
  <div class="modal-checkboxes">
3219
- <label><input type="checkbox" id="prompt-subagents" checked /> All implementation, verification, and report creation will be done by the sub-agents.</label>
3220
- <label><input type="checkbox" id="prompt-reviw" checked /> Open in REVIW next time.</label>
3221
- <label><input type="checkbox" id="prompt-screenshots" checked /> Update all screenshots and videos.</label>
3222
- <label><input type="checkbox" id="prompt-user-feedback-todo" checked /> Add the user's feedback to the Todo list, and do not check it off without the user's approval.</label>
3223
- <label><input type="checkbox" id="prompt-deep-dive" checked /> Before implementing, deeply probe the user's request. If using Claude Code, start with AskUserQuestion and EnterPlanMode; otherwise achieve the same depth through interactive questions and planning, even if the UI differs.</label>
3816
+ <label><input type="checkbox" id="prompt-subagents" checked /> 🤖 Delegate to sub-agents (implement, verify, report)</label>
3817
+ <label><input type="checkbox" id="prompt-reviw" checked /> 👁️ Open in REVIW next time</label>
3818
+ <label><input type="checkbox" id="prompt-screenshots" checked /> 📸 Update all screenshots/videos</label>
3819
+ <label><input type="checkbox" id="prompt-user-feedback-todo" checked /> Add feedback to Todo (require approval)</label>
3820
+ <label><input type="checkbox" id="prompt-deep-dive" checked /> 🔍 Probe requirements before implementing</label>
3224
3821
  </div>
3225
3822
  <div class="modal-actions">
3226
3823
  <button id="modal-cancel">Cancel</button>
@@ -3285,6 +3882,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
3285
3882
  const FILE_NAME = ${titleJson};
3286
3883
  const MODE = ${modeJson};
3287
3884
  const REVIW_QUESTIONS = ${questionsJson};
3885
+ const HISTORY_DATA = ${historyJson};
3288
3886
 
3289
3887
  // --- Theme Management ---
3290
3888
  (function initTheme() {
@@ -3337,6 +3935,134 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
3337
3935
  themeToggle.addEventListener('click', toggleTheme);
3338
3936
  })();
3339
3937
 
3938
+ // --- History Management ---
3939
+ // History is now server-side (file-based), HISTORY_DATA is provided by server
3940
+
3941
+ function loadHistory() {
3942
+ // Return server-provided history data
3943
+ return Array.isArray(HISTORY_DATA) ? HISTORY_DATA : [];
3944
+ }
3945
+
3946
+ // saveToHistory is handled server-side via /exit endpoint
3947
+ function saveToHistory(payload) {
3948
+ // No-op on client - server saves history when receiving /exit
3949
+ }
3950
+
3951
+ function formatDate(isoString) {
3952
+ const d = new Date(isoString);
3953
+ return d.toLocaleDateString('ja-JP', { year: 'numeric', month: '2-digit', day: '2-digit' });
3954
+ }
3955
+
3956
+ function formatTime(isoString) {
3957
+ const d = new Date(isoString);
3958
+ return d.toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit' });
3959
+ }
3960
+
3961
+ function getBasename(filepath) {
3962
+ return filepath.split('/').pop() || filepath;
3963
+ }
3964
+
3965
+ function escapeHtmlForHistory(s) {
3966
+ if (!s) return '';
3967
+ return String(s).replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c] || c));
3968
+ }
3969
+
3970
+ function renderHistoryPanel() {
3971
+ const body = document.getElementById('history-panel-body');
3972
+ const history = loadHistory();
3973
+ if (history.length === 0) {
3974
+ body.innerHTML = '<div class="history-empty">No review history yet.</div>';
3975
+ return;
3976
+ }
3977
+
3978
+ const grouped = {};
3979
+ history.forEach((item, idx) => {
3980
+ const date = formatDate(item.submittedAt);
3981
+ if (!grouped[date]) grouped[date] = [];
3982
+ grouped[date].push({ ...item, _idx: idx });
3983
+ });
3984
+
3985
+ let html = '';
3986
+ for (const date of Object.keys(grouped)) {
3987
+ html += \`<div class="history-date-group">
3988
+ <div class="history-date">\${date}</div>\`;
3989
+ for (const item of grouped[date]) {
3990
+ const commentCount = item.comments?.length || 0;
3991
+ html += \`<div class="history-item" data-idx="\${item._idx}">
3992
+ <div class="history-item-header">
3993
+ <span class="history-item-file">\${escapeHtmlForHistory(getBasename(item.file))}</span>
3994
+ <span class="history-item-time">\${formatTime(item.submittedAt)}<span class="history-badge">\${commentCount}</span></span>
3995
+ </div>
3996
+ <div class="history-item-body">\`;
3997
+ if (item.summary) {
3998
+ html += \`<div class="history-summary">
3999
+ <div class="history-summary-label">Summary</div>
4000
+ <div class="history-summary-text">\${escapeHtmlForHistory(item.summary)}</div>
4001
+ </div>\`;
4002
+ }
4003
+ if (commentCount > 0) {
4004
+ html += \`<div class="history-comments-label">Line Comments (\${commentCount})</div>\`;
4005
+ for (const c of item.comments) {
4006
+ const lineLabel = c.line ? \`L\${c.line}\${c.lineEnd ? '-' + c.lineEnd : ''}\` : (c.row != null ? \`L\${c.row}\` : '');
4007
+ const text = c.comment || c.text || '';
4008
+ // Support both direct content and context.content structures
4009
+ const content = c.content || c.context?.content || c.value || '';
4010
+ html += \`<div class="history-comment">
4011
+ <div class="history-comment-line">\${lineLabel}</div>\`;
4012
+ if (content) {
4013
+ html += \`<div class="history-comment-quote">\${escapeHtmlForHistory(content)}</div>\`;
4014
+ }
4015
+ html += \`<div class="history-comment-text">\${escapeHtmlForHistory(text)}</div>
4016
+ </div>\`;
4017
+ }
4018
+ }
4019
+ html += \`</div></div>\`;
4020
+ }
4021
+ html += \`</div>\`;
4022
+ }
4023
+ body.innerHTML = html;
4024
+
4025
+ body.querySelectorAll('.history-item-header').forEach(header => {
4026
+ header.addEventListener('click', () => {
4027
+ header.parentElement.classList.toggle('expanded');
4028
+ });
4029
+ });
4030
+ }
4031
+
4032
+ // History Panel Toggle
4033
+ (function initHistoryPanel() {
4034
+ const toggle = document.getElementById('history-toggle');
4035
+ const panel = document.getElementById('history-panel');
4036
+ const closeBtn = document.getElementById('history-panel-close');
4037
+
4038
+ function openPanel() {
4039
+ panel.classList.add('open');
4040
+ document.body.classList.add('history-open');
4041
+ renderHistoryPanel();
4042
+ }
4043
+
4044
+ function closePanel() {
4045
+ panel.classList.remove('open');
4046
+ document.body.classList.remove('history-open');
4047
+ }
4048
+
4049
+ toggle?.addEventListener('click', () => {
4050
+ if (panel.classList.contains('open')) {
4051
+ closePanel();
4052
+ } else {
4053
+ openPanel();
4054
+ }
4055
+ });
4056
+
4057
+ closeBtn?.addEventListener('click', closePanel);
4058
+
4059
+ document.addEventListener('keydown', (e) => {
4060
+ if (e.key === 'Escape' && panel.classList.contains('open')) {
4061
+ closePanel();
4062
+ }
4063
+ });
4064
+ })();
4065
+
3340
4066
  const tbody = document.getElementById('tbody');
3341
4067
  const table = document.getElementById('csv-table');
3342
4068
  const colgroup = document.getElementById('colgroup');
@@ -4083,7 +4809,18 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
4083
4809
  document.getElementById('clear-comment').addEventListener('click', clearCurrent);
4084
4810
  document.getElementById('close-card').addEventListener('click', closeCard);
4085
4811
  document.addEventListener('keydown', (e) => {
4086
- if (e.key === 'Escape') closeCard();
4812
+ if (e.key === 'Escape') {
4813
+ // Don't close card if any fullscreen overlay is open
4814
+ const imageOverlay = document.getElementById('image-fullscreen');
4815
+ const videoOverlay = document.getElementById('video-fullscreen');
4816
+ const mermaidOverlay = document.getElementById('mermaid-fullscreen');
4817
+ if (imageOverlay?.classList.contains('visible') ||
4818
+ videoOverlay?.classList.contains('visible') ||
4819
+ mermaidOverlay?.classList.contains('visible')) {
4820
+ return; // Let the fullscreen handlers handle ESC
4821
+ }
4822
+ closeCard();
4823
+ }
4087
4824
  if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') saveCurrent();
4088
4825
  });
4089
4826
 
@@ -4217,13 +4954,13 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
4217
4954
  const modalCancel = document.getElementById('modal-cancel');
4218
4955
  const modalSubmit = document.getElementById('modal-submit');
4219
4956
 
4220
- // Prompt checkboxes
4957
+ // Prompt checkboxes - text is the strong enforcement prompt for YAML output
4221
4958
  const promptCheckboxes = [
4222
- { id: 'prompt-subagents', text: 'All implementation, verification, and report creation will be done by the sub-agents.' },
4223
- { id: 'prompt-reviw', text: 'Open in REVIW next time.' },
4224
- { id: 'prompt-screenshots', text: 'Update all screenshots and videos.' },
4225
- { id: 'prompt-user-feedback-todo', text: "Add the user's feedback to the Todo list, and do not check it off without the user's approval." },
4226
- { id: 'prompt-deep-dive', text: "Before implementing, deeply probe the user's request. If using Claude Code, start with AskUserQuestion and EnterPlanMode; otherwise achieve the same depth through interactive questions and planning, even if the UI differs." }
4959
+ { id: 'prompt-subagents', text: 'MANDATORY: You MUST delegate ALL implementation, verification, and report creation to sub-agents. Direct execution on the main thread is PROHIBITED.' },
4960
+ { id: 'prompt-reviw', text: 'REQUIRED: Before reporting completion, you MUST open the result in REVIW for user review. Skipping this step is NOT allowed.' },
4961
+ { id: 'prompt-screenshots', text: 'MANDATORY: You MUST update ALL screenshots and videos as evidence. Reports without visual proof are REJECTED.' },
4962
+ { id: 'prompt-user-feedback-todo', text: "STRICT RULE: Add ALL user feedback to the Todo list. You are FORBIDDEN from marking any item complete without explicit user approval." },
4963
+ { id: 'prompt-deep-dive', text: "REQUIRED: Before ANY implementation, you MUST deeply probe the user's requirements using AskUserQuestion and EnterPlanMode. Starting implementation without thorough requirement analysis is PROHIBITED." }
4227
4964
  ];
4228
4965
  const PROMPT_STORAGE_KEY = 'reviw-prompt-prefs';
4229
4966
 
@@ -4269,13 +5006,75 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
4269
5006
  return prompts;
4270
5007
  }
4271
5008
 
5009
+ // Find nearest heading for a given line number (markdown context)
5010
+ function findNearestHeading(lineNum) {
5011
+ let nearestHeading = null;
5012
+ for (let i = lineNum - 1; i >= 0; i--) {
5013
+ const line = DATA[i] ? DATA[i][0] : '';
5014
+ const match = line.match(/^(#{1,6})\\s+(.+)/);
5015
+ if (match) {
5016
+ nearestHeading = match[2].trim();
5017
+ break;
5018
+ }
5019
+ }
5020
+ return nearestHeading;
5021
+ }
5022
+
5023
+ // Check if line is inside a table
5024
+ function getTableContext(lineNum) {
5025
+ const line = DATA[lineNum] ? DATA[lineNum][0] : '';
5026
+ if (!line.includes('|')) return null;
5027
+ // Find table header (look backwards for header row)
5028
+ for (let i = lineNum; i >= 0; i--) {
5029
+ const l = DATA[i] ? DATA[i][0] : '';
5030
+ if (!l.includes('|')) break;
5031
+ // Check if next line is separator (---|---)
5032
+ const nextLine = DATA[i + 1] ? DATA[i + 1][0] : '';
5033
+ if (nextLine && nextLine.match(/^\\|?[\\s-:|]+\\|/)) {
5034
+ // This is the header row
5035
+ return l.replace(/^\\|\\s*/, '').replace(/\\s*\\|$/, '').split('|').map(h => h.trim()).slice(0, 3).join(' | ') + (l.split('|').length > 4 ? ' ...' : '');
5036
+ }
5037
+ }
5038
+ return null;
5039
+ }
5040
+
5041
+ // Transform comments for markdown mode
5042
+ function transformMarkdownComments(rawComments) {
5043
+ return rawComments.map(c => {
5044
+ const lineNum = c.row || c.startRow || 0;
5045
+ const section = findNearestHeading(lineNum);
5046
+ const tableHeader = getTableContext(lineNum);
5047
+ const content = c.content || c.value || '';
5048
+ const truncatedContent = content.length > 60 ? content.substring(0, 60) + '...' : content;
5049
+
5050
+ const transformed = {
5051
+ line: lineNum + 1,
5052
+ context: {}
5053
+ };
5054
+ if (section) transformed.context.section = section;
5055
+ if (tableHeader) transformed.context.table = tableHeader;
5056
+ if (truncatedContent) transformed.context.content = truncatedContent;
5057
+ transformed.comment = c.text;
5058
+
5059
+ if (c.isRange) {
5060
+ transformed.lineEnd = (c.endRow || c.startRow) + 1;
5061
+ }
5062
+ return transformed;
5063
+ });
5064
+ }
5065
+
4272
5066
  function payload(reason) {
5067
+ const rawComments = Object.values(comments);
5068
+ const transformedComments = MODE === 'markdown'
5069
+ ? transformMarkdownComments(rawComments)
5070
+ : rawComments;
5071
+
4273
5072
  const data = {
4274
5073
  file: FILE_NAME,
4275
5074
  mode: MODE,
4276
- reason,
4277
- at: new Date().toISOString(),
4278
- comments: Object.values(comments)
5075
+ submittedBy: reason,
5076
+ submittedAt: new Date().toISOString(),
5077
+ comments: transformedComments
4279
5078
  };
4280
5079
  if (globalComment.trim()) {
4281
5080
  data.summary = globalComment.trim();
@@ -4304,7 +5103,9 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
4304
5103
  if (sent) return;
4305
5104
  sent = true;
4306
5105
  clearCommentsFromStorage();
4307
- const blob = new Blob([JSON.stringify(payload(reason))], { type: 'application/json' });
5106
+ const p = payload(reason);
5107
+ saveToHistory(p);
5108
+ const blob = new Blob([JSON.stringify(p)], { type: 'application/json' });
4308
5109
  navigator.sendBeacon('/exit', blob);
4309
5110
  }
4310
5111
  function showSubmitModal() {
@@ -5622,11 +6423,12 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
5622
6423
 
5623
6424
  function buildHtml(filePath) {
5624
6425
  const data = loadData(filePath);
6426
+ const history = loadHistoryFromFile(filePath);
5625
6427
  if (data.mode === "diff") {
5626
- return diffHtmlTemplate(data);
6428
+ return diffHtmlTemplate(data, history);
5627
6429
  }
5628
6430
  const { rows, cols, projectRoot, relativePath, mode, preview, reviwQuestions } = data;
5629
- return htmlTemplate(rows, cols, projectRoot, relativePath, mode, preview, reviwQuestions);
6431
+ return htmlTemplate(rows, cols, projectRoot, relativePath, mode, preview, reviwQuestions, history);
5630
6432
  }
5631
6433
 
5632
6434
  // --- HTTP Server -----------------------------------------------------------
@@ -5746,6 +6548,54 @@ function checkExistingServer(filePath) {
5746
6548
  }
5747
6549
  }
5748
6550
 
6551
+ // --- History File Management ---
6552
+ const HISTORY_DIR = path.join(os.homedir(), '.reviw', 'history');
6553
+ const HISTORY_MAX = 50;
6554
+
6555
+ function getHistoryFilePath(filePath) {
6556
+ // Use SHA256 hash of absolute path (same as lock files)
6557
+ const hash = crypto.createHash('sha256').update(path.resolve(filePath)).digest('hex').slice(0, 16);
6558
+ return path.join(HISTORY_DIR, hash + '.json');
6559
+ }
6560
+
6561
+ function ensureHistoryDir() {
6562
+ try {
6563
+ if (!fs.existsSync(HISTORY_DIR)) {
6564
+ fs.mkdirSync(HISTORY_DIR, { recursive: true, mode: 0o700 });
6565
+ }
6566
+ } catch (err) {
6567
+ // Ignore errors
6568
+ }
6569
+ }
6570
+
6571
+ function loadHistoryFromFile(filePath) {
6572
+ try {
6573
+ const historyPath = getHistoryFilePath(filePath);
6574
+ if (!fs.existsSync(historyPath)) {
6575
+ return [];
6576
+ }
6577
+ const data = JSON.parse(fs.readFileSync(historyPath, 'utf8'));
6578
+ return Array.isArray(data) ? data : [];
6579
+ } catch (err) {
6580
+ return [];
6581
+ }
6582
+ }
6583
+
6584
+ function saveHistoryToFile(filePath, historyEntry) {
6585
+ try {
6586
+ ensureHistoryDir();
6587
+ const historyPath = getHistoryFilePath(filePath);
6588
+ let history = loadHistoryFromFile(filePath);
6589
+ history.unshift(historyEntry);
6590
+ if (history.length > HISTORY_MAX) {
6591
+ history = history.slice(0, HISTORY_MAX);
6592
+ }
6593
+ fs.writeFileSync(historyPath, JSON.stringify(history, null, 2), { mode: 0o600 });
6594
+ } catch (err) {
6595
+ // Ignore errors - history is optional
6596
+ }
6597
+ }
6598
+
5749
6599
  // Try to activate an existing browser tab with the given URL (macOS only)
5750
6600
  // Returns true if a tab was activated, false otherwise
5751
6601
  function tryActivateExistingTab(url) {
@@ -6021,6 +6871,10 @@ function createFileServer(filePath, fileIndex = 0) {
6021
6871
  if (raw && raw.trim()) {
6022
6872
  payload = JSON.parse(raw);
6023
6873
  }
6874
+ // Save to file-based history (only if there are comments)
6875
+ if (payload && (payload.comments?.length > 0 || payload.submitComment)) {
6876
+ saveHistoryToFile(ctx.filePath, payload);
6877
+ }
6024
6878
  res.writeHead(200, { "Content-Type": "text/plain" });
6025
6879
  res.end("bye");
6026
6880
  // Notify all tabs to close before shutting down
@@ -6283,6 +7137,12 @@ function createDiffServer(diffContent) {
6283
7137
  if (raw && raw.trim()) {
6284
7138
  payload = JSON.parse(raw);
6285
7139
  }
7140
+ // Save to file-based history (only if there are comments)
7141
+ // For diff mode, use relativePath as identifier
7142
+ if (payload && (payload.comments?.length > 0 || payload.submitComment)) {
7143
+ const filePath = ctx.diffData?.relativePath || 'stdin-diff';
7144
+ saveHistoryToFile(filePath, payload);
7145
+ }
6286
7146
  res.writeHead(200, { "Content-Type": "text/plain" });
6287
7147
  res.end("bye");
6288
7148
  // Notify all tabs to close before shutting down
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reviw",
3
- "version": "0.16.1",
3
+ "version": "0.16.3",
4
4
  "description": "Lightweight file reviewer with in-browser comments for CSV, TSV, Markdown, and Git diffs.",
5
5
  "type": "module",
6
6
  "bin": {