@yfwu2020/dsh-selection-explain 0.10.2 → 0.10.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/README.md +7 -3
- package/lib/client.js +8 -2
- package/package.json +1 -1
- package/scripts/smoke-panel.mjs +66 -0
- package/scripts/test-escape-focus.mjs +39 -4
- package/src/client/index.js +8 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ DSH 中的翻译、上下文解读与语音追问。
|
|
|
8
8
|
|
|
9
9
|
[快速安装](#install) · [语音输入](#voice) · [功能一览](#read) · [使用与设置](#settings)
|
|
10
10
|
|
|
11
|
-
`DSH 插件` · `v0.10.
|
|
11
|
+
`DSH 插件` · `v0.10.3` · `MIT`
|
|
12
12
|
|
|
13
13
|
</div>
|
|
14
14
|
|
|
@@ -103,11 +103,11 @@ dsh plugin --profile web add @yfwu2020/dsh-selection-explain
|
|
|
103
103
|
<details>
|
|
104
104
|
<summary>安装 Release 构建包</summary>
|
|
105
105
|
|
|
106
|
-
将下载的 `.tgz` 放到当前目录,以 v0.10.
|
|
106
|
+
将下载的 `.tgz` 放到当前目录,以 v0.10.3 为例:
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
109
|
mkdir -p ~/.dsh/plugins/dsh-selection-explain
|
|
110
|
-
tar -xzf yfwu2020-dsh-selection-explain-0.10.
|
|
110
|
+
tar -xzf yfwu2020-dsh-selection-explain-0.10.3.tgz -C ~/.dsh/plugins/dsh-selection-explain --strip-components=1
|
|
111
111
|
dsh plugin --profile web add ~/.dsh/plugins/dsh-selection-explain
|
|
112
112
|
```
|
|
113
113
|
|
|
@@ -159,6 +159,10 @@ dsh plugin --profile web remove @yfwu2020/dsh-selection-explain
|
|
|
159
159
|
|
|
160
160
|
聊天与解读可以同时打开。与 `dsh-full-view` **0.1.6**、`dsh-recent-sessions` **0.2.3** 配合时,`Esc` 先退出最后操作的小窗;点击两窗之外后先退解读,再按一次收起聊天。此次调整需一起更新两个插件,未改变 Harness 的公共浮层或设置。
|
|
161
161
|
|
|
162
|
+
### 快捷键与 Esc 层级(0.10.3)
|
|
163
|
+
|
|
164
|
+
模型菜单收起后不再被 DSH 识别为弹层,避免设置等快捷键失效。官方设置等模态浮层打开时,`Esc` 优先交给官方浮层处理,保留解读小窗、插件设置抽屉、模型菜单和语音卡;官方浮层关闭后,再按 `Esc` 继续退出插件层级。
|
|
165
|
+
|
|
162
166
|
## 数据与使用范围
|
|
163
167
|
|
|
164
168
|
选中文字、必要的前后文,以及开启的会话背景会交给你配置的模型。可在设置中关闭会话背景或工具调用。
|
package/lib/client.js
CHANGED
|
@@ -2648,7 +2648,7 @@ window.__ModuleLoader__.load({
|
|
|
2648
2648
|
modelPill.appendChild(modelPillTier)
|
|
2649
2649
|
modelPill.appendChild(el('span', 'dsh-sel-picker-caret', '▼'))
|
|
2650
2650
|
var modelMenu = el('div', 'dsh-sel-pickermenu')
|
|
2651
|
-
|
|
2651
|
+
// role 随开合同步:DSH 按 [role="menu"] 判定弹层,不检查 CSS 可见性。
|
|
2652
2652
|
|
|
2653
2653
|
// ── 录音行(与主会话 ui-voice-input 同一套结构)──
|
|
2654
2654
|
// [✕ 取消] [实时波形 / 状态文案] [■ 停止 或 行内动作] …… [↑ 发送(原位不动)]
|
|
@@ -9484,6 +9484,7 @@ window.__ModuleLoader__.load({
|
|
|
9484
9484
|
*/
|
|
9485
9485
|
var offVoiceEscape = listen(document, 'keydown', function (event) {
|
|
9486
9486
|
if (event.key !== 'Escape' || !vcardState.open || event.defaultPrevented) return
|
|
9487
|
+
if (document.querySelector('[role="dialog"][aria-modal="true"]')) return
|
|
9487
9488
|
event.preventDefault()
|
|
9488
9489
|
event.stopPropagation()
|
|
9489
9490
|
vcardState.suppress = true
|
|
@@ -9936,11 +9937,13 @@ window.__ModuleLoader__.load({
|
|
|
9936
9937
|
|
|
9937
9938
|
function closeModelMenu() {
|
|
9938
9939
|
modelMenu.setAttribute('data-open', '0')
|
|
9940
|
+
modelMenu.removeAttribute('role')
|
|
9939
9941
|
modelPill.setAttribute('aria-expanded', 'false')
|
|
9940
9942
|
}
|
|
9941
9943
|
|
|
9942
9944
|
function openModelMenu() {
|
|
9943
9945
|
modelMenu.setAttribute('data-open', '1')
|
|
9946
|
+
modelMenu.setAttribute('role', 'menu')
|
|
9944
9947
|
modelPill.setAttribute('aria-expanded', 'true')
|
|
9945
9948
|
renderModelMenu()
|
|
9946
9949
|
loadModelCatalog(false).then(function () {
|
|
@@ -9962,7 +9965,7 @@ window.__ModuleLoader__.load({
|
|
|
9962
9965
|
event.stopPropagation() // 菜单内部点击不要触发"点到外面"
|
|
9963
9966
|
})
|
|
9964
9967
|
var offModelKeys = listen(modelMenu, 'keydown', function (event) {
|
|
9965
|
-
if (event.key === 'Escape') closeModelMenu()
|
|
9968
|
+
if (event.key === 'Escape' && !document.querySelector('[role="dialog"][aria-modal="true"]')) closeModelMenu()
|
|
9966
9969
|
})
|
|
9967
9970
|
|
|
9968
9971
|
// —— 网页模式开关 ——
|
|
@@ -10140,6 +10143,9 @@ window.__ModuleLoader__.load({
|
|
|
10140
10143
|
|
|
10141
10144
|
var offKeyDown = listen(document, 'keydown', function (event) {
|
|
10142
10145
|
if (event.key !== 'Escape' || !panelOpen || event.defaultPrevented || event.isComposing || event.keyCode === 229) return
|
|
10146
|
+
// 官方模态浮层的 Esc 在冒泡阶段处理;capture 不消费,先让官方设置等浮层退出。
|
|
10147
|
+
// 插件面板和设置抽屉是非模态 dialog,不带 aria-modal,不会命中这里。
|
|
10148
|
+
if (document.querySelector('[role="dialog"][aria-modal="true"]')) return
|
|
10143
10149
|
// 完整视图记录最后点击/操作的小窗;不能只看可能停在另一窗的键盘焦点。
|
|
10144
10150
|
var fullViewFrame = document.querySelector('[data-dsh-full-view]')
|
|
10145
10151
|
if (fullViewFrame) {
|
package/package.json
CHANGED
package/scripts/smoke-panel.mjs
CHANGED
|
@@ -1504,8 +1504,57 @@ async function main() {
|
|
|
1504
1504
|
hook.open('模型菜单勾选', '', '菜单冒烟')
|
|
1505
1505
|
await waitFor(function () { return hook.state().phase === 'done' }, 8000)
|
|
1506
1506
|
await hook.loadModels(true)
|
|
1507
|
+
var modelNodes = hook.modelNodes()
|
|
1508
|
+
// DSH 按属性判定弹层,不检查 CSS 可见性(Issue #1)。
|
|
1509
|
+
var modalSelector = '[role="dialog"][aria-modal="true"], [role="menu"]'
|
|
1510
|
+
function modelMenuOwnsShortcuts() {
|
|
1511
|
+
return Array.from(document.querySelectorAll(modalSelector)).includes(modelNodes.menu)
|
|
1512
|
+
}
|
|
1513
|
+
function checkModelMenuClosed(reason) {
|
|
1514
|
+
check(reason + ':模型菜单不再占用 DSH 快捷键',
|
|
1515
|
+
!modelMenuOwnsShortcuts() && modelNodes.menu.getAttribute('data-open') !== '1' &&
|
|
1516
|
+
modelNodes.pill.getAttribute('aria-expanded') === 'false')
|
|
1517
|
+
}
|
|
1518
|
+
checkModelMenuClosed('初始收起')
|
|
1519
|
+
// 官方 useModalLayer 在 document 冒泡阶段处理 Esc;插件 capture 必须让行。
|
|
1520
|
+
function checkHostEscapeFirst(target, label) {
|
|
1521
|
+
var hostSettings = document.createElement('div')
|
|
1522
|
+
hostSettings.setAttribute('role', 'dialog')
|
|
1523
|
+
hostSettings.setAttribute('aria-modal', 'true')
|
|
1524
|
+
hostSettings.setAttribute('data-shortcut-modal', 'settings')
|
|
1525
|
+
var hostInput = document.createElement('input')
|
|
1526
|
+
hostSettings.appendChild(hostInput)
|
|
1527
|
+
document.body.appendChild(hostSettings)
|
|
1528
|
+
function dismissHost(event) {
|
|
1529
|
+
if (event.key !== 'Escape' || event.defaultPrevented) return
|
|
1530
|
+
event.preventDefault()
|
|
1531
|
+
hostSettings.remove()
|
|
1532
|
+
}
|
|
1533
|
+
document.addEventListener('keydown', dismissHost)
|
|
1534
|
+
try {
|
|
1535
|
+
var event = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })
|
|
1536
|
+
;(target || hostInput).dispatchEvent(event)
|
|
1537
|
+
check(label + ':Esc 到达官方浮层并只关闭官方设置',
|
|
1538
|
+
event.defaultPrevented && !hostSettings.isConnected && panel.style.display === 'flex')
|
|
1539
|
+
} finally {
|
|
1540
|
+
document.removeEventListener('keydown', dismissHost)
|
|
1541
|
+
hostSettings.remove()
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
var escapeModeFrame = document.createElement('div')
|
|
1545
|
+
escapeModeFrame.setAttribute('data-dsh-full-view', '')
|
|
1546
|
+
escapeModeFrame.setAttribute('data-dsh-active-floating-window', 'explanation')
|
|
1547
|
+
for (var fullViewMode of [false, true]) {
|
|
1548
|
+
if (fullViewMode) document.body.appendChild(escapeModeFrame)
|
|
1549
|
+
var modeLabel = fullViewMode ? '完整视图' : '普通分栏'
|
|
1550
|
+
checkHostEscapeFirst(null, modeLabel + '/焦点在官方设置')
|
|
1551
|
+
checkHostEscapeFirst(document.querySelector('.dsh-sel-askbox'), modeLabel + '/焦点仍在解读输入框')
|
|
1552
|
+
}
|
|
1553
|
+
escapeModeFrame.remove()
|
|
1507
1554
|
hook.openModel()
|
|
1508
1555
|
await sleep(120)
|
|
1556
|
+
check('模型菜单展开时占用 DSH 快捷键',
|
|
1557
|
+
modelMenuOwnsShortcuts() && modelNodes.pill.getAttribute('aria-expanded') === 'true')
|
|
1509
1558
|
var menu = hook.modelNodes().menu
|
|
1510
1559
|
var rows = Array.prototype.slice.call(menu.querySelectorAll('.dsh-sel-pickerrow'))
|
|
1511
1560
|
check('模型菜单列出 3 个模型(含同名 id 的另一 provider)', rows.length === 3, String(rows.length))
|
|
@@ -1529,7 +1578,24 @@ async function main() {
|
|
|
1529
1578
|
var rowBox = onRows[0] ? onRows[0].getBoundingClientRect() : null
|
|
1530
1579
|
check('打勾那一行仍是单行(高度不超过 30px,没折成两行)',
|
|
1531
1580
|
!!rowBox && rowBox.height <= 30, rowBox ? Math.round(rowBox.height) + 'px' : '没找到那一行')
|
|
1581
|
+
|
|
1582
|
+
checkHostEscapeFirst(modelNodes.menu, '模型菜单仍展开')
|
|
1583
|
+
check('官方设置关闭时保留插件模型菜单', modelNodes.menu.getAttribute('data-open') === '1')
|
|
1584
|
+
|
|
1585
|
+
clickOn(modelNodes.pill)
|
|
1586
|
+
checkModelMenuClosed('再次点击胶囊')
|
|
1587
|
+
hook.openModel()
|
|
1588
|
+
modelNodes.menu.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
1589
|
+
checkModelMenuClosed('按 Esc')
|
|
1590
|
+
hook.openModel()
|
|
1591
|
+
document.getElementById('docp').dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
|
|
1592
|
+
checkModelMenuClosed('点击菜单外部')
|
|
1593
|
+
hook.openModel()
|
|
1594
|
+
clickOn(modelNodes.menu.querySelector('.dsh-sel-pickerrow'))
|
|
1595
|
+
checkModelMenuClosed('选择模型')
|
|
1596
|
+
hook.openModel()
|
|
1532
1597
|
hook.close() // 关窗会顺手收起模型菜单
|
|
1598
|
+
checkModelMenuClosed('关闭解读面板')
|
|
1533
1599
|
await sleep(30)
|
|
1534
1600
|
}
|
|
1535
1601
|
|
|
@@ -7,12 +7,14 @@ const start = source.indexOf(' var offKeyDown = listen(document,')
|
|
|
7
7
|
const end = source.indexOf('\n\n var offSettingsOpen', start)
|
|
8
8
|
assert.ok(start >= 0 && end > start)
|
|
9
9
|
const ask = {}, historyInput = {}, outside = {}, expandedChat = { closest: selector => selector.startsWith('[data-dsh-floating-chat]') ? {} : null }, compactChat = { closest: () => null }, nativeMenu = { closest: selector => selector.startsWith('[role=') ? {} : null }
|
|
10
|
-
let handler, fullView = true, calls = [], activeWindow = null, chatExpanded = true
|
|
10
|
+
let handler, fullView = true, calls = [], activeWindow = null, chatExpanded = true, hostModalOpen = false
|
|
11
11
|
const context = vm.createContext({
|
|
12
12
|
panelOpen: true, settingsOpen: false,
|
|
13
13
|
panel: { contains: node => node === ask },
|
|
14
14
|
historyList: { contains: node => node === historyInput },
|
|
15
|
-
document: { querySelector: selector => selector === '[
|
|
15
|
+
document: { querySelector: selector => selector === '[role="dialog"][aria-modal="true"]'
|
|
16
|
+
? hostModalOpen ? {} : null
|
|
17
|
+
: selector === '[data-dsh-full-view]' && fullView
|
|
16
18
|
? { getAttribute: () => activeWindow }
|
|
17
19
|
: selector.startsWith('[data-dsh-floating-chat]') && chatExpanded ? {} : null },
|
|
18
20
|
listen(target, type, callback, capture) { assert.equal(type, 'keydown'); assert.equal(capture, true); handler = callback },
|
|
@@ -27,7 +29,22 @@ function escape(target, extra = {}) {
|
|
|
27
29
|
const event = { key: 'Escape', target, ...extra, preventDefault() { calls.push('prevent') }, stopPropagation() { calls.push('stop') } }
|
|
28
30
|
handler(event)
|
|
29
31
|
}
|
|
30
|
-
function reset() { calls = []; activeWindow = null; chatExpanded = true; context.panelOpen = true; context.settingsOpen = false; context.settingsByKey = {}; context.captureRow.getAttribute = () => '0' }
|
|
32
|
+
function reset() { calls = []; activeWindow = null; chatExpanded = true; hostModalOpen = false; context.panelOpen = true; context.settingsOpen = false; context.settingsByKey = {}; context.captureRow.getAttribute = () => '0' }
|
|
33
|
+
for (const mode of [true, false]) {
|
|
34
|
+
for (const target of [ask, historyInput, outside, expandedChat]) {
|
|
35
|
+
reset(); fullView = mode; hostModalOpen = true; activeWindow = 'explanation'
|
|
36
|
+
escape(target)
|
|
37
|
+
assert.deepEqual(calls, [], '官方模态设置浮层优先消费 Esc,不受视图模式、焦点或最后操作窗口影响')
|
|
38
|
+
assert.equal(context.panelOpen, true)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
reset(); fullView = true; hostModalOpen = true; context.settingsOpen = true; escape(ask)
|
|
42
|
+
assert.deepEqual(calls, [], '官方设置打开时不关闭插件设置抽屉')
|
|
43
|
+
assert.equal(context.settingsOpen, true)
|
|
44
|
+
reset(); hostModalOpen = true; context.captureRow.getAttribute = () => '1'; escape(ask)
|
|
45
|
+
assert.deepEqual(calls, [], '官方设置打开时不取消插件录音')
|
|
46
|
+
hostModalOpen = false; escape(ask)
|
|
47
|
+
assert.deepEqual(calls, ['prevent', 'stop', 'voice'], '官方设置退出后,下一次 Esc 恢复插件的内层退出顺序')
|
|
31
48
|
reset(); activeWindow = 'chat'; escape(ask)
|
|
32
49
|
assert.deepEqual(calls, [], '最后点击聊天后,即使键盘仍在解读输入框,也让聊天先退出')
|
|
33
50
|
reset(); activeWindow = 'explanation'; escape(expandedChat)
|
|
@@ -63,4 +80,22 @@ for (const target of [ask, historyInput]) {
|
|
|
63
80
|
}
|
|
64
81
|
reset(); fullView = false; escape(outside)
|
|
65
82
|
assert.equal(context.panelOpen, false, '普通分栏保留解读原来的全局 Esc 行为')
|
|
66
|
-
|
|
83
|
+
|
|
84
|
+
// 语音卡另有一个更早注册的 capture 监听,也必须给官方浮层让出 Esc。
|
|
85
|
+
const panelHandler = handler
|
|
86
|
+
const voiceStart = source.indexOf(' var offVoiceEscape = listen(document,')
|
|
87
|
+
const voiceEnd = source.indexOf('\n }, true)', voiceStart) + '\n }, true)'.length
|
|
88
|
+
assert.ok(voiceStart >= 0 && voiceEnd > voiceStart)
|
|
89
|
+
context.vcardState = { open: true, suppress: false }
|
|
90
|
+
context.closeVCard = () => { calls.push('vcard'); context.vcardState.open = false }
|
|
91
|
+
new vm.Script(source.slice(voiceStart, voiceEnd)).runInContext(context)
|
|
92
|
+
const voiceHandler = handler
|
|
93
|
+
handler = panelHandler
|
|
94
|
+
reset(); hostModalOpen = true
|
|
95
|
+
voiceHandler({ key: 'Escape', preventDefault() { calls.push('prevent') }, stopPropagation() { calls.push('stop') } })
|
|
96
|
+
assert.deepEqual(calls, [], '官方设置打开时,语音卡不抢先消费 Esc')
|
|
97
|
+
assert.equal(context.vcardState.open, true)
|
|
98
|
+
hostModalOpen = false
|
|
99
|
+
voiceHandler({ key: 'Escape', preventDefault() { calls.push('prevent') }, stopPropagation() { calls.push('stop') } })
|
|
100
|
+
assert.deepEqual(calls, ['prevent', 'stop', 'vcard'], '官方设置退出后,Esc 可以关闭语音卡')
|
|
101
|
+
console.log('PASS focused Esc: host modals, independent windows, IME, nested settings, voice, history and split-mode behavior')
|
package/src/client/index.js
CHANGED
|
@@ -2648,7 +2648,7 @@ window.__ModuleLoader__.load({
|
|
|
2648
2648
|
modelPill.appendChild(modelPillTier)
|
|
2649
2649
|
modelPill.appendChild(el('span', 'dsh-sel-picker-caret', '▼'))
|
|
2650
2650
|
var modelMenu = el('div', 'dsh-sel-pickermenu')
|
|
2651
|
-
|
|
2651
|
+
// role 随开合同步:DSH 按 [role="menu"] 判定弹层,不检查 CSS 可见性。
|
|
2652
2652
|
|
|
2653
2653
|
// ── 录音行(与主会话 ui-voice-input 同一套结构)──
|
|
2654
2654
|
// [✕ 取消] [实时波形 / 状态文案] [■ 停止 或 行内动作] …… [↑ 发送(原位不动)]
|
|
@@ -9484,6 +9484,7 @@ window.__ModuleLoader__.load({
|
|
|
9484
9484
|
*/
|
|
9485
9485
|
var offVoiceEscape = listen(document, 'keydown', function (event) {
|
|
9486
9486
|
if (event.key !== 'Escape' || !vcardState.open || event.defaultPrevented) return
|
|
9487
|
+
if (document.querySelector('[role="dialog"][aria-modal="true"]')) return
|
|
9487
9488
|
event.preventDefault()
|
|
9488
9489
|
event.stopPropagation()
|
|
9489
9490
|
vcardState.suppress = true
|
|
@@ -9936,11 +9937,13 @@ window.__ModuleLoader__.load({
|
|
|
9936
9937
|
|
|
9937
9938
|
function closeModelMenu() {
|
|
9938
9939
|
modelMenu.setAttribute('data-open', '0')
|
|
9940
|
+
modelMenu.removeAttribute('role')
|
|
9939
9941
|
modelPill.setAttribute('aria-expanded', 'false')
|
|
9940
9942
|
}
|
|
9941
9943
|
|
|
9942
9944
|
function openModelMenu() {
|
|
9943
9945
|
modelMenu.setAttribute('data-open', '1')
|
|
9946
|
+
modelMenu.setAttribute('role', 'menu')
|
|
9944
9947
|
modelPill.setAttribute('aria-expanded', 'true')
|
|
9945
9948
|
renderModelMenu()
|
|
9946
9949
|
loadModelCatalog(false).then(function () {
|
|
@@ -9962,7 +9965,7 @@ window.__ModuleLoader__.load({
|
|
|
9962
9965
|
event.stopPropagation() // 菜单内部点击不要触发"点到外面"
|
|
9963
9966
|
})
|
|
9964
9967
|
var offModelKeys = listen(modelMenu, 'keydown', function (event) {
|
|
9965
|
-
if (event.key === 'Escape') closeModelMenu()
|
|
9968
|
+
if (event.key === 'Escape' && !document.querySelector('[role="dialog"][aria-modal="true"]')) closeModelMenu()
|
|
9966
9969
|
})
|
|
9967
9970
|
|
|
9968
9971
|
// —— 网页模式开关 ——
|
|
@@ -10140,6 +10143,9 @@ window.__ModuleLoader__.load({
|
|
|
10140
10143
|
|
|
10141
10144
|
var offKeyDown = listen(document, 'keydown', function (event) {
|
|
10142
10145
|
if (event.key !== 'Escape' || !panelOpen || event.defaultPrevented || event.isComposing || event.keyCode === 229) return
|
|
10146
|
+
// 官方模态浮层的 Esc 在冒泡阶段处理;capture 不消费,先让官方设置等浮层退出。
|
|
10147
|
+
// 插件面板和设置抽屉是非模态 dialog,不带 aria-modal,不会命中这里。
|
|
10148
|
+
if (document.querySelector('[role="dialog"][aria-modal="true"]')) return
|
|
10143
10149
|
// 完整视图记录最后点击/操作的小窗;不能只看可能停在另一窗的键盘焦点。
|
|
10144
10150
|
var fullViewFrame = document.querySelector('[data-dsh-full-view]')
|
|
10145
10151
|
if (fullViewFrame) {
|