dsh-better-workspace 0.9.5 → 0.9.6
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 +1 -1
- package/README_EN.md +1 -1
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/client.js +62 -40
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ web/ ← 虚拟分组(不是真实目录,纯命名层级)
|
|
|
33
33
|
|
|
34
34
|
工作区里的会话名称同样用 `/` 分层(如 `测试1/两个插件维护`)。会话分组用**次级配色且无文件夹图标**渲染,和工作区文件夹一眼可辨;会话分组可整组重命名(批量改写成员标题)。
|
|
35
35
|
|
|
36
|
-
**重启后标题不回退**:DSH 重启后的冷列表只为命中投影缓存的会话携带标题——分叉出的会话与从未写过检查点的会话会暂时回退成「工作区名」显示,真实的标题其实还在会话日志里。本插件把每个会话**最近一次的真实标题**持久化在浏览器里(
|
|
36
|
+
**重启后标题不回退**:DSH 重启后的冷列表只为命中投影缓存的会话携带标题——分叉出的会话与从未写过检查点的会话会暂时回退成「工作区名」显示,真实的标题其实还在会话日志里。本插件把每个会话**最近一次的真实标题**持久化在浏览器里(带防抖的本地缓存,变更合并整写一次),回退窗口内直接用记忆标题渲染,`/` 分级分组照常成立;打开会话后官方数据到位即自动纠正。
|
|
37
37
|
|
|
38
38
|
**跨端手动同步(Web ↔ 桌面客户端)**:外观自定义、显式分组与开关默认保存在各设备自己的浏览器里;设置卡片新增「跨端同步」区——Web 上点「从客户端获取」、客户端上点「从 Web 获取」,可选**覆盖本设备**或**合并两端**(并集保留各自独有项),另一端的数据经宿主设置存储(~/.dsh/settings.yaml,两端共享)送达;旁边有「发送本设备数据」按钮,旧版本的历史数据首次同步推一次即可,之后每次修改都会自动写入宿主、对端随手可取。折叠状态仍保持每端独立。
|
|
39
39
|
|
package/README_EN.md
CHANGED
|
@@ -31,7 +31,7 @@ Right-click any folder row: new subfolder (parent prefix pre-filled), new worksp
|
|
|
31
31
|
|
|
32
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).
|
|
33
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 (
|
|
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 (a debounced local cache: real changes coalesce into one write) 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
35
|
|
|
36
36
|
**Manual cross-device sync (web ↔ desktop app)**: appearance, explicit folders, and toggles live in each device's own browser by default. The settings card adds a "Cross-device sync" section — "Pull from desktop app" on the web / "Pull from web" in the desktop app, with an **overwrite-this-device** or **merge-both-sides** mode (the union keeps each side's unique entries); the other surface's data travels through the host settings store (~/.dsh/settings.yaml, shared by both surfaces). A "Send this device's data" button next to it covers history created before this version — push once, and afterwards every edit is written to the host automatically for the other surface to pull. Expansion state stays per-device.
|
|
37
37
|
|
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.6",
|
|
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.6",
|
|
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": {
|
package/src/client.js
CHANGED
|
@@ -749,31 +749,61 @@ window.__ModuleLoader__.load({
|
|
|
749
749
|
// last REAL wire title per session id (learned only from snapshots where
|
|
750
750
|
// summary.title exists — never from displayTitle, which is basename
|
|
751
751
|
// degraded in exactly the window being patched), so the tree restores
|
|
752
|
-
// the "/" grouping until the wire catches up.
|
|
753
|
-
//
|
|
754
|
-
//
|
|
752
|
+
// the "/" grouping until the wire catches up. 0.9.6: plain module
|
|
753
|
+
// state with one debounced whole-file save per real change batch
|
|
754
|
+
// (issue #1: per-session reactive-store dispatches cost O(sessions)
|
|
755
|
+
// immer produces per snapshot tick and froze large installs). Same-
|
|
756
|
+
// value scans allocate nothing; entries evict oldest-at past the hard
|
|
757
|
+
// cap. A stale remembered title (renamed
|
|
755
758
|
// elsewhere, cleared browser storage) self-heals the moment the wire
|
|
756
759
|
// carries the truth again; a missing entry just leaves today's fallback.
|
|
757
760
|
const TITLE_CACHE_LIMIT = 3000
|
|
758
761
|
const TITLE_CACHE_KEEP = 2400
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
const
|
|
767
|
-
if (
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
}
|
|
775
|
-
},
|
|
776
|
-
}
|
|
762
|
+
const TITLE_CACHE_KEY = 'dsh.betterWorkspace.titles.v1'
|
|
763
|
+
const titleCache = { byId: {} }
|
|
764
|
+
let titleSaveTimer = null
|
|
765
|
+
const loadTitleCache = () => {
|
|
766
|
+
try {
|
|
767
|
+
const raw = localStorage.getItem(TITLE_CACHE_KEY)
|
|
768
|
+
if (raw !== null) {
|
|
769
|
+
const parsed = JSON.parse(raw)
|
|
770
|
+
if (parsed && typeof parsed.byId === 'object' && parsed.byId !== null) titleCache.byId = parsed.byId
|
|
771
|
+
}
|
|
772
|
+
} catch (e) { /* storage unavailable/corrupt: cold start without cache */ }
|
|
773
|
+
}
|
|
774
|
+
const scheduleTitleSave = () => {
|
|
775
|
+
clearTimeout(titleSaveTimer)
|
|
776
|
+
titleSaveTimer = setTimeout(() => {
|
|
777
|
+
try { localStorage.setItem(TITLE_CACHE_KEY, JSON.stringify(titleCache)) } catch (e) {}
|
|
778
|
+
}, 200)
|
|
779
|
+
}
|
|
780
|
+
// One plain-object batch pass per snapshot (issue #1). Unchanged scans
|
|
781
|
+
// cost one loop with zero allocation; only real changes mark dirty;
|
|
782
|
+
// eviction runs once per changed batch followed by ONE debounced save.
|
|
783
|
+
const rememberAllTitles = (list) => {
|
|
784
|
+
if (!list || !list.byId) return false
|
|
785
|
+
const byId = titleCache.byId
|
|
786
|
+
const now = Date.now()
|
|
787
|
+
let changed = false
|
|
788
|
+
for (const id of Object.keys(list.byId)) {
|
|
789
|
+
const summary = list.byId[id]
|
|
790
|
+
if (!summary || summary.blank) continue
|
|
791
|
+
const title = summary.title
|
|
792
|
+
if (typeof title !== 'string' || title === '') continue
|
|
793
|
+
const prev = byId[id]
|
|
794
|
+
if (prev && prev.title === title) continue
|
|
795
|
+
byId[id] = { title: title, at: now }
|
|
796
|
+
changed = true
|
|
797
|
+
}
|
|
798
|
+
if (!changed) return false
|
|
799
|
+
const keys = Object.keys(byId)
|
|
800
|
+
if (keys.length > TITLE_CACHE_LIMIT) {
|
|
801
|
+
keys.sort((a, b) => ((byId[a] && byId[a].at) || 0) - ((byId[b] && byId[b].at) || 0))
|
|
802
|
+
for (let i = 0; i < keys.length - TITLE_CACHE_KEEP; i++) delete byId[keys[i]]
|
|
803
|
+
}
|
|
804
|
+
scheduleTitleSave()
|
|
805
|
+
return true
|
|
806
|
+
}
|
|
777
807
|
|
|
778
808
|
/* ======================= host settings sync ======================= */
|
|
779
809
|
|
|
@@ -1560,7 +1590,7 @@ window.__ModuleLoader__.load({
|
|
|
1560
1590
|
const archivedSet = React.useMemo(() => new Set(archivedSessionIds), [archivedSessionIds])
|
|
1561
1591
|
const subCounts = React.useMemo(() => subagentRunningCounts(list ? list.byId : {}), [list ? list.byId : null])
|
|
1562
1592
|
// Last-known real titles for the cold-restart fallback window (see
|
|
1563
|
-
//
|
|
1593
|
+
// the title-cache block). Read per render via getSnapshot: no
|
|
1564
1594
|
// subscription, because a cache write only happens when summary.title
|
|
1565
1595
|
// exists — at that moment the row already renders the wire truth and
|
|
1566
1596
|
// no re-render is wanted.
|
|
@@ -1572,21 +1602,13 @@ window.__ModuleLoader__.load({
|
|
|
1572
1602
|
// Learn real titles from every snapshot: remember exactly what the
|
|
1573
1603
|
// wire carried (summary.title), never displayTitle — that is already
|
|
1574
1604
|
// basename-degraded in the very window this patches. Blank rows are
|
|
1575
|
-
// provisional New Sessions and never learn.
|
|
1576
|
-
//
|
|
1605
|
+
// provisional New Sessions and never learn. One plain-object batch
|
|
1606
|
+
// call per snapshot (issue #1): unchanged scans cost one loop with
|
|
1607
|
+
// zero allocation, real changes schedule one debounced save.
|
|
1577
1608
|
React.useEffect(() => {
|
|
1578
1609
|
if (!list || !list.byId) return
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
: null
|
|
1582
|
-
if (typeof remember !== 'function') return
|
|
1583
|
-
const now = Date.now()
|
|
1584
|
-
for (const id of Object.keys(list.byId)) {
|
|
1585
|
-
const summary = list.byId[id]
|
|
1586
|
-
if (!summary || summary.blank) continue
|
|
1587
|
-
const title = summary.title
|
|
1588
|
-
if (typeof title !== 'string' || title === '') continue
|
|
1589
|
-
remember(String(id), title, now)
|
|
1610
|
+
if (titleCacheRef && typeof titleCacheRef.rememberAllTitles === 'function') {
|
|
1611
|
+
titleCacheRef.rememberAllTitles(list)
|
|
1590
1612
|
}
|
|
1591
1613
|
}, [list ? list.byId : null])
|
|
1592
1614
|
|
|
@@ -2685,11 +2707,11 @@ window.__ModuleLoader__.load({
|
|
|
2685
2707
|
// same persisted state (expansion, folder list, prefs, styling).
|
|
2686
2708
|
const viewStore = createViewStore()
|
|
2687
2709
|
|
|
2688
|
-
// Cold-restart title fallback
|
|
2689
|
-
//
|
|
2690
|
-
//
|
|
2691
|
-
|
|
2692
|
-
titleCacheRef =
|
|
2710
|
+
// Cold-restart title fallback (see the title-cache block). Hydration
|
|
2711
|
+
// reads localStorage synchronously here, before the first render, so a
|
|
2712
|
+
// restart's first tree already shows every remembered title.
|
|
2713
|
+
loadTitleCache()
|
|
2714
|
+
titleCacheRef = { getSnapshot: () => titleCache, rememberAllTitles }
|
|
2693
2715
|
|
|
2694
2716
|
// Cross-device settings scope (manual sync, see the host-settings-sync
|
|
2695
2717
|
// block). Binding is best-effort: a missing/failed bind leaves the
|