dsh-better-workspace 0.16.0 → 0.17.1

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/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-better-workspace",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
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.16.0",
3
+ "version": "0.17.1",
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
@@ -142,6 +142,21 @@ window.__ModuleLoader__.load({
142
142
  'group.move.title': '移动到分组',
143
143
  'group.pick': '选择已有分组',
144
144
  'group.new': '新建分组',
145
+ 'group.new.title': "新建分组",
146
+ 'group.new.sess': "会话分组",
147
+ 'group.new.ws': "工作区分组",
148
+ 'group.new.both': "新建会话或工作区分组",
149
+ 'group.new.where': "选择创建位置",
150
+ 'group.new.placeholder': "分组名称,可用 / 嵌套",
151
+ 'group.new.nameHint': "名称中的 / 即嵌套层级,例如 前端/组件",
152
+ 'group.new.empty': "请输入分组名称",
153
+ 'group.new.root': "顶层",
154
+ 'group.new.pickWorkspace': "选择一个工作区",
155
+ 'group.new.noWorkspace': "还没有工作区,请先添加一个",
156
+ 'group.new.pickFirst': "请先选择一个工作区",
157
+ 'group.delete.title': "删除分组",
158
+ 'group.delete.body': "「{name}」是空分组,删除后不可恢复。",
159
+ 'menu.deleteGroup': "删除分组",
145
160
  }
146
161
 
147
162
  const en = {
@@ -255,6 +270,21 @@ window.__ModuleLoader__.load({
255
270
  'group.move.title': 'Move to group',
256
271
  'group.pick': 'Pick an existing group',
257
272
  'group.new': 'New group',
273
+ 'group.new.title': "New group",
274
+ 'group.new.sess': "Session group",
275
+ 'group.new.ws': "Workspace group",
276
+ 'group.new.both': "New session or workspace group",
277
+ 'group.new.where': "Choose where to create it",
278
+ 'group.new.placeholder': "Group name, \"/\" nests",
279
+ 'group.new.nameHint': "A \"/\" nests further, e.g. frontend/components",
280
+ 'group.new.empty': "Enter a group name",
281
+ 'group.new.root': "Top level",
282
+ 'group.new.pickWorkspace': "Pick a workspace",
283
+ 'group.new.noWorkspace': "No workspaces yet — add one first",
284
+ 'group.new.pickFirst': "Pick a workspace first",
285
+ 'group.delete.title': "Delete group",
286
+ 'group.delete.body': "\"{name}\" is an empty group; deleting it cannot be undone.",
287
+ 'menu.deleteGroup': "Delete group",
258
288
  }
259
289
 
260
290
  /* Third-language dictionaries, keyed by BCP 47 tag. Every entry must carry
@@ -376,6 +406,21 @@ window.__ModuleLoader__.load({
376
406
  'group.move.title': 'نقل إلى مجموعة',
377
407
  'group.pick': 'اختر مجموعة موجودة',
378
408
  'group.new': 'مجموعة جديدة',
409
+ 'group.new.title': "مجموعة جديدة",
410
+ 'group.new.sess': "مجموعة جلسات",
411
+ 'group.new.ws': "مجموعة مساحات عمل",
412
+ 'group.new.both': "مجموعة جلسات أو مساحات عمل جديدة",
413
+ 'group.new.where': "اختر مكان الإنشاء",
414
+ 'group.new.placeholder': "اسم المجموعة، \"/\" للتداخل",
415
+ 'group.new.nameHint': "الشرطة المائلة \"/\" تنشئ مستوى أعمق، مثل frontend/components",
416
+ 'group.new.empty': "أدخل اسم المجموعة",
417
+ 'group.new.root': "المستوى الأعلى",
418
+ 'group.new.pickWorkspace': "اختر مساحة عمل",
419
+ 'group.new.noWorkspace': "لا توجد مساحات عمل بعد — أضف واحدة أولاً",
420
+ 'group.new.pickFirst': "اختر مساحة عمل أولاً",
421
+ 'group.delete.title': "حذف المجموعة",
422
+ 'group.delete.body': "\"{name}\" مجموعة فارغة؛ لا يمكن التراجع عن الحذف.",
423
+ 'menu.deleteGroup': "حذف المجموعة",
379
424
  },
380
425
  /* locale: de */
381
426
  'de': {
@@ -489,6 +534,21 @@ window.__ModuleLoader__.load({
489
534
  'group.move.title': 'In Gruppe verschieben',
490
535
  'group.pick': 'Vorhandene Gruppe wählen',
491
536
  'group.new': 'Neue Gruppe',
537
+ 'group.new.title': "Neue Gruppe",
538
+ 'group.new.sess': "Sitzungsgruppe",
539
+ 'group.new.ws': "Arbeitsbereichsgruppe",
540
+ 'group.new.both': "Neue Sitzungs- oder Arbeitsbereichsgruppe",
541
+ 'group.new.where': "Speicherort wählen",
542
+ 'group.new.placeholder': "Gruppenname, „/\" verschachtelt",
543
+ 'group.new.nameHint': "Ein „/\" verschachtelt weiter, z. B. frontend/components",
544
+ 'group.new.empty': "Gruppennamen eingeben",
545
+ 'group.new.root': "Oberste Ebene",
546
+ 'group.new.pickWorkspace': "Arbeitsbereich wählen",
547
+ 'group.new.noWorkspace': "Noch keine Arbeitsbereiche — zuerst einen hinzufügen",
548
+ 'group.new.pickFirst': "Zuerst einen Arbeitsbereich wählen",
549
+ 'group.delete.title': "Gruppe löschen",
550
+ 'group.delete.body': "„{name}\" ist eine leere Gruppe; das Löschen kann nicht rückgängig gemacht werden.",
551
+ 'menu.deleteGroup': "Gruppe löschen",
492
552
  },
493
553
  /* locale: fr */
494
554
  'fr': {
@@ -602,6 +662,21 @@ window.__ModuleLoader__.load({
602
662
  'group.move.title': 'Déplacer vers le groupe',
603
663
  'group.pick': 'Choisir un groupe existant',
604
664
  'group.new': 'Nouveau groupe',
665
+ 'group.new.title': "Nouveau groupe",
666
+ 'group.new.sess': "Groupe de sessions",
667
+ 'group.new.ws': "Groupe d’espaces de travail",
668
+ 'group.new.both': "Nouveau groupe de sessions ou d’espaces de travail",
669
+ 'group.new.where': "Choisir l’emplacement",
670
+ 'group.new.placeholder': "Nom du groupe, « / » imbrique",
671
+ 'group.new.nameHint': "Un « / » imbrique davantage, par ex. frontend/components",
672
+ 'group.new.empty': "Saisissez un nom de groupe",
673
+ 'group.new.root': "Niveau supérieur",
674
+ 'group.new.pickWorkspace': "Choisissez un espace de travail",
675
+ 'group.new.noWorkspace': "Aucun espace de travail — ajoutez-en un d’abord",
676
+ 'group.new.pickFirst': "Choisissez d’abord un espace de travail",
677
+ 'group.delete.title': "Supprimer le groupe",
678
+ 'group.delete.body': "« {name} » est un groupe vide ; la suppression est irréversible.",
679
+ 'menu.deleteGroup': "Supprimer le groupe",
605
680
  },
606
681
  /* locale: hi */
607
682
  'hi': {
@@ -715,6 +790,21 @@ window.__ModuleLoader__.load({
715
790
  'group.move.title': 'समूह में ले जाएँ',
716
791
  'group.pick': 'मौजूदा समूह चुनें',
717
792
  'group.new': 'नया समूह',
793
+ 'group.new.title': "नया समूह",
794
+ 'group.new.sess': "सत्र समूह",
795
+ 'group.new.ws': "कार्यस्थान समूह",
796
+ 'group.new.both': "नया सत्र या कार्यस्थान समूह",
797
+ 'group.new.where': "बनाने का स्थान चुनें",
798
+ 'group.new.placeholder': "समूह का नाम, \"/\" से नेस्टिंग",
799
+ 'group.new.nameHint': "\"/\" और गहरा स्तर बनाता है, जैसे frontend/components",
800
+ 'group.new.empty': "समूह का नाम दर्ज करें",
801
+ 'group.new.root': "शीर्ष स्तर",
802
+ 'group.new.pickWorkspace': "एक कार्यस्थान चुनें",
803
+ 'group.new.noWorkspace': "अभी कोई कार्यस्थान नहीं — पहले एक जोड़ें",
804
+ 'group.new.pickFirst': "पहले एक कार्यस्थान चुनें",
805
+ 'group.delete.title': "समूह हटाएँ",
806
+ 'group.delete.body': "\"{name}\" एक खाली समूह है; हटाने पर इसे वापस नहीं लाया जा सकता।",
807
+ 'menu.deleteGroup': "समूह हटाएँ",
718
808
  },
719
809
  /* locale: id */
720
810
  'id': {
@@ -828,6 +918,21 @@ window.__ModuleLoader__.load({
828
918
  'group.move.title': 'Pindahkan ke grup',
829
919
  'group.pick': 'Pilih grup yang ada',
830
920
  'group.new': 'Grup baru',
921
+ 'group.new.title': "Grup baru",
922
+ 'group.new.sess': "Grup sesi",
923
+ 'group.new.ws': "Grup ruang kerja",
924
+ 'group.new.both': "Grup sesi atau ruang kerja baru",
925
+ 'group.new.where': "Pilih lokasi pembuatan",
926
+ 'group.new.placeholder': "Nama grup, \"/\" untuk bersarang",
927
+ 'group.new.nameHint': "\"/\" membuat tingkat lebih dalam, mis. frontend/components",
928
+ 'group.new.empty': "Masukkan nama grup",
929
+ 'group.new.root': "Tingkat teratas",
930
+ 'group.new.pickWorkspace': "Pilih ruang kerja",
931
+ 'group.new.noWorkspace': "Belum ada ruang kerja — tambahkan dulu",
932
+ 'group.new.pickFirst': "Pilih ruang kerja dulu",
933
+ 'group.delete.title': "Hapus grup",
934
+ 'group.delete.body': "\"{name}\" adalah grup kosong; penghapusan tidak bisa dibatalkan.",
935
+ 'menu.deleteGroup': "Hapus grup",
831
936
  },
832
937
  /* locale: it */
833
938
  'it': {
@@ -941,6 +1046,21 @@ window.__ModuleLoader__.load({
941
1046
  'group.move.title': 'Sposta nel gruppo',
942
1047
  'group.pick': 'Scegli un gruppo esistente',
943
1048
  'group.new': 'Nuovo gruppo',
1049
+ 'group.new.title': "Nuovo gruppo",
1050
+ 'group.new.sess': "Gruppo di sessioni",
1051
+ 'group.new.ws': "Gruppo di aree di lavoro",
1052
+ 'group.new.both': "Nuovo gruppo di sessioni o aree di lavoro",
1053
+ 'group.new.where': "Scegli dove crearlo",
1054
+ 'group.new.placeholder': "Nome del gruppo, «/» annida",
1055
+ 'group.new.nameHint': "Una «/» annida ulteriormente, es. frontend/components",
1056
+ 'group.new.empty': "Inserisci un nome di gruppo",
1057
+ 'group.new.root': "Livello superiore",
1058
+ 'group.new.pickWorkspace': "Scegli un’area di lavoro",
1059
+ 'group.new.noWorkspace': "Nessuna area di lavoro — aggiungine una prima",
1060
+ 'group.new.pickFirst': "Scegli prima un’area di lavoro",
1061
+ 'group.delete.title': "Elimina gruppo",
1062
+ 'group.delete.body': "«{name}» è un gruppo vuoto; l’eliminazione non è reversibile.",
1063
+ 'menu.deleteGroup': "Elimina gruppo",
944
1064
  },
945
1065
  /* locale: ja */
946
1066
  'ja': {
@@ -1054,6 +1174,21 @@ window.__ModuleLoader__.load({
1054
1174
  'group.move.title': 'グループに移動',
1055
1175
  'group.pick': '既存のグループを選択',
1056
1176
  'group.new': '新しいグループ',
1177
+ 'group.new.title': "新しいグループ",
1178
+ 'group.new.sess': "セッショングループ",
1179
+ 'group.new.ws': "ワークスペースグループ",
1180
+ 'group.new.both': "新しいセッション/ワークスペースグループ",
1181
+ 'group.new.where': "作成先を選択",
1182
+ 'group.new.placeholder': "グループ名、/ で入れ子",
1183
+ 'group.new.nameHint': "/ でさらに深い階層になります(例: frontend/components)",
1184
+ 'group.new.empty': "グループ名を入力してください",
1185
+ 'group.new.root': "最上位",
1186
+ 'group.new.pickWorkspace': "ワークスペースを選択",
1187
+ 'group.new.noWorkspace': "ワークスペースがありません。先に追加してください",
1188
+ 'group.new.pickFirst': "先にワークスペースを選択してください",
1189
+ 'group.delete.title': "グループを削除",
1190
+ 'group.delete.body': "「{name}」は空のグループです。削除すると元に戻せません。",
1191
+ 'menu.deleteGroup': "グループを削除",
1057
1192
  },
1058
1193
  /* locale: ko */
1059
1194
  'ko': {
@@ -1167,6 +1302,21 @@ window.__ModuleLoader__.load({
1167
1302
  'group.move.title': '그룹으로 이동',
1168
1303
  'group.pick': '기존 그룹 선택',
1169
1304
  'group.new': '새 그룹',
1305
+ 'group.new.title': "새 그룹",
1306
+ 'group.new.sess': "세션 그룹",
1307
+ 'group.new.ws': "워크스페이스 그룹",
1308
+ 'group.new.both': "새 세션 또는 워크스페이스 그룹",
1309
+ 'group.new.where': "만들 위치 선택",
1310
+ 'group.new.placeholder': "그룹 이름, \"/\"로 중첩",
1311
+ 'group.new.nameHint': "\"/\"로 더 깊은 단계, 예: frontend/components",
1312
+ 'group.new.empty': "그룹 이름을 입력하세요",
1313
+ 'group.new.root': "최상위",
1314
+ 'group.new.pickWorkspace': "워크스페이스 선택",
1315
+ 'group.new.noWorkspace': "워크스페이스가 없습니다. 먼저 추가하세요",
1316
+ 'group.new.pickFirst': "먼저 워크스페이스를 선택하세요",
1317
+ 'group.delete.title': "그룹 삭제",
1318
+ 'group.delete.body': "\"{name}\"은(는) 빈 그룹입니다. 삭제하면 되돌릴 수 없습니다.",
1319
+ 'menu.deleteGroup': "그룹 삭제",
1170
1320
  },
1171
1321
  /* locale: nl */
1172
1322
  'nl': {
@@ -1280,6 +1430,21 @@ window.__ModuleLoader__.load({
1280
1430
  'group.move.title': 'Naar groep verplaatsen',
1281
1431
  'group.pick': 'Bestaande groep kiezen',
1282
1432
  'group.new': 'Nieuwe groep',
1433
+ 'group.new.title': "Nieuwe groep",
1434
+ 'group.new.sess': "Sessiegroep",
1435
+ 'group.new.ws': "Werkruimtegroep",
1436
+ 'group.new.both': "Nieuwe sessie- of werkruimtegroep",
1437
+ 'group.new.where': "Kies waar je maakt",
1438
+ 'group.new.placeholder': "Groepsnaam, \"/\" nest",
1439
+ 'group.new.nameHint': "Een \"/\" nest dieper, bijv. frontend/components",
1440
+ 'group.new.empty': "Voer een groepsnaam in",
1441
+ 'group.new.root': "Bovenste niveau",
1442
+ 'group.new.pickWorkspace': "Kies een werkruimte",
1443
+ 'group.new.noWorkspace': "Nog geen werkruimten — voeg er eerst een toe",
1444
+ 'group.new.pickFirst': "Kies eerst een werkruimte",
1445
+ 'group.delete.title': "Groep verwijderen",
1446
+ 'group.delete.body': "\"{name}\" is een lege groep; verwijderen kan niet ongedaan worden gemaakt.",
1447
+ 'menu.deleteGroup': "Groep verwijderen",
1283
1448
  },
1284
1449
  /* locale: pl */
1285
1450
  'pl': {
@@ -1393,6 +1558,21 @@ window.__ModuleLoader__.load({
1393
1558
  'group.move.title': 'Przenieś do grupy',
1394
1559
  'group.pick': 'Wybierz istniejącą grupę',
1395
1560
  'group.new': 'Nowa grupa',
1561
+ 'group.new.title': "Nowa grupa",
1562
+ 'group.new.sess': "Grupa sesji",
1563
+ 'group.new.ws': "Grupa obszarów roboczych",
1564
+ 'group.new.both': "Nowa grupa sesji lub obszarów roboczych",
1565
+ 'group.new.where': "Wybierz miejsce utworzenia",
1566
+ 'group.new.placeholder': "Nazwa grupy, „/” zagnieżdża",
1567
+ 'group.new.nameHint': "„/” zagnieżdża głębiej, np. frontend/components",
1568
+ 'group.new.empty': "Podaj nazwę grupy",
1569
+ 'group.new.root': "Najwyższy poziom",
1570
+ 'group.new.pickWorkspace': "Wybierz obszar roboczy",
1571
+ 'group.new.noWorkspace': "Brak obszarów roboczych — najpierw dodaj jeden",
1572
+ 'group.new.pickFirst': "Najpierw wybierz obszar roboczy",
1573
+ 'group.delete.title': "Usuń grupę",
1574
+ 'group.delete.body': "„{name}” to pusta grupa; usunięcia nie można cofnąć.",
1575
+ 'menu.deleteGroup': "Usuń grupę",
1396
1576
  },
1397
1577
  /* locale: pt */
1398
1578
  'pt': {
@@ -1506,6 +1686,21 @@ window.__ModuleLoader__.load({
1506
1686
  'group.move.title': 'Mover para o grupo',
1507
1687
  'group.pick': 'Escolher um grupo existente',
1508
1688
  'group.new': 'Novo grupo',
1689
+ 'group.new.title': "Novo grupo",
1690
+ 'group.new.sess': "Grupo de sessões",
1691
+ 'group.new.ws': "Grupo de áreas de trabalho",
1692
+ 'group.new.both': "Novo grupo de sessões ou áreas de trabalho",
1693
+ 'group.new.where': "Escolha onde criar",
1694
+ 'group.new.placeholder': "Nome do grupo, \"/\" aninha",
1695
+ 'group.new.nameHint': "Uma \"/\" aninha mais, ex. frontend/components",
1696
+ 'group.new.empty': "Digite um nome de grupo",
1697
+ 'group.new.root': "Nível superior",
1698
+ 'group.new.pickWorkspace': "Escolha uma área de trabalho",
1699
+ 'group.new.noWorkspace': "Ainda não há áreas de trabalho — adicione uma primeiro",
1700
+ 'group.new.pickFirst': "Escolha uma área de trabalho primeiro",
1701
+ 'group.delete.title': "Excluir grupo",
1702
+ 'group.delete.body': "\"{name}\" é um grupo vazio; a exclusão não pode ser desfeita.",
1703
+ 'menu.deleteGroup': "Excluir grupo",
1509
1704
  },
1510
1705
  /* locale: ru */
1511
1706
  'ru': {
@@ -1619,6 +1814,21 @@ window.__ModuleLoader__.load({
1619
1814
  'group.move.title': 'Переместить в группу',
1620
1815
  'group.pick': 'Выбрать существующую группу',
1621
1816
  'group.new': 'Новая группа',
1817
+ 'group.new.title': "Новая группа",
1818
+ 'group.new.sess': "Группа сеансов",
1819
+ 'group.new.ws': "Группа рабочих областей",
1820
+ 'group.new.both': "Новая группа сеансов или рабочих областей",
1821
+ 'group.new.where': "Выберите место создания",
1822
+ 'group.new.placeholder': "Имя группы, «/» вкладывает",
1823
+ 'group.new.nameHint': "«/» вкладывает глубже, напр. frontend/components",
1824
+ 'group.new.empty': "Введите имя группы",
1825
+ 'group.new.root': "Верхний уровень",
1826
+ 'group.new.pickWorkspace': "Выберите рабочую область",
1827
+ 'group.new.noWorkspace': "Рабочих областей пока нет — сначала добавьте одну",
1828
+ 'group.new.pickFirst': "Сначала выберите рабочую область",
1829
+ 'group.delete.title': "Удалить группу",
1830
+ 'group.delete.body': "«{name}» — пустая группа; удаление необратимо.",
1831
+ 'menu.deleteGroup': "Удалить группу",
1622
1832
  },
1623
1833
  /* locale: sv */
1624
1834
  'sv': {
@@ -1732,6 +1942,21 @@ window.__ModuleLoader__.load({
1732
1942
  'group.move.title': 'Flytta till grupp',
1733
1943
  'group.pick': 'Välj en befintlig grupp',
1734
1944
  'group.new': 'Ny grupp',
1945
+ 'group.new.title': "Ny grupp",
1946
+ 'group.new.sess': "Sessionsgrupp",
1947
+ 'group.new.ws': "Arbetsytegrupp",
1948
+ 'group.new.both': "Ny sessions- eller arbetsytegrupp",
1949
+ 'group.new.where': "Välj var den skapas",
1950
+ 'group.new.placeholder': "Gruppnamn, \"/\" nästlar",
1951
+ 'group.new.nameHint': "Ett \"/\" nästlar djupare, t.ex. frontend/components",
1952
+ 'group.new.empty': "Ange ett gruppnamn",
1953
+ 'group.new.root': "Översta nivån",
1954
+ 'group.new.pickWorkspace': "Välj en arbetsyta",
1955
+ 'group.new.noWorkspace': "Inga arbetsytor än — lägg till en först",
1956
+ 'group.new.pickFirst': "Välj en arbetsyta först",
1957
+ 'group.delete.title': "Ta bort grupp",
1958
+ 'group.delete.body': "\"{name}\" är en tom grupp; borttagningen kan inte ångras.",
1959
+ 'menu.deleteGroup': "Ta bort grupp",
1735
1960
  },
1736
1961
  /* locale: th */
1737
1962
  'th': {
@@ -1845,6 +2070,21 @@ window.__ModuleLoader__.load({
1845
2070
  'group.move.title': 'ย้ายไปยังกลุ่ม',
1846
2071
  'group.pick': 'เลือกกลุ่มที่มีอยู่',
1847
2072
  'group.new': 'กลุ่มใหม่',
2073
+ 'group.new.title': "กลุ่มใหม่",
2074
+ 'group.new.sess': "กลุ่มเซสชัน",
2075
+ 'group.new.ws': "กลุ่มเวิร์กสเปซ",
2076
+ 'group.new.both': "กลุ่มเซสชันหรือเวิร์กสเปซใหม่",
2077
+ 'group.new.where': "เลือกตำแหน่งที่สร้าง",
2078
+ 'group.new.placeholder': "ชื่อกลุ่ม ใช้ / ซ้อนระดับ",
2079
+ 'group.new.nameHint': "/ ใช้ซ้อนระดับลึกขึ้น เช่น frontend/components",
2080
+ 'group.new.empty': "กรอกชื่อกลุ่ม",
2081
+ 'group.new.root': "ระดับบนสุด",
2082
+ 'group.new.pickWorkspace': "เลือกเวิร์กสเปซ",
2083
+ 'group.new.noWorkspace': "ยังไม่มีเวิร์กสเปซ — เพิ่มก่อน",
2084
+ 'group.new.pickFirst': "เลือกเวิร์กสเปซก่อน",
2085
+ 'group.delete.title': "ลบกลุ่ม",
2086
+ 'group.delete.body': "\"{name}\" เป็นกลุ่มว่าง ลบแล้วกู้คืนไม่ได้",
2087
+ 'menu.deleteGroup': "ลบกลุ่ม",
1848
2088
  },
1849
2089
  /* locale: tr */
1850
2090
  'tr': {
@@ -1958,6 +2198,21 @@ window.__ModuleLoader__.load({
1958
2198
  'group.move.title': 'Gruba taşı',
1959
2199
  'group.pick': 'Mevcut bir grup seç',
1960
2200
  'group.new': 'Yeni grup',
2201
+ 'group.new.title': "Yeni grup",
2202
+ 'group.new.sess': "Oturum grubu",
2203
+ 'group.new.ws': "Çalışma alanı grubu",
2204
+ 'group.new.both': "Yeni oturum veya çalışma alanı grubu",
2205
+ 'group.new.where': "Oluşturulacak yeri seçin",
2206
+ 'group.new.placeholder': "Grup adı, \"/\" iç içe koyar",
2207
+ 'group.new.nameHint': "\"/\" daha derin iç içe koyar, örn. frontend/components",
2208
+ 'group.new.empty': "Bir grup adı girin",
2209
+ 'group.new.root': "En üst düzey",
2210
+ 'group.new.pickWorkspace': "Bir çalışma alanı seçin",
2211
+ 'group.new.noWorkspace': "Henüz çalışma alanı yok — önce bir tane ekleyin",
2212
+ 'group.new.pickFirst': "Önce bir çalışma alanı seçin",
2213
+ 'group.delete.title': "Grubu sil",
2214
+ 'group.delete.body': "\"{name}\" boş bir grup; silme geri alınamaz.",
2215
+ 'menu.deleteGroup': "Grubu sil",
1961
2216
  },
1962
2217
  /* locale: vi */
1963
2218
  'vi': {
@@ -2071,6 +2326,21 @@ window.__ModuleLoader__.load({
2071
2326
  'group.move.title': 'Chuyển vào nhóm',
2072
2327
  'group.pick': 'Chọn một nhóm có sẵn',
2073
2328
  'group.new': 'Nhóm mới',
2329
+ 'group.new.title': "Nhóm mới",
2330
+ 'group.new.sess': "Nhóm phiên",
2331
+ 'group.new.ws': "Nhóm không gian làm việc",
2332
+ 'group.new.both': "Nhóm phiên hoặc không gian làm việc mới",
2333
+ 'group.new.where': "Chọn nơi tạo",
2334
+ 'group.new.placeholder': "Tên nhóm, \"/\" để lồng nhau",
2335
+ 'group.new.nameHint': "\"/\" lồng sâu hơn, ví dụ frontend/components",
2336
+ 'group.new.empty': "Nhập tên nhóm",
2337
+ 'group.new.root': "Cấp cao nhất",
2338
+ 'group.new.pickWorkspace': "Chọn một không gian làm việc",
2339
+ 'group.new.noWorkspace': "Chưa có không gian làm việc — hãy thêm trước",
2340
+ 'group.new.pickFirst': "Chọn không gian làm việc trước",
2341
+ 'group.delete.title': "Xóa nhóm",
2342
+ 'group.delete.body': "\"{name}\" là nhóm trống; không thể hoàn tác sau khi xóa.",
2343
+ 'menu.deleteGroup': "Xóa nhóm",
2074
2344
  },
2075
2345
  /* locale: zh-HK */
2076
2346
  'zh-HK': {
@@ -2184,6 +2454,21 @@ window.__ModuleLoader__.load({
2184
2454
  'group.move.title': '移動到分組',
2185
2455
  'group.pick': '選擇已有分組',
2186
2456
  'group.new': '新增分組',
2457
+ 'group.new.title': "新增分組",
2458
+ 'group.new.sess': "工作階段分組",
2459
+ 'group.new.ws': "工作區分組",
2460
+ 'group.new.both': "新增工作階段或工作區分組",
2461
+ 'group.new.where': "揀建立位置",
2462
+ 'group.new.placeholder': "分組名,可用 / 嵌套",
2463
+ 'group.new.nameHint': "名入面嘅 / 即嵌套層級,例如 前端/組件",
2464
+ 'group.new.empty': "請輸入分組名",
2465
+ 'group.new.root': "最頂層",
2466
+ 'group.new.pickWorkspace': "揀一個工作區",
2467
+ 'group.new.noWorkspace': "仲未有工作區,請先加一個",
2468
+ 'group.new.pickFirst': "請先揀一個工作區",
2469
+ 'group.delete.title': "刪除分組",
2470
+ 'group.delete.body': "「{name}」係空分組,刪咗冇得返轉頭。",
2471
+ 'menu.deleteGroup': "刪除分組",
2187
2472
  },
2188
2473
  /* locale: zh-MO */
2189
2474
  'zh-MO': {
@@ -2297,6 +2582,21 @@ window.__ModuleLoader__.load({
2297
2582
  'group.move.title': '移動到分組',
2298
2583
  'group.pick': '選擇已有分組',
2299
2584
  'group.new': '新增分組',
2585
+ 'group.new.title': "新增分組",
2586
+ 'group.new.sess': "工作階段分組",
2587
+ 'group.new.ws': "工作區分組",
2588
+ 'group.new.both': "新增工作階段或工作區分組",
2589
+ 'group.new.where': "揀建立位置",
2590
+ 'group.new.placeholder': "分組名,可用 / 嵌套",
2591
+ 'group.new.nameHint': "名入面嘅 / 即嵌套層級,例如 前端/組件",
2592
+ 'group.new.empty': "請輸入分組名",
2593
+ 'group.new.root': "最頂層",
2594
+ 'group.new.pickWorkspace': "揀一個工作區",
2595
+ 'group.new.noWorkspace': "仲未有工作區,請先加一個",
2596
+ 'group.new.pickFirst': "請先揀一個工作區",
2597
+ 'group.delete.title': "刪除分組",
2598
+ 'group.delete.body': "「{name}」係空分組,刪咗冇得返轉頭。",
2599
+ 'menu.deleteGroup': "刪除分組",
2300
2600
  },
2301
2601
  /* locale: zh-TW */
2302
2602
  'zh-TW': {
@@ -2410,6 +2710,21 @@ window.__ModuleLoader__.load({
2410
2710
  'group.move.title': '移動到分組',
2411
2711
  'group.pick': '選擇現有群組',
2412
2712
  'group.new': '新增分組',
2713
+ 'group.new.title': "新增分組",
2714
+ 'group.new.sess': "工作階段分組",
2715
+ 'group.new.ws': "工作區分組",
2716
+ 'group.new.both': "新增工作階段或工作區分組",
2717
+ 'group.new.where': "選擇建立位置",
2718
+ 'group.new.placeholder': "分組名稱,可用 / 巢狀",
2719
+ 'group.new.nameHint': "名稱中的 / 即巢狀層級,例如 前端/元件",
2720
+ 'group.new.empty': "請輸入分組名稱",
2721
+ 'group.new.root': "最頂層",
2722
+ 'group.new.pickWorkspace': "選擇一個工作區",
2723
+ 'group.new.noWorkspace': "還沒有工作區,請先新增一個",
2724
+ 'group.new.pickFirst': "請先選擇一個工作區",
2725
+ 'group.delete.title': "刪除分組",
2726
+ 'group.delete.body': "「{name}」是空分組,刪除後無法復原。",
2727
+ 'menu.deleteGroup': "刪除分組",
2413
2728
  },
2414
2729
  }
2415
2730
 
@@ -2633,7 +2948,7 @@ window.__ModuleLoader__.load({
2633
2948
  * titles. Groups are virtual (projection of names). Rows keep the Host
2634
2949
  * workspace.sessionIds (manual) order — drag-to-reorder must be visible.
2635
2950
  */
2636
- function buildSessionTree(rows, groupBySlash) {
2951
+ function buildSessionTree(rows, groupBySlash, explicitSess, groupScope) {
2637
2952
  const root = { path: '', name: '', groups: [], sessions: [] }
2638
2953
  const byPath = new Map([['', root]])
2639
2954
  // Slash grouping OFF (v0.13 view option): every row lands at the
@@ -2662,6 +2977,7 @@ window.__ModuleLoader__.load({
2662
2977
  }
2663
2978
  return node
2664
2979
  }
2980
+ for (const segs of explicitSegsOf(explicitSess, groupScope)) ensure(segs)
2665
2981
  for (const row of rows || []) {
2666
2982
  const segs = splitTitleSegs(row.title)
2667
2983
  const folderPath = segs.slice(0, -1).join('/')
@@ -2772,7 +3088,30 @@ window.__ModuleLoader__.load({
2772
3088
  folderPath: '',
2773
3089
  })
2774
3090
 
2775
- function buildTree(items, groupBySlash) {
3091
+ /**
3092
+ * Declared-but-empty groups, resolved per level. `explicit` is the store's
3093
+ * `folders.ws` bag (level idPrefix → path list); the empty path splits into
3094
+ * segments the same way a title prefix does, so a declared "a/b" produces
3095
+ * the same two-level chain a member would.
3096
+ */
3097
+ const EMPTY_FOLDERS = Object.freeze({})
3098
+ /** True when this scope declares at least one group (empty groups included). */
3099
+ const hasDeclared = (explicit, scope) => {
3100
+ const raw = explicit && typeof explicit === 'object' ? explicit[String(scope)] : undefined
3101
+ return Array.isArray(raw) && raw.length > 0
3102
+ }
3103
+
3104
+ const explicitSegsOf = (explicit, scope) => {
3105
+ const raw = explicit && typeof explicit === 'object' ? explicit[String(scope)] : undefined
3106
+ const out = []
3107
+ for (const path of Array.isArray(raw) ? raw : []) {
3108
+ const segs = String(path).split('/').filter((s) => s !== '')
3109
+ if (segs.length > 0) out.push(segs)
3110
+ }
3111
+ return out
3112
+ }
3113
+
3114
+ function buildTree(items, groupBySlash, explicitWs) {
2776
3115
  const list = items || []
2777
3116
  const diskParentOf = diskParentMapOf(list)
2778
3117
  const childrenOf = new Map()
@@ -2800,6 +3139,9 @@ window.__ModuleLoader__.load({
2800
3139
  folders: [],
2801
3140
  workspaces: [],
2802
3141
  idPath: (idPrefix !== '' ? idPrefix + '//' : '') + key,
3142
+ // Declared-group scope of THIS level (v0.17.0): the exact key a
3143
+ // new child group must be filed under in folders.ws.
3144
+ scope: idPrefix,
2803
3145
  }
2804
3146
  byPath.set(key, next)
2805
3147
  node.folders.push(next)
@@ -2808,11 +3150,19 @@ window.__ModuleLoader__.load({
2808
3150
  }
2809
3151
  return node
2810
3152
  }
3153
+ // Declared empty groups land BEFORE the members: a declared path that
3154
+ // also carries members is the same node either way (ensure is
3155
+ // idempotent), and declaring first means an empty one still renders.
3156
+ if (groupBySlash !== false) {
3157
+ for (const segs of explicitSegsOf(explicitWs, idPrefix)) ensure(segs)
3158
+ }
2811
3159
  for (const workspace of wsList) {
2812
3160
  const segs = splitTitleSegs(workspace.title)
2813
3161
  const folderPath = segs.slice(0, -1).join('/')
2814
3162
  const leaf = segs.length > 0 ? segs[segs.length - 1] : (basename(workspace.path) || String(workspace.title || '') || String(workspace.workspaceId || ''))
2815
3163
  const subList = childrenOf.get(workspace.workspaceId)
3164
+ // The disk level this workspace OWNS — its declared groups file here.
3165
+ const subPrefix = (idPrefix !== '' ? idPrefix + '//' : '') + workspace.workspaceId
2816
3166
  const entry = {
2817
3167
  workspaceId: workspace.workspaceId,
2818
3168
  title: String(workspace.title || ''),
@@ -2820,7 +3170,8 @@ window.__ModuleLoader__.load({
2820
3170
  sessionIds: Array.isArray(workspace.sessionIds) ? workspace.sessionIds : [],
2821
3171
  leaf,
2822
3172
  folderPath,
2823
- sub: subList ? buildLevel(subList, (idPrefix !== '' ? idPrefix + '//' : '') + workspace.workspaceId) : null,
3173
+ subPrefix,
3174
+ sub: subList ? buildLevel(subList, subPrefix) : null,
2824
3175
  }
2825
3176
  // Slash grouping OFF (v0.13 view option): skip the name layer —
2826
3177
  // workspaces mount straight at their disk level; the DISK layer
@@ -2934,7 +3285,30 @@ window.__ModuleLoader__.load({
2934
3285
  // rim + a tinted wash so "this drop joins a group" reads stronger than
2935
3286
  // the reorder dashes above.
2936
3287
  '.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)}',
2937
- '.bw-newgroup-btn{flex:none;width:auto;padding:0 6px;font-size:12px;height:20px;border-radius:6px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.1));color:inherit;cursor:pointer}',
3288
+ // v0.17.0: the new-group entry is an ICON button now (same family as the
3289
+ // search / add buttons beside it); the wording moved into its tooltip.
3290
+ '.bw-newgroup-btn{flex:none}',
3291
+ '.bw-seg{display:flex;gap:0;width:fit-content;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.25));border-radius:7px;overflow:hidden}',
3292
+ '.bw-seg-btn{border:none;background:transparent;color:var(--dsw-alias-label-secondary,#b8b8b8);font:inherit;font-size:12.5px;padding:5px 14px;cursor:pointer}',
3293
+ '.bw-seg-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,#e6e6e6)}',
3294
+ '.bw-seg-btn-on{background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.16));color:var(--dsw-alias-label-primary,#e6e6e6);font-weight:500}',
3295
+ '.bw-seg-btn+.bw-seg-btn{border-left:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.25))}',
3296
+ '.bw-gn-hint{font-size:12px;color:var(--dsw-alias-label-tertiary,#9a9a9a)}',
3297
+ // A container-only node (a workspace name-group in the session layer):
3298
+ // still expandable, but it carries no workspaceId to file against.
3299
+ '.bw-gn-row-hold{cursor:default}',
3300
+ '.bw-gn-row-hold:hover{background:transparent}',
3301
+ '.bw-gn-row-hold .bw-gn-label{color:var(--dsw-alias-label-secondary,#b8b8b8)}',
3302
+ '.bw-gn-empty{font-size:12px;color:var(--dsw-alias-label-tertiary,#9a9a9a);padding:10px 8px;text-align:center}',
3303
+ '.bw-gn-tree{max-height:250px;overflow:auto;padding:4px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.22));border-radius:8px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.06))}',
3304
+ '.bw-gn-row{display:flex;align-items:center;gap:6px;min-height:26px;padding-right:8px;border-radius:6px;cursor:pointer;user-select:none}',
3305
+ '.bw-gn-row:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}',
3306
+ '.bw-gn-row-on{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));box-shadow:inset 0 0 0 1px var(--dsw-alias-brand-primary,#5b8def)}',
3307
+ '.bw-gn-twist{flex:none;display:grid;place-items:center;width:14px;height:14px;padding:0;border:none;background:transparent;color:var(--dsw-alias-label-tertiary,#9a9a9a);cursor:pointer}',
3308
+ '.bw-gn-twist-open{transform:rotate(90deg)}',
3309
+ '.bw-gn-icon{flex:none;display:grid;place-items:center;width:16px;height:16px;color:var(--dsw-alias-label-secondary,#b8b8b8)}',
3310
+ '.bw-gn-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12.5px;color:var(--dsw-alias-label-primary,#e6e6e6)}',
3311
+ '.bw-gn-input{width:100%;box-sizing:border-box}',
2938
3312
  '.bw-row:hover{background:var(--dsw-specific-sidebar-nav-item-hover,var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12)))}',
2939
3313
  '.bw-row:hover{background:color-mix(in srgb,var(--dsw-specific-sidebar-nav-item-hover,rgba(127,127,127,.14)) 50%,transparent)}',
2940
3314
  '.bw-row-current{background:var(--dsw-specific-sidebar-nav-item-active,rgba(91,141,239,.15))}',
@@ -3068,8 +3442,16 @@ window.__ModuleLoader__.load({
3068
3442
  */
3069
3443
  const workspaceSlashOf = (value) => value === undefined ? true : value === true
3070
3444
 
3445
+ /**
3446
+ * Normalize a declared group path: split on "/", drop blank segments (so
3447
+ * " " and "a//b" cannot create nameless levels), trim each name. Returns
3448
+ * '' when nothing is left — the store actions treat that as "do nothing".
3449
+ */
3450
+ const declaredPathOf = (raw) => String(raw == null ? '' : raw)
3451
+ .split('/').map((s) => s.trim()).filter((s) => s !== '').join('/')
3452
+
3071
3453
  const createViewStore = () => storeKit.defineStore({
3072
- init: () => ({ expanded: {}, sessionsExpanded: {}, sessionGroups: {}, sessionOrder: {}, prefs: { compactChains: true }, styling: {}, groupBy: 'workspace-tree', orderBy: 'manual', sessionTitleSlash: true, workspaceTitleSlash: false }),
3454
+ init: () => ({ expanded: {}, sessionsExpanded: {}, sessionGroups: {}, sessionOrder: {}, prefs: { compactChains: true }, styling: {}, groupBy: 'workspace-tree', orderBy: 'manual', sessionTitleSlash: true, workspaceTitleSlash: false, folders: { ws: {}, sess: {} } }),
3073
3455
  // NOTE: hydration REPLACES the state with the persisted whole value —
3074
3456
  // init defaults never merge. Every action must tolerate a missing key
3075
3457
  // (states persisted by older plugin versions lack sessionGroups), and
@@ -3120,6 +3502,44 @@ window.__ModuleLoader__.load({
3120
3502
  setOrderBy: (d, value) => { d.orderBy = value },
3121
3503
  setSessionTitleSlash: (d, value) => { d.sessionTitleSlash = value !== false },
3122
3504
  setWorkspaceTitleSlash: (d, value) => { d.workspaceTitleSlash = value !== false },
3505
+ // Explicit (possibly EMPTY) groups, v0.17.0. A group has always been a
3506
+ // projection of "/" in member titles, so a group with no members simply
3507
+ // did not exist and "create a group" had nothing to write. These two
3508
+ // actions are that missing write path: a declared path renders as a
3509
+ // real row even while it holds nothing, and disappears again when the
3510
+ // user deletes it. Scope keys follow the tree's own identities —
3511
+ // workspaces live in the DISK level they belong to (folders.ws keyed by
3512
+ // the level's idPrefix: '' for the top level, '<wsId>' one disk level
3513
+ // down, '<a>//<b>' deeper), sessions live in one workspace
3514
+ // (folders.sess keyed by workspaceId). Both bags are plain path lists.
3515
+ addFolder: (d, kind, scope, path) => {
3516
+ const clean = declaredPathOf(path)
3517
+ if (clean === '') return
3518
+ if (!d.folders || typeof d.folders !== 'object') d.folders = { ws: {}, sess: {} }
3519
+ const bagKey = kind === 'session' ? 'sess' : 'ws'
3520
+ if (!d.folders[bagKey] || typeof d.folders[bagKey] !== 'object') d.folders[bagKey] = {}
3521
+ const bag = d.folders[bagKey]
3522
+ const key = String(scope == null ? '' : scope)
3523
+ const cur = Array.isArray(bag[key]) ? bag[key].slice() : []
3524
+ if (cur.indexOf(clean) === -1) cur.push(clean)
3525
+ bag[key] = cur
3526
+ },
3527
+ // Deleting a declared group takes its declared DESCENDANTS with it —
3528
+ // "a", "a/b" and "a/b/c" are one branch, and leaving the children
3529
+ // behind would resurrect the parent as an implicit row.
3530
+ removeFolder: (d, kind, scope, path) => {
3531
+ const clean = declaredPathOf(path)
3532
+ if (clean === '') return
3533
+ if (!d.folders || typeof d.folders !== 'object') return
3534
+ const bag = d.folders[kind === 'session' ? 'sess' : 'ws']
3535
+ if (!bag || typeof bag !== 'object') return
3536
+ const key = String(scope == null ? '' : scope)
3537
+ const cur = Array.isArray(bag[key]) ? bag[key] : []
3538
+ const prefix = clean + '/'
3539
+ const next = cur.filter((p) => p !== clean && p.indexOf(prefix) !== 0)
3540
+ if (next.length === 0) delete bag[key]
3541
+ else bag[key] = next
3542
+ },
3123
3543
  },
3124
3544
  })
3125
3545
 
@@ -3396,6 +3816,112 @@ window.__ModuleLoader__.load({
3396
3816
  * Degradation: callers keep a native control for hosts whose primitives
3397
3817
  * have no Menu, so picking never becomes impossible.
3398
3818
  */
3819
+
3820
+ /**
3821
+ * "New group" dialog (v0.17.0). Three decisions, top to bottom:
3822
+ *
3823
+ * 1. WHICH LAYER — session groups, or workspace groups. A single segmented
3824
+ * pair of buttons; the workspace side only exists while "/" grouping is
3825
+ * on for workspaces, and a host where only one layer is live renders no
3826
+ * switch at all (the wording of the header button already promised it).
3827
+ * 2. WHERE — an expandable TREE of containers rather than a flat dropdown:
3828
+ * the parent is picked visually, so "under this workspace" and "inside
3829
+ * that group" read the same way they do in the sidebar.
3830
+ * 3. WHAT — the new group's name ("a/b" nests).
3831
+ *
3832
+ * Declared-empty groups did not use to exist (a group was a projection of
3833
+ * member titles, so a member-less one could not be represented); this dialog
3834
+ * writes the declaration itself, which is why it is not the move dialog.
3835
+ */
3836
+ function GroupNewDialog(props) {
3837
+ const { kinds, initialKind, treeOf, onConfirm, onClose, t } = props
3838
+ const [kind, setKind] = React.useState(initialKind)
3839
+ const [picked, setPicked] = React.useState(null)
3840
+ const [openKeys, setOpenKeys] = React.useState({})
3841
+ const [name, setName] = React.useState('')
3842
+ const [error, setError] = React.useState('')
3843
+ const plan = treeOf(kind)
3844
+ const toggle = (id) => setOpenKeys((prev) => ({ ...prev, [id]: !prev[id] }))
3845
+ const pick = (node) => { if (node.pickable === false) return; setPicked(node); setError('') }
3846
+ const submit = () => {
3847
+ const clean = String(name).split('/').filter((s) => s !== '').join('/')
3848
+ if (clean === '') { setError(t('group.new.empty')); return }
3849
+ if (!picked) { setError(t('group.new.pickFirst')); return }
3850
+ onConfirm(kind, picked.scope, picked.path === '' ? clean : picked.path + '/' + clean)
3851
+ }
3852
+ const renderNodes = (list, depth) => {
3853
+ const out = []
3854
+ for (const node of list) {
3855
+ const hasKids = node.children.length > 0
3856
+ // The first level opens by default: the root row is a header ("pick a
3857
+ // workspace"), so a collapsed tree would show the user a header and
3858
+ // nothing to pick. Deeper levels stay closed until asked for.
3859
+ const expanded = openKeys[node.id] === true || (depth === 0 && openKeys[node.id] !== false)
3860
+ out.push(E('div', {
3861
+ 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'),
3863
+ style: { paddingLeft: 6 + depth * 14 },
3864
+ role: 'treeitem',
3865
+ 'aria-selected': picked && picked.id === node.id ? 'true' : 'false',
3866
+ onClick: () => pick(node),
3867
+ },
3868
+ hasKids ? E('button', {
3869
+ type: 'button',
3870
+ className: cls('bw-gn-twist', expanded && 'bw-gn-twist-open'),
3871
+ tabIndex: -1,
3872
+ 'aria-label': node.label,
3873
+ onClick: (e) => { e.stopPropagation(); toggle(node.id) },
3874
+ }, 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)),
3876
+ E('span', { className: 'bw-gn-label' }, node.label),
3877
+ ))
3878
+ if (hasKids && expanded) out.push(...renderNodes(node.children, depth + 1))
3879
+ }
3880
+ return out
3881
+ }
3882
+ return E(ui.Modal, {
3883
+ open: true,
3884
+ onClose,
3885
+ closeLabel: t('close'),
3886
+ title: t('group.new.title'),
3887
+ footer: E('div', { className: 'bw-modal-actions' },
3888
+ E(BTN, { variant: 'outline', onClick: onClose }, t('cancel')),
3889
+ E(BTN, { variant: 'primary', onClick: submit }, t('confirm')),
3890
+ ),
3891
+ },
3892
+ E('div', { className: 'bw-modal-body' },
3893
+ kinds.length > 1 ? E('div', { className: 'bw-seg', role: 'radiogroup', 'aria-label': t('group.new.title') },
3894
+ E('button', {
3895
+ type: 'button', role: 'radio', 'aria-checked': kind === 'session' ? 'true' : 'false',
3896
+ className: cls('bw-seg-btn', kind === 'session' && 'bw-seg-btn-on'),
3897
+ onClick: () => { setKind('session'); setPicked(null) },
3898
+ }, t('group.new.sess')),
3899
+ E('button', {
3900
+ type: 'button', role: 'radio', 'aria-checked': kind === 'workspace' ? 'true' : 'false',
3901
+ className: cls('bw-seg-btn', kind === 'workspace' && 'bw-seg-btn-on'),
3902
+ onClick: () => { setKind('workspace'); setPicked(null) },
3903
+ }, t('group.new.ws')),
3904
+ ) : null,
3905
+ E('div', { className: 'bw-gn-hint' }, plan.hint || t('group.new.where')),
3906
+ E('div', { className: 'bw-gn-tree', role: 'tree', 'aria-label': t('group.new.where') },
3907
+ plan.nodes.length > 0 ? renderNodes(plan.nodes, 0) : E('div', { className: 'bw-gn-empty' }, t('group.new.noWorkspace'))),
3908
+ E('div', { className: 'bw-field' },
3909
+ E('input', {
3910
+ className: 'bw-input bw-gn-input',
3911
+ value: name,
3912
+ autoFocus: true,
3913
+ placeholder: t('group.new.placeholder'),
3914
+ 'aria-label': t('group.new.title'),
3915
+ onChange: (e) => { setName(e.target.value); setError('') },
3916
+ onKeyDown: (e) => { if (e.key === 'Enter') submit() },
3917
+ }),
3918
+ ),
3919
+ E('div', { className: 'bw-hint' }, t('group.new.nameHint')),
3920
+ error !== '' ? E('div', { className: 'bw-error-text', role: 'alert' }, error) : null,
3921
+ ),
3922
+ )
3923
+ }
3924
+
3399
3925
  function MenuPicker({ options, value, onPick, placeholder, chevronOnly, title, t }) {
3400
3926
  const [open, setOpen] = React.useState(false)
3401
3927
  const list = Array.isArray(options) ? options : []
@@ -4368,6 +4894,12 @@ window.__ModuleLoader__.load({
4368
4894
  const prefsMap = useStore ? (useStore(s => s.prefs) || {}) : {}
4369
4895
  const stylingMap = useStore ? (useStore(s => s.styling) || {}) : {}
4370
4896
  const sessionOrderMap = useStore ? (useStore(s => s.sessionOrder) || {}) : {}
4897
+ // Declared (possibly empty) groups, v0.17.0. Hydration replaces the whole
4898
+ // state, so a snapshot written by an older version has no `folders` key —
4899
+ // the fallback object keeps every read below total.
4900
+ const foldersMap = useStore ? (useStore(s => s.folders) || {}) : {}
4901
+ const wsFolders = foldersMap && typeof foldersMap.ws === 'object' && foldersMap.ws !== null ? foldersMap.ws : EMPTY_FOLDERS
4902
+ const sessFolders = foldersMap && typeof foldersMap.sess === 'object' && foldersMap.sess !== null ? foldersMap.sess : EMPTY_FOLDERS
4371
4903
  // View options (v0.13). Selector reads take fallbacks: hydration
4372
4904
  // replaces the whole persisted value, so states written by older
4373
4905
  // plugin versions lack these keys (hard store discipline).
@@ -4634,10 +5166,10 @@ window.__ModuleLoader__.load({
4634
5166
  // their drop targets live inside workspace rows, which compression merges.
4635
5167
  const draggingWorkspace = drag !== null && drag.kind === 'workspace'
4636
5168
  const tree = React.useMemo(() => {
4637
- const built = buildTree(items, workspaceSlash)
5169
+ const built = buildTree(items, workspaceSlash, wsFolders)
4638
5170
  if (!compactChains || draggingWorkspace) return built
4639
5171
  return { ...built, folders: built.folders.map((f) => materializeChain(compressTree(f))), workspaces: built.workspaces }
4640
- }, [items, compactChains, draggingWorkspace, workspaceSlash])
5172
+ }, [items, compactChains, draggingWorkspace, workspaceSlash, wsFolders])
4641
5173
  // Disk-layer ancestry for drag semantics (same derivation as buildTree).
4642
5174
  const diskParentMap = React.useMemo(() => diskParentMapOf(items), [items])
4643
5175
 
@@ -4744,7 +5276,7 @@ window.__ModuleLoader__.load({
4744
5276
  // tree AND its disk-nested child levels.
4745
5277
  const wsAllPulseOf = (workspace) => {
4746
5278
  if (!statusPulse || searching) return null
4747
- let best = nodePulseOf(buildSessionTree(sessionsOf(workspace), sessionSlash))
5279
+ let best = nodePulseOf(buildSessionTree(sessionsOf(workspace), sessionSlash, sessFolders, workspace.workspaceId))
4748
5280
  if (workspace.sub) {
4749
5281
  for (const folder of workspace.sub.folders) {
4750
5282
  const s = folderPulseOf(folder)
@@ -4827,6 +5359,91 @@ window.__ModuleLoader__.load({
4827
5359
  * button itself disappears entirely when neither is (see the header).
4828
5360
  */
4829
5361
  const pickerKindOf = () => workspaceSlash && sessionSlash ? undefined : (workspaceSlash ? 'workspace' : 'session')
5362
+ // The header button opens "new group" (v0.17.0); its wording and its
5363
+ // default layer follow which name layers are live. With workspaces
5364
+ // disk-only there is no workspace-group layer to create into, so the
5365
+ // default is the session layer — and a single live layer means no switch.
5366
+ const newGroupLayers = () => (workspaceSlash && sessionSlash ? ['session', 'workspace'] : (workspaceSlash ? ['workspace'] : ['session']))
5367
+ const defaultNewGroupLayer = () => (workspaceSlash && !sessionSlash ? 'workspace' : 'session')
5368
+ const newGroupLabel = () => (workspaceSlash && sessionSlash ? t('group.new.both') : (workspaceSlash ? t('group.new.ws') : t('group.new.sess')))
5369
+
5370
+ // Container trees for the new-group dialog. Each node carries the exact
5371
+ // write target: `scope` is the store's bags key (the disk level's idPrefix
5372
+ // for workspaces, the workspaceId for sessions) and `path` is the parent
5373
+ // 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
+ }
5387
+ const sessGroupNodesOf = (node, workspaceId) => (node.groups || []).map((g) => ({
5388
+ id: workspaceId + '|' + g.path, scope: workspaceId, path: g.path, label: g.name, kind: 'sgroup',
5389
+ children: sessGroupNodesOf(g, workspaceId),
5390
+ }))
5391
+ const newGroupTreeOf = (kind) => {
5392
+ // The container tree MIRRORS the sidebar's current view mode (v0.17.1):
5393
+ // "workspace tree" offers the real hierarchy (disk nesting + name
5394
+ // groups); the one-level modes ("workspaces" / "flat") render no
5395
+ // hierarchy to mirror, so they fall back to a flat workspace list.
5396
+ // Offering a nesting the sidebar is not showing would be a lie about
5397
+ // where the new group lands.
5398
+ const treeView = groupBy === 'workspace-tree'
5399
+ if (kind === 'workspace') {
5400
+ // "Top level" IS a real target here (a group at the root of the
5401
+ // workspace tree), so it is a node like any other.
5402
+ return {
5403
+ hint: null,
5404
+ nodes: [{
5405
+ id: '|', scope: '', path: '', label: t('group.new.root'), kind: 'folder',
5406
+ children: treeView ? wsGroupNodesOf(tree) : [],
5407
+ }],
5408
+ }
5409
+ }
5410
+ // A session group lives INSIDE one workspace (folders.sess is keyed by
5411
+ // workspaceId), so there is no "top level" to declare into — the
5412
+ // workspace tree IS the top level, introduced by a plain hint rather
5413
+ // than wrapped in a fake root node (a header row that looked like a
5414
+ // tree node read as a container you could pick).
5415
+ //
5416
+ // The walk follows the SAME tree the sidebar renders (disk nesting
5417
+ // included), not the flat registry list: a child workspace must appear
5418
+ // nested under its parent exactly as it does in "workspace tree" view.
5419
+ // Name-group folders are containers here — a session group cannot be
5420
+ // filed against one (they carry no workspaceId) — so they expand but
5421
+ // refuse selection.
5422
+ const sessNodesOf = (node) => {
5423
+ const out = []
5424
+ 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) })
5426
+ }
5427
+ for (const w of node.workspaces || []) {
5428
+ const inner = sessGroupNodesOf(buildSessionTree(sessionsOf(w), sessionSlash, sessFolders, w.workspaceId), w.workspaceId)
5429
+ const subNodes = w.sub ? sessNodesOf(w.sub) : []
5430
+ out.push({
5431
+ id: w.workspaceId + '|', scope: w.workspaceId, path: '', label: w.leaf || w.title, kind: 'ws',
5432
+ children: [...inner, ...subNodes],
5433
+ })
5434
+ }
5435
+ return out
5436
+ }
5437
+ if (treeView) return { hint: t('group.new.pickWorkspace'), nodes: sessNodesOf(tree) }
5438
+ // One-level modes: every workspace sits at the root, exactly as the
5439
+ // sidebar draws it (raw items — there is no .sub level to nest here).
5440
+ const flat = []
5441
+ for (const w of items) {
5442
+ const inner = sessGroupNodesOf(buildSessionTree(sessionsOf(w), sessionSlash, sessFolders, w.workspaceId), w.workspaceId)
5443
+ flat.push({ id: w.workspaceId + '|', scope: w.workspaceId, path: '', label: w.leaf || w.title, kind: 'ws', children: inner })
5444
+ }
5445
+ return { hint: t('group.new.pickWorkspace'), nodes: flat }
5446
+ }
4830
5447
  /**
4831
5448
  * Picker entries for the header button: every workspace, then its
4832
5449
  * sessions — each list present only while its OWN name layer is on, so
@@ -4906,7 +5523,7 @@ window.__ModuleLoader__.load({
4906
5523
  if (target.kind === 'sgroup') {
4907
5524
  const workspace = (items || []).find((w) => w.workspaceId === target.workspaceId)
4908
5525
  if (!workspace) { setDialog(null); return }
4909
- const node = findSessionGroup(buildSessionTree(sessionsOf(workspace), sessionSlash), target.path)
5526
+ const node = findSessionGroup(buildSessionTree(sessionsOf(workspace), sessionSlash, sessFolders, workspace.workspaceId), target.path)
4910
5527
  if (!node) { setDialog(null); return }
4911
5528
  const affected = collectSessionRows(node)
4912
5529
  Promise.resolve()
@@ -5245,7 +5862,7 @@ window.__ModuleLoader__.load({
5245
5862
  if (name === target.name) { setDialog(null); return }
5246
5863
  const workspace = (items || []).find(w => w.workspaceId === target.workspaceId)
5247
5864
  if (!workspace) { setDialog(null); return }
5248
- const node = findSessionGroup(buildSessionTree(sessionsOf(workspace), sessionSlash), target.path)
5865
+ const node = findSessionGroup(buildSessionTree(sessionsOf(workspace), sessionSlash, sessFolders, workspace.workspaceId), target.path)
5249
5866
  if (!node) { setDialog(null); return }
5250
5867
  const parentPath = target.path.includes('/') ? target.path.slice(0, target.path.lastIndexOf('/')) : ''
5251
5868
  const nextPath = parentPath !== '' ? parentPath + '/' + name : name
@@ -5305,8 +5922,8 @@ window.__ModuleLoader__.load({
5305
5922
  // row keeps its count badge); an open one shows the full session tree.
5306
5923
  if (!searching && !sessionsOpenOf(workspace.workspaceId)) return []
5307
5924
  const rows = sessionsOf(workspace)
5308
- if (rows.length === 0) return []
5309
- return renderSessionNode(buildSessionTree(rows, sessionSlash), workspace.workspaceId, depth)
5925
+ if (rows.length === 0 && !hasDeclared(sessFolders, workspace.workspaceId)) return []
5926
+ return renderSessionNode(buildSessionTree(rows, sessionSlash, sessFolders, workspace.workspaceId), workspace.workspaceId, depth)
5310
5927
  }
5311
5928
  const searchSessionNode = (node) => {
5312
5929
  const groups = []
@@ -5333,7 +5950,7 @@ window.__ModuleLoader__.load({
5333
5950
  pulse: (statusPulse && !open) ? nodePulseOf(group) : null,
5334
5951
  count: countSessionTree(group),
5335
5952
  onToggle: () => { if (!searching) actions.setSessionGroupExpanded(key, !open) },
5336
- onContextMenu: (e) => openCtx('sgroup', { workspaceId, path: group.path, name: group.name }, e),
5953
+ onContextMenu: (e) => openCtx('sgroup', { workspaceId, path: group.path, name: group.name, empty: countSessionTree(group) === 0 }, e),
5337
5954
  dropInto: sgroupDropInto(workspaceId, group.path),
5338
5955
  dragEvents: sgroupDropEvents(workspaceId, group.path),
5339
5956
  custStyle: rowStyleOf('sgroup:' + workspaceId + '|' + group.path),
@@ -5362,7 +5979,7 @@ window.__ModuleLoader__.load({
5362
5979
 
5363
5980
  const renderWorkspaceEntry = (entry, depth, pulse) => {
5364
5981
  const { workspace } = entry
5365
- const count = countSessionTree(buildSessionTree(sessionsOf(workspace), sessionSlash))
5982
+ const count = countSessionTree(buildSessionTree(sessionsOf(workspace), sessionSlash, sessFolders, workspace.workspaceId))
5366
5983
  const rows = [E(WorkspaceRow, {
5367
5984
  key: 'ws-' + workspace.workspaceId,
5368
5985
  workspace,
@@ -5412,7 +6029,7 @@ window.__ModuleLoader__.load({
5412
6029
  depth,
5413
6030
  expanded,
5414
6031
  onToggle: () => { if (!searching) actions.setExpanded(idPath, !expanded) },
5415
- onContextMenu: (e) => openCtx('folder', { path: node.path, name: node.name, idPath }, e),
6032
+ onContextMenu: (e) => openCtx('folder', { path: node.path, name: node.name, idPath, scope: node.scope, empty: countWorkspaces(node) === 0 }, e),
5416
6033
  dropInto: wsDropInto(idPath),
5417
6034
  dragEvents: folderDropEvents(idPath),
5418
6035
  custStyle: rowStyleOf('folder:' + idPath),
@@ -5491,20 +6108,30 @@ window.__ModuleLoader__.load({
5491
6108
  // and offering the item would promise a container the tree no longer
5492
6109
  // renders. The two layers gate independently, so a session row keeps
5493
6110
  // the entry even when workspaces have gone disk-only.
5494
- const groupItem = { id: 'move-group', label: t('menu.moveToGroup'), icon: icon('IconFolderOutline16', 16) }
6111
+ // IconFolderOutline16 is NOT in this host's icon set (retired early):
6112
+ // probing it silently yielded a glyph-less row. IconFolderOpenOutline16
6113
+ // is present and reads as "move into a group" just as well.
6114
+ const groupItem = { id: 'move-group', label: t('menu.moveToGroup'), icon: icon('IconFolderOpenOutline16', 16) }
5495
6115
  const wsGroupItems = workspaceSlash ? [groupItem] : []
5496
6116
  const sgroupItems = sessionSlash ? [groupItem] : []
6117
+ // "Delete group" only makes sense for a DECLARED group that holds
6118
+ // nothing (v0.17.0): a group with members is a projection of their
6119
+ // titles, so removing the declaration would change nothing on screen
6120
+ // and the row would stay — offering it would be a lie.
6121
+ const deleteFolderEntry = { id: 'delete-folder', label: t('menu.deleteGroup'), icon: icon('IconTrashOutline16', 16), danger: true }
6122
+ const deleteSgroupEntry = { id: 'delete-sgroup', label: t('menu.deleteGroup'), icon: icon('IconTrashOutline16', 16), danger: true }
5497
6123
  if (ctx.kind === 'folder') return [
5498
6124
  { id: 'rename-folder', label: t('menu.renameFolder'), icon: icon('IconEditOutline16', 16) },
5499
- ...wsGroupItems,
6125
+ ...(ctx.payload && ctx.payload.empty ? [deleteFolderEntry] : []),
5500
6126
  { type: 'separator', id: 'bw-sep' },
6127
+ ...wsGroupItems,
5501
6128
  customizeEntry,
5502
6129
  ]
5503
6130
  if (ctx.kind === 'workspace') return [
5504
6131
  { id: 'rename', label: ot ? ot('rename') : t('menu.rename'), icon: icon('IconEditOutline16', 16) },
5505
6132
  { id: 'delete', label: ot ? ot('delete.workspace') : t('menu.delete'), icon: icon('IconTrashOutline16', 16), danger: true },
5506
- ...wsGroupItems,
5507
6133
  { type: 'separator', id: 'bw-sep' },
6134
+ ...wsGroupItems,
5508
6135
  customizeEntry,
5509
6136
  ]
5510
6137
  if (ctx.kind === 'session') return [
@@ -5514,12 +6141,13 @@ window.__ModuleLoader__.load({
5514
6141
  // registry-global archive set and never touches the session log —
5515
6142
  // deliberately NOT styled destructive.
5516
6143
  { id: 'archive', label: ot ? ot('menu.archiveSession') : t('menu.archive'), icon: icon('IconArchiveOutline20', 16) },
5517
- ...sgroupItems,
5518
6144
  { type: 'separator', id: 'bw-sep' },
6145
+ ...sgroupItems,
5519
6146
  customizeEntry,
5520
6147
  ]
5521
6148
  return [
5522
6149
  { id: 'rename-sgroup', label: t('menu.renameSgroup'), icon: icon('IconEditOutline16', 16) },
6150
+ ...(ctx.payload && ctx.payload.empty ? [deleteSgroupEntry] : []),
5523
6151
  ...sgroupItems,
5524
6152
  { type: 'separator', id: 'bw-sep' },
5525
6153
  customizeEntry,
@@ -5548,6 +6176,8 @@ window.__ModuleLoader__.load({
5548
6176
  setDialog({ kind: 'group-move', target, initial: groupPrefixOf(target.title), groupPaths: collectGroupPaths(target.kind) })
5549
6177
  }
5550
6178
  else if (kind === 'folder' && id === 'rename-folder') setDialog({ kind: 'folder-rename', path: payload.path })
6179
+ else if (kind === 'folder' && id === 'delete-folder') setDialog({ kind: 'folder-delete', path: payload.path, scope: payload.scope, name: payload.name })
6180
+ else if (kind === 'sgroup' && id === 'delete-sgroup') setDialog({ kind: 'sgroup-delete', target: payload })
5551
6181
  else if (kind === 'workspace' && id === 'rename') setDialog({ kind: 'ws-rename', workspace: payload })
5552
6182
  else if (kind === 'workspace' && id === 'delete') setDialog({ kind: 'ws-delete', workspace: payload })
5553
6183
  else if (kind === 'sgroup' && id === 'rename-sgroup') setDialog({ kind: 'sgroup-rename', target: payload })
@@ -5584,6 +6214,25 @@ window.__ModuleLoader__.load({
5584
6214
  onClose: () => setDialog(null),
5585
6215
  t,
5586
6216
  })
6217
+ // Declared-empty groups are the only ones that can be deleted (the menu
6218
+ // only offers it there): removing the declaration of a group whose
6219
+ // members still carry the prefix would change nothing on screen.
6220
+ if (dialog.kind === 'folder-delete') return E(ConfirmDialog, {
6221
+ key: 'folder-delete',
6222
+ title: t('group.delete.title'),
6223
+ body: t('group.delete.body', { name: dialog.name }),
6224
+ onConfirm: () => { actions.removeFolder('workspace', dialog.scope, dialog.path); setDialog(null) },
6225
+ onClose: () => setDialog(null),
6226
+ t,
6227
+ })
6228
+ if (dialog.kind === 'sgroup-delete') return E(ConfirmDialog, {
6229
+ key: 'sgroup-delete',
6230
+ title: t('group.delete.title'),
6231
+ body: t('group.delete.body', { name: dialog.target.name }),
6232
+ onConfirm: () => { actions.removeFolder('session', dialog.target.workspaceId, dialog.target.path); setDialog(null) },
6233
+ onClose: () => setDialog(null),
6234
+ t,
6235
+ })
5587
6236
  if (dialog.kind === 'ws-delete') return E(ConfirmDialog, {
5588
6237
  key: 'ws-delete',
5589
6238
  title: t('ws.delete.title'),
@@ -5618,6 +6267,15 @@ window.__ModuleLoader__.load({
5618
6267
  onClose: () => setDialog(null),
5619
6268
  t,
5620
6269
  })
6270
+ if (dialog.kind === 'group-new') return E(GroupNewDialog, {
6271
+ key: 'group-new',
6272
+ kinds: newGroupLayers(),
6273
+ initialKind: dialog.layer === 'workspace' ? 'workspace' : 'session',
6274
+ treeOf: newGroupTreeOf,
6275
+ onConfirm: (kind, scope, path) => { actions.addFolder(kind, scope, path); setDialog(null) },
6276
+ onClose: () => setDialog(null),
6277
+ t,
6278
+ })
5621
6279
  if (dialog.kind === 'group-move') return E(GroupDialog, {
5622
6280
  key: 'group-move',
5623
6281
  title: t('group.move.title'),
@@ -5689,17 +6347,18 @@ window.__ModuleLoader__.load({
5689
6347
  StyleNode(),
5690
6348
  E('div', { className: 'bw-header' },
5691
6349
  E('div', { className: 'bw-header-title' }, t('title')),
5692
- // Group discoverability (v0.15): a visible "New group" entry point
5693
- // that doubles as the row-drop zone. Clicking opens the picker
5694
- // shape; dropping a row onto it locks that row in the same dialog.
6350
+ // Group discoverability: an ICON entry point (v0.17.0 it used to be
6351
+ // a text button, which crowded the header) that doubles as the row-drop
6352
+ // zone. Its tooltip names the layer(s) the button can actually create
6353
+ // into; the click opens the NEW-group dialog, not the move dialog.
5695
6354
  (workspaceSlash || sessionSlash) ? E('button', {
5696
6355
  type: 'button',
5697
- className: cls('bw-newgroup-btn', groupDropTarget() && 'bw-drop-into-strong'),
5698
- 'aria-label': t('group.new'),
5699
- title: t('group.new'),
5700
- onClick: () => setDialog({ kind: 'group-move', target: null, targets: targetListOf(), initial: '', groupPaths: collectGroupPaths(pickerKindOf()) }),
6356
+ className: cls('bw-icon-btn', 'bw-newgroup-btn', groupDropTarget() && 'bw-drop-into-strong'),
6357
+ 'aria-label': newGroupLabel(),
6358
+ title: newGroupLabel(),
6359
+ onClick: () => setDialog({ kind: 'group-new', layer: defaultNewGroupLayer() }),
5701
6360
  ...groupDropEvents,
5702
- }, t('group.new')) : null,
6361
+ }, icon('IconFolderOpen16', 16)) : null,
5703
6362
  (searchOpen || query !== '') ? E('input', {
5704
6363
  className: 'bw-input',
5705
6364
  style: { width: 130, flex: 'none' },