From 437fa1c0c4a08939ee96a0a398c242f490f1e137 Mon Sep 17 00:00:00 2001 From: weichen Date: Wed, 30 Sep 2026 05:52:21 +0000 Subject: [PATCH] Consume Agent-System-UI via source-composition alias Why: The schema renderer and design tokens moved to the new Agent-System-UI repo (D29); Frontend becomes the first host consuming it through the @ui source alias so future inbox/card/workspace components land in the shared library instead of this repo. Evidence: tsc+build clean with modules/ui materialized; vitest 9/9 pass; combined CSS (tokens.css + ui.css + styles.css) verified rule-identical to the pre-migration styles.css except comments and a collision-free block relocation; doc-ref store access replaced by injected DocBridge at the two call sites (plugin panels, card parts, HITL forms). Impact: Requires sibling modules/ui checkout (Dev worktree provides it); vite dedupe keeps single react/marked copies in the bundle. Refs: Agent-System-Documents proposal 2026-09-30-ui-inbox-command-workspace (D29) --- src/components/Conversation.tsx | 7 +- src/components/UIRenderer.tsx | 113 -------------------------------- src/components/Workbench.tsx | 13 +++- src/main.tsx | 2 + src/styles.css | 47 +------------ tsconfig.json | 7 +- tsconfig.tsbuildinfo | 2 +- vite.config.ts | 9 +++ 8 files changed, 35 insertions(+), 165 deletions(-) delete mode 100644 src/components/UIRenderer.tsx diff --git a/src/components/Conversation.tsx b/src/components/Conversation.tsx index 2fcec2e..d781f95 100644 --- a/src/components/Conversation.tsx +++ b/src/components/Conversation.tsx @@ -4,7 +4,8 @@ import { marked } from 'marked' import { api } from '../api' import { store, useStore, activeRun, type LiveState, type ContextObs } from '../store' import type { Message, Part, HITLCard } from '../contracts' -import { UIRenderer } from './UIRenderer' +import { UIRenderer } from '@ui' +import { docBridge } from './Workbench' marked.setOptions({ breaks: true }) @@ -34,7 +35,7 @@ function PartView({ part, live }: { part: Part; live?: boolean | null }) { case 'plan': return null // 计划在右栏 Plan 标签呈现 case 'card': - return
+ return
case 'error': return
{part.error?.code}: {part.error?.message}
default: @@ -154,7 +155,7 @@ function HITLCardView({ card }: { card: HITLCard }) { )} - {card.form && card.form.length > 0 && } + {card.form && card.form.length > 0 && } ) } diff --git a/src/components/UIRenderer.tsx b/src/components/UIRenderer.tsx deleted file mode 100644 index 7935cf7..0000000 --- a/src/components/UIRenderer.tsx +++ /dev/null @@ -1,113 +0,0 @@ -// UI schema 通用渲染器(4.1 原则 3):有限词汇;插件面板/卡片/HITL 表单/设置共用。 -// 未知词汇静默忽略。插件不注入 React 组件——高级交互走 Core 契约扩展。 -import { useState } from 'react' -import type { UINode } from '../contracts' -import { store } from '../store' - -export function UIRenderer({ nodes, onAction }: { nodes: UINode[]; onAction?: (id: string) => void }) { - return <>{nodes.map((n, i) => )} -} - -function UINodeView({ node, onAction }: { node: UINode; onAction?: (id: string) => void }) { - const [form, setForm] = useState>({}) - switch (node.type) { - case 'section': - return ( -
- {node.title &&
{node.title}
} - {node.children && } -
- ) - case 'list': - return ( -
- {node.title &&
{node.title}
} -
    {node.items?.map((it, i) =>
  • {it}
  • )}
-
- ) - case 'kv': - return ( -
- {node.title &&
{node.title}
} -
{node.kv?.map(([k, v], i) => [
{k}
,
{v}
])}
-
- ) - case 'markdown': - return
- case 'doc-ref': { - const artifacts = store.getState().artifacts - const art = artifacts.find(a => a.id === node.docId) - return ( -
- -
- ) - } - case 'actions': - return ( -
- {node.actions?.map(a => ( - - ))} -
- ) - case 'form': - return ( -
- {node.title &&
{node.title}
} - {node.fields?.map(f => ( - - ))} -
- ) - case 'chart-lite': - return - default: - return null // 未知词汇静默忽略 - } -} - -// 无依赖迷你条形图(chart-lite 词汇的最小实现)。 -function ChartLite({ node }: { node: UINode }) { - const chart = node.chart - if (!chart || chart.series.length === 0) return null - const max = Math.max(...chart.series.map(Math.abs), 1) - return ( -
- {node.title &&
{node.title}
} - {chart.labels.map((label, i) => ( -
- {label} -
-
-
- {chart.series[i]} -
- ))} -
- ) -} - -// marked 引入放 UIRenderer 顶部以复用(markdown 词汇渲染)。 -import { marked } from 'marked' -marked.setOptions({ breaks: true }) -function mdSafe(s: string): string { - return marked.parse(s) as string -} diff --git a/src/components/Workbench.tsx b/src/components/Workbench.tsx index da14ed2..46bc84e 100644 --- a/src/components/Workbench.tsx +++ b/src/components/Workbench.tsx @@ -1,7 +1,16 @@ // 右栏工作台:Plan / Todos / Artifacts / Agents + 插件面板(4.4);tab 显隐排序见设置。 import { store, useStore, isTerminal } from '../store' import type { Run } from '../contracts' -import { UIRenderer } from './UIRenderer' +import { UIRenderer, type DocBridge } from '@ui' + +// 工件解析/打开注入:渲染器纯展示,doc-ref 词汇经此桥接本仓状态(D29)。 +export const docBridge: DocBridge = { + resolveDoc: id => store.getState().artifacts.find(a => a.id === id), + onOpenDoc: id => { + const art = store.getState().artifacts.find(a => a.id === id) + if (art) store.viewArtifact(art) + }, +} export function Workbench() { const ui = useStore(s => s.ui) @@ -34,7 +43,7 @@ export function Workbench() { {ui.rightTab === 'artifacts' && } {ui.rightTab === 'agents' && } {ui.rightTab.startsWith('plugin:') && ( - `plugin:${p.id}` === ui.rightTab)?.body ?? []} /> + `plugin:${p.id}` === ui.rightTab)?.body ?? []} docBridge={docBridge} /> )}
diff --git a/src/main.tsx b/src/main.tsx index 8661d46..238076b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,8 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { App } from './App' +import '@ui/tokens.css' +import '@ui/ui.css' import './styles.css' createRoot(document.getElementById('root')!).render( diff --git a/src/styles.css b/src/styles.css index 7c640b3..1038ba3 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,37 +1,4 @@ -/* 设计令牌(4.15):组件只消费令牌;亮/暗主题切换 = data-theme 属性。 */ -:root, :root[data-theme='dark'] { - --bg: #16181d; - --bg-raised: #1d2026; - --bg-panel: #191c21; - --bg-hover: #262a31; - --bg-active: #2d323b; - --border: #2b2f37; - --text: #e4e7ec; - --text-dim: #9aa1ac; - --text-faint: #6b7280; - --accent: #4f8cff; - --accent-soft: rgba(79, 140, 255, 0.14); - --ok: #3fb972; - --warn: #e0a83a; - --err: #e5605c; - --radius: 8px; - --radius-sm: 6px; - --font: -apple-system, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC', sans-serif; - --mono: ui-monospace, 'Cascadia Code', 'SF Mono', Consolas, monospace; -} -:root[data-theme='light'] { - --bg: #f7f8fa; - --bg-raised: #ffffff; - --bg-panel: #f2f3f6; - --bg-hover: #e9ebf0; - --bg-active: #dfe3ea; - --border: #dcdfe6; - --text: #1c2230; - --text-dim: #5a6272; - --text-faint: #8b93a3; - --accent: #2f6fe4; - --accent-soft: rgba(47, 111, 228, 0.1); -} +/* 设计令牌与 UI 组件样式已迁入 Agent-System-UI(@ui/tokens.css、@ui/ui.css,main.tsx 引入)。 */ * { box-sizing: border-box; } html, body, #root { height: 100%; margin: 0; } @@ -182,23 +149,13 @@ button { font-family: inherit; } .effect-hot { color: var(--ok); border-color: var(--ok); } .effect-new-run { color: var(--warn); border-color: var(--warn); } -/* ---- UI schema 渲染器 ---- */ -.ui-section { margin: 10px 0; } -.ui-section h5 { margin: 0 0 6px; font-size: 13px; } -.ui-list li { margin: 3px 0; font-size: 13px; } -.ui-kv { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 13px; } -.ui-kv dt { color: var(--text-faint); } -.ui-actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 8px 0; } -.ui-form label { display: block; font-size: 12.5px; color: var(--text-dim); margin: 8px 0 3px; } -.ui-form input, .ui-form select { width: 100%; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text); padding: 7px 10px; } +/* UI schema 渲染器样式(.ui-*)与 .doc-viewer 已迁入 @ui/ui.css。 */ /* ---- 杂项 ---- */ .toast { position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); background: var(--bg-raised); border: 1px solid var(--border); color: var(--text); padding: 9px 16px; border-radius: 20px; box-shadow: 0 8px 30px rgba(0,0,0,.35); z-index: 90; font-size: 13px; } .conn-dot { width: 8px; height: 8px; border-radius: 50%; } .conn-dot.on { background: var(--ok); } .conn-dot.off { background: var(--err); } -.doc-viewer { font-size: 13.5px; line-height: 1.7; } -.doc-viewer pre { background: var(--bg); border: 1px solid var(--border); padding: 10px; border-radius: var(--radius-sm); overflow-x: auto; } .doc-toolbar { display: flex; gap: 8px; margin-bottom: 10px; align-items: center; } .search-input { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text); padding: 6px 10px; font-family: inherit; width: 100%; } ::-webkit-scrollbar { width: 9px; height: 9px; } diff --git a/tsconfig.json b/tsconfig.json index 1a066cc..71e593e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,12 @@ "skipLibCheck": true, "isolatedModules": true, "noEmit": true, - "types": ["vite/client"] + "types": ["vite/client"], + "baseUrl": ".", + "paths": { + "@ui": ["../ui/src/index.ts"], + "@ui/*": ["../ui/src/*"] + } }, "include": ["src", "test"] } diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index b044b4e..fe98e6f 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/api.ts","./src/auth.ts","./src/contracts.ts","./src/main.tsx","./src/store.ts","./src/components/AgentTracker.tsx","./src/components/Conversation.tsx","./src/components/DocViewer.tsx","./src/components/ProjectNav.tsx","./src/components/SettingsModal.tsx","./src/components/UIRenderer.tsx","./src/components/Workbench.tsx","./test/auth.test.ts","./test/contracts.test.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/App.tsx","./src/api.ts","./src/auth.ts","./src/contracts.ts","./src/main.tsx","./src/store.ts","./src/components/AgentTracker.tsx","./src/components/Conversation.tsx","./src/components/DocViewer.tsx","./src/components/ProjectNav.tsx","./src/components/SettingsModal.tsx","./src/components/Workbench.tsx","./test/auth.test.ts","./test/contracts.test.ts"],"version":"5.9.3"} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 2e5b959..36cb30d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,8 +1,17 @@ +import { fileURLToPath } from 'node:url' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +// UI 组件库源码组合(modules/ui,源码级别名;见 Agent-System-Documents 提案 D29)。 +const uiSrc = fileURLToPath(new URL('../ui/src', import.meta.url)) + export default defineConfig({ plugins: [react()], + resolve: { + alias: [{ find: '@ui', replacement: uiSrc }], + // 源码组合下 ui 仓有自己的 node_modules;dedupe 强制最终 bundle 用 frontend 的单副本。 + dedupe: ['react', 'react-dom', 'marked'], + }, server: { port: 5173, proxy: { -- 2.54.0