@wenbin_wb/dsh-bridge 2.10.12 → 2.10.14

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.
@@ -20,6 +20,55 @@ export const MOBILE_STYLES_CSS = `
20
20
  --dsh-mobile-safe-bottom: env(safe-area-inset-bottom, 0px);
21
21
  }
22
22
 
23
+ /* 挂在 <body> 下、自带遮罩的「模态根」必须能盖在设置弹窗之上。
24
+ —— 这是本插件抬高设置弹窗层叠后必须自己补上的一环(谁抬的谁负责)。
25
+
26
+ 故障现象:插件市场设置页点「安装」没有任何反应。
27
+ 根因链:
28
+ 1) 本插件把设置弹窗抬到 z-index:10002(见下面 @media 块),以便盖住自己渲染的
29
+ 移动端顶栏(9998)/抽屉遮罩(9999);
30
+ 2) 插件市场(dshmarket 1.44.0)的安装确认框是挂在 <body> 下的
31
+ div._root_w1urq_2,position:fixed,但只有 z-index:1000;
32
+ 3) 该浮层内部的对话框 div._dialog_w1urq_22 虽然带 role="dialog"、被下面的
33
+ 10005 规则抬起来了,**但 z-index 无法逃出祖先的层叠上下文** —— 外层模态根
34
+ 只有 1000,整棵子树(连同那个 10005)一起被设置弹窗(10002)盖死;
35
+ 4) 结果:竖屏点安装完全看不到浮层(设置弹窗的遮罩在 elementFromPoint 上命中);
36
+ 横屏(宽度 > 767 时本插件样式不生效,设置弹窗回落成宿主的 1000)浮层虽出现,
37
+ 却与页面内容互相穿透、显示不完整。
38
+
39
+ 选择器刻意不硬编码任何第三方类名/哈希,只认模态根的两个**结构特征**:
40
+ 「body 的直接子级」+「自带遮罩子元素(或有 role=dialog 子元素)」。
41
+ 实测在打开设置前/后/点安装后三个阶段,本页面上该选择器**只命中市场那一个浮层**,
42
+ 无误伤;本插件自己的 #dsh-remote-workspace-modal 有 ID 规则(100000),优先级更高,
43
+ 不受影响。仅在设置弹窗打开时生效(body:has(...)),把影响面压到最小。
44
+
45
+ 【已知边界】判据只看结构,无法区分「真模态」与「恰好长成这样、且不可关闭的全屏
46
+ 遮罩」。若某个插件往 <body> 下挂一个带 mask 子元素的全屏层,它也会被抬到设置
47
+ 弹窗之上并盖住设置页。这是「模态就该盖在设置弹窗之上」的既定取舍:宁可让真模态
48
+ 可见,也不接受"弹了却点不到"。独立验收已实测该构造会命中;若将来真踩到,
49
+ 应改为按「该浮层是否可关闭」进一步收窄,而不是回退这条规则。 */
50
+ body:has(div[class*="VOzbGW_overlay"]) > div:has(> div[class*="mask"], > div[role="dialog"]) {
51
+ z-index: 10050 !important;
52
+ }
53
+
54
+ /* 同上场景的第二个独立问题:第三方模态对话框「比视口还高」。
55
+ 插件市场的安装确认框是固定 584px 高 + overflow-y:hidden,在矮视口(横屏手机
56
+ 320–430px 高,以及 320×480 这类小屏)里被居中撑开 → 上下同时被切掉,底部
57
+ 「Cancel / Confirm install」落到视口之外,用户点不到、流程走不下去
58
+ (实测 667×375 时对话框 rect y=-116 h=608、按钮 y=432;844×390 同样)。
59
+
60
+ 修法是让模态对话框不超过视口并允许内部滚动。这条是**自限性**的:
61
+ max-height 只在「视口比对话框矮」时才起作用 —— 375×667 / 390×844 等正常竖屏下
62
+ 100dvh-16px(=651/828) > 584,声明不产生任何视觉变化。
63
+ vh 与 dvh 各写一遍:不支持 dvh 的环境退回 vh;两条都失效也只是保持原样。 */
64
+ body:has(div[class*="VOzbGW_overlay"]) > div:has(> div[role="dialog"]) > div[role="dialog"] {
65
+ /* 留 32px 而不是 16px:该对话框是 content-box + 24px 纵向内边距,
66
+ 按内容盒算的 max-height 会再多出内边距的边框盒高度,16px 时仍溢出 4px。 */
67
+ max-height: calc(100vh - 32px) !important;
68
+ max-height: calc(100dvh - 32px) !important;
69
+ overflow-y: auto !important;
70
+ }
71
+
23
72
  /* 断点与宿主判据对齐:宿主用 viewportWidth < 768 决定右侧栏自动全屏
24
73
  (dsh-client-ui-sidebar-right/lib/client.js),故移动端样式取 <=767px,
25
74
  768px 起彻底交还桌面布局,避免"桥渲染顶栏、宿主却未全屏"的错位。 */
@@ -664,56 +713,91 @@ export const MOBILE_STYLES_CSS = `
664
713
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25) !important;
665
714
  background: var(--dsw-alias-bg-layer-1, #ffffff) !important;
666
715
  }
716
+ /* 6.1 481–767px:保留左侧图标轨道。
717
+ 78px 轨道把 "Agent presets"/"Plugin Market" 挤到两侧只剩 ~5px、标签被压到
718
+ 10.5px 且 word-break:break-all 会从词中间断字,故放宽到 88px / 11.5px,
719
+ 并改用 overflow-wrap 兜底(正常单词不再被拆)。 */
667
720
  nav[class*="VOzbGW_nav"] {
668
- width: 78px !important;
669
- min-width: 78px !important;
670
- max-width: 78px !important;
671
- padding: 10px 4px !important;
721
+ width: 88px !important;
722
+ min-width: 88px !important;
723
+ max-width: 88px !important;
724
+ padding: 10px 6px !important;
672
725
  box-sizing: border-box !important;
673
726
  border-right: 1px solid var(--dsw-alias-border-l2, #e5e7eb) !important;
674
727
  display: flex !important;
675
728
  flex-direction: column !important;
676
729
  gap: 6px !important;
677
730
  overflow-y: auto !important;
731
+ overscroll-behavior: contain !important;
678
732
  }
679
733
  nav[class*="VOzbGW_nav"] button[class*="VOzbGW_navCell"],
680
734
  button[class*="VOzbGW_navCell"] {
681
- padding: 8px 2px !important;
735
+ padding: 7px 2px !important;
682
736
  display: flex !important;
683
737
  flex-direction: column !important;
684
738
  align-items: center !important;
685
739
  justify-content: center !important;
686
740
  text-align: center !important;
687
741
  height: auto !important;
688
- min-height: 48px !important;
689
- gap: 4px !important;
742
+ min-height: 46px !important;
743
+ /* flex:none:单元格不得被压缩,否则矮视口下末项会贴着轨道下沿被切掉 */
744
+ flex: 0 0 auto !important;
745
+ gap: 3px !important;
690
746
  border-radius: 10px !important;
691
747
  }
692
748
  span[class*="VOzbGW_navLabel"] {
693
- font-size: 10.5px !important;
694
- line-height: 1.2 !important;
749
+ font-size: 11.5px !important;
750
+ line-height: 1.25 !important;
695
751
  white-space: normal !important;
696
- word-break: break-all !important;
752
+ word-break: normal !important;
753
+ overflow-wrap: anywhere !important;
697
754
  text-align: center !important;
698
755
  }
756
+
757
+ /* 矮视口(横屏手机):压缩轨道单元格,保证 6 个分类全部落在面板内,
758
+ 而不是靠 nav 自身的 overflow-y 滚动把末项藏起来(当前无任何可滚动提示)。 */
759
+ @media (max-height: 500px) {
760
+ nav[class*="VOzbGW_nav"] {
761
+ padding: 8px 6px !important;
762
+ gap: 4px !important;
763
+ }
764
+ nav[class*="VOzbGW_nav"] button[class*="VOzbGW_navCell"],
765
+ button[class*="VOzbGW_navCell"] {
766
+ min-height: 40px !important;
767
+ padding: 4px 2px !important;
768
+ }
769
+ div[class*="VOzbGW_navTitle"] {
770
+ font-size: 13px !important;
771
+ line-height: 1.3 !important;
772
+ padding: 0 4px !important;
773
+ }
774
+ }
775
+
699
776
  div[class*="VOzbGW_content"] {
700
777
  flex: 1 1 auto !important;
701
778
  min-width: 0 !important;
702
- width: calc(100% - 78px) !important;
703
- max-width: calc(100% - 78px) !important;
779
+ width: calc(100% - 88px) !important;
780
+ max-width: calc(100% - 88px) !important;
704
781
  display: flex !important;
705
782
  flex-direction: column !important;
706
783
  overflow: hidden !important;
707
784
  }
785
+ /* overflow-x 必须是 auto 而不是 hidden:宿主原样式只写了 overflow-y:auto,
786
+ 按 CSS Overflow 规范 overflow-x 会计算为 auto(可横滑)。此前插件强制
787
+ hidden,把「可滚动」降级成「静默裁切」——溢出内容既看不到也滑不出来
788
+ (实测 375px 下裁掉 42px,Plugin Market 的 Installed/Advanced 两个 Tab
789
+ 直接不可达)。这里恢复宿主默认,保证任何第三方设置节的内容至少可达。 */
708
790
  div[class*="VOzbGW_options"] {
709
791
  flex: 1 1 auto !important;
710
792
  width: 100% !important;
711
793
  max-width: 100% !important;
794
+ min-width: 0 !important;
712
795
  box-sizing: border-box !important;
713
796
  padding: 0 14px 20px !important;
714
- overflow-x: hidden !important;
797
+ overflow-x: auto !important;
715
798
  overflow-y: auto !important;
716
799
  -webkit-overflow-scrolling: touch !important;
800
+ overscroll-behavior: contain !important;
717
801
  }
718
802
 
719
803
  /* 设置中心选项行手机自适应(垂直流式,防文字单字折行) */
@@ -723,12 +807,15 @@ export const MOBILE_STYLES_CSS = `
723
807
  align-items: stretch !important;
724
808
  gap: 8px !important;
725
809
  width: 100% !important;
810
+ /* min-width:0:让 flex 子项可以真正收缩,而不是把父容器顶宽后溢出 */
811
+ min-width: 0 !important;
726
812
  padding: 12px 0 !important;
727
813
  box-sizing: border-box !important;
728
814
  }
729
815
  div[class*="VOzbGW_options"] div[class*="_rowText"] {
730
816
  width: 100% !important;
731
817
  max-width: 100% !important;
818
+ min-width: 0 !important;
732
819
  }
733
820
  div[class*="VOzbGW_options"] button[class*="_selector"],
734
821
  div[class*="VOzbGW_options"] select,
@@ -796,6 +883,209 @@ export const MOBILE_STYLES_CSS = `
796
883
  }
797
884
  }
798
885
 
886
+ /* 6.2 <=480px 手机竖屏:设置中心改为「两级钻取」。
887
+ 侧边轨道在 375px 屏上要吃掉 88px(21% 宽度),内容列只剩 277px,第三方设置节
888
+ (Plugin Market 的 Tab 条需 470px)必然溢出。改为:分类列表页 ↔ 内容详情页,
889
+ 内容独占整宽(375px 下由 277px 提升到 355px,+28%),标签回到 15px、触控行 52px。
890
+
891
+ 【整块由 html[data-dshbr-drilldown="ready"] 门控】
892
+ 本块默认把内容区(options)隐藏、只显示分类列表;能不能从列表走进内容,完全
893
+ 取决于 client/index.js 的点击监听器。所以本块不能以「JS 一定在」为无条件前提:
894
+ 开关没打开时本块整体惰性,<=480px 退回 767 块的 88px 图标轨道(窄,但分类与
895
+ 内容都完整可达),不会出现「内容被藏了、又没人能把用户带进去」的死列表。
896
+
897
+ 开关何时打开(client/index.js 的 setupSettingsDrilldown):
898
+ - 该函数是 apply() 的第一条语句,早于注入本 CSS 的 injectMobileStyles();
899
+ - 只有 document.addEventListener('click', …) 成功返回后才打上 ready。
900
+ 因此「本 CSS 已生效、而点击监听器没装上」在可执行路径上不可达;即使将来有人把
901
+ 该调用挪到会抛异常的初始化之后,开关未开也只是退回轨道布局。
902
+
903
+ 明确未覆盖:无法在脚本内探测「addEventListener 被环境静默丢弃」这类异常 ——
904
+ 要做这种自检就必须在 document 上派发合成 click,而宿主与第三方插件合计有 6 个
905
+ document 级 click 监听(另有多组 mousedown/pointerdown)会被误触发,得不偿失。
906
+ 插件 JS 完全不执行时本 CSS 也不会注入,同样不会藏内容。
907
+
908
+ 开关放在 <html> 而非 panel:监听器必须在弹窗出现之前就装好,那时 panel 还不存在。
909
+
910
+ panel 上的 data-dshbr-settings-view 只表达「列表页 / 详情页」:
911
+ - 无该属性(含开关刚打开的首帧)→ 分类列表页
912
+ - "detail" → 内容详情页 */
913
+ @media (max-width: 480px) {
914
+ /* 面板由 767 块的横向轨道布局(row)改为纵向:列表页/详情页都是上下结构。
915
+ 漏掉这一条会让 nav 与 content 并排抢宽度,分类列表被压成 0 宽(实测 options
916
+ 只剩 24px),是本方案最容易漏的关键一步。 */
917
+ html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"] {
918
+ flex-direction: column !important;
919
+ /* ✕ 要绝对定位到面板右上角,必须有定位祖先 */
920
+ position: relative !important;
921
+ }
922
+
923
+ /* 关闭按钮钉在面板右上角,与标题/返回行同处第一行 —— 这是弹窗的常规约定。
924
+ 之所以要绝对定位:宿主 DOM 里 ✕ 属于 content>header,而标题属于 nav,二者是
925
+ 两棵子树。此前靠 order:-1 把 content 提到 nav 之前,结果 ✕ 落在标题上面一行;
926
+ 详情页反过来,✕ 落到返回行下面一行。绝对定位是唯一能在不注入 DOM 的前提下
927
+ 把两者拉回同一行的办法。 */
928
+ html[data-dshbr-drilldown="ready"] button[class*="VOzbGW_close"] {
929
+ position: absolute !important;
930
+ top: 9px !important;
931
+ right: 9px !important;
932
+ width: 32px !important;
933
+ height: 32px !important;
934
+ z-index: 5 !important;
935
+ }
936
+
937
+ /* 分类列表页:内容区只剩 header(宿主/第三方 action 槽),下沉为底部动作栏。
938
+ 不再用 order:-1 —— 标题回到 DOM 顺序的第一行,顺带让焦点顺序与视觉顺序一致。 */
939
+ html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"]:not([data-dshbr-settings-view="detail"]) > div[class*="VOzbGW_content"] {
940
+ flex: 0 0 auto !important;
941
+ width: 100% !important;
942
+ max-width: 100% !important;
943
+ border-top: 1px solid var(--dsw-alias-border-l2, #e5e7eb) !important;
944
+ }
945
+ html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"]:not([data-dshbr-settings-view="detail"]) div[class*="VOzbGW_options"] {
946
+ display: none !important;
947
+ }
948
+ html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"]:not([data-dshbr-settings-view="detail"]) > nav[class*="VOzbGW_nav"] {
949
+ flex: 1 1 auto !important;
950
+ min-height: 0 !important;
951
+ overflow-y: auto !important;
952
+ overscroll-behavior: contain !important;
953
+ }
954
+
955
+ /* 详情页:折叠导航为「‹ 标题」一行,充当返回入口 */
956
+ html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"][data-dshbr-settings-view="detail"] div[class*="VOzbGW_navList"] {
957
+ display: none !important;
958
+ }
959
+ html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"][data-dshbr-settings-view="detail"] > nav[class*="VOzbGW_nav"] {
960
+ flex: 0 0 auto !important;
961
+ overflow: visible !important;
962
+ cursor: pointer !important;
963
+ }
964
+ html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"][data-dshbr-settings-view="detail"] > nav[class*="VOzbGW_nav"]:active {
965
+ background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05)) !important;
966
+ }
967
+ /* 详情页也把动作栏沉到底部,与列表页保持一致。
968
+ 用 flex order 交换 header/options 的视觉位置:options 仍是那个 overflow:auto
969
+ 的滚动容器,滚动语义不变(不用 column-reverse,避免滚动原点翻转)。 */
970
+ html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"][data-dshbr-settings-view="detail"] div[class*="VOzbGW_header"] {
971
+ order: 2 !important;
972
+ border-top: 1px solid var(--dsw-alias-border-l2, #e5e7eb) !important;
973
+ }
974
+ html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"][data-dshbr-settings-view="detail"] div[class*="VOzbGW_options"] {
975
+ order: 1 !important;
976
+ }
977
+
978
+ /* 轨道在窄屏改成整宽列表容器 */
979
+ html[data-dshbr-drilldown="ready"] nav[class*="VOzbGW_nav"] {
980
+ width: 100% !important;
981
+ min-width: 0 !important;
982
+ max-width: 100% !important;
983
+ padding: 10px 12px 12px !important;
984
+ gap: 4px !important;
985
+ border-right: none !important;
986
+ border-bottom: none !important;
987
+ }
988
+ html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_navTitle"] {
989
+ font-size: 17px !important;
990
+ line-height: 1.3 !important;
991
+ /* 不可折行的超长 token(无空格长串)会整块溢出、把文字顶到 ✕ 下面。
992
+ 独立验收实测 81 字符无空格标题时文字右端 823px、与 ✕ 交叠 707px²。
993
+ overflow-wrap: anywhere 让这种 token 也能断行 —— 留白只管盒内,管不住溢出。 */
994
+ overflow-wrap: anywhere !important;
995
+ /* 右侧 52px 给绝对定位的 ✕ 让位(紧邻规则里的声明会被后置同名规则覆盖,
996
+ 所以留白必须写在这一条上,不能只写在前面那条) */
997
+ padding: 0 52px 4px 4px !important;
998
+ }
999
+ html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_navList"] {
1000
+ flex-direction: column !important;
1001
+ gap: 2px !important;
1002
+ width: 100% !important;
1003
+ min-width: 0 !important;
1004
+ }
1005
+
1006
+ /* 列表行:图标左、文字右、行尾 › 指示 */
1007
+ html[data-dshbr-drilldown="ready"] nav[class*="VOzbGW_nav"] button[class*="VOzbGW_navCell"],
1008
+ html[data-dshbr-drilldown="ready"] button[class*="VOzbGW_navCell"] {
1009
+ flex-direction: row !important;
1010
+ justify-content: flex-start !important;
1011
+ align-items: center !important;
1012
+ text-align: left !important;
1013
+ width: 100% !important;
1014
+ min-height: 52px !important;
1015
+ padding: 12px 14px !important;
1016
+ gap: 12px !important;
1017
+ border-radius: 12px !important;
1018
+ }
1019
+ html[data-dshbr-drilldown="ready"] nav[class*="VOzbGW_nav"] button[class*="VOzbGW_navCell"]::after {
1020
+ content: '' !important;
1021
+ width: 7px !important;
1022
+ height: 7px !important;
1023
+ margin-left: auto !important;
1024
+ flex: none !important;
1025
+ border-right: 1.7px solid var(--dsw-alias-label-tertiary, #9ca3af) !important;
1026
+ border-bottom: 1.7px solid var(--dsw-alias-label-tertiary, #9ca3af) !important;
1027
+ transform: rotate(-45deg) !important;
1028
+ }
1029
+ html[data-dshbr-drilldown="ready"] nav[class*="VOzbGW_nav"] button[class*="VOzbGW_navCell"] svg {
1030
+ width: 20px !important;
1031
+ height: 20px !important;
1032
+ }
1033
+ html[data-dshbr-drilldown="ready"] span[class*="VOzbGW_navLabel"] {
1034
+ font-size: 15px !important;
1035
+ line-height: 1.35 !important;
1036
+ white-space: normal !important;
1037
+ word-break: normal !important;
1038
+ text-align: left !important;
1039
+ flex: 0 1 auto !important;
1040
+ }
1041
+
1042
+ /* 详情页返回行:‹ 箭头 + 标题 */
1043
+ /* 详情页返回行:‹ 箭头 + 标题。右侧同样要留 52px 给 ✕ ——
1044
+ 本条比基类多一个属性选择器、特异性更高,会**整体覆盖**基类的 padding,
1045
+ 所以留白必须在这里再写一遍(漏写时基类的 52px 会被这条的 4px 顶掉,
1046
+ 长标题会钻到 ✕ 下面;独立验收实测注入 61 字符标题后交叠 608/707 px²)。 */
1047
+ html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"][data-dshbr-settings-view="detail"] div[class*="VOzbGW_navTitle"] {
1048
+ display: flex !important;
1049
+ align-items: center !important;
1050
+ overflow-wrap: anywhere !important;
1051
+ padding: 2px 52px 2px 4px !important;
1052
+ }
1053
+ html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_panel"][data-dshbr-settings-view="detail"] div[class*="VOzbGW_navTitle"]::before {
1054
+ content: '' !important;
1055
+ width: 8px !important;
1056
+ height: 8px !important;
1057
+ margin: 0 10px 0 6px !important;
1058
+ flex: none !important;
1059
+ border-left: 2px solid var(--dsw-alias-label-primary, #111827) !important;
1060
+ border-bottom: 2px solid var(--dsw-alias-label-primary, #111827) !important;
1061
+ transform: rotate(45deg) !important;
1062
+ }
1063
+
1064
+ /* content/header 必须是 static —— ✕ 是绝对定位的,它的包含块取决于最近的**定位祖先**。
1065
+ 本来这个祖先就是 panel(宿主自带 position:relative);但一旦 content 或 header 被
1066
+ 任何来源(本块、宿主其它规则、第三方插件)设成 relative/absolute,包含块就会下移,
1067
+ ✕ 会从面板右上角跑到内容区里去。独立验收实测:给 content 加 position:relative 后
1068
+ ✕ 的 relTop 由 9 变成 579、并与底部动作栏重叠 837 px²。 */
1069
+ html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_content"] {
1070
+ position: static !important;
1071
+ width: 100% !important;
1072
+ max-width: 100% !important;
1073
+ flex: 1 1 auto !important;
1074
+ }
1075
+ /* header 不再当顶栏用:它现在只承载 action 槽,高度随内容(可能是宿主的一个
1076
+ 按钮,也可能是多个插件注入的多行按钮),靠 flex 自然撑开,不设固定高度。 */
1077
+ html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_header"] {
1078
+ position: static !important;
1079
+ height: auto !important;
1080
+ padding: 8px 10px !important;
1081
+ align-items: center !important;
1082
+ justify-content: flex-end !important;
1083
+ }
1084
+ html[data-dshbr-drilldown="ready"] div[class*="VOzbGW_options"] {
1085
+ padding: 0 12px 16px !important;
1086
+ }
1087
+ }
1088
+
799
1089
  /* 远程工作区选择弹窗移动端/桌面端自适应样式 */
800
1090
  #dsh-remote-workspace-modal {
801
1091
  position: fixed !important;
@@ -0,0 +1,77 @@
1
+ // DSH 官方目录选择器的「让位」判定(issue #28 第 2 条)
2
+ //
3
+ // 背景:DSH 0.1.5 起自带官方目录选择器,且注册到与 dsh-bridge 完全相同的两个 Slot:
4
+ // conversation.hero.workspace.directoryFlow
5
+ // sidebar.workspaces.directoryFlow
6
+ // 后端由 @deepseek-ai/dsh-host-directory-picker-auto 按宿主事实分发:非回环绑定 /
7
+ // SSH / Linux 无 zenity·kdialog 等图形 chooser → browse(网页树形选择器),
8
+ // macOS·Windows 或 Linux 有 chooser → native(系统对话框)。
9
+ //
10
+ // 而 DSH 的 slots 是 shadow 语义:@deepseek-ai/dsh-cordis-client-runner 的注释明确
11
+ // "Page-local shadowing rank. A later registration receives a lower priority."
12
+ // "a dynamically registered entry is assigned a lower priority than the shipped
13
+ // one, which makes it the winner"
14
+ // 即数值越小越优先、后注册者胜。动态加载的 dsh-bridge 因此天然覆盖内置 picker,
15
+ // 叠加本插件显式写的 `priority: -10`,连本机(127.0.0.1 / Electron)也被插件自己的
16
+ // 远程目录选择器接管:点「添加工作区」弹的是插件的远程抽屉而非 DSH 官方选择器。
17
+ //
18
+ // 让位条件(两者同时成立才让位):
19
+ // 1. 本机访问(isLocalEnvironment())—— 本机有完整权限,官方 picker 直接可用;
20
+ // 2. DSH 已自带官方 picker —— 旧版 DSH 没有,必须继续由插件兜底,
21
+ // 否则本机点「添加工作区」将彻底没有反应。
22
+ // 远程 / 移动访问一律继续由插件接管:那里需要管理密码解锁与 local_only 目录策略,
23
+ // 不能换成官方 picker(会绕过插件的访问限制)。
24
+ //
25
+ // 为什么探测用 ctx.get() 而不是 ctx.uiWorkspace:
26
+ // cordis 本体对**未在插件 inject 中声明**的服务做属性访问会直接抛错:
27
+ // cannot get property "uiWorkspace" without inject (cordis/lib/index.js)
28
+ // —— dsh-bridge 的 client half 是静态 client 插件,其 ctx 是真 cordis Context
29
+ // (不是动态包的 runner guard facade),所以拦在这里的是 cordis 本体的规则。
30
+ // 而 `ctx.get(name)` 是允许的「可选查找」,不要求声明。
31
+ // 又不能把 uiWorkspace 写进 inject:旧版 DSH(无该 seat)会因此把整个插件 park 掉
32
+ // (provider 缺失即不 apply),所以只能用 ctx.get() 探测。
33
+ //
34
+ // ⚠️ 已知时序依赖(独立验收记录,真机 6/6 冷启动实测未触发):
35
+ // cordis 的 ctx.get() 在 provider fiber 尚未进入 ACTIVE 时会返回 undefined。理论上若
36
+ // 本判定早于 ui-workspace 插件 apply 完成,就会把「有官方 picker」误判为「无」而仍然
37
+ // 注册,让位失效。当前稳定是因为 dsh-bridge 自身 inject 的 workspaces / sessions 由
38
+ // roster 中排在 ui-workspace **之后**的插件提供,插件真正激活时 ui-workspace 早已
39
+ // apply 完成 —— 这是对 roster 顺序的**隐式耦合,不是显式保证**。若将来调整 roster
40
+ // 顺序或去掉 workspaces 依赖,需重新评估(加固思路:注册后等 uiWorkspace 可用时
41
+ // 再撤销自己的注册)。
42
+
43
+ /** 官方 workspace seat 的服务名(DSH 0.1.5 起提供 pickDirectory) */
44
+ export const OFFICIAL_WORKSPACE_SEAT = 'uiWorkspace';
45
+
46
+ /**
47
+ * 探测 DSH 是否已提供官方目录选择器。
48
+ *
49
+ * 用 `ctx.get()` 做可选查找:seat 不存在(旧版 DSH)、宿主未暴露 pickDirectory、
50
+ * 查询或属性读取抛错时一律返回 false —— 探测本身绝不外抛影响插件加载。
51
+ *
52
+ * @param {{ get?: (name: string) => unknown }} [ctx] cordis 客户端 context
53
+ * @returns {boolean} 官方 picker 是否可用
54
+ */
55
+ export function hasOfficialDirectoryPicker(ctx) {
56
+ try {
57
+ if (typeof ctx?.get !== 'function') return false;
58
+ const seat = ctx.get(OFFICIAL_WORKSPACE_SEAT);
59
+ // typeof 也放在 try 内:seat 可能是带异常 getter 的对象或 Proxy,
60
+ // 读 pickDirectory 本身就可能抛错。
61
+ return typeof seat?.pickDirectory === 'function';
62
+ } catch {
63
+ // ctx.get 抛错(宿主异常),或属性读取抛错(异常 getter / Proxy):
64
+ // 一律按「无官方 picker」处理,保持插件原有兜底行为,探测绝不外抛。
65
+ return false;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * 是否应让位给 DSH 官方目录选择器。
71
+ *
72
+ * @param {{ local?: boolean, officialPicker?: boolean }} [facts]
73
+ * @returns {boolean} true = 不注册插件选择器,本机交给 DSH 官方处理
74
+ */
75
+ export function shouldYieldToOfficialPicker(facts) {
76
+ return Boolean(facts?.local) && Boolean(facts?.officialPicker);
77
+ }