acdev 1.0.0 → 1.0.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/package.json +3 -3
- package/public/app.js +277 -26
- package/public/index.html +22 -5
- package/public/styles.css +50 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "acdev",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Local CLI + web UI for running AI agents on GitHub issues via git worktrees",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@anthropic-ai/claude-agent-sdk": "0.1.77",
|
|
25
|
+
"acdev": "^1.0.0",
|
|
25
26
|
"dotenv": "^17.4.2",
|
|
26
27
|
"express": "^4.21.2",
|
|
27
28
|
"open": "^10.1.0",
|
|
@@ -51,6 +52,5 @@
|
|
|
51
52
|
"main": "index.js",
|
|
52
53
|
"directories": {
|
|
53
54
|
"test": "test"
|
|
54
|
-
}
|
|
55
|
-
"devDependencies": {}
|
|
55
|
+
}
|
|
56
56
|
}
|
package/public/app.js
CHANGED
|
@@ -106,9 +106,14 @@ let reviewFilter = loadReviewFilter();
|
|
|
106
106
|
let reviewSearch = loadReviewSearch();
|
|
107
107
|
/** @type {string | null} */
|
|
108
108
|
let activeInlineCommentKey = null;
|
|
109
|
+
/** @type {string | null} id of line comment currently being edited */
|
|
110
|
+
let editingCommentId = null;
|
|
109
111
|
/** @type {Record<string, {
|
|
110
112
|
* generalComment: string,
|
|
111
|
-
* lineComments: Array<{ id: string, path: string, line: number, side: 'LEFT' | 'RIGHT', body: string }
|
|
113
|
+
* lineComments: Array<{ id: string, path: string, line: number, side: 'LEFT' | 'RIGHT', body: string }>,
|
|
114
|
+
* prTitle: string | null,
|
|
115
|
+
* prBody: string | null,
|
|
116
|
+
* prMetaDirty: boolean,
|
|
112
117
|
* }>} */
|
|
113
118
|
let reviewDrafts = {};
|
|
114
119
|
/** @type {Record<string, string[]>} changed file paths per job (from API or parsed diff) */
|
|
@@ -199,6 +204,8 @@ const els = {
|
|
|
199
204
|
reviewFilesDeselectAll: document.getElementById('review-files-deselect-all'),
|
|
200
205
|
diffHeader: document.getElementById('diff-header'),
|
|
201
206
|
diffViewer: document.getElementById('diff-viewer'),
|
|
207
|
+
diffSection: document.getElementById('diff-section'),
|
|
208
|
+
diffFullscreenBtn: document.getElementById('diff-fullscreen-btn'),
|
|
202
209
|
approveDraftBtn: document.getElementById('approve-draft-btn'),
|
|
203
210
|
approveReadyBtn: document.getElementById('approve-ready-btn'),
|
|
204
211
|
rejectBtn: document.getElementById('reject-btn'),
|
|
@@ -294,11 +301,20 @@ function reviewDraftStorageKey(jobId) {
|
|
|
294
301
|
* @param {string} jobId
|
|
295
302
|
* @returns {{
|
|
296
303
|
* generalComment: string,
|
|
297
|
-
* lineComments: Array<{ id: string, path: string, line: number, side: 'LEFT' | 'RIGHT', body: string }
|
|
304
|
+
* lineComments: Array<{ id: string, path: string, line: number, side: 'LEFT' | 'RIGHT', body: string }>,
|
|
305
|
+
* prTitle: string | null,
|
|
306
|
+
* prBody: string | null,
|
|
307
|
+
* prMetaDirty: boolean,
|
|
298
308
|
* }}
|
|
299
309
|
*/
|
|
300
310
|
function emptyReviewDraft() {
|
|
301
|
-
return {
|
|
311
|
+
return {
|
|
312
|
+
generalComment: '',
|
|
313
|
+
lineComments: [],
|
|
314
|
+
prTitle: null,
|
|
315
|
+
prBody: null,
|
|
316
|
+
prMetaDirty: false,
|
|
317
|
+
};
|
|
302
318
|
}
|
|
303
319
|
|
|
304
320
|
/** @param {string} jobId */
|
|
@@ -329,6 +345,9 @@ function loadReviewDraft(jobId) {
|
|
|
329
345
|
body: c.body,
|
|
330
346
|
}))
|
|
331
347
|
: [],
|
|
348
|
+
prTitle: typeof parsed?.prTitle === 'string' ? parsed.prTitle : null,
|
|
349
|
+
prBody: typeof parsed?.prBody === 'string' ? parsed.prBody : null,
|
|
350
|
+
prMetaDirty: Boolean(parsed?.prMetaDirty),
|
|
332
351
|
};
|
|
333
352
|
reviewDrafts[jobId] = draft;
|
|
334
353
|
return draft;
|
|
@@ -352,10 +371,82 @@ function saveReviewDraft(jobId) {
|
|
|
352
371
|
}
|
|
353
372
|
}
|
|
354
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Resolve editable PR title/body for a job (local draft wins when dirty).
|
|
376
|
+
* @param {{ id: string, prTitle?: string, prBody?: string }} job
|
|
377
|
+
*/
|
|
378
|
+
function resolvePrMeta(job) {
|
|
379
|
+
const draft = loadReviewDraft(job.id);
|
|
380
|
+
if (!draft.prMetaDirty) {
|
|
381
|
+
return {
|
|
382
|
+
prTitle: job.prTitle || '',
|
|
383
|
+
prBody: job.prBody || '',
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
prTitle: draft.prTitle != null ? draft.prTitle : job.prTitle || '',
|
|
388
|
+
prBody: draft.prBody != null ? draft.prBody : job.prBody || '',
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Persist PR title/body edits from the form into the review draft.
|
|
394
|
+
* @param {string} jobId
|
|
395
|
+
*/
|
|
396
|
+
function persistPrMetaFromForm(jobId) {
|
|
397
|
+
if (!els.prTitle || !els.prBody) return;
|
|
398
|
+
const draft = loadReviewDraft(jobId);
|
|
399
|
+
draft.prTitle = els.prTitle.value;
|
|
400
|
+
draft.prBody = els.prBody.value;
|
|
401
|
+
draft.prMetaDirty = true;
|
|
402
|
+
saveReviewDraft(jobId);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Keep PR title/body across comment-only draft clears (submit review).
|
|
407
|
+
* @param {string} jobId
|
|
408
|
+
*/
|
|
409
|
+
function clearReviewCommentDraft(jobId) {
|
|
410
|
+
const prev = loadReviewDraft(jobId);
|
|
411
|
+
const kept = {
|
|
412
|
+
prTitle: prev.prTitle,
|
|
413
|
+
prBody: prev.prBody,
|
|
414
|
+
prMetaDirty: prev.prMetaDirty,
|
|
415
|
+
};
|
|
416
|
+
clearReviewDraft(jobId);
|
|
417
|
+
if (kept.prMetaDirty) {
|
|
418
|
+
const draft = loadReviewDraft(jobId);
|
|
419
|
+
draft.prTitle = kept.prTitle;
|
|
420
|
+
draft.prBody = kept.prBody;
|
|
421
|
+
draft.prMetaDirty = true;
|
|
422
|
+
saveReviewDraft(jobId);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function isDiffFullscreen() {
|
|
427
|
+
return Boolean(els.diffSection?.classList.contains('diff-fullscreen'));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** @param {boolean} on */
|
|
431
|
+
function setDiffFullscreen(on) {
|
|
432
|
+
if (!els.diffSection) return;
|
|
433
|
+
els.diffSection.classList.toggle('diff-fullscreen', on);
|
|
434
|
+
document.body.classList.toggle('diff-fs-open', on);
|
|
435
|
+
if (els.diffFullscreenBtn) {
|
|
436
|
+
els.diffFullscreenBtn.textContent = on ? 'Exit fullscreen' : 'Fullscreen';
|
|
437
|
+
els.diffFullscreenBtn.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function toggleDiffFullscreen() {
|
|
442
|
+
setDiffFullscreen(!isDiffFullscreen());
|
|
443
|
+
}
|
|
444
|
+
|
|
355
445
|
/** @param {string} jobId */
|
|
356
446
|
function clearReviewDraft(jobId) {
|
|
357
447
|
delete reviewDrafts[jobId];
|
|
358
448
|
activeInlineCommentKey = null;
|
|
449
|
+
editingCommentId = null;
|
|
359
450
|
try {
|
|
360
451
|
sessionStorage.removeItem(reviewDraftStorageKey(jobId));
|
|
361
452
|
} catch {
|
|
@@ -363,6 +454,70 @@ function clearReviewDraft(jobId) {
|
|
|
363
454
|
}
|
|
364
455
|
}
|
|
365
456
|
|
|
457
|
+
/**
|
|
458
|
+
* Re-render diff + pending list after draft mutation.
|
|
459
|
+
* @param {string} jobId
|
|
460
|
+
*/
|
|
461
|
+
function refreshReviewCommentUi(jobId) {
|
|
462
|
+
const job = jobsById[jobId];
|
|
463
|
+
if (job) {
|
|
464
|
+
const diff =
|
|
465
|
+
job.status === 'awaiting_review'
|
|
466
|
+
? filterDiffForReviewJob(job)
|
|
467
|
+
: job.diff || '';
|
|
468
|
+
renderDiff(diff, {
|
|
469
|
+
interactive: job.status === 'awaiting_review',
|
|
470
|
+
jobId,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
renderPendingCommentList(jobId);
|
|
474
|
+
syncSubmitReviewButton(jobId);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* @param {{
|
|
479
|
+
* initialBody: string,
|
|
480
|
+
* saveLabel?: string,
|
|
481
|
+
* onSave: (body: string) => void,
|
|
482
|
+
* onCancel: () => void,
|
|
483
|
+
* }} opts
|
|
484
|
+
*/
|
|
485
|
+
function buildCommentEditForm(opts) {
|
|
486
|
+
const form = document.createElement('div');
|
|
487
|
+
form.className = 'diff-inline-form';
|
|
488
|
+
const ta = document.createElement('textarea');
|
|
489
|
+
ta.className = 'input textarea';
|
|
490
|
+
ta.rows = 3;
|
|
491
|
+
ta.value = opts.initialBody;
|
|
492
|
+
ta.placeholder = 'Edit comment…';
|
|
493
|
+
const actions = document.createElement('div');
|
|
494
|
+
actions.className = 'diff-inline-actions';
|
|
495
|
+
const save = document.createElement('button');
|
|
496
|
+
save.type = 'button';
|
|
497
|
+
save.className = 'btn btn-primary btn-sm';
|
|
498
|
+
save.textContent = opts.saveLabel || 'Save';
|
|
499
|
+
const cancel = document.createElement('button');
|
|
500
|
+
cancel.type = 'button';
|
|
501
|
+
cancel.className = 'btn btn-secondary btn-sm';
|
|
502
|
+
cancel.textContent = 'Cancel';
|
|
503
|
+
cancel.addEventListener('click', () => opts.onCancel());
|
|
504
|
+
save.addEventListener('click', () => {
|
|
505
|
+
const body = ta.value.trim();
|
|
506
|
+
if (!body) return;
|
|
507
|
+
opts.onSave(body);
|
|
508
|
+
});
|
|
509
|
+
actions.appendChild(save);
|
|
510
|
+
actions.appendChild(cancel);
|
|
511
|
+
form.appendChild(ta);
|
|
512
|
+
form.appendChild(actions);
|
|
513
|
+
requestAnimationFrame(() => {
|
|
514
|
+
ta.focus();
|
|
515
|
+
const len = ta.value.length;
|
|
516
|
+
ta.setSelectionRange(len, len);
|
|
517
|
+
});
|
|
518
|
+
return form;
|
|
519
|
+
}
|
|
520
|
+
|
|
366
521
|
/** @param {string} jobId */
|
|
367
522
|
function reviewFilesStorageKey(jobId) {
|
|
368
523
|
return `${REVIEW_FILES_PREFIX}${jobId}`;
|
|
@@ -1162,6 +1317,7 @@ function renderDiff(diff, opts = {}) {
|
|
|
1162
1317
|
plus.addEventListener('click', (e) => {
|
|
1163
1318
|
e.stopPropagation();
|
|
1164
1319
|
activeInlineCommentKey = lineCommentKey(path, line, side);
|
|
1320
|
+
editingCommentId = null;
|
|
1165
1321
|
renderDiff(diff, opts);
|
|
1166
1322
|
renderPendingCommentList(jobId);
|
|
1167
1323
|
syncSubmitReviewButton(jobId);
|
|
@@ -1171,6 +1327,7 @@ function renderDiff(diff, opts = {}) {
|
|
|
1171
1327
|
cell.addEventListener('click', (e) => {
|
|
1172
1328
|
if (e.target.closest('button, textarea, a')) return;
|
|
1173
1329
|
activeInlineCommentKey = lineCommentKey(path, line, side);
|
|
1330
|
+
editingCommentId = null;
|
|
1174
1331
|
renderDiff(diff, opts);
|
|
1175
1332
|
renderPendingCommentList(jobId);
|
|
1176
1333
|
syncSubmitReviewButton(jobId);
|
|
@@ -1225,6 +1382,27 @@ function renderDiff(diff, opts = {}) {
|
|
|
1225
1382
|
thread.appendChild(meta);
|
|
1226
1383
|
|
|
1227
1384
|
for (const c of existing) {
|
|
1385
|
+
if (editingCommentId === c.id) {
|
|
1386
|
+
thread.appendChild(
|
|
1387
|
+
buildCommentEditForm({
|
|
1388
|
+
initialBody: c.body,
|
|
1389
|
+
onSave: (body) => {
|
|
1390
|
+
const d = loadReviewDraft(jobId);
|
|
1391
|
+
const target = d.lineComments.find((x) => x.id === c.id);
|
|
1392
|
+
if (target) target.body = body;
|
|
1393
|
+
saveReviewDraft(jobId);
|
|
1394
|
+
editingCommentId = null;
|
|
1395
|
+
refreshReviewCommentUi(jobId);
|
|
1396
|
+
},
|
|
1397
|
+
onCancel: () => {
|
|
1398
|
+
editingCommentId = null;
|
|
1399
|
+
refreshReviewCommentUi(jobId);
|
|
1400
|
+
},
|
|
1401
|
+
})
|
|
1402
|
+
);
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1228
1406
|
const chip = document.createElement('div');
|
|
1229
1407
|
chip.className = 'diff-pending-chip';
|
|
1230
1408
|
const body = document.createElement('div');
|
|
@@ -1232,6 +1410,15 @@ function renderDiff(diff, opts = {}) {
|
|
|
1232
1410
|
body.textContent = c.body;
|
|
1233
1411
|
const actions = document.createElement('div');
|
|
1234
1412
|
actions.className = 'diff-pending-chip-actions';
|
|
1413
|
+
const edit = document.createElement('button');
|
|
1414
|
+
edit.type = 'button';
|
|
1415
|
+
edit.className = 'btn btn-muted-text btn-sm';
|
|
1416
|
+
edit.textContent = 'Edit';
|
|
1417
|
+
edit.addEventListener('click', () => {
|
|
1418
|
+
editingCommentId = c.id;
|
|
1419
|
+
activeInlineCommentKey = null;
|
|
1420
|
+
refreshReviewCommentUi(jobId);
|
|
1421
|
+
});
|
|
1235
1422
|
const remove = document.createElement('button');
|
|
1236
1423
|
remove.type = 'button';
|
|
1237
1424
|
remove.className = 'btn btn-muted-text btn-sm';
|
|
@@ -1240,10 +1427,10 @@ function renderDiff(diff, opts = {}) {
|
|
|
1240
1427
|
const d = loadReviewDraft(jobId);
|
|
1241
1428
|
d.lineComments = d.lineComments.filter((x) => x.id !== c.id);
|
|
1242
1429
|
saveReviewDraft(jobId);
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
syncSubmitReviewButton(jobId);
|
|
1430
|
+
if (editingCommentId === c.id) editingCommentId = null;
|
|
1431
|
+
refreshReviewCommentUi(jobId);
|
|
1246
1432
|
});
|
|
1433
|
+
actions.appendChild(edit);
|
|
1247
1434
|
actions.appendChild(remove);
|
|
1248
1435
|
chip.appendChild(body);
|
|
1249
1436
|
chip.appendChild(actions);
|
|
@@ -1495,11 +1682,46 @@ function renderPendingCommentList(jobId) {
|
|
|
1495
1682
|
const loc = document.createElement('div');
|
|
1496
1683
|
loc.className = 'review-pending-item-loc';
|
|
1497
1684
|
loc.textContent = `${c.path}:${c.line} (${c.side})`;
|
|
1685
|
+
main.appendChild(loc);
|
|
1686
|
+
|
|
1687
|
+
if (editingCommentId === c.id) {
|
|
1688
|
+
main.appendChild(
|
|
1689
|
+
buildCommentEditForm({
|
|
1690
|
+
initialBody: c.body,
|
|
1691
|
+
onSave: (body) => {
|
|
1692
|
+
const d = loadReviewDraft(jobId);
|
|
1693
|
+
const target = d.lineComments.find((x) => x.id === c.id);
|
|
1694
|
+
if (target) target.body = body;
|
|
1695
|
+
saveReviewDraft(jobId);
|
|
1696
|
+
editingCommentId = null;
|
|
1697
|
+
refreshReviewCommentUi(jobId);
|
|
1698
|
+
},
|
|
1699
|
+
onCancel: () => {
|
|
1700
|
+
editingCommentId = null;
|
|
1701
|
+
refreshReviewCommentUi(jobId);
|
|
1702
|
+
},
|
|
1703
|
+
})
|
|
1704
|
+
);
|
|
1705
|
+
item.appendChild(main);
|
|
1706
|
+
els.reviewPendingList.appendChild(item);
|
|
1707
|
+
continue;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1498
1710
|
const body = document.createElement('div');
|
|
1499
1711
|
body.className = 'review-pending-item-body';
|
|
1500
1712
|
body.textContent = c.body;
|
|
1501
|
-
main.appendChild(loc);
|
|
1502
1713
|
main.appendChild(body);
|
|
1714
|
+
const actions = document.createElement('div');
|
|
1715
|
+
actions.className = 'review-pending-item-actions';
|
|
1716
|
+
const edit = document.createElement('button');
|
|
1717
|
+
edit.type = 'button';
|
|
1718
|
+
edit.className = 'btn btn-muted-text btn-sm';
|
|
1719
|
+
edit.textContent = 'Edit';
|
|
1720
|
+
edit.addEventListener('click', () => {
|
|
1721
|
+
editingCommentId = c.id;
|
|
1722
|
+
activeInlineCommentKey = null;
|
|
1723
|
+
refreshReviewCommentUi(jobId);
|
|
1724
|
+
});
|
|
1503
1725
|
const remove = document.createElement('button');
|
|
1504
1726
|
remove.type = 'button';
|
|
1505
1727
|
remove.className = 'btn btn-muted-text btn-sm';
|
|
@@ -1508,19 +1730,13 @@ function renderPendingCommentList(jobId) {
|
|
|
1508
1730
|
const d = loadReviewDraft(jobId);
|
|
1509
1731
|
d.lineComments = d.lineComments.filter((x) => x.id !== c.id);
|
|
1510
1732
|
saveReviewDraft(jobId);
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
const diff =
|
|
1514
|
-
job.status === 'awaiting_review'
|
|
1515
|
-
? filterDiffForReviewJob(job)
|
|
1516
|
-
: job.diff || '';
|
|
1517
|
-
renderDiff(diff, { interactive: job.status === 'awaiting_review', jobId });
|
|
1518
|
-
}
|
|
1519
|
-
renderPendingCommentList(jobId);
|
|
1520
|
-
syncSubmitReviewButton(jobId);
|
|
1733
|
+
if (editingCommentId === c.id) editingCommentId = null;
|
|
1734
|
+
refreshReviewCommentUi(jobId);
|
|
1521
1735
|
});
|
|
1736
|
+
actions.appendChild(edit);
|
|
1737
|
+
actions.appendChild(remove);
|
|
1522
1738
|
item.appendChild(main);
|
|
1523
|
-
item.appendChild(
|
|
1739
|
+
item.appendChild(actions);
|
|
1524
1740
|
els.reviewPendingList.appendChild(item);
|
|
1525
1741
|
}
|
|
1526
1742
|
}
|
|
@@ -1533,6 +1749,9 @@ function syncSubmitReviewButton(jobId) {
|
|
|
1533
1749
|
}
|
|
1534
1750
|
|
|
1535
1751
|
function setView(view) {
|
|
1752
|
+
if (view !== 'review' && isDiffFullscreen()) {
|
|
1753
|
+
setDiffFullscreen(false);
|
|
1754
|
+
}
|
|
1536
1755
|
currentView = view;
|
|
1537
1756
|
els.viewTitle.textContent = VIEW_TITLES[view] || view;
|
|
1538
1757
|
|
|
@@ -2021,6 +2240,7 @@ function renderReview(jobs) {
|
|
|
2021
2240
|
if (!hasPool) {
|
|
2022
2241
|
els.reviewNoMatches.classList.add('hidden');
|
|
2023
2242
|
els.reviewLayout.classList.add('hidden');
|
|
2243
|
+
if (isDiffFullscreen()) setDiffFullscreen(false);
|
|
2024
2244
|
return;
|
|
2025
2245
|
}
|
|
2026
2246
|
|
|
@@ -2084,9 +2304,10 @@ function renderReview(jobs) {
|
|
|
2084
2304
|
els.reviewPrSection.classList.toggle('hidden', !isOpened);
|
|
2085
2305
|
els.reviewTerminalSection.classList.toggle('hidden', !isTerminal);
|
|
2086
2306
|
|
|
2307
|
+
const prMeta = resolvePrMeta(job);
|
|
2087
2308
|
if (document.activeElement !== els.prTitle && document.activeElement !== els.prBody) {
|
|
2088
|
-
els.prTitle.value =
|
|
2089
|
-
els.prBody.value =
|
|
2309
|
+
els.prTitle.value = prMeta.prTitle;
|
|
2310
|
+
els.prBody.value = prMeta.prBody;
|
|
2090
2311
|
}
|
|
2091
2312
|
|
|
2092
2313
|
if (editable) {
|
|
@@ -2097,7 +2318,12 @@ function renderReview(jobs) {
|
|
|
2097
2318
|
) {
|
|
2098
2319
|
els.reviewGeneralComment.value = draft.generalComment;
|
|
2099
2320
|
}
|
|
2100
|
-
|
|
2321
|
+
const focusInPending = Boolean(
|
|
2322
|
+
els.reviewPendingList?.contains(document.activeElement)
|
|
2323
|
+
);
|
|
2324
|
+
if (!(editingCommentId && focusInPending)) {
|
|
2325
|
+
renderPendingCommentList(job.id);
|
|
2326
|
+
}
|
|
2101
2327
|
syncSubmitReviewButton(job.id);
|
|
2102
2328
|
renderReviewFilesPanel(job);
|
|
2103
2329
|
}
|
|
@@ -2126,7 +2352,9 @@ function renderReview(jobs) {
|
|
|
2126
2352
|
}
|
|
2127
2353
|
|
|
2128
2354
|
const focusInDiff = els.diffViewer?.contains(document.activeElement);
|
|
2129
|
-
|
|
2355
|
+
const preservingDiffCompose =
|
|
2356
|
+
(activeInlineCommentKey || editingCommentId) && focusInDiff;
|
|
2357
|
+
if (!preservingDiffCompose) {
|
|
2130
2358
|
renderDiff(displayDiff, { interactive: editable, jobId: job.id });
|
|
2131
2359
|
}
|
|
2132
2360
|
|
|
@@ -3113,12 +3341,14 @@ async function approve(draft) {
|
|
|
3113
3341
|
b.dataset.busy = '1';
|
|
3114
3342
|
});
|
|
3115
3343
|
try {
|
|
3344
|
+
persistPrMetaFromForm(jobId);
|
|
3345
|
+
const prMeta = resolvePrMeta(jobsById[jobId] || { id: jobId });
|
|
3116
3346
|
const res = await fetch(`/api/jobs/${jobId}/approve`, {
|
|
3117
3347
|
method: 'POST',
|
|
3118
3348
|
headers: { 'Content-Type': 'application/json' },
|
|
3119
3349
|
body: JSON.stringify({
|
|
3120
|
-
prTitle:
|
|
3121
|
-
prBody:
|
|
3350
|
+
prTitle: prMeta.prTitle,
|
|
3351
|
+
prBody: prMeta.prBody,
|
|
3122
3352
|
draft,
|
|
3123
3353
|
excludedPaths,
|
|
3124
3354
|
}),
|
|
@@ -3126,6 +3356,7 @@ async function approve(draft) {
|
|
|
3126
3356
|
if (res.ok) {
|
|
3127
3357
|
clearExcludedPaths(jobId);
|
|
3128
3358
|
delete reviewFileLists[jobId];
|
|
3359
|
+
clearReviewDraft(jobId);
|
|
3129
3360
|
}
|
|
3130
3361
|
await fetchJobs();
|
|
3131
3362
|
if (!res.ok) {
|
|
@@ -3188,7 +3419,7 @@ async function submitReview() {
|
|
|
3188
3419
|
return;
|
|
3189
3420
|
}
|
|
3190
3421
|
|
|
3191
|
-
|
|
3422
|
+
clearReviewCommentDraft(jobId);
|
|
3192
3423
|
if (els.reviewGeneralComment) els.reviewGeneralComment.value = '';
|
|
3193
3424
|
selectedRunId = jobId;
|
|
3194
3425
|
selectedReviewId = null;
|
|
@@ -3234,9 +3465,29 @@ els.reviewGeneralComment?.addEventListener('input', () => {
|
|
|
3234
3465
|
syncSubmitReviewButton(selectedReviewId);
|
|
3235
3466
|
});
|
|
3236
3467
|
|
|
3468
|
+
function onPrMetaInput() {
|
|
3469
|
+
if (!selectedReviewId) return;
|
|
3470
|
+
persistPrMetaFromForm(selectedReviewId);
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
els.prTitle?.addEventListener('input', onPrMetaInput);
|
|
3474
|
+
els.prBody?.addEventListener('input', onPrMetaInput);
|
|
3475
|
+
|
|
3476
|
+
els.diffFullscreenBtn?.addEventListener('click', () => toggleDiffFullscreen());
|
|
3477
|
+
|
|
3478
|
+
document.addEventListener('keydown', (e) => {
|
|
3479
|
+
if (e.key === 'Escape' && isDiffFullscreen()) {
|
|
3480
|
+
e.preventDefault();
|
|
3481
|
+
setDiffFullscreen(false);
|
|
3482
|
+
}
|
|
3483
|
+
});
|
|
3484
|
+
|
|
3237
3485
|
els.rejectBtn.addEventListener('click', async () => {
|
|
3238
3486
|
if (!selectedReviewId) return;
|
|
3239
|
-
|
|
3487
|
+
const jobId = selectedReviewId;
|
|
3488
|
+
await fetch(`/api/jobs/${jobId}/reject`, { method: 'POST' });
|
|
3489
|
+
clearReviewDraft(jobId);
|
|
3490
|
+
if (isDiffFullscreen()) setDiffFullscreen(false);
|
|
3240
3491
|
selectedReviewId = null;
|
|
3241
3492
|
await fetchJobs();
|
|
3242
3493
|
});
|
package/public/index.html
CHANGED
|
@@ -185,11 +185,23 @@
|
|
|
185
185
|
<div class="review-detail" id="review-detail">
|
|
186
186
|
<div class="empty-dashed" id="review-empty-detail">Select a job to review</div>
|
|
187
187
|
<div class="stack hidden" id="review-detail-content">
|
|
188
|
-
<div class="card">
|
|
188
|
+
<div class="card" id="review-pr-meta-card">
|
|
189
189
|
<div class="field-label">PR title</div>
|
|
190
|
-
<input
|
|
190
|
+
<input
|
|
191
|
+
id="pr-title"
|
|
192
|
+
class="input input-title"
|
|
193
|
+
type="text"
|
|
194
|
+
placeholder="Pull request title"
|
|
195
|
+
autocomplete="off"
|
|
196
|
+
>
|
|
191
197
|
<div class="field-label field-label-spaced">Description</div>
|
|
192
|
-
<textarea
|
|
198
|
+
<textarea
|
|
199
|
+
id="pr-body"
|
|
200
|
+
class="input textarea"
|
|
201
|
+
rows="4"
|
|
202
|
+
placeholder="Pull request description…"
|
|
203
|
+
></textarea>
|
|
204
|
+
<p class="field-hint" id="pr-meta-hint">Editable before approve. Edits stick across refreshes until you approve or reject.</p>
|
|
193
205
|
</div>
|
|
194
206
|
|
|
195
207
|
<div class="card" id="review-files-section">
|
|
@@ -204,8 +216,13 @@
|
|
|
204
216
|
<div id="review-files-list" class="review-files-list"></div>
|
|
205
217
|
</div>
|
|
206
218
|
|
|
207
|
-
<div>
|
|
208
|
-
<div class="section-
|
|
219
|
+
<div id="diff-section" class="diff-section">
|
|
220
|
+
<div class="diff-section-bar">
|
|
221
|
+
<div class="section-label" id="diff-header">Diff</div>
|
|
222
|
+
<button type="button" class="btn btn-muted-text btn-sm" id="diff-fullscreen-btn" aria-pressed="false">
|
|
223
|
+
Fullscreen
|
|
224
|
+
</button>
|
|
225
|
+
</div>
|
|
209
226
|
<div id="diff-viewer" class="diff-viewer amcp-scroll"></div>
|
|
210
227
|
</div>
|
|
211
228
|
|
package/public/styles.css
CHANGED
|
@@ -1233,6 +1233,24 @@ a { color: var(--primary); text-underline-offset: 3px; }
|
|
|
1233
1233
|
}
|
|
1234
1234
|
|
|
1235
1235
|
/* —— Diff (side-by-side, GitHub-style column panes) —— */
|
|
1236
|
+
.diff-section {
|
|
1237
|
+
display: flex;
|
|
1238
|
+
flex-direction: column;
|
|
1239
|
+
gap: 8px;
|
|
1240
|
+
min-width: 0;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
.diff-section-bar {
|
|
1244
|
+
display: flex;
|
|
1245
|
+
align-items: center;
|
|
1246
|
+
justify-content: space-between;
|
|
1247
|
+
gap: 12px;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
.diff-section-bar .section-label {
|
|
1251
|
+
margin: 0;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1236
1254
|
.diff-viewer {
|
|
1237
1255
|
border: 1px solid var(--border);
|
|
1238
1256
|
border-radius: 10px;
|
|
@@ -1244,6 +1262,28 @@ a { color: var(--primary); text-underline-offset: 3px; }
|
|
|
1244
1262
|
background: var(--surface);
|
|
1245
1263
|
}
|
|
1246
1264
|
|
|
1265
|
+
.diff-section.diff-fullscreen {
|
|
1266
|
+
position: fixed;
|
|
1267
|
+
inset: 0;
|
|
1268
|
+
z-index: 1200;
|
|
1269
|
+
margin: 0;
|
|
1270
|
+
padding: 14px 16px 16px;
|
|
1271
|
+
background: var(--bg);
|
|
1272
|
+
gap: 10px;
|
|
1273
|
+
box-sizing: border-box;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
.diff-section.diff-fullscreen .diff-viewer {
|
|
1277
|
+
max-height: none;
|
|
1278
|
+
flex: 1 1 auto;
|
|
1279
|
+
min-height: 0;
|
|
1280
|
+
border-radius: 12px;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
body.diff-fs-open {
|
|
1284
|
+
overflow: hidden;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1247
1287
|
.diff-file + .diff-file {
|
|
1248
1288
|
border-top: 1px solid var(--border);
|
|
1249
1289
|
}
|
|
@@ -1439,6 +1479,9 @@ a { color: var(--primary); text-underline-offset: 3px; }
|
|
|
1439
1479
|
|
|
1440
1480
|
.diff-pending-chip-actions {
|
|
1441
1481
|
margin-top: 6px;
|
|
1482
|
+
display: flex;
|
|
1483
|
+
gap: 4px;
|
|
1484
|
+
flex-wrap: wrap;
|
|
1442
1485
|
}
|
|
1443
1486
|
|
|
1444
1487
|
.review-pending-list {
|
|
@@ -1469,6 +1512,13 @@ a { color: var(--primary); text-underline-offset: 3px; }
|
|
|
1469
1512
|
flex: 1;
|
|
1470
1513
|
}
|
|
1471
1514
|
|
|
1515
|
+
.review-pending-item-actions {
|
|
1516
|
+
display: flex;
|
|
1517
|
+
flex-direction: column;
|
|
1518
|
+
gap: 2px;
|
|
1519
|
+
flex-shrink: 0;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1472
1522
|
.review-pending-item-loc {
|
|
1473
1523
|
font-size: 11px;
|
|
1474
1524
|
font-weight: 600;
|