dsh-better-workspace 0.11.2 → 0.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/README_EN.md +2 -0
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/client.js +618 -16
package/README.md
CHANGED
|
@@ -27,6 +27,8 @@ web/ ← 虚拟分组(不是真实目录,纯命名层级)
|
|
|
27
27
|
|
|
28
28
|
点「添加工作区」并选完文件夹后,弹出一个小对话框,一行选择工作区所属分组——直接输入(如 `web`)、从下拉里选已有层级(datalist 原生补全),留空就是根分组。确认后插件创建工作区并把分组前缀写进名称,树上自然落位。
|
|
29
29
|
|
|
30
|
+
**用哪种目录选择器,跟随宿主实际组装的后端**:webserver 只绑本机 loopback 时是宿主桌面上的**原生选择器**;绑到 `0.0.0.0`(局域网访问)、SSH 转发或宿主没有可用显示会话时,宿主只组装 `browse` 后端——它仅提供「列目录 / 新建目录」原语,`pick` 会被明确拒绝——插件这时改用**应用内目录浏览器**(Home 起点的面包屑、可编辑路径、行内新建文件夹、显示隐藏文件),选定目录后照样进同一个「所属分组」对话框。若确实想在这种绑定下走原生选择器,只能由用户显式钉死宿主的 native 后端包,而那会在**服务器桌面**上弹窗,局域网客户端根本够不着。
|
|
31
|
+
|
|
30
32
|
### 分组的增删
|
|
31
33
|
|
|
32
34
|
右键任意分组行即可:**新增子分组**(路径自动带上父分组前缀)、**新增子工作区**(选完目录后所属分组输入框自动预填)、重命名分组、删除空分组;标题栏不再放独立按钮。空分组持久保存在浏览器本地;一旦有工作区落进去,分组就由名称自然派生。
|
package/README_EN.md
CHANGED
|
@@ -25,6 +25,8 @@ web/ <- virtual folder (naming only, not a real directory)
|
|
|
25
25
|
|
|
26
26
|
After you pick a directory for a new workspace, a small dialog asks for the parent group — type one (`web`), pick an existing level from the datalist, or leave it empty for the root. The plugin creates the workspace and writes the prefix into its title, so it lands on the right branch of the tree.
|
|
27
27
|
|
|
28
|
+
**Which picker you get follows the backend the host actually composes.** A loopback-only webserver bind gives you the host's **native OS chooser**; an all-interfaces/LAN bind, an SSH launch, or a host without a usable display session composes the `browse` backend instead — listing and directory creation only, with `pick` refused by design — and the plugin then uses an **in-app directory browser** (Home-rooted breadcrumbs, editable path, inline new-folder row, hidden-file toggle) before the very same parent-group dialog. Forcing the native chooser under such a bind means pinning the host's native backend package yourself, and that dialog opens on the **server's** desktop, out of reach for LAN clients.
|
|
29
|
+
|
|
28
30
|
### Folder create/delete via context menu
|
|
29
31
|
|
|
30
32
|
Right-click any folder row: new subfolder (parent prefix pre-filled), new workspace here (the add-flow group field pre-fills with this folder), rename folder, delete empty folder — no standalone header button anymore. Explicit empty folders persist in the browser until workspaces live inside them.
|
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-better-workspace",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"description": "侧边栏工作区层级树:名称中的 / 即虚拟分组(web/前端 · web/后端),添加工作区的目录流附所属分组弹窗,重命名即时重排层级,可新建空分组。Hierarchy tree for the DSH sidebar workspace list.",
|
|
6
6
|
"engines": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-better-workspace",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
4
4
|
"description": "DSH web plugin: a hierarchical workspace tree for the sidebar. Workspace titles containing \"/\" group into virtual folders (web/前端 · web/后端); the add-workspace directory flow gains a parent-group popup; renames re-derive the tree; a new-folder button creates empty levels.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/client.js
CHANGED
|
@@ -116,6 +116,17 @@ window.__ModuleLoader__.load({
|
|
|
116
116
|
'flow.parent': '所属分组',
|
|
117
117
|
'flow.parentHint': '输入或下拉选择分组路径,留空表示根分组;多级用 / 分隔',
|
|
118
118
|
'flow.creating': '正在创建…',
|
|
119
|
+
'browse.title': '选择工作区目录',
|
|
120
|
+
'browse.home': '主目录',
|
|
121
|
+
'browse.up': '上一层',
|
|
122
|
+
'browse.newFolder': '新建文件夹',
|
|
123
|
+
'browse.folderName': '文件夹名称',
|
|
124
|
+
'browse.empty': '此文件夹没有子文件夹',
|
|
125
|
+
'browse.loading': '加载中…',
|
|
126
|
+
'browse.truncated': '文件夹过多,仅显示开头部分。',
|
|
127
|
+
'browse.showHidden': '显示隐藏文件',
|
|
128
|
+
'browse.editPath': '编辑路径',
|
|
129
|
+
'browse.select': '选择此文件夹',
|
|
119
130
|
'error.title': '出错了',
|
|
120
131
|
'cancel': '取消',
|
|
121
132
|
'create': '创建',
|
|
@@ -224,6 +235,17 @@ window.__ModuleLoader__.load({
|
|
|
224
235
|
'flow.parent': 'Parent group',
|
|
225
236
|
'flow.parentHint': 'Type or pick a group path; empty means root. Nest with /',
|
|
226
237
|
'flow.creating': 'Creating…',
|
|
238
|
+
'browse.title': 'Select Workspace Directory',
|
|
239
|
+
'browse.home': 'Home',
|
|
240
|
+
'browse.up': 'Up',
|
|
241
|
+
'browse.newFolder': 'New folder',
|
|
242
|
+
'browse.folderName': 'Folder name',
|
|
243
|
+
'browse.empty': 'No subfolders here',
|
|
244
|
+
'browse.loading': 'Loading…',
|
|
245
|
+
'browse.truncated': 'Too many folders to list; only the beginning is shown.',
|
|
246
|
+
'browse.showHidden': 'Show hidden files',
|
|
247
|
+
'browse.editPath': 'Edit path',
|
|
248
|
+
'browse.select': 'Use this folder',
|
|
227
249
|
'error.title': 'Something went wrong',
|
|
228
250
|
'cancel': 'Cancel',
|
|
229
251
|
'create': 'Create',
|
|
@@ -340,6 +362,17 @@ window.__ModuleLoader__.load({
|
|
|
340
362
|
'flow.parent': 'المجموعة التابعة لها',
|
|
341
363
|
'flow.parentHint': 'اكتب مسار المجموعة أو اختره؛ الفراغ يعني الجذر؛ تُفصل المستويات بـ /',
|
|
342
364
|
'flow.creating': 'جارٍ الإنشاء…',
|
|
365
|
+
'browse.title': 'اختيار مجلد مساحة العمل',
|
|
366
|
+
'browse.home': 'المجلد الرئيسي',
|
|
367
|
+
'browse.up': 'المستوى الأعلى',
|
|
368
|
+
'browse.newFolder': 'مجلد جديد',
|
|
369
|
+
'browse.folderName': 'اسم المجلد',
|
|
370
|
+
'browse.empty': 'لا توجد مجلدات فرعية هنا',
|
|
371
|
+
'browse.loading': 'جارٍ التحميل…',
|
|
372
|
+
'browse.truncated': 'المجلدات كثيرة جدًا؛ يُعرض الجزء الأول فقط.',
|
|
373
|
+
'browse.showHidden': 'إظهار الملفات المخفية',
|
|
374
|
+
'browse.editPath': 'تعديل المسار',
|
|
375
|
+
'browse.select': 'استخدام هذا المجلد',
|
|
343
376
|
'error.title': 'حدث خطأ ما',
|
|
344
377
|
'cancel': 'إلغاء',
|
|
345
378
|
'create': 'إنشاء',
|
|
@@ -448,6 +481,17 @@ window.__ModuleLoader__.load({
|
|
|
448
481
|
'flow.parent': 'Übergeordnete Gruppe',
|
|
449
482
|
'flow.parentHint': 'Gruppenpfad eingeben oder auswählen, leer bedeutet Wurzel; Ebenen mit / trennen',
|
|
450
483
|
'flow.creating': 'Wird erstellt…',
|
|
484
|
+
'browse.title': 'Arbeitsbereichsordner wählen',
|
|
485
|
+
'browse.home': 'Home',
|
|
486
|
+
'browse.up': 'Übergeordnet',
|
|
487
|
+
'browse.newFolder': 'Neuer Ordner',
|
|
488
|
+
'browse.folderName': 'Ordnername',
|
|
489
|
+
'browse.empty': 'Keine Unterordner vorhanden',
|
|
490
|
+
'browse.loading': 'Wird geladen…',
|
|
491
|
+
'browse.truncated': 'Zu viele Ordner; nur der Anfang wird angezeigt.',
|
|
492
|
+
'browse.showHidden': 'Versteckte Dateien anzeigen',
|
|
493
|
+
'browse.editPath': 'Pfad bearbeiten',
|
|
494
|
+
'browse.select': 'Diesen Ordner verwenden',
|
|
451
495
|
'error.title': 'Etwas ist schiefgelaufen',
|
|
452
496
|
'cancel': 'Abbrechen',
|
|
453
497
|
'create': 'Erstellen',
|
|
@@ -556,6 +600,17 @@ window.__ModuleLoader__.load({
|
|
|
556
600
|
'flow.parent': 'Groupe parent',
|
|
557
601
|
'flow.parentHint': 'Saisissez ou choisissez un chemin de groupe; vide = racine; niveaux séparés par /',
|
|
558
602
|
'flow.creating': 'Création…',
|
|
603
|
+
'browse.title': 'Choisir le dossier de l’espace de travail',
|
|
604
|
+
'browse.home': 'Dossier personnel',
|
|
605
|
+
'browse.up': 'Niveau supérieur',
|
|
606
|
+
'browse.newFolder': 'Nouveau dossier',
|
|
607
|
+
'browse.folderName': 'Nom du dossier',
|
|
608
|
+
'browse.empty': 'Aucun sous-dossier ici',
|
|
609
|
+
'browse.loading': 'Chargement…',
|
|
610
|
+
'browse.truncated': 'Trop de dossiers ; seul le début est affiché.',
|
|
611
|
+
'browse.showHidden': 'Afficher les fichiers cachés',
|
|
612
|
+
'browse.editPath': 'Modifier le chemin',
|
|
613
|
+
'browse.select': 'Utiliser ce dossier',
|
|
559
614
|
'error.title': 'Une erreur est survenue',
|
|
560
615
|
'cancel': 'Annuler',
|
|
561
616
|
'create': 'Créer',
|
|
@@ -664,6 +719,17 @@ window.__ModuleLoader__.load({
|
|
|
664
719
|
'flow.parent': 'संबंधित समूह',
|
|
665
720
|
'flow.parentHint': 'समूह पथ लिखें या चुनें; खाली का अर्थ मूल है; कई स्तर / से अलग करें',
|
|
666
721
|
'flow.creating': 'बनाया जा रहा है…',
|
|
722
|
+
'browse.title': 'कार्यस्थान फ़ोल्डर चुनें',
|
|
723
|
+
'browse.home': 'होम',
|
|
724
|
+
'browse.up': 'ऊपर',
|
|
725
|
+
'browse.newFolder': 'नया फ़ोल्डर',
|
|
726
|
+
'browse.folderName': 'फ़ोल्डर का नाम',
|
|
727
|
+
'browse.empty': 'यहाँ कोई उप-फ़ोल्डर नहीं',
|
|
728
|
+
'browse.loading': 'लोड हो रहा है…',
|
|
729
|
+
'browse.truncated': 'बहुत अधिक फ़ोल्डर; केवल शुरुआत दिखाई गई है।',
|
|
730
|
+
'browse.showHidden': 'छिपी फ़ाइलें दिखाएँ',
|
|
731
|
+
'browse.editPath': 'पथ संपादित करें',
|
|
732
|
+
'browse.select': 'यह फ़ोल्डर चुनें',
|
|
667
733
|
'error.title': 'कुछ गड़बड़ हो गई',
|
|
668
734
|
'cancel': 'रद्द करें',
|
|
669
735
|
'create': 'बनाएँ',
|
|
@@ -772,6 +838,17 @@ window.__ModuleLoader__.load({
|
|
|
772
838
|
'flow.parent': 'Grup induk',
|
|
773
839
|
'flow.parentHint': 'Ketik atau pilih jalur grup; kosong = akar; tingkat dipisah dengan /',
|
|
774
840
|
'flow.creating': 'Membuat…',
|
|
841
|
+
'browse.title': 'Pilih folder ruang kerja',
|
|
842
|
+
'browse.home': 'Beranda',
|
|
843
|
+
'browse.up': 'Naik',
|
|
844
|
+
'browse.newFolder': 'Folder baru',
|
|
845
|
+
'browse.folderName': 'Nama folder',
|
|
846
|
+
'browse.empty': 'Tidak ada subfolder di sini',
|
|
847
|
+
'browse.loading': 'Memuat…',
|
|
848
|
+
'browse.truncated': 'Terlalu banyak folder; hanya bagian awal yang ditampilkan.',
|
|
849
|
+
'browse.showHidden': 'Tampilkan file tersembunyi',
|
|
850
|
+
'browse.editPath': 'Edit jalur',
|
|
851
|
+
'browse.select': 'Gunakan folder ini',
|
|
775
852
|
'error.title': 'Terjadi kesalahan',
|
|
776
853
|
'cancel': 'Batal',
|
|
777
854
|
'create': 'Buat',
|
|
@@ -880,6 +957,17 @@ window.__ModuleLoader__.load({
|
|
|
880
957
|
'flow.parent': 'Gruppo di appartenenza',
|
|
881
958
|
'flow.parentHint': 'Digita o scegli un percorso di gruppo; vuoto = radice; livelli separati da /',
|
|
882
959
|
'flow.creating': 'Creazione…',
|
|
960
|
+
'browse.title': 'Scegli cartella dell’area di lavoro',
|
|
961
|
+
'browse.home': 'Home',
|
|
962
|
+
'browse.up': 'Livello superiore',
|
|
963
|
+
'browse.newFolder': 'Nuova cartella',
|
|
964
|
+
'browse.folderName': 'Nome cartella',
|
|
965
|
+
'browse.empty': 'Nessuna sottocartella qui',
|
|
966
|
+
'browse.loading': 'Caricamento…',
|
|
967
|
+
'browse.truncated': 'Troppe cartelle; è mostrato solo l’inizio.',
|
|
968
|
+
'browse.showHidden': 'Mostra file nascosti',
|
|
969
|
+
'browse.editPath': 'Modifica percorso',
|
|
970
|
+
'browse.select': 'Usa questa cartella',
|
|
883
971
|
'error.title': 'Si è verificato un errore',
|
|
884
972
|
'cancel': 'Annulla',
|
|
885
973
|
'create': 'Crea',
|
|
@@ -988,6 +1076,17 @@ window.__ModuleLoader__.load({
|
|
|
988
1076
|
'flow.parent': '所属グループ',
|
|
989
1077
|
'flow.parentHint': 'グループのパスを入力または選択します,空欄はルート,階層は / で区切ります',
|
|
990
1078
|
'flow.creating': '作成中…',
|
|
1079
|
+
'browse.title': 'ワークスペースのフォルダーを選択',
|
|
1080
|
+
'browse.home': 'ホーム',
|
|
1081
|
+
'browse.up': '上の階層',
|
|
1082
|
+
'browse.newFolder': '新しいフォルダー',
|
|
1083
|
+
'browse.folderName': 'フォルダー名',
|
|
1084
|
+
'browse.empty': 'ここにサブフォルダーはありません',
|
|
1085
|
+
'browse.loading': '読み込み中…',
|
|
1086
|
+
'browse.truncated': 'フォルダーが多すぎます。先頭のみ表示しています。',
|
|
1087
|
+
'browse.showHidden': '隠しファイルを表示',
|
|
1088
|
+
'browse.editPath': 'パスを編集',
|
|
1089
|
+
'browse.select': 'このフォルダーを選択',
|
|
991
1090
|
'error.title': 'エラーが発生しました',
|
|
992
1091
|
'cancel': 'キャンセル',
|
|
993
1092
|
'create': '作成',
|
|
@@ -1096,6 +1195,17 @@ window.__ModuleLoader__.load({
|
|
|
1096
1195
|
'flow.parent': '소속 그룹',
|
|
1097
1196
|
'flow.parentHint': '그룹 경로를 입력하거나 선택하세요, 비우면 루트, 여러 단계는 / 로 구분합니다',
|
|
1098
1197
|
'flow.creating': '만드는 중…',
|
|
1198
|
+
'browse.title': '워크스페이스 폴더 선택',
|
|
1199
|
+
'browse.home': '홈',
|
|
1200
|
+
'browse.up': '상위',
|
|
1201
|
+
'browse.newFolder': '새 폴더',
|
|
1202
|
+
'browse.folderName': '폴더 이름',
|
|
1203
|
+
'browse.empty': '여기에 하위 폴더가 없습니다',
|
|
1204
|
+
'browse.loading': '불러오는 중…',
|
|
1205
|
+
'browse.truncated': '폴더가 너무 많습니다. 앞부분만 표시합니다.',
|
|
1206
|
+
'browse.showHidden': '숨김 파일 표시',
|
|
1207
|
+
'browse.editPath': '경로 편집',
|
|
1208
|
+
'browse.select': '이 폴더 선택',
|
|
1099
1209
|
'error.title': '문제가 발생했습니다',
|
|
1100
1210
|
'cancel': '취소',
|
|
1101
1211
|
'create': '만들기',
|
|
@@ -1204,6 +1314,17 @@ window.__ModuleLoader__.load({
|
|
|
1204
1314
|
'flow.parent': 'Bovenliggende groep',
|
|
1205
1315
|
'flow.parentHint': 'Typ of kies een groeppad; leeg betekent hoofdmap; niveaus scheiden met /',
|
|
1206
1316
|
'flow.creating': 'Bezig met aanmaken…',
|
|
1317
|
+
'browse.title': 'Werkruimtemap kiezen',
|
|
1318
|
+
'browse.home': 'Home',
|
|
1319
|
+
'browse.up': 'Omhoog',
|
|
1320
|
+
'browse.newFolder': 'Nieuwe map',
|
|
1321
|
+
'browse.folderName': 'Mapnaam',
|
|
1322
|
+
'browse.empty': 'Geen submappen hier',
|
|
1323
|
+
'browse.loading': 'Laden…',
|
|
1324
|
+
'browse.truncated': 'Te veel mappen; alleen het begin wordt getoond.',
|
|
1325
|
+
'browse.showHidden': 'Verborgen bestanden tonen',
|
|
1326
|
+
'browse.editPath': 'Pad bewerken',
|
|
1327
|
+
'browse.select': 'Deze map gebruiken',
|
|
1207
1328
|
'error.title': 'Er is iets misgegaan',
|
|
1208
1329
|
'cancel': 'Annuleren',
|
|
1209
1330
|
'create': 'Aanmaken',
|
|
@@ -1312,6 +1433,17 @@ window.__ModuleLoader__.load({
|
|
|
1312
1433
|
'flow.parent': 'Grupa nadrzędna',
|
|
1313
1434
|
'flow.parentHint': 'Wpisz lub wybierz ścieżkę grupy; puste = główna; poziomy rozdziela /',
|
|
1314
1435
|
'flow.creating': 'Tworzenie…',
|
|
1436
|
+
'browse.title': 'Wybierz folder obszaru roboczego',
|
|
1437
|
+
'browse.home': 'Katalog domowy',
|
|
1438
|
+
'browse.up': 'W górę',
|
|
1439
|
+
'browse.newFolder': 'Nowy folder',
|
|
1440
|
+
'browse.folderName': 'Nazwa folderu',
|
|
1441
|
+
'browse.empty': 'Brak podfolderów',
|
|
1442
|
+
'browse.loading': 'Wczytywanie…',
|
|
1443
|
+
'browse.truncated': 'Zbyt wiele folderów; pokazano tylko początek.',
|
|
1444
|
+
'browse.showHidden': 'Pokaż ukryte pliki',
|
|
1445
|
+
'browse.editPath': 'Edytuj ścieżkę',
|
|
1446
|
+
'browse.select': 'Użyj tego folderu',
|
|
1315
1447
|
'error.title': 'Coś poszło nie tak',
|
|
1316
1448
|
'cancel': 'Anuluj',
|
|
1317
1449
|
'create': 'Utwórz',
|
|
@@ -1420,6 +1552,17 @@ window.__ModuleLoader__.load({
|
|
|
1420
1552
|
'flow.parent': 'Grupo pai',
|
|
1421
1553
|
'flow.parentHint': 'Digite ou escolha o caminho do grupo; vazio = raiz; níveis separados por /',
|
|
1422
1554
|
'flow.creating': 'Criando…',
|
|
1555
|
+
'browse.title': 'Escolher pasta da área de trabalho',
|
|
1556
|
+
'browse.home': 'Início',
|
|
1557
|
+
'browse.up': 'Acima',
|
|
1558
|
+
'browse.newFolder': 'Nova pasta',
|
|
1559
|
+
'browse.folderName': 'Nome da pasta',
|
|
1560
|
+
'browse.empty': 'Nenhuma subpasta aqui',
|
|
1561
|
+
'browse.loading': 'Carregando…',
|
|
1562
|
+
'browse.truncated': 'Pastas demais; apenas o início é exibido.',
|
|
1563
|
+
'browse.showHidden': 'Mostrar arquivos ocultos',
|
|
1564
|
+
'browse.editPath': 'Editar caminho',
|
|
1565
|
+
'browse.select': 'Usar esta pasta',
|
|
1423
1566
|
'error.title': 'Algo deu errado',
|
|
1424
1567
|
'cancel': 'Cancelar',
|
|
1425
1568
|
'create': 'Criar',
|
|
@@ -1528,6 +1671,17 @@ window.__ModuleLoader__.load({
|
|
|
1528
1671
|
'flow.parent': 'Родительская группа',
|
|
1529
1672
|
'flow.parentHint': 'Введите или выберите путь группы; пусто — корень; уровни разделяются /',
|
|
1530
1673
|
'flow.creating': 'Создание…',
|
|
1674
|
+
'browse.title': 'Выбор папки рабочей области',
|
|
1675
|
+
'browse.home': 'Домашняя папка',
|
|
1676
|
+
'browse.up': 'На уровень выше',
|
|
1677
|
+
'browse.newFolder': 'Новая папка',
|
|
1678
|
+
'browse.folderName': 'Имя папки',
|
|
1679
|
+
'browse.empty': 'Здесь нет вложенных папок',
|
|
1680
|
+
'browse.loading': 'Загрузка…',
|
|
1681
|
+
'browse.truncated': 'Слишком много папок; показано только начало.',
|
|
1682
|
+
'browse.showHidden': 'Показывать скрытые файлы',
|
|
1683
|
+
'browse.editPath': 'Изменить путь',
|
|
1684
|
+
'browse.select': 'Выбрать эту папку',
|
|
1531
1685
|
'error.title': 'Что-то пошло не так',
|
|
1532
1686
|
'cancel': 'Отмена',
|
|
1533
1687
|
'create': 'Создать',
|
|
@@ -1636,6 +1790,17 @@ window.__ModuleLoader__.load({
|
|
|
1636
1790
|
'flow.parent': 'Överordnad grupp',
|
|
1637
1791
|
'flow.parentHint': 'Skriv eller välj en gruppsökväg; tomt = rot; nivåer avgränsas med /',
|
|
1638
1792
|
'flow.creating': 'Skapar…',
|
|
1793
|
+
'browse.title': 'Välj arbetsyte-mapp',
|
|
1794
|
+
'browse.home': 'Hem',
|
|
1795
|
+
'browse.up': 'Upp',
|
|
1796
|
+
'browse.newFolder': 'Ny mapp',
|
|
1797
|
+
'browse.folderName': 'Mappnamn',
|
|
1798
|
+
'browse.empty': 'Inga undermappar här',
|
|
1799
|
+
'browse.loading': 'Laddar…',
|
|
1800
|
+
'browse.truncated': 'För många mappar; endast början visas.',
|
|
1801
|
+
'browse.showHidden': 'Visa dolda filer',
|
|
1802
|
+
'browse.editPath': 'Redigera sökväg',
|
|
1803
|
+
'browse.select': 'Använd den här mappen',
|
|
1639
1804
|
'error.title': 'Något gick fel',
|
|
1640
1805
|
'cancel': 'Avbryt',
|
|
1641
1806
|
'create': 'Skapa',
|
|
@@ -1744,6 +1909,17 @@ window.__ModuleLoader__.load({
|
|
|
1744
1909
|
'flow.parent': 'กลุ่มต้นทาง',
|
|
1745
1910
|
'flow.parentHint': 'พิมพ์หรือเลือกเส้นทางกลุ่ม เว้นว่างคือระดับราก หลายระดับคั่นด้วย /',
|
|
1746
1911
|
'flow.creating': 'กำลังสร้าง…',
|
|
1912
|
+
'browse.title': 'เลือกโฟลเดอร์เวิร์กสเปซ',
|
|
1913
|
+
'browse.home': 'โฮม',
|
|
1914
|
+
'browse.up': 'ขึ้นหนึ่งระดับ',
|
|
1915
|
+
'browse.newFolder': 'โฟลเดอร์ใหม่',
|
|
1916
|
+
'browse.folderName': 'ชื่อโฟลเดอร์',
|
|
1917
|
+
'browse.empty': 'ไม่มีโฟลเดอร์ย่อยที่นี่',
|
|
1918
|
+
'browse.loading': 'กำลังโหลด…',
|
|
1919
|
+
'browse.truncated': 'มีโฟลเดอร์มากเกินไป แสดงเฉพาะส่วนต้น',
|
|
1920
|
+
'browse.showHidden': 'แสดงไฟล์ที่ซ่อนอยู่',
|
|
1921
|
+
'browse.editPath': 'แก้ไขเส้นทาง',
|
|
1922
|
+
'browse.select': 'ใช้โฟลเดอร์นี้',
|
|
1747
1923
|
'error.title': 'เกิดข้อผิดพลาด',
|
|
1748
1924
|
'cancel': 'ยกเลิก',
|
|
1749
1925
|
'create': 'สร้าง',
|
|
@@ -1852,6 +2028,17 @@ window.__ModuleLoader__.load({
|
|
|
1852
2028
|
'flow.parent': 'Bağlı olduğu grup',
|
|
1853
2029
|
'flow.parentHint': 'Grup yolunu yazın veya seçin; boş = kök; düzeyler / ile ayrılır',
|
|
1854
2030
|
'flow.creating': 'Oluşturuluyor…',
|
|
2031
|
+
'browse.title': 'Çalışma alanı klasörünü seç',
|
|
2032
|
+
'browse.home': 'Ana dizin',
|
|
2033
|
+
'browse.up': 'Üst düzey',
|
|
2034
|
+
'browse.newFolder': 'Yeni klasör',
|
|
2035
|
+
'browse.folderName': 'Klasör adı',
|
|
2036
|
+
'browse.empty': 'Burada alt klasör yok',
|
|
2037
|
+
'browse.loading': 'Yükleniyor…',
|
|
2038
|
+
'browse.truncated': 'Çok fazla klasör var; yalnızca başlangıç gösteriliyor.',
|
|
2039
|
+
'browse.showHidden': 'Gizli dosyaları göster',
|
|
2040
|
+
'browse.editPath': 'Yolu düzenle',
|
|
2041
|
+
'browse.select': 'Bu klasörü kullan',
|
|
1855
2042
|
'error.title': 'Bir şeyler ters gitti',
|
|
1856
2043
|
'cancel': 'İptal',
|
|
1857
2044
|
'create': 'Oluştur',
|
|
@@ -1960,6 +2147,17 @@ window.__ModuleLoader__.load({
|
|
|
1960
2147
|
'flow.parent': 'Nhóm trực thuộc',
|
|
1961
2148
|
'flow.parentHint': 'Nhập hoặc chọn đường dẫn nhóm; để trống là gốc; nhiều tầng cách nhau bằng /',
|
|
1962
2149
|
'flow.creating': 'Đang tạo…',
|
|
2150
|
+
'browse.title': 'Chọn thư mục không gian làm việc',
|
|
2151
|
+
'browse.home': 'Thư mục chính',
|
|
2152
|
+
'browse.up': 'Lên một cấp',
|
|
2153
|
+
'browse.newFolder': 'Thư mục mới',
|
|
2154
|
+
'browse.folderName': 'Tên thư mục',
|
|
2155
|
+
'browse.empty': 'Không có thư mục con ở đây',
|
|
2156
|
+
'browse.loading': 'Đang tải…',
|
|
2157
|
+
'browse.truncated': 'Quá nhiều thư mục; chỉ hiển thị phần đầu.',
|
|
2158
|
+
'browse.showHidden': 'Hiện tệp ẩn',
|
|
2159
|
+
'browse.editPath': 'Sửa đường dẫn',
|
|
2160
|
+
'browse.select': 'Dùng thư mục này',
|
|
1963
2161
|
'error.title': 'Đã xảy ra lỗi',
|
|
1964
2162
|
'cancel': 'Hủy',
|
|
1965
2163
|
'create': 'Tạo',
|
|
@@ -2068,6 +2266,17 @@ window.__ModuleLoader__.load({
|
|
|
2068
2266
|
'flow.parent': '所屬分組',
|
|
2069
2267
|
'flow.parentHint': '輸入或者揀分組路徑,留空代表根分組;多級用 / 分隔',
|
|
2070
2268
|
'flow.creating': '建立緊…',
|
|
2269
|
+
'browse.title': '揀工作區資料夾',
|
|
2270
|
+
'browse.home': '主目錄',
|
|
2271
|
+
'browse.up': '上一層',
|
|
2272
|
+
'browse.newFolder': '新增資料夾',
|
|
2273
|
+
'browse.folderName': '資料夾名稱',
|
|
2274
|
+
'browse.empty': '呢個資料夾冇子資料夾',
|
|
2275
|
+
'browse.loading': '載入緊…',
|
|
2276
|
+
'browse.truncated': '資料夾太多,只顯示開頭部分。',
|
|
2277
|
+
'browse.showHidden': '顯示隱藏檔案',
|
|
2278
|
+
'browse.editPath': '編輯路徑',
|
|
2279
|
+
'browse.select': '揀呢個資料夾',
|
|
2071
2280
|
'error.title': '出咗錯',
|
|
2072
2281
|
'cancel': '取消',
|
|
2073
2282
|
'create': '建立',
|
|
@@ -2176,6 +2385,17 @@ window.__ModuleLoader__.load({
|
|
|
2176
2385
|
'flow.parent': '所屬分組',
|
|
2177
2386
|
'flow.parentHint': '輸入或者揀分組路徑,留空代表根分組;多級用 / 分隔',
|
|
2178
2387
|
'flow.creating': '建立緊…',
|
|
2388
|
+
'browse.title': '揀工作區資料夾',
|
|
2389
|
+
'browse.home': '主目錄',
|
|
2390
|
+
'browse.up': '上一層',
|
|
2391
|
+
'browse.newFolder': '新增資料夾',
|
|
2392
|
+
'browse.folderName': '資料夾名稱',
|
|
2393
|
+
'browse.empty': '呢個資料夾冇子資料夾',
|
|
2394
|
+
'browse.loading': '載入緊…',
|
|
2395
|
+
'browse.truncated': '資料夾太多,只顯示開頭部分。',
|
|
2396
|
+
'browse.showHidden': '顯示隱藏檔案',
|
|
2397
|
+
'browse.editPath': '編輯路徑',
|
|
2398
|
+
'browse.select': '揀呢個資料夾',
|
|
2179
2399
|
'error.title': '出咗錯',
|
|
2180
2400
|
'cancel': '取消',
|
|
2181
2401
|
'create': '建立',
|
|
@@ -2284,6 +2504,17 @@ window.__ModuleLoader__.load({
|
|
|
2284
2504
|
'flow.parent': '所屬群組',
|
|
2285
2505
|
'flow.parentHint': '輸入或下拉選擇群組路徑,留空表示根群組;多層用 / 分隔',
|
|
2286
2506
|
'flow.creating': '正在建立…',
|
|
2507
|
+
'browse.title': '選擇工作區資料夾',
|
|
2508
|
+
'browse.home': '主目錄',
|
|
2509
|
+
'browse.up': '上一層',
|
|
2510
|
+
'browse.newFolder': '新增資料夾',
|
|
2511
|
+
'browse.folderName': '資料夾名稱',
|
|
2512
|
+
'browse.empty': '此資料夾沒有子資料夾',
|
|
2513
|
+
'browse.loading': '載入中…',
|
|
2514
|
+
'browse.truncated': '資料夾過多,僅顯示開頭部分。',
|
|
2515
|
+
'browse.showHidden': '顯示隱藏檔案',
|
|
2516
|
+
'browse.editPath': '編輯路徑',
|
|
2517
|
+
'browse.select': '選擇此資料夾',
|
|
2287
2518
|
'error.title': '發生錯誤',
|
|
2288
2519
|
'cancel': '取消',
|
|
2289
2520
|
'create': '建立',
|
|
@@ -2798,6 +3029,38 @@ window.__ModuleLoader__.load({
|
|
|
2798
3029
|
'.bw-sync-btn{padding:5px 12px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.3));background:transparent;color:var(--dsw-alias-label-secondary,#b8b8b8);cursor:pointer;font-size:12px}',
|
|
2799
3030
|
'.bw-sync-btn:disabled{opacity:.45;cursor:default}',
|
|
2800
3031
|
'.bw-sync-btn-primary{border-color:var(--dsw-alias-brand-primary,#5b8def);color:var(--dsw-alias-label-primary,#e6e6e6)}',
|
|
3032
|
+
/*
|
|
3033
|
+
* In-app directory browser (Hosts composing the browse backend). Every
|
|
3034
|
+
* surface rides the theme token chain like the rest of the plugin — the
|
|
3035
|
+
* list panel uses bg-layer-2 so a transparent/background plugin still
|
|
3036
|
+
* reads correctly; nothing may hard-code an opaque background.
|
|
3037
|
+
*/
|
|
3038
|
+
'.bw-browse-body{min-width:360px;max-width:460px}',
|
|
3039
|
+
'.bw-browse-bar{display:flex;align-items:center;gap:4px}',
|
|
3040
|
+
'.bw-browse-crumbs{display:flex;align-items:center;gap:2px;flex:1;min-width:0;overflow-x:auto}',
|
|
3041
|
+
'.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}',
|
|
3042
|
+
'.bw-browse-crumb:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,#e6e6e6)}',
|
|
3043
|
+
'.bw-browse-crumb:disabled{opacity:.4;cursor:default}',
|
|
3044
|
+
'.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}',
|
|
3045
|
+
'.bw-browse-nav{font-size:13px;line-height:1}',
|
|
3046
|
+
'.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}',
|
|
3047
|
+
'.bw-browse-edit:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,#e6e6e6)}',
|
|
3048
|
+
'.bw-browse-edit svg{width:14px;height:14px}',
|
|
3049
|
+
'.bw-browse-edit-row{display:flex;gap:6px;align-items:center}',
|
|
3050
|
+
'.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))}',
|
|
3051
|
+
'.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}',
|
|
3052
|
+
'.bw-browse-row:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}',
|
|
3053
|
+
'.bw-browse-row:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5b8def);outline-offset:-2px}',
|
|
3054
|
+
'.bw-browse-row svg{width:15px;height:15px;flex:none;color:var(--dsw-alias-label-secondary,#b8b8b8)}',
|
|
3055
|
+
'.bw-browse-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
3056
|
+
'.bw-browse-note{font-size:12px;color:var(--dsw-alias-label-tertiary,#9a9a9a);padding:10px 8px;text-align:center}',
|
|
3057
|
+
'.bw-browse-new-row{display:flex;gap:6px;align-items:center}',
|
|
3058
|
+
'.bw-browse-tools{display:flex;align-items:center;gap:6px;flex-wrap:wrap}',
|
|
3059
|
+
'.bw-browse-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px}',
|
|
3060
|
+
'.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}',
|
|
3061
|
+
'.bw-browse-tool:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,#e6e6e6)}',
|
|
3062
|
+
'.bw-browse-tool:disabled{opacity:.45;cursor:default}',
|
|
3063
|
+
'.bw-browse-tool-on{border-color:var(--dsw-alias-brand-primary,#5b8def);color:var(--dsw-alias-label-primary,#e6e6e6)}',
|
|
2801
3064
|
].join('')
|
|
2802
3065
|
|
|
2803
3066
|
const StyleNode = () => E('style', null, CSS_TEXT)
|
|
@@ -3031,6 +3294,65 @@ window.__ModuleLoader__.load({
|
|
|
3031
3294
|
},
|
|
3032
3295
|
})
|
|
3033
3296
|
|
|
3297
|
+
/* ==================== directory-picker capability ================== */
|
|
3298
|
+
|
|
3299
|
+
// The host composes exactly ONE directory-picking backend
|
|
3300
|
+
// (@deepseek-ai/dsh-host-directory-picker-auto): a loopback-only webserver
|
|
3301
|
+
// bind on a display-bearing host gets `native` — one OS chooser on the
|
|
3302
|
+
// HOST's own screen — while every other bind (all-interfaces/LAN, SSH,
|
|
3303
|
+
// headless) gets `browse`, whose wire verbs are list/createDirectory ONLY
|
|
3304
|
+
// and whose `pick` is refused BY DESIGN with `directory-picker/unavailable`
|
|
3305
|
+
// ("... needs the native capability; the composed picker serves browse").
|
|
3306
|
+
// A flow that hard-assumed the OS chooser therefore dies on every LAN bind
|
|
3307
|
+
// and no workspace can be added at all. Probe the composed backend once per
|
|
3308
|
+
// page and keep BOTH interactions: the OS chooser, and an in-app browser
|
|
3309
|
+
// driven by the browse primitives (see DirectoryBrowseDialog).
|
|
3310
|
+
const pickerState = { kind: 'unknown', probe: null, api: null }
|
|
3311
|
+
/**
|
|
3312
|
+
* True for the refusal a Host serving the OTHER capability throws: the wire
|
|
3313
|
+
* code when the caller still carries it, the Host's fixed sentence
|
|
3314
|
+
* otherwise (uiWorkspace.pickDirectory wraps the RPC failure in a plain
|
|
3315
|
+
* Error, so the code is gone by the time the flow sees it).
|
|
3316
|
+
*/
|
|
3317
|
+
const pickerRefusal = (error) => {
|
|
3318
|
+
if (error && typeof error === 'object' && error.rpcError
|
|
3319
|
+
&& error.rpcError.code === 'directory-picker/unavailable') return true
|
|
3320
|
+
const message = messageOf(error)
|
|
3321
|
+
return message.indexOf('needs the native capability') !== -1
|
|
3322
|
+
|| message.indexOf('directory-picker/unavailable') !== -1
|
|
3323
|
+
}
|
|
3324
|
+
/**
|
|
3325
|
+
* Resolve which backend this page's Host composes. `list` succeeds exactly
|
|
3326
|
+
* on `browse` and is refused with the capability code on `native`, so one
|
|
3327
|
+
* call answers both worlds without ever opening a chooser or creating a
|
|
3328
|
+
* directory. An unclassified failure (a carrier still connecting at boot)
|
|
3329
|
+
* is NOT cached: the next open re-probes, and the flow falls back to trying
|
|
3330
|
+
* the chooser itself.
|
|
3331
|
+
*/
|
|
3332
|
+
const pickerCapabilityNow = () => {
|
|
3333
|
+
if (pickerState.kind !== 'unknown') return Promise.resolve(pickerState.kind)
|
|
3334
|
+
if (pickerState.probe !== null) return pickerState.probe
|
|
3335
|
+
const api = pickerState.api
|
|
3336
|
+
if (!api || typeof api.listDirectory !== 'function') return Promise.resolve('unknown')
|
|
3337
|
+
const probe = Promise.resolve()
|
|
3338
|
+
.then(() => api.listDirectory(undefined, undefined))
|
|
3339
|
+
.then(
|
|
3340
|
+
() => 'browse',
|
|
3341
|
+
(error) => (pickerRefusal(error) ? 'native' : 'unknown'),
|
|
3342
|
+
)
|
|
3343
|
+
pickerState.probe = probe.then((kind) => {
|
|
3344
|
+
if (kind === 'unknown') { pickerState.probe = null; return kind }
|
|
3345
|
+
pickerState.kind = kind
|
|
3346
|
+
return kind
|
|
3347
|
+
})
|
|
3348
|
+
return pickerState.probe
|
|
3349
|
+
}
|
|
3350
|
+
/** Commit a browse verdict the flow discovered the hard way (a refused pick). */
|
|
3351
|
+
const markPickerBrowse = () => {
|
|
3352
|
+
pickerState.kind = 'browse'
|
|
3353
|
+
pickerState.probe = Promise.resolve('browse')
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3034
3356
|
/* ============================ flow dialog ========================= */
|
|
3035
3357
|
|
|
3036
3358
|
const BTN = (props) => (
|
|
@@ -3106,16 +3428,251 @@ window.__ModuleLoader__.load({
|
|
|
3106
3428
|
}
|
|
3107
3429
|
|
|
3108
3430
|
/**
|
|
3109
|
-
* The
|
|
3110
|
-
*
|
|
3111
|
-
*
|
|
3112
|
-
*
|
|
3431
|
+
* The in-app directory browser: the interaction a `browse`-composed Host can
|
|
3432
|
+
* actually serve. It appears when the probe (or a refused pick) proves the
|
|
3433
|
+
* OS chooser is not this Host's backend — a LAN/all-interfaces bind, an SSH
|
|
3434
|
+
* launch, a headless host — where `pick` is refused by design and
|
|
3435
|
+
* list/createDirectory are the only wire verbs. One level at a time: a
|
|
3436
|
+
* clickable Home-rooted breadcrumb chain, an editable path, an inline
|
|
3437
|
+
* new-folder row and the hidden-entry toggle; the listed level (or any
|
|
3438
|
+
* ancestor crumb) is the pick. Browse failures stay inside the dialog — the
|
|
3439
|
+
* owner's error surface belongs to the pick/create conversation, and a
|
|
3440
|
+
* denied listing must never close the flow.
|
|
3441
|
+
*/
|
|
3442
|
+
function DirectoryBrowseDialog(props) {
|
|
3443
|
+
const { open, busy, listDirectory, createDirectory, onPick, onClose, t } = props
|
|
3444
|
+
const [listing, setListing] = React.useState(null)
|
|
3445
|
+
const [loading, setLoading] = React.useState(false)
|
|
3446
|
+
const [error, setError] = React.useState('')
|
|
3447
|
+
const [showHidden, setShowHidden] = React.useState(false)
|
|
3448
|
+
const [editing, setEditing] = React.useState(false)
|
|
3449
|
+
const [draft, setDraft] = React.useState('')
|
|
3450
|
+
const [creating, setCreating] = React.useState(false)
|
|
3451
|
+
const [createName, setCreateName] = React.useState('')
|
|
3452
|
+
const [createBusy, setCreateBusy] = React.useState(false)
|
|
3453
|
+
const [createError, setCreateError] = React.useState('')
|
|
3454
|
+
// Guards a superseded scan: a slow level must not overwrite a newer one.
|
|
3455
|
+
const seqRef = React.useRef(0)
|
|
3456
|
+
|
|
3457
|
+
const go = (path) => {
|
|
3458
|
+
const seq = seqRef.current + 1
|
|
3459
|
+
seqRef.current = seq
|
|
3460
|
+
setLoading(true)
|
|
3461
|
+
setError('')
|
|
3462
|
+
Promise.resolve()
|
|
3463
|
+
.then(() => listDirectory(path, undefined))
|
|
3464
|
+
.then((next) => {
|
|
3465
|
+
if (seqRef.current !== seq) return
|
|
3466
|
+
setListing(next || null)
|
|
3467
|
+
setLoading(false)
|
|
3468
|
+
})
|
|
3469
|
+
.catch((reason) => {
|
|
3470
|
+
if (seqRef.current !== seq) return
|
|
3471
|
+
setLoading(false)
|
|
3472
|
+
setError(messageOf(reason))
|
|
3473
|
+
})
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
React.useEffect(() => {
|
|
3477
|
+
if (!open) return undefined
|
|
3478
|
+
seqRef.current += 1
|
|
3479
|
+
setListing(null)
|
|
3480
|
+
setError('')
|
|
3481
|
+
setEditing(false)
|
|
3482
|
+
setCreating(false)
|
|
3483
|
+
setCreateName('')
|
|
3484
|
+
setCreateBusy(false)
|
|
3485
|
+
setCreateError('')
|
|
3486
|
+
setShowHidden(false)
|
|
3487
|
+
go(undefined)
|
|
3488
|
+
return () => { seqRef.current += 1 }
|
|
3489
|
+
}, [open])
|
|
3490
|
+
|
|
3491
|
+
if (!open) return null
|
|
3492
|
+
|
|
3493
|
+
// Home-rooted chain: inside the host account's home subtree the display
|
|
3494
|
+
// starts at a localized Home crumb, outside it the full ancestry shows.
|
|
3495
|
+
const crumbs = (() => {
|
|
3496
|
+
const all = listing && Array.isArray(listing.crumbs) ? listing.crumbs : []
|
|
3497
|
+
const home = listing && typeof listing.home === 'string' ? listing.home : ''
|
|
3498
|
+
const at = home === '' ? -1 : all.findIndex((crumb) => crumb.path === home)
|
|
3499
|
+
const tail = at === -1 ? all : all.slice(at)
|
|
3500
|
+
return tail.map((crumb, index) => (index === 0 && at !== -1
|
|
3501
|
+
? { path: crumb.path, name: t('browse.home') }
|
|
3502
|
+
: crumb))
|
|
3503
|
+
})()
|
|
3504
|
+
const currentPath = listing && typeof listing.path === 'string' ? listing.path : ''
|
|
3505
|
+
const parentPath = crumbs.length > 1 ? crumbs[crumbs.length - 2].path : ''
|
|
3506
|
+
const entries = (listing && Array.isArray(listing.entries) ? listing.entries : [])
|
|
3507
|
+
.filter((entry) => showHidden || !entry.hidden)
|
|
3508
|
+
|
|
3509
|
+
const submitDraft = () => {
|
|
3510
|
+
const path = draft.trim()
|
|
3511
|
+
setEditing(false)
|
|
3512
|
+
if (path === '') { go(undefined); return }
|
|
3513
|
+
go(path)
|
|
3514
|
+
}
|
|
3515
|
+
const submitCreate = () => {
|
|
3516
|
+
const name = createName.trim()
|
|
3517
|
+
if (name === '' || createBusy || currentPath === '') return
|
|
3518
|
+
setCreateBusy(true)
|
|
3519
|
+
setCreateError('')
|
|
3520
|
+
Promise.resolve()
|
|
3521
|
+
.then(() => createDirectory(currentPath, name))
|
|
3522
|
+
.then(() => {
|
|
3523
|
+
setCreateBusy(false)
|
|
3524
|
+
setCreating(false)
|
|
3525
|
+
setCreateName('')
|
|
3526
|
+
go(currentPath)
|
|
3527
|
+
})
|
|
3528
|
+
.catch((reason) => {
|
|
3529
|
+
setCreateBusy(false)
|
|
3530
|
+
setCreateError(messageOf(reason))
|
|
3531
|
+
})
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
const head = editing
|
|
3535
|
+
? E('div', { className: 'bw-browse-edit-row' },
|
|
3536
|
+
E('input', {
|
|
3537
|
+
className: 'bw-input',
|
|
3538
|
+
value: draft,
|
|
3539
|
+
autoFocus: true,
|
|
3540
|
+
spellCheck: false,
|
|
3541
|
+
placeholder: currentPath,
|
|
3542
|
+
onChange: (e) => setDraft(e.target.value),
|
|
3543
|
+
onKeyDown: (e) => {
|
|
3544
|
+
if (e.key === 'Enter') submitDraft()
|
|
3545
|
+
else if (e.key === 'Escape') setEditing(false)
|
|
3546
|
+
},
|
|
3547
|
+
}),
|
|
3548
|
+
E(BTN, { variant: 'outline', onClick: () => setEditing(false) }, t('cancel')),
|
|
3549
|
+
)
|
|
3550
|
+
: E('div', { className: 'bw-browse-bar' },
|
|
3551
|
+
E('button', {
|
|
3552
|
+
type: 'button',
|
|
3553
|
+
className: 'bw-browse-crumb bw-browse-nav',
|
|
3554
|
+
title: t('browse.up'),
|
|
3555
|
+
'aria-label': t('browse.up'),
|
|
3556
|
+
disabled: parentPath === '',
|
|
3557
|
+
onClick: () => { if (parentPath !== '') go(parentPath) },
|
|
3558
|
+
}, '↑'),
|
|
3559
|
+
E('div', { className: 'bw-browse-crumbs' },
|
|
3560
|
+
crumbs.map((crumb, index) => (index === crumbs.length - 1
|
|
3561
|
+
? E('span', { key: crumb.path, className: 'bw-browse-crumb bw-browse-current', title: crumb.path }, crumb.name)
|
|
3562
|
+
: E('button', {
|
|
3563
|
+
key: crumb.path,
|
|
3564
|
+
type: 'button',
|
|
3565
|
+
className: 'bw-browse-crumb',
|
|
3566
|
+
title: crumb.path,
|
|
3567
|
+
onClick: () => go(crumb.path),
|
|
3568
|
+
}, crumb.name))),
|
|
3569
|
+
),
|
|
3570
|
+
E('button', {
|
|
3571
|
+
type: 'button',
|
|
3572
|
+
className: 'bw-browse-edit',
|
|
3573
|
+
title: t('browse.editPath'),
|
|
3574
|
+
'aria-label': t('browse.editPath'),
|
|
3575
|
+
onClick: () => { setDraft(currentPath); setEditing(true) },
|
|
3576
|
+
}, icon('IconEditOutline16')),
|
|
3577
|
+
)
|
|
3578
|
+
|
|
3579
|
+
const rows = entries.map((entry) => E('button', {
|
|
3580
|
+
key: entry.path,
|
|
3581
|
+
type: 'button',
|
|
3582
|
+
className: 'bw-browse-row',
|
|
3583
|
+
title: entry.path,
|
|
3584
|
+
onClick: () => go(entry.path),
|
|
3585
|
+
}, icon('IconFolderClose16'), E('span', { className: 'bw-browse-name' }, entry.name)))
|
|
3586
|
+
|
|
3587
|
+
const body = loading
|
|
3588
|
+
? E('div', { className: 'bw-browse-note' }, t('browse.loading'))
|
|
3589
|
+
: rows.length > 0
|
|
3590
|
+
? rows
|
|
3591
|
+
: E('div', { className: 'bw-browse-note' }, t('browse.empty'))
|
|
3592
|
+
|
|
3593
|
+
const newRow = creating
|
|
3594
|
+
? E('div', { className: 'bw-browse-new-row' },
|
|
3595
|
+
E('input', {
|
|
3596
|
+
className: 'bw-input',
|
|
3597
|
+
value: createName,
|
|
3598
|
+
autoFocus: true,
|
|
3599
|
+
spellCheck: false,
|
|
3600
|
+
placeholder: t('browse.folderName'),
|
|
3601
|
+
disabled: createBusy,
|
|
3602
|
+
onChange: (e) => setCreateName(e.target.value),
|
|
3603
|
+
onKeyDown: (e) => {
|
|
3604
|
+
if (e.key === 'Enter') submitCreate()
|
|
3605
|
+
else if (e.key === 'Escape') { setCreating(false); setCreateError('') }
|
|
3606
|
+
},
|
|
3607
|
+
}),
|
|
3608
|
+
E(BTN, {
|
|
3609
|
+
variant: 'outline',
|
|
3610
|
+
disabled: createBusy,
|
|
3611
|
+
onClick: () => { setCreating(false); setCreateError('') },
|
|
3612
|
+
}, t('cancel')),
|
|
3613
|
+
E(BTN, { variant: 'primary', onClick: submitCreate, disabled: createBusy },
|
|
3614
|
+
createBusy ? t('flow.creating') : t('create')),
|
|
3615
|
+
)
|
|
3616
|
+
: null
|
|
3617
|
+
|
|
3618
|
+
// The dialog is deliberately narrower than the official browser, so the
|
|
3619
|
+
// two browsing tools sit on their own row above the list instead of
|
|
3620
|
+
// sharing the footer with the commit pair (they used to wrap there).
|
|
3621
|
+
const tools = E('div', { className: 'bw-browse-tools' },
|
|
3622
|
+
E('button', {
|
|
3623
|
+
type: 'button',
|
|
3624
|
+
className: cls('bw-browse-tool', showHidden && 'bw-browse-tool-on'),
|
|
3625
|
+
'aria-pressed': showHidden ? 'true' : 'false',
|
|
3626
|
+
onClick: () => setShowHidden((value) => !value),
|
|
3627
|
+
}, t('browse.showHidden')),
|
|
3628
|
+
E('button', {
|
|
3629
|
+
type: 'button',
|
|
3630
|
+
className: 'bw-browse-tool',
|
|
3631
|
+
disabled: creating || currentPath === '',
|
|
3632
|
+
onClick: () => { setCreating(true); setCreateName(''); setCreateError('') },
|
|
3633
|
+
}, t('browse.newFolder')),
|
|
3634
|
+
)
|
|
3635
|
+
|
|
3636
|
+
const footer = E('div', { className: 'bw-browse-actions' },
|
|
3637
|
+
E(BTN, { variant: 'outline', onClick: onClose, disabled: busy === true }, t('cancel')),
|
|
3638
|
+
E(BTN, {
|
|
3639
|
+
variant: 'primary',
|
|
3640
|
+
disabled: busy === true || currentPath === '',
|
|
3641
|
+
onClick: () => { if (currentPath !== '') onPick(currentPath) },
|
|
3642
|
+
}, t('browse.select')),
|
|
3643
|
+
)
|
|
3644
|
+
|
|
3645
|
+
return E(ui.Modal, {
|
|
3646
|
+
open: true,
|
|
3647
|
+
onClose: () => { if (busy !== true) onClose() },
|
|
3648
|
+
closeLabel: t('close'),
|
|
3649
|
+
title: t('browse.title'),
|
|
3650
|
+
footer,
|
|
3651
|
+
},
|
|
3652
|
+
E('div', { className: 'bw-modal-body bw-browse-body' },
|
|
3653
|
+
head,
|
|
3654
|
+
createError !== '' ? E('div', { className: 'bw-error-text', role: 'alert' }, createError) : null,
|
|
3655
|
+
tools,
|
|
3656
|
+
newRow,
|
|
3657
|
+
E('div', { className: 'bw-browse-list', role: 'list' }, body),
|
|
3658
|
+
listing && listing.truncated === true ? E('div', { className: 'bw-hint' }, t('browse.truncated')) : null,
|
|
3659
|
+
error !== '' ? E('div', { className: 'bw-error-text', role: 'alert' }, error) : null,
|
|
3660
|
+
),
|
|
3661
|
+
StyleNode())
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
/**
|
|
3665
|
+
* The add-workspace picking interaction: whichever directory interaction the
|
|
3666
|
+
* Host composes (OS chooser or in-app browser), then a small parent-group
|
|
3667
|
+
* popup, then create + rename with the chosen prefix. Works as a
|
|
3668
|
+
* directoryFlow occupant (owner conversation props) and as the browser's
|
|
3669
|
+
* directly composed flow (same props, owner state lives above).
|
|
3113
3670
|
*/
|
|
3114
3671
|
function BetterFlow(props) {
|
|
3115
|
-
const { open, busy, onPicked, onCancel, onError, createWorkspace, renameWorkspace, pickDirectory, useWorkspaces, useStore, t } = props
|
|
3672
|
+
const { open, busy, onPicked, onCancel, onError, createWorkspace, renameWorkspace, pickDirectory, listDirectory, createDirectory, useWorkspaces, useStore, t } = props
|
|
3116
3673
|
const initialParent = props.initialParent || ''
|
|
3117
3674
|
const actions = props.actions
|
|
3118
|
-
const [phase, setPhase] = React.useState('idle') // idle | picking | picked | submitting
|
|
3675
|
+
const [phase, setPhase] = React.useState('idle') // idle | picking | browsing | picked | submitting
|
|
3119
3676
|
const [pickedPath, setPickedPath] = React.useState('')
|
|
3120
3677
|
const [parentInput, setParentInput] = React.useState('')
|
|
3121
3678
|
// All hooks run before any early return: the flow unmounts its dialog
|
|
@@ -3134,24 +3691,54 @@ window.__ModuleLoader__.load({
|
|
|
3134
3691
|
}
|
|
3135
3692
|
let alive = true
|
|
3136
3693
|
setPhase('picking')
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3694
|
+
// Which backend this Host composes decides the whole interaction: a
|
|
3695
|
+
// `browse` Host renders the in-app browser directly (its `pick` is
|
|
3696
|
+
// refused by design, so trying the chooser first would only surface an
|
|
3697
|
+
// error), a `native` Host opens the OS chooser, and an unresolved
|
|
3698
|
+
// verdict still tries the chooser so a silent probe never changes the
|
|
3699
|
+
// behavior this plugin had before the probe existed.
|
|
3700
|
+
pickerCapabilityNow()
|
|
3701
|
+
.then((kind) => {
|
|
3702
|
+
if (!alive) return undefined
|
|
3703
|
+
if (kind === 'browse') { setPhase('browsing'); return undefined }
|
|
3704
|
+
return Promise.resolve()
|
|
3705
|
+
.then(() => pickDirectory())
|
|
3706
|
+
.then((path) => {
|
|
3707
|
+
if (!alive) return
|
|
3708
|
+
if (!path) { onCancel(); return }
|
|
3709
|
+
setPickedPath(String(path))
|
|
3710
|
+
setParentInput(initialParent)
|
|
3711
|
+
setPhase('picked')
|
|
3712
|
+
})
|
|
3145
3713
|
})
|
|
3146
3714
|
.catch((reason) => {
|
|
3147
3715
|
if (!alive) return
|
|
3716
|
+
// A refusal is a backend fact, not a failure: this Host serves the
|
|
3717
|
+
// browse verbs, so switch interactions instead of erroring out.
|
|
3718
|
+
if (pickerRefusal(reason)) { markPickerBrowse(); setPhase('browsing'); return }
|
|
3148
3719
|
setPhase('idle')
|
|
3149
3720
|
onError(messageOf(reason))
|
|
3150
3721
|
})
|
|
3151
3722
|
return () => { alive = false }
|
|
3152
3723
|
}, [open])
|
|
3153
3724
|
|
|
3154
|
-
if (!open
|
|
3725
|
+
if (!open) return null
|
|
3726
|
+
if (phase === 'browsing') {
|
|
3727
|
+
return E(DirectoryBrowseDialog, {
|
|
3728
|
+
open: true,
|
|
3729
|
+
busy: busy === true,
|
|
3730
|
+
listDirectory,
|
|
3731
|
+
createDirectory,
|
|
3732
|
+
onPick: (path) => {
|
|
3733
|
+
setPickedPath(String(path))
|
|
3734
|
+
setParentInput(initialParent)
|
|
3735
|
+
setPhase('picked')
|
|
3736
|
+
},
|
|
3737
|
+
onClose: onCancel,
|
|
3738
|
+
t,
|
|
3739
|
+
})
|
|
3740
|
+
}
|
|
3741
|
+
if (phase !== 'picked' && phase !== 'submitting') return null
|
|
3155
3742
|
|
|
3156
3743
|
const folderOptions = (() => {
|
|
3157
3744
|
const set = new Set(storeFolders)
|
|
@@ -3954,7 +4541,8 @@ window.__ModuleLoader__.load({
|
|
|
3954
4541
|
useSessions, useSessionPendingInteraction, useWorkspaces,
|
|
3955
4542
|
useStore, actions,
|
|
3956
4543
|
startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace,
|
|
3957
|
-
archiveSession, createWorkspace, pickDirectory,
|
|
4544
|
+
archiveSession, createWorkspace, pickDirectory, listDirectory, createDirectory,
|
|
4545
|
+
insertWorkspaceBefore, insertSessionBefore,
|
|
3958
4546
|
t,
|
|
3959
4547
|
} = props
|
|
3960
4548
|
|
|
@@ -5007,6 +5595,8 @@ window.__ModuleLoader__.load({
|
|
|
5007
5595
|
createWorkspace,
|
|
5008
5596
|
renameWorkspace,
|
|
5009
5597
|
pickDirectory,
|
|
5598
|
+
listDirectory,
|
|
5599
|
+
createDirectory,
|
|
5010
5600
|
useWorkspaces,
|
|
5011
5601
|
actions,
|
|
5012
5602
|
useStore,
|
|
@@ -5092,6 +5682,10 @@ window.__ModuleLoader__.load({
|
|
|
5092
5682
|
})
|
|
5093
5683
|
return
|
|
5094
5684
|
}
|
|
5685
|
+
// The capability probe talks to the composed backend through this
|
|
5686
|
+
// service (list is served by browse alone), so the state module needs
|
|
5687
|
+
// the handle before any flow can open.
|
|
5688
|
+
pickerState.api = uiWorkspace
|
|
5095
5689
|
|
|
5096
5690
|
if (ctx.locale && typeof ctx.locale.register === 'function') {
|
|
5097
5691
|
ctx.effect(() => {
|
|
@@ -5143,6 +5737,10 @@ window.__ModuleLoader__.load({
|
|
|
5143
5737
|
archiveSession: (sessionId) => uiWorkspace.archiveSession(sessionId),
|
|
5144
5738
|
createWorkspace: (input) => workspaces.create(input),
|
|
5145
5739
|
pickDirectory: () => uiWorkspace.pickDirectory(),
|
|
5740
|
+
// Browse-side primitives: the fallback interaction whenever the Host
|
|
5741
|
+
// composes the browse backend instead of the OS chooser.
|
|
5742
|
+
listDirectory: (path, signal) => uiWorkspace.listDirectory(path, signal),
|
|
5743
|
+
createDirectory: (path, name) => uiWorkspace.createDirectory(path, name),
|
|
5146
5744
|
hooks: {
|
|
5147
5745
|
directoryFlow: flowSource(slots, 'sidebar.workspaces.directoryFlow'),
|
|
5148
5746
|
},
|
|
@@ -5151,6 +5749,10 @@ window.__ModuleLoader__.load({
|
|
|
5151
5749
|
createWorkspace: (input) => workspaces.create(input),
|
|
5152
5750
|
renameWorkspace: (workspaceId, title) => workspaces.rename(workspaceId, title),
|
|
5153
5751
|
pickDirectory: () => uiWorkspace.pickDirectory(),
|
|
5752
|
+
// Same browse-side primitives as the sidebar flow: the two holes and
|
|
5753
|
+
// the inlined dialog must agree on which interaction they can run.
|
|
5754
|
+
listDirectory: (path, signal) => uiWorkspace.listDirectory(path, signal),
|
|
5755
|
+
createDirectory: (path, name) => uiWorkspace.createDirectory(path, name),
|
|
5154
5756
|
hooks: { directoryFlow: flowSource(slots, hole) },
|
|
5155
5757
|
})
|
|
5156
5758
|
|