dsh-sidebar-row-menu 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,106 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.6.0 — 2026-11-05
4
+
5
+ ### Added
6
+
7
+ - **Workspace entries can opt into the native "…" menu on DSH 0.2.2.** The
8
+ workspace-row entries (New session, pinned app, Open with, Copy workspace path,
9
+ Open in new tab, Delete workspace and all sessions) are now right-click only by
10
+ default, matching the session rows: a provider answers only while a right-click
11
+ gesture is active. Enabling the matching switch under Settings → "Also show in the
12
+ "…" menu → workspace" makes it skip that check, so the entry also lands in the
13
+ host's "…" menu.
14
+
15
+ ### Fixed
16
+
17
+ - **"Open with" keeps its icon on DSH 0.2.2.** The entry is contributed to the
18
+ host menu as data, so it had no icon of its own; it now registers an
19
+ `contextMenu.icon` slot (`sidebar-row-menu.open-with`) and points the action at
20
+ it, matching the self-drawn 0.2.1 menu.
21
+ - **Menu separators stay visible at 125–150% display scaling.** The host draws
22
+ 0.5px hairlines; at fractional device pixel ratios one of them was painted as a
23
+ ~0.4px smear and read as missing. A `min-resolution: 1.2dppx` media query now
24
+ thickens menu separators (and the 0.2.1 `.dsrm-sep`) to 1px in that range, and
25
+ leaves 1x and 2x displays untouched.
26
+ - **The workspace menu follows the session menu's grouping.** On 0.2.2 the entries
27
+ were interleaved as right-click group → pinned/open-with → copy path, so the
28
+ separator before "Open with" landed in a different place than in the session
29
+ menu. The workspace providers now use the session order — row actions, then
30
+ Copy path/Reveal, then the open-with group, then Delete — so both menus split
31
+ into the same three blocks.
32
+ - **"Open with" shows its submenu affordance again.** Host menu items draw no
33
+ caret for submenu parents. The action's label is rendered by the host as an
34
+ element, so the workspace entry now passes a label that lays the text out with
35
+ a right-aligned chevron, matching the session menu's arrow instead of a "›"
36
+ glued to the text (the 0.2.1 self-drawn menu keeps its own arrow icon).
37
+
38
+ ### Changed
39
+
40
+ - **The workspace-row switches are no longer hidden on 0.2.2.** The section note now
41
+ explains that 0.2.2 shares one host menu between right-click and the `…` button
42
+ while the plugin's entries still follow the gesture, and that the switches opt
43
+ individual entries into that shared menu. This reverses the 1.5.0 decision to hide
44
+ the group.
45
+
46
+ ### 中文摘要
47
+
48
+ - **0.2.2 上工作区条目默认只出现在右键菜单**:工作区 provider 解析时检查同一枚
49
+ 右键手势标记,行尾「…」的 `pointerdown` 会先清标,所以「…」菜单保持原生内容。
50
+ - **工作区行可以在设置里「同时放进「…」菜单」**:逐项开启后 provider 跳过手势判断,
51
+ 对应条目也会进入宿主的「…」菜单;0.2.2 上这组开关不再隐藏,说明文字改成描述
52
+ 共用菜单 + 手势区分的真实行为。0.2.1 仍由插件接管「…」按钮自绘。
53
+ - **0.2.2 的「打开方式」恢复图标**:数据贡献的条目本身没有图标,现在注册
54
+ `contextMenu.icon`(`sidebar-row-menu.open-with`)并由动作引用,与 0.2.1 自绘
55
+ 菜单一致。
56
+ - **125%–150% 缩放下菜单分隔线可见**:宿主用的是 0.5px 细线,在分数像素比下会被
57
+ 摊成约 0.4px 而像「消失」;现在该区间内分隔线(含 0.2.1 的 `.dsrm-sep`)补到
58
+ 1px,1x 与 2x 显示不变。
59
+ - **工作区菜单改用会话菜单的分组规则**:原来「打开方式」组夹在复制路径之前,分隔线
60
+ 落点与会话菜单不一致;现在顺序为行内动作 → 复制路径/在文件夹显示 → 打开方式组 →
61
+ 删除,两份菜单都分成同样三块。
62
+ - **「打开方式」恢复子菜单箭头**:宿主不给子菜单父项画箭头,但 label 是当元素渲染的,
63
+ 所以工作区条目改成「文字 + 右对齐箭头」的 label,与会话菜单的箭头位置一致(不再是
64
+ 贴在文字后面的「›」);0.2.1 自绘菜单仍用自己的箭头图标。
65
+
66
+ ## 1.5.1 — 2026-11-05
67
+
68
+ ### Fixed
69
+
70
+ - **Session-row right-click entries appear every time on DSH 0.2.2.** The entries
71
+ (pinned app, Open with, permanent delete, delete archived) used to be gated by a
72
+ 400 ms time window measured from the `contextmenu` event. 0.2.2 mounts the menu
73
+ asynchronously and, when a menu is already open, tears the previous panel down
74
+ before mounting the new one — that teardown reset the window, so the next menu
75
+ could come up without the plugin entries. The gate is now a gesture mark that is
76
+ cleared only once the menu panel has actually mounted, so right-click keeps its
77
+ entries while the row's `…` button still opens the untouched native menu.
78
+ On 0.2.2 the plugin also stops intercepting `contextmenu` and forwarding it as an
79
+ ellipsis click: the row already carries the host trigger, so the host opens its
80
+ own menu at the pointer (no anchor rewriting, no synthetic click).
81
+
82
+ ### Changed
83
+
84
+ - **Docs and registry metadata describe the 0.2.2 behavior.** README (both languages),
85
+ the npm description and the marketplace entry no longer present the plugin as a
86
+ self-drawn sidebar menu. They now state that DSH 0.2.2 renders the menus and the
87
+ plugin contributes entries (workspace items interleaved by `order`, session rows
88
+ extended in the host menu opened at the pointer), and that 0.2.1 keeps the
89
+ self-drawn workspace menu.
90
+
91
+ ### 中文摘要
92
+
93
+ - **修复 0.2.2 上会话行右键菜单「有时没有插件条目」**:条目原来用 `contextmenu`
94
+ 之后 400 ms 的时间窗口判断,而 0.2.2 的菜单是异步挂载的,替换已有菜单时还会
95
+ 先卸载旧面板再挂新面板,这次卸载会把窗口清掉,于是下一次菜单可能不带插件条目。
96
+ 改为「右键手势标记」:只有菜单面板真正挂载后才收尾,右键稳定带条目,行尾「…」
97
+ 打开的菜单仍然保持原生内容。
98
+ 0.2.2 上也不再拦截 `contextmenu` 后转成「…」点击:行本身已带宿主触发器,直接
99
+ 由宿主把菜单开在指针处(不改锚点、不补发点击)。
100
+ - **文档与市场文案改为描述 0.2.2 的真实行为**:README(中英)、npm 描述与市场条目
101
+ 不再写成「插件自绘侧栏菜单」,而是说明 0.2.2 由宿主渲染菜单、插件按 `order` 贡献
102
+ 条目(工作区项混排、会话行扩展宿主开在指针处的菜单),0.2.1 上保留自绘工作区菜单。
103
+
3
104
  ## 1.5.0 — 2026-11-05
4
105
 
5
106
  ### Added
package/README.en.md CHANGED
@@ -9,42 +9,64 @@
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 (a reimplementation of
13
- `dsh-sidebar-context-menu`'s capabilities);
14
- - **Session rows nested under a workspace** — right-click opens that row's native
15
- "…" menu at the pointer, with the plugin's own entries appended.
16
-
17
- The workspace menu is drawn with DSH's official `MenuSurface` + `MenuItemButton`
18
- (the official `Menu` cannot nest submenus), so rows, icons, danger color and group
19
- separators reuse the built-in look, and keyboard navigation plus click-outside
20
- dismissal match the native menus. Session menu content comes from the
21
- `sidebar.workspaces.session.menu.item` slot — built-in actions and other plugins'
22
- actions are all there. This plugin only appends its own entries; it does not copy
23
- or hardcode any built-in action.
12
+ - **Workspace rows** — right-click opens a menu (since DSH 0.2.2 it is part of the
13
+ host's native menu, with the plugin's items interleaved by `order`);
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.
24
29
 
25
30
  > The "…" button menu stays exactly as DSH ships it. The plugin's entries appear
26
- > only when the menu is opened by right-click. A right-click menu does not
27
- > close when the pointer leaves it (same as the workspace menu); it closes only on
28
- > an outside click, an item selection, or Escape.
31
+ > only when the menu is opened by right-click — on 0.2.2 that holds for workspace
32
+ > and session rows alike, and each entry can be opted into the "…" menu in Settings.
33
+ > On 0.2.2 both gestures share
34
+ > one host menu, so the plugin tells them apart by the gesture itself: a right-click
35
+ > sets a mark that is released once the menu panel has actually mounted, while the
36
+ > pointerdown on the "…" button clears it immediately. A right-click menu does not
37
+ > close when the pointer leaves it; it closes only on an outside click, an item
38
+ > selection, or Escape.
29
39
 
30
40
  ## Features
31
41
 
32
42
  ### Workspace row
33
43
 
44
+ On 0.2.2 the host renders this menu, and the plugin's items interleave with the
45
+ built-in Rename (100), Reveal in file manager (200) and Delete workspace (300),
46
+ using the same grouping rules as the session menu: New session 90, Open in new
47
+ tab 110, Copy workspace path 150, pinned app 210, Open with 220, Delete workspace
48
+ and all sessions 310. These items are right-click only by default; the row's "…"
49
+ menu keeps its native content unless you opt entries in under Settings → "Also
50
+ show in the "…" menu → workspace". On 0.2.1 the plugin draws the whole menu and
51
+ runs the official dialogs for Rename and Delete workspace.
52
+
34
53
  | Item | Behavior |
35
54
  | --- | --- |
36
55
  | New session | Expands the workspace and starts a session (same as the row's `+`) |
37
- | Rename | Opens the official "Rename workspace" dialog |
38
- | 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
- | Open with | A submenu of external apps (editors, terminals, file managers) sharing the session menu's catalog, pinning and toggles; hover expands it |
40
- | Copy workspace path | Copies the directory path |
56
+ | Rename (built-in) | Opens the official "Rename workspace" dialog; on 0.2.1 the plugin renders this item itself |
41
57
  | 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" |
58
+ | Copy workspace path | Copies the directory path |
59
+ | 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 |
60
+ | 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 |
61
+ | Open with | A submenu of external apps (editors, terminals, file managers) sharing the session menu's catalog, pinning and toggles; hover expands it. Host menu items draw no submenu caret, so the plugin renders the label as text plus a right-aligned chevron, matching the session menu; the 0.2.1 self-drawn menu uses an arrow icon |
62
+ | 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
63
  | 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
64
 
45
65
  ### Session row
46
66
 
47
- Right-click does not build a second menu; it opens the row's own "…" menu:
67
+ Right-click does not build a second menu: on 0.2.2 the host opens the row's native
68
+ menu at the pointer, and on 0.2.1 the plugin triggers the row's "…" menu and moves
69
+ its anchor to the pointer. Either way the content comes from the same slot:
48
70
 
49
71
  | Source | Entries |
50
72
  | --- | --- |
@@ -69,17 +91,26 @@ unarchived sessions alone by default; the opt-in switch extends the single-sessi
69
91
  entry to them. Deletion never cascades into forked child sessions.
70
92
 
71
93
  These entries are **right-click only by default**: they do not mount when the "…"
72
- button opens the menu. A short-lived window flag is set before opening; outside that
73
- window the entries render `null`.
94
+ button opens the menu. There is no time window any more; the plugin sets a
95
+ right-click gesture mark and releases it only once the menu panel has actually
96
+ appeared (0.2.2 mounts the menu asynchronously), and the pointerdown on the "…"
97
+ button clears it at once. While the mark is live the entries render normally;
98
+ otherwise they render `null`.
74
99
 
75
100
  To use them from the row's "…" button, enable them one by one under Settings →
76
- "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 row's "…"
78
- button and render the official "Rename / Delete workspace" items alongside the
79
- enabled plugin entries. With everything off, the "…" button behaves exactly like
80
- DSH's own.
101
+ "Row menu" → "Also show in the "…" menu":
102
+
103
+ - session entries mount into the native "…" menu;
104
+ - workspace entries on 0.2.2: the host menu is data-driven, so an enabled entry skips
105
+ the gesture check and appears in the "…" menu; with everything off that menu keeps
106
+ only the native items;
107
+ - workspace entries on 0.2.1: enabling any of them makes this plugin take over the
108
+ row's "…" button and render the official "Rename / Delete workspace" items
109
+ alongside the enabled plugin entries.
110
+
111
+ With everything off, the "…" button behaves exactly like DSH's own.
81
112
 
82
- Blank draft sessions have no native "…" menu and are left alone. Rows that are
113
+ Blank draft sessions have no right-click menu and are left alone. Rows that are
83
114
  neither a workspace nor a session (e.g. "Show more") are not intercepted.
84
115
 
85
116
  ### Settings
@@ -93,15 +124,16 @@ Settings → "Row menu" toggles every new entry:
93
124
  | Delete session | Hides the single-session delete entry in session menus |
94
125
  | Allow deleting unarchived sessions | Off by default; also shows "Delete session permanently" on unarchived rows. Requires the previous switch |
95
126
  | 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 |
127
+ | 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. On 0.2.1 enabling any of them makes this plugin render the "…" menu |
97
128
  | 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
129
 
99
130
  The "Also show in the "…" menu" switches are all off by default; until one is
100
131
  enabled the "…" menu is exactly DSH's own.
101
132
 
102
- On DSH 0.2.2 and later, the workspace row's right-click and "…" already open the
103
- same host menu, so the plugin entries always show up there; the "Also show in the
104
- "…" menu → workspace" switches only affect 0.2.1 and older hosts.
133
+ On DSH 0.2.2 and later the "…" button and right-click share one host menu, yet the
134
+ plugin's entries still appear only on right-click; flipping a switch also puts that
135
+ entry into the "…" menu. On 0.2.1 workspace rows have no host menu to contribute to,
136
+ so the plugin takes over the "…" button and draws the menu itself.
105
137
 
106
138
  Switches live in browser `localStorage` (`dsh.sidebar-row-menu.items.v1`,
107
139
  `dsh.sidebar-row-menu.open-with.v1`, `dsh.sidebar-row-menu.open-with.pinned.v1` and
@@ -332,46 +364,78 @@ row components are:
332
364
 
333
365
  The plugin reads `__reactFiber$` from the event target during the capture phase of
334
366
  `contextmenu`, then walks up to 60 `return` links looking for matching
335
- `memoizedProps`. If nothing matches it does not intercept at all.
367
+ `memoizedProps`. If nothing matches it does not intercept at all. On 0.2.2 the fiber
368
+ is also used to tell whether the row already carries the host's own context menu
369
+ trigger (`memoizedProps.onContextMenu` is a function): if it does, the host opens the
370
+ menu at the pointer by itself and the plugin only drops a gesture mark, never
371
+ touching the event.
336
372
 
337
373
  ### Why session actions are not hardcoded
338
374
 
339
- The native "…" menu is not a hardcoded button list: it is
375
+ The native session menu is not a hardcoded button list: it is
340
376
  `renderSlot('sidebar.workspaces.session.menu.item', ...)`. Fork, pin, archive and
341
377
  third-party actions are all slot entries. Slot ownership is enforced by the
342
378
  renderer: an entry may only render child slots it declared itself, so a third
343
379
  party cannot re-layout someone else's slot.
344
380
 
345
- Therefore a session right-click does exactly one thing: find the `Menu` in the
346
- row's fiber subtree and trigger its anchor (the ellipsis button). The same slot
347
- content opens. The plugin's own actions are registered through
348
- `ctx.slots.inject` + `ctx.slots.register`, so:
381
+ So a session right-click never copies menu content:
382
+
383
+ - 0.2.2: the row component carries the host's `getTriggerProps` (its `onContextMenu`
384
+ hands `kind:'session'` to `ctx.contextMenus`), so the host owns the gesture and the
385
+ plugin does not even need to find a `Menu` or click a button;
386
+ - 0.2.1: find the `Menu` in the row's fiber subtree and trigger its anchor (the
387
+ ellipsis button), which opens the same slot content.
388
+
389
+ The plugin's own actions are registered through `ctx.slots.inject` +
390
+ `ctx.slots.register`, so:
349
391
 
350
392
  - they do not depend on built-in fork/pin entries existing;
351
393
  - new entries from DSH or other plugins show up automatically;
352
394
  - no built-in action is copied or hardcoded.
353
395
 
354
- Workspace rows have no such slot, so their menu is still composed by this plugin.
355
- The official `Menu` accepts only a flat `items` list and cannot express submenus, so
356
- the workspace menu is drawn with `MenuSurface` + `MenuItemButton`: rows, icons,
357
- danger styling and separators reuse the official controls.
396
+ Workspace rows have no such slot on 0.2.1, so their menu is composed by this plugin.
397
+ Since 0.2.2 the host provides `ctx.contextMenus` and the workspace items become data
398
+ contributions — again just "adding its own items to the host's menu". The official
399
+ `Menu` accepts only a flat `items` list and cannot express submenus, so the 0.2.1
400
+ workspace menu is drawn with `MenuSurface` + `MenuItemButton`: rows, icons, danger
401
+ styling and separators reuse the official controls.
358
402
 
359
403
  ### Why the session menu appears at the pointer
360
404
 
361
- The official `Menu` anchors to the ellipsis button, i.e. to the right of it. On
362
- right-click the plugin temporarily points that button's (and two ancestors')
405
+ On 0.2.2 there is nothing to do: the host's `onContextMenu` passes `clientX /
406
+ clientY` to `ctx.contextMenus.open()`, and the panel renders at the pointer.
407
+
408
+ On 0.2.1 the official `Menu` anchors to the ellipsis button, i.e. to the right of it.
409
+ On right-click the plugin temporarily points that button's (and two ancestors')
363
410
  `getBoundingClientRect` at the pointer as a `0×0` rect. `Menu` re-reads the anchor
364
411
  rect every frame, so the panel lands next to the cursor. The original methods are
365
412
  restored as soon as the panel leaves `body`, so the button menu keeps its normal
366
413
  position.
367
414
 
368
- The "…" menu and the right-click menu share one `Menu` instance and one slot
369
- content; the only difference is the `rightClickMenuUntil` window — the plugin's
370
- entries mount only inside it, and the window is cleared when the menu closes.
415
+ ### Why the right-click entries never show in the "…" menu
416
+
417
+ On 0.2.2 both gestures open the same host menu, and React cannot tell where it came
418
+ from, so the plugin watches the gesture: a right-click sets an in-process mark, and a
419
+ `MutationObserver` waits for a new `[role="menu"]` panel to appear (0.2.2 mounts the
420
+ menu asynchronously, after every provider is ready); once the panel shows up the mark
421
+ is released 400 ms later, with a 3 s fallback in case another plugin swallows the
422
+ gesture. The `pointerdown` on the row's "…" button clears the mark immediately, and
423
+ only trusted input counts (`isTrusted`) — the synthetic `pointerdown` the plugin
424
+ re-dispatches is not a gesture.
425
+
426
+ Entries read the mark once while mounting, so a slow panel does not matter, and the
427
+ mark only lives until "the panel for this gesture appeared", never into the next one.
428
+
429
+ Workspace rows on 0.2.2 take a different path: the host menu is built from each
430
+ `ctx.contextMenus.register` provider's `resolve`, which has no React entry to read the
431
+ mark, so the provider checks the same mark while resolving. The "…" pointerdown
432
+ clears it before the host resolves, hence the entries show on right-click only; the
433
+ "Also show in the "…" menu → workspace" switches make the provider skip that check.
371
434
 
372
- ### Why the workspace menu is mounted imperatively
435
+ ### Why the 0.2.1 menu is mounted imperatively
373
436
 
374
- A contextmenu event is not part of React's render flow. The workspace menu creates
437
+ On 0.2.2 the host mounts every menu and the plugin creates no container. On 0.2.1 a
438
+ contextmenu event is not part of React's render flow, so the workspace menu creates
375
439
  a `0×0`, `z-index:2147483647` host on demand and `createRoot`s a self-drawn panel;
376
440
  after mount it measures once and clamps its corner into the viewport, then unmounts
377
441
  and removes the host on close. The session menu mounts nothing new; it only opens
@@ -426,15 +490,18 @@ The package declares its supported DSH range in `package.json` as
426
490
  incompatible installs, and the plugin market shows it as the supported version;
427
491
  0.3 and later need re-verification before the upper bound is raised.
428
492
 
429
- Built against DSH client `0.2.1-rc.5` (`ProjectRowItem`, `SessionNodeItem`,
430
- `Menu`). DSH 0.2.2 moved the sidebar context menus to data-driven
431
- `ctx.contextMenus`, and the plugin picks the path by host capability:
493
+ Built against DSH client `0.2.1-rc.5` and `0.2.2` (`ProjectRowItem`,
494
+ `SessionNodeItem`, `Menu`, `contextMenus`). DSH 0.2.2 moved the sidebar context
495
+ menus to a data-driven service, and the plugin picks the path by host capability:
432
496
 
433
497
  - **0.2.2+**: workspace entries are registered with `ctx.contextMenus.register`
434
- (New session 90, pinned app 150, Open with 180, Copy workspace path 250,
435
- Open in new tab 260, Delete workspace and all sessions 310), interleaved by
436
- order with the native Rename (100), Reveal (200) and Delete workspace (300).
437
- Right-click and the "…" button share the host's single menu.
498
+ (New session 90, Open in new tab 110, Copy workspace path 150, pinned app 210,
499
+ Open with 220, Delete workspace and all sessions 310), interleaved by
500
+ order with the native Rename (100), Reveal (200) and Delete workspace (300) and
501
+ grouped with the same separators as the session menu.
502
+ Right-click and the "…" button share the host's single menu; providers answer only
503
+ during a right-click gesture (or when the matching "Also show in the "…" menu"
504
+ switch is on).
438
505
  - **0.2.1**: no `contextMenus` service, so the plugin keeps drawing its own
439
506
  workspace menu.
440
507
  - **Session rows**: 0.2.2 still supports the
@@ -450,12 +517,14 @@ If a DSH upgrade changes those row components or the menu positioning:
450
517
  ## Known limitations
451
518
 
452
519
  - On 0.2.1 the plugin depends on the props shape of the built-in row components.
453
- From 0.2.2 the workspace entries use `contextMenus`; the session entries still
454
- rely on the legacy extension point. If DSH changes the corresponding
455
- implementation, right-click may silently do nothing.
456
- - Session right-click depends on the row's `Menu` staying in the fiber subtree. If
457
- it is not found, the plugin clicks the row-actions ellipsis button; failing that
458
- it gives up — it never builds a competing menu and never triggers row open.
520
+ From 0.2.2 the workspace entries use `contextMenus`, the session entries still
521
+ use the legacy extension point, and `memoizedProps.onContextMenu` decides whether
522
+ the host has taken the row over. If DSH changes that implementation, right-click
523
+ may silently do nothing.
524
+ - On 0.2.1 session right-click depends on the row's `Menu` staying in the fiber
525
+ subtree. If it is not found, the plugin clicks the row-actions ellipsis button;
526
+ failing that it gives up — it never builds a competing menu and never triggers row
527
+ open. On 0.2.2 no fallback is needed: the host is the trigger.
459
528
  - Permanent deletion is offered for **archived** sessions by default; enable
460
529
  "Allow deleting unarchived sessions" to delete unarchived sessions from their
461
530
  right-click menu as well. Deletion does not cascade into forked child sessions —
package/README.md CHANGED
@@ -9,36 +9,52 @@
9
9
 
10
10
  给 [DeepSeek Harness](https://github.com/deepseek-ai)(DSH)左侧边栏加右键菜单:
11
11
 
12
- - **工作区行**:右键弹出菜单(复刻 `dsh-sidebar-context-menu` 的能力);
13
- - **工作区下层的会话行**:右键在指针处展开该行内置的「…」菜单,并追加本插件的条目。
12
+ - **工作区行**:右键弹出菜单(DSH 0.2.2 起并入宿主原生菜单,条目按 order 与内置项交错);
13
+ - **工作区下层的会话行**:右键在指针处展开该行自己的菜单,并追加本插件的条目。
14
14
 
15
- 工作区菜单用 DSH 官方的 `MenuSurface` + `MenuItemButton` 自绘(官方 `Menu` 的条目不支持
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
- > 点行尾「…」按钮看到的菜单保持 DSH 原样;本插件自己的条目只在右键展开时出现。
22
- > 右键展开的菜单不会因为鼠标移出而关闭(与工作区右键一致),只有点击外部、选中条目或按 Esc 才关闭。
25
+ > 点行尾「…」按钮看到的菜单保持 DSH 原样;本插件的条目默认只在右键展开时出现(0.2.2 上
26
+ > 工作区行与会话行都是如此,可以在设置里逐项开启)。
27
+ > 0.2.2 上右键与「…」共用同一份宿主菜单,插件按“右键手势”区分:右键时打标,等菜单面板
28
+ > 真正挂载后才收尾;点「…」的按下事件会立刻清标,所以那个菜单不带右键条目。
29
+ > 右键展开的菜单不会因为鼠标移出而关闭,只有点击外部、选中条目或按 Esc 才关闭。
23
30
 
24
31
  ## 功能
25
32
 
26
33
  ### 工作区行右键
27
34
 
35
+ 0.2.2 上这份菜单由宿主渲染,本插件条目与内置「重命名」(100)、「在文件夹显示」(200)、
36
+ 「删除工作区」(300)按 order 交错,分组规则与会话菜单一致:新会话 90、在新标签打开 110、
37
+ 复制工作区路径 150、置顶应用 210、打开方式 220、删除工作区及所有会话 310。这些条目默认
38
+ 只出现在右键菜单;行尾「…」里的菜单保持原生,想让它也带上插件条目,可在设置里逐项开启
39
+ 「同时放进「…」菜单 → 工作区」。0.2.1 上整份菜单由本插件自绘,「重命名」「删除工作区」
40
+ 由插件调用官方对话框。
41
+
28
42
  | 菜单项 | 行为 |
29
43
  | --- | --- |
30
44
  | 新会话 | 展开该工作区并新建会话(等价于行尾 `+`) |
31
- | 重命名 | 打开官方“重命名工作区”对话框 |
32
- | 置顶的打开方式 | 被置顶的应用直接显示在一级菜单(例如「用 Visual Studio Code 打开」),点击后用该应用打开工作区目录 |
33
- | 打开方式 | 二级菜单列出宿主提供的外部应用(编辑器、终端、文件管理器);与会话菜单共用同一套应用列表、置顶与开关设置,鼠标悬浮自动展开 |
34
- | 复制工作区路径 | 复制目录路径到剪贴板 |
45
+ | 重命名(内置) | 打开官方“重命名工作区”对话框;0.2.1 上由本插件代为渲染 |
35
46
  | 在新标签中打开 | 仅在装有 `dsh-project-tabs`(暴露 `openWorkspaceInTab`)时出现 |
36
- | 删除工作区 | 打开官方删除确认对话框;只删除工作区登记,会话保留并归入「未分组」 |
47
+ | 复制工作区路径 | 复制目录路径到剪贴板 |
48
+ | 在文件夹显示(内置) | 在资源管理器里打开工作区目录;0.2.2 由宿主提供,0.2.1 上不出现 |
49
+ | 置顶的打开方式 | 被置顶的应用直接显示在一级菜单(例如「用 Visual Studio Code 打开」),点击后用该应用打开工作区目录 |
50
+ | 打开方式 | 二级菜单列出宿主提供的外部应用(编辑器、终端、文件管理器);与会话菜单共用同一套应用列表、置顶与开关设置,鼠标悬浮自动展开。宿主的菜单项不画二级箭头,本插件把标签渲染成「文字 + 右对齐箭头」,与会话菜单同一位置;0.2.1 自绘菜单画的是箭头图标 |
51
+ | 删除工作区(内置) | 打开官方删除确认对话框;只删除工作区登记,会话保留并归入「未分组」。0.2.1 上由本插件代为渲染 |
37
52
  | 删除工作区及所有会话 | 先永久删除该工作区下的每个会话及其日志,全部成功后再删除工作区登记;任一会话删除失败则保留工作区,便于重试。工作区目录本身不删除 |
38
53
 
39
54
  ### 会话行右键
40
55
 
41
- 右键会话行不会另起一套菜单,而是展开该行自己的「…」菜单:
56
+ 右键会话行不会另起一套菜单:0.2.2 由宿主把该行的原生菜单开在指针处;0.2.1 由本插件触发行尾
57
+ 「…」菜单,并临时把它的锚点移到指针。两种情况下菜单内容都来自同一份 slot:
42
58
 
43
59
  | 来源 | 条目 |
44
60
  | --- | --- |
@@ -60,14 +76,19 @@
60
76
  一级菜单。两个删除入口由本插件自己的宿主路由完成,默认不会删除未归档会话;
61
77
  开启「允许删除未归档会话」后才会对普通会话开放。删除不会级联删除 fork 子会话。
62
78
 
63
- 这些条目默认是**右键专属**:点行尾「…」按钮时它们不挂载。实现上使用一个只在右键展开瞬间
64
- 有效的窗口标记,窗口外的渲染直接返回 `null`。
79
+ 这些条目默认是**右键专属**:点行尾「…」按钮时它们不挂载。实现上不再用时间窗口,而是
80
+ 「右键手势标记」:右键时打标,等菜单面板真正出现(0.2.2 的挂载是异步的)后才释放;点
81
+ 「…」的按下事件会立刻清标。标记有效期内的条目正常渲染,其余情况渲染直接返回 `null`。
65
82
 
66
83
  如果想直接使用行尾「…」菜单,可以在设置 →「行菜单」→「同时放进「…」菜单」里逐个开启:
67
- 会话条目会挂进原生「…」菜单;工作区行开启任意一项后,本插件会接管行尾「…」按钮,用同一套
68
- 菜单展示官方「重命名 / 删除工作区」加上已开启的插件条目。全部关闭时「…」按钮保持 DSH 原样。
69
84
 
70
- 草稿会话(还没有任何消息的空白会话)没有内置「…」菜单,本插件同样不接管。
85
+ - 会话条目:开启后挂进原生「…」菜单;
86
+ - 工作区条目(0.2.2):宿主菜单本身是数据驱动的,开启后 provider 解析时不再看手势,
87
+ 条目直接进入「…」菜单;全部关闭时「…」里只有原生项;
88
+ - 工作区条目(0.2.1):开启任意一项后,本插件接管行尾「…」按钮,用同一套菜单展示
89
+ 官方「重命名 / 删除工作区」加上已开启的插件条目。
90
+
91
+ 草稿会话(还没有任何消息的空白会话)没有右键菜单,本插件同样不接管。
71
92
  非工作区 / 非会话的侧边栏行(如“显示更多”)也不会被拦截。
72
93
 
73
94
  ### 设置
@@ -81,13 +102,14 @@
81
102
  | 永久删除会话 | 关闭后会话菜单里不再出现单条删除入口 |
82
103
  | 允许删除未归档会话 | 默认关闭;开启后未归档会话的右键菜单也会出现「永久删除会话」,需先开启上一项 |
83
104
  | 删除所有已归档会话 | 关闭后不再出现批量删除入口 |
84
- | 同时放进「…」菜单 → 工作区 | 逐项把工作区行右键条目加进行尾「…」菜单:新会话、打开方式、复制工作区路径、在新标签中打开、删除工作区及所有会话。开启任意一项后「…」按钮改由本插件渲染 |
105
+ | 同时放进「…」菜单 → 工作区 | 逐项把工作区行右键条目也放进行尾「…」菜单:新会话、打开方式、复制工作区路径、在新标签中打开、删除工作区及所有会话。0.2.1 上开启任意一项后「…」按钮改由本插件渲染 |
85
106
  | 同时放进「…」菜单 → 会话 | 逐项把会话行右键条目加进原生「…」菜单:打开方式、永久删除会话、删除所有已归档会话 |
86
107
 
87
108
  「同时放进「…」菜单」下的开关默认全部关闭,`…` 菜单在未开启时与 DSH 完全一致。
88
109
 
89
- DSH 0.2.2 起,工作区行的右键与「…」本身就是同一份宿主菜单,插件条目会一直出现在「…」里,
90
- 「同时放进「…」菜单 → 工作区」这组开关只影响 0.2.1 及更早的宿主。
110
+ DSH 0.2.2 起行尾「…」与右键共用同一份宿主菜单,但插件条目仍按手势只出现在右键菜单里;
111
+ 上表的开关打开后,对应条目才会同时进入「…」菜单。0.2.1 上工作区行没有宿主菜单可贡献,
112
+ 改由插件接管「…」按钮自绘。
91
113
 
92
114
  开关存在浏览器 `localStorage`(`dsh.sidebar-row-menu.items.v1`、
93
115
  `dsh.sidebar-row-menu.open-with.v1`、`dsh.sidebar-row-menu.open-with.pinned.v1` 与
@@ -263,8 +285,9 @@ npm run build # npm pack(prepack 会先跑 check),产出 .tgz
263
285
  测试覆盖宿主侧纯函数(路径校验、`vscode://file` 编码、loopback 判定、路由状态码)
264
286
  和客户端模块(模块 id、导出、依赖白名单、监听器安装/卸载、slot 注册表),并用伪造的
265
287
  `__reactFiber$`、DOM 与 `Menu` 宿主做集成测试:工作区右键的菜单项顺序、会话右键是否
266
- 展开内置菜单而不是自建菜单、草稿行是否被放过、复制工作区路径的 Toast 反馈都在断言里。
267
- 真实 fiber 与服务仍需在运行中的 GUI 里做人工验证。
288
+ 展开内置菜单而不是自建菜单、行自身带宿主 `onContextMenu` 时是否完全不拦截(只补一次
289
+ `pointerdown`)、点「…」后右键条目是否立刻消失、草稿行是否被放过、复制工作区路径的
290
+ Toast 反馈都在断言里。真实 fiber 与服务仍需在运行中的 GUI 里做人工验证。
268
291
 
269
292
  目录:
270
293
 
@@ -294,43 +317,67 @@ test/*.test.mjs node:test
294
317
  `{ id, title, blank, running, archived, pinned, updatedAt }`。
295
318
 
296
319
  插件在 `contextmenu` 捕获阶段从目标节点拿 `__reactFiber$`,向上最多 60 层找匹配的
297
- `memoizedProps`。找不到就完全不拦截,避免影响其他行和其他插件。
320
+ `memoizedProps`。找不到就完全不拦截,避免影响其他行和其他插件。0.2.2 上 fiber 还用来判断
321
+ 这一行是否天生带宿主右键触发器(`memoizedProps.onContextMenu` 是函数):有就说明宿主会把
322
+ 菜单开在指针处,插件只补一个手势标记,不再干预事件。
298
323
 
299
324
  ### 会话行为什么不硬编码
300
325
 
301
- 内置「…」菜单不是硬编码的按钮列表,而是
326
+ 内置会话菜单不是硬编码的按钮列表,而是
302
327
  `renderSlot('sidebar.workspaces.session.menu.item', ...)` 的结果;内置的分叉、置顶、
303
328
  归档,以及第三方插件的能力,都以 slot 条目形式注册。slot 的所有权由渲染器校验:
304
329
  每个条目只能渲染自己声明过的子 slot,第三方无法直接把手伸进别人的 slot 重新排版。
305
330
 
306
- 因此会话行右键只做一件事:在行组件的 fiber 子树里找到那个 `Menu`,触发它 anchor
307
- (省略号按钮)的点击,展开的自然是同一份 slot 内容。本插件自己的动作也通过
308
- `ctx.slots.inject` + `ctx.slots.register` 注册进去,所以:
331
+ 因此会话行右键从不复制菜单内容:
332
+
333
+ - 0.2.2:行组件自己带宿主的 `getTriggerProps`(`onContextMenu` 直接把 `kind:'session'`
334
+ 交给 `ctx.contextMenus`),右键由宿主处理,插件连「找 Menu、代点按钮」都不需要做;
335
+ - 0.2.1:在行组件的 fiber 子树里找到那个 `Menu`,触发它 anchor(省略号按钮)的点击,
336
+ 展开的自然是同一份 slot 内容。
337
+
338
+ 本插件自己的动作也通过 `ctx.slots.inject` + `ctx.slots.register` 注册进去,所以:
309
339
 
310
340
  - 不依赖「分叉」「置顶」这些内置条目是否存在;
311
341
  - 内置或别的插件以后新增条目,右键菜单自动出现;
312
342
  - 不复制、不硬编码任何内置动作。
313
343
 
314
- 工作区行没有这样的 slot,所以工作区菜单仍由本插件自己组项。官方 `Menu` 的
315
- `items` 只支持单层条目,无法表达子菜单,因此工作区菜单用 `MenuSurface` +
316
- `MenuItemButton` 自绘:行、图标、危险色和分隔线都复用官方控件,交互与官方菜单一致。
344
+ 工作区行在 0.2.1 上没有这样的 slot,所以菜单由本插件自己组项;0.2.2 起宿主提供
345
+ `ctx.contextMenus`,工作区条目改为数据贡献,同样只是“往宿主菜单里加自己的项”。
346
+ 官方 `Menu` 的 `items` 只支持单层条目,无法表达子菜单,因此 0.2.1 的工作区菜单用
347
+ `MenuSurface` + `MenuItemButton` 自绘:行、图标、危险色和分隔线都复用官方控件。
317
348
 
318
349
  ### 为什么会话菜单能贴着鼠标
319
350
 
320
- 官方 `Menu` 的 `anchor` 是那个省略号按钮,默认位置就在按钮右侧。右键时插件临时把
351
+ 0.2.2 上什么都不用做:宿主的 `onContextMenu` 直接把 `clientX / clientY` 交给
352
+ `ctx.contextMenus.open()`,面板就渲染在指针处。
353
+
354
+ 0.2.1 上官方 `Menu` 的 `anchor` 是那个省略号按钮,默认位置就在按钮右侧。右键时插件临时把
321
355
  该按钮及其两层祖先的 `getBoundingClientRect` 指到指针位置(`0×0` 矩形),官方 `Menu`
322
356
  每帧读取锚点 rect 重新定位,于是面板出现在鼠标旁;面板从 `body` 移除时立即还原原始
323
357
  方法,不影响按钮菜单的默认位置。
324
358
 
325
- 「…」按钮菜单与右键菜单共用同一个 `Menu` 实例和同一份 slot 内容,区别只在
326
- `rightClickMenuUntil` 窗口:本插件条目只在窗口内挂载,窗口随菜单关闭一并清零。
359
+ ### 为什么右键条目不会出现在「…」菜单里
360
+
361
+ 0.2.2 上右键与「…」打开的是同一份宿主菜单,React 侧无法区分来源,只能看手势:右键时在
362
+ 进程内打一个标记,用 `MutationObserver` 等一个新的 `[role="menu"]` 面板出现(0.2.2 的
363
+ 菜单是异步挂载的,还要等 provider 就绪),面板出现后再延迟 400 ms 收尾;3 秒兜底防止手势
364
+ 被别的插件吞掉。行尾「…」的 `pointerdown` 会立刻清标,只认真实输入(`isTrusted`),因为
365
+ 插件自己补发的合成 `pointerdown` 不算手势。
366
+
367
+ 条目在挂载时读一次标记,所以面板挂得晚也不影响;标记只活到“这次手势对应的面板出现”,
368
+ 不会漏进下一次。
369
+
370
+ 工作区行在 0.2.2 上走的是另一条路:宿主菜单的数据来自 `ctx.contextMenus.register` 的
371
+ `resolve`,没有 React 条目可以读标记,所以 provider 在解析时直接判断同一枚手势标记,
372
+ 并且行尾「…」的 `pointerdown` 清标发生在宿主解析之前 —— 右键时条目进菜单,点「…」时
373
+ 不进。「同时放进「…」菜单 → 工作区」的开关则让 provider 跳过手势判断,条目不依赖右键。
327
374
 
328
- ### 为什么菜单是命令式挂载
375
+ ### 为什么 0.2.1 的菜单是命令式挂载
329
376
 
330
- 右键事件不是 React 渲染流的一部分。工作区菜单按需创建一个 `0×0`、
331
- `z-index:2147483647` 的容器,`createRoot` 渲染自绘面板;面板挂载后量一次尺寸,
332
- 再把左上角夹进视口,避免贴边时被裁掉。关闭时 `unmount()` 并移除容器。会话菜单不挂载任何
333
- 新容器,只是打开行内已有的官方菜单。
377
+ 0.2.2 的菜单全部由宿主挂载,插件不创建任何容器。0.2.1 上右键事件不是 React 渲染流的一部分,
378
+ 工作区菜单按需创建一个 `0×0`、`z-index:2147483647` 的容器,`createRoot` 渲染自绘面板;面板
379
+ 挂载后量一次尺寸,再把左上角夹进视口,避免贴边时被裁掉。关闭时 `unmount()` 并移除容器。
380
+ 会话菜单不挂载任何新容器,只是打开行内已有的官方菜单。
334
381
 
335
382
  ### 宿主路由
336
383
 
@@ -372,15 +419,17 @@ test/*.test.mjs node:test
372
419
  DSH 宿主按该范围拒绝安装不兼容的宿主,插件市场也按它显示支持版本;0.3 起需要重新验证后
373
420
  再放宽上限。
374
421
 
375
- 按 DSH 客户端 `0.2.1-rc.5` 的 `ProjectRowItem` / `SessionNodeItem` / `Menu` 实现。
376
- DSH 0.2.2 起把侧栏右键菜单改成数据驱动(`ctx.contextMenus`),本包会按宿主能力自动选择路径:
422
+ 按 DSH 客户端 `0.2.1-rc.5` 与 `0.2.2` 的 `ProjectRowItem` / `SessionNodeItem` / `Menu`
423
+ 与 `contextMenus` 实现适配。DSH 0.2.2 起把侧栏右键菜单改成数据驱动,本包按宿主能力自动选路:
377
424
 
378
425
  - **0.2.2 及以上**:工作区行改用 `ctx.contextMenus.register` 贡献动作
379
- (新会话 90、置顶应用 150、打开方式 180、复制路径 250、新标签 260、删除工作区及所有会话 310),
380
- 与原生「重命名」「在文件夹显示」「删除工作区」按 order 交错排列,右键与「…」共用同一份菜单;
381
- - **0.2.1**:没有 `contextMenus` 服务,保持原来的自绘工作区菜单;
382
- - **会话行**:0.2.2 仍保留 `sidebar.workspaces.session.menu.item` 扩展点,本包条目继续从这里
383
- 渲染;右键仍按指针位置展开宿主菜单。
426
+ (新会话 90、新标签 110、复制路径 150、置顶应用 210、打开方式 220、删除工作区及所有会话 310),
427
+ 与原生「重命名」「在文件夹显示」「删除工作区」按 order 交错排列;分组分隔线与会话菜单同一
428
+ 套规则。右键与「…」共用同一份宿主菜单,插件不再自绘,也不再接管「…」按钮。provider 解析时
429
+ 按右键手势(或设置里「同时放进「…」菜单」的开关)决定是否返回条目。会话行由宿主自己把原生
430
+ 菜单开在指针处,本包条目继续从 `sidebar.workspaces.session.menu.item` 渲染;
431
+ - **0.2.1**:没有 `contextMenus` 服务,保持原来的自绘工作区菜单;会话右键仍由插件把
432
+ 行尾「…」菜单搬到指针。
384
433
 
385
434
  DSH 升级若改动行组件或菜单定位实现:
386
435
 
@@ -390,9 +439,11 @@ DSH 升级若改动行组件或菜单定位实现:
390
439
  ## 已知限制
391
440
 
392
441
  - 0.2.1 下依赖 DSH 内置行组件的 props 形状;0.2.2 起工作区条目改走 `contextMenus`,
393
- 会话条目仍依赖旧扩展点。DSH 升级若改掉对应实现,右键可能静默不弹。
394
- - 会话右键依赖行内 `Menu` 组件仍在 fiber 子树里。找不到时退回点击行内操作区的
395
- 省略号按钮,再找不到就放弃 —— 不会自建菜单,也不会误触行的打开动作。
442
+ 会话条目仍依赖旧扩展点,并用 `memoizedProps.onContextMenu` 判断这一行是否已由宿主接管。
443
+ DSH 升级若改掉对应实现,右键可能静默不弹。
444
+ - 0.2.1 的会话右键依赖行内 `Menu` 组件仍在 fiber 子树里。找不到时退回点击行内操作区的
445
+ 省略号按钮,再找不到就放弃 —— 不会自建菜单,也不会误触行的打开动作。0.2.2 上不需要
446
+ 这段回退,宿主自己就是触发器。
396
447
  - 永久删除默认只对**已归档**会话开放;在设置里开启「允许删除未归档会话」后,未归档
397
448
  会话也能从右键菜单直接删除。删除不会级联删除 fork 出来的子会话,需要时请分别删除。
398
449
  - 删除操作直接作用于会话日志目录,宿主没有回收站;确认框里已明确提示不可恢复。
package/lib/client.js CHANGED
@@ -186,8 +186,7 @@ window.__ModuleLoader__.load({
186
186
  'settings.expand': '展开应用列表',
187
187
  'settings.collapse': '收起应用列表',
188
188
  'settings.ellipsisTitle': '同时放进「…」菜单',
189
- 'settings.ellipsisHint': '勾选后,对应条目会额外出现在行尾「…」按钮的菜单里,并沿用上面的单项开关;不勾选则只在右键菜单出现。',
190
- 'settings.ellipsisHintNative': '当前 DSH 版本的「…」按钮与右键共用同一份菜单,工作区行无需单独登记;这里只保留会话行的开关。',
189
+ 'settings.ellipsisHint': 'DSH 0.2.2 起「…」按钮与右键共用同一份菜单:勾选后对应条目也会出现在「…」菜单里,并沿用上面的单项开关;不勾选则只在右键菜单出现。',
191
190
  'settings.ellipsisWorkspace': '工作区行',
192
191
  'settings.ellipsisSession': '会话行',
193
192
  'settings.ellipsis.newSession.label': '新建会话',
@@ -271,8 +270,7 @@ window.__ModuleLoader__.load({
271
270
  'settings.expand': 'Show applications',
272
271
  'settings.collapse': 'Hide applications',
273
272
  'settings.ellipsisTitle': 'Also show in the "…" menu',
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
+ 'settings.ellipsisHint': 'Since DSH 0.2.2 the "…" button opens the same menu as right-click: checked entries also appear there, still subject to the switches above. Unchecked entries stay right-click only.',
276
274
  'settings.ellipsisWorkspace': 'Workspace rows',
277
275
  'settings.ellipsisSession': 'Session rows',
278
276
  'settings.ellipsis.newSession.label': 'New session',
@@ -1503,11 +1501,6 @@ window.__ModuleLoader__.load({
1503
1501
  const children = useChildren();
1504
1502
  const apps = useApps();
1505
1503
  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;
1511
1504
  const [expanded, setExpanded] = useState(false);
1512
1505
  const anyConflict = takenOver('openWith') || takenOver('deleteSession') || takenOver('deleteArchived');
1513
1506
  const locked = items.openWith === false;
@@ -1587,7 +1580,7 @@ window.__ModuleLoader__.load({
1587
1580
  return key === 'openWith' && childRows ? [row, childRows] : [row];
1588
1581
  });
1589
1582
 
1590
- const ellipsisGroups = ellipsisRows.map(([groupKey, titleKey, rows]) => h('div', {
1583
+ const ellipsisGroups = ELLIPSIS_ROWS.map(([groupKey, titleKey, rows]) => h('div', {
1591
1584
  className: 'dsrm-settings-group',
1592
1585
  key: groupKey,
1593
1586
  },
@@ -1615,7 +1608,7 @@ window.__ModuleLoader__.load({
1615
1608
  anyConflict ? h('div', { className: 'dsrm-settings-note' }, t('settings.conflictNote')) : null,
1616
1609
  h('div', { className: 'dsrm-settings-list' }, rows),
1617
1610
  h('h2', { className: 'dsrm-settings-title dsrm-settings-subtitle' }, t('settings.ellipsisTitle')),
1618
- h('div', { className: 'dsrm-settings-note' }, t(nativeEllipsis ? 'settings.ellipsisHintNative' : 'settings.ellipsisHint')),
1611
+ h('div', { className: 'dsrm-settings-note' }, t('settings.ellipsisHint')),
1619
1612
  h('div', { className: 'dsrm-settings-groups' }, ellipsisGroups),
1620
1613
  );
1621
1614
  }
@@ -1895,6 +1888,9 @@ window.__ModuleLoader__.load({
1895
1888
  if (event.defaultPrevented || event.button !== 0) return;
1896
1889
  if (event.target === null || event.target === undefined) return;
1897
1890
  if (rootCtx === null || !workspaceEllipsisOn()) return;
1891
+ // 0.2.2 起宿主菜单本身就是数据驱动的:附加条目由 provider 按开关加进去,
1892
+ // 再接管「…」只会把原生菜单换成自绘的那一份。
1893
+ if (contextMenusOf(rootCtx) !== null) return;
1898
1894
  const target = event.target;
1899
1895
  if (typeof target.closest !== 'function') return;
1900
1896
  const row = target.closest('[data-row-key]');
@@ -1925,13 +1921,68 @@ window.__ModuleLoader__.load({
1925
1921
  /* ------------------------------------------------ 会话菜单的插件条目 */
1926
1922
 
1927
1923
  /**
1928
- * 条目只在「右键打开的菜单」里出现;点「…」打开的菜单保持原样。
1929
- * 在挂载时取值,避免菜单后续重渲染时条目突然消失。
1924
+ * 条目只在「右键打开的菜单」里出现;点行尾「…」打开的菜单保持原样。
1925
+ *
1926
+ * 不能用时间窗口判断:0.2.2 的菜单是异步挂载的(要等所有 provider 的
1927
+ * ready 和 prepare 延时),菜单一替换还会先卸载旧面板再挂新面板。
1928
+ * 这里改成「手势标记 + 面板出现即收尾」:右键时打标记,条目在挂载时
1929
+ * 读一次;新面板出现(或兜底超时)后标记立刻清掉,不会残留到下一次。
1930
1930
  */
1931
- let rightClickMenuUntil = 0;
1931
+ let rightClickMark = false;
1932
+ let rightClickToken = 0;
1933
+ let rightClickWatch = null;
1934
+
1935
+ /** 收掉右键标记;期间若又发生一次右键(token 变了)则保留新的标记。 */
1936
+ function scheduleRightClickMarkEnd(delay) {
1937
+ const token = rightClickToken;
1938
+ window.setTimeout(() => {
1939
+ if (rightClickToken === token) rightClickMark = false;
1940
+ }, delay);
1941
+ }
1942
+
1943
+ /** 当前文档里的菜单面板(测试里的最小 document stub 可能没有 querySelectorAll)。 */
1944
+ function menuPanels() {
1945
+ if (typeof document.querySelectorAll !== 'function') return [];
1946
+ return [...document.querySelectorAll('[role="menu"]')];
1947
+ }
1948
+
1949
+ /** 右键手势开始:打标记,并等菜单面板出现后收尾。 */
1950
+ function markRightClickMenu() {
1951
+ rightClickToken += 1;
1952
+ rightClickMark = true;
1953
+ if (rightClickWatch !== null) return;
1954
+ const known = new Set(menuPanels());
1955
+ const watch = { observer: null, timer: 0 };
1956
+ rightClickWatch = watch;
1957
+ const release = (delay) => {
1958
+ if (rightClickWatch !== watch) return;
1959
+ rightClickWatch = null;
1960
+ window.clearTimeout(watch.timer);
1961
+ if (watch.observer !== null) watch.observer.disconnect();
1962
+ scheduleRightClickMarkEnd(delay);
1963
+ };
1964
+ // 面板迟迟不出现(手势被吞掉)也要收尾,避免标记残留。
1965
+ watch.timer = window.setTimeout(() => release(0), 3000);
1966
+ if (typeof MutationObserver !== 'function' || document.body === null) return;
1967
+ watch.observer = new MutationObserver(() => {
1968
+ if (rightClickWatch !== watch) return;
1969
+ for (const panel of menuPanels()) {
1970
+ if (known.has(panel)) continue;
1971
+ release(400);
1972
+ return;
1973
+ }
1974
+ });
1975
+ watch.observer.observe(document.body, { childList: true, subtree: true });
1976
+ }
1977
+
1978
+ /** 点行尾「…」时先清掉上一轮的右键标记:那个菜单不该出现右键条目。 */
1979
+ function clearRightClickMark() {
1980
+ rightClickToken += 1;
1981
+ rightClickMark = false;
1982
+ }
1932
1983
 
1933
1984
  function useRightClickEntry() {
1934
- const [visible] = useState(() => Date.now() < rightClickMenuUntil);
1985
+ const [visible] = useState(() => rightClickMark);
1935
1986
  return visible;
1936
1987
  }
1937
1988
 
@@ -2064,39 +2115,32 @@ window.__ModuleLoader__.load({
2064
2115
  const apps = appsSnapshot();
2065
2116
  const withApps = itemsSnapshot().openWith !== false;
2066
2117
  const pinned = withApps ? effectivePinned(apps.ids).filter((id) => childOn(id)) : [];
2067
- pinned.forEach((id, index) => {
2068
- items.push({
2069
- id: `pinned-${id}`,
2070
- group: 'pinned',
2071
- separatorBefore: index === 0,
2072
- label: t('openWithApp', { name: appLabel(id, t) }),
2073
- icon: { key: 'sidebar-row-menu.app', src: ICON_ROUTE + encodeURIComponent(id) },
2074
- run: () => openAppAt(id, cwd, t),
2075
- });
2076
- });
2077
2118
  const enabledApps = withApps ? apps.ids.filter((id) => childOn(id)) : [];
2078
2119
  const pinnedSet = new Set(pinned);
2079
2120
  const submenuApps = enabledApps.filter((id) => !pinnedSet.has(id));
2080
2121
  if (withApps) ensureApps();
2081
- if (withApps && submenuApps.length > 0) {
2122
+
2123
+ // 与会话菜单同一套分组:行内动作 → 复制/在文件夹显示 → 打开方式 → 删除。
2124
+ const tabs = serviceOf(rootCtx, 'projectTabs') ?? globalThis.__DSH_PROJECT_TABS__ ?? null;
2125
+ if (tabs !== null && typeof tabs?.openWorkspaceInTab === 'function') {
2082
2126
  items.push({
2083
- id: 'open-with',
2084
- group: 'open-with',
2085
- separatorBefore: pinned.length === 0,
2086
- label: t('openWith'),
2087
- children: submenuApps.map((id) => ({
2088
- id: `app-${id}`,
2089
- label: appLabel(id, t),
2090
- icon: { key: 'sidebar-row-menu.app', src: ICON_ROUTE + encodeURIComponent(id) },
2091
- run: () => openAppAt(id, cwd, t),
2092
- })),
2127
+ id: 'tab',
2128
+ group: 'tab',
2129
+ label: t('openInTab'),
2130
+ icon: { key: 'sidebar-row-menu.tab' },
2131
+ run: () => {
2132
+ Promise.resolve().then(() => tabs.openWorkspaceInTab(workspaceId)).catch((reason) => {
2133
+ console.warn('[dsh-sidebar-row-menu] open in tab failed:', reason);
2134
+ toast(t('actionFailed'));
2135
+ });
2136
+ },
2093
2137
  });
2094
2138
  }
2095
2139
 
2096
2140
  items.push({
2097
2141
  id: 'copy-path',
2098
2142
  group: 'copy',
2099
- separatorBefore: pinned.length === 0 && submenuApps.length === 0,
2143
+ separatorBefore: true,
2100
2144
  label: t('copyPath'),
2101
2145
  icon: { key: 'sidebar-row-menu.copy' },
2102
2146
  run: () => {
@@ -2108,19 +2152,36 @@ window.__ModuleLoader__.load({
2108
2152
  },
2109
2153
  });
2110
2154
 
2111
- const tabs = serviceOf(rootCtx, 'projectTabs') ?? globalThis.__DSH_PROJECT_TABS__ ?? null;
2112
- if (tabs !== null && typeof tabs?.openWorkspaceInTab === 'function') {
2155
+ pinned.forEach((id, index) => {
2113
2156
  items.push({
2114
- id: 'tab',
2115
- group: 'tab',
2116
- label: t('openInTab'),
2117
- icon: { key: 'sidebar-row-menu.tab' },
2118
- run: () => {
2119
- Promise.resolve().then(() => tabs.openWorkspaceInTab(workspaceId)).catch((reason) => {
2120
- console.warn('[dsh-sidebar-row-menu] open in tab failed:', reason);
2121
- toast(t('actionFailed'));
2122
- });
2123
- },
2157
+ id: `pinned-${id}`,
2158
+ group: 'pinned',
2159
+ separatorBefore: index === 0,
2160
+ label: t('openWithApp', { name: appLabel(id, t) }),
2161
+ icon: { key: 'sidebar-row-menu.app', src: ICON_ROUTE + encodeURIComponent(id) },
2162
+ run: () => openAppAt(id, cwd, t),
2163
+ });
2164
+ });
2165
+
2166
+ if (withApps && submenuApps.length > 0) {
2167
+ items.push({
2168
+ id: 'open-with',
2169
+ group: 'open-with',
2170
+ separatorBefore: pinned.length === 0,
2171
+ // 宿主的菜单项对子菜单只设 aria-haspopup,不画右侧箭头。label 在宿主里
2172
+ // 是直接当 children 渲染的,所以这里塞一个右对齐的箭头元素,与会话菜单
2173
+ // 里的「打开方式」保持同一套外观(0.2.1 自绘菜单本来就是自己画的)。
2174
+ label: h('span', { className: 'dsrm-host-label' },
2175
+ h('span', { className: 'dsrm-host-label-text' }, t('openWith')),
2176
+ jsx(IconChevronRightOutlineRegular, { size: 12, className: 'dsrm-submenu-chevron' }),
2177
+ ),
2178
+ icon: { key: 'sidebar-row-menu.open-with' },
2179
+ children: submenuApps.map((id) => ({
2180
+ id: `app-${id}`,
2181
+ label: appLabel(id, t),
2182
+ icon: { key: 'sidebar-row-menu.app', src: ICON_ROUTE + encodeURIComponent(id) },
2183
+ run: () => openAppAt(id, cwd, t),
2184
+ })),
2124
2185
  });
2125
2186
  }
2126
2187
 
@@ -2148,13 +2209,32 @@ window.__ModuleLoader__.load({
2148
2209
  /** 宿主只按 provider.order 排序,同组顺序即数组顺序;故每组单独注册。 */
2149
2210
  const WORKSPACE_MENU_GROUPS = [
2150
2211
  { key: 'new', order: 90 },
2151
- { key: 'pinned', order: 150 },
2152
- { key: 'open-with', order: 180 },
2153
- { key: 'copy', order: 250 },
2154
- { key: 'tab', order: 260 },
2212
+ { key: 'tab', order: 110 },
2213
+ { key: 'copy', order: 150 },
2214
+ { key: 'pinned', order: 210 },
2215
+ { key: 'open-with', order: 220 },
2155
2216
  { key: 'delete', order: 310 },
2156
2217
  ];
2157
2218
 
2219
+ /**
2220
+ * 0.2.2 的「…」按钮与右键共用同一份宿主菜单,条目本身无法感知手势,
2221
+ * 所以按右键标记决定是否进入这份菜单;打开对应附加开关后,点「…」也显示。
2222
+ */
2223
+ const WORKSPACE_GROUP_ELLIPSIS = {
2224
+ new: 'newSession',
2225
+ pinned: 'openWith',
2226
+ 'open-with': 'openWith',
2227
+ copy: 'copyPath',
2228
+ tab: 'openInTab',
2229
+ delete: 'deleteSessions',
2230
+ };
2231
+
2232
+ function workspaceGroupVisible(groupKey) {
2233
+ if (rightClickMark) return true;
2234
+ const key = WORKSPACE_GROUP_ELLIPSIS[groupKey];
2235
+ return key !== undefined && ellipsisSettings[key] === true;
2236
+ }
2237
+
2158
2238
  /** 注册工作区动作;服务可能晚于插件 apply 出现,带有限重试。 */
2159
2239
  function registerWorkspaceContextMenu(ctx) {
2160
2240
  let disposers = null;
@@ -2168,6 +2248,7 @@ window.__ModuleLoader__.load({
2168
2248
  yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.new' }, IconNewChatOutlineRegular);
2169
2249
  yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.copy' }, IconCopyOutlineRegular);
2170
2250
  yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.tab' }, IconProjectAddOutlineRegular);
2251
+ yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.open-with' }, IconRightUpOutlineRegular);
2171
2252
  yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.delete' }, IconTrashOutlineRegular);
2172
2253
  yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.app' }, ContextMenuAppIcon);
2173
2254
  });
@@ -2192,9 +2273,12 @@ window.__ModuleLoader__.load({
2192
2273
  target: 'workspace',
2193
2274
  order: group.order,
2194
2275
  observe: () => [appsSource],
2195
- resolve: (target) => workspaceMenuActions(ctx, target, localeOf(ctx))
2196
- .filter((action) => action.group === group.key)
2197
- .map(({ group: _group, ...action }) => action),
2276
+ resolve: (target) => {
2277
+ if (!workspaceGroupVisible(group.key)) return [];
2278
+ return workspaceMenuActions(ctx, target, localeOf(ctx))
2279
+ .filter((action) => action.group === group.key)
2280
+ .map(({ group: _group, ...action }) => action);
2281
+ },
2198
2282
  }));
2199
2283
  } catch (error) {
2200
2284
  console.warn(`[dsh-sidebar-row-menu] workspace context registration failed (${group.key}):`, error);
@@ -2302,7 +2386,6 @@ window.__ModuleLoader__.load({
2302
2386
  /* ignore */
2303
2387
  }
2304
2388
  }
2305
- rightClickMenuUntil = 0;
2306
2389
  }
2307
2390
 
2308
2391
  /**
@@ -2400,9 +2483,8 @@ window.__ModuleLoader__.load({
2400
2483
  }
2401
2484
 
2402
2485
  /**
2403
- * 右键会话行时在指针处展开该行自带的「…」菜单。
2404
- * 菜单内容来自 slot 注册表:内置条目和第三方条目都在,本插件不复制内置动作;
2405
- * 本插件自己的三条只在 rightClickMenuUntil 窗口内挂载。
2486
+ * 右键会话行时在指针处展开该行自带的菜单(0.2.1 的旧内置 Menu)。
2487
+ * 0.2.2 起宿主自己提供 contextmenu 触发器,不再走这里。
2406
2488
  */
2407
2489
  function openSessionRowMenu(row, x, y) {
2408
2490
  const trigger = typeof row.querySelector === 'function'
@@ -2411,12 +2493,8 @@ window.__ModuleLoader__.load({
2411
2493
  const fiber = fiberOf(row);
2412
2494
  const menu = fiber === null ? null : findMenuProps(fiber.child, 0, { count: 0 });
2413
2495
  if (menu !== null && menu.open === true) return;
2414
- // 先还原旧锚点(会清掉上一轮的窗口),再设置本轮窗口并挂新锚点。
2415
2496
  patchAnchorRect(trigger, x, y);
2416
- rightClickMenuUntil = Date.now() + 400;
2417
- window.setTimeout(() => {
2418
- rightClickMenuUntil = 0;
2419
- }, 600);
2497
+ markRightClickMenu();
2420
2498
  primeRowActionPointer(trigger);
2421
2499
  if (menu !== null) {
2422
2500
  menu.anchor.props.onClick();
@@ -2433,11 +2511,15 @@ window.__ModuleLoader__.load({
2433
2511
  .dsrm-submenu-trigger:hover:not(:disabled),.dsrm-submenu-trigger:focus-visible{background:var(--dsw-alias-interactive-bg-hover);outline:none}
2434
2512
  .dsrm-submenu-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2435
2513
  .dsrm-submenu-chevron{color:var(--dsw-alias-label-caption)}
2514
+ .dsrm-host-label{display:flex;align-items:center;gap:6px;width:100%}
2515
+ .dsrm-host-label-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}
2436
2516
  .dsrm-submenu{position:fixed;z-index:1101;min-width:180px;max-width:300px;max-height:min(380px,64vh);overflow:auto;padding:4px}
2437
2517
  .dsrm-submenu-empty{padding:6px 8px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
2438
2518
  .dsrm-menu{position:fixed;z-index:2147483647}
2439
2519
  .dsrm-menu-surface{min-width:210px;padding:4px}
2440
2520
  .dsrm-sep{height:.5px;margin:3px 2px;background:var(--dsw-alias-border-l2)}
2521
+ /* 150% 缩放等分数像素比下,宿主 0.5px 的分隔线会被摊成两行近乎透明,补到 1px 才看得见。 */
2522
+ @media (min-resolution:1.2dppx) and (max-resolution:1.99dppx){[role="menu"] [role="separator"],.dsrm-sep{height:1px}}
2441
2523
  .dsrm-app-icon{width:14px;height:14px;border-radius:3px;display:block;object-fit:contain}
2442
2524
  .dsrm-settings{max-width:820px;margin:0 auto;padding:2px 0 36px}
2443
2525
  .dsrm-settings-title{margin:0;font-size:16px;line-height:24px;font-weight:600}
@@ -2481,6 +2563,17 @@ window.__ModuleLoader__.load({
2481
2563
 
2482
2564
  /* -------------------------------------------------------------- 安装 */
2483
2565
 
2566
+ /**
2567
+ * 0.2.2 的会话行自己带 contextmenu 触发器(宿主 getTriggerProps 展开的
2568
+ * onContextMenu),它会把同一份菜单开在指针处。0.2.1 的行没有这个处理器,
2569
+ * 只能由插件代点行尾「…」再改锚点。
2570
+ */
2571
+ function hasNativeRowMenu(row) {
2572
+ const fiber = fiberOf(row);
2573
+ const props = fiber === null ? null : fiber.memoizedProps;
2574
+ return props !== null && props !== undefined && typeof props.onContextMenu === 'function';
2575
+ }
2576
+
2484
2577
  /** 全局 contextmenu 拦截:只有命中工作区 / 会话行才接管。 */
2485
2578
  function onContextMenu(event) {
2486
2579
  if (event.defaultPrevented) return;
@@ -2494,6 +2587,18 @@ window.__ModuleLoader__.load({
2494
2587
  const rowKey = row.getAttribute('data-row-key') ?? '';
2495
2588
  if (!rowKey.startsWith('workspace:') && !rowKey.startsWith('session:')) return;
2496
2589
 
2590
+ // 宿主自己会处理右键(0.2.2):只打手势标记,菜单交给宿主开在指针处。
2591
+ // 这样既不用改锚点、也不用代点「…」,少两层时序假设;provider 在解析时用
2592
+ // 这个标记决定条目是否进入菜单(「…」的按下会先清标)。
2593
+ if (hasNativeRowMenu(row)) {
2594
+ if (rowKey.startsWith('session:') && resolveSessionRow(start) === null) return;
2595
+ markRightClickMenu();
2596
+ primeRowActionPointer(typeof row.querySelector === 'function'
2597
+ ? row.querySelector('[class*="rowActions"] button')
2598
+ : null);
2599
+ return;
2600
+ }
2601
+
2497
2602
  if (rowKey.startsWith('workspace:')) {
2498
2603
  const props = resolveWorkspaceRow(start);
2499
2604
  if (props === null) {
@@ -2516,12 +2621,26 @@ window.__ModuleLoader__.load({
2516
2621
  openSessionRowMenu(row, event.clientX, event.clientY);
2517
2622
  }
2518
2623
 
2624
+ /** 行尾「…」按下时先收掉右键标记:那个菜单不该出现右键条目。 */
2625
+ function onRowActionsPointerDown(event) {
2626
+ // 只认真实输入:插件自己补发的 pointerdown(primeRowActionPointer)不算手势。
2627
+ if (event.isTrusted === false) return;
2628
+ const target = event.target;
2629
+ if (target === null || target === undefined) return;
2630
+ const start = target.nodeType === 3 ? target.parentElement : target;
2631
+ if (start === null || start === undefined || typeof start.closest !== 'function') return;
2632
+ if (start.closest('[class*="rowActions"]') === null) return;
2633
+ clearRightClickMark();
2634
+ }
2635
+
2519
2636
  function install() {
2520
2637
  window.addEventListener('contextmenu', onContextMenu, true);
2638
+ document.addEventListener('pointerdown', onRowActionsPointerDown, true);
2521
2639
  document.addEventListener('click', onWorkspaceEllipsisClick, true);
2522
2640
  console.info('[dsh-sidebar-row-menu] installed');
2523
2641
  return () => {
2524
2642
  window.removeEventListener('contextmenu', onContextMenu, true);
2643
+ document.removeEventListener('pointerdown', onRowActionsPointerDown, true);
2525
2644
  document.removeEventListener('click', onWorkspaceEllipsisClick, true);
2526
2645
  closeMenu();
2527
2646
  closeToast();
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "dsh-sidebar-row-menu",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
- "description": "DSH sidebar right-click menus: a self-composed menu for workspace rows, and the native slot menu opened at the pointer for session rows, with open-with and archived-session cleanup.",
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",