reviw 0.16.3 → 0.17.0
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/cli.cjs +336 -55
- package/package.json +1 -1
package/cli.cjs
CHANGED
|
@@ -946,7 +946,7 @@ function diffHtmlTemplate(diffData, history = []) {
|
|
|
946
946
|
}
|
|
947
947
|
.theme-toggle:hover { background: var(--border); }
|
|
948
948
|
|
|
949
|
-
.wrap { padding: 16px 20px
|
|
949
|
+
.wrap { padding: 16px 20px 16px; max-width: 1200px; margin: 0 auto; }
|
|
950
950
|
.diff-container {
|
|
951
951
|
background: var(--panel);
|
|
952
952
|
border: 1px solid var(--border);
|
|
@@ -1140,19 +1140,8 @@ function diffHtmlTemplate(diffData, history = []) {
|
|
|
1140
1140
|
.comment-list li:hover { color: var(--accent); }
|
|
1141
1141
|
.comment-list .hint { color: var(--muted); font-size: 11px; margin-top: 8px; }
|
|
1142
1142
|
.comment-list.collapsed { opacity: 0; pointer-events: none; transform: translateY(8px); }
|
|
1143
|
-
.
|
|
1144
|
-
|
|
1145
|
-
right: 16px;
|
|
1146
|
-
bottom: 16px;
|
|
1147
|
-
padding: 8px 12px;
|
|
1148
|
-
border-radius: 6px;
|
|
1149
|
-
border: 1px solid var(--border);
|
|
1150
|
-
background: var(--panel-alpha);
|
|
1151
|
-
color: var(--text);
|
|
1152
|
-
cursor: pointer;
|
|
1153
|
-
font-size: 12px;
|
|
1154
|
-
}
|
|
1155
|
-
.pill { display: inline-flex; align-items: center; gap: 6px; padding: 4px 8px; border-radius: 999px; background: var(--selected-bg); border: 1px solid var(--border); font-size: 12px; }
|
|
1143
|
+
.pill { display: inline-flex; align-items: center; gap: 6px; padding: 4px 8px; border-radius: 999px; background: var(--selected-bg); border: 1px solid var(--border); font-size: 12px; cursor: pointer; transition: background 150ms ease, border-color 150ms ease; }
|
|
1144
|
+
.pill:hover { background: var(--hover-bg); border-color: var(--accent); }
|
|
1156
1145
|
.pill strong { font-weight: 700; }
|
|
1157
1146
|
|
|
1158
1147
|
.modal-overlay {
|
|
@@ -1213,6 +1202,15 @@ function diffHtmlTemplate(diffData, history = []) {
|
|
|
1213
1202
|
}
|
|
1214
1203
|
.modal-actions button:hover { background: var(--border); }
|
|
1215
1204
|
.modal-actions button.primary { background: var(--accent); color: var(--text-inverse); border-color: var(--accent); }
|
|
1205
|
+
.image-attach-area { margin: 12px 0; }
|
|
1206
|
+
.image-attach-area label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
|
|
1207
|
+
.image-attach-area.image-attach-small { margin: 8px 0; }
|
|
1208
|
+
.image-attach-area.image-attach-small label { font-size: 11px; }
|
|
1209
|
+
.image-preview-list { display: flex; flex-wrap: wrap; gap: 8px; min-height: 24px; }
|
|
1210
|
+
.image-preview-item { position: relative; }
|
|
1211
|
+
.image-preview-item img { max-width: 80px; max-height: 60px; border-radius: 4px; border: 1px solid var(--border); object-fit: cover; }
|
|
1212
|
+
.image-preview-item .remove-image { position: absolute; top: -6px; right: -6px; width: 18px; height: 18px; border-radius: 50%; background: var(--error, #ef4444); color: #fff; border: none; cursor: pointer; font-size: 12px; line-height: 1; display: flex; align-items: center; justify-content: center; }
|
|
1213
|
+
.image-preview-item .remove-image:hover { background: #dc2626; }
|
|
1216
1214
|
|
|
1217
1215
|
.modal-checkboxes { margin: 12px 0; }
|
|
1218
1216
|
.modal-checkboxes label {
|
|
@@ -1458,7 +1456,7 @@ function diffHtmlTemplate(diffData, history = []) {
|
|
|
1458
1456
|
<div class="meta">
|
|
1459
1457
|
<h1>${projectRoot ? `<span class="title-path">${projectRoot}</span>` : ""}<span class="title-file">${relativePath}</span></h1>
|
|
1460
1458
|
<span class="badge">${fileCount} file${fileCount !== 1 ? "s" : ""} changed</span>
|
|
1461
|
-
<
|
|
1459
|
+
<button class="pill" id="pill-comments" title="Toggle comment panel">Comments <strong id="comment-count">0</strong></button>
|
|
1462
1460
|
</div>
|
|
1463
1461
|
<div class="actions">
|
|
1464
1462
|
<button class="history-toggle" id="history-toggle" title="Review History">☰</button>
|
|
@@ -1492,17 +1490,20 @@ function diffHtmlTemplate(diffData, history = []) {
|
|
|
1492
1490
|
</header>
|
|
1493
1491
|
<div id="cell-preview" style="font-size:11px; color: var(--muted); margin-bottom:8px; white-space: pre-wrap; max-height: 60px; overflow: hidden;"></div>
|
|
1494
1492
|
<textarea id="comment-input" placeholder="Enter your comment"></textarea>
|
|
1493
|
+
<div class="image-attach-area image-attach-small" id="comment-image-area">
|
|
1494
|
+
<label>📎 Image (⌘V, max 1)</label>
|
|
1495
|
+
<div class="image-preview-list" id="comment-image-preview"></div>
|
|
1496
|
+
</div>
|
|
1495
1497
|
<div class="actions">
|
|
1496
1498
|
<button class="primary" id="save-comment">Save</button>
|
|
1497
1499
|
</div>
|
|
1498
1500
|
</div>
|
|
1499
1501
|
|
|
1500
|
-
<aside class="comment-list">
|
|
1502
|
+
<aside class="comment-list collapsed">
|
|
1501
1503
|
<h3>Comments</h3>
|
|
1502
1504
|
<ol id="comment-list"></ol>
|
|
1503
1505
|
<p class="hint">Click "Submit & Exit" to finish review.</p>
|
|
1504
1506
|
</aside>
|
|
1505
|
-
<button class="comment-toggle" id="comment-toggle">Comments (0)</button>
|
|
1506
1507
|
|
|
1507
1508
|
<div class="modal-overlay" id="submit-modal">
|
|
1508
1509
|
<div class="modal-dialog">
|
|
@@ -1510,6 +1511,10 @@ function diffHtmlTemplate(diffData, history = []) {
|
|
|
1510
1511
|
<p class="modal-summary" id="modal-summary"></p>
|
|
1511
1512
|
<label for="global-comment">Overall comment (optional)</label>
|
|
1512
1513
|
<textarea id="global-comment" placeholder="Add a summary or overall feedback..."></textarea>
|
|
1514
|
+
<div class="image-attach-area" id="submit-image-area">
|
|
1515
|
+
<label>📎 Attach images (⌘V to paste, max 5)</label>
|
|
1516
|
+
<div class="image-preview-list" id="submit-image-preview"></div>
|
|
1517
|
+
</div>
|
|
1513
1518
|
<div class="modal-checkboxes">
|
|
1514
1519
|
<label><input type="checkbox" id="prompt-subagents" checked /> 🤖 Delegate to sub-agents (implement, verify, report)</label>
|
|
1515
1520
|
<label><input type="checkbox" id="prompt-reviw" checked /> 👁️ Open in REVIW next time</label>
|
|
@@ -1716,7 +1721,7 @@ function diffHtmlTemplate(diffData, history = []) {
|
|
|
1716
1721
|
const commentList = document.getElementById('comment-list');
|
|
1717
1722
|
const commentCount = document.getElementById('comment-count');
|
|
1718
1723
|
const commentPanel = document.querySelector('.comment-list');
|
|
1719
|
-
const
|
|
1724
|
+
const pillComments = document.getElementById('pill-comments');
|
|
1720
1725
|
|
|
1721
1726
|
const comments = {};
|
|
1722
1727
|
let currentKey = null;
|
|
@@ -1726,6 +1731,81 @@ function diffHtmlTemplate(diffData, history = []) {
|
|
|
1726
1731
|
let dragEnd = null;
|
|
1727
1732
|
let selection = null;
|
|
1728
1733
|
|
|
1734
|
+
// Image attachment state
|
|
1735
|
+
const submitImages = []; // base64 images for submit modal (max 5)
|
|
1736
|
+
let currentCommentImage = null; // base64 image for current comment (max 1)
|
|
1737
|
+
|
|
1738
|
+
// Image attachment handlers
|
|
1739
|
+
const submitImagePreview = document.getElementById('submit-image-preview');
|
|
1740
|
+
const commentImagePreview = document.getElementById('comment-image-preview');
|
|
1741
|
+
|
|
1742
|
+
function addImageToPreview(container, images, maxCount, base64) {
|
|
1743
|
+
if (images.length >= maxCount) return;
|
|
1744
|
+
images.push(base64);
|
|
1745
|
+
renderImagePreviews(container, images);
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
function renderImagePreviews(container, images) {
|
|
1749
|
+
container.innerHTML = '';
|
|
1750
|
+
images.forEach((base64, idx) => {
|
|
1751
|
+
const item = document.createElement('div');
|
|
1752
|
+
item.className = 'image-preview-item';
|
|
1753
|
+
item.innerHTML = \`<img src="\${base64}" alt="attached image"><button class="remove-image" data-idx="\${idx}">×</button>\`;
|
|
1754
|
+
item.querySelector('.remove-image').addEventListener('click', () => {
|
|
1755
|
+
images.splice(idx, 1);
|
|
1756
|
+
renderImagePreviews(container, images);
|
|
1757
|
+
});
|
|
1758
|
+
container.appendChild(item);
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
function renderCommentImagePreview() {
|
|
1763
|
+
commentImagePreview.innerHTML = '';
|
|
1764
|
+
if (currentCommentImage) {
|
|
1765
|
+
const item = document.createElement('div');
|
|
1766
|
+
item.className = 'image-preview-item';
|
|
1767
|
+
item.innerHTML = \`<img src="\${currentCommentImage}" alt="attached image"><button class="remove-image">×</button>\`;
|
|
1768
|
+
item.querySelector('.remove-image').addEventListener('click', () => {
|
|
1769
|
+
currentCommentImage = null;
|
|
1770
|
+
renderCommentImagePreview();
|
|
1771
|
+
});
|
|
1772
|
+
commentImagePreview.appendChild(item);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// Global paste handler
|
|
1777
|
+
document.addEventListener('paste', (e) => {
|
|
1778
|
+
const items = e.clipboardData?.items;
|
|
1779
|
+
if (!items) return;
|
|
1780
|
+
for (const item of items) {
|
|
1781
|
+
if (item.type.startsWith('image/')) {
|
|
1782
|
+
e.preventDefault();
|
|
1783
|
+
const file = item.getAsFile();
|
|
1784
|
+
const reader = new FileReader();
|
|
1785
|
+
reader.onload = () => {
|
|
1786
|
+
const base64 = reader.result;
|
|
1787
|
+
const activeEl = document.activeElement;
|
|
1788
|
+
// Prioritize comment card if its textarea has focus
|
|
1789
|
+
if (card.style.display !== 'none' && activeEl === commentInput) {
|
|
1790
|
+
if (!currentCommentImage) {
|
|
1791
|
+
currentCommentImage = base64;
|
|
1792
|
+
renderCommentImagePreview();
|
|
1793
|
+
}
|
|
1794
|
+
} else if (submitModal.classList.contains('visible')) {
|
|
1795
|
+
addImageToPreview(submitImagePreview, submitImages, 5, base64);
|
|
1796
|
+
} else if (card.style.display !== 'none') {
|
|
1797
|
+
if (!currentCommentImage) {
|
|
1798
|
+
currentCommentImage = base64;
|
|
1799
|
+
renderCommentImagePreview();
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
reader.readAsDataURL(file);
|
|
1804
|
+
break;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
});
|
|
1808
|
+
|
|
1729
1809
|
function makeKey(start, end) {
|
|
1730
1810
|
return start === end ? String(start) : (start + '-' + end);
|
|
1731
1811
|
}
|
|
@@ -1947,7 +2027,6 @@ function diffHtmlTemplate(diffData, history = []) {
|
|
|
1947
2027
|
commentList.innerHTML = '';
|
|
1948
2028
|
const items = Object.values(comments).sort((a, b) => (a.startRow ?? a.row) - (b.startRow ?? b.row));
|
|
1949
2029
|
commentCount.textContent = items.length;
|
|
1950
|
-
commentToggle.textContent = 'Comments (' + items.length + ')';
|
|
1951
2030
|
if (items.length === 0) panelOpen = false;
|
|
1952
2031
|
commentPanel.classList.toggle('collapsed', !panelOpen || items.length === 0);
|
|
1953
2032
|
if (!items.length) {
|
|
@@ -1968,7 +2047,7 @@ function diffHtmlTemplate(diffData, history = []) {
|
|
|
1968
2047
|
});
|
|
1969
2048
|
}
|
|
1970
2049
|
|
|
1971
|
-
|
|
2050
|
+
pillComments.addEventListener('click', () => {
|
|
1972
2051
|
panelOpen = !panelOpen;
|
|
1973
2052
|
if (panelOpen && Object.keys(comments).length === 0) panelOpen = false;
|
|
1974
2053
|
commentPanel.classList.toggle('collapsed', !panelOpen);
|
|
@@ -1980,7 +2059,7 @@ function diffHtmlTemplate(diffData, history = []) {
|
|
|
1980
2059
|
const range = keyToRange(currentKey);
|
|
1981
2060
|
if (!range) return;
|
|
1982
2061
|
const rowIdx = range.start;
|
|
1983
|
-
if (text) {
|
|
2062
|
+
if (text || currentCommentImage) {
|
|
1984
2063
|
if (range.start === range.end) {
|
|
1985
2064
|
comments[currentKey] = { row: rowIdx, text, content: DATA[rowIdx]?.content || '' };
|
|
1986
2065
|
} else {
|
|
@@ -1992,11 +2071,16 @@ function diffHtmlTemplate(diffData, history = []) {
|
|
|
1992
2071
|
content: DATA.slice(range.start, range.end + 1).map(r => r?.content || '').join('\\n')
|
|
1993
2072
|
};
|
|
1994
2073
|
}
|
|
2074
|
+
if (currentCommentImage) {
|
|
2075
|
+
comments[currentKey].image = currentCommentImage;
|
|
2076
|
+
}
|
|
1995
2077
|
setDotRange(range.start, range.end, true);
|
|
1996
2078
|
} else {
|
|
1997
2079
|
delete comments[currentKey];
|
|
1998
2080
|
setDotRange(range.start, range.end, false);
|
|
1999
2081
|
}
|
|
2082
|
+
currentCommentImage = null;
|
|
2083
|
+
renderCommentImagePreview();
|
|
2000
2084
|
refreshList();
|
|
2001
2085
|
closeCard();
|
|
2002
2086
|
saveToStorage();
|
|
@@ -2112,17 +2196,28 @@ function diffHtmlTemplate(diffData, history = []) {
|
|
|
2112
2196
|
function payload(reason) {
|
|
2113
2197
|
const data = { file: FILE_NAME, mode: MODE, submittedBy: reason, submittedAt: new Date().toISOString(), comments: Object.values(comments) };
|
|
2114
2198
|
if (globalComment.trim()) data.summary = globalComment.trim();
|
|
2199
|
+
if (submitImages.length > 0) data.summaryImages = submitImages;
|
|
2115
2200
|
const prompts = getSelectedPrompts();
|
|
2116
2201
|
if (prompts.length > 0) data.prompts = prompts;
|
|
2117
2202
|
return data;
|
|
2118
2203
|
}
|
|
2119
|
-
function sendAndExit(reason = 'button') {
|
|
2204
|
+
async function sendAndExit(reason = 'button') {
|
|
2120
2205
|
if (sent) return;
|
|
2121
2206
|
sent = true;
|
|
2122
2207
|
clearStorage();
|
|
2123
2208
|
const p = payload(reason);
|
|
2124
2209
|
saveToHistory(p);
|
|
2125
|
-
|
|
2210
|
+
try {
|
|
2211
|
+
// Use fetch with keepalive to handle large payloads (images)
|
|
2212
|
+
await fetch('/exit', {
|
|
2213
|
+
method: 'POST',
|
|
2214
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2215
|
+
body: JSON.stringify(p),
|
|
2216
|
+
// Note: keepalive has 64KB limit like sendBeacon, so we don't use it for large payloads
|
|
2217
|
+
});
|
|
2218
|
+
} catch (err) {
|
|
2219
|
+
console.error('Failed to send exit request:', err);
|
|
2220
|
+
}
|
|
2126
2221
|
}
|
|
2127
2222
|
function showSubmitModal() {
|
|
2128
2223
|
const count = Object.keys(comments).length;
|
|
@@ -2134,11 +2229,11 @@ function diffHtmlTemplate(diffData, history = []) {
|
|
|
2134
2229
|
function hideSubmitModal() { submitModal.classList.remove('visible'); }
|
|
2135
2230
|
document.getElementById('send-and-exit').addEventListener('click', showSubmitModal);
|
|
2136
2231
|
document.getElementById('modal-cancel').addEventListener('click', hideSubmitModal);
|
|
2137
|
-
function doSubmit() {
|
|
2232
|
+
async function doSubmit() {
|
|
2138
2233
|
globalComment = globalCommentInput.value;
|
|
2139
2234
|
savePromptPrefs();
|
|
2140
2235
|
hideSubmitModal();
|
|
2141
|
-
sendAndExit('button');
|
|
2236
|
+
await sendAndExit('button');
|
|
2142
2237
|
// Try to close window; if it fails (browser security), show completion message
|
|
2143
2238
|
setTimeout(() => {
|
|
2144
2239
|
window.close();
|
|
@@ -2343,7 +2438,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
2343
2438
|
}
|
|
2344
2439
|
.theme-toggle:hover { background: var(--hover-bg); transform: scale(1.05); }
|
|
2345
2440
|
|
|
2346
|
-
.wrap { padding: 12px 16px
|
|
2441
|
+
.wrap { padding: 12px 16px 12px; }
|
|
2347
2442
|
.toolbar {
|
|
2348
2443
|
display: flex;
|
|
2349
2444
|
gap: 12px;
|
|
@@ -2607,27 +2702,14 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
2607
2702
|
}
|
|
2608
2703
|
.comment-list li { margin-bottom: 6px; }
|
|
2609
2704
|
.comment-list .hint { color: var(--muted); font-size: 12px; }
|
|
2610
|
-
.pill { display: inline-flex; align-items: center; gap: 6px; padding: 4px 8px; border-radius: 999px; background: var(--selected-bg); border: 1px solid var(--border); font-size: 12px; color: var(--text); }
|
|
2705
|
+
.pill { display: inline-flex; align-items: center; gap: 6px; padding: 4px 8px; border-radius: 999px; background: var(--selected-bg); border: 1px solid var(--border); font-size: 12px; color: var(--text); cursor: pointer; transition: background 150ms ease, border-color 150ms ease; }
|
|
2706
|
+
.pill:hover { background: var(--hover-bg); border-color: var(--accent); }
|
|
2611
2707
|
.pill strong { color: var(--text); font-weight: 700; }
|
|
2612
2708
|
.comment-list.collapsed {
|
|
2613
2709
|
opacity: 0;
|
|
2614
2710
|
pointer-events: none;
|
|
2615
2711
|
transform: translateY(8px) scale(0.98);
|
|
2616
2712
|
}
|
|
2617
|
-
.comment-toggle {
|
|
2618
|
-
position: fixed;
|
|
2619
|
-
right: 14px;
|
|
2620
|
-
bottom: 14px;
|
|
2621
|
-
padding: 10px 12px;
|
|
2622
|
-
border-radius: 10px;
|
|
2623
|
-
border: 1px solid var(--border);
|
|
2624
|
-
background: var(--selected-bg);
|
|
2625
|
-
color: var(--text);
|
|
2626
|
-
cursor: pointer;
|
|
2627
|
-
box-shadow: 0 10px 24px var(--shadow-color);
|
|
2628
|
-
font-size: 13px;
|
|
2629
|
-
transition: background 200ms ease, border-color 200ms ease;
|
|
2630
|
-
}
|
|
2631
2713
|
.md-preview {
|
|
2632
2714
|
background: var(--input-bg);
|
|
2633
2715
|
border: 1px solid var(--border);
|
|
@@ -2641,7 +2723,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
2641
2723
|
gap: 16px;
|
|
2642
2724
|
align-items: stretch;
|
|
2643
2725
|
margin-top: 8px;
|
|
2644
|
-
height: calc(100vh -
|
|
2726
|
+
height: calc(100vh - 80px);
|
|
2645
2727
|
}
|
|
2646
2728
|
.md-left {
|
|
2647
2729
|
flex: 1;
|
|
@@ -3354,6 +3436,15 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3354
3436
|
}
|
|
3355
3437
|
.modal-actions button:hover { background: var(--hover-bg); }
|
|
3356
3438
|
.modal-actions button.primary { background: var(--accent); color: var(--text-inverse); border-color: var(--accent); }
|
|
3439
|
+
.image-attach-area { margin: 12px 0; }
|
|
3440
|
+
.image-attach-area label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
|
|
3441
|
+
.image-attach-area.image-attach-small { margin: 8px 0; }
|
|
3442
|
+
.image-attach-area.image-attach-small label { font-size: 11px; }
|
|
3443
|
+
.image-preview-list { display: flex; flex-wrap: wrap; gap: 8px; min-height: 24px; }
|
|
3444
|
+
.image-preview-item { position: relative; }
|
|
3445
|
+
.image-preview-item img { max-width: 80px; max-height: 60px; border-radius: 4px; border: 1px solid var(--border); object-fit: cover; }
|
|
3446
|
+
.image-preview-item .remove-image { position: absolute; top: -6px; right: -6px; width: 18px; height: 18px; border-radius: 50%; background: var(--error, #ef4444); color: #fff; border: none; cursor: pointer; font-size: 12px; line-height: 1; display: flex; align-items: center; justify-content: center; }
|
|
3447
|
+
.image-preview-item .remove-image:hover { background: #dc2626; }
|
|
3357
3448
|
.modal-actions button.primary:hover { background: #7dd3fc; }
|
|
3358
3449
|
|
|
3359
3450
|
.modal-checkboxes { margin: 12px 0; }
|
|
@@ -3686,7 +3777,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3686
3777
|
<div class="meta">
|
|
3687
3778
|
<h1><span class="title-path">${projectRoot}</span><span class="title-file">${relativePath}</span></h1>
|
|
3688
3779
|
<span class="badge">Click to comment / ESC to cancel</span>
|
|
3689
|
-
<
|
|
3780
|
+
<button class="pill" id="pill-comments" title="Toggle comment panel">Comments <strong id="comment-count">0</strong></button>
|
|
3690
3781
|
</div>
|
|
3691
3782
|
<div class="actions">
|
|
3692
3783
|
<button class="history-toggle" id="history-toggle" title="Review History">☰</button>
|
|
@@ -3772,17 +3863,20 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3772
3863
|
</header>
|
|
3773
3864
|
<div id="cell-preview" style="font-size:12px; color: var(--muted); margin-bottom:8px;"></div>
|
|
3774
3865
|
<textarea id="comment-input" placeholder="Enter your comment or note"></textarea>
|
|
3866
|
+
<div class="image-attach-area image-attach-small" id="comment-image-area">
|
|
3867
|
+
<label>📎 Image (⌘V, max 1)</label>
|
|
3868
|
+
<div class="image-preview-list" id="comment-image-preview"></div>
|
|
3869
|
+
</div>
|
|
3775
3870
|
<div class="actions">
|
|
3776
3871
|
<button class="primary" id="save-comment">Save</button>
|
|
3777
3872
|
</div>
|
|
3778
3873
|
</div>
|
|
3779
3874
|
|
|
3780
|
-
<aside class="comment-list">
|
|
3875
|
+
<aside class="comment-list collapsed">
|
|
3781
3876
|
<h3>Comments</h3>
|
|
3782
3877
|
<ol id="comment-list"></ol>
|
|
3783
3878
|
<p class="hint">Close the tab or click "Submit & Exit" to send comments and stop the server.</p>
|
|
3784
3879
|
</aside>
|
|
3785
|
-
<button class="comment-toggle" id="comment-toggle">Comments (0)</button>
|
|
3786
3880
|
<div class="filter-menu" id="filter-menu">
|
|
3787
3881
|
<label class="menu-check"><input type="checkbox" id="freeze-col-check" /> Freeze up to this column</label>
|
|
3788
3882
|
<button data-action="not-empty">Rows where not empty</button>
|
|
@@ -3812,6 +3906,10 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3812
3906
|
<p class="modal-summary" id="modal-summary"></p>
|
|
3813
3907
|
<label for="global-comment">Overall comment (optional)</label>
|
|
3814
3908
|
<textarea id="global-comment" placeholder="Add a summary or overall feedback..."></textarea>
|
|
3909
|
+
<div class="image-attach-area" id="submit-image-area">
|
|
3910
|
+
<label>📎 Attach images (⌘V to paste, max 5)</label>
|
|
3911
|
+
<div class="image-preview-list" id="submit-image-preview"></div>
|
|
3912
|
+
</div>
|
|
3815
3913
|
<div class="modal-checkboxes">
|
|
3816
3914
|
<label><input type="checkbox" id="prompt-subagents" checked /> 🤖 Delegate to sub-agents (implement, verify, report)</label>
|
|
3817
3915
|
<label><input type="checkbox" id="prompt-reviw" checked /> 👁️ Open in REVIW next time</label>
|
|
@@ -4074,7 +4172,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4074
4172
|
const commentCount = document.getElementById('comment-count');
|
|
4075
4173
|
const fitBtn = document.getElementById('fit-width');
|
|
4076
4174
|
const commentPanel = document.querySelector('.comment-list');
|
|
4077
|
-
const
|
|
4175
|
+
const pillComments = document.getElementById('pill-comments');
|
|
4078
4176
|
const filterMenu = document.getElementById('filter-menu');
|
|
4079
4177
|
const rowMenu = document.getElementById('row-menu');
|
|
4080
4178
|
const freezeColCheck = document.getElementById('freeze-col-check');
|
|
@@ -4144,6 +4242,85 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4144
4242
|
let dragEnd = null; // {row, col}
|
|
4145
4243
|
let selection = null; // {startRow, endRow, startCol, endCol}
|
|
4146
4244
|
|
|
4245
|
+
// Image attachment state
|
|
4246
|
+
const submitImages = []; // base64 images for submit modal (max 5)
|
|
4247
|
+
let currentCommentImage = null; // base64 image for current comment (max 1)
|
|
4248
|
+
|
|
4249
|
+
// Image attachment handlers
|
|
4250
|
+
const submitImagePreview = document.getElementById('submit-image-preview');
|
|
4251
|
+
const commentImagePreview = document.getElementById('comment-image-preview');
|
|
4252
|
+
|
|
4253
|
+
function addImageToPreview(container, images, maxCount, base64) {
|
|
4254
|
+
if (images.length >= maxCount) return;
|
|
4255
|
+
images.push(base64);
|
|
4256
|
+
renderImagePreviews(container, images);
|
|
4257
|
+
}
|
|
4258
|
+
|
|
4259
|
+
function renderImagePreviews(container, images) {
|
|
4260
|
+
container.innerHTML = '';
|
|
4261
|
+
images.forEach((base64, idx) => {
|
|
4262
|
+
const item = document.createElement('div');
|
|
4263
|
+
item.className = 'image-preview-item';
|
|
4264
|
+
item.innerHTML = \`<img src="\${base64}" alt="attached image"><button class="remove-image" data-idx="\${idx}">×</button>\`;
|
|
4265
|
+
item.querySelector('.remove-image').addEventListener('click', () => {
|
|
4266
|
+
images.splice(idx, 1);
|
|
4267
|
+
renderImagePreviews(container, images);
|
|
4268
|
+
});
|
|
4269
|
+
container.appendChild(item);
|
|
4270
|
+
});
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4273
|
+
function renderCommentImagePreview() {
|
|
4274
|
+
commentImagePreview.innerHTML = '';
|
|
4275
|
+
if (currentCommentImage) {
|
|
4276
|
+
const item = document.createElement('div');
|
|
4277
|
+
item.className = 'image-preview-item';
|
|
4278
|
+
item.innerHTML = \`<img src="\${currentCommentImage}" alt="attached image"><button class="remove-image">×</button>\`;
|
|
4279
|
+
item.querySelector('.remove-image').addEventListener('click', () => {
|
|
4280
|
+
currentCommentImage = null;
|
|
4281
|
+
renderCommentImagePreview();
|
|
4282
|
+
});
|
|
4283
|
+
commentImagePreview.appendChild(item);
|
|
4284
|
+
}
|
|
4285
|
+
}
|
|
4286
|
+
|
|
4287
|
+
// Global paste handler for images
|
|
4288
|
+
document.addEventListener('paste', (e) => {
|
|
4289
|
+
const items = e.clipboardData?.items;
|
|
4290
|
+
if (!items) return;
|
|
4291
|
+
for (const item of items) {
|
|
4292
|
+
if (item.type.startsWith('image/')) {
|
|
4293
|
+
e.preventDefault();
|
|
4294
|
+
const file = item.getAsFile();
|
|
4295
|
+
const reader = new FileReader();
|
|
4296
|
+
reader.onload = () => {
|
|
4297
|
+
const base64 = reader.result;
|
|
4298
|
+
const modal = document.getElementById('submit-modal');
|
|
4299
|
+
const commentCard = document.getElementById('comment-card');
|
|
4300
|
+
const activeEl = document.activeElement;
|
|
4301
|
+
const commentInput = document.getElementById('comment-input');
|
|
4302
|
+
|
|
4303
|
+
// Prioritize comment card if its textarea has focus
|
|
4304
|
+
if (commentCard?.style.display !== 'none' && activeEl === commentInput) {
|
|
4305
|
+
if (!currentCommentImage) {
|
|
4306
|
+
currentCommentImage = base64;
|
|
4307
|
+
renderCommentImagePreview();
|
|
4308
|
+
}
|
|
4309
|
+
} else if (modal?.classList.contains('visible')) {
|
|
4310
|
+
addImageToPreview(submitImagePreview, submitImages, 5, base64);
|
|
4311
|
+
} else if (commentCard?.style.display !== 'none') {
|
|
4312
|
+
if (!currentCommentImage) {
|
|
4313
|
+
currentCommentImage = base64;
|
|
4314
|
+
renderCommentImagePreview();
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
};
|
|
4318
|
+
reader.readAsDataURL(file);
|
|
4319
|
+
break;
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4322
|
+
});
|
|
4323
|
+
|
|
4147
4324
|
// --- localStorage Comment Persistence ---
|
|
4148
4325
|
const STORAGE_KEY = 'reviw:comments:' + FILE_NAME;
|
|
4149
4326
|
const STORAGE_TTL = 3 * 60 * 60 * 1000; // 3 hours in milliseconds
|
|
@@ -4566,7 +4743,6 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4566
4743
|
return aRow === bRow ? aCol - bCol : aRow - bRow;
|
|
4567
4744
|
});
|
|
4568
4745
|
commentCount.textContent = items.length;
|
|
4569
|
-
commentToggle.textContent = 'Comments (' + items.length + ')';
|
|
4570
4746
|
if (items.length === 0) {
|
|
4571
4747
|
panelOpen = false;
|
|
4572
4748
|
}
|
|
@@ -4608,7 +4784,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4608
4784
|
});
|
|
4609
4785
|
}
|
|
4610
4786
|
|
|
4611
|
-
|
|
4787
|
+
pillComments.addEventListener('click', () => {
|
|
4612
4788
|
panelOpen = !panelOpen;
|
|
4613
4789
|
if (panelOpen && Object.keys(comments).length === 0) {
|
|
4614
4790
|
panelOpen = false; // keep hidden if no comments
|
|
@@ -4745,8 +4921,11 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4745
4921
|
if (isRangeKey(currentKey)) {
|
|
4746
4922
|
// Range (rectangular) comment
|
|
4747
4923
|
const { startRow, startCol, endRow, endCol } = parseRangeKey(currentKey);
|
|
4748
|
-
if (text) {
|
|
4924
|
+
if (text || currentCommentImage) {
|
|
4749
4925
|
comments[currentKey] = { startRow, startCol, endRow, endCol, text, isRange: true };
|
|
4926
|
+
if (currentCommentImage) {
|
|
4927
|
+
comments[currentKey].image = currentCommentImage;
|
|
4928
|
+
}
|
|
4750
4929
|
for (let r = startRow; r <= endRow; r++) {
|
|
4751
4930
|
for (let c = startCol; c <= endCol; c++) {
|
|
4752
4931
|
setDot(r, c, true);
|
|
@@ -4768,14 +4947,19 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4768
4947
|
const [row, col] = currentKey.split('-').map(Number);
|
|
4769
4948
|
const td = tbody.querySelector('td[data-row="' + row + '"][data-col="' + col + '"]');
|
|
4770
4949
|
const value = td ? td.textContent : '';
|
|
4771
|
-
if (text) {
|
|
4950
|
+
if (text || currentCommentImage) {
|
|
4772
4951
|
comments[currentKey] = { row, col, text, value };
|
|
4952
|
+
if (currentCommentImage) {
|
|
4953
|
+
comments[currentKey].image = currentCommentImage;
|
|
4954
|
+
}
|
|
4773
4955
|
setDot(row, col, true);
|
|
4774
4956
|
} else {
|
|
4775
4957
|
delete comments[currentKey];
|
|
4776
4958
|
setDot(row, col, false);
|
|
4777
4959
|
}
|
|
4778
4960
|
}
|
|
4961
|
+
currentCommentImage = null;
|
|
4962
|
+
renderCommentImagePreview();
|
|
4779
4963
|
refreshList();
|
|
4780
4964
|
closeCard();
|
|
4781
4965
|
saveCommentsToStorage();
|
|
@@ -5059,6 +5243,10 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
5059
5243
|
if (c.isRange) {
|
|
5060
5244
|
transformed.lineEnd = (c.endRow || c.startRow) + 1;
|
|
5061
5245
|
}
|
|
5246
|
+
// Preserve image attachment
|
|
5247
|
+
if (c.image) {
|
|
5248
|
+
transformed.image = c.image;
|
|
5249
|
+
}
|
|
5062
5250
|
return transformed;
|
|
5063
5251
|
});
|
|
5064
5252
|
}
|
|
@@ -5079,6 +5267,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
5079
5267
|
if (globalComment.trim()) {
|
|
5080
5268
|
data.summary = globalComment.trim();
|
|
5081
5269
|
}
|
|
5270
|
+
if (submitImages.length > 0) data.summaryImages = submitImages;
|
|
5082
5271
|
const prompts = getSelectedPrompts();
|
|
5083
5272
|
if (prompts.length > 0) data.prompts = prompts;
|
|
5084
5273
|
// Include answered questions
|
|
@@ -5099,14 +5288,24 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
5099
5288
|
}
|
|
5100
5289
|
return data;
|
|
5101
5290
|
}
|
|
5102
|
-
function sendAndExit(reason = 'pagehide') {
|
|
5291
|
+
async function sendAndExit(reason = 'pagehide') {
|
|
5103
5292
|
if (sent) return;
|
|
5104
5293
|
sent = true;
|
|
5105
5294
|
clearCommentsFromStorage();
|
|
5106
5295
|
const p = payload(reason);
|
|
5107
5296
|
saveToHistory(p);
|
|
5108
|
-
|
|
5109
|
-
|
|
5297
|
+
try {
|
|
5298
|
+
// Use fetch with keepalive to handle large payloads (images)
|
|
5299
|
+
// keepalive allows the request to outlive the page
|
|
5300
|
+
await fetch('/exit', {
|
|
5301
|
+
method: 'POST',
|
|
5302
|
+
headers: { 'Content-Type': 'application/json' },
|
|
5303
|
+
body: JSON.stringify(p),
|
|
5304
|
+
// Note: keepalive has 64KB limit like sendBeacon, so we don't use it for large payloads
|
|
5305
|
+
});
|
|
5306
|
+
} catch (err) {
|
|
5307
|
+
console.error('Failed to send exit request:', err);
|
|
5308
|
+
}
|
|
5110
5309
|
}
|
|
5111
5310
|
function showSubmitModal() {
|
|
5112
5311
|
const count = Object.keys(comments).length;
|
|
@@ -5122,11 +5321,11 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
5122
5321
|
}
|
|
5123
5322
|
document.getElementById('send-and-exit').addEventListener('click', showSubmitModal);
|
|
5124
5323
|
modalCancel.addEventListener('click', hideSubmitModal);
|
|
5125
|
-
function doSubmit() {
|
|
5324
|
+
async function doSubmit() {
|
|
5126
5325
|
globalComment = globalCommentInput.value;
|
|
5127
5326
|
savePromptPrefs();
|
|
5128
5327
|
hideSubmitModal();
|
|
5129
|
-
sendAndExit('button');
|
|
5328
|
+
await sendAndExit('button');
|
|
5130
5329
|
// Try to close window; if it fails (browser security), show completion message
|
|
5131
5330
|
setTimeout(() => {
|
|
5132
5331
|
window.close();
|
|
@@ -6496,6 +6695,79 @@ function removeLockFile(filePath) {
|
|
|
6496
6695
|
}
|
|
6497
6696
|
}
|
|
6498
6697
|
|
|
6698
|
+
// --- Image Saving Helper ---
|
|
6699
|
+
function saveBase64Image(base64Data, baseDir) {
|
|
6700
|
+
try {
|
|
6701
|
+
// Parse data URL: data:image/png;base64,iVBORw0K...
|
|
6702
|
+
const match = base64Data.match(/^data:image\/(\w+);base64,(.+)$/);
|
|
6703
|
+
if (!match) return null;
|
|
6704
|
+
|
|
6705
|
+
const ext = match[1] === 'jpeg' ? 'jpg' : match[1];
|
|
6706
|
+
const data = match[2];
|
|
6707
|
+
|
|
6708
|
+
// Create ./tmp/ directory if it doesn't exist
|
|
6709
|
+
const tmpDir = path.join(baseDir, 'tmp');
|
|
6710
|
+
if (!fs.existsSync(tmpDir)) {
|
|
6711
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
6712
|
+
}
|
|
6713
|
+
|
|
6714
|
+
// Generate unique filename using timestamp and random string
|
|
6715
|
+
const timestamp = Date.now();
|
|
6716
|
+
const random = crypto.randomBytes(4).toString('hex');
|
|
6717
|
+
const filename = `reviw-${timestamp}-${random}.${ext}`;
|
|
6718
|
+
const filepath = path.join(tmpDir, filename);
|
|
6719
|
+
|
|
6720
|
+
// Decode base64 and save to file
|
|
6721
|
+
const buffer = Buffer.from(data, 'base64');
|
|
6722
|
+
fs.writeFileSync(filepath, buffer);
|
|
6723
|
+
|
|
6724
|
+
// Return relative path (./tmp/filename)
|
|
6725
|
+
return `./tmp/${filename}`;
|
|
6726
|
+
} catch (err) {
|
|
6727
|
+
console.error('Failed to save image:', err);
|
|
6728
|
+
return null;
|
|
6729
|
+
}
|
|
6730
|
+
}
|
|
6731
|
+
|
|
6732
|
+
// Process payload images: save to disk and replace base64 with paths
|
|
6733
|
+
function processPayloadImages(payload, baseDir) {
|
|
6734
|
+
// Process summaryImages (images attached to summary)
|
|
6735
|
+
if (payload.summaryImages && Array.isArray(payload.summaryImages)) {
|
|
6736
|
+
const imagePaths = [];
|
|
6737
|
+
for (const base64 of payload.summaryImages) {
|
|
6738
|
+
const savedPath = saveBase64Image(base64, baseDir);
|
|
6739
|
+
if (savedPath) {
|
|
6740
|
+
imagePaths.push(savedPath);
|
|
6741
|
+
}
|
|
6742
|
+
}
|
|
6743
|
+
// Replace base64 array with file paths (keeps same key position)
|
|
6744
|
+
payload.summaryImages = imagePaths.length > 0 ? imagePaths : undefined;
|
|
6745
|
+
if (!payload.summaryImages) delete payload.summaryImages;
|
|
6746
|
+
}
|
|
6747
|
+
|
|
6748
|
+
// Process comment images
|
|
6749
|
+
if (payload.comments && Array.isArray(payload.comments)) {
|
|
6750
|
+
for (const comment of payload.comments) {
|
|
6751
|
+
if (comment.image) {
|
|
6752
|
+
const savedPath = saveBase64Image(comment.image, baseDir);
|
|
6753
|
+
if (savedPath) {
|
|
6754
|
+
comment.imagePath = savedPath;
|
|
6755
|
+
}
|
|
6756
|
+
delete comment.image; // Remove base64 data from comment
|
|
6757
|
+
}
|
|
6758
|
+
}
|
|
6759
|
+
}
|
|
6760
|
+
|
|
6761
|
+
// Add image reading instruction if any images are attached
|
|
6762
|
+
const hasCommentImages = payload.comments?.some(c => c.imagePath);
|
|
6763
|
+
const hasSummaryImages = payload.summaryImages?.length > 0;
|
|
6764
|
+
if (hasCommentImages || hasSummaryImages) {
|
|
6765
|
+
payload._imageReadingNote = "MANDATORY: You MUST read ALL images (imagePath and summaryImages) using the Read tool. Skipping image reading is PROHIBITED.";
|
|
6766
|
+
}
|
|
6767
|
+
|
|
6768
|
+
return payload;
|
|
6769
|
+
}
|
|
6770
|
+
|
|
6499
6771
|
function checkExistingServer(filePath) {
|
|
6500
6772
|
try {
|
|
6501
6773
|
const lockPath = getLockFilePath(filePath);
|
|
@@ -6871,6 +7143,10 @@ function createFileServer(filePath, fileIndex = 0) {
|
|
|
6871
7143
|
if (raw && raw.trim()) {
|
|
6872
7144
|
payload = JSON.parse(raw);
|
|
6873
7145
|
}
|
|
7146
|
+
// Process images: save to ./tmp/ and replace base64 with paths
|
|
7147
|
+
if (payload) {
|
|
7148
|
+
payload = processPayloadImages(payload, ctx.baseDir);
|
|
7149
|
+
}
|
|
6874
7150
|
// Save to file-based history (only if there are comments)
|
|
6875
7151
|
if (payload && (payload.comments?.length > 0 || payload.submitComment)) {
|
|
6876
7152
|
saveHistoryToFile(ctx.filePath, payload);
|
|
@@ -7137,6 +7413,11 @@ function createDiffServer(diffContent) {
|
|
|
7137
7413
|
if (raw && raw.trim()) {
|
|
7138
7414
|
payload = JSON.parse(raw);
|
|
7139
7415
|
}
|
|
7416
|
+
// Process images: save to ./tmp/ and replace base64 with paths
|
|
7417
|
+
// For diff mode, use current working directory
|
|
7418
|
+
if (payload) {
|
|
7419
|
+
payload = processPayloadImages(payload, process.cwd());
|
|
7420
|
+
}
|
|
7140
7421
|
// Save to file-based history (only if there are comments)
|
|
7141
7422
|
// For diff mode, use relativePath as identifier
|
|
7142
7423
|
if (payload && (payload.comments?.length > 0 || payload.submitComment)) {
|