dsh-zen-remote 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/AGENTS.md +51 -0
  2. package/LICENSE +38 -0
  3. package/README.md +237 -0
  4. package/assets/home.png +0 -0
  5. package/assets/info.png +0 -0
  6. package/assets/pairing.png +0 -0
  7. package/assets/session.png +0 -0
  8. package/assets/sheet.png +0 -0
  9. package/cordis.patch.yml +17 -0
  10. package/cordis.patch.yml.example +16 -0
  11. package/docs/interface.md +200 -0
  12. package/docs/remote-access.en.md +335 -0
  13. package/docs/remote-access.md +422 -0
  14. package/dsh-push.mjs +213 -0
  15. package/lan-gate.mjs +78 -0
  16. package/lib/client.js +6615 -0
  17. package/lib/index.js +310 -0
  18. package/lib/index.js.map +1 -0
  19. package/lib/lan-gate-server.cjs +626 -0
  20. package/lib/types/client/MobileAttachButton.d.ts +25 -0
  21. package/lib/types/client/MobileAttachButton.d.ts.map +1 -0
  22. package/lib/types/client/MobileAttachChips.d.ts +32 -0
  23. package/lib/types/client/MobileAttachChips.d.ts.map +1 -0
  24. package/lib/types/client/MobileDrawerFooter.d.ts +21 -0
  25. package/lib/types/client/MobileDrawerFooter.d.ts.map +1 -0
  26. package/lib/types/client/MobileHome.d.ts +24 -0
  27. package/lib/types/client/MobileHome.d.ts.map +1 -0
  28. package/lib/types/client/MobileHomeChips.d.ts +76 -0
  29. package/lib/types/client/MobileHomeChips.d.ts.map +1 -0
  30. package/lib/types/client/MobileNavOverlay.d.ts +15 -0
  31. package/lib/types/client/MobileNavOverlay.d.ts.map +1 -0
  32. package/lib/types/client/MobileNavToggle.d.ts +18 -0
  33. package/lib/types/client/MobileNavToggle.d.ts.map +1 -0
  34. package/lib/types/client/MobileSessionHeader.d.ts +58 -0
  35. package/lib/types/client/MobileSessionHeader.d.ts.map +1 -0
  36. package/lib/types/client/MobileSessionInfo.d.ts +41 -0
  37. package/lib/types/client/MobileSessionInfo.d.ts.map +1 -0
  38. package/lib/types/client/attach-upload.d.ts +78 -0
  39. package/lib/types/client/attach-upload.d.ts.map +1 -0
  40. package/lib/types/client/chips-store.d.ts +13 -0
  41. package/lib/types/client/chips-store.d.ts.map +1 -0
  42. package/lib/types/client/debug.d.ts +9 -0
  43. package/lib/types/client/debug.d.ts.map +1 -0
  44. package/lib/types/client/effects/aionui-compat.d.ts +4 -0
  45. package/lib/types/client/effects/aionui-compat.d.ts.map +1 -0
  46. package/lib/types/client/effects/gestures.d.ts +7 -0
  47. package/lib/types/client/effects/gestures.d.ts.map +1 -0
  48. package/lib/types/client/effects/header-status.d.ts +15 -0
  49. package/lib/types/client/effects/header-status.d.ts.map +1 -0
  50. package/lib/types/client/effects/phone-chrome.d.ts +106 -0
  51. package/lib/types/client/effects/phone-chrome.d.ts.map +1 -0
  52. package/lib/types/client/effects/turn-fold.d.ts +25 -0
  53. package/lib/types/client/effects/turn-fold.d.ts.map +1 -0
  54. package/lib/types/client/index.d.ts +18 -0
  55. package/lib/types/client/index.d.ts.map +1 -0
  56. package/lib/types/client/locales.d.ts +58 -0
  57. package/lib/types/client/locales.d.ts.map +1 -0
  58. package/lib/types/client/nav-store.d.ts +73 -0
  59. package/lib/types/client/nav-store.d.ts.map +1 -0
  60. package/lib/types/client/session-dot.d.ts +12 -0
  61. package/lib/types/client/session-dot.d.ts.map +1 -0
  62. package/lib/types/client/styles/base.css.d.ts +2 -0
  63. package/lib/types/client/styles/base.css.d.ts.map +1 -0
  64. package/lib/types/client/styles/chips.css.d.ts +2 -0
  65. package/lib/types/client/styles/chips.css.d.ts.map +1 -0
  66. package/lib/types/client/styles/compat.css.d.ts +2 -0
  67. package/lib/types/client/styles/compat.css.d.ts.map +1 -0
  68. package/lib/types/client/styles/composer.css.d.ts +2 -0
  69. package/lib/types/client/styles/composer.css.d.ts.map +1 -0
  70. package/lib/types/client/styles/header.css.d.ts +2 -0
  71. package/lib/types/client/styles/header.css.d.ts.map +1 -0
  72. package/lib/types/client/styles/home.css.d.ts +2 -0
  73. package/lib/types/client/styles/home.css.d.ts.map +1 -0
  74. package/lib/types/client/styles/index.d.ts +15 -0
  75. package/lib/types/client/styles/index.d.ts.map +1 -0
  76. package/lib/types/client/styles/info.css.d.ts +2 -0
  77. package/lib/types/client/styles/info.css.d.ts.map +1 -0
  78. package/lib/types/client/styles/layout.css.d.ts +2 -0
  79. package/lib/types/client/styles/layout.css.d.ts.map +1 -0
  80. package/lib/types/client/styles/misc.css.d.ts +2 -0
  81. package/lib/types/client/styles/misc.css.d.ts.map +1 -0
  82. package/lib/types/client/styles/turn-fold.css.d.ts +2 -0
  83. package/lib/types/client/styles/turn-fold.css.d.ts.map +1 -0
  84. package/lib/types/index.d.ts +83 -0
  85. package/lib/types/index.d.ts.map +1 -0
  86. package/llms.txt +20 -0
  87. package/package.json +128 -0
  88. package/pwa/app.css +95 -0
  89. package/pwa/icons/icon-192.png +0 -0
  90. package/pwa/icons/icon-512.png +0 -0
  91. package/pwa/icons/icon-maskable-512.png +0 -0
  92. package/pwa/icons/icon-maskable.svg +1 -0
  93. package/pwa/icons/icon.svg +1 -0
  94. package/pwa/inject.js +190 -0
  95. package/pwa/manifest.json +29 -0
  96. package/pwa/offline.html +38 -0
  97. package/pwa/sw.js +176 -0
  98. package/pwa/touch-gestures.js +94 -0
  99. package/scripts/build-client.mjs +101 -0
  100. package/scripts/cdp-probe.mjs +64 -0
  101. package/scripts/check-attach-upload.mjs +99 -0
  102. package/scripts/check-sunk-viewport.mjs +33 -0
  103. package/scripts/check-upload-endpoint.mjs +168 -0
  104. package/src/client/MobileAttachButton.tsx +125 -0
  105. package/src/client/MobileAttachChips.tsx +85 -0
  106. package/src/client/MobileDrawerFooter.tsx +56 -0
  107. package/src/client/MobileHome.tsx +348 -0
  108. package/src/client/MobileHomeChips.tsx +426 -0
  109. package/src/client/MobileNavOverlay.tsx +279 -0
  110. package/src/client/MobileNavToggle.tsx +52 -0
  111. package/src/client/MobileSessionHeader.tsx +242 -0
  112. package/src/client/MobileSessionInfo.tsx +303 -0
  113. package/src/client/attach-upload.ts +140 -0
  114. package/src/client/chips-store.ts +41 -0
  115. package/src/client/debug.ts +253 -0
  116. package/src/client/effects/aionui-compat.ts +183 -0
  117. package/src/client/effects/gestures.ts +265 -0
  118. package/src/client/effects/header-status.ts +30 -0
  119. package/src/client/effects/phone-chrome.ts +265 -0
  120. package/src/client/effects/turn-fold.ts +258 -0
  121. package/src/client/index.tsx +227 -0
  122. package/src/client/locales.ts +109 -0
  123. package/src/client/nav-store.ts +78 -0
  124. package/src/client/session-dot.ts +17 -0
  125. package/src/client/styles/base.css.ts +167 -0
  126. package/src/client/styles/chips.css.ts +224 -0
  127. package/src/client/styles/compat.css.ts +681 -0
  128. package/src/client/styles/composer.css.ts +504 -0
  129. package/src/client/styles/header.css.ts +368 -0
  130. package/src/client/styles/home.css.ts +477 -0
  131. package/src/client/styles/index.ts +25 -0
  132. package/src/client/styles/info.css.ts +238 -0
  133. package/src/client/styles/layout.css.ts +501 -0
  134. package/src/client/styles/misc.css.ts +128 -0
  135. package/src/client/styles/turn-fold.css.ts +106 -0
  136. package/src/index.ts +343 -0
@@ -0,0 +1,253 @@
1
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
2
+ /**
3
+ * Debug badge — ?mobile-nav-debug=1
4
+ * Renders a live state overlay (URL, viewport, media queries, shell chrome,
5
+ * aionui columns, genui cards, captured errors) so a phone-side repro can be
6
+ * diagnosed without guessing. No-op unless the query param is present.
7
+ */
8
+ export function installDebugBadge(ctx: ClientContext): void {
9
+ /**
10
+ * Fake safe-area inset — ?mobile-nav-inset=54 or ?mobile-nav-inset=54,34
11
+ * env(safe-area-inset-*) is hard 0 on every desktop browser (CDP included),
12
+ * so notch bugs are invisible until a real phone loads the page — the S2.1
13
+ * hotfix exists because of exactly that. Overriding --mnav-sat / --mnav-sab
14
+ * (see styles/base.css.ts) on the root element reproduces a notch anywhere.
15
+ * No param = the variables keep their env() values = zero behaviour change.
16
+ *
17
+ * One value fakes the TOP inset only (the historic behaviour — every S1-S3
18
+ * verification recipe passes `=54`, and they must keep meaning what they
19
+ * did). A second, comma-separated value fakes the BOTTOM inset too:
20
+ * `=54,34` is the iPhone notch + home-indicator pair. The bottom half was
21
+ * added in S4.1 because the home-bar padding it guards (composer clearance
22
+ * over the indicator) is otherwise untestable off-device for exactly the
23
+ * same reason the top half exists.
24
+ */
25
+ ctx.effect(() => {
26
+ const raw = new URLSearchParams(location.search).get('mobile-nav-inset')
27
+ if (raw === null) return () => {}
28
+ const [topRaw, bottomRaw] = raw.split(',')
29
+ const top = Number(topRaw)
30
+ if (!Number.isFinite(top)) return () => {}
31
+ const root = document.documentElement
32
+ root.style.setProperty('--mnav-sat', `${top}px`)
33
+ const bottom = bottomRaw === undefined ? Number.NaN : Number(bottomRaw)
34
+ if (Number.isFinite(bottom)) root.style.setProperty('--mnav-sab', `${bottom}px`)
35
+ return () => {
36
+ root.style.removeProperty('--mnav-sat')
37
+ root.style.removeProperty('--mnav-sab')
38
+ }
39
+ }, 'dsh-mobile-nav: fake safe-area inset')
40
+
41
+ const DEBUG_KEY = 'dsh-mobile-nav.debug'
42
+
43
+ /**
44
+ * No-URL toggle — 5 quick taps on the home screen's top bar.
45
+ * A standalone PWA has no address bar, and the paired-device cookie lives
46
+ * in the PWA's own jar (Safari hits the pairing wall instead), so the
47
+ * ?mobile-nav-debug=1 param is unreachable exactly where the badge is
48
+ * needed most.
49
+ *
50
+ * The target is deliberately NOT [data-mobile-nav="ws-switch"] any more:
51
+ * that button opens the workspace sheet on the FIRST tap, so taps 2..5
52
+ * land on the sheet backdrop, `closest()` misses, and the counter never
53
+ * reaches five — the original binding only ever fired by luck, and it left
54
+ * a user stuck inside debug mode with no way back out. The logo img has no
55
+ * click handler at all; the home-top container minus the switch button is
56
+ * the fallback for when the icon 404s and the img is not rendered.
57
+ */
58
+ ctx.effect(() => {
59
+ let taps = 0
60
+ let firstTap = 0
61
+ const onTap = (event: Event) => {
62
+ if (!(event.target instanceof Element)) return
63
+ const inTopBar = event.target.closest('[data-mobile-nav="home-logo"]') !== null
64
+ || (event.target.closest('[data-mobile-nav="home-top"]') !== null
65
+ && event.target.closest('[data-mobile-nav="ws-switch"]') === null)
66
+ if (!inTopBar) return
67
+ const now = Date.now()
68
+ if (now - firstTap > 2500) { taps = 0; firstTap = now }
69
+ taps += 1
70
+ if (taps >= 5) {
71
+ if (localStorage.getItem(DEBUG_KEY) === '1') localStorage.removeItem(DEBUG_KEY)
72
+ else localStorage.setItem(DEBUG_KEY, '1')
73
+ location.reload()
74
+ }
75
+ }
76
+ document.addEventListener('click', onTap, true)
77
+ return () => document.removeEventListener('click', onTap, true)
78
+ }, 'dsh-mobile-nav: debug badge tap toggle')
79
+
80
+ ctx.effect(() => {
81
+ const enabled = new URLSearchParams(location.search).has('mobile-nav-debug')
82
+ || localStorage.getItem('dsh-mobile-nav.debug') === '1'
83
+ if (!enabled) return () => {}
84
+ const errors: string[] = []
85
+ const onError = (event: ErrorEvent) => errors.push(`ERR ${event.message.slice(0, 120)}`)
86
+ const onRejection = (event: PromiseRejectionEvent) => errors.push(`REJ ${String(event.reason).slice(0, 120)}`)
87
+ window.addEventListener('error', onError)
88
+ window.addEventListener('unhandledrejection', onRejection)
89
+
90
+ /* Probe element: reads the REAL env(safe-area-inset-*) as computed padding
91
+ — the --mnav-* vars can be faked by the inset param, this cannot. */
92
+ const probe = document.createElement('div')
93
+ probe.style.cssText =
94
+ 'position:fixed;visibility:hidden;pointer-events:none;' +
95
+ 'padding-top:env(safe-area-inset-top,0px);padding-bottom:env(safe-area-inset-bottom,0px)'
96
+ document.body.appendChild(probe)
97
+
98
+ const badge = document.createElement('div')
99
+ badge.style.cssText = [
100
+ /* Sits BELOW the notch on purpose: at the old flat top:40px the badge
101
+ painted into the status-bar strip and covered the session header,
102
+ which read on a screenshot as "the header ran under the notch in
103
+ debug mode" — a measurement artefact that cost a whole round of
104
+ diagnosis. env() here, not --mnav-sat: the badge must stay put even
105
+ when the inset param is faking that variable. */
106
+ 'position:fixed', 'top:calc(env(safe-area-inset-top, 0px) + 8px)', 'right:6px', 'z-index:2147483000',
107
+ 'background:rgba(0,0,0,.82)', 'color:#fff', 'font:11px/1.5 ui-monospace,monospace',
108
+ 'padding:8px 10px', 'border-radius:8px', 'max-width:94vw', 'max-height:70vh',
109
+ /* pointer-events must be auto for the escape hatch below — with `none`
110
+ the badge could only be dismissed by editing localStorage, which is
111
+ not reachable from a standalone PWA. touch-action/user-select keep
112
+ the double tap from being read as zoom or text selection. */
113
+ 'overflow:auto', 'white-space:pre-wrap', 'pointer-events:auto',
114
+ 'touch-action:manipulation', '-webkit-user-select:none', 'user-select:none',
115
+ ].join(';')
116
+ /* Escape hatch: two taps on the badge within 600ms turn debug off. The
117
+ enable gesture lives on the home screen, which a user deep inside a
118
+ session cannot reach without first getting out of the way of the badge
119
+ — so the off switch belongs on the badge itself. */
120
+ /* The full read-out is tall enough to cover the session list, and with
121
+ pointer-events:auto (required for the escape hatch) it swallowed every
122
+ tap under it — the user could not open a session with debug on. The
123
+ badge therefore starts COLLAPSED (a one-line pill showing vh, the
124
+ number that matters for the viewport-shrink bug) and only expands on
125
+ demand. Single tap toggles collapsed/expanded (deferred 620ms so it
126
+ can be distinguished from the exit gesture); double tap still exits. */
127
+ let lastTap = 0
128
+ let collapsed = true
129
+ let singleTapTimer: ReturnType<typeof setTimeout> | undefined
130
+ const onBadgeTap = (event: Event) => {
131
+ event.preventDefault()
132
+ event.stopPropagation()
133
+ const now = Date.now()
134
+ if (now - lastTap < 600) {
135
+ if (singleTapTimer !== undefined) clearTimeout(singleTapTimer)
136
+ localStorage.removeItem(DEBUG_KEY)
137
+ const url = new URL(location.href)
138
+ url.searchParams.delete('mobile-nav-debug')
139
+ location.replace(url.toString())
140
+ return
141
+ }
142
+ lastTap = now
143
+ singleTapTimer = setTimeout(() => {
144
+ collapsed = !collapsed
145
+ paint()
146
+ }, 620)
147
+ }
148
+ badge.addEventListener('click', onBadgeTap)
149
+
150
+ const read = (): string => {
151
+ const q = (sel: string) => !!document.querySelector(sel)
152
+ const vis = (sel: string) => {
153
+ const el = document.querySelector<HTMLElement>(sel)
154
+ return el === null ? 'absent' : getComputedStyle(el).visibility
155
+ }
156
+ const frame = document.querySelector<HTMLElement>('[data-mobile-nav="frame"]')
157
+ if (collapsed) return `DBG vh ${innerHeight}/${screen.height} ▸ 单击展开 双击关闭`
158
+ return [
159
+ '▾ 单击收起 · 双击关闭 debug',
160
+ `URL ${location.pathname}${location.search}`,
161
+ `W ${innerWidth} x ${innerHeight} dpr ${devicePixelRatio}`,
162
+ `mq≤1023 ${matchMedia('(max-width: 1023px)').matches} mq≥1024 ${matchMedia('(min-width: 1024px)').matches}`,
163
+ `css ${q('style[data-plugin-css*="mobile"]')} frame ${!!frame}`,
164
+ `previewCol ${vis('[data-aionui-preview-col]')} explorerCol ${vis('[data-aionui-explorer-col]')}`,
165
+ `previewOpen ${frame?.hasAttribute('data-aionui-preview-open') ?? '?'} explorerOpen ${frame?.hasAttribute('data-aionui-explorer-open') ?? '?'} previewFull ${frame?.hasAttribute('data-mobile-preview-full') ?? '?'}`,
166
+ `header ${vis('[data-phase] header')} h${Math.round(document.querySelector('[data-phase] header')?.getBoundingClientRect().height ?? 0)} composer ${q('textarea')}`,
167
+ `sat ${getComputedStyle(document.documentElement).getPropertyValue('--mnav-sat').trim() || '?'} sab ${getComputedStyle(document.documentElement).getPropertyValue('--mnav-sab').trim() || '?'}`,
168
+ (() => {
169
+ /* Bottom-gap forensics: real env values + who ends where. */
170
+ const ps = getComputedStyle(probe)
171
+ const bottomOf = (sel: string): string => {
172
+ const el = document.querySelector(sel)
173
+ return el === null ? '—' : String(Math.round(el.getBoundingClientRect().bottom))
174
+ }
175
+ /* Composer ledger: bottom/gap-to-viewport for every layer between
176
+ the textarea and the screen edge, so a "white band under the
177
+ composer" screenshot says WHICH layer owns it instead of only
178
+ where the textarea ends. Anchors per AGENTS.md: InputBar root is
179
+ the padding carrier, _card is the rounded visual box, _composerSeat
180
+ is the sticky seat, _scrollBody is the real scroll container. */
181
+ const pair = (sel: string): string => {
182
+ const el = document.querySelector(sel)
183
+ if (el === null) return '—'
184
+ const b = el.getBoundingClientRect().bottom
185
+ return `${Math.round(b)}/${Math.round(innerHeight - b)}`
186
+ }
187
+ const barRoot = document.querySelector('[data-slot="conversation.composer.bar"] > [class$="_root"]')
188
+ /* Top-edge forensics: rect.top of the surfaces that consume
189
+ --mnav-sat. These separate the two competing readings of the
190
+ device numbers — a viewport SUNK below the status bar (origin at
191
+ screen y=59, so a working sat padding puts the header at 118 and
192
+ the top gap is doubled) versus a viewport anchored at screen y=0
193
+ that is merely 59px short at the bottom (header at 59, top
194
+ correct, the missing 59 showing as a band under the composer).
195
+ innerHeight alone cannot tell them apart; the viewport ORIGIN can,
196
+ hence screenY / visualViewport offsets / the edge markers. */
197
+ const topOf = (sel: string): string => {
198
+ const el = document.querySelector(sel)
199
+ return el === null ? '—' : String(Math.round(el.getBoundingClientRect().top))
200
+ }
201
+ const framePadTop = frame === null ? '—' : getComputedStyle(frame).paddingTop
202
+ const vv = window.visualViewport
203
+ return [
204
+ `env sat ${ps.paddingTop} sab ${ps.paddingBottom}`,
205
+ `screen ${screen.width}x${screen.height} avail ${screen.availWidth}x${screen.availHeight}`,
206
+ `vh ${innerHeight} vv ${Math.round(vv?.height ?? -1)} outH ${outerHeight}`,
207
+ `scrXY ${screenX},${screenY} vvOff ${Math.round(vv?.offsetTop ?? -1)} vvPage ${Math.round(vv?.pageTop ?? -1)} vvScale ${vv?.scale ?? '?'}`,
208
+ /* The ENFORCED state, not a re-derivation: installSunkInset()
209
+ stamps data-mnav-sunk, and its absence means the override is
210
+ not running at all (the inset param owns the variables). sabOvr
211
+ is the receipt — the inline value it wrote, or (env) when the
212
+ normal compensation is in force. */
213
+ `sunk? ${document.documentElement.dataset.mnavSunk ?? 'n/a'} standalone ${matchMedia('(display-mode: standalone)').matches} sabOvr ${document.documentElement.style.getPropertyValue('--mnav-sab') || '(env)'}`,
214
+ `frameTop ${topOf('[data-mobile-nav="frame"]')} padTop ${framePadTop} headerTop ${topOf('[data-phase] header')} homeTopY ${topOf('[data-mobile-nav="home-top"]')}`,
215
+ `botFrame ${bottomOf('[data-mobile-nav="frame"]')} composer ${bottomOf('[data-phase] textarea')}`,
216
+ `taB ${pair('[data-phase] textarea')} cardB ${pair('[data-slot="conversation.composer.bar"] [class$="_card"]')}`,
217
+ `barB ${pair('[data-slot="conversation.composer.bar"] > [class$="_root"]')} seatB ${pair('[class$="_composerSeat"]')} scrollB ${pair('[class$="_scrollBody"]')}`,
218
+ `sabPad ${barRoot === null ? '—' : getComputedStyle(barRoot).paddingBottom}`,
219
+ `botList ${bottomOf('[data-mobile-nav="home-list"]')} fab ${bottomOf('[data-mobile-nav="home-fab"]')}`,
220
+ `cssLen ${document.querySelector('style[data-plugin-css*="mobile"]')?.textContent?.length ?? 0}`,
221
+ ].join('\n')
222
+ })(),
223
+ `genui cards ${document.querySelectorAll('[data-genui]').length} panel ${q('[data-genui-panel]')}`,
224
+ `phase ${document.querySelector('[data-phase]')?.getAttribute('data-phase') ?? '?'}`,
225
+ `errs ${errors.slice(-5).join(' | ') || 'none'}`,
226
+ ].join('\n')
227
+ }
228
+ /* Feedback-loop guards: painting the badge mutates the body subtree the
229
+ observer watches — without these two checks the observer retriggers
230
+ itself forever and freezes the page (the badge was rarely actually
231
+ enabled before, which is how this survived). */
232
+ const paint = (): void => {
233
+ const next = read()
234
+ if (next !== badge.textContent) badge.textContent = next
235
+ }
236
+ paint()
237
+ const observer = new MutationObserver((mutations) => {
238
+ if (mutations.every((m) => m.target === badge || badge.contains(m.target) || m.target === probe)) return
239
+ paint()
240
+ })
241
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true })
242
+ const timer = setInterval(paint, 1500)
243
+ document.body.appendChild(badge)
244
+ return () => {
245
+ window.removeEventListener('error', onError)
246
+ window.removeEventListener('unhandledrejection', onRejection)
247
+ observer.disconnect()
248
+ clearInterval(timer)
249
+ probe.remove()
250
+ badge.remove()
251
+ }
252
+ }, 'dsh-mobile-nav: debug badge')
253
+ }
@@ -0,0 +1,183 @@
1
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
2
+
3
+ /** dsh-web-ui 兼容:explorer / preview 列的显隐标记与升起动画(同域同机制,合并一处)。 */
4
+ export function installAionuiCompat(ctx: ClientContext): void {
5
+ // dsh-web-ui compatibility: the aionui explorer column would render as a
6
+ // sheet over the whole mobile UI whenever its (persisted) expanded state
7
+ // is active — including right after a reload, with no way out (the
8
+ // suite's floating expand button only exists while collapsed). Instead
9
+ // of fighting the suite's store timing, the mobile stylesheet keeps the
10
+ // explorer column hidden by default and the header's Files action (plus
11
+ // the drawer footer entry) opens it via the `data-aionui-explorer-open`
12
+ // marker on the frame. This effect just clears that marker when the
13
+ // sheet's own collapse chevron is tapped, so closing is symmetric with
14
+ // opening.
15
+ ctx.effect(() => {
16
+ // Arm on the CURRENT width and re-arm on every width change: the guard
17
+ // used to run once at apply time, so a wide→narrow transition (desktop
18
+ // resize, tablet split view) left the markers dead and the explorer /
19
+ // preview sheets could neither open nor close properly.
20
+ const narrow = window.matchMedia('(max-width: 1023px)')
21
+ let cleanup: (() => void) | undefined
22
+ const install = (): void => {
23
+ cleanup?.()
24
+ if (!narrow.matches) {
25
+ cleanup = undefined
26
+ return
27
+ }
28
+ const onChevronClick = (event: MouseEvent) => {
29
+ const target = event.target as HTMLElement | null
30
+ if (target === null || !target.closest('.aionui-collapse-chevron')) return
31
+ document.querySelector('[data-mobile-nav="frame"]')?.removeAttribute('data-aionui-explorer-open')
32
+ }
33
+ document.addEventListener('click', onChevronClick, true)
34
+ cleanup = () => document.removeEventListener('click', onChevronClick, true)
35
+ }
36
+ install()
37
+ narrow.addEventListener('change', install)
38
+ return () => {
39
+ narrow.removeEventListener('change', install)
40
+ cleanup?.()
41
+ }
42
+ }, 'dsh-mobile-nav: aionui explorer close marker')
43
+
44
+ // dsh-web-ui compatibility: the aionui preview column persists its open
45
+ // tabs in localStorage and restores them on load, which would pop the
46
+ // preview sheet over the fresh UI after a reload. Gate it like the
47
+ // explorer: the stylesheet keeps the column hidden unless the frame
48
+ // carries `data-aionui-preview-open`; this effect sets that marker when
49
+ // the user actually taps a file row in the explorer sheet, and clears it
50
+ // whenever the suite hides the column again (collapse chevron / tab
51
+ // close), so a restored-but-unwanted sheet never appears.
52
+ ctx.effect(() => {
53
+ // Arm on the CURRENT width and re-arm on every width change (see the
54
+ // explorer marker effect for why).
55
+ const narrow = window.matchMedia('(max-width: 1023px)')
56
+ let cleanup: (() => void) | undefined
57
+ const install = (): void => {
58
+ cleanup?.()
59
+ if (!narrow.matches) {
60
+ cleanup = undefined
61
+ return
62
+ }
63
+ const frame = (): HTMLElement | null => document.querySelector('[data-mobile-nav="frame"]')
64
+ // Closing the preview sheet also drops the fullscreen marker, so the
65
+ // next preview starts in the sheet layout again.
66
+ const closePreview = (): void => {
67
+ frame()?.removeAttribute('data-aionui-preview-open')
68
+ frame()?.removeAttribute('data-mobile-preview-full')
69
+ }
70
+ const onTap = (event: MouseEvent) => {
71
+ const target = event.target as HTMLElement | null
72
+ if (target === null) return
73
+ const row = target.closest('[data-aionui-explorer-col] [class*="_treeRow"]')
74
+ if (row === null) return
75
+ // Only FILE rows open the preview sheet. Directory rows toggle
76
+ // expansion and must not pop the (possibly stale, restored-from-
77
+ // localStorage) preview tab over the tree. Substring matching:
78
+ // the suite's hashed classes carry a hash prefix, so the exact-token
79
+ // `class~=` form never matches and the trailing `$=` form misses
80
+ // selected rows (`_treeRowSelected`) and open arrows
81
+ // (`_treeArrowOpen`) — regressions of issue #8. The arrow gate must
82
+ // additionally exclude the leaf marker: FILE rows render a
83
+ // `_treeArrowEmpty` span whose class still contains the `_treeArrow`
84
+ // substring, so a bare substring match would treat every row as a
85
+ // directory and no preview would ever open.
86
+ if (row.querySelector('[class*="_treeArrow"]:not([class*="_treeArrowEmpty"])') !== null) return
87
+ frame()?.setAttribute('data-aionui-preview-open', '')
88
+ }
89
+ // The preview sheet's own collapse button (the two inward arrows in the
90
+ // tab bar) closes the AionUI store, but on mobile the suite's layout sync
91
+ // can be skipped while its shell-track mirror is not ready yet — in that
92
+ // case the inline visibility never flips to hidden and the visibility
93
+ // watcher below would never clear our marker. Clear it directly on the
94
+ // button click so the sheet always closes regardless of the suite's sync.
95
+ const onCollapse = (event: MouseEvent) => {
96
+ const target = event.target as HTMLElement | null
97
+ if (target === null) return
98
+ if (target.closest('[data-aionui-preview-col] [class$="_panelCollapse"]') !== null) {
99
+ closePreview()
100
+ }
101
+ }
102
+ const sync = (): void => {
103
+ const pv = document.querySelector<HTMLElement>('[data-aionui-preview-col]')
104
+ if (pv === null) return
105
+ // Read the suite's inline visibility, not the computed value: while the
106
+ // `data-aionui-preview-open` marker is present our stylesheet forces the
107
+ // sheet visible with !important, so getComputedStyle() would never report
108
+ // hidden and the marker would never be cleared.
109
+ if (pv.style.visibility === 'hidden') closePreview()
110
+ }
111
+ document.addEventListener('click', onTap, true)
112
+ document.addEventListener('click', onCollapse, true)
113
+ const observer = new MutationObserver(sync)
114
+ observer.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['style'] })
115
+ sync()
116
+ cleanup = () => {
117
+ document.removeEventListener('click', onTap, true)
118
+ document.removeEventListener('click', onCollapse, true)
119
+ observer.disconnect()
120
+ }
121
+ }
122
+ install()
123
+ narrow.addEventListener('change', install)
124
+ return () => {
125
+ narrow.removeEventListener('change', install)
126
+ cleanup?.()
127
+ }
128
+ }, 'dsh-mobile-nav: preview sheet open marker')
129
+
130
+ // The dsh-web-ui explorer / preview columns toggle via `visibility`
131
+ // (their inline style), which never restarts a CSS animation — so the
132
+ // sheets would only animate on first mount. Replay the rise animation
133
+ // with the Web Animations API each time a column turns visible, then
134
+ // leave the resting state to the stylesheet.
135
+ ctx.effect(() => {
136
+ // Arm on the CURRENT width and re-arm on every width change (see the
137
+ // explorer marker effect for why).
138
+ const narrow = window.matchMedia('(max-width: 1023px)')
139
+ let cleanup: (() => void) | undefined
140
+ const install = (): void => {
141
+ cleanup?.()
142
+ if (!narrow.matches) {
143
+ cleanup = undefined
144
+ return
145
+ }
146
+ const cols = ['[data-aionui-explorer-col]', '[data-aionui-preview-col]']
147
+ const seen = new Map<string, boolean>()
148
+ const play = (el: Element): void => {
149
+ el.animate(
150
+ [
151
+ { opacity: 0, transform: 'translateY(28px)' },
152
+ { opacity: 1, transform: 'none' },
153
+ ],
154
+ { duration: 280, easing: 'cubic-bezier(.16, 1, .3, 1)', fill: 'backwards' },
155
+ )
156
+ }
157
+ const check = (): void => {
158
+ for (const sel of cols) {
159
+ const el = document.querySelector(sel)
160
+ if (el === null) continue
161
+ const visible = getComputedStyle(el).visibility === 'visible'
162
+ const prev = seen.get(sel) ?? false
163
+ if (visible && !prev) play(el)
164
+ seen.set(sel, visible)
165
+ }
166
+ }
167
+ const observer = new MutationObserver(check)
168
+ // Visibility flips come through inline style mutations (suite) or the
169
+ // explorer-open marker on the frame; class changes are watched too.
170
+ observer.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['style', 'class', 'data-aionui-explorer-open'] })
171
+ check()
172
+ cleanup = () => {
173
+ observer.disconnect()
174
+ }
175
+ }
176
+ install()
177
+ narrow.addEventListener('change', install)
178
+ return () => {
179
+ narrow.removeEventListener('change', install)
180
+ cleanup?.()
181
+ }
182
+ }, 'dsh-mobile-nav: sheet rise animation replay')
183
+ }