# @deepseek-ai/dsh-client-ui-cordis

[English](README.md) | 中文

Cordis 动态插件的浏览器半：一个覆盖整个框架的面板，操作 host 持有的全部定义；以及一张只读的 `cordis_define` 卡片，记录某个会话定义了什么。

**面板做成全局是刻意的。** 模型发起的 `cordis_run` 会在 host 侧阻塞于一次 `cordis/request-run` 往返，而它的答案是**有人按下允许**；并且它可能指向一个当前没人在看的会话里的定义——审批入口若只存在于那个会话的对话流里，就会在它正阻塞模型的时候恰好不可达。所以应答面是一个 `shell.overlay` 条目（本包为 `ui-layout` 补的通用「框架级悬浮席位」，不是自建 React root）：角标计数 = 在跑数 + 待确认数，点开后列出每个定义及其运行控件。列表**同理**从不按会话过滤；当前会话的行置顶成组，其他会话的行仍在下方列出。行来自 host 的全局 `inventory` 调用，并在公告改变「有哪些定义」时**重读而非打补丁**——因为公告不携带名字与用途。而那次读取覆盖不到的开放请求**仍然有行**：直接用运行活动自带的会话、标题、用途与请求标识渲染——`cordis_define` 不广播任何东西，所以在上一次读取之后定义的包，对本页是未知的，而它的请求已经在阻塞模型；此时角标数着一个列表给不出的答案，就等于把模型困死。这样一个请求还会额外触发一次注册表读取，把运行控件补给该行。**归组只读每行上一个已解析好的字段**——有活动的 run 用它自己的会话，其余用注册表行的——而不是按阶段去问不同的 store；活动的**两条臂都带这个字段**，所以应答一个请求不会让它的行跳出人刚刚操作的那一组。组内，阻塞着模型的行排在最前。

**卡片是一份记录。** 它显示模型写下的 name 与 purpose、它写的源码，以及那个定义是否在跑——没有开关、没有审批，只有一句指向面板的指引。素材只取自冻结的 call/result 切片（标签取自 `argsRaw`，host 铸出的 id 取自结果的 `meta.id`），因此 replay 渲染出同一张卡；而有一个读数天生属于卡片：本会话日志里成功的 `cordis_undefine` 是持久且终态的，它**压过 wire**——在 wire 上，retract 公告与一次普通 stop 长得一模一样。

**一行同时读两个互相独立的事实。** 「host 在跑什么」来自 inventory，「**本页**装载了什么」来自 runner 的 live set。每次刷新这两者必然分叉：host 照旧跑着全部，而全新页面什么都没装。host 在跑且本页已装的行，给出全局 stop；host 在跑但本页**没装**的行，先给「装回本页」、再给那个 stop——**两个独立控件**，因为「把浏览器半装到本页」与「为所有页面停掉这个定义」是两件不同的事。**只有 host 半的定义完全不参与这一套**：它没有浏览器半可装，「本页没装」就是它永远的样子，所以它的行如实读作「运行中」，并且只给 stop。未运行时它给出的是一个如实标注的「运行」——本页什么都不会装进来，控件就不这么说——而 run 请求把 `hasClientHalf` 一路带给 runner，正是这一位让它只起 host 半、不去取浏览器半。把这两个事实塌缩成 host 的 `running` 一个，会让刷新后的页面只剩全局 stop，于是文档写明的刷新恢复路径（拉 inventory、把定义装回本页）**没有任何控件可走**。

**装载成功之后的崩溃，同样属于这一行。** 浏览器半可以装载成功、让 `cordis_run` 答 ok，之后才在 React 渲染它时抛出异常——像误用 `setInterval` 这类错误的教学文案只会落到浏览器控制台，而模型和看着面板的人都不读那里。所以该行会把 runner 记下的、属于本页的最后一次渲染失败就地显示在行内，与装载失败共用同一个位置：一个是「它从来没装上」，另一个是「它装上了、然后抛了」，而一行可以如实地把两者都显示出来。这行文案会点名崩溃的那个座位，并回答随之而来的那个问题——他们的东西还有没有留在屏幕上：遮蔽式座位会让崩溃的条目退场，出厂 UI 随之回来；而 chain 条目会保住自己的位置，它的 UI 可能只是不完整。

两个面都不把运行态放进组件 state——define 调用结算时卡片会在聊天流里换位置并重挂。事实活在「谁能关闭它、就归谁」的观察量里：浏览器侧 runner 拥有开放请求、编排结果（是它发出 resolve，包括别的页面先应答的情形）与本页的 live set及其渲染失败——它是唯一在监视 `slots.onEntryError` 的一方，也是唯一能把崩溃的条目映射回注册它的那个包的一方——而本包拥有自己读来的清单与折叠过的公告。

`/client` 导出面是插件体（`apply`/`inject`）加注入面、运行态、端口与事件载荷类型。

## 模型体验

间接影响，经由这两个面驱动的 run 与 stop 动词——run 走浏览器侧 runner 的编排，stop 走 `dynamicCordisRunner.stop`，与模型的 `cordis_run` / `cordis_stop` 工具是同一批 host 动词。因此正在运行的定义随后贡献了什么是 runner 的效果，而本包不产生任何模型可见输入：它只渲染已落日志的 call/result 切片与一次 host 清单读取，不加 prompt 内容、不写会话事件，并刻意不为「有人批准 / 拒绝 / 运行 / 停止」留下会话日志痕迹。

#### KV 缓存影响

无：没有任何 prompt 输入源自这里，应答一次 run 请求既不延长也不改写历史尾部。

## 已知限制与欠账

- **已展开的面板看不到「不广播任何东西」的注册表变化** —— `cordis_define`，以及对一个并未在运行的定义执行 undefine，都会改变注册表却不发出下发公告；因此跨过这类变化时，已展开的面板会保留旧行，直到收起再展开（展开即重读）。run 请求是例外，因为它阻塞模型：它既自己渲染出行，也触发一次读取。对一个 host 已不持有的行动手仍然是诚实的：调用会答 `definition-missing`，该行随即转入终态。「展开期间轮询」评估过，代价不值，已否决。
- **只有请求、没有清单的行可应答但不可操作** —— 它只提供允许/拒绝，因为 run/stop 开关需要那次读取尚未送达的注册表行。归组与文案不受影响（ask 自带会话、标题与用途）。
- **若编排跑在读取之前，该行会消失一次读取的时长** —— 活动的 orchestrating 臂带会话但**刻意不带标题**：用户自发的 run 根本不存在可取标题的 ask，而拿 id 当名字比短暂缺行更难看。所以一个已批准、但注册表读取尚未落地的请求，在读取落地前没有行。实践中这次读取在请求**到达时**就已触发，所以等到有人应答时它几乎总已落地。
- **渲染失败是本页自己的读数，而且它来得太晚、赶不上 run 的回执** —— 面板显示的是 runner 在**本页**看到的最后一次崩溃，所以一个在本标签页渲染正常的包，即使正在另一个标签页里崩溃，这里也什么都不显示。它同样不可能出现在 `cordis_run` 的答复里：渲染发生在 run 结算之后，所以模型只能靠主动去问（`cordis_inspect what:"temporary"`）才知道，而不是从它已经发出的那次调用里得知。
- **某一页的装载失败对其他页不可见** —— host 以首个装载回报结算一次 dispatch，更晚的回报只记录不动作；因此在另一页确认之后浏览器半才失败的页面，仍会读作运行中。那一页能在自己的行上看到原因（runner 会报），其他页看不到。
- **任何页面都可以应答任何请求** —— 审批按设计是框架级的，所以某个标签页里的人可以批准模型为另一个标签页正在看的会话所发起的 run。首个应答生效、其余收敛；收窄「谁有权应答」延后。
- **call head 掉出事件窗的卡片会丢掉标签** —— 卡片的 name 与 purpose 取自调用参数，会话长到把它们截断时，卡片只能以自己的 call id 自称。面板不受影响：host 清单携带标签。
- **窗口截断会降级「已卸载」这个读数** —— 卡片凭本会话日志里成功的 `cordis_undefine` 判定已卸载；会话长到把那条结果挤出窗口时，该定义只会显示为未运行。
