dev-loops 0.1.0

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.
Files changed (156) hide show
  1. package/.pi/dev-loop/defaults.yaml +477 -0
  2. package/AGENTS.md +25 -0
  3. package/CHANGELOG.md +18 -0
  4. package/LICENSE +21 -0
  5. package/README.md +178 -0
  6. package/agents/dev-loop.agent.md +82 -0
  7. package/agents/developer.agent.md +37 -0
  8. package/agents/docs.agent.md +33 -0
  9. package/agents/fixer.agent.md +53 -0
  10. package/agents/quality.agent.md +28 -0
  11. package/agents/refiner.agent.md +87 -0
  12. package/agents/review.agent.md +64 -0
  13. package/cli/index.mjs +424 -0
  14. package/extension/README.md +233 -0
  15. package/extension/checks.ts +94 -0
  16. package/extension/index.ts +131 -0
  17. package/extension/post-merge-update.ts +512 -0
  18. package/extension/presentation.ts +107 -0
  19. package/lib/dev-loops-core.mjs +284 -0
  20. package/package.json +103 -0
  21. package/scripts/README.md +1007 -0
  22. package/scripts/_cli-primitives.mjs +10 -0
  23. package/scripts/_core-helpers.mjs +30 -0
  24. package/scripts/docs/validate-links.mjs +567 -0
  25. package/scripts/docs/validate-no-duplicate-rules.mjs +250 -0
  26. package/scripts/github/_review-thread-mutations.mjs +214 -0
  27. package/scripts/github/capture-review-threads.mjs +180 -0
  28. package/scripts/github/create-draft-pr.mjs +108 -0
  29. package/scripts/github/detect-checkpoint-evidence.mjs +393 -0
  30. package/scripts/github/detect-linked-issue-pr.mjs +331 -0
  31. package/scripts/github/manage-sub-issues.mjs +394 -0
  32. package/scripts/github/probe-copilot-review.mjs +323 -0
  33. package/scripts/github/ready-for-review.mjs +93 -0
  34. package/scripts/github/reconcile-draft-gate.mjs +328 -0
  35. package/scripts/github/reply-resolve-review-thread.mjs +42 -0
  36. package/scripts/github/reply-resolve-review-threads.mjs +329 -0
  37. package/scripts/github/request-copilot-review.mjs +551 -0
  38. package/scripts/github/resolve-tracker-local-spec.mjs +205 -0
  39. package/scripts/github/stage-reviewer-draft.mjs +191 -0
  40. package/scripts/github/upsert-checkpoint-verdict.mjs +694 -0
  41. package/scripts/github/verify-fresh-review-context.mjs +125 -0
  42. package/scripts/github/write-gate-findings-log.mjs +212 -0
  43. package/scripts/loop/_checkpoint-io.mjs +55 -0
  44. package/scripts/loop/_checkpoint-paths.mjs +28 -0
  45. package/scripts/loop/_handoff-contract.mjs +230 -0
  46. package/scripts/loop/_inspect-run-viewer-adapter.mjs +345 -0
  47. package/scripts/loop/_loop-evidence.mjs +32 -0
  48. package/scripts/loop/_pr-runner-coordination.mjs +611 -0
  49. package/scripts/loop/_stale-runner-detection.mjs +145 -0
  50. package/scripts/loop/_steering-state-file.mjs +134 -0
  51. package/scripts/loop/build-handoff-envelope.mjs +181 -0
  52. package/scripts/loop/checkpoint-contract.mjs +49 -0
  53. package/scripts/loop/conductor-monitor.mjs +1850 -0
  54. package/scripts/loop/conductor.mjs +214 -0
  55. package/scripts/loop/copilot-pr-handoff.mjs +493 -0
  56. package/scripts/loop/debt-remediate.mjs +304 -0
  57. package/scripts/loop/detect-change-scope.mjs +102 -0
  58. package/scripts/loop/detect-copilot-loop-state.mjs +454 -0
  59. package/scripts/loop/detect-copilot-session-activity.mjs +186 -0
  60. package/scripts/loop/detect-initial-copilot-pr-state.mjs +318 -0
  61. package/scripts/loop/detect-internal-only-pr.mjs +270 -0
  62. package/scripts/loop/detect-issue-refinement-artifact.mjs +163 -0
  63. package/scripts/loop/detect-pr-gate-coordination-state.mjs +509 -0
  64. package/scripts/loop/detect-reviewer-loop-state.mjs +231 -0
  65. package/scripts/loop/detect-stale-runner.mjs +250 -0
  66. package/scripts/loop/detect-tracker-first-loop-state.mjs +76 -0
  67. package/scripts/loop/detect-tracker-pr-state.mjs +102 -0
  68. package/scripts/loop/info.mjs +267 -0
  69. package/scripts/loop/inspect-run-viewer/cli.mjs +117 -0
  70. package/scripts/loop/inspect-run-viewer/constants.mjs +80 -0
  71. package/scripts/loop/inspect-run-viewer/graph.mjs +757 -0
  72. package/scripts/loop/inspect-run-viewer/handoff-envelope-renderer.mjs +398 -0
  73. package/scripts/loop/inspect-run-viewer/inbox.mjs +308 -0
  74. package/scripts/loop/inspect-run-viewer/managed-instance.mjs +750 -0
  75. package/scripts/loop/inspect-run-viewer/rendering.mjs +411 -0
  76. package/scripts/loop/inspect-run-viewer/server.mjs +638 -0
  77. package/scripts/loop/inspect-run-viewer/shared.mjs +103 -0
  78. package/scripts/loop/inspect-run-viewer/status.mjs +715 -0
  79. package/scripts/loop/inspect-run-viewer-ci-changes.mjs +77 -0
  80. package/scripts/loop/inspect-run-viewer.mjs +82 -0
  81. package/scripts/loop/inspect-run.mjs +382 -0
  82. package/scripts/loop/outer-loop.mjs +419 -0
  83. package/scripts/loop/pr-runner-coordination.mjs +143 -0
  84. package/scripts/loop/pre-commit-branch-guard.mjs +68 -0
  85. package/scripts/loop/pre-flight-gate.mjs +236 -0
  86. package/scripts/loop/pre-pr-ready-gate.mjs +183 -0
  87. package/scripts/loop/pre-push-main-guard.mjs +103 -0
  88. package/scripts/loop/pre-write-remote-freshness-guard.mjs +32 -0
  89. package/scripts/loop/print-gates.mjs +42 -0
  90. package/scripts/loop/resolve-dev-loop-startup.mjs +533 -0
  91. package/scripts/loop/run-conductor-cycle.mjs +322 -0
  92. package/scripts/loop/run-queue.mjs +124 -0
  93. package/scripts/loop/run-refinement-audit.mjs +513 -0
  94. package/scripts/loop/run-watch-cycle.mjs +358 -0
  95. package/scripts/loop/steer-loop.mjs +841 -0
  96. package/scripts/loop/ui-designer-review-contract.mjs +76 -0
  97. package/scripts/loop/watch-initial-copilot-pr.mjs +253 -0
  98. package/scripts/projects/add-queue-item.mjs +528 -0
  99. package/scripts/projects/ensure-queue-board.mjs +837 -0
  100. package/scripts/projects/list-queue-items.mjs +489 -0
  101. package/scripts/projects/move-queue-item.mjs +549 -0
  102. package/scripts/projects/reorder-queue-item.mjs +518 -0
  103. package/scripts/refine/_refine-helpers.mjs +258 -0
  104. package/scripts/refine/prose-linkage-detector.mjs +92 -0
  105. package/scripts/refine/refinement-completeness-checker.mjs +88 -0
  106. package/scripts/refine/scope-boundary-cross-checker.mjs +163 -0
  107. package/scripts/refine/tree-integrity-validator.mjs +211 -0
  108. package/scripts/refine/verify.mjs +178 -0
  109. package/scripts/repo-wiki-local.mjs +156 -0
  110. package/scripts/repo-wiki.mjs +119 -0
  111. package/skills/copilot-pr-followup/SKILL.md +380 -0
  112. package/skills/dev-loop/SKILL.md +141 -0
  113. package/skills/dev-loop/scripts/dev-mode-context.mjs +152 -0
  114. package/skills/dev-loop/scripts/dev-mode-context.test.mjs +80 -0
  115. package/skills/dev-loop/scripts/init-phase.mjs +71 -0
  116. package/skills/dev-loop/scripts/log-bash-exit-1.mjs +25 -0
  117. package/skills/dev-loop/scripts/phase-files.mjs +29 -0
  118. package/skills/dev-loop/scripts/post-gate-verdict-fallback.mjs +480 -0
  119. package/skills/dev-loop/scripts/post-gate-verdict-fallback.test.mjs +732 -0
  120. package/skills/dev-loop/scripts/render-template.mjs +82 -0
  121. package/skills/dev-loop/scripts/render-template.test.mjs +63 -0
  122. package/skills/dev-loop/templates/bootstrap-agents.md +26 -0
  123. package/skills/dev-loop/templates/bootstrap-implementation-state.md +31 -0
  124. package/skills/dev-loop/templates/bootstrap-implementation-workflow.md +17 -0
  125. package/skills/dev-loop/templates/dev-mode-retrospective.md +15 -0
  126. package/skills/dev-loop/templates/dev-mode-review.md +17 -0
  127. package/skills/dev-loop/templates/dev-mode-skill-changes.md +11 -0
  128. package/skills/dev-loop/templates/merged-phase-plan.md +19 -0
  129. package/skills/dev-loop/templates/phase-doc.md +27 -0
  130. package/skills/dev-loop/templates/phase-summary.md +13 -0
  131. package/skills/dev-loop/templates/phase-variant.md +15 -0
  132. package/skills/dev-loop/templates/retrospective.md +11 -0
  133. package/skills/dev-loop/templates/review.md +32 -0
  134. package/skills/dev-loop/templates/ui-vision-review.md +55 -0
  135. package/skills/docs/acceptance-criteria-verification.md +21 -0
  136. package/skills/docs/anti-patterns.md +21 -0
  137. package/skills/docs/artifact-authority-contract.md +119 -0
  138. package/skills/docs/confirmation-rules.md +28 -0
  139. package/skills/docs/copilot-ci-status-contract.md +52 -0
  140. package/skills/docs/copilot-loop-operations.md +233 -0
  141. package/skills/docs/debt-remediation-contract.md +107 -0
  142. package/skills/docs/entrypoint-strategies.md +115 -0
  143. package/skills/docs/epic-tree-refinement-procedure.md +234 -0
  144. package/skills/docs/issue-intake-procedure.md +235 -0
  145. package/skills/docs/main-agent-contract.md +72 -0
  146. package/skills/docs/merge-preconditions.md +29 -0
  147. package/skills/docs/pr-lifecycle-contract.md +209 -0
  148. package/skills/docs/public-dev-loop-contract.md +497 -0
  149. package/skills/docs/retrospective-checkpoint-contract.md +159 -0
  150. package/skills/docs/stop-conditions.md +29 -0
  151. package/skills/docs/structural-quality.md +42 -0
  152. package/skills/docs/tracker-first-loop-state.md +281 -0
  153. package/skills/docs/validation-policy.md +27 -0
  154. package/skills/docs/workflow-handoff-contract.md +135 -0
  155. package/skills/final-approval/SKILL.md +19 -0
  156. package/skills/local-implementation/SKILL.md +640 -0
@@ -0,0 +1,308 @@
1
+ import {
2
+ DEFAULT_INBOX_MODE,
3
+ DEFAULT_INBOX_PAGE,
4
+ DEFAULT_INBOX_PR_STATE,
5
+ DEFAULT_INBOX_UPDATED_WITHIN_DAYS,
6
+ INBOX_MODE_FILTER_PRESETS,
7
+ INBOX_STATE_FILTER_PRESETS,
8
+ INBOX_UPDATED_FILTER_PRESETS,
9
+ } from "./constants.mjs";
10
+ import { dedupeRepoSlugOptions, repoSlugEquals } from "@dev-loops/core/github/repo-slug";
11
+ import {
12
+ escapeHtml,
13
+ formatStateToken,
14
+ normalizeInboxSignal,
15
+ renderSnapshotStateLabel,
16
+ renderTargetKey,
17
+ } from "./shared.mjs";
18
+ import { deriveInboxSignalFromSnapshot, summarizeCurrentPrStatus } from "./status.mjs";
19
+
20
+ function formatInboxUpdatedAt(updatedAt) {
21
+ if (typeof updatedAt !== "string" || updatedAt.trim().length === 0) {
22
+ return "updated unknown";
23
+ }
24
+ const parsed = Date.parse(updatedAt);
25
+ if (Number.isNaN(parsed)) {
26
+ return `updated ${updatedAt.trim()}`;
27
+ }
28
+ return `updated ${new Date(parsed).toISOString().slice(0, 10)}`;
29
+ }
30
+
31
+ function inboxSignalEmoji(signal) {
32
+ switch (signal) {
33
+ case "ready": return "✅";
34
+ case "attention": return "🔴";
35
+ case "waiting": return "⏳";
36
+ case "pending": return "🔄";
37
+ case "gate": return "🛡️";
38
+ case "closed": return "✖️";
39
+ default: return "❓";
40
+ }
41
+ }
42
+
43
+ function describeInboxSignal(signal) {
44
+ switch (signal) {
45
+ case "attention":
46
+ return { label: "Needs attention", shortLabel: "Attention" };
47
+ case "pending":
48
+ return { label: "CI pending", shortLabel: "CI" };
49
+ case "gate":
50
+ return { label: "Gate review required", shortLabel: "Gate" };
51
+ case "ready":
52
+ return { label: "Ready", shortLabel: "Ready" };
53
+ case "closed":
54
+ return { label: "Closed", shortLabel: "Closed" };
55
+ case "unknown":
56
+ return { label: "State unavailable", shortLabel: "Unknown" };
57
+ case "waiting":
58
+ default:
59
+ return { label: "Waiting", shortLabel: "Waiting" };
60
+ }
61
+ }
62
+
63
+ function summarizeInboxRow(snapshot, fallbackSignal = "unknown") {
64
+ const normalizedFallbackSignal = normalizeInboxSignal(fallbackSignal);
65
+ const signal = normalizedFallbackSignal !== "unknown"
66
+ ? normalizedFallbackSignal
67
+ : snapshot
68
+ ? deriveInboxSignalFromSnapshot(snapshot)
69
+ : normalizedFallbackSignal;
70
+ if (!snapshot) {
71
+ return {
72
+ signal,
73
+ signalLabel: describeInboxSignal(signal),
74
+ statusClass: null,
75
+ trustLabel: null,
76
+ needsAttention: signal === "attention",
77
+ headline: null,
78
+ };
79
+ }
80
+
81
+ return {
82
+ signal,
83
+ signalLabel: describeInboxSignal(signal),
84
+ statusClass: formatStateToken(snapshot.statusClass, "unknown"),
85
+ trustLabel: renderSnapshotStateLabel(snapshot),
86
+ needsAttention: snapshot.needsAttention === true,
87
+ headline: summarizeCurrentPrStatus(snapshot).headline,
88
+ };
89
+ }
90
+
91
+ function appendInboxViewParams(params, {
92
+ selectedTarget = null,
93
+ scopeFilter = null,
94
+ updatedWithinDays = DEFAULT_INBOX_UPDATED_WITHIN_DAYS,
95
+ state = DEFAULT_INBOX_PR_STATE,
96
+ mode = DEFAULT_INBOX_MODE,
97
+ page = DEFAULT_INBOX_PAGE,
98
+ } = {}) {
99
+ if (typeof scopeFilter === "string" && scopeFilter.trim().length > 0) {
100
+ params.set("scope", scopeFilter.trim());
101
+ }
102
+ if (selectedTarget?.repo !== undefined && selectedTarget?.repo !== null) {
103
+ params.set("repo", String(selectedTarget.repo));
104
+ }
105
+ if (selectedTarget?.pr !== undefined && selectedTarget?.pr !== null) {
106
+ params.set("pr", String(selectedTarget.pr));
107
+ }
108
+ if (updatedWithinDays === null) {
109
+ params.set("updated", "all");
110
+ } else if (updatedWithinDays !== DEFAULT_INBOX_UPDATED_WITHIN_DAYS) {
111
+ params.set("updated", String(updatedWithinDays));
112
+ }
113
+ if (page > DEFAULT_INBOX_PAGE) {
114
+ params.set("page", String(page));
115
+ }
116
+ params.set("state", state);
117
+ params.set("mode", mode);
118
+ }
119
+
120
+ function buildInboxHref(target, { scopeFilter = null, updatedWithinDays = DEFAULT_INBOX_UPDATED_WITHIN_DAYS, state = DEFAULT_INBOX_PR_STATE, mode = DEFAULT_INBOX_MODE, page = DEFAULT_INBOX_PAGE } = {}) {
121
+ const params = new URLSearchParams();
122
+ appendInboxViewParams(params, { selectedTarget: target, scopeFilter, updatedWithinDays, state, mode, page });
123
+ return `/?${params.toString()}`;
124
+ }
125
+
126
+ export function buildSnapshotHref(target, scopeFilter = null) {
127
+ if (!target) {
128
+ return null;
129
+ }
130
+ const params = new URLSearchParams();
131
+ appendInboxViewParams(params, { selectedTarget: target, scopeFilter, updatedWithinDays: DEFAULT_INBOX_UPDATED_WITHIN_DAYS, state: DEFAULT_INBOX_PR_STATE, mode: DEFAULT_INBOX_MODE, page: DEFAULT_INBOX_PAGE });
132
+ params.delete("scope");
133
+ params.delete("updated");
134
+ params.delete("limit");
135
+ params.delete("state");
136
+ params.delete("mode");
137
+ return `/snapshot.json?${params.toString()}`;
138
+ }
139
+
140
+ function renderInboxFilterHref(selectedTarget, { scopeFilter = null, updatedWithinDays = DEFAULT_INBOX_UPDATED_WITHIN_DAYS, state = DEFAULT_INBOX_PR_STATE, mode = DEFAULT_INBOX_MODE, page = DEFAULT_INBOX_PAGE } = {}) {
141
+ const params = new URLSearchParams();
142
+ appendInboxViewParams(params, { selectedTarget, scopeFilter, updatedWithinDays, state, mode, page });
143
+ const query = params.toString();
144
+ return query.length === 0 ? "/" : `/?${query}`;
145
+ }
146
+
147
+ function renderScopeSelectHref(selectedTarget, scopeFilter, { updatedWithinDays = DEFAULT_INBOX_UPDATED_WITHIN_DAYS, state = DEFAULT_INBOX_PR_STATE, mode = DEFAULT_INBOX_MODE } = {}) {
148
+ const retainedTarget = selectedTarget && (scopeFilter === null || repoSlugEquals(selectedTarget.repo, scopeFilter))
149
+ ? selectedTarget
150
+ : null;
151
+ return renderInboxFilterHref(retainedTarget, { scopeFilter, updatedWithinDays, state, mode, page: DEFAULT_INBOX_PAGE });
152
+ }
153
+
154
+ function renderInboxPageHref(selectedTarget, { scopeFilter = null, updatedWithinDays = DEFAULT_INBOX_UPDATED_WITHIN_DAYS, state = DEFAULT_INBOX_PR_STATE, mode = DEFAULT_INBOX_MODE, page = DEFAULT_INBOX_PAGE } = {}) {
155
+ return renderInboxFilterHref(selectedTarget, { scopeFilter, updatedWithinDays, state, mode, page });
156
+ }
157
+
158
+ function renderInboxPagination({ selectedTarget = null, scopeFilter = null, updatedWithinDays = DEFAULT_INBOX_UPDATED_WITHIN_DAYS, state = DEFAULT_INBOX_PR_STATE, mode = DEFAULT_INBOX_MODE, page = DEFAULT_INBOX_PAGE, totalPages = 1 } = {}) {
159
+ if (totalPages <= 1) {
160
+ return "";
161
+ }
162
+
163
+ const previousPage = Math.max(DEFAULT_INBOX_PAGE, page - 1);
164
+ const nextPage = Math.min(totalPages, page + 1);
165
+
166
+ return `<nav class="assigned-pr-pagination" aria-label="Sidebar pagination">
167
+ <a class="assigned-pr-page-link ${page <= DEFAULT_INBOX_PAGE ? "is-disabled" : ""}" href="${escapeHtml(renderInboxPageHref(selectedTarget, { scopeFilter, updatedWithinDays, state, mode, page: previousPage }))}" aria-label="Previous page" ${page <= DEFAULT_INBOX_PAGE ? 'aria-disabled="true" tabindex="-1"' : ""}>←</a>
168
+ <span class="assigned-pr-page-status">${escapeHtml(String(page))}/${escapeHtml(String(totalPages))}</span>
169
+ <a class="assigned-pr-page-link ${page >= totalPages ? "is-disabled" : ""}" href="${escapeHtml(renderInboxPageHref(selectedTarget, { scopeFilter, updatedWithinDays, state, mode, page: nextPage }))}" aria-label="Next page" ${page >= totalPages ? 'aria-disabled="true" tabindex="-1"' : ""}>→</a>
170
+ </nav>`;
171
+ }
172
+
173
+ export function renderInboxSidebar(items, selectedTarget, { scopeFilter = null, scopeOptions = [], updatedWithinDays = DEFAULT_INBOX_UPDATED_WITHIN_DAYS, state = DEFAULT_INBOX_PR_STATE, mode = DEFAULT_INBOX_MODE, page = DEFAULT_INBOX_PAGE, totalPages = 1 } = {}) {
174
+ const selectedKey = renderTargetKey(selectedTarget);
175
+ const uniqueScopeOptions = ["All repos", ...dedupeRepoSlugOptions(scopeOptions)].sort((left, right) => {
176
+ if (left === "All repos") {
177
+ return -1;
178
+ }
179
+ if (right === "All repos") {
180
+ return 1;
181
+ }
182
+ return left.localeCompare(right);
183
+ });
184
+ return `<aside class="assigned-pr-inbox" data-sidebar-collapsed="false">
185
+ <div class="assigned-pr-inbox-header">
186
+ <h2>PR inspection dashboard</h2>
187
+ <button type="button" class="inbox-collapse-toggle" data-inbox-toggle aria-expanded="true" aria-label="Collapse sidebar" title="Collapse sidebar">◀</button>
188
+ </div>
189
+ <div class="assigned-pr-controls">
190
+ <div class="assigned-pr-control-row assigned-pr-scope-row">
191
+ <label class="assigned-pr-filter-label" for="assigned-pr-scope-select">Scope</label>
192
+ <select id="assigned-pr-scope-select" class="assigned-pr-select" data-nav-select>
193
+ ${uniqueScopeOptions.map((option) => {
194
+ const optionScope = option === "All repos" ? null : option;
195
+ const selected = optionScope === null ? scopeFilter === null : repoSlugEquals(optionScope, scopeFilter);
196
+ return `<option value="${escapeHtml(renderScopeSelectHref(selectedTarget, optionScope, { updatedWithinDays, state, mode }))}" ${selected ? "selected" : ""}>${escapeHtml(option)}</option>`;
197
+ }).join("")}
198
+ </select>
199
+ </div>
200
+ <div class="assigned-pr-control-row assigned-pr-secondary-controls">
201
+ <label class="assigned-pr-filter-label" for="assigned-pr-state-select">State</label>
202
+ <select id="assigned-pr-mode-select" class="assigned-pr-select assigned-pr-select-mid" data-nav-select aria-label="Assignment mode">
203
+ ${INBOX_MODE_FILTER_PRESETS.map((preset) => {
204
+ const selected = preset.value === mode;
205
+ return `<option value="${escapeHtml(renderInboxFilterHref(selectedTarget, { scopeFilter, updatedWithinDays, state, mode: preset.value, page: DEFAULT_INBOX_PAGE }))}" ${selected ? "selected" : ""}>${escapeHtml(preset.label)}</option>`;
206
+ }).join("")}
207
+ </select>
208
+ <select id="assigned-pr-state-select" class="assigned-pr-select assigned-pr-select-mid" data-nav-select>
209
+ ${INBOX_STATE_FILTER_PRESETS.map((preset) => {
210
+ const selected = preset.value === state;
211
+ return `<option value="${escapeHtml(renderInboxFilterHref(selectedTarget, { scopeFilter, updatedWithinDays, state: preset.value, mode, page: DEFAULT_INBOX_PAGE }))}" ${selected ? "selected" : ""}>${escapeHtml(preset.label)}</option>`;
212
+ }).join("")}
213
+ </select>
214
+ <select id="assigned-pr-updated-select" class="assigned-pr-select assigned-pr-select-sm assigned-pr-select-updated" data-nav-select aria-label="Updated window">
215
+ ${INBOX_UPDATED_FILTER_PRESETS.map((preset) => {
216
+ const selected = preset.value === updatedWithinDays;
217
+ return `<option value="${escapeHtml(renderInboxFilterHref(selectedTarget, { scopeFilter, updatedWithinDays: preset.value, state, mode, page: DEFAULT_INBOX_PAGE }))}" ${selected ? "selected" : ""}>${escapeHtml(preset.label)}</option>`;
218
+ }).join("")}
219
+ </select>
220
+ </div>
221
+ </div>
222
+ <label class="inbox-search-label" for="inbox-search">Search PRs</label>
223
+ <input id="inbox-search" class="inbox-search-input" type="search" placeholder="Search PR # or title…" data-inbox-search />
224
+ <ul class="assigned-pr-list" data-inbox-list>
225
+ ${items.map((item) => {
226
+ const summary = summarizeInboxRow(item.snapshot ?? null, item.signal ?? "unknown");
227
+ const target = item.target;
228
+ const key = renderTargetKey(target);
229
+ const selected = key === selectedKey;
230
+ const searchText = `${target.repo} #${target.pr} ${item.title ?? ""} ${summary.signalLabel.label} ${summary.statusClass ?? ""} ${summary.trustLabel ?? ""} ${summary.headline ?? ""} ${item.updatedAt ?? ""}`.toLowerCase();
231
+ return `<li class="assigned-pr-row assigned-pr-row-${escapeHtml(summary.signal)} ${selected ? "is-selected" : ""}" data-inbox-item data-inbox-signal="${escapeHtml(summary.signal)}" data-search="${escapeHtml(searchText)}">
232
+ <a class="assigned-pr-link" href="${escapeHtml(buildInboxHref(target, { scopeFilter, updatedWithinDays, state, mode, page }))}" ${selected ? 'aria-current="page"' : ""}>
233
+ <div class="assigned-pr-line assigned-pr-title-line">
234
+ <span class="assigned-pr-id-col">
235
+ <span class="assigned-pr-id">#${escapeHtml(String(target.pr))}</span>
236
+ <span class="assigned-pr-signal-emoji" aria-label="${escapeHtml(summary.signalLabel.label)}">${inboxSignalEmoji(summary.signal)}</span>
237
+ </span>
238
+ <span class="assigned-pr-title">${escapeHtml(item.title ?? "Untitled pull request")}</span>
239
+ </div>
240
+ <div class="assigned-pr-line assigned-pr-meta assigned-pr-meta-primary">
241
+ ${scopeFilter === null ? `<span class="assigned-pr-repo">${escapeHtml(target.repo)}</span>` : ""}
242
+ <span class="assigned-pr-updated">${escapeHtml(formatInboxUpdatedAt(item.updatedAt))}</span>
243
+ </div>
244
+ <div class="assigned-pr-line assigned-pr-meta assigned-pr-meta-secondary">
245
+ ${summary.statusClass ? `<span class="assigned-pr-status">${escapeHtml(summary.statusClass)}</span>` : ""}
246
+ ${summary.trustLabel ? `<span class="assigned-pr-trust">${escapeHtml(summary.trustLabel)}</span>` : ""}
247
+ ${summary.headline ? `<span class="assigned-pr-headline">${escapeHtml(summary.headline)}</span>` : ""}
248
+ </div>
249
+ </a>
250
+ </li>`;
251
+ }).join("")}
252
+ </ul>
253
+ <p class="assigned-pr-empty" data-inbox-empty data-empty-default="No assigned PRs are visible in this view." data-empty-search="No assigned PRs match this search." hidden>No assigned PRs are visible in this view.</p>
254
+ ${renderInboxPagination({ selectedTarget, scopeFilter, updatedWithinDays, state, mode, page, totalPages })}
255
+ </aside>`;
256
+ }
257
+
258
+ export function renderInboxShellScript() {
259
+ return `<script>
260
+ (() => {
261
+ const sidebar = document.querySelector(".assigned-pr-inbox");
262
+ const toggle = document.querySelector("[data-inbox-toggle]");
263
+ const search = document.querySelector("[data-inbox-search]");
264
+ const navSelects = Array.from(document.querySelectorAll("[data-nav-select]"));
265
+ const items = Array.from(document.querySelectorAll("[data-inbox-item]"));
266
+ const empty = document.querySelector("[data-inbox-empty]");
267
+ const updateFilter = () => {
268
+ const query = (search?.value ?? "").trim().toLowerCase();
269
+ let visibleCount = 0;
270
+ items.forEach((item) => {
271
+ const haystack = item.getAttribute("data-search") ?? "";
272
+ const visible = query.length === 0 || haystack.includes(query);
273
+ item.hidden = !visible;
274
+ if (visible) {
275
+ visibleCount += 1;
276
+ }
277
+ });
278
+ if (empty) {
279
+ const defaultMessage = empty.dataset.emptyDefault ?? "No assigned PRs are visible in this view.";
280
+ const searchMessage = empty.dataset.emptySearch ?? "No assigned PRs match this search.";
281
+ empty.textContent = query.length === 0 ? defaultMessage : searchMessage;
282
+ empty.hidden = visibleCount !== 0;
283
+ }
284
+ };
285
+ toggle?.addEventListener("click", () => {
286
+ const collapsed = sidebar?.dataset.sidebarCollapsed === "true";
287
+ if (!sidebar) {
288
+ return;
289
+ }
290
+ sidebar.dataset.sidebarCollapsed = collapsed ? "false" : "true";
291
+ toggle.textContent = collapsed ? "◀" : "▶";
292
+ toggle.setAttribute("aria-label", collapsed ? "Collapse sidebar" : "Expand sidebar");
293
+ toggle.setAttribute("title", collapsed ? "Collapse sidebar" : "Expand sidebar");
294
+ toggle.setAttribute("aria-expanded", collapsed ? "true" : "false");
295
+ });
296
+ search?.addEventListener("input", updateFilter);
297
+ navSelects.forEach((select) => {
298
+ select.addEventListener("change", () => {
299
+ const href = select.value;
300
+ if (typeof href === "string" && href.length > 0) {
301
+ window.location.assign(href);
302
+ }
303
+ });
304
+ });
305
+ updateFilter();
306
+ })();
307
+ </script>`;
308
+ }