# dsh-composer-live

**为 DeepSeek Harness（DSH）Web 输入框带来 Markdown 实时渲染与输入体验增强。**

[English](./README.md) · [简体中文](./README.zh-CN.md)

![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)
![DSH](https://img.shields.io/badge/DSH-%3E%3D%200.1.5--rc.2-4c6ef5)
![Tests](https://img.shields.io/badge/unit%20tests-188%20passing-2ea44f)

dsh-composer-live 把 DSH Web 的输入框变成 Open WebUI 风格的实时编辑器：你打 Markdown，它边打边渲染——加粗、斜体、带语法高亮的代码块、列表、引用、表格——而草稿本身始终是纯 Markdown 源文本，发送出去的就是你输入的原文。

它是一个**纯浏览器端插件**：零运行时依赖、不注入任何官方服务、不改官方源码。它在官方 0.1.5 Lexical 输入框之上叠加增强，不重复 DSH 已原生提供的功能。

> 找 DSH **0.1.x**（textarea 架构输入框）的版本？那是 [`dsh-composer-md`](#faq)——本插件的前身，已停止维护。

## 截图

### Markdown 实时渲染

![输入框实时渲染效果](docs/screenshots/overview.png)

### 代码块

![代码块语法高亮与语言标签](docs/screenshots/code-block.png)

### 选区浮动格式条

![选区上方浮现的浮动格式条](docs/screenshots/selection-bar.png)

## 功能

### Markdown 实时渲染

- 行内样式边打边生效：`**加粗**`、`*斜体*`、`` `行内代码` ``（品牌蓝胶囊）、`~~删除线~~`、`[链接](url)`（白名单协议）、`![图片](url)` 胶囊。
- **光标字符级精确对齐**。标记字符（`**`、`` ` ``、`~~`、`[]()`）淡化显示但保留精确宽度，粗体/标题用 paint-only 伪粗——渲染行与编辑器透明文字逐字符完全对齐，光标落在行内任何位置（包括格式区间内部）都精确贴齐，打字零漂移。
- 任务列表项：`- [ ]` / `- [x]` 渲染成勾选胶囊；已完成项内容划线。
- 草稿始终是纯 Markdown 源文本。渲染纯视觉——发送的消息就是你输入的源文本。

### 代码块

- 输入 ``` 时按普通文本显示，直到围栏**闭合**——不会出现打一半就被渲染的残块。
- **未闭合围栏行上按 Shift+Enter** 自动补全封闭块（块内空行 + 闭合围栏 + 块后空行），光标落回块内，直接开写。
- 已闭合块最后一个内容行按 ↓ 直接跳出，落到块后的普通行。
- 零依赖正则语法高亮：js/ts/py/sh/json/yml/css/html，未标语言按内容自动推断——中文长文不会误判成代码。
- 围栏行右上角语言标签；GitHub-dark / GitHub-light 两套块主题；围栏字符淡化。
- 等宽：草稿含已闭合代码块期间，整个输入框（编辑器 + 渲染层）同步切换等宽字体栈（与官方代码字体同源）。中文字符刻意保持界面字体，两侧换行点永不发散。

### 列表、引用、表格

- 列表行尾按 **Enter** 自动续项：无序列表保持符号（`-` `*` `+`）、有序列表递增编号（`1.` → `2.`、`3)` → `4)`）、缩进保留。**空列表项上按 Enter 退出列表**——两次 Shift+Enter 结束一个列表。
- `> ` 引用行同样续项（`>text` 无空格也算）；空引用项上按 Enter 退出。
- **Tab / Shift+Tab** 缩进 / 反缩进两空格——单行或整块选区。光标在列表标记或行首空白处按 Tab 即嵌套子列表。Tab 全境拦截：焦点永远不会被切出输入框。
- 表格首行（`|列A|列B|`，至少 2 列）行尾按 **Enter** 自动补 `|---|---|` 分隔行加一个空行，光标落空行。

### 工具栏与快捷键

- **常驻顶部工具栏**：加粗 / 斜体 / 行内代码 / 链接 / 列表 / 代码块 / 展开。选中文字时包裹、未选中插入空标记；光标落在 `**…**` 内时加粗键自动点亮。工具栏钉在滚动区顶部，永不与正文或图片附件重叠。
- **选区浮动格式条**（Open WebUI 形态）：选中一段文字，加粗/斜体/代码/链接四键浮现在选区上方。
- **Ctrl+B / Ctrl+I / Ctrl+E** 快捷加粗 / 斜体 / 行内代码——同时拦掉浏览器对 contenteditable 的原生加粗（那会悄悄往草稿里插入真正的 `<b>` 标签）。

### 粘贴处理

- 大段粘贴（超过 8 行或 4KB）且内容像代码/日志时，**自动用代码围栏包起来**——单一撤销单元，配 toast 轻提示。中文长文不误判；光标已在代码块内或内容本身含围栏时不包。

### Esc 分发、主题、兼容

- **Esc** 按语境做对的事：官方候选菜单开着 → 归官方处理；展开态 → 收起；AI 生成中 → 停止生成。
- 一键**展开 70vh** 长文写作模式。
- 明暗主题自适应；输入法安全（拼音组字不闪烁）；渲染按 rAF 合帧、增量进行。
- **@ 引用胶囊**保持官方原样、可编辑。含胶囊的段落优雅降级为官方显示（胶囊宽度无法复刻，零错位优先）。热词文本装饰不受影响。
- 顺带修复官方在图片附件在场时的两个定位问题：候选菜单悬空浮出输入框、工具栏盖住缩略图。

### 不与 DSH 0.1.5 重复

官方输入框已原生提供、本插件刻意不重做的：`/` 命令菜单、`@` 文件引用、粘贴图片/文件转附件、Enter 发送 / Shift+Enter 换行、输入法保护、草稿持久化。

## 速查表

| 按键 | 语境 | 行为 |
| --- | --- | --- |
| Enter | 普通行 | 发送（官方） |
| Shift+Enter | 普通行 | 换行（官方） |
| Enter / Shift+Enter | 列表 / 引用行尾 | 续标记（有序递增编号） |
| Enter / Shift+Enter | 空列表 / 空引用项 | 删标记，退出列表 / 引用 |
| Enter / Shift+Enter | 未闭合围栏行 | 封闭代码块；光标落块内 |
| Enter / Shift+Enter | 表格首行（≥ 2 列） | 补分隔行 + 空行 |
| Tab | 代码块内 | 光标处插 2 空格 |
| Tab | 列表/引用标记头或行首空白 | 行首缩进（嵌套列表） |
| Tab | 其他位置 | 光标处插 2 空格 |
| Tab / Shift+Tab | 多行选区 | 整块缩进 / 反缩进 |
| Shift+Tab | 任意行 | 行首反缩进最多 2 空格（无缩进时只吞按键） |
| Ctrl+B / Ctrl+I / Ctrl+E | 任意 | 包裹选中加粗 / 斜体 / 代码，或插入空标记 |
| ↓ | 已闭合块最后一个内容行 | 跳到块后 |
| Esc | 菜单开着→官方 · 展开态→收起 · 生成中→停止 | 按语境分发 |

## 环境要求

- **DSH ≥ 0.1.5-rc.2**，`web` profile——本插件面向 0.1.5 的 Lexical contenteditable 输入框。
- **Chromium 系浏览器**（Chrome、Edge 等）：透明化技术依赖 `-webkit-text-fill-color`。
- 与操作系统无关——纯 DOM/CSS，无原生依赖。

## 安装

```bash
# 从 npm 安装（推荐）
dsh plugin --profile web add dsh-composer-live

# 或从本地目录安装（file: 安装为物理拷贝）
dsh plugin --profile web add "file:/path/to/dsh-composer-live"
```

**装完必须重启 web 实例。** 插件按启动时快照加载，光刷新页面会新旧 bundle 并存。

跑的是隔离实例？用 `DSH_HOME` 指向它，插件就装进那个实例（否则装进默认的 `~/.dsh`）：

```bash
DSH_HOME=/path/to/your/.dsh dsh plugin --profile web add dsh-composer-live
```

## 使用

装上即全量生效——安装、重启、直接在输入框里打 Markdown。

典型工作流：

- **代码块**：输入 ```` ```js ````，按 Shift+Enter——封闭块出现、光标在块内。写代码实时高亮；Tab 缩进两空格；最后一个内容行按 ↓ 跳出块后。
- **列表**：输入 `- 项目`，Shift+Enter 逐项续写；空项上再按一次 Shift+Enter 结束列表。Tab 嵌套，Shift+Tab 升回。
- **长文写作**：点展开键——输入框拉到 70vh；Esc 收起。
- **贴日志**：一大段代码或日志自动变成代码块；不想要就 Ctrl+Z。

## 工作原理

官方输入框是一个 Lexical contenteditable 编辑器。本插件用 `-webkit-text-fill-color: transparent` 把它的文字变透明（`color` 属性保留，所以光标和官方装饰照常工作），再绘制自己的渲染层——每个渲染字符都定位在编辑器文字的真实坐标上（逐字符测量 Range rect）。

渲染层从不自己排版，所以对齐类问题（漂移、抖动、错位）在架构上不存在：实测各场景偏差 ≤ 0.02px，连续打字逐帧偏差 0.00。

插件不注入任何官方服务。它直接读编辑器 DOM 推导「投影文本」（胶囊 → U+FFFC，`<br>`/段落边界 → 换行），对官方 client API 变化免疫；DOM 锚定用稳定的 `data-` 属性而非哈希过的 CSS Modules 类名。改动草稿的操作（列表续项、围栏补全、缩进……）走 Lexical 原生通道——保撤销栈的合成 `beforeinput` 事件——绝不直接改 DOM。

完整深读见 [docs/architecture.md](./docs/architecture.md)。

## 已知限制

- 含 **@ 胶囊**的段落按官方纯文本显示（胶囊宽度无法复刻，零错位优先）。热词文本装饰不受影响。
- 块级视觉（标题、引用、列表、表格标记）只上色/加粗/加背景——不改字号、行高、缩进；源文布局保留，行度量永不变化。
- 只要草稿含已闭合代码块，整个输入框切等宽（不是逐行——字面 `\n` 单文本节点的草稿形态让逐行换字体物理不可行）。
- 围栏补全的光标落位等 80ms（Lexical 异步提交）；极慢机器上光标可能偶发落位偏后。
- ↓ 跳出仅在**已闭合**块最后一个内容行触发；未闭合围栏走官方默认行为。
- 「停止生成」按 aria-label 双语枚举匹配；官方改文案时请直接点按钮。
- 仅 `web` profile；仅 Chromium 系浏览器。

## 开发

```bash
node test-live.cjs    # 188 项单测（纯函数，无需浏览器）
node e2e/run-e2e.mjs  # 62 场景 × 10 不变量的 E2E 套件（对着运行中的实例测）
```

E2E runner 要求被测实例已部署当前源码，路径全部由环境变量推导：`DSH_CL_PORT`（默认 8124）与 `DSH_CL_DSH_HOME`（默认取 `DSH_HOME`，再默认 `~/.dsh`）定位部署副本做版本对照，鉴权 cookie 自动获取。完整变量表见 [e2e/README.md](./e2e/README.md)。

套件复刻六种真实输入通道（字面 `\n` / 字面 `\r\n` / 逐行 br / insertParagraph / 合成 paste / 刷新恢复多段落），用一份独立投影与插件自身的投影逐字符交叉验证。

版本纪律：每次改动 bump `package.json` 并在 [CHANGELOG.md](./CHANGELOG.md) 记一笔。

## FAQ

**为什么只支持 Chromium 系？**
透明化手法（`-webkit-text-fill-color`）和若干光标行为是 Chromium 特有的。Firefox/Safari 需要另一套方案。

**会改变我发送的内容吗？**
不会。草稿始终是纯 Markdown 源文本，渲染纯视觉。

**逐字符测量不慢吗？**
按渲染帧合批，合并成少量绝对定位 span。渲染层 `pointer-events: none` 且脱离文档流，不引起编辑器重排。实测逐击键偏差为 0，无可感知输入延迟。

**DSH 升级会弄坏它吗？**
零服务注入 + `data-` 属性锚定让它对日常升级（类名哈希变化、client API 重构）免疫。当官方输入框**架构本身**变化时——比如 0.1.1 → 0.1.5 从 textarea 换成 Lexical——插件需要重写。本插件正是那次重写的产物：`dsh-composer-md` 是它的 0.1.x 前身，现已停止维护。

## 许可证

[MIT](./LICENSE)
