dsh-session-groups 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +18 -13
- package/lib/client.js +57 -0
- package/lib/index.js +163 -35
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.1 — 2026-11-12
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- 工作区右键 **新建会话**:在当前工作区创建并打开新会话。
|
|
8
|
+
- 分组右键 **在此分组新建会话**:新会话创建后直接归入该分组,并自动展开分组和上级。
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- 桌面端分组数据未落盘:宿主侧改为通过 `webServer` 注册已认证的 `/session-tree` 路由(`load` / `save`),使用 Connection 的 `requestRejection` 校验浏览器凭证;保留结构校验、原子写入和 `.bak` 备份。
|
|
13
|
+
- 宿主路由失败时客户端继续使用 `localStorage` 缓存,不阻塞侧边栏。
|
|
14
|
+
|
|
15
|
+
## 0.2.0 — 2026-11-11
|
|
16
|
+
|
|
17
|
+
- 首个公开版本:工作区内的可嵌套会话分组树、分组/会话排序、移动、导入导出和本地落盘。
|
package/README.md
CHANGED
|
@@ -26,6 +26,8 @@ DSH 原生的层级是:
|
|
|
26
26
|
|
|
27
27
|
- 每个工作区独立维护自己的分组树
|
|
28
28
|
- 新建根分组、新建子分组
|
|
29
|
+
- 工作区右键 **新建会话**:在该工作区直接创建并打开新会话
|
|
30
|
+
- 分组右键 **在此分组新建会话**:新会话创建后直接归入该分组,无需再移动
|
|
29
31
|
- 行内重命名(Enter 提交,Esc 取消)
|
|
30
32
|
- 折叠 / 展开分组,分组标题显示会话数量
|
|
31
33
|
- 通过右键菜单“移动会话”归类会话,也可直接拖拽会话到分组标题
|
|
@@ -37,26 +39,29 @@ DSH 原生的层级是:
|
|
|
37
39
|
- 分组和归类关系自动落盘到 DSH 主目录(原子写入并保留 `.bak` 备份),浏览器 `localStorage` 只作为缓存和降级
|
|
38
40
|
- 支持导出 / 导入分组数据,方便备份、迁移和分享
|
|
39
41
|
- 同一浏览器的多个标签页通过 `localStorage` 事件自动同步
|
|
40
|
-
-
|
|
42
|
+
- 工作区右键菜单增加:新建会话、新建分组、显示全部会话、展开全部分组、折叠全部分组、导出分组数据、导入分组数据
|
|
41
43
|
- 打开有分组的工作区时自动展开被原生“显示更多”隐藏的会话,保证分组树完整
|
|
42
44
|
|
|
43
45
|
## 使用
|
|
44
46
|
|
|
45
47
|
1. 右键工作区标题 → **新建分组**(或右键已有分组 → **新建子分组**)。
|
|
46
48
|
2. 输入分组名,按 Enter。
|
|
47
|
-
3.
|
|
49
|
+
3. 新建会话:
|
|
50
|
+
- 右键工作区 → **新建会话**:在该工作区创建并打开;
|
|
51
|
+
- 右键分组 → **在此分组新建会话**:创建后直接归入该分组。
|
|
52
|
+
4. 归类会话:
|
|
48
53
|
- 右键会话 → **移动会话** → 选择目标分组,或选择 **新建分组并移入…**;或
|
|
49
54
|
- 把会话行拖到分组标题上。
|
|
50
|
-
|
|
55
|
+
5. 在分组标题上:
|
|
51
56
|
- 点击箭头 / 标题:折叠或展开
|
|
52
57
|
- `+`:新建子分组
|
|
53
58
|
- 铅笔:重命名
|
|
54
59
|
- 垃圾桶:删除分组(有确认);会话和子分组会移到上一级并自动展开
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
- 右键:新建会话 / 新建子分组 / 重命名 / 折叠 / 移到顶层 / 分组排序 / 删除
|
|
61
|
+
6. 会话排序:右键组内会话 → **排序** → 移到最前 / 上移 / 下移 / 移到最后;需要回到原生顺序时选 **恢复默认顺序**。
|
|
62
|
+
7. 分组排序:右键分组 → **分组排序** → 移到最前 / 上移 / 下移 / 移到最后;同级分组也可按名称重排。
|
|
63
|
+
8. 要取消归类:右键会话 → **移动会话** → **移出分组**,或把会话拖到“未分组”。
|
|
64
|
+
9. 备份或迁移:右键任意工作区 → **导出分组数据**,会下载一个 JSON 文件;在新机器或重装后右键工作区 → **导入分组数据** 选择该文件即可。导入会替换当前全部分组,不会删除任何会话。
|
|
60
65
|
|
|
61
66
|
> 分组树只出现在原生“分组方式 = 工作区 / 工作区树”的列表中。搜索结果是临时平铺列表,不显示分组;切回工作区列表后分组会恢复。
|
|
62
67
|
|
|
@@ -133,16 +138,16 @@ window.__DSH_SESSION_TREE__.importJson(text) // 弹出确认后整份替换
|
|
|
133
138
|
- `MutationObserver` 监听行增减,重新计算分组布局。
|
|
134
139
|
- 分组右键菜单通过 `ctx.contextMenus` 注册一个插件私有 target;会话/工作区菜单通过官方 API 追加动作。
|
|
135
140
|
- 归类关系按 `workspaceId → sessionId → groupId` 保存;不修改会话数据。
|
|
136
|
-
-
|
|
141
|
+
- 宿主半侧通过 `ctx.webServer` 注册已认证的 `/session-tree` 前缀路由(`load` / `save`),用官方 Connection 的 `requestRejection` 校验浏览器凭证;不依赖浏览器端 `connection.rpc` 的 HTTP 转发。
|
|
137
142
|
- 宿主只做结构校验、大小上限和原子文件写入;名称、父链、归类关系仍由客户端 `sanitizeState` 修正。
|
|
138
|
-
-
|
|
143
|
+
- Connection 服务不可用(例如非 Web combo)时只允许回环地址访问;客户端仍照常退回 `localStorage`。
|
|
139
144
|
|
|
140
145
|
## 兼容性
|
|
141
146
|
|
|
142
147
|
- 目标版本:DSH `0.2.2-rc.1`(桌面端 Web GUI)。
|
|
143
148
|
- 依赖官方行上的 `data-row-key` 约定和 `sidebar.workspaces` 区域的 DOM 结构。DSH 大版本升级后如果行结构变化,需要同步调整 `sectionForElement` / `wrapperOf`。
|
|
144
|
-
- 宿主落盘依赖 Web combo
|
|
145
|
-
- 不使用构建步骤:直接编辑 `lib/client.js`,刷新页面即可生效;修改 `lib/index.js`
|
|
149
|
+
- 宿主落盘依赖 Web combo 的 `webServer` 与 `connection` 服务;`connection` 缺失时只允许回环地址读写,不退回 localStorage。
|
|
150
|
+
- 不使用构建步骤:直接编辑 `lib/client.js`,刷新页面即可生效;修改 `lib/index.js` 后建议重启 DSH。插件管理的关闭/启用可能触发 HMR 重载宿主,但已打开的页面可能仍持有旧客户端 bundle,需要重新加载甚至重启才能恢复。
|
|
146
151
|
|
|
147
152
|
## 已知限制
|
|
148
153
|
|
|
@@ -152,5 +157,5 @@ window.__DSH_SESSION_TREE__.importJson(text) // 弹出确认后整份替换
|
|
|
152
157
|
- 分组数据落在 `$DSH_HOME` 下;同一台机器、同一个 DSH 主目录的多个 profile 共用一份文件,不同主目录不共享,也不会跨设备同步。
|
|
153
158
|
- 导入是“整份替换”,不是合并;导入前建议先导出一份当前数据。
|
|
154
159
|
- 不支持跨工作区移动会话:DSH 的会话归属由会话工作目录/工作区记录决定,插件只保存显示分组;把会话显示到另一个工作区会与原生归属、权限和工作目录语义冲突。跨区需求应通过“在目标工作区新建会话”完成,而不是移动。
|
|
155
|
-
- 落盘依赖 `
|
|
160
|
+
- 落盘依赖 `webServer` 路由和 Connection 认证;`connection` 缺失时只允许回环地址访问。客户端 RPC 失败或宿主不可用时,数据保留在浏览器 `localStorage`,并在 Console 给出警告。
|
|
156
161
|
- 插件通过 DOM 增强官方列表,不提供官方 API 级别的替换;如果 DSH 改变侧边栏 DOM 结构,需要适配。
|
package/lib/client.js
CHANGED
|
@@ -51,6 +51,9 @@ window.__ModuleLoader__.load({
|
|
|
51
51
|
const STRINGS = {
|
|
52
52
|
zh: {
|
|
53
53
|
newGroup: "新建分组",
|
|
54
|
+
newSession: "新建会话",
|
|
55
|
+
newSessionInGroup: "在此分组新建会话",
|
|
56
|
+
sessionCreateFailed: "新建会话失败",
|
|
54
57
|
newSubgroup: "新建子分组",
|
|
55
58
|
rename: "重命名",
|
|
56
59
|
deleteGroup: "删除分组",
|
|
@@ -101,6 +104,9 @@ window.__ModuleLoader__.load({
|
|
|
101
104
|
},
|
|
102
105
|
en: {
|
|
103
106
|
newGroup: "New group",
|
|
107
|
+
newSession: "New session",
|
|
108
|
+
newSessionInGroup: "New session in this group",
|
|
109
|
+
sessionCreateFailed: "Could not create a session",
|
|
104
110
|
newSubgroup: "New subgroup",
|
|
105
111
|
rename: "Rename",
|
|
106
112
|
deleteGroup: "Delete group",
|
|
@@ -1426,6 +1432,51 @@ window.__ModuleLoader__.load({
|
|
|
1426
1432
|
return group;
|
|
1427
1433
|
}
|
|
1428
1434
|
|
|
1435
|
+
async function createSession(wsId, groupId) {
|
|
1436
|
+
const sessions = getService("sessions");
|
|
1437
|
+
if (!sessions || typeof sessions.create !== "function") {
|
|
1438
|
+
toast(t("sessionCreateFailed"));
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
try {
|
|
1442
|
+
ensureWorkspaceExpanded(wsId);
|
|
1443
|
+
const sessionId = await sessions.create({ workspaceId: wsId });
|
|
1444
|
+
if (!sessionId) throw new Error("session create returned no id");
|
|
1445
|
+
|
|
1446
|
+
if (groupId) {
|
|
1447
|
+
const ws = getWorkspace(wsId, true);
|
|
1448
|
+
const group = findGroup(ws, groupId);
|
|
1449
|
+
if (ws && group) {
|
|
1450
|
+
const previous = ws.assignments[sessionId];
|
|
1451
|
+
if (previous !== groupId) {
|
|
1452
|
+
normalizeGroupOrder(wsId, groupId);
|
|
1453
|
+
ws.assignments[sessionId] = groupId;
|
|
1454
|
+
ws.sessionOrder = ws.sessionOrder || {};
|
|
1455
|
+
ws.sessionOrder[sessionId] = nextSessionOrder(ws, groupId);
|
|
1456
|
+
}
|
|
1457
|
+
group.collapsed = false;
|
|
1458
|
+
let parentId = group.parentId;
|
|
1459
|
+
while (parentId) {
|
|
1460
|
+
const parent = findGroup(ws, parentId);
|
|
1461
|
+
if (!parent) break;
|
|
1462
|
+
parent.collapsed = false;
|
|
1463
|
+
parentId = parent.parentId;
|
|
1464
|
+
}
|
|
1465
|
+
saveState();
|
|
1466
|
+
scheduleApply();
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
const uiWorkspace = getService("uiWorkspace");
|
|
1471
|
+
if (uiWorkspace && typeof uiWorkspace.openSession === "function") {
|
|
1472
|
+
uiWorkspace.openSession(sessionId);
|
|
1473
|
+
}
|
|
1474
|
+
} catch (error) {
|
|
1475
|
+
console.warn("[" + MARK + "] could not create a session", error);
|
|
1476
|
+
toast(t("sessionCreateFailed"));
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1429
1480
|
function beginRename(wsId, groupId, isNew) {
|
|
1430
1481
|
const ws = getWorkspace(wsId, false);
|
|
1431
1482
|
const group = findGroup(ws, groupId);
|
|
@@ -1839,6 +1890,7 @@ window.__ModuleLoader__.load({
|
|
|
1839
1890
|
resolve: ({ workspaceId }) => {
|
|
1840
1891
|
if (!workspaceId) return [];
|
|
1841
1892
|
return [
|
|
1893
|
+
{ id: "new-session", label: t("newSession"), run: () => createSession(workspaceId) },
|
|
1842
1894
|
{ id: "new-group", label: t("newGroup") + "…", run: () => createGroup(workspaceId, null) },
|
|
1843
1895
|
{ id: "show-all", label: t("showAll"), run: () => expandAllSessions(workspaceId, false) },
|
|
1844
1896
|
{ id: "expand-groups", label: t("expandAllGroups"), run: () => setAllGroupsCollapsed(workspaceId, false) },
|
|
@@ -1865,6 +1917,11 @@ window.__ModuleLoader__.load({
|
|
|
1865
1917
|
const orderIndex = siblings.findIndex((candidate) => candidate.id === groupId);
|
|
1866
1918
|
const orderLast = siblings.length - 1;
|
|
1867
1919
|
return [
|
|
1920
|
+
{
|
|
1921
|
+
id: "new-session",
|
|
1922
|
+
label: t("newSessionInGroup"),
|
|
1923
|
+
run: () => createSession(workspaceId, groupId),
|
|
1924
|
+
},
|
|
1868
1925
|
{ id: "subgroup", label: t("newSubgroup") + "…", run: () => createGroup(workspaceId, groupId) },
|
|
1869
1926
|
{ id: "rename", label: t("rename") + "…", run: () => beginRename(workspaceId, groupId, false) },
|
|
1870
1927
|
{
|
package/lib/index.js
CHANGED
|
@@ -20,8 +20,8 @@ import { dirname, join } from 'node:path'
|
|
|
20
20
|
/** Stable Cordis plugin name. */
|
|
21
21
|
export const name = 'session-tree'
|
|
22
22
|
|
|
23
|
-
/**
|
|
24
|
-
export const inject = []
|
|
23
|
+
/** The shared HTTP server supplies the persistence route. */
|
|
24
|
+
export const inject = ['webServer']
|
|
25
25
|
|
|
26
26
|
const CHANNEL = '/session-tree'
|
|
27
27
|
const FORMAT = 'dsh-session-tree'
|
|
@@ -160,52 +160,180 @@ function failure(code, message) {
|
|
|
160
160
|
return { ok: false, error: { code, message, details: {} } }
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
163
|
+
const MAX_REQUEST_BYTES = MAX_STATE_BYTES + 64 * 1024
|
|
164
|
+
|
|
165
|
+
/** Whether the Host connection service accepts this request as authenticated. */
|
|
166
|
+
function authorized(req, connection) {
|
|
167
|
+
if (connection && typeof connection.requestRejection === 'function') {
|
|
168
|
+
try {
|
|
169
|
+
return connection.requestRejection(req) === undefined
|
|
170
|
+
} catch {
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
169
173
|
}
|
|
174
|
+
// No Connection service (for example a non-Web combo): only loopback may
|
|
175
|
+
// read or replace local group data.
|
|
176
|
+
const address = (req.socket && req.socket.remoteAddress) || ''
|
|
177
|
+
return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1'
|
|
178
|
+
}
|
|
170
179
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
180
|
+
/** Read a bounded request body as UTF-8. */
|
|
181
|
+
function readBody(req) {
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
const chunks = []
|
|
184
|
+
let total = 0
|
|
185
|
+
let settled = false
|
|
186
|
+
const fail = (error) => {
|
|
187
|
+
if (settled) return
|
|
188
|
+
settled = true
|
|
189
|
+
reject(error)
|
|
190
|
+
}
|
|
191
|
+
req.on('data', (chunk) => {
|
|
192
|
+
if (settled) return
|
|
193
|
+
total += chunk.length
|
|
194
|
+
if (total > MAX_REQUEST_BYTES) {
|
|
195
|
+
fail(Object.assign(new Error('request is too large'), { code: 'request-too-large' }))
|
|
196
|
+
return
|
|
179
197
|
}
|
|
198
|
+
chunks.push(chunk)
|
|
199
|
+
})
|
|
200
|
+
req.on('end', () => {
|
|
201
|
+
if (settled) return
|
|
202
|
+
settled = true
|
|
203
|
+
resolve(Buffer.concat(chunks).toString('utf8'))
|
|
204
|
+
})
|
|
205
|
+
req.on('error', fail)
|
|
206
|
+
req.on('aborted', () => fail(new Error('request aborted')))
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Write one JSON response without leaking connection-level details. */
|
|
211
|
+
function sendJson(res, status, value) {
|
|
212
|
+
const body = JSON.stringify(value)
|
|
213
|
+
res.writeHead(status, {
|
|
214
|
+
'cache-control': 'no-store',
|
|
215
|
+
'content-type': 'application/json; charset=utf-8',
|
|
216
|
+
'content-length': Buffer.byteLength(body, 'utf8'),
|
|
217
|
+
})
|
|
218
|
+
res.end(body)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function handleRpc(req, res, connection) {
|
|
222
|
+
if (req.method !== 'POST') {
|
|
223
|
+
res.writeHead(405, { allow: 'POST', 'cache-control': 'no-store' })
|
|
224
|
+
res.end()
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
if (!authorized(req, connection)) {
|
|
228
|
+
res.writeHead(401, { 'cache-control': 'no-store' })
|
|
229
|
+
res.end('unauthorized')
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
const contentType = String(req.headers['content-type'] || '')
|
|
233
|
+
.split(';', 1)[0]
|
|
234
|
+
.trim()
|
|
235
|
+
.toLowerCase()
|
|
236
|
+
if (contentType !== 'application/json') {
|
|
237
|
+
res.writeHead(415, { 'cache-control': 'no-store' })
|
|
238
|
+
res.end('content type must be application/json')
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let envelope
|
|
243
|
+
try {
|
|
244
|
+
envelope = JSON.parse(await readBody(req))
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (error && error.code === 'request-too-large') {
|
|
247
|
+
res.writeHead(413, { 'cache-control': 'no-store' })
|
|
248
|
+
res.end('request is too large')
|
|
249
|
+
return
|
|
180
250
|
}
|
|
251
|
+
res.writeHead(400, { 'cache-control': 'no-store' })
|
|
252
|
+
res.end('body is not JSON')
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
if (
|
|
256
|
+
!envelope ||
|
|
257
|
+
typeof envelope !== 'object' ||
|
|
258
|
+
envelope.type !== 'client-request' ||
|
|
259
|
+
typeof envelope.rpcId !== 'string' ||
|
|
260
|
+
typeof envelope.method !== 'string'
|
|
261
|
+
) {
|
|
262
|
+
res.writeHead(400, { 'cache-control': 'no-store' })
|
|
263
|
+
res.end('invalid request envelope')
|
|
264
|
+
return
|
|
265
|
+
}
|
|
181
266
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return failure('session-tree/save-failed', 'could not save groups')
|
|
191
|
-
}
|
|
267
|
+
let result
|
|
268
|
+
if (envelope.method === 'load') {
|
|
269
|
+
try {
|
|
270
|
+
const stored = await serialize(loadStored)
|
|
271
|
+
result = { ok: true, value: stored }
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.warn('[dsh-session-tree] host load failed', error)
|
|
274
|
+
result = failure('session-tree/load-failed', 'could not read saved groups')
|
|
192
275
|
}
|
|
276
|
+
} else if (envelope.method === 'save') {
|
|
277
|
+
const state = envelope.payload && typeof envelope.payload === 'object' ? envelope.payload.state : undefined
|
|
278
|
+
try {
|
|
279
|
+
assertState(state)
|
|
280
|
+
const savedAt = await serialize(() => saveStored(state))
|
|
281
|
+
result = { ok: true, value: { savedAt } }
|
|
282
|
+
} catch (error) {
|
|
283
|
+
console.warn('[dsh-session-tree] host save failed', error)
|
|
284
|
+
result = failure('session-tree/save-failed', 'could not save groups')
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
result = failure('session-tree/not-found', 'unknown endpoint ' + JSON.stringify(String(envelope.method)))
|
|
288
|
+
}
|
|
289
|
+
sendJson(res, 200, { type: 'server-response', rpcId: envelope.rpcId, result })
|
|
290
|
+
}
|
|
193
291
|
|
|
194
|
-
|
|
292
|
+
function installRoute(scope, webServer) {
|
|
293
|
+
const dispose = webServer.register({
|
|
294
|
+
kind: 'prefix',
|
|
295
|
+
path: CHANNEL,
|
|
296
|
+
handler: (req, res) => {
|
|
297
|
+
let connection
|
|
298
|
+
try {
|
|
299
|
+
connection = typeof scope.get === 'function' ? scope.get('connection') : scope.connection
|
|
300
|
+
} catch {
|
|
301
|
+
connection = undefined
|
|
302
|
+
}
|
|
303
|
+
void handleRpc(req, res, connection).catch((error) => {
|
|
304
|
+
console.warn('[dsh-session-tree] host route failed', error)
|
|
305
|
+
if (!res.headersSent) {
|
|
306
|
+
res.writeHead(500, { 'cache-control': 'no-store' })
|
|
307
|
+
res.end('internal error')
|
|
308
|
+
} else {
|
|
309
|
+
res.end()
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
},
|
|
195
313
|
})
|
|
314
|
+
return dispose
|
|
196
315
|
}
|
|
197
316
|
|
|
198
317
|
/** Mount the host half. */
|
|
199
318
|
export function apply(ctx) {
|
|
200
319
|
try {
|
|
201
|
-
ctx.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
320
|
+
const webServer = ctx.webServer || (typeof ctx.get === 'function' ? ctx.get('webServer') : undefined)
|
|
321
|
+
if (!webServer || typeof webServer.register !== 'function') {
|
|
322
|
+
console.warn('[dsh-session-tree] web server is unavailable; host persistence is off')
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
const dispose = installRoute(ctx, webServer)
|
|
326
|
+
ctx.effect(
|
|
327
|
+
() => () => {
|
|
328
|
+
try {
|
|
329
|
+
if (typeof dispose === 'function') dispose()
|
|
330
|
+
} catch {
|
|
331
|
+
/* ignore */
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
'session-tree: host persistence route',
|
|
335
|
+
)
|
|
208
336
|
} catch (error) {
|
|
209
|
-
console.warn('[dsh-session-tree] could not
|
|
337
|
+
console.warn('[dsh-session-tree] could not register host route', error)
|
|
210
338
|
}
|
|
211
339
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-groups",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "DSH Web GUI sidebar session tree: group sessions inside each workspace into folders, persisted to disk with import/export.",
|
|
5
5
|
"author": "westformula1",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"README.md",
|
|
25
25
|
"LICENSE",
|
|
26
26
|
"PUBLISHING.md",
|
|
27
|
+
"CHANGELOG.md",
|
|
27
28
|
"assets",
|
|
28
29
|
"cordis.patch.yml"
|
|
29
30
|
],
|