dsh-better-workspace 0.15.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-better-workspace",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "main": "./src/index.js",
5
5
  "description": "侧边栏工作区树:按官方磁盘目录嵌套(官方 workspace-tree 语义),可选按名称中的 / 分组(默认关闭,设置里开启);添加工作区由官方目录流承载。Workspace tree for the DSH sidebar workspace list, nested by official disk folders with optional \"/\"-name grouping.",
6
6
  "engines": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-better-workspace",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "侧边栏工作区树:按官方磁盘目录嵌套(官方 workspace-tree 语义),可选按名称中的 / 分组(默认关闭,设置里开启);添加工作区走官方目录流。Workspace tree for the DSH sidebar: disk-path nesting (official workspace-tree semantics), with optional \"/\"-name grouping (off by default); add-workspace rides the official directory flow.",
5
5
  "type": "module",
6
6
  "repository": {
package/src/client.js CHANGED
@@ -125,20 +125,8 @@ window.__ModuleLoader__.load({
125
125
  'sync.off': '当前环境不支持同步(需要宿主设置服务)',
126
126
  'sync.loading': '正在连接宿主设置…',
127
127
  'flow.creating': '正在创建…',
128
- 'browse.title': '选择工作区目录',
129
- 'browse.home': '主目录',
130
- 'browse.up': '上一层',
131
- 'browse.newFolder': '新建文件夹',
132
- 'browse.folderName': '文件夹名称',
133
- 'browse.empty': '此文件夹没有子文件夹',
134
- 'browse.loading': '加载中…',
135
- 'browse.truncated': '文件夹过多,仅显示开头部分。',
136
- 'browse.showHidden': '显示隐藏文件',
137
- 'browse.editPath': '编辑路径',
138
- 'browse.select': '选择此文件夹',
139
- 'browse.enter': '进入',
140
- 'browse.drives': '盘符',
141
- 'browse.selectNamed': '选择「{name}」',
128
+ 'add.guide.title': "应用内添加工作区",
129
+ 'add.guide': "此主机不提供桌面目录选择器。请到「新会话」页,用官方的「添加工作区」打开应用内浏览器。",
142
130
  'error.title': '出错了',
143
131
  'cancel': '取消',
144
132
  'create': '创建',
@@ -250,20 +238,8 @@ window.__ModuleLoader__.load({
250
238
  'sync.off': 'Sync unavailable here (requires the host settings service)',
251
239
  'sync.loading': 'Connecting to host settings…',
252
240
  'flow.creating': 'Creating…',
253
- 'browse.title': 'Select Workspace Directory',
254
- 'browse.home': 'Home',
255
- 'browse.up': 'Up',
256
- 'browse.newFolder': 'New folder',
257
- 'browse.folderName': 'Folder name',
258
- 'browse.empty': 'No subfolders here',
259
- 'browse.loading': 'Loading…',
260
- 'browse.truncated': 'Too many folders to list; only the beginning is shown.',
261
- 'browse.showHidden': 'Show hidden files',
262
- 'browse.editPath': 'Edit path',
263
- 'browse.select': 'Use this folder',
264
- 'browse.enter': 'Open',
265
- 'browse.drives': 'Drives',
266
- 'browse.selectNamed': 'Use "{name}"',
241
+ 'add.guide.title': "Add workspace in-app",
242
+ 'add.guide': "This host has no desktop directory chooser. Use the official \"Add workspace\" entry on the New Session page.",
267
243
  'error.title': 'Something went wrong',
268
244
  'cancel': 'Cancel',
269
245
  'create': 'Create',
@@ -383,20 +359,8 @@ window.__ModuleLoader__.load({
383
359
  'sync.off': 'المزامنة غير متاحة هنا (تحتاج خدمة إعدادات المضيف)',
384
360
  'sync.loading': 'الاتصال بإعدادات المضيف…',
385
361
  'flow.creating': 'جارٍ الإنشاء…',
386
- 'browse.title': 'اختيار مجلد مساحة العمل',
387
- 'browse.home': 'المجلد الرئيسي',
388
- 'browse.up': 'المستوى الأعلى',
389
- 'browse.newFolder': 'مجلد جديد',
390
- 'browse.folderName': 'اسم المجلد',
391
- 'browse.empty': 'لا توجد مجلدات فرعية هنا',
392
- 'browse.loading': 'جارٍ التحميل…',
393
- 'browse.truncated': 'المجلدات كثيرة جدًا؛ يُعرض الجزء الأول فقط.',
394
- 'browse.showHidden': 'إظهار الملفات المخفية',
395
- 'browse.editPath': 'تعديل المسار',
396
- 'browse.select': 'استخدام هذا المجلد',
397
- 'browse.enter': 'فتح',
398
- 'browse.drives': 'الأقراص',
399
- 'browse.selectNamed': 'استخدام "{name}"',
362
+ 'add.guide.title': "إضافة مساحة عمل داخل التطبيق",
363
+ 'add.guide': "لا يوفر هذا المضيف منتقي مجلدات سطح المكتب. استخدم إدخال \"إضافة مساحة عمل\" الرسمي في صفحة الجلسة الجديدة.",
400
364
  'error.title': 'حدث خطأ ما',
401
365
  'cancel': 'إلغاء',
402
366
  'create': 'إنشاء',
@@ -508,20 +472,8 @@ window.__ModuleLoader__.load({
508
472
  'sync.off': 'Hier nicht verfügbar (erfordert den Host-Einstellungsdienst)',
509
473
  'sync.loading': 'Verbindung zum Host-Einstellungsspeicher…',
510
474
  'flow.creating': 'Wird erstellt…',
511
- 'browse.title': 'Arbeitsbereichsordner wählen',
512
- 'browse.home': 'Home',
513
- 'browse.up': 'Übergeordnet',
514
- 'browse.newFolder': 'Neuer Ordner',
515
- 'browse.folderName': 'Ordnername',
516
- 'browse.empty': 'Keine Unterordner vorhanden',
517
- 'browse.loading': 'Wird geladen…',
518
- 'browse.truncated': 'Zu viele Ordner; nur der Anfang wird angezeigt.',
519
- 'browse.showHidden': 'Versteckte Dateien anzeigen',
520
- 'browse.editPath': 'Pfad bearbeiten',
521
- 'browse.select': 'Diesen Ordner verwenden',
522
- 'browse.enter': 'Öffnen',
523
- 'browse.drives': 'Laufwerke',
524
- 'browse.selectNamed': '"{name}" verwenden',
475
+ 'add.guide.title': "Arbeitsbereich in der App hinzufügen",
476
+ 'add.guide': "Dieser Host bietet keinen Desktop-Ordnerdialog. Nutze den offiziellen Eintrag „Arbeitsbereich hinzufügen“ auf der Seite „Neue Sitzung“.",
525
477
  'error.title': 'Etwas ist schiefgelaufen',
526
478
  'cancel': 'Abbrechen',
527
479
  'create': 'Erstellen',
@@ -633,20 +585,8 @@ window.__ModuleLoader__.load({
633
585
  'sync.off': 'Synchronisation indisponible ici (service de réglages de l\'hôte requis)',
634
586
  'sync.loading': 'Connexion aux réglages de l\'hôte…',
635
587
  'flow.creating': 'Création…',
636
- 'browse.title': 'Choisir le dossier de l’espace de travail',
637
- 'browse.home': 'Dossier personnel',
638
- 'browse.up': 'Niveau supérieur',
639
- 'browse.newFolder': 'Nouveau dossier',
640
- 'browse.folderName': 'Nom du dossier',
641
- 'browse.empty': 'Aucun sous-dossier ici',
642
- 'browse.loading': 'Chargement…',
643
- 'browse.truncated': 'Trop de dossiers ; seul le début est affiché.',
644
- 'browse.showHidden': 'Afficher les fichiers cachés',
645
- 'browse.editPath': 'Modifier le chemin',
646
- 'browse.select': 'Utiliser ce dossier',
647
- 'browse.enter': 'Ouvrir',
648
- 'browse.drives': 'Lecteurs',
649
- 'browse.selectNamed': 'Utiliser « {name} »',
588
+ 'add.guide.title': "Ajouter un espace de travail dans l’application",
589
+ 'add.guide': "Cet hôte n’a pas de sélecteur de dossier natif. Utilisez l’entrée officielle « Ajouter un espace de travail » sur la page Nouvelle session.",
650
590
  'error.title': 'Une erreur est survenue',
651
591
  'cancel': 'Annuler',
652
592
  'create': 'Créer',
@@ -758,20 +698,8 @@ window.__ModuleLoader__.load({
758
698
  'sync.off': 'यहाँ समन्वयन उपलब्ध नहीं (होस्ट सेटिंग सेवा चाहिए)',
759
699
  'sync.loading': 'होस्ट सेटिंग से जुड़ रहे हैं…',
760
700
  'flow.creating': 'बनाया जा रहा है…',
761
- 'browse.title': 'कार्यस्थान फ़ोल्डर चुनें',
762
- 'browse.home': 'होम',
763
- 'browse.up': 'ऊपर',
764
- 'browse.newFolder': 'नया फ़ोल्डर',
765
- 'browse.folderName': 'फ़ोल्डर का नाम',
766
- 'browse.empty': 'यहाँ कोई उप-फ़ोल्डर नहीं',
767
- 'browse.loading': 'लोड हो रहा है…',
768
- 'browse.truncated': 'बहुत अधिक फ़ोल्डर; केवल शुरुआत दिखाई गई है।',
769
- 'browse.showHidden': 'छिपी फ़ाइलें दिखाएँ',
770
- 'browse.editPath': 'पथ संपादित करें',
771
- 'browse.select': 'यह फ़ोल्डर चुनें',
772
- 'browse.enter': 'खोलें',
773
- 'browse.drives': 'ड्राइव',
774
- 'browse.selectNamed': '"{name}" चुनें',
701
+ 'add.guide.title': "ऐप में कार्यस्थान जोड़ें",
702
+ 'add.guide': "इस होस्ट में डेस्कटॉप फ़ोल्डर चयनकर्ता नहीं है। नए सत्र पृष्ठ पर आधिकारिक \"कार्यस्थान जोड़ें\" प्रविष्टि का उपयोग करें।",
775
703
  'error.title': 'कुछ गड़बड़ हो गई',
776
704
  'cancel': 'रद्द करें',
777
705
  'create': 'बनाएँ',
@@ -883,20 +811,8 @@ window.__ModuleLoader__.load({
883
811
  'sync.off': 'Sinkronisasi tidak tersedia di sini (perlu layanan pengaturan host)',
884
812
  'sync.loading': 'Menyambung ke pengaturan host…',
885
813
  'flow.creating': 'Membuat…',
886
- 'browse.title': 'Pilih folder ruang kerja',
887
- 'browse.home': 'Beranda',
888
- 'browse.up': 'Naik',
889
- 'browse.newFolder': 'Folder baru',
890
- 'browse.folderName': 'Nama folder',
891
- 'browse.empty': 'Tidak ada subfolder di sini',
892
- 'browse.loading': 'Memuat…',
893
- 'browse.truncated': 'Terlalu banyak folder; hanya bagian awal yang ditampilkan.',
894
- 'browse.showHidden': 'Tampilkan file tersembunyi',
895
- 'browse.editPath': 'Edit jalur',
896
- 'browse.select': 'Gunakan folder ini',
897
- 'browse.enter': 'Buka',
898
- 'browse.drives': 'Drive',
899
- 'browse.selectNamed': 'Gunakan "{name}"',
814
+ 'add.guide.title': "Tambah ruang kerja di aplikasi",
815
+ 'add.guide': "Host ini tidak punya pemilih folder desktop. Gunakan entri resmi \"Tambah ruang kerja\" di halaman Sesi Baru.",
900
816
  'error.title': 'Terjadi kesalahan',
901
817
  'cancel': 'Batal',
902
818
  'create': 'Buat',
@@ -1008,20 +924,8 @@ window.__ModuleLoader__.load({
1008
924
  'sync.off': 'Sincronizzazione non disponibile qui (serve il servizio impostazioni dell\'host)',
1009
925
  'sync.loading': 'Connessione alle impostazioni dell\'host…',
1010
926
  'flow.creating': 'Creazione…',
1011
- 'browse.title': 'Scegli cartella dell’area di lavoro',
1012
- 'browse.home': 'Home',
1013
- 'browse.up': 'Livello superiore',
1014
- 'browse.newFolder': 'Nuova cartella',
1015
- 'browse.folderName': 'Nome cartella',
1016
- 'browse.empty': 'Nessuna sottocartella qui',
1017
- 'browse.loading': 'Caricamento…',
1018
- 'browse.truncated': 'Troppe cartelle; è mostrato solo l’inizio.',
1019
- 'browse.showHidden': 'Mostra file nascosti',
1020
- 'browse.editPath': 'Modifica percorso',
1021
- 'browse.select': 'Usa questa cartella',
1022
- 'browse.enter': 'Apri',
1023
- 'browse.drives': 'Unità',
1024
- 'browse.selectNamed': 'Usa "{name}"',
927
+ 'add.guide.title': "Aggiungi area di lavoro nell’app",
928
+ 'add.guide': "Questo host non ha un selettore di cartelle desktop. Usa la voce ufficiale «Aggiungi area di lavoro» nella pagina Nuova sessione.",
1025
929
  'error.title': 'Si è verificato un errore',
1026
930
  'cancel': 'Annulla',
1027
931
  'create': 'Crea',
@@ -1133,20 +1037,8 @@ window.__ModuleLoader__.load({
1133
1037
  'sync.off': 'この環境では同期できません(ホスト設定サービスが必要)',
1134
1038
  'sync.loading': 'ホスト設定に接続中…',
1135
1039
  'flow.creating': '作成中…',
1136
- 'browse.title': 'ワークスペースのフォルダーを選択',
1137
- 'browse.home': 'ホーム',
1138
- 'browse.up': '上の階層',
1139
- 'browse.newFolder': '新しいフォルダー',
1140
- 'browse.folderName': 'フォルダー名',
1141
- 'browse.empty': 'ここにサブフォルダーはありません',
1142
- 'browse.loading': '読み込み中…',
1143
- 'browse.truncated': 'フォルダーが多すぎます。先頭のみ表示しています。',
1144
- 'browse.showHidden': '隠しファイルを表示',
1145
- 'browse.editPath': 'パスを編集',
1146
- 'browse.select': 'このフォルダーを選択',
1147
- 'browse.enter': '開く',
1148
- 'browse.drives': 'ドライブ',
1149
- 'browse.selectNamed': '「{name}」を選択',
1040
+ 'add.guide.title': "アプリ内でワークスペースを追加",
1041
+ 'add.guide': "このホストにはデスクトップのフォルダー選択画面がありません。「新規セッション」ページの公式の「ワークスペースを追加」から開いてください。",
1150
1042
  'error.title': 'エラーが発生しました',
1151
1043
  'cancel': 'キャンセル',
1152
1044
  'create': '作成',
@@ -1258,20 +1150,8 @@ window.__ModuleLoader__.load({
1258
1150
  'sync.off': '이 환경에서는 동기화할 수 없습니다(호스트 설정 서비스 필요)',
1259
1151
  'sync.loading': '호스트 설정에 연결하는 중…',
1260
1152
  'flow.creating': '만드는 중…',
1261
- 'browse.title': '워크스페이스 폴더 선택',
1262
- 'browse.home': '홈',
1263
- 'browse.up': '상위',
1264
- 'browse.newFolder': '새 폴더',
1265
- 'browse.folderName': '폴더 이름',
1266
- 'browse.empty': '여기에 하위 폴더가 없습니다',
1267
- 'browse.loading': '불러오는 중…',
1268
- 'browse.truncated': '폴더가 너무 많습니다. 앞부분만 표시합니다.',
1269
- 'browse.showHidden': '숨김 파일 표시',
1270
- 'browse.editPath': '경로 편집',
1271
- 'browse.select': '이 폴더 선택',
1272
- 'browse.enter': '열기',
1273
- 'browse.drives': '드라이브',
1274
- 'browse.selectNamed': '"{name}" 선택',
1153
+ 'add.guide.title': "앱에서 워크스페이스 추가",
1154
+ 'add.guide': "이 호스트에는 데스크톱 폴더 선택기가 없습니다. 새 세션 페이지의 공식 \"워크스페이스 추가\" 항목을 사용하세요.",
1275
1155
  'error.title': '문제가 발생했습니다',
1276
1156
  'cancel': '취소',
1277
1157
  'create': '만들기',
@@ -1383,20 +1263,8 @@ window.__ModuleLoader__.load({
1383
1263
  'sync.off': 'Synchronisatie is hier niet beschikbaar (hostinstellingenservice nodig)',
1384
1264
  'sync.loading': 'Verbinden met hostinstellingen…',
1385
1265
  'flow.creating': 'Bezig met aanmaken…',
1386
- 'browse.title': 'Werkruimtemap kiezen',
1387
- 'browse.home': 'Home',
1388
- 'browse.up': 'Omhoog',
1389
- 'browse.newFolder': 'Nieuwe map',
1390
- 'browse.folderName': 'Mapnaam',
1391
- 'browse.empty': 'Geen submappen hier',
1392
- 'browse.loading': 'Laden…',
1393
- 'browse.truncated': 'Te veel mappen; alleen het begin wordt getoond.',
1394
- 'browse.showHidden': 'Verborgen bestanden tonen',
1395
- 'browse.editPath': 'Pad bewerken',
1396
- 'browse.select': 'Deze map gebruiken',
1397
- 'browse.enter': 'Openen',
1398
- 'browse.drives': 'Stations',
1399
- 'browse.selectNamed': '"{name}" gebruiken',
1266
+ 'add.guide.title': "Werkruimte in de app toevoegen",
1267
+ 'add.guide': "Deze host heeft geen systeemmapkiezer. Gebruik de officiële optie \"Werkruimte toevoegen\" op de pagina Nieuwe sessie.",
1400
1268
  'error.title': 'Er is iets misgegaan',
1401
1269
  'cancel': 'Annuleren',
1402
1270
  'create': 'Aanmaken',
@@ -1508,20 +1376,8 @@ window.__ModuleLoader__.load({
1508
1376
  'sync.off': 'Synchronizacja niedostępna (wymaga usługi ustawień hosta)',
1509
1377
  'sync.loading': 'Łączenie z ustawieniami hosta…',
1510
1378
  'flow.creating': 'Tworzenie…',
1511
- 'browse.title': 'Wybierz folder obszaru roboczego',
1512
- 'browse.home': 'Katalog domowy',
1513
- 'browse.up': 'W górę',
1514
- 'browse.newFolder': 'Nowy folder',
1515
- 'browse.folderName': 'Nazwa folderu',
1516
- 'browse.empty': 'Brak podfolderów',
1517
- 'browse.loading': 'Wczytywanie…',
1518
- 'browse.truncated': 'Zbyt wiele folderów; pokazano tylko początek.',
1519
- 'browse.showHidden': 'Pokaż ukryte pliki',
1520
- 'browse.editPath': 'Edytuj ścieżkę',
1521
- 'browse.select': 'Użyj tego folderu',
1522
- 'browse.enter': 'Otwórz',
1523
- 'browse.drives': 'Dyski',
1524
- 'browse.selectNamed': 'Użyj "{name}"',
1379
+ 'add.guide.title': "Dodaj obszar roboczy w aplikacji",
1380
+ 'add.guide': "Ten host nie ma systemowego wyboru folderów. Użyj oficjalnej pozycji „Dodaj obszar roboczy” na stronie Nowa sesja.",
1525
1381
  'error.title': 'Coś poszło nie tak',
1526
1382
  'cancel': 'Anuluj',
1527
1383
  'create': 'Utwórz',
@@ -1633,20 +1489,8 @@ window.__ModuleLoader__.load({
1633
1489
  'sync.off': 'Sincronização indisponível aqui (requer o serviço de configurações do host)',
1634
1490
  'sync.loading': 'Conectando às configurações do host…',
1635
1491
  'flow.creating': 'Criando…',
1636
- 'browse.title': 'Escolher pasta da área de trabalho',
1637
- 'browse.home': 'Início',
1638
- 'browse.up': 'Acima',
1639
- 'browse.newFolder': 'Nova pasta',
1640
- 'browse.folderName': 'Nome da pasta',
1641
- 'browse.empty': 'Nenhuma subpasta aqui',
1642
- 'browse.loading': 'Carregando…',
1643
- 'browse.truncated': 'Pastas demais; apenas o início é exibido.',
1644
- 'browse.showHidden': 'Mostrar arquivos ocultos',
1645
- 'browse.editPath': 'Editar caminho',
1646
- 'browse.select': 'Usar esta pasta',
1647
- 'browse.enter': 'Abrir',
1648
- 'browse.drives': 'Unidades',
1649
- 'browse.selectNamed': 'Usar "{name}"',
1492
+ 'add.guide.title': "Adicionar área de trabalho no app",
1493
+ 'add.guide': "Este host não tem seletor de pastas do sistema. Use a entrada oficial \"Adicionar área de trabalho\" na página Nova sessão.",
1650
1494
  'error.title': 'Algo deu errado',
1651
1495
  'cancel': 'Cancelar',
1652
1496
  'create': 'Criar',
@@ -1758,20 +1602,8 @@ window.__ModuleLoader__.load({
1758
1602
  'sync.off': 'Синхронизация недоступна (нужна служба настроек хоста)',
1759
1603
  'sync.loading': 'Подключение к настройкам хоста…',
1760
1604
  'flow.creating': 'Создание…',
1761
- 'browse.title': 'Выбор папки рабочей области',
1762
- 'browse.home': 'Домашняя папка',
1763
- 'browse.up': 'На уровень выше',
1764
- 'browse.newFolder': 'Новая папка',
1765
- 'browse.folderName': 'Имя папки',
1766
- 'browse.empty': 'Здесь нет вложенных папок',
1767
- 'browse.loading': 'Загрузка…',
1768
- 'browse.truncated': 'Слишком много папок; показано только начало.',
1769
- 'browse.showHidden': 'Показывать скрытые файлы',
1770
- 'browse.editPath': 'Изменить путь',
1771
- 'browse.select': 'Выбрать эту папку',
1772
- 'browse.enter': 'Открыть',
1773
- 'browse.drives': 'Диски',
1774
- 'browse.selectNamed': 'Выбрать «{name}»',
1605
+ 'add.guide.title': "Добавить рабочую область в приложении",
1606
+ 'add.guide': "На этом хосте нет системного выбора папок. Используйте официальный пункт «Добавить рабочую область» на странице «Новый сеанс».",
1775
1607
  'error.title': 'Что-то пошло не так',
1776
1608
  'cancel': 'Отмена',
1777
1609
  'create': 'Создать',
@@ -1883,20 +1715,8 @@ window.__ModuleLoader__.load({
1883
1715
  'sync.off': 'Synk är inte tillgänglig här (kräver värduppläggets inställningstjänst)',
1884
1716
  'sync.loading': 'Ansluter till värduppläggets inställningar…',
1885
1717
  'flow.creating': 'Skapar…',
1886
- 'browse.title': 'Välj arbetsyte-mapp',
1887
- 'browse.home': 'Hem',
1888
- 'browse.up': 'Upp',
1889
- 'browse.newFolder': 'Ny mapp',
1890
- 'browse.folderName': 'Mappnamn',
1891
- 'browse.empty': 'Inga undermappar här',
1892
- 'browse.loading': 'Laddar…',
1893
- 'browse.truncated': 'För många mappar; endast början visas.',
1894
- 'browse.showHidden': 'Visa dolda filer',
1895
- 'browse.editPath': 'Redigera sökväg',
1896
- 'browse.select': 'Använd den här mappen',
1897
- 'browse.enter': 'Öppna',
1898
- 'browse.drives': 'Enheter',
1899
- 'browse.selectNamed': 'Använd "{name}"',
1718
+ 'add.guide.title': "Lägg till arbetsyta i appen",
1719
+ 'add.guide': "Den här värden har ingen systemmappväljare. Använd den officiella posten \"Lägg till arbetsyta\" på sidan Ny session.",
1900
1720
  'error.title': 'Något gick fel',
1901
1721
  'cancel': 'Avbryt',
1902
1722
  'create': 'Skapa',
@@ -2008,20 +1828,8 @@ window.__ModuleLoader__.load({
2008
1828
  'sync.off': 'สภาพแวดล้อมนี้ไม่รองรับการซิงก์ (ต้องมีบริการการตั้งค่าของโฮสต์)',
2009
1829
  'sync.loading': 'กำลังเชื่อมต่อการตั้งค่าโฮสต์…',
2010
1830
  'flow.creating': 'กำลังสร้าง…',
2011
- 'browse.title': 'เลือกโฟลเดอร์เวิร์กสเปซ',
2012
- 'browse.home': 'โฮม',
2013
- 'browse.up': 'ขึ้นหนึ่งระดับ',
2014
- 'browse.newFolder': 'โฟลเดอร์ใหม่',
2015
- 'browse.folderName': 'ชื่อโฟลเดอร์',
2016
- 'browse.empty': 'ไม่มีโฟลเดอร์ย่อยที่นี่',
2017
- 'browse.loading': 'กำลังโหลด…',
2018
- 'browse.truncated': 'มีโฟลเดอร์มากเกินไป แสดงเฉพาะส่วนต้น',
2019
- 'browse.showHidden': 'แสดงไฟล์ที่ซ่อนอยู่',
2020
- 'browse.editPath': 'แก้ไขเส้นทาง',
2021
- 'browse.select': 'ใช้โฟลเดอร์นี้',
2022
- 'browse.enter': 'เปิด',
2023
- 'browse.drives': 'ไดรฟ์',
2024
- 'browse.selectNamed': 'ใช้ "{name}"',
1831
+ 'add.guide.title': "เพิ่มเวิร์กสเปซในแอป",
1832
+ 'add.guide': "โฮสต์นี้ไม่มีตัวเลือกโฟลเดอร์ของระบบ ใช้รายการ \"เพิ่มเวิร์กสเปซ\" อย่างเป็นทางการในหน้าเซสชันใหม่",
2025
1833
  'error.title': 'เกิดข้อผิดพลาด',
2026
1834
  'cancel': 'ยกเลิก',
2027
1835
  'create': 'สร้าง',
@@ -2133,20 +1941,8 @@ window.__ModuleLoader__.load({
2133
1941
  'sync.off': 'Bu ortamda eşitleme yok (ana makine ayar servisi gerekir)',
2134
1942
  'sync.loading': 'Ana makine ayarlarına bağlanılıyor…',
2135
1943
  'flow.creating': 'Oluşturuluyor…',
2136
- 'browse.title': 'Çalışma alanı klasörünü seç',
2137
- 'browse.home': 'Ana dizin',
2138
- 'browse.up': 'Üst düzey',
2139
- 'browse.newFolder': 'Yeni klasör',
2140
- 'browse.folderName': 'Klasör adı',
2141
- 'browse.empty': 'Burada alt klasör yok',
2142
- 'browse.loading': 'Yükleniyor…',
2143
- 'browse.truncated': 'Çok fazla klasör var; yalnızca başlangıç gösteriliyor.',
2144
- 'browse.showHidden': 'Gizli dosyaları göster',
2145
- 'browse.editPath': 'Yolu düzenle',
2146
- 'browse.select': 'Bu klasörü kullan',
2147
- 'browse.enter': 'Aç',
2148
- 'browse.drives': 'Sürücüler',
2149
- 'browse.selectNamed': '"{name}" kullan',
1944
+ 'add.guide.title': "Uygulama içinde çalışma alanı ekle",
1945
+ 'add.guide': "Bu ana makinede masaüstü klasör seçici yok. Yeni Oturum sayfasındaki resmî \"Çalışma alanı ekle\" girişini kullanın.",
2150
1946
  'error.title': 'Bir şeyler ters gitti',
2151
1947
  'cancel': 'İptal',
2152
1948
  'create': 'Oluştur',
@@ -2258,20 +2054,8 @@ window.__ModuleLoader__.load({
2258
2054
  '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ủ)',
2259
2055
  'sync.loading': 'Đang kết nối cài đặt máy chủ…',
2260
2056
  'flow.creating': 'Đang tạo…',
2261
- 'browse.title': 'Chọn thư mục không gian làm việc',
2262
- 'browse.home': 'Thư mục chính',
2263
- 'browse.up': 'Lên một cấp',
2264
- 'browse.newFolder': 'Thư mục mới',
2265
- 'browse.folderName': 'Tên thư mục',
2266
- 'browse.empty': 'Không có thư mục con ở đây',
2267
- 'browse.loading': 'Đang tải…',
2268
- 'browse.truncated': 'Quá nhiều thư mục; chỉ hiển thị phần đầu.',
2269
- 'browse.showHidden': 'Hiện tệp ẩn',
2270
- 'browse.editPath': 'Sửa đường dẫn',
2271
- 'browse.select': 'Dùng thư mục này',
2272
- 'browse.enter': 'Mở',
2273
- 'browse.drives': 'Ổ đĩa',
2274
- 'browse.selectNamed': 'Dùng "{name}"',
2057
+ 'add.guide.title': "Thêm không gian làm việc trong ứng dụng",
2058
+ 'add.guide': "Máy chủ này không có trình chọn thư mục trên màn hình. Dùng mục chính thức \"Thêm không gian làm việc\" ở trang Phiên mới.",
2275
2059
  'error.title': 'Đã xảy ra lỗi',
2276
2060
  'cancel': 'Hủy',
2277
2061
  'create': 'Tạo',
@@ -2383,20 +2167,8 @@ window.__ModuleLoader__.load({
2383
2167
  'sync.off': '呢個環境唔支援同步(需要宿主設定服務)',
2384
2168
  'sync.loading': '連接緊宿主設定…',
2385
2169
  'flow.creating': '建立緊…',
2386
- 'browse.title': '揀工作區資料夾',
2387
- 'browse.home': '主目錄',
2388
- 'browse.up': '上一層',
2389
- 'browse.newFolder': '新增資料夾',
2390
- 'browse.folderName': '資料夾名稱',
2391
- 'browse.empty': '呢個資料夾冇子資料夾',
2392
- 'browse.loading': '載入緊…',
2393
- 'browse.truncated': '資料夾太多,只顯示開頭部分。',
2394
- 'browse.showHidden': '顯示隱藏檔案',
2395
- 'browse.editPath': '編輯路徑',
2396
- 'browse.select': '揀呢個資料夾',
2397
- 'browse.enter': '入去',
2398
- 'browse.drives': '磁碟',
2399
- 'browse.selectNamed': '揀「{name}」',
2170
+ 'add.guide.title': "應用程式內新增工作區",
2171
+ 'add.guide': "呢部主機冇桌面資料夾選擇器。請去「新工作階段」頁,用官方嘅「新增工作區」開應用程式內瀏覽器。",
2400
2172
  'error.title': '出咗錯',
2401
2173
  'cancel': '取消',
2402
2174
  'create': '建立',
@@ -2508,20 +2280,8 @@ window.__ModuleLoader__.load({
2508
2280
  'sync.off': '呢個環境唔支援同步(需要宿主設定服務)',
2509
2281
  'sync.loading': '連接緊宿主設定…',
2510
2282
  'flow.creating': '建立緊…',
2511
- 'browse.title': '揀工作區資料夾',
2512
- 'browse.home': '主目錄',
2513
- 'browse.up': '上一層',
2514
- 'browse.newFolder': '新增資料夾',
2515
- 'browse.folderName': '資料夾名稱',
2516
- 'browse.empty': '呢個資料夾冇子資料夾',
2517
- 'browse.loading': '載入緊…',
2518
- 'browse.truncated': '資料夾太多,只顯示開頭部分。',
2519
- 'browse.showHidden': '顯示隱藏檔案',
2520
- 'browse.editPath': '編輯路徑',
2521
- 'browse.select': '揀呢個資料夾',
2522
- 'browse.enter': '入去',
2523
- 'browse.drives': '磁碟',
2524
- 'browse.selectNamed': '揀「{name}」',
2283
+ 'add.guide.title': "應用程式內新增工作區",
2284
+ 'add.guide': "呢部主機冇桌面資料夾選擇器。請去「新工作階段」頁,用官方嘅「新增工作區」開應用程式內瀏覽器。",
2525
2285
  'error.title': '出咗錯',
2526
2286
  'cancel': '取消',
2527
2287
  'create': '建立',
@@ -2633,20 +2393,8 @@ window.__ModuleLoader__.load({
2633
2393
  'sync.off': '目前環境不支援同步(需要宿主設定服務)',
2634
2394
  'sync.loading': '正在連線宿主設定…',
2635
2395
  'flow.creating': '正在建立…',
2636
- 'browse.title': '選擇工作區資料夾',
2637
- 'browse.home': '主目錄',
2638
- 'browse.up': '上一層',
2639
- 'browse.newFolder': '新增資料夾',
2640
- 'browse.folderName': '資料夾名稱',
2641
- 'browse.empty': '此資料夾沒有子資料夾',
2642
- 'browse.loading': '載入中…',
2643
- 'browse.truncated': '資料夾過多,僅顯示開頭部分。',
2644
- 'browse.showHidden': '顯示隱藏檔案',
2645
- 'browse.editPath': '編輯路徑',
2646
- 'browse.select': '選擇此資料夾',
2647
- 'browse.enter': '進入',
2648
- 'browse.drives': '磁碟',
2649
- 'browse.selectNamed': '選擇「{name}」',
2396
+ 'add.guide.title': "應用程式內新增工作區",
2397
+ 'add.guide': "此主機沒有桌面資料夾選擇器。請至「新工作階段」頁,用官方的「新增工作區」開啟應用程式內瀏覽器。",
2650
2398
  'error.title': '發生錯誤',
2651
2399
  'cancel': '取消',
2652
2400
  'create': '建立',
@@ -3272,39 +3020,6 @@ window.__ModuleLoader__.load({
3272
3020
  '.bw-btn-primary{background:var(--dsw-alias-brand-primary,#5b8def);border-color:transparent;color:var(--dsw-alias-brand-text,#fff)}',
3273
3021
  '.bw-btn-primary:hover{background:var(--dsw-alias-button-primary-hover,var(--dsw-alias-brand-primary,#5b8def))}',
3274
3022
  '.bw-btn:disabled{opacity:.5;cursor:default}',
3275
- '.bw-browse-body{min-width:360px;max-width:460px}',
3276
- '.bw-browse-bar{display:flex;align-items:center;gap:4px}',
3277
- '.bw-browse-crumbs{display:flex;align-items:center;gap:2px;flex:1;min-width:0;overflow-x:auto}',
3278
- '.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}',
3279
- '.bw-browse-crumb:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,#e6e6e6)}',
3280
- '.bw-browse-crumb:disabled{opacity:.4;cursor:default}',
3281
- '.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}',
3282
- '.bw-browse-nav{font-size:13px;line-height:1}',
3283
- '.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}',
3284
- '.bw-browse-edit:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,#e6e6e6)}',
3285
- '.bw-browse-edit svg{width:14px;height:14px}',
3286
- '.bw-browse-edit-row{display:flex;gap:6px;align-items:center}',
3287
- '.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))}',
3288
- '.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}',
3289
- '.bw-browse-row:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}',
3290
- '.bw-browse-row:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5b8def);outline-offset:-2px}',
3291
- '.bw-browse-row svg{width:15px;height:15px;flex:none;color:var(--dsw-alias-label-secondary,#b8b8b8)}',
3292
- '.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)}',
3293
- '.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}',
3294
- '.bw-browse-open:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.16));color:var(--dsw-alias-label-primary,#e6e6e6)}',
3295
- '.bw-browse-drives{display:flex;align-items:center;gap:4px;margin-left:auto}',
3296
- '.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}',
3297
- '.bw-browse-drive:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,#e6e6e6)}',
3298
- '.bw-browse-drive-on{border-color:var(--dsw-alias-brand-primary,#5b8def);color:var(--dsw-alias-label-primary,#e6e6e6)}',
3299
- '.bw-browse-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
3300
- '.bw-browse-note{font-size:12px;color:var(--dsw-alias-label-tertiary,#9a9a9a);padding:10px 8px;text-align:center}',
3301
- '.bw-browse-new-row{display:flex;gap:6px;align-items:center}',
3302
- '.bw-browse-tools{display:flex;align-items:center;gap:6px;flex-wrap:wrap}',
3303
- '.bw-browse-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px}',
3304
- '.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}',
3305
- '.bw-browse-tool:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,#e6e6e6)}',
3306
- '.bw-browse-tool:disabled{opacity:.45;cursor:default}',
3307
- '.bw-browse-tool-on{border-color:var(--dsw-alias-brand-primary,#5b8def);color:var(--dsw-alias-label-primary,#e6e6e6)}',
3308
3023
  '.bw-error-text{font-size:12.5px;color:var(--dsw-alias-state-error-primary,#f85149);word-break:break-all;max-width:380px}',
3309
3024
  '.bw-dialog-input-row{display:flex;gap:6px;align-items:center}',
3310
3025
  '.bw-glow-row{display:flex;align-items:center;gap:10px}',
@@ -3558,10 +3273,17 @@ window.__ModuleLoader__.load({
3558
3273
  // headless) gets `browse`, whose wire verbs are list/createDirectory ONLY
3559
3274
  // and whose `pick` is refused BY DESIGN with `directory-picker/unavailable`
3560
3275
  // ("... needs the native capability; the composed picker serves browse").
3561
- // A flow that hard-assumed the OS chooser therefore dies on every LAN bind
3562
- // and no workspace can be added at all. Probe the composed backend once per
3563
- // page and keep BOTH interactions: the OS chooser, and an in-app browser
3564
- // driven by the browse primitives (see DirectoryBrowseDialog).
3276
+ // v0.16.0: the sidebar no longer draws its own in-app browser. The browse
3277
+ // backend SHIPS one (dsh-client-ui-directory-picker-browse, mounted as a
3278
+ // host+surface pair, its client half registering into both directory-flow
3279
+ // holes) — but it can only be rendered by the entry that DECLARED the
3280
+ // child hole (the shipped WorkspaceBrowser), and a child declaration is
3281
+ // exclusive, so shadowing that seat forfeits the authorization for good
3282
+ // (ui-renderer: "not declared by this entry's children"). A native Host
3283
+ // still gets the official OS chooser; a browse Host is pointed at the
3284
+ // official in-app flow in the conversation empty state instead of a
3285
+ // second-hand imitation. The probe stays: it decides which of the two
3286
+ // this page gets, and a refused pick still classifies the backend.
3565
3287
  const pickerState = { kind: 'unknown', probe: null, api: null }
3566
3288
  /**
3567
3289
  * True for the refusal a Host serving the OTHER capability throws: the wire
@@ -3602,11 +3324,6 @@ window.__ModuleLoader__.load({
3602
3324
  })
3603
3325
  return pickerState.probe
3604
3326
  }
3605
- /** Commit a browse verdict the flow discovered the hard way (a refused pick). */
3606
- const markPickerBrowse = () => {
3607
- pickerState.kind = 'browse'
3608
- pickerState.probe = Promise.resolve('browse')
3609
- }
3610
3327
 
3611
3328
  /* ============================ flow dialog ========================= */
3612
3329
 
@@ -3805,404 +3522,7 @@ window.__ModuleLoader__.load({
3805
3522
  }
3806
3523
  }
3807
3524
 
3808
- /**
3809
- * The in-app directory browser: the interaction a `browse`-composed Host can
3810
- * actually serve. It appears when the probe (or a refused pick) proves the
3811
- * OS chooser is not this Host's backend — a LAN/all-interfaces bind, an SSH
3812
- * launch, a headless host — where `pick` is refused by design and
3813
- * list/createDirectory are the only wire verbs. One level at a time: a
3814
- * clickable Home-rooted breadcrumb chain, an editable path, an inline
3815
- * new-folder row and the hidden-entry toggle; the listed level (or any
3816
- * ancestor crumb) is the pick. Browse failures stay inside the dialog — the
3817
- * owner's error surface belongs to the pick/create conversation, and a
3818
- * denied listing must never close the flow.
3819
- */
3820
- /**
3821
- * Page-lifetime cache of the Windows drive letters this Host actually
3822
- * serves. The browse API can only list a path, so the first open probes the
3823
- * usual letters once (six cheap listings, a missing drive fails silently)
3824
- * and every later open renders the chips straight from here.
3825
- */
3826
- const browseDrives = { probed: false, probing: false, list: [] }
3827
-
3828
- /**
3829
- * Run the one wire verb that actually puts a folder on disk and hand back
3830
- * the created path. This is deliberately a module-level seam the smoke suite
3831
- * drives for real: v0.11.4 folded the call into the success handler and
3832
- * dropped it, so the form closed on a resolved `undefined` — no request, no
3833
- * error, no folder. A string-level assertion cannot see that; calling it can.
3834
- */
3835
- const createFolderIn = (createDirectory, path, name) => Promise.resolve()
3836
- .then(() => createDirectory(path, name))
3837
-
3838
- function DirectoryBrowseDialog(props) {
3839
- const { open, busy, listDirectory, createDirectory, onPick, onClose, t } = props
3840
- const [listing, setListing] = React.useState(null)
3841
- const [loading, setLoading] = React.useState(false)
3842
- const [error, setError] = React.useState('')
3843
- const [showHidden, setShowHidden] = React.useState(false)
3844
- const [editing, setEditing] = React.useState(false)
3845
- const [draft, setDraft] = React.useState('')
3846
- const [creating, setCreating] = React.useState(false)
3847
- const [createName, setCreateName] = React.useState('')
3848
- const [createBusy, setCreateBusy] = React.useState(false)
3849
- const [createError, setCreateError] = React.useState('')
3850
- const [selected, setSelected] = React.useState('')
3851
- const [drives, setDrives] = React.useState(browseDrives.list)
3852
- // Guards a superseded scan: a slow level must not overwrite a newer one.
3853
- const seqRef = React.useRef(0)
3854
- const crumbsRef = React.useRef(null)
3855
-
3856
- const go = (path, keepSelected) => {
3857
- const seq = seqRef.current + 1
3858
- seqRef.current = seq
3859
- setLoading(true)
3860
- setError('')
3861
- if (keepSelected !== true) setSelected('')
3862
- Promise.resolve()
3863
- .then(() => listDirectory(path, undefined))
3864
- .then((next) => {
3865
- if (seqRef.current !== seq) return
3866
- setListing(next || null)
3867
- setLoading(false)
3868
- })
3869
- .catch((reason) => {
3870
- if (seqRef.current !== seq) return
3871
- setLoading(false)
3872
- setError(messageOf(reason))
3873
- })
3874
- }
3875
-
3876
- React.useEffect(() => {
3877
- if (!open) return undefined
3878
- seqRef.current += 1
3879
- setListing(null)
3880
- setError('')
3881
- setEditing(false)
3882
- setCreating(false)
3883
- setCreateName('')
3884
- setCreateBusy(false)
3885
- setCreateError('')
3886
- setShowHidden(false)
3887
- setSelected('')
3888
- go(undefined)
3889
- return () => { seqRef.current += 1 }
3890
- }, [open])
3891
-
3892
- const currentPath = listing && typeof listing.path === 'string' ? listing.path : ''
3893
- const homePath = listing && typeof listing.home === 'string' ? listing.home : ''
3894
- const windowsHost = homePath.indexOf('\\') !== -1
3895
- const hasListing = listing !== null
3896
-
3897
- // Windows drives are separate filesystem roots and the browse API can only
3898
- // list a path, so probe the usual letters once per page (cached above) and
3899
- // offer the ones that answered. A drive nobody probed is still reachable
3900
- // through the path editor, which is the only general answer anyway.
3901
- React.useEffect(() => {
3902
- if (!open || !hasListing || !windowsHost) return undefined
3903
- if (browseDrives.probed || browseDrives.probing) {
3904
- if (browseDrives.list.length > 0) setDrives(browseDrives.list)
3905
- return undefined
3906
- }
3907
- browseDrives.probing = true
3908
- let alive = true
3909
- const letters = ['C:', 'D:', 'E:', 'F:', 'G:', 'H:']
3910
- Promise.all(letters.map((letter) => Promise.resolve()
3911
- .then(() => listDirectory(letter + '\\', undefined))
3912
- .then(() => letter, () => null)))
3913
- .then((found) => {
3914
- browseDrives.list = found.filter(Boolean)
3915
- browseDrives.probed = true
3916
- browseDrives.probing = false
3917
- if (alive) setDrives(browseDrives.list)
3918
- }, () => { browseDrives.probing = false })
3919
- return () => { alive = false }
3920
- }, [open, hasListing, windowsHost])
3921
-
3922
- // A deep path must show its TAIL — that is where you are — so the chain
3923
- // keeps its right edge in view instead of cutting the last crumb off.
3924
- React.useEffect(() => {
3925
- const node = crumbsRef.current
3926
- if (node) node.scrollLeft = node.scrollWidth
3927
- }, [listing])
3928
-
3929
- if (!open) return null
3930
-
3931
- // The whole ancestry from the filesystem ROOT: rooting the chain at Home
3932
- // (the official dialog's choice) hides where the listed level actually is,
3933
- // and the drive crumb answers "which disk am I on" at a glance.
3934
- const crumbs = listing && Array.isArray(listing.crumbs) ? listing.crumbs : []
3935
- const parentPath = crumbs.length > 1 ? crumbs[crumbs.length - 2].path : ''
3936
- const entries = (listing && Array.isArray(listing.entries) ? listing.entries : [])
3937
- .filter((entry) => showHidden || !entry.hidden)
3938
- const selectedEntry = entries.find((entry) => entry.path === selected) || null
3939
- const targetPath = selectedEntry ? selectedEntry.path : currentPath
3940
-
3941
- const submitDraft = () => {
3942
- const path = draft.trim()
3943
- setEditing(false)
3944
- if (path === '') { go(undefined); return }
3945
- go(path)
3946
- }
3947
- const submitCreate = () => {
3948
- const name = createName.trim()
3949
- if (name === '' || createBusy || currentPath === '') return
3950
- setCreateBusy(true)
3951
- setCreateError('')
3952
- createFolderIn(createDirectory, currentPath, name)
3953
- .then((created) => {
3954
- setCreateBusy(false)
3955
- setCreating(false)
3956
- setCreateName('')
3957
- // Land on the folder just made: it is almost always the one the
3958
- // operator wants to adopt next.
3959
- if (typeof created === 'string' && created !== '') setSelected(created)
3960
- go(currentPath, true)
3961
- })
3962
- .catch((reason) => {
3963
- setCreateBusy(false)
3964
- setCreateError(messageOf(reason))
3965
- })
3966
- }
3967
-
3968
- const head = editing
3969
- ? E('div', { className: 'bw-browse-edit-row' },
3970
- E('input', {
3971
- className: 'bw-input',
3972
- value: draft,
3973
- autoFocus: true,
3974
- spellCheck: false,
3975
- placeholder: currentPath,
3976
- onChange: (e) => setDraft(e.target.value),
3977
- onKeyDown: (e) => {
3978
- if (e.key === 'Enter') submitDraft()
3979
- else if (e.key === 'Escape') setEditing(false)
3980
- },
3981
- }),
3982
- E(BTN, { variant: 'outline', onClick: () => setEditing(false) }, t('cancel')),
3983
- )
3984
- : E('div', { className: 'bw-browse-bar' },
3985
- E('button', {
3986
- type: 'button',
3987
- className: 'bw-browse-crumb bw-browse-nav',
3988
- title: t('browse.up'),
3989
- 'aria-label': t('browse.up'),
3990
- disabled: parentPath === '',
3991
- onClick: () => { if (parentPath !== '') go(parentPath) },
3992
- }, '↑'),
3993
- E('div', { className: 'bw-browse-crumbs', ref: crumbsRef },
3994
- crumbs.map((crumb, index) => (index === crumbs.length - 1
3995
- ? E('span', { key: crumb.path, className: 'bw-browse-crumb bw-browse-current', title: crumb.path }, crumb.name)
3996
- : E('button', {
3997
- key: crumb.path,
3998
- type: 'button',
3999
- className: 'bw-browse-crumb',
4000
- title: crumb.path,
4001
- onClick: () => go(crumb.path),
4002
- }, crumb.name))),
4003
- ),
4004
- E('button', {
4005
- type: 'button',
4006
- className: 'bw-browse-edit',
4007
- title: t('browse.editPath'),
4008
- 'aria-label': t('browse.editPath'),
4009
- onClick: () => { setDraft(currentPath); setEditing(true) },
4010
- }, icon('IconEditOutline16')),
4011
- )
4012
-
4013
- // One click SELECTS a row (the footer button then adopts it — no need to
4014
- // step inside just to pick a folder), a double click or the row's chevron
4015
- // enters it. Touch needs the chevron: a double tap is not a gesture.
4016
- const rows = entries.map((entry) => E('div', {
4017
- key: entry.path,
4018
- className: cls('bw-browse-row', selected === entry.path && 'bw-browse-row-on'),
4019
- title: entry.path,
4020
- role: 'button',
4021
- tabIndex: 0,
4022
- onClick: () => setSelected((current) => (current === entry.path ? '' : entry.path)),
4023
- onDoubleClick: () => go(entry.path),
4024
- onKeyDown: (event) => {
4025
- if (event.key === 'Enter') go(entry.path)
4026
- else if (event.key === ' ') { event.preventDefault(); setSelected((current) => (current === entry.path ? '' : entry.path)) }
4027
- },
4028
- },
4029
- icon('IconFolderClose16'),
4030
- E('span', { className: 'bw-browse-name' }, entry.name),
4031
- E('button', {
4032
- type: 'button',
4033
- className: 'bw-browse-open',
4034
- title: t('browse.enter'),
4035
- 'aria-label': t('browse.enter'),
4036
- onClick: (event) => { event.stopPropagation(); go(entry.path) },
4037
- }, '›'),
4038
- ))
4039
-
4040
- const body = loading
4041
- ? E('div', { className: 'bw-browse-note' }, t('browse.loading'))
4042
- : rows.length > 0
4043
- ? rows
4044
- : E('div', { className: 'bw-browse-note' }, t('browse.empty'))
4045
-
4046
- const newRow = creating
4047
- ? E('div', { className: 'bw-browse-new-row' },
4048
- E('input', {
4049
- className: 'bw-input',
4050
- value: createName,
4051
- autoFocus: true,
4052
- spellCheck: false,
4053
- placeholder: t('browse.folderName'),
4054
- disabled: createBusy,
4055
- onChange: (e) => setCreateName(e.target.value),
4056
- onKeyDown: (e) => {
4057
- if (e.key === 'Enter') submitCreate()
4058
- else if (e.key === 'Escape') { setCreating(false); setCreateError('') }
4059
- },
4060
- }),
4061
- E(BTN, {
4062
- variant: 'outline',
4063
- disabled: createBusy,
4064
- onClick: () => { setCreating(false); setCreateError('') },
4065
- }, t('cancel')),
4066
- E(BTN, { variant: 'primary', onClick: submitCreate, disabled: createBusy },
4067
- createBusy ? t('flow.creating') : t('create')),
4068
- )
4069
- : null
4070
-
4071
- // The dialog is deliberately narrower than the official browser, so the
4072
- // two browsing tools sit on their own row above the list instead of
4073
- // sharing the footer with the commit pair (they used to wrap there).
4074
- const tools = E('div', { className: 'bw-browse-tools' },
4075
- E('button', {
4076
- type: 'button',
4077
- className: cls('bw-browse-tool', currentPath !== '' && currentPath === homePath && 'bw-browse-tool-on'),
4078
- disabled: homePath === '' || currentPath === homePath,
4079
- onClick: () => { if (homePath !== '') go(homePath) },
4080
- }, t('browse.home')),
4081
- E('button', {
4082
- type: 'button',
4083
- className: cls('bw-browse-tool', showHidden && 'bw-browse-tool-on'),
4084
- 'aria-pressed': showHidden ? 'true' : 'false',
4085
- onClick: () => setShowHidden((value) => !value),
4086
- }, t('browse.showHidden')),
4087
- E('button', {
4088
- type: 'button',
4089
- className: 'bw-browse-tool',
4090
- disabled: creating || currentPath === '',
4091
- onClick: () => { setCreating(true); setCreateName(''); setCreateError('') },
4092
- }, t('browse.newFolder')),
4093
- drives.length > 1
4094
- ? E('div', {
4095
- className: 'bw-browse-drives',
4096
- title: t('browse.drives'),
4097
- role: 'group',
4098
- 'aria-label': t('browse.drives'),
4099
- }, drives.map((drive) => E('button', {
4100
- key: drive,
4101
- type: 'button',
4102
- className: cls('bw-browse-drive', currentPath.slice(0, 2).toUpperCase() === drive && 'bw-browse-drive-on'),
4103
- title: drive + '\\',
4104
- onClick: () => go(drive + '\\'),
4105
- }, drive)))
4106
- : null,
4107
- )
4108
-
4109
- const footer = E('div', { className: 'bw-browse-actions' },
4110
- E(BTN, { variant: 'outline', onClick: onClose, disabled: busy === true }, t('cancel')),
4111
- E(BTN, {
4112
- variant: 'primary',
4113
- disabled: busy === true || targetPath === '',
4114
- onClick: () => { if (targetPath !== '') onPick(targetPath) },
4115
- }, selectedEntry !== null ? t('browse.selectNamed', { name: selectedEntry.name }) : t('browse.select')),
4116
- )
4117
-
4118
- return E(ui.Modal, {
4119
- open: true,
4120
- onClose: () => { if (busy !== true) onClose() },
4121
- closeLabel: t('close'),
4122
- title: t('browse.title'),
4123
- footer,
4124
- },
4125
- E('div', { className: 'bw-modal-body bw-browse-body' },
4126
- head,
4127
- createError !== '' ? E('div', { className: 'bw-error-text', role: 'alert' }, createError) : null,
4128
- tools,
4129
- newRow,
4130
- E('div', { className: 'bw-browse-list', role: 'list' }, body),
4131
- listing && listing.truncated === true ? E('div', { className: 'bw-hint' }, t('browse.truncated')) : null,
4132
- error !== '' ? E('div', { className: 'bw-error-text', role: 'alert' }, error) : null,
4133
- ),
4134
- StyleNode())
4135
- }
4136
-
4137
- /**
4138
- * The add-workspace picking interaction as the sidebar hole's OCCUPANT
4139
- * (v0.12.1): the official flow cannot be rendered here — a child-hole
4140
- * declaration is exclusive and the shipped browser entry, though it
4141
- * loses the render race, still owns 'sidebar.workspaces.directoryFlow's
4142
- * declaration, so re-declaring it throws and renderSlot authorization
4143
- * is runtime-enforced. This flow therefore probes the composed backend
4144
- * (v0.11.3 discipline): a native Host gets its OS chooser through the
4145
- * SAME official host service (uiWorkspace.pickDirectory), a browse Host
4146
- * gets the in-app DirectoryBrowseDialog (the v0.11.4/0.11.5 interaction
4147
- * this plugin polished). A picked path is adopted by the owner with
4148
- * createWorkspace + startSession — no parent-group popup (groups derive
4149
- * from names; rename or drag to group). The conversation hero flow stays
4150
- * purely official: this plugin does NOT occupy that hole.
4151
- */
4152
- function BetterFlow(props) {
4153
- const { open, busy, onPicked, onCancel, onError, pickDirectory, listDirectory, createDirectory, t } = props
4154
- const [phase, setPhase] = React.useState('idle') // idle | picking | browsing
4155
- // All hooks run before any early return: the flow unmounts while
4156
- // closed, but its hook sequence must stay stable.
4157
- React.useEffect(() => {
4158
- if (!open) {
4159
- setPhase('idle')
4160
- return
4161
- }
4162
- let alive = true
4163
- setPhase('picking')
4164
- // Which backend this Host composes decides the interaction: browse
4165
- // renders the in-app browser directly (its pick is refused by
4166
- // design, so trying the chooser first would only surface an error);
4167
- // native opens the OS chooser; an unresolved verdict still tries the
4168
- // chooser so a silent probe never changes the fallback.
4169
- pickerCapabilityNow()
4170
- .then((kind) => {
4171
- if (!alive) return undefined
4172
- if (kind === 'browse') { setPhase('browsing'); return undefined }
4173
- return Promise.resolve()
4174
- .then(() => pickDirectory())
4175
- .then((path) => {
4176
- if (!alive) return
4177
- if (!path) { onCancel(); return }
4178
- onPicked(String(path))
4179
- })
4180
- })
4181
- .catch((reason) => {
4182
- if (!alive) return
4183
- // A refusal is a backend fact, not a failure: this Host serves
4184
- // the browse verbs, so switch interactions instead of erroring.
4185
- if (pickerRefusal(reason)) { markPickerBrowse(); setPhase('browsing'); return }
4186
- setPhase('idle')
4187
- onError(messageOf(reason))
4188
- })
4189
- return () => { alive = false }
4190
- }, [open])
4191
3525
 
4192
- if (!open) return null
4193
- if (phase === 'browsing') {
4194
- return E(DirectoryBrowseDialog, {
4195
- open: true,
4196
- busy: busy === true,
4197
- listDirectory,
4198
- createDirectory,
4199
- onPick: (path) => onPicked(String(path)),
4200
- onClose: onCancel,
4201
- t,
4202
- })
4203
- }
4204
- return null
4205
- }
4206
3526
 
4207
3527
 
4208
3528
  /* ============================== rows ============================== */
@@ -5139,15 +4459,12 @@ window.__ModuleLoader__.load({
5139
4459
 
5140
4460
  const [query, setQuery] = React.useState('')
5141
4461
  const [searchOpen, setSearchOpen] = React.useState(false)
5142
- const [flowOpen, setFlowOpen] = React.useState(false)
5143
4462
  const [flowBusy, setFlowBusy] = React.useState(false) // createWorkspace in flight after a pick
5144
- // Occupancy of this surface's directory-flow hole (our own picking
5145
- // flow fills it): gates the add affordance, and an occupant that
5146
- // unloads mid-interaction withdraws an open flow.
4463
+ // Occupancy of this surface's directory-flow hole. Since v0.16.0 the
4464
+ // occupant is the official backend's client half (native: a renderless
4465
+ // OS-chooser driver; browse: the shipped in-app browser), so this only
4466
+ // gates the add affordance: an uncomposed host has no way to pick at all.
5147
4467
  const flowAvailable = typeof useDirectoryFlow === 'function' ? useDirectoryFlow(occupied => occupied) : false
5148
- React.useEffect(() => {
5149
- if (flowOpen && !flowAvailable) setFlowOpen(false)
5150
- }, [flowOpen, flowAvailable])
5151
4468
  const [dialog, setDialog] = React.useState(null) // { kind, ... }
5152
4469
  const [ctx, setCtx] = React.useState(null) // context menu { kind, payload, x, y }
5153
4470
  const [customize, setCustomize] = React.useState(null) // { kind, entryKey, name }
@@ -6249,7 +5566,7 @@ window.__ModuleLoader__.load({
6249
5566
  return E('div', { className: 'bw-rail' },
6250
5567
  StyleNode(),
6251
5568
  E('button', { type: 'button', className: 'bw-rail-btn', 'aria-label': t('rail.search'), onClick: () => { expandSidebar(); setSearchOpen(true) } }, icon('IconSearchOutline16', 18)),
6252
- flowAvailable ? E('button', { type: 'button', className: 'bw-rail-btn', 'aria-label': t('rail.add'), onClick: () => { expandSidebar(); setFlowOpen(true) } }, icon('IconProjectAddOutline16', 18)) : null,
5569
+ flowAvailable ? E('button', { type: 'button', className: 'bw-rail-btn', 'aria-label': t('rail.add'), onClick: () => { expandSidebar(); startAddFlow() }, disabled: flowBusy }, icon('IconProjectAddOutline16', 18)) : null,
6253
5570
  )
6254
5571
  }
6255
5572
 
@@ -6314,6 +5631,20 @@ window.__ModuleLoader__.load({
6314
5631
  onClose: () => setDialog(null),
6315
5632
  t,
6316
5633
  })
5634
+ if (dialog.kind === 'add-guide') return E(ui.Modal, {
5635
+ key: 'add-guide',
5636
+ open: true,
5637
+ onClose: () => setDialog(null),
5638
+ closeLabel: t('close'),
5639
+ title: t('add.guide.title'),
5640
+ footer: E('div', { className: 'bw-modal-actions' },
5641
+ E(BTN, { variant: 'primary', onClick: () => setDialog(null) }, t('confirm')),
5642
+ ),
5643
+ },
5644
+ E('div', { className: 'bw-modal-body' },
5645
+ E('div', { className: 'bw-hint' }, t('add.guide')),
5646
+ ),
5647
+ )
6317
5648
  return null
6318
5649
  })()
6319
5650
 
@@ -6321,25 +5652,37 @@ window.__ModuleLoader__.load({
6321
5652
  // (busy) until the Host answers, then startSession — exactly what the
6322
5653
  // shipped browser does after the same pick. Groups never ride this
6323
5654
  // flow (v0.12.0+): rename or drag to group instead.
6324
- const adoptDirectory = (path) => Promise.resolve()
6325
- .then(() => createWorkspace({ path: String(path) }))
6326
- .then((workspace) => {
6327
- setFlowOpen(false)
6328
- if (workspace && workspace.workspaceId && typeof startSession === 'function') startSession(workspace.workspaceId)
6329
- })
6330
- .catch((reason) => {
6331
- fail(messageOf(reason))
6332
- setFlowOpen(false)
6333
- })
6334
- const flowOwner = {
6335
- open: flowOpen,
6336
- busy: flowBusy,
6337
- onPicked: (path) => {
6338
- setFlowBusy(true)
6339
- Promise.resolve(adoptDirectory(path)).then(() => setFlowBusy(false))
6340
- },
6341
- onCancel: () => setFlowOpen(false),
6342
- onError: fail,
5655
+ // Adopt keeps the busy flag up until the Host answers, then startSession —
5656
+ // exactly what the shipped browser does after the same pick. Groups never
5657
+ // ride this flow (v0.12.0+): rename or drag to group instead.
5658
+ const adoptDirectory = (path) => {
5659
+ setFlowBusy(true)
5660
+ return Promise.resolve()
5661
+ .then(() => createWorkspace({ path: String(path) }))
5662
+ .then((workspace) => {
5663
+ if (workspace && workspace.workspaceId && typeof startSession === 'function') startSession(workspace.workspaceId)
5664
+ })
5665
+ .catch((reason) => { fail(messageOf(reason)) })
5666
+ .then(() => setFlowBusy(false))
5667
+ }
5668
+
5669
+ // v0.16.0: the sidebar add affordance rides the OFFICIAL chooser only.
5670
+ // A `browse` host refuses that pick by design, so the click explains where
5671
+ // the official in-app flow lives instead of opening a plugin-built copy —
5672
+ // the shipped dialog cannot be rendered from here (see the capability block).
5673
+ const startAddFlow = () => {
5674
+ Promise.resolve(pickerCapabilityNow())
5675
+ .then((kind) => {
5676
+ if (kind === 'browse') { setDialog({ kind: 'add-guide' }); return undefined }
5677
+ return Promise.resolve()
5678
+ .then(() => pickDirectory())
5679
+ .then((path) => (path ? adoptDirectory(String(path)) : undefined))
5680
+ .catch((reason) => {
5681
+ if (pickerRefusal(reason)) { setDialog({ kind: 'add-guide' }); return }
5682
+ fail(messageOf(reason))
5683
+ })
5684
+ })
5685
+ .catch((reason) => { fail(messageOf(reason)) })
6343
5686
  }
6344
5687
 
6345
5688
  return E('div', { className: 'bw-root' },
@@ -6376,22 +5719,9 @@ window.__ModuleLoader__.load({
6376
5719
  t,
6377
5720
  }),
6378
5721
  E('button', { type: 'button', className: 'bw-icon-btn', 'aria-label': t('search.placeholder'), onClick: () => setSearchOpen(v => !v) }, icon('IconSearchOutline16')),
6379
- flowAvailable ? E('button', { type: 'button', className: 'bw-icon-btn', 'aria-label': t('add'), onClick: () => setFlowOpen(true) }, icon('IconProjectAddOutline16')) : null,
5722
+ flowAvailable ? E('button', { type: 'button', className: 'bw-icon-btn', 'aria-label': t('add'), onClick: startAddFlow, disabled: flowBusy }, icon('IconProjectAddOutline16')) : null,
6380
5723
  ),
6381
5724
  E('div', { ref: treeRef, className: 'bw-tree', role: 'tree', 'aria-label': t('title') }, bodyRows),
6382
- // Our own occupant serves the sidebar hole (direct composition, the
6383
- // same props conversation the official owner share defines).
6384
- E(BetterFlow, {
6385
- open: flowOpen,
6386
- busy: flowBusy,
6387
- onPicked: flowOwner.onPicked,
6388
- onCancel: flowOwner.onCancel,
6389
- onError: flowOwner.onError,
6390
- pickDirectory,
6391
- listDirectory,
6392
- createDirectory,
6393
- t,
6394
- }),
6395
5725
  dialogElement,
6396
5726
  (ctx !== null && typeof ui.Menu === 'function') ? E(ui.Menu, {
6397
5727
  // Right-click opens the SAME menu surface the official ⋯ button
@@ -6580,15 +5910,6 @@ window.__ModuleLoader__.load({
6580
5910
  },
6581
5911
  })
6582
5912
 
6583
- // Occupant inject face for the sidebar hole (v0.12.1): the picking
6584
- // primitives BetterFlow needs. The conversation hero hole is
6585
- // deliberately NOT occupied — that flow stays purely official.
6586
- const flowInjected = () => ({
6587
- pickDirectory: () => uiWorkspace.pickDirectory(),
6588
- listDirectory: (path, signal) => uiWorkspace.listDirectory(path, signal),
6589
- createDirectory: (path, name) => uiWorkspace.createDirectory(path, name),
6590
- hooks: { directoryFlow: flowSource(slots, 'sidebar.workspaces.directoryFlow') },
6591
- })
6592
5913
 
6593
5914
  // Registration helper: a thrown register (semantics drift, vanishing
6594
5915
  // hole declaration mid-transition) degrades this one seat, never the
@@ -6634,15 +5955,6 @@ window.__ModuleLoader__.load({
6634
5955
  prefsScopeRef = null
6635
5956
  }
6636
5957
 
6637
- // The sidebar hole occupation itself (v0.12.1): priority -1 shadows
6638
- // the official composed picker inside the hole — the hero hole keeps
6639
- // the official occupant, so the conversation flow stays stock.
6640
- slots.inject('sidebar.workspaces.directoryFlow', guarded(
6641
- 'sidebar.workspaces.directoryFlow',
6642
- { name: 'sidebar.workspaces.directoryFlow', inject: flowInjected, locale: NS, priority: -1 },
6643
- BetterFlow,
6644
- ))
6645
-
6646
5958
  // Settings → Plugins card only (the tab dispatches the intersection of
6647
5959
  // served namespaces — registered host-side — and settings.plugin.item
6648
5960
  // cards). The old left-nav settings.section entry was removed: the