holo-codex 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 (149) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/CONTRIBUTING.md +54 -0
  3. package/LICENSE +21 -0
  4. package/README.md +215 -0
  5. package/README.zh-CN.md +215 -0
  6. package/SECURITY.md +39 -0
  7. package/assets/brand/README.md +35 -0
  8. package/assets/brand/holo-codex-icon.svg +28 -0
  9. package/assets/brand/holo-codex-lockup.svg +49 -0
  10. package/assets/brand/holo-codex-mark.svg +33 -0
  11. package/assets/brand/holo-codex-plugin-card.png +0 -0
  12. package/assets/brand/holo-codex-plugin-card.svg +81 -0
  13. package/assets/brand/holo-codex-readme-hero.png +0 -0
  14. package/assets/brand/holo-codex-readme-hero.svg +140 -0
  15. package/assets/brand/holo-codex-social-preview.png +0 -0
  16. package/assets/brand/holo-codex-social-preview.svg +130 -0
  17. package/assets/brand/holo-codex-wordmark-options.svg +52 -0
  18. package/docs/checklists/agent-loop-first-delivery-audit.md +129 -0
  19. package/docs/examples/generic-loop-repo-hygiene.md +168 -0
  20. package/docs/install.md +190 -0
  21. package/docs/local-release-readiness.md +206 -0
  22. package/docs/release-checklist.md +144 -0
  23. package/docs/self-bootstrap.md +150 -0
  24. package/docs/trust-and-safety.md +45 -0
  25. package/package.json +83 -0
  26. package/plugins/autonomous-pr-loop/.codex-plugin/plugin.json +17 -0
  27. package/plugins/autonomous-pr-loop/.mcp.json +13 -0
  28. package/plugins/autonomous-pr-loop/bin/agent-loop.mjs +31 -0
  29. package/plugins/autonomous-pr-loop/core/artifacts.ts +164 -0
  30. package/plugins/autonomous-pr-loop/core/autonomy-policy.ts +206 -0
  31. package/plugins/autonomous-pr-loop/core/ci.ts +131 -0
  32. package/plugins/autonomous-pr-loop/core/cli-i18n.ts +123 -0
  33. package/plugins/autonomous-pr-loop/core/cli.ts +1413 -0
  34. package/plugins/autonomous-pr-loop/core/command-runner.ts +446 -0
  35. package/plugins/autonomous-pr-loop/core/command.ts +47 -0
  36. package/plugins/autonomous-pr-loop/core/config-editor.ts +140 -0
  37. package/plugins/autonomous-pr-loop/core/config.ts +293 -0
  38. package/plugins/autonomous-pr-loop/core/controller-host.ts +19 -0
  39. package/plugins/autonomous-pr-loop/core/dashboard-server.ts +536 -0
  40. package/plugins/autonomous-pr-loop/core/delivery-work-item.ts +217 -0
  41. package/plugins/autonomous-pr-loop/core/doctor.ts +335 -0
  42. package/plugins/autonomous-pr-loop/core/errors.ts +82 -0
  43. package/plugins/autonomous-pr-loop/core/gate-recovery.ts +176 -0
  44. package/plugins/autonomous-pr-loop/core/gates.ts +26 -0
  45. package/plugins/autonomous-pr-loop/core/generic-lifecycle.ts +399 -0
  46. package/plugins/autonomous-pr-loop/core/git.ts +213 -0
  47. package/plugins/autonomous-pr-loop/core/github.ts +269 -0
  48. package/plugins/autonomous-pr-loop/core/gitnexus.ts +90 -0
  49. package/plugins/autonomous-pr-loop/core/happy.ts +42 -0
  50. package/plugins/autonomous-pr-loop/core/hook-capture.ts +115 -0
  51. package/plugins/autonomous-pr-loop/core/hook-events.ts +22 -0
  52. package/plugins/autonomous-pr-loop/core/hook-installation.ts +85 -0
  53. package/plugins/autonomous-pr-loop/core/hook-observer.ts +84 -0
  54. package/plugins/autonomous-pr-loop/core/hook-policy.ts +423 -0
  55. package/plugins/autonomous-pr-loop/core/hook-router.ts +452 -0
  56. package/plugins/autonomous-pr-loop/core/index.ts +32 -0
  57. package/plugins/autonomous-pr-loop/core/local-install.ts +778 -0
  58. package/plugins/autonomous-pr-loop/core/locale.ts +60 -0
  59. package/plugins/autonomous-pr-loop/core/loop-shapes.ts +190 -0
  60. package/plugins/autonomous-pr-loop/core/mcp-controller.ts +1479 -0
  61. package/plugins/autonomous-pr-loop/core/notification-feed.ts +263 -0
  62. package/plugins/autonomous-pr-loop/core/plan-parser.ts +206 -0
  63. package/plugins/autonomous-pr-loop/core/plugin-paths.ts +32 -0
  64. package/plugins/autonomous-pr-loop/core/policy.ts +65 -0
  65. package/plugins/autonomous-pr-loop/core/pr-lifecycle.ts +464 -0
  66. package/plugins/autonomous-pr-loop/core/pr-selector.ts +284 -0
  67. package/plugins/autonomous-pr-loop/core/profiles.ts +439 -0
  68. package/plugins/autonomous-pr-loop/core/redaction.ts +17 -0
  69. package/plugins/autonomous-pr-loop/core/repo-root.ts +22 -0
  70. package/plugins/autonomous-pr-loop/core/review-comments.ts +77 -0
  71. package/plugins/autonomous-pr-loop/core/scope-guard.ts +179 -0
  72. package/plugins/autonomous-pr-loop/core/state-machine.ts +828 -0
  73. package/plugins/autonomous-pr-loop/core/state-types.ts +130 -0
  74. package/plugins/autonomous-pr-loop/core/storage.ts +2527 -0
  75. package/plugins/autonomous-pr-loop/core/types.ts +567 -0
  76. package/plugins/autonomous-pr-loop/core/worker-events.ts +412 -0
  77. package/plugins/autonomous-pr-loop/core/worker-policy.ts +72 -0
  78. package/plugins/autonomous-pr-loop/core/worker-prompts.ts +182 -0
  79. package/plugins/autonomous-pr-loop/core/worker.ts +809 -0
  80. package/plugins/autonomous-pr-loop/core/workflow-board.ts +1515 -0
  81. package/plugins/autonomous-pr-loop/hooks/dist/permission-request.js +2462 -0
  82. package/plugins/autonomous-pr-loop/hooks/dist/post-compact.js +2462 -0
  83. package/plugins/autonomous-pr-loop/hooks/dist/post-tool-use.js +2462 -0
  84. package/plugins/autonomous-pr-loop/hooks/dist/pre-compact.js +2462 -0
  85. package/plugins/autonomous-pr-loop/hooks/dist/pre-tool-use.js +3460 -0
  86. package/plugins/autonomous-pr-loop/hooks/dist/session-start.js +2462 -0
  87. package/plugins/autonomous-pr-loop/hooks/dist/stop.js +2462 -0
  88. package/plugins/autonomous-pr-loop/hooks/dist/user-prompt-submit.js +2462 -0
  89. package/plugins/autonomous-pr-loop/hooks/hooks.json +106 -0
  90. package/plugins/autonomous-pr-loop/hooks/observe-runner.ts +25 -0
  91. package/plugins/autonomous-pr-loop/hooks/permission-request.ts +4 -0
  92. package/plugins/autonomous-pr-loop/hooks/post-compact.ts +4 -0
  93. package/plugins/autonomous-pr-loop/hooks/post-tool-use.ts +4 -0
  94. package/plugins/autonomous-pr-loop/hooks/pre-compact.ts +4 -0
  95. package/plugins/autonomous-pr-loop/hooks/pre-tool-use.ts +44 -0
  96. package/plugins/autonomous-pr-loop/hooks/session-start.ts +4 -0
  97. package/plugins/autonomous-pr-loop/hooks/stop.ts +4 -0
  98. package/plugins/autonomous-pr-loop/hooks/user-prompt-submit.ts +4 -0
  99. package/plugins/autonomous-pr-loop/mcp-server/src/index.ts +87 -0
  100. package/plugins/autonomous-pr-loop/mcp-server/src/tools.ts +205 -0
  101. package/plugins/autonomous-pr-loop/package.json +9 -0
  102. package/plugins/autonomous-pr-loop/schemas/config.schema.json +74 -0
  103. package/plugins/autonomous-pr-loop/schemas/marketplace.schema.json +46 -0
  104. package/plugins/autonomous-pr-loop/schemas/plugin.schema.json +32 -0
  105. package/plugins/autonomous-pr-loop/schemas/state.schema.json +19 -0
  106. package/plugins/autonomous-pr-loop/schemas/worker-event.schema.json +19 -0
  107. package/plugins/autonomous-pr-loop/schemas/worker-result.schema.json +58 -0
  108. package/plugins/autonomous-pr-loop/scripts/agent-loop.ts +44 -0
  109. package/plugins/autonomous-pr-loop/skills/autonomous-pr-loop/SKILL.md +26 -0
  110. package/plugins/autonomous-pr-loop/skills/autonomous-pr-loop/agents/openai.yaml +6 -0
  111. package/plugins/autonomous-pr-loop/ui/index.html +26 -0
  112. package/plugins/autonomous-pr-loop/ui/public/favicon.svg +7 -0
  113. package/plugins/autonomous-pr-loop/ui/src/api.ts +639 -0
  114. package/plugins/autonomous-pr-loop/ui/src/app.tsx +238 -0
  115. package/plugins/autonomous-pr-loop/ui/src/components/ActivityBadge.tsx +31 -0
  116. package/plugins/autonomous-pr-loop/ui/src/components/BrandMark.tsx +36 -0
  117. package/plugins/autonomous-pr-loop/ui/src/components/Collapsible.tsx +6 -0
  118. package/plugins/autonomous-pr-loop/ui/src/components/CommandPreview.tsx +15 -0
  119. package/plugins/autonomous-pr-loop/ui/src/components/ConfigEditor.tsx +389 -0
  120. package/plugins/autonomous-pr-loop/ui/src/components/EmptyState.tsx +10 -0
  121. package/plugins/autonomous-pr-loop/ui/src/components/ErrorState.tsx +12 -0
  122. package/plugins/autonomous-pr-loop/ui/src/components/List.tsx +7 -0
  123. package/plugins/autonomous-pr-loop/ui/src/components/MetricRow.tsx +6 -0
  124. package/plugins/autonomous-pr-loop/ui/src/components/ResponsiveTable.tsx +65 -0
  125. package/plugins/autonomous-pr-loop/ui/src/components/RiskBadge.tsx +10 -0
  126. package/plugins/autonomous-pr-loop/ui/src/components/StatusBadge.tsx +29 -0
  127. package/plugins/autonomous-pr-loop/ui/src/components/TopMetric.tsx +10 -0
  128. package/plugins/autonomous-pr-loop/ui/src/fixtures.ts +1152 -0
  129. package/plugins/autonomous-pr-loop/ui/src/i18n.ts +1105 -0
  130. package/plugins/autonomous-pr-loop/ui/src/main.tsx +14 -0
  131. package/plugins/autonomous-pr-loop/ui/src/pages/CommandCenter.tsx +470 -0
  132. package/plugins/autonomous-pr-loop/ui/src/pages/CommandCenterParts.tsx +276 -0
  133. package/plugins/autonomous-pr-loop/ui/src/pages/agent-timeline/AgentTimelineView.tsx +73 -0
  134. package/plugins/autonomous-pr-loop/ui/src/pages/artifact-viewer/ArtifactViewer.tsx +44 -0
  135. package/plugins/autonomous-pr-loop/ui/src/pages/dry-run-preview/DryRunPreview.tsx +66 -0
  136. package/plugins/autonomous-pr-loop/ui/src/pages/event-ledger/EventLedger.tsx +17 -0
  137. package/plugins/autonomous-pr-loop/ui/src/pages/gate-center/GateCenter.tsx +34 -0
  138. package/plugins/autonomous-pr-loop/ui/src/pages/mission-control/MissionControl.tsx +104 -0
  139. package/plugins/autonomous-pr-loop/ui/src/pages/mission-control/WorkflowBoard.tsx +577 -0
  140. package/plugins/autonomous-pr-loop/ui/src/pages/notifications/NotificationsView.tsx +30 -0
  141. package/plugins/autonomous-pr-loop/ui/src/pages/plan-navigator/PlanNavigator.tsx +19 -0
  142. package/plugins/autonomous-pr-loop/ui/src/pages/policy-config/PolicyConfig.tsx +22 -0
  143. package/plugins/autonomous-pr-loop/ui/src/pages/pr-inbox/PrInbox.tsx +26 -0
  144. package/plugins/autonomous-pr-loop/ui/src/pages/recovery-center/RecoveryCenter.tsx +125 -0
  145. package/plugins/autonomous-pr-loop/ui/src/pages/scope-guard/ScopeGuard.tsx +16 -0
  146. package/plugins/autonomous-pr-loop/ui/src/pages/worker-runs/WorkerRuns.tsx +39 -0
  147. package/plugins/autonomous-pr-loop/ui/src/styles.css +2673 -0
  148. package/plugins/autonomous-pr-loop/ui/src/theme.ts +57 -0
  149. package/tsconfig.json +18 -0
@@ -0,0 +1,276 @@
1
+ import type { JSX } from "react";
2
+ import { useState } from "react";
3
+ import type {
4
+ AgentTimelineEntry,
5
+ DashboardApi,
6
+ DryRunPreviewData,
7
+ LoopNotification,
8
+ MissionControlData,
9
+ PlanItem,
10
+ PlanNavigatorData,
11
+ PrSelectionData,
12
+ WorkerSummary
13
+ } from "../api.js";
14
+ import { EmptyState } from "../components/EmptyState.js";
15
+ import { StatusBadge, toneForStatus, type StatusTone } from "../components/StatusBadge.js";
16
+ import { List } from "../components/List.js";
17
+ import { MetricRow } from "../components/MetricRow.js";
18
+ import { ResponsiveTable } from "../components/ResponsiveTable.js";
19
+ import { displayValueLabel, t } from "../i18n.js";
20
+ import type { EffectiveLocale } from "../../../core/locale.js";
21
+
22
+ export type { EffectiveLocale };
23
+
24
+ export interface ArtifactPreviewState {
25
+ id: string;
26
+ text: string;
27
+ error?: string;
28
+ truncated?: boolean;
29
+ }
30
+
31
+ export type TimelinePreset = "all" | "live" | "hooks" | "gates" | "artifacts";
32
+
33
+ export const timelinePresets: Array<{ id: TimelinePreset; labelKey: string; sources?: AgentTimelineEntry["source"][] }> = [
34
+ { id: "all", labelKey: "timelinePresetAll" },
35
+ { id: "live", labelKey: "timelinePresetLive", sources: ["worker", "worker_event"] },
36
+ { id: "hooks", labelKey: "timelinePresetHooks", sources: ["event", "worker_event"] },
37
+ { id: "gates", labelKey: "timelinePresetGates", sources: ["gate", "decision"] },
38
+ { id: "artifacts", labelKey: "timelinePresetArtifacts", sources: ["artifact"] }
39
+ ];
40
+
41
+ export function PlanSelectionSummary({ data, selection, locale }: { data: PlanNavigatorData | undefined; selection: PrSelectionData | undefined; locale: EffectiveLocale }): JSX.Element {
42
+ return (
43
+ <section className="focus-panel">
44
+ <div>
45
+ <p className="eyebrow">{selectionEyebrow(selection, locale)}</p>
46
+ <h2>{selectionTitle(selection, data?.selectedNext, locale)}</h2>
47
+ <p>{selectionDetail(selection, data?.selectedNext, data?.ambiguous ?? false, locale)}</p>
48
+ </div>
49
+ <StatusBadge value={selectionStatus(selection, data?.ambiguous ?? false, locale)} tone={selectionTone(selection, data?.ambiguous ?? false)} />
50
+ </section>
51
+ );
52
+ }
53
+
54
+ export function PlanList({ items, locale }: { items: PlanNavigatorData["candidates"]; locale: EffectiveLocale }): JSX.Element {
55
+ return items.length === 0 ? (
56
+ <EmptyState title={t(locale, "noPrs")} message={t(locale, "noPrsMessage")} />
57
+ ) : (
58
+ <div className="plan-list-table">
59
+ <ResponsiveTable columns={[t(locale, "tablePr"), t(locale, "tableStatus"), t(locale, "title"), t(locale, "issues")]} rows={items.map((item) => ({ key: item.id, cells: [item.id, item.status, item.title, item.issueRefs.join(", ") || "-"], cardTitle: item.id, cardMeta: item.status, cardSummary: item.title }))} empty={t(locale, "noPrsEmpty")} />
60
+ </div>
61
+ );
62
+ }
63
+
64
+ export function NotificationList({ items, locale }: { items: LoopNotification[]; locale: EffectiveLocale }): JSX.Element {
65
+ return items.length === 0 ? <EmptyState title={t(locale, "noNotifications")} message={t(locale, "noNotificationsMessage")} /> : <div className="notification-list">{items.map((item) => <details key={item.id} className="notification-item"><summary><span>{item.title}</span><StatusBadge value={severityLabel(locale, item.severity)} tone={toneForStatus(item.severity)} /></summary><p>{item.reason}</p><pre>{JSON.stringify(item.payload ?? {}, null, 2)}</pre></details>)}</div>;
66
+ }
67
+
68
+ export function ProfileDetails({ data, locale }: { data: MissionControlData; locale: EffectiveLocale }): JSX.Element {
69
+ const profile = data.profile;
70
+ if (!profile) return <EmptyState title={t(locale, "workflowUnavailable")} message={t(locale, "workflowUnavailableMessage")} />;
71
+ return (
72
+ <div className="two-stack compact-stack">
73
+ <section className="summary-panel">
74
+ <MetricRow label={t(locale, "fieldLoopShape")} value={profile.loopShape} tone="blue" />
75
+ <MetricRow label={t(locale, "fieldWorkflowProfile")} value={profile.workflowLabel} tone="blue" />
76
+ <MetricRow label={t(locale, "fieldRoleProfile")} value={displayValueLabel(locale, profile.roleProfile)} tone="blue" />
77
+ <MetricRow label={t(locale, "currentRole")} value={profile.currentRole ? `${profile.currentRole.label} / ${profile.currentRole.sandbox}` : t(locale, "none")} tone={profile.currentRole ? "yellow" : "muted"} />
78
+ </section>
79
+ <List items={[profile.workflowDescription, profile.autonomyBoundary, profile.handoffSummary, profile.validationPosture]} locale={locale} />
80
+ <ResponsiveTable
81
+ columns={[t(locale, "tableState"), t(locale, "tableRole"), t(locale, "tableWorker"), t(locale, "tableSandbox")]}
82
+ rows={profile.roleMapping.map((role) => ({ key: `${role.state}:${role.alias}`, cells: [role.state, role.label, role.workerType, role.sandbox], cardTitle: role.state, cardMeta: role.label, cardSummary: `${role.workerType} / ${role.sandbox}` }))}
83
+ empty={t(locale, "noRoles")}
84
+ />
85
+ </div>
86
+ );
87
+ }
88
+
89
+ export function WorkflowStages({ stages, locale }: { stages: NonNullable<DryRunPreviewData["workflowStages"]>; locale: EffectiveLocale }): JSX.Element {
90
+ return <ResponsiveTable columns={[t(locale, "tableState"), t(locale, "tableRole"), t(locale, "tableWorker"), t(locale, "tableGate")]} rows={stages.map((stage) => ({ key: stage.state, cells: [stage.state, stage.roleAlias ?? "-", stage.workerType ?? "-", stage.gateExpected ? t(locale, "yes") : t(locale, "no")], cardTitle: stage.state, cardMeta: stage.roleAlias ?? stage.workerType ?? "-", cardSummary: stage.gateExpected ? t(locale, "possibleGates") : t(locale, "no") }))} empty={t(locale, "noWorkflowStages")} />;
91
+ }
92
+
93
+ export function WorkerEventDetails({ worker, api, locale }: { worker: WorkerSummary; api: DashboardApi; locale: EffectiveLocale }): JSX.Element {
94
+ const [entries, setEntries] = useState<AgentTimelineEntry[]>();
95
+ const [error, setError] = useState<string>();
96
+ const load = async (): Promise<void> => {
97
+ if (entries !== undefined || error) return;
98
+ const result = await api.agentTimeline({ workerId: worker.id, sources: ["worker_event"], limit: 25 });
99
+ if (!result.ok || !result.data) {
100
+ setError(result.error?.message ?? t(locale, "timelineLoadError"));
101
+ return;
102
+ }
103
+ setEntries(result.data.entries);
104
+ };
105
+ return (
106
+ <details className="inline-details" onToggle={(event) => event.currentTarget.open && void load()}>
107
+ <summary>{worker.resultArtifactId ?? worker.error ?? t(locale, "workerEvents")}</summary>
108
+ {error ? <p>{error}</p> : <TimelineEntries entries={entries ?? []} locale={locale} compact />}
109
+ </details>
110
+ );
111
+ }
112
+
113
+ export function TimelineEntries({ entries, locale, compact = false }: { entries: AgentTimelineEntry[]; locale: EffectiveLocale; compact?: boolean }): JSX.Element {
114
+ if (entries.length === 0) {
115
+ return <EmptyState title={t(locale, "timelineEmpty")} message={t(locale, "timelineEmptyMessage")} />;
116
+ }
117
+ return (
118
+ <div className={compact ? "timeline-list timeline-list--compact" : "timeline-list"}>
119
+ {entries.map((entry) => (
120
+ <details key={entry.cursor} className="timeline-item">
121
+ <summary>
122
+ <span>{formatTime(entry.occurredAt)}</span>
123
+ <strong>{entry.title}</strong>
124
+ <StatusBadge value={entry.source} tone={toneForStatus(entry.status ?? entry.source)} />
125
+ </summary>
126
+ <dl className="detail-list">
127
+ <div><dt>{t(locale, "tableSeq")}</dt><dd>{String(entry.timelineSeq)}</dd></div>
128
+ <div><dt>{t(locale, "timelineSource")}</dt><dd>{entry.source}</dd></div>
129
+ <div><dt>{t(locale, "tableKind")}</dt><dd>{entry.kind}</dd></div>
130
+ {entry.workerId ? <div><dt>{t(locale, "tableWorker")}</dt><dd>{entry.workerId}</dd></div> : null}
131
+ {entry.threadId ? <div><dt>{t(locale, "timelineThread")}</dt><dd>{entry.threadId}</dd></div> : null}
132
+ <div><dt>{t(locale, "tableDetails")}</dt><dd>{entry.summary || "-"}</dd></div>
133
+ </dl>
134
+ </details>
135
+ ))}
136
+ </div>
137
+ );
138
+ }
139
+
140
+ export function TimelineSummary({ data, locale }: { data: MissionControlData; locale: EffectiveLocale }): JSX.Element {
141
+ const summary = data.timelineSummary;
142
+ const latest = summary?.latest;
143
+ const lastFailure = summary?.lastFailure;
144
+ return (
145
+ <section className="summary-panel">
146
+ <MetricRow label={t(locale, "timelineLatest")} value={latest ? `${latest.source}: ${latest.title}` : t(locale, "none")} tone={latest ? "blue" : "muted"} />
147
+ <MetricRow label={t(locale, "timelineLastFailure")} value={lastFailure ? `${lastFailure.source}: ${lastFailure.title}` : t(locale, "none")} tone={lastFailure ? "red" : "green"} />
148
+ <MetricRow label={t(locale, "timelineActiveWorker")} value={summary?.activeWorker ? `${summary.activeWorker.type} / ${summary.activeWorker.status}` : t(locale, "none")} tone={summary?.activeWorker ? "yellow" : "muted"} />
149
+ <MetricRow label={t(locale, "timelineObservationGap")} value={summary?.hasObservationGap ? t(locale, "timelineGap") : t(locale, "timelineNoGap")} tone={summary?.hasObservationGap ? "yellow" : "green"} />
150
+ </section>
151
+ );
152
+ }
153
+
154
+ export function filterTimelinePreset(entries: AgentTimelineEntry[], preset: TimelinePreset): AgentTimelineEntry[] {
155
+ if (preset !== "hooks") {
156
+ return entries;
157
+ }
158
+ return entries.filter((entry) =>
159
+ entry.kind.toLowerCase().includes("hook") ||
160
+ entry.kind.toLowerCase().includes("permission") ||
161
+ entry.title.toLowerCase().includes("hook") ||
162
+ entry.title.toLowerCase().includes("permission")
163
+ );
164
+ }
165
+
166
+ export function workflowRoleShapeSummary(data: MissionControlData, locale: EffectiveLocale): string {
167
+ const workflow = data.profile?.workflowLabel ?? displayValueLabel(locale, "default_pr_loop");
168
+ const role = data.profile?.currentRole?.label ?? displayValueLabel(locale, data.current.run?.currentState ?? data.current.status);
169
+ const shape = data.profile?.loopShape ?? "pr-loop";
170
+ return `${workflow} / ${role} / ${shape}`;
171
+ }
172
+
173
+ export function selectionCompact(selection: PrSelectionData | undefined, fallback: PlanItem | undefined, locale: EffectiveLocale): string {
174
+ if (selection?.mode === "generic_loop") return displayValueLabel(locale, selection.workflowProfile ?? "generic-loop");
175
+ if (selection?.mode === "ambiguous") return t(locale, "ambiguous");
176
+ const item = selection?.item ?? fallback;
177
+ if (!item) return t(locale, "unknown");
178
+ return selection?.prNumber ? `${item.id} / #${selection.prNumber}` : item.id;
179
+ }
180
+
181
+ export function selectionEyebrow(selection: PrSelectionData | undefined, locale: EffectiveLocale): string {
182
+ if (selection?.mode === "generic_loop") return t(locale, "genericLoop");
183
+ if (selection?.mode === "current_pr") return t(locale, "currentPr");
184
+ if (selection?.mode === "next_spec") return t(locale, "nextPr");
185
+ return t(locale, "selectedNextPr");
186
+ }
187
+
188
+ export function selectionTitle(selection: PrSelectionData | undefined, fallback: PlanItem | undefined, locale: EffectiveLocale): string {
189
+ if (selection?.mode === "generic_loop") return displayValueLabel(locale, selection.workflowProfile ?? "generic-loop");
190
+ if (selection?.mode === "ambiguous") return t(locale, "noUniqueNextPr");
191
+ const item = selection?.item ?? fallback;
192
+ if (!item) return t(locale, "noUniqueNextPr");
193
+ return selection?.prNumber ? `${item.title} / #${selection.prNumber}` : item.title;
194
+ }
195
+
196
+ export function selectionDetail(selection: PrSelectionData | undefined, fallback: PlanItem | undefined, ambiguous: boolean, locale: EffectiveLocale): string {
197
+ if (selection?.mode === "generic_loop") return selection.evidence[0] ?? t(locale, "genericLoopNoPrSelection");
198
+ if (selection?.mode === "ambiguous") return selection.reason ?? "ambiguous_next_pr";
199
+ const item = selection?.item ?? fallback;
200
+ const evidence = selection?.evidence[0] ?? item?.whySelected;
201
+ const branch = selection?.branchName ? `${t(locale, "tableBranch")}: ${selection.branchName}` : undefined;
202
+ return [branch, evidence ?? (ambiguous ? "ambiguous_next_pr" : t(locale, "noNextPrMessage"))].filter(Boolean).join(" - ");
203
+ }
204
+
205
+ export function selectionStatus(selection: PrSelectionData | undefined, ambiguous: boolean, locale: EffectiveLocale): string {
206
+ if (selection?.mode === "generic_loop") return t(locale, "selected");
207
+ if (selection?.mode === "current_pr") return t(locale, "current");
208
+ if (selection?.mode === "next_spec") return t(locale, "selected");
209
+ return ambiguous || selection?.mode === "ambiguous" ? t(locale, "ambiguous") : t(locale, "selected");
210
+ }
211
+
212
+ export function selectionTone(selection: PrSelectionData | undefined, ambiguous: boolean): StatusTone {
213
+ if (selection?.mode === "generic_loop") return "green";
214
+ if (selection?.mode === "current_pr") return "green";
215
+ if (selection?.mode === "next_spec") return "blue";
216
+ return ambiguous || selection?.mode === "ambiguous" ? "yellow" : "green";
217
+ }
218
+
219
+ export function severityLabel(locale: EffectiveLocale, severity: LoopNotification["severity"]): string {
220
+ const keys: Record<LoopNotification["severity"], string> = {
221
+ blocked: "severityBlocked",
222
+ confirmation_required: "severityConfirmationRequired",
223
+ attention: "severityAttention",
224
+ informational: "severityInformational"
225
+ };
226
+ return t(locale, keys[severity]);
227
+ }
228
+
229
+ export function formatTime(value: string): string {
230
+ const date = new Date(value);
231
+ if (Number.isNaN(date.getTime())) return value;
232
+ return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
233
+ }
234
+
235
+ export function decodeBase64Preview(id: string, value: string, locale: EffectiveLocale = "en-US"): { id: string; text: string; truncated?: boolean } {
236
+ const maxChars = 500_000;
237
+ if (value.length > maxChars) {
238
+ return { id, text: t(locale, "artifactLarge"), truncated: true };
239
+ }
240
+ try {
241
+ const binary = atob(value);
242
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
243
+ return { id, text: new TextDecoder().decode(bytes) };
244
+ } catch {
245
+ return { id, text: t(locale, "artifactDecodeError") };
246
+ }
247
+ }
248
+
249
+ export function RawMessageDetails({ message, locale }: { message: string; locale: EffectiveLocale }): JSX.Element {
250
+ return (
251
+ <details className="raw-message-details">
252
+ <summary>{t(locale, "rawOriginalMessage")}</summary>
253
+ <pre>{message}</pre>
254
+ </details>
255
+ );
256
+ }
257
+
258
+ export function summarizeRawMessage(message: string | undefined, locale: EffectiveLocale, maxLength = 160): string | undefined {
259
+ if (!message) return undefined;
260
+ const singleLine = message.replace(/\s+/g, " ").trim();
261
+ if (singleLine.length <= maxLength) return singleLine;
262
+ return `${singleLine.slice(0, maxLength - 1).trimEnd()}${t(locale, "ellipsis")}`;
263
+ }
264
+
265
+ export function workerScopeSummary(workers: WorkerSummary[], locale: EffectiveLocale): string {
266
+ // Current-run data may arrive as either normalized activity or the legacy activityReason.
267
+ const current = workers.filter((worker) => worker.activity === "active" || worker.activityReason === "current_run").length;
268
+ const historical = workers.filter((worker) => worker.activity === "historical").length;
269
+ const stale = workers.filter((worker) => worker.activityReason === "stale_worker_failure").length;
270
+ return t(locale, "workerScopeSummary", {
271
+ current,
272
+ total: workers.length,
273
+ historical,
274
+ stale
275
+ });
276
+ }
@@ -0,0 +1,73 @@
1
+ import type { JSX } from "react";
2
+ import { useEffect, useState } from "react";
3
+ import type { AgentTimelineEntry, DashboardApi } from "../../api.js";
4
+ import { ErrorState } from "../../components/ErrorState.js";
5
+ import { t } from "../../i18n.js";
6
+ import {
7
+ TimelineEntries,
8
+ filterTimelinePreset,
9
+ timelinePresets,
10
+ type EffectiveLocale,
11
+ type TimelinePreset
12
+ } from "../CommandCenterParts.js";
13
+
14
+ export function AgentTimelineView({ api, runId, locale }: { api: DashboardApi; runId?: string; locale: EffectiveLocale }): JSX.Element {
15
+ const [entries, setEntries] = useState<AgentTimelineEntry[]>([]);
16
+ const [cursor, setCursor] = useState<string>();
17
+ const [error, setError] = useState<string>();
18
+ const [loading, setLoading] = useState(false);
19
+ const [preset, setPreset] = useState<TimelinePreset>("all");
20
+ const load = async (nextCursor?: string): Promise<void> => {
21
+ setLoading(true);
22
+ setError(undefined);
23
+ try {
24
+ const activePreset = timelinePresets.find((item) => item.id === preset);
25
+ const result = await api.agentTimeline({
26
+ limit: 50,
27
+ ...(runId ? { runId } : {}),
28
+ ...(activePreset?.sources ? { sources: activePreset.sources } : {}),
29
+ ...(nextCursor ? { cursor: nextCursor } : {})
30
+ });
31
+ if (!result.ok || !result.data) {
32
+ setError(result.error?.message ?? t(locale, "timelineLoadError"));
33
+ return;
34
+ }
35
+ const filtered = filterTimelinePreset(result.data.entries, preset);
36
+ setEntries((current) => nextCursor ? [...current, ...filtered] : filtered);
37
+ setCursor(result.data.nextCursor);
38
+ } finally {
39
+ setLoading(false);
40
+ }
41
+ };
42
+ useEffect(() => {
43
+ setEntries([]);
44
+ setCursor(undefined);
45
+ void load();
46
+ }, [api, locale, preset, runId]);
47
+ if (error) return <ErrorState title={t(locale, "timelineUnavailable")} message={error} />;
48
+ return (
49
+ <div className="two-stack">
50
+ <section className="focus-panel observability-console">
51
+ <div>
52
+ <p className="eyebrow">{t(locale, "observabilityConsole")}</p>
53
+ <h2>{t(locale, "observabilityTitle")}</h2>
54
+ <p>{t(locale, "observabilityMessage")}</p>
55
+ </div>
56
+ <div className="preset-tabs" role="tablist" aria-label={t(locale, "timelinePresets")}>
57
+ {timelinePresets.map((item) => (
58
+ <button
59
+ key={item.id}
60
+ className={item.id === preset ? "ghost-button is-active" : "ghost-button"}
61
+ type="button"
62
+ onClick={() => setPreset(item.id)}
63
+ >
64
+ {t(locale, item.labelKey)}
65
+ </button>
66
+ ))}
67
+ </div>
68
+ </section>
69
+ <TimelineEntries entries={entries} locale={locale} />
70
+ {cursor ? <button className="ghost-button" type="button" disabled={loading} onClick={() => void load(cursor)}>{loading ? t(locale, "actionReading") : t(locale, "actionLoadMore")}</button> : null}
71
+ </div>
72
+ );
73
+ }
@@ -0,0 +1,44 @@
1
+ import type { JSX } from "react";
2
+ import { useState } from "react";
3
+ import type { DashboardApi, MissionControlData } from "../../api.js";
4
+ import { ResponsiveTable } from "../../components/ResponsiveTable.js";
5
+ import { t } from "../../i18n.js";
6
+ import { decodeBase64Preview, formatTime, type ArtifactPreviewState, type EffectiveLocale } from "../CommandCenterParts.js";
7
+
8
+ export function ArtifactViewer({ api, data, preview, onPreview, locale }: { api: DashboardApi; data: MissionControlData; preview: ArtifactPreviewState | undefined; onPreview: (preview: ArtifactPreviewState) => void; locale: EffectiveLocale; }): JSX.Element {
9
+ const [readingArtifactId, setReadingArtifactId] = useState<string>();
10
+ const readArtifact = async (id: string): Promise<void> => {
11
+ setReadingArtifactId(id);
12
+ try {
13
+ const result = await api.artifact(id);
14
+ if (!result.ok || !result.data) {
15
+ onPreview({ id, text: "", error: result.error?.message ?? t(locale, "artifactReadError") });
16
+ return;
17
+ }
18
+ onPreview(decodeBase64Preview(id, result.data.contentBase64, locale));
19
+ } catch (error) {
20
+ onPreview({ id, text: "", error: error instanceof Error ? error.message : t(locale, "artifactReadError") });
21
+ } finally {
22
+ setReadingArtifactId(undefined);
23
+ }
24
+ };
25
+ return (
26
+ <div className="two-stack">
27
+ <ResponsiveTable
28
+ columns={[t(locale, "tableArtifact"), t(locale, "tableKind"), t(locale, "tablePath"), t(locale, "tableCreated"), t(locale, "tableAction")]}
29
+ rows={data.artifacts.map((artifact) => ({
30
+ key: artifact.id,
31
+ cells: [artifact.name, artifact.kind, artifact.path, formatTime(artifact.createdAt), <button className="ghost-button" disabled={readingArtifactId !== undefined} key={artifact.id} type="button" onClick={() => void readArtifact(artifact.id)}>{readingArtifactId === artifact.id ? t(locale, "actionReading") : t(locale, "actionRead")}</button>],
32
+ cardTitle: artifact.name,
33
+ cardMeta: `${artifact.kind} / ${formatTime(artifact.createdAt)}`,
34
+ cardSummary: artifact.path
35
+ }))}
36
+ empty={t(locale, "noArtifacts")}
37
+ />
38
+ <section className="artifact-preview" aria-label={t(locale, "artifactPreviewAria")}>
39
+ <span>{preview ? `Artifact ${preview.id.slice(0, 8)}${preview.truncated ? ` / ${t(locale, "truncated")}` : ""}` : t(locale, "noArtifactSelected")}</span>
40
+ <pre>{preview?.error ?? preview?.text ?? t(locale, "artifactSelectMessage")}</pre>
41
+ </section>
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,66 @@
1
+ import type { JSX } from "react";
2
+ import { useEffect, useState } from "react";
3
+ import type { DashboardApi, DryRunPreviewData } from "../../api.js";
4
+ import { CommandPreview } from "../../components/CommandPreview.js";
5
+ import { Collapsible } from "../../components/Collapsible.js";
6
+ import { EmptyState } from "../../components/EmptyState.js";
7
+ import { ErrorState } from "../../components/ErrorState.js";
8
+ import { List } from "../../components/List.js";
9
+ import { StatusBadge } from "../../components/StatusBadge.js";
10
+ import { displayValueLabel, t } from "../../i18n.js";
11
+ import {
12
+ WorkflowStages,
13
+ selectionDetail,
14
+ selectionEyebrow,
15
+ selectionStatus,
16
+ selectionTitle,
17
+ selectionTone,
18
+ type EffectiveLocale
19
+ } from "../CommandCenterParts.js";
20
+
21
+ export function DryRunPreview({ api, onAction, locale }: { api: DashboardApi; onAction: (path: string) => void; locale: EffectiveLocale }): JSX.Element {
22
+ const [preview, setPreview] = useState<DryRunPreviewData>();
23
+ const [error, setError] = useState<string>();
24
+ useEffect(() => {
25
+ void api.dryRunPreview().then((result) => {
26
+ if (result.ok && result.data) setPreview(result.data);
27
+ else setError(result.error?.message ?? t(locale, "dryRunLoadError"));
28
+ });
29
+ }, [api, locale]);
30
+ if (error) return <ErrorState title={t(locale, "dryRunUnavailable")} message={error} />;
31
+ if (!preview) return <EmptyState title={t(locale, "loadingPreview")} message={t(locale, "loadingPreviewMessage")} />;
32
+ const mergeForecast = preview.mergeForecast;
33
+ const forecastMessage = mergeForecast
34
+ ? (mergeForecast.ready ? t(locale, "mergeReady") : t(locale, "mergeMissing"))
35
+ : t(locale, "genericForecastReady");
36
+ const forecastStatus = mergeForecast ? displayValueLabel(locale, mergeForecast.state) : displayValueLabel(locale, preview.profile?.loopShape ?? "generic-loop");
37
+ return (
38
+ <div className="two-stack">
39
+ <section className="focus-panel">
40
+ <div>
41
+ <p className="eyebrow">{selectionEyebrow(preview.selection, locale)}</p>
42
+ <h2>{selectionTitle(preview.selection, preview.nextPr, locale)}</h2>
43
+ <p>{selectionDetail(preview.selection, preview.nextPr, false, locale)}</p>
44
+ </div>
45
+ <StatusBadge value={selectionStatus(preview.selection, false, locale)} tone={selectionTone(preview.selection, false)} />
46
+ </section>
47
+ <section className="focus-panel">
48
+ <div>
49
+ <p className="eyebrow">{t(locale, "forecastSummary")}</p>
50
+ <h2>{preview.autonomyForecast.summary}</h2>
51
+ <p>{forecastMessage}</p>
52
+ </div>
53
+ <StatusBadge value={forecastStatus} tone={mergeForecast?.ready === false ? "yellow" : "green"} />
54
+ </section>
55
+ <div className="button-row wide">
56
+ <button className="success-button" type="button" onClick={() => onAction("/api/run-until-gate")}>{t(locale, "actionStartRealRun")}</button>
57
+ <button className="ghost-button" type="button" onClick={() => onAction("/api/stop")}>{t(locale, "actionStopRun")}</button>
58
+ </div>
59
+ <Collapsible title={t(locale, "commandsPlanned")} chip={t(locale, "commandsChip", { count: preview.commandsPlanned.length })}><CommandPreview commands={preview.commandsPlanned} emptyMessage={t(locale, "noCommandsPlanned")} /></Collapsible>
60
+ <Collapsible title={t(locale, "workflowStages")} chip={t(locale, "stagesChip", { count: preview.workflowStages?.length ?? 0 })}><WorkflowStages stages={preview.workflowStages ?? []} locale={locale} /></Collapsible>
61
+ <Collapsible title={t(locale, "possibleGates")} chip={t(locale, "gatesChip", { count: preview.possibleGates.length })}><List items={preview.possibleGates} locale={locale} /></Collapsible>
62
+ <Collapsible title={t(locale, "missingConditions")} chip={t(locale, "missingCount", { count: preview.missingConditions.length })}><List items={preview.missingConditions} locale={locale} /></Collapsible>
63
+ <Collapsible title={t(locale, "filesLikelyTouched")} chip={t(locale, "pathsChip", { count: preview.filesLikelyTouched.length })}><List items={preview.filesLikelyTouched} locale={locale} /></Collapsible>
64
+ </div>
65
+ );
66
+ }
@@ -0,0 +1,17 @@
1
+ import type { JSX } from "react";
2
+ import type { EventSummary } from "../../api.js";
3
+ import { ResponsiveTable } from "../../components/ResponsiveTable.js";
4
+ import { t } from "../../i18n.js";
5
+ import { formatTime, type EffectiveLocale } from "../CommandCenterParts.js";
6
+
7
+ export function EventLedger({ events, locale }: { events: EventSummary[]; locale: EffectiveLocale }): JSX.Element {
8
+ return (
9
+ <div className="event-ledger-table">
10
+ <ResponsiveTable
11
+ columns={[t(locale, "tableSeq"), t(locale, "tableTime"), t(locale, "tableEvent"), t(locale, "tableDetails")]}
12
+ rows={events.map((event) => ({ key: event.id, cells: [String(event.seq), formatTime(event.createdAt), event.kind, event.message], cardTitle: event.kind, cardMeta: `${formatTime(event.createdAt)} / #${event.seq}`, cardSummary: event.message }))}
13
+ empty={t(locale, "noEvents")}
14
+ />
15
+ </div>
16
+ );
17
+ }
@@ -0,0 +1,34 @@
1
+ import type { JSX } from "react";
2
+ import type { MissionControlData } from "../../api.js";
3
+ import { ActivityBadge, activityReasonLabel } from "../../components/ActivityBadge.js";
4
+ import { ResponsiveTable } from "../../components/ResponsiveTable.js";
5
+ import { StatusBadge, toneForStatus } from "../../components/StatusBadge.js";
6
+ import { displayValueLabel, t } from "../../i18n.js";
7
+ import { RawMessageDetails, formatTime, summarizeRawMessage, type EffectiveLocale } from "../CommandCenterParts.js";
8
+
9
+ export function GateCenter({ data, locale }: { data: MissionControlData; locale: EffectiveLocale }): JSX.Element {
10
+ const active = data.gates.filter((gate) => gate.activity === "active").length;
11
+ const historical = data.gates.filter((gate) => gate.activity === "historical").length;
12
+ return (
13
+ <div className="two-stack">
14
+ <p className="scope-note">{t(locale, "gateScopeSummary", { active, historical, total: data.gates.length })}</p>
15
+ <ResponsiveTable
16
+ columns={[t(locale, "tableGate"), t(locale, "tableActivity"), t(locale, "tableStatus"), t(locale, "tableRawMessage"), t(locale, "tableOpened")]}
17
+ rows={data.gates.map((gate) => ({
18
+ key: gate.id,
19
+ cells: [
20
+ gate.kind,
21
+ <ActivityBadge key={`${gate.id}-activity`} activity={gate.activity} reason={gate.activityReason} locale={locale} />,
22
+ <StatusBadge key={gate.id} value={displayValueLabel(locale, gate.status)} tone={toneForStatus(gate.status)} />,
23
+ <RawMessageDetails key={`${gate.id}-message`} message={gate.message} locale={locale} />,
24
+ formatTime(gate.createdAt)
25
+ ],
26
+ cardTitle: gate.kind,
27
+ cardMeta: `${formatTime(gate.createdAt)} / ${activityReasonLabel(locale, gate.activityReason)}`,
28
+ cardSummary: summarizeRawMessage(gate.message, locale) ?? ""
29
+ }))}
30
+ empty={t(locale, "noGates")}
31
+ />
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,104 @@
1
+ import type { JSX } from "react";
2
+ import type { DashboardApi, MissionControlData, WorkflowDrillDownTarget } from "../../api.js";
3
+ import { StatusBadge } from "../../components/StatusBadge.js";
4
+ import { Collapsible } from "../../components/Collapsible.js";
5
+ import { List } from "../../components/List.js";
6
+ import { MetricRow } from "../../components/MetricRow.js";
7
+ import { displayValueLabel, t } from "../../i18n.js";
8
+ import {
9
+ PlanSelectionSummary,
10
+ ProfileDetails,
11
+ TimelineSummary,
12
+ selectionCompact,
13
+ workerScopeSummary,
14
+ workflowRoleShapeSummary,
15
+ type EffectiveLocale
16
+ } from "../CommandCenterParts.js";
17
+ import { EventLedger } from "../event-ledger/EventLedger.js";
18
+ import { WorkerRuns } from "../worker-runs/WorkerRuns.js";
19
+ import { WorkflowBoardView } from "./WorkflowBoard.js";
20
+
21
+ export function MissionControl({
22
+ data,
23
+ api,
24
+ stale,
25
+ locale,
26
+ onNavigate
27
+ }: {
28
+ data: MissionControlData;
29
+ api: DashboardApi;
30
+ stale: boolean;
31
+ locale: EffectiveLocale;
32
+ onNavigate?: (page: WorkflowDrillDownTarget["page"]) => void;
33
+ }): JSX.Element {
34
+ const historicalGates = data.gates.filter((gate) => gate.activity === "historical");
35
+ const workflowRefreshKey = [
36
+ data.current.run?.id ?? "no-run",
37
+ data.current.run?.status ?? data.current.status,
38
+ data.current.run?.currentState ?? "no-state",
39
+ data.current.run?.updatedAt ?? "no-update",
40
+ data.events[0]?.id ?? "no-event",
41
+ data.decisions?.[0]?.id ?? "no-decision"
42
+ ].join(":");
43
+ return (
44
+ <div className="mission-grid">
45
+ <section className="summary-panel summary-panel--mission">
46
+ <MetricRow label={t(locale, "metricNextAction")} value={data.current.nextAction} tone="yellow" />
47
+ <MetricRow label={t(locale, "metricAutonomyBoundary")} value={displayValueLabel(locale, data.autonomy?.autonomyMode ?? "autonomous_until_gate")} tone="blue" />
48
+ <MetricRow label={t(locale, "metricAttention")} value={t(locale, "metricItems", { count: data.notifications?.filter((item) => item.severity !== "informational").length ?? 0 })} tone="yellow" />
49
+ <MetricRow
50
+ label={data.profile?.loopShape === "generic-loop" ? t(locale, "deliverableReadiness") : t(locale, "metricMergeReadiness")}
51
+ value={data.profile?.loopShape === "generic-loop" ? displayValueLabel(locale, data.profile.expectedDeliverable ?? data.profile.loopShape) : displayValueLabel(locale, data.mergeReadiness?.state ?? "manual")}
52
+ tone={data.profile?.loopShape === "generic-loop" || data.mergeReadiness?.ready ? "green" : "yellow"}
53
+ />
54
+ <MetricRow label={t(locale, "metricWorkflow")} value={workflowRoleShapeSummary(data, locale)} tone="blue" />
55
+ </section>
56
+ <WorkflowBoardView
57
+ api={api}
58
+ runId={data.current.run?.id}
59
+ refreshKey={workflowRefreshKey}
60
+ locale={locale}
61
+ onEvidenceAppended={() => undefined}
62
+ {...(onNavigate ? { onNavigate } : {})}
63
+ />
64
+ <section className="focus-panel">
65
+ <div>
66
+ <p className="eyebrow">{t(locale, "autonomyPosture")}</p>
67
+ <h2>{data.autonomy?.summary ?? t(locale, "autonomyFallback")}</h2>
68
+ </div>
69
+ <StatusBadge value={stale ? t(locale, "stale") : t(locale, "live")} tone={stale ? "yellow" : "green"} />
70
+ </section>
71
+ <Collapsible title={t(locale, "policyDetails")} chip={t(locale, "notifyRules", { count: data.autonomy?.notifyWhen.length ?? 0 })}>
72
+ <List items={[...(data.autonomy?.notifyWhen ?? []), ...(data.autonomy?.requiresConfirmation ?? [])]} locale={locale} />
73
+ </Collapsible>
74
+ <Collapsible title={t(locale, "workflowProfile")} chip={data.profile?.loopShape ?? "pr-loop"}>
75
+ <ProfileDetails data={data} locale={locale} />
76
+ </Collapsible>
77
+ <Collapsible title={t(locale, "selectedNextPr")} chip={selectionCompact(data.selection, data.plan?.selectedNext, locale)}>
78
+ <PlanSelectionSummary data={data.plan} selection={data.selection} locale={locale} />
79
+ </Collapsible>
80
+ {data.profile?.loopShape === "generic-loop" ? null : (
81
+ <Collapsible title={t(locale, "mergeEvidence")} chip={data.mergeReadiness?.ready ? t(locale, "allEvidenceReady") : t(locale, "missingCount", { count: data.mergeReadiness?.missingConditions.length ?? 0 })}>
82
+ <List items={[...(data.mergeReadiness?.evidence ?? []), ...(data.mergeReadiness?.missingConditions ?? [])]} locale={locale} />
83
+ </Collapsible>
84
+ )}
85
+ {historicalGates.length ? (
86
+ <Collapsible title={t(locale, "historicalGates")} chip={t(locale, "gatesChip", { count: historicalGates.length })}>
87
+ <List items={historicalGates.map((gate) => `${gate.kind}: ${gate.message}`)} locale={locale} />
88
+ </Collapsible>
89
+ ) : null}
90
+ <Collapsible title={t(locale, "timelineLatest")} chip={data.timelineSummary?.hasObservationGap ? t(locale, "timelineGap") : t(locale, "timelineNoGap")}>
91
+ <TimelineSummary data={data} locale={locale} />
92
+ </Collapsible>
93
+ <Collapsible title={t(locale, "workerRunsRepoScope")} chip={workerScopeSummary(data.workers, locale)}>
94
+ <>
95
+ <p className="scope-note">{t(locale, "workerScopePreview", { shown: Math.min(data.workers.length, 5), total: data.workers.length })}</p>
96
+ <WorkerRuns workers={data.workers.slice(0, 5)} api={api} locale={locale} hideScopeNote />
97
+ </>
98
+ </Collapsible>
99
+ <Collapsible title={t(locale, "pageEvents")} chip={t(locale, "metricItems", { count: data.events.length })}>
100
+ <EventLedger events={data.events.slice(0, 8)} locale={locale} />
101
+ </Collapsible>
102
+ </div>
103
+ );
104
+ }