dsh-better-workspace 0.8.0 → 0.9.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 +2 -2
- package/README_EN.md +2 -2
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/client.js +138 -86
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ web/ ← 虚拟分组(不是真实目录,纯命名层级)
|
|
|
52
52
|
<table>
|
|
53
53
|
<tr>
|
|
54
54
|
<td align="center" width="58%"><img src="docs/screenshots/4-settings-card.png" alt="插件设置卡片"/></td>
|
|
55
|
-
<td valign="top"><b>设置卡片</b><br/>设置 → 插件 → 插件配置 里新增「更好的工作区」卡片(官方折叠卡片样式),提供<b>单链分组合并</b>开关(如 测试层1 下只有 AI交易 一个工作区时,合并显示为一行 测试层1/AI交易)与<b
|
|
55
|
+
<td valign="top"><b>设置卡片</b><br/>设置 → 插件 → 插件配置 里新增「更好的工作区」卡片(官方折叠卡片样式),提供<b>单链分组合并</b>开关(如 测试层1 下只有 AI交易 一个工作区时,合并显示为一行 测试层1/AI交易)与<b>状态呼吸灯</b>开关(被折叠藏起的状态灯沿层级向外冒泡呼吸发光,默认开);展开状态与外观自定义经 dsh 客户端 store 持久化在当前浏览器。</td>
|
|
56
56
|
</tr>
|
|
57
57
|
</table>
|
|
58
58
|
|
|
@@ -60,7 +60,7 @@ web/ ← 虚拟分组(不是真实目录,纯命名层级)
|
|
|
60
60
|
|
|
61
61
|
- **文件夹式收起**:点工作区行 = 收起/展开该工作区的全部会话(收起后行上保留会话数角标);会话分组同样可收起;搜索时自动全部展开。
|
|
62
62
|
- **拖拽还原原版能力**:工作区行可拖拽——拖到另一工作区行上/下半即调整顺序(写回宿主 registry 序),拖到分组行则移动进该分组;会话行可拖拽——同工作区拖到另一会话上/下调整顺序,拖到会话子分组即移动进该分组。显示顺序 = 宿主手动序,拖拽结果立即生效。拖拽工作区期间,单链合并显示的行(如 工作/A)临时展开回文件夹树——路径里的每一级都是可投放的分组行,松手后自动合并回去。
|
|
63
|
-
- **不丢原生能力**:会话行(打开/重命名/分叉/归档)、每工作区「新会话」(点 + 总是先展开该工作区,让新会话立刻可见)、当前会话高亮、官方状态灯(蓝色运行环 / 琥珀待交互 / 绿色完成提醒)
|
|
63
|
+
- **不丢原生能力**:会话行(打开/重命名/分叉/归档)、每工作区「新会话」(点 + 总是先展开该工作区,让新会话立刻可见)、当前会话高亮、官方状态灯(蓝色运行环 / 琥珀待交互 / 绿色完成提醒)、**状态呼吸灯**(折叠藏起的状态灯向外冒泡:工作区/分组行图标按状态色呼吸发光——琥珀待交互/绿完成,自定义过发光的标题一起呼吸;运行中只在会话行本层蓝色呼吸,不外渗;可在设置关闭)、**活跃定时任务角标**(有活动 Schedule 的会话显示闹钟图标,与官方行一致)、未分组会话兜底、中英双语跟随界面语言。
|
|
64
64
|
- **URL 标题感知**:会话/工作区标题里的 URL(如 `https://host/api/v1/x`)按 URL 语义分层——域名与协议合成一段、路径各段自成层级,同前缀 URL 自动聚到同一分组下,不再被撕成伪分组。
|
|
65
65
|
- **对话空态页同样生效**:会话空态页的「添加工作区」菜单走的也是本插件的拾取交互,同样带所属分组弹窗。
|
|
66
66
|
|
package/README_EN.md
CHANGED
|
@@ -50,7 +50,7 @@ Session titles nest on `/` too (e.g. `test1/plugin maintenance`). Session groups
|
|
|
50
50
|
<table>
|
|
51
51
|
<tr>
|
|
52
52
|
<td align="center" width="58%"><img src="docs/screenshots/4-settings-card.png" alt="Settings card"/></td>
|
|
53
|
-
<td valign="top"><b>Settings card</b><br/>Settings → Plugins → Configuration adds a “Better Workspaces” card (official accordion style) with a <b>single-child-chain merge</b> toggle (e.g. level1 containing only AI trade renders as one row level1/AI trade); expansion state and styling persist via the dsh client store in the browser.</td>
|
|
53
|
+
<td valign="top"><b>Settings card</b><br/>Settings → Plugins → Configuration adds a “Better Workspaces” card (official accordion style) with a <b>single-child-chain merge</b> toggle (e.g. level1 containing only AI trade renders as one row level1/AI trade) and a <b>status breathing light</b> toggle (status dots hidden by collapse bubble outward as breathing glows; on by default); expansion state and styling persist via the dsh client store in the browser.</td>
|
|
54
54
|
</tr>
|
|
55
55
|
</table>
|
|
56
56
|
|
|
@@ -58,7 +58,7 @@ Session titles nest on `/` too (e.g. `test1/plugin maintenance`). Session groups
|
|
|
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
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 (the + button always expands the workspace first so the new session is immediately visible), current-session highlight, official status dots (blue running ring / amber pending / green done), **
|
|
61
|
+
- **Native capabilities kept**: open/rename/fork/archive sessions, per-workspace new-session (the + button always expands the workspace first so the new session is immediately visible), current-session highlight, official status dots (blue running ring / amber pending / green done), **status breathing lights** (dots hidden by collapse bubble outward: workspace/folder rows breathe on their icon in the status color — amber pending / green done, custom-glow labels breathe along; running breathes blue only on the session row itself and never relays; off-able in settings), **active-schedule badge** (alarm clock icon for sessions with active Schedule records, matching the official rows), ungrouped-session fallback, zh/en localization.
|
|
62
62
|
- **URL-aware titles**: URLs in session/workspace titles (e.g. `https://host/api/v1/x`) split the way URLs mean it — scheme://host as one segment, each path segment a level — so same-prefix URLs nest together instead of shredding into pseudo-folders.
|
|
63
63
|
- **Conversation hero too**: the empty-state “Add workspace” menu uses the same picking interaction with the same group popup.
|
|
64
64
|
|
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.9.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.9.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
|
@@ -47,7 +47,6 @@ window.__ModuleLoader__.load({
|
|
|
47
47
|
'time.years': '{n} 年',
|
|
48
48
|
'status.running': '生成中',
|
|
49
49
|
'status.completed': '已完成',
|
|
50
|
-
'toast.completed': '会话已完成',
|
|
51
50
|
'status.approval': '等待批准',
|
|
52
51
|
'status.planReview': '等待计划确认',
|
|
53
52
|
'status.question': '等待回答',
|
|
@@ -68,8 +67,8 @@ window.__ModuleLoader__.load({
|
|
|
68
67
|
'settings.collapse': '收起',
|
|
69
68
|
'settings.compactChains': '单链分组折叠显示',
|
|
70
69
|
'settings.compactChains.hint': '单层链合并为一行,出现多个子级时自动展开为树状;拖拽工作区期间单链临时展开回文件夹树,可放入任意一级;展开状态与自定义外观保存在当前浏览器。',
|
|
71
|
-
'settings.
|
|
72
|
-
'settings.
|
|
70
|
+
'settings.statusPulse': '状态呼吸灯',
|
|
71
|
+
'settings.statusPulse.hint': '被折叠藏起的状态灯(完成绿 / 运行蓝 / 待交互琥珀)沿层级向外冒泡:工作区与分组行以图标呼吸发光(颜色随状态,自定义过发光的标题一起呼吸),会话分组行显示呼吸状态灯;默认开启,可在此关闭。',
|
|
73
72
|
'custom.title': '自定义外观',
|
|
74
73
|
'custom.color': '颜色',
|
|
75
74
|
'custom.glow': '发光',
|
|
@@ -137,7 +136,6 @@ window.__ModuleLoader__.load({
|
|
|
137
136
|
'time.years': '{n}y',
|
|
138
137
|
'status.running': 'Running',
|
|
139
138
|
'status.completed': 'Completed',
|
|
140
|
-
'toast.completed': 'Session completed',
|
|
141
139
|
'status.approval': 'Waiting for approval',
|
|
142
140
|
'status.planReview': 'Waiting for plan review',
|
|
143
141
|
'status.question': 'Waiting for answer',
|
|
@@ -158,8 +156,8 @@ window.__ModuleLoader__.load({
|
|
|
158
156
|
'settings.collapse': 'Collapse',
|
|
159
157
|
'settings.compactChains': 'Merge single-child chains',
|
|
160
158
|
'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.',
|
|
161
|
-
'settings.
|
|
162
|
-
'settings.
|
|
159
|
+
'settings.statusPulse': 'Status breathing light',
|
|
160
|
+
'settings.statusPulse.hint': 'Status dots hidden by collapse (done green / running blue / pending amber) bubble outward: workspace and folder rows breathe on their icon in the status color (custom-glow labels breathe along), session-group rows show a breathing dot; on by default, turn it off here.',
|
|
163
161
|
'custom.title': 'Customize',
|
|
164
162
|
'custom.color': 'Color',
|
|
165
163
|
'custom.glow': 'Glow',
|
|
@@ -615,11 +613,10 @@ window.__ModuleLoader__.load({
|
|
|
615
613
|
'.bw-preview-icon{flex:none;display:grid;place-items:center;width:20px;height:20px;color:var(--dsw-alias-label-primary,#e6e6e6)}',
|
|
616
614
|
'.bw-preview-icon svg{width:18px;height:18px}',
|
|
617
615
|
'.bw-preview-label{font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:18px}',
|
|
618
|
-
'.bw-
|
|
619
|
-
'
|
|
620
|
-
'
|
|
621
|
-
'
|
|
622
|
-
'.bw-toast-kind{flex:none;font-size:11px;color:var(--dsw-alias-label-tertiary,#9a9a9a)}',
|
|
616
|
+
'.bw-pulse{animation:bw-breathe 1.8s ease-in-out infinite}',
|
|
617
|
+
'@keyframes bw-breathe{0%,100%{filter:drop-shadow(0 0 1px var(--bw-pulse-color));opacity:.55}50%{filter:drop-shadow(0 0 6px var(--bw-pulse-color));opacity:1}}',
|
|
618
|
+
'.bw-pulse-text{animation:bw-breathe-text 1.8s ease-in-out infinite}',
|
|
619
|
+
'@keyframes bw-breathe-text{0%,100%{text-shadow:0 0 1px var(--bw-pulse-color);opacity:.65}50%{text-shadow:0 0 7px var(--bw-pulse-color);opacity:1}}',
|
|
623
620
|
].join('')
|
|
624
621
|
|
|
625
622
|
const StyleNode = () => E('style', null, CSS_TEXT)
|
|
@@ -868,11 +865,17 @@ window.__ModuleLoader__.load({
|
|
|
868
865
|
.map(n => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, '0'))
|
|
869
866
|
.join('')
|
|
870
867
|
|
|
871
|
-
function FolderRow({ node, depth, expanded, onToggle, onContextMenu, dropInto, dragEvents, custStyle, iconMode, t }) {
|
|
868
|
+
function FolderRow({ node, depth, expanded, onToggle, onContextMenu, dropInto, dragEvents, custStyle, iconMode, pulse, t }) {
|
|
872
869
|
const total = countWorkspaces(node)
|
|
870
|
+
const iconEl = iconOf(iconMode, expanded)
|
|
871
|
+
// Hidden status dots breathe on the icon; with the icon hidden (mode
|
|
872
|
+
// "none" or a missing primitive) the official StateDot stands in.
|
|
873
|
+
const iconChild = pulse
|
|
874
|
+
? E(PulseGlow, { state: pulse }, iconEl || (typeof ui.StateDot === 'function' ? E(ui.StateDot, { state: pulse, size: 10 }) : null))
|
|
875
|
+
: iconEl
|
|
873
876
|
return E('div', {
|
|
874
877
|
className: cls('bw-row', dropInto && 'bw-drop-into'),
|
|
875
|
-
style: { paddingLeft: 4 + depth * 12, ...(custStyle || {}) },
|
|
878
|
+
style: { paddingLeft: 4 + depth * 12, ...(custStyle || {}), ...(pulse ? { '--bw-pulse-color': PULSE_COLORS[pulse] || PULSE_COLORS.ongoing } : null) },
|
|
876
879
|
onClick: onToggle,
|
|
877
880
|
onContextMenu: onContextMenu,
|
|
878
881
|
role: 'treeitem',
|
|
@@ -880,16 +883,21 @@ window.__ModuleLoader__.load({
|
|
|
880
883
|
...(dragEvents || {}),
|
|
881
884
|
},
|
|
882
885
|
E('span', { className: cls('bw-chevron', expanded && 'bw-chevron-open') }, icon('IconTriangleRightFill14', 14)),
|
|
883
|
-
E('span', { className: 'bw-row-icon' },
|
|
884
|
-
|
|
886
|
+
E('span', { className: 'bw-row-icon' }, iconChild),
|
|
887
|
+
// A row that already carries a custom label glow breathes in sync.
|
|
888
|
+
E('span', { className: cls('bw-row-label', pulse && custStyle && custStyle.textShadow && 'bw-pulse-text') }, node.name),
|
|
885
889
|
total > 0 ? E('span', { className: 'bw-row-count' }, String(total)) : null,
|
|
886
890
|
)
|
|
887
891
|
}
|
|
888
892
|
|
|
889
|
-
function WorkspaceRow({ workspace, depth, count, sessionsOpen, onToggle, onStart, onContextMenu, currentInside, dropHalf, dragEvents, custStyle, iconMode, t }) {
|
|
893
|
+
function WorkspaceRow({ workspace, depth, count, sessionsOpen, onToggle, onStart, onContextMenu, currentInside, dropHalf, dragEvents, custStyle, iconMode, pulse, t }) {
|
|
894
|
+
const iconEl = iconOf(iconMode, sessionsOpen)
|
|
895
|
+
const iconChild = pulse
|
|
896
|
+
? E(PulseGlow, { state: pulse }, iconEl || (typeof ui.StateDot === 'function' ? E(ui.StateDot, { state: pulse, size: 10 }) : null))
|
|
897
|
+
: iconEl
|
|
890
898
|
return E('div', {
|
|
891
899
|
className: cls('bw-row', currentInside && 'bw-row-current', dropHalf === 'before' && 'bw-drop-before', dropHalf === 'after' && 'bw-drop-after'),
|
|
892
|
-
style: { paddingLeft: 6 + depth * 12, ...(custStyle || {}) },
|
|
900
|
+
style: { paddingLeft: 6 + depth * 12, ...(custStyle || {}), ...(pulse ? { '--bw-pulse-color': PULSE_COLORS[pulse] || PULSE_COLORS.ongoing } : null) },
|
|
893
901
|
onClick: onToggle,
|
|
894
902
|
onContextMenu: onContextMenu,
|
|
895
903
|
role: 'treeitem',
|
|
@@ -897,8 +905,8 @@ window.__ModuleLoader__.load({
|
|
|
897
905
|
...(dragEvents || {}),
|
|
898
906
|
},
|
|
899
907
|
E('span', { className: cls('bw-chevron', sessionsOpen && 'bw-chevron-open') }, icon('IconTriangleRightFill14', 14)),
|
|
900
|
-
E('span', { className: 'bw-row-icon' },
|
|
901
|
-
E('span', { className: 'bw-row-label', title: workspace.title || workspace.leaf }, workspace.leaf),
|
|
908
|
+
E('span', { className: 'bw-row-icon' }, iconChild),
|
|
909
|
+
E('span', { className: cls('bw-row-label', pulse && custStyle && custStyle.textShadow && 'bw-pulse-text'), title: workspace.title || workspace.leaf }, workspace.leaf),
|
|
902
910
|
count > 0 ? E('span', { className: 'bw-row-count' }, String(count)) : null,
|
|
903
911
|
E('span', { className: 'bw-row-actions', onClick: (e) => e.stopPropagation() },
|
|
904
912
|
E('button', { type: 'button', className: 'bw-icon-btn', 'aria-label': t('session.new'), onClick: (e) => { e.stopPropagation(); onStart() } }, icon('IconPlusOutline16')),
|
|
@@ -906,16 +914,53 @@ window.__ModuleLoader__.load({
|
|
|
906
914
|
)
|
|
907
915
|
}
|
|
908
916
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
if (
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
917
|
+
// Official status priority: pending interaction > running > running
|
|
918
|
+
// subagents > completed reminder; idle rows show no dot at all.
|
|
919
|
+
const sessionStateOf = (row) => {
|
|
920
|
+
if (!row) return null
|
|
921
|
+
if (row.pending === 'approval' || row.pending === 'plan-review' || row.pending === 'question') return 'warning'
|
|
922
|
+
if (row.running || row.subagents > 0) return 'ongoing'
|
|
923
|
+
if (row.completed) return 'done'
|
|
924
|
+
return null
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Status breathing light (preference-controlled, default ON): status dots
|
|
929
|
+
* hidden by a collapse bubble outward to the nearest visible container
|
|
930
|
+
* row. Workspace/folder rows breathe on their icon (drop-shadow glow in
|
|
931
|
+
* the status color; a hidden/no icon falls back to the official StateDot);
|
|
932
|
+
* session-group rows carry a breathing StateDot in front of the label.
|
|
933
|
+
* Colors mirror the official dots: warning amber > ongoing blue > done
|
|
934
|
+
* green, so the glow always matches the lamp it relays.
|
|
935
|
+
*/
|
|
936
|
+
const PULSE_RANK = { warning: 3, ongoing: 2, done: 1 }
|
|
937
|
+
const PULSE_COLORS = { warning: '#d29922', ongoing: '#5b8def', done: '#3fb950' }
|
|
938
|
+
const pulseRank = (state) => (state ? (PULSE_RANK[state] || 0) : 0)
|
|
939
|
+
|
|
940
|
+
function PulseGlow({ state, children }) {
|
|
941
|
+
return E('span', {
|
|
942
|
+
className: 'bw-pulse',
|
|
943
|
+
style: { '--bw-pulse-color': PULSE_COLORS[state] || PULSE_COLORS.ongoing },
|
|
944
|
+
}, children)
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// RUNNING deliberately stays on the session row itself (its official ring
|
|
948
|
+
// breathes blue there) and does NOT bubble outward — the user found the
|
|
949
|
+
// ongoing relay noisy. Only pending-amber and done-green propagate.
|
|
950
|
+
const relayStateOf = (row) => {
|
|
951
|
+
const s = sessionStateOf(row)
|
|
952
|
+
return s === 'ongoing' ? null : s
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function SessionRow({ node, depth, current, onOpen, onContextMenu, now, dropHalf, dragEvents, custStyle, breathing, t }) {
|
|
956
|
+
const state = sessionStateOf(node)
|
|
957
|
+
const status = state === null ? null : {
|
|
958
|
+
state,
|
|
959
|
+
title: state === 'warning'
|
|
960
|
+
? t('status.' + (node.pending === 'plan-review' ? 'planReview' : node.pending))
|
|
961
|
+
: state === 'ongoing'
|
|
962
|
+
? (node.running ? t('status.running') : t('status.subagents', { n: node.subagents }))
|
|
963
|
+
: t('status.completed'),
|
|
919
964
|
}
|
|
920
965
|
return E('div', {
|
|
921
966
|
className: cls('bw-row', 'bw-session-row', current && 'bw-row-current', dropHalf === 'before' && 'bw-drop-before', dropHalf === 'after' && 'bw-drop-after'),
|
|
@@ -927,7 +972,11 @@ window.__ModuleLoader__.load({
|
|
|
927
972
|
},
|
|
928
973
|
E('span', { className: 'bw-row-icon', title: status ? status.title : undefined },
|
|
929
974
|
status && typeof ui.StateDot === 'function'
|
|
930
|
-
?
|
|
975
|
+
? (state === 'ongoing' && breathing
|
|
976
|
+
// Running breathes blue ON the session row only (never relays);
|
|
977
|
+
// the settings toggle covers this breathing too.
|
|
978
|
+
? E(PulseGlow, { state: 'ongoing' }, E(ui.StateDot, { state: status.state, size: 10 }))
|
|
979
|
+
: E(ui.StateDot, { state: status.state, size: 10 }))
|
|
931
980
|
: E('span', { className: 'bw-dot' }),
|
|
932
981
|
),
|
|
933
982
|
E('span', { className: 'bw-row-label' }, node.leaf || node.title),
|
|
@@ -943,7 +992,7 @@ window.__ModuleLoader__.load({
|
|
|
943
992
|
* titles). Deliberately NOT styled like a workspace folder — no folder
|
|
944
993
|
* icon, tertiary color — so a session level never reads as a workspace.
|
|
945
994
|
*/
|
|
946
|
-
function SessionGroupRow({ name, depth, expanded, count, onToggle, onContextMenu, dropInto, dragEvents, custStyle, t }) {
|
|
995
|
+
function SessionGroupRow({ name, depth, expanded, count, onToggle, onContextMenu, dropInto, dragEvents, custStyle, pulse, t }) {
|
|
947
996
|
return E('div', {
|
|
948
997
|
className: cls('bw-row', 'bw-sgroup-row', dropInto && 'bw-drop-into'),
|
|
949
998
|
style: { paddingLeft: 10 + depth * 12, ...(custStyle || {}) },
|
|
@@ -954,6 +1003,9 @@ window.__ModuleLoader__.load({
|
|
|
954
1003
|
...(dragEvents || {}),
|
|
955
1004
|
},
|
|
956
1005
|
E('span', { className: cls('bw-chevron', expanded && 'bw-chevron-open') }, icon('IconTriangleRightFill14', 14)),
|
|
1006
|
+
// Session groups have no icon slot: a collapsed group with hidden
|
|
1007
|
+
// status relays through a breathing official StateDot instead.
|
|
1008
|
+
pulse ? E(PulseGlow, { state: pulse }, typeof ui.StateDot === 'function' ? E(ui.StateDot, { state: pulse, size: 10 }) : null) : null,
|
|
957
1009
|
E('span', { className: 'bw-row-label' }, name),
|
|
958
1010
|
count > 0 ? E('span', { className: 'bw-row-count' }, String(count)) : null,
|
|
959
1011
|
)
|
|
@@ -1146,7 +1198,7 @@ window.__ModuleLoader__.load({
|
|
|
1146
1198
|
function BetterWorkspaceSettings({ useStore, actions, t }) {
|
|
1147
1199
|
const prefs = useStore ? (useStore(s => s.prefs) || {}) : {}
|
|
1148
1200
|
const compactChains = prefs.compactChains !== false
|
|
1149
|
-
const
|
|
1201
|
+
const statusPulse = prefs.statusPulse !== false
|
|
1150
1202
|
return E('div', { className: 'bw-settings' },
|
|
1151
1203
|
StyleNode(),
|
|
1152
1204
|
E('div', { className: 'bw-setting-row' },
|
|
@@ -1162,17 +1214,17 @@ window.__ModuleLoader__.load({
|
|
|
1162
1214
|
),
|
|
1163
1215
|
E('div', { className: 'bw-hint' }, t('settings.compactChains.hint')),
|
|
1164
1216
|
E('div', { className: 'bw-setting-row', style: { marginTop: 10 } },
|
|
1165
|
-
E('div', { className: 'bw-setting-label' }, t('settings.
|
|
1217
|
+
E('div', { className: 'bw-setting-label' }, t('settings.statusPulse')),
|
|
1166
1218
|
E('button', {
|
|
1167
1219
|
type: 'button',
|
|
1168
1220
|
role: 'switch',
|
|
1169
|
-
'aria-checked':
|
|
1170
|
-
'aria-label': t('settings.
|
|
1171
|
-
className: cls('bw-switch',
|
|
1172
|
-
onClick: () => { actions.setPref('
|
|
1221
|
+
'aria-checked': statusPulse,
|
|
1222
|
+
'aria-label': t('settings.statusPulse'),
|
|
1223
|
+
className: cls('bw-switch', statusPulse && 'bw-switch-on'),
|
|
1224
|
+
onClick: () => { actions.setPref('statusPulse', !statusPulse) },
|
|
1173
1225
|
}, E('span', { className: 'bw-switch-thumb' })),
|
|
1174
1226
|
),
|
|
1175
|
-
E('div', { className: 'bw-hint' }, t('settings.
|
|
1227
|
+
E('div', { className: 'bw-hint' }, t('settings.statusPulse.hint')),
|
|
1176
1228
|
)
|
|
1177
1229
|
}
|
|
1178
1230
|
|
|
@@ -1230,7 +1282,7 @@ window.__ModuleLoader__.load({
|
|
|
1230
1282
|
const prefsMap = useStore ? (useStore(s => s.prefs) || {}) : {}
|
|
1231
1283
|
const stylingMap = useStore ? (useStore(s => s.styling) || {}) : {}
|
|
1232
1284
|
const compactChains = prefsMap.compactChains !== false
|
|
1233
|
-
const
|
|
1285
|
+
const statusPulse = prefsMap.statusPulse !== false
|
|
1234
1286
|
const archivedSet = React.useMemo(() => new Set(archivedSessionIds), [archivedSessionIds])
|
|
1235
1287
|
const subCounts = React.useMemo(() => subagentRunningCounts(list ? list.byId : {}), [list ? list.byId : null])
|
|
1236
1288
|
|
|
@@ -1242,8 +1294,6 @@ window.__ModuleLoader__.load({
|
|
|
1242
1294
|
const [ctx, setCtx] = React.useState(null) // context menu { kind, payload, x, y }
|
|
1243
1295
|
const [customize, setCustomize] = React.useState(null) // { kind, entryKey, name }
|
|
1244
1296
|
const [errorText, setErrorText] = React.useState(null)
|
|
1245
|
-
const [toasts, setToasts] = React.useState([]) // completion relays: { key, title }
|
|
1246
|
-
const toastTimers = React.useRef([])
|
|
1247
1297
|
const [drag, setDrag] = React.useState(null) // { kind: 'workspace'|'session', source, over } | null
|
|
1248
1298
|
// Workspace drags arm their state one frame LATE (see workspaceDragEvents):
|
|
1249
1299
|
// arming synchronously re-renders during the dragstart dispatch, the chain
|
|
@@ -1251,43 +1301,9 @@ window.__ModuleLoader__.load({
|
|
|
1251
1301
|
// source element, and Chromium cancels the whole gesture. dragEnd clears
|
|
1252
1302
|
// the timer so a same-tick cancel never leaves a ghost drag behind.
|
|
1253
1303
|
const wsDragArmTimer = React.useRef(null)
|
|
1254
|
-
const completedSeenRef = React.useRef(null) // previous render's completed session ids
|
|
1255
|
-
// Declared BEFORE the completion effect: its dependency array evaluates
|
|
1256
|
-
// during render, so referencing a later const would be a TDZ crash.
|
|
1257
1304
|
const normalizedQuery = query.trim().toLowerCase()
|
|
1258
1305
|
const now = Date.now()
|
|
1259
1306
|
|
|
1260
|
-
// Completion relay (preference-controlled, default ON): when a session
|
|
1261
|
-
// flips to completed===true while its workspace row is COLLAPSED, the
|
|
1262
|
-
// official green dot is invisible, so a brief non-blocking toast pops in
|
|
1263
|
-
// the sidebar. Edge-triggered against the previous snapshot (no storm on
|
|
1264
|
-
// reload), skipped while searching (rows are force-expanded there), and
|
|
1265
|
-
// pointer-events:none keeps typing and clicks completely uninterrupted.
|
|
1266
|
-
React.useEffect(() => {
|
|
1267
|
-
const cur = new Set()
|
|
1268
|
-
if (list && list.byId) {
|
|
1269
|
-
for (const id of Object.keys(list.byId)) {
|
|
1270
|
-
const summary = list.byId[id]
|
|
1271
|
-
if (summary && summary.completed === true && sessionVisible(summary, list.current, archivedSet)) cur.add(id)
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
const prev = completedSeenRef.current
|
|
1275
|
-
completedSeenRef.current = cur
|
|
1276
|
-
if (prev === null || !completionToasts) return // first snapshot records only
|
|
1277
|
-
for (const id of cur) {
|
|
1278
|
-
if (prev.has(id)) continue
|
|
1279
|
-
const workspace = (items || []).find(w => (w.sessionIds || []).includes(id))
|
|
1280
|
-
if (!workspace) continue
|
|
1281
|
-
if (normalizedQuery === '' && sessionsOpenOf(workspace.workspaceId)) continue // green dot already visible
|
|
1282
|
-
const toast = { key: id + ':' + String(Date.now()), title: sessionTitleOf(list.byId[id], t) }
|
|
1283
|
-
setToasts(current => current.concat([toast]))
|
|
1284
|
-
toastTimers.current.push(setTimeout(() => {
|
|
1285
|
-
setToasts(current => current.filter(x => x.key !== toast.key))
|
|
1286
|
-
}, 5200))
|
|
1287
|
-
}
|
|
1288
|
-
}, [list, items, normalizedQuery !== '', completionToasts, sessionsExpandedMap, archivedSet])
|
|
1289
|
-
React.useEffect(() => () => { for (const timer of toastTimers.current) clearTimeout(timer) }, [])
|
|
1290
|
-
|
|
1291
1307
|
const fail = (text) => { setFlowOpen(false); setDialog(null); setErrorText(String(text || 'unknown error')) }
|
|
1292
1308
|
|
|
1293
1309
|
const styleEntry = (key) => stylingMap[key] || null
|
|
@@ -1402,6 +1418,45 @@ window.__ModuleLoader__.load({
|
|
|
1402
1418
|
const searched = normalizedQuery ? searchAgent(tree) : null
|
|
1403
1419
|
const searching = normalizedQuery !== ''
|
|
1404
1420
|
|
|
1421
|
+
/* --------------------- status breathing relay -------------------- */
|
|
1422
|
+
|
|
1423
|
+
// Highest-priority status across a whole session subtree (groups + rows).
|
|
1424
|
+
const nodePulseOf = (sessionNode) => {
|
|
1425
|
+
let best = null
|
|
1426
|
+
for (const group of sessionNode.groups || []) {
|
|
1427
|
+
const s = nodePulseOf(group)
|
|
1428
|
+
if (pulseRank(s) > pulseRank(best)) best = s
|
|
1429
|
+
}
|
|
1430
|
+
for (const row of sessionNode.sessions || []) {
|
|
1431
|
+
const s = relayStateOf(row)
|
|
1432
|
+
if (pulseRank(s) > pulseRank(best)) best = s
|
|
1433
|
+
}
|
|
1434
|
+
return best
|
|
1435
|
+
}
|
|
1436
|
+
// A COLLAPSED workspace row relays its whole session tree; an open row
|
|
1437
|
+
// shows the real dots (deeper collapsed groups relay on their own rows).
|
|
1438
|
+
const wsPulseOf = (workspace) => (!statusPulse || searching || sessionsOpenOf(workspace.workspaceId))
|
|
1439
|
+
? null
|
|
1440
|
+
: nodePulseOf(buildSessionTree(sessionsOf(workspace)))
|
|
1441
|
+
// A collapsed FOLDER hides everything below — including open workspaces —
|
|
1442
|
+
// so its aggregation ignores inner expansion states entirely.
|
|
1443
|
+
const wsAllPulseOf = (workspace) => (!statusPulse || searching)
|
|
1444
|
+
? null
|
|
1445
|
+
: nodePulseOf(buildSessionTree(sessionsOf(workspace)))
|
|
1446
|
+
const folderPulseOf = (node) => {
|
|
1447
|
+
if (node.kind === 'ws') return wsAllPulseOf(node.workspace)
|
|
1448
|
+
let best = null
|
|
1449
|
+
for (const child of node.folders) {
|
|
1450
|
+
const s = folderPulseOf(child)
|
|
1451
|
+
if (pulseRank(s) > pulseRank(best)) best = s
|
|
1452
|
+
}
|
|
1453
|
+
for (const w of node.workspaces) {
|
|
1454
|
+
const s = wsAllPulseOf(w)
|
|
1455
|
+
if (pulseRank(s) > pulseRank(best)) best = s
|
|
1456
|
+
}
|
|
1457
|
+
return best
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1405
1460
|
const folderExpanded = (path) => (expandedMap ? expandedMap[path] !== false : true)
|
|
1406
1461
|
const sessionsOpenOf = (workspaceId) => (sessionsExpandedMap ? sessionsExpandedMap[workspaceId] !== false : true)
|
|
1407
1462
|
const sessionGroupOpen = (key) => (sessionGroupsMap ? sessionGroupsMap[key] !== false : true)
|
|
@@ -1765,6 +1820,7 @@ window.__ModuleLoader__.load({
|
|
|
1765
1820
|
name: group.name,
|
|
1766
1821
|
depth,
|
|
1767
1822
|
expanded: open,
|
|
1823
|
+
pulse: (statusPulse && !open) ? nodePulseOf(group) : null,
|
|
1768
1824
|
count: countSessionTree(group),
|
|
1769
1825
|
onToggle: () => { if (!searching) actions.setSessionGroupExpanded(key, !open) },
|
|
1770
1826
|
onContextMenu: (e) => openCtx('sgroup', { workspaceId, path: group.path, name: group.name }, e),
|
|
@@ -1790,10 +1846,11 @@ window.__ModuleLoader__.load({
|
|
|
1790
1846
|
dropHalf: workspaceId ? sessDropHalf(session.id) : null,
|
|
1791
1847
|
dragEvents: workspaceId ? sessionDragEvents(session, workspaceId) : undefined,
|
|
1792
1848
|
custStyle: rowStyleOf('session:' + session.id),
|
|
1849
|
+
breathing: statusPulse,
|
|
1793
1850
|
t,
|
|
1794
1851
|
})
|
|
1795
1852
|
|
|
1796
|
-
const renderWorkspaceEntry = (entry, depth) => {
|
|
1853
|
+
const renderWorkspaceEntry = (entry, depth, pulse) => {
|
|
1797
1854
|
const { workspace } = entry
|
|
1798
1855
|
const count = countSessionTree(buildSessionTree(sessionsOf(workspace)))
|
|
1799
1856
|
const rows = [E(WorkspaceRow, {
|
|
@@ -1812,6 +1869,7 @@ window.__ModuleLoader__.load({
|
|
|
1812
1869
|
dragEvents: workspaceDragEvents(workspace),
|
|
1813
1870
|
custStyle: rowStyleOf('workspace:' + workspace.workspaceId),
|
|
1814
1871
|
iconMode: (styleEntry('workspace:' + workspace.workspaceId) || {}).icon || 'solid',
|
|
1872
|
+
pulse,
|
|
1815
1873
|
t,
|
|
1816
1874
|
})]
|
|
1817
1875
|
rows.push(...renderSessionTree(workspace, depth + 1))
|
|
@@ -1819,7 +1877,7 @@ window.__ModuleLoader__.load({
|
|
|
1819
1877
|
}
|
|
1820
1878
|
|
|
1821
1879
|
const renderPlainFolder = (node, depth) => {
|
|
1822
|
-
if (node.kind === 'ws') return renderWorkspaceEntry({ workspace: node.workspace }, depth)
|
|
1880
|
+
if (node.kind === 'ws') return renderWorkspaceEntry({ workspace: node.workspace }, depth, wsPulseOf(node.workspace))
|
|
1823
1881
|
const expanded = searching || folderExpanded(node.path)
|
|
1824
1882
|
const rows = [E(FolderRow, {
|
|
1825
1883
|
key: 'f-' + node.path,
|
|
@@ -1832,11 +1890,12 @@ window.__ModuleLoader__.load({
|
|
|
1832
1890
|
dragEvents: folderDropEvents(node.path),
|
|
1833
1891
|
custStyle: rowStyleOf('folder:' + node.path),
|
|
1834
1892
|
iconMode: (styleEntry('folder:' + node.path) || {}).icon || 'solid',
|
|
1893
|
+
pulse: expanded ? null : folderPulseOf(node),
|
|
1835
1894
|
t,
|
|
1836
1895
|
})]
|
|
1837
1896
|
if (expanded) {
|
|
1838
1897
|
for (const child of node.folders) rows.push(...renderPlainFolder(child, depth + 1))
|
|
1839
|
-
for (const workspace of node.workspaces) rows.push(...renderWorkspaceEntry({ workspace }, depth + 1))
|
|
1898
|
+
for (const workspace of node.workspaces) rows.push(...renderWorkspaceEntry({ workspace }, depth + 1, wsPulseOf(workspace)))
|
|
1840
1899
|
}
|
|
1841
1900
|
return rows
|
|
1842
1901
|
}
|
|
@@ -1869,7 +1928,7 @@ window.__ModuleLoader__.load({
|
|
|
1869
1928
|
}
|
|
1870
1929
|
} else {
|
|
1871
1930
|
for (const folder of tree.folders) bodyRows.push(...renderPlainFolder(folder, 0))
|
|
1872
|
-
for (const workspace of tree.workspaces) bodyRows.push(...renderWorkspaceEntry({ workspace }, 0))
|
|
1931
|
+
for (const workspace of tree.workspaces) bodyRows.push(...renderWorkspaceEntry({ workspace }, 0, wsPulseOf(workspace)))
|
|
1873
1932
|
if (ungrouped.length > 0) {
|
|
1874
1933
|
bodyRows.push(E('div', { key: 'ungrouped-label', className: 'bw-header-title', style: { padding: '10px 6px 2px' } }, t('group.ungrouped')))
|
|
1875
1934
|
for (const s of ungrouped) bodyRows.push(renderSessionRow(s, 0))
|
|
@@ -2093,13 +2152,6 @@ window.__ModuleLoader__.load({
|
|
|
2093
2152
|
onClose: () => setCustomize(null),
|
|
2094
2153
|
t,
|
|
2095
2154
|
}),
|
|
2096
|
-
toasts.length > 0 ? E('div', { className: 'bw-toasts', role: 'status', 'aria-live': 'polite' },
|
|
2097
|
-
toasts.map(x => E('div', { key: x.key, className: 'bw-toast' },
|
|
2098
|
-
typeof ui.StateDot === 'function' ? E(ui.StateDot, { state: 'done', size: 10 }) : null,
|
|
2099
|
-
E('span', { className: 'bw-toast-title', title: x.title }, x.title),
|
|
2100
|
-
E('span', { className: 'bw-toast-kind' }, t('toast.completed')),
|
|
2101
|
-
)),
|
|
2102
|
-
) : null,
|
|
2103
2155
|
E(ui.Modal, {
|
|
2104
2156
|
open: errorText !== null,
|
|
2105
2157
|
onClose: () => setErrorText(null),
|