docs(tasks): 工作图打磨的设计文档与真机验证证据;卡片细节两处修正

- 新增 docs/plans/2026-09-27-tasks-graph-polish.md:本轮问答结论、架构与文件、
  关键设计(卡片两段 / 合并活动 / 扫光 / 折叠聚合 / ctx.jobs / 布局与窄栏)、
  移植与丢弃清单、实施偏差记录、验证证据(含 3080 真机逐项目击记录与截图路径)、
  未在真机复现的两项与已知取舍。
- 2026-09-14 图分支设计文档加「本轮打磨」小节指向新文档;README 任务页条目
  与 AGENTS §1/§3.1#4 同步(后台任务改读宿主客户端 ctx.jobs,不再自建回放路由)。
- 卡片两处修正(真机截图暴露):
  ① 小条的钩子从 `data-state` 改为 `data-card-bar` —— 宿主自己的 primitive 也在
     发 `data-state`(菜单分隔、编辑器等),同名属性让「卡片内恰好一条小条」的
     断言与用户 CSS 都不可靠;
  ② 上段的 mono 行改由新的 `agentIdentity()` 产出(只讲「是谁」),根节点返回空
     —— 原先复用 `agentMeta()` 会把状态词印两遍(徽章下方 + 小条),正是用户
     反馈的「节点内信息太密」。可访问名继续用含状态的 `agentMeta()`。

验证:typecheck / lint 通过;pnpm test 1240 passed / 9 skipped;
挂载 lane 7/7(真实 DSH 0.1.7-rc.2);3080 真机自测通过(见文档表格)。

(cherry picked from commit 338a54820799c26ab91141e8d98c673b8d474610)
This commit is contained in:
Menghuan1918
2026-09-27 22:18:49 +08:00
parent 48294d336e
commit 5ea2f2d6ec
7 changed files with 210 additions and 11 deletions
+2 -2
View File
@@ -11,7 +11,7 @@
- **代码改动必须走 PR**:非文档改动在 `feat/*` / `fix/*` 分支开发,`gh pr create` 发起,review 合并后进 main;**仅纯文档改动**(README / AGENTS.md / docs/)允许直推 main。
- **挂载只走 `cordis.patch.yml` + profile 机制**(`~/.dsh/profiles/<profile>/`),插件作为独立包被 profile 引用,不反向侵入 DSH。
- **市场受管安装约束**:`dependencies` / `peerDependencies` / `optionalDependencies` **一律不得出现 `cordis`**(按名硬拒,optional 无效),`scripts` 不得含 `preinstall` / `install` / `postinstall` / `prepare`。由 `tests/market-manifest.spec.ts` 守护。
- 缺能力时用 DSH 现成只读/公开 API 或插件自有路由(如 `jobs.output` 事件回放:读会话事件日志而非动注册表);做不到先向用户说明取舍,不改 DSH。
- 缺能力时用 DSH 现成只读/公开 API 或插件自有路由(会话事件日志回放是既有手段之一,但后台任务已有宿主客户端 `ctx.jobs`,别再自建回放路由);做不到先向用户说明取舍,不改 DSH。
---
@@ -39,7 +39,7 @@
3. **会话格式 v3 → v4**(`packages/session/session-format-v3-to-v4`):两处变化。
- **`source.kind === 'plugin'` 被硬拒**(`message-sources.ts` 抛 `format v4 message requires a producer-owned source kind`),而插件的 sidechat 边界注入正是那个形状。改用 **`plugin:dsh-better-sidebar`**:与 DSH 自己的 v3→v4 迁移对历史行产出的 kind **逐字一致**(`producerKind()` 对未知插件名返回 `` `plugin:${plugin}` ``),所以新旧数据形状统一,**历史 sidechat 会话不会丢**。类型层面还需一条 `declare module '@deepseek-ai/dsh-llm'` 的 `MessageSourceMap` 增强(0.1.7 把它改成合并可扩展的封闭联合,没有共享 catch-all)。
- **tool 结果消息重写**:`role: 'user'` + 嵌套 `type: 'tool-result'` 包装块 → `role: 'tool'` + **顶层** `content` / `isError` / `toolCallId`。插件的 4 个解析器原先靠 `candidate.type === 'tool-result'` 命中,在新形状下恒返回空串(**静默空白**)。现在四处都**同时接受新旧形状**(旧分支必须保留:历史日志里就是旧形状)。
4. **客户端会话快照换字段**:`SessionListState.subagentsByParent` / `jobsBySession` 被 `projectionsBySession` 取代,且 **`setSubagentCatalogOpen` 这个按需观察 API 被整体删除**(0.1.7 在连接建立时一次性加载所有 Session 的 projection,所以那套 observe/unobserve 机制应当删除而不是改名)。子代理目录改读 `projectionsBySession[sid]?.values.subagentCatalog`(`{id, createdAt, mode: 'one-shot'|'continuable'|'unknown', label?}`,**注意 `unknown` 是新值**);后台任务列表**没有替代字段**,改由插件新增的 `jobs.list` 路由读 `ctx.jobs.list(sessionId)`。同一版里 `ctx.jobs.list/get/read/kill` 的 caller 从 `Agent` 变成 `SessionId`(插件因此可以彻底不碰 `ctx.agents` 来调注册表)。
4. **客户端会话快照换字段**:`SessionListState.subagentsByParent` / `jobsBySession` 被 `projectionsBySession` 取代,且 **`setSubagentCatalogOpen` 这个按需观察 API 被整体删除**(0.1.7 在连接建立时一次性加载所有 Session 的 projection,所以那套 observe/unobserve 机制应当删除而不是改名)。子代理目录改读 `projectionsBySession[sid]?.values.subagentCatalog`(`{id, createdAt, mode: 'one-shot'|'continuable'|'unknown', label?}`,**注意 `unknown` 是新值**);后台任务列表**没有替代字段**,但 0.1.7 的 web profile 会挂载**客户端** `ctx.jobs` 服务(`@deepseek-ai/dsh-api-job-controller/client`,`web-app/cordis.patch.yml`),任务页直接读它——`state.getSnapshot()/subscribe()` 是整份快照(`{rows: 按被观察会话的 roster, observed: 按任务 id 的保留输出}`),`watchRows(sessionId)` 是按会话引用计数的推送 roster(空集合即该会话无任务),`observe(sessionId|undefined, jobId)` 是**非消费**的输出流(返回释放函数),`kill(sessionId, jobId)` 是注册表准入。插件因此**删掉了自建的 `jobs.list` / `jobs.output` / `jobs.kill` 三条路由**(`src/jobs-routes.ts` 已不存在),也不再用事件回放读模型已读的输出:那个 pane 从诞生起就受「不能碰模型 `job_output` 游标」的约束,宿主服务把这条约束变成了能力。同一版里 `ctx.jobs.list/get/read/kill` 的 caller 从 `Agent` 变成 `SessionId`(插件因此可以彻底不碰 `ctx.agents` 来调注册表)。
### 3.2 本版的能力让出与收敛
@@ -188,6 +188,17 @@ PR:[#680](https://github.com/omdsh-dev/DSH-better-sidebar/pull/680)(分支 `
- `tests/bundle-route.spec.ts` 的 `CHUNK_NAMES` 期望回到 4 项;`tests/manifest-consistency.spec.ts` 的「每个 chunk 都被 `package.json#files` 覆盖」断言保留,列表继续从 `CHUNK_NAMES` 派生(不手抄)。
- `README.md` 两处「视觉基座为 shadcn/ui + Tailwind v4」描述删除;`docs/external-plugin-guide.md` 删除 §12.3「在插件里用 Tailwind / shadcn/ui」整节(含 preflight 规避、令牌桥、静态面板零阴影三条)。
### 验收(回退后,2026-09-21)
## 本轮打磨(2026-09-27,`feat/tasks-graph-polish`)
图模式(主显示模式)按用户 brief 换成两段式卡片:上段是类型徽章 + 相位徽标 + 名称 + 元信息,
下段小条是状态点 + 状态词 + **主 Agent 同款合并活动行** + 已完成节点的折叠 chevron;
运行中小条从左到右扫光;层级/分支与 workflow 相位各用一套令牌色阶分组;后台任务从插件自建的
三条路由改成宿主客户端 `ctx.jobs`(推送 roster + 非消费观察流 + kill,`src/jobs-routes.ts` 已删除);
窄栏可读性按 a–f 六项逐一处理(卡片高度预算重算、内容包围盒居中、缩放手感、平移夹取、连线 stub 加粗、
相位框配色)。**树模式保持原样**(只随共享数据类型做最小适配)。
完整设计与偏差记录见 [2026-09-27-tasks-graph-polish.md](2026-09-27-tasks-graph-polish.md)。
## 验收(回退后,2026-09-21)
`pnpm typecheck` ✅ 0 错误;`pnpm lint` ✅ 0 错误;`pnpm vitest run` **133 文件通过 / 1410 用例通过 / 9 skipped / 0 失败**(回退前:4 个文件失败,其中 `theme.spec.ts`、`ui-foundation.spec.ts` 在收集期就因 `src/client/ui/theme.css` 缺失报 ENOENT,`ui-shadows.spec.ts` 2 例、`bundle-route.spec.ts` 1 例断言失败)。
+168
View File
@@ -0,0 +1,168 @@
# 任务管理页「工作图」打磨:双段式卡片 + 主 Agent 同款合并活动 + 折叠聚合 + 窄栏可读性
日期:2026-09-27 分支:`feat/tasks-graph-polish`(基于 `feat/tasks-graph-workflow-teams`,接 PR #680)
## 背景:为什么不是「再做一版卡片列表」
上一轮(`feat/tasks-page-card-redesign`)把任务页当成「树 + 卡片」重做了一遍,方向被用户否掉:**主显示模式仍然是工作流图**(`feat/tasks-graph-workflow-teams` 里那一版图),树模式保留即可;图模式才是与官方做出差异化的地方。用户随后给出这一轮的 brief(原文):
> 参考图分支,将其 UI 等合并到之前我说的现代美观干净的风格:
> - 改为组合的双段式的结构,即上方为显示节点类型和节点详细内容,而下方是一个小条,用于显示当前的状态、执行的及执行的工具,已完成、折叠展开等操作。
> - 执行时有一个从左到右扫光的特效,同时执行的工具逻辑和主 Agent 中合并显示的逻辑一样。
> - 当为已完成的状态时,要有一个可点击的折叠按钮,在下方的小横条中。
> - 优化排布逻辑,考虑有很多节点/阶段的情况,考虑窄屏的特点。
因此这一轮 = **图分支的骨架 + 上一轮验证过的视觉/交互语言**。上一轮的卡片 UI 与树改造整体丢弃,只把钱花在已验证的数据层上(见「移植与丢弃」)。
## 用户确认的决策(问答摘要)
| # | 问题 | 结论 |
|---|---|---|
| 1 | 打磨范围 | ② 运行中节点的扫光/执行态视觉;③ 后台任务改读 `ctx.jobs`;④ 图布局与窄栏可读性;⑤ 折叠聚合打磨 |
| 2 | 「阶段」要不要做成可展示列表 | **不要**:「阶段做到卡片徽标 + 颜色标明分组即可」 |
| 3 | 阶段徽标针对哪种分组 | **两者都要**:层级/分支分组 + workflow 相位分组 |
| 4 | 图节点双段式怎么落地 | 图上也是**真两段**(宽度保持现有量级,高度按新结构重算) |
| 5 | 树模式要不要一起换新语言 | **不要**:树模式保持现状 |
| 6 | 图节点小条上的折叠按钮 | **收进「已完成聚合」**(不是收起上段) |
| 7 | 窄栏具体痛点 | a 卡片太窄文字被截 / b 偏小、不居中、缩放手感 / c 节点内信息太密 / d 连线与相位框 / e 拖拽与缩放手感 / f 折叠聚合 —— 全选 |
| 8 | 代码落点 | 本工作区从图分支开新分支;上一轮版本丢弃 |
## 架构与文件
```
host 半(src/)
process-activity.ts 新增:宿主 ui-chat 的 process group 算法移植(分类 / live 详情 /
范围切割),裁剪掉逐阶段列表(阶段改由徽标表达)
subagent-live-route.ts 改为:树内每个子节点(含已结算)与拓扑根都回一帧
SidebarChildLiveView { running?, text?, summary?, lastEventTime? }
context-types.ts SidebarChildLiveView / SidebarClientJobsService / SidebarJobsSnapshot /
SidebarObservedJob;SidebarJobView 补 owner、progress
删 jobs-routes.ts 自建三条路由退出历史(见下)
client 半(src/client/)
process-labels.ts 新增:宿主 chat 命名空间措辞(liveActivityLabel / doneActivityTitle)
tasks-card.tsx 新增:卡片两段(CardTop / CardBar)+ 类型徽章 + 深度/相位色阶
jobs-client.ts 新增:ctx.jobs 读取层(watchRows 引用计数 / observe / collectRows)
block-labels.ts 新增:TerminalBlock 的 labels(零新增词条)
TasksGraph.tsx 三个节点渲染器改双段式;fit / 缩放 / 平移 / 连线重做
tasks-graph.module.css 卡片两段 / 徽标 / 小条 / 扫光 / 相位框配色
tasks-graph-layout.ts 卡片高度预算按新行重算,宽度 176 → 190
SubagentView.tsx foldedIds(手动折叠集)+ 树内 jobs 改读客户端服务
sidebar/use-host-feeds.ts 任务自动激活改骑推送 roster(detectNewJob 加观测起点时钟)
```
## 关键设计
### 1. 卡片两段(图模式)
```
┌────────────────────────────────┐
│▎[类型徽章] [相位徽标] │ ← 上段:身份 + 组色左描边(2px)+ 极淡底色
│ 名称(最多两行 clamp) │
│ mode/model · 角色(mono) │
│ ☑ 任务主题 · 状态 +N(团队成员) │
├────────────────────────────────┤
│ ● 运行中 · 正在运行命令 · npm… ⌄ │ ← 小条 22px:状态点 + 状态词 + 合并活动 + 折叠
└────────────────────────────────┘
```
- 类型徽章用页面自己的词汇:`subagentMainAgent`(主代理,复用)+ 新增 `tasksKindSubagent` / `tasksKindTeammate` / `tasksKindWorkflow`;聚合节点用 `tasksFoldCompleted`(`✓N 已完成`)。
- **组色 = 树深度**:4 档令牌色阶(`color-mix(in srgb, var(--dsw-alias-state-business-primary) 100/72/48/28%, transparent)`),作用于左描边与上段底色;更深层级钳到最后一档。
- **相位徽标 = workflow 相位**:走 ink 色族(`--dsw-alias-label-primary` 的 100/78/58/42% 色阶),与组色族分开,避免两套分组撞色;虚线相位框保留(空间分组),改用同色极淡描边。
- **皮肤契约约束**:宿主不提供图表调色板,插件也不允许硬编码颜色,所以「颜色分组」只能做成令牌色阶 —— 这是本轮唯一被契约限死的设计点,不是取舍。
### 2. 合并活动(主 Agent 同款)
`foldProcess(events, { live })` 把子会话日志折成宿主 process group 的形状:分类计数(`activityOf` 表逐条照抄)、最新未结算调用(含 `preparing` = 模型宣布但未派发)、`runningDetail`(宿主键优先级 + 160 字 grapheme 截断)、范围边界(`user/message` 与带文本的 `assistant/message`)。客户端 `liveActivityLabel` / `doneActivityTitle` 用宿主 `chat` 命名空间的 `message.stepProcess.*` 词条合成措辞(`chatT` 失败即回退插件文案),所以卡片上的字与主对话完全一致。
**一条刻意偏差**:最新范围为空(子代理刚回复完)时回退到上一个「做过事」的范围 —— 否则已结算卡片的小条只会显示「已完成分析」。回退范围永不报告运行中。
### 3. 扫光(执行态)
小条 `data-running="true"` 时叠加一条 45% 宽的令牌渐变带,`background-position 0% → 100%`,1.5s `cubic-bezier(0.33,0,0.67,1)` 循环、末 1/3 停顿;`prefers-reduced-motion: reduce` 下关闭动画(状态词与活动文本已承载同样信息)。方向从左到右(用户确认;宿主的文本 shimmer 是反向,这里刻意不同)。
### 4. 折叠聚合
- 页面持有 `foldedIds`(读者从卡片小条逐个收起)与 `folded`(控制条的全局规则),模型里两者走**同一组守卫**:只折已结算叶子、永不折当前会话、永不折有子节点的、永不折团队成员。
- 聚合节点(`fold:<parentId>`)点击 = **全部展开**(清空手动集 + 关闭全局折叠);控制条按钮只切全局规则。
- 折叠/展开不重置读者当前的缩放与平移(自动 fit 只在读者未触碰视图时生效)。
### 5. 后台任务改读宿主客户端服务
0.1.7 的 web profile 挂载 `ctx.jobs`(`@deepseek-ai/dsh-api-job-controller/client`):`state` 是整份快照(按被观察会话的 roster + 按任务 id 的保留输出),`watchRows` 是按会话引用计数的**推送** roster,`observe` 是**非消费**输出流(返回释放函数),`kill` 是注册表准入。插件因此删掉自建的三条路由与事件回放:
- 删除 `src/jobs-routes.ts`、`index.ts` 的 `jobs.list/output/kill`、`api.jobsList/jobOutput/jobKill`、`SidebarJobsService`、页面的 `jobs.list` 轮询与 `collectTreeJobs`;
- 抽屉的行来自推送 roster(状态/时长/owner/进度)、输出面换宿主 `TerminalBlock` 显示观察流(`gapBefore` → 截断提示、`error` → 失败提示)、kill 走 `jobs.kill(owner, jobId)`(两击确认不变);
- 无 `ctx.jobs` 的部署**整段不渲染**(结构性降级),插件的其它面不受影响。
**旧路由为什么必须死**:它读的是「模型已读到的输出」(事件回放),因为注册表当时只有模型的**消费**游标可读;宿主服务把这条约束变成了能力(观察流不碰游标),继续维护回放既多余又永远落后于流。
### 6. 布局与窄栏(a–f 全项)
- 卡片高度预算重算:徽章 16 + 两行标题 36 + 元信息 16 + 小条 22 + 内边距 11 + 边框 2 = **103px**(团队成员 +18 任务行);宽度 176 → **190px**(360px 侧栏仍两列,缩放 ~0.86)。**live 行不再占高度** —— 合并活动在小条里,任何状态的行高一致。
- fit 按**内容包围盒**(而非含 padding 的整张画布)双轴居中,`FIT_MAX_SCALE` 1.15 → 1.30;空画布**双击**重新适配并恢复自动 fit。
- 缩放范围 0.3–1.5 → **0.4–2.0**,并按 `deltaMode` 归一化滚轮刻度(行模式鼠标不再比触控板慢 30 倍)。
- 平移加**边界夹取**(至少 72px 内容留在视口内),拖不出视野。
- 连线改「垂直出线 stub(8px)+ 贝塞尔」,线宽 1 → **1.5px**(0.78–0.86 的 fit 缩放下 1px 会落到一个设备像素以下,整棵层级会糊掉)。
## 移植与丢弃
| 上一轮产物 | 处置 |
|---|---|
| `src/process-activity.ts` + 44 例 | **移植并裁剪**(删逐阶段机制,保留分类/详情/范围切割);新增「空范围回退」一条并补测 |
| `src/client/process-labels.ts` + 10 例 | 原样移植 |
| `chatT` + 4 例回归(含「不许解绑 bind」) | 原样移植 |
| `src/client/block-labels.ts` | 原样移植(jobs 输出面复用) |
| `ctx.jobs` 读取层(`clientJobs` / `useJobsSnapshot` / `collectRows`) | 移植数据层;展示组件丢弃(本分支有自己的抽屉) |
| `subagents.live` 新载荷 + `SidebarChildLiveView` | 移植(去掉 `expand`/`stages`) |
| 卡片 UI(`subagent-card.tsx` / `subagent-runs.ts` / 树重写 / 5 个词条) | **丢弃** |
## 实施偏差记录(写给复审)
1. **`process-activity.ts` 是裁剪版而不是原样搬运**:本轮「阶段」= 徽标 + 颜色,逐阶段列表没有消费者,留着就是死代码。裁剪后 44 例 → 30 例(阶段相关用例随机制一起删除)。
2. **`CardBar` 的折叠 chevron 用新词条 `tasksFoldOne`(收进已完成聚合)而不是复用 `tasksFoldCollapse`(折叠已完成的节点)**:后者已经是控制条全局开关的可访问名,同一个 aria-label 出现在两个按钮上会让屏幕阅读器与测试都无法区分「折一个」和「折全部」。
3. **`TasksAgentNode.depth` 是为此新增的字段**:卡片组色需要层级,而模型的递归本来就知道深度;顺带给图节点加 `data-depth`(用户 CSS 与测试的可寻址面,和树行的 `aria-level` 同性质)。
4. **`detectNewJob` 加了第三个参数(观测起点时钟)**:roster 改成推送后,宿主在「该会话没有任务」时会**删掉这个 key**,于是空的首页帧与「还没送到」不可区分 —— 页面挂载时已经在跑的任务会被 id 差集误判成新工作并弹任务页。用 `job.startedAt >= 观测开始时刻` 过滤;自动激活 spec 里两条用例钉住这个语义。
5. **两个测试期望被改而不是改代码**:`subagent-live-polling.spec.tsx` 原先断言卡片上有「思考中…」/「hello」文本行 —— 那是旧卡片的 live 行,现在文本行只在小条的活动行里;`subagent-jobs-view.spec.tsx` 的「多任务仍紧凑」用例原先假设抽屉默认折叠,而折叠阈值数的是**代理数**(1 个代理 → 默认展开)。两处都是断言跟随设计,不是放宽。
## 验证证据(2026-09-27)
| 项 | 结果 |
|---|---|
| `pnpm typecheck` | ✅ 0 错误 |
| `pnpm lint` | ✅ 0 错误 |
| `pnpm test` | ✅ **1240 passed / 9 skipped / 0 failed**(119 文件) |
| `pnpm build` + `pnpm pack` | ✅ tarball `dsh-better-sidebar-0.21.1.tgz`,`lib/client.js` 含 `dsh-tasks-bar-sweep` 与卡片类 |
| 新增/改写的守护 | `process-activity`(30)/ `process-labels`(10)/ `tasks-model`(depth、phase、foldedIds)/ `tasks-graph-layout`(新高度预算与带宽)/ `tasks-page`(双段结构、`data-running`、chevron→聚合、聚合→全展开、相位徽标)/ `subagent-jobs-view`(14,假 `ctx.jobs`)/ `sidebar-auto-activation`(12,假 `ctx.jobs`)/ `locales`(新词条 ×20 词典) |
| `pnpm test:mount` | 见下节(真机挂载 lane) |
| 3080 真机自测 | 见下节(Playwright 驱动真实工作 + 截图) |
### 真机挂载 lane
`DSH_CMD=/tmp/dsh-rc-home-shim.sh DSH_EXPECT_VERSION=0.1.7-rc.2 bash scripts/e2e-mount.sh` → **7 passed**(日志 `/tmp/e2e-polish.log`)。本地的 shim 是必需的:桌面版 `~/.local/bin/dsh` 把 `DSH_HOME` 硬编码成 `~/.dsh`,lane 的 scratch profile 会写进真实配置目录。lane 同时证明宿主侧挂了 `@deepseek-ai/dsh-api-job-controller/client.js` 与 `@deepseek-ai/dsh-client-ui-jobs/client.js`(本轮 `ctx.jobs` 依赖的服务确实在 web profile 里)。
### 3080 真机自测(真实工作 + Playwright)
装法:`cp` 到一个**新文件名**再 `dsh plugin --profile web add file:...` —— 同路径同版本号的 tarball 会被 pnpm 当成已装而不换内容(实测:旧的 client bundle 仍在,页面渲染的还是上一轮的卡片版)。装完 `pm2 restart dsh-web`,从 pm2 日志取一次性 token。
驱动真实工作(两个后台任务 + 一个子代理),逐项核验:
| 目标 | 真机证据 |
|---|---|
| G1 双段式 | 根卡 innerText = `主代理 / 并行启动后台任务与统计子代理 / 运行中 / 正在分析请求`(徽章 + 名称 + 小条),每张卡都有 `[data-card-bar]` |
| G2 扫光 | 运行中小条的 `::after` computed style:`animationName` 含 `dsh-tasks-bar-sweep`、`1.5s`、`cubic-bezier(0.33, 0, 0.67, 1)`、`infinite`、`background-size: 45% 100%`,采样时正处在 `6.03%` 的行程中 |
| G3 合并活动 | 小条文案 `正在分析请求`(宿主 `chat` 词条的运行时输出,不是插件自造词);结算后读数为 `已调用工具` / `已完成分析` |
| G4 折叠聚合 | 一次点击 `button[aria-label="收进已完成聚合"]` 后该节点从画布消失、聚合卡出现(`✓ 1 已完成 / Count files in directory`);点聚合后节点回归 |
| G5 徽标与组色 | 卡片徽章 `主代理` / `✓ 1 已完成`;上段左侧 2px 组色描边在截图中可见;相位徽标**未在真机复现**(该实例没有 workflow run) |
| G6 窄栏 | 视口收到 820px 时宿主把面板给到 **364px**:面板 `scrollWidth == clientWidth == 364`(无横向溢出)、文档两轴都不可滚、画布 363×672、卡片 **247×134**(= 190×1.30 / 103×1.30,与布局预算逐像素一致)、小条 30px(= 22×1.30)、fit 读数 130% |
| G7 后台任务 | 抽屉显示两个真实任务(`运行中 · 0 秒` / 结算后 `已完成 · exit code: 0 · 30 秒`);点开一行后输出浮窗用宿主 `TerminalBlock` 流式显示 `tick 1`(`ctx.jobs.observe` 的非消费流),并带 `终止`(两击)与 `跟随最新` |
| 无崩溃 | 无 pageerror、无插件错误条 |
截图:`/tmp/dsh-selfcheck/01-running-graph.png`(运行态)、`02-job-stream.png`(后台任务输出流)、`03-settled-graph.png`、`04-folded-one.png`、`05-final.png`、`06/08-narrow*.png`(364px 窄栏)。
**未在真机复现的两项**(如实记录):① 相位徽标/相位框的真机观感(该实例没有 workflow run,无法在不造数据的前提下产生);② 扫光的**像素级**运动(只验证了 computed style 与行程采样,没有做逐帧像素 diff)。另外宿主面板的宽度手柄对本轮的合成拖拽不响应,窄栏是通过视口宽度达成的。
### 已知取舍(复审可见)
- 组色只能做成令牌色阶(宿主无图表调色板,皮肤契约禁止字面色),超过 4 档的深度钳到最后一档:颜色不再区分,徽章文本仍区分。
- 结算节点的合并摘要只反映「最近一个做过事的范围」;再往前的历史只能进节点详情浮窗。
- `Session.snapshotEvents()` 在 0.1.7 标了 `@deprecated`(实现是缓存冻结快照,成本可接受),折叠过程只扫到第一个可汇报的范围边界。
+2 -2
View File
@@ -40,7 +40,7 @@ import {
type GraphBox,
} from './tasks-graph-layout.ts'
import {
agentMeta, foldPreviews, TaskLine, WorkflowGlyph, workflowMeta,
agentIdentity, agentMeta, foldPreviews, TaskLine, WorkflowGlyph, workflowMeta,
} from './tasks-shared.tsx'
import { CardBar, CardTop, phaseClass, type CardKind } from './tasks-card.tsx'
import { doneActivityTitle, liveActivityLabel } from './process-labels.ts'
@@ -574,7 +574,7 @@ function renderAgentNode(
depth={node.depth}
{...(node.phase === undefined ? {} : { phase: node.phase })}
name={node.label}
meta={agentMeta(node)}
meta={agentIdentity(node)}
>
<TaskLine tasks={node.tasks} onOpenTask={onOpenTask} />
</CardTop>
+4 -2
View File
@@ -99,7 +99,9 @@ export function CardTop(props: {
)}
</span>
<span className={clsx(css.cardName, props.extraClass)} title={props.name}>{props.name}</span>
{props.meta !== undefined && <span className={css.cardMeta} title={props.meta}>{props.meta}</span>}
{props.meta !== undefined && props.meta !== '' && (
<span className={css.cardMeta} title={props.meta}>{props.meta}</span>
)}
{props.children}
</span>
)
@@ -125,8 +127,8 @@ export function CardBar(props: {
return (
<span
className={css.cardBar}
data-card-bar={state}
data-running={running === true ? 'true' : undefined}
data-state={state}
>
{props.children ?? (
<>
+18
View File
@@ -121,6 +121,24 @@ export function agentMeta(node: TasksAgentNode): string {
return mode === undefined ? state : `${mode} · ${state}`
}
/**
* The CARD's mono meta line: WHO the node is, never its state.
*
* The state belongs to the card's bottom bar (the state word sits next to the
* state dot there), so repeating it here would print "已完成" twice inside one
* 190px card — the densest complaint the readability pass had to fix. The
* accessible name keeps {@link agentMeta} (state included), because a screen
* reader never sees the bar's dot.
*/
export function agentIdentity(node: TasksAgentNode): string {
// The root's badge already reads 主代理: an identity line would repeat it.
if (node.parentId === undefined) return ''
if (node.team?.role === 'teammate' && node.team.model !== undefined && node.team.model !== '') {
return node.team.model
}
return modeLabel(node.mode) ?? ''
}
/** The mono meta line of a workflow run card: status · member tally. */
export function workflowMeta(node: TasksWorkflowNode): string {
const { run } = node
+4 -4
View File
@@ -597,7 +597,7 @@ describe('Tasks page: the shared task window', () => {
expect(child.getAttribute('data-depth')).toBe('1')
expect(child.textContent).toContain('子代理')
// Both segments are present: the identity block and the state bar.
const bar = child.querySelector('[data-state]') as HTMLElement
const bar = child.querySelector('[data-card-bar]') as HTMLElement
expect(bar).not.toBeNull()
expect(bar.textContent).toContain('已完成')
unmount()
@@ -620,13 +620,13 @@ describe('Tasks page: the shared task window', () => {
const foldToggle = container.querySelector('button[aria-label="展开已完成的节点"]') as HTMLButtonElement
await act(async () => { foldToggle.click() })
const child = container.querySelector('[data-graph-node="child-0"]') as HTMLElement
const bar = child.querySelector('[data-state]') as HTMLElement
const bar = child.querySelector('[data-card-bar]') as HTMLElement
expect(bar.getAttribute('data-running')).toBe('true')
// The merged activity wording comes from the plugin's own copy when the
// host's chat namespace is absent (this harness attaches no locale).
expect(bar.textContent).toBeTruthy()
const settled = container.querySelector('[data-graph-node="child-1"]') as HTMLElement
expect(settled.querySelector('[data-state]')?.getAttribute('data-running')).toBeNull()
expect(settled.querySelector('[data-card-bar]')?.getAttribute('data-running')).toBeNull()
unmount()
})
@@ -702,7 +702,7 @@ describe('Tasks page: the shared task window', () => {
const runNode = container.querySelector('[data-graph-node="run:run-1"]') as HTMLElement
expect(runNode).not.toBeNull()
expect(runNode.textContent).toContain('工作流')
expect(runNode.querySelector('[data-state]')).not.toBeNull()
expect(runNode.querySelector('[data-card-bar]')).not.toBeNull()
// Each member card carries ITS phase badge (the second grouping axis).
const first = container.querySelector('[data-graph-node="m1"]') as HTMLElement
const second = container.querySelector('[data-graph-node="m2"]') as HTMLElement