dsh-sidebar-row-menu 1.4.3 → 1.5.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,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.0 — 2026-11-05
4
+
5
+ ### Added
6
+
7
+ - **DSH 0.2.2 native context menus**: on hosts that expose `ctx.contextMenus`, the
8
+ workspace-row entries are registered as data contributions instead of a self-drawn
9
+ menu. Entries keep their positions relative to the host items —
10
+ New session 90, pinned app 150, Open with 180, Copy workspace path 250,
11
+ Open in new tab 260, Delete workspace and all sessions 310 — so they interleave
12
+ with the native Rename (100), Reveal (200) and Delete workspace (300). Right-click
13
+ and the row's `…` button open the same menu, because the host now shares one menu
14
+ between them. On 0.2.1 the plugin still draws its own workspace menu. Session-row
15
+ entries keep using the `sidebar.workspaces.session.menu.item` extension point.
16
+
17
+ ### Fixed
18
+
19
+ - The permanent-delete confirmation falls back to “Untitled session” instead of
20
+ showing an empty `会话「」` for sessions whose title has not been generated yet.
21
+ - On 0.2.2 the settings page now hides the workspace-row “Also show in the … menu”
22
+ switches: the host shares one native menu between right-click and the `…` button,
23
+ so those per-entry toggles could never take effect there. The session-row switches
24
+ still work and stay visible.
25
+
26
+ ### 中文摘要
27
+
28
+ - **支持 DSH 0.2.2 的原生右键菜单**:宿主提供 `ctx.contextMenus` 时,工作区行条目改为
29
+ 数据贡献注册,按 order 与原生项交错排列(新会话 90、置顶应用 150、打开方式 180、
30
+ 复制工作区路径 250、在新标签打开 260、删除工作区及所有会话 310;原生为重命名 100、
31
+ 在文件夹显示 200、删除工作区 300)。右键与行尾「…」共用宿主同一份菜单。0.2.1 上仍
32
+ 走自绘工作区菜单;会话行条目继续通过 `sidebar.workspaces.session.menu.item` 扩展点渲染。
33
+ - 修复未命名会话「永久删除」确认框显示空标题的问题,回退为「未命名会话」。
34
+ - 0.2.2 的设置页隐藏工作区行「同时放进「…」菜单」开关(该版本右键与「…」共用同一份
35
+ 菜单,开关无从生效);会话行的同类开关保留且仍然有效。
36
+
3
37
  ## 1.4.3 — 2026-11-04
4
38
 
5
39
  ### Changed
package/README.en.md CHANGED
@@ -99,6 +99,10 @@ Settings → "Row menu" toggles every new entry:
99
99
  The "Also show in the "…" menu" switches are all off by default; until one is
100
100
  enabled the "…" menu is exactly DSH's own.
101
101
 
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.
105
+
102
106
  Switches live in browser `localStorage` (`dsh.sidebar-row-menu.items.v1`,
103
107
  `dsh.sidebar-row-menu.open-with.v1`, `dsh.sidebar-row-menu.open-with.pinned.v1` and
104
108
  `dsh.sidebar-row-menu.ellipsis.v1`)
@@ -423,15 +427,32 @@ incompatible installs, and the plugin market shows it as the supported version;
423
427
  0.3 and later need re-verification before the upper bound is raised.
424
428
 
425
429
  Built against DSH client `0.2.1-rc.5` (`ProjectRowItem`, `SessionNodeItem`,
426
- `Menu`). If a DSH upgrade changes those row components or the menu positioning:
427
-
428
- - workspace right-click silently stops appearing (no error, no UI breakage);
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:
432
+
433
+ - **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.
438
+ - **0.2.1**: no `contextMenus` service, so the plugin keeps drawing its own
439
+ workspace menu.
440
+ - **Session rows**: 0.2.2 still supports the
441
+ `sidebar.workspaces.session.menu.item` extension point; the plugin entries keep
442
+ mounting there, and right-click still opens the host menu at the pointer.
443
+
444
+ If a DSH upgrade changes those row components or the menu positioning:
445
+
446
+ - workspace right-click falls back to the self-drawn menu (0.2.1 path) or silently
447
+ stops appearing (no error, no UI breakage);
429
448
  - session right-click falls back to opening next to the "…" button, with correct content.
430
449
 
431
450
  ## Known limitations
432
451
 
433
- - Depends on the props shape of the built-in row components. If DSH changes them,
434
- right-click silently does nothing.
452
+ - 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.
435
456
  - Session right-click depends on the row's `Menu` staying in the fiber subtree. If
436
457
  it is not found, the plugin clicks the row-actions ellipsis button; failing that
437
458
  it gives up — it never builds a competing menu and never triggers row open.
package/README.md CHANGED
@@ -86,6 +86,9 @@
86
86
 
87
87
  「同时放进「…」菜单」下的开关默认全部关闭,`…` 菜单在未开启时与 DSH 完全一致。
88
88
 
89
+ DSH 0.2.2 起,工作区行的右键与「…」本身就是同一份宿主菜单,插件条目会一直出现在「…」里,
90
+ 「同时放进「…」菜单 → 工作区」这组开关只影响 0.2.1 及更早的宿主。
91
+
89
92
  开关存在浏览器 `localStorage`(`dsh.sidebar-row-menu.items.v1`、
90
93
  `dsh.sidebar-row-menu.open-with.v1`、`dsh.sidebar-row-menu.open-with.pinned.v1` 与
91
94
  `dsh.sidebar-row-menu.ellipsis.v1`),
@@ -370,14 +373,24 @@ DSH 宿主按该范围拒绝安装不兼容的宿主,插件市场也按它显
370
373
  再放宽上限。
371
374
 
372
375
  按 DSH 客户端 `0.2.1-rc.5` 的 `ProjectRowItem` / `SessionNodeItem` / `Menu` 实现。
376
+ DSH 0.2.2 起把侧栏右键菜单改成数据驱动(`ctx.contextMenus`),本包会按宿主能力自动选择路径:
377
+
378
+ - **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
+ 渲染;右键仍按指针位置展开宿主菜单。
384
+
373
385
  DSH 升级若改动行组件或菜单定位实现:
374
386
 
375
- - 工作区右键会静默不弹(不会报错、不影响原界面);
387
+ - 工作区右键会退回自绘菜单(0.2.1 路径),或静默不弹(不会报错、不影响原界面);
376
388
  - 会话右键会退回行尾「…」按钮旁,内容仍然正确。
377
389
 
378
390
  ## 已知限制
379
391
 
380
- - 依赖 DSH 内置行组件的 props 形状。DSH 升级若改了这两个组件,右键会静默不弹。
392
+ - 0.2.1 下依赖 DSH 内置行组件的 props 形状;0.2.2 起工作区条目改走 `contextMenus`,
393
+ 会话条目仍依赖旧扩展点。DSH 升级若改掉对应实现,右键可能静默不弹。
381
394
  - 会话右键依赖行内 `Menu` 组件仍在 fiber 子树里。找不到时退回点击行内操作区的
382
395
  省略号按钮,再找不到就放弃 —— 不会自建菜单,也不会误触行的打开动作。
383
396
  - 永久删除默认只对**已归档**会话开放;在设置里开启「允许删除未归档会话」后,未归档
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 = ELLIPSIS_ROWS.map(([groupKey, titleKey, rows]) => h('div', {
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
  }
@@ -1961,6 +1970,257 @@ window.__ModuleLoader__.load({
1961
1970
  });
1962
1971
  }
1963
1972
 
1973
+ /* -------------------------------- 工作区行:0.2.2 原生右键贡献 */
1974
+
1975
+ /**
1976
+ * DSH 0.2.2 把右键菜单改成数据贡献(ctx.contextMenus),工作区行也不再向
1977
+ * 行组件传 actions/onCreate,插件原来的自绘入口会取不到 props。这里改为
1978
+ * 向宿主注册同一批动作;0.2.1 没有这个服务,保持自绘菜单不变。
1979
+ */
1980
+ function contextMenusOf(ctx) {
1981
+ const usable = (menus) => (menus !== null && menus !== undefined && typeof menus.register === 'function' ? menus : null);
1982
+ // 先走 ctx.get:未 inject 时直接读 ctx.contextMenus 会抛错,读不到服务。
1983
+ try {
1984
+ const viaGet = usable(ctx?.get?.('contextMenus'));
1985
+ if (viaGet !== null) return viaGet;
1986
+ } catch {
1987
+ /* 服务容器尚未就绪 */
1988
+ }
1989
+ try {
1990
+ return usable(ctx?.contextMenus);
1991
+ } catch {
1992
+ /* 未 inject 的服务属性访问会抛错,按不存在处理 */
1993
+ }
1994
+ return null;
1995
+ }
1996
+
1997
+ /** 应用缓存包成宿主可订阅的数据源,应用列表到达后菜单会自动重解析。 */
1998
+ const appsSource = {
1999
+ getSnapshot: () => appsState,
2000
+ subscribe: (listener) => subscribeApps(listener),
2001
+ };
2002
+
2003
+ /** 应用图标走 data 贡献:resolve 里带 src,组件只负责渲染。 */
2004
+ function ContextMenuAppIcon({ icon }) {
2005
+ const src = typeof icon?.src === 'string' ? icon.src : '';
2006
+ if (src === '') return null;
2007
+ return jsx('img', {
2008
+ className: 'dsrm-app-icon',
2009
+ src,
2010
+ alt: '',
2011
+ onError: (event) => {
2012
+ event.currentTarget.style.visibility = 'hidden';
2013
+ },
2014
+ });
2015
+ }
2016
+
2017
+ function workspaceSessionIds(workspaceId) {
2018
+ const snapshot = serviceOf(rootCtx, 'workspaces')?.list?.getSnapshot?.();
2019
+ const entry = Array.isArray(snapshot?.items)
2020
+ ? snapshot.items.find((candidate) => candidate?.workspaceId === workspaceId)
2021
+ : undefined;
2022
+ return Array.isArray(entry?.sessionIds)
2023
+ ? entry.sessionIds.filter((id) => typeof id === 'string' && id !== '')
2024
+ : [];
2025
+ }
2026
+
2027
+ function openAppAt(appId, cwd, t) {
2028
+ if (cwd === '') {
2029
+ toast(t('noPath'));
2030
+ return;
2031
+ }
2032
+ openApplication(appId, cwd).then((ok) => {
2033
+ if (!ok) toast(t('openFailed'));
2034
+ });
2035
+ }
2036
+
2037
+ /**
2038
+ * 数据贡献的一批工作区动作;group 字段决定它挂到哪个 order 的 provider,
2039
+ * 从而夹在原生 rename(100)/reveal(200)/delete(300) 之间。
2040
+ */
2041
+ function workspaceMenuActions(ctx, value, t) {
2042
+ const workspaceId = typeof value?.workspaceId === 'string' ? value.workspaceId : '';
2043
+ if (workspaceId === '') return [];
2044
+ const cwd = typeof value?.path === 'string' ? value.path : '';
2045
+ const items = [];
2046
+
2047
+ items.push({
2048
+ id: 'new-session',
2049
+ group: 'new',
2050
+ label: t('newSession'),
2051
+ icon: { key: 'sidebar-row-menu.new' },
2052
+ run: () => {
2053
+ const ui = serviceOf(rootCtx, 'uiWorkspace');
2054
+ if (typeof ui?.startSession === 'function') {
2055
+ try {
2056
+ ui.startSession(workspaceId, {});
2057
+ } catch (error) {
2058
+ console.warn('[dsh-sidebar-row-menu] new session failed:', error);
2059
+ }
2060
+ }
2061
+ },
2062
+ });
2063
+
2064
+ const apps = appsSnapshot();
2065
+ const withApps = itemsSnapshot().openWith !== false;
2066
+ 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
+ const enabledApps = withApps ? apps.ids.filter((id) => childOn(id)) : [];
2078
+ const pinnedSet = new Set(pinned);
2079
+ const submenuApps = enabledApps.filter((id) => !pinnedSet.has(id));
2080
+ if (withApps) ensureApps();
2081
+ if (withApps && submenuApps.length > 0) {
2082
+ 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
+ })),
2093
+ });
2094
+ }
2095
+
2096
+ items.push({
2097
+ id: 'copy-path',
2098
+ group: 'copy',
2099
+ separatorBefore: pinned.length === 0 && submenuApps.length === 0,
2100
+ label: t('copyPath'),
2101
+ icon: { key: 'sidebar-row-menu.copy' },
2102
+ run: () => {
2103
+ if (cwd === '') {
2104
+ toast(t('noPath'));
2105
+ return;
2106
+ }
2107
+ copyText(cwd, t);
2108
+ },
2109
+ });
2110
+
2111
+ const tabs = serviceOf(rootCtx, 'projectTabs') ?? globalThis.__DSH_PROJECT_TABS__ ?? null;
2112
+ if (tabs !== null && typeof tabs?.openWorkspaceInTab === 'function') {
2113
+ 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
+ },
2124
+ });
2125
+ }
2126
+
2127
+ items.push({
2128
+ id: 'delete-sessions',
2129
+ group: 'delete',
2130
+ danger: true,
2131
+ separatorBefore: false,
2132
+ label: t('deleteWorkspaceSessions'),
2133
+ icon: { key: 'sidebar-row-menu.delete' },
2134
+ run: () => {
2135
+ const title = typeof value?.displayTitle === 'string' && value.displayTitle !== '' ? value.displayTitle : cwd;
2136
+ requestDelete({
2137
+ mode: 'workspace',
2138
+ workspaceId,
2139
+ sessionIds: workspaceSessionIds(workspaceId),
2140
+ displayTitle: title,
2141
+ });
2142
+ },
2143
+ });
2144
+
2145
+ return items;
2146
+ }
2147
+
2148
+ /** 宿主只按 provider.order 排序,同组顺序即数组顺序;故每组单独注册。 */
2149
+ const WORKSPACE_MENU_GROUPS = [
2150
+ { 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 },
2155
+ { key: 'delete', order: 310 },
2156
+ ];
2157
+
2158
+ /** 注册工作区动作;服务可能晚于插件 apply 出现,带有限重试。 */
2159
+ function registerWorkspaceContextMenu(ctx) {
2160
+ let disposers = null;
2161
+ let icons = null;
2162
+ let timer = 0;
2163
+ let attempts = 0;
2164
+
2165
+ const registerIcons = () => {
2166
+ if (ctx.slots === null || ctx.slots === undefined) return;
2167
+ icons = ctx.slots.inject('contextMenu.icon', function* () {
2168
+ yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.new' }, IconNewChatOutlineRegular);
2169
+ yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.copy' }, IconCopyOutlineRegular);
2170
+ yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.tab' }, IconProjectAddOutlineRegular);
2171
+ yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.delete' }, IconTrashOutlineRegular);
2172
+ yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.app' }, ContextMenuAppIcon);
2173
+ });
2174
+ };
2175
+
2176
+ const register = () => {
2177
+ timer = 0;
2178
+ if (disposers !== null) return;
2179
+ const menus = contextMenusOf(ctx);
2180
+ if (menus === null) {
2181
+ if (attempts >= 40 || typeof setTimeout !== 'function') return;
2182
+ attempts += 1;
2183
+ timer = setTimeout(register, 250);
2184
+ return;
2185
+ }
2186
+ registerIcons();
2187
+ const registered = [];
2188
+ for (const group of WORKSPACE_MENU_GROUPS) {
2189
+ try {
2190
+ registered.push(menus.register({
2191
+ id: `sidebar-row-menu.workspace.${group.key}`,
2192
+ target: 'workspace',
2193
+ order: group.order,
2194
+ observe: () => [appsSource],
2195
+ resolve: (target) => workspaceMenuActions(ctx, target, localeOf(ctx))
2196
+ .filter((action) => action.group === group.key)
2197
+ .map(({ group: _group, ...action }) => action),
2198
+ }));
2199
+ } catch (error) {
2200
+ console.warn(`[dsh-sidebar-row-menu] workspace context registration failed (${group.key}):`, error);
2201
+ }
2202
+ }
2203
+ if (registered.length > 0) disposers = registered;
2204
+ };
2205
+
2206
+ register();
2207
+ return () => {
2208
+ if (timer !== 0) clearTimeout(timer);
2209
+ for (const dispose of disposers ?? []) {
2210
+ try {
2211
+ dispose?.();
2212
+ } catch {
2213
+ /* 注册已随插件生命周期撤销 */
2214
+ }
2215
+ }
2216
+ try {
2217
+ icons?.();
2218
+ } catch {
2219
+ /* icon 条目可能未注册成功 */
2220
+ }
2221
+ };
2222
+ }
2223
+
1964
2224
  /** 删除确认框挂到 shell.overlay,避免菜单关闭时被一起卸载。 */
1965
2225
  function registerDeleteDialog(ctx) {
1966
2226
  return ctx.slots.inject('shell.overlay', function* () {
@@ -2288,10 +2548,12 @@ window.__ModuleLoader__.load({
2288
2548
  if (typeof ctx.effect === 'function') {
2289
2549
  ctx.effect(() => installStyles(), 'sidebar-row-menu: styles');
2290
2550
  }
2551
+ const stopWorkspaceMenu = registerWorkspaceContextMenu(ctx);
2291
2552
  const teardown = install();
2292
2553
  if (ctx !== null && typeof ctx.effect === 'function') {
2293
2554
  ctx.effect(() => () => {
2294
2555
  rootCtx = null;
2556
+ stopWorkspaceMenu();
2295
2557
  teardown();
2296
2558
  }, 'sidebar-row-menu: row context menus');
2297
2559
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-sidebar-row-menu",
3
- "version": "1.4.3",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
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.",
6
6
  "keywords": [