dsh-sidecard-ask 1.8.2 → 1.8.3
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 +84 -2
- package/README.md +56 -11
- package/client.js +368 -51
- package/index.js +13 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,81 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.8.3
|
|
4
|
+
|
|
5
|
+
**主题:右侧栏打开时,答案卡直接铺满它、读成栏内的一页——而不是 340px 的浮层压在右栏正文上。**
|
|
6
|
+
|
|
7
|
+
起因是用户截图:右栏开着「团队任务板」,答案卡浮在它上面,正文被挤成一条窄缝,标题与操作行占掉大半。
|
|
8
|
+
|
|
9
|
+
- **`dockRail`(默认 `true`)**:右栏可见且已展开时,把卡片铺到**标签条下方**的内容区——宽度取右栏宽,
|
|
10
|
+
高度取到帧底;右栏自己的标签条仍然露出可点。这个形态下卡片去掉圆角、描边与阴影,只留一条左边线
|
|
11
|
+
与右栏内容区分界;标题栏(追问 + 徽标 + ⇥/×)与底部操作行固定上下两端,`AnswerBody` 独立滚动,
|
|
12
|
+
长答案不再把操作行推出视野。收起为胶囊(⇥)、关闭(×)或关掉本项,立即回到常规浮层。
|
|
13
|
+
- **几何是量出来的,不是猜的**:`dockedBodyRect(node)` 取右栏面板的 `getBoundingClientRect()`,若它在
|
|
14
|
+
`[data-dockkit-pane]` 里,再把顶边抬到所有 `[data-dockkit-tab]` 的底边——否则卡片会盖住右栏自己的标签条。
|
|
15
|
+
`findRail(scope)` 按世代尝试选择器(最具体的在前):
|
|
16
|
+
`[data-dsa-rail-host]`(本插件自己渲染在右栏里的 `CardHost` 节点——它所在的位置就是「一页正文」)→
|
|
17
|
+
`[data-sidebar-right-panel][data-sidebar-right-open] [data-dockkit-pane]` → 面板本身 →
|
|
18
|
+
`[data-sidebar-right-session][data-sidebar-right-open]` → `[data-dockkit-pane]` / `[data-dockkit-column]`
|
|
19
|
+
(只渲染 dockkit 标记的构建)→ `.rightbar` → `[data-slot^="sidebar.right"]`;
|
|
20
|
+
**只接受展开态**(折叠的面板会滑出帧外,它的 rect 不能用;`railOnScreen` 负责拒绝),量不到、
|
|
21
|
+
宽 < 280px 或高 < 240px 一律回退浮层。
|
|
22
|
+
这些标记不是文档给的,是从安装包里的 `@deepseek-ai/dsh-client-ui-sidebar-right/lib/client.js` 读出来的。
|
|
23
|
+
- **纯函数 `railDock`**:`enabled` 为假、坐标非有限、宽高低于 280/240 → `null`(= 不 dock);矩形夹进
|
|
24
|
+
`[0,frameWidth] × [0,frameHeight]`,夹完仍不足最小尺寸也返回 `null`;`clipped` 表示「画出来的不是量到的」——
|
|
25
|
+
尺寸被夹小,或原点被拉回帧内(右栏入场动画期间可能量到负坐标)。已导出到 `api.pure`,测试与运行时共用一份实现。
|
|
26
|
+
- **渲染**:`docked = mode === 'full' && dock !== null`;容器改 `data-dsa-float-mode="dock"` 并写 inline
|
|
27
|
+
`left/top/width/height`(同时释放基类的 `right/bottom`,否则会过约束),`.dsa-stack` 带
|
|
28
|
+
`data-dsa-dock-body="<px>"` 供对照;三种形态复用同一份 `cardNodes`,没有分叉出第二套卡片。
|
|
29
|
+
- **设置页**新增「右侧栏打开时占满它」复选框(只写草稿,点保存才落盘),并给出「收起为胶囊或关闭即恢复原状;
|
|
30
|
+
右侧栏不在时不生效」的说明;三个 i18n 键(zh/en 各三条)。
|
|
31
|
+
- **为什么不做成"真的注册右栏的一页"(如实)**:`sidebarRightTabs` / `sidebarRight` 两个服务在当前构建里
|
|
32
|
+
**确实存在**(从 `app.asar` 里读到 `ctx.reflect.provide("sidebarRightTabs", tabs)`),注册形状
|
|
33
|
+
(`register({id, kind, guide, …})`)与槽派发键(`entryKey = definition?.id ?? tab.kind`,本插件两处都是
|
|
34
|
+
`sidecard-ask:card`)也逐一核对过;但实测 `controller.openTab` 路径仍回落到浮层,且没定位到抛错点。
|
|
35
|
+
在它修好之前,`dockRail` 是**视觉等价、失败即回退**的兜底层——README §6.1 仍把它标为占位接口,没有说成官方能力。
|
|
36
|
+
- **已知代价(如实)**:dock 期间会盖住右栏内容区里的其他面板(那些面板仍在下面),也会暂时压住右栏的
|
|
37
|
+
拖拽改宽手柄;这两点都写进了 README §4.2 与代码注释。
|
|
38
|
+
- 测试:`float-placement` 新增 **40** 项(`railDock` 的夹取、最小尺寸、`clipped` 语义、不修改入参、非有限输入、
|
|
39
|
+
`dockRail` 默认值、源码钩子、上面三个缺陷的回归断言,以及承载面自动链的顺序断言);`contract` 新增 **8** 项
|
|
40
|
+
(设置页复选框:唯一一行、单个 checkbox、默认勾选、带写草稿的 onChange、说明文案;承载面 `auto` 的
|
|
41
|
+
新偏好);3 个新 i18n 键由既有词典用例自动覆盖。
|
|
42
|
+
总项数 1293 → **1350**。
|
|
43
|
+
- **发布前的一次真实回合(用户截图 + 一个「?」)修掉的三个缺陷**——首次交付的 dock 在真机上**没有生效**:
|
|
44
|
+
卡片仍以约 505px 浮在右栏上,而右栏里留着一个正文写着「该卡片已关闭」的空 tab。
|
|
45
|
+
1. `apply()` 里残留 `rail` 变量:改用 `railBox` 后旧守卫没删干净,一旦 `ResizeObserver` 回调进来就抛
|
|
46
|
+
`ReferenceError: rail is not defined`,并且**右栏节点永远不会被 observe**。已删,并加回归断言
|
|
47
|
+
(源码里不得再出现 `observed.has(rail)`)。
|
|
48
|
+
2. **右栏开合不触发重算**:effect 依赖只有 `[maxWidth, total, mode, dockWanted]`,而被观察的是挂载时
|
|
49
|
+
就已存在的节点——右栏是后开的,量不到就永远不 dock。现在的触发源有四个:`window resize`、
|
|
50
|
+
`ResizeObserver`(对话区 + 当前被接受的那个右栏节点)、**只盯属性的 `MutationObserver`**
|
|
51
|
+
(`data-sidebar-right-open` / `data-dockkit-pane-active` / `style` / `class`;**不盯 childList**,
|
|
52
|
+
否则流式 token 每帧都会唤醒几何计算)、以及挂载后 `[80,200,420,800]ms` 四次重测(罩住右栏滑入动画)。
|
|
53
|
+
3. **不再靠猜右栏标记**:`CardHost` 自己渲染在右栏里的节点就是「一页正文」,它的矩形就是答案。三个
|
|
54
|
+
`CardHost` 包装节点加 `data-dsa-rail-host`,并作为 `RAIL_SELECTORS` 的**第一优先**锚点(设置页根节点
|
|
55
|
+
故意不加,否则浮层会停靠到设置面板上);其后才是 `data-sidebar-right-*`、dockkit 世代、`.rightbar`
|
|
56
|
+
与 `sidebar.right` 槽。折叠态另由 `railOnScreen()` 拒绝(`left >= frameWidth - 1` 或
|
|
57
|
+
`top >= frameHeight - 1` 视为不在屏上),不会被 `railDock` 夹进视野。
|
|
58
|
+
- **诊断属性**:浮层根节点新增 `data-dsa-dock`,取值 `dock` / `no-rail` / `rail-too-small` / `off` / `mode`,
|
|
59
|
+
一眼区分「没找到右栏」「找到了但太窄」「开关关了」「当前不是完整卡片形态」。
|
|
60
|
+
- **承载面 `auto` 的顺序改了:dsh-better-sidebar → 内置浮层卡片;原生右侧栏退出自动链。**
|
|
61
|
+
原因是那个空 tab:卡片被塞进右栏只是一个 tab,tab 的生命周期不归插件管——卡片关掉后右栏仍留着一条
|
|
62
|
+
读作「该卡片已关闭」的标签,还和用户自己的右栏标签抢位置,落点又取决于我们不控制的宿主细节。
|
|
63
|
+
原生右侧栏保留为**强制选项**(下拉里标注「强制,不参与自动」,新增 `surfaceNativeForce` 文案),
|
|
64
|
+
显式 `sideSurface: 'native-rightbar'` 仍然走它。下拉顺序同步为 auto / 插件 / 内置卡片 / 强制右侧栏。
|
|
65
|
+
连带把三条依赖「auto 会挑原生」的既有用例改成显式指定 `native-rightbar`(它们测的是原生适配器本身,
|
|
66
|
+
不该再由 auto 顶上来)。
|
|
67
|
+
- 版本号三处同步到 1.8.3。
|
|
68
|
+
|
|
69
|
+
本机实测(四项均 exit 0):verify **57** / contract **989** / float-placement **78** / smoke **226**
|
|
70
|
+
= **1350 项全部通过**(1.8.2 为 1293;float-placement 由 38 → 78,含 `railDock` 用例、上面三个缺陷的回归钩子,
|
|
71
|
+
以及「自动链里不出现原生右侧栏」的顺序断言;contract 972 → 989)。
|
|
72
|
+
|
|
73
|
+
> 未验证(如实记录):`design-preview/09-dock-{light,dark,narrow-light}.png` 三张图是**离屏渲染**——
|
|
74
|
+
> 无头 Edge 加载"从 `client.js` 切出的真实样式表 + 本机 uiskin 皮肤令牌真值 + 按 `app.asar` 里读到的
|
|
75
|
+
> 右栏标记搭的静态结构"的页面,**不是**运行中 DSH 窗口的截图。因此它证明的是"这套 CSS 在给定矩形里
|
|
76
|
+
> 摆成什么样",不证明真实页面里右栏的实测矩形、滚动性能,也不证明 dock 与右栏拖宽手柄的冲突手感。
|
|
77
|
+
> 这些仍需刷新页面人工确认(README §9 A 路径第 9 条给了可执行的判据)。
|
|
78
|
+
|
|
3
79
|
## 1.8.2
|
|
4
80
|
|
|
5
81
|
**主题:提问框底部的「作答位置」从"两枚看不出选中哪一侧的标签"改成一眼可辨的分段芯片;并把 1.8.1 之后写在本地、尚未提交的弹层对比度修补一并发布。**
|
|
@@ -17,10 +93,16 @@
|
|
|
17
93
|
`--dsw-alias-brand-primary` 提亮到 `#5b9cf5`,白色 600 字在其上低于 4.5:1 正文下限、也低于 3:1 大字下限。
|
|
18
94
|
改法:`[data-ds-dark-theme]` 下选中侧改用深墨 `#0a1428`(实测 6.57:1);浅色保持白字
|
|
19
95
|
(`#2563c0` 上 5.79:1)。令牌值取自本机 `uiskin-theme`,对比度按 WCAG 相对亮度公式实算,不是目测。
|
|
20
|
-
-
|
|
21
|
-
改显式中灰 `rgba(127,127,127,.55)
|
|
96
|
+
- **弹层边框/阴影修补(1.8.1 之后写在本地、未提交,本次一并发布;含一处数字更正)**:`.dsa-pop` 的 1px 边
|
|
97
|
+
改显式中灰 `rgba(127,127,127,.55)`,阴影改两层衰减
|
|
22
98
|
`0 14px 36px rgba(0,0,0,.30), 0 3px 10px rgba(0,0,0,.14)`;`.dsa-pop` 内的次级文案改用
|
|
23
99
|
`label-primary` 并保留 `.78` 不透明度。
|
|
100
|
+
**更正**:随该补丁一起写下的代码注释声称"外框用 `border-l2` 只量到 ≈1.3–1.7:1",实测不成立——
|
|
101
|
+
按本机皮肤令牌复合(bg-base .55 over white → bg-overlay .98)后,`border-l2` 是 **1.95:1(浅)/ 2.88:1(深)**;
|
|
102
|
+
1.40:1 那个数字属于内层 `border-l1` 的 1px 边。换成中灰 .55 后测得 **1.96:1(浅)/ 2.12:1(深)**——
|
|
103
|
+
所以这一步是**色度修正(蓝对蓝 → 中性灰),不是亮度对比度提升**,深色下还略弱。
|
|
104
|
+
真正抬高可读性的是两层阴影与次级文案换成 `label-primary`(6.11:1)。
|
|
105
|
+
若需要更强的实体边框,中灰 .85 测得 ≈3.03:1,但视觉更硬,未擅自采用。
|
|
24
106
|
- 测试:contract 新增 **16** 项钉住这个控件(唯一 `.dsa-seg`、恰好两个按钮、恰好一个
|
|
25
107
|
`aria-pressed="true"`、每个选项带一枚内联 `svg`、`data-on` 只有一个真值、两个去向的标签不互换、
|
|
26
108
|
品牌实底规则与深色覆盖规则必须出现在样式表里)。**点击切换本身仍然不可测**:harness 的
|
package/README.md
CHANGED
|
@@ -156,10 +156,11 @@ plugin_manager(action="install_bundle", target="D:\\Users\\34332\\AI\\dsh-sideca
|
|
|
156
156
|
|---|---|---|---|---|
|
|
157
157
|
| `trigger` | `selection` \| `shortcut` \| `both` | `selection` | **触发方式**:选中即浮出按钮 / 只用快捷键 / 两者都要 | 改后立即(客户端读 /state) |
|
|
158
158
|
| `defaultCarrier` | `main` \| `side` | `side` | **默认作答位置**:提问框里仍可临时切换 | 立即 |
|
|
159
|
-
| `sideSurface` | `auto` \| `native-rightbar` \| `better-sidebar` \| `flow` | `auto` |
|
|
159
|
+
| `sideSurface` | `auto` \| `native-rightbar` \| `better-sidebar` \| `flow` | `auto` | **窗口模式(侧边承载面)**:自动挑选(先 dsh-better-sidebar,再内置浮层卡片)/ 强制 DSH 原生右侧栏(不参与自动)/ 强制 dsh-better-sidebar / 强制内置浮层卡片 | 立即 |
|
|
160
160
|
| `maxChars` | 200–60000 | `4000` | **最大字符数**:超出部分头尾保留、中间截断并标注 | 立即 |
|
|
161
161
|
| `floatMode` | `capsule` \| `full` \| `off` | `capsule` | **内置浮层的默认形态**:胶囊(一行,最小遮挡)/ 完整卡片栈 / 只留一个计数圆点。运行时可即时收起、隐藏,但不写回本项——刷新后仍回到这里设的默认 | 立即 |
|
|
162
162
|
| `floatMaxWidth` | 240–560 | `340` | **内置浮层宽度上限**(px):实际宽度 = `min(本项, 对话区右侧可用带宽)`,可用带宽不足 240px 时才会压入对话区 | 立即 |
|
|
163
|
+
| `dockRail` | 布尔 | `true` | **右侧栏打开时占满它**:答案卡铺满右栏标签条下方的内容区,读起来像栏内的一页,而不是 340px 的小浮层。收起为胶囊、关闭浮层、右栏宽度 < 280px 或高度 < 240px 时自动回到常规浮层;实现靠测量右栏 DOM 矩形,**没有**走 DSH 的右栏 API(见 §4.2 与 §6.1) | 立即 |
|
|
163
164
|
| `shortcut` | 形如 `Alt+Q`、`Ctrl+Shift+K` | `Alt+Q` | **快捷键**:唤起提问框(`trigger` 允许时) | 立即 |
|
|
164
165
|
| `captureZones` | `auto` \| `chat` \| `task` \| `chat+task` | `auto` | 只在哪些区域触发 | 立即 |
|
|
165
166
|
| `showInUnclassified` | 布尔 | `true` | 无法归类的区域是否也触发 | 立即 |
|
|
@@ -202,6 +203,35 @@ bleeds = 对话区右边界 - left > 1 // 只有带宽 < 240px 时才
|
|
|
202
203
|
- 挂载时、窗口 resize 时、右栏开合时(`ResizeObserver`)都会重算;流式刷新只重渲染,不动几何。
|
|
203
204
|
- 浮层根节点带 `data-layer-width`(实际宽度)与 `data-dsa-float-mode`:报告问题时可直接读这两个值。
|
|
204
205
|
|
|
206
|
+
### 4.2 右侧栏打开时「占满它」(`dockRail`,1.8.3)
|
|
207
|
+
|
|
208
|
+
340px 的浮层在右栏打开时会同时压住两块内容:主对话的最后几行、以及右栏自己的正文。1.8.3 起默认
|
|
209
|
+
(`dockRail: true`)把整张卡铺到右栏里,读起来像栏内的一页:
|
|
210
|
+
|
|
211
|
+
- 位置 = 右栏**标签条之下**的内容区矩形,宽度取右栏宽度,高度取到底部;右栏自己的标签条仍然露出、可点。
|
|
212
|
+
- 卡片在这个形态下没有圆角/描边/阴影(`.dsa-card` 的装饰被 `[data-dsa-float-mode="dock"]` 覆盖),只有一条左边线与右栏内容区分界。
|
|
213
|
+
- 标题栏(追问 + 徽标 + ⇥ / ×)与底部操作行固定上下两端,中间 `AnswerBody` 独立滚动——长答案不再把操作行推出视野。
|
|
214
|
+
- 收起为胶囊(⇥)、关闭(×)、或把 `dockRail` 关掉,都立即回到常规浮层;右栏宽度 < 280px 或高度 < 240px 时也回退(窄栏里铺满会把正文挤成每行 8–9 字)。
|
|
215
|
+
- 折叠的右栏不会被误用:`railOnScreen()` 会拒绝停在帧外(`left >= frameWidth - 1`)或尺寸不足的面板,量不到就回退,绝不把卡片画到帧外。
|
|
216
|
+
- 矩形从哪来:**优先量本插件自己渲染在右栏里的 `CardHost` 节点**(`[data-dsa-rail-host]`)——它所在的位置按定义就是「一页正文」,不依赖任何未被文档承诺的标记;找不到时再按 `data-sidebar-right-*`、dockkit 世代、`.rightbar`、`sidebar.right` 槽逐代回退。
|
|
217
|
+
- 什么时候重算:窗口改宽、右栏开合(只盯属性的 `MutationObserver`)、右栏自身改宽(`ResizeObserver`),以及挂载后的四次延时重测(罩住右栏的滑入动画)。流式刷新不会触发几何重算。
|
|
218
|
+
|
|
219
|
+
为什么是「测量」而不是「右栏内的一页」:DSH 的右栏服务确实存在(`sidebarRightTabs` / `sidebarRight`,
|
|
220
|
+
见 §6.1),本插件也有完整注册代码,但在当前构建里 `openTab` 路径实测仍回落到浮层,且没定位到抛错点。
|
|
221
|
+
在它修好之前,`dockRail` 是视觉上等价、失败即回退的兜底层——它盖住右栏内容区的其他面板(那些面板仍在
|
|
222
|
+
下面,取消勾选或收起胶囊即恢复),也会暂时压住右栏的拖拽改宽手柄。
|
|
223
|
+
|
|
224
|
+
诊断:`data-dsa-float-mode="dock"` 表示处于该形态;`.dsa-stack` 上的 `data-dsa-dock-body="<px>"` 是量到的
|
|
225
|
+
宽度(与卡片宽度一致)。浮层根节点还有 `data-dsa-dock`,它的取值直接说明**为什么**没有铺满:
|
|
226
|
+
|
|
227
|
+
| 值 | 含义 |
|
|
228
|
+
|---|---|
|
|
229
|
+
| `dock` | 正在铺满 |
|
|
230
|
+
| `no-rail` | 没找到已展开的右栏(右栏关着,或这个构建的标记与预期不同) |
|
|
231
|
+
| `rail-too-small` | 找到了,但宽 < 280px / 高 < 240px,或它停在帧外(折叠态被拒绝) |
|
|
232
|
+
| `off` | 设置里的 `dockRail` 被关掉了 |
|
|
233
|
+
| `mode` | 当前不是完整卡片形态(胶囊 / 隐藏),本来就不该铺满 |
|
|
234
|
+
|
|
205
235
|
---
|
|
206
236
|
|
|
207
237
|
## 五、侧边承载面与"侧边卡片插件"适配
|
|
@@ -210,15 +240,19 @@ bleeds = 对话区右边界 - left > 1 // 只有带宽 < 240px 时才
|
|
|
210
240
|
|
|
211
241
|
| 顺序 | 承载面 | 依赖 | 失败时 |
|
|
212
242
|
|---|---|---|---|
|
|
213
|
-
| 1 |
|
|
214
|
-
| 2 |
|
|
215
|
-
|
|
|
243
|
+
| 1 | **dsh-better-sidebar**(侧边卡片插件) | 客户端服务 `ctx.betterSidebar`:`registerTab` + `openTab(seed, scope)`;卡片 id 走 `tab.meta.cardId`,因此**要求其 `features` 含 `tabMeta`** | 记 `surfaces.better.error`(缺 `tabMeta` 时记 `no-tab-meta` 并跳过,而不是开一个空 tab),落到下一层 |
|
|
244
|
+
| 2 | **内置浮层卡片**(兜底) | 只需要 `shell.overlay` 槽位 | ——(这是保底面,永远可用) |
|
|
245
|
+
| — | DSH 原生右侧栏 | `ctx.sidebarRightTabs`(注册 tab 类型)+ `ctx.sidebarRight`(`openTab`),并占用槽位 `sidebar.right.pane.tab` / `…tab.title` | **不在 `auto` 链里**:只有显式 `sideSurface: 'native-rightbar'` 才用它;注册失败会回滚(见下) |
|
|
216
246
|
|
|
217
247
|
- 装了 `dsh-better-sidebar`:卡片以它的 tab 形式出现在它的面板里,关闭卡片会同时 `closeTab`,不残留空 tab。
|
|
218
|
-
|
|
248
|
+
这是 `auto` 的**第一选择**。
|
|
249
|
+
- 没装:自动使用内置浮层卡片,功能完全一致——**这条路径是第二选择,也是保底路径**。
|
|
219
250
|
浮层默认是**贴帧右边缘的胶囊**(一行高,只有状态与问题首句),点击才展开成完整卡片,
|
|
220
251
|
标题栏右侧的「收起 / 隐藏」可随时收回;它的宽度由 `floatPlacement` 计算,
|
|
221
252
|
只会占用**对话区右侧那条预留带**,不会压住正文(细节见 §4.1)。
|
|
253
|
+
- **原生右侧栏为什么退出 `auto`**:卡片被塞进右栏只是一个 tab,而这个 tab 的生命周期不归插件管——
|
|
254
|
+
卡片关闭后右栏仍留着一条读作「该卡片已关闭」的空标签,和你自己的右栏标签抢位置,
|
|
255
|
+
落点还取决于我们并不控制的宿主实现细节。它会保留成一个**强制选项**(下拉里标注「强制,不参与自动」)。
|
|
222
256
|
- 强制指定了一个不可用的承载面:回退到内置浮层,并在卡片上标注「已回退」。
|
|
223
257
|
|
|
224
258
|
### 5.1 与 dsh-better-sidebar 0.22.1 的适配核对(2026-09-28)
|
|
@@ -280,7 +314,7 @@ better-sidebar 会把收到的每个 tab 描述符**镜像注册进同一个原
|
|
|
280
314
|
| `sidecard-ask:card` | 本插件的**原生右侧栏**承载面(1.2.1 之前完全缺失) |
|
|
281
315
|
| `dsh-better-sidebar:sidecard-ask:card:workbench` | 经 dsh-better-sidebar 镜像注册的承载面(独立 kind) |
|
|
282
316
|
|
|
283
|
-
两者共存、均 `active: true` —— 即"同 kind 冲突"
|
|
317
|
+
两者共存、均 `active: true` —— 即"同 kind 冲突"已被消除。`auto` 走的是 **dsh-better-sidebar → 内置浮层卡片**这条链(1.8.3 起原生右侧栏只在显式 `sideSurface: native-rightbar` 时使用),两个强制选项都仍可用。
|
|
284
318
|
|
|
285
319
|
### 5.3 与 DSH 桌面版(Electron)的适配核对(2026-09-30)
|
|
286
320
|
|
|
@@ -426,7 +460,7 @@ node tools/compat-probe.mjs --json tools/.cache/matrix.json
|
|
|
426
460
|
|
|
427
461
|
| 缺口 | 适配实现 | 在真机/测试里的表现 |
|
|
428
462
|
|---|---|---|
|
|
429
|
-
| 0.1.2 / 0.1.3 没有原生右侧栏 | 承载面探测链:原生右侧栏 → `dsh-better-sidebar` → **内置浮层卡片**;不可用的一律不计入 `pickSurface` | `contract-test` 断言 `pickSurface('auto')` 在无插件组合下落到 `flow`;`
|
|
463
|
+
| 0.1.2 / 0.1.3 没有原生右侧栏 | 承载面探测链:原生右侧栏 → `dsh-better-sidebar` → **内置浮层卡片**;不可用的一律不计入 `pickSurface` | `contract-test` 断言 `pickSurface('auto')` 在无插件组合下落到 `flow`、两端可用时选 `better-sidebar`;`float-placement` 钉住自动链里不出现原生右侧栏 |
|
|
430
464
|
| ≤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
465
|
| ≤0.1.6-alpha.2 没有归档门 | 不再因"父会话已归档"直接拒绝:仍优先挑未归档代理,但只有归档候选时**照常尝试**,并在 `start` 事件里带 `parentArchived:true`;真被门拒绝时把空 turn 的 `refusal` 翻译成 `blocked-step` 并说明原因 | `smoke-test`:全归档组合仍能拿到答案;`refusal` 用例断言 `code=blocked-step` 且文案提到归档 |
|
|
432
466
|
| 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 +598,14 @@ node tools/compat-probe.mjs --json tools/.cache/matrix.json
|
|
|
564
598
|
**关键判据:胶囊/卡片的左边缘不得越过对话区(`[data-slot^="conversation"]`)的右边界**。
|
|
565
599
|
把窗口从 1920px 拖到 900px、再开关一次右侧栏,浮层应随之变窄而不是往左压住正文;
|
|
566
600
|
只有窗口窄到可用带宽不足 240px 时才允许压入(此时是设计内的降级,不是 bug)。
|
|
601
|
+
9. **占满右侧栏(1.8.3,`dockRail`)**:先把右侧栏打开(任意面板,例如「团队任务板」),再追问一次 →
|
|
602
|
+
答案卡应铺满**标签条下方**的整块内容区:右边缘贴帧边、下边缘贴窗口底,右栏的标签条仍然露出可点。
|
|
603
|
+
点卡片标题栏的 ⇥(收起为胶囊)或 × → 卡片立即退出占满、回到常规浮层,右栏内容区恢复。
|
|
604
|
+
自检:F12 读浮层根节点,`data-dsa-float-mode` 应为 `dock`,`data-dsa-dock` 也应为 `dock`;
|
|
605
|
+
`.dsa-stack` 上的 `data-dsa-dock-body` 应等于卡片宽度(px)。**没铺满就看 `data-dsa-dock`**:
|
|
606
|
+
`no-rail` = 没找到右栏,`rail-too-small` = 找到了但太窄或在帧外,`off` = 开关关了,`mode` = 不是完整形态。
|
|
607
|
+
把右栏拖到 260px 宽 → 应自动回退成常规浮层(低于 280px 不铺满);再拖回 400px → 应重新铺满。
|
|
608
|
+
设置页取消「右侧栏打开时占满它」并保存 → 同一操作不再铺满。
|
|
567
609
|
|
|
568
610
|
**失败信号**:卡片停在「作答中…」不动 = SSE 帧没到达(看 console 是否有 `/sidecard-ask/api/ask` 报错);`done.streaming=false` = 该版本没有流式帧(属预期降级,卡片会写明)。
|
|
569
611
|
|
|
@@ -604,10 +646,13 @@ node test/smoke-test.mjs # 端到端:流式/截断/取消/持久化/失
|
|
|
604
646
|
```
|
|
605
647
|
|
|
606
648
|
四个脚本都以 `process.exitCode` 反映结果,失败会列出具体条目;测试会把 `DSH_HOME` 指向临时目录,不会污染真实配置。
|
|
607
|
-
当前规模(1.8.
|
|
608
|
-
smoke **226** 项 = **
|
|
609
|
-
|
|
610
|
-
|
|
649
|
+
当前规模(1.8.3 本机实测,四项均 exit 0):verify **57** 项 + contract **989** 项 + float-placement **78** 项 +
|
|
650
|
+
smoke **226** 项 = **1350 项全部通过**(1.8.2 为 1293 项:contract 972 → 989,新增 8 项钉住设置页的「占满
|
|
651
|
+
右侧栏」复选框、3 个新 i18n 键,以及承载面 `auto` 的新顺序;float-placement 38 → 78,新增 40 项覆盖 `railDock` 的夹取/最小尺寸/
|
|
652
|
+
`clipped` 语义、真机上 dock 没生效那次发现的三个缺陷(残留的 `rail` 变量、右栏开合不触发重算、
|
|
653
|
+
右栏锚点必须优先用插件自己的 `[data-dsa-rail-host]` 节点),以及「自动链里不出现原生右侧栏」这条顺序断言)。更早的 1.8.1 为 1277 项,其中 contract
|
|
654
|
+
由 956 → 972 是提问框「作答位置」分段芯片的 16 项——唯一 `.dsa-seg`、两个按钮、恰好一个
|
|
655
|
+
`aria-pressed`、各带一枚内联图标、品牌实底规则与深色覆盖规则;点击切换本身不可测,原因见 1.8.2 的 CHANGELOG。
|
|
611
656
|
|
|
612
657
|
**真机探针(不属于四套自测)**:`.compat-report/scripts/composer-probe.mjs` 需要
|
|
613
658
|
`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.
|
|
50
|
+
const CLIENT_VERSION = '1.8.3'
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 }
|
|
@@ -1919,13 +2096,18 @@ window.__ModuleLoader__.load({
|
|
|
1919
2096
|
trigger drops its own fixed positioning, chips join the row directly. */
|
|
1920
2097
|
.dsa-trigger-embedded{position:static;box-shadow:0 2px 10px rgba(0,0,0,.18)}
|
|
1921
2098
|
.dsa-quickbar-chips{display:contents}
|
|
1922
|
-
/* Contrast pass
|
|
1923
|
-
|
|
1924
|
-
is why the card read as a
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
2099
|
+
/* Contrast pass on the popover frame. Measured against the card surface with the
|
|
2100
|
+
bundled skin's tokens (bg-base .55 over white → bg-overlay .98): the inner 1px
|
|
2101
|
+
edges drawn with border-l1 sit at ≈1.40:1, which is why the card read as a
|
|
2102
|
+
translucent sticker rather than a framed popover. The outer frame drawn with
|
|
2103
|
+
border-l2 was already ≈1.95:1 (light) / ≈2.88:1 (dark), so swapping it for mid
|
|
2104
|
+
grey is a *hue* choice, not a contrast gain: mid grey .55 measures ≈1.96:1
|
|
2105
|
+
light / ≈2.12:1 dark — neutral instead of blue-on-blue, and slightly weaker in
|
|
2106
|
+
dark. Explicit mid grey is still used over border-l2 because the host may
|
|
2107
|
+
resolve that alias to the surrounding fill; if a genuinely stronger frame is
|
|
2108
|
+
wanted, grey .85 measures ≈3.03:1. The two-layer shadow is what lifts the card
|
|
2109
|
+
off the page; secondary text inside the popover uses label-primary (6.11:1)
|
|
2110
|
+
rather than label-secondary. */
|
|
1929
2111
|
.dsa-pop{pointer-events:auto;position:fixed;width:min(420px, 92vw);display:flex;flex-direction:column;gap:8px;
|
|
1930
2112
|
padding:12px;border-radius:12px;border:1px solid rgba(127,127,127,.55);
|
|
1931
2113
|
background:var(--dsw-alias-bg-overlay, #fff);color:var(--dsw-alias-label-primary, #111);
|
|
@@ -1990,6 +2172,12 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"],
|
|
|
1990
2172
|
html[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover,
|
|
1991
2173
|
body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
1992
2174
|
color:#0a1428;text-shadow:0 1px 0 rgba(255,255,255,.25)}
|
|
2175
|
+
/* The same switch is reused on the settings page, inside a grid cell that can be
|
|
2176
|
+
narrower than the popover: tighten the chips there, never let the pair spill out
|
|
2177
|
+
of its cell, and keep the rail hugging the chips (dsa-field is a stretch
|
|
2178
|
+
column, which would otherwise paint a 400px-wide rail around two small chips). */
|
|
2179
|
+
.dsa-field .dsa-seg{max-width:100%;align-self:flex-start}
|
|
2180
|
+
.dsa-field .dsa-seg button{padding:4px 10px}
|
|
1993
2181
|
.dsa-card{pointer-events:auto;display:flex;flex-direction:column;gap:8px;padding:12px;border-radius:12px;
|
|
1994
2182
|
border:1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.35));
|
|
1995
2183
|
background:var(--dsw-alias-bg-overlay, #fff);color:var(--dsw-alias-label-primary, #111);
|
|
@@ -2001,6 +2189,19 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
2001
2189
|
.dsa-float{pointer-events:none;position:fixed;right:var(--dsa-gap-right, 8px);bottom:16px;
|
|
2002
2190
|
display:flex;flex-direction:column;gap:8px;width:var(--dsa-float-w, 340px);max-width:92vw}
|
|
2003
2191
|
.dsa-float > *{pointer-events:auto}
|
|
2192
|
+
/* Docked presentation (1.8.3): the same container, but pinned to the right
|
|
2193
|
+
rail's measured rectangle (inline left/top/width/height) instead of the
|
|
2194
|
+
frame's bottom-right corner. Right/bottom must be released, otherwise the
|
|
2195
|
+
base rule's right:8px/bottom:16px would over-constrain the box. The rail's
|
|
2196
|
+
resize handle is covered while docked — collapsing to the capsule (⇥) or
|
|
2197
|
+
closing gives it back. */
|
|
2198
|
+
.dsa-float[data-dsa-float-mode="dock"]{right:auto;bottom:auto;max-width:none;pointer-events:auto;
|
|
2199
|
+
border-left:1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.35));
|
|
2200
|
+
background:var(--dsw-alias-bg-overlay, #fff);overflow:hidden}
|
|
2201
|
+
.dsa-float[data-dsa-float-mode="dock"] .dsa-stack{max-height:none;height:100%;min-height:0;overflow:hidden}
|
|
2202
|
+
.dsa-float[data-dsa-float-mode="dock"] .dsa-card{height:100%;min-height:0;max-height:none;border:0;border-radius:0;
|
|
2203
|
+
box-shadow:none;background:transparent;padding:10px 12px 8px}
|
|
2204
|
+
.dsa-float[data-dsa-float-mode="dock"] .dsa-answer{max-height:none;flex:1 1 auto;min-height:0;overflow:auto}
|
|
2004
2205
|
.dsa-stack{display:flex;flex-direction:column;gap:8px;max-height:52vh;overflow:auto}
|
|
2005
2206
|
.dsa-capsule{display:flex;align-items:center;gap:8px;width:100%;box-sizing:border-box;text-align:left;
|
|
2006
2207
|
padding:8px 12px;border-radius:999px;cursor:pointer;font:inherit;font-size:12px;line-height:18px;
|
|
@@ -3818,11 +4019,14 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
3818
4019
|
*/
|
|
3819
4020
|
function FloatingLayer({ cards, actions }) {
|
|
3820
4021
|
const containerRef = React.useRef(null)
|
|
4022
|
+
const dockRef = React.useRef(null)
|
|
3821
4023
|
const state = useStoreState()
|
|
4024
|
+
const [dock, setDock] = React.useState(null)
|
|
3822
4025
|
const visible = cards.filter(card => card.surface === 'flow' && card.open)
|
|
3823
4026
|
const total = visible.length
|
|
3824
4027
|
const mode = state.floatMode ?? state.config.floatMode ?? CLIENT_DEFAULTS.floatMode
|
|
3825
4028
|
const maxWidth = state.config.floatMaxWidth ?? CLIENT_DEFAULTS.floatMaxWidth
|
|
4029
|
+
const dockWanted = state.config.dockRail !== false
|
|
3826
4030
|
|
|
3827
4031
|
// Measure the conversation column + right rail, then pin the layer's
|
|
3828
4032
|
// width to what is left. Recomputed on frame resize and on rail toggles
|
|
@@ -3833,6 +4037,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
3833
4037
|
const node = containerRef.current
|
|
3834
4038
|
if (node === null || node === undefined) return undefined
|
|
3835
4039
|
const selector = '[data-slot^="conversation"], .rightbar, [data-slot^="sidebar.right"], [data-dsh-panel-host]'
|
|
4040
|
+
const observed = new Set()
|
|
3836
4041
|
const all = () => {
|
|
3837
4042
|
try {
|
|
3838
4043
|
return typeof document.querySelectorAll === 'function'
|
|
@@ -3844,20 +4049,73 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
3844
4049
|
try { return typeof document.querySelector === 'function' ? document.querySelector(query) : null } catch { return null }
|
|
3845
4050
|
}
|
|
3846
4051
|
let frame = 0
|
|
4052
|
+
let observer = null
|
|
4053
|
+
let mutation = null
|
|
4054
|
+
let observedRail = null
|
|
4055
|
+
const timers = []
|
|
3847
4056
|
const apply = () => {
|
|
3848
4057
|
if (typeof node.style?.setProperty !== 'function') return
|
|
3849
4058
|
const lane = one('[data-slot^="conversation"]')
|
|
3850
|
-
const rail = one('.rightbar, [data-slot^="sidebar.right"], [data-dsh-panel-host]')
|
|
3851
4059
|
const rootWidth = document.documentElement?.clientWidth ?? window.innerWidth
|
|
3852
|
-
const
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
4060
|
+
const rootHeight = document.documentElement?.clientHeight ?? window.innerHeight
|
|
4061
|
+
const measure = target => {
|
|
4062
|
+
if (target === null || target === undefined) return null
|
|
4063
|
+
try {
|
|
4064
|
+
const rect = typeof target.getBoundingClientRect === 'function'
|
|
4065
|
+
? target.getBoundingClientRect()
|
|
4066
|
+
: null
|
|
4067
|
+
return rect === null ? null : rect
|
|
4068
|
+
} catch { return null }
|
|
4069
|
+
}
|
|
4070
|
+
const laneRect = measure(lane)
|
|
4071
|
+
const rail = findRail(document)
|
|
4072
|
+
const railBox = rail === null ? null : rail.box
|
|
4073
|
+
const laneRight = laneRect === null ? rootWidth : laneRect.right
|
|
4074
|
+
const railLeft = railBox === null ? null : railBox.left
|
|
3856
4075
|
const placement = floatPlacement({ frameWidth: rootWidth, laneRight, railLeft, maxWidth })
|
|
3857
4076
|
node.style.setProperty('--dsa-float-w', `${placement.width}px`)
|
|
3858
4077
|
node.style.setProperty('--dsa-gap-right', `${FLOAT_GAP_RIGHT}px`)
|
|
3859
4078
|
// Readable placement, so a user report can say WHERE the layer sat.
|
|
3860
4079
|
node.dataset.layerWidth = String(placement.width)
|
|
4080
|
+
|
|
4081
|
+
// Docked presentation: the card fills the rail page's body instead of
|
|
4082
|
+
// floating over it. Only measured in `full` mode — a capsule/launcher
|
|
4083
|
+
// is deliberately small and must never take the rail over.
|
|
4084
|
+
const next = railBox === null || mode !== 'full'
|
|
4085
|
+
? null
|
|
4086
|
+
: railDock({
|
|
4087
|
+
railLeft: railBox.left,
|
|
4088
|
+
railTop: railBox.top,
|
|
4089
|
+
railWidth: railBox.width,
|
|
4090
|
+
railHeight: railBox.height,
|
|
4091
|
+
frameWidth: rootWidth,
|
|
4092
|
+
frameHeight: rootHeight,
|
|
4093
|
+
enabled: dockWanted,
|
|
4094
|
+
})
|
|
4095
|
+
const prev = dockRef.current
|
|
4096
|
+
const same = prev === next || (prev !== null && next !== null
|
|
4097
|
+
&& prev.left === next.left && prev.top === next.top
|
|
4098
|
+
&& prev.width === next.width && prev.height === next.height
|
|
4099
|
+
&& prev.clipped === next.clipped)
|
|
4100
|
+
if (!same) {
|
|
4101
|
+
dockRef.current = next
|
|
4102
|
+
setDock(next)
|
|
4103
|
+
}
|
|
4104
|
+
// Why the layer is (not) docked, readable from DevTools. A rail that
|
|
4105
|
+
// exists but is too narrow reads as `rail-too-small`, which is a
|
|
4106
|
+
// different story from `no-rail` and is what a bug report needs.
|
|
4107
|
+
node.dataset.dsaDock = next !== null
|
|
4108
|
+
? 'dock'
|
|
4109
|
+
: (mode !== 'full'
|
|
4110
|
+
? 'mode'
|
|
4111
|
+
: (dockWanted ? (railBox === null ? 'no-rail' : 'rail-too-small') : 'off'))
|
|
4112
|
+
// The rail can appear AFTER this effect ran (opening it mounts new
|
|
4113
|
+
// nodes), and it can be re-created later: keep the observer pointed at
|
|
4114
|
+
// whichever node is currently accepted.
|
|
4115
|
+
if (observer !== null && rail !== null && rail.node !== observedRail) {
|
|
4116
|
+
observedRail = rail.node
|
|
4117
|
+
try { observer.observe(rail.node) } catch { /* not observable */ }
|
|
4118
|
+
}
|
|
3861
4119
|
}
|
|
3862
4120
|
const schedule = () => {
|
|
3863
4121
|
if (typeof window.requestAnimationFrame !== 'function') { apply(); return }
|
|
@@ -3868,7 +4126,6 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
3868
4126
|
})
|
|
3869
4127
|
}
|
|
3870
4128
|
apply()
|
|
3871
|
-
let observer = null
|
|
3872
4129
|
if (typeof window.ResizeObserver === 'function') {
|
|
3873
4130
|
observer = new window.ResizeObserver(schedule)
|
|
3874
4131
|
for (const target of all()) {
|
|
@@ -3876,12 +4133,35 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
3876
4133
|
}
|
|
3877
4134
|
}
|
|
3878
4135
|
if (typeof window.addEventListener === 'function') window.addEventListener('resize', schedule)
|
|
4136
|
+
// Opening/closing the rail changes no size this effect already watches,
|
|
4137
|
+
// so the DOM itself has to be the trigger. Attributes only (no
|
|
4138
|
+
// childList): streaming tokens rewrite text nodes continuously and must
|
|
4139
|
+
// not wake the layer on every frame.
|
|
4140
|
+
if (typeof window.MutationObserver === 'function' && document.body !== null && document.body !== undefined) {
|
|
4141
|
+
try {
|
|
4142
|
+
mutation = new window.MutationObserver(schedule)
|
|
4143
|
+
mutation.observe(document.body, {
|
|
4144
|
+
attributes: true,
|
|
4145
|
+
subtree: true,
|
|
4146
|
+
attributeFilter: ['data-sidebar-right-open', 'data-dockkit-pane-active', 'style', 'class'],
|
|
4147
|
+
})
|
|
4148
|
+
} catch { mutation = null }
|
|
4149
|
+
}
|
|
4150
|
+
// The rail slides in; a rect taken mid-animation is wrong. Re-measure a
|
|
4151
|
+
// few times right after a card/mode change, then stop for good.
|
|
4152
|
+
if (typeof window.setTimeout === 'function') {
|
|
4153
|
+
for (const delay of [80, 200, 420, 800]) timers.push(window.setTimeout(schedule, delay))
|
|
4154
|
+
}
|
|
3879
4155
|
return () => {
|
|
3880
4156
|
if (typeof window.removeEventListener === 'function') window.removeEventListener('resize', schedule)
|
|
3881
4157
|
if (observer !== null) { try { observer.disconnect() } catch { /* already gone */ } }
|
|
4158
|
+
if (mutation !== null) { try { mutation.disconnect() } catch { /* already gone */ } }
|
|
4159
|
+
for (const timer of timers) {
|
|
4160
|
+
if (typeof window.clearTimeout === 'function') window.clearTimeout(timer)
|
|
4161
|
+
}
|
|
3882
4162
|
if (frame !== 0 && typeof window.cancelAnimationFrame === 'function') window.cancelAnimationFrame(frame)
|
|
3883
4163
|
}
|
|
3884
|
-
}, [maxWidth, total, mode])
|
|
4164
|
+
}, [maxWidth, total, mode, dockWanted])
|
|
3885
4165
|
|
|
3886
4166
|
if (total === 0) return null
|
|
3887
4167
|
|
|
@@ -3894,13 +4174,47 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
3894
4174
|
: t('floatDone', { n: total })
|
|
3895
4175
|
const firstQuestion = visible[0].question
|
|
3896
4176
|
|
|
4177
|
+
// Docked geometry is only meaningful in `full` mode; the effect nulls it
|
|
4178
|
+
// out otherwise, and this guard keeps a stale rect from a previous mode
|
|
4179
|
+
// from being painted for one frame.
|
|
4180
|
+
const docked = mode === 'full' && dock !== null
|
|
4181
|
+
|
|
3897
4182
|
const container = children => h('div', {
|
|
3898
|
-
className: 'dsa-float',
|
|
4183
|
+
className: docked ? 'dsa-float dsa-dock' : 'dsa-float',
|
|
3899
4184
|
'data-dsa-root': '',
|
|
3900
4185
|
ref: containerRef,
|
|
3901
|
-
'data-dsa-float-mode': mode,
|
|
4186
|
+
'data-dsa-float-mode': docked ? 'dock' : mode,
|
|
4187
|
+
...(docked
|
|
4188
|
+
? {
|
|
4189
|
+
style: {
|
|
4190
|
+
left: `${dock.left}px`,
|
|
4191
|
+
top: `${dock.top}px`,
|
|
4192
|
+
width: `${dock.width}px`,
|
|
4193
|
+
height: `${dock.height}px`,
|
|
4194
|
+
},
|
|
4195
|
+
}
|
|
4196
|
+
: null),
|
|
3902
4197
|
}, children)
|
|
3903
4198
|
|
|
4199
|
+
const cardNodes = visible.map(card => h(AnswerCard, {
|
|
4200
|
+
key: card.id,
|
|
4201
|
+
card,
|
|
4202
|
+
surfaceLabel: null,
|
|
4203
|
+
onCopy: element => actions.copy(card, element),
|
|
4204
|
+
onFollowUp: text => actions.followUp(card, text),
|
|
4205
|
+
onRetry: () => actions.retry(card),
|
|
4206
|
+
onRetryEdit: text => actions.retryEdit(card, text),
|
|
4207
|
+
onCancel: () => actions.cancel(card),
|
|
4208
|
+
onSendToMain: () => actions.sendToMain(card),
|
|
4209
|
+
onClose: () => actions.close(card),
|
|
4210
|
+
onLocate: target => actions.locateCard(target ?? card),
|
|
4211
|
+
onExport: target => actions.exportCard(target ?? card),
|
|
4212
|
+
onInsert: target => actions.insertAnswer(target ?? card),
|
|
4213
|
+
onReplace: target => actions.insertAnswer(target ?? card, { replace: true }),
|
|
4214
|
+
onViewVersion: index => actions.viewVersion(card, index),
|
|
4215
|
+
onFloatMode: setFloatMode,
|
|
4216
|
+
}))
|
|
4217
|
+
|
|
3904
4218
|
if (mode === 'off') {
|
|
3905
4219
|
return container(h('button', {
|
|
3906
4220
|
type: 'button',
|
|
@@ -3921,25 +4235,14 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
3921
4235
|
h('span', { className: 'dsa-capsule-chev' }, '›')))
|
|
3922
4236
|
}
|
|
3923
4237
|
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
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
|
-
}))))
|
|
4238
|
+
// Docked: the card takes the rail's whole rectangle, so it reads as one of
|
|
4239
|
+
// the rail's pages rather than a 340px card hovering over it. The card
|
|
4240
|
+
// keeps its own header (title + badges + collapse/hide controls), which is
|
|
4241
|
+
// what makes the page dismissible without leaving the rail.
|
|
4242
|
+
return container(h('div', {
|
|
4243
|
+
className: 'dsa-stack',
|
|
4244
|
+
'data-dsa-dock-body': docked ? String(dock.width) : undefined,
|
|
4245
|
+
}, cardNodes))
|
|
3943
4246
|
}
|
|
3944
4247
|
|
|
3945
4248
|
/** The `shell.overlay` entry: styles + trigger + popover + cards + toast. */
|
|
@@ -4016,7 +4319,10 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
4016
4319
|
else toast(t('notifyGranted'))
|
|
4017
4320
|
}
|
|
4018
4321
|
|
|
4019
|
-
|
|
4322
|
+
// `style` is optional: the two-chip carrier switch needs two grid tracks
|
|
4323
|
+
// (`.dsa-grid` fits columns down to 150px, and two nowrap chips need
|
|
4324
|
+
// ~175px), so that field spans two tracks instead of overflowing.
|
|
4325
|
+
const field = (label, control, hint, style) => h('div', { className: 'dsa-field', style },
|
|
4020
4326
|
h('label', null, label),
|
|
4021
4327
|
control,
|
|
4022
4328
|
hint !== undefined ? h('span', { className: 'dsa-muted' }, hint) : null)
|
|
@@ -4247,6 +4553,8 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
4247
4553
|
? h('div', { className: 'dsa-family-notice', role: 'status' }, t('familyLegacyToast'))
|
|
4248
4554
|
: null)
|
|
4249
4555
|
|
|
4556
|
+
// NOT marked `data-dsa-rail-host`: the settings page is not a rail page,
|
|
4557
|
+
// and marking it would let the floating layer dock over the settings panel.
|
|
4250
4558
|
return h('div', { className: 'dsa-settings', 'data-dsa-root': '' },
|
|
4251
4559
|
h(Styles, null),
|
|
4252
4560
|
h('div', null,
|
|
@@ -4259,15 +4567,15 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
4259
4567
|
['shortcut', t('setTriggerShortcut')],
|
|
4260
4568
|
['both', t('setTriggerBoth')],
|
|
4261
4569
|
])),
|
|
4262
|
-
field(t('setCarrier'),
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4570
|
+
field(t('setCarrier'), h(CarrierSeg, {
|
|
4571
|
+
value: draft.defaultCarrier,
|
|
4572
|
+
onChange: id => setDraft({ ...draft, defaultCarrier: id }),
|
|
4573
|
+
}), undefined, { gridColumn: 'span 2' }),
|
|
4266
4574
|
field(t('setSurface'), select('sideSurface', [
|
|
4267
4575
|
['auto', t('setSurfaceAuto')],
|
|
4268
|
-
['flow', t('setSurfaceFlow')],
|
|
4269
|
-
['native-rightbar', t('surfaceNative')],
|
|
4270
4576
|
['better-sidebar', t('surfaceBetter')],
|
|
4577
|
+
['flow', t('setSurfaceFlow')],
|
|
4578
|
+
['native-rightbar', t('surfaceNativeForce')],
|
|
4271
4579
|
])),
|
|
4272
4580
|
field(t('setZones'), select('captureZones', [
|
|
4273
4581
|
['auto', t('setZonesAuto')],
|
|
@@ -4347,6 +4655,14 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
4347
4655
|
}),
|
|
4348
4656
|
t('setFollowUps')),
|
|
4349
4657
|
h('span', { className: 'dsa-muted' }, t('setFollowUpsHint')),
|
|
4658
|
+
h('label', { className: 'dsa-check' },
|
|
4659
|
+
h('input', {
|
|
4660
|
+
type: 'checkbox',
|
|
4661
|
+
checked: draft.dockRail !== false,
|
|
4662
|
+
onChange: event => setDraft({ ...draft, dockRail: event.target.checked }),
|
|
4663
|
+
}),
|
|
4664
|
+
t('setDockRail')),
|
|
4665
|
+
h('span', { className: 'dsa-muted' }, t('setDockRailHint')),
|
|
4350
4666
|
historySection,
|
|
4351
4667
|
h('div', { className: 'dsa-row' },
|
|
4352
4668
|
h('button', {
|
|
@@ -5774,7 +6090,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
5774
6090
|
}
|
|
5775
6091
|
}, [cardId])
|
|
5776
6092
|
if (card === undefined) {
|
|
5777
|
-
return h('div', { className: 'dsa-settings', 'data-dsa-root': '' },
|
|
6093
|
+
return h('div', { className: 'dsa-settings', 'data-dsa-root': '', 'data-dsa-rail-host': '' },
|
|
5778
6094
|
h(Styles, null),
|
|
5779
6095
|
h('div', { className: 'dsa-muted' }, t('cardClosed')))
|
|
5780
6096
|
}
|
|
@@ -5785,11 +6101,11 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
5785
6101
|
// note for a rail that keeps it.
|
|
5786
6102
|
try { surfaces.native.close(card) } catch { /* the rail owns its lifecycle */ }
|
|
5787
6103
|
try { surfaces.better.close(card) } catch { /* the host may already be gone */ }
|
|
5788
|
-
return h('div', { className: 'dsa-settings', 'data-dsa-root': '' },
|
|
6104
|
+
return h('div', { className: 'dsa-settings', 'data-dsa-root': '', 'data-dsa-rail-host': '' },
|
|
5789
6105
|
h(Styles, null),
|
|
5790
6106
|
h('div', { className: 'dsa-muted' }, t('cardMovedToFlow')))
|
|
5791
6107
|
}
|
|
5792
|
-
return h('div', { className: 'dsa-settings', 'data-dsa-root': '', style: { padding: '10px' } },
|
|
6108
|
+
return h('div', { className: 'dsa-settings', 'data-dsa-root': '', 'data-dsa-rail-host': '', style: { padding: '10px' } },
|
|
5793
6109
|
h(Styles, null),
|
|
5794
6110
|
h(AnswerCard, {
|
|
5795
6111
|
card,
|
|
@@ -5967,6 +6283,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
5967
6283
|
renderRichText,
|
|
5968
6284
|
pickSurface,
|
|
5969
6285
|
floatPlacement,
|
|
6286
|
+
railDock,
|
|
5970
6287
|
compareVersions,
|
|
5971
6288
|
electFamilyLeader,
|
|
5972
6289
|
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.
|
|
45
|
+
export const PLUGIN_VERSION = '1.8.3'
|
|
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}: 必须是布尔值,已忽略`)
|