dsh-zen-remote 1.0.0 → 1.0.2

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.
Files changed (33) hide show
  1. package/README.md +20 -0
  2. package/docs/interface.md +1 -0
  3. package/lib/client.js +458 -33
  4. package/lib/types/client/effects/keyboard-guard.d.ts +27 -0
  5. package/lib/types/client/effects/keyboard-guard.d.ts.map +1 -0
  6. package/lib/types/client/effects/turn-fold.d.ts.map +1 -1
  7. package/lib/types/client/effects/welcome-notice.d.ts +20 -0
  8. package/lib/types/client/effects/welcome-notice.d.ts.map +1 -0
  9. package/lib/types/client/index.d.ts.map +1 -1
  10. package/lib/types/client/styles/base.css.d.ts +1 -1
  11. package/lib/types/client/styles/base.css.d.ts.map +1 -1
  12. package/lib/types/client/styles/chips.css.d.ts +1 -1
  13. package/lib/types/client/styles/chips.css.d.ts.map +1 -1
  14. package/lib/types/client/styles/compat.css.d.ts +1 -1
  15. package/lib/types/client/styles/compat.css.d.ts.map +1 -1
  16. package/lib/types/client/styles/content.css.d.ts +2 -0
  17. package/lib/types/client/styles/content.css.d.ts.map +1 -0
  18. package/lib/types/client/styles/header.css.d.ts +1 -1
  19. package/lib/types/client/styles/header.css.d.ts.map +1 -1
  20. package/lib/types/client/styles/index.d.ts +3 -2
  21. package/lib/types/client/styles/index.d.ts.map +1 -1
  22. package/package.json +7 -9
  23. package/pwa/app.css +7 -2
  24. package/src/client/effects/keyboard-guard.ts +121 -0
  25. package/src/client/effects/turn-fold.ts +14 -0
  26. package/src/client/effects/welcome-notice.ts +66 -0
  27. package/src/client/index.tsx +12 -0
  28. package/src/client/styles/base.css.ts +28 -13
  29. package/src/client/styles/chips.css.ts +10 -10
  30. package/src/client/styles/compat.css.ts +51 -0
  31. package/src/client/styles/content.css.ts +103 -0
  32. package/src/client/styles/header.css.ts +41 -7
  33. package/src/client/styles/index.ts +5 -3
package/README.md CHANGED
@@ -207,11 +207,31 @@ open http://127.0.0.1:3088/lan-gate/admin
207
207
  - 真 PWA:manifest + service worker,可装到主屏、可离线打开
208
208
  - 真 Web Push:VAPID + aes128gcm,通知默认不带对话正文
209
209
  - `push_notify` 工具:模型可在关键节点自己推一条,带限流
210
+ - 「内测声明」弹窗注入「不再弹出」可选项:远程访问每次刷新都会重弹声明,点一次后本设备记住选择、以后自动关闭
210
211
 
211
212
  深度说明:[界面](docs/interface.md) · [公网接入](docs/remote-access.md)
212
213
 
213
214
  ---
214
215
 
216
+ ## 已适配的第三方插件
217
+
218
+ 移动端 UI 对下列插件做了专门适配。所有适配都锚定对应插件自己的 DOM 标记:
219
+ 没装该插件时规则不生效,装了未列出的插件也不会被误伤。
220
+
221
+ | 插件 | 移动端适配内容 | 实测版本 |
222
+ | --- | --- | --- |
223
+ | [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) | 会话页头部提供工作台入口按钮;面板变手机全宽抽屉并避让刘海安全区;底部居中的关闭按钮 | 0.12.2 |
224
+ | [@nanmicoder/dsh-agent-teams](https://github.com/NanmiCoder/dsh-agent-teams) | AgentTeams 活动浮层挪到会话头部下方(原位置压住头部按钮)、会话列表页自动隐藏;子代理会话头部保留可点的父会话标题,一键切回主会话 | 0.1.6 |
225
+ | [dsh-usage-stats](https://github.com/Ychris12138/dsh-usage-stats) | 用量与余额入口收进主屏 chips 行 | — |
226
+ | [@opendsh/dsh-plugin-scheduled-tasks](https://github.com/Ceelog/dsh-plugins) | 定时任务入口收进主屏 chips 行 | 0.2.0 |
227
+ | dsh-at-file | @文件引用,配合附件上传的 `@` 路径引用使用 | — |
228
+ | [dsh-vision-toolkit](https://www.npmjs.com/package/@anionex/dsh-vision-toolkit) | 图像 Q&A/OCR,配合手机端附件上传使用 | — |
229
+ | [dsh-web-ui 全家桶](https://www.npmjs.com/package/@linxin666/dsh-web-ui-all) | 沿用上游 dsh-web-mobile 的兼容规则(文件树 / 预览浮层限宽居中等) | — |
230
+
231
+ 各项适配的技术细节(锚点选择器、断点、取舍记录)见[界面文档](docs/interface.md)的「兼容插件」一节。
232
+
233
+ ---
234
+
215
235
  ## 已知问题
216
236
 
217
237
  **iOS 26.x 独立 PWA 视口缩水**:加到主屏后视口底部会少掉一条状态栏高度,普通 Safari 标签页正常。这是 iOS 系统缺陷,缺掉的区域在文档之外,CSS 够不着;本插件做了三层缓解(浅色 manifest 背景 + 安全区补偿 + 强制重排),能减轻但不保证复原。彻底恢复只能整个 App 退出重开。
package/docs/interface.md CHANGED
@@ -162,6 +162,7 @@ composer 最左的回形针打开的是**手机本地**的文件选择器(iOS
162
162
  ## 兼容插件
163
163
 
164
164
  - [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar)(移动端全宽抽屉,本插件在会话页头部给它留入口位)——本机实测 **0.12.2**
165
+ - [@nanmicoder/dsh-agent-teams](https://github.com/NanmiCoder/dsh-agent-teams)(AgentTeams 活动浮层:手机端挪到会话头部下方、避开安全区,会话列表页隐藏;子代理会话头部保留可点的父会话面包屑用于切回)——本机实测 **0.1.6**
165
166
  - [dsh-usage-stats](https://github.com/Ychris12138/dsh-usage-stats)(用量与余额,主屏 chips 行可直接打开)
166
167
  - [@opendsh/dsh-plugin-scheduled-tasks](https://github.com/Ceelog/dsh-plugins)(定时任务,主屏 chips 行自动收割入口)——本机实测 **0.2.0**
167
168
  - dsh-at-file(@文件引用)
package/lib/client.js CHANGED
@@ -1692,24 +1692,39 @@ exports.BASE_CSS = `
1692
1692
  }
1693
1693
  }
1694
1694
 
1695
- /* ---------- beta-notice suppression (ALL widths — deliberate exception) ----
1696
- User-directed (2026-08-17): hide DSH's beta/preview notices everywhere,
1697
- including desktop. This is the ONE place this plugin intentionally breaks
1698
- its own ">=1024px strict no-op" rule — do not "fix" it back.
1699
- Selector notes:
1695
+ /* ---------- beta-notice handling (ALL widths — deliberate exception) ----
1696
+ User-directed (2026-08-17): tone down DSH's beta/preview notices
1697
+ everywhere, including desktop. This is the ONE place this plugin
1698
+ intentionally breaks its own ">=1024px strict no-op" rule.
1700
1699
  - The hero "预览版" badge only carries a hashed class; per repo convention
1701
1700
  the stable part is the suffix (_previewBadge), matched with [class$=].
1702
- - The first-run "内测声明" modal is hidden via its ARIA label. The label is
1703
- LOCALIZED zh is covered; if the UI runs in another language the dialog
1704
- shows (harmless, dismiss by hand). Hiding targets the presentation
1705
- wrapper via :has() so the modal's backdrop cannot linger invisibly and
1706
- block taps; the app never requires acknowledging this dialog to work. */
1701
+ - The first-run "内测声明" modal must NOT be hidden with CSS: while it is
1702
+ mounted it holds document #root inert, so display:none leaves the whole
1703
+ app unclickable (2026-08-18 incident). effects/welcome-notice.ts instead
1704
+ injects a "不再弹出" opt-out button; this styles it. */
1707
1705
  [class$="_previewBadge"] {
1708
1706
  display: none !important;
1709
1707
  }
1710
- [role="presentation"]:has(> [role="dialog"][aria-label="内测声明"]),
1711
- [role="dialog"][aria-label="内测声明"] {
1712
- display: none !important;
1708
+ /* Mirrors the official primitives Button (md capsule, outline variant):
1709
+ 36px height, 18px radius, --dsw-alias-border-l2 hairline. */
1710
+ .zen-welcome-optout {
1711
+ display: inline-flex;
1712
+ align-items: center;
1713
+ justify-content: center;
1714
+ height: 36px;
1715
+ padding: 0 14px;
1716
+ margin-right: 8px;
1717
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, .15));
1718
+ border-radius: 18px;
1719
+ background: transparent;
1720
+ color: var(--dsw-alias-label-primary, inherit);
1721
+ font-size: 14px;
1722
+ line-height: 22px;
1723
+ cursor: pointer;
1724
+ -webkit-tap-highlight-color: transparent;
1725
+ }
1726
+ .zen-welcome-optout:hover {
1727
+ background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));
1713
1728
  }
1714
1729
  `;
1715
1730
  };
@@ -2901,6 +2916,57 @@ exports.COMPAT_CSS = ` /* ---------- dsh-web-ui family compatibility ----------
2901
2916
  background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));
2902
2917
  }
2903
2918
  }
2919
+
2920
+ /* ---------- dsh-agent-teams: phone floater reposition (2026-08-18) ----------
2921
+ THIRD-PARTY COMPAT RULE — @nanmicoder/dsh-agent-teams (the AgentTeams
2922
+ activity floater: a collapsed badge + expandable panel, body-portaled
2923
+ under its own mount marker div[data-agent-teams-host]). Its own CSS
2924
+ fixes both at top:56px (<=640px) viewport coordinates and never reads
2925
+ env(safe-area-inset-*), so on a notched iPhone the badge lands INSIDE
2926
+ the safe-area + this plugin's 76px session header band — overlapping
2927
+ the info and workbench buttons (real-device report, 2026-08-18).
2928
+ Move both below the header instead: safe-area + 76px header chrome
2929
+ (48px title row + 28px view-switch row, header.css.ts) + 8px gap.
2930
+ Anchored on the plugin's own mount marker + class-suffix selectors per
2931
+ this repo's hashed-class convention — when dsh-agent-teams is not
2932
+ installed the marker never exists and this whole section is inert
2933
+ (same presence-gating as the dsh-better-sidebar rules above).
2934
+ !important because their positions come from html-level CSS vars
2935
+ (--agent-teams-panel-top et al.) whose style tag order vs ours is not
2936
+ guaranteed. z-index: their 2147483000 would float the badge over every
2937
+ one of this plugin's own overlays (drawer 40, info sheet 55-57, close
2938
+ pill 70); 30 keeps it above content but below all of them. */
2939
+ @media (max-width: 767px) {
2940
+ [data-agent-teams-host] [class$="_badge"],
2941
+ [data-agent-teams-host] [class$="_panel"] {
2942
+ top: calc(var(--mnav-sat) + 84px) !important;
2943
+ right: 10px !important;
2944
+ z-index: 30 !important;
2945
+ }
2946
+ [data-agent-teams-host] [class$="_panel"] {
2947
+ left: 10px !important;
2948
+ width: auto !important;
2949
+ /* Their min-height:min(560px, calc(100dvh - 56 - 56)) resolves against
2950
+ the OLD top var — on short phones it exceeds the space left below
2951
+ the moved-down top edge and would force overflow; content-sized with
2952
+ a hard cap replaces it. 76px bottom clearance keeps the collapsed
2953
+ composer's top edge visible under the panel. */
2954
+ min-height: 0 !important;
2955
+ max-height: calc(100dvh - var(--mnav-sat) - 84px - var(--mnav-sab) - 76px) !important;
2956
+ }
2957
+ /* The floater belongs to the SESSION page: on the home list it hovered
2958
+ over the session rows (user feedback, 2026-08-18). The phone page
2959
+ stack is this plugin's own overlay — the official conversation (and
2960
+ with it the floater's "current session") stays mounted underneath, so
2961
+ agent-teams keeps rendering it; hide it whenever the home level is the
2962
+ visible one (data-view flips to "session" inside a session, and the
2963
+ badge/panel come right back). :has() is already a documented
2964
+ requirement of this plugin (Chromium 105+, docs/interface.md). */
2965
+ body:has([data-mobile-nav="home"][data-view="home"]) [data-agent-teams-host] [class$="_badge"],
2966
+ body:has([data-mobile-nav="home"][data-view="home"]) [data-agent-teams-host] [class$="_panel"] {
2967
+ display: none !important;
2968
+ }
2969
+ }
2904
2970
  `;
2905
2971
  };
2906
2972
  __modules["styles/misc.css.js"] = function (require, module, exports) {
@@ -3678,21 +3744,55 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
3678
3744
  min-width: 0;
3679
3745
  overflow: hidden;
3680
3746
  }
3681
- /* Only the current session's own name is the "title" — the parent chain
3682
- (subagent breadcrumbs) is the "多余项" the design calls out to hide,
3683
- while the current segment (and its running dot, see below) stays. */
3684
- [data-phase] header [class$="_crumbSeg"]:not(:last-child) {
3747
+ /* Subagent sessions keep ONE ancestor crumb (2026-08-18): the PC header
3748
+ lets you tap the parent session's title to switch back — that affordance
3749
+ was lost on phone when the whole parent chain was hidden as "多余项".
3750
+ Show only the immediate parent (for agent-teams member sessions that IS
3751
+ the captain/main session; deeper ancestors stay hidden — a 3-level chain
3752
+ cannot fit the ~206px center column at 390px). The parent crumb is the
3753
+ official breadcrumb <button> (dsh-client-ui-conversation renders
3754
+ open(parentId) as its onClick), so tapping needs no JS from this plugin,
3755
+ and main sessions (chain length 1) have no :nth-last-child(2) seg at
3756
+ all — the centered single title is pixel-identical to before. */
3757
+ [data-phase] header [class$="_crumbSeg"]:not(:nth-last-child(-n+2)) {
3685
3758
  display: none !important;
3686
3759
  }
3760
+ [data-phase] header [class$="_crumbSeg"]:nth-last-child(2) {
3761
+ display: inline-flex;
3762
+ align-items: center;
3763
+ flex: none;
3764
+ min-width: 0;
3765
+ max-width: 38%;
3766
+ }
3767
+ /* Ancestors BEFORE the parent seg are hidden, which would leave the
3768
+ parent seg's own leading "/" dangling at the start of the title. */
3769
+ [data-phase] header [class$="_crumbSeg"]:nth-last-child(2) [class$="_crumbSep"] {
3770
+ display: none !important;
3771
+ }
3772
+ /* Parent crumb: small tertiary text so the 19px current title stays the
3773
+ visual anchor; min-height keeps a finger-sized target inside the 48px
3774
+ row. Specificity (0,4,1) outranks the shared 19px _crumb rule below. */
3775
+ [data-phase] header [class$="_crumbSeg"]:nth-last-child(2) [class$="_crumb"] {
3776
+ font-size: 13px;
3777
+ font-weight: 400;
3778
+ color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .45));
3779
+ min-height: 44px;
3780
+ display: inline-flex;
3781
+ align-items: center;
3782
+ padding: 0 2px 0 6px;
3783
+ touch-action: manipulation;
3784
+ -webkit-tap-highlight-color: transparent;
3785
+ }
3687
3786
  [data-phase] header [class$="_crumbSeg"]:last-child {
3688
3787
  display: flex;
3689
3788
  align-items: center;
3690
3789
  min-width: 0;
3691
3790
  max-width: 100%;
3692
3791
  }
3693
- [data-phase] header [class$="_crumbSeg"]:last-child [class$="_crumbSep"] {
3694
- display: none !important;
3695
- }
3792
+ /* The last seg's own "/" is the divider between parent and current title —
3793
+ it only exists when there IS an ancestor (index > 0), so it can simply
3794
+ stay visible now that the parent seg shows. (It used to be hidden here:
3795
+ with the parent hidden it dangled before the title.) */
3696
3796
  /* BOTH selectors are required: [class$=] matches the whole class
3697
3797
  ATTRIBUTE's suffix, and the current-session title button's attribute is
3698
3798
  "wSkVaW_crumb wSkVaW_crumbCurrent" — it ends in _crumbCurrent, so the _crumb
@@ -4840,10 +4940,14 @@ exports.CHIPS_CSS = `/* ---------- chips row + settings entry (S5) ---------- */
4840
4940
  }
4841
4941
 
4842
4942
  /* Plugin-entry chips row: one horizontally-scrolling line of 34px pills
4843
- between the workspace title and the session list (prototype spec). A
4844
- thin scrollbar is the overflow affordance (AGENTS.md: an early
4845
- no-scrollbar version silently cut off the last tab with no way to
4846
- discover more content). */
4943
+ between the workspace title and the session list (prototype spec).
4944
+ Scrollbar hidden on BOTH engines (2026-08-18 user decision): iOS
4945
+ overlay scrollbars never showed anyway, but desktop browsers painted a
4946
+ permanent 2px strip under the pills. This revisits the AGENTS.md
4947
+ "silently cut off the last tab" lesson deliberately — with 4+ chips the
4948
+ row now always overflows mid-chip (a partially visible pill is the
4949
+ scroll affordance), unlike the early incident where the chips fit
4950
+ flush and the hidden overflow was undiscoverable. */
4847
4951
  [data-mobile-nav="chip-row"] {
4848
4952
  flex: 0 0 auto;
4849
4953
  display: flex;
@@ -4853,14 +4957,10 @@ exports.CHIPS_CSS = `/* ---------- chips row + settings entry (S5) ---------- */
4853
4957
  overflow-x: auto;
4854
4958
  overscroll-behavior: contain;
4855
4959
  -webkit-overflow-scrolling: touch;
4856
- scrollbar-width: thin;
4960
+ scrollbar-width: none;
4857
4961
  }
4858
4962
  [data-mobile-nav="chip-row"]::-webkit-scrollbar {
4859
- height: 2px;
4860
- }
4861
- [data-mobile-nav="chip-row"]::-webkit-scrollbar-thumb {
4862
- background: var(--dsw-alias-border-l2, rgba(0, 0, 0, .16));
4863
- border-radius: 2px;
4963
+ display: none;
4864
4964
  }
4865
4965
  [data-mobile-nav="chip"],
4866
4966
  [data-mobile-nav="chip-more"] {
@@ -4975,6 +5075,113 @@ exports.CHIPS_CSS = `/* ---------- chips row + settings entry (S5) ---------- */
4975
5075
  }
4976
5076
  `;
4977
5077
  };
5078
+ __modules["styles/content.css.js"] = function (require, module, exports) {
5079
+ "use strict";
5080
+ // content — chat markdown readability on phones (2026-08-18). Shares no
5081
+ // selector with any other file; appended last in styles/index.ts.
5082
+ Object.defineProperty(exports, "__esModule", { value: true });
5083
+ exports.CONTENT_CSS = void 0;
5084
+ exports.CONTENT_CSS = `/* ---------- chat markdown content (< 768px) ---------- */
5085
+
5086
+ @media (max-width: 767px) {
5087
+ /* Inline code: upstream renders it display:inline-flex — an atomic inline
5088
+ box that can never wrap across lines, so one long code span walks off
5089
+ the right edge of the screen. Plain inline keeps the background pill
5090
+ per line fragment and lets long tokens break anywhere. Deliberately
5091
+ unscoped (any :not(pre) > code, not just the _markdown_ module):
5092
+ inline code also renders outside that container (user bubbles,
5093
+ reasoning rows), and on a phone there is no width at which an
5094
+ unwrappable inline box is right. !important beats the upstream
5095
+ display:inline-flex on specificity ties. */
5096
+ :not(pre) > code {
5097
+ display: inline !important;
5098
+ overflow-wrap: anywhere;
5099
+ /* iOS WebKit: overflow-wrap alone still failed to break long tokens in
5100
+ inline code on a real iPhone (screenshot-verified 2026-08-18, li >
5101
+ code path case) while desktop Chromium broke them fine. break-all is
5102
+ the everywhere-supported hammer; acceptable for code pills. */
5103
+ word-break: break-all;
5104
+ }
5105
+ /* Belt-and-suspenders for engines that compute soft-wrap opportunities
5106
+ from the block container rather than the inline itself (historical
5107
+ WebKit behavior): inherit anywhere through the whole transcript so the
5108
+ block side agrees. Prose is untouched until something would overflow;
5109
+ the table is already pinned wide by min-width: max-content below. */
5110
+ [data-chat-flow],
5111
+ [class*="_markdown_"] {
5112
+ overflow-wrap: anywhere;
5113
+ }
5114
+
5115
+ /* Markdown tables: the official _tableScroll_ wrapper is already a
5116
+ horizontal scroll viewport around a width:max-content table, but its
5117
+ cells are capped at min(30vw, 320px) — barely 120px on a phone, which
5118
+ crushes every column into a vertical word-stack. Let columns size to
5119
+ their content instead and read the overflow by scrolling the wrapper.
5120
+ 480px (not a vw cap — 85vw was ~320px on a 375px phone and still
5121
+ wrapped mid-length cells) keeps typical cells on one line; only truly
5122
+ prose-heavy cells wrap, instead of dragging the table kilometers
5123
+ wide. */
5124
+ [class*="_tableScroll_"] th,
5125
+ [class*="_tableScroll_"] td {
5126
+ max-width: 480px !important;
5127
+ }
5128
+
5129
+ /* iOS WebKit fallback: upstream's own table { width: max-content } is what
5130
+ keeps the table at its natural width inside the scroll wrapper, but
5131
+ WebKit's max-content support on table boxes is incomplete — on a real
5132
+ iPhone the table fell back to shrink-to-fit and every column got
5133
+ crushed to its min-width (the "ra/nk" vertical-word-stack screenshot,
5134
+ 2026-08-18). Redeclare the natural-width intent with both width and
5135
+ min-width so whichever keyword the engine honors wins; the wrapper
5136
+ already scrolls the overflow. */
5137
+ [class*="_tableScroll_"] table {
5138
+ width: max-content !important;
5139
+ min-width: max-content !important;
5140
+ max-width: none !important;
5141
+ }
5142
+
5143
+ /* Turn-tail metadata (2026-08-18): the timing stats ("18:06 · 用时 29秒 ·
5144
+ 首 token 0.9秒 · 163 tok/s") are desktop-hover-revealed (official
5145
+ [data-time-hover-root] gate holds them at opacity 0 until :hover /
5146
+ :focus-within) — a phone never hovers, so they were simply invisible;
5147
+ and even ignoring that, the row is nowrap + overflow:hidden (plus this
5148
+ plugin's own legacy ellipsis clamp in layout.css.ts), which clipped the
5149
+ text. On phones: always visible, own full-width line under the action
5150
+ icons, wrapping freely, one type size down. !important + the extra
5151
+ [class$="_actions"] hop out-specifies both upstream and layout.css.ts. */
5152
+ /* Pin the tail row's width chain before allowing wrap: with wrap enabled
5153
+ the row's intrinsic (shrink-to-fit) width collapses to its widest
5154
+ single icon, taking the whole tail down to ~16px. */
5155
+ [data-chat-flow-kind="turn-tail"] [class$="_root"],
5156
+ [data-chat-flow-kind="turn-tail"] [class$="_actions"] {
5157
+ width: 100%;
5158
+ }
5159
+ [data-chat-flow-kind="turn-tail"] [class$="_actions"] {
5160
+ flex-wrap: wrap;
5161
+ row-gap: 2px;
5162
+ }
5163
+ /* Wide companion to the rule below: mirrors the upstream hover-gate
5164
+ selector shape directly, with no assumptions about the tail's DOM
5165
+ structure, so the stats can't stay invisible even if the phone's tail
5166
+ renders differently than desktop. */
5167
+ [data-time-hover-root] [class$="_timeEnd"],
5168
+ [data-time-hover-root] [class$="_timeStart"] {
5169
+ opacity: 1 !important;
5170
+ transition: none !important;
5171
+ }
5172
+ [data-chat-flow-kind="turn-tail"] [class$="_actions"] [class$="_timeEnd"] {
5173
+ opacity: 1 !important;
5174
+ flex-basis: 100% !important;
5175
+ white-space: normal !important;
5176
+ overflow: visible !important;
5177
+ text-overflow: clip !important;
5178
+ padding-left: 0 !important;
5179
+ font-size: 12px;
5180
+ line-height: 18px;
5181
+ }
5182
+ }
5183
+ `;
5184
+ };
4978
5185
  __modules["styles/index.js"] = function (require, module, exports) {
4979
5186
  "use strict";
4980
5187
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -4989,6 +5196,7 @@ const composer_css_ts_1 = require("./styles/composer.css.js");
4989
5196
  const info_css_ts_1 = require("./styles/info.css.js");
4990
5197
  const turn_fold_css_ts_1 = require("./styles/turn-fold.css.js");
4991
5198
  const chips_css_ts_1 = require("./styles/chips.css.js");
5199
+ const content_css_ts_1 = require("./styles/content.css.js");
4992
5200
  /**
4993
5201
  * All mobile styles, concatenated in the exact order of the original
4994
5202
  * single-file stylesheet (base → layout → compat → misc, where misc keeps
@@ -4999,10 +5207,11 @@ const chips_css_ts_1 = require("./styles/chips.css.js");
4999
5207
  * win that tie too), the turn-process fold (turn-fold, S8, which shares no
5000
5208
  * selector with any of them), and finally the chips row / settings entry /
5001
5209
  * portal fix (chips, S5) — all appended in this order so their <768px rules
5002
- * win ties against the shared <=1023px block. Injected as ONE
5003
- * <style data-plugin> tag do not reorder.
5210
+ * win ties against the shared <=1023px block, then the chat markdown
5211
+ * readability overrides (content, which shares no selector with any of
5212
+ * them). Injected as ONE <style data-plugin> tag — do not reorder.
5004
5213
  */
5005
- exports.MOBILE_CSS = [base_css_ts_1.BASE_CSS, layout_css_ts_1.LAYOUT_CSS, compat_css_ts_1.COMPAT_CSS, misc_css_ts_1.MISC_CSS, home_css_ts_1.HOME_CSS, header_css_ts_1.HEADER_CSS, composer_css_ts_1.COMPOSER_CSS, info_css_ts_1.INFO_CSS, turn_fold_css_ts_1.TURN_FOLD_CSS, chips_css_ts_1.CHIPS_CSS].join('\n');
5214
+ exports.MOBILE_CSS = [base_css_ts_1.BASE_CSS, layout_css_ts_1.LAYOUT_CSS, compat_css_ts_1.COMPAT_CSS, misc_css_ts_1.MISC_CSS, home_css_ts_1.HOME_CSS, header_css_ts_1.HEADER_CSS, composer_css_ts_1.COMPOSER_CSS, info_css_ts_1.INFO_CSS, turn_fold_css_ts_1.TURN_FOLD_CSS, chips_css_ts_1.CHIPS_CSS, content_css_ts_1.CONTENT_CSS].join('\n');
5006
5215
  };
5007
5216
  __modules["debug.js"] = function (require, module, exports) {
5008
5217
  "use strict";
@@ -6295,11 +6504,13 @@ function installTurnFold(ctx) {
6295
6504
  const running = flow.querySelector(':scope > [role="status"]') !== null;
6296
6505
  const groups = groupsOf(flow);
6297
6506
  const live = new Set();
6507
+ const liveItems = new Set();
6298
6508
  groups.forEach((group, index) => {
6299
6509
  if (group.items.length === 0)
6300
6510
  return;
6301
6511
  const open = expanded.has(group.key);
6302
6512
  for (const item of group.items) {
6513
+ liveItems.add(item);
6303
6514
  item.setAttribute(FOLD, '');
6304
6515
  if (open)
6305
6516
  item.setAttribute(OPEN, '');
@@ -6332,6 +6543,18 @@ function installTurnFold(ctx) {
6332
6543
  if (key === undefined || !live.has(key))
6333
6544
  chip.remove();
6334
6545
  }
6546
+ // Unmark elements that stopped being process items. Without this, an
6547
+ // assistant-step row folded whole while it streamed ONLY reasoning kept
6548
+ // its row-level fold after prose started arriving in the same DOM node
6549
+ // (React updates the row in place), hiding the streaming reply until a
6550
+ // completion re-render rebuilt the row — the "reply only appears when
6551
+ // the turn finishes" phone bug (2026-08-18).
6552
+ for (const stale of flow.querySelectorAll(`[${FOLD}]`)) {
6553
+ if (liveItems.has(stale))
6554
+ continue;
6555
+ stale.removeAttribute(FOLD);
6556
+ stale.removeAttribute(OPEN);
6557
+ }
6335
6558
  };
6336
6559
  const schedule = () => {
6337
6560
  // The locale subscription below lives outside attach/detach, so this
@@ -6405,6 +6628,198 @@ function installTurnFold(ctx) {
6405
6628
  }, 'dsh-mobile-nav: turn process fold');
6406
6629
  }
6407
6630
  };
6631
+ __modules["effects/welcome-notice.js"] = function (require, module, exports) {
6632
+ "use strict";
6633
+ Object.defineProperty(exports, "__esModule", { value: true });
6634
+ exports.installWelcomeNoticeOptOut = installWelcomeNoticeOptOut;
6635
+ /** Per-browser opt-out flag; localStorage because remote (non-loopback)
6636
+ connections never persist the acknowledgement host-side. */
6637
+ const OPTOUT_KEY = 'dsh-zen-remote:welcome-notice-optout';
6638
+ /** Localized aria-labels of the DSH first-run notice dialog
6639
+ (@deepseek-ai/dsh-client-ui-settings-models WELCOME_NOTICE_COPY). */
6640
+ const NOTICE_LABELS = ['内测声明', 'Internal Testing Notice'];
6641
+ /**
6642
+ * "内测声明" first-run notice, opt-out half (ALL widths — same user-directed
6643
+ * exception as the _previewBadge rule in styles/base.css.ts).
6644
+ *
6645
+ * Why not CSS: the notice dialog keeps document #root inert while MOUNTED and
6646
+ * only restores it on unmount (its acknowledge button). display:none hid the
6647
+ * dialog without unmounting it, which left the whole app permanently
6648
+ * unclickable on remote connections (2026-08-18 incident) — remote browsers
6649
+ * re-mount the notice on every page load because their acknowledgement is
6650
+ * memory-only (`connection.isLoopback ? "host" : "memory"` upstream).
6651
+ *
6652
+ * So the dialog now shows normally, plus one injected "不再弹出" button. The
6653
+ * user chooses: acknowledging via that button records the opt-out in this
6654
+ * browser's localStorage, and later mounts are auto-acknowledged by clicking
6655
+ * the dialog's own continue button — the component unmounts through its
6656
+ * normal path and #root's inert is properly restored.
6657
+ */
6658
+ function installWelcomeNoticeOptOut(ctx) {
6659
+ ctx.effect(() => {
6660
+ const handle = (dialog) => {
6661
+ // The notice's action row holds exactly one button ("继续"/"Continue").
6662
+ const continueBtn = dialog.querySelector('button');
6663
+ if (continueBtn === null)
6664
+ return;
6665
+ if (localStorage.getItem(OPTOUT_KEY) !== null) {
6666
+ continueBtn.click();
6667
+ return;
6668
+ }
6669
+ const row = continueBtn.parentElement;
6670
+ if (row === null || row.querySelector('.zen-welcome-optout') !== null)
6671
+ return;
6672
+ const optOut = document.createElement('button');
6673
+ optOut.type = 'button';
6674
+ optOut.className = 'zen-welcome-optout';
6675
+ optOut.textContent =
6676
+ dialog.getAttribute('aria-label') === '内测声明' ? '不再弹出' : "Don't show again";
6677
+ optOut.addEventListener('click', () => {
6678
+ localStorage.setItem(OPTOUT_KEY, '1');
6679
+ continueBtn.click();
6680
+ });
6681
+ row.insertBefore(optOut, continueBtn);
6682
+ };
6683
+ const scan = () => {
6684
+ for (const label of NOTICE_LABELS) {
6685
+ const dialog = document.querySelector(`[role="dialog"][aria-label="${label}"]`);
6686
+ if (dialog !== null)
6687
+ handle(dialog);
6688
+ }
6689
+ };
6690
+ // The dialog portals straight under <body>, so childList on body is
6691
+ // enough; scan() once first in case it mounted before this plugin loaded.
6692
+ scan();
6693
+ const observer = new MutationObserver(scan);
6694
+ observer.observe(document.body, { childList: true });
6695
+ return () => observer.disconnect();
6696
+ }, 'dsh-mobile-nav: welcome-notice opt-out');
6697
+ }
6698
+ };
6699
+ __modules["effects/keyboard-guard.js"] = function (require, module, exports) {
6700
+ "use strict";
6701
+ Object.defineProperty(exports, "__esModule", { value: true });
6702
+ exports.installKeyboardGuard = installKeyboardGuard;
6703
+ /** Phone breakpoint — same query every phone-only effect in this plugin uses. */
6704
+ const PHONE_QUERY = '(max-width: 767px)';
6705
+ /** The official composer slot wrapper (same marker composer.css.ts styles). */
6706
+ const COMPOSER = '[data-slot="conversation.composer.bar"]';
6707
+ /** A tap/keystroke older than this no longer explains a focus. */
6708
+ const INTENT_WINDOW_MS = 1000;
6709
+ /**
6710
+ * S9 — keep the phone keyboard down until the user asks for it.
6711
+ *
6712
+ * dsh-client-ui-conversation focuses the composer textarea on every
6713
+ * sessionId change (lib/client.js:3423 — el.focus({preventScroll:true}) in a
6714
+ * [locked, sessionId] effect). Sensible on desktop; on a phone it pops the
6715
+ * software keyboard over half the screen every time a session opens.
6716
+ *
6717
+ * Rule: focus on the composer textarea survives only when the user asked for
6718
+ * it — a tap on the textarea itself or typing on a hardware keyboard.
6719
+ * Anything else (session-open autofocus, push-deep-link opens, the refocus
6720
+ * side effects of the other composer buttons — slash-command toggle, attach,
6721
+ * send) is blurred.
6722
+ *
6723
+ * Two triggers, because one is not enough:
6724
+ * - focusin catches the autofocus the moment it happens;
6725
+ * - a body MutationObserver re-runs the check after transcript swaps
6726
+ * (opening a session re-renders the flow but may reuse the same textarea,
6727
+ * and a focus that landed before this plugin loaded never fired focusin
6728
+ * for us at all).
6729
+ * Once focus is user-granted it stays granted until the textarea blurs, so
6730
+ * the observer never yanks a keyboard the user opened (e.g. while the agent
6731
+ * streams and the user pauses typing).
6732
+ */
6733
+ function installKeyboardGuard(ctx) {
6734
+ ctx.effect(() => {
6735
+ const narrow = window.matchMedia(PHONE_QUERY);
6736
+ let lastIntent = 0;
6737
+ let granted = false;
6738
+ let observer = null;
6739
+ let frame = 0;
6740
+ const composerTextarea = (node) => node instanceof HTMLElement && node.tagName === 'TEXTAREA' && node.closest(COMPOSER) !== null
6741
+ ? node
6742
+ : null;
6743
+ const onPointerDown = (event) => {
6744
+ // Only the textarea itself grants the keyboard. A tap on any other
6745
+ // composer control (slash-command toggle, attach, model menu, send)
6746
+ // must not: several of them refocus the input as a side effect, which
6747
+ // popped the keyboard on every command-button tap.
6748
+ if (composerTextarea(event.target) !== null) {
6749
+ lastIntent = Date.now();
6750
+ granted = true;
6751
+ }
6752
+ };
6753
+ const onKeyDown = () => {
6754
+ lastIntent = Date.now();
6755
+ if (composerTextarea(document.activeElement) !== null)
6756
+ granted = true;
6757
+ };
6758
+ const sweep = () => {
6759
+ const el = composerTextarea(document.activeElement);
6760
+ if (el === null)
6761
+ return;
6762
+ if (granted || Date.now() - lastIntent < INTENT_WINDOW_MS) {
6763
+ granted = true;
6764
+ return;
6765
+ }
6766
+ el.blur();
6767
+ };
6768
+ const onFocusIn = (event) => {
6769
+ if (composerTextarea(event.target) === null)
6770
+ return;
6771
+ sweep();
6772
+ };
6773
+ const onFocusOut = (event) => {
6774
+ if (composerTextarea(event.target) === null)
6775
+ return;
6776
+ granted = false;
6777
+ };
6778
+ const schedule = () => {
6779
+ if (observer === null || frame !== 0)
6780
+ return;
6781
+ // setTimeout, not requestAnimationFrame: rAF pauses in hidden/
6782
+ // backgrounded pages, where the autofocus still happens — the sweep
6783
+ // must run there too or the keyboard pops when the page is foregrounded.
6784
+ frame = window.setTimeout(() => {
6785
+ frame = 0;
6786
+ sweep();
6787
+ }, 100);
6788
+ };
6789
+ const attach = () => {
6790
+ if (observer !== null)
6791
+ return;
6792
+ document.addEventListener('pointerdown', onPointerDown, { capture: true, passive: true });
6793
+ document.addEventListener('keydown', onKeyDown, { capture: true, passive: true });
6794
+ document.addEventListener('focusin', onFocusIn, true);
6795
+ document.addEventListener('focusout', onFocusOut, true);
6796
+ observer = new MutationObserver(schedule);
6797
+ observer.observe(document.body, { childList: true, subtree: true });
6798
+ sweep();
6799
+ };
6800
+ const detach = () => {
6801
+ document.removeEventListener('pointerdown', onPointerDown, true);
6802
+ document.removeEventListener('keydown', onKeyDown, true);
6803
+ document.removeEventListener('focusin', onFocusIn, true);
6804
+ document.removeEventListener('focusout', onFocusOut, true);
6805
+ observer?.disconnect();
6806
+ observer = null;
6807
+ if (frame !== 0)
6808
+ window.clearTimeout(frame);
6809
+ frame = 0;
6810
+ granted = false;
6811
+ };
6812
+ if (narrow.matches)
6813
+ attach();
6814
+ const onChange = (event) => (event.matches ? attach() : detach());
6815
+ narrow.addEventListener('change', onChange);
6816
+ return () => {
6817
+ narrow.removeEventListener('change', onChange);
6818
+ detach();
6819
+ };
6820
+ }, 'dsh-mobile-nav: composer keyboard guard');
6821
+ }
6822
+ };
6408
6823
  __modules["index.js"] = function (require, module, exports) {
6409
6824
  "use strict";
6410
6825
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -6426,6 +6841,8 @@ const aionui_compat_ts_1 = require("./effects/aionui-compat.js");
6426
6841
  const header_status_ts_1 = require("./effects/header-status.js");
6427
6842
  const gestures_ts_1 = require("./effects/gestures.js");
6428
6843
  const turn_fold_ts_1 = require("./effects/turn-fold.js");
6844
+ const welcome_notice_ts_1 = require("./effects/welcome-notice.js");
6845
+ const keyboard_guard_ts_1 = require("./effects/keyboard-guard.js");
6429
6846
  const locales_ts_1 = require("./locales.js");
6430
6847
  /** Required services (cordis fiber inject — the loader passes all module exports as an object plugin). */
6431
6848
  exports.inject = ['slots', 'layout', 'locale', 'sessionLogDownload', 'sessions', 'workspaces'];
@@ -6469,6 +6886,14 @@ function apply(ctx) {
6469
6886
  // effects/turn-fold.ts for why the keyed conversation.chat.node seat could
6470
6887
  // not carry this.
6471
6888
  (0, turn_fold_ts_1.installTurnFold)(ctx);
6889
+ // "内测声明" first-run notice: keep it visible (CSS-hiding it leaked the
6890
+ // dialog's #root inert lock — see effects/welcome-notice.ts) and offer a
6891
+ // per-browser "不再弹出" opt-out instead.
6892
+ (0, welcome_notice_ts_1.installWelcomeNoticeOptOut)(ctx);
6893
+ // S9: opening a session must not pop the phone keyboard — the official
6894
+ // composer autofocuses on every sessionId change; keep focus only when the
6895
+ // user tapped the composer (or typed) themselves.
6896
+ (0, keyboard_guard_ts_1.installKeyboardGuard)(ctx);
6472
6897
  // Page-stack store (apply world) — created before any registration so
6473
6898
  // every slot below (the phone home screen, the session header's back
6474
6899
  // button) shares the exact same handle/instance.