dsh-sidecard-ask 1.8.2 → 1.8.4

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
@@ -1,5 +1,115 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.8.4
4
+
5
+ **主题:选区旁的动作条从「六枚等价胶囊排成一排」改成一块面板加一个主操作——并顺手堵掉右侧被窗口裁掉的那条路。**
6
+
7
+ 起因是用户截图(574×42):「💬 追问选中内容」后面紧接 `解释 / 翻译 / 总结 / 润色 / 纠错` 五枚带边框胶囊,
8
+ 全部挤在一行,右端被窗口边缘切断;用户的意见是「全部排成一排不好看」。
9
+
10
+ - **诊断(不是「一行」的问题,是层级与预算两个问题)**:
11
+ 1. 六枚胶囊同色同粗、各自带边框与阴影,读起来像六个并列功能,没有主操作;
12
+ 2. 定位宽度是**估算**出来的(`estimateLabelWidth(label) + 24` 相加),它只用于夹取 `left`,
13
+ 一旦估小或选区靠右,整条就会顶出右边缘——`max-width:min(620px,92vw)` 也挡不住,
14
+ 因为它是相对视口的,不是相对面板左边剩下的空间。
15
+ - **单一面板**:`.dsa-quickbar` 从「无底色的 flex 行」改成一块控件——`padding:3px`、`border-radius:12px`、
16
+ 中灰描边 `rgba(127,127,127,.5)`、`0 6px 20px rgba(0,0,0,.16)` 投影、`background:bg-overlay`。
17
+ - **一个主操作**:`.dsa-quickbar .dsa-trigger-embedded` 改成品牌实底 + 白字 + 600 字重;
18
+ 深色主题改深墨字 `#0a1428`(本机 uiskin 深色品牌 `#5b9cf5` 上白字实测 **2.79:1**,低于 4.5:1 正文下限;
19
+ 深墨 **6.57:1**),与 1.8.2 的分段芯片同一处理。
20
+ - **一条细分隔线**:`.dsa-quickbar-sep`(1px × 15px,`border-l1`)分开「做那一件事」与「或从现成问题里挑一个」,
21
+ 只在有快捷动作时渲染。
22
+ - **五个动作降级为幽灵胶囊**:`.dsa-quickbar .dsa-chip` 去掉边框/底色/阴影,字色 `label-secondary`,
23
+ hover 才浮出 `bg-layer-2` 底与 `label-primary` 字色。弹层里的 chip 与右键菜单不受影响。
24
+ - **宽度改成算出来的**:`TriggerBar` 读 `window.innerWidth`,给面板加
25
+ `max-width: min(620, 视口宽 − 面板左边 − 8)`,并用 `${Math.min(620, room)}px` 落到内联样式;
26
+ 放不下就换行(`flex-wrap:wrap`),不再被窗口切掉。`window` 不存在时(测试/SSR)取 620。
27
+ - **行为零变化**:键盘漫游(←/→/Home/End)、`Alt+1…5`、右键菜单、点芯片直接发问都照旧;
28
+ 不新增配置项——这是版式修正,不需要开关。
29
+ - 测试:`contract` 新增 **9** 项(渲染侧 3 条:恰好一个 `.dsa-quickbar-sep`、chips 自成一组、
30
+ 主按钮是 `dsa-trigger-embedded` 变体;样式侧 6 条:单一面板、品牌实底主按钮、chips 去边框、
31
+ `.dsa-quickbar-sep` 规则存在、深色墨字覆盖、面板自带 `maxWidth` 上限)。总项数 1350 → **1359**。
32
+ - **如实标注**:交付时给的四张图(`design-preview/10-quickbar-*.png`)是**离屏渲染**——
33
+ 真 CSS 从 `client.js` 抽、主题值取自本机 uiskin 皮肤、无头 Edge 截图,不是运行中页面的截图;
34
+ hover / focus 态与真实选区定位没有截图证明,需要在页面上手测(刷新即生效,`link:` 安装)。
35
+ - 顺手把 README §4.3 补成这一条的外观说明(含深色原因、换行行为与「不写配置项」的结论)。
36
+
37
+ ## 1.8.3
38
+
39
+ **主题:右侧栏打开时,答案卡直接铺满它、读成栏内的一页——而不是 340px 的浮层压在右栏正文上。**
40
+
41
+ 起因是用户截图:右栏开着「团队任务板」,答案卡浮在它上面,正文被挤成一条窄缝,标题与操作行占掉大半。
42
+
43
+ - **`dockRail`(默认 `true`)**:右栏可见且已展开时,把卡片铺到**标签条下方**的内容区——宽度取右栏宽,
44
+ 高度取到帧底;右栏自己的标签条仍然露出可点。这个形态下卡片去掉圆角、描边与阴影,只留一条左边线
45
+ 与右栏内容区分界;标题栏(追问 + 徽标 + ⇥/×)与底部操作行固定上下两端,`AnswerBody` 独立滚动,
46
+ 长答案不再把操作行推出视野。收起为胶囊(⇥)、关闭(×)或关掉本项,立即回到常规浮层。
47
+ - **几何是量出来的,不是猜的**:`dockedBodyRect(node)` 取右栏面板的 `getBoundingClientRect()`,若它在
48
+ `[data-dockkit-pane]` 里,再把顶边抬到所有 `[data-dockkit-tab]` 的底边——否则卡片会盖住右栏自己的标签条。
49
+ `findRail(scope)` 按世代尝试选择器(最具体的在前):
50
+ `[data-dsa-rail-host]`(本插件自己渲染在右栏里的 `CardHost` 节点——它所在的位置就是「一页正文」)→
51
+ `[data-sidebar-right-panel][data-sidebar-right-open] [data-dockkit-pane]` → 面板本身 →
52
+ `[data-sidebar-right-session][data-sidebar-right-open]` → `[data-dockkit-pane]` / `[data-dockkit-column]`
53
+ (只渲染 dockkit 标记的构建)→ `.rightbar` → `[data-slot^="sidebar.right"]`;
54
+ **只接受展开态**(折叠的面板会滑出帧外,它的 rect 不能用;`railOnScreen` 负责拒绝),量不到、
55
+ 宽 < 280px 或高 < 240px 一律回退浮层。
56
+ 这些标记不是文档给的,是从安装包里的 `@deepseek-ai/dsh-client-ui-sidebar-right/lib/client.js` 读出来的。
57
+ - **纯函数 `railDock`**:`enabled` 为假、坐标非有限、宽高低于 280/240 → `null`(= 不 dock);矩形夹进
58
+ `[0,frameWidth] × [0,frameHeight]`,夹完仍不足最小尺寸也返回 `null`;`clipped` 表示「画出来的不是量到的」——
59
+ 尺寸被夹小,或原点被拉回帧内(右栏入场动画期间可能量到负坐标)。已导出到 `api.pure`,测试与运行时共用一份实现。
60
+ - **渲染**:`docked = mode === 'full' && dock !== null`;容器改 `data-dsa-float-mode="dock"` 并写 inline
61
+ `left/top/width/height`(同时释放基类的 `right/bottom`,否则会过约束),`.dsa-stack` 带
62
+ `data-dsa-dock-body="<px>"` 供对照;三种形态复用同一份 `cardNodes`,没有分叉出第二套卡片。
63
+ - **设置页**新增「右侧栏打开时占满它」复选框(只写草稿,点保存才落盘),并给出「收起为胶囊或关闭即恢复原状;
64
+ 右侧栏不在时不生效」的说明;三个 i18n 键(zh/en 各三条)。
65
+ - **为什么不做成"真的注册右栏的一页"(如实)**:`sidebarRightTabs` / `sidebarRight` 两个服务在当前构建里
66
+ **确实存在**(从 `app.asar` 里读到 `ctx.reflect.provide("sidebarRightTabs", tabs)`),注册形状
67
+ (`register({id, kind, guide, …})`)与槽派发键(`entryKey = definition?.id ?? tab.kind`,本插件两处都是
68
+ `sidecard-ask:card`)也逐一核对过;但实测 `controller.openTab` 路径仍回落到浮层,且没定位到抛错点。
69
+ 在它修好之前,`dockRail` 是**视觉等价、失败即回退**的兜底层——README §6.1 仍把它标为占位接口,没有说成官方能力。
70
+ - **已知代价(如实)**:dock 期间会盖住右栏内容区里的其他面板(那些面板仍在下面),也会暂时压住右栏的
71
+ 拖拽改宽手柄;这两点都写进了 README §4.2 与代码注释。
72
+ - 测试:`float-placement` 新增 **40** 项(`railDock` 的夹取、最小尺寸、`clipped` 语义、不修改入参、非有限输入、
73
+ `dockRail` 默认值、源码钩子、上面三个缺陷的回归断言,以及承载面自动链的顺序断言);`contract` 新增 **8** 项
74
+ (设置页复选框:唯一一行、单个 checkbox、默认勾选、带写草稿的 onChange、说明文案;承载面 `auto` 的
75
+ 新偏好);3 个新 i18n 键由既有词典用例自动覆盖。
76
+ 总项数 1293 → **1350**。
77
+ - **发布前的一次真实回合(用户截图 + 一个「?」)修掉的三个缺陷**——首次交付的 dock 在真机上**没有生效**:
78
+ 卡片仍以约 505px 浮在右栏上,而右栏里留着一个正文写着「该卡片已关闭」的空 tab。
79
+ 1. `apply()` 里残留 `rail` 变量:改用 `railBox` 后旧守卫没删干净,一旦 `ResizeObserver` 回调进来就抛
80
+ `ReferenceError: rail is not defined`,并且**右栏节点永远不会被 observe**。已删,并加回归断言
81
+ (源码里不得再出现 `observed.has(rail)`)。
82
+ 2. **右栏开合不触发重算**:effect 依赖只有 `[maxWidth, total, mode, dockWanted]`,而被观察的是挂载时
83
+ 就已存在的节点——右栏是后开的,量不到就永远不 dock。现在的触发源有四个:`window resize`、
84
+ `ResizeObserver`(对话区 + 当前被接受的那个右栏节点)、**只盯属性的 `MutationObserver`**
85
+ (`data-sidebar-right-open` / `data-dockkit-pane-active` / `style` / `class`;**不盯 childList**,
86
+ 否则流式 token 每帧都会唤醒几何计算)、以及挂载后 `[80,200,420,800]ms` 四次重测(罩住右栏滑入动画)。
87
+ 3. **不再靠猜右栏标记**:`CardHost` 自己渲染在右栏里的节点就是「一页正文」,它的矩形就是答案。三个
88
+ `CardHost` 包装节点加 `data-dsa-rail-host`,并作为 `RAIL_SELECTORS` 的**第一优先**锚点(设置页根节点
89
+ 故意不加,否则浮层会停靠到设置面板上);其后才是 `data-sidebar-right-*`、dockkit 世代、`.rightbar`
90
+ 与 `sidebar.right` 槽。折叠态另由 `railOnScreen()` 拒绝(`left >= frameWidth - 1` 或
91
+ `top >= frameHeight - 1` 视为不在屏上),不会被 `railDock` 夹进视野。
92
+ - **诊断属性**:浮层根节点新增 `data-dsa-dock`,取值 `dock` / `no-rail` / `rail-too-small` / `off` / `mode`,
93
+ 一眼区分「没找到右栏」「找到了但太窄」「开关关了」「当前不是完整卡片形态」。
94
+ - **承载面 `auto` 的顺序改了:dsh-better-sidebar → 内置浮层卡片;原生右侧栏退出自动链。**
95
+ 原因是那个空 tab:卡片被塞进右栏只是一个 tab,tab 的生命周期不归插件管——卡片关掉后右栏仍留着一条
96
+ 读作「该卡片已关闭」的标签,还和用户自己的右栏标签抢位置,落点又取决于我们不控制的宿主细节。
97
+ 原生右侧栏保留为**强制选项**(下拉里标注「强制,不参与自动」,新增 `surfaceNativeForce` 文案),
98
+ 显式 `sideSurface: 'native-rightbar'` 仍然走它。下拉顺序同步为 auto / 插件 / 内置卡片 / 强制右侧栏。
99
+ 连带把三条依赖「auto 会挑原生」的既有用例改成显式指定 `native-rightbar`(它们测的是原生适配器本身,
100
+ 不该再由 auto 顶上来)。
101
+ - 版本号三处同步到 1.8.3。
102
+
103
+ 本机实测(四项均 exit 0):verify **57** / contract **989** / float-placement **78** / smoke **226**
104
+ = **1350 项全部通过**(1.8.2 为 1293;float-placement 由 38 → 78,含 `railDock` 用例、上面三个缺陷的回归钩子,
105
+ 以及「自动链里不出现原生右侧栏」的顺序断言;contract 972 → 989)。
106
+
107
+ > 未验证(如实记录):`design-preview/09-dock-{light,dark,narrow-light}.png` 三张图是**离屏渲染**——
108
+ > 无头 Edge 加载"从 `client.js` 切出的真实样式表 + 本机 uiskin 皮肤令牌真值 + 按 `app.asar` 里读到的
109
+ > 右栏标记搭的静态结构"的页面,**不是**运行中 DSH 窗口的截图。因此它证明的是"这套 CSS 在给定矩形里
110
+ > 摆成什么样",不证明真实页面里右栏的实测矩形、滚动性能,也不证明 dock 与右栏拖宽手柄的冲突手感。
111
+ > 这些仍需刷新页面人工确认(README §9 A 路径第 9 条给了可执行的判据)。
112
+
3
113
  ## 1.8.2
4
114
 
5
115
  **主题:提问框底部的「作答位置」从"两枚看不出选中哪一侧的标签"改成一眼可辨的分段芯片;并把 1.8.1 之后写在本地、尚未提交的弹层对比度修补一并发布。**
@@ -17,10 +127,16 @@
17
127
  `--dsw-alias-brand-primary` 提亮到 `#5b9cf5`,白色 600 字在其上低于 4.5:1 正文下限、也低于 3:1 大字下限。
18
128
  改法:`[data-ds-dark-theme]` 下选中侧改用深墨 `#0a1428`(实测 6.57:1);浅色保持白字
19
129
  (`#2563c0` 上 5.79:1)。令牌值取自本机 `uiskin-theme`,对比度按 WCAG 相对亮度公式实算,不是目测。
20
- - **弹层边框/阴影对比度修补(1.8.1 之后写在本地、未提交,本次一并发布)**:`.dsa-pop` 的 1px 边
21
- 改显式中灰 `rgba(127,127,127,.55)`(`border-l2` 落在弹层表面只量到 ≈1.3:1),阴影改两层衰减
130
+ - **弹层边框/阴影修补(1.8.1 之后写在本地、未提交,本次一并发布;含一处数字更正)**:`.dsa-pop` 的 1px 边
131
+ 改显式中灰 `rgba(127,127,127,.55)`,阴影改两层衰减
22
132
  `0 14px 36px rgba(0,0,0,.30), 0 3px 10px rgba(0,0,0,.14)`;`.dsa-pop` 内的次级文案改用
23
133
  `label-primary` 并保留 `.78` 不透明度。
134
+ **更正**:随该补丁一起写下的代码注释声称"外框用 `border-l2` 只量到 ≈1.3–1.7:1",实测不成立——
135
+ 按本机皮肤令牌复合(bg-base .55 over white → bg-overlay .98)后,`border-l2` 是 **1.95:1(浅)/ 2.88:1(深)**;
136
+ 1.40:1 那个数字属于内层 `border-l1` 的 1px 边。换成中灰 .55 后测得 **1.96:1(浅)/ 2.12:1(深)**——
137
+ 所以这一步是**色度修正(蓝对蓝 → 中性灰),不是亮度对比度提升**,深色下还略弱。
138
+ 真正抬高可读性的是两层阴影与次级文案换成 `label-primary`(6.11:1)。
139
+ 若需要更强的实体边框,中灰 .85 测得 ≈3.03:1,但视觉更硬,未擅自采用。
24
140
  - 测试:contract 新增 **16** 项钉住这个控件(唯一 `.dsa-seg`、恰好两个按钮、恰好一个
25
141
  `aria-pressed="true"`、每个选项带一枚内联 `svg`、`data-on` 只有一个真值、两个去向的标签不互换、
26
142
  品牌实底规则与深色覆盖规则必须出现在样式表里)。**点击切换本身仍然不可测**:harness 的
package/README.md CHANGED
@@ -8,6 +8,11 @@
8
8
 
9
9
  聊天记录、任务详情、右侧栏内容……只要是能选中的文本都能用;**没装侧边卡片插件也能跑**(自动回退到内置浮层卡片)。
10
10
 
11
+ > **1.8.4 增强一览**(详见 CHANGELOG):选区旁的动作条不再「六枚等价胶囊排成一排」——改成**一块面板 + 一个主操作**:
12
+ > 品牌实底按钮「追问选中内容」、一条细分隔线、五个无边框文字胶囊(hover 才浮出底色),深色主题把主按钮
13
+ > 的字改成深墨色(白字在深色品牌上只有 2.79:1);宽度也改成按「视口宽 − 面板左边 − 8」算,
14
+ > 放不下就换行,不再被窗口右边缘切掉。见 §4.3。
15
+
11
16
  > **1.8.0 增强一览**(P2,详见 CHANGELOG):**回答风格档位**(`answerStyle` 精简/标准/详细)
12
17
  > 与 ≤500 字的自定义指令;改写类答案可**写回主对话输入框**(先做了真实页面探针,走官方
13
18
  > `inputActions.insertText`,DOM 路径退化为 `beforeinput` 追加);卡片与历史条目可**查看原文**
@@ -156,10 +161,11 @@ plugin_manager(action="install_bundle", target="D:\\Users\\34332\\AI\\dsh-sideca
156
161
  |---|---|---|---|---|
157
162
  | `trigger` | `selection` \| `shortcut` \| `both` | `selection` | **触发方式**:选中即浮出按钮 / 只用快捷键 / 两者都要 | 改后立即(客户端读 /state) |
158
163
  | `defaultCarrier` | `main` \| `side` | `side` | **默认作答位置**:提问框里仍可临时切换 | 立即 |
159
- | `sideSurface` | `auto` \| `native-rightbar` \| `better-sidebar` \| `flow` | `auto` | **窗口模式(侧边承载面)**:自动挑选 / 强制 DSH 原生右侧栏 / 强制 dsh-better-sidebar / 强制内置浮层卡片 | 立即 |
164
+ | `sideSurface` | `auto` \| `native-rightbar` \| `better-sidebar` \| `flow` | `auto` | **窗口模式(侧边承载面)**:自动挑选(先 dsh-better-sidebar,再内置浮层卡片)/ 强制 DSH 原生右侧栏(不参与自动)/ 强制 dsh-better-sidebar / 强制内置浮层卡片 | 立即 |
160
165
  | `maxChars` | 200–60000 | `4000` | **最大字符数**:超出部分头尾保留、中间截断并标注 | 立即 |
161
166
  | `floatMode` | `capsule` \| `full` \| `off` | `capsule` | **内置浮层的默认形态**:胶囊(一行,最小遮挡)/ 完整卡片栈 / 只留一个计数圆点。运行时可即时收起、隐藏,但不写回本项——刷新后仍回到这里设的默认 | 立即 |
162
167
  | `floatMaxWidth` | 240–560 | `340` | **内置浮层宽度上限**(px):实际宽度 = `min(本项, 对话区右侧可用带宽)`,可用带宽不足 240px 时才会压入对话区 | 立即 |
168
+ | `dockRail` | 布尔 | `true` | **右侧栏打开时占满它**:答案卡铺满右栏标签条下方的内容区,读起来像栏内的一页,而不是 340px 的小浮层。收起为胶囊、关闭浮层、右栏宽度 < 280px 或高度 < 240px 时自动回到常规浮层;实现靠测量右栏 DOM 矩形,**没有**走 DSH 的右栏 API(见 §4.2 与 §6.1) | 立即 |
163
169
  | `shortcut` | 形如 `Alt+Q`、`Ctrl+Shift+K` | `Alt+Q` | **快捷键**:唤起提问框(`trigger` 允许时) | 立即 |
164
170
  | `captureZones` | `auto` \| `chat` \| `task` \| `chat+task` | `auto` | 只在哪些区域触发 | 立即 |
165
171
  | `showInUnclassified` | 布尔 | `true` | 无法归类的区域是否也触发 | 立即 |
@@ -202,6 +208,53 @@ bleeds = 对话区右边界 - left > 1 // 只有带宽 < 240px 时才
202
208
  - 挂载时、窗口 resize 时、右栏开合时(`ResizeObserver`)都会重算;流式刷新只重渲染,不动几何。
203
209
  - 浮层根节点带 `data-layer-width`(实际宽度)与 `data-dsa-float-mode`:报告问题时可直接读这两个值。
204
210
 
211
+ ### 4.2 右侧栏打开时「占满它」(`dockRail`,1.8.3)
212
+
213
+ 340px 的浮层在右栏打开时会同时压住两块内容:主对话的最后几行、以及右栏自己的正文。1.8.3 起默认
214
+ (`dockRail: true`)把整张卡铺到右栏里,读起来像栏内的一页:
215
+
216
+ - 位置 = 右栏**标签条之下**的内容区矩形,宽度取右栏宽度,高度取到底部;右栏自己的标签条仍然露出、可点。
217
+ - 卡片在这个形态下没有圆角/描边/阴影(`.dsa-card` 的装饰被 `[data-dsa-float-mode="dock"]` 覆盖),只有一条左边线与右栏内容区分界。
218
+ - 标题栏(追问 + 徽标 + ⇥ / ×)与底部操作行固定上下两端,中间 `AnswerBody` 独立滚动——长答案不再把操作行推出视野。
219
+ - 收起为胶囊(⇥)、关闭(×)、或把 `dockRail` 关掉,都立即回到常规浮层;右栏宽度 < 280px 或高度 < 240px 时也回退(窄栏里铺满会把正文挤成每行 8–9 字)。
220
+ - 折叠的右栏不会被误用:`railOnScreen()` 会拒绝停在帧外(`left >= frameWidth - 1`)或尺寸不足的面板,量不到就回退,绝不把卡片画到帧外。
221
+ - 矩形从哪来:**优先量本插件自己渲染在右栏里的 `CardHost` 节点**(`[data-dsa-rail-host]`)——它所在的位置按定义就是「一页正文」,不依赖任何未被文档承诺的标记;找不到时再按 `data-sidebar-right-*`、dockkit 世代、`.rightbar`、`sidebar.right` 槽逐代回退。
222
+ - 什么时候重算:窗口改宽、右栏开合(只盯属性的 `MutationObserver`)、右栏自身改宽(`ResizeObserver`),以及挂载后的四次延时重测(罩住右栏的滑入动画)。流式刷新不会触发几何重算。
223
+
224
+ 为什么是「测量」而不是「右栏内的一页」:DSH 的右栏服务确实存在(`sidebarRightTabs` / `sidebarRight`,
225
+ 见 §6.1),本插件也有完整注册代码,但在当前构建里 `openTab` 路径实测仍回落到浮层,且没定位到抛错点。
226
+ 在它修好之前,`dockRail` 是视觉上等价、失败即回退的兜底层——它盖住右栏内容区的其他面板(那些面板仍在
227
+ 下面,取消勾选或收起胶囊即恢复),也会暂时压住右栏的拖拽改宽手柄。
228
+
229
+ 诊断:`data-dsa-float-mode="dock"` 表示处于该形态;`.dsa-stack` 上的 `data-dsa-dock-body="<px>"` 是量到的
230
+ 宽度(与卡片宽度一致)。浮层根节点还有 `data-dsa-dock`,它的取值直接说明**为什么**没有铺满:
231
+
232
+ | 值 | 含义 |
233
+ |---|---|
234
+ | `dock` | 正在铺满 |
235
+ | `no-rail` | 没找到已展开的右栏(右栏关着,或这个构建的标记与预期不同) |
236
+ | `rail-too-small` | 找到了,但宽 < 280px / 高 < 240px,或它停在帧外(折叠态被拒绝) |
237
+ | `off` | 设置里的 `dockRail` 被关掉了 |
238
+ | `mode` | 当前不是完整卡片形态(胶囊 / 隐藏),本来就不该铺满 |
239
+
240
+ ### 4.3 选区旁的动作条(外观)
241
+
242
+ 选中文本后浮出的那条东西以前是**六枚等价的胶囊排成一排**(`追问选中内容` 与 `解释/翻译/总结/润色/纠错`),
243
+ 彼此同色同粗、各带边框和阴影:既没有主次,也没有分组,窗口窄一点或选区靠右时整条还会顶出右边缘被裁掉。
244
+ 现在它是**一块面板 + 一个主操作**:
245
+
246
+ - 面板 `.dsa-quickbar`:`padding:3px`、`border-radius:12px`、中灰描边、`0 6px 20px` 投影,整条读作一个控件;
247
+ - 主按钮(`.dsa-trigger-embedded`):品牌实底 + 白字 + 600 字重(深色主题改为深墨字,原因同 §九 A 的分段芯片:
248
+ uiskin 深色品牌 `#5b9cf5` 上白字只有 2.79:1);
249
+ - 细分隔线 `.dsa-quickbar-sep`(1px × 15px,`border-l1`)把「做那一件事」和「或从这些现成问题里挑一个」分开;
250
+ - 五个快捷动作降级为**无边框文字胶囊**(`.dsa-quickbar .dsa-chip`):透明底 + `label-secondary` 字色,
251
+ hover 才浮出 `bg-layer-2` 底与 `label-primary` 字色——层级低于主按钮,但仍然一眼可点;
252
+ - **宽度不再靠估算**:组件给面板加 `max-width: min(620, 视口宽 - 面板左边 - 8)`,放不下就换行,
253
+ 不会被窗口边缘切掉;分隔线随主按钮留在第一行,换行的胶囊继续从左边排。
254
+
255
+ 外观之外没有行为变化:键盘漫游、`Alt+1…5`、右键菜单、点击后直接发问都照旧。这条也不写配置项——
256
+ 它是版式修正,不需要开关。
257
+
205
258
  ---
206
259
 
207
260
  ## 五、侧边承载面与"侧边卡片插件"适配
@@ -210,15 +263,19 @@ bleeds = 对话区右边界 - left > 1 // 只有带宽 < 240px 时才
210
263
 
211
264
  | 顺序 | 承载面 | 依赖 | 失败时 |
212
265
  |---|---|---|---|
213
- | 1 | DSH 原生右侧栏 | `ctx.sidebarRightTabs`(注册 tab 类型)+ `ctx.sidebarRight`(`openTab`),并占用槽位 `sidebar.right.pane.tab` / `…tab.title` | 记 `surfaces.native.error`,落到下一层;**类型注册会回滚**(见下) |
214
- | 2 | **dsh-better-sidebar**(侧边卡片插件) | 客户端服务 `ctx.betterSidebar`:`registerTab` + `openTab(seed, scope)`;卡片 id 走 `tab.meta.cardId`,因此**要求其 `features` 含 `tabMeta`** | 同上(缺 `tabMeta` 时记 `no-tab-meta` 并跳过,而不是开一个空 tab) |
215
- | 3 | **内置浮层卡片**(默认兜底) | 只需要 `shell.overlay` 槽位 | ——(这是保底面,永远可用) |
266
+ | 1 | **dsh-better-sidebar**(侧边卡片插件) | 客户端服务 `ctx.betterSidebar`:`registerTab` + `openTab(seed, scope)`;卡片 id 走 `tab.meta.cardId`,因此**要求其 `features` 含 `tabMeta`** | 记 `surfaces.better.error`(缺 `tabMeta` 时记 `no-tab-meta` 并跳过,而不是开一个空 tab),落到下一层 |
267
+ | 2 | **内置浮层卡片**(兜底) | 只需要 `shell.overlay` 槽位 | ——(这是保底面,永远可用) |
268
+ | — | DSH 原生右侧栏 | `ctx.sidebarRightTabs`(注册 tab 类型)+ `ctx.sidebarRight`(`openTab`),并占用槽位 `sidebar.right.pane.tab` / `…tab.title` | **不在 `auto` 链里**:只有显式 `sideSurface: 'native-rightbar'` 才用它;注册失败会回滚(见下) |
216
269
 
217
270
  - 装了 `dsh-better-sidebar`:卡片以它的 tab 形式出现在它的面板里,关闭卡片会同时 `closeTab`,不残留空 tab。
218
- - 没装:自动使用内置浮层卡片,功能完全一致——**这条路径是本插件的默认与保底路径**。
271
+ 这是 `auto` 的**第一选择**。
272
+ - 没装:自动使用内置浮层卡片,功能完全一致——**这条路径是第二选择,也是保底路径**。
219
273
  浮层默认是**贴帧右边缘的胶囊**(一行高,只有状态与问题首句),点击才展开成完整卡片,
220
274
  标题栏右侧的「收起 / 隐藏」可随时收回;它的宽度由 `floatPlacement` 计算,
221
275
  只会占用**对话区右侧那条预留带**,不会压住正文(细节见 §4.1)。
276
+ - **原生右侧栏为什么退出 `auto`**:卡片被塞进右栏只是一个 tab,而这个 tab 的生命周期不归插件管——
277
+ 卡片关闭后右栏仍留着一条读作「该卡片已关闭」的空标签,和你自己的右栏标签抢位置,
278
+ 落点还取决于我们并不控制的宿主实现细节。它会保留成一个**强制选项**(下拉里标注「强制,不参与自动」)。
222
279
  - 强制指定了一个不可用的承载面:回退到内置浮层,并在卡片上标注「已回退」。
223
280
 
224
281
  ### 5.1 与 dsh-better-sidebar 0.22.1 的适配核对(2026-09-28)
@@ -280,7 +337,7 @@ better-sidebar 会把收到的每个 tab 描述符**镜像注册进同一个原
280
337
  | `sidecard-ask:card` | 本插件的**原生右侧栏**承载面(1.2.1 之前完全缺失) |
281
338
  | `dsh-better-sidebar:sidecard-ask:card:workbench` | 经 dsh-better-sidebar 镜像注册的承载面(独立 kind) |
282
339
 
283
- 两者共存、均 `active: true` —— 即"同 kind 冲突"已被消除,`auto` 默认走原生右侧栏,强制 `sideSurface: better-sidebar` 仍可用。
340
+ 两者共存、均 `active: true` —— 即"同 kind 冲突"已被消除。`auto` 走的是 **dsh-better-sidebar → 内置浮层卡片**这条链(1.8.3 起原生右侧栏只在显式 `sideSurface: native-rightbar` 时使用),两个强制选项都仍可用。
284
341
 
285
342
  ### 5.3 与 DSH 桌面版(Electron)的适配核对(2026-09-30)
286
343
 
@@ -426,7 +483,7 @@ node tools/compat-probe.mjs --json tools/.cache/matrix.json
426
483
 
427
484
  | 缺口 | 适配实现 | 在真机/测试里的表现 |
428
485
  |---|---|---|
429
- | 0.1.2 / 0.1.3 没有原生右侧栏 | 承载面探测链:原生右侧栏 → `dsh-better-sidebar` → **内置浮层卡片**;不可用的一律不计入 `pickSurface` | `contract-test` 断言 `pickSurface('auto')` 在无插件组合下落到 `flow`;`auto` 在可用时优先原生 |
486
+ | 0.1.2 / 0.1.3 没有原生右侧栏 | 承载面探测链:原生右侧栏 → `dsh-better-sidebar` → **内置浮层卡片**;不可用的一律不计入 `pickSurface` | `contract-test` 断言 `pickSurface('auto')` 在无插件组合下落到 `flow`、两端可用时选 `better-sidebar`;`float-placement` 钉住自动链里不出现原生右侧栏 |
430
487
  | ≤0.1.6-alpha.1 没有 `using`/`retain` | 主对话提交**四级阶梯**(调用时逐级探测,不查版本表):`sessions.using` → `sessions.retain`+`release` → 槽位标准 prop `inputActions`(`setDraft` + `submit`)→ 仅写入输入框并明确提示"请按 Enter" | `contract-test` 逐级断言 `via`:`sessions.using` / `sessions.retain`(并断言 `release()` 被调用)/ `inputActions.submit` / `composer.draft` / 全无时抛 `no-main-carrier` |
431
488
  | ≤0.1.6-alpha.2 没有归档门 | 不再因"父会话已归档"直接拒绝:仍优先挑未归档代理,但只有归档候选时**照常尝试**,并在 `start` 事件里带 `parentArchived:true`;真被门拒绝时把空 turn 的 `refusal` 翻译成 `blocked-step` 并说明原因 | `smoke-test`:全归档组合仍能拿到答案;`refusal` 用例断言 `code=blocked-step` 且文案提到归档 |
432
489
  | 0.1.2-rc.1 没有进程内帧 | 增加**第二条流式源**:订阅 `session/event` 的持久化 `assistant/chunk`(`{turn,step,chunk}`,形状取自该版本自己的 `chunk-rows.js`),与帧源**互斥**(先说话的那个生效,绝不重复计一次文本) | `smoke-test`:`frameMode:'chunks'` 用例断言 delta 拼出完整答案、`streaming:true`、`streamSource:'chunks'`;同时断言两源并存时文本不重复 |
@@ -564,6 +621,14 @@ node tools/compat-probe.mjs --json tools/.cache/matrix.json
564
621
  **关键判据:胶囊/卡片的左边缘不得越过对话区(`[data-slot^="conversation"]`)的右边界**。
565
622
  把窗口从 1920px 拖到 900px、再开关一次右侧栏,浮层应随之变窄而不是往左压住正文;
566
623
  只有窗口窄到可用带宽不足 240px 时才允许压入(此时是设计内的降级,不是 bug)。
624
+ 9. **占满右侧栏(1.8.3,`dockRail`)**:先把右侧栏打开(任意面板,例如「团队任务板」),再追问一次 →
625
+ 答案卡应铺满**标签条下方**的整块内容区:右边缘贴帧边、下边缘贴窗口底,右栏的标签条仍然露出可点。
626
+ 点卡片标题栏的 ⇥(收起为胶囊)或 × → 卡片立即退出占满、回到常规浮层,右栏内容区恢复。
627
+ 自检:F12 读浮层根节点,`data-dsa-float-mode` 应为 `dock`,`data-dsa-dock` 也应为 `dock`;
628
+ `.dsa-stack` 上的 `data-dsa-dock-body` 应等于卡片宽度(px)。**没铺满就看 `data-dsa-dock`**:
629
+ `no-rail` = 没找到右栏,`rail-too-small` = 找到了但太窄或在帧外,`off` = 开关关了,`mode` = 不是完整形态。
630
+ 把右栏拖到 260px 宽 → 应自动回退成常规浮层(低于 280px 不铺满);再拖回 400px → 应重新铺满。
631
+ 设置页取消「右侧栏打开时占满它」并保存 → 同一操作不再铺满。
567
632
 
568
633
  **失败信号**:卡片停在「作答中…」不动 = SSE 帧没到达(看 console 是否有 `/sidecard-ask/api/ask` 报错);`done.streaming=false` = 该版本没有流式帧(属预期降级,卡片会写明)。
569
634
 
@@ -604,10 +669,16 @@ node test/smoke-test.mjs # 端到端:流式/截断/取消/持久化/失
604
669
  ```
605
670
 
606
671
  四个脚本都以 `process.exitCode` 反映结果,失败会列出具体条目;测试会把 `DSH_HOME` 指向临时目录,不会污染真实配置。
607
- 当前规模(1.8.2 本机实测,四项均 exit 0):verify **57** 项 + contract **972** 项 + float-placement **38** 项 +
608
- smoke **226** 项 = **1293 项全部通过**(1.8.1 为 1277 项,其中 contract 由 956 → 972:新增 16 项钉住提问框
609
- 「作答位置」分段芯片——唯一 `.dsa-seg`、两个按钮、恰好一个 `aria-pressed`、各带一枚内联图标、
610
- 品牌实底规则与深色覆盖规则;点击切换本身不可测,原因见 1.8.2 的 CHANGELOG)。
672
+ 当前规模(本机实测,四项均 exit 0):verify **57** 项 + contract **998** 项 + float-placement **78** 项 +
673
+ smoke **226** 项 = **1359 项全部通过**(较 1.8.3 的 1350 多 9 项:选区动作条改成单一面板后新增的
674
+ 3 条渲染断言——恰好一个 `.dsa-quickbar-sep`、chips 自成一组、主按钮是 embedded 变体——与 6 条样式断言:
675
+ 单一面板、品牌实底主按钮、chips 去边框、分隔线规则、深色墨字覆盖、以及面板自带 `maxWidth` 上限。
676
+ 1.8.3 为 1350 项:contract 972 → 989,新增 8 项钉住设置页的「占满
677
+ 右侧栏」复选框、3 个新 i18n 键,以及承载面 `auto` 的新顺序;float-placement 38 → 78,新增 40 项覆盖 `railDock` 的夹取/最小尺寸/
678
+ `clipped` 语义、真机上 dock 没生效那次发现的三个缺陷(残留的 `rail` 变量、右栏开合不触发重算、
679
+ 右栏锚点必须优先用插件自己的 `[data-dsa-rail-host]` 节点),以及「自动链里不出现原生右侧栏」这条顺序断言)。更早的 1.8.1 为 1277 项,其中 contract
680
+ 由 956 → 972 是提问框「作答位置」分段芯片的 16 项——唯一 `.dsa-seg`、两个按钮、恰好一个
681
+ `aria-pressed`、各带一枚内联图标、品牌实底规则与深色覆盖规则;点击切换本身不可测,原因见 1.8.2 的 CHANGELOG。
611
682
 
612
683
  **真机探针(不属于四套自测)**:`.compat-report/scripts/composer-probe.mjs` 需要
613
684
  `playwright-core` 与本机 Edge,对**真实 DSH 页面**回答 composer DOM / 哪条写入路径真能改变
package/client.js CHANGED
@@ -47,7 +47,7 @@ window.__ModuleLoader__.load({
47
47
  * the Host and the module's `api.version` both read it, so a report can
48
48
  * never claim a generation the browser is not actually running.
49
49
  */
50
- const CLIENT_VERSION = '1.8.2'
50
+ const CLIENT_VERSION = '1.8.4'
51
51
 
52
52
  /**
53
53
  * Tab kind served by the DSH native right rail (also its implementation id).
@@ -98,6 +98,10 @@ window.__ModuleLoader__.load({
98
98
  sideProvider: 'auto',
99
99
  floatMode: 'capsule',
100
100
  floatMaxWidth: 340,
101
+ // Docked presentation (1.8.3): when the right rail is on screen, the
102
+ // answer paints inside the rail's rectangle instead of floating as a
103
+ // 340px card over it. See §1b `railDock`.
104
+ dockRail: true,
101
105
  // Mirrors the Host's DEFAULT_CONFIG.quickActions (contract-asserted):
102
106
  // `builtin:*` entries are resolved through QUICK_ACTION_BUILTINS, while
103
107
  // custom entries (no builtin prefix) carry their own label/question.
@@ -250,6 +254,163 @@ window.__ModuleLoader__.load({
250
254
  return { width, avail, capped: width < cap, bleeds: lane - left > 1 }
251
255
  }
252
256
 
257
+ const DOCK_MIN_WIDTH = 280
258
+ const DOCK_MIN_HEIGHT = 240
259
+
260
+ /**
261
+ * How the Client half recognizes the right rail in the live DOM, most
262
+ * specific generation first.
263
+ *
264
+ * The rail has no published DOM contract, so the markers were read out of
265
+ * the installed build (`@deepseek-ai/dsh-client-ui-sidebar-right`): its panel
266
+ * is a div carrying `data-sidebar-right-panel="push|fullscreen"` and, while
267
+ * expanded, `data-sidebar-right-open`; each session surface also carries
268
+ * `data-sidebar-right-session`. Two older spellings are kept behind those so
269
+ * a downgraded shell does not silently lose the dock: `.rightbar` (pre-rail
270
+ * builds) and the `sidebar.right` slot subtree. The OPEN marker matters: the
271
+ * panel slides off the frame while collapsed, so its rect is off-screen and
272
+ * must not be docked into.
273
+ */
274
+ const RAIL_SELECTORS = [
275
+ // The plugin's own nodes are the most reliable anchor there is: a tab body
276
+ // rendered by `CardHost` IS a page body, so wherever it sits is exactly
277
+ // where a page goes. It also works on builds whose rail markers we have
278
+ // never seen — including the leftover "card closed" tab, which still
279
+ // occupies a page body.
280
+ '[data-dsa-rail-host]',
281
+ '[data-sidebar-right-panel][data-sidebar-right-open] [data-dockkit-pane]',
282
+ '[data-sidebar-right-panel][data-sidebar-right-open]',
283
+ '[data-sidebar-right-session][data-sidebar-right-open]',
284
+ // Dockkit-only builds: the pane is the page body, and a build that renders
285
+ // no `data-sidebar-right-*` marker at all would otherwise never dock. These
286
+ // are last because they say "a pane exists", not "a pane is open" — the
287
+ // on-screen check in `findRail` is what keeps a collapsed (off-frame) rail
288
+ // from being docked into.
289
+ '[data-dockkit-pane]',
290
+ '[data-dockkit-column]',
291
+ '.rightbar',
292
+ '[data-slot^="sidebar.right"]',
293
+ ]
294
+
295
+ /**
296
+ * The box a tab's BODY occupies inside one rail node.
297
+ *
298
+ * The pane's tab strip sits at the top of the same box, so a raw pane rect
299
+ * would make the card cover the rail's own tabs. The strip's bottom edge is
300
+ * measured instead (every tab carries `data-dockkit-tab`), which keeps the
301
+ * rail navigable and places the card exactly where a page's content goes.
302
+ * Without any tab to measure, the node's own top edge is the answer.
303
+ */
304
+ function dockedBodyRect(node) {
305
+ const rect = node.getBoundingClientRect()
306
+ let top = rect.top
307
+ const pane = typeof node.closest === 'function' ? node.closest('[data-dockkit-pane]') : null
308
+ const tabs = pane !== null && typeof pane.querySelectorAll === 'function'
309
+ ? pane.querySelectorAll('[data-dockkit-tab]')
310
+ : null
311
+ if (tabs !== null) {
312
+ for (const tab of tabs) {
313
+ if (typeof tab.getBoundingClientRect !== 'function') continue
314
+ const tabRect = tab.getBoundingClientRect()
315
+ if (Number.isFinite(tabRect.bottom) && tabRect.bottom > top) top = tabRect.bottom
316
+ }
317
+ }
318
+ return { left: rect.left, top, width: rect.width, height: rect.bottom - top }
319
+ }
320
+
321
+ /**
322
+ * Whether a measured rail box is actually on screen.
323
+ *
324
+ * The collapsed rail is animated off the frame (its rect sits past the right
325
+ * edge, or is zero-sized), so "an element matched" is not the same as "the
326
+ * user can see it". A box that starts outside the frame or is thinner than
327
+ * the minimum is rejected here rather than clamped into view by `railDock`.
328
+ */
329
+ function railOnScreen(box) {
330
+ if (box === null || box === undefined) return false
331
+ const frameW = Number.isFinite(document.documentElement?.clientWidth)
332
+ ? document.documentElement.clientWidth
333
+ : (typeof window.innerWidth === 'number' ? window.innerWidth : 0)
334
+ const frameH = Number.isFinite(document.documentElement?.clientHeight)
335
+ ? document.documentElement.clientHeight
336
+ : (typeof window.innerHeight === 'number' ? window.innerHeight : 0)
337
+ if (!Number.isFinite(box.left) || !Number.isFinite(box.top)) return false
338
+ if (box.width < DOCK_MIN_WIDTH || box.height < DOCK_MIN_HEIGHT) return false
339
+ // `left >= frameW` is the collapsed-rail case (parked past the edge); a
340
+ // negative left with a visible body is a rail mid-animation and is fine.
341
+ if (box.left >= frameW - 1) return false
342
+ if (box.top >= frameH - 1) return false
343
+ return true
344
+ }
345
+
346
+ /**
347
+ * The on-screen rail's dockable box, plus the node it was measured from.
348
+ *
349
+ * Every candidate is measured before it is accepted: a hidden or zero-sized
350
+ * node (a collapsed rail, a slot wrapper that only holds styles) is skipped
351
+ * instead of being treated as a 0x0 rail. The node is returned because the
352
+ * caller keeps a ResizeObserver pointed at whatever it accepted — the rail is
353
+ * re-rendered (new node) on some open/close transitions.
354
+ *
355
+ * @returns {{node: Element, box: {left: number, top: number, width: number, height: number}}|null}
356
+ */
357
+ function findRail(scope) {
358
+ if (scope === null || scope === undefined || typeof scope.querySelector !== 'function') return null
359
+ for (const selector of RAIL_SELECTORS) {
360
+ let node = null
361
+ try { node = scope.querySelector(selector) } catch { node = null }
362
+ if (node === null || typeof node.getBoundingClientRect !== 'function') continue
363
+ let box = null
364
+ try { box = dockedBodyRect(node) } catch { box = null }
365
+ if (railOnScreen(box)) return { node, box }
366
+ }
367
+ return null
368
+ }
369
+
370
+ /**
371
+ * The rectangle a side card occupies when the right rail is on screen.
372
+ *
373
+ * Pure (no DOM) so the runtime and the tests share one implementation: the
374
+ * caller passes the rail's measured box. `null` means "do not dock" — the
375
+ * rail is missing, unmeasurable, or too small to be worth a page, and the
376
+ * caller keeps the floating presentation instead.
377
+ *
378
+ * Nothing is ever allowed to spill outside the frame: the rectangle is
379
+ * clamped to `[0, frameWidth] × [0, frameHeight]` and `clipped` reports
380
+ * whether that clamp actually bit (a rail straddling the frame edge).
381
+ *
382
+ * @param {{railLeft: number, railTop: number, railWidth: number, railHeight: number,
383
+ * frameWidth?: number|null, frameHeight?: number|null, enabled?: boolean}} input
384
+ * @returns {{left: number, top: number, width: number, height: number,
385
+ * clipped: boolean}|null}
386
+ */
387
+ function railDock({ railLeft, railTop, railWidth, railHeight, frameWidth = null, frameHeight = null, enabled = true }) {
388
+ if (enabled !== true) return null
389
+ if (!Number.isFinite(railLeft) || !Number.isFinite(railTop)) return null
390
+ const width = Number.isFinite(railWidth) ? Math.floor(railWidth) : 0
391
+ const height = Number.isFinite(railHeight) ? Math.floor(railHeight) : 0
392
+ if (width < DOCK_MIN_WIDTH || height < DOCK_MIN_HEIGHT) return null
393
+ const frameW = Number.isFinite(frameWidth) ? frameWidth : railLeft + width
394
+ const frameH = Number.isFinite(frameHeight) ? frameHeight : railTop + height
395
+ const left = Math.max(0, Math.min(Math.floor(railLeft), Math.floor(frameW)))
396
+ const top = Math.max(0, Math.min(Math.floor(railTop), Math.floor(frameH)))
397
+ const dockWidth = Math.max(0, Math.min(width, Math.floor(frameW - left)))
398
+ const dockHeight = Math.max(0, Math.min(height, Math.floor(frameH - top)))
399
+ if (dockWidth < DOCK_MIN_WIDTH || dockHeight < DOCK_MIN_HEIGHT) return null
400
+ // `clipped` means "what was painted is not what was measured": either the
401
+ // size shrank, or the origin had to be pulled back inside the frame (a rail
402
+ // still animating in can measure negative). Callers surface it so a
403
+ // surprising rectangle is explainable instead of mysterious.
404
+ return {
405
+ left,
406
+ top,
407
+ width: dockWidth,
408
+ height: dockHeight,
409
+ clipped: dockWidth < width || dockHeight < height
410
+ || left !== Math.floor(railLeft) || top !== Math.floor(railTop),
411
+ }
412
+ }
413
+
253
414
  // ─────────────────────────────────────────────────────────────────────
254
415
  // §2 i18n
255
416
  // ─────────────────────────────────────────────────────────────────────
@@ -327,8 +488,9 @@ window.__ModuleLoader__.load({
327
488
  setTriggerBoth: '按钮 + 快捷键',
328
489
  setCarrier: '默认作答位置',
329
490
  setSurface: '侧边卡片承载面',
330
- setSurfaceAuto: '自动选择',
491
+ setSurfaceAuto: '自动选择(插件 → 内置卡片)',
331
492
  setSurfaceFlow: '内置浮层卡片',
493
+ surfaceNativeForce: '右侧栏卡片(强制,不参与自动)',
332
494
  setMaxChars: '最大字符数',
333
495
  setMaxCharsHint: '超过部分截断并在提问中标注',
334
496
  setFloatMode: '浮层默认形态',
@@ -338,6 +500,9 @@ window.__ModuleLoader__.load({
338
500
  setFloatModeHint: '浮层贴右侧边缘,只在空间不足时才可能压到对话区',
339
501
  setFloatMaxWidth: '浮层宽度上限(px)',
340
502
  setFloatMaxWidthHint: '空间不足时自动收窄,不会硬顶',
503
+ setDockRail: '右侧栏打开时占满它',
504
+ setDockRailHint: '答案直接铺满右侧栏(读得像栏内的一页),收起为胶囊或关闭即恢复原状;右侧栏不在时不生效',
505
+ floatDocked: '已占满右侧栏',
341
506
  floatExpand: '展开侧边卡片',
342
507
  floatCollapse: '收起为胶囊(不遮挡对话)',
343
508
  floatHide: '隐藏浮层(只留小圆点)',
@@ -564,8 +729,9 @@ window.__ModuleLoader__.load({
564
729
  setTriggerBoth: 'Button + shortcut',
565
730
  setCarrier: 'Default answer carrier',
566
731
  setSurface: 'Side-card surface',
567
- setSurfaceAuto: 'Automatic',
732
+ setSurfaceAuto: 'Automatic (plugin, then built-in)',
568
733
  setSurfaceFlow: 'Built-in floating card',
734
+ surfaceNativeForce: 'Right rail (forced, excluded from automatic)',
569
735
  setMaxChars: 'Max characters',
570
736
  setMaxCharsHint: 'Longer selections are truncated and marked in the question',
571
737
  setFloatMode: 'Floating layer default',
@@ -575,6 +741,9 @@ window.__ModuleLoader__.load({
575
741
  setFloatModeHint: 'The layer is anchored to the right edge and only overlaps on very narrow frames',
576
742
  setFloatMaxWidth: 'Floating layer max width (px)',
577
743
  setFloatMaxWidthHint: 'Narrows automatically when there is not enough room',
744
+ setDockRail: 'Fill the right rail while it is open',
745
+ setDockRailHint: 'The answer fills the right rail (it reads as one of its pages) and comes back to normal when collapsed to the capsule or closed; ignored while no rail is on screen',
746
+ floatDocked: 'Filled the right rail',
578
747
  floatExpand: 'Expand the side cards',
579
748
  floatCollapse: 'Collapse to a capsule (stops covering the conversation)',
580
749
  floatHide: 'Hide the layer (leaves a small launcher)',
@@ -1839,12 +2008,20 @@ window.__ModuleLoader__.load({
1839
2008
  * @returns {{adapter: object, fellBack: boolean}}
1840
2009
  */
1841
2010
  function pickSurface(wanted) {
1842
- const order = [surfaces.native, surfaces.better, surfaces.flow]
2011
+ // `auto` deliberately skips the shipped right rail. A card re-hosted as a
2012
+ // rail tab is a tab the plugin does not own: it outlives the card (the
2013
+ // rail keeps an empty tab reading "this card was closed"), it competes
2014
+ // with the user's own rail tabs, and where it lands depends on a
2015
+ // service-surface detail we do not control. The order asked for is: the
2016
+ // side-card plugin first, the built-in card second. The rail stays
2017
+ // reachable, but only by an explicit `native-rightbar` choice.
2018
+ const autoOrder = [surfaces.better, surfaces.flow]
2019
+ const all = [surfaces.native, surfaces.better, surfaces.flow]
1843
2020
  if (wanted === 'auto' || wanted === undefined) {
1844
- for (const adapter of order) if (adapter.available()) return { adapter, fellBack: false }
2021
+ for (const adapter of autoOrder) if (adapter.available()) return { adapter, fellBack: false }
1845
2022
  return { adapter: surfaces.flow, fellBack: false }
1846
2023
  }
1847
- const match = order.find(adapter => adapter.kind === wanted || adapter === surfaces[wanted])
2024
+ const match = all.find(adapter => adapter.kind === wanted || adapter === surfaces[wanted])
1848
2025
  if (match === undefined) return { adapter: surfaces.flow, fellBack: true }
1849
2026
  if (!match.available()) return { adapter: surfaces.flow, fellBack: true }
1850
2027
  return { adapter: match, fellBack: false }
@@ -1875,12 +2052,28 @@ window.__ModuleLoader__.load({
1875
2052
  font-family:inherit;white-space:nowrap}
1876
2053
  .dsa-trigger:hover{border-color:var(--dsw-alias-brand-primary, #4c8dff)}
1877
2054
  .dsa-trigger:focus-visible{outline:2px solid var(--dsw-alias-brand-primary, #4c8dff);outline-offset:1px}
1878
- /* The one-click action bar: the trigger button plus compact chips, laid out
1879
- in one wrapping row under the selection. The container is positioned by the
1880
- component; chips keep their own pointer-events because .dsa-layer is a
1881
- click-through layer. */
1882
- .dsa-quickbar{pointer-events:auto;position:fixed;display:flex;align-items:center;gap:6px;
1883
- max-width:min(620px, 92vw);flex-wrap:wrap;z-index:60}
2055
+ /* The one-click action bar: the main ask button plus the quick chips, inside ONE
2056
+ rounded surface. Six bordered pills in a row read as six competing actions —
2057
+ no primary, no grouping — so the bar is a single control: a filled brand
2058
+ button for "ask", a hairline separator, then quiet ghost chips. It wraps
2059
+ instead of clipping (the component also caps its width to the viewport). */
2060
+ .dsa-quickbar{pointer-events:auto;position:fixed;display:inline-flex;align-items:center;
2061
+ gap:2px;row-gap:2px;padding:3px;border-radius:12px;flex-wrap:wrap;z-index:60;
2062
+ border:1px solid rgba(127,127,127,.5);
2063
+ background:var(--dsw-alias-bg-overlay, #fff);
2064
+ box-shadow:0 6px 20px rgba(0,0,0,.16), 0 1px 3px rgba(0,0,0,.10)}
2065
+ .dsa-quickbar .dsa-trigger-embedded{position:static;border:0;border-radius:9px;
2066
+ padding:4px 10px;font-weight:600;box-shadow:none;
2067
+ background:var(--dsw-alias-brand-primary, #4c8dff);color:#fff}
2068
+ .dsa-quickbar .dsa-trigger-embedded:hover{border-color:transparent;filter:brightness(1.06)}
2069
+ /* Dark brand (#5b9cf5 on the bundled skin) puts white at 2.79:1 — below the 4.5:1
2070
+ body-text floor. Dark ink measures 6.57:1, same remedy as the carrier switch. */
2071
+ html[data-ds-dark-theme] .dsa-quickbar .dsa-trigger-embedded,
2072
+ body[data-ds-dark-theme] .dsa-quickbar .dsa-trigger-embedded{color:#0a1428}
2073
+ /* A hairline between "do the main thing" and "or pick a canned question". */
2074
+ .dsa-quickbar-sep{flex:none;align-self:center;width:1px;height:15px;margin:0 2px;
2075
+ background:var(--dsw-alias-border-l1, rgba(127,127,127,.28))}
2076
+ .dsa-quickbar-chips{display:contents}
1884
2077
  .dsa-chip{pointer-events:auto;padding:3px 10px;border-radius:999px;
1885
2078
  border:1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.35));
1886
2079
  background:var(--dsw-alias-bg-overlay, #fff);color:var(--dsw-alias-label-primary, #111);
@@ -1888,6 +2081,14 @@ window.__ModuleLoader__.load({
1888
2081
  box-shadow:0 2px 8px rgba(0,0,0,.14)}
1889
2082
  .dsa-chip:hover{border-color:var(--dsw-alias-brand-primary, #4c8dff)}
1890
2083
  .dsa-chip:focus-visible{outline:2px solid var(--dsw-alias-brand-primary, #4c8dff);outline-offset:1px}
2084
+ /* Inside the action bar the chips lose their own frame: the bar is the frame.
2085
+ Secondary text colour keeps them quieter than the filled ask button while
2086
+ still clearing 6:1 on the bundled skin (label-secondary 9.36:1 in dark). */
2087
+ .dsa-quickbar .dsa-chip{border:0;background:transparent;box-shadow:none;
2088
+ padding:3px 8px;border-radius:7px;color:var(--dsw-alias-label-secondary, #666)}
2089
+ .dsa-quickbar .dsa-chip:hover{background:var(--dsw-alias-bg-layer-2, rgba(127,127,127,.14));
2090
+ color:var(--dsw-alias-label-primary, #111)}
2091
+ .dsa-quickbar .dsa-chip:focus-visible{outline-offset:0}
1891
2092
  /* Chips rendered INSIDE the popover need no floating shadow. */
1892
2093
  .dsa-pop-chips{display:flex;gap:6px;flex-wrap:wrap}
1893
2094
  .dsa-pop-chips .dsa-chip{box-shadow:none}
@@ -1919,13 +2120,18 @@ window.__ModuleLoader__.load({
1919
2120
  trigger drops its own fixed positioning, chips join the row directly. */
1920
2121
  .dsa-trigger-embedded{position:static;box-shadow:0 2px 10px rgba(0,0,0,.18)}
1921
2122
  .dsa-quickbar-chips{display:contents}
1922
- /* Contrast pass, driven by a light-theme reference screenshot: every 1px edge
1923
- inside the popover measured only ≈1.3–1.7:1 against the card surface, which
1924
- is why the card read as a translucent sticker instead of a framed popover.
1925
- The frame is therefore drawn explicitly here rather than through border-l2,
1926
- which the host resolves below that level; mid grey is used instead of a
1927
- black/white alpha so the stroke stays visible on both themes. The shadow is
1928
- a two-layer falloff so the card lifts off the page as well. */
2123
+ /* Contrast pass on the popover frame. Measured against the card surface with the
2124
+ bundled skin's tokens (bg-base .55 over white → bg-overlay .98): the inner 1px
2125
+ edges drawn with border-l1 sit at ≈1.40:1, which is why the card read as a
2126
+ translucent sticker rather than a framed popover. The outer frame drawn with
2127
+ border-l2 was already ≈1.95:1 (light) / ≈2.88:1 (dark), so swapping it for mid
2128
+ grey is a *hue* choice, not a contrast gain: mid grey .55 measures ≈1.96:1
2129
+ light / ≈2.12:1 dark — neutral instead of blue-on-blue, and slightly weaker in
2130
+ dark. Explicit mid grey is still used over border-l2 because the host may
2131
+ resolve that alias to the surrounding fill; if a genuinely stronger frame is
2132
+ wanted, grey .85 measures ≈3.03:1. The two-layer shadow is what lifts the card
2133
+ off the page; secondary text inside the popover uses label-primary (6.11:1)
2134
+ rather than label-secondary. */
1929
2135
  .dsa-pop{pointer-events:auto;position:fixed;width:min(420px, 92vw);display:flex;flex-direction:column;gap:8px;
1930
2136
  padding:12px;border-radius:12px;border:1px solid rgba(127,127,127,.55);
1931
2137
  background:var(--dsw-alias-bg-overlay, #fff);color:var(--dsw-alias-label-primary, #111);
@@ -1990,6 +2196,12 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"],
1990
2196
  html[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover,
1991
2197
  body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
1992
2198
  color:#0a1428;text-shadow:0 1px 0 rgba(255,255,255,.25)}
2199
+ /* The same switch is reused on the settings page, inside a grid cell that can be
2200
+ narrower than the popover: tighten the chips there, never let the pair spill out
2201
+ of its cell, and keep the rail hugging the chips (dsa-field is a stretch
2202
+ column, which would otherwise paint a 400px-wide rail around two small chips). */
2203
+ .dsa-field .dsa-seg{max-width:100%;align-self:flex-start}
2204
+ .dsa-field .dsa-seg button{padding:4px 10px}
1993
2205
  .dsa-card{pointer-events:auto;display:flex;flex-direction:column;gap:8px;padding:12px;border-radius:12px;
1994
2206
  border:1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.35));
1995
2207
  background:var(--dsw-alias-bg-overlay, #fff);color:var(--dsw-alias-label-primary, #111);
@@ -2001,6 +2213,19 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
2001
2213
  .dsa-float{pointer-events:none;position:fixed;right:var(--dsa-gap-right, 8px);bottom:16px;
2002
2214
  display:flex;flex-direction:column;gap:8px;width:var(--dsa-float-w, 340px);max-width:92vw}
2003
2215
  .dsa-float > *{pointer-events:auto}
2216
+ /* Docked presentation (1.8.3): the same container, but pinned to the right
2217
+ rail's measured rectangle (inline left/top/width/height) instead of the
2218
+ frame's bottom-right corner. Right/bottom must be released, otherwise the
2219
+ base rule's right:8px/bottom:16px would over-constrain the box. The rail's
2220
+ resize handle is covered while docked — collapsing to the capsule (⇥) or
2221
+ closing gives it back. */
2222
+ .dsa-float[data-dsa-float-mode="dock"]{right:auto;bottom:auto;max-width:none;pointer-events:auto;
2223
+ border-left:1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.35));
2224
+ background:var(--dsw-alias-bg-overlay, #fff);overflow:hidden}
2225
+ .dsa-float[data-dsa-float-mode="dock"] .dsa-stack{max-height:none;height:100%;min-height:0;overflow:hidden}
2226
+ .dsa-float[data-dsa-float-mode="dock"] .dsa-card{height:100%;min-height:0;max-height:none;border:0;border-radius:0;
2227
+ box-shadow:none;background:transparent;padding:10px 12px 8px}
2228
+ .dsa-float[data-dsa-float-mode="dock"] .dsa-answer{max-height:none;flex:1 1 auto;min-height:0;overflow:auto}
2004
2229
  .dsa-stack{display:flex;flex-direction:column;gap:8px;max-height:52vh;overflow:auto}
2005
2230
  .dsa-capsule{display:flex;align-items:center;gap:8px;width:100%;box-sizing:border-box;text-align:left;
2006
2231
  padding:8px 12px;border-radius:999px;cursor:pointer;font:inherit;font-size:12px;line-height:18px;
@@ -3239,12 +3464,23 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
3239
3464
  (sum, action) => sum + Math.ceil(estimateLabelWidth(action.label) + 24), 0)
3240
3465
  const rowWidth = Math.min(620, mainWidth + (actions.length > 0 ? 6 + chipsWidth : 0))
3241
3466
  const box = clampBox(candidate.rect, rowWidth, 34, 'below')
3467
+ // The estimate above is a clamp input, not a promise: if it is wrong the
3468
+ // bar must wrap, never spill past the frame edge. Cap its width to the
3469
+ // space actually left of it (and still to 620px, which is about as wide
3470
+ // as an action bar stays readable).
3471
+ const viewportWidth = (typeof window !== 'undefined' && Number.isFinite(window.innerWidth))
3472
+ ? window.innerWidth
3473
+ : 0
3474
+ const room = viewportWidth > 0 ? Math.max(220, viewportWidth - box.left - 8) : 620
3242
3475
  return h('div', {
3243
3476
  className: 'dsa-quickbar',
3244
- style: { left: `${box.left}px`, top: `${box.top}px` },
3477
+ style: { left: `${box.left}px`, top: `${box.top}px`, maxWidth: `${Math.min(620, room)}px` },
3245
3478
  onMouseDown: event => event.stopPropagation(),
3246
3479
  },
3247
3480
  h(TriggerButton, { candidate, onOpen, embedded: true }),
3481
+ actions.length > 0
3482
+ ? h('span', { key: 'sep', className: 'dsa-quickbar-sep', 'aria-hidden': true })
3483
+ : null,
3248
3484
  h(QuickChips, { actions, onPick: onQuick, inline: true }))
3249
3485
  }
3250
3486
 
@@ -3818,11 +4054,14 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
3818
4054
  */
3819
4055
  function FloatingLayer({ cards, actions }) {
3820
4056
  const containerRef = React.useRef(null)
4057
+ const dockRef = React.useRef(null)
3821
4058
  const state = useStoreState()
4059
+ const [dock, setDock] = React.useState(null)
3822
4060
  const visible = cards.filter(card => card.surface === 'flow' && card.open)
3823
4061
  const total = visible.length
3824
4062
  const mode = state.floatMode ?? state.config.floatMode ?? CLIENT_DEFAULTS.floatMode
3825
4063
  const maxWidth = state.config.floatMaxWidth ?? CLIENT_DEFAULTS.floatMaxWidth
4064
+ const dockWanted = state.config.dockRail !== false
3826
4065
 
3827
4066
  // Measure the conversation column + right rail, then pin the layer's
3828
4067
  // width to what is left. Recomputed on frame resize and on rail toggles
@@ -3833,6 +4072,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
3833
4072
  const node = containerRef.current
3834
4073
  if (node === null || node === undefined) return undefined
3835
4074
  const selector = '[data-slot^="conversation"], .rightbar, [data-slot^="sidebar.right"], [data-dsh-panel-host]'
4075
+ const observed = new Set()
3836
4076
  const all = () => {
3837
4077
  try {
3838
4078
  return typeof document.querySelectorAll === 'function'
@@ -3844,20 +4084,73 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
3844
4084
  try { return typeof document.querySelector === 'function' ? document.querySelector(query) : null } catch { return null }
3845
4085
  }
3846
4086
  let frame = 0
4087
+ let observer = null
4088
+ let mutation = null
4089
+ let observedRail = null
4090
+ const timers = []
3847
4091
  const apply = () => {
3848
4092
  if (typeof node.style?.setProperty !== 'function') return
3849
4093
  const lane = one('[data-slot^="conversation"]')
3850
- const rail = one('.rightbar, [data-slot^="sidebar.right"], [data-dsh-panel-host]')
3851
4094
  const rootWidth = document.documentElement?.clientWidth ?? window.innerWidth
3852
- const laneRight = lane === null
3853
- ? rootWidth
3854
- : lane.getBoundingClientRect().right
3855
- const railLeft = rail === null ? null : rail.getBoundingClientRect().left
4095
+ const rootHeight = document.documentElement?.clientHeight ?? window.innerHeight
4096
+ const measure = target => {
4097
+ if (target === null || target === undefined) return null
4098
+ try {
4099
+ const rect = typeof target.getBoundingClientRect === 'function'
4100
+ ? target.getBoundingClientRect()
4101
+ : null
4102
+ return rect === null ? null : rect
4103
+ } catch { return null }
4104
+ }
4105
+ const laneRect = measure(lane)
4106
+ const rail = findRail(document)
4107
+ const railBox = rail === null ? null : rail.box
4108
+ const laneRight = laneRect === null ? rootWidth : laneRect.right
4109
+ const railLeft = railBox === null ? null : railBox.left
3856
4110
  const placement = floatPlacement({ frameWidth: rootWidth, laneRight, railLeft, maxWidth })
3857
4111
  node.style.setProperty('--dsa-float-w', `${placement.width}px`)
3858
4112
  node.style.setProperty('--dsa-gap-right', `${FLOAT_GAP_RIGHT}px`)
3859
4113
  // Readable placement, so a user report can say WHERE the layer sat.
3860
4114
  node.dataset.layerWidth = String(placement.width)
4115
+
4116
+ // Docked presentation: the card fills the rail page's body instead of
4117
+ // floating over it. Only measured in `full` mode — a capsule/launcher
4118
+ // is deliberately small and must never take the rail over.
4119
+ const next = railBox === null || mode !== 'full'
4120
+ ? null
4121
+ : railDock({
4122
+ railLeft: railBox.left,
4123
+ railTop: railBox.top,
4124
+ railWidth: railBox.width,
4125
+ railHeight: railBox.height,
4126
+ frameWidth: rootWidth,
4127
+ frameHeight: rootHeight,
4128
+ enabled: dockWanted,
4129
+ })
4130
+ const prev = dockRef.current
4131
+ const same = prev === next || (prev !== null && next !== null
4132
+ && prev.left === next.left && prev.top === next.top
4133
+ && prev.width === next.width && prev.height === next.height
4134
+ && prev.clipped === next.clipped)
4135
+ if (!same) {
4136
+ dockRef.current = next
4137
+ setDock(next)
4138
+ }
4139
+ // Why the layer is (not) docked, readable from DevTools. A rail that
4140
+ // exists but is too narrow reads as `rail-too-small`, which is a
4141
+ // different story from `no-rail` and is what a bug report needs.
4142
+ node.dataset.dsaDock = next !== null
4143
+ ? 'dock'
4144
+ : (mode !== 'full'
4145
+ ? 'mode'
4146
+ : (dockWanted ? (railBox === null ? 'no-rail' : 'rail-too-small') : 'off'))
4147
+ // The rail can appear AFTER this effect ran (opening it mounts new
4148
+ // nodes), and it can be re-created later: keep the observer pointed at
4149
+ // whichever node is currently accepted.
4150
+ if (observer !== null && rail !== null && rail.node !== observedRail) {
4151
+ observedRail = rail.node
4152
+ try { observer.observe(rail.node) } catch { /* not observable */ }
4153
+ }
3861
4154
  }
3862
4155
  const schedule = () => {
3863
4156
  if (typeof window.requestAnimationFrame !== 'function') { apply(); return }
@@ -3868,7 +4161,6 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
3868
4161
  })
3869
4162
  }
3870
4163
  apply()
3871
- let observer = null
3872
4164
  if (typeof window.ResizeObserver === 'function') {
3873
4165
  observer = new window.ResizeObserver(schedule)
3874
4166
  for (const target of all()) {
@@ -3876,12 +4168,35 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
3876
4168
  }
3877
4169
  }
3878
4170
  if (typeof window.addEventListener === 'function') window.addEventListener('resize', schedule)
4171
+ // Opening/closing the rail changes no size this effect already watches,
4172
+ // so the DOM itself has to be the trigger. Attributes only (no
4173
+ // childList): streaming tokens rewrite text nodes continuously and must
4174
+ // not wake the layer on every frame.
4175
+ if (typeof window.MutationObserver === 'function' && document.body !== null && document.body !== undefined) {
4176
+ try {
4177
+ mutation = new window.MutationObserver(schedule)
4178
+ mutation.observe(document.body, {
4179
+ attributes: true,
4180
+ subtree: true,
4181
+ attributeFilter: ['data-sidebar-right-open', 'data-dockkit-pane-active', 'style', 'class'],
4182
+ })
4183
+ } catch { mutation = null }
4184
+ }
4185
+ // The rail slides in; a rect taken mid-animation is wrong. Re-measure a
4186
+ // few times right after a card/mode change, then stop for good.
4187
+ if (typeof window.setTimeout === 'function') {
4188
+ for (const delay of [80, 200, 420, 800]) timers.push(window.setTimeout(schedule, delay))
4189
+ }
3879
4190
  return () => {
3880
4191
  if (typeof window.removeEventListener === 'function') window.removeEventListener('resize', schedule)
3881
4192
  if (observer !== null) { try { observer.disconnect() } catch { /* already gone */ } }
4193
+ if (mutation !== null) { try { mutation.disconnect() } catch { /* already gone */ } }
4194
+ for (const timer of timers) {
4195
+ if (typeof window.clearTimeout === 'function') window.clearTimeout(timer)
4196
+ }
3882
4197
  if (frame !== 0 && typeof window.cancelAnimationFrame === 'function') window.cancelAnimationFrame(frame)
3883
4198
  }
3884
- }, [maxWidth, total, mode])
4199
+ }, [maxWidth, total, mode, dockWanted])
3885
4200
 
3886
4201
  if (total === 0) return null
3887
4202
 
@@ -3894,13 +4209,47 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
3894
4209
  : t('floatDone', { n: total })
3895
4210
  const firstQuestion = visible[0].question
3896
4211
 
4212
+ // Docked geometry is only meaningful in `full` mode; the effect nulls it
4213
+ // out otherwise, and this guard keeps a stale rect from a previous mode
4214
+ // from being painted for one frame.
4215
+ const docked = mode === 'full' && dock !== null
4216
+
3897
4217
  const container = children => h('div', {
3898
- className: 'dsa-float',
4218
+ className: docked ? 'dsa-float dsa-dock' : 'dsa-float',
3899
4219
  'data-dsa-root': '',
3900
4220
  ref: containerRef,
3901
- 'data-dsa-float-mode': mode,
4221
+ 'data-dsa-float-mode': docked ? 'dock' : mode,
4222
+ ...(docked
4223
+ ? {
4224
+ style: {
4225
+ left: `${dock.left}px`,
4226
+ top: `${dock.top}px`,
4227
+ width: `${dock.width}px`,
4228
+ height: `${dock.height}px`,
4229
+ },
4230
+ }
4231
+ : null),
3902
4232
  }, children)
3903
4233
 
4234
+ const cardNodes = visible.map(card => h(AnswerCard, {
4235
+ key: card.id,
4236
+ card,
4237
+ surfaceLabel: null,
4238
+ onCopy: element => actions.copy(card, element),
4239
+ onFollowUp: text => actions.followUp(card, text),
4240
+ onRetry: () => actions.retry(card),
4241
+ onRetryEdit: text => actions.retryEdit(card, text),
4242
+ onCancel: () => actions.cancel(card),
4243
+ onSendToMain: () => actions.sendToMain(card),
4244
+ onClose: () => actions.close(card),
4245
+ onLocate: target => actions.locateCard(target ?? card),
4246
+ onExport: target => actions.exportCard(target ?? card),
4247
+ onInsert: target => actions.insertAnswer(target ?? card),
4248
+ onReplace: target => actions.insertAnswer(target ?? card, { replace: true }),
4249
+ onViewVersion: index => actions.viewVersion(card, index),
4250
+ onFloatMode: setFloatMode,
4251
+ }))
4252
+
3904
4253
  if (mode === 'off') {
3905
4254
  return container(h('button', {
3906
4255
  type: 'button',
@@ -3921,25 +4270,14 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
3921
4270
  h('span', { className: 'dsa-capsule-chev' }, '›')))
3922
4271
  }
3923
4272
 
3924
- return container(h('div', { className: 'dsa-stack' },
3925
- visible.map(card => h(AnswerCard, {
3926
- key: card.id,
3927
- card,
3928
- surfaceLabel: null,
3929
- onCopy: element => actions.copy(card, element),
3930
- onFollowUp: text => actions.followUp(card, text),
3931
- onRetry: () => actions.retry(card),
3932
- onRetryEdit: text => actions.retryEdit(card, text),
3933
- onCancel: () => actions.cancel(card),
3934
- onSendToMain: () => actions.sendToMain(card),
3935
- onClose: () => actions.close(card),
3936
- onLocate: target => actions.locateCard(target ?? card),
3937
- onExport: target => actions.exportCard(target ?? card),
3938
- onInsert: target => actions.insertAnswer(target ?? card),
3939
- onReplace: target => actions.insertAnswer(target ?? card, { replace: true }),
3940
- onViewVersion: index => actions.viewVersion(card, index),
3941
- onFloatMode: setFloatMode,
3942
- }))))
4273
+ // Docked: the card takes the rail's whole rectangle, so it reads as one of
4274
+ // the rail's pages rather than a 340px card hovering over it. The card
4275
+ // keeps its own header (title + badges + collapse/hide controls), which is
4276
+ // what makes the page dismissible without leaving the rail.
4277
+ return container(h('div', {
4278
+ className: 'dsa-stack',
4279
+ 'data-dsa-dock-body': docked ? String(dock.width) : undefined,
4280
+ }, cardNodes))
3943
4281
  }
3944
4282
 
3945
4283
  /** The `shell.overlay` entry: styles + trigger + popover + cards + toast. */
@@ -4016,7 +4354,10 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
4016
4354
  else toast(t('notifyGranted'))
4017
4355
  }
4018
4356
 
4019
- const field = (label, control, hint) => h('div', { className: 'dsa-field' },
4357
+ // `style` is optional: the two-chip carrier switch needs two grid tracks
4358
+ // (`.dsa-grid` fits columns down to 150px, and two nowrap chips need
4359
+ // ~175px), so that field spans two tracks instead of overflowing.
4360
+ const field = (label, control, hint, style) => h('div', { className: 'dsa-field', style },
4020
4361
  h('label', null, label),
4021
4362
  control,
4022
4363
  hint !== undefined ? h('span', { className: 'dsa-muted' }, hint) : null)
@@ -4247,6 +4588,8 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
4247
4588
  ? h('div', { className: 'dsa-family-notice', role: 'status' }, t('familyLegacyToast'))
4248
4589
  : null)
4249
4590
 
4591
+ // NOT marked `data-dsa-rail-host`: the settings page is not a rail page,
4592
+ // and marking it would let the floating layer dock over the settings panel.
4250
4593
  return h('div', { className: 'dsa-settings', 'data-dsa-root': '' },
4251
4594
  h(Styles, null),
4252
4595
  h('div', null,
@@ -4259,15 +4602,15 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
4259
4602
  ['shortcut', t('setTriggerShortcut')],
4260
4603
  ['both', t('setTriggerBoth')],
4261
4604
  ])),
4262
- field(t('setCarrier'), select('defaultCarrier', [
4263
- ['side', t('carrierSide')],
4264
- ['main', t('carrierMain')],
4265
- ])),
4605
+ field(t('setCarrier'), h(CarrierSeg, {
4606
+ value: draft.defaultCarrier,
4607
+ onChange: id => setDraft({ ...draft, defaultCarrier: id }),
4608
+ }), undefined, { gridColumn: 'span 2' }),
4266
4609
  field(t('setSurface'), select('sideSurface', [
4267
4610
  ['auto', t('setSurfaceAuto')],
4268
- ['flow', t('setSurfaceFlow')],
4269
- ['native-rightbar', t('surfaceNative')],
4270
4611
  ['better-sidebar', t('surfaceBetter')],
4612
+ ['flow', t('setSurfaceFlow')],
4613
+ ['native-rightbar', t('surfaceNativeForce')],
4271
4614
  ])),
4272
4615
  field(t('setZones'), select('captureZones', [
4273
4616
  ['auto', t('setZonesAuto')],
@@ -4347,6 +4690,14 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
4347
4690
  }),
4348
4691
  t('setFollowUps')),
4349
4692
  h('span', { className: 'dsa-muted' }, t('setFollowUpsHint')),
4693
+ h('label', { className: 'dsa-check' },
4694
+ h('input', {
4695
+ type: 'checkbox',
4696
+ checked: draft.dockRail !== false,
4697
+ onChange: event => setDraft({ ...draft, dockRail: event.target.checked }),
4698
+ }),
4699
+ t('setDockRail')),
4700
+ h('span', { className: 'dsa-muted' }, t('setDockRailHint')),
4350
4701
  historySection,
4351
4702
  h('div', { className: 'dsa-row' },
4352
4703
  h('button', {
@@ -5774,7 +6125,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
5774
6125
  }
5775
6126
  }, [cardId])
5776
6127
  if (card === undefined) {
5777
- return h('div', { className: 'dsa-settings', 'data-dsa-root': '' },
6128
+ return h('div', { className: 'dsa-settings', 'data-dsa-root': '', 'data-dsa-rail-host': '' },
5778
6129
  h(Styles, null),
5779
6130
  h('div', { className: 'dsa-muted' }, t('cardClosed')))
5780
6131
  }
@@ -5785,11 +6136,11 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
5785
6136
  // note for a rail that keeps it.
5786
6137
  try { surfaces.native.close(card) } catch { /* the rail owns its lifecycle */ }
5787
6138
  try { surfaces.better.close(card) } catch { /* the host may already be gone */ }
5788
- return h('div', { className: 'dsa-settings', 'data-dsa-root': '' },
6139
+ return h('div', { className: 'dsa-settings', 'data-dsa-root': '', 'data-dsa-rail-host': '' },
5789
6140
  h(Styles, null),
5790
6141
  h('div', { className: 'dsa-muted' }, t('cardMovedToFlow')))
5791
6142
  }
5792
- return h('div', { className: 'dsa-settings', 'data-dsa-root': '', style: { padding: '10px' } },
6143
+ return h('div', { className: 'dsa-settings', 'data-dsa-root': '', 'data-dsa-rail-host': '', style: { padding: '10px' } },
5793
6144
  h(Styles, null),
5794
6145
  h(AnswerCard, {
5795
6146
  card,
@@ -5967,6 +6318,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
5967
6318
  renderRichText,
5968
6319
  pickSurface,
5969
6320
  floatPlacement,
6321
+ railDock,
5970
6322
  compareVersions,
5971
6323
  electFamilyLeader,
5972
6324
  findLegacyLayers,
package/index.js CHANGED
@@ -42,7 +42,7 @@ export const name = 'dsh-sidecard-ask'
42
42
  export const inject = ['webServer']
43
43
 
44
44
  /** Version of this plugin (kept in step with package.json by test/verify.mjs). */
45
- export const PLUGIN_VERSION = '1.8.2'
45
+ export const PLUGIN_VERSION = '1.8.4'
46
46
 
47
47
  /** Route prefix of the plugin's own API. */
48
48
  export const ROUTE_PREFIX = '/sidecard-ask/api'
@@ -83,6 +83,17 @@ export const DEFAULT_CONFIG = {
83
83
  floatMode: 'capsule',
84
84
  /** Hard cap, in CSS px, for the floating layer's width. */
85
85
  floatMaxWidth: 340,
86
+ /**
87
+ * Dock the answer card into the right rail when one is on screen, so the
88
+ * answer gets the whole rail instead of a 340px card floating over it.
89
+ *
90
+ * The rail exposes no "add a page" API to plugins on this DSH build (the
91
+ * `sidebarRightTabs` service is absent; see README §6), so the Client half
92
+ * measures the rail's rectangle and paints the card inside it. Turning this
93
+ * off restores the pure floating behaviour; with no rail on screen the
94
+ * floating layer is used either way.
95
+ */
96
+ dockRail: true,
86
97
  /**
87
98
  * One-click quick actions rendered beside the selection trigger.
88
99
  *
@@ -403,7 +414,7 @@ export function sanitizeLayer(raw, problems = []) {
403
414
  if (points >= 0 && points <= MAX_CUSTOM_PERSONA_CHARS) out.customPersona = raw.customPersona
404
415
  else problems.push(`customPersona: 必须是 ≤${MAX_CUSTOM_PERSONA_CHARS} 字符的字符串,已忽略`)
405
416
  }
406
- for (const boolKey of ['showInUnclassified', 'notifyOnDone', 'contextMenu', 'followUpSuggestions']) {
417
+ for (const boolKey of ['showInUnclassified', 'notifyOnDone', 'contextMenu', 'followUpSuggestions', 'dockRail']) {
407
418
  if (raw[boolKey] === undefined) continue
408
419
  if (typeof raw[boolKey] === 'boolean') out[boolKey] = raw[boolKey]
409
420
  else problems.push(`${boolKey}: 必须是布尔值,已忽略`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-sidecard-ask",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "DSH 划词追问(侧边卡片作答):在聊天区与任务区选中文本就地追问,答案由独立子代理在侧边卡片里流式呈现,也可选择落回主对话。",
5
5
  "type": "module",
6
6
  "main": "index.js",