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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acdev",
3
- "version": "1.0.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 { generalComment: '', lineComments: [] };
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
- renderDiff(diff, opts);
1244
- renderPendingCommentList(jobId);
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
- const job = jobsById[jobId];
1512
- if (job) {
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(remove);
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 = job.prTitle || '';
2089
- els.prBody.value = job.prBody || '';
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
- renderPendingCommentList(job.id);
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
- if (!(activeInlineCommentKey && focusInDiff)) {
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: els.prTitle.value,
3121
- prBody: els.prBody.value,
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
- clearReviewDraft(jobId);
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
- await fetch(`/api/jobs/${selectedReviewId}/reject`, { method: 'POST' });
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 id="pr-title" class="input input-title" type="text">
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 id="pr-body" class="input textarea" rows="4"></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-label" id="diff-header">Diff</div>
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;