reviw 0.10.7 → 0.11.2

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 (3) hide show
  1. package/README.md +56 -0
  2. package/cli.cjs +892 -52
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -20,6 +20,8 @@ A lightweight browser-based tool for reviewing and annotating tabular data, text
20
20
  ### Media Fullscreen
21
21
  - Click images in Markdown preview to open fullscreen viewer
22
22
  - Click videos to open fullscreen playback with native controls
23
+ - Click anywhere (including the image/video itself) to close the fullscreen overlay
24
+ - Clicking media automatically highlights the corresponding source line in the Markdown panel
23
25
 
24
26
  ### UI Features
25
27
  - **Theme toggle**: Switch between light and dark modes
@@ -103,10 +105,64 @@ comments:
103
105
  summary: Overall the data looks good, minor issues noted above.
104
106
  ```
105
107
 
108
+ ## Claude Code Plugin
109
+
110
+ This repository also serves as a Claude Code plugin marketplace. The plugin integrates reviw into Claude Code workflows with task management and review automation.
111
+
112
+ ### Installation
113
+
114
+ ```bash
115
+ # In Claude Code
116
+ /plugin marketplace add kazuph/reviw
117
+ /plugin install reviw-plugin@reviw-marketplace
118
+ ```
119
+
120
+ ### Plugin Features
121
+
122
+ | Component | Name | Description |
123
+ |-----------|------|-------------|
124
+ | **Command** | `/reviw:do` | Start a task - create worktree, plan, register todos |
125
+ | **Command** | `/reviw:done` | Complete checklist - collect evidence, start review with reviw |
126
+ | **Agent** | `report-builder` | Prepare reports and evidence for user review |
127
+ | **Skill** | `artifact-proof` | Collect evidence (screenshots, videos, logs) + reviw review workflow |
128
+ | **Hook** | PreToolUse | Remind to review before git commit/push |
129
+ | **Hook** | Stop | Warn if task is still in progress |
130
+
131
+ ### Workflow
132
+
133
+ ```
134
+ /reviw:do <task description>
135
+
136
+ Create worktree + Plan
137
+
138
+ Implementation
139
+
140
+ /reviw:done
141
+
142
+ Collect evidence + Create report
143
+
144
+ npx reviw opens report (foreground)
145
+
146
+ User comments → Submit & Exit
147
+
148
+ Register feedback to Todo
149
+
150
+ Fix → Re-review until approved
151
+ ```
152
+
153
+ ### Completion Criteria
154
+
155
+ | Stage | Content |
156
+ |-------|---------|
157
+ | 1/3 | Implementation complete |
158
+ | 2/3 | Build, start, verification complete |
159
+ | 3/3 | Review with reviw → User approval |
160
+
106
161
  ## Development
107
162
 
108
163
  - Main source: `cli.cjs`
109
164
  - Tests: `npm test` (vitest + playwright)
165
+ - Plugin: `plugin/` directory
110
166
 
111
167
  ## License
112
168
 
package/cli.cjs CHANGED
@@ -13,6 +13,7 @@
13
13
 
14
14
  const fs = require("fs");
15
15
  const http = require("http");
16
+ const os = require("os");
16
17
  const path = require("path");
17
18
  const { spawn } = require("child_process");
18
19
  const chardet = require("chardet");
@@ -465,7 +466,8 @@ function loadDiff(diffText) {
465
466
  return {
466
467
  rows,
467
468
  files: sortedFiles,
468
- title: "Git Diff",
469
+ projectRoot: "",
470
+ relativePath: "Git Diff",
469
471
  mode: "diff",
470
472
  };
471
473
  }
@@ -574,7 +576,7 @@ function loadCsv(filePath) {
574
576
  return {
575
577
  rows,
576
578
  cols: Math.max(1, maxCols),
577
- title: path.basename(filePath),
579
+ ...formatTitlePaths(filePath),
578
580
  };
579
581
  }
580
582
 
@@ -585,7 +587,7 @@ function loadText(filePath) {
585
587
  return {
586
588
  rows: lines.map((line) => [line]),
587
589
  cols: 1,
588
- title: path.basename(filePath),
590
+ ...formatTitlePaths(filePath),
589
591
  preview: null,
590
592
  };
591
593
  }
@@ -598,6 +600,7 @@ function loadMarkdown(filePath) {
598
600
  // Parse YAML frontmatter
599
601
  let frontmatterHtml = "";
600
602
  let contentStart = 0;
603
+ let reviwQuestions = []; // Extract reviw questions for modal
601
604
 
602
605
  if (lines[0] && lines[0].trim() === "---") {
603
606
  let frontmatterEnd = -1;
@@ -615,30 +618,91 @@ function loadMarkdown(filePath) {
615
618
  try {
616
619
  const frontmatter = yaml.load(frontmatterText);
617
620
  if (frontmatter && typeof frontmatter === "object") {
618
- // Create HTML table for frontmatter
619
- frontmatterHtml = '<div class="frontmatter-table"><table>';
620
- frontmatterHtml += '<colgroup><col style="width:12%"><col style="width:88%"></colgroup>';
621
- frontmatterHtml += '<thead><tr><th colspan="2">Document Metadata</th></tr></thead>';
622
- frontmatterHtml += "<tbody>";
623
-
624
- function renderValue(val) {
625
- if (Array.isArray(val)) {
626
- return val
627
- .map((v) => '<span class="fm-tag">' + escapeHtmlChars(String(v)) + "</span>")
628
- .join(" ");
621
+ // Extract reviw questions if present
622
+ if (frontmatter.reviw && Array.isArray(frontmatter.reviw.questions)) {
623
+ reviwQuestions = frontmatter.reviw.questions.map((q, idx) => ({
624
+ id: q.id || "q" + (idx + 1),
625
+ question: q.question || "",
626
+ resolved: q.resolved === true,
627
+ answer: q.answer || "",
628
+ options: Array.isArray(q.options) ? q.options : [],
629
+ }));
630
+ }
631
+
632
+ // Create HTML table for frontmatter (show reviw questions in detail for 1:1 correspondence with YAML source)
633
+ const displayFrontmatter = { ...frontmatter };
634
+ // Keep reviw as-is for detailed rendering
635
+
636
+ if (Object.keys(displayFrontmatter).length > 0) {
637
+ frontmatterHtml = '<div class="frontmatter-table"><table>';
638
+ frontmatterHtml += '<colgroup><col style="width:12%"><col style="width:88%"></colgroup>';
639
+ frontmatterHtml += '<thead><tr><th colspan="2">Document Metadata</th></tr></thead>';
640
+ frontmatterHtml += "<tbody>";
641
+
642
+ // Render reviw.questions as detailed cards
643
+ function renderReviwQuestions(questions) {
644
+ if (!Array.isArray(questions) || questions.length === 0) {
645
+ return '<span class="fm-tag">質問なし</span>';
646
+ }
647
+ let html = '<div class="reviw-questions-preview">';
648
+ questions.forEach((q, idx) => {
649
+ const statusIcon = q.resolved ? '✅' : '⏳';
650
+ const statusClass = q.resolved ? 'resolved' : 'pending';
651
+ html += '<div class="reviw-q-card ' + statusClass + '">';
652
+ html += '<div class="reviw-q-header">' + statusIcon + ' <strong>' + escapeHtmlChars(q.id || 'Q' + (idx + 1)) + '</strong></div>';
653
+ html += '<div class="reviw-q-question">' + escapeHtmlChars(q.question || '') + '</div>';
654
+ if (q.options && Array.isArray(q.options) && q.options.length > 0) {
655
+ html += '<div class="reviw-q-options">';
656
+ q.options.forEach(opt => {
657
+ html += '<span class="fm-tag">' + escapeHtmlChars(opt) + '</span>';
658
+ });
659
+ html += '</div>';
660
+ }
661
+ if (q.answer) {
662
+ html += '<div class="reviw-q-answer">💬 ' + escapeHtmlChars(q.answer) + '</div>';
663
+ }
664
+ html += '</div>';
665
+ });
666
+ html += '</div>';
667
+ return html;
668
+ }
669
+
670
+ function renderValue(val, key) {
671
+ // Special handling for reviw object
672
+ if (key === 'reviw' && typeof val === 'object' && val !== null) {
673
+ let html = '';
674
+ if (val.questions && Array.isArray(val.questions)) {
675
+ html += renderReviwQuestions(val.questions);
676
+ }
677
+ // Render other reviw properties
678
+ const { questions, ...rest } = val;
679
+ if (Object.keys(rest).length > 0) {
680
+ html += '<div style="margin-top: 8px;">';
681
+ for (const [k, v] of Object.entries(rest)) {
682
+ html += '<div><strong>' + escapeHtmlChars(k) + ':</strong> ' + escapeHtmlChars(String(v)) + '</div>';
683
+ }
684
+ html += '</div>';
685
+ }
686
+ return html || '<span class="fm-tag">-</span>';
687
+ }
688
+ if (Array.isArray(val)) {
689
+ return val
690
+ .map((v) => '<span class="fm-tag">' + escapeHtmlChars(String(v)) + "</span>")
691
+ .join(" ");
692
+ }
693
+ if (typeof val === "object" && val !== null) {
694
+ return "<pre>" + escapeHtmlChars(JSON.stringify(val, null, 2)) + "</pre>";
695
+ }
696
+ return escapeHtmlChars(String(val));
629
697
  }
630
- if (typeof val === "object" && val !== null) {
631
- return "<pre>" + escapeHtmlChars(JSON.stringify(val, null, 2)) + "</pre>";
698
+
699
+ for (const [key, val] of Object.entries(displayFrontmatter)) {
700
+ frontmatterHtml +=
701
+ "<tr><th>" + escapeHtmlChars(key) + "</th><td>" + renderValue(val, key) + "</td></tr>";
632
702
  }
633
- return escapeHtmlChars(String(val));
634
- }
635
703
 
636
- for (const [key, val] of Object.entries(frontmatter)) {
637
- frontmatterHtml +=
638
- "<tr><th>" + escapeHtmlChars(key) + "</th><td>" + renderValue(val) + "</td></tr>";
704
+ frontmatterHtml += "</tbody></table></div>";
639
705
  }
640
-
641
- frontmatterHtml += "</tbody></table></div>";
642
706
  contentStart = frontmatterEnd + 1;
643
707
  }
644
708
  } catch (e) {
@@ -654,8 +718,9 @@ function loadMarkdown(filePath) {
654
718
  return {
655
719
  rows: lines.map((line) => [line]),
656
720
  cols: 1,
657
- title: path.basename(filePath),
721
+ ...formatTitlePaths(filePath),
658
722
  preview,
723
+ reviwQuestions, // Pass questions to UI
659
724
  };
660
725
  }
661
726
 
@@ -667,6 +732,20 @@ function escapeHtmlChars(str) {
667
732
  .replace(/"/g, "&quot;");
668
733
  }
669
734
 
735
+ function formatTitlePaths(filePath) {
736
+ const cwd = process.cwd();
737
+ const home = os.homedir();
738
+ const relativePath = path.relative(cwd, filePath) || path.basename(filePath);
739
+ let projectRoot = cwd;
740
+ if (projectRoot.startsWith(home)) {
741
+ projectRoot = "~" + projectRoot.slice(home.length);
742
+ }
743
+ if (!projectRoot.endsWith("/")) {
744
+ projectRoot += "/";
745
+ }
746
+ return { projectRoot, relativePath };
747
+ }
748
+
670
749
  function loadData(filePath) {
671
750
  // Check if path exists
672
751
  if (!fs.existsSync(filePath)) {
@@ -714,7 +793,7 @@ function serializeForScript(value) {
714
793
  }
715
794
 
716
795
  function diffHtmlTemplate(diffData) {
717
- const { rows, title } = diffData;
796
+ const { rows, projectRoot, relativePath } = diffData;
718
797
  const serialized = serializeForScript(rows);
719
798
  const fileCount = rows.filter((r) => r.type === "file").length;
720
799
 
@@ -726,7 +805,7 @@ function diffHtmlTemplate(diffData) {
726
805
  <meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate" />
727
806
  <meta http-equiv="Pragma" content="no-cache" />
728
807
  <meta http-equiv="Expires" content="0" />
729
- <title>${title} | reviw</title>
808
+ <title>${relativePath} | reviw</title>
730
809
  <style>
731
810
  :root {
732
811
  color-scheme: dark;
@@ -795,7 +874,9 @@ function diffHtmlTemplate(diffData) {
795
874
  }
796
875
  header .meta { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
797
876
  header .actions { display: flex; gap: 8px; align-items: center; }
798
- header h1 { font-size: 16px; margin: 0; font-weight: 600; }
877
+ header h1 { display: flex; flex-direction: column; margin: 0; line-height: 1.3; }
878
+ header h1 .title-path { font-size: 11px; font-weight: 400; color: var(--muted); }
879
+ header h1 .title-file { font-size: 16px; font-weight: 600; }
799
880
  header .badge {
800
881
  background: var(--selected-bg);
801
882
  color: var(--text);
@@ -958,11 +1039,14 @@ function diffHtmlTemplate(diffData) {
958
1039
  display: none;
959
1040
  }
960
1041
  .floating header {
961
- position: relative;
1042
+ position: static;
962
1043
  background: transparent;
1044
+ backdrop-filter: none;
963
1045
  border: none;
964
1046
  padding: 0 0 10px 0;
1047
+ display: flex;
965
1048
  justify-content: space-between;
1049
+ align-items: center;
966
1050
  }
967
1051
  .floating h2 { font-size: 14px; margin: 0; font-weight: 600; }
968
1052
  .floating button {
@@ -987,6 +1071,10 @@ function diffHtmlTemplate(diffData) {
987
1071
  font-size: 13px;
988
1072
  font-family: inherit;
989
1073
  }
1074
+ .floating textarea:focus {
1075
+ outline: none;
1076
+ border-color: var(--accent);
1077
+ }
990
1078
  .floating .actions {
991
1079
  display: flex;
992
1080
  gap: 8px;
@@ -1092,6 +1180,21 @@ function diffHtmlTemplate(diffData) {
1092
1180
  .modal-actions button:hover { background: var(--border); }
1093
1181
  .modal-actions button.primary { background: var(--accent); color: var(--text-inverse); border-color: var(--accent); }
1094
1182
 
1183
+ .modal-checkboxes { margin: 12px 0; }
1184
+ .modal-checkboxes label {
1185
+ display: flex;
1186
+ align-items: flex-start;
1187
+ gap: 8px;
1188
+ font-size: 12px;
1189
+ color: var(--text);
1190
+ margin-bottom: 8px;
1191
+ cursor: pointer;
1192
+ }
1193
+ .modal-checkboxes input[type="checkbox"] {
1194
+ margin-top: 2px;
1195
+ accent-color: var(--accent);
1196
+ }
1197
+
1095
1198
  .no-diff {
1096
1199
  text-align: center;
1097
1200
  padding: 60px 20px;
@@ -1104,7 +1207,7 @@ function diffHtmlTemplate(diffData) {
1104
1207
  <body>
1105
1208
  <header>
1106
1209
  <div class="meta">
1107
- <h1>${title}</h1>
1210
+ <h1>${projectRoot ? `<span class="title-path">${projectRoot}</span>` : ""}<span class="title-file">${relativePath}</span></h1>
1108
1211
  <span class="badge">${fileCount} file${fileCount !== 1 ? "s" : ""} changed</span>
1109
1212
  <span class="pill">Comments <strong id="comment-count">0</strong></span>
1110
1213
  </div>
@@ -1146,6 +1249,11 @@ function diffHtmlTemplate(diffData) {
1146
1249
  <p class="modal-summary" id="modal-summary"></p>
1147
1250
  <label for="global-comment">Overall comment (optional)</label>
1148
1251
  <textarea id="global-comment" placeholder="Add a summary or overall feedback..."></textarea>
1252
+ <div class="modal-checkboxes">
1253
+ <label><input type="checkbox" id="prompt-subagents" checked /> All implementation, verification, and report creation will be done by the sub-agents.</label>
1254
+ <label><input type="checkbox" id="prompt-reviw" checked /> Open in REVIW next time.</label>
1255
+ <label><input type="checkbox" id="prompt-screenshots" checked /> Update all screenshots and videos.</label>
1256
+ </div>
1149
1257
  <div class="modal-actions">
1150
1258
  <button id="modal-cancel">Cancel</button>
1151
1259
  <button class="primary" id="modal-submit">Submit</button>
@@ -1514,9 +1622,61 @@ function diffHtmlTemplate(diffData) {
1514
1622
  const modalSummary = document.getElementById('modal-summary');
1515
1623
  const globalCommentInput = document.getElementById('global-comment');
1516
1624
 
1625
+ // Prompt checkboxes
1626
+ const promptCheckboxes = [
1627
+ { id: 'prompt-subagents', text: 'All implementation, verification, and report creation will be done by the sub-agents.' },
1628
+ { id: 'prompt-reviw', text: 'Open in REVIW next time.' },
1629
+ { id: 'prompt-screenshots', text: 'Update all screenshots and videos.' }
1630
+ ];
1631
+ const PROMPT_STORAGE_KEY = 'reviw-prompt-prefs';
1632
+
1633
+ // Load saved preferences
1634
+ function loadPromptPrefs() {
1635
+ try {
1636
+ const saved = localStorage.getItem(PROMPT_STORAGE_KEY);
1637
+ if (saved) {
1638
+ const prefs = JSON.parse(saved);
1639
+ promptCheckboxes.forEach(p => {
1640
+ const el = document.getElementById(p.id);
1641
+ if (el && typeof prefs[p.id] === 'boolean') el.checked = prefs[p.id];
1642
+ });
1643
+ }
1644
+ } catch (e) {}
1645
+ }
1646
+
1647
+ // Save preferences
1648
+ function savePromptPrefs() {
1649
+ try {
1650
+ const prefs = {};
1651
+ promptCheckboxes.forEach(p => {
1652
+ const el = document.getElementById(p.id);
1653
+ if (el) prefs[p.id] = el.checked;
1654
+ });
1655
+ localStorage.setItem(PROMPT_STORAGE_KEY, JSON.stringify(prefs));
1656
+ } catch (e) {}
1657
+ }
1658
+
1659
+ // Initialize checkbox listeners
1660
+ promptCheckboxes.forEach(p => {
1661
+ const el = document.getElementById(p.id);
1662
+ if (el) el.addEventListener('change', savePromptPrefs);
1663
+ });
1664
+ loadPromptPrefs();
1665
+
1666
+ function getSelectedPrompts() {
1667
+ const prompts = [];
1668
+ promptCheckboxes.forEach(p => {
1669
+ const el = document.getElementById(p.id);
1670
+ if (el && el.checked) prompts.push(p.text);
1671
+ });
1672
+ return prompts;
1673
+ }
1674
+
1517
1675
  function payload(reason) {
1518
1676
  const data = { file: FILE_NAME, mode: MODE, reason, at: new Date().toISOString(), comments: Object.values(comments) };
1519
1677
  if (globalComment.trim()) data.summary = globalComment.trim();
1678
+ const prompts = getSelectedPrompts();
1679
+ if (prompts.length > 0) data.prompts = prompts;
1520
1680
  return data;
1521
1681
  }
1522
1682
  function sendAndExit(reason = 'button') {
@@ -1537,6 +1697,7 @@ function diffHtmlTemplate(diffData) {
1537
1697
  document.getElementById('modal-cancel').addEventListener('click', hideSubmitModal);
1538
1698
  function doSubmit() {
1539
1699
  globalComment = globalCommentInput.value;
1700
+ savePromptPrefs();
1540
1701
  hideSubmitModal();
1541
1702
  sendAndExit('button');
1542
1703
  // Try to close window; if it fails (browser security), show completion message
@@ -1594,10 +1755,11 @@ function diffHtmlTemplate(diffData) {
1594
1755
  }
1595
1756
 
1596
1757
  // --- HTML template ---------------------------------------------------------
1597
- function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1758
+ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHtml, reviwQuestions = []) {
1598
1759
  const serialized = serializeForScript(dataRows);
1599
1760
  const modeJson = serializeForScript(mode);
1600
- const titleJson = serializeForScript(title);
1761
+ const titleJson = serializeForScript(relativePath); // Use relativePath as file identifier
1762
+ const questionsJson = serializeForScript(reviwQuestions || []);
1601
1763
  const hasPreview = !!previewHtml;
1602
1764
  return `<!doctype html>
1603
1765
  <html lang="ja">
@@ -1607,7 +1769,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1607
1769
  <meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate" />
1608
1770
  <meta http-equiv="Pragma" content="no-cache" />
1609
1771
  <meta http-equiv="Expires" content="0" />
1610
- <title>${title} | reviw</title>
1772
+ <title>${relativePath} | reviw</title>
1611
1773
  <style>
1612
1774
  /* Dark theme (default) */
1613
1775
  :root {
@@ -1688,7 +1850,9 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1688
1850
  }
1689
1851
  header .meta { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
1690
1852
  header .actions { display: flex; gap: 8px; align-items: center; }
1691
- header h1 { font-size: 16px; margin: 0; font-weight: 700; }
1853
+ header h1 { display: flex; flex-direction: column; margin: 0; line-height: 1.3; }
1854
+ header h1 .title-path { font-size: 11px; font-weight: 400; color: var(--muted); }
1855
+ header h1 .title-file { font-size: 16px; font-weight: 700; }
1692
1856
  header .badge {
1693
1857
  background: var(--selected-bg);
1694
1858
  color: var(--text);
@@ -1909,12 +2073,14 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1909
2073
  transition: background 200ms ease, border-color 200ms ease;
1910
2074
  }
1911
2075
  .floating header {
1912
- position: relative;
1913
- top: 0;
2076
+ position: static;
1914
2077
  background: transparent;
2078
+ backdrop-filter: none;
1915
2079
  border: none;
1916
2080
  padding: 0 0 8px 0;
2081
+ display: flex;
1917
2082
  justify-content: space-between;
2083
+ align-items: center;
1918
2084
  }
1919
2085
  .floating h2 { font-size: 14px; margin: 0; color: var(--text); }
1920
2086
  .floating button {
@@ -1943,6 +2109,10 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1943
2109
  line-height: 1.4;
1944
2110
  transition: background 200ms ease, border-color 200ms ease;
1945
2111
  }
2112
+ .floating textarea:focus {
2113
+ outline: none;
2114
+ border-color: var(--accent);
2115
+ }
1946
2116
  .floating .actions {
1947
2117
  display: flex;
1948
2118
  gap: 8px;
@@ -2120,6 +2290,50 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2120
2290
  border-radius: 4px;
2121
2291
  font-size: 11px;
2122
2292
  }
2293
+ /* Reviw questions preview cards */
2294
+ .reviw-questions-preview {
2295
+ display: flex;
2296
+ flex-direction: column;
2297
+ gap: 8px;
2298
+ }
2299
+ .reviw-q-card {
2300
+ background: var(--code-bg);
2301
+ border: 1px solid var(--border);
2302
+ border-radius: 8px;
2303
+ padding: 10px 12px;
2304
+ }
2305
+ .reviw-q-card.resolved {
2306
+ border-left: 3px solid #22c55e;
2307
+ }
2308
+ .reviw-q-card.pending {
2309
+ border-left: 3px solid #f59e0b;
2310
+ }
2311
+ .reviw-q-header {
2312
+ font-size: 12px;
2313
+ color: var(--text-dim);
2314
+ margin-bottom: 4px;
2315
+ }
2316
+ .reviw-q-header strong {
2317
+ color: var(--accent);
2318
+ }
2319
+ .reviw-q-question {
2320
+ font-size: 13px;
2321
+ color: var(--text);
2322
+ margin-bottom: 6px;
2323
+ }
2324
+ .reviw-q-options {
2325
+ display: flex;
2326
+ flex-wrap: wrap;
2327
+ gap: 4px;
2328
+ margin-bottom: 6px;
2329
+ }
2330
+ .reviw-q-answer {
2331
+ font-size: 12px;
2332
+ color: #22c55e;
2333
+ background: rgba(34, 197, 94, 0.1);
2334
+ padding: 4px 8px;
2335
+ border-radius: 4px;
2336
+ }
2123
2337
  [data-theme="light"] .frontmatter-table tbody th {
2124
2338
  color: #7c3aed;
2125
2339
  }
@@ -2267,6 +2481,267 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2267
2481
  border-radius: 8px;
2268
2482
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
2269
2483
  }
2484
+ /* Reviw Questions Modal */
2485
+ .reviw-questions-overlay {
2486
+ display: none;
2487
+ position: fixed;
2488
+ inset: 0;
2489
+ background: rgba(0, 0, 0, 0.8);
2490
+ z-index: 1100;
2491
+ justify-content: center;
2492
+ align-items: center;
2493
+ }
2494
+ .reviw-questions-overlay.visible {
2495
+ display: flex;
2496
+ }
2497
+ .reviw-questions-modal {
2498
+ background: var(--card-bg);
2499
+ border: 1px solid var(--border);
2500
+ border-radius: 16px;
2501
+ width: 90%;
2502
+ max-width: 600px;
2503
+ max-height: 80vh;
2504
+ display: flex;
2505
+ flex-direction: column;
2506
+ box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
2507
+ }
2508
+ .reviw-questions-header {
2509
+ display: flex;
2510
+ justify-content: space-between;
2511
+ align-items: center;
2512
+ padding: 16px 20px;
2513
+ border-bottom: 1px solid var(--border);
2514
+ }
2515
+ .reviw-questions-header h2 {
2516
+ margin: 0;
2517
+ font-size: 16px;
2518
+ font-weight: 600;
2519
+ color: var(--text);
2520
+ }
2521
+ .reviw-questions-header h2 span {
2522
+ font-size: 14px;
2523
+ color: var(--text-dim);
2524
+ font-weight: 400;
2525
+ }
2526
+ .reviw-questions-close {
2527
+ width: 32px;
2528
+ height: 32px;
2529
+ border: none;
2530
+ background: transparent;
2531
+ color: var(--text-dim);
2532
+ font-size: 18px;
2533
+ cursor: pointer;
2534
+ border-radius: 8px;
2535
+ transition: all 150ms ease;
2536
+ }
2537
+ .reviw-questions-close:hover {
2538
+ background: var(--border);
2539
+ color: var(--text);
2540
+ }
2541
+ .reviw-questions-body {
2542
+ flex: 1;
2543
+ overflow-y: auto;
2544
+ padding: 16px 20px;
2545
+ }
2546
+ .reviw-questions-footer {
2547
+ padding: 12px 20px;
2548
+ border-top: 1px solid var(--border);
2549
+ display: flex;
2550
+ justify-content: flex-end;
2551
+ }
2552
+ .reviw-questions-later {
2553
+ padding: 8px 16px;
2554
+ border: 1px solid var(--border);
2555
+ background: transparent;
2556
+ color: var(--text-dim);
2557
+ border-radius: 8px;
2558
+ cursor: pointer;
2559
+ font-size: 13px;
2560
+ transition: all 150ms ease;
2561
+ }
2562
+ .reviw-questions-later:hover {
2563
+ background: var(--border);
2564
+ color: var(--text);
2565
+ }
2566
+ /* Question Item */
2567
+ .reviw-question-item {
2568
+ margin-bottom: 20px;
2569
+ padding-bottom: 20px;
2570
+ border-bottom: 1px solid var(--border);
2571
+ }
2572
+ .reviw-question-item:last-child {
2573
+ margin-bottom: 0;
2574
+ padding-bottom: 0;
2575
+ border-bottom: none;
2576
+ }
2577
+ .reviw-question-text {
2578
+ font-size: 14px;
2579
+ color: var(--text);
2580
+ margin-bottom: 12px;
2581
+ line-height: 1.5;
2582
+ }
2583
+ .reviw-question-options {
2584
+ display: flex;
2585
+ flex-wrap: wrap;
2586
+ gap: 8px;
2587
+ margin-bottom: 12px;
2588
+ }
2589
+ .reviw-question-option {
2590
+ padding: 8px 14px;
2591
+ border: 1px solid var(--border);
2592
+ background: transparent;
2593
+ color: var(--text);
2594
+ border-radius: 8px;
2595
+ cursor: pointer;
2596
+ font-size: 13px;
2597
+ transition: all 150ms ease;
2598
+ }
2599
+ .reviw-question-option:hover {
2600
+ border-color: var(--accent);
2601
+ background: rgba(96, 165, 250, 0.1);
2602
+ }
2603
+ .reviw-question-option.selected {
2604
+ border-color: var(--accent);
2605
+ background: var(--accent);
2606
+ color: var(--text-inverse);
2607
+ }
2608
+ .reviw-question-input {
2609
+ width: 100%;
2610
+ padding: 10px 12px;
2611
+ border: 1px solid var(--border);
2612
+ background: var(--input-bg);
2613
+ color: var(--text);
2614
+ border-radius: 8px;
2615
+ font-size: 13px;
2616
+ resize: vertical;
2617
+ min-height: 60px;
2618
+ }
2619
+ .reviw-question-input:focus {
2620
+ outline: none;
2621
+ border-color: var(--accent);
2622
+ }
2623
+ .reviw-question-input::placeholder {
2624
+ color: var(--text-dim);
2625
+ }
2626
+ .reviw-check-mark {
2627
+ color: #22c55e;
2628
+ font-weight: bold;
2629
+ }
2630
+ .reviw-question-item.answered {
2631
+ border-color: #22c55e;
2632
+ background: rgba(34, 197, 94, 0.05);
2633
+ }
2634
+ .reviw-question-submit {
2635
+ margin-top: 10px;
2636
+ padding: 8px 16px;
2637
+ border: none;
2638
+ background: var(--accent);
2639
+ color: var(--text-inverse);
2640
+ border-radius: 8px;
2641
+ cursor: pointer;
2642
+ font-size: 13px;
2643
+ font-weight: 500;
2644
+ transition: all 150ms ease;
2645
+ }
2646
+ .reviw-question-submit:hover {
2647
+ filter: brightness(1.1);
2648
+ }
2649
+ .reviw-question-submit:disabled {
2650
+ opacity: 0.5;
2651
+ cursor: not-allowed;
2652
+ }
2653
+ /* Resolved Section */
2654
+ .reviw-resolved-section {
2655
+ margin-top: 16px;
2656
+ border-top: 1px solid var(--border);
2657
+ padding-top: 12px;
2658
+ }
2659
+ .reviw-resolved-toggle {
2660
+ display: flex;
2661
+ align-items: center;
2662
+ gap: 8px;
2663
+ background: none;
2664
+ border: none;
2665
+ color: var(--text-dim);
2666
+ font-size: 13px;
2667
+ cursor: pointer;
2668
+ padding: 4px 0;
2669
+ }
2670
+ .reviw-resolved-toggle:hover {
2671
+ color: var(--text);
2672
+ }
2673
+ .reviw-resolved-toggle .arrow {
2674
+ transition: transform 150ms ease;
2675
+ }
2676
+ .reviw-resolved-toggle.open .arrow {
2677
+ transform: rotate(90deg);
2678
+ }
2679
+ .reviw-resolved-list {
2680
+ display: none;
2681
+ margin-top: 12px;
2682
+ }
2683
+ .reviw-resolved-list.visible {
2684
+ display: block;
2685
+ }
2686
+ .reviw-resolved-item {
2687
+ padding: 10px 12px;
2688
+ background: var(--input-bg);
2689
+ border-radius: 8px;
2690
+ margin-bottom: 8px;
2691
+ opacity: 0.7;
2692
+ }
2693
+ .reviw-resolved-item:last-child {
2694
+ margin-bottom: 0;
2695
+ }
2696
+ .reviw-resolved-q {
2697
+ font-size: 12px;
2698
+ color: var(--text-dim);
2699
+ margin-bottom: 4px;
2700
+ }
2701
+ .reviw-resolved-a {
2702
+ font-size: 13px;
2703
+ color: var(--text);
2704
+ }
2705
+ /* Notice Bar */
2706
+ .reviw-questions-bar {
2707
+ display: none;
2708
+ position: fixed;
2709
+ top: 0;
2710
+ left: 0;
2711
+ right: 0;
2712
+ background: var(--accent);
2713
+ color: var(--text-inverse);
2714
+ padding: 8px 16px;
2715
+ font-size: 13px;
2716
+ z-index: 1050;
2717
+ justify-content: center;
2718
+ align-items: center;
2719
+ gap: 12px;
2720
+ }
2721
+ .reviw-questions-bar.visible {
2722
+ display: flex;
2723
+ }
2724
+ .reviw-questions-bar button {
2725
+ padding: 4px 12px;
2726
+ border: 1px solid rgba(255, 255, 255, 0.3);
2727
+ background: rgba(255, 255, 255, 0.1);
2728
+ color: var(--text-inverse);
2729
+ border-radius: 6px;
2730
+ cursor: pointer;
2731
+ font-size: 12px;
2732
+ transition: all 150ms ease;
2733
+ }
2734
+ .reviw-questions-bar button:hover {
2735
+ background: rgba(255, 255, 255, 0.2);
2736
+ }
2737
+ /* Adjust layout when bar is visible */
2738
+ body.has-questions-bar header {
2739
+ top: 36px;
2740
+ }
2741
+ body.has-questions-bar .toolbar,
2742
+ body.has-questions-bar .table-wrap {
2743
+ margin-top: 36px;
2744
+ }
2270
2745
  /* Copy notification toast */
2271
2746
  .copy-toast {
2272
2747
  position: fixed;
@@ -2379,6 +2854,22 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2379
2854
  .modal-actions button:hover { background: var(--hover-bg); }
2380
2855
  .modal-actions button.primary { background: var(--accent); color: var(--text-inverse); border-color: var(--accent); }
2381
2856
  .modal-actions button.primary:hover { background: #7dd3fc; }
2857
+
2858
+ .modal-checkboxes { margin: 12px 0; }
2859
+ .modal-checkboxes label {
2860
+ display: flex;
2861
+ align-items: flex-start;
2862
+ gap: 8px;
2863
+ font-size: 12px;
2864
+ color: var(--text);
2865
+ margin-bottom: 8px;
2866
+ cursor: pointer;
2867
+ }
2868
+ .modal-checkboxes input[type="checkbox"] {
2869
+ margin-top: 2px;
2870
+ accent-color: var(--accent);
2871
+ }
2872
+
2382
2873
  body.dragging { user-select: none; cursor: crosshair; }
2383
2874
  body.dragging .diff-line { cursor: crosshair; }
2384
2875
  @media (max-width: 840px) {
@@ -2529,7 +3020,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2529
3020
  <body>
2530
3021
  <header>
2531
3022
  <div class="meta">
2532
- <h1>${title}</h1>
3023
+ <h1><span class="title-path">${projectRoot}</span><span class="title-file">${relativePath}</span></h1>
2533
3024
  <span class="badge">Click to comment / ESC to cancel</span>
2534
3025
  <span class="pill">Comments <strong id="comment-count">0</strong></span>
2535
3026
  </div>
@@ -2645,6 +3136,11 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2645
3136
  <p class="modal-summary" id="modal-summary"></p>
2646
3137
  <label for="global-comment">Overall comment (optional)</label>
2647
3138
  <textarea id="global-comment" placeholder="Add a summary or overall feedback..."></textarea>
3139
+ <div class="modal-checkboxes">
3140
+ <label><input type="checkbox" id="prompt-subagents" checked /> All implementation, verification, and report creation will be done by the sub-agents.</label>
3141
+ <label><input type="checkbox" id="prompt-reviw" checked /> Open in REVIW next time.</label>
3142
+ <label><input type="checkbox" id="prompt-screenshots" checked /> Update all screenshots and videos.</label>
3143
+ </div>
2648
3144
  <div class="modal-actions">
2649
3145
  <button id="modal-cancel">Cancel</button>
2650
3146
  <button class="primary" id="modal-submit">Submit</button>
@@ -2682,11 +3178,32 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2682
3178
  <div class="video-container" id="video-container"></div>
2683
3179
  </div>
2684
3180
 
3181
+ <!-- Reviw Questions Modal -->
3182
+ <div class="reviw-questions-overlay" id="reviw-questions-overlay">
3183
+ <div class="reviw-questions-modal" id="reviw-questions-modal">
3184
+ <div class="reviw-questions-header">
3185
+ <h2>📋 AIからの質問 <span id="reviw-questions-count"></span></h2>
3186
+ <button class="reviw-questions-close" id="reviw-questions-close" aria-label="Close">✕</button>
3187
+ </div>
3188
+ <div class="reviw-questions-body" id="reviw-questions-body"></div>
3189
+ <div class="reviw-questions-footer">
3190
+ <button class="reviw-questions-later" id="reviw-questions-later">後で回答する</button>
3191
+ </div>
3192
+ </div>
3193
+ </div>
3194
+
3195
+ <!-- Reviw Questions Notice Bar -->
3196
+ <div class="reviw-questions-bar" id="reviw-questions-bar">
3197
+ <span id="reviw-questions-bar-message">\ud83d\udccb \u672a\u56de\u7b54\u306e\u8cea\u554f\u304c<span id="reviw-questions-bar-count">0</span>\u4ef6\u3042\u308a\u307e\u3059</span>
3198
+ <button id="reviw-questions-bar-open">\u8cea\u554f\u3092\u898b\u308b</button>
3199
+ </div>
3200
+
2685
3201
  <script>
2686
3202
  const DATA = ${serialized};
2687
3203
  const MAX_COLS = ${cols};
2688
3204
  const FILE_NAME = ${titleJson};
2689
3205
  const MODE = ${modeJson};
3206
+ const REVIW_QUESTIONS = ${questionsJson};
2690
3207
 
2691
3208
  // --- Theme Management ---
2692
3209
  (function initTheme() {
@@ -3045,6 +3562,12 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3045
3562
 
3046
3563
  function openCardForSelection() {
3047
3564
  if (!selection) return;
3565
+ // Don't open card while image/video modal is visible
3566
+ const imageOverlay = document.getElementById('image-fullscreen');
3567
+ const videoOverlay = document.getElementById('video-fullscreen');
3568
+ if (imageOverlay?.classList.contains('visible') || videoOverlay?.classList.contains('visible')) {
3569
+ return;
3570
+ }
3048
3571
  const { startRow, endRow, startCol, endCol } = selection;
3049
3572
  const isSingleCell = startRow === endRow && startCol === endCol;
3050
3573
 
@@ -3550,6 +4073,56 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3550
4073
  const modalCancel = document.getElementById('modal-cancel');
3551
4074
  const modalSubmit = document.getElementById('modal-submit');
3552
4075
 
4076
+ // Prompt checkboxes
4077
+ const promptCheckboxes = [
4078
+ { id: 'prompt-subagents', text: 'All implementation, verification, and report creation will be done by the sub-agents.' },
4079
+ { id: 'prompt-reviw', text: 'Open in REVIW next time.' },
4080
+ { id: 'prompt-screenshots', text: 'Update all screenshots and videos.' }
4081
+ ];
4082
+ const PROMPT_STORAGE_KEY = 'reviw-prompt-prefs';
4083
+
4084
+ // Load saved preferences
4085
+ function loadPromptPrefs() {
4086
+ try {
4087
+ const saved = localStorage.getItem(PROMPT_STORAGE_KEY);
4088
+ if (saved) {
4089
+ const prefs = JSON.parse(saved);
4090
+ promptCheckboxes.forEach(p => {
4091
+ const el = document.getElementById(p.id);
4092
+ if (el && typeof prefs[p.id] === 'boolean') el.checked = prefs[p.id];
4093
+ });
4094
+ }
4095
+ } catch (e) {}
4096
+ }
4097
+
4098
+ // Save preferences
4099
+ function savePromptPrefs() {
4100
+ try {
4101
+ const prefs = {};
4102
+ promptCheckboxes.forEach(p => {
4103
+ const el = document.getElementById(p.id);
4104
+ if (el) prefs[p.id] = el.checked;
4105
+ });
4106
+ localStorage.setItem(PROMPT_STORAGE_KEY, JSON.stringify(prefs));
4107
+ } catch (e) {}
4108
+ }
4109
+
4110
+ // Initialize checkbox listeners
4111
+ promptCheckboxes.forEach(p => {
4112
+ const el = document.getElementById(p.id);
4113
+ if (el) el.addEventListener('change', savePromptPrefs);
4114
+ });
4115
+ loadPromptPrefs();
4116
+
4117
+ function getSelectedPrompts() {
4118
+ const prompts = [];
4119
+ promptCheckboxes.forEach(p => {
4120
+ const el = document.getElementById(p.id);
4121
+ if (el && el.checked) prompts.push(p.text);
4122
+ });
4123
+ return prompts;
4124
+ }
4125
+
3553
4126
  function payload(reason) {
3554
4127
  const data = {
3555
4128
  file: FILE_NAME,
@@ -3561,6 +4134,24 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3561
4134
  if (globalComment.trim()) {
3562
4135
  data.summary = globalComment.trim();
3563
4136
  }
4137
+ const prompts = getSelectedPrompts();
4138
+ if (prompts.length > 0) data.prompts = prompts;
4139
+ // Include answered questions
4140
+ if (window.REVIW_ANSWERS) {
4141
+ const answeredQuestions = [];
4142
+ for (const [id, answer] of Object.entries(window.REVIW_ANSWERS)) {
4143
+ if (answer.selected || answer.text.trim()) {
4144
+ answeredQuestions.push({
4145
+ id,
4146
+ selected: answer.selected,
4147
+ text: answer.text.trim()
4148
+ });
4149
+ }
4150
+ }
4151
+ if (answeredQuestions.length > 0) {
4152
+ data.reviwAnswers = answeredQuestions;
4153
+ }
4154
+ }
3564
4155
  return data;
3565
4156
  }
3566
4157
  function sendAndExit(reason = 'pagehide') {
@@ -3586,6 +4177,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3586
4177
  modalCancel.addEventListener('click', hideSubmitModal);
3587
4178
  function doSubmit() {
3588
4179
  globalComment = globalCommentInput.value;
4180
+ savePromptPrefs();
3589
4181
  hideSubmitModal();
3590
4182
  sendAndExit('button');
3591
4183
  // Try to close window; if it fails (browser security), show completion message
@@ -4042,8 +4634,9 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
4042
4634
  }
4043
4635
 
4044
4636
  if (imageOverlay) {
4637
+ // Close on any click (including image itself)
4045
4638
  imageOverlay.addEventListener('click', (e) => {
4046
- if (e.target === imageOverlay) closeImageOverlay();
4639
+ closeImageOverlay();
4047
4640
  });
4048
4641
  }
4049
4642
 
@@ -4058,7 +4651,8 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
4058
4651
  img.title = 'Click to view fullscreen';
4059
4652
 
4060
4653
  img.addEventListener('click', (e) => {
4061
- e.stopPropagation();
4654
+ // Don't stop propagation - allow select to work
4655
+ e.preventDefault();
4062
4656
 
4063
4657
  imageContainer.innerHTML = '';
4064
4658
  const clonedImg = img.cloneNode(true);
@@ -4103,8 +4697,9 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
4103
4697
  }
4104
4698
 
4105
4699
  if (videoOverlay) {
4700
+ // Close on any click (including video itself)
4106
4701
  videoOverlay.addEventListener('click', (e) => {
4107
- if (e.target === videoOverlay) closeVideoOverlay();
4702
+ closeVideoOverlay();
4108
4703
  });
4109
4704
  }
4110
4705
 
@@ -4123,7 +4718,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
4123
4718
 
4124
4719
  link.addEventListener('click', (e) => {
4125
4720
  e.preventDefault();
4126
- e.stopPropagation();
4721
+ // Don't stop propagation - allow select to work
4127
4722
 
4128
4723
  // Remove existing video if any
4129
4724
  const existingVideo = videoContainer.querySelector('video');
@@ -4347,15 +4942,11 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
4347
4942
 
4348
4943
  // Click on block elements
4349
4944
  preview.addEventListener('click', (e) => {
4350
- // Handle image clicks
4945
+ // Handle image clicks - always select, even if modal is showing
4351
4946
  if (e.target.tagName === 'IMG') {
4352
- if (!e.defaultPrevented) {
4353
- const line = findImageSourceLine(e.target.src);
4354
- if (line > 0) {
4355
- e.preventDefault();
4356
- e.stopPropagation();
4357
- selectSourceRange(line);
4358
- }
4947
+ const line = findImageSourceLine(e.target.src);
4948
+ if (line > 0) {
4949
+ selectSourceRange(line);
4359
4950
  }
4360
4951
  return;
4361
4952
  }
@@ -4425,6 +5016,227 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
4425
5016
  }, 10);
4426
5017
  });
4427
5018
  })();
5019
+
5020
+ // --- Reviw Questions Modal ---
5021
+ (function initReviwQuestions() {
5022
+ if (MODE !== 'markdown') return;
5023
+ if (!REVIW_QUESTIONS || REVIW_QUESTIONS.length === 0) return;
5024
+
5025
+ const overlay = document.getElementById('reviw-questions-overlay');
5026
+ const modal = document.getElementById('reviw-questions-modal');
5027
+ const body = document.getElementById('reviw-questions-body');
5028
+ const closeBtn = document.getElementById('reviw-questions-close');
5029
+ const laterBtn = document.getElementById('reviw-questions-later');
5030
+ const countSpan = document.getElementById('reviw-questions-count');
5031
+ const bar = document.getElementById('reviw-questions-bar');
5032
+ const barMessage = document.getElementById('reviw-questions-bar-message');
5033
+ const barCount = document.getElementById('reviw-questions-bar-count');
5034
+ const barOpenBtn = document.getElementById('reviw-questions-bar-open');
5035
+
5036
+ if (!overlay || !modal || !body) return;
5037
+
5038
+ // Store answers locally
5039
+ const answers = {};
5040
+ REVIW_QUESTIONS.forEach(q => {
5041
+ answers[q.id] = { selected: '', text: '' };
5042
+ });
5043
+
5044
+ // Count unresolved questions
5045
+ const unresolvedQuestions = REVIW_QUESTIONS.filter(q => !q.resolved);
5046
+ const resolvedQuestions = REVIW_QUESTIONS.filter(q => q.resolved);
5047
+
5048
+ function getUnansweredCount() {
5049
+ // Count questions that have no answer (no selection and no text)
5050
+ return unresolvedQuestions.filter(q => {
5051
+ const a = answers[q.id];
5052
+ return !a.selected && !a.text.trim();
5053
+ }).length;
5054
+ }
5055
+
5056
+ function updateCounts() {
5057
+ const unansweredCount = getUnansweredCount();
5058
+ if (unansweredCount > 0) {
5059
+ countSpan.textContent = '(' + unansweredCount + '\u4ef6\u672a\u56de\u7b54)';
5060
+ barMessage.innerHTML = '\ud83d\udccb \u672a\u56de\u7b54\u306e\u8cea\u554f\u304c<span id="reviw-questions-bar-count">' + unansweredCount + '</span>\u4ef6\u3042\u308a\u307e\u3059';
5061
+ laterBtn.textContent = '\u5f8c\u3067\u56de\u7b54\u3059\u308b';
5062
+ } else {
5063
+ countSpan.textContent = '(\u5168\u3066\u56de\u7b54\u6e08\u307f)';
5064
+ barMessage.innerHTML = '\u2705 \u5168\u3066\u306e\u8cea\u554f\u306b\u56de\u7b54\u3057\u307e\u3057\u305f';
5065
+ laterBtn.textContent = '\u9589\u3058\u308b';
5066
+ }
5067
+ }
5068
+
5069
+ function checkAllAnswered() {
5070
+ if (getUnansweredCount() === 0) {
5071
+ // All answered - close modal but keep bar visible
5072
+ setTimeout(() => {
5073
+ overlay.classList.remove('visible');
5074
+ // Keep bar visible with different message
5075
+ bar.classList.add('visible');
5076
+ document.body.classList.add('has-questions-bar');
5077
+ }, 500);
5078
+ }
5079
+ }
5080
+
5081
+ function renderQuestions() {
5082
+ body.innerHTML = '';
5083
+
5084
+ // Render unresolved questions
5085
+ unresolvedQuestions.forEach((q, idx) => {
5086
+ const item = document.createElement('div');
5087
+ item.className = 'reviw-question-item';
5088
+ item.dataset.id = q.id;
5089
+
5090
+ let optionsHtml = '';
5091
+ if (q.options && q.options.length > 0) {
5092
+ optionsHtml = '<div class="reviw-question-options">' +
5093
+ q.options.map(opt =>
5094
+ '<button class="reviw-question-option" data-value="' + escapeAttr(opt) + '">' + escapeHtml(opt) + '</button>'
5095
+ ).join('') +
5096
+ '</div>';
5097
+ }
5098
+
5099
+ const isOkOnly = q.options && q.options.length === 1 && q.options[0] === 'OK';
5100
+
5101
+ item.innerHTML =
5102
+ '<div class="reviw-question-text">Q' + (idx + 1) + '. ' + escapeHtml(q.question) + '<span class="reviw-check-mark"></span></div>' +
5103
+ optionsHtml +
5104
+ (isOkOnly ? '' : '<textarea class="reviw-question-input" placeholder="\u30c6\u30ad\u30b9\u30c8\u3067\u56de\u7b54\u30fb\u88dc\u8db3..."></textarea>');
5105
+
5106
+ body.appendChild(item);
5107
+
5108
+ // Check mark element
5109
+ const checkMark = item.querySelector('.reviw-check-mark');
5110
+
5111
+ function updateCheckMark() {
5112
+ const answer = answers[q.id];
5113
+ const hasAnswer = answer.selected || answer.text.trim();
5114
+ if (hasAnswer) {
5115
+ checkMark.textContent = ' \u2713';
5116
+ item.classList.add('answered');
5117
+ } else {
5118
+ checkMark.textContent = '';
5119
+ item.classList.remove('answered');
5120
+ }
5121
+ }
5122
+
5123
+ // Option click handlers - always toggle
5124
+ const optionBtns = item.querySelectorAll('.reviw-question-option');
5125
+ optionBtns.forEach(btn => {
5126
+ btn.addEventListener('click', () => {
5127
+ const wasSelected = btn.classList.contains('selected');
5128
+ optionBtns.forEach(b => b.classList.remove('selected'));
5129
+ if (!wasSelected) {
5130
+ btn.classList.add('selected');
5131
+ answers[q.id].selected = btn.dataset.value;
5132
+ } else {
5133
+ answers[q.id].selected = '';
5134
+ }
5135
+ updateCheckMark();
5136
+ updateCounts();
5137
+ checkAllAnswered();
5138
+ });
5139
+ });
5140
+
5141
+ // Text input handler
5142
+ const textarea = item.querySelector('.reviw-question-input');
5143
+ if (textarea) {
5144
+ textarea.addEventListener('input', () => {
5145
+ answers[q.id].text = textarea.value;
5146
+ updateCheckMark();
5147
+ updateCounts();
5148
+ checkAllAnswered();
5149
+ });
5150
+ }
5151
+
5152
+ updateCheckMark();
5153
+ });
5154
+
5155
+ // Render resolved questions (collapsed)
5156
+ if (resolvedQuestions.length > 0) {
5157
+ const section = document.createElement('div');
5158
+ section.className = 'reviw-resolved-section';
5159
+ section.innerHTML =
5160
+ '<button class="reviw-resolved-toggle">' +
5161
+ '<span class="arrow">\u25b6</span> \u89e3\u6c7a\u6e08\u307f (' + resolvedQuestions.length + '\u4ef6)' +
5162
+ '</button>' +
5163
+ '<div class="reviw-resolved-list">' +
5164
+ resolvedQuestions.map(q =>
5165
+ '<div class="reviw-resolved-item">' +
5166
+ '<div class="reviw-resolved-q">' + escapeHtml(q.question) + '</div>' +
5167
+ '<div class="reviw-resolved-a">\u2192 ' + escapeHtml(q.answer || '(no answer)') + '</div>' +
5168
+ '</div>'
5169
+ ).join('') +
5170
+ '</div>';
5171
+ body.appendChild(section);
5172
+
5173
+ const toggle = section.querySelector('.reviw-resolved-toggle');
5174
+ const list = section.querySelector('.reviw-resolved-list');
5175
+ toggle.addEventListener('click', () => {
5176
+ toggle.classList.toggle('open');
5177
+ list.classList.toggle('visible');
5178
+ });
5179
+ }
5180
+ }
5181
+
5182
+ function escapeHtml(str) {
5183
+ return String(str)
5184
+ .replace(/&/g, '&amp;')
5185
+ .replace(/</g, '&lt;')
5186
+ .replace(/>/g, '&gt;');
5187
+ }
5188
+
5189
+ function escapeAttr(str) {
5190
+ return String(str)
5191
+ .replace(/&/g, '&amp;')
5192
+ .replace(/"/g, '&quot;');
5193
+ }
5194
+
5195
+ function openModal() {
5196
+ overlay.classList.add('visible');
5197
+ bar.classList.remove('visible');
5198
+ document.body.classList.remove('has-questions-bar');
5199
+ }
5200
+
5201
+ function closeModal(allAnswered) {
5202
+ overlay.classList.remove('visible');
5203
+ const unansweredCount = getUnansweredCount();
5204
+ if (unansweredCount > 0 && !allAnswered) {
5205
+ bar.classList.add('visible');
5206
+ document.body.classList.add('has-questions-bar');
5207
+ } else {
5208
+ bar.classList.remove('visible');
5209
+ document.body.classList.remove('has-questions-bar');
5210
+ }
5211
+ }
5212
+
5213
+ // Event listeners
5214
+ closeBtn.addEventListener('click', () => closeModal(false));
5215
+ laterBtn.addEventListener('click', () => closeModal(false));
5216
+ barOpenBtn.addEventListener('click', openModal);
5217
+
5218
+ overlay.addEventListener('click', (e) => {
5219
+ if (e.target === overlay) closeModal(false);
5220
+ });
5221
+
5222
+ document.addEventListener('keydown', (e) => {
5223
+ if (e.key === 'Escape' && overlay.classList.contains('visible')) {
5224
+ closeModal(false);
5225
+ }
5226
+ });
5227
+
5228
+ // Expose answers for submit (only answered ones)
5229
+ window.REVIW_ANSWERS = answers;
5230
+
5231
+ // Initialize
5232
+ updateCounts();
5233
+ renderQuestions();
5234
+
5235
+ // Show modal on load if there are unresolved questions
5236
+ if (unresolvedQuestions.length > 0) {
5237
+ setTimeout(() => openModal(), 300);
5238
+ }
5239
+ })();
4428
5240
  </script>
4429
5241
  </body>
4430
5242
  </html>`;
@@ -4435,8 +5247,8 @@ function buildHtml(filePath) {
4435
5247
  if (data.mode === "diff") {
4436
5248
  return diffHtmlTemplate(data);
4437
5249
  }
4438
- const { rows, cols, title, mode, preview } = data;
4439
- return htmlTemplate(rows, cols, title, mode, preview);
5250
+ const { rows, cols, projectRoot, relativePath, mode, preview, reviwQuestions } = data;
5251
+ return htmlTemplate(rows, cols, projectRoot, relativePath, mode, preview, reviwQuestions);
4440
5252
  }
4441
5253
 
4442
5254
  // --- HTTP Server -----------------------------------------------------------
@@ -4468,6 +5280,20 @@ function outputAllResults() {
4468
5280
  const yamlOut = yaml.dump(combined, { noRefs: true, lineWidth: 120 });
4469
5281
  console.log(yamlOut.trim());
4470
5282
  }
5283
+
5284
+ // Output answered questions if any
5285
+ const allAnswers = [];
5286
+ for (const result of allResults) {
5287
+ if (result.reviwAnswers && result.reviwAnswers.length > 0) {
5288
+ allAnswers.push(...result.reviwAnswers);
5289
+ }
5290
+ }
5291
+ if (allAnswers.length > 0) {
5292
+ console.log("\n[REVIW_ANSWERS]");
5293
+ const answersYaml = yaml.dump(allAnswers, { noRefs: true, lineWidth: 120 });
5294
+ console.log(answersYaml.trim());
5295
+ console.log("[/REVIW_ANSWERS]");
5296
+ }
4471
5297
  }
4472
5298
 
4473
5299
  function checkAllDone() {
@@ -4714,7 +5540,14 @@ function createFileServer(filePath, fileIndex = 0) {
4714
5540
  const delay = fileIndex * 300;
4715
5541
  setTimeout(() => {
4716
5542
  try {
4717
- spawn(opener, [url], { stdio: "ignore", detached: true });
5543
+ const child = spawn(opener, [url], { stdio: "ignore", detached: true });
5544
+ child.on('error', (err) => {
5545
+ console.warn(
5546
+ "Failed to open browser automatically. Please open this URL manually:",
5547
+ url,
5548
+ );
5549
+ });
5550
+ child.unref();
4718
5551
  } catch (err) {
4719
5552
  console.warn(
4720
5553
  "Failed to open browser automatically. Please open this URL manually:",
@@ -4882,7 +5715,14 @@ function createDiffServer(diffContent) {
4882
5715
  ? "start"
4883
5716
  : "xdg-open";
4884
5717
  try {
4885
- spawn(opener, [url], { stdio: "ignore", detached: true });
5718
+ const child = spawn(opener, [url], { stdio: "ignore", detached: true });
5719
+ child.on('error', (err) => {
5720
+ console.warn(
5721
+ "Failed to open browser automatically. Please open this URL manually:",
5722
+ url,
5723
+ );
5724
+ });
5725
+ child.unref();
4886
5726
  } catch (err) {
4887
5727
  console.warn(
4888
5728
  "Failed to open browser automatically. Please open this URL manually:",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reviw",
3
- "version": "0.10.7",
3
+ "version": "0.11.2",
4
4
  "description": "Lightweight file reviewer with in-browser comments for CSV, TSV, Markdown, and Git diffs.",
5
5
  "type": "module",
6
6
  "bin": {