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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # dsh-better-workspace
2
2
 
3
+ [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](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
+ [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](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.2",
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.2",
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', 'connection', 'uiWorkspace'],
2518
+ inject: ['slots', 'sessions', 'workspaces', 'locale', 'uiWorkspace'],
2430
2519
  apply,
2431
2520
  }
2432
2521
  },