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 +118 -2
- package/README.md +82 -11
- package/client.js +410 -58
- package/index.js +13 -2
- package/package.json +1 -1
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
|
-
-
|
|
21
|
-
改显式中灰 `rgba(127,127,127,.55)
|
|
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` |
|
|
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 |
|
|
214
|
-
| 2 |
|
|
215
|
-
|
|
|
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 冲突"
|
|
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`;`
|
|
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
|
-
|
|
608
|
-
smoke **226** 项 = **
|
|
609
|
-
|
|
610
|
-
|
|
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.
|
|
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
|
-
|
|
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 }
|
|
@@ -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
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
.
|
|
1883
|
-
|
|
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
|
|
1923
|
-
|
|
1924
|
-
is why the card read as a
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
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
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
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
|
-
|
|
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
|
-
}))))
|
|
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
|
-
|
|
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'),
|
|
4263
|
-
|
|
4264
|
-
|
|
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.
|
|
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}: 必须是布尔值,已忽略`)
|