dsh-better-workspace 0.9.2 → 0.9.4
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/README.md +4 -0
- package/README_EN.md +4 -0
- package/dsh.plugin.json +1 -1
- package/package.json +3 -2
- package/src/client.js +97 -8
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# dsh-better-workspace
|
|
2
2
|
|
|
3
|
+
[](https://awesome-dsh-plugin.com)
|
|
4
|
+
|
|
3
5
|
> 给 [DeepSeek Harness (DSH)](https://www.npmjs.com/package/@deepseek-ai/dsh) 的侧边栏「工作区」列表装上一套**文件夹系统**——工作区还是那个工作区(一个目录),但名字里的 `/` 就是层级,让「网页前端」「网页后端」这类工作区可以归到同一个 `web` 分组下。
|
|
4
6
|
|
|
5
7
|
```
|
|
@@ -31,6 +33,8 @@ web/ ← 虚拟分组(不是真实目录,纯命名层级)
|
|
|
31
33
|
|
|
32
34
|
工作区里的会话名称同样用 `/` 分层(如 `测试1/两个插件维护`)。会话分组用**次级配色且无文件夹图标**渲染,和工作区文件夹一眼可辨;会话分组可整组重命名(批量改写成员标题)。
|
|
33
35
|
|
|
36
|
+
**重启后标题不回退**:DSH 重启后的冷列表只为命中投影缓存的会话携带标题——分叉出的会话与从未写过检查点的会话会暂时回退成「工作区名」显示,真实的标题其实还在会话日志里。本插件把每个会话**最近一次的真实标题**持久化在浏览器里(dsh 客户端 store),回退窗口内直接用记忆标题渲染,`/` 分级分组照常成立;打开会话后官方数据到位即自动纠正。
|
|
37
|
+
|
|
34
38
|
<table>
|
|
35
39
|
<tr>
|
|
36
40
|
<td align="center" width="58%"><img src="docs/screenshots/3-appearance-dialog.png" alt="外观自定义弹窗(实时预览)"/></td>
|
package/README_EN.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# dsh-better-workspace
|
|
2
2
|
|
|
3
|
+
[](https://awesome-dsh-plugin.com)
|
|
4
|
+
|
|
3
5
|
> A **folder system** for the DeepSeek Harness (DSH) sidebar workspace list — a workspace is still one directory, but every `/` in its name becomes hierarchy, so `web/frontend` and `web/backend` group under one virtual `web` folder.
|
|
4
6
|
|
|
5
7
|
```
|
|
@@ -29,6 +31,8 @@ Right-click any folder row: new subfolder (parent prefix pre-filled), new worksp
|
|
|
29
31
|
|
|
30
32
|
Session titles nest on `/` too (e.g. `test1/plugin maintenance`). Session groups render in a secondary color without a folder icon, so they are clearly distinct from workspace folders; a session group can be renamed as a whole (rewrites member titles).
|
|
31
33
|
|
|
34
|
+
**No title regression after a restart**: the cold list right after a DSH restart only carries titles for sessions that hit the persisted projection cache — fork-born and never-checkpointed sessions temporarily fall back to the workspace *basename*, even though the real title still lives in the session log. This plugin remembers each session's **last real wire title** in the browser (dsh client store) and renders from memory during that window, so `/` grouping keeps working; the moment a session opens, the official data takes over and self-heals.
|
|
35
|
+
|
|
32
36
|
<table>
|
|
33
37
|
<tr>
|
|
34
38
|
<td align="center" width="58%"><img src="docs/screenshots/3-appearance-dialog.png" alt="Appearance dialog with live preview"/></td>
|
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-better-workspace",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"description": "侧边栏工作区层级树:名称中的 / 即虚拟分组(web/前端 · web/后端),添加工作区的目录流附所属分组弹窗,重命名即时重排层级,可新建空分组。Hierarchy tree for the DSH sidebar workspace list.",
|
|
6
6
|
"engines": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-better-workspace",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"description": "DSH web plugin: a hierarchical workspace tree for the sidebar. Workspace titles containing \"/\" group into virtual folders (web/前端 · web/后端); the add-workspace directory flow gains a parent-group popup; renames re-derive the tree; a new-folder button creates empty levels.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"./client": "./src/client.js"
|
|
17
17
|
},
|
|
18
18
|
"engines": {
|
|
19
|
-
"node": ">=18"
|
|
19
|
+
"node": ">=18",
|
|
20
|
+
"dsh": ">=0.1.0"
|
|
20
21
|
},
|
|
21
22
|
"dsh": {
|
|
22
23
|
"bundle": {
|
package/src/client.js
CHANGED
|
@@ -304,9 +304,17 @@ window.__ModuleLoader__.load({
|
|
|
304
304
|
return p.kind || p.status || p.type || 'pending'
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
const sessionTitleOf = (summary, t) => {
|
|
307
|
+
const sessionTitleOf = (summary, t, rememberedTitle) => {
|
|
308
308
|
if (!summary) return ''
|
|
309
309
|
if (summary.blank) return t('session.new')
|
|
310
|
+
if (summary.title) return String(summary.title)
|
|
311
|
+
// Cold-restart window: when the wire omits title, displayTitle is the
|
|
312
|
+
// host's basename fallback (dsh list only serves the projection cache,
|
|
313
|
+
// and fork-born/never-checkpointed sessions always miss it — the durable
|
|
314
|
+
// title stays in the session log, unread for a list row). A title
|
|
315
|
+
// remembered from an earlier snapshot restores the "/" grouping until
|
|
316
|
+
// the session opens and the wire catches up.
|
|
317
|
+
if (typeof rememberedTitle === 'string' && rememberedTitle !== '') return rememberedTitle
|
|
310
318
|
return String(summary.displayTitle || summary.title || '')
|
|
311
319
|
}
|
|
312
320
|
|
|
@@ -676,6 +684,44 @@ window.__ModuleLoader__.load({
|
|
|
676
684
|
},
|
|
677
685
|
})
|
|
678
686
|
|
|
687
|
+
/* ========================= title cache store ====================== */
|
|
688
|
+
|
|
689
|
+
// Cold-restart title fallback. The host list serves titles only from the
|
|
690
|
+
// persisted projection cache: sessions that never wrote a checkpoint —
|
|
691
|
+
// and every fork-born (seeded) session, which the list skips entirely —
|
|
692
|
+
// come back after a restart with title absent and displayTitle already
|
|
693
|
+
// degraded to the workspace basename (dsh displayTitleOf: title → cwd
|
|
694
|
+
// basename → id). The durable titles live on in each session's log, but
|
|
695
|
+
// nothing reads a log for a cheap list row. This store remembers the
|
|
696
|
+
// last REAL wire title per session id (learned only from snapshots where
|
|
697
|
+
// summary.title exists — never from displayTitle, which is basename
|
|
698
|
+
// degraded in exactly the window being patched), so the tree restores
|
|
699
|
+
// the "/" grouping until the wire catches up. Same-value writes are
|
|
700
|
+
// no-ops (immer produces the same state, nothing persists); entries
|
|
701
|
+
// evict oldest-at past the hard cap. A stale remembered title (renamed
|
|
702
|
+
// elsewhere, cleared browser storage) self-heals the moment the wire
|
|
703
|
+
// carries the truth again; a missing entry just leaves today's fallback.
|
|
704
|
+
const TITLE_CACHE_LIMIT = 3000
|
|
705
|
+
const TITLE_CACHE_KEEP = 2400
|
|
706
|
+
const createTitleCacheStore = () => storeKit.defineStore({
|
|
707
|
+
init: () => ({ byId: {} }),
|
|
708
|
+
persist: 'dsh.betterWorkspace.titles.v1',
|
|
709
|
+
actions: {
|
|
710
|
+
rememberTitle: (d, id, title, at) => {
|
|
711
|
+
if (typeof id !== 'string' || id === '' || typeof title !== 'string' || title === '') return
|
|
712
|
+
if (!d.byId) d.byId = {}
|
|
713
|
+
const prev = d.byId[id]
|
|
714
|
+
if (prev && prev.title === title) return
|
|
715
|
+
d.byId[id] = { title: title, at: typeof at === 'number' ? at : Date.now() }
|
|
716
|
+
const keys = Object.keys(d.byId)
|
|
717
|
+
if (keys.length > TITLE_CACHE_LIMIT) {
|
|
718
|
+
keys.sort((a, b) => ((d.byId[a] && d.byId[a].at) || 0) - ((d.byId[b] && d.byId[b].at) || 0))
|
|
719
|
+
for (let i = 0; i < keys.length - TITLE_CACHE_KEEP; i++) delete d.byId[keys[i]]
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
})
|
|
724
|
+
|
|
679
725
|
/* ============================ flow dialog ========================= */
|
|
680
726
|
|
|
681
727
|
const BTN = (props) => (
|
|
@@ -1306,6 +1352,36 @@ window.__ModuleLoader__.load({
|
|
|
1306
1352
|
const statusPulse = prefsMap.statusPulse !== false
|
|
1307
1353
|
const archivedSet = React.useMemo(() => new Set(archivedSessionIds), [archivedSessionIds])
|
|
1308
1354
|
const subCounts = React.useMemo(() => subagentRunningCounts(list ? list.byId : {}), [list ? list.byId : null])
|
|
1355
|
+
// Last-known real titles for the cold-restart fallback window (see
|
|
1356
|
+
// createTitleCacheStore). Read per render via getSnapshot: no
|
|
1357
|
+
// subscription, because a cache write only happens when summary.title
|
|
1358
|
+
// exists — at that moment the row already renders the wire truth and
|
|
1359
|
+
// no re-render is wanted.
|
|
1360
|
+
const remembered = (titleCacheRef && typeof titleCacheRef.getSnapshot === 'function')
|
|
1361
|
+
? (titleCacheRef.getSnapshot().byId || {})
|
|
1362
|
+
: {}
|
|
1363
|
+
const rememberedTitleOf = (id) => (remembered[id] ? remembered[id].title : undefined)
|
|
1364
|
+
|
|
1365
|
+
// Learn real titles from every snapshot: remember exactly what the
|
|
1366
|
+
// wire carried (summary.title), never displayTitle — that is already
|
|
1367
|
+
// basename-degraded in the very window this patches. Blank rows are
|
|
1368
|
+
// provisional New Sessions and never learn. rememberTitle itself
|
|
1369
|
+
// no-ops on unchanged values, so the loop is cheap when idle.
|
|
1370
|
+
React.useEffect(() => {
|
|
1371
|
+
if (!list || !list.byId) return
|
|
1372
|
+
const remember = titleCacheRef && titleCacheRef.actions
|
|
1373
|
+
? titleCacheRef.actions.rememberTitle
|
|
1374
|
+
: null
|
|
1375
|
+
if (typeof remember !== 'function') return
|
|
1376
|
+
const now = Date.now()
|
|
1377
|
+
for (const id of Object.keys(list.byId)) {
|
|
1378
|
+
const summary = list.byId[id]
|
|
1379
|
+
if (!summary || summary.blank) continue
|
|
1380
|
+
const title = summary.title
|
|
1381
|
+
if (typeof title !== 'string' || title === '') continue
|
|
1382
|
+
remember(String(id), title, now)
|
|
1383
|
+
}
|
|
1384
|
+
}, [list ? list.byId : null])
|
|
1309
1385
|
|
|
1310
1386
|
const [query, setQuery] = React.useState('')
|
|
1311
1387
|
const [searchOpen, setSearchOpen] = React.useState(false)
|
|
@@ -1451,8 +1527,8 @@ window.__ModuleLoader__.load({
|
|
|
1451
1527
|
if (accounted.has(id) || !sessionVisible(summary, list.current, archivedSet)) continue
|
|
1452
1528
|
ungrouped.push({
|
|
1453
1529
|
id,
|
|
1454
|
-
title: sessionTitleOf(summary, t),
|
|
1455
|
-
leaf: sessionTitleOf(summary, t),
|
|
1530
|
+
title: sessionTitleOf(summary, t, rememberedTitleOf(id)),
|
|
1531
|
+
leaf: sessionTitleOf(summary, t, rememberedTitleOf(id)),
|
|
1456
1532
|
blank: !!summary.blank,
|
|
1457
1533
|
running: !!summary.running,
|
|
1458
1534
|
completed: summary.completed === true,
|
|
@@ -1486,8 +1562,8 @@ window.__ModuleLoader__.load({
|
|
|
1486
1562
|
if (!sessionVisible(summary, list ? list.current : undefined, archivedSet)) continue
|
|
1487
1563
|
rows.push({
|
|
1488
1564
|
id,
|
|
1489
|
-
title: sessionTitleOf(summary, t),
|
|
1490
|
-
leaf: sessionTitleOf(summary, t),
|
|
1565
|
+
title: sessionTitleOf(summary, t, rememberedTitleOf(id)),
|
|
1566
|
+
leaf: sessionTitleOf(summary, t, rememberedTitleOf(id)),
|
|
1491
1567
|
blank: !!summary.blank,
|
|
1492
1568
|
running: !!summary.running,
|
|
1493
1569
|
completed: summary.completed === true,
|
|
@@ -2276,6 +2352,15 @@ window.__ModuleLoader__.load({
|
|
|
2276
2352
|
|
|
2277
2353
|
/* ============================ plugin ============================== */
|
|
2278
2354
|
|
|
2355
|
+
// Live title-cache store instance, set once per apply() and read directly
|
|
2356
|
+
// by BetterBrowser. NOT a registration store seat: remembered titles only
|
|
2357
|
+
// matter before the wire catches up, and every write happens when the
|
|
2358
|
+
// wire already carries the same truth, so selector-hook re-renders would
|
|
2359
|
+
// be pure noise. One instance per activation (never per mount) keeps the
|
|
2360
|
+
// persist key single-writer — dsh-client-store warns same-key instances
|
|
2361
|
+
// cross-pollinate one localStorage entry.
|
|
2362
|
+
let titleCacheRef = null
|
|
2363
|
+
|
|
2279
2364
|
const flowSource = (slots, hole) => ({
|
|
2280
2365
|
getSnapshot: () => {
|
|
2281
2366
|
try { return slots.entries(hole).length > 0 } catch { return false }
|
|
@@ -2294,7 +2379,6 @@ window.__ModuleLoader__.load({
|
|
|
2294
2379
|
const sessions = ctx.get('sessions')
|
|
2295
2380
|
const workspaces = ctx.get('workspaces')
|
|
2296
2381
|
const uiWorkspace = ctx.get('uiWorkspace')
|
|
2297
|
-
const connection = ctx.get('connection')
|
|
2298
2382
|
if (!sessions || !workspaces || !uiWorkspace) {
|
|
2299
2383
|
console.error('[dsh-better-workspace] required services missing', {
|
|
2300
2384
|
sessions: !!sessions, workspaces: !!workspaces, uiWorkspace: !!uiWorkspace,
|
|
@@ -2354,7 +2438,6 @@ window.__ModuleLoader__.load({
|
|
|
2354
2438
|
pickDirectory: () => uiWorkspace.pickDirectory(),
|
|
2355
2439
|
hooks: {
|
|
2356
2440
|
directoryFlow: flowSource(slots, 'sidebar.workspaces.directoryFlow'),
|
|
2357
|
-
connectionGeneration: connection ? connection.generation : undefined,
|
|
2358
2441
|
},
|
|
2359
2442
|
})
|
|
2360
2443
|
const flowInjected = (hole) => () => ({
|
|
@@ -2395,6 +2478,12 @@ window.__ModuleLoader__.load({
|
|
|
2395
2478
|
// same persisted state (expansion, folder list, prefs, styling).
|
|
2396
2479
|
const viewStore = createViewStore()
|
|
2397
2480
|
|
|
2481
|
+
// Cold-restart title fallback instance (see createTitleCacheStore).
|
|
2482
|
+
// getSnapshot() during render is intentional: hydration reads
|
|
2483
|
+
// localStorage synchronously at create(), so the first render after a
|
|
2484
|
+
// restart already sees every remembered title.
|
|
2485
|
+
titleCacheRef = createTitleCacheStore().create()
|
|
2486
|
+
|
|
2398
2487
|
// Settings → Plugins card only (the tab dispatches the intersection of
|
|
2399
2488
|
// served namespaces — registered host-side — and settings.plugin.item
|
|
2400
2489
|
// cards). The old left-nav settings.section entry was removed: the
|
|
@@ -2426,7 +2515,7 @@ window.__ModuleLoader__.load({
|
|
|
2426
2515
|
|
|
2427
2516
|
return {
|
|
2428
2517
|
name: 'dsh-better-workspace',
|
|
2429
|
-
inject: ['slots', 'sessions', 'workspaces', 'locale', '
|
|
2518
|
+
inject: ['slots', 'sessions', 'workspaces', 'locale', 'uiWorkspace'],
|
|
2430
2519
|
apply,
|
|
2431
2520
|
}
|
|
2432
2521
|
},
|