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.
- package/.agents/plugins/marketplace.json +20 -0
- package/CONTRIBUTING.md +54 -0
- package/LICENSE +21 -0
- package/README.md +215 -0
- package/README.zh-CN.md +215 -0
- package/SECURITY.md +39 -0
- package/assets/brand/README.md +35 -0
- package/assets/brand/holo-codex-icon.svg +28 -0
- package/assets/brand/holo-codex-lockup.svg +49 -0
- package/assets/brand/holo-codex-mark.svg +33 -0
- package/assets/brand/holo-codex-plugin-card.png +0 -0
- package/assets/brand/holo-codex-plugin-card.svg +81 -0
- package/assets/brand/holo-codex-readme-hero.png +0 -0
- package/assets/brand/holo-codex-readme-hero.svg +140 -0
- package/assets/brand/holo-codex-social-preview.png +0 -0
- package/assets/brand/holo-codex-social-preview.svg +130 -0
- package/assets/brand/holo-codex-wordmark-options.svg +52 -0
- package/docs/checklists/agent-loop-first-delivery-audit.md +129 -0
- package/docs/examples/generic-loop-repo-hygiene.md +168 -0
- package/docs/install.md +190 -0
- package/docs/local-release-readiness.md +206 -0
- package/docs/release-checklist.md +144 -0
- package/docs/self-bootstrap.md +150 -0
- package/docs/trust-and-safety.md +45 -0
- package/package.json +83 -0
- package/plugins/autonomous-pr-loop/.codex-plugin/plugin.json +17 -0
- package/plugins/autonomous-pr-loop/.mcp.json +13 -0
- package/plugins/autonomous-pr-loop/bin/agent-loop.mjs +31 -0
- package/plugins/autonomous-pr-loop/core/artifacts.ts +164 -0
- package/plugins/autonomous-pr-loop/core/autonomy-policy.ts +206 -0
- package/plugins/autonomous-pr-loop/core/ci.ts +131 -0
- package/plugins/autonomous-pr-loop/core/cli-i18n.ts +123 -0
- package/plugins/autonomous-pr-loop/core/cli.ts +1413 -0
- package/plugins/autonomous-pr-loop/core/command-runner.ts +446 -0
- package/plugins/autonomous-pr-loop/core/command.ts +47 -0
- package/plugins/autonomous-pr-loop/core/config-editor.ts +140 -0
- package/plugins/autonomous-pr-loop/core/config.ts +293 -0
- package/plugins/autonomous-pr-loop/core/controller-host.ts +19 -0
- package/plugins/autonomous-pr-loop/core/dashboard-server.ts +536 -0
- package/plugins/autonomous-pr-loop/core/delivery-work-item.ts +217 -0
- package/plugins/autonomous-pr-loop/core/doctor.ts +335 -0
- package/plugins/autonomous-pr-loop/core/errors.ts +82 -0
- package/plugins/autonomous-pr-loop/core/gate-recovery.ts +176 -0
- package/plugins/autonomous-pr-loop/core/gates.ts +26 -0
- package/plugins/autonomous-pr-loop/core/generic-lifecycle.ts +399 -0
- package/plugins/autonomous-pr-loop/core/git.ts +213 -0
- package/plugins/autonomous-pr-loop/core/github.ts +269 -0
- package/plugins/autonomous-pr-loop/core/gitnexus.ts +90 -0
- package/plugins/autonomous-pr-loop/core/happy.ts +42 -0
- package/plugins/autonomous-pr-loop/core/hook-capture.ts +115 -0
- package/plugins/autonomous-pr-loop/core/hook-events.ts +22 -0
- package/plugins/autonomous-pr-loop/core/hook-installation.ts +85 -0
- package/plugins/autonomous-pr-loop/core/hook-observer.ts +84 -0
- package/plugins/autonomous-pr-loop/core/hook-policy.ts +423 -0
- package/plugins/autonomous-pr-loop/core/hook-router.ts +452 -0
- package/plugins/autonomous-pr-loop/core/index.ts +32 -0
- package/plugins/autonomous-pr-loop/core/local-install.ts +778 -0
- package/plugins/autonomous-pr-loop/core/locale.ts +60 -0
- package/plugins/autonomous-pr-loop/core/loop-shapes.ts +190 -0
- package/plugins/autonomous-pr-loop/core/mcp-controller.ts +1479 -0
- package/plugins/autonomous-pr-loop/core/notification-feed.ts +263 -0
- package/plugins/autonomous-pr-loop/core/plan-parser.ts +206 -0
- package/plugins/autonomous-pr-loop/core/plugin-paths.ts +32 -0
- package/plugins/autonomous-pr-loop/core/policy.ts +65 -0
- package/plugins/autonomous-pr-loop/core/pr-lifecycle.ts +464 -0
- package/plugins/autonomous-pr-loop/core/pr-selector.ts +284 -0
- package/plugins/autonomous-pr-loop/core/profiles.ts +439 -0
- package/plugins/autonomous-pr-loop/core/redaction.ts +17 -0
- package/plugins/autonomous-pr-loop/core/repo-root.ts +22 -0
- package/plugins/autonomous-pr-loop/core/review-comments.ts +77 -0
- package/plugins/autonomous-pr-loop/core/scope-guard.ts +179 -0
- package/plugins/autonomous-pr-loop/core/state-machine.ts +828 -0
- package/plugins/autonomous-pr-loop/core/state-types.ts +130 -0
- package/plugins/autonomous-pr-loop/core/storage.ts +2527 -0
- package/plugins/autonomous-pr-loop/core/types.ts +567 -0
- package/plugins/autonomous-pr-loop/core/worker-events.ts +412 -0
- package/plugins/autonomous-pr-loop/core/worker-policy.ts +72 -0
- package/plugins/autonomous-pr-loop/core/worker-prompts.ts +182 -0
- package/plugins/autonomous-pr-loop/core/worker.ts +809 -0
- package/plugins/autonomous-pr-loop/core/workflow-board.ts +1515 -0
- package/plugins/autonomous-pr-loop/hooks/dist/permission-request.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/post-compact.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/post-tool-use.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/pre-compact.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/pre-tool-use.js +3460 -0
- package/plugins/autonomous-pr-loop/hooks/dist/session-start.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/stop.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/user-prompt-submit.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/hooks.json +106 -0
- package/plugins/autonomous-pr-loop/hooks/observe-runner.ts +25 -0
- package/plugins/autonomous-pr-loop/hooks/permission-request.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/post-compact.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/post-tool-use.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/pre-compact.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/pre-tool-use.ts +44 -0
- package/plugins/autonomous-pr-loop/hooks/session-start.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/stop.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/user-prompt-submit.ts +4 -0
- package/plugins/autonomous-pr-loop/mcp-server/src/index.ts +87 -0
- package/plugins/autonomous-pr-loop/mcp-server/src/tools.ts +205 -0
- package/plugins/autonomous-pr-loop/package.json +9 -0
- package/plugins/autonomous-pr-loop/schemas/config.schema.json +74 -0
- package/plugins/autonomous-pr-loop/schemas/marketplace.schema.json +46 -0
- package/plugins/autonomous-pr-loop/schemas/plugin.schema.json +32 -0
- package/plugins/autonomous-pr-loop/schemas/state.schema.json +19 -0
- package/plugins/autonomous-pr-loop/schemas/worker-event.schema.json +19 -0
- package/plugins/autonomous-pr-loop/schemas/worker-result.schema.json +58 -0
- package/plugins/autonomous-pr-loop/scripts/agent-loop.ts +44 -0
- package/plugins/autonomous-pr-loop/skills/autonomous-pr-loop/SKILL.md +26 -0
- package/plugins/autonomous-pr-loop/skills/autonomous-pr-loop/agents/openai.yaml +6 -0
- package/plugins/autonomous-pr-loop/ui/index.html +26 -0
- package/plugins/autonomous-pr-loop/ui/public/favicon.svg +7 -0
- package/plugins/autonomous-pr-loop/ui/src/api.ts +639 -0
- package/plugins/autonomous-pr-loop/ui/src/app.tsx +238 -0
- package/plugins/autonomous-pr-loop/ui/src/components/ActivityBadge.tsx +31 -0
- package/plugins/autonomous-pr-loop/ui/src/components/BrandMark.tsx +36 -0
- package/plugins/autonomous-pr-loop/ui/src/components/Collapsible.tsx +6 -0
- package/plugins/autonomous-pr-loop/ui/src/components/CommandPreview.tsx +15 -0
- package/plugins/autonomous-pr-loop/ui/src/components/ConfigEditor.tsx +389 -0
- package/plugins/autonomous-pr-loop/ui/src/components/EmptyState.tsx +10 -0
- package/plugins/autonomous-pr-loop/ui/src/components/ErrorState.tsx +12 -0
- package/plugins/autonomous-pr-loop/ui/src/components/List.tsx +7 -0
- package/plugins/autonomous-pr-loop/ui/src/components/MetricRow.tsx +6 -0
- package/plugins/autonomous-pr-loop/ui/src/components/ResponsiveTable.tsx +65 -0
- package/plugins/autonomous-pr-loop/ui/src/components/RiskBadge.tsx +10 -0
- package/plugins/autonomous-pr-loop/ui/src/components/StatusBadge.tsx +29 -0
- package/plugins/autonomous-pr-loop/ui/src/components/TopMetric.tsx +10 -0
- package/plugins/autonomous-pr-loop/ui/src/fixtures.ts +1152 -0
- package/plugins/autonomous-pr-loop/ui/src/i18n.ts +1105 -0
- package/plugins/autonomous-pr-loop/ui/src/main.tsx +14 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/CommandCenter.tsx +470 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/CommandCenterParts.tsx +276 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/agent-timeline/AgentTimelineView.tsx +73 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/artifact-viewer/ArtifactViewer.tsx +44 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/dry-run-preview/DryRunPreview.tsx +66 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/event-ledger/EventLedger.tsx +17 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/gate-center/GateCenter.tsx +34 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/mission-control/MissionControl.tsx +104 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/mission-control/WorkflowBoard.tsx +577 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/notifications/NotificationsView.tsx +30 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/plan-navigator/PlanNavigator.tsx +19 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/policy-config/PolicyConfig.tsx +22 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/pr-inbox/PrInbox.tsx +26 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/recovery-center/RecoveryCenter.tsx +125 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/scope-guard/ScopeGuard.tsx +16 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/worker-runs/WorkerRuns.tsx +39 -0
- package/plugins/autonomous-pr-loop/ui/src/styles.css +2673 -0
- package/plugins/autonomous-pr-loop/ui/src/theme.ts +57 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { StrictMode } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { App } from "./app.js";
|
|
4
|
+
|
|
5
|
+
const root = document.getElementById("root");
|
|
6
|
+
if (!root) {
|
|
7
|
+
throw new Error("Dashboard root element was not found.");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
createRoot(root).render(
|
|
11
|
+
<StrictMode>
|
|
12
|
+
<App />
|
|
13
|
+
</StrictMode>
|
|
14
|
+
);
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Activity,
|
|
3
|
+
AlertTriangle,
|
|
4
|
+
Archive,
|
|
5
|
+
Bell,
|
|
6
|
+
Bot,
|
|
7
|
+
Boxes,
|
|
8
|
+
CheckCircle2,
|
|
9
|
+
CircleStop,
|
|
10
|
+
Clock3,
|
|
11
|
+
FileText,
|
|
12
|
+
GitBranch,
|
|
13
|
+
GitPullRequest,
|
|
14
|
+
Languages,
|
|
15
|
+
ListChecks,
|
|
16
|
+
Monitor,
|
|
17
|
+
Moon,
|
|
18
|
+
Play,
|
|
19
|
+
RotateCcw,
|
|
20
|
+
Settings2,
|
|
21
|
+
StepForward,
|
|
22
|
+
Sun,
|
|
23
|
+
TerminalSquare
|
|
24
|
+
} from "lucide-react";
|
|
25
|
+
import type { JSX } from "react";
|
|
26
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
27
|
+
import type {
|
|
28
|
+
DashboardApi,
|
|
29
|
+
DashboardResult,
|
|
30
|
+
GateReevaluationData,
|
|
31
|
+
DashboardMetaData,
|
|
32
|
+
GateSummary,
|
|
33
|
+
LoopNotification,
|
|
34
|
+
MissionControlData
|
|
35
|
+
} from "../api.js";
|
|
36
|
+
import { ActivityBadge, activityReasonLabel } from "../components/ActivityBadge.js";
|
|
37
|
+
import { BrandMark } from "../components/BrandMark.js";
|
|
38
|
+
import { RiskBadge } from "../components/RiskBadge.js";
|
|
39
|
+
import { StatusBadge, toneForStatus } from "../components/StatusBadge.js";
|
|
40
|
+
import { TopMetric } from "../components/TopMetric.js";
|
|
41
|
+
import { displayValueLabel, localeOptionLabel, t, themeOptionLabel } from "../i18n.js";
|
|
42
|
+
import { THEME_SETTINGS, type EffectiveTheme, type ThemeSetting } from "../theme.js";
|
|
43
|
+
import { LOCALE_SETTINGS, type EffectiveLocale, type LocaleSetting } from "../../../core/locale.js";
|
|
44
|
+
import { AgentTimelineView as AgentTimelineSection } from "./agent-timeline/AgentTimelineView.js";
|
|
45
|
+
import { ArtifactViewer as ArtifactViewerSection } from "./artifact-viewer/ArtifactViewer.js";
|
|
46
|
+
import { formatTime, workerScopeSummary } from "./CommandCenterParts.js";
|
|
47
|
+
import { DryRunPreview as DryRunPreviewSection } from "./dry-run-preview/DryRunPreview.js";
|
|
48
|
+
import { EventLedger as EventLedgerSection } from "./event-ledger/EventLedger.js";
|
|
49
|
+
import { GateCenter as GateCenterSection } from "./gate-center/GateCenter.js";
|
|
50
|
+
import { MissionControl as MissionControlSection } from "./mission-control/MissionControl.js";
|
|
51
|
+
import { NotificationsView as NotificationsSection } from "./notifications/NotificationsView.js";
|
|
52
|
+
import { PlanNavigator as PlanNavigatorSection } from "./plan-navigator/PlanNavigator.js";
|
|
53
|
+
import { PolicyConfig as PolicyConfigSection } from "./policy-config/PolicyConfig.js";
|
|
54
|
+
import { PrInbox as PrInboxSection } from "./pr-inbox/PrInbox.js";
|
|
55
|
+
import { RecoveryCenter as RecoveryCenterSection } from "./recovery-center/RecoveryCenter.js";
|
|
56
|
+
import { ScopeGuard as ScopeGuardSection } from "./scope-guard/ScopeGuard.js";
|
|
57
|
+
import { WorkerRuns as WorkerRunsSection } from "./worker-runs/WorkerRuns.js";
|
|
58
|
+
|
|
59
|
+
interface CommandCenterProps {
|
|
60
|
+
data: MissionControlData;
|
|
61
|
+
meta?: DashboardMetaData | undefined;
|
|
62
|
+
api: DashboardApi;
|
|
63
|
+
stale: boolean;
|
|
64
|
+
onRefresh: () => void;
|
|
65
|
+
locale: EffectiveLocale;
|
|
66
|
+
localeSetting: LocaleSetting;
|
|
67
|
+
onLocaleSettingChange: (locale: LocaleSetting) => void;
|
|
68
|
+
themeSetting: ThemeSetting;
|
|
69
|
+
effectiveTheme: EffectiveTheme;
|
|
70
|
+
onThemeSettingChange: (theme: ThemeSetting) => void;
|
|
71
|
+
actionMessage?: string | undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type Page =
|
|
75
|
+
| "Mission Control"
|
|
76
|
+
| "Plan Navigator"
|
|
77
|
+
| "Policy Config"
|
|
78
|
+
| "Dry-run Preview"
|
|
79
|
+
| "Notifications"
|
|
80
|
+
| "Agent Timeline"
|
|
81
|
+
| "Gate Center"
|
|
82
|
+
| "PR Inbox"
|
|
83
|
+
| "Worker Runs"
|
|
84
|
+
| "Scope Guard"
|
|
85
|
+
| "Event Ledger"
|
|
86
|
+
| "Artifact Diff Viewer"
|
|
87
|
+
| "Recovery Center";
|
|
88
|
+
|
|
89
|
+
const pages: Array<{ name: Page; icon: typeof Activity }> = [
|
|
90
|
+
{ name: "Mission Control", icon: Activity },
|
|
91
|
+
{ name: "Plan Navigator", icon: GitBranch },
|
|
92
|
+
{ name: "Policy Config", icon: Settings2 },
|
|
93
|
+
{ name: "Dry-run Preview", icon: TerminalSquare },
|
|
94
|
+
{ name: "Notifications", icon: Bell },
|
|
95
|
+
{ name: "Agent Timeline", icon: Clock3 },
|
|
96
|
+
{ name: "Gate Center", icon: ListChecks },
|
|
97
|
+
{ name: "PR Inbox", icon: GitPullRequest },
|
|
98
|
+
{ name: "Worker Runs", icon: Bot },
|
|
99
|
+
{ name: "Scope Guard", icon: CheckCircle2 },
|
|
100
|
+
{ name: "Event Ledger", icon: Archive },
|
|
101
|
+
{ name: "Artifact Diff Viewer", icon: FileText },
|
|
102
|
+
{ name: "Recovery Center", icon: RotateCcw }
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
const pageKeys: Record<Page, string> = {
|
|
106
|
+
"Mission Control": "pageMission",
|
|
107
|
+
"Plan Navigator": "pagePlan",
|
|
108
|
+
"Policy Config": "pagePolicy",
|
|
109
|
+
"Dry-run Preview": "pageDryRun",
|
|
110
|
+
"Notifications": "pageNotifications",
|
|
111
|
+
"Agent Timeline": "pageTimeline",
|
|
112
|
+
"Gate Center": "pageGates",
|
|
113
|
+
"PR Inbox": "pagePr",
|
|
114
|
+
"Worker Runs": "pageWorkers",
|
|
115
|
+
"Scope Guard": "pageScope",
|
|
116
|
+
"Event Ledger": "pageEvents",
|
|
117
|
+
"Artifact Diff Viewer": "pageArtifacts",
|
|
118
|
+
"Recovery Center": "pageRecovery"
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export function CommandCenter({ data, meta, api, stale, onRefresh, locale, localeSetting, onLocaleSettingChange, themeSetting, effectiveTheme, onThemeSettingChange, actionMessage }: CommandCenterProps): JSX.Element {
|
|
122
|
+
const [page, setPage] = useState<Page>("Mission Control");
|
|
123
|
+
const [note, setNote] = useState("");
|
|
124
|
+
const [gateNextState, setGateNextState] = useState("");
|
|
125
|
+
const [busyAction, setBusyAction] = useState<string>();
|
|
126
|
+
const [artifactPreview, setArtifactPreview] = useState<{ id: string; text: string; error?: string; truncated?: boolean }>();
|
|
127
|
+
const stableApi = useRef(api);
|
|
128
|
+
const dismissedOneShotNotifications = useRef(new Set<string>());
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
stableApi.current = api;
|
|
131
|
+
}, [api]);
|
|
132
|
+
|
|
133
|
+
const run = data.current.run;
|
|
134
|
+
const activeGate = data.gates.find((gate) => gate.status === "open" && gate.activity === "active");
|
|
135
|
+
const historicalOpenGate = data.gates.find((gate) => gate.status === "open" && gate.activity === "historical");
|
|
136
|
+
const notifications = data.notifications ?? [];
|
|
137
|
+
const attention = highestAttention(notifications, activeGate?.kind ?? data.current.gate?.kind, locale);
|
|
138
|
+
const selectedArtifact = data.artifacts[0];
|
|
139
|
+
const gateNextStates = allowedGateNextStates(activeGate);
|
|
140
|
+
const gateDecisionPayload = gateNextState ? { nextState: gateNextState } : {};
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
setNote("");
|
|
144
|
+
setGateNextState(defaultGateNextState(activeGate));
|
|
145
|
+
}, [activeGate?.id]);
|
|
146
|
+
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
const oneShotIds = notifications
|
|
149
|
+
.map((notification) => notification.id)
|
|
150
|
+
.filter((id) => id.startsWith("longrunning:") && !dismissedOneShotNotifications.current.has(id));
|
|
151
|
+
if (oneShotIds.length === 0) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
for (const id of oneShotIds) {
|
|
155
|
+
dismissedOneShotNotifications.current.add(id);
|
|
156
|
+
}
|
|
157
|
+
void stableApi.current.mutate("/api/notifications/dismiss", { notificationIds: oneShotIds }).then((result) => {
|
|
158
|
+
if (result.ok) {
|
|
159
|
+
onRefresh();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
for (const id of oneShotIds) {
|
|
163
|
+
dismissedOneShotNotifications.current.delete(id);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}, [notifications, onRefresh]);
|
|
167
|
+
|
|
168
|
+
const runAction = async <T = unknown,>(label: string, path: string, body?: unknown): Promise<DashboardResult<T>> => {
|
|
169
|
+
setBusyAction(label);
|
|
170
|
+
try {
|
|
171
|
+
// Mutating endpoints return route-specific payloads; callers validate any narrowed shape before use.
|
|
172
|
+
const result = await stableApi.current.mutate(path, body) as DashboardResult<T>;
|
|
173
|
+
onRefresh();
|
|
174
|
+
return result;
|
|
175
|
+
} finally {
|
|
176
|
+
setBusyAction(undefined);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const pageContent = useMemo(() => {
|
|
181
|
+
if (page === "Plan Navigator") return <PlanNavigatorSection data={data.plan} selection={data.selection} locale={locale} />;
|
|
182
|
+
if (page === "Policy Config") return <PolicyConfigSection api={stableApi.current} onRefresh={onRefresh} locale={locale} />;
|
|
183
|
+
if (page === "Dry-run Preview") return <DryRunPreviewSection api={stableApi.current} locale={locale} onAction={(path) => void runAction(path, path)} />;
|
|
184
|
+
if (page === "Notifications") return <NotificationsSection notifications={notifications} api={stableApi.current} onRefresh={onRefresh} locale={locale} />;
|
|
185
|
+
if (page === "Agent Timeline") return <AgentTimelineSection api={stableApi.current} {...(data.current.run?.id ? { runId: data.current.run.id } : {})} locale={locale} />;
|
|
186
|
+
if (page === "Gate Center") return <GateCenterSection data={data} locale={locale} />;
|
|
187
|
+
if (page === "PR Inbox") return <PrInboxSection data={data} locale={locale} />;
|
|
188
|
+
if (page === "Worker Runs") return <WorkerRunsSection workers={data.workers} api={stableApi.current} locale={locale} scopeNote={workerScopeSummary(data.workers, locale)} />;
|
|
189
|
+
if (page === "Scope Guard") return <ScopeGuardSection data={data} locale={locale} />;
|
|
190
|
+
if (page === "Event Ledger") return <EventLedgerSection events={data.events} locale={locale} />;
|
|
191
|
+
if (page === "Artifact Diff Viewer") return <ArtifactViewerSection api={stableApi.current} data={data} preview={artifactPreview} onPreview={setArtifactPreview} locale={locale} />;
|
|
192
|
+
if (page === "Recovery Center") {
|
|
193
|
+
return (
|
|
194
|
+
<RecoveryCenterSection
|
|
195
|
+
data={data}
|
|
196
|
+
stale={stale}
|
|
197
|
+
onRecover={() => void runAction("recover", "/api/recover")}
|
|
198
|
+
onReevaluateGate={(gateId) => runAction<GateReevaluationData>("re-evaluate", `/api/gates/${gateId}/re-evaluate`)}
|
|
199
|
+
onMarkGateHandled={(gateId) => void runAction("mark-handled", `/api/gates/${gateId}/mark-handled`)}
|
|
200
|
+
locale={locale}
|
|
201
|
+
/>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return <MissionControlSection data={data} api={stableApi.current} stale={stale} locale={locale} onNavigate={setPage} />;
|
|
205
|
+
}, [artifactPreview, data, locale, notifications, onRefresh, page, stale]);
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<div className="dashboard-shell">
|
|
209
|
+
<aside className="dashboard-sidebar">
|
|
210
|
+
<div className="dashboard-brand">
|
|
211
|
+
<div className="brand-mark"><BrandMark /></div>
|
|
212
|
+
<div>
|
|
213
|
+
<strong>{t(locale, "brandTitle")}</strong>
|
|
214
|
+
<span>{t(locale, "brandSubtitle")}</span>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
<nav className="nav-list" aria-label={t(locale, "navAria")}>
|
|
218
|
+
{pages.map((item) => {
|
|
219
|
+
const Icon = item.icon;
|
|
220
|
+
const label = t(locale, pageKeys[item.name]);
|
|
221
|
+
return (
|
|
222
|
+
<button
|
|
223
|
+
aria-label={label}
|
|
224
|
+
className={item.name === page ? "nav-item is-active" : "nav-item"}
|
|
225
|
+
key={item.name}
|
|
226
|
+
title={label}
|
|
227
|
+
type="button"
|
|
228
|
+
onClick={() => setPage(item.name)}
|
|
229
|
+
>
|
|
230
|
+
<Icon size={17} />
|
|
231
|
+
<span>{label}</span>
|
|
232
|
+
</button>
|
|
233
|
+
);
|
|
234
|
+
})}
|
|
235
|
+
</nav>
|
|
236
|
+
<div className="sidebar-footer-compact" aria-label={`${t(locale, "language")} / ${t(locale, "theme")}`}>
|
|
237
|
+
<label className="compact-icon-control compact-icon-control--select" title={`${t(locale, "language")}: ${localeOptionLabel(locale, localeSetting)}`}>
|
|
238
|
+
<Languages size={17} />
|
|
239
|
+
<select
|
|
240
|
+
aria-label={`${t(locale, "language")}: ${localeOptionLabel(locale, localeSetting)}`}
|
|
241
|
+
value={localeSetting}
|
|
242
|
+
onChange={(event) => onLocaleSettingChange(event.target.value as LocaleSetting)}
|
|
243
|
+
>
|
|
244
|
+
{LOCALE_SETTINGS.map((option) => <option key={option} value={option}>{localeOptionLabel(locale, option)}</option>)}
|
|
245
|
+
</select>
|
|
246
|
+
</label>
|
|
247
|
+
<div className="compact-theme-switcher" role="group" aria-label={t(locale, "theme")}>
|
|
248
|
+
{THEME_SETTINGS.map((option) => {
|
|
249
|
+
const Icon = themeIcon(option);
|
|
250
|
+
const label = themeOptionLabel(locale, option);
|
|
251
|
+
return (
|
|
252
|
+
<button
|
|
253
|
+
aria-label={`${t(locale, "theme")}: ${label}`}
|
|
254
|
+
aria-pressed={themeSetting === option}
|
|
255
|
+
className={themeSetting === option ? "compact-icon-control is-active" : "compact-icon-control"}
|
|
256
|
+
key={option}
|
|
257
|
+
title={`${label} / ${t(locale, "themeEffective")}: ${themeOptionLabel(locale, effectiveTheme)}`}
|
|
258
|
+
type="button"
|
|
259
|
+
onClick={() => onThemeSettingChange(option)}
|
|
260
|
+
>
|
|
261
|
+
<Icon size={17} />
|
|
262
|
+
</button>
|
|
263
|
+
);
|
|
264
|
+
})}
|
|
265
|
+
</div>
|
|
266
|
+
<div className="compact-icon-control" title={`${meta?.targetRepo?.repoId ?? run?.branch ?? data.pr?.branch ?? t(locale, "noActiveBranch")} / ${stale ? t(locale, "stale") : t(locale, "live")}`}>
|
|
267
|
+
<Boxes size={17} />
|
|
268
|
+
<span className={stale ? "compact-status-dot compact-status-dot--yellow" : "compact-status-dot compact-status-dot--green"} />
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
<div className="sidebar-footer">
|
|
272
|
+
<label className="locale-switcher">
|
|
273
|
+
<span>{t(locale, "language")}</span>
|
|
274
|
+
<select
|
|
275
|
+
aria-label={t(locale, "language")}
|
|
276
|
+
value={localeSetting}
|
|
277
|
+
onChange={(event) => onLocaleSettingChange(event.target.value as LocaleSetting)}
|
|
278
|
+
>
|
|
279
|
+
{LOCALE_SETTINGS.map((option) => <option key={option} value={option}>{localeOptionLabel(locale, option)}</option>)}
|
|
280
|
+
</select>
|
|
281
|
+
</label>
|
|
282
|
+
<div className="theme-switcher" aria-label={t(locale, "theme")}>
|
|
283
|
+
<span>{t(locale, "theme")}</span>
|
|
284
|
+
<div className="segmented-control" role="group" aria-label={t(locale, "theme")}>
|
|
285
|
+
{THEME_SETTINGS.map((option) => {
|
|
286
|
+
const Icon = themeIcon(option);
|
|
287
|
+
const label = themeOptionLabel(locale, option);
|
|
288
|
+
return (
|
|
289
|
+
<button
|
|
290
|
+
aria-pressed={themeSetting === option}
|
|
291
|
+
className={themeSetting === option ? "segmented-button is-active" : "segmented-button"}
|
|
292
|
+
key={option}
|
|
293
|
+
title={`${label} / ${t(locale, "themeEffective")}: ${themeOptionLabel(locale, effectiveTheme)}`}
|
|
294
|
+
type="button"
|
|
295
|
+
onClick={() => onThemeSettingChange(option)}
|
|
296
|
+
>
|
|
297
|
+
<Icon size={16} />
|
|
298
|
+
<span>{label}</span>
|
|
299
|
+
</button>
|
|
300
|
+
);
|
|
301
|
+
})}
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
<div className="repo-chip repo-chip--stacked" title={meta?.targetRepo?.root}>
|
|
305
|
+
<Boxes size={18} />
|
|
306
|
+
<span>{meta?.targetRepo?.repoId ?? run?.branch ?? data.pr?.branch ?? t(locale, "noActiveBranch")}</span>
|
|
307
|
+
{meta?.targetRepo?.root ? <small>{meta.targetRepo.root}</small> : null}
|
|
308
|
+
</div>
|
|
309
|
+
<StatusBadge value={stale ? t(locale, "stale") : t(locale, "live")} tone={stale ? "yellow" : "green"} />
|
|
310
|
+
</div>
|
|
311
|
+
</aside>
|
|
312
|
+
|
|
313
|
+
<main className="dashboard-main">
|
|
314
|
+
<header className="topbar">
|
|
315
|
+
<TopMetric icon={GitBranch} label={t(locale, "topCurrentState")} value={run?.currentState ?? data.current.status} tone={toneForStatus(data.current.status)} locale={locale} />
|
|
316
|
+
<TopMetric icon={Activity} label={t(locale, "topAutonomy")} value={data.autonomy?.autonomyMode ?? "default"} tone="blue" locale={locale} />
|
|
317
|
+
<TopMetric
|
|
318
|
+
icon={GitPullRequest}
|
|
319
|
+
label={data.profile?.loopShape === "generic-loop" ? t(locale, "fieldLoopShape") : t(locale, "topMerge")}
|
|
320
|
+
value={data.profile?.loopShape === "generic-loop" ? data.profile.loopShape : data.mergeReadiness?.state ?? data.autonomy?.mergeMode ?? "manual"}
|
|
321
|
+
tone={data.profile?.loopShape === "generic-loop" || data.mergeReadiness?.ready ? "green" : "yellow"}
|
|
322
|
+
locale={locale}
|
|
323
|
+
/>
|
|
324
|
+
<TopMetric icon={Bell} label={t(locale, "topAttention")} value={String(notifications.filter((item) => item.severity !== "informational").length)} tone={notifications.some((item) => item.severity === "blocked") ? "red" : "yellow"} />
|
|
325
|
+
<TopMetric icon={Clock3} label={t(locale, "topPolling")} value={stale ? t(locale, "stale") : t(locale, "live")} tone={stale ? "yellow" : "green"} />
|
|
326
|
+
</header>
|
|
327
|
+
|
|
328
|
+
<section className="workspace">
|
|
329
|
+
<div className="dashboard-content">
|
|
330
|
+
<div className="section-heading">
|
|
331
|
+
<div>
|
|
332
|
+
<h1>{t(locale, pageKeys[page])}</h1>
|
|
333
|
+
<p>{subtitleFor(page, locale)}</p>
|
|
334
|
+
</div>
|
|
335
|
+
<div className="heading-actions">
|
|
336
|
+
{actionMessage ? <span className="action-message">{actionMessage}</span> : null}
|
|
337
|
+
<button className="ghost-button" type="button" onClick={onRefresh}>{t(locale, "actionRefresh")}</button>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
{pageContent}
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
<aside className="intervention-panel" aria-label={t(locale, "interventionPanel")}>
|
|
344
|
+
<div className="inspector-section">
|
|
345
|
+
<p className="eyebrow">{t(locale, "interventionPanelEyebrow")}</p>
|
|
346
|
+
<div className="gate-title">
|
|
347
|
+
{attention.risk === "high" ? <AlertTriangle size={26} /> : <CheckCircle2 size={26} />}
|
|
348
|
+
<div>
|
|
349
|
+
<h2>{attention.title}</h2>
|
|
350
|
+
<RiskBadge risk={attention.risk} locale={locale} />
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
<dl className="detail-list">
|
|
354
|
+
<div><dt>{t(locale, "runId")}</dt><dd>{run?.id ?? t(locale, "none")}</dd></div>
|
|
355
|
+
<div><dt>{t(locale, "gateId")}</dt><dd>{activeGate?.id ?? historicalOpenGate?.id ?? t(locale, "none")}</dd></div>
|
|
356
|
+
<div>
|
|
357
|
+
<dt>{t(locale, "tableActivity")}</dt>
|
|
358
|
+
<dd>
|
|
359
|
+
{activeGate ? (
|
|
360
|
+
<ActivityBadge activity={activeGate.activity} reason={activeGate.activityReason} locale={locale} />
|
|
361
|
+
) : historicalOpenGate ? (
|
|
362
|
+
<>
|
|
363
|
+
<ActivityBadge activity={historicalOpenGate.activity} reason={historicalOpenGate.activityReason} locale={locale} />
|
|
364
|
+
<span className="inline-detail">{activityReasonLabel(locale, historicalOpenGate.activityReason)}</span>
|
|
365
|
+
</>
|
|
366
|
+
) : t(locale, "none")}
|
|
367
|
+
</dd>
|
|
368
|
+
</div>
|
|
369
|
+
<div><dt>{t(locale, "reason")}</dt><dd>{attention.reason}</dd></div>
|
|
370
|
+
<div><dt>{t(locale, "updated")}</dt><dd>{run ? formatTime(run.updatedAt) : t(locale, "notStarted")}</dd></div>
|
|
371
|
+
</dl>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
<div className="inspector-section">
|
|
375
|
+
<p className="eyebrow">{t(locale, "decisionNoteEyebrow")}</p>
|
|
376
|
+
<textarea aria-label={t(locale, "decisionNote")} value={note} onChange={(event) => setNote(event.target.value)} placeholder={t(locale, "decisionNotePlaceholder")} />
|
|
377
|
+
{gateNextStates.length > 0 ? (
|
|
378
|
+
<label>
|
|
379
|
+
{t(locale, "gateNextState")}
|
|
380
|
+
<select aria-label={t(locale, "gateNextState")} value={gateNextState} onChange={(event) => setGateNextState(event.target.value)}>
|
|
381
|
+
{gateNextStates.map((state) => <option key={state} value={state}>{displayValueLabel(locale, state)}</option>)}
|
|
382
|
+
</select>
|
|
383
|
+
</label>
|
|
384
|
+
) : null}
|
|
385
|
+
<div className="button-row">
|
|
386
|
+
<button className="success-button" disabled={!activeGate || note.trim().length === 0 || busyAction !== undefined} type="button" onClick={() => activeGate && runAction("approve", `/api/gates/${activeGate.id}/approve`, { note, source: "ui", payload: gateDecisionPayload })}>
|
|
387
|
+
<CheckCircle2 size={16} /> {t(locale, "actionApprove")}
|
|
388
|
+
</button>
|
|
389
|
+
<button className="danger-button" disabled={!activeGate || note.trim().length === 0 || busyAction !== undefined} type="button" onClick={() => activeGate && runAction("reject", `/api/gates/${activeGate.id}/reject`, { note, source: "ui", payload: {} })}>
|
|
390
|
+
<AlertTriangle size={16} /> {t(locale, "actionReject")}
|
|
391
|
+
</button>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
<div className="inspector-section">
|
|
396
|
+
<p className="eyebrow">{t(locale, "loopControls")}</p>
|
|
397
|
+
<div className="control-grid">
|
|
398
|
+
<button type="button" onClick={() => runAction("run", "/api/run-until-gate")} disabled={busyAction !== undefined}><Play size={16} /> {t(locale, "actionRunToGate")}</button>
|
|
399
|
+
<button type="button" onClick={() => runAction("step", "/api/step")} disabled={busyAction !== undefined}><StepForward size={16} /> {t(locale, "actionStep")}</button>
|
|
400
|
+
<button type="button" onClick={() => runAction("resume", "/api/resume")} disabled={busyAction !== undefined}><RotateCcw size={16} /> {t(locale, "actionResume")}</button>
|
|
401
|
+
<button type="button" onClick={() => runAction("stop", "/api/stop")} disabled={busyAction !== undefined}><CircleStop size={16} /> {t(locale, "actionStop")}</button>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
<div className="inspector-section">
|
|
406
|
+
<p className="eyebrow">{t(locale, "artifactShortcut")}</p>
|
|
407
|
+
<div className="artifact-shortcut"><FileText size={17} /><span>{selectedArtifact?.name ?? t(locale, "noArtifactsYet")}</span></div>
|
|
408
|
+
</div>
|
|
409
|
+
</aside>
|
|
410
|
+
</section>
|
|
411
|
+
</main>
|
|
412
|
+
</div>
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
function allowedGateNextStates(gate: GateSummary | undefined): string[] {
|
|
418
|
+
const details = gateDetails(gate);
|
|
419
|
+
const states = details?.allowedNextStates;
|
|
420
|
+
return Array.isArray(states) ? states.filter((state): state is string => typeof state === "string") : [];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function defaultGateNextState(gate: GateSummary | undefined): string {
|
|
424
|
+
const details = gateDetails(gate);
|
|
425
|
+
const defaultState = details?.defaultNextState;
|
|
426
|
+
if (typeof defaultState === "string") return defaultState;
|
|
427
|
+
return allowedGateNextStates(gate)[0] ?? "";
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function gateDetails(gate: GateSummary | undefined): Record<string, unknown> | undefined {
|
|
431
|
+
const details = gate?.details;
|
|
432
|
+
return typeof details === "object" && details !== null && !Array.isArray(details) ? details as Record<string, unknown> : undefined;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
function highestAttention(notifications: LoopNotification[], gate: string | undefined, locale: EffectiveLocale): { title: string; reason: string; risk: "low" | "medium" | "high" } {
|
|
437
|
+
const blocker = notifications.find((item) => item.severity === "blocked" || item.severity === "confirmation_required");
|
|
438
|
+
if (blocker) return { title: blocker.title, reason: blocker.reason, risk: "high" };
|
|
439
|
+
const attention = notifications.find((item) => item.severity === "attention");
|
|
440
|
+
if (attention) return { title: attention.title, reason: attention.reason, risk: "medium" };
|
|
441
|
+
if (gate) return { title: gate, reason: t(locale, "visibleGateReason"), risk: "medium" };
|
|
442
|
+
return { title: t(locale, "noInterventionTitle"), reason: t(locale, "noInterventionReason"), risk: "low" };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function subtitleFor(page: Page, locale: EffectiveLocale): string {
|
|
446
|
+
const map: Record<Page, string> = {
|
|
447
|
+
"Mission Control": "subtitleMission",
|
|
448
|
+
"Plan Navigator": "subtitlePlan",
|
|
449
|
+
"Policy Config": "subtitlePolicy",
|
|
450
|
+
"Dry-run Preview": "subtitleDryRun",
|
|
451
|
+
"Notifications": "subtitleNotifications",
|
|
452
|
+
"Agent Timeline": "subtitleTimeline",
|
|
453
|
+
"Gate Center": "subtitleGates",
|
|
454
|
+
"PR Inbox": "subtitlePr",
|
|
455
|
+
"Worker Runs": "subtitleWorkers",
|
|
456
|
+
"Scope Guard": "subtitleScope",
|
|
457
|
+
"Event Ledger": "subtitleEvents",
|
|
458
|
+
"Artifact Diff Viewer": "subtitleArtifacts",
|
|
459
|
+
"Recovery Center": "subtitleRecovery"
|
|
460
|
+
};
|
|
461
|
+
return t(locale, map[page]);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export { decodeBase64Preview } from "./CommandCenterParts.js";
|
|
465
|
+
|
|
466
|
+
function themeIcon(theme: ThemeSetting): typeof Sun {
|
|
467
|
+
if (theme === "light") return Sun;
|
|
468
|
+
if (theme === "dark") return Moon;
|
|
469
|
+
return Monitor;
|
|
470
|
+
}
|