dsh-better-workspace 0.17.2 → 0.19.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/README.md CHANGED
@@ -69,7 +69,7 @@
69
69
  ### 其余亮点
70
70
 
71
71
  - **文件夹式收起**:点工作区行 = 收起/展开该工作区的全部会话(收起后行上保留会话数角标);会话分组同样可收起;搜索时自动全部展开。
72
- - **拖拽还原原版能力**:工作区行可拖拽——拖到另一工作区行上/下半即调整顺序(写回宿主 registry 序),拖到分组行则移动进该分组(改写名称前缀);会话行可拖拽——同工作区拖到另一会话上/下调整顺序,拖到会话子分组即移动进该分组。显示顺序 = 宿主手动序,拖拽结果立即生效。**磁盘层是文件系统事实,不随拖拽改变**:跨磁盘层的放置只重组名称分组(工作区落回自身磁盘层的末尾),同层放置才走锚点序。拖拽工作区期间,单链合并显示的行(如 工作/A)临时展开回文件夹树——路径里的每一级都是可投放的分组行,松手后自动合并回去。
72
+ - **拖拽还原原版能力**:工作区行可拖拽——拖到另一工作区行上/下半即调整顺序(写回宿主 registry 序),拖到分组行则移动进该分组(改写名称前缀);会话行可拖拽——同组内拖到另一会话上/下即调整顺序,拖到**别组的会话行**会把名字前缀改写到那个组(拖到根层的行 = 移出分组),拖到会话子分组行即移动进该分组,拖到**自己的工作区行**或**树里任何组外的地方**(含空白区域)同样移出分组 —— 拖拽期间工作区行整行高亮、树底部有一条淡虚线提示这个出口;带分组前缀的会话行右键还多一项「移出分组」。显示顺序 = 宿主手动序,拖拽结果立即生效。**磁盘层是文件系统事实,不随拖拽改变**:跨磁盘层的放置只重组名称分组(工作区落回自身磁盘层的末尾),同层放置才走锚点序。拖拽工作区期间,单链合并显示的行(如 工作/A)临时展开回文件夹树——路径里的每一级都是可投放的分组行,松手后自动合并回去。
73
73
  - **不丢原生能力**:会话行(打开/重命名/分叉/归档)、每工作区「新会话」(点 + 总是先展开该工作区,让新会话立刻可见)、当前会话高亮、官方状态灯(蓝色运行环 / 琥珀待交互 / 绿色完成提醒)、**状态呼吸灯**(折叠藏起的状态灯向外冒泡:工作区/分组行图标按状态色呼吸发光——琥珀待交互/绿完成,自定义过发光的标题一起呼吸;运行中只在会话行本层蓝色呼吸,不外渗;可在设置关闭)、**活跃定时任务角标**(有活动 Schedule 的会话显示闹钟图标,与官方行一致)、未分组会话兜底、界面文案跟随 DSH 的语言设置(内置 21 种语言,含简繁中文)。
74
74
  - **URL/斜杠标题不拆层**:宿主自动生成的会话标题落地时若含 `/`(如复述了 URL),整串自动包上 `“”` 原样显示,绝不解析成层级——只对本次启动后**新建的会话**生效,并等命名稳定(约 20 秒,让原生 AI 命名先落地)才处理;存量会话与你手动命名的 `/` 分组标题永不被改动。引号内(含手动添加)的 `/` 永不切分,孤立引号只当普通字符;无引号但含 URL 的旧标题也有平铺兜底。
75
75
  - **对话空态页同样生效**:会话空态页的「添加工作区」菜单同样回归官方目录流(原生选择器 / 官方应用内浏览器)。
@@ -116,7 +116,7 @@ DSH 的侧边栏浏览区是一个公开插槽 `sidebar.workspaces`(single),官
116
116
 
117
117
  ## 限制与路线图
118
118
 
119
- - 会话拖到**另一分组内的会话行**仅做扁平重排(标题分组归属不变);要改分组请拖到分组行或走重命名。
119
+ - 会话**跨工作区**拖拽仍被拒绝(会话归属工作区);同一工作区内的分组归属已随拖拽生效(v0.18.0:拖到别组的行改写前缀,拖到自己的工作区行移出分组,右键也有「移出分组」)。
120
120
  - 搜索为本地标题过滤(两层扁平化后逐工作区匹配);宿主内容搜索(`session.search`)计划接回。
121
121
  - flat(平铺)视图未接管,层级树即视图。
122
122
 
package/README_EN.md CHANGED
@@ -69,7 +69,7 @@ Session titles nest on `/` too (e.g. `test1/plugin maintenance`). Session groups
69
69
  ### More highlights
70
70
 
71
71
  - **Folder-style collapse**: clicking a workspace row collapses/expands all of its sessions (the row keeps a session counter when collapsed); session groups collapse too; search force-expands everything.
72
- - **Native drag restored**: drag a workspace row above/below another to reorder (writes back the host registry order) or onto a folder row to move it into that group (rewrites the title prefix); drag session rows to reorder within the same workspace or onto a session group to move. Display order = host manual order, drag results are visible immediately. **The disk layer is a filesystem fact and never changes through dragging**: a cross-level drop only re-groups the name prefix (the workspace lands at the end of its own disk level), same-level drops take the anchor order. While dragging a workspace, merged single-chain rows (e.g. work/A) temporarily re-expand into folder rows — every level of the path becomes a drop target, and chains merge back when the drag ends.
72
+ - **Native drag restored**: drag a workspace row above/below another to reorder (writes back the host registry order) or onto a folder row to move it into that group (rewrites the title prefix); drag session rows to reorder inside their group, onto a row of ANOTHER group to move into that group (onto a root-level row to leave the group), onto a session-group row to move into it, or onto their OWN workspace row / anywhere outside a group (empty space included) to leave it — the workspace row highlights and the tree shows a faint dashed hint while such a drag is in flight; a grouped session row also offers "Move out of group" in its context menu. Display order = host manual order, drag results are visible immediately. **The disk layer is a filesystem fact and never changes through dragging**: a cross-level drop only re-groups the name prefix (the workspace lands at the end of its own disk level), same-level drops take the anchor order. While dragging a workspace, merged single-chain rows (e.g. work/A) temporarily re-expand into folder rows — every level of the path becomes a drop target, and chains merge back when the drag ends.
73
73
  - **Native capabilities kept**: open/rename/fork/archive sessions, per-workspace new-session (the + button always expands the workspace first so the new session is immediately visible), current-session highlight, official status dots (blue running ring / amber pending / green done), **status breathing lights** (dots hidden by collapse bubble outward: workspace/folder rows breathe on their icon in the status color — amber pending / green done, custom-glow labels breathe along; running breathes blue only on the session row itself and never relays; off-able in settings), **active-schedule badge** (alarm clock icon for sessions with active Schedule records, matching the official rows), ungrouped-session fallback, UI copy in 21 languages following the DSH language preference (Simplified and Traditional Chinese included).
74
74
  - **Slash-bearing titles never nest from URLs**: when a host-generated session title lands containing `/` (e.g. echoing a pasted URL), it is auto-wrapped in `“”` and shown verbatim — only for sessions **created after this launch**, and only once the name stays stable for ~20s (letting the native AI naming land first); pre-existing sessions and your manually named `/` groupings are never touched. Quotes (manual ones too) never split on `/`, and a lone quote is just an ordinary character; unquoted URL tails still fall back flat.
75
75
  - **Conversation hero too**: the empty-state “Add workspace” menu returns to the official directory flow (native chooser / official in-app browser).
@@ -102,7 +102,7 @@ Same `dsh-better-*` family, zero overlap: [dsh-better-sidebar](https://github.co
102
102
 
103
103
  ## Limitations / roadmap
104
104
 
105
- - A session dragged onto a session row **inside another group** only gets reordered in the flat list (its title group stays); drag onto the group row or rename to move it.
105
+ - Dragging a session **across workspaces** is still refused (a session belongs to its workspace); inside one workspace the group now follows the drop (v0.18.0: another group's row rewrites the prefix, the workspace row takes it out of every group, and the context menu offers "Move out of group").
106
106
  - Search is local title filtering (both layers flattened, per-workspace matching); host content search (`session.search`) is planned.
107
107
  - The flat view is not taken over; the hierarchical tree is the view.
108
108
 
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-better-workspace",
3
- "version": "0.17.2",
3
+ "version": "0.19.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/icon.svg ADDED
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" fill="none">
2
+ <path d="M4 10.5A2.5 2.5 0 0 1 6.5 8h6l2.4 2.8H29.5A2.5 2.5 0 0 1 32 13.3V26a2.5 2.5 0 0 1-2.5 2.5h-23A2.5 2.5 0 0 1 4 26V10.5z" stroke="#4493f8" stroke-width="1.7" stroke-linejoin="round"/>
3
+ <path d="M11 17.5h9M11 21.5h14" stroke="#4493f8" stroke-width="1.5" stroke-linecap="round"/>
4
+ <circle cx="24.5" cy="17.5" r="1.3" fill="#f0883e"/>
5
+ </svg>
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Better Workspace",
4
+ "description": "Upgrades the sidebar workspace list into a two-layer hierarchy tree (disk nesting by the official owningParentFolder semantics, optional name-slash grouping), explicit empty folders, appearance customization with live preview, and the official add-workspace flow for every backend."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "更好的工作区",
4
+ "description": "把侧边栏工作区列表升级为两层层级树(磁盘层按官方 owningParentFolder 语义嵌套,名称层按标题里的 / 可选分组),支持显式空分组、带实时预览的外观自定义,添加工作区全程走官方目录流。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-better-workspace",
3
- "version": "0.17.2",
3
+ "version": "0.19.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": {
@@ -13,7 +13,8 @@
13
13
  "main": "src/index.js",
14
14
  "exports": {
15
15
  ".": "./src/index.js",
16
- "./client": "./src/client.js"
16
+ "./client": "./src/client.js",
17
+ "./locale/*.json": "./locale/*.json"
17
18
  },
18
19
  "engines": {
19
20
  "node": ">=18",
@@ -41,7 +42,9 @@
41
42
  "dsh.plugin.json",
42
43
  "README.md",
43
44
  "README_EN.md",
44
- "LICENSE"
45
+ "LICENSE",
46
+ "locale",
47
+ "icon.svg"
45
48
  ],
46
49
  "scripts": {
47
50
  "test": "node --test tests/smoke.mjs",
@@ -64,5 +67,9 @@
64
67
  "homepage": "https://github.com/KannaKuron/dsh-better-workspace",
65
68
  "bugs": {
66
69
  "url": "https://github.com/KannaKuron/dsh-better-workspace/issues"
70
+ },
71
+ "icon": "./icon.svg",
72
+ "devDependencies": {
73
+ "@deepseek-ai/schemastery": "^3.18.1"
67
74
  }
68
75
  }
package/src/client.js CHANGED
@@ -72,6 +72,7 @@ window.__ModuleLoader__.load({
72
72
  'menu.renameFolder': '重命名分组',
73
73
  'menu.renameSgroup': '重命名会话分组',
74
74
  'menu.moveToGroup': '移动到分组…',
75
+ 'menu.moveOutGroup': '移出分组',
75
76
  'settings.title': '更好的工作区',
76
77
  'settings.desc': '工作区树的外观与折叠偏好',
77
78
  'settings.expand': '展开',
@@ -201,6 +202,7 @@ window.__ModuleLoader__.load({
201
202
  'menu.renameFolder': 'Rename folder',
202
203
  'menu.renameSgroup': 'Rename session group',
203
204
  'menu.moveToGroup': 'Move to group…',
205
+ 'menu.moveOutGroup': 'Move out of group',
204
206
  'settings.title': 'Better Workspaces',
205
207
  'settings.desc': 'Workspace tree appearance and folding preferences',
206
208
  'settings.expand': 'Expand',
@@ -338,6 +340,7 @@ window.__ModuleLoader__.load({
338
340
  'menu.renameFolder': 'إعادة تسمية المجلد',
339
341
  'menu.renameSgroup': 'إعادة تسمية مجموعة الجلسات',
340
342
  'menu.moveToGroup': 'نقل إلى مجموعة…',
343
+ 'menu.moveOutGroup': 'إخراج من المجموعة',
341
344
  'settings.title': 'مساحات عمل أفضل',
342
345
  'settings.desc': 'مظهر شجرة مساحات العمل وتفضيلات الطيّ',
343
346
  'settings.expand': 'توسيع',
@@ -467,6 +470,7 @@ window.__ModuleLoader__.load({
467
470
  'menu.renameFolder': 'Ordner umbenennen',
468
471
  'menu.renameSgroup': 'Sitzungsgruppe umbenennen',
469
472
  'menu.moveToGroup': 'In Gruppe verschieben…',
473
+ 'menu.moveOutGroup': 'Aus der Gruppe entfernen',
470
474
  'settings.title': 'Bessere Arbeitsbereiche',
471
475
  'settings.desc': 'Aussehen und Einklappverhalten des Arbeitsbereich-Baums',
472
476
  'settings.expand': 'Ausklappen',
@@ -596,6 +600,7 @@ window.__ModuleLoader__.load({
596
600
  'menu.renameFolder': 'Renommer le dossier',
597
601
  'menu.renameSgroup': 'Renommer le groupe de sessions',
598
602
  'menu.moveToGroup': 'Déplacer vers le groupe…',
603
+ 'menu.moveOutGroup': 'Retirer du groupe',
599
604
  'settings.title': 'Espaces de travail améliorés',
600
605
  'settings.desc': 'Apparence et repli de l\'arborescence des espaces de travail',
601
606
  'settings.expand': 'Développer',
@@ -725,6 +730,7 @@ window.__ModuleLoader__.load({
725
730
  'menu.renameFolder': 'फ़ोल्डर का नाम बदलें',
726
731
  'menu.renameSgroup': 'सत्र समूह का नाम बदलें',
727
732
  'menu.moveToGroup': 'समूह में ले जाएँ…',
733
+ 'menu.moveOutGroup': 'समूह से बाहर निकालें',
728
734
  'settings.title': 'बेहतर कार्यस्थान',
729
735
  'settings.desc': 'कार्यस्थान वृक्ष का रूप और समेटने की प्राथमिकताएँ',
730
736
  'settings.expand': 'फैलाएँ',
@@ -854,6 +860,7 @@ window.__ModuleLoader__.load({
854
860
  'menu.renameFolder': 'Ganti nama folder',
855
861
  'menu.renameSgroup': 'Ganti nama grup sesi',
856
862
  'menu.moveToGroup': 'Pindahkan ke grup…',
863
+ 'menu.moveOutGroup': 'Keluarkan dari grup',
857
864
  'settings.title': 'Ruang kerja yang lebih baik',
858
865
  'settings.desc': 'Tampilan dan pelipatan pohon ruang kerja',
859
866
  'settings.expand': 'Bentangkan',
@@ -983,6 +990,7 @@ window.__ModuleLoader__.load({
983
990
  'menu.renameFolder': 'Rinomina cartella',
984
991
  'menu.renameSgroup': 'Rinomina gruppo di sessioni',
985
992
  'menu.moveToGroup': 'Sposta nel gruppo…',
993
+ 'menu.moveOutGroup': 'Rimuovi dal gruppo',
986
994
  'settings.title': 'Aree di lavoro migliori',
987
995
  'settings.desc': 'Aspetto e compressione dell\'albero delle aree di lavoro',
988
996
  'settings.expand': 'Espandi',
@@ -1112,6 +1120,7 @@ window.__ModuleLoader__.load({
1112
1120
  'menu.renameFolder': 'フォルダー名を変更',
1113
1121
  'menu.renameSgroup': 'セッショングループ名を変更',
1114
1122
  'menu.moveToGroup': 'グループに移動…',
1123
+ 'menu.moveOutGroup': 'グループから出す',
1115
1124
  'settings.title': 'より良いワークスペース',
1116
1125
  'settings.desc': 'ワークスペースツリーの外観と折りたたみ設定',
1117
1126
  'settings.expand': '展開',
@@ -1241,6 +1250,7 @@ window.__ModuleLoader__.load({
1241
1250
  'menu.renameFolder': '폴더 이름 바꾸기',
1242
1251
  'menu.renameSgroup': '세션 그룹 이름 바꾸기',
1243
1252
  'menu.moveToGroup': '그룹으로 이동…',
1253
+ 'menu.moveOutGroup': '그룹에서 제거',
1244
1254
  'settings.title': '더 나은 워크스페이스',
1245
1255
  'settings.desc': '워크스페이스 트리의 모양과 접기 설정',
1246
1256
  'settings.expand': '펼치기',
@@ -1370,6 +1380,7 @@ window.__ModuleLoader__.load({
1370
1380
  'menu.renameFolder': 'Mapnaam wijzigen',
1371
1381
  'menu.renameSgroup': 'Sessiegroep naam wijzigen',
1372
1382
  'menu.moveToGroup': 'Naar groep verplaatsen…',
1383
+ 'menu.moveOutGroup': 'Uit groep halen',
1373
1384
  'settings.title': 'Betere werkruimten',
1374
1385
  'settings.desc': 'Uiterlijk en inklapgedrag van de werkruimteboom',
1375
1386
  'settings.expand': 'Uitklappen',
@@ -1499,6 +1510,7 @@ window.__ModuleLoader__.load({
1499
1510
  'menu.renameFolder': 'Zmień nazwę folderu',
1500
1511
  'menu.renameSgroup': 'Zmień nazwę grupy sesji',
1501
1512
  'menu.moveToGroup': 'Przenieś do grupy…',
1513
+ 'menu.moveOutGroup': 'Przenieś poza grupę',
1502
1514
  'settings.title': 'Lepsze obszary robocze',
1503
1515
  'settings.desc': 'Wygląd i zwijanie drzewa obszarów roboczych',
1504
1516
  'settings.expand': 'Rozwiń',
@@ -1628,6 +1640,7 @@ window.__ModuleLoader__.load({
1628
1640
  'menu.renameFolder': 'Renomear pasta',
1629
1641
  'menu.renameSgroup': 'Renomear grupo de sessões',
1630
1642
  'menu.moveToGroup': 'Mover para o grupo…',
1643
+ 'menu.moveOutGroup': 'Remover do grupo',
1631
1644
  'settings.title': 'Espaços de trabalho melhores',
1632
1645
  'settings.desc': 'Aparência e recolhimento da árvore de espaços de trabalho',
1633
1646
  'settings.expand': 'Expandir',
@@ -1757,6 +1770,7 @@ window.__ModuleLoader__.load({
1757
1770
  'menu.renameFolder': 'Переименовать папку',
1758
1771
  'menu.renameSgroup': 'Переименовать группу сессий',
1759
1772
  'menu.moveToGroup': 'Переместить в группу…',
1773
+ 'menu.moveOutGroup': 'Убрать из группы',
1760
1774
  'settings.title': 'Улучшенные рабочие области',
1761
1775
  'settings.desc': 'Внешний вид и сворачивание дерева рабочих областей',
1762
1776
  'settings.expand': 'Развернуть',
@@ -1886,6 +1900,7 @@ window.__ModuleLoader__.load({
1886
1900
  'menu.renameFolder': 'Byt namn på mapp',
1887
1901
  'menu.renameSgroup': 'Byt namn på sessionsgrupp',
1888
1902
  'menu.moveToGroup': 'Flytta till grupp…',
1903
+ 'menu.moveOutGroup': 'Flytta ut ur gruppen',
1889
1904
  'settings.title': 'Bättre arbetsytor',
1890
1905
  'settings.desc': 'Utseende och ihopfällning för arbetsyteträdet',
1891
1906
  'settings.expand': 'Fäll ut',
@@ -2015,6 +2030,7 @@ window.__ModuleLoader__.load({
2015
2030
  'menu.renameFolder': 'เปลี่ยนชื่อโฟลเดอร์',
2016
2031
  'menu.renameSgroup': 'เปลี่ยนชื่อกลุ่มเซสชัน',
2017
2032
  'menu.moveToGroup': 'ย้ายไปยังกลุ่ม…',
2033
+ 'menu.moveOutGroup': 'ย้ายออกจากกลุ่ม',
2018
2034
  'settings.title': 'พื้นที่ทำงานที่ดีขึ้น',
2019
2035
  'settings.desc': 'รูปลักษณ์และการย่อของต้นไม้พื้นที่ทำงาน',
2020
2036
  'settings.expand': 'ขยาย',
@@ -2144,6 +2160,7 @@ window.__ModuleLoader__.load({
2144
2160
  'menu.renameFolder': 'Klasörü yeniden adlandır',
2145
2161
  'menu.renameSgroup': 'Oturum grubunu yeniden adlandır',
2146
2162
  'menu.moveToGroup': 'Gruba taşı…',
2163
+ 'menu.moveOutGroup': 'Gruptan çıkar',
2147
2164
  'settings.title': 'Daha iyi çalışma alanları',
2148
2165
  'settings.desc': 'Çalışma alanı ağacının görünümü ve daraltma tercihleri',
2149
2166
  'settings.expand': 'Genişlet',
@@ -2273,6 +2290,7 @@ window.__ModuleLoader__.load({
2273
2290
  'menu.renameFolder': 'Đổi tên thư mục',
2274
2291
  'menu.renameSgroup': 'Đổi tên nhóm phiên',
2275
2292
  'menu.moveToGroup': 'Chuyển vào nhóm…',
2293
+ 'menu.moveOutGroup': 'Đưa ra khỏi nhóm',
2276
2294
  'settings.title': 'Không gian làm việc tốt hơn',
2277
2295
  'settings.desc': 'Giao diện và cách thu gọn của cây không gian làm việc',
2278
2296
  'settings.expand': 'Mở rộng',
@@ -2402,6 +2420,7 @@ window.__ModuleLoader__.load({
2402
2420
  'menu.renameFolder': '重新命名分組',
2403
2421
  'menu.renameSgroup': '重新命名對話分組',
2404
2422
  'menu.moveToGroup': '移動到分組…',
2423
+ 'menu.moveOutGroup': '移出分組',
2405
2424
  'settings.title': '更好嘅工作區',
2406
2425
  'settings.desc': '工作區樹嘅外觀同收起偏好',
2407
2426
  'settings.expand': '展開',
@@ -2531,6 +2550,7 @@ window.__ModuleLoader__.load({
2531
2550
  'menu.renameFolder': '重新命名分組',
2532
2551
  'menu.renameSgroup': '重新命名對話分組',
2533
2552
  'menu.moveToGroup': '移動到分組…',
2553
+ 'menu.moveOutGroup': '移出分組',
2534
2554
  'settings.title': '更好嘅工作區',
2535
2555
  'settings.desc': '工作區樹嘅外觀同收起偏好',
2536
2556
  'settings.expand': '展開',
@@ -2660,6 +2680,7 @@ window.__ModuleLoader__.load({
2660
2680
  'menu.renameFolder': '重新命名群組',
2661
2681
  'menu.renameSgroup': '重新命名工作階段群組',
2662
2682
  'menu.moveToGroup': '移動到分組…',
2683
+ 'menu.moveOutGroup': '移出分組',
2663
2684
  'settings.title': '更好的工作區',
2664
2685
  'settings.desc': '工作區樹的外觀與收合偏好',
2665
2686
  'settings.expand': '展開',
@@ -3314,6 +3335,10 @@ window.__ModuleLoader__.load({
3314
3335
  // rim + a tinted wash so "this drop joins a group" reads stronger than
3315
3336
  // the reorder dashes above.
3316
3337
  '.bw-drop-into-strong{outline:2px solid var(--dsw-alias-brand-primary,#5b8def);outline-offset:-2px;background:color-mix(in srgb, var(--dsw-alias-brand-primary,#5b8def) 16%, transparent)}',
3338
+ // Session drag whose source sits in a group: a quiet dashed line at the
3339
+ // bottom of the tree says "anywhere outside a group = out of it". Kept
3340
+ // faint on purpose — the row outlines are the loud feedback.
3341
+ '.bw-root-drop-out::after{content:"";position:absolute;left:10px;right:10px;bottom:6px;border-bottom:1px dashed var(--dsw-alias-border-secondary,rgba(230,230,230,.35));pointer-events:none}',
3317
3342
  // v0.17.0: the new-group entry is an ICON button now (same family as the
3318
3343
  // search / add buttons beside it); the wording moved into its tooltip.
3319
3344
  '.bw-newgroup-btn{flex:none}',
@@ -3658,6 +3683,10 @@ window.__ModuleLoader__.load({
3658
3683
  // simply never mirror and never sync, which is exactly the pre-0.9.5
3659
3684
  // browser-local behavior.
3660
3685
  let prefsScopeRef = null
3686
+ // 'settingsScope' (dsh <= 0.1.6, the shared ~/.dsh/settings.yaml home —
3687
+ // the only world where CROSS-DEVICE sync makes sense) or 'configForms'
3688
+ // (dsh >= 0.1.7, per-profile patch persistence). null = browser-local.
3689
+ let prefsScopeVia = null
3661
3690
  const HOST_SNAP_UNAVAILABLE = { status: 'unavailable', value: undefined }
3662
3691
  const hostPrefsOf = (snap) => (snap && snap.status === 'ready' && snap.value && typeof snap.value === 'object'
3663
3692
  ? snap.value
@@ -4246,13 +4275,13 @@ window.__ModuleLoader__.load({
4246
4275
  )
4247
4276
  }
4248
4277
 
4249
- function WorkspaceRow({ workspace, depth, count, sessionsOpen, onToggle, onStart, onContextMenu, currentInside, dropHalf, dragEvents, custStyle, iconMode, pulse, t }) {
4278
+ function WorkspaceRow({ workspace, depth, count, sessionsOpen, onToggle, onStart, onContextMenu, currentInside, dropHalf, dropInto, dragEvents, custStyle, iconMode, pulse, t }) {
4250
4279
  const iconEl = iconOf(iconMode, sessionsOpen)
4251
4280
  const iconChild = pulse
4252
4281
  ? E(PulseGlow, { state: pulse }, iconEl || (typeof ui.StateDot === 'function' ? E(ui.StateDot, { state: pulse, size: 10 }) : null))
4253
4282
  : iconEl
4254
4283
  return E('div', {
4255
- className: cls('bw-row', currentInside && 'bw-row-current', dropHalf === 'before' && 'bw-drop-before', dropHalf === 'after' && 'bw-drop-after'),
4284
+ className: cls('bw-row', currentInside && 'bw-row-current', dropHalf === 'before' && 'bw-drop-before', dropHalf === 'after' && 'bw-drop-after', dropInto && 'bw-drop-into', dropInto && 'bw-drop-into-strong'),
4256
4285
  style: { paddingLeft: 6 + depth * 12, ...(custStyle || {}), ...(pulse ? { '--bw-pulse-color': PULSE_COLORS[pulse] || PULSE_COLORS.ongoing } : null) },
4257
4286
  onClick: onToggle,
4258
4287
  onContextMenu: onContextMenu,
@@ -4839,19 +4868,24 @@ window.__ModuleLoader__.load({
4839
4868
  E(BTN, { variant: 'outline', onClick: () => setAppearance(DEFAULT_APPEARANCE) }, t('settings.appearance.reset')),
4840
4869
  ),
4841
4870
  ),
4842
- E('div', { className: 'bw-setting-label', style: { marginTop: 16 } }, t('sync.title')),
4843
- E('div', { className: 'bw-hint' }, t('sync.desc')),
4844
- E('div', { className: 'bw-sync-modes' },
4845
- modeButton('overwrite', t('sync.mode.overwrite')),
4846
- modeButton('merge', t('sync.mode.merge')),
4847
- ),
4848
- E('div', { className: 'bw-sync-actions' },
4849
- syncActionButton(t(IS_DESKTOP_SURFACE ? 'sync.pull.web' : 'sync.pull.desktop'), onPull, true, !scopeLive || snap.status === 'loading'),
4850
- syncActionButton(t('sync.push'), onPush, false, !scopeLive),
4851
- ),
4852
- syncMsg !== null ? E('div', { className: 'bw-hint' }, syncMsg)
4853
- : (scopeLive ? null : E('div', { className: 'bw-hint' }, t('sync.off'))),
4854
- scopeLive && snap.status === 'loading' ? E('div', { className: 'bw-hint' }, t('sync.loading')) : null,
4871
+ // Manual cross-device sync exists only where a SHARED host home does
4872
+ // (dsh <= 0.1.6 settings.yaml; both surfaces read one file). On
4873
+ // dsh >= 0.1.7 settings persist per profile, so the section hides.
4874
+ prefsScopeVia === 'settingsScope' ? E(React.Fragment, null,
4875
+ E('div', { className: 'bw-setting-label', style: { marginTop: 16 } }, t('sync.title')),
4876
+ E('div', { className: 'bw-hint' }, t('sync.desc')),
4877
+ E('div', { className: 'bw-sync-modes' },
4878
+ modeButton('overwrite', t('sync.mode.overwrite')),
4879
+ modeButton('merge', t('sync.mode.merge')),
4880
+ ),
4881
+ E('div', { className: 'bw-sync-actions' },
4882
+ syncActionButton(t(IS_DESKTOP_SURFACE ? 'sync.pull.web' : 'sync.pull.desktop'), onPull, true, !scopeLive || snap.status === 'loading'),
4883
+ syncActionButton(t('sync.push'), onPush, false, !scopeLive),
4884
+ ),
4885
+ syncMsg !== null ? E('div', { className: 'bw-hint' }, syncMsg)
4886
+ : (scopeLive ? null : E('div', { className: 'bw-hint' }, t('sync.off'))),
4887
+ scopeLive && snap.status === 'loading' ? E('div', { className: 'bw-hint' }, t('sync.loading')) : null,
4888
+ ) : null,
4855
4889
  )
4856
4890
  }
4857
4891
 
@@ -5372,6 +5406,18 @@ window.__ModuleLoader__.load({
5372
5406
  const s = splitTitleSegs(String(title || ''))
5373
5407
  return s.length > 0 ? s[s.length - 1] : (fallback || String(title || ''))
5374
5408
  }
5409
+ /**
5410
+ * Retitle `title` so its leaf lands in the group `anchorTitle` lives in
5411
+ * (v0.18.0). '' as the anchor group means the root level. Used by the
5412
+ * cross-group session drop: the visible order of session rows is decided
5413
+ * by these groups, so a drop that only reorders the flat list would leave
5414
+ * the row exactly where it was.
5415
+ */
5416
+ const titleMovedToGroup = (title, anchorTitle) => {
5417
+ const leaf = leafOf(title)
5418
+ const group = groupPrefixOf(anchorTitle)
5419
+ return group ? group + '/' + leaf : leaf
5420
+ }
5375
5421
  /**
5376
5422
  * Every existing group path, deduplicated (datalist source). `kind`
5377
5423
  * narrows it to the name layer the dialog is about: workspace rows
@@ -5661,6 +5707,22 @@ window.__ModuleLoader__.load({
5661
5707
  setDrag(null)
5662
5708
  },
5663
5709
  onDragOver: (event) => {
5710
+ // A SESSION dragged onto the workspace row lands at the root level of
5711
+ // its session tree — the way OUT of any group (v0.18.0). Only for
5712
+ // rows that actually sit in a group: a top-level session gains
5713
+ // nothing here, so this row must not light up for it (the browser
5714
+ // then shows its plain "no drop" cursor, which tells the truth).
5715
+ if (dragMatches('session')) {
5716
+ if (!sessionSlash || drag.source.workspaceId !== workspace.workspaceId) return
5717
+ if (leafOf(drag.source.title) === drag.source.title) return
5718
+ event.preventDefault()
5719
+ event.stopPropagation()
5720
+ try { event.dataTransfer.dropEffect = 'move' } catch { }
5721
+ setDrag(current => (current && current.over && current.over.kind === 'ws-root' && current.over.target === workspace.workspaceId)
5722
+ ? current
5723
+ : (current ? { ...current, over: { kind: 'ws-root', target: workspace.workspaceId } } : current))
5724
+ return
5725
+ }
5664
5726
  if (!dragMatches('workspace')) return
5665
5727
  event.preventDefault()
5666
5728
  event.stopPropagation()
@@ -5671,6 +5733,14 @@ window.__ModuleLoader__.load({
5671
5733
  : (current ? { ...current, over: { kind: 'workspace', target: workspace.workspaceId, half } } : current))
5672
5734
  },
5673
5735
  onDrop: (event) => {
5736
+ if (dragMatches('session')) {
5737
+ if (!sessionSlash || drag.source.workspaceId !== workspace.workspaceId) return
5738
+ if (leafOf(drag.source.title) === drag.source.title) return
5739
+ event.preventDefault()
5740
+ event.stopPropagation()
5741
+ commitSessionMoveOut(workspace.workspaceId)
5742
+ return
5743
+ }
5674
5744
  if (!dragMatches('workspace')) return
5675
5745
  event.preventDefault()
5676
5746
  event.stopPropagation()
@@ -5814,17 +5884,55 @@ window.__ModuleLoader__.load({
5814
5884
  const anchor = half === 'after'
5815
5885
  ? (index === -1 ? undefined : (flat[index + 1] ? flat[index + 1].id : undefined))
5816
5886
  : targetSessionId
5817
- if (typeof insertSessionBefore === 'function') {
5818
- Promise.resolve()
5819
- .then(() => (anchor !== undefined ? insertSessionBefore(workspaceId, source.sessionId, anchor) : insertSessionBefore(workspaceId, source.sessionId)))
5820
- .catch(fail)
5821
- return
5822
- }
5823
- // Host channel gone: commit the same move into the browser-local order.
5824
- if (actions && typeof actions.setSessionOrder === 'function') {
5825
- actions.setSessionOrder(workspaceId, reorderIds(flat.map((s) => s.id), source.sessionId, anchor))
5887
+ // v0.18.0 — a drop on a row of ANOTHER group MOVES the row (the name
5888
+ // prefix is rewritten to the target row's group). Until then only the
5889
+ // flat list was reordered while the visible order is decided by the
5890
+ // groups, so a cross-group drop left the row exactly where it was and
5891
+ // read as "the drag did nothing" (user report). Same group keeps the
5892
+ // pure reorder; with the name layer off every group is '' — which is
5893
+ // the same thing.
5894
+ const target = flat.find(s => s.id === targetSessionId)
5895
+ const nextTitle = sessionSlash && target ? titleMovedToGroup(source.title, target.title) : null
5896
+ const reorder = () => {
5897
+ if (typeof insertSessionBefore === 'function') {
5898
+ return (anchor !== undefined ? insertSessionBefore(workspaceId, source.sessionId, anchor) : insertSessionBefore(workspaceId, source.sessionId))
5899
+ }
5900
+ // Host channel gone: commit the same move into the browser-local order.
5901
+ if (actions && typeof actions.setSessionOrder === 'function') {
5902
+ actions.setSessionOrder(workspaceId, reorderIds(flat.map((s) => s.id), source.sessionId, anchor))
5903
+ }
5904
+ return undefined
5826
5905
  }
5906
+ const chain = nextTitle !== null && nextTitle !== source.title
5907
+ ? Promise.resolve().then(() => renameByUser(source.sessionId, nextTitle))
5908
+ : Promise.resolve()
5909
+ chain.then(reorder).catch(fail)
5910
+ }
5911
+ /**
5912
+ * Drop a session ON ITS OWN WORKSPACE ROW = leave the group (v0.18.0).
5913
+ * The workspace row is the root of the session tree, so it is the one
5914
+ * drop target that always exists OUTSIDE every group — the user's "drag
5915
+ * it out of 测试1" gesture had nowhere to land before this.
5916
+ */
5917
+ const commitSessionMoveOut = (workspaceId) => {
5918
+ const source = drag.source
5919
+ setDrag(null)
5920
+ if (!source || source.workspaceId !== workspaceId) return
5921
+ const leaf = leafOf(source.title)
5922
+ if (leaf === source.title) return
5923
+ Promise.resolve()
5924
+ .then(() => renameByUser(source.sessionId, leaf))
5925
+ .then(() => {
5926
+ // Append to the end of the workspace's flat order: the row now
5927
+ // lives at the root level, and the host order is what the tree
5928
+ // renders there — without this it would surface somewhere in the
5929
+ // middle of the top-level rows, wherever its id happens to sit.
5930
+ if (typeof insertSessionBefore === 'function') return insertSessionBefore(workspaceId, source.sessionId)
5931
+ return undefined
5932
+ })
5933
+ .catch(fail)
5827
5934
  }
5935
+ const wsRootDropActive = (workspaceId) => dragMatches('session') && drag.over !== null && drag.over.kind === 'ws-root' && drag.over.target === workspaceId
5828
5936
  const commitSessionMoveInto = (workspaceId, groupPath) => {
5829
5937
  const source = drag.source
5830
5938
  setDrag(null)
@@ -6039,6 +6147,7 @@ window.__ModuleLoader__.load({
6039
6147
  onStart: () => { actions.setSessionsExpanded(workspace.workspaceId, true); startSession(workspace.workspaceId) },
6040
6148
  onContextMenu: (e) => openCtx('workspace', workspace, e),
6041
6149
  dropHalf: wsDropHalf(workspace.workspaceId),
6150
+ dropInto: wsRootDropActive(workspace.workspaceId),
6042
6151
  dragEvents: workspaceDragEvents(workspace),
6043
6152
  custStyle: rowStyleOf('workspace:' + workspace.workspaceId),
6044
6153
  iconMode: (styleEntry('workspace:' + workspace.workspaceId) || {}).icon || 'solid',
@@ -6158,8 +6267,14 @@ window.__ModuleLoader__.load({
6158
6267
  // probing it silently yielded a glyph-less row. IconFolderOpenOutline16
6159
6268
  // is present and reads as "move into a group" just as well.
6160
6269
  const groupItem = { id: 'move-group', label: t('menu.moveToGroup'), icon: icon('IconFolderOpenOutline16', 16) }
6270
+ // "Leave the group" (v0.18.0) — the counterpart of move-group, offered
6271
+ // only where a group actually exists to leave: a SESSION row whose own
6272
+ // title carries a "/" prefix, with the session name layer on. A
6273
+ // top-level row gets no entry (there is nothing to leave).
6274
+ const ungroupItem = { id: 'move-out', label: t('menu.moveOutGroup'), icon: icon('IconRightUpOutline16', 16) }
6161
6275
  const wsGroupItems = workspaceSlash ? [groupItem] : []
6162
6276
  const sgroupItems = sessionSlash ? [groupItem] : []
6277
+ const sessionUngroupItems = sessionSlash && ctx.kind === 'session' && ctx.payload && groupPrefixOf(String(ctx.payload.title || '')) !== '' ? [ungroupItem] : []
6163
6278
  // "Delete group" only makes sense for a DECLARED group that holds
6164
6279
  // nothing (v0.17.0): a group with members is a projection of their
6165
6280
  // titles, so removing the declaration would change nothing on screen
@@ -6188,6 +6303,7 @@ window.__ModuleLoader__.load({
6188
6303
  // deliberately NOT styled destructive.
6189
6304
  { id: 'archive', label: ot ? ot('menu.archiveSession') : t('menu.archive'), icon: icon('IconArchiveOutline20', 16) },
6190
6305
  { type: 'separator', id: 'bw-sep' },
6306
+ ...sessionUngroupItems,
6191
6307
  ...sgroupItems,
6192
6308
  customizeEntry,
6193
6309
  ]
@@ -6230,6 +6346,12 @@ window.__ModuleLoader__.load({
6230
6346
  else if (kind === 'session' && id === 'rename') setDialog({ kind: 'sess-rename', session: payload })
6231
6347
  else if (kind === 'session' && id === 'fork') forkSession(payload.id)
6232
6348
  else if (kind === 'session' && id === 'archive') { Promise.resolve().then(() => archiveSession(payload.id)).catch(fail) }
6349
+ // Plain retitle back to the leaf: the row leaves its group and returns
6350
+ // to the top level of its workspace (v0.18.0).
6351
+ else if (kind === 'session' && id === 'move-out') {
6352
+ const leaf = leafOf(String(payload.title || ''))
6353
+ if (leaf !== payload.title) Promise.resolve().then(() => renameByUser(payload.id, leaf)).catch(fail)
6354
+ }
6233
6355
  }
6234
6356
 
6235
6357
  /* --------------------------- dialogs --------------------------- */
@@ -6393,7 +6515,27 @@ window.__ModuleLoader__.load({
6393
6515
  .catch((reason) => { fail(messageOf(reason)) })
6394
6516
  }
6395
6517
 
6396
- return E('div', { className: 'bw-root' },
6518
+ // v0.18.0 — "drag it OUT" must never land on nothing. The row level
6519
+ // already covers the workspace row and the root-level session rows; this
6520
+ // catches everything else (the empty space below the tree, the header
6521
+ // gap), so a drop outside every group always means the same thing:
6522
+ // leave the group. Row handlers stopPropagation, so this only fires
6523
+ // where no row claimed the drop.
6524
+ const sessionCouldLeaveGroup = dragMatches('session') && sessionSlash && drag.source && leafOf(drag.source.title) !== drag.source.title
6525
+ const rootDropEvents = {
6526
+ onDragOver: (event) => {
6527
+ if (!sessionCouldLeaveGroup) return
6528
+ event.preventDefault()
6529
+ try { event.dataTransfer.dropEffect = 'move' } catch { }
6530
+ },
6531
+ onDrop: (event) => {
6532
+ if (!sessionCouldLeaveGroup) return
6533
+ event.preventDefault()
6534
+ commitSessionMoveOut(drag.source.workspaceId)
6535
+ },
6536
+ }
6537
+
6538
+ return E('div', { className: cls('bw-root', sessionCouldLeaveGroup && 'bw-root-drop-out'), ...rootDropEvents },
6397
6539
  StyleNode(),
6398
6540
  E('div', { className: 'bw-header' },
6399
6541
  E('div', { className: 'bw-header-title' }, t('title')),
@@ -6655,14 +6797,38 @@ window.__ModuleLoader__.load({
6655
6797
  // block). Binding is best-effort: a missing/failed bind leaves the
6656
6798
  // plugin fully browser-local — the pre-0.9.5 behavior.
6657
6799
  prefsScopeRef = null
6800
+ prefsScopeVia = null
6658
6801
  try {
6659
6802
  if (ctx.settingsScope && typeof ctx.settingsScope.bind === 'function') {
6660
6803
  prefsScopeRef = ctx.settingsScope.bind({ namespace: 'better-workspace' })
6804
+ prefsScopeVia = 'settingsScope'
6661
6805
  }
6662
6806
  } catch (error) {
6663
6807
  console.warn('[dsh-better-workspace] settings scope bind failed; sync stays local', error)
6664
6808
  prefsScopeRef = null
6665
6809
  }
6810
+ // dsh >= 0.1.7: the settings service is gone; one ConfigForm per live
6811
+ // profile entry replaces it (same getSnapshot/set/subscribe contract).
6812
+ // Values then persist in THIS profile's patch — there is no shared home
6813
+ // any more, so the manual cross-device sync section stays hidden.
6814
+ try {
6815
+ if (ctx.inject) {
6816
+ ctx.inject(['configForms'], (fctx) => {
6817
+ try {
6818
+ if (prefsScopeVia === 'settingsScope') return
6819
+ const forms = fctx && fctx.configForms
6820
+ if (forms && typeof forms.get === 'function') {
6821
+ prefsScopeRef = forms.get('better-workspace')
6822
+ prefsScopeVia = 'configForms'
6823
+ }
6824
+ } catch (error) {
6825
+ console.warn('[dsh-better-workspace] configForms acquisition failed', error)
6826
+ }
6827
+ })
6828
+ }
6829
+ } catch (error) {
6830
+ console.warn('[dsh-better-workspace] configForms wiring failed', error)
6831
+ }
6666
6832
 
6667
6833
  // Settings → Plugins card only (the tab dispatches the intersection of
6668
6834
  // served namespaces — registered host-side — and settings.plugin.item
@@ -6716,7 +6882,7 @@ window.__ModuleLoader__.load({
6716
6882
 
6717
6883
  return {
6718
6884
  name: 'dsh-better-workspace',
6719
- inject: ['slots', 'sessions', 'workspaces', 'locale', 'uiWorkspace', 'settingsScope'],
6885
+ inject: ['slots', 'sessions', 'workspaces', 'locale', 'uiWorkspace'],
6720
6886
  apply,
6721
6887
  }
6722
6888
  },
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import Schema from '@deepseek-ai/schemastery'
1
2
  /**
2
3
  * dsh-better-workspace — host half (plain JavaScript, no build step).
3
4
  *
@@ -10,6 +11,36 @@
10
11
  */
11
12
  export const name = 'dsh-better-workspace'
12
13
 
14
+ /**
15
+ * dsh >= 0.1.7 marks a Config field live-editable without remount; a
16
+ * 0.1.6-era schemastery predates the method and the guard keeps this module
17
+ * loadable there (values then behave as ordinary config, read through the
18
+ * registered settings namespace instead).
19
+ */
20
+ function live(schema) {
21
+ return typeof schema.volatile === 'function' ? schema.volatile() : schema
22
+ }
23
+
24
+ /**
25
+ * Row Config = the settings surface on dsh >= 0.1.7 (values persist under the
26
+ * row id 'better-workspace' — the same string as the old settings namespace,
27
+ * so the one-shot legacy settings.yaml import maps old user values onto the
28
+ * new home). Inert metadata on older hosts. NOTE: settings are per-profile
29
+ * on the new host; the client hides the manual cross-device sync section
30
+ * accordingly (the shared settings.yaml home no longer exists).
31
+ */
32
+ export const Config = Schema.object({
33
+ compactChains: live(Schema.boolean().default(true)),
34
+ statusPulse: live(Schema.boolean().default(true)),
35
+ styling: live(Schema.dict(Schema.any()).default({})),
36
+ appearance: live(Schema.dict(Schema.any()).default({})),
37
+ })
38
+
39
+ /** Read one Config value across eras: Volatile ref (>= 0.1.7) or plain value. */
40
+ export function valueOf(value) {
41
+ return value && typeof value.get === 'function' ? value.get() : value
42
+ }
43
+
13
44
  export function apply(ctx) {
14
45
  const log = ctx && ctx.logger && typeof ctx.logger.info === 'function'
15
46
  ? (msg) => ctx.logger.info(msg)
@@ -24,7 +55,18 @@ export function apply(ctx) {
24
55
  // The dsh-settings and zod modules resolve only through the dsh Loader; the
25
56
  // smoke test imports this file in plain Node with a logger-only ctx and
26
57
  // never reaches this path.
27
- ctx.inject(['settings'], (sctx) => {
58
+ // OLD-era namespace registration (dsh <= 0.1.6): the row Config above IS
59
+ // the surface on dsh >= 0.1.7 — register() no longer exists there and this
60
+ // block is skipped entirely.
61
+ const legacySettings = (() => {
62
+ try {
63
+ const settings = ctx.get('settings')
64
+ return !!(settings && typeof settings.register === 'function')
65
+ } catch {
66
+ return false
67
+ }
68
+ })()
69
+ if (legacySettings) ctx.inject(['settings'], (sctx) => {
28
70
  log('[dsh-better-workspace] settings inject fired')
29
71
  // The settings service calls the schema AS A FUNCTION to resolve a value
30
72
  // (schema(mergeLayers(...))) — that is @deepseek-ai/schemastery's contract: