dsh-plugin-tether 0.1.21 → 0.1.22

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/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  本文件记录面向用户的变化。每个版本的这一节会原样作为该版本 Release 的说明。
4
4
 
5
+ ## 0.1.22
6
+
7
+ ### 修复
8
+
9
+ - **dsh 0.2.1-rc 上侧栏里的「连接手机」按钮不见了;dsh desktop 上它从来没出现过**([#16](https://github.com/zexadev/dsh-tether/pull/16))。这个按钮是照抄侧栏「设置」入口的样式挂上去的,抄不到就干脆不挂。dsh 0.2.1-rc 把设置入口从侧栏挪走了,浏览器界面上的按钮于是跟着消失;dsh desktop 的设置本来就在应用自己的菜单里,侧栏从来没有可抄的对象,所以那边一次都没出现过。首次配对不受影响(白名单为空时会自动弹出配对码),但换手机、加第二台设备只能靠这个按钮,没有它就只能手工删配对记录。现在抄不到「设置」就改抄侧栏底部那一行(登录后是账号行,没登录是「更多」),两处都抄不到也照样把按钮挂出来。顺带一提,注入界面走的通道也换了:dsh desktop 的界面来自应用自带的前端产物,老通道根本到不了它。
10
+ - **(手机本地模式)目录 fsync 的跳过条件收严**。0.1.20 为了绕开沙箱限制,改成「打不开的目录就跳过 fsync」;但「打不开」和「写不进去」是两回事,一个可写不可读的目录里真可能有本进程刚创建的条目,跳过就等于悄悄少落一次盘。现在要「打不开、而且也确实写不进去」才跳过。手机上那条路径(`/data/user/0` 及其以上对 App 只有执行权限)行为不变。
11
+
12
+ ### 变更
13
+
14
+ - **手机本地模式内置的 dsh 从 `0.2.0-rc.2` 升到 `0.2.1-rc.2`**。装上这一版后,第一次启动本地模式会重新解压运行时。真机逐条验过:解压、选工作区、发一条消息、发一张图。arm64 包因内置运行时变大,从约 78 MB 涨到约 83 MB;其余 ABI 不含运行时,体积不变。
15
+ - **已验证的 dsh 版本补上 `0.2.1-rc.2`**。声明的兼容范围本来就覆盖 0.2.1 这一族,不用改,装不装得上没有变化。
16
+
17
+ ### English
18
+
19
+ The Connect a phone button disappeared from the sidebar on dsh 0.2.1-rc, and never appeared at all in dsh desktop ([#16](https://github.com/zexadev/dsh-tether/pull/16)). The button is mounted by copying the style of the sidebar Settings entry, and it simply did not mount when there was nothing to copy. dsh 0.2.1-rc moved that entry out of the sidebar, so the button vanished in the browser interface; dsh desktop keeps its settings in the application menu, so the sidebar there never had anything to copy and the button never showed up at all. First pairing was unaffected — a pairing code appears by itself while no device is paired — but switching phones or adding a second device needs this button, and without it the only way out was deleting the pairing record by hand. The button now falls back to copying the row at the bottom of the sidebar (the account row when signed in, the More row when not), and it mounts with its own styling when neither reference exists. The channel the interface is injected through changed as well: the dsh desktop interface comes from the frontend bundled into the application, which the old channel could never reach.
20
+
21
+ In local mode on the phone, the condition for skipping a directory fsync is now stricter. To work around the sandbox, 0.1.20 skipped fsync for any directory the app could not open; but "cannot open" and "cannot write" are different things, and a directory that is writable but not readable may genuinely hold entries this process just created, so skipping it quietly loses a durability step. The fallback now skips only when the directory can neither be opened nor written. Behaviour on the phone is unchanged, since `/data/user/0` and everything above it grants an ordinary app execute only.
22
+
23
+ The dsh bundled into Android local mode moves from `0.2.0-rc.2` to `0.2.1-rc.2`. The first local-mode start after this update unpacks the runtime again. Verified on a real device end to end — unpacking, choosing a workspace, sending a message, and sending an image. The `arm64` APK grows from about 78 MB to about 83 MB because of the larger bundled runtime; the other ABIs carry no runtime and are unchanged.
24
+
25
+ The list of verified dsh versions gains `0.2.1-rc.2`. The declared compatibility range already covered the 0.2.1 family, so nothing changes about whether the plugin installs.
5
26
  ## 0.1.21
6
27
 
7
28
  ### 修复
package/README.md CHANGED
@@ -87,10 +87,10 @@ From then on the app connects by itself when you open it.
87
87
 
88
88
  Open the app → **Hosts** in the top bar → **Run on this phone**. The first start unpacks the runtime first (about 9 seconds measured); after that the DSH interface appears in about 2 seconds. Enter an API key in Settings and start talking.
89
89
 
90
- - **Install and go**: no Termux, no terminal, no commands, no root. Node 24 and the full dependency tree of dsh `0.2.0-rc.2` ship inside the APK and unpack offline on first start; after that only model API calls need the network.
90
+ - **Install and go**: no Termux, no terminal, no commands, no root. Node 24 and the full dependency tree of dsh `0.2.1-rc.2` ship inside the APK and unpack offline on first start; after that only model API calls need the network.
91
91
  - **No pairing**: dsh is on this phone, there is no remote end. Sessions, settings and workspaces live only on the phone and are entirely separate from what you see when connected to a machine; switching back and forth touches neither.
92
92
  - **Stays up**: while running there is a persistent notification (a foreground service), so backgrounding or locking the screen does not get the process reclaimed. If the process is killed anyway, the next launch restarts it; sessions are already on disk.
93
- - **Versioned with the app**: the bundled dsh upgrades with the app; currently `0.2.0-rc.2`.
93
+ - **Versioned with the app**: the bundled dsh upgrades with the app; currently `0.2.1-rc.2`.
94
94
  - Only the `arm64` APK carries local mode; iOS forbids child processes, so the iOS build has no such entry.
95
95
  - The phone has no bash and no full coreutils, so shell-dependent tools are incomplete; chat and file read/write are unaffected.
96
96
 
@@ -136,8 +136,8 @@ The Termux route still works if you prefer it: install dsh and this plugin insid
136
136
  - The app connects when you open it and holds no background connection — Android's doze would not let it anyway.
137
137
  - The interface on the phone is DSH's own; the narrow-screen fit comes from minimal injected styles, so a dsh layout change may need a follow-up here.
138
138
  - English and Chinese: the app follows your phone's system language, terminal output follows the computer's, and the part injected into the dsh interface follows dsh's own language setting. There is no separate language switch.
139
- - Local mode exists only in the `arm64` build, never on iOS; the phone lacks bash and a full coreutils, so shell-dependent tools are incomplete. The `arm64` APK is about 78 MB because of the bundled runtime; the others are about 32 MB.
140
- - Verified against dsh **`0.1.0-rc.7`**, **`0.1.0-rc.8`**, **`0.1.2`** (alpha and rc.1), **`0.1.5-alpha.1`**, **`0.1.5-rc.2`**, **`0.1.6-alpha.2`**, **`0.1.7-rc.2`**, **`0.2.0-rc.2`** and **`0.2.1-alpha.1`**. dsh is in developer preview — check this line before assuming a newer dsh works. From 0.1.7 dsh refuses to install a plugin whose declared range does not cover it, so a version outside this line is a refusal at install time rather than a silent risk. The browser authentication dsh introduced in 0.1.2-alpha is handled entirely on the machine side; the phone app needs no update for it.
139
+ - Local mode exists only in the `arm64` build, never on iOS; the phone lacks bash and a full coreutils, so shell-dependent tools are incomplete. The `arm64` APK is about 83 MB because of the bundled runtime; the others are about 32 MB.
140
+ - Verified against dsh **`0.1.0-rc.7`**, **`0.1.0-rc.8`**, **`0.1.2`** (alpha and rc.1), **`0.1.5-alpha.1`**, **`0.1.5-rc.2`**, **`0.1.6-alpha.2`**, **`0.1.7-rc.2`**, **`0.2.0-rc.2`**, **`0.2.1-alpha.1`**, **`0.2.1-alpha.2`** and **`0.2.1-rc.2`**. dsh is in developer preview — check this line before assuming a newer dsh works. From 0.1.7 dsh refuses to install a plugin whose declared range does not cover it, so a version outside this line is a refusal at install time rather than a silent risk. The browser authentication dsh introduced in 0.1.2-alpha is handled entirely on the machine side; the phone app needs no update for it.
141
141
 
142
142
  ## What has been verified
143
143
 
package/README.zh.md CHANGED
@@ -87,10 +87,10 @@ dsh plugin --profile web add .
87
87
 
88
88
  打开 App → 顶栏「主机」→ **在本机运行**。第一次要先解压运行时(实测约 9 秒),之后每次约 2 秒出现 dsh 的界面;在设置里填上 API Key 就能对话。
89
89
 
90
- - **装完即用**:不装 Termux、不开终端、不敲命令、不要 root。Node 24 与 dsh `0.2.0-rc.2` 的完整依赖树打在 APK 里,首次启动离线解压;之后只有调用模型 API 才需要网络。
90
+ - **装完即用**:不装 Termux、不开终端、不敲命令、不要 root。Node 24 与 dsh `0.2.1-rc.2` 的完整依赖树打在 APK 里,首次启动离线解压;之后只有调用模型 API 才需要网络。
91
91
  - **没有配对**:dsh 就在这台手机上,没有远端。会话、设置、工作区只存在手机里,和连电脑时看到的完全是两套,来回切换互不影响。
92
92
  - **常驻**:运行期间有一条常驻通知(前台服务),退后台、锁屏不会被系统收掉;进程真被杀了,下次打开 App 自动重起,会话已落盘不丢。
93
- - **版本随 App 走**:内置的 dsh 随 App 升级一起升,当前内置 `0.2.0-rc.2`。
93
+ - **版本随 App 走**:内置的 dsh 随 App 升级一起升,当前内置 `0.2.1-rc.2`。
94
94
  - 只有 `arm64` 的 APK 带本地模式;iOS 的沙箱不允许子进程,iOS 版没有这个入口。
95
95
  - 手机上没有 bash 和完整的 coreutils,依赖 shell 的工具用不全;对话、文件读写不受影响。
96
96
 
@@ -136,8 +136,8 @@ dsh plugin --profile web add .
136
136
  - App 打开时才连接,不在后台常驻——Android 的 doze 也留不住它。
137
137
  - 手机上的界面是 DSH 自己的,窄屏适配靠注入的最小样式完成;dsh 改版式时可能需要跟进。
138
138
  - 中英双语:App 界面跟随手机的系统语言;电脑终端上的输出跟随电脑的系统语言;注入 dsh 界面的那部分跟随 dsh 自己的语言设置。没有单独的语言开关。
139
- - 本地模式只有 `arm64` 包有,iOS 没有;手机上没有 bash 与完整 coreutils,依赖 shell 的工具用不全。arm64 包因内置运行时约 78 MB,其余包约 32 MB。
140
- - 已针对 dsh **`0.1.0-rc.7`**、**`0.1.0-rc.8`**、**`0.1.2`**(alpha 与 rc.1)、**`0.1.5-alpha.1`**、**`0.1.5-rc.2`**、**`0.1.6-alpha.2`**、**`0.1.7-rc.2`**、**`0.2.0-rc.2`** 与 **`0.2.1-alpha.1`** 验证。dsh 处于 developer preview,换更新的 dsh 之前先看这一行。0.1.7 起 dsh 会拒绝安装声明范围没覆盖自己的插件,所以这一行之外的版本是装不上,而不是装上了埋雷。dsh 0.1.2-alpha 引入的浏览器认证完全在电脑侧处理,手机 App 无需更新。
139
+ - 本地模式只有 `arm64` 包有,iOS 没有;手机上没有 bash 与完整 coreutils,依赖 shell 的工具用不全。arm64 包因内置运行时约 83 MB,其余包约 32 MB。
140
+ - 已针对 dsh **`0.1.0-rc.7`**、**`0.1.0-rc.8`**、**`0.1.2`**(alpha 与 rc.1)、**`0.1.5-alpha.1`**、**`0.1.5-rc.2`**、**`0.1.6-alpha.2`**、**`0.1.7-rc.2`**、**`0.2.0-rc.2`**、**`0.2.1-alpha.1`**、**`0.2.1-alpha.2`** 与 **`0.2.1-rc.2`** 验证。dsh 处于 developer preview,换更新的 dsh 之前先看这一行。0.1.7 起 dsh 会拒绝安装声明范围没覆盖自己的插件,所以这一行之外的版本是装不上,而不是装上了埋雷。dsh 0.1.2-alpha 引入的浏览器认证完全在电脑侧处理,手机 App 无需更新。
141
141
 
142
142
  ## 验证过什么
143
143
 
package/index.js CHANGED
@@ -113,7 +113,7 @@ function refuse(res) {
113
113
 
114
114
  /** 与 sidecar 无关的部分:窄屏适配注入,以及手机上看配置文件的路由 */
115
115
  function applyUi(ctx) {
116
- ctx.effect(() => ctx.webServer.tapIndex(injectNarrowScreenCss))
116
+ mountTetherUi(ctx)
117
117
 
118
118
  // 「打开配置文件」在宿主机桌面开编辑器,手机上按了毫无反应。这条路由把同一份
119
119
  // 文件的内容原样交给手机自己渲染。路径由 Host 侧的 settings.documentPath 决定,
@@ -169,8 +169,22 @@ function apply(ctx, config = {}) {
169
169
  )}`,
170
170
  )
171
171
  }
172
- // webServer.port 是真实监听端口(config 写 0 时为 OS 分配值),手机由此拿到完整界面
173
- const proxyTarget = `${ctx.webServer.host}:${ctx.webServer.port}`
172
+ // dsh 0.2.1-alpha.2 起 webServer 能开 TLS。隧道里转的是明文 HTTP(代理流还要改写请求头),
173
+ // 对着 HTTPS 监听器只会让手机白屏,所以这里直接拒,不留给用户去猜。protocol 是那一版才有的,
174
+ // 旧版本读不到就按 http 算
175
+ if ((ctx.webServer.protocol ?? 'http:') !== 'http:') {
176
+ throw new Error(
177
+ `[tether] ${tt(
178
+ 'dsh web 开了 TLS(https),而手机走的代理流只能转明文 HTTP,连上只会白屏。去掉 webServer 的 tls 配置再用手机连接。',
179
+ 'dsh web is serving HTTPS, but the proxy stream the phone uses can only carry plain HTTP — the phone would get a blank page. Remove the tls config from webServer before connecting the phone.',
180
+ )}`,
181
+ )
182
+ }
183
+ // webServer.port 是真实监听端口(config 写 0 时为 OS 分配值),手机由此拿到完整界面。
184
+ // host 从 0.2.1-alpha.2 起可以是任意字面地址:IPv6 的要带方括号,否则 sidecar 那边按
185
+ // SocketAddr 解析 --proxy-target 直接失败(`::1:3000` 不是合法写法)
186
+ const host = ctx.webServer.host.includes(':') ? `[${ctx.webServer.host}]` : ctx.webServer.host
187
+ const proxyTarget = `${host}:${ctx.webServer.port}`
174
188
 
175
189
  // sidecar 的人读日志与这里打在同一个终端上,语言跟着走,免得一半中文一半英文
176
190
  const args = ['host', '--proxy-target', proxyTarget, '--lang', terminalZh ? 'zh' : 'en']
@@ -384,13 +398,38 @@ function apply(ctx, config = {}) {
384
398
  })
385
399
  }
386
400
 
401
+ /**
402
+ * 注入有两条通道,按宿主 webserver 的能力二选一:
403
+ *
404
+ * - 结构化的 `webserver/index-inject` 表(dsh 0.2.x 起):行是纯数据,一张表喂两个
405
+ * 渲染器。浏览器那边由宿主渲进 index.html 文本;**dsh desktop** 的 index.html 是
406
+ * 应用自带的前端产物,走 file:// 加 IPC,根本不经过宿主 renderIndex,只会把这张表
407
+ * 经 boot 通道(ready.injections)交给渲染器逐行建元素。桌面端要看到这部分界面,
408
+ * 只能走这条。
409
+ * - `tapIndex` 字符串变换(老宿主):只作用于宿主渲染的 index.html,桌面端永远看不到。
410
+ *
411
+ * 判据是能力(collectIndexInjections 在不在)而不是版本号。两条只走一条:同一份文本
412
+ * 注两遍没有意义,样式和监听器都会多一份。
413
+ */
414
+ function mountTetherUi(ctx) {
415
+ if (typeof ctx.webServer.collectIndexInjections === 'function') {
416
+ ctx.effect(() =>
417
+ ctx.on('webserver/index-inject', (table) => {
418
+ table.push({ kind: 'style', text: tetherCss })
419
+ table.push({ kind: 'script', placement: 'head', text: tetherScript })
420
+ }),
421
+ )
422
+ return
423
+ }
424
+ ctx.effect(() => ctx.webServer.tapIndex(injectNarrowScreenCss))
425
+ }
426
+
387
427
  /**
388
428
  * 窄屏修正:只在 ≤640px 生效,桌面浏览器拿到同一份 index.html 也不受影响。
389
429
  * 只用语义选择器(role/aria),不碰 dsh 的 CSS Module 类名——那些名字带内容
390
430
  * 哈希,dsh 一改样式就会变,针对它们写规则必碎。
391
431
  */
392
- function injectNarrowScreenCss(html) {
393
- const css = `
432
+ const tetherCss = `
394
433
  /* 侧栏底部的 sidebar.footer.action 是横排 flex 行,官方设想里放并排的小动作。
395
434
  「连接手机」照抄「设置」的类,是占满一行的触发行;别的插件(dsh-cost-meter)
396
435
  又往同一行放 width:100% 的面板,横排下两者互挤:本按钮被压到只剩图标,
@@ -516,9 +555,10 @@ function injectNarrowScreenCss(html) {
516
555
  z-index: 2;
517
556
  }
518
557
  }`
519
- // 抽屉遮罩只是画上去的,点它不会收起——那需要脚本。这段只做这一件事:
520
- // 窄屏下点到抽屉外面就替用户按一次侧栏开关。
521
- const script = `
558
+
559
+ // 抽屉遮罩只是画上去的,点它不会收起——那需要脚本。这段只做这一件事:
560
+ // 窄屏下点到抽屉外面就替用户按一次侧栏开关。
561
+ const tetherScript = `
522
562
  (function () {
523
563
  var narrow = window.matchMedia('(max-width: 640px)')
524
564
  // 这一部分长在 dsh 界面里,语言就该跟 dsh 的设置走;它把当前语言写在 <html lang> 上
@@ -708,37 +748,90 @@ function injectNarrowScreenCss(html) {
708
748
  .catch(function (e) { viewer(tx('配对', 'Pairing'), '', String(e && e.message ? e.message : e)) })
709
749
  }
710
750
 
751
+ // 可抄样式的参照行。浏览器侧是「设置」:triggerRow(定高的横排容器)套 trigger
752
+ // (flex:1 1 0%)两层,只抄里层放进纵向堆叠的插槽,basis 0% 会变成高度、按钮塌成
753
+ // 一行字,所以两层都抄。dsh desktop 的设置入口在原生壳里(⌘,),侧栏里没有
754
+ // settings.trigger,但 footer 底下那一行(登录后是账号行,没登录是「更多」)是同一
755
+ // 族的 triggerRow,改抄它。
756
+ function hostsReference() {
757
+ var settings = document.querySelector('[data-slot="settings.trigger"]')
758
+ var trigger = settings && settings.closest ? settings.closest('button') : null
759
+ if (trigger && trigger.parentElement) return { row: trigger.parentElement, trigger: trigger, browser: true }
760
+ var row = document.querySelector('[class*="settingsArea"] [class*="triggerRow"]')
761
+ var button = row && row.querySelector('button')
762
+ if (row && button) return { row: row, trigger: button, browser: false }
763
+ return null
764
+ }
765
+
766
+ // 同一行里 padding + 图标 + gap 决定文字从哪开始,所以图标与参照行一样宽,两行的
767
+ // 图标列和文字列才对齐。桌面端那一行会随登录状态换形态(没登录是 14px 字形,登录
768
+ // 后是 24px 头像),宽度因此是量参照行量出来的,不写死;量不到就退回 16。
769
+ function referenceIconWidth(trigger) {
770
+ var art = trigger.querySelector('svg, img')
771
+ var width = art ? Math.round(art.getBoundingClientRect().width) : 0
772
+ return width >= 12 && width <= 24 ? width : 16
773
+ }
774
+
775
+ // dsh 自己那套小图标的画法:16 视图框、1.3px 描边,字形落在 1.5~14.5 的方框里
776
+ function phoneIcon(size) {
777
+ return '<svg width="' + size + '" height="' + size + '" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" aria-hidden="true">'
778
+ + '<rect x="5.15" y="2.15" width="5.7" height="11.7" rx="1.6"/><path d="M7.1 11.6h1.8" stroke-linecap="round"/></svg>'
779
+ }
780
+
711
781
  function mountHostsTrigger() {
712
782
  var slot = document.querySelector('[data-slot="sidebar.footer.action"]')
713
783
  if (!slot || slot.querySelector('[data-dsh-tether="hosts-trigger"]')) return
714
- var settings = document.querySelector('[data-slot="settings.trigger"]')
715
- var reference = settings && settings.closest ? settings.closest('button') : null
716
- if (!reference || !reference.parentElement) return
717
- // 「设置」是 triggerRow(定高的横排容器)套 trigger(flex:1 1 0%)两层。只抄
718
- // 里层放进纵向堆叠的插槽,basis 0% 会变成高度,按钮塌成一行字;两层都抄,
719
- // 几何才在宽态 / rail 态都与「设置」一致。
784
+ var reference = hostsReference()
720
785
  var row = document.createElement('div')
721
786
  row.setAttribute('data-dsh-tether', 'hosts-row')
722
- row.className = reference.parentElement.className
723
787
  var button = document.createElement('button')
724
788
  button.type = 'button'
725
789
  button.setAttribute('data-dsh-tether', 'hosts-trigger')
726
790
  var label = framed ? tx('我的电脑', 'My computers') : tx('连接手机', 'Connect a phone')
727
791
  button.setAttribute('aria-label', label)
728
- // 类名带内容哈希,照抄隔壁「设置」按钮当前的,外观自然一致
729
- button.className = reference.className
730
- button.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="7" y="2" width="10" height="20" rx="2"/><path d="M11 18h2"/></svg><span data-dsh-tether="hosts-label">' + label + '</span>'
792
+ // 浏览器侧沿用原来那个 18px 图标,一字不改;抄不到「设置」时按参照行的尺寸画
793
+ var icon = reference === null || reference.browser
794
+ ? '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="7" y="2" width="10" height="20" rx="2"/><path d="M11 18h2"/></svg>'
795
+ : phoneIcon(referenceIconWidth(reference.trigger))
796
+ button.innerHTML = icon + '<span data-dsh-tether="hosts-label">' + label + '</span>'
731
797
  button.addEventListener('click', function () {
732
798
  if (framed) window.parent.postMessage({ type: 'dsh-tether:open-hosts' }, '*')
733
799
  else showPairing()
734
800
  })
735
801
  row.append(button)
736
802
  slot.append(row)
737
- // 侧栏展开/收起时「设置」两层都会换类名(rail 与否),跟着同步
738
- new MutationObserver(function () {
739
- button.className = reference.className
740
- row.className = reference.parentElement.className
741
- }).observe(reference.parentElement, { attributes: true, attributeFilter: ['class'], subtree: true })
803
+ if (reference === null) {
804
+ ensureFallbackStyle()
805
+ } else {
806
+ var labelSpan = button.querySelector('[data-dsh-tether="hosts-label"]')
807
+ var referenceLabel = reference.trigger.querySelector('span')
808
+ // 类名带内容哈希,照抄参照行当前的,外观自然跟随 dsh 自己的构建。侧栏收起/
809
+ // 展开、登录状态变化时它会换类名,也会换图标尺寸,跟着同步
810
+ var sync = function () {
811
+ row.className = reference.row.className
812
+ button.className = reference.trigger.className
813
+ if (reference.browser) return
814
+ if (referenceLabel) labelSpan.className = referenceLabel.className
815
+ // 账号行的形态写在自己的 data-* 上(没登录 32px 矮一截,收起时 36×36 居中),
816
+ // 光抄类名只拿到组件的默认形态,会比隔壁高一截。属性名按前缀抄,不写死是哪几个
817
+ for (var i = button.attributes.length - 1; i >= 0; i--) {
818
+ var own = button.attributes[i].name
819
+ if (own.indexOf('data-') === 0 && own !== 'data-dsh-tether') button.removeAttribute(own)
820
+ }
821
+ for (var j = 0; j < reference.trigger.attributes.length; j++) {
822
+ var attr = reference.trigger.attributes[j]
823
+ if (attr.name.indexOf('data-') === 0 && attr.name !== 'data-dsh-tether') button.setAttribute(attr.name, attr.value)
824
+ }
825
+ var svg = button.querySelector('svg')
826
+ var width = referenceIconWidth(reference.trigger)
827
+ if (svg) { svg.setAttribute('width', width); svg.setAttribute('height', width) }
828
+ }
829
+ sync()
830
+ // 浏览器侧只有类名会变,照旧只盯 class;桌面端那一行换形态是改 data-*,得全盯
831
+ new MutationObserver(sync).observe(reference.row, reference.browser
832
+ ? { attributes: true, attributeFilter: ['class'], subtree: true }
833
+ : { attributes: true, subtree: true })
834
+ }
742
835
  // dsh 的语言能在设置里当场切,这个标签跟着换
743
836
  new MutationObserver(function () {
744
837
  var next = framed ? tx('我的电脑', 'My computers') : tx('连接手机', 'Connect a phone')
@@ -747,6 +840,33 @@ function injectNarrowScreenCss(html) {
747
840
  if (span) span.textContent = next
748
841
  }).observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] })
749
842
  }
843
+ // 两处参照都抄不到时的兜底。只保证这一行能看、能点:宽度铺满插槽、行高与 dsh 的
844
+ // panelRow 一致,颜色圆角焦点环全取 --dsw-* 变量,不碰任何带内容哈希的类名,也不
845
+ // 去猜隔壁此刻长什么样。抄不到就不挂按钮的话,桌面端等于没有配对入口。
846
+ var fallbackStyleInstalled = false
847
+ function ensureFallbackStyle() {
848
+ if (fallbackStyleInstalled) return
849
+ fallbackStyleInstalled = true
850
+ var style = document.createElement('style')
851
+ style.setAttribute('data-dsh-tether', 'fallback-style')
852
+ style.textContent = [
853
+ '[data-dsh-tether="hosts-row"] { box-sizing: border-box; flex: none; display: flex; align-items: center; width: 100%; }',
854
+ '[data-dsh-tether="hosts-row"] > [data-dsh-tether="hosts-trigger"] {',
855
+ ' -webkit-appearance: none; appearance: none; box-sizing: border-box;',
856
+ ' flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 8px;',
857
+ ' height: 36px; margin: 0; padding: 6px; border: none;',
858
+ ' border-radius: var(--dsw-radius-md); background: transparent;',
859
+ ' color: var(--dsw-alias-label-primary); font: inherit; font-size: 14px; line-height: 22px;',
860
+ ' text-align: left; cursor: pointer; overflow: hidden;',
861
+ '}',
862
+ '[data-dsh-tether="hosts-row"] > [data-dsh-tether="hosts-trigger"]:hover { background: var(--dsw-alias-interactive-bg-hover); }',
863
+ '[data-dsh-tether="hosts-row"] [data-dsh-tether="hosts-label"] { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
864
+ '[data-sidebar-collapsed] [data-dsh-tether="hosts-row"] > [data-dsh-tether="hosts-trigger"] { justify-content: center; gap: 0; padding: 0; }',
865
+ '[data-sidebar-collapsed] [data-dsh-tether="hosts-label"] { display: none; }',
866
+ ].join(' ')
867
+ document.head.append(style)
868
+ }
869
+
750
870
  mountHostsTrigger()
751
871
  new MutationObserver(mountHostsTrigger).observe(document.documentElement, { childList: true, subtree: true })
752
872
 
@@ -763,9 +883,12 @@ function injectNarrowScreenCss(html) {
763
883
  .catch(function (e) { viewer(tx('配置文件', 'Config file'), '', String(e && e.message ? e.message : e)) })
764
884
  }, true)
765
885
  })()`
886
+
887
+ /** 把那两份注入以字符串变换塞进 index.html */
888
+ function injectNarrowScreenCss(html) {
766
889
  return html.replace(
767
890
  '</head>',
768
- `<style data-dsh-tether="narrow-screen">${css}\n</style><script data-dsh-tether="narrow-screen">${script}\n</script></head>`,
891
+ `<style data-dsh-tether="narrow-screen">${tetherCss}\n</style><script data-dsh-tether="narrow-screen">${tetherScript}\n</script></head>`,
769
892
  )
770
893
  }
771
894
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-tether",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Mobile client for the DeepSeek Harness — use the dsh on your dev machine from your Android or iOS phone, remotely across networks, peer-to-peer over iroh with no server to set up (falls back to a relay that only sees ciphertext)",
5
5
  "keywords": [
6
6
  "dsh",
@@ -42,8 +42,9 @@
42
42
  },
43
43
  "androidRuntime": {
44
44
  "node": "24.18.0",
45
- "dsh": "0.2.0-rc.2",
46
- "koffi": "3.3.0"
45
+ "dsh": "0.2.1-rc.2",
46
+ "koffi": "3.3.0",
47
+ "zod": "4.4.3"
47
48
  }
48
49
  },
49
50
  "peerDependencies": {
@@ -55,11 +56,11 @@
55
56
  "@deepseek-ai/dsh-user-approval": { "optional": true }
56
57
  },
57
58
  "optionalDependencies": {
58
- "dsh-tether-host-android-arm64": "0.1.21",
59
- "dsh-tether-host-darwin-arm64": "0.1.21",
60
- "dsh-tether-host-darwin-x64": "0.1.21",
61
- "dsh-tether-host-linux-arm64": "0.1.21",
62
- "dsh-tether-host-linux-x64": "0.1.21",
63
- "dsh-tether-host-win32-x64": "0.1.21"
59
+ "dsh-tether-host-android-arm64": "0.1.22",
60
+ "dsh-tether-host-darwin-arm64": "0.1.22",
61
+ "dsh-tether-host-darwin-x64": "0.1.22",
62
+ "dsh-tether-host-linux-arm64": "0.1.22",
63
+ "dsh-tether-host-linux-x64": "0.1.22",
64
+ "dsh-tether-host-win32-x64": "0.1.22"
64
65
  }
65
66
  }