dsh-better-workspace 0.15.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-better-workspace",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "main": "./src/index.js",
5
5
  "description": "侧边栏工作区树:按官方磁盘目录嵌套(官方 workspace-tree 语义),可选按名称中的 / 分组(默认关闭,设置里开启);添加工作区由官方目录流承载。Workspace tree for the DSH sidebar workspace list, nested by official disk folders with optional \"/\"-name grouping.",
6
6
  "engines": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-better-workspace",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "侧边栏工作区树:按官方磁盘目录嵌套(官方 workspace-tree 语义),可选按名称中的 / 分组(默认关闭,设置里开启);添加工作区走官方目录流。Workspace tree for the DSH sidebar: disk-path nesting (official workspace-tree semantics), with optional \"/\"-name grouping (off by default); add-workspace rides the official directory flow.",
5
5
  "type": "module",
6
6
  "repository": {
package/src/client.js CHANGED
@@ -152,6 +152,7 @@ window.__ModuleLoader__.load({
152
152
  'folder.rename.hint': '重命名会同步更新组内所有工作区名称',
153
153
  'folder.error.empty': '分组路径不能为空',
154
154
  'group.move.title': '移动到分组',
155
+ 'group.pick': '选择已有分组',
155
156
  'group.new': '新建分组',
156
157
  }
157
158
 
@@ -276,6 +277,7 @@ window.__ModuleLoader__.load({
276
277
  'folder.rename.hint': 'Renaming updates every workspace title inside the folder',
277
278
  'folder.error.empty': 'Folder path must not be empty',
278
279
  'group.move.title': 'Move to group',
280
+ 'group.pick': 'Pick an existing group',
279
281
  'group.new': 'New group',
280
282
  }
281
283
 
@@ -408,6 +410,7 @@ window.__ModuleLoader__.load({
408
410
  'folder.rename.hint': 'تحديث الاسم يعيد تسمية كل مساحات العمل داخل المجموعة',
409
411
  'folder.error.empty': 'لا يمكن أن يكون مسار المجموعة فارغاً',
410
412
  'group.move.title': 'نقل إلى مجموعة',
413
+ 'group.pick': 'اختر مجموعة موجودة',
411
414
  'group.new': 'مجموعة جديدة',
412
415
  },
413
416
  /* locale: de */
@@ -532,6 +535,7 @@ window.__ModuleLoader__.load({
532
535
  'folder.rename.hint': 'Umbenennen aktualisiert alle Arbeitsbereichsnamen im Ordner',
533
536
  'folder.error.empty': 'Der Ordnerpfad darf nicht leer sein',
534
537
  'group.move.title': 'In Gruppe verschieben',
538
+ 'group.pick': 'Vorhandene Gruppe wählen',
535
539
  'group.new': 'Neue Gruppe',
536
540
  },
537
541
  /* locale: fr */
@@ -656,6 +660,7 @@ window.__ModuleLoader__.load({
656
660
  'folder.rename.hint': 'Le renommage met à jour tous les espaces de travail du dossier',
657
661
  'folder.error.empty': 'Le chemin du dossier ne doit pas être vide',
658
662
  'group.move.title': 'Déplacer vers le groupe',
663
+ 'group.pick': 'Choisir un groupe existant',
659
664
  'group.new': 'Nouveau groupe',
660
665
  },
661
666
  /* locale: hi */
@@ -780,6 +785,7 @@ window.__ModuleLoader__.load({
780
785
  'folder.rename.hint': 'नाम बदलने पर समूह के सभी कार्यस्थानों के नाम भी बदल जाते हैं',
781
786
  'folder.error.empty': 'समूह पथ खाली नहीं हो सकता',
782
787
  'group.move.title': 'समूह में ले जाएँ',
788
+ 'group.pick': 'मौजूदा समूह चुनें',
783
789
  'group.new': 'नया समूह',
784
790
  },
785
791
  /* locale: id */
@@ -904,6 +910,7 @@ window.__ModuleLoader__.load({
904
910
  'folder.rename.hint': 'Mengganti nama akan memperbarui semua ruang kerja di folder',
905
911
  'folder.error.empty': 'Jalur folder tidak boleh kosong',
906
912
  'group.move.title': 'Pindahkan ke grup',
913
+ 'group.pick': 'Pilih grup yang ada',
907
914
  'group.new': 'Grup baru',
908
915
  },
909
916
  /* locale: it */
@@ -1028,6 +1035,7 @@ window.__ModuleLoader__.load({
1028
1035
  'folder.rename.hint': 'La rinomina aggiorna tutte le aree di lavoro nella cartella',
1029
1036
  'folder.error.empty': 'Il percorso della cartella non può essere vuoto',
1030
1037
  'group.move.title': 'Sposta nel gruppo',
1038
+ 'group.pick': 'Scegli un gruppo esistente',
1031
1039
  'group.new': 'Nuovo gruppo',
1032
1040
  },
1033
1041
  /* locale: ja */
@@ -1152,6 +1160,7 @@ window.__ModuleLoader__.load({
1152
1160
  'folder.rename.hint': '名前を変更すると,フォルダー内のすべてのワークスペース名も更新されます',
1153
1161
  'folder.error.empty': 'フォルダーのパスを入力してください',
1154
1162
  'group.move.title': 'グループに移動',
1163
+ 'group.pick': '既存のグループを選択',
1155
1164
  'group.new': '新しいグループ',
1156
1165
  },
1157
1166
  /* locale: ko */
@@ -1276,6 +1285,7 @@ window.__ModuleLoader__.load({
1276
1285
  'folder.rename.hint': '이름을 바꾸면 폴더 안 모든 워크스페이스 이름도 함께 바뀝니다',
1277
1286
  'folder.error.empty': '폴더 경로를 입력해야 합니다',
1278
1287
  'group.move.title': '그룹으로 이동',
1288
+ 'group.pick': '기존 그룹 선택',
1279
1289
  'group.new': '새 그룹',
1280
1290
  },
1281
1291
  /* locale: nl */
@@ -1400,6 +1410,7 @@ window.__ModuleLoader__.load({
1400
1410
  'folder.rename.hint': 'Naam wijzigen werkt alle werkruimtenamen in de map bij',
1401
1411
  'folder.error.empty': 'Het mappad mag niet leeg zijn',
1402
1412
  'group.move.title': 'Naar groep verplaatsen',
1413
+ 'group.pick': 'Bestaande groep kiezen',
1403
1414
  'group.new': 'Nieuwe groep',
1404
1415
  },
1405
1416
  /* locale: pl */
@@ -1524,6 +1535,7 @@ window.__ModuleLoader__.load({
1524
1535
  'folder.rename.hint': 'Zmiana nazwy zaktualizuje wszystkie obszary robocze w folderze',
1525
1536
  'folder.error.empty': 'Ścieżka folderu nie może być pusta',
1526
1537
  'group.move.title': 'Przenieś do grupy',
1538
+ 'group.pick': 'Wybierz istniejącą grupę',
1527
1539
  'group.new': 'Nowa grupa',
1528
1540
  },
1529
1541
  /* locale: pt */
@@ -1648,6 +1660,7 @@ window.__ModuleLoader__.load({
1648
1660
  'folder.rename.hint': 'Renomear atualiza todos os espaços de trabalho da pasta',
1649
1661
  'folder.error.empty': 'O caminho da pasta não pode ficar vazio',
1650
1662
  'group.move.title': 'Mover para o grupo',
1663
+ 'group.pick': 'Escolher um grupo existente',
1651
1664
  'group.new': 'Novo grupo',
1652
1665
  },
1653
1666
  /* locale: ru */
@@ -1772,6 +1785,7 @@ window.__ModuleLoader__.load({
1772
1785
  'folder.rename.hint': 'Переименование обновит все рабочие области в папке',
1773
1786
  'folder.error.empty': 'Путь папки не может быть пустым',
1774
1787
  'group.move.title': 'Переместить в группу',
1788
+ 'group.pick': 'Выбрать существующую группу',
1775
1789
  'group.new': 'Новая группа',
1776
1790
  },
1777
1791
  /* locale: sv */
@@ -1896,6 +1910,7 @@ window.__ModuleLoader__.load({
1896
1910
  'folder.rename.hint': 'Namnbytet uppdaterar alla arbetsytor i mappen',
1897
1911
  'folder.error.empty': 'Mappsökvägen får inte vara tom',
1898
1912
  'group.move.title': 'Flytta till grupp',
1913
+ 'group.pick': 'Välj en befintlig grupp',
1899
1914
  'group.new': 'Ny grupp',
1900
1915
  },
1901
1916
  /* locale: th */
@@ -2020,6 +2035,7 @@ window.__ModuleLoader__.load({
2020
2035
  'folder.rename.hint': 'การเปลี่ยนชื่อจะอัปเดตพื้นที่ทำงานทั้งหมดในกลุ่ม',
2021
2036
  'folder.error.empty': 'เส้นทางกลุ่มต้องไม่ว่าง',
2022
2037
  'group.move.title': 'ย้ายไปยังกลุ่ม',
2038
+ 'group.pick': 'เลือกกลุ่มที่มีอยู่',
2023
2039
  'group.new': 'กลุ่มใหม่',
2024
2040
  },
2025
2041
  /* locale: tr */
@@ -2144,6 +2160,7 @@ window.__ModuleLoader__.load({
2144
2160
  'folder.rename.hint': 'Yeniden adlandırma klasördeki tüm çalışma alanlarını günceller',
2145
2161
  'folder.error.empty': 'Klasör yolu boş olamaz',
2146
2162
  'group.move.title': 'Gruba taşı',
2163
+ 'group.pick': 'Mevcut bir grup seç',
2147
2164
  'group.new': 'Yeni grup',
2148
2165
  },
2149
2166
  /* locale: vi */
@@ -2268,6 +2285,7 @@ window.__ModuleLoader__.load({
2268
2285
  'folder.rename.hint': 'Đổi tên sẽ cập nhật mọi không gian làm việc trong nhóm',
2269
2286
  'folder.error.empty': 'Đường dẫn nhóm không được để trống',
2270
2287
  'group.move.title': 'Chuyển vào nhóm',
2288
+ 'group.pick': 'Chọn một nhóm có sẵn',
2271
2289
  'group.new': 'Nhóm mới',
2272
2290
  },
2273
2291
  /* locale: zh-HK */
@@ -2392,6 +2410,7 @@ window.__ModuleLoader__.load({
2392
2410
  'folder.rename.hint': '重新命名會同步更新組內所有工作區名稱',
2393
2411
  'folder.error.empty': '分組路徑唔可以係空',
2394
2412
  'group.move.title': '移動到分組',
2413
+ 'group.pick': '選擇已有分組',
2395
2414
  'group.new': '新增分組',
2396
2415
  },
2397
2416
  /* locale: zh-MO */
@@ -2516,6 +2535,7 @@ window.__ModuleLoader__.load({
2516
2535
  'folder.rename.hint': '重新命名會同步更新組內所有工作區名稱',
2517
2536
  'folder.error.empty': '分組路徑唔可以係空',
2518
2537
  'group.move.title': '移動到分組',
2538
+ 'group.pick': '選擇已有分組',
2519
2539
  'group.new': '新增分組',
2520
2540
  },
2521
2541
  /* locale: zh-TW */
@@ -2640,6 +2660,7 @@ window.__ModuleLoader__.load({
2640
2660
  'folder.rename.hint': '重新命名會同步更新群組內所有工作區名稱',
2641
2661
  'folder.error.empty': '群組路徑不能為空',
2642
2662
  'group.move.title': '移動到分組',
2663
+ 'group.pick': '選擇現有群組',
2643
2664
  'group.new': '新增分組',
2644
2665
  },
2645
2666
  }
@@ -2986,6 +3007,23 @@ window.__ModuleLoader__.load({
2986
3007
  }
2987
3008
  return map
2988
3009
  }
3010
+ /**
3011
+ * The row entry the official 'workspace' grouping mode renders.
3012
+ *
3013
+ * A raw WorkspaceView carries no `leaf`, and WorkspaceRow renders exactly
3014
+ * that field as its label (the tree path fills it while building) — so
3015
+ * handing the raw item to renderWorkspaceEntry, as v0.13 did, drew every
3016
+ * row with its icon and sessions but NO NAME AT ALL. Reported 2026-09-19.
3017
+ *
3018
+ * The label is the FULL title: this mode renders no name groups, so a "/"
3019
+ * has nothing to group into and must stay in the text.
3020
+ */
3021
+ const flatWorkspaceEntry = (workspace) => ({
3022
+ ...workspace,
3023
+ leaf: String(workspace.title || '') || basename(workspace.path) || String(workspace.workspaceId || ''),
3024
+ folderPath: '',
3025
+ })
3026
+
2989
3027
  function buildTree(items, groupBySlash) {
2990
3028
  const list = items || []
2991
3029
  const diskParentOf = diskParentMapOf(list)
@@ -3127,6 +3165,18 @@ window.__ModuleLoader__.load({
3127
3165
  '.bw-input{width:100%;box-sizing:border-box;height:26px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.1));border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.25));border-radius:6px;color:inherit;padding:0 8px;font-size:12px;outline:none;font-family:inherit}',
3128
3166
  '.bw-input:focus{border-color:var(--dsw-alias-brand-primary,#5b8def)}',
3129
3167
  '.bw-input::placeholder{color:var(--dsw-alias-label-quaternary,#8a8a8a)}',
3168
+ // Dropdown triggers (MenuPicker): the same surface as .bw-input so a
3169
+ // picker and a text field read as one family.
3170
+ '.bw-select{display:flex;align-items:center;gap:6px;width:100%;box-sizing:border-box;height:26px;padding:0 8px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.1));border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.25));border-radius:6px;color:inherit;font-size:12px;font-family:inherit;cursor:pointer;text-align:left}',
3171
+ '.bw-select:hover{border-color:var(--dsw-alias-brand-primary,#5b8def)}',
3172
+ '.bw-select-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
3173
+ '.bw-select-chevron{flex:none;display:inline-flex;align-items:center;color:var(--dsw-alias-label-quaternary,#8a8a8a);font-size:10px;line-height:1}',
3174
+ '.bw-select-icon{width:26px;flex:none;justify-content:center;padding:0}',
3175
+ '.bw-combo{display:flex;align-items:center;gap:6px}',
3176
+ // The Menu root is a <span>; these make it fill the field, or sit inline
3177
+ // as the chevron-only button beside an input.
3178
+ '.bw-menu-block{display:block;width:100%}',
3179
+ '.bw-menu-inline{display:inline-block;width:auto;flex:none}',
3130
3180
  '.bw-tree{flex:1;overflow-y:auto;overflow-x:hidden;padding:2px 6px 12px;min-height:0}',
3131
3181
  '.bw-row{display:flex;align-items:center;gap:6px;min-height:28px;padding:0 6px;border-radius:6px;cursor:pointer;user-select:none;font-size:13px;color:var(--dsw-alias-label-primary,#e6e6e6);position:relative}',
3132
3182
  '.bw-drop-before::after{content:"";position:absolute;left:8px;right:8px;top:-1px;height:2px;border-radius:1px;background:var(--dsw-alias-brand-primary,#5b8def);pointer-events:none}',
@@ -3610,6 +3660,52 @@ window.__ModuleLoader__.load({
3610
3660
  }, E('div', { className: 'bw-modal-body' }, E('div', { className: 'bw-hint' }, body)), StyleNode())
3611
3661
  }
3612
3662
 
3663
+ /**
3664
+ * One picker over the official primitives Menu, used wherever this plugin
3665
+ * would otherwise reach for a native <select> / <datalist>.
3666
+ *
3667
+ * WHY NOT NATIVE: the OS draws a native select's popup itself, so over this
3668
+ * plugin's translucent panel it comes back as a light system list that
3669
+ * ignores the theme entirely. dsh-ide-git hit the same defect twice and
3670
+ * replaced its selects with a button-over-menu for exactly that reason (see
3671
+ * the RepoSelect / FilterSelect notes there) — but it had to lay that menu
3672
+ * out INSIDE the panel, because dsh-better-sidebar declares
3673
+ * `contain: layout` and would strand a portal off-screen. This plugin sits
3674
+ * in a native DSH sidebar slot with no such constraint, so it can use the
3675
+ * real primitives Menu: official look and keyboard model, portal above the
3676
+ * modal (z-index 1100 vs 1000), and its own scrolling list — which the
3677
+ * workspace x session target list needs.
3678
+ *
3679
+ * Degradation: callers keep a native control for hosts whose primitives
3680
+ * have no Menu, so picking never becomes impossible.
3681
+ */
3682
+ function MenuPicker({ options, value, onPick, placeholder, chevronOnly, title, t }) {
3683
+ const [open, setOpen] = React.useState(false)
3684
+ const list = Array.isArray(options) ? options : []
3685
+ let current = null
3686
+ for (const option of list) { if (String(option.value) === String(value)) { current = option; break } }
3687
+ const chevron = icon('IconChevronDownOutline14', 14)
3688
+ return E(ui.Menu, {
3689
+ open,
3690
+ onClose: () => { setOpen(false) },
3691
+ items: list.map((option) => ({ id: String(option.value), label: String(option.label) })),
3692
+ selectedId: current === null ? undefined : String(current.value),
3693
+ onSelect: (id) => { setOpen(false); onPick(id) },
3694
+ align: 'start',
3695
+ dense: true,
3696
+ portal: true,
3697
+ className: cls('bw-menu-block', chevronOnly && 'bw-menu-inline'),
3698
+ anchor: E('button', {
3699
+ type: 'button',
3700
+ className: cls('bw-select', chevronOnly && 'bw-select-icon'),
3701
+ title: title === undefined ? undefined : title,
3702
+ onClick: () => setOpen(v => !v),
3703
+ },
3704
+ chevronOnly ? null : E('span', { className: 'bw-select-label' }, current === null ? (placeholder || '') : String(current.label)),
3705
+ E('span', { className: 'bw-select-chevron' }, chevron || '\u25be')),
3706
+ })
3707
+ }
3708
+
3613
3709
  /**
3614
3710
  * "Move to group…": the discoverable front door for the name-projection
3615
3711
  * grouping this plugin has always had (no empty groups — moving an entry
@@ -3617,8 +3713,9 @@ window.__ModuleLoader__.load({
3617
3713
  * - a free target (the header "New group" button, or a row drop on it)
3618
3714
  * locks the entry and only asks for the group name;
3619
3715
  * - the picker shape (header button click) asks which entry first.
3620
- * The group name input is backed by a datalist of every existing group
3621
- * path, so the dropdown doubles as the discovery surface.
3716
+ * Both dropdowns are MenuPicker (see its note): a native select's popup and
3717
+ * a native datalist's suggestions are drawn by the OS and ignore the panel
3718
+ * theme, which is exactly what users reported here.
3622
3719
  */
3623
3720
  function GroupDialog({ title, hint, initial, targets, groupPaths, onConfirm, onClose, t }) {
3624
3721
  const hasTargets = Array.isArray(targets) && targets.length > 0
@@ -3628,6 +3725,7 @@ window.__ModuleLoader__.load({
3628
3725
  React.useEffect(() => { if (inputRef.current) { inputRef.current.focus(); inputRef.current.select() } }, [])
3629
3726
  const commit = () => onConfirm(hasTargets ? targetValue : undefined, value)
3630
3727
  const hintList = Array.isArray(groupPaths) ? groupPaths.filter((p) => p !== '') : []
3728
+ const menuReady = typeof ui.Menu === 'function'
3631
3729
  return E(ui.Modal, {
3632
3730
  open: true,
3633
3731
  onClose,
@@ -3641,24 +3739,42 @@ window.__ModuleLoader__.load({
3641
3739
  E('div', { className: 'bw-modal-body' },
3642
3740
  hasTargets
3643
3741
  ? E('div', { className: 'bw-field' },
3644
- E('select', {
3645
- className: 'bw-input',
3646
- defaultValue: targets[0].value,
3647
- onChange: (e) => setTargetValue(e.target.value),
3648
- }, targets.map((item) => E('option', { key: String(item.value), value: String(item.value) }, item.label))),
3742
+ menuReady
3743
+ ? E(MenuPicker, {
3744
+ options: targets.map((item) => ({ value: item.value, label: item.label })),
3745
+ value: targetValue,
3746
+ onPick: setTargetValue,
3747
+ t,
3748
+ })
3749
+ : E('select', {
3750
+ className: 'bw-input',
3751
+ defaultValue: targets[0].value,
3752
+ onChange: (e) => setTargetValue(e.target.value),
3753
+ }, targets.map((item) => E('option', { key: String(item.value), value: String(item.value) }, item.label))),
3649
3754
  )
3650
3755
  : null,
3651
3756
  E('div', { className: 'bw-field' },
3652
- E('input', {
3653
- ref: inputRef,
3654
- className: 'bw-input',
3655
- list: 'bw-group-paths',
3656
- value,
3657
- onChange: (e) => setValue(e.target.value),
3658
- onKeyDown: (e) => { if (e.key === 'Enter') commit() },
3659
- }),
3660
- E('datalist', { id: 'bw-group-paths' },
3661
- hintList.map((path) => E('option', { key: String(path), value: String(path) })),
3757
+ E('div', { className: 'bw-combo' },
3758
+ E('input', {
3759
+ ref: inputRef,
3760
+ className: 'bw-input',
3761
+ value,
3762
+ onChange: (e) => setValue(e.target.value),
3763
+ onKeyDown: (e) => { if (e.key === 'Enter') commit() },
3764
+ }),
3765
+ // The old datalist's suggestions, as a real menu: one chevron
3766
+ // button beside the field. Typing a brand-new group name still
3767
+ // works exactly as before.
3768
+ (menuReady && hintList.length > 0)
3769
+ ? E(MenuPicker, {
3770
+ options: hintList.map((path) => ({ value: path, label: path })),
3771
+ value,
3772
+ onPick: setValue,
3773
+ chevronOnly: true,
3774
+ title: t('group.pick'),
3775
+ t,
3776
+ })
3777
+ : null,
3662
3778
  ),
3663
3779
  hint ? E('div', { className: 'bw-hint' }, hint) : null,
3664
3780
  ),
@@ -6023,7 +6139,11 @@ window.__ModuleLoader__.load({
6023
6139
  // the root, no name groups, no disk nesting, no chain compression.
6024
6140
  // Session rows keep the slash toggle; disk-nested children do not
6025
6141
  // recurse here because raw items carry no .sub level.
6026
- for (const workspace of items || []) bodyRows.push(...renderWorkspaceEntry({ workspace }, 0, wsPulseOf(workspace)))
6142
+ // flatWorkspaceEntry, never the raw item: see its note the raw one
6143
+ // has no `leaf` and the row would render nameless.
6144
+ for (const workspace of items || []) {
6145
+ bodyRows.push(...renderWorkspaceEntry({ workspace: flatWorkspaceEntry(workspace) }, 0, wsPulseOf(workspace)))
6146
+ }
6027
6147
  bodyRows.push(...renderUngroupedBlock())
6028
6148
  } else {
6029
6149
  for (const folder of tree.folders) bodyRows.push(...renderPlainFolder(folder, 0))