dsh-claude-style 0.10.5 → 0.10.6

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,6 +2,70 @@
2
2
 
3
3
  All notable changes to `dsh-claude-style` are documented here, newest first.
4
4
 
5
+ ## [0.10.6] - 2026-10-05
6
+
7
+ [中文](#cn-0.10.6) | [English](#en-0.10.6)
8
+
9
+ <h3 id="cn-0.10.6">新增功能</h3>
10
+
11
+ - **聊天气泡动效**:提交消息的那一下,输入卡片原样浮起一份,一边飞一边把多余的收掉——工具栏左右两组贴着最近的角缩小、淡出,描边与阴影跟着形状收回,草稿里的字跟着变窄的形状一行一行重新排——落地时正好就是那条真实气泡;真实气泡在飞行期间藏着,落定后原位出现。设置页的「动画效果」选了「减弱」、两端不在同一屏、或起点读不到(快捷键与程序化提交)时都不飞;页面切到后台时另有定时器兜底,藏起来的消息一定会放出来。与对话区其他动画一起合并在设置页「对话」页的「聊天区动画效果」开关里,默认开启。
12
+ - **文件变更行**:从 run_code 程序里派发出去的写入与编辑按直接调用的样子显示——行尾带 `+n -m`,展开是改动卡片,路径可点开文件;失败与中断的行保留裁决信息、不再给路径链接,状态另有给读屏的说明。改动内容无法从参数推出的调用保留系统的输入 / 输出卡片。与对话区其他动画一起合并在设置页「对话」页的「聊天区动画效果」开关里,默认开启。
13
+ - **新到的文字先淡后实**:流式回答里新出现的字符从两成不透明度开始,约 0.12 秒内坐实到它自己的颜色,并按到达次序略作错开,读起来像文字正被写下。整段一次到达的内容(切会话、翻历史)、一次几千字的突发、以及刚被折叠重排过的文字都保持本色;主线程忙不过来时它自己让路,闲下来再继续。与对话区其他动画一起合并在设置页「对话」页的「聊天区动画效果」开关里,默认开启。
14
+ - **折叠不再瞬间切换,展开体像卷帘门一样拉下来**:读者点开或收起一行(工具卡片、思考行、命令卡片)或一个过程组时,高度逐帧变化,下方内容被真的推开或收回。门只走读者看得见的那一段,两千像素的展开体和两万像素的展开体在眼前的速度一样;展开体里是多张卡片时(代码卡片加输出卡片)整扇门一起走,不会先挤没能缩的那一张。主线程卡住时门停一下再接着走,不会跳变;读者自己滚动离开底部之后,收尾不会把他拽回去。它与其他对话区动画一起挂在「聊天区动画效果」这个开关上(默认开启),关掉后读者点开收起恢复系统原来的瞬开瞬收。
15
+ - **思考行与过程组自动开合**:模型还在思考时思考行自动开着,思考停下就收回去;运行中的过程组自动展开,这一段过程结束再收起,组体展开时组头的标签仍带实时细节的流光。读者自己按过的行或组,在当时的阶段里不再被改动;「详细」与「完全展开」两档不收纳组体,插件不碰它们。与对话区其他动画一起合并在设置页「对话」页的「聊天区动画效果」开关里,默认开启。
16
+ - **输入框插入符动效**:输入框里的光标改由插件自己绘制,移动时带一段位移过渡;提问卡片的作答框与排队消息的行内编辑框同样覆盖。设置页的「对话」页新增三档:每一格(默认,连打字也滑)、只在移动时(方向键与点击才滑,打字瞬时)、关闭(完全不动手,用浏览器自己的光标)。任何一次测量失败都会把原生光标还回来,不会出现光标看不见的情况。
17
+ - **聊天区跟随交给系统自己的跟随,封顶的工作过程不再掉队**:思考行收起、工具调用行出现这些结构时刻,贴着底部的读者现在被交还给系统自己的跟随,内容成片到达时不再停在离底部几十像素的地方;「标准」与「简洁」档里封顶的过程组(思考与工具输出收在一个带滚动条的组体里)同样补到底部,最新两行不再长期悬在下方。补到底部是按曲线走完的:零散到达的字收得住尾巴,一次涌进上百个 token 那样的厚块则以一段恒定速度滑过去,再落到末尾——流式输出时新加的一行把上文顶起来是平滑的,不再几十像素一下地跳。流式输出期间主滚动条的跟随也交给同一条曲线:系统原来是把末尾一帧写到位,现在这段位移由插件接管并滑过去;输出很快时最新几行会短暂拖在屏幕下缘之外,流一停就滑到位。读者自己发出的消息不受这条曲线影响:宿主把新消息滚进视野的那一下照旧一步到位,插件在消息到达前后短暂让开。读者自己滚动离开底部之后,插件不再插手,直到他自己回到底部。与对话区其他动画一起合并在设置页「对话」页的「聊天区动画效果」开关里,默认开启。
18
+ - **与 dsh-chat-ux 共存**:本机同时装着 [dsh-chat-ux](https://github.com/alm-allen/dsh-chat-ux) 时,上面这几项整体让位——那个插件实现了同一批交互,两套同时生效会互相拦点击、抢同一批按钮、往宿主同一个座位键位按同一优先级注册。设置页的「对话」页把「聊天区动画效果」与「输入框插入符动效」两行的控件禁用,控件本身仍显示你自己设的值,下面另起一行用强调色写明「该选项由 dsh-chat-ux 管理」;你的设置不会被改写,那个插件中途装卸也照常跟随。它不在时这些交互按本插件自己的开关运行。
19
+
20
+ ### 体验优化
21
+
22
+ - **权限菜单在窗口上方放不下时改为在控件下方展开**:权限分段控件的卡片固定朝上展开,窗口很矮时它的上缘顶出屏幕外,最上面几项点不到。现在上方容不下整张卡片时,它在控件下方展开。
23
+ - **上述动效跟随「动画效果」设置**:卷帘门过渡、聊天气泡动效、新文字淡入、插入符的滑动与闪烁、组头的流光此前只认系统的「减少动态效果」:设置页选「减弱」关不掉它们,选「总是」在系统开着减弱时也照样不动。现在它们读的是设置页解析出的那一档。
24
+
25
+ ### 问题修复
26
+
27
+ - **「更多模型」的二级弹层在指针离开后收拢**:指针从「更多模型」挪到一级列表的模型行上之后,二级弹层在宽限期结束后收起,一级弹层保持不动。修复前只要指针还在一级弹层里它就永远挂着,遮住一级列表。
28
+ - **模型选择器打开二级弹层的瞬间即定位**:此前打开动作先定位后标记打开,而定位只认已标记打开的卡片,于是二级弹层沿用上一次的位置(连同上一次的窗口宽度算出的坐标),要等下一次指针事件才可能被纠正。现在先标记打开再定位,每次打开都落在当前位置。
29
+ - **插入符拆除后在途的那一帧不再落笔**:瞬落一帧之后恢复过渡属性的那次补写走的是一处未登记的帧,插件拆除时取消不到它,时机凑巧时它会在拆除之后对已收回的图层写入一次。现在这帧与队列帧一样登记在案,拆除时一并取消,回调也不再越过拆除线。
30
+ - **「文件变更行」的开关此前不起作用**:关掉它之后,run_code 程序里派发出去的写入与编辑仍然按新的样式显示。现在关掉即交还系统原来的行,打开即时接管,不需要刷新页面。
31
+ - **拖入附件后,输入区工具栏打开的面板不再被附件区域遮住**:附件存在时,从输入区工具栏按钮打开的面板(模型选择、思考强度等)中间被上方那块附件区域盖住一大截,只在卡片上方和输入框下方各露出一条边,面板里的行点不到。现在这类面板完整显示。
32
+ - **侧栏账号区因故障停用后,设置快捷键不再失灵**:插件的侧栏账号区出错退役后,按 Ctrl+,(macOS 为 ⌘,,网页版宿主为 Ctrl+Alt+,)什么都不打开。现在按键交还宿主,宿主自己的设置快捷键照常打开设置。
33
+
34
+ ### 其他变更
35
+
36
+ - **昵称与封号页语言只存在设置里**:早先在宿主半边不认这两个字段时存进浏览器本地的值,会在打开页面后写进插件设置,本地那份随后删除;此后两项与其他设置一样只保存在宿主的设置表单里。
37
+
38
+ <h3 id="en-0.10.6">New Features</h3>
39
+
40
+ - **Send flight**: on submission the composer card lifts as it is and sheds what is extra on the way — the toolbar's two groups shrink and fade into their nearest corners, the hairline and shadow shrink with the shape, the words in the draft re-flow into the narrowing shape a line at a time — landing exactly on the real bubble, which hides while the flight is up and reappears in place when it lands. Nothing flies when the animation choice is Reduced, when the two ends are not on one screen, or when the origin cannot be read (a shortcut or a programmatic submission); a timer releases a hidden message if the page is in the background. It rides the Chat-area animations switch on the settings page's Conversation tab, on by default.
41
+ - **File change rows**: a write or edit dispatched from inside a run_code program is shown like a directly called one — the row carries the `+n -m` tail, expands into the diff card and its path opens the file; a failed or interrupted row keeps its verdict, drops the path link and announces its state to a screen reader. A call whose changed text cannot be derived from its arguments keeps the host's IN/OUT card. It rides the Chat-area animations switch on the settings page's Conversation tab, on by default.
42
+ - **New text fades in**: characters arriving in a streaming answer start at a fifth opacity and settle to their own colour in about 0.12 s, staggered slightly by arrival order, so the text reads as being written. A block arriving whole (a session switch, a page of history), a burst of thousands of characters and text that just reflowed from a fold all stay solid; when the main thread is too busy the fade gives way and resumes once it is free. It rides the Chat-area animations switch on the settings page's Conversation tab, on by default.
43
+ - **Folds no longer snap: a body rolls down like a door**: opening or closing a row (a tool card, a thinking row, a command card) or a process group now moves the height frame by frame, really pushing the content below away or pulling it back. The door only rolls the stretch the reader can see, so a two-thousand-pixel body and a twenty-thousand-pixel one move at the same speed in front of him; a body holding several cards (a code card plus an output card) rolls as one door rather than squeezing away the card that can shrink. A stalled main thread does not make it jump — the door pauses and carries on — and a reader who scrolled away from the bottom is not pulled back when the fold settles. It rides the Chat-area animations switch together with the entrance fade of an expanded body (on by default); with that switch off, a reader's own press snaps open and shut the way the host does it.
44
+ - **Thinking rows and process groups open and close by themselves**: a thinking row opens while the model reasons and folds back when it stops; a running process group opens and folds back once that piece of work ends, its header keeping the live-detail sweep while the body is open. A row or group the reader pressed himself keeps what he chose for that phase, and the Detailed and Fully expanded tiers — which do not cap a body — are never touched. It rides the Chat-area animations switch on the settings page's Conversation tab, on by default.
45
+ - **Composer caret motion**: the composer's text caret is drawn by the plugin and glides between positions; a question card's answer box and a queued message's inline editor are covered as well. The Conversation tab gains a three-way choice — Every move (the default, typing included), Explicit moves (an arrow key or a click glides, a keystroke lands instantly) and Off. A measurement that fails hands the native caret back, so a caret is never lost.
46
+ - **The chat area's scroll goes back to the host's own follow, and a capped piece of work no longer lags behind**: at the structural moments — a thinking row folding, a tool call row arriving — a reader sitting at the bottom is now handed back to the host's own follow instead of being left tens of pixels short by the burst of content; a process group capped in the Standard and Compact tiers (thinking and tool output kept in one scrolling body) is caught up the same way, so the last two lines no longer hang below the fold. Catching up runs on a curve: a trickle of characters settles softly, and a thick burst — a hundred-odd tokens arriving at once — glides at one steady speed before landing, so while text streams a new line pushes the text above it up smoothly instead of in forty-pixel steps. While content streams, the main scroller's follow runs along the same curve: where the host wrote its end in one frame, that displacement is now taken over and walked, so with a fast stream the newest lines trail just below the fold and slide into place once it stops. A message the reader sends is not affected by that curve: the host's scroll to bring it into view lands at once, and the plugin stands down around the arrival. Once the reader scrolls away from the bottom himself, the plugin stays out of it until he returns. It rides the Chat-area animations switch on the settings page's Conversation tab, on by default.
47
+ - **Coexisting with dsh-chat-ux**: with [dsh-chat-ux](https://github.com/alm-allen/dsh-chat-ux) installed on the same machine these effects stand down whole — that plugin implements the same interactions, and two copies intercept each other's clicks, press the same controls and register the same seat key at the same priority. The settings page's Conversation tab disables the Chat-area animations and Composer caret motion rows while each one goes on showing the value you set, with a line underneath in the accent colour reading "Managed by dsh-chat-ux"; your settings are not rewritten, and the hand-over follows that plugin being loaded or removed mid-session. With it gone they run on this plugin's own switches as usual.
48
+
49
+ ### Improvements
50
+
51
+ - **The permission menu opens below the control when it does not fit above**: the permission control's card always unfolded upward, so in a short window its top edge ran off-screen and the first rows could not be clicked. It now opens below the control when the card does not fit above.
52
+ - **Those animations follow the Animation setting**: the rolling door, the send flight, the token fade, the caret's glide and blink and the header's live-detail sweep used to read the system's reduced-motion query alone, so Reduced on the settings page did not stop them and Always did nothing while the system asked for reduced motion. They now read the resolved choice.
53
+
54
+ ### Bug Fixes
55
+
56
+ - **The More-models card folds once the pointer leaves**: moving the pointer from the More-models cell onto a model row of the first level now folds the second level after the grace, the first level staying put. Before, it hung open as long as the pointer stayed anywhere in the first level, covering its list.
57
+ - **The model picker places its second level at the moment it opens**: the open path used to place the card before marking it open, and the placement only reads cards marked open — so the card kept its previous position (with coordinates computed for the previous window width) until some later pointer event corrected it. It is marked open first and placed after, so every open lands on the current position.
58
+ - **The caret's teardown no longer leaves a frame that still draws**: the one-off frame that hands the transition back after an instant landing was untracked, so the teardown could not cancel it and, given the right timing, it fired after the teardown and wrote into a layer that had already been released. It is tracked like the queue's frame now — cancelled with the rest, and its callback stands down past the teardown.
59
+ - **The File change rows switch did nothing**: with it off, a write or edit dispatched from inside a run_code program still drew the new row. Off now hands the host's own row back and on takes it over, without a reload.
60
+ - **A panel opened from the composer's toolbar is no longer covered by the attachment area**: with an attachment in the composer, a panel opened from a toolbar button — the model picker, the reasoning-effort slider — lost its middle to the attachment area above it and showed only a strip above the card and another below the input box, so none of its rows could be clicked. Such panels now draw in full.
61
+ - **The settings shortcut keeps working after the sidebar account area is switched off by a fault**: once the plugin's sidebar account area failed and retired, Ctrl+, (⌘, on macOS, Ctrl+Alt+, in the web host) opened nothing. The keys now go back to the host, and the host's own settings shortcut opens the settings as usual.
62
+
63
+ ### Chores
64
+
65
+ - **The nickname and the account-hold page's language live in the settings alone**: a value kept in the browser's local storage while the host half did not know these fields yet is written into the plugin settings when the page opens, and the local copy is removed afterwards; from then on both are saved in the host's settings form like every other setting.
66
+
67
+ **Full Changelog**: [v0.10.5...v0.10.6](https://github.com/Nwflower/dsh-claude-style/compare/v0.10.5...v0.10.6)
68
+
5
69
  ## [0.10.5] - 2026-10-04
6
70
 
7
71
  [中文](#cn-0.10.5) | [English](#en-0.10.5)
package/README.en.md CHANGED
@@ -77,11 +77,25 @@ The settings page appears both in the settings dialog (the "Claude Style" tab) a
77
77
  | Appearance | Brand mark, Colours, Typefaces, Mascot, Where it appears |
78
78
  | Composer | Composer restyle, Home layout, Redraw the model picker (with Quick providers under it), Redraw the permission control |
79
79
  | Sidebar | Collapse the sidebar settings area, Sidebar search, In progress / Archived view |
80
- | Conversation | Turn status line, Chat / Trajectory tabs |
80
+ | Conversation | Turn status line, Chat-area animations, Composer caret motion, Chat / Trajectory tabs |
81
81
 
82
- Every feature that takes over part of the host's interface has its own switch; turning it off brings the host's original back at once, without a reload.
82
+ Every feature that takes over part of the host's interface has its own switch; turning it off brings the host's original back at once, without a reload. The conversation area's animations — the follow, the automatic folding with its rolling door, the text fade, the file change rows and the send flight — share one Chat-area animations switch; the caret keeps its own three-way choice.
83
83
 
84
- **Alongside other theme plugins**: with Colours and Typefaces set to Follow the host, the skin no longer rewrites the host's colours and fonts and keeps only its layout and controls; the colours are left to DSH itself, or to another theme plugin enabled at the same time. With [dsh-wallpaper-engine](https://github.com/elysia395/dsh-wallpaper-engine), for example, the wallpaper shows through the sidebar and the conversation, and the skin's own popovers take that plugin's glass, translucent and blurring the picture behind them.
84
+ **Alongside other theme plugins**: with Colours and Typefaces set to Follow the host, the skin no longer rewrites the host's colours and fonts and keeps only its layout and controls; the colours are left to DSH itself, or to another theme plugin enabled at the same time. With [dsh-wallpaper-engine](https://github.com/elysia395/dsh-wallpaper-engine), for example, the wallpaper shows through the sidebar and the conversation, and the skin's own popovers take that plugin's glass, translucent and blurring the picture behind them. **Alongside [dsh-chat-ux](https://github.com/alm-allen/dsh-chat-ux)**: that plugin implements the same chat-area interactions (the rolling door, automatic folding, the token fade, file change rows, the send flight, the drawn caret), and two copies of them intercept each other's clicks and press the same controls. So this skin stands down when it sees that plugin: the Chat-area animations and Composer caret motion rows on the settings page's Conversation tab have their controls disabled while each still shows the value you set, with a line underneath in the accent colour reading "Managed by dsh-chat-ux". Your settings are not rewritten, and they take effect as they are once dsh-chat-ux is removed.
85
+
86
+ **Chat-area follow**: at the structural moments — a thinking row folding, a tool call row arriving — a reader sitting at the bottom is handed back to the host's own follow, instead of being left tens of pixels short by the burst of content; a process group capped in the Standard and Compact tiers (thinking and tool output kept in one scrolling body) is caught up the same way. Catching up runs on a curve: a trickle of characters settles softly and a burst glides at one steady speed before landing. While content streams, the main scroller's own follow is walked along that curve too rather than written to the end in a single frame — with a fast stream the newest lines trail just below the fold and slide into place once it stops. A message you send does not go through it: the host's scroll to bring it into view lands at once. Once the reader scrolls away from the bottom himself, the plugin stays out of it until he returns.
87
+
88
+ **Automatic folding**: a thinking row opens while the model reasons and folds back when it stops; a running process group opens and folds back once that piece of work ends. A row or group the reader pressed himself keeps what he chose for that phase.
89
+
90
+ **Send flight**: on submission the composer card lifts as it is and narrows into the bubble as it travels, its words re-flowing into the shape, landing on the message row.
91
+
92
+ **File change rows**: a write or edit dispatched from inside a run_code program carries the `+n -m` tail and an expandable diff card, and its path opens the file; a failed or interrupted row keeps its verdict.
93
+
94
+ **New text fades in**: characters arriving in a streaming answer start faint and settle over about 0.12 s, staggered slightly by arrival order; a block arriving whole, a burst of thousands of characters and text that just reflowed from a fold stay solid.
95
+
96
+ **A rolling door for folds**: opening or closing a row (a tool card, a thinking row, a command card) or a process group moves the height frame by frame, really pushing the content below away or pulling it back. The door only rolls the stretch the reader can see, so any length moves at the same speed, and a body holding several cards rolls as one door. It rides the Chat-area animations switch, together with the entrance fade of an expanded body.
97
+
98
+ **Composer caret motion**: the composer's text caret is drawn by the plugin and glides when it moves; a question card's answer box and a queued message's inline editor are covered as well. The Conversation tab offers Every move (the default), Explicit moves and Off.
85
99
 
86
100
  **Mascot**: a pixel companion stands on the composer's top edge and changes its animation with what the agent is doing (thinking, writing and calling tools, several sessions at work, subagents, waiting on you, compacting the context, finished, failed, asleep). Follow the brand shows the pixel crab under Claude and Deepy the whale under DeepSeek; either can be picked for good, or none. Where it appears keeps it to the new-conversation page, or puts it in conversations as well.
87
101
 
package/README.md CHANGED
@@ -75,11 +75,25 @@
75
75
  | 外观 | 品牌标识、配色、字体、吉祥物、出现位置 |
76
76
  | 输入区 | 输入区重绘范围、首页版面、重绘模型选择器(及其下的快捷供应商)、重绘权限控件 |
77
77
  | 侧栏 | 折叠侧栏设置区、侧栏搜索、进行中 / 已归档视图 |
78
- | 对话 | 轮次状态行、对话 / 轨迹标签条 |
78
+ | 对话 | 轮次状态行、聊天区动画效果、输入框插入符动效、对话 / 轨迹标签条 |
79
79
 
80
- 每一项接管宿主界面的功能都有自己的开关,关闭后宿主原来的界面原样回来,不需要刷新页面。
80
+ 每一项接管宿主界面的功能都有自己的开关,关闭后宿主原来的界面原样回来,不需要刷新页面。对话区的那几项动画(跟随、自动开合与卷帘门、新文字淡入、文件变更行、聊天气泡动效)合成一个「聊天区动画效果」开关,插入符动效仍是自己的三档。
81
81
 
82
- **与其他主题插件同时使用**:「配色」与「字体」选「跟随宿主」后,本插件不再改写宿主的颜色与字体,只保留布局和控件;颜色交给 DSH 自己,或者交给同时启用的其他主题插件。例如与 [dsh-wallpaper-engine](https://github.com/elysia395/dsh-wallpaper-engine) 一起用时,壁纸透过侧栏与对话区显示出来,插件自己的弹层随壁纸插件的玻璃效果变成半透明并模糊背后的画面。
82
+ **与其他主题插件同时使用**:「配色」与「字体」选「跟随宿主」后,本插件不再改写宿主的颜色与字体,只保留布局和控件;颜色交给 DSH 自己,或者交给同时启用的其他主题插件。例如与 [dsh-wallpaper-engine](https://github.com/elysia395/dsh-wallpaper-engine) 一起用时,壁纸透过侧栏与对话区显示出来,插件自己的弹层随壁纸插件的玻璃效果变成半透明并模糊背后的画面。**与 [dsh-chat-ux](https://github.com/alm-allen/dsh-chat-ux) 同时使用时**:那个插件实现了同一批聊天区交互(卷帘门、自动开合、文字淡入、文件变更行、聊天气泡动效、插入符动效),两套同时生效会互相拦点击、抢同一批按钮。所以本插件在检测到它时让位:设置页「对话」页的「聊天区动画效果」与「输入框插入符动效」两行控件禁用,但仍显示你自己设的值,下面另起一行用强调色注明「该选项由 dsh-chat-ux 管理」;你的设置不会被改写,卸载 dsh-chat-ux 之后原样生效。
83
+
84
+ **聊天区跟随**:思考行收起、工具调用行出现这些结构时刻,贴着底部的读者被交还给系统自己的跟随,内容成片到达时不再停在离底部几十像素的地方;「标准」与「简洁」档里封顶的过程组(思考与工具输出收在一个带滚动条的组体里)同样补到底部。补到底部走的是曲线:零散到达的字收得住尾巴,成片涌进的文字以一段稳速滑过去,再落到末尾。流式输出期间主滚动条的新内容也沿这条曲线推进,不再一帧写到底;输出很快时最新几行会短暂拖在屏幕下缘之外,流一停就滑到位。你自己发出的消息不走这条曲线:宿主把它滚进视野的那一下照旧一步到位。读者自己滚动离开底部之后,插件不再插手,直到他自己回到底部。
85
+
86
+ **思考与过程自动开合**:模型还在思考时思考行开着,思考停下就收回去;运行中的过程组自动展开,这一段结束再收起。读者自己按过的行或组在当时的阶段里不再被改动。
87
+
88
+ **聊天气泡动效**:提交消息时输入卡片原样浮起、一路收成那条气泡,草稿里的字跟着形状重新排,落地正好接上真实的消息行。
89
+
90
+ **文件变更行**:run_code 程序里派发出去的写入与编辑带上行尾的 `+n -m` 与可展开的改动卡片,路径可点开文件;失败与中断的行保留裁决信息。
91
+
92
+ **新文字淡入**:流式回答里新出现的字符先淡后实(约 0.12 秒),并按到达次序略作错开;整段一次到达的内容、几千字的突发与刚被折叠重排过的文字保持本色。
93
+
94
+ **卷帘门过渡**:点开或收起一行(工具卡片、思考行、命令卡片)以及过程组时,高度逐帧变化,下方内容被真的推开或收回;门只走读者看得见的那一段,内容再长也是同一速度,多张卡片的展开体整扇门一起走。它与展开体的入场淡入一起挂在「聊天区动画效果」这个开关上。
95
+
96
+ **输入框插入符动效**:输入框里的光标由插件自己绘制,移动时滑过去;提问卡片的作答框与排队消息的行内编辑框同样覆盖。设置页的「对话」页有三档:每一格(默认)、只在移动时、关闭。
83
97
 
84
98
  **吉祥物**:输入框上沿站着一只像素小伙伴,随智能体的工作状态换动画(思考、写回答与调用工具、多个会话同时工作、子代理、等你操作、压缩上下文、完成、失败、睡着)。「跟随品牌」在 Claude 品牌下是像素螃蟹,在 DeepSeek 品牌下是小鲸鱼 Deepy,也可以固定选一个或者关闭;「出现位置」决定它只在新会话页出现,还是新会话页和对话页都出现。
85
99
 
package/docs/STYLE.md CHANGED
@@ -305,7 +305,7 @@ Its horizontal place is the one thing the skin takes over. ui-chat hangs the
305
305
  panel from the anchor's LEFT edge and only then clamps it into the viewport
306
306
  (`useStatDialog`, align `start`), so a trigger at the end of the composer row
307
307
  leaves the panel against the window's right margin instead of under the ring.
308
- `features/permissions/session-stats.js` reads the meter's right edge and the
308
+ `features/context-stats/session-stats.js` reads the meter's right edge and the
309
309
  panel's own layout width — `offsetWidth`, not its rect: the entrance scales the
310
310
  box, and a transformed rect is two per cent narrower than the one that settles —
311
311
  and writes `--dsh-claude-context-panel-left` with the mark that turns it on;
@@ -314,7 +314,7 @@ host's inline value. The reading is re-taken when the panel's box changes (a
314
314
  `ResizeObserver`) and when the viewport moves (`reposition('viewport')`).
315
315
 
316
316
  What the skin appends to it is the session's numbers
317
- (`features/permissions/session-stats.js`), read from the host's `sessionStats`
317
+ (`features/context-stats/session-stats.js`), read from the host's `sessionStats`
318
318
  and `tokenUsage` projections — the same durable whole-log values the host's own
319
319
  pills render — never by opening the host's stat dialogs. The labels and the
320
320
  duration / token templates come from the host's `chat` locale namespace, so the
@@ -13,7 +13,7 @@
13
13
  - 构建与源码:D1 拼接构建 · D5 模型文案是数据 · D18 源码布局与共用部件层 · D4 输入框样式门控
14
14
  - 宿主边界:D2 只做浏览器半边 · D3 宿主选择器纪律 · D19 宿主契约与构建编号 · D10 设置传输 · D11 私有路由与字符串安全 · D28 桌面标题带的一处读取 · D30 配色与字体可以交给宿主 · D31 Windows 顶栏透明
15
15
  - 运行时:D6 单一调度器 · D13 功能契约 · D29 功能开关 · D12 快速失败与功能隔离 · D9 样式性能
16
- - 功能:D14 账号表面 · D15 HDSL 契约 · D16 弹层基准 · D17 权限档位 · D20 滑动高亮 · D21 已归档列表 · D22 搜索面板 · D23 轮次状态行 · D24 吉祥物 · D25 封号彩蛋页的桌面顶栏 · D26 动画效果偏好 · D27 会话数字收进上下文弹层
16
+ - 功能:D14 账号表面 · D15 HDSL 契约 · D16 弹层基准 · D17 权限档位 · D20 滑动高亮 · D21 已归档列表 · D22 搜索面板 · D23 轮次状态行 · D24 吉祥物 · D25 封号彩蛋页的桌面顶栏 · D26 动画效果偏好 · D27 会话数字收进上下文弹层 · D32 搬来的聊天区交互
17
17
  - 分发:D7 行为层与皮肤同包 · D8 多主题
18
18
 
19
19
  ---
@@ -21,7 +21,7 @@
21
21
  ## D1. 零构建工具链:构建期逐字拼接成单文件
22
22
 
23
23
  - **决定**:`scripts/build.mjs` 按 `FRAGMENTS` / `STYLE_FILES` 的顺序把 `src/` 下的 JS 片段与样式表拼成 `lib/client.js`。所有片段共用一个工厂作用域:不写 import/export,顶层名字在整个产物里唯一;React 由构建头部经加载器 `require('react')` 取得,其他宿主包在用到它的功能里直接 `require`。片段用现代语法(`const` / `let`、箭头函数、可选链),基础缩进 4 空格。品牌 SVG、螃蟹的帧条 PNG 与厂商锁定标在构建时内联,字体、模型文案与小鲸鱼的帧图(D24,构建时复制到 `lib/deepy/`)走宿主半边的路由。
24
- - **构建检查**:`src/` 下每个 `.js` / `.css` 都在两份清单里;`%%TOKEN%%` 全部替换;输入框样式门控(D4);`:has()` 位置(D9);产物能被 `vm.Script` 解析;模型文案文档校验(D5);小鲸鱼的帧图与 `DEEPY_SHEETS` 表一一对应、每行的帧数、裁切框与静止帧合法(D24);`src/entry.js` 的 FEATURES 表与 `FEATURE_MAINS` 登记的功能主文件双向对上、`src/features/` 下每个目录都有安装项(`checkFeatureRegistry`)。产物写入构建编号(D19)。
24
+ - **构建检查**:`src/` 下每个 `.js` / `.css` 都在两份清单里;`%%TOKEN%%` 全部替换;输入框样式门控(D4);`:has()` 位置(D9);产物能被 `vm.Script` 解析;模型文案文档校验(D5);小鲸鱼的帧图与 `DEEPY_SHEETS` 表一一对应、每行的帧数、裁切框与静止帧合法(D24);`src/entry.js` 的 FEATURES 表与 `FEATURE_MAINS` 登记的功能主文件双向对上、`src/features/` 下每个目录都有安装项(`checkFeatureRegistry`);两个半边的偏好默认值逐键逐值相同(`checkPrefDefaults`,D10)。产物写入构建编号(D19)。
25
25
  - **理由**:DSH 的插件加载器没有相对 require,也没有资产 URL,打包器产出的分块与资产引用无处安放。加载器另有包内异步分包(`require.async('./client.<名字>.js')`),评估后不采用:产物 gzip 后约 288 KiB,首帧解析成本可忽略;大负载已按 D5 与 D24 在产物外按需加载(模型文案、帧图,字体走宿主路由);改用分包要让构建发出带 `__ModuleLoader__.load` 注册的多个文件、让桩宿主扮分包加载,并把共享作用域的片段在分包边界上显式导出,而收益只是省下一次本来就快的解析;这条管线目前只有官方包在用,第三方插件包的分包资源没有公开契约(D2)。
26
26
  - **代价**:顶层名字共用一个作用域,靠命名约定避免冲突(D18);没有按需裁剪,产物体积靠自律控制。
27
27
  - **重审条件**:加载器原生支持 ES module 相对导入与资产 URL;或引擎把插件包的异步分包写进公开契约,且产物 gzip 后超过约 1 MiB 时,重新评估分包。
@@ -65,7 +65,11 @@
65
65
 
66
66
  ## D6. 单一调度器
67
67
 
68
- - **决定**:`src/core/scheduler.js` 持有唯一常驻的 MutationObserver(`<body>` 子树,属性只看 `aria-label` / `aria-selected`),用 requestAnimationFrame 把变化合并成每帧一次刷新,按功能顺序调用各功能的 `sync()`。常驻的指针、键盘、焦点、滚动与尺寸监听也只注册在这里,经功能句柄上的钩子分发(D13)。例外只有两类,两处都要随自己的开合或状态撤下:弹层开着期间的指针与按键(模型选择器、快捷供应商、推理推条、hero 菜单、权限弹层各自在打开时挂、关闭或卸载时摘);调度器看不了、又不得喂给任何一轮刷新的状态——跟随窗口焦点的文本选区(`features/selection`,窗口事件需要捕获阶段)、主题翻转的 `data-ds-dark-theme`(`features/theme-flip` 自己盯,只影响自己的抑制标记)、分段控件的尺寸变化(`shared/sliding-pill.js`)。指针移动只经 `onActivity` 告诉功能「读者在」,钩子只记下时刻,一次扫过不触发刷新。键盘路径只认用户的按键:皮肤自己代发的键盘事件带标记(账号表面对宿主菜单派发的 Escape 带 `__dshHostMenuEscape`),调度器见到标记即跳过——那次代发只送达宿主的菜单,不得顺带触发全局 Esc 路由。刷新状态在调度器开头声明:订阅偏好时,已就绪的设置表单会同步通知并触发 `schedule()`。
68
+ - **决定**:`src/core/scheduler.js` 持有唯一常驻的 MutationObserver(`<body>` 子树,属性只看 `aria-label` / `aria-selected`),用 requestAnimationFrame 把变化合并成每帧一次刷新,按功能顺序调用各功能的 `sync()`。常驻的指针、键盘、焦点、滚动与尺寸监听也只注册在这里,经功能句柄上的钩子分发(D13)。例外在下面逐条列出,每一处都要随自己的开合或状态撤下:弹层开着期间的指针与按键(模型选择器、快捷供应商、推理推条、hero 菜单、权限弹层各自在打开时挂、关闭或卸载时摘);调度器看不了、又不得喂给任何一轮刷新的状态——跟随窗口焦点的文本选区(`features/selection`,窗口事件需要捕获阶段)、主题翻转的 `data-ds-dark-theme`(`features/theme-flip` 自己盯,只影响自己的抑制标记)、分段控件的尺寸变化(`shared/sliding-pill.js`)。指针移动只经 `onActivity` 告诉功能「读者在」,钩子只记下时刻,一次扫过不触发刷新。键盘路径只认用户的按键:皮肤自己代发的键盘事件带标记(账号表面对宿主菜单派发的 Escape 带 `__dshHostMenuEscape`),调度器见到标记即跳过——那次代发只送达宿主的菜单,不得顺带触发全局 Esc 路由。刷新状态在调度器开头声明:订阅偏好时,已就绪的设置表单会同步通知并触发 `schedule()`。
69
+ - **观察者的例外**:皮肤自己的记账节点带 `data-dsh-claude-quiet`(QUIET_ATTR)——容器内部的改动(含容器内文字的改写)、以及增删本身带这个标记的节点,都不算变化。搬来的插入符动效每帧建测量探针、画自己的光标(D32),没有这一条,读者在输入框里每敲一个字都会换来一轮刷新。
70
+ - **第二类例外:搬来的聊天区交互自带的观察者**(D32)。跟随守护、思考行与过程组的自动开合、卷帘门过渡、token 淡入与插入符动效各自持有自己的 MutationObserver / ResizeObserver / 定时器与捕获监听。它们不并进调度器,理由是观察面不同:调度器的观察者只看 `aria-label` / `aria-selected` 两个属性(这一层是皮肤与宿主共享的刷新入口,扩大它的过滤面会让无关变化喂进每一轮刷新),而这些功能要盯的是流式容器子树里的字符变化、`hidden` 与 `data-streaming` 这类属性;插进调度器等于让每一次流式变化都走一遍全部功能的 `sync()`。每一处都随自己功能的安装/卸载成对增删,元素级观察器在元素离开时撤销(`process-follow.js` 的 `sync()`),并在冒烟里验证卸载后不留监听与标记。
71
+ - **第三类例外:跟随另一个聊天插件装卸的一处观察者**(D32)。上游包按读者的开关装卸,装卸时它只改 `<head>` 里的一个 `<style>`,而调度器观察的是 `<body>` 子树,看不到这一处;所以 `src/shared/peer-plugin.js` 在有订阅者期间挂一个只看 `<head>` 子节点增删的观察者,最后一个订阅撤下时立即断开。它带回的是一次存在性重算与一轮环境通知(偏好监听重跑、座位键位重注册),不是每帧的开销。
72
+ - **环境通知**:系统的「减少动态效果」中途翻转、或另一个聊天插件装卸,都不改读者的偏好值,但都会让功能的取舍变化。两处都走同一个动作:`refreshMotionAttribute()` 重解动画档位、`notifyEnvironmentChange()` 单纯重跑一遍偏好监听(`src/core/prefs.js`)——功能因此不必各自去听媒体查询或去猜。
69
73
  - **理由**:各功能自挂观察者与监听会互相干扰、重复扫描、难以卸载干净。
70
74
  - **代价**:每个 `sync()` 必须能快速提前返回,并且只在值变化时写 DOM(同值写入也会让元素样式失效);页面持续变化时每帧都有一次刷新,其开销见 D9。
71
75
 
@@ -92,6 +96,7 @@
92
96
  ## D10. 设置传输只走官方 Config 表单
93
97
 
94
98
  - **决定**:客户端只有 `configForms` 一条传输:绑定宿主实际提供的命名空间(候选依次为加载器入口 id、包名、`cordis.patch.yml` 插入的 id,以宿主的命名空间目录为准),读写都经表单控制器(值、写队列、修订号栅栏)。命名空间晚到时订阅目录,到达即绑定。宿主半边导出 `Config` 作为 schema:只有 `.volatile()` 字段进表单;schemastery 用顶层 await 加守卫导入,解析不到时 `Config` 为 `undefined`、皮肤照常加载——这是导入规定的唯一例外。设置席位注册为 `plugins.bundle.config`(键为包名);宿主没有 `configForms` 时才注册整页的 `settings.section`。
99
+ - 字段只有一份声明:宿主半边 `host/settings.js` 的 `PREFS_DEFAULT` 生成 `Config`;浏览器半边 `src/constants.js` 的 `PREF_DEFAULTS`(默认值)与 `PREF_CHOICES`(取值集合)生成表单答复之前的初值与读到值时的规整。构建导入 `PREFS_DEFAULT`,与 `PREF_DEFAULTS` 逐键逐值核对(`checkPrefDefaults`)。偏好只存在宿主的表单里:早先存进浏览器本地存储的昵称与封号页语言,在表单第一次带值时写进表单,表单持有自己的值后删掉本地那份。
95
100
  - **代价**:`Config` 的顶层 await 让宿主半边晚一步求值(加载器本就等待导入,无实际影响)。
96
101
  - **重审条件**:宿主提供不依赖 `Config` 的设置注册方式,或 `configForms` 契约再变。
97
102
 
@@ -113,7 +118,8 @@
113
118
  - 功能隔离:teardown 最先经 `ctx.effect` 注册且幂等;每个功能单独安装,装不上的报告一次并退役;每轮刷新里每个 `sync()` 单独执行,连续失败 3 轮即报告并退役(`ui.retire`)。调度器自身装不上时整体回滚到宿主原样。
114
119
  - 退役就是运行该功能自己的 teardown,并交还它接管的宿主界面:页脚接管(`FOOTER_ATTR`)与输入框重绘(`COMPOSER_ATTR`)的门控强制关闭;权限控件替换的宿主控件由它自己的标记把守,随它的 teardown 交还。每个功能的 teardown 是它 DOM 与标记的唯一清理。
115
120
  - 需要隔离、但错误必须可见的地方用 `reportError`:`notifyAll` 逐个通知监听函数,某个出错就报告、其余照常;卸载时单个功能失败同样报告后继续;功能开关(D29)关闭某个功能时它的 teardown 抛错,同样报告,这个功能在本代内不再装回。
116
- - 允许的 catch 只有以下几类,每处写明原因:上述隔离;宿主以抛错表达的预期状态(设置表单的 `set()` 拒绝不属于该配置的字段;`modelDirectories.directoryFor` 对还没建立的会话抛错,下一轮重试;`sessionQuery.readSession` 以 `SESSION_QUERY_CORRUPT_SESSION` / `SESSION_QUERY_SESSION_NOT_FOUND` 表示某个存档读不了或在列出后被删,内容搜索只把这一个会话记为空并记一条警告);Promise 上表示「宿主半边没有应答」或「宿主自己会提示」的失败回调;小鲸鱼的帧图矢量化两处——Cache API 拒绝读写时视为缓存永远未命中(每次页面加载重转,控制台警告一次),表图能解码但转换失败时按读者要求回退为原 PNG 播放(控制台警告一次)。
121
+ - 允许的 catch 只有以下几类,每处写明原因:上述隔离;宿主以抛错表达的预期状态(设置表单的 `set()` 拒绝不属于该配置的字段;`modelDirectories.directoryFor` 对还没建立的会话抛错,下一轮重试;`sessionQuery.readSession` 以 `SESSION_QUERY_CORRUPT_SESSION` / `SESSION_QUERY_SESSION_NOT_FOUND` 表示某个存档读不了或在列出后被删,内容搜索与用量汇总只把这一个会话记为空并记一条警告);文件变更行解析流式工具的原始参数——参数是逐块到达的,半截 JSON 是这段流水的正常中途状态,解析不了就当作「这一行的改动推不出来」、交回宿主的输入 / 输出卡片(`features/chat-files/file-row-model.js`,那里写着原因);Promise 上表示「宿主半边没有应答」或「宿主自己会提示」的失败回调;小鲸鱼的帧图矢量化两处——Cache API 拒绝读写时视为缓存永远未命中(每次页面加载重转,控制台警告一次),表图能解码但转换失败时按读者要求回退为原 PNG 播放(控制台警告一次)。
122
+ - 宿主半边另有以下几处,同样每处写明原因:`webServer.register` 以抛错拒绝已被别的插件占用的路径——逐条注册,拒绝的那条记警告,其余照常注册(宿主半边的纤程失败会连带撤下浏览器半边的整个包);卸载时逐个运行路由的撤销函数,单个失败记警告后继续;删除会话的请求体不是 JSON 时按客户端错误答 400;删除过程中的任何故障答 500 并带上错误信息;成本计量插件的账本是别的插件的文件、读取时与它的写入方没有协调,解析不了时当作没有账本、改用本地折算;用量汇总自己的缓存只省工作量,读不了时记警告并整体重算覆盖,写不进时记警告、本轮算出的结果照常返回。宿主服务是否存在一律用 `ctx.get` 的返回值判断(cordis 对缺席的服务返回 `undefined`,不抛错);可选的文件(读者自己放进来的字体、HDSL 头像、尚未建立的会话根目录)用 `existsSync` 或 `statSync(…, { throwIfNoEntry: false })` 判断,不用 catch。
117
123
  - **理由**:被吞掉的错误会在别处以更难排查的方式出现;隔离只负责不让一个功能拖垮其他功能,失败本身必须留下记录。
118
124
  - **代价**:功能失效时界面上没有提示,只有控制台记录;依赖同一宿主服务的功能会一起退役(smoke 的「会话列表故障」用例覆盖)。
119
125
  - **重审条件**:宿主提供插件级的错误上报时,把报告接过去。
@@ -122,7 +128,7 @@
122
128
 
123
129
  - **决定**:
124
130
  - `src/entry.js` 的 FEATURES 表(`{ name, handle?, pref | ungated, install }`)同时决定安装顺序与刷新顺序:刷新顺序就是这个顺序,每轮跳过句柄此刻不存在或没有 `sync` 的功能。每项必须声明开关或不设开关的理由(D29)。
125
- - 调度器只认功能句柄上的可选钩子(类型定义在 scheduler.js 开头):`sync` / `owns` + `close('outside')` / `onPointerDown` / `close('escape')` / `close('composer')` / `onInput` / `onFocusIn` / `reposition('viewport' | 'composer')` / `onCopyChange` / `onKey` / `onActivity`;没实现的钩子直接跳过,每个功能保留自己的关闭方式。
131
+ - 调度器只认功能句柄上的可选钩子(类型定义在 scheduler.js 开头):`sync` / `owns` + `close('outside')` / `onPointerDown` / `close('escape')` / `close('composer')` / `onInput` / `onFocusIn` / `reposition('viewport' | 'composer')` / `onCopyChange` / `onKey` / `onActivity`;没实现的钩子直接跳过,每个功能保留自己的关闭方式。`onKey` 收到读者的每一次按键,接手这个键的功能自己调用 `preventDefault()`:调度器不认识任何具体的快捷键,接手它的功能退役后,这个键交还宿主。
126
132
  - `retire` 按 name 或 handle 匹配:只匹配到 handle 时只停止 sync、不拆安装(设置页的 `settingsNav`)。
127
133
  - 功能拆出的部分写成顶层工厂 `createX(...)`:状态留在工厂自己的闭包里,访问器与回调经参数传入,不伸手进别的闭包;名字以功能开头(`createAccountProfile`)。
128
134
  - **理由**:加一个功能只需在 FEATURES 表加一行、在句柄上实现钩子;调度器不认识任何具体功能,没有要手工同步的清单。
@@ -271,7 +277,8 @@
271
277
 
272
278
  - **决定**:
273
279
  - 宿主画在输入卡片下方的统计行(轮次与步数 · tok/s、总 tokens · 缓存命中)与它的两张明细面板一起隐藏(`features/composer/inline-bar.css`):输入行只剩右端的上下文计量环,数字进它自己的那个弹层。行仍留在文档里,只为让「性能与用量」这一档(详细 / 简洁)的结构还读得出来——详细档给出宿主那两组所有的行,简洁档只给四个数字:总用时(模型用时加工具调用用时,宿主没有这个合计,用皮肤自己的文案)、首 token 平均、输出速度与缓存命中,不带分组小标题,直接排成 2×2 网格。
274
- - 数字按**数据**读,不读渲染结果:`sessionStats` 与 `tokenUsage` 是宿主在整份日志上折出来的会话投影,会话面交出它们的按 key 读取面(`session.projections.faceOf`,也就是宿主自己 `useProjection` 走的那个座位;`features/permissions/session-stats.js` 经 `sessions.binding(id)` 取)。面板打开时按当前值填,订阅到新帧就地重写:不再按胶囊、不再等宿主的对话框挂载。
280
+ - 这是独立的功能 `contextStats`(`src/features/context-stats/`),与权限控件共用 `permissionsControl` 开关(D29)。宿主的两张明细对话框只在它安装期间隐藏(`<body>` 上的 `SESSION_STATS_ATTR`),关掉即交还宿主。计量环按结构认:输入区底部那一行里画着圆环(`svg > circle`)的对话框触发按钮,统计胶囊不画圆环。
281
+ - 数字按**数据**读,不读渲染结果:`sessionStats` 与 `tokenUsage` 是宿主在整份日志上折出来的会话投影,会话面交出它们的按 key 读取面(`session.projections.faceOf`,也就是宿主自己 `useProjection` 走的那个座位;`features/context-stats/session-stats.js` 经 `sessions.binding(id)` 取)。面板打开时按当前值填,订阅到新帧就地重写:不再按胶囊、不再等宿主的对话框挂载。
275
282
  - **文字与格式向宿主对齐**:标签、时长模板与分词符都取自宿主的 `chat` 语言命名空间(`locale.bind('chat')`);宿主在原始数值之上做的四条格式规则(`formatDuration`、`formatTokensPerSecond`、`formatExactTokens`、`formatCacheHitPercent`)在皮肤里各写一份,函数名对着来源,输出与宿主自己的显示逐字符相同。
276
283
  - 面板靠结构辨认(非模态、带 `dl`),开合靠按宿主的触发按钮(`aria-expanded` 是权威);悬停进出走共享的停留与宽限,并跟随「悬停打开弹层」偏好,指针停在面板里时不收起。既然面板是宿主挂载的,皮肤给它打 `data-dsh-claude-context-panel` 并让它取卡片那套一次性入场(同一个 4px、同一个 0.15 秒)——同一个动作在哪个控件上都以同样的方式出现。
277
284
  - 面板的左右位置由皮肤重写:宿主从定位点的左缘放下弹层(`useStatDialog` 的 `align: 'start'`),输入行右端按这个位置只够把它挤到窗口右缘,离计量环很远。皮肤量出计量环的右缘与弹层自己的布局宽度(入场动画把盒子缩到 0.98,量矩形会少两个百分点,所以读 `offsetWidth`),写成 `--dsh-claude-context-panel-left` 并同时打上 `data-dsh-claude-context-aligned`,样式表用 `!important` 读取——与 hero 菜单同一套交接。盒子变化时由 ResizeObserver、视口移动时由 `reposition('viewport')` 重新量。
@@ -290,7 +297,7 @@
290
297
  ## D29. 功能开关:每个功能声明开关或不设开关的理由
291
298
 
292
299
  - **决定**:
293
- - FEATURES 表(D13)的每一项二选一地声明 `pref: '<偏好键>'` 或 `ungated: '<理由>'`;两个都没写、两个都写,或 `pref` 不是 `host/settings.js` 的 `PREFS_DEFAULT` 里的键,构建拒绝(`checkFeatureSwitches`)。划分标准:替换或挪动宿主原有的控件与区域、或往界面上加新可见元素的功能设开关;只修宿主的毛病而不改变功能的(`selection`、`themeFlip`)、从属于另一个已有开关的(`copy` 与 `heroMenu` 跟随输入框改造的范围,`effort` 跟随模型选择器——宿主的工作强度在宿主自己的模型菜单里——`quickProviders` 是模型选择器的设置项)、只在点击时出现的(`ban`)以及设置页本身不设。
300
+ - FEATURES 表(D13)的每一项二选一地声明 `pref: '<偏好键>'` 或 `ungated: '<理由>'`;两个都没写、两个都写,或 `pref` 不是 `host/settings.js` 的 `PREFS_DEFAULT` 里的键,构建拒绝(`checkFeatureSwitches`)。划分标准:替换或挪动宿主原有的控件与区域、或往界面上加新可见元素的功能设开关;只修宿主的毛病而不改变功能的(`selection`、`themeFlip`)、从属于另一个已有开关的(`copy` 与 `heroMenu` 跟随输入框改造的范围,`effort` 跟随模型选择器——宿主的工作强度在宿主自己的模型菜单里——`quickProviders` 是模型选择器的设置项)、只在点击时出现的(`ban`)以及设置页本身不设。两个功能可以共用一个开关:`contextStats`(D27)与 `permissions` 都声明 `permissionsControl`,两者一起接管输入行的底部,一起开关。
294
301
  - `pref` 是 `FEATURE_PREF_DEFAULTS`(`src/constants.js`,全部默认开)里的键时由 `entry.js` 统一执行:启动时关着的功能不安装;偏好每次被采用时逐个对照,关掉就运行这个功能自己的 teardown、删掉它的句柄,交还它接管的宿主界面,打开就重新安装,都不刷新页面。调度器的两张清单列出全部句柄名,每次使用时检查句柄在不在。其余的 `pref`(`composerScope`、`homeLayout`、`modelPicker`、`collapseFooter`、`mascot`)由功能自己读。退役过的功能(D12)在本代内不因偏好变化装回。
295
302
  - 设置页按分页组织(通用、外观、输入区、侧栏、对话),每页的行定义各在 `src/features/settings/settings-tab-*.js`,控件在 `settings-controls.js`;从属于另一项的行紧跟在它下面、两行之间不画分隔线,父项关闭时变灰、不能操作。
296
303
  - **理由**:新功能登记时就要回答「读者能不能关掉它」,不会等到发布后才发现漏了;开关的语义统一为「关掉 = 宿主原样」,每个功能的 teardown 本来就负责交还宿主界面(D12),开关只是在运行中调用它。
@@ -313,3 +320,15 @@
313
320
  - **理由**:Claude Code 桌面端的侧栏与主界面都通到窗口顶端,中间一条分隔线从上到下;宿主的顶栏是另一种颜色的横条,三个按钮还单独托在一块同色矩形上,页面这一侧一变(遮罩、壁纸插件)就露出来。外阴影只画在列的盒子外面,列里的内容和宿主的布局都不动,拖动列宽与收起动画时自动跟着列走。
314
321
  - **代价**:认探针靠它内联样式里的 `--dsw-specific-sidebar-fill` 子串,属于宿主 preload 的内部写法、不是公开契约(D3 的例外,记在这里);宿主改了写法,按钮区退回宿主的底色,不会出错。收起侧栏时宿主立即移除拖动把手,顶栏里那段分隔线比列宽动画先消失。
315
322
  - **重审条件**:宿主的桌面端提供顶栏透明的选项或按钮区底色的公开接口时,改用宿主的。
323
+
324
+ ## D32. 搬来的聊天区交互:一份宿主契约、一套自己的前缀、一套自己的偏好
325
+
326
+ - **决定**:从 dsh-chat-ux 搬入的聊天区交互(token 淡入、思考行与过程组自动开合、卷帘门过渡、增强跟随、组体跟随、文件变更行、插入符动效、聊天气泡动效)按本仓库的功能归档分目录:`chat-follow`、`chat-fold`、`chat-reveal`、`chat-files`、`chat-send`、`caret`(D18)。它们只读宿主自己写的属性;聊天区的整份宿主契约(流式容器、工具调用行、过程组、会话滚动条、输入框与回显气泡等)集中在 `src/shared/chat-dom.js` 一张表里,不论读它的功能有几个——一份契约放在一处,对照两套实现时能一眼看完宿主给了什么,也不会有第二个功能再写一份同样的选择器。功能自己写出的标记(DOM 标记、类名、`::highlight()` 名称、自定义属性、关键帧名称)一律换成 `data-dsh-claude-` 前缀并留在自己的目录里,与上游包同时装着时两套实现读不到对方的状态。
327
+ - **三处不止读标记的地方**:文件变更行接管宿主 `tool.call.toolview` 座位上的 `edit` 与 `write` 两个键位(优先级 -1,低于宿主自己那两个),改动派生与宿主的卡片模型只差「不排除子调用」这一条;聊天气泡动效在 `<body>` 末尾挂一个临时的替身,替身带 `inert` 与 `aria-hidden`,摘掉身份标记但保留样式钩子,飞行期间给真实那一行写标记把它藏起来(保留布局盒);增强跟随在流式输出期间接管会话滚动条的跟随(`src/features/chat-follow/chat-follow.js` 的滑行)——宿主每次内容增长都把末尾一帧写到位,滑行在同一帧绘制之前把这次位移撤回并交给同一条曲线滑过去,同时把宿主自己那个「回到底部」按钮临时藏起来(只写自己的标记,样式表隐藏,宿主的状态不动);读者自己发出的消息到达时(消息行或回显出现、换掉)滑行整体让开一段,宿主把新消息滚进视野的那一下照旧一步到位。这一处上游没有,是本仓库自己的行为。
328
+ - **设置**:这些功能共用皮肤自己的偏好条目与设置页(D10、D29),不动上游 `dsh-chat-ux` 条目的配置。对话区的这几项合成一个偏好键 `chatAnimations`(设置页的「聊天区动画效果」),登记在 `FEATURE_PREF_DEFAULTS`(D29 的常规机制):开关关掉的含义是「一次都不动手」,而这五项里有三项本身就是靠整个功能卸载撤回的——文件变更行接管宿主的两个座位键位、卷帘门过渡拦下读者点击、自动开合给宿主插入的展开体加快淡入——所以这一项关掉时五个功能一起卸载、各自把界面交还宿主。插入符动效仍是自己的一档三选(`caretMotion`),不并进这一个开关。需要随偏好出现的样式规则由功能把标记写到 `<body>` 上并订阅偏好增删(如 `data-dsh-claude-chat-follow`)。
329
+ - **读者在场时的让位**:上游包与这套搬来的实现不能同时生效——两套卷帘门都会拦下同一次点击并互相回放,两套自动开合各自点同一个开关,文件变更行两边都往 `tool.call.toolview` 的同一键位按同一优先级注册(同优先级二次注册会抛错)。所以页面装的是上游时,这些功能整体让位(`src/shared/peer-plugin.js` 读宿主启动图里的条目,也读上游浏览器半边留下的样式表节点),设置页把「聊天区动画效果」与「输入框插入符动效」两行的控件禁用并保留读者自己设的值(禁用不等于改写),下面另起一行以强调色写明由谁接管;读者自己存下的偏好不被改写,卸载上游后原样回来。只把标记换成自己的前缀不足以让两套实现共存——那样只保证互不认领状态,行为仍然叠加。让位是**现算**的,不是只在安装时判一次:上游中途装卸(热重载、开关)会立刻让位或恢复,包括座位键位——文件变更行订阅这一处存在性(`subscribePeerPresence`)并随之注册或交还两个键位,其余几项走偏好监听重跑。
330
+ - **动画效果(D26)**:这些动效读 `motionReduced()` 解析出的结果,不问系统媒体查询——「减弱」要能压过系统,「总是」要能在系统开着减弱时照常播放,而这只有解析后的属性做得到;样式表一侧同样以 `<body>` 上的动画属性为门控。
331
+ - **理由**:上游是 TypeScript 与打包产物,与本仓库的拼接构建(D1)不同形,因此按本仓库的结构重写,不引入构建工具;上游的属性名若原样保留,两套实现同时装着时会互相认领对方的标记(例如两套自动开合各自看到一个写着自己名字的开关标记)。
332
+ - **没有搬的一处**:上游会把网页端「工作步骤展示」的缺省档位补成「标准」(桌面端本来就是它),因为它的组体跟随与自动开合都针对封顶的组体。本仓库不代写宿主的 `ui-chat` 设置:皮肤只在自己的偏好条目里记事(D2)。代价是网页端默认档位(「详细」)下组体不封顶,自动开合与组体跟随在那两处不发生;读者把档位选到「标准」或「简洁」后即生效,README 与文案都写明这一点。
333
+ - **代价**:上游的更新要人工对照搬运;行为要自己写 smoke 覆盖(`scripts/smoke/` 的 `chat-follow` 等用例、替身页面的聊天区标记,以及一个上游在场的让位用例)。
334
+ - **重审条件**:上游把这几项拆成可依赖的库并公开稳定接口,或宿主自己把这些交互收成正式能力时,改为依赖而不搬运。
@@ -0,0 +1,21 @@
1
+ /**
2
+ * One path under the harness home, resolved the host's own way.
3
+ *
4
+ * The harness publishes its home directory through the `dshHomePath` service,
5
+ * which joins the segments it is handed; when that service is absent,
6
+ * `$DSH_HOME` stands in, with `~/.dsh` behind it. Every host-half module that
7
+ * touches stored sessions resolves through here, so the delete route, the usage
8
+ * roll-up and the search index all read the same directory.
9
+ *
10
+ * @param ctx - host plugin context.
11
+ * @param segments - path segments under the harness home.
12
+ * @returns the resolved path.
13
+ */
14
+ import { homedir } from 'node:os'
15
+ import { join } from 'node:path'
16
+
17
+ export function harnessPath(ctx, ...segments) {
18
+ const resolvePath = ctx.get('dshHomePath')
19
+ if (typeof resolvePath === 'function') return resolvePath(...segments)
20
+ return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), ...segments)
21
+ }
package/host/hdsl.js CHANGED
@@ -46,20 +46,9 @@ export function createHdslAccount(ctx) {
46
46
  const read = () => {
47
47
  if (reading !== null) return reading
48
48
  reading = Promise.resolve().then(() => {
49
- let env = null
50
- try {
51
- env = ctx.get(DSH_LAUNCH_ENVIRONMENT_KEY)
52
- } catch {
53
- env = null
54
- }
55
- if (env === null || env === undefined || typeof env.getFrom !== 'function') return { contract: false }
56
- const value = (name) => {
57
- try {
58
- return env.getFrom(name, HDSL_LAYERS)?.value
59
- } catch {
60
- return undefined
61
- }
62
- }
49
+ const env = ctx.get(DSH_LAUNCH_ENVIRONMENT_KEY)
50
+ if (typeof env?.getFrom !== 'function') return { contract: false }
51
+ const value = (name) => env.getFrom(name, HDSL_LAYERS)?.value
63
52
  if (value('HDSL_ACCOUNT_CONTRACT') !== HDSL_CONTRACT) return { contract: false }
64
53
  const skinFile = value('HDSL_ACCOUNT_SKIN_FILE')
65
54
  const hasSkinImage = typeof skinFile === 'string' && skinFile !== ''
package/host/index.js CHANGED
@@ -50,18 +50,11 @@ export function apply(ctx) {
50
50
  // Always register through inject, never on the bare ctx. `ctx.get()` reads a
51
51
  // service leniently (no inject declaration needed), but the PROPERTY access
52
52
  // inside registerRoutes (`scope.webServer`) is gated by the fiber's inject
53
- // declaration — so a host half that re-applies while the web server is
54
- // ALREADY running (a generation relink after a client-bundle rebuild) used to
55
- // take the `else registerRoutes(ctx)` branch, every registration threw
56
- // "cannot get property "webServer" without inject", and all three asset
57
- // routes stayed down for that generation. The browser half then fetched the
58
- // copy document in vain — and since loadModelCopy() is one-shot per client
59
- // generation, the picker rendered without the document (English labels, no
60
- // vendor lockups, catalog descriptions) until the next HMR. inject() waits
61
- // for the service and hands registerRoutes a scope that HAS the declaration,
62
- // so both a boot-time apply (web server not up yet) and a hot relink land on
63
- // the working path. A host without a web server simply waits forever here,
64
- // which keeps the defensive contract: the skin still activates.
53
+ // declaration, and a host half re-applies while the web server is already
54
+ // running (a generation relink after a client-bundle rebuild). inject()
55
+ // waits for the service and hands registerRoutes a scope that HAS the
56
+ // declaration, so both a boot-time apply and a hot relink register. A host
57
+ // without a web server simply waits here, and the skin still activates.
65
58
  if (typeof ctx.inject === 'function') ctx.inject(['webServer'], (scope) => { registerRoutes(ctx, scope) })
66
59
  else registerRoutes(ctx, ctx)
67
60