dsh-session-mng 0.1.1 → 0.1.3
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/CHANGELOG.md +37 -0
- package/README.md +25 -6
- package/ROADMAP.md +30 -4
- package/client.js +272 -66
- package/docs/01-design.md +23 -13
- package/docs/02-open-source-and-marketplace.md +21 -0
- package/docs/HANDOVER-2026-10-27.md +133 -0
- package/docs/PLAN-2026-10-27.md +2 -0
- package/docs/PLAN-2026-10-28-branch-visibility.md +116 -0
- package/docs/PLAN-2026-10-28-legacy-fork-safety.md +62 -0
- package/docs/RELEASE-CHECKLIST-0.1.1.md +18 -5
- package/docs/RELEASE-CHECKLIST-0.1.2.md +31 -0
- package/docs/RELEASE-CHECKLIST-0.1.3.md +38 -0
- package/lib/service.js +64 -16
- package/lib/state.js +54 -9
- package/package.json +4 -3
package/client.js
CHANGED
|
@@ -43,7 +43,13 @@ window.__ModuleLoader__.load({
|
|
|
43
43
|
sortCreated: '创建时间',
|
|
44
44
|
sortSize: '大小',
|
|
45
45
|
sortTitle: '标题',
|
|
46
|
-
showSubagents: '
|
|
46
|
+
showSubagents: '显示子代理会话',
|
|
47
|
+
showSubagentsHint: '子代理会话由委派产生,默认隐藏;分支会话始终显示。',
|
|
48
|
+
showBlank: '显示空会话',
|
|
49
|
+
cleanupBlank: '清理空会话({n})',
|
|
50
|
+
cleanupBlankHint: '批量删除已归档的空会话;未归档的空会话需先归档。',
|
|
51
|
+
blankActiveHint: '另有 {n} 个未归档空会话,归档后才能清理。',
|
|
52
|
+
hiddenBlankCount: '已隐藏 {n} 个空会话',
|
|
47
53
|
selectAll: '全选当前列表',
|
|
48
54
|
expandAll: '展开全部',
|
|
49
55
|
collapseAll: '收起全部',
|
|
@@ -65,7 +71,14 @@ window.__ModuleLoader__.load({
|
|
|
65
71
|
untitled: '(未命名)',
|
|
66
72
|
running: '运行中',
|
|
67
73
|
archived: '已归档',
|
|
68
|
-
subagent: '
|
|
74
|
+
subagent: '子代理',
|
|
75
|
+
branch: '分支',
|
|
76
|
+
fromParent: '↳ 来自:{title}',
|
|
77
|
+
hiddenSubagentCount: '已隐藏 {n} 个子代理会话',
|
|
78
|
+
forkCount: '{n} 分支',
|
|
79
|
+
forkCountHint: '该会话有 {n} 个分支会话',
|
|
80
|
+
parentSession: '父会话',
|
|
81
|
+
parentMissing: '来源父会话已不存在',
|
|
69
82
|
pinned: '已置顶',
|
|
70
83
|
liveBadge: '已打开',
|
|
71
84
|
noCwd: '无工作目录',
|
|
@@ -108,7 +121,9 @@ window.__ModuleLoader__.load({
|
|
|
108
121
|
moving: '正在移动…',
|
|
109
122
|
moveNoTarget: '没有可移动到的目标工作区。',
|
|
110
123
|
moveLiveHint: '该会话已在当前 DSH 进程中打开,重启 DSH 后再移动。',
|
|
111
|
-
moveSubagentHint: '
|
|
124
|
+
moveSubagentHint: '子代理会话不能单独移动。',
|
|
125
|
+
moveForkHint: '分支会话必须和父会话留在同一工作区,暂不支持单独移动。',
|
|
126
|
+
moveForkChildrenHint: '该会话有 {n} 个分支会话;移动只迁移本会话,分支会留在原工作区并继续独立使用。',
|
|
112
127
|
moveDescendantsHint: '该会话包含子代理会话,暂时不能单独移动。',
|
|
113
128
|
deleteAction: '彻底删除',
|
|
114
129
|
deleteHint: '只允许删除已归档会话;删除后日志目录与工作区记录都会被移除,无法恢复。',
|
|
@@ -125,6 +140,7 @@ window.__ModuleLoader__.load({
|
|
|
125
140
|
deleteNotArchived: '只有已归档会话可以彻底删除。',
|
|
126
141
|
deleteLiveHint: '该会话已在当前 DSH 进程中打开,重启 DSH 后再删除。',
|
|
127
142
|
deleteCascadeNote: '如果该会话有子代理会话,会一并删除。',
|
|
143
|
+
deleteForkNote: '另有 {n} 个历史分支会话不会被删除,但会失去「来自父会话」的来源标识;如需一起删除,请把它们也勾选进来。',
|
|
128
144
|
archiveAction: '归档',
|
|
129
145
|
archiving: '正在归档…',
|
|
130
146
|
archiveDone: '已归档 {n} 个会话。',
|
|
@@ -159,7 +175,7 @@ window.__ModuleLoader__.load({
|
|
|
159
175
|
timeout: '模型调用超时。',
|
|
160
176
|
llmError: '模型调用失败:{message}',
|
|
161
177
|
liveHint: '已打开的会话不能移动或彻底删除;重启 DSH 后重试。',
|
|
162
|
-
moveUnavailableHint: '
|
|
178
|
+
moveUnavailableHint: '所选会话中存在不可移动项(如跨工作区、分支/子代理会话或状态未就绪)。',
|
|
163
179
|
archiveHint: '归档后会话会从活跃列表移到已归档,可在本页恢复。',
|
|
164
180
|
moveArchivedHint: '移动不会改变归档状态,会话仍会留在已归档。',
|
|
165
181
|
working: '处理中…',
|
|
@@ -192,6 +208,12 @@ window.__ModuleLoader__.load({
|
|
|
192
208
|
sortSize: 'Size',
|
|
193
209
|
sortTitle: 'Title',
|
|
194
210
|
showSubagents: 'Show subagent sessions',
|
|
211
|
+
showSubagentsHint: 'Subagent sessions are produced by delegation and stay hidden by default; branch sessions are always listed.',
|
|
212
|
+
showBlank: 'Show empty sessions',
|
|
213
|
+
cleanupBlank: 'Clean up empty sessions ({n})',
|
|
214
|
+
cleanupBlankHint: 'Permanently delete archived empty sessions; unarchived empty sessions must be archived first.',
|
|
215
|
+
blankActiveHint: '{n} unarchived empty sessions must be archived before cleanup.',
|
|
216
|
+
hiddenBlankCount: '{n} empty sessions hidden',
|
|
195
217
|
selectAll: 'Select all shown',
|
|
196
218
|
expandAll: 'Expand all',
|
|
197
219
|
collapseAll: 'Collapse all',
|
|
@@ -214,6 +236,13 @@ window.__ModuleLoader__.load({
|
|
|
214
236
|
running: 'Running',
|
|
215
237
|
archived: 'Archived',
|
|
216
238
|
subagent: 'Subagent',
|
|
239
|
+
branch: 'Branch',
|
|
240
|
+
fromParent: '↳ from: {title}',
|
|
241
|
+
hiddenSubagentCount: '{n} subagent sessions hidden',
|
|
242
|
+
forkCount: '{n} branch',
|
|
243
|
+
forkCountHint: 'This session has {n} branch sessions',
|
|
244
|
+
parentSession: 'Parent session',
|
|
245
|
+
parentMissing: 'Source session no longer exists',
|
|
217
246
|
pinned: 'Pinned',
|
|
218
247
|
liveBadge: 'Open',
|
|
219
248
|
noCwd: 'No working directory',
|
|
@@ -257,6 +286,8 @@ window.__ModuleLoader__.load({
|
|
|
257
286
|
moveNoTarget: 'No target workspace is available.',
|
|
258
287
|
moveLiveHint: 'This session is open in this DSH process; restart DSH and retry.',
|
|
259
288
|
moveSubagentHint: 'Subagent sessions cannot be moved on their own.',
|
|
289
|
+
moveForkHint: 'A branch session must stay in its parent\'s workspace, so it cannot be moved on its own.',
|
|
290
|
+
moveForkChildrenHint: 'This session has {n} branch sessions; moving migrates only this session, and the branches stay in the original workspace and keep working independently.',
|
|
260
291
|
moveDescendantsHint: 'This session has subagent descendants and cannot move alone yet.',
|
|
261
292
|
deleteAction: 'Delete permanently',
|
|
262
293
|
deleteHint: 'Only archived sessions can be deleted. The log directory and workspace record are removed; this cannot be undone.',
|
|
@@ -273,6 +304,7 @@ window.__ModuleLoader__.load({
|
|
|
273
304
|
deleteNotArchived: 'Only archived sessions can be permanently deleted.',
|
|
274
305
|
deleteLiveHint: 'This session is open in this DSH process; restart DSH and retry.',
|
|
275
306
|
deleteCascadeNote: 'Any subagent descendants are deleted too.',
|
|
307
|
+
deleteForkNote: '{n} branch sessions are not deleted, but they lose their "from parent" label. Select them too if they should be deleted as well.',
|
|
276
308
|
archiveAction: 'Archive',
|
|
277
309
|
archiving: 'Archiving…',
|
|
278
310
|
archiveDone: 'Archived {n} sessions.',
|
|
@@ -307,7 +339,7 @@ window.__ModuleLoader__.load({
|
|
|
307
339
|
timeout: 'The model call timed out.',
|
|
308
340
|
llmError: 'The model call failed: {message}',
|
|
309
341
|
liveHint: 'An open session cannot be moved or deleted; restart DSH and retry.',
|
|
310
|
-
moveUnavailableHint: 'Some selected sessions cannot be moved (cross-workspace, subagent
|
|
342
|
+
moveUnavailableHint: 'Some selected sessions cannot be moved (cross-workspace, branch or subagent sessions, or not ready).',
|
|
311
343
|
archiveHint: 'Archiving moves the session from the active list to the archived list; restore it here.',
|
|
312
344
|
moveArchivedHint: 'Moving does not change the archive state; the session stays archived.',
|
|
313
345
|
working: 'Working…',
|
|
@@ -393,6 +425,7 @@ window.__ModuleLoader__.load({
|
|
|
393
425
|
.dsm-row-main{display:flex;flex-direction:column;align-items:flex-start;gap:1px;min-width:0;
|
|
394
426
|
border:none;background:transparent;color:inherit;font:inherit;text-align:left;padding:0;cursor:pointer}
|
|
395
427
|
.dsm-row-title{max-width:100%;font-size:13px;line-height:19px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
428
|
+
.dsm-branch-glyph{margin-right:4px;font-size:12px;line-height:1;color:var(--dsw-alias-label-secondary);opacity:.85}
|
|
396
429
|
.dsm-row-meta{max-width:100%;font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
397
430
|
.dsm-badges{display:flex;align-items:center;gap:4px;flex-wrap:wrap}
|
|
398
431
|
.dsm-badge{display:inline-flex;align-items:center;height:17px;padding:0 5px;border-radius:4px;font-size:10px;line-height:17px;
|
|
@@ -430,6 +463,22 @@ window.__ModuleLoader__.load({
|
|
|
430
463
|
.dsm-scope-row .dsm-mono{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
431
464
|
.dsm-danger-box{border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;padding:10px;background:var(--dsw-alias-bg-layer-1)}
|
|
432
465
|
.dsm-danger-box h3{color:var(--dsw-alias-state-error-primary)}
|
|
466
|
+
.dsm-scope-overlay{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;
|
|
467
|
+
padding:20px;background:rgba(0,0,0,.58)}
|
|
468
|
+
.dsm-scope-dialog{display:flex;flex-direction:column;width:min(660px,100%);max-height:min(82vh,760px);
|
|
469
|
+
border:1px solid var(--dsw-alias-state-error-primary);border-radius:10px;overflow:hidden;
|
|
470
|
+
background:var(--dsw-alias-bg-layer-2);box-shadow:0 24px 70px rgba(0,0,0,.5)}
|
|
471
|
+
.dsm-scope-head{display:flex;align-items:center;justify-content:space-between;gap:10px;
|
|
472
|
+
padding:11px 14px;border-bottom:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1)}
|
|
473
|
+
.dsm-scope-head h3{margin:0;font-size:13px;line-height:18px;color:var(--dsw-alias-state-error-primary)}
|
|
474
|
+
.dsm-scope-body{flex:1 1 auto;min-height:0;overflow:auto;padding:12px 14px}
|
|
475
|
+
.dsm-scope-foot{display:flex;flex-direction:column;gap:8px;padding:10px 14px;
|
|
476
|
+
border-top:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1)}
|
|
477
|
+
.dsm-scope-dialog .dsm-scope-list{max-height:none;overflow:visible;margin:8px 0}
|
|
478
|
+
@media (max-width:640px){
|
|
479
|
+
.dsm-scope-overlay{padding:10px}
|
|
480
|
+
.dsm-scope-dialog{max-height:92vh}
|
|
481
|
+
}
|
|
433
482
|
.dsm-footer{font-size:11px;line-height:17px;color:var(--dsw-alias-label-secondary)}
|
|
434
483
|
.dsm-spin{display:inline-block;width:12px;height:12px;border:2px solid var(--dsw-alias-border-l2);
|
|
435
484
|
border-top-color:var(--dsw-alias-brand-primary);border-radius:50%;animation:dsm-spin .8s linear infinite;vertical-align:-2px}
|
|
@@ -517,6 +566,7 @@ window.__ModuleLoader__.load({
|
|
|
517
566
|
if (code === 'session-running') return t('sessionRunning')
|
|
518
567
|
if (code === 'session-writer-held') return t('writerHeld')
|
|
519
568
|
if (code === 'has-descendants') return t('moveDescendantsHint')
|
|
569
|
+
if (code === 'branch-session') return t('moveForkHint')
|
|
520
570
|
if (code === 'archived-required') return t('deleteNotArchived')
|
|
521
571
|
if (code === 'unsupported-backend') return t('unsupportedBackend')
|
|
522
572
|
if (code === 'unsupported-registry') return t('unsupportedRegistry')
|
|
@@ -633,15 +683,28 @@ window.__ModuleLoader__.load({
|
|
|
633
683
|
}
|
|
634
684
|
|
|
635
685
|
function Row({ row, selected, onToggle, onOpen, t }) {
|
|
686
|
+
const depth = row.branchDepth ?? 0
|
|
687
|
+
const branch = row.kind === 'fork'
|
|
688
|
+
const forkCount = row.forkCount ?? 0
|
|
636
689
|
const badges = []
|
|
637
690
|
if (row.running) badges.push(h(Badge, { key: 'run', tone: 'run' }, t('running')))
|
|
638
691
|
if (row.archived) badges.push(h(Badge, { key: 'arch' }, t('archived')))
|
|
639
692
|
if (row.live) badges.push(h(Badge, { key: 'live', tone: 'brand' }, t('liveBadge')))
|
|
640
693
|
if (row.pinned) badges.push(h(Badge, { key: 'pin' }, t('pinned')))
|
|
694
|
+
if (branch) badges.push(h(Badge, { key: 'fork', tone: 'brand' }, t('branch')))
|
|
641
695
|
if (row.isSubagent) badges.push(h(Badge, { key: 'sub', tone: 'warn' }, t('subagent')))
|
|
696
|
+
if (forkCount > 0) {
|
|
697
|
+
badges.push(
|
|
698
|
+
h('span', { key: 'children', title: t('forkCountHint', { n: forkCount }) },
|
|
699
|
+
h(Badge, null, t('forkCount', { n: forkCount }))),
|
|
700
|
+
)
|
|
701
|
+
}
|
|
642
702
|
return h(
|
|
643
703
|
'div',
|
|
644
|
-
{
|
|
704
|
+
{
|
|
705
|
+
className: `dsm-row${selected === true ? ' selected' : ''}${depth > 0 ? ' dsm-row-branch' : ''}`,
|
|
706
|
+
style: depth > 0 ? { paddingLeft: `${depth * 18}px` } : undefined,
|
|
707
|
+
},
|
|
645
708
|
h('input', {
|
|
646
709
|
type: 'checkbox',
|
|
647
710
|
checked: selected === true,
|
|
@@ -651,10 +714,20 @@ window.__ModuleLoader__.load({
|
|
|
651
714
|
h(
|
|
652
715
|
'button',
|
|
653
716
|
{ type: 'button', className: 'dsm-row-main', onClick: onOpen, title: row.cwd || row.id },
|
|
654
|
-
h(
|
|
717
|
+
h(
|
|
718
|
+
'span',
|
|
719
|
+
{ className: 'dsm-row-title' },
|
|
720
|
+
depth > 0 ? h('span', { className: 'dsm-branch-glyph', 'aria-hidden': 'true' }, '↳') : null,
|
|
721
|
+
row.title === '' ? t('untitled') : row.title,
|
|
722
|
+
),
|
|
655
723
|
h(
|
|
656
724
|
'span',
|
|
657
725
|
{ className: 'dsm-row-meta' },
|
|
726
|
+
branch && row.parentTitle !== ''
|
|
727
|
+
? `${t('fromParent', { title: row.parentTitle })} · `
|
|
728
|
+
: branch && row.parentPresent === false
|
|
729
|
+
? `${t('parentMissing')} · `
|
|
730
|
+
: '',
|
|
658
731
|
row.cwd === '' ? t('noCwd') : row.cwd,
|
|
659
732
|
' · ',
|
|
660
733
|
row.id,
|
|
@@ -919,6 +992,7 @@ window.__ModuleLoader__.load({
|
|
|
919
992
|
React.useEffect(() => {
|
|
920
993
|
const controller = new AbortController()
|
|
921
994
|
setPhase('loading')
|
|
995
|
+
setChecked(false)
|
|
922
996
|
post('delete/preview', { sessionIds: ids }, controller.signal)
|
|
923
997
|
.then((value) => {
|
|
924
998
|
if (controller.signal.aborted) return
|
|
@@ -933,43 +1007,89 @@ window.__ModuleLoader__.load({
|
|
|
933
1007
|
return () => controller.abort()
|
|
934
1008
|
}, [idsKey, t])
|
|
935
1009
|
|
|
1010
|
+
React.useEffect(() => {
|
|
1011
|
+
const onKeyDown = (event) => {
|
|
1012
|
+
if (event.key !== 'Escape') return
|
|
1013
|
+
event.preventDefault()
|
|
1014
|
+
event.stopPropagation()
|
|
1015
|
+
if (busy !== true) onCancel()
|
|
1016
|
+
}
|
|
1017
|
+
document.addEventListener('keydown', onKeyDown, true)
|
|
1018
|
+
return () => document.removeEventListener('keydown', onKeyDown, true)
|
|
1019
|
+
}, [busy, onCancel])
|
|
1020
|
+
|
|
936
1021
|
const rows = preview?.sessions ?? []
|
|
937
1022
|
const blocked = rows.some((row) => row.live || row.running)
|
|
1023
|
+
const ready = phase === 'ready' && preview !== null
|
|
938
1024
|
return h(
|
|
939
1025
|
'div',
|
|
940
|
-
{
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1026
|
+
{
|
|
1027
|
+
className: 'dsm-scope-overlay',
|
|
1028
|
+
onMouseDown: (event) => {
|
|
1029
|
+
if (event.target === event.currentTarget && busy !== true) onCancel()
|
|
1030
|
+
},
|
|
1031
|
+
},
|
|
1032
|
+
h(
|
|
1033
|
+
'div',
|
|
1034
|
+
{
|
|
1035
|
+
className: 'dsm-scope-dialog',
|
|
1036
|
+
role: 'dialog',
|
|
1037
|
+
'aria-modal': 'true',
|
|
1038
|
+
'aria-label': t('deleteScope'),
|
|
1039
|
+
},
|
|
1040
|
+
h(
|
|
1041
|
+
'div',
|
|
1042
|
+
{ className: 'dsm-scope-head' },
|
|
1043
|
+
h('h3', null, t('deleteScope')),
|
|
1044
|
+
h('button', {
|
|
1045
|
+
type: 'button',
|
|
1046
|
+
className: 'dsm-btn ghost',
|
|
1047
|
+
'aria-label': t('close'),
|
|
1048
|
+
disabled: busy,
|
|
1049
|
+
onClick: onCancel,
|
|
1050
|
+
}, '×'),
|
|
1051
|
+
),
|
|
1052
|
+
h(
|
|
1053
|
+
'div',
|
|
1054
|
+
{ className: 'dsm-scope-body' },
|
|
1055
|
+
h('p', { className: 'dsm-hint', style: { margin: '0 0 8px' } }, t('deleteHint')),
|
|
1056
|
+
phase === 'loading'
|
|
1057
|
+
? h('p', { className: 'dsm-hint' }, t('deletePreviewLoading'))
|
|
1058
|
+
: phase === 'error'
|
|
1059
|
+
? h('p', { className: 'dsm-error' }, error)
|
|
1060
|
+
: h(
|
|
1061
|
+
React.Fragment,
|
|
1062
|
+
null,
|
|
1063
|
+
h('div', { className: 'dsm-hint' },
|
|
1064
|
+
`${t('deleteRoots')}: ${preview?.roots?.length ?? 0} · ${t('deleteDescendants')}: ${preview?.descendantCount ?? 0} · ${t('deleteTotalSize', { size: formatBytes(preview?.totalBytes ?? 0) })}`,
|
|
1065
|
+
),
|
|
1066
|
+
(preview?.forkChildren?.length ?? 0) > 0
|
|
1067
|
+
? h('p', { className: 'dsm-hint' }, t('deleteForkNote', { n: preview.forkChildren.length }))
|
|
1068
|
+
: null,
|
|
1069
|
+
h('div', { className: 'dsm-scope-list' },
|
|
1070
|
+
rows.map((row) =>
|
|
1071
|
+
h(
|
|
1072
|
+
'div',
|
|
1073
|
+
{ key: row.id, className: 'dsm-scope-row' },
|
|
1074
|
+
h('span', { className: 'dsm-badge' }, row.descendant ? t('scopeChild') : t('scopeRoot')),
|
|
1075
|
+
h('span', { className: 'dsm-mono', title: row.dir ?? row.id }, row.id),
|
|
1076
|
+
row.live ? h(Badge, { tone: 'brand' }, t('liveBadge')) : null,
|
|
1077
|
+
row.running ? h(Badge, { tone: 'run' }, t('running')) : null,
|
|
1078
|
+
row.missing ? h(Badge, { tone: 'warn' }, t('missingDir')) : null,
|
|
1079
|
+
row.readable === false ? h(Badge, { tone: 'warn' }, t('unreadable')) : null,
|
|
1080
|
+
h('span', { style: { marginLeft: 'auto' } }, formatBytes(row.bytes)),
|
|
1081
|
+
),
|
|
1082
|
+
),
|
|
965
1083
|
),
|
|
1084
|
+
h('p', { className: 'dsm-hint', style: { margin: '0 0 6px' } }, t('deleteCascadeNote')),
|
|
1085
|
+
blocked ? h('p', { className: 'dsm-error' }, t('deleteLiveHint')) : null,
|
|
966
1086
|
),
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1087
|
+
),
|
|
1088
|
+
h(
|
|
1089
|
+
'div',
|
|
1090
|
+
{ className: 'dsm-scope-foot' },
|
|
1091
|
+
ready
|
|
1092
|
+
? h('label', { className: 'dsm-check' },
|
|
973
1093
|
h('input', {
|
|
974
1094
|
type: 'checkbox',
|
|
975
1095
|
checked,
|
|
@@ -977,20 +1097,22 @@ window.__ModuleLoader__.load({
|
|
|
977
1097
|
onChange: (event) => setChecked(event.target.checked),
|
|
978
1098
|
}),
|
|
979
1099
|
t('deleteConfirmCheck'),
|
|
980
|
-
)
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1100
|
+
)
|
|
1101
|
+
: null,
|
|
1102
|
+
h(
|
|
1103
|
+
'div',
|
|
1104
|
+
{ className: 'dsm-actions' },
|
|
1105
|
+
h('button', {
|
|
1106
|
+
type: 'button',
|
|
1107
|
+
className: 'dsm-btn danger',
|
|
1108
|
+
disabled: busy || ready !== true || blocked || checked !== true,
|
|
1109
|
+
onClick: () => onConfirmed(ids),
|
|
1110
|
+
}, busy ? t('deleting') : t('deleteConfirm')),
|
|
1111
|
+
h('button', { type: 'button', className: 'dsm-btn ghost', disabled: busy, onClick: onCancel }, t('cancel')),
|
|
1112
|
+
blocked ? h('span', { className: 'dsm-hint' }, t('liveHint')) : null,
|
|
1113
|
+
),
|
|
1114
|
+
),
|
|
1115
|
+
),
|
|
994
1116
|
)
|
|
995
1117
|
}
|
|
996
1118
|
|
|
@@ -1046,6 +1168,7 @@ window.__ModuleLoader__.load({
|
|
|
1046
1168
|
row.archived ? h(Badge, null, t('archived')) : null,
|
|
1047
1169
|
row.live ? h(Badge, { tone: 'brand' }, t('liveBadge')) : null,
|
|
1048
1170
|
row.pinned ? h(Badge, null, t('pinned')) : null,
|
|
1171
|
+
row.kind === 'fork' ? h(Badge, { tone: 'brand' }, t('branch')) : null,
|
|
1049
1172
|
row.isSubagent ? h(Badge, { tone: 'warn' }, t('subagent')) : null,
|
|
1050
1173
|
),
|
|
1051
1174
|
),
|
|
@@ -1067,6 +1190,16 @@ window.__ModuleLoader__.load({
|
|
|
1067
1190
|
{ className: 'dsm-meta' },
|
|
1068
1191
|
h('dt', null, t('workspace')),
|
|
1069
1192
|
h('dd', null, row.workspaceTitle || t('workspaceUngrouped')),
|
|
1193
|
+
row.kind === 'fork'
|
|
1194
|
+
? h(React.Fragment, null,
|
|
1195
|
+
h('dt', null, t('parentSession')),
|
|
1196
|
+
h('dd', null,
|
|
1197
|
+
row.parentTitle !== ''
|
|
1198
|
+
? row.parentTitle
|
|
1199
|
+
: row.parentPresent === false
|
|
1200
|
+
? t('parentMissing')
|
|
1201
|
+
: (row.parentSessionId || '')))
|
|
1202
|
+
: null,
|
|
1070
1203
|
h('dt', null, t('path')),
|
|
1071
1204
|
h('dd', { title: row.cwd }, h('span', { className: 'dsm-mono' }, row.cwd || t('noCwd'))),
|
|
1072
1205
|
h('dt', null, t('id')),
|
|
@@ -1096,9 +1229,12 @@ window.__ModuleLoader__.load({
|
|
|
1096
1229
|
h('h3', null, t('moveTo')),
|
|
1097
1230
|
h('p', { className: 'dsm-hint', style: { margin: '0 0 8px' } }, t('moveHint')),
|
|
1098
1231
|
row.archived ? h('p', { className: 'dsm-hint', style: { margin: '0 0 8px' } }, t('moveArchivedHint')) : null,
|
|
1232
|
+
movable && (row.forkCount ?? 0) > 0
|
|
1233
|
+
? h('p', { className: 'dsm-hint', style: { margin: '0 0 8px' } }, t('moveForkChildrenHint', { n: row.forkCount }))
|
|
1234
|
+
: null,
|
|
1099
1235
|
!movable
|
|
1100
1236
|
? h('p', { className: 'dsm-hint' },
|
|
1101
|
-
row.live ? t('moveLiveHint') : row.isSubagent ? t('moveSubagentHint') : row.can?.move === false ? t('moveDescendantsHint') : t('moveNoTarget'))
|
|
1237
|
+
row.live ? t('moveLiveHint') : row.kind === 'fork' ? t('moveForkHint') : row.isSubagent ? t('moveSubagentHint') : row.can?.move === false ? t('moveDescendantsHint') : t('moveNoTarget'))
|
|
1102
1238
|
: h(
|
|
1103
1239
|
'div',
|
|
1104
1240
|
{ className: 'dsm-title-row' },
|
|
@@ -1149,11 +1285,43 @@ window.__ModuleLoader__.load({
|
|
|
1149
1285
|
// Page
|
|
1150
1286
|
// ---------------------------------------------------------------------
|
|
1151
1287
|
|
|
1288
|
+
/**
|
|
1289
|
+
* Order one group's rows so a branch sits directly under its parent, the
|
|
1290
|
+
* way the sidebar nests it. A branch whose parent is filtered out of this
|
|
1291
|
+
* group stays at the top level and keeps its "from parent" meta line.
|
|
1292
|
+
*/
|
|
1293
|
+
function orderBranchRows(rows) {
|
|
1294
|
+
const ids = new Set(rows.map((row) => row.id))
|
|
1295
|
+
const childrenOf = new Map()
|
|
1296
|
+
const tops = []
|
|
1297
|
+
for (const row of rows) {
|
|
1298
|
+
const parentId = row.kind === 'fork' ? row.parentSessionId : null
|
|
1299
|
+
if (parentId !== null && parentId !== row.id && ids.has(parentId)) {
|
|
1300
|
+
const list = childrenOf.get(parentId) ?? []
|
|
1301
|
+
list.push(row)
|
|
1302
|
+
childrenOf.set(parentId, list)
|
|
1303
|
+
} else {
|
|
1304
|
+
tops.push(row)
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
const out = []
|
|
1308
|
+
const visited = new Set()
|
|
1309
|
+
const visit = (row, depth) => {
|
|
1310
|
+
if (visited.has(row.id)) return
|
|
1311
|
+
visited.add(row.id)
|
|
1312
|
+
out.push(depth === 0 ? row : { ...row, branchDepth: depth })
|
|
1313
|
+
for (const child of childrenOf.get(row.id) ?? []) visit(child, depth + 1)
|
|
1314
|
+
}
|
|
1315
|
+
for (const row of tops) visit(row, 0)
|
|
1316
|
+
// A corrupt parent cycle must not silently drop rows from the list.
|
|
1317
|
+
return out.length === rows.length ? out : rows
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1152
1320
|
/**
|
|
1153
1321
|
* Derive the filtered / grouped / rendered view for the list.
|
|
1154
1322
|
* Pure so the selection-pruning effect can use it before the loading guard.
|
|
1155
1323
|
*/
|
|
1156
|
-
function computeView({ state, tab, workspaceFilter, query, sort, expanded, defaultExpanded, limits }) {
|
|
1324
|
+
function computeView({ state, tab, workspaceFilter, query, sort, expanded, defaultExpanded, limits, showBlank }) {
|
|
1157
1325
|
const rows = state?.sessions ?? []
|
|
1158
1326
|
const workspaceOrder = state?.workspaces ?? []
|
|
1159
1327
|
const text = query.trim().toLowerCase()
|
|
@@ -1163,8 +1331,9 @@ window.__ModuleLoader__.load({
|
|
|
1163
1331
|
if (tab === 'archived' && !row.archived) return false
|
|
1164
1332
|
if (workspaceFilter === 'ungrouped' && row.workspaceId !== null) return false
|
|
1165
1333
|
if (workspaceFilter !== 'all' && workspaceFilter !== 'ungrouped' && row.workspaceId !== workspaceFilter) return false
|
|
1334
|
+
if (showBlank !== true && row.blank === true) return false
|
|
1166
1335
|
if (filtering) {
|
|
1167
|
-
const haystack = `${row.title}\n${row.cwd}\n${row.id}`.toLowerCase()
|
|
1336
|
+
const haystack = `${row.title}\n${row.cwd}\n${row.id}\n${row.parentTitle ?? ''}`.toLowerCase()
|
|
1168
1337
|
if (!haystack.includes(text)) return false
|
|
1169
1338
|
}
|
|
1170
1339
|
return true
|
|
@@ -1177,7 +1346,7 @@ window.__ModuleLoader__.load({
|
|
|
1177
1346
|
})
|
|
1178
1347
|
const baseGroups = []
|
|
1179
1348
|
for (const workspace of workspaceOrder) {
|
|
1180
|
-
const groupRows = sorted.filter((row) => row.workspaceId === workspace.id)
|
|
1349
|
+
const groupRows = orderBranchRows(sorted.filter((row) => row.workspaceId === workspace.id))
|
|
1181
1350
|
if (groupRows.length === 0) continue
|
|
1182
1351
|
baseGroups.push({
|
|
1183
1352
|
...workspace,
|
|
@@ -1185,7 +1354,7 @@ window.__ModuleLoader__.load({
|
|
|
1185
1354
|
bytes: groupRows.reduce((sum, row) => sum + row.sizeBytes, 0),
|
|
1186
1355
|
})
|
|
1187
1356
|
}
|
|
1188
|
-
const ungroupedRows = sorted.filter((row) => row.workspaceId === null)
|
|
1357
|
+
const ungroupedRows = orderBranchRows(sorted.filter((row) => row.workspaceId === null))
|
|
1189
1358
|
if (ungroupedRows.length > 0) {
|
|
1190
1359
|
baseGroups.push({
|
|
1191
1360
|
id: 'ungrouped',
|
|
@@ -1233,6 +1402,7 @@ window.__ModuleLoader__.load({
|
|
|
1233
1402
|
const [bulkTarget, setBulkTarget] = React.useState('')
|
|
1234
1403
|
const [expanded, setExpanded] = React.useState(null)
|
|
1235
1404
|
const [limits, setLimits] = React.useState({})
|
|
1405
|
+
const [showBlank, setShowBlank] = React.useState(false)
|
|
1236
1406
|
const abortRef = React.useRef(null)
|
|
1237
1407
|
const searchRef = React.useRef(null)
|
|
1238
1408
|
const listScroll = React.useRef(0)
|
|
@@ -1298,6 +1468,7 @@ window.__ModuleLoader__.load({
|
|
|
1298
1468
|
if (selectedId === null) return undefined
|
|
1299
1469
|
const onKeyDown = (event) => {
|
|
1300
1470
|
if (event.key === 'Escape') {
|
|
1471
|
+
if (document.querySelector('.dsm-scope-overlay') !== null) return
|
|
1301
1472
|
event.stopPropagation()
|
|
1302
1473
|
backToList()
|
|
1303
1474
|
}
|
|
@@ -1308,13 +1479,13 @@ window.__ModuleLoader__.load({
|
|
|
1308
1479
|
|
|
1309
1480
|
React.useEffect(() => {
|
|
1310
1481
|
setLimits({})
|
|
1311
|
-
}, [tab, workspaceFilter, query, includeSubagents])
|
|
1482
|
+
}, [tab, workspaceFilter, query, includeSubagents, showBlank])
|
|
1312
1483
|
|
|
1313
1484
|
const rows = state?.sessions ?? []
|
|
1314
1485
|
const defaultExpanded = (state?.totals?.all ?? 0) <= DEFAULT_COLLAPSE_AFTER
|
|
1315
1486
|
const view = React.useMemo(
|
|
1316
|
-
() => computeView({ state, tab, workspaceFilter, query, sort, expanded, defaultExpanded, limits }),
|
|
1317
|
-
[state, tab, workspaceFilter, query, sort, expanded, defaultExpanded, limits],
|
|
1487
|
+
() => computeView({ state, tab, workspaceFilter, query, sort, expanded, defaultExpanded, limits, showBlank }),
|
|
1488
|
+
[state, tab, workspaceFilter, query, sort, expanded, defaultExpanded, limits, showBlank],
|
|
1318
1489
|
)
|
|
1319
1490
|
const viewKey = React.useMemo(
|
|
1320
1491
|
() => [
|
|
@@ -1323,10 +1494,11 @@ window.__ModuleLoader__.load({
|
|
|
1323
1494
|
workspaceFilter,
|
|
1324
1495
|
query,
|
|
1325
1496
|
includeSubagents ? 1 : 0,
|
|
1497
|
+
showBlank ? 1 : 0,
|
|
1326
1498
|
defaultExpanded ? 1 : 0,
|
|
1327
1499
|
expanded === null ? 'auto' : [...expanded].sort().join(','),
|
|
1328
1500
|
].join('|'),
|
|
1329
|
-
[state?.generatedAt, tab, workspaceFilter, query, includeSubagents, defaultExpanded, expanded],
|
|
1501
|
+
[state?.generatedAt, tab, workspaceFilter, query, includeSubagents, showBlank, defaultExpanded, expanded],
|
|
1330
1502
|
)
|
|
1331
1503
|
React.useEffect(() => {
|
|
1332
1504
|
setSelected((previous) => {
|
|
@@ -1455,10 +1627,14 @@ window.__ModuleLoader__.load({
|
|
|
1455
1627
|
return h('div', { className: 'dsm-page' }, h('p', { className: 'dsm-hint' }, h(Spinner, { t, label: t('loading') })))
|
|
1456
1628
|
}
|
|
1457
1629
|
|
|
1630
|
+
const rowsForTabs = rows.filter((row) => showBlank || row.blank !== true)
|
|
1631
|
+
const blankRows = rows.filter((row) => row.blank === true)
|
|
1632
|
+
const blankArchivedRows = blankRows.filter((row) => row.archived === true && row.can?.delete === true)
|
|
1633
|
+
const blankActiveCount = blankRows.filter((row) => row.archived !== true || row.can?.delete !== true).length
|
|
1458
1634
|
const tabs = [
|
|
1459
|
-
{ key: 'all', label: t('tabAll'), count:
|
|
1460
|
-
{ key: 'active', label: t('tabActive'), count:
|
|
1461
|
-
{ key: 'archived', label: t('tabArchived'), count:
|
|
1635
|
+
{ key: 'all', label: t('tabAll'), count: rowsForTabs.length },
|
|
1636
|
+
{ key: 'active', label: t('tabActive'), count: rowsForTabs.filter((row) => !row.archived).length },
|
|
1637
|
+
{ key: 'archived', label: t('tabArchived'), count: rowsForTabs.filter((row) => row.archived).length },
|
|
1462
1638
|
]
|
|
1463
1639
|
const visibleAllSelected = view.visibleRows.length > 0 && view.visibleRows.every((row) => selected.has(row.id))
|
|
1464
1640
|
const selectedRows = view.visibleRows.filter((row) => selected.has(row.id))
|
|
@@ -1564,9 +1740,27 @@ window.__ModuleLoader__.load({
|
|
|
1564
1740
|
archived: state.totals.archived,
|
|
1565
1741
|
size: formatBytes(state.totals.bytes),
|
|
1566
1742
|
}),
|
|
1743
|
+
blankRows.length > 0 && !showBlank
|
|
1744
|
+
? ` · ${t('hiddenBlankCount', { n: blankRows.length })}`
|
|
1745
|
+
: '',
|
|
1746
|
+
(state.totals?.hiddenSubagents ?? 0) > 0 && !includeSubagents
|
|
1747
|
+
? ` · ${t('hiddenSubagentCount', { n: state.totals.hiddenSubagents })}`
|
|
1748
|
+
: '',
|
|
1567
1749
|
),
|
|
1750
|
+
blankActiveCount > 0 && showBlank
|
|
1751
|
+
? h('p', { className: 'dsm-hint' }, t('blankActiveHint', { n: blankActiveCount }))
|
|
1752
|
+
: null,
|
|
1568
1753
|
),
|
|
1569
1754
|
h('div', { className: 'dsm-head-actions' },
|
|
1755
|
+
blankArchivedRows.length > 0
|
|
1756
|
+
? h('button', {
|
|
1757
|
+
type: 'button',
|
|
1758
|
+
className: 'dsm-btn',
|
|
1759
|
+
disabled: busy,
|
|
1760
|
+
title: `${t('cleanupBlankHint')}${blankActiveCount > 0 ? ` ${t('blankActiveHint', { n: blankActiveCount })}` : ''}`,
|
|
1761
|
+
onClick: () => setBulkDeleteIds(blankArchivedRows.map((row) => row.id)),
|
|
1762
|
+
}, t('cleanupBlank', { n: blankArchivedRows.length }))
|
|
1763
|
+
: null,
|
|
1570
1764
|
h('button', { type: 'button', className: 'dsm-btn', disabled: busy, onClick: () => void load({ silent: true }) },
|
|
1571
1765
|
busy ? h(Spinner, { t, label: t('working') }) : t('refresh')),
|
|
1572
1766
|
),
|
|
@@ -1631,7 +1825,7 @@ window.__ModuleLoader__.load({
|
|
|
1631
1825
|
h('option', { value: 'size' }, t('sortSize')),
|
|
1632
1826
|
h('option', { value: 'title' }, t('sortTitle')),
|
|
1633
1827
|
),
|
|
1634
|
-
h('label', { className: 'dsm-check' },
|
|
1828
|
+
h('label', { className: 'dsm-check', title: t('showSubagentsHint') },
|
|
1635
1829
|
h('input', {
|
|
1636
1830
|
type: 'checkbox',
|
|
1637
1831
|
checked: includeSubagents,
|
|
@@ -1639,6 +1833,14 @@ window.__ModuleLoader__.load({
|
|
|
1639
1833
|
}),
|
|
1640
1834
|
t('showSubagents'),
|
|
1641
1835
|
),
|
|
1836
|
+
h('label', { className: 'dsm-check' },
|
|
1837
|
+
h('input', {
|
|
1838
|
+
type: 'checkbox',
|
|
1839
|
+
checked: showBlank,
|
|
1840
|
+
onChange: (event) => setShowBlank(event.target.checked),
|
|
1841
|
+
}),
|
|
1842
|
+
t('showBlank'),
|
|
1843
|
+
),
|
|
1642
1844
|
),
|
|
1643
1845
|
view.visibleRows.length > 0
|
|
1644
1846
|
? h(
|
|
@@ -1699,7 +1901,11 @@ window.__ModuleLoader__.load({
|
|
|
1699
1901
|
}, t('clearSelection')),
|
|
1700
1902
|
!selectedMovable && selectedRows.length > 0
|
|
1701
1903
|
? h('span', { className: 'dsm-hint' },
|
|
1702
|
-
selectedRows.some((row) => row.live || row.running)
|
|
1904
|
+
selectedRows.some((row) => row.live || row.running)
|
|
1905
|
+
? t('liveHint')
|
|
1906
|
+
: selectedRows.some((row) => row.kind === 'fork')
|
|
1907
|
+
? t('moveForkHint')
|
|
1908
|
+
: t('moveUnavailableHint'))
|
|
1703
1909
|
: null,
|
|
1704
1910
|
)
|
|
1705
1911
|
: null,
|