dsh-sidebar-row-menu 1.6.0 → 1.7.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 +16 -0
- package/README.en.md +16 -3
- package/README.md +12 -2
- package/lib/client.js +329 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.7.0 — 2026-11-05
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Per-workspace archived-session toggle in the workspace menu.** A workspace
|
|
8
|
+
whose group holds archived sessions now gets "Show archived sessions" /
|
|
9
|
+
"Hide archived sessions" in its right-click menu (and in the row's "…" menu
|
|
10
|
+
when opted in), using the same archive glyphs as the native View options
|
|
11
|
+
items. DSH only offers one global archived filter; the plugin switches it to
|
|
12
|
+
"all conversations" and hides every other workspace's (and "Ungrouped")
|
|
13
|
+
archived rows, so each workspace can reveal its own archived sessions
|
|
14
|
+
independently. The choice persists across reloads, and changing the native filter clears it so the
|
|
15
|
+
host's global choice stays authoritative. The entry can be switched off under
|
|
16
|
+
Settings → row menu and is only shown when the workspace actually has
|
|
17
|
+
archived sessions.
|
|
18
|
+
|
|
3
19
|
## 1.6.0 — 2026-11-05
|
|
4
20
|
|
|
5
21
|
### Added
|
package/README.en.md
CHANGED
|
@@ -44,8 +44,8 @@ its own entries; it does not copy or hardcode any built-in action.
|
|
|
44
44
|
On 0.2.2 the host renders this menu, and the plugin's items interleave with the
|
|
45
45
|
built-in Rename (100), Reveal in file manager (200) and Delete workspace (300),
|
|
46
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,
|
|
48
|
-
and all sessions 310. These items are right-click only by default; the row's "…"
|
|
47
|
+
tab 110, archived-session toggle 130, Copy workspace path 150, pinned app 210,
|
|
48
|
+
Open with 220, Delete workspace and all sessions 310. These items are right-click only by default; the row's "…"
|
|
49
49
|
menu keeps its native content unless you opt entries in under Settings → "Also
|
|
50
50
|
show in the "…" menu → workspace". On 0.2.1 the plugin draws the whole menu and
|
|
51
51
|
runs the official dialogs for Rename and Delete workspace.
|
|
@@ -55,6 +55,7 @@ runs the official dialogs for Rename and Delete workspace.
|
|
|
55
55
|
| New session | Expands the workspace and starts a session (same as the row's `+`) |
|
|
56
56
|
| Rename (built-in) | Opens the official "Rename workspace" dialog; on 0.2.1 the plugin renders this item itself |
|
|
57
57
|
| Open in new tab | Only when `dsh-project-tabs` (exposing `openWorkspaceInTab`) is installed |
|
|
58
|
+
| Show / hide archived sessions | Only on workspaces that have archived sessions; the label follows the current state. "Show archived sessions" switches the sidebar's global archived filter to all conversations and then hides every other workspace's archived rows (including "Ungrouped"), so only this workspace's archived sessions appear; "Hide archived sessions" puts them back. Per-workspace choices persist across reloads; changing the global filter under "View options" wins over them |
|
|
58
59
|
| Copy workspace path | Copies the directory path |
|
|
59
60
|
| 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
61
|
| 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 |
|
|
@@ -124,7 +125,8 @@ Settings → "Row menu" toggles every new entry:
|
|
|
124
125
|
| Delete session | Hides the single-session delete entry in session menus |
|
|
125
126
|
| Allow deleting unarchived sessions | Off by default; also shows "Delete session permanently" on unarchived rows. Requires the previous switch |
|
|
126
127
|
| Delete all archived sessions | Hides the bulk-delete entry |
|
|
127
|
-
|
|
|
128
|
+
| Show archived sessions | Hides the workspace "Show / hide archived sessions" entry (on by default; the entry itself only appears when that workspace has archived sessions) |
|
|
129
|
+
| 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, archived-session toggle, Delete workspace and all sessions. On 0.2.1 enabling any of them makes this plugin render the "…" menu |
|
|
128
130
|
| 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 |
|
|
129
131
|
|
|
130
132
|
The "Also show in the "…" menu" switches are all off by default; until one is
|
|
@@ -370,6 +372,17 @@ trigger (`memoizedProps.onContextMenu` is a function): if it does, the host open
|
|
|
370
372
|
menu at the pointer by itself and the plugin only drops a gesture mark, never
|
|
371
373
|
touching the event.
|
|
372
374
|
|
|
375
|
+
### Why archived sessions cannot use the global filter alone
|
|
376
|
+
|
|
377
|
+
DSH exposes one global archived-visibility filter (`archivedFilter`: hide archived /
|
|
378
|
+
all conversations / archived only) and no per-workspace equivalent. The plugin builds
|
|
379
|
+
the per-workspace layer on top of it: it drives the host view action to "all
|
|
380
|
+
conversations", then hides other workspaces' (and "Ungrouped") archived rows with an inline
|
|
381
|
+
`display:none`. The snapshot's `archivedSessionIds` tells it which rows are archived,
|
|
382
|
+
and a MutationObserver re-applies the hiding whenever React replaces the row nodes.
|
|
383
|
+
The plugin does not keep the global filter hostage: once the user picks a filter under
|
|
384
|
+
"View options", the per-workspace hidden set is cleared and the host's choice wins.
|
|
385
|
+
|
|
373
386
|
### Why session actions are not hardcoded
|
|
374
387
|
|
|
375
388
|
The native session menu is not a hardcoded button list: it is
|
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ DSH 0.2.2 把侧栏右键改成数据驱动:工作区菜单由宿主统一渲
|
|
|
34
34
|
|
|
35
35
|
0.2.2 上这份菜单由宿主渲染,本插件条目与内置「重命名」(100)、「在文件夹显示」(200)、
|
|
36
36
|
「删除工作区」(300)按 order 交错,分组规则与会话菜单一致:新会话 90、在新标签打开 110、
|
|
37
|
-
|
|
37
|
+
归档会话开关 130、复制工作区路径 150、置顶应用 210、打开方式 220、删除工作区及所有会话 310。这些条目默认
|
|
38
38
|
只出现在右键菜单;行尾「…」里的菜单保持原生,想让它也带上插件条目,可在设置里逐项开启
|
|
39
39
|
「同时放进「…」菜单 → 工作区」。0.2.1 上整份菜单由本插件自绘,「重命名」「删除工作区」
|
|
40
40
|
由插件调用官方对话框。
|
|
@@ -44,6 +44,7 @@ DSH 0.2.2 把侧栏右键改成数据驱动:工作区菜单由宿主统一渲
|
|
|
44
44
|
| 新会话 | 展开该工作区并新建会话(等价于行尾 `+`) |
|
|
45
45
|
| 重命名(内置) | 打开官方“重命名工作区”对话框;0.2.1 上由本插件代为渲染 |
|
|
46
46
|
| 在新标签中打开 | 仅在装有 `dsh-project-tabs`(暴露 `openWorkspaceInTab`)时出现 |
|
|
47
|
+
| 显示 / 隐藏已归档会话 | 只在该工作区有已归档会话时出现,文案随当前状态切换。点击「显示已归档会话」会把侧边栏的全局归档筛选切到「全部对话」,再藏起其他工作区(含「未分组」)的归档行,于是只有这个工作区的已归档会话露出来;点击「隐藏已归档会话」把它重新收回。逐工作区的选择存在本地、刷新后保持;在「视图选项」里改全局筛选则以全局选择为准 |
|
|
47
48
|
| 复制工作区路径 | 复制目录路径到剪贴板 |
|
|
48
49
|
| 在文件夹显示(内置) | 在资源管理器里打开工作区目录;0.2.2 由宿主提供,0.2.1 上不出现 |
|
|
49
50
|
| 置顶的打开方式 | 被置顶的应用直接显示在一级菜单(例如「用 Visual Studio Code 打开」),点击后用该应用打开工作区目录 |
|
|
@@ -102,7 +103,8 @@ DSH 0.2.2 把侧栏右键改成数据驱动:工作区菜单由宿主统一渲
|
|
|
102
103
|
| 永久删除会话 | 关闭后会话菜单里不再出现单条删除入口 |
|
|
103
104
|
| 允许删除未归档会话 | 默认关闭;开启后未归档会话的右键菜单也会出现「永久删除会话」,需先开启上一项 |
|
|
104
105
|
| 删除所有已归档会话 | 关闭后不再出现批量删除入口 |
|
|
105
|
-
|
|
|
106
|
+
| 显示已归档会话 | 关闭后工作区菜单里不再出现「显示 / 隐藏已归档会话」(默认开启;该项只在当前工作区存在已归档会话时出现) |
|
|
107
|
+
| 同时放进「…」菜单 → 工作区 | 逐项把工作区行右键条目也放进行尾「…」菜单:新会话、打开方式、复制工作区路径、在新标签中打开、归档会话开关、删除工作区及所有会话。0.2.1 上开启任意一项后「…」按钮改由本插件渲染 |
|
|
106
108
|
| 同时放进「…」菜单 → 会话 | 逐项把会话行右键条目加进原生「…」菜单:打开方式、永久删除会话、删除所有已归档会话 |
|
|
107
109
|
|
|
108
110
|
「同时放进「…」菜单」下的开关默认全部关闭,`…` 菜单在未开启时与 DSH 完全一致。
|
|
@@ -321,6 +323,14 @@ test/*.test.mjs node:test
|
|
|
321
323
|
这一行是否天生带宿主右键触发器(`memoizedProps.onContextMenu` 是函数):有就说明宿主会把
|
|
322
324
|
菜单开在指针处,插件只补一个手势标记,不再干预事件。
|
|
323
325
|
|
|
326
|
+
### 归档会话为什么不是纯全局筛选
|
|
327
|
+
|
|
328
|
+
DSH 的归档可见性只有一个全局筛选(`archivedFilter`:隐藏已归档 / 全部对话 / 仅显示已归档),
|
|
329
|
+
没有逐工作区的概念。插件把「逐工作区」做成两层:先通过宿主视图动作把全局切到「全部对话」,
|
|
330
|
+
再把其他工作区(含「未分组」)的归档行用行内 `display:none` 藏起来;快照里的 `archivedSessionIds` 决定哪些
|
|
331
|
+
行是归档行,React 重渲染替换掉节点后由 MutationObserver 重新隐藏。插件不长期改写全局筛选:
|
|
332
|
+
用户在「视图选项」里改过之后,之前逐工作区的隐藏会被清空,完全跟随宿主的选择。
|
|
333
|
+
|
|
324
334
|
### 会话行为什么不硬编码
|
|
325
335
|
|
|
326
336
|
内置会话菜单不是硬编码的按钮列表,而是
|
package/lib/client.js
CHANGED
|
@@ -69,6 +69,8 @@ window.__ModuleLoader__.load({
|
|
|
69
69
|
IconWarningOutlineRegular,
|
|
70
70
|
IconCopyOutlineRegular,
|
|
71
71
|
IconChevronRightOutlineRegular,
|
|
72
|
+
IconArchiveOutlineRegular,
|
|
73
|
+
IconArchiveOffOutlineRegular,
|
|
72
74
|
IconRightUpOutlineRegular,
|
|
73
75
|
IconTriangleRightFillRegular,
|
|
74
76
|
} = require('@deepseek-ai/dsh-client-ui-primitives');
|
|
@@ -112,6 +114,7 @@ window.__ModuleLoader__.load({
|
|
|
112
114
|
*/
|
|
113
115
|
const ITEM_DEFAULTS = {
|
|
114
116
|
openWith: true,
|
|
117
|
+
archivedSessions: true,
|
|
115
118
|
deleteSession: true,
|
|
116
119
|
deleteArchived: true,
|
|
117
120
|
deleteUnarchived: false,
|
|
@@ -122,8 +125,16 @@ window.__ModuleLoader__.load({
|
|
|
122
125
|
* 全部默认关闭:不勾选时点「…」看到的仍是 DSH 原生菜单。
|
|
123
126
|
*/
|
|
124
127
|
const ELLIPSIS_KEY = 'dsh.sidebar-row-menu.ellipsis.v1';
|
|
128
|
+
/**
|
|
129
|
+
* 按工作区「已隐藏归档会话」的集合。宿主只有全局归档筛选(default/show/only),
|
|
130
|
+
* 没有「这个工作区显示归档、那个工作区不显示」的概念,所以这里记住要单独
|
|
131
|
+
* 隐藏归档行的工作区,再把全局筛选开到 show,最后在 DOM 里把其余工作区的
|
|
132
|
+
* 归档行藏起来。
|
|
133
|
+
*/
|
|
134
|
+
const ARCHIVED_HIDDEN_KEY = 'dsh.sidebar-row-menu.archived-hidden.v1';
|
|
125
135
|
const ELLIPSIS_DEFAULTS = {
|
|
126
136
|
newSession: false,
|
|
137
|
+
showArchived: false,
|
|
127
138
|
openWith: false,
|
|
128
139
|
copyPath: false,
|
|
129
140
|
openInTab: false,
|
|
@@ -154,6 +165,8 @@ window.__ModuleLoader__.load({
|
|
|
154
165
|
openInTab: '在新标签中打开',
|
|
155
166
|
deleteWorkspace: '删除工作区',
|
|
156
167
|
deleteWorkspaceSessions: '删除工作区及所有会话',
|
|
168
|
+
showArchivedSessions: '显示已归档会话',
|
|
169
|
+
hideArchivedSessions: '隐藏已归档会话',
|
|
157
170
|
copied: '已复制',
|
|
158
171
|
copyFailed: '复制失败,请重试',
|
|
159
172
|
noPath: '没有可用的目录路径',
|
|
@@ -166,6 +179,8 @@ window.__ModuleLoader__.load({
|
|
|
166
179
|
'settings.title': '行菜单',
|
|
167
180
|
'settings.openWith.label': '打开方式',
|
|
168
181
|
'settings.openWith.hint': '在子菜单里选择编辑器、终端或文件管理器打开工作区或会话目录;置顶的应用直接显示在菜单一级。',
|
|
182
|
+
'settings.archivedSessions.label': '显示已归档会话',
|
|
183
|
+
'settings.archivedSessions.hint': '在工作区右键菜单里单独显示或隐藏该工作区内所有已归档会话,不影响其他工作区。',
|
|
169
184
|
'settings.deleteSession.label': '永久删除会话',
|
|
170
185
|
'settings.deleteSession.hint': '在会话菜单里永久删除这一个会话及其日志;默认只对已归档会话显示。',
|
|
171
186
|
'settings.deleteArchived.label': '删除所有已归档会话',
|
|
@@ -191,6 +206,8 @@ window.__ModuleLoader__.load({
|
|
|
191
206
|
'settings.ellipsisSession': '会话行',
|
|
192
207
|
'settings.ellipsis.newSession.label': '新建会话',
|
|
193
208
|
'settings.ellipsis.newSession.hint': '在「…」菜单里直接新建该工作区的会话。',
|
|
209
|
+
'settings.ellipsis.showArchived.label': '显示已归档会话',
|
|
210
|
+
'settings.ellipsis.showArchived.hint': '在「…」菜单里单独显示或隐藏该工作区的已归档会话。',
|
|
194
211
|
'settings.ellipsis.openWith.label': '打开方式',
|
|
195
212
|
'settings.ellipsis.openWith.hint': '在「…」菜单里用外部应用打开工作区或会话目录。',
|
|
196
213
|
'settings.ellipsis.copyPath.label': '拷贝工作区路径',
|
|
@@ -238,6 +255,8 @@ window.__ModuleLoader__.load({
|
|
|
238
255
|
openInTab: 'Open in new tab',
|
|
239
256
|
deleteWorkspace: 'Delete workspace',
|
|
240
257
|
deleteWorkspaceSessions: 'Delete workspace and all sessions',
|
|
258
|
+
showArchivedSessions: 'Show archived sessions',
|
|
259
|
+
hideArchivedSessions: 'Hide archived sessions',
|
|
241
260
|
copied: 'Copied',
|
|
242
261
|
copyFailed: 'Copy failed, please retry',
|
|
243
262
|
noPath: 'No usable directory path',
|
|
@@ -250,6 +269,8 @@ window.__ModuleLoader__.load({
|
|
|
250
269
|
'settings.title': 'Row menu',
|
|
251
270
|
'settings.openWith.label': 'Open with',
|
|
252
271
|
'settings.openWith.hint': 'Pick an editor, terminal or file manager for workspace or session directories; pinned apps show up directly in the menu.',
|
|
272
|
+
'settings.archivedSessions.label': 'Show archived sessions',
|
|
273
|
+
'settings.archivedSessions.hint': 'Show or hide this workspace\'s archived sessions from its context menu, without affecting other workspaces.',
|
|
253
274
|
'settings.deleteSession.label': 'Delete session permanently',
|
|
254
275
|
'settings.deleteSession.hint': 'Permanently delete one session and its transcript from its menu; shown on archived sessions by default.',
|
|
255
276
|
'settings.deleteArchived.label': 'Delete all archived sessions',
|
|
@@ -275,6 +296,8 @@ window.__ModuleLoader__.load({
|
|
|
275
296
|
'settings.ellipsisSession': 'Session rows',
|
|
276
297
|
'settings.ellipsis.newSession.label': 'New session',
|
|
277
298
|
'settings.ellipsis.newSession.hint': 'Create a session for this workspace from the "…" menu.',
|
|
299
|
+
'settings.ellipsis.showArchived.label': 'Show archived sessions',
|
|
300
|
+
'settings.ellipsis.showArchived.hint': 'Show or hide this workspace\'s archived sessions from the "…" menu.',
|
|
278
301
|
'settings.ellipsis.openWith.label': 'Open with',
|
|
279
302
|
'settings.ellipsis.openWith.hint': 'Open the workspace or session directory with an external app from the "…" menu.',
|
|
280
303
|
'settings.ellipsis.copyPath.label': 'Copy workspace path',
|
|
@@ -1477,6 +1500,7 @@ window.__ModuleLoader__.load({
|
|
|
1477
1500
|
['deleteSession', 'settings.deleteSession.label', 'settings.deleteSession.hint'],
|
|
1478
1501
|
['deleteUnarchived', 'settings.deleteUnarchived.label', 'settings.deleteUnarchived.hint'],
|
|
1479
1502
|
['deleteArchived', 'settings.deleteArchived.label', 'settings.deleteArchived.hint'],
|
|
1503
|
+
['archivedSessions', 'settings.archivedSessions.label', 'settings.archivedSessions.hint'],
|
|
1480
1504
|
];
|
|
1481
1505
|
|
|
1482
1506
|
/** 「…」菜单附加开关;按行分组,避免用户分不清工作区项和会话项。 */
|
|
@@ -1486,6 +1510,7 @@ window.__ModuleLoader__.load({
|
|
|
1486
1510
|
['openWith', 'settings.ellipsis.openWith.label', 'settings.ellipsis.openWith.hint'],
|
|
1487
1511
|
['copyPath', 'settings.ellipsis.copyPath.label', 'settings.ellipsis.copyPath.hint'],
|
|
1488
1512
|
['openInTab', 'settings.ellipsis.openInTab.label', 'settings.ellipsis.openInTab.hint'],
|
|
1513
|
+
['showArchived', 'settings.ellipsis.showArchived.label', 'settings.ellipsis.showArchived.hint'],
|
|
1489
1514
|
['deleteSessions', 'settings.ellipsis.deleteSessions.label', 'settings.ellipsis.deleteSessions.hint'],
|
|
1490
1515
|
]],
|
|
1491
1516
|
['session', 'settings.ellipsisSession', [
|
|
@@ -1744,7 +1769,9 @@ window.__ModuleLoader__.load({
|
|
|
1744
1769
|
return;
|
|
1745
1770
|
}
|
|
1746
1771
|
copyText(cwd, t);
|
|
1772
|
+
return;
|
|
1747
1773
|
}
|
|
1774
|
+
if (id === 'archived') toggleWorkspaceArchived(workspaceId);
|
|
1748
1775
|
};
|
|
1749
1776
|
|
|
1750
1777
|
const rows = [];
|
|
@@ -1813,6 +1840,19 @@ window.__ModuleLoader__.load({
|
|
|
1813
1840
|
children: t('openInTab'),
|
|
1814
1841
|
}));
|
|
1815
1842
|
}
|
|
1843
|
+
const archivedCount = items.archivedSessions === false ? 0 : archivedCountOfWorkspace(workspaceId);
|
|
1844
|
+
if (archivedCount > 0 && show('showArchived')) {
|
|
1845
|
+
const archivedVisible = archivedRowsVisibleIn(workspaceId);
|
|
1846
|
+
rows.push(jsx(MenuItemButton, {
|
|
1847
|
+
key: 'archived',
|
|
1848
|
+
onSelect: () => {
|
|
1849
|
+
onClose();
|
|
1850
|
+
run('archived');
|
|
1851
|
+
},
|
|
1852
|
+
icon: jsx(archivedVisible ? IconArchiveOffOutlineRegular : IconArchiveOutlineRegular, { size: 14 }),
|
|
1853
|
+
children: t(archivedVisible ? 'hideArchivedSessions' : 'showArchivedSessions'),
|
|
1854
|
+
}));
|
|
1855
|
+
}
|
|
1816
1856
|
rows.push(jsx(MenuItemButton, {
|
|
1817
1857
|
key: 'delete',
|
|
1818
1858
|
danger: true,
|
|
@@ -2065,6 +2105,275 @@ window.__ModuleLoader__.load({
|
|
|
2065
2105
|
});
|
|
2066
2106
|
}
|
|
2067
2107
|
|
|
2108
|
+
/* --------------------- 工作区归档会话:按工作区单独显示/隐藏 */
|
|
2109
|
+
|
|
2110
|
+
function readHiddenWorkspaces() {
|
|
2111
|
+
const hidden = new Set();
|
|
2112
|
+
try {
|
|
2113
|
+
const raw = globalThis.localStorage?.getItem(ARCHIVED_HIDDEN_KEY);
|
|
2114
|
+
if (typeof raw !== 'string' || raw === '') return hidden;
|
|
2115
|
+
const parsed = JSON.parse(raw);
|
|
2116
|
+
if (!Array.isArray(parsed)) return hidden;
|
|
2117
|
+
for (const id of parsed) {
|
|
2118
|
+
if (typeof id === 'string' && id !== '') hidden.add(id);
|
|
2119
|
+
}
|
|
2120
|
+
} catch {
|
|
2121
|
+
/* 坏数据按空集合处理 */
|
|
2122
|
+
}
|
|
2123
|
+
return hidden;
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
let hiddenArchivedWorkspaces = readHiddenWorkspaces();
|
|
2127
|
+
|
|
2128
|
+
function persistHiddenWorkspaces() {
|
|
2129
|
+
try {
|
|
2130
|
+
globalThis.localStorage?.setItem(ARCHIVED_HIDDEN_KEY, JSON.stringify([...hiddenArchivedWorkspaces]));
|
|
2131
|
+
} catch {
|
|
2132
|
+
/* 隐私模式写不进去也照样生效到本次会话 */
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
/**
|
|
2137
|
+
* 读宿主工作区视图的归档筛选。插件没有公开 API 订阅宿主视图 store,但视图
|
|
2138
|
+
* 状态会持久化到 localStorage 的 `dsh.workspace.view.vN`(筛选一变就写),
|
|
2139
|
+
* 所以读这里就能知道用户是否在「视图选项」里改过全局筛选。
|
|
2140
|
+
*/
|
|
2141
|
+
function readViewFilter() {
|
|
2142
|
+
try {
|
|
2143
|
+
const storage = globalThis.localStorage;
|
|
2144
|
+
if (storage === null || storage === undefined || typeof storage.key !== 'function') return 'default';
|
|
2145
|
+
const keys = [];
|
|
2146
|
+
for (let index = 0; index < storage.length; index += 1) {
|
|
2147
|
+
const key = storage.key(index);
|
|
2148
|
+
if (typeof key === 'string' && key.startsWith('dsh.workspace.view.')) keys.push(key);
|
|
2149
|
+
}
|
|
2150
|
+
// 版本号是数字后缀,v10 必须排在 v5 前面。
|
|
2151
|
+
const versionOf = (key) => {
|
|
2152
|
+
const match = /\.v(\d+)$/.exec(key);
|
|
2153
|
+
return match === null ? -1 : Number(match[1]);
|
|
2154
|
+
};
|
|
2155
|
+
keys.sort((left, right) => versionOf(right) - versionOf(left));
|
|
2156
|
+
for (const key of keys) {
|
|
2157
|
+
const parsed = JSON.parse(storage.getItem(key) ?? '');
|
|
2158
|
+
if (parsed !== null && typeof parsed === 'object' && typeof parsed.archivedFilter === 'string') {
|
|
2159
|
+
return parsed.archivedFilter;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
} catch {
|
|
2163
|
+
/* 视图状态不可读时按默认筛选处理 */
|
|
2164
|
+
}
|
|
2165
|
+
return 'default';
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
/**
|
|
2169
|
+
* 宿主把 view actions 挂在 WorkspaceBrowser 组件的 props 上;插件没有服务可以
|
|
2170
|
+
* 拿到它,只能从行的 __reactFiber$ 往上找。找不到时静默放弃(筛选仍可手动切)。
|
|
2171
|
+
*/
|
|
2172
|
+
function findArchivedViewActions() {
|
|
2173
|
+
if (typeof document === 'undefined' || typeof document.querySelector !== 'function') return null;
|
|
2174
|
+
const row = document.querySelector('[data-row-key^="session:"], [data-row-key^="workspace:"]');
|
|
2175
|
+
if (row === null || row === undefined) return null;
|
|
2176
|
+
const field = Object.keys(row).find((name) => name.startsWith('__reactFiber$'));
|
|
2177
|
+
let fiber = field === undefined ? null : row[field];
|
|
2178
|
+
for (let depth = 0; depth < 48 && fiber !== null && fiber !== undefined; depth += 1) {
|
|
2179
|
+
const actions = fiber.memoizedProps?.actions;
|
|
2180
|
+
if (actions !== null && typeof actions === 'object' && typeof actions.setArchivedFilter === 'function') {
|
|
2181
|
+
return actions;
|
|
2182
|
+
}
|
|
2183
|
+
fiber = fiber.return;
|
|
2184
|
+
}
|
|
2185
|
+
return null;
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
let lastViewFilter = readViewFilter();
|
|
2189
|
+
|
|
2190
|
+
function writeViewFilter(filter) {
|
|
2191
|
+
const actions = findArchivedViewActions();
|
|
2192
|
+
if (actions === null) return false;
|
|
2193
|
+
try {
|
|
2194
|
+
actions.setArchivedFilter(filter);
|
|
2195
|
+
lastViewFilter = filter;
|
|
2196
|
+
return true;
|
|
2197
|
+
} catch (error) {
|
|
2198
|
+
console.warn('[dsh-sidebar-row-menu] archived filter change failed:', error);
|
|
2199
|
+
return false;
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
/** 当前快照里的归档会话集合,以及每个会话所属工作区(未归组的会话没有)。 */
|
|
2204
|
+
function archivedWorkspaceIndex() {
|
|
2205
|
+
const snapshot = serviceOf(rootCtx, 'workspaces')?.list?.getSnapshot?.();
|
|
2206
|
+
const archived = new Set(Array.isArray(snapshot?.archivedSessionIds) ? snapshot.archivedSessionIds : []);
|
|
2207
|
+
const owner = new Map();
|
|
2208
|
+
for (const item of Array.isArray(snapshot?.items) ? snapshot.items : []) {
|
|
2209
|
+
const workspaceId = typeof item?.workspaceId === 'string' ? item.workspaceId : '';
|
|
2210
|
+
if (workspaceId === '' || !Array.isArray(item?.sessionIds)) continue;
|
|
2211
|
+
for (const sessionId of item.sessionIds) {
|
|
2212
|
+
if (typeof sessionId === 'string' && sessionId !== '') owner.set(sessionId, workspaceId);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
return { archived, owner };
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
function archivedCountOfWorkspace(workspaceId) {
|
|
2219
|
+
const { archived } = archivedWorkspaceIndex();
|
|
2220
|
+
let count = 0;
|
|
2221
|
+
for (const sessionId of workspaceSessionIds(workspaceId)) {
|
|
2222
|
+
if (archived.has(sessionId)) count += 1;
|
|
2223
|
+
}
|
|
2224
|
+
return count;
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
/**
|
|
2228
|
+
* 归档会话的归属桶:工作区 id,未归组的会话算在 UNGROUPED_OWNER 这个桶里。
|
|
2229
|
+
* 逐工作区显示时未归组的归档也要一起藏起来,否则它们会跟着全局 show 漏出来。
|
|
2230
|
+
*/
|
|
2231
|
+
const UNGROUPED_OWNER = '';
|
|
2232
|
+
|
|
2233
|
+
function archivedHolders() {
|
|
2234
|
+
const { archived, owner } = archivedWorkspaceIndex();
|
|
2235
|
+
const holders = new Set();
|
|
2236
|
+
for (const sessionId of archived) holders.add(owner.get(sessionId) ?? UNGROUPED_OWNER);
|
|
2237
|
+
return holders;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
function archivedRowsVisibleIn(workspaceId) {
|
|
2241
|
+
return readViewFilter() !== 'default' && !hiddenArchivedWorkspaces.has(workspaceId);
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
/**
|
|
2245
|
+
* 逐工作区开关:宿主只有全局筛选,所以「显示某工作区」= 全局切到 show,
|
|
2246
|
+
* 再把其他所有归档归属桶(含未归组)记进隐藏集合;「隐藏」= 记进隐藏集合,
|
|
2247
|
+
* 全部藏完后把全局收回 default,侧边栏恢复原样。
|
|
2248
|
+
*/
|
|
2249
|
+
function toggleWorkspaceArchived(workspaceId) {
|
|
2250
|
+
if (workspaceId === '') return;
|
|
2251
|
+
const filter = readViewFilter();
|
|
2252
|
+
const visible = filter !== 'default' && !hiddenArchivedWorkspaces.has(workspaceId);
|
|
2253
|
+
if (visible) {
|
|
2254
|
+
hiddenArchivedWorkspaces.add(workspaceId);
|
|
2255
|
+
} else {
|
|
2256
|
+
hiddenArchivedWorkspaces.delete(workspaceId);
|
|
2257
|
+
// 从「隐藏已归档」进入逐工作区显示:只留这一个工作区,其他桶全部藏掉。
|
|
2258
|
+
if (filter === 'default') {
|
|
2259
|
+
for (const holder of archivedHolders()) {
|
|
2260
|
+
if (holder !== workspaceId) hiddenArchivedWorkspaces.add(holder);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
// 清掉已经没有归档会话的桶,避免隐藏集合里堆着永远不会再出现的 id。
|
|
2265
|
+
const holders = archivedHolders();
|
|
2266
|
+
for (const id of [...hiddenArchivedWorkspaces]) {
|
|
2267
|
+
if (!holders.has(id)) hiddenArchivedWorkspaces.delete(id);
|
|
2268
|
+
}
|
|
2269
|
+
persistHiddenWorkspaces();
|
|
2270
|
+
if (!visible) {
|
|
2271
|
+
if (filter === 'default') writeViewFilter('show');
|
|
2272
|
+
} else if (filter === 'show') {
|
|
2273
|
+
// 有归档会话的桶全部被藏起来后,就没必要再把全局筛选停在 show。
|
|
2274
|
+
if (holders.size > 0 && [...holders].every((id) => hiddenArchivedWorkspaces.has(id))) {
|
|
2275
|
+
hiddenArchivedWorkspaces = new Set();
|
|
2276
|
+
persistHiddenWorkspaces();
|
|
2277
|
+
writeViewFilter('default');
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
scheduleArchivedRows();
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
const ARCHIVED_ROW_ATTR = 'data-dsrm-archived-hidden';
|
|
2284
|
+
|
|
2285
|
+
/**
|
|
2286
|
+
* 把不需要显示的归档行藏掉。宿主在 show 模式下会把所有工作区的归档行都渲染
|
|
2287
|
+
* 出来(没有逐工作区筛选),所以这里只能按 data-row-key 的会话 id 与快照
|
|
2288
|
+
* 里的归属关系决定藏哪些行。行组件重渲染时会被整体替换,MutationObserver
|
|
2289
|
+
* 会再次跑一遍。
|
|
2290
|
+
*/
|
|
2291
|
+
function applyArchivedRows() {
|
|
2292
|
+
if (typeof document === 'undefined' || typeof document.querySelectorAll !== 'function') return;
|
|
2293
|
+
syncViewFilter();
|
|
2294
|
+
const filter = readViewFilter();
|
|
2295
|
+
const active = (filter === 'show' || filter === 'only') && hiddenArchivedWorkspaces.size > 0;
|
|
2296
|
+
const { archived, owner } = active ? archivedWorkspaceIndex() : { archived: new Set(), owner: new Map() };
|
|
2297
|
+
for (const row of document.querySelectorAll('[data-row-key^="session:"]')) {
|
|
2298
|
+
const sessionId = row.getAttribute('data-row-key').slice('session:'.length);
|
|
2299
|
+
const bucket = owner.get(sessionId) ?? UNGROUPED_OWNER;
|
|
2300
|
+
const hide = active && archived.has(sessionId) && hiddenArchivedWorkspaces.has(bucket);
|
|
2301
|
+
if (hide) {
|
|
2302
|
+
if (row.style.display !== 'none') {
|
|
2303
|
+
row.setAttribute(ARCHIVED_ROW_ATTR, '1');
|
|
2304
|
+
row.style.display = 'none';
|
|
2305
|
+
}
|
|
2306
|
+
} else if (row.getAttribute(ARCHIVED_ROW_ATTR) === '1') {
|
|
2307
|
+
row.removeAttribute(ARCHIVED_ROW_ATTR);
|
|
2308
|
+
row.style.display = '';
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
/** 用户在「视图选项」里改了全局筛选:以全局选择为准,清掉逐工作区隐藏。 */
|
|
2314
|
+
function syncViewFilter() {
|
|
2315
|
+
const current = readViewFilter();
|
|
2316
|
+
if (current === lastViewFilter) return;
|
|
2317
|
+
lastViewFilter = current;
|
|
2318
|
+
hiddenArchivedWorkspaces = new Set();
|
|
2319
|
+
persistHiddenWorkspaces();
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
let archivedApplyTimer = 0;
|
|
2323
|
+
let archivedObserver = null;
|
|
2324
|
+
|
|
2325
|
+
function scheduleArchivedRows() {
|
|
2326
|
+
if (typeof setTimeout !== 'function') {
|
|
2327
|
+
applyArchivedRows();
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
if (archivedApplyTimer !== 0) return;
|
|
2331
|
+
archivedApplyTimer = setTimeout(() => {
|
|
2332
|
+
archivedApplyTimer = 0;
|
|
2333
|
+
applyArchivedRows();
|
|
2334
|
+
}, 40);
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
function archivedRowMutation(records) {
|
|
2338
|
+
for (const record of records) {
|
|
2339
|
+
for (const node of record.addedNodes) {
|
|
2340
|
+
if (node.nodeType === 1 && (node.matches?.('[data-row-key]') || node.querySelector?.('[data-row-key]'))) {
|
|
2341
|
+
return true;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
for (const node of record.removedNodes) {
|
|
2345
|
+
if (node.nodeType === 1 && (node.matches?.('[data-row-key]') || node.querySelector?.('[data-row-key]'))) {
|
|
2346
|
+
return true;
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
return false;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
function startArchivedRows() {
|
|
2354
|
+
scheduleArchivedRows();
|
|
2355
|
+
if (typeof MutationObserver !== 'function') return () => {};
|
|
2356
|
+
if (typeof document === 'undefined' || document.body === null || document.body === undefined) return () => {};
|
|
2357
|
+
archivedObserver = new MutationObserver((records) => {
|
|
2358
|
+
if (archivedRowMutation(records)) scheduleArchivedRows();
|
|
2359
|
+
});
|
|
2360
|
+
archivedObserver.observe(document.body, { childList: true, subtree: true });
|
|
2361
|
+
return () => {
|
|
2362
|
+
archivedObserver?.disconnect();
|
|
2363
|
+
archivedObserver = null;
|
|
2364
|
+
if (archivedApplyTimer !== 0 && typeof clearTimeout === 'function') {
|
|
2365
|
+
clearTimeout(archivedApplyTimer);
|
|
2366
|
+
archivedApplyTimer = 0;
|
|
2367
|
+
}
|
|
2368
|
+
if (typeof document === 'undefined' || typeof document.querySelectorAll !== 'function') return;
|
|
2369
|
+
for (const row of document.querySelectorAll(`[${ARCHIVED_ROW_ATTR}="1"]`)) {
|
|
2370
|
+
row.removeAttribute(ARCHIVED_ROW_ATTR);
|
|
2371
|
+
row.style.display = '';
|
|
2372
|
+
}
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
|
|
2068
2377
|
function workspaceSessionIds(workspaceId) {
|
|
2069
2378
|
const snapshot = serviceOf(rootCtx, 'workspaces')?.list?.getSnapshot?.();
|
|
2070
2379
|
const entry = Array.isArray(snapshot?.items)
|
|
@@ -2137,6 +2446,20 @@ window.__ModuleLoader__.load({
|
|
|
2137
2446
|
});
|
|
2138
2447
|
}
|
|
2139
2448
|
|
|
2449
|
+
// 归档会话的显示开关:只有该工作区确实存在归档会话时才出现,状态用图标
|
|
2450
|
+
// 与文案区分(宿主菜单项没有 checked 字段)。
|
|
2451
|
+
const archivedCount = itemOn('archivedSessions') ? archivedCountOfWorkspace(workspaceId) : 0;
|
|
2452
|
+
if (archivedCount > 0) {
|
|
2453
|
+
const archivedVisible = archivedRowsVisibleIn(workspaceId);
|
|
2454
|
+
items.push({
|
|
2455
|
+
id: 'archived',
|
|
2456
|
+
group: 'archived',
|
|
2457
|
+
label: t(archivedVisible ? 'hideArchivedSessions' : 'showArchivedSessions'),
|
|
2458
|
+
icon: { key: archivedVisible ? 'sidebar-row-menu.archive-off' : 'sidebar-row-menu.archive' },
|
|
2459
|
+
run: () => toggleWorkspaceArchived(workspaceId),
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2140
2463
|
items.push({
|
|
2141
2464
|
id: 'copy-path',
|
|
2142
2465
|
group: 'copy',
|
|
@@ -2210,6 +2533,7 @@ window.__ModuleLoader__.load({
|
|
|
2210
2533
|
const WORKSPACE_MENU_GROUPS = [
|
|
2211
2534
|
{ key: 'new', order: 90 },
|
|
2212
2535
|
{ key: 'tab', order: 110 },
|
|
2536
|
+
{ key: 'archived', order: 130 },
|
|
2213
2537
|
{ key: 'copy', order: 150 },
|
|
2214
2538
|
{ key: 'pinned', order: 210 },
|
|
2215
2539
|
{ key: 'open-with', order: 220 },
|
|
@@ -2226,6 +2550,7 @@ window.__ModuleLoader__.load({
|
|
|
2226
2550
|
'open-with': 'openWith',
|
|
2227
2551
|
copy: 'copyPath',
|
|
2228
2552
|
tab: 'openInTab',
|
|
2553
|
+
archived: 'showArchived',
|
|
2229
2554
|
delete: 'deleteSessions',
|
|
2230
2555
|
};
|
|
2231
2556
|
|
|
@@ -2248,6 +2573,8 @@ window.__ModuleLoader__.load({
|
|
|
2248
2573
|
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.new' }, IconNewChatOutlineRegular);
|
|
2249
2574
|
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.copy' }, IconCopyOutlineRegular);
|
|
2250
2575
|
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.tab' }, IconProjectAddOutlineRegular);
|
|
2576
|
+
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.archive' }, IconArchiveOutlineRegular);
|
|
2577
|
+
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.archive-off' }, IconArchiveOffOutlineRegular);
|
|
2251
2578
|
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.open-with' }, IconRightUpOutlineRegular);
|
|
2252
2579
|
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.delete' }, IconTrashOutlineRegular);
|
|
2253
2580
|
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.app' }, ContextMenuAppIcon);
|
|
@@ -2668,11 +2995,13 @@ window.__ModuleLoader__.load({
|
|
|
2668
2995
|
ctx.effect(() => installStyles(), 'sidebar-row-menu: styles');
|
|
2669
2996
|
}
|
|
2670
2997
|
const stopWorkspaceMenu = registerWorkspaceContextMenu(ctx);
|
|
2998
|
+
const stopArchivedRows = startArchivedRows();
|
|
2671
2999
|
const teardown = install();
|
|
2672
3000
|
if (ctx !== null && typeof ctx.effect === 'function') {
|
|
2673
3001
|
ctx.effect(() => () => {
|
|
2674
3002
|
rootCtx = null;
|
|
2675
3003
|
stopWorkspaceMenu();
|
|
3004
|
+
stopArchivedRows();
|
|
2676
3005
|
teardown();
|
|
2677
3006
|
}, 'sidebar-row-menu: row context menus');
|
|
2678
3007
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-sidebar-row-menu",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"type": "module",
|
|
5
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": [
|