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 +3 -3
- package/README_EN.md +3 -3
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/client.js +51 -11
- package/src/index.js +10 -1
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 图标库同族的
|
|
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 (
|
|
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 & 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.
|
|
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.
|
|
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
|
-
*
|
|
274
|
-
*
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|