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 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.1",
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.1",
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
- /** Custom icon value → glyph: legacy slots (solid/outline/none) or any primitives icon name. */
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', 'IconSendOutline16', 'IconSettingsOutline14',
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 one — the
1411
- * folder glyph. The outline color is never picked by hand (see the
1412
- * appearance block above): it is derived from the effective font color, so
1413
- * it keeps working when the palette flips.
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
- ICON_CHOICES.map((mode) => E('button', {
1648
+ ICON_PICKER_CHOICES.map((mode) => E('button', {
1574
1649
  key: mode,
1575
1650
  type: 'button',
1576
- className: cls('bw-icon-cell', iconMode === mode && 'bw-icon-cell-active'),
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
- return rows
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
- const canReorderSessions = typeof insertSessionBefore === 'function'
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
- Promise.resolve()
2377
- .then(() => (anchor !== undefined ? insertSessionBefore(workspaceId, source.sessionId, anchor) : insertSessionBefore(workspaceId, source.sessionId)))
2378
- .catch(fail)
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