dsh-better-workspace 0.10.1 → 0.10.2
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 +5 -0
- package/README_EN.md +5 -0
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/client.js +126 -15
package/README.md
CHANGED
|
@@ -106,6 +106,11 @@ DSH 的侧边栏浏览区是一个公开插槽 `sidebar.workspaces`(single),官
|
|
|
106
106
|
|
|
107
107
|
同属 `dsh-better-*` 家族,但互不重叠:[dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) 是**右侧**的 VSCode 式面板(资源管理器/终端/Git);本插件只接管**左侧**工作区列表。两者可同装。
|
|
108
108
|
|
|
109
|
+
## 版本兼容
|
|
110
|
+
|
|
111
|
+
- **dsh 0.1.6-alpha.1**:图标集与浏览器注入面都变了,本插件 **0.10.2 起自动适配**——图标网格只列本宿主真正导出的字形(退役的 `IconSendOutline16` 自动落到等价的 `IconSendOutline14`,已保存的旧选择不会变成空图标位),会话拖拽排序在宿主不再注入 `insertSessionBefore` 时改用浏览器本地顺序,**功能不丢**。升级 dsh 与升级插件谁先谁后都收敛。
|
|
112
|
+
- **跨端**:web 与桌面共用同一套客户端资产;外观 / 分组 / 开关可经设置卡片手动跨端同步,**折叠状态与会话本地序每端本地**(设计如此)。
|
|
113
|
+
|
|
109
114
|
## 限制与路线图
|
|
110
115
|
|
|
111
116
|
- 会话拖到**另一分组内的会话行**仅做扁平重排(标题分组归属不变);要改分组请拖到分组行或走重命名。
|
package/README_EN.md
CHANGED
|
@@ -90,6 +90,11 @@ View state (folder collapse, session expansion, explicit empty folders), custom
|
|
|
90
90
|
|
|
91
91
|
Same `dsh-better-*` family, zero overlap: [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) is the VSCode-like panel on the right; this plugin only takes over the left workspace list. They can be installed together.
|
|
92
92
|
|
|
93
|
+
## Version compatibility
|
|
94
|
+
|
|
95
|
+
- **dsh 0.1.6-alpha.1**: both the primitives icon set and the browser injection face changed; this plugin **adapts automatically since 0.10.2**. The icon grid only lists glyphs this host actually exports (the retired `IconSendOutline16` resolves to the equivalent `IconSendOutline14`, so a previously saved choice never turns into an empty icon slot), and session drag-to-reorder falls back to a browser-local order when the host no longer injects `insertSessionBefore` — **no feature is lost**. Either upgrade order (plugin first or dsh first) converges.
|
|
96
|
+
- **Across surfaces**: web and desktop share one set of client assets; appearance / groups / switches sync manually through the settings card, while **collapse state and the local session order stay per-browser** by design.
|
|
97
|
+
|
|
93
98
|
## Limitations / roadmap
|
|
94
99
|
|
|
95
100
|
- A session dragged onto a session row **inside another group** only gets reordered in the flat list (its title group stays); drag onto the group row or rename to move it.
|
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-better-workspace",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
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.10.
|
|
3
|
+
"version": "0.10.2",
|
|
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
|
@@ -317,6 +317,28 @@ window.__ModuleLoader__.load({
|
|
|
317
317
|
}
|
|
318
318
|
const normPath = (p) => splitTitleSegs(p).join('/')
|
|
319
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Glyphs a host may have RETIRED, mapped to the surviving equivalents in
|
|
322
|
+
* preference order. dsh 0.1.6-alpha.1 replaced IconSendOutline16 with
|
|
323
|
+
* IconPaperPlaneOutline14 and deleted the old export, so a saved custom
|
|
324
|
+
* icon naming it must land on something real rather than an empty cell.
|
|
325
|
+
*/
|
|
326
|
+
const ICON_ALIASES = { IconSendOutline16: ['IconSendOutline14'] }
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Resolve one configured icon value to a glyph the LOADED primitives
|
|
330
|
+
* actually export (pure). Retired names follow their alias chain, unknown
|
|
331
|
+
* names degrade to '' — so both the picker grid and a choice persisted
|
|
332
|
+
* before an upgrade survive a host that renamed its icon set.
|
|
333
|
+
*/
|
|
334
|
+
const resolveIconName = (name) => {
|
|
335
|
+
if (typeof name !== 'string' || name === '') return ''
|
|
336
|
+
if (ui[name]) return name
|
|
337
|
+
const aliases = ICON_ALIASES[name]
|
|
338
|
+
if (aliases) for (const alias of aliases) if (ui[alias]) return alias
|
|
339
|
+
return ''
|
|
340
|
+
}
|
|
341
|
+
|
|
320
342
|
/** Render a primitives icon by name; unknown names degrade to null, never crash. */
|
|
321
343
|
const icon = (name, size) => {
|
|
322
344
|
const C = ui[name]
|
|
@@ -456,6 +478,20 @@ window.__ModuleLoader__.load({
|
|
|
456
478
|
return root
|
|
457
479
|
}
|
|
458
480
|
|
|
481
|
+
/**
|
|
482
|
+
* Move one id inside a flat list: before `anchor`, or to the end when the
|
|
483
|
+
* anchor is missing (pure). Mirrors the host action's insertSessionBefore
|
|
484
|
+
* semantics, so the browser-local fallback orders rows exactly the way the
|
|
485
|
+
* host would have.
|
|
486
|
+
*/
|
|
487
|
+
function reorderIds(ids, id, anchor) {
|
|
488
|
+
const next = (ids || []).filter((x) => x !== id)
|
|
489
|
+
const at = anchor === undefined ? -1 : next.indexOf(anchor)
|
|
490
|
+
if (at === -1) next.push(id)
|
|
491
|
+
else next.splice(at, 0, id)
|
|
492
|
+
return next
|
|
493
|
+
}
|
|
494
|
+
|
|
459
495
|
const countSessionTree = (node) => node.sessions.length + node.groups.reduce((sum, group) => sum + countSessionTree(group), 0)
|
|
460
496
|
|
|
461
497
|
const collectSessionRows = (node) => {
|
|
@@ -705,7 +741,7 @@ window.__ModuleLoader__.load({
|
|
|
705
741
|
/* ========================== view store =========================== */
|
|
706
742
|
|
|
707
743
|
const createViewStore = () => storeKit.defineStore({
|
|
708
|
-
init: () => ({ folders: [], expanded: {}, sessionsExpanded: {}, sessionGroups: {}, prefs: { compactChains: true }, styling: {} }),
|
|
744
|
+
init: () => ({ folders: [], expanded: {}, sessionsExpanded: {}, sessionGroups: {}, sessionOrder: {}, prefs: { compactChains: true }, styling: {} }),
|
|
709
745
|
// NOTE: hydration REPLACES the state with the persisted whole value —
|
|
710
746
|
// init defaults never merge. Every action must tolerate a missing key
|
|
711
747
|
// (states persisted by older plugin versions lack sessionGroups), and
|
|
@@ -743,6 +779,16 @@ window.__ModuleLoader__.load({
|
|
|
743
779
|
setSessionGroupExpanded: (d, key, value) => { if (!d.sessionGroups) d.sessionGroups = {}; d.sessionGroups[key] = value },
|
|
744
780
|
setPref: (d, key, value) => { if (!d.prefs) d.prefs = {}; d.prefs[key] = value },
|
|
745
781
|
setStyling: (d, key, value) => { if (!d.styling) d.styling = {}; if (value === null) delete d.styling[key]; else d.styling[key] = value },
|
|
782
|
+
// Browser-local session order (v0.10.2): the fallback channel for
|
|
783
|
+
// drag-to-reorder on hosts that no longer inject insertSessionBefore
|
|
784
|
+
// (dsh 0.1.6-alpha.1). Per-workspace and per-browser on purpose — the
|
|
785
|
+
// host order stays authoritative wherever the action exists, and this
|
|
786
|
+
// never rides the cross-device sync.
|
|
787
|
+
setSessionOrder: (d, workspaceId, order) => {
|
|
788
|
+
if (!d.sessionOrder || typeof d.sessionOrder !== 'object') d.sessionOrder = {}
|
|
789
|
+
if (!Array.isArray(order) || order.length === 0) delete d.sessionOrder[workspaceId]
|
|
790
|
+
else d.sessionOrder[workspaceId] = order.slice()
|
|
791
|
+
},
|
|
746
792
|
addFolder: (d, path) => {
|
|
747
793
|
if (!Array.isArray(d.folders)) d.folders = []
|
|
748
794
|
const p = normPath(path)
|
|
@@ -1114,12 +1160,16 @@ window.__ModuleLoader__.load({
|
|
|
1114
1160
|
/* ============================== rows ============================== */
|
|
1115
1161
|
|
|
1116
1162
|
/** Folder glyph variants from the primitives family (solid / outline / hidden). */
|
|
1117
|
-
/**
|
|
1163
|
+
/**
|
|
1164
|
+
* Custom icon value → glyph: legacy slots (solid/outline/none) or any
|
|
1165
|
+
* primitives icon name, resolved through ICON_ALIASES so a value persisted
|
|
1166
|
+
* before a host renamed its icon set still renders something.
|
|
1167
|
+
*/
|
|
1118
1168
|
const iconOf = (mode, expanded) => {
|
|
1119
1169
|
if (!mode || mode === 'none') return null
|
|
1120
1170
|
if (mode === 'solid') return icon(expanded ? 'IconFolderOpen16' : 'IconFolderClose16')
|
|
1121
1171
|
if (mode === 'outline') return icon('IconFolderOpenOutline16')
|
|
1122
|
-
return icon(mode)
|
|
1172
|
+
return icon(resolveIconName(mode))
|
|
1123
1173
|
}
|
|
1124
1174
|
|
|
1125
1175
|
const colorToRgb = (hex) => {
|
|
@@ -1397,20 +1447,42 @@ window.__ModuleLoader__.load({
|
|
|
1397
1447
|
'IconLoadingOutline16', 'IconPanelLeftOutline16', 'IconPaperclipOutline16', 'IconPersonalizationOutline16',
|
|
1398
1448
|
'IconPlayOutline16', 'IconPauseOutline16', 'IconPlusOutline16', 'IconQuestionOutline14',
|
|
1399
1449
|
'IconQueueOutline14', 'IconRefreshOutline14', 'IconRefreshOutline16', 'IconRightUpOutline14',
|
|
1400
|
-
'IconSearchOutline16', 'IconSendOutline14', '
|
|
1450
|
+
'IconSearchOutline16', 'IconSendOutline14', 'IconSettingsOutline14',
|
|
1401
1451
|
'IconSettingsOutline16', 'IconShareOutline16', 'IconStopFill16', 'IconThinkOutline14',
|
|
1402
1452
|
'IconThinkOutline16', 'IconTrashOutline16', 'IconUserOutline16', 'IconWarningOutline16',
|
|
1403
1453
|
'IconLikeOutline16', 'IconDislikeOutline16', 'IconFollowsystemOutline16',
|
|
1404
1454
|
'IconAlarmClockOutline16', 'IconClockOutline16', 'IconDatabaseOutline16',
|
|
1455
|
+
// Added with dsh 0.1.6-alpha.1 — absent on older hosts, where the picker
|
|
1456
|
+
// filters them out (see ICON_PICKER_CHOICES).
|
|
1457
|
+
'IconPaperPlaneOutline14', 'IconShieldOutline16', 'IconPlanOutline14',
|
|
1458
|
+
'IconWrapLinesOutline16', 'IconCompactOutline16',
|
|
1405
1459
|
]
|
|
1406
1460
|
|
|
1461
|
+
/**
|
|
1462
|
+
* What the picker actually offers on THIS host: every legacy slot, plus the
|
|
1463
|
+
* primitives this page's icon module really exports, deduped by resolved
|
|
1464
|
+
* name so a retired alias can never double a cell. Computed once — the icon
|
|
1465
|
+
* module is fixed for the lifetime of the page.
|
|
1466
|
+
*/
|
|
1467
|
+
const ICON_PICKER_CHOICES = (() => {
|
|
1468
|
+
const seen = new Set()
|
|
1469
|
+
return ICON_CHOICES.filter((mode) => {
|
|
1470
|
+
if (mode === 'solid' || mode === 'outline' || mode === 'none') return true
|
|
1471
|
+
const name = resolveIconName(mode)
|
|
1472
|
+
if (name === '' || seen.has(name)) return false
|
|
1473
|
+
seen.add(name)
|
|
1474
|
+
return true
|
|
1475
|
+
})
|
|
1476
|
+
})()
|
|
1477
|
+
|
|
1407
1478
|
/**
|
|
1408
1479
|
* Appearance controls shared by the per-row dialog and the settings card's
|
|
1409
1480
|
* "default appearance" section: color, glow, weight, shadow, the text
|
|
1410
|
-
* outline (on/off + width) and — where the row actually renders
|
|
1411
|
-
* folder glyph.
|
|
1412
|
-
*
|
|
1413
|
-
*
|
|
1481
|
+
* outline (on/off + width + color) and — where the row actually renders
|
|
1482
|
+
* one — the folder glyph. Since v0.10.1 the outline color is a normal
|
|
1483
|
+
* choice (presets + picker, gray by default); "auto" is the mode that
|
|
1484
|
+
* derives the contrasting pole from the effective font color, so a palette
|
|
1485
|
+
* flip keeps working without a re-render.
|
|
1414
1486
|
*/
|
|
1415
1487
|
function AppearanceControls({ value, onChange, allowIcon, t }) {
|
|
1416
1488
|
const appearance = readAppearance(value)
|
|
@@ -1422,6 +1494,9 @@ window.__ModuleLoader__.load({
|
|
|
1422
1494
|
const strokeWidth = appearance.strokeWidth
|
|
1423
1495
|
const strokeColor = appearance.strokeColor
|
|
1424
1496
|
const iconMode = value && value.icon ? value.icon : 'solid'
|
|
1497
|
+
// The glyph this choice actually resolves to on THIS host: a value saved
|
|
1498
|
+
// before the host renamed its icon set still highlights a real cell.
|
|
1499
|
+
const activeGlyph = resolveIconName(iconMode)
|
|
1425
1500
|
const patch = (next) => { if (typeof onChange === 'function') onChange(next) }
|
|
1426
1501
|
const channels = colorToRgb(color)
|
|
1427
1502
|
const setChannel = (index, raw) => {
|
|
@@ -1570,10 +1645,13 @@ window.__ModuleLoader__.load({
|
|
|
1570
1645
|
allowIcon ? E('div', { className: 'bw-field' },
|
|
1571
1646
|
t('custom.icon'),
|
|
1572
1647
|
E('div', { className: 'bw-icon-grid' },
|
|
1573
|
-
|
|
1648
|
+
ICON_PICKER_CHOICES.map((mode) => E('button', {
|
|
1574
1649
|
key: mode,
|
|
1575
1650
|
type: 'button',
|
|
1576
|
-
|
|
1651
|
+
// A value saved before an alias flip still highlights the cell it
|
|
1652
|
+
// now resolves to, instead of leaving the grid with no selection.
|
|
1653
|
+
className: cls('bw-icon-cell',
|
|
1654
|
+
(iconMode === mode || (activeGlyph !== '' && resolveIconName(mode) === activeGlyph)) && 'bw-icon-cell-active'),
|
|
1577
1655
|
title: mode === 'solid' ? t('custom.icon.solid') : (mode === 'outline' ? t('custom.icon.outline') : mode),
|
|
1578
1656
|
'aria-label': mode === 'solid' ? t('custom.icon.solid') : (mode === 'outline' ? t('custom.icon.outline') : mode),
|
|
1579
1657
|
onClick: () => patch({ icon: mode }),
|
|
@@ -1819,6 +1897,7 @@ window.__ModuleLoader__.load({
|
|
|
1819
1897
|
const sessionGroupsMap = useStore ? (useStore(s => s.sessionGroups) || {}) : {}
|
|
1820
1898
|
const prefsMap = useStore ? (useStore(s => s.prefs) || {}) : {}
|
|
1821
1899
|
const stylingMap = useStore ? (useStore(s => s.styling) || {}) : {}
|
|
1900
|
+
const sessionOrderMap = useStore ? (useStore(s => s.sessionOrder) || {}) : {}
|
|
1822
1901
|
// Dual-write wrappers: every preference mutation lands in the local
|
|
1823
1902
|
// store (immediate echo + scope-less fallback) AND the host settings
|
|
1824
1903
|
// store (durable cross-device copy for the manual pull on the other
|
|
@@ -2071,6 +2150,13 @@ window.__ModuleLoader__.load({
|
|
|
2071
2150
|
return { ...built, folders: built.folders.map((f) => materializeChain(compressTree(f))), workspaces: built.workspaces }
|
|
2072
2151
|
}, [items, storeFolders, compactChains, draggingWorkspace])
|
|
2073
2152
|
|
|
2153
|
+
/** Browser-local flat session order for one workspace (fallback channel). */
|
|
2154
|
+
const sessionOrderOf = (workspaceId) => {
|
|
2155
|
+
const map = sessionOrderMap && typeof sessionOrderMap === 'object' ? sessionOrderMap : {}
|
|
2156
|
+
const list = map[workspaceId]
|
|
2157
|
+
return Array.isArray(list) ? list : []
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2074
2160
|
const sessionsOf = (workspace) => {
|
|
2075
2161
|
const rows = []
|
|
2076
2162
|
for (const id of workspace.sessionIds || []) {
|
|
@@ -2089,7 +2175,21 @@ window.__ModuleLoader__.load({
|
|
|
2089
2175
|
pending: pendingKindOf(pending, id),
|
|
2090
2176
|
})
|
|
2091
2177
|
}
|
|
2092
|
-
|
|
2178
|
+
// Browser-local reorder fallback (dsh 0.1.6-alpha.1 stopped injecting
|
|
2179
|
+
// insertSessionBefore): the local flat order wins for the workspaces the
|
|
2180
|
+
// user dragged in, and is empty everywhere else — so a host that still
|
|
2181
|
+
// exposes the action keeps its authoritative order untouched.
|
|
2182
|
+
// Host order stays authoritative wherever the action exists.
|
|
2183
|
+
const local = typeof insertSessionBefore === 'function' ? [] : sessionOrderOf(workspace.workspaceId)
|
|
2184
|
+
if (local.length === 0) return rows
|
|
2185
|
+
const remaining = new Map(rows.map((row) => [row.id, row]))
|
|
2186
|
+
const out = []
|
|
2187
|
+
for (const id of local) {
|
|
2188
|
+
const row = remaining.get(id)
|
|
2189
|
+
if (row) { out.push(row); remaining.delete(id) }
|
|
2190
|
+
}
|
|
2191
|
+
for (const row of rows) if (remaining.has(row.id)) out.push(row)
|
|
2192
|
+
return out
|
|
2093
2193
|
}
|
|
2094
2194
|
|
|
2095
2195
|
const searchAgent = (agent) => {
|
|
@@ -2195,7 +2295,11 @@ window.__ModuleLoader__.load({
|
|
|
2195
2295
|
const canDragWorkspace = typeof insertWorkspaceBefore === 'function'
|
|
2196
2296
|
// Session REORDER needs the host insertSessionBefore action; dragging a
|
|
2197
2297
|
// session onto a sub-group row (a pure rename) stays available without it.
|
|
2198
|
-
|
|
2298
|
+
// Session reorder keeps working either way: the host action is preferred
|
|
2299
|
+
// (authoritative, visible to every surface), while hosts that no longer
|
|
2300
|
+
// inject it — dsh 0.1.6-alpha.1 dropped insertSessionBefore from the
|
|
2301
|
+
// browser contract — fall back to the browser-local flat order.
|
|
2302
|
+
const canReorderSessions = true
|
|
2199
2303
|
|
|
2200
2304
|
/* --------------------- workspace drag & drop -------------------- */
|
|
2201
2305
|
|
|
@@ -2373,9 +2477,16 @@ window.__ModuleLoader__.load({
|
|
|
2373
2477
|
const anchor = half === 'after'
|
|
2374
2478
|
? (index === -1 ? undefined : (flat[index + 1] ? flat[index + 1].id : undefined))
|
|
2375
2479
|
: targetSessionId
|
|
2376
|
-
|
|
2377
|
-
.
|
|
2378
|
-
|
|
2480
|
+
if (typeof insertSessionBefore === 'function') {
|
|
2481
|
+
Promise.resolve()
|
|
2482
|
+
.then(() => (anchor !== undefined ? insertSessionBefore(workspaceId, source.sessionId, anchor) : insertSessionBefore(workspaceId, source.sessionId)))
|
|
2483
|
+
.catch(fail)
|
|
2484
|
+
return
|
|
2485
|
+
}
|
|
2486
|
+
// Host channel gone: commit the same move into the browser-local order.
|
|
2487
|
+
if (actions && typeof actions.setSessionOrder === 'function') {
|
|
2488
|
+
actions.setSessionOrder(workspaceId, reorderIds(flat.map((s) => s.id), source.sessionId, anchor))
|
|
2489
|
+
}
|
|
2379
2490
|
}
|
|
2380
2491
|
const commitSessionMoveInto = (workspaceId, groupPath) => {
|
|
2381
2492
|
const source = drag.source
|