dsh-better-workspace 0.7.2 → 0.8.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 +6 -5
- package/README_EN.md +5 -4
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/client.js +221 -75
package/README.md
CHANGED
|
@@ -23,9 +23,9 @@ web/ ← 虚拟分组(不是真实目录,纯命名层级)
|
|
|
23
23
|
|
|
24
24
|
点「添加工作区」并选完文件夹后,弹出一个小对话框,一行选择工作区所属分组——直接输入(如 `web`)、从下拉里选已有层级(datalist 原生补全),留空就是根分组。确认后插件创建工作区并把分组前缀写进名称,树上自然落位。
|
|
25
25
|
|
|
26
|
-
###
|
|
26
|
+
### 分组的增删
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
右键任意分组行即可:**新增子分组**(路径自动带上父分组前缀)、**新增子工作区**(选完目录后所属分组输入框自动预填)、重命名分组、删除空分组;标题栏不再放独立按钮。空分组持久保存在浏览器本地;一旦有工作区落进去,分组就由名称自然派生。
|
|
29
29
|
|
|
30
30
|
### 会话也吃这套规则
|
|
31
31
|
|
|
@@ -45,14 +45,14 @@ web/ ← 虚拟分组(不是真实目录,纯命名层级)
|
|
|
45
45
|
<table>
|
|
46
46
|
<tr>
|
|
47
47
|
<td align="center" width="58%"><img src="docs/screenshots/5-context-menu.png" alt="右键菜单"/></td>
|
|
48
|
-
<td valign="top"><b>右键菜单</b><br/>操作都收进右键:工作区行——重命名 / 删除 / 分叉 / 归档 /
|
|
48
|
+
<td valign="top"><b>右键菜单</b><br/>操作都收进右键:工作区行——重命名 / 删除 / 分叉 / 归档 / 自定义外观;工作区分组行——新增子分组 / 新增子工作区 / 重命名分组(同步更新组内所有工作区名称)/ 删除空分组 / 自定义外观;会话与会话分组行——重命名 / 分叉 / 归档 / 自定义外观。</td>
|
|
49
49
|
</tr>
|
|
50
50
|
</table>
|
|
51
51
|
|
|
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交易);展开状态与外观自定义经 dsh 客户端 store 持久化在当前浏览器。</td>
|
|
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,8 @@ web/ ← 虚拟分组(不是真实目录,纯命名层级)
|
|
|
60
60
|
|
|
61
61
|
- **文件夹式收起**:点工作区行 = 收起/展开该工作区的全部会话(收起后行上保留会话数角标);会话分组同样可收起;搜索时自动全部展开。
|
|
62
62
|
- **拖拽还原原版能力**:工作区行可拖拽——拖到另一工作区行上/下半即调整顺序(写回宿主 registry 序),拖到分组行则移动进该分组;会话行可拖拽——同工作区拖到另一会话上/下调整顺序,拖到会话子分组即移动进该分组。显示顺序 = 宿主手动序,拖拽结果立即生效。拖拽工作区期间,单链合并显示的行(如 工作/A)临时展开回文件夹树——路径里的每一级都是可投放的分组行,松手后自动合并回去。
|
|
63
|
-
- **不丢原生能力**:会话行(打开/重命名/分叉/归档)
|
|
63
|
+
- **不丢原生能力**:会话行(打开/重命名/分叉/归档)、每工作区「新会话」(点 + 总是先展开该工作区,让新会话立刻可见)、当前会话高亮、官方状态灯(蓝色运行环 / 琥珀待交互 / 绿色完成提醒)、**完成提醒浮窗**(折叠的工作区里有会话完成时在侧边栏弹数秒非阻塞提醒,不打断输入,可在设置关闭)、**活跃定时任务角标**(有活动 Schedule 的会话显示闹钟图标,与官方行一致)、未分组会话兜底、中英双语跟随界面语言。
|
|
64
|
+
- **URL 标题感知**:会话/工作区标题里的 URL(如 `https://host/api/v1/x`)按 URL 语义分层——域名与协议合成一段、路径各段自成层级,同前缀 URL 自动聚到同一分组下,不再被撕成伪分组。
|
|
64
65
|
- **对话空态页同样生效**:会话空态页的「添加工作区」菜单走的也是本插件的拾取交互,同样带所属分组弹窗。
|
|
65
66
|
|
|
66
67
|
## 安装
|
package/README_EN.md
CHANGED
|
@@ -21,9 +21,9 @@ web/ <- virtual folder (naming only, not a real directory)
|
|
|
21
21
|
|
|
22
22
|
After you pick a directory for a new workspace, a small dialog asks for the parent group — type one (`web`), pick an existing level from the datalist, or leave it empty for the root. The plugin creates the workspace and writes the prefix into its title, so it lands on the right branch of the tree.
|
|
23
23
|
|
|
24
|
-
###
|
|
24
|
+
### Folder create/delete via context menu
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Right-click any folder row: new subfolder (parent prefix pre-filled), new workspace here (the add-flow group field pre-fills with this folder), rename folder, delete empty folder — no standalone header button anymore. Explicit empty folders persist in the browser until workspaces live inside them.
|
|
27
27
|
|
|
28
28
|
### Sessions follow the same rule
|
|
29
29
|
|
|
@@ -43,7 +43,7 @@ Session titles nest on `/` too (e.g. `test1/plugin maintenance`). Session groups
|
|
|
43
43
|
<table>
|
|
44
44
|
<tr>
|
|
45
45
|
<td align="center" width="58%"><img src="docs/screenshots/5-context-menu.png" alt="Context menu"/></td>
|
|
46
|
-
<td valign="top"><b>Context menu everywhere</b><br/>Actions live in the right-click menu: workspace rows — rename / delete / fork / archive / customize; folder rows — rename group (updates all descendant workspaces) / delete empty group / customize; session & session-group rows — rename / fork / archive / customize.</td>
|
|
46
|
+
<td valign="top"><b>Context menu everywhere</b><br/>Actions live in the right-click menu: workspace rows — rename / delete / fork / archive / customize; folder rows — new subfolder / new workspace here / rename group (updates all descendant workspaces) / delete empty group / customize; session & session-group rows — rename / fork / archive / customize.</td>
|
|
47
47
|
</tr>
|
|
48
48
|
</table>
|
|
49
49
|
|
|
@@ -58,7 +58,8 @@ 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, 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.
|
|
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), **completion toasts** (a session finishing inside a collapsed workspace pops a brief non-blocking toast in the sidebar; never intercepts typing, 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
|
+
- **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.
|
|
62
63
|
- **Conversation hero too**: the empty-state “Add workspace” menu uses the same picking interaction with the same group popup.
|
|
63
64
|
|
|
64
65
|
## 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.8.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.8.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
|
@@ -31,7 +31,6 @@ window.__ModuleLoader__.load({
|
|
|
31
31
|
'title': '工作区',
|
|
32
32
|
'search.placeholder': '搜索工作区或会话',
|
|
33
33
|
'add': '添加工作区',
|
|
34
|
-
'newFolder': '新建分组',
|
|
35
34
|
'rail.search': '搜索',
|
|
36
35
|
'rail.add': '添加工作区',
|
|
37
36
|
'empty': '暂无工作区',
|
|
@@ -48,6 +47,7 @@ window.__ModuleLoader__.load({
|
|
|
48
47
|
'time.years': '{n} 年',
|
|
49
48
|
'status.running': '生成中',
|
|
50
49
|
'status.completed': '已完成',
|
|
50
|
+
'toast.completed': '会话已完成',
|
|
51
51
|
'status.approval': '等待批准',
|
|
52
52
|
'status.planReview': '等待计划确认',
|
|
53
53
|
'status.question': '等待回答',
|
|
@@ -57,6 +57,8 @@ window.__ModuleLoader__.load({
|
|
|
57
57
|
'menu.delete': '删除',
|
|
58
58
|
'menu.fork': '分叉',
|
|
59
59
|
'menu.archive': '归档',
|
|
60
|
+
'menu.newSubfolder': '新增子分组',
|
|
61
|
+
'menu.newSubWorkspace': '新增子工作区',
|
|
60
62
|
'menu.renameFolder': '重命名分组',
|
|
61
63
|
'menu.removeFolder': '删除分组',
|
|
62
64
|
'menu.renameSgroup': '重命名会话分组',
|
|
@@ -66,6 +68,8 @@ window.__ModuleLoader__.load({
|
|
|
66
68
|
'settings.collapse': '收起',
|
|
67
69
|
'settings.compactChains': '单链分组折叠显示',
|
|
68
70
|
'settings.compactChains.hint': '单层链合并为一行,出现多个子级时自动展开为树状;拖拽工作区期间单链临时展开回文件夹树,可放入任意一级;展开状态与自定义外观保存在当前浏览器。',
|
|
71
|
+
'settings.completionToasts': '完成提醒浮窗',
|
|
72
|
+
'settings.completionToasts.hint': '工作区处于折叠状态时,其中的会话完成后在侧边栏弹出数秒的非阻塞提醒(不打断输入等任何操作);默认开启,可在此关闭。',
|
|
69
73
|
'custom.title': '自定义外观',
|
|
70
74
|
'custom.color': '颜色',
|
|
71
75
|
'custom.glow': '发光',
|
|
@@ -117,7 +121,6 @@ window.__ModuleLoader__.load({
|
|
|
117
121
|
'title': 'Workspaces',
|
|
118
122
|
'search.placeholder': 'Search workspaces or sessions',
|
|
119
123
|
'add': 'Add workspace',
|
|
120
|
-
'newFolder': 'New folder',
|
|
121
124
|
'rail.search': 'Search',
|
|
122
125
|
'rail.add': 'Add workspace',
|
|
123
126
|
'empty': 'No workspaces yet',
|
|
@@ -134,6 +137,7 @@ window.__ModuleLoader__.load({
|
|
|
134
137
|
'time.years': '{n}y',
|
|
135
138
|
'status.running': 'Running',
|
|
136
139
|
'status.completed': 'Completed',
|
|
140
|
+
'toast.completed': 'Session completed',
|
|
137
141
|
'status.approval': 'Waiting for approval',
|
|
138
142
|
'status.planReview': 'Waiting for plan review',
|
|
139
143
|
'status.question': 'Waiting for answer',
|
|
@@ -143,6 +147,8 @@ window.__ModuleLoader__.load({
|
|
|
143
147
|
'menu.delete': 'Delete',
|
|
144
148
|
'menu.fork': 'Fork',
|
|
145
149
|
'menu.archive': 'Archive',
|
|
150
|
+
'menu.newSubfolder': 'New subfolder',
|
|
151
|
+
'menu.newSubWorkspace': 'New workspace here',
|
|
146
152
|
'menu.renameFolder': 'Rename folder',
|
|
147
153
|
'menu.removeFolder': 'Delete folder',
|
|
148
154
|
'menu.renameSgroup': 'Rename session group',
|
|
@@ -152,6 +158,8 @@ window.__ModuleLoader__.load({
|
|
|
152
158
|
'settings.collapse': 'Collapse',
|
|
153
159
|
'settings.compactChains': 'Merge single-child chains',
|
|
154
160
|
'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.completionToasts': 'Completion toasts',
|
|
162
|
+
'settings.completionToasts.hint': 'When its workspace row is collapsed, a finishing session pops a brief non-blocking toast in the sidebar (never intercepts typing); on by default, turn it off here.',
|
|
155
163
|
'custom.title': 'Customize',
|
|
156
164
|
'custom.color': 'Color',
|
|
157
165
|
'custom.glow': 'Glow',
|
|
@@ -210,8 +218,47 @@ window.__ModuleLoader__.load({
|
|
|
210
218
|
const i = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\'))
|
|
211
219
|
return i === -1 ? s : s.slice(i + 1)
|
|
212
220
|
}
|
|
213
|
-
|
|
214
|
-
|
|
221
|
+
/**
|
|
222
|
+
* URL-aware segment splitting for titles. A bare "/" split shreds URLs
|
|
223
|
+
* ("https:" → empty → host → path...), so when a "://" shows up the tail is
|
|
224
|
+
* parsed as a URL and split the way URLs mean it: scheme://host is ONE
|
|
225
|
+
* segment (the authority is not a hierarchy level), each pathname segment
|
|
226
|
+
* is a level, and query/hash glue to the last segment. Consequences:
|
|
227
|
+
* several sessions sharing a URL prefix (".../v1/m", ".../v1/n") nest
|
|
228
|
+
* under the same derived folder instead of piling up as long flat rows,
|
|
229
|
+
* while "docs/see https://x/a/b" keeps the plain "docs" head. Titles whose
|
|
230
|
+
* URL-looking tail does not parse stay opaque from the last "/" before the
|
|
231
|
+
* scheme marker. Grouping is a pure projection of titles, so sessions that
|
|
232
|
+
* were previously shredded into pseudo-folders re-flow on reload.
|
|
233
|
+
*/
|
|
234
|
+
const urlSegsOf = (raw) => {
|
|
235
|
+
let u = null
|
|
236
|
+
try { u = new URL(raw) } catch { return null }
|
|
237
|
+
const host = u.protocol + '//' + u.host
|
|
238
|
+
const pathSegs = u.pathname.split('/').filter(Boolean)
|
|
239
|
+
const tail = (u.search || '') + (u.hash || '')
|
|
240
|
+
if (pathSegs.length === 0) return [host + tail]
|
|
241
|
+
if (tail !== '') pathSegs[pathSegs.length - 1] += tail
|
|
242
|
+
return [host].concat(pathSegs)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const splitTitleSegs = (title) => {
|
|
246
|
+
const s = String(title || '')
|
|
247
|
+
const schemeAt = s.indexOf('://')
|
|
248
|
+
if (schemeAt === -1) return s.split('/').map(x => x.trim()).filter(Boolean)
|
|
249
|
+
let start = schemeAt
|
|
250
|
+
while (start > 0 && /[a-zA-Z0-9+.-]/.test(s[start - 1])) start--
|
|
251
|
+
const head = s.slice(0, start).split('/').map(x => x.trim()).filter(Boolean)
|
|
252
|
+
const urlSegs = urlSegsOf(s.slice(start).trim())
|
|
253
|
+
if (urlSegs !== null) return head.concat(urlSegs)
|
|
254
|
+
// Not a parseable URL after all: opaque from the last "/" before the marker.
|
|
255
|
+
const cut = s.lastIndexOf('/', schemeAt)
|
|
256
|
+
const segs = (cut === -1 ? '' : s.slice(0, cut)).split('/').map(x => x.trim()).filter(Boolean)
|
|
257
|
+
const tailText = s.slice(cut + 1).trim()
|
|
258
|
+
if (tailText !== '') segs.push(tailText)
|
|
259
|
+
return segs
|
|
260
|
+
}
|
|
261
|
+
const normPath = (p) => splitTitleSegs(p).join('/')
|
|
215
262
|
|
|
216
263
|
/** Render a primitives icon by name; unknown names degrade to null, never crash. */
|
|
217
264
|
const icon = (name, size) => {
|
|
@@ -219,22 +266,6 @@ window.__ModuleLoader__.load({
|
|
|
219
266
|
return C ? E(C, { size: size || 16 }) : null
|
|
220
267
|
}
|
|
221
268
|
|
|
222
|
-
/** Folder-with-plus glyph for the new-group header button (absent from the primitives set). */
|
|
223
|
-
const FolderPlusIcon = (props) => E('svg', {
|
|
224
|
-
width: props.size || 16,
|
|
225
|
-
height: props.size || 16,
|
|
226
|
-
viewBox: '0 0 16 16',
|
|
227
|
-
fill: 'none',
|
|
228
|
-
stroke: 'currentColor',
|
|
229
|
-
strokeWidth: 1.2,
|
|
230
|
-
strokeLinecap: 'round',
|
|
231
|
-
strokeLinejoin: 'round',
|
|
232
|
-
'aria-hidden': 'true',
|
|
233
|
-
},
|
|
234
|
-
E('path', { d: 'M1.75 4.6c0-.74.6-1.35 1.35-1.35h2.5l1.4 1.6h4.9c.74 0 1.35.6 1.35 1.35v5.9c0 .74-.6 1.35-1.35 1.35H3.1c-.74 0-1.35-.6-1.35-1.35V4.6z' }),
|
|
235
|
-
E('path', { d: 'M8 7.2v3.6M6.2 9h3.6' }),
|
|
236
|
-
)
|
|
237
|
-
|
|
238
269
|
const FALLBACK_UNITS = { minutes: 'm', hours: 'h', days: 'd', months: 'mo', years: 'y' }
|
|
239
270
|
const timeLabel = (updatedAt, now, t) => {
|
|
240
271
|
if (typeof ui.relativeTime !== 'function') return ''
|
|
@@ -326,22 +357,31 @@ window.__ModuleLoader__.load({
|
|
|
326
357
|
function buildSessionTree(rows) {
|
|
327
358
|
const root = { path: '', name: '', groups: [], sessions: [] }
|
|
328
359
|
const byPath = new Map([['', root]])
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
360
|
+
// SEGMENT-driven: paths arrive as already-split segment arrays (URL-aware
|
|
361
|
+
// splitTitleSegs can yield segments containing "//", e.g. the
|
|
362
|
+
// "scheme://host" authority segment) and are joined into the path KEY
|
|
363
|
+
// verbatim — never re-split on "/", which would shred an authority
|
|
364
|
+
// segment into "https:" + "" + host.
|
|
365
|
+
const ensure = (segs) => {
|
|
366
|
+
let node = root
|
|
367
|
+
let key = ''
|
|
368
|
+
for (const seg of segs) {
|
|
369
|
+
key = key === '' ? seg : key + '/' + seg
|
|
370
|
+
let next = byPath.get(key)
|
|
371
|
+
if (!next) {
|
|
372
|
+
next = { path: key, name: seg, groups: [], sessions: [] }
|
|
373
|
+
byPath.set(key, next)
|
|
374
|
+
node.groups.push(next)
|
|
375
|
+
}
|
|
376
|
+
node = next
|
|
377
|
+
}
|
|
338
378
|
return node
|
|
339
379
|
}
|
|
340
380
|
for (const row of rows || []) {
|
|
341
381
|
const segs = splitTitleSegs(row.title)
|
|
342
382
|
const folderPath = segs.slice(0, -1).join('/')
|
|
343
383
|
const leaf = segs.length > 0 ? segs[segs.length - 1] : row.title
|
|
344
|
-
ensure(
|
|
384
|
+
ensure(segs.slice(0, -1)).sessions.push({ ...row, leaf })
|
|
345
385
|
}
|
|
346
386
|
const sortRec = (node) => {
|
|
347
387
|
node.groups.sort((a, b) => a.name.localeCompare(b.name, 'zh'))
|
|
@@ -377,26 +417,32 @@ window.__ModuleLoader__.load({
|
|
|
377
417
|
function buildTree(items, explicitFolders) {
|
|
378
418
|
const root = { path: '', name: '', folders: [], workspaces: [] }
|
|
379
419
|
const byPath = new Map([['', root]])
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
420
|
+
// SEGMENT-driven, same as buildSessionTree: the joined path is only a
|
|
421
|
+
// KEY; parsing it back on "/" would break URL authority segments.
|
|
422
|
+
const ensure = (segs) => {
|
|
423
|
+
let node = root
|
|
424
|
+
let key = ''
|
|
425
|
+
for (const seg of segs) {
|
|
426
|
+
key = key === '' ? seg : key + '/' + seg
|
|
427
|
+
let next = byPath.get(key)
|
|
428
|
+
if (!next) {
|
|
429
|
+
next = { path: key, name: seg, folders: [], workspaces: [] }
|
|
430
|
+
byPath.set(key, next)
|
|
431
|
+
node.folders.push(next)
|
|
432
|
+
}
|
|
433
|
+
node = next
|
|
434
|
+
}
|
|
389
435
|
return node
|
|
390
436
|
}
|
|
391
437
|
for (const folder of explicitFolders || []) {
|
|
392
|
-
const
|
|
393
|
-
if (
|
|
438
|
+
const segs = splitTitleSegs(normPath(folder))
|
|
439
|
+
if (segs.length > 0) ensure(segs)
|
|
394
440
|
}
|
|
395
441
|
for (const workspace of items || []) {
|
|
396
442
|
const segs = splitTitleSegs(workspace.title)
|
|
397
443
|
const folderPath = segs.slice(0, -1).join('/')
|
|
398
444
|
const leaf = segs.length > 0 ? segs[segs.length - 1] : (basename(workspace.path) || String(workspace.title || '') || String(workspace.workspaceId || ''))
|
|
399
|
-
ensure(
|
|
445
|
+
ensure(segs.slice(0, -1)).workspaces.push({
|
|
400
446
|
workspaceId: workspace.workspaceId,
|
|
401
447
|
title: String(workspace.title || ''),
|
|
402
448
|
path: String(workspace.path || ''),
|
|
@@ -418,40 +464,59 @@ window.__ModuleLoader__.load({
|
|
|
418
464
|
/**
|
|
419
465
|
* VS Code-style single-child chain compression (preference-controlled):
|
|
420
466
|
* a folder level holding exactly ONE child and nothing else merges into a
|
|
421
|
-
* single display row
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
467
|
+
* single display row; a chain ending in one workspace becomes that
|
|
468
|
+
* workspace row with the merged label. Only presentation changes; the
|
|
469
|
+
* underlying workspace/title data is untouched.
|
|
470
|
+
*
|
|
471
|
+
* The merged label is the chain RELATIVE to the first un-compressed
|
|
472
|
+
* ancestor (VS Code explorer behaviour): a chain a/b/c holding only
|
|
473
|
+
* workspace W shows "a/b/c/W" at the root, but the same chain nested
|
|
474
|
+
* inside a populated folder "a" shows "b/c/W". Labels are therefore
|
|
475
|
+
* assembled from RELATIVE segments (segs + pure leaf name) and only
|
|
476
|
+
* materialised into a display node at the chain's top — the pre-0.8 code
|
|
477
|
+
* prefixed each recursion level with the FULL node.path, so nested chains
|
|
478
|
+
* rendered duplicated prefixes like "1/2/1/2/3/A".
|
|
425
479
|
*/
|
|
480
|
+
const materializeChain = (chain) => chain.kind === 'ws'
|
|
481
|
+
? {
|
|
482
|
+
kind: 'ws',
|
|
483
|
+
path: chain.path,
|
|
484
|
+
workspace: { ...chain.workspace, leaf: chain.segs.join('/') + '/' + chain.pure, title: chain.workspace.title, folderPath: '' },
|
|
485
|
+
folders: [],
|
|
486
|
+
workspaces: [],
|
|
487
|
+
}
|
|
488
|
+
: { kind: 'folder', path: chain.path, name: chain.segs.join('/'), folders: chain.folders, workspaces: chain.workspaces }
|
|
489
|
+
|
|
426
490
|
function compressTree(node) {
|
|
427
491
|
const folders = (node.folders || []).map(compressTree)
|
|
428
492
|
const workspaces = node.workspaces || []
|
|
429
493
|
if (workspaces.length === 0 && folders.length === 1) {
|
|
494
|
+
// Continue the chain upward: one more relative segment in front of
|
|
495
|
+
// whatever the child chain accumulated. path stays the DEEPEST full
|
|
496
|
+
// path (expansion identity); the label is joined only at the top.
|
|
430
497
|
const child = folders[0]
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
dropPath: child.path,
|
|
440
|
-
}
|
|
498
|
+
return {
|
|
499
|
+
kind: child.kind,
|
|
500
|
+
path: child.path,
|
|
501
|
+
segs: [node.name].concat(child.segs || []),
|
|
502
|
+
pure: child.pure,
|
|
503
|
+
workspace: child.workspace,
|
|
504
|
+
folders: child.folders,
|
|
505
|
+
workspaces: child.workspaces,
|
|
441
506
|
}
|
|
442
|
-
return { kind: 'folder', path: child.path, name: node.name + '/' + child.name, folders: child.folders || [], workspaces: child.workspaces || [], dropPath: child.path || node.path }
|
|
443
507
|
}
|
|
444
508
|
if (folders.length === 0 && workspaces.length === 1) {
|
|
445
|
-
|
|
446
|
-
return { kind: 'ws', path: node.path, workspace: { ...ws, leaf: (node.path !== '' ? node.path + '/' : '') + ws.leaf, title: ws.title, folderPath: '' }, folders: [], workspaces: [], dropPath: node.path }
|
|
509
|
+
return { kind: 'ws', path: node.path, segs: [node.name], pure: workspaces[0].leaf, workspace: workspaces[0], folders: [], workspaces: [] }
|
|
447
510
|
}
|
|
448
|
-
|
|
511
|
+
// Chain top (multiple children, or a mix): children are materialised
|
|
512
|
+
// relative to this node; this node itself keeps its single name.
|
|
513
|
+
return { kind: 'folder', path: node.path, segs: [node.name], folders: folders.map(materializeChain), workspaces }
|
|
449
514
|
}
|
|
450
515
|
|
|
451
516
|
/* ============================== styles ============================ */
|
|
452
517
|
|
|
453
518
|
const CSS_TEXT = [
|
|
454
|
-
'.bw-root{height:100%;display:flex;flex-direction:column;min-height:0;color:var(--dsw-alias-label-primary,#e6e6e6)}',
|
|
519
|
+
'.bw-root{height:100%;display:flex;flex-direction:column;min-height:0;position:relative;color:var(--dsw-alias-label-primary,#e6e6e6)}',
|
|
455
520
|
'.bw-header{display:flex;align-items:center;gap:2px;padding:10px 10px 4px;flex:none}',
|
|
456
521
|
'.bw-header-title{flex:1;font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--dsw-alias-label-secondary,#b8b8b8);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
457
522
|
'.bw-icon-btn{flex:none;width:24px;height:24px;border:none;background:transparent;border-radius:6px;display:grid;place-items:center;color:var(--dsw-alias-label-secondary,#b8b8b8);cursor:pointer;padding:0}',
|
|
@@ -550,6 +615,11 @@ window.__ModuleLoader__.load({
|
|
|
550
615
|
'.bw-preview-icon{flex:none;display:grid;place-items:center;width:20px;height:20px;color:var(--dsw-alias-label-primary,#e6e6e6)}',
|
|
551
616
|
'.bw-preview-icon svg{width:18px;height:18px}',
|
|
552
617
|
'.bw-preview-label{font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:18px}',
|
|
618
|
+
'.bw-toasts{position:absolute;right:10px;bottom:14px;display:flex;flex-direction:column;gap:6px;z-index:30;pointer-events:none;max-width:280px}',
|
|
619
|
+
'.bw-toast{display:flex;align-items:center;gap:8px;max-width:280px;padding:8px 10px;border-radius:8px;background:var(--dsw-specific-menu,var(--dsw-alias-bg-overlay,rgba(28,28,32,.72)));-webkit-backdrop-filter:var(--dsh-any-blur-card-panels,blur(12px) saturate(1.15));backdrop-filter:var(--dsh-any-blur-card-panels,blur(12px) saturate(1.15));border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.3));box-shadow:0 8px 24px rgba(0,0,0,.35);animation:bw-toast-in .18s ease}',
|
|
620
|
+
'@keyframes bw-toast-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}',
|
|
621
|
+
'.bw-toast-title{font-size:12.5px;color:var(--dsw-alias-label-primary,#e6e6e6);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
622
|
+
'.bw-toast-kind{flex:none;font-size:11px;color:var(--dsw-alias-label-tertiary,#9a9a9a)}',
|
|
553
623
|
].join('')
|
|
554
624
|
|
|
555
625
|
const StyleNode = () => E('style', null, CSS_TEXT)
|
|
@@ -670,6 +740,7 @@ window.__ModuleLoader__.load({
|
|
|
670
740
|
*/
|
|
671
741
|
function BetterFlow(props) {
|
|
672
742
|
const { open, busy, onPicked, onCancel, onError, createWorkspace, renameWorkspace, pickDirectory, useWorkspaces, useStore, t } = props
|
|
743
|
+
const initialParent = props.initialParent || ''
|
|
673
744
|
const actions = props.actions
|
|
674
745
|
const [phase, setPhase] = React.useState('idle') // idle | picking | picked | submitting
|
|
675
746
|
const [pickedPath, setPickedPath] = React.useState('')
|
|
@@ -694,7 +765,7 @@ window.__ModuleLoader__.load({
|
|
|
694
765
|
if (!alive) return
|
|
695
766
|
if (!path) { onCancel(); return }
|
|
696
767
|
setPickedPath(String(path))
|
|
697
|
-
setParentInput(
|
|
768
|
+
setParentInput(initialParent)
|
|
698
769
|
setPhase('picked')
|
|
699
770
|
})
|
|
700
771
|
.catch((reason) => {
|
|
@@ -1075,6 +1146,7 @@ window.__ModuleLoader__.load({
|
|
|
1075
1146
|
function BetterWorkspaceSettings({ useStore, actions, t }) {
|
|
1076
1147
|
const prefs = useStore ? (useStore(s => s.prefs) || {}) : {}
|
|
1077
1148
|
const compactChains = prefs.compactChains !== false
|
|
1149
|
+
const completionToasts = prefs.completionToasts !== false
|
|
1078
1150
|
return E('div', { className: 'bw-settings' },
|
|
1079
1151
|
StyleNode(),
|
|
1080
1152
|
E('div', { className: 'bw-setting-row' },
|
|
@@ -1089,6 +1161,18 @@ window.__ModuleLoader__.load({
|
|
|
1089
1161
|
}, E('span', { className: 'bw-switch-thumb' })),
|
|
1090
1162
|
),
|
|
1091
1163
|
E('div', { className: 'bw-hint' }, t('settings.compactChains.hint')),
|
|
1164
|
+
E('div', { className: 'bw-setting-row', style: { marginTop: 10 } },
|
|
1165
|
+
E('div', { className: 'bw-setting-label' }, t('settings.completionToasts')),
|
|
1166
|
+
E('button', {
|
|
1167
|
+
type: 'button',
|
|
1168
|
+
role: 'switch',
|
|
1169
|
+
'aria-checked': completionToasts,
|
|
1170
|
+
'aria-label': t('settings.completionToasts'),
|
|
1171
|
+
className: cls('bw-switch', completionToasts && 'bw-switch-on'),
|
|
1172
|
+
onClick: () => { actions.setPref('completionToasts', !completionToasts) },
|
|
1173
|
+
}, E('span', { className: 'bw-switch-thumb' })),
|
|
1174
|
+
),
|
|
1175
|
+
E('div', { className: 'bw-hint' }, t('settings.completionToasts.hint')),
|
|
1092
1176
|
)
|
|
1093
1177
|
}
|
|
1094
1178
|
|
|
@@ -1146,16 +1230,20 @@ window.__ModuleLoader__.load({
|
|
|
1146
1230
|
const prefsMap = useStore ? (useStore(s => s.prefs) || {}) : {}
|
|
1147
1231
|
const stylingMap = useStore ? (useStore(s => s.styling) || {}) : {}
|
|
1148
1232
|
const compactChains = prefsMap.compactChains !== false
|
|
1233
|
+
const completionToasts = prefsMap.completionToasts !== false
|
|
1149
1234
|
const archivedSet = React.useMemo(() => new Set(archivedSessionIds), [archivedSessionIds])
|
|
1150
1235
|
const subCounts = React.useMemo(() => subagentRunningCounts(list ? list.byId : {}), [list ? list.byId : null])
|
|
1151
1236
|
|
|
1152
1237
|
const [query, setQuery] = React.useState('')
|
|
1153
1238
|
const [searchOpen, setSearchOpen] = React.useState(false)
|
|
1154
1239
|
const [flowOpen, setFlowOpen] = React.useState(false)
|
|
1240
|
+
const [flowParent, setFlowParent] = React.useState('') // parent path prefill for the add-workspace flow (context menu entry)
|
|
1155
1241
|
const [dialog, setDialog] = React.useState(null) // { kind, ... }
|
|
1156
1242
|
const [ctx, setCtx] = React.useState(null) // context menu { kind, payload, x, y }
|
|
1157
1243
|
const [customize, setCustomize] = React.useState(null) // { kind, entryKey, name }
|
|
1158
1244
|
const [errorText, setErrorText] = React.useState(null)
|
|
1245
|
+
const [toasts, setToasts] = React.useState([]) // completion relays: { key, title }
|
|
1246
|
+
const toastTimers = React.useRef([])
|
|
1159
1247
|
const [drag, setDrag] = React.useState(null) // { kind: 'workspace'|'session', source, over } | null
|
|
1160
1248
|
// Workspace drags arm their state one frame LATE (see workspaceDragEvents):
|
|
1161
1249
|
// arming synchronously re-renders during the dragstart dispatch, the chain
|
|
@@ -1163,9 +1251,43 @@ window.__ModuleLoader__.load({
|
|
|
1163
1251
|
// source element, and Chromium cancels the whole gesture. dragEnd clears
|
|
1164
1252
|
// the timer so a same-tick cancel never leaves a ghost drag behind.
|
|
1165
1253
|
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.
|
|
1166
1257
|
const normalizedQuery = query.trim().toLowerCase()
|
|
1167
1258
|
const now = Date.now()
|
|
1168
1259
|
|
|
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
|
+
|
|
1169
1291
|
const fail = (text) => { setFlowOpen(false); setDialog(null); setErrorText(String(text || 'unknown error')) }
|
|
1170
1292
|
|
|
1171
1293
|
const styleEntry = (key) => stylingMap[key] || null
|
|
@@ -1226,7 +1348,7 @@ window.__ModuleLoader__.load({
|
|
|
1226
1348
|
const tree = React.useMemo(() => {
|
|
1227
1349
|
const built = buildTree(items, storeFolders)
|
|
1228
1350
|
if (!compactChains || draggingWorkspace) return built
|
|
1229
|
-
return { ...built, folders: built.folders.map(compressTree), workspaces: built.workspaces }
|
|
1351
|
+
return { ...built, folders: built.folders.map((f) => materializeChain(compressTree(f))), workspaces: built.workspaces }
|
|
1230
1352
|
}, [items, storeFolders, compactChains, draggingWorkspace])
|
|
1231
1353
|
|
|
1232
1354
|
const sessionsOf = (workspace) => {
|
|
@@ -1566,10 +1688,19 @@ window.__ModuleLoader__.load({
|
|
|
1566
1688
|
return null
|
|
1567
1689
|
}
|
|
1568
1690
|
|
|
1569
|
-
const submitFolderNew = (rawPath) => {
|
|
1570
|
-
const
|
|
1571
|
-
|
|
1572
|
-
if (
|
|
1691
|
+
const submitFolderNew = (parentPath, rawPath) => {
|
|
1692
|
+
const parent = normPath(parentPath)
|
|
1693
|
+
const sub = normPath(rawPath)
|
|
1694
|
+
if (sub === '') { setErrorText(t('folder.error.empty')); return }
|
|
1695
|
+
const p = parent !== '' ? parent + '/' + sub : sub
|
|
1696
|
+
// Existence check covers explicit folders AND every folder derived
|
|
1697
|
+
// from workspace titles, so "already exists" means either kind.
|
|
1698
|
+
const derived = new Set()
|
|
1699
|
+
for (const w of items || []) {
|
|
1700
|
+
const segs = splitTitleSegs(w.title)
|
|
1701
|
+
for (let i = 1; i < segs.length; i++) derived.add(segs.slice(0, i).join('/'))
|
|
1702
|
+
}
|
|
1703
|
+
if (storeFolders.includes(p) || derived.has(p)) { setErrorText(t('folder.error.exists')); return }
|
|
1573
1704
|
actions.addFolder(p)
|
|
1574
1705
|
setDialog(null)
|
|
1575
1706
|
}
|
|
@@ -1673,7 +1804,9 @@ window.__ModuleLoader__.load({
|
|
|
1673
1804
|
sessionsOpen: searching ? true : sessionsOpenOf(workspace.workspaceId),
|
|
1674
1805
|
currentInside: !!(list && list.current && (workspace.sessionIds || []).includes(list.current)),
|
|
1675
1806
|
onToggle: () => { if (!searching) actions.setSessionsExpanded(workspace.workspaceId, !sessionsOpenOf(workspace.workspaceId)) },
|
|
1676
|
-
|
|
1807
|
+
// Starting a session force-expands the workspace row: the user must
|
|
1808
|
+
// SEE the new session appear, even if the row was collapsed.
|
|
1809
|
+
onStart: () => { actions.setSessionsExpanded(workspace.workspaceId, true); startSession(workspace.workspaceId) },
|
|
1677
1810
|
onContextMenu: (e) => openCtx('workspace', workspace, e),
|
|
1678
1811
|
dropHalf: wsDropHalf(workspace.workspaceId),
|
|
1679
1812
|
dragEvents: workspaceDragEvents(workspace),
|
|
@@ -1752,7 +1885,11 @@ window.__ModuleLoader__.load({
|
|
|
1752
1885
|
const ctxItems = () => {
|
|
1753
1886
|
if (ctx === null) return []
|
|
1754
1887
|
if (ctx.kind === 'folder') {
|
|
1755
|
-
const items = [
|
|
1888
|
+
const items = [
|
|
1889
|
+
{ id: 'new-subfolder', label: t('menu.newSubfolder') },
|
|
1890
|
+
{ id: 'new-subworkspace', label: t('menu.newSubWorkspace') },
|
|
1891
|
+
{ id: 'rename-folder', label: t('menu.renameFolder') },
|
|
1892
|
+
]
|
|
1756
1893
|
if (storeFolders.includes(ctx.payload.path)) items.push({ id: 'remove-folder', label: t('menu.removeFolder'), danger: true })
|
|
1757
1894
|
items.push({ sep: true })
|
|
1758
1895
|
items.push({ id: 'customize', label: t('custom.title') })
|
|
@@ -1790,7 +1927,9 @@ window.__ModuleLoader__.load({
|
|
|
1790
1927
|
}
|
|
1791
1928
|
const { kind, payload } = current
|
|
1792
1929
|
setCtx(null)
|
|
1793
|
-
if (kind === 'folder' && id === '
|
|
1930
|
+
if (kind === 'folder' && id === 'new-subfolder') setDialog({ kind: 'folder-new', parentPath: payload.path })
|
|
1931
|
+
else if (kind === 'folder' && id === 'new-subworkspace') { setFlowParent(payload.path); setFlowOpen(true) }
|
|
1932
|
+
else if (kind === 'folder' && id === 'rename-folder') setDialog({ kind: 'folder-rename', path: payload.path })
|
|
1794
1933
|
else if (kind === 'folder' && id === 'remove-folder') setDialog({ kind: 'folder-delete', path: payload.path })
|
|
1795
1934
|
else if (kind === 'workspace' && id === 'rename') setDialog({ kind: 'ws-rename', workspace: payload })
|
|
1796
1935
|
else if (kind === 'workspace' && id === 'delete') setDialog({ kind: 'ws-delete', workspace: payload })
|
|
@@ -1810,7 +1949,7 @@ window.__ModuleLoader__.load({
|
|
|
1810
1949
|
return E('div', { className: 'bw-rail' },
|
|
1811
1950
|
StyleNode(),
|
|
1812
1951
|
E('button', { type: 'button', className: 'bw-rail-btn', 'aria-label': t('rail.search'), onClick: () => { expandSidebar(); setSearchOpen(true) } }, icon('IconSearchOutline16', 18)),
|
|
1813
|
-
E('button', { type: 'button', className: 'bw-rail-btn', 'aria-label': t('rail.add'), onClick: () => { expandSidebar(); setFlowOpen(true) } }, icon('IconProjectAddOutline16', 18)),
|
|
1952
|
+
E('button', { type: 'button', className: 'bw-rail-btn', 'aria-label': t('rail.add'), onClick: () => { expandSidebar(); setFlowParent(''); setFlowOpen(true) } }, icon('IconProjectAddOutline16', 18)),
|
|
1814
1953
|
)
|
|
1815
1954
|
}
|
|
1816
1955
|
|
|
@@ -1854,8 +1993,8 @@ window.__ModuleLoader__.load({
|
|
|
1854
1993
|
key: 'folder-new',
|
|
1855
1994
|
title: t('folder.new.title'),
|
|
1856
1995
|
hint: t('folder.new.hint'),
|
|
1857
|
-
initial: '',
|
|
1858
|
-
onConfirm: submitFolderNew,
|
|
1996
|
+
initial: dialog.parentPath ? dialog.parentPath + '/' : '',
|
|
1997
|
+
onConfirm: (v) => submitFolderNew(dialog.parentPath || '', v),
|
|
1859
1998
|
onClose: () => setDialog(null),
|
|
1860
1999
|
t,
|
|
1861
2000
|
})
|
|
@@ -1904,13 +2043,13 @@ window.__ModuleLoader__.load({
|
|
|
1904
2043
|
onBlur: () => { if (query === '') setSearchOpen(false) },
|
|
1905
2044
|
}) : null,
|
|
1906
2045
|
E('button', { type: 'button', className: 'bw-icon-btn', 'aria-label': t('search.placeholder'), onClick: () => setSearchOpen(v => !v) }, icon('IconSearchOutline16')),
|
|
1907
|
-
E('button', { type: 'button', className: 'bw-icon-btn', 'aria-label': t('
|
|
1908
|
-
E('button', { type: 'button', className: 'bw-icon-btn', 'aria-label': t('add'), onClick: () => setFlowOpen(true) }, icon('IconProjectAddOutline16')),
|
|
2046
|
+
E('button', { type: 'button', className: 'bw-icon-btn', 'aria-label': t('add'), onClick: () => { setFlowParent(''); setFlowOpen(true) } }, icon('IconProjectAddOutline16')),
|
|
1909
2047
|
),
|
|
1910
2048
|
E('div', { className: 'bw-tree', role: 'tree', 'aria-label': t('title') }, bodyRows),
|
|
1911
2049
|
E(BetterFlow, {
|
|
1912
2050
|
open: flowOpen,
|
|
1913
2051
|
busy: false,
|
|
2052
|
+
initialParent: flowParent,
|
|
1914
2053
|
onPicked: flowOwner.onPicked,
|
|
1915
2054
|
onCancel: flowOwner.onCancel,
|
|
1916
2055
|
onError: flowOwner.onError,
|
|
@@ -1954,6 +2093,13 @@ window.__ModuleLoader__.load({
|
|
|
1954
2093
|
onClose: () => setCustomize(null),
|
|
1955
2094
|
t,
|
|
1956
2095
|
}),
|
|
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,
|
|
1957
2103
|
E(ui.Modal, {
|
|
1958
2104
|
open: errorText !== null,
|
|
1959
2105
|
onClose: () => setErrorText(null),
|