dsh-claude-style 0.5.3 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,7 +2,55 @@
2
2
 
3
3
  All notable changes to `dsh-claude-style` are documented here, newest first.
4
4
 
5
- ## [Unreleased]
5
+ ## [0.6.1] - 2026-09-24
6
+
7
+ [中文](#cn-0.6.1) | [English](#en-0.6.1)
8
+
9
+ <h3 id="cn-0.6.1">新增功能</h3>
10
+
11
+ - **适配 DSH 0.1.7 内置自动审查(Auto review)**:分段控件与权限菜单增加审查模式,分段控件显示为 `Auto`、弹层与当前状态显示为 `Auto review`;原 `Auto`(完全权限)更名为 `Yolo`;选择审查模式或完全权限时均正常调起宿主风险确认对话框。审查档位跟随宿主的权限目录:只有宿主的 `permissionPresets` 目录携带 `auto`(内置自动审查已启用)时才显示,目录变化时随之增减;目录读不到时整个权限控件交还宿主的访问模式按钮。
12
+ - **已归档会话的删除按钮恢复可用**:宿主没有给浏览器半边提供删除会话的接口(工作区控制器只有归档与取消归档,agent 协议里的会话删除由宿主委托给持有存储的 ACP agent),插件此前调用第三方插件的路由,该插件不在环境里时点下去没有任何反应。现在由插件的宿主半边新增私有路由 `POST /dsh-claude-style/session-delete` 删除本机会话目录:只接受 POST 与同源请求,id 必须匹配宿主自身的会话 id 形状,正在打开的会话拒绝删除,目录解析后必须留在会话根目录内。
13
+
14
+ <h3 id="en-0.6.1">New Features</h3>
15
+
16
+ - **Support DSH 0.1.7 built-in Auto review**: add review mode to the permission segmented control and popover, displayed as `Auto` in segments and `Auto review` in the menu and current status; rename the former `Auto` (Full access) segment to `Yolo`; selecting either review mode or Full access drives the host's risk confirmation dialog. The review tier follows the host's permission catalog: it appears only while the host's `permissionPresets` catalog carries `auto` (the built-in auto review is enabled), and tracks catalog changes; when the catalog cannot be read, the whole permission control hands the host's access-mode button back.
17
+ - **The archived rows' delete button works again**: the host gives the browser half no way to delete a session (the workspace controller only archives and unarchives, and the agent protocol's session delete is the host delegating to an ACP agent that owns the storage), so the skin used to call a third-party plugin's route and did nothing when that plugin was absent. The plugin's host half now serves a private route, `POST /dsh-claude-style/session-delete`, which removes the local session directory: POST and same-origin requests only, the id must match the host's own session id shape, an open session is refused, and the resolved directory must stay inside the sessions root.
18
+
19
+ **Full Changelog**: [v0.6.0...v0.6.1](https://github.com/Nwflower/dsh-claude-style/compare/v0.6.0...v0.6.1)
20
+
21
+ ## [0.6.0] - 2026-09-24
22
+
23
+ [中文](#cn-0.6.0) | [English](#en-0.6.0)
24
+
25
+ <h3 id="cn-0.6.0">问题修复</h3>
26
+
27
+ - 修复 **会话切换或卸载时因移动 React 插槽节点导致浏览器卡死与 DOM 异常**:停止将 `conversation.composer.dock` 容器内的会话统计药丸与上下文计量器通过 DOM 操作移入工具栏行,改为纯样式定位覆盖,保留节点在 React 虚拟树中的原生父子归属,消除 `Node.removeChild: The node to be removed is not a child of this node` 抛错与死循环卡死。
28
+ - 修复 **权限控件或会话统计出错时整个输入区样式被一并关闭**:出错只关掉权限控件本身,交还宿主的访问模式按钮、统计弹窗与统计行,输入区其余样式照常。
29
+
30
+ ### 移除
31
+
32
+ - **移除 0.1.5 及更早宿主的适配**:偏好读写只走宿主官方的 `configForms` 表单,删掉插件自建的 `/prefs` 路由;最低宿主版本提高到 0.1.7。
33
+
34
+ ### 其他变更
35
+
36
+ - **内部结构整理,行为无变化**:输入区的布局工作收进独立的 composer 特性,推理强度滑块的点阵与账号行各自拆成独立碎片,重复的宿主查询与残留节点清理合并成共用函数;调度器不再点名具体特性。
37
+ - **回归脚本共用一套无头浏览器启动**:smoke / probe / probe-timing / shoot 的浏览器配置目录改放 `.debug/`,调试端口由浏览器自选,`--cdp-port` 参数取消;probe 打开已有会话,跳过「新会话」。
38
+
39
+ <h3 id="en-0.6.0">Bug Fixes</h3>
40
+
41
+ - Fix **browser freezes and DOM exceptions during session switching or unmounting caused by moving React slot nodes**: stop moving the session stats pills and context meter out of the `conversation.composer.dock` container via DOM manipulation into the toolbar row; position them via CSS overlay instead, preserving native parent-child relationships in the React virtual tree and eliminating `Node.removeChild: The node to be removed is not a child of this node` crashes and freeze loops.
42
+ - Fix **the whole composer restyle switching off when the permission control or the session stats fail**: a failure now switches off only the permission control, which hands back the host's access-mode button, statistics dialogs and statistics row, while the rest of the composer keeps its styling.
43
+
44
+ ### Removals
45
+
46
+ - **Remove the adaptation for hosts 0.1.5 and earlier**: preference reads and writes go only through the host's `configForms` form, dropping the plugin's own `/prefs` route; the minimum host version rises to 0.1.7.
47
+
48
+ ### Chores
49
+
50
+ - **Internal restructuring, no behavior change**: the composer's layout work moves into a composer feature of its own, the reasoning-effort slider's dot matrix and the account rows move into fragments of their own, and repeated host lookups and leftover-node sweeps merge into shared helpers; the scheduler no longer names individual features.
51
+ - **One headless-browser launcher for the regression scripts**: smoke / probe / probe-timing / shoot keep the browser profile under `.debug/` and let the browser pick its own debugging port (the `--cdp-port` option is gone); probe opens an existing session and skips "New session".
52
+
53
+ **Full Changelog**: [v0.5.3...v0.6.0](https://github.com/Nwflower/dsh-claude-style/compare/v0.5.3...v0.6.0)
6
54
 
7
55
  ## [0.5.3] - 2026-09-23
8
56
 
package/README.en.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # DSH Claude Style
4
4
 
5
- **A theme plugin that recreates the look and feel of Claude Code Desktop for the DeepSeek Harness Web GUI.**
5
+ **A theme plugin that brings the look and feel of Claude Code Desktop to the DeepSeek Harness Web GUI.**
6
6
 
7
7
  > **Claude Code Desktop, right inside DSH.**
8
8
 
@@ -25,11 +25,11 @@
25
25
  </tr>
26
26
  </table>
27
27
 
28
- > Light: ivory canvas `#FCFCFB` with a pale sidebar `#FBFBF9`; dark: warm black `#141413`. The theme follows the system light/dark mode, and ember orange `#D97757` is the single action accent on both canvases.
28
+ > Light mode pairs an ivory canvas `#FCFCFB` with a pale sidebar `#FBFBF9`; dark mode uses warm black `#141413`. The theme follows your system's light/dark setting, and ember orange `#D97757` is the single action accent across both canvases.
29
29
 
30
30
  ## Fonts
31
31
 
32
- > **Important: the Anthropic fonts are not distributed with the npm package — they live in this repository under [`fonts/`](fonts/).** You can install them system-wide, or skip the install entirely: drop the two `.ttf` files into the plugin package's `fonts/` directory and the host serves them as webfonts the same way (identical files, identical result). Either way, refresh / restart the web UI.
32
+ > **Important: the Anthropic fonts are not bundled with the npm package.** They are available for download in this repository under [`fonts/`](fonts/). You can either install them on your system, or skip the install entirely — drop the two `.ttf` files into the plugin package's `fonts/` directory and the host will serve them as webfonts (the files are identical, so the result is the same). Either way, refresh or restart the web UI for the fonts to take effect.
33
33
 
34
34
  | Font | Used for | File |
35
35
  |---|---|---|
@@ -37,19 +37,19 @@
37
37
  | Anthropic Serif Web Text | Conversation body / Markdown | [`fonts/AnthropicSerifWebText.ttf`](https://github.com/Nwflower/dsh-claude-style/raw/main/fonts/AnthropicSerifWebText.ttf) |
38
38
  | JetBrains Mono Variable | Code / code blocks | [`fonts/JetBrainsMonoVariable.ttf`](https://github.com/Nwflower/dsh-claude-style/raw/main/fonts/JetBrainsMonoVariable.ttf), [`fonts/JetBrainsMonoItalicVariable.ttf`](https://github.com/Nwflower/dsh-claude-style/raw/main/fonts/JetBrainsMonoItalicVariable.ttf) |
39
39
 
40
- Enabling the Anthropic fonts (pick one):
40
+ To enable the Anthropic fonts, choose one of the following:
41
41
 
42
- ① Install system-wide — on Windows, double-click the `.ttf` → "Install"; on macOS, import it with Font Book.
42
+ ① Install them on your system — on Windows, double-click each `.ttf` and choose "Install"; on macOS, import them with Font Book.
43
43
 
44
- ② No install — copy the `.ttf` files into the plugin package's `fonts/` directory. Refresh the page afterwards.
44
+ ② Skip the install — copy the `.ttf` files into the plugin package's `fonts/` directory, then refresh the page.
45
45
 
46
- > The Anthropic Sans/Serif fonts are copyright Anthropic, licensed for personal use only, and are not covered by the MIT license.
46
+ > The Anthropic Sans and Serif fonts are the property of Anthropic, licensed for personal use only, and are not covered by this project's MIT license.
47
47
 
48
48
  ## Installation
49
49
 
50
- > Requires dsh ≥ 0.1.5-rc.2
50
+ > Requires dsh ≥ 0.1.7
51
51
 
52
- 1. From a terminal
52
+ 1. From a terminal:
53
53
 
54
54
  ```bash
55
55
  dsh plugin --profile web add dsh-claude-style # npm package (recommended)
@@ -58,49 +58,49 @@ dsh plugin --profile web add Nwflower/dsh-claude-style # GitHub source
58
58
 
59
59
  2. From the [plugin market](https://github.com/dsh-market/dsh-market)
60
60
 
61
- Keep only one theme enabled at a time. After installing, **restart `dsh web`** and refresh the page.
61
+ Keep only one theme enabled at a time. After installation, **restart `dsh web`** and refresh the page.
62
62
 
63
63
  ## Features
64
64
 
65
- 1. **Theme** — global the moment it is installed, nothing to configure. Ivory `#FCFCFB` in light mode, warm black `#141413` in dark mode, with the same ember orange `#D97757` action accent on both; light/dark follows the system colour mode.
66
- 2. **Composer** — the input box is rebuilt end to end: a permission segment control (Full access / Read only / …), a model trigger carrying the vendor lockup, and the tool row and the stats sharing one line, with the send and stop keys unified into a 7px rounded rectangle. The stats sentence and the model trigger share one type size and colour.
67
- 3. **Model picker** — a new two-level popover: level one lists the official service plus the quick providers picked in settings, and the "More models" second level groups the rest by provider. Every row carries its vendor lockup and description; the foot holds the reasoning-effort slider (stepless — it settles on the nearest level when you let go) and the "More models" entry. Level two is bottom-aligned with level one, providers already shown in level one are not repeated, and a model without thinking support draws no slider at all. Hover opens after a 50ms dwell and closes after 150ms, and the sliver between the two cards does not count as leaving.
68
- 4. **Workspace** — the sidebar's "Workspace" heading becomes an **Active / Archived** segment control: Active keeps the host's own session tree, while Archived is the skin's own flat list (title, time, and an unarchive plus a delete button on every row). The rows line up with the host's own session rows item by item (row x=12 / width 251 / height 28 / title x=36). On a host without archiving support the control is not drawn and the plain heading stays.
69
- 5. **Sidebar** — the New session and Plugins rows take Claude's own shape: no plate at rest, a plate on hover, the "+" of New session set in a circular chip, and both icons turning 90° clockwise on hover (four-fold symmetric glyphs, so they land back on themselves); the account drawer and the ban-screen easter egg stay as they were.
70
- 6. **Account area** — after a desktop sign-in the row shows the **real avatar and nickname** (read from the server only at startup, on hot reload, and on sign-in and sign-out — never polled); the account popover is the host's own: the host's account row is the entry, and the popover lists the account header, the other plugins' footer entries and the host's own Settings / Feedback / Sign out, each keeping its shipped copy, order and click behavior. On a host without an account area (Web, 0.1.5) the plugin builds a popover of the same look. The card uses a pure white background and spans the sidebar's width.
71
- 7. **Conversation views** — in the desktop Windows titlebar mode the Conversation / Trajectory control sits centred and always visible on the titlebar row (the same row as the New session button); on a wide window with a short title it stays on the title's row, and otherwise it keeps its own row under the title.
65
+ 1. **Theme** — applies globally the moment it is installed, with nothing to configure. Light mode uses ivory `#FCFCFB`, dark mode warm black `#141413`, and both share the same ember orange `#D97757` action accent; light/dark follows the system color mode.
66
+ 2. **Composer** — the input box is rebuilt from the ground up: a permission segmented control (Read / Edit / Auto / Yolo — the Auto tier appears only when the host has the built-in auto review enabled), a model trigger carrying the vendor lockup, and the toolbar and status stats laid out on a single line, with the send and stop buttons unified into 7px rounded rectangles. The stats sentence and the model trigger share the same font size and color.
67
+ 3. **Model picker** — a new two-level popover. The first level lists the official service plus the quick providers selected in settings; "More models" opens a second level grouped by provider. Every row carries its vendor lockup and a description, and the footer holds the reasoning-effort slider (stepless — it snaps to the nearest level on release) together with the "More models" entry. The second level aligns its bottom edge with the first, providers already shown on the first level are not repeated, and models without thinking support get no slider. Hovering opens the popover after a 50 ms dwell and closes it after 150 ms; the gap between the two cards does not count as leaving.
68
+ 4. **Workspace** — the sidebar's "Workspace" heading becomes an **Active / Archived** segmented control. Active keeps the host's session tree, while Archived is the skin's own flat list (title, time, and per-row unarchive and delete icon buttons). Rows match the host's session rows measurement for measurement (row x=12 / width 251 / height 28 / title x=36).
69
+ 5. **Sidebar** — the New session and Plugins rows take Claude's real shape: no background at rest, a background on hover, the "+" of New session set inside a circular chip, and both icons rotating 90° clockwise on hover (four-fold symmetric glyphs, so they land back on themselves). The account drawer and the ban-screen easter egg remain as they were.
70
+ 6. **Account area** — after signing in on desktop, the row shows your **real avatar and nickname** (fetched from the server only at startup, on hot reload, and on sign-in and sign-out — never polled; a hand-drawn starburst is the fallback). The account popover remains the host's own: the host's account row is the entry point, and the popover lists the account header, the other plugins' footer entries, and the host's own Settings / Feedback / Sign out, each keeping its original copy, order, and click behavior. On hosts without an account area (Web), the plugin builds a popover with the same look. The card uses a pure white background and spans the sidebar's width.
71
+ 7. **Conversation views** — in the desktop Windows titlebar mode, the Conversation / Trajectory control sits centered and always visible on the titlebar row (the same row as the New session button). On wide windows with a short title it stays on the title's row; otherwise it keeps its own row below the title.
72
72
 
73
73
  ## Disabling and uninstalling
74
74
 
75
- To pause the theme without uninstalling it, add this to the profile's `cordis.patch.yml` (`~/.dsh/profiles/web/cordis.patch.yml`):
75
+ To pause the theme without uninstalling it, add the following to the profile's `cordis.patch.yml` (`~/.dsh/profiles/web/cordis.patch.yml`):
76
76
 
77
77
  ```yaml
78
78
  - id: ui-skin-claude-style
79
79
  disabled: true
80
80
  ```
81
81
 
82
- It takes effect within about a second; refresh the page to get the stock look back.
82
+ The change takes effect within about a second — refresh the page to restore the stock appearance.
83
83
 
84
84
  ```bash
85
85
  dsh plugin --profile web remove dsh-claude-style # uninstall
86
86
  ```
87
87
 
88
- Then restart `dsh web`; if you added this theme's entry to `cordis.patch.yml` by hand, remove that too.
88
+ Then restart `dsh web`. If you previously added this theme's entry to `cordis.patch.yml` by hand, remove it as well.
89
89
 
90
90
  ## Documentation
91
91
 
92
- | Document | What it covers |
92
+ | Document | Contents |
93
93
  | --- | --- |
94
- | [Design tokens](docs/STYLE.md) | Palette, fonts, shapes, source layout and the host-selector discipline (in English) |
95
- | [Architecture decisions](docs/architecture.md) | Single-file bundling, the one-scheduler rule, the feature contract, the account surface, and their trade-offs |
94
+ | [Design tokens](docs/STYLE.md) | Palette, typography, shapes, source layout, and host-selector discipline (in English) |
95
+ | [Architecture decisions](docs/architecture.md) | Single-file bundling, the single-scheduler rule, the feature contract, the account surface, and their trade-offs |
96
96
  | [Changelog](CHANGELOG.md) | Version history |
97
- | [Contributing](CONTRIBUTING.md) | Building from `src/`, commit conventions, screenshot and regression tooling (in English) |
97
+ | [Contributing](CONTRIBUTING.md) | Building from `src/`, commit conventions, and the screenshot and regression tooling (in English) |
98
98
 
99
- ## Friends
99
+ ## Related projects
100
100
 
101
- > Running several themes at once? Try [dsh-skin-manager](https://github.com/xiaoyangcheng84-svg/dsh-skin-manager) — it switches between every installed theme from one settings page.
101
+ > Running several themes at once? Try [dsh-skin-manager](https://github.com/xiaoyangcheng84-svg/dsh-skin-manager) — it switches between all installed themes from a single settings page.
102
102
 
103
- > Want to bring your Claude Code / Codex sessions into DSH and keep chatting? Try the author's other plugin, [dsh-chat-import](https://github.com/Nwflower/dsh-chat-import).
103
+ > Want to import your Claude Code / Codex session history into DSH and keep the conversation going? Try the author's other plugin, [dsh-chat-import](https://github.com/Nwflower/dsh-chat-import).
104
104
 
105
105
  ## Star History
106
106
 
package/README.md CHANGED
@@ -47,7 +47,7 @@ Anthropic 字体启用(二选一):
47
47
 
48
48
  ## 安装
49
49
 
50
- > 需要 dsh ≥ 0.1.5-rc.2
50
+ > 需要 dsh ≥ 0.1.7
51
51
 
52
52
  1. 通过终端安装
53
53
 
@@ -63,11 +63,11 @@ dsh plugin --profile web add Nwflower/dsh-claude-style # GitHub 源
63
63
  ## 特点
64
64
 
65
65
  1. **主题** —— 安装即全局生效,无需配置。亮色象牙白 `#FCFCFB`、暗色暖黑 `#141413`,两套画布共用同一个操作强调色陶烬橙 `#D97757`;亮暗跟随系统颜色模式切换。
66
- 2. **输入框** —— 输入框整块重做:权限分段控件(完全权限 / 只读 / …)、带品牌锁定标的模型触发器、工具栏与状态统计同一行排版,发送键与停止键统一成 7px 圆角。统计句与模型触发器共用一套字号与颜色。
66
+ 2. **输入框** —— 输入框整块重做:权限分段控件(只读 / 工作区 / 自动审查 / Yolo,自动审查档位仅在宿主启用内置自动审查时出现)、带品牌锁定标的模型触发器、工具栏与状态统计同一行排版,发送键与停止键统一成 7px 圆角。统计句与模型触发器共用一套字号与颜色。
67
67
  3. **模型选择器** —— 全新的两级弹层:一级列官方服务与设置页勾选的快捷供应商,「更多模型」二级按供应商分组;每行带厂商锁定标与说明文案,底部是推理等级推条(无极滑动,松手对齐最近档位)与「更多模型」入口。二级底边与一级对齐,一级已显示的供应商不再重复;模型不支持思考时不画推条。悬停停留 50ms 打开、离开 150ms 关闭,两卡之间的空隙不算离开。
68
- 4. **工作区** —— 侧栏「工作区」标题改成 **进行中 / 已归档** 分段控件:进行中沿用宿主的会话树,已归档是皮肤自己的平铺列表(标题、时间、每行「取消归档」与「删除」两个图标按钮)。行排版与宿主的会话行逐项一致(行 x=12 / 宽 251 / 高 28 / 标题起点 x=36)。宿主没有归档能力时(如 0.1.5 的某些构建)不显示控件、保留原标题。
68
+ 4. **工作区** —— 侧栏「工作区」标题改成 **进行中 / 已归档** 分段控件:进行中沿用宿主的会话树,已归档是皮肤自己的平铺列表(标题、时间、每行「取消归档」与「删除」两个图标按钮)。行排版与宿主的会话行逐项一致(行 x=12 / 宽 251 / 高 28 / 标题起点 x=36)。
69
69
  5. **侧栏** —— 新会话与插件两行取 Claude 真机形状:默认无底色、hover 才有底色,新会话的「+」套一枚圆形底,两行图标 hover 顺时针转 90°(四重对称图形,转回原位);底部账户抽屉、封号彩蛋一并保留。
70
- 6. **账号区** —— 桌面端登录后显示**真实头像与昵称**(只在启动、热重载、登录与登出时向服务器读取,不轮询;失败回退手绘星芒);账号弹层沿用宿主自带的那一个:宿主的账号行就是入口,弹层里依次是账号头部、其它插件的页脚条目与宿主自己的「设置 / 意见反馈 / 退出登录」,各行的文案、顺序与点击行为保持宿主原样;宿主没有账号区的环境(Web、0.1.5)由插件自建同样外观的弹层。弹层卡片为纯白背景、宽度与侧栏一致。
70
+ 6. **账号区** —— 桌面端登录后显示**真实头像与昵称**(只在启动、热重载、登录与登出时向服务器读取,不轮询;失败回退手绘星芒);账号弹层沿用宿主自带的那一个:宿主的账号行就是入口,弹层里依次是账号头部、其它插件的页脚条目与宿主自己的「设置 / 意见反馈 / 退出登录」,各行的文案、顺序与点击行为保持宿主原样;宿主没有账号区的环境(Web)由插件自建同样外观的弹层。弹层卡片为纯白背景、宽度与侧栏一致。
71
71
  7. **对话视图** —— 对话 / 轨迹控件在桌面 Windows 标题栏模式下居中常显在标题栏那一行(与新会话按钮同一行);宽窗口且标题较短时它停在标题那一行,否则留在标题下方自己那一行。
72
72
 
73
73
  ## 停用与卸载
@@ -68,24 +68,24 @@
68
68
  ## D9. composer 的 :has() 分支改 JS 写属性
69
69
 
70
70
  - **背景**:皮肤在流式输出期间的渲染压力主要来自两处:D6 的每帧全量 pass,以及 CSS 中约 70 处对 DOM 结构敏感的 `:has()`(绝大多数是 `[class*="composerStack"]` 上的 hero/inline 分支)——每次 DOM 变更都触发昂贵的选择器重算。
71
- - **决定**:把「结构感知」从 CSS 移到 JS:`permissions.js` 的 `syncSegments()` 在 scheduler 每轮 pass 里为 composerStack 祖先写 `data-composer-variant="hero|inline"` 属性(observer 的 attributeFilter 不含 data-*,不会反触发;写前比较旧值防抖动;同轮去重避免多卡命中同一 stack 反复写),CSS 改为读属性。交互敏感的 `:has()`(`:hover`、`:focus-within`)与低频的 dialog `:has()` 保留。
72
- - **落地**:已执行完毕。仍未消除的结构感知 `:has()` 只剩 placeholder 那条(`card` 上的 `:not(:has([data-composer-placeholder]))`):`copy.js` 本就管理 placeholder,可在它的 sync 里同步写 `data-has-placeholder` 再改 CSS,属可选的后续优化。
71
+ - **决定**:把「结构感知」从 CSS 移到 JS:`composer.js` 的 pass(每轮第一个同步)为卡片与 composerStack 祖先写 `data-composer-variant="hero|inline"` 属性(hero 以宿主会话根上的 `data-phase="hero"` 为准;observer 的 attributeFilter 不含 data-*,不会反触发;写前比较旧值防抖动;同轮去重避免多卡命中同一 stack 反复写),CSS 改为读属性。附件同理:同一个 pass 找出输入栏里的附件缩略图并标上 `data-dsh-claude-attachment`,缩略图样式只读这个属性,不再在 CSS 里用 `:has(img)` 逐次判定。交互敏感的 `:has()`(`:hover`、`:focus-within`)与低频的 dialog `:has()` 保留。
72
+ - **落地**:已执行完毕。仍未消除的结构感知 `:has()` 还有两处:placeholder 那几条(`card` 与 inline 规则上的 `:has([data-composer-placeholder])`)——`copy.js` 本就管理 placeholder,可在它的 sync 里同步写 `data-has-placeholder` 再改 CSS;以及 hero 分支上的 `*:has([data-composer-card])`。两处都属可选的后续优化。
73
73
  - **重审条件**:实测证明 :has() 不再是热点,或宿主提供 hero/inline 的稳定属性契约。
74
74
 
75
- ## D10. 设置按宿主世代分流:官方 Config 表单 + 旧版命名空间注册
75
+ ## D10. 设置传输只走官方 Config 表单
76
76
 
77
- - **背景**:0.1.7 删掉了 `settings.register(ns, schema)`——命名空间不再是插件自取的名字,而是 profile entry id,schema 就是插件导出的 `Config`,只有 `.volatile()` 字段进表单,值写进 profile 的 Cordis patch;客户端服务 `settingsScope` 改名 `configForms`,插件设置席位从 `settings.plugin.item` 变成 `plugins.bundle.config`(键 = 包名)/ `plugins.row.config`。旧宿主(0.1.5-rc.2,桌面端内置)仍是注册制,且其 schemastery 3.18.2 没有 `.volatile()`——两套 API 互斥,`.volatile()` 在旧版上会直接抛错。
78
- - **决定**:设置层按宿主世代走两条路,探测点各只有一个——宿主半边看 `settings.register` 是否存在,客户端看 `ctx.get('configForms')` 是否可用。
79
- - 宿主半边导出 `Config`:八个偏好字段,`volatileField()` 逐字段探测 `.volatile()` 存在才加标记;schemastery 用顶层 await 守卫导入,解析不到就让 `Config` 为 `undefined`,皮肤照常加载。新宿主把命名空间取成 `ctx.fiber.entry.id`(读不到回落 patch 里的常量),只调 `settings.configure({ auto: false }, ctx.fiber)` 声明自带页面;旧宿主仍 `register('claude-style', schema)`。两条路共用同一个可变命名空间,`describePrefs/updatePrefs` 与自建 prefs 路由都读它。
80
- - 客户端:`ctx.configForms.get(entryId)` 可用就用官方表单(值 + 写队列 + revision 栅栏),否则回落到自建路由;设置界面在 0.1.7 注册成 `plugins.bundle.config`(键 = 包名,渲染在插件页上),旧宿主注册成 `settings.section` 整页。分流靠 `slots.inject` 的「槽被声明才触发」语义(旧宿主从不声明前者),再用 `configForms` 是否存在否决后者在 0.1.7 上的重复注册。
81
- - **代价**:设置层有两套传输与两套席位,回归必须覆盖两种宿主形态(`.debug/settings-bridge-check.cjs` 跑宿主半边两代,`.debug/client-settings-check.cjs` 在无头浏览器里跑客户端两代);`Config` 的顶层 await 让宿主半边模块求值晚一步(loader 本就 await 导入,无实际影响)。
82
- - **重审条件**:不再支持 0.1.5-rc.2(桌面端内置升级到 0.1.7+)时,删掉旧路径与两处探测,只留 `Config` + `plugins.bundle.config`。
77
+ - **背景**:0.1.7 删掉了 `settings.register(ns, schema)` 的旧契约——命名空间是 profile entry id,schema 就是插件导出的 `Config`,只有 `.volatile()` 字段进表单,值写进 profile 的 Cordis patch;客户端服务 `settingsScope` 改名 `configForms`,插件设置席位从 `settings.plugin.item` 变成 `plugins.bundle.config`(键 = 包名)。没有 `configForms` 的旧宿主曾让客户端保留一条自建 `/prefs` 路由作为第二传输。
78
+ - **决定**:客户端只有 `configForms` 一条传输——`ctx.configForms.get(entryId)` 绑定官方表单,读、写都走控制器(值 + 写队列 + revision 栅栏),插件自建的 `/prefs` 路由与其回退分支一并删除。
79
+ - 宿主半边导出 `Config` 作为命名空间的 schema:八个偏好字段,`volatileField()` 逐字段探测 `.volatile()` 存在才加标记;schemastery 用顶层 await 守卫导入,解析不到就让 `Config` 为 `undefined`,皮肤照常加载。命名空间取成 `ctx.fiber.entry.id`(读不到回落 patch 里的常量),并调 `settings.configure({ auto: false }, ctx.fiber)` 声明自带页面。`configForms` 只服务宿主已注册的命名空间,所以这个导出是偏好可用的前提。宿主仍拥有旧的命令式注册表时,保留 `settings.register('claude-style', schema)` 分支,当前宿主不走它。
80
+ - 设置席位:当前宿主注册成 `plugins.bundle.config`(键 = 包名,渲染在插件页上);`settings.section` 整页席位保留,但只在宿主没有 `configForms` 时注册。
81
+ - **代价**:`Config` 的顶层 await 让宿主半边模块求值晚一步(loader 本就 await 导入,无实际影响);schemastery 的守卫导入是 AGENTS 记录在案的那条例外,它要在解析不到时保住皮肤,不让整包失败。
82
+ - **重审条件**:宿主重新提供不需要 `Config` 的设置注册方式时,或 `configForms` 契约再变时,重写这一层。
83
83
 
84
84
  ## D11. 插件自有路由借宿主的请求栅栏;宿主/用户来源的字符串只以文本上屏
85
85
 
86
- - **背景**:`webServer.register()` 交给插件的是裸请求。宿主自己的 `/api` 挂在 Host/Origin 栅栏与浏览器会话 cookie 之后(`connection.requestRejection()`),插件路由不在其内。本插件的 `/prefs`(写设置)与 `/username`(读系统用户名)因此曾经完全无鉴权:跨站页面用 `text/plain` 发 POST 无需预检即可写入设置,`dsh web` 绑定 `0.0.0.0` 时局域网里任何人都能直接写。写进去的用户名又被客户端拼进 `innerHTML`——实测在 GUI 页面里执行了脚本,而这个页面能驱动执行 shell 命令的智能体。
87
- - **决定**:两层各守一道。宿主半边:`/prefs` 与 `/username` 处理前先调 `ctx.get('connection').requestRejection(req)`(0.1.5-rc.2 起即有),拒绝即回 401/403;`/prefs` 另要求 `Content-Type: application/json`(跨站页面发不出不经预检的 JSON)、请求体上限 16 KiB、快捷供应商至多 64 个短 id。宿主没有该服务时,插件自带的替身只服务回环(回环 Host、无跨站标记、Origin 与 Host 一致)。模型文案与字体等静态资产保持公开。客户端:一切来自设置、账号服务、系统或第三方插件的字符串只用 `textContent` / 元素属性写入,图标复制节点而非重新解析 markup。
88
- - **代价**:路由依赖宿主的 connection 服务。已核对两种壳都能通过:浏览器同源请求带会话 cookie;0.1.7 桌面壳经 `forwardWebRequest` 转发到回环 Host、剥掉页面 Origin 并自带 cookie(宿主真实的 `isTrustedApiRequest` 对这两种请求放行,对跨站与 DNS 重绑定请求拒绝)。0.1.5-rc.2 桌面端根本不把插件路径路由到 webServer(非 `/api` 一律走静态资产),不受影响。
86
+ - **背景**:`webServer.register()` 交给插件的是裸请求。宿主自己的 `/api` 挂在 Host/Origin 栅栏与浏览器会话 cookie 之后(`connection.requestRejection()`),插件路由不在其内。本插件的 `/username`(读系统用户名)因此曾经完全无鉴权:`dsh web` 绑定 `0.0.0.0` 时局域网里任何人都能直接读。读到的用户名又被客户端拼进 `innerHTML`——实测在 GUI 页面里执行了脚本,而这个页面能驱动执行 shell 命令的智能体。
87
+ - **决定**:宿主半边 `/username` 处理前先调 `ctx.get('connection').requestRejection(req)`,拒绝即回 401/403。宿主没有该服务时,插件自带的替身只服务回环(回环 Host、无跨站标记、Origin 与 Host 一致)。模型文案与字体等静态资产保持公开。删除会话的私有路由 `POST /dsh-claude-style/session-delete` 同样先过栅栏,另加四道:只接受 POST,id 必须匹配宿主自身的会话 id 形状,正在打开的会话拒绝,目录解析后必须留在会话根目录内(删除发生在宿主半边,浏览器半边只提交一个 id)。客户端:一切来自设置、账号服务、系统或第三方插件的字符串只用 `textContent` / 元素属性写入,图标复制节点而非重新解析 markup。
88
+ - **代价**:路由依赖宿主的 connection 服务。已核对两种壳都能通过:浏览器同源请求带会话 cookie;0.1.7 桌面壳经 `forwardWebRequest` 转发到回环 Host、剥掉页面 Origin 并自带 cookie(宿主真实的 `isTrustedApiRequest` 对这两种请求放行,对跨站与 DNS 重绑定请求拒绝)。
89
89
  - **重审条件**:宿主为插件提供自带鉴权的路由注册(如 `connection.fetch.register`)时,迁移过去并删掉本地替身。
90
90
 
91
91
  ## D12. 特性级失败隔离:一个特性出错只关掉它自己
@@ -94,17 +94,17 @@
94
94
  - **决定**:
95
95
  - teardown 最先经 `ctx.effect` 注册且幂等;每个特性单独 try/catch 安装,装不上的报一次 `console.error` 并退役。调度器本身装不上时整体回滚到宿主原样——没有调度器,其余特性都不会同步,留着只是一张半套的皮。
96
96
  - 每轮 pass 里每个特性的 `sync()` 单独 try/catch,连续失败 3 轮即报一次并退役(`ui.retire`)。
97
- - 退役 = 跑该特性自己的 teardown,并把它接管的宿主界面还回去。页脚接管(`FOOTER_ATTR`,由偏好写)与 composer 重绘(`COMPOSER_ATTR`,由 permissions 的 pass 写)都会隐藏宿主控件,所以退役 `footer` / `permissions` 时对应的闸门强制关闭、不再随偏好打开。特性的 teardown 因此必须撤干净自己的 DOM 与标记(模型选择器此前只清变量,已补上);只做装饰的 pass 用自己的句柄名(`ui.settingsNav`),退役它不会卸掉设置页。
98
- - **代价**:特性失效时界面上没有提示,只有控制台一行;依赖它的特性(例如都读 `ui.copy.isComposerActive()`)会各自连续失败、依次退役——降级而非崩溃。
97
+ - 退役 = 跑该特性自己的 teardown,并把它接管的宿主界面还回去。页脚接管(`FOOTER_ATTR`,由偏好写)与 composer 重绘(`COMPOSER_ATTR`,由 composer 的 pass 写)都会隐藏宿主控件,所以退役 `footer` / `composer` 时对应的闸门强制关闭、不再随偏好打开。权限控件替换掉的宿主控件(访问模式按钮、统计弹窗、详细统计行)另由它自己的标记把守——body 上的 `PERMISSIONS_ATTR` 与统计行上的模式标记,两者都由它的 teardown 摘掉——所以权限控件或会话统计出错时,只有它们交还宿主,composer 重绘照常。特性的 teardown 因此必须撤干净自己的 DOM 与标记(模型选择器此前只清变量,已补上);调度器拆除时不替任何特性清扫,每个特性的 teardown 就是它 DOM 与标记的唯一清理,smoke 的「拆除后不留节点」检查因此直接覆盖到每个特性。只做装饰的 pass 用自己的句柄名(`ui.settingsNav`),退役它不会卸掉设置页。
98
+ - **代价**:特性失效时界面上没有提示,只有控制台一行;依赖它的特性(例如都读 `ui.composer.isActive()`)会各自连续失败、依次退役——降级而非崩溃。
99
99
  - **重审条件**:宿主提供插件级的错误上报 / 健康面板时,把报告接过去。
100
100
 
101
101
  ## D13. 特性碎片的拆分布局与调度契约
102
102
 
103
103
  - **背景**:重构开始时,三个特性碎片都越过 750 行上限——`account-footer.js` 1357 行(账户资料、宿主账号菜单桥接、其他插件页脚条目的镜像、抽屉壳、行构建五份工作挤在一个闭包里)、`model-picker.js` 900 行(模型目录订阅与行构建也塞在里面)、`permissions.js` 868 行(会话统计卡也在里面);`composer/inline.css` 847 行与 `components/model-picker.css` 767 行两条 CSS 同样超限。调度器同时把每个特性的触发调用硬编码在手写清单里:`PASS_FEATURES` 按名列 pass 序、与 entry.js 的安装序分开维护,另有 21 处特性专属调用散在各触发分支里,而且已经与特性漂移——`ui.heroMenu.close` 是被守卫着的死调用(heroMenu 的句柄只有 `{ sync, reposition }`)。`ui` 是跨特性共享的服务注册表,却没有任何文档说明句柄有哪些方法、谁可以读谁。
104
104
  - **决定**(重构 Phase 0–3 已落地):
105
- - **辅助碎片导出顶层 `createX(...)` 工厂**,仿 popover-utils 的 `createHoverIntent` 与 model-effort 的 `createEffortControl`:状态收在工厂自己的闭包里,返回一个小对象;访问器与回调经参数传入(如 `{ isOpen: fn, onChange: fn }`),绝不伸手进别的闭包。特性的 `installX` 负责把工厂接起来(现有七个:`createAccountProfile`、`createHostAccountMenu`、`createFooterMirror`、`createAccountSurface`、`createModelCatalog`、`createModelRows`、`createSessionStats`)。
106
- - **拆出多个碎片的特性建一个子目录**(`overrides/account/`、`overrides/model/`);只拆出一个辅助碎片的特性把它放在特性旁边(`overrides/session-stats.js`)——单文件目录是噪音。FRAGMENTS 里列在特性碎片紧前面;顶层名对整个 bundle 全局唯一、以特性起名(`createAccountProfile`,不是 `createProfile`)。移动就是移动:注释随行、风格与名字不变,只有闭包变量必须变成参数时才改签名。
107
- - **特性契约**:entry.js 的 FEATURES 表(`{ name, handle?, install }`)统一安装序与 pass 序——pass 序就是安装序过滤出句柄带 `sync` 的特性(`settings` 安装到 `ui.settingsNav`)。scheduler 只认 FeatureHandle 的可选钩子(typedef 在 scheduler.js 头部):`sync` / `owns` + `close('outside')` / `onPointerDown` / `close('escape')` / `close('composer')` / `onInput` / `reposition` / `onCopyChange` / `onKey`;没实现的钩子直接跳过,每个特性保住自己原有的关闭路线(permissions 没有外部点击关闭,quickProviders 只在 composer 聚焦时关)。`retire` 按 name 或 handle 匹配:纯 handle 命中只停 sync、不拆安装(settingsNav 的显式分支——失败计数器已拒绝后续 pass,安装继续跑、设置页不卸);退役 `footer` / `permissions` 仍强制归还 body 属性(FOOTER_ATTR / COMPOSER_ATTR)。
105
+ - **辅助碎片导出顶层 `createX(...)` 工厂**,仿 popover-utils 的 `createHoverIntent`:状态收在工厂自己的闭包里,返回一个小对象;访问器与回调经参数传入(如 `{ isOpen: fn, onChange: fn }`),绝不伸手进别的闭包。特性的 `installX` 负责把工厂接起来(现有十个:`createAccountProfile`、`createHostAccountMenu`、`createAccountRows`、`createFooterMirror`、`createAccountSurface`、`createModelCatalog`、`createModelRows`、`createEffortControl`、`createEffortMatrix`、`createSessionStats`;`createEffortMatrix` 由 `createEffortControl` 接起来)。
106
+ - **拆出多个碎片的特性建一个子目录**(`overrides/account/`、`overrides/model/`、`overrides/effort/`);只拆出一个辅助碎片的特性把它放在特性旁边(`overrides/session-stats.js`)——单文件目录是噪音。FRAGMENTS 里列在特性碎片紧前面;顶层名对整个 bundle 全局唯一、以特性起名(`createAccountProfile`,不是 `createProfile`)。移动就是移动:注释随行、风格与名字不变,只有闭包变量必须变成参数时才改签名。
107
+ - **特性契约**:entry.js 的 FEATURES 表(`{ name, handle?, install }`)统一安装序与 pass 序——pass 序就是安装序过滤出句柄带 `sync` 的特性(`settings` 安装到 `ui.settingsNav`)。scheduler 只认 FeatureHandle 的可选钩子(typedef 在 scheduler.js 头部):`sync` / `owns` + `close('outside')` / `onPointerDown` / `close('escape')` / `close('composer')` / `onInput` / `reposition('viewport' | 'composer')` / `onCopyChange` / `onKey`;没实现的钩子直接跳过,每个特性保住自己原有的关闭路线(permissions 没有外部点击关闭,quickProviders 只在 composer 聚焦时关)。调度器自己的触发源同样不点名特性:时钟每分钟跑一轮整体 pass(hero 问候语这类跟着时钟变的文案靠它翻页),composer 卡片的尺寸变化经 `reposition('composer')` 派发,卡片上的点击聚焦与消息列表跟随都是 composer 特性的钩子。`retire` 按 name 或 handle 匹配:纯 handle 命中只停 sync、不拆安装(settingsNav 的显式分支——失败计数器已拒绝后续 pass,安装继续跑、设置页不卸);退役 `footer` / `composer` 仍强制归还 body 属性(FOOTER_ATTR / COMPOSER_ATTR)。
108
108
  - **理由**:拆分前「加一个特性」要改两处清单(entry.js 安装序列 + scheduler 的 PASS_FEATURES)再往各触发分支加调用;现在变成往 FEATURES 表加一行、在句柄上实现钩子——scheduler 不再认识任何具体特性,手写清单无从漂移。拆分把千行闭包变成状态自持的工厂加薄编排,750 行上限重新可守。移动就是移动(注释随行、闭包变量变参数才改签名),搬运提交的 diff 因此可审:搬运里不该出现逻辑改动。本决策不推翻 D1/D6/D12:仍是单文件逐字拼接(D1)、仍是单一调度器统一驱动(D6,钩子只是把硬编码调用变成句柄方法)、特性级失败隔离与 retire 语义原样保留(D12)——它在三者之内工作。
109
109
  - **代价**:多一层间接——特性内部状态要经工厂参数表交接,动状态时多过一遍参数;「加一个特性」前要先读这条决策与 scheduler.js 的 typedef。
110
110
  - **重审条件**:钩子表继续膨胀(例如第二类键盘事件或第二种观察源进场)时,重审契约粒度——按事件域分组,或让特性自己声明要订阅的触发器。
@@ -112,7 +112,7 @@
112
112
  ## D14. 账号表面:一套行模型,两个挂载点
113
113
 
114
114
  - **背景**:DSH 0.1.7 的桌面端自带账号区——侧栏页脚里的账号行,以及它自己的账号弹层。此前的做法是隐藏宿主账号行、由插件自建抽屉,并把宿主菜单里的条目读出来镜像进抽屉:隐形点开宿主菜单、等它的 portal 出现、复制文案与图标、再把点击转发回去。这套驱动依赖宿主菜单的渲染时序与双语标签,先后出过图标逃逸、文案过期、行重复等问题。
115
- - **决定**:插件的行(账号头部、其它插件的页脚条目、设置行)只在弹层里出现,弹层有两个挂载点,由 `overrides/account/surface.js` 每轮判定。宿主有账号区时,宿主账号行就是入口(插件只给它打标记,供样式表重绘),插件把容器追加到宿主账号菜单列表的首位,宿主自己渲染它那几行;宿主没有账号区(Web、0.1.5)时,插件自建账号行与弹层,容器就是弹层主体。宿主的行不复制、不移动、不转发点击;插件的行被点击后需要收起菜单时,派发一次 Escape 交给宿主的菜单处理。卡片与行的外观由插件的样式表按标记重绘,两条路径共用同一套样式。
115
+ - **决定**:插件的行(账号头部、其它插件的页脚条目、设置行)只在弹层里出现,弹层有两个挂载点,由 `overrides/account/surface.js` 每轮判定。宿主有账号区时,宿主账号行就是入口(插件只给它打标记,供样式表重绘),插件把容器追加到宿主账号菜单列表的首位,宿主自己渲染它那几行;宿主没有账号区(Web)时,插件自建账号行与弹层,容器就是弹层主体。宿主的行不复制、不移动、不转发点击;插件的行被点击后需要收起菜单时,派发一次 Escape 交给宿主的菜单处理。卡片与行的外观由插件的样式表按标记重绘,两条路径共用同一套样式。
116
116
  - **理由**:宿主的菜单自带定位、动画、键盘遍历、Esc 与外部点击关闭,追加进去的行会加入同一套键盘遍历;删除驱动与镜像之后,账号区不再依赖宿主菜单的渲染时序与标签文案。
117
117
  - **代价**:宿主菜单关闭即卸载,容器每轮重新确认并追加;宿主的列表是 React 管理的子树,追加位置固定在列表首位,宿主重渲染后由每轮同步自愈。
118
118
  - **重审条件**:宿主为账号菜单提供追加行的插槽时,改用插槽注册,去掉 DOM 追加。