# Control UI 前端开发指南

> 本文档描述 `ui/` 目录下 Control UI 的整体架构与开发约定。

---

## 技术栈

| 层次     | 技术                                         |
| -------- | -------------------------------------------- |
| UI 框架  | [Lit 3.x](https://lit.dev)（Web Components） |
| 语言     | TypeScript ESM，strict 模式                  |
| 构建工具 | Vite 8                                       |
| 样式     | 全局 CSS（无 CSS-in-JS）                     |
| 后端通信 | WebSocket gateway（非 REST）                 |

---

## 核心架构

### 单组件 + 扁平状态

整个 UI 只有 **一个 LitElement 组件** (`app.ts`)，所有响应式状态都以 `@state()` 装饰器挂在它上面。

```
app.ts  (LitElement)
  │
  ├── @state() tab = "chat"
  ├── @state() fileTreeOpen = false
  ├── @state() chatMessages = []
  └── ...（数百个 @state() 字段）
```

渲染时把 `this` 直接传给纯函数 `renderApp(this)`，所以所有渲染函数收到的 `state` 参数就是组件实例本身。

**修改状态 = 修改组件实例字段**，Lit 检测到变化自动触发重渲染：

```typescript
// 等价于 this.fileTreeOpen = true
state.fileTreeOpen = true;
```

### 数据流

```
用户操作
  │
  ▼
事件处理器（在 template 里内联或来自 app-render.helpers.ts）
  │
  ├── 直接修改 state.xxx（触发重渲染）
  └── 调用 controllers/ 里的异步函数
        │
        ├── state.client.request("method", params)   ← gateway API
        └── 修改 state.xxx（触发重渲染）
```

---

## 目录结构

```
ui/src/ui/
├── app.ts                     # LitElement 组件；声明所有 @state() 字段和初始值
├── app-view-state.ts          # TypeScript 接口，描述 AppViewState 形状（不含逻辑）
├── app-render.ts              # 主渲染函数 renderApp()，按 tab 分发视图
├── app-render.helpers.ts      # 可复用的小渲染片段（topbar 按钮、状态指示等）
├── app-chat.ts                # 聊天相关副作用
├── app-settings.ts            # 设置、主题、导航相关
├── app-lifecycle.ts           # 组件生命周期钩子
├── app-gateway.ts             # WebSocket 连接管理
├── icons.ts                   # Lucide 风格 SVG 图标集合
├── navigation.ts              # 导航 tab 定义和分组
├── types.ts                   # UI 层类型（响应体 VO 等）
│
├── controllers/               # 副作用层：调 API、修改 state
│   ├── agents.ts
│   ├── agent-files.ts
│   ├── workspace-output.ts
│   └── ...
│
├── views/                     # 复杂视图的渲染逻辑（从 app-render.ts 拆出）
│   ├── chat.ts
│   ├── agents-panels-status-files.ts
│   └── ...
│
└── chat/                      # 聊天子系统

ui/src/styles/
├── layout.css                 # Shell 布局、topbar、sidebar、内容区
├── components.css             # 通用组件样式（按钮、卡片、表单等）
├── chat.css                   # 聊天视图样式
└── ...
```

---

## 与后端通信

后端是一个本机运行的 Node.js **gateway 进程**，通过 **WebSocket** 暴露 RPC 接口。

### 发起请求

```typescript
const res = await state.client.request<MyResultType>("method.name", { param1, param2 });
```

### 监听事件

```typescript
state.client.on("session.message", (event) => {
  // 处理推送事件
});
```

### 可用方法

所有方法名注册在 `src/gateway/server-methods-list.ts`。完整协议定义在 `src/gateway/protocol/`。

---

## 添加新功能的完整流程

以"查看工作区输出文件"为例，完整改动路径如下：

### 1. 后端协议层

**`src/gateway/protocol/schema/agents-models-skills.ts`**

```typescript
// 定义请求参数和响应结果的 TypeBox schema
export const WorkspaceOutputListParamsSchema = Type.Object({ agentId: NonEmptyString }, ...);
export const WorkspaceOutputListResultSchema = Type.Object({ ... }, ...);
```

**`src/gateway/protocol/schema/protocol-schemas.ts`**

```typescript
// 注册到 ProtocolSchemas 对象
WorkspaceOutputListParams: WorkspaceOutputListParamsSchema,
WorkspaceOutputListResult: WorkspaceOutputListResultSchema,
```

**`src/gateway/protocol/schema/types.ts`** — 导出 TypeScript 类型
**`src/gateway/protocol/index.ts`** — 导出 AJV 编译后的 validator 和类型

### 2. 后端处理器

**`src/gateway/server-methods/agents.ts`**

```typescript
"workspace.output.list": async ({ params, respond, context }) => {
  if (!validateWorkspaceOutputListParams(params)) { ... }
  // 实现逻辑
  respond(true, result, undefined);
},
```

**`src/gateway/server-methods-list.ts`**

```typescript
"workspace.output.list",  // 注册方法名
```

### 3. 前端类型

**`ui/src/ui/types.ts`**

```typescript
export type WorkspaceOutputListResult = { agentId: string; files: WorkspaceOutputFile[]; ... };
```

### 4. 状态声明

**`ui/src/ui/app-view-state.ts`** — 在接口里加字段：

```typescript
fileTreeOpen: boolean;
fileTreeLoading: boolean;
fileTreeResult: WorkspaceOutputListResult | null;
```

**`ui/src/ui/app.ts`** — 用 `@state()` 初始化：

```typescript
@state() fileTreeOpen = false;
@state() fileTreeResult: WorkspaceOutputListResult | null = null;
```

### 5. 控制器

**`ui/src/ui/controllers/workspace-output.ts`**

```typescript
export async function loadWorkspaceOutput(state: WorkspaceOutputState, agentId: string) {
  if (!state.client || !state.connected || state.fileTreeLoading) return;
  state.fileTreeLoading = true;
  try {
    const res = await state.client.request("workspace.output.list", { agentId });
    if (res) state.fileTreeResult = res;
  } finally {
    state.fileTreeLoading = false;
  }
}
```

### 6. 渲染

**`ui/src/ui/app-render.helpers.ts`** — 小片段（topbar 按钮）：

```typescript
export function renderTopbarFileTreeToggle(state: AppViewState) {
  return html`<button
    @click=${() => {
      state.fileTreeOpen = !state.fileTreeOpen;
    }}
  >
    ...
  </button>`;
}
```

**`ui/src/ui/app-render.ts`** — 引用按钮和面板：

```typescript
${renderTopbarFileTreeToggle(state)}
${renderFileTreePanel(state)}
```

### 7. 样式

**`ui/src/styles/layout.css`** — 添加对应 CSS 类。

---

## 关键约定

### 渲染函数中的副作用

惰性加载数据的惯用模式：

```typescript
function renderSomePanel(state: AppViewState) {
  // 首次渲染时触发数据加载（有 loading 守卫，不会重复请求）
  if (!state.someData && !state.someLoading && !state.someError) {
    void loadSomeData(state, agentId);
  }
  // 渲染 UI...
}
```

### 状态修改

```typescript
// 直接赋值即可触发重渲染
state.tab = "chat";
state.fileTreeOpen = true;
// 对象/数组需要新引用
state.chatMessages = [...state.chatMessages, newMsg];
```

### 图标使用

```typescript
import { icons } from "./icons.ts";
// 在 html 模板里直接插值
html`${icons.folder}`;
```

所有图标均为 Lucide 风格 SVG，定义在 `ui/src/ui/icons.ts`。

### 样式规范

- 使用设计系统 CSS 变量：`var(--accent)`、`var(--muted)`、`var(--border)`、`var(--bg-elevated)` 等
- 颜色混合：`color-mix(in srgb, var(--border) 74%, transparent)`
- 圆角：`var(--radius-md)`、`var(--radius-full)` 等
- 过渡时长：`var(--duration-fast)`

### 格式化

```bash
# 检查
pnpm exec oxfmt --check --threads=1 <files...>
# 修复
pnpm exec oxfmt --write --threads=1 <files...>
```

---

## 开发命令

```bash
pnpm dev           # 启动开发服务器
pnpm build         # 构建
pnpm check:changed # 检查改动文件的类型和测试（推荐在 Testbox 上跑）
pnpm test <file>   # 针对性测试
```
