dsh-sidebar-row-menu 1.4.3 → 1.5.1
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 +72 -0
- package/README.en.md +125 -54
- package/README.md +90 -39
- package/lib/client.js +361 -16
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,77 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.1 — 2026-11-05
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Session-row right-click entries appear every time on DSH 0.2.2.** The entries
|
|
8
|
+
(pinned app, Open with, permanent delete, delete archived) used to be gated by a
|
|
9
|
+
400 ms time window measured from the `contextmenu` event. 0.2.2 mounts the menu
|
|
10
|
+
asynchronously and, when a menu is already open, tears the previous panel down
|
|
11
|
+
before mounting the new one — that teardown reset the window, so the next menu
|
|
12
|
+
could come up without the plugin entries. The gate is now a gesture mark that is
|
|
13
|
+
cleared only once the menu panel has actually mounted, so right-click keeps its
|
|
14
|
+
entries while the row's `…` button still opens the untouched native menu.
|
|
15
|
+
On 0.2.2 the plugin also stops intercepting `contextmenu` and forwarding it as an
|
|
16
|
+
ellipsis click: the row already carries the host trigger, so the host opens its
|
|
17
|
+
own menu at the pointer (no anchor rewriting, no synthetic click).
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **Docs and registry metadata describe the 0.2.2 behavior.** README (both languages),
|
|
22
|
+
the npm description and the marketplace entry no longer present the plugin as a
|
|
23
|
+
self-drawn sidebar menu. They now state that DSH 0.2.2 renders the menus and the
|
|
24
|
+
plugin contributes entries (workspace items interleaved by `order`, session rows
|
|
25
|
+
extended in the host menu opened at the pointer), and that 0.2.1 keeps the
|
|
26
|
+
self-drawn workspace menu.
|
|
27
|
+
|
|
28
|
+
### 中文摘要
|
|
29
|
+
|
|
30
|
+
- **修复 0.2.2 上会话行右键菜单「有时没有插件条目」**:条目原来用 `contextmenu`
|
|
31
|
+
之后 400 ms 的时间窗口判断,而 0.2.2 的菜单是异步挂载的,替换已有菜单时还会
|
|
32
|
+
先卸载旧面板再挂新面板,这次卸载会把窗口清掉,于是下一次菜单可能不带插件条目。
|
|
33
|
+
改为「右键手势标记」:只有菜单面板真正挂载后才收尾,右键稳定带条目,行尾「…」
|
|
34
|
+
打开的菜单仍然保持原生内容。
|
|
35
|
+
0.2.2 上也不再拦截 `contextmenu` 后转成「…」点击:行本身已带宿主触发器,直接
|
|
36
|
+
由宿主把菜单开在指针处(不改锚点、不补发点击)。
|
|
37
|
+
- **文档与市场文案改为描述 0.2.2 的真实行为**:README(中英)、npm 描述与市场条目
|
|
38
|
+
不再写成「插件自绘侧栏菜单」,而是说明 0.2.2 由宿主渲染菜单、插件按 `order` 贡献
|
|
39
|
+
条目(工作区项混排、会话行扩展宿主开在指针处的菜单),0.2.1 上保留自绘工作区菜单。
|
|
40
|
+
|
|
41
|
+
## 1.5.0 — 2026-11-05
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **DSH 0.2.2 native context menus**: on hosts that expose `ctx.contextMenus`, the
|
|
46
|
+
workspace-row entries are registered as data contributions instead of a self-drawn
|
|
47
|
+
menu. Entries keep their positions relative to the host items —
|
|
48
|
+
New session 90, pinned app 150, Open with 180, Copy workspace path 250,
|
|
49
|
+
Open in new tab 260, Delete workspace and all sessions 310 — so they interleave
|
|
50
|
+
with the native Rename (100), Reveal (200) and Delete workspace (300). Right-click
|
|
51
|
+
and the row's `…` button open the same menu, because the host now shares one menu
|
|
52
|
+
between them. On 0.2.1 the plugin still draws its own workspace menu. Session-row
|
|
53
|
+
entries keep using the `sidebar.workspaces.session.menu.item` extension point.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- The permanent-delete confirmation falls back to “Untitled session” instead of
|
|
58
|
+
showing an empty `会话「」` for sessions whose title has not been generated yet.
|
|
59
|
+
- On 0.2.2 the settings page now hides the workspace-row “Also show in the … menu”
|
|
60
|
+
switches: the host shares one native menu between right-click and the `…` button,
|
|
61
|
+
so those per-entry toggles could never take effect there. The session-row switches
|
|
62
|
+
still work and stay visible.
|
|
63
|
+
|
|
64
|
+
### 中文摘要
|
|
65
|
+
|
|
66
|
+
- **支持 DSH 0.2.2 的原生右键菜单**:宿主提供 `ctx.contextMenus` 时,工作区行条目改为
|
|
67
|
+
数据贡献注册,按 order 与原生项交错排列(新会话 90、置顶应用 150、打开方式 180、
|
|
68
|
+
复制工作区路径 250、在新标签打开 260、删除工作区及所有会话 310;原生为重命名 100、
|
|
69
|
+
在文件夹显示 200、删除工作区 300)。右键与行尾「…」共用宿主同一份菜单。0.2.1 上仍
|
|
70
|
+
走自绘工作区菜单;会话行条目继续通过 `sidebar.workspaces.session.menu.item` 扩展点渲染。
|
|
71
|
+
- 修复未命名会话「永久删除」确认框显示空标题的问题,回退为「未命名会话」。
|
|
72
|
+
- 0.2.2 的设置页隐藏工作区行「同时放进「…」菜单」开关(该版本右键与「…」共用同一份
|
|
73
|
+
菜单,开关无从生效);会话行的同类开关保留且仍然有效。
|
|
74
|
+
|
|
3
75
|
## 1.4.3 — 2026-11-04
|
|
4
76
|
|
|
5
77
|
### Changed
|
package/README.en.md
CHANGED
|
@@ -9,42 +9,59 @@
|
|
|
9
9
|
|
|
10
10
|
Right-click menus for the sidebar of [DeepSeek Harness](https://github.com/deepseek-ai) (DSH):
|
|
11
11
|
|
|
12
|
-
- **Workspace rows** — right-click opens a menu (
|
|
13
|
-
|
|
14
|
-
- **Session rows nested under a workspace** — right-click opens that row's
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
12
|
+
- **Workspace rows** — right-click opens a menu (since DSH 0.2.2 it is part of the
|
|
13
|
+
host's native menu, the same one the row's "…" button opens);
|
|
14
|
+
- **Session rows nested under a workspace** — right-click opens that row's own menu
|
|
15
|
+
at the pointer, with the plugin's own entries appended.
|
|
16
|
+
|
|
17
|
+
DSH 0.2.2 turned the sidebar context menus into data: the host renders the workspace
|
|
18
|
+
menu and this plugin contributes its items by `order`; the session menu is still
|
|
19
|
+
rendered from its slot by the host, and the host also opens it at the pointer. On
|
|
20
|
+
0.2.2 the look, row height, icons, danger color, keyboard navigation and
|
|
21
|
+
click-outside / Escape dismissal are therefore exactly the built-in menu's — the
|
|
22
|
+
plugin only supplies entries. Hosts before 0.2.2 have no such service: the plugin
|
|
23
|
+
still draws the workspace menu with DSH's official `MenuSurface` + `MenuItemButton`,
|
|
24
|
+
and still moves the row's "…" menu to the pointer on right-click.
|
|
25
|
+
|
|
26
|
+
Session menu content comes from the `sidebar.workspaces.session.menu.item` slot —
|
|
27
|
+
built-in actions and other plugins' actions are all there. This plugin only appends
|
|
28
|
+
its own entries; it does not copy or hardcode any built-in action.
|
|
29
|
+
|
|
30
|
+
> The "…" button menu stays exactly as DSH ships it. The plugin's session entries
|
|
31
|
+
> appear only when the menu is opened by right-click. On 0.2.2 both gestures share
|
|
32
|
+
> one host menu, so the plugin tells them apart by the gesture itself: a right-click
|
|
33
|
+
> sets a mark that is released once the menu panel has actually mounted, while the
|
|
34
|
+
> pointerdown on the "…" button clears it immediately. A right-click menu does not
|
|
35
|
+
> close when the pointer leaves it; it closes only on an outside click, an item
|
|
36
|
+
> selection, or Escape.
|
|
29
37
|
|
|
30
38
|
## Features
|
|
31
39
|
|
|
32
40
|
### Workspace row
|
|
33
41
|
|
|
42
|
+
On 0.2.2 the host renders this menu, and the plugin's items interleave with the
|
|
43
|
+
built-in Rename (100), Reveal in file manager (200) and Delete workspace (300):
|
|
44
|
+
New session 90, pinned app 150, Open with 180, Copy workspace path 250, Open in new
|
|
45
|
+
tab 260, Delete workspace and all sessions 310. On 0.2.1 the plugin draws the whole
|
|
46
|
+
menu and runs the official dialogs for Rename and Delete workspace.
|
|
47
|
+
|
|
34
48
|
| Item | Behavior |
|
|
35
49
|
| --- | --- |
|
|
36
50
|
| New session | Expands the workspace and starts a session (same as the row's `+`) |
|
|
37
|
-
| Rename | Opens the official "Rename workspace" dialog |
|
|
51
|
+
| Rename (built-in) | Opens the official "Rename workspace" dialog; on 0.2.1 the plugin renders this item itself |
|
|
38
52
|
| Pinned open-with app | A pinned app shows directly at the top level (e.g. "Open with Visual Studio Code") and opens the workspace directory |
|
|
39
53
|
| Open with | A submenu of external apps (editors, terminals, file managers) sharing the session menu's catalog, pinning and toggles; hover expands it |
|
|
54
|
+
| Reveal in file manager (built-in) | Opens the workspace directory in Explorer; provided by the host on 0.2.2, absent on 0.2.1 |
|
|
40
55
|
| Copy workspace path | Copies the directory path |
|
|
41
56
|
| Open in new tab | Only when `dsh-project-tabs` (exposing `openWorkspaceInTab`) is installed |
|
|
42
|
-
| Delete workspace | Opens the official delete confirmation; removes only the workspace registration and leaves its sessions under "Ungrouped" |
|
|
57
|
+
| Delete workspace (built-in) | Opens the official delete confirmation; removes only the workspace registration and leaves its sessions under "Ungrouped". On 0.2.1 the plugin renders this item itself |
|
|
43
58
|
| Delete workspace and all sessions | Permanently deletes every session and transcript under the workspace first, then the workspace registration. If any session fails, the workspace is kept so the user can retry. The directory itself is not deleted |
|
|
44
59
|
|
|
45
60
|
### Session row
|
|
46
61
|
|
|
47
|
-
Right-click does not build a second menu
|
|
62
|
+
Right-click does not build a second menu: on 0.2.2 the host opens the row's native
|
|
63
|
+
menu at the pointer, and on 0.2.1 the plugin triggers the row's "…" menu and moves
|
|
64
|
+
its anchor to the pointer. Either way the content comes from the same slot:
|
|
48
65
|
|
|
49
66
|
| Source | Entries |
|
|
50
67
|
| --- | --- |
|
|
@@ -69,17 +86,21 @@ unarchived sessions alone by default; the opt-in switch extends the single-sessi
|
|
|
69
86
|
entry to them. Deletion never cascades into forked child sessions.
|
|
70
87
|
|
|
71
88
|
These entries are **right-click only by default**: they do not mount when the "…"
|
|
72
|
-
button opens the menu.
|
|
73
|
-
|
|
89
|
+
button opens the menu. There is no time window any more; the plugin sets a
|
|
90
|
+
right-click gesture mark and releases it only once the menu panel has actually
|
|
91
|
+
appeared (0.2.2 mounts the menu asynchronously), and the pointerdown on the "…"
|
|
92
|
+
button clears it at once. While the mark is live the entries render normally;
|
|
93
|
+
otherwise they render `null`.
|
|
74
94
|
|
|
75
95
|
To use them from the row's "…" button, enable them one by one under Settings →
|
|
76
96
|
"Row menu" → "Also show in the "…" menu". Session entries mount into the native
|
|
77
|
-
"…" menu; enabling any workspace entry makes this plugin take over the
|
|
78
|
-
button and render the official "Rename / Delete workspace" items alongside
|
|
79
|
-
enabled plugin entries.
|
|
80
|
-
DSH's
|
|
97
|
+
"…" menu; on 0.2.1, enabling any workspace entry makes this plugin take over the
|
|
98
|
+
row's "…" button and render the official "Rename / Delete workspace" items alongside
|
|
99
|
+
the enabled plugin entries (on 0.2.2 the host has only one menu, so that group of
|
|
100
|
+
switches is hidden). With everything off, the "…" button behaves exactly like DSH's
|
|
101
|
+
own.
|
|
81
102
|
|
|
82
|
-
Blank draft sessions have no
|
|
103
|
+
Blank draft sessions have no right-click menu and are left alone. Rows that are
|
|
83
104
|
neither a workspace nor a session (e.g. "Show more") are not intercepted.
|
|
84
105
|
|
|
85
106
|
### Settings
|
|
@@ -93,12 +114,17 @@ Settings → "Row menu" toggles every new entry:
|
|
|
93
114
|
| Delete session | Hides the single-session delete entry in session menus |
|
|
94
115
|
| Allow deleting unarchived sessions | Off by default; also shows "Delete session permanently" on unarchived rows. Requires the previous switch |
|
|
95
116
|
| Delete all archived sessions | Hides the bulk-delete entry |
|
|
96
|
-
| Also show in the "…" menu → workspace | Adds workspace right-click entries one by one to the row's "…" menu: New session, Open with, Copy workspace path, Open in new tab, Delete workspace and all sessions. Enabling any of them makes this plugin render the "…" menu |
|
|
117
|
+
| Also show in the "…" menu → workspace (0.2.1 only) | Adds workspace right-click entries one by one to the row's "…" menu: New session, Open with, Copy workspace path, Open in new tab, Delete workspace and all sessions. Enabling any of them makes this plugin render the "…" menu |
|
|
97
118
|
| Also show in the "…" menu → session | Adds session right-click entries one by one to the native "…" menu: Open with, Delete session, Delete all archived sessions |
|
|
98
119
|
|
|
99
120
|
The "Also show in the "…" menu" switches are all off by default; until one is
|
|
100
121
|
enabled the "…" menu is exactly DSH's own.
|
|
101
122
|
|
|
123
|
+
On DSH 0.2.2 and later, the workspace row's right-click and "…" already open the
|
|
124
|
+
same host menu, so the plugin entries always show up there; the "Also show in the
|
|
125
|
+
"…" menu → workspace" switches only affect 0.2.1 and older hosts, so they are hidden
|
|
126
|
+
on 0.2.2.
|
|
127
|
+
|
|
102
128
|
Switches live in browser `localStorage` (`dsh.sidebar-row-menu.items.v1`,
|
|
103
129
|
`dsh.sidebar-row-menu.open-with.v1`, `dsh.sidebar-row-menu.open-with.pinned.v1` and
|
|
104
130
|
`dsh.sidebar-row-menu.ellipsis.v1`)
|
|
@@ -328,46 +354,72 @@ row components are:
|
|
|
328
354
|
|
|
329
355
|
The plugin reads `__reactFiber$` from the event target during the capture phase of
|
|
330
356
|
`contextmenu`, then walks up to 60 `return` links looking for matching
|
|
331
|
-
`memoizedProps`. If nothing matches it does not intercept at all.
|
|
357
|
+
`memoizedProps`. If nothing matches it does not intercept at all. On 0.2.2 the fiber
|
|
358
|
+
is also used to tell whether the row already carries the host's own context menu
|
|
359
|
+
trigger (`memoizedProps.onContextMenu` is a function): if it does, the host opens the
|
|
360
|
+
menu at the pointer by itself and the plugin only drops a gesture mark, never
|
|
361
|
+
touching the event.
|
|
332
362
|
|
|
333
363
|
### Why session actions are not hardcoded
|
|
334
364
|
|
|
335
|
-
The native
|
|
365
|
+
The native session menu is not a hardcoded button list: it is
|
|
336
366
|
`renderSlot('sidebar.workspaces.session.menu.item', ...)`. Fork, pin, archive and
|
|
337
367
|
third-party actions are all slot entries. Slot ownership is enforced by the
|
|
338
368
|
renderer: an entry may only render child slots it declared itself, so a third
|
|
339
369
|
party cannot re-layout someone else's slot.
|
|
340
370
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
`
|
|
371
|
+
So a session right-click never copies menu content:
|
|
372
|
+
|
|
373
|
+
- 0.2.2: the row component carries the host's `getTriggerProps` (its `onContextMenu`
|
|
374
|
+
hands `kind:'session'` to `ctx.contextMenus`), so the host owns the gesture and the
|
|
375
|
+
plugin does not even need to find a `Menu` or click a button;
|
|
376
|
+
- 0.2.1: find the `Menu` in the row's fiber subtree and trigger its anchor (the
|
|
377
|
+
ellipsis button), which opens the same slot content.
|
|
378
|
+
|
|
379
|
+
The plugin's own actions are registered through `ctx.slots.inject` +
|
|
380
|
+
`ctx.slots.register`, so:
|
|
345
381
|
|
|
346
382
|
- they do not depend on built-in fork/pin entries existing;
|
|
347
383
|
- new entries from DSH or other plugins show up automatically;
|
|
348
384
|
- no built-in action is copied or hardcoded.
|
|
349
385
|
|
|
350
|
-
Workspace rows have no such slot, so their menu is
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
386
|
+
Workspace rows have no such slot on 0.2.1, so their menu is composed by this plugin.
|
|
387
|
+
Since 0.2.2 the host provides `ctx.contextMenus` and the workspace items become data
|
|
388
|
+
contributions — again just "adding its own items to the host's menu". The official
|
|
389
|
+
`Menu` accepts only a flat `items` list and cannot express submenus, so the 0.2.1
|
|
390
|
+
workspace menu is drawn with `MenuSurface` + `MenuItemButton`: rows, icons, danger
|
|
391
|
+
styling and separators reuse the official controls.
|
|
354
392
|
|
|
355
393
|
### Why the session menu appears at the pointer
|
|
356
394
|
|
|
357
|
-
|
|
358
|
-
|
|
395
|
+
On 0.2.2 there is nothing to do: the host's `onContextMenu` passes `clientX /
|
|
396
|
+
clientY` to `ctx.contextMenus.open()`, and the panel renders at the pointer.
|
|
397
|
+
|
|
398
|
+
On 0.2.1 the official `Menu` anchors to the ellipsis button, i.e. to the right of it.
|
|
399
|
+
On right-click the plugin temporarily points that button's (and two ancestors')
|
|
359
400
|
`getBoundingClientRect` at the pointer as a `0×0` rect. `Menu` re-reads the anchor
|
|
360
401
|
rect every frame, so the panel lands next to the cursor. The original methods are
|
|
361
402
|
restored as soon as the panel leaves `body`, so the button menu keeps its normal
|
|
362
403
|
position.
|
|
363
404
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
405
|
+
### Why the right-click entries never show in the "…" menu
|
|
406
|
+
|
|
407
|
+
On 0.2.2 both gestures open the same host menu, and React cannot tell where it came
|
|
408
|
+
from, so the plugin watches the gesture: a right-click sets an in-process mark, and a
|
|
409
|
+
`MutationObserver` waits for a new `[role="menu"]` panel to appear (0.2.2 mounts the
|
|
410
|
+
menu asynchronously, after every provider is ready); once the panel shows up the mark
|
|
411
|
+
is released 400 ms later, with a 3 s fallback in case another plugin swallows the
|
|
412
|
+
gesture. The `pointerdown` on the row's "…" button clears the mark immediately, and
|
|
413
|
+
only trusted input counts (`isTrusted`) — the synthetic `pointerdown` the plugin
|
|
414
|
+
re-dispatches is not a gesture.
|
|
367
415
|
|
|
368
|
-
|
|
416
|
+
Entries read the mark once while mounting, so a slow panel does not matter, and the
|
|
417
|
+
mark only lives until "the panel for this gesture appeared", never into the next one.
|
|
369
418
|
|
|
370
|
-
|
|
419
|
+
### Why the 0.2.1 menu is mounted imperatively
|
|
420
|
+
|
|
421
|
+
On 0.2.2 the host mounts every menu and the plugin creates no container. On 0.2.1 a
|
|
422
|
+
contextmenu event is not part of React's render flow, so the workspace menu creates
|
|
371
423
|
a `0×0`, `z-index:2147483647` host on demand and `createRoot`s a self-drawn panel;
|
|
372
424
|
after mount it measures once and clamps its corner into the viewport, then unmounts
|
|
373
425
|
and removes the host on close. The session menu mounts nothing new; it only opens
|
|
@@ -422,19 +474,38 @@ The package declares its supported DSH range in `package.json` as
|
|
|
422
474
|
incompatible installs, and the plugin market shows it as the supported version;
|
|
423
475
|
0.3 and later need re-verification before the upper bound is raised.
|
|
424
476
|
|
|
425
|
-
Built against DSH client `0.2.1-rc.5` (`ProjectRowItem`,
|
|
426
|
-
`Menu`).
|
|
427
|
-
|
|
428
|
-
|
|
477
|
+
Built against DSH client `0.2.1-rc.5` and `0.2.2` (`ProjectRowItem`,
|
|
478
|
+
`SessionNodeItem`, `Menu`, `contextMenus`). DSH 0.2.2 moved the sidebar context
|
|
479
|
+
menus to a data-driven service, and the plugin picks the path by host capability:
|
|
480
|
+
|
|
481
|
+
- **0.2.2+**: workspace entries are registered with `ctx.contextMenus.register`
|
|
482
|
+
(New session 90, pinned app 150, Open with 180, Copy workspace path 250,
|
|
483
|
+
Open in new tab 260, Delete workspace and all sessions 310), interleaved by
|
|
484
|
+
order with the native Rename (100), Reveal (200) and Delete workspace (300).
|
|
485
|
+
Right-click and the "…" button share the host's single menu.
|
|
486
|
+
- **0.2.1**: no `contextMenus` service, so the plugin keeps drawing its own
|
|
487
|
+
workspace menu.
|
|
488
|
+
- **Session rows**: 0.2.2 still supports the
|
|
489
|
+
`sidebar.workspaces.session.menu.item` extension point; the plugin entries keep
|
|
490
|
+
mounting there, and right-click still opens the host menu at the pointer.
|
|
491
|
+
|
|
492
|
+
If a DSH upgrade changes those row components or the menu positioning:
|
|
493
|
+
|
|
494
|
+
- workspace right-click falls back to the self-drawn menu (0.2.1 path) or silently
|
|
495
|
+
stops appearing (no error, no UI breakage);
|
|
429
496
|
- session right-click falls back to opening next to the "…" button, with correct content.
|
|
430
497
|
|
|
431
498
|
## Known limitations
|
|
432
499
|
|
|
433
|
-
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
500
|
+
- On 0.2.1 the plugin depends on the props shape of the built-in row components.
|
|
501
|
+
From 0.2.2 the workspace entries use `contextMenus`, the session entries still
|
|
502
|
+
use the legacy extension point, and `memoizedProps.onContextMenu` decides whether
|
|
503
|
+
the host has taken the row over. If DSH changes that implementation, right-click
|
|
504
|
+
may silently do nothing.
|
|
505
|
+
- On 0.2.1 session right-click depends on the row's `Menu` staying in the fiber
|
|
506
|
+
subtree. If it is not found, the plugin clicks the row-actions ellipsis button;
|
|
507
|
+
failing that it gives up — it never builds a competing menu and never triggers row
|
|
508
|
+
open. On 0.2.2 no fallback is needed: the host is the trigger.
|
|
438
509
|
- Permanent deletion is offered for **archived** sessions by default; enable
|
|
439
510
|
"Allow deleting unarchived sessions" to delete unarchived sessions from their
|
|
440
511
|
right-click menu as well. Deletion does not cascade into forked child sessions —
|
package/README.md
CHANGED
|
@@ -9,36 +9,49 @@
|
|
|
9
9
|
|
|
10
10
|
给 [DeepSeek Harness](https://github.com/deepseek-ai)(DSH)左侧边栏加右键菜单:
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
12
|
+
- **工作区行**:右键弹出菜单(DSH 0.2.2 起并入宿主原生菜单,与行尾「…」是同一份);
|
|
13
|
+
- **工作区下层的会话行**:右键在指针处展开该行自己的菜单,并追加本插件的条目。
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
DSH 0.2.2 把侧栏右键改成数据驱动:工作区菜单由宿主统一渲染,本插件按 order 把条目混排
|
|
16
|
+
进去;会话菜单仍由宿主渲染 slot 内容,右键也由宿主开在指针处。所以 0.2.2 上菜单的视觉、
|
|
17
|
+
行高、图标、危险色、键盘导航,以及“点外部关闭 / Esc 关闭”全部与内置菜单一致,本插件只
|
|
18
|
+
提供条目。0.2.1 及更早的宿主没有这套服务:工作区菜单仍由本插件用官方 `MenuSurface` +
|
|
19
|
+
`MenuItemButton` 自绘,会话右键仍由插件把行尾「…」菜单搬到指针处。
|
|
20
|
+
|
|
21
|
+
会话菜单的内容来自
|
|
18
22
|
`sidebar.workspaces.session.menu.item` slot —— 内置动作和其他插件的动作都在里面,
|
|
19
23
|
本插件只追加自己的条目,不复制、不硬编码任何内置动作。
|
|
20
24
|
|
|
21
|
-
>
|
|
22
|
-
>
|
|
25
|
+
> 点行尾「…」按钮看到的会话菜单保持 DSH 原样;本插件的会话条目只在右键展开时出现。
|
|
26
|
+
> 0.2.2 上右键与「…」共用同一份宿主菜单,插件按“右键手势”区分:右键时打标,等菜单面板
|
|
27
|
+
> 真正挂载后才收尾;点「…」的按下事件会立刻清标,所以那个菜单不带右键条目。
|
|
28
|
+
> 右键展开的菜单不会因为鼠标移出而关闭,只有点击外部、选中条目或按 Esc 才关闭。
|
|
23
29
|
|
|
24
30
|
## 功能
|
|
25
31
|
|
|
26
32
|
### 工作区行右键
|
|
27
33
|
|
|
34
|
+
0.2.2 上这份菜单由宿主渲染,本插件条目与内置「重命名」(100)、「在文件夹显示」(200)、
|
|
35
|
+
「删除工作区」(300)按 order 交错:新会话 90、置顶应用 150、打开方式 180、复制工作区
|
|
36
|
+
路径 250、在新标签打开 260、删除工作区及所有会话 310。0.2.1 上整份菜单由本插件自绘,
|
|
37
|
+
「重命名」「删除工作区」由插件调用官方对话框。
|
|
38
|
+
|
|
28
39
|
| 菜单项 | 行为 |
|
|
29
40
|
| --- | --- |
|
|
30
41
|
| 新会话 | 展开该工作区并新建会话(等价于行尾 `+`) |
|
|
31
|
-
|
|
|
42
|
+
| 重命名(内置) | 打开官方“重命名工作区”对话框;0.2.1 上由本插件代为渲染 |
|
|
32
43
|
| 置顶的打开方式 | 被置顶的应用直接显示在一级菜单(例如「用 Visual Studio Code 打开」),点击后用该应用打开工作区目录 |
|
|
33
44
|
| 打开方式 | 二级菜单列出宿主提供的外部应用(编辑器、终端、文件管理器);与会话菜单共用同一套应用列表、置顶与开关设置,鼠标悬浮自动展开 |
|
|
45
|
+
| 在文件夹显示(内置) | 在资源管理器里打开工作区目录;0.2.2 由宿主提供,0.2.1 上不出现 |
|
|
34
46
|
| 复制工作区路径 | 复制目录路径到剪贴板 |
|
|
35
47
|
| 在新标签中打开 | 仅在装有 `dsh-project-tabs`(暴露 `openWorkspaceInTab`)时出现 |
|
|
36
|
-
|
|
|
48
|
+
| 删除工作区(内置) | 打开官方删除确认对话框;只删除工作区登记,会话保留并归入「未分组」。0.2.1 上由本插件代为渲染 |
|
|
37
49
|
| 删除工作区及所有会话 | 先永久删除该工作区下的每个会话及其日志,全部成功后再删除工作区登记;任一会话删除失败则保留工作区,便于重试。工作区目录本身不删除 |
|
|
38
50
|
|
|
39
51
|
### 会话行右键
|
|
40
52
|
|
|
41
|
-
|
|
53
|
+
右键会话行不会另起一套菜单:0.2.2 由宿主把该行的原生菜单开在指针处;0.2.1 由本插件触发行尾
|
|
54
|
+
「…」菜单,并临时把它的锚点移到指针。两种情况下菜单内容都来自同一份 slot:
|
|
42
55
|
|
|
43
56
|
| 来源 | 条目 |
|
|
44
57
|
| --- | --- |
|
|
@@ -60,14 +73,16 @@
|
|
|
60
73
|
一级菜单。两个删除入口由本插件自己的宿主路由完成,默认不会删除未归档会话;
|
|
61
74
|
开启「允许删除未归档会话」后才会对普通会话开放。删除不会级联删除 fork 子会话。
|
|
62
75
|
|
|
63
|
-
|
|
64
|
-
|
|
76
|
+
这些条目默认是**右键专属**:点行尾「…」按钮时它们不挂载。实现上不再用时间窗口,而是
|
|
77
|
+
「右键手势标记」:右键时打标,等菜单面板真正出现(0.2.2 的挂载是异步的)后才释放;点
|
|
78
|
+
「…」的按下事件会立刻清标。标记有效期内的条目正常渲染,其余情况渲染直接返回 `null`。
|
|
65
79
|
|
|
66
80
|
如果想直接使用行尾「…」菜单,可以在设置 →「行菜单」→「同时放进「…」菜单」里逐个开启:
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
会话条目会挂进原生「…」菜单;0.2.1 上工作区行开启任意一项后,本插件会接管行尾「…」按钮,
|
|
82
|
+
用同一套菜单展示官方「重命名 / 删除工作区」加上已开启的插件条目(0.2.2 的宿主本来就只有
|
|
83
|
+
一份菜单,这组开关在设置页隐藏)。全部关闭时「…」按钮保持 DSH 原样。
|
|
69
84
|
|
|
70
|
-
|
|
85
|
+
草稿会话(还没有任何消息的空白会话)没有右键菜单,本插件同样不接管。
|
|
71
86
|
非工作区 / 非会话的侧边栏行(如“显示更多”)也不会被拦截。
|
|
72
87
|
|
|
73
88
|
### 设置
|
|
@@ -81,11 +96,14 @@
|
|
|
81
96
|
| 永久删除会话 | 关闭后会话菜单里不再出现单条删除入口 |
|
|
82
97
|
| 允许删除未归档会话 | 默认关闭;开启后未归档会话的右键菜单也会出现「永久删除会话」,需先开启上一项 |
|
|
83
98
|
| 删除所有已归档会话 | 关闭后不再出现批量删除入口 |
|
|
84
|
-
| 同时放进「…」菜单 →
|
|
99
|
+
| 同时放进「…」菜单 → 工作区(仅 0.2.1) | 逐项把工作区行右键条目加进行尾「…」菜单:新会话、打开方式、复制工作区路径、在新标签中打开、删除工作区及所有会话。开启任意一项后「…」按钮改由本插件渲染 |
|
|
85
100
|
| 同时放进「…」菜单 → 会话 | 逐项把会话行右键条目加进原生「…」菜单:打开方式、永久删除会话、删除所有已归档会话 |
|
|
86
101
|
|
|
87
102
|
「同时放进「…」菜单」下的开关默认全部关闭,`…` 菜单在未开启时与 DSH 完全一致。
|
|
88
103
|
|
|
104
|
+
DSH 0.2.2 起,工作区行的右键与「…」本身就是同一份宿主菜单,插件条目会一直出现在「…」里,
|
|
105
|
+
「同时放进「…」菜单 → 工作区」这组开关只影响 0.2.1 及更早的宿主,所以在 0.2.2 上隐藏。
|
|
106
|
+
|
|
89
107
|
开关存在浏览器 `localStorage`(`dsh.sidebar-row-menu.items.v1`、
|
|
90
108
|
`dsh.sidebar-row-menu.open-with.v1`、`dsh.sidebar-row-menu.open-with.pinned.v1` 与
|
|
91
109
|
`dsh.sidebar-row-menu.ellipsis.v1`),
|
|
@@ -260,8 +278,9 @@ npm run build # npm pack(prepack 会先跑 check),产出 .tgz
|
|
|
260
278
|
测试覆盖宿主侧纯函数(路径校验、`vscode://file` 编码、loopback 判定、路由状态码)
|
|
261
279
|
和客户端模块(模块 id、导出、依赖白名单、监听器安装/卸载、slot 注册表),并用伪造的
|
|
262
280
|
`__reactFiber$`、DOM 与 `Menu` 宿主做集成测试:工作区右键的菜单项顺序、会话右键是否
|
|
263
|
-
|
|
264
|
-
|
|
281
|
+
展开内置菜单而不是自建菜单、行自身带宿主 `onContextMenu` 时是否完全不拦截(只补一次
|
|
282
|
+
`pointerdown`)、点「…」后右键条目是否立刻消失、草稿行是否被放过、复制工作区路径的
|
|
283
|
+
Toast 反馈都在断言里。真实 fiber 与服务仍需在运行中的 GUI 里做人工验证。
|
|
265
284
|
|
|
266
285
|
目录:
|
|
267
286
|
|
|
@@ -291,43 +310,62 @@ test/*.test.mjs node:test
|
|
|
291
310
|
`{ id, title, blank, running, archived, pinned, updatedAt }`。
|
|
292
311
|
|
|
293
312
|
插件在 `contextmenu` 捕获阶段从目标节点拿 `__reactFiber$`,向上最多 60 层找匹配的
|
|
294
|
-
`memoizedProps`。找不到就完全不拦截,避免影响其他行和其他插件。
|
|
313
|
+
`memoizedProps`。找不到就完全不拦截,避免影响其他行和其他插件。0.2.2 上 fiber 还用来判断
|
|
314
|
+
这一行是否天生带宿主右键触发器(`memoizedProps.onContextMenu` 是函数):有就说明宿主会把
|
|
315
|
+
菜单开在指针处,插件只补一个手势标记,不再干预事件。
|
|
295
316
|
|
|
296
317
|
### 会话行为什么不硬编码
|
|
297
318
|
|
|
298
|
-
|
|
319
|
+
内置会话菜单不是硬编码的按钮列表,而是
|
|
299
320
|
`renderSlot('sidebar.workspaces.session.menu.item', ...)` 的结果;内置的分叉、置顶、
|
|
300
321
|
归档,以及第三方插件的能力,都以 slot 条目形式注册。slot 的所有权由渲染器校验:
|
|
301
322
|
每个条目只能渲染自己声明过的子 slot,第三方无法直接把手伸进别人的 slot 重新排版。
|
|
302
323
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
324
|
+
因此会话行右键从不复制菜单内容:
|
|
325
|
+
|
|
326
|
+
- 0.2.2:行组件自己带宿主的 `getTriggerProps`(`onContextMenu` 直接把 `kind:'session'`
|
|
327
|
+
交给 `ctx.contextMenus`),右键由宿主处理,插件连「找 Menu、代点按钮」都不需要做;
|
|
328
|
+
- 0.2.1:在行组件的 fiber 子树里找到那个 `Menu`,触发它 anchor(省略号按钮)的点击,
|
|
329
|
+
展开的自然是同一份 slot 内容。
|
|
330
|
+
|
|
331
|
+
本插件自己的动作也通过 `ctx.slots.inject` + `ctx.slots.register` 注册进去,所以:
|
|
306
332
|
|
|
307
333
|
- 不依赖「分叉」「置顶」这些内置条目是否存在;
|
|
308
334
|
- 内置或别的插件以后新增条目,右键菜单自动出现;
|
|
309
335
|
- 不复制、不硬编码任何内置动作。
|
|
310
336
|
|
|
311
|
-
|
|
312
|
-
`
|
|
313
|
-
`
|
|
337
|
+
工作区行在 0.2.1 上没有这样的 slot,所以菜单由本插件自己组项;0.2.2 起宿主提供
|
|
338
|
+
`ctx.contextMenus`,工作区条目改为数据贡献,同样只是“往宿主菜单里加自己的项”。
|
|
339
|
+
官方 `Menu` 的 `items` 只支持单层条目,无法表达子菜单,因此 0.2.1 的工作区菜单用
|
|
340
|
+
`MenuSurface` + `MenuItemButton` 自绘:行、图标、危险色和分隔线都复用官方控件。
|
|
314
341
|
|
|
315
342
|
### 为什么会话菜单能贴着鼠标
|
|
316
343
|
|
|
317
|
-
|
|
344
|
+
0.2.2 上什么都不用做:宿主的 `onContextMenu` 直接把 `clientX / clientY` 交给
|
|
345
|
+
`ctx.contextMenus.open()`,面板就渲染在指针处。
|
|
346
|
+
|
|
347
|
+
0.2.1 上官方 `Menu` 的 `anchor` 是那个省略号按钮,默认位置就在按钮右侧。右键时插件临时把
|
|
318
348
|
该按钮及其两层祖先的 `getBoundingClientRect` 指到指针位置(`0×0` 矩形),官方 `Menu`
|
|
319
349
|
每帧读取锚点 rect 重新定位,于是面板出现在鼠标旁;面板从 `body` 移除时立即还原原始
|
|
320
350
|
方法,不影响按钮菜单的默认位置。
|
|
321
351
|
|
|
322
|
-
|
|
323
|
-
|
|
352
|
+
### 为什么右键条目不会出现在「…」菜单里
|
|
353
|
+
|
|
354
|
+
0.2.2 上右键与「…」打开的是同一份宿主菜单,React 侧无法区分来源,只能看手势:右键时在
|
|
355
|
+
进程内打一个标记,用 `MutationObserver` 等一个新的 `[role="menu"]` 面板出现(0.2.2 的
|
|
356
|
+
菜单是异步挂载的,还要等 provider 就绪),面板出现后再延迟 400 ms 收尾;3 秒兜底防止手势
|
|
357
|
+
被别的插件吞掉。行尾「…」的 `pointerdown` 会立刻清标,只认真实输入(`isTrusted`),因为
|
|
358
|
+
插件自己补发的合成 `pointerdown` 不算手势。
|
|
359
|
+
|
|
360
|
+
条目在挂载时读一次标记,所以面板挂得晚也不影响;标记只活到“这次手势对应的面板出现”,
|
|
361
|
+
不会漏进下一次。
|
|
324
362
|
|
|
325
|
-
###
|
|
363
|
+
### 为什么 0.2.1 的菜单是命令式挂载
|
|
326
364
|
|
|
327
|
-
|
|
328
|
-
`z-index:2147483647` 的容器,`createRoot`
|
|
329
|
-
|
|
330
|
-
|
|
365
|
+
0.2.2 的菜单全部由宿主挂载,插件不创建任何容器。0.2.1 上右键事件不是 React 渲染流的一部分,
|
|
366
|
+
工作区菜单按需创建一个 `0×0`、`z-index:2147483647` 的容器,`createRoot` 渲染自绘面板;面板
|
|
367
|
+
挂载后量一次尺寸,再把左上角夹进视口,避免贴边时被裁掉。关闭时 `unmount()` 并移除容器。
|
|
368
|
+
会话菜单不挂载任何新容器,只是打开行内已有的官方菜单。
|
|
331
369
|
|
|
332
370
|
### 宿主路由
|
|
333
371
|
|
|
@@ -369,17 +407,30 @@ test/*.test.mjs node:test
|
|
|
369
407
|
DSH 宿主按该范围拒绝安装不兼容的宿主,插件市场也按它显示支持版本;0.3 起需要重新验证后
|
|
370
408
|
再放宽上限。
|
|
371
409
|
|
|
372
|
-
按 DSH 客户端 `0.2.1-rc.5` 的 `ProjectRowItem` / `SessionNodeItem` / `Menu`
|
|
410
|
+
按 DSH 客户端 `0.2.1-rc.5` 与 `0.2.2` 的 `ProjectRowItem` / `SessionNodeItem` / `Menu`
|
|
411
|
+
与 `contextMenus` 实现适配。DSH 0.2.2 起把侧栏右键菜单改成数据驱动,本包按宿主能力自动选路:
|
|
412
|
+
|
|
413
|
+
- **0.2.2 及以上**:工作区行改用 `ctx.contextMenus.register` 贡献动作
|
|
414
|
+
(新会话 90、置顶应用 150、打开方式 180、复制路径 250、新标签 260、删除工作区及所有会话 310),
|
|
415
|
+
与原生「重命名」「在文件夹显示」「删除工作区」按 order 交错排列;右键与「…」共用同一份
|
|
416
|
+
宿主菜单,插件不再自绘,也不再接管「…」按钮。会话行由宿主自己把原生菜单开在指针处,
|
|
417
|
+
本包条目继续从 `sidebar.workspaces.session.menu.item` 渲染;
|
|
418
|
+
- **0.2.1**:没有 `contextMenus` 服务,保持原来的自绘工作区菜单;会话右键仍由插件把
|
|
419
|
+
行尾「…」菜单搬到指针。
|
|
420
|
+
|
|
373
421
|
DSH 升级若改动行组件或菜单定位实现:
|
|
374
422
|
|
|
375
|
-
-
|
|
423
|
+
- 工作区右键会退回自绘菜单(0.2.1 路径),或静默不弹(不会报错、不影响原界面);
|
|
376
424
|
- 会话右键会退回行尾「…」按钮旁,内容仍然正确。
|
|
377
425
|
|
|
378
426
|
## 已知限制
|
|
379
427
|
|
|
380
|
-
-
|
|
381
|
-
|
|
382
|
-
|
|
428
|
+
- 0.2.1 下依赖 DSH 内置行组件的 props 形状;0.2.2 起工作区条目改走 `contextMenus`,
|
|
429
|
+
会话条目仍依赖旧扩展点,并用 `memoizedProps.onContextMenu` 判断这一行是否已由宿主接管。
|
|
430
|
+
DSH 升级若改掉对应实现,右键可能静默不弹。
|
|
431
|
+
- 0.2.1 的会话右键依赖行内 `Menu` 组件仍在 fiber 子树里。找不到时退回点击行内操作区的
|
|
432
|
+
省略号按钮,再找不到就放弃 —— 不会自建菜单,也不会误触行的打开动作。0.2.2 上不需要
|
|
433
|
+
这段回退,宿主自己就是触发器。
|
|
383
434
|
- 永久删除默认只对**已归档**会话开放;在设置里开启「允许删除未归档会话」后,未归档
|
|
384
435
|
会话也能从右键菜单直接删除。删除不会级联删除 fork 出来的子会话,需要时请分别删除。
|
|
385
436
|
- 删除操作直接作用于会话日志目录,宿主没有回收站;确认框里已明确提示不可恢复。
|
package/lib/client.js
CHANGED
|
@@ -187,6 +187,7 @@ window.__ModuleLoader__.load({
|
|
|
187
187
|
'settings.collapse': '收起应用列表',
|
|
188
188
|
'settings.ellipsisTitle': '同时放进「…」菜单',
|
|
189
189
|
'settings.ellipsisHint': '勾选后,对应条目会额外出现在行尾「…」按钮的菜单里,并沿用上面的单项开关;不勾选则只在右键菜单出现。',
|
|
190
|
+
'settings.ellipsisHintNative': '当前 DSH 版本的「…」按钮与右键共用同一份菜单,工作区行无需单独登记;这里只保留会话行的开关。',
|
|
190
191
|
'settings.ellipsisWorkspace': '工作区行',
|
|
191
192
|
'settings.ellipsisSession': '会话行',
|
|
192
193
|
'settings.ellipsis.newSession.label': '新建会话',
|
|
@@ -207,6 +208,7 @@ window.__ModuleLoader__.load({
|
|
|
207
208
|
'settings.ellipsis.sessionDeleteArchived.hint': '在已归档会话的「…」菜单里一次清理全部归档会话。',
|
|
208
209
|
'delete.title': '永久删除会话?',
|
|
209
210
|
'delete.description': '会话「{name}」的日志将从磁盘永久删除,此操作无法撤销。',
|
|
211
|
+
'delete.untitled': '未命名会话',
|
|
210
212
|
'delete.archivedTitle': '删除所有已归档会话?',
|
|
211
213
|
'delete.archivedDescription': '将永久删除 {n} 个已归档会话及其日志,此操作无法撤销。',
|
|
212
214
|
'delete.confirm': '永久删除',
|
|
@@ -270,6 +272,7 @@ window.__ModuleLoader__.load({
|
|
|
270
272
|
'settings.collapse': 'Hide applications',
|
|
271
273
|
'settings.ellipsisTitle': 'Also show in the "…" menu',
|
|
272
274
|
'settings.ellipsisHint': 'Checked entries also appear in the row\'s "…" button menu, still subject to the switches above. Unchecked entries stay right-click only.',
|
|
275
|
+
'settings.ellipsisHintNative': 'In this DSH version the "…" button opens the same menu as right-click, so workspace rows need no separate entries; only the session row switches below apply.',
|
|
273
276
|
'settings.ellipsisWorkspace': 'Workspace rows',
|
|
274
277
|
'settings.ellipsisSession': 'Session rows',
|
|
275
278
|
'settings.ellipsis.newSession.label': 'New session',
|
|
@@ -290,6 +293,7 @@ window.__ModuleLoader__.load({
|
|
|
290
293
|
'settings.ellipsis.sessionDeleteArchived.hint': 'Clean up every archived session from an archived row\'s "…" menu.',
|
|
291
294
|
'delete.title': 'Delete this session permanently?',
|
|
292
295
|
'delete.description': 'The log of "{name}" will be removed from disk. This cannot be undone.',
|
|
296
|
+
'delete.untitled': 'Untitled session',
|
|
293
297
|
'delete.archivedTitle': 'Delete all archived sessions?',
|
|
294
298
|
'delete.archivedDescription': 'Permanently delete {n} archived sessions and their logs. This cannot be undone.',
|
|
295
299
|
'delete.confirm': 'Delete permanently',
|
|
@@ -1140,7 +1144,7 @@ window.__ModuleLoader__.load({
|
|
|
1140
1144
|
: t('delete.workspaceEmptyDescription', { name: request.displayTitle ?? '' }))
|
|
1141
1145
|
: bulk
|
|
1142
1146
|
? t('delete.archivedDescription', { n: ids.length })
|
|
1143
|
-
: t('delete.description', { name: request.displayTitle
|
|
1147
|
+
: t('delete.description', { name: request.displayTitle || t('delete.untitled') }),
|
|
1144
1148
|
footer: h(Fragment, null,
|
|
1145
1149
|
jsx(Button, { variant: 'outline', disabled: busy, onClick: close, children: t('delete.cancel') }),
|
|
1146
1150
|
jsx(Button, {
|
|
@@ -1499,6 +1503,11 @@ window.__ModuleLoader__.load({
|
|
|
1499
1503
|
const children = useChildren();
|
|
1500
1504
|
const apps = useApps();
|
|
1501
1505
|
usePinned();
|
|
1506
|
+
// 0.2.2 起行尾「…」与右键共用同一份原生菜单,工作区行的附加开关不再生效。
|
|
1507
|
+
const nativeEllipsis = contextMenusOf(rootCtx) !== null;
|
|
1508
|
+
const ellipsisRows = nativeEllipsis
|
|
1509
|
+
? ELLIPSIS_ROWS.filter(([groupKey]) => groupKey !== 'workspace')
|
|
1510
|
+
: ELLIPSIS_ROWS;
|
|
1502
1511
|
const [expanded, setExpanded] = useState(false);
|
|
1503
1512
|
const anyConflict = takenOver('openWith') || takenOver('deleteSession') || takenOver('deleteArchived');
|
|
1504
1513
|
const locked = items.openWith === false;
|
|
@@ -1578,7 +1587,7 @@ window.__ModuleLoader__.load({
|
|
|
1578
1587
|
return key === 'openWith' && childRows ? [row, childRows] : [row];
|
|
1579
1588
|
});
|
|
1580
1589
|
|
|
1581
|
-
const ellipsisGroups =
|
|
1590
|
+
const ellipsisGroups = ellipsisRows.map(([groupKey, titleKey, rows]) => h('div', {
|
|
1582
1591
|
className: 'dsrm-settings-group',
|
|
1583
1592
|
key: groupKey,
|
|
1584
1593
|
},
|
|
@@ -1606,7 +1615,7 @@ window.__ModuleLoader__.load({
|
|
|
1606
1615
|
anyConflict ? h('div', { className: 'dsrm-settings-note' }, t('settings.conflictNote')) : null,
|
|
1607
1616
|
h('div', { className: 'dsrm-settings-list' }, rows),
|
|
1608
1617
|
h('h2', { className: 'dsrm-settings-title dsrm-settings-subtitle' }, t('settings.ellipsisTitle')),
|
|
1609
|
-
h('div', { className: 'dsrm-settings-note' }, t('settings.ellipsisHint')),
|
|
1618
|
+
h('div', { className: 'dsrm-settings-note' }, t(nativeEllipsis ? 'settings.ellipsisHintNative' : 'settings.ellipsisHint')),
|
|
1610
1619
|
h('div', { className: 'dsrm-settings-groups' }, ellipsisGroups),
|
|
1611
1620
|
);
|
|
1612
1621
|
}
|
|
@@ -1916,13 +1925,68 @@ window.__ModuleLoader__.load({
|
|
|
1916
1925
|
/* ------------------------------------------------ 会话菜单的插件条目 */
|
|
1917
1926
|
|
|
1918
1927
|
/**
|
|
1919
|
-
*
|
|
1920
|
-
*
|
|
1928
|
+
* 条目只在「右键打开的菜单」里出现;点行尾「…」打开的菜单保持原样。
|
|
1929
|
+
*
|
|
1930
|
+
* 不能用时间窗口判断:0.2.2 的菜单是异步挂载的(要等所有 provider 的
|
|
1931
|
+
* ready 和 prepare 延时),菜单一替换还会先卸载旧面板再挂新面板。
|
|
1932
|
+
* 这里改成「手势标记 + 面板出现即收尾」:右键时打标记,条目在挂载时
|
|
1933
|
+
* 读一次;新面板出现(或兜底超时)后标记立刻清掉,不会残留到下一次。
|
|
1921
1934
|
*/
|
|
1922
|
-
let
|
|
1935
|
+
let rightClickMark = false;
|
|
1936
|
+
let rightClickToken = 0;
|
|
1937
|
+
let rightClickWatch = null;
|
|
1938
|
+
|
|
1939
|
+
/** 收掉右键标记;期间若又发生一次右键(token 变了)则保留新的标记。 */
|
|
1940
|
+
function scheduleRightClickMarkEnd(delay) {
|
|
1941
|
+
const token = rightClickToken;
|
|
1942
|
+
window.setTimeout(() => {
|
|
1943
|
+
if (rightClickToken === token) rightClickMark = false;
|
|
1944
|
+
}, delay);
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
/** 当前文档里的菜单面板(测试里的最小 document stub 可能没有 querySelectorAll)。 */
|
|
1948
|
+
function menuPanels() {
|
|
1949
|
+
if (typeof document.querySelectorAll !== 'function') return [];
|
|
1950
|
+
return [...document.querySelectorAll('[role="menu"]')];
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
/** 右键手势开始:打标记,并等菜单面板出现后收尾。 */
|
|
1954
|
+
function markRightClickMenu() {
|
|
1955
|
+
rightClickToken += 1;
|
|
1956
|
+
rightClickMark = true;
|
|
1957
|
+
if (rightClickWatch !== null) return;
|
|
1958
|
+
const known = new Set(menuPanels());
|
|
1959
|
+
const watch = { observer: null, timer: 0 };
|
|
1960
|
+
rightClickWatch = watch;
|
|
1961
|
+
const release = (delay) => {
|
|
1962
|
+
if (rightClickWatch !== watch) return;
|
|
1963
|
+
rightClickWatch = null;
|
|
1964
|
+
window.clearTimeout(watch.timer);
|
|
1965
|
+
if (watch.observer !== null) watch.observer.disconnect();
|
|
1966
|
+
scheduleRightClickMarkEnd(delay);
|
|
1967
|
+
};
|
|
1968
|
+
// 面板迟迟不出现(手势被吞掉)也要收尾,避免标记残留。
|
|
1969
|
+
watch.timer = window.setTimeout(() => release(0), 3000);
|
|
1970
|
+
if (typeof MutationObserver !== 'function' || document.body === null) return;
|
|
1971
|
+
watch.observer = new MutationObserver(() => {
|
|
1972
|
+
if (rightClickWatch !== watch) return;
|
|
1973
|
+
for (const panel of menuPanels()) {
|
|
1974
|
+
if (known.has(panel)) continue;
|
|
1975
|
+
release(400);
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
watch.observer.observe(document.body, { childList: true, subtree: true });
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
/** 点行尾「…」时先清掉上一轮的右键标记:那个菜单不该出现右键条目。 */
|
|
1983
|
+
function clearRightClickMark() {
|
|
1984
|
+
rightClickToken += 1;
|
|
1985
|
+
rightClickMark = false;
|
|
1986
|
+
}
|
|
1923
1987
|
|
|
1924
1988
|
function useRightClickEntry() {
|
|
1925
|
-
const [visible] = useState(() =>
|
|
1989
|
+
const [visible] = useState(() => rightClickMark);
|
|
1926
1990
|
return visible;
|
|
1927
1991
|
}
|
|
1928
1992
|
|
|
@@ -1961,6 +2025,257 @@ window.__ModuleLoader__.load({
|
|
|
1961
2025
|
});
|
|
1962
2026
|
}
|
|
1963
2027
|
|
|
2028
|
+
/* -------------------------------- 工作区行:0.2.2 原生右键贡献 */
|
|
2029
|
+
|
|
2030
|
+
/**
|
|
2031
|
+
* DSH 0.2.2 把右键菜单改成数据贡献(ctx.contextMenus),工作区行也不再向
|
|
2032
|
+
* 行组件传 actions/onCreate,插件原来的自绘入口会取不到 props。这里改为
|
|
2033
|
+
* 向宿主注册同一批动作;0.2.1 没有这个服务,保持自绘菜单不变。
|
|
2034
|
+
*/
|
|
2035
|
+
function contextMenusOf(ctx) {
|
|
2036
|
+
const usable = (menus) => (menus !== null && menus !== undefined && typeof menus.register === 'function' ? menus : null);
|
|
2037
|
+
// 先走 ctx.get:未 inject 时直接读 ctx.contextMenus 会抛错,读不到服务。
|
|
2038
|
+
try {
|
|
2039
|
+
const viaGet = usable(ctx?.get?.('contextMenus'));
|
|
2040
|
+
if (viaGet !== null) return viaGet;
|
|
2041
|
+
} catch {
|
|
2042
|
+
/* 服务容器尚未就绪 */
|
|
2043
|
+
}
|
|
2044
|
+
try {
|
|
2045
|
+
return usable(ctx?.contextMenus);
|
|
2046
|
+
} catch {
|
|
2047
|
+
/* 未 inject 的服务属性访问会抛错,按不存在处理 */
|
|
2048
|
+
}
|
|
2049
|
+
return null;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
/** 应用缓存包成宿主可订阅的数据源,应用列表到达后菜单会自动重解析。 */
|
|
2053
|
+
const appsSource = {
|
|
2054
|
+
getSnapshot: () => appsState,
|
|
2055
|
+
subscribe: (listener) => subscribeApps(listener),
|
|
2056
|
+
};
|
|
2057
|
+
|
|
2058
|
+
/** 应用图标走 data 贡献:resolve 里带 src,组件只负责渲染。 */
|
|
2059
|
+
function ContextMenuAppIcon({ icon }) {
|
|
2060
|
+
const src = typeof icon?.src === 'string' ? icon.src : '';
|
|
2061
|
+
if (src === '') return null;
|
|
2062
|
+
return jsx('img', {
|
|
2063
|
+
className: 'dsrm-app-icon',
|
|
2064
|
+
src,
|
|
2065
|
+
alt: '',
|
|
2066
|
+
onError: (event) => {
|
|
2067
|
+
event.currentTarget.style.visibility = 'hidden';
|
|
2068
|
+
},
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
function workspaceSessionIds(workspaceId) {
|
|
2073
|
+
const snapshot = serviceOf(rootCtx, 'workspaces')?.list?.getSnapshot?.();
|
|
2074
|
+
const entry = Array.isArray(snapshot?.items)
|
|
2075
|
+
? snapshot.items.find((candidate) => candidate?.workspaceId === workspaceId)
|
|
2076
|
+
: undefined;
|
|
2077
|
+
return Array.isArray(entry?.sessionIds)
|
|
2078
|
+
? entry.sessionIds.filter((id) => typeof id === 'string' && id !== '')
|
|
2079
|
+
: [];
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
function openAppAt(appId, cwd, t) {
|
|
2083
|
+
if (cwd === '') {
|
|
2084
|
+
toast(t('noPath'));
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
openApplication(appId, cwd).then((ok) => {
|
|
2088
|
+
if (!ok) toast(t('openFailed'));
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
/**
|
|
2093
|
+
* 数据贡献的一批工作区动作;group 字段决定它挂到哪个 order 的 provider,
|
|
2094
|
+
* 从而夹在原生 rename(100)/reveal(200)/delete(300) 之间。
|
|
2095
|
+
*/
|
|
2096
|
+
function workspaceMenuActions(ctx, value, t) {
|
|
2097
|
+
const workspaceId = typeof value?.workspaceId === 'string' ? value.workspaceId : '';
|
|
2098
|
+
if (workspaceId === '') return [];
|
|
2099
|
+
const cwd = typeof value?.path === 'string' ? value.path : '';
|
|
2100
|
+
const items = [];
|
|
2101
|
+
|
|
2102
|
+
items.push({
|
|
2103
|
+
id: 'new-session',
|
|
2104
|
+
group: 'new',
|
|
2105
|
+
label: t('newSession'),
|
|
2106
|
+
icon: { key: 'sidebar-row-menu.new' },
|
|
2107
|
+
run: () => {
|
|
2108
|
+
const ui = serviceOf(rootCtx, 'uiWorkspace');
|
|
2109
|
+
if (typeof ui?.startSession === 'function') {
|
|
2110
|
+
try {
|
|
2111
|
+
ui.startSession(workspaceId, {});
|
|
2112
|
+
} catch (error) {
|
|
2113
|
+
console.warn('[dsh-sidebar-row-menu] new session failed:', error);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
},
|
|
2117
|
+
});
|
|
2118
|
+
|
|
2119
|
+
const apps = appsSnapshot();
|
|
2120
|
+
const withApps = itemsSnapshot().openWith !== false;
|
|
2121
|
+
const pinned = withApps ? effectivePinned(apps.ids).filter((id) => childOn(id)) : [];
|
|
2122
|
+
pinned.forEach((id, index) => {
|
|
2123
|
+
items.push({
|
|
2124
|
+
id: `pinned-${id}`,
|
|
2125
|
+
group: 'pinned',
|
|
2126
|
+
separatorBefore: index === 0,
|
|
2127
|
+
label: t('openWithApp', { name: appLabel(id, t) }),
|
|
2128
|
+
icon: { key: 'sidebar-row-menu.app', src: ICON_ROUTE + encodeURIComponent(id) },
|
|
2129
|
+
run: () => openAppAt(id, cwd, t),
|
|
2130
|
+
});
|
|
2131
|
+
});
|
|
2132
|
+
const enabledApps = withApps ? apps.ids.filter((id) => childOn(id)) : [];
|
|
2133
|
+
const pinnedSet = new Set(pinned);
|
|
2134
|
+
const submenuApps = enabledApps.filter((id) => !pinnedSet.has(id));
|
|
2135
|
+
if (withApps) ensureApps();
|
|
2136
|
+
if (withApps && submenuApps.length > 0) {
|
|
2137
|
+
items.push({
|
|
2138
|
+
id: 'open-with',
|
|
2139
|
+
group: 'open-with',
|
|
2140
|
+
separatorBefore: pinned.length === 0,
|
|
2141
|
+
label: t('openWith'),
|
|
2142
|
+
children: submenuApps.map((id) => ({
|
|
2143
|
+
id: `app-${id}`,
|
|
2144
|
+
label: appLabel(id, t),
|
|
2145
|
+
icon: { key: 'sidebar-row-menu.app', src: ICON_ROUTE + encodeURIComponent(id) },
|
|
2146
|
+
run: () => openAppAt(id, cwd, t),
|
|
2147
|
+
})),
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
items.push({
|
|
2152
|
+
id: 'copy-path',
|
|
2153
|
+
group: 'copy',
|
|
2154
|
+
separatorBefore: pinned.length === 0 && submenuApps.length === 0,
|
|
2155
|
+
label: t('copyPath'),
|
|
2156
|
+
icon: { key: 'sidebar-row-menu.copy' },
|
|
2157
|
+
run: () => {
|
|
2158
|
+
if (cwd === '') {
|
|
2159
|
+
toast(t('noPath'));
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
copyText(cwd, t);
|
|
2163
|
+
},
|
|
2164
|
+
});
|
|
2165
|
+
|
|
2166
|
+
const tabs = serviceOf(rootCtx, 'projectTabs') ?? globalThis.__DSH_PROJECT_TABS__ ?? null;
|
|
2167
|
+
if (tabs !== null && typeof tabs?.openWorkspaceInTab === 'function') {
|
|
2168
|
+
items.push({
|
|
2169
|
+
id: 'tab',
|
|
2170
|
+
group: 'tab',
|
|
2171
|
+
label: t('openInTab'),
|
|
2172
|
+
icon: { key: 'sidebar-row-menu.tab' },
|
|
2173
|
+
run: () => {
|
|
2174
|
+
Promise.resolve().then(() => tabs.openWorkspaceInTab(workspaceId)).catch((reason) => {
|
|
2175
|
+
console.warn('[dsh-sidebar-row-menu] open in tab failed:', reason);
|
|
2176
|
+
toast(t('actionFailed'));
|
|
2177
|
+
});
|
|
2178
|
+
},
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
items.push({
|
|
2183
|
+
id: 'delete-sessions',
|
|
2184
|
+
group: 'delete',
|
|
2185
|
+
danger: true,
|
|
2186
|
+
separatorBefore: false,
|
|
2187
|
+
label: t('deleteWorkspaceSessions'),
|
|
2188
|
+
icon: { key: 'sidebar-row-menu.delete' },
|
|
2189
|
+
run: () => {
|
|
2190
|
+
const title = typeof value?.displayTitle === 'string' && value.displayTitle !== '' ? value.displayTitle : cwd;
|
|
2191
|
+
requestDelete({
|
|
2192
|
+
mode: 'workspace',
|
|
2193
|
+
workspaceId,
|
|
2194
|
+
sessionIds: workspaceSessionIds(workspaceId),
|
|
2195
|
+
displayTitle: title,
|
|
2196
|
+
});
|
|
2197
|
+
},
|
|
2198
|
+
});
|
|
2199
|
+
|
|
2200
|
+
return items;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
/** 宿主只按 provider.order 排序,同组顺序即数组顺序;故每组单独注册。 */
|
|
2204
|
+
const WORKSPACE_MENU_GROUPS = [
|
|
2205
|
+
{ key: 'new', order: 90 },
|
|
2206
|
+
{ key: 'pinned', order: 150 },
|
|
2207
|
+
{ key: 'open-with', order: 180 },
|
|
2208
|
+
{ key: 'copy', order: 250 },
|
|
2209
|
+
{ key: 'tab', order: 260 },
|
|
2210
|
+
{ key: 'delete', order: 310 },
|
|
2211
|
+
];
|
|
2212
|
+
|
|
2213
|
+
/** 注册工作区动作;服务可能晚于插件 apply 出现,带有限重试。 */
|
|
2214
|
+
function registerWorkspaceContextMenu(ctx) {
|
|
2215
|
+
let disposers = null;
|
|
2216
|
+
let icons = null;
|
|
2217
|
+
let timer = 0;
|
|
2218
|
+
let attempts = 0;
|
|
2219
|
+
|
|
2220
|
+
const registerIcons = () => {
|
|
2221
|
+
if (ctx.slots === null || ctx.slots === undefined) return;
|
|
2222
|
+
icons = ctx.slots.inject('contextMenu.icon', function* () {
|
|
2223
|
+
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.new' }, IconNewChatOutlineRegular);
|
|
2224
|
+
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.copy' }, IconCopyOutlineRegular);
|
|
2225
|
+
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.tab' }, IconProjectAddOutlineRegular);
|
|
2226
|
+
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.delete' }, IconTrashOutlineRegular);
|
|
2227
|
+
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.app' }, ContextMenuAppIcon);
|
|
2228
|
+
});
|
|
2229
|
+
};
|
|
2230
|
+
|
|
2231
|
+
const register = () => {
|
|
2232
|
+
timer = 0;
|
|
2233
|
+
if (disposers !== null) return;
|
|
2234
|
+
const menus = contextMenusOf(ctx);
|
|
2235
|
+
if (menus === null) {
|
|
2236
|
+
if (attempts >= 40 || typeof setTimeout !== 'function') return;
|
|
2237
|
+
attempts += 1;
|
|
2238
|
+
timer = setTimeout(register, 250);
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
registerIcons();
|
|
2242
|
+
const registered = [];
|
|
2243
|
+
for (const group of WORKSPACE_MENU_GROUPS) {
|
|
2244
|
+
try {
|
|
2245
|
+
registered.push(menus.register({
|
|
2246
|
+
id: `sidebar-row-menu.workspace.${group.key}`,
|
|
2247
|
+
target: 'workspace',
|
|
2248
|
+
order: group.order,
|
|
2249
|
+
observe: () => [appsSource],
|
|
2250
|
+
resolve: (target) => workspaceMenuActions(ctx, target, localeOf(ctx))
|
|
2251
|
+
.filter((action) => action.group === group.key)
|
|
2252
|
+
.map(({ group: _group, ...action }) => action),
|
|
2253
|
+
}));
|
|
2254
|
+
} catch (error) {
|
|
2255
|
+
console.warn(`[dsh-sidebar-row-menu] workspace context registration failed (${group.key}):`, error);
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
if (registered.length > 0) disposers = registered;
|
|
2259
|
+
};
|
|
2260
|
+
|
|
2261
|
+
register();
|
|
2262
|
+
return () => {
|
|
2263
|
+
if (timer !== 0) clearTimeout(timer);
|
|
2264
|
+
for (const dispose of disposers ?? []) {
|
|
2265
|
+
try {
|
|
2266
|
+
dispose?.();
|
|
2267
|
+
} catch {
|
|
2268
|
+
/* 注册已随插件生命周期撤销 */
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
try {
|
|
2272
|
+
icons?.();
|
|
2273
|
+
} catch {
|
|
2274
|
+
/* icon 条目可能未注册成功 */
|
|
2275
|
+
}
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
|
|
1964
2279
|
/** 删除确认框挂到 shell.overlay,避免菜单关闭时被一起卸载。 */
|
|
1965
2280
|
function registerDeleteDialog(ctx) {
|
|
1966
2281
|
return ctx.slots.inject('shell.overlay', function* () {
|
|
@@ -2042,7 +2357,6 @@ window.__ModuleLoader__.load({
|
|
|
2042
2357
|
/* ignore */
|
|
2043
2358
|
}
|
|
2044
2359
|
}
|
|
2045
|
-
rightClickMenuUntil = 0;
|
|
2046
2360
|
}
|
|
2047
2361
|
|
|
2048
2362
|
/**
|
|
@@ -2140,9 +2454,8 @@ window.__ModuleLoader__.load({
|
|
|
2140
2454
|
}
|
|
2141
2455
|
|
|
2142
2456
|
/**
|
|
2143
|
-
*
|
|
2144
|
-
*
|
|
2145
|
-
* 本插件自己的三条只在 rightClickMenuUntil 窗口内挂载。
|
|
2457
|
+
* 右键会话行时在指针处展开该行自带的菜单(0.2.1 的旧内置 Menu)。
|
|
2458
|
+
* 0.2.2 起宿主自己提供 contextmenu 触发器,不再走这里。
|
|
2146
2459
|
*/
|
|
2147
2460
|
function openSessionRowMenu(row, x, y) {
|
|
2148
2461
|
const trigger = typeof row.querySelector === 'function'
|
|
@@ -2151,12 +2464,8 @@ window.__ModuleLoader__.load({
|
|
|
2151
2464
|
const fiber = fiberOf(row);
|
|
2152
2465
|
const menu = fiber === null ? null : findMenuProps(fiber.child, 0, { count: 0 });
|
|
2153
2466
|
if (menu !== null && menu.open === true) return;
|
|
2154
|
-
// 先还原旧锚点(会清掉上一轮的窗口),再设置本轮窗口并挂新锚点。
|
|
2155
2467
|
patchAnchorRect(trigger, x, y);
|
|
2156
|
-
|
|
2157
|
-
window.setTimeout(() => {
|
|
2158
|
-
rightClickMenuUntil = 0;
|
|
2159
|
-
}, 600);
|
|
2468
|
+
markRightClickMenu();
|
|
2160
2469
|
primeRowActionPointer(trigger);
|
|
2161
2470
|
if (menu !== null) {
|
|
2162
2471
|
menu.anchor.props.onClick();
|
|
@@ -2221,6 +2530,17 @@ window.__ModuleLoader__.load({
|
|
|
2221
2530
|
|
|
2222
2531
|
/* -------------------------------------------------------------- 安装 */
|
|
2223
2532
|
|
|
2533
|
+
/**
|
|
2534
|
+
* 0.2.2 的会话行自己带 contextmenu 触发器(宿主 getTriggerProps 展开的
|
|
2535
|
+
* onContextMenu),它会把同一份菜单开在指针处。0.2.1 的行没有这个处理器,
|
|
2536
|
+
* 只能由插件代点行尾「…」再改锚点。
|
|
2537
|
+
*/
|
|
2538
|
+
function hasNativeRowMenu(row) {
|
|
2539
|
+
const fiber = fiberOf(row);
|
|
2540
|
+
const props = fiber === null ? null : fiber.memoizedProps;
|
|
2541
|
+
return props !== null && props !== undefined && typeof props.onContextMenu === 'function';
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2224
2544
|
/** 全局 contextmenu 拦截:只有命中工作区 / 会话行才接管。 */
|
|
2225
2545
|
function onContextMenu(event) {
|
|
2226
2546
|
if (event.defaultPrevented) return;
|
|
@@ -2250,18 +2570,41 @@ window.__ModuleLoader__.load({
|
|
|
2250
2570
|
}
|
|
2251
2571
|
|
|
2252
2572
|
if (resolveSessionRow(start) === null) return;
|
|
2573
|
+
// 宿主自己会处理右键(0.2.2):只打手势标记,菜单交给宿主开在指针处。
|
|
2574
|
+
// 这样既不用改锚点、也不用代点「…」,少两层时序假设。
|
|
2575
|
+
if (hasNativeRowMenu(row)) {
|
|
2576
|
+
markRightClickMenu();
|
|
2577
|
+
primeRowActionPointer(typeof row.querySelector === 'function'
|
|
2578
|
+
? row.querySelector('[class*="rowActions"] button')
|
|
2579
|
+
: null);
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2253
2582
|
event.preventDefault();
|
|
2254
2583
|
event.stopPropagation();
|
|
2255
2584
|
event.stopImmediatePropagation?.();
|
|
2256
2585
|
openSessionRowMenu(row, event.clientX, event.clientY);
|
|
2257
2586
|
}
|
|
2258
2587
|
|
|
2588
|
+
/** 行尾「…」按下时先收掉右键标记:那个菜单不该出现右键条目。 */
|
|
2589
|
+
function onRowActionsPointerDown(event) {
|
|
2590
|
+
// 只认真实输入:插件自己补发的 pointerdown(primeRowActionPointer)不算手势。
|
|
2591
|
+
if (event.isTrusted === false) return;
|
|
2592
|
+
const target = event.target;
|
|
2593
|
+
if (target === null || target === undefined) return;
|
|
2594
|
+
const start = target.nodeType === 3 ? target.parentElement : target;
|
|
2595
|
+
if (start === null || start === undefined || typeof start.closest !== 'function') return;
|
|
2596
|
+
if (start.closest('[class*="rowActions"]') === null) return;
|
|
2597
|
+
clearRightClickMark();
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2259
2600
|
function install() {
|
|
2260
2601
|
window.addEventListener('contextmenu', onContextMenu, true);
|
|
2602
|
+
document.addEventListener('pointerdown', onRowActionsPointerDown, true);
|
|
2261
2603
|
document.addEventListener('click', onWorkspaceEllipsisClick, true);
|
|
2262
2604
|
console.info('[dsh-sidebar-row-menu] installed');
|
|
2263
2605
|
return () => {
|
|
2264
2606
|
window.removeEventListener('contextmenu', onContextMenu, true);
|
|
2607
|
+
document.removeEventListener('pointerdown', onRowActionsPointerDown, true);
|
|
2265
2608
|
document.removeEventListener('click', onWorkspaceEllipsisClick, true);
|
|
2266
2609
|
closeMenu();
|
|
2267
2610
|
closeToast();
|
|
@@ -2288,10 +2631,12 @@ window.__ModuleLoader__.load({
|
|
|
2288
2631
|
if (typeof ctx.effect === 'function') {
|
|
2289
2632
|
ctx.effect(() => installStyles(), 'sidebar-row-menu: styles');
|
|
2290
2633
|
}
|
|
2634
|
+
const stopWorkspaceMenu = registerWorkspaceContextMenu(ctx);
|
|
2291
2635
|
const teardown = install();
|
|
2292
2636
|
if (ctx !== null && typeof ctx.effect === 'function') {
|
|
2293
2637
|
ctx.effect(() => () => {
|
|
2294
2638
|
rootCtx = null;
|
|
2639
|
+
stopWorkspaceMenu();
|
|
2295
2640
|
teardown();
|
|
2296
2641
|
}, 'sidebar-row-menu: row context menus');
|
|
2297
2642
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-sidebar-row-menu",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "DSH sidebar
|
|
5
|
+
"description": "Adds open-with and cleanup entries to the DSH sidebar context menus: interleaved with the built-in workspace menu and extending the session menu at the pointer on DSH 0.2.2+, with a self-drawn workspace menu on 0.2.1.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"deepseek",
|
|
8
8
|
"dsh",
|