dsh-better-workspace 0.11.2 → 0.11.4

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
@@ -27,6 +27,8 @@ web/ ← 虚拟分组(不是真实目录,纯命名层级)
27
27
 
28
28
  点「添加工作区」并选完文件夹后,弹出一个小对话框,一行选择工作区所属分组——直接输入(如 `web`)、从下拉里选已有层级(datalist 原生补全),留空就是根分组。确认后插件创建工作区并把分组前缀写进名称,树上自然落位。
29
29
 
30
+ **用哪种目录选择器,跟随宿主实际组装的后端**:webserver 只绑本机 loopback 时是宿主桌面上的**原生选择器**;绑到 `0.0.0.0`(局域网访问)、SSH 转发或宿主没有可用显示会话时,宿主只组装 `browse` 后端——它仅提供「列目录 / 新建目录」原语,`pick` 会被明确拒绝——插件这时改用**应用内目录浏览器**(Home 起点的面包屑、可编辑路径、行内新建文件夹、显示隐藏文件),选定目录后照样进同一个「所属分组」对话框。若确实想在这种绑定下走原生选择器,只能由用户显式钉死宿主的 native 后端包,而那会在**服务器桌面**上弹窗,局域网客户端根本够不着。
31
+
30
32
  ### 分组的增删
31
33
 
32
34
  右键任意分组行即可:**新增子分组**(路径自动带上父分组前缀)、**新增子工作区**(选完目录后所属分组输入框自动预填)、重命名分组、删除空分组;标题栏不再放独立按钮。空分组持久保存在浏览器本地;一旦有工作区落进去,分组就由名称自然派生。
package/README_EN.md CHANGED
@@ -25,6 +25,8 @@ web/ <- virtual folder (naming only, not a real directory)
25
25
 
26
26
  After you pick a directory for a new workspace, a small dialog asks for the parent group — type one (`web`), pick an existing level from the datalist, or leave it empty for the root. The plugin creates the workspace and writes the prefix into its title, so it lands on the right branch of the tree.
27
27
 
28
+ **Which picker you get follows the backend the host actually composes.** A loopback-only webserver bind gives you the host's **native OS chooser**; an all-interfaces/LAN bind, an SSH launch, or a host without a usable display session composes the `browse` backend instead — listing and directory creation only, with `pick` refused by design — and the plugin then uses an **in-app directory browser** (Home-rooted breadcrumbs, editable path, inline new-folder row, hidden-file toggle) before the very same parent-group dialog. Forcing the native chooser under such a bind means pinning the host's native backend package yourself, and that dialog opens on the **server's** desktop, out of reach for LAN clients.
29
+
28
30
  ### Folder create/delete via context menu
29
31
 
30
32
  Right-click any folder row: new subfolder (parent prefix pre-filled), new workspace here (the add-flow group field pre-fills with this folder), rename folder, delete empty folder — no standalone header button anymore. Explicit empty folders persist in the browser until workspaces live inside them.
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-better-workspace",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
4
4
  "main": "./src/index.js",
5
5
  "description": "侧边栏工作区层级树:名称中的 / 即虚拟分组(web/前端 · web/后端),添加工作区的目录流附所属分组弹窗,重命名即时重排层级,可新建空分组。Hierarchy tree for the DSH sidebar workspace list.",
6
6
  "engines": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-better-workspace",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
4
4
  "description": "DSH web plugin: a hierarchical workspace tree for the sidebar. Workspace titles containing \"/\" group into virtual folders (web/前端 · web/后端); the add-workspace directory flow gains a parent-group popup; renames re-derive the tree; a new-folder button creates empty levels.",
5
5
  "type": "module",
6
6
  "repository": {
package/src/client.js CHANGED
@@ -116,6 +116,20 @@ window.__ModuleLoader__.load({
116
116
  'flow.parent': '所属分组',
117
117
  'flow.parentHint': '输入或下拉选择分组路径,留空表示根分组;多级用 / 分隔',
118
118
  'flow.creating': '正在创建…',
119
+ 'browse.title': '选择工作区目录',
120
+ 'browse.home': '主目录',
121
+ 'browse.up': '上一层',
122
+ 'browse.newFolder': '新建文件夹',
123
+ 'browse.folderName': '文件夹名称',
124
+ 'browse.empty': '此文件夹没有子文件夹',
125
+ 'browse.loading': '加载中…',
126
+ 'browse.truncated': '文件夹过多,仅显示开头部分。',
127
+ 'browse.showHidden': '显示隐藏文件',
128
+ 'browse.editPath': '编辑路径',
129
+ 'browse.select': '选择此文件夹',
130
+ 'browse.enter': '进入',
131
+ 'browse.drives': '盘符',
132
+ 'browse.selectNamed': '选择「{name}」',
119
133
  'error.title': '出错了',
120
134
  'cancel': '取消',
121
135
  'create': '创建',
@@ -224,6 +238,20 @@ window.__ModuleLoader__.load({
224
238
  'flow.parent': 'Parent group',
225
239
  'flow.parentHint': 'Type or pick a group path; empty means root. Nest with /',
226
240
  'flow.creating': 'Creating…',
241
+ 'browse.title': 'Select Workspace Directory',
242
+ 'browse.home': 'Home',
243
+ 'browse.up': 'Up',
244
+ 'browse.newFolder': 'New folder',
245
+ 'browse.folderName': 'Folder name',
246
+ 'browse.empty': 'No subfolders here',
247
+ 'browse.loading': 'Loading…',
248
+ 'browse.truncated': 'Too many folders to list; only the beginning is shown.',
249
+ 'browse.showHidden': 'Show hidden files',
250
+ 'browse.editPath': 'Edit path',
251
+ 'browse.select': 'Use this folder',
252
+ 'browse.enter': 'Open',
253
+ 'browse.drives': 'Drives',
254
+ 'browse.selectNamed': 'Use "{name}"',
227
255
  'error.title': 'Something went wrong',
228
256
  'cancel': 'Cancel',
229
257
  'create': 'Create',
@@ -340,6 +368,20 @@ window.__ModuleLoader__.load({
340
368
  'flow.parent': 'المجموعة التابعة لها',
341
369
  'flow.parentHint': 'اكتب مسار المجموعة أو اختره؛ الفراغ يعني الجذر؛ تُفصل المستويات بـ /',
342
370
  'flow.creating': 'جارٍ الإنشاء…',
371
+ 'browse.title': 'اختيار مجلد مساحة العمل',
372
+ 'browse.home': 'المجلد الرئيسي',
373
+ 'browse.up': 'المستوى الأعلى',
374
+ 'browse.newFolder': 'مجلد جديد',
375
+ 'browse.folderName': 'اسم المجلد',
376
+ 'browse.empty': 'لا توجد مجلدات فرعية هنا',
377
+ 'browse.loading': 'جارٍ التحميل…',
378
+ 'browse.truncated': 'المجلدات كثيرة جدًا؛ يُعرض الجزء الأول فقط.',
379
+ 'browse.showHidden': 'إظهار الملفات المخفية',
380
+ 'browse.editPath': 'تعديل المسار',
381
+ 'browse.select': 'استخدام هذا المجلد',
382
+ 'browse.enter': 'فتح',
383
+ 'browse.drives': 'الأقراص',
384
+ 'browse.selectNamed': 'استخدام "{name}"',
343
385
  'error.title': 'حدث خطأ ما',
344
386
  'cancel': 'إلغاء',
345
387
  'create': 'إنشاء',
@@ -448,6 +490,20 @@ window.__ModuleLoader__.load({
448
490
  'flow.parent': 'Übergeordnete Gruppe',
449
491
  'flow.parentHint': 'Gruppenpfad eingeben oder auswählen, leer bedeutet Wurzel; Ebenen mit / trennen',
450
492
  'flow.creating': 'Wird erstellt…',
493
+ 'browse.title': 'Arbeitsbereichsordner wählen',
494
+ 'browse.home': 'Home',
495
+ 'browse.up': 'Übergeordnet',
496
+ 'browse.newFolder': 'Neuer Ordner',
497
+ 'browse.folderName': 'Ordnername',
498
+ 'browse.empty': 'Keine Unterordner vorhanden',
499
+ 'browse.loading': 'Wird geladen…',
500
+ 'browse.truncated': 'Zu viele Ordner; nur der Anfang wird angezeigt.',
501
+ 'browse.showHidden': 'Versteckte Dateien anzeigen',
502
+ 'browse.editPath': 'Pfad bearbeiten',
503
+ 'browse.select': 'Diesen Ordner verwenden',
504
+ 'browse.enter': 'Öffnen',
505
+ 'browse.drives': 'Laufwerke',
506
+ 'browse.selectNamed': '"{name}" verwenden',
451
507
  'error.title': 'Etwas ist schiefgelaufen',
452
508
  'cancel': 'Abbrechen',
453
509
  'create': 'Erstellen',
@@ -556,6 +612,20 @@ window.__ModuleLoader__.load({
556
612
  'flow.parent': 'Groupe parent',
557
613
  'flow.parentHint': 'Saisissez ou choisissez un chemin de groupe; vide = racine; niveaux séparés par /',
558
614
  'flow.creating': 'Création…',
615
+ 'browse.title': 'Choisir le dossier de l’espace de travail',
616
+ 'browse.home': 'Dossier personnel',
617
+ 'browse.up': 'Niveau supérieur',
618
+ 'browse.newFolder': 'Nouveau dossier',
619
+ 'browse.folderName': 'Nom du dossier',
620
+ 'browse.empty': 'Aucun sous-dossier ici',
621
+ 'browse.loading': 'Chargement…',
622
+ 'browse.truncated': 'Trop de dossiers ; seul le début est affiché.',
623
+ 'browse.showHidden': 'Afficher les fichiers cachés',
624
+ 'browse.editPath': 'Modifier le chemin',
625
+ 'browse.select': 'Utiliser ce dossier',
626
+ 'browse.enter': 'Ouvrir',
627
+ 'browse.drives': 'Lecteurs',
628
+ 'browse.selectNamed': 'Utiliser « {name} »',
559
629
  'error.title': 'Une erreur est survenue',
560
630
  'cancel': 'Annuler',
561
631
  'create': 'Créer',
@@ -664,6 +734,20 @@ window.__ModuleLoader__.load({
664
734
  'flow.parent': 'संबंधित समूह',
665
735
  'flow.parentHint': 'समूह पथ लिखें या चुनें; खाली का अर्थ मूल है; कई स्तर / से अलग करें',
666
736
  'flow.creating': 'बनाया जा रहा है…',
737
+ 'browse.title': 'कार्यस्थान फ़ोल्डर चुनें',
738
+ 'browse.home': 'होम',
739
+ 'browse.up': 'ऊपर',
740
+ 'browse.newFolder': 'नया फ़ोल्डर',
741
+ 'browse.folderName': 'फ़ोल्डर का नाम',
742
+ 'browse.empty': 'यहाँ कोई उप-फ़ोल्डर नहीं',
743
+ 'browse.loading': 'लोड हो रहा है…',
744
+ 'browse.truncated': 'बहुत अधिक फ़ोल्डर; केवल शुरुआत दिखाई गई है।',
745
+ 'browse.showHidden': 'छिपी फ़ाइलें दिखाएँ',
746
+ 'browse.editPath': 'पथ संपादित करें',
747
+ 'browse.select': 'यह फ़ोल्डर चुनें',
748
+ 'browse.enter': 'खोलें',
749
+ 'browse.drives': 'ड्राइव',
750
+ 'browse.selectNamed': '"{name}" चुनें',
667
751
  'error.title': 'कुछ गड़बड़ हो गई',
668
752
  'cancel': 'रद्द करें',
669
753
  'create': 'बनाएँ',
@@ -772,6 +856,20 @@ window.__ModuleLoader__.load({
772
856
  'flow.parent': 'Grup induk',
773
857
  'flow.parentHint': 'Ketik atau pilih jalur grup; kosong = akar; tingkat dipisah dengan /',
774
858
  'flow.creating': 'Membuat…',
859
+ 'browse.title': 'Pilih folder ruang kerja',
860
+ 'browse.home': 'Beranda',
861
+ 'browse.up': 'Naik',
862
+ 'browse.newFolder': 'Folder baru',
863
+ 'browse.folderName': 'Nama folder',
864
+ 'browse.empty': 'Tidak ada subfolder di sini',
865
+ 'browse.loading': 'Memuat…',
866
+ 'browse.truncated': 'Terlalu banyak folder; hanya bagian awal yang ditampilkan.',
867
+ 'browse.showHidden': 'Tampilkan file tersembunyi',
868
+ 'browse.editPath': 'Edit jalur',
869
+ 'browse.select': 'Gunakan folder ini',
870
+ 'browse.enter': 'Buka',
871
+ 'browse.drives': 'Drive',
872
+ 'browse.selectNamed': 'Gunakan "{name}"',
775
873
  'error.title': 'Terjadi kesalahan',
776
874
  'cancel': 'Batal',
777
875
  'create': 'Buat',
@@ -880,6 +978,20 @@ window.__ModuleLoader__.load({
880
978
  'flow.parent': 'Gruppo di appartenenza',
881
979
  'flow.parentHint': 'Digita o scegli un percorso di gruppo; vuoto = radice; livelli separati da /',
882
980
  'flow.creating': 'Creazione…',
981
+ 'browse.title': 'Scegli cartella dell’area di lavoro',
982
+ 'browse.home': 'Home',
983
+ 'browse.up': 'Livello superiore',
984
+ 'browse.newFolder': 'Nuova cartella',
985
+ 'browse.folderName': 'Nome cartella',
986
+ 'browse.empty': 'Nessuna sottocartella qui',
987
+ 'browse.loading': 'Caricamento…',
988
+ 'browse.truncated': 'Troppe cartelle; è mostrato solo l’inizio.',
989
+ 'browse.showHidden': 'Mostra file nascosti',
990
+ 'browse.editPath': 'Modifica percorso',
991
+ 'browse.select': 'Usa questa cartella',
992
+ 'browse.enter': 'Apri',
993
+ 'browse.drives': 'Unità',
994
+ 'browse.selectNamed': 'Usa "{name}"',
883
995
  'error.title': 'Si è verificato un errore',
884
996
  'cancel': 'Annulla',
885
997
  'create': 'Crea',
@@ -988,6 +1100,20 @@ window.__ModuleLoader__.load({
988
1100
  'flow.parent': '所属グループ',
989
1101
  'flow.parentHint': 'グループのパスを入力または選択します,空欄はルート,階層は / で区切ります',
990
1102
  'flow.creating': '作成中…',
1103
+ 'browse.title': 'ワークスペースのフォルダーを選択',
1104
+ 'browse.home': 'ホーム',
1105
+ 'browse.up': '上の階層',
1106
+ 'browse.newFolder': '新しいフォルダー',
1107
+ 'browse.folderName': 'フォルダー名',
1108
+ 'browse.empty': 'ここにサブフォルダーはありません',
1109
+ 'browse.loading': '読み込み中…',
1110
+ 'browse.truncated': 'フォルダーが多すぎます。先頭のみ表示しています。',
1111
+ 'browse.showHidden': '隠しファイルを表示',
1112
+ 'browse.editPath': 'パスを編集',
1113
+ 'browse.select': 'このフォルダーを選択',
1114
+ 'browse.enter': '開く',
1115
+ 'browse.drives': 'ドライブ',
1116
+ 'browse.selectNamed': '「{name}」を選択',
991
1117
  'error.title': 'エラーが発生しました',
992
1118
  'cancel': 'キャンセル',
993
1119
  'create': '作成',
@@ -1096,6 +1222,20 @@ window.__ModuleLoader__.load({
1096
1222
  'flow.parent': '소속 그룹',
1097
1223
  'flow.parentHint': '그룹 경로를 입력하거나 선택하세요, 비우면 루트, 여러 단계는 / 로 구분합니다',
1098
1224
  'flow.creating': '만드는 중…',
1225
+ 'browse.title': '워크스페이스 폴더 선택',
1226
+ 'browse.home': '홈',
1227
+ 'browse.up': '상위',
1228
+ 'browse.newFolder': '새 폴더',
1229
+ 'browse.folderName': '폴더 이름',
1230
+ 'browse.empty': '여기에 하위 폴더가 없습니다',
1231
+ 'browse.loading': '불러오는 중…',
1232
+ 'browse.truncated': '폴더가 너무 많습니다. 앞부분만 표시합니다.',
1233
+ 'browse.showHidden': '숨김 파일 표시',
1234
+ 'browse.editPath': '경로 편집',
1235
+ 'browse.select': '이 폴더 선택',
1236
+ 'browse.enter': '열기',
1237
+ 'browse.drives': '드라이브',
1238
+ 'browse.selectNamed': '"{name}" 선택',
1099
1239
  'error.title': '문제가 발생했습니다',
1100
1240
  'cancel': '취소',
1101
1241
  'create': '만들기',
@@ -1204,6 +1344,20 @@ window.__ModuleLoader__.load({
1204
1344
  'flow.parent': 'Bovenliggende groep',
1205
1345
  'flow.parentHint': 'Typ of kies een groeppad; leeg betekent hoofdmap; niveaus scheiden met /',
1206
1346
  'flow.creating': 'Bezig met aanmaken…',
1347
+ 'browse.title': 'Werkruimtemap kiezen',
1348
+ 'browse.home': 'Home',
1349
+ 'browse.up': 'Omhoog',
1350
+ 'browse.newFolder': 'Nieuwe map',
1351
+ 'browse.folderName': 'Mapnaam',
1352
+ 'browse.empty': 'Geen submappen hier',
1353
+ 'browse.loading': 'Laden…',
1354
+ 'browse.truncated': 'Te veel mappen; alleen het begin wordt getoond.',
1355
+ 'browse.showHidden': 'Verborgen bestanden tonen',
1356
+ 'browse.editPath': 'Pad bewerken',
1357
+ 'browse.select': 'Deze map gebruiken',
1358
+ 'browse.enter': 'Openen',
1359
+ 'browse.drives': 'Stations',
1360
+ 'browse.selectNamed': '"{name}" gebruiken',
1207
1361
  'error.title': 'Er is iets misgegaan',
1208
1362
  'cancel': 'Annuleren',
1209
1363
  'create': 'Aanmaken',
@@ -1312,6 +1466,20 @@ window.__ModuleLoader__.load({
1312
1466
  'flow.parent': 'Grupa nadrzędna',
1313
1467
  'flow.parentHint': 'Wpisz lub wybierz ścieżkę grupy; puste = główna; poziomy rozdziela /',
1314
1468
  'flow.creating': 'Tworzenie…',
1469
+ 'browse.title': 'Wybierz folder obszaru roboczego',
1470
+ 'browse.home': 'Katalog domowy',
1471
+ 'browse.up': 'W górę',
1472
+ 'browse.newFolder': 'Nowy folder',
1473
+ 'browse.folderName': 'Nazwa folderu',
1474
+ 'browse.empty': 'Brak podfolderów',
1475
+ 'browse.loading': 'Wczytywanie…',
1476
+ 'browse.truncated': 'Zbyt wiele folderów; pokazano tylko początek.',
1477
+ 'browse.showHidden': 'Pokaż ukryte pliki',
1478
+ 'browse.editPath': 'Edytuj ścieżkę',
1479
+ 'browse.select': 'Użyj tego folderu',
1480
+ 'browse.enter': 'Otwórz',
1481
+ 'browse.drives': 'Dyski',
1482
+ 'browse.selectNamed': 'Użyj "{name}"',
1315
1483
  'error.title': 'Coś poszło nie tak',
1316
1484
  'cancel': 'Anuluj',
1317
1485
  'create': 'Utwórz',
@@ -1420,6 +1588,20 @@ window.__ModuleLoader__.load({
1420
1588
  'flow.parent': 'Grupo pai',
1421
1589
  'flow.parentHint': 'Digite ou escolha o caminho do grupo; vazio = raiz; níveis separados por /',
1422
1590
  'flow.creating': 'Criando…',
1591
+ 'browse.title': 'Escolher pasta da área de trabalho',
1592
+ 'browse.home': 'Início',
1593
+ 'browse.up': 'Acima',
1594
+ 'browse.newFolder': 'Nova pasta',
1595
+ 'browse.folderName': 'Nome da pasta',
1596
+ 'browse.empty': 'Nenhuma subpasta aqui',
1597
+ 'browse.loading': 'Carregando…',
1598
+ 'browse.truncated': 'Pastas demais; apenas o início é exibido.',
1599
+ 'browse.showHidden': 'Mostrar arquivos ocultos',
1600
+ 'browse.editPath': 'Editar caminho',
1601
+ 'browse.select': 'Usar esta pasta',
1602
+ 'browse.enter': 'Abrir',
1603
+ 'browse.drives': 'Unidades',
1604
+ 'browse.selectNamed': 'Usar "{name}"',
1423
1605
  'error.title': 'Algo deu errado',
1424
1606
  'cancel': 'Cancelar',
1425
1607
  'create': 'Criar',
@@ -1528,6 +1710,20 @@ window.__ModuleLoader__.load({
1528
1710
  'flow.parent': 'Родительская группа',
1529
1711
  'flow.parentHint': 'Введите или выберите путь группы; пусто — корень; уровни разделяются /',
1530
1712
  'flow.creating': 'Создание…',
1713
+ 'browse.title': 'Выбор папки рабочей области',
1714
+ 'browse.home': 'Домашняя папка',
1715
+ 'browse.up': 'На уровень выше',
1716
+ 'browse.newFolder': 'Новая папка',
1717
+ 'browse.folderName': 'Имя папки',
1718
+ 'browse.empty': 'Здесь нет вложенных папок',
1719
+ 'browse.loading': 'Загрузка…',
1720
+ 'browse.truncated': 'Слишком много папок; показано только начало.',
1721
+ 'browse.showHidden': 'Показывать скрытые файлы',
1722
+ 'browse.editPath': 'Изменить путь',
1723
+ 'browse.select': 'Выбрать эту папку',
1724
+ 'browse.enter': 'Открыть',
1725
+ 'browse.drives': 'Диски',
1726
+ 'browse.selectNamed': 'Выбрать «{name}»',
1531
1727
  'error.title': 'Что-то пошло не так',
1532
1728
  'cancel': 'Отмена',
1533
1729
  'create': 'Создать',
@@ -1636,6 +1832,20 @@ window.__ModuleLoader__.load({
1636
1832
  'flow.parent': 'Överordnad grupp',
1637
1833
  'flow.parentHint': 'Skriv eller välj en gruppsökväg; tomt = rot; nivåer avgränsas med /',
1638
1834
  'flow.creating': 'Skapar…',
1835
+ 'browse.title': 'Välj arbetsyte-mapp',
1836
+ 'browse.home': 'Hem',
1837
+ 'browse.up': 'Upp',
1838
+ 'browse.newFolder': 'Ny mapp',
1839
+ 'browse.folderName': 'Mappnamn',
1840
+ 'browse.empty': 'Inga undermappar här',
1841
+ 'browse.loading': 'Laddar…',
1842
+ 'browse.truncated': 'För många mappar; endast början visas.',
1843
+ 'browse.showHidden': 'Visa dolda filer',
1844
+ 'browse.editPath': 'Redigera sökväg',
1845
+ 'browse.select': 'Använd den här mappen',
1846
+ 'browse.enter': 'Öppna',
1847
+ 'browse.drives': 'Enheter',
1848
+ 'browse.selectNamed': 'Använd "{name}"',
1639
1849
  'error.title': 'Något gick fel',
1640
1850
  'cancel': 'Avbryt',
1641
1851
  'create': 'Skapa',
@@ -1744,6 +1954,20 @@ window.__ModuleLoader__.load({
1744
1954
  'flow.parent': 'กลุ่มต้นทาง',
1745
1955
  'flow.parentHint': 'พิมพ์หรือเลือกเส้นทางกลุ่ม เว้นว่างคือระดับราก หลายระดับคั่นด้วย /',
1746
1956
  'flow.creating': 'กำลังสร้าง…',
1957
+ 'browse.title': 'เลือกโฟลเดอร์เวิร์กสเปซ',
1958
+ 'browse.home': 'โฮม',
1959
+ 'browse.up': 'ขึ้นหนึ่งระดับ',
1960
+ 'browse.newFolder': 'โฟลเดอร์ใหม่',
1961
+ 'browse.folderName': 'ชื่อโฟลเดอร์',
1962
+ 'browse.empty': 'ไม่มีโฟลเดอร์ย่อยที่นี่',
1963
+ 'browse.loading': 'กำลังโหลด…',
1964
+ 'browse.truncated': 'มีโฟลเดอร์มากเกินไป แสดงเฉพาะส่วนต้น',
1965
+ 'browse.showHidden': 'แสดงไฟล์ที่ซ่อนอยู่',
1966
+ 'browse.editPath': 'แก้ไขเส้นทาง',
1967
+ 'browse.select': 'ใช้โฟลเดอร์นี้',
1968
+ 'browse.enter': 'เปิด',
1969
+ 'browse.drives': 'ไดรฟ์',
1970
+ 'browse.selectNamed': 'ใช้ "{name}"',
1747
1971
  'error.title': 'เกิดข้อผิดพลาด',
1748
1972
  'cancel': 'ยกเลิก',
1749
1973
  'create': 'สร้าง',
@@ -1852,6 +2076,20 @@ window.__ModuleLoader__.load({
1852
2076
  'flow.parent': 'Bağlı olduğu grup',
1853
2077
  'flow.parentHint': 'Grup yolunu yazın veya seçin; boş = kök; düzeyler / ile ayrılır',
1854
2078
  'flow.creating': 'Oluşturuluyor…',
2079
+ 'browse.title': 'Çalışma alanı klasörünü seç',
2080
+ 'browse.home': 'Ana dizin',
2081
+ 'browse.up': 'Üst düzey',
2082
+ 'browse.newFolder': 'Yeni klasör',
2083
+ 'browse.folderName': 'Klasör adı',
2084
+ 'browse.empty': 'Burada alt klasör yok',
2085
+ 'browse.loading': 'Yükleniyor…',
2086
+ 'browse.truncated': 'Çok fazla klasör var; yalnızca başlangıç gösteriliyor.',
2087
+ 'browse.showHidden': 'Gizli dosyaları göster',
2088
+ 'browse.editPath': 'Yolu düzenle',
2089
+ 'browse.select': 'Bu klasörü kullan',
2090
+ 'browse.enter': 'Aç',
2091
+ 'browse.drives': 'Sürücüler',
2092
+ 'browse.selectNamed': '"{name}" kullan',
1855
2093
  'error.title': 'Bir şeyler ters gitti',
1856
2094
  'cancel': 'İptal',
1857
2095
  'create': 'Oluştur',
@@ -1960,6 +2198,20 @@ window.__ModuleLoader__.load({
1960
2198
  'flow.parent': 'Nhóm trực thuộc',
1961
2199
  'flow.parentHint': 'Nhập hoặc chọn đường dẫn nhóm; để trống là gốc; nhiều tầng cách nhau bằng /',
1962
2200
  'flow.creating': 'Đang tạo…',
2201
+ 'browse.title': 'Chọn thư mục không gian làm việc',
2202
+ 'browse.home': 'Thư mục chính',
2203
+ 'browse.up': 'Lên một cấp',
2204
+ 'browse.newFolder': 'Thư mục mới',
2205
+ 'browse.folderName': 'Tên thư mục',
2206
+ 'browse.empty': 'Không có thư mục con ở đây',
2207
+ 'browse.loading': 'Đang tải…',
2208
+ 'browse.truncated': 'Quá nhiều thư mục; chỉ hiển thị phần đầu.',
2209
+ 'browse.showHidden': 'Hiện tệp ẩn',
2210
+ 'browse.editPath': 'Sửa đường dẫn',
2211
+ 'browse.select': 'Dùng thư mục này',
2212
+ 'browse.enter': 'Mở',
2213
+ 'browse.drives': 'Ổ đĩa',
2214
+ 'browse.selectNamed': 'Dùng "{name}"',
1963
2215
  'error.title': 'Đã xảy ra lỗi',
1964
2216
  'cancel': 'Hủy',
1965
2217
  'create': 'Tạo',
@@ -2068,6 +2320,20 @@ window.__ModuleLoader__.load({
2068
2320
  'flow.parent': '所屬分組',
2069
2321
  'flow.parentHint': '輸入或者揀分組路徑,留空代表根分組;多級用 / 分隔',
2070
2322
  'flow.creating': '建立緊…',
2323
+ 'browse.title': '揀工作區資料夾',
2324
+ 'browse.home': '主目錄',
2325
+ 'browse.up': '上一層',
2326
+ 'browse.newFolder': '新增資料夾',
2327
+ 'browse.folderName': '資料夾名稱',
2328
+ 'browse.empty': '呢個資料夾冇子資料夾',
2329
+ 'browse.loading': '載入緊…',
2330
+ 'browse.truncated': '資料夾太多,只顯示開頭部分。',
2331
+ 'browse.showHidden': '顯示隱藏檔案',
2332
+ 'browse.editPath': '編輯路徑',
2333
+ 'browse.select': '揀呢個資料夾',
2334
+ 'browse.enter': '入去',
2335
+ 'browse.drives': '磁碟',
2336
+ 'browse.selectNamed': '揀「{name}」',
2071
2337
  'error.title': '出咗錯',
2072
2338
  'cancel': '取消',
2073
2339
  'create': '建立',
@@ -2176,6 +2442,20 @@ window.__ModuleLoader__.load({
2176
2442
  'flow.parent': '所屬分組',
2177
2443
  'flow.parentHint': '輸入或者揀分組路徑,留空代表根分組;多級用 / 分隔',
2178
2444
  'flow.creating': '建立緊…',
2445
+ 'browse.title': '揀工作區資料夾',
2446
+ 'browse.home': '主目錄',
2447
+ 'browse.up': '上一層',
2448
+ 'browse.newFolder': '新增資料夾',
2449
+ 'browse.folderName': '資料夾名稱',
2450
+ 'browse.empty': '呢個資料夾冇子資料夾',
2451
+ 'browse.loading': '載入緊…',
2452
+ 'browse.truncated': '資料夾太多,只顯示開頭部分。',
2453
+ 'browse.showHidden': '顯示隱藏檔案',
2454
+ 'browse.editPath': '編輯路徑',
2455
+ 'browse.select': '揀呢個資料夾',
2456
+ 'browse.enter': '入去',
2457
+ 'browse.drives': '磁碟',
2458
+ 'browse.selectNamed': '揀「{name}」',
2179
2459
  'error.title': '出咗錯',
2180
2460
  'cancel': '取消',
2181
2461
  'create': '建立',
@@ -2284,6 +2564,20 @@ window.__ModuleLoader__.load({
2284
2564
  'flow.parent': '所屬群組',
2285
2565
  'flow.parentHint': '輸入或下拉選擇群組路徑,留空表示根群組;多層用 / 分隔',
2286
2566
  'flow.creating': '正在建立…',
2567
+ 'browse.title': '選擇工作區資料夾',
2568
+ 'browse.home': '主目錄',
2569
+ 'browse.up': '上一層',
2570
+ 'browse.newFolder': '新增資料夾',
2571
+ 'browse.folderName': '資料夾名稱',
2572
+ 'browse.empty': '此資料夾沒有子資料夾',
2573
+ 'browse.loading': '載入中…',
2574
+ 'browse.truncated': '資料夾過多,僅顯示開頭部分。',
2575
+ 'browse.showHidden': '顯示隱藏檔案',
2576
+ 'browse.editPath': '編輯路徑',
2577
+ 'browse.select': '選擇此資料夾',
2578
+ 'browse.enter': '進入',
2579
+ 'browse.drives': '磁碟',
2580
+ 'browse.selectNamed': '選擇「{name}」',
2287
2581
  'error.title': '發生錯誤',
2288
2582
  'cancel': '取消',
2289
2583
  'create': '建立',
@@ -2798,6 +3092,45 @@ window.__ModuleLoader__.load({
2798
3092
  '.bw-sync-btn{padding:5px 12px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.3));background:transparent;color:var(--dsw-alias-label-secondary,#b8b8b8);cursor:pointer;font-size:12px}',
2799
3093
  '.bw-sync-btn:disabled{opacity:.45;cursor:default}',
2800
3094
  '.bw-sync-btn-primary{border-color:var(--dsw-alias-brand-primary,#5b8def);color:var(--dsw-alias-label-primary,#e6e6e6)}',
3095
+ /*
3096
+ * In-app directory browser (Hosts composing the browse backend). Every
3097
+ * surface rides the theme token chain like the rest of the plugin — the
3098
+ * list panel uses bg-layer-2 so a transparent/background plugin still
3099
+ * reads correctly; nothing may hard-code an opaque background.
3100
+ */
3101
+ '.bw-browse-body{min-width:360px;max-width:460px}',
3102
+ '.bw-browse-bar{display:flex;align-items:center;gap:4px}',
3103
+ '.bw-browse-crumbs{display:flex;align-items:center;gap:2px;flex:1;min-width:0;overflow-x:auto}',
3104
+ '.bw-browse-crumb{border:none;background:transparent;color:var(--dsw-alias-label-secondary,#b8b8b8);font:inherit;font-size:12px;padding:2px 5px;border-radius:4px;cursor:pointer;white-space:nowrap;max-width:150px;overflow:hidden;text-overflow:ellipsis;flex:none}',
3105
+ '.bw-browse-crumb:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,#e6e6e6)}',
3106
+ '.bw-browse-crumb:disabled{opacity:.4;cursor:default}',
3107
+ '.bw-browse-current{color:var(--dsw-alias-label-primary,#e6e6e6);font-size:12px;padding:2px 5px;max-width:190px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
3108
+ '.bw-browse-nav{font-size:13px;line-height:1}',
3109
+ '.bw-browse-edit{flex:none;display:grid;place-items:center;width:24px;height:24px;padding:0;border:none;border-radius:5px;background:transparent;color:var(--dsw-alias-label-tertiary,#9a9a9a);cursor:pointer}',
3110
+ '.bw-browse-edit:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,#e6e6e6)}',
3111
+ '.bw-browse-edit svg{width:14px;height:14px}',
3112
+ '.bw-browse-edit-row{display:flex;gap:6px;align-items:center}',
3113
+ '.bw-browse-list{display:flex;flex-direction:column;gap:1px;height:240px;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))}',
3114
+ '.bw-browse-row{display:flex;align-items:center;gap:8px;width:100%;flex:none;text-align:left;border:none;background:transparent;color:var(--dsw-alias-label-primary,#e6e6e6);font:inherit;font-size:12.5px;padding:5px 8px;border-radius:6px;cursor:pointer;user-select:none}',
3115
+ '.bw-browse-row:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}',
3116
+ '.bw-browse-row:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5b8def);outline-offset:-2px}',
3117
+ '.bw-browse-row svg{width:15px;height:15px;flex:none;color:var(--dsw-alias-label-secondary,#b8b8b8)}',
3118
+ '.bw-browse-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)}',
3119
+ '.bw-browse-open{flex:none;width:22px;height:22px;padding:0;border:none;border-radius:5px;background:transparent;color:var(--dsw-alias-label-tertiary,#9a9a9a);font:inherit;font-size:15px;line-height:1;cursor:pointer}',
3120
+ '.bw-browse-open:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.16));color:var(--dsw-alias-label-primary,#e6e6e6)}',
3121
+ '.bw-browse-drives{display:flex;align-items:center;gap:4px;margin-left:auto}',
3122
+ '.bw-browse-drive{min-width:30px;padding:3px 6px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.25));border-radius:5px;background:transparent;color:var(--dsw-alias-label-secondary,#b8b8b8);font:inherit;font-size:11.5px;cursor:pointer}',
3123
+ '.bw-browse-drive:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,#e6e6e6)}',
3124
+ '.bw-browse-drive-on{border-color:var(--dsw-alias-brand-primary,#5b8def);color:var(--dsw-alias-label-primary,#e6e6e6)}',
3125
+ '.bw-browse-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
3126
+ '.bw-browse-note{font-size:12px;color:var(--dsw-alias-label-tertiary,#9a9a9a);padding:10px 8px;text-align:center}',
3127
+ '.bw-browse-new-row{display:flex;gap:6px;align-items:center}',
3128
+ '.bw-browse-tools{display:flex;align-items:center;gap:6px;flex-wrap:wrap}',
3129
+ '.bw-browse-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px}',
3130
+ '.bw-browse-tool{border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.25));background:transparent;color:var(--dsw-alias-label-secondary,#b8b8b8);font:inherit;font-size:12px;padding:4px 10px;border-radius:6px;cursor:pointer;white-space:nowrap}',
3131
+ '.bw-browse-tool:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,#e6e6e6)}',
3132
+ '.bw-browse-tool:disabled{opacity:.45;cursor:default}',
3133
+ '.bw-browse-tool-on{border-color:var(--dsw-alias-brand-primary,#5b8def);color:var(--dsw-alias-label-primary,#e6e6e6)}',
2801
3134
  ].join('')
2802
3135
 
2803
3136
  const StyleNode = () => E('style', null, CSS_TEXT)
@@ -3031,6 +3364,65 @@ window.__ModuleLoader__.load({
3031
3364
  },
3032
3365
  })
3033
3366
 
3367
+ /* ==================== directory-picker capability ================== */
3368
+
3369
+ // The host composes exactly ONE directory-picking backend
3370
+ // (@deepseek-ai/dsh-host-directory-picker-auto): a loopback-only webserver
3371
+ // bind on a display-bearing host gets `native` — one OS chooser on the
3372
+ // HOST's own screen — while every other bind (all-interfaces/LAN, SSH,
3373
+ // headless) gets `browse`, whose wire verbs are list/createDirectory ONLY
3374
+ // and whose `pick` is refused BY DESIGN with `directory-picker/unavailable`
3375
+ // ("... needs the native capability; the composed picker serves browse").
3376
+ // A flow that hard-assumed the OS chooser therefore dies on every LAN bind
3377
+ // and no workspace can be added at all. Probe the composed backend once per
3378
+ // page and keep BOTH interactions: the OS chooser, and an in-app browser
3379
+ // driven by the browse primitives (see DirectoryBrowseDialog).
3380
+ const pickerState = { kind: 'unknown', probe: null, api: null }
3381
+ /**
3382
+ * True for the refusal a Host serving the OTHER capability throws: the wire
3383
+ * code when the caller still carries it, the Host's fixed sentence
3384
+ * otherwise (uiWorkspace.pickDirectory wraps the RPC failure in a plain
3385
+ * Error, so the code is gone by the time the flow sees it).
3386
+ */
3387
+ const pickerRefusal = (error) => {
3388
+ if (error && typeof error === 'object' && error.rpcError
3389
+ && error.rpcError.code === 'directory-picker/unavailable') return true
3390
+ const message = messageOf(error)
3391
+ return message.indexOf('needs the native capability') !== -1
3392
+ || message.indexOf('directory-picker/unavailable') !== -1
3393
+ }
3394
+ /**
3395
+ * Resolve which backend this page's Host composes. `list` succeeds exactly
3396
+ * on `browse` and is refused with the capability code on `native`, so one
3397
+ * call answers both worlds without ever opening a chooser or creating a
3398
+ * directory. An unclassified failure (a carrier still connecting at boot)
3399
+ * is NOT cached: the next open re-probes, and the flow falls back to trying
3400
+ * the chooser itself.
3401
+ */
3402
+ const pickerCapabilityNow = () => {
3403
+ if (pickerState.kind !== 'unknown') return Promise.resolve(pickerState.kind)
3404
+ if (pickerState.probe !== null) return pickerState.probe
3405
+ const api = pickerState.api
3406
+ if (!api || typeof api.listDirectory !== 'function') return Promise.resolve('unknown')
3407
+ const probe = Promise.resolve()
3408
+ .then(() => api.listDirectory(undefined, undefined))
3409
+ .then(
3410
+ () => 'browse',
3411
+ (error) => (pickerRefusal(error) ? 'native' : 'unknown'),
3412
+ )
3413
+ pickerState.probe = probe.then((kind) => {
3414
+ if (kind === 'unknown') { pickerState.probe = null; return kind }
3415
+ pickerState.kind = kind
3416
+ return kind
3417
+ })
3418
+ return pickerState.probe
3419
+ }
3420
+ /** Commit a browse verdict the flow discovered the hard way (a refused pick). */
3421
+ const markPickerBrowse = () => {
3422
+ pickerState.kind = 'browse'
3423
+ pickerState.probe = Promise.resolve('browse')
3424
+ }
3425
+
3034
3426
  /* ============================ flow dialog ========================= */
3035
3427
 
3036
3428
  const BTN = (props) => (
@@ -3106,16 +3498,336 @@ window.__ModuleLoader__.load({
3106
3498
  }
3107
3499
 
3108
3500
  /**
3109
- * The add-workspace picking interaction: native directory pick, then a
3110
- * small parent-group popup, then create + rename with the chosen prefix.
3111
- * Works as a directoryFlow occupant (owner conversation props) and as the
3112
- * browser's directly composed flow (same props, owner state lives above).
3501
+ * The in-app directory browser: the interaction a `browse`-composed Host can
3502
+ * actually serve. It appears when the probe (or a refused pick) proves the
3503
+ * OS chooser is not this Host's backend a LAN/all-interfaces bind, an SSH
3504
+ * launch, a headless host where `pick` is refused by design and
3505
+ * list/createDirectory are the only wire verbs. One level at a time: a
3506
+ * clickable Home-rooted breadcrumb chain, an editable path, an inline
3507
+ * new-folder row and the hidden-entry toggle; the listed level (or any
3508
+ * ancestor crumb) is the pick. Browse failures stay inside the dialog — the
3509
+ * owner's error surface belongs to the pick/create conversation, and a
3510
+ * denied listing must never close the flow.
3511
+ */
3512
+ /**
3513
+ * Page-lifetime cache of the Windows drive letters this Host actually
3514
+ * serves. The browse API can only list a path, so the first open probes the
3515
+ * usual letters once (six cheap listings, a missing drive fails silently)
3516
+ * and every later open renders the chips straight from here.
3517
+ */
3518
+ const browseDrives = { probed: false, probing: false, list: [] }
3519
+
3520
+ function DirectoryBrowseDialog(props) {
3521
+ const { open, busy, listDirectory, createDirectory, onPick, onClose, t } = props
3522
+ const [listing, setListing] = React.useState(null)
3523
+ const [loading, setLoading] = React.useState(false)
3524
+ const [error, setError] = React.useState('')
3525
+ const [showHidden, setShowHidden] = React.useState(false)
3526
+ const [editing, setEditing] = React.useState(false)
3527
+ const [draft, setDraft] = React.useState('')
3528
+ const [creating, setCreating] = React.useState(false)
3529
+ const [createName, setCreateName] = React.useState('')
3530
+ const [createBusy, setCreateBusy] = React.useState(false)
3531
+ const [createError, setCreateError] = React.useState('')
3532
+ const [selected, setSelected] = React.useState('')
3533
+ const [drives, setDrives] = React.useState(browseDrives.list)
3534
+ // Guards a superseded scan: a slow level must not overwrite a newer one.
3535
+ const seqRef = React.useRef(0)
3536
+ const crumbsRef = React.useRef(null)
3537
+
3538
+ const go = (path, keepSelected) => {
3539
+ const seq = seqRef.current + 1
3540
+ seqRef.current = seq
3541
+ setLoading(true)
3542
+ setError('')
3543
+ if (keepSelected !== true) setSelected('')
3544
+ Promise.resolve()
3545
+ .then(() => listDirectory(path, undefined))
3546
+ .then((next) => {
3547
+ if (seqRef.current !== seq) return
3548
+ setListing(next || null)
3549
+ setLoading(false)
3550
+ })
3551
+ .catch((reason) => {
3552
+ if (seqRef.current !== seq) return
3553
+ setLoading(false)
3554
+ setError(messageOf(reason))
3555
+ })
3556
+ }
3557
+
3558
+ React.useEffect(() => {
3559
+ if (!open) return undefined
3560
+ seqRef.current += 1
3561
+ setListing(null)
3562
+ setError('')
3563
+ setEditing(false)
3564
+ setCreating(false)
3565
+ setCreateName('')
3566
+ setCreateBusy(false)
3567
+ setCreateError('')
3568
+ setShowHidden(false)
3569
+ setSelected('')
3570
+ go(undefined)
3571
+ return () => { seqRef.current += 1 }
3572
+ }, [open])
3573
+
3574
+ const currentPath = listing && typeof listing.path === 'string' ? listing.path : ''
3575
+ const homePath = listing && typeof listing.home === 'string' ? listing.home : ''
3576
+ const windowsHost = homePath.indexOf('\\') !== -1
3577
+ const hasListing = listing !== null
3578
+
3579
+ // Windows drives are separate filesystem roots and the browse API can only
3580
+ // list a path, so probe the usual letters once per page (cached above) and
3581
+ // offer the ones that answered. A drive nobody probed is still reachable
3582
+ // through the path editor, which is the only general answer anyway.
3583
+ React.useEffect(() => {
3584
+ if (!open || !hasListing || !windowsHost) return undefined
3585
+ if (browseDrives.probed || browseDrives.probing) {
3586
+ if (browseDrives.list.length > 0) setDrives(browseDrives.list)
3587
+ return undefined
3588
+ }
3589
+ browseDrives.probing = true
3590
+ let alive = true
3591
+ const letters = ['C:', 'D:', 'E:', 'F:', 'G:', 'H:']
3592
+ Promise.all(letters.map((letter) => Promise.resolve()
3593
+ .then(() => listDirectory(letter + '\\', undefined))
3594
+ .then(() => letter, () => null)))
3595
+ .then((found) => {
3596
+ browseDrives.list = found.filter(Boolean)
3597
+ browseDrives.probed = true
3598
+ browseDrives.probing = false
3599
+ if (alive) setDrives(browseDrives.list)
3600
+ }, () => { browseDrives.probing = false })
3601
+ return () => { alive = false }
3602
+ }, [open, hasListing, windowsHost])
3603
+
3604
+ // A deep path must show its TAIL — that is where you are — so the chain
3605
+ // keeps its right edge in view instead of cutting the last crumb off.
3606
+ React.useEffect(() => {
3607
+ const node = crumbsRef.current
3608
+ if (node) node.scrollLeft = node.scrollWidth
3609
+ }, [listing])
3610
+
3611
+ if (!open) return null
3612
+
3613
+ // The whole ancestry from the filesystem ROOT: rooting the chain at Home
3614
+ // (the official dialog's choice) hides where the listed level actually is,
3615
+ // and the drive crumb answers "which disk am I on" at a glance.
3616
+ const crumbs = listing && Array.isArray(listing.crumbs) ? listing.crumbs : []
3617
+ const parentPath = crumbs.length > 1 ? crumbs[crumbs.length - 2].path : ''
3618
+ const entries = (listing && Array.isArray(listing.entries) ? listing.entries : [])
3619
+ .filter((entry) => showHidden || !entry.hidden)
3620
+ const selectedEntry = entries.find((entry) => entry.path === selected) || null
3621
+ const targetPath = selectedEntry ? selectedEntry.path : currentPath
3622
+
3623
+ const submitDraft = () => {
3624
+ const path = draft.trim()
3625
+ setEditing(false)
3626
+ if (path === '') { go(undefined); return }
3627
+ go(path)
3628
+ }
3629
+ const submitCreate = () => {
3630
+ const name = createName.trim()
3631
+ if (name === '' || createBusy || currentPath === '') return
3632
+ setCreateBusy(true)
3633
+ setCreateError('')
3634
+ Promise.resolve()
3635
+ .then((created) => {
3636
+ setCreateBusy(false)
3637
+ setCreating(false)
3638
+ setCreateName('')
3639
+ // Land on the folder just made: it is almost always the one the
3640
+ // operator wants to adopt next.
3641
+ if (typeof created === 'string' && created !== '') setSelected(created)
3642
+ go(currentPath, true)
3643
+ })
3644
+ .catch((reason) => {
3645
+ setCreateBusy(false)
3646
+ setCreateError(messageOf(reason))
3647
+ })
3648
+ }
3649
+
3650
+ const head = editing
3651
+ ? E('div', { className: 'bw-browse-edit-row' },
3652
+ E('input', {
3653
+ className: 'bw-input',
3654
+ value: draft,
3655
+ autoFocus: true,
3656
+ spellCheck: false,
3657
+ placeholder: currentPath,
3658
+ onChange: (e) => setDraft(e.target.value),
3659
+ onKeyDown: (e) => {
3660
+ if (e.key === 'Enter') submitDraft()
3661
+ else if (e.key === 'Escape') setEditing(false)
3662
+ },
3663
+ }),
3664
+ E(BTN, { variant: 'outline', onClick: () => setEditing(false) }, t('cancel')),
3665
+ )
3666
+ : E('div', { className: 'bw-browse-bar' },
3667
+ E('button', {
3668
+ type: 'button',
3669
+ className: 'bw-browse-crumb bw-browse-nav',
3670
+ title: t('browse.up'),
3671
+ 'aria-label': t('browse.up'),
3672
+ disabled: parentPath === '',
3673
+ onClick: () => { if (parentPath !== '') go(parentPath) },
3674
+ }, '↑'),
3675
+ E('div', { className: 'bw-browse-crumbs', ref: crumbsRef },
3676
+ crumbs.map((crumb, index) => (index === crumbs.length - 1
3677
+ ? E('span', { key: crumb.path, className: 'bw-browse-crumb bw-browse-current', title: crumb.path }, crumb.name)
3678
+ : E('button', {
3679
+ key: crumb.path,
3680
+ type: 'button',
3681
+ className: 'bw-browse-crumb',
3682
+ title: crumb.path,
3683
+ onClick: () => go(crumb.path),
3684
+ }, crumb.name))),
3685
+ ),
3686
+ E('button', {
3687
+ type: 'button',
3688
+ className: 'bw-browse-edit',
3689
+ title: t('browse.editPath'),
3690
+ 'aria-label': t('browse.editPath'),
3691
+ onClick: () => { setDraft(currentPath); setEditing(true) },
3692
+ }, icon('IconEditOutline16')),
3693
+ )
3694
+
3695
+ // One click SELECTS a row (the footer button then adopts it — no need to
3696
+ // step inside just to pick a folder), a double click or the row's chevron
3697
+ // enters it. Touch needs the chevron: a double tap is not a gesture.
3698
+ const rows = entries.map((entry) => E('div', {
3699
+ key: entry.path,
3700
+ className: cls('bw-browse-row', selected === entry.path && 'bw-browse-row-on'),
3701
+ title: entry.path,
3702
+ role: 'button',
3703
+ tabIndex: 0,
3704
+ onClick: () => setSelected((current) => (current === entry.path ? '' : entry.path)),
3705
+ onDoubleClick: () => go(entry.path),
3706
+ onKeyDown: (event) => {
3707
+ if (event.key === 'Enter') go(entry.path)
3708
+ else if (event.key === ' ') { event.preventDefault(); setSelected((current) => (current === entry.path ? '' : entry.path)) }
3709
+ },
3710
+ },
3711
+ icon('IconFolderClose16'),
3712
+ E('span', { className: 'bw-browse-name' }, entry.name),
3713
+ E('button', {
3714
+ type: 'button',
3715
+ className: 'bw-browse-open',
3716
+ title: t('browse.enter'),
3717
+ 'aria-label': t('browse.enter'),
3718
+ onClick: (event) => { event.stopPropagation(); go(entry.path) },
3719
+ }, '›'),
3720
+ ))
3721
+
3722
+ const body = loading
3723
+ ? E('div', { className: 'bw-browse-note' }, t('browse.loading'))
3724
+ : rows.length > 0
3725
+ ? rows
3726
+ : E('div', { className: 'bw-browse-note' }, t('browse.empty'))
3727
+
3728
+ const newRow = creating
3729
+ ? E('div', { className: 'bw-browse-new-row' },
3730
+ E('input', {
3731
+ className: 'bw-input',
3732
+ value: createName,
3733
+ autoFocus: true,
3734
+ spellCheck: false,
3735
+ placeholder: t('browse.folderName'),
3736
+ disabled: createBusy,
3737
+ onChange: (e) => setCreateName(e.target.value),
3738
+ onKeyDown: (e) => {
3739
+ if (e.key === 'Enter') submitCreate()
3740
+ else if (e.key === 'Escape') { setCreating(false); setCreateError('') }
3741
+ },
3742
+ }),
3743
+ E(BTN, {
3744
+ variant: 'outline',
3745
+ disabled: createBusy,
3746
+ onClick: () => { setCreating(false); setCreateError('') },
3747
+ }, t('cancel')),
3748
+ E(BTN, { variant: 'primary', onClick: submitCreate, disabled: createBusy },
3749
+ createBusy ? t('flow.creating') : t('create')),
3750
+ )
3751
+ : null
3752
+
3753
+ // The dialog is deliberately narrower than the official browser, so the
3754
+ // two browsing tools sit on their own row above the list instead of
3755
+ // sharing the footer with the commit pair (they used to wrap there).
3756
+ const tools = E('div', { className: 'bw-browse-tools' },
3757
+ E('button', {
3758
+ type: 'button',
3759
+ className: cls('bw-browse-tool', currentPath !== '' && currentPath === homePath && 'bw-browse-tool-on'),
3760
+ disabled: homePath === '' || currentPath === homePath,
3761
+ onClick: () => { if (homePath !== '') go(homePath) },
3762
+ }, t('browse.home')),
3763
+ E('button', {
3764
+ type: 'button',
3765
+ className: cls('bw-browse-tool', showHidden && 'bw-browse-tool-on'),
3766
+ 'aria-pressed': showHidden ? 'true' : 'false',
3767
+ onClick: () => setShowHidden((value) => !value),
3768
+ }, t('browse.showHidden')),
3769
+ E('button', {
3770
+ type: 'button',
3771
+ className: 'bw-browse-tool',
3772
+ disabled: creating || currentPath === '',
3773
+ onClick: () => { setCreating(true); setCreateName(''); setCreateError('') },
3774
+ }, t('browse.newFolder')),
3775
+ drives.length > 1
3776
+ ? E('div', {
3777
+ className: 'bw-browse-drives',
3778
+ title: t('browse.drives'),
3779
+ role: 'group',
3780
+ 'aria-label': t('browse.drives'),
3781
+ }, drives.map((drive) => E('button', {
3782
+ key: drive,
3783
+ type: 'button',
3784
+ className: cls('bw-browse-drive', currentPath.slice(0, 2).toUpperCase() === drive && 'bw-browse-drive-on'),
3785
+ title: drive + '\\',
3786
+ onClick: () => go(drive + '\\'),
3787
+ }, drive)))
3788
+ : null,
3789
+ )
3790
+
3791
+ const footer = E('div', { className: 'bw-browse-actions' },
3792
+ E(BTN, { variant: 'outline', onClick: onClose, disabled: busy === true }, t('cancel')),
3793
+ E(BTN, {
3794
+ variant: 'primary',
3795
+ disabled: busy === true || targetPath === '',
3796
+ onClick: () => { if (targetPath !== '') onPick(targetPath) },
3797
+ }, selectedEntry !== null ? t('browse.selectNamed', { name: selectedEntry.name }) : t('browse.select')),
3798
+ )
3799
+
3800
+ return E(ui.Modal, {
3801
+ open: true,
3802
+ onClose: () => { if (busy !== true) onClose() },
3803
+ closeLabel: t('close'),
3804
+ title: t('browse.title'),
3805
+ footer,
3806
+ },
3807
+ E('div', { className: 'bw-modal-body bw-browse-body' },
3808
+ head,
3809
+ createError !== '' ? E('div', { className: 'bw-error-text', role: 'alert' }, createError) : null,
3810
+ tools,
3811
+ newRow,
3812
+ E('div', { className: 'bw-browse-list', role: 'list' }, body),
3813
+ listing && listing.truncated === true ? E('div', { className: 'bw-hint' }, t('browse.truncated')) : null,
3814
+ error !== '' ? E('div', { className: 'bw-error-text', role: 'alert' }, error) : null,
3815
+ ),
3816
+ StyleNode())
3817
+ }
3818
+
3819
+ /**
3820
+ * The add-workspace picking interaction: whichever directory interaction the
3821
+ * Host composes (OS chooser or in-app browser), then a small parent-group
3822
+ * popup, then create + rename with the chosen prefix. Works as a
3823
+ * directoryFlow occupant (owner conversation props) and as the browser's
3824
+ * directly composed flow (same props, owner state lives above).
3113
3825
  */
3114
3826
  function BetterFlow(props) {
3115
- const { open, busy, onPicked, onCancel, onError, createWorkspace, renameWorkspace, pickDirectory, useWorkspaces, useStore, t } = props
3827
+ const { open, busy, onPicked, onCancel, onError, createWorkspace, renameWorkspace, pickDirectory, listDirectory, createDirectory, useWorkspaces, useStore, t } = props
3116
3828
  const initialParent = props.initialParent || ''
3117
3829
  const actions = props.actions
3118
- const [phase, setPhase] = React.useState('idle') // idle | picking | picked | submitting
3830
+ const [phase, setPhase] = React.useState('idle') // idle | picking | browsing | picked | submitting
3119
3831
  const [pickedPath, setPickedPath] = React.useState('')
3120
3832
  const [parentInput, setParentInput] = React.useState('')
3121
3833
  // All hooks run before any early return: the flow unmounts its dialog
@@ -3134,24 +3846,54 @@ window.__ModuleLoader__.load({
3134
3846
  }
3135
3847
  let alive = true
3136
3848
  setPhase('picking')
3137
- Promise.resolve()
3138
- .then(() => pickDirectory())
3139
- .then((path) => {
3140
- if (!alive) return
3141
- if (!path) { onCancel(); return }
3142
- setPickedPath(String(path))
3143
- setParentInput(initialParent)
3144
- setPhase('picked')
3849
+ // Which backend this Host composes decides the whole interaction: a
3850
+ // `browse` Host renders the in-app browser directly (its `pick` is
3851
+ // refused by design, so trying the chooser first would only surface an
3852
+ // error), a `native` Host opens the OS chooser, and an unresolved
3853
+ // verdict still tries the chooser so a silent probe never changes the
3854
+ // behavior this plugin had before the probe existed.
3855
+ pickerCapabilityNow()
3856
+ .then((kind) => {
3857
+ if (!alive) return undefined
3858
+ if (kind === 'browse') { setPhase('browsing'); return undefined }
3859
+ return Promise.resolve()
3860
+ .then(() => pickDirectory())
3861
+ .then((path) => {
3862
+ if (!alive) return
3863
+ if (!path) { onCancel(); return }
3864
+ setPickedPath(String(path))
3865
+ setParentInput(initialParent)
3866
+ setPhase('picked')
3867
+ })
3145
3868
  })
3146
3869
  .catch((reason) => {
3147
3870
  if (!alive) return
3871
+ // A refusal is a backend fact, not a failure: this Host serves the
3872
+ // browse verbs, so switch interactions instead of erroring out.
3873
+ if (pickerRefusal(reason)) { markPickerBrowse(); setPhase('browsing'); return }
3148
3874
  setPhase('idle')
3149
3875
  onError(messageOf(reason))
3150
3876
  })
3151
3877
  return () => { alive = false }
3152
3878
  }, [open])
3153
3879
 
3154
- if (!open || (phase !== 'picked' && phase !== 'submitting')) return null
3880
+ if (!open) return null
3881
+ if (phase === 'browsing') {
3882
+ return E(DirectoryBrowseDialog, {
3883
+ open: true,
3884
+ busy: busy === true,
3885
+ listDirectory,
3886
+ createDirectory,
3887
+ onPick: (path) => {
3888
+ setPickedPath(String(path))
3889
+ setParentInput(initialParent)
3890
+ setPhase('picked')
3891
+ },
3892
+ onClose: onCancel,
3893
+ t,
3894
+ })
3895
+ }
3896
+ if (phase !== 'picked' && phase !== 'submitting') return null
3155
3897
 
3156
3898
  const folderOptions = (() => {
3157
3899
  const set = new Set(storeFolders)
@@ -3954,7 +4696,8 @@ window.__ModuleLoader__.load({
3954
4696
  useSessions, useSessionPendingInteraction, useWorkspaces,
3955
4697
  useStore, actions,
3956
4698
  startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace,
3957
- archiveSession, createWorkspace, pickDirectory, insertWorkspaceBefore, insertSessionBefore,
4699
+ archiveSession, createWorkspace, pickDirectory, listDirectory, createDirectory,
4700
+ insertWorkspaceBefore, insertSessionBefore,
3958
4701
  t,
3959
4702
  } = props
3960
4703
 
@@ -5007,6 +5750,8 @@ window.__ModuleLoader__.load({
5007
5750
  createWorkspace,
5008
5751
  renameWorkspace,
5009
5752
  pickDirectory,
5753
+ listDirectory,
5754
+ createDirectory,
5010
5755
  useWorkspaces,
5011
5756
  actions,
5012
5757
  useStore,
@@ -5092,6 +5837,10 @@ window.__ModuleLoader__.load({
5092
5837
  })
5093
5838
  return
5094
5839
  }
5840
+ // The capability probe talks to the composed backend through this
5841
+ // service (list is served by browse alone), so the state module needs
5842
+ // the handle before any flow can open.
5843
+ pickerState.api = uiWorkspace
5095
5844
 
5096
5845
  if (ctx.locale && typeof ctx.locale.register === 'function') {
5097
5846
  ctx.effect(() => {
@@ -5143,6 +5892,10 @@ window.__ModuleLoader__.load({
5143
5892
  archiveSession: (sessionId) => uiWorkspace.archiveSession(sessionId),
5144
5893
  createWorkspace: (input) => workspaces.create(input),
5145
5894
  pickDirectory: () => uiWorkspace.pickDirectory(),
5895
+ // Browse-side primitives: the fallback interaction whenever the Host
5896
+ // composes the browse backend instead of the OS chooser.
5897
+ listDirectory: (path, signal) => uiWorkspace.listDirectory(path, signal),
5898
+ createDirectory: (path, name) => uiWorkspace.createDirectory(path, name),
5146
5899
  hooks: {
5147
5900
  directoryFlow: flowSource(slots, 'sidebar.workspaces.directoryFlow'),
5148
5901
  },
@@ -5151,6 +5904,10 @@ window.__ModuleLoader__.load({
5151
5904
  createWorkspace: (input) => workspaces.create(input),
5152
5905
  renameWorkspace: (workspaceId, title) => workspaces.rename(workspaceId, title),
5153
5906
  pickDirectory: () => uiWorkspace.pickDirectory(),
5907
+ // Same browse-side primitives as the sidebar flow: the two holes and
5908
+ // the inlined dialog must agree on which interaction they can run.
5909
+ listDirectory: (path, signal) => uiWorkspace.listDirectory(path, signal),
5910
+ createDirectory: (path, name) => uiWorkspace.createDirectory(path, name),
5154
5911
  hooks: { directoryFlow: flowSource(slots, hole) },
5155
5912
  })
5156
5913