dsh-better-workspace 0.5.0 → 0.7.0

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
@@ -34,7 +34,7 @@ web/ ← 虚拟分组(不是真实目录,纯命名层级)
34
34
  <table>
35
35
  <tr>
36
36
  <td align="center" width="58%"><img src="docs/screenshots/3-appearance-dialog.png" alt="外观自定义弹窗(实时预览)"/></td>
37
- <td valign="top"><b>外观自定义</b><br/>右键任意一层(工作区分组 / 工作区 / 会话 / 会话分组)→「自定义外观」:颜色(预设色板 + 取色器 + RGB)、发光强度(0..14 拖动条,只作用于文字)、字体粗细、字体阴影开关、图标(与 dsh 图标库同族的 66 个,工作区与工作区分组专属),底部带实时预览。清除自定义一键复原。</td>
37
+ <td valign="top"><b>外观自定义</b><br/>右键任意一层(工作区分组 / 工作区 / 会话 / 会话分组)→「自定义外观」:颜色(预设色板 + 取色器 + RGB)、发光强度(0..14 拖动条,只作用于文字)、字体粗细、字体阴影开关、图标(与 dsh 图标库同族的 69 个,工作区与工作区分组专属),底部带实时预览。清除自定义一键复原。</td>
38
38
  </tr>
39
39
  <tr>
40
40
  <td align="center" width="58%"><img src="docs/screenshots/2-customized-tree.png" alt="外观自定义效果"/></td>
@@ -59,8 +59,8 @@ web/ ← 虚拟分组(不是真实目录,纯命名层级)
59
59
  ### 其余亮点
60
60
 
61
61
  - **文件夹式收起**:点工作区行 = 收起/展开该工作区的全部会话(收起后行上保留会话数角标);会话分组同样可收起;搜索时自动全部展开。
62
- - **拖拽还原原版能力**:工作区行可拖拽——拖到另一工作区行上/下半即调整顺序(写回宿主 registry 序),拖到分组行则移动进该分组;会话行可拖拽——同工作区拖到另一会话上/下调整顺序,拖到会话子分组即移动进该分组。显示顺序 = 宿主手动序,拖拽结果立即生效。
63
- - **不丢原生能力**:会话行(打开/重命名/分叉/归档)、每工作区「新会话」、当前会话高亮、官方状态灯(蓝色运行环 / 琥珀待交互 / 绿色完成提醒)、未分组会话兜底、中英双语跟随界面语言。
62
+ - **拖拽还原原版能力**:工作区行可拖拽——拖到另一工作区行上/下半即调整顺序(写回宿主 registry 序),拖到分组行则移动进该分组;会话行可拖拽——同工作区拖到另一会话上/下调整顺序,拖到会话子分组即移动进该分组。显示顺序 = 宿主手动序,拖拽结果立即生效。拖拽工作区期间,单链合并显示的行(如 工作/A)临时展开回文件夹树——路径里的每一级都是可投放的分组行,松手后自动合并回去。
63
+ - **不丢原生能力**:会话行(打开/重命名/分叉/归档)、每工作区「新会话」、当前会话高亮、官方状态灯(蓝色运行环 / 琥珀待交互 / 绿色完成提醒)、**活跃定时任务角标**(有活动 Schedule 的会话显示闹钟图标,与官方行一致)、未分组会话兜底、中英双语跟随界面语言。
64
64
  - **对话空态页同样生效**:会话空态页的「添加工作区」菜单走的也是本插件的拾取交互,同样带所属分组弹窗。
65
65
 
66
66
  ## 安装
package/README_EN.md CHANGED
@@ -32,7 +32,7 @@ Session titles nest on `/` too (e.g. `test1/plugin maintenance`). Session groups
32
32
  <table>
33
33
  <tr>
34
34
  <td align="center" width="58%"><img src="docs/screenshots/3-appearance-dialog.png" alt="Appearance dialog with live preview"/></td>
35
- <td valign="top"><b>Appearance customization</b><br/>Right-click any row (workspace folder / workspace / session / session group) → Customize: color (swatches + picker + RGB), glow strength (0–14 slider, text only), font weight, font shadow, an icon grid (66 icons from the dsh primitives family, workspace &amp; folder rows only), with a live preview at the bottom. Reset returns to default in one click.</td>
35
+ <td valign="top"><b>Appearance customization</b><br/>Right-click any row (workspace folder / workspace / session / session group) → Customize: color (swatches + picker + RGB), glow strength (0–14 slider, text only), font weight, font shadow, an icon grid (69 icons from the dsh primitives family, workspace &amp; folder rows only), with a live preview at the bottom. Reset returns to default in one click.</td>
36
36
  </tr>
37
37
  <tr>
38
38
  <td align="center" width="58%"><img src="docs/screenshots/2-customized-tree.png" alt="Custom appearance result"/></td>
@@ -57,8 +57,8 @@ Session titles nest on `/` too (e.g. `test1/plugin maintenance`). Session groups
57
57
  ### More highlights
58
58
 
59
59
  - **Folder-style collapse**: clicking a workspace row collapses/expands all of its sessions (the row keeps a session counter when collapsed); session groups collapse too; search force-expands everything.
60
- - **Native drag restored**: drag a workspace row above/below another to reorder (writes back the host registry order) or onto a folder row to move it into that group; drag session rows to reorder within the same workspace or onto a session group to move. Display order = host manual order, drag results are visible immediately.
61
- - **Native capabilities kept**: open/rename/fork/archive sessions, per-workspace new-session, current-session highlight, official status dots (blue running ring / amber pending / green done), ungrouped-session fallback, zh/en localization.
60
+ - **Native drag restored**: drag a workspace row above/below another to reorder (writes back the host registry order) or onto a folder row to move it into that group; drag session rows to reorder within the same workspace or onto a session group to move. Display order = host manual order, drag results are visible immediately. While dragging a workspace, merged single-chain rows (e.g. work/A) temporarily re-expand into folder rows — every level of the path becomes a drop target, and chains merge back when the drag ends.
61
+ - **Native capabilities kept**: open/rename/fork/archive sessions, per-workspace new-session, current-session highlight, official status dots (blue running ring / amber pending / green done), **active-schedule badge** (alarm clock icon for sessions with active Schedule records, matching the official rows), ungrouped-session fallback, zh/en localization.
62
62
  - **Conversation hero too**: the empty-state “Add workspace” menu uses the same picking interaction with the same group popup.
63
63
 
64
64
  ## Install
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-better-workspace",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
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.5.0",
3
+ "version": "0.7.0",
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
@@ -52,6 +52,7 @@ window.__ModuleLoader__.load({
52
52
  'status.planReview': '等待计划确认',
53
53
  'status.question': '等待回答',
54
54
  'status.subagents': '{n} 个子任务运行中',
55
+ 'schedule.active': '有活动定时任务',
55
56
  'menu.rename': '重命名',
56
57
  'menu.delete': '删除',
57
58
  'menu.fork': '分叉',
@@ -64,7 +65,7 @@ window.__ModuleLoader__.load({
64
65
  'settings.expand': '展开',
65
66
  'settings.collapse': '收起',
66
67
  'settings.compactChains': '单链分组折叠显示',
67
- 'settings.compactChains.hint': '单层链合并为一行,出现多个子级时自动展开为树状;展开状态与自定义外观保存在当前浏览器。',
68
+ 'settings.compactChains.hint': '单层链合并为一行,出现多个子级时自动展开为树状;拖拽工作区期间单链临时展开回文件夹树,可放入任意一级;展开状态与自定义外观保存在当前浏览器。',
68
69
  'custom.title': '自定义外观',
69
70
  'custom.color': '颜色',
70
71
  'custom.glow': '发光',
@@ -137,6 +138,7 @@ window.__ModuleLoader__.load({
137
138
  'status.planReview': 'Waiting for plan review',
138
139
  'status.question': 'Waiting for answer',
139
140
  'status.subagents': '{n} subagent(s) running',
141
+ 'schedule.active': 'Has active scheduled task',
140
142
  'menu.rename': 'Rename',
141
143
  'menu.delete': 'Delete',
142
144
  'menu.fork': 'Fork',
@@ -149,7 +151,7 @@ window.__ModuleLoader__.load({
149
151
  'settings.expand': 'Expand',
150
152
  'settings.collapse': 'Collapse',
151
153
  'settings.compactChains': 'Merge single-child chains',
152
- 'settings.compactChains.hint': 'Single-child chains merge into one row; levels with multiple children expand as a tree. State and custom styling persist in this browser.',
154
+ 'settings.compactChains.hint': 'Single-child chains merge into one row; levels with multiple children expand as a tree. Chains re-expand into folder rows while you drag a workspace, so it can drop into any level. State and custom styling persist in this browser.',
153
155
  'custom.title': 'Customize',
154
156
  'custom.color': 'Color',
155
157
  'custom.glow': 'Glow',
@@ -268,18 +270,33 @@ window.__ModuleLoader__.load({
268
270
  && !(archivedSet && archivedSet.has(summary.id))
269
271
  && (!summary.blank || summary.id === current)
270
272
 
273
+ /**
274
+ * Active-schedule marker, mirroring the official tree's
275
+ * hasActiveSchedule(): the list projection carries one entry per active
276
+ * Schedule record, and a non-empty projection is the badge's only gate.
277
+ * Defensive shapes (missing/mis-typed projection) degrade to false.
278
+ */
279
+ const hasActiveScheduleOf = (summary) => !!(summary
280
+ && summary.projectionValues
281
+ && Array.isArray(summary.projectionValues.schedule)
282
+ && summary.projectionValues.schedule.length > 0)
283
+
271
284
  /**
272
285
  * Running subagent descendants per session (light lineage walk over
273
- * parentSessionId links) — a parent row keeps its "ongoing" ring while a
274
- * spawned subagent is still working.
286
+ * parent links) — a parent row keeps its "ongoing" ring while a spawned
287
+ * subagent is still working. The client SessionSummary exposes the
288
+ * parent as parentId (the session controller maps parentSessionId to
289
+ * parentId on the wire); the old parentSessionId spelling is kept as a
290
+ * fallback for profiles serving the pre-rename shape.
275
291
  */
276
292
  const subagentRunningCounts = (byId) => {
277
293
  const children = new Map()
278
294
  for (const id of Object.keys(byId || {})) {
279
295
  const summary = byId[id]
280
- if (!summary || !summary.parentSessionId) continue
281
- let list = children.get(summary.parentSessionId)
282
- if (!list) { list = []; children.set(summary.parentSessionId, list) }
296
+ const parentId = summary && (summary.parentId || summary.parentSessionId)
297
+ if (!summary || !parentId) continue
298
+ let list = children.get(parentId)
299
+ if (!list) { list = []; children.set(parentId, list) }
283
300
  list.push(summary)
284
301
  }
285
302
  const countFor = (rootId) => {
@@ -458,9 +475,10 @@ window.__ModuleLoader__.load({
458
475
  '.bw-row-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
459
476
  '.bw-row-count{flex:none;font-size:11px;color:var(--dsw-alias-label-quaternary,#8a8a8a)}',
460
477
  '.bw-row-time{flex:none;font-size:11px;color:var(--dsw-alias-label-quaternary,#8a8a8a)}',
478
+ '.bw-schedule-badge{flex:none;display:inline-flex;align-items:center;color:var(--dsw-alias-label-tertiary,#9a9a9a);margin:0 6px}',
461
479
  '.bw-row-actions{flex:none;display:none;align-items:center;gap:2px}',
462
480
  '.bw-row:hover .bw-row-actions{display:flex}',
463
- '.bw-row:hover .bw-row-time,.bw-row:hover .bw-row-count{display:none}',
481
+ '.bw-row:hover .bw-row-time,.bw-row:hover .bw-row-count,.bw-row:hover .bw-schedule-badge{display:none}',
464
482
  '.bw-dot{flex:none;width:6px;height:6px;border-radius:50%;background:transparent}',
465
483
  '.bw-session-row{font-size:12.5px;color:var(--dsw-alias-label-secondary,#b8b8b8);min-height:26px}',
466
484
  '.bw-sgroup-row{font-size:12.5px;color:var(--dsw-alias-label-tertiary,#9a9a9a);min-height:24px}',
@@ -842,6 +860,9 @@ window.__ModuleLoader__.load({
842
860
  : E('span', { className: 'bw-dot' }),
843
861
  ),
844
862
  E('span', { className: 'bw-row-label' }, node.leaf || node.title),
863
+ node.hasActiveSchedule
864
+ ? E('span', { className: 'bw-schedule-badge', role: 'img', 'aria-label': t('schedule.active'), title: t('schedule.active') }, icon('IconAlarmClockOutline16', 14))
865
+ : null,
845
866
  E('span', { className: 'bw-row-time' }, timeLabel(node.updatedAt, now, t)),
846
867
  )
847
868
  }
@@ -890,6 +911,7 @@ window.__ModuleLoader__.load({
890
911
  'IconSettingsOutline16', 'IconShareOutline16', 'IconStopFill16', 'IconThinkOutline14',
891
912
  'IconThinkOutline16', 'IconTrashOutline16', 'IconUserOutline16', 'IconWarningOutline16',
892
913
  'IconLikeOutline16', 'IconDislikeOutline16', 'IconFollowsystemOutline16',
914
+ 'IconAlarmClockOutline16', 'IconClockOutline16', 'IconDatabaseOutline16',
893
915
  ]
894
916
 
895
917
  /**
@@ -1178,6 +1200,7 @@ window.__ModuleLoader__.load({
1178
1200
  blank: !!summary.blank,
1179
1201
  running: !!summary.running,
1180
1202
  completed: summary.completed === true,
1203
+ hasActiveSchedule: hasActiveScheduleOf(summary),
1181
1204
  subagents: subCounts.get(id) || 0,
1182
1205
  updatedAt: summary.updatedAt || 0,
1183
1206
  pending: pendingKindOf(pending, id),
@@ -1186,11 +1209,19 @@ window.__ModuleLoader__.load({
1186
1209
  ungrouped.sort((a, b) => b.updatedAt - a.updatedAt)
1187
1210
  }
1188
1211
 
1212
+ // While a workspace drag is active, single-child chains render UNCOMPRESSED:
1213
+ // a merged "group/workspace" row hides every folder level of the chain inside
1214
+ // its label, and those levels are exactly the drop targets for "move into
1215
+ // this group". Suspending the merge for the drag's duration re-exposes each
1216
+ // level as a real folder row (folder expansion defaults apply); when the
1217
+ // drag ends, the chains merge back. Session drags keep the merged view —
1218
+ // their drop targets live inside workspace rows, which compression merges.
1219
+ const draggingWorkspace = drag !== null && drag.kind === 'workspace'
1189
1220
  const tree = React.useMemo(() => {
1190
1221
  const built = buildTree(items, storeFolders)
1191
- if (!compactChains) return built
1222
+ if (!compactChains || draggingWorkspace) return built
1192
1223
  return { ...built, folders: built.folders.map(compressTree), workspaces: built.workspaces }
1193
- }, [items, storeFolders, compactChains])
1224
+ }, [items, storeFolders, compactChains, draggingWorkspace])
1194
1225
 
1195
1226
  const sessionsOf = (workspace) => {
1196
1227
  const rows = []
@@ -1204,6 +1235,7 @@ window.__ModuleLoader__.load({
1204
1235
  blank: !!summary.blank,
1205
1236
  running: !!summary.running,
1206
1237
  completed: summary.completed === true,
1238
+ hasActiveSchedule: hasActiveScheduleOf(summary),
1207
1239
  subagents: subCounts.get(id) || 0,
1208
1240
  updatedAt: summary.updatedAt || 0,
1209
1241
  pending: pendingKindOf(pending, id),
@@ -1292,7 +1324,15 @@ window.__ModuleLoader__.load({
1292
1324
  event.dataTransfer.effectAllowed = 'move'
1293
1325
  event.dataTransfer.setData('text/plain', workspace.workspaceId)
1294
1326
  } catch { /* drag payload is best-effort */ }
1295
- setDrag({ kind: 'workspace', source: { workspaceId: workspace.workspaceId, leaf: workspace.leaf, folderPath: workspace.folderPath || '' }, over: null })
1327
+ // Drag identity derives from the RAW title, never the displayed leaf:
1328
+ // a compressed single-chain row shows "group/workspace" as its leaf with
1329
+ // folderPath '' — carrying that would make move-into / cross-folder
1330
+ // drops rebuild titles like "x/group/workspace". The real leaf and
1331
+ // folder keep every drop target's rename correct.
1332
+ const segs = splitTitleSegs(workspace.title)
1333
+ const sourceLeaf = segs.length > 0 ? segs[segs.length - 1] : (workspace.leaf || String(workspace.workspaceId))
1334
+ const sourceFolder = segs.length > 1 ? segs.slice(0, -1).join('/') : ''
1335
+ setDrag({ kind: 'workspace', source: { workspaceId: workspace.workspaceId, leaf: sourceLeaf, folderPath: sourceFolder }, over: null })
1296
1336
  },
1297
1337
  onDragEnd: () => setDrag(null),
1298
1338
  onDragOver: (event) => {
package/src/index.js CHANGED
@@ -40,7 +40,16 @@ export function apply(ctx) {
40
40
  const settings = sctx && sctx.settings
41
41
  if (!settings || typeof settings.register !== 'function') return
42
42
  const Schema = sm.default
43
- settings.register(ds.settingsNamespace('better-workspace'), Schema.object({ compactChains: Schema.boolean().default(true) }))
43
+ // dsh >= 0.1.2-alpha.2 removed the settingsNamespace() helper from
44
+ // @deepseek-ai/dsh-settings: register() now takes a plain string and
45
+ // validates it at runtime (parseSettingsNamespace). The helper's old
46
+ // signature was compile-time branding only, so a plain string is also
47
+ // accepted by the older register() — one call, both eras. Only use the
48
+ // helper when the installed package still ships it.
49
+ const ns = typeof ds.settingsNamespace === 'function'
50
+ ? ds.settingsNamespace('better-workspace')
51
+ : 'better-workspace'
52
+ settings.register(ns, Schema.object({ compactChains: Schema.boolean().default(true) }))
44
53
  log('[dsh-better-workspace] settings namespace registered: better-workspace')
45
54
  })
46
55
  .catch((error) => {