dsh-sidebar-row-menu 1.4.2 → 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 +48 -0
- package/README.en.md +31 -5
- package/README.md +19 -2
- package/lib/client.js +265 -3
- package/package.json +5 -4
- package/scripts/marketplace-preflight.mjs +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
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
|
+
|
|
37
|
+
## 1.4.3 — 2026-11-04
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- **Declared the supported DSH range**: `package.json` now carries
|
|
42
|
+
`engines.dsh: ">=0.2.1-rc.5 <0.3.0-0"`. DSH refuses to install or activate the
|
|
43
|
+
plugin on a host outside that range, and the plugin market lists the range on the
|
|
44
|
+
card instead of treating the plugin as undeclared. No runtime code changed.
|
|
45
|
+
|
|
46
|
+
### 中文摘要
|
|
47
|
+
|
|
48
|
+
- 在 `package.json` 中声明 `engines.dsh: ">=0.2.1-rc.5 <0.3.0-0"`:范围外的 DSH 会拒绝
|
|
49
|
+
安装/激活,插件市场会显示支持版本,不再当作“未声明”。运行代码无改动。
|
|
50
|
+
|
|
3
51
|
## 1.4.2 — 2026-11-03
|
|
4
52
|
|
|
5
53
|
### Added
|
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`)
|
|
@@ -417,16 +421,38 @@ the row's existing menu.
|
|
|
417
421
|
|
|
418
422
|
## Compatibility
|
|
419
423
|
|
|
420
|
-
|
|
421
|
-
`
|
|
424
|
+
The package declares its supported DSH range in `package.json` as
|
|
425
|
+
`engines.dsh`: `>=0.2.1-rc.5 <0.3.0-0`. The DSH host uses it to refuse
|
|
426
|
+
incompatible installs, and the plugin market shows it as the supported version;
|
|
427
|
+
0.3 and later need re-verification before the upper bound is raised.
|
|
422
428
|
|
|
423
|
-
|
|
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:
|
|
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);
|
|
424
448
|
- session right-click falls back to opening next to the "…" button, with correct content.
|
|
425
449
|
|
|
426
450
|
## Known limitations
|
|
427
451
|
|
|
428
|
-
-
|
|
429
|
-
|
|
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.
|
|
430
456
|
- Session right-click depends on the row's `Menu` staying in the fiber subtree. If
|
|
431
457
|
it is not found, the plugin clicks the row-actions ellipsis button; failing that
|
|
432
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`),
|
|
@@ -365,15 +368,29 @@ test/*.test.mjs node:test
|
|
|
365
368
|
|
|
366
369
|
## 兼容性
|
|
367
370
|
|
|
371
|
+
本包在 `package.json` 的 `engines.dsh` 中声明支持范围:`>=0.2.1-rc.5 <0.3.0-0`。
|
|
372
|
+
DSH 宿主按该范围拒绝安装不兼容的宿主,插件市场也按它显示支持版本;0.3 起需要重新验证后
|
|
373
|
+
再放宽上限。
|
|
374
|
+
|
|
368
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
|
+
|
|
369
385
|
DSH 升级若改动行组件或菜单定位实现:
|
|
370
386
|
|
|
371
|
-
-
|
|
387
|
+
- 工作区右键会退回自绘菜单(0.2.1 路径),或静默不弹(不会报错、不影响原界面);
|
|
372
388
|
- 会话右键会退回行尾「…」按钮旁,内容仍然正确。
|
|
373
389
|
|
|
374
390
|
## 已知限制
|
|
375
391
|
|
|
376
|
-
-
|
|
392
|
+
- 0.2.1 下依赖 DSH 内置行组件的 props 形状;0.2.2 起工作区条目改走 `contextMenus`,
|
|
393
|
+
会话条目仍依赖旧扩展点。DSH 升级若改掉对应实现,右键可能静默不弹。
|
|
377
394
|
- 会话右键依赖行内 `Menu` 组件仍在 fiber 子树里。找不到时退回点击行内操作区的
|
|
378
395
|
省略号按钮,再找不到就放弃 —— 不会自建菜单,也不会误触行的打开动作。
|
|
379
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 =
|
|
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.
|
|
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": [
|
|
@@ -55,11 +55,12 @@
|
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
57
|
"engines": {
|
|
58
|
-
"node": ">=20"
|
|
58
|
+
"node": ">=20",
|
|
59
|
+
"dsh": ">=0.2.1-rc.5 <0.3.0-0"
|
|
59
60
|
},
|
|
60
61
|
"scripts": {
|
|
61
|
-
"test": "node --test test/host.test.mjs test/client.test.mjs",
|
|
62
|
-
"test:local": "node --test --test-isolation=none test/host.test.mjs test/client.test.mjs",
|
|
62
|
+
"test": "node --test test/host.test.mjs test/manifest.test.mjs test/client.test.mjs",
|
|
63
|
+
"test:local": "node --test --test-isolation=none test/host.test.mjs test/manifest.test.mjs test/client.test.mjs",
|
|
63
64
|
"check": "node --check lib/index.js && node --check lib/client.js && node --check lib/host/paths.js && node --check lib/host/security.js && node --check lib/host/launch.js && node --check lib/host/route.js && node --check lib/host/session-delete.js && node --check lib/host/workspace-delete.js",
|
|
64
65
|
"build": "npm pack",
|
|
65
66
|
"marketplace:check": "node scripts/marketplace-preflight.mjs",
|
|
@@ -65,6 +65,13 @@ String(pkg.repository?.url ?? '').includes(repoSlug)
|
|
|
65
65
|
? pass('repository', pkg.repository.url)
|
|
66
66
|
: fail('repository', 'does not point at the GitHub repo');
|
|
67
67
|
|
|
68
|
+
// The market index and the DSH host both read the top-level `engines.dsh`; the
|
|
69
|
+
// nested `dsh.engines.dsh` the organizer-style manifests use is ignored by both.
|
|
70
|
+
const enginesDsh = pkg.engines?.dsh;
|
|
71
|
+
typeof enginesDsh === 'string' && enginesDsh.trim()
|
|
72
|
+
? pass('engines.dsh', enginesDsh)
|
|
73
|
+
: fail('engines.dsh', 'missing — the market would show no supported DSH version');
|
|
74
|
+
|
|
68
75
|
// --- storefront icon --------------------------------------------------------
|
|
69
76
|
|
|
70
77
|
if (!existsSync(iconPath)) {
|