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,389 @@
1
+ import type { JSX } from "react";
2
+ import { useMemo, useState } from "react";
3
+ import type { ConfigSnapshot, DashboardApi } from "../api.js";
4
+ import { displayValueLabel, localeOptionLabel, t } from "../i18n.js";
5
+ import { RiskBadge } from "./RiskBadge.js";
6
+ import { StatusBadge } from "./StatusBadge.js";
7
+ import { LOCALE_SETTINGS, type EffectiveLocale, type LocaleSetting } from "../../../core/locale.js";
8
+
9
+ interface ConfigEditorProps {
10
+ snapshot: ConfigSnapshot;
11
+ api: DashboardApi;
12
+ onSaved: () => void;
13
+ locale: EffectiveLocale;
14
+ }
15
+
16
+ const groups = [
17
+ "Workflow",
18
+ "Autonomy",
19
+ "Language",
20
+ "Merge",
21
+ "Notifications",
22
+ "Review Handling",
23
+ "Safety Guards",
24
+ "Dashboard",
25
+ "Advanced Compatibility"
26
+ ] as const;
27
+
28
+ const groupLabelKeys: Record<(typeof groups)[number], string> = {
29
+ Workflow: "configGroupWorkflow",
30
+ Autonomy: "configGroupAutonomy",
31
+ Language: "configGroupLanguage",
32
+ Merge: "configGroupMerge",
33
+ Notifications: "configGroupNotifications",
34
+ "Review Handling": "configGroupReview",
35
+ "Safety Guards": "configGroupSafety",
36
+ Dashboard: "configGroupDashboard",
37
+ "Advanced Compatibility": "configGroupAdvanced"
38
+ };
39
+
40
+ const fieldLabelKeys: Record<string, string> = {
41
+ loopShape: "fieldLoopShape",
42
+ workflowProfile: "fieldWorkflowProfile",
43
+ roleProfile: "fieldRoleProfile",
44
+ autonomyMode: "fieldAutonomyMode",
45
+ locale: "fieldConfigLocale",
46
+ mergeMode: "fieldMergeMode",
47
+ requireReviewApproval: "fieldRequireReviewApproval",
48
+ notifyMode: "fieldNotifyMode",
49
+ reviewHandling: "fieldReviewHandling",
50
+ carryoverTarget: "fieldCarryoverTarget",
51
+ gitnexusRequired: "fieldGitnexusRequired",
52
+ requiredChecks: "fieldRequiredChecks",
53
+ protectedPaths: "fieldProtectedPaths",
54
+ maxReviewFixRounds: "fieldMaxReviewFixRounds",
55
+ maxTestFixRounds: "fieldMaxTestFixRounds",
56
+ maxCiReruns: "fieldMaxCiReruns",
57
+ allowAutoMerge: "fieldAllowAutoMerge"
58
+ };
59
+
60
+ const PR_WORKFLOW_PROFILES = ["default_pr_loop", "docs_only_loop", "review_fix_loop", "release_ready_loop"];
61
+ const GENERIC_WORKFLOW_PROFILES = ["research_report_loop", "document_preparation_loop", "repo_hygiene_loop", "weekly_review_loop", "data_extraction_loop"];
62
+
63
+ export function ConfigEditor({ snapshot, api, onSaved, locale }: ConfigEditorProps): JSX.Element {
64
+ const [expectedHash, setExpectedHash] = useState(snapshot.hash);
65
+ const [baselineConfig, setBaselineConfig] = useState<Record<string, unknown>>(snapshot.config);
66
+ const [config, setConfig] = useState<Record<string, unknown>>(snapshot.config);
67
+ const [note, setNote] = useState("");
68
+ const [confirmationToken, setConfirmationToken] = useState("");
69
+ const [message, setMessage] = useState("");
70
+ const diff = useMemo(() => changedFields(baselineConfig, config), [baselineConfig, config]);
71
+ const highRisk = diff.some((field) => ["mergeMode", "requireReviewApproval", "gitnexusRequired", "protectedPaths", "reviewHandling"].includes(field));
72
+ const dangerous = hasDangerousChange(baselineConfig, config);
73
+ const save = async (): Promise<void> => {
74
+ const result = await api.mutate("/api/policy-config", {
75
+ expectedHash,
76
+ nextConfig: config,
77
+ note,
78
+ confirmationToken
79
+ });
80
+ if (result.ok) {
81
+ const saved = isRecord(result.data) ? result.data : {};
82
+ const savedConfig = isRecord(saved.config) ? saved.config : config;
83
+ const savedSnapshot = isRecord(saved.snapshot) ? saved.snapshot : undefined;
84
+ setMessage(t(locale, "configSaved"));
85
+ setConfig(savedConfig);
86
+ setBaselineConfig(savedConfig);
87
+ if (savedSnapshot && typeof savedSnapshot.hash === "string") {
88
+ setExpectedHash(savedSnapshot.hash);
89
+ }
90
+ setConfirmationToken("");
91
+ onSaved();
92
+ } else {
93
+ setMessage(result.error?.message ?? t(locale, "configSaveFailed"));
94
+ }
95
+ };
96
+
97
+ return (
98
+ <div className="config-editor">
99
+ <div className="forecast-strip">
100
+ <Summary label={t(locale, "configGroupWorkflow")} value={displayValueLabel(locale, String(config.workflowProfile ?? "default_pr_loop"))} />
101
+ <Summary label={t(locale, "configGroupAutonomy")} value={displayValueLabel(locale, String(config.autonomyMode ?? "autonomous_until_gate"))} />
102
+ <Summary label={t(locale, "configGroupMerge")} value={displayValueLabel(locale, String(config.mergeMode ?? "manual"))} />
103
+ <Summary label={t(locale, "configGroupNotifications")} value={displayValueLabel(locale, String(config.notifyMode ?? "important_only"))} />
104
+ </div>
105
+
106
+ {groups.map((group) => (
107
+ <details className="disclosure-panel" key={group}>
108
+ <summary>
109
+ <span>{t(locale, groupLabelKeys[group])}</span>
110
+ <StatusBadge value={summaryFor(group, config, locale)} tone={group === "Merge" && config.mergeMode === "conditional" ? "yellow" : "blue"} />
111
+ </summary>
112
+ <ConfigGroup group={group} config={config} setConfig={setConfig} locale={locale} />
113
+ </details>
114
+ ))}
115
+
116
+ <section className="config-save-panel">
117
+ <div className="section-heading compact">
118
+ <div>
119
+ <h2>{t(locale, "configDiffTitle")}</h2>
120
+ <p>{diff.length === 0 ? t(locale, "configNoChanges") : t(locale, "configChangedFields", { count: diff.length })}</p>
121
+ </div>
122
+ <RiskBadge risk={highRisk ? "high" : diff.length > 0 ? "medium" : "low"} locale={locale} />
123
+ </div>
124
+ <div className="diff-list">
125
+ {diff.length === 0 ? <span className="muted-copy">{t(locale, "configChangeControl")}</span> : diff.map((field) => (
126
+ <code key={field}>{field}: {JSON.stringify(baselineConfig[field])} {"->"} {JSON.stringify(config[field])}</code>
127
+ ))}
128
+ </div>
129
+ {dangerous ? <p className="warning-copy">{t(locale, "configDangerous")}</p> : null}
130
+ <label>
131
+ {t(locale, "operatorNote")}
132
+ <textarea
133
+ aria-label={t(locale, "policyChangeNote")}
134
+ value={note}
135
+ onChange={(event) => setNote(event.target.value)}
136
+ placeholder={t(locale, "operatorNotePlaceholder")}
137
+ />
138
+ </label>
139
+ <label>
140
+ {t(locale, "confirmationToken")}
141
+ <input
142
+ aria-label={t(locale, "confirmationToken")}
143
+ value={confirmationToken}
144
+ onChange={(event) => setConfirmationToken(event.target.value)}
145
+ placeholder={t(locale, "confirmationTokenPlaceholder")}
146
+ />
147
+ </label>
148
+ <div className="button-row">
149
+ <button className="success-button" type="button" disabled={diff.length === 0 || (dangerous && confirmationToken.trim() !== "CONFIRM")} onClick={() => void save()}>
150
+ {t(locale, "actionSaveConfig")}
151
+ </button>
152
+ <button className="ghost-button" type="button" onClick={() => setConfig(baselineConfig)}>
153
+ {t(locale, "actionRevert")}
154
+ </button>
155
+ </div>
156
+ {message ? <p className="action-message">{message}</p> : null}
157
+ </section>
158
+ </div>
159
+ );
160
+ }
161
+
162
+ function ConfigGroup({
163
+ group,
164
+ config,
165
+ setConfig,
166
+ locale,
167
+ }: {
168
+ group: (typeof groups)[number];
169
+ config: Record<string, unknown>;
170
+ setConfig: (config: Record<string, unknown>) => void;
171
+ locale: EffectiveLocale;
172
+ }): JSX.Element {
173
+ if (group === "Workflow") {
174
+ const loopShape = String(config.loopShape ?? "pr-loop");
175
+ const workflowOptions = loopShape === "generic-loop" ? GENERIC_WORKFLOW_PROFILES : PR_WORKFLOW_PROFILES;
176
+ return (
177
+ <div className="form-grid">
178
+ <SelectField field="loopShape" options={["pr-loop", "generic-loop"]} config={config} setConfig={(next) => {
179
+ const nextShape = String(next.loopShape ?? "pr-loop");
180
+ setConfig({
181
+ ...next,
182
+ workflowProfile: nextShape === "generic-loop" ? "research_report_loop" : "default_pr_loop"
183
+ });
184
+ }} locale={locale} />
185
+ <SelectField field="workflowProfile" options={workflowOptions} config={config} setConfig={setConfig} locale={locale} />
186
+ <SelectField field="roleProfile" options={["default_pr_roles"]} config={config} setConfig={setConfig} locale={locale} />
187
+ </div>
188
+ );
189
+ }
190
+ if (group === "Autonomy") {
191
+ return <SelectField field="autonomyMode" options={["supervised", "autonomous_until_gate", "autonomous_until_terminal"]} config={config} setConfig={setConfig} locale={locale} />;
192
+ }
193
+ if (group === "Language") {
194
+ return <SelectField field="locale" options={[...LOCALE_SETTINGS]} config={config} setConfig={setConfig} locale={locale} />;
195
+ }
196
+ if (group === "Merge") {
197
+ return (
198
+ <div className="form-grid merge-config-grid">
199
+ <SelectField field="mergeMode" options={["manual", "conditional", "disabled"]} config={config} setConfig={setConfig} locale={locale} />
200
+ <ToggleField field="requireReviewApproval" config={config} setConfig={setConfig} locale={locale} />
201
+ </div>
202
+ );
203
+ }
204
+ if (group === "Notifications") {
205
+ return <SelectField field="notifyMode" options={["all_gates", "important_only", "blockers_only"]} config={config} setConfig={setConfig} locale={locale} />;
206
+ }
207
+ if (group === "Review Handling") {
208
+ return (
209
+ <div className="form-grid">
210
+ <SelectField field="reviewHandling" options={["fix_scoped_and_carry_forward", "ask_on_any_review", "require_zero_open_findings"]} config={config} setConfig={setConfig} locale={locale} />
211
+ <TextField field="carryoverTarget" config={config} setConfig={setConfig} locale={locale} />
212
+ </div>
213
+ );
214
+ }
215
+ if (group === "Safety Guards") {
216
+ return (
217
+ <div className="form-grid">
218
+ <ToggleField field="gitnexusRequired" config={config} setConfig={setConfig} locale={locale} />
219
+ <ArrayField field="requiredChecks" config={config} setConfig={setConfig} locale={locale} />
220
+ <ArrayField field="protectedPaths" config={config} setConfig={setConfig} locale={locale} />
221
+ <NumberField field="maxReviewFixRounds" config={config} setConfig={setConfig} locale={locale} />
222
+ <NumberField field="maxTestFixRounds" config={config} setConfig={setConfig} locale={locale} />
223
+ <NumberField field="maxCiReruns" config={config} setConfig={setConfig} locale={locale} />
224
+ </div>
225
+ );
226
+ }
227
+ if (group === "Dashboard") {
228
+ const dashboard = (config.dashboard as Record<string, unknown> | undefined) ?? { enabled: true, host: "127.0.0.1" };
229
+ return (
230
+ <div className="form-grid">
231
+ <label>
232
+ {t(locale, "dashboardHost")}
233
+ <input
234
+ aria-label={t(locale, "dashboardHost")}
235
+ value={String(dashboard.host ?? "127.0.0.1")}
236
+ onChange={(event) => setConfig({ ...config, dashboard: { ...dashboard, host: event.target.value } })}
237
+ />
238
+ </label>
239
+ <label>
240
+ {t(locale, "dashboardPort")}
241
+ <input
242
+ aria-label={t(locale, "dashboardPort")}
243
+ type="number"
244
+ value={String(dashboard.port ?? "")}
245
+ onChange={(event) => setConfig({ ...config, dashboard: { ...dashboard, port: event.target.value ? Number(event.target.value) : undefined } })}
246
+ />
247
+ </label>
248
+ </div>
249
+ );
250
+ }
251
+ return (
252
+ <div className="form-grid">
253
+ <ToggleField field="allowAutoMerge" config={config} setConfig={setConfig} locale={locale} disabled />
254
+ <p className="muted-copy">{t(locale, "compatibilityView")}</p>
255
+ </div>
256
+ );
257
+ }
258
+
259
+ function SelectField({
260
+ field,
261
+ options,
262
+ config,
263
+ setConfig,
264
+ locale,
265
+ }: {
266
+ field: string;
267
+ options: string[];
268
+ config: Record<string, unknown>;
269
+ setConfig: (config: Record<string, unknown>) => void;
270
+ locale: EffectiveLocale;
271
+ }): JSX.Element {
272
+ const label = fieldLabel(locale, field);
273
+ return (
274
+ <label>
275
+ {label}
276
+ <select
277
+ aria-label={label}
278
+ value={String(config[field] ?? options[0])}
279
+ onChange={(event) => {
280
+ setConfig({ ...config, [field]: event.target.value });
281
+ }}
282
+ >
283
+ {options.map((option) => <option key={option} value={option}>{optionLabel(locale, field, option)}</option>)}
284
+ </select>
285
+ </label>
286
+ );
287
+ }
288
+
289
+ function ToggleField({
290
+ field,
291
+ config,
292
+ setConfig,
293
+ locale,
294
+ disabled
295
+ }: {
296
+ field: string;
297
+ config: Record<string, unknown>;
298
+ setConfig: (config: Record<string, unknown>) => void;
299
+ locale: EffectiveLocale;
300
+ disabled?: boolean;
301
+ }): JSX.Element {
302
+ const label = fieldLabel(locale, field);
303
+ return (
304
+ <label className="toggle-row">
305
+ <input
306
+ aria-label={label}
307
+ type="checkbox"
308
+ checked={Boolean(config[field])}
309
+ disabled={disabled}
310
+ onChange={(event) => setConfig({ ...config, [field]: event.target.checked })}
311
+ />
312
+ {label}
313
+ </label>
314
+ );
315
+ }
316
+
317
+ function TextField({ field, config, setConfig, locale }: { field: string; config: Record<string, unknown>; setConfig: (config: Record<string, unknown>) => void; locale: EffectiveLocale }): JSX.Element {
318
+ const label = fieldLabel(locale, field);
319
+ return (
320
+ <label>
321
+ {label}
322
+ <input aria-label={label} value={String(config[field] ?? "")} onChange={(event) => setConfig({ ...config, [field]: event.target.value })} />
323
+ </label>
324
+ );
325
+ }
326
+
327
+ function NumberField({ field, config, setConfig, locale }: { field: string; config: Record<string, unknown>; setConfig: (config: Record<string, unknown>) => void; locale: EffectiveLocale }): JSX.Element {
328
+ const label = fieldLabel(locale, field);
329
+ return (
330
+ <label>
331
+ {label}
332
+ <input aria-label={label} type="number" min="0" value={Number(config[field] ?? 0)} onChange={(event) => setConfig({ ...config, [field]: Number(event.target.value) })} />
333
+ </label>
334
+ );
335
+ }
336
+
337
+ function ArrayField({ field, config, setConfig, locale }: { field: string; config: Record<string, unknown>; setConfig: (config: Record<string, unknown>) => void; locale: EffectiveLocale }): JSX.Element {
338
+ const label = fieldLabel(locale, field);
339
+ return (
340
+ <label>
341
+ {label}
342
+ <input
343
+ aria-label={label}
344
+ value={Array.isArray(config[field]) ? (config[field] as string[]).join(", ") : ""}
345
+ onChange={(event) => setConfig({ ...config, [field]: event.target.value.split(",").map((value) => value.trim()).filter(Boolean) })}
346
+ />
347
+ </label>
348
+ );
349
+ }
350
+
351
+ function Summary({ label, value }: { label: string; value: string }): JSX.Element {
352
+ return <div><span>{label}</span><strong>{value}</strong></div>;
353
+ }
354
+
355
+ function changedFields(before: Record<string, unknown>, after: Record<string, unknown>): string[] {
356
+ return Object.keys({ ...before, ...after }).filter((field) => JSON.stringify(before[field]) !== JSON.stringify(after[field]));
357
+ }
358
+
359
+ function hasDangerousChange(before: Record<string, unknown>, after: Record<string, unknown>): boolean {
360
+ return (before.mergeMode !== after.mergeMode && after.mergeMode === "conditional") ||
361
+ (before.requireReviewApproval !== after.requireReviewApproval && after.requireReviewApproval === false);
362
+ }
363
+
364
+ function isRecord(value: unknown): value is Record<string, unknown> {
365
+ return typeof value === "object" && value !== null && !Array.isArray(value);
366
+ }
367
+
368
+ function fieldLabel(locale: EffectiveLocale, field: string): string {
369
+ return t(locale, fieldLabelKeys[field] ?? field);
370
+ }
371
+
372
+ function summaryFor(group: string, config: Record<string, unknown>, locale: EffectiveLocale): string {
373
+ if (group === "Workflow") return displayValueLabel(locale, String(config.workflowProfile ?? "default_pr_loop"));
374
+ if (group === "Autonomy") return displayValueLabel(locale, String(config.autonomyMode ?? t(locale, "summaryDefault")));
375
+ if (group === "Language") return localeOptionLabel(locale, (config.locale ?? "zh-CN") as LocaleSetting);
376
+ if (group === "Merge") return displayValueLabel(locale, String(config.mergeMode ?? "manual"));
377
+ if (group === "Notifications") return displayValueLabel(locale, String(config.notifyMode ?? "important_only"));
378
+ if (group === "Review Handling") return displayValueLabel(locale, String(config.reviewHandling ?? t(locale, "summaryDefault")));
379
+ if (group === "Safety Guards") return Boolean(config.gitnexusRequired) ? t(locale, "summaryGuarded") : t(locale, "summaryRelaxed");
380
+ if (group === "Dashboard") return t(locale, "summaryLocal");
381
+ return t(locale, "summaryDerived");
382
+ }
383
+
384
+ function optionLabel(locale: EffectiveLocale, field: string, option: string): string {
385
+ if (field === "locale") {
386
+ return localeOptionLabel(locale, option as LocaleSetting);
387
+ }
388
+ return displayValueLabel(locale, option);
389
+ }
@@ -0,0 +1,10 @@
1
+ import type { JSX } from "react";
2
+
3
+ export function EmptyState({ title, message }: { title: string; message: string }): JSX.Element {
4
+ return (
5
+ <section className="soft-state">
6
+ <h2>{title}</h2>
7
+ <p>{message}</p>
8
+ </section>
9
+ );
10
+ }
@@ -0,0 +1,12 @@
1
+ import { AlertTriangle } from "lucide-react";
2
+ import type { JSX } from "react";
3
+
4
+ export function ErrorState({ title, message }: { title: string; message: string }): JSX.Element {
5
+ return (
6
+ <section className="soft-state soft-state--error">
7
+ <AlertTriangle size={18} />
8
+ <h2>{title}</h2>
9
+ <p>{message}</p>
10
+ </section>
11
+ );
12
+ }
@@ -0,0 +1,7 @@
1
+ import type { JSX } from "react";
2
+ import type { EffectiveLocale } from "../../../core/locale.js";
3
+ import { t } from "../i18n.js";
4
+
5
+ export function List({ items, locale }: { items: string[]; locale: EffectiveLocale }): JSX.Element {
6
+ return <ul className="plain-list">{items.length === 0 ? <li>{t(locale, "noneList")}</li> : items.map((item, index) => <li key={`${item}:${index}`}>{item}</li>)}</ul>;
7
+ }
@@ -0,0 +1,6 @@
1
+ import type { JSX } from "react";
2
+ import type { StatusTone } from "./StatusBadge.js";
3
+
4
+ export function MetricRow({ label, value, tone }: { label: string; value: string; tone: StatusTone }): JSX.Element {
5
+ return <div className={`metric-row metric-row--${tone}`}><span>{label}</span><strong>{value}</strong></div>;
6
+ }
@@ -0,0 +1,65 @@
1
+ import type { JSX } from "react";
2
+ import { useEffect, useState } from "react";
3
+
4
+ export interface ResponsiveTableRow {
5
+ key: string;
6
+ cells: Array<string | JSX.Element>;
7
+ cardTitle?: string;
8
+ cardMeta?: string;
9
+ cardSummary?: string;
10
+ }
11
+
12
+ export function Table({ columns, rows, empty }: { columns: string[]; rows: Array<Array<string | JSX.Element>>; empty: string; }): JSX.Element {
13
+ return <ResponsiveTable columns={columns} rows={rows.map((cells, index) => ({ key: String(index), cells }))} empty={empty} />;
14
+ }
15
+
16
+ export function ResponsiveTable({ columns, rows, empty }: { columns: string[]; rows: ResponsiveTableRow[]; empty: string; }): JSX.Element {
17
+ const compact = useCompactTable();
18
+ if (compact) {
19
+ return (
20
+ <section className="table-panel table-panel--compact">
21
+ <div className="compact-card-list" aria-label="Compact data">
22
+ {rows.length === 0 ? <article className="compact-data-card"><p>{empty}</p></article> : rows.map((row) => (
23
+ <article className="compact-data-card" key={row.key}>
24
+ <div className="compact-data-card__head">
25
+ <strong>{row.cardTitle ?? String(row.cells[0] ?? "")}</strong>
26
+ {row.cardMeta ? <span>{row.cardMeta}</span> : null}
27
+ </div>
28
+ {row.cardSummary ? <p>{row.cardSummary}</p> : null}
29
+ <dl>
30
+ {columns.map((column, index) => (
31
+ <div key={column}>
32
+ <dt>{column}</dt>
33
+ <dd>{row.cells[index] ?? "-"}</dd>
34
+ </div>
35
+ ))}
36
+ </dl>
37
+ </article>
38
+ ))}
39
+ </div>
40
+ </section>
41
+ );
42
+ }
43
+ return (
44
+ <section className="table-panel">
45
+ <table><thead><tr>{columns.map((column) => <th key={column}>{column}</th>)}</tr></thead><tbody>{rows.length === 0 ? <tr><td colSpan={columns.length} className="empty-cell">{empty}</td></tr> : rows.map((row) => <tr key={row.key}>{row.cells.map((cell, cellIndex) => <td key={cellIndex}>{cell}</td>)}</tr>)}</tbody></table>
46
+ </section>
47
+ );
48
+ }
49
+
50
+ function useCompactTable(): boolean {
51
+ const [compact, setCompact] = useState(() => compactTableMatches());
52
+ useEffect(() => {
53
+ const update = (): void => setCompact(compactTableMatches());
54
+ update();
55
+ window.addEventListener("resize", update);
56
+ return () => window.removeEventListener("resize", update);
57
+ }, []);
58
+ return compact;
59
+ }
60
+
61
+ function compactTableMatches(): boolean {
62
+ if (typeof window === "undefined") return false;
63
+ if (typeof window.matchMedia === "function") return window.matchMedia("(max-width: 560px)").matches;
64
+ return window.innerWidth <= 560;
65
+ }
@@ -0,0 +1,10 @@
1
+ import type { JSX } from "react";
2
+ import type { EffectiveLocale } from "../../../core/locale.js";
3
+ import { t } from "../i18n.js";
4
+
5
+ /** Render a localized risk badge for human-readable dashboard status. */
6
+ export function RiskBadge({ risk, locale = "en-US" }: { risk: "low" | "medium" | "high"; locale?: EffectiveLocale }): JSX.Element {
7
+ const tone = risk === "high" ? "red" : risk === "medium" ? "yellow" : "green";
8
+ const labelKey = risk === "high" ? "riskHigh" : risk === "medium" ? "riskMedium" : "riskLow";
9
+ return <span className={`status-badge status-badge--${tone}`}>{t(locale, labelKey)}</span>;
10
+ }
@@ -0,0 +1,29 @@
1
+ import type { JSX } from "react";
2
+
3
+ export type StatusTone = "green" | "yellow" | "red" | "blue" | "muted";
4
+
5
+ interface StatusBadgeProps {
6
+ value: string;
7
+ tone?: StatusTone;
8
+ }
9
+
10
+ export function StatusBadge({ value, tone = "muted" }: StatusBadgeProps): JSX.Element {
11
+ return <span className={`status-badge status-badge--${tone}`}>{value}</span>;
12
+ }
13
+
14
+ export function toneForStatus(value: string | undefined): StatusTone {
15
+ const normalized = value?.toLowerCase() ?? "";
16
+ if (["running", "ready", "green", "passed", "succeeded", "clean", "approved", "open"].includes(normalized)) {
17
+ return "green";
18
+ }
19
+ if (["blocked", "pending", "stopped", "waiting", "draft"].includes(normalized)) {
20
+ return "yellow";
21
+ }
22
+ if (["failed", "error", "rejected", "timed_out", "invalid_output"].includes(normalized)) {
23
+ return "red";
24
+ }
25
+ if (["idle", "needs_repo_init"].includes(normalized)) {
26
+ return "blue";
27
+ }
28
+ return "muted";
29
+ }
@@ -0,0 +1,10 @@
1
+ import { Activity } from "lucide-react";
2
+ import type { JSX } from "react";
3
+ import { displayValueLabel } from "../i18n.js";
4
+ import type { EffectiveLocale } from "../../../core/locale.js";
5
+ import type { StatusTone } from "./StatusBadge.js";
6
+
7
+ export function TopMetric({ icon: Icon, label, value, tone, locale }: { icon: typeof Activity; label: string; value: string; tone: StatusTone; locale?: EffectiveLocale }): JSX.Element {
8
+ const displayValue = locale ? displayValueLabel(locale, value) : value;
9
+ return <div className={`top-metric top-metric--${tone}`}><Icon size={19} /><div><span>{label}</span><strong title={value}>{displayValue}</strong></div></div>;
10
+ }