dsh-pocket-relay 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/LICENSE +344 -0
- package/README.en.md +219 -0
- package/README.md +258 -0
- package/bin/dsh-pocket-relay.mjs +216 -0
- package/client/api.js +88 -0
- package/client/build.mjs +47 -0
- package/client/client.js +3238 -0
- package/client/index.jsx +979 -0
- package/client/mobile/LICENSE.dsh-web-mobile +4 -0
- package/client/mobile/MobileDrawerFooter.tsx +56 -0
- package/client/mobile/MobileNavOverlay.tsx +290 -0
- package/client/mobile/MobileNavToggle.tsx +52 -0
- package/client/mobile/fileGuard.ts +200 -0
- package/client/mobile/layout-mode.mjs +46 -0
- package/client/mobile/locales.ts +23 -0
- package/client/mobile/mobile-apply.tsx +378 -0
- package/client/mobile/mobile.css.ts +1005 -0
- package/client/mobile/nav-targets.mjs +85 -0
- package/client/pocket-locales.js +331 -0
- package/cordis.patch.yml +11 -0
- package/lib/device-auth-http.mjs +338 -0
- package/lib/device-auth.mjs +485 -0
- package/lib/index.js +414 -0
- package/lib/ip.mjs +11 -0
- package/lib/proxy.mjs +1223 -0
- package/lib/relay-protocol.mjs +101 -0
- package/lib/relay.mjs +355 -0
- package/lib/restart.js +112 -0
- package/lib/service.mjs +578 -0
- package/lib/settings.mjs +392 -0
- package/lib/tunnel.mjs +621 -0
- package/lib/web-rpc.js +622 -0
- package/package.json +89 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// dsh-web-mobile 移植(MIT,见 LICENSE.dsh-web-mobile):移动端适配的 client 侧实现。
|
|
2
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
3
|
+
import { MobileNavToggle } from './MobileNavToggle.tsx'
|
|
4
|
+
import { MobileNavOverlay } from './MobileNavOverlay.tsx'
|
|
5
|
+
import { MobileDrawerFooter } from './MobileDrawerFooter.tsx'
|
|
6
|
+
import { startFileGuard } from './fileGuard.ts'
|
|
7
|
+
import { MOBILE_CSS } from './mobile.css.ts'
|
|
8
|
+
import { POCKET_RPC_CHANNEL, POCKET_ENDPOINTS } from '../api.js'
|
|
9
|
+
import { NS, en, zh } from './locales.ts'
|
|
10
|
+
import type { MobileNavKey } from './locales.ts'
|
|
11
|
+
import { resolveLayout, persistLayoutFromUrl } from './layout-mode.mjs'
|
|
12
|
+
|
|
13
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
14
|
+
interface LocaleNamespaceMap {
|
|
15
|
+
/** Directory-drawer controls copy. */
|
|
16
|
+
'mobileNav': MobileNavKey
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Required services (cordis fiber inject — the loader passes all module exports as an object plugin). */
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Mobile-adaptive shell, browser half: injects the mobile stylesheet, then
|
|
24
|
+
* contributes the directory toggle to the session header and the backdrop +
|
|
25
|
+
* floating button to the shell overlay.
|
|
26
|
+
* @param ctx - client root context.
|
|
27
|
+
*/
|
|
28
|
+
export function mobileApply(ctx): void {
|
|
29
|
+
// 布局模式(issue #74):URL 参数 > localStorage > auto(=matchMedia)。
|
|
30
|
+
// desktop 模式(宽屏手机/平板强制电脑布局)下整段 mobile 效果都不挂——
|
|
31
|
+
// 不加 styles、不挂 slots、不跑 effects,直接走 DSH 原生桌面 UI。
|
|
32
|
+
const urlValue = new URL(window.location.href).searchParams.get('dsh-layout') ?? '';
|
|
33
|
+
const narrowMQ = window.matchMedia('(max-width: 1023px)');
|
|
34
|
+
const stored = persistLayoutFromUrl(urlValue);
|
|
35
|
+
const layout = resolveLayout({ urlValue, stored, narrowMatch: narrowMQ.matches });
|
|
36
|
+
document.body?.setAttribute('data-dsh-pocket-layout', layout);
|
|
37
|
+
if (layout === 'desktop') return;
|
|
38
|
+
// 强制 mobile:narrow 永远 true;宽度变化不再切换(用户已显式选 mobile)
|
|
39
|
+
// auto 模式:narrow 是真实的 matchMedia,宽度变化会触发 effect 挂载/卸载
|
|
40
|
+
let narrow: MediaQueryList = narrowMQ;
|
|
41
|
+
if (layout === 'mobile') {
|
|
42
|
+
narrow = { matches: true, addEventListener: () => {}, removeEventListener: () => {} } as MediaQueryList;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-mobile-nav: dictionaries')
|
|
46
|
+
|
|
47
|
+
ctx.effect(() => {
|
|
48
|
+
const tag = document.createElement('style')
|
|
49
|
+
tag.dataset.plugin = '@dsh-external/dsh-mobile-nav'
|
|
50
|
+
tag.dataset.pluginCss = '@dsh-external/dsh-mobile-nav/mobile.css'
|
|
51
|
+
tag.textContent = MOBILE_CSS
|
|
52
|
+
document.head.appendChild(tag)
|
|
53
|
+
return () => {
|
|
54
|
+
tag.remove()
|
|
55
|
+
}
|
|
56
|
+
}, 'dsh-mobile-nav: styles')
|
|
57
|
+
|
|
58
|
+
// Phone chrome: KEEP the system status bar (no fullscreen) and make it
|
|
59
|
+
// blend into the page. On narrow screens:
|
|
60
|
+
// - The viewport meta gains viewport-fit=cover, so env(safe-area-inset-top)
|
|
61
|
+
// is the real status-bar / notch height and the stylesheet can push every
|
|
62
|
+
// surface below it (off notched phones, or in a browser tab where the
|
|
63
|
+
// layout viewport already sits below the status bar, the inset is 0 and
|
|
64
|
+
// nothing shifts).
|
|
65
|
+
// - A theme-color meta tracks the shell background (the official theme is
|
|
66
|
+
// toggled by body[data-ds-dark-theme], which flips --dsw-alias-bg-base):
|
|
67
|
+
// Android then paints the status bar / URL bar with the page's own base
|
|
68
|
+
// color, so the status bar reads as part of the UI instead of a foreign
|
|
69
|
+
// strip. The drawer paints the same strip on iOS / notch displays.
|
|
70
|
+
// - gesturestart is suppressed as the legacy-iOS fallback for double-tap
|
|
71
|
+
// zoom; modern browsers are covered by the stylesheet's
|
|
72
|
+
// touch-action: manipulation (which keeps pan and pinch zoom).
|
|
73
|
+
ctx.effect(() => {
|
|
74
|
+
const viewport = document.querySelector<HTMLMetaElement>('meta[name="viewport"]')
|
|
75
|
+
const originalViewport = viewport?.content ?? ''
|
|
76
|
+
const themeMeta = document.createElement('meta')
|
|
77
|
+
themeMeta.name = 'theme-color'
|
|
78
|
+
const bodyBg = (): string => getComputedStyle(document.body).backgroundColor
|
|
79
|
+
|
|
80
|
+
const sync = (): void => {
|
|
81
|
+
if (viewport !== null) viewport.content = 'width=device-width, initial-scale=1, viewport-fit=cover'
|
|
82
|
+
themeMeta.content = bodyBg()
|
|
83
|
+
if (themeMeta.parentElement === null) document.head.appendChild(themeMeta)
|
|
84
|
+
}
|
|
85
|
+
const restore = (): void => {
|
|
86
|
+
if (viewport !== null) viewport.content = originalViewport
|
|
87
|
+
themeMeta.remove()
|
|
88
|
+
}
|
|
89
|
+
const onGestureStart = (event: Event) => event.preventDefault()
|
|
90
|
+
if (narrow.matches) sync()
|
|
91
|
+
const onChange = (event: MediaQueryListEvent) => (event.matches ? sync() : restore())
|
|
92
|
+
narrow.addEventListener('change', onChange)
|
|
93
|
+
const observer = new MutationObserver(() => {
|
|
94
|
+
if (narrow.matches) themeMeta.content = bodyBg()
|
|
95
|
+
})
|
|
96
|
+
observer.observe(document.body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] })
|
|
97
|
+
document.addEventListener('gesturestart', onGestureStart)
|
|
98
|
+
return () => {
|
|
99
|
+
narrow.removeEventListener('change', onChange)
|
|
100
|
+
observer.disconnect()
|
|
101
|
+
document.removeEventListener('gesturestart', onGestureStart)
|
|
102
|
+
restore()
|
|
103
|
+
}
|
|
104
|
+
}, 'dsh-mobile-nav: status bar theme + viewport + zoom guard')
|
|
105
|
+
|
|
106
|
+
// dsh-web-ui compatibility: the aionui explorer column would render as a
|
|
107
|
+
// sheet over the whole mobile UI whenever its (persisted) expanded state
|
|
108
|
+
// is active — including right after a reload, with no way out (the
|
|
109
|
+
// suite's floating expand button only exists while collapsed). Instead
|
|
110
|
+
// of fighting the suite's store timing, the mobile stylesheet keeps the
|
|
111
|
+
// explorer column hidden by default and the header's Files action (plus
|
|
112
|
+
// the drawer footer entry) opens it via the `data-aionui-explorer-open`
|
|
113
|
+
// marker on the frame. This effect just clears that marker when the
|
|
114
|
+
// sheet's own collapse chevron is tapped, so closing is symmetric with
|
|
115
|
+
// opening.
|
|
116
|
+
ctx.effect(() => {
|
|
117
|
+
if (!narrow.matches) return () => {}
|
|
118
|
+
const onChevronClick = (event: MouseEvent) => {
|
|
119
|
+
const target = event.target as HTMLElement | null
|
|
120
|
+
if (target === null || !target.closest('.aionui-collapse-chevron')) return
|
|
121
|
+
document.querySelector('[data-mobile-nav="frame"]')?.removeAttribute('data-aionui-explorer-open')
|
|
122
|
+
}
|
|
123
|
+
document.addEventListener('click', onChevronClick, true)
|
|
124
|
+
return () => document.removeEventListener('click', onChevronClick, true)
|
|
125
|
+
}, 'dsh-mobile-nav: aionui explorer close marker')
|
|
126
|
+
|
|
127
|
+
// The mobile "Files" entries (header icon + drawer footer) open the
|
|
128
|
+
// dsh-web-ui **aionui explorer column** — a host-side component that plain
|
|
129
|
+
// DeepSeek Harness does NOT ship (issue #48). Without it the entries are
|
|
130
|
+
// dead buttons (they only toggle a frame attribute; nothing ever renders).
|
|
131
|
+
// Detect whether the host provides the column and mark the frame with
|
|
132
|
+
// `data-mobile-nav-explorer="1|0"` so the stylesheet can hide the entries
|
|
133
|
+
// on hosts without it (dsh-web-ui installs keep the feature).
|
|
134
|
+
ctx.effect(() => {
|
|
135
|
+
if (!narrow.matches) return () => {}
|
|
136
|
+
const frame = (): HTMLElement | null => document.querySelector('[data-mobile-nav="frame"]')
|
|
137
|
+
const check = () => {
|
|
138
|
+
const has = document.querySelector('[data-aionui-explorer-col]') !== null
|
|
139
|
+
frame()?.setAttribute('data-mobile-nav-explorer', has ? '1' : '0')
|
|
140
|
+
}
|
|
141
|
+
check()
|
|
142
|
+
const timer = window.setTimeout(check, 1500) // 宿主懒渲染:稍后再查一次
|
|
143
|
+
const observer = new MutationObserver(check)
|
|
144
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
145
|
+
return () => {
|
|
146
|
+
window.clearTimeout(timer)
|
|
147
|
+
observer.disconnect()
|
|
148
|
+
}
|
|
149
|
+
}, 'dsh-mobile-nav: explorer availability (issue #48)')
|
|
150
|
+
|
|
151
|
+
// dsh-web-ui compatibility: the aionui preview column persists its open
|
|
152
|
+
// tabs in localStorage and restores them on load, which would pop the
|
|
153
|
+
// preview sheet over the fresh UI after a reload. Gate it like the
|
|
154
|
+
// explorer: the stylesheet keeps the column hidden unless the frame
|
|
155
|
+
// carries `data-aionui-preview-open`; this effect sets that marker when
|
|
156
|
+
// the user actually taps a file row in the explorer sheet, and clears it
|
|
157
|
+
// whenever the suite hides the column again (collapse chevron / tab
|
|
158
|
+
// close), so a restored-but-unwanted sheet never appears.
|
|
159
|
+
ctx.effect(() => {
|
|
160
|
+
if (!narrow.matches) return () => {}
|
|
161
|
+
const frame = (): HTMLElement | null => document.querySelector('[data-mobile-nav="frame"]')
|
|
162
|
+
const onTap = (event: MouseEvent) => {
|
|
163
|
+
const target = event.target as HTMLElement | null
|
|
164
|
+
if (target === null) return
|
|
165
|
+
if (target.closest('[data-aionui-explorer-col] [class$="_treeRow"]') === null) return
|
|
166
|
+
frame()?.setAttribute('data-aionui-preview-open', '')
|
|
167
|
+
}
|
|
168
|
+
const sync = (): void => {
|
|
169
|
+
const pv = document.querySelector('[data-aionui-preview-col]')
|
|
170
|
+
if (pv === null) return
|
|
171
|
+
if (getComputedStyle(pv).visibility === 'hidden') frame()?.removeAttribute('data-aionui-preview-open')
|
|
172
|
+
}
|
|
173
|
+
document.addEventListener('click', onTap, true)
|
|
174
|
+
const observer = new MutationObserver(sync)
|
|
175
|
+
observer.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['style'] })
|
|
176
|
+
sync()
|
|
177
|
+
return () => {
|
|
178
|
+
document.removeEventListener('click', onTap, true)
|
|
179
|
+
observer.disconnect()
|
|
180
|
+
}
|
|
181
|
+
}, 'dsh-mobile-nav: preview sheet open marker')
|
|
182
|
+
|
|
183
|
+
// The official conversation status row (turns / steps / LLM time / TTFT /
|
|
184
|
+
// cache) has a hashed class, so the stylesheet cannot target it directly.
|
|
185
|
+
// Its stable boundary is the official conversation.composer.dock slot. Mark
|
|
186
|
+
// only the metrics root inside that slot; never scan every *_root under the
|
|
187
|
+
// composer because the editor itself is now contenteditable (not textarea)
|
|
188
|
+
// and its root also contains the dock text.
|
|
189
|
+
ctx.effect(() => {
|
|
190
|
+
if (!narrow.matches) return () => {}
|
|
191
|
+
// The composer root renders the TPS readout ("TPS 89.4 tok/s") as its
|
|
192
|
+
// own row BELOW the status strip; fold it into the strip so every
|
|
193
|
+
// metric scrolls together. The suite re-renders its own tree, so this
|
|
194
|
+
// must be idempotent and re-run on every mutation.
|
|
195
|
+
const moveTps = (stats: Element): void => {
|
|
196
|
+
if ([...stats.children].some((c) => /^TPS\s+\d/.test((c.textContent ?? '').trim()))) return
|
|
197
|
+
const stack = stats.closest('[class$="_composerStack"]')
|
|
198
|
+
if (stack === null) return
|
|
199
|
+
for (const el of stack.querySelectorAll('div')) {
|
|
200
|
+
const text = (el.textContent ?? '').trim()
|
|
201
|
+
if (!/^TPS\s+\d/.test(text)) continue
|
|
202
|
+
if (el.children.length > 0) continue
|
|
203
|
+
stats.appendChild(el)
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const mark = (): void => {
|
|
208
|
+
const selector = '[data-phase] [data-slot="conversation.composer.dock"] [class$="_root"]'
|
|
209
|
+
for (const root of document.querySelectorAll(selector)) {
|
|
210
|
+
const text = root.textContent ?? ''
|
|
211
|
+
if (!/(turns|steps|\bLLM\b|轮|步)/.test(text)) continue
|
|
212
|
+
root.setAttribute('data-mobile-nav', 'stats')
|
|
213
|
+
moveTps(root)
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const observer = new MutationObserver(mark)
|
|
218
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
219
|
+
mark()
|
|
220
|
+
return () => {
|
|
221
|
+
observer.disconnect()
|
|
222
|
+
}
|
|
223
|
+
}, 'dsh-mobile-nav: stats line marker')
|
|
224
|
+
|
|
225
|
+
// The dsh-web-ui explorer / preview columns toggle via `visibility`
|
|
226
|
+
// (their inline style), which never restarts a CSS animation — so the
|
|
227
|
+
// sheets would only animate on first mount. Replay the rise animation
|
|
228
|
+
// with the Web Animations API each time a column turns visible, then
|
|
229
|
+
// leave the resting state to the stylesheet.
|
|
230
|
+
ctx.effect(() => {
|
|
231
|
+
if (!narrow.matches) return () => {}
|
|
232
|
+
const cols = ['[data-aionui-explorer-col]', '[data-aionui-preview-col]']
|
|
233
|
+
const seen = new Map<string, boolean>()
|
|
234
|
+
const play = (el: Element): void => {
|
|
235
|
+
el.animate(
|
|
236
|
+
[
|
|
237
|
+
{ opacity: 0, transform: 'translateY(28px)' },
|
|
238
|
+
{ opacity: 1, transform: 'none' },
|
|
239
|
+
],
|
|
240
|
+
{ duration: 280, easing: 'cubic-bezier(.16, 1, .3, 1)', fill: 'backwards' },
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
const check = (): void => {
|
|
244
|
+
for (const sel of cols) {
|
|
245
|
+
const el = document.querySelector(sel)
|
|
246
|
+
if (el === null) continue
|
|
247
|
+
const visible = getComputedStyle(el).visibility === 'visible'
|
|
248
|
+
const prev = seen.get(sel) ?? false
|
|
249
|
+
if (visible && !prev) play(el)
|
|
250
|
+
seen.set(sel, visible)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const observer = new MutationObserver(check)
|
|
254
|
+
// Visibility flips come through inline style mutations (suite) or the
|
|
255
|
+
// explorer-open marker on the frame; class changes are watched too.
|
|
256
|
+
observer.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['style', 'class', 'data-aionui-explorer-open'] })
|
|
257
|
+
check()
|
|
258
|
+
return () => {
|
|
259
|
+
observer.disconnect()
|
|
260
|
+
}
|
|
261
|
+
}, 'dsh-mobile-nav: sheet rise animation replay')
|
|
262
|
+
|
|
263
|
+
// 移动端文件守卫(issue #17 修正):手机上点 dsh-web 渲染的文件链接会触发桌面
|
|
264
|
+
// 端 workspaces.openPath(open ...) —— 既打不开(路径在电脑上),又会抛
|
|
265
|
+
// "path open failed"。这里在捕获阶段拦截这类点击 / 键盘激活,改为弹一个提示,
|
|
266
|
+
// 并隐藏「添加工作区」入口(手机上配工作区无意义);同时在文件链接旁注入
|
|
267
|
+
// 「复制」按钮,点它经主机 RPC 读取文件正文再写入剪贴板。只挂窄屏。
|
|
268
|
+
ctx.effect(() => {
|
|
269
|
+
if (!narrow.matches) return () => {}
|
|
270
|
+
// 尽量拿到当前工作区 cwd(文件链接文案是相对它的),传给主机 RPC 做精确解析;
|
|
271
|
+
// 拿不到就回退到主机 process.cwd()。dsh-web 的 workspaces 服务暴露当前工作区。
|
|
272
|
+
const getWorkspaceCwd = (): string => {
|
|
273
|
+
try {
|
|
274
|
+
const ws = (ctx as unknown as { get?: (k: string) => unknown }).get?.('workspaces')
|
|
275
|
+
?? (ctx as unknown as { workspaces?: unknown }).workspaces
|
|
276
|
+
const list = (ws as { list?: unknown })?.list
|
|
277
|
+
const arr: unknown[] | null = Array.isArray(list)
|
|
278
|
+
? list
|
|
279
|
+
: (list && typeof list === 'object' && 'value' in (list as object)
|
|
280
|
+
? (list as { value: unknown[] }).value
|
|
281
|
+
: null)
|
|
282
|
+
if (Array.isArray(arr)) {
|
|
283
|
+
for (const w of arr) {
|
|
284
|
+
const c = (w as { cwd?: string; root?: string })?.cwd
|
|
285
|
+
?? (w as { cwd?: string; root?: string })?.root
|
|
286
|
+
if (typeof c === 'string' && c) return c
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} catch { /* 忽略,回退 process.cwd() */ }
|
|
290
|
+
return ''
|
|
291
|
+
}
|
|
292
|
+
// 手机侧读文件回调:走 dsh-pocket 的 RPC 通道,由主机侧 fileRead 端点处理。
|
|
293
|
+
const readFile = (filePath: string) =>
|
|
294
|
+
ctx.connection.rpc.call(
|
|
295
|
+
POCKET_RPC_CHANNEL,
|
|
296
|
+
POCKET_ENDPOINTS.fileRead,
|
|
297
|
+
{ path: filePath, cwd: getWorkspaceCwd() },
|
|
298
|
+
) as Promise<{ ok: boolean; value?: { content: string; path: string; size: number }; error?: { message: string } }>
|
|
299
|
+
return startFileGuard(readFile)
|
|
300
|
+
}, 'dsh-mobile-nav: file open guard + copy button + hide add-workspace (issue #17)')
|
|
301
|
+
|
|
302
|
+
// 手机上模型 / 提供方设置加载失败:上游 dsh-web 会渲染「加载提供方目录失败 /
|
|
303
|
+
// Settings are unavailable in this browser」。这条文案不是 dsh-pocket 的,但手机侧
|
|
304
|
+
// 本就不支持改模型设置,原报错只会吓到用户。窄屏下用 MutationObserver 就地把该报错
|
|
305
|
+
// 文本替换成「去电脑端修改」的引导提示(仅手机,桌面端不受影响)。
|
|
306
|
+
ctx.effect(() => {
|
|
307
|
+
if (!narrow.matches) return () => {}
|
|
308
|
+
const PHRASES = ['加载提供方目录失败', 'Settings are unavailable in this browser']
|
|
309
|
+
const NOTICE = '手机上不支持模型设置,请去电脑端修改设置'
|
|
310
|
+
// 取包含报错文案的最深节点,避免把外层大容器整块清掉。
|
|
311
|
+
const findDeepest = (el: Element): Element => {
|
|
312
|
+
let deepest = el
|
|
313
|
+
for (const child of el.querySelectorAll('*')) {
|
|
314
|
+
if (PHRASES.some((p) => (child.textContent ?? '').includes(p))) deepest = child
|
|
315
|
+
}
|
|
316
|
+
return deepest
|
|
317
|
+
}
|
|
318
|
+
const patch = (): void => {
|
|
319
|
+
for (const el of document.querySelectorAll('body *')) {
|
|
320
|
+
const t = el.textContent ?? ''
|
|
321
|
+
if (!PHRASES.some((p) => t.includes(p))) continue
|
|
322
|
+
if ((el as HTMLElement).dataset?.dshpModelNotice === '1') continue
|
|
323
|
+
const target = findDeepest(el)
|
|
324
|
+
target.textContent = NOTICE
|
|
325
|
+
;(target as HTMLElement).dataset.dshpModelNotice = '1'
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const observer = new MutationObserver(patch)
|
|
329
|
+
observer.observe(document.body, { childList: true, subtree: true, characterData: true })
|
|
330
|
+
patch()
|
|
331
|
+
return () => observer.disconnect()
|
|
332
|
+
}, 'dsh-mobile-nav: replace model-settings load error with mobile hint')
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
ctx.slots.inject('conversation.session.header.actions', () => ctx.slots.register({
|
|
336
|
+
name: 'conversation.session.header.actions',
|
|
337
|
+
id: 'mobile-nav-toggle',
|
|
338
|
+
order: 10,
|
|
339
|
+
locale: NS,
|
|
340
|
+
inject: () => ({
|
|
341
|
+
toggleSidebar: () => ctx.layout.toggleSidebar(),
|
|
342
|
+
}),
|
|
343
|
+
}, MobileNavToggle))
|
|
344
|
+
|
|
345
|
+
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
346
|
+
name: 'shell.overlay',
|
|
347
|
+
id: 'mobile-nav-overlay',
|
|
348
|
+
order: 10,
|
|
349
|
+
locale: NS,
|
|
350
|
+
inject: () => ({
|
|
351
|
+
toggleSidebar: () => ctx.layout.toggleSidebar(),
|
|
352
|
+
}),
|
|
353
|
+
}, MobileNavOverlay))
|
|
354
|
+
|
|
355
|
+
// Session log download, relocated from the session header to the drawer
|
|
356
|
+
// footer on mobile (the header capsule is hidden by CSS); the drawer
|
|
357
|
+
// footer also hosts the Files action that opens the dsh-web-ui explorer
|
|
358
|
+
// sheet.
|
|
359
|
+
ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
|
|
360
|
+
name: 'sidebar.footer.action',
|
|
361
|
+
id: 'mobile-nav-session-log',
|
|
362
|
+
order: 10,
|
|
363
|
+
locale: NS,
|
|
364
|
+
inject: () => ({
|
|
365
|
+
downloadSessionLog: (sessionId: string) => ctx.sessionLogDownload.download(sessionId),
|
|
366
|
+
toggleSidebar: () => ctx.layout.toggleSidebar(),
|
|
367
|
+
}),
|
|
368
|
+
}, MobileDrawerFooter))
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Type-only augmentation imports: pull the layout / conversation / sidebar
|
|
372
|
+
// SlotMap merges and the sessionLogDownload service typing into this program
|
|
373
|
+
// without any runtime import.
|
|
374
|
+
import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
|
|
375
|
+
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
376
|
+
import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
|
|
377
|
+
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
378
|
+
import type {} from '@deepseek-ai/dsh-session-log-export/client'
|