git-reviewer 2.7.0 → 2.8.1
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.md +2 -2
- package/package.json +1 -1
- package/public/app.js +120 -15
- package/public/index.html +2 -1
- package/public/style.css +20 -0
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://github.com/dheerajjha/reviewer/actions/workflows/ci.yml)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](package.json)
|
|
6
|
-
[](test/)
|
|
7
7
|
[](package.json)
|
|
8
8
|
|
|
9
9
|
When an agent writes the code, reading it becomes the bottleneck — and the tool
|
|
@@ -294,7 +294,7 @@ rather than a public issue.
|
|
|
294
294
|
|
|
295
295
|
```bash
|
|
296
296
|
npm install # 3 dependencies, no build step, ~6MB
|
|
297
|
-
npm test #
|
|
297
|
+
npm test # 285 tests
|
|
298
298
|
npm run test:watch
|
|
299
299
|
npm run test:coverage
|
|
300
300
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-reviewer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.1",
|
|
4
4
|
"description": "Review your coding agent's changes like a pull request — locally, with inline comments and threaded follow-ups, then hand the marked-up review back to the agent",
|
|
5
5
|
"author": "Dheeraj Jha",
|
|
6
6
|
"license": "MIT",
|
package/public/app.js
CHANGED
|
@@ -222,6 +222,17 @@ async function loadRepo() {
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
// Display list of changed files
|
|
225
|
+
function fileStatusClass(status) {
|
|
226
|
+
switch (status) {
|
|
227
|
+
case 'A': return 'file-status-added';
|
|
228
|
+
case 'D': return 'file-status-deleted';
|
|
229
|
+
case 'R': return 'file-status-renamed';
|
|
230
|
+
case 'B': return 'file-status-binary';
|
|
231
|
+
case 'M':
|
|
232
|
+
default: return 'file-status-modified';
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
225
236
|
function displayFiles(files) {
|
|
226
237
|
const sidebar = document.getElementById('sidebar');
|
|
227
238
|
const filesList = document.getElementById('filesList');
|
|
@@ -244,7 +255,7 @@ function displayFiles(files) {
|
|
|
244
255
|
const displayText = path
|
|
245
256
|
? `${escapeHtml(path)}/<span class="filename">${escapeHtml(filename)}</span>`
|
|
246
257
|
: `<span class="filename">${escapeHtml(filename)}</span>`;
|
|
247
|
-
const statusClass = status
|
|
258
|
+
const statusClass = fileStatusClass(status);
|
|
248
259
|
const statusBadge = `<span class="file-status ${statusClass}">${status}</span>`;
|
|
249
260
|
|
|
250
261
|
// The row used to carry the file name itself, inside a string literal
|
|
@@ -426,7 +437,7 @@ function displayCode(filePath, diffLines, binary) {
|
|
|
426
437
|
const newLineClick = diffLine.type !== 'delete' ? `onclick="toggleCommentInput(${Number(diffLineIndex)})"` : '';
|
|
427
438
|
|
|
428
439
|
html += `
|
|
429
|
-
<div class="${lineClass}" data-diff-index="${diffLineIndex}">
|
|
440
|
+
<div class="${lineClass}" tabindex="0" data-diff-index="${diffLineIndex}">
|
|
430
441
|
${hasComment ? '<span class="comment-indicator"></span>' : ''}
|
|
431
442
|
<div class="line-numbers">
|
|
432
443
|
<span class="old-line-number" ${oldLineClick}>${diffLine.oldLine || ''}</span>
|
|
@@ -506,7 +517,7 @@ function displayCode(filePath, diffLines, binary) {
|
|
|
506
517
|
// rendered from. The row's random id went with it: the position is the
|
|
507
518
|
// identifier now.
|
|
508
519
|
html += `
|
|
509
|
-
<div class="unmatched-comment-item collapsed">
|
|
520
|
+
<div class="unmatched-comment-item collapsed" tabindex="0">
|
|
510
521
|
<div class="unmatched-comment-header" onclick="toggleUnmatchedComment(${Number(unmatchedIndex)})">
|
|
511
522
|
<span class="unmatched-comment-icon">⚠️</span>
|
|
512
523
|
<span class="unmatched-comment-title">Comment not found (Line ${comment.line} changed)</span>
|
|
@@ -1357,7 +1368,7 @@ function displayFullContext(filePath, lines) {
|
|
|
1357
1368
|
const dataDiffIndexAttr = diffLineIndex !== undefined ? `data-diff-index="${diffLineIndex}"` : '';
|
|
1358
1369
|
|
|
1359
1370
|
html += `
|
|
1360
|
-
<div class="${lineClass}" ${dataDiffIndexAttr}>
|
|
1371
|
+
<div class="${lineClass}" tabindex="0" ${dataDiffIndexAttr}>
|
|
1361
1372
|
${hasComment ? '<span class="comment-indicator"></span>' : ''}
|
|
1362
1373
|
<div class="line-numbers">
|
|
1363
1374
|
<span class="old-line-number">${diffLine.oldLine || ''}</span>
|
|
@@ -1636,6 +1647,76 @@ function navigateFiles(direction) {
|
|
|
1636
1647
|
}
|
|
1637
1648
|
}
|
|
1638
1649
|
|
|
1650
|
+
// Navigate to comments in current file
|
|
1651
|
+
function navigateComments(direction) {
|
|
1652
|
+
const commentElements = Array.from(document.querySelectorAll('.line.commented, .unmatched-comment-item'));
|
|
1653
|
+
if (commentElements.length === 0) return;
|
|
1654
|
+
|
|
1655
|
+
const active = document.activeElement;
|
|
1656
|
+
const currentIndex = commentElements.findIndex(el => el === active || el.contains(active));
|
|
1657
|
+
|
|
1658
|
+
let newIndex = currentIndex;
|
|
1659
|
+
if (currentIndex === -1) {
|
|
1660
|
+
newIndex = direction === 'next' ? 0 : commentElements.length - 1;
|
|
1661
|
+
} else {
|
|
1662
|
+
// stop at boundaries, matching existing file navigation style
|
|
1663
|
+
if (direction === 'next') {
|
|
1664
|
+
newIndex = Math.min(currentIndex + 1, commentElements.length - 1);
|
|
1665
|
+
} else {
|
|
1666
|
+
newIndex = Math.max(currentIndex - 1, 0);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
if (newIndex !== currentIndex && commentElements[newIndex]) {
|
|
1671
|
+
const target = commentElements[newIndex];
|
|
1672
|
+
target.focus();
|
|
1673
|
+
target.scrollIntoView({ block: 'nearest' });
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function handleCommentShortcut() {
|
|
1678
|
+
const active = document.activeElement;
|
|
1679
|
+
if (!active) return;
|
|
1680
|
+
const line = active.closest('.line');
|
|
1681
|
+
if (!line) return;
|
|
1682
|
+
const diffIndex = parseInt(line.dataset.diffIndex, 10);
|
|
1683
|
+
if (!isNaN(diffIndex)) {
|
|
1684
|
+
toggleCommentInput(diffIndex);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
/**
|
|
1689
|
+
* Pure function mapping keyboard events to application actions.
|
|
1690
|
+
* @param {string} key
|
|
1691
|
+
* @param {string} targetTagName
|
|
1692
|
+
* @param {{ctrl?: boolean, meta?: boolean, alt?: boolean}} [modifiers]
|
|
1693
|
+
* @returns {string|null} Action to take, or null if ignored.
|
|
1694
|
+
*/
|
|
1695
|
+
function getKeyboardShortcut(key, targetTagName, modifiers = {}) {
|
|
1696
|
+
if (modifiers.ctrl || modifiers.meta || modifiers.alt) return null;
|
|
1697
|
+
|
|
1698
|
+
// Escape means "never mind" everywhere — including inside inputs — so it
|
|
1699
|
+
// must be checked before the typing guard.
|
|
1700
|
+
if (key === 'Escape') return 'escape';
|
|
1701
|
+
|
|
1702
|
+
if (targetTagName === 'INPUT' || targetTagName === 'TEXTAREA') {
|
|
1703
|
+
return null;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
const map = {
|
|
1707
|
+
'ArrowUp': 'prevFile',
|
|
1708
|
+
'k': 'prevFile',
|
|
1709
|
+
'ArrowDown': 'nextFile',
|
|
1710
|
+
'j': 'nextFile',
|
|
1711
|
+
'n': 'nextComment',
|
|
1712
|
+
'p': 'prevComment',
|
|
1713
|
+
'c': 'commentFocus',
|
|
1714
|
+
'?': 'help'
|
|
1715
|
+
};
|
|
1716
|
+
|
|
1717
|
+
return map[key] || null;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1639
1720
|
// Allow Enter key to load repo
|
|
1640
1721
|
document.addEventListener('DOMContentLoaded', () => {
|
|
1641
1722
|
document.getElementById('repoPath').addEventListener('keypress', (e) => {
|
|
@@ -1647,22 +1728,31 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
1647
1728
|
// Initialize resize handle
|
|
1648
1729
|
initResizeHandle();
|
|
1649
1730
|
|
|
1650
|
-
// Add keyboard navigation
|
|
1731
|
+
// Add keyboard navigation
|
|
1651
1732
|
document.addEventListener('keydown', (e) => {
|
|
1652
|
-
|
|
1653
|
-
if (
|
|
1654
|
-
return;
|
|
1655
|
-
}
|
|
1733
|
+
const action = getKeyboardShortcut(e.key, e.target.tagName, { ctrl: e.ctrlKey, meta: e.metaKey, alt: e.altKey });
|
|
1734
|
+
if (!action || action === 'escape') return; // escape is handled separately
|
|
1656
1735
|
|
|
1657
|
-
if (
|
|
1736
|
+
if (action === 'prevFile') {
|
|
1658
1737
|
e.preventDefault();
|
|
1659
1738
|
navigateFiles('up');
|
|
1660
|
-
} else if (
|
|
1739
|
+
} else if (action === 'nextFile') {
|
|
1661
1740
|
e.preventDefault();
|
|
1662
1741
|
navigateFiles('down');
|
|
1742
|
+
} else if (action === 'nextComment') {
|
|
1743
|
+
e.preventDefault();
|
|
1744
|
+
navigateComments('next');
|
|
1745
|
+
} else if (action === 'prevComment') {
|
|
1746
|
+
e.preventDefault();
|
|
1747
|
+
navigateComments('prev');
|
|
1748
|
+
} else if (action === 'commentFocus') {
|
|
1749
|
+
e.preventDefault();
|
|
1750
|
+
handleCommentShortcut();
|
|
1751
|
+
} else if (action === 'help') {
|
|
1752
|
+
e.preventDefault();
|
|
1753
|
+
document.getElementById('helpButton').click();
|
|
1663
1754
|
}
|
|
1664
1755
|
});
|
|
1665
|
-
|
|
1666
1756
|
for (const id of ['recentList', 'browseList']) {
|
|
1667
1757
|
const list = document.getElementById(id);
|
|
1668
1758
|
list.addEventListener('click', onPickerActivate);
|
|
@@ -1672,13 +1762,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
1672
1762
|
// Escape, and a click on the dimmed area around it, both mean "never mind"
|
|
1673
1763
|
// -- but only when there is a review behind the picker to go back to.
|
|
1674
1764
|
document.addEventListener('keydown', event => {
|
|
1675
|
-
if (event.key !== '
|
|
1765
|
+
if (getKeyboardShortcut(event.key, event.target.tagName, { ctrl: event.ctrlKey, meta: event.metaKey, alt: event.altKey }) !== 'escape') {
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1676
1768
|
|
|
1677
1769
|
// Topmost first: the help sits over the picker, which sits over a review.
|
|
1678
1770
|
if (!document.getElementById('helpModal').classList.contains('hidden')) {
|
|
1679
1771
|
closeHelp();
|
|
1680
1772
|
return;
|
|
1681
1773
|
}
|
|
1774
|
+
|
|
1775
|
+
const reviewModal = document.querySelector('.review-modal');
|
|
1776
|
+
if (reviewModal) {
|
|
1777
|
+
reviewModal.remove();
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1682
1781
|
closePicker();
|
|
1683
1782
|
});
|
|
1684
1783
|
|
|
@@ -1817,8 +1916,8 @@ async function loadRecent() {
|
|
|
1817
1916
|
.map(project => {
|
|
1818
1917
|
const meta = project.exists
|
|
1819
1918
|
? [project.comments > 0 ? `${project.comments} comment${project.comments === 1 ? '' : 's'}` : '', timeAgo(project.openedAt)]
|
|
1820
|
-
|
|
1821
|
-
|
|
1919
|
+
.filter(Boolean)
|
|
1920
|
+
.join(' · ')
|
|
1822
1921
|
: 'no longer there';
|
|
1823
1922
|
|
|
1824
1923
|
return `
|
|
@@ -1972,3 +2071,9 @@ function offerHelpOnce() {
|
|
|
1972
2071
|
if (helpWasSeen()) return;
|
|
1973
2072
|
openHelp();
|
|
1974
2073
|
}
|
|
2074
|
+
|
|
2075
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
2076
|
+
module.exports = {
|
|
2077
|
+
getKeyboardShortcut
|
|
2078
|
+
};
|
|
2079
|
+
}
|
package/public/index.html
CHANGED
|
@@ -149,7 +149,8 @@
|
|
|
149
149
|
<div class="help-foot">
|
|
150
150
|
<span class="help-keys">
|
|
151
151
|
<kbd>⌘</kbd><kbd>Enter</kbd> save · <kbd>Esc</kbd> cancel ·
|
|
152
|
-
<kbd>↑</kbd><kbd>↓</kbd>
|
|
152
|
+
<kbd>↑</kbd><kbd>↓</kbd><kbd>j</kbd><kbd>k</kbd> files ·
|
|
153
|
+
<kbd>n</kbd><kbd>p</kbd> comments · <kbd>c</kbd> new comment · <kbd>?</kbd> help
|
|
153
154
|
</span>
|
|
154
155
|
<button class="help-done" onclick="closeHelp()">Got it</button>
|
|
155
156
|
</div>
|
package/public/style.css
CHANGED
|
@@ -221,6 +221,21 @@ button:disabled {
|
|
|
221
221
|
color: white;
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
.file-status-deleted {
|
|
225
|
+
background: #d73a49;
|
|
226
|
+
color: white;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.file-status-renamed {
|
|
230
|
+
background: #6f42c1;
|
|
231
|
+
color: white;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.file-status-binary {
|
|
235
|
+
background: #6a737d;
|
|
236
|
+
color: white;
|
|
237
|
+
}
|
|
238
|
+
|
|
224
239
|
.file-item:hover {
|
|
225
240
|
background: #f3f4f6;
|
|
226
241
|
border-left-color: #0366d6;
|
|
@@ -441,6 +456,11 @@ button:disabled {
|
|
|
441
456
|
background: rgba(0, 0, 0, 0.03);
|
|
442
457
|
}
|
|
443
458
|
|
|
459
|
+
.line:focus, .unmatched-comment-item:focus {
|
|
460
|
+
outline: 2px solid #0366d6;
|
|
461
|
+
outline-offset: -2px;
|
|
462
|
+
}
|
|
463
|
+
|
|
444
464
|
/* Diff-specific styling */
|
|
445
465
|
.diff-add {
|
|
446
466
|
background: #e6ffed;
|