acdev 1.0.0 → 1.0.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.
- package/package.json +3 -3
- package/public/app.js +354 -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.2",
|
|
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,18 @@ 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;
|
|
111
|
+
/** @type {'inline' | 'pending' | null} where the edit form should render */
|
|
112
|
+
let editingCommentSurface = null;
|
|
113
|
+
/** Original body when edit started — restored on Cancel after live sync. */
|
|
114
|
+
let editingCommentOriginalBody = null;
|
|
109
115
|
/** @type {Record<string, {
|
|
110
116
|
* generalComment: string,
|
|
111
|
-
* lineComments: Array<{ id: string, path: string, line: number, side: 'LEFT' | 'RIGHT', body: string }
|
|
117
|
+
* lineComments: Array<{ id: string, path: string, line: number, side: 'LEFT' | 'RIGHT', body: string }>,
|
|
118
|
+
* prTitle: string | null,
|
|
119
|
+
* prBody: string | null,
|
|
120
|
+
* prMetaDirty: boolean,
|
|
112
121
|
* }>} */
|
|
113
122
|
let reviewDrafts = {};
|
|
114
123
|
/** @type {Record<string, string[]>} changed file paths per job (from API or parsed diff) */
|
|
@@ -199,6 +208,8 @@ const els = {
|
|
|
199
208
|
reviewFilesDeselectAll: document.getElementById('review-files-deselect-all'),
|
|
200
209
|
diffHeader: document.getElementById('diff-header'),
|
|
201
210
|
diffViewer: document.getElementById('diff-viewer'),
|
|
211
|
+
diffSection: document.getElementById('diff-section'),
|
|
212
|
+
diffFullscreenBtn: document.getElementById('diff-fullscreen-btn'),
|
|
202
213
|
approveDraftBtn: document.getElementById('approve-draft-btn'),
|
|
203
214
|
approveReadyBtn: document.getElementById('approve-ready-btn'),
|
|
204
215
|
rejectBtn: document.getElementById('reject-btn'),
|
|
@@ -294,11 +305,20 @@ function reviewDraftStorageKey(jobId) {
|
|
|
294
305
|
* @param {string} jobId
|
|
295
306
|
* @returns {{
|
|
296
307
|
* generalComment: string,
|
|
297
|
-
* lineComments: Array<{ id: string, path: string, line: number, side: 'LEFT' | 'RIGHT', body: string }
|
|
308
|
+
* lineComments: Array<{ id: string, path: string, line: number, side: 'LEFT' | 'RIGHT', body: string }>,
|
|
309
|
+
* prTitle: string | null,
|
|
310
|
+
* prBody: string | null,
|
|
311
|
+
* prMetaDirty: boolean,
|
|
298
312
|
* }}
|
|
299
313
|
*/
|
|
300
314
|
function emptyReviewDraft() {
|
|
301
|
-
return {
|
|
315
|
+
return {
|
|
316
|
+
generalComment: '',
|
|
317
|
+
lineComments: [],
|
|
318
|
+
prTitle: null,
|
|
319
|
+
prBody: null,
|
|
320
|
+
prMetaDirty: false,
|
|
321
|
+
};
|
|
302
322
|
}
|
|
303
323
|
|
|
304
324
|
/** @param {string} jobId */
|
|
@@ -329,6 +349,9 @@ function loadReviewDraft(jobId) {
|
|
|
329
349
|
body: c.body,
|
|
330
350
|
}))
|
|
331
351
|
: [],
|
|
352
|
+
prTitle: typeof parsed?.prTitle === 'string' ? parsed.prTitle : null,
|
|
353
|
+
prBody: typeof parsed?.prBody === 'string' ? parsed.prBody : null,
|
|
354
|
+
prMetaDirty: Boolean(parsed?.prMetaDirty),
|
|
332
355
|
};
|
|
333
356
|
reviewDrafts[jobId] = draft;
|
|
334
357
|
return draft;
|
|
@@ -352,10 +375,84 @@ function saveReviewDraft(jobId) {
|
|
|
352
375
|
}
|
|
353
376
|
}
|
|
354
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Resolve editable PR title/body for a job (local draft wins when dirty).
|
|
380
|
+
* @param {{ id: string, prTitle?: string, prBody?: string }} job
|
|
381
|
+
*/
|
|
382
|
+
function resolvePrMeta(job) {
|
|
383
|
+
const draft = loadReviewDraft(job.id);
|
|
384
|
+
if (!draft.prMetaDirty) {
|
|
385
|
+
return {
|
|
386
|
+
prTitle: job.prTitle || '',
|
|
387
|
+
prBody: job.prBody || '',
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
prTitle: draft.prTitle != null ? draft.prTitle : job.prTitle || '',
|
|
392
|
+
prBody: draft.prBody != null ? draft.prBody : job.prBody || '',
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Persist PR title/body edits from the form into the review draft.
|
|
398
|
+
* @param {string} jobId
|
|
399
|
+
*/
|
|
400
|
+
function persistPrMetaFromForm(jobId) {
|
|
401
|
+
if (!els.prTitle || !els.prBody) return;
|
|
402
|
+
const draft = loadReviewDraft(jobId);
|
|
403
|
+
draft.prTitle = els.prTitle.value;
|
|
404
|
+
draft.prBody = els.prBody.value;
|
|
405
|
+
draft.prMetaDirty = true;
|
|
406
|
+
saveReviewDraft(jobId);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Keep PR title/body across comment-only draft clears (submit review).
|
|
411
|
+
* @param {string} jobId
|
|
412
|
+
*/
|
|
413
|
+
function clearReviewCommentDraft(jobId) {
|
|
414
|
+
const prev = loadReviewDraft(jobId);
|
|
415
|
+
const kept = {
|
|
416
|
+
prTitle: prev.prTitle,
|
|
417
|
+
prBody: prev.prBody,
|
|
418
|
+
prMetaDirty: prev.prMetaDirty,
|
|
419
|
+
};
|
|
420
|
+
clearReviewDraft(jobId);
|
|
421
|
+
if (kept.prMetaDirty) {
|
|
422
|
+
const draft = loadReviewDraft(jobId);
|
|
423
|
+
draft.prTitle = kept.prTitle;
|
|
424
|
+
draft.prBody = kept.prBody;
|
|
425
|
+
draft.prMetaDirty = true;
|
|
426
|
+
saveReviewDraft(jobId);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function isDiffFullscreen() {
|
|
431
|
+
return Boolean(els.diffSection?.classList.contains('diff-fullscreen'));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** @param {boolean} on */
|
|
435
|
+
function setDiffFullscreen(on) {
|
|
436
|
+
if (!els.diffSection) return;
|
|
437
|
+
els.diffSection.classList.toggle('diff-fullscreen', on);
|
|
438
|
+
document.body.classList.toggle('diff-fs-open', on);
|
|
439
|
+
if (els.diffFullscreenBtn) {
|
|
440
|
+
els.diffFullscreenBtn.textContent = on ? 'Exit fullscreen' : 'Fullscreen';
|
|
441
|
+
els.diffFullscreenBtn.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function toggleDiffFullscreen() {
|
|
446
|
+
setDiffFullscreen(!isDiffFullscreen());
|
|
447
|
+
}
|
|
448
|
+
|
|
355
449
|
/** @param {string} jobId */
|
|
356
450
|
function clearReviewDraft(jobId) {
|
|
357
451
|
delete reviewDrafts[jobId];
|
|
358
452
|
activeInlineCommentKey = null;
|
|
453
|
+
editingCommentId = null;
|
|
454
|
+
editingCommentSurface = null;
|
|
455
|
+
editingCommentOriginalBody = null;
|
|
359
456
|
try {
|
|
360
457
|
sessionStorage.removeItem(reviewDraftStorageKey(jobId));
|
|
361
458
|
} catch {
|
|
@@ -363,6 +460,155 @@ function clearReviewDraft(jobId) {
|
|
|
363
460
|
}
|
|
364
461
|
}
|
|
365
462
|
|
|
463
|
+
/** Clear in-progress line-comment edit state. */
|
|
464
|
+
function clearCommentEditState() {
|
|
465
|
+
editingCommentId = null;
|
|
466
|
+
editingCommentSurface = null;
|
|
467
|
+
editingCommentOriginalBody = null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* @param {string} jobId
|
|
472
|
+
* @param {string} commentId
|
|
473
|
+
* @param {string} body
|
|
474
|
+
*/
|
|
475
|
+
function updateLineCommentBody(jobId, commentId, body) {
|
|
476
|
+
const d = loadReviewDraft(jobId);
|
|
477
|
+
const target = d.lineComments.find((x) => x.id === commentId);
|
|
478
|
+
if (!target) return;
|
|
479
|
+
target.body = body;
|
|
480
|
+
saveReviewDraft(jobId);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Refresh the non-active edit surface so both views stay in sync.
|
|
485
|
+
* @param {string} jobId
|
|
486
|
+
*/
|
|
487
|
+
function mirrorCommentEditToOtherSurface(jobId) {
|
|
488
|
+
if (editingCommentSurface === 'inline') {
|
|
489
|
+
renderPendingCommentList(jobId);
|
|
490
|
+
syncSubmitReviewButton(jobId);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (editingCommentSurface === 'pending') {
|
|
494
|
+
const job = jobsById[jobId];
|
|
495
|
+
if (job) {
|
|
496
|
+
const diff =
|
|
497
|
+
job.status === 'awaiting_review'
|
|
498
|
+
? filterDiffForReviewJob(job)
|
|
499
|
+
: job.diff || '';
|
|
500
|
+
renderDiff(diff, {
|
|
501
|
+
interactive: job.status === 'awaiting_review',
|
|
502
|
+
jobId,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
syncSubmitReviewButton(jobId);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Commit or discard an in-progress comment edit.
|
|
511
|
+
* @param {string} jobId
|
|
512
|
+
* @param {string} commentId
|
|
513
|
+
* @param {string | null} nextBody null = cancel / restore original
|
|
514
|
+
*/
|
|
515
|
+
function finishCommentEdit(jobId, commentId, nextBody) {
|
|
516
|
+
if (nextBody == null) {
|
|
517
|
+
if (editingCommentOriginalBody != null) {
|
|
518
|
+
updateLineCommentBody(jobId, commentId, editingCommentOriginalBody);
|
|
519
|
+
}
|
|
520
|
+
} else {
|
|
521
|
+
updateLineCommentBody(jobId, commentId, nextBody);
|
|
522
|
+
}
|
|
523
|
+
clearCommentEditState();
|
|
524
|
+
refreshReviewCommentUi(jobId);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Start editing a pending line comment on a specific surface.
|
|
529
|
+
* @param {string} commentId
|
|
530
|
+
* @param {'inline' | 'pending'} surface
|
|
531
|
+
* @param {string} jobId
|
|
532
|
+
*/
|
|
533
|
+
function startCommentEdit(commentId, surface, jobId) {
|
|
534
|
+
const draft = loadReviewDraft(jobId);
|
|
535
|
+
const target = draft.lineComments.find((x) => x.id === commentId);
|
|
536
|
+
editingCommentId = commentId;
|
|
537
|
+
editingCommentSurface = surface;
|
|
538
|
+
editingCommentOriginalBody = target ? target.body : '';
|
|
539
|
+
activeInlineCommentKey = null;
|
|
540
|
+
refreshReviewCommentUi(jobId);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Re-render diff + pending list after draft mutation.
|
|
545
|
+
* @param {string} jobId
|
|
546
|
+
*/
|
|
547
|
+
function refreshReviewCommentUi(jobId) {
|
|
548
|
+
const job = jobsById[jobId];
|
|
549
|
+
if (job) {
|
|
550
|
+
const diff =
|
|
551
|
+
job.status === 'awaiting_review'
|
|
552
|
+
? filterDiffForReviewJob(job)
|
|
553
|
+
: job.diff || '';
|
|
554
|
+
renderDiff(diff, {
|
|
555
|
+
interactive: job.status === 'awaiting_review',
|
|
556
|
+
jobId,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
renderPendingCommentList(jobId);
|
|
560
|
+
syncSubmitReviewButton(jobId);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* @param {{
|
|
565
|
+
* initialBody: string,
|
|
566
|
+
* saveLabel?: string,
|
|
567
|
+
* onSave: (body: string) => void,
|
|
568
|
+
* onCancel: () => void,
|
|
569
|
+
* onInput?: (body: string) => void,
|
|
570
|
+
* }} opts
|
|
571
|
+
*/
|
|
572
|
+
function buildCommentEditForm(opts) {
|
|
573
|
+
const form = document.createElement('div');
|
|
574
|
+
form.className = 'diff-inline-form';
|
|
575
|
+
const ta = document.createElement('textarea');
|
|
576
|
+
ta.className = 'input textarea';
|
|
577
|
+
ta.rows = 3;
|
|
578
|
+
ta.value = opts.initialBody;
|
|
579
|
+
ta.placeholder = 'Edit comment…';
|
|
580
|
+
if (opts.onInput) {
|
|
581
|
+
ta.addEventListener('input', () => opts.onInput(ta.value));
|
|
582
|
+
}
|
|
583
|
+
const actions = document.createElement('div');
|
|
584
|
+
actions.className = 'diff-inline-actions';
|
|
585
|
+
const save = document.createElement('button');
|
|
586
|
+
save.type = 'button';
|
|
587
|
+
save.className = 'btn btn-primary btn-sm';
|
|
588
|
+
save.textContent = opts.saveLabel || 'Save';
|
|
589
|
+
const cancel = document.createElement('button');
|
|
590
|
+
cancel.type = 'button';
|
|
591
|
+
cancel.className = 'btn btn-secondary btn-sm';
|
|
592
|
+
cancel.textContent = 'Cancel';
|
|
593
|
+
cancel.addEventListener('click', () => opts.onCancel());
|
|
594
|
+
save.addEventListener('click', () => {
|
|
595
|
+
const body = ta.value.trim();
|
|
596
|
+
if (!body) return;
|
|
597
|
+
opts.onSave(body);
|
|
598
|
+
});
|
|
599
|
+
actions.appendChild(save);
|
|
600
|
+
actions.appendChild(cancel);
|
|
601
|
+
form.appendChild(ta);
|
|
602
|
+
form.appendChild(actions);
|
|
603
|
+
requestAnimationFrame(() => {
|
|
604
|
+
ta.focus();
|
|
605
|
+
const len = ta.value.length;
|
|
606
|
+
ta.setSelectionRange(len, len);
|
|
607
|
+
ta.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
608
|
+
});
|
|
609
|
+
return form;
|
|
610
|
+
}
|
|
611
|
+
|
|
366
612
|
/** @param {string} jobId */
|
|
367
613
|
function reviewFilesStorageKey(jobId) {
|
|
368
614
|
return `${REVIEW_FILES_PREFIX}${jobId}`;
|
|
@@ -1162,6 +1408,7 @@ function renderDiff(diff, opts = {}) {
|
|
|
1162
1408
|
plus.addEventListener('click', (e) => {
|
|
1163
1409
|
e.stopPropagation();
|
|
1164
1410
|
activeInlineCommentKey = lineCommentKey(path, line, side);
|
|
1411
|
+
clearCommentEditState();
|
|
1165
1412
|
renderDiff(diff, opts);
|
|
1166
1413
|
renderPendingCommentList(jobId);
|
|
1167
1414
|
syncSubmitReviewButton(jobId);
|
|
@@ -1171,6 +1418,7 @@ function renderDiff(diff, opts = {}) {
|
|
|
1171
1418
|
cell.addEventListener('click', (e) => {
|
|
1172
1419
|
if (e.target.closest('button, textarea, a')) return;
|
|
1173
1420
|
activeInlineCommentKey = lineCommentKey(path, line, side);
|
|
1421
|
+
clearCommentEditState();
|
|
1174
1422
|
renderDiff(diff, opts);
|
|
1175
1423
|
renderPendingCommentList(jobId);
|
|
1176
1424
|
syncSubmitReviewButton(jobId);
|
|
@@ -1225,6 +1473,21 @@ function renderDiff(diff, opts = {}) {
|
|
|
1225
1473
|
thread.appendChild(meta);
|
|
1226
1474
|
|
|
1227
1475
|
for (const c of existing) {
|
|
1476
|
+
if (editingCommentId === c.id && editingCommentSurface === 'inline') {
|
|
1477
|
+
thread.appendChild(
|
|
1478
|
+
buildCommentEditForm({
|
|
1479
|
+
initialBody: c.body,
|
|
1480
|
+
onInput: (body) => {
|
|
1481
|
+
updateLineCommentBody(jobId, c.id, body);
|
|
1482
|
+
mirrorCommentEditToOtherSurface(jobId);
|
|
1483
|
+
},
|
|
1484
|
+
onSave: (body) => finishCommentEdit(jobId, c.id, body),
|
|
1485
|
+
onCancel: () => finishCommentEdit(jobId, c.id, null),
|
|
1486
|
+
})
|
|
1487
|
+
);
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1228
1491
|
const chip = document.createElement('div');
|
|
1229
1492
|
chip.className = 'diff-pending-chip';
|
|
1230
1493
|
const body = document.createElement('div');
|
|
@@ -1232,6 +1495,13 @@ function renderDiff(diff, opts = {}) {
|
|
|
1232
1495
|
body.textContent = c.body;
|
|
1233
1496
|
const actions = document.createElement('div');
|
|
1234
1497
|
actions.className = 'diff-pending-chip-actions';
|
|
1498
|
+
const edit = document.createElement('button');
|
|
1499
|
+
edit.type = 'button';
|
|
1500
|
+
edit.className = 'btn btn-muted-text btn-sm';
|
|
1501
|
+
edit.textContent = 'Edit';
|
|
1502
|
+
edit.addEventListener('click', () => {
|
|
1503
|
+
startCommentEdit(c.id, 'inline', jobId);
|
|
1504
|
+
});
|
|
1235
1505
|
const remove = document.createElement('button');
|
|
1236
1506
|
remove.type = 'button';
|
|
1237
1507
|
remove.className = 'btn btn-muted-text btn-sm';
|
|
@@ -1240,10 +1510,10 @@ function renderDiff(diff, opts = {}) {
|
|
|
1240
1510
|
const d = loadReviewDraft(jobId);
|
|
1241
1511
|
d.lineComments = d.lineComments.filter((x) => x.id !== c.id);
|
|
1242
1512
|
saveReviewDraft(jobId);
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
syncSubmitReviewButton(jobId);
|
|
1513
|
+
if (editingCommentId === c.id) clearCommentEditState();
|
|
1514
|
+
refreshReviewCommentUi(jobId);
|
|
1246
1515
|
});
|
|
1516
|
+
actions.appendChild(edit);
|
|
1247
1517
|
actions.appendChild(remove);
|
|
1248
1518
|
chip.appendChild(body);
|
|
1249
1519
|
chip.appendChild(actions);
|
|
@@ -1495,11 +1765,38 @@ function renderPendingCommentList(jobId) {
|
|
|
1495
1765
|
const loc = document.createElement('div');
|
|
1496
1766
|
loc.className = 'review-pending-item-loc';
|
|
1497
1767
|
loc.textContent = `${c.path}:${c.line} (${c.side})`;
|
|
1768
|
+
main.appendChild(loc);
|
|
1769
|
+
|
|
1770
|
+
if (editingCommentId === c.id && editingCommentSurface === 'pending') {
|
|
1771
|
+
main.appendChild(
|
|
1772
|
+
buildCommentEditForm({
|
|
1773
|
+
initialBody: c.body,
|
|
1774
|
+
onInput: (body) => {
|
|
1775
|
+
updateLineCommentBody(jobId, c.id, body);
|
|
1776
|
+
mirrorCommentEditToOtherSurface(jobId);
|
|
1777
|
+
},
|
|
1778
|
+
onSave: (body) => finishCommentEdit(jobId, c.id, body),
|
|
1779
|
+
onCancel: () => finishCommentEdit(jobId, c.id, null),
|
|
1780
|
+
})
|
|
1781
|
+
);
|
|
1782
|
+
item.appendChild(main);
|
|
1783
|
+
els.reviewPendingList.appendChild(item);
|
|
1784
|
+
continue;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1498
1787
|
const body = document.createElement('div');
|
|
1499
1788
|
body.className = 'review-pending-item-body';
|
|
1500
1789
|
body.textContent = c.body;
|
|
1501
|
-
main.appendChild(loc);
|
|
1502
1790
|
main.appendChild(body);
|
|
1791
|
+
const actions = document.createElement('div');
|
|
1792
|
+
actions.className = 'review-pending-item-actions';
|
|
1793
|
+
const edit = document.createElement('button');
|
|
1794
|
+
edit.type = 'button';
|
|
1795
|
+
edit.className = 'btn btn-muted-text btn-sm';
|
|
1796
|
+
edit.textContent = 'Edit';
|
|
1797
|
+
edit.addEventListener('click', () => {
|
|
1798
|
+
startCommentEdit(c.id, 'pending', jobId);
|
|
1799
|
+
});
|
|
1503
1800
|
const remove = document.createElement('button');
|
|
1504
1801
|
remove.type = 'button';
|
|
1505
1802
|
remove.className = 'btn btn-muted-text btn-sm';
|
|
@@ -1508,19 +1805,13 @@ function renderPendingCommentList(jobId) {
|
|
|
1508
1805
|
const d = loadReviewDraft(jobId);
|
|
1509
1806
|
d.lineComments = d.lineComments.filter((x) => x.id !== c.id);
|
|
1510
1807
|
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);
|
|
1808
|
+
if (editingCommentId === c.id) clearCommentEditState();
|
|
1809
|
+
refreshReviewCommentUi(jobId);
|
|
1521
1810
|
});
|
|
1811
|
+
actions.appendChild(edit);
|
|
1812
|
+
actions.appendChild(remove);
|
|
1522
1813
|
item.appendChild(main);
|
|
1523
|
-
item.appendChild(
|
|
1814
|
+
item.appendChild(actions);
|
|
1524
1815
|
els.reviewPendingList.appendChild(item);
|
|
1525
1816
|
}
|
|
1526
1817
|
}
|
|
@@ -1533,6 +1824,9 @@ function syncSubmitReviewButton(jobId) {
|
|
|
1533
1824
|
}
|
|
1534
1825
|
|
|
1535
1826
|
function setView(view) {
|
|
1827
|
+
if (view !== 'review' && isDiffFullscreen()) {
|
|
1828
|
+
setDiffFullscreen(false);
|
|
1829
|
+
}
|
|
1536
1830
|
currentView = view;
|
|
1537
1831
|
els.viewTitle.textContent = VIEW_TITLES[view] || view;
|
|
1538
1832
|
|
|
@@ -2021,6 +2315,7 @@ function renderReview(jobs) {
|
|
|
2021
2315
|
if (!hasPool) {
|
|
2022
2316
|
els.reviewNoMatches.classList.add('hidden');
|
|
2023
2317
|
els.reviewLayout.classList.add('hidden');
|
|
2318
|
+
if (isDiffFullscreen()) setDiffFullscreen(false);
|
|
2024
2319
|
return;
|
|
2025
2320
|
}
|
|
2026
2321
|
|
|
@@ -2084,9 +2379,10 @@ function renderReview(jobs) {
|
|
|
2084
2379
|
els.reviewPrSection.classList.toggle('hidden', !isOpened);
|
|
2085
2380
|
els.reviewTerminalSection.classList.toggle('hidden', !isTerminal);
|
|
2086
2381
|
|
|
2382
|
+
const prMeta = resolvePrMeta(job);
|
|
2087
2383
|
if (document.activeElement !== els.prTitle && document.activeElement !== els.prBody) {
|
|
2088
|
-
els.prTitle.value =
|
|
2089
|
-
els.prBody.value =
|
|
2384
|
+
els.prTitle.value = prMeta.prTitle;
|
|
2385
|
+
els.prBody.value = prMeta.prBody;
|
|
2090
2386
|
}
|
|
2091
2387
|
|
|
2092
2388
|
if (editable) {
|
|
@@ -2097,7 +2393,12 @@ function renderReview(jobs) {
|
|
|
2097
2393
|
) {
|
|
2098
2394
|
els.reviewGeneralComment.value = draft.generalComment;
|
|
2099
2395
|
}
|
|
2100
|
-
|
|
2396
|
+
const focusInPending = Boolean(
|
|
2397
|
+
els.reviewPendingList?.contains(document.activeElement)
|
|
2398
|
+
);
|
|
2399
|
+
if (!(editingCommentId && editingCommentSurface === 'pending' && focusInPending)) {
|
|
2400
|
+
renderPendingCommentList(job.id);
|
|
2401
|
+
}
|
|
2101
2402
|
syncSubmitReviewButton(job.id);
|
|
2102
2403
|
renderReviewFilesPanel(job);
|
|
2103
2404
|
}
|
|
@@ -2126,7 +2427,11 @@ function renderReview(jobs) {
|
|
|
2126
2427
|
}
|
|
2127
2428
|
|
|
2128
2429
|
const focusInDiff = els.diffViewer?.contains(document.activeElement);
|
|
2129
|
-
|
|
2430
|
+
const preservingDiffCompose =
|
|
2431
|
+
(Boolean(activeInlineCommentKey) ||
|
|
2432
|
+
(editingCommentId && editingCommentSurface === 'inline')) &&
|
|
2433
|
+
focusInDiff;
|
|
2434
|
+
if (!preservingDiffCompose) {
|
|
2130
2435
|
renderDiff(displayDiff, { interactive: editable, jobId: job.id });
|
|
2131
2436
|
}
|
|
2132
2437
|
|
|
@@ -3113,12 +3418,14 @@ async function approve(draft) {
|
|
|
3113
3418
|
b.dataset.busy = '1';
|
|
3114
3419
|
});
|
|
3115
3420
|
try {
|
|
3421
|
+
persistPrMetaFromForm(jobId);
|
|
3422
|
+
const prMeta = resolvePrMeta(jobsById[jobId] || { id: jobId });
|
|
3116
3423
|
const res = await fetch(`/api/jobs/${jobId}/approve`, {
|
|
3117
3424
|
method: 'POST',
|
|
3118
3425
|
headers: { 'Content-Type': 'application/json' },
|
|
3119
3426
|
body: JSON.stringify({
|
|
3120
|
-
prTitle:
|
|
3121
|
-
prBody:
|
|
3427
|
+
prTitle: prMeta.prTitle,
|
|
3428
|
+
prBody: prMeta.prBody,
|
|
3122
3429
|
draft,
|
|
3123
3430
|
excludedPaths,
|
|
3124
3431
|
}),
|
|
@@ -3126,6 +3433,7 @@ async function approve(draft) {
|
|
|
3126
3433
|
if (res.ok) {
|
|
3127
3434
|
clearExcludedPaths(jobId);
|
|
3128
3435
|
delete reviewFileLists[jobId];
|
|
3436
|
+
clearReviewDraft(jobId);
|
|
3129
3437
|
}
|
|
3130
3438
|
await fetchJobs();
|
|
3131
3439
|
if (!res.ok) {
|
|
@@ -3188,7 +3496,7 @@ async function submitReview() {
|
|
|
3188
3496
|
return;
|
|
3189
3497
|
}
|
|
3190
3498
|
|
|
3191
|
-
|
|
3499
|
+
clearReviewCommentDraft(jobId);
|
|
3192
3500
|
if (els.reviewGeneralComment) els.reviewGeneralComment.value = '';
|
|
3193
3501
|
selectedRunId = jobId;
|
|
3194
3502
|
selectedReviewId = null;
|
|
@@ -3234,9 +3542,29 @@ els.reviewGeneralComment?.addEventListener('input', () => {
|
|
|
3234
3542
|
syncSubmitReviewButton(selectedReviewId);
|
|
3235
3543
|
});
|
|
3236
3544
|
|
|
3545
|
+
function onPrMetaInput() {
|
|
3546
|
+
if (!selectedReviewId) return;
|
|
3547
|
+
persistPrMetaFromForm(selectedReviewId);
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
els.prTitle?.addEventListener('input', onPrMetaInput);
|
|
3551
|
+
els.prBody?.addEventListener('input', onPrMetaInput);
|
|
3552
|
+
|
|
3553
|
+
els.diffFullscreenBtn?.addEventListener('click', () => toggleDiffFullscreen());
|
|
3554
|
+
|
|
3555
|
+
document.addEventListener('keydown', (e) => {
|
|
3556
|
+
if (e.key === 'Escape' && isDiffFullscreen()) {
|
|
3557
|
+
e.preventDefault();
|
|
3558
|
+
setDiffFullscreen(false);
|
|
3559
|
+
}
|
|
3560
|
+
});
|
|
3561
|
+
|
|
3237
3562
|
els.rejectBtn.addEventListener('click', async () => {
|
|
3238
3563
|
if (!selectedReviewId) return;
|
|
3239
|
-
|
|
3564
|
+
const jobId = selectedReviewId;
|
|
3565
|
+
await fetch(`/api/jobs/${jobId}/reject`, { method: 'POST' });
|
|
3566
|
+
clearReviewDraft(jobId);
|
|
3567
|
+
if (isDiffFullscreen()) setDiffFullscreen(false);
|
|
3240
3568
|
selectedReviewId = null;
|
|
3241
3569
|
await fetchJobs();
|
|
3242
3570
|
});
|
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;
|