DeepSeek Harness Plugin Hub

Publish and manage complete Harness Profiles. Discover Plugins for your next setup.

Explore

PluginsPresetsDocsNews

Community

Publish a pluginContactReport an issue

Resources

Plugin Hub on GitHubDeepSeek HarnessSystem statusPrivacy notice
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

Independent and unofficial. Not affiliated with, authorized by, or endorsed by DeepSeek.

DeepSeek Harness Plugin Hub
ProfilesPluginsCategoriesNewsDocsSign inManage Profiles
ProfilesPluginsCategoriesNewsDocsSign in
← Plugins
S

dsh-smooth-skin

Smooth Skin

Smooth: DSH Web interface skin — unified colors, rounded corners, and list cards, with six built-in theme presets including First Snow White and Black Iron

The plugin will be installed here. Keep web if you are unsure.

npx -y @deepseek-ai/dsh plugin --profile web add github:Super-Gagaga/dsh-smooth-skin#b0f4c1172677c4b5e57de0567d84335cae36ba94
READMECompatibilityVersions

Compatibility and provenance

Smooth Skin is published as dsh-smooth-skin and currently resolves to version 0.1.0. The Hub verifies its manifest and preserves the exact installation source for reproducible installs.

DSH compatibility
*
Runtime surfaces
web
Release source
github
Registry updated
11/1/2026

Versions

0.1.0stable
11/1/2026

Related plugins

Loading related plugins…

Latest
0.1.0
DSH
*
HMR
Process restart
Tree shaking
Safe tree shaking not declared
Unpacked size
Unavailable
Files
Unavailable
Surface
web
License
MIT
Source
github
GitHub
★ 1
Weekly downloads
0
Last push
11/5/2026
View source ↗
README badge

Click the badge to copy Markdown for your README.

Do you maintain this Plugin?Claim benefit · Priority security scan

Verify the GitHub repository declared in package.json to manage this listing. After you claim it, Hub will prioritize a security scan of the current version and publish the result when it passes.

Claim this Plugin →
Report an issue

Related plugins

More verified plugins in ui-customization.

Web App@deepseek-ai/dsh-web-appThe dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)Experimental Session Titles Bundle@deepseek-ai/dsh-experimental-session-titles-bundleRefresh session titles as new human prompts arrive.Personalized Recommendations@deepseek-ai/dsh-personalized-recommendationsPersonalized pages and built-in plugin recommendations offered as cards below answersExperimental Agent Team Web Profile@deepseek-ai/dsh-experimental-agent-team-web-profileExperimental Web profile layer for Agent Teams Remote and UI plugins

README

Smooth

DSH 界面美化插件,依据 assets/样例图/index.html、settings.html 及预期截图调整侧栏、顶部标签、欢迎区、双层输入卡片和整页设置;插件页沿用原生页面,仅调整页面背景。图标使用宿主原生组件,导航和主题切换调用宿主接口。关闭皮肤后恢复原生样式和外观选项;侧栏若已被手动拖动,则保留手动宽度。当前按 DSH 0.2.1-rc.5 核对宿主契约(rc.3 起可用);0.2.0 缺少 composer dock 的新属性,需要 0.2.1 及以上才能完整隐藏该行。

外观

  • 应用外层固定在窗口视口内,侧栏、对话和问答内容分别内部滚动;聚焦输入框或滚动到区域边缘时,不带动整个页面下移
  • 图标栏 #191b1d、侧栏 #202123、主工作区 #292c2e、顶部标签栏与表单 #303335、输入卡片 #323537;图标栏通过背景色区分层次,主面板向会话列表投射两层外阴影,沿圆角轮廓形成抬高的层次,输入卡片的信息栏略亮于编辑区
  • 深色模式正文 #f1f2f4,次要文字 #c3c5ca,辅助文字 #b4b7bf(三档按 OKLCh 亮度同比提亮,层级间距不变)
  • 跟随宿主的字体设置:body 继承宿主 --dsw-font-family(DSH 0.2.1 的文字字体选择),旧版没有该用户变量时回退到原来的 Segoe UI / 微软雅黑 / PingFang 栈;代码与终端的字号由宿主自己的 --dsh-code-font-size / --dsh-terminal-font-size 控制,皮肤不覆盖
  • 原生导航按钮与头像组成固定尺寸的紧凑左侧图标栏(栏宽约 38px、按钮 35px、图标 22px(按钮与图标保持原尺寸,只把图标栏底色收窄到贴着按钮),不随窗口变化),按钮沿栏中线对齐;项目树、新会话和品牌显示在右侧。桌面「新版本」更新 pill 在栏内收成与其余按钮同尺寸的方形按钮,并浮在 footer 上方、独占一行(position:absolute + bottom:calc(100% + 8px) + translateX(-50%),footer 是包含块)——宿主把 footer 排成两行(账号行 + 设置行),而设置行本身是横向 flex,单列栏放不下就会横向溢出、落到头像背后;order/display:contents 这类换序手段受宿主的嵌套层级影响,所以直接把它抬出那一行,位置不再依赖内部结构;文字按设置按钮的方式只做视觉隐藏(aria-label 与 tooltip 仍带版本信息);按钮本身也改回栏内按钮的样式:去掉宿主给的品牌蓝描边与 8% 蓝底,换成透明的底与 --smooth-surface-3 的悬停填充,但图标保留宿主那支新版本蓝 --dsw-alias-brand-primary-new-colorprimary-new-color(悬停也不变色;忙碌中的 aria-disabled="true" 不显示悬停,错误点仍是红色);旁边的连接指示灯带 data-phase、不带 aria-disabled,因此不会被这条规则命中。面板列表里的「更多」按钮用 flex order:1 固定排在图标列最末——第三方面板行(如记忆库)会把自己追加到宿主 panelList 的末尾,只靠 DOM 顺序它会跑到「更多」上面,用 order 才能无论后来插入什么都保持在最底部
  • Home 使用线条房子图标:有会话标签时只关掉压在它上面的设置或面板,回到那个会话(标签和对话都原样留着,已经在主页面时再点不动);没有会话标签时(只剩设置/插件标签,或标签已经全关掉)直接回欢迎页——宿主会把最后那段对话继续留在主视图里,此时只关设置和面板会露出一个标签条已经不代表的页面,所以点 Home 会把主视图一并清空(clearMain()),标签条上也不再有任何一页是「当前」(clearActive()),主视图因此是宿主首页,data-smooth-home 一挂上就是带用量面板、不带输入卡片的欢迎页;新建仍使用「新会话」按钮;点回「设置」标签会回到离开时所在的分区,而不是宿主的「通用设置」落地页——宿主关闭设置时会清空自己的 activeId,皮肤从设置存储的快照里记住最后显示的分区(第一次打开仍是通用设置)
  • 会话树按参考列表收紧:会话行 28px、工作区行 29px(原生 32 / 34),会话名 12.5px/18px 用次要文字色,工作区名 13.5px/19px 用正文色、字重 500,时间是 11px/16px 且与会话名同色(原生 10px、比标题暗一整档),顶部「工作区」标签 12px。工作区行不再画文件夹图标,标签直接落在列表自身的左边距上;行首那个 16px 前导格里画一个空心点,它和宿主进行中的 spinner 用同一份几何:r=9.5、stroke-width=2.71、24 单位 viewBox、按 8.84px 显示,也就是外径 8px、线宽 1px。空闲时这个圆是 ::before 上用同一支 SVG 做的遮罩(mask + background: color-mix(次要文字色 72%)),进行中由宿主自己的 spinner 顶上来(圆点 opacity:0,轨道压到 40%,圆环轮廓还在、转动的亮弧更显眼),所以两种状态由同一条矢量路径绘制,圆角完全一致——先前用 CSS border 画的圆环和 spinner 的矢量描边圆角对不上,一大一小两个圆。参考图是 6px,但 150% 下只有 9 个物理像素,边缘会被读成圆角方块,所以取 8px(12 个物理像素);行首永远是同一个大小、同一个位置的圈,进行中只是转起来。文件夹和悬停时才出现的三角共用同一个 16px 格子,两者一律不画——只藏其中一个的话,指针进入行时标签会横移一格
  • 侧栏默认宽 352px(保存自当前布局,不再按窗口宽度的 28.5% 重算;仍可手动拖动,关闭皮肤时保留手动宽度);顶部显示已操作的对话/页面:新建的会话先占一个可替换的临时预览标签,只有真正发出第一条消息时才固定成标签(打开、点按钮、打字都不算);从会话树里点开的会话同样先占预览位,在正文里操作后固定;新标签排在最前面:预览标签占最左位,固定后仍留在原位,更早的标签依次往右,溢出时才出现滚动箭头;标签条高度直接取宿主的标题栏高度(桌面端 40px),标签本身尺寸不变,只收掉上下多出来的留白;侧栏收起时标签条左边界不写死:皮肤量出原生「应用 / 编辑」菜单的右边缘再留 8px,左侧依次是 12px 的展开箭头、48px 的新会话按钮和菜单本身;箭头与新会话按钮都由宿主固定在标题栏同一水平线、同为 28px(面板按钮量不到时回退 100px),标签不会再压住或挤歪它们
  • 会话归档后它的标签随之下线:标签账本订阅宿主工作区快照里的 archivedSessionIds,归档发生时把对应的预览/固定标签一并移除;如果归档的是当前会话,宿主本就会清空主视图,标签条不会留下一个打不开的入口,也不再有标签处于选中态。已归档的会话不会被重新加回(在仍打开的归档对话里点按、发送都不会复活标签);取消归档也不会自动恢复标签,需要重新从会话树打开
  • 已归档的每一行末尾多一个删除按钮:只有归档行才渲染(注册在宿主的行操作槽 sidebar.workspaces.session.row.action,order: 300,排在归档/置顶之后),按钮是宿主的紧凑图标规格(14px 图标 / 20px 命中区),悬停才转成错误色;点击先弹原生 Modal 确认(「将永久删除『标题』的本地会话记录,无法恢复」),确认后由 Host 半的私有桥接删除该会话的持久化日志目录与其投影缓存,再重拉会话列表,归档行即时消失。删除只对「已归档且没有仍在运行的活动」的会话生效:宿主的归档只会停掉工作、不会把 Agent 从内存 store 里清掉,所以内存里仍有这个会话也可以删;Host 半先询问归档准入所问的 workspace/session-activity 提供方,仍有运行的轮次 / 任务 / 子代理 / 计划任务会拒绝删除,空闲时再通过 workspace/session-stop 收尾,并顺带清掉工作区记账与投影缓存;如果该会话仍留在宿主的内存 store 里,删除后再把对应的 store 条目摘掉(控制器把随后的 session/disposed 转成 api-session/removed,客户端刷新后不会把归档行加回来)。Host 半不可用时对话框会显示错误。修改过 Host 半(lib/index.js)后需要完全退出 DSH 再打开才生效
  • 新会话收成 30 单位高的小按钮(原 46),上下各留 2 / 3 单位边距;侧栏列表顶部分隔线与右侧会话头部底边因此落在同一水平线(实测 162 / 161px),不再一高一低
  • 悬停左侧区域时,「工作区」标题那一行上浮出一枚深色分段按钮「进行中 / 已归档」(选中项高亮,24px 高、圆角 8px、字号 12px),点一下就在运行中的会话与归档之间切换。它直接读写宿主工作区浏览器自己的视图状态(archivedFilter),所以和「...」菜单里的归档筛选是同一份,不另存拷贝;控制条固定在量出的标题行上(窗口缩放或侧栏展开时跟标题栏一起重新量),侧栏收起或搜索展开时让位,键盘焦点进入时也会显示,减少动态效果时不淡入。按钮使用 aria-pressed,因此读屏和自动化都能读到当前的选中侧
  • 收起再展开时左侧宽度保持不变:宿主折叠侧栏后会用自己默认的 280px 重新打开,皮肤记住收起前的宽度(默认 352px,或用户拖出来的宽度),只在这个 0 → 280 的切换里把宽度写回,并把 --smooth-left-width 同步成同一数值;如果用户本来就手动拖到 280px,则不会被误改成 352px
  • 会话区里的对话列宽度会记住当前宽度:宿主把拖动结果写进 localStorage(dsh.conversation.contentWidth),插件在启用时把这个宽度同时存成插件默认(dsh.smooth.conversationWidth)、写回宿主并同步页面上的 --dsh-chat-user-width,重载或宿主偏好被清掉后仍按这个宽度打开;没有拖动记录时,直接把当前显示的正文宽度(宿主发布的 --dsh-chat-user-width 或计算出的 --dsh-chat-content-width)记为默认
  • 会话头部按参考排版:标题占第一行(左),「智能体团队 / 创造模式」等挂在 header actions 槽位里的标签落到标题下方第二行,对话 / 轨迹 两个胶囊居中压在标题那一行上——宿主原本把这两个标签和「更多会话视图」(…)按钮一起放在头部网格的第二个整行里(标题行下面靠左),皮肤把整行绝对定位到标题行顶部并居中:… 按钮紧贴在胶囊右侧,两者同排显示,左侧用一个只在按钮存在时出现的等宽占位,让胶囊本身仍然落在头部中线上,再把标题簇从一行 flex 拆成两行 grid(面包屑一行、actions 一行),标题行右侧的 utilities / corner 仍靠右,并与标题那一行对齐。整个头部继续收紧:内边距 4 / 18 / 6 / 14px、min-height:0、标题簇行距 1px,标题与元信息两行的间距因此比原生矮一截。对话 / 轨迹 换成参考图那种滑块:一条胶囊轨道里一块 50% 宽的滑块,选中项靠 :has(>[role="tab"]:nth-child(2)[aria-selected="true"]) 把它 translateX(100%) 推过去,.26s cubic-bezier(.22,1,.36,1) 平滑过渡;这一行默认 opacity:0,鼠标进入会话列(或键盘焦点落到控件里、或「更多」菜单展开)才 .18s 淡入,离开淡出——「更多」按钮与胶囊共用同一个显隐条件,一起出现、一起隐藏。抬起后的整行仍然铺满头部宽度,但行本身 pointer-events:none,只给胶囊和「更多」重新打开命中区——否则这层透明整行会盖住标题行右侧的 header actions(智能体团队、打开方式分体按钮)与 utilities/corner,按钮看得见却点不到。只有底色还是皮肤自己的。
  • 顶部标签可直接拖动重排:指针移动超过 4px 才进入拖动,普通点击切换/关闭不受影响;拖动中在目标插入间隙显示 2px 的 |(不贴标签边框),松手只改顺序、不激活标签;预览标签被拖动后会固定并落在被拖到的位置,没有拖动的预览始终占最左位(往它左边拖的固定标签会落在它右边)
  • 标签区左右滚动箭头是浮在标签条上的绝对定位按钮,不作为 flex 项占 22px + gap;出现或消失不会改变标签条宽度,因此不会压缩标签,也不会让 overflow 观察器来回切换而卡顿。标签条右侧留出箭头自己的槽位(padding-right: 30px + 10u),标签内容在箭头前一个缝隙处就被裁掉,所以「点箭头」不会点到压在同一点上的标签关闭按钮。箭头只在该方向真的还有内容时才出现:滚到最右端,右侧箭头跟着收起,而槽位留在标题栏自己的内边距里,所以标签不会因为按钮消失而移动
  • 鼠标在顶部标签栏上滚动时,上滚向左、下滚向右,以平滑缓动累积连续滚轮位移;保留触控板横向滚动,支持对话页与设置页,减少动态效果模式下直接滚动
  • 桌面窗口右上角的最小化 / 最大化 / 关闭按钮不再铺自己的底色:preload 测量 probe 的填充改为 rgba(0,0,0,0),原生 Window Controls Overlay 以 0% 透明合成,按钮字形仍取 label-primary,页面表面直接透出
  • 顶部标签栏是固定尺寸的 chrome,不随窗口变化:标题栏高度钉在 48px、标签条左上的圆角钉在 18px、左右内边距钉在 35px / 5px,左侧栏与工作区之间的那道分界渐变也跟着用 18px——原来是 clamp(48px, 65 * 100vw/1950, 64px) 加 35 * var(--smooth-u),窗口一宽这排背景就跟着长高、变圆;标签本身早就用固定单位(--smooth-tab-u: .65px),现在整条标签栏都是定值。
  • 顶部标签的选中胶囊在浅色下按标签栏自己的底色配平:同一个 --smooth-selected 令牌落在近白的标题栏上是 1.31:1 的一块深色,落在侧栏的灰底上却只有 1.12:1,浅色主题因此把它按 46% 混回标题栏表面——三种浅色预设都落到 1.13:1,和侧栏选中行同重;选中态悬停再加深一档到 1.19:1,未选中标签的悬停改成一抹 4% 文字墨(1.07:1)而不是原来的灰色层,否则悬停比选中胶囊还靠近底色。深色主题仍用原令牌,那里的标题栏底色本来就与它同调。
  • 主面板、原生图标底板和输入框圆角按样例比例缩放,但不再随窗口放大:样例形状按 1950px 画布定(--smooth-u: min(1px, 100vw/1950)),左栏 chrome 直接钉死(--smooth-l: .91px,参考文件对侧栏栏位用的就是这个值),标题栏高度停在 64px
  • 圆角统一到五档标尺:皮肤自绘的矩形一律取 --smooth-radius-xl/lg/md/sm/xs = 26 / 16 / 11 / 8 / 4px(宿主侧 --dsw-radius-panel/xl/lg/md 同步为 26/26/16/11,sm/xs 沿用宿主默认 8/4):外壳左下角与大面积面板 26、菜单/弹层/卡片/输入井 16、输入框/选项卡/图标栏按钮 11、列表行与小按钮 8、16px 图标按钮 4;胶囊与圆形仍用 999px/50%。只有标题栏左上角 18px(窗口 chrome)、嵌套内圆(外圆减内衬,如筛选内选项 8−2=6、模型媒体图 16−1=15)和数据小色块(热力格 3px、色块 2px)保留字面值。原先 14/15/19.5/10.92/9.1 等散落取值、视口相关的 calc(35 * --smooth-u),以及被覆盖的旧设置弹窗/导航/新会话圆角都已收口。
  • 插件页保留原生布局、图标、列表和开关;整页设置不再沿用样例图的大画布比例,改用 Harness 自身的界面尺度(表单 52px 行高、14px 标题、13px 说明、30px 选择器、36×20 开关),主题预设收成一行六格的小卡片(色板点 + 名称),不再有日夜模式的三列开关卡片。点卡片切预设时,整页用 View Transition 做一次从左上角展开的圆形揭示:切换前先快照旧页面,新调色板的快照被裁进一个从视口左上角向外扩大的圆里(.52s 缓出),圆内已是新主题、圆外保持旧主题,两张快照不做交叉淡入;快照期间宿主色调与 --smooth-* 令牌已同步写完,等一帧让设置页的选中药丸也画好再取「新」快照。只有真正改变当前色调下配色的切换才播(auto 与它当前解析到的预设互点不播动画),减少动态效果、平台没有 startViewTransition 或界面不可见时直接切换;连续点预设时旧快照被新一次替换、不会把过时主题写回去,快照期间改的其它设置也不会被主题写入回滚。左侧导航按用户给的参考截图取尺度:面板标题 20px/700,导航项 15px/22px、34px 行高、18px 图标、图标与文字间距 13px,选中项是 8px 圆角的浅色填充;左侧因此是「一块文字」而不是一列小字,文字在栏宽里的占比明显提高。它按「账户 / 应用 / 扩展」分为三组:应用组收拢通用设置、模型、内置插件与 Agent 预设;扩展组收拢提示词优化、插件市场、Smooth 皮肤与侧边卡片(原先的「外观」标题并入扩展,市场 / 皮肤 / 卡片都是插件提供的条目):宿主把 settings.section 渲染成一个扁平列表,导航项身上没有 id 或分组属性,因此分组标题是按位置锚定的,增删设置项会连带移动分组边界。分组标题与条目同号(15px/500,灰色),坐在组内第一个条目上方 34px 的空隙里——4px 留给上一组、20px 是标题自己的行盒、10px 留给本组第一项,所以分开三组的是标题本身而不是空行;navList 的 padding-top 留到 34px,否则第一个分组标题会被滚动容器裁掉。整页设置占住工作区右侧后,宿主的模态浮层和透明遮罩改为 pointer-events:none、只让面板自己接收点击,所以设置打开时左侧栏图标、顶部标签条与 Home 都能点;点侧栏里的面板会先关闭设置再打开对应页面,账户菜单的浮层(1100)仍盖在设置(1000)之上。
  • 问答卡里的自由输入框获得焦点时不再画宿主那圈蓝色内描边,边框保持与空闲时同一档灰色,用光标本身提示焦点
  • 对话正文里的行内代码(反引号片段)用与宿主文件引用(file mention)相同的链接蓝(--dsw-alias-link),围栏代码块保持自己的语法配色,不跟着变色
  • 滚动条默认细线,滚动时在 160ms 内变粗,停止 650ms 后用 280ms 渐细;保持原生拖动和固定轨道宽度,减少动态效果模式下直接切换粗细。宿主没有全局滚动条规则,因此自定义样式只作用于应用自身的表层(会话滚动、侧栏、设置、菜单):正文流内部的嵌套滚动容器保持平台叠加式滚动条,不为宽表格、代码块预留 8px 槽位
  • 强调色:#8fb7ff(品牌)与 #ffcd96(暖色点缀)
  • 启动动画随 index 一起注入,首帧直接显示宿主 FishLogo 的内联矢量鲸鱼。图标宽度为 112–176px,深色显示白鲸、浅色显示黑鲸,没有文字、图标底板、边框、进度线或彩色光晕;整段开场没有任何主线程动画,这是被「应用启动期间主线程很忙」逼出来的结论:唯一动作是挂在外层 HTML 盒子上的 CSS 摆动(不是里面的 svg),transform-origin:50% 60% + will-change:transform 让它单独成层、交给 compositor——鲸鱼只光栅化一次,之后移动的是合成器里那张纹理,主线程被 bundle 解析和首次 mount 占满时它照跑;周期 2.4s、幅度 rotate(-2.4deg)/translate(-.3px,-.5px) → rotate(1deg)/translate(.2px,.15px),比英雄区 hover 时更慢更小,掉一帧也看不出来。英雄区那套 SMIL 摆尾(d 变形)没有采用:它必须在主线程逐帧重算几何,正是启动窗口里最不该有的活。鲸鱼自身没有入场或出场动画——开场层首帧就是最终样子,只在交接时 340ms 淡出一次,随后外壳各部件用同一组纯透明度淡入(错落靠延迟,没有位移)。最短停留 1500ms 并等应用挂载后淡出,保留启动动画开关和减少动态效果时自动跳过。设置 → Smooth → 启动动画 右侧的「预览」会就地重播一次:同一份标记、同一份样式、同一组时长,与真启动完全一致,点完即时可见,不用重开应用
  • 光感集中在输入卡片、选中标签与导航项:左上方向的轻微渐变配合顶部内高光,输入框聚焦时不再叠加强调色反光(描边已去掉,靠投影与内高光分层);按钮悬停和按下以 160ms 平滑明暗反馈,减少动态效果模式下直接切换
  • 原生任务清单采用紧凑标题栏与统一行距,进行中的任务有淡强调色背景;长任务标题自然换行,列表高度随窗口受限并保留内部滚动。展开与收起恢复原生即时切换,任务状态、计数和折叠继续由宿主处理
  • 原生问答卡片统一题目、选项及说明的文字层次,说明独立分行,选中项使用淡蓝底色;长问题和选项可分别滚动,底部翻页与提交区保持可见,窄卡片自动重排。保留原生单选、多选、自定义答案、倒计时、草稿与提交逻辑
  • 「Smooth 皮肤」设置页按 外观 / 启动 分组,标题旁有一枚实时状态徽标(已启用 / 已关闭);窗口尺寸行在说明下显示当前窗口的外框尺寸,并提供行内「恢复 1440 × 900」(只重置宽高这一对数字,不碰开关、不碰窗口),底部「恢复默认设置」旁注明「更改会自动保存」。所有改动即时写入 localStorage,没有单独的保存按钮

启动窗口尺寸默认 1440 × 900,在 设置 → Smooth → 启动窗口尺寸 里可直接改宽高(带开关,可关掉交还给应用自己的 1280 × 820),改宽高立即生效,下次启动同样沿用;开关只决定下次启动用不用这个尺寸,不碰当前窗口。桌面应用的主窗口是隐藏创建的(lib/main.js 的 createWindow():1280 × 820,最小 520 × 600),只有在 loadURL 完成后才 show();插件注入 index 的内联脚本运行在文档解析期,正好落在这个窗口不可见的区间里,所以用户看到的第一帧就是设定尺寸。尺寸因此只在两个时机请求:启动脚本在窗口仍不可见时设一次,客户端半只在用户改宽或高这两个数字时再设一次(点开关不算:它只决定下次启动用不用这个尺寸,永远不碰当前可见窗口)——模块激活本身不请求尺寸,切换主题预设、启动动画等其它设置也不请求尺寸(outerWidth 含窗口边框,通常不等于配置的宽度,所以旧实现会在每次切换时调用 resizeTo 并把窗口蹭偏;现在比较尺寸时留 ±2px 容差,边框造成的那 1px 差值不再被当成“尺寸不符”)(存储里还没有该字段的旧状态最明显:启动脚本按默认值设好,激活再设一次就会看到窗口突变)。启动脚本还用 sessionStorage 的 dsh-smooth-skin:session-window-sized 标记保证每个渲染会话只套用一次:F5 / Ctrl+R 保留用户拖过的尺寸与位置,新应用窗口或下次启动才重新套用并居中。存储里缺少该字段时,启动脚本沿用与 normalizeSize 相同的默认值,两半不会给出不同答案。同一个脚本还会把窗口在屏幕可用区域里居中(availLeft/availTop 一并计入,多显示器不会算到主屏原点上):moveTo 与 resizeTo 走同一条渲染进程→主进程的窗口通道;拿不到可用区域时跳过居中,避免把窗口挪到一个编造的原点。居中只发生在启动时;在设置里改尺寸只改大小、不动位置——每次应用前记下窗口原点,resizeTo 之后 moveTo 回同一个原点(并按可用区域收窄,窗口变大后也不会有一部分跑到屏幕外),所以改动宽高(包括连点数字步进)只会改大小,不会把窗口一点点蹭走;点开关则连一个窗口请求都不会发出去。请求超过屏幕可用区域时按可用区域收窄(比屏幕还大的窗口会把标题栏推到够不着的地方,而渲染进程没有移动窗口的能力),低于应用最小值时抬到 520 × 600。

主题预设共六项,设置 → Smooth → 主题 可选「跟随主题 / 初雪白 / 玄铁黑 / 苔痕绿 / 落日橙 / 深海蓝」,默认「跟随主题」:跟随主题时浅色实际用初雪白、深色用玄铁黑(由宿主明暗切换);选择固定预设会把宿主明暗临时切到该预设的色调,退回「跟随主题」或关闭皮肤时再还原。宿主「通用设置」里不再出现第二份日夜模式控件,只在顶部保留「通用设置」标题。 主题预设下方是**「标记颜色」(蓝 / 青 / 紫 / 绿 / 橙 / 玫红,默认蓝):这一个选择写入宿主的链接色 --dsw-alias-link,改变对话里的引用文件、重点提示**(链接色文字与行内代码)。左侧会话树里的工作区分组名称不染色(曾短暂用 --smooth-mark 染蓝,已移除;标题只保留字号/字重调整),会话标题保持原来的层级;切换立即生效并写入 localStorage,随「恢复默认设置」回到蓝色。 页面按扁平处理,不再有「光感」层:卡片/面板顶部的白色高光与内阴影(原 --smooth-light-wash / --smooth-light-edge)、主面板投到侧栏的环境阴影(原 --smooth-shadow-workspace)、以及选中控件上的局部反光都已移除;表面改为纯色平铺,只保留菜单/弹层与浮动卡片必要的分隔阴影(--smooth-shadow-1/2)。 插件页面的开关/按钮保持各自样式:皮肤不再全局改写 [role="switch"] 的尺寸与配色(旧规则会把插件自带的开关重绘成白底、大号深色圆钮并顶出容器,技能中心等插件页因此显示异常),只对宿主「通用设置」行里的原生开关重申 36 × 20 的默认几何。

  • 六套预设各自带完整调色板(背景、导轨、面板、文字、边框、品牌色、滑杆与状态色),切换时通过宿主的 overrideTokens 同时写 light / dark 两档;固定预设两档取同一套颜色,所以宿主明暗怎么变都是该预设。
  • 旧版本存下的 light / dark 会自动迁移成「初雪白」/「玄铁黑」。

覆盖范围

应用外壳与滚动条、图标栏与会话树、会话胶囊标签、整页设置与表单控件、插件管理页、输入框卡片、欢迎页、对话气泡、弹层与菜单。没有复制参考 HTML 的 SVG、图标字体或示例数据。首页房子、账号用量与帮助图标使用简短的本地 SVG,其余复用宿主图标。会话名称、工作区和插件列表来自宿主,会话树沿用当前已安装的 Codex UI 插件。欢迎页沿用宿主自己的堆叠与占比:标题(含原生「预览版」徽标)、工作区胶囊、输入卡片依次排列,卡片宽度由宿主 --dsh-composer-card-max-width 决定(与对话中同一个上限)。皮肤不再手工摆位——早期版本把标题、工作区行和卡片各自绝对定位(40.65% 系列偏移、卡片钉在下半屏并压到 150px 高、max-width:none),这些位置规则已删除,改为在 [data-conversation-scroll] 与 [data-composer-seat] 上显式 justify-content:center,所以整块在窗口里居中且占比与参考图一致。皮肤自己的文案只留下标题里的产品名前缀:那一行工作区(皮肤的「工作区」标签 + 原生工作区选择器 + 它带的分支)连下面那行「在这里开始新的对话。」都不画——卡片自己的信息栏已经写了工作区与分支,那一行只是重复一遍。隐藏写在 heroWorkspaceRow 上(display:none,其 ::after 随之消失),而「工作区」标签与那句话的规则仍留在源码里,改一行就能把整行放回来。欢迎页另按「主视图是什么」分两种状态:宿主停在首页时(应用刚启动、标签全被关掉,或点了 Home 把对话清掉而标签还在)只有标题,输入卡片不画;一旦有会话/面板/设置显示,卡片以 .26s 淡入(减少动态效果时不播)。开关由浏览器半按宿主当前视图发布在 body[data-smooth-home](src/layout.js),样式在 src/skin.js 读它,关掉皮肤时随皮肤一并撤掉。它不看标签数量:Home 会清掉对话但把标签留在标签条上,那一刻显示的仍然是欢迎页。输入卡片是按它自己的 data-composer-card 属性选中的,不依赖它在宿主里的层级——宿主把 composer 挂在自己的 slot 包装层里,子代选择器会漏掉它。

标题下方那张用量面板只出现在欢迎页(body[data-smooth-home],和隐藏输入卡片是同一个开关):新建会话的欢迎页仍只有标题与卡片,不画这块。它照 dsh-claude-style 的 Studio 用量卡来做:一张 12px 内边距、16px 圆角的卡(宽 min(560px, 100%),居中在标题下),卡头左边是「概览 / 模型」两个标签、右边是「全部 / 30 天 / 7 天」三段区间;概览页先排六格数字——会话数、消息数、Token 总量、活跃天数、高峰时段、最常用模型(3 列 × 2 行,窄窗口降到 2 列),再是 26 周 × 7 天的热力图(周日起始,grid-auto-columns:1fr 让格子随卡宽保持正方形、间距 3px、圆角 3px),最后一句「你的 Token 用量相当于 N 本《…》」。热力图的四档亮度是当天占窗口内最高一天的比例(20% / 40% / 65% / 100% 混合 --smooth-heat,空白天是 --smooth-text 的 11%),所以忙季和淡季能分开;单元格悬停用 ::after 显示「日期 — X Token」,最左、最右三列把浮层贴边对齐,避免被窗口裁掉;加载中整块画成 5% 高的骨架并轻微呼吸,解析失败(没有 Host 桥)则整块不画。模型页是同一份数据按 token 排序的列表:色块(按名次的蓝色阶梯)、模型名、token 数、占比,超过 6 行折叠成「再显示 N 个」。所有数字沿用宿主状态栏的 k/M/B 一位小数计数,计数类用千分位。数据来自 Host 半的 usage 动作:只读扫描 $DSH_HOME/sessions/ 下每个会话日志(JSONL,逐批次 zstd 帧),按 assistant/message 结算折叠成「本地日 → {四类 token, 消息数, 24 小时直方图, 按模型 token}」,同一 (turn, step) 的再次结算只算最后一次(和宿主 token 投影的规则一致);会话 id 取自日志所在的会话目录,所以「会话数」是区间内去重后的真实会话数,「高峰时段」取 24 小时直方图的峰值小时(上午 5 点 / 下午 5 点),「最常用模型」按 token 取第一名。日志按 (size, mtime) 缓存到 $DSH_HOME/cache/smooth-usage-v2.json,只重新解码写过的那些。 账号菜单在原生 settings.launcher 上增加头像资料、“剩余用量”和帮助入口,并按参考图排列,字号跟随内容区。点击“剩余用量”后,余额面板在该项下方展开,右侧箭头旋转向下,再次点击收起;充值余额四舍五入显示两位小数,赠金钱包为空或为零时隐藏,有赠金时保留其小数精度,不合并不同币种。金额按十进制字符串处理,不改变接口原始数据。打开菜单和展开时刷新,支持手动重试;读取失败不冒充零余额。账号资料和登录状态来自原生 account store,不写死昵称或 Plus 套餐。设置、反馈、登录、退出确认和账号通知仍由原生组件处理;关闭皮肤后撤销覆盖。账号弹窗从自己的左下角(头像所在的角)由小到大展开:transform-origin:left bottom,.2s 内 scale(.82)→1 轻微上浮淡入(will-change 让缩放走合成层)。资料卡插入弹窗后由皮肤显式武装入场动画(data-smooth-account-open),让可见的缩放等宿主行和资料卡都布局完再开始,不会因动画中途插入内容而卡住;.12s 的延迟版只作为未武装时的兜底。96% 不透明的面板也不再继承宿主 backdrop 模糊(几乎看不出来,却会让每帧缩放重算背景)。收回时宿主会立刻卸载原菜单,皮肤在关闭事件里复制一份菜单(data-smooth-account-leave)原地播放反向动画:从 1 缩回 .82 并淡出(.16s),动画结束即移除副本;点击外部、Esc、失焦和点原生菜单项都走这条路。减少动态效果时进出都直接切换。

首页通过宿主 remote.workspaceFiles.read() 读取当前会话工作区的 .git/HEAD,支持子目录、worktree 和 detached HEAD;切换工作区立即清除旧分支,聚焦窗口及每 30 秒刷新。读取失败或无 Git 仓库时不显示分支。宿主未公开完整的 Git status 接口,因此不展示待暂存文件数量。语音输入按钮沿用语音插件的原生插槽,仅在宿主提供该功能时出现。

输入框拆成上下两层:上层是一块独立描边的输入区(1px --smooth-line-strong、16px 圆角、输入底色,发送键单独放在它的右上角;有图片时正方形缩略图(124 × 124、16px 圆角、object-fit:cover 裁切、自身不描边;原生附件条本身只有 64px 高,会把方形预览裁掉一截,所以轨道项一起抬到 124px)横向排进同一个描边框,缩略图与输入区共用一条外框、图片和文字之间没有分隔线,占位提示与发送键落到图片下面一行;图片出现时输入区向上平滑展开(.15s 缓出,减少动态效果时不播动画)),下层是页面底色上的功能行。欢迎页与对话中结构一致:同一块描边输入区,没有图片时最小高度 42px(上下内边距各 10px,字号仍是 14px),占位提示两处都是单行省略(宿主在欢迎页原本会折成两行),对话中卡片下方那条渐隐也改为收到本皮肤的地板色而不是宿主基色,两个场景因此是同一块面。卡片高度交给宿主自己的排版(工作区、分支与自动审查都收在卡片底部的功能行里,不再在顶部占一行)(欢迎页的栈宽与卡片上限都取 --dsh-composer-card-max-width,皮肤不再给它 150px 的最小高度;工作区、分支与代理模式以小字排在输入框上方,卡片底部只留加号、自动审查与模型选择(加号在前、权限档在后))。文本区随内容增高,最高 300px(小窗口不超过 40vh),同时受页面剩余高度限制,超出后内部滚动。模式入口保持 13px、32px 高;模型入口保留原生紧凑控件(模型名 + 1017 徽标 + 当前 High + 右侧箭头),点击时在它上方展开一张面板:面板内容是模型名(点它滑到模型列表页)和一条不带刻度文字的思考强度滑杆,当前挡位(Off/Low/High/Max)显示在模型名上方,切换时轻微上浮淡入一次;当该模型的目录提供宿主自己的 Default 思考(宿主菜单那条「Default」只在 defaultEffort 未固定时出现)时,挡位名左侧多一枚 auto 小按钮:点击按宿主原生菜单同样的方式提交(请求里不带 reasoningEffort),把推理等级交还给 Provider 默认,挡位名显示 Default、滑杆暂时变暗表示它不再决定当前等级,再拖动或点滑杆任一挡位就回到显式挡位;蓝色填充到选中挡位,拖动经过挡位或点轨道时即时通过宿主模型目录写入 reasoningEffort;按住拖动时滑块跟手——指针位置换算成 0–1 的比例直接写到滑块元素的 --smooth-model-ratio 上(不触发 React 重渲染,所以面板里的图和粒子不会在拖动中重建),松手后把这个属性交回给当前挡位的比例,滑块带 .3s 缓动落到最近的挡位;每次展开面板时滑块直接停在当前挡位——属性在首帧绘制前就写好,那一帧临时关掉过渡(借用 is-dragging),下一帧再摘掉,所以不会看到从最左边滑过来的回放,松手只补一次最终确认,不再等防抖计时。原生模型菜单被这张面板替换:面板有两页——思考强度页和模型列表页,点模型名时两页像账号菜单滑出帮助页那样横向淡入淡出(opacity .2s + translateX 14px、.26s 高度过渡,面板高度跟着当前页走),选中模型、按返回或 Esc 再滑回强度页;面板本身的展开/收回只做轻量动效:首帧就是最终尺寸(不等待测量、不做高度展开),只叠一个 .16s 的淡入 + 6px 上浮(transform-origin 贴底),点下去立即可见内容;关闭(点外部、Esc、或再点一次模型入口)用 .15s 淡出下移,.16s 后卸载 portal,减少动态效果时两个方向都直接切换;面板的挂载点放在 composer 的浮层槽 conversation.input.overlay,它随任何会话的输入卡片渲染,所以新建会话的欢迎页点模型按钮也是同一张强度面板,而不是宿主原生模型菜单(原来的 conversation.input.right 要等输入状态就绪才渲染,欢迎页不满足,原生菜单因此漏了出来)——但前提是客户端的 inject 里声明了 remote.session:modelDirectories.directoryFor(sessionId) 要解析该会话的 remote 绑定,Cordis 会按调用方插件的 inject 校验 remote.session,缺了它全新会话(输入框还空着)解析模型目录会抛 cannot get property "remote.session" without inject,面板宿主判定未就绪、触发器监听装不上,点模型按钮就落回原生菜单(要等第一次输入把会话绑定起来才恢复);现在 inject 已声明,全新会话空输入直接点也能打开强度面板;滑杆轨道、填充、滑块和每个挡位的浅色小点使用独立的 --smooth-model-track/blue/thumb/dot 双色 token:浅色是浅轨深蓝,深色是深轨亮蓝。滑块放大到 28px,滑块本身是一颗跟随挡位滑动的球:原生滑块只保留命中区域并隐藏绘制;--smooth-model-ratio(0–1,用 @property 注册成可动画的 <number>)同时决定蓝色填充宽度和球的 left,点轨道或按方向键时只对这一个数做 300ms 缓动,填充边缘和球因此在每一帧都重合(填充按轨道内宽算,减掉左右各 1px 边框,正好停在球心),不会出现球到了、蓝条还在半路的情况;按住拖动时滑杆容器关掉这段过渡,填充和球一起直接跟手。按下有平滑缩放和阴影过渡,每个挡位在轨道上有一个浅色小点;小点画在蓝色填充之上、滑块之下:已填过的挡位仍能看到小点,滑块只盖住它正停住的那个挡位点。发送键钉在输入区右下角(没有图片时输入区只有一行,看起来就是右上角),沿用皮肤原来的 32px 主题蓝圆球:空内容时灰显,悬停提亮。输入光标也由皮肤自己画:一个 2px 的竖条,位置跟着选区矩形走、以 .1s 缓动滑到新位置,闪烁从硬切改成柔和明暗;输入法组合期间也沿用这支竖条(落在拼音末尾),减少动态效果时竖条不闪;软换行(Shift+Enter)之后 Lexical 把选区锚在段落元素上,直接量选区矩形只会得到整段的外框或空矩形,竖条因此会跳回句首——现在按 DOM 位置重建插入点(换行后的内容另起一行,段尾 之后落到下一行),几何完全量不出时交还原生光标、不再猜一个起点;发送成功后宿主以程序化方式清空草稿,浏览器会把 DOM 选区重新锚到空编辑器上却不派发 ,光标此前因此停在发送前的位置——现在皮肤同时跟随编辑器自身的内容变化(),内容变为空时直接把竖条归位到文本起点(跳过缓动、不做从旧位置滑回来的动画),清空后的光标状态与刚打开输入框时一致。普通占位提示缩短为“描述任务,/ 调用指令,@ 引用文件或对话”;工作区选择、不可用、计划和运行状态提示仍沿用原生内容。工作区、分支与代理模式选择(标准模式 / 创造模式)以小字排在输入框上方左侧(欢迎页卡片有这些,对话里没有工作区/分支):工作区按钮点一下宿主隐藏的 WorkspaceChip,只借它切换宿主的开合状态(菜单本身由皮肤画,见下文),锚点改到可见按钮上;分支按钮打开 Git 菜单,支持搜索、切换分支、创建并检出新分支,当前分支下显示未提交文件数;代理模式选择紧接在分支之后,这几个小字统一 12px、20px 高、同一条基线;整行与上方标题之间留 34px(卡片加 margin-top),不再贴着标题。输入框下面的功能行左侧只有「自动审查」与加号,右侧整行留给模型与强度选择(靠右对齐)。卡片顶部没有单独的信息条;轮次/步数、tok/s、token 总量与缓存命中、上下文环这几组状态仍由宿主的 dock 渲染在卡片下方居中,不再被钉到功能行上。两个入口由 Host 半的私有 监听提供:固定 argv、只接受带令牌的本地请求,令牌(写在 :页面手里的那份不会因为重新激活、重建或重启而失效,否则旧页面会被 403 挡在门外直到刷新);Host 半更新后需重启 DSH,未加载时分支按钮回退为复制分支名。编辑器、附件、发送与停止继续由原生组件处理。设置页里的表单控件(原生下拉与插件自己的数字输入)聚焦时保留一圈中性色( / )而不是品牌蓝,键盘导航仍然看得见焦点。;时,滑块上会出现一串向左跑的粒子(96 个,起点、行程、速度、大小、透明度各自不同;起点按 0.38 次幂压向右侧,左三分之一基本空、右半段最密,大小与亮度也随位置加权,所以是从滑块附近涌出、向左飞散、由多到少);同时进度条上还有一层——4 小片(直径 32–68px,只比轨道高一截),、不被染白:绝大多数位置仍是纯蓝,光斑透明度只有 0.13–0.25、 混合,只在经过的地方把蓝条提亮一点点(单点峰值约 +30/255 的红绿分量);光层(同样吃 ),所以白只会落在蓝条上、不会溢到滑块右边的空轨;每片用 把圆心压在轨道中线上(否则光斑圆心会掉到 24px 条子下方,只剩近乎透明的边缘,等于看不见),(填充之上、粒子之下),各自有起点、水平/斜向漂移方向与 4.5–8.5s 的往返周期(水平最大约 80px、纵向最多 14px),互不同步,看起来就是几小片白光在蓝底上随机游走;离开最高挡位就撤掉,减少动态效果时不画。输入框下方那条宿主数据区(轮次/步数、tok/s、token 总量、缓存命中与它自己的上下文环)整行移除,改为在模型选择按钮左侧画一枚 22px 的占用率圆环:蓝色弧线、浅灰底环、百分比居中,环的粗细与配色取 与主题 token(样式参考 的环形图)。点击圆环弹出浮层:顶部是「上下文已用 N%」与 ,下面一条系统提示词 / 工具定义 / 对话消息的三段占比条和带 token 数的图例,再往下是「会话统计」(模型用时、工具调用用时、首 token 平均、输出速度)与「Token 用量」(缓存命中、未缓存输入、缓存读取、输出);数字全部直接取宿主的 / / / 四个投影,口径与原来的上下文环和两个统计弹层一致,面板沿用宿主的浮层样式并在 Esc 或点外部时关闭。

新会话页的工作区切换(卡片上方带文件夹图标的那一枚)改为皮肤自己画的菜单:注册在宿主的 conversation.hero.workspace 槽上,用 decorate 以 priority: -10 盖住原生菜单。开合仍由宿主状态驱动(点可见按钮时仍点宿主隐藏的 WorkspaceChip 来切 pickerOpen,锚点被改到可见按钮上),选中仍走宿主的 onPick,所以草稿随工作区迁移、当前项高亮与宿主的选择语义都保留。菜单顶部是实时搜索框(按名字过滤,忽略大小写;标题为 default-workspace 的自动工作区显示当前语言的默认名),列表在 min(38vh, 300px) 内滚动、弹层本身按锚点下方剩余空间封顶(下方不足 230px 时向上翻),搜索行与「添加工作区…」行不随列表滚动;每行只有文件夹图标 + 工作区名(不显示路径),菜单宽 300px,选中行带宿主同款 14px 勾选图标(IconCheckOutlineRegular,与原生菜单同一枚图形)和 data-current 高亮,↑/↓/Home/End 移动、Enter 选择、Esc 关闭,活动行会滚进可视区;无匹配时给出「没有匹配的工作区 / 清除搜索」,一个工作区都没有时给出「还没有工作区」。添加工作区仍渲染宿主的目录流(conversation.hero.workspace.directoryFlow),沿用它的错误对话框并多一个「重新选择」。关掉皮肤时包装组件直接渲染原生 picker。

整页设置里,顶部标签条就是离开设置的通道(点标签、点「主页面」、按 Esc 都行),所以原生那枚关闭 ✕ 被去掉:它紧贴在页面自己的动作按钮(例如「通用设置」右上角的「打开配置文件」)右边,看起来像同一颗按钮上粘了个叉。参考图里那个位置本来也没有 ✕。除此之外皮肤不隐藏任何宿主内容:display:none 只出现在参考图里同样不可见的装饰性预览徽标、插件自己可折起的余额面板,以及这枚关闭按钮上。

点击账号菜单中的“帮助”,下方操作区以 220ms 淡入滑动过渡切换为帮助链接,并可点击“返回”恢复。四个入口分别为 Harness GitHub、开发者文档、社区插件和 cordis 论文,点击后通过宿主外链处理器在系统浏览器打开。切换期间保留原生设置、反馈和退出操作,非当前页不参与键盘导航。

工作机制

  • Host 半(lib/index.js):不读任何服务。Cordis 对未写进 inject 的服务名会直接抛 cannot get property "..." without inject,而事件不需要 inject,所以这一半仍然只导出对象形态插件 { name, inject: [], apply },apply 里仅 ctx.on('webserver/index-inject', …) 把启动动画的三行(style / html / script)和桥接地址/能力令牌推进每份 index 的注入表,监听器随插件一起释放。它不读状态、不碰 React 树;行必须是可 JSON 序列化的纯数据,因为静态部署会用同一张表。另有一个私有回环监听(/smooth-skin/git):status / checkout 两个 Git 动作,落地页用量表的 usage 动作(只读扫描会话日志,见上文),以及归档列表删除按钮的 session-delete 动作——它只在请求到达时才用 ctx.get 读取 sessions / workspaceRegistry / sessionPersistence,并用 ctx.waterfall / ctx.parallel 走归档准入事件,因此插件激活仍然不声明任何 inject;只接受带本次激活令牌的 POST,且只删除「已归档、没有仍在运行活动」的会话目录:删除前通过 workspace/session-activity 检查活动、workspace/session-stop 收尾,并清掉工作区记账与内存 store 条目(驱动客户端的 api-session/removed),插件释放时关闭。
  • Client 半(lib/client.js):注册 settings.section 页面、注入作用域样式表(自带 data-plugin 标记,避免被其他插件的热替换清理)、按状态调用 theme.overrideTokens(),并用 body[data-smooth-skin] 作为样式门控。状态存在浏览器 localStorage(键 dsh-smooth-skin:state),不经过任何 Host 通道。
  • Client 产物显式导出 name / inject / apply;服务 theme / slots / locale / layout / uiWorkspace / sessions / remote / remote.workspaceFiles 均声明在 inject 中,manifest 同时列出相应提供模块和设置、会话模块。
  • 首页权限选择通过 React portal 放入输入框顶部,模式选择放入底部;重新注册原生组件时保留它的全部注册选项(locale、store、inject、children 以及宿主后续新增的字段,只排除会冲突的 id 并调整 priority/registrant)和确认交互。children 不能丢:DSH 0.2.1 的模型选择用它在 conversation.input.model.label 下声明子插槽,丢了 renderSlot 会变成 undefined。关闭皮肤时撤销插槽覆盖和 portal,恢复原生位置;不手动搬移宿主 React 管理的 DOM。
  • 顶部标签通过 layout.selectPanel()、uiWorkspace.startSession() / openSession() 和原生设置 store 导航。打开对话或页面先显示临时标签;在内容区输入、修改表单或点击操作控件后固定,单纯浏览不会累积标签,也不会自动加入其他历史会话。双击标签也可固定。点击关闭按钮或在标签上按 Delete 仅关闭标签,保留对话数据;关闭当前标签后切换到相邻标签,全部关闭时回到首页,且首页由页面本身承担,不占标签位也不作为固定目标。标签状态属于本次客户端生命周期。通过 slots 发布的共享 store 读取侧栏宽度和设置状态;在原生设置插槽内渲染标签,保留设置窗口的焦点管理。三块外观选项调用原生 ,关闭时撤销插槽覆盖。

开发

node build.mjs                # 生成 lib/index.js 与 lib/client.js
node build.mjs --watch        # 监听 src/ 增量重建
node tools/activate-host.mjs  # 激活试验台
node tools/export-icons.mjs   # 从 src/boot.js 导出 assets/whale.svg
python tools/prepare-think.py # 从 assets/think/*.png 生成抠好的 cutout/*.webp(原图不入库,换图时才需要)
node tools/export-think.mjs   # 把 cutout 内联进 src/think.js(浏览器端无法运行时读文件)
node tools/startup-profile.mjs # 启动耗时与载荷剖析(改造前后对比用)
node tools/host-contract.mjs  # 更新 DSH 后核对宿主的类名、属性、插槽、令牌与方法
node --test tests/*.test.mjs

窗口尺寸这条路没有宿主 API 可走:渲染进程的 dshDesktop 桥(lib/preload-app.cjs)只有 browser/keyboard/shortcuts/updates/deviceInfo,桌面 IPC 列表里有 window-fullscreen 却没有尺寸通道,主窗口的尺寸在 lib/main.js 里写死也不持久化,Host 半所在的子进程协议只有 ready/platform-session/shutdown/fatal。可用的一条是渲染进程自己的 window.resizeTo——Electron 会照做,包括对隐藏窗口(用随应用同版本的 Electron 44 实测:1280 × 820 的隐藏窗口在加载完成后为 1441 × 901,即请求的 1440 × 900 加一像素边框)。

tools/activate-host.mjs 把 Cordis / 插件加载器里 unwrapExports → resolve → 读 name/inject/Config 的判定逐行抄出来,导入构建产物核对插件契约,并断言 Host 半只订阅 webserver/index-inject、没有多余的 ctx 访问——这两类问题都曾让 DSH 启动即失败或条目无法激活。

tools/host-contract.mjs 是更新 DSH 前的兼容性闸门。它直接读当前安装的 resources/app.asar,把 src/ 里用到的宿主 CSS module 类名、data-* 属性、slot、--dsw-* 令牌和服务方法逐项对照宿主 bundle;缺哪一项就列出哪一项,并退出非零。npm test 里的 tests/host-contract.test.mjs 会自动跑这条检查,找不到安装时跳过;安装路径不在默认位置时用 DSH_APP_ASAR 或 --asar <path> 指定。更新前可以先跑一次 node tools/host-contract.mjs --json > host-before.json 留快照,更新后再跑并对比。检查本身有正反两组用例,删掉任意一个类名、属性、slot、方法、令牌、属性值或包名都必须被报出来。

tools/export-icons.mjs 把开场鲸鱼从 src/boot.js 的常量导出成 assets/whale.svg。仓里只有一条鲸鱼:Host 半把路径当纯数据注入 index,不能运行时读文件,所以路径常量是唯一定义,assets/ 下的文件是它的镜像;node tools/export-icons.mjs --check 只核对不写入,tests/boot.test.mjs 断言两者逐字节一致,改了 src/boot.js 忘了导出会直接测试失败。

src/shared.js 被打包进两个产物,是配色与状态规范化的唯一来源。

思考强度面板的插图走一条和鲸鱼相同的“导出 → 内联”链路:tools/prepare-think.py 读取 assets/think/<名字>-Off/Low/High/Max.png(四张原图不入库:单张 3.5–4.5MB,且只在换图时才需要。把它们放回 assets/think/ 再跑这条命令即可,.gitignore 只忽略这一层 PNG),优先使用源图自带的透明通道(透明区里残留的白/黑 RGB 不会被当成底色而保留成不透明块),只有源图被压平成不透明背景时才从边框泛洪抠掉白色或黑色(角色内部的同色区域保留),再按文件名后缀生成 780px 宽的直通 alpha WebP 到 assets/think/cutout/;tools/export-think.mjs 再把它 base64 内联成 src/think.js 的 THINK_ART,build.mjs 把这份常量拼进 lib/client.js。--check 只核对不写入,tests/think.test.mjs 断言四张图存在、确实是 WebP、体积受限且与 cutout 逐字节一致——换图后忘了导出会直接测试失败。面板打开时按当前挡位显示对应插图(先匹配思考挡位 id,匹配不到按位置换算),拖动或点标签换挡时四张图常驻在 DOM 里交叉淡入。插图只属于 DeepSeek:只有模型目录里 DeepSeek、DeepSeek Account 这两组的模型才显示这块背景,其它 provider(例如 xiaomi)保持没有插图的纯面板;挡位文字和滑杆对所有带 reasoning 的模型照常显示。整块背景(插图连同底部虚化)比媒体区顶边低 6px,图片和渐变一起下移,不会出现“只有图动了、虚化没动”。

tests/client.test.mjs 直接执行浏览器构建产物,检查模块导出、依赖访问、设置页注册、启用/关闭和卸载清理。源码与构建产物必须完全一致,修改 src/ 后需重新构建。tests/git-bridge.test.mjs 在临时 Git 仓库里验证分支列表、未提交计数、建分支和带能力令牌的 Host 监听。

落地页用量面板也分两半:Host 半在 src/host.js 里扫描 $DSH_HOME/sessions/**/session.v<N>.jsonl[.zstd](逐帧解 zstd,撕裂的尾帧不算事件),折叠出每天的 token 四件套、消息数、24 小时直方图与按模型的 token;浏览器半在 src/usage.js 只做取数、区间过滤、名次与排版,src/bridge.js 是同一条桥的公共入口(Git 菜单与用量面板共用,页面没有桥时两边都回退)。tests/usage.test.mjs 用合成日志覆盖日记录、同一步骤重试只算一次、纯 JSONL、撕裂尾帧、(size, mtime) 缓存复用与窗口外日志跳过,真的起一个监听请求 usage 动作,并把 src/usage.js 放进 VM 里单测那几个纯函数(计数格式、热力档位、区间过滤、高峰时段、书目换算)。缓存文件落在 $DSH_HOME/cache/smooth-usage-v2.json,删掉它只会让下一次扫描变慢。

手工改 profile 的 cordis.patch.yml 时,备份名不要用 <patch>.bak-<纯数字>:dshmarket 把这种命名当成 DSH 自己 sanitizeProfile 留下的宿主恢复残留(备份行比当前 patch 多时会弹「宿主恢复」横幅),而那条横幅渲染的 IconWarningOutline16 在本版 DSH 的原语里不存在(只有 IconWarningOutlineRegular),会让插件市场整页崩成「插件市场无法显示」。用 -manual 之类的后缀把数字挪到名字中间即可绕开检测。 若仍出现旧版 lib/index.js 访问 host 的报错,先构建,再从托盘完全退出 DeepSeek Harness 并重新打开;只刷新页面不会清除 Host 进程已导入的模块。

结构

package.json      manifest:dsh.bundle.patch + dsh.client
cordis.patch.yml  bundle 补丁,插入 smooth-skin 行
assets/样例图/     原始 HTML 设计参考,不参与插件运行
icon.svg          插件页图标:36×36 透明底、固定色,package.json 的 icon 字段指向它(宿主按 36 / 30 渲染)
assets/whale.svg  开场鲸鱼的独立 SVG 文件,从 src/boot.js 导出,同样不参与运行
assets/think/     四挡思考强度插图:cutout/*.webp 入库用于生成面板背景;原始 PNG 不入库
locale/{zh,en}.json  插件管理页显示的标题与描述
src/shared.js     调色板、主题令牌映射、状态规范化、locale 词条
src/think.js      由 tools/export-think.mjs 生成的 THINK_ART(四挡插图的内联 data URL),不要手改
src/skin.js       作用域样式表
src/boot.js       启动动画唯一定义:注入 index 的样式/标记/脚本 + 设置页预览
src/watch.js      等待宿主表面:一个共享的 body observer + 按帧合并的检查
src/bridge.js     Host 私有桥的浏览器侧入口(Git 菜单与每日用量共用)
src/composer.js   真实 Git 分支读取、分支菜单、信息栏与原生控件 portal
src/usage.js      落地页的每日 token 表:26 周热力图、图例与悬停提示
src/account.js    账号资料、向下展开的真实余额与原生账号菜单扩展
src/layout.js     原生导航标签、归档行删除按钮、共享布局连接与三块外观选项
src/scrollbar.js  各滚动区域的粗细动画、空闲恢复与卸载清理
src/client.js     Client 半:设置页 + 令牌覆盖 + 样式注入 + localStorage 状态
src/host.js       Host 半:启动动画注入 + 私有 /smooth-skin/git 桥(Git、每日 token、归档会话删除)
build.mjs         构建脚本
tools/            激活试验台、图标导出、启动剖析与宿主契约审计(除客户端钩子外,也核对 Host 侧 `sessions` / `workspaceRegistry` / `sessionPersistence` 的桥接符号)
tests/            契约、样式作用域、宿主兼容性与交互测试
<br>
selectionchange
MutationObserver
/smooth-skin/git
git
跨激活保留
$DSH_HOME/cache/smooth-bridge-token
--smooth-line-strong
border-l3
拉到最高挡位
随机移动的光层
很淡的白光斑
蓝色底色保持原样
screen
裁在蓝色填充的宽度里
--smooth-model-ratio
translate:-50% -50%
z-index:1
--smooth-accent
assets/occupancy-ring/
~已用 / 窗口
contextPressure
contextBreakdown
sessionStats
tokenUsage
theme.setTheme()
  • 配色通过 theme.overrideTokens() 提供 light / dark 两套值;样式以 body[data-smooth-skin] 为开关。选择器优先使用宿主的 data-plugin-panel、data-plugin-package、data-composer-card 等真实属性。没有公开属性的部位只匹配已核对的 CSS 模块语义后缀,不绑定构建哈希;宿主大版本升级后需重新检查这些部位。
  • 启动动画由 Host 半的 bootInjections() 注入 style、html、script 三行纯数据,服务端与桌面端使用相同内容。鲸鱼路径直接来自宿主 FishLogo(23.16 × 17.04),坐标取 2 位小数(最坏偏差 0.005 用户单位 ≈ 176px 下的 0.04px):三行合计 5.2 KB(gzip 2.4 KB);样式行在注入时去掉注释与缩进(该表内没有任何字符串,所以只删读者才需要的东西),源码里的版本保持可读。标记里没有 SMIL,脚本里没有几何运算:注入脚本只做三件事——读 localStorage 与减少动态效果偏好(不满足就摘掉整层)、等 #root 挂载、最短停留后 340ms 淡出并在 640ms 后清理;应用未挂载时最多多等 8s。宿主 index 里的 <div id="root"> 是静态节点(已在 app.asar 的 dsh-web-frontend/dist/index.html 核对),所以等待只观察它的直接子节点,不再把应用挂载期间的每一次 DOM 变更都收进来;只有 #root 尚未解析出来时才退回观察整个文档子树,行为与之前一致。设置在下次页面加载时生效。
  • 启动开销的可复现度量在 tools/startup-profile.mjs:产物字节与 gzip、Host/Client 两半的模块解析求值、factory 与 apply 耗时,以及样式表的规则数、选择器数、:is(/:has( 计数。它用 V8(与 Chromium 同引擎)测量并扣除新建上下文的固定开销,因此样式重算、首屏绘制与传输时间不在其内,那几项只能在浏览器 DevTools 里读。浏览器侧的对照测量(真实时间、合成外壳、逐阶段计时)另有一份本地脚本,结论记在这里:外壳挂载期间样式重算是这份皮肤为启动付出的主要成本,其次才是 client 包的取回/解析/求值(约 6ms);src/watch.js 就是据此把等待宿主表面的 observer 收成一个、并把检查按帧合并——启动期对整篇文档的查询从每次 commit 一次降到每帧一次。字节上并没有收益:watch.js 净增约 2.8 KB(lib/client.js 151538 → 154294 字节),换来的是挂载期的查询次数,而不是包体。
  • 等待宿主表面(输入卡片、英雄区、账号菜单)统一走 src/watch.js:一个 MutationObserver(document.body, {childList, subtree}) 服务全部 watcher,回调合并到一帧一次,最后一个 watcher 释放时 observer 一并断开;每个 watcher 的首次检查仍是同步的,所以它装饰的元素在首帧就位。这样 React 挂载期的多次 commit 只换来一次整文档查询,而不是每个 commit 每个 watcher 各一次。账号菜单需要 aria-expanded,因此保留自己的 observer,但回调同样按帧合并。
  • 设置页的「预览」按钮把 src/boot.js 这份唯一定义再用一次:splashInnerHtml() 渲染同一个 #dsh-smooth-splash,时长仍取 SPLASH_HOLD_MS / SPLASH_SETTLE_MS,动画也来自同一份样式表,所以预览与真播不会各写一套。它 portal 到 body,只在点击后挂载,播完即卸载;若当前页面没有收到 Host 注入的样式表(旧 index、插件后装),客户端会补一份带 data-plugin 标记的副本,随插件卸载一起移除。系统开启「减少动态效果」时按钮禁用并在说明里写明原因——那种情况下启动也不会播。