<p align="center">
  <img src="https://img.shields.io/badge/dsh-%E6%8F%92%E4%BB%B6-4D6BFE?style=flat-square" alt="dsh 插件">
  <img src="https://img.shields.io/badge/topic-dsh--plugin-4D6BFE?style=flat-square" alt="topic dsh-plugin">
  <img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license MIT">
  <img src="https://img.shields.io/badge/tests-188%20assertions%20passing-brightgreen?style=flat-square" alt="tests">
  <img src="https://img.shields.io/badge/runtime-web%20%7C%20desktop-4D6BFE?style=flat-square" alt="runtime">
</p>

<h1 align="center">dsh-live-progress</h1>

<p align="center"><b>输入框上方的真实任务进度条。</b><br>
它只显示宿主真的在做的事，剩下的宁可不显示。</p>

<p align="center"><a href="README.md">English</a> | <b>中文</b></p>

---

<p align="center"><img src="docs/images/progress-bar.svg" alt="输入框上方的进度条：待命时为空的轨道，运行中蓝色逐渐加深，100% 时为实心深蓝" width="760"></p>

## 它做什么

一条细长的圆角进度条，**通栏**贴在输入框正上方、与输入卡片同宽。任务开始之前它就在那里
（待命、空轨道、低调），任务开始后它一点点填满，完成后停在满格深蓝，直到下一个任务开始。

进度条下方是一行文字：正在做什么、现在是第几步、已经完成多少；**只有当分母真实存在时**，
才出现百分比。

| 状态 | 你会看到 |
| --- | --- |
| 待命 | 空轨道 + 待命中，没有百分比 |
| 运行中，没有真实分母 | 诚实的不确定态填充，第 3 步 · 已完成 2 个动作，**不编造百分比** |
| 运行中，已有计划 | 按计划一步步推进，真实百分比（40%），蓝色随进度加深 |
| 出错 | 任务已结束（有错误），细节行给出失败次数 |
| 完成 | 100%，实心深蓝，一直保留到下一次任务 |

## 百分比为什么可信

多数进度条会动，是因为有个定时器在推它。这一条只在宿主真的报告了事情发生时才会动：

- **计划就是分母。** 任务调用 `todo_write` 后，计划本身就是真实的步骤清单，`2/5` 就是真实的
  40%，而不是又一个估算。
- **工作流阶段同样是分母。** `workflow/phase` 报 `第 2/4 阶段` 就是 50%。
- **没有分母就没有百分比。** 没有计划也没有工作流时，进度条走不确定态动画，文字只报真实计数
  （步数、工具调用数、响应数），绝不写一个自己辩护不了的数字。
- **模型思考时不推进。** 不做插值、不做缓慢爬升、不卡在 99%。
- **100% 就是真的结束了。** 只有 `turn/end` 或 `agent/status idle` 真的到达才会满格。
- **估算会标注。** 同一类任务在会话里完整跑过两次之后，宿主才允许给出基于历史的估算，并且渲染成
  `~42%`、带 `data-estimate="true"`、无障碍播报为「约 42%」。

## 特性

- 通栏圆角长条，挂在官方 `conversation.input.dock` 插槽 &mdash; 不往别人的 DOM 里注入节点。
- 真实进度来自宿主事件（14 个事件名、每会话独立状态、SSE 推送）。
- 认得计划：读取会话真实的 `todo_write` 计划并按状态推进。
- 只有蓝色，且随进度加深：起步浅蓝，完成时深蓝。
- 同一时刻只有一个强调色 &mdash; 进度条里不出现绿、橙、红。
- `transform` 平滑动画，GPU 友好，尊重 `prefers-reduced-motion`。
- 无障碍：`role="progressbar"`、`aria-valuenow`，不确定态下 `aria-valuetext` 也会播报真实细节。
- 出错就闭嘴：浏览器侧任何异常都只会把自己降级成极简只读一行，并把错误回报给宿主
  （`/task-progress/client-report`），不会拖垮页面。
- 零构建：宿主侧是纯 ESM，浏览器侧是预构建 CommonJS bundle。
- 运行时零依赖。

## 环境要求

- 带 Web 对话界面的 DSH（`@deepseek-ai/dsh-client-ui-conversation`）。
- 能加载 `dsh.bundle` patch 层的 profile（web 或 desktop profile 均可）。

## 安装

### 从 npm 安装（推荐）

```sh
dsh plugin --profile desktop add dsh-live-progress
```

装完重启 DSH，然后刷新页面。

### 从本地目录安装

```sh
dsh plugin --profile desktop add /path/to/dsh-live-progress
```

### 手动安装（不用 CLI）

1. 把本目录拷到 `<profile>/node_modules/dsh-live-progress`。
2. 修改 `<profile>/package.json`：

   ```json
   {
     "dependencies": { "dsh-live-progress": "file:/path/to/dsh-live-progress" },
     "dsh": { "profile": { "bundles": ["...", "dsh-live-progress"] } }
   }
   ```

3. 重启 DSH，然后刷新页面。

卸载：删掉依赖项与 `dsh.profile.bundles` 里的条目，重启 DSH。

## 验证它还活着

宿主半边是普通 HTTP/SSE（按 `dsh-host-webserver` 的设计，非 `/api` 路由不设鉴权围栏）：

```sh
curl -s ${DSH_WEB}/task-progress/health   # 订阅情况 + 活跃流
curl -s "${DSH_WEB}/task-progress/state?session=<id>"
curl -sN "${DSH_WEB}/task-progress/stream?session=<id>"   # SSE 帧
curl -s ${DSH_WEB}/task-progress/debug    # 事件形状 + 自检 + 浏览器诊断
```

## 离线验证

四套断言脚本跑的就是浏览器与宿主真正加载的那些文件，不需要起 DSH：

```sh
node run-selftest.mjs      # 25 项 · 宿主自检 + 进度模型
node run-real-shapes.mjs   # 30 项 · 真机 session/event 形状
node run-client-test.mjs   # 58 项 · 假 React 下跑浏览器 bundle
node run-host-routes.mjs   # 75 项 · HTTP 路由、JSON 契约、SSE 帧
```

188 项断言全部通过。

## 工作原理

```text
DSH 宿主                                            浏览器
─────────────────────────────────────────────       ───────────────────────────────
session/event · agent/status · tools/*              conversation.input.dock 插槽
workflow/* · subagent/* · api-session/*                     │
        │                                                   │  fetch + EventSource
        ▼                                                   │
lib/collector.js  ── 真实事件 → 每会话一份状态               │
        │                                                   │
GET  /task-progress/state?session=<id>  ─── JSON ───────────┤
GET  /task-progress/stream?session=<id> ─── SSE  ───────────┘
POST /task-progress/client-report       ← 浏览器诊断上报
```

- `index.js` &mdash; 宿主半边：订阅事件、持有聚合器、提供路由、推 SSE 帧、内置自检。
- `lib/collector.js` &mdash; 进度模型：真实计数、计划跟踪、诚实规则、每会话状态、运行历史。
- `client.js` &mdash; 浏览器半边：注册插槽、经 SSE 读状态、渲染进度条、平滑动画、出错静默降级。
- `cordis.patch.yml` &mdash; 一行 bundle patch，把插件挂进 profile 的 Loader 树。

更多细节见 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)。

## 兼容性

- 在 Windows 的 DSH desktop profile、`dsh 0.2.0-rc.1` 上实测。
- 声明 `peerDependencies: { "@deepseek-ai/cordis": "^4.0.0" }`。插件不打包 SDK，跟随安装它的宿主，
  而不是钉死一份副本。

## 设计说明

- 填充只用一种色相，从浅蓝随进度加深到深蓝。不用第二种颜色表达任何含义；其余状态（出错、待命）
  都由文字和填充行为表达。
- 所有状态下这一行高度一致，输入框不会跳动。
- 完成态保持 100% 直到下一次任务开始 &mdash; 这是刻意的，它是上一个任务结束的凭据。
- 样式写在插件生命周期的 style 标签里（`data-plugin-css="dsh-live-progress:css"`），卸载时一并移除。

## 已知边界

- 百分比需要真实分母（计划或工作流阶段）。普通的工具调用回合只给计数、不给百分比，这是刻意的。
- 浏览器半边看不到宿主事件，它渲染的是宿主推给它的状态。
- 宿主侧代码改动需要重启 DSH（加载器缓存宿主模块）；浏览器侧改动刷新页面即可。

## 许可证

[MIT](LICENSE) &copy; 2026 chioplet
