@moretea-labs/forge 1.5.1 → 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.
Files changed (124) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/DESIGN.md +7 -2
  3. package/README.md +4 -4
  4. package/README.zh-CN.md +2 -2
  5. package/THIRD_PARTY_NOTICES.md +1 -1
  6. package/assets/hooks/pre-edit-guard.sh +8 -39
  7. package/assets/hooks/prompt-guard.sh +2 -2
  8. package/assets/plugin-registry.v1.json +54 -3
  9. package/assets/reference-configs/hook-operations.md +1 -1
  10. package/assets/reference-configs/sprint-contracts.md +2 -2
  11. package/assets/skill-version.json +2 -2
  12. package/assets/templates/helpers/ensure-task-workflow.sh +2 -2
  13. package/assets/templates/helpers/sprint-backlog.sh +6 -6
  14. package/docs/architecture/current/automation-and-schedule-engine.md +9 -3
  15. package/docs/architecture/current/codegraph-context-provider.md +15 -3
  16. package/docs/architecture/current/dispatch-and-agent-strategy.md +32 -13
  17. package/docs/architecture/current/entity-model.md +20 -0
  18. package/docs/architecture/current/external-plugin-governance.md +3 -3
  19. package/docs/architecture/current/system-overview.md +26 -1
  20. package/docs/architecture/current/thin-harness-v1.md +10 -0
  21. package/docs/operations/chatgpt-personal-assistant-runtime.md +12 -2
  22. package/docs/operations/controller-browser-plugin.md +12 -16
  23. package/docs/operations/features.md +1 -1
  24. package/docs/operations/homebrew.md +2 -2
  25. package/docs/tutorials/01-install-and-start.md +1 -1
  26. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  27. package/docs/wiki/Installation.md +1 -1
  28. package/docs/wiki/Quick-Start.md +1 -1
  29. package/package.json +1 -1
  30. package/scripts/check-public-docs.sh +14 -1
  31. package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
  32. package/scripts/sprint-backlog.sh +1 -1
  33. package/src/cli/commands/chatgpt.ts +31 -2
  34. package/src/cli/commands/plugin.ts +36 -9
  35. package/src/cli/controller/context-pack.ts +158 -6
  36. package/src/cli/hook/prompt-guard-decision.ts +17 -18
  37. package/src/cli/hook/prompt-intents.ts +1 -1
  38. package/src/cli/local-bridge/console-automations.ts +205 -23
  39. package/src/cli/local-bridge/console-view-models.ts +2 -0
  40. package/src/cli/local-bridge/facade-api.ts +2 -0
  41. package/src/cli/local-bridge/frontend/src/App.tsx +1 -1
  42. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +143 -3
  43. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +1 -1
  44. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +181 -3
  45. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
  46. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
  47. package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -6
  48. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
  49. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -1
  50. package/src/cli/local-bridge/frontend/src/styles.css +27 -11
  51. package/src/cli/local-bridge/frontend/src/types.ts +6 -2
  52. package/src/cli/local-bridge/server.ts +29 -19
  53. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  54. package/src/cli/local-bridge/ui-dist/app.js +5003 -4815
  55. package/src/cli/mcp/paths.ts +8 -2
  56. package/src/cli/mcp/repository-tools.ts +124 -9
  57. package/src/cli/mcp/server.ts +164 -19
  58. package/src/cli/mcp/transports/http.ts +25 -2
  59. package/src/cli/mcp/transports/session-registry.ts +24 -0
  60. package/src/cli/repositories/command-classifier.ts +113 -0
  61. package/src/cli/repositories/safe-patch.ts +6 -4
  62. package/src/cli/repositories/structured-git.ts +49 -7
  63. package/src/cli/repository/inspector.ts +16 -3
  64. package/src/runtime/assistant/intent.ts +3 -3
  65. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +157 -2
  66. package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
  67. package/src/runtime/control-plane/execution/validation.ts +15 -2
  68. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +32 -0
  69. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +6 -7
  70. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +2 -1
  71. package/src/runtime/control-plane/facade/capability-registry.ts +1 -1
  72. package/src/runtime/control-plane/facade/controller-session-store.ts +12 -13
  73. package/src/runtime/control-plane/facade/goal-workloop-access.ts +7 -2
  74. package/src/runtime/control-plane/facade/goal-workloop.ts +231 -8
  75. package/src/runtime/control-plane/facade/plan-contract-store.ts +32 -12
  76. package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
  77. package/src/runtime/control-plane/facade/types.ts +33 -6
  78. package/src/runtime/control-plane/facade/work-contract-store.ts +31 -2
  79. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +375 -23
  80. package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
  81. package/src/runtime/diagnostics/mcp-timing.ts +6 -0
  82. package/src/runtime/execution/process-runtime/check-facade.ts +1 -10
  83. package/src/runtime/execution/process-runtime/check-result.ts +10 -2
  84. package/src/runtime/execution/process-runtime/command-facade.ts +1 -0
  85. package/src/runtime/execution/process-runtime/gc.ts +64 -10
  86. package/src/runtime/execution/process-runtime/persisted-check.ts +10 -2
  87. package/src/runtime/execution/process-runtime/resource-claims.ts +87 -2
  88. package/src/runtime/execution/process-runtime/runtime.ts +16 -1
  89. package/src/runtime/execution/process-runtime/store.ts +6 -0
  90. package/src/runtime/gateway/mcp/execution-tools.ts +27 -12
  91. package/src/runtime/gateway/mcp/router.ts +1 -1
  92. package/src/runtime/gateway/mcp/runtime-tools.ts +482 -178
  93. package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
  94. package/src/runtime/plugins/browser-adapter.ts +19 -72
  95. package/src/runtime/plugins/browser-handoff-host.ts +6 -31
  96. package/src/runtime/plugins/browser-handoff.ts +0 -3
  97. package/src/runtime/plugins/browser-macos-bridge.ts +8 -0
  98. package/src/runtime/plugins/desktop-operator-registration.ts +8 -11
  99. package/src/runtime/plugins/external-registration.ts +84 -5
  100. package/src/runtime/plugins/first-party-registry.ts +2 -2
  101. package/src/runtime/plugins/gmail-adapter.ts +28 -2
  102. package/src/runtime/plugins/local-system-adapter.ts +2 -2
  103. package/src/runtime/plugins/plugin-management-adapter.ts +140 -0
  104. package/src/runtime/plugins/store.ts +107 -5
  105. package/src/runtime/plugins/types.ts +4 -0
  106. package/src/runtime/recovery/actions.ts +0 -10
  107. package/src/runtime/recovery/classifier.ts +0 -9
  108. package/src/runtime/recovery/maintenance-executor.ts +43 -11
  109. package/src/runtime/recovery/probe.ts +0 -1
  110. package/src/runtime/recovery/types.ts +0 -1
  111. package/src/runtime/root/release-materialize.ts +143 -19
  112. package/src/runtime/root/service.ts +6 -4
  113. package/src/runtime/safe-tooling/index.ts +0 -1
  114. package/src/runtime/safe-tooling/types.ts +0 -30
  115. package/src/runtime/standalone-recovery/core.ts +2 -2
  116. package/src/runtime/workflow/schedules/browser-probe.ts +180 -0
  117. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
  118. package/src/runtime/workflow/schedules/engine.ts +357 -64
  119. package/src/runtime/workflow/schedules/types.ts +5 -0
  120. package/src/runtime/workflow/schedules/work-continuation.ts +100 -27
  121. package/bin/forge-desktop-helper.mjs +0 -204
  122. package/docs/operations/controller-desktop-plugin.md +0 -112
  123. package/src/runtime/plugins/desktop-adapter.ts +0 -374
  124. package/src/runtime/safe-tooling/browser-safe.ts +0 -133
@@ -100,6 +100,8 @@ export interface ConsoleErrorViewModel {
100
100
 
101
101
  export interface WorkSummaryViewModel {
102
102
  id: string;
103
+ repoId: string;
104
+ updatedAt: string;
103
105
  title: string;
104
106
  objective: string;
105
107
  modeLabel: string;
@@ -414,6 +414,8 @@ export function mapWorkSummary(
414
414
  || (work.status === 'running' ? '任务执行中…' : status.label);
415
415
  return {
416
416
  id: work.workId,
417
+ repoId: work.repoId,
418
+ updatedAt: work.updatedAt,
417
419
  title: work.objective.slice(0, 160) || '未命名任务',
418
420
  objective: work.objective,
419
421
  modeLabel: mode.label,
@@ -1,5 +1,5 @@
1
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()}}
2
+ async function loadData():Promise<ConsoleData>{const[commandCenter,work,workPortfolio,automations,automationSettings,connector]=await Promise.all([api.commandCenter(),api.work(),api.workPortfolio(),api.automations(),api.automationSettings(),api.connector().catch(()=>undefined)]);return{commandCenter,work,workPortfolio,automations,automationSettings,connector,generatedAt:new Date().toISOString()}}
3
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
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
5
  useEffect(()=>{void refresh();const onHash=()=>setRoute(routeFromHash());addEventListener('hashchange',onHash);return()=>removeEventListener('hashchange',onHash)},[refresh]);
@@ -1,3 +1,143 @@
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></>}
1
+ import { useMemo, useState } from 'react';
2
+ import type { AutomationHistoryView, AutomationView, ConsoleData } from '../../types';
3
+ import { CommandBar } from '../../shell/CommandBar';
4
+ import { Segmented } from '../../components/Segmented';
5
+ import { DetailPane, DefinitionList } from '../../components/DetailPane';
6
+ import { StatusText } from '../../components/StatusText';
7
+ import { Button } from '../../components/Button';
8
+ import { formatDate, compact } from '../../lib/format';
9
+
10
+ type Filter = 'enabled' | 'paused' | 'attention' | 'all';
11
+
12
+ function statusLabel(status: AutomationView['status']): string {
13
+ if (status === 'enabled') return '运行中';
14
+ if (status === 'paused') return '已暂停';
15
+ if (status === 'attention') return '需要处理';
16
+ return '已停用';
17
+ }
18
+
19
+ function statusTone(status: AutomationView['status']): string {
20
+ if (status === 'enabled') return 'success';
21
+ if (status === 'attention') return 'danger';
22
+ if (status === 'paused') return 'warning';
23
+ return 'neutral';
24
+ }
25
+
26
+ function observationLabel(value?: AutomationView['observationStatus']): string | undefined {
27
+ if (value === 'baseline') return '已建立基线';
28
+ if (value === 'unchanged') return '无变化';
29
+ if (value === 'changed') return '检测到变化';
30
+ if (value === 'keepalive') return '登录保持正常';
31
+ if (value === 'auth_required') return '需要重新登录';
32
+ return undefined;
33
+ }
34
+
35
+ function reasoningLabel(value?: string): string {
36
+ if (value === 'high') return '高';
37
+ if (value === 'medium') return '中';
38
+ if (value === 'xhigh') return '超高';
39
+ return value ?? '—';
40
+ }
41
+
42
+ function tabPolicyLabel(value?: string): string {
43
+ if (value === 'auto') return '自动 · 同一任务优先复用';
44
+ if (value === 'reuse') return '始终复用已绑定会话';
45
+ if (value === 'new') return '每次新开标签页';
46
+ return value ?? '—';
47
+ }
48
+
49
+ function executionDetails(automation: AutomationView): Array<[string, string]> {
50
+ if (!automation.agentModel) return [];
51
+ return [
52
+ ['执行模型', `${automation.agentModel} · ${reasoningLabel(automation.reasoningLevel)}推理`],
53
+ ['浏览器会话', tabPolicyLabel(automation.tabPolicy)],
54
+ ];
55
+ }
56
+
57
+ function historyTone(tone: AutomationHistoryView['tone']): string {
58
+ if (tone === 'green') return 'success';
59
+ if (tone === 'amber') return 'warning';
60
+ if (tone === 'red') return 'danger';
61
+ if (tone === 'blue') return 'info';
62
+ return 'neutral';
63
+ }
64
+
65
+ function History({ items }: { items: AutomationHistoryView[] }) {
66
+ if (!items.length) return <div className="automation-history-empty">还没有执行记录。</div>;
67
+ return <div className="automation-history">{items.map((item) => <div className="automation-history-row" key={item.id}>
68
+ <div className={`automation-history-mark ${item.tone}`} />
69
+ <div className="automation-history-copy">
70
+ <div className="automation-history-top"><StatusText label={item.result} tone={historyTone(item.tone)} /><time>{formatDate(item.at)}</time></div>
71
+ {item.reason && <p>{item.reason}</p>}
72
+ {item.trigger && <small>{item.trigger}触发</small>}
73
+ </div>
74
+ </div>)}</div>;
75
+ }
76
+
77
+ export function AutomationsView({ data, busy, onRefresh, onAction }: {
78
+ data: ConsoleData;
79
+ busy: boolean;
80
+ onRefresh: () => void;
81
+ onAction: (automation: AutomationView, action: 'run' | 'pause' | 'resume') => Promise<void>;
82
+ }) {
83
+ const all = data.automations.automations;
84
+ const [filter, setFilter] = useState<Filter>('enabled');
85
+ const list = useMemo(() => all.filter((automation) => filter === 'all'
86
+ || (filter === 'paused' ? automation.status === 'paused' || automation.status === 'disabled' : automation.status === filter)), [all, filter]);
87
+ const [selectedKey, setSelectedKey] = useState<string>();
88
+ const key = (automation: AutomationView) => `${automation.source}:${automation.repoId}:${automation.id}`;
89
+ const selected = list.find((automation) => key(automation) === selectedKey) ?? list[0];
90
+ const observation = selected ? observationLabel(selected.observationStatus) : undefined;
91
+
92
+ return <>
93
+ <CommandBar eyebrow="AUTOMATION" title="Automations" description="查看长期任务如何触发、当前是否正常,以及每一次实际执行发生了什么。" refreshedAt={data.automations.generatedAt} busy={busy} onRefresh={onRefresh} />
94
+ <div className="toolbar automation-toolbar">
95
+ <Segmented value={filter} onChange={setFilter} items={[
96
+ { id: 'enabled', label: '运行中', count: all.filter((item) => item.status === 'enabled').length },
97
+ { id: 'paused', label: '已暂停', count: all.filter((item) => item.status === 'paused' || item.status === 'disabled').length },
98
+ { id: 'attention', label: '需要处理', count: all.filter((item) => item.status === 'attention').length },
99
+ { id: 'all', label: '全部', count: all.length },
100
+ ]} />
101
+ </div>
102
+ <div className="split-workspace automation-layout">
103
+ <div className="table-wrap">
104
+ <table className="data-table automation-table">
105
+ <thead><tr><th>Automation</th><th>触发</th><th>行为</th><th>状态</th><th>最近结果</th></tr></thead>
106
+ <tbody>{list.map((automation) => <tr key={key(automation)} className={selected && key(selected) === key(automation) ? 'selected' : ''} onClick={() => setSelectedKey(key(automation))}>
107
+ <td><strong>{automation.name}</strong><small>{automation.repositoryName} · {automation.modeLabel}</small></td>
108
+ <td><span>{automation.schedule}</span><small>{automation.timezone ?? '本地时区'}</small></td>
109
+ <td><span>{automation.delivery ?? '本地执行'}</span>{automation.targetLabel && <small>{automation.targetLabel}</small>}</td>
110
+ <td><StatusText label={statusLabel(automation.status)} tone={statusTone(automation.status)} />{automation.live !== undefined && <small>{automation.live ? 'Live' : 'Shadow'}</small>}</td>
111
+ <td><span>{compact(automation.lastResult, 26) || '—'}</span><small>{formatDate(automation.lastRunAt)}</small></td>
112
+ </tr>)}</tbody>
113
+ </table>
114
+ {!list.length && <div className="quiet-empty">这个筛选条件下没有 Automation。</div>}
115
+ </div>
116
+ <DetailPane title={selected?.name} subtitle={selected?.summary} empty="选择一个 Automation 查看配置与执行历史">
117
+ {selected && <>
118
+ <div className="automation-detail-status">
119
+ <div><span className="eyebrow">CURRENT STATE</span><StatusText label={statusLabel(selected.status)} tone={statusTone(selected.status)} /></div>
120
+ <div className="automation-result"><strong>{selected.lastResult ?? '尚未执行'}</strong><small>{formatDate(selected.observationAt ?? selected.lastRunAt)}</small></div>
121
+ </div>
122
+ <DefinitionList items={[
123
+ ['类型', selected.modeLabel],
124
+ ['触发计划', `${selected.schedule}${selected.timezone ? ` · ${selected.timezone}` : ''}`],
125
+ ['触发后的行为', selected.delivery ?? '本地执行'],
126
+ ['观察目标', selected.targetLabel ?? '—'],
127
+ ['观察状态', observation ?? '—'],
128
+ ['绑定 Work', selected.boundWorkObjective ? compact(selected.boundWorkObjective, 96) : selected.boundWorkId ?? '—'],
129
+ ['运行模式', selected.live === undefined ? '—' : selected.live ? 'Live · 会产生实际动作' : 'Shadow · 只记录预演'],
130
+ ...executionDetails(selected),
131
+ ['运行策略', selected.policySummary ?? '—'],
132
+ ]} />
133
+ {selected.pausedReason && <div className="detail-callout warning"><strong>暂停原因</strong><p>{selected.pausedReason}</p></div>}
134
+ <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' ? '立即运行' : action === 'pause' ? '暂停任务' : '开启任务'}</Button>)}</div>
135
+ <div className="automation-section-head"><div><span className="eyebrow">EXECUTION HISTORY</span><h3>最近执行</h3></div><span>{selected.history.length} 条</span></div>
136
+ <History items={selected.history} />
137
+ <details className="advanced automation-advanced"><summary>技术信息</summary><pre>{JSON.stringify({ scheduleId: selected.source === 'schedule' ? selected.id : undefined, workId: selected.boundWorkId, source: selected.source, next: selected.nextRunHint, failureCount: selected.failureCount }, null, 2)}</pre></details>
138
+ <p className="detail-note">这里只展示配置、状态与执行摘要;邮件正文、浏览器 Cookie、登录凭据和 continuation prompt 不会复制到控制台。</p>
139
+ </>}
140
+ </DetailPane>
141
+ </div>
142
+ </>;
143
+ }
@@ -1,4 +1,4 @@
1
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
2
  type Filter='all'|'extensions'|'services'|'execution'|'models';
3
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>}</>}
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},{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>}</>}
@@ -1,3 +1,181 @@
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></>}
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="管理 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></>}
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,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></>}
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
- 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;
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"><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>}
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
- 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>}
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>}