# 更新日志

本文件记录 `dsh-client-ui-notes` 每个版本的变化。版本号遵循语义化版本。

插件装进 DSH profile 之后，可以在**侧栏「插件」页**或**插件市场**里升级；
0.1.7 起插件详情页自己的「更新」按钮也能直接升级。

## [0.1.10] - 2026-10-19

### 新增

- **文件树刷新**：外部编辑器改了文件名、增删了文件之后，树里原本会一直留着旧条目。
  - 切回 DSH 窗口（或从别的标签页切回来）时**自动重新读取**整棵树；
  - 树头新增**刷新按钮**，随时手动触发；
  - 两种方式都只重列**已展开的目录**，展开状态、滚动位置、置顶与标签都不受影响。
- **文件名搜索**：置顶区下方新增搜索框，输入即列出当前目录下文件名匹配的文件，
  上下键移动、回车或点击选中 —— 会同时展开树中所在目录、滚过去高亮，并在右侧打开该文件。
  首次聚焦时整棵遍历一次并缓存，之后输入只在本地过滤。
- **技能浏览（只读）**：文件树标题栏新增「技能」按钮，没选文件夹时的引导页也有「看技能」，
  点开列出当前可用的全部 skill，点一条就在阅读器里看它的 SKILL.md 全文。
  - 列表来自**磁盘扫描 + 官方注册表补充**：按 DSH 约定扫 `<项目根>/.dsh/skills`、
    `<项目根>/.agents/skills`、`~/.dsh/skills`、`~/.agents/skills`（不递归、跳过 `.system`），
    再与 `ctx.skills` 的结果按名字合并。
    为什么不能只问注册表：Web profile 把本地技能发现挂在**每个 agent preset 的层**上，
    宿主平面从 `ctx.skills` 只能看到内置技能（DSH 的 `dsh-web-app` 补丁里
    `skill-filesystem` 明确写着 `disabled: true`，由 preset 接管）；
  - 支持按名称/说明筛选、显示每条技能自带的图标（`assets/icon.png` 等）；
  - **插件内置的技能也能看**（如 Office 三件套）：这类技能磁盘上没有 `SKILL.md`，
    正文由 DSH 注册表在内存里提供。宿主另开一个只读接口，用列表时下发的一次性 token
    换取正文；客户端不接受按技能名直接取，也**不允许编辑或保存**（没有文件可存）；
  - **只做「看」**：不启用/禁用、不改任何文件，那部分交给专门的技能管理插件。
- **文件夹下拉**：左上角的文件夹按钮不再是「点一下就弹系统选择框」，改成下拉：
  当前目录（带对勾）+ **最近用过的目录**（每项右侧 × 可单独移除，只删记录不碰磁盘）+
  「浏览技能…」+「选择其他文件夹…」。多工作区来回切换不再每次都面对同一个系统对话框。
  历史上限 8 条，随状态一起持久化。
- **技能列表变成单例标签页**：点「浏览技能…」会在 tab 栏打开一个「技能」tab（永远只有一个）。
  点开某个技能后，**左边的技能 tab 还在**，点一下即可回到列表 —— 不再需要「再点一次按钮」。
  从技能回到列表时会自动还原打开前的文件夹（读技能期间切换的根目录不会留下来）。
- **卸载不留痕迹**：DSH 卸载插件只做 `pnpm remove` + 重载组合，**不会碰浏览器存储**。
  以前卸载后 tab / 置顶 / 文件夹历史会一直躺在 `localStorage` 里，重装会「复活」
  一批可能已失效的旧路径。现在监听插件管理器的状态：一旦本插件从「已安装」里消失，
  就清掉 `dsh.notes.v1`。只在**确定发生过卸载**时触发（先见到「在」再见到「不在」），
  正常启动、禁用、启用都不会误清。
- **技能不再改动文件夹**：之前打开技能会临时把当前文件夹切到 `~/.dsh/skills/xxx`，
  结果正好反了 —— 技能目录占了位置，用户真正的工作目录反而从历史里消失。
  现在新增宿主只读接口 `GET /api/dsh-notes/read-skill-file`（围栏为「必须在技能根之内」，
  不需要用户选的根目录），打开技能**完全不动文件夹与历史**。技能文件也因此是只读的。
- **标签页拖动排序**：按住任意标签向左/右拖即可调整顺序，拖动时其他标签实时让位、
  被拖的那个半透明抬起；顺序随状态一起保存。用的是指针事件 + CSS `order` 预览
  （与置顶区同一套做法），不用原生 HTML5 拖拽 —— 后者会因重排触发 `dragend`，
  表现为「往后拖不动」。
- **插件页里的配置面板**：之前插件页只能看到 DSH 自带的启用开关。原因是 DSH **不会自动**渲染插件配置
  —— 只给「自己注册了配置页」的组件显示「配置」入口（`configure.has(row)`）。现在按官方
  `ui-settings-shell` 的写法占用 `plugins.row.config` seat，插件页里本插件那一行会出现「⚙ 配置笔记」，
  里面有：启用技能阅读、额外的技能目录、读写上限。
- **技能文件的右键菜单收紧**：技能的 `SKILL.md` 是 DSH 自己的资产，不是笔记。右键菜单只保留
  「打开」和「打开所在位置」，**不再给置顶和删除** —— 置顶会把它混进「我的收藏」，删除会直接破坏技能。
- **技能列表分组**：技能页按来源分两块 —— 「系统内置」（Office 三件套这类随 DSH 自带的，
  数量固定、改不了）在上，「我的技能」（你自己写的）在下。混在一起时内置的按字母排前面，
  会把自己写的挤到看不见的地方。
- **去掉重复的「启用」开关**：本插件不再提供 `enabled` 配置项 —— DSH 插件页自带的
  启用/禁用已经能停掉整个插件，再给一个只是同一件事的两个入口。
  ~~同时所有配置字段都不再标记 `volatile`~~ —— **这条判断是错的**，已在 0.1.10 修正：
  没有 volatile 字段时 DSH 根本不会给这一行生成配置入口，见 0.1.10 的说明。

## [0.1.9] - 2026-10-19

### 修复

- **插件页里没有配置入口**（`ui-notes` 那行是纯文本，点不动）。
  之前一直在 `plugins.row.config` + `configForms` + `SettingsFormModel`
  这条路上打转，方向就错了。参照官方 `dsh-workbuddy-connect` 的做法重做：
  - 挂 **`plugins.bundle.config`**，key 就是**包名**。这是 DSH 0.1.6+ 的
    **包级**入口，插件管理页在包详情里直接渲染（判定 `ledger.bundles.has(pkg.name)`），
    不需要点进某一行；`plugins.row.config` 保留一份给老宿主。
  - 配置页**自己画**，不再依赖 `configForms` / `SettingsFormModel` /
    `SettingsValueField` 这些内部表单机制 —— 版本升级换掉它们也带不崩配置页。
  - 值走插件自己的路由 `api/dsh-notes/config`：GET 读、POST 写。
    写入经 `ctx.configEditor.edit()` 落进 profile 的 `cordis.patch.yml`，
    重启后仍在；没有该服务的部署降级成只读。
- **`.volatile()` 的隐藏陷阱**：schemastery 的 volatile 字段解析出来是
  `{ get(), [volatile.write]() }` 访问器对象，不是原始值。直接写
  `config.skillsReading !== false` 永远为真 —— 开关关了也关不掉。
  现在统一走 `readConfig()`，先用 `isVolatile()` 判定再 `get()`（与 `dsh-settings`
  的 `plainConfig` 同一套做法）。`maxReadBytes`/`maxWriteBytes`/`skillRoots`
  同样改成调用时现读：volatile 改动不会重启插件，apply 阶段取一次会失真。
- **保存后第一次读回的是旧值，要点两次才生效**。`.volatile()` 字段的生效路径是
  `edit() → 写 profile 补丁 → reconcile → loader._commitVolatile()` 把新值
  `updateVolatile` 进运行中的 ref，这条链是异步的：`edit()` 返回不代表 ref 已换新。
  之前 POST 之后立刻「现读一次」回给客户端，于是拿到的还是旧值，界面表现为
  「第一次保存被还原，第二次才成功」。
  现在 POST 直接回**刚写进去的值**；GET 也改成**落盘值优先**
  （`configEditor.configuration()` 读补丁文件），运行中值只作兜底。
- **文件大小上限改用 MB 显示**。原来标签写「（字节）」，输入框里是 `8388608`
  这种数字 —— 对用户没有意义，数错一位就差三个数量级。
  现在标签直接是「单个文件读取上限」，输入框里是 `8`，右侧标 `MB`。
  存储语义完全不变（宿主 schema 与所有路由判断仍按字节），换算只在显示/解析层做。
  允许小数（`1.5` → 1572864 字节）。**不编辑则不回写**：非整 MB 的旧值
  （如 `1000000`）显示时会舍入成 `0.95`，若自动回写就会把它悄悄改成 `996147`，
  所以只有用户真的改了输入框才上报新值。
- **配置页分层：只留开关在明面上**。文件大小上限、自定义技能目录这些
  「90% 的人用不上」的项收进「高级」折叠区（默认收起，收起时若改过会显示
  一个小圆点），常态只看到「启用技能阅读」一个开关。
  收起不影响保存：改过的值照样能存，折叠只影响可见性。
  高级区整条是一颗带边框的胶囊按钮（不是只有「高级」两个字可点），右侧带
  「展开 / 收起」字样，悬停与键盘聚焦都有反馈 —— 之前只有词可点，用户点了
  旁边没反应，看起来就像坏掉了。
- **选择文件夹弹出的系统面板，左侧栏一直灰、`Open` 按钮发灰打不动**。
  根因是**调用路径不同**：DSH 官方的目录选择器客户端里有
  ```js
  const desktop = globalThis.__DSH_DIRECTORY_PICKER__;
  const pick = desktop === void 0 ? () => ctx.uiWorkspace.pickDirectory() : () => desktop.pick()
  ```
  官方用的是 **Electron preload 注入的桌面桥**（底层 `dsh-desktop:directory-pick` IPC），
  我们用的是 `uiWorkspace.pickDirectory()`（Remote RPC）。而 DSH 的 macOS 选择器是一段
  AppleScript，`activateIgnoringOtherApps(true)` 之后**立刻** `runModal` 阻塞 ——
  它依赖调用瞬间的窗口状态。RPC 往返把这段时间拉长，面板起来时窗口已不是「刚被点击的
  前台窗口」，系统就按非活动窗口渲染它：侧栏文字发灰、`Open` 按钮发灰。
  现在改成与官方一致，优先走桌面桥；桥不存在（Web 部署）时依次退回 RPC 与 Remote。
- **技能开关在服务端真正生效**：关掉后两条技能路由返回 404，
  `health` 如实回报 `skillsReading: false`，客户端随即隐藏所有技能入口。
- **可选服务改为惰性获取**：`skills`/`sessions` 以前在 apply 时就 `ctx.get`
  一次定死，apply 若早于 skill 插件就永远拿不到；现在每次用到时再取。
- **点保存后界面回滚成保存前的内容**。- **点保存后界面回滚成保存前的内容**。文件其实已经正确写入，但编辑器显示的是
  `草稿 ?? 文件基线`：保存只清掉了草稿，没有把「文件基线」推进到刚写入的内容，
  显示于是退回打开文件时读到的那份旧内容。切换标签再切回来（重新读盘）才正常。
  现在保存成功后先推进基线、再清草稿，两步之间没有空档。

### 说明

- 这一版同时用于验证插件详情页的「更新」按钮（0.1.7 起改用 tarball 规格覆盖安装）。

## [0.1.8] - 2026-10-17

### 新增

- **插件页展示本地化标题与描述**：中文界面显示「笔记」，英文界面显示「Notes」，
  描述随界面语言切换（`locale/zh.json`、`locale/en.json`）。
- **插件图标**：插件卡片与详情页显示自带图标（`icon.svg`）。
- **更新日志**：本文件，补齐 0.1.0 以来的全部变化。

### 说明

- 这是一次**验证版本**：用于确认 0.1.7 起详情页的「更新」按钮能真正完成升级。

## [0.1.7] - 2026-10-17

### 修复

- **详情页「更新」按钮必定失败**。带 HMR 的宿主（0.2.1-rc.2 起）会在跑 pnpm 之前，
  就以 `already-installed` 拒绝「spec 指明已安装包」的安装请求，而按钮原来正是用
  `包名@版本` 去覆盖安装自己。
  现在宿主查版本时一并返回 npm 的 `dist.tarball`，客户端优先用 **tarball 直链**做
  安装规格：它解析不出包名，那道检查不触发；pnpm 的 `minimumReleaseAge` 也只按
  registry 版本判定，不作用于 URL 依赖。
- 宿主没提供 tarball 时（0.1.6 及更早），仍退回 `包名@精确版本`。
- 错误码补上 `already-installed` 的中文说明，并给出可执行的替代路径（到插件页或
  插件市场更新），不再只显示英文原文。

## [0.1.6] - 2026-10-17

### 修复

- **「选择文件夹」崩溃**：提示
  `cannot get property "remote.directoryPicker" without inject`。
  根因是兜底分支把 `ctx.remote.directoryPicker` 的**属性访问**当作能力探测，
  而 cordis 对未写进 `inject` 的服务做属性门控，读取即抛错。
  现在改用 `ctx.get('remote.directoryPicker')`（gateway 就是以这个服务名注册的，
  查不到返回 `undefined` 而不抛），并且每个能力都改成「apply 期快照 → 调用时再取
  一次」两级，避免服务就绪晚于本插件时能力凭空消失。
- `uiWorkspace.pickDirectory()` 失败时不再把内核错误直接抛给用户，改为降级到
  Remote 直连；两者都不可用时给出可读提示「当前环境不支持选择文件夹」。

## [0.1.5] - 2026-10-16

### 修复

- **插件页的更新入口一进页面就失败**。原来用插件管理服务的 `inspect` 做检查，而它是
  「安装前校验」，已装进 profile 的包必被它以 `already-installed` 拒绝。
  改为由本插件自己的宿主半边查 npm 的 `latest` dist-tag（带 5 分钟缓存、8 秒超时）。
- 失败时显示**原始错误**，不再吞成一句「检查更新失败」。
- 客户端已更新、宿主未重启时新路由会 404，把这句 404 翻译成可执行提示：
  「请完全退出并重开 DSH」。

## [0.1.4] - 2026-10-16

### 新增

- **文件树三个按钮**：定位当前文件、展开全部文件夹、收起全部文件夹。
- **右键「在访达中打开」**（macOS）/「打开文件所在位置」（Windows）。
- **插件页一键更新**入口。

## [0.1.3] - 2026-10-16

### 修复

- **Windows 上文件夹与置顶显示完整路径**：路径取名只认正斜杠，反斜杠路径上取不到
  文件名。改用同时识别两种分隔符的取名方式（文件树、置顶胶囊、标签名三处）。

## [0.1.2] - 2026-10-16

### 新增

- **置顶区拖动排序**：按住拖动实时预览落点，松手落定。
- **置顶区单栏 / 双栏切换**。

## [0.1.1] - 2026-10-16

### 新增

- **Windows 上不提供删除**：Windows 没有可移植、可靠且可还原的用户态回收站，
  既然做不到「可还原的删除」，就不提供删除入口。

### 修复

- **让插件在 Windows 上可用**：客户端路径处理原来只认正斜杠，反斜杠路径上
  `basenameOf` 返回 -1，拖拽移动后标签名会变成完整路径。
- **暗色主题下主按钮文字看不清**：改用会随主题反转的按钮前景色。

## [0.1.0] - 2026-10-16

首个版本。主面板阅读器 + 备忘录，含 Host 半边（文件读写路由）与 Web 客户端半边
（侧栏入口 + 主面板接管）。

- 浏览器式可关闭标签页，支持关闭其他 / 右侧 / 全部，溢出时下拉查看
- 文件树（隐藏点文件，根目录按「文件夹 / 根目录文件」分组）
- 置顶区（上限 6 个）
- Markdown 左编右渲 / 纯文本单栏编辑，手动保存 + 未保存圆点
- 编辑 / 分栏 / 仅预览三种显示方式，分栏支持比例双向同步滚动
- 文件可见范围开关（可编辑 / 全部）；不可预览的文件提供「打开方式」控件，
  可选用系统程序打开或显示文件位置
- 拖拽移动文件到文件夹，悬停 600ms 自动展开
- 状态持久化（标签、置顶、展开、滚动位置、草稿）
- 删除移入系统回收站（macOS `~/.Trash`、Linux XDG），无系统回收站时移入
  笔记目录内的 `dsh-notes-trash`

运行时零第三方依赖：客户端产物只使用 DSH 冻结模块表中的平台模块。
