dsh-better-workspace 0.17.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -69,7 +69,7 @@
69
69
  ### 其余亮点
70
70
 
71
71
  - **文件夹式收起**:点工作区行 = 收起/展开该工作区的全部会话(收起后行上保留会话数角标);会话分组同样可收起;搜索时自动全部展开。
72
- - **拖拽还原原版能力**:工作区行可拖拽——拖到另一工作区行上/下半即调整顺序(写回宿主 registry 序),拖到分组行则移动进该分组(改写名称前缀);会话行可拖拽——同工作区拖到另一会话上/下调整顺序,拖到会话子分组即移动进该分组。显示顺序 = 宿主手动序,拖拽结果立即生效。**磁盘层是文件系统事实,不随拖拽改变**:跨磁盘层的放置只重组名称分组(工作区落回自身磁盘层的末尾),同层放置才走锚点序。拖拽工作区期间,单链合并显示的行(如 工作/A)临时展开回文件夹树——路径里的每一级都是可投放的分组行,松手后自动合并回去。
72
+ - **拖拽还原原版能力**:工作区行可拖拽——拖到另一工作区行上/下半即调整顺序(写回宿主 registry 序),拖到分组行则移动进该分组(改写名称前缀);会话行可拖拽——同组内拖到另一会话上/下即调整顺序,拖到**别组的会话行**会把名字前缀改写到那个组(拖到根层的行 = 移出分组),拖到会话子分组行即移动进该分组,拖到**自己的工作区行**或**树里任何组外的地方**(含空白区域)同样移出分组 —— 拖拽期间工作区行整行高亮、树底部有一条淡虚线提示这个出口;带分组前缀的会话行右键还多一项「移出分组」。显示顺序 = 宿主手动序,拖拽结果立即生效。**磁盘层是文件系统事实,不随拖拽改变**:跨磁盘层的放置只重组名称分组(工作区落回自身磁盘层的末尾),同层放置才走锚点序。拖拽工作区期间,单链合并显示的行(如 工作/A)临时展开回文件夹树——路径里的每一级都是可投放的分组行,松手后自动合并回去。
73
73
  - **不丢原生能力**:会话行(打开/重命名/分叉/归档)、每工作区「新会话」(点 + 总是先展开该工作区,让新会话立刻可见)、当前会话高亮、官方状态灯(蓝色运行环 / 琥珀待交互 / 绿色完成提醒)、**状态呼吸灯**(折叠藏起的状态灯向外冒泡:工作区/分组行图标按状态色呼吸发光——琥珀待交互/绿完成,自定义过发光的标题一起呼吸;运行中只在会话行本层蓝色呼吸,不外渗;可在设置关闭)、**活跃定时任务角标**(有活动 Schedule 的会话显示闹钟图标,与官方行一致)、未分组会话兜底、界面文案跟随 DSH 的语言设置(内置 21 种语言,含简繁中文)。
74
74
  - **URL/斜杠标题不拆层**:宿主自动生成的会话标题落地时若含 `/`(如复述了 URL),整串自动包上 `“”` 原样显示,绝不解析成层级——只对本次启动后**新建的会话**生效,并等命名稳定(约 20 秒,让原生 AI 命名先落地)才处理;存量会话与你手动命名的 `/` 分组标题永不被改动。引号内(含手动添加)的 `/` 永不切分,孤立引号只当普通字符;无引号但含 URL 的旧标题也有平铺兜底。
75
75
  - **对话空态页同样生效**:会话空态页的「添加工作区」菜单同样回归官方目录流(原生选择器 / 官方应用内浏览器)。
@@ -116,7 +116,7 @@ DSH 的侧边栏浏览区是一个公开插槽 `sidebar.workspaces`(single),官
116
116
 
117
117
  ## 限制与路线图
118
118
 
119
- - 会话拖到**另一分组内的会话行**仅做扁平重排(标题分组归属不变);要改分组请拖到分组行或走重命名。
119
+ - 会话**跨工作区**拖拽仍被拒绝(会话归属工作区);同一工作区内的分组归属已随拖拽生效(v0.18.0:拖到别组的行改写前缀,拖到自己的工作区行移出分组,右键也有「移出分组」)。
120
120
  - 搜索为本地标题过滤(两层扁平化后逐工作区匹配);宿主内容搜索(`session.search`)计划接回。
121
121
  - flat(平铺)视图未接管,层级树即视图。
122
122
 
package/README_EN.md CHANGED
@@ -69,7 +69,7 @@ Session titles nest on `/` too (e.g. `test1/plugin maintenance`). Session groups
69
69
  ### More highlights
70
70
 
71
71
  - **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.
72
- - **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 (rewrites the title prefix); 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. **The disk layer is a filesystem fact and never changes through dragging**: a cross-level drop only re-groups the name prefix (the workspace lands at the end of its own disk level), same-level drops take the anchor order. 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.
72
+ - **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 (rewrites the title prefix); drag session rows to reorder inside their group, onto a row of ANOTHER group to move into that group (onto a root-level row to leave the group), onto a session-group row to move into it, or onto their OWN workspace row / anywhere outside a group (empty space included) to leave it — the workspace row highlights and the tree shows a faint dashed hint while such a drag is in flight; a grouped session row also offers "Move out of group" in its context menu. Display order = host manual order, drag results are visible immediately. **The disk layer is a filesystem fact and never changes through dragging**: a cross-level drop only re-groups the name prefix (the workspace lands at the end of its own disk level), same-level drops take the anchor order. 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.
73
73
  - **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, UI copy in 21 languages following the DSH language preference (Simplified and Traditional Chinese included).
74
74
  - **Slash-bearing titles never nest from URLs**: when a host-generated session title lands containing `/` (e.g. echoing a pasted URL), it is auto-wrapped in `“”` and shown verbatim — only for sessions **created after this launch**, and only once the name stays stable for ~20s (letting the native AI naming land first); pre-existing sessions and your manually named `/` groupings are never touched. Quotes (manual ones too) never split on `/`, and a lone quote is just an ordinary character; unquoted URL tails still fall back flat.
75
75
  - **Conversation hero too**: the empty-state “Add workspace” menu returns to the official directory flow (native chooser / official in-app browser).
@@ -102,7 +102,7 @@ Same `dsh-better-*` family, zero overlap: [dsh-better-sidebar](https://github.co
102
102
 
103
103
  ## Limitations / roadmap
104
104
 
105
- - A session dragged onto a session row **inside another group** only gets reordered in the flat list (its title group stays); drag onto the group row or rename to move it.
105
+ - Dragging a session **across workspaces** is still refused (a session belongs to its workspace); inside one workspace the group now follows the drop (v0.18.0: another group's row rewrites the prefix, the workspace row takes it out of every group, and the context menu offers "Move out of group").
106
106
  - Search is local title filtering (both layers flattened, per-workspace matching); host content search (`session.search`) is planned.
107
107
  - The flat view is not taken over; the hierarchical tree is the view.
108
108
 
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-better-workspace",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "main": "./src/index.js",
5
5
  "description": "侧边栏工作区树:按官方磁盘目录嵌套(官方 workspace-tree 语义),可选按名称中的 / 分组(默认关闭,设置里开启);添加工作区由官方目录流承载。Workspace tree for the DSH sidebar workspace list, nested by official disk folders with optional \"/\"-name grouping.",
6
6
  "engines": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-better-workspace",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "侧边栏工作区树:按官方磁盘目录嵌套(官方 workspace-tree 语义),可选按名称中的 / 分组(默认关闭,设置里开启);添加工作区走官方目录流。Workspace tree for the DSH sidebar: disk-path nesting (official workspace-tree semantics), with optional \"/\"-name grouping (off by default); add-workspace rides the official directory flow.",
5
5
  "type": "module",
6
6
  "repository": {
package/src/client.js CHANGED
@@ -72,6 +72,7 @@ window.__ModuleLoader__.load({
72
72
  'menu.renameFolder': '重命名分组',
73
73
  'menu.renameSgroup': '重命名会话分组',
74
74
  'menu.moveToGroup': '移动到分组…',
75
+ 'menu.moveOutGroup': '移出分组',
75
76
  'settings.title': '更好的工作区',
76
77
  'settings.desc': '工作区树的外观与折叠偏好',
77
78
  'settings.expand': '展开',
@@ -151,6 +152,7 @@ window.__ModuleLoader__.load({
151
152
  'group.new.nameHint': "名称中的 / 即嵌套层级,例如 前端/组件",
152
153
  'group.new.empty': "请输入分组名称",
153
154
  'group.new.root': "顶层",
155
+ 'group.new.independent': "新建顶层分组",
154
156
  'group.new.pickWorkspace': "选择一个工作区",
155
157
  'group.new.noWorkspace': "还没有工作区,请先添加一个",
156
158
  'group.new.pickFirst': "请先选择一个工作区",
@@ -200,6 +202,7 @@ window.__ModuleLoader__.load({
200
202
  'menu.renameFolder': 'Rename folder',
201
203
  'menu.renameSgroup': 'Rename session group',
202
204
  'menu.moveToGroup': 'Move to group…',
205
+ 'menu.moveOutGroup': 'Move out of group',
203
206
  'settings.title': 'Better Workspaces',
204
207
  'settings.desc': 'Workspace tree appearance and folding preferences',
205
208
  'settings.expand': 'Expand',
@@ -279,6 +282,7 @@ window.__ModuleLoader__.load({
279
282
  'group.new.nameHint': "A \"/\" nests further, e.g. frontend/components",
280
283
  'group.new.empty': "Enter a group name",
281
284
  'group.new.root': "Top level",
285
+ 'group.new.independent': "New top-level group",
282
286
  'group.new.pickWorkspace': "Pick a workspace",
283
287
  'group.new.noWorkspace': "No workspaces yet — add one first",
284
288
  'group.new.pickFirst': "Pick a workspace first",
@@ -336,6 +340,7 @@ window.__ModuleLoader__.load({
336
340
  'menu.renameFolder': 'إعادة تسمية المجلد',
337
341
  'menu.renameSgroup': 'إعادة تسمية مجموعة الجلسات',
338
342
  'menu.moveToGroup': 'نقل إلى مجموعة…',
343
+ 'menu.moveOutGroup': 'إخراج من المجموعة',
339
344
  'settings.title': 'مساحات عمل أفضل',
340
345
  'settings.desc': 'مظهر شجرة مساحات العمل وتفضيلات الطيّ',
341
346
  'settings.expand': 'توسيع',
@@ -415,6 +420,7 @@ window.__ModuleLoader__.load({
415
420
  'group.new.nameHint': "الشرطة المائلة \"/\" تنشئ مستوى أعمق، مثل frontend/components",
416
421
  'group.new.empty': "أدخل اسم المجموعة",
417
422
  'group.new.root': "المستوى الأعلى",
423
+ 'group.new.independent': "مجموعة جديدة في المستوى الأعلى",
418
424
  'group.new.pickWorkspace': "اختر مساحة عمل",
419
425
  'group.new.noWorkspace': "لا توجد مساحات عمل بعد — أضف واحدة أولاً",
420
426
  'group.new.pickFirst': "اختر مساحة عمل أولاً",
@@ -464,6 +470,7 @@ window.__ModuleLoader__.load({
464
470
  'menu.renameFolder': 'Ordner umbenennen',
465
471
  'menu.renameSgroup': 'Sitzungsgruppe umbenennen',
466
472
  'menu.moveToGroup': 'In Gruppe verschieben…',
473
+ 'menu.moveOutGroup': 'Aus der Gruppe entfernen',
467
474
  'settings.title': 'Bessere Arbeitsbereiche',
468
475
  'settings.desc': 'Aussehen und Einklappverhalten des Arbeitsbereich-Baums',
469
476
  'settings.expand': 'Ausklappen',
@@ -543,6 +550,7 @@ window.__ModuleLoader__.load({
543
550
  'group.new.nameHint': "Ein „/\" verschachtelt weiter, z. B. frontend/components",
544
551
  'group.new.empty': "Gruppennamen eingeben",
545
552
  'group.new.root': "Oberste Ebene",
553
+ 'group.new.independent': "Neue Gruppe auf oberster Ebene",
546
554
  'group.new.pickWorkspace': "Arbeitsbereich wählen",
547
555
  'group.new.noWorkspace': "Noch keine Arbeitsbereiche — zuerst einen hinzufügen",
548
556
  'group.new.pickFirst': "Zuerst einen Arbeitsbereich wählen",
@@ -592,6 +600,7 @@ window.__ModuleLoader__.load({
592
600
  'menu.renameFolder': 'Renommer le dossier',
593
601
  'menu.renameSgroup': 'Renommer le groupe de sessions',
594
602
  'menu.moveToGroup': 'Déplacer vers le groupe…',
603
+ 'menu.moveOutGroup': 'Retirer du groupe',
595
604
  'settings.title': 'Espaces de travail améliorés',
596
605
  'settings.desc': 'Apparence et repli de l\'arborescence des espaces de travail',
597
606
  'settings.expand': 'Développer',
@@ -671,6 +680,7 @@ window.__ModuleLoader__.load({
671
680
  'group.new.nameHint': "Un « / » imbrique davantage, par ex. frontend/components",
672
681
  'group.new.empty': "Saisissez un nom de groupe",
673
682
  'group.new.root': "Niveau supérieur",
683
+ 'group.new.independent': "Nouveau groupe de premier niveau",
674
684
  'group.new.pickWorkspace': "Choisissez un espace de travail",
675
685
  'group.new.noWorkspace': "Aucun espace de travail — ajoutez-en un d’abord",
676
686
  'group.new.pickFirst': "Choisissez d’abord un espace de travail",
@@ -720,6 +730,7 @@ window.__ModuleLoader__.load({
720
730
  'menu.renameFolder': 'फ़ोल्डर का नाम बदलें',
721
731
  'menu.renameSgroup': 'सत्र समूह का नाम बदलें',
722
732
  'menu.moveToGroup': 'समूह में ले जाएँ…',
733
+ 'menu.moveOutGroup': 'समूह से बाहर निकालें',
723
734
  'settings.title': 'बेहतर कार्यस्थान',
724
735
  'settings.desc': 'कार्यस्थान वृक्ष का रूप और समेटने की प्राथमिकताएँ',
725
736
  'settings.expand': 'फैलाएँ',
@@ -799,6 +810,7 @@ window.__ModuleLoader__.load({
799
810
  'group.new.nameHint': "\"/\" और गहरा स्तर बनाता है, जैसे frontend/components",
800
811
  'group.new.empty': "समूह का नाम दर्ज करें",
801
812
  'group.new.root': "शीर्ष स्तर",
813
+ 'group.new.independent': "नया शीर्ष-स्तरीय समूह",
802
814
  'group.new.pickWorkspace': "एक कार्यस्थान चुनें",
803
815
  'group.new.noWorkspace': "अभी कोई कार्यस्थान नहीं — पहले एक जोड़ें",
804
816
  'group.new.pickFirst': "पहले एक कार्यस्थान चुनें",
@@ -848,6 +860,7 @@ window.__ModuleLoader__.load({
848
860
  'menu.renameFolder': 'Ganti nama folder',
849
861
  'menu.renameSgroup': 'Ganti nama grup sesi',
850
862
  'menu.moveToGroup': 'Pindahkan ke grup…',
863
+ 'menu.moveOutGroup': 'Keluarkan dari grup',
851
864
  'settings.title': 'Ruang kerja yang lebih baik',
852
865
  'settings.desc': 'Tampilan dan pelipatan pohon ruang kerja',
853
866
  'settings.expand': 'Bentangkan',
@@ -927,6 +940,7 @@ window.__ModuleLoader__.load({
927
940
  'group.new.nameHint': "\"/\" membuat tingkat lebih dalam, mis. frontend/components",
928
941
  'group.new.empty': "Masukkan nama grup",
929
942
  'group.new.root': "Tingkat teratas",
943
+ 'group.new.independent': "Grup tingkat teratas baru",
930
944
  'group.new.pickWorkspace': "Pilih ruang kerja",
931
945
  'group.new.noWorkspace': "Belum ada ruang kerja — tambahkan dulu",
932
946
  'group.new.pickFirst': "Pilih ruang kerja dulu",
@@ -976,6 +990,7 @@ window.__ModuleLoader__.load({
976
990
  'menu.renameFolder': 'Rinomina cartella',
977
991
  'menu.renameSgroup': 'Rinomina gruppo di sessioni',
978
992
  'menu.moveToGroup': 'Sposta nel gruppo…',
993
+ 'menu.moveOutGroup': 'Rimuovi dal gruppo',
979
994
  'settings.title': 'Aree di lavoro migliori',
980
995
  'settings.desc': 'Aspetto e compressione dell\'albero delle aree di lavoro',
981
996
  'settings.expand': 'Espandi',
@@ -1055,6 +1070,7 @@ window.__ModuleLoader__.load({
1055
1070
  'group.new.nameHint': "Una «/» annida ulteriormente, es. frontend/components",
1056
1071
  'group.new.empty': "Inserisci un nome di gruppo",
1057
1072
  'group.new.root': "Livello superiore",
1073
+ 'group.new.independent': "Nuovo gruppo di primo livello",
1058
1074
  'group.new.pickWorkspace': "Scegli un’area di lavoro",
1059
1075
  'group.new.noWorkspace': "Nessuna area di lavoro — aggiungine una prima",
1060
1076
  'group.new.pickFirst': "Scegli prima un’area di lavoro",
@@ -1104,6 +1120,7 @@ window.__ModuleLoader__.load({
1104
1120
  'menu.renameFolder': 'フォルダー名を変更',
1105
1121
  'menu.renameSgroup': 'セッショングループ名を変更',
1106
1122
  'menu.moveToGroup': 'グループに移動…',
1123
+ 'menu.moveOutGroup': 'グループから出す',
1107
1124
  'settings.title': 'より良いワークスペース',
1108
1125
  'settings.desc': 'ワークスペースツリーの外観と折りたたみ設定',
1109
1126
  'settings.expand': '展開',
@@ -1183,6 +1200,7 @@ window.__ModuleLoader__.load({
1183
1200
  'group.new.nameHint': "/ でさらに深い階層になります(例: frontend/components)",
1184
1201
  'group.new.empty': "グループ名を入力してください",
1185
1202
  'group.new.root': "最上位",
1203
+ 'group.new.independent': "最上位に新しいグループ",
1186
1204
  'group.new.pickWorkspace': "ワークスペースを選択",
1187
1205
  'group.new.noWorkspace': "ワークスペースがありません。先に追加してください",
1188
1206
  'group.new.pickFirst': "先にワークスペースを選択してください",
@@ -1232,6 +1250,7 @@ window.__ModuleLoader__.load({
1232
1250
  'menu.renameFolder': '폴더 이름 바꾸기',
1233
1251
  'menu.renameSgroup': '세션 그룹 이름 바꾸기',
1234
1252
  'menu.moveToGroup': '그룹으로 이동…',
1253
+ 'menu.moveOutGroup': '그룹에서 제거',
1235
1254
  'settings.title': '더 나은 워크스페이스',
1236
1255
  'settings.desc': '워크스페이스 트리의 모양과 접기 설정',
1237
1256
  'settings.expand': '펼치기',
@@ -1311,6 +1330,7 @@ window.__ModuleLoader__.load({
1311
1330
  'group.new.nameHint': "\"/\"로 더 깊은 단계, 예: frontend/components",
1312
1331
  'group.new.empty': "그룹 이름을 입력하세요",
1313
1332
  'group.new.root': "최상위",
1333
+ 'group.new.independent': "최상위 새 그룹",
1314
1334
  'group.new.pickWorkspace': "워크스페이스 선택",
1315
1335
  'group.new.noWorkspace': "워크스페이스가 없습니다. 먼저 추가하세요",
1316
1336
  'group.new.pickFirst': "먼저 워크스페이스를 선택하세요",
@@ -1360,6 +1380,7 @@ window.__ModuleLoader__.load({
1360
1380
  'menu.renameFolder': 'Mapnaam wijzigen',
1361
1381
  'menu.renameSgroup': 'Sessiegroep naam wijzigen',
1362
1382
  'menu.moveToGroup': 'Naar groep verplaatsen…',
1383
+ 'menu.moveOutGroup': 'Uit groep halen',
1363
1384
  'settings.title': 'Betere werkruimten',
1364
1385
  'settings.desc': 'Uiterlijk en inklapgedrag van de werkruimteboom',
1365
1386
  'settings.expand': 'Uitklappen',
@@ -1439,6 +1460,7 @@ window.__ModuleLoader__.load({
1439
1460
  'group.new.nameHint': "Een \"/\" nest dieper, bijv. frontend/components",
1440
1461
  'group.new.empty': "Voer een groepsnaam in",
1441
1462
  'group.new.root': "Bovenste niveau",
1463
+ 'group.new.independent': "Nieuwe groep op hoogste niveau",
1442
1464
  'group.new.pickWorkspace': "Kies een werkruimte",
1443
1465
  'group.new.noWorkspace': "Nog geen werkruimten — voeg er eerst een toe",
1444
1466
  'group.new.pickFirst': "Kies eerst een werkruimte",
@@ -1488,6 +1510,7 @@ window.__ModuleLoader__.load({
1488
1510
  'menu.renameFolder': 'Zmień nazwę folderu',
1489
1511
  'menu.renameSgroup': 'Zmień nazwę grupy sesji',
1490
1512
  'menu.moveToGroup': 'Przenieś do grupy…',
1513
+ 'menu.moveOutGroup': 'Przenieś poza grupę',
1491
1514
  'settings.title': 'Lepsze obszary robocze',
1492
1515
  'settings.desc': 'Wygląd i zwijanie drzewa obszarów roboczych',
1493
1516
  'settings.expand': 'Rozwiń',
@@ -1567,6 +1590,7 @@ window.__ModuleLoader__.load({
1567
1590
  'group.new.nameHint': "„/” zagnieżdża głębiej, np. frontend/components",
1568
1591
  'group.new.empty': "Podaj nazwę grupy",
1569
1592
  'group.new.root': "Najwyższy poziom",
1593
+ 'group.new.independent': "Nowa grupa najwyższego poziomu",
1570
1594
  'group.new.pickWorkspace': "Wybierz obszar roboczy",
1571
1595
  'group.new.noWorkspace': "Brak obszarów roboczych — najpierw dodaj jeden",
1572
1596
  'group.new.pickFirst': "Najpierw wybierz obszar roboczy",
@@ -1616,6 +1640,7 @@ window.__ModuleLoader__.load({
1616
1640
  'menu.renameFolder': 'Renomear pasta',
1617
1641
  'menu.renameSgroup': 'Renomear grupo de sessões',
1618
1642
  'menu.moveToGroup': 'Mover para o grupo…',
1643
+ 'menu.moveOutGroup': 'Remover do grupo',
1619
1644
  'settings.title': 'Espaços de trabalho melhores',
1620
1645
  'settings.desc': 'Aparência e recolhimento da árvore de espaços de trabalho',
1621
1646
  'settings.expand': 'Expandir',
@@ -1695,6 +1720,7 @@ window.__ModuleLoader__.load({
1695
1720
  'group.new.nameHint': "Uma \"/\" aninha mais, ex. frontend/components",
1696
1721
  'group.new.empty': "Digite um nome de grupo",
1697
1722
  'group.new.root': "Nível superior",
1723
+ 'group.new.independent': "Novo grupo de nível superior",
1698
1724
  'group.new.pickWorkspace': "Escolha uma área de trabalho",
1699
1725
  'group.new.noWorkspace': "Ainda não há áreas de trabalho — adicione uma primeiro",
1700
1726
  'group.new.pickFirst': "Escolha uma área de trabalho primeiro",
@@ -1744,6 +1770,7 @@ window.__ModuleLoader__.load({
1744
1770
  'menu.renameFolder': 'Переименовать папку',
1745
1771
  'menu.renameSgroup': 'Переименовать группу сессий',
1746
1772
  'menu.moveToGroup': 'Переместить в группу…',
1773
+ 'menu.moveOutGroup': 'Убрать из группы',
1747
1774
  'settings.title': 'Улучшенные рабочие области',
1748
1775
  'settings.desc': 'Внешний вид и сворачивание дерева рабочих областей',
1749
1776
  'settings.expand': 'Развернуть',
@@ -1823,6 +1850,7 @@ window.__ModuleLoader__.load({
1823
1850
  'group.new.nameHint': "«/» вкладывает глубже, напр. frontend/components",
1824
1851
  'group.new.empty': "Введите имя группы",
1825
1852
  'group.new.root': "Верхний уровень",
1853
+ 'group.new.independent': "Новая группа верхнего уровня",
1826
1854
  'group.new.pickWorkspace': "Выберите рабочую область",
1827
1855
  'group.new.noWorkspace': "Рабочих областей пока нет — сначала добавьте одну",
1828
1856
  'group.new.pickFirst': "Сначала выберите рабочую область",
@@ -1872,6 +1900,7 @@ window.__ModuleLoader__.load({
1872
1900
  'menu.renameFolder': 'Byt namn på mapp',
1873
1901
  'menu.renameSgroup': 'Byt namn på sessionsgrupp',
1874
1902
  'menu.moveToGroup': 'Flytta till grupp…',
1903
+ 'menu.moveOutGroup': 'Flytta ut ur gruppen',
1875
1904
  'settings.title': 'Bättre arbetsytor',
1876
1905
  'settings.desc': 'Utseende och ihopfällning för arbetsyteträdet',
1877
1906
  'settings.expand': 'Fäll ut',
@@ -1951,6 +1980,7 @@ window.__ModuleLoader__.load({
1951
1980
  'group.new.nameHint': "Ett \"/\" nästlar djupare, t.ex. frontend/components",
1952
1981
  'group.new.empty': "Ange ett gruppnamn",
1953
1982
  'group.new.root': "Översta nivån",
1983
+ 'group.new.independent': "Ny grupp på översta nivån",
1954
1984
  'group.new.pickWorkspace': "Välj en arbetsyta",
1955
1985
  'group.new.noWorkspace': "Inga arbetsytor än — lägg till en först",
1956
1986
  'group.new.pickFirst': "Välj en arbetsyta först",
@@ -2000,6 +2030,7 @@ window.__ModuleLoader__.load({
2000
2030
  'menu.renameFolder': 'เปลี่ยนชื่อโฟลเดอร์',
2001
2031
  'menu.renameSgroup': 'เปลี่ยนชื่อกลุ่มเซสชัน',
2002
2032
  'menu.moveToGroup': 'ย้ายไปยังกลุ่ม…',
2033
+ 'menu.moveOutGroup': 'ย้ายออกจากกลุ่ม',
2003
2034
  'settings.title': 'พื้นที่ทำงานที่ดีขึ้น',
2004
2035
  'settings.desc': 'รูปลักษณ์และการย่อของต้นไม้พื้นที่ทำงาน',
2005
2036
  'settings.expand': 'ขยาย',
@@ -2079,6 +2110,7 @@ window.__ModuleLoader__.load({
2079
2110
  'group.new.nameHint': "/ ใช้ซ้อนระดับลึกขึ้น เช่น frontend/components",
2080
2111
  'group.new.empty': "กรอกชื่อกลุ่ม",
2081
2112
  'group.new.root': "ระดับบนสุด",
2113
+ 'group.new.independent': "กลุ่มระดับบนสุดใหม่",
2082
2114
  'group.new.pickWorkspace': "เลือกเวิร์กสเปซ",
2083
2115
  'group.new.noWorkspace': "ยังไม่มีเวิร์กสเปซ — เพิ่มก่อน",
2084
2116
  'group.new.pickFirst': "เลือกเวิร์กสเปซก่อน",
@@ -2128,6 +2160,7 @@ window.__ModuleLoader__.load({
2128
2160
  'menu.renameFolder': 'Klasörü yeniden adlandır',
2129
2161
  'menu.renameSgroup': 'Oturum grubunu yeniden adlandır',
2130
2162
  'menu.moveToGroup': 'Gruba taşı…',
2163
+ 'menu.moveOutGroup': 'Gruptan çıkar',
2131
2164
  'settings.title': 'Daha iyi çalışma alanları',
2132
2165
  'settings.desc': 'Çalışma alanı ağacının görünümü ve daraltma tercihleri',
2133
2166
  'settings.expand': 'Genişlet',
@@ -2207,6 +2240,7 @@ window.__ModuleLoader__.load({
2207
2240
  'group.new.nameHint': "\"/\" daha derin iç içe koyar, örn. frontend/components",
2208
2241
  'group.new.empty': "Bir grup adı girin",
2209
2242
  'group.new.root': "En üst düzey",
2243
+ 'group.new.independent': "Yeni üst düzey grup",
2210
2244
  'group.new.pickWorkspace': "Bir çalışma alanı seçin",
2211
2245
  'group.new.noWorkspace': "Henüz çalışma alanı yok — önce bir tane ekleyin",
2212
2246
  'group.new.pickFirst': "Önce bir çalışma alanı seçin",
@@ -2256,6 +2290,7 @@ window.__ModuleLoader__.load({
2256
2290
  'menu.renameFolder': 'Đổi tên thư mục',
2257
2291
  'menu.renameSgroup': 'Đổi tên nhóm phiên',
2258
2292
  'menu.moveToGroup': 'Chuyển vào nhóm…',
2293
+ 'menu.moveOutGroup': 'Đưa ra khỏi nhóm',
2259
2294
  'settings.title': 'Không gian làm việc tốt hơn',
2260
2295
  'settings.desc': 'Giao diện và cách thu gọn của cây không gian làm việc',
2261
2296
  'settings.expand': 'Mở rộng',
@@ -2335,6 +2370,7 @@ window.__ModuleLoader__.load({
2335
2370
  'group.new.nameHint': "\"/\" lồng sâu hơn, ví dụ frontend/components",
2336
2371
  'group.new.empty': "Nhập tên nhóm",
2337
2372
  'group.new.root': "Cấp cao nhất",
2373
+ 'group.new.independent': "Nhóm cấp cao nhất mới",
2338
2374
  'group.new.pickWorkspace': "Chọn một không gian làm việc",
2339
2375
  'group.new.noWorkspace': "Chưa có không gian làm việc — hãy thêm trước",
2340
2376
  'group.new.pickFirst': "Chọn không gian làm việc trước",
@@ -2384,6 +2420,7 @@ window.__ModuleLoader__.load({
2384
2420
  'menu.renameFolder': '重新命名分組',
2385
2421
  'menu.renameSgroup': '重新命名對話分組',
2386
2422
  'menu.moveToGroup': '移動到分組…',
2423
+ 'menu.moveOutGroup': '移出分組',
2387
2424
  'settings.title': '更好嘅工作區',
2388
2425
  'settings.desc': '工作區樹嘅外觀同收起偏好',
2389
2426
  'settings.expand': '展開',
@@ -2463,6 +2500,7 @@ window.__ModuleLoader__.load({
2463
2500
  'group.new.nameHint': "名入面嘅 / 即嵌套層級,例如 前端/組件",
2464
2501
  'group.new.empty': "請輸入分組名",
2465
2502
  'group.new.root': "最頂層",
2503
+ 'group.new.independent': "新增頂層分組",
2466
2504
  'group.new.pickWorkspace': "揀一個工作區",
2467
2505
  'group.new.noWorkspace': "仲未有工作區,請先加一個",
2468
2506
  'group.new.pickFirst': "請先揀一個工作區",
@@ -2512,6 +2550,7 @@ window.__ModuleLoader__.load({
2512
2550
  'menu.renameFolder': '重新命名分組',
2513
2551
  'menu.renameSgroup': '重新命名對話分組',
2514
2552
  'menu.moveToGroup': '移動到分組…',
2553
+ 'menu.moveOutGroup': '移出分組',
2515
2554
  'settings.title': '更好嘅工作區',
2516
2555
  'settings.desc': '工作區樹嘅外觀同收起偏好',
2517
2556
  'settings.expand': '展開',
@@ -2591,6 +2630,7 @@ window.__ModuleLoader__.load({
2591
2630
  'group.new.nameHint': "名入面嘅 / 即嵌套層級,例如 前端/組件",
2592
2631
  'group.new.empty': "請輸入分組名",
2593
2632
  'group.new.root': "最頂層",
2633
+ 'group.new.independent': "新增頂層分組",
2594
2634
  'group.new.pickWorkspace': "揀一個工作區",
2595
2635
  'group.new.noWorkspace': "仲未有工作區,請先加一個",
2596
2636
  'group.new.pickFirst': "請先揀一個工作區",
@@ -2640,6 +2680,7 @@ window.__ModuleLoader__.load({
2640
2680
  'menu.renameFolder': '重新命名群組',
2641
2681
  'menu.renameSgroup': '重新命名工作階段群組',
2642
2682
  'menu.moveToGroup': '移動到分組…',
2683
+ 'menu.moveOutGroup': '移出分組',
2643
2684
  'settings.title': '更好的工作區',
2644
2685
  'settings.desc': '工作區樹的外觀與收合偏好',
2645
2686
  'settings.expand': '展開',
@@ -2719,6 +2760,7 @@ window.__ModuleLoader__.load({
2719
2760
  'group.new.nameHint': "名稱中的 / 即巢狀層級,例如 前端/元件",
2720
2761
  'group.new.empty': "請輸入分組名稱",
2721
2762
  'group.new.root': "最頂層",
2763
+ 'group.new.independent': "新增頂層分組",
2722
2764
  'group.new.pickWorkspace': "選擇一個工作區",
2723
2765
  'group.new.noWorkspace': "還沒有工作區,請先新增一個",
2724
2766
  'group.new.pickFirst': "請先選擇一個工作區",
@@ -3101,6 +3143,14 @@ window.__ModuleLoader__.load({
3101
3143
  return Array.isArray(raw) && raw.length > 0
3102
3144
  }
3103
3145
 
3146
+ /**
3147
+ * Display name of a tree folder node. compressTree materializes a
3148
+ * single-child chain as a `kind:'ws'` row, and THAT shape carries `segs`
3149
+ * but no `name` — reading .name alone rendered a blank row (a lone
3150
+ * "E" above its only member F came out empty).
3151
+ */
3152
+ const folderLabelOf = (node) => String(node.name || (Array.isArray(node.segs) ? node.segs.join('/') : '') || node.path || '')
3153
+
3104
3154
  const explicitSegsOf = (explicit, scope) => {
3105
3155
  const raw = explicit && typeof explicit === 'object' ? explicit[String(scope)] : undefined
3106
3156
  const out = []
@@ -3285,6 +3335,10 @@ window.__ModuleLoader__.load({
3285
3335
  // rim + a tinted wash so "this drop joins a group" reads stronger than
3286
3336
  // the reorder dashes above.
3287
3337
  '.bw-drop-into-strong{outline:2px solid var(--dsw-alias-brand-primary,#5b8def);outline-offset:-2px;background:color-mix(in srgb, var(--dsw-alias-brand-primary,#5b8def) 16%, transparent)}',
3338
+ // Session drag whose source sits in a group: a quiet dashed line at the
3339
+ // bottom of the tree says "anywhere outside a group = out of it". Kept
3340
+ // faint on purpose — the row outlines are the loud feedback.
3341
+ '.bw-root-drop-out::after{content:"";position:absolute;left:10px;right:10px;bottom:6px;border-bottom:1px dashed var(--dsw-alias-border-secondary,rgba(230,230,230,.35));pointer-events:none}',
3288
3342
  // v0.17.0: the new-group entry is an ICON button now (same family as the
3289
3343
  // search / add buttons beside it); the wording moved into its tooltip.
3290
3344
  '.bw-newgroup-btn{flex:none}',
@@ -3296,6 +3350,10 @@ window.__ModuleLoader__.load({
3296
3350
  '.bw-gn-hint{font-size:12px;color:var(--dsw-alias-label-tertiary,#9a9a9a)}',
3297
3351
  // A container-only node (a workspace name-group in the session layer):
3298
3352
  // still expandable, but it carries no workspaceId to file against.
3353
+ // The trailing "new top-level group" row: separated from the containers
3354
+ // above it, because it belongs to none of them.
3355
+ '.bw-gn-row-new{margin-top:4px;border-top:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.22));border-radius:0 0 6px 6px;padding-top:4px}',
3356
+ '.bw-gn-row-new .bw-gn-label{color:var(--dsw-alias-label-secondary,#b8b8b8)}',
3299
3357
  '.bw-gn-row-hold{cursor:default}',
3300
3358
  '.bw-gn-row-hold:hover{background:transparent}',
3301
3359
  '.bw-gn-row-hold .bw-gn-label{color:var(--dsw-alias-label-secondary,#b8b8b8)}',
@@ -3834,7 +3892,7 @@ window.__ModuleLoader__.load({
3834
3892
  * writes the declaration itself, which is why it is not the move dialog.
3835
3893
  */
3836
3894
  function GroupNewDialog(props) {
3837
- const { kinds, initialKind, treeOf, onConfirm, onClose, t } = props
3895
+ const { kinds, initialKind, treeOf, label, onConfirm, onClose, t } = props
3838
3896
  const [kind, setKind] = React.useState(initialKind)
3839
3897
  const [picked, setPicked] = React.useState(null)
3840
3898
  const [openKeys, setOpenKeys] = React.useState({})
@@ -3859,7 +3917,7 @@ window.__ModuleLoader__.load({
3859
3917
  const expanded = openKeys[node.id] === true || (depth === 0 && openKeys[node.id] !== false)
3860
3918
  out.push(E('div', {
3861
3919
  key: 'n-' + node.id,
3862
- className: cls('bw-gn-row', node.pickable === false && 'bw-gn-row-hold', picked && picked.id === node.id && 'bw-gn-row-on'),
3920
+ className: cls('bw-gn-row', node.pickable === false && 'bw-gn-row-hold', node.kind === 'new' && 'bw-gn-row-new', picked && picked.id === node.id && 'bw-gn-row-on'),
3863
3921
  style: { paddingLeft: 6 + depth * 14 },
3864
3922
  role: 'treeitem',
3865
3923
  'aria-selected': picked && picked.id === node.id ? 'true' : 'false',
@@ -3872,7 +3930,10 @@ window.__ModuleLoader__.load({
3872
3930
  'aria-label': node.label,
3873
3931
  onClick: (e) => { e.stopPropagation(); toggle(node.id) },
3874
3932
  }, E('span', { className: 'bw-chevron' })) : E('span', { className: 'bw-gn-twist' }),
3875
- E('span', { className: 'bw-gn-icon' }, node.kind === 'ws' ? icon('IconFolderOpen16', 15) : icon('IconFolderClose16', 15)),
3933
+ E('span', { className: 'bw-gn-icon' },
3934
+ node.kind === 'new' ? icon('IconPlusOutline16', 15)
3935
+ : node.kind === 'ws' ? icon('IconFolderOpen16', 15)
3936
+ : icon('IconFolderClose16', 15)),
3876
3937
  E('span', { className: 'bw-gn-label' }, node.label),
3877
3938
  ))
3878
3939
  if (hasKids && expanded) out.push(...renderNodes(node.children, depth + 1))
@@ -3883,7 +3944,7 @@ window.__ModuleLoader__.load({
3883
3944
  open: true,
3884
3945
  onClose,
3885
3946
  closeLabel: t('close'),
3886
- title: t('group.new.title'),
3947
+ title: label,
3887
3948
  footer: E('div', { className: 'bw-modal-actions' },
3888
3949
  E(BTN, { variant: 'outline', onClick: onClose }, t('cancel')),
3889
3950
  E(BTN, { variant: 'primary', onClick: submit }, t('confirm')),
@@ -4210,13 +4271,13 @@ window.__ModuleLoader__.load({
4210
4271
  )
4211
4272
  }
4212
4273
 
4213
- function WorkspaceRow({ workspace, depth, count, sessionsOpen, onToggle, onStart, onContextMenu, currentInside, dropHalf, dragEvents, custStyle, iconMode, pulse, t }) {
4274
+ function WorkspaceRow({ workspace, depth, count, sessionsOpen, onToggle, onStart, onContextMenu, currentInside, dropHalf, dropInto, dragEvents, custStyle, iconMode, pulse, t }) {
4214
4275
  const iconEl = iconOf(iconMode, sessionsOpen)
4215
4276
  const iconChild = pulse
4216
4277
  ? E(PulseGlow, { state: pulse }, iconEl || (typeof ui.StateDot === 'function' ? E(ui.StateDot, { state: pulse, size: 10 }) : null))
4217
4278
  : iconEl
4218
4279
  return E('div', {
4219
- className: cls('bw-row', currentInside && 'bw-row-current', dropHalf === 'before' && 'bw-drop-before', dropHalf === 'after' && 'bw-drop-after'),
4280
+ className: cls('bw-row', currentInside && 'bw-row-current', dropHalf === 'before' && 'bw-drop-before', dropHalf === 'after' && 'bw-drop-after', dropInto && 'bw-drop-into', dropInto && 'bw-drop-into-strong'),
4220
4281
  style: { paddingLeft: 6 + depth * 12, ...(custStyle || {}), ...(pulse ? { '--bw-pulse-color': PULSE_COLORS[pulse] || PULSE_COLORS.ongoing } : null) },
4221
4282
  onClick: onToggle,
4222
4283
  onContextMenu: onContextMenu,
@@ -5170,6 +5231,13 @@ window.__ModuleLoader__.load({
5170
5231
  if (!compactChains || draggingWorkspace) return built
5171
5232
  return { ...built, folders: built.folders.map((f) => materializeChain(compressTree(f))), workspaces: built.workspaces }
5172
5233
  }, [items, compactChains, draggingWorkspace, workspaceSlash, wsFolders])
5234
+ // UNCOMPRESSED tree, for the new-group dialog only. compressTree merges a
5235
+ // single-child chain into one row that stands for BOTH a group and the
5236
+ // workspace under it — fine for drawing, useless for choosing a
5237
+ // container: one half must stay unselectable and the other must be
5238
+ // selectable, and a merged row cannot be both. (It also carried no
5239
+ // `name`, which is how a lone "E" rendered blank.)
5240
+ const rawTree = React.useMemo(() => buildTree(items, workspaceSlash, wsFolders), [items, workspaceSlash, wsFolders])
5173
5241
  // Disk-layer ancestry for drag semantics (same derivation as buildTree).
5174
5242
  const diskParentMap = React.useMemo(() => diskParentMapOf(items), [items])
5175
5243
 
@@ -5329,6 +5397,18 @@ window.__ModuleLoader__.load({
5329
5397
  const s = splitTitleSegs(String(title || ''))
5330
5398
  return s.length > 0 ? s[s.length - 1] : (fallback || String(title || ''))
5331
5399
  }
5400
+ /**
5401
+ * Retitle `title` so its leaf lands in the group `anchorTitle` lives in
5402
+ * (v0.18.0). '' as the anchor group means the root level. Used by the
5403
+ * cross-group session drop: the visible order of session rows is decided
5404
+ * by these groups, so a drop that only reorders the flat list would leave
5405
+ * the row exactly where it was.
5406
+ */
5407
+ const titleMovedToGroup = (title, anchorTitle) => {
5408
+ const leaf = leafOf(title)
5409
+ const group = groupPrefixOf(anchorTitle)
5410
+ return group ? group + '/' + leaf : leaf
5411
+ }
5332
5412
  /**
5333
5413
  * Every existing group path, deduplicated (datalist source). `kind`
5334
5414
  * narrows it to the name layer the dialog is about: workspace rows
@@ -5371,19 +5451,16 @@ window.__ModuleLoader__.load({
5371
5451
  // write target: `scope` is the store's bags key (the disk level's idPrefix
5372
5452
  // for workspaces, the workspaceId for sessions) and `path` is the parent
5373
5453
  // group's name path — the dialog appends the new name to it.
5374
- const wsGroupNodesOf = (node) => {
5375
- const out = []
5376
- for (const f of node.folders || []) {
5377
- out.push({ id: f.scope + '|' + f.path, scope: f.scope, path: f.path, label: f.name, kind: 'folder', children: wsGroupNodesOf(f) })
5378
- }
5379
- for (const w of node.workspaces || []) {
5380
- out.push({
5381
- id: w.subPrefix + '|', scope: w.subPrefix, path: '', label: w.leaf || w.title, kind: 'ws',
5382
- children: w.sub ? wsGroupNodesOf(w.sub) : [],
5383
- })
5384
- }
5385
- return out
5386
- }
5454
+ // Workspace-GROUP containers: name-group folders only, never the
5455
+ // workspaces themselves. A workspace title is "group/leaf", so with
5456
+ // members A/B, A/C, D and E/F the only groups that exist are A and E —
5457
+ // B, C, D and F are leaves (they are workspaces, not containers), and
5458
+ // offering them would invite a group named after a leaf. Disk nesting is
5459
+ // a workspace fact too, so it is skipped for the same reason.
5460
+ const wsFolderNodesOf = (node) => (node.folders || []).map((f) => ({
5461
+ id: f.scope + '|' + f.path, scope: f.scope, path: f.path, label: folderLabelOf(f), kind: 'folder',
5462
+ children: wsFolderNodesOf(f),
5463
+ }))
5387
5464
  const sessGroupNodesOf = (node, workspaceId) => (node.groups || []).map((g) => ({
5388
5465
  id: workspaceId + '|' + g.path, scope: workspaceId, path: g.path, label: g.name, kind: 'sgroup',
5389
5466
  children: sessGroupNodesOf(g, workspaceId),
@@ -5399,12 +5476,18 @@ window.__ModuleLoader__.load({
5399
5476
  if (kind === 'workspace') {
5400
5477
  // "Top level" IS a real target here (a group at the root of the
5401
5478
  // workspace tree), so it is a node like any other.
5479
+ // A dedicated top-level row, not a root node that both expands AND
5480
+ // accepts the new name: that read as "pick a spot inside the top
5481
+ // level", and it left no way to say "give me a group NEXT TO A and
5482
+ // E". The row below is that way, and A / E sit beside it as the
5483
+ // top-level groups they actually are.
5484
+ const groups = treeView ? wsFolderNodesOf(rawTree) : []
5402
5485
  return {
5403
- hint: null,
5404
- nodes: [{
5405
- id: '|', scope: '', path: '', label: t('group.new.root'), kind: 'folder',
5406
- children: treeView ? wsGroupNodesOf(tree) : [],
5407
- }],
5486
+ hint: t('group.new.where'),
5487
+ // The "top level" row sits LAST (user's call): it reads as "and
5488
+ // here is one more, belonging to none of the above" instead of as
5489
+ // a header the others hang under.
5490
+ nodes: [...groups, { id: '@top', scope: '', path: '', label: t('group.new.independent'), kind: 'new', children: [] }],
5408
5491
  }
5409
5492
  }
5410
5493
  // A session group lives INSIDE one workspace (folders.sess is keyed by
@@ -5422,7 +5505,7 @@ window.__ModuleLoader__.load({
5422
5505
  const sessNodesOf = (node) => {
5423
5506
  const out = []
5424
5507
  for (const f of node.folders || []) {
5425
- out.push({ id: f.scope + '|' + f.path, scope: '', path: '', label: f.name, kind: 'folder', pickable: false, children: sessNodesOf(f) })
5508
+ out.push({ id: f.scope + '|' + f.path, scope: '', path: '', label: folderLabelOf(f), kind: 'folder', pickable: false, children: sessNodesOf(f) })
5426
5509
  }
5427
5510
  for (const w of node.workspaces || []) {
5428
5511
  const inner = sessGroupNodesOf(buildSessionTree(sessionsOf(w), sessionSlash, sessFolders, w.workspaceId), w.workspaceId)
@@ -5434,7 +5517,7 @@ window.__ModuleLoader__.load({
5434
5517
  }
5435
5518
  return out
5436
5519
  }
5437
- if (treeView) return { hint: t('group.new.pickWorkspace'), nodes: sessNodesOf(tree) }
5520
+ if (treeView) return { hint: t('group.new.pickWorkspace'), nodes: sessNodesOf(rawTree) }
5438
5521
  // One-level modes: every workspace sits at the root, exactly as the
5439
5522
  // sidebar draws it (raw items — there is no .sub level to nest here).
5440
5523
  const flat = []
@@ -5615,6 +5698,22 @@ window.__ModuleLoader__.load({
5615
5698
  setDrag(null)
5616
5699
  },
5617
5700
  onDragOver: (event) => {
5701
+ // A SESSION dragged onto the workspace row lands at the root level of
5702
+ // its session tree — the way OUT of any group (v0.18.0). Only for
5703
+ // rows that actually sit in a group: a top-level session gains
5704
+ // nothing here, so this row must not light up for it (the browser
5705
+ // then shows its plain "no drop" cursor, which tells the truth).
5706
+ if (dragMatches('session')) {
5707
+ if (!sessionSlash || drag.source.workspaceId !== workspace.workspaceId) return
5708
+ if (leafOf(drag.source.title) === drag.source.title) return
5709
+ event.preventDefault()
5710
+ event.stopPropagation()
5711
+ try { event.dataTransfer.dropEffect = 'move' } catch { }
5712
+ setDrag(current => (current && current.over && current.over.kind === 'ws-root' && current.over.target === workspace.workspaceId)
5713
+ ? current
5714
+ : (current ? { ...current, over: { kind: 'ws-root', target: workspace.workspaceId } } : current))
5715
+ return
5716
+ }
5618
5717
  if (!dragMatches('workspace')) return
5619
5718
  event.preventDefault()
5620
5719
  event.stopPropagation()
@@ -5625,6 +5724,14 @@ window.__ModuleLoader__.load({
5625
5724
  : (current ? { ...current, over: { kind: 'workspace', target: workspace.workspaceId, half } } : current))
5626
5725
  },
5627
5726
  onDrop: (event) => {
5727
+ if (dragMatches('session')) {
5728
+ if (!sessionSlash || drag.source.workspaceId !== workspace.workspaceId) return
5729
+ if (leafOf(drag.source.title) === drag.source.title) return
5730
+ event.preventDefault()
5731
+ event.stopPropagation()
5732
+ commitSessionMoveOut(workspace.workspaceId)
5733
+ return
5734
+ }
5628
5735
  if (!dragMatches('workspace')) return
5629
5736
  event.preventDefault()
5630
5737
  event.stopPropagation()
@@ -5768,17 +5875,55 @@ window.__ModuleLoader__.load({
5768
5875
  const anchor = half === 'after'
5769
5876
  ? (index === -1 ? undefined : (flat[index + 1] ? flat[index + 1].id : undefined))
5770
5877
  : targetSessionId
5771
- if (typeof insertSessionBefore === 'function') {
5772
- Promise.resolve()
5773
- .then(() => (anchor !== undefined ? insertSessionBefore(workspaceId, source.sessionId, anchor) : insertSessionBefore(workspaceId, source.sessionId)))
5774
- .catch(fail)
5775
- return
5776
- }
5777
- // Host channel gone: commit the same move into the browser-local order.
5778
- if (actions && typeof actions.setSessionOrder === 'function') {
5779
- actions.setSessionOrder(workspaceId, reorderIds(flat.map((s) => s.id), source.sessionId, anchor))
5878
+ // v0.18.0 a drop on a row of ANOTHER group MOVES the row (the name
5879
+ // prefix is rewritten to the target row's group). Until then only the
5880
+ // flat list was reordered while the visible order is decided by the
5881
+ // groups, so a cross-group drop left the row exactly where it was and
5882
+ // read as "the drag did nothing" (user report). Same group keeps the
5883
+ // pure reorder; with the name layer off every group is '' — which is
5884
+ // the same thing.
5885
+ const target = flat.find(s => s.id === targetSessionId)
5886
+ const nextTitle = sessionSlash && target ? titleMovedToGroup(source.title, target.title) : null
5887
+ const reorder = () => {
5888
+ if (typeof insertSessionBefore === 'function') {
5889
+ return (anchor !== undefined ? insertSessionBefore(workspaceId, source.sessionId, anchor) : insertSessionBefore(workspaceId, source.sessionId))
5890
+ }
5891
+ // Host channel gone: commit the same move into the browser-local order.
5892
+ if (actions && typeof actions.setSessionOrder === 'function') {
5893
+ actions.setSessionOrder(workspaceId, reorderIds(flat.map((s) => s.id), source.sessionId, anchor))
5894
+ }
5895
+ return undefined
5780
5896
  }
5897
+ const chain = nextTitle !== null && nextTitle !== source.title
5898
+ ? Promise.resolve().then(() => renameByUser(source.sessionId, nextTitle))
5899
+ : Promise.resolve()
5900
+ chain.then(reorder).catch(fail)
5901
+ }
5902
+ /**
5903
+ * Drop a session ON ITS OWN WORKSPACE ROW = leave the group (v0.18.0).
5904
+ * The workspace row is the root of the session tree, so it is the one
5905
+ * drop target that always exists OUTSIDE every group — the user's "drag
5906
+ * it out of 测试1" gesture had nowhere to land before this.
5907
+ */
5908
+ const commitSessionMoveOut = (workspaceId) => {
5909
+ const source = drag.source
5910
+ setDrag(null)
5911
+ if (!source || source.workspaceId !== workspaceId) return
5912
+ const leaf = leafOf(source.title)
5913
+ if (leaf === source.title) return
5914
+ Promise.resolve()
5915
+ .then(() => renameByUser(source.sessionId, leaf))
5916
+ .then(() => {
5917
+ // Append to the end of the workspace's flat order: the row now
5918
+ // lives at the root level, and the host order is what the tree
5919
+ // renders there — without this it would surface somewhere in the
5920
+ // middle of the top-level rows, wherever its id happens to sit.
5921
+ if (typeof insertSessionBefore === 'function') return insertSessionBefore(workspaceId, source.sessionId)
5922
+ return undefined
5923
+ })
5924
+ .catch(fail)
5781
5925
  }
5926
+ const wsRootDropActive = (workspaceId) => dragMatches('session') && drag.over !== null && drag.over.kind === 'ws-root' && drag.over.target === workspaceId
5782
5927
  const commitSessionMoveInto = (workspaceId, groupPath) => {
5783
5928
  const source = drag.source
5784
5929
  setDrag(null)
@@ -5993,6 +6138,7 @@ window.__ModuleLoader__.load({
5993
6138
  onStart: () => { actions.setSessionsExpanded(workspace.workspaceId, true); startSession(workspace.workspaceId) },
5994
6139
  onContextMenu: (e) => openCtx('workspace', workspace, e),
5995
6140
  dropHalf: wsDropHalf(workspace.workspaceId),
6141
+ dropInto: wsRootDropActive(workspace.workspaceId),
5996
6142
  dragEvents: workspaceDragEvents(workspace),
5997
6143
  custStyle: rowStyleOf('workspace:' + workspace.workspaceId),
5998
6144
  iconMode: (styleEntry('workspace:' + workspace.workspaceId) || {}).icon || 'solid',
@@ -6112,8 +6258,14 @@ window.__ModuleLoader__.load({
6112
6258
  // probing it silently yielded a glyph-less row. IconFolderOpenOutline16
6113
6259
  // is present and reads as "move into a group" just as well.
6114
6260
  const groupItem = { id: 'move-group', label: t('menu.moveToGroup'), icon: icon('IconFolderOpenOutline16', 16) }
6261
+ // "Leave the group" (v0.18.0) — the counterpart of move-group, offered
6262
+ // only where a group actually exists to leave: a SESSION row whose own
6263
+ // title carries a "/" prefix, with the session name layer on. A
6264
+ // top-level row gets no entry (there is nothing to leave).
6265
+ const ungroupItem = { id: 'move-out', label: t('menu.moveOutGroup'), icon: icon('IconRightUpOutline16', 16) }
6115
6266
  const wsGroupItems = workspaceSlash ? [groupItem] : []
6116
6267
  const sgroupItems = sessionSlash ? [groupItem] : []
6268
+ const sessionUngroupItems = sessionSlash && ctx.kind === 'session' && ctx.payload && groupPrefixOf(String(ctx.payload.title || '')) !== '' ? [ungroupItem] : []
6117
6269
  // "Delete group" only makes sense for a DECLARED group that holds
6118
6270
  // nothing (v0.17.0): a group with members is a projection of their
6119
6271
  // titles, so removing the declaration would change nothing on screen
@@ -6142,6 +6294,7 @@ window.__ModuleLoader__.load({
6142
6294
  // deliberately NOT styled destructive.
6143
6295
  { id: 'archive', label: ot ? ot('menu.archiveSession') : t('menu.archive'), icon: icon('IconArchiveOutline20', 16) },
6144
6296
  { type: 'separator', id: 'bw-sep' },
6297
+ ...sessionUngroupItems,
6145
6298
  ...sgroupItems,
6146
6299
  customizeEntry,
6147
6300
  ]
@@ -6184,6 +6337,12 @@ window.__ModuleLoader__.load({
6184
6337
  else if (kind === 'session' && id === 'rename') setDialog({ kind: 'sess-rename', session: payload })
6185
6338
  else if (kind === 'session' && id === 'fork') forkSession(payload.id)
6186
6339
  else if (kind === 'session' && id === 'archive') { Promise.resolve().then(() => archiveSession(payload.id)).catch(fail) }
6340
+ // Plain retitle back to the leaf: the row leaves its group and returns
6341
+ // to the top level of its workspace (v0.18.0).
6342
+ else if (kind === 'session' && id === 'move-out') {
6343
+ const leaf = leafOf(String(payload.title || ''))
6344
+ if (leaf !== payload.title) Promise.resolve().then(() => renameByUser(payload.id, leaf)).catch(fail)
6345
+ }
6187
6346
  }
6188
6347
 
6189
6348
  /* --------------------------- dialogs --------------------------- */
@@ -6272,6 +6431,10 @@ window.__ModuleLoader__.load({
6272
6431
  kinds: newGroupLayers(),
6273
6432
  initialKind: dialog.layer === 'workspace' ? 'workspace' : 'session',
6274
6433
  treeOf: newGroupTreeOf,
6434
+ // The heading names the layer(s) the button promised: with both
6435
+ // layers live the switch below decides, so the generic wording is
6436
+ // accurate there and only there.
6437
+ label: newGroupLabel(),
6275
6438
  onConfirm: (kind, scope, path) => { actions.addFolder(kind, scope, path); setDialog(null) },
6276
6439
  onClose: () => setDialog(null),
6277
6440
  t,
@@ -6343,7 +6506,27 @@ window.__ModuleLoader__.load({
6343
6506
  .catch((reason) => { fail(messageOf(reason)) })
6344
6507
  }
6345
6508
 
6346
- return E('div', { className: 'bw-root' },
6509
+ // v0.18.0 "drag it OUT" must never land on nothing. The row level
6510
+ // already covers the workspace row and the root-level session rows; this
6511
+ // catches everything else (the empty space below the tree, the header
6512
+ // gap), so a drop outside every group always means the same thing:
6513
+ // leave the group. Row handlers stopPropagation, so this only fires
6514
+ // where no row claimed the drop.
6515
+ const sessionCouldLeaveGroup = dragMatches('session') && sessionSlash && drag.source && leafOf(drag.source.title) !== drag.source.title
6516
+ const rootDropEvents = {
6517
+ onDragOver: (event) => {
6518
+ if (!sessionCouldLeaveGroup) return
6519
+ event.preventDefault()
6520
+ try { event.dataTransfer.dropEffect = 'move' } catch { }
6521
+ },
6522
+ onDrop: (event) => {
6523
+ if (!sessionCouldLeaveGroup) return
6524
+ event.preventDefault()
6525
+ commitSessionMoveOut(drag.source.workspaceId)
6526
+ },
6527
+ }
6528
+
6529
+ return E('div', { className: cls('bw-root', sessionCouldLeaveGroup && 'bw-root-drop-out'), ...rootDropEvents },
6347
6530
  StyleNode(),
6348
6531
  E('div', { className: 'bw-header' },
6349
6532
  E('div', { className: 'bw-header-title' }, t('title')),