dsh-better-workspace 0.34.1 → 0.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -70,7 +70,7 @@
70
70
 
71
71
  - **文件夹式收起**:点工作区行 = 收起/展开该工作区的全部会话(收起后行上保留会话数角标;角标 v0.34.0 起默认隐藏、对齐官方,设置卡「显示会话数」可开);会话分组同样可收起;搜索时自动全部展开。
72
72
  - **拖拽还原原版能力**:工作区行可拖拽——拖到另一工作区行上/下半即调整顺序(写回宿主 registry 序),拖到分组行则移动进该分组(改写名称前缀);会话行可拖拽——同组内拖到另一会话上/下即调整顺序,拖到**别组的会话行**会把名字前缀改写到那个组(拖到根层的行 = 移出分组),拖到会话子分组行即移动进该分组,拖到**自己的工作区行**或**树里任何组外的地方**(含空白区域)同样移出分组 —— 拖拽期间工作区行整行高亮、树底部有一条淡虚线提示这个出口;带分组前缀的会话行右键还多一项「移出分组」。显示顺序 = 宿主手动序,拖拽结果立即生效。**磁盘层是文件系统事实,不随拖拽改变**:跨磁盘层的放置只重组名称分组(工作区落回自身磁盘层的末尾),同层放置才走锚点序。拖拽工作区期间,单链合并显示的行(如 工作/A)临时展开回文件夹树——路径里的每一级都是可投放的分组行,松手后自动合并回去。
73
- - **不丢原生能力**:会话行(打开/重命名/分叉/归档;行内还有官方式 **⋯ 按钮**开同一套菜单,**双击标题**重命名,v0.34.0 起)、**官方行样式逐项对齐**(v0.34.0 起:恒定 16px 状态槽——运行转圈不再挤动标题、悬停 800ms 弹官方 HoverCard(标题/路径/时间/状态,可复制)、超长标题悬停匀速滚动、活动链工作区文件夹图标着蓝、拖放指示用官方斜纹图案)、每工作区「新会话」(点 + 总是先展开该工作区,让新会话立刻可见)、当前会话高亮、官方状态灯(蓝色运行环 / 琥珀待交互 / 绿色完成提醒)、**状态呼吸灯**(折叠藏起的状态灯向外冒泡:工作区/分组行图标按状态色呼吸发光——琥珀待交互/绿完成,自定义过发光的标题一起呼吸;运行中只在会话行本层蓝色呼吸,不外渗;可在设置关闭)、**活跃定时任务角标**(有活动 Schedule 的会话显示闹钟图标;v0.23.0 起直接渲染官方 `sidebar.session.row.leading` 座位的占用者,与官方行同源)、未分组会话兜底、界面文案跟随 DSH 的语言设置(内置 21 种语言,含简繁中文)。
73
+ - **不丢原生能力**:会话行(打开/重命名/分叉/归档;**双击标题**重命名,v0.34.0 起;**右键是唯一菜单入口**——行内「⋯」已退役,悬停只留快捷钮,工作区行同样只留新会话钮,v0.35.1 起)、**官方行样式逐项对齐**(v0.34.0 起:恒定 16px 状态槽——运行转圈不再挤动标题、悬停 800ms 弹官方 HoverCard(标题/路径/时间/状态,可复制)、超长标题悬停匀速滚动、活动链工作区文件夹图标着蓝、拖放指示用官方斜纹图案)、每工作区「新会话」(点 + 总是先展开该工作区,让新会话立刻可见)、当前会话高亮、官方状态灯(蓝色运行环 / 琥珀待交互 / 绿色完成提醒)、**状态呼吸灯**(折叠藏起的状态灯向外冒泡:工作区/分组行图标按状态色呼吸发光——琥珀待交互/绿完成,自定义过发光的标题一起呼吸;运行中只在会话行本层蓝色呼吸,不外渗;可在设置关闭)、**活跃定时任务角标**(有活动 Schedule 的会话显示闹钟图标;v0.23.0 起直接渲染官方 `sidebar.session.row.leading` 座位的占用者,与官方行同源)、未分组会话兜底、界面文案跟随 DSH 的语言设置(内置 21 种语言,含简繁中文)。
74
74
  - **新增项自动配色 + 图标(v0.25.0,默认关闭)**:设置卡里一个开关,打开后**新创建的会话**自动分到一个内置色、**新工作区**再额外分到一个图标,懒得逐个设置的人直接开箱即用。颜色来自**定死的 13 个槽位**,每个槽位配**两色**——浅色主题用深色系、深色主题用亮色系——按「洗牌袋 + 近期回避窗口」抽取:一轮之内绝不重复,跨轮也不会立刻撞上刚出过的 3 个,连续新建几项肉眼可辨。**可读性是真达标**:每个色在**对应主题**背景下的对比度都 **≥4.5**,即 WCAG **正文级 AA**(对比度是脚本真算的,测试逐色断言;浅色主题按比纯白更暗的悬停面、深色主题按最亮的面这种保守取值)。行里存的是**槽位 id(`auto:3`)而不是色值**,所以**切换深浅主题时颜色自动换成同色系的另一套**,既不用重新设置,也不会失配。**只影响打开开关之后新建的项**:你手动设置过外观的行(存的是 hex)永不被覆盖,关闭开关也不回收已分配的样式,同一项不会被二次随机。图标从既有图标候选集里取、按宿主实际导出过滤;**图标只给工作区**——会话行的那个位置是官方状态灯与日程角标,没有图标位。
75
75
  - **官方新语义与快捷键都接上(v0.23.0,对齐 dsh 0.1.7-rc.2)**:① 归档筛选是官方新版三态直选(**隐藏已归档** / **全部对话(显示已归档)** / **仅显示已归档**);选「仅显示已归档」时**没有归档会话的工作区整行隐藏**,空列表显示「暂无已归档会话」+「查看其他会话」一键回退;② 官方快捷键 **⌥⌘K 搜索 / ⌥⌘O 添加工作区 / ⌥⌘N 新会话**(桌面端为 ⌘K / ⌘O / ⌘N)在本插件的自绘侧栏里**真的生效**——搜索键打开并聚焦本插件的搜索框、添加键走同一个官方目录流(原生宿主弹系统选择器,browse 宿主弹应用内入口提示),按钮文案还会带上宿主当前的键位;③ 首次使用的工作区显示为本地化的「默认工作区」,不再露出磁盘目录名。
76
76
  - **URL/斜杠标题不拆层**:宿主自动生成的会话标题落地时若含 `/`(如复述了 URL),整串自动包上 `“”` 原样显示,绝不解析成层级——只对本次启动后**新建的会话**生效,并等命名稳定(约 20 秒,让原生 AI 命名先落地)才处理;存量会话与你手动命名的 `/` 分组标题永不被改动。引号内(含手动添加)的 `/` 永不切分,孤立引号只当普通字符;无引号但含 URL 的旧标题也有平铺兜底。
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-better-workspace",
3
- "version": "0.34.1",
3
+ "version": "0.35.1",
4
4
  "main": "./src/index.js",
5
5
  "description": "侧边栏工作区树:按官方磁盘目录嵌套(官方 workspace-tree 语义),可选按名称中的 / 分组(默认关闭,设置里开启);新会话页的工作区选择器同树。Workspace tree for the DSH sidebar workspace list, nested by official disk folders with optional \"/\"-name grouping; the New Session workspace picker shows the same tree.",
6
6
  "engines": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-better-workspace",
3
- "version": "0.34.1",
3
+ "version": "0.35.1",
4
4
  "description": "侧边栏工作区树:按官方磁盘目录嵌套(官方 workspace-tree 语义),可选按名称中的 / 分组(默认关闭,设置里开启);新会话页的工作区选择器同树(同一份视图选项即时同步);添加工作区走官方目录流。Workspace tree for the DSH sidebar: disk-path nesting (official workspace-tree semantics), with optional \"/\"-name grouping (off by default), and the same tree in the New Session workspace picker; add-workspace rides the official directory flow.",
5
5
  "type": "module",
6
6
  "repository": {
package/src/client.js CHANGED
@@ -125,8 +125,6 @@ window.__ModuleLoader__.load({
125
125
  'settings.sessionLimit': "展开时显示会话数",
126
126
  'settings.sessionLimit.all': "全部",
127
127
  'settings.sessionLimit.hint': "展开工作区时先只显示最近的部分会话,其余收进「展开 {n} 个会话」一行,点击展开该组全部;置顶、进行中和等待你的会话始终显示。全部 = 不限量(默认)。",
128
- 'settings.sessionMenu': "会话右键菜单",
129
- 'settings.sessionMenu.hint': "开启时右键点击会话行即可打开操作菜单(包含官方的置顶、归档、停止并归档等);关闭后改为在行尾悬停显示官方样式的 ⋯ 按钮,点击打开同一个菜单。默认开启。",
130
128
  'settings.graft.title': "自动标题保留分组前缀",
131
129
  'settings.graft.hint': "「随对话更新的标题」实验功能会把会话改名成不带「/」分组的新名字,分组会因此丢失。开启后本插件视图保留原「/」前缀、只替换最后一段(如 A/xxx 自动变成 A/yyyy)。手动重命名不受影响。默认开启。",
132
130
  'settings.rowActions': "会话行悬停按钮",
@@ -303,8 +301,6 @@ window.__ModuleLoader__.load({
303
301
  'settings.sessionLimit': "Sessions shown when expanded",
304
302
  'settings.sessionLimit.all': "All",
305
303
  'settings.sessionLimit.hint': "An expanded workspace first shows only its most recent sessions, the rest behind one Show-more row; pinned, running and waiting-for-you sessions always stay visible. All = no limit (default).",
306
- 'settings.sessionMenu': "Session context menu",
307
- '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.",
308
304
  'settings.graft.title': "Auto titles keep the group prefix",
309
305
  'settings.graft.hint': "The 'titles that follow the conversation' experiment renames sessions to names without the '/' group, which dissolves the grouping. On, this view keeps the original '/' prefix and swaps only the last segment (A/xxx becomes A/yyyy). Manual renames are unaffected. On by default.",
310
306
  'settings.rowActions': "Session row hover buttons",
@@ -489,8 +485,6 @@ window.__ModuleLoader__.load({
489
485
  'settings.sessionLimit': "عدد الجلسات عند التوسيع",
490
486
  'settings.sessionLimit.all': "الكل",
491
487
  'settings.sessionLimit.hint': "تعرض مساحة العمل الموسعة أحدث الجلسات أولاً، والبقية خلف سطر عرض المزيد؛ الجلسات المثبتة والجارية والمنتظرة لك تبقى مرئية دائماً. الكل = بلا حد (افتراضي).",
492
- 'settings.sessionMenu': "قائمة النقر الأيمن للجلسة",
493
- 'settings.sessionMenu.hint': "عند التفعيل: انقر بزر الفأرة الأيمن على سطر الجلسة لفتح قائمة الإجراءات (بما في ذلك التثبيت والأرشفة الرسمية). عند الإيقاف: يظهر زر ⋯ بأسلوب رسمي عند تمرير المؤشر يفتح القائمة نفسها. مفعّل افتراضياً.",
494
488
  'settings.graft.title': "عناوين تلقائية تحتفظ ببادئة المجموعة",
495
489
  'settings.graft.hint': "تجربة 'العناوين التي تتبع المحادثة' تعيد تسمية الجلسات بأسماء بدون مجموعة '/'، فيختفي التقسيم. عند التفعيل يحتفظ هذا العرض بالبادئة الأصلية ويستبدل المقطع الأخير فقط (A/xxx يصبح A/yyyy). إعادة التسمية اليدوية لا تتأثر. مفعّل افتراضيًا.",
496
490
  'settings.rowActions': "أزرار تمرير سطر الجلسة",
@@ -667,8 +661,6 @@ window.__ModuleLoader__.load({
667
661
  'settings.sessionLimit': "Sitzungen beim Aufklappen",
668
662
  'settings.sessionLimit.all': "Alle",
669
663
  'settings.sessionLimit.hint': "Ein aufgeklappter Arbeitsbereich zeigt zuerst nur die neuesten Sitzungen, der Rest hinter einer Mehr-anzeigen-Zeile; angeheftete, laufende und auf Sie wartende Sitzungen bleiben immer sichtbar. Alle = unbegrenzt (Standard).",
670
- 'settings.sessionMenu': "Sitzungs-Kontextmenü",
671
- '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.",
672
664
  'settings.graft.title': "Automatische Titel behalten den Gruppenpräfix",
673
665
  'settings.graft.hint': "Das Experiment 'Titel, die der Unterhaltung folgen' benennt Sitzungen ohne '/'-Gruppe um, wodurch die Gruppierung verloren geht. Aktiv behält diese Ansicht den ursprünglichen Präfix und tauscht nur das letzte Segment (A/xxx wird A/yyyy). Manuelle Umbenennungen sind nicht betroffen. Standardmäßig aktiv.",
674
666
  'settings.rowActions': "Sitzungszeilen-Schnellknöpfe",
@@ -845,8 +837,6 @@ window.__ModuleLoader__.load({
845
837
  'settings.sessionLimit': "Sessions affichées à l’ouverture",
846
838
  'settings.sessionLimit.all': "Tout",
847
839
  'settings.sessionLimit.hint': "Un espace ouvert montre d’abord ses sessions les plus récentes, le reste derrière une ligne afficher-plus ; les sessions épinglées, en cours et en attente restent toujours visibles. Tout = sans limite (par défaut).",
848
- 'settings.sessionMenu': "Menu contextuel des sessions",
849
- '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.",
850
840
  'settings.graft.title': "Les titres automatiques gardent le préfixe de groupe",
851
841
  'settings.graft.hint': "L'expérience 'titres qui suivent la conversation' renomme les sessions sans le groupe '/', ce qui dissout le groupement. Activé, cette vue conserve le préfixe d'origine et ne remplace que le dernier segment (A/xxx devient A/yyyy). Les renommages manuels ne sont pas affectés. Activé par défaut.",
852
842
  'settings.rowActions': "Boutons de survol des sessions",
@@ -1023,8 +1013,6 @@ window.__ModuleLoader__.load({
1023
1013
  'settings.sessionLimit': "विस्तार पर दिखने वाले सेशन",
1024
1014
  'settings.sessionLimit.all': "सभी",
1025
1015
  'settings.sessionLimit.hint': "विस्तारित वर्कस्पेस पहले केवल हाल के सेशन दिखाता है, बाकी एक और-दिखाएँ पंक्ति के पीछे; पिन, चल रहे और आपकी प्रतीक्षा में सेशन सदा दिखते हैं। सभी = असीमित (डिफ़ॉल्ट)।",
1026
- 'settings.sessionMenu': "सेशन संदर्भ मेनू",
1027
- 'settings.sessionMenu.hint': "चालू: सेशन पंक्ति पर राइट-क्लिक करने से उसका क्रिया मेनू खुलता है (आधिकारिक पिन, आर्काइव और रोककर-आर्काइव सहित)। बंद: पंक्ति पर होवर करने पर आधिकारिक शैली का ⋯ बटन दिखता है, जो वही मेनू खोलता है। डिफ़ॉल्ट रूप से चालू।",
1028
1016
  'settings.graft.title': "स्वतः शीर्षक समूह उपसर्ग रखते हैं",
1029
1017
  'settings.graft.hint': "'बातचीत के अनुसार शीर्षक' प्रयोग सत्रों का नाम बिना '/' समूह के बदल देता है, जिससे समूहन टूट जाता है। चालू होने पर यह दृश्य मूल उपसर्ग रखता है और केवल अंतिम खंड बदलता है (A/xxx → A/yyyy)। मैन्युअल नाम बदलना प्रभावित नहीं होता। डिफ़ॉल्ट रूप से चालू।",
1030
1018
  'settings.rowActions': "सेशन पंक्ति होवर बटन",
@@ -1201,8 +1189,6 @@ window.__ModuleLoader__.load({
1201
1189
  'settings.sessionLimit': "Sesi saat dibuka",
1202
1190
  'settings.sessionLimit.all': "Semua",
1203
1191
  'settings.sessionLimit.hint': "Ruang kerja yang dibuka pertama menampilkan sesi terbaru, sisanya di balik satu baris tampilkan-lebih; sesi pin, berjalan, dan menunggu Anda selalu terlihat. Semua = tanpa batas (bawaan).",
1204
- 'settings.sessionMenu': "Menu klik kanan sesi",
1205
- '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.",
1206
1192
  'settings.graft.title': "Judul otomatis mempertahankan awalan grup",
1207
1193
  'settings.graft.hint': "Eksperimen 'judul yang mengikuti percakapan' mengganti nama sesi tanpa grup '/', sehingga pengelompokan hilang. Saat aktif, tampilan ini mempertahankan awalan asli dan hanya mengganti segmen terakhir (A/xxx menjadi A/yyyy). Penggantian nama manual tidak terpengaruh. Aktif secara bawaan.",
1208
1194
  'settings.rowActions': "Tombol hover baris sesi",
@@ -1379,8 +1365,6 @@ window.__ModuleLoader__.load({
1379
1365
  'settings.sessionLimit': "Sessioni mostrate all’apertura",
1380
1366
  'settings.sessionLimit.all': "Tutte",
1381
1367
  'settings.sessionLimit.hint': "Un’area aperta mostra prima le sessioni più recenti, il resto dietro una riga mostra-altro; sessioni fissate, in corso e in attesa restano sempre visibili. Tutte = senza limiti (predefinito).",
1382
- 'settings.sessionMenu': "Menu contestuale sessione",
1383
- '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.",
1384
1368
  'settings.graft.title': "I titoli automatici conservano il prefisso del gruppo",
1385
1369
  'settings.graft.hint': "L'esperimento 'titoli che seguono la conversazione' rinomina le sessioni senza il gruppo '/', sciogliendo il raggruppamento. Attivo, questa vista conserva il prefisso originale e sostituisce solo l'ultimo segmento (A/xxx diventa A/yyyy). Le rinomine manuali non sono influenzate. Attivo per impostazione predefinita.",
1386
1370
  'settings.rowActions': "Pulsanti al passaggio della sessione",
@@ -1557,8 +1541,6 @@ window.__ModuleLoader__.load({
1557
1541
  'settings.sessionLimit': "展開時に表示するセッション数",
1558
1542
  'settings.sessionLimit.all': "すべて",
1559
1543
  'settings.sessionLimit.hint': "展開したワークスペースは最近のセッションを先に表示し、残りは「さらに表示」行の後ろに収まります。ピン留め・実行中・対応待ちのセッションは常に表示。すべて = 無制限(既定)。",
1560
- 'settings.sessionMenu': "セッション右クリックメニュー",
1561
- 'settings.sessionMenu.hint': "オン:セッション行を右クリックすると操作メニューを開きます(公式のピン留め・アーカイブ・停止してアーカイブを含む)。オフ:行にカーソルを合わせると公式スタイルの ⋯ ボタンが表示され、同じメニューを開きます。既定はオン。",
1562
1544
  'settings.graft.title': "自動タイトルはグループ接頭辞を保持",
1563
1545
  'settings.graft.hint': "「会話に合わせて更新されるタイトル」実験機能は「/」グループなしの名前にセッションを改名するため、グルーピングが失われます。オンにすると、このビューは元の「/」接頭辞を保持し最終セグメントだけを差し替えます(A/xxx → A/yyyy)。手動リネームには影響しません。デフォルトでオン。",
1564
1546
  'settings.rowActions': "セッション行のホバーボタン",
@@ -1735,8 +1717,6 @@ window.__ModuleLoader__.load({
1735
1717
  'settings.sessionLimit': "펼칠 때 표시할 세션",
1736
1718
  'settings.sessionLimit.all': "전체",
1737
1719
  'settings.sessionLimit.hint': "펼친 작업 공간은 최근 세션을 먼저 보여주고 나머지는 더보기 행 뒤에 둡니다. 고정, 실행 중, 대기 중인 세션은 항상 표시됩니다. 전체 = 제한 없음(기본값).",
1738
- 'settings.sessionMenu': "세션 우클릭 메뉴",
1739
- 'settings.sessionMenu.hint': "켬: 세션 행을 우클릭하면 작업 메뉴가 열립니다(공식 고정, 보관, 중지 후 보관 포함). 끔: 행에 마우스를 올리면 공식 스타일의 ⋯ 버튼이 표시되어 같은 메뉴를 엽니다. 기본값은 켬.",
1740
1720
  'settings.graft.title': "자동 제목은 그룹 접두어를 유지",
1741
1721
  'settings.graft.hint': "'대화에 따라 업데이트되는 제목' 실험 기능이 '/' 그룹 없는 이름으로 세션 이름을 바꿔 그룹핑이 사라집니다. 켜면 이 뷰는 원래 '/' 접두어를 유지하고 마지막 세그먼트만 교체합니다(A/xxx → A/yyyy). 수동 이름 바꾸기는 영향을 받지 않습니다. 기본적으로 켬.",
1742
1722
  'settings.rowActions': "세션 행 호버 버튼",
@@ -1913,8 +1893,6 @@ window.__ModuleLoader__.load({
1913
1893
  'settings.sessionLimit': "Sessies bij uitklappen",
1914
1894
  'settings.sessionLimit.all': "Alles",
1915
1895
  'settings.sessionLimit.hint': "Een uitgeklapte werkruimte toont eerst de nieuwste sessies, de rest achter een toon-meer rij; vastgemaakte, lopende en op u wachtende sessies blijven altijd zichtbaar. Alles = onbeperkt (standaard).",
1916
- 'settings.sessionMenu': "Sessie-contextmenu",
1917
- '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.",
1918
1896
  'settings.graft.title': "Automatische titels behouden het groepsprefix",
1919
1897
  'settings.graft.hint': "Het experiment 'titels die het gesprek volgen' hernoemt sessies naar namen zonder de '/'-groep, waardoor de groepering verloren gaat. Aan: deze weergave behoudt het oorspronkelijke prefix en vervangt alleen het laatste segment (A/xxx wordt A/yyyy). Handmatige hernoemingen zijn niet getroffen. Standaard aan.",
1920
1898
  'settings.rowActions': "Sessierij-snelknoppen",
@@ -2091,8 +2069,6 @@ window.__ModuleLoader__.load({
2091
2069
  'settings.sessionLimit': "Sesje po rozwinięciu",
2092
2070
  'settings.sessionLimit.all': "Wszystkie",
2093
2071
  'settings.sessionLimit.hint': "Rozwinięty obszar pokazuje najpierw najnowsze sesje, reszta czeka za wierszem pokaż-więcej; przypięte, uruchomione i czekające sesje są zawsze widoczne. Wszystkie = bez limitu (domyślnie).",
2094
- 'settings.sessionMenu': "Menu kontekstowe sesji",
2095
- '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.",
2096
2072
  'settings.graft.title': "Automatyczne tytuły zachowują prefiks grupy",
2097
2073
  'settings.graft.hint': "Eksperyment 'tytuły podążające za rozmową' zmienia nazwy sesji na bez grupy '/', co rozbija grupowanie. Włączone: ten widok zachowuje oryginalny prefiks i zamienia tylko ostatni segment (A/xxx staje się A/yyyy). Ręczne zmiany nazwy nie są objęte. Domyślnie włączone.",
2098
2074
  'settings.rowActions': "Przyciski najechania wiersza sesji",
@@ -2269,8 +2245,6 @@ window.__ModuleLoader__.load({
2269
2245
  'settings.sessionLimit': "Sessões ao expandir",
2270
2246
  'settings.sessionLimit.all': "Todas",
2271
2247
  'settings.sessionLimit.hint': "Um espaço expandido mostra primeiro as sessões mais recentes, o restante atrás de uma linha mostrar-mais; sessões fixadas, em execução e aguardando você permanecem visíveis. Todas = sem limite (predefinição).",
2272
- 'settings.sessionMenu': "Menu de contexto da sessão",
2273
- '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.",
2274
2248
  'settings.graft.title': "Títulos automáticos mantêm o prefixo do grupo",
2275
2249
  'settings.graft.hint': "O experimento 'títulos que acompanham a conversa' renomeia sessões sem o grupo '/', dissolvendo o agrupamento. Ativo: esta visão mantém o prefixo original e troca apenas o último segmento (A/xxx vira A/yyyy). Renomeações manuais não são afetadas. Ativo por padrão.",
2276
2250
  'settings.rowActions': "Botões de passagem da sessão",
@@ -2447,8 +2421,6 @@ window.__ModuleLoader__.load({
2447
2421
  'settings.sessionLimit': "Сессий при разворачивании",
2448
2422
  'settings.sessionLimit.all': "Все",
2449
2423
  'settings.sessionLimit.hint': "Развёрнутая область сначала показывает последние сессии, остальные — за строкой «показать ещё»; закреплённые, идущие и ожидающие вас сессии видны всегда. Все = без ограничения (по умолчанию).",
2450
- 'settings.sessionMenu': "Контекстное меню сессии",
2451
- 'settings.sessionMenu.hint': "Вкл: правый клик по строке сессии открывает меню действий (включая официальные закрепление, архивацию и «остановить и архивировать»). Выкл: при наведении в строке появляется официальная кнопка ⋯, открывающая то же меню. Включено по умолчанию.",
2452
2424
  'settings.graft.title': "Автозаголовки сохраняют префикс группы",
2453
2425
  'settings.graft.hint': "Эксперимент «заголовки, следующие за беседой» переименовывает сессии в имена без группы «/», из-за чего группировка теряется. Включено: этот вид сохраняет исходный префикс «/» и заменяет только последний сегмент (A/xxx становится A/yyyy). Ручные переименования не затрагиваются. Включено по умолчанию.",
2454
2426
  'settings.rowActions': "Кнопки наведения строки сессии",
@@ -2625,8 +2597,6 @@ window.__ModuleLoader__.load({
2625
2597
  'settings.sessionLimit': "Sessioner vid utfällning",
2626
2598
  'settings.sessionLimit.all': "Alla",
2627
2599
  'settings.sessionLimit.hint': "En utfälld arbetsyta visar först de senaste sessionerna, resten bakom en visa-mer-rad; fästa, pågående och väntande sessioner syns alltid. Alla = obegränsat (standard).",
2628
- 'settings.sessionMenu': "Sessionens högerklicksmeny",
2629
- '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.",
2630
2600
  'settings.graft.title': "Automatiska titlar behåller gruppprefixet",
2631
2601
  'settings.graft.hint': "Experimentet 'titlar som följer samtalet' döper om sessioner till namn utan '/'-gruppen, vilket löser upp grupperingen. På: den här vyn behåller det ursprungliga prefixet och byter bara ut sista segmentet (A/xxx blir A/yyyy). Manuella namnbyten påverkas inte. På som standard.",
2632
2602
  'settings.rowActions': "Hovringsknappar för sessionsraden",
@@ -2803,8 +2773,6 @@ window.__ModuleLoader__.load({
2803
2773
  'settings.sessionLimit': "จำนวนเซสชันเมื่อกาง",
2804
2774
  'settings.sessionLimit.all': "ทั้งหมด",
2805
2775
  'settings.sessionLimit.hint': "พื้นที่ที่กางจะแสดงเซสชันล่าสุดก่อน ส่วนที่เหลืออยู่หลังแถวแสดงเพิ่ม เซสชันที่ปักหมุด กำลังทำงาน และรอคุณจะแสดงตลอด ทั้งหมด = ไม่จำกัด (ค่าเริ่มต้น)",
2806
- 'settings.sessionMenu': "เมนูคลิกขวาของเซสชัน",
2807
- 'settings.sessionMenu.hint': "เปิด: คลิกขวาที่แถวเซสชันเพื่อเปิดเมนูการกระทำ (รวมปักหมุด, เก็บถาวร และหยุด-แล้ว-เก็บถาวรแบบทางการ) ปิด: เมื่อชี้ที่แถวจะแสดงปุ่ม ⋯ สไตล์ทางการเปิดเมนูเดียวกันแทน เปิดตามค่าเริ่มต้น",
2808
2776
  'settings.graft.title': "ชื่ออัตโนมัติคงคำนำหน้ากลุ่ม",
2809
2777
  'settings.graft.hint': "ฟีเจอร์ทดลอง 'ชื่อที่ตามการสนทนา' เปลี่ยนชื่อเซสชันเป็นชื่อที่ไม่มีกลุ่ม '/' ทำให้การจัดกลุ่มหายไป เมื่อเปิด มุมมองนี้จะคงคำนำหน้าเดิมและเปลี่ยนเฉพาะส่วนท้าย (A/xxx กลายเป็น A/yyyy) การเปลี่ยนชื่อด้วยมือไม่ได้รับผลกระทบ เปิดตามค่าเริ่มต้น",
2810
2778
  'settings.rowActions': "ปุ่มเมื่อชี้แถวเซสชัน",
@@ -2981,8 +2949,6 @@ window.__ModuleLoader__.load({
2981
2949
  'settings.sessionLimit': "Açıldığında gösterilecek oturum",
2982
2950
  'settings.sessionLimit.all': "Tümü",
2983
2951
  'settings.sessionLimit.hint': "Açılan çalışma alanı önce en yeni oturumları gösterir, kalanı bir daha-göster satırının ardındadır; sabitlenmiş, çalışan ve sizi bekleyen oturumlar her zaman görünür. Tümü = sınırsız (varsayılan).",
2984
- 'settings.sessionMenu': "Oturum sağ tık menüsü",
2985
- '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.",
2986
2952
  'settings.graft.title': "Otomatik başlıklar grup önekini korur",
2987
2953
  'settings.graft.hint': "Deneme özelliği 'konuşmayı izleyen başlıklar' oturumları '/' grubu olmadan yeniden adlandırır, bu da gruplamayı çözer. Açıkken bu görünüm özgün öneki korur ve yalnızca son bölümü değiştirir (A/xxx, A/yyyy olur). Manuel yeniden adlandırmalar etkilenmez. Varsayılan olarak açık.",
2988
2954
  'settings.rowActions': "Oturum satırı düğmeleri",
@@ -3159,8 +3125,6 @@ window.__ModuleLoader__.load({
3159
3125
  'settings.sessionLimit': "Số phiên khi mở rộng",
3160
3126
  'settings.sessionLimit.all': "Tất cả",
3161
3127
  'settings.sessionLimit.hint': "Không gian được mở rộng sẽ hiện các phiên gần đây trước, phần còn lại nằm sau một hàng xem-thêm; phiên ghim, đang chạy và đang chờ bạn luôn hiển thị. Tất cả = không giới hạn (mặc định).",
3162
- 'settings.sessionMenu': "Menu chuột phải phiên",
3163
- '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.",
3164
3128
  'settings.graft.title': "Tiêu đề tự động giữ tiền tố nhóm",
3165
3129
  'settings.graft.hint': "Thử nghiệm 'tiêu đề cập nhật theo hội thoại' đổi tên phiên thành tên không có nhóm '/', làm mất phân nhóm. Bật: khung nhìn này giữ tiền tố gốc và chỉ thay đoạn cuối (A/xxx thành A/yyyy). Đổi tên thủ công không bị ảnh hưởng. Bật theo mặc định.",
3166
3130
  'settings.rowActions': "Nút hover dòng phiên",
@@ -3337,8 +3301,6 @@ window.__ModuleLoader__.load({
3337
3301
  'settings.sessionLimit': "展開時顯示會話數",
3338
3302
  'settings.sessionLimit.all': "全部",
3339
3303
  'settings.sessionLimit.hint': "展開工作區時先只顯示最近的部分會話,其餘收進「展開 {n} 個會話」一行,點擊展開該組全部;置頂、進行中和等待你的會話始終顯示。全部 = 不限量(預設)。",
3340
- 'settings.sessionMenu': "會話右鍵選單",
3341
- 'settings.sessionMenu.hint': "開啟時右鍵點擊會話行即可開啟操作選單(包含官方的置頂、封存、停止並封存等);關閉後改為在行尾懸停顯示官方樣式的 ⋯ 按鈕,點擊開啟同一個選單。預設開啟。",
3342
3304
  'settings.graft.title': "自動標題保留分組前綴",
3343
3305
  'settings.graft.hint': "「隨對話更新的標題」實驗功能會把會話改名成不帶「/」分組的新名字,分組會因此丟失。開啟後本插件視圖保留原「/」前綴、只替換最後一段(如 A/xxx 自動變成 A/yyyy)。手動重新命名不受影響。預設開啟。",
3344
3306
  'settings.rowActions': "會話行懸停按鈕",
@@ -3515,8 +3477,6 @@ window.__ModuleLoader__.load({
3515
3477
  'settings.sessionLimit': "展開時顯示會話數",
3516
3478
  'settings.sessionLimit.all': "全部",
3517
3479
  'settings.sessionLimit.hint': "展開工作區時先只顯示最近的部分會話,其餘收進「展開 {n} 個會話」一行,點擊展開該組全部;置頂、進行中和等待你的會話始終顯示。全部 = 不限量(預設)。",
3518
- 'settings.sessionMenu': "會話右鍵選單",
3519
- 'settings.sessionMenu.hint': "開啟時右鍵點擊會話行即可開啟操作選單(包含官方的置頂、歸檔、停止並歸檔等);關閉後改為在行尾懸停顯示官方樣式的 ⋯ 按鈕,點擊開啟同一個選單。預設開啟。",
3520
3480
  'settings.graft.title': "自動標題保留分組前綴",
3521
3481
  'settings.graft.hint': "「隨對話更新的標題」實驗功能會把會話改名成不帶「/」分組的新名字,分組會因此丟失。開啟後本外掛檢視保留原「/」前綴、只替換最後一段(如 A/xxx 自動變成 A/yyyy)。手動重新命名不受影響。預設開啟。",
3522
3482
  'settings.rowActions': "會話行懸停按鈕",
@@ -3693,8 +3653,6 @@ window.__ModuleLoader__.load({
3693
3653
  'settings.sessionLimit': "展開時顯示會話數",
3694
3654
  'settings.sessionLimit.all': "全部",
3695
3655
  'settings.sessionLimit.hint': "展開工作區時先只顯示最近的部分會話,其餘收進「展開 {n} 個會話」一行,點擊展開該組全部;釘選、進行中和等待你的會話始終顯示。全部 = 不限量(預設)。",
3696
- 'settings.sessionMenu': "會話右鍵選單",
3697
- 'settings.sessionMenu.hint': "開啟時右鍵點擊會話列即可開啟動作選單(包含官方的釘選、封存、停止並封存等);關閉後改為在列尾懸停顯示官方樣式的 ⋯ 按鈕,點擊開啟同一個選單。預設開啟。",
3698
3656
  'settings.graft.title': "自動標題保留分組前綴",
3699
3657
  'settings.graft.hint': "「隨對話更新的標題」實驗功能會把會話改名成不帶「/」分組的新名字,分組會因此丟失。開啟後本插件檢視保留原「/」前綴、只替換最後一段(如 A/xxx 自動變成 A/yyyy)。手動重新命名不受影響。預設開啟。",
3700
3658
  'settings.rowActions': "會話列懸停按鈕",
@@ -4794,7 +4752,11 @@ window.__ModuleLoader__.load({
4794
4752
  // cancels it, so text still starts and truncates where it did.
4795
4753
  '.bw-row-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:3px;margin:-3px}',
4796
4754
  '.bw-session-row .bw-row-label{padding:0;margin:0 6px 0 4px}',
4797
- '.bw-session-row .bw-row-label[data-scrolled]{mask-image:linear-gradient(to right,transparent,#000 12px);-webkit-mask-image:linear-gradient(to right,transparent,#000 12px)}',
4755
+ /* Marquee: Chromium keeps painting the ellipsis at the right edge of a
4756
+ scrolling ellipsis element (engine behavior; official rows share it).
4757
+ Switch to clip while scrolling — the fade masks carry both edges and
4758
+ the full heading reads cleanly; the resting state keeps ellipsis. */
4759
+ '.bw-session-row .bw-row-label[data-scrolled]{text-overflow:clip;mask-image:linear-gradient(to right,transparent,#000 12px);-webkit-mask-image:linear-gradient(to right,transparent,#000 12px)}',
4798
4760
  '.bw-session-row .bw-row-label[data-clipped]{mask-image:linear-gradient(to left,transparent,#000 12px);-webkit-mask-image:linear-gradient(to left,transparent,#000 12px)}',
4799
4761
  '.bw-session-row .bw-row-label[data-scrolled][data-clipped]{mask-image:linear-gradient(to right,transparent,#000 12px,#000 calc(100% - 12px),transparent);-webkit-mask-image:linear-gradient(to right,transparent,#000 12px,#000 calc(100% - 12px),transparent)}',
4800
4762
  '.bw-row-count{flex:none;display:none;font-size:12px;color:var(--dsw-alias-label-quaternary,#8a8a8a)}',
@@ -6044,7 +6006,7 @@ window.__ModuleLoader__.load({
6044
6006
  )
6045
6007
  }
6046
6008
 
6047
- function WorkspaceRow({ workspace, depth, count, sessionsOpen, onToggle, onStart, onMenu, onContextMenu, currentInside, menuOpen, dropHalf, dropInto, dragEvents, custStyle, iconMode, pulse, newSessionLabel, newSessionAria, menuAria, hoverBody, dataRowKey, t }) {
6009
+ function WorkspaceRow({ workspace, depth, count, sessionsOpen, onToggle, onStart, onContextMenu, currentInside, menuOpen, dropHalf, dropInto, dragEvents, custStyle, iconMode, pulse, newSessionLabel, newSessionAria, hoverBody, dataRowKey, t }) {
6048
6010
  const iconEl = iconOf(iconMode, sessionsOpen)
6049
6011
  const iconChild = pulse
6050
6012
  ? E(PulseGlow, { state: pulse }, iconEl || (typeof ui.StateDot === 'function' ? E(ui.StateDot, { state: pulse, size: 10 }) : null))
@@ -6067,14 +6029,10 @@ window.__ModuleLoader__.load({
6067
6029
  E('span', { className: cls('bw-row-icon', 'bw-folder-cell', currentInside && !pulse && 'bw-folder-active') }, iconChild),
6068
6030
  E('span', { className: cls('bw-row-label', pulse && custStyle && custStyle.textShadow && 'bw-pulse-text'), title: workspace.title || workspace.leaf }, workspace.leaf),
6069
6031
  count > 0 ? E('span', { className: 'bw-row-count' }, String(count)) : null,
6032
+ // The in-row ellipsis is retired (mirrors the session-row decision):
6033
+ // the ctx surface is the right-click path; hover keeps only the
6034
+ // new-session affordance.
6070
6035
  E('span', { className: 'bw-row-actions', onClick: (e) => e.stopPropagation() },
6071
- E('button', {
6072
- type: 'button',
6073
- className: 'bw-row-btn',
6074
- 'aria-label': menuAria || '...',
6075
- title: menuAria || undefined,
6076
- onClick: (e) => { e.stopPropagation(); if (onMenu) onMenu(e) },
6077
- }, icon('IconEllipsisOutline16', 15)),
6078
6036
  E('button', {
6079
6037
  type: 'button',
6080
6038
  className: 'bw-row-btn',
@@ -6226,58 +6184,28 @@ window.__ModuleLoader__.load({
6226
6184
  }
6227
6185
  const archivedTitle = official('row.archived')
6228
6186
  const pinnedTitle = official('row.pinned')
6229
- // In-row "..." menu (official Rows.tsx): the ellipsis button anchors a
6230
- // primitives Menu whose entries are the SAME shared list the
6231
- // right-click menu builds (sessionActions.menuItems) and whose trailing
6232
- // children are the shipped menu.item seat occupants; picks dispatch
6233
- // through the SAME handler (sessionActions.pick), so the two surfaces
6234
- // cannot drift. Hosts without the primitives Menu keep the old
6235
- // open-context-menu trigger button.
6236
- const inlineMenu = !node.blank && sessionActions && typeof ui.Menu === 'function'
6237
- && typeof sessionActions.menuItems === 'function' && typeof sessionActions.pick === 'function'
6238
- const [menuOpen, setMenuOpen] = React.useState(false)
6239
- const menuOpenPair = React.useMemo(() => [menuOpen, setMenuOpen], [menuOpen])
6240
- const menuSeatChildren = inlineMenu && Array.isArray(sessionActions.menuSeat)
6241
- ? sessionActions.menuSeat.map((seat, index) => E(ShippedSeatOccupant, {
6242
- key: 'menu-seat-' + index,
6243
- seat,
6244
- ownerProps: { sessionId: node.id, displayTitle: node.leaf || node.title },
6245
- t: seat.t,
6246
- // The menu.item hook-context factory (ui-slots) hands occupants
6247
- // the open-state pair; reproduced here for the in-row menu.
6248
- useMenuOpenState: () => menuOpenPair,
6249
- }))
6250
- : null
6251
- // The shipped row.action seat renders the OFFICIAL pin/archive quick
6252
- // buttons; our own fallback buttons step aside so the row never shows
6253
- // two sets (same rule as the schedule mark's leading cell).
6187
+ // Row-trailing quick actions: the shipped row.action seat renders the
6188
+ // OFFICIAL pin/archive buttons; our own fallback buttons render only
6189
+ // where the seat has no entries, so the row never shows two sets. The
6190
+ // in-row "..." menu (added v0.34.0) is RETIRED (issue #12): the context
6191
+ // menu already carries every entry, and the menu.item occupants depend
6192
+ // on slot-level inject hooks (useShortcuts on 0.2.1-rc.2) that an
6193
+ // in-row reproduction cannot supply — one missing hook took the whole
6194
+ // sidebar down. Each seat occupant sits in a QuietBoundary so a broken
6195
+ // seat degrades to a missing button, never a blank panel.
6254
6196
  const quickSeatUsed = Array.isArray(sessionActions.rowActionSeat) && sessionActions.rowActionSeat.length > 0
6255
- const strip = !node.blank && sessionActions && (inlineMenu || sessionActions.trigger || (sessionActions.quick && !quickSeatUsed))
6197
+ const strip = !node.blank && sessionActions && sessionActions.quick && (quickSeatUsed || sessionActions.pin || sessionActions.archive)
6256
6198
  ? E('span', { className: 'bw-row-actions', onClick: (e) => e.stopPropagation() },
6257
- inlineMenu ? E(ui.Menu, {
6258
- open: menuOpen,
6259
- onClose: () => setMenuOpen(false),
6260
- portal: true,
6261
- closeOnPointerLeave: true,
6262
- dense: true,
6263
- anchor: E('button', {
6264
- type: 'button',
6265
- className: 'bw-row-btn',
6266
- 'aria-label': official('actions.session.aria', { name: node.leaf || node.title }) || '...',
6267
- title: sessionActions.triggerTitle,
6268
- onClick: () => setMenuOpen((open) => !open),
6269
- }, icon('IconEllipsisOutline16', 15)),
6270
- items: sessionActions.menuItems(node),
6271
- onSelect: (id) => { setMenuOpen(false); sessionActions.pick(node, id) },
6272
- }, menuSeatChildren)
6273
- : sessionActions.trigger ? E('button', {
6274
- type: 'button',
6275
- className: 'bw-icon-btn',
6276
- 'aria-label': official('actions.session.aria', { name: node.leaf || node.title }) || '...',
6277
- title: sessionActions.triggerTitle,
6278
- onClick: (e) => sessionActions.openMenu(node, e),
6279
- }, icon('IconEllipsisOutline16', 15)) : null,
6280
- !quickSeatUsed && sessionActions.quick && sessionActions.pin && !node.archived ? E('button', {
6199
+ quickSeatUsed
6200
+ ? sessionActions.rowActionSeat.map((seat, index) => E(QuietBoundary, { key: 'row-action-seat-' + index },
6201
+ E(ShippedSeatOccupant, {
6202
+ key: 'row-action-seat-' + index,
6203
+ seat,
6204
+ ownerProps: { sessionId: node.id, displayTitle: node.leaf || node.title },
6205
+ t: seat.t,
6206
+ })))
6207
+ : null,
6208
+ !quickSeatUsed && sessionActions.pin && !node.archived ? E('button', {
6281
6209
  type: 'button',
6282
6210
  className: 'bw-icon-btn',
6283
6211
  'aria-label': official(node.pinned ? 'menu.unpinSession' : 'menu.pinSession') || '',
@@ -6302,7 +6230,6 @@ window.__ModuleLoader__.load({
6302
6230
  let row = E('div', {
6303
6231
  className: cls('bw-row', 'bw-session-row', current && 'bw-row-current',
6304
6232
  node.archived && 'bw-row-archived',
6305
- menuOpen && 'menu-open',
6306
6233
  dropHalf === 'before' && 'bw-drop-before', dropHalf === 'after' && 'bw-drop-after'),
6307
6234
  'data-row-key': dataRowKey,
6308
6235
  style: { '--bw-indent': (depth * 12) + 'px', ...(custStyle || {}) },
@@ -6361,7 +6288,6 @@ window.__ModuleLoader__.load({
6361
6288
  content: sessionActions.hoverBody(node, status, state),
6362
6289
  openDelayMs: 800,
6363
6290
  grouped: true,
6364
- disabled: menuOpen,
6365
6291
  // copyText present REQUIRES both labels (primitives HoverCard
6366
6292
  // contract): copy only renders where the official copy survives.
6367
6293
  }, copyText && copyLabel && copiedLabel ? { copyText, copyLabel, copiedLabel } : {}))
@@ -6515,7 +6441,7 @@ window.__ModuleLoader__.load({
6515
6441
  'IconSkillOutline16', 'IconSparkle16', 'IconNewChatOutline16',
6516
6442
  'IconCheckOutline14', 'IconCheckOutline16', 'IconChevronDownOutline14', 'IconChevronLeftOutline14',
6517
6443
  'IconChevronRightOutline14', 'IconChevronUpOutline14', 'IconCloseOutline16', 'IconDarkOutline16',
6518
- 'IconEditOutline16', 'IconEllipsisOutline16', 'IconFolderClose16', 'IconFolderOpen16',
6444
+ 'IconEditOutline16', 'IconFolderClose16', 'IconFolderOpen16',
6519
6445
  'IconFolderOpenOutline16', 'IconFullscreenOutline16', 'IconLightOutline16', 'IconLinkOutline14',
6520
6446
  'IconLoadingOutline16', 'IconPanelLeftOutline16', 'IconPaperclipOutline16', 'IconPersonalizationOutline16',
6521
6447
  'IconPlayOutline16', 'IconPauseOutline16', 'IconPlusOutline16', 'IconQuestionOutline14',
@@ -6974,7 +6900,6 @@ window.__ModuleLoader__.load({
6974
6900
  const localStyling = useStore ? (useStore(s => s.styling) || {}) : {}
6975
6901
  const compactChains = prefs.compactChains !== false
6976
6902
  const statusPulse = prefs.statusPulse !== false
6977
- const sessionMenu = prefs.sessionMenu !== false
6978
6903
  const rowActions = prefs.rowActions !== false
6979
6904
  // v0.30.2: auto titles keep the '/' group prefix — on unless opted out.
6980
6905
  const graftPrefix = prefs.graftTitlePrefix !== false
@@ -7121,18 +7046,6 @@ window.__ModuleLoader__.load({
7121
7046
  E('div', { className: 'bw-hint' }, t('settings.sessionLimit.hint')),
7122
7047
  ),
7123
7048
  fold('settings.group.interaction',
7124
- E('div', { className: 'bw-setting-row' },
7125
- E('div', { className: 'bw-setting-label' }, t('settings.sessionMenu')),
7126
- E('button', {
7127
- type: 'button',
7128
- role: 'switch',
7129
- 'aria-checked': sessionMenu,
7130
- 'aria-label': t('settings.sessionMenu'),
7131
- className: cls('bw-switch', sessionMenu && 'bw-switch-on'),
7132
- onClick: () => { setPref('sessionMenu', !sessionMenu) },
7133
- }, E('span', { className: 'bw-switch-thumb' })),
7134
- ),
7135
- E('div', { className: 'bw-hint' }, t('settings.sessionMenu.hint')),
7136
7049
  E('div', { className: 'bw-setting-row' },
7137
7050
  E('div', { className: 'bw-setting-label' }, t('settings.graft.title')),
7138
7051
  E('button', {
@@ -7246,7 +7159,14 @@ window.__ModuleLoader__.load({
7246
7159
  /* --------- settings → plug-ins card (accordion like official cards) --------- */
7247
7160
 
7248
7161
  function BetterWorkspacePluginCard(props) {
7249
- const { useStore, actions, t } = props
7162
+ const { useStore: injectedUseStore, actions: injectedActions, localStore: localStoreProp, t } = props
7163
+ // Issue #13: same private-store switch as the browser seat — the card
7164
+ // keeps working no matter what a shadowing occupant does to the props.
7165
+ const localStoreFace = typeof localStoreProp === 'function' ? localStoreProp() : null
7166
+ const useStore = localStoreFace && typeof localStoreFace.useStore === 'function'
7167
+ ? localStoreFace.useStore
7168
+ : injectedUseStore
7169
+ const actions = localStoreFace && localStoreFace.actions ? localStoreFace.actions : injectedActions
7250
7170
  // The bundle page (plugins.bundle.config, dsh 0.1.6-alpha.2+) renders
7251
7171
  // this card inline with view: 'page' — no accordion chrome there, the
7252
7172
  // settings body sits directly under a one-line title.
@@ -7549,7 +7469,8 @@ window.__ModuleLoader__.load({
7549
7469
  const {
7550
7470
  wide, expandSidebar,
7551
7471
  useSessions, useSessionPendingInteraction, useSessionStatus, useWorkspaces,
7552
- useStore, actions,
7472
+ useStore: injectedUseStore, actions: injectedActions,
7473
+ localStore: localStoreProp,
7553
7474
  startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace,
7554
7475
  archiveSession, createWorkspace, pinSession, unpinSession, unarchiveSession,
7555
7476
  insertWorkspaceBefore, insertSessionBefore,
@@ -7561,6 +7482,17 @@ window.__ModuleLoader__.load({
7561
7482
  presetFaceOf,
7562
7483
  t,
7563
7484
  } = props
7485
+ // Issue #13: local reads and writes go through OUR store instance, never
7486
+ // the renderer-synthesized props — a shadowing occupant can swap those
7487
+ // props for its own store (issue #13). Host projections (useSessions /
7488
+ // useWorkspaces) stay on the injected props; only the plugin's OWN view
7489
+ // state switches. Names keep their original spelling so the component
7490
+ // body needs no other change.
7491
+ const localStoreFace = typeof localStoreProp === 'function' ? localStoreProp() : null
7492
+ const useStore = localStoreFace && typeof localStoreFace.useStore === 'function'
7493
+ ? localStoreFace.useStore
7494
+ : injectedUseStore
7495
+ const actions = localStoreFace && localStoreFace.actions ? localStoreFace.actions : injectedActions
7564
7496
  // v0.32.2: resolve the stable getter ONCE per render (see
7565
7497
  // browserInjected for why this is a function and not a value prop).
7566
7498
  // Every consumer below keeps reading this local, so a late-arriving
@@ -7638,10 +7570,9 @@ window.__ModuleLoader__.load({
7638
7570
  const shared = makeSharedWrites(actions, stylingMap)
7639
7571
  const compactChains = prefsMap.compactChains !== false
7640
7572
  const statusPulse = prefsMap.statusPulse !== false
7641
- // Session-action surface gates (v0.20.0): the right-click menu and the
7642
- // official-style hover strip can each be turned off; defaults follow
7643
- // the plugin's historical behavior (right-click on, quick buttons on).
7644
- const sessionMenuEnabled = prefsMap.sessionMenu !== false
7573
+ // Session-action surface gate (v0.20.0): the hover quick buttons. The
7574
+ // right-click ctx menu is always on since the in-row "..." triggers
7575
+ // retired (session row v0.35.0, workspace row v0.35.1).
7645
7576
  const rowActionsEnabled = prefsMap.rowActions !== false
7646
7577
  // Auto color + icon for new items (v0.24.0). Default OFF: absent key =
7647
7578
  // off, so every existing snapshot keeps its behavior untouched.
@@ -7774,13 +7705,14 @@ window.__ModuleLoader__.load({
7774
7705
  // The shipped schedule mark's seat occupant (see scheduleSeatOf); absent
7775
7706
  // whenever the Schedule overlay is off, which is the shipped default.
7776
7707
  const scheduleSeat = typeof scheduleSeatReader === 'function' ? scheduleSeatReader() : null
7777
- // The shipped session-row seats (official Rows.tsx): menu.item
7778
- // occupants become the in-row menu's trailing children, row.action
7779
- // occupants become the official pin/archive quick buttons (our own
7708
+ // The shipped session-row seats (official Rows.tsx): row.action
7709
+ // occupants render the official pin/archive quick buttons (our own
7780
7710
  // fallback buttons step aside), session.row.hover decorates the hover
7781
- // card. All read off the ledger like the schedule mark.
7711
+ // card. The menu.item seat is deliberately NOT read anymore: its
7712
+ // occupants need slot-level inject hooks (useShortcuts on 0.2.1-rc.2)
7713
+ // and the in-row menu they anchored was retired in issue #12. All read
7714
+ // off the ledger like the schedule mark.
7782
7715
  const seatEntriesOf = typeof seatEntriesReader === 'function' ? seatEntriesReader : null
7783
- const rowMenuSeat = seatEntriesOf ? seatEntriesOf('sidebar.workspaces.session.menu.item') : []
7784
7716
  const rowActionSeat = seatEntriesOf ? seatEntriesOf('sidebar.workspaces.session.row.action') : []
7785
7717
  const hoverSeat = seatEntriesOf ? seatEntriesOf('sidebar.session.row.hover') : []
7786
7718
  /** Effective catalog row of one shipped command (current keybinding, label). */
@@ -8399,9 +8331,8 @@ window.__ModuleLoader__.load({
8399
8331
  const unarchiveAvailable = typeof unarchiveSession === 'function'
8400
8332
  const sessionActions = {
8401
8333
  ot: officialT,
8402
- // Right-click is the default trigger; turning the setting off moves
8403
- // the trigger to the official-style "..." hover button.
8404
- trigger: !sessionMenuEnabled,
8334
+ // Right-click is the only ctx trigger: the in-row "..." affordances
8335
+ // are retired (session row in v0.35.0, workspace row likewise).
8405
8336
  quick: rowActionsEnabled,
8406
8337
  pin: pinAvailable
8407
8338
  ? (id, pinning) => {
@@ -8431,22 +8362,13 @@ window.__ModuleLoader__.load({
8431
8362
  }
8432
8363
  : undefined,
8433
8364
  openMenu: (node, e) => openCtx('session', node, e),
8434
- // In-row "..." menu (official Rows.tsx): the SAME shared entry list
8435
- // (sessionMenuEntriesFor) and the SAME pick dispatch the right-click
8436
- // menu uses, so the two surfaces cannot drift. menuSeat carries the
8437
- // shipped menu.item occupants (rendered as Menu children);
8438
- // rowActionSeat the official quick buttons (our own buttons step
8439
- // aside while it has entries). An id the shared dispatcher does not
8440
- // own (bw extensions: move-group / customize) falls back to the
8441
- // ctx surface's handler through a transient payload.
8442
- menuItems: (node) => (typeof sessionMenuEntriesFor === 'function' ? sessionMenuEntriesFor(node) : []),
8443
- menuSeat: rowMenuSeat,
8365
+ // Row-trailing quick actions: the shipped row.action seat carries the
8366
+ // official pin/archive buttons; our own buttons render only where the
8367
+ // seat is empty. The in-row "..." menu is RETIRED (issue #12 — its
8368
+ // menu.item occupants need slot-level inject hooks an in-row
8369
+ // reproduction cannot supply); the context menu keeps every entry
8370
+ // through sessionMenuEntriesFor + handleCtxPick.
8444
8371
  rowActionSeat,
8445
- pick: (node, id) => {
8446
- if (typeof pickSessionAction === 'function' && pickSessionAction(node, id)) return
8447
- setCtx({ kind: 'session', payload: node, x: 0, y: 0 })
8448
- if (typeof handleCtxPick === 'function') handleCtxPick(id)
8449
- },
8450
8372
  rename: (node) => setDialog({ kind: 'sess-rename', session: node }),
8451
8373
  hoverBody: (node, status) => sessionHoverBody(node, status),
8452
8374
  explainArchived: () => {
@@ -9262,13 +9184,11 @@ window.__ModuleLoader__.load({
9262
9184
  // SEE the new session appear, even if the row was collapsed.
9263
9185
  onStart: () => { actions.setSessionsExpanded(workspace.workspaceId, true); startSession(workspace.workspaceId) },
9264
9186
  onContextMenu: (e) => openCtx('workspace', workspace, e),
9265
- // Official Rows.tsx: the ellipsis button anchors the row's own
9266
- // menu; the ctx surface stays the right-click path. While the
9267
- // menu is open the row pins (hover fill + visible actions),
9268
- // matching the official menuOpen state.
9269
- onMenu: (e) => openCtx('workspace', workspace, e),
9187
+ // The in-row ellipsis is retired; the ctx surface stays the
9188
+ // right-click path. While the menu is open the row still pins
9189
+ // (hover fill + visible actions), matching the official menuOpen
9190
+ // state.
9270
9191
  menuOpen: !!(ctx && ctx.kind === 'workspace' && ctx.payload && ctx.payload.workspaceId === workspace.workspaceId),
9271
- menuAria: officialText('actions.workspace.aria', { name: workspace.title || workspace.leaf }) || undefined,
9272
9192
  hoverBody: workspaceHoverBody,
9273
9193
  dropHalf: wsDropHalf(workspace.workspaceId),
9274
9194
  dropInto: wsRootDropActive(workspace.workspaceId),
@@ -9581,8 +9501,8 @@ window.__ModuleLoader__.load({
9581
9501
  ]
9582
9502
  if (ctx.kind === 'session') {
9583
9503
  // The shared builder owns the session list (see
9584
- // sessionMenuEntriesFor): the row's own "..." menu renders the
9585
- // exact same entries, so the two surfaces cannot drift.
9504
+ // sessionMenuEntriesFor): the ctx menu and any future surface share
9505
+ // it, so the entries cannot drift.
9586
9506
  return sessionMenuEntriesFor(ctx.payload || {})
9587
9507
  }
9588
9508
  return [
@@ -10321,6 +10241,10 @@ window.__ModuleLoader__.load({
10321
10241
  // closure live, so every render and every menu build re-asks; hosts
10322
10242
  // without the service keep returning null, which simply hides the row.
10323
10243
  presetFaceOf: () => presetFace,
10244
+ // Issue #13: the PRIVATE store face — a stable getter like presetFaceOf
10245
+ // (the slot renderer caches inject results; keep the same discipline
10246
+ // even though this face never changes).
10247
+ localStore: () => ({ useStore: useViewStore, actions: viewInstance.actions }),
10324
10248
  // The shipped sidebar's OWN face (dsh 0.1.7-rc.2): the shortcut request
10325
10249
  // channel and the catalog of effective keybindings. Both belong to the
10326
10250
  // entry this seat shadows, so they are read off the ledger and consumed
@@ -10435,6 +10359,22 @@ window.__ModuleLoader__.load({
10435
10359
  // One shared store handle: the browser and the settings page must see the
10436
10360
  // same persisted state (expansion, folder list, prefs, styling).
10437
10361
  const viewStore = createViewStore()
10362
+ // Issue #13 hardening: our OWN store instance. The renderer synthesizes
10363
+ // props.useStore/actions from the seat's store handle — and a third-party
10364
+ // occupant wrapper (im-connect, issue #13) that shadows the workspace seat
10365
+ // replaces those props with ITS channel store, silently severing every
10366
+ // local read and write. The bundle therefore keeps one PRIVATE instance,
10367
+ // handed to the components through the inject face: local state no longer
10368
+ // depends on what the renderer (or anything shadowing it) put into props.
10369
+ // Same persist key, so on a healthy host both instances hydrate the same
10370
+ // snapshot; the renderer-side instance simply never writes again (no
10371
+ // consumer dispatches it) and ours becomes the only live one.
10372
+ const viewInstance = viewStore.create()
10373
+ const useViewStore = typeof React.useSyncExternalStore === 'function'
10374
+ ? (selector) => React.useSyncExternalStore(
10375
+ viewInstance.subscribe,
10376
+ () => { try { return selector(viewInstance.getSnapshot()) } catch (error) { return undefined } })
10377
+ : null
10438
10378
 
10439
10379
  // Cold-restart title fallback (see the title-cache block). Hydration
10440
10380
  // reads localStorage synchronously here, before the first render, so a
@@ -10510,6 +10450,9 @@ window.__ModuleLoader__.load({
10510
10450
  key: 'dsh-better-workspace',
10511
10451
  locale: NS,
10512
10452
  store: viewStore,
10453
+ // Issue #13: the settings card reads/writes through our PRIVATE store
10454
+ // instance too (same rationale as the browser seat).
10455
+ inject: () => ({ localStore: () => ({ useStore: useViewStore, actions: viewInstance.actions }) }),
10513
10456
  },
10514
10457
  BetterWorkspacePluginCard,
10515
10458
  ))