@in-the-loop-labs/pair-review 3.5.0 → 3.5.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/README.md +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/index.html +90 -0
- package/public/js/index.js +298 -25
- package/src/ai/claude-provider.js +68 -56
- package/src/ai/codex-provider.js +64 -33
- package/src/chat/api-reference.js +1 -1
- package/src/chat/chat-providers.js +26 -0
- package/src/chat/codex-bridge.js +238 -29
- package/src/chat/session-manager.js +1 -0
- package/src/main.js +3 -2
- package/src/routes/github-collections.js +168 -90
package/README.md
CHANGED
|
@@ -493,7 +493,7 @@ Configure your preferred models in `providers.pi.models` — see [AI Provider Co
|
|
|
493
493
|
}
|
|
494
494
|
```
|
|
495
495
|
|
|
496
|
-
Available chat provider IDs: `pi`, `claude`, `codex`, `copilot-acp`, `gemini-acp`, `opencode-acp`, `cursor-acp`. Each supports `command`, `args` (replaces defaults), `extra_args` (appends), and `env` overrides.
|
|
496
|
+
Available chat provider IDs: `pi`, `claude`, `codex`, `copilot-acp`, `gemini-acp`, `opencode-acp`, `cursor-acp`. Each supports `command`, `args` (replaces defaults), `extra_args` (appends), and `env` overrides. Codex chat also supports `sandbox`: use `workspace-write` by default, or `read-only` for discussion-only sessions.
|
|
497
497
|
|
|
498
498
|
**Keyboard shortcut:** Press `p` then `c` to toggle the chat panel.
|
|
499
499
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-review",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.2",
|
|
4
4
|
"description": "pair-review app integration — Open PRs and local changes in the pair-review web UI, run server-side AI analysis, and address review feedback. Requires the pair-review MCP server.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "in-the-loop-labs",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-critic",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.2",
|
|
4
4
|
"description": "AI-powered code review analysis — Run three-level AI analysis and implement-review-fix loops directly in your coding agent. Works standalone, no server required.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "in-the-loop-labs",
|
package/public/index.html
CHANGED
|
@@ -960,6 +960,65 @@
|
|
|
960
960
|
animation: spin 0.8s linear infinite;
|
|
961
961
|
}
|
|
962
962
|
|
|
963
|
+
/* Team Review Requests: filter-by-team control */
|
|
964
|
+
.team-filter-group {
|
|
965
|
+
display: flex;
|
|
966
|
+
align-items: center;
|
|
967
|
+
gap: 8px;
|
|
968
|
+
/* Push the filter to the left; fetched-at + refresh stay right. */
|
|
969
|
+
margin-right: auto;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
.team-filter {
|
|
973
|
+
display: flex;
|
|
974
|
+
align-items: center;
|
|
975
|
+
gap: 4px;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
.team-filter-input {
|
|
979
|
+
width: 160px;
|
|
980
|
+
height: 32px;
|
|
981
|
+
padding: 0 8px;
|
|
982
|
+
font-size: 13px;
|
|
983
|
+
background: var(--color-bg-primary);
|
|
984
|
+
border: 1px solid var(--color-border-primary);
|
|
985
|
+
border-radius: var(--radius-md);
|
|
986
|
+
color: var(--color-text-primary);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
.team-filter-input:focus {
|
|
990
|
+
outline: none;
|
|
991
|
+
border-color: var(--ai-primary);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
.team-filter-input.invalid {
|
|
995
|
+
border-color: var(--color-danger);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
.team-filter-apply,
|
|
999
|
+
.team-filter-clear {
|
|
1000
|
+
height: 32px;
|
|
1001
|
+
padding: 0 10px;
|
|
1002
|
+
font-size: 13px;
|
|
1003
|
+
background: transparent;
|
|
1004
|
+
border: 1px solid var(--color-border-primary);
|
|
1005
|
+
border-radius: var(--radius-md);
|
|
1006
|
+
color: var(--color-text-tertiary);
|
|
1007
|
+
cursor: pointer;
|
|
1008
|
+
transition: all var(--transition-fast);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
.team-filter-apply:hover,
|
|
1012
|
+
.team-filter-clear:hover {
|
|
1013
|
+
background: var(--color-bg-secondary);
|
|
1014
|
+
color: var(--color-text-primary);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
.team-filter-hint {
|
|
1018
|
+
font-size: 12px;
|
|
1019
|
+
color: var(--color-danger);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
963
1022
|
.collection-pr-row {
|
|
964
1023
|
cursor: pointer;
|
|
965
1024
|
}
|
|
@@ -1343,6 +1402,7 @@
|
|
|
1343
1402
|
<div class="tab-bar" id="unified-tab-bar">
|
|
1344
1403
|
<button class="tab-btn active" data-tab="pr-tab" type="button">Pull Requests</button>
|
|
1345
1404
|
<button class="tab-btn" data-tab="review-requests-tab" type="button">My Review Requests</button>
|
|
1405
|
+
<button class="tab-btn" data-tab="team-reviews-tab" type="button">Team Review Requests</button>
|
|
1346
1406
|
<button class="tab-btn" data-tab="my-prs-tab" type="button">My PRs</button>
|
|
1347
1407
|
<span class="tab-divider"></span>
|
|
1348
1408
|
<button class="tab-btn" data-tab="local-tab" type="button">Local Reviews</button>
|
|
@@ -1392,6 +1452,36 @@
|
|
|
1392
1452
|
</div>
|
|
1393
1453
|
</div>
|
|
1394
1454
|
|
|
1455
|
+
<!-- Team Review Requests Tab -->
|
|
1456
|
+
<div class="tab-pane" id="team-reviews-tab">
|
|
1457
|
+
<div class="tab-pane-header">
|
|
1458
|
+
<div class="team-filter-group">
|
|
1459
|
+
<form class="team-filter" id="team-reviews-filter">
|
|
1460
|
+
<input
|
|
1461
|
+
type="text"
|
|
1462
|
+
class="team-filter-input"
|
|
1463
|
+
id="team-reviews-team-input"
|
|
1464
|
+
placeholder="org/team"
|
|
1465
|
+
autocomplete="off"
|
|
1466
|
+
spellcheck="false"
|
|
1467
|
+
aria-label="Filter by team (org/team)"
|
|
1468
|
+
aria-describedby="team-reviews-filter-hint"
|
|
1469
|
+
>
|
|
1470
|
+
<button type="submit" class="team-filter-apply" id="team-reviews-filter-apply" title="Filter by this team">Filter</button>
|
|
1471
|
+
<button type="button" class="team-filter-clear" id="team-reviews-filter-clear" title="Show all teams" hidden>Clear</button>
|
|
1472
|
+
</form>
|
|
1473
|
+
<span class="team-filter-hint" id="team-reviews-filter-hint" role="alert" hidden>Use the form org/team.</span>
|
|
1474
|
+
</div>
|
|
1475
|
+
<span class="fetched-at-label" id="team-reviews-fetched-at"></span>
|
|
1476
|
+
<button class="btn-refresh" id="refresh-team-reviews" title="Refresh from GitHub">
|
|
1477
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M1.705 8.005a.75.75 0 0 1 .834.656 5.5 5.5 0 0 0 9.592 2.97l-1.204-1.204a.25.25 0 0 1 .177-.427h3.646a.25.25 0 0 1 .25.25v3.646a.25.25 0 0 1-.427.177l-1.38-1.38A7.002 7.002 0 0 1 1.05 8.84a.75.75 0 0 1 .656-.834ZM8 2.5a5.487 5.487 0 0 0-4.131 1.869l1.204 1.204A.25.25 0 0 1 4.896 6H1.25A.25.25 0 0 1 1 5.75V2.104a.25.25 0 0 1 .427-.177l1.38 1.38A7.002 7.002 0 0 1 14.95 7.16a.75.75 0 0 1-1.49.178A5.5 5.5 0 0 0 8 2.5Z"/></svg>
|
|
1478
|
+
</button>
|
|
1479
|
+
</div>
|
|
1480
|
+
<div id="team-reviews-container" class="recent-reviews-loading">
|
|
1481
|
+
Loading...
|
|
1482
|
+
</div>
|
|
1483
|
+
</div>
|
|
1484
|
+
|
|
1395
1485
|
<!-- My PRs Tab -->
|
|
1396
1486
|
<div class="tab-pane" id="my-prs-tab">
|
|
1397
1487
|
<div class="tab-pane-header">
|
package/public/js/index.js
CHANGED
|
@@ -303,7 +303,21 @@
|
|
|
303
303
|
loaded: false
|
|
304
304
|
};
|
|
305
305
|
|
|
306
|
-
// ─── GitHub PR Collections (
|
|
306
|
+
// ─── GitHub PR Collections (Review Requests / Team Reviews / My PRs) ───────
|
|
307
|
+
|
|
308
|
+
/** Empty-state message per collection (used when a fetch returned no PRs). */
|
|
309
|
+
var COLLECTION_EMPTY_MESSAGES = {
|
|
310
|
+
'review-requests': 'No pull requests awaiting your review.',
|
|
311
|
+
'team-reviews': 'No pull requests awaiting your team’s review.',
|
|
312
|
+
'my-prs': 'You have no open pull requests.'
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/** Noun used in the "Configure a GitHub token to see …" message per collection. */
|
|
316
|
+
var COLLECTION_TOKEN_LABELS = {
|
|
317
|
+
'review-requests': 'review requests',
|
|
318
|
+
'team-reviews': 'team review requests',
|
|
319
|
+
'my-prs': 'your pull requests'
|
|
320
|
+
};
|
|
307
321
|
|
|
308
322
|
var reviewRequestsState = {
|
|
309
323
|
loaded: false,
|
|
@@ -311,16 +325,90 @@
|
|
|
311
325
|
fetchedAt: null
|
|
312
326
|
};
|
|
313
327
|
|
|
328
|
+
var teamReviewsState = {
|
|
329
|
+
loaded: false,
|
|
330
|
+
prs: [],
|
|
331
|
+
fetchedAt: null,
|
|
332
|
+
// Monotonic token for the most recent team-reviews request (see
|
|
333
|
+
// beginTeamReviewsRequest). Stale responses compare against this and bail.
|
|
334
|
+
activeRequest: 0
|
|
335
|
+
};
|
|
336
|
+
|
|
314
337
|
var myPrsState = {
|
|
315
338
|
loaded: false,
|
|
316
339
|
prs: [],
|
|
317
340
|
fetchedAt: null
|
|
318
341
|
};
|
|
319
342
|
|
|
343
|
+
// Filter-by-team support for the Team Review Requests tab.
|
|
344
|
+
// Client-side validation is UX only; the server re-validates before
|
|
345
|
+
// interpolating the value into the GitHub search query (query-injection
|
|
346
|
+
// guard). Pattern must match the server's TEAM_SLUG_PATTERN.
|
|
347
|
+
var TEAM_SLUG_PATTERN = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
348
|
+
var TEAM_FILTER_LS_KEY = 'github-collection-team:team-reviews';
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Read the persisted team filter, returning '' (all teams) for an absent or
|
|
352
|
+
* invalid stored value.
|
|
353
|
+
* @returns {string} A validated `org/team` slug, or ''.
|
|
354
|
+
*/
|
|
355
|
+
function getStoredTeamFilter() {
|
|
356
|
+
try {
|
|
357
|
+
var v = (localStorage.getItem(TEAM_FILTER_LS_KEY) || '').trim();
|
|
358
|
+
return v && TEAM_SLUG_PATTERN.test(v) ? v : '';
|
|
359
|
+
} catch (e) {
|
|
360
|
+
return '';
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Begin a new team-reviews request "epoch" and return its token. Every user
|
|
366
|
+
* action that (re)loads the team-reviews view — filter apply, tab switch,
|
|
367
|
+
* manual refresh, initial load — calls this once and threads the returned id
|
|
368
|
+
* through BOTH the cached GET and the live refresh. Because multiple in-flight
|
|
369
|
+
* fetches write into the shared `teamReviewsState` and render into the same
|
|
370
|
+
* container, completion order alone could otherwise let a slower earlier
|
|
371
|
+
* request (e.g. a previous team, or the all-teams view) repaint the table with
|
|
372
|
+
* PRs that no longer match the active selection. loadCollectionPrs /
|
|
373
|
+
* refreshCollectionPrs bail after `await fetch(...)` when their token is no
|
|
374
|
+
* longer `activeRequest`, discarding the stale response before it mutates
|
|
375
|
+
* state or the DOM.
|
|
376
|
+
* @returns {number} The token for this request epoch.
|
|
377
|
+
*/
|
|
378
|
+
function beginTeamReviewsRequest() {
|
|
379
|
+
teamReviewsState.activeRequest += 1;
|
|
380
|
+
return teamReviewsState.activeRequest;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Resync the Team Review Requests filter controls to a known-applied value
|
|
385
|
+
* (typically getStoredTeamFilter()). Switching tabs only toggles `.active` —
|
|
386
|
+
* it does not rebuild the Team Reviews DOM — so unapplied draft text typed
|
|
387
|
+
* into the input survives a tab switch. Because every reload path keys its
|
|
388
|
+
* fetch off getStoredTeamFilter() (the persisted value) rather than the live
|
|
389
|
+
* input, the visible filter could otherwise advertise one team while the
|
|
390
|
+
* queue, row-click navigation, and bulk actions operate on another. Any path
|
|
391
|
+
* that re-consumes the stored filter must call this first so the displayed
|
|
392
|
+
* filter cannot diverge from the team actually being fetched and rendered.
|
|
393
|
+
* @param {string} value - The validated `org/team` slug to display, or '' for all teams.
|
|
394
|
+
*/
|
|
395
|
+
function syncTeamFilterControls(value) {
|
|
396
|
+
var input = document.getElementById('team-reviews-team-input');
|
|
397
|
+
if (input) {
|
|
398
|
+
input.value = value || '';
|
|
399
|
+
input.classList.remove('invalid');
|
|
400
|
+
input.removeAttribute('aria-invalid');
|
|
401
|
+
}
|
|
402
|
+
var hint = document.getElementById('team-reviews-filter-hint');
|
|
403
|
+
if (hint) hint.hidden = true;
|
|
404
|
+
var clearBtn = document.getElementById('team-reviews-filter-clear');
|
|
405
|
+
if (clearBtn) clearBtn.hidden = !value;
|
|
406
|
+
}
|
|
407
|
+
|
|
320
408
|
/**
|
|
321
409
|
* Render a single row for a collection PR table.
|
|
322
410
|
* @param {Object} pr - PR object from the API
|
|
323
|
-
* @param {string} collection - The collection name ('review-requests' or 'my-prs')
|
|
411
|
+
* @param {string} collection - The collection name ('review-requests', 'team-reviews', or 'my-prs')
|
|
324
412
|
* @returns {string} HTML string for the table row
|
|
325
413
|
*/
|
|
326
414
|
function renderCollectionPrRow(pr, collection) {
|
|
@@ -366,15 +454,20 @@
|
|
|
366
454
|
* Render the collection table into a container element.
|
|
367
455
|
* @param {HTMLElement} container - The container element
|
|
368
456
|
* @param {Object} state - The collection state object
|
|
369
|
-
* @param {string} collection - The collection name ('review-requests' or 'my-prs')
|
|
457
|
+
* @param {string} collection - The collection name ('review-requests', 'team-reviews', or 'my-prs')
|
|
370
458
|
*/
|
|
371
459
|
function renderCollectionTable(container, state, collection) {
|
|
372
|
-
var sel = collection
|
|
373
|
-
sel.exit();
|
|
460
|
+
var sel = collectionSelections[collection];
|
|
461
|
+
if (sel) sel.exit();
|
|
374
462
|
|
|
375
|
-
var
|
|
376
|
-
var fetchedAtEl = document.getElementById(fetchedAtId);
|
|
463
|
+
var fetchedAtEl = document.getElementById(collection + '-fetched-at');
|
|
377
464
|
if (fetchedAtEl) {
|
|
465
|
+
// Note: this key is intentionally NOT per-team — unlike the DB cache key
|
|
466
|
+
// in src/routes/github-collections.js (which uses cacheKey()), the
|
|
467
|
+
// fetched-at timestamp is informational only. We accept a brief label
|
|
468
|
+
// mismatch when the user switches the team-reviews filter; the label
|
|
469
|
+
// updates once that view's next refresh completes. See the write site in
|
|
470
|
+
// refreshCollectionPrs.
|
|
378
471
|
var lsKey = 'github-collection-fetched-at:' + collection;
|
|
379
472
|
var displayTs = localStorage.getItem(lsKey) || state.fetchedAt;
|
|
380
473
|
fetchedAtEl.textContent = displayTs
|
|
@@ -383,13 +476,11 @@
|
|
|
383
476
|
}
|
|
384
477
|
|
|
385
478
|
if (state.prs.length === 0) {
|
|
386
|
-
var
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
emptyMsg = 'Click refresh to fetch from GitHub.';
|
|
392
|
-
}
|
|
479
|
+
var emptyLsKey = 'github-collection-fetched-at:' + collection;
|
|
480
|
+
var hasFetched = state.fetchedAt || localStorage.getItem(emptyLsKey);
|
|
481
|
+
var emptyMsg = hasFetched
|
|
482
|
+
? (COLLECTION_EMPTY_MESSAGES[collection] || 'No pull requests found.')
|
|
483
|
+
: 'Click refresh to fetch from GitHub.';
|
|
393
484
|
|
|
394
485
|
container.innerHTML =
|
|
395
486
|
'<div class="recent-reviews-empty">' +
|
|
@@ -401,7 +492,7 @@
|
|
|
401
492
|
|
|
402
493
|
var authorTh = collection === 'my-prs' ? '' : '<th>Author</th>';
|
|
403
494
|
|
|
404
|
-
var tbodyId = collection
|
|
495
|
+
var tbodyId = collection + '-tbody';
|
|
405
496
|
|
|
406
497
|
container.innerHTML =
|
|
407
498
|
'<table class="recent-reviews-table">' +
|
|
@@ -424,18 +515,28 @@
|
|
|
424
515
|
|
|
425
516
|
/**
|
|
426
517
|
* Load collection PRs from cached backend data.
|
|
427
|
-
* @param {string} collection - 'review-requests' or 'my-prs'
|
|
518
|
+
* @param {string} collection - 'review-requests', 'team-reviews', or 'my-prs'
|
|
428
519
|
* @param {string} containerId - DOM id of the container element
|
|
429
520
|
* @param {Object} state - The collection state object
|
|
521
|
+
* @param {string} [team] - Optional validated `org/team` filter (team-reviews
|
|
522
|
+
* only). When set, requests the namespaced cache for that team.
|
|
523
|
+
* @param {number} [requestId] - Optional team-reviews request token (see
|
|
524
|
+
* beginTeamReviewsRequest). When provided, the response is discarded if a
|
|
525
|
+
* newer request has superseded it before this fetch resolved.
|
|
430
526
|
*/
|
|
431
|
-
async function loadCollectionPrs(collection, containerId, state) {
|
|
527
|
+
async function loadCollectionPrs(collection, containerId, state, team, requestId) {
|
|
432
528
|
var container = document.getElementById(containerId);
|
|
433
529
|
|
|
434
530
|
try {
|
|
435
|
-
var
|
|
531
|
+
var url = '/api/github/' + collection + (team ? '?team=' + encodeURIComponent(team) : '');
|
|
532
|
+
var response = await fetch(url);
|
|
436
533
|
if (!response.ok) throw new Error('Failed to fetch');
|
|
437
534
|
var data = await response.json();
|
|
438
535
|
|
|
536
|
+
// Discard a response a newer team-reviews request has superseded, so a
|
|
537
|
+
// slow earlier load can't repaint the table with a stale team's PRs.
|
|
538
|
+
if (requestId != null && requestId !== state.activeRequest) return;
|
|
539
|
+
|
|
439
540
|
state.loaded = true;
|
|
440
541
|
state.prs = data.prs || [];
|
|
441
542
|
state.fetchedAt = data.fetched_at;
|
|
@@ -443,6 +544,8 @@
|
|
|
443
544
|
renderCollectionTable(container, state, collection);
|
|
444
545
|
} catch (error) {
|
|
445
546
|
console.error('Error loading ' + collection + ':', error);
|
|
547
|
+
// Don't paint the error over a view a newer request now owns.
|
|
548
|
+
if (requestId != null && requestId !== state.activeRequest) return;
|
|
446
549
|
container.innerHTML =
|
|
447
550
|
'<div class="recent-reviews-empty">' +
|
|
448
551
|
'<p>Failed to load. Click refresh to try again.</p>' +
|
|
@@ -453,14 +556,18 @@
|
|
|
453
556
|
|
|
454
557
|
/**
|
|
455
558
|
* Refresh collection PRs by fetching fresh data from GitHub.
|
|
456
|
-
* @param {string} collection - 'review-requests' or 'my-prs'
|
|
559
|
+
* @param {string} collection - 'review-requests', 'team-reviews', or 'my-prs'
|
|
457
560
|
* @param {string} containerId - DOM id of the container element
|
|
458
561
|
* @param {Object} state - The collection state object
|
|
562
|
+
* @param {string} [team] - Optional validated `org/team` filter (team-reviews
|
|
563
|
+
* only). When set, refreshes and caches under the namespaced key.
|
|
564
|
+
* @param {number} [requestId] - Optional team-reviews request token (see
|
|
565
|
+
* beginTeamReviewsRequest). When provided, the response is discarded if a
|
|
566
|
+
* newer request has superseded it before this fetch resolved.
|
|
459
567
|
*/
|
|
460
|
-
async function refreshCollectionPrs(collection, containerId, state) {
|
|
568
|
+
async function refreshCollectionPrs(collection, containerId, state, team, requestId) {
|
|
461
569
|
var container = document.getElementById(containerId);
|
|
462
|
-
var
|
|
463
|
-
var btn = document.getElementById(btnId);
|
|
570
|
+
var btn = document.getElementById('refresh-' + collection);
|
|
464
571
|
|
|
465
572
|
if (btn) btn.classList.add('refreshing');
|
|
466
573
|
|
|
@@ -470,7 +577,12 @@
|
|
|
470
577
|
}
|
|
471
578
|
|
|
472
579
|
try {
|
|
473
|
-
var
|
|
580
|
+
var refreshUrl = '/api/github/' + collection + '/refresh' + (team ? '?team=' + encodeURIComponent(team) : '');
|
|
581
|
+
var response = await fetch(refreshUrl, { method: 'POST' });
|
|
582
|
+
|
|
583
|
+
// A newer team-reviews request has superseded this one; drop it before
|
|
584
|
+
// touching the 401 message, shared state, or the table.
|
|
585
|
+
if (requestId != null && requestId !== state.activeRequest) return;
|
|
474
586
|
|
|
475
587
|
if (!response.ok) {
|
|
476
588
|
var errData = await response.json().catch(function() { return {}; });
|
|
@@ -478,7 +590,7 @@
|
|
|
478
590
|
container.innerHTML =
|
|
479
591
|
'<div class="recent-reviews-empty">' +
|
|
480
592
|
'<p>Configure a GitHub token to see ' +
|
|
481
|
-
(collection
|
|
593
|
+
(COLLECTION_TOKEN_LABELS[collection] || 'pull requests') +
|
|
482
594
|
'.</p>' +
|
|
483
595
|
'</div>';
|
|
484
596
|
container.classList.remove('recent-reviews-loading');
|
|
@@ -488,14 +600,26 @@
|
|
|
488
600
|
}
|
|
489
601
|
|
|
490
602
|
var data = await response.json();
|
|
603
|
+
|
|
604
|
+
// Re-check after the body parse: a newer request may have started while
|
|
605
|
+
// the response was streaming.
|
|
606
|
+
if (requestId != null && requestId !== state.activeRequest) return;
|
|
607
|
+
|
|
491
608
|
state.prs = data.prs || [];
|
|
492
609
|
state.fetchedAt = data.fetched_at;
|
|
493
610
|
state.loaded = true;
|
|
611
|
+
// Note: this key is intentionally NOT per-team — unlike the DB cache key
|
|
612
|
+
// in src/routes/github-collections.js (which uses cacheKey()), the
|
|
613
|
+
// fetched-at timestamp is informational only. We accept a brief label
|
|
614
|
+
// mismatch when the user switches the team-reviews filter; the label
|
|
615
|
+
// updates once the next refresh completes (read site: renderCollectionTable).
|
|
494
616
|
localStorage.setItem('github-collection-fetched-at:' + collection, new Date().toISOString());
|
|
495
617
|
|
|
496
618
|
renderCollectionTable(container, state, collection);
|
|
497
619
|
} catch (error) {
|
|
498
620
|
console.error('Error refreshing ' + collection + ':', error);
|
|
621
|
+
// Don't repaint a view a newer request now owns.
|
|
622
|
+
if (requestId != null && requestId !== state.activeRequest) return;
|
|
499
623
|
// If we had existing data, keep showing it
|
|
500
624
|
if (state.prs.length > 0) {
|
|
501
625
|
renderCollectionTable(container, state, collection);
|
|
@@ -511,6 +635,62 @@
|
|
|
511
635
|
}
|
|
512
636
|
}
|
|
513
637
|
|
|
638
|
+
/**
|
|
639
|
+
* Apply the Team Review Requests team filter from the input field.
|
|
640
|
+
* Validates client-side (UX only), persists the value, toggles the Clear
|
|
641
|
+
* affordance and inline hint, then reloads the team-reviews view for the
|
|
642
|
+
* selected team (empty input = all teams).
|
|
643
|
+
*/
|
|
644
|
+
function applyTeamFilter() {
|
|
645
|
+
var input = document.getElementById('team-reviews-team-input');
|
|
646
|
+
if (!input) return;
|
|
647
|
+
var hint = document.getElementById('team-reviews-filter-hint');
|
|
648
|
+
var clearBtn = document.getElementById('team-reviews-filter-clear');
|
|
649
|
+
var value = input.value.trim();
|
|
650
|
+
|
|
651
|
+
// Non-empty input must match the org/team format; skip the fetch and show
|
|
652
|
+
// the inline hint on invalid input. Empty means "all teams" (valid).
|
|
653
|
+
if (value !== '' && !TEAM_SLUG_PATTERN.test(value)) {
|
|
654
|
+
input.classList.add('invalid');
|
|
655
|
+
input.setAttribute('aria-invalid', 'true');
|
|
656
|
+
if (hint) hint.hidden = false;
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
input.classList.remove('invalid');
|
|
661
|
+
input.removeAttribute('aria-invalid');
|
|
662
|
+
if (hint) hint.hidden = true;
|
|
663
|
+
|
|
664
|
+
try {
|
|
665
|
+
if (value) {
|
|
666
|
+
localStorage.setItem(TEAM_FILTER_LS_KEY, value);
|
|
667
|
+
} else {
|
|
668
|
+
localStorage.removeItem(TEAM_FILTER_LS_KEY);
|
|
669
|
+
}
|
|
670
|
+
} catch (e) { /* localStorage unavailable; filter still applies for this view */ }
|
|
671
|
+
|
|
672
|
+
if (clearBtn) clearBtn.hidden = !value;
|
|
673
|
+
|
|
674
|
+
// Reset loaded state so we read the (namespaced) cache for the new view
|
|
675
|
+
// before refreshing live from GitHub.
|
|
676
|
+
teamReviewsState.loaded = false;
|
|
677
|
+
teamReviewsState.prs = [];
|
|
678
|
+
// Paint a placeholder so the previous team's rows don't linger while
|
|
679
|
+
// loadCollectionPrs / refreshCollectionPrs run asynchronously.
|
|
680
|
+
var container = document.getElementById('team-reviews-container');
|
|
681
|
+
if (container) {
|
|
682
|
+
container.innerHTML = '<div class="recent-reviews-loading">Loading...</div>';
|
|
683
|
+
}
|
|
684
|
+
var team = value || '';
|
|
685
|
+
// One token for this user action, threaded through both the cached GET and
|
|
686
|
+
// the live refresh, so a slower earlier request can't clobber this view.
|
|
687
|
+
var requestId = beginTeamReviewsRequest();
|
|
688
|
+
loadCollectionPrs('team-reviews', 'team-reviews-container', teamReviewsState, team, requestId)
|
|
689
|
+
.then(function () {
|
|
690
|
+
refreshCollectionPrs('team-reviews', 'team-reviews-container', teamReviewsState, team, requestId);
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
514
694
|
/**
|
|
515
695
|
* Fetch and display local review sessions (initial load).
|
|
516
696
|
*/
|
|
@@ -1610,6 +1790,17 @@
|
|
|
1610
1790
|
]
|
|
1611
1791
|
});
|
|
1612
1792
|
|
|
1793
|
+
var teamReviewsSelection = new SelectionMode({
|
|
1794
|
+
tabId: 'team-reviews-tab',
|
|
1795
|
+
containerId: 'team-reviews-container',
|
|
1796
|
+
tbodyId: 'team-reviews-tbody',
|
|
1797
|
+
rowIdAttr: 'prUrl',
|
|
1798
|
+
actions: [
|
|
1799
|
+
{ label: 'Open', className: 'btn-bulk-open', handler: handleBulkOpen },
|
|
1800
|
+
{ label: 'Analyze', className: 'btn-bulk-analyze', handler: handleBulkAnalyze }
|
|
1801
|
+
]
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1613
1804
|
var myPrsSelection = new SelectionMode({
|
|
1614
1805
|
tabId: 'my-prs-tab',
|
|
1615
1806
|
containerId: 'my-prs-container',
|
|
@@ -1621,6 +1812,13 @@
|
|
|
1621
1812
|
]
|
|
1622
1813
|
});
|
|
1623
1814
|
|
|
1815
|
+
/** Map of collection name → its SelectionMode instance. */
|
|
1816
|
+
var collectionSelections = {
|
|
1817
|
+
'review-requests': reviewRequestsSelection,
|
|
1818
|
+
'team-reviews': teamReviewsSelection,
|
|
1819
|
+
'my-prs': myPrsSelection
|
|
1820
|
+
};
|
|
1821
|
+
|
|
1624
1822
|
async function handleBulkDeletePR(selectedIds, selectionInstance) {
|
|
1625
1823
|
var count = selectedIds.size;
|
|
1626
1824
|
selectionInstance.showConfirm('Delete ' + count + ' review' + (count === 1 ? '' : 's') + '?', async function () {
|
|
@@ -1762,6 +1960,19 @@
|
|
|
1762
1960
|
return;
|
|
1763
1961
|
}
|
|
1764
1962
|
|
|
1963
|
+
var refreshTeamReviewsBtn = event.target.closest('#refresh-team-reviews');
|
|
1964
|
+
if (refreshTeamReviewsBtn) {
|
|
1965
|
+
event.preventDefault();
|
|
1966
|
+
// Resync the controls to the applied filter before refreshing: unapplied
|
|
1967
|
+
// draft text in the input must not advertise a team different from the one
|
|
1968
|
+
// we actually fetch (which keys off the persisted value).
|
|
1969
|
+
var teamReviewsRefreshFilter = getStoredTeamFilter();
|
|
1970
|
+
syncTeamFilterControls(teamReviewsRefreshFilter);
|
|
1971
|
+
var teamReviewsRefreshId = beginTeamReviewsRequest();
|
|
1972
|
+
refreshCollectionPrs('team-reviews', 'team-reviews-container', teamReviewsState, teamReviewsRefreshFilter, teamReviewsRefreshId);
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1765
1976
|
var refreshMyPrsBtn = event.target.closest('#refresh-my-prs');
|
|
1766
1977
|
if (refreshMyPrsBtn) {
|
|
1767
1978
|
event.preventDefault();
|
|
@@ -1774,7 +1985,7 @@
|
|
|
1774
1985
|
if (selectToggle) {
|
|
1775
1986
|
event.preventDefault();
|
|
1776
1987
|
var tabId = selectToggle.dataset.selectionTab;
|
|
1777
|
-
var instances = { 'pr-tab': prSelection, 'local-tab': localSelection, 'review-requests-tab': reviewRequestsSelection, 'my-prs-tab': myPrsSelection };
|
|
1988
|
+
var instances = { 'pr-tab': prSelection, 'local-tab': localSelection, 'review-requests-tab': reviewRequestsSelection, 'team-reviews-tab': teamReviewsSelection, 'my-prs-tab': myPrsSelection };
|
|
1778
1989
|
var instance = instances[tabId];
|
|
1779
1990
|
if (instance) instance.toggle();
|
|
1780
1991
|
return;
|
|
@@ -1851,6 +2062,18 @@
|
|
|
1851
2062
|
}
|
|
1852
2063
|
refreshCollectionPrs('review-requests', 'review-requests-container', reviewRequestsState);
|
|
1853
2064
|
}
|
|
2065
|
+
if (tabId === 'team-reviews-tab') {
|
|
2066
|
+
var teamFilter = getStoredTeamFilter();
|
|
2067
|
+
// Re-entering the tab preserves any unapplied draft text in the input
|
|
2068
|
+
// (switchTab does not rebuild the DOM). Resync the controls to the
|
|
2069
|
+
// applied filter so the visible team matches the one we reload.
|
|
2070
|
+
syncTeamFilterControls(teamFilter);
|
|
2071
|
+
var teamReviewsTabRequestId = beginTeamReviewsRequest();
|
|
2072
|
+
if (!teamReviewsState.loaded) {
|
|
2073
|
+
await loadCollectionPrs('team-reviews', 'team-reviews-container', teamReviewsState, teamFilter, teamReviewsTabRequestId);
|
|
2074
|
+
}
|
|
2075
|
+
refreshCollectionPrs('team-reviews', 'team-reviews-container', teamReviewsState, teamFilter, teamReviewsTabRequestId);
|
|
2076
|
+
}
|
|
1854
2077
|
if (tabId === 'my-prs-tab') {
|
|
1855
2078
|
if (!myPrsState.loaded) {
|
|
1856
2079
|
await loadCollectionPrs('my-prs', 'my-prs-container', myPrsState);
|
|
@@ -1894,6 +2117,12 @@
|
|
|
1894
2117
|
loadCollectionPrs('review-requests', 'review-requests-container', reviewRequestsState)
|
|
1895
2118
|
.then(function () { refreshCollectionPrs('review-requests', 'review-requests-container', reviewRequestsState); });
|
|
1896
2119
|
}
|
|
2120
|
+
if (savedTab === 'team-reviews-tab') {
|
|
2121
|
+
var initialTeamFilter = getStoredTeamFilter();
|
|
2122
|
+
var initialTeamRequestId = beginTeamReviewsRequest();
|
|
2123
|
+
loadCollectionPrs('team-reviews', 'team-reviews-container', teamReviewsState, initialTeamFilter, initialTeamRequestId)
|
|
2124
|
+
.then(function () { refreshCollectionPrs('team-reviews', 'team-reviews-container', teamReviewsState, initialTeamFilter, initialTeamRequestId); });
|
|
2125
|
+
}
|
|
1897
2126
|
if (savedTab === 'my-prs-tab') {
|
|
1898
2127
|
loadCollectionPrs('my-prs', 'my-prs-container', myPrsState)
|
|
1899
2128
|
.then(function () { refreshCollectionPrs('my-prs', 'my-prs-container', myPrsState); });
|
|
@@ -1922,6 +2151,37 @@
|
|
|
1922
2151
|
browseBtn.addEventListener('click', handleBrowseLocal);
|
|
1923
2152
|
}
|
|
1924
2153
|
|
|
2154
|
+
// Set up Team Review Requests team-filter handlers and restore the
|
|
2155
|
+
// persisted value.
|
|
2156
|
+
const teamFilterInput = document.getElementById('team-reviews-team-input');
|
|
2157
|
+
const teamFilterForm = document.getElementById('team-reviews-filter');
|
|
2158
|
+
const teamFilterClearBtn = document.getElementById('team-reviews-filter-clear');
|
|
2159
|
+
if (teamFilterInput) {
|
|
2160
|
+
// Restore the persisted filter into the controls on initial load (same
|
|
2161
|
+
// resync the reload paths use, so the displayed team always matches the
|
|
2162
|
+
// applied filter).
|
|
2163
|
+
syncTeamFilterControls(getStoredTeamFilter());
|
|
2164
|
+
// Clear the invalid state / inline hint as the user edits.
|
|
2165
|
+
teamFilterInput.addEventListener('input', function () {
|
|
2166
|
+
teamFilterInput.classList.remove('invalid');
|
|
2167
|
+
teamFilterInput.removeAttribute('aria-invalid');
|
|
2168
|
+
const hint = document.getElementById('team-reviews-filter-hint');
|
|
2169
|
+
if (hint) hint.hidden = true;
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
if (teamFilterForm) {
|
|
2173
|
+
teamFilterForm.addEventListener('submit', function (event) {
|
|
2174
|
+
event.preventDefault();
|
|
2175
|
+
applyTeamFilter();
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
if (teamFilterClearBtn) {
|
|
2179
|
+
teamFilterClearBtn.addEventListener('click', function () {
|
|
2180
|
+
if (teamFilterInput) teamFilterInput.value = '';
|
|
2181
|
+
applyTeamFilter();
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
|
|
1925
2185
|
// Set up PR Analyze button handler
|
|
1926
2186
|
const analyzeReviewBtn = document.getElementById('analyze-review-btn');
|
|
1927
2187
|
if (analyzeReviewBtn) {
|
|
@@ -1985,6 +2245,19 @@
|
|
|
1985
2245
|
rrHeader.insertBefore(rrBtn, rrHeader.firstChild);
|
|
1986
2246
|
}
|
|
1987
2247
|
|
|
2248
|
+
// Team Reviews tab: add to existing header. Insert before the fetched-at
|
|
2249
|
+
// label rather than as firstChild — the leading `.team-filter-group` carries
|
|
2250
|
+
// `margin-right: auto`, so a firstChild button would be shoved to the far
|
|
2251
|
+
// left. Placing it here keeps Select grouped with fetched-at + refresh on
|
|
2252
|
+
// the right, matching the other collection tabs.
|
|
2253
|
+
var trHeader = document.querySelector('#team-reviews-tab .tab-pane-header');
|
|
2254
|
+
if (trHeader) {
|
|
2255
|
+
var trBtn = createSelectButton('team-reviews-tab');
|
|
2256
|
+
teamReviewsSelection._toggleBtn = trBtn;
|
|
2257
|
+
var trFetchedAt = document.getElementById('team-reviews-fetched-at');
|
|
2258
|
+
trHeader.insertBefore(trBtn, trFetchedAt || trHeader.firstChild);
|
|
2259
|
+
}
|
|
2260
|
+
|
|
1988
2261
|
// My PRs tab: add to existing header
|
|
1989
2262
|
var mpHeader = document.querySelector('#my-prs-tab .tab-pane-header');
|
|
1990
2263
|
if (mpHeader) {
|