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.
- package/AGENTS.md +51 -0
- package/LICENSE +38 -0
- package/README.md +237 -0
- package/assets/home.png +0 -0
- package/assets/info.png +0 -0
- package/assets/pairing.png +0 -0
- package/assets/session.png +0 -0
- package/assets/sheet.png +0 -0
- package/cordis.patch.yml +17 -0
- package/cordis.patch.yml.example +16 -0
- package/docs/interface.md +200 -0
- package/docs/remote-access.en.md +335 -0
- package/docs/remote-access.md +422 -0
- package/dsh-push.mjs +213 -0
- package/lan-gate.mjs +78 -0
- package/lib/client.js +6615 -0
- package/lib/index.js +310 -0
- package/lib/index.js.map +1 -0
- package/lib/lan-gate-server.cjs +626 -0
- package/lib/types/client/MobileAttachButton.d.ts +25 -0
- package/lib/types/client/MobileAttachButton.d.ts.map +1 -0
- package/lib/types/client/MobileAttachChips.d.ts +32 -0
- package/lib/types/client/MobileAttachChips.d.ts.map +1 -0
- package/lib/types/client/MobileDrawerFooter.d.ts +21 -0
- package/lib/types/client/MobileDrawerFooter.d.ts.map +1 -0
- package/lib/types/client/MobileHome.d.ts +24 -0
- package/lib/types/client/MobileHome.d.ts.map +1 -0
- package/lib/types/client/MobileHomeChips.d.ts +76 -0
- package/lib/types/client/MobileHomeChips.d.ts.map +1 -0
- package/lib/types/client/MobileNavOverlay.d.ts +15 -0
- package/lib/types/client/MobileNavOverlay.d.ts.map +1 -0
- package/lib/types/client/MobileNavToggle.d.ts +18 -0
- package/lib/types/client/MobileNavToggle.d.ts.map +1 -0
- package/lib/types/client/MobileSessionHeader.d.ts +58 -0
- package/lib/types/client/MobileSessionHeader.d.ts.map +1 -0
- package/lib/types/client/MobileSessionInfo.d.ts +41 -0
- package/lib/types/client/MobileSessionInfo.d.ts.map +1 -0
- package/lib/types/client/attach-upload.d.ts +78 -0
- package/lib/types/client/attach-upload.d.ts.map +1 -0
- package/lib/types/client/chips-store.d.ts +13 -0
- package/lib/types/client/chips-store.d.ts.map +1 -0
- package/lib/types/client/debug.d.ts +9 -0
- package/lib/types/client/debug.d.ts.map +1 -0
- package/lib/types/client/effects/aionui-compat.d.ts +4 -0
- package/lib/types/client/effects/aionui-compat.d.ts.map +1 -0
- package/lib/types/client/effects/gestures.d.ts +7 -0
- package/lib/types/client/effects/gestures.d.ts.map +1 -0
- package/lib/types/client/effects/header-status.d.ts +15 -0
- package/lib/types/client/effects/header-status.d.ts.map +1 -0
- package/lib/types/client/effects/phone-chrome.d.ts +106 -0
- package/lib/types/client/effects/phone-chrome.d.ts.map +1 -0
- package/lib/types/client/effects/turn-fold.d.ts +25 -0
- package/lib/types/client/effects/turn-fold.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +18 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +58 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/nav-store.d.ts +73 -0
- package/lib/types/client/nav-store.d.ts.map +1 -0
- package/lib/types/client/session-dot.d.ts +12 -0
- package/lib/types/client/session-dot.d.ts.map +1 -0
- package/lib/types/client/styles/base.css.d.ts +2 -0
- package/lib/types/client/styles/base.css.d.ts.map +1 -0
- package/lib/types/client/styles/chips.css.d.ts +2 -0
- package/lib/types/client/styles/chips.css.d.ts.map +1 -0
- package/lib/types/client/styles/compat.css.d.ts +2 -0
- package/lib/types/client/styles/compat.css.d.ts.map +1 -0
- package/lib/types/client/styles/composer.css.d.ts +2 -0
- package/lib/types/client/styles/composer.css.d.ts.map +1 -0
- package/lib/types/client/styles/header.css.d.ts +2 -0
- package/lib/types/client/styles/header.css.d.ts.map +1 -0
- package/lib/types/client/styles/home.css.d.ts +2 -0
- package/lib/types/client/styles/home.css.d.ts.map +1 -0
- package/lib/types/client/styles/index.d.ts +15 -0
- package/lib/types/client/styles/index.d.ts.map +1 -0
- package/lib/types/client/styles/info.css.d.ts +2 -0
- package/lib/types/client/styles/info.css.d.ts.map +1 -0
- package/lib/types/client/styles/layout.css.d.ts +2 -0
- package/lib/types/client/styles/layout.css.d.ts.map +1 -0
- package/lib/types/client/styles/misc.css.d.ts +2 -0
- package/lib/types/client/styles/misc.css.d.ts.map +1 -0
- package/lib/types/client/styles/turn-fold.css.d.ts +2 -0
- package/lib/types/client/styles/turn-fold.css.d.ts.map +1 -0
- package/lib/types/index.d.ts +83 -0
- package/lib/types/index.d.ts.map +1 -0
- package/llms.txt +20 -0
- package/package.json +128 -0
- package/pwa/app.css +95 -0
- package/pwa/icons/icon-192.png +0 -0
- package/pwa/icons/icon-512.png +0 -0
- package/pwa/icons/icon-maskable-512.png +0 -0
- package/pwa/icons/icon-maskable.svg +1 -0
- package/pwa/icons/icon.svg +1 -0
- package/pwa/inject.js +190 -0
- package/pwa/manifest.json +29 -0
- package/pwa/offline.html +38 -0
- package/pwa/sw.js +176 -0
- package/pwa/touch-gestures.js +94 -0
- package/scripts/build-client.mjs +101 -0
- package/scripts/cdp-probe.mjs +64 -0
- package/scripts/check-attach-upload.mjs +99 -0
- package/scripts/check-sunk-viewport.mjs +33 -0
- package/scripts/check-upload-endpoint.mjs +168 -0
- package/src/client/MobileAttachButton.tsx +125 -0
- package/src/client/MobileAttachChips.tsx +85 -0
- package/src/client/MobileDrawerFooter.tsx +56 -0
- package/src/client/MobileHome.tsx +348 -0
- package/src/client/MobileHomeChips.tsx +426 -0
- package/src/client/MobileNavOverlay.tsx +279 -0
- package/src/client/MobileNavToggle.tsx +52 -0
- package/src/client/MobileSessionHeader.tsx +242 -0
- package/src/client/MobileSessionInfo.tsx +303 -0
- package/src/client/attach-upload.ts +140 -0
- package/src/client/chips-store.ts +41 -0
- package/src/client/debug.ts +253 -0
- package/src/client/effects/aionui-compat.ts +183 -0
- package/src/client/effects/gestures.ts +265 -0
- package/src/client/effects/header-status.ts +30 -0
- package/src/client/effects/phone-chrome.ts +265 -0
- package/src/client/effects/turn-fold.ts +258 -0
- package/src/client/index.tsx +227 -0
- package/src/client/locales.ts +109 -0
- package/src/client/nav-store.ts +78 -0
- package/src/client/session-dot.ts +17 -0
- package/src/client/styles/base.css.ts +167 -0
- package/src/client/styles/chips.css.ts +224 -0
- package/src/client/styles/compat.css.ts +681 -0
- package/src/client/styles/composer.css.ts +504 -0
- package/src/client/styles/header.css.ts +368 -0
- package/src/client/styles/home.css.ts +477 -0
- package/src/client/styles/index.ts +25 -0
- package/src/client/styles/info.css.ts +238 -0
- package/src/client/styles/layout.css.ts +501 -0
- package/src/client/styles/misc.css.ts +128 -0
- package/src/client/styles/turn-fold.css.ts +106 -0
- 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
|
+
}
|