Compare commits

...

1 Commits

Author SHA1 Message Date
Naiyuan Qing
bcfb98a576 chore(docs): remove shipped agent-runtime redesign + workspace audit docs
These were transitional handoff/design docs that fulfilled their purpose:

- docs/agent-runtime-status-redesign.md (802 lines) — design + plan for
  PR #1794 (presence v3, availability + last-task split). Shipped.
- docs/agent-runtime-ui-design-brief.md (530 lines) — paired designer
  brief for the same redesign. Shipped.
- HANDOFF_ARCHITECTURE_AUDIT.md (383 lines) — 4-task audit packaged for
  the workspace URL refactor (PR #1138/#1141). The URL refactor itself
  shipped; the other tasks are either resolved or live in code as the
  source of truth. File:line snapshots inside have rotted.

Follows the precedent set by #1504 (chore(docs): remove shipped plan and
proposal docs). Code is the source of truth once the work is in.
2026-04-29 15:07:45 +08:00
3 changed files with 0 additions and 1715 deletions

View File

@@ -1,383 +0,0 @@
# Architecture Audit — Workspace & Realtime Cache
> 基于代码审计整理的 4 个任务。优先级P0 一个、P1 一个、P2 两个。每个任务都包含问题、根因、受影响的 issue、复现步骤、修复方案、改动范围。
---
## 任务 1 — [P0] 空闲后列表数据陈旧
**关联 issue**[#951](https://github.com/multica-ai/multica/issues/951)
### 问题
用户登录后静置一段时间Issue 列表里缺失一部分数据(其他成员期间新建/变更的 issue 不出现)。登出再登入可以恢复。`ec5af33b` 声称 "Closes #951",但 issue 仍为 OPEN 状态 —— 因为它只修了 401 一种场景,没修 WS 半开这一种。
### 根因
系统把 cache 新鲜度的全部责任压给了 WebSocket 推送:
- `packages/core/query-client.ts:7``staleTime: Infinity`cache 永不主动过期
- `packages/core/query-client.ts:9``refetchOnWindowFocus: false`tab 重新获得焦点也不 refetch
- 依赖 WS 推送 `issue:created` / `issue:updated` 事件 invalidate cache
但 WS 层存在一个**不对称**
- **服务端**`server/internal/realtime/hub.go:83-96, 420-475` 有 54s ping / 60s pongWait会清理死连接
- **客户端**`packages/core/api/ws-client.ts`142 行全貌)**完全没有心跳检测**,只靠 `onclose` 事件触发重连
浏览器原生 `WebSocket` API 不把 ping/pong 帧暴露给 JS所以 JS 层无法主动探测 "半开" 连接。当 NAT / 负载均衡器 / 笔记本睡眠导致 TCP 连接被静默切断时:
1. 浏览器 `readyState` 仍是 `OPEN`
2. `onclose` 不触发
3. `ws-client.ts:70-73` 的 3 秒重连逻辑不跑
4. `packages/core/realtime/use-realtime-sync.ts:462-487``onReconnect` 全量 invalidate 不跑
5. 期间的 WS 事件进黑洞
6. cache 保持旧快照
### 复现
**浏览器 DevTools 里的 "Block request URL" 不行** —— 那会触发 `onclose`,走正常重连 → 不复现。真正的半开需要在网络层静默丢包。
**方法 A推荐最接近真实场景**macOS 用 pfctl 丢包
```bash
# 假设后端在 8080
sudo pfctl -E
echo "block drop out quick proto tcp to any port 8080" | sudo pfctl -f -
# 观察:
# - Console 里没有 "disconnected, reconnecting in 3s" 日志
# - Network 里 WS 连接仍显示 Pending / 101
# 用另一个账号/CLI 创建一个 issue
# 回到原客户端: 列表不更新
# 登出再登入: 列表恢复完整
sudo pfctl -d # 解除
```
**方法 B不动网络**:临时修改代码,在 `packages/core/api/ws-client.ts:52``onmessage` 处理器里加一行 `return;` 在前面,吞掉所有入站消息。效果等价于半开。
### 修复方案(三个选项,推荐 C
#### 选项 A — 浏览器端心跳探活(治本,改动大)
`ws-client.ts` 加客户端侧的心跳检测:记录 `lastMessageTime`,定时器检查若超过 N 秒没收到任何消息就主动 `ws.close()`,触发现有重连逻辑。
- 优点:从根本上解决半开问题
- 缺点:浏览器原生 API 没有 ping 能力,需要服务端配合发"应用层 heartbeat"消息供客户端更新 `lastMessageTime`;服务端改 + 客户端改
#### 选项 B — Page Visibility API 触发 invalidate治标改动小
`packages/core/platform/core-provider.tsx``visibilitychange` 监听tab 重新可见时强制 `queryClient.invalidateQueries({ queryKey: issueKeys.all(wsId) })`(及其他关键 key
- 优点:~10 行代码,能兜住 80% 场景(睡眠、切后台 tab
- 缺点treats symptom, 不是真正的半开检测;对"一直保持 tab 可见但网络层断了"的场景无效
#### 选项 C — **A + B 组合**(推荐)
- 短期上 B立刻止血
- 中期上 A把 cache 新鲜度从"只信 WS"改成"WS 是优化Visibility 是兜底"
- 可选加 `refetchOnWindowFocus: true` 或把 `staleTime` 改成一个有限值(比如 5 min作为第三层保险
### 改动范围
| 方案 | 文件 | 改动规模 |
|---|---|---|
| B | `packages/core/platform/core-provider.tsx` | ~10 行 |
| A 客户端 | `packages/core/api/ws-client.ts` | ~30 行 |
| A 服务端 | `server/internal/realtime/hub.go` | 加 app-level heartbeat message |
### 验证
修完之后:
1. 跑方法 A 复现流程,确认数据不再丢失
2. 加 e2e 测试:模拟 `document.dispatchEvent(new Event('visibilitychange'))` + 验证 issue list 被 refetch
---
## 任务 2 — [P1] Workspace 不在 URL 路径中
**关联 issue**MUL-723slug 不在 URL、MUL-43切换 workspace 报错、MUL-509手机端无法切换
> **注意**:审计中提到的 MUL-43 / MUL-476 issue 编号需要当面核对一次 —— agent 查询 GitHub 后返回的标题对不上(看起来是别的 PR。交接时请让执行人以具体症状为准。
### 问题
当前 workspace 身份完全靠 `X-Workspace-ID` HTTP header + Zustand store + localStorage 承载URL 里没有 workspace 信息。所有路径都是 `/issues``/issues/:id` 这种 workspace-agnostic 的。
### 根因
**数据库和 API 已经支持 slug**
- `server/migrations/001_init.up.sql:15-23` — workspace 表有 `slug TEXT UNIQUE NOT NULL`
- `server/pkg/db/queries/workspace.sql:11-13` — 有 `GetWorkspaceBySlug` 查询
- `packages/core/types/workspace.ts:8-19` — Workspace 类型里有 slug 字段
**但前端路由和导航层没用它**
- Web 路由:`apps/web/app/(dashboard)/` 下 25 个 route file 都是 workspace-implicit
- Desktop 路由:`apps/desktop/src/renderer/src/routes.tsx:71-143` 同样
- Navigation 适配器 `apps/web/platform/navigation.tsx` 直接透传 `router.push`,没有任何 workspace 前缀逻辑
**workspace 切换只靠 sidebar UI**`packages/views/layout/app-sidebar.tsx:284-286`
```tsx
if (ws.id !== workspace?.id) {
push("/issues"); // 硬跳 /issuesworkspace-implicit
switchWorkspace(ws); // 然后改 store
}
```
这种设计使得:
- 手机端因为没 sidebar UI也没 URL 层切换入口,**完全切不了 workspace**MUL-509
-`/issues/xxx` 链接发给处于不同 workspace 的同事,会打开错误 workspace 下的 issue或找不到报错MUL-43 系列)
- 分享链接没有 workspace 上下文,接收方必须先手动切对 workspace
### 复现
1. **MUL-723**:登录 → 观察地址栏,没有任何 workspace 标识
2. **MUL-43**
- 加入两个 workspace A 和 B
- 在 A 中打开某个 issue `/issues/abc123`
- 切到 BURL 不变 → 访问失败 / 显示错数据
3. **MUL-509**:手机浏览器打开,尝试切 workspace → 无法切换UI 不显示 sidebar 触发器或触发器无法切)
### 修复方案(三个选项,推荐 A
#### 选项 A — `/ws/:slug/...` URL 前缀(根本方案,推荐)
所有路径加上 workspace slug 前缀。例如 `/issues/abc123``/ws/my-team/issues/abc123`
**要改的地方**
1. **Web 路由目录结构**`apps/web/app/(dashboard)/` 下全部搬到 `apps/web/app/(dashboard)/ws/[slug]/...`~25 个文件)
2. **Desktop 路由**`apps/desktop/src/renderer/src/routes.tsx:71-143` 给所有路径加 `/ws/:slug` 前缀
3. **Navigation 适配器**
- `apps/web/platform/navigation.tsx``push(path)` 内部前置 `/ws/${workspace.slug}``pathname` 读取时去掉前缀
- `apps/desktop/src/renderer/src/platform/navigation.tsx` — 同上
4. **Sidebar 切换逻辑**`packages/views/layout/app-sidebar.tsx:284-286` 改成 `push('/ws/${ws.slug}/issues')`(或依赖适配器自动加前缀就不用改)
5. **服务端中间件**`server/internal/middleware/workspace.go:41-46` 增加 "从 URL path 解析 slug → 查 ID → 校验 membership" 的逻辑header 继续作为 fallback迁移期兼容
**预计改动**~50-100 个文件(大部分是 route 搬迁,不是逻辑改动)、~5-7 人天
**不改也能工作的部分**
- `packages/core/api/client.ts` — 仍旧走 header不用改
- 所有 `packages/views/` 下的组件 —— 它们用 `useNavigation().push()` 抽象,适配器层处理前缀就行
**风险**
- 旧的 bookmark URL 失效(如果产品还没正式 ship问题不大
- E2E 测试需要更新所有 URL 断言
#### 选项 B — `?ws=slug` query param折中
URL 形如 `/issues?ws=my-team`。改动更小(~30 个文件URL 丑但向后兼容。推荐度低于 A。
#### 选项 C — 只修症状不动架构
`switchWorkspace` 和各个 query 之间加 debounce、error boundary 等 workaround。不解决根因技术债越攒越多。**不推荐**。
### 改动范围(选项 A
| 模块 | 文件数 | 备注 |
|---|---|---|
| Web routes | ~25 | 目录搬迁 |
| Desktop routes | 1 | 路径前缀 |
| Navigation adapters | 2 | 前缀逻辑 |
| Server middleware | 1-2 | slug → ID 解析 |
| 组件(不用改) | 30-40 | 用 `useNavigation` 的不受影响 |
| E2E tests | 20-30 | URL 断言更新 |
---
## 任务 3 — [P1] Workspace 切换时 navigation 状态未隔离
**关联 issue**MUL-43切换报错、MUL-476本地缓存未按 workspace 隔离)
> 同上,这两个编号建议交接时核对症状。
### 问题
绝大多数 workspace-scoped 的 Zustand store 都正确使用了 `createWorkspaceAwareStorage`key 后缀加 wsId 自动隔离),但 **`useNavigationStore` 是个例外**:它持久化了 `lastPath`,但用的是 global storage切换 workspace 后里面仍是上个 workspace 的路径。
### 根因
**`packages/core/navigation/store.ts:15-31`**
```typescript
export const useNavigationStore = create<NavigationState>()(
persist(
(set) => ({
lastPath: "/issues",
onPathChange: (path) => { /* ... */ set({ lastPath: path }); },
}),
{
name: "multica_navigation",
storage: createJSONStorage(() => createPersistStorage(defaultStorage)), // ← 这里用的是 global不是 workspace-aware
partialize: (state) => ({ lastPath: state.lastPath }),
}
)
);
// ← 没有调 registerForWorkspaceRehydration
```
**对比:其他 store 都是正确的**
| Store | 是否 workspace-aware | 是否注册 rehydration |
|---|---|---|
| useNavigationStore | ❌ | ❌ |
| useIssuesScopeStore | ✅ | ✅ |
| useIssueDraftStore | ✅ | ✅ |
| useRecentIssuesStore | ✅ | ✅ |
| useIssueViewStore | ✅ | ✅ |
| myIssuesViewStore | ✅ | ✅ |
| useChatStore | ✅(手动用 wsKey| ✅ |
另外 `packages/core/platform/storage-cleanup.ts:10-19``WORKSPACE_SCOPED_KEYS` 列表里也漏了 `multica_navigation`
**现有的 workaround**`packages/views/layout/app-sidebar.tsx:285` 切 workspace 时硬跳到 `/issues`,正是为了绕开这个 bug。修好 navigation store 之后这行 hack 可以删掉。
### 复现
1. 在 workspace A 中打开一个具体 issue `/issues/abc123`
2. 切到 workspace B
3. 观察:如果没有 sidebar 的硬跳 workaround会尝试恢复到 `/issues/abc123`,但那个 issue 不属于 B导致 404 或错误
目前因为有硬跳 workaround症状表现为"切 workspace 后总是回到 issue 首页"—— 这本身也是 bug用户期望记住上次位置
### 修复方案(推荐 Option C组合
**三处改动**
1. `packages/core/navigation/store.ts:28` —— 把 `createPersistStorage(defaultStorage)` 改成 `createWorkspaceAwareStorage(defaultStorage)`
2. 同文件在末尾加:`registerForWorkspaceRehydration(() => useNavigationStore.persist.rehydrate());`
3. `packages/core/platform/storage-cleanup.ts:10-19``WORKSPACE_SCOPED_KEYS` 数组里加 `"multica_navigation"`
**可选**:清理 `packages/views/layout/app-sidebar.tsx:285``push("/issues")` workaround改完之后不再需要
### 改动范围
| 文件 | 改动 |
|---|---|
| `packages/core/navigation/store.ts` | 改 storage 类型、加 rehydration 注册(~3 行) |
| `packages/core/platform/storage-cleanup.ts` | 数组加一行 |
| `packages/core/platform/workspace-storage.test.ts` | 加 rehydration 的单测 |
| `packages/views/layout/app-sidebar.tsx`(可选) | 移除硬跳 workaround |
**风险**:极低。只是把 navigation store 对齐到其他 store 已经在用的模式。
---
## 任务 4 — [P2] Workspace 生命周期副作用散落
**关联 issue**MUL-727创建后闪页、MUL-728删除确认、MUL-820接受邀请不自动切
### 问题
创建 / 删除 / 切换 / 加入 workspace 的副作用分散在 mutation 的 `onSuccess` 和各处 UI 回调里,没有统一抽象。几个具体 bug
### 4.1 MUL-727 — 创建 workspace 后闪一下 `/issues` 再跳 `/onboarding`
**根因**:两个 `onSuccess` 回调同时跑,顺序不确定。
- `packages/core/workspace/mutations.ts:7-21``useCreateWorkspace.onSuccess` 里调了 `switchWorkspace(newWs)` —— 同步改 Zustand`/issues` 路由开始用新 workspace 渲染
- `packages/views/modals/create-workspace.tsx:68-70` 的 UI `onSuccess` 里调了 `router.push("/onboarding")` —— 异步 schedule 导航
于是:`/issues` 先渲染(闪一下)→ 导航到 `/onboarding`
**修复**:把 `switchWorkspace` 从 mutation 里拿出来,让 UI 层主导。在 `create-workspace.tsx``onSuccess` 里先 `switchWorkspace``push`,保证同一个微任务里完成。
**文件**`packages/core/workspace/mutations.ts``packages/views/modals/create-workspace.tsx`、可能 `packages/views/onboarding/step-workspace.tsx`
### 4.2 MUL-728 — 删除 workspace 的"缺少确认"
**核查结果**`packages/views/settings/components/workspace-tab.tsx:102-119, 236-255` **已经有 AlertDialog 确认**了。
**真实问题**:删除成功后**没有导航**,用户停在 `/settings`,而当前 workspace 已经是删除后系统挑的另一个。
**修复**:在 `handleDeleteWorkspace``onConfirm` 成功分支里加 `push("/issues")`
**文件**`packages/views/settings/components/workspace-tab.tsx`(加一行)
### 4.3 MUL-820 — 接受邀请不自动切换 workspace
**核查结果**:有两条路径:
-`/invite/:id` 独立页(`packages/views/invite/invite-page.tsx:32-52`)是**正确的**accept → switchWorkspace → push("/issues")
-**Sidebar 下拉里的 "Join" 按钮**`packages/views/layout/app-sidebar.tsx:203-209, 321-324`**是错的**:只 invalidate cache不切也不跳
**修复(推荐 Option 2**Sidebar 的 "Join" 改成跳转到 `/invite/:id` 页面,不再就地接受。单一入口、单一行为。
```tsx
<DropdownMenuItem onClick={() => push(`/invite/${inv.id}`)}>
{inv.workspace_name}
</DropdownMenuItem>
```
**文件**`packages/views/layout/app-sidebar.tsx`~10 行)
### 复现
| Issue | 步骤 |
|---|---|
| MUL-727 | 创建新 workspace → 仔细看是否闪了一下 `/issues` 再跳 `/onboarding` |
| MUL-728 | 删除当前 workspace → 观察删完后是否留在 `/settings` 页面BUG: 没有自动跳走) |
| MUL-820 | 被邀请用户登录 → sidebar 下拉 → 点 "Join" → 观察当前 workspace 是否切过去BUG: 不切)|
### 长期架构建议(可选)
抽一个 `useWorkspaceLifecycle` hook 统一管这些副作用。Agent 报告里有完整设计,文件:`packages/core/workspace/hooks.ts`(新建)。但建议先修 MUL-727/728/820 三个具体 bughook 抽象作为后续迭代。
### 改动范围
| Issue | 文件 | 改动规模 |
|---|---|---|
| MUL-727 | mutations.ts + create-workspace.tsx | ~10 行 |
| MUL-728 | workspace-tab.tsx | ~1 行 |
| MUL-820 | app-sidebar.tsx | ~10 行 |
---
## 总览
| 任务 | Issue | 优先级 | 预估规模 | 风险 |
|---|---|---|---|---|
| 1. WS 半开 + 陈旧 cache | #951 | **P0** | Option B ~10 行Option C ~1-2 天 | 低 |
| 2. Workspace URL 化 | MUL-723/43/509 | P1 | 5-7 人天(大部分是搬迁)| 中影响面大、e2e 要改)|
| 3. Navigation store 隔离 | MUL-43/476 | P1 | ~0.5 天 | 低 |
| 4. Workspace 生命周期 bug | MUL-727/728/820 | P2 | ~1 天 | 低 |
### 建议推进顺序
1. **立刻做**:任务 1 的 Option Bvisibilitychange 触发 invalidate—— 代码最少、收益最明显,能当天止血
2. **同步开始**:任务 3navigation store 隔离)—— 影响小、风险低、顺便清掉一个 workaround
3. **规划立项**:任务 2URL 化)—— 大改造,需要单独开一个 iteration
4. **次要修补**:任务 4 的三个小 bug —— 可以拆成独立 PR各自 review
### 重要澄清
- **Issue 编号核对**MUL-43 / MUL-476 的编号需要核对一次agent 查询 GitHub 返回的标题看起来对不上(可能是内部 issue tracker 编号 vs GitHub 编号混用)。以症状为准。
- **MUL-728 实际状态**:确认对话框已经存在,真实缺的是"删除后跳走"。
- **MUL-820 实际状态**`/invite/:id` 页面路径工作正常,只是 sidebar 下拉按钮坏了。
### 所有关键代码位置索引
```
packages/core/query-client.ts:7-10 # staleTime: Infinity
packages/core/api/ws-client.ts:1-142 # 客户端 WS无心跳
packages/core/realtime/use-realtime-sync.ts:462-487 # onReconnect 全量 invalidate
packages/core/platform/core-provider.tsx # 加 visibilitychange 的位置
packages/core/navigation/store.ts:15-31 # lastPath 未隔离
packages/core/platform/storage-cleanup.ts:10-19 # WORKSPACE_SCOPED_KEYS
packages/core/workspace/store.ts:43-77 # hydrateWorkspace / switchWorkspace
packages/core/workspace/mutations.ts:7-57 # create/leave/delete 三个 mutation
packages/views/layout/app-sidebar.tsx:203-324 # 侧边栏切 workspace、接受邀请入口
packages/views/modals/create-workspace.tsx:63-82 # 创建 workspace 入口
packages/views/settings/components/workspace-tab.tsx:102-119 # 删除 workspace 入口
packages/views/invite/invite-page.tsx:32-52 # 接受邀请正确实现参考
server/internal/realtime/hub.go:83-96 # 服务端 WS 心跳
server/internal/middleware/workspace.go:41-46 # wsId resolution
server/migrations/001_init.up.sql:15-23 # workspace.slug 已存在
```

View File

@@ -1,802 +0,0 @@
# Agent / Runtime 状态系统重设计
> **文档定位**:这是一份完整的设计 + 实施方案。任何一个新加入的工程师 / 设计师 / 产品,看完这份文档应该能:
> - 理解我们要解决的问题、为什么这么解决
> - 知道每个阶段做什么、按什么顺序做、产出什么
> - 在不读代码的前提下能独立讨论方案
>
> 本文档是 [agent-status-design-brief.md](./agent-status-design-brief.md) 和 [agent-status-redesign-plan.md](./agent-status-redesign-plan.md) 的合并升级版,达成共识后会取代它们。
---
## 目录
1. [背景与目标](#一背景与目标)
2. [核心思想](#二核心思想)
3. [状态系统规范](#三状态系统规范)
4. [数据架构](#四数据架构)
5. [跨平台策略](#五跨平台策略)
6. [设计语言](#六设计语言)
7. [实施分阶段](#七实施分阶段)
8. [验收标准](#八验收标准)
9. [边界与不做的事](#九边界与不做的事)
10. [风险与注意事项](#十风险与注意事项)
11. [参考](#十一参考)
---
## 一、背景与目标
### 1.1 Multica 的产品定位提醒
Multica 是 AI-native 的任务管理平台——agent 是和人对等的"同事"。一个工作区里同时有人和 agent 在协作,相互分配任务、评论、订阅。
理解这个定位很重要,因为它直接决定了状态系统的需求:**用户对 agent 的预期跟"对同事的预期"是相同的**——我希望随时知道这个同事现在能不能接活、在不在线、是不是出问题了。
### 1.2 当前的核心问题
**所有界面都在直接展示后端的原始字段,缺少"用户视角"的状态翻译层。**
具体表现(按用户感知严重度排序):
1. **Agent 列表的 "Idle" 绿点会骗人**——daemon 已经死了agent 仍然显示 Idle。用户分配任务后没有任何反馈长时间困惑"为什么 agent 不动"。
2. **Runtime 列表只有一个圆点**——"刚断线 5 分钟"和"3 个月前断线"视觉一模一样,用户判断不出严重程度。
3. **Issue 详情页多个 agent 同时工作时只有 1 个可见**——其他 agent 的卡片埋在下方滚动区,没有总览。
4. **任务失败时只有红色 X**——`agent_error`agent 自己挂了)和 `runtime_offline`daemon 离线)处理方式完全不同,但视觉上不区分。
5. **Chat 发消息后只有一个 spinner 转数分钟**——无法区分"排队"、"思考"、"调用工具"、"生成回答"四个阶段。
### 1.3 根本原因
后端字段是**任务调度的内部状态**`agent.status` = idle/working/blocked/error不是给用户看的。当前前端把后端字段直接渲染给用户就把"调度内部视角"暴露成了"用户视角"。
但用户不关心 agent 的内部调度状态,用户关心的是:
- **能不能用?**(在线 / 离线)
- **如果在用,在干什么?**(工作中 / 排队中 / 失败了)
- **如果不能用,为什么?**daemon 离线 / CLI 没装 / 任务超时)
这些问题的答案,**没有任何一个能从单一字段直接得到**——它们都需要把多个数据源聚合后才能算出来。
### 1.4 目标
**做一套"用户视角"的状态系统**,覆盖三类对象:
- **Agent**5 态available / working / pending / failed / offline跨界面一致
- **Runtime**4 态online / recently_lost / offline / about_to_gc
- **Task**阶段化queued / dispatched / thinking / using_tool / generating / completed / failed
完成这套系统后,下列现有界面会自然变好:
- Agents 列表 / 详情:看到真实可用性
- Runtimes 列表 / 详情:看到机器健康度
- Issue Detail多 agent 全景 + 失败原因显示
- 跨界面 hover cardissue assignee / autopilot / chat / @mention):状态一致
- Chat分阶段进度
---
## 二、核心思想
### 2.1 一句话
**把"用户视角的状态"做成前端的派生量**——后端只暴露真相(任务存在、心跳到达),前端按 UI 需要把这些真相聚合成"用户能理解的状态"。
### 2.2 三个设计原则
#### 原则 1派生函数住在前端不污染后端
`agent.status` / `runtime.status` 这些后端字段是 **物理事实**
- "task X 现在 running"
- "daemon Y 45 秒前发了心跳"
而 "Available / Working / Pending / Failed / Offline" 这些是 **UI 翻译**
- "FAILED 状态保持 2 分钟" 是设计决策
- "蓝色表示 working" 是视觉决策
- 不同界面可能要不同视角issue 里看"任务阶段"、列表里看"是否可分配"
**UI 翻译应该住在前端,跟着设计需求一起迭代。** 把它放进后端,每改一次都要 migration + WS payload 兼容 + 老客户端处理,迭代周期从分钟变周。
#### 原则 2服务器状态住在 TanStack Query cache不复制进 Zustand
我们的全局状态有两套:
- **Zustand** 管 client stateUI 选中、筛选器、modal 开关)
- **TanStack Query cache** 管 server state来自 API 的所有数据)
TQ cache 不是组件级缓存,**它本身就是 server state 的全局状态管理**。跨组件共享、按 key 索引、自动去重。
派生状态是 server data 的纯函数。结果不存——每次组件渲染时按需用 `useMemo` 算一遍。算的成本是几个 filter + some 调用,可忽略。
#### 原则 3聚合在前端做但要避免 N+1
派生状态需要 3 份原始数据:
- agents 列表
- runtimes 列表
- 当前活跃任务列表active tasks
朴素做法:每个组件 `useAgentTasks(agentId)`——一个 issues 列表 30 个 agent 头像 = 30 次请求。这就是 N+1。
正解:**进工作区时一次性拉"全工作区的活跃任务"**(数据量天然不大,活跃任务永远是少数),存进 TQ cache。所有组件共享这一份缓存按 agentId 在内存里 filter——零额外请求。
这是把"per-agent 的数据需求"转换成"全工作区的集合数据需求"。集合数据天然只需要 1 次请求。
---
## 三、状态系统规范
### 3.1 Agent 五态
| 状态 | 颜色 | 用户语义 | 出现条件 |
|---|---|---|---|
| **Available** | 🟢 绿 | 在线空闲,可以接活 | runtime 在线 + 没有活跃任务 |
| **Working** | 🔵 蓝 | 正在干活 | runtime 在线 + 至少一个任务在执行 |
| **Pending** | 🟡 黄 | 任务排着但没在跑 | runtime 在线 + 0 个执行中 + ≥1 个排队 |
| **Failed** | 🔴 红 | 最近一次失败 | 最近 2 分钟内有任务失败 |
| **Offline** | ⚫ 灰 | Daemon 离线,不可用 | runtime 离线(包含 CLI 未安装) |
**复合维度**:当 agent 是 Working 但同时有任务排队,主状态保持 Working旁边带 `+N` 角标("还有 N 个排队")。
**派生规则**(按优先级匹配,命中即返回):
```ts
type AgentPresence = "available" | "working" | "pending" | "failed" | "offline"
function deriveAgentPresence(input: {
agent: Agent
runtime: AgentRuntime
recentTasks: AgentTask[] // 该 agent 最近 N 个任务
now: number // 当前时间戳
}): AgentPresence {
// 1. Runtime 离线(含 CLI 未安装)→ offline
if (input.runtime.status === "offline") return "offline"
// 2. 最近窗口内有 failed task → failed
const recentFailed = input.recentTasks.find(
t => t.status === "failed" &&
(input.now - new Date(t.completed_at).getTime()) < FAILED_WINDOW_MS
)
if (recentFailed) return "failed"
// 3. 有 running task → working
if (input.recentTasks.some(t => t.status === "running")) return "working"
// 4. 有 queued/dispatched task → pending
if (input.recentTasks.some(t => t.status === "queued" || t.status === "dispatched")) {
return "pending"
}
// 5. Otherwise → available
return "available"
}
```
**复合维度的派生**
```ts
function deriveAgentPresenceDetail(...): {
presence: AgentPresence
runningCount: number
queuedCount: number
failureReason?: TaskFailureReason // 仅 presence === "failed" 时有值
}
```
**待确认常量**
- `FAILED_WINDOW_MS = 2 * 60 * 1000`2 分钟)。
- 短到避免污染(任务失败的红点不会一直黏着)
- 长到让用户能看见(不会还没看就消失)
- **未来可能扩展**2 分钟内强提示(红色动效),之后降级为 tooltip 的"最近失败"摘要——这样既不刺眼,又不丢信息。本期先用固定 2 分钟。
### 3.2 Runtime 四态
| 状态 | 触发条件 | 用户语义 |
|---|---|---|
| **Online** | 最近 45 秒内有心跳 | 健康,能接任务 |
| **Recently Lost** | 离线但 < 5 分钟 | 可能短暂网络抖动 |
| **Offline** | 离线 5 分钟 ~ 7 天 | 长期离线,需要排查 |
| **About to GC** | 离线接近 7 天阈值 | 系统将自动清理 |
```ts
function deriveRuntimeHealth(runtime: AgentRuntime, now: number): RuntimeHealth {
if (runtime.status === "online") return "online"
const lastSeen = runtime.last_seen_at
? new Date(runtime.last_seen_at).getTime()
: 0
const offlineFor = now - lastSeen
if (offlineFor < 5 * 60 * 1000) return "recently_lost"
if (offlineFor > 6 * 24 * 3600 * 1000) return "about_to_gc" // 7d - 1d
return "offline"
}
```
> **关于 CLI 未安装等"runtime 在线但跑不了"的场景**:归并到 `offline`tooltip 写明具体原因("CLI 未安装"、"Daemon 启动中")。不为此引入第六态——状态空间膨胀代价高,分辨率应该用 tooltip / detail 而不是顶层枚举。
### 3.3 Task 阶段化
用于 Issue Detail 和 Chat 显示当前任务在哪一步:
```ts
type TaskStage =
| "queued"
| "dispatched" // dispatched 但进程没起来
| "thinking" // running最后一条 message 是 thinking
| "using_tool" // running最后一条是 tool_use / tool_result
| "generating" // running最后一条是 text
| "completed"
| "failed"
| "cancelled"
```
派生函数读 task 状态 + 最后一条 message 的 type 即可决定阶段。
### 3.4 失败原因映射
`task.failure_reason` 来自后端 migration 0555 个值映射到中文标签:
```ts
const FAILURE_REASON_LABEL: Record<TaskFailureReason, string> = {
agent_error: "Agent 执行报错",
timeout: "执行超时",
runtime_offline: "Daemon 离线",
runtime_recovery: "Daemon 重启回收",
manual: "用户取消",
}
```
每种原因要给用户对应的处理建议(具体文案待设计师定)。
> ⚠️ **当前差异**:后端 schema 已有 `failure_reason`migration 055但前端 `packages/core/types/agent.ts` 的 `AgentTask` 接口**未暴露此字段**。阶段 0 必须同步:
> - 前端类型补 `failure_reason: TaskFailureReason | null`
> - 检查后端 `ListAgentTasks` / 新增的 `ListActiveTasksByWorkspace` 是否 SELECT 了这个字段
---
## 四、数据架构
### 4.1 数据流
```
后端真相
┌────────────┬─────────────┬──────────────┐
│ agents │ runtimes │ active_tasks│
│ (HTTP) │ (HTTP) │ (HTTP) │
└─────┬──────┴──────┬──────┴───────┬──────┘
│ │ │
└─────────────┴──────────────┘
TanStack Query cache
(全局共享)
派生函数(纯函数)
┌───────────────┴───────────────┐
▼ ▼
AgentPresence RuntimeHealth
│ │
▼ ▼
组件渲染5 态视觉) 组件渲染4 态视觉)
──────────────────────────────────────────────
实时更新
后端 WS 事件 ──→ 前端 invalidate query ──→ 重拉 ──→ 派生重算 ──→ UI 更新
桌面端额外:本机 IPC ──→ setQueryData 直接预填 ──→ 亚秒级响应
```
### 4.2 三个 query
```ts
// 进工作区时一次性拉
useQuery(['ws', wsId, 'agents']) // listAgents
useQuery(['ws', wsId, 'runtimes']) // listRuntimes
useQuery(['ws', wsId, 'active-tasks']) // ★ 新增getActiveTasksForWorkspace
```
**`active-tasks` 是新增 query**——返回当前工作区所有 status ∈ {queued, dispatched, running} 的任务。这份数据天然小(活跃任务不会很多),值得做成"全工作区一次拉"。
> ⚠️ **当前差异**:后端**没有此 endpoint**。现有只有 `listAgentTasks(agentId)`per-agent和 `getActiveTasksForIssue(issueId)`per-issue。阶段 0 必须新增 `GET /api/workspaces/:slug/active-tasks`——它仍然返回原始 task 列表,不做任何派生,**不违反"零后端聚合"原则**。
#### Runtime → Agents 是 reverse query
后端没有 "runtime 服务的 agent 列表" API。Agent 持有 `runtime_id` 外键,但 Runtime 没有反向关联。
**前端处理**:从已缓存的 `agents` 列表 `filter(a => a.runtime_id === rtId)` 即可。这不算 N+1agents 列表本来就要拉),是免费的 join。
### 4.3 WS 事件接线表
| WS 事件 | 触发的 invalidate |
|---|---|
| `agent:created` / `agent:archived` / `agent:updated` | `['ws', wsId, 'agents']` |
| `agent:status` | `['ws', wsId, 'agents']`(即使我们不用这个值,缓存 fresh 仍要) |
| `daemon:register` | `['ws', wsId, 'runtimes']` |
| `task:dispatch` / `task:completed` / `task:failed` / `task:cancelled` | `['ws', wsId, 'active-tasks']` |
| `task:progress` | (考虑节流,避免高频任务刷新缓存) |
| `daemon:heartbeat` | **故意忽略** —— 后端发送但前端不订阅,避免每 15 秒过度 refetch。后果runtime online → recently_lost 切换最坏延迟 75 秒45s sweeper + 30s 间隔)。设计上接受这个延迟。 |
**关键不变量**:每个派生函数依赖的字段都必须有对应 WS 事件覆盖。任何字段没事件覆盖,状态会卡住。每次新增派生维度都要回头检查这张表。
### 4.4 桌面端 IPC 桥接
桌面端通过 Electron IPC 直接读本机 daemon。这份数据
- 比 server WS **快 75 秒**IPC 亚秒server sweep 最坏 75s
- 包含 server 看不到的 `starting` / `stopping` / `cli_not_found` 等中间态
**不修改派生函数签名**——桌面端把 IPC 数据用 `setQueryData` 直接写进 runtime cache
```ts
// 仅桌面端apps/desktop/src/renderer/src/platform/daemon-ipc-bridge.ts
window.electron.onDaemonStatus((status) => {
queryClient.setQueryData(
['ws', wsId, 'runtimes'],
(old) => old?.map(rt =>
rt.id === status.runtimeId ? mergeDaemonStatus(rt, status) : rt
)
)
})
```
派生函数完全不知道数据从哪来——它只读 cache。**桌面端自动获得亚秒级体验,零派生函数改动**。
---
## 五、跨平台策略
### 5.1 状态系统是平台无关的
派生函数、5 态/4 态规范、UI 视觉——**两端共享同一套**。设计稿只画一份。
### 5.2 数据源平台相关
| 平台 | Runtime 数据来源 | 状态变化感知延迟 |
|---|---|---|
| Web | server WS / HTTP | 最坏 75 秒 |
| Desktop本机 daemon | IPC + server | < 1 秒 |
| Desktop别人的 daemon | server WS / HTTP | 跟 Web 一样 |
数据源不同不影响 UI——派生函数读 cachecache 是"哪边更新就更新"。
### 5.3 操作能力平台相关
| 操作 | Web | Desktop自己机器 | Desktop别人机器 |
|---|---|---|---|
| 看状态 | ✅ | ✅ | ✅ |
| 重启 daemon | ❌ | ✅ | ❌ |
| 看 daemon logs | ❌ | ✅ | ❌ |
| 看 CLI 安装详情 | ❌ | ✅ | ❌ |
实现方式:组件按 `isLocalDaemon && isOwner` 条件渲染按钮。**不需要写进派生函数 / 状态系统**,是 UI 局部决策。
### 5.4 Daemon 卡片视觉对齐
桌面端 settings 里的"本机 daemon 卡片"必须跟云端 runtime 列表项视觉一致。同一台机器同一个概念,不能两套设计。
---
## 六、设计语言
### 6.1 直接复用 Skills 界面PR #1607、#1614、#1618、#1610
Skills 界面已经在 2026-04 完成重设计是当前产品的视觉锚点。Agents / Runtimes 直接照搬其规则:
1. **统一页头** `PageHeader`h-12 + mobile sidebar trigger
2. **响应式网格列表**`grid-cols-[minmax(0,1.6fr)_minmax(0,0.8fr)_minmax(0,1.2fr)_minmax(0,6rem)_auto]`
3. **每行三层信息**:主标题 → 描述line-clamp-1 muted→ 元数据xs muted
4. **关联对象用头像堆栈**:最多 3 + `+N`size=22 + `ring-2 ring-background` + `-space-x-1.5`
5. **卡片化列表 + 卡片内工具栏**:搜索和 scope tab 在 `CardToolbar`h-12不在页面级
6. **创建用多步 dialog**chooser → 表单可回退宽度按方法切换300ms 过渡
7. **空状态分文案**:图标 + 标题 + 三行说明 + 清晰 CTA
8. **长列表 `useScrollFade`**:上下边缘淡出
9. **头像统一 `ActorAvatar`**:传 `size`,自动支持 agent / 人
10. **权限检查 hook 化**`useCanEdit...`UI 提前隐藏/禁用
### 6.2 状态视觉的三个层级
每种派生状态在三个层级要保持一致:
- **Dot**(圆点):列表项、头像旁,最紧凑
- **Badge**(徽章):详情页头部、卡片角落,带图标 + 文字
- **Tooltip / Hover Card**:鼠标悬停展开完整信息
**跨界面一致性**:同一个 agent 无论出现在哪agents 列表 / issue assignee picker / autopilot 编辑 / chat 选择面板 / 评论 @),状态视觉必须完全一致。
---
## 七、实施分阶段
### 阶段 0 — 数据层地基(无 UI 改动)
**目标**:派生函数 + cache + WS + IPC 桥接全部就位。UI 暂不动。完成后所有 UI 阶段都能放心假设"派生状态可用、零额外请求"。
#### 后端工作Go
| 文件 | 改动 |
|---|---|
| `server/pkg/db/queries/agent_task.sql` | 新增 sqlc query`ListActiveTasksByWorkspace`SELECT 所有字段含 `failure_reason`,过滤 status ∈ {queued, dispatched, running} |
| `server/pkg/db/agent_task.sql.go` | `make sqlc` 自动生成 |
| `server/internal/handler/agent.go`(或新建 task handler | `ListActiveTasksByWorkspace` handler权限校验用户必须是 workspace member |
| `server/cmd/server/router.go` | 注册路由 `GET /api/workspaces/{slug}/active-tasks` |
| **核查**:现有 `ListAgentTasks` query | 确认 SELECT `failure_reason` 字段;如未 SELECT补上 |
#### 前端类型补全
| 文件 | 改动 |
|---|---|
| `packages/core/types/agent.ts` | `AgentTask` 接口加 `failure_reason: TaskFailureReason \| null`;新增 `TaskFailureReason = "agent_error" \| "timeout" \| "runtime_offline" \| "runtime_recovery" \| "manual"` |
#### 前端 API client
| 文件 | 改动 |
|---|---|
| `packages/core/api/client.ts` | 新增方法 `getActiveTasksForWorkspace(wsSlug): Promise<AgentTask[]>` |
#### 前端派生函数 + 类型
| 文件 | 内容 |
|---|---|
| `packages/core/agents/types.ts`(如不存在则新建) | `AgentPresence` / `AgentPresenceDetail` / `RuntimeHealth` 等类型 |
| `packages/core/agents/derive-presence.ts` | `deriveAgentPresence` / `deriveAgentPresenceDetail` 纯函数 |
| `packages/core/agents/derive-presence.test.ts` | 5 态全分支 + 边界 caseruntime null / tasks 空 / 时钟边界) |
| `packages/core/runtimes/derive-health.ts` | `deriveRuntimeHealth` |
| `packages/core/runtimes/derive-health.test.ts` | 4 态全分支 |
#### 前端 query + hook
| 文件 | 内容 |
|---|---|
| `packages/core/agents/active-tasks-query.ts` | `activeTasksOptions(wsId)` query options |
| `packages/core/agents/use-agent-presence.ts` | `useAgentPresence(agentId)` hook读 3 份 cache → 派生 |
| `packages/core/runtimes/use-runtime-health.ts` | `useRuntimeHealth(runtimeId)` hook |
| `packages/core/runtimes/use-runtime-agents.ts` | `useRuntimeAgents(runtimeId)` hook从 agents cache filter 出绑定的 agents |
#### 前端 WS 接线
| 文件 | 改动 |
|---|---|
| `packages/core/realtime/agent-runtime-sync.ts` | 新增专用 sync。订阅 `agent:*` / `task:dispatch` / `task:completed` / `task:failed` / `task:cancelled` / `daemon:register` → invalidate 对应 query。**显式不订阅 `daemon:heartbeat`**(接受 75 秒延迟) |
| `packages/core/realtime/use-realtime-sync.ts`(如已有全局 hook | 集成新 sync |
#### 桌面端 IPC 桥接
| 文件 | 内容 |
|---|---|
| `apps/desktop/src/renderer/src/platform/daemon-ipc-bridge.ts` | 监听 `window.daemonAPI.onStatus(...)`,用 `queryClient.setQueryData` 把本机 daemon status merge 进对应 runtime cache |
#### 完成标准
- [ ] 后端 `GET /api/workspaces/:slug/active-tasks` 通 curl 测试,返回 active tasks 列表
- [ ] `deriveAgentPresence` / `deriveRuntimeHealth` 单测全部通过
- [ ] 控制台调用 `useActiveTasks(wsId)` 能拿到全工作区活跃任务
- [ ] 控制台调用 `useAgentPresence(agentId)` 能拿到正确的 5 态状态
- [ ] WS 接线表里所有事件都能正确 invalidate手测覆盖
- [ ] 关本机 daemon 后桌面端 runtime cache **1 秒内**变 offline
- [ ] 不动任何 UI 文件——这阶段 zero UI delta
### 阶段 1 — Agents + Runtimes 列表页
**目标**:两个列表用上派生状态,互相能看到对方。
#### 设计师产出(先于代码)
- 5 态 dot / badge / tooltip 三层视觉规范
- Working + 排队角标的复合视觉
- Failed 状态的 2 分钟时间窗口动效
- Runtime 4 态的视觉差异(不能再是同一个浅灰圆点)
#### 改造文件
| 文件 | 改动 |
|---|---|
| `packages/views/agents/components/agent-list-item.tsx` | 替换 `statusConfig[agent.status]``useAgentPresence(agentId)` |
| `packages/views/agents/components/agents-page.tsx` | 接 WS 订阅 |
| `packages/views/agents/config.ts` | 删除 `statusConfig`,新建 `presenceConfig` |
| `packages/views/runtimes/components/runtime-list.tsx` | 用派生 4 态;展示 last_seen、关联 agent 数、当前任务数 |
| `packages/views/runtimes/components/runtimes-page.tsx` | 接 WS 订阅 |
| `apps/desktop/.../local-daemon-card.tsx` | 视觉对齐云端 runtime 卡片 |
### 阶段 2 — Agents + Runtimes 详情页
**目标**详情页头部、profile card 状态联动Runtime token usage 信息架构整理。
#### 改造文件
| 文件 | 改动 |
|---|---|
| `packages/views/agents/components/agent-detail.tsx` | 头部 status badge 用派生 |
| `packages/views/agents/components/agent-profile-card.tsx` | 状态行和 runtime 行联动;展示当前任务数 + 最近失败原因 |
| `packages/views/runtimes/components/runtime-detail.tsx` | Token usage 主次重排核心指标置顶5 个图表折叠 / 下沉 |
| `packages/views/runtimes/components/usage-section.tsx` | API 调用按时间窗口拉(不再总是 90 天) |
### 阶段 3 — Issue Detail 任务展示
**目标**:多 agent 全景视图;任务阶段化;失败原因显式。
#### 新增文件
| 文件 | 内容 |
|---|---|
| `packages/core/agents/derive-task-stage.ts` | `deriveTaskStage` |
| `packages/core/agents/derive-task-stage.test.ts` | 单测 |
| `packages/views/issues/components/agent-task-row.tsx` | 单 agent 单任务一行 |
#### 改造文件
| 文件 | 改动 |
|---|---|
| `packages/views/issues/components/agent-live-card.tsx` | 从"sticky 一个 + 折叠列表"改为"每个 agent 一行" |
| `packages/views/issues/components/agent-transcript-dialog.tsx` | 失败时展示 failure_reason |
### 阶段 4 — 跨界面 Hover Card
**目标**:所有 agent 头像出现的位置都用统一的 hover card。
#### Hover Card 必须显示的内容(按重要度排序)
1. 派生 5 态状态
2. Runtime 健康(在线性 + last_seen 相对时间)
3. 当前任务N running / M queued
4. 最近失败(如果有):原因 + 时间
5. Agent 名称 + description
6. 关联 skills前 3 个 + `+N`
7. Owner
#### 必须接入的位置
| 位置 | 当前状态 |
|---|---|
| Agents 列表 / 详情 | ✅ 已有 |
| Issue Assignee Picker | ❌ 仅头像无状态 |
| Issue Detail 头部 assignee | ❌ 仅头像无状态 |
| Issue 列表 / 看板的分配头像 | ❌ 仅头像 |
| Autopilot 列表 / 编辑assignee | ❌ 仅头像 |
| Project lead picker | ❌ 仅头像 |
| Chat 选择 agent 面板 | ❌ 待确认 |
| 评论里的 @agent | ❌ 仅头像 |
#### 实施
`ActorAvatar` 组件挂载 hover card——一处改动上面所有位置自动获得统一卡片。
**N+1 风险已经被阶段 0 的"全工作区 active-tasks"消除**——hover card 只读 cache零额外请求。
### 阶段 5 — Chat 状态分阶段(独立 PR
工作量较大,跟流式渲染相关,单独排期。
#### 改造文件
| 文件 | 改动 |
|---|---|
| `packages/views/chat/components/chat-message-list.tsx` | `AssistantMessage``deriveTaskStage` 替代单 spinner |
| `packages/views/chat/components/chat-page.tsx` | WS 断线重连后的消息回拉 fallback |
#### 必须解决
- 取代单 spinner按阶段显示
- Failed task 显示原因
- WS 断线重连后能拉回历史消息
#### 加分项
- Typing indicatorgenerating 阶段的逐字感)
- 全局任务进度 FAB
- Stop 按钮的明确反馈
---
## 八、验收标准
### 阶段 0
- [ ] `deriveAgentPresence` / `deriveRuntimeHealth` 单测覆盖所有分支 + 边界 caseruntime null / tasks 空 / 时钟边界)
- [ ] 控制台调用 `useActiveTasks(wsId)` 能拿到数据
- [ ] WS 事件接线表里每个事件都能正确 invalidate手测
- [ ] 桌面端关本机 daemon 后 runtime cache 在 1s 内变 offline
### 阶段 1
- [ ] Daemon 关闭后Agent 列表项 75 秒内变成 Offline 灰点
- [ ] Agent 跑任务时,列表项变成 Working 蓝点;排队 N 个时带 `+N` 角标
- [ ] Agent 任务失败后,列表项 2 分钟内显示 Failed 红点 + tooltip 含失败原因2 分钟后自动恢复
- [ ] Runtime 列表能区分 Online / Recently Lost / Offline / About to GC 四态
- [ ] Runtime 列表行展示关联 agent 数 + 当前任务数
- [ ] 桌面端本机 daemon 卡片视觉跟云端 runtime 列表项一致
- [ ] 全局 grep `agent.status` 在 views 层无引用
### 阶段 2
- [ ] Agent 详情头部状态跟列表一致
- [ ] Profile card 状态行 + runtime 行不再自相矛盾
- [ ] Runtime 详情页能在不展开图表的前提下看到本期成本
- [ ] Token usage API 按选中的时间窗口拉(不再总是 90 天)
### 阶段 3
- [ ] 同一 issue 多 agent 工作时,每个 agent 一行实时状态
- [ ] queued / dispatched / running 三态视觉差异清晰
- [ ] 任务失败时,行内显示中文 failure reason + 处理建议
- [ ] 不支持 live log 的 provider 显式说明"等任务完成后查看结果"
### 阶段 4
- [ ] 所有展示 agent 头像的位置 hover 都能看到完整状态卡
- [ ] 渲染 30+ agent 头像的页面hover 不触发任何新 HTTP 请求
### 阶段 5
- [ ] Chat 中能看到任务在哪个阶段
- [ ] WS 断线后重连能补齐历史消息
- [ ] Failed task 显示原因
---
## 九、边界与不做的事
明确**不在本次范围内**
1. **不改后端 schema**——`agent.status` / `blocked` / `error` 字段保留(迁移风险大,无收益)
2. **不让后端做派生**——所有派生在前端做(迭代速度 + UI 解耦)
3. **不新增 WS 事件类型**——只订阅现有但未用的(如 `agent:status`
4. **不动 `agent_runtime.status` 的 online/offline 二态**——前端从这两态 + `last_seen_at` 派生四态
5. **不重构 Skills 界面**——它已经是参考样板
6. **Phase 5 不做"逐字流式渲染"**——后端 stream-json 是批量推送typing indicator 是视觉技巧
7. **不做"agent 健康综合评分"**——只暴露原始信号,不算综合分
8. **不为 CLI 未安装等场景引入第六态**——归到 offline 的 tooltip 子分类
9. **不在 Zustand 里存派生结果**——server data 不能复制进 store
---
## 十、风险与注意事项
### 10.1 WS 事件覆盖完整性
派生函数的"实时性"靠的是依赖 query 都被 WS 正确 invalidate。一个事件没接好状态就会卡住。
**缓解**:阶段 0 的接线表是必须维护的"契约"。每次新增派生维度,回头检查这张表,确保新依赖的字段有事件覆盖。
### 10.2 时钟一致性
Failed 状态依赖客户端时间和 `completed_at` 的差。客户端时钟漂移会让 2 分钟窗口不准。
**缓解**2 分钟 ± 30 秒不影响判断,可接受。需要时可用 server 在 WS 心跳里携带的时间作为参考。
### 10.3 旧字段引用残留
`statusConfig` 删除后,遗漏的引用会运行时错误。
**缓解**
- TypeScript 严格模式 + 改类型
- 全局 grep 验证
- 长期防御:在 `packages/core/agents/types.ts``agent.status` 从外部消费的 Agent 类型里 omit 掉,仅保留在 RawAgent 内部类型里给 API 层用
### 10.4 跨界面状态一致性
不同地方调用同一个 agent 的派生函数,必须结果一致。
**缓解**:所有调用方走 `useAgentPresence(agentId)` 这个唯一 hook不允许直接调派生函数。Hook 集中管理输入数据收集。
### 10.5 active-tasks 数据量
如果工作区某天有 1000+ 活跃任务,全工作区一次拉的设计会受影响。
**缓解**
- 当前活跃任务有天然上限(受 `max_concurrent_tasks` × agent 数量约束)
- 监控加上cache 大小超过阈值时上报
- 必要时再考虑 windowing最近 N 个)或 server 端聚合
---
## 十一、参考
- [产品全景文档](./product-overview.md) —— Agent / Runtime / Daemon 的产品定位
- [Skills 界面源代码](../packages/views/skills/) —— 设计语言样板
- 关键 PR#1607Skills 重设计)、#1614Card + PageHeader#1618(描述恢复)、#1610Dialog 闪烁修复)
- 后端关键代码:
- `server/internal/service/task.go` —— `agent.status` 更新逻辑(`ReconcileAgentStatus`
- `server/cmd/server/runtime_sweeper.go` —— Runtime 心跳 / sweeper 时间常量
- `server/migrations/055_task_lease_and_retry.up.sql` —— Task `failure_reason` 五态
- `server/migrations/037_fix_pending_task_unique_index.up.sql` —— 一个 issue 多 agent 处理的设计依据
---
## 附录 A当前现状清单保留作为重设计前的存档
> 这部分原本在 design-brief.md 里,迁移过来作为重设计前的现状记录。设计师在画稿前可以贴上当前界面截图作为对照。
### A.1 Agent 字段可见性
| 字段 | 当前状态 | 备注 |
|---|---|---|
| `name` / `avatar_url` / `description` | ✅ | 列表 + 详情都展示 |
| `archived_at` | ✅ | 列表项灰显 + 详情头部 banner |
| `status`idle/working/... | ✅ 但语义错误 | **要替换成派生 5 态** |
| `runtime_mode`local/cloud | ✅ 图标 | 列表项右侧 Cloud / Monitor 图标 |
| `instructions` | ✅ | Instructions tab |
| `custom_env` / `custom_env_redacted` | ✅ | Env tab |
| `custom_args` | ✅ | Custom Args tab |
| `visibility`workspace / private | ✅ | Settings tab |
| `max_concurrent_tasks` | ✅ | Settings tab |
| `model` | ✅ | Settings tab |
| `runtime_id` 关联 | ✅ | Settings tab |
| `skills` | ✅ | Skills tab + profile card 前 3 个 |
| `owner_id` | ✅ | Profile card |
| `created_at` / `updated_at` | 🆕 | 后端有UI 完全没展示 |
| `archived_by` | 🆕 | 后端有UI 隐藏 |
### A.2 Runtime 字段可见性
| 字段 | 当前状态 | 备注 |
|---|---|---|
| `name` | ✅ | 列表 + 详情头部 |
| `provider` | ✅ Logo | 9 种 |
| `runtime_mode` | ✅ 文字 | `RuntimeModeIcon` 组件存在但从未被调用 |
| `status`online/offline | ✅ 圆点 + 徽章 | 离线圆点浅色主题下几乎不可见 |
| `last_seen_at` | ✅ 仅详情页 | **列表完全看不到** |
| `device_info` | ✅ 详情页 | 没有人类可读化 |
| `daemon_id` | ✅ mono 字体 | 不可复制 |
| `metadata.cli_version` | ✅ | CLI 更新部分 |
| `metadata.launched_by` | ✅ | "Managed by Desktop" |
| `owner_id` | ✅ | 头像 + 名字 |
| `created_at` / `updated_at` | ✅ | ISO 时间戳 |
### A.3 桌面端 IPC 数据
```
DaemonStatus (本地 IPC)
├─ state: running / stopped / starting / stopping / installing_cli / cli_not_found
├─ pid, uptime
├─ daemonId, deviceName, serverUrl
├─ agents: 当前运行的 agent IDs
├─ workspaceCount
└─ profile
```
### A.4 截图占位区
设计师拿到这份文档后,请把以下界面的当前截图贴在对应位置:
- **Agents 界面**:列表页 / 详情页(每 tab 一张)/ 创建对话框
- **Runtimes 界面**:列表页 / 详情页(含 5 个图表)/ 桌面端 daemon 卡片
- **Issue Detail**:无任务执行时 / 单 agent 执行中 / 多 agent 并发 / 全屏 transcript dialog
### A.5 Token Usage 5 个图表的数据细节
Runtime 详情页底部 token usage 部分5 个图表 + 1 张表格全部展开,没有主次。阶段 2 改造时按下表理解每个图表的数据契约:
| 图表 | 数据源 | 时间粒度 | 维度 | 度量 |
|---|---|---|---|---|
| **Activity Heatmap** | `getRuntimeUsage?days=90` | 日 | date | 4 级强度,按 token 总量百分位分级 |
| **Hourly Activity** | `getRuntimeTaskActivity` | 小时0-23 | hour | 任务数 |
| **Daily Token Chart** | 同 Heatmap客户端聚合 | 日 | date | input/output/cacheRead/cacheWrite 总和 |
| **Daily Cost Chart** | 同上 + 客户端定价计算 | 日 | date | 美元成本,按 model pricing 表 |
| **Model Distribution** | 同上聚合 by model | 全周期 | model | tokens 占比 + cost |
**当前实现的问题**API 总是取 90 天数据,客户端做 7d/30d 过滤——浪费服务端资源,首次加载慢。改造时按选中窗口拉。
### A.6 Issue Detail 任务展示的当前实现
主要组件:`packages/views/issues/components/agent-live-card.tsx`
#### 已有能力
- 多 agent 并发执行同一 issue 时,第一个卡片是 sticky顶部固定其他在下方滚动
- 每个 task 卡片可展开 timeline显示 tool_use / tool_result / thinking / text / error 五种消息类型
- 实时滚动WS 事件 `task:message` 到达时追加 timeline
- 工具调用计数 badge
- Stop 按钮可取消任务
- 全屏 transcript dialog支持事件类型筛选 + 复制)
- 已完成/失败/取消的任务进入 TaskRunHistory 折叠区
#### 数据来源
| 数据 | API |
|---|---|
| 当前 issue 的活跃 task 列表 | `getActiveTasksForIssue(issueId)` |
| 每个 task 的历史消息 | `listTaskMessages(taskId)` |
| 实时消息流 | WS `task:message` |
| 状态变化 | WS `task:dispatch` / `task:completed` / `task:failed` / `task:cancelled` |
| 取消任务 | `cancelTask(issueId, taskId)` |
阶段 3 在此基础上重构成"每个 agent 一行的全景视图"。

View File

@@ -1,530 +0,0 @@
# Agents & Runtimes 界面重设计 · 设计师 Brief
> **文档定位**:这份文档专门给负责重设计 Agents 和 Runtimes 界面的 UI/UX 设计师。
>
> 看完这份文档,你应该能:
> - 理解我们在解决什么问题、目标体感是什么
> - 知道每个界面有哪些数据可用、哪些是新展示的、哪些需要工程补
> - 知道有哪些现有交互不能动(避免破坏用户已建立的习惯)
> - 知道可以参考哪些已经做完的设计Skills 是样板)
> - 直接进入设计稿环节
>
> **必读伴侣文档**[Agent / Runtime 状态系统重设计](./agent-runtime-status-redesign.md) —— 完整的工程方案、状态规范、实施阶段。本 brief 是它的"设计师视角切片"。
---
## 目录
1. [一句话目标](#一一句话目标)
2. [必读:状态视觉规范](#二必读状态视觉规范)
3. [Agents 界面](#三agents-界面)
4. [Runtimes 界面](#四runtimes-界面)
5. [跨界面统一Agent Hover Card](#五跨界面统一agent-hover-card)
6. [跨平台差异处理](#六跨平台差异处理)
7. [设计语言参考Skills 界面](#七设计语言参考skills-界面)
8. [工程会同步交付的能力](#八工程会同步交付的能力)
9. [设计师产出清单](#九设计师产出清单)
10. [附录:截图占位区](#十附录截图占位区)
---
## 一、一句话目标
**当前所有界面都在直接展示后端字段,缺少"用户视角"的状态翻译层。**
用户看到 `Idle` / `Online` / spinner但这些词没有回答"agent 现在能不能用 / 在做什么 / 出问题了没"。我们要做的是一套**用户视角的状态系统**:让用户一眼就知道一个 agent / runtime 的健康状况,跨界面一致。
---
## 二、必读:状态视觉规范
这套规范是所有界面的共同基础。**先输出这套规范,再画具体界面**。
### 2.1 Agent 五态
| 状态 | 颜色 | 用户语义 | 出现条件 |
|---|---|---|---|
| **Available** | 🟢 绿 | 在线空闲,可以接活 | runtime 在线 + 没有活跃任务 |
| **Working** | 🔵 蓝(品牌色) | 正在干活 | runtime 在线 + 至少一个任务在执行 |
| **Pending** | 🟡 黄 | 任务排着但没在跑 | runtime 在线 + 0 个执行中 + ≥1 个排队 |
| **Failed** | 🔴 红 | 最近一次失败 | 最近 2 分钟内有任务失败 |
| **Offline** | ⚫ 灰 | Daemon 离线,不可用 | runtime 离线 |
**复合维度**:当 agent 是 Working 但同时有任务排队,主状态保持 Working旁边带 `+N` 角标("还有 N 个排队")。
**Failed 状态特别说明**:失败显示**保持 2 分钟**,之后自动恢复(避免红点黏太久)。设计上要表达"这是临时强提示"。
### 2.2 Runtime 四态
| 状态 | 触发条件 | 用户语义 |
|---|---|---|
| **Online** | 最近 45 秒内有心跳 | 健康 |
| **Recently Lost** | 离线但 < 5 分钟 | 可能短暂网络抖动 |
| **Offline** | 离线 5 分钟 ~ 7 天 | 长期离线,需排查 |
| **About to GC** | 离线接近 7 天阈值 | 系统将自动清理 |
> **关于 CLI 未安装等"runtime 在线但跑不了"的场景**:归并到 Offlinetooltip 写明具体原因("CLI 未安装"、"Daemon 启动中")。**不要为这些子情况新设状态色**,色彩枚举太多反而失去信号。
### 2.3 视觉表达分三层
每种状态在以下三个层级要保持一致:
- **Dot**(圆点):列表项、头像旁的小圆点,最紧凑场景
- **Badge**(徽章):详情页头部、卡片角落,带图标 + 文字
- **Tooltip / Hover Card**:鼠标悬停时展开完整信息
**跨界面一致性**:同一个 agent无论出现在哪agents 列表 / issue assignee picker / autopilot 编辑 / chat 选择面板 / 评论 @),状态视觉**必须完全一致**。这是这次设计能立住的关键。
---
## 三、Agents 界面
### 3.1 当前界面长什么样
主要文件:
- `packages/views/agents/components/agents-page.tsx` — 列表页容器
- `packages/views/agents/components/agent-list-item.tsx` — 列表项
- `packages/views/agents/components/agent-detail.tsx` — 详情页(含 tabsInstructions / Skills / Tasks / Environment / Custom Args / Settings
- `packages/views/agents/components/agent-profile-card.tsx` — 详情顶部的 profile 卡片
- `packages/views/agents/components/create-agent-dialog.tsx` — 创建对话框
**当前的视觉语言跟 Skills 重设计前一样——这次目标是迁到 Skills 风格。**
### 3.2 当前的核心痛点
按用户感知严重度排列:
1. **列表上的 "Idle" 绿点会骗人**——daemon 已经死了agent 仍然显示 Idle。用户分配任务后没有任何反馈。**根因**`agent-list-item.tsx` 完全忽略 runtime 在线性,只读 `agent.status` 这个后端字段。
2. **`agent-profile-card` 状态行和 runtime 行不联动**——状态显 Idleruntime 显 Offline自相矛盾。
3. **跨界面状态展示不一致**——issue assignee picker、autopilot picker、chat 选择 agent 时**完全不显示状态**,用户做选择时不知道哪个能用。
4. **创建 agent 时无法预知能否立即跑起来**——dialog 显示了 runtime 在线点,但不知道 model 是否合法、初始化会不会失败。
5. **Archived agent 和 active agent 混在同一列表**——切换视图全靠手动按钮,不够清晰。
6. **零 WS 订阅**——离线/上线必须手动刷新页面才能感知。
### 3.3 重设计目标
#### 必须达成
- 列表项一眼能看出 agent 是否真的可用(融合 runtime 在线性的派生 5 态)
- Hover card 内必须看到派生状态、当前任务数、runtime 健康、最近失败原因
- 跨界面一致:所有显示 agent 头像的位置都用同一个 hover card详见第 5 节)
- 创建/编辑 agent 时,能预看"如果保存agent 会变成什么状态"
- 实时更新——状态变化 75 秒内反映到 UI
#### 加分项
- Archived agent 独立 tab/折叠区,不再和 active 混在一起
- 列表支持按"最近活动时间"排序
- 支持按状态筛选("显示所有 Failed"、"显示所有 Pending"
### 3.4 可用数据清单(设计稿可以放心假设可用)
> **图例**:✅ = 当前已展示;🆕 = 已可用但当前 UI 没用;🔧 = 工程会在阶段 0 补上;📡 = 实时事件可用
#### Agent 主体字段(来自 `Agent` type
| 字段 | 类型 | 当前状态 | 说明 |
|---|---|---|---|
| `name` / `avatar_url` / `description` | string | ✅ | 已展示 |
| `archived_at` / `archived_by` | string | ✅ / 🆕 | 列表灰显archived_by 后端有但 UI 隐藏 |
| `runtime_mode` | "local" / "cloud" | ✅ 图标 | Cloud / Monitor 图标区分 |
| `instructions` | string | ✅ | Instructions tab |
| `custom_env` / `custom_env_redacted` | KV / bool | ✅ | Env tab权限控制隐藏值 |
| `custom_args` | string[] | ✅ | Custom Args tab |
| `visibility` | "workspace" / "private" | ✅ | Settings tab |
| `max_concurrent_tasks` | number | ✅ | Settings tab默认 6 |
| `model` | string | ✅ | Settings tab |
| `runtime_id` | string | ✅ 选择器 | Settings tab |
| `skills` | Skill[] | ✅ | Skills tab + profile card 前 3 个 |
| `owner_id` | string | ✅ | Profile card 显示名字 |
| `created_at` / `updated_at` | string | 🆕 | **后端有UI 完全没展示** —— 可以做"最近创建"、"最近修改"标签 |
| ~~`status`~~idle/working/blocked/error/offline | enum | ✅ 但**已废弃**展示 | 后端字段保留但 UI 完全不读 |
#### 派生数据(工程在阶段 0 提供,设计稿可放心引用)
| 派生信息 | 来源 |
|---|---|
| **Agent 派生 5 态状态** | 🔧 由 agent + runtime + active tasks 派生 |
| **当前 running 任务数** | 🔧 派生 |
| **当前 queued 任务数** | 🔧 派生 |
| **最近一次失败的原因**5 种 enum | 🔧 派生 |
| **关联 runtime 健康状态** | 🔧 派生 |
| **runtime last_seen 相对时间** | 🔧 已有工具函数 |
**失败原因 5 个枚举(中文文案待你定)**
- `agent_error` — Agent 执行报错
- `timeout` — 执行超时
- `runtime_offline` — Daemon 离线
- `runtime_recovery` — Daemon 重启回收
- `manual` — 用户取消
每种原因用户的处理方式不同UI 应给出对应建议(你来设计文案)。
#### 实时事件WS
| 事件 | 状态 | 用途 |
|---|---|---|
| `agent:status` | 📡 后端发,工程接 | agent 字段变化 |
| `agent:created` / `agent:archived` / `agent:restored` | 📡 工程接 | 列表增删 |
| `task:dispatch` / `task:completed` / `task:failed` / `task:cancelled` | 📡 工程接 | 状态派生关键信号 |
| `daemon:register` | 📡 工程接 | runtime 上下线 |
设计上**不需要为"加载/loading"过度设计**——状态变化是实时的,几乎不会出现"等数据"的 loading 态。
### 3.5 不能动的现有交互(必须保留)
- 创建 agent dialog 的 chooser → 表单两步流(见 Skills 的 `create-skill-dialog`
- 各 tab 的编辑能力Instructions / Env / Custom Args / Settings 全部可编辑
- Archive / Restore 操作
- Tasks tab展示该 agent 的历史 task 列表(按状态分组:活跃 → 完成)
- Skills tab可挂载/卸载 skills
### 3.6 关键问题留给你定的
1. **Archived agent 怎么收纳**:独立 tab、折叠区、还是 segment 切换?
2. **状态筛选**是 chips 还是 dropdown
3. **Failed 红点的 2 分钟动效**:脉冲?颜色渐变?
4. **复合状态Working + 排队 N**角标位置dot 旁 / 头像下角 / Badge 内嵌)?
5. **创建 dialog 的"预览状态"**:怎么不打扰主流程的同时让用户知道"这个 agent 创建出来会是什么色"
---
## 四、Runtimes 界面
### 4.1 当前界面长什么样
主要文件:
- `packages/views/runtimes/components/runtimes-page.tsx` — 容器,含 owner filtermine/all
- `packages/views/runtimes/components/runtime-list.tsx` — 列表
- `packages/views/runtimes/components/runtime-detail.tsx` — 详情头部
- `packages/views/runtimes/components/usage-section.tsx` — Token usage 主区
- `packages/views/runtimes/components/charts/` — 5 个图表组件
- `packages/views/runtimes/components/update-section.tsx` — CLI 更新流程
- `apps/desktop/src/renderer/src/components/daemon-runtime-card.tsx` — 桌面端独有:本机 daemon 卡片(通过 IPC独立于 server runtime 列表)
### 4.2 当前的核心痛点
按严重度排列:
1. **离线圆点几乎不可见**——浅色主题下 `bg-muted-foreground/40` 视觉上消失。
2. **列表无 last_seen**——无法区分"刚断线 5 分钟"和"3 个月前断线"。
3. **看不到 runtime 服务了哪些 agent / 当前有几个 task 在跑**——runtime 在用户心智里成了"孤岛"。
4. **7 天 GC 阈值无任何 UI 提示**——runtime 突然消失,用户不知道为什么。
5. **桌面端 daemon 卡片和云端 runtime 卡片视觉分裂**——同一台机器同一概念,两套设计。
6. **Token usage 信息过载**——5 个图表 + 1 张表全部展开,普通用户找不到"本月花了多少钱"。
7. **无 ping / 诊断按钮**——遇到断线没法主动验证。
8. **`RuntimeModeIcon` 死代码**——本地 vs 云端在列表项里没有图标区分。
### 4.3 重设计目标
#### 必须达成
- 列表项一眼区分四态Online / Recently Lost / Offline / About to GC
- 列表项展示**关联 agent 数量** + **当前任务数**runtime 不再是孤岛)
- 桌面端 daemon 卡片和云端 runtime 卡片用统一视觉语言
- Token usage 区分主次核心指标本期成本、token 总量)放顶部,详细图表折叠或下沉
#### 加分项
- Runtime 健康综合评分(在线 + 心跳新鲜度 + 任务负载)
- 7 天 GC 倒计时提示
- Ping / 诊断按钮
- 高使用量 runtime 的视觉强调(成本警告、利用率热图 sparkline
- Local vs Cloud 图标区分(启用废弃组件 `RuntimeModeIcon`
### 4.4 可用数据清单
#### Runtime 主体字段(来自 `RuntimeDevice` type
| 字段 | 类型 | 当前状态 | 说明 |
|---|---|---|---|
| `name` | string | ✅ | 列表 + 详情头部 |
| `provider` | string | ✅ Logo | 9 种claude / codex / opencode / openclaw / hermes / gemini / pi / cursor 等 |
| `runtime_mode` | "local" / "cloud" | ✅ 文字 | 列表项里没图标(死代码) |
| `status` | "online" / "offline" | ✅ 圆点 | 浅色主题下离线几乎不可见 |
| `last_seen_at` | string | ✅ 仅详情页 | **列表完全看不到** |
| `device_info` | string | ✅ 详情页 | 显示原始字符串如 `darwin-arm64`,无人类可读化 |
| `daemon_id` | string | ✅ mono 字体 | 不可复制 / 不可点击 |
| `metadata.cli_version` | string | ✅ | CLI 更新部分用 |
| `metadata.launched_by` | string | ✅ | 桌面端启动时显示 "Managed by Desktop" |
| `owner_id` | string | ✅ | 头像 + 名字 |
| `created_at` / `updated_at` | string | ✅ | 详情页底部 ISO 时间戳 |
#### 派生数据(工程阶段 0 提供)
| 派生信息 | 来源 |
|---|---|
| **Runtime 4 态健康** | 🔧 由 status + last_seen_at 派生 |
| **服务的 agent 列表 + 数量** | 🔧 前端 joinagent 持有 runtime_id列表数据已在 cache |
| **当前在跑 task 数** | 🔧 前端 filter active-tasks |
| **last_seen 相对时间字符串**"5 minutes ago" | 🔧 工具函数已有 |
#### Token Usage 5 个图表的数据契约
| 图表 | 数据源 | 时间粒度 | 维度 | 度量 |
|---|---|---|---|---|
| **Activity Heatmap** | `getRuntimeUsage?days=90` | 日 | date | 4 级强度,按 token 总量百分位分级 |
| **Hourly Activity** | `getRuntimeTaskActivity` | 小时0-23 | hour | 任务数 |
| **Daily Token Chart** | 同 Heatmap客户端聚合 | 日 | date | input/output/cacheRead/cacheWrite 总和 |
| **Daily Cost Chart** | 同上 + 客户端定价 | 日 | date | 美元成本 |
| **Model Distribution** | 同上聚合 by model | 全周期 | model | tokens 占比 + cost |
**当前实现的问题**API 总是取 90 天数据,客户端做 7d/30d 过滤——浪费服务端资源。改造时按选中窗口拉。
#### 实时事件
| 事件 | 状态 |
|---|---|
| `daemon:register` | 📡 已订阅,触发列表刷新 |
| `daemon:heartbeat` | 📡 后端发但前端**故意忽略**(防过度刷新)。设计时假设状态变化最坏 75 秒内可见 |
#### 桌面端独有的本机 IPC 数据(仅桌面端可见)
```
DaemonStatus (本机 IPC亚秒级实时)
├─ state: running / stopped / starting / stopping / installing_cli / cli_not_found
├─ pid, uptime, daemonId, deviceName, serverUrl
├─ agents: 当前运行的 agent IDs
├─ workspaceCount
└─ profile
```
工程会把这份数据自动喂进 cache**设计上不要为"桌面端有更多数据"做特殊视觉**——派生状态对设计师而言是统一的,只是桌面端响应更快。但桌面端**对自己的本机 daemon 有更多操作能力**(见第 6 节)。
### 4.5 不能动的现有交互(必须保留)
- Owner filtermine/all toggle
- Delete runtime 操作(含权限检查)
- CLI 更新流程(`update-section.tsx`):检查更新、触发更新、查看更新状态
- 5 个图表的数据展示(信息架构可以重排,但数据本身要保留)
- 桌面端start/stop/restart 本机 daemon 的按钮
### 4.6 关键问题留给你定的
1. **列表项到底放多少信息**last_seen + agent count + active tasks 放哪?避免拥挤
2. **桌面端 daemon 卡片和云端 runtime 卡片"视觉对齐"的尺度**100% 同模板?还是同卡片框 + 内容差异化?
3. **Token usage 主次怎么排**:本期成本数字 + 单图表 + "查看更多" 折叠?或者 dashboard 化?
4. **About to GC 怎么提示**横幅badge倒计时
5. **Ping / 诊断按钮的位置**:详情页头部?右上角菜单?
6. **关联 agent 列表展示**:堆叠头像?文字 "3 agents"?还是子区块?
---
## 五、跨界面统一Agent Hover Card
### 5.1 现有组件
`packages/views/agents/components/agent-profile-card.tsx`——已经存在,但只在 Agents 主页 hover 时出现。
这次会把它升级成**统一 hover card**,挂到所有展示 agent 头像的地方。
### 5.2 必须出现的位置
| 位置 | 当前状态 |
|---|---|
| Agents 列表 / 详情 | ✅ 已有 |
| Issue Assignee Picker | ❌ 仅头像无状态 |
| Issue Detail 头部 assignee | ❌ 仅头像无状态 |
| Issue 列表 / 看板的分配头像 | ❌ 仅头像 |
| Autopilot 列表 / 编辑assignee | ❌ 仅头像 |
| Project lead picker | ❌ 仅头像 |
| Chat 选择 agent 面板 | ❌ 待确认 |
| 评论里的 @agent | ❌ 仅头像 |
### 5.3 卡片必须显示什么(按重要度)
1. **派生 5 态状态**(不是 `agent.status` 原始值)
2. **Runtime 健康**:在线性 + last_seen 相对时间
3. **当前任务**N running / M queued
4. **最近失败**(如果有):原因 + 时间
5. **Agent 名称 + description**
6. **关联 skills**(前 3 个 + `+N`
7. **Owner**
### 5.4 设计要点
- 卡片宽度跨多种使用场景要适配issue 列表很窄、设置页很宽)
- 触发延迟hover delay跟 Skills 已有的卡片保持一致
- 暗色主题下信息层级要清晰
---
## 六、跨平台差异处理
### 6.1 状态视觉是平台无关的
派生 5 态 / 4 态、视觉规范、hover card——**两端共享同一套设计**。设计稿只画一份。
### 6.2 数据响应速度差异(不影响视觉)
| 平台 | Runtime 状态变化感知延迟 |
|---|---|
| Web | 最坏 75 秒 |
| Desktop看自己机器 | < 1 秒IPC |
| Desktop看别人机器 | 最坏 75 秒(跟 Web 一样) |
设计上不需要透出"快慢"——用户感知不到这是 IPC 还是 server。
### 6.3 操作能力差异(影响按钮可见性)
| 操作 | Web | Desktop自己机器 | Desktop别人机器 |
|---|---|---|---|
| 看状态 | ✅ | ✅ | ✅ |
| 重启 daemon | ❌ | ✅ | ❌ |
| 看 daemon logs | ❌ | ✅ | ❌ |
| 看 CLI 安装详情 | ❌ | ✅ | ❌ |
**设计要点**:操作按钮在不该有权限的位置应该**直接隐藏**,不要灰显(避免视觉噪音)。
### 6.4 桌面端独有的"本机 daemon 卡片"
当前桌面端有一个独立卡片显示本机 daemon。重设计后
- 视觉上跟云端 runtime 列表项**用同一套视觉语言**
- 但承载更多本地操作(重启、看日志、安装 CLI、profile 切换)
- 位置:列表顶部 sticky / 列表头部突出 / 右侧独立 panel —— 由你定
---
## 七、设计语言参考Skills 界面
### 7.1 Skills 是这次重设计的视觉锚点
Skills 界面已经在 2026-04 完成重设计PR #1607#1614#1618#1610)。这次 Agents 和 Runtimes **直接照搬 Skills 的视觉语言**,保持产品体感一致。
参考目录:`packages/views/skills/`
### 7.2 必须复用的 10 条规则
1. **统一页头** `PageHeader`h-12 + mobile sidebar trigger
2. **响应式网格列表**`grid-cols-[minmax(0,1.6fr)_minmax(0,0.8fr)_minmax(0,1.2fr)_minmax(0,6rem)_auto]`,不用 flexbox
3. **每行三层信息**主标题font-medium→ 描述line-clamp-1 muted→ 元数据xs muted
4. **关联对象用头像堆栈**:最多 3 + `+N`size=22 + `ring-2 ring-background` + `-space-x-1.5`
5. **卡片化列表 + 卡片内工具栏**:搜索和 scope tab 在 `CardToolbar`h-12不在页面级
6. **创建用多步 dialog**chooser → 表单可回退Dialog 宽度按方法切换manual/url 用 `!max-w-md`runtime 用 `!max-w-2xl`300ms 平滑过渡
7. **空状态 / 筛选无结果分别有详细文案**:图标 + 标题 + 三行说明 + 清晰 CTA
8. **长列表加 `useScrollFade`**:滚动容器上下边缘淡出
9. **头像统一用 `ActorAvatar`**:传 `size`,自动支持 agent / 人员
10. **权限检查 hook 化**`useCanEdit...`UI 提前隐藏/禁用操作按钮
---
## 八、工程会同步交付的能力
阶段 0数据层地基完成后**设计稿可以放心假设以下能力都到位**
- 任意位置都能拿到一个 agent 的派生 5 态状态
- 任意位置都能拿到 agent 的当前任务数running / queued
- 任意位置都能拿到 agent 关联的 runtime 4 态健康
- 任意位置都能拿到 runtime 服务的 agent 列表 + 数量
- 任意位置都能拿到 runtime 当前任务数
- 状态变化是实时的(订阅 WS 事件后会自动更新)
- 桌面端会自动获得"亚秒级"响应——不需要为此画两套稿
### 已有 API不需要新加可放心引用
- `listAgents` / `getAgent` / `createAgent` / `updateAgent` / `archiveAgent` / `restoreAgent`
- `listAgentTasks(agentId)` — 单 agent 历史任务
- `listRuntimes` / `deleteRuntime`
- `getRuntimeUsage(runtimeId, { days })` — token 用量
- `getRuntimeTaskActivity(runtimeId)` — 小时级活动
### 工程要在阶段 0 补的(设计稿可以假设有,但要知道这是新增)
- **后端**`GET /api/workspaces/:slug/active-tasks` — 全工作区活跃任务一次拉
- **后端**:诊断 / Ping API如果你的设计稿用到工程要评估优先级
- **前端类型**`AgentTask.failure_reason` 字段5 枚举agent_error / timeout / runtime_offline / runtime_recovery / manual暴露到前端类型
- **前端**:派生函数(`deriveAgentPresence` / `deriveRuntimeHealth`+ 全工作区 active-tasks query + WS 接线
### 工程不会做的(设计稿不要假设有)
- 不引入"agent 健康综合评分"——只暴露原始信号
- 不做"从历史 task 自动推断 agent 类型"等 AI 派生
- 不为 runtime 引入新状态色(稳定在 4 态)
- 不做后端聚合 API除了 active-tasks 这个补全)
---
## 九、设计师产出清单
按优先级排列,**P0 必须先于 P1**。
### P0 — 状态视觉规范(基础,所有界面共用)
- 5 态颜色 tokenAvailable / Working / Pending / Failed / Offline
- 4 态颜色 tokenOnline / Recently Lost / Offline / About to GC
- Dot / Badge / Tooltip 三层视觉规范
- 复合维度Working + 排队角标)的视觉表达
- Failed 状态的 2 分钟时间窗口动效(强提示 + 自动消失)
### P0 — Agents 界面
- 列表页(含派生状态、关联 runtime 健康、最近活动时间)
- 详情页头部 + Profile Card状态行联动
- 创建对话框(保留两步流,加入 runtime 在线状态预览)
### P0 — Runtimes 界面
- 列表页(暴露 last_seen、关联 agents、当前 task 数)
- 详情页头部4 态 badge、device info 人类可读化)
- Token usage 信息架构重整:核心指标置顶,详细图表下沉/折叠
### P1 — Hover Card 跨界面统一
- 一个适配多场景宽度的卡片设计
- 7 项内容的信息层级
- hover 触发交互(与 Skills 一致)
### P1 — 桌面端本机 daemon 卡片
- 视觉对齐云端 runtime 卡片
- 本机操作按钮(重启 / 日志 / CLI 安装)的位置
- Profile 切换(如果做多 profile
### P2 — 加分项
- Runtime 健康综合评分(视觉化)
- 7 天 GC 倒计时
- 高使用量 runtime 的成本警告
- Local vs Cloud 图标区分
- Agent archived 独立 tab/折叠区
- 列表按"最近活动"排序、按状态筛选
---
## 十、附录:截图占位区
> 设计师拿到这份文档后,请把以下三个界面的当前截图贴在对应位置,作为重设计前的现状记录,方便对比。
### 10.1 Agents 界面
- 列表页截图__待贴__
- 详情页截图(每个 tab 一张Instructions / Skills / Tasks / Environment / Custom Args / Settings__待贴__
- Profile Card 截图__待贴__
- 创建对话框截图chooser + form 两步__待贴__
- Hover card 当前样式截图__待贴__
### 10.2 Runtimes 界面
- 列表页截图mine / all 两态__待贴__
- 详情页截图(含 5 个图表全部展开__待贴__
- 桌面端 daemon 卡片截图__待贴__
- CLI 更新流程截图__待贴__
### 10.3 跨界面 agent 头像出现的位置
- Issue Assignee Picker__待贴__
- Issue Detail 头部 assignee__待贴__
- Issue 列表 / 看板__待贴__
- Autopilot 列表 / 编辑__待贴__
- Project lead picker__待贴__
- Chat agent 选择面板__待贴__
- 评论 @agent__待贴__
---
## 参考文档
- [Agent / Runtime 状态系统重设计(主文档)](./agent-runtime-status-redesign.md) — 完整工程方案、状态规范、实施阶段
- [产品全景文档](./product-overview.md) — 理解 agent / runtime / daemon 在整个产品里的位置
- [Skills 界面源代码](../packages/views/skills/) — 直接参考的设计语言样板
- 相关 PR#1607Skills 重设计)、#1614Card + PageHeader#1618(描述恢复)、#1610Dialog 闪烁修复)