dsh-better-workspace 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-better-workspace",
3
- "version": "0.18.0",
3
+ "version": "0.20.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.18.0",
3
+ "version": "0.20.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
@@ -83,6 +83,10 @@ window.__ModuleLoader__.load({
83
83
  'settings.workspaceSlash.hint': '关闭时工作区只按官方文件夹(磁盘目录)嵌套,标题原样显示;开启后标题里的 / 重新成为分组层级(组名/名字)。默认关闭,只影响工作区;会话分组另有独立开关。',
84
84
  'settings.statusPulse': '状态呼吸灯',
85
85
  'settings.statusPulse.hint': '被折叠藏起的状态灯(完成绿 / 运行蓝 / 待交互琥珀)沿层级向外冒泡:工作区与分组行以图标呼吸发光(颜色随状态,自定义过发光的标题一起呼吸),会话分组行显示呼吸状态灯;默认开启,可在此关闭。',
86
+ 'settings.sessionMenu': "会话右键菜单",
87
+ 'settings.sessionMenu.hint': "开启时右键点击会话行即可打开操作菜单(包含官方的置顶、归档、停止并归档等);关闭后改为在行尾悬停显示官方样式的 ⋯ 按钮,点击打开同一个菜单。默认开启。",
88
+ 'settings.rowActions': "会话行悬停按钮",
89
+ 'settings.rowActions.hint': "悬停会话行时在行尾显示置顶 / 归档快捷按钮,与官方行保持一致;关闭后这些动作只保留在菜单里。默认开启。",
86
90
  'settings.appearance': '默认外观',
87
91
  'settings.appearance.hint': '没有单独自定义过的行使用这套外观;字体描边默认开启——背景画面下不描边文字常常看不清。字体颜色留空即跟随主题。',
88
92
  'settings.appearance.reset': '恢复默认外观',
@@ -213,6 +217,10 @@ window.__ModuleLoader__.load({
213
217
  'settings.workspaceSlash.hint': 'Off: workspaces nest purely by the official folders (disk directories) and titles render whole. On: "/" in a title becomes a group level again ("group/name"). Off by default, workspaces only — session grouping has its own switch.',
214
218
  'settings.statusPulse': 'Status breathing light',
215
219
  'settings.statusPulse.hint': 'Status dots hidden by collapse (done green / running blue / pending amber) bubble outward: workspace and folder rows breathe on their icon in the status color (custom-glow labels breathe along), session-group rows show a breathing dot; on by default, turn it off here.',
220
+ 'settings.sessionMenu': "Session context menu",
221
+ 'settings.sessionMenu.hint': "On: right-click a session row to open its action menu (including the official pin, archive, and stop-and-archive). Off: the row shows the official-style ⋯ button on hover instead, opening the same menu. On by default.",
222
+ 'settings.rowActions': "Session row hover buttons",
223
+ 'settings.rowActions.hint': "Show pin / archive quick buttons at the end of a hovered session row, matching the official rows. Off: these actions stay in the menu only. On by default.",
216
224
  'settings.appearance': 'Default appearance',
217
225
  'settings.appearance.hint': 'Rows that were never customized use this appearance; the outline is on by default — text without it is often unreadable over a background image. Leave the color empty to follow the theme.',
218
226
  'settings.appearance.reset': 'Reset to default',
@@ -351,6 +359,10 @@ window.__ModuleLoader__.load({
351
359
  'settings.workspaceSlash.hint': 'عند الإيقاف تتداخل مساحات العمل حسب المجلدات الرسمية (مجلدات القرص) فقط وتظهر العناوين كاملة؛ وعند التشغيل تعود "/" في العنوان مستوى تجميع («المجموعة/الاسم»). معطّل افتراضياً، ويخص مساحات العمل وحدها — لتجميع الجلسات مفتاحه المستقل.',
352
360
  'settings.statusPulse': 'مؤشر حالة نابض',
353
361
  'settings.statusPulse.hint': 'مؤشرات الحالة التي يخفيها الطيّ (مكتمل أخضر / قيد التشغيل أزرق / بانتظار تفاعل كهرماني) تتصاعد عبر المستويات: تصدر أسطر مساحات العمل والمجلدات وميضاً نابضاً للأيقونة بلون الحالة (وتنبض معها العناوين ذات التوهج المخصص)، وتعرض أسطر مجموعات الجلسات نقطة حالة نابضة؛ مفعّل افتراضياً ويمكن إيقافه هنا.',
362
+ 'settings.sessionMenu': "قائمة النقر الأيمن للجلسة",
363
+ 'settings.sessionMenu.hint': "عند التفعيل: انقر بزر الفأرة الأيمن على سطر الجلسة لفتح قائمة الإجراءات (بما في ذلك التثبيت والأرشفة الرسمية). عند الإيقاف: يظهر زر ⋯ بأسلوب رسمي عند تمرير المؤشر يفتح القائمة نفسها. مفعّل افتراضياً.",
364
+ 'settings.rowActions': "أزرار تمرير سطر الجلسة",
365
+ 'settings.rowActions.hint': "إظهار أزرار التثبيت / الأرشفة السريعة في نهاية سطر الجلسة عند تمرير المؤشر، مطابقة للأسطر الرسمية. عند الإيقاف: تبقى هذه الإجراءات في القائمة فقط. مفعّل افتراضياً.",
354
366
  'settings.appearance': 'المظهر الافتراضي',
355
367
  'settings.appearance.hint': 'تستخدم الأسطر غير المخصصة هذا المظهر؛ حدّ النص مفعّل افتراضياً — فبدونه يصعب قراءة النص فوق صورة خلفية. اترك اللون فارغاً لاتباع السمة.',
356
368
  'settings.appearance.reset': 'استعادة المظهر الافتراضي',
@@ -481,6 +493,10 @@ window.__ModuleLoader__.load({
481
493
  'settings.workspaceSlash.hint': 'Aus: Arbeitsbereiche verschachteln nur nach den offiziellen Ordnern (Verzeichnisse auf der Platte), Titel bleiben vollständig. An: "/" im Titel wird wieder eine Gruppenebene ("Gruppe/Name"). Standardmäßig aus, gilt nur für Arbeitsbereiche — Sitzungsgruppen haben einen eigenen Schalter.',
482
494
  'settings.statusPulse': 'Status-Pulslicht',
483
495
  'settings.statusPulse.hint': 'Vom Einklappen verdeckte Statuspunkte (fertig grün / läuft blau / wartet amber) steigen die Hierarchie hinauf: Arbeitsbereich- und Ordnerzeilen lassen ihr Symbol in der Statusfarbe pulsieren (eigens gefärbte Titel pulsieren mit), Sitzungsgruppen zeigen einen pulsierenden Statuspunkt; standardmäßig an, hier abschaltbar.',
496
+ 'settings.sessionMenu': "Sitzungs-Kontextmenü",
497
+ 'settings.sessionMenu.hint': "Ein: Rechtsklick auf eine Sitzungszeile öffnet das Aktionsmenü (inklusive offiziellem Anheften, Archivieren und Stoppen-und-Archivieren). Aus: beim Daraufzeigen erscheint stattdessen der offizielle ⋯-Knopf am Zeilenende und öffnet dasselbe Menü. Standardmäßig ein.",
498
+ 'settings.rowActions': "Sitzungszeilen-Schnellknöpfe",
499
+ 'settings.rowActions.hint': "Zeigt beim Daraufzeigen Schnellknöpfe zum Anheften / Archivieren am Zeilenende – wie die offiziellen Zeilen. Aus: diese Aktionen bleiben nur im Menü. Standardmäßig ein.",
484
500
  'settings.appearance': 'Standardaussehen',
485
501
  'settings.appearance.hint': 'Zeilen ohne eigene Anpassung nutzen dieses Aussehen; die Textkontur ist standardmäßig an – ohne sie ist Text über einem Hintergrundbild oft kaum lesbar. Bleibt die Farbe leer, folgt sie dem Thema.',
486
502
  'settings.appearance.reset': 'Standard wiederherstellen',
@@ -611,6 +627,10 @@ window.__ModuleLoader__.load({
611
627
  'settings.workspaceSlash.hint': 'Désactivé : les espaces s\'imbriquent uniquement selon les dossiers officiels (répertoires du disque) et les titres restent entiers. Activé : le « / » d\'un titre redevient un niveau de groupe (« groupe/nom »). Désactivé par défaut, espaces uniquement — les groupes de sessions ont leur propre interrupteur.',
612
628
  'settings.statusPulse': 'Voyant d\'état pulsant',
613
629
  'settings.statusPulse.hint': 'Les voyants masqués par le repli (terminé vert / en cours bleu / en attente ambre) remontent la hiérarchie: les lignes d\'espace de travail et de dossier font pulser leur icône dans la couleur d\'état (les titres à lueur personnalisée pulsent aussi), les lignes de groupe de sessions affichent un voyant pulsant; activé par défaut, désactivable ici.',
630
+ 'settings.sessionMenu': "Menu contextuel des sessions",
631
+ 'settings.sessionMenu.hint': "Activé : un clic droit sur une ligne de session ouvre son menu d’actions (épinglage, archivage et arrêt-avec-archivage officiels inclus). Désactivé : la ligne affiche plutôt le bouton ⋯ officiel au survol, ouvrant le même menu. Activé par défaut.",
632
+ 'settings.rowActions': "Boutons de survol des sessions",
633
+ 'settings.rowActions.hint': "Affiche des boutons rapides d’épinglage / d’archivage en fin de ligne au survol, comme les lignes officielles. Désactivé : ces actions restent dans le menu uniquement. Activé par défaut.",
614
634
  'settings.appearance': 'Apparence par défaut',
615
635
  'settings.appearance.hint': 'Les lignes sans personnalisation utilisent cette apparence; le contour du texte est activé par défaut — sans lui, le texte reste souvent illisible sur une image de fond. Laissez la couleur vide pour suivre le thème.',
616
636
  'settings.appearance.reset': 'Rétablir l\'apparence par défaut',
@@ -741,6 +761,10 @@ window.__ModuleLoader__.load({
741
761
  'settings.workspaceSlash.hint': 'बंद: कार्यस्थान केवल आधिकारिक फ़ोल्डरों (डिस्क निर्देशिकाओं) के अनुसार नेस्ट होते हैं और शीर्षक पूरे दिखते हैं। चालू: शीर्षक में "/" फिर से समूह स्तर बन जाता है ("समूह/नाम")। डिफ़ॉल्ट रूप से बंद, केवल कार्यस्थानों पर लागू — सत्र समूह का अपना स्विच है।',
742
762
  'settings.statusPulse': 'स्थिति की साँस लेती बत्ती',
743
763
  'settings.statusPulse.hint': 'समेटने से छिपी स्थिति बत्तियाँ (पूर्ण हरी / चल रहा नीली / प्रतीक्षारत अंबर) पदानुक्रम में ऊपर उठती हैं: कार्यस्थान और फ़ोल्डर पंक्तियों का चिह्न स्थिति के रंग में साँस लेता है (अनुकूलित चमक वाले शीर्षक भी साथ साँस लेते हैं), सत्र समूह पंक्तियों पर साँस लेता स्थिति बिंदु दिखता है; डिफ़ॉल्ट रूप से चालू, यहाँ बंद किया जा सकता है।',
764
+ 'settings.sessionMenu': "सेशन संदर्भ मेनू",
765
+ 'settings.sessionMenu.hint': "चालू: सेशन पंक्ति पर राइट-क्लिक करने से उसका क्रिया मेनू खुलता है (आधिकारिक पिन, आर्काइव और रोककर-आर्काइव सहित)। बंद: पंक्ति पर होवर करने पर आधिकारिक शैली का ⋯ बटन दिखता है, जो वही मेनू खोलता है। डिफ़ॉल्ट रूप से चालू।",
766
+ 'settings.rowActions': "सेशन पंक्ति होवर बटन",
767
+ 'settings.rowActions.hint': "होवर करने पर सेशन पंक्ति के अंत में पिन / आर्काइव क्विक बटन दिखाता है, आधिकारिक पंक्तियों जैसा। बंद: ये क्रियाएँ केवल मेनू में रहती हैं। डिफ़ॉल्ट रूप से चालू।",
744
768
  'settings.appearance': 'डिफ़ॉल्ट रूप',
745
769
  'settings.appearance.hint': 'जिन पंक्तियों को अलग से अनुकूलित नहीं किया गया वे यह रूप इस्तेमाल करती हैं; पाठ की रूपरेखा डिफ़ॉल्ट रूप से चालू है — पृष्ठभूमि छवि पर बिना रूपरेखा वाला पाठ अक्सर पढ़ा नहीं जाता। रंग खाली छोड़ने पर वह थीम का अनुसरण करता है।',
746
770
  'settings.appearance.reset': 'डिफ़ॉल्ट रूप पर लौटें',
@@ -871,6 +895,10 @@ window.__ModuleLoader__.load({
871
895
  'settings.workspaceSlash.hint': 'Mati: ruang kerja bersarang hanya menurut folder resmi (direktori disk) dan judul tampil utuh. Nyala: "/" pada judul kembali menjadi tingkat grup ("grup/nama"). Default mati, hanya ruang kerja — pengelompokan sesi punya sakelarnya sendiri.',
872
896
  'settings.statusPulse': 'Lampu status bernapas',
873
897
  'settings.statusPulse.hint': 'Lampu status yang tersembunyi karena pelipatan (selesai hijau / berjalan biru / menunggu kuning) menggelembung ke tingkat atas: baris ruang kerja dan folder membuat ikonnya bernapas dalam warna status (judul dengan pendar khusus ikut bernapas), baris grup sesi menampilkan titik status yang bernapas; aktif secara bawaan, bisa dimatikan di sini.',
898
+ 'settings.sessionMenu': "Menu klik kanan sesi",
899
+ 'settings.sessionMenu.hint': "Aktif: klik kanan pada baris sesi untuk membuka menu aksinya (termasuk pin, arsip, dan henti-dan-arsip resmi). Nonaktif: baris menampilkan tombol ⋯ gaya resmi saat dihover, membuka menu yang sama. Aktif secara bawaan.",
900
+ 'settings.rowActions': "Tombol hover baris sesi",
901
+ 'settings.rowActions.hint': "Menampilkan tombol cepat pin / arsip di ujung baris sesi saat dihover, mengikuti baris resmi. Nonaktif: aksi ini hanya ada di menu. Aktif secara bawaan.",
874
902
  'settings.appearance': 'Tampilan bawaan',
875
903
  'settings.appearance.hint': 'Baris yang belum disesuaikan memakai tampilan ini; garis luar teks aktif secara bawaan — tanpa itu teks sering tak terbaca di atas gambar latar. Biarkan warna kosong agar mengikuti tema.',
876
904
  'settings.appearance.reset': 'Kembalikan tampilan bawaan',
@@ -1001,6 +1029,10 @@ window.__ModuleLoader__.load({
1001
1029
  'settings.workspaceSlash.hint': 'Off: le aree di lavoro si annidano solo secondo le cartelle ufficiali (directory del disco) e i titoli restano interi. On: la "/" nel titolo torna a essere un livello di gruppo ("gruppo/nome"). Disattivato per impostazione predefinita, solo aree di lavoro — le sessioni hanno il proprio interruttore.',
1002
1030
  'settings.statusPulse': 'Spia di stato pulsante',
1003
1031
  'settings.statusPulse.hint': 'Le spie nascoste dalla compressione (completato verde / in esecuzione blu / in attesa ambra) risalgono la gerarchia: le righe di aree di lavoro e cartelle fanno pulsare l\'icona nel colore di stato (anche i titoli con bagliore personalizzato pulsano), le righe dei gruppi di sessioni mostrano una spia pulsante; attiva per impostazione predefinita, disattivabile qui.',
1032
+ 'settings.sessionMenu': "Menu contestuale sessione",
1033
+ 'settings.sessionMenu.hint': "Attivo: il clic destro su una riga di sessione apre il menu azioni (con pin, archiviazione e ferma-e-archivia ufficiali). Disattivo: la riga mostra il pulsante ⋯ ufficiale al passaggio, aprendo lo stesso menu. Attivo per impostazione predefinita.",
1034
+ 'settings.rowActions': "Pulsanti al passaggio della sessione",
1035
+ 'settings.rowActions.hint': "Mostra pulsanti rapidi di pin / archiviazione in fondo alla riga al passaggio, come le righe ufficiali. Disattivo: queste azioni restano solo nel menu. Attivo per impostazione predefinita.",
1004
1036
  'settings.appearance': 'Aspetto predefinito',
1005
1037
  'settings.appearance.hint': 'Le righe mai personalizzate usano questo aspetto; il contorno del testo è attivo per impostazione predefinita — senza di esso il testo è spesso illeggibile sopra un\'immagine di sfondo. Lascia il colore vuoto per seguire il tema.',
1006
1038
  'settings.appearance.reset': 'Ripristina l\'aspetto predefinito',
@@ -1131,6 +1163,10 @@ window.__ModuleLoader__.load({
1131
1163
  'settings.workspaceSlash.hint': 'オフ:ワークスペースは公式フォルダー(ディスクのディレクトリ)だけで入れ子になり、タイトルはそのまま表示されます。オン:タイトル内の「/」が再びグループ階層になります(「グループ/名前」)。既定はオフ、対象はワークスペースのみ — セッションのグループ化には専用のスイッチがあります。',
1132
1164
  'settings.statusPulse': 'ステータス表示灯',
1133
1165
  'settings.statusPulse.hint': '折りたたみで隠れたステータス表示(完了は緑 / 実行中は青 / 操作待ちは琥珀)が上位の階層へ伝わります:ワークスペースとフォルダーの行はアイコンがステータス色で明滅し(グローを設定したタイトルも連動),セッショングループの行には明滅するステータスドットが出ます;既定はオンで,ここでオフにできます。',
1166
+ 'settings.sessionMenu': "セッション右クリックメニュー",
1167
+ 'settings.sessionMenu.hint': "オン:セッション行を右クリックすると操作メニューを開きます(公式のピン留め・アーカイブ・停止してアーカイブを含む)。オフ:行にカーソルを合わせると公式スタイルの ⋯ ボタンが表示され、同じメニューを開きます。既定はオン。",
1168
+ 'settings.rowActions': "セッション行のホバーボタン",
1169
+ 'settings.rowActions.hint': "セッション行にカーソルを合わせると行末にピン / アーカイブのクイックボタンを表示し、公式の行と揃えます。オフ:これらの操作はメニュー内のみ。既定はオン。",
1134
1170
  'settings.appearance': '既定の外観',
1135
1171
  'settings.appearance.hint': '個別にカスタマイズしていない行に適用される外観です;文字の縁取りは既定でオンです——背景画像の上では縁取りのない文字が読みにくいためです。色を空にするとテーマに従います。',
1136
1172
  'settings.appearance.reset': '既定の外観に戻す',
@@ -1261,6 +1297,10 @@ window.__ModuleLoader__.load({
1261
1297
  'settings.workspaceSlash.hint': '끄면: 작업 공간은 공식 폴더(디스크 디렉터리)로만 중첩되고 제목은 그대로 표시됩니다. 켜면: 제목의 "/"가 다시 그룹 단계가 됩니다("그룹/이름"). 기본값은 끔이며 작업 공간에만 적용됩니다 — 세션 그룹화에는 별도 스위치가 있습니다.',
1262
1298
  'settings.statusPulse': '상태 표시등',
1263
1299
  'settings.statusPulse.hint': '접혀서 가려진 상태 표시(완료는 초록 / 실행은 파랑 / 대기는 호박색)가 상위 단계로 번집니다: 워크스페이스와 폴더 행은 아이콘이 상태 색으로 깜빡이고(글로우를 지정한 제목도 함께 깜빡임), 세션 그룹 행에는 깜빡이는 상태 점이 표시됩니다; 기본값은 켜짐이며 여기서 끌 수 있습니다.',
1300
+ 'settings.sessionMenu': "세션 우클릭 메뉴",
1301
+ 'settings.sessionMenu.hint': "켬: 세션 행을 우클릭하면 작업 메뉴가 열립니다(공식 고정, 보관, 중지 후 보관 포함). 끔: 행에 마우스를 올리면 공식 스타일의 ⋯ 버튼이 표시되어 같은 메뉴를 엽니다. 기본값은 켬.",
1302
+ 'settings.rowActions': "세션 행 호버 버튼",
1303
+ 'settings.rowActions.hint': "세션 행에 마우스를 올리면 행 끝에 고정 / 보관 빠른 버튼을 표시하여 공식 행과 일치시킵니다. 끔: 이 작업들은 메뉴에만 남습니다. 기본값은 켬.",
1264
1304
  'settings.appearance': '기본 모양',
1265
1305
  'settings.appearance.hint': '따로 지정하지 않은 행에 적용되는 모양입니다; 글자 외곽선은 기본으로 켜져 있습니다——배경 이미지 위에서는 외곽선 없는 글자가 잘 보이지 않기 때문입니다. 색을 비우면 테마를 따릅니다.',
1266
1306
  'settings.appearance.reset': '기본 모양으로 되돌리기',
@@ -1391,6 +1431,10 @@ window.__ModuleLoader__.load({
1391
1431
  'settings.workspaceSlash.hint': 'Uit: werkruimten nesten alleen volgens de officiële mappen (schijfmappen) en titels blijven volledig. Aan: "/" in een titel wordt weer een groepsniveau ("groep/naam"). Standaard uit, alleen werkruimten — sessiegroepen hebben hun eigen schakelaar.',
1392
1432
  'settings.statusPulse': 'Pulserend statuslampje',
1393
1433
  'settings.statusPulse.hint': 'Statuslampjes die door het inklappen verborgen zijn (voltooid groen / actief blauw / wachtend amber) borrelen door de hiërarchie omhoog: werkruimte- en mapregels laten hun pictogram in de statuskleur pulseren (titels met eigen gloed pulseren mee), sessiegroepregels tonen een pulserend statusstipje; standaard aan, hier uit te zetten.',
1434
+ 'settings.sessionMenu': "Sessie-contextmenu",
1435
+ 'settings.sessionMenu.hint': "Aan: rechtermuisklik op een sessierij opent het actiemenu (inclusief officieel vastmaken, archiveren en stoppen-en-archiveren). Uit: bij aanwijzen verschijnt in plaats daarvan de officiële ⋯-knop aan het rij-einde, die hetzelfde menu opent. Standaard aan.",
1436
+ 'settings.rowActions': "Sessierij-snelknoppen",
1437
+ 'settings.rowActions.hint': "Toont snelknoppen voor vastmaken / archiveren aan het einde van een aangewezen sessierij, gelijk aan de officiële rijen. Uit: deze acties blijven alleen in het menu. Standaard aan.",
1394
1438
  'settings.appearance': 'Standaarduiterlijk',
1395
1439
  'settings.appearance.hint': 'Regels zonder eigen aanpassing gebruiken dit uiterlijk; de tekstomtrek staat standaard aan — zonder omtrek is tekst op een achtergrondafbeelding vaak slecht leesbaar. Laat de kleur leeg om het thema te volgen.',
1396
1440
  'settings.appearance.reset': 'Standaarduiterlijk herstellen',
@@ -1521,6 +1565,10 @@ window.__ModuleLoader__.load({
1521
1565
  'settings.workspaceSlash.hint': 'Wył.: obszary robocze zagnieżdżają się wyłącznie według oficjalnych folderów (katalogów dysku), a tytuły są pełne. Wł.: "/" w tytule znów staje się poziomem grupy ("grupa/nazwa"). Domyślnie wył., dotyczy tylko obszarów roboczych — grupy sesji mają własny przełącznik.',
1522
1566
  'settings.statusPulse': 'Pulsująca lampka stanu',
1523
1567
  'settings.statusPulse.hint': 'Lampki ukryte przez zwinięcie (ukończono zielona / trwa niebieska / czeka bursztynowa) bąbelkują w górę hierarchii: wiersze obszarów roboczych i folderów pulsują ikoną w kolorze stanu (tytuły z własną poświatą pulsują razem), wiersze grup sesji pokazują pulsującą kropkę stanu; domyślnie włączone, tutaj można wyłączyć.',
1568
+ 'settings.sessionMenu': "Menu kontekstowe sesji",
1569
+ 'settings.sessionMenu.hint': "Włączone: kliknięcie prawym przyciskiem wiersza sesji otwiera jego menu akcji (w tym oficjalne przypinanie, archiwizację i zatrzymanie z archiwizacją). Wyłączone: przy najechaniu wiersz pokazuje oficjalny przycisk ⋯ otwierający to samo menu. Domyślnie włączone.",
1570
+ 'settings.rowActions': "Przyciski najechania wiersza sesji",
1571
+ 'settings.rowActions.hint': "Pokazuje szybkie przyciski przypnij / archiwizuj na końcu najechanego wiersza sesji, jak w oficjalnych wierszach. Wyłączone: te akcje zostają tylko w menu. Domyślnie włączone.",
1524
1572
  'settings.appearance': 'Domyślny wygląd',
1525
1573
  'settings.appearance.hint': 'Wiersze bez własnych ustawień używają tego wyglądu; obrys tekstu jest domyślnie włączony — bez niego tekst na obrazie tła bywa nieczytelny. Puste pole koloru oznacza podążanie za motywem.',
1526
1574
  'settings.appearance.reset': 'Przywróć domyślny wygląd',
@@ -1651,6 +1699,10 @@ window.__ModuleLoader__.load({
1651
1699
  'settings.workspaceSlash.hint': 'Desligado: as áreas de trabalho aninham-se apenas pelas pastas oficiais (diretórios do disco) e os títulos ficam inteiros. Ligado: a "/" no título volta a ser um nível de grupo ("grupo/nome"). Desligado por padrão, só áreas de trabalho — os grupos de sessões têm o próprio interruptor.',
1652
1700
  'settings.statusPulse': 'Luz de status pulsante',
1653
1701
  'settings.statusPulse.hint': 'As luzes escondidas pelo recolhimento (concluído verde / em execução azul / aguardando âmbar) sobem pela hierarquia: linhas de espaço de trabalho e pasta pulsam o ícone na cor do status (títulos com brilho personalizado pulsam junto), linhas de grupo de sessões mostram um ponto pulsante; ativado por padrão, pode ser desligado aqui.',
1702
+ 'settings.sessionMenu': "Menu de contexto da sessão",
1703
+ 'settings.sessionMenu.hint': "Ligado: clicar com o botão direito numa linha de sessão abre o menu de ações (incluindo fixar, arquivar e parar-e-arquivar oficiais). Desligado: a linha mostra o botão ⋯ oficial ao passar o cursor, abrindo o mesmo menu. Ligado por predefinição.",
1704
+ 'settings.rowActions': "Botões de passagem da sessão",
1705
+ 'settings.rowActions.hint': "Mostra botões rápidos de fixar / arquivar no fim da linha ao passar o cursor, como nas linhas oficiais. Desligado: essas ações ficam apenas no menu. Ligado por predefinição.",
1654
1706
  'settings.appearance': 'Aparência padrão',
1655
1707
  'settings.appearance.hint': 'Linhas sem personalização usam esta aparência; o contorno do texto vem ativado por padrão — sem ele, o texto costuma ficar ilegível sobre uma imagem de fundo. Deixe a cor vazia para seguir o tema.',
1656
1708
  'settings.appearance.reset': 'Restaurar aparência padrão',
@@ -1781,6 +1833,10 @@ window.__ModuleLoader__.load({
1781
1833
  'settings.workspaceSlash.hint': 'Выкл.: рабочие области вкладываются только по официальным папкам (каталогам диска), заголовки показываются целиком. Вкл.: «/» в заголовке снова становится уровнем группы («группа/имя»). По умолчанию выкл., только рабочие области — у групп сессий свой переключатель.',
1782
1834
  'settings.statusPulse': 'Пульсирующий индикатор состояния',
1783
1835
  'settings.statusPulse.hint': 'Индикаторы, скрытые сворачиванием (завершено — зелёный / выполняется — синий / ожидает — янтарный), всплывают вверх по иерархии: строки рабочих областей и папок пульсируют значком в цвете состояния (заголовки со своим свечением пульсируют вместе с ними), строки групп сессий показывают пульсирующую точку; включено по умолчанию, здесь можно отключить.',
1836
+ 'settings.sessionMenu': "Контекстное меню сессии",
1837
+ 'settings.sessionMenu.hint': "Вкл: правый клик по строке сессии открывает меню действий (включая официальные закрепление, архивацию и «остановить и архивировать»). Выкл: при наведении в строке появляется официальная кнопка ⋯, открывающая то же меню. Включено по умолчанию.",
1838
+ 'settings.rowActions': "Кнопки наведения строки сессии",
1839
+ 'settings.rowActions.hint': "Показывает быстрые кнопки закрепить / архивировать в конце строки при наведении, как в официальных строках. Выкл: эти действия остаются только в меню. Включено по умолчанию.",
1784
1840
  'settings.appearance': 'Оформление по умолчанию',
1785
1841
  'settings.appearance.hint': 'Строки без своей настройки используют это оформление; обводка текста включена по умолчанию — без неё текст на фоновом изображении часто нечитаем. Пустой цвет означает следование теме.',
1786
1842
  'settings.appearance.reset': 'Вернуть оформление по умолчанию',
@@ -1911,6 +1967,10 @@ window.__ModuleLoader__.load({
1911
1967
  'settings.workspaceSlash.hint': 'Av: arbetsytor kapslas in enbart efter de officiella mapparna (diskmappar) och titlar visas hela. På: "/" i en titel blir åter en gruppnivå ("grupp/namn"). Av som standard, endast arbetsytor — sessionsgrupper har sin egen växlare.',
1912
1968
  'settings.statusPulse': 'Pulserande statuslampa',
1913
1969
  'settings.statusPulse.hint': 'Statuslampor som döljs av ihopfällningen (klart grön / körs blå / väntar bärnsten) bubblar uppåt i hierarkin: rader för arbetsytor och mappar pulserar ikonen i statusfärgen (titlar med egen glöd pulserar med), sessionsgrupper visar en pulserande prick; på som standard, stängs av här.',
1970
+ 'settings.sessionMenu': "Sessionens högerklicksmeny",
1971
+ 'settings.sessionMenu.hint': "På: högerklick på en sessionsrad öppnar dess åtgärdsmeny (inklusive officiell fästning, arkivering och stoppa-och-arkivera). Av: raden visar istället den officiella ⋯-knappen vid hovring, som öppnar samma meny. På som standard.",
1972
+ 'settings.rowActions': "Hovringsknappar för sessionsraden",
1973
+ 'settings.rowActions.hint': "Visar snabbknappar för fäst / arkivera i radens slut vid hovring, som de officiella raderna. Av: åtgärderna finns bara i menyn. På som standard.",
1914
1974
  'settings.appearance': 'Standardutseende',
1915
1975
  'settings.appearance.hint': 'Rader utan egen anpassning använder detta utseende; textkonturen är på som standard — utan den är texten ofta oläslig ovanpå en bakgrundsbild. Lämna färgen tom för att följa temat.',
1916
1976
  'settings.appearance.reset': 'Återställ standardutseende',
@@ -2041,6 +2101,10 @@ window.__ModuleLoader__.load({
2041
2101
  'settings.workspaceSlash.hint': 'ปิด: พื้นที่ทำงานซ้อนกันตามโฟลเดอร์ทางการ (ไดเรกทอรีบนดิสก์) เท่านั้น และชื่อเรื่องแสดงเต็ม ปิดเป็นค่าเริ่มต้น ใช้กับพื้นที่ทำงานเท่านั้น — การจัดกลุ่มเซสชันมีสวิตช์แยก',
2042
2102
  'settings.statusPulse': 'ไฟสถานะหายใจ',
2043
2103
  'settings.statusPulse.hint': 'ไฟสถานะที่ถูกย่อซ่อนไว้ (เสร็จแล้วสีเขียว / กำลังทำงานสีน้ำเงิน / รอตอบสนองสีเหลืองอำพัน) จะลอยขึ้นตามลำดับชั้น: แถวพื้นที่ทำงานและโฟลเดอร์จะให้ไอคอนหายใจเป็นสีตามสถานะ (ชื่อที่มีการเรืองแสงกำหนดเองก็หายใจตาม) แถวกลุ่มเซสชันจะแสดงจุดสถานะที่หายใจ เปิดไว้เป็นค่าเริ่มต้น ปิดได้ที่นี่',
2104
+ 'settings.sessionMenu': "เมนูคลิกขวาของเซสชัน",
2105
+ 'settings.sessionMenu.hint': "เปิด: คลิกขวาที่แถวเซสชันเพื่อเปิดเมนูการกระทำ (รวมปักหมุด, เก็บถาวร และหยุด-แล้ว-เก็บถาวรแบบทางการ) ปิด: เมื่อชี้ที่แถวจะแสดงปุ่ม ⋯ สไตล์ทางการเปิดเมนูเดียวกันแทน เปิดตามค่าเริ่มต้น",
2106
+ 'settings.rowActions': "ปุ่มเมื่อชี้แถวเซสชัน",
2107
+ 'settings.rowActions.hint': "แสดงปุ่มด่วนปักหมุด / เก็บถาวรท้ายแถวเมื่อชี้ ให้ตรงกับแถวทางการ ปิด: การกระทำเหล่านี้มีเฉพาะในเมนู เปิดตามค่าเริ่มต้น",
2044
2108
  'settings.appearance': 'รูปลักษณ์เริ่มต้น',
2045
2109
  'settings.appearance.hint': 'แถวที่ยังไม่ได้กำหนดเองจะใช้รูปลักษณ์นี้ เส้นขอบตัวอักษรเปิดไว้เป็นค่าเริ่มต้น — บนภาพพื้นหลัง ตัวอักษรที่ไม่มีเส้นขอบมักอ่านยาก เว้นสีว่างไว้เพื่อตามธีม',
2046
2110
  'settings.appearance.reset': 'คืนค่ารูปลักษณ์เริ่มต้น',
@@ -2171,6 +2235,10 @@ window.__ModuleLoader__.load({
2171
2235
  'settings.workspaceSlash.hint': 'Kapalı: çalışma alanları yalnızca resmî klasörlere (disk dizinleri) göre iç içe geçer ve başlıklar tam görünür. Açık: başlıktaki "/" yeniden bir grup düzeyi olur ("grup/ad"). Varsayılan kapalı, yalnızca çalışma alanları — oturum gruplamasının kendi anahtarı var.',
2172
2236
  'settings.statusPulse': 'Nefes alan durum ışığı',
2173
2237
  'settings.statusPulse.hint': 'Daraltmayla gizlenen durum ışıkları (tamamlandı yeşil / çalışıyor mavi / bekliyor amber) hiyerarşide yukarı taşar: çalışma alanı ve klasör satırlarının simgesi durum renginde nefes alır (özel ışıması olan başlıklar da birlikte nefes alır), oturum grubu satırları nefes alan bir durum noktası gösterir; varsayılan olarak açık, buradan kapatılabilir.',
2238
+ 'settings.sessionMenu': "Oturum sağ tık menüsü",
2239
+ 'settings.sessionMenu.hint': "Açık: oturum satırına sağ tıklayınca eylem menüsü açılır (resmî sabitleme, arşivleme ve durdurup-arşivleme dahil). Kapalı: satırın üzerine gelince resmî ⋯ düğmesi görünür ve aynı menüyü açar. Varsayılan açık.",
2240
+ 'settings.rowActions': "Oturum satırı düğmeleri",
2241
+ 'settings.rowActions.hint': "Üzerine gelince satır sonunda sabitle / arşivle hızlı düğmelerini gösterir, resmî satırlarla aynı. Kapalı: bu eylemler yalnızca menüde kalır. Varsayılan açık.",
2174
2242
  'settings.appearance': 'Varsayılan görünüm',
2175
2243
  'settings.appearance.hint': 'Özel olarak uyarlanmamış satırlar bu görünümü kullanır; metin konturu varsayılan olarak açıktır — arka plan görseli üzerinde kontursuz metin çoğu zaman okunmaz. Rengi boş bırakırsanız temayı izler.',
2176
2244
  'settings.appearance.reset': 'Varsayılan görünüme dön',
@@ -2301,6 +2369,10 @@ window.__ModuleLoader__.load({
2301
2369
  'settings.workspaceSlash.hint': 'Tắt: không gian làm việc lồng nhau chỉ theo thư mục chính thức (thư mục trên đĩa) và tiêu đề hiển thị nguyên vẹn. Bật: "/" trong tiêu đề lại trở thành một cấp nhóm ("nhóm/tên"). Mặc định tắt, chỉ áp dụng cho không gian làm việc — nhóm phiên có công tắc riêng.',
2302
2370
  'settings.statusPulse': 'Đèn trạng thái nhấp nháy',
2303
2371
  'settings.statusPulse.hint': 'Những đèn trạng thái bị thu gọn che đi (xong màu xanh lá / đang chạy màu xanh dương / đang chờ màu hổ phách) sẽ nổi dần lên các tầng trên: hàng không gian làm việc và thư mục cho biểu tượng nhấp nháy theo màu trạng thái (tiêu đề có đặt phát sáng cũng nhấp nháy theo), hàng nhóm phiên hiện một chấm trạng thái nhấp nháy; mặc định bật, có thể tắt tại đây.',
2372
+ 'settings.sessionMenu': "Menu chuột phải phiên",
2373
+ 'settings.sessionMenu.hint': "Bật: nhấp chuột phải vào dòng phiên để mở menu thao tác (bao gồm ghim, lưu trữ và dừng-rồi-lưu trữ chính thức). Tắt: dòng hiển thị nút ⋯ kiểu chính thức khi di chuột, mở cùng menu đó. Bật theo mặc định.",
2374
+ 'settings.rowActions': "Nút hover dòng phiên",
2375
+ 'settings.rowActions.hint': "Hiện nút nhanh ghim / lưu trữ ở cuối dòng khi di chuột, giống các dòng chính thức. Tắt: các thao tác này chỉ còn trong menu. Bật theo mặc định.",
2304
2376
  'settings.appearance': 'Giao diện mặc định',
2305
2377
  'settings.appearance.hint': 'Những hàng chưa tùy chỉnh riêng dùng giao diện này; viền chữ mặc định bật — trên ảnh nền, chữ không viền thường khó đọc. Để trống màu để theo chủ đề.',
2306
2378
  'settings.appearance.reset': 'Khôi phục giao diện mặc định',
@@ -2431,6 +2503,10 @@ window.__ModuleLoader__.load({
2431
2503
  'settings.workspaceSlash.hint': '關閉時工作區只按官方資料夾(磁碟目錄)嵌套,標題原樣顯示;開啟後標題裡的 / 重新成為分組層級(組名/名字)。預設關閉;只影響工作區,對話分組另有獨立開關。',
2432
2504
  'settings.statusPulse': '狀態呼吸燈',
2433
2505
  'settings.statusPulse.hint': '被摺疊遮住嘅狀態燈(完成綠 / 運行藍 / 待互動琥珀)會沿住層級向外冒泡:工作區同分組行會以圖示呼吸發光(顏色跟狀態,自訂過發光嘅標題一齊呼吸),對話分組行就會顯示呼吸狀態燈;預設開啟,可以喺度閂咗佢。',
2506
+ 'settings.sessionMenu': "會話右鍵選單",
2507
+ 'settings.sessionMenu.hint': "開啟時右鍵點擊會話行即可開啟操作選單(包含官方的置頂、封存、停止並封存等);關閉後改為在行尾懸停顯示官方樣式的 ⋯ 按鈕,點擊開啟同一個選單。預設開啟。",
2508
+ 'settings.rowActions': "會話行懸停按鈕",
2509
+ 'settings.rowActions.hint': "懸停會話行時在行尾顯示置頂 / 封存快捷按鈕,與官方行保持一致;關閉後這些動作只保留在選單裡。預設開啟。",
2434
2510
  'settings.appearance': '預設外觀',
2435
2511
  'settings.appearance.hint': '未單獨自訂過嘅行會用呢套外觀;字體描邊預設開啟——有背景圖嗰陣唔描邊嘅字經常睇唔清。字體顏色留空就會跟主題。',
2436
2512
  'settings.appearance.reset': '還原預設外觀',
@@ -2561,6 +2637,10 @@ window.__ModuleLoader__.load({
2561
2637
  'settings.workspaceSlash.hint': '關閉時工作區只按官方資料夾(磁碟目錄)嵌套,標題原樣顯示;開啟後標題裡的 / 重新成為分組層級(組名/名字)。預設關閉;只影響工作區,對話分組另有獨立開關。',
2562
2638
  'settings.statusPulse': '狀態呼吸燈',
2563
2639
  'settings.statusPulse.hint': '被摺疊遮住嘅狀態燈(完成綠 / 運行藍 / 待互動琥珀)會沿住層級向外冒泡:工作區同分組行會以圖示呼吸發光(顏色跟狀態,自訂過發光嘅標題一齊呼吸),對話分組行就會顯示呼吸狀態燈;預設開啟,可以喺度閂咗佢。',
2640
+ 'settings.sessionMenu': "會話右鍵選單",
2641
+ 'settings.sessionMenu.hint': "開啟時右鍵點擊會話行即可開啟操作選單(包含官方的置頂、歸檔、停止並歸檔等);關閉後改為在行尾懸停顯示官方樣式的 ⋯ 按鈕,點擊開啟同一個選單。預設開啟。",
2642
+ 'settings.rowActions': "會話行懸停按鈕",
2643
+ 'settings.rowActions.hint': "懸停會話行時在行尾顯示置頂 / 歸檔快捷按鈕,與官方行保持一致;關閉後這些動作只保留在選單裡。預設開啟。",
2564
2644
  'settings.appearance': '預設外觀',
2565
2645
  'settings.appearance.hint': '未單獨自訂過嘅行會用呢套外觀;字體描邊預設開啟——有背景圖嗰陣唔描邊嘅字經常睇唔清。字體顏色留空就會跟主題。',
2566
2646
  'settings.appearance.reset': '還原預設外觀',
@@ -2691,6 +2771,10 @@ window.__ModuleLoader__.load({
2691
2771
  'settings.workspaceSlash.hint': '關閉時工作區只按官方資料夾(磁碟目錄)巢狀,標題原樣顯示;開啟後標題裡的 / 重新成為群組層級(群組名/名字)。預設關閉;只影響工作區,工作階段分組另有獨立開關。',
2692
2772
  'settings.statusPulse': '狀態呼吸燈',
2693
2773
  'settings.statusPulse.hint': '被收合藏起的狀態燈(完成綠 / 執行藍 / 待互動琥珀)會沿著層級向外冒出:工作區與群組列以圖示呼吸發光(顏色隨狀態,自訂過發光的標題一起呼吸),工作階段群組列則顯示呼吸狀態燈;預設開啟,可在此關閉。',
2774
+ 'settings.sessionMenu': "會話右鍵選單",
2775
+ 'settings.sessionMenu.hint': "開啟時右鍵點擊會話列即可開啟動作選單(包含官方的釘選、封存、停止並封存等);關閉後改為在列尾懸停顯示官方樣式的 ⋯ 按鈕,點擊開啟同一個選單。預設開啟。",
2776
+ 'settings.rowActions': "會話列懸停按鈕",
2777
+ 'settings.rowActions.hint': "懸停會話列時在列尾顯示釘選 / 封存快捷按鈕,與官方列保持一致;關閉後這些動作只保留在選單裡。預設開啟。",
2694
2778
  'settings.appearance': '預設外觀',
2695
2779
  'settings.appearance.hint': '未個別自訂過的列會使用這套外觀;字型外框預設開啟——在背景圖片上沒有外框的文字常常看不清楚。字型顏色留空即跟隨佈景主題。',
2696
2780
  'settings.appearance.reset': '還原預設外觀',
@@ -2863,12 +2947,22 @@ window.__ModuleLoader__.load({
2863
2947
  if (ui[name]) return name
2864
2948
  const aliases = ICON_ALIASES[name]
2865
2949
  if (aliases) for (const alias of aliases) if (ui[alias]) return alias
2950
+ // Icon-set generation fallback (dsh 0.1.7): the primitives renamed the
2951
+ // whole product set from pixel-size suffixes (…16/…14/…20) to stroke
2952
+ // weights (…Regular/…Medium). Strip the trailing size and try the new
2953
+ // spellings so pre-0.1.7 names keep rendering on current hosts — the
2954
+ // header/menus call icon() with historical names directly.
2955
+ const base = typeof name === 'string' ? name.replace(/\d+$/, '') : ''
2956
+ if (base !== '' && base !== name) {
2957
+ if (ui[base + 'Regular']) return base + 'Regular'
2958
+ if (ui[base + 'Medium']) return base + 'Medium'
2959
+ }
2866
2960
  return ''
2867
2961
  }
2868
2962
 
2869
2963
  /** Render a primitives icon by name; unknown names degrade to null, never crash. */
2870
2964
  const icon = (name, size) => {
2871
- const C = ui[name]
2965
+ const C = ui[resolveIconName(name)]
2872
2966
  return C ? E(C, { size: size || 16 }) : null
2873
2967
  }
2874
2968
 
@@ -2891,6 +2985,35 @@ window.__ModuleLoader__.load({
2891
2985
  return p.kind || p.status || p.type || 'pending'
2892
2986
  }
2893
2987
 
2988
+ // Official visiblePendingKind (dsh 0.1.7 tree.ts): only the three
2989
+ // navigation kinds light the warning lamp; anything else a domain
2990
+ // publishes stays off the sidebar.
2991
+ const visiblePendingKind = (kind) => (kind === 'approval' || kind === 'plan-review' || kind === 'question' ? kind : undefined)
2992
+
2993
+ // In-place flavor for arrays built by the caller (same bucket order).
2994
+ const pinnedPartitionInPlace = (rows) => {
2995
+ const arranged = pinnedPartition(rows)
2996
+ if (arranged !== rows) rows.splice(0, rows.length, ...arranged)
2997
+ return rows
2998
+ }
2999
+
3000
+ // Official pinned-block partition (sectionMembers): placeholders first,
3001
+ // then the non-archived pinned rows, then everything else — preserving
3002
+ // each caller-side order inside every bucket.
3003
+ const pinnedPartition = (rows) => {
3004
+ if (!rows || rows.length === 0) return rows
3005
+ const blanks = []
3006
+ const pins = []
3007
+ const rest = []
3008
+ for (const row of rows) {
3009
+ if (row.pinned) pins.push(row)
3010
+ else if (row.blank) blanks.push(row)
3011
+ else rest.push(row)
3012
+ }
3013
+ if (pins.length === 0) return rows
3014
+ return [...blanks, ...pins, ...rest]
3015
+ }
3016
+
2894
3017
  const sessionTitleOf = (summary, t, rememberedTitle) => {
2895
3018
  if (!summary) return ''
2896
3019
  if (summary.blank) return t('session.new')
@@ -2906,14 +3029,21 @@ window.__ModuleLoader__.load({
2906
3029
  }
2907
3030
 
2908
3031
  /**
2909
- * Official visibility rule (dsh tree.ts sessionVisible): subagent children
2910
- * live in their parent's catalog, archived sessions are visible nowhere,
2911
- * and a blank row is the provisional New Session of the current selection.
3032
+ * Official visibility rule (dsh 0.1.7 tree.ts sessionVisible): subagent
3033
+ * children live in their parent's catalog, a blank row is the provisional
3034
+ * New Session of the current selection, and archived rows follow the
3035
+ * archived filter — 'default' hides them, 'show' mixes them into their
3036
+ * kept slots, 'only' restricts the view to them. Pre-filter hosts only
3037
+ * ever pass 'default' (the historical hide-archived behavior).
2912
3038
  */
2913
- const sessionVisible = (summary, current, archivedSet) => !!summary
2914
- && summary.origin !== 'subagent'
2915
- && !(archivedSet && archivedSet.has(summary.id))
2916
- && (!summary.blank || summary.id === current)
3039
+ const sessionVisible = (summary, current, archivedSet, archivedFilter) => {
3040
+ if (!summary || summary.origin === 'subagent') return false
3041
+ if (summary.blank && summary.id !== current) return false
3042
+ const archived = archivedSet && archivedSet.has(summary.id)
3043
+ if (archivedFilter === 'show') return true
3044
+ if (archivedFilter === 'only') return !!archived
3045
+ return !archived
3046
+ }
2917
3047
 
2918
3048
  /**
2919
3049
  * The "open" session, across BOTH host generations.
@@ -3388,6 +3518,8 @@ window.__ModuleLoader__.load({
3388
3518
  '.bw-row-actions{flex:none;display:none;align-items:center;gap:2px}',
3389
3519
  '.bw-row:hover .bw-row-actions{display:flex}',
3390
3520
  '.bw-row:hover .bw-row-time,.bw-row:hover .bw-row-count,.bw-row:hover .bw-schedule-badge{display:none}',
3521
+ '.bw-row-archived{opacity:.55}',
3522
+ '.bw-pin-indicator{flex:none;display:inline-flex;align-items:center;color:var(--dsw-alias-brand-primary,#5b8def);margin:0 4px}',
3391
3523
  '.bw-dot{flex:none;width:6px;height:6px;border-radius:50%;background:transparent}',
3392
3524
  '.bw-session-row{font-size:12.5px;color:var(--dsw-alias-label-secondary,#b8b8b8);min-height:26px}',
3393
3525
  '.bw-sgroup-row{font-size:12.5px;color:var(--dsw-alias-label-tertiary,#9a9a9a);min-height:24px}',
@@ -3509,7 +3641,7 @@ window.__ModuleLoader__.load({
3509
3641
  .split('/').map((s) => s.trim()).filter((s) => s !== '').join('/')
3510
3642
 
3511
3643
  const createViewStore = () => storeKit.defineStore({
3512
- init: () => ({ expanded: {}, sessionsExpanded: {}, sessionGroups: {}, sessionOrder: {}, prefs: { compactChains: true }, styling: {}, groupBy: 'workspace-tree', orderBy: 'manual', sessionTitleSlash: true, workspaceTitleSlash: false, folders: { ws: {}, sess: {} } }),
3644
+ init: () => ({ expanded: {}, sessionsExpanded: {}, sessionGroups: {}, sessionOrder: {}, prefs: { compactChains: true }, styling: {}, groupBy: 'workspace-tree', orderBy: 'manual', archivedFilter: 'default', sessionTitleSlash: true, workspaceTitleSlash: false, folders: { ws: {}, sess: {} } }),
3513
3645
  // NOTE: hydration REPLACES the state with the persisted whole value —
3514
3646
  // init defaults never merge. Every action must tolerate a missing key
3515
3647
  // (states persisted by older plugin versions lack sessionGroups), and
@@ -3558,6 +3690,7 @@ window.__ModuleLoader__.load({
3558
3690
  // (tree + both slash layers on + manual host order).
3559
3691
  setGroupBy: (d, value) => { d.groupBy = value },
3560
3692
  setOrderBy: (d, value) => { d.orderBy = value },
3693
+ setArchivedFilter: (d, value) => { d.archivedFilter = value === 'show' || value === 'only' ? value : 'default' },
3561
3694
  setSessionTitleSlash: (d, value) => { d.sessionTitleSlash = value !== false },
3562
3695
  setWorkspaceTitleSlash: (d, value) => { d.workspaceTitleSlash = value !== false },
3563
3696
  // Explicit (possibly EMPTY) groups, v0.17.0. A group has always been a
@@ -3683,6 +3816,10 @@ window.__ModuleLoader__.load({
3683
3816
  // simply never mirror and never sync, which is exactly the pre-0.9.5
3684
3817
  // browser-local behavior.
3685
3818
  let prefsScopeRef = null
3819
+ // 'settingsScope' (dsh <= 0.1.6, the shared ~/.dsh/settings.yaml home —
3820
+ // the only world where CROSS-DEVICE sync makes sense) or 'configForms'
3821
+ // (dsh >= 0.1.7, per-profile patch persistence). null = browser-local.
3822
+ let prefsScopeVia = null
3686
3823
  const HOST_SNAP_UNAVAILABLE = { status: 'unavailable', value: undefined }
3687
3824
  const hostPrefsOf = (snap) => (snap && snap.status === 'ready' && snap.value && typeof snap.value === 'object'
3688
3825
  ? snap.value
@@ -3842,7 +3979,7 @@ window.__ModuleLoader__.load({
3842
3979
  )
3843
3980
  }
3844
3981
 
3845
- function ConfirmDialog({ title, body, onConfirm, onClose, t }) {
3982
+ function ConfirmDialog({ title, body, confirmLabel, onConfirm, onClose, t }) {
3846
3983
  return E(ui.Modal, {
3847
3984
  open: true,
3848
3985
  onClose,
@@ -3850,7 +3987,7 @@ window.__ModuleLoader__.load({
3850
3987
  title,
3851
3988
  footer: E('div', { className: 'bw-modal-actions' },
3852
3989
  E(BTN, { variant: 'outline', onClick: onClose }, t('cancel')),
3853
- E(BTN, { variant: 'primary', onClick: onConfirm }, t('confirm')),
3990
+ E(BTN, { variant: 'primary', onClick: onConfirm }, confirmLabel || t('confirm')),
3854
3991
  ),
3855
3992
  }, E('div', { className: 'bw-modal-body' }, E('div', { className: 'bw-hint' }, body)), StyleNode())
3856
3993
  }
@@ -4333,7 +4470,7 @@ window.__ModuleLoader__.load({
4333
4470
  return s === 'ongoing' ? null : s
4334
4471
  }
4335
4472
 
4336
- function SessionRow({ node, depth, current, onOpen, onContextMenu, now, dropHalf, dragEvents, custStyle, breathing, t }) {
4473
+ function SessionRow({ node, depth, current, onOpen, onContextMenu, now, dropHalf, dragEvents, custStyle, breathing, sessionActions, t }) {
4337
4474
  const state = sessionStateOf(node)
4338
4475
  const status = state === null ? null : {
4339
4476
  state,
@@ -4343,12 +4480,61 @@ window.__ModuleLoader__.load({
4343
4480
  ? (node.running ? t('status.running') : t('status.subagents', { n: node.subagents }))
4344
4481
  : t('status.completed'),
4345
4482
  }
4483
+ // Official mirror surface (dsh 0.1.7 Rows.tsx): pinned rows carry the
4484
+ // pin marker; archived rows render grayed and never open; the hover
4485
+ // strip hosts the "..." trigger and the pin/archive quick buttons.
4486
+ // The strip replaces the time cell on hover exactly like the official
4487
+ // row, and its clicks never reach the row (stopPropagation).
4488
+ const ot = sessionActions ? sessionActions.ot : null
4489
+ const official = (key, params) => {
4490
+ if (typeof ot !== 'function') return null
4491
+ const value = ot(key, params)
4492
+ return typeof value === 'string' && value !== '' && value !== key ? value : null
4493
+ }
4494
+ const archivedTitle = official('row.archived')
4495
+ const pinnedTitle = official('row.pinned')
4496
+ const strip = !node.blank && sessionActions && (sessionActions.trigger || sessionActions.quick)
4497
+ ? E('span', { className: 'bw-row-actions', onClick: (e) => e.stopPropagation() },
4498
+ sessionActions.trigger ? E('button', {
4499
+ type: 'button',
4500
+ className: 'bw-icon-btn',
4501
+ 'aria-label': official('actions.session.aria', { name: node.leaf || node.title }) || '...',
4502
+ title: sessionActions.triggerTitle,
4503
+ onClick: (e) => sessionActions.openMenu(node, e),
4504
+ }, icon('IconEllipsisOutline16', 15)) : null,
4505
+ sessionActions.quick && sessionActions.pin && !node.archived ? E('button', {
4506
+ type: 'button',
4507
+ className: 'bw-icon-btn',
4508
+ 'aria-label': official(node.pinned ? 'menu.unpinSession' : 'menu.pinSession') || '',
4509
+ title: official(node.pinned ? 'actions.unpin' : 'actions.pin') || undefined,
4510
+ onClick: (e) => { e.stopPropagation(); sessionActions.pin(node.id, !node.pinned) },
4511
+ }, icon(node.pinned ? 'IconPinFillRegular' : 'IconPinOutlineRegular', 14)) : null,
4512
+ sessionActions.quick && sessionActions.archive ? E('button', {
4513
+ type: 'button',
4514
+ className: 'bw-icon-btn',
4515
+ 'aria-label': official(node.archived ? 'menu.unarchiveSession' : 'menu.archiveSession') || '',
4516
+ title: official(node.archived ? 'actions.unarchive' : 'actions.archive') || undefined,
4517
+ onClick: (e) => { e.stopPropagation(); sessionActions.archive(node.id, !node.archived) },
4518
+ }, icon(node.archived ? 'IconUnarchiveOutlineRegular' : 'IconArchiveOutline20', 14)) : null,
4519
+ )
4520
+ : null
4346
4521
  return E('div', {
4347
- className: cls('bw-row', 'bw-session-row', current && 'bw-row-current', dropHalf === 'before' && 'bw-drop-before', dropHalf === 'after' && 'bw-drop-after'),
4522
+ className: cls('bw-row', 'bw-session-row', current && 'bw-row-current',
4523
+ node.archived && 'bw-row-archived',
4524
+ dropHalf === 'before' && 'bw-drop-before', dropHalf === 'after' && 'bw-drop-after'),
4348
4525
  style: { paddingLeft: 8 + depth * 12, ...(custStyle || {}) },
4349
- onClick: () => onOpen(node.id),
4526
+ onClick: () => {
4527
+ if (node.archived) {
4528
+ // Official semantics (guardedOpen): an archived row is not
4529
+ // openable — the click explains instead of navigating.
4530
+ if (sessionActions && typeof sessionActions.explainArchived === 'function') sessionActions.explainArchived()
4531
+ return
4532
+ }
4533
+ onOpen(node.id)
4534
+ },
4350
4535
  onContextMenu: onContextMenu,
4351
4536
  role: 'treeitem',
4537
+ 'aria-description': node.archived ? archivedTitle || undefined : undefined,
4352
4538
  ...(dragEvents || {}),
4353
4539
  },
4354
4540
  E('span', { className: 'bw-row-icon', title: status ? status.title : undefined },
@@ -4365,6 +4551,8 @@ window.__ModuleLoader__.load({
4365
4551
  ? E('span', { className: 'bw-schedule-badge', role: 'img', 'aria-label': t('schedule.active'), title: t('schedule.active') }, icon('IconAlarmClockOutline16', 14))
4366
4552
  : null,
4367
4553
  E('span', { className: 'bw-row-time' }, timeLabel(node.updatedAt, now, t)),
4554
+ node.pinned ? E('span', { className: 'bw-pin-indicator', role: 'img', 'aria-label': pinnedTitle || undefined, title: pinnedTitle || undefined }, icon('IconPinFillRegular', 13)) : null,
4555
+ strip,
4368
4556
  )
4369
4557
  }
4370
4558
 
@@ -4405,9 +4593,25 @@ window.__ModuleLoader__.load({
4405
4593
  * name layer). The primitives Menu is feature-probed like every
4406
4594
  * component — without it the button simply never renders.
4407
4595
  */
4408
- function ViewOptionsMenu({ groupBy, orderBy, sessionSlash, onPick, onToggle, t }) {
4596
+ function ViewOptionsMenu({ groupBy, orderBy, sessionSlash, archivedFilter, onPick, onToggle, onFilterPick, ot, t }) {
4409
4597
  const [open, setOpen] = React.useState(false)
4410
4598
  if (typeof ui.Menu !== 'function') return null
4599
+ // Archived filter (dsh 0.1.7 official ViewOptionsMenu mirror): the
4600
+ // section rides the OFFICIAL dictionary — when the host's 'workspace'
4601
+ // dictionary lacks the keys (pre-filter hosts) the whole section stays
4602
+ // hidden, matching that host's native menu. ot(key) returns the key
4603
+ // itself for a missing entry, which is the detection.
4604
+ const official = (key) => {
4605
+ if (typeof ot !== 'function') return null
4606
+ const value = ot(key)
4607
+ return typeof value === 'string' && value !== '' && value !== key ? value : null
4608
+ }
4609
+ const filterLabel = official('filterBy.label')
4610
+ const showLabel = official('viewOptions.showArchived')
4611
+ const onlyLabel = official('viewOptions.onlyArchived')
4612
+ const hasFilter = filterLabel !== null && showLabel !== null && onlyLabel !== null
4613
+ && typeof onFilterPick === 'function'
4614
+ const filter = archivedFilter === 'show' || archivedFilter === 'only' ? archivedFilter : 'default'
4411
4615
  return E(ui.Menu, {
4412
4616
  open,
4413
4617
  onClose: () => { setOpen(false) },
@@ -4420,6 +4624,14 @@ window.__ModuleLoader__.load({
4420
4624
  { type: 'label', id: 'bw-order-by', text: t('viewOptions.orderBy') },
4421
4625
  { id: 'manual', label: t('viewOptions.manualOrder') },
4422
4626
  { id: 'updated', label: t('viewOptions.updatedOrder') },
4627
+ ...(hasFilter ? [
4628
+ { type: 'separator', id: 'bw-filter-sep' },
4629
+ { type: 'label', id: 'bw-filter-by', text: filterLabel },
4630
+ // Both items are exclusive toggles (official semantics):
4631
+ // re-picking the selected one returns to the default view.
4632
+ { id: 'show-archived', label: showLabel, icon: icon('IconArchiveOutline20', 16) },
4633
+ { id: 'only-archived', label: onlyLabel, icon: icon('IconArchiveCheckOutlineRegular', 16) },
4634
+ ] : []),
4423
4635
  { type: 'separator', id: 'bw-slash-sep' },
4424
4636
  // The workspace name layer moved OUT of this menu in v0.15: it is
4425
4637
  // opt-in now and lives in the settings card, where "why is my tree
@@ -4427,10 +4639,14 @@ window.__ModuleLoader__.load({
4427
4639
  { id: 'session-slash', label: t('viewOptions.sessionSlash'), disabled: groupBy === 'flat' },
4428
4640
  ],
4429
4641
  selectedIds: [groupBy, orderBy]
4430
- .concat(sessionSlash ? ['session-slash'] : []),
4642
+ .concat(sessionSlash ? ['session-slash'] : [])
4643
+ .concat(filter === 'show' ? ['show-archived'] : [])
4644
+ .concat(filter === 'only' ? ['only-archived'] : []),
4431
4645
  onSelect: (id) => {
4432
4646
  if (id === 'workspace' || id === 'workspace-tree' || id === 'flat') { onPick('groupBy', id); setOpen(false); return }
4433
4647
  if (id === 'manual' || id === 'updated') { onPick('orderBy', id); setOpen(false); return }
4648
+ if (id === 'show-archived') { onFilterPick(filter === 'show' ? 'default' : 'show'); setOpen(false); return }
4649
+ if (id === 'only-archived') { onFilterPick(filter === 'only' ? 'default' : 'only'); setOpen(false); return }
4434
4650
  if (id === 'session-slash') { onToggle('sessionTitleSlash', !sessionSlash); return }
4435
4651
  },
4436
4652
  align: 'end',
@@ -4478,6 +4694,18 @@ window.__ModuleLoader__.load({
4478
4694
  // filters them out (see ICON_PICKER_CHOICES).
4479
4695
  'IconPaperPlaneOutline14', 'IconShieldOutline16', 'IconPlanOutline14',
4480
4696
  'IconWrapLinesOutline16', 'IconCompactOutline16',
4697
+ // dsh 0.1.7 generation (resolveIconName filters them out on older
4698
+ // hosts): the renamed set added pin/archive-check/tree/list glyphs the
4699
+ // native browser now ships. Historical names above resolve through the
4700
+ // Regular/Medium generation fallback, so both spellings coexist here.
4701
+ 'IconPinOutlineRegular', 'IconPinFillRegular', 'IconSlidersTwoOutlineRegular',
4702
+ 'IconArchiveCheckOutlineRegular', 'IconUnarchiveOutlineRegular',
4703
+ 'IconWorkspaceTreeOutlineRegular', 'IconFlatListOutlineRegular',
4704
+ 'IconChevronsUpDownOutlineRegular', 'IconGaugeOutlineRegular',
4705
+ 'IconInfoOutlineRegular', 'IconWarningTriangleOutlineRegular',
4706
+ 'IconCompareSplitOutlineRegular', 'IconPluginPinwheelOutlineRegular',
4707
+ 'IconMicrophoneOutlineRegular', 'IconSendOutlineRegular',
4708
+ 'IconTreeCornerRegular', 'IconDeliverDocRegular', 'IconWrapFillRegular',
4481
4709
  ]
4482
4710
 
4483
4711
  /**
@@ -4767,6 +4995,8 @@ window.__ModuleLoader__.load({
4767
4995
  const localStyling = useStore ? (useStore(s => s.styling) || {}) : {}
4768
4996
  const compactChains = prefs.compactChains !== false
4769
4997
  const statusPulse = prefs.statusPulse !== false
4998
+ const sessionMenu = prefs.sessionMenu !== false
4999
+ const rowActions = prefs.rowActions !== false
4770
5000
  // Opt-in workspace name layer (v0.15): the same tri-state read the tree
4771
5001
  // uses — see workspaceSlashOf. Deliberately NOT a `prefs` entry: it rides
4772
5002
  // the view store's top-level key next to groupBy/orderBy and stays
@@ -4851,6 +5081,30 @@ window.__ModuleLoader__.load({
4851
5081
  }, E('span', { className: 'bw-switch-thumb' })),
4852
5082
  ),
4853
5083
  E('div', { className: 'bw-hint' }, t('settings.statusPulse.hint')),
5084
+ E('div', { className: 'bw-setting-row', style: { marginTop: 10 } },
5085
+ E('div', { className: 'bw-setting-label' }, t('settings.sessionMenu')),
5086
+ E('button', {
5087
+ type: 'button',
5088
+ role: 'switch',
5089
+ 'aria-checked': sessionMenu,
5090
+ 'aria-label': t('settings.sessionMenu'),
5091
+ className: cls('bw-switch', sessionMenu && 'bw-switch-on'),
5092
+ onClick: () => { setPref('sessionMenu', !sessionMenu) },
5093
+ }, E('span', { className: 'bw-switch-thumb' })),
5094
+ ),
5095
+ E('div', { className: 'bw-hint' }, t('settings.sessionMenu.hint')),
5096
+ E('div', { className: 'bw-setting-row', style: { marginTop: 10 } },
5097
+ E('div', { className: 'bw-setting-label' }, t('settings.rowActions')),
5098
+ E('button', {
5099
+ type: 'button',
5100
+ role: 'switch',
5101
+ 'aria-checked': rowActions,
5102
+ 'aria-label': t('settings.rowActions'),
5103
+ className: cls('bw-switch', rowActions && 'bw-switch-on'),
5104
+ onClick: () => { setPref('rowActions', !rowActions) },
5105
+ }, E('span', { className: 'bw-switch-thumb' })),
5106
+ ),
5107
+ E('div', { className: 'bw-hint' }, t('settings.rowActions.hint')),
4854
5108
  E('div', { className: 'bw-setting-label', style: { marginTop: 16 } }, t('settings.appearance')),
4855
5109
  E('div', { className: 'bw-hint' }, t('settings.appearance.hint')),
4856
5110
  E('div', { className: 'bw-appearance-box' },
@@ -4864,19 +5118,24 @@ window.__ModuleLoader__.load({
4864
5118
  E(BTN, { variant: 'outline', onClick: () => setAppearance(DEFAULT_APPEARANCE) }, t('settings.appearance.reset')),
4865
5119
  ),
4866
5120
  ),
4867
- E('div', { className: 'bw-setting-label', style: { marginTop: 16 } }, t('sync.title')),
4868
- E('div', { className: 'bw-hint' }, t('sync.desc')),
4869
- E('div', { className: 'bw-sync-modes' },
4870
- modeButton('overwrite', t('sync.mode.overwrite')),
4871
- modeButton('merge', t('sync.mode.merge')),
4872
- ),
4873
- E('div', { className: 'bw-sync-actions' },
4874
- syncActionButton(t(IS_DESKTOP_SURFACE ? 'sync.pull.web' : 'sync.pull.desktop'), onPull, true, !scopeLive || snap.status === 'loading'),
4875
- syncActionButton(t('sync.push'), onPush, false, !scopeLive),
4876
- ),
4877
- syncMsg !== null ? E('div', { className: 'bw-hint' }, syncMsg)
4878
- : (scopeLive ? null : E('div', { className: 'bw-hint' }, t('sync.off'))),
4879
- scopeLive && snap.status === 'loading' ? E('div', { className: 'bw-hint' }, t('sync.loading')) : null,
5121
+ // Manual cross-device sync exists only where a SHARED host home does
5122
+ // (dsh <= 0.1.6 settings.yaml; both surfaces read one file). On
5123
+ // dsh >= 0.1.7 settings persist per profile, so the section hides.
5124
+ prefsScopeVia === 'settingsScope' ? E(React.Fragment, null,
5125
+ E('div', { className: 'bw-setting-label', style: { marginTop: 16 } }, t('sync.title')),
5126
+ E('div', { className: 'bw-hint' }, t('sync.desc')),
5127
+ E('div', { className: 'bw-sync-modes' },
5128
+ modeButton('overwrite', t('sync.mode.overwrite')),
5129
+ modeButton('merge', t('sync.mode.merge')),
5130
+ ),
5131
+ E('div', { className: 'bw-sync-actions' },
5132
+ syncActionButton(t(IS_DESKTOP_SURFACE ? 'sync.pull.web' : 'sync.pull.desktop'), onPull, true, !scopeLive || snap.status === 'loading'),
5133
+ syncActionButton(t('sync.push'), onPush, false, !scopeLive),
5134
+ ),
5135
+ syncMsg !== null ? E('div', { className: 'bw-hint' }, syncMsg)
5136
+ : (scopeLive ? null : E('div', { className: 'bw-hint' }, t('sync.off'))),
5137
+ scopeLive && snap.status === 'loading' ? E('div', { className: 'bw-hint' }, t('sync.loading')) : null,
5138
+ ) : null,
4880
5139
  )
4881
5140
  }
4882
5141
 
@@ -4928,10 +5187,10 @@ window.__ModuleLoader__.load({
4928
5187
  function BetterBrowser(props) {
4929
5188
  const {
4930
5189
  wide, expandSidebar,
4931
- useSessions, useSessionPendingInteraction, useWorkspaces,
5190
+ useSessions, useSessionPendingInteraction, useSessionStatus, useWorkspaces,
4932
5191
  useStore, actions,
4933
5192
  startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace,
4934
- archiveSession, createWorkspace,
5193
+ archiveSession, createWorkspace, pinSession, unpinSession, unarchiveSession,
4935
5194
  insertWorkspaceBefore, insertSessionBefore,
4936
5195
  useDirectoryFlow,
4937
5196
  pickDirectory, listDirectory, createDirectory,
@@ -4947,8 +5206,20 @@ window.__ModuleLoader__.load({
4947
5206
  const items = useWorkspaces(s => s.items)
4948
5207
  const phase = useWorkspaces(s => s.phase)
4949
5208
  const archivedSessionIds = useWorkspaces(s => s.archivedSessionIds) || []
5209
+ // Registry-global pin set (dsh 0.1.7): absent on older hosts, which
5210
+ // simply never see pinned rows or pin actions.
5211
+ const pinnedSessionIds = useWorkspaces(s => s.pinnedSessionIds) || []
4950
5212
  const list = useSessions(s => s)
5213
+ // Session UI status, BOTH eras: dsh 0.1.7 replaced the pending-only
5214
+ // hook with the unified status map (running / pendingInteraction /
5215
+ // completionUnread). Prefer it when bound; the legacy hook stays the
5216
+ // pre-0.1.7 source, and summary fields fill whatever neither carries.
4951
5217
  const pending = useSessionPendingInteraction ? useSessionPendingInteraction(s => s) : null
5218
+ const statusMap = typeof useSessionStatus === 'function' ? useSessionStatus(s => s) : null
5219
+ const statusOf = (id) => {
5220
+ if (!statusMap || typeof statusMap.get !== 'function') return undefined
5221
+ return statusMap.get(id)
5222
+ }
4952
5223
  const expandedMap = useStore ? (useStore(s => s.expanded) || {}) : {}
4953
5224
  const sessionsExpandedMap = useStore ? (useStore(s => s.sessionsExpanded) || {}) : {}
4954
5225
  const sessionGroupsMap = useStore ? (useStore(s => s.sessionGroups) || {}) : {}
@@ -4975,7 +5246,16 @@ window.__ModuleLoader__.load({
4975
5246
  const shared = makeSharedWrites(actions, stylingMap)
4976
5247
  const compactChains = prefsMap.compactChains !== false
4977
5248
  const statusPulse = prefsMap.statusPulse !== false
5249
+ // Session-action surface gates (v0.20.0): the right-click menu and the
5250
+ // official-style hover strip can each be turned off; defaults follow
5251
+ // the plugin's historical behavior (right-click on, quick buttons on).
5252
+ const sessionMenuEnabled = prefsMap.sessionMenu !== false
5253
+ const rowActionsEnabled = prefsMap.rowActions !== false
5254
+ // Archived-row visibility (v0.20.0, official ArchivedFilter mirror);
5255
+ // persisted snapshots from older plugin versions lack the key.
5256
+ const archivedFilter = useStore ? (useStore(s => s.archivedFilter) || 'default') : 'default'
4978
5257
  const archivedSet = React.useMemo(() => new Set(archivedSessionIds), [archivedSessionIds])
5258
+ const pinnedSet = React.useMemo(() => new Set(pinnedSessionIds), [pinnedSessionIds])
4979
5259
  const subCounts = React.useMemo(() => subagentRunningCounts(list ? list.byId : {}), [list ? list.byId : null])
4980
5260
  // Current session across both host generations (see mainSessionIdOf).
4981
5261
  // `current` belongs in the deps: 0.1.5-rc.x switches it without
@@ -5201,21 +5481,25 @@ window.__ModuleLoader__.load({
5201
5481
  if (list && Array.isArray(list.ids)) {
5202
5482
  for (const id of list.ids) {
5203
5483
  const summary = list.byId[id]
5204
- if (accounted.has(id) || !sessionVisible(summary, currentId, archivedSet)) continue
5484
+ if (accounted.has(id) || !sessionVisible(summary, currentId, archivedSet, archivedFilter)) continue
5485
+ const st = statusOf(id)
5205
5486
  ungrouped.push({
5206
5487
  id,
5207
5488
  title: sessionTitleOf(summary, t, rememberedTitleOf(id)),
5208
5489
  leaf: sessionTitleOf(summary, t, rememberedTitleOf(id)),
5209
5490
  blank: !!summary.blank,
5210
- running: !!summary.running,
5211
- completed: summary.completed === true,
5491
+ running: (st && st.running === true) || !!summary.running,
5492
+ completed: summary.completed === true || (st && st.completionUnread === true),
5212
5493
  hasActiveSchedule: hasActiveScheduleOf(summary),
5213
5494
  subagents: subCounts.get(id) || 0,
5214
5495
  updatedAt: summary.updatedAt || 0,
5215
- pending: pendingKindOf(pending, id),
5496
+ pending: visiblePendingKind(st && st.pendingInteraction && st.pendingInteraction.kind) || pendingKindOf(pending, id),
5497
+ pinned: pinnedSet.has(id) && !archivedSet.has(id),
5498
+ archived: archivedSet.has(id),
5216
5499
  })
5217
5500
  }
5218
5501
  ungrouped.sort((a, b) => b.updatedAt - a.updatedAt)
5502
+ pinnedPartitionInPlace(ungrouped)
5219
5503
  }
5220
5504
 
5221
5505
  // While a workspace drag is active, single-child chains render UNCOMPRESSED:
@@ -5252,18 +5536,21 @@ window.__ModuleLoader__.load({
5252
5536
  const rows = []
5253
5537
  for (const id of workspace.sessionIds || []) {
5254
5538
  const summary = list && list.byId ? list.byId[id] : undefined
5255
- if (!sessionVisible(summary, currentId, archivedSet)) continue
5539
+ if (!sessionVisible(summary, currentId, archivedSet, archivedFilter)) continue
5540
+ const st = statusOf(id)
5256
5541
  rows.push({
5257
5542
  id,
5258
5543
  title: sessionTitleOf(summary, t, rememberedTitleOf(id)),
5259
5544
  leaf: sessionTitleOf(summary, t, rememberedTitleOf(id)),
5260
5545
  blank: !!summary.blank,
5261
- running: !!summary.running,
5262
- completed: summary.completed === true,
5546
+ running: (st && st.running === true) || !!summary.running,
5547
+ completed: summary.completed === true || (st && st.completionUnread === true),
5263
5548
  hasActiveSchedule: hasActiveScheduleOf(summary),
5264
5549
  subagents: subCounts.get(id) || 0,
5265
5550
  updatedAt: summary.updatedAt || 0,
5266
- pending: pendingKindOf(pending, id),
5551
+ pending: visiblePendingKind(st && st.pendingInteraction && st.pendingInteraction.kind) || pendingKindOf(pending, id),
5552
+ pinned: pinnedSet.has(id) && !archivedSet.has(id),
5553
+ archived: archivedSet.has(id),
5267
5554
  })
5268
5555
  }
5269
5556
  // Browser-local reorder fallback (dsh 0.1.6-alpha.1 stopped injecting
@@ -5284,10 +5571,10 @@ window.__ModuleLoader__.load({
5284
5571
  const at = sorted.indexOf(pin)
5285
5572
  if (at > 0) { sorted.splice(at, 1); sorted.unshift(pin) }
5286
5573
  }
5287
- return sorted
5574
+ return pinnedPartition(sorted)
5288
5575
  }
5289
5576
  const local = typeof insertSessionBefore === 'function' ? [] : sessionOrderOf(workspace.workspaceId)
5290
- if (local.length === 0) return rows
5577
+ if (local.length === 0) return pinnedPartition(rows)
5291
5578
  const remaining = new Map(rows.map((row) => [row.id, row]))
5292
5579
  const out = []
5293
5580
  for (const id of local) {
@@ -5295,7 +5582,7 @@ window.__ModuleLoader__.load({
5295
5582
  if (row) { out.push(row); remaining.delete(id) }
5296
5583
  }
5297
5584
  for (const row of rows) if (remaining.has(row.id)) out.push(row)
5298
- return out
5585
+ return pinnedPartition(out)
5299
5586
  }
5300
5587
 
5301
5588
  // Search flattens BOTH layers depth-first: every workspace (top level,
@@ -5383,6 +5670,60 @@ window.__ModuleLoader__.load({
5383
5670
  setCtx({ kind, payload, x: e.clientX, y: e.clientY })
5384
5671
  }
5385
5672
 
5673
+ // Session-action surface (v0.20.0, official mirror): the right-click
5674
+ // menu, the hover "..." trigger, and the pin/archive quick buttons all
5675
+ // drive the same calls the official row entries make — pin/unpin and
5676
+ // unarchive go straight to the uiWorkspace service; archive raises the
5677
+ // stop-and-archive confirmation when the Host refuses a running
5678
+ // session (WorkspaceArchiveError, workspace/session-active). Copy rides
5679
+ // the official 'workspace' dictionary (auto-rewording), with graceful
5680
+ // degradation on hosts that lack a key.
5681
+ const officialText = (key, params, fallback) => {
5682
+ const value = typeof officialT === 'function' ? officialT(key, params) : null
5683
+ return typeof value === 'string' && value !== '' && value !== key ? value : fallback
5684
+ }
5685
+ const pinAvailable = typeof pinSession === 'function' && typeof unpinSession === 'function'
5686
+ const unarchiveAvailable = typeof unarchiveSession === 'function'
5687
+ const sessionActions = {
5688
+ ot: officialT,
5689
+ // Right-click is the default trigger; turning the setting off moves
5690
+ // the trigger to the official-style "..." hover button.
5691
+ trigger: !sessionMenuEnabled,
5692
+ quick: rowActionsEnabled,
5693
+ pin: pinAvailable
5694
+ ? (id, pinning) => {
5695
+ Promise.resolve().then(() => (pinning ? pinSession : unpinSession)(id)).catch((reason) => {
5696
+ fail(officialText(pinning ? 'toast.pinFailed' : 'toast.unpinFailed', undefined, reason instanceof Error ? reason.message : String(reason || 'pin failed')))
5697
+ })
5698
+ }
5699
+ : undefined,
5700
+ archive: typeof archiveSession === 'function'
5701
+ ? (id, archiving) => {
5702
+ if (!archiving) {
5703
+ if (!unarchiveAvailable) return
5704
+ Promise.resolve().then(() => unarchiveSession(id)).catch((reason) => { console.warn('session unarchive rejected:', reason) })
5705
+ return
5706
+ }
5707
+ Promise.resolve().then(() => archiveSession(id)).catch((reason) => {
5708
+ // Official refusal flow: running work refuses the plain archive
5709
+ // (WorkspaceArchiveError / workspace/session-active) and the
5710
+ // confirmation offers to stop it first. Anything else reports
5711
+ // through the error modal.
5712
+ if (reason && reason.name === 'WorkspaceArchiveError') {
5713
+ setDialog({ kind: 'sess-archive-confirm', sessionId: id })
5714
+ return
5715
+ }
5716
+ fail(reason)
5717
+ })
5718
+ }
5719
+ : undefined,
5720
+ openMenu: (node, e) => openCtx('session', node, e),
5721
+ explainArchived: () => {
5722
+ const text = officialText('toast.archivedNotOpenable', undefined, '') || officialText('row.archived', undefined, '')
5723
+ if (text) setErrorText(text)
5724
+ },
5725
+ }
5726
+
5386
5727
  /* ----------------------- group move helpers --------------------- */
5387
5728
  // Grouping is a NAME PROJECTION (v0.12.0 retired explicit empty
5388
5729
  // groups): a "group" is nothing but the slash-prefix of a title, so
@@ -5884,6 +6225,13 @@ window.__ModuleLoader__.load({
5884
6225
  // the same thing.
5885
6226
  const target = flat.find(s => s.id === targetSessionId)
5886
6227
  const nextTitle = sessionSlash && target ? titleMovedToGroup(source.title, target.title) : null
6228
+ // Official pinned-block rule: a pinned row reorders only among pinned
6229
+ // rows. A cross-block drop that would be a pure reorder is ignored;
6230
+ // group moves keep working (the pin fronts the row in its new group).
6231
+ if (target && nextTitle === null) {
6232
+ const sourceRow = flat.find(s => s.id === source.sessionId)
6233
+ if (sourceRow && !!sourceRow.pinned !== !!target.pinned) return
6234
+ }
5887
6235
  const reorder = () => {
5888
6236
  if (typeof insertSessionBefore === 'function') {
5889
6237
  return (anchor !== undefined ? insertSessionBefore(workspaceId, source.sessionId, anchor) : insertSessionBefore(workspaceId, source.sessionId))
@@ -6115,9 +6463,10 @@ window.__ModuleLoader__.load({
6115
6463
  now,
6116
6464
  onOpen: (id) => open(id),
6117
6465
  onContextMenu: (e) => openCtx('session', session, e),
6118
- dropHalf: workspaceId ? sessDropHalf(session.id) : null,
6119
- dragEvents: workspaceId ? sessionDragEvents(session, workspaceId) : undefined,
6466
+ dropHalf: workspaceId && !session.archived ? sessDropHalf(session.id) : null,
6467
+ dragEvents: workspaceId && !session.archived ? sessionDragEvents(session, workspaceId) : undefined,
6120
6468
  custStyle: rowStyleOf('session:' + session.id),
6469
+ sessionActions,
6121
6470
  breathing: statusPulse,
6122
6471
  t,
6123
6472
  })
@@ -6286,18 +6635,42 @@ window.__ModuleLoader__.load({
6286
6635
  ...wsGroupItems,
6287
6636
  customizeEntry,
6288
6637
  ]
6289
- if (ctx.kind === 'session') return [
6290
- { id: 'rename', label: ot ? ot('rename') : t('menu.rename'), icon: icon('IconEditOutline16', 16) },
6291
- { id: 'fork', label: ot ? ot('menu.fork') : t('menu.fork'), icon: icon('IconBranchOutline16', 16) },
6292
- // Official semantics (Rows.tsx): archive hides the row through the
6293
- // registry-global archive set and never touches the session log —
6294
- // deliberately NOT styled destructive.
6295
- { id: 'archive', label: ot ? ot('menu.archiveSession') : t('menu.archive'), icon: icon('IconArchiveOutline20', 16) },
6296
- { type: 'separator', id: 'bw-sep' },
6297
- ...sessionUngroupItems,
6298
- ...sgroupItems,
6299
- customizeEntry,
6300
- ]
6638
+ if (ctx.kind === 'session') {
6639
+ // Official 0.1.7 mirror (Rows.tsx session menu + the shipped slot
6640
+ // entries): pin, rename, fork, archive — same order, same icons,
6641
+ // same archived-state behavior (pin hides; archive becomes
6642
+ // unarchive). Copy rides the official dictionary, so upstream
6643
+ // rewording AND future item additions land without a plugin
6644
+ // release; every verb is gated on its service method so older
6645
+ // hosts simply never offer what they cannot do.
6646
+ const payload = ctx.payload || {}
6647
+ const archived = !!(payload.archived || (archivedSet && archivedSet.has(payload.id)))
6648
+ const pinned = !!(payload.pinned || (pinnedSet && pinnedSet.has(payload.id)))
6649
+ const official = (key, fallback) => {
6650
+ const value = typeof ot === 'function' ? ot(key) : null
6651
+ return typeof value === 'string' && value !== '' && value !== key ? value : fallback
6652
+ }
6653
+ return [
6654
+ ...(pinAvailable && !archived ? [{
6655
+ id: pinned ? 'unpin' : 'pin',
6656
+ label: official(pinned ? 'menu.unpinSession' : 'menu.pinSession', t('menu.archive')),
6657
+ icon: icon(pinned ? 'IconPinFillRegular' : 'IconPinOutlineRegular', 16),
6658
+ }] : []),
6659
+ { id: 'rename', label: official('rename', t('menu.rename')), icon: icon('IconEditOutline16', 16) },
6660
+ { id: 'fork', label: official('menu.fork', t('menu.fork')), icon: icon('IconBranchOutline16', 16) },
6661
+ // Official semantics: archive hides the row through the
6662
+ // registry-global archive set and never touches the session log
6663
+ // — deliberately NOT styled destructive. An archived row gets
6664
+ // the restore entry instead.
6665
+ archived
6666
+ ? { id: 'unarchive', label: official('menu.unarchiveSession', t('menu.archive')), icon: icon('IconUnarchiveOutlineRegular', 16) }
6667
+ : { id: 'archive', label: official('menu.archiveSession', t('menu.archive')), icon: icon('IconArchiveOutline20', 16) },
6668
+ { type: 'separator', id: 'bw-sep' },
6669
+ ...sessionUngroupItems,
6670
+ ...sgroupItems,
6671
+ customizeEntry,
6672
+ ]
6673
+ }
6301
6674
  return [
6302
6675
  { id: 'rename-sgroup', label: t('menu.renameSgroup'), icon: icon('IconEditOutline16', 16) },
6303
6676
  ...(ctx.payload && ctx.payload.empty ? [deleteSgroupEntry] : []),
@@ -6336,7 +6709,23 @@ window.__ModuleLoader__.load({
6336
6709
  else if (kind === 'sgroup' && id === 'rename-sgroup') setDialog({ kind: 'sgroup-rename', target: payload })
6337
6710
  else if (kind === 'session' && id === 'rename') setDialog({ kind: 'sess-rename', session: payload })
6338
6711
  else if (kind === 'session' && id === 'fork') forkSession(payload.id)
6339
- else if (kind === 'session' && id === 'archive') { Promise.resolve().then(() => archiveSession(payload.id)).catch(fail) }
6712
+ else if (kind === 'session' && (id === 'pin' || id === 'unpin') && pinAvailable) {
6713
+ Promise.resolve().then(() => (id === 'pin' ? pinSession : unpinSession)(payload.id)).catch((reason) => {
6714
+ fail(officialText(id === 'pin' ? 'toast.pinFailed' : 'toast.unpinFailed', undefined, reason instanceof Error ? reason.message : String(reason || 'pin failed')))
6715
+ })
6716
+ }
6717
+ else if (kind === 'session' && id === 'unarchive' && unarchiveAvailable) {
6718
+ Promise.resolve().then(() => unarchiveSession(payload.id)).catch((reason) => { console.warn('session unarchive rejected:', reason) })
6719
+ }
6720
+ else if (kind === 'session' && id === 'archive' && typeof archiveSession === 'function') {
6721
+ Promise.resolve().then(() => archiveSession(payload.id)).catch((reason) => {
6722
+ if (reason && reason.name === 'WorkspaceArchiveError') {
6723
+ setDialog({ kind: 'sess-archive-confirm', sessionId: payload.id })
6724
+ return
6725
+ }
6726
+ fail(reason)
6727
+ })
6728
+ }
6340
6729
  // Plain retitle back to the leaf: the row leaves its group and returns
6341
6730
  // to the top level of its workspace (v0.18.0).
6342
6731
  else if (kind === 'session' && id === 'move-out') {
@@ -6408,6 +6797,27 @@ window.__ModuleLoader__.load({
6408
6797
  onClose: () => setDialog(null),
6409
6798
  t,
6410
6799
  })
6800
+ if (dialog.kind === 'sess-archive-confirm') {
6801
+ // Official stop-and-archive confirmation (dsh 0.1.7
6802
+ // SessionArchiveConfirmDialog mirror): the Host refused the plain
6803
+ // archive because this session still runs; confirming asks the
6804
+ // Host to stop that work first. Copy rides the official dictionary.
6805
+ const summary = list && list.byId ? list.byId[dialog.sessionId] : undefined
6806
+ const displayTitle = summary ? sessionTitleOf(summary, t, rememberedTitleOf(dialog.sessionId)) : ''
6807
+ return E(ConfirmDialog, {
6808
+ key: 'sess-archive-confirm',
6809
+ title: officialText('archive.confirm.title', undefined, t('menu.archive')),
6810
+ body: officialText('archive.confirm.desc', { title: displayTitle }, ''),
6811
+ confirmLabel: officialText('archive.confirm.action', undefined, undefined),
6812
+ onConfirm: () => {
6813
+ Promise.resolve().then(() => archiveSession(dialog.sessionId, { stopActivity: true }))
6814
+ .then(() => setDialog(null))
6815
+ .catch(fail)
6816
+ },
6817
+ onClose: () => setDialog(null),
6818
+ t,
6819
+ })
6820
+ }
6411
6821
  if (dialog.kind === 'sgroup-rename') return E(TextDialog, {
6412
6822
  key: 'sgroup-rename',
6413
6823
  title: t('menu.renameSgroup'),
@@ -6556,7 +6966,10 @@ window.__ModuleLoader__.load({
6556
6966
  groupBy,
6557
6967
  orderBy,
6558
6968
  sessionSlash,
6969
+ archivedFilter,
6559
6970
  onPick: (kind, value) => { if (kind === 'groupBy') actions.setGroupBy(value); else actions.setOrderBy(value) },
6971
+ onFilterPick: (value) => { if (actions && typeof actions.setArchivedFilter === 'function') actions.setArchivedFilter(value) },
6972
+ ot: officialT,
6560
6973
  onToggle: (key, value) => { if (key === 'sessionTitleSlash') actions.setSessionTitleSlash(value) },
6561
6974
  t,
6562
6975
  }),
@@ -6740,7 +7153,12 @@ window.__ModuleLoader__.load({
6740
7153
  insertSessionBefore: typeof workspaces.insertSessionBefore === 'function'
6741
7154
  ? (workspaceId, sessionId, beforeSessionId) => workspaces.insertSessionBefore(workspaceId, sessionId, beforeSessionId)
6742
7155
  : undefined,
6743
- archiveSession: (sessionId) => uiWorkspace.archiveSession(sessionId),
7156
+ // options rides through for the 0.1.7 stop-and-archive hop; older
7157
+ // services simply never receive a second argument.
7158
+ archiveSession: (sessionId, options) => uiWorkspace.archiveSession(sessionId, options),
7159
+ pinSession: typeof uiWorkspace.pinSession === 'function' ? (sessionId) => uiWorkspace.pinSession(sessionId) : undefined,
7160
+ unpinSession: typeof uiWorkspace.unpinSession === 'function' ? (sessionId) => uiWorkspace.unpinSession(sessionId) : undefined,
7161
+ unarchiveSession: typeof uiWorkspace.unarchiveSession === 'function' ? (sessionId) => uiWorkspace.unarchiveSession(sessionId) : undefined,
6744
7162
  createWorkspace: (input) => workspaces.create(input),
6745
7163
  pickDirectory: () => uiWorkspace.pickDirectory(),
6746
7164
  // Browse-side primitives: the in-app dialog needs them whenever the
@@ -6788,14 +7206,38 @@ window.__ModuleLoader__.load({
6788
7206
  // block). Binding is best-effort: a missing/failed bind leaves the
6789
7207
  // plugin fully browser-local — the pre-0.9.5 behavior.
6790
7208
  prefsScopeRef = null
7209
+ prefsScopeVia = null
6791
7210
  try {
6792
7211
  if (ctx.settingsScope && typeof ctx.settingsScope.bind === 'function') {
6793
7212
  prefsScopeRef = ctx.settingsScope.bind({ namespace: 'better-workspace' })
7213
+ prefsScopeVia = 'settingsScope'
6794
7214
  }
6795
7215
  } catch (error) {
6796
7216
  console.warn('[dsh-better-workspace] settings scope bind failed; sync stays local', error)
6797
7217
  prefsScopeRef = null
6798
7218
  }
7219
+ // dsh >= 0.1.7: the settings service is gone; one ConfigForm per live
7220
+ // profile entry replaces it (same getSnapshot/set/subscribe contract).
7221
+ // Values then persist in THIS profile's patch — there is no shared home
7222
+ // any more, so the manual cross-device sync section stays hidden.
7223
+ try {
7224
+ if (ctx.inject) {
7225
+ ctx.inject(['configForms'], (fctx) => {
7226
+ try {
7227
+ if (prefsScopeVia === 'settingsScope') return
7228
+ const forms = fctx && fctx.configForms
7229
+ if (forms && typeof forms.get === 'function') {
7230
+ prefsScopeRef = forms.get('better-workspace')
7231
+ prefsScopeVia = 'configForms'
7232
+ }
7233
+ } catch (error) {
7234
+ console.warn('[dsh-better-workspace] configForms acquisition failed', error)
7235
+ }
7236
+ })
7237
+ }
7238
+ } catch (error) {
7239
+ console.warn('[dsh-better-workspace] configForms wiring failed', error)
7240
+ }
6799
7241
 
6800
7242
  // Settings → Plugins card only (the tab dispatches the intersection of
6801
7243
  // served namespaces — registered host-side — and settings.plugin.item
@@ -6849,7 +7291,7 @@ window.__ModuleLoader__.load({
6849
7291
 
6850
7292
  return {
6851
7293
  name: 'dsh-better-workspace',
6852
- inject: ['slots', 'sessions', 'workspaces', 'locale', 'uiWorkspace', 'settingsScope'],
7294
+ inject: ['slots', 'sessions', 'workspaces', 'locale', 'uiWorkspace'],
6853
7295
  apply,
6854
7296
  }
6855
7297
  },
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: