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,577 @@
1
+ import { AlertTriangle, CheckCircle2, ChevronLeft, ChevronRight, Info, Link2, PlusCircle, X } from "lucide-react";
2
+ import type { CSSProperties, JSX } from "react";
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import type {
5
+ DashboardApi,
6
+ WorkflowBoard,
7
+ WorkflowBoardStage,
8
+ WorkflowCheckRow,
9
+ WorkflowDrillDownTarget,
10
+ WorkflowEvidenceRef,
11
+ WorkflowReviewReportRow,
12
+ WorkflowStageId,
13
+ WorkflowStageStatus
14
+ } from "../../api.js";
15
+ import { StatusBadge, type StatusTone } from "../../components/StatusBadge.js";
16
+ import { displayValueLabel, t } from "../../i18n.js";
17
+ import { formatTime, type EffectiveLocale } from "../CommandCenterParts.js";
18
+
19
+ interface WorkflowBoardViewProps {
20
+ api: DashboardApi;
21
+ runId?: string | undefined;
22
+ refreshKey?: string | undefined;
23
+ locale: EffectiveLocale;
24
+ onEvidenceAppended: () => void;
25
+ onNavigate?: (page: WorkflowDrillDownTarget["page"]) => void;
26
+ }
27
+
28
+ export function WorkflowBoardView({ api, runId, refreshKey, locale, onEvidenceAppended, onNavigate }: WorkflowBoardViewProps): JSX.Element {
29
+ const [board, setBoard] = useState<WorkflowBoard>();
30
+ const [error, setError] = useState<string>();
31
+ const [selectedStageId, setSelectedStageId] = useState<WorkflowStageId>("work_item");
32
+ const [collapsed, setCollapsed] = useState(false);
33
+ const [peekStageId, setPeekStageId] = useState<WorkflowStageId>();
34
+ const [peekPosition, setPeekPosition] = useState<{ left: number; top: number }>();
35
+ const [summary, setSummary] = useState("");
36
+ const [appending, setAppending] = useState(false);
37
+ const [appendMessage, setAppendMessage] = useState<string>();
38
+ const boardRef = useRef<HTMLElement>(null);
39
+ const railRef = useRef<HTMLDivElement>(null);
40
+ const peekCloseTimer = useRef<number | undefined>(undefined);
41
+ const manualStageSelection = useRef(false);
42
+ const boardRequestSeq = useRef(0);
43
+
44
+ useEffect(() => {
45
+ manualStageSelection.current = false;
46
+ }, [runId]);
47
+
48
+ useEffect(() => {
49
+ let cancelled = false;
50
+ const requestSeq = boardRequestSeq.current + 1;
51
+ boardRequestSeq.current = requestSeq;
52
+ if (!api.workflowBoard) {
53
+ setError(t(locale, "workflowBoardLoadError"));
54
+ return () => {
55
+ cancelled = true;
56
+ };
57
+ }
58
+ void api.workflowBoard(runId ? { runId } : undefined).then((result) => {
59
+ if (cancelled || requestSeq !== boardRequestSeq.current) return;
60
+ if (!result.ok || !result.data) {
61
+ setError(result.error?.message ?? t(locale, "workflowBoardLoadError"));
62
+ return;
63
+ }
64
+ if (!Array.isArray(result.data.stages)) {
65
+ setError(t(locale, "workflowBoardLoadError"));
66
+ return;
67
+ }
68
+ const nextBoard = result.data;
69
+ setBoard(nextBoard);
70
+ setSelectedStageId((current) => (
71
+ manualStageSelection.current && nextBoard.stages.some((stage) => stage.id === current)
72
+ ? current
73
+ : nextBoard.activeStageId ?? nextBoard.selectedStageId
74
+ ));
75
+ setError(undefined);
76
+ });
77
+ return () => {
78
+ cancelled = true;
79
+ };
80
+ }, [api, locale, refreshKey, runId]);
81
+
82
+ useEffect(() => {
83
+ const close = (event: KeyboardEvent): void => {
84
+ if (event.key === "Escape") setPeekStageId(undefined);
85
+ };
86
+ window.addEventListener("keydown", close);
87
+ return () => {
88
+ window.removeEventListener("keydown", close);
89
+ if (peekCloseTimer.current !== undefined) window.clearTimeout(peekCloseTimer.current);
90
+ };
91
+ }, []);
92
+
93
+ const selectedStage = useMemo(
94
+ () => board?.stages.find((stage) => stage.id === selectedStageId) ?? board?.stages[0],
95
+ [board, selectedStageId]
96
+ );
97
+ const peekStage = board?.stages.find((stage) => stage.id === peekStageId);
98
+
99
+ const openPeek = (stageId: WorkflowStageId, target?: HTMLElement): void => {
100
+ if (peekCloseTimer.current !== undefined) window.clearTimeout(peekCloseTimer.current);
101
+ if (target && boardRef.current) {
102
+ const stageRect = target.getBoundingClientRect();
103
+ const boardRect = boardRef.current.getBoundingClientRect();
104
+ const minLeft = 180;
105
+ const maxLeft = Math.max(minLeft, boardRect.width - minLeft);
106
+ const centeredLeft = stageRect.left - boardRect.left + stageRect.width / 2;
107
+ setPeekPosition({
108
+ left: Math.min(Math.max(centeredLeft, minLeft), maxLeft),
109
+ top: stageRect.top - boardRect.top - 12
110
+ });
111
+ }
112
+ setPeekStageId(stageId);
113
+ };
114
+ const schedulePeekClose = (): void => {
115
+ if (peekCloseTimer.current !== undefined) window.clearTimeout(peekCloseTimer.current);
116
+ peekCloseTimer.current = window.setTimeout(() => setPeekStageId(undefined), 140);
117
+ };
118
+
119
+ useEffect(() => {
120
+ const selected = railRef.current?.querySelector<HTMLElement>(`[data-stage-id="${selectedStageId}"]`);
121
+ if (typeof selected?.scrollIntoView === "function") {
122
+ selected.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
123
+ }
124
+ }, [selectedStageId, board?.runId]);
125
+
126
+ const appendEvidence = async (): Promise<void> => {
127
+ if (!board || !selectedStage || summary.trim().length === 0) return;
128
+ if (!api.appendWorkflowEvidence) {
129
+ setAppendMessage(t(locale, "workflowEvidenceAppendFailed"));
130
+ return;
131
+ }
132
+ setAppending(true);
133
+ setAppendMessage(undefined);
134
+ try {
135
+ const result = await api.appendWorkflowEvidence({
136
+ runId: board.runId,
137
+ stageId: selectedStage.id,
138
+ summary: summary.trim(),
139
+ source: "dashboard",
140
+ actor: "codex",
141
+ status: "done"
142
+ });
143
+ if (result.ok) {
144
+ setSummary("");
145
+ setAppendMessage(t(locale, "workflowEvidenceAppended"));
146
+ onEvidenceAppended();
147
+ const refreshed = await api.workflowBoard?.(board.runId ? { runId: board.runId } : undefined);
148
+ if (refreshed?.ok && refreshed.data) setBoard(refreshed.data);
149
+ return;
150
+ }
151
+ setAppendMessage(result.error?.message ?? t(locale, "workflowEvidenceAppendFailed"));
152
+ } finally {
153
+ setAppending(false);
154
+ }
155
+ };
156
+
157
+ if (error) {
158
+ return <section className="workflow-board"><h2>{t(locale, "workflowBoardTitle")}</h2><p className="muted-copy">{error}</p></section>;
159
+ }
160
+ if (!board || !selectedStage) {
161
+ return <section className="workflow-board"><h2>{t(locale, "workflowBoardTitle")}</h2><p className="muted-copy">{t(locale, "loadingPreviewMessage")}</p></section>;
162
+ }
163
+
164
+ return (
165
+ <section className="workflow-board" aria-label={t(locale, "workflowBoardTitle")} ref={boardRef}>
166
+ <div className="workflow-board__summary">
167
+ <div>
168
+ <p className="eyebrow">{t(locale, "workflowBoardTitle")}</p>
169
+ <h2>{workItemTitle(board.workItem, t(locale, "workflowNoRun"))}</h2>
170
+ <p>{board.message ?? t(locale, "workflowBoardSubtitle")}</p>
171
+ </div>
172
+ <div className="workflow-board__facts">
173
+ <Fact label={t(locale, "runId")} value={board.workItem.runId ?? t(locale, "none")} />
174
+ <Fact label={t(locale, "workflowStage")} value={selectedStage.label} />
175
+ <Fact label={t(locale, "workflowStageSource")} value={stageSourceLabel(locale, board.stageSource)} />
176
+ <Fact label={t(locale, "workflowRunState")} value={board.workItem.currentState ?? t(locale, "none")} />
177
+ <Fact label={t(locale, "workflowHookCapture")} value={hookCaptureLabel(locale, board.hookCapture)} />
178
+ {board.workItem.prNumber ? <Fact label="PR" value={`#${board.workItem.prNumber}`} /> : null}
179
+ <Fact label={t(locale, "tableStatus")} value={statusLabel(locale, selectedStage.status)} />
180
+ <Fact label={t(locale, "updated")} value={board.workItem.lastUpdate ? formatTime(board.workItem.lastUpdate) : t(locale, "notStarted")} />
181
+ {board.stageSourceEvent ? <Fact label={t(locale, "workflowStageEvidence")} value={`${board.stageSourceEvent.id.slice(0, 8)} ${formatTime(board.stageSourceEvent.createdAt)}`} /> : null}
182
+ </div>
183
+ </div>
184
+
185
+ <div className="workflow-rail" aria-label={t(locale, "workflowRail")} ref={railRef}>
186
+ {board.stages.map((stage, index) => (
187
+ <button
188
+ className={`workflow-rail__stage workflow-rail__stage--${stage.status}${stage.id === selectedStage.id ? " is-selected" : ""}`}
189
+ key={stage.id}
190
+ data-stage-id={stage.id}
191
+ type="button"
192
+ onClick={(event) => {
193
+ manualStageSelection.current = true;
194
+ setSelectedStageId(stage.id);
195
+ openPeek(stage.id, event.currentTarget);
196
+ }}
197
+ onFocus={(event) => openPeek(stage.id, event.currentTarget)}
198
+ onBlur={schedulePeekClose}
199
+ onMouseEnter={(event) => openPeek(stage.id, event.currentTarget)}
200
+ onMouseLeave={schedulePeekClose}
201
+ onPointerEnter={(event) => openPeek(stage.id, event.currentTarget)}
202
+ onPointerLeave={schedulePeekClose}
203
+ >
204
+ <span className="workflow-rail__node" aria-hidden="true">{stage.status === "done" ? <CheckCircle2 size={15} /> : index + 1}</span>
205
+ <span className="workflow-rail__label">{stage.label}</span>
206
+ <span className="workflow-rail__meta">
207
+ <StatusBadge value={statusLabel(locale, stage.status)} tone={toneForWorkflowStatus(stage.status)} />
208
+ <small>{evidenceTotal(stage.evidenceCounts)} {t(locale, "workflowEvidenceShort")}</small>
209
+ </span>
210
+ </button>
211
+ ))}
212
+ </div>
213
+
214
+ {peekStage ? (
215
+ <div
216
+ className="workflow-peek"
217
+ role="dialog"
218
+ aria-label={peekStage.label}
219
+ style={peekPosition ? ({ "--workflow-peek-left": `${peekPosition.left}px`, "--workflow-peek-top": `${peekPosition.top}px` } as CSSProperties) : undefined}
220
+ onMouseEnter={() => openPeek(peekStage.id)}
221
+ onMouseLeave={schedulePeekClose}
222
+ >
223
+ <button className="icon-button" type="button" onClick={() => setPeekStageId(undefined)} aria-label={t(locale, "actionClose")}><X size={16} /></button>
224
+ <h3>{peekStage.label}</h3>
225
+ <p>{peekStage.nextAction}</p>
226
+ <EvidenceList refs={stageEvidence(board, peekStage)} locale={locale} compact {...(onNavigate ? { onNavigate } : {})} />
227
+ </div>
228
+ ) : null}
229
+
230
+ <div className="workflow-board__body">
231
+ <aside className={collapsed ? "workflow-inspector is-collapsed" : "workflow-inspector"}>
232
+ <div className="workflow-inspector__control">
233
+ <button
234
+ className="icon-button workflow-inspector__toggle"
235
+ type="button"
236
+ onClick={() => setCollapsed(!collapsed)}
237
+ aria-label={collapsed ? t(locale, "workflowInspectorOpen") : t(locale, "workflowInspectorCollapse")}
238
+ title={collapsed ? t(locale, "workflowInspectorOpen") : t(locale, "workflowInspectorCollapse")}
239
+ >
240
+ {collapsed ? <ChevronRight size={15} /> : <ChevronLeft size={15} />}
241
+ </button>
242
+ </div>
243
+ {collapsed ? (
244
+ <div className="workflow-inspector__collapsed" aria-label={selectedStage.label} title={selectedStage.label}>
245
+ <span className={`workflow-inspector__dot workflow-inspector__dot--${selectedStage.status}`} aria-hidden="true" />
246
+ </div>
247
+ ) : (
248
+ <div className="workflow-inspector__stage">
249
+ <div className="workflow-inspector__heading">
250
+ <div>
251
+ <strong>{selectedStage.label}</strong>
252
+ </div>
253
+ <StatusBadge value={statusLabel(locale, selectedStage.status)} tone={toneForWorkflowStatus(selectedStage.status)} />
254
+ </div>
255
+ <p className="workflow-inspector__hint">{selectedStage.latestAction?.label ?? selectedStage.nextAction}</p>
256
+ <ul>
257
+ {selectedStage.substages.map((substage) => (
258
+ <li key={substage.id}>
259
+ <span>{substage.label}</span>
260
+ <span className="workflow-inspector__submeta">
261
+ <StatusBadge value={statusLabel(locale, substage.status)} tone={toneForWorkflowStatus(substage.status)} />
262
+ <small>{evidenceTotal(substage.evidenceCounts)} {t(locale, "workflowEvidenceShort")}</small>
263
+ </span>
264
+ </li>
265
+ ))}
266
+ </ul>
267
+ </div>
268
+ )}
269
+ </aside>
270
+
271
+ <main className="workflow-detail">
272
+ <div className="workflow-detail__header">
273
+ <div>
274
+ <p className="eyebrow">{t(locale, "workflowCurrentStage")}</p>
275
+ <h3>{selectedStage.label}</h3>
276
+ <p>{selectedStage.nextAction}</p>
277
+ </div>
278
+ <div className="workflow-detail__side">
279
+ <StatusBadge value={statusLabel(locale, selectedStage.status)} tone={toneForWorkflowStatus(selectedStage.status)} />
280
+ <div className="actor-chip-row">
281
+ {selectedStage.actorChips.map((actor) => <span className="actor-chip" key={`${selectedStage.id}-${actor.actor}`}>{actor.label}<small>{statusLabel(locale, actor.status)}</small></span>)}
282
+ </div>
283
+ </div>
284
+ </div>
285
+
286
+ {selectedStage.blockers.length ? (
287
+ <div className="workflow-blocker">
288
+ <AlertTriangle size={18} />
289
+ <div>
290
+ <strong>{selectedStage.blockers[0]?.title}</strong>
291
+ <p>{selectedStage.blockers[0]?.reason}</p>
292
+ <dl>
293
+ <dt>{t(locale, "owner")}</dt>
294
+ <dd>{selectedStage.blockers[0]?.owner}</dd>
295
+ <dt>{t(locale, "workflowNextAction")}</dt>
296
+ <dd>{selectedStage.blockers[0]?.nextAction}</dd>
297
+ </dl>
298
+ </div>
299
+ </div>
300
+ ) : null}
301
+
302
+ <StageSpecificDetail board={board} stage={selectedStage} locale={locale} />
303
+
304
+ {board.appendEvidenceEnabled ? (
305
+ <div className="workflow-append">
306
+ <label>
307
+ {t(locale, "workflowAttachEvidence")}
308
+ <textarea value={summary} maxLength={280} onChange={(event) => setSummary(event.target.value)} placeholder={t(locale, "workflowEvidencePlaceholder")} />
309
+ </label>
310
+ <div className="button-row">
311
+ <button type="button" disabled={appending || summary.trim().length === 0} onClick={() => void appendEvidence()}>
312
+ <PlusCircle size={16} /> {t(locale, "workflowAttachEvidence")}
313
+ </button>
314
+ {appendMessage ? <span className="action-message">{appendMessage}</span> : null}
315
+ </div>
316
+ </div>
317
+ ) : null}
318
+
319
+ <div className="workflow-evidence">
320
+ <h4>{t(locale, "workflowEvidence")}</h4>
321
+ <EvidenceList refs={stageEvidence(board, selectedStage)} locale={locale} {...(onNavigate ? { onNavigate } : {})} />
322
+ </div>
323
+ </main>
324
+ </div>
325
+ </section>
326
+ );
327
+ }
328
+
329
+ function StageSpecificDetail({ board, stage, locale }: { board: WorkflowBoard; stage: WorkflowBoardStage; locale: EffectiveLocale }): JSX.Element {
330
+ if (stage.id === "verify") return <CheckTable title={t(locale, "workflowVerifyMatrix")} rows={board.verificationChecks} locale={locale} />;
331
+ if (stage.id === "review") return <ReviewMatrix rows={board.reviewReports} locale={locale} />;
332
+ if (stage.id === "merge_readiness") {
333
+ return (
334
+ <CheckTable
335
+ title={t(locale, "workflowMergeChecklist")}
336
+ rows={board.mergeReadinessChecks}
337
+ locale={locale}
338
+ note={t(locale, "workflowGithubMergeNote")}
339
+ action={board.workItem.prUrl ? { href: board.workItem.prUrl, label: t(locale, "workflowOpenGithubPr") } : undefined}
340
+ />
341
+ );
342
+ }
343
+ if (stage.id === "cleanup") return <CheckTable title={t(locale, "workflowCleanupChecklist")} rows={board.cleanupChecks} locale={locale} />;
344
+ if (stage.id === "pr") {
345
+ return (
346
+ <CheckTable
347
+ title={t(locale, "workflowPrChecklist")}
348
+ rows={[
349
+ { id: "pr-opened", label: t(locale, "workflowPrOpened"), status: board.workItem.prUrl ? "passed" : "pending", evidence: board.workItem.prUrl ?? t(locale, "notLinked"), owner: "GitHub" }
350
+ ]}
351
+ locale={locale}
352
+ note={t(locale, "workflowGithubPrNote")}
353
+ action={board.workItem.prUrl ? { href: board.workItem.prUrl, label: t(locale, "workflowOpenGithubPr") } : undefined}
354
+ />
355
+ );
356
+ }
357
+ return (
358
+ <div className="workflow-stage-card">
359
+ <h4>{t(locale, "workflowStageSummary")}</h4>
360
+ <p>{stage.latestAction?.label ?? stage.nextAction}</p>
361
+ </div>
362
+ );
363
+ }
364
+
365
+ function CheckTable({
366
+ title,
367
+ rows,
368
+ locale,
369
+ note,
370
+ action
371
+ }: {
372
+ title: string;
373
+ rows: WorkflowCheckRow[];
374
+ locale: EffectiveLocale;
375
+ note?: string | undefined;
376
+ action?: { href: string; label: string } | undefined;
377
+ }): JSX.Element {
378
+ return (
379
+ <div className="workflow-matrix">
380
+ <h4>{title}</h4>
381
+ {note ? (
382
+ <p className="workflow-matrix__note">
383
+ <span>{note}</span>
384
+ {action ? <a href={action.href} target="_blank" rel="noreferrer">{action.label}</a> : null}
385
+ </p>
386
+ ) : null}
387
+ <div className="workflow-matrix__rows">
388
+ {rows.length === 0 ? <p className="muted-copy">{t(locale, "noneList")}</p> : rows.map((row) => (
389
+ <div className="workflow-matrix__row" key={row.id}>
390
+ <span>{row.label}</span>
391
+ <StatusBadge value={displayValueLabel(locale, row.status)} tone={toneForCheck(row.status)} />
392
+ <span>{row.evidence}</span>
393
+ <small>{row.owner}</small>
394
+ </div>
395
+ ))}
396
+ </div>
397
+ </div>
398
+ );
399
+ }
400
+
401
+ function ReviewMatrix({ rows, locale }: { rows: WorkflowReviewReportRow[]; locale: EffectiveLocale }): JSX.Element {
402
+ return (
403
+ <div className="workflow-matrix workflow-matrix--review">
404
+ <h4>{t(locale, "workflowReviewMatrix")}</h4>
405
+ <div className="workflow-matrix__rows">
406
+ <div className="workflow-matrix__row workflow-matrix__row--header workflow-matrix__row--review">
407
+ <span>{t(locale, "workflowReviewer")}</span>
408
+ <span>{t(locale, "workflowRequirement")}</span>
409
+ <span>{t(locale, "workflowProgress")}</span>
410
+ <span>{t(locale, "workflowResult")}</span>
411
+ <span>{t(locale, "workflowEvidence")}</span>
412
+ </div>
413
+ {rows.map((row) => (
414
+ <div className="workflow-matrix__row workflow-matrix__row--review" key={row.id}>
415
+ <span>{row.agent}</span>
416
+ <StatusBadge value={displayValueLabel(locale, row.requirement ?? "unknown")} tone={toneForRequirement(row.requirement ?? "unknown")} />
417
+ <StatusBadge value={displayValueLabel(locale, row.progress ?? "unknown")} tone={toneForProgress(row.progress ?? "unknown", row.status)} />
418
+ <span>
419
+ <StatusBadge value={displayValueLabel(locale, row.result ?? row.status)} tone={toneForReview(row.status)} />
420
+ <small>{row.severitySummary}</small>
421
+ </span>
422
+ <span>
423
+ {row.commentUrl ? <a href={row.commentUrl} target="_blank" rel="noopener noreferrer">{displayValueLabel(locale, row.prComment)}</a> : <strong>{displayValueLabel(locale, row.prComment)}</strong>}
424
+ <small>{reviewEvidenceSummary(row, locale)}</small>
425
+ </span>
426
+ </div>
427
+ ))}
428
+ </div>
429
+ </div>
430
+ );
431
+ }
432
+
433
+ function reviewEvidenceSummary(row: WorkflowReviewReportRow, locale: EffectiveLocale): string {
434
+ const refs = [
435
+ row.model ? `model: ${row.model}` : undefined,
436
+ row.sessionId ? `session: ${row.sessionId}` : undefined,
437
+ row.conversationId ? `conversation: ${row.conversationId}` : undefined,
438
+ row.commentId ? `comment: ${row.commentId}` : undefined
439
+ ].filter((item): item is string => Boolean(item));
440
+ return refs.length > 0 ? refs.join(" / ") : row.nextAction ?? row.reason ?? row.followUp ?? t(locale, "none");
441
+ }
442
+
443
+ function EvidenceList({
444
+ refs,
445
+ locale,
446
+ compact = false,
447
+ onNavigate
448
+ }: {
449
+ refs: WorkflowEvidenceRef[];
450
+ locale: EffectiveLocale;
451
+ compact?: boolean;
452
+ onNavigate?: (page: WorkflowDrillDownTarget["page"]) => void;
453
+ }): JSX.Element {
454
+ const [preview, setPreview] = useState<WorkflowEvidenceRef>();
455
+ if (refs.length === 0) return <p className="muted-copy">{t(locale, "workflowNoEvidence")}</p>;
456
+ return (
457
+ <div className={compact ? "workflow-evidence-list workflow-evidence-list--compact" : "workflow-evidence-list"}>
458
+ {refs.slice(0, compact ? 4 : 8).map((ref) => (
459
+ <button
460
+ className={`workflow-evidence-chip workflow-evidence-chip--${ref.interaction}`}
461
+ key={ref.id}
462
+ title={ref.summary}
463
+ type="button"
464
+ onClick={() => {
465
+ if (ref.interaction === "drill_down_link" && ref.drillDownTarget && onNavigate) {
466
+ onNavigate(ref.drillDownTarget.page);
467
+ return;
468
+ }
469
+ setPreview(preview?.id === ref.id ? undefined : ref);
470
+ }}
471
+ >
472
+ {ref.interaction === "drill_down_link" ? <Link2 size={14} /> : <Info size={14} />}
473
+ <strong>{ref.label}</strong>
474
+ <small>{ref.summary}</small>
475
+ </button>
476
+ ))}
477
+ {preview ? (
478
+ <div className="workflow-evidence-preview" role="dialog" aria-label={preview.label}>
479
+ <button className="icon-button" type="button" onClick={() => setPreview(undefined)} aria-label={t(locale, "actionClose")}><X size={14} /></button>
480
+ <strong>{preview.label}</strong>
481
+ <p>{preview.summary}</p>
482
+ <dl>
483
+ <dt>{t(locale, "tableKind")}</dt>
484
+ <dd>{preview.kind}</dd>
485
+ <dt>{t(locale, "workflowEvidenceTarget")}</dt>
486
+ <dd>{preview.drillDownTarget?.page ?? preview.interaction}</dd>
487
+ </dl>
488
+ </div>
489
+ ) : null}
490
+ </div>
491
+ );
492
+ }
493
+
494
+ function Fact({ label, value }: { label: string; value: string }): JSX.Element {
495
+ return <div><span>{label}</span><strong>{value}</strong></div>;
496
+ }
497
+
498
+ function stageEvidence(board: WorkflowBoard, stage: WorkflowBoardStage): WorkflowEvidenceRef[] {
499
+ return board.evidenceRefs.filter((ref) => ref.source === stage.id || ref.id.startsWith(`${stage.id}:`));
500
+ }
501
+
502
+ function evidenceTotal(counts: { events: number; artifacts: number; gates: number; prComments: number; gitnexus: number; browser: number; ci: number; reports: number }): number {
503
+ return counts.events + counts.artifacts + counts.gates + counts.prComments + counts.gitnexus + counts.browser + counts.ci + counts.reports;
504
+ }
505
+
506
+ function workItemTitle(workItem: {
507
+ issueNumber?: number | undefined;
508
+ issueTitle?: string | undefined;
509
+ prNumber?: number | undefined;
510
+ branch?: string | undefined;
511
+ runId?: string | undefined;
512
+ }, fallback: string): string {
513
+ if (workItem.issueNumber && workItem.issueTitle) return `#${workItem.issueNumber} ${workItem.issueTitle}`;
514
+ if (workItem.issueNumber) return `#${workItem.issueNumber}`;
515
+ if (workItem.prNumber) return `PR #${workItem.prNumber}`;
516
+ return workItem.branch ?? workItem.runId ?? fallback;
517
+ }
518
+
519
+ function statusLabel(locale: EffectiveLocale, status: WorkflowStageStatus): string {
520
+ return displayValueLabel(locale, status);
521
+ }
522
+
523
+ function stageSourceLabel(locale: EffectiveLocale, source: WorkflowBoard["stageSource"]): string {
524
+ return displayValueLabel(locale, source ?? "unknown");
525
+ }
526
+
527
+ function hookCaptureLabel(locale: EffectiveLocale, capture: WorkflowBoard["hookCapture"]): string {
528
+ if (!capture) return t(locale, "unknown");
529
+ const reasons: Record<string, { "en-US": string; "zh-CN": string }> = {
530
+ captured: { "en-US": "recent hook event observed", "zh-CN": "最近有 hook 事件" },
531
+ not_seen: { "en-US": "current session not observed", "zh-CN": "当前线程未观察到" },
532
+ stale: { "en-US": "last hook event is stale", "zh-CN": "最近未观察到" },
533
+ ambiguous: { "en-US": "multiple bindings match", "zh-CN": "绑定不唯一" },
534
+ unavailable: { "en-US": "no usable binding", "zh-CN": "无可用绑定" }
535
+ };
536
+ return `${displayValueLabel(locale, capture.status)} - ${reasons[capture.status]?.[locale] ?? capture.reason}`;
537
+ }
538
+
539
+ function toneForWorkflowStatus(status: WorkflowStageStatus): StatusTone {
540
+ if (status === "done") return "green";
541
+ if (status === "blocked" || status === "failed") return "red";
542
+ if (status === "active" || status === "manual") return "yellow";
543
+ if (status === "skipped") return "blue";
544
+ return "muted";
545
+ }
546
+
547
+ function toneForCheck(status: WorkflowCheckRow["status"]): StatusTone {
548
+ if (status === "passed") return "green";
549
+ if (status === "failed" || status === "blocked") return "red";
550
+ if (status === "pending") return "yellow";
551
+ if (status === "skipped") return "blue";
552
+ return "muted";
553
+ }
554
+
555
+ function toneForReview(status: WorkflowReviewReportRow["status"]): StatusTone {
556
+ if (status === "pass") return "green";
557
+ if (status === "block") return "red";
558
+ if (status === "warn") return "yellow";
559
+ if (status === "pending") return "yellow";
560
+ if (status === "skipped") return "blue";
561
+ return "muted";
562
+ }
563
+
564
+ function toneForRequirement(requirement: NonNullable<WorkflowReviewReportRow["requirement"]>): StatusTone {
565
+ if (requirement === "required") return "yellow";
566
+ if (requirement === "not_required") return "blue";
567
+ if (requirement === "optional") return "muted";
568
+ return "muted";
569
+ }
570
+
571
+ function toneForProgress(progress: NonNullable<WorkflowReviewReportRow["progress"]>, status: WorkflowReviewReportRow["status"]): StatusTone {
572
+ if (progress === "complete") return status === "block" ? "red" : "green";
573
+ if (progress === "incomplete") return "red";
574
+ if (progress === "requested" || progress === "started" || progress === "in_progress") return "yellow";
575
+ if (progress === "skipped") return "blue";
576
+ return "muted";
577
+ }
@@ -0,0 +1,30 @@
1
+ import type { JSX } from "react";
2
+ import type { DashboardApi, LoopNotification } from "../../api.js";
3
+ import { Collapsible } from "../../components/Collapsible.js";
4
+ import { t } from "../../i18n.js";
5
+ import { NotificationList, severityLabel, type EffectiveLocale } from "../CommandCenterParts.js";
6
+
7
+ export function NotificationsView({ notifications, api, onRefresh, locale }: { notifications: LoopNotification[]; api: DashboardApi; onRefresh: () => void | Promise<void>; locale: EffectiveLocale }): JSX.Element {
8
+ const groups = ["blocked", "confirmation_required", "attention", "informational"] as const;
9
+ const markRead = async (): Promise<void> => {
10
+ await api.mutate("/api/notifications/mark-read", { notificationIds: notifications.map((item) => item.id) });
11
+ await Promise.resolve(onRefresh());
12
+ };
13
+ return (
14
+ <div className="two-stack">
15
+ <section className="focus-panel">
16
+ <div>
17
+ <p className="eyebrow">{t(locale, "needsAttention")}</p>
18
+ <h2>{t(locale, "notificationsCount", { count: notifications.filter((item) => item.severity !== "informational").length })}</h2>
19
+ <p>{t(locale, "quietProgress")}</p>
20
+ </div>
21
+ <button className="ghost-button" type="button" disabled={notifications.length === 0} onClick={() => void markRead()}>{t(locale, "actionMarkAllRead")}</button>
22
+ </section>
23
+ {groups.map((group) => (
24
+ <Collapsible key={group} title={severityLabel(locale, group)} chip={t(locale, "metricItems", { count: notifications.filter((item) => item.severity === group).length })} defaultOpen={group !== "informational"}>
25
+ <NotificationList items={notifications.filter((item) => item.severity === group)} locale={locale} />
26
+ </Collapsible>
27
+ ))}
28
+ </div>
29
+ );
30
+ }
@@ -0,0 +1,19 @@
1
+ import type { JSX } from "react";
2
+ import type { PlanNavigatorData, PrSelectionData } from "../../api.js";
3
+ import { Collapsible } from "../../components/Collapsible.js";
4
+ import { EmptyState } from "../../components/EmptyState.js";
5
+ import { List } from "../../components/List.js";
6
+ import { t } from "../../i18n.js";
7
+ import { PlanList, PlanSelectionSummary, type EffectiveLocale } from "../CommandCenterParts.js";
8
+
9
+ export function PlanNavigator({ data, selection, locale }: { data: PlanNavigatorData | undefined; selection: PrSelectionData | undefined; locale: EffectiveLocale }): JSX.Element {
10
+ if (!data) return <EmptyState title={t(locale, "noPlanData")} message={t(locale, "noPlanDataMessage")} />;
11
+ return (
12
+ <div className="two-stack">
13
+ <PlanSelectionSummary data={data} selection={selection} locale={locale} />
14
+ <Collapsible title={t(locale, "completedPrs")} chip={t(locale, "completedChip", { count: data.completed.length })}><PlanList items={data.completed} locale={locale} /></Collapsible>
15
+ <Collapsible title={t(locale, "candidatePrs")} chip={t(locale, "candidatesChip", { count: data.candidates.length })}><PlanList items={data.candidates} locale={locale} /></Collapsible>
16
+ <Collapsible title={t(locale, "rawEvidence")} chip={t(locale, "sourcesChip", { count: data.evidence.length })}><List items={[data.convention, ...data.evidence]} locale={locale} /></Collapsible>
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,22 @@
1
+ import type { JSX } from "react";
2
+ import { useEffect, useState } from "react";
3
+ import type { ConfigSnapshot, DashboardApi } from "../../api.js";
4
+ import { ConfigEditor } from "../../components/ConfigEditor.js";
5
+ import { EmptyState } from "../../components/EmptyState.js";
6
+ import { ErrorState } from "../../components/ErrorState.js";
7
+ import { t } from "../../i18n.js";
8
+ import type { EffectiveLocale } from "../CommandCenterParts.js";
9
+
10
+ export function PolicyConfig({ api, onRefresh, locale }: { api: DashboardApi; onRefresh: () => void; locale: EffectiveLocale }): JSX.Element {
11
+ const [snapshot, setSnapshot] = useState<ConfigSnapshot>();
12
+ const [error, setError] = useState<string>();
13
+ useEffect(() => {
14
+ void api.policyConfig().then((result) => {
15
+ if (result.ok && result.data) setSnapshot(result.data);
16
+ else setError(result.error?.message ?? t(locale, "policyConfigLoadError"));
17
+ });
18
+ }, [api, locale]);
19
+ if (error) return <ErrorState title={t(locale, "policyConfigUnavailable")} message={error} />;
20
+ if (!snapshot) return <EmptyState title={t(locale, "loadingConfig")} message={t(locale, "loadingConfigMessage")} />;
21
+ return <ConfigEditor snapshot={snapshot} api={api} onSaved={onRefresh} locale={locale} />;
22
+ }