@moretea-labs/forge 1.5.0-rc.1 → 1.5.1

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 (264) hide show
  1. package/AGENTS.md +9 -11
  2. package/CHANGELOG.md +27 -1
  3. package/CLAUDE.md +8 -10
  4. package/DESIGN.md +278 -0
  5. package/README.md +70 -90
  6. package/README.zh-CN.md +52 -55
  7. package/SKILL.md +8 -11
  8. package/THIRD_PARTY_NOTICES.md +9 -3
  9. package/assets/branding/README.md +4 -2
  10. package/assets/hooks/lib/workflow-state.sh +19 -18
  11. package/assets/hooks/post-edit-guard.sh +6 -75
  12. package/assets/hooks/prompt-guard.sh +24 -12
  13. package/assets/hooks/session-start-context.sh +17 -17
  14. package/assets/hooks/stop-orchestrator.sh +2 -2
  15. package/assets/partials/04-project-structure.partial.md +6 -6
  16. package/assets/partials/05-workflow.partial.md +6 -6
  17. package/assets/partials/07-footer.partial.md +1 -1
  18. package/assets/partials/08-orchestration.partial.md +2 -2
  19. package/assets/partials-agents/02-operating-mode.partial.md +5 -5
  20. package/assets/partials-agents/03-orchestration.partial.md +3 -3
  21. package/assets/partials-agents/04-task-protocol.partial.md +3 -3
  22. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  23. package/assets/reference-configs/agentic-development-flow.md +49 -86
  24. package/assets/reference-configs/external-tooling.md +21 -61
  25. package/assets/reference-configs/handoff-protocol.md +25 -20
  26. package/assets/reference-configs/harness-overview.md +8 -8
  27. package/assets/skill-version.json +2 -2
  28. package/assets/templates/helpers/archive-workflow.sh +0 -1
  29. package/assets/templates/helpers/capture-plan.sh +3 -3
  30. package/assets/templates/helpers/check-task-workflow.sh +2 -2
  31. package/assets/templates/helpers/codex-handoff-resume.sh +5 -5
  32. package/assets/templates/helpers/ensure-task-workflow.sh +37 -35
  33. package/assets/templates/helpers/new-plan.sh +1 -1
  34. package/assets/templates/helpers/plan-to-todo.sh +2 -2
  35. package/assets/templates/helpers/prepare-codex-handoff.sh +2 -2
  36. package/assets/templates/helpers/prepare-handoff.sh +9 -9
  37. package/assets/templates/helpers/refresh-current-status.sh +2 -2
  38. package/assets/templates/helpers/ship-worktrees.sh +1 -1
  39. package/assets/templates/helpers/sprint-backlog.sh +1 -1
  40. package/assets/templates/helpers/verify-sprint.sh +7 -7
  41. package/assets/templates/helpers/workstream-sync.sh +1 -1
  42. package/assets/templates/plan.template.md +1 -1
  43. package/assets/workflow-contract.v1.json +6 -3
  44. package/docs/architecture/current/README.md +10 -9
  45. package/docs/architecture/current/approved-target-architecture.zh-CN.md +2 -2
  46. package/docs/architecture/current/architecture-invariants.md +2 -2
  47. package/docs/architecture/current/automation-and-schedule-engine.md +15 -15
  48. package/docs/architecture/current/codegraph-context-provider.md +2 -2
  49. package/docs/architecture/current/control-plane-authority-inventory.md +12 -0
  50. package/docs/architecture/current/dispatch-and-agent-strategy.md +11 -11
  51. package/docs/architecture/current/entity-model.md +0 -27
  52. package/docs/architecture/current/failure-recovery.md +0 -24
  53. package/docs/architecture/current/governance.md +1 -1
  54. package/docs/architecture/current/implementation-status.md +1 -1
  55. package/docs/architecture/current/legacy-governance.md +58 -0
  56. package/docs/architecture/current/multi-repository-execution.md +1 -1
  57. package/docs/architecture/current/peer-supercontroller-runtime.md +1 -1
  58. package/docs/architecture/current/personal-assistant-plugin-baseline.md +4 -5
  59. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -10
  60. package/docs/architecture/current/target-requirements-traceability.md +2 -2
  61. package/docs/architecture/current/thin-harness-governance-policy.md +1 -1
  62. package/docs/architecture/current/thin-harness-v1.md +9 -11
  63. package/docs/architecture/domains/runtime-harness.md +1 -1
  64. package/docs/forge-autonomous-goal-loop.md +3 -1
  65. package/docs/forge-chatgpt-mcp-setup.md +67 -176
  66. package/docs/images/forge-banner-cn.svg +26 -1
  67. package/docs/images/forge-banner.svg +26 -1
  68. package/docs/operations/features.md +1 -1
  69. package/docs/operations/homebrew.md +3 -3
  70. package/docs/operations/mcp-tool-exposure.md +5 -5
  71. package/docs/operations/platform-support.md +45 -37
  72. package/docs/operations/platform-support.zh-CN.md +40 -38
  73. package/docs/operations/releasing.md +3 -3
  74. package/docs/operations/releasing.zh-CN.md +3 -3
  75. package/docs/operations/troubleshooting.md +11 -6
  76. package/docs/operations/troubleshooting.zh-CN.md +11 -6
  77. package/docs/public-usage-guide.md +28 -21
  78. package/docs/public-usage-guide.zh-CN.md +28 -21
  79. package/docs/tutorials/01-install-and-start.md +58 -35
  80. package/docs/tutorials/01-install-and-start.zh-CN.md +58 -35
  81. package/docs/tutorials/02-connect-chatgpt.md +118 -46
  82. package/docs/tutorials/02-connect-chatgpt.zh-CN.md +119 -45
  83. package/docs/wiki/Architecture.md +1 -1
  84. package/docs/wiki/Installation.md +2 -2
  85. package/docs/wiki/Quick-Start.md +1 -1
  86. package/docs/wiki/Work-Lifecycle.md +1 -1
  87. package/install.ps1 +4 -5
  88. package/install.sh +4 -6
  89. package/package.json +15 -5
  90. package/scripts/archive-workflow.sh +0 -1
  91. package/scripts/benchmark-thin-harness-mcp-e2e.ts +27 -14
  92. package/scripts/build-controller-ui.ts +28 -0
  93. package/scripts/capture-plan.sh +4 -4
  94. package/scripts/check-agent-tooling.sh +12 -1
  95. package/scripts/check-platform-support.mjs +2 -2
  96. package/scripts/check-public-docs.sh +16 -5
  97. package/scripts/check-release-readiness.sh +5 -11
  98. package/scripts/check-runtime-architecture.mjs +14 -22
  99. package/scripts/check-tarball-install-smoke.sh +3 -2
  100. package/scripts/check-task-workflow.sh +2 -2
  101. package/scripts/codex-handoff-resume.sh +5 -5
  102. package/scripts/create-project-dirs.sh +3 -3
  103. package/scripts/ensure-task-workflow.sh +45 -35
  104. package/scripts/init-project.sh +4 -4
  105. package/scripts/install-desktop-operator-registration.ts +2 -2
  106. package/scripts/lib/project-init-lib.sh +46 -44
  107. package/scripts/migrate-project-template.sh +7 -4
  108. package/scripts/new-plan.sh +1 -1
  109. package/scripts/plan-to-todo.sh +2 -2
  110. package/scripts/prepare-codex-handoff.sh +2 -2
  111. package/scripts/prepare-handoff.sh +9 -9
  112. package/scripts/refresh-current-status.sh +2 -2
  113. package/scripts/setup-plugins.sh +3 -3
  114. package/scripts/ship-worktrees.sh +1 -1
  115. package/scripts/sprint-backlog.sh +5 -5
  116. package/scripts/sync-codex-installed-copies.sh +2 -3
  117. package/scripts/verify-sprint.sh +7 -7
  118. package/scripts/workstream-sync.sh +1 -1
  119. package/src/cli/chatgpt-browser/bridge-extension.ts +1 -1
  120. package/src/cli/chatgpt-browser/bridge-provider.ts +29 -0
  121. package/src/cli/chatgpt-browser/file-policy.ts +3 -1
  122. package/src/cli/commands/chatgpt.ts +51 -0
  123. package/src/cli/commands/doctor.ts +1 -1
  124. package/src/cli/commands/init-hook.ts +213 -70
  125. package/src/cli/commands/init.ts +5 -6
  126. package/src/cli/commands/mcp.ts +31 -5
  127. package/src/cli/commands/runtime.ts +61 -8
  128. package/src/cli/commands/setup-profile.ts +472 -0
  129. package/src/cli/controller/check-runner.ts +20 -3
  130. package/src/cli/controller/codex-continuation.ts +4 -7
  131. package/src/cli/controller/context-pack.ts +118 -14
  132. package/src/cli/controller/operational-plan.ts +4 -4
  133. package/src/cli/controller/task-ledger.ts +10 -10
  134. package/src/cli/controller/work-mode.ts +6 -13
  135. package/src/cli/index.ts +14 -35
  136. package/src/cli/local-bridge/connector-freshness.ts +1 -1
  137. package/src/cli/local-bridge/console-assets.ts +17 -0
  138. package/src/cli/local-bridge/console-automations.ts +160 -0
  139. package/src/cli/local-bridge/dashboard.ts +8 -1303
  140. package/src/cli/local-bridge/facade-api.ts +16 -7
  141. package/src/cli/local-bridge/frontend/src/App.tsx +10 -0
  142. package/src/cli/local-bridge/frontend/src/components/Button.tsx +1 -0
  143. package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +3 -0
  144. package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +1 -0
  145. package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +1 -0
  146. package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +1 -0
  147. package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +3 -0
  148. package/src/cli/local-bridge/frontend/src/design/icons.tsx +11 -0
  149. package/src/cli/local-bridge/frontend/src/design/tokens.css +8 -0
  150. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +3 -0
  151. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +4 -0
  152. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +3 -0
  153. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +2 -0
  154. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +2 -0
  155. package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +2 -0
  156. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +3 -0
  157. package/src/cli/local-bridge/frontend/src/lib/api.ts +23 -0
  158. package/src/cli/local-bridge/frontend/src/lib/format.ts +4 -0
  159. package/src/cli/local-bridge/frontend/src/lib/routes.ts +7 -0
  160. package/src/cli/local-bridge/frontend/src/main.tsx +1 -0
  161. package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +1 -0
  162. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +2 -0
  163. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +3 -0
  164. package/src/cli/local-bridge/frontend/src/styles.css +12 -0
  165. package/src/cli/local-bridge/frontend/src/types.ts +14 -0
  166. package/src/cli/local-bridge/frontend/src/vite-env.d.ts +1 -0
  167. package/src/cli/local-bridge/frontend/vite.config.ts +27 -0
  168. package/src/cli/local-bridge/server.ts +45 -0
  169. package/src/cli/local-bridge/ui-dist/app.css +1 -0
  170. package/src/cli/local-bridge/ui-dist/app.js +11094 -0
  171. package/src/cli/mcp/auth.ts +2 -0
  172. package/src/cli/mcp/instructions.ts +3 -3
  173. package/src/cli/mcp/legacy-tool-service.ts +27 -23
  174. package/src/cli/mcp/multi-repository.ts +2 -0
  175. package/src/cli/mcp/policy.ts +7 -5
  176. package/src/cli/mcp/repository-tools.ts +53 -14
  177. package/src/cli/mcp/server.ts +5 -10
  178. package/src/cli/mcp/setup.ts +220 -276
  179. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  180. package/src/cli/mcp/toolset-names.ts +1 -1
  181. package/src/cli/mcp/toolset.ts +0 -1
  182. package/src/cli/mcp/transports/http.ts +51 -4
  183. package/src/cli/repositories/branch-name-policy.ts +0 -7
  184. package/src/cli/repositories/controller-home.ts +110 -3
  185. package/src/cli/repositories/safe-patch.ts +1 -1
  186. package/src/cli/repositories/selected-path-actions.ts +26 -24
  187. package/src/core/adoption/gitignore-plan.ts +2 -3
  188. package/src/core/adoption/plan.ts +4 -1
  189. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +556 -0
  190. package/src/runtime/control-plane/execution/work-handle-store.ts +128 -1
  191. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +277 -0
  192. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +169 -0
  193. package/src/runtime/control-plane/facade/capability-registry.ts +0 -10
  194. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +44 -25
  195. package/src/runtime/control-plane/facade/suggested-actions.ts +7 -1
  196. package/src/runtime/control-plane/facade/types.ts +0 -1
  197. package/src/runtime/control-plane/global-scheduler/scheduler.ts +30 -8
  198. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +114 -0
  199. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +175 -0
  200. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +161 -0
  201. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +64 -0
  202. package/src/runtime/control-plane/launcher/thin-launcher.ts +117 -24
  203. package/src/runtime/control-plane/routing/route-policy.ts +14 -14
  204. package/src/runtime/control-plane/runtime-cleanup.ts +0 -1
  205. package/src/runtime/evidence/event-ledger.ts +1 -1
  206. package/src/runtime/{workflow/campaigns/workspace.ts → execution/managed-workspace.ts} +41 -43
  207. package/src/runtime/execution/process-runtime/check-facade.ts +6 -0
  208. package/src/runtime/execution/process-runtime/check-result.ts +45 -0
  209. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +42 -16
  210. package/src/runtime/execution/process-runtime/command-facade.ts +4 -2
  211. package/src/runtime/execution/process-runtime/gc.ts +1 -1
  212. package/src/runtime/execution/process-runtime/index.ts +3 -0
  213. package/src/runtime/execution/process-runtime/interactive-admission.ts +14 -5
  214. package/src/runtime/execution/process-runtime/persisted-check.ts +285 -0
  215. package/src/runtime/execution/process-runtime/resource-claims.ts +30 -0
  216. package/src/runtime/execution/process-runtime/runtime.ts +18 -1
  217. package/src/runtime/execution/process-runtime/store.ts +1 -19
  218. package/src/runtime/execution/process-runtime/types.ts +4 -0
  219. package/src/runtime/execution/thin-harness/execution-router.ts +0 -1
  220. package/src/runtime/execution/thin-harness/fast-executor.ts +1 -1
  221. package/src/runtime/execution/thin-harness/lightweight-lanes.ts +1 -2
  222. package/src/runtime/execution/thin-harness/types.ts +0 -1
  223. package/src/runtime/execution/workers/executor.ts +1 -1
  224. package/src/runtime/gateway/mcp/execution-tools.ts +1 -1
  225. package/src/runtime/gateway/mcp/persisted-check-process.ts +3 -255
  226. package/src/runtime/gateway/mcp/process-tools.ts +16 -3
  227. package/src/runtime/gateway/mcp/resource-policy.ts +0 -9
  228. package/src/runtime/gateway/mcp/router.ts +117 -182
  229. package/src/runtime/gateway/mcp/runtime-tools.ts +331 -409
  230. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +1 -219
  231. package/src/runtime/plugins/browser-adapter.ts +5 -0
  232. package/src/runtime/plugins/desktop-operator-registration.ts +1 -1
  233. package/src/runtime/plugins/store.ts +14 -6
  234. package/src/runtime/plugins/types.ts +1 -1
  235. package/src/runtime/projections/materialized-view.ts +1 -23
  236. package/src/runtime/recovery/maintenance-executor.ts +9 -0
  237. package/src/runtime/resources/ownership.ts +0 -1
  238. package/src/runtime/root/gateway-adapter.ts +2 -0
  239. package/src/runtime/root/mcp-transport.ts +10 -2
  240. package/src/runtime/root/package-connector-service.ts +136 -0
  241. package/src/runtime/root/package-runtime-release.ts +161 -0
  242. package/src/runtime/root/package-runtime-service.ts +185 -0
  243. package/src/runtime/root/release-materialize.ts +19 -0
  244. package/src/runtime/root/runtime.ts +2 -1
  245. package/src/runtime/root/scheduler.ts +5 -0
  246. package/src/runtime/root/service.ts +18 -5
  247. package/src/runtime/standalone-recovery/core.ts +65 -16
  248. package/src/runtime/workflow/findings/store.ts +145 -106
  249. package/src/runtime/workflow/findings/types.ts +22 -0
  250. package/src/runtime/workflow/schedules/engine.ts +132 -2
  251. package/src/runtime/workflow/schedules/pending-activation.ts +139 -0
  252. package/src/runtime/workflow/schedules/work-continuation.ts +218 -0
  253. package/docs/CHANGELOG.md +0 -978
  254. package/docs/README.zh-CN.md +0 -58
  255. package/scripts/smoke-chatgpt-supervised-automation.ts +0 -208
  256. package/src/cli/commands/controller.ts +0 -680
  257. package/src/runtime/workflow/campaigns/cleanup.ts +0 -263
  258. package/src/runtime/workflow/campaigns/engine.ts +0 -112
  259. package/src/runtime/workflow/campaigns/normalize.ts +0 -100
  260. package/src/runtime/workflow/campaigns/review.ts +0 -315
  261. package/src/runtime/workflow/campaigns/store.ts +0 -526
  262. package/src/runtime/workflow/campaigns/supervisor.ts +0 -112
  263. package/src/runtime/workflow/campaigns/types.ts +0 -290
  264. /package/src/runtime/{workflow/campaigns → execution}/workspace-agent.ts +0 -0
@@ -17,6 +17,7 @@ import { observeRuntimeStatus } from '../../runtime/root/status';
17
17
  import { readForgeRuntimeStatus } from '../../runtime/control-plane/runtime-status-client';
18
18
  import { evaluateActiveRuntimeSourceDrift, readRuntimeGeneration } from '../../runtime/control-plane/runtime-generation';
19
19
  import {
20
+ controllerPluginRepository,
20
21
  getAssistantPluginManifest,
21
22
  listAssistantPluginManifests,
22
23
  } from '../../runtime/plugins/store';
@@ -944,17 +945,25 @@ export function mapPluginCard(manifest: AssistantPluginManifest): PluginCardView
944
945
  }
945
946
 
946
947
  export function listConsolePlugins(ctx: ConsoleFacadeContext): PluginCardViewModel[] {
947
- return listAssistantPluginManifests(ctx.controllerHome, ctx.repository, {
948
- preferStored: true,
949
- }).map(mapPluginCard);
948
+ const repositories = [ctx.repository, controllerPluginRepository(ctx.controllerHome)];
949
+ const byPluginId = new Map<string, PluginCardViewModel>();
950
+ for (const repository of repositories) {
951
+ for (const manifest of listAssistantPluginManifests(ctx.controllerHome, repository, { preferStored: true })) {
952
+ if (!byPluginId.has(manifest.pluginId)) byPluginId.set(manifest.pluginId, mapPluginCard(manifest));
953
+ }
954
+ }
955
+ return [...byPluginId.values()].sort((left, right) => left.name.localeCompare(right.name));
950
956
  }
951
957
 
952
958
  export function getConsolePlugin(ctx: ConsoleFacadeContext, pluginId: string): PluginCardViewModel | null {
953
- try {
954
- return mapPluginCard(getAssistantPluginManifest(ctx.controllerHome, ctx.repository, pluginId));
955
- } catch {
956
- return null;
959
+ for (const repository of [ctx.repository, controllerPluginRepository(ctx.controllerHome)]) {
960
+ try {
961
+ return mapPluginCard(getAssistantPluginManifest(ctx.controllerHome, repository, pluginId));
962
+ } catch {
963
+ // Try the other supported plugin scope before reporting an unknown plugin.
964
+ }
957
965
  }
966
+ return null;
958
967
  }
959
968
 
960
969
  export function buildPluginSummary(plugins: PluginCardViewModel[]): PluginSummaryViewModel {
@@ -0,0 +1,10 @@
1
+ import{useCallback,useEffect,useState}from'react';import'./design/tokens.css';import'./styles.css';import type{AutomationView,ConsoleData,LocalToolView,ProviderView,RouteId}from'./types';import{api}from'./lib/api';import{routeFromHash}from'./lib/routes';import{AppShell}from'./shell/AppShell';import{OverviewView}from'./features/overview/OverviewView';import{AutomationsView}from'./features/automations/AutomationsView';import{WorkView}from'./features/work/WorkView';import{CapabilitiesView}from'./features/capabilities/CapabilitiesView';import{RepositoriesView}from'./features/repositories/RepositoriesView';import{SettingsView}from'./features/settings/SettingsView';import{SystemView}from'./features/system/SystemView';
2
+ async function loadData():Promise<ConsoleData>{const[commandCenter,work,automations,automationSettings,connector]=await Promise.all([api.commandCenter(),api.work(),api.automations(),api.automationSettings(),api.connector().catch(()=>undefined)]);return{commandCenter,work,automations,automationSettings,connector,generatedAt:new Date().toISOString()}}
3
+ export default function App(){const[route,setRoute]=useState<RouteId>(routeFromHash());const[data,setData]=useState<ConsoleData>();const[busy,setBusy]=useState(false);const[error,setError]=useState('');
4
+ const refresh=useCallback(async()=>{setBusy(true);setError('');try{setData(await loadData())}catch(e){setError(e instanceof Error?e.message:String(e))}finally{setBusy(false)}},[]);
5
+ useEffect(()=>{void refresh();const onHash=()=>setRoute(routeFromHash());addEventListener('hashchange',onHash);return()=>removeEventListener('hashchange',onHash)},[refresh]);
6
+ const mutate=useCallback(async(fn:()=>Promise<unknown>)=>{setBusy(true);try{await fn();setData(await loadData())}catch(e){setError(e instanceof Error?e.message:String(e))}finally{setBusy(false)}},[]);
7
+ if(!data)return <div className="boot-state"><span className="brand-mark">F</span><strong>{error?'Forge console unavailable':'Loading Forge…'}</strong>{error&&<><p>{error}</p><button className="button" onClick={()=>void refresh()}>Retry</button></>}</div>;
8
+ const common={data,busy,onRefresh:()=>void refresh()};let view;
9
+ switch(route){case'automations':view=<AutomationsView {...common} onAction={(a:AutomationView,action:'run'|'pause'|'resume')=>mutate(()=>api.automationAction(a.source,a.repoId,a.id,action))}/>;break;case'work':view=<WorkView {...common}/>;break;case'capabilities':view=<CapabilitiesView {...common}/>;break;case'repositories':view=<RepositoriesView {...common} onRegister={(p,n)=>mutate(()=>api.registerRepository(p,n))} onRemove={id=>mutate(()=>api.removeRepository(id))}/>;break;case'settings':view=<SettingsView {...common} onProviderAction={(p:ProviderView,a:'enable'|'disable')=>mutate(()=>api.providerAction(p.providerId,a))} onProviderHealth={(p:ProviderView)=>mutate(()=>api.providerHealth(p.providerId))} onToolAction={(t:LocalToolView,a:'enable'|'disable')=>mutate(()=>api.localToolAction(t.toolId,a))} onToolHealth={(t:LocalToolView)=>mutate(()=>api.localToolHealth(t.toolId))}/>;break;case'system':view=<SystemView {...common}/>;break;default:view=<OverviewView {...common}/>}
10
+ return <AppShell route={route}>{error&&<div className="global-error"><strong>Last action failed</strong><span>{error}</span><button onClick={()=>setError('')}>×</button></div>}{view}</AppShell>}
@@ -0,0 +1 @@
1
+ import type{ButtonHTMLAttributes,PropsWithChildren}from'react';export function Button({children,className='',...props}:PropsWithChildren<ButtonHTMLAttributes<HTMLButtonElement>>){return <button className={`button ${className}`.trim()} {...props}>{children}</button>}
@@ -0,0 +1,3 @@
1
+ import type{PropsWithChildren,ReactNode}from'react';
2
+ export function DetailPane({title,subtitle,actions,children,empty}:{title?:string;subtitle?:string;actions?:ReactNode;empty?:string}&PropsWithChildren){return <aside className="detail-pane">{title?<><div className="detail-head"><div><div className="eyebrow">DETAIL</div><h2>{title}</h2>{subtitle&&<p>{subtitle}</p>}</div>{actions&&<div className="detail-actions">{actions}</div>}</div><div className="detail-body">{children}</div></>:<div className="detail-empty">{empty??'选择一项查看详细配置'}</div>}</aside>}
3
+ export function DefinitionList({items}:{items:Array<[string,ReactNode]>}){return <dl className="definition-list">{items.map(([k,v])=><div key={k}><dt>{k}</dt><dd>{v}</dd></div>)}</dl>}
@@ -0,0 +1 @@
1
+ import{SearchIcon}from'../design/icons';export function SearchField({value,onChange,placeholder='Search…'}:{value:string;onChange:(v:string)=>void;placeholder?:string}){return <label className="search-field"><SearchIcon/><input value={value} onChange={e=>onChange(e.target.value)} placeholder={placeholder}/></label>}
@@ -0,0 +1 @@
1
+ import type{ReactNode}from'react';export function SectionHeader({title,meta,actions}:{title:string;meta?:ReactNode;actions?:ReactNode}){return <div className="section-header"><div><h2>{title}</h2>{meta&&<span>{meta}</span>}</div>{actions&&<div>{actions}</div>}</div>}
@@ -0,0 +1 @@
1
+ export interface Segment<T extends string>{id:T;label:string;count?:number}export function Segmented<T extends string>({items,value,onChange}:{items:Segment<T>[];value:T;onChange:(v:T)=>void}){return <div className="segmented" role="tablist">{items.map(i=><button key={i.id} role="tab" aria-selected={value===i.id} className={value===i.id?'selected':''} onClick={()=>onChange(i.id)}>{i.label}{i.count!==undefined&&<span>{i.count}</span>}</button>)}</div>}
@@ -0,0 +1,3 @@
1
+ export type StatusTone='success'|'warning'|'danger'|'info'|'neutral';
2
+ export function toneFor(value?:string):StatusTone{const s=(value??'').toLowerCase();if(/ready|enabled|healthy|success|done|completed|active/.test(s))return'success';if(/attention|blocked|error|fail|danger/.test(s))return'danger';if(/pause|waiting|warn|degrad|stale|planned/.test(s))return'warning';if(/info|running/.test(s))return'info';return'neutral';}
3
+ export function StatusText({label,tone}:{label:string;tone?:StatusTone|string}){const t=(tone&&['success','warning','danger','info','neutral'].includes(tone)?tone:toneFor(tone??label))as StatusTone;return <span className="status-text"><i className={`status-dot ${t}`}/>{label}</span>}
@@ -0,0 +1,11 @@
1
+ import type { SVGProps } from 'react';
2
+ function Icon({children,...props}:SVGProps<SVGSVGElement>){return <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.55" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}>{children}</svg>}
3
+ export const OverviewIcon=(p:SVGProps<SVGSVGElement>)=><Icon {...p}><path d="M3 9.2 10 3l7 6.2v7.1a.7.7 0 0 1-.7.7h-4.2v-5H7.9v5H3.7a.7.7 0 0 1-.7-.7z"/></Icon>;
4
+ export const AutomationIcon=(p:SVGProps<SVGSVGElement>)=><Icon {...p}><path d="M4.2 6.3A6.5 6.5 0 0 1 16 7"/><path d="m16 3 .4 4.4-4.4.4"/><path d="M15.8 13.7A6.5 6.5 0 0 1 4 13"/><path d="m4 17-.4-4.4 4.4-.4"/></Icon>;
5
+ export const WorkIcon=(p:SVGProps<SVGSVGElement>)=><Icon {...p}><path d="M4 5.2h12v10.6H4z"/><path d="M7 5.2V3.6h6v1.6M7 9h6M7 12h4"/></Icon>;
6
+ export const CapabilityIcon=(p:SVGProps<SVGSVGElement>)=><Icon {...p}><path d="m10 2.8 2 4 4.4.7-3.2 3.1.8 4.4-4-2.1L6 15l.8-4.4-3.2-3.1L8 6.8z"/></Icon>;
7
+ export const RepoIcon=(p:SVGProps<SVGSVGElement>)=><Icon {...p}><path d="M4 3.5h5l1.4 2H16v11H4z"/><path d="M4 8h12"/></Icon>;
8
+ export const SettingsIcon=(p:SVGProps<SVGSVGElement>)=><Icon {...p}><circle cx="10" cy="10" r="2.5"/><path d="M10 2.8v2M10 15.2v2M2.8 10h2M15.2 10h2M4.9 4.9l1.4 1.4M13.7 13.7l1.4 1.4M15.1 4.9l-1.4 1.4M6.3 13.7l-1.4 1.4"/></Icon>;
9
+ export const SystemIcon=(p:SVGProps<SVGSVGElement>)=><Icon {...p}><path d="M3.2 4.5h13.6v9.2H3.2z"/><path d="M7 17h6M10 13.7V17"/></Icon>;
10
+ export const RefreshIcon=(p:SVGProps<SVGSVGElement>)=><Icon {...p}><path d="M15.5 6A6 6 0 1 0 16 12"/><path d="m15.5 2.8.3 3.7-3.7.2"/></Icon>;
11
+ export const SearchIcon=(p:SVGProps<SVGSVGElement>)=><Icon {...p}><circle cx="8.8" cy="8.8" r="5"/><path d="m12.5 12.5 4 4"/></Icon>;
@@ -0,0 +1,8 @@
1
+ :root{
2
+ --canvas:#F5F5F3;--surface:#FFFFFF;--surface-subtle:#F0F0EE;--surface-strong:#E8E8E5;
3
+ --rail:#19191B;--rail-hover:#242427;--rail-selected:#303037;--ink:#1D1D20;--muted:#646469;--faint:#909096;--inverse:#F7F7F8;
4
+ --border:#E1E1DE;--border-strong:#CECECA;--accent:#5E54E8;--accent-hover:#5046D9;--accent-soft:#EFEEFF;
5
+ --success:#237A57;--success-soft:#EAF5EF;--warning:#946200;--warning-soft:#FFF4D8;--danger:#B44444;--danger-soft:#FBECEC;--info:#3567B7;--focus:#786FF7;
6
+ --font-sans:Inter,-apple-system,BlinkMacSystemFont,"SF Pro Text","PingFang SC","Segoe UI",sans-serif;--font-mono:"SFMono-Regular",Consolas,"Liberation Mono",monospace;
7
+ --r-xs:4px;--r-sm:6px;--r-md:8px;--r-lg:10px;--rail-width:224px;--content-max:1280px;
8
+ }
@@ -0,0 +1,3 @@
1
+ import{useMemo,useState}from'react';import type{AutomationView,ConsoleData}from'../../types';import{CommandBar}from'../../shell/CommandBar';import{Segmented}from'../../components/Segmented';import{DetailPane,DefinitionList}from'../../components/DetailPane';import{StatusText}from'../../components/StatusText';import{Button}from'../../components/Button';import{formatDate,compact}from'../../lib/format';
2
+ type Filter='enabled'|'paused'|'attention'|'all';
3
+ export function AutomationsView({data,busy,onRefresh,onAction}:{data:ConsoleData;busy:boolean;onRefresh:()=>void;onAction:(a:AutomationView,action:'run'|'pause'|'resume')=>Promise<void>}){const all=data.automations.automations;const[filter,setFilter]=useState<Filter>('enabled');const list=useMemo(()=>all.filter(a=>filter==='all'||a.status===filter),[all,filter]);const[selectedKey,setSelectedKey]=useState<string>();const key=(a:AutomationView)=>`${a.source}:${a.repoId}:${a.id}`;const selected=list.find(a=>key(a)===selectedKey)??list[0];return <><CommandBar eyebrow="LONG-RUNNING CONFIG" title="Automations" description="管理 Forge 持久化 Schedule 与 Assistant Routine;结果正文继续发送到 ChatGPT / Email。" refreshedAt={data.automations.generatedAt} busy={busy} onRefresh={onRefresh}/><div className="toolbar"><Segmented value={filter} onChange={setFilter} items={[{id:'enabled',label:'Enabled',count:all.filter(x=>x.status==='enabled').length},{id:'paused',label:'Paused',count:all.filter(x=>x.status==='paused').length},{id:'attention',label:'Attention',count:all.filter(x=>x.status==='attention').length},{id:'all',label:'All',count:all.length}]}/></div><div className="split-workspace automation-layout"><div className="table-wrap"><table className="data-table"><thead><tr><th>Automation</th><th>Schedule</th><th>Delivery</th><th>Status</th><th>Last</th><th>Next</th></tr></thead><tbody>{list.map(a=><tr key={key(a)} className={selected&&key(selected)===key(a)?'selected':''} onClick={()=>setSelectedKey(key(a))}><td><strong>{a.name}</strong><small>{a.repositoryName} · {a.source}</small></td><td>{a.schedule}</td><td>{a.delivery??'—'}</td><td><StatusText label={a.status} tone={a.status}/></td><td><span>{compact(a.lastResult,30)||'—'}</span><small>{formatDate(a.lastRunAt)}</small></td><td>{formatDate(a.nextRunHint)}</td></tr>)}</tbody></table>{!list.length&&<div className="quiet-empty">这个筛选条件下没有 Automation。</div>}</div><DetailPane title={selected?.name} subtitle={selected?.summary} empty="选择一个 Automation 查看配置">{selected&&<><DefinitionList items={[["Status",<StatusText label={selected.status} tone={selected.status}/>],["Schedule",selected.schedule],["Source",selected.source],["Repository",selected.repositoryName],["Delivery",selected.delivery??'—'],["Last result",selected.lastResult??'—'],["Last run",formatDate(selected.lastRunAt)],["Next",formatDate(selected.nextRunHint)]]}/>{selected.pausedReason&&<div className="detail-callout warning"><strong>Paused reason</strong><p>{selected.pausedReason}</p></div>}<div className="detail-button-row">{selected.actions.map(action=><Button key={action} disabled={busy} className={action==='pause'?'danger-text':''} onClick={()=>void onAction(selected,action)}>{action==='run'?'Run now':action==='pause'?'Pause':'Resume'}</Button>)}</div><p className="detail-note">这里只保存调度配置与最近一次结果摘要,不复制日报、SEO 或研究正文。</p></>}</DetailPane></div></>}
@@ -0,0 +1,4 @@
1
+ import{useMemo,useState}from'react';import type{ConsoleData,PluginView}from'../../types';import{CommandBar}from'../../shell/CommandBar';import{Segmented}from'../../components/Segmented';import{DetailPane,DefinitionList}from'../../components/DetailPane';import{StatusText}from'../../components/StatusText';import{compact,pretty}from'../../lib/format';
2
+ type Filter='all'|'extensions'|'services'|'execution'|'models';
3
+ function category(p:PluginView):Exclude<Filter,'all'|'models'>{const s=`${p.name} ${p.provider} ${(p.capabilityLabels??[]).join(' ')}`.toLowerCase();if(/gmail|calendar|github|google task|notion/.test(s))return'services';if(/browser|desktop|ios|repository|codegraph|local/.test(s))return'execution';return'extensions'}
4
+ export function CapabilitiesView({data,busy,onRefresh}:{data:ConsoleData;busy:boolean;onRefresh:()=>void}){const plugins=data.commandCenter.plugins??[],providers=data.automationSettings.providers??[];const[filter,setFilter]=useState<Filter>('all');const[selectedId,setSelectedId]=useState<string>();const filtered=useMemo(()=>plugins.filter(p=>filter==='all'||filter==='models'||category(p)===filter),[plugins,filter]);const selected=filtered.find(x=>x.id===selectedId)??filtered[0];return <><CommandBar eyebrow="CAPABILITY CATALOG" title="Capabilities" description="从“Forge 能做什么”查看扩展、服务、执行能力和模型,而不是浏览 MCP tool 清单。" refreshedAt={data.generatedAt} busy={busy} onRefresh={onRefresh}/><div className="toolbar"><Segmented value={filter} onChange={setFilter} items={[{id:'all',label:'All',count:plugins.length+providers.length},{id:'extensions',label:'Extensions',count:plugins.filter(x=>category(x)==='extensions').length},{id:'services',label:'Services',count:plugins.filter(x=>category(x)==='services').length},{id:'execution',label:'Execution',count:plugins.filter(x=>category(x)==='execution').length},{id:'models',label:'Models',count:providers.length}]}/></div>{filter==='models'?<div className="single-list"><div className="scan-list">{providers.map(p=><div className="scan-row static" key={p.providerId}><div className="scan-main"><strong>{p.displayName}</strong><p>{compact(p.explanation??p.summary,110)}</p></div><StatusText label={p.statusLabel??p.status??'Unknown'} tone={p.status}/></div>)}</div></div>:<div className="split-workspace"><div className="scan-list">{filtered.map(p=><button key={p.id} className={`scan-row ${selected?.id===p.id?'selected':''}`} onClick={()=>setSelectedId(p.id)}><div className="scan-main"><span className="row-eyebrow">{category(p).toUpperCase()}</span><strong>{p.name}</strong><p>{compact(p.description,100)}</p></div><StatusText label={p.statusLabel??p.status??'Unknown'} tone={p.status??p.tone}/></button>)}</div><DetailPane title={selected?.name} subtitle={selected?.description}>{selected&&<><DefinitionList items={[["Status",<StatusText label={selected.statusLabel??selected.status??'Unknown'} tone={selected.status??selected.tone}/>],["Provider",selected.provider??'—'],["Health",selected.healthLabel??'—'],["Lifecycle",selected.lifecycleLabel??'—']]}/>{selected.nextStep&&<div className="detail-callout warning"><strong>Next step</strong><p>{selected.nextStep}</p></div>}{(selected.capabilityLabels??[]).length>0&&<div className="capability-lines">{selected.capabilityLabels!.map(x=><span key={x}>{x}</span>)}</div>}{(selected.warnings??[]).map(w=><div className="detail-callout warning" key={w}>{w}</div>)}<details className="advanced"><summary>Advanced · actions & protocol</summary><pre>{pretty({actions:selected.actions,advanced:selected.advanced})}</pre></details></>}</DetailPane></div>}</>}
@@ -0,0 +1,3 @@
1
+ import type{ConsoleData,Dict}from'../../types';import{CommandBar}from'../../shell/CommandBar';import{StatusText}from'../../components/StatusText';import{SectionHeader}from'../../components/SectionHeader';import{compact}from'../../lib/format';
2
+ function attentionKey(h:Dict){return`${String(h.title??'')}:${String(h.reason??'')}`}
3
+ export function OverviewView({data,busy,onRefresh}:{data:ConsoleData;busy:boolean;onRefresh:()=>void}){const cc=data.commandCenter,a=data.automations.summary,w=data.work,p=cc.pluginSummary??{},repos=cc.repositories??[],readiness=cc.readiness??{};const unique=[...(cc.handoffs??[])].filter((h,i,all)=>all.findIndex(x=>attentionKey(x)===attentionKey(h))===i);const workAttention=w.waitingForUserCount??0;const attention=[...unique.slice(0,3),...(workAttention?[{title:'Persistent work needs attention',reason:`${workAttention} 项长期目标需要确认`,statusLabel:'Review',tone:'warning'}]:[]),...((p.needsAttention??0)?[{title:'Capabilities need attention',reason:`${p.needsAttention} 项能力需要配置或检查`,statusLabel:'Inspect',tone:'warning'}]:[])].slice(0,4)as Dict[];return <><CommandBar eyebrow="FORGE" title="Overview" description="长期配置与系统可用性。执行结果和通知继续由 ChatGPT 承担。" refreshedAt={data.generatedAt} busy={busy} onRefresh={onRefresh}/><div className="metric-strip"><a href="#/automations"><span>Automations</span><strong>{a.enabled}</strong><small>{a.paused?`${a.paused} paused`:'enabled'}</small></a><a href="#/capabilities"><span>Capabilities</span><strong>{p.ready??0}/{p.total??(cc.plugins??[]).length}</strong><small>ready</small></a><a href="#/repositories"><span>Repositories</span><strong>{repos.length}</strong><small>registered</small></a><a href="#/work"><span>Persistent work</span><strong>{w.activeRequirementCount??0}</strong><small>{w.waitingForUserCount??0} waiting</small></a></div><section className="page-section"><SectionHeader title="Needs attention" meta={attention.length?`${attention.length} actionable groups`:'All clear'}/>{attention.length?<div className="attention-list">{attention.map((h,i)=><div className="attention-row" key={`${String(h.title)}-${i}`}><span className="attention-index">{String(i+1).padStart(2,'0')}</span><div><strong>{String(h.title??'需要处理')}</strong><p>{compact(String(h.reason??'请在 ChatGPT 中确认'),120)}</p></div><StatusText label={String(h.statusLabel??'Review')} tone={String(h.tone??'warning')}/></div>)}</div>:<div className="quiet-empty"><StatusText label={String(readiness.label??readiness.headline??'Ready')} tone={String(readiness.state??'success')}/><p>当前没有需要你在控制台处理的配置问题。</p></div>}</section><section className="page-section"><SectionHeader title="Configured work" meta="只显示持久化目标,不展示执行流水"/><div className="compact-work-list">{(w.requirements??[]).filter(x=>x.state!=='done'&&x.state!=='cancelled').slice(0,5).map(x=><a href="#/work" key={x.requirementId}><div><strong>{x.title}</strong><p>{compact(x.outcome,100)}</p></div><StatusText label={x.state==='waiting_for_user'?'Waiting':x.state==='planned'?'Planned':'Active'} tone={x.state}/></a>)}</div></section></>}
@@ -0,0 +1,2 @@
1
+ import{useMemo,useState}from'react';import type{ConsoleData}from'../../types';import{CommandBar}from'../../shell/CommandBar';import{SearchField}from'../../components/SearchField';import{DetailPane,DefinitionList}from'../../components/DetailPane';import{StatusText}from'../../components/StatusText';import{Button}from'../../components/Button';import{compact,pretty}from'../../lib/format';
2
+ export function RepositoriesView({data,busy,onRefresh,onRegister,onRemove}:{data:ConsoleData;busy:boolean;onRefresh:()=>void;onRegister:(path:string,name?:string)=>Promise<void>;onRemove:(id:string)=>Promise<void>}){const all=data.commandCenter.repositories??[];const[q,setQ]=useState('');const[selectedId,setSelectedId]=useState<string>();const[path,setPath]=useState('');const[name,setName]=useState('');const list=useMemo(()=>all.filter(r=>`${r.name} ${r.path} ${r.branchLabel}`.toLowerCase().includes(q.toLowerCase())),[all,q]);const selected=list.find(x=>x.id===selectedId)??list[0];return <><CommandBar eyebrow="CONTROLLER REGISTRY" title="Repositories" description="管理 Forge 已注册仓库;不存在隐含的全局“当前仓库”。" refreshedAt={data.generatedAt} busy={busy} onRefresh={onRefresh}/><div className="repository-tools"><SearchField value={q} onChange={setQ} placeholder="Search repositories…"/><form onSubmit={e=>{e.preventDefault();if(path.trim())void onRegister(path.trim(),name.trim()||undefined).then(()=>{setPath('');setName('')})}}><input value={path} onChange={e=>setPath(e.target.value)} placeholder="/absolute/path"/><input value={name} onChange={e=>setName(e.target.value)} placeholder="Display name (optional)"/><Button disabled={busy||!path.trim()}>Add</Button></form></div><div className="split-workspace"><div className="scan-list">{list.map(r=><button key={r.id} className={`scan-row ${selected?.id===r.id?'selected':''}`} onClick={()=>setSelectedId(r.id)}><div className="scan-main"><strong>{r.name}</strong><p>{compact(r.path,100)}</p></div><div className="scan-meta"><StatusText label={r.readinessLabel??r.statusLabel??'Registered'} tone={r.readinessLabel??r.statusLabel}/><span>{r.branchLabel??'—'}</span></div></button>)}</div><DetailPane title={selected?.name} subtitle={selected?.path}>{selected&&<><DefinitionList items={[["Repository id",<code>{selected.id}</code>],["Branch",selected.branchLabel??'—'],["Working tree",selected.dirtyLabel??'—'],["Readiness",<StatusText label={selected.readinessLabel??selected.statusLabel??'Registered'} tone={selected.readinessLabel??selected.statusLabel}/>]]}/><details className="advanced"><summary>Advanced registry metadata</summary><pre>{pretty(selected.advanced)}</pre></details><div className="detail-button-row"><Button className="danger-text" disabled={busy} onClick={()=>void onRemove(selected.id)}>Remove registry entry</Button></div></>}</DetailPane></div></>}
@@ -0,0 +1,2 @@
1
+ import type{ConsoleData,LocalToolView,ProviderView}from'../../types';import{CommandBar}from'../../shell/CommandBar';import{StatusText}from'../../components/StatusText';import{Button}from'../../components/Button';import{SectionHeader}from'../../components/SectionHeader';import{compact,pretty}from'../../lib/format';
2
+ export function SettingsView({data,busy,onRefresh,onProviderAction,onProviderHealth,onToolAction,onToolHealth}:{data:ConsoleData;busy:boolean;onRefresh:()=>void;onProviderAction:(p:ProviderView,a:'enable'|'disable')=>Promise<void>;onProviderHealth:(p:ProviderView)=>Promise<void>;onToolAction:(t:LocalToolView,a:'enable'|'disable')=>Promise<void>;onToolHealth:(t:LocalToolView)=>Promise<void>}){const s=data.automationSettings;return <><CommandBar eyebrow="LONG-LIVED CONFIG" title="Settings" description="模型、Provider 与本地工具的长期默认配置。Automation 调度不在这里。" refreshedAt={data.generatedAt} busy={busy} onRefresh={onRefresh}/><div className="settings-column">{(s.warnings??[]).map(w=><div className="detail-callout warning" key={w}>{w}</div>)}<section><SectionHeader title="Models & routing" meta={`${s.providers?.length??0} providers`}/><div className="settings-list">{(s.providers??[]).map(p=><div className="settings-row" key={p.providerId}><div><strong>{p.displayName}</strong><p>{compact(p.explanation??p.summary,120)}</p></div><StatusText label={p.statusLabel??p.status??'Unknown'} tone={p.status}/><div className="settings-actions"><Button disabled={busy} onClick={()=>void onProviderHealth(p)}>Check</Button><Button disabled={busy} onClick={()=>void onProviderAction(p,p.enabled===false?'enable':'disable')}>{p.enabled===false?'Enable':'Disable'}</Button></div></div>)}</div></section><section><SectionHeader title="Local tools" meta={`${s.localTools?.length??0} configured`}/><div className="settings-list">{(s.localTools??[]).map(t=><div className="settings-row" key={t.toolId}><div><strong>{t.displayName}</strong><p>{compact(t.summary,120)}</p></div><StatusText label={t.status??(t.enabled===false?'Disabled':'Configured')} tone={t.status}/><div className="settings-actions"><Button disabled={busy} onClick={()=>void onToolHealth(t)}>Check</Button><Button disabled={busy} onClick={()=>void onToolAction(t,t.enabled===false?'enable':'disable')}>{t.enabled===false?'Enable':'Disable'}</Button></div></div>)}</div></section><details className="advanced"><summary>Advanced routing & credentials metadata</summary><pre>{pretty({routing:s.routing,credentials:s.credentials,overview:s.overview})}</pre></details></div></>}
@@ -0,0 +1,2 @@
1
+ import{useState}from'react';import type{ConsoleData,Dict}from'../../types';import{CommandBar}from'../../shell/CommandBar';import{DefinitionList}from'../../components/DetailPane';import{StatusText}from'../../components/StatusText';import{pretty,asText}from'../../lib/format';import{api}from'../../lib/api';
2
+ export function SystemView({data,busy,onRefresh}:{data:ConsoleData;busy:boolean;onRefresh:()=>void}){const[advanced,setAdvanced]=useState<Dict>();const r=data.commandCenter.readiness??{};return <><CommandBar eyebrow="MAINTENANCE" title="System" description="低频工程维护入口。正常使用 Forge 不需要理解这里的运行时细节。" refreshedAt={data.generatedAt} busy={busy} onRefresh={onRefresh}/><div className="system-layout"><section className="system-summary"><div className="system-posture"><div><span className="eyebrow">SYSTEM POSTURE</span><h2>{asText(r.label??r.headline,'Controller state')}</h2><p>{asText(r.explanation??r.summary,'Controller and connector status')}</p></div><StatusText label={asText(r.state,'Unknown')} tone={asText(r.state)}/></div><DefinitionList items={[["Controller",asText(r.label??r.headline,'—')],["Connector",asText((data.connector as Dict|undefined)?.status,'—')],["Repositories",String(data.commandCenter.repositories?.length??0)],["Plugins",String(data.commandCenter.plugins?.length??0)]]}/></section><section><button className="text-button" onClick={()=>void api.advanced().then(setAdvanced)}>Load advanced diagnostics</button>{advanced&&<details className="advanced" open><summary>Advanced diagnostics</summary><pre>{pretty(advanced)}</pre></details>}</section></div></>}
@@ -0,0 +1,3 @@
1
+ import{useMemo,useState}from'react';import type{ConsoleData,TrackedWorkView}from'../../types';import{CommandBar}from'../../shell/CommandBar';import{Segmented}from'../../components/Segmented';import{DetailPane,DefinitionList}from'../../components/DetailPane';import{StatusText}from'../../components/StatusText';import{formatDate,compact}from'../../lib/format';
2
+ type Filter='active'|'waiting'|'completed'|'all';function match(w:TrackedWorkView,f:Filter){if(f==='all')return true;if(f==='waiting')return w.state==='waiting_for_user';if(f==='completed')return w.state==='done'||w.state==='cancelled';return w.state==='active'||w.state==='planned'}
3
+ export function WorkView({data,busy,onRefresh}:{data:ConsoleData;busy:boolean;onRefresh:()=>void}){const all=data.work.requirements??[];const[filter,setFilter]=useState<Filter>('active');const list=useMemo(()=>all.filter(w=>match(w,filter)),[all,filter]);const[selectedId,setSelectedId]=useState<string>();const selected=list.find(x=>x.requirementId===selectedId)??list[0];return <><CommandBar eyebrow="PERSISTENT GOALS" title="Work" description="粗粒度查看长期目标;不展示 Agent 步骤、Run 流水或推测进度。" refreshedAt={data.generatedAt} busy={busy} onRefresh={onRefresh}/><div className="toolbar"><Segmented value={filter} onChange={setFilter} items={[{id:'active',label:'Open',count:all.filter(x=>match(x,'active')).length},{id:'waiting',label:'Waiting',count:all.filter(x=>match(x,'waiting')).length},{id:'completed',label:'Completed',count:all.filter(x=>match(x,'completed')).length},{id:'all',label:'All',count:all.length}]}/></div><div className="split-workspace"><div className="scan-list">{list.map(w=><button key={w.requirementId} className={`scan-row ${selected?.requirementId===w.requirementId?'selected':''}`} onClick={()=>setSelectedId(w.requirementId)}><div className="scan-main"><strong>{w.title}</strong><p>{compact(w.outcome||w.blocker,96)}</p></div><div className="scan-meta"><StatusText label={w.state==='waiting_for_user'?'Waiting':w.state==='done'?'Completed':w.state==='planned'?'Planned':'Active'} tone={w.state}/><time>{formatDate(w.updatedAt)}</time></div></button>)}{!list.length&&<div className="quiet-empty">这个筛选条件下没有持久化目标。</div>}</div><DetailPane title={selected?.title} subtitle={selected?.outcome} empty="选择一个长期目标查看完整上下文">{selected&&<><DefinitionList items={[["State",<StatusText label={selected.state} tone={selected.state}/>],["Updated",formatDate(selected.updatedAt)],["Requirement",<code>{selected.requirementId}</code>]]}/>{selected.blocker&&<div className="detail-callout warning"><strong>Blocker</strong><p>{selected.blocker}</p></div>}{selected.requiredUserDecision&&<div className="detail-callout danger"><strong>Decision required</strong><p>{selected.requiredUserDecision}</p></div>}<p className="detail-note">执行步骤、检查证据和 Run 细节不在这里展开;继续工作请回到 ChatGPT。</p></>}</DetailPane></div></>}
@@ -0,0 +1,23 @@
1
+ import type { AutomationSettingsView, AutomationsResponse, CommandCenterView, Dict, WorkResponse } from '../types';
2
+ export class ApiError extends Error { constructor(message:string, readonly status:number, readonly payload:unknown){super(message);} }
3
+ export async function requestJson<T>(path:string, init:RequestInit={}):Promise<T>{
4
+ const headers=new Headers(init.headers); if(init.body&&!headers.has('content-type'))headers.set('content-type','application/json');
5
+ const response=await fetch(path,{...init,headers,credentials:'same-origin'}); let payload:unknown={}; try{payload=await response.json();}catch{}
6
+ if(!response.ok){const r=payload&&typeof payload==='object'?payload as Dict:{}; const message=typeof r.error==='string'?r.error:typeof r.message==='string'?r.message:`Request failed (${response.status})`; throw new ApiError(message,response.status,payload);}
7
+ return payload as T;
8
+ }
9
+ export const api={
10
+ commandCenter:()=>requestJson<CommandCenterView>('/api/console/command-center'),
11
+ work:()=>requestJson<WorkResponse>('/api/console/requirements'),
12
+ automations:()=>requestJson<AutomationsResponse>('/api/console/automations'),
13
+ automationSettings:()=>requestJson<AutomationSettingsView>('/api/console/automation-settings'),
14
+ connector:()=>requestJson<Dict>('/api/console/connector/status'),
15
+ advanced:()=>requestJson<Dict>('/api/console/advanced'),
16
+ automationAction:(source:string,repoId:string,id:string,action:string)=>requestJson<Dict>(`/api/console/automations/${encodeURIComponent(source)}/${encodeURIComponent(repoId)}/${encodeURIComponent(id)}/${encodeURIComponent(action)}`,{method:'POST',body:'{}'}),
17
+ providerAction:(id:string,action:'enable'|'disable')=>requestJson<Dict>(`/api/console/providers/${encodeURIComponent(id)}/${action}`,{method:'POST',body:'{}'}),
18
+ providerHealth:(id:string)=>requestJson<Dict>('/api/console/providers/health',{method:'POST',body:JSON.stringify({providerId:id})}),
19
+ localToolAction:(id:string,action:'enable'|'disable')=>requestJson<Dict>(`/api/console/local-tools/${encodeURIComponent(id)}/${action}`,{method:'POST',body:'{}'}),
20
+ localToolHealth:(id:string)=>requestJson<Dict>('/api/console/local-tools/health',{method:'POST',body:JSON.stringify({toolId:id})}),
21
+ registerRepository:(path:string,displayName?:string)=>requestJson<Dict>('/api/repositories/register',{method:'POST',body:JSON.stringify({path,displayName})}),
22
+ removeRepository:(id:string)=>requestJson<Dict>(`/api/repositories/${encodeURIComponent(id)}/remove`,{method:'POST',body:'{}'}),
23
+ };
@@ -0,0 +1,4 @@
1
+ export function formatDate(value?:string):string{if(!value)return '—';const d=new Date(value);if(Number.isNaN(d.getTime()))return value;return new Intl.DateTimeFormat('zh-CN',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',hour12:false}).format(d);}
2
+ export function compact(value?:string,max=86):string{const s=(value??'').trim();return s.length>max?`${s.slice(0,max-1)}…`:s;}
3
+ export function asText(value:unknown,fallback='—'):string{return typeof value==='string'&&value.trim()?value:String(value??fallback);}
4
+ export function pretty(value:unknown):string{return JSON.stringify(value??{},null,2);}
@@ -0,0 +1,7 @@
1
+ import type { RouteId } from '../types';
2
+ export const routes:{id:RouteId;label:string;group:'daily'|'manage'|'system'}[]=[
3
+ {id:'overview',label:'Overview',group:'daily'}, {id:'automations',label:'Automations',group:'daily'}, {id:'work',label:'Work',group:'daily'},
4
+ {id:'capabilities',label:'Capabilities',group:'manage'}, {id:'repositories',label:'Repositories',group:'manage'}, {id:'settings',label:'Settings',group:'manage'},
5
+ {id:'system',label:'System',group:'system'},
6
+ ];
7
+ export function routeFromHash():RouteId{const id=location.hash.replace(/^#\/?/,'').split('/')[0] as RouteId;return routes.some(r=>r.id===id)?id:'overview';}
@@ -0,0 +1 @@
1
+ import{StrictMode}from'react';import{createRoot}from'react-dom/client';import App from'./App';const root=document.getElementById('app');if(!root)throw new Error('Forge console root missing');createRoot(root).render(<StrictMode><App/></StrictMode>);
@@ -0,0 +1 @@
1
+ import type{PropsWithChildren}from'react';import type{RouteId}from'../types';import{Sidebar}from'./Sidebar';export function AppShell({route,children}:PropsWithChildren<{route:RouteId}>){return <div className="app-shell"><Sidebar route={route}/><main className="workspace">{children}</main></div>}
@@ -0,0 +1,2 @@
1
+ import type{ReactNode}from'react';import{RefreshIcon}from'../design/icons';import{formatDate}from'../lib/format';
2
+ export function CommandBar({eyebrow,title,description,refreshedAt,busy,onRefresh,actions}:{eyebrow?:string;title:string;description:string;refreshedAt?:string;busy?:boolean;onRefresh:()=>void;actions?:ReactNode}){return <header className="command-bar"><div className="command-title">{eyebrow&&<div className="eyebrow">{eyebrow}</div>}<h1>{title}</h1><p>{description}</p></div><div className="command-actions"><span className="refreshed">Updated {formatDate(refreshedAt)}</span><button className="icon-button" onClick={onRefresh} disabled={busy} title="Refresh"><RefreshIcon/></button>{actions}<a className="button ghost-link" href="https://chatgpt.com" target="_blank" rel="noreferrer">ChatGPT ↗</a></div></header>}
@@ -0,0 +1,3 @@
1
+ import type{ComponentType,SVGProps}from'react';import type{RouteId}from'../types';import{routes}from'../lib/routes';import{AutomationIcon,CapabilityIcon,OverviewIcon,RepoIcon,SettingsIcon,SystemIcon,WorkIcon}from'../design/icons';
2
+ const icons:Record<RouteId,ComponentType<SVGProps<SVGSVGElement>>>={overview:OverviewIcon,automations:AutomationIcon,work:WorkIcon,capabilities:CapabilityIcon,repositories:RepoIcon,settings:SettingsIcon,system:SystemIcon};
3
+ export function Sidebar({route}:{route:RouteId}){let last='';return <aside className="sidebar"><div className="brand"><span className="brand-mark">F</span><div><strong>Forge</strong><small>Utility Console</small></div></div><nav>{routes.map(item=>{const Icon=icons[item.id];const sep=Boolean(last&&last!==item.group);last=item.group;return <div key={item.id} className={sep?'nav-group-start':''}><a href={`#/${item.id}`} className={route===item.id?'active':''}><Icon/><span>{item.label}</span></a></div>})}</nav><div className="sidebar-foot"><span className="pulse-dot"/>ChatGPT is the primary workspace</div></aside>}
@@ -0,0 +1,12 @@
1
+ *{box-sizing:border-box}html,body,#app{min-height:100%;margin:0}body{font-family:var(--font-sans);font-size:13px;color:var(--ink);background:var(--canvas);-webkit-font-smoothing:antialiased}button,input{font:inherit}button,a{transition:background-color .14s,border-color .14s,color .14s,opacity .14s}a{color:inherit;text-decoration:none}code,pre{font-family:var(--font-mono)}button:focus-visible,a:focus-visible,input:focus-visible,summary:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.app-shell{min-height:100vh;display:grid;grid-template-columns:var(--rail-width) minmax(0,1fr)}
2
+ .sidebar{position:sticky;top:0;height:100vh;background:var(--rail);color:#B8B8BF;padding:18px 12px 14px;display:flex;flex-direction:column}.brand{height:50px;display:flex;align-items:center;gap:10px;padding:0 8px;color:var(--inverse)}.brand-mark{display:grid;place-items:center;width:26px;height:26px;border-radius:7px;background:var(--accent);color:white;font-weight:750}.brand strong{display:block;font-size:14px}.brand small{display:block;color:#77777F;font-size:10px;margin-top:1px}.sidebar nav{margin-top:18px;display:grid;gap:2px}.sidebar nav>div.nav-group-start{margin-top:13px;padding-top:13px;border-top:1px solid #2A2A2E}.sidebar nav a{display:flex;height:34px;align-items:center;gap:10px;padding:0 9px;border-radius:6px;color:#9C9CA4}.sidebar nav a:hover{background:var(--rail-hover);color:#DCDCE0}.sidebar nav a.active{background:var(--rail-selected);color:var(--inverse);box-shadow:inset 2px 0 var(--accent)}.sidebar nav svg{width:16px;height:16px}.sidebar-foot{margin-top:auto;border-top:1px solid #2A2A2E;padding:13px 8px 0;color:#75757D;font-size:10px;display:flex;align-items:center;gap:7px}.pulse-dot{width:6px;height:6px;border-radius:50%;background:#5CC58A}
3
+ .workspace{width:100%;max-width:calc(var(--content-max) + 80px);padding:0 40px 64px;min-width:0}.command-bar{min-height:126px;display:flex;justify-content:space-between;gap:28px;align-items:flex-end;padding:28px 0 20px;border-bottom:1px solid var(--border)}.command-title{min-width:0}.eyebrow,.row-eyebrow{font-size:10px;letter-spacing:.1em;color:var(--faint);font-weight:700}.command-title h1{font-size:24px;line-height:1.2;letter-spacing:-.02em;margin:4px 0 5px;font-weight:650}.command-title p{margin:0;color:var(--muted);max-width:700px;line-height:1.45}.command-actions{display:flex;align-items:center;gap:8px;white-space:nowrap}.refreshed{color:var(--faint);font-size:11px;margin-right:2px}
4
+ .button,.icon-button,.text-button{border:1px solid var(--border-strong);background:var(--surface);color:var(--ink);border-radius:6px;min-height:32px;padding:0 10px;cursor:pointer}.button:hover,.icon-button:hover{background:var(--surface-subtle)}.button:disabled,.icon-button:disabled{opacity:.45;cursor:default}.ghost-link{display:inline-flex;align-items:center}.icon-button{width:32px;padding:0;display:grid;place-items:center}.icon-button svg{width:15px;height:15px}.text-button{border:0;background:transparent;padding:0;color:var(--accent);min-height:auto}.text-button:hover{text-decoration:underline}.global-error{margin:12px 0 -4px;border:1px solid #E7BFBF;background:var(--danger-soft);border-radius:8px;padding:10px 12px;display:flex;gap:10px;align-items:center}.global-error span{color:var(--muted);flex:1}.global-error button{border:0;background:transparent;font-size:18px;cursor:pointer}
5
+ .metric-strip{margin-top:24px;background:var(--surface);border:1px solid var(--border);border-radius:8px;display:grid;grid-template-columns:repeat(4,1fr);overflow:hidden}.metric-strip a{padding:16px 18px;border-right:1px solid var(--border);min-width:0}.metric-strip a:last-child{border-right:0}.metric-strip a:hover{background:#FAFAF9}.metric-strip span{display:block;color:var(--muted);font-size:11px}.metric-strip strong{display:inline-block;font-size:22px;letter-spacing:-.02em;margin:5px 6px 0 0}.metric-strip small{color:var(--faint)}.page-section{margin-top:30px}.section-header{height:32px;display:flex;align-items:flex-start;justify-content:space-between;border-bottom:1px solid var(--border)}.section-header>div:first-child{display:flex;align-items:baseline;gap:10px}.section-header h2{font-size:15px;margin:0;font-weight:650}.section-header span{font-size:11px;color:var(--faint)}
6
+ .attention-list,.compact-work-list{background:var(--surface);border-bottom:1px solid var(--border)}.attention-row{display:grid;grid-template-columns:38px minmax(0,1fr) auto;align-items:center;gap:10px;min-height:62px;padding:9px 4px;border-bottom:1px solid var(--border)}.attention-row:last-child{border-bottom:0}.attention-index{font-family:var(--font-mono);font-size:10px;color:var(--faint)}.attention-row strong,.compact-work-list strong{font-size:13px}.attention-row p,.compact-work-list p{margin:3px 0 0;color:var(--muted);font-size:12px}.compact-work-list a>div{min-width:0}.compact-work-list p{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.compact-work-list a{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:16px;align-items:center;min-height:54px;padding:8px 4px;border-bottom:1px solid var(--border)}.compact-work-list a:last-child{border-bottom:0}.compact-work-list a:hover strong{color:var(--accent)}.quiet-empty{padding:28px 4px;color:var(--muted)}.quiet-empty p{margin:6px 0 0}.status-text{display:inline-flex;align-items:center;gap:7px;white-space:nowrap;font-size:11px;color:var(--muted)}.status-dot{width:7px;height:7px;border-radius:50%;background:var(--faint)}.status-dot.success{background:var(--success)}.status-dot.warning{background:var(--warning)}.status-dot.danger{background:var(--danger)}.status-dot.info{background:var(--info)}
7
+ .toolbar{min-height:58px;display:flex;align-items:center;border-bottom:1px solid var(--border)}.segmented{display:inline-flex;padding:2px;background:var(--surface-strong);border-radius:7px;gap:1px}.segmented button{border:0;background:transparent;color:var(--muted);border-radius:5px;height:28px;padding:0 9px;cursor:pointer;font-size:11px}.segmented button.selected{background:var(--surface);color:var(--ink);box-shadow:0 1px 2px rgba(0,0,0,.05)}.segmented button span{margin-left:5px;color:var(--faint);font-size:10px}.split-workspace{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(300px,.85fr);min-height:520px;border-bottom:1px solid var(--border)}.automation-layout{grid-template-columns:minmax(0,2.2fr) minmax(300px,.9fr)}.scan-list{min-width:0}.scan-row{width:100%;min-height:68px;border:0;border-bottom:1px solid var(--border);background:transparent;text-align:left;padding:11px 12px 11px 4px;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:18px;align-items:center;cursor:pointer;color:var(--ink)}.scan-row.static{cursor:default}.scan-row:hover{background:#FAFAF8}.scan-row.selected{background:var(--accent-soft);box-shadow:inset 2px 0 var(--accent)}.scan-main{min-width:0}.scan-main strong{display:block;font-size:13px;font-weight:620;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.scan-main p{margin:4px 0 0;color:var(--muted);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.row-eyebrow{display:block;margin-bottom:3px}.scan-meta{text-align:right;display:grid;justify-items:end;gap:5px;color:var(--faint);font-size:10px}
8
+ .detail-pane{border-left:1px solid var(--border);background:rgba(255,255,255,.55);min-width:0}.detail-head{padding:18px 18px 15px;border-bottom:1px solid var(--border);display:flex;gap:12px;justify-content:space-between}.detail-head h2{font-size:16px;margin:4px 0 5px;line-height:1.3}.detail-head p{margin:0;color:var(--muted);font-size:12px;line-height:1.5}.detail-body{padding:18px}.detail-empty{display:grid;place-items:center;min-height:260px;color:var(--faint);font-size:12px}.definition-list{margin:0}.definition-list>div{display:grid;grid-template-columns:100px minmax(0,1fr);gap:10px;padding:8px 0;border-bottom:1px solid var(--border)}.definition-list dt{color:var(--faint);font-size:11px}.definition-list dd{margin:0;min-width:0;overflow-wrap:anywhere}.definition-list code{font-size:10px;color:var(--muted)}.detail-callout{margin-top:16px;border-left:2px solid var(--warning);padding:4px 0 4px 12px}.detail-callout.danger{border-color:var(--danger)}.detail-callout p{margin:5px 0 0;color:var(--muted);line-height:1.5}.detail-note{margin-top:18px;color:var(--faint);font-size:11px;line-height:1.55}.detail-button-row{display:flex;gap:7px;margin-top:18px;flex-wrap:wrap}.danger-text{color:var(--danger)}
9
+ .table-wrap{min-width:0;overflow:auto}.data-table{width:100%;border-collapse:collapse;font-size:12px}.data-table th{text-align:left;height:38px;color:var(--faint);font-size:10px;font-weight:650;letter-spacing:.03em;border-bottom:1px solid var(--border);padding:0 10px}.data-table td{height:62px;border-bottom:1px solid var(--border);padding:9px 10px;vertical-align:middle}.data-table tbody tr{cursor:pointer}.data-table tbody tr:hover{background:#FAFAF8}.data-table tbody tr.selected{background:var(--accent-soft)}.data-table td:first-child{max-width:280px}.data-table td strong{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.data-table td small{display:block;color:var(--faint);margin-top:3px}.capability-lines{display:flex;flex-wrap:wrap;gap:6px;margin-top:16px}.capability-lines span{font-size:10px;color:var(--muted);border:1px solid var(--border);border-radius:5px;padding:4px 6px;background:var(--surface)}.advanced{margin-top:18px;border-top:1px solid var(--border);padding-top:12px}.advanced summary{cursor:pointer;color:var(--muted);font-size:11px}.advanced pre{white-space:pre-wrap;word-break:break-word;background:var(--surface-subtle);border-radius:6px;padding:10px;font-size:10px;max-height:420px;overflow:auto}
10
+ .repository-tools{min-height:64px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:14px;justify-content:space-between}.repository-tools form{display:flex;gap:6px}.repository-tools input,.search-field input{height:32px;border:1px solid var(--border-strong);border-radius:6px;background:var(--surface);padding:0 9px;color:var(--ink);min-width:170px}.search-field{display:flex;align-items:center;position:relative}.search-field svg{position:absolute;left:9px;width:14px;color:var(--faint)}.search-field input{padding-left:30px;width:230px}.settings-column{max-width:900px;padding-top:12px}.settings-column section{margin-top:26px}.settings-list{border-top:1px solid var(--border)}.settings-row{display:grid;grid-template-columns:minmax(0,1fr) 130px auto;gap:20px;align-items:center;min-height:68px;border-bottom:1px solid var(--border);padding:9px 0}.settings-row strong{font-size:13px}.settings-row p{margin:3px 0 0;color:var(--muted);font-size:12px}.settings-actions{display:flex;gap:6px}.system-layout{max-width:900px;padding-top:26px}.system-summary{border-bottom:1px solid var(--border);padding-bottom:20px}.system-posture{display:flex;justify-content:space-between;align-items:flex-start;gap:20px;margin-bottom:12px}.system-posture h2{font-size:18px;margin:4px 0}.system-posture p{margin:0;color:var(--muted)}.system-layout>section+section{margin-top:24px}.boot-state{min-height:100vh;display:grid;place-content:center;justify-items:center;gap:10px;background:var(--canvas);color:var(--muted)}.boot-state .brand-mark{width:34px;height:34px}.boot-state p{max-width:460px;text-align:center}.single-list{max-width:900px}
11
+ @media(max-width:1080px){.workspace{padding-left:28px;padding-right:28px}.command-bar{align-items:flex-start;flex-direction:column}.command-actions{width:100%;justify-content:flex-start}.metric-strip{grid-template-columns:repeat(2,1fr)}.metric-strip a:nth-child(2){border-right:0}.metric-strip a:nth-child(-n+2){border-bottom:1px solid var(--border)}.split-workspace,.automation-layout{grid-template-columns:1fr}.detail-pane{border-left:0;border-top:1px solid var(--border)}.data-table{min-width:760px}}
12
+ @media(max-width:760px){.app-shell{display:block}.sidebar{position:sticky;z-index:10;top:0;width:100%;height:auto;padding:8px 12px;flex-direction:row;align-items:center;overflow:auto}.brand{height:38px;min-width:120px}.brand small,.sidebar-foot{display:none}.sidebar nav{display:flex;margin:0 0 0 8px;gap:2px}.sidebar nav>div.nav-group-start{margin:0;padding:0;border:0}.sidebar nav a{white-space:nowrap;height:32px}.sidebar nav svg{display:none}.workspace{padding:0 18px 44px}.command-actions{flex-wrap:wrap}.refreshed{width:100%}.metric-strip{grid-template-columns:1fr 1fr}.repository-tools{align-items:flex-start;flex-direction:column;padding:12px 0}.repository-tools form{width:100%;flex-wrap:wrap}.repository-tools input{flex:1}.settings-row{grid-template-columns:1fr auto}.settings-row>.status-text{grid-column:2}.settings-actions{grid-column:1/-1}.definition-list>div{grid-template-columns:88px 1fr}}
@@ -0,0 +1,14 @@
1
+ export type RouteId = 'overview' | 'automations' | 'work' | 'capabilities' | 'repositories' | 'settings' | 'system';
2
+ export type Dict = Record<string, unknown>;
3
+ export interface RepositoryView { id:string; name:string; path?:string; branchLabel?:string; dirtyLabel?:string; readinessLabel?:string; statusLabel?:string; current?:boolean; advanced?:Dict }
4
+ export interface PluginActionView { id:string; title?:string; description?:string; risk?:string; riskLabel?:string; readOnly?:boolean; canPreview?:boolean; confirmationLabel?:string }
5
+ export interface PluginView { id:string; name:string; description?:string; status?:string; statusLabel?:string; tone?:string; nextStep?:string; provider?:string; healthLabel?:string; lifecycleLabel?:string; capabilityLabels?:string[]; actions?:PluginActionView[]; warnings?:string[]; advanced?:Dict }
6
+ export interface ProviderView { providerId:string; displayName:string; status?:string; statusLabel?:string; kindLabel?:string; summary?:string; explanation?:string; directDispatch?:boolean; handoffOnly?:boolean; enabled?:boolean; priority?:number; capabilities?:string[]; lastErrorSummary?:string }
7
+ export interface LocalToolView { toolId:string; displayName:string; status?:string; summary?:string; enabled?:boolean; executablePath?:string; version?:string; usedByWorkflows?:string[] }
8
+ export interface AutomationSettingsView { schemaVersion?:number; overview?:Dict; providers?:ProviderView[]; localTools?:LocalToolView[]; credentials?:Dict[]; routing?:{orders?:Record<string,string[]>}; warnings?:string[] }
9
+ export interface CommandCenterView { readiness?:Dict; repositories?:RepositoryView[]; handoffs?:Dict[]; plugins?:PluginView[]; pluginSummary?:{total?:number;ready?:number;needsAttention?:number}; warnings?:string[] }
10
+ export interface AutomationView { id:string; source:'schedule'|'routine'; repoId:string; repositoryName:string; name:string; summary?:string; status:'enabled'|'paused'|'attention'|'disabled'; schedule:string; timezone?:string; delivery?:string; lastRunAt?:string; lastResult?:string; nextRunHint?:string; pausedReason?:string; actions:Array<'run'|'pause'|'resume'> }
11
+ export interface AutomationsResponse { schemaVersion:1; generatedAt:string; summary:{total:number;enabled:number;paused:number;needsAttention:number}; automations:AutomationView[] }
12
+ export interface TrackedWorkView { requirementId:string; title:string; outcome?:string; state:'planned'|'active'|'waiting_for_user'|'done'|'cancelled'; needsAttention?:boolean; blocker?:string; requiredUserDecision?:string; updatedAt?:string }
13
+ export interface WorkResponse { schemaVersion?:number; requirementCount?:number; activeRequirementCount?:number; waitingForUserCount?:number; needsAttentionCount?:number; requirements?:TrackedWorkView[]; requirementTruncatedCount?:number }
14
+ export interface ConsoleData { commandCenter:CommandCenterView; work:WorkResponse; automations:AutomationsResponse; automationSettings:AutomationSettingsView; connector?:Dict; generatedAt:string }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,27 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import { resolve } from 'node:path';
4
+
5
+ export default defineConfig({
6
+ define: { 'process.env.NODE_ENV': JSON.stringify('production') },
7
+ plugins: [react()],
8
+ build: {
9
+ outDir: resolve(import.meta.dirname, '../ui-dist'),
10
+ emptyOutDir: true,
11
+ sourcemap: false,
12
+ minify: 'esbuild',
13
+ cssCodeSplit: false,
14
+ lib: {
15
+ entry: resolve(import.meta.dirname, 'src/main.tsx'),
16
+ formats: ['es'],
17
+ fileName: () => 'app.js',
18
+ cssFileName: 'app',
19
+ },
20
+ rollupOptions: {
21
+ output: {
22
+ entryFileNames: 'app.js',
23
+ assetFileNames: (asset) => asset.name?.endsWith('.css') ? 'app.css' : '[name][extname]',
24
+ },
25
+ },
26
+ },
27
+ });
@@ -1,4 +1,5 @@
1
1
  import { randomBytes } from "crypto";
2
+ import { readFileSync } from "fs";
2
3
  import { spawn } from "child_process";
3
4
  import { createServer, type Server } from "http";
4
5
  import express, {
@@ -72,6 +73,8 @@ import {
72
73
  verifyEditSession,
73
74
  } from "../editing/edit-session";
74
75
  import { localBridgeDashboardHtml } from "./dashboard";
76
+ import { readConsoleAsset, type ConsoleAssetName } from "./console-assets";
77
+ import { applyConsoleAutomationAction, listConsoleAutomations, summarizeConsoleAutomations } from "./console-automations";
75
78
  import {
76
79
  ackConsoleHandoff,
77
80
  approveConsoleHandoff,
@@ -987,6 +990,14 @@ export async function startLocalBridgeServer(
987
990
  next();
988
991
  };
989
992
 
993
+ app.get("/console-assets/:asset", (request, response) => {
994
+ const asset = String(request.params.asset ?? "");
995
+ if (asset !== "app.js" && asset !== "app.css") { response.status(404).end(); return; }
996
+ response.setHeader("Cache-Control", "no-cache");
997
+ response.type(asset.endsWith(".css") ? "text/css" : "application/javascript");
998
+ response.send(readConsoleAsset(asset as ConsoleAssetName));
999
+ });
1000
+
990
1001
  app.get("/", (_request, response) => {
991
1002
  response.setHeader("Cache-Control", "no-store, max-age=0");
992
1003
  response.setHeader("Pragma", "no-cache");
@@ -1266,6 +1277,40 @@ export async function startLocalBridgeServer(
1266
1277
  }
1267
1278
  });
1268
1279
 
1280
+ app.get("/api/console/requirements", (_request, response) => {
1281
+ try {
1282
+ response.json(buildRequirementBoard({ controllerHome }));
1283
+ } catch (error) {
1284
+ response.status(400).json({ error: errorMessage(error) });
1285
+ }
1286
+ });
1287
+
1288
+ app.get("/api/console/automations", (_request, response) => {
1289
+ try {
1290
+ const repositories = loadRepositoryRegistry(controllerHome).repositories;
1291
+ const automations = listConsoleAutomations(controllerHome, repositories);
1292
+ response.json({ schemaVersion: 1, generatedAt: new Date().toISOString(), summary: summarizeConsoleAutomations(automations), automations });
1293
+ } catch (error) {
1294
+ response.status(400).json({ error: errorMessage(error) });
1295
+ }
1296
+ });
1297
+
1298
+ app.post("/api/console/automations/:source/:repoId/:automationId/:action", async (request, response) => {
1299
+ try {
1300
+ const result = await applyConsoleAutomationAction(
1301
+ controllerHome,
1302
+ loadRepositoryRegistry(controllerHome).repositories,
1303
+ String(request.params.source ?? ""),
1304
+ String(request.params.repoId ?? ""),
1305
+ String(request.params.automationId ?? ""),
1306
+ String(request.params.action ?? ""),
1307
+ );
1308
+ response.json({ result });
1309
+ } catch (error) {
1310
+ response.status(400).json({ error: errorMessage(error) });
1311
+ }
1312
+ });
1313
+
1269
1314
  app.get("/api/console/readiness", async (request, response) => {
1270
1315
  try {
1271
1316
  response.json(await buildSystemReadiness(consoleCtx(request)));
@@ -0,0 +1 @@
1
+ :root{--canvas:#F5F5F3;--surface:#FFFFFF;--surface-subtle:#F0F0EE;--surface-strong:#E8E8E5;--rail:#19191B;--rail-hover:#242427;--rail-selected:#303037;--ink:#1D1D20;--muted:#646469;--faint:#909096;--inverse:#F7F7F8;--border:#E1E1DE;--border-strong:#CECECA;--accent:#5E54E8;--accent-hover:#5046D9;--accent-soft:#EFEEFF;--success:#237A57;--success-soft:#EAF5EF;--warning:#946200;--warning-soft:#FFF4D8;--danger:#B44444;--danger-soft:#FBECEC;--info:#3567B7;--focus:#786FF7;--font-sans:Inter,-apple-system,BlinkMacSystemFont,"SF Pro Text","PingFang SC","Segoe UI",sans-serif;--font-mono:"SFMono-Regular",Consolas,"Liberation Mono",monospace;--r-xs:4px;--r-sm:6px;--r-md:8px;--r-lg:10px;--rail-width:224px;--content-max:1280px}*{box-sizing:border-box}html,body,#app{min-height:100%;margin:0}body{font-family:var(--font-sans);font-size:13px;color:var(--ink);background:var(--canvas);-webkit-font-smoothing:antialiased}button,input{font:inherit}button,a{transition:background-color .14s,border-color .14s,color .14s,opacity .14s}a{color:inherit;text-decoration:none}code,pre{font-family:var(--font-mono)}button:focus-visible,a:focus-visible,input:focus-visible,summary:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.app-shell{min-height:100vh;display:grid;grid-template-columns:var(--rail-width) minmax(0,1fr)}.sidebar{position:sticky;top:0;height:100vh;background:var(--rail);color:#b8b8bf;padding:18px 12px 14px;display:flex;flex-direction:column}.brand{height:50px;display:flex;align-items:center;gap:10px;padding:0 8px;color:var(--inverse)}.brand-mark{display:grid;place-items:center;width:26px;height:26px;border-radius:7px;background:var(--accent);color:#fff;font-weight:750}.brand strong{display:block;font-size:14px}.brand small{display:block;color:#77777f;font-size:10px;margin-top:1px}.sidebar nav{margin-top:18px;display:grid;gap:2px}.sidebar nav>div.nav-group-start{margin-top:13px;padding-top:13px;border-top:1px solid #2A2A2E}.sidebar nav a{display:flex;height:34px;align-items:center;gap:10px;padding:0 9px;border-radius:6px;color:#9c9ca4}.sidebar nav a:hover{background:var(--rail-hover);color:#dcdce0}.sidebar nav a.active{background:var(--rail-selected);color:var(--inverse);box-shadow:inset 2px 0 var(--accent)}.sidebar nav svg{width:16px;height:16px}.sidebar-foot{margin-top:auto;border-top:1px solid #2A2A2E;padding:13px 8px 0;color:#75757d;font-size:10px;display:flex;align-items:center;gap:7px}.pulse-dot{width:6px;height:6px;border-radius:50%;background:#5cc58a}.workspace{width:100%;max-width:calc(var(--content-max) + 80px);padding:0 40px 64px;min-width:0}.command-bar{min-height:126px;display:flex;justify-content:space-between;gap:28px;align-items:flex-end;padding:28px 0 20px;border-bottom:1px solid var(--border)}.command-title{min-width:0}.eyebrow,.row-eyebrow{font-size:10px;letter-spacing:.1em;color:var(--faint);font-weight:700}.command-title h1{font-size:24px;line-height:1.2;letter-spacing:-.02em;margin:4px 0 5px;font-weight:650}.command-title p{margin:0;color:var(--muted);max-width:700px;line-height:1.45}.command-actions{display:flex;align-items:center;gap:8px;white-space:nowrap}.refreshed{color:var(--faint);font-size:11px;margin-right:2px}.button,.icon-button,.text-button{border:1px solid var(--border-strong);background:var(--surface);color:var(--ink);border-radius:6px;min-height:32px;padding:0 10px;cursor:pointer}.button:hover,.icon-button:hover{background:var(--surface-subtle)}.button:disabled,.icon-button:disabled{opacity:.45;cursor:default}.ghost-link{display:inline-flex;align-items:center}.icon-button{width:32px;padding:0;display:grid;place-items:center}.icon-button svg{width:15px;height:15px}.text-button{border:0;background:transparent;padding:0;color:var(--accent);min-height:auto}.text-button:hover{text-decoration:underline}.global-error{margin:12px 0 -4px;border:1px solid #E7BFBF;background:var(--danger-soft);border-radius:8px;padding:10px 12px;display:flex;gap:10px;align-items:center}.global-error span{color:var(--muted);flex:1}.global-error button{border:0;background:transparent;font-size:18px;cursor:pointer}.metric-strip{margin-top:24px;background:var(--surface);border:1px solid var(--border);border-radius:8px;display:grid;grid-template-columns:repeat(4,1fr);overflow:hidden}.metric-strip a{padding:16px 18px;border-right:1px solid var(--border);min-width:0}.metric-strip a:last-child{border-right:0}.metric-strip a:hover{background:#fafaf9}.metric-strip span{display:block;color:var(--muted);font-size:11px}.metric-strip strong{display:inline-block;font-size:22px;letter-spacing:-.02em;margin:5px 6px 0 0}.metric-strip small{color:var(--faint)}.page-section{margin-top:30px}.section-header{height:32px;display:flex;align-items:flex-start;justify-content:space-between;border-bottom:1px solid var(--border)}.section-header>div:first-child{display:flex;align-items:baseline;gap:10px}.section-header h2{font-size:15px;margin:0;font-weight:650}.section-header span{font-size:11px;color:var(--faint)}.attention-list,.compact-work-list{background:var(--surface);border-bottom:1px solid var(--border)}.attention-row{display:grid;grid-template-columns:38px minmax(0,1fr) auto;align-items:center;gap:10px;min-height:62px;padding:9px 4px;border-bottom:1px solid var(--border)}.attention-row:last-child{border-bottom:0}.attention-index{font-family:var(--font-mono);font-size:10px;color:var(--faint)}.attention-row strong,.compact-work-list strong{font-size:13px}.attention-row p,.compact-work-list p{margin:3px 0 0;color:var(--muted);font-size:12px}.compact-work-list a>div{min-width:0}.compact-work-list p{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.compact-work-list a{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:16px;align-items:center;min-height:54px;padding:8px 4px;border-bottom:1px solid var(--border)}.compact-work-list a:last-child{border-bottom:0}.compact-work-list a:hover strong{color:var(--accent)}.quiet-empty{padding:28px 4px;color:var(--muted)}.quiet-empty p{margin:6px 0 0}.status-text{display:inline-flex;align-items:center;gap:7px;white-space:nowrap;font-size:11px;color:var(--muted)}.status-dot{width:7px;height:7px;border-radius:50%;background:var(--faint)}.status-dot.success{background:var(--success)}.status-dot.warning{background:var(--warning)}.status-dot.danger{background:var(--danger)}.status-dot.info{background:var(--info)}.toolbar{min-height:58px;display:flex;align-items:center;border-bottom:1px solid var(--border)}.segmented{display:inline-flex;padding:2px;background:var(--surface-strong);border-radius:7px;gap:1px}.segmented button{border:0;background:transparent;color:var(--muted);border-radius:5px;height:28px;padding:0 9px;cursor:pointer;font-size:11px}.segmented button.selected{background:var(--surface);color:var(--ink);box-shadow:0 1px 2px #0000000d}.segmented button span{margin-left:5px;color:var(--faint);font-size:10px}.split-workspace{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(300px,.85fr);min-height:520px;border-bottom:1px solid var(--border)}.automation-layout{grid-template-columns:minmax(0,2.2fr) minmax(300px,.9fr)}.scan-list{min-width:0}.scan-row{width:100%;min-height:68px;border:0;border-bottom:1px solid var(--border);background:transparent;text-align:left;padding:11px 12px 11px 4px;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:18px;align-items:center;cursor:pointer;color:var(--ink)}.scan-row.static{cursor:default}.scan-row:hover{background:#fafaf8}.scan-row.selected{background:var(--accent-soft);box-shadow:inset 2px 0 var(--accent)}.scan-main{min-width:0}.scan-main strong{display:block;font-size:13px;font-weight:620;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.scan-main p{margin:4px 0 0;color:var(--muted);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.row-eyebrow{display:block;margin-bottom:3px}.scan-meta{text-align:right;display:grid;justify-items:end;gap:5px;color:var(--faint);font-size:10px}.detail-pane{border-left:1px solid var(--border);background:#ffffff8c;min-width:0}.detail-head{padding:18px 18px 15px;border-bottom:1px solid var(--border);display:flex;gap:12px;justify-content:space-between}.detail-head h2{font-size:16px;margin:4px 0 5px;line-height:1.3}.detail-head p{margin:0;color:var(--muted);font-size:12px;line-height:1.5}.detail-body{padding:18px}.detail-empty{display:grid;place-items:center;min-height:260px;color:var(--faint);font-size:12px}.definition-list{margin:0}.definition-list>div{display:grid;grid-template-columns:100px minmax(0,1fr);gap:10px;padding:8px 0;border-bottom:1px solid var(--border)}.definition-list dt{color:var(--faint);font-size:11px}.definition-list dd{margin:0;min-width:0;overflow-wrap:anywhere}.definition-list code{font-size:10px;color:var(--muted)}.detail-callout{margin-top:16px;border-left:2px solid var(--warning);padding:4px 0 4px 12px}.detail-callout.danger{border-color:var(--danger)}.detail-callout p{margin:5px 0 0;color:var(--muted);line-height:1.5}.detail-note{margin-top:18px;color:var(--faint);font-size:11px;line-height:1.55}.detail-button-row{display:flex;gap:7px;margin-top:18px;flex-wrap:wrap}.danger-text{color:var(--danger)}.table-wrap{min-width:0;overflow:auto}.data-table{width:100%;border-collapse:collapse;font-size:12px}.data-table th{text-align:left;height:38px;color:var(--faint);font-size:10px;font-weight:650;letter-spacing:.03em;border-bottom:1px solid var(--border);padding:0 10px}.data-table td{height:62px;border-bottom:1px solid var(--border);padding:9px 10px;vertical-align:middle}.data-table tbody tr{cursor:pointer}.data-table tbody tr:hover{background:#fafaf8}.data-table tbody tr.selected{background:var(--accent-soft)}.data-table td:first-child{max-width:280px}.data-table td strong{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.data-table td small{display:block;color:var(--faint);margin-top:3px}.capability-lines{display:flex;flex-wrap:wrap;gap:6px;margin-top:16px}.capability-lines span{font-size:10px;color:var(--muted);border:1px solid var(--border);border-radius:5px;padding:4px 6px;background:var(--surface)}.advanced{margin-top:18px;border-top:1px solid var(--border);padding-top:12px}.advanced summary{cursor:pointer;color:var(--muted);font-size:11px}.advanced pre{white-space:pre-wrap;word-break:break-word;background:var(--surface-subtle);border-radius:6px;padding:10px;font-size:10px;max-height:420px;overflow:auto}.repository-tools{min-height:64px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:14px;justify-content:space-between}.repository-tools form{display:flex;gap:6px}.repository-tools input,.search-field input{height:32px;border:1px solid var(--border-strong);border-radius:6px;background:var(--surface);padding:0 9px;color:var(--ink);min-width:170px}.search-field{display:flex;align-items:center;position:relative}.search-field svg{position:absolute;left:9px;width:14px;color:var(--faint)}.search-field input{padding-left:30px;width:230px}.settings-column{max-width:900px;padding-top:12px}.settings-column section{margin-top:26px}.settings-list{border-top:1px solid var(--border)}.settings-row{display:grid;grid-template-columns:minmax(0,1fr) 130px auto;gap:20px;align-items:center;min-height:68px;border-bottom:1px solid var(--border);padding:9px 0}.settings-row strong{font-size:13px}.settings-row p{margin:3px 0 0;color:var(--muted);font-size:12px}.settings-actions{display:flex;gap:6px}.system-layout{max-width:900px;padding-top:26px}.system-summary{border-bottom:1px solid var(--border);padding-bottom:20px}.system-posture{display:flex;justify-content:space-between;align-items:flex-start;gap:20px;margin-bottom:12px}.system-posture h2{font-size:18px;margin:4px 0}.system-posture p{margin:0;color:var(--muted)}.system-layout>section+section{margin-top:24px}.boot-state{min-height:100vh;display:grid;place-content:center;justify-items:center;gap:10px;background:var(--canvas);color:var(--muted)}.boot-state .brand-mark{width:34px;height:34px}.boot-state p{max-width:460px;text-align:center}.single-list{max-width:900px}@media(max-width:1080px){.workspace{padding-left:28px;padding-right:28px}.command-bar{align-items:flex-start;flex-direction:column}.command-actions{width:100%;justify-content:flex-start}.metric-strip{grid-template-columns:repeat(2,1fr)}.metric-strip a:nth-child(2){border-right:0}.metric-strip a:nth-child(-n+2){border-bottom:1px solid var(--border)}.split-workspace,.automation-layout{grid-template-columns:1fr}.detail-pane{border-left:0;border-top:1px solid var(--border)}.data-table{min-width:760px}}@media(max-width:760px){.app-shell{display:block}.sidebar{position:sticky;z-index:10;top:0;width:100%;height:auto;padding:8px 12px;flex-direction:row;align-items:center;overflow:auto}.brand{height:38px;min-width:120px}.brand small,.sidebar-foot{display:none}.sidebar nav{display:flex;margin:0 0 0 8px;gap:2px}.sidebar nav>div.nav-group-start{margin:0;padding:0;border:0}.sidebar nav a{white-space:nowrap;height:32px}.sidebar nav svg{display:none}.workspace{padding:0 18px 44px}.command-actions{flex-wrap:wrap}.refreshed{width:100%}.metric-strip{grid-template-columns:1fr 1fr}.repository-tools{align-items:flex-start;flex-direction:column;padding:12px 0}.repository-tools form{width:100%;flex-wrap:wrap}.repository-tools input{flex:1}.settings-row{grid-template-columns:1fr auto}.settings-row>.status-text{grid-column:2}.settings-actions{grid-column:1/-1}.definition-list>div{grid-template-columns:88px 1fr}}