@moretea-labs/forge 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -1
- package/DESIGN.md +7 -2
- package/README.md +7 -7
- package/README.zh-CN.md +5 -3
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/assets/hooks/pre-edit-guard.sh +8 -39
- package/assets/hooks/prompt-guard.sh +2 -2
- package/assets/plugin-registry.v1.json +54 -3
- package/assets/reference-configs/hook-operations.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +2 -2
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/ensure-task-workflow.sh +2 -2
- package/assets/templates/helpers/sprint-backlog.sh +6 -6
- package/docs/architecture/current/automation-and-schedule-engine.md +9 -3
- package/docs/architecture/current/codegraph-context-provider.md +15 -3
- package/docs/architecture/current/dispatch-and-agent-strategy.md +32 -13
- package/docs/architecture/current/entity-model.md +20 -0
- package/docs/architecture/current/external-plugin-governance.md +3 -3
- package/docs/architecture/current/system-overview.md +26 -1
- package/docs/architecture/current/thin-harness-v1.md +10 -0
- package/docs/forge-chatgpt-mcp-setup.md +27 -3
- package/docs/operations/chatgpt-personal-assistant-runtime.md +12 -2
- package/docs/operations/controller-browser-plugin.md +12 -16
- package/docs/operations/features.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/public-usage-guide.md +1 -1
- package/docs/public-usage-guide.zh-CN.md +1 -1
- package/docs/tutorials/01-install-and-start.md +6 -4
- package/docs/tutorials/01-install-and-start.zh-CN.md +3 -1
- package/docs/tutorials/02-connect-chatgpt.md +8 -2
- package/docs/tutorials/02-connect-chatgpt.zh-CN.md +8 -2
- package/docs/wiki/Installation.md +1 -1
- package/docs/wiki/Quick-Start.md +1 -1
- package/package.json +1 -1
- package/scripts/check-public-docs.sh +14 -1
- package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
- package/scripts/sprint-backlog.sh +1 -1
- package/src/cli/commands/chatgpt.ts +31 -2
- package/src/cli/commands/init-hook.ts +3 -1
- package/src/cli/commands/mcp.ts +2 -0
- package/src/cli/commands/plugin.ts +36 -9
- package/src/cli/commands/runtime.ts +1 -0
- package/src/cli/commands/setup-profile.ts +49 -18
- package/src/cli/controller/context-pack.ts +158 -6
- package/src/cli/hook/prompt-guard-decision.ts +17 -18
- package/src/cli/hook/prompt-intents.ts +1 -1
- package/src/cli/local-bridge/console-assets.ts +17 -0
- package/src/cli/local-bridge/console-automations.ts +205 -23
- package/src/cli/local-bridge/console-view-models.ts +2 -0
- package/src/cli/local-bridge/facade-api.ts +2 -0
- package/src/cli/local-bridge/frontend/src/App.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +143 -3
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +181 -3
- package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
- package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -6
- package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -1
- package/src/cli/local-bridge/frontend/src/styles.css +27 -11
- package/src/cli/local-bridge/frontend/src/types.ts +6 -2
- package/src/cli/local-bridge/server.ts +31 -20
- package/src/cli/local-bridge/ui-dist/app.css +1 -1
- package/src/cli/local-bridge/ui-dist/app.js +5003 -4815
- package/src/cli/mcp/auth.ts +2 -0
- package/src/cli/mcp/paths.ts +8 -2
- package/src/cli/mcp/repository-tools.ts +124 -9
- package/src/cli/mcp/server.ts +164 -19
- package/src/cli/mcp/setup.ts +9 -1
- package/src/cli/mcp/transports/http.ts +52 -2
- package/src/cli/mcp/transports/session-registry.ts +24 -0
- package/src/cli/repositories/command-classifier.ts +113 -0
- package/src/cli/repositories/safe-patch.ts +6 -4
- package/src/cli/repositories/structured-git.ts +49 -7
- package/src/cli/repository/inspector.ts +16 -3
- package/src/runtime/assistant/intent.ts +3 -3
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +157 -2
- package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
- package/src/runtime/control-plane/execution/validation.ts +15 -2
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +32 -0
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +6 -7
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +2 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +1 -1
- package/src/runtime/control-plane/facade/controller-session-store.ts +12 -13
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +7 -2
- package/src/runtime/control-plane/facade/goal-workloop.ts +231 -8
- package/src/runtime/control-plane/facade/plan-contract-store.ts +32 -12
- package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
- package/src/runtime/control-plane/facade/types.ts +33 -6
- package/src/runtime/control-plane/facade/work-contract-store.ts +31 -2
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +375 -23
- package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
- package/src/runtime/diagnostics/mcp-timing.ts +6 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +1 -10
- package/src/runtime/execution/process-runtime/check-result.ts +10 -2
- package/src/runtime/execution/process-runtime/command-facade.ts +1 -0
- package/src/runtime/execution/process-runtime/gc.ts +64 -10
- package/src/runtime/execution/process-runtime/persisted-check.ts +10 -2
- package/src/runtime/execution/process-runtime/resource-claims.ts +87 -2
- package/src/runtime/execution/process-runtime/runtime.ts +16 -1
- package/src/runtime/execution/process-runtime/store.ts +6 -0
- package/src/runtime/gateway/mcp/execution-tools.ts +27 -12
- package/src/runtime/gateway/mcp/router.ts +1 -1
- package/src/runtime/gateway/mcp/runtime-tools.ts +482 -178
- package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
- package/src/runtime/plugins/browser-adapter.ts +19 -72
- package/src/runtime/plugins/browser-handoff-host.ts +6 -31
- package/src/runtime/plugins/browser-handoff.ts +0 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +8 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +8 -11
- package/src/runtime/plugins/external-registration.ts +84 -5
- package/src/runtime/plugins/first-party-registry.ts +2 -2
- package/src/runtime/plugins/gmail-adapter.ts +28 -2
- package/src/runtime/plugins/local-system-adapter.ts +2 -2
- package/src/runtime/plugins/plugin-management-adapter.ts +140 -0
- package/src/runtime/plugins/store.ts +107 -5
- package/src/runtime/plugins/types.ts +4 -0
- package/src/runtime/recovery/actions.ts +0 -10
- package/src/runtime/recovery/classifier.ts +0 -9
- package/src/runtime/recovery/maintenance-executor.ts +43 -11
- package/src/runtime/recovery/probe.ts +0 -1
- package/src/runtime/recovery/types.ts +0 -1
- package/src/runtime/root/package-connector-service.ts +136 -0
- package/src/runtime/root/package-runtime-service.ts +25 -12
- package/src/runtime/root/release-materialize.ts +161 -18
- package/src/runtime/root/service.ts +6 -4
- package/src/runtime/safe-tooling/index.ts +0 -1
- package/src/runtime/safe-tooling/types.ts +0 -30
- package/src/runtime/standalone-recovery/core.ts +2 -2
- package/src/runtime/workflow/schedules/browser-probe.ts +180 -0
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
- package/src/runtime/workflow/schedules/engine.ts +357 -64
- package/src/runtime/workflow/schedules/types.ts +5 -0
- package/src/runtime/workflow/schedules/work-continuation.ts +100 -27
- package/bin/forge-desktop-helper.mjs +0 -204
- package/docs/operations/controller-desktop-plugin.md +0 -112
- package/src/runtime/plugins/desktop-adapter.ts +0 -374
- package/src/runtime/safe-tooling/browser-safe.ts +0 -133
|
@@ -1,3 +1,181 @@
|
|
|
1
|
-
import type{ConsoleData,Dict}from'../../types';
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { ConsoleData, Dict, WorkPortfolioItem } from '../../types';
|
|
2
|
+
import { CommandBar } from '../../shell/CommandBar';
|
|
3
|
+
import { StatusText } from '../../components/StatusText';
|
|
4
|
+
import { SectionHeader } from '../../components/SectionHeader';
|
|
5
|
+
import { compact } from '../../lib/format';
|
|
6
|
+
|
|
7
|
+
type AttentionItem = {
|
|
8
|
+
key: string;
|
|
9
|
+
source: string;
|
|
10
|
+
title: string;
|
|
11
|
+
summary: string;
|
|
12
|
+
statusLabel: string;
|
|
13
|
+
tone: string;
|
|
14
|
+
href: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type WorkspaceRow = {
|
|
18
|
+
key: string;
|
|
19
|
+
label: string;
|
|
20
|
+
summary: string;
|
|
21
|
+
href: string;
|
|
22
|
+
statusLabel?: string;
|
|
23
|
+
tone?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function workStatus(item: WorkPortfolioItem): string {
|
|
27
|
+
return item.advanced?.status ?? '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isAttentionWork(item: WorkPortfolioItem): boolean {
|
|
31
|
+
return ['blocked', 'failed'].includes(workStatus(item));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function attentionKey(item: Dict): string {
|
|
35
|
+
return `${String(item.title ?? '')}:${String(item.reason ?? '')}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function repositoryNeedsAttention(repository: NonNullable<ConsoleData['commandCenter']['repositories']>[number]): boolean {
|
|
39
|
+
const state = `${repository.readinessLabel ?? ''} ${repository.statusLabel ?? ''}`;
|
|
40
|
+
return /error|failed|blocked|unavailable|degraded|warning|attention/i.test(state);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function OverviewView({ data, busy, onRefresh }: { data: ConsoleData; busy: boolean; onRefresh: () => void }) {
|
|
44
|
+
const cc = data.commandCenter;
|
|
45
|
+
const portfolio = data.workPortfolio;
|
|
46
|
+
const automations = data.automations.summary;
|
|
47
|
+
const plugins = cc.pluginSummary ?? {};
|
|
48
|
+
const repositories = cc.repositories ?? [];
|
|
49
|
+
const readiness = cc.readiness ?? {};
|
|
50
|
+
const capabilityTotal = plugins.total ?? (cc.plugins ?? []).length;
|
|
51
|
+
const repositoryAttention = repositories.filter(repositoryNeedsAttention).length;
|
|
52
|
+
const readinessState = String(readiness.state ?? readiness.status ?? 'ready');
|
|
53
|
+
const runtimeNeedsAttention = /error|failed|blocked|unavailable|degraded|warning|attention/i.test(readinessState);
|
|
54
|
+
const runtimeLabel = String(readiness.label ?? readiness.headline ?? (runtimeNeedsAttention ? 'Needs attention' : 'Ready'));
|
|
55
|
+
|
|
56
|
+
const attentionWork = portfolio.items.filter(isAttentionWork).slice(0, 4);
|
|
57
|
+
const uniqueHandoffs = [...(cc.handoffs ?? [])].filter((handoff, index, all) => all.findIndex((candidate) => attentionKey(candidate) === attentionKey(handoff)) === index);
|
|
58
|
+
|
|
59
|
+
const attention: AttentionItem[] = [
|
|
60
|
+
...(runtimeNeedsAttention ? [{
|
|
61
|
+
key: 'runtime',
|
|
62
|
+
source: 'System',
|
|
63
|
+
title: runtimeLabel,
|
|
64
|
+
summary: compact(String(readiness.explanation ?? readiness.summary ?? 'Inspect Runtime status.'), 112),
|
|
65
|
+
statusLabel: 'Inspect',
|
|
66
|
+
tone: readinessState,
|
|
67
|
+
href: '#/system',
|
|
68
|
+
}] : []),
|
|
69
|
+
...attentionWork.map((item) => ({
|
|
70
|
+
key: `work:${item.id}`,
|
|
71
|
+
source: `Work · ${item.repositoryName}`,
|
|
72
|
+
title: item.title,
|
|
73
|
+
summary: compact(item.latestSummary || item.nextAction || item.objective, 112),
|
|
74
|
+
statusLabel: item.statusLabel,
|
|
75
|
+
tone: item.tone ?? 'warning',
|
|
76
|
+
href: '#/work',
|
|
77
|
+
})),
|
|
78
|
+
...uniqueHandoffs.slice(0, 2).map((handoff, index) => ({
|
|
79
|
+
key: `handoff:${index}:${attentionKey(handoff)}`,
|
|
80
|
+
source: 'Decision',
|
|
81
|
+
title: String(handoff.title ?? 'Needs review'),
|
|
82
|
+
summary: compact(String(handoff.reason ?? handoff.summary ?? 'Review in ChatGPT.'), 112),
|
|
83
|
+
statusLabel: String(handoff.statusLabel ?? 'Review'),
|
|
84
|
+
tone: String(handoff.tone ?? 'warning'),
|
|
85
|
+
href: '#/work',
|
|
86
|
+
})),
|
|
87
|
+
...((automations.needsAttention ?? 0) > 0 ? [{
|
|
88
|
+
key: 'automations',
|
|
89
|
+
source: 'Automations',
|
|
90
|
+
title: `${automations.needsAttention} automation${automations.needsAttention === 1 ? '' : 's'} need attention`,
|
|
91
|
+
summary: 'Inspect configured routines and schedules.',
|
|
92
|
+
statusLabel: 'Inspect',
|
|
93
|
+
tone: 'warning',
|
|
94
|
+
href: '#/automations',
|
|
95
|
+
}] : []),
|
|
96
|
+
...((plugins.needsAttention ?? 0) > 0 ? [{
|
|
97
|
+
key: 'capabilities',
|
|
98
|
+
source: 'Capabilities',
|
|
99
|
+
title: `${plugins.needsAttention} ${plugins.needsAttention === 1 ? 'capability' : 'capabilities'} need attention`,
|
|
100
|
+
summary: 'Inspect configured capability readiness.',
|
|
101
|
+
statusLabel: 'Inspect',
|
|
102
|
+
tone: 'warning',
|
|
103
|
+
href: '#/capabilities',
|
|
104
|
+
}] : []),
|
|
105
|
+
...(repositoryAttention > 0 ? [{
|
|
106
|
+
key: 'repositories',
|
|
107
|
+
source: 'Repositories',
|
|
108
|
+
title: `${repositoryAttention} ${repositoryAttention === 1 ? 'repository' : 'repositories'} need attention`,
|
|
109
|
+
summary: 'Inspect repository registration and readiness.',
|
|
110
|
+
statusLabel: 'Inspect',
|
|
111
|
+
tone: 'warning',
|
|
112
|
+
href: '#/repositories',
|
|
113
|
+
}] : []),
|
|
114
|
+
].filter((item, index, all) => all.findIndex((candidate) => candidate.key === item.key) === index).slice(0, 6);
|
|
115
|
+
|
|
116
|
+
const workSummary = portfolio.summary.needsAttention
|
|
117
|
+
? `${portfolio.summary.open} open · ${portfolio.summary.needsAttention} need attention`
|
|
118
|
+
: `${portfolio.summary.open} open · no attention needed`;
|
|
119
|
+
const automationSummary = automations.needsAttention
|
|
120
|
+
? `${automations.enabled} enabled · ${automations.needsAttention} need attention`
|
|
121
|
+
: automations.paused
|
|
122
|
+
? `${automations.enabled} enabled · ${automations.paused} paused`
|
|
123
|
+
: `${automations.enabled} enabled · healthy`;
|
|
124
|
+
const capabilitySummary = (plugins.needsAttention ?? 0) > 0
|
|
125
|
+
? `${plugins.ready ?? 0} / ${capabilityTotal} ready · ${plugins.needsAttention} need attention`
|
|
126
|
+
: `${plugins.ready ?? 0} / ${capabilityTotal} ready`;
|
|
127
|
+
const repositorySummary = repositoryAttention
|
|
128
|
+
? `${repositories.length} registered · ${repositoryAttention} need attention`
|
|
129
|
+
: `${repositories.length} registered`;
|
|
130
|
+
|
|
131
|
+
const workspaces: WorkspaceRow[] = [
|
|
132
|
+
{ key: 'work', label: 'Work', summary: workSummary, href: '#/work', statusLabel: portfolio.summary.needsAttention ? 'Attention' : undefined, tone: portfolio.summary.needsAttention ? 'warning' : undefined },
|
|
133
|
+
{ key: 'automations', label: 'Automations', summary: automationSummary, href: '#/automations', statusLabel: automations.needsAttention ? 'Attention' : undefined, tone: automations.needsAttention ? 'warning' : undefined },
|
|
134
|
+
{ key: 'capabilities', label: 'Capabilities', summary: capabilitySummary, href: '#/capabilities', statusLabel: (plugins.needsAttention ?? 0) > 0 ? 'Attention' : undefined, tone: (plugins.needsAttention ?? 0) > 0 ? 'warning' : undefined },
|
|
135
|
+
{ key: 'repositories', label: 'Repositories', summary: repositorySummary, href: '#/repositories', statusLabel: repositoryAttention ? 'Attention' : undefined, tone: repositoryAttention ? 'warning' : undefined },
|
|
136
|
+
{ key: 'system', label: 'System', summary: runtimeNeedsAttention ? runtimeLabel : 'Runtime ready', href: '#/system', statusLabel: runtimeNeedsAttention ? 'Attention' : 'Ready', tone: runtimeNeedsAttention ? readinessState : 'success' },
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
return <>
|
|
140
|
+
<CommandBar
|
|
141
|
+
eyebrow="FORGE CONTROL PLANE"
|
|
142
|
+
title="Overview"
|
|
143
|
+
description="需要处理的事项,以及 Forge 各工作区当前状态。"
|
|
144
|
+
refreshedAt={data.generatedAt}
|
|
145
|
+
busy={busy}
|
|
146
|
+
onRefresh={onRefresh}
|
|
147
|
+
/>
|
|
148
|
+
|
|
149
|
+
<div className="overview-home">
|
|
150
|
+
<section className="page-section overview-attention-section">
|
|
151
|
+
<SectionHeader title="Needs attention" meta={attention.length ? `${attention.length} items` : 'All clear'} />
|
|
152
|
+
{attention.length ? <div className="overview-attention-list">
|
|
153
|
+
{attention.map((item) => <a className="overview-attention-row" href={item.href} key={item.key}>
|
|
154
|
+
<div className="overview-attention-source">{item.source}</div>
|
|
155
|
+
<div className="overview-attention-copy">
|
|
156
|
+
<strong>{compact(item.title, 82)}</strong>
|
|
157
|
+
<p>{item.summary}</p>
|
|
158
|
+
</div>
|
|
159
|
+
<StatusText label={item.statusLabel} tone={item.tone} />
|
|
160
|
+
<span className="overview-row-arrow" aria-hidden="true">→</span>
|
|
161
|
+
</a>)}
|
|
162
|
+
</div> : <div className="overview-clear-state">
|
|
163
|
+
<StatusText label="No action needed" tone="success" />
|
|
164
|
+
<span>Forge is operating normally.</span>
|
|
165
|
+
</div>}
|
|
166
|
+
</section>
|
|
167
|
+
|
|
168
|
+
<section className="page-section overview-workspace-section">
|
|
169
|
+
<SectionHeader title="Workspace" meta="Current state" />
|
|
170
|
+
<div className="overview-workspace-list">
|
|
171
|
+
{workspaces.map((workspace) => <a className="overview-workspace-row" href={workspace.href} key={workspace.key}>
|
|
172
|
+
<strong>{workspace.label}</strong>
|
|
173
|
+
<span>{workspace.summary}</span>
|
|
174
|
+
{workspace.statusLabel && <StatusText label={workspace.statusLabel} tone={workspace.tone} />}
|
|
175
|
+
<span className="overview-row-arrow" aria-hidden="true">→</span>
|
|
176
|
+
</a>)}
|
|
177
|
+
</div>
|
|
178
|
+
</section>
|
|
179
|
+
</div>
|
|
180
|
+
</>;
|
|
181
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
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="
|
|
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[adding,setAdding]=useState(false);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} actions={<Button onClick={()=>setAdding(v=>!v)} aria-expanded={adding}>{adding?'Cancel':'Add repository'}</Button>}/><div className="repository-tools"><SearchField value={q} onChange={setQ} placeholder="Search repositories…"/><span className="repository-count">{list.length===all.length?`${all.length} registered`:`${list.length} of ${all.length}`}</span></div>{adding&&<form className="repository-add-panel" onSubmit={e=>{e.preventDefault();if(path.trim())void onRegister(path.trim(),name.trim()||undefined).then(()=>{setPath('');setName('');setAdding(false)})}}><div className="repository-add-fields"><label><span>Local path</span><input autoFocus value={path} onChange={e=>setPath(e.target.value)} placeholder="/absolute/path"/></label><label><span>Display name</span><input value={name} onChange={e=>setName(e.target.value)} placeholder="Optional"/></label><Button type="submit" disabled={busy||!path.trim()}>Register</Button></div><p>只为需要持久化 Work、缓存、并发隔离或发布治理的仓库建立注册项。</p></form>}<div className="split-workspace repository-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></>}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import{useMemo,useState}from'react';import type{ConsoleData,
|
|
2
|
-
type Filter='
|
|
3
|
-
|
|
1
|
+
import{useMemo,useState}from'react';import type{ConsoleData,WorkPortfolioItem}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='open'|'attention'|'completed'|'all';
|
|
3
|
+
function statusOf(w:WorkPortfolioItem){return w.advanced?.status??''}function match(w:WorkPortfolioItem,f:Filter){const s=statusOf(w);if(f==='all')return true;if(f==='attention')return s==='blocked'||s==='failed';if(f==='completed')return s==='completed'||s==='cancelled';return s==='open'||s==='running'||s==='ready'}
|
|
4
|
+
export function WorkView({data,busy,onRefresh}:{data:ConsoleData;busy:boolean;onRefresh:()=>void}){const portfolio=data.workPortfolio;const all=portfolio.items??[];const[filter,setFilter]=useState<Filter>('open');const[repoId,setRepoId]=useState('all');const[selectedId,setSelectedId]=useState<string>();const list=useMemo(()=>all.filter(w=>match(w,filter)&&(repoId==='all'||w.repoId===repoId)),[all,filter,repoId]);const selected=list.find(x=>x.id===selectedId)??list[0];return <><CommandBar eyebrow="EXECUTION WORK" title="Work" description="查看所有已注册仓库的持久 Work;仓库是归属维度,默认聚合展示。" refreshedAt={portfolio.generatedAt} busy={busy} onRefresh={onRefresh}/><div className="toolbar work-toolbar"><Segmented value={filter} onChange={setFilter} items={[{id:'open',label:'Open',count:all.filter(x=>match(x,'open')).length},{id:'attention',label:'Needs attention',count:all.filter(x=>match(x,'attention')).length},{id:'completed',label:'Completed',count:all.filter(x=>match(x,'completed')).length},{id:'all',label:'All',count:all.length}]}/><label className="repository-filter"><span>Repository</span><select value={repoId} onChange={e=>{setRepoId(e.target.value);setSelectedId(undefined)}}><option value="all">All repositories</option>{portfolio.repositories.map(r=><option key={r.repoId} value={r.repoId}>{r.repositoryName}</option>)}</select></label></div><div className="split-workspace"><div className="scan-list">{list.map(w=><button key={w.id} className={`scan-row work-row ${selected?.id===w.id?'selected':''}`} onClick={()=>setSelectedId(w.id)}><div className="scan-main"><span className="row-eyebrow">{w.repositoryName}</span><strong>{w.title}</strong><p>{compact(w.latestSummary||w.objective,108)}</p></div><div className="scan-meta"><StatusText label={w.statusLabel} tone={w.tone}/><time>{formatDate(w.updatedAt)}</time></div></button>)}{!list.length&&<div className="quiet-empty">这个筛选条件下没有 Work。</div>}</div><DetailPane title={selected?.title} subtitle={selected?.objective} empty="选择一个 Work 查看完整上下文">{selected&&<><DefinitionList items={[["Repository",selected.repositoryName],["Status",<StatusText label={selected.statusLabel} tone={selected.tone}/>],["Updated",formatDate(selected.updatedAt)],["Work id",<code>{selected.id}</code>]]}/>{selected.latestSummary&&<div className="detail-callout"><strong>Latest</strong><p>{selected.latestSummary}</p></div>}<p className="detail-note">这里聚合所有仓库的持久 Work。具体执行、检查和继续操作仍由 ChatGPT 主控。</p></>}</DetailPane></div></>}
|
|
@@ -1,14 +1,26 @@
|
|
|
1
|
-
import type { AutomationSettingsView, AutomationsResponse, CommandCenterView, Dict, WorkResponse } from '../types';
|
|
1
|
+
import type { AutomationSettingsView, AutomationsResponse, CommandCenterView, Dict, WorkPortfolioResponse, WorkResponse } from '../types';
|
|
2
2
|
export class ApiError extends Error { constructor(message:string, readonly status:number, readonly payload:unknown){super(message);} }
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
const CONSOLE_REQUEST_TIMEOUT_MS=15_000;
|
|
4
|
+
export async function requestJson<T>(path:string,init:RequestInit={}):Promise<T>{
|
|
5
|
+
const headers=new Headers(init.headers);if(init.body&&!headers.has('content-type'))headers.set('content-type','application/json');
|
|
6
|
+
const controller=new AbortController();let timedOut=false;const callerSignal=init.signal;const abortFromCaller=()=>controller.abort();
|
|
7
|
+
if(callerSignal){if(callerSignal.aborted)abortFromCaller();else callerSignal.addEventListener('abort',abortFromCaller,{once:true});}
|
|
8
|
+
const timer=setTimeout(()=>{timedOut=true;controller.abort();},CONSOLE_REQUEST_TIMEOUT_MS);
|
|
9
|
+
try{
|
|
10
|
+
const response=await fetch(path,{...init,headers,signal:controller.signal,credentials:'same-origin'});let payload:unknown={};try{payload=await response.json();}catch{}
|
|
11
|
+
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);}
|
|
12
|
+
return payload as T;
|
|
13
|
+
}catch(error){
|
|
14
|
+
if(timedOut)throw new Error('Forge Runtime 暂时没有响应。请在连接恢复后重试。');
|
|
15
|
+
throw error;
|
|
16
|
+
}finally{
|
|
17
|
+
clearTimeout(timer);callerSignal?.removeEventListener('abort',abortFromCaller);
|
|
18
|
+
}
|
|
8
19
|
}
|
|
9
20
|
export const api={
|
|
10
21
|
commandCenter:()=>requestJson<CommandCenterView>('/api/console/command-center'),
|
|
11
22
|
work:()=>requestJson<WorkResponse>('/api/console/requirements'),
|
|
23
|
+
workPortfolio:()=>requestJson<WorkPortfolioResponse>('/api/console/work-portfolio'),
|
|
12
24
|
automations:()=>requestJson<AutomationsResponse>('/api/console/automations'),
|
|
13
25
|
automationSettings:()=>requestJson<AutomationSettingsView>('/api/console/automation-settings'),
|
|
14
26
|
connector:()=>requestJson<Dict>('/api/console/connector/status'),
|
|
@@ -1,2 +1,2 @@
|
|
|
1
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"><
|
|
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"><div className="command-meta"><span>Last synced</span><strong>{formatDate(refreshedAt)}</strong></div><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">Open ChatGPT ↗</a></div></header>}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
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
2
|
const icons:Record<RouteId,ComponentType<SVGProps<SVGSVGElement>>>={overview:OverviewIcon,automations:AutomationIcon,work:WorkIcon,capabilities:CapabilityIcon,repositories:RepoIcon,settings:SettingsIcon,system:SystemIcon};
|
|
3
|
-
|
|
3
|
+
const groupLabels={daily:'Workspace',manage:'Configure',system:'System'} as const;
|
|
4
|
+
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 first=item.group!==last;last=item.group;return <div key={item.id} className={first?'nav-group-start':'nav-item'}>{first&&<div className="nav-group-label">{groupLabels[item.group]}</div>}<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"/><span><strong>Runtime connected</strong><small>ChatGPT remains the primary workspace</small></span></div></aside>}
|
|
@@ -1,12 +1,28 @@
|
|
|
1
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:
|
|
3
|
-
.workspace{width:100%;max-width:calc(var(--content-max) +
|
|
4
|
-
.button,.icon-button,.text-button{border:1px solid var(--border-strong);background:var(--surface);color:var(--ink);border-radius:
|
|
5
|
-
.
|
|
6
|
-
.attention-list,.compact-work-list{
|
|
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.
|
|
8
|
-
.detail-pane{border
|
|
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
|
|
10
|
-
.repository-tools{min-height:
|
|
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}.
|
|
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
|
|
2
|
+
.sidebar{position:sticky;top:0;height:100vh;background:linear-gradient(180deg,#1A1A1D 0%,#171719 100%);color:#B8B8BF;padding:20px 14px 16px;display:flex;flex-direction:column;border-right:1px solid #252529}.brand{height:48px;display:flex;align-items:center;gap:11px;padding:0 8px;color:var(--inverse)}.brand-mark{display:grid;place-items:center;width:28px;height:28px;border-radius:9px;background:linear-gradient(145deg,#746CF0,#554AE0);box-shadow:0 6px 18px rgba(94,84,232,.28);color:white;font-weight:780}.brand strong{display:block;font-size:14px;letter-spacing:-.01em}.brand small{display:block;color:#77777F;font-size:10px;margin-top:2px}.sidebar nav{margin-top:20px;display:grid;gap:2px}.sidebar nav>div.nav-group-start{margin-top:15px}.sidebar nav>div.nav-group-start:first-child{margin-top:0}.nav-group-label{height:24px;display:flex;align-items:center;padding:0 10px;color:#626269;font-size:9px;font-weight:750;letter-spacing:.12em;text-transform:uppercase}.sidebar nav a{display:flex;height:36px;align-items:center;gap:10px;padding:0 10px;border-radius:8px;color:#9999A1;position:relative}.sidebar nav a:hover{background:rgba(255,255,255,.045);color:#E0E0E4}.sidebar nav a.active{background:rgba(255,255,255,.075);color:var(--inverse)}.sidebar nav a.active:before{content:"";position:absolute;left:-4px;top:9px;bottom:9px;width:3px;border-radius:3px;background:var(--accent)}.sidebar nav svg{width:16px;height:16px}.sidebar-foot{margin-top:auto;border-top:1px solid #28282C;padding:14px 8px 0;color:#77777F;font-size:10px;display:flex;align-items:flex-start;gap:8px}.sidebar-foot>span:last-child{display:grid;gap:2px}.sidebar-foot strong{font-size:10px;font-weight:620;color:#A7A7AE}.sidebar-foot small{font-size:9px;color:#696970}.pulse-dot{width:7px;height:7px;margin-top:3px;border-radius:50%;background:#5CC58A;box-shadow:0 0 0 3px rgba(92,197,138,.1)}
|
|
3
|
+
.workspace{width:100%;max-width:calc(var(--content-max) + 88px);padding:0 44px 72px;min-width:0}.command-bar{min-height:112px;display:flex;justify-content:space-between;gap:32px;align-items:center;padding:24px 0 20px}.command-title{min-width:0}.eyebrow,.row-eyebrow{font-size:9px;letter-spacing:.12em;color:var(--faint);font-weight:760}.command-title h1{font-size:24px;line-height:1.2;letter-spacing:-.02em;margin:4px 0 6px;font-weight:650}.command-title p{margin:0;color:var(--muted);max-width:720px;line-height:1.5}.command-actions{display:flex;align-items:center;gap:8px;white-space:nowrap}.command-meta{display:grid;justify-items:end;line-height:1.2;margin-right:3px}.command-meta span{font-size:9px;text-transform:uppercase;letter-spacing:.08em;color:var(--faint)}.command-meta strong{font-size:10px;color:var(--muted);font-weight:580;margin-top:3px}.refreshed{display:none}
|
|
4
|
+
.button,.icon-button,.text-button{border:1px solid var(--border-strong);background:var(--surface);color:var(--ink);border-radius:8px;min-height:34px;padding:0 11px;cursor:pointer}.button:hover,.icon-button:hover{background:var(--surface-subtle);border-color:#C5C5C0}.button:disabled,.icon-button:disabled{opacity:.45;cursor:default}.ghost-link{display:inline-flex;align-items:center}.icon-button{width:34px;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
|
+
.overview-hero{margin-top:10px;min-height:190px;display:grid;grid-template-columns:minmax(0,1.7fr) minmax(300px,.8fr);gap:26px;align-items:stretch;padding:28px 30px;border-radius:var(--r-lg);background:linear-gradient(135deg,var(--accent-soft),#FCFCFB 60%,#F1F1EE);box-shadow:inset 0 0 0 1px rgba(94,84,232,.09)}.overview-posture{align-self:center;max-width:760px}.overview-posture-title{display:flex;align-items:center;gap:14px;margin-top:7px}.overview-posture h2{font-size:25px;line-height:1.18;letter-spacing:-.03em;margin:0;font-weight:670}.overview-posture p{margin:10px 0 0;color:var(--muted);font-size:13px;line-height:1.6}.overview-actions{display:grid;grid-template-columns:1fr 1fr;gap:10px;align-self:stretch}.overview-action{min-width:0;border-radius:11px;background:rgba(255,255,255,.68);padding:17px;display:flex;flex-direction:column;justify-content:flex-end;box-shadow:inset 0 0 0 1px rgba(34,34,39,.06)}a.overview-action:hover{background:#FFF;box-shadow:inset 0 0 0 1px rgba(34,34,39,.08);transform:translateY(-1px)}.overview-action.primary{background:var(--ink);color:white;box-shadow:none}.overview-action.primary:hover{background:#2B2B30}.overview-action.calm{background:rgba(255,255,255,.5)}.overview-action span{font-size:10px;color:var(--faint)}.overview-action.primary span,.overview-action.primary small{color:#B9B9C0}.overview-action strong{font-size:28px;line-height:1;margin-top:10px;letter-spacing:-.04em}.overview-action small{font-size:10px;color:var(--muted);margin-top:8px}.signal-strip{margin-top:18px;display:grid;grid-template-columns:repeat(4,1fr);gap:4px;padding:0 4px}.signal-strip a{padding:14px 16px;border-radius:9px;min-width:0;position:relative}.signal-strip a:not(:last-child):after{content:"";position:absolute;right:-2px;top:17px;bottom:17px;width:1px;background:var(--border)}.signal-strip a:hover{background:rgba(255,255,255,.72)}.signal-strip span{display:block;color:var(--muted);font-size:10px}.signal-strip strong{display:block;font-size:19px;letter-spacing:-.02em;margin-top:5px;text-transform:capitalize}.signal-strip strong i{font-style:normal;font-size:12px;color:var(--faint);font-weight:550}.signal-strip small{display:block;color:var(--faint);font-size:10px;margin-top:3px}.page-section{margin-top:34px}.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{border-bottom:1px solid var(--border)}.attention-section .attention-list{margin-top:4px}.attention-row{display:grid;grid-template-columns:42px minmax(0,1fr) auto;align-items:center;gap:12px;min-height:66px;padding:10px 8px;border-bottom:1px solid var(--border);border-radius:8px}.attention-row:hover{background:rgba(255,255,255,.7)}.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:30px 8px;color:var(--muted)}.success-empty{padding:24px 8px;border-radius:10px;background:linear-gradient(90deg,var(--success-soft),transparent 72%)}.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);position:sticky;top:0;z-index:4;background:rgba(245,245,243,.92);backdrop-filter:blur(12px)}.work-toolbar{justify-content:space-between;gap:18px}.repository-filter{display:flex;align-items:center;gap:8px;color:var(--faint);font-size:10px}.repository-filter select{height:30px;max-width:220px;border:1px solid var(--border-strong);border-radius:var(--r-sm);background:var(--surface);color:var(--ink);padding:0 28px 0 9px;font:inherit}.work-row .row-eyebrow{color:var(--accent);letter-spacing:.06em;text-transform:none}.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.65fr) minmax(310px,.85fr);gap:22px;min-height:520px;padding-top:16px}.automation-layout{grid-template-columns:minmax(0,2.1fr) minmax(320px,.9fr)}.scan-list{min-width:0}.scan-row{width:100%;min-height:68px;border:0;border-bottom:1px solid var(--border);border-radius:9px;background:transparent;text-align:left;padding:11px 12px;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:rgba(255,255,255,.72)}.scan-row.selected{background:var(--accent-soft);box-shadow:inset 0 0 0 1px rgba(94,84,232,.12)}.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:1px solid var(--border);border-radius:12px;background:var(--surface);min-width:0;align-self:start;position:sticky;top:18px;overflow:hidden}.detail-head{padding:19px 20px 16px;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;position:sticky;top:58px;background:rgba(245,245,243,.96);backdrop-filter:blur(10px);z-index:2}.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:rgba(255,255,255,.72)}.data-table tbody tr.selected{background:var(--accent-soft);box-shadow:inset 3px 0 var(--accent)}.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:58px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:14px;justify-content:space-between}.repository-count{color:var(--faint);font-size:10px}.repository-tools input,.search-field input,.repository-add-panel input{height:34px;border:1px solid var(--border-strong);border-radius:8px;background:var(--surface);padding:0 10px;color:var(--ink);min-width:170px}.search-field{display:flex;align-items:center;position:relative}.search-field svg{position:absolute;left:10px;width:14px;color:var(--faint)}.search-field input{padding-left:31px;width:250px}.repository-add-panel{margin:14px 0 2px;padding:18px 20px;border-radius:11px;background:var(--surface);border:1px solid var(--border)}.repository-add-fields{display:grid;grid-template-columns:minmax(260px,1.4fr) minmax(180px,.8fr) auto;gap:10px;align-items:end}.repository-add-fields label{display:grid;gap:6px}.repository-add-fields label>span{font-size:10px;color:var(--faint);font-weight:650}.repository-add-fields input{width:100%}.repository-add-panel p{margin:11px 0 0;color:var(--faint);font-size:10px}.repository-workspace{padding-top:14px}.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}.command-meta{justify-items:start}.overview-hero{grid-template-columns:1fr}.overview-actions{min-height:130px}.signal-strip{grid-template-columns:repeat(2,1fr)}.signal-strip a:nth-child(2):after{display:none}.split-workspace,.automation-layout{grid-template-columns:1fr}.detail-pane{position:static;border:1px solid var(--border);margin-bottom:20px}.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,.nav-group-label{display:none}.sidebar nav{display:flex;margin:0 0 0 8px;gap:2px}.sidebar nav>div.nav-group-start{margin:0}.sidebar nav a{white-space:nowrap;height:32px}.sidebar nav svg{display:none}.workspace{padding:0 18px 44px}.command-actions{flex-wrap:wrap}.command-meta{width:100%;justify-items:start}.overview-hero{padding:22px 20px}.overview-actions{grid-template-columns:1fr}.signal-strip{grid-template-columns:1fr 1fr}.signal-strip a:after{display:none}.repository-tools{align-items:center;padding:10px 0}.repository-add-fields{grid-template-columns:1fr}.repository-add-panel{padding:16px}.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}}
|
|
13
|
+
|
|
14
|
+
.overview-posture{margin-top:8px;max-width:none;min-height:144px;display:grid;grid-template-columns:minmax(0,1fr) 180px;gap:32px;align-items:center;padding:24px 4px;border-top:1px solid var(--border);border-bottom:1px solid var(--border)}
|
|
15
|
+
.overview-posture-copy{max-width:760px}.overview-posture h2{font-size:20px;line-height:1.25;letter-spacing:-.02em;font-weight:650}.overview-posture p{margin-top:9px;line-height:1.55}
|
|
16
|
+
.posture-attention{min-height:88px;border-left:1px solid var(--border);padding:8px 0 8px 22px;display:flex;flex-direction:column;justify-content:center}.posture-attention.has-attention{border-left-color:var(--warning)}.posture-attention:hover strong{color:var(--accent)}.posture-attention span,.posture-attention small{font-size:10px;color:var(--faint)}.posture-attention strong{font-size:25px;line-height:1;margin-top:7px;letter-spacing:-.03em}.posture-attention small{margin-top:7px;color:var(--muted)}
|
|
17
|
+
|
|
18
|
+
.signal-strip{margin-top:14px;gap:0;padding:0 4px}.signal-strip a{border-radius:0;padding:14px 16px}.signal-strip a:not(:last-child):after{right:0;top:16px;bottom:16px}.signal-strip a:hover{background:transparent}.signal-strip a:hover strong{color:var(--accent)}
|
|
19
|
+
.detail-pane{border-radius:var(--r-md);background:var(--surface);top:76px;box-shadow:none}.repository-workspace .detail-pane{top:18px}.repository-add-panel{border-radius:var(--r-md);background:var(--surface-subtle);box-shadow:none}
|
|
20
|
+
@media(max-width:1080px){.overview-posture{grid-template-columns:1fr 160px}.detail-pane{top:auto}}
|
|
21
|
+
@media(max-width:760px){.overview-posture{grid-template-columns:1fr;padding:20px 4px}.posture-attention{border-left:0;border-top:1px solid var(--border);padding:16px 0 0;min-height:auto}}
|
|
22
|
+
|
|
23
|
+
/* Overview V2: portfolio-first workspace; hierarchy comes from rows, whitespace and hairlines. */
|
|
24
|
+
.overview-home{width:min(100%,920px);padding-top:2px}.overview-home .page-section{margin-top:0}.overview-home .page-section+.page-section{margin-top:42px}.overview-attention-list,.overview-workspace-list{border-bottom:1px solid var(--border)}.overview-attention-row{min-height:66px;display:grid;grid-template-columns:150px minmax(0,1fr) auto 16px;gap:18px;align-items:center;padding:12px 4px 12px 0;border-top:1px solid var(--border)}.overview-attention-row:hover .overview-attention-copy strong,.overview-workspace-row:hover>strong{color:var(--accent)}.overview-attention-source{color:var(--faint);font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.045em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.overview-attention-copy{min-width:0}.overview-attention-copy strong{display:block;font-size:13px;font-weight:650;line-height:1.35;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.overview-attention-copy p{margin:4px 0 0;color:var(--muted);font-size:11px;line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.overview-attention-row>.status-text{white-space:nowrap}.overview-row-arrow{color:var(--faint);font-size:13px;text-align:right}.overview-clear-state{min-height:52px;display:flex;align-items:center;gap:12px;border-top:1px solid var(--border);border-bottom:1px solid var(--border);color:var(--muted);font-size:12px}.overview-workspace-row{min-height:50px;display:grid;grid-template-columns:150px minmax(0,1fr) auto 16px;gap:18px;align-items:center;padding:0 4px 0 0;border-top:1px solid var(--border)}.overview-workspace-row>strong{font-size:13px;font-weight:650}.overview-workspace-row>span:nth-child(2){color:var(--muted);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.overview-workspace-row>.status-text{white-space:nowrap}
|
|
25
|
+
@media(max-width:760px){.overview-home{width:100%}.overview-home .page-section+.page-section{margin-top:32px}.overview-attention-row{grid-template-columns:minmax(0,1fr) auto 14px;gap:10px;padding:12px 0}.overview-attention-source{grid-column:1/-1;margin-bottom:-4px}.overview-attention-copy p{white-space:normal}.overview-workspace-row{grid-template-columns:112px minmax(0,1fr) 14px;gap:10px}.overview-workspace-row>.status-text{display:none}}
|
|
26
|
+
|
|
27
|
+
/* Automation configuration + execution history */
|
|
28
|
+
.automation-toolbar{justify-content:flex-start}.automation-table th:nth-child(1){width:30%}.automation-table th:nth-child(2){width:17%}.automation-table th:nth-child(3){width:23%}.automation-table th:nth-child(4){width:13%}.automation-table th:nth-child(5){width:17%}.automation-detail-status{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:16px;padding:2px 0 16px;margin-bottom:4px;border-bottom:1px solid var(--border)}.automation-detail-status>div:first-child{display:grid;gap:7px}.automation-result{text-align:right;display:grid;gap:3px}.automation-result strong{font-size:14px;font-weight:650}.automation-result small{color:var(--faint);font-size:10px}.automation-section-head{display:flex;justify-content:space-between;align-items:end;margin:22px 0 8px;padding-top:16px;border-top:1px solid var(--border)}.automation-section-head h3{font-size:13px;margin:3px 0 0;font-weight:650}.automation-section-head>span{font-size:10px;color:var(--faint)}.automation-history{display:grid}.automation-history-row{display:grid;grid-template-columns:12px minmax(0,1fr);gap:9px;padding:10px 0;border-bottom:1px solid var(--border)}.automation-history-row:last-child{border-bottom:0}.automation-history-mark{width:7px;height:7px;border-radius:50%;margin-top:6px;background:var(--faint)}.automation-history-mark.green{background:#268a5c}.automation-history-mark.amber{background:#b27820}.automation-history-mark.red{background:#b74c4c}.automation-history-mark.blue{background:#557bd8}.automation-history-copy{min-width:0}.automation-history-top{display:flex;justify-content:space-between;gap:12px;align-items:center}.automation-history-top time{color:var(--faint);font-size:10px;white-space:nowrap}.automation-history-copy p{margin:5px 0 0;color:var(--muted);font-size:11px;line-height:1.45}.automation-history-copy small{display:block;margin-top:4px;color:var(--faint);font-size:10px}.automation-history-empty{padding:18px 0;color:var(--faint);font-size:11px}.automation-advanced{margin-top:14px}@media(max-width:720px){.automation-detail-status{grid-template-columns:1fr}.automation-result{text-align:left}.automation-table{min-width:680px}}
|
|
@@ -7,8 +7,12 @@ export interface ProviderView { providerId:string; displayName:string; status?:s
|
|
|
7
7
|
export interface LocalToolView { toolId:string; displayName:string; status?:string; summary?:string; enabled?:boolean; executablePath?:string; version?:string; usedByWorkflows?:string[] }
|
|
8
8
|
export interface AutomationSettingsView { schemaVersion?:number; overview?:Dict; providers?:ProviderView[]; localTools?:LocalToolView[]; credentials?:Dict[]; routing?:{orders?:Record<string,string[]>}; warnings?:string[] }
|
|
9
9
|
export interface CommandCenterView { readiness?:Dict; repositories?:RepositoryView[]; handoffs?:Dict[]; plugins?:PluginView[]; pluginSummary?:{total?:number;ready?:number;needsAttention?:number}; warnings?:string[] }
|
|
10
|
-
export
|
|
10
|
+
export type AutomationMode='browser_watch'|'browser_keepalive'|'continuation'|'routine'|'schedule';
|
|
11
|
+
export interface AutomationHistoryView { id:string; at:string; result:string; tone:'green'|'amber'|'red'|'blue'|'gray'; reason?:string; trigger?:string }
|
|
12
|
+
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; mode:AutomationMode; modeLabel:string; live?:boolean; targetLabel?:string; boundWorkId?:string; boundWorkObjective?:string; observationStatus?:'baseline'|'unchanged'|'changed'|'keepalive'|'auth_required'; observationAt?:string; failureCount?:number; policySummary?:string; agentModel?:string; reasoningLevel?:string; tabPolicy?:string; lastRunAt?:string; lastResult?:string; nextRunHint?:string; pausedReason?:string; history:AutomationHistoryView[]; actions:Array<'run'|'pause'|'resume'> }
|
|
11
13
|
export interface AutomationsResponse { schemaVersion:1; generatedAt:string; summary:{total:number;enabled:number;paused:number;needsAttention:number}; automations:AutomationView[] }
|
|
12
14
|
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
15
|
export interface WorkResponse { schemaVersion?:number; requirementCount?:number; activeRequirementCount?:number; waitingForUserCount?:number; needsAttentionCount?:number; requirements?:TrackedWorkView[]; requirementTruncatedCount?:number }
|
|
14
|
-
export interface
|
|
16
|
+
export interface WorkPortfolioItem { id:string; repoId:string; repositoryName:string; updatedAt:string; title:string; objective:string; modeLabel:string; statusLabel:string; tone?:string; phase:string; phaseLabel:string; nextAction:string; latestSummary:string; latestVerification?:Dict; changedFiles?:Dict; advanced?:{workId:string;status:string;checkIds:string[];handoffRefs:string[]} }
|
|
17
|
+
export interface WorkPortfolioResponse { schemaVersion:1; generatedAt:string; summary:{total:number;open:number;needsAttention:number;completed:number}; repositories:Array<{repoId:string;repositoryName:string}>; items:WorkPortfolioItem[] }
|
|
18
|
+
export interface ConsoleData { commandCenter:CommandCenterView; work:WorkResponse; workPortfolio:WorkPortfolioResponse; automations:AutomationsResponse; automationSettings:AutomationSettingsView; connector?:Dict; generatedAt:string }
|
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
verifyEditSession,
|
|
74
74
|
} from "../editing/edit-session";
|
|
75
75
|
import { localBridgeDashboardHtml } from "./dashboard";
|
|
76
|
+
import { readConsoleAsset, type ConsoleAssetName } from "./console-assets";
|
|
76
77
|
import { applyConsoleAutomationAction, listConsoleAutomations, summarizeConsoleAutomations } from "./console-automations";
|
|
77
78
|
import {
|
|
78
79
|
ackConsoleHandoff,
|
|
@@ -170,7 +171,7 @@ import { approveAssistantActionProposal, listAssistantActionProposals, rejectAss
|
|
|
170
171
|
import { assistantModelReadiness } from "../../runtime/assistant/model-provider";
|
|
171
172
|
import { createAssistantStandingGrant, listAssistantStandingGrants, revokeAssistantStandingGrant } from "../../runtime/assistant/standing-grants";
|
|
172
173
|
import { buildAssistantReadinessReport } from "../../runtime/assistant/readiness";
|
|
173
|
-
import {
|
|
174
|
+
import { summarizePluginForLowInterception } from "../../runtime/safe-tooling";
|
|
174
175
|
import { buildModelClientSummary, buildModelControlPlaneSummary, deepSeekControllerManifest, deepSeekFunctionToolManifest, prepareDeepSeekControllerHandoff, prepareDeepSeekControllerRequest, prepareDeepSeekToolCall } from "../../runtime/model-clients";
|
|
175
176
|
import { applyRuntimeCleanup, previewRuntimeCleanup } from "../../runtime/maintenance/cleanup";
|
|
176
177
|
import { assertRecoveryAuthorized, buildCapabilityRecoverySnapshot, buildRecoveryAuditRecord, recoveryActionById, writeRecoveryAuditRecord } from "../../runtime/recovery";
|
|
@@ -994,7 +995,7 @@ export async function startLocalBridgeServer(
|
|
|
994
995
|
if (asset !== "app.js" && asset !== "app.css") { response.status(404).end(); return; }
|
|
995
996
|
response.setHeader("Cache-Control", "no-cache");
|
|
996
997
|
response.type(asset.endsWith(".css") ? "text/css" : "application/javascript");
|
|
997
|
-
response.send(
|
|
998
|
+
response.send(readConsoleAsset(asset as ConsoleAssetName));
|
|
998
999
|
});
|
|
999
1000
|
|
|
1000
1001
|
app.get("/", (_request, response) => {
|
|
@@ -1284,6 +1285,34 @@ export async function startLocalBridgeServer(
|
|
|
1284
1285
|
}
|
|
1285
1286
|
});
|
|
1286
1287
|
|
|
1288
|
+
app.get("/api/console/work-portfolio", (_request, response) => {
|
|
1289
|
+
try {
|
|
1290
|
+
const repositories = loadRepositoryRegistry(controllerHome).repositories
|
|
1291
|
+
.filter((repository) => repository.enabled && !repository.removedAt);
|
|
1292
|
+
const items = repositories.flatMap((repository) =>
|
|
1293
|
+
listConsoleWork({ controllerHome, repository }, "all").map((work) => ({
|
|
1294
|
+
...work,
|
|
1295
|
+
repositoryName: repository.displayName,
|
|
1296
|
+
})),
|
|
1297
|
+
).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
1298
|
+
const statusOf = (item: (typeof items)[number]) => String(item.advanced?.status ?? "");
|
|
1299
|
+
response.json({
|
|
1300
|
+
schemaVersion: 1,
|
|
1301
|
+
generatedAt: new Date().toISOString(),
|
|
1302
|
+
summary: {
|
|
1303
|
+
total: items.length,
|
|
1304
|
+
open: items.filter((item) => ["open", "running", "ready"].includes(statusOf(item))).length,
|
|
1305
|
+
needsAttention: items.filter((item) => ["blocked", "failed"].includes(statusOf(item))).length,
|
|
1306
|
+
completed: items.filter((item) => ["completed", "cancelled"].includes(statusOf(item))).length,
|
|
1307
|
+
},
|
|
1308
|
+
repositories: repositories.map((repository) => ({ repoId: repository.repoId, repositoryName: repository.displayName })),
|
|
1309
|
+
items,
|
|
1310
|
+
});
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
response.status(400).json({ error: errorMessage(error) });
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1287
1316
|
app.get("/api/console/automations", (_request, response) => {
|
|
1288
1317
|
try {
|
|
1289
1318
|
const repositories = loadRepositoryRegistry(controllerHome).repositories;
|
|
@@ -2915,24 +2944,6 @@ export async function startLocalBridgeServer(
|
|
|
2915
2944
|
response.status(404).json({ error: errorMessage(error) });
|
|
2916
2945
|
}
|
|
2917
2946
|
});
|
|
2918
|
-
app.get("/api/toolchain/web-targets", (request, response) => {
|
|
2919
|
-
try {
|
|
2920
|
-
const repository = requestRepositorySelection(request, options, controllerHome);
|
|
2921
|
-
const manifest = getAssistantPluginManifest(controllerHome, repository, "browser");
|
|
2922
|
-
response.json({ targets: listWebTargets(repository.canonicalRoot, manifest), arbitraryUrlAccepted: false });
|
|
2923
|
-
} catch (error) {
|
|
2924
|
-
response.status(400).json({ error: errorMessage(error) });
|
|
2925
|
-
}
|
|
2926
|
-
});
|
|
2927
|
-
app.post("/api/toolchain/web-domain-preview", (request, response) => {
|
|
2928
|
-
try {
|
|
2929
|
-
const repository = requestRepositorySelection(request, options, controllerHome);
|
|
2930
|
-
const manifest = getAssistantPluginManifest(controllerHome, repository, "browser");
|
|
2931
|
-
response.json({ preview: previewBrowserDomainAccess(repository.canonicalRoot, request.body?.domain, request.body?.reason, manifest) });
|
|
2932
|
-
} catch (error) {
|
|
2933
|
-
response.status(400).json({ error: errorMessage(error) });
|
|
2934
|
-
}
|
|
2935
|
-
});
|
|
2936
2947
|
app.get("/api/toolchain/model-clients", (_request, response) => {
|
|
2937
2948
|
response.json({ clients: buildModelClientSummary(), policyOwner: "forge" });
|
|
2938
2949
|
});
|