@oss-autopilot/core 0.42.1 → 0.42.3

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.
@@ -3543,6 +3543,10 @@ function debug(module2, message, ...args) {
3543
3543
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3544
3544
  console.error(`[${timestamp}] [DEBUG] [${module2}] ${message}`, ...args);
3545
3545
  }
3546
+ function info(module2, message, ...args) {
3547
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3548
+ console.error(`[${timestamp}] [INFO] [${module2}] ${message}`, ...args);
3549
+ }
3546
3550
  function warn(module2, message, ...args) {
3547
3551
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3548
3552
  console.error(`[${timestamp}] [WARN] [${module2}] ${message}`, ...args);
@@ -4383,10 +4387,10 @@ var init_state = __esm({
4383
4387
  }
4384
4388
  // === Dismiss / Undismiss Issues ===
4385
4389
  /**
4386
- * Dismiss an issue by URL. Dismissed issues are excluded from `new_response` notifications
4390
+ * Dismiss an issue or PR by URL. Dismissed URLs are excluded from `new_response` notifications
4387
4391
  * until new activity occurs after the dismiss timestamp.
4388
- * @param url - The full GitHub issue URL.
4389
- * @param timestamp - ISO timestamp of when the issue was dismissed.
4392
+ * @param url - The full GitHub issue or PR URL.
4393
+ * @param timestamp - ISO timestamp of when the issue/PR was dismissed.
4390
4394
  * @returns true if newly dismissed, false if already dismissed.
4391
4395
  */
4392
4396
  dismissIssue(url, timestamp) {
@@ -4400,8 +4404,8 @@ var init_state = __esm({
4400
4404
  return true;
4401
4405
  }
4402
4406
  /**
4403
- * Undismiss an issue by URL.
4404
- * @param url - The full GitHub issue URL.
4407
+ * Undismiss an issue or PR by URL.
4408
+ * @param url - The full GitHub issue or PR URL.
4405
4409
  * @returns true if found and removed, false if not dismissed.
4406
4410
  */
4407
4411
  undismissIssue(url) {
@@ -4412,8 +4416,8 @@ var init_state = __esm({
4412
4416
  return true;
4413
4417
  }
4414
4418
  /**
4415
- * Get the timestamp when an issue was dismissed.
4416
- * @param url - The full GitHub issue URL.
4419
+ * Get the timestamp when an issue or PR was dismissed.
4420
+ * @param url - The full GitHub issue or PR URL.
4417
4421
  * @returns The ISO dismiss timestamp, or undefined if not dismissed.
4418
4422
  */
4419
4423
  getIssueDismissedAt(url) {
@@ -4465,11 +4469,11 @@ var init_state = __esm({
4465
4469
  * @returns true if the PR is snoozed and the snooze has not expired.
4466
4470
  */
4467
4471
  isSnoozed(url) {
4468
- const info = this.getSnoozeInfo(url);
4469
- if (!info) return false;
4470
- const expiresAtMs = new Date(info.expiresAt).getTime();
4472
+ const info2 = this.getSnoozeInfo(url);
4473
+ if (!info2) return false;
4474
+ const expiresAtMs = new Date(info2.expiresAt).getTime();
4471
4475
  if (isNaN(expiresAtMs)) {
4472
- warn(MODULE2, `Invalid expiresAt for snoozed PR ${url}: "${info.expiresAt}". Treating as not snoozed.`);
4476
+ warn(MODULE2, `Invalid expiresAt for snoozed PR ${url}: "${info2.expiresAt}". Treating as not snoozed.`);
4473
4477
  return false;
4474
4478
  }
4475
4479
  return expiresAtMs > Date.now();
@@ -4490,8 +4494,8 @@ var init_state = __esm({
4490
4494
  if (!this.state.config.snoozedPRs) return [];
4491
4495
  const expired = [];
4492
4496
  const now = Date.now();
4493
- for (const [url, info] of Object.entries(this.state.config.snoozedPRs)) {
4494
- const expiresAtMs = new Date(info.expiresAt).getTime();
4497
+ for (const [url, info2] of Object.entries(this.state.config.snoozedPRs)) {
4498
+ const expiresAtMs = new Date(info2.expiresAt).getTime();
4495
4499
  if (isNaN(expiresAtMs) || expiresAtMs <= now) {
4496
4500
  expired.push(url);
4497
4501
  }
@@ -9968,8 +9972,8 @@ function throttling(octokit, octokitOptions) {
9968
9972
  "error",
9969
9973
  (e) => octokit.log.warn("Error in throttling-plugin limit handler", e)
9970
9974
  );
9971
- state.retryLimiter.on("failed", async function(error, info) {
9972
- const [state2, request2, options] = info.args;
9975
+ state.retryLimiter.on("failed", async function(error, info2) {
9976
+ const [state2, request2, options] = info2.args;
9973
9977
  const { pathname } = new URL(options.url, "http://github.test");
9974
9978
  const shouldRetryGraphQL = pathname.startsWith("/graphql") && error.status !== 401;
9975
9979
  if (!(shouldRetryGraphQL || error.status === 403 || error.status === 429)) {
@@ -10617,14 +10621,14 @@ function checkUnrespondedComments(comments, reviews, reviewComments, username) {
10617
10621
  for (const review of reviews) {
10618
10622
  if (!review.submitted_at) continue;
10619
10623
  const body = (review.body || "").trim();
10620
- if (!body && review.state !== "COMMENTED") continue;
10624
+ if (!body && review.state !== "COMMENTED" && review.state !== "CHANGES_REQUESTED") continue;
10621
10625
  const author = review.user?.login || "unknown";
10622
10626
  if (!body && review.state === "COMMENTED" && review.id != null) {
10623
10627
  if (isAllSelfReplies(review.id, reviewComments)) {
10624
10628
  continue;
10625
10629
  }
10626
10630
  }
10627
- const resolvedBody = body || (review.id != null ? getInlineCommentBody(review.id, reviewComments) : void 0) || "(posted inline review comments)";
10631
+ const resolvedBody = body || (review.id != null ? getInlineCommentBody(review.id, reviewComments) : void 0) || (review.state === "CHANGES_REQUESTED" ? "(requested changes via inline review comments)" : "(posted inline review comments)");
10628
10632
  timeline.push({
10629
10633
  author,
10630
10634
  body: resolvedBody,
@@ -11291,6 +11295,9 @@ var init_pr_monitor = __esm({
11291
11295
  } = input;
11292
11296
  if (hasUnrespondedComment) {
11293
11297
  if (latestCommitDate && lastMaintainerCommentDate && latestCommitDate > lastMaintainerCommentDate) {
11298
+ if (latestChangesRequestedDate && latestCommitDate < latestChangesRequestedDate) {
11299
+ return "needs_response";
11300
+ }
11294
11301
  if (ciStatus === "failing") return "failing_ci";
11295
11302
  return "changes_addressed";
11296
11303
  }
@@ -12178,7 +12185,7 @@ var init_issue_discovery = __esm({
12178
12185
  * Updates the state manager with the list and timestamp.
12179
12186
  */
12180
12187
  async fetchStarredRepos() {
12181
- console.log("Fetching starred repositories...");
12188
+ info(MODULE9, "Fetching starred repositories...");
12182
12189
  const starredRepos = [];
12183
12190
  try {
12184
12191
  const iterator2 = this.octokit.paginate.iterator(this.octokit.activity.listReposStarredByAuthenticatedUser, {
@@ -12199,11 +12206,11 @@ var init_issue_discovery = __esm({
12199
12206
  }
12200
12207
  pageCount++;
12201
12208
  if (pageCount >= 5) {
12202
- console.log("Reached pagination limit for starred repos (500)");
12209
+ info(MODULE9, "Reached pagination limit for starred repos (500)");
12203
12210
  break;
12204
12211
  }
12205
12212
  }
12206
- console.log(`Fetched ${starredRepos.length} starred repositories`);
12213
+ info(MODULE9, `Fetched ${starredRepos.length} starred repositories`);
12207
12214
  this.stateManager.setStarredRepos(starredRepos);
12208
12215
  return starredRepos;
12209
12216
  } catch (error) {
@@ -12234,6 +12241,48 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12234
12241
  }
12235
12242
  return this.stateManager.getStarredRepos();
12236
12243
  }
12244
+ /**
12245
+ * Shared pipeline for Phases 2 and 3: spam-filter, repo-exclusion, vetting, and star-count filter.
12246
+ * Extracts the common logic so each phase only needs to supply search results and context.
12247
+ */
12248
+ async filterVetAndScore(items, filterIssues, excludedRepoSets, remainingNeeded, minStars, phaseLabel) {
12249
+ const spamRepos = detectLabelFarmingRepos(items);
12250
+ if (spamRepos.size > 0) {
12251
+ const spamCount = items.filter((i) => spamRepos.has(i.repository_url.split("/").slice(-2).join("/"))).length;
12252
+ debug(
12253
+ MODULE9,
12254
+ `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(", ")}`
12255
+ );
12256
+ }
12257
+ const itemsToVet = filterIssues(items).filter((item) => {
12258
+ const repoFullName = item.repository_url.split("/").slice(-2).join("/");
12259
+ if (spamRepos.has(repoFullName)) return false;
12260
+ return excludedRepoSets.every((s) => !s.has(repoFullName));
12261
+ }).slice(0, remainingNeeded * 2);
12262
+ if (itemsToVet.length === 0) {
12263
+ debug(MODULE9, `[${phaseLabel}] All ${items.length} items filtered before vetting`);
12264
+ return { candidates: [], allVetFailed: false, rateLimitHit: false };
12265
+ }
12266
+ const {
12267
+ candidates: results,
12268
+ allFailed: allVetFailed,
12269
+ rateLimitHit
12270
+ } = await this.vetter.vetIssuesParallel(
12271
+ itemsToVet.map((i) => i.html_url),
12272
+ remainingNeeded,
12273
+ "normal"
12274
+ );
12275
+ const starFiltered = results.filter((c) => {
12276
+ if (c.projectHealth.checkFailed) return true;
12277
+ const stars = c.projectHealth.stargazersCount ?? 0;
12278
+ return stars >= minStars;
12279
+ });
12280
+ const starFilteredCount = results.length - starFiltered.length;
12281
+ if (starFilteredCount > 0) {
12282
+ debug(MODULE9, `[STAR_FILTER] Filtered ${starFilteredCount} ${phaseLabel} candidates below ${minStars} stars`);
12283
+ }
12284
+ return { candidates: starFiltered, allVetFailed, rateLimitHit };
12285
+ }
12237
12286
  /**
12238
12287
  * Search for issues matching our criteria.
12239
12288
  * Searches in priority order: merged-PR repos first (no label filter), then starred repos,
@@ -12245,6 +12294,7 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12245
12294
  const languages = options.languages || config.languages;
12246
12295
  const labels = options.labels || config.labels;
12247
12296
  const maxResults = options.maxResults || 10;
12297
+ const minStars = config.minStars ?? 50;
12248
12298
  const allCandidates = [];
12249
12299
  let phase0Error = null;
12250
12300
  let phase1Error = null;
@@ -12281,7 +12331,8 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12281
12331
  const includeDocIssues = config.includeDocIssues ?? true;
12282
12332
  const aiBlocklisted = new Set(config.aiPolicyBlocklist ?? DEFAULT_CONFIG.aiPolicyBlocklist ?? []);
12283
12333
  if (aiBlocklisted.size > 0) {
12284
- console.log(
12334
+ debug(
12335
+ MODULE9,
12285
12336
  `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(", ")}`
12286
12337
  );
12287
12338
  }
@@ -12304,7 +12355,8 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12304
12355
  if (phase0Repos.length > 0) {
12305
12356
  const mergedInPhase0 = Math.min(mergedPRRepos.length, phase0Repos.length);
12306
12357
  const openInPhase0 = phase0Repos.length - mergedInPhase0;
12307
- console.log(
12358
+ info(
12359
+ MODULE9,
12308
12360
  `Phase 0: Searching issues in ${phase0Repos.length} repos (${mergedInPhase0} merged-PR, ${openInPhase0} open-PR, no label filter)...`
12309
12361
  );
12310
12362
  const mergedPhase0Repos = phase0Repos.slice(0, mergedInPhase0);
@@ -12323,7 +12375,7 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12323
12375
  if (rateLimitHit) {
12324
12376
  rateLimitHitDuringSearch = true;
12325
12377
  }
12326
- console.log(`Found ${mergedCandidates.length} candidates from merged-PR repos`);
12378
+ info(MODULE9, `Found ${mergedCandidates.length} candidates from merged-PR repos`);
12327
12379
  }
12328
12380
  }
12329
12381
  const openPhase0Repos = phase0Repos.slice(mergedInPhase0);
@@ -12343,14 +12395,14 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12343
12395
  if (rateLimitHit) {
12344
12396
  rateLimitHitDuringSearch = true;
12345
12397
  }
12346
- console.log(`Found ${openCandidates.length} candidates from open-PR repos`);
12398
+ info(MODULE9, `Found ${openCandidates.length} candidates from open-PR repos`);
12347
12399
  }
12348
12400
  }
12349
12401
  }
12350
12402
  if (allCandidates.length < maxResults && starredRepos.length > 0) {
12351
12403
  const reposToSearch = starredRepos.filter((r) => !phase0RepoSet.has(r));
12352
12404
  if (reposToSearch.length > 0) {
12353
- console.log(`Phase 1: Searching issues in ${reposToSearch.length} starred repos...`);
12405
+ info(MODULE9, `Phase 1: Searching issues in ${reposToSearch.length} starred repos...`);
12354
12406
  const remainingNeeded = maxResults - allCandidates.length;
12355
12407
  if (remainingNeeded > 0) {
12356
12408
  const {
@@ -12365,13 +12417,13 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12365
12417
  if (rateLimitHit) {
12366
12418
  rateLimitHitDuringSearch = true;
12367
12419
  }
12368
- console.log(`Found ${starredCandidates.length} candidates from starred repos`);
12420
+ info(MODULE9, `Found ${starredCandidates.length} candidates from starred repos`);
12369
12421
  }
12370
12422
  }
12371
12423
  }
12372
12424
  let phase2Error = null;
12373
12425
  if (allCandidates.length < maxResults) {
12374
- console.log("Phase 2: General issue search...");
12426
+ info(MODULE9, "Phase 2: General issue search...");
12375
12427
  const remainingNeeded = maxResults - allCandidates.length;
12376
12428
  try {
12377
12429
  const { data } = await this.octokit.search.issuesAndPullRequests({
@@ -12381,43 +12433,20 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12381
12433
  per_page: remainingNeeded * 3
12382
12434
  // Fetch extra since some will be filtered
12383
12435
  });
12384
- console.log(`Found ${data.total_count} issues in general search, processing top ${data.items.length}...`);
12385
- const spamRepos = detectLabelFarmingRepos(data.items);
12386
- if (spamRepos.size > 0) {
12387
- const spamCount = data.items.filter(
12388
- (i) => spamRepos.has(i.repository_url.split("/").slice(-2).join("/"))
12389
- ).length;
12390
- console.log(
12391
- `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(", ")}`
12392
- );
12393
- }
12436
+ info(MODULE9, `Found ${data.total_count} issues in general search, processing top ${data.items.length}...`);
12394
12437
  const seenRepos = new Set(allCandidates.map((c) => c.issue.repo));
12395
- const itemsToVet = filterIssues(data.items).filter((item) => {
12396
- const repoFullName = item.repository_url.split("/").slice(-2).join("/");
12397
- return !spamRepos.has(repoFullName);
12398
- }).filter((item) => {
12399
- const repoFullName = item.repository_url.split("/").slice(-2).join("/");
12400
- return !phase0RepoSet.has(repoFullName) && !starredRepoSet.has(repoFullName) && !seenRepos.has(repoFullName);
12401
- }).slice(0, remainingNeeded * 2);
12402
12438
  const {
12403
- candidates: results,
12404
- allFailed: allVetFailed,
12439
+ candidates: starFiltered,
12440
+ allVetFailed,
12405
12441
  rateLimitHit: vetRateLimitHit
12406
- } = await this.vetter.vetIssuesParallel(
12407
- itemsToVet.map((i) => i.html_url),
12442
+ } = await this.filterVetAndScore(
12443
+ data.items,
12444
+ filterIssues,
12445
+ [phase0RepoSet, starredRepoSet, seenRepos],
12408
12446
  remainingNeeded,
12409
- "normal"
12447
+ minStars,
12448
+ "Phase 2"
12410
12449
  );
12411
- const minStars = config.minStars ?? 50;
12412
- const starFiltered = results.filter((c) => {
12413
- if (c.projectHealth.checkFailed) return true;
12414
- const stars = c.projectHealth.stargazersCount ?? 0;
12415
- return stars >= minStars;
12416
- });
12417
- const starFilteredCount = results.length - starFiltered.length;
12418
- if (starFilteredCount > 0) {
12419
- console.log(`[STAR_FILTER] Filtered ${starFilteredCount} candidates below ${minStars} stars`);
12420
- }
12421
12450
  allCandidates.push(...starFiltered);
12422
12451
  if (allVetFailed) {
12423
12452
  phase2Error = (phase2Error ? phase2Error + "; " : "") + "all vetting failed";
@@ -12425,7 +12454,7 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12425
12454
  if (vetRateLimitHit) {
12426
12455
  rateLimitHitDuringSearch = true;
12427
12456
  }
12428
- console.log(`Found ${starFiltered.length} candidates from general search`);
12457
+ info(MODULE9, `Found ${starFiltered.length} candidates from general search`);
12429
12458
  } catch (error) {
12430
12459
  const errMsg = errorMessage(error);
12431
12460
  phase2Error = errMsg;
@@ -12437,13 +12466,12 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12437
12466
  }
12438
12467
  let phase3Error = null;
12439
12468
  if (allCandidates.length < maxResults) {
12440
- console.log("Phase 3: Searching actively maintained repos...");
12469
+ info(MODULE9, "Phase 3: Searching actively maintained repos...");
12441
12470
  const remainingNeeded = maxResults - allCandidates.length;
12442
12471
  const thirtyDaysAgo = /* @__PURE__ */ new Date();
12443
12472
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
12444
12473
  const pushedSince = thirtyDaysAgo.toISOString().split("T")[0];
12445
- const phase3MinStars = config.minStars ?? 50;
12446
- const phase3Query = `is:issue is:open no:assignee ${langQuery} stars:>=${phase3MinStars} pushed:>=${pushedSince} archived:false`.replace(/ +/g, " ").trim();
12474
+ const phase3Query = `is:issue is:open no:assignee ${langQuery} stars:>=${minStars} pushed:>=${pushedSince} archived:false`.replace(/ +/g, " ").trim();
12447
12475
  try {
12448
12476
  const { data } = await this.octokit.search.issuesAndPullRequests({
12449
12477
  q: phase3Query,
@@ -12451,42 +12479,23 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12451
12479
  order: "desc",
12452
12480
  per_page: remainingNeeded * 3
12453
12481
  });
12454
- console.log(
12482
+ info(
12483
+ MODULE9,
12455
12484
  `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`
12456
12485
  );
12457
- const spamRepos = detectLabelFarmingRepos(data.items);
12458
- if (spamRepos.size > 0) {
12459
- const spamCount = data.items.filter(
12460
- (i) => spamRepos.has(i.repository_url.split("/").slice(-2).join("/"))
12461
- ).length;
12462
- console.log(
12463
- `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(", ")}`
12464
- );
12465
- }
12466
12486
  const seenRepos = new Set(allCandidates.map((c) => c.issue.repo));
12467
- const itemsToVet = filterIssues(data.items).filter((item) => {
12468
- const repoFullName = item.repository_url.split("/").slice(-2).join("/");
12469
- return !spamRepos.has(repoFullName) && !phase0RepoSet.has(repoFullName) && !starredRepoSet.has(repoFullName) && !seenRepos.has(repoFullName);
12470
- }).slice(0, remainingNeeded * 2);
12471
12487
  const {
12472
- candidates: results,
12473
- allFailed: allVetFailed,
12488
+ candidates: starFiltered,
12489
+ allVetFailed,
12474
12490
  rateLimitHit: vetRateLimitHit
12475
- } = await this.vetter.vetIssuesParallel(
12476
- itemsToVet.map((i) => i.html_url),
12491
+ } = await this.filterVetAndScore(
12492
+ data.items,
12493
+ filterIssues,
12494
+ [phase0RepoSet, starredRepoSet, seenRepos],
12477
12495
  remainingNeeded,
12478
- "normal"
12496
+ minStars,
12497
+ "Phase 3"
12479
12498
  );
12480
- const minStars = config.minStars ?? 50;
12481
- const starFiltered = results.filter((c) => {
12482
- if (c.projectHealth.checkFailed) return true;
12483
- const stars = c.projectHealth.stargazersCount ?? 0;
12484
- return stars >= minStars;
12485
- });
12486
- const starFilteredCount = results.length - starFiltered.length;
12487
- if (starFilteredCount > 0) {
12488
- console.log(`[STAR_FILTER] Filtered ${starFilteredCount} Phase 3 candidates below ${minStars} stars`);
12489
- }
12490
12499
  allCandidates.push(...starFiltered);
12491
12500
  if (allVetFailed) {
12492
12501
  phase3Error = "all vetting failed";
@@ -12494,7 +12503,7 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12494
12503
  if (vetRateLimitHit) {
12495
12504
  rateLimitHitDuringSearch = true;
12496
12505
  }
12497
- console.log(`Found ${starFiltered.length} candidates from maintained-repo search`);
12506
+ info(MODULE9, `Found ${starFiltered.length} candidates from maintained-repo search`);
12498
12507
  } catch (error) {
12499
12508
  const errMsg = errorMessage(error);
12500
12509
  phase3Error = errMsg;
@@ -12665,7 +12674,7 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12665
12674
  content += `- **Recommendation**: Y = approve, N = skip, ? = needs_review
12666
12675
  `;
12667
12676
  fs4.writeFileSync(outputFile, content, "utf-8");
12668
- console.log(`Saved ${sorted.length} issues to ${outputFile}`);
12677
+ info(MODULE9, `Saved ${sorted.length} issues to ${outputFile}`);
12669
12678
  return outputFile;
12670
12679
  }
12671
12680
  /**
@@ -13698,14 +13707,15 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
13698
13707
  const snoozedUrls = new Set(
13699
13708
  Object.keys(stateManager2.getState().config.snoozedPRs ?? {}).filter((url) => stateManager2.isSnoozed(url))
13700
13709
  );
13701
- const actionableIssues = collectActionableIssues(activePRs, snoozedUrls);
13710
+ const dismissedUrls = new Set(Object.keys(stateManager2.getState().config.dismissedIssues ?? {}));
13711
+ const nonDismissedPRs = activePRs.filter((pr) => !dismissedUrls.has(pr.url));
13712
+ const actionableIssues = collectActionableIssues(nonDismissedPRs, snoozedUrls);
13702
13713
  digest.summary.totalNeedingAttention = actionableIssues.length;
13703
13714
  const briefSummary = formatBriefSummary(digest, actionableIssues.length, issueResponses.length);
13704
13715
  const actionMenu = computeActionMenu(actionableIssues, capacity, filteredCommentedIssues);
13705
13716
  const repoGroups = groupPRsByRepo(activePRs);
13706
13717
  return {
13707
13718
  digest,
13708
- updates: [],
13709
13719
  capacity,
13710
13720
  summary,
13711
13721
  briefSummary,
@@ -13719,7 +13729,6 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
13719
13729
  function toDailyOutput(result) {
13720
13730
  return {
13721
13731
  digest: deduplicateDigest(result.digest),
13722
- updates: result.updates,
13723
13732
  capacity: result.capacity,
13724
13733
  summary: result.summary,
13725
13734
  briefSummary: result.briefSummary,
@@ -13856,8 +13865,12 @@ var init_search = __esm({
13856
13865
  // src/commands/validation.ts
13857
13866
  function validateGitHubUrl(url, pattern, entityType) {
13858
13867
  if (pattern.test(url)) return;
13859
- const example = entityType === "PR" ? "https://github.com/owner/repo/pull/123" : "https://github.com/owner/repo/issues/123";
13860
- throw new ValidationError(`Invalid ${entityType} URL: ${url}. Expected format: ${example}`);
13868
+ const examples = {
13869
+ PR: "https://github.com/owner/repo/pull/123",
13870
+ issue: "https://github.com/owner/repo/issues/123",
13871
+ "issue or PR": "https://github.com/owner/repo/issues/123 or https://github.com/owner/repo/pull/123"
13872
+ };
13873
+ throw new ValidationError(`Invalid ${entityType} URL: ${url}. Expected format: ${examples[entityType]}`);
13861
13874
  }
13862
13875
  function validateUrl(url) {
13863
13876
  if (url.length > MAX_URL_LENGTH) {
@@ -13895,13 +13908,13 @@ function validateGitHubUsername(username) {
13895
13908
  }
13896
13909
  return trimmed;
13897
13910
  }
13898
- var PR_URL_PATTERN, ISSUE_URL_PATTERN, MAX_URL_LENGTH, MAX_MESSAGE_LENGTH, MAX_USERNAME_LENGTH, USERNAME_CHARS_PATTERN, CONSECUTIVE_HYPHENS_PATTERN;
13911
+ var PR_URL_PATTERN, ISSUE_OR_PR_URL_PATTERN, MAX_URL_LENGTH, MAX_MESSAGE_LENGTH, MAX_USERNAME_LENGTH, USERNAME_CHARS_PATTERN, CONSECUTIVE_HYPHENS_PATTERN;
13899
13912
  var init_validation = __esm({
13900
13913
  "src/commands/validation.ts"() {
13901
13914
  "use strict";
13902
13915
  init_errors();
13903
13916
  PR_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+$/;
13904
- ISSUE_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+$/;
13917
+ ISSUE_OR_PR_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/(issues|pull)\/\d+$/;
13905
13918
  MAX_URL_LENGTH = 2048;
13906
13919
  MAX_MESSAGE_LENGTH = 1e3;
13907
13920
  MAX_USERNAME_LENGTH = 39;
@@ -14554,7 +14567,7 @@ var init_dashboard_data = __esm({
14554
14567
  }
14555
14568
  });
14556
14569
 
14557
- // src/commands/dashboard-templates.ts
14570
+ // src/commands/dashboard-formatters.ts
14558
14571
  function escapeHtml(text) {
14559
14572
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
14560
14573
  }
@@ -14573,78 +14586,18 @@ function buildDashboardStats(digest, state) {
14573
14586
  mergeRate: `${(summary.mergeRate ?? 0).toFixed(1)}%`
14574
14587
  };
14575
14588
  }
14576
- function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state, issueResponses = []) {
14577
- const approachingDormantDays = state.config?.approachingDormantDays ?? 25;
14578
- const shelvedPRs = digest.shelvedPRs || [];
14579
- const autoUnshelvedPRs = digest.autoUnshelvedPRs || [];
14580
- const recentlyMerged = digest.recentlyMergedPRs || [];
14581
- const shelvedUrls = new Set(shelvedPRs.map((pr) => pr.url));
14582
- const activePRList = (digest.openPRs || []).filter((pr) => !shelvedUrls.has(pr.url));
14583
- const actionRequired = [
14584
- ...digest.prsNeedingResponse || [],
14585
- ...digest.needsChangesPRs || [],
14586
- ...digest.ciFailingPRs || [],
14587
- ...digest.mergeConflictPRs || [],
14588
- ...digest.incompleteChecklistPRs || [],
14589
- ...digest.missingRequiredFilesPRs || [],
14590
- ...digest.needsRebasePRs || []
14591
- ];
14592
- const waitingOnOthers = [
14593
- ...digest.changesAddressedPRs || [],
14594
- ...digest.waitingOnMaintainerPRs || [],
14595
- ...digest.ciBlockedPRs || [],
14596
- ...digest.ciNotRunningPRs || []
14597
- ];
14598
- function truncateTitle(title, max = 50) {
14599
- const truncated = title.length <= max ? title : title.slice(0, max) + "...";
14600
- return escapeHtml(truncated);
14601
- }
14602
- function renderHealthItems(prs, cssClass, svgPaths, labelFn, metaFn) {
14603
- return prs.map((pr) => {
14604
- const rawLabel = typeof labelFn === "string" ? labelFn : labelFn(pr);
14605
- const label = escapeHtml(rawLabel);
14606
- return `
14607
- <div class="health-item ${cssClass}" data-status="${cssClass}" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
14608
- <div class="health-icon">
14609
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
14610
- ${svgPaths}
14611
- </svg>
14612
- </div>
14613
- <div class="health-content">
14614
- <div class="health-title"><a href="${escapeHtml(pr.url)}" target="_blank">${escapeHtml(pr.repo)}#${pr.number}</a> - ${label}</div>
14615
- <div class="health-meta">${metaFn(pr)}</div>
14616
- </div>
14617
- </div>`;
14618
- }).join("");
14619
- }
14620
- const SVG = {
14621
- comment: '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',
14622
- edit: '<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>',
14623
- xCircle: '<circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>',
14624
- conflict: '<path d="M8 3v3a2 2 0 0 1-2 2H3"/><path d="M21 8h-3a2 2 0 0 1-2-2V3"/><path d="M3 16h3a2 2 0 0 1 2 2v3"/><path d="M16 21v-3a2 2 0 0 1 2-2h3"/>',
14625
- checklist: '<path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>',
14626
- file: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/>',
14627
- checkCircle: '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>',
14628
- clock: '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
14629
- lock: '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
14630
- infoCircle: '<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>',
14631
- refresh: '<polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>',
14632
- box: '<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>',
14633
- bell: '<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/>',
14634
- gitMerge: '<circle cx="7" cy="18" r="3"/><circle cx="7" cy="6" r="3"/><circle cx="17" cy="12" r="3"/><line x1="7" y1="9" x2="7" y2="15"/><path d="M7 9c0 4 10 3 10 3"/>'
14635
- };
14636
- const titleMeta = (pr) => truncateTitle(pr.title);
14637
- return `<!DOCTYPE html>
14638
- <html lang="en">
14639
- <head>
14640
- <meta charset="UTF-8">
14641
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
14642
- <title>OSS Autopilot - Mission Control</title>
14643
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
14644
- <link rel="preconnect" href="https://fonts.googleapis.com">
14645
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
14646
- <link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
14647
- <style>
14589
+ var init_dashboard_formatters = __esm({
14590
+ "src/commands/dashboard-formatters.ts"() {
14591
+ "use strict";
14592
+ }
14593
+ });
14594
+
14595
+ // src/commands/dashboard-styles.ts
14596
+ var DASHBOARD_CSS;
14597
+ var init_dashboard_styles = __esm({
14598
+ "src/commands/dashboard-styles.ts"() {
14599
+ "use strict";
14600
+ DASHBOARD_CSS = `
14648
14601
  :root, [data-theme="dark"] {
14649
14602
  --bg-base: #080b10;
14650
14603
  --bg-surface: rgba(22, 27, 34, 0.65);
@@ -15404,167 +15357,526 @@ function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpene
15404
15357
  .health-item[data-hidden="true"] {
15405
15358
  display: none;
15406
15359
  }
15407
- </style>
15408
- </head>
15409
- <body>
15410
- <div class="container">
15411
- <header class="header">
15412
- <div class="header-left">
15413
- <div class="logo">
15414
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
15415
- <circle cx="12" cy="12" r="10"/>
15416
- <path d="M12 6v6l4 2"/>
15417
- </svg>
15418
- </div>
15419
- <div>
15420
- <h1>OSS Autopilot</h1>
15421
- <span class="header-subtitle">Mission Control</span>
15422
- </div>
15423
- </div>
15424
- <div class="header-controls">
15425
- <div class="timestamp">
15426
- Last updated: ${digest.generatedAt ? new Date(digest.generatedAt).toLocaleString("en-US", {
15427
- weekday: "short",
15428
- month: "short",
15429
- day: "numeric",
15430
- year: "numeric",
15431
- hour: "2-digit",
15432
- minute: "2-digit",
15433
- second: "2-digit",
15434
- hour12: false
15435
- }) : "Unknown"}
15436
- </div>
15437
- <button class="theme-toggle" id="themeToggle" title="Toggle light/dark mode">
15438
- <svg id="themeIconSun" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
15439
- <circle cx="12" cy="12" r="5"/>
15440
- <line x1="12" y1="1" x2="12" y2="3"/>
15441
- <line x1="12" y1="21" x2="12" y2="23"/>
15442
- <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
15443
- <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
15444
- <line x1="1" y1="12" x2="3" y2="12"/>
15445
- <line x1="21" y1="12" x2="23" y2="12"/>
15446
- <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
15447
- <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
15448
- </svg>
15449
- <svg id="themeIconMoon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;">
15450
- <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
15451
- </svg>
15452
- <span id="themeLabel">Light</span>
15453
- </button>
15454
- </div>
15455
- </header>
15456
-
15457
- <div class="stats-grid">
15458
- <div class="stat-card active">
15459
- <div class="stat-value">${stats.activePRs}</div>
15460
- <div class="stat-label">Active PRs</div>
15461
- </div>
15462
- <div class="stat-card shelved">
15463
- <div class="stat-value">${stats.shelvedPRs}</div>
15464
- <div class="stat-label">Shelved</div>
15465
- </div>
15466
- <div class="stat-card merged">
15467
- <div class="stat-value">${stats.mergedPRs}</div>
15468
- <div class="stat-label">Merged</div>
15469
- </div>
15470
- <div class="stat-card closed">
15471
- <div class="stat-value">${stats.closedPRs}</div>
15472
- <div class="stat-label">Closed</div>
15473
- </div>
15474
- <div class="stat-card rate">
15475
- <div class="stat-value">${stats.mergeRate}</div>
15476
- <div class="stat-label">Merge Rate</div>
15477
- </div>
15478
- </div>
15360
+ `;
15361
+ }
15362
+ });
15479
15363
 
15480
- <div class="filter-toolbar" id="filterToolbar">
15481
- <label>Filters</label>
15482
- <input type="text" class="filter-search" id="searchInput" placeholder="Search by PR title..." />
15483
- <select class="filter-select" id="statusFilter">
15484
- <option value="all">All Statuses</option>
15485
- <option value="needs-response">Needs Response</option>
15486
- <option value="needs-changes">Needs Changes</option>
15487
- <option value="ci-failing">CI Failing</option>
15488
- <option value="conflict">Merge Conflict</option>
15489
- <option value="changes-addressed">Changes Addressed</option>
15490
- <option value="waiting-maintainer">Waiting on Maintainer</option>
15491
- <option value="ci-blocked">CI Blocked</option>
15492
- <option value="ci-not-running">CI Not Running</option>
15493
- <option value="incomplete-checklist">Incomplete Checklist</option>
15494
- <option value="missing-files">Missing Files</option>
15495
- <option value="needs-rebase">Needs Rebase</option>
15496
- <option value="shelved">Shelved</option>
15497
- <option value="merged">Recently Merged</option>
15498
- <option value="closed">Recently Closed</option>
15499
- <option value="auto-unshelved">Auto-Unshelved</option>
15500
- <option value="active">Active (No Issues)</option>
15501
- </select>
15502
- <select class="filter-select" id="repoFilter">
15503
- <option value="all">All Repositories</option>
15504
- ${(() => {
15505
- const repos = /* @__PURE__ */ new Set();
15506
- for (const pr of activePRList) repos.add(pr.repo);
15507
- for (const pr of shelvedPRs) repos.add(pr.repo);
15508
- for (const pr of actionRequired) repos.add(pr.repo);
15509
- for (const pr of waitingOnOthers) repos.add(pr.repo);
15510
- for (const pr of recentlyMerged) repos.add(pr.repo);
15511
- for (const pr of digest.recentlyClosedPRs || []) repos.add(pr.repo);
15512
- for (const pr of autoUnshelvedPRs) repos.add(pr.repo);
15513
- return Array.from(repos).sort().map((repo) => `<option value="${escapeHtml(repo)}">${escapeHtml(repo)}</option>`).join("\n ");
15514
- })()}
15515
- </select>
15516
- <span class="filter-count" id="filterCount"></span>
15517
- </div>
15364
+ // src/commands/dashboard-components.ts
15365
+ function truncateTitle(title, max = 50) {
15366
+ const truncated = title.length <= max ? title : title.slice(0, max) + "...";
15367
+ return escapeHtml(truncated);
15368
+ }
15369
+ function renderHealthItems(prs, cssClass, svgPaths, labelFn, metaFn) {
15370
+ return prs.map((pr) => {
15371
+ const rawLabel = typeof labelFn === "string" ? labelFn : labelFn(pr);
15372
+ const label = escapeHtml(rawLabel);
15373
+ return `
15374
+ <div class="health-item ${cssClass}" data-status="${cssClass}" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
15375
+ <div class="health-icon">
15376
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
15377
+ ${svgPaths}
15378
+ </svg>
15379
+ </div>
15380
+ <div class="health-content">
15381
+ <div class="health-title"><a href="${escapeHtml(pr.url)}" target="_blank">${escapeHtml(pr.repo)}#${pr.number}</a> - ${label}</div>
15382
+ <div class="health-meta">${metaFn(pr)}</div>
15383
+ </div>
15384
+ </div>`;
15385
+ }).join("");
15386
+ }
15387
+ function titleMeta(pr) {
15388
+ return truncateTitle(pr.title);
15389
+ }
15390
+ var SVG_ICONS;
15391
+ var init_dashboard_components = __esm({
15392
+ "src/commands/dashboard-components.ts"() {
15393
+ "use strict";
15394
+ init_dashboard_formatters();
15395
+ SVG_ICONS = {
15396
+ comment: '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',
15397
+ edit: '<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>',
15398
+ xCircle: '<circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>',
15399
+ conflict: '<path d="M8 3v3a2 2 0 0 1-2 2H3"/><path d="M21 8h-3a2 2 0 0 1-2-2V3"/><path d="M3 16h3a2 2 0 0 1 2 2v3"/><path d="M16 21v-3a2 2 0 0 1 2-2h3"/>',
15400
+ checklist: '<path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>',
15401
+ file: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/>',
15402
+ checkCircle: '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>',
15403
+ clock: '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
15404
+ lock: '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
15405
+ infoCircle: '<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>',
15406
+ refresh: '<polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>',
15407
+ box: '<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>',
15408
+ bell: '<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/>',
15409
+ gitMerge: '<circle cx="7" cy="18" r="3"/><circle cx="7" cy="6" r="3"/><circle cx="17" cy="12" r="3"/><line x1="7" y1="9" x2="7" y2="15"/><path d="M7 9c0 4 10 3 10 3"/>'
15410
+ };
15411
+ }
15412
+ });
15518
15413
 
15519
- ${actionRequired.length > 0 ? `
15520
- <section class="health-section">
15521
- <div class="health-header">
15522
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-warning)" stroke-width="2">
15523
- <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
15524
- <line x1="12" y1="9" x2="12" y2="13"/>
15525
- <line x1="12" y1="17" x2="12.01" y2="17"/>
15526
- </svg>
15527
- <h2>Action Required</h2>
15528
- <span class="health-badge">${actionRequired.length} issue${actionRequired.length !== 1 ? "s" : ""}</span>
15529
- </div>
15530
- <div class="health-items">
15531
- ${renderHealthItems(
15532
- digest.prsNeedingResponse || [],
15533
- "needs-response",
15534
- SVG.comment,
15535
- "Needs Response",
15536
- (pr) => pr.lastMaintainerComment ? `@${escapeHtml(pr.lastMaintainerComment.author)}: ${truncateTitle(pr.lastMaintainerComment.body, 40)}` : truncateTitle(pr.title)
15537
- )}
15538
- ${renderHealthItems(digest.needsChangesPRs || [], "needs-changes", SVG.edit, "Needs Changes", titleMeta)}
15539
- ${renderHealthItems(digest.ciFailingPRs || [], "ci-failing", SVG.xCircle, "CI Failing", titleMeta)}
15540
- ${renderHealthItems(digest.mergeConflictPRs || [], "conflict", SVG.conflict, "Merge Conflict", titleMeta)}
15541
- ${renderHealthItems(
15542
- digest.incompleteChecklistPRs || [],
15543
- "incomplete-checklist",
15544
- SVG.checklist,
15545
- (pr) => `Incomplete Checklist${pr.checklistStats ? ` (${pr.checklistStats.checked}/${pr.checklistStats.total})` : ""}`,
15546
- titleMeta
15547
- )}
15548
- ${renderHealthItems(
15549
- digest.missingRequiredFilesPRs || [],
15550
- "missing-files",
15551
- SVG.file,
15552
- "Missing Required Files",
15553
- (pr) => pr.missingRequiredFiles ? escapeHtml(pr.missingRequiredFiles.join(", ")) : truncateTitle(pr.title)
15554
- )}
15555
- ${renderHealthItems(
15556
- digest.needsRebasePRs || [],
15557
- "needs-rebase",
15558
- SVG.refresh,
15559
- (pr) => `Needs Rebase${pr.commitsBehindUpstream ? ` (${pr.commitsBehindUpstream} behind)` : ""}`,
15560
- titleMeta
15561
- )}
15562
- </div>
15563
- </section>
15564
- ` : ""}
15414
+ // src/commands/dashboard-scripts.ts
15415
+ function generateDashboardScripts(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state) {
15416
+ const statusChart = `
15417
+ Chart.defaults.color = '#6e7681';
15418
+ Chart.defaults.borderColor = 'rgba(48, 54, 61, 0.4)';
15419
+ Chart.defaults.font.family = "'Geist', sans-serif";
15420
+ Chart.defaults.font.size = 11;
15565
15421
 
15566
- ${waitingOnOthers.length > 0 ? `
15567
- <section class="health-section waiting-section">
15422
+ // === Status Doughnut ===
15423
+ new Chart(document.getElementById('statusChart'), {
15424
+ type: 'doughnut',
15425
+ data: {
15426
+ labels: ['Active', 'Shelved', 'Merged', 'Closed'],
15427
+ datasets: [{
15428
+ data: [${stats.activePRs}, ${stats.shelvedPRs}, ${stats.mergedPRs}, ${stats.closedPRs}],
15429
+ backgroundColor: ['#3fb950', '#6e7681', '#a855f7', '#484f58'],
15430
+ borderColor: 'rgba(8, 11, 16, 0.8)',
15431
+ borderWidth: 2,
15432
+ hoverOffset: 8
15433
+ }]
15434
+ },
15435
+ options: {
15436
+ responsive: true,
15437
+ maintainAspectRatio: false,
15438
+ cutout: '65%',
15439
+ plugins: {
15440
+ legend: {
15441
+ position: 'bottom',
15442
+ labels: { padding: 16, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } }
15443
+ }
15444
+ }
15445
+ }
15446
+ });`;
15447
+ const repoChart = (() => {
15448
+ const { excludeRepos: exRepos = [], excludeOrgs: exOrgs, minStars } = state.config;
15449
+ const starThreshold = minStars ?? 50;
15450
+ const shouldExcludeRepo = (repo) => {
15451
+ const repoLower = repo.toLowerCase();
15452
+ if (exRepos.some((r) => r.toLowerCase() === repoLower)) return true;
15453
+ if (exOrgs?.some((o) => o.toLowerCase() === repoLower.split("/")[0])) return true;
15454
+ const score = (state.repoScores || {})[repo];
15455
+ if (score?.stargazersCount !== void 0 && score.stargazersCount < starThreshold) return true;
15456
+ return false;
15457
+ };
15458
+ const allRepoEntries = Object.entries(
15459
+ // Rebuild from full prsByRepo to get all repos, not just top 10
15460
+ (() => {
15461
+ const all = {};
15462
+ for (const pr of digest.openPRs || []) {
15463
+ if (shouldExcludeRepo(pr.repo)) continue;
15464
+ if (!all[pr.repo]) all[pr.repo] = { active: 0, merged: 0, closed: 0 };
15465
+ all[pr.repo].active++;
15466
+ }
15467
+ for (const [repo, score] of Object.entries(state.repoScores || {})) {
15468
+ if (shouldExcludeRepo(repo)) continue;
15469
+ if (!all[repo]) all[repo] = { active: 0, merged: 0, closed: 0 };
15470
+ all[repo].merged = score.mergedPRCount;
15471
+ all[repo].closed = score.closedWithoutMergeCount;
15472
+ }
15473
+ return all;
15474
+ })()
15475
+ ).sort((a, b) => {
15476
+ const totalA = a[1].merged + a[1].active + a[1].closed;
15477
+ const totalB = b[1].merged + b[1].active + b[1].closed;
15478
+ return totalB - totalA;
15479
+ });
15480
+ const displayRepos = allRepoEntries.slice(0, 10);
15481
+ const otherRepos = allRepoEntries.slice(10);
15482
+ const grandTotal = allRepoEntries.reduce((sum, [, d]) => sum + d.merged + d.active + d.closed, 0);
15483
+ if (otherRepos.length > 0) {
15484
+ const otherData = otherRepos.reduce(
15485
+ (acc, [, d]) => ({
15486
+ active: acc.active + d.active,
15487
+ merged: acc.merged + d.merged,
15488
+ closed: acc.closed + d.closed
15489
+ }),
15490
+ { active: 0, merged: 0, closed: 0 }
15491
+ );
15492
+ displayRepos.push(["Other", otherData]);
15493
+ }
15494
+ const repoLabels = displayRepos.map(([repo]) => repo === "Other" ? "Other" : repo.split("/")[1] || repo);
15495
+ const mergedData = displayRepos.map(([, d]) => d.merged);
15496
+ const activeData = displayRepos.map(([, d]) => d.active);
15497
+ const closedData = displayRepos.map(([, d]) => d.closed);
15498
+ return `
15499
+ new Chart(document.getElementById('reposChart'), {
15500
+ type: 'bar',
15501
+ data: {
15502
+ labels: ${JSON.stringify(repoLabels)},
15503
+ datasets: [
15504
+ { label: 'Merged', data: ${JSON.stringify(mergedData)}, backgroundColor: '#a855f7', borderRadius: 3 },
15505
+ { label: 'Active', data: ${JSON.stringify(activeData)}, backgroundColor: '#3fb950', borderRadius: 3 },
15506
+ { label: 'Closed', data: ${JSON.stringify(closedData)}, backgroundColor: '#484f58', borderRadius: 3 }
15507
+ ]
15508
+ },
15509
+ options: {
15510
+ responsive: true,
15511
+ maintainAspectRatio: false,
15512
+ scales: {
15513
+ x: { stacked: true, grid: { display: false }, ticks: { font: { size: 10 } } },
15514
+ y: { stacked: true, grid: { color: 'rgba(48, 54, 61, 0.3)' }, ticks: { stepSize: 1 } }
15515
+ },
15516
+ plugins: {
15517
+ legend: { position: 'bottom', labels: { padding: 16, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } } },
15518
+ tooltip: {
15519
+ callbacks: {
15520
+ afterBody: function(context) {
15521
+ const idx = context[0].dataIndex;
15522
+ const total = ${JSON.stringify(mergedData)}[idx] + ${JSON.stringify(activeData)}[idx] + ${JSON.stringify(closedData)}[idx];
15523
+ const pct = ${grandTotal} > 0 ? ((total / ${grandTotal}) * 100).toFixed(1) : '0.0';
15524
+ return pct + '% of all PRs';
15525
+ }
15526
+ }
15527
+ }
15528
+ }
15529
+ }
15530
+ });`;
15531
+ })();
15532
+ const timelineChart = (() => {
15533
+ const now = /* @__PURE__ */ new Date();
15534
+ const allMonths = [];
15535
+ for (let offset = 5; offset >= 0; offset--) {
15536
+ const d = new Date(now.getFullYear(), now.getMonth() - offset, 1);
15537
+ allMonths.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`);
15538
+ }
15539
+ return `
15540
+ const timelineMonths = ${JSON.stringify(allMonths)};
15541
+ const openedData = ${JSON.stringify(monthlyOpened)};
15542
+ const mergedData = ${JSON.stringify(monthlyMerged)};
15543
+ const closedData = ${JSON.stringify(monthlyClosed)};
15544
+ new Chart(document.getElementById('monthlyChart'), {
15545
+ type: 'bar',
15546
+ data: {
15547
+ labels: timelineMonths,
15548
+ datasets: [
15549
+ {
15550
+ label: 'Opened',
15551
+ data: timelineMonths.map(m => openedData[m] || 0),
15552
+ backgroundColor: '#58a6ff',
15553
+ borderRadius: 3
15554
+ },
15555
+ {
15556
+ label: 'Merged',
15557
+ data: timelineMonths.map(m => mergedData[m] || 0),
15558
+ backgroundColor: '#a855f7',
15559
+ borderRadius: 3
15560
+ },
15561
+ {
15562
+ label: 'Closed',
15563
+ data: timelineMonths.map(m => closedData[m] || 0),
15564
+ backgroundColor: '#484f58',
15565
+ borderRadius: 3
15566
+ }
15567
+ ]
15568
+ },
15569
+ options: {
15570
+ responsive: true,
15571
+ maintainAspectRatio: false,
15572
+ scales: {
15573
+ x: { grid: { display: false } },
15574
+ y: { grid: { color: 'rgba(48, 54, 61, 0.3)' }, beginAtZero: true, ticks: { stepSize: 1 } }
15575
+ },
15576
+ plugins: {
15577
+ legend: { position: 'bottom', labels: { padding: 16, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } } }
15578
+ },
15579
+ interaction: { intersect: false, mode: 'index' }
15580
+ }
15581
+ });`;
15582
+ })();
15583
+ return THEME_AND_FILTER_SCRIPT + statusChart + "\n" + repoChart + "\n" + timelineChart;
15584
+ }
15585
+ var THEME_AND_FILTER_SCRIPT;
15586
+ var init_dashboard_scripts = __esm({
15587
+ "src/commands/dashboard-scripts.ts"() {
15588
+ "use strict";
15589
+ THEME_AND_FILTER_SCRIPT = `
15590
+ // === Theme Toggle ===
15591
+ (function() {
15592
+ var html = document.documentElement;
15593
+ var toggle = document.getElementById('themeToggle');
15594
+ var sunIcon = document.getElementById('themeIconSun');
15595
+ var moonIcon = document.getElementById('themeIconMoon');
15596
+ var label = document.getElementById('themeLabel');
15597
+
15598
+ function getEffectiveTheme() {
15599
+ try {
15600
+ var stored = localStorage.getItem('oss-dashboard-theme');
15601
+ if (stored === 'light' || stored === 'dark') return stored;
15602
+ } catch (e) { /* localStorage unavailable (private browsing) */ }
15603
+ return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
15604
+ }
15605
+
15606
+ function applyTheme(theme) {
15607
+ html.setAttribute('data-theme', theme);
15608
+ if (theme === 'light') {
15609
+ sunIcon.style.display = 'none';
15610
+ moonIcon.style.display = 'block';
15611
+ label.textContent = 'Dark';
15612
+ } else {
15613
+ sunIcon.style.display = 'block';
15614
+ moonIcon.style.display = 'none';
15615
+ label.textContent = 'Light';
15616
+ }
15617
+ }
15618
+
15619
+ applyTheme(getEffectiveTheme());
15620
+
15621
+ toggle.addEventListener('click', function() {
15622
+ var current = html.getAttribute('data-theme');
15623
+ var next = current === 'dark' ? 'light' : 'dark';
15624
+ try { localStorage.setItem('oss-dashboard-theme', next); } catch (e) { /* private browsing */ }
15625
+ applyTheme(next);
15626
+ });
15627
+ })();
15628
+
15629
+ // === Filtering & Search ===
15630
+ (function() {
15631
+ var searchInput = document.getElementById('searchInput');
15632
+ var statusFilter = document.getElementById('statusFilter');
15633
+ var repoFilter = document.getElementById('repoFilter');
15634
+ var filterCount = document.getElementById('filterCount');
15635
+
15636
+ function applyFilters() {
15637
+ var query = searchInput.value.toLowerCase().trim();
15638
+ var status = statusFilter.value;
15639
+ var repo = repoFilter.value;
15640
+ var allItems = document.querySelectorAll('.health-item[data-status], .pr-item[data-status]');
15641
+ var visible = 0;
15642
+ var total = allItems.length;
15643
+
15644
+ allItems.forEach(function(item) {
15645
+ var itemStatus = item.getAttribute('data-status') || '';
15646
+ var itemRepo = item.getAttribute('data-repo') || '';
15647
+ var itemTitle = item.getAttribute('data-title') || '';
15648
+
15649
+ var matchesStatus = (status === 'all') || (itemStatus === status);
15650
+ var matchesRepo = (repo === 'all') || (itemRepo === repo);
15651
+ var matchesSearch = !query || itemTitle.indexOf(query) !== -1;
15652
+
15653
+ if (matchesStatus && matchesRepo && matchesSearch) {
15654
+ item.setAttribute('data-hidden', 'false');
15655
+ visible++;
15656
+ } else {
15657
+ item.setAttribute('data-hidden', 'true');
15658
+ }
15659
+ });
15660
+
15661
+ // Show/hide parent sections if all children are hidden
15662
+ var sections = document.querySelectorAll('.health-section, .pr-list-section');
15663
+ sections.forEach(function(section) {
15664
+ var items = section.querySelectorAll('.health-item[data-status], .pr-item[data-status]');
15665
+ if (items.length === 0) return; // sections without filterable items (e.g. empty state)
15666
+ var anyVisible = false;
15667
+ items.forEach(function(item) {
15668
+ if (item.getAttribute('data-hidden') !== 'true') anyVisible = true;
15669
+ });
15670
+ section.style.display = anyVisible ? '' : 'none';
15671
+ });
15672
+
15673
+ var isFiltering = (status !== 'all' || repo !== 'all' || query.length > 0);
15674
+ filterCount.textContent = isFiltering ? (visible + ' of ' + total + ' items') : '';
15675
+ }
15676
+
15677
+ searchInput.addEventListener('input', applyFilters);
15678
+ statusFilter.addEventListener('change', applyFilters);
15679
+ repoFilter.addEventListener('change', applyFilters);
15680
+ })();
15681
+ `;
15682
+ }
15683
+ });
15684
+
15685
+ // src/commands/dashboard-templates.ts
15686
+ function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state, issueResponses = []) {
15687
+ const approachingDormantDays = state.config?.approachingDormantDays ?? 25;
15688
+ const shelvedPRs = digest.shelvedPRs || [];
15689
+ const autoUnshelvedPRs = digest.autoUnshelvedPRs || [];
15690
+ const recentlyMerged = digest.recentlyMergedPRs || [];
15691
+ const shelvedUrls = new Set(shelvedPRs.map((pr) => pr.url));
15692
+ const activePRList = (digest.openPRs || []).filter((pr) => !shelvedUrls.has(pr.url));
15693
+ const actionRequired = [
15694
+ ...digest.prsNeedingResponse || [],
15695
+ ...digest.needsChangesPRs || [],
15696
+ ...digest.ciFailingPRs || [],
15697
+ ...digest.mergeConflictPRs || [],
15698
+ ...digest.incompleteChecklistPRs || [],
15699
+ ...digest.missingRequiredFilesPRs || [],
15700
+ ...digest.needsRebasePRs || []
15701
+ ];
15702
+ const waitingOnOthers = [
15703
+ ...digest.changesAddressedPRs || [],
15704
+ ...digest.waitingOnMaintainerPRs || [],
15705
+ ...digest.ciBlockedPRs || [],
15706
+ ...digest.ciNotRunningPRs || []
15707
+ ];
15708
+ return `<!DOCTYPE html>
15709
+ <html lang="en">
15710
+ <head>
15711
+ <meta charset="UTF-8">
15712
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
15713
+ <title>OSS Autopilot - Mission Control</title>
15714
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
15715
+ <link rel="preconnect" href="https://fonts.googleapis.com">
15716
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
15717
+ <link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
15718
+ <style>${DASHBOARD_CSS}
15719
+ </style>
15720
+ </head>
15721
+ <body>
15722
+ <div class="container">
15723
+ <header class="header">
15724
+ <div class="header-left">
15725
+ <div class="logo">
15726
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
15727
+ <circle cx="12" cy="12" r="10"/>
15728
+ <path d="M12 6v6l4 2"/>
15729
+ </svg>
15730
+ </div>
15731
+ <div>
15732
+ <h1>OSS Autopilot</h1>
15733
+ <span class="header-subtitle">Mission Control</span>
15734
+ </div>
15735
+ </div>
15736
+ <div class="header-controls">
15737
+ <div class="timestamp">
15738
+ Last updated: ${digest.generatedAt ? new Date(digest.generatedAt).toLocaleString("en-US", {
15739
+ weekday: "short",
15740
+ month: "short",
15741
+ day: "numeric",
15742
+ year: "numeric",
15743
+ hour: "2-digit",
15744
+ minute: "2-digit",
15745
+ second: "2-digit",
15746
+ hour12: false
15747
+ }) : "Unknown"}
15748
+ </div>
15749
+ <button class="theme-toggle" id="themeToggle" title="Toggle light/dark mode">
15750
+ <svg id="themeIconSun" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
15751
+ <circle cx="12" cy="12" r="5"/>
15752
+ <line x1="12" y1="1" x2="12" y2="3"/>
15753
+ <line x1="12" y1="21" x2="12" y2="23"/>
15754
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
15755
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
15756
+ <line x1="1" y1="12" x2="3" y2="12"/>
15757
+ <line x1="21" y1="12" x2="23" y2="12"/>
15758
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
15759
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
15760
+ </svg>
15761
+ <svg id="themeIconMoon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;">
15762
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
15763
+ </svg>
15764
+ <span id="themeLabel">Light</span>
15765
+ </button>
15766
+ </div>
15767
+ </header>
15768
+
15769
+ <div class="stats-grid">
15770
+ <div class="stat-card active">
15771
+ <div class="stat-value">${stats.activePRs}</div>
15772
+ <div class="stat-label">Active PRs</div>
15773
+ </div>
15774
+ <div class="stat-card shelved">
15775
+ <div class="stat-value">${stats.shelvedPRs}</div>
15776
+ <div class="stat-label">Shelved</div>
15777
+ </div>
15778
+ <div class="stat-card merged">
15779
+ <div class="stat-value">${stats.mergedPRs}</div>
15780
+ <div class="stat-label">Merged</div>
15781
+ </div>
15782
+ <div class="stat-card closed">
15783
+ <div class="stat-value">${stats.closedPRs}</div>
15784
+ <div class="stat-label">Closed</div>
15785
+ </div>
15786
+ <div class="stat-card rate">
15787
+ <div class="stat-value">${stats.mergeRate}</div>
15788
+ <div class="stat-label">Merge Rate</div>
15789
+ </div>
15790
+ </div>
15791
+
15792
+ <div class="filter-toolbar" id="filterToolbar">
15793
+ <label>Filters</label>
15794
+ <input type="text" class="filter-search" id="searchInput" placeholder="Search by PR title..." />
15795
+ <select class="filter-select" id="statusFilter">
15796
+ <option value="all">All Statuses</option>
15797
+ <option value="needs-response">Needs Response</option>
15798
+ <option value="needs-changes">Needs Changes</option>
15799
+ <option value="ci-failing">CI Failing</option>
15800
+ <option value="conflict">Merge Conflict</option>
15801
+ <option value="changes-addressed">Changes Addressed</option>
15802
+ <option value="waiting-maintainer">Waiting on Maintainer</option>
15803
+ <option value="ci-blocked">CI Blocked</option>
15804
+ <option value="ci-not-running">CI Not Running</option>
15805
+ <option value="incomplete-checklist">Incomplete Checklist</option>
15806
+ <option value="missing-files">Missing Files</option>
15807
+ <option value="needs-rebase">Needs Rebase</option>
15808
+ <option value="shelved">Shelved</option>
15809
+ <option value="merged">Recently Merged</option>
15810
+ <option value="closed">Recently Closed</option>
15811
+ <option value="auto-unshelved">Auto-Unshelved</option>
15812
+ <option value="active">Active (No Issues)</option>
15813
+ </select>
15814
+ <select class="filter-select" id="repoFilter">
15815
+ <option value="all">All Repositories</option>
15816
+ ${(() => {
15817
+ const repos = /* @__PURE__ */ new Set();
15818
+ for (const pr of activePRList) repos.add(pr.repo);
15819
+ for (const pr of shelvedPRs) repos.add(pr.repo);
15820
+ for (const pr of actionRequired) repos.add(pr.repo);
15821
+ for (const pr of waitingOnOthers) repos.add(pr.repo);
15822
+ for (const pr of recentlyMerged) repos.add(pr.repo);
15823
+ for (const pr of digest.recentlyClosedPRs || []) repos.add(pr.repo);
15824
+ for (const pr of autoUnshelvedPRs) repos.add(pr.repo);
15825
+ return Array.from(repos).sort().map((repo) => `<option value="${escapeHtml(repo)}">${escapeHtml(repo)}</option>`).join("\n ");
15826
+ })()}
15827
+ </select>
15828
+ <span class="filter-count" id="filterCount"></span>
15829
+ </div>
15830
+
15831
+ ${actionRequired.length > 0 ? `
15832
+ <section class="health-section">
15833
+ <div class="health-header">
15834
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-warning)" stroke-width="2">
15835
+ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
15836
+ <line x1="12" y1="9" x2="12" y2="13"/>
15837
+ <line x1="12" y1="17" x2="12.01" y2="17"/>
15838
+ </svg>
15839
+ <h2>Action Required</h2>
15840
+ <span class="health-badge">${actionRequired.length} issue${actionRequired.length !== 1 ? "s" : ""}</span>
15841
+ </div>
15842
+ <div class="health-items">
15843
+ ${renderHealthItems(
15844
+ digest.prsNeedingResponse || [],
15845
+ "needs-response",
15846
+ SVG_ICONS.comment,
15847
+ "Needs Response",
15848
+ (pr) => pr.lastMaintainerComment ? `@${escapeHtml(pr.lastMaintainerComment.author)}: ${truncateTitle(pr.lastMaintainerComment.body, 40)}` : truncateTitle(pr.title)
15849
+ )}
15850
+ ${renderHealthItems(digest.needsChangesPRs || [], "needs-changes", SVG_ICONS.edit, "Needs Changes", titleMeta)}
15851
+ ${renderHealthItems(digest.ciFailingPRs || [], "ci-failing", SVG_ICONS.xCircle, "CI Failing", titleMeta)}
15852
+ ${renderHealthItems(digest.mergeConflictPRs || [], "conflict", SVG_ICONS.conflict, "Merge Conflict", titleMeta)}
15853
+ ${renderHealthItems(
15854
+ digest.incompleteChecklistPRs || [],
15855
+ "incomplete-checklist",
15856
+ SVG_ICONS.checklist,
15857
+ (pr) => `Incomplete Checklist${pr.checklistStats ? ` (${pr.checklistStats.checked}/${pr.checklistStats.total})` : ""}`,
15858
+ titleMeta
15859
+ )}
15860
+ ${renderHealthItems(
15861
+ digest.missingRequiredFilesPRs || [],
15862
+ "missing-files",
15863
+ SVG_ICONS.file,
15864
+ "Missing Required Files",
15865
+ (pr) => pr.missingRequiredFiles ? escapeHtml(pr.missingRequiredFiles.join(", ")) : truncateTitle(pr.title)
15866
+ )}
15867
+ ${renderHealthItems(
15868
+ digest.needsRebasePRs || [],
15869
+ "needs-rebase",
15870
+ SVG_ICONS.refresh,
15871
+ (pr) => `Needs Rebase${pr.commitsBehindUpstream ? ` (${pr.commitsBehindUpstream} behind)` : ""}`,
15872
+ titleMeta
15873
+ )}
15874
+ </div>
15875
+ </section>
15876
+ ` : ""}
15877
+
15878
+ ${waitingOnOthers.length > 0 ? `
15879
+ <section class="health-section waiting-section">
15568
15880
  <div class="health-header">
15569
15881
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-info)" stroke-width="2">
15570
15882
  <circle cx="12" cy="12" r="10"/>
@@ -15577,13 +15889,13 @@ function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpene
15577
15889
  ${renderHealthItems(
15578
15890
  digest.changesAddressedPRs || [],
15579
15891
  "changes-addressed",
15580
- SVG.checkCircle,
15892
+ SVG_ICONS.checkCircle,
15581
15893
  "Changes Addressed",
15582
15894
  (pr) => `Awaiting re-review${pr.lastMaintainerComment ? ` from @${escapeHtml(pr.lastMaintainerComment.author)}` : ""}`
15583
15895
  )}
15584
- ${renderHealthItems(digest.waitingOnMaintainerPRs || [], "waiting-maintainer", SVG.clock, "Waiting on Maintainer", titleMeta)}
15585
- ${renderHealthItems(digest.ciBlockedPRs || [], "ci-blocked", SVG.lock, "CI Blocked", titleMeta)}
15586
- ${renderHealthItems(digest.ciNotRunningPRs || [], "ci-not-running", SVG.infoCircle, "CI Not Running", titleMeta)}
15896
+ ${renderHealthItems(digest.waitingOnMaintainerPRs || [], "waiting-maintainer", SVG_ICONS.clock, "Waiting on Maintainer", titleMeta)}
15897
+ ${renderHealthItems(digest.ciBlockedPRs || [], "ci-blocked", SVG_ICONS.lock, "CI Blocked", titleMeta)}
15898
+ ${renderHealthItems(digest.ciNotRunningPRs || [], "ci-not-running", SVG_ICONS.infoCircle, "CI Not Running", titleMeta)}
15587
15899
  </div>
15588
15900
  </section>
15589
15901
  ` : ""}
@@ -15607,7 +15919,7 @@ function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpene
15607
15919
  <section class="health-section" style="animation-delay: 0.15s;">
15608
15920
  <div class="health-header">
15609
15921
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-merged)" stroke-width="2">
15610
- ${SVG.gitMerge}
15922
+ ${SVG_ICONS.gitMerge}
15611
15923
  </svg>
15612
15924
  <h2>Recently Merged</h2>
15613
15925
  <span class="health-badge" style="background: var(--accent-merged-dim); color: var(--accent-merged);">${recentlyMerged.length} merged</span>
@@ -15618,7 +15930,7 @@ function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpene
15618
15930
  <div class="health-item" style="border-left-color: var(--accent-merged);" data-status="merged" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
15619
15931
  <div class="health-icon" style="background: var(--accent-merged-dim); color: var(--accent-merged);">
15620
15932
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
15621
- ${SVG.gitMerge}
15933
+ ${SVG_ICONS.gitMerge}
15622
15934
  </svg>
15623
15935
  </div>
15624
15936
  <div class="health-content">
@@ -15669,7 +15981,7 @@ function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpene
15669
15981
  <section class="health-section" style="animation-delay: 0.25s;">
15670
15982
  <div class="health-header">
15671
15983
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-info)" stroke-width="2">
15672
- ${SVG.bell}
15984
+ ${SVG_ICONS.bell}
15673
15985
  </svg>
15674
15986
  <h2>Auto-Unshelved</h2>
15675
15987
  <span class="health-badge" style="background: var(--accent-info-dim); color: var(--accent-info);">${autoUnshelvedPRs.length} unshelved</span>
@@ -15678,7 +15990,7 @@ function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpene
15678
15990
  ${renderHealthItems(
15679
15991
  autoUnshelvedPRs,
15680
15992
  "auto-unshelved",
15681
- SVG.bell,
15993
+ SVG_ICONS.bell,
15682
15994
  (pr) => "Auto-Unshelved (" + pr.status.replace(/_/g, " ") + ")",
15683
15995
  titleMeta
15684
15996
  )}
@@ -15690,7 +16002,7 @@ function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpene
15690
16002
  <section class="health-section" style="animation-delay: 0.3s;">
15691
16003
  <div class="health-header">
15692
16004
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-info)" stroke-width="2">
15693
- ${SVG.comment}
16005
+ ${SVG_ICONS.comment}
15694
16006
  </svg>
15695
16007
  <h2>Issue Conversations</h2>
15696
16008
  <span class="health-badge" style="background: var(--accent-info-dim); color: var(--accent-info);">${issueResponses.length} repl${issueResponses.length !== 1 ? "ies" : "y"}</span>
@@ -15701,7 +16013,7 @@ function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpene
15701
16013
  <div class="health-item changes-addressed">
15702
16014
  <div class="health-icon" style="background: var(--accent-info-dim); color: var(--accent-info);">
15703
16015
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
15704
- ${SVG.comment}
16016
+ ${SVG_ICONS.comment}
15705
16017
  </svg>
15706
16018
  </div>
15707
16019
  <div class="health-content">
@@ -15737,394 +16049,131 @@ function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpene
15737
16049
  </div>
15738
16050
  </div>
15739
16051
  </div>
15740
- </div>
15741
-
15742
- <div class="card" style="margin-bottom: 1.25rem;">
15743
- <div class="card-header">
15744
- <span class="card-title">Contribution Timeline</span>
15745
- </div>
15746
- <div class="card-body">
15747
- <div class="chart-container" style="height: 250px;">
15748
- <canvas id="monthlyChart"></canvas>
15749
- </div>
15750
- </div>
15751
- </div>
15752
-
15753
-
15754
- ${activePRList.length > 0 ? `
15755
- <section class="pr-list-section">
15756
- <div class="pr-list-header">
15757
- <h2 class="pr-list-title">Active Pull Requests</h2>
15758
- <span class="pr-count">${activePRList.length} open</span>
15759
- </div>
15760
- <div class="pr-list">
15761
- ${activePRList.map((pr) => {
15762
- const hasIssues = pr.ciStatus === "failing" || pr.hasMergeConflict || pr.hasUnrespondedComment && pr.status !== "changes_addressed" || pr.status === "needs_changes";
15763
- const isStale = pr.daysSinceActivity >= approachingDormantDays;
15764
- const itemClass = hasIssues ? "has-issues" : isStale ? "stale" : "";
15765
- const prStatus = pr.ciStatus === "failing" ? "ci-failing" : pr.hasMergeConflict ? "conflict" : pr.hasUnrespondedComment && pr.status !== "changes_addressed" && pr.status !== "failing_ci" ? "needs-response" : pr.status === "needs_changes" ? "needs-changes" : pr.status === "changes_addressed" ? "changes-addressed" : "active";
15766
- return `
15767
- <div class="pr-item ${itemClass}" data-status="${prStatus}" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
15768
- <div class="pr-status-indicator">
15769
- ${hasIssues ? `
15770
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
15771
- <circle cx="12" cy="12" r="10"/>
15772
- <line x1="12" y1="8" x2="12" y2="12"/>
15773
- <line x1="12" y1="16" x2="12.01" y2="16"/>
15774
- </svg>
15775
- ` : `
15776
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
15777
- <circle cx="12" cy="12" r="10"/>
15778
- <line x1="12" y1="16" x2="12" y2="12"/>
15779
- <line x1="12" y1="8" x2="12.01" y2="8"/>
15780
- </svg>
15781
- `}
15782
- </div>
15783
- <div class="pr-content">
15784
- <div class="pr-title-row">
15785
- <a href="${escapeHtml(pr.url)}" target="_blank" class="pr-title">${escapeHtml(pr.title)}</a>
15786
- <span class="pr-repo">${escapeHtml(pr.repo)}#${pr.number}</span>
15787
- </div>
15788
- <div class="pr-badges">
15789
- ${pr.ciStatus === "failing" ? '<span class="badge badge-ci-failing">CI Failing</span>' : ""}
15790
- ${pr.ciStatus === "passing" ? '<span class="badge badge-passing">CI Passing</span>' : ""}
15791
- ${pr.ciStatus === "pending" ? '<span class="badge badge-pending">CI Pending</span>' : ""}
15792
- ${pr.hasMergeConflict ? '<span class="badge badge-conflict">Merge Conflict</span>' : ""}
15793
- ${pr.hasUnrespondedComment && pr.status === "changes_addressed" ? '<span class="badge badge-changes-addressed">Changes Addressed</span>' : ""}
15794
- ${pr.hasUnrespondedComment && pr.status !== "changes_addressed" && pr.status !== "failing_ci" ? '<span class="badge badge-needs-response">Needs Response</span>' : ""}
15795
- ${pr.reviewDecision === "changes_requested" ? '<span class="badge badge-changes-requested">Changes Requested</span>' : ""}
15796
- ${isStale ? `<span class="badge badge-stale">${pr.daysSinceActivity}d inactive</span>` : ""}
15797
- </div>
15798
- </div>
15799
- <div class="pr-activity">
15800
- ${pr.daysSinceActivity === 0 ? "Today" : pr.daysSinceActivity === 1 ? "Yesterday" : pr.daysSinceActivity + "d ago"}
15801
- </div>
15802
- </div>`;
15803
- }).join("")}
15804
- </div>
15805
- </section>
15806
- ` : `
15807
- <section class="pr-list-section">
15808
- <div class="pr-list-header">
15809
- <h2 class="pr-list-title">Active Pull Requests</h2>
15810
- <span class="pr-count">0 open</span>
15811
- </div>
15812
- <div class="empty-state">
15813
- <div class="empty-state-icon">
15814
- <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
15815
- <circle cx="12" cy="12" r="10"/>
15816
- <path d="M8 12h8"/>
15817
- </svg>
15818
- </div>
15819
- <p>No active pull requests</p>
15820
- </div>
15821
- </section>
15822
- `}
15823
-
15824
- ${shelvedPRs.length > 0 ? `
15825
- <section class="pr-list-section" style="margin-top: 1.25rem; opacity: 0.7;">
15826
- <div class="pr-list-header">
15827
- <h2 class="pr-list-title">Shelved Pull Requests</h2>
15828
- <span class="pr-count" style="background: rgba(110, 118, 129, 0.15); color: var(--text-muted);">${shelvedPRs.length} shelved</span>
15829
- </div>
15830
- <div class="pr-list">
15831
- ${shelvedPRs.map(
15832
- (pr) => `
15833
- <div class="pr-item" data-status="shelved" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
15834
- <div class="pr-status-indicator" style="background: rgba(110, 118, 129, 0.1); color: var(--text-muted);">
15835
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
15836
- ${SVG.box}
15837
- </svg>
15838
- </div>
15839
- <div class="pr-content">
15840
- <div class="pr-title-row">
15841
- <a href="${escapeHtml(pr.url)}" target="_blank" class="pr-title">${escapeHtml(pr.title)}</a>
15842
- <span class="pr-repo">${escapeHtml(pr.repo)}#${pr.number}</span>
15843
- </div>
15844
- <div class="pr-badges">
15845
- <span class="badge badge-days">${pr.daysSinceActivity}d inactive</span>
15846
- </div>
15847
- </div>
15848
- <div class="pr-activity">
15849
- ${pr.daysSinceActivity === 0 ? "Today" : pr.daysSinceActivity === 1 ? "Yesterday" : pr.daysSinceActivity + "d ago"}
15850
- </div>
15851
- </div>`
15852
- ).join("")}
15853
- </div>
15854
- </section>
15855
- ` : ""}
15856
-
15857
- <footer class="footer">
15858
- <p>OSS Autopilot // Mission Control</p>
15859
- <p style="margin-top: 0.25rem;">Dashboard generated: ${digest.generatedAt ? new Date(digest.generatedAt).toISOString() : "Unknown"}</p>
15860
- </footer>
15861
- </div>
15862
-
15863
- <script>
15864
- // === Theme Toggle ===
15865
- (function() {
15866
- var html = document.documentElement;
15867
- var toggle = document.getElementById('themeToggle');
15868
- var sunIcon = document.getElementById('themeIconSun');
15869
- var moonIcon = document.getElementById('themeIconMoon');
15870
- var label = document.getElementById('themeLabel');
15871
-
15872
- function getEffectiveTheme() {
15873
- try {
15874
- var stored = localStorage.getItem('oss-dashboard-theme');
15875
- if (stored === 'light' || stored === 'dark') return stored;
15876
- } catch (e) { /* localStorage unavailable (private browsing) */ }
15877
- return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
15878
- }
15879
-
15880
- function applyTheme(theme) {
15881
- html.setAttribute('data-theme', theme);
15882
- if (theme === 'light') {
15883
- sunIcon.style.display = 'none';
15884
- moonIcon.style.display = 'block';
15885
- label.textContent = 'Dark';
15886
- } else {
15887
- sunIcon.style.display = 'block';
15888
- moonIcon.style.display = 'none';
15889
- label.textContent = 'Light';
15890
- }
15891
- }
15892
-
15893
- applyTheme(getEffectiveTheme());
15894
-
15895
- toggle.addEventListener('click', function() {
15896
- var current = html.getAttribute('data-theme');
15897
- var next = current === 'dark' ? 'light' : 'dark';
15898
- try { localStorage.setItem('oss-dashboard-theme', next); } catch (e) { /* private browsing */ }
15899
- applyTheme(next);
15900
- });
15901
- })();
15902
-
15903
- // === Filtering & Search ===
15904
- (function() {
15905
- var searchInput = document.getElementById('searchInput');
15906
- var statusFilter = document.getElementById('statusFilter');
15907
- var repoFilter = document.getElementById('repoFilter');
15908
- var filterCount = document.getElementById('filterCount');
15909
-
15910
- function applyFilters() {
15911
- var query = searchInput.value.toLowerCase().trim();
15912
- var status = statusFilter.value;
15913
- var repo = repoFilter.value;
15914
- var allItems = document.querySelectorAll('.health-item[data-status], .pr-item[data-status]');
15915
- var visible = 0;
15916
- var total = allItems.length;
15917
-
15918
- allItems.forEach(function(item) {
15919
- var itemStatus = item.getAttribute('data-status') || '';
15920
- var itemRepo = item.getAttribute('data-repo') || '';
15921
- var itemTitle = item.getAttribute('data-title') || '';
15922
-
15923
- var matchesStatus = (status === 'all') || (itemStatus === status);
15924
- var matchesRepo = (repo === 'all') || (itemRepo === repo);
15925
- var matchesSearch = !query || itemTitle.indexOf(query) !== -1;
15926
-
15927
- if (matchesStatus && matchesRepo && matchesSearch) {
15928
- item.setAttribute('data-hidden', 'false');
15929
- visible++;
15930
- } else {
15931
- item.setAttribute('data-hidden', 'true');
15932
- }
15933
- });
15934
-
15935
- // Show/hide parent sections if all children are hidden
15936
- var sections = document.querySelectorAll('.health-section, .pr-list-section');
15937
- sections.forEach(function(section) {
15938
- var items = section.querySelectorAll('.health-item[data-status], .pr-item[data-status]');
15939
- if (items.length === 0) return; // sections without filterable items (e.g. empty state)
15940
- var anyVisible = false;
15941
- items.forEach(function(item) {
15942
- if (item.getAttribute('data-hidden') !== 'true') anyVisible = true;
15943
- });
15944
- section.style.display = anyVisible ? '' : 'none';
15945
- });
15946
-
15947
- var isFiltering = (status !== 'all' || repo !== 'all' || query.length > 0);
15948
- filterCount.textContent = isFiltering ? (visible + ' of ' + total + ' items') : '';
15949
- }
15950
-
15951
- searchInput.addEventListener('input', applyFilters);
15952
- statusFilter.addEventListener('change', applyFilters);
15953
- repoFilter.addEventListener('change', applyFilters);
15954
- })();
15955
-
15956
- // === Chart.js Configuration ===
15957
- Chart.defaults.color = '#6e7681';
15958
- Chart.defaults.borderColor = 'rgba(48, 54, 61, 0.4)';
15959
- Chart.defaults.font.family = "'Geist', sans-serif";
15960
- Chart.defaults.font.size = 11;
15961
-
15962
- // === Status Doughnut ===
15963
- new Chart(document.getElementById('statusChart'), {
15964
- type: 'doughnut',
15965
- data: {
15966
- labels: ['Active', 'Shelved', 'Merged', 'Closed'],
15967
- datasets: [{
15968
- data: [${stats.activePRs}, ${stats.shelvedPRs}, ${stats.mergedPRs}, ${stats.closedPRs}],
15969
- backgroundColor: ['#3fb950', '#6e7681', '#a855f7', '#484f58'],
15970
- borderColor: 'rgba(8, 11, 16, 0.8)',
15971
- borderWidth: 2,
15972
- hoverOffset: 8
15973
- }]
15974
- },
15975
- options: {
15976
- responsive: true,
15977
- maintainAspectRatio: false,
15978
- cutout: '65%',
15979
- plugins: {
15980
- legend: {
15981
- position: 'bottom',
15982
- labels: { padding: 16, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } }
15983
- }
15984
- }
15985
- }
15986
- });
16052
+ </div>
15987
16053
 
15988
- // === Repository Breakdown (with "Other" bucket + percentage tooltips) ===
15989
- ${(() => {
15990
- const { excludeRepos: exRepos = [], excludeOrgs: exOrgs, minStars } = state.config;
15991
- const starThreshold = minStars ?? 50;
15992
- const shouldExcludeRepo = (repo) => {
15993
- const repoLower = repo.toLowerCase();
15994
- if (exRepos.some((r) => r.toLowerCase() === repoLower)) return true;
15995
- if (exOrgs?.some((o) => o.toLowerCase() === repoLower.split("/")[0])) return true;
15996
- const score = (state.repoScores || {})[repo];
15997
- if (score?.stargazersCount !== void 0 && score.stargazersCount < starThreshold) return true;
15998
- return false;
15999
- };
16000
- const allRepoEntries = Object.entries(
16001
- // Rebuild from full prsByRepo to get all repos, not just top 10
16002
- (() => {
16003
- const all = {};
16004
- for (const pr of digest.openPRs || []) {
16005
- if (shouldExcludeRepo(pr.repo)) continue;
16006
- if (!all[pr.repo]) all[pr.repo] = { active: 0, merged: 0, closed: 0 };
16007
- all[pr.repo].active++;
16008
- }
16009
- for (const [repo, score] of Object.entries(state.repoScores || {})) {
16010
- if (shouldExcludeRepo(repo)) continue;
16011
- if (!all[repo]) all[repo] = { active: 0, merged: 0, closed: 0 };
16012
- all[repo].merged = score.mergedPRCount;
16013
- all[repo].closed = score.closedWithoutMergeCount;
16014
- }
16015
- return all;
16016
- })()
16017
- ).sort((a, b) => {
16018
- const totalA = a[1].merged + a[1].active + a[1].closed;
16019
- const totalB = b[1].merged + b[1].active + b[1].closed;
16020
- return totalB - totalA;
16021
- });
16022
- const displayRepos = allRepoEntries.slice(0, 10);
16023
- const otherRepos = allRepoEntries.slice(10);
16024
- const grandTotal = allRepoEntries.reduce((sum, [, d]) => sum + d.merged + d.active + d.closed, 0);
16025
- if (otherRepos.length > 0) {
16026
- const otherData = otherRepos.reduce(
16027
- (acc, [, d]) => ({
16028
- active: acc.active + d.active,
16029
- merged: acc.merged + d.merged,
16030
- closed: acc.closed + d.closed
16031
- }),
16032
- { active: 0, merged: 0, closed: 0 }
16033
- );
16034
- displayRepos.push(["Other", otherData]);
16035
- }
16036
- const repoLabels = displayRepos.map(([repo]) => repo === "Other" ? "Other" : repo.split("/")[1] || repo);
16037
- const mergedData = displayRepos.map(([, d]) => d.merged);
16038
- const activeData = displayRepos.map(([, d]) => d.active);
16039
- const closedData = displayRepos.map(([, d]) => d.closed);
16040
- return `
16041
- new Chart(document.getElementById('reposChart'), {
16042
- type: 'bar',
16043
- data: {
16044
- labels: ${JSON.stringify(repoLabels)},
16045
- datasets: [
16046
- { label: 'Merged', data: ${JSON.stringify(mergedData)}, backgroundColor: '#a855f7', borderRadius: 3 },
16047
- { label: 'Active', data: ${JSON.stringify(activeData)}, backgroundColor: '#3fb950', borderRadius: 3 },
16048
- { label: 'Closed', data: ${JSON.stringify(closedData)}, backgroundColor: '#484f58', borderRadius: 3 }
16049
- ]
16050
- },
16051
- options: {
16052
- responsive: true,
16053
- maintainAspectRatio: false,
16054
- scales: {
16055
- x: { stacked: true, grid: { display: false }, ticks: { font: { size: 10 } } },
16056
- y: { stacked: true, grid: { color: 'rgba(48, 54, 61, 0.3)' }, ticks: { stepSize: 1 } }
16057
- },
16058
- plugins: {
16059
- legend: { position: 'bottom', labels: { padding: 16, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } } },
16060
- tooltip: {
16061
- callbacks: {
16062
- afterBody: function(context) {
16063
- const idx = context[0].dataIndex;
16064
- const total = ${JSON.stringify(mergedData)}[idx] + ${JSON.stringify(activeData)}[idx] + ${JSON.stringify(closedData)}[idx];
16065
- const pct = ${grandTotal} > 0 ? ((total / ${grandTotal}) * 100).toFixed(1) : '0.0';
16066
- return pct + '% of all PRs';
16067
- }
16068
- }
16069
- }
16070
- }
16071
- }
16072
- });`;
16073
- })()}
16054
+ <div class="card" style="margin-bottom: 1.25rem;">
16055
+ <div class="card-header">
16056
+ <span class="card-title">Contribution Timeline</span>
16057
+ </div>
16058
+ <div class="card-body">
16059
+ <div class="chart-container" style="height: 250px;">
16060
+ <canvas id="monthlyChart"></canvas>
16061
+ </div>
16062
+ </div>
16063
+ </div>
16074
16064
 
16075
- // === Contribution Timeline (grouped bar: Opened/Merged/Closed) ===
16076
- ${(() => {
16077
- const now = /* @__PURE__ */ new Date();
16078
- const allMonths = [];
16079
- for (let offset = 5; offset >= 0; offset--) {
16080
- const d = new Date(now.getFullYear(), now.getMonth() - offset, 1);
16081
- allMonths.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`);
16082
- }
16065
+
16066
+ ${activePRList.length > 0 ? `
16067
+ <section class="pr-list-section">
16068
+ <div class="pr-list-header">
16069
+ <h2 class="pr-list-title">Active Pull Requests</h2>
16070
+ <span class="pr-count">${activePRList.length} open</span>
16071
+ </div>
16072
+ <div class="pr-list">
16073
+ ${activePRList.map((pr) => {
16074
+ const hasIssues = pr.ciStatus === "failing" || pr.hasMergeConflict || pr.hasUnrespondedComment && pr.status !== "changes_addressed" || pr.status === "needs_changes";
16075
+ const isStale = pr.daysSinceActivity >= approachingDormantDays;
16076
+ const itemClass = hasIssues ? "has-issues" : isStale ? "stale" : "";
16077
+ const prStatus = pr.ciStatus === "failing" ? "ci-failing" : pr.hasMergeConflict ? "conflict" : pr.hasUnrespondedComment && pr.status !== "changes_addressed" && pr.status !== "failing_ci" ? "needs-response" : pr.status === "needs_changes" ? "needs-changes" : pr.status === "changes_addressed" ? "changes-addressed" : "active";
16083
16078
  return `
16084
- const timelineMonths = ${JSON.stringify(allMonths)};
16085
- const openedData = ${JSON.stringify(monthlyOpened)};
16086
- const mergedData = ${JSON.stringify(monthlyMerged)};
16087
- const closedData = ${JSON.stringify(monthlyClosed)};
16088
- new Chart(document.getElementById('monthlyChart'), {
16089
- type: 'bar',
16090
- data: {
16091
- labels: timelineMonths,
16092
- datasets: [
16093
- {
16094
- label: 'Opened',
16095
- data: timelineMonths.map(m => openedData[m] || 0),
16096
- backgroundColor: '#58a6ff',
16097
- borderRadius: 3
16098
- },
16099
- {
16100
- label: 'Merged',
16101
- data: timelineMonths.map(m => mergedData[m] || 0),
16102
- backgroundColor: '#a855f7',
16103
- borderRadius: 3
16104
- },
16105
- {
16106
- label: 'Closed',
16107
- data: timelineMonths.map(m => closedData[m] || 0),
16108
- backgroundColor: '#484f58',
16109
- borderRadius: 3
16110
- }
16111
- ]
16112
- },
16113
- options: {
16114
- responsive: true,
16115
- maintainAspectRatio: false,
16116
- scales: {
16117
- x: { grid: { display: false } },
16118
- y: { grid: { color: 'rgba(48, 54, 61, 0.3)' }, beginAtZero: true, ticks: { stepSize: 1 } }
16119
- },
16120
- plugins: {
16121
- legend: { position: 'bottom', labels: { padding: 16, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } } }
16122
- },
16123
- interaction: { intersect: false, mode: 'index' }
16124
- }
16125
- });`;
16126
- })()}
16079
+ <div class="pr-item ${itemClass}" data-status="${prStatus}" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
16080
+ <div class="pr-status-indicator">
16081
+ ${hasIssues ? `
16082
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
16083
+ <circle cx="12" cy="12" r="10"/>
16084
+ <line x1="12" y1="8" x2="12" y2="12"/>
16085
+ <line x1="12" y1="16" x2="12.01" y2="16"/>
16086
+ </svg>
16087
+ ` : `
16088
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
16089
+ <circle cx="12" cy="12" r="10"/>
16090
+ <line x1="12" y1="16" x2="12" y2="12"/>
16091
+ <line x1="12" y1="8" x2="12.01" y2="8"/>
16092
+ </svg>
16093
+ `}
16094
+ </div>
16095
+ <div class="pr-content">
16096
+ <div class="pr-title-row">
16097
+ <a href="${escapeHtml(pr.url)}" target="_blank" class="pr-title">${escapeHtml(pr.title)}</a>
16098
+ <span class="pr-repo">${escapeHtml(pr.repo)}#${pr.number}</span>
16099
+ </div>
16100
+ <div class="pr-badges">
16101
+ ${pr.ciStatus === "failing" ? '<span class="badge badge-ci-failing">CI Failing</span>' : ""}
16102
+ ${pr.ciStatus === "passing" ? '<span class="badge badge-passing">CI Passing</span>' : ""}
16103
+ ${pr.ciStatus === "pending" ? '<span class="badge badge-pending">CI Pending</span>' : ""}
16104
+ ${pr.hasMergeConflict ? '<span class="badge badge-conflict">Merge Conflict</span>' : ""}
16105
+ ${pr.hasUnrespondedComment && pr.status === "changes_addressed" ? '<span class="badge badge-changes-addressed">Changes Addressed</span>' : ""}
16106
+ ${pr.hasUnrespondedComment && pr.status !== "changes_addressed" && pr.status !== "failing_ci" ? '<span class="badge badge-needs-response">Needs Response</span>' : ""}
16107
+ ${pr.reviewDecision === "changes_requested" ? '<span class="badge badge-changes-requested">Changes Requested</span>' : ""}
16108
+ ${isStale ? `<span class="badge badge-stale">${pr.daysSinceActivity}d inactive</span>` : ""}
16109
+ </div>
16110
+ </div>
16111
+ <div class="pr-activity">
16112
+ ${pr.daysSinceActivity === 0 ? "Today" : pr.daysSinceActivity === 1 ? "Yesterday" : pr.daysSinceActivity + "d ago"}
16113
+ </div>
16114
+ </div>`;
16115
+ }).join("")}
16116
+ </div>
16117
+ </section>
16118
+ ` : `
16119
+ <section class="pr-list-section">
16120
+ <div class="pr-list-header">
16121
+ <h2 class="pr-list-title">Active Pull Requests</h2>
16122
+ <span class="pr-count">0 open</span>
16123
+ </div>
16124
+ <div class="empty-state">
16125
+ <div class="empty-state-icon">
16126
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
16127
+ <circle cx="12" cy="12" r="10"/>
16128
+ <path d="M8 12h8"/>
16129
+ </svg>
16130
+ </div>
16131
+ <p>No active pull requests</p>
16132
+ </div>
16133
+ </section>
16134
+ `}
16135
+
16136
+ ${shelvedPRs.length > 0 ? `
16137
+ <section class="pr-list-section" style="margin-top: 1.25rem; opacity: 0.7;">
16138
+ <div class="pr-list-header">
16139
+ <h2 class="pr-list-title">Shelved Pull Requests</h2>
16140
+ <span class="pr-count" style="background: rgba(110, 118, 129, 0.15); color: var(--text-muted);">${shelvedPRs.length} shelved</span>
16141
+ </div>
16142
+ <div class="pr-list">
16143
+ ${shelvedPRs.map(
16144
+ (pr) => `
16145
+ <div class="pr-item" data-status="shelved" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
16146
+ <div class="pr-status-indicator" style="background: rgba(110, 118, 129, 0.1); color: var(--text-muted);">
16147
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
16148
+ ${SVG_ICONS.box}
16149
+ </svg>
16150
+ </div>
16151
+ <div class="pr-content">
16152
+ <div class="pr-title-row">
16153
+ <a href="${escapeHtml(pr.url)}" target="_blank" class="pr-title">${escapeHtml(pr.title)}</a>
16154
+ <span class="pr-repo">${escapeHtml(pr.repo)}#${pr.number}</span>
16155
+ </div>
16156
+ <div class="pr-badges">
16157
+ <span class="badge badge-days">${pr.daysSinceActivity}d inactive</span>
16158
+ </div>
16159
+ </div>
16160
+ <div class="pr-activity">
16161
+ ${pr.daysSinceActivity === 0 ? "Today" : pr.daysSinceActivity === 1 ? "Yesterday" : pr.daysSinceActivity + "d ago"}
16162
+ </div>
16163
+ </div>`
16164
+ ).join("")}
16165
+ </div>
16166
+ </section>
16167
+ ` : ""}
16168
+
16169
+ <footer class="footer">
16170
+ <p>OSS Autopilot // Mission Control</p>
16171
+ <p style="margin-top: 0.25rem;">Dashboard generated: ${digest.generatedAt ? new Date(digest.generatedAt).toISOString() : "Unknown"}</p>
16172
+ </footer>
16173
+ </div>
16127
16174
 
16175
+ <script>
16176
+ ${generateDashboardScripts(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state)}
16128
16177
  </script>
16129
16178
  </body>
16130
16179
  </html>`;
@@ -16132,6 +16181,11 @@ function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpene
16132
16181
  var init_dashboard_templates = __esm({
16133
16182
  "src/commands/dashboard-templates.ts"() {
16134
16183
  "use strict";
16184
+ init_dashboard_formatters();
16185
+ init_dashboard_styles();
16186
+ init_dashboard_components();
16187
+ init_dashboard_scripts();
16188
+ init_dashboard_formatters();
16135
16189
  }
16136
16190
  });
16137
16191
 
@@ -16807,15 +16861,15 @@ async function runCheckIntegration(options) {
16807
16861
  }
16808
16862
  referencedBy = [...new Set(referencedBy)];
16809
16863
  const isIntegrated = referencedBy.length > 0;
16810
- const info = {
16864
+ const info2 = {
16811
16865
  path: newFile,
16812
16866
  referencedBy,
16813
16867
  isIntegrated
16814
16868
  };
16815
16869
  if (!isIntegrated) {
16816
- info.suggestedEntryPoints = suggestEntryPoints(newFile, allFiles);
16870
+ info2.suggestedEntryPoints = suggestEntryPoints(newFile, allFiles);
16817
16871
  }
16818
- results.push(info);
16872
+ results.push(info2);
16819
16873
  }
16820
16874
  const unreferencedCount = results.filter((r) => !r.isIntegrated).length;
16821
16875
  return { newFiles: results, unreferencedCount };
@@ -17145,29 +17199,28 @@ var init_shelve = __esm({
17145
17199
  // src/commands/dismiss.ts
17146
17200
  var dismiss_exports = {};
17147
17201
  __export(dismiss_exports, {
17148
- ISSUE_URL_PATTERN: () => ISSUE_URL_PATTERN,
17149
17202
  runDismiss: () => runDismiss,
17150
17203
  runUndismiss: () => runUndismiss
17151
17204
  });
17152
17205
  async function runDismiss(options) {
17153
- validateUrl(options.issueUrl);
17154
- validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, "issue");
17206
+ validateUrl(options.url);
17207
+ validateGitHubUrl(options.url, ISSUE_OR_PR_URL_PATTERN, "issue or PR");
17155
17208
  const stateManager2 = getStateManager();
17156
- const added = stateManager2.dismissIssue(options.issueUrl, (/* @__PURE__ */ new Date()).toISOString());
17209
+ const added = stateManager2.dismissIssue(options.url, (/* @__PURE__ */ new Date()).toISOString());
17157
17210
  if (added) {
17158
17211
  stateManager2.save();
17159
17212
  }
17160
- return { dismissed: added, url: options.issueUrl };
17213
+ return { dismissed: added, url: options.url };
17161
17214
  }
17162
17215
  async function runUndismiss(options) {
17163
- validateUrl(options.issueUrl);
17164
- validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, "issue");
17216
+ validateUrl(options.url);
17217
+ validateGitHubUrl(options.url, ISSUE_OR_PR_URL_PATTERN, "issue or PR");
17165
17218
  const stateManager2 = getStateManager();
17166
- const removed = stateManager2.undismissIssue(options.issueUrl);
17219
+ const removed = stateManager2.undismissIssue(options.url);
17167
17220
  if (removed) {
17168
17221
  stateManager2.save();
17169
17222
  }
17170
- return { undismissed: removed, url: options.issueUrl };
17223
+ return { undismissed: removed, url: options.url };
17171
17224
  }
17172
17225
  var init_dismiss = __esm({
17173
17226
  "src/commands/dismiss.ts"() {
@@ -17248,10 +17301,10 @@ init_errors();
17248
17301
  init_json();
17249
17302
  function printRepos(repos) {
17250
17303
  const entries = Object.entries(repos).sort(([a], [b]) => a.localeCompare(b));
17251
- for (const [remote, info] of entries) {
17252
- const branch = info.currentBranch ? ` (${info.currentBranch})` : "";
17304
+ for (const [remote, info2] of entries) {
17305
+ const branch = info2.currentBranch ? ` (${info2.currentBranch})` : "";
17253
17306
  console.log(` ${remote}${branch}`);
17254
- console.log(` ${info.path}`);
17307
+ console.log(` ${info2.path}`);
17255
17308
  }
17256
17309
  }
17257
17310
  function handleCommandError(err, json) {
@@ -17784,34 +17837,34 @@ program2.command("unshelve <pr-url>").description("Unshelve a PR (include in cap
17784
17837
  handleCommandError(err, options.json);
17785
17838
  }
17786
17839
  });
17787
- program2.command("dismiss <issue-url>").description("Dismiss issue reply notifications (resurfaces on new activity)").option("--json", "Output as JSON").action(async (issueUrl, options) => {
17840
+ program2.command("dismiss <url>").description("Dismiss notifications for an issue or PR (resurfaces on new activity)").option("--json", "Output as JSON").action(async (url, options) => {
17788
17841
  try {
17789
17842
  const { runDismiss: runDismiss2 } = await Promise.resolve().then(() => (init_dismiss(), dismiss_exports));
17790
- const data = await runDismiss2({ issueUrl });
17843
+ const data = await runDismiss2({ url });
17791
17844
  if (options.json) {
17792
17845
  outputJson(data);
17793
17846
  } else if (data.dismissed) {
17794
- console.log(`Dismissed: ${issueUrl}`);
17795
- console.log("Issue reply notifications are now muted.");
17847
+ console.log(`Dismissed: ${url}`);
17848
+ console.log("Notifications are now muted.");
17796
17849
  console.log("New responses after this point will resurface automatically.");
17797
17850
  } else {
17798
- console.log("Issue is already dismissed.");
17851
+ console.log("Already dismissed.");
17799
17852
  }
17800
17853
  } catch (err) {
17801
17854
  handleCommandError(err, options.json);
17802
17855
  }
17803
17856
  });
17804
- program2.command("undismiss <issue-url>").description("Undismiss an issue (re-enable reply notifications)").option("--json", "Output as JSON").action(async (issueUrl, options) => {
17857
+ program2.command("undismiss <url>").description("Undismiss an issue or PR (re-enable notifications)").option("--json", "Output as JSON").action(async (url, options) => {
17805
17858
  try {
17806
17859
  const { runUndismiss: runUndismiss2 } = await Promise.resolve().then(() => (init_dismiss(), dismiss_exports));
17807
- const data = await runUndismiss2({ issueUrl });
17860
+ const data = await runUndismiss2({ url });
17808
17861
  if (options.json) {
17809
17862
  outputJson(data);
17810
17863
  } else if (data.undismissed) {
17811
- console.log(`Undismissed: ${issueUrl}`);
17812
- console.log("Issue reply notifications are active again.");
17864
+ console.log(`Undismissed: ${url}`);
17865
+ console.log("Notifications are active again.");
17813
17866
  } else {
17814
- console.log("Issue was not dismissed.");
17867
+ console.log("Was not dismissed.");
17815
17868
  }
17816
17869
  } catch (err) {
17817
17870
  handleCommandError(err, options.json);