dsh-claude-style 0.4.0 → 0.5.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 +81 -1
- package/LICENSE +0 -8
- package/README.en.md +33 -31
- package/README.md +14 -13
- package/docs/STYLE.md +10 -6
- package/docs/architecture.md +27 -1
- package/lib/claude-mark.svg +1 -0
- package/lib/client.js +4416 -652
- package/lib/index.js +297 -57
- package/lib/model-descriptions.json +99 -47
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +7 -5
- package/fonts/GoogleSansFlexPicker.woff2 +0 -0
- package/fonts/OFL-GoogleSansFlex.txt +0 -93
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,86 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [0.5.1] - 2026-09-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **冒烟测试 `npm run smoke`(`scripts/smoke.cjs`)**:零依赖、不需要运行中的 dsh,直接检查构建产物。宿主半边:用假的 cordis 上下文驱动 `lib/index.js`,对私有路由发跨站页面、局域网、DNS 重绑定、浏览器同源、桌面壳转发几种请求,分别走宿主的 `requestRejection()` 与本地替身,核对拒绝与放行、415 / 413 / 400 与数组上限。浏览器半边:在无头 Chrome/Edge 里把 `lib/client.js` 装进一个替身宿主页面(模块加载器、ctx、带账号菜单与两个插件条目的侧栏页脚、按宿主键位处理回车的输入框),核对启动完整、空闲时 0 pass、设置 / 账号 / 插件来的字符串不按 markup 解析、回车交给宿主、宿主 API 出故障时只退役对应特性(安装期与同步期各一例)、teardown(包括 pass 正在排队时的 teardown)不留任何节点、标记、属性与样式表。它第一次跑就抓到了两个此前没人发现的问题:对话页权限按钮让调度器空转,以及 teardown 之后复活的那一轮 pass。
|
|
7
|
+
- **账号行显示真实头像与昵称(桌面端登录)**:桌面端登录后,皮肤从官方 `remote.account.getProfile()` 取昵称(`value.name ?? contact`)与 `avatarUrl`,账号行改绘**正圆头像**(`border-radius: 50%`),照片加载失败时手绘星芒仍作为兜底显示;未登录 / 非桌面端 / 取数失败时保持原有的星芒与已存用户名。账号信息以 **60 秒轮询**保持新鲜(登录、登出、换头像无需刷新页面)。
|
|
8
|
+
- **账号抽屉接管宿主账号菜单**:抽屉按宿主菜单**动态镜像**条目(顺序、文案、图标、禁用态一致),点击**直连官方行为**——设置走宿主设置入口、意见反馈打开官方问卷、退出登录调 `remote.account.signOut()`、登录走 `startSignIn`;未知条目回退为驱动宿主菜单。宿主自己的账号触发器以 `visibility: hidden` 隐藏(保留盒子供转发),不会出现在界面上。
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **0.1.7 设置页选项能显示但存不进去**:浏览器 boot 给每个插件条目生成的是**随机 loader id**(`dsh-client-modules` 的 `create` 只传 `name`),而设置命名空间是本插件 profile 条目 id——此前直接拿 `ctx.fiber.entry.id` 当命名空间,`configForms.get(随机 id)` 绑到一个没人拥有的命名空间上:读停在默认值、写被宿主拒(`No configurable plugin entry`),且因为 `prefsForm` 非空,读路径连自建路由都不再走。现在命名空间从 `configForms` **已服务的名单**里按候选挑(客户端条目 id / 包名 `dsh-claude-style` / patch 声明的 `ui-skin-claude-style`),挑不到就不绑;官方表单没 ready 或被拒时,读 / 写都回落自建 `/dsh-claude-style/prefs` 路由。
|
|
12
|
+
- **斜杠命令 / @ 提及菜单打开时,回车直接把半截文字发了出去**:皮肤在 `document` 的捕获阶段拦截回车、阻止传播并直接点「发送」,宿主输入框(Lexical)的回车处理因此根本收不到这一键——而正是它负责先选中打开菜单的高亮项、照顾输入法(含 Safari 在 `compositionend` 之后才到的那次回车)、忽略按住不放的连发,最后才发送。所有受支持的宿主(0.1.5-rc.2 起)本来就是回车发送,这段拦截没有任何增益,已删除。**实测**:在模拟宿主键位的输入框里对打开的菜单按回车,旧版点了「发送」,新版由宿主选中了菜单项。
|
|
13
|
+
- **关掉插件或热重载后,皮肤又把自己画了回来**:调度器的 teardown 既不取消已经排上的那一帧 pass,也不拦之后的 `schedule()`(例如账号资料请求晚到时)。teardown 发生在 pass 排上之后——流式输出期间几乎每帧都是——这一轮 pass 就会对已拆掉的特性再跑一遍,把账号行、页脚标记、输入框的 body 属性重新建出来,而且再没有人会清理它们。**实测**(冒烟测试在有 pass 排队时 dispose):teardown 之后又冒出 11–38 个皮肤节点。现在 teardown 取消排队中的那一帧,之后的 `schedule()` 一律不生效。
|
|
14
|
+
- **一个特性出错,整张皮就半挂在页面上**:`apply()` 依次装 14 个特性,却在最后才把 teardown 交给宿主;调度器每轮 pass 也不逐个兜底——一个 sync 抛错,排在它后面的全部跳过,而且每轮如此。**实测**:宿主的 `remote.account.getProfile()` 返回非 Promise 时 `apply()` 中途抛错,样式表与 body 属性留在页面上、调度器没装上、teardown 没注册,关掉插件也清不掉。现在 teardown 最先注册,每个特性单独安装、单独同步:装不上或连续 3 轮同步失败的特性在控制台报一次、跑自己的 teardown 退役,其余照常工作;页脚接管与输入框重绘这两处会隐藏宿主控件的接管,在对应特性退役时把宿主原来的界面还回去;只有调度器本身装不上时才整体回滚。模型选择器的 teardown 此前只清变量、不撤 DOM,现在会真正交还宿主的模型座位。**实测**(无头 Chrome):同样的 `getProfile()` 故障下其余特性照常、页脚回到宿主原样;permissions 的 sync 持续抛错时 3 轮后报一次、输入框重绘关闭;teardown 后页面上不留任何皮肤节点、属性或样式表。
|
|
15
|
+
- **调度器在空闲时也每帧跑一轮 pass**:三处自持循环——① 账号抽屉每轮 pass 都把镜像的宿主菜单行删掉重建,哪怕内容没变、哪怕抽屉开着;② 页脚插件条目有两个以上时,镜像条目的排序按正序逐个插到「设置」行前,每插一个都落在已排好的后面,于是每轮都把每个条目挪一遍;③ 对话页(行内输入框)的权限按钮每轮 pass 都把同一段标签文字重写一遍——同值写 `textContent` 照样替换文本节点。前两处只在登录的桌面端或页脚插件较多时出现,第三处在任何一个打开的对话里都会出现(由新增的冒烟测试发现)。而 `<body>` 下的任何 DOM 变动都会排下一轮 pass,所以永不停歇。**实测**(无头 Chrome,真实 `lib/client.js`):空闲时约 160 passes/s(两处叠加约 320),在 2.2 万节点的模拟对话上每轮约 5.4 ms——60 Hz 下约三分之一的主线程在什么都没发生时被吃掉;抽屉开着时行被替换还会吞掉点击。现在账号行只在内容变化时重建、抽屉开着时不动(揭开前补一次同步),排序只挪真正错位的条目,空闲 pass 降为 0。顺带:账号资料晚到时直接请求一轮 pass(此前写一个 body 属性当「推一下」,但调度器的 `attributeFilter` 根本看不到它,一直靠上面的死循环掩盖;连带删掉从未被读取的头像探测图);只在宿主账号触发器存在时才去读它的菜单,读空或失败后最多每 5 秒重读一次(此前每轮 pass 都开关一次宿主菜单);登出后(`getProfile()` 返回 `null`)账号行回到本地用户名与星芒,不再停在旧昵称和头像上。
|
|
16
|
+
- **非桌面端把「在应用中打开」菜单收进账号抽屉**:宿主账号行被皮肤替换后,其菜单锚点不在 DOM 里,而旧的「页脚里第一个非我们的锚点」兜底会挑中「在应用中打开」,把文件资源管理器 / Cursor / VS Code 等条目收进抽屉。现在**按账号标签匹配触发器**(`账号菜单` / account),匹配不到就只显示「设置」,不再混入别的菜单。
|
|
17
|
+
- **「设置」行变成账号名、点击弹出账号菜单**:设置行的标签与点击目标此前兜底接受任意 button,桌面端该槽位上是宿主账号触发器。现在只接受 `aria-haspopup="dialog"` 的按钮,其次接受非 menu 锚点的按钮,**绝不接受 menu 锚点**。
|
|
18
|
+
- **图标型登出按钮没被排除**:宿主登出按钮是纯图标(无文案),此前按文案判断会漏。现在同时读 `aria-label` / `title` / 类名,并跳过宿主账号区容器内的条目。
|
|
19
|
+
- **设置存储误报「不可用」**:宿主注册命名空间晚于页面加载时,启动时的一次性判定会把设置页卡在「存储不可用」。现在读路径**不再做可用性判定**,控件保持可用;只有写入真的失败才提示。
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **松开推条后旋钮会先弹回原档位、再跳回新档位**:宿主的目录快照**滞后于提交**——`selectModel` 往返按秒计(注册了联网供应商时实测 2–8s),在这段时间里快照的 `effective` 仍是**旧档位**;而 `update()` 无条件跟随快照(`selected = next.index; place(...)`),于是松手后第一次调度 pass 就把旋钮拽回起点,等 echo 落地再拉回新档位。这是推条自带的缺陷(不是本轮换脸引入的,只是胶囊旋钮 + 填充段让这段回弹更显眼)。现在提交时置 `pendingEcho` 并记下所提交档位的 id,`update()` 在等待期间不跟随快照;快照真的报出该档位(echo)、梯子换了(换模型,旧 echo 不会来)、或 12s 兜底超时后才恢复跟随。**实测**(无头 Chrome 驱动真实 `createEffortControl`,模拟「松手 → 5 次陈旧 pass → echo」):把守卫摘掉的对照组旋钮轨迹是 `246 → 148 ×5 → 246`(148 即旧档位 High 的停靠位),守卫在场时全程 `246`,`onPick` 只发一次 `max`。另外 `commit()` 本就只在档位真的变化时才提交,所以「拖走又拖回原档」不会多发一次 RPC。
|
|
23
|
+
- **松手瞬间档位名连跳两下、最高档点阵动画先中止再重播**:**主因在 picker 的渲染,不在推条自身**——`renderModelBody()` 每次渲染都先把整个 footer 清空(`while (modelFooter.firstChild) removeChild`)再 `appendChild` 推条,而 DOM 里「移除再插入」会**重启该子树内所有 CSS 动画**:档位名的模糊聚焦、最高档点阵的入场扫入与闪烁相位全部从头再播。渲染签名里带 `status`,一次选择至少触发两次渲染(`selecting` → 回声后的 `idle`),于是正好「跳两下」。现在 footer 的布局交给新的 `layoutModelFooter()`:它只移除**除推条以外**的节点,推条节点始终挂载、动画时间线不被碰;「加载中/空目录」那条分支显式调 `layoutModelFooter(false, false)` 收掉整块 footer,渲染函数本身不再直接动 footer。**次因**是 `settle()` 的作画顺序:先 `paintValue()`、再用 `commit()` 移动 `selected`、然后又补一次 `paintValue()`,而 `paintValue()` 读的是 `selected`——第一次画的是**手势开始前的旧档位**(名字退回旧名并重播模糊、`data-apex` 关掉使点阵入场中止),提交后再画一次才回到新档。现在改成**先 `commit()` 再只画一次**。顺带给 `place()` 加同值守卫:位移与填充宽度没变就完全不碰 DOM,避免每个调度 pass 的重写打断正在进行的 0.18s 滑行。**实测**(无头 Chrome,三套独立证据):**(1) 机制**——把真实控件拆开再插入后 `animationstart` 计数为 名字 +1 / 入场 +2 / 闪烁 +1,证明「重插即重播」;**(2) 修复**——用真实抽出的 `layoutModelFooter` 源码连跑三次渲染,推条从未出现在 `removedNodes` 里(对照组:旧清空写法第一次渲染就把推条拆掉),且结构顺序为 `divider > effort > more`,同时对 `renderModelBody` 做结构断言(不再直接触碰 footer、必须经 `layoutModelFooter`);**(3) 点击轨迹**——挂钩 `textContent` / `setAttribute` / `setProperty` 的写入点,修复前是 8 步(`text:Max → apex:on → text:High → blur → apex:off → text:Max → blur → apex:on`),修复后收敛为 `text:Max → apex:on`。
|
|
24
|
+
|
|
25
|
+
- **拖动时滑块冲出终点线、并与填充脱开**:上一轮给滑块加的拖动放大用的是独立属性 `scale`,而位移仍写在 `transform` 里——两者的合成顺序是 `translate → scale → transform`,也就是 **`scale` 会把 `transform` 里的位移一起放大**:1.1 倍下终点处的 246px 位移被渲染成 270px(滑块冲出轨道右端 24px),中途则整体超前约 10%,于是滑块跑到填充前面、看着像「脱离」。修法是把位移改用独立属性 `translate`(合成顺序在 `scale` 之前,不被放大),过渡列表里的 `transform` 也换成 `translate`。另外填充右端此前是 8px 圆角,它在滑块中心线处收边、上下两角向内退,即使位置正确也会在槽的上下沿露出缝——右端改为直角(左端仍保留圆角与滑槽对齐),而右端始终藏在 16px 宽的滑块之下。**实测**(无头 Chrome,DOM 量测):修复前拖动到最右端 `knobRelLeft=270`(轨道宽 262,即冲出 25px),修复后为 `245`、`rightGap=-1`(仅放大本身的 0.8px),且 `fill→knobCentre=0`、填充右缘始终在滑块左缘之内 9px。
|
|
26
|
+
|
|
27
|
+
- **点击两个档位正中间有概率让档位选择器「崩溃」**:宿主每提交一次档位都会**重新枚举整个目录**(实测 2–8s),这段窗口里快照可以既没有分组、也没有当前席位,`modelEffort()` 于是返回 null;而新拆出的档位碎片把「读不到档位」直接当成「这个模型没有档位」,于是**在提交后的 RPC 期间把触发器和已打开的卡片一起摘掉了**——看起来就是选择器崩掉。老 picker 早就踩过同一个坑(它曾在每次选择时把整个列表清空,见下方 [0.5.0] 的修复),这次是把那条纪律重新漏掉了一遍。修法两步:`ui.model` 新增 `settled()`(「目录现在能不能报出席位」,就是 picker 自己那条判据的唯一来源),碎片在**未 settle 期间既不摘触发器也不关卡片**,并保留最后一次读到的档位梯子(`lastEffort`)继续画,只有**已 settle 且确实没有档位**时才收掉触发器、让控件回到空态。**实测**(无头 Chrome 用 stub 的 `ui` 驱动真实 `installEffortPicker`,模拟「提交 → 目录空白 6 次 sync → 回声」):触发器仍在、卡片仍开、滑块未进空态且档位名保持旧值,回声到达后更新为新档位;把 `settled()` 置真且梯子为空时触发器才被摘除。
|
|
28
|
+
- **点击滑槽其他位置时滑块瞬移**:`onPointerDown` 里先置 `dragging = true` 再写位移,而 `place()` 在 `dragging` 时走「直写」分支(本意是拖动跟手),于是单击远处时滑块**直接跳过去**;即便改成先动画再置标志也不行——`data-dragging` 在同一个任务里就被设上,浏览器在下一帧做样式重算时过渡列表里已经没有 `translate`,动画根本没开始(实测同步读与 40ms 读都是终点值)。现在把「按下」与「拖动」拆成两个状态:**`pressed`** 从 pointerdown 起(此时过渡仍在,滑块 180ms 滑向按下位置,读数与填充一起动),**`dragging`** 从第一次 pointermove 起(此时才加 `data-dragging`、改为直写跟手);`settle()` 与 `update()` 的守卫改看 `pressed`,`isDragging()` 也返回 `pressed`(悬停关闭的豁免要从按下就生效)。**实测**(无头 Chrome):按下后 40ms 时 `getAnimations()` 里有一条 `translate` 过渡在跑(180ms),260ms 到达按下位置、松手精确落在最近档位;只点不动(不触发 pointermove)也能正常提交。
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- **档位名换档改成「交换」动画,不再生硬替换**:此前档位名只是重新赋值 + 一次模糊聚焦;现在旧名会先被复制到一个**绝对定位的 ghost**(位置取自 `valueEl.offsetLeft`,与名字原位精确重合,且不占布局),随后**旧名向上模糊淡出、新名自下模糊淡入**(`translateY(5px)` → 0,`blur(4px)` → 0,280ms)。两条动画都由 override 用 `!important` 内联重置来重播(样式表规则必须带 `!important` 才能压过宿主,普通内联 `none` 会在级联里输掉、重启变成空操作)。拖动中也照此执行:滑块每越过一个档位,名字就像滚动计数器一样换一次。**实测**(无头 Chrome 驱动真实控件):换档瞬间 ghost 文本为旧名 `High`、value 为 `Low`、`ghost.style.left` 与 `value.offsetLeft` 一致(58px = 58px)、两条动画分别在 `running`、360ms 后 ghost 计算透明度为 `0`。
|
|
32
|
+
- **档位两端文案随语境本地化(中文:更快 / 更强)**:此前 `effortFaster` / `effortSmarter` 的 `zh` 值刻意写的英文——按「它们说的是方向、不是档位名」的旧约定保持英文。现按用户要求改为中文语境下显示**更快 / 更强**,英文语境仍是 `Faster` / `Smarter`;只是文案表 `src/model-descriptions.json` 的数据改动,控件本身照旧走 `copyLabel()`,不需要改 JS。**实测**(把真实文案表注入预览页后读 DOM):两端渲染为 `更快` / `更强`。
|
|
33
|
+
- **最高档的点阵动画:纯哈希粒子**(这条改了四轮,最终回到第一版):最终形态是每块方块按自己的**哈希相位**闪烁,周期长度也各自抖动 ±8%,色调从 8 档里哈希取一档——**没有任何排序或方向**,读起来是一片各自为政的粒子;羽流形状(右端实、向左渐隐)由**逐块静态透明度**承担,颜色闪烁从它中间穿过,两者互不干扰。中途试过三种「有方向」的版本并被逐一否掉:严格按距离排序的波(像一根横条扫过去)、流动项 + 随机项(刷新向左偏置)、以及整片持续左移的传送带(那是真的在动,不是刷新倾向)——用户最终确认第一版的纯随机粒子最好,故全部回退,只保留动画效果本身的回退,其余(档位独立弹层、圆角、拖动放大、阻尼、触发器间距)不变。**实测**(无头 Chrome 读真实方块):无传送带节点、无滚动动画;260 块、相位散布 **1.37s**、周期 **1.334–1.565s**(抖动在)、**8** 种随机色调;列位置与平均延迟的相关系数 **-0.039**、左右三分之一落差 0.008s(即**没有方向偏置**);逐块透明度左 1/3 均值 **0.101** / 右 1/3 **0.998**(羽流形状);容器无 `mask-image`。
|
|
34
|
+
- **座位里模型与档位两枚触发器靠拢**:拆成两枚按钮后,两边的对称内边距在名字和档位之间留了 16px 的空档,读起来像两个不相干的控件;现在档位触发器 `margin-left: -6px` + 左内边距 2px,实测**名字与档位之间只剩 4px**,两枚读作一句「Opus 5.5 High」。
|
|
35
|
+
- **推理等级从模型弹层拆出来,成为独立的触发器与弹层**:此前模型选择器的 footer 里同时住着模型列表和档位推条——选模型和定思考力度是两件节奏完全不同的事(模型很少换、档位常微调),却共用一次开合与同一张卡片。现在座位上是**两枚并排触发器**:左边模型名(开模型弹层)、右边当前档位名(开档位弹层,用静音色,模型是主语、档位是修饰);模型弹层里只剩模型列表 + 更多模型(分隔线仍按「有没有更多模型」决定画不画),档位推条搬进自己的卡片。**结构上按仓库的 750 行停止线拆出一个新碎片** `src/overrides/effort-picker.js`(199 行):它持有档位触发器、档位卡片与开关/定位/清理,只向 `ui.model` 要三样东西——座位元素 `seat()`、档位描述 `effort()`、提交 `pickEffort()`;`model-picker.js` 相应摘掉推条、`effortSlider` 与 footer 里的滑块逻辑(`layoutModelFooter(showMore)` 现在只画分隔线 + 更多模型)。上一轮为「picker 重渲染拆节点导致动画重播」立的纪律跟着搬到新卡片:滑块节点建好后**永不重挂**,卡片每次 sync 只更新数值与位置。两枚触发器与两张卡片都遵守既有弹层纪律:`autoPopover === 'all'` 时悬停开、否则点击开,开一张关另一张,点击外部 / Esc 关闭(scheduler 新增 `ui.effort` 的 sync / owns / close 三处接线)。**实测**(无头 Chrome 用 stub 的 `ui` 驱动真实 `installEffortPicker`,16 项断言全过):触发器落在模型按钮右侧且标签随席位回声更新、点击开卡片并把模型卡片关掉、连续 6 次 sync 后滑块**从未**出现在 `removedNodes` 里、拖到第二档会经 `pickEffort('low')` 提交、模型无档位时触发器摘除且卡片关闭、teardown 两个节点都清理;另用真实抽出的 `layoutModelFooter` 源码断言新 footer 形状为 `divider > more` / 空。
|
|
36
|
+
- **推条手感三处打磨**:**(1) 圆角收小**——滑槽/填充/点阵从 999px 全圆改为 8px 圆角矩形,滑块从胶囊改为 5px 圆角矩形(录屏里滑槽端部约高度的 1/3、滑块顶边有明显平段,此前的全圆比 Claude 更圆)。**(2) 拖动放大 10%**——按住时滑块 `scale: 1.1`、松手回 1,0.12s 缓动;`translate` 被排除在过渡之外(拖动每帧写它),所以放大/缩回平滑而跟手不受影响。**(3) 档位点阻尼**——指针位移经阻尼曲线 `t − 0.8·sin(2πt)/2π` 映射到滑块:档位点附近滑块只走指针约**五分之一**的速度(斜率 1−D),段中点则以 1.8 倍速度反超(「离开档位点有阻尼」),段中点仍 1:1 跟手、后段略超前,视觉上像被吸向下一档;曲线在每个档位点与每个中点都精确不动且单调,所以「最近档位」的判定仍可用原始指针位置,松手也照旧精确落在档位上。**实测**(无头 Chrome 驱动真实控件):f=0.125 时指针 6px / 滑块 2px(33%),f=0.25 时指针 12px / 滑块 6px(50%),f=0.5 时两者同为 25px,f=1.0 时同为 49px;四处圆角 computed 为 8/5/8/8px;拖动中 `scale=1.1`、松手后为 1。
|
|
37
|
+
- **推理推条换脸成 Claude Desktop 同款:填充段 + 档位刻点 + 胶囊旋钮,最高档触发点阵动画**:对照 Claude 的真机录屏逐像素量过——滑槽不是此前的 14px 浅槽配圆珠,而是一条 **26px 的粗槽**,旋钮是 **16×30 的竖直圆角滑块**(上下各探出槽 2px;圆角随后又收小,见下方手感打磨一条)、两态都钉在纯白上(暗色里深旋钮会沉进槽里);槽里**从左端到旋钮中心铺一段填充**(标签墨色 26% 透明度,一条规则同时喂亮暗两态),**每个档位在槽心留一枚 4px 刻点**,刻点与填充同色——划过的刻度被填充吞掉、当前档被旋钮盖住,刻点只在裸槽上露面。**最高档是整条的华彩**:到达最后一档(拖动中的实时位置也算)时填充与刻点退场,槽内浮起一片**点阵**——5 行约 4px 的方块按整设备像素解算网格(不出现 7/8px 交替缝),每块带着哈希撒开的相位在深紫与更亮的峰值色之间闪烁、左端按掩码淡出回槽色、进入时从右向左扫入,旋钮同时染上紫光晕、档位名变紫——这正是 Claude「Ultracode」那一档的招牌动效,且「最高档即特殊档」(点阵技法参考了社区插件 domitor-syh/dsh-ui-skin-switcher 的实现:整设备像素网格、雪崩哈希散相、右起扫入)。**紫色是对仓库「陶烬橙唯一强调色」的一次显式破例**(用户指定:最高档沿用 Claude 的 Ultracode 紫,不做橙色转译),色值从录屏里量出——方块深紫 ≈ rgb(150,127,200)、亮紫 ≈ rgb(208,203,228)、档位名 ink 更深,落在 --dsh-claude-apex / --dsh-claude-apex-flash / --dsh-claude-apex-ink 三个令牌上(亮暗各一套)。**方块的闪烁色一律写在样式表里**:override 只给每块挑一个色调档(`data-tone`,四档从深紫铺到浅紫),行内只留动画相位——这条是有代价换来的:早先把逐块峰值 rgb 写在行内 `--dsh-flash-light` 上,样式表一换代,旧一代 DOM 里的行内橙色值不会跟着变,于是出现「底色已经变紫、闪烁却还是橙色方块」的残留态(实测新构建全新加载为 90 种颜色、0 个橙色像素,问题只出在热替换残留的节点上)。颜色回到样式表后,任何一次换表都会把每个方块一起重染。档位名在**非拖动**的换档时重播一次 280ms 的模糊聚焦;拖动中跨档保持清晰不糊。无档位的模型行为不变(旋钮照拖、松手回右端、不铺填充)。键盘路径(方向键/Home/End)同样走完填充/刻点/点阵的全套状态。
|
|
38
|
+
- **输入框底部的「背景色阴影」改成座位自己画的一条横条 + 一条渐变**:此前让消息在输入框一带淡出,靠的是在卡片(以及输入域)上刷三层 `--dsw-alias-bg-base` 阴影——亮/暗/聚焦/带附件每一态都要重写一遍(共 12 处),凡是重置卡片 `box-shadow` 的规则都得跟着重述(附件轨因此只能写成无阴影,hero 还得用四条规则把阴影剥掉)。现在由宿主那个 `[data-composer-seat]` 自己承担:座位整块刷成画布色,于是**横条高度就是座位高度**(卡片 + 工具行 + 统计句 + dock),不需要跟任何盒子对齐;座位顶边之上挂一条 `--dsh-composer-fade-h`(40px)的渐变,由画布色向上淡到透明。这也正是宿主自己的做法——它在 active 态本就给座位铺了一条 36px 的同款渐变,只是没盖住座位下半段,消息才从工具行后面透出来;皮肤现在只是把座位补满。`--dsh-composer-fade-h` 同时是消息区的底部留白(`viewArea` 的 `padding-bottom`),所以最后一轮永远停在渐变带的上沿,不会半行被吃掉。
|
|
39
|
+
- **模型介绍文案整体校订一轮**:事实未动,只修写法。**(1) 一处中英语义冲突**——`kimi-for-coding-highspeed` 中文括号读作「常规版约 180 token/s」、英文却说高速版自己跑 180 tokens/s,两句相反;现统一为「高速版约 180 tokens/s,为常规版的 5–6 倍」。**(2) 单句措辞**——`doubao-seed-2.1` 补上缺的「以」并消去重复的「智能体」、mercury「快达同类的 10 倍」、doubao「多模态可用」、hunyuan「中文与 Agent 场景均衡」等生硬或中英夹杂的句子改写通顺;`gpt-5.4` 的「原生电脑操作」改为「原生 computer use(电脑操控)」。**(3) 全文统一**——速度一律 `tokens/s`(TPS、token/s 不再混用)、上下文一律大写 `K`(256k/128k 消除)、价格英文一律 `per MTok`、「开放权重」取代混用的「开源权重 / open / open-source」、「编程」取代混用的「编码」、「图像」取代「图片」、「智能体」取代残留的英文 Agent 与「代理式」。**(4) 向仓库约定收口**——celeris 家族规则里钉死的 Celeris-1 数字(1,664 tokens/s、MMLU-Pro 75.9%)挪进新增的 `celeris-1` 精确条目,家族文案改为版本无关的「扩散架构大语言模型」,下一代 Celeris 不再继承过时数字;gpt 兜底删去「最新模型均」的时效断言。另修正两处中英不对齐:deepseek 兜底的「普惠」英文实为 affordable 而非 efficient、doubao 的「千亿级」英文实漏两个数量级。deepseek pro「比肩顶级闭源模型」(官方标题)与 mistral「前沿模型」两处擦边最高级保留不动。
|
|
40
|
+
|
|
41
|
+
### Security
|
|
42
|
+
- **用户名、账号昵称、头像地址与页脚插件条目不再拼进 HTML**:账号行、封号彩蛋的提示条、账号抽屉镜像的页脚插件条目,此前把这些字符串直接拼进 `innerHTML`——用户名来自设置(任何能写入设置的人都能改它),昵称和头像地址来自账号服务,条目文案与角标来自第三方插件。**实测**:把用户名设成 `<img src=x onerror=…>` 后,在无头 Chrome 里加载真实的 `lib/client.js`,这段脚本在 GUI 页面里执行了——而 GUI 页面能驱动会执行 shell 命令的智能体。现在这些值一律以文本写入(`textContent`),图标改为复制宿主/插件自己的节点、不再重新解析 markup。头像改为一个真正的 `<img>`(`referrerPolicy="no-referrer"`,与宿主自己的头像一致),只接受 http(s) 地址并以属性写入。顺带修好了**头像从未显示**:旧代码用 `JSON.stringify` 把地址拼进 `style="…"`,双引号提前截断了属性(实测解析结果只剩 `--dsh-claude-account-photo:url(`,地址碎成几个无意义的属性名);而且头像只在账号行首次创建时写入,账号信息晚到一步就再也不画。现在每次 pass 同步,加载失败时图片自行隐藏、露出下面的星芒。镜像条目的角标后来才出现时也会补上(此前只在建条目那一刻读一次)。
|
|
43
|
+
- **设置与用户名路由不再对任何人敞开**:宿主半边的 `/dsh-claude-style/prefs`(读写设置)与 `/dsh-claude-style/username`(系统用户名)挂在宿主 web server 上,而插件路由不经过宿主给 `/api` 的鉴权。**实测**:一个带陌生 Host / Origin、`Content-Type: text/plain`、不带 cookie 的 POST(跨站页面无需预检就能发出,局域网里也能直接 curl)返回 200 并写入了用户名;同样的请求还能读到全部设置命名空间与系统用户名,且请求体与快捷供应商数组不设上限(2 万条照单全收)。现在这两条路由先过宿主自己的请求栅栏(`connection.requestRejection()`:Host/Origin 校验 + 浏览器会话 cookie,与 `/api` 同一套,0.1.5-rc.2 起即有),写入还要求 JSON 内容类型、请求体不超过 16 KiB、快捷供应商至多 64 个;宿主没有该服务时退回只服务回环的本地栅栏。已核对浏览器同源请求与桌面壳转发的请求都能通过宿主真实的栅栏,跨站与 DNS 重绑定的请求被拒绝。模型文案与字体仍公开。
|
|
44
|
+
|
|
45
|
+
## [0.5.0] - 2026-09-22
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
- **插件页有了自己的图标(DSH 0.1.7)**:0.1.7 的插件清单会读 `package.json` 的 `icon`——必须是**相对清单的路径**、SVG/PNG/JPEG/WebP、realpath 后仍留在包目录内、且不超过 256 KiB(绝对路径与 URL 一律拒绝);宿主把文件读成 base64 data URI 交给插件卡片与详情页上的 `<img>`,失败只在这张卡片上留一条 `meta.error`,不影响插件本身。这里填的是**陶烬橙的 Claude 星芒**:源文件 `src/assets/brand/claude-mark-clay.svg`,构建期复制到 `lib/claude-mark.svg`(与模型文案同一套「源在 `src/`、`lib/` 是产物」的做法,`files` 里的 `lib` 已覆盖它,无需新增条目)。选 clay 版而不是纯黑版是有原因的:图标以 `<img>` 渲染、拿不到 `currentColor`,纯黑星芒在暖黑画布上会消失,陶烬橙两态都读得出。
|
|
49
|
+
- **插件卡片有了本地化的标题与描述(DSH 0.1.7)**:同一套元数据读取还会解析 `<包>/locale/en.json`,并以它所在目录为锚点枚举其余 `<语言>.json`,每个文件取 `meta.title` / `meta.description`(必须是非空字符串,文件名必须是语言 id),最后交给客户端一个 `{ en, zh, … }` 记录——`en` 永远兜底到 `package.json` 的 name/description,其余语言按 shell 语言取。此前卡片上显示的是包名 `dsh-claude-style`,现在 `locale/en.json` 给 `Claude Code Style`、`locale/zh.json` 给 `Claude Code 风格`,描述也一并本地化,`locale` 加进 `files`。**这里有个必须避开的陷阱**:宿主是逐个文件走包 `exports` 解析的,任何一个 locale 文件没被导出都会在枚举目录时抛错,而这个错会被外层捕获、把**整份元数据(含图标与两段文案)一起降级成 `meta.error`**——所以 `exports` 加的是通配 `"./locale/*": "./locale/*"` 而不是逐文件列举,以后新增语言文件不会漏。
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
- **侧栏「工作区」标题改成 进行中 / 已归档 分段控件**:宿主本来就带这个筛选(视图选项 → 显示已归档 / 仅显示已归档),它的树会自己把已归档对话连同标题、时间一起渲染出来——所以这里没有再写一份归档列表,而是**去驱动宿主那个筛选**:分段控件替掉原来的「工作区」文字,点「已归档」时打开宿主的视图选项菜单、把筛选切到「仅显示已归档」再关掉菜单,点「进行中」时切回默认;控件的选中态则**从树里读回来**(树的会话行里出现已归档 id 就算已归档),因此从宿主菜单里手动改筛选,控件也跟着变。归档 id 由归档插件的 remote 提供(`ctx.get('remote.workspaceRegistry')`,按名字宽松取用,不声明别的插件的命名空间)。另外**给已归档的会话行挂一个 SVG 删除按钮**:绝对定位挂在宿主行右侧(不改宿主行布局),静止透明、指针落到行上才浮现,行上的时间戳同时淡出给它让位;点击直接调 `deleteSession`,成功后立刻把按钮摘掉。实测(0.1.7-alpha.1 真实 GUI):分段控件文字与选中态随点击翻转、「进行中」时树里 0 行已归档且无删除按钮、宿主筛选被切到「仅显示已归档」后树只列已归档分组、点回「进行中」树恢复 6 行活跃会话;删除请求以**打桩的传输**验证——请求带着正确的 sessionId 发出,随后按钮从行上摘除,真实会话未被删除。**注意**:宿主在已归档态按「分组方式」分组(当前是「按工作区」),组是折叠的,所以要先展开某个工作区才能看到里面的已归档对话与删除按钮;想要一进来就平铺,把分组方式切成「单列表」即可(这一条未做自动切换,见下方待办)。
|
|
53
|
+
|
|
54
|
+
### Removed
|
|
55
|
+
- **模型行的厂商字体(Google Sans Flex)整块移除**:此前只有 Gemini 行会把名称换成 Google Sans Flex(一枚自子集化的拉丁字体),其余行都是界面字体——一个厂商有字体、别的厂商没有,这不是「厂商标记」而是一个特例,何况模型行已经在用厂商锁定标表达同一件事。现在模型行一律用界面字体:字体资产(`fonts/GoogleSansFlexPicker.woff2` 与它的 OFL 文本)、子集化脚本 `scripts/slim-google-sans.py`、`--dsw-font-brand-gemini` 令牌与对应的 `@font-face`、宿主字体白名单里的那一条,以及 package.json 的 `files` 条目一并删除,npm 包小约 9 KB。
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
- **推理等级从二级弹层改成一级弹层底部的推条**:此前「推理等级」是一行下钻,点开二级弹层再从列表里选档;现在它是一条推条——标签后面跟着当前档位名,两端写 `Faster` / `Smarter`(这两个词在任何界面语言下都保持英文:它们说的是方向,不是档位名,档位名就在标签旁边)。推条是**无极滑动**的:拖动时旋钮跟手走,松手或指针离开控件时才对齐到**最近的档位**,所以从一档到另一档不会像开关那样硬切。模型没有任何档位时推条照样显示——旋钮能拖、不改变任何值、松手回到右端,一级弹层底部因此永远长成同一个形状。键盘同样可用:方向键逐档、`Home` / `End` 到两端,推条带 `role="slider"` 与完整 aria 值。二级弹层此后只剩「更多模型」。
|
|
59
|
+
- **`model-picker.js` 拆出两块,回到仓库的 750 行停止线内**:推条控件进新碎片 `src/overrides/model-effort.js`(它只吃一个 `read()` 回调,自己把目录的档位表映射成推条的刻度),模型文案解析(精确条目 → 家族规则 → 档位规则 → 目录自带文本)进新碎片 `src/overrides/model-copy-lookup.js`(纯查表,不碰 picker 闭包)。拆完 `model-picker.js` 791 → 720 行;随二级档位列表一起消失的还有 `modelSubKind` 那套「二级弹层当前是哪一种」的状态与 `noEffort` 文案。
|
|
60
|
+
- **推条的滑槽与旋钮几乎等高**:滑槽从 6px 细线改为 14px 圆角槽,旋钮 16px 不变,对齐 Claude 真机 Effort 推条的比例——6px 的槽配 16px 的旋钮,控件读成「细线上骑着一颗珠子」;等高之后整条才读作一个滑杆,旋钮是嵌在槽里而不是浮在线上。纯样式改动:拖拽几何读的是轨道宽度与旋钮直径,不随槽高变化。
|
|
61
|
+
- **弹层悬停时间统一:停留 50ms 打开、离开 100ms 关闭**:此前打开一律是**即时**的(指针扫过触发器就展开),关闭各有各的延迟(账户抽屉与权限弹层 150ms、模型选择器 180ms、首页 hero 菜单与统计卡片 160ms)。现在 `createHoverIntent` 两侧都可调度:`scheduleOpen` 先等 50ms 的停留(扫过的指针不再展开任何东西),`scheduleClose` 等 100ms(够指针跨过触发器与卡片之间那道缝,且 `scheduleClose` 会顺手撤掉未到期的打开,所以离开只需要调一次);两个值作为 `POPOVER_OPEN_DELAY` / `POPOVER_CLOSE_DELAY` 放在共享碎片 `popover-utils.js`,模型选择器(含「更多模型」二级弹层,指针扫过 cell 也会撤销待打开的二级)、权限弹层、账户抽屉、首页 hero 菜单(它的悬停是委托 `mouseover` 去点宿主触发器,停留同样加在这一步)全部改用它。统计卡片的**关闭**并入 100ms;它的**打开停留**仍是你上一轮定的 300ms(触发区在工具栏行正中、是指针过路的必经之地,50ms 挡不住误触)。实测(0.1.7-alpha.1 真实 GUI,页面内时间戳,并按同环境 `setTimeout` 校准):模型选择器 104ms / 权限 101ms / 账户 128ms / 二级弹层 117ms(该次校准下裸 `setTimeout(50)` 就要 101ms),关闭 134–154ms(裸 `setTimeout(100)` 为 135ms)——即两侧都落在配置值上,多出来的部分是 headless 的定时器对齐。
|
|
62
|
+
- **「更多模型」二级弹层收口,模型不支持思考时不再画推条**:四处调整。**(1) 底对齐**——二级此前与一级**顶部对齐**,内容少时它吊在一级上半部、下方空出一段,指针从「更多模型」那一行挪过去要穿过这段空白;现在二级的**底边与一级底边对齐**(内容比一级还高时仍保留自己的顶边,免得被推出屏幕上沿)。**(2) 不重复供应商**——二级此前列**全部**供应商,一级已经露过面的会再出现一次;现在只列一级**没有**以分组形式显示的(官方服务 + 设置页勾选的快捷供应商)。只以**当前席位**一行出现的供应商**不算**「已显示」——那一行只有一个模型,把它的其余模型藏到二级之外就再也够不着了。**(3) 没得列就整行去掉**——一级的分组已经把全部供应商显示出来时,二级只会是一张空卡,于是「更多模型」这一行不再画。**(4) 不支持思考就不画推条**——目录里没有 reasoning 元数据的模型,此前照画一条无档位可停的推条(旋钮能拖、松手回右端),现在底部只剩分隔线。**(5) 底行空了连分隔线一起收掉**——分隔线的作用是把模型列表和它下面的东西分开;推条与「更多模型」都不画时,列表下面没有任何东西可分,一条孤零零的线只会读成多余的横线,于是整块底行(分隔线 + 两行)一起不画。实测(0.1.7-alpha.1 真实 GUI):二级底边与一级底边差 **0px**(顶边差 96px,即短的一级二级确实按底对齐);当前席位是 `r4-code/deepseek-v4.1-flash`(一级 = 官方分组 + 它的席位行)时二级只剩 `R4 Code` 一个分组、`DeepSeek` 不再重复出现;当前模型 `r4-code/deepseek-v4.1-flash`(无 reasoning,二级非空)底行为 `[分隔线, 更多模型]`,切到 `deepseek-official/deepseek-flash`(有 reasoning)底行为 `[分隔线, 推条, 更多模型]`。「推条与更多模型都不画」这一态需要一级已列全部供应商,本机实例的皮肤偏好写不进去(0.1.7 下 `settings.register` 缺失导致命名空间未注册,属另一处在办的事),所以这一态由条件本身保证(`if (showEffort || showMore)`)而未做现场对照。
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
- **真圆被画成鹅卵石:`corner-shape` 是继承的**:页面从 `<html>` 继承 `corner-shape: superellipse(1.5)`(超椭圆 = 鹅卵石),于是任何 `border-radius: 50%` 的盒子都渲染成鹅卵石 —— 新会话行那枚圆形 chip 就是这样。宿主自己的圆形元素(spinner / dot / tag / toggle / thumb)都**显式**写了 `corner-shape: round`,皮肤照同一做法给六处真圆与滑槽补上声明:新会话 chip、会话行空槽圆点、进行中状态环、封禁页步骤序号、设置页开关钮、推条滑槽与旋钮。实测 chip 的 computed `corner-shape` 从 `superellipse(1.5)` 变为 `superellipse(1)`(Chrome 把 `round` 序列化成一阶超椭圆,即正圆)。**圆角矩形不受影响**:卡片与弹层的 6px / 12px / 18px 圆角仍按宿主的超椭圆语言渲染,只是「真圆」不再被拉成鹅卵石。
|
|
66
|
+
- **右侧边栏全屏展开时,对话区顶栏的 preset 标签与「N 个后台任务」会浮在面板之上**:`chrome.css` 给 `[data-slot="conversation.session.header.actions"] > *` 写了 `z-index: 100 !important`,本意是「压过消息层」,但它与同一段里的三个 composer overlay 处境不同——队列 dock、目标条、输入 dock 都长在 `.composerSeat` 内部(`position: sticky` + `z-index: 7`,自成一个层叠上下文),100 只在这个上下文内部排序,对整页没有影响;顶栏动作不在任何层叠上下文里,100 直接对整页生效。而全屏面板的覆盖靠 ui-dockkit 的 dock 层(`--dsh-dockkit-dock-layer`:普通 10、全屏 40),40 < 100,于是这一行穿过面板露出来。现在把这条规则降到 **9**:高于消息层(置顶消息头 7、回到底部控件 8),低于面板的 10 / 40,规则本意保留、穿透消失。宿主(DSH 本体)无需改动——它的对话区自身层高不超过 10。
|
|
67
|
+
- **热重载后模型选择器会失去点击效果**:HMR(及任何「清扫上一代而不销毁本代」的路径)会把上一代留下的弹层节点从 DOM 里拿掉,而新一代闭包里的 `modelPop` / `modelSubPop` 引用还指着那个已被摘除的节点——重建条件只写了 `=== null`,引用非空就不会重建,于是点触发器时 `data-open` 被写在一个不在文档里的节点上,弹层永远不出现,也就是「点击没效果」。本次把 `ensureModelChrome()` 的判空改成「为空**或已脱离文档**」(与账户区同款幂等护栏),重建时同时重置渲染签名让下一轮 pass 把内容画进新节点;触发器本就有同样的护栏,两级弹层补齐后选择器在双代并存期间也能自我修复。
|
|
68
|
+
- **宿主半边的资产路由在热重链后整代失联,选择器整代渲染成「无锁定标、英文标签」的降级态**:`lib/index.js` 原本在 `ctx.get('webServer')` 已就绪时直接 `registerRoutes(ctx)`,而 `ctx.webServer` 属性访问受 fiber 的 inject 声明门控——启动时 webServer 往往尚未挂载,走 inject 分支一直正常;但客户端 bundle 重建触发 generation 重链后,宿主半边会在 webServer 已运行时重挂,`ctx.get('webServer')` 宽松读到服务、`else` 分支被选中,三处注册全部抛 `cannot get property "webServer" without inject`(宿主日志可见成串告警),模型文案/偏好/用户名三条路由整代没有注册。浏览器半边每代只拉一次文案、失败不重试,于是那一代的选择器没有厂商锁定标、标签回退英文常量、简介降级到目录自带的英文文本,直到下一次重挂/HMR 重新拉到文档才恢复。现在无条件经 `ctx.inject(['webServer'], registerRoutes)` 注册:注入作用域带声明,启动与热重链两条时序都落在能工作的路径上;没有 webServer 的宿主只是等待,防御性契约(无 webServer 也照常激活)不变。
|
|
69
|
+
- **推条拖动掉帧、不跟手**:拖动路径在每个 pointermove 上都做一次强制回流——`place()` 先写 `transition: none`、再写位移、`void offsetWidth` 强制同步排版、最后把过渡还回去,可这套舞步是多余的:拖动期间 `data-dragging` 本就让样式表用 `!important` 关掉过渡。pointermove 的触发频率高于帧率,等于每秒上百次强制排版;每次 move 还无条件重写档位名的 `textContent`(同值写入也是一次 DOM 变更),喂给调度器的 MutationObserver;而调度器的 pass 自身又在无差别重写触发器 aria-label(正落在观察器过滤名单里)、模型名与档位两个 textContent、以及开着时弹层的 left/top——同值写入照样排队,pass 于是每帧自续一轮,弹层每次重定位都把排版弄脏,拖动的下一次几何读取只能强制重排。现在拖动位移并入每帧一次的 rAF(先读边界与几何、后写位移,松手前先落未决帧再结算档位),`place()` 在拖动期直写 transform,档位名/触发器标签/弹层定位全部加同值护栏——空闲时的 pass 循环随之断开,拖动每帧只剩一次 transform 写入。
|
|
70
|
+
- **二级弹层悬停打开后不随指针移开而收起**:「更多模型」的二级弹层是 hover 打开的,但它的归属判定缺了半边——`modelSubPop.mouseleave` 只负责排队全关,而指针一进一级弹层 `modelPop.mouseenter` 就无条件取消定时器,没有「收起二级」的动作;于是从二级弹层移到滑块(一级弹层 footer 内)时,定时器被取消、二级弹层保持打开,挡在旁边。更隐蔽的是另一条路径:hover cell 打开二级后,指针在一级弹层**内部**移到滑块——全程不穿越弹层边界,连 `mouseenter` 都不会触发,二级永远不收。而「指针回到一级、二级该收」本就是既有语义(`openModelPopover()` 打开一级时就显式关二级),只是 hover 路径漏了这一步。现在在一级弹层上委托 `mouseover`:二级开着时,指针落到入口 cell 以外的任何位置(滑块、模型行、卡片空白)即收起二级,一级保持打开;用委托而非 `mouseenter` 正是为了覆盖「cell → 滑块」这种纯内部移动。cell ↔ 二级弹层之间的往返不受影响(悬在入口上不收,跨过间隙进二级照常保持)。
|
|
71
|
+
- **切换推理档位后模型选择器会「一段时间不可用」**:推条每次落档都走宿主同一条 `selectModel` RPC,而宿主在整个往返期间把目录标记为 `selecting`——我们把这个状态当成「没有数据」,于是把整张卡片清空成一行「正在加载模型…」,分隔线、推条和「更多模型」一并被摘掉,要等宿主应答才重建。问题在于这个往返在插件注册的供应商上要**按秒计**:实测 `antigravity/gemini-3.8-flash`(用户当时的默认模型)2.0–3.2s,`kimi-code` 最高 7.7s,`deepseek-official` 也要 2.0–2.5s,所以每推一次档位,选择器就空转同样长时间。根因不在某个插件抢 DOM,而是我们的渲染判据错了:一次进行中的选择**不改变目录里有什么**,模型列表与档位表都还有效。现在 `renderModelBody()` 只在「解析不出当前席位且没有任何分组」时才显示加载行;已有列表时 `selecting` / `loading` / `idle` 一律继续画列表与 footer(进行中的选择在触发器上也不再置灰——置灰只留给「连席位都叫不出名字」的状态)。顺带说明慢的来历:宿主把每次选择(含纯档位切换)写进 `agent-default-model` 设置,广播 `settings/document-updated` 后重新枚举整个目录——实测每次选择触发 2–3 次 `modelCatalog`,每次 1.8–6.4s,而枚举会对每个供应商调用 `listModels()` 与逐模型 `resolveModelInfo()`,`@eddyskywalker/dsh-chatgpt-subscription` 注册的几个供应商在这里联网抓目录,这就是 RPC 慢的主因(宿主与对方插件的行为,本插件侧无法消除,只是不再被它冻住)。
|
|
72
|
+
- **composer 获焦时 `ui.heroMenu.close is not a function` 每次必抛**:`hero-menu.js` 只导出 `sync` 与 `reposition`(首页菜单的生死归宿主悬停状态管,`syncHeroMenu` 负责发现它消失),调度器的 `onComposerFocusIn` 却无条件调用 `close()`——于是每次焦点落进对话卡片都抛一次 TypeError,该处理器余下的收起动作也随之中断。改为与上一行 `closeStats` 同样的存在性护栏。
|
|
73
|
+
- **上下文标记在 DSH 0.1.7 上被落在「第二层」,现在归位到模型触发器右侧**:0.1.7 把输入条的 composer 重构成「卡片 + dock 行」两层——**上下文占用表(`ContextMeter`)从卡片内 `trailing` 组(0.1.5 里它挨着发送键)搬到了卡片下方新的 dock 行**,和会话统计药丸同一个 `conversation.composer.dock` 槽;本皮肤只把统计药丸搬进工具栏行(`mergeStatsIntoRow`),没人管那颗环,于是它独自留在 dock 里、按宿主的 `justify-content:center` 居中成卡片下方**单独一行**(截图里「◯ 42%」那条)。现在新增 `mergeContextMeterIntoRow()`:识别方式是它那颗触发按钮——`button[aria-haspopup="dialog"]` 且**整个标签就是占用读数**(`42%`,统计药丸永远不会是),因此不依赖宿主按构建哈希的类名;找到后插进 `trailing` 里**模型触发器所在簇的右侧**(触发器右侧、发送键之前),并打上 `data-dsh-claude-context-meter` 供样式表挂钩。旧宿主把环留在卡片里,这里找不到可搬的东西、不做任何事;皮肤关闭 composer 重绘时按统计药丸同样的路径把它放回 dock。实测(0.1.7-alpha.1 真实 GUI):`trailing` 子节点顺序为 `standardControls → 环 → activity → primary`,即环在模型触发器右侧;植入标签为 `95% Cache` 的假药丸原地不动(负对照)。
|
|
74
|
+
- **composer 底行三种控件统一字型**:环、模型触发器、统计句原本各带一套字(宿主环用 `--dsh-content-font-size-secondary`、统计 12px/400 tertiary、模型触发器 13px/500 secondary),挤在同一行里读起来像三种控件;现在统一为**同一字体、13px、500 字重、20px 行高、`--dsw-alias-label-secondary` 同色**(统计的内层按钮经既有的 `font: inherit` 继承,环的触发按钮因宿主自带颜色与字号而单独点名)。实测三者 computed style 完全一致(13px / 500 / 20px / `rgb(110,106,96)`)。
|
|
75
|
+
- **统计句改为自适应宽度**:此前 `flex: 1 1 auto` 让统计**盒子**填满左右两组控件之间的全部空隙(文字居中,但 hover 底板横跨半个行);现在 `flex: 0 1 auto` + `margin: 0 auto` —— 盒子贴合文字、由自动外边距在空隙里居中。实测文字中心与改动前一致(788px),盒子从整段空隙收窄到 259px(文字宽),底板只覆盖文字本身。
|
|
76
|
+
- **DSH 0.1.7 上设置静默失效:偏好改走官方 Config 表单,并按宿主世代分流**:0.1.7 删掉了 `settings.register(ns, schema)`——命名空间不再是插件自取的名字,而是 profile entry id,schema 就是插件导出的 `Config`,且只有 `.volatile()` 字段进表单、值写进 profile 的 Cordis patch。旧代码在 0.1.7 上抛 `register is not a function`(被 try/catch 吞成一条 warn),于是 `claude-style` 命名空间从未存在:`/dsh-claude-style/prefs` 永远返回默认值,写入抛 `No configurable plugin entry "claude-style"` 又被路由当成冲突,设置页还在但开关点了不生效、重启即丢。现在宿主半边导出 `Config`(八个偏好字段,`volatileField()` 逐字段探测 `.volatile()` 存在才加标记——桌面端内置的 schemastery 3.18.2 没有它,硬加会直接抛错;schemastery 本身用顶层 await 守卫导入,解析不到就让 `Config` 为 `undefined`,皮肤照常加载),新宿主把命名空间取成 `ctx.fiber.entry.id`(读不到回落 patch 里的 `ui-skin-claude-style`)并只调 `settings.configure({ auto: false }, ctx.fiber)` 声明自带页面,旧宿主仍走注册制;客户端有 `ctx.configForms` 就用官方表单(值 + 写队列 + revision 栅栏,一次 `set()` 一个字段串行提交),没有就回落到自建路由。设置界面同样分流:0.1.7 注册成 `plugins.bundle.config`(键 = 包名 `dsh-claude-style`,渲染在「设置 → 插件 → dsh-claude-style」页上),旧宿主注册成 `settings.section` 整页——靠 `slots.inject` 的「槽被声明才触发」天然分流,再用 `configForms` 是否存在否决整页在 0.1.7 上的重复注册。
|
|
77
|
+
|
|
78
|
+
- **统计卡片:内容会缺一块、卡片关不掉、还容易被误触发**:三个毛病同源,都在「统计卡片」这一处。**(1) 内容缺块**——卡片的两节(会话统计 / Token 用量)是**点开宿主那两个药丸、读出它们的弹层**再拼出来的,而给卡片新加的「点击打开」监听挂在统计句的根上:采集时那些**合成点击会冒泡回这个监听**,于是采集在自身内部又触发一次采集,两次读取互相打架——实测同一张卡在「会话统计 + Token 用量」与只剩「Token 用量」之间来回变,看起来就像**两个不同的弹层**(用户报的「一个是会话统计,一个是会话统计+Token 用量」)。现在只认**真实点击**(`event.isTrusted`),合成点击不再回灌。**(2) 关不掉**——同一条回灌路径每次都取消掉待执行的隐藏定时器,所以指针离开后卡片留在原地;回灌修掉后实测每轮都能收起。**(3) 误触发**——统计句横在工具栏行中间,指针从权限控件移向模型触发器时会经过它,于是卡片随机弹出;现在**悬停需停留 300ms** 才打开(快速划过不再触发),并且**点击也能打开**(宿主自己的两个弹层被样式表藏住,点击必须落在某处)。另外补上**热重载遗留卡片的清扫**与**按世代重新绑定**:宿主拥有统计节点并在客户端 HMR 重载后复用,原先的布尔标记让新一代**完全不绑定**,于是上一代的闭包继续供着那张内容陈旧的卡;现在标记改为按世代取值,每轮 pass 顺带清掉不属于本代的 `.dsh-claude-stats-popover`(模型选择器与账户区早就这么做,统计卡是漏掉的那个)。实测(0.1.7-alpha.1 真实 GUI,连续 5 轮开合):每轮都是两节齐全(8 项)且离开即收起;快速划过 100ms 不弹、停留 1.2s 弹、点击弹。
|
|
79
|
+
|
|
80
|
+
- **0.1.7 上设置仍然存不进去:软链安装下 schemastery 解析不到,Config 建不出来**:宿主半边导出 `Config` 需要 schemastery,而本仓库是 `link:D:/Build/dsh-claude-style` 装进 profile 的——Node 按**真实路径**(仓库目录)向上找 `node_modules`,永远走不到 profile 的拦截层,`import('@deepseek-ai/schemastery')` 直接 ERR_MODULE_NOT_FOUND(宿主日志里那条「schemastery did not resolve」就是它);而拦截层里那份指向的是**桌面端的 3.18.2**,本来也没有 `.volatile()`。于是 `Config` 为 undefined、条目没有可编辑字段,`configForms` 那边一写就被拒,设置页显示「设置存储不可用,改动不会被保存」。现在先按 **harness 自己的解析基准**取 schemastery——`createRequire(process.argv[1])`,也就是 `dsh` 的 `lib/bin.js`——那正是设置域自己校验表单用的同一份实例,0.1.7 上就是带 `.volatile()` 的 3.18.3;取不到才回落到普通 import。回归脚本 `.debug/schema-resolution-check.cjs` 用替身 harness 目录跑真实子进程验证:锚点在时 `Config` 八个字段全部 volatile,锚点不在时 `Config` 为 undefined(负对照)。
|
|
81
|
+
- **输入框下面的「+」在 0.1.7 上失去皮肤的样式与位置**:0.1.7 把「添加附件」与「指令」两个按钮合并成一个「+」,它的 `aria-label` 也从 `添加附件` / `Add attachment` 变成 `添加文件或调用指令` / `Add files or run commands` —— 皮肤的规则是按**精确标签**匹配的,于是它整条退回宿主默认:28px 圆、hover 变成 `rgb(46,44,41)` 的近黑板、并按宿主顺序排到权限控件**前面**。现在标签列表补上两个子串匹配(`*="添加文件"` / `*="Add files"`),「+」回到 24px、透明底、浅色 hover 板,并回到权限控件之后(`order: 3`)—— 即 0.1.7 之前的样式与位置。实测:hover 底色从 `rgb(46,44,41)` 变回 `rgba(0,0,0,0.08)`,盒子 28×28 → **24×24**、`order` 3。
|
|
82
|
+
- **侧栏两行改成 Claude 真机的形状:默认无底色、hover 才有底色、「+」套一枚圆形底**:0.1.7 在新会话按钮下新增了一个 panel row(`插件`),宿主给它 36px 高 / 14px / 12px 圆角,而新会话行是 28px / 13px / 6px —— 两行叠在一起却像两份不同的列表;而且两行都常驻一块灰色底板,Claude 真机的侧栏是**平的**,底色只在指针落上去时出现。现在两行取同一套度量(**含 `min-height`**:宿主用 min-height 把面板行顶在 36px,只写 `height` 压不住)、**静止透明 / hover 才铺底板**,并且图标盒统一成 **20px**:新会话那颗「+」不再只是一枚字形,而是一枚 **20px 圆形底 + 12px 加号**的 chip(Claude 用它标记主操作行),插件行的 13px 四角星居中放进同样的 20px 盒。实测两行 computed 逐项一致(`28px / 256px / x=12 / 6px / 13px 500 / padding 0 8px / gap 6px`),**两行文字都从 x=46 起排**(此前 45 / 39,差 6px —— 旧写法给字形又加了 6px 外边距,与行自身的 gap 叠在一起)。chip 用 `border-radius: 50%` + 一张内联 SVG 加号做 `background-image`:mask 做不到——它会把元素裁成字形,圆形底一起被裁掉;代价是加号颜色不能再用 `currentColor`,因此亮/暗各一条规则。
|
|
83
|
+
- **侧栏两行的图标 hover 转 90°**:新会话的「+」与插件行那颗四角星都是四重对称图形,转 90° 落回原形,读起来是一次旋转而不是状态变化;`transition: transform .25s` 让指针离开时自动转回去。实测(0.1.7-alpha.1 真实 GUI):两行 hover 时 `transform: matrix(0, 1, -1, 0, 0, 0)`(即 `rotate(90deg)`),指针离开回到 `none`。
|
|
4
84
|
|
|
5
85
|
## [0.4.0] - 2026-09-21
|
|
6
86
|
|
package/LICENSE
CHANGED
|
@@ -39,12 +39,4 @@ owners.
|
|
|
39
39
|
are JetBrains Mono, distributed under the SIL Open Font License
|
|
40
40
|
(`fonts/OFL.txt`). They may be redistributed with this plugin.
|
|
41
41
|
|
|
42
|
-
- `fonts/GoogleSansFlexPicker.woff2` is a Latin-only subset of Google Sans Flex
|
|
43
|
-
(standard weight, upright), built by `scripts/slim-google-sans.py` and renamed
|
|
44
|
-
to `Google Sans Flex Picker`. Google Sans Flex is distributed under the SIL
|
|
45
|
-
Open Font License (`fonts/OFL-GoogleSansFlex.txt`, Copyright 2022 The Google
|
|
46
|
-
Sans Flex Project Authors); upstream declares no Reserved Font Name, and this
|
|
47
|
-
subset is renamed so it cannot shadow a full system-installed copy. It ships
|
|
48
|
-
with the npm package and may be redistributed with this plugin, under no other
|
|
49
|
-
licence.
|
|
50
42
|
|
package/README.en.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
<div align="center">
|
|
2
1
|
# DSH Claude Style
|
|
3
2
|
|
|
4
|
-
**A
|
|
3
|
+
**A theme plugin that recreates the look and feel of Claude Code Desktop for the DeepSeek Harness Web GUI.**
|
|
4
|
+
|
|
5
|
+
<div align="center">
|
|
5
6
|
|
|
6
7
|
[](README.en.md) [](README.md)
|
|
7
8
|
|
|
@@ -19,72 +20,73 @@
|
|
|
19
20
|
</tr>
|
|
20
21
|
</table>
|
|
21
22
|
|
|
22
|
-
> Light: ivory canvas `#FCFCFB` with a
|
|
23
|
+
> 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.
|
|
23
24
|
|
|
24
25
|
## Fonts
|
|
25
26
|
|
|
26
|
-
> **Important: the
|
|
27
|
+
> **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.
|
|
27
28
|
|
|
28
29
|
| Font | Used for | File |
|
|
29
30
|
|---|---|---|
|
|
30
|
-
| Anthropic Sans Web Text | UI
|
|
31
|
-
| Anthropic Serif Web Text | Conversation body /
|
|
31
|
+
| Anthropic Sans Web Text | Interface / UI | [`fonts/AnthropicSansWebText.ttf`](https://github.com/Nwflower/dsh-claude-style/raw/main/fonts/AnthropicSansWebText.ttf) |
|
|
32
|
+
| Anthropic Serif Web Text | Conversation body / Markdown | [`fonts/AnthropicSerifWebText.ttf`](https://github.com/Nwflower/dsh-claude-style/raw/main/fonts/AnthropicSerifWebText.ttf) |
|
|
32
33
|
| 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) |
|
|
33
|
-
| Google Sans Flex Picker | Model picker · Gemini row name | Shipped with the package: [`fonts/GoogleSansFlexPicker.woff2`](fonts/GoogleSansFlexPicker.woff2) |
|
|
34
34
|
|
|
35
|
-
Enabling the Anthropic
|
|
35
|
+
Enabling the Anthropic fonts (pick one):
|
|
36
|
+
|
|
37
|
+
① Install system-wide — on Windows, double-click the `.ttf` → "Install"; on macOS, import it with Font Book.
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
② No install — copy the `.ttf` files into the plugin package's `fonts/` directory. Refresh the page afterwards.
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
> The Anthropic Sans/Serif fonts are copyright Anthropic, licensed for personal use only, and are not covered by the MIT license.
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
## Installation
|
|
42
44
|
|
|
43
|
-
1.
|
|
45
|
+
1. From a terminal
|
|
44
46
|
|
|
45
47
|
```bash
|
|
46
48
|
dsh plugin --profile web add dsh-claude-style # npm package (recommended)
|
|
47
49
|
dsh plugin --profile web add Nwflower/dsh-claude-style # GitHub source
|
|
48
50
|
```
|
|
49
51
|
|
|
50
|
-
2.
|
|
52
|
+
2. From the [plugin market](https://github.com/dsh-market/dsh-market)
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
Keep only one theme enabled at a time. After installing, **restart `dsh web`** and refresh the page.
|
|
53
55
|
|
|
54
|
-
##
|
|
56
|
+
## Features
|
|
55
57
|
|
|
56
|
-
1. **Theme** —
|
|
57
|
-
2. **
|
|
58
|
-
3. **Model picker**
|
|
59
|
-
4. **
|
|
60
|
-
5. **
|
|
61
|
-
6. **
|
|
58
|
+
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.
|
|
59
|
+
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.
|
|
60
|
+
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.
|
|
61
|
+
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.
|
|
62
|
+
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.
|
|
63
|
+
6. **Account area** — after a desktop sign-in the row shows the **real avatar and nickname** (kept fresh by a 60-second poll); the account drawer mirrors the host's own menu (Settings / Feedback / Sign out — or Sign in), and each action calls the official behavior directly: the questionnaire opens in a new tab, sign-out hits the account namespace, and the host's own trigger stays out of sight. On hosts without the desktop account the drawer keeps only its Settings entry.
|
|
62
64
|
|
|
63
|
-
##
|
|
65
|
+
## Disabling and uninstalling
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
To pause the theme without uninstalling it, add this to the profile's `cordis.patch.yml` (`~/.dsh/profiles/web/cordis.patch.yml`):
|
|
66
68
|
|
|
67
69
|
```yaml
|
|
68
70
|
- id: ui-skin-claude-style
|
|
69
71
|
disabled: true
|
|
70
72
|
```
|
|
71
73
|
|
|
72
|
-
It
|
|
74
|
+
It takes effect within about a second; refresh the page to get the stock look back.
|
|
73
75
|
|
|
74
76
|
```bash
|
|
75
77
|
dsh plugin --profile web remove dsh-claude-style # uninstall
|
|
76
78
|
```
|
|
77
79
|
|
|
78
|
-
Then restart `dsh web`;
|
|
80
|
+
Then restart `dsh web`; if you added this theme's entry to `cordis.patch.yml` by hand, remove that too.
|
|
79
81
|
|
|
80
|
-
##
|
|
82
|
+
## Documentation
|
|
81
83
|
|
|
82
|
-
| Document |
|
|
84
|
+
| Document | What it covers |
|
|
83
85
|
| --- | --- |
|
|
84
|
-
| [Design
|
|
86
|
+
| [Design tokens](docs/STYLE.md) | Palette, fonts, shapes, source layout and the host-selector discipline (in English) |
|
|
85
87
|
| [Changelog](CHANGELOG.md) | Version history |
|
|
86
|
-
| [Contributing](CONTRIBUTING.md) |
|
|
88
|
+
| [Contributing](CONTRIBUTING.md) | Building from `src/`, commit conventions, screenshot and regression tooling (in English) |
|
|
87
89
|
|
|
88
|
-
##
|
|
90
|
+
## Friends
|
|
89
91
|
|
|
90
|
-
> Running several themes
|
|
92
|
+
> 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.
|
package/README.md
CHANGED
|
@@ -24,22 +24,23 @@
|
|
|
24
24
|
|
|
25
25
|
## 字体
|
|
26
26
|
|
|
27
|
-
> **重要:
|
|
27
|
+
> **重要:Anthropic 字体不随 npm 包分发,仅在仓库 [`fonts/`](fonts/) 供下载**——既可以直接安装到系统,也可以免安装:把两个 `.ttf` 放进插件包的 `fonts/` 目录,宿主会以同样的 webfont 方式提供它们(两种方式的字体文件完全一致,效果相同)。生效均需刷新 / 重启 web。
|
|
28
28
|
|
|
29
29
|
| 字体 | 用途 | 文件 |
|
|
30
30
|
|---|---|---|
|
|
31
31
|
| Anthropic Sans Web Text | 界面 / UI | [`fonts/AnthropicSansWebText.ttf`](https://github.com/Nwflower/dsh-claude-style/raw/main/fonts/AnthropicSansWebText.ttf) |
|
|
32
32
|
| Anthropic Serif Web Text | 对话正文 / Markdown | [`fonts/AnthropicSerifWebText.ttf`](https://github.com/Nwflower/dsh-claude-style/raw/main/fonts/AnthropicSerifWebText.ttf) |
|
|
33
33
|
| JetBrains Mono Variable | 代码 / 代码块 | [`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) |
|
|
34
|
-
| Google Sans Flex Picker | 模型选择器 · Gemini 行名称 | 随包分发:[`fonts/GoogleSansFlexPicker.woff2`](fonts/GoogleSansFlexPicker.woff2) |
|
|
35
34
|
|
|
36
|
-
Anthropic
|
|
35
|
+
Anthropic 字体启用(二选一):
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
① 安装到系统——Windows 双击 `.ttf` → 「安装」,macOS 用「字体册」导入;
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
② 免安装——把 `.ttf` 复制到插件包的 `fonts/` 目录。完成后刷新页面生效。
|
|
40
|
+
|
|
41
|
+
> Anthropic Sans/Serif 字体版权归 Anthropic 所有,仅供个人使用,不适用 MIT 许可。
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
## 安装
|
|
43
44
|
|
|
44
45
|
1. 通过终端安装
|
|
45
46
|
|
|
@@ -52,14 +53,14 @@ dsh plugin --profile web add Nwflower/dsh-claude-style # GitHub 源
|
|
|
52
53
|
|
|
53
54
|
同一时刻建议只启用一个主题。安装后**重启 `dsh web`** 并刷新页面即生效。
|
|
54
55
|
|
|
55
|
-
##
|
|
56
|
+
## 特点
|
|
56
57
|
|
|
57
|
-
1. **主题** ——
|
|
58
|
-
2.
|
|
59
|
-
3.
|
|
60
|
-
4.
|
|
61
|
-
5.
|
|
62
|
-
6.
|
|
58
|
+
1. **主题** —— 安装即全局生效,无需配置。亮色象牙白 `#FCFCFB`、暗色暖黑 `#141413`,两套画布共用同一个操作强调色陶烬橙 `#D97757`;亮暗跟随系统颜色模式切换。
|
|
59
|
+
2. **输入框** —— 输入框整块重做:权限分段控件(完全权限 / 只读 / …)、带品牌锁定标的模型触发器、工具栏与状态统计同一行排版,发送键与停止键统一成 7px 圆角。统计句与模型触发器共用一套字号与颜色。
|
|
60
|
+
3. **模型选择器** —— 全新的两级弹层:一级列官方服务与设置页勾选的快捷供应商,「更多模型」二级按供应商分组;每行带厂商锁定标与说明文案,底部是推理等级推条(无极滑动,松手对齐最近档位)与「更多模型」入口。二级底边与一级对齐,一级已显示的供应商不再重复;模型不支持思考时不画推条。悬停停留 50ms 打开、离开 150ms 关闭,两卡之间的空隙不算离开。
|
|
61
|
+
4. **工作区** —— 侧栏「工作区」标题改成 **进行中 / 已归档** 分段控件:进行中沿用宿主的会话树,已归档是皮肤自己的平铺列表(标题、时间、每行「取消归档」与「删除」两个图标按钮)。行排版与宿主的会话行逐项一致(行 x=12 / 宽 251 / 高 28 / 标题起点 x=36)。宿主没有归档能力时(如 0.1.5 的某些构建)不显示控件、保留原标题。
|
|
62
|
+
5. **侧栏** —— 新会话与插件两行取 Claude 真机形状:默认无底色、hover 才有底色,新会话的「+」套一枚圆形底,两行图标 hover 顺时针转 90°(四重对称图形,转回原位);底部账户抽屉、封号彩蛋一并保留。
|
|
63
|
+
6. **账号区** —— 桌面端登录后显示**真实头像与昵称**(60 秒轮询保持新鲜,失败回退手绘星芒);账号抽屉接管宿主账号菜单:**动态镜像**「设置 / 意见反馈 / 退出登录(或登录)」,点击直连官方行为(官方问卷、账号登出、宿主设置页),宿主自己的触发器隐藏不出现在界面上;非桌面端自动只保留「设置」入口。
|
|
63
64
|
|
|
64
65
|
## 停用与卸载
|
|
65
66
|
|
package/docs/STYLE.md
CHANGED
|
@@ -26,18 +26,22 @@ Rules:
|
|
|
26
26
|
- **Serif display** — headings / editorial statements (`--dsw-font-serif`).
|
|
27
27
|
- **Sans UI** — chrome, body (`--dsw-font-family`).
|
|
28
28
|
- **Mono** — code, technical labels (`--dsw-font-code`).
|
|
29
|
-
|
|
30
|
-
vendor's mark (`--dsw-font-brand-gemini`: a Latin-only Google Sans Flex subset,
|
|
31
|
-
standard weight, SIL OFL 1.1). A vendor face is a *material*, not a new UI role:
|
|
32
|
-
it never replaces the sans stack, it sits in front of it, so text the subset does
|
|
33
|
-
not cover falls through instead of turning to tofu. Rebuilt with
|
|
34
|
-
`scripts/slim-google-sans.py`; licence in `fonts/OFL-GoogleSansFlex.txt`.
|
|
29
|
+
|
|
35
30
|
|
|
36
31
|
## Shapes
|
|
37
32
|
|
|
38
33
|
- Radius: 4 / 8 / 16 px; pills for CTAs (`9999px`).
|
|
39
34
|
- Borders: 1px warm hairline.
|
|
40
35
|
- Spacing: 4 px rhythm.
|
|
36
|
+
- Bottom edge: the transcript never fades behind the composer by shadow. The
|
|
37
|
+
composer seat paints an opaque `var(--dsw-alias-bg-base)` bar across its own
|
|
38
|
+
box (so the bar's height *is* the composer's height) and one
|
|
39
|
+
`--dsh-composer-fade-h` (40px) gradient band above it, running to transparent.
|
|
40
|
+
The same token is the transcript's bottom clearance, so the last turn rests
|
|
41
|
+
exactly at the band's top edge. Never re-add background-coloured halo shadows
|
|
42
|
+
to the card to hide the transcript: they have to be restated in every
|
|
43
|
+
light / dark / focus / attachment rule, and every rule that resets the card's
|
|
44
|
+
`box-shadow` inherits the job.
|
|
41
45
|
- Focus: the composer field lights up — its hairline takes the input box's own
|
|
42
46
|
shadow colour (espresso `#141413` in light) with a 1px halo in the same tone
|
|
43
47
|
and a deeper drop shadow. Dark inverts the face: on the near-black canvas a
|
package/docs/architecture.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
## D2. 纯浏览器半边实现,不动宿主
|
|
21
21
|
|
|
22
22
|
- **背景**:主题是皮肤,不应 fork DSH;宿主升级要快跟随。
|
|
23
|
-
- **决定**:一切效果通过 CSS 覆盖与客户端 DOM override 实现;宿主半边(`lib/index.js`)只提供静态路由(模型文案 JSON
|
|
23
|
+
- **决定**:一切效果通过 CSS 覆盖与客户端 DOM override 实现;宿主半边(`lib/index.js`)只提供静态路由(模型文案 JSON、位图图标)。(后来宿主半边还承担了字体、设置读写与系统用户名路由;私有路由的安全约束见 D11。)
|
|
24
24
|
- **代价**:依赖宿主带哈希的 CSS-module 类名,宿主改版可能击穿选择器——用 D3 的纪律和 probe 回归对冲。
|
|
25
25
|
- **重审条件**:DSH 官方开放主题 API / 插槽覆盖所 target 的区域时,逐步迁移过去。
|
|
26
26
|
|
|
@@ -71,3 +71,29 @@
|
|
|
71
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
72
|
- **落地**:已执行完毕。仍未消除的结构感知 `:has()` 只剩 placeholder 那条(`card` 上的 `:not(:has([data-composer-placeholder]))`):`copy.js` 本就管理 placeholder,可在它的 sync 里同步写 `data-has-placeholder` 再改 CSS,属可选的后续优化。
|
|
73
73
|
- **重审条件**:实测证明 :has() 不再是热点,或宿主提供 hero/inline 的稳定属性契约。
|
|
74
|
+
|
|
75
|
+
## D10. 设置按宿主世代分流:官方 Config 表单 + 旧版命名空间注册
|
|
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`。
|
|
83
|
+
|
|
84
|
+
## D11. 插件自有路由借宿主的请求栅栏;宿主/用户来源的字符串只以文本上屏
|
|
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` 一律走静态资产),不受影响。
|
|
89
|
+
- **重审条件**:宿主为插件提供自带鉴权的路由注册(如 `connection.fetch.register`)时,迁移过去并删掉本地替身。
|
|
90
|
+
|
|
91
|
+
## D12. 特性级失败隔离:一个特性出错只关掉它自己
|
|
92
|
+
|
|
93
|
+
- **背景**:`apply()` 依次装 14 个特性,teardown 却在最后才交给 `ctx.effect`;每轮 pass 里各 `sync()` 也没有逐个兜底——一个抛错,排在它后面的全部跳过,而且每轮如此。实测:宿主 `remote.account.getProfile()` 返回非 Promise 时 `apply()` 中途抛错,样式表与 body 属性留在页面上、调度器没装上、teardown 没注册(关掉插件也清不掉),前面装好的特性的监听器一并泄漏。effort-picker 那次「Loading plugins…」卡死是同一类问题。
|
|
94
|
+
- **决定**:
|
|
95
|
+
- teardown 最先经 `ctx.effect` 注册且幂等;每个特性单独 try/catch 安装,装不上的报一次 `console.error` 并退役。调度器本身装不上时整体回滚到宿主原样——没有调度器,其余特性都不会同步,留着只是一张半套的皮。
|
|
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()`)会各自连续失败、依次退役——降级而非崩溃。
|
|
99
|
+
- **重审条件**:宿主提供插件级的错误上报 / 健康面板时,把报告接过去。
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 148.09 148.18'><g transform='translate(-75.96,-223.53)'><path fill='#D97757' d='m 105.01,322.07 29.14,-16.35 0.49,-1.42 -0.49,-0.79 h -1.42 l -4.87,-0.3 -16.65,-0.45 -14.44,-0.6 -13.99,-0.75 -3.52,-0.75 -3.3,-4.35 0.34,-2.17 2.96,-1.99 4.24,0.37 9.37,0.64 14.06,0.97 10.2,0.6 15.11,1.57 h 2.4 l 0.34,-0.97 -0.82,-0.6 -0.64,-0.6 -14.55,-9.86 -15.75,-10.42 -8.25,-6 -4.46,-3.04 -2.25,-2.85 -0.97,-6.22 4.05,-4.46 5.44,0.37 1.39,0.37 5.51,4.24 11.77,9.11 15.37,11.32 2.25,1.87 0.9,-0.64 0.11,-0.45 -1.01,-1.69 -8.36,-15.11 -8.92,-15.37 -3.97,-6.37 -1.05,-3.82 c -0.37,-1.57 -0.64,-2.89 -0.64,-4.5 l 4.61,-6.26 2.55,-0.82 6.15,0.82 2.59,2.25 3.82,8.74 6.19,13.76 9.6,18.71 2.81,5.55 1.5,5.14 0.56,1.57 h 0.97 v -0.9 l 0.79,-10.54 1.46,-12.94 1.42,-16.65 0.49,-4.69 2.32,-5.62 4.61,-3.04 3.6,1.72 2.96,4.24 -0.41,2.74 -1.76,11.44 -3.45,17.92 -2.25,12 h 1.31 l 1.5,-1.5 6.07,-8.06 10.2,-12.75 4.5,-5.06 5.25,-5.59 3.37,-2.66 h 6.37 l 4.69,6.97 -2.1,7.2 -6.56,8.32 -5.44,7.05 -7.8,10.5 -4.87,8.4 0.45,0.67 1.16,-0.11 17.62,-3.75 9.52,-1.72 11.36,-1.95 5.14,2.4 0.56,2.44 -2.02,4.99 -12.15,3 -14.25,2.85 -21.22,5.02 -0.26,0.19 0.3,0.37 9.56,0.9 4.09,0.22 h 10.01 l 18.64,1.39 4.87,3.22 2.92,3.94 -0.49,3 -7.5,3.82 -10.12,-2.4 -23.62,-5.62 -8.1,-2.02 h -1.12 v 0.67 l 6.75,6.6 12.37,11.17 15.49,14.4 0.79,3.56 -1.99,2.81 -2.1,-0.3 -13.61,-10.24 -5.25,-4.61 -11.89,-10.01 h -0.79 v 1.05 l 2.74,4.01 14.47,21.75 0.75,6.67 -1.05,2.17 -3.75,1.31 -4.12,-0.75 -8.47,-11.89 -8.74,-13.39 -7.05,-12 -0.86,0.49 -4.16,44.81 -1.95,2.29 -4.5,1.72 -3.75,-2.85 -1.99,-4.61 1.99,-9.11 2.4,-11.89 1.95,-9.45 1.76,-11.74 1.05,-3.9 -0.07,-0.26 -0.86,0.11 -8.85,12.15 -13.46,18.19 -10.65,11.4 -2.55,1.01 -4.42,-2.29 0.41,-4.09 2.47,-3.64 14.74,-18.75 8.89,-11.62 5.74,-6.71 -0.04,-0.97 h -0.34 l -39.15,25.42 -6.97,0.9 -3,-2.81 0.37,-4.61 1.42,-1.5 11.77,-8.1 -0.04,0.04 z'/></g></svg>
|