# C 批设计:客户端面(团队面板 + 形态可视化)+ 🔵 登记 4 条 / 本批执行 3 条(① 已在上一批修完)(2026-09-27) > **一句话**:把**已经能用工具读到**的东西,做成**在界面上看得见**的一屏;外加评审遗留的 🔵 —— **登记 4 条 / 本批执行 3 条**(① 默认值脚枪已在上一批修完)。 --- ## §1 现状(事实,不是愿望) - **客户端半边** `lib/client.js`(2,455 行)已注册:`conversation.chat.node`(keyed `context`,priority -100)· **头部动作条** · **侧栏「会话工具」入口**。 inject:`dsh-client-locale` · `dsh-client-ui-conversation` · `dsh-client-ui-workspace` · `dsh-api-session-controller` · `dsh-api-workspace-controller`。 - **宿主半边**已有全部数据面:`team_link_status`(六段 + ⑦形态)· `roster get`(含**落点行**)· `team_read`(台账 + 收件视图 + 派生回执)· `list_sessions`(读窗 / 点名 / 分页)。 - **缺口**:以上全是**模型可见**的工具输出 —— **人**要在对话里读它们,**没有一块给人看的界面**。 - **形态缺口**:多会话档 vs agent-team 档在界面上**不可辨**(宿主投影里有 Lead 与成员,插件只在模型面输出)。 --- ## §2 需求与判据(R) - **R1 一屏可读**:团队状态(落点 + 团队与角色 + 换届 pending + 看门狗 + 台账尾)在客户端**一屏**看完,不必问模型。 - **R2 形态可辨**:agent-team 档的会话(Lead / 成员)**一眼可辨**,且**不冒充**多会话档的语义。 - **R3 只读优先**:面板默认**只读**;有副作用的动作(换届 / 建队 / 改纪律 / 改落点)**不在面板里做**(双门与确认框是对话面的事)。 - **R4 诚实降级**:任何一段取不到(宿主 API 不可用 / 投影读不到 / 文件不存在)⇒ 该段**如实说缺什么**,不静默留空。 - **R5 落点行必须在**:面板顶部保留宿主那三态之一(`存储:…`)—— 它是「状态此刻存在哪」的**唯一**用户可见出口。 --- ## §3 设计 ### 3.0 取数通道(跨半边;评审 🔴 —— 本批最难的架构决定) **事实**:面板在**浏览器半边**,而六段数据全在**宿主半边**;本仓 `README.md` §二 明确记着「本插件的浏览器半边**没有任何跨半边取数通道**」 —— 这正是当初「侧栏状态点只做两态」的原因。**通道必须先设计出来,否则 W1–W7 字面不可实现。** **设计:复用本仓已有的「宿主 HTTP 路由」形态**(导出路由 `/team-link/export` 就是这条路 —— 客户端头部 ⬇ 按钮**就是**它在浏览器里的调用方 ⇒ **通道确实存在**,此前只用于导出): 1. **新增只读路由** `GET /team-link/panel`,与导出路由**同一道栅栏**:`webServer` + 连接层同源策略 + 无 token ⇒ **401** + `Host` 不符 ⇒ **403** + **仅 GET** + 跨站标记(`Origin` / `Sec-Fetch-Site`)一律拒。 2. **响应形态**(JSON):`{ placementLine, teams[], pending[], watchdogs[], tasksTail[], sessions[], form }`,其中: - `placementLine` 是**宿主渲染好的整串**(三态之一),客户端**只显示、不重算** ⇒ 直接满足 W2「与宿主同源」(不必两侧各写一份格式化逻辑); - 每段都带降级字段(`present: false` / `unavailable: { reason }`)作为 **R4 / W3 的载体**; - `form` = 形态段(徽标所需的三态 + 档位结论句),同样由宿主给字节。 3. **零写**:面板读路径走**状态卡那一套**(宿主 read 内部)+**纯派生扫描**,**绝不**调用 `team_read` 的惰性清扫 —— 那条路会**写** (清 pending、删过期 provisional),会让 W5 变红。⇒ 新增判据 **W9**。 4. **预算与刷新**:**一次渲染 = 一次路由调用 = 与工具面同一个「≤12 次面读」的不变量**(`README.md` 的预算锁);**只在打开面板 / 手动刷新时拉**,**不轮询**(无定时器)。 5. **降级(R4)**:路由缺席 / 被栅栏拒 / 宿主 API 不可用 ⇒ 面板**照常打开**并显示 `⚠ 面板数据不可用:<原因>`(不静默留空、不假装「零团队」)。 **与 §3.4 的关系**:这就是「与工具面同一批宿主 API」的落地方式 —— **同源靠「宿主渲染、客户端只显示」保证**,而不是靠两侧各写一份逻辑。 **§3.0.1 实施形态(2026-09-27 落码时回填;本节只记**实际形状**,不改上面的裁定)** 响应是一个 JSON 对象(HTTP 200;参数非法是 400 + `{ ok:false, error }`): { ok: true, generatedAt, placementLine, form, teams, pending, watchdogs, tasksTail, sessions } - **每一段**都是一个对象 `{ present: boolean, lines: string[], unavailable?: { reason } }`(文档上面的 `teams[]` 是简写:一段要能同时带「有内容」与「present:false + 为什么」,一个裸数组放不下后者),`lines` 是**宿主渲染好的整串**; - `placementLine` = `policy.placementLine()`(与 roster / status 两个读面**同一个函数**,因此逐字同源); - `form` = `{ present, lines, conclusion, badges }`:`conclusion` 复用 `TEAM_MODE_CONCLUSION`(「单会话兜底档」那句),`badges` 是「会话 id → 三态徽标」的投影(C2); - `tasksTail.derived` 与 `team_link_team_read` 的派生读数**共用** `tasksDerivedLines()`(同一扫描窗、同一 `renderTaskView`); - `sessions` 额外带 `rows`(每行 `{ id, badge, lines }`,未读行的那一行就是「未读」句)、`tail`(`listReadTail`:未读行数 + `offset`)、`page`(`{ offset, size, total, nextOffset }`,翻页就是同一路由带上 `offset`)。 ### 3.1 载体:**复用既有侧栏入口**(不新造 Modal) 面板作为**侧栏「会话工具」入口**的一个分区(该入口 0.3.9 批 3 已落地)。 理由:新 Modal 要自己管焦点 / ESC / 遮罩,而宿主 Modal 有**独占交互契约**(`docs/hardening-and-recovery-design-2026-09-21.md` §4.3.7:官方 Modal 拥有 Escape,一个关闭路径);侧栏入口**已经在**,且不与对话抢焦点。 **口径定案(评审 🟡#4)**:入口 = **侧栏「会话工具」分区**(原 §8.1 的问题在此定案)。该入口的**单一事实源**是 `docs/hardening-and-recovery-design-2026-09-21.md` §4.3 + `README.md` §二 的行为表 ⇒ **本批同批修订这两处**(不制造「第二个描述者」)。 ### 3.2 面板分区(六段,与宿主状态卡**同构**) 1. **落点行**(R5)—— 三态逐字与宿主**同源**(同一函数或同一常量,不许各写一份)。 2. **团队与角色** —— 含活性诊断;`seated-dead` / `vacant` 用宿主**同款措辞**。 3. **换届 pending** —— token **掩码**;**只读**。 4. **看门狗** —— 谁盯谁 / 阈值 / 到期。 5. **台账尾** —— `tasks.md` 末 N 行 + 派生读数(**最后主张 / 未消解存疑**)。 6. **会话面 / 读窗** —— §12 窗口(`docs/observability-batch-design-2026-09-26.md` §12):未读行**显示为未读**,带**只读**的「读更多」翻页。 ### 3.3 形态可视化(R2) - **数据源**:宿主 `agentTeams` 投影(宿主半边已可读 ✓,读面形状已核过)。 - **呈现**:在同一面板的会话区给每个会话加**档位徽标** —— `agent-team·Lead` / `agent-team·成员` / `多会话`;并在「团队与角色」段顶部给一句**当前档位结论**(措辞复用 `docs/team-mode-batch-design-2026-09-26.md` §4.3 的「**单会话兜底档**」)。 - **不做**:**不改宿主的会话列表 UI**(那是宿主的面,插件不该改)—— 只在插件自己的面里标注 + 一行说明。 ### 3.4 与模型面的关系(红线) 面板**只读**且**不新增数据源**:它读的与工具面是**同一批**宿主 API。两边不一致就是 bug —— 由断言锁住(W6)。 --- ## §4 判据(C 批,实施前必须逐条先红) | 号 | 断言 | |---|---| | **W1** | 面板注册在**既有侧栏入口**上(不新建 Modal);入口缺席时**不报错**(复用 `guardedSlot` 同款守卫) | | **W2** | 落点行在面板顶部,三态**与宿主同源**(同一函数/常量;不是各写一份字符串) | | **W3** | 六段齐全;每段数据取不到时**如实说缺什么**(R4 的正例 + 反例各一条) | | **W4** | 形态徽标三态正确(Lead / 成员 / 多会话);**投影读不到时不冒充**(如实说读不到) | | **W5** | 面板**零副作用**:渲染一次不改任何宿主状态(渲染前后 settings / `policy.json` 的 mtime 与字节不变) | | **W6** | 台账尾与派生读数与 `team_read` **同源同形**(「最后主张 / 未消解存疑」措辞逐字一致) | | **W7** | 读窗未读行**显示为未读**(不得含伪造的 verdict);翻页**只读** | | **W8** | 客户端套件现有 **269 条全绿**;新增判据不降低既有覆盖(落地后实测 **299 条全绿** —— host `1264 → 1299` / client `269 → 299`;2026-09-27 分歧审计修复轮又 **+1** ⇒ host **1300** / client **299**;2026-09-27 **代码评审修复轮**又 host **+6** / client **+6** ⇒ host **1306** / client **305**,三处读数见 README §十 与 `docs/verification-log.md`,本轮见 §9) | | **W9** | **面板读路径零写**(评审 🟡#2):**绝不**走 `team_read` 的惰性清扫;夹具带「过期 pending / 过期 provisional」时,渲染前后 `policy.json` **字节与 mtime 均不变** | --- ## §5 🔵 收尾:登记 4 条 / 本批执行 3 条(① 已在上一批修完;登记于 `docs/policy-persistence-design-2026-09-27.md` §8) 照 §8 执行:② 快路径归档留痕**条件化**(现在无条件承诺「随后文件会被改写」)· ③ **「快路径 + fold failed」补判据**(现在只覆盖晚挂路径)· ④ `setup()` 形参**排版**(① 默认值脚枪**已修**)。 **判据枚举(评审 🟡#5)**:**C3-a** 快路径归档留痕**条件化** —— 未折叠时那行不再承诺「随后文件会被改写」(红相:现文案在「快路径 + fold failed」下与实况不符)· **C3-b** 补「**快路径 + fold failed**」夹具 —— 文件逐字节不变 + `foldedAt` 不重盖 + 归档数目与留痕一致 · **C3-c** `setup()` 形参排版(纯排版,随两套件绿)。 --- ## §6 分期(给实施者的 stages) 1. **C1 面板骨架 + 落点行 + 六段只读**(W1–W3, W5–W7) 2. **C2 形态可视化**(W4) 3. **C3 🔵 登记 4 条 / 本批执行 3 条(① 已在上一批修完)+ 文档/账本/读数同批** --- ## §7 明确不做 - ✗ **不在面板里做有副作用的动作**(换届 / 建队 / 改纪律 / 改落点)—— 那是对话面双门的事。 - ✗ **不改宿主会话列表 UI**。 - ✗ **不新增数据源**(面板与工具面同源)。 - ✗ **不为面板新造 Modal**(宿主 Modal 有独占交互契约)。 --- ## §7.1 可分割性(评审 🔵#7) 若 §3.0 的通道设计在实施中变大,**C3(🔵 登记 4 条 / 本批执行 3 条)可单独成一批**先发 —— C1/C2 才是本批的架构风险所在。 --- ## §8 评审时一并定的两处交互口径(**两问均已回填**) 1. 面板入口 = **侧栏「会话工具」里加分区** ✅ **已定案**(见 §3.1;同批修订 `hardening §4.3` + README §二 行为表)。 2. 台账尾默认显示**几条**(宿主状态卡默认 5、上限 20)—— 面板是否同值? ✅ **已回填(2026-09-27 分歧审计修复轮 🔵#4):面板 = 状态卡的 5(同值)。** 实现取 `rows.slice(-STATUS_TASKS_TAIL_DEFAULT)`(as-of 本轮 `lib/index.js` 面板台账段的 `:11313`,常量在同档 `:140` = **5**)。口径说清楚:状态卡的 5 是 `tasksTail` 的**默认值**(参数上限 20 只对状态卡那条参数生效),而面板路由**不收** `tasksTail`(请求参数只有 `session` 与 `offset`,见同档 `:11519–11531`)⇒ 面板恒取默认值 5,**与状态卡默认值同值**。 --- ## §8.1 后续项登记(本批**不做**,只登记,待父侧裁决) 1. **`tasksTail` 的两种「不可读」只降级到 lines 文本**(分歧审计观察 ③,父侧裁定「顺带登记」):无 workspace / 文件读失败时,那一段仍是 `present: true` + `lines` 里一行可读原因,**没有断言覆盖**,客户端也拿不到结构化降级信号。若将来给客户端一个结构化降级信号(供面板单独渲染「这一段取不到」),这两支就应改 `present: false` + `unavailable.reason`(与其余五段同形)。**本批不动**。 2. **收敛残渣:`lib/index.js` 列表工具里的一个死局部**(本轮核实的新发现,随 🔵#2 的六处收敛清单一起查出):`const running = agent?.status === "running";` 是收敛前那行内联状态词表达式的遗留 —— 收敛后同一循环体内**再无引用**(实测:该循环体 `:10822–10850` 里 `running` 只出现 1 次 —— as-of 本轮 `:10826`,就是声明本身)。它是**纯死代码**(不影响任何输出),删除是一行的事,但**不在本批的四项裁定内**,故只登记、**本批不动**。 --- ## §9 代码评审修复轮(2026-09-27):2 🟡 + 4 🔵 的逐条闭环 > 评审结论 **PASS、零 🔴**(5 轮收敛上限内)。本轮**只闭环这 6 条**,不夹带。改动面 = `lib/index.js` / `lib/client.js` / `host-half.test.mjs` / `client-half.test.mjs` + 四处文档(计数与本节)。 ### 9.1 六条闭环 | # | 条目 | 落点(as-of 本轮) | | --- | --- | --- | | 🟡#1 | 面板 13 个 `.dshsl-pn-*` 类**没有一条 CSS 规则**(宿主渲染的多空格缩进被 `
` 折叠 ⇒ 削弱 R1) | `lib/client.js` 样式数组末尾补 13 个类的规则(`.dshsl-pn-line` / `.dshsl-pn-placement` 带 `white-space:pre-wrap`;徽标与段落间距照 `.dshsl-relay*`)+ 源码级对照判据(`className:` 里每个 `dshsl-pn` 类都要按选择器找得到规则;两侧集合非空) |
| 🟡#2 | `nextOffset` 取「第一个未读行」⇒ 第二页起恒为 0,翻页回卷、第 25 行起不可达 | `buildPanelPayload` 改为 `nextOffset = windowEnd`(本页 offset + 本页读窗行数),越过末行置 `null` 隐藏按钮;判据含**沿按钮链**逐页前进、末页消失、两页夹具的末页也隐藏 |
| 🔵#3 | `PANEL_LV_WINDOW_NOTE` 在分页路径上自称「前 12 行」 | 新增 `panelWindowNote(offset)`:第一页字节不变,分页页改说「本面板本次只读了 12 行,从第 k+1 行起」 |
| 🔵#4 | 多团队时 `tasksTail.derived` 不带团队归属 | 新增 `teamDerivedLines(teamName, derivedLines)`:块首 `-