dsh-better-workspace 0.12.0 → 0.12.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.12.0",
3
+ "version": "0.12.1",
4
4
  "main": "./src/index.js",
5
5
  "description": "侧边栏工作区两层层级树:工作区按磁盘目录嵌套(官方 workspace-tree 语义),名称中的 / 即虚拟分组(web/前端 · web/后端);添加工作区由官方目录流承载。Two-layer 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.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "侧边栏工作区两层层级树:磁盘目录嵌套(官方 workspace-tree 语义)+ 名称 / 虚拟分组,添加工作区走官方目录流。Two-layer workspace tree for the DSH sidebar: disk-path nesting plus \"/\"-name groups; add-workspace rides the official directory flow.",
5
5
  "type": "module",
6
6
  "repository": {
package/src/client.js CHANGED
@@ -112,6 +112,21 @@ window.__ModuleLoader__.load({
112
112
  'sync.empty': '另一端暂无数据可获取',
113
113
  'sync.off': '当前环境不支持同步(需要宿主设置服务)',
114
114
  'sync.loading': '正在连接宿主设置…',
115
+ 'flow.creating': '正在创建…',
116
+ 'browse.title': '选择工作区目录',
117
+ 'browse.home': '主目录',
118
+ 'browse.up': '上一层',
119
+ 'browse.newFolder': '新建文件夹',
120
+ 'browse.folderName': '文件夹名称',
121
+ 'browse.empty': '此文件夹没有子文件夹',
122
+ 'browse.loading': '加载中…',
123
+ 'browse.truncated': '文件夹过多,仅显示开头部分。',
124
+ 'browse.showHidden': '显示隐藏文件',
125
+ 'browse.editPath': '编辑路径',
126
+ 'browse.select': '选择此文件夹',
127
+ 'browse.enter': '进入',
128
+ 'browse.drives': '盘符',
129
+ 'browse.selectNamed': '选择「{name}」',
115
130
  'error.title': '出错了',
116
131
  'cancel': '取消',
117
132
  'create': '创建',
@@ -207,6 +222,21 @@ window.__ModuleLoader__.load({
207
222
  'sync.empty': 'No data on the other surface yet',
208
223
  'sync.off': 'Sync unavailable here (requires the host settings service)',
209
224
  'sync.loading': 'Connecting to host settings…',
225
+ 'flow.creating': 'Creating…',
226
+ 'browse.title': 'Select Workspace Directory',
227
+ 'browse.home': 'Home',
228
+ 'browse.up': 'Up',
229
+ 'browse.newFolder': 'New folder',
230
+ 'browse.folderName': 'Folder name',
231
+ 'browse.empty': 'No subfolders here',
232
+ 'browse.loading': 'Loading…',
233
+ 'browse.truncated': 'Too many folders to list; only the beginning is shown.',
234
+ 'browse.showHidden': 'Show hidden files',
235
+ 'browse.editPath': 'Edit path',
236
+ 'browse.select': 'Use this folder',
237
+ 'browse.enter': 'Open',
238
+ 'browse.drives': 'Drives',
239
+ 'browse.selectNamed': 'Use "{name}"',
210
240
  'error.title': 'Something went wrong',
211
241
  'cancel': 'Cancel',
212
242
  'create': 'Create',
@@ -310,6 +340,21 @@ window.__ModuleLoader__.load({
310
340
  'sync.empty': 'لا توجد بيانات لدى الطرف الآخر بعد',
311
341
  'sync.off': 'المزامنة غير متاحة هنا (تحتاج خدمة إعدادات المضيف)',
312
342
  'sync.loading': 'الاتصال بإعدادات المضيف…',
343
+ 'flow.creating': 'جارٍ الإنشاء…',
344
+ 'browse.title': 'اختيار مجلد مساحة العمل',
345
+ 'browse.home': 'المجلد الرئيسي',
346
+ 'browse.up': 'المستوى الأعلى',
347
+ 'browse.newFolder': 'مجلد جديد',
348
+ 'browse.folderName': 'اسم المجلد',
349
+ 'browse.empty': 'لا توجد مجلدات فرعية هنا',
350
+ 'browse.loading': 'جارٍ التحميل…',
351
+ 'browse.truncated': 'المجلدات كثيرة جدًا؛ يُعرض الجزء الأول فقط.',
352
+ 'browse.showHidden': 'إظهار الملفات المخفية',
353
+ 'browse.editPath': 'تعديل المسار',
354
+ 'browse.select': 'استخدام هذا المجلد',
355
+ 'browse.enter': 'فتح',
356
+ 'browse.drives': 'الأقراص',
357
+ 'browse.selectNamed': 'استخدام "{name}"',
313
358
  'error.title': 'حدث خطأ ما',
314
359
  'cancel': 'إلغاء',
315
360
  'create': 'إنشاء',
@@ -405,6 +450,21 @@ window.__ModuleLoader__.load({
405
450
  'sync.empty': 'Auf der anderen Seite liegen noch keine Daten',
406
451
  'sync.off': 'Hier nicht verfügbar (erfordert den Host-Einstellungsdienst)',
407
452
  'sync.loading': 'Verbindung zum Host-Einstellungsspeicher…',
453
+ 'flow.creating': 'Wird erstellt…',
454
+ 'browse.title': 'Arbeitsbereichsordner wählen',
455
+ 'browse.home': 'Home',
456
+ 'browse.up': 'Übergeordnet',
457
+ 'browse.newFolder': 'Neuer Ordner',
458
+ 'browse.folderName': 'Ordnername',
459
+ 'browse.empty': 'Keine Unterordner vorhanden',
460
+ 'browse.loading': 'Wird geladen…',
461
+ 'browse.truncated': 'Zu viele Ordner; nur der Anfang wird angezeigt.',
462
+ 'browse.showHidden': 'Versteckte Dateien anzeigen',
463
+ 'browse.editPath': 'Pfad bearbeiten',
464
+ 'browse.select': 'Diesen Ordner verwenden',
465
+ 'browse.enter': 'Öffnen',
466
+ 'browse.drives': 'Laufwerke',
467
+ 'browse.selectNamed': '"{name}" verwenden',
408
468
  'error.title': 'Etwas ist schiefgelaufen',
409
469
  'cancel': 'Abbrechen',
410
470
  'create': 'Erstellen',
@@ -500,6 +560,21 @@ window.__ModuleLoader__.load({
500
560
  'sync.empty': 'Aucune donnée à récupérer de l\'autre côté',
501
561
  'sync.off': 'Synchronisation indisponible ici (service de réglages de l\'hôte requis)',
502
562
  'sync.loading': 'Connexion aux réglages de l\'hôte…',
563
+ 'flow.creating': 'Création…',
564
+ 'browse.title': 'Choisir le dossier de l’espace de travail',
565
+ 'browse.home': 'Dossier personnel',
566
+ 'browse.up': 'Niveau supérieur',
567
+ 'browse.newFolder': 'Nouveau dossier',
568
+ 'browse.folderName': 'Nom du dossier',
569
+ 'browse.empty': 'Aucun sous-dossier ici',
570
+ 'browse.loading': 'Chargement…',
571
+ 'browse.truncated': 'Trop de dossiers ; seul le début est affiché.',
572
+ 'browse.showHidden': 'Afficher les fichiers cachés',
573
+ 'browse.editPath': 'Modifier le chemin',
574
+ 'browse.select': 'Utiliser ce dossier',
575
+ 'browse.enter': 'Ouvrir',
576
+ 'browse.drives': 'Lecteurs',
577
+ 'browse.selectNamed': 'Utiliser « {name} »',
503
578
  'error.title': 'Une erreur est survenue',
504
579
  'cancel': 'Annuler',
505
580
  'create': 'Créer',
@@ -595,6 +670,21 @@ window.__ModuleLoader__.load({
595
670
  'sync.empty': 'दूसरे सिरे पर अभी कोई डेटा नहीं',
596
671
  'sync.off': 'यहाँ समन्वयन उपलब्ध नहीं (होस्ट सेटिंग सेवा चाहिए)',
597
672
  'sync.loading': 'होस्ट सेटिंग से जुड़ रहे हैं…',
673
+ 'flow.creating': 'बनाया जा रहा है…',
674
+ 'browse.title': 'कार्यस्थान फ़ोल्डर चुनें',
675
+ 'browse.home': 'होम',
676
+ 'browse.up': 'ऊपर',
677
+ 'browse.newFolder': 'नया फ़ोल्डर',
678
+ 'browse.folderName': 'फ़ोल्डर का नाम',
679
+ 'browse.empty': 'यहाँ कोई उप-फ़ोल्डर नहीं',
680
+ 'browse.loading': 'लोड हो रहा है…',
681
+ 'browse.truncated': 'बहुत अधिक फ़ोल्डर; केवल शुरुआत दिखाई गई है।',
682
+ 'browse.showHidden': 'छिपी फ़ाइलें दिखाएँ',
683
+ 'browse.editPath': 'पथ संपादित करें',
684
+ 'browse.select': 'यह फ़ोल्डर चुनें',
685
+ 'browse.enter': 'खोलें',
686
+ 'browse.drives': 'ड्राइव',
687
+ 'browse.selectNamed': '"{name}" चुनें',
598
688
  'error.title': 'कुछ गड़बड़ हो गई',
599
689
  'cancel': 'रद्द करें',
600
690
  'create': 'बनाएँ',
@@ -690,6 +780,21 @@ window.__ModuleLoader__.load({
690
780
  'sync.empty': 'Sisi lain belum punya data',
691
781
  'sync.off': 'Sinkronisasi tidak tersedia di sini (perlu layanan pengaturan host)',
692
782
  'sync.loading': 'Menyambung ke pengaturan host…',
783
+ 'flow.creating': 'Membuat…',
784
+ 'browse.title': 'Pilih folder ruang kerja',
785
+ 'browse.home': 'Beranda',
786
+ 'browse.up': 'Naik',
787
+ 'browse.newFolder': 'Folder baru',
788
+ 'browse.folderName': 'Nama folder',
789
+ 'browse.empty': 'Tidak ada subfolder di sini',
790
+ 'browse.loading': 'Memuat…',
791
+ 'browse.truncated': 'Terlalu banyak folder; hanya bagian awal yang ditampilkan.',
792
+ 'browse.showHidden': 'Tampilkan file tersembunyi',
793
+ 'browse.editPath': 'Edit jalur',
794
+ 'browse.select': 'Gunakan folder ini',
795
+ 'browse.enter': 'Buka',
796
+ 'browse.drives': 'Drive',
797
+ 'browse.selectNamed': 'Gunakan "{name}"',
693
798
  'error.title': 'Terjadi kesalahan',
694
799
  'cancel': 'Batal',
695
800
  'create': 'Buat',
@@ -785,6 +890,21 @@ window.__ModuleLoader__.load({
785
890
  'sync.empty': 'Nessun dato da scaricare dall\'altro lato',
786
891
  'sync.off': 'Sincronizzazione non disponibile qui (serve il servizio impostazioni dell\'host)',
787
892
  'sync.loading': 'Connessione alle impostazioni dell\'host…',
893
+ 'flow.creating': 'Creazione…',
894
+ 'browse.title': 'Scegli cartella dell’area di lavoro',
895
+ 'browse.home': 'Home',
896
+ 'browse.up': 'Livello superiore',
897
+ 'browse.newFolder': 'Nuova cartella',
898
+ 'browse.folderName': 'Nome cartella',
899
+ 'browse.empty': 'Nessuna sottocartella qui',
900
+ 'browse.loading': 'Caricamento…',
901
+ 'browse.truncated': 'Troppe cartelle; è mostrato solo l’inizio.',
902
+ 'browse.showHidden': 'Mostra file nascosti',
903
+ 'browse.editPath': 'Modifica percorso',
904
+ 'browse.select': 'Usa questa cartella',
905
+ 'browse.enter': 'Apri',
906
+ 'browse.drives': 'Unità',
907
+ 'browse.selectNamed': 'Usa "{name}"',
788
908
  'error.title': 'Si è verificato un errore',
789
909
  'cancel': 'Annulla',
790
910
  'create': 'Crea',
@@ -880,6 +1000,21 @@ window.__ModuleLoader__.load({
880
1000
  'sync.empty': 'もう一方に取得できるデータがありません',
881
1001
  'sync.off': 'この環境では同期できません(ホスト設定サービスが必要)',
882
1002
  'sync.loading': 'ホスト設定に接続中…',
1003
+ 'flow.creating': '作成中…',
1004
+ 'browse.title': 'ワークスペースのフォルダーを選択',
1005
+ 'browse.home': 'ホーム',
1006
+ 'browse.up': '上の階層',
1007
+ 'browse.newFolder': '新しいフォルダー',
1008
+ 'browse.folderName': 'フォルダー名',
1009
+ 'browse.empty': 'ここにサブフォルダーはありません',
1010
+ 'browse.loading': '読み込み中…',
1011
+ 'browse.truncated': 'フォルダーが多すぎます。先頭のみ表示しています。',
1012
+ 'browse.showHidden': '隠しファイルを表示',
1013
+ 'browse.editPath': 'パスを編集',
1014
+ 'browse.select': 'このフォルダーを選択',
1015
+ 'browse.enter': '開く',
1016
+ 'browse.drives': 'ドライブ',
1017
+ 'browse.selectNamed': '「{name}」を選択',
883
1018
  'error.title': 'エラーが発生しました',
884
1019
  'cancel': 'キャンセル',
885
1020
  'create': '作成',
@@ -975,6 +1110,21 @@ window.__ModuleLoader__.load({
975
1110
  'sync.empty': '반대쪽에 가져올 데이터가 없습니다',
976
1111
  'sync.off': '이 환경에서는 동기화할 수 없습니다(호스트 설정 서비스 필요)',
977
1112
  'sync.loading': '호스트 설정에 연결하는 중…',
1113
+ 'flow.creating': '만드는 중…',
1114
+ 'browse.title': '워크스페이스 폴더 선택',
1115
+ 'browse.home': '홈',
1116
+ 'browse.up': '상위',
1117
+ 'browse.newFolder': '새 폴더',
1118
+ 'browse.folderName': '폴더 이름',
1119
+ 'browse.empty': '여기에 하위 폴더가 없습니다',
1120
+ 'browse.loading': '불러오는 중…',
1121
+ 'browse.truncated': '폴더가 너무 많습니다. 앞부분만 표시합니다.',
1122
+ 'browse.showHidden': '숨김 파일 표시',
1123
+ 'browse.editPath': '경로 편집',
1124
+ 'browse.select': '이 폴더 선택',
1125
+ 'browse.enter': '열기',
1126
+ 'browse.drives': '드라이브',
1127
+ 'browse.selectNamed': '"{name}" 선택',
978
1128
  'error.title': '문제가 발생했습니다',
979
1129
  'cancel': '취소',
980
1130
  'create': '만들기',
@@ -1070,6 +1220,21 @@ window.__ModuleLoader__.load({
1070
1220
  'sync.empty': 'Aan de andere kant staan nog geen gegevens',
1071
1221
  'sync.off': 'Synchronisatie is hier niet beschikbaar (hostinstellingenservice nodig)',
1072
1222
  'sync.loading': 'Verbinden met hostinstellingen…',
1223
+ 'flow.creating': 'Bezig met aanmaken…',
1224
+ 'browse.title': 'Werkruimtemap kiezen',
1225
+ 'browse.home': 'Home',
1226
+ 'browse.up': 'Omhoog',
1227
+ 'browse.newFolder': 'Nieuwe map',
1228
+ 'browse.folderName': 'Mapnaam',
1229
+ 'browse.empty': 'Geen submappen hier',
1230
+ 'browse.loading': 'Laden…',
1231
+ 'browse.truncated': 'Te veel mappen; alleen het begin wordt getoond.',
1232
+ 'browse.showHidden': 'Verborgen bestanden tonen',
1233
+ 'browse.editPath': 'Pad bewerken',
1234
+ 'browse.select': 'Deze map gebruiken',
1235
+ 'browse.enter': 'Openen',
1236
+ 'browse.drives': 'Stations',
1237
+ 'browse.selectNamed': '"{name}" gebruiken',
1073
1238
  'error.title': 'Er is iets misgegaan',
1074
1239
  'cancel': 'Annuleren',
1075
1240
  'create': 'Aanmaken',
@@ -1165,6 +1330,21 @@ window.__ModuleLoader__.load({
1165
1330
  'sync.empty': 'Druga strona nie ma jeszcze danych',
1166
1331
  'sync.off': 'Synchronizacja niedostępna (wymaga usługi ustawień hosta)',
1167
1332
  'sync.loading': 'Łączenie z ustawieniami hosta…',
1333
+ 'flow.creating': 'Tworzenie…',
1334
+ 'browse.title': 'Wybierz folder obszaru roboczego',
1335
+ 'browse.home': 'Katalog domowy',
1336
+ 'browse.up': 'W górę',
1337
+ 'browse.newFolder': 'Nowy folder',
1338
+ 'browse.folderName': 'Nazwa folderu',
1339
+ 'browse.empty': 'Brak podfolderów',
1340
+ 'browse.loading': 'Wczytywanie…',
1341
+ 'browse.truncated': 'Zbyt wiele folderów; pokazano tylko początek.',
1342
+ 'browse.showHidden': 'Pokaż ukryte pliki',
1343
+ 'browse.editPath': 'Edytuj ścieżkę',
1344
+ 'browse.select': 'Użyj tego folderu',
1345
+ 'browse.enter': 'Otwórz',
1346
+ 'browse.drives': 'Dyski',
1347
+ 'browse.selectNamed': 'Użyj "{name}"',
1168
1348
  'error.title': 'Coś poszło nie tak',
1169
1349
  'cancel': 'Anuluj',
1170
1350
  'create': 'Utwórz',
@@ -1260,6 +1440,21 @@ window.__ModuleLoader__.load({
1260
1440
  'sync.empty': 'O outro lado ainda não tem dados',
1261
1441
  'sync.off': 'Sincronização indisponível aqui (requer o serviço de configurações do host)',
1262
1442
  'sync.loading': 'Conectando às configurações do host…',
1443
+ 'flow.creating': 'Criando…',
1444
+ 'browse.title': 'Escolher pasta da área de trabalho',
1445
+ 'browse.home': 'Início',
1446
+ 'browse.up': 'Acima',
1447
+ 'browse.newFolder': 'Nova pasta',
1448
+ 'browse.folderName': 'Nome da pasta',
1449
+ 'browse.empty': 'Nenhuma subpasta aqui',
1450
+ 'browse.loading': 'Carregando…',
1451
+ 'browse.truncated': 'Pastas demais; apenas o início é exibido.',
1452
+ 'browse.showHidden': 'Mostrar arquivos ocultos',
1453
+ 'browse.editPath': 'Editar caminho',
1454
+ 'browse.select': 'Usar esta pasta',
1455
+ 'browse.enter': 'Abrir',
1456
+ 'browse.drives': 'Unidades',
1457
+ 'browse.selectNamed': 'Usar "{name}"',
1263
1458
  'error.title': 'Algo deu errado',
1264
1459
  'cancel': 'Cancelar',
1265
1460
  'create': 'Criar',
@@ -1355,6 +1550,21 @@ window.__ModuleLoader__.load({
1355
1550
  'sync.empty': 'На другой стороне пока нет данных',
1356
1551
  'sync.off': 'Синхронизация недоступна (нужна служба настроек хоста)',
1357
1552
  'sync.loading': 'Подключение к настройкам хоста…',
1553
+ 'flow.creating': 'Создание…',
1554
+ 'browse.title': 'Выбор папки рабочей области',
1555
+ 'browse.home': 'Домашняя папка',
1556
+ 'browse.up': 'На уровень выше',
1557
+ 'browse.newFolder': 'Новая папка',
1558
+ 'browse.folderName': 'Имя папки',
1559
+ 'browse.empty': 'Здесь нет вложенных папок',
1560
+ 'browse.loading': 'Загрузка…',
1561
+ 'browse.truncated': 'Слишком много папок; показано только начало.',
1562
+ 'browse.showHidden': 'Показывать скрытые файлы',
1563
+ 'browse.editPath': 'Изменить путь',
1564
+ 'browse.select': 'Выбрать эту папку',
1565
+ 'browse.enter': 'Открыть',
1566
+ 'browse.drives': 'Диски',
1567
+ 'browse.selectNamed': 'Выбрать «{name}»',
1358
1568
  'error.title': 'Что-то пошло не так',
1359
1569
  'cancel': 'Отмена',
1360
1570
  'create': 'Создать',
@@ -1450,6 +1660,21 @@ window.__ModuleLoader__.load({
1450
1660
  'sync.empty': 'Andra sidan har inga data ännu',
1451
1661
  'sync.off': 'Synk är inte tillgänglig här (kräver värduppläggets inställningstjänst)',
1452
1662
  'sync.loading': 'Ansluter till värduppläggets inställningar…',
1663
+ 'flow.creating': 'Skapar…',
1664
+ 'browse.title': 'Välj arbetsyte-mapp',
1665
+ 'browse.home': 'Hem',
1666
+ 'browse.up': 'Upp',
1667
+ 'browse.newFolder': 'Ny mapp',
1668
+ 'browse.folderName': 'Mappnamn',
1669
+ 'browse.empty': 'Inga undermappar här',
1670
+ 'browse.loading': 'Laddar…',
1671
+ 'browse.truncated': 'För många mappar; endast början visas.',
1672
+ 'browse.showHidden': 'Visa dolda filer',
1673
+ 'browse.editPath': 'Redigera sökväg',
1674
+ 'browse.select': 'Använd den här mappen',
1675
+ 'browse.enter': 'Öppna',
1676
+ 'browse.drives': 'Enheter',
1677
+ 'browse.selectNamed': 'Använd "{name}"',
1453
1678
  'error.title': 'Något gick fel',
1454
1679
  'cancel': 'Avbryt',
1455
1680
  'create': 'Skapa',
@@ -1545,6 +1770,21 @@ window.__ModuleLoader__.load({
1545
1770
  'sync.empty': 'อีกฝั่งยังไม่มีข้อมูลให้ดึง',
1546
1771
  'sync.off': 'สภาพแวดล้อมนี้ไม่รองรับการซิงก์ (ต้องมีบริการการตั้งค่าของโฮสต์)',
1547
1772
  'sync.loading': 'กำลังเชื่อมต่อการตั้งค่าโฮสต์…',
1773
+ 'flow.creating': 'กำลังสร้าง…',
1774
+ 'browse.title': 'เลือกโฟลเดอร์เวิร์กสเปซ',
1775
+ 'browse.home': 'โฮม',
1776
+ 'browse.up': 'ขึ้นหนึ่งระดับ',
1777
+ 'browse.newFolder': 'โฟลเดอร์ใหม่',
1778
+ 'browse.folderName': 'ชื่อโฟลเดอร์',
1779
+ 'browse.empty': 'ไม่มีโฟลเดอร์ย่อยที่นี่',
1780
+ 'browse.loading': 'กำลังโหลด…',
1781
+ 'browse.truncated': 'มีโฟลเดอร์มากเกินไป แสดงเฉพาะส่วนต้น',
1782
+ 'browse.showHidden': 'แสดงไฟล์ที่ซ่อนอยู่',
1783
+ 'browse.editPath': 'แก้ไขเส้นทาง',
1784
+ 'browse.select': 'ใช้โฟลเดอร์นี้',
1785
+ 'browse.enter': 'เปิด',
1786
+ 'browse.drives': 'ไดรฟ์',
1787
+ 'browse.selectNamed': 'ใช้ "{name}"',
1548
1788
  'error.title': 'เกิดข้อผิดพลาด',
1549
1789
  'cancel': 'ยกเลิก',
1550
1790
  'create': 'สร้าง',
@@ -1640,6 +1880,21 @@ window.__ModuleLoader__.load({
1640
1880
  'sync.empty': 'Diğer tarafta alınacak veri yok',
1641
1881
  'sync.off': 'Bu ortamda eşitleme yok (ana makine ayar servisi gerekir)',
1642
1882
  'sync.loading': 'Ana makine ayarlarına bağlanılıyor…',
1883
+ 'flow.creating': 'Oluşturuluyor…',
1884
+ 'browse.title': 'Çalışma alanı klasörünü seç',
1885
+ 'browse.home': 'Ana dizin',
1886
+ 'browse.up': 'Üst düzey',
1887
+ 'browse.newFolder': 'Yeni klasör',
1888
+ 'browse.folderName': 'Klasör adı',
1889
+ 'browse.empty': 'Burada alt klasör yok',
1890
+ 'browse.loading': 'Yükleniyor…',
1891
+ 'browse.truncated': 'Çok fazla klasör var; yalnızca başlangıç gösteriliyor.',
1892
+ 'browse.showHidden': 'Gizli dosyaları göster',
1893
+ 'browse.editPath': 'Yolu düzenle',
1894
+ 'browse.select': 'Bu klasörü kullan',
1895
+ 'browse.enter': 'Aç',
1896
+ 'browse.drives': 'Sürücüler',
1897
+ 'browse.selectNamed': '"{name}" kullan',
1643
1898
  'error.title': 'Bir şeyler ters gitti',
1644
1899
  'cancel': 'İptal',
1645
1900
  'create': 'Oluştur',
@@ -1735,6 +1990,21 @@ window.__ModuleLoader__.load({
1735
1990
  'sync.empty': 'Bên kia chưa có dữ liệu để lấy',
1736
1991
  'sync.off': 'Môi trường này không hỗ trợ đồng bộ (cần dịch vụ cài đặt của máy chủ)',
1737
1992
  'sync.loading': 'Đang kết nối cài đặt máy chủ…',
1993
+ 'flow.creating': 'Đang tạo…',
1994
+ 'browse.title': 'Chọn thư mục không gian làm việc',
1995
+ 'browse.home': 'Thư mục chính',
1996
+ 'browse.up': 'Lên một cấp',
1997
+ 'browse.newFolder': 'Thư mục mới',
1998
+ 'browse.folderName': 'Tên thư mục',
1999
+ 'browse.empty': 'Không có thư mục con ở đây',
2000
+ 'browse.loading': 'Đang tải…',
2001
+ 'browse.truncated': 'Quá nhiều thư mục; chỉ hiển thị phần đầu.',
2002
+ 'browse.showHidden': 'Hiện tệp ẩn',
2003
+ 'browse.editPath': 'Sửa đường dẫn',
2004
+ 'browse.select': 'Dùng thư mục này',
2005
+ 'browse.enter': 'Mở',
2006
+ 'browse.drives': 'Ổ đĩa',
2007
+ 'browse.selectNamed': 'Dùng "{name}"',
1738
2008
  'error.title': 'Đã xảy ra lỗi',
1739
2009
  'cancel': 'Hủy',
1740
2010
  'create': 'Tạo',
@@ -1830,6 +2100,21 @@ window.__ModuleLoader__.load({
1830
2100
  'sync.empty': '另一端暫時未有資料可以獲取',
1831
2101
  'sync.off': '呢個環境唔支援同步(需要宿主設定服務)',
1832
2102
  'sync.loading': '連接緊宿主設定…',
2103
+ 'flow.creating': '建立緊…',
2104
+ 'browse.title': '揀工作區資料夾',
2105
+ 'browse.home': '主目錄',
2106
+ 'browse.up': '上一層',
2107
+ 'browse.newFolder': '新增資料夾',
2108
+ 'browse.folderName': '資料夾名稱',
2109
+ 'browse.empty': '呢個資料夾冇子資料夾',
2110
+ 'browse.loading': '載入緊…',
2111
+ 'browse.truncated': '資料夾太多,只顯示開頭部分。',
2112
+ 'browse.showHidden': '顯示隱藏檔案',
2113
+ 'browse.editPath': '編輯路徑',
2114
+ 'browse.select': '揀呢個資料夾',
2115
+ 'browse.enter': '入去',
2116
+ 'browse.drives': '磁碟',
2117
+ 'browse.selectNamed': '揀「{name}」',
1833
2118
  'error.title': '出咗錯',
1834
2119
  'cancel': '取消',
1835
2120
  'create': '建立',
@@ -1925,6 +2210,21 @@ window.__ModuleLoader__.load({
1925
2210
  'sync.empty': '另一端暫時未有資料可以獲取',
1926
2211
  'sync.off': '呢個環境唔支援同步(需要宿主設定服務)',
1927
2212
  'sync.loading': '連接緊宿主設定…',
2213
+ 'flow.creating': '建立緊…',
2214
+ 'browse.title': '揀工作區資料夾',
2215
+ 'browse.home': '主目錄',
2216
+ 'browse.up': '上一層',
2217
+ 'browse.newFolder': '新增資料夾',
2218
+ 'browse.folderName': '資料夾名稱',
2219
+ 'browse.empty': '呢個資料夾冇子資料夾',
2220
+ 'browse.loading': '載入緊…',
2221
+ 'browse.truncated': '資料夾太多,只顯示開頭部分。',
2222
+ 'browse.showHidden': '顯示隱藏檔案',
2223
+ 'browse.editPath': '編輯路徑',
2224
+ 'browse.select': '揀呢個資料夾',
2225
+ 'browse.enter': '入去',
2226
+ 'browse.drives': '磁碟',
2227
+ 'browse.selectNamed': '揀「{name}」',
1928
2228
  'error.title': '出咗錯',
1929
2229
  'cancel': '取消',
1930
2230
  'create': '建立',
@@ -2020,6 +2320,21 @@ window.__ModuleLoader__.load({
2020
2320
  'sync.empty': '另一端尚無資料可取得',
2021
2321
  'sync.off': '目前環境不支援同步(需要宿主設定服務)',
2022
2322
  'sync.loading': '正在連線宿主設定…',
2323
+ 'flow.creating': '正在建立…',
2324
+ 'browse.title': '選擇工作區資料夾',
2325
+ 'browse.home': '主目錄',
2326
+ 'browse.up': '上一層',
2327
+ 'browse.newFolder': '新增資料夾',
2328
+ 'browse.folderName': '資料夾名稱',
2329
+ 'browse.empty': '此資料夾沒有子資料夾',
2330
+ 'browse.loading': '載入中…',
2331
+ 'browse.truncated': '資料夾過多,僅顯示開頭部分。',
2332
+ 'browse.showHidden': '顯示隱藏檔案',
2333
+ 'browse.editPath': '編輯路徑',
2334
+ 'browse.select': '選擇此資料夾',
2335
+ 'browse.enter': '進入',
2336
+ 'browse.drives': '磁碟',
2337
+ 'browse.selectNamed': '選擇「{name}」',
2023
2338
  'error.title': '發生錯誤',
2024
2339
  'cancel': '取消',
2025
2340
  'create': '建立',
@@ -2570,7 +2885,40 @@ window.__ModuleLoader__.load({
2570
2885
  '.bw-btn-primary{background:var(--dsw-alias-brand-primary,#5b8def);border-color:transparent;color:var(--dsw-alias-brand-text,#fff)}',
2571
2886
  '.bw-btn-primary:hover{background:var(--dsw-alias-button-primary-hover,var(--dsw-alias-brand-primary,#5b8def))}',
2572
2887
  '.bw-btn:disabled{opacity:.5;cursor:default}',
2573
- '.bw-error-text{font-size:12.5px;color:var(--dsw-alias-state-error-primary,#f85149);word-break:break-all;max-width:380px}',
2888
+ '.bw-browse-body{min-width:360px;max-width:460px}',
2889
+ '.bw-browse-bar{display:flex;align-items:center;gap:4px}',
2890
+ '.bw-browse-crumbs{display:flex;align-items:center;gap:2px;flex:1;min-width:0;overflow-x:auto}',
2891
+ '.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}',
2892
+ '.bw-browse-crumb:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,#e6e6e6)}',
2893
+ '.bw-browse-crumb:disabled{opacity:.4;cursor:default}',
2894
+ '.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}',
2895
+ '.bw-browse-nav{font-size:13px;line-height:1}',
2896
+ '.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}',
2897
+ '.bw-browse-edit:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,#e6e6e6)}',
2898
+ '.bw-browse-edit svg{width:14px;height:14px}',
2899
+ '.bw-browse-edit-row{display:flex;gap:6px;align-items:center}',
2900
+ '.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))}',
2901
+ '.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}',
2902
+ '.bw-browse-row:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}',
2903
+ '.bw-browse-row:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5b8def);outline-offset:-2px}',
2904
+ '.bw-browse-row svg{width:15px;height:15px;flex:none;color:var(--dsw-alias-label-secondary,#b8b8b8)}',
2905
+ '.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)}',
2906
+ '.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}',
2907
+ '.bw-browse-open:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.16));color:var(--dsw-alias-label-primary,#e6e6e6)}',
2908
+ '.bw-browse-drives{display:flex;align-items:center;gap:4px;margin-left:auto}',
2909
+ '.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}',
2910
+ '.bw-browse-drive:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,#e6e6e6)}',
2911
+ '.bw-browse-drive-on{border-color:var(--dsw-alias-brand-primary,#5b8def);color:var(--dsw-alias-label-primary,#e6e6e6)}',
2912
+ '.bw-browse-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
2913
+ '.bw-browse-note{font-size:12px;color:var(--dsw-alias-label-tertiary,#9a9a9a);padding:10px 8px;text-align:center}',
2914
+ '.bw-browse-new-row{display:flex;gap:6px;align-items:center}',
2915
+ '.bw-browse-tools{display:flex;align-items:center;gap:6px;flex-wrap:wrap}',
2916
+ '.bw-browse-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px}',
2917
+ '.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}',
2918
+ '.bw-browse-tool:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,#e6e6e6)}',
2919
+ '.bw-browse-tool:disabled{opacity:.45;cursor:default}',
2920
+ '.bw-browse-tool-on{border-color:var(--dsw-alias-brand-primary,#5b8def);color:var(--dsw-alias-label-primary,#e6e6e6)}',
2921
+ '.bw-error-text{font-size:12.5px;color:var(--dsw-alias-state-error-primary,#f85149);word-break:break-all;max-width:380px}',
2574
2922
  '.bw-dialog-input-row{display:flex;gap:6px;align-items:center}',
2575
2923
  '.bw-glow-row{display:flex;align-items:center;gap:10px}',
2576
2924
  '.bw-slider{flex:1;accent-color:var(--dsw-alias-brand-primary,#5b8def);height:22px}',
@@ -2790,6 +3138,65 @@ window.__ModuleLoader__.load({
2790
3138
  },
2791
3139
  })
2792
3140
 
3141
+ /* ==================== directory-picker capability ================== */
3142
+
3143
+ // The host composes exactly ONE directory-picking backend
3144
+ // (@deepseek-ai/dsh-host-directory-picker-auto): a loopback-only webserver
3145
+ // bind on a display-bearing host gets `native` — one OS chooser on the
3146
+ // HOST's own screen — while every other bind (all-interfaces/LAN, SSH,
3147
+ // headless) gets `browse`, whose wire verbs are list/createDirectory ONLY
3148
+ // and whose `pick` is refused BY DESIGN with `directory-picker/unavailable`
3149
+ // ("... needs the native capability; the composed picker serves browse").
3150
+ // A flow that hard-assumed the OS chooser therefore dies on every LAN bind
3151
+ // and no workspace can be added at all. Probe the composed backend once per
3152
+ // page and keep BOTH interactions: the OS chooser, and an in-app browser
3153
+ // driven by the browse primitives (see DirectoryBrowseDialog).
3154
+ const pickerState = { kind: 'unknown', probe: null, api: null }
3155
+ /**
3156
+ * True for the refusal a Host serving the OTHER capability throws: the wire
3157
+ * code when the caller still carries it, the Host's fixed sentence
3158
+ * otherwise (uiWorkspace.pickDirectory wraps the RPC failure in a plain
3159
+ * Error, so the code is gone by the time the flow sees it).
3160
+ */
3161
+ const pickerRefusal = (error) => {
3162
+ if (error && typeof error === 'object' && error.rpcError
3163
+ && error.rpcError.code === 'directory-picker/unavailable') return true
3164
+ const message = messageOf(error)
3165
+ return message.indexOf('needs the native capability') !== -1
3166
+ || message.indexOf('directory-picker/unavailable') !== -1
3167
+ }
3168
+ /**
3169
+ * Resolve which backend this page's Host composes. `list` succeeds exactly
3170
+ * on `browse` and is refused with the capability code on `native`, so one
3171
+ * call answers both worlds without ever opening a chooser or creating a
3172
+ * directory. An unclassified failure (a carrier still connecting at boot)
3173
+ * is NOT cached: the next open re-probes, and the flow falls back to trying
3174
+ * the chooser itself.
3175
+ */
3176
+ const pickerCapabilityNow = () => {
3177
+ if (pickerState.kind !== 'unknown') return Promise.resolve(pickerState.kind)
3178
+ if (pickerState.probe !== null) return pickerState.probe
3179
+ const api = pickerState.api
3180
+ if (!api || typeof api.listDirectory !== 'function') return Promise.resolve('unknown')
3181
+ const probe = Promise.resolve()
3182
+ .then(() => api.listDirectory(undefined, undefined))
3183
+ .then(
3184
+ () => 'browse',
3185
+ (error) => (pickerRefusal(error) ? 'native' : 'unknown'),
3186
+ )
3187
+ pickerState.probe = probe.then((kind) => {
3188
+ if (kind === 'unknown') { pickerState.probe = null; return kind }
3189
+ pickerState.kind = kind
3190
+ return kind
3191
+ })
3192
+ return pickerState.probe
3193
+ }
3194
+ /** Commit a browse verdict the flow discovered the hard way (a refused pick). */
3195
+ const markPickerBrowse = () => {
3196
+ pickerState.kind = 'browse'
3197
+ pickerState.probe = Promise.resolve('browse')
3198
+ }
3199
+
2793
3200
  /* ============================ flow dialog ========================= */
2794
3201
 
2795
3202
  const BTN = (props) => (
@@ -2864,6 +3271,406 @@ window.__ModuleLoader__.load({
2864
3271
  }
2865
3272
  }
2866
3273
 
3274
+ /**
3275
+ * The in-app directory browser: the interaction a `browse`-composed Host can
3276
+ * actually serve. It appears when the probe (or a refused pick) proves the
3277
+ * OS chooser is not this Host's backend — a LAN/all-interfaces bind, an SSH
3278
+ * launch, a headless host — where `pick` is refused by design and
3279
+ * list/createDirectory are the only wire verbs. One level at a time: a
3280
+ * clickable Home-rooted breadcrumb chain, an editable path, an inline
3281
+ * new-folder row and the hidden-entry toggle; the listed level (or any
3282
+ * ancestor crumb) is the pick. Browse failures stay inside the dialog — the
3283
+ * owner's error surface belongs to the pick/create conversation, and a
3284
+ * denied listing must never close the flow.
3285
+ */
3286
+ /**
3287
+ * Page-lifetime cache of the Windows drive letters this Host actually
3288
+ * serves. The browse API can only list a path, so the first open probes the
3289
+ * usual letters once (six cheap listings, a missing drive fails silently)
3290
+ * and every later open renders the chips straight from here.
3291
+ */
3292
+ const browseDrives = { probed: false, probing: false, list: [] }
3293
+
3294
+ /**
3295
+ * Run the one wire verb that actually puts a folder on disk and hand back
3296
+ * the created path. This is deliberately a module-level seam the smoke suite
3297
+ * drives for real: v0.11.4 folded the call into the success handler and
3298
+ * dropped it, so the form closed on a resolved `undefined` — no request, no
3299
+ * error, no folder. A string-level assertion cannot see that; calling it can.
3300
+ */
3301
+ const createFolderIn = (createDirectory, path, name) => Promise.resolve()
3302
+ .then(() => createDirectory(path, name))
3303
+
3304
+ function DirectoryBrowseDialog(props) {
3305
+ const { open, busy, listDirectory, createDirectory, onPick, onClose, t } = props
3306
+ const [listing, setListing] = React.useState(null)
3307
+ const [loading, setLoading] = React.useState(false)
3308
+ const [error, setError] = React.useState('')
3309
+ const [showHidden, setShowHidden] = React.useState(false)
3310
+ const [editing, setEditing] = React.useState(false)
3311
+ const [draft, setDraft] = React.useState('')
3312
+ const [creating, setCreating] = React.useState(false)
3313
+ const [createName, setCreateName] = React.useState('')
3314
+ const [createBusy, setCreateBusy] = React.useState(false)
3315
+ const [createError, setCreateError] = React.useState('')
3316
+ const [selected, setSelected] = React.useState('')
3317
+ const [drives, setDrives] = React.useState(browseDrives.list)
3318
+ // Guards a superseded scan: a slow level must not overwrite a newer one.
3319
+ const seqRef = React.useRef(0)
3320
+ const crumbsRef = React.useRef(null)
3321
+
3322
+ const go = (path, keepSelected) => {
3323
+ const seq = seqRef.current + 1
3324
+ seqRef.current = seq
3325
+ setLoading(true)
3326
+ setError('')
3327
+ if (keepSelected !== true) setSelected('')
3328
+ Promise.resolve()
3329
+ .then(() => listDirectory(path, undefined))
3330
+ .then((next) => {
3331
+ if (seqRef.current !== seq) return
3332
+ setListing(next || null)
3333
+ setLoading(false)
3334
+ })
3335
+ .catch((reason) => {
3336
+ if (seqRef.current !== seq) return
3337
+ setLoading(false)
3338
+ setError(messageOf(reason))
3339
+ })
3340
+ }
3341
+
3342
+ React.useEffect(() => {
3343
+ if (!open) return undefined
3344
+ seqRef.current += 1
3345
+ setListing(null)
3346
+ setError('')
3347
+ setEditing(false)
3348
+ setCreating(false)
3349
+ setCreateName('')
3350
+ setCreateBusy(false)
3351
+ setCreateError('')
3352
+ setShowHidden(false)
3353
+ setSelected('')
3354
+ go(undefined)
3355
+ return () => { seqRef.current += 1 }
3356
+ }, [open])
3357
+
3358
+ const currentPath = listing && typeof listing.path === 'string' ? listing.path : ''
3359
+ const homePath = listing && typeof listing.home === 'string' ? listing.home : ''
3360
+ const windowsHost = homePath.indexOf('\\') !== -1
3361
+ const hasListing = listing !== null
3362
+
3363
+ // Windows drives are separate filesystem roots and the browse API can only
3364
+ // list a path, so probe the usual letters once per page (cached above) and
3365
+ // offer the ones that answered. A drive nobody probed is still reachable
3366
+ // through the path editor, which is the only general answer anyway.
3367
+ React.useEffect(() => {
3368
+ if (!open || !hasListing || !windowsHost) return undefined
3369
+ if (browseDrives.probed || browseDrives.probing) {
3370
+ if (browseDrives.list.length > 0) setDrives(browseDrives.list)
3371
+ return undefined
3372
+ }
3373
+ browseDrives.probing = true
3374
+ let alive = true
3375
+ const letters = ['C:', 'D:', 'E:', 'F:', 'G:', 'H:']
3376
+ Promise.all(letters.map((letter) => Promise.resolve()
3377
+ .then(() => listDirectory(letter + '\\', undefined))
3378
+ .then(() => letter, () => null)))
3379
+ .then((found) => {
3380
+ browseDrives.list = found.filter(Boolean)
3381
+ browseDrives.probed = true
3382
+ browseDrives.probing = false
3383
+ if (alive) setDrives(browseDrives.list)
3384
+ }, () => { browseDrives.probing = false })
3385
+ return () => { alive = false }
3386
+ }, [open, hasListing, windowsHost])
3387
+
3388
+ // A deep path must show its TAIL — that is where you are — so the chain
3389
+ // keeps its right edge in view instead of cutting the last crumb off.
3390
+ React.useEffect(() => {
3391
+ const node = crumbsRef.current
3392
+ if (node) node.scrollLeft = node.scrollWidth
3393
+ }, [listing])
3394
+
3395
+ if (!open) return null
3396
+
3397
+ // The whole ancestry from the filesystem ROOT: rooting the chain at Home
3398
+ // (the official dialog's choice) hides where the listed level actually is,
3399
+ // and the drive crumb answers "which disk am I on" at a glance.
3400
+ const crumbs = listing && Array.isArray(listing.crumbs) ? listing.crumbs : []
3401
+ const parentPath = crumbs.length > 1 ? crumbs[crumbs.length - 2].path : ''
3402
+ const entries = (listing && Array.isArray(listing.entries) ? listing.entries : [])
3403
+ .filter((entry) => showHidden || !entry.hidden)
3404
+ const selectedEntry = entries.find((entry) => entry.path === selected) || null
3405
+ const targetPath = selectedEntry ? selectedEntry.path : currentPath
3406
+
3407
+ const submitDraft = () => {
3408
+ const path = draft.trim()
3409
+ setEditing(false)
3410
+ if (path === '') { go(undefined); return }
3411
+ go(path)
3412
+ }
3413
+ const submitCreate = () => {
3414
+ const name = createName.trim()
3415
+ if (name === '' || createBusy || currentPath === '') return
3416
+ setCreateBusy(true)
3417
+ setCreateError('')
3418
+ createFolderIn(createDirectory, currentPath, name)
3419
+ .then((created) => {
3420
+ setCreateBusy(false)
3421
+ setCreating(false)
3422
+ setCreateName('')
3423
+ // Land on the folder just made: it is almost always the one the
3424
+ // operator wants to adopt next.
3425
+ if (typeof created === 'string' && created !== '') setSelected(created)
3426
+ go(currentPath, true)
3427
+ })
3428
+ .catch((reason) => {
3429
+ setCreateBusy(false)
3430
+ setCreateError(messageOf(reason))
3431
+ })
3432
+ }
3433
+
3434
+ const head = editing
3435
+ ? E('div', { className: 'bw-browse-edit-row' },
3436
+ E('input', {
3437
+ className: 'bw-input',
3438
+ value: draft,
3439
+ autoFocus: true,
3440
+ spellCheck: false,
3441
+ placeholder: currentPath,
3442
+ onChange: (e) => setDraft(e.target.value),
3443
+ onKeyDown: (e) => {
3444
+ if (e.key === 'Enter') submitDraft()
3445
+ else if (e.key === 'Escape') setEditing(false)
3446
+ },
3447
+ }),
3448
+ E(BTN, { variant: 'outline', onClick: () => setEditing(false) }, t('cancel')),
3449
+ )
3450
+ : E('div', { className: 'bw-browse-bar' },
3451
+ E('button', {
3452
+ type: 'button',
3453
+ className: 'bw-browse-crumb bw-browse-nav',
3454
+ title: t('browse.up'),
3455
+ 'aria-label': t('browse.up'),
3456
+ disabled: parentPath === '',
3457
+ onClick: () => { if (parentPath !== '') go(parentPath) },
3458
+ }, '↑'),
3459
+ E('div', { className: 'bw-browse-crumbs', ref: crumbsRef },
3460
+ crumbs.map((crumb, index) => (index === crumbs.length - 1
3461
+ ? E('span', { key: crumb.path, className: 'bw-browse-crumb bw-browse-current', title: crumb.path }, crumb.name)
3462
+ : E('button', {
3463
+ key: crumb.path,
3464
+ type: 'button',
3465
+ className: 'bw-browse-crumb',
3466
+ title: crumb.path,
3467
+ onClick: () => go(crumb.path),
3468
+ }, crumb.name))),
3469
+ ),
3470
+ E('button', {
3471
+ type: 'button',
3472
+ className: 'bw-browse-edit',
3473
+ title: t('browse.editPath'),
3474
+ 'aria-label': t('browse.editPath'),
3475
+ onClick: () => { setDraft(currentPath); setEditing(true) },
3476
+ }, icon('IconEditOutline16')),
3477
+ )
3478
+
3479
+ // One click SELECTS a row (the footer button then adopts it — no need to
3480
+ // step inside just to pick a folder), a double click or the row's chevron
3481
+ // enters it. Touch needs the chevron: a double tap is not a gesture.
3482
+ const rows = entries.map((entry) => E('div', {
3483
+ key: entry.path,
3484
+ className: cls('bw-browse-row', selected === entry.path && 'bw-browse-row-on'),
3485
+ title: entry.path,
3486
+ role: 'button',
3487
+ tabIndex: 0,
3488
+ onClick: () => setSelected((current) => (current === entry.path ? '' : entry.path)),
3489
+ onDoubleClick: () => go(entry.path),
3490
+ onKeyDown: (event) => {
3491
+ if (event.key === 'Enter') go(entry.path)
3492
+ else if (event.key === ' ') { event.preventDefault(); setSelected((current) => (current === entry.path ? '' : entry.path)) }
3493
+ },
3494
+ },
3495
+ icon('IconFolderClose16'),
3496
+ E('span', { className: 'bw-browse-name' }, entry.name),
3497
+ E('button', {
3498
+ type: 'button',
3499
+ className: 'bw-browse-open',
3500
+ title: t('browse.enter'),
3501
+ 'aria-label': t('browse.enter'),
3502
+ onClick: (event) => { event.stopPropagation(); go(entry.path) },
3503
+ }, '›'),
3504
+ ))
3505
+
3506
+ const body = loading
3507
+ ? E('div', { className: 'bw-browse-note' }, t('browse.loading'))
3508
+ : rows.length > 0
3509
+ ? rows
3510
+ : E('div', { className: 'bw-browse-note' }, t('browse.empty'))
3511
+
3512
+ const newRow = creating
3513
+ ? E('div', { className: 'bw-browse-new-row' },
3514
+ E('input', {
3515
+ className: 'bw-input',
3516
+ value: createName,
3517
+ autoFocus: true,
3518
+ spellCheck: false,
3519
+ placeholder: t('browse.folderName'),
3520
+ disabled: createBusy,
3521
+ onChange: (e) => setCreateName(e.target.value),
3522
+ onKeyDown: (e) => {
3523
+ if (e.key === 'Enter') submitCreate()
3524
+ else if (e.key === 'Escape') { setCreating(false); setCreateError('') }
3525
+ },
3526
+ }),
3527
+ E(BTN, {
3528
+ variant: 'outline',
3529
+ disabled: createBusy,
3530
+ onClick: () => { setCreating(false); setCreateError('') },
3531
+ }, t('cancel')),
3532
+ E(BTN, { variant: 'primary', onClick: submitCreate, disabled: createBusy },
3533
+ createBusy ? t('flow.creating') : t('create')),
3534
+ )
3535
+ : null
3536
+
3537
+ // The dialog is deliberately narrower than the official browser, so the
3538
+ // two browsing tools sit on their own row above the list instead of
3539
+ // sharing the footer with the commit pair (they used to wrap there).
3540
+ const tools = E('div', { className: 'bw-browse-tools' },
3541
+ E('button', {
3542
+ type: 'button',
3543
+ className: cls('bw-browse-tool', currentPath !== '' && currentPath === homePath && 'bw-browse-tool-on'),
3544
+ disabled: homePath === '' || currentPath === homePath,
3545
+ onClick: () => { if (homePath !== '') go(homePath) },
3546
+ }, t('browse.home')),
3547
+ E('button', {
3548
+ type: 'button',
3549
+ className: cls('bw-browse-tool', showHidden && 'bw-browse-tool-on'),
3550
+ 'aria-pressed': showHidden ? 'true' : 'false',
3551
+ onClick: () => setShowHidden((value) => !value),
3552
+ }, t('browse.showHidden')),
3553
+ E('button', {
3554
+ type: 'button',
3555
+ className: 'bw-browse-tool',
3556
+ disabled: creating || currentPath === '',
3557
+ onClick: () => { setCreating(true); setCreateName(''); setCreateError('') },
3558
+ }, t('browse.newFolder')),
3559
+ drives.length > 1
3560
+ ? E('div', {
3561
+ className: 'bw-browse-drives',
3562
+ title: t('browse.drives'),
3563
+ role: 'group',
3564
+ 'aria-label': t('browse.drives'),
3565
+ }, drives.map((drive) => E('button', {
3566
+ key: drive,
3567
+ type: 'button',
3568
+ className: cls('bw-browse-drive', currentPath.slice(0, 2).toUpperCase() === drive && 'bw-browse-drive-on'),
3569
+ title: drive + '\\',
3570
+ onClick: () => go(drive + '\\'),
3571
+ }, drive)))
3572
+ : null,
3573
+ )
3574
+
3575
+ const footer = E('div', { className: 'bw-browse-actions' },
3576
+ E(BTN, { variant: 'outline', onClick: onClose, disabled: busy === true }, t('cancel')),
3577
+ E(BTN, {
3578
+ variant: 'primary',
3579
+ disabled: busy === true || targetPath === '',
3580
+ onClick: () => { if (targetPath !== '') onPick(targetPath) },
3581
+ }, selectedEntry !== null ? t('browse.selectNamed', { name: selectedEntry.name }) : t('browse.select')),
3582
+ )
3583
+
3584
+ return E(ui.Modal, {
3585
+ open: true,
3586
+ onClose: () => { if (busy !== true) onClose() },
3587
+ closeLabel: t('close'),
3588
+ title: t('browse.title'),
3589
+ footer,
3590
+ },
3591
+ E('div', { className: 'bw-modal-body bw-browse-body' },
3592
+ head,
3593
+ createError !== '' ? E('div', { className: 'bw-error-text', role: 'alert' }, createError) : null,
3594
+ tools,
3595
+ newRow,
3596
+ E('div', { className: 'bw-browse-list', role: 'list' }, body),
3597
+ listing && listing.truncated === true ? E('div', { className: 'bw-hint' }, t('browse.truncated')) : null,
3598
+ error !== '' ? E('div', { className: 'bw-error-text', role: 'alert' }, error) : null,
3599
+ ),
3600
+ StyleNode())
3601
+ }
3602
+
3603
+ /**
3604
+ * The add-workspace picking interaction as the sidebar hole's OCCUPANT
3605
+ * (v0.12.1): the official flow cannot be rendered here — a child-hole
3606
+ * declaration is exclusive and the shipped browser entry, though it
3607
+ * loses the render race, still owns 'sidebar.workspaces.directoryFlow's
3608
+ * declaration, so re-declaring it throws and renderSlot authorization
3609
+ * is runtime-enforced. This flow therefore probes the composed backend
3610
+ * (v0.11.3 discipline): a native Host gets its OS chooser through the
3611
+ * SAME official host service (uiWorkspace.pickDirectory), a browse Host
3612
+ * gets the in-app DirectoryBrowseDialog (the v0.11.4/0.11.5 interaction
3613
+ * this plugin polished). A picked path is adopted by the owner with
3614
+ * createWorkspace + startSession — no parent-group popup (groups derive
3615
+ * from names; rename or drag to group). The conversation hero flow stays
3616
+ * purely official: this plugin does NOT occupy that hole.
3617
+ */
3618
+ function BetterFlow(props) {
3619
+ const { open, busy, onPicked, onCancel, onError, pickDirectory, listDirectory, createDirectory, t } = props
3620
+ const [phase, setPhase] = React.useState('idle') // idle | picking | browsing
3621
+ // All hooks run before any early return: the flow unmounts while
3622
+ // closed, but its hook sequence must stay stable.
3623
+ React.useEffect(() => {
3624
+ if (!open) {
3625
+ setPhase('idle')
3626
+ return
3627
+ }
3628
+ let alive = true
3629
+ setPhase('picking')
3630
+ // Which backend this Host composes decides the interaction: browse
3631
+ // renders the in-app browser directly (its pick is refused by
3632
+ // design, so trying the chooser first would only surface an error);
3633
+ // native opens the OS chooser; an unresolved verdict still tries the
3634
+ // chooser so a silent probe never changes the fallback.
3635
+ pickerCapabilityNow()
3636
+ .then((kind) => {
3637
+ if (!alive) return undefined
3638
+ if (kind === 'browse') { setPhase('browsing'); return undefined }
3639
+ return Promise.resolve()
3640
+ .then(() => pickDirectory())
3641
+ .then((path) => {
3642
+ if (!alive) return
3643
+ if (!path) { onCancel(); return }
3644
+ onPicked(String(path))
3645
+ })
3646
+ })
3647
+ .catch((reason) => {
3648
+ if (!alive) return
3649
+ // A refusal is a backend fact, not a failure: this Host serves
3650
+ // the browse verbs, so switch interactions instead of erroring.
3651
+ if (pickerRefusal(reason)) { markPickerBrowse(); setPhase('browsing'); return }
3652
+ setPhase('idle')
3653
+ onError(messageOf(reason))
3654
+ })
3655
+ return () => { alive = false }
3656
+ }, [open])
3657
+
3658
+ if (!open) return null
3659
+ if (phase === 'browsing') {
3660
+ return E(DirectoryBrowseDialog, {
3661
+ open: true,
3662
+ busy: busy === true,
3663
+ listDirectory,
3664
+ createDirectory,
3665
+ onPick: (path) => onPicked(String(path)),
3666
+ onClose: onCancel,
3667
+ t,
3668
+ })
3669
+ }
3670
+ return null
3671
+ }
3672
+
3673
+
2867
3674
  /* ============================== rows ============================== */
2868
3675
 
2869
3676
  /** Folder glyph variants from the primitives family (solid / outline / hidden). */
@@ -3614,7 +4421,8 @@ window.__ModuleLoader__.load({
3614
4421
  startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace,
3615
4422
  archiveSession, createWorkspace,
3616
4423
  insertWorkspaceBefore, insertSessionBefore,
3617
- useDirectoryFlow, renderSlot,
4424
+ useDirectoryFlow,
4425
+ pickDirectory, listDirectory, createDirectory,
3618
4426
  t,
3619
4427
  } = props
3620
4428
 
@@ -3712,10 +4520,9 @@ window.__ModuleLoader__.load({
3712
4520
  const [searchOpen, setSearchOpen] = React.useState(false)
3713
4521
  const [flowOpen, setFlowOpen] = React.useState(false)
3714
4522
  const [flowBusy, setFlowBusy] = React.useState(false) // createWorkspace in flight after a pick
3715
- // Occupancy of this surface's directory-flow hole (the official
3716
- // composed picker fills the child hole our registration declares):
3717
- // gates the add affordance exactly like the shipped browser, and an
3718
- // occupant that unloads mid-interaction withdraws an open flow.
4523
+ // Occupancy of this surface's directory-flow hole (our own picking
4524
+ // flow fills it): gates the add affordance, and an occupant that
4525
+ // unloads mid-interaction withdraws an open flow.
3719
4526
  const flowAvailable = typeof useDirectoryFlow === 'function' ? useDirectoryFlow(occupied => occupied) : false
3720
4527
  React.useEffect(() => {
3721
4528
  if (flowOpen && !flowAvailable) setFlowOpen(false)
@@ -4609,7 +5416,8 @@ window.__ModuleLoader__.load({
4609
5416
 
4610
5417
  // The owner side of the flow conversation: adopt keeps the flow open
4611
5418
  // (busy) until the Host answers, then startSession — exactly what the
4612
- // shipped browser does after the same pick.
5419
+ // shipped browser does after the same pick. Groups never ride this
5420
+ // flow (v0.12.0+): rename or drag to group instead.
4613
5421
  const adoptDirectory = (path) => Promise.resolve()
4614
5422
  .then(() => createWorkspace({ path: String(path) }))
4615
5423
  .then((workspace) => {
@@ -4649,10 +5457,19 @@ window.__ModuleLoader__.load({
4649
5457
  flowAvailable ? E('button', { type: 'button', className: 'bw-icon-btn', 'aria-label': t('add'), onClick: () => setFlowOpen(true) }, icon('IconProjectAddOutline16')) : null,
4650
5458
  ),
4651
5459
  E('div', { ref: treeRef, className: 'bw-tree', role: 'tree', 'aria-label': t('title') }, bodyRows),
4652
- // The official composed picker serves this hole; renderSlot is the
4653
- // narrowed child-slot share the renderer injects for the children
4654
- // our registration declares.
4655
- typeof renderSlot === 'function' ? renderSlot('sidebar.workspaces.directoryFlow', flowOwner) : null,
5460
+ // Our own occupant serves the sidebar hole (direct composition, the
5461
+ // same props conversation the official owner share defines).
5462
+ E(BetterFlow, {
5463
+ open: flowOpen,
5464
+ busy: flowBusy,
5465
+ onPicked: flowOwner.onPicked,
5466
+ onCancel: flowOwner.onCancel,
5467
+ onError: flowOwner.onError,
5468
+ pickDirectory,
5469
+ listDirectory,
5470
+ createDirectory,
5471
+ t,
5472
+ }),
4656
5473
  dialogElement,
4657
5474
  ctx !== null ? E('div', {
4658
5475
  className: 'bw-ctx-overlay',
@@ -4733,6 +5550,11 @@ window.__ModuleLoader__.load({
4733
5550
  })
4734
5551
  return
4735
5552
  }
5553
+ // The capability probe talks to the composed backend through this
5554
+ // service (list is served by browse alone), so the state module needs
5555
+ // the handle before any flow can open.
5556
+ pickerState.api = uiWorkspace
5557
+
4736
5558
  if (ctx.locale && typeof ctx.locale.register === 'function') {
4737
5559
  ctx.effect(() => {
4738
5560
  try {
@@ -4782,14 +5604,26 @@ window.__ModuleLoader__.load({
4782
5604
  : undefined,
4783
5605
  archiveSession: (sessionId) => uiWorkspace.archiveSession(sessionId),
4784
5606
  createWorkspace: (input) => workspaces.create(input),
4785
- // Occupancy of this surface's directory-flow hole (the official
4786
- // composed picker fills it): gates the add affordance exactly like
4787
- // the shipped browser does.
5607
+ pickDirectory: () => uiWorkspace.pickDirectory(),
5608
+ // Browse-side primitives: the in-app dialog needs them whenever the
5609
+ // Host composes the browse backend instead of the OS chooser.
5610
+ listDirectory: (path, signal) => uiWorkspace.listDirectory(path, signal),
5611
+ createDirectory: (path, name) => uiWorkspace.createDirectory(path, name),
4788
5612
  hooks: {
4789
5613
  directoryFlow: flowSource(slots, 'sidebar.workspaces.directoryFlow'),
4790
5614
  },
4791
5615
  })
4792
5616
 
5617
+ // Occupant inject face for the sidebar hole (v0.12.1): the picking
5618
+ // primitives BetterFlow needs. The conversation hero hole is
5619
+ // deliberately NOT occupied — that flow stays purely official.
5620
+ const flowInjected = () => ({
5621
+ pickDirectory: () => uiWorkspace.pickDirectory(),
5622
+ listDirectory: (path, signal) => uiWorkspace.listDirectory(path, signal),
5623
+ createDirectory: (path, name) => uiWorkspace.createDirectory(path, name),
5624
+ hooks: { directoryFlow: flowSource(slots, 'sidebar.workspaces.directoryFlow') },
5625
+ })
5626
+
4793
5627
  // Registration helper: a thrown register (semantics drift, vanishing
4794
5628
  // hole declaration mid-transition) degrades this one seat, never the
4795
5629
  // plugin fiber — the whole web boot is all-or-nothing.
@@ -4802,11 +5636,14 @@ window.__ModuleLoader__.load({
4802
5636
  }
4803
5637
  }
4804
5638
 
4805
- // The two directory-flow holes are NOT occupied anymore (v0.12.0): the
4806
- // official composed picker the OS chooser on loopbound display hosts,
4807
- // the official in-app browser everywhere else serves the conversation
4808
- // hero flow again, and the sidebar flow through the child hole our own
4809
- // browser registration declares below.
5639
+ // The SIDEBAR hole is occupied by our own picking flow at priority -1:
5640
+ // rendering the official occupant is impossible here (a child-hole
5641
+ // declaration is exclusive the shipped browser entry keeps owning
5642
+ // 'sidebar.workspaces.directoryFlow' even after losing the render race,
5643
+ // and re-declaring throws), so the flow probes the backend itself — the
5644
+ // OS chooser through the same official host service on native Hosts,
5645
+ // the polished in-app dialog on browse Hosts. The HERO hole stays
5646
+ // untouched: the conversation empty-state flow is purely official.
4810
5647
 
4811
5648
  // One shared store handle: the browser and the settings page must see the
4812
5649
  // same persisted state (expansion, folder list, prefs, styling).
@@ -4831,6 +5668,15 @@ window.__ModuleLoader__.load({
4831
5668
  prefsScopeRef = null
4832
5669
  }
4833
5670
 
5671
+ // The sidebar hole occupation itself (v0.12.1): priority -1 shadows
5672
+ // the official composed picker inside the hole — the hero hole keeps
5673
+ // the official occupant, so the conversation flow stays stock.
5674
+ slots.inject('sidebar.workspaces.directoryFlow', guarded(
5675
+ 'sidebar.workspaces.directoryFlow',
5676
+ { name: 'sidebar.workspaces.directoryFlow', inject: flowInjected, locale: NS, priority: -1 },
5677
+ BetterFlow,
5678
+ ))
5679
+
4834
5680
  // Settings → Plugins card only (the tab dispatches the intersection of
4835
5681
  // served namespaces — registered host-side — and settings.plugin.item
4836
5682
  // cards). The old left-nav settings.section entry was removed: the
@@ -4863,17 +5709,16 @@ window.__ModuleLoader__.load({
4863
5709
  ))
4864
5710
 
4865
5711
  // the browser itself — lowest priority renders, shadowing the shipped
4866
- // entry. The registration DECLARES the directory-flow child hole without
4867
- // occupying it: the official composed picker registers into it and the
4868
- // browser consumes it through its narrowed renderSlot prop (exactly the
4869
- // official WorkspaceBrowser pattern), so add-workspace runs whichever
4870
- // interaction the Host actually serves.
5712
+ // entry. NO children declaration here (v0.12.1): the child-hole
5713
+ // declaration is exclusive and the shipped entry already owns it
5714
+ // re-declaring would throw and silently unregister this seat (the
5715
+ // v0.12.0 regression). The add flow rides the sidebar-hole occupation
5716
+ // registered above instead.
4871
5717
  slots.inject('sidebar.workspaces', guarded(
4872
5718
  'sidebar.workspaces',
4873
5719
  {
4874
5720
  name: 'sidebar.workspaces',
4875
5721
  priority: -1,
4876
- children: { 'sidebar.workspaces.directoryFlow': { kind: 'single', scope: 'root' } },
4877
5722
  store: viewStore,
4878
5723
  inject: browserInjected,
4879
5724
  locale: NS,