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,258 @@
|
|
|
1
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
2
|
+
import { NS } from '../locales.ts'
|
|
3
|
+
|
|
4
|
+
/** Phone breakpoint — same query every phone-only effect in this plugin uses. */
|
|
5
|
+
const PHONE_QUERY = '(max-width: 767px)'
|
|
6
|
+
|
|
7
|
+
/** ChatView's flow column (dsh-client-ui-conversation lib/client.js:5512).
|
|
8
|
+
* Trajectory and every other view render their own tree and never carry this
|
|
9
|
+
* marker, so scoping here is what keeps this effect Chat-only. */
|
|
10
|
+
const FLOW = '[data-chat-flow]'
|
|
11
|
+
|
|
12
|
+
/** Marker + selector of the injected summary row. */
|
|
13
|
+
const CHIP = 'turn-fold'
|
|
14
|
+
const CHIP_SELECTOR = '[data-mobile-nav="turn-fold"]'
|
|
15
|
+
|
|
16
|
+
/** Whole-row node kinds that are turn *process*, not conversation content.
|
|
17
|
+
* Keys are ChatNodeSeat's own `data-chat-flow-kind` dispatch values
|
|
18
|
+
* (registerChatNodeRenderers, lib/client.js:9322): tool calls, injected
|
|
19
|
+
* context rows, and slash-command rows. Everything else stays visible —
|
|
20
|
+
* `user`/`steering` (the question), `assistant-step` (the prose, folded
|
|
21
|
+
* per-block below), `turn-tail` (the footer with its actions), and the
|
|
22
|
+
* error notices `turn-error` / `turn-max-tokens` / `model-retry`, which are
|
|
23
|
+
* exactly what a reader must not have to hunt for. */
|
|
24
|
+
const PROCESS_KINDS = new Set(['context', 'tool-call', 'command'])
|
|
25
|
+
|
|
26
|
+
/** Node kinds that open a new turn group. */
|
|
27
|
+
const TURN_HEAD_KINDS = new Set(['user', 'steering'])
|
|
28
|
+
|
|
29
|
+
/** ReasoningRow's own marker (lib/client.js:8966) — the Think disclosure
|
|
30
|
+
* rendered inside an assistant-step row, beside that step's prose. */
|
|
31
|
+
const THINK = '[data-variant="think"]'
|
|
32
|
+
|
|
33
|
+
/** Attribute the phone stylesheet turns into `display: none`. */
|
|
34
|
+
const FOLD = 'data-mnav-fold'
|
|
35
|
+
/** Per-item override written while its turn is expanded. */
|
|
36
|
+
const OPEN = 'data-mnav-fold-open'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* True when the reasoning rows are the ONLY thing this row renders, so the
|
|
40
|
+
* whole flow item has to go: the flow column is `display: flex` with
|
|
41
|
+
* `gap: 16px`, and an emptied-out flex item still claims its gap — the
|
|
42
|
+
* reader would see the folded steps as a column of blank bands.
|
|
43
|
+
* Structural test, no class names: every ancestor from the reasoning rows'
|
|
44
|
+
* shared parent up to the flow item must be an only child.
|
|
45
|
+
*/
|
|
46
|
+
function foldsWholeRow(row: Element, thinks: readonly Element[]): boolean {
|
|
47
|
+
const first = thinks[0]
|
|
48
|
+
if (first === undefined) return false
|
|
49
|
+
const body = first.parentElement
|
|
50
|
+
if (body === null || body.childElementCount !== thinks.length) return false
|
|
51
|
+
let node: Element = body
|
|
52
|
+
while (node !== row) {
|
|
53
|
+
const parent = node.parentElement
|
|
54
|
+
if (parent === null) return false
|
|
55
|
+
if (parent.childElementCount !== 1) return false
|
|
56
|
+
node = parent
|
|
57
|
+
}
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The process items one flow row contributes: the row itself, its reasoning
|
|
62
|
+
* rows, or nothing at all. */
|
|
63
|
+
function processItems(row: Element): readonly Element[] {
|
|
64
|
+
const kind = row.getAttribute('data-chat-flow-kind')
|
|
65
|
+
if (kind === null) return []
|
|
66
|
+
if (PROCESS_KINDS.has(kind)) return [row]
|
|
67
|
+
if (kind !== 'assistant-step') return []
|
|
68
|
+
const thinks = [...row.querySelectorAll(THINK)]
|
|
69
|
+
if (thinks.length === 0) return []
|
|
70
|
+
return foldsWholeRow(row, thinks) ? [row] : thinks
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The summary row sitting immediately before `anchor`, created if absent.
|
|
75
|
+
* Idempotent by construction — React re-rendering the flow either leaves our
|
|
76
|
+
* button where it is (it only ever inserts and removes its OWN nodes) or
|
|
77
|
+
* drops it, and the next rescan puts it back.
|
|
78
|
+
*/
|
|
79
|
+
function chipBefore(anchor: Element): HTMLElement | null {
|
|
80
|
+
const previous = anchor.previousElementSibling
|
|
81
|
+
if (previous instanceof HTMLElement && previous.dataset['mobileNav'] === CHIP) return previous
|
|
82
|
+
const parent = anchor.parentElement
|
|
83
|
+
if (parent === null) return null
|
|
84
|
+
const chip = document.createElement('button')
|
|
85
|
+
chip.type = 'button'
|
|
86
|
+
chip.dataset['mobileNav'] = CHIP
|
|
87
|
+
parent.insertBefore(chip, anchor)
|
|
88
|
+
return chip
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface Group {
|
|
92
|
+
/** Stable id: the turn-opening row's node key (a positional id for a
|
|
93
|
+
* leading group whose question has not been paged in). */
|
|
94
|
+
readonly key: string
|
|
95
|
+
/** Every process element of this turn, in flow order. */
|
|
96
|
+
readonly items: Element[]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Split the flow column into turns: a group runs from one user/steering row
|
|
100
|
+
* up to (not including) the next. Rows that precede the first user row —
|
|
101
|
+
* a turn whose question has not been paged in yet — form a leading group of
|
|
102
|
+
* their own, so their process never becomes unreachable. */
|
|
103
|
+
function groupsOf(flow: Element): readonly Group[] {
|
|
104
|
+
const groups: Group[] = []
|
|
105
|
+
let current: Group | undefined
|
|
106
|
+
for (const row of flow.children) {
|
|
107
|
+
const kind = row.getAttribute('data-chat-flow-kind')
|
|
108
|
+
if (kind === null) continue
|
|
109
|
+
if (current === undefined || TURN_HEAD_KINDS.has(kind)) {
|
|
110
|
+
current = { key: row.getAttribute('data-chat-flow-key') ?? `pos${groups.length}`, items: [] }
|
|
111
|
+
groups.push(current)
|
|
112
|
+
}
|
|
113
|
+
current.items.push(...processItems(row))
|
|
114
|
+
}
|
|
115
|
+
return groups
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* S8 — collapse a turn's process into one summary row (< 768px only).
|
|
120
|
+
*
|
|
121
|
+
* Route taken: DOM marking, not the `conversation.chat.node` keyed slot.
|
|
122
|
+
* That slot dispatches on node kind and a second registration at a key
|
|
123
|
+
* SHADOWS the official renderer (dsh-client-ui-slots index.d.ts:542 — the
|
|
124
|
+
* cell's lowest-priority live entry renders, there is no wrapping form and
|
|
125
|
+
* no `children` handle to the shadowed component). Taking `tool-call` would
|
|
126
|
+
* mean re-implementing every tool's presentation, its `t` seat comes from
|
|
127
|
+
* the entry's own declared locale namespace and its services from the
|
|
128
|
+
* registrant's own inject face — neither is reachable from here — and the
|
|
129
|
+
* registration is global, so the desktop no-op would be gone too.
|
|
130
|
+
*
|
|
131
|
+
* The DOM route needs no class names: ChatNodeSeat stamps every row with
|
|
132
|
+
* `data-chat-flow-kind` / `data-chat-flow-key` (lib/client.js:5228) and
|
|
133
|
+
* ReasoningRow stamps `data-variant="think"` — turn grouping is pure
|
|
134
|
+
* structure (the run of rows following one `user` row), never text.
|
|
135
|
+
*
|
|
136
|
+
* ponytail: one rAF-coalesced full rescan per DOM mutation batch, O(rows).
|
|
137
|
+
* If a very long session ever makes streaming feel heavy, narrow the
|
|
138
|
+
* observer to the flow column's own childList and diff instead.
|
|
139
|
+
*/
|
|
140
|
+
export function installTurnFold(ctx: ClientContext): void {
|
|
141
|
+
ctx.effect(() => {
|
|
142
|
+
const narrow = window.matchMedia(PHONE_QUERY)
|
|
143
|
+
const t = ctx.locale.bind(NS)
|
|
144
|
+
/** Turns the reader opened. Deliberately not persisted (spec: a reload
|
|
145
|
+
* returns to the default folded state). */
|
|
146
|
+
const expanded = new Set<string>()
|
|
147
|
+
let observer: MutationObserver | null = null
|
|
148
|
+
let frame = 0
|
|
149
|
+
|
|
150
|
+
const label = (chip: HTMLElement, count: number, running: boolean): void => {
|
|
151
|
+
const next = running ? t('turnFoldRunning', { count }) : t('turnFold', { count })
|
|
152
|
+
if (chip.textContent !== next) chip.textContent = next
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const scan = (): void => {
|
|
156
|
+
const flow = document.querySelector(FLOW)
|
|
157
|
+
if (flow === null) return
|
|
158
|
+
// TurnStatus, ChatView's own running banner — present only while the
|
|
159
|
+
// last turn is still working (lib/client.js:5548).
|
|
160
|
+
const running = flow.querySelector(':scope > [role="status"]') !== null
|
|
161
|
+
const groups = groupsOf(flow)
|
|
162
|
+
const live = new Set<string>()
|
|
163
|
+
|
|
164
|
+
groups.forEach((group, index) => {
|
|
165
|
+
if (group.items.length === 0) return
|
|
166
|
+
const open = expanded.has(group.key)
|
|
167
|
+
for (const item of group.items) {
|
|
168
|
+
item.setAttribute(FOLD, '')
|
|
169
|
+
if (open) item.setAttribute(OPEN, '')
|
|
170
|
+
else item.removeAttribute(OPEN)
|
|
171
|
+
}
|
|
172
|
+
const first = group.items[0]
|
|
173
|
+
if (first === undefined) return
|
|
174
|
+
const anchor = first.closest('[data-chat-flow-key]') ?? first
|
|
175
|
+
const chip = chipBefore(anchor)
|
|
176
|
+
if (chip === null) return
|
|
177
|
+
chip.dataset['foldGroup'] = group.key
|
|
178
|
+
chip.setAttribute('aria-expanded', open ? 'true' : 'false')
|
|
179
|
+
if (open) chip.dataset['open'] = ''
|
|
180
|
+
else delete chip.dataset['open']
|
|
181
|
+
const groupRunning = running && index === groups.length - 1
|
|
182
|
+
if (groupRunning) chip.dataset['running'] = ''
|
|
183
|
+
else delete chip.dataset['running']
|
|
184
|
+
label(chip, group.items.length, groupRunning)
|
|
185
|
+
live.add(group.key)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
for (const chip of flow.querySelectorAll<HTMLElement>(CHIP_SELECTOR)) {
|
|
189
|
+
const key = chip.dataset['foldGroup']
|
|
190
|
+
if (key === undefined || !live.has(key)) chip.remove()
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const schedule = (): void => {
|
|
195
|
+
// The locale subscription below lives outside attach/detach, so this
|
|
196
|
+
// guard is what keeps a late dictionary registration from marking up
|
|
197
|
+
// the flow at >= 768px (real WebKit regression: chips and fold
|
|
198
|
+
// attributes appeared at 1280px — invisible, since every rule that
|
|
199
|
+
// hides anything is inside the phone media query, but still wrong).
|
|
200
|
+
if (observer === null) return
|
|
201
|
+
if (frame !== 0) return
|
|
202
|
+
frame = requestAnimationFrame(() => {
|
|
203
|
+
frame = 0
|
|
204
|
+
scan()
|
|
205
|
+
// Drop the records our own chip insertions just queued, so a rescan
|
|
206
|
+
// can never re-trigger itself (the debug-badge feedback-loop freeze).
|
|
207
|
+
observer?.takeRecords()
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const onClick = (event: Event): void => {
|
|
212
|
+
const target = event.target
|
|
213
|
+
if (!(target instanceof Element)) return
|
|
214
|
+
const chip = target.closest<HTMLElement>(CHIP_SELECTOR)
|
|
215
|
+
const key = chip?.dataset['foldGroup']
|
|
216
|
+
if (key === undefined) return
|
|
217
|
+
if (expanded.has(key)) expanded.delete(key)
|
|
218
|
+
else expanded.add(key)
|
|
219
|
+
scan()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const clear = (): void => {
|
|
223
|
+
for (const chip of document.querySelectorAll(CHIP_SELECTOR)) chip.remove()
|
|
224
|
+
for (const item of document.querySelectorAll(`[${FOLD}]`)) {
|
|
225
|
+
item.removeAttribute(FOLD)
|
|
226
|
+
item.removeAttribute(OPEN)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const attach = (): void => {
|
|
231
|
+
if (observer !== null) return
|
|
232
|
+
observer = new MutationObserver(schedule)
|
|
233
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
234
|
+
document.addEventListener('click', onClick)
|
|
235
|
+
scan()
|
|
236
|
+
}
|
|
237
|
+
const detach = (): void => {
|
|
238
|
+
observer?.disconnect()
|
|
239
|
+
observer = null
|
|
240
|
+
document.removeEventListener('click', onClick)
|
|
241
|
+
if (frame !== 0) cancelAnimationFrame(frame)
|
|
242
|
+
frame = 0
|
|
243
|
+
expanded.clear()
|
|
244
|
+
clear()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (narrow.matches) attach()
|
|
248
|
+
const onChange = (event: MediaQueryListEvent): void => (event.matches ? attach() : detach())
|
|
249
|
+
narrow.addEventListener('change', onChange)
|
|
250
|
+
// A language switch changes the chip copy of every already-rendered turn.
|
|
251
|
+
const stopLocale = ctx.locale.subscribe(schedule)
|
|
252
|
+
return () => {
|
|
253
|
+
narrow.removeEventListener('change', onChange)
|
|
254
|
+
stopLocale()
|
|
255
|
+
detach()
|
|
256
|
+
}
|
|
257
|
+
}, 'dsh-mobile-nav: turn process fold')
|
|
258
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type { ClientContext, SessionId, WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client'
|
|
2
|
+
import { MobileNavToggle } from './MobileNavToggle.tsx'
|
|
3
|
+
import { MobileNavOverlay } from './MobileNavOverlay.tsx'
|
|
4
|
+
import { MobileDrawerFooter } from './MobileDrawerFooter.tsx'
|
|
5
|
+
import { MobileHome } from './MobileHome.tsx'
|
|
6
|
+
import { MobileHeaderActions, MobileHeaderUtilities } from './MobileSessionHeader.tsx'
|
|
7
|
+
import { MobileSessionInfo } from './MobileSessionInfo.tsx'
|
|
8
|
+
import { MobileAttachButton } from './MobileAttachButton.tsx'
|
|
9
|
+
import { MobileAttachChips } from './MobileAttachChips.tsx'
|
|
10
|
+
import { createNavStore } from './nav-store.ts'
|
|
11
|
+
import { MOBILE_CSS } from './styles/index.ts'
|
|
12
|
+
import { installDebugBadge } from './debug.ts'
|
|
13
|
+
import { installPhoneChrome, installSunkInset, installViewportHeal } from './effects/phone-chrome.ts'
|
|
14
|
+
import { installAionuiCompat } from './effects/aionui-compat.ts'
|
|
15
|
+
import { installHeaderStatusDot } from './effects/header-status.ts'
|
|
16
|
+
import { installGestures } from './effects/gestures.ts'
|
|
17
|
+
import { installTurnFold } from './effects/turn-fold.ts'
|
|
18
|
+
import { NS, en, zh } from './locales.ts'
|
|
19
|
+
import type { MobileNavKey } from './locales.ts'
|
|
20
|
+
|
|
21
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
22
|
+
interface LocaleNamespaceMap {
|
|
23
|
+
/** Directory-drawer controls copy. */
|
|
24
|
+
'mobileNav': MobileNavKey
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Required services (cordis fiber inject — the loader passes all module exports as an object plugin). */
|
|
29
|
+
export const inject = ['slots', 'layout', 'locale', 'sessionLogDownload', 'sessions', 'workspaces']
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Mobile-adaptive shell, browser half: injects the mobile stylesheet, then
|
|
33
|
+
* contributes the directory toggle to the session header and the backdrop +
|
|
34
|
+
* floating button to the shell overlay.
|
|
35
|
+
* @param ctx - client root context.
|
|
36
|
+
*/
|
|
37
|
+
export function apply(ctx: ClientContext): void {
|
|
38
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-mobile-nav: dictionaries')
|
|
39
|
+
|
|
40
|
+
ctx.effect(() => {
|
|
41
|
+
const tag = document.createElement('style')
|
|
42
|
+
tag.dataset.plugin = 'dsh-zen-remote'
|
|
43
|
+
tag.dataset.pluginCss = 'dsh-zen-remote/mobile.css'
|
|
44
|
+
tag.textContent = MOBILE_CSS
|
|
45
|
+
document.head.appendChild(tag)
|
|
46
|
+
return () => {
|
|
47
|
+
tag.remove()
|
|
48
|
+
}
|
|
49
|
+
}, 'dsh-mobile-nav: styles')
|
|
50
|
+
|
|
51
|
+
// Diagnostic overlay for phone-side repros (?mobile-nav-debug=1).
|
|
52
|
+
installDebugBadge(ctx)
|
|
53
|
+
|
|
54
|
+
installPhoneChrome(ctx)
|
|
55
|
+
|
|
56
|
+
// Standalone-PWA only: undo the WebKit keyboard bug that permanently steals
|
|
57
|
+
// the status-bar height from the viewport (the ~60px band under the
|
|
58
|
+
// composer and the session list). No-op in any browser tab.
|
|
59
|
+
installViewportHeal(ctx)
|
|
60
|
+
|
|
61
|
+
// Registered after the heal so a viewport that heals is measured healed:
|
|
62
|
+
// when the strip is real, drop the home-indicator padding that would only
|
|
63
|
+
// stack more blank page on top of it.
|
|
64
|
+
installSunkInset(ctx)
|
|
65
|
+
|
|
66
|
+
installAionuiCompat(ctx)
|
|
67
|
+
|
|
68
|
+
// Session header running-status dot (S2): no official element exists to
|
|
69
|
+
// reposition, so this reads ctx.sessions directly and self-draws via CSS.
|
|
70
|
+
installHeaderStatusDot(ctx)
|
|
71
|
+
|
|
72
|
+
// S6: content-area swipe (Chat/Trajectory) + sheet drag-to-close.
|
|
73
|
+
installGestures(ctx)
|
|
74
|
+
|
|
75
|
+
// S8: fold a turn's process (tool calls, injected context, slash commands,
|
|
76
|
+
// Think rows) behind one summary chip. Chat view only, phone only — see
|
|
77
|
+
// effects/turn-fold.ts for why the keyed conversation.chat.node seat could
|
|
78
|
+
// not carry this.
|
|
79
|
+
installTurnFold(ctx)
|
|
80
|
+
|
|
81
|
+
// Page-stack store (apply world) — created before any registration so
|
|
82
|
+
// every slot below (the phone home screen, the session header's back
|
|
83
|
+
// button) shares the exact same handle/instance.
|
|
84
|
+
const nav = createNavStore()
|
|
85
|
+
|
|
86
|
+
ctx.slots.inject('conversation.session.header.actions', () => ctx.slots.register({
|
|
87
|
+
name: 'conversation.session.header.actions',
|
|
88
|
+
id: 'mobile-nav-toggle',
|
|
89
|
+
order: 10,
|
|
90
|
+
locale: NS,
|
|
91
|
+
inject: () => ({
|
|
92
|
+
toggleSidebar: () => ctx.layout.toggleSidebar(),
|
|
93
|
+
}),
|
|
94
|
+
}, MobileNavToggle))
|
|
95
|
+
|
|
96
|
+
// Session header back button + Chat/Trajectory view-switch row (S2).
|
|
97
|
+
// Renders unconditionally; CSS (styles/header.css.ts) keeps it hidden at
|
|
98
|
+
// >= 768px. Order is irrelevant here — the phone stylesheet hides every
|
|
99
|
+
// other header.actions entry and only re-shows this one.
|
|
100
|
+
//
|
|
101
|
+
// No `store: nav` here: this slot is session-scope while `nav` already
|
|
102
|
+
// mounts at shell.overlay's root scope, and a handle can only mount
|
|
103
|
+
// under one scope (runtime throws otherwise — see nav-store.ts). The
|
|
104
|
+
// back button dispatches GO_HOME_EVENT and MobileHome applies it.
|
|
105
|
+
ctx.slots.inject('conversation.session.header.actions', () => ctx.slots.register({
|
|
106
|
+
name: 'conversation.session.header.actions',
|
|
107
|
+
id: 'mobile-header-actions',
|
|
108
|
+
order: 0,
|
|
109
|
+
locale: NS,
|
|
110
|
+
}, MobileHeaderActions))
|
|
111
|
+
|
|
112
|
+
// Session-info entry (placeholder — S4 owns the sheet) + workbench entry
|
|
113
|
+
// (dsh-better-sidebar, see MobileSessionHeader.tsx for the trigger).
|
|
114
|
+
ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({
|
|
115
|
+
name: 'conversation.session.header.utilities',
|
|
116
|
+
id: 'mobile-header-utilities',
|
|
117
|
+
order: 0,
|
|
118
|
+
locale: NS,
|
|
119
|
+
}, MobileHeaderUtilities))
|
|
120
|
+
|
|
121
|
+
// Session-info sheet (S4): a second, sibling entry on the SAME slot as
|
|
122
|
+
// the ⓘ button above — it listens for the CustomEvent that button fires
|
|
123
|
+
// instead of sharing render state with it. Session scope gives this
|
|
124
|
+
// entry useProjection/sessionId (the stats grid) for free alongside the
|
|
125
|
+
// always-present useSessions/useWorkspaces (see MobileSessionInfo.tsx's
|
|
126
|
+
// header comment for the full mount-point tradeoff against shell.overlay).
|
|
127
|
+
ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({
|
|
128
|
+
name: 'conversation.session.header.utilities',
|
|
129
|
+
id: 'mobile-session-info',
|
|
130
|
+
order: 10,
|
|
131
|
+
locale: NS,
|
|
132
|
+
// The factory's own sessionId param is unused: every function below
|
|
133
|
+
// takes its own session id explicitly (they're generic action bindings
|
|
134
|
+
// reused verbatim, not closures over one particular session).
|
|
135
|
+
inject: (_sessionId: SessionId) => ({
|
|
136
|
+
forkSession: (id: SessionId) => ctx.sessions.fork({ sessionId: id }),
|
|
137
|
+
openSession: (id: SessionId) => ctx.sessions.open(id),
|
|
138
|
+
renameSession: (id: SessionId, title: string) => ctx.sessions.binding(id)?.session.rename(title),
|
|
139
|
+
archiveSession: (id: SessionId) => ctx.workspaces.archiveSession(id),
|
|
140
|
+
downloadSessionLog: (id: SessionId) => ctx.sessionLogDownload.download(id),
|
|
141
|
+
}),
|
|
142
|
+
}, MobileSessionInfo))
|
|
143
|
+
|
|
144
|
+
// Composer attachment seat (S7). Registered unconditionally;
|
|
145
|
+
// styles/composer.css.ts hides it at >= 768px and orders it into the
|
|
146
|
+
// leftmost seat of the phone composer row.
|
|
147
|
+
//
|
|
148
|
+
// No `inject` here: every attachment now rides the host upload route and
|
|
149
|
+
// the standard session props (`sessionId`, `inputActions`), so the button
|
|
150
|
+
// needs nothing bound off ctx. S7.1 removed the session.prompt binding that
|
|
151
|
+
// used to send inlineable images straight into the conversation.
|
|
152
|
+
ctx.slots.inject('conversation.input.left', () => ctx.slots.register({
|
|
153
|
+
name: 'conversation.input.left',
|
|
154
|
+
id: 'mobile-attach',
|
|
155
|
+
order: 0,
|
|
156
|
+
locale: NS,
|
|
157
|
+
}, MobileAttachButton))
|
|
158
|
+
|
|
159
|
+
// Attachment preview row (S7.1), above the composer card. Renders purely
|
|
160
|
+
// off the draft's @.dsh-uploads/ tokens — see MobileAttachChips.tsx. Order 0
|
|
161
|
+
// puts it left of the git branch chip (order 100) on the shared dock line.
|
|
162
|
+
ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
|
|
163
|
+
name: 'conversation.input.dock',
|
|
164
|
+
id: 'mobile-attach-chips',
|
|
165
|
+
order: 0,
|
|
166
|
+
locale: NS,
|
|
167
|
+
}, MobileAttachChips))
|
|
168
|
+
|
|
169
|
+
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
170
|
+
name: 'shell.overlay',
|
|
171
|
+
id: 'mobile-nav-overlay',
|
|
172
|
+
order: 10,
|
|
173
|
+
locale: NS,
|
|
174
|
+
inject: () => ({
|
|
175
|
+
toggleSidebar: () => ctx.layout.toggleSidebar(),
|
|
176
|
+
}),
|
|
177
|
+
}, MobileNavOverlay))
|
|
178
|
+
|
|
179
|
+
// Phone app shell (< 768px): the full-screen session list that is level 1
|
|
180
|
+
// of the page stack. Owns the `nav` handle created above (root scope);
|
|
181
|
+
// the session header's back button listens for GO_HOME_EVENT instead of
|
|
182
|
+
// sharing the handle directly (see nav-store.ts and the comment above).
|
|
183
|
+
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
184
|
+
name: 'shell.overlay',
|
|
185
|
+
id: 'mobile-home',
|
|
186
|
+
order: 20,
|
|
187
|
+
locale: NS,
|
|
188
|
+
store: nav,
|
|
189
|
+
inject: () => ({
|
|
190
|
+
openSession: (id: SessionId) => ctx.sessions.open(id),
|
|
191
|
+
startSession: (workspaceId?: WorkspaceId) => ctx.workspaces.startSession(workspaceId),
|
|
192
|
+
// S5 session-log chip — the same service call MobileDrawerFooter uses.
|
|
193
|
+
downloadSessionLog: (id: SessionId) => ctx.sessionLogDownload.download(id),
|
|
194
|
+
}),
|
|
195
|
+
}, MobileHome))
|
|
196
|
+
|
|
197
|
+
// Session log download, relocated from the session header to the drawer
|
|
198
|
+
// footer on mobile (the header capsule is hidden by CSS); the drawer
|
|
199
|
+
// footer also hosts the Files action that opens the dsh-web-ui explorer
|
|
200
|
+
// sheet.
|
|
201
|
+
//
|
|
202
|
+
// Footer stacking relies on the list-slot sort by (priority, order):
|
|
203
|
+
// dsh-remote-web-ui leaves it unset (default 0, its two icon buttons stay
|
|
204
|
+
// on top) and dsh-usage-stats uses 10. Order 5 keeps the Files + Session
|
|
205
|
+
// log pills directly under the icon row with the usage/balance badge
|
|
206
|
+
// below them — instead of a tie at 10 where registration order could
|
|
207
|
+
// wedge the badge between the icons and the pills.
|
|
208
|
+
ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
|
|
209
|
+
name: 'sidebar.footer.action',
|
|
210
|
+
id: 'mobile-nav-session-log',
|
|
211
|
+
order: 5,
|
|
212
|
+
locale: NS,
|
|
213
|
+
inject: () => ({
|
|
214
|
+
downloadSessionLog: (sessionId: string) => ctx.sessionLogDownload.download(sessionId),
|
|
215
|
+
toggleSidebar: () => ctx.layout.toggleSidebar(),
|
|
216
|
+
}),
|
|
217
|
+
}, MobileDrawerFooter))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Type-only augmentation imports: pull the layout / conversation / sidebar
|
|
221
|
+
// SlotMap merges and the sessionLogDownload service typing into this program
|
|
222
|
+
// without any runtime import.
|
|
223
|
+
import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
|
|
224
|
+
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
225
|
+
import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
|
|
226
|
+
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
227
|
+
import type {} from '@deepseek-ai/dsh-session-log-export/client'
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** `mobileNav` namespace dictionaries: drawer controls. */
|
|
2
|
+
export const NS = 'mobileNav'
|
|
3
|
+
|
|
4
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
5
|
+
export const zh = {
|
|
6
|
+
'open': '打开目录',
|
|
7
|
+
'close': '收起目录',
|
|
8
|
+
'backdrop': '点击关闭目录',
|
|
9
|
+
'sessionLog': '导出会话日志',
|
|
10
|
+
'files': '文件浏览',
|
|
11
|
+
'previewFullscreen': '全屏预览',
|
|
12
|
+
'previewExitFullscreen': '退出全屏',
|
|
13
|
+
'allWorkspaces': '全部',
|
|
14
|
+
'switchWorkspace': '切换工作区',
|
|
15
|
+
'newSession': '新建会话',
|
|
16
|
+
'noSessions': '还没有会话,点右下角加号开始',
|
|
17
|
+
'homeStatusOngoing': '运行中',
|
|
18
|
+
'homeStatusWarning': '待处理',
|
|
19
|
+
'homeStatusDone': '已完成',
|
|
20
|
+
'backToList': '返回会话列表',
|
|
21
|
+
'sessionInfo': '会话信息',
|
|
22
|
+
'workbench': '工作台',
|
|
23
|
+
'workbenchClose': '关闭工作台',
|
|
24
|
+
'switchView': '切换视图',
|
|
25
|
+
'attach': '添加附件',
|
|
26
|
+
'attachBusy': '上传中…',
|
|
27
|
+
'attachFailed': '上传失败,点按重试',
|
|
28
|
+
'attachRemove': '移除附件 {name}',
|
|
29
|
+
'infoClose': '关闭',
|
|
30
|
+
'infoMode': '模式',
|
|
31
|
+
'infoCwdFallback': '未知目录',
|
|
32
|
+
'infoSubagents': '{count} 个子代理',
|
|
33
|
+
'infoStatTurns': '轮次',
|
|
34
|
+
'infoStatSteps': '步骤',
|
|
35
|
+
'infoStatTtft': '首字延迟',
|
|
36
|
+
'infoStatLlm': '模型耗时',
|
|
37
|
+
'infoStatTool': '工具耗时',
|
|
38
|
+
'infoStatTokens': 'Token',
|
|
39
|
+
'infoCacheHit': '缓存 {percent}%',
|
|
40
|
+
'infoExport': '导出日志',
|
|
41
|
+
'infoRename': '重命名',
|
|
42
|
+
'infoRenamePrompt': '会话新名称',
|
|
43
|
+
'infoFork': 'Fork 会话',
|
|
44
|
+
'infoArchive': '归档',
|
|
45
|
+
'infoArchiveConfirm': '归档后将从会话列表隐藏,确定继续?',
|
|
46
|
+
'infoActionError': '操作失败:{message}',
|
|
47
|
+
'turnFold': '过程 · {count} 步',
|
|
48
|
+
'turnFoldRunning': '进行中 · {count} 步',
|
|
49
|
+
'settings': '设置',
|
|
50
|
+
'chipTaskboard': '任务看板',
|
|
51
|
+
'chipSsh': 'SSH',
|
|
52
|
+
'chipUsage': '用量',
|
|
53
|
+
'chipCustomize': '自定义入口',
|
|
54
|
+
} as const
|
|
55
|
+
|
|
56
|
+
/** English dictionary, key-identical to the Chinese source of truth. */
|
|
57
|
+
export const en: Record<MobileNavKey, string> = {
|
|
58
|
+
'open': 'Open directory',
|
|
59
|
+
'close': 'Close directory',
|
|
60
|
+
'backdrop': 'Click to close directory',
|
|
61
|
+
'sessionLog': 'Session log',
|
|
62
|
+
'files': 'Files',
|
|
63
|
+
'previewFullscreen': 'Fullscreen preview',
|
|
64
|
+
'previewExitFullscreen': 'Exit fullscreen',
|
|
65
|
+
'allWorkspaces': 'All',
|
|
66
|
+
'switchWorkspace': 'Switch workspace',
|
|
67
|
+
'newSession': 'New session',
|
|
68
|
+
'noSessions': 'No sessions yet — tap + to start one',
|
|
69
|
+
'homeStatusOngoing': 'Running',
|
|
70
|
+
'homeStatusWarning': 'Needs attention',
|
|
71
|
+
'homeStatusDone': 'Done',
|
|
72
|
+
'backToList': 'Back to sessions',
|
|
73
|
+
'sessionInfo': 'Session info',
|
|
74
|
+
'workbench': 'Workbench',
|
|
75
|
+
'workbenchClose': 'Close workbench',
|
|
76
|
+
'switchView': 'Switch view',
|
|
77
|
+
'attach': 'Add attachment',
|
|
78
|
+
'attachBusy': 'Uploading…',
|
|
79
|
+
'attachFailed': 'Upload failed — tap to retry',
|
|
80
|
+
'attachRemove': 'Remove attachment {name}',
|
|
81
|
+
'infoClose': 'Close',
|
|
82
|
+
'infoMode': 'Mode',
|
|
83
|
+
'infoCwdFallback': 'Unknown directory',
|
|
84
|
+
'infoSubagents': '{count} subagents',
|
|
85
|
+
'infoStatTurns': 'Turns',
|
|
86
|
+
'infoStatSteps': 'Steps',
|
|
87
|
+
'infoStatTtft': 'TTFT',
|
|
88
|
+
'infoStatLlm': 'LLM time',
|
|
89
|
+
'infoStatTool': 'Tool time',
|
|
90
|
+
'infoStatTokens': 'Tokens',
|
|
91
|
+
'infoCacheHit': 'Cache {percent}%',
|
|
92
|
+
'infoExport': 'Export log',
|
|
93
|
+
'infoRename': 'Rename',
|
|
94
|
+
'infoRenamePrompt': 'New session name',
|
|
95
|
+
'infoFork': 'Fork session',
|
|
96
|
+
'infoArchive': 'Archive',
|
|
97
|
+
'infoArchiveConfirm': 'Archiving hides this session from the list. Continue?',
|
|
98
|
+
'infoActionError': 'Action failed: {message}',
|
|
99
|
+
'turnFold': 'Process · {count} steps',
|
|
100
|
+
'turnFoldRunning': 'Working · {count} steps',
|
|
101
|
+
'settings': 'Settings',
|
|
102
|
+
'chipTaskboard': 'Task board',
|
|
103
|
+
'chipSsh': 'SSH',
|
|
104
|
+
'chipUsage': 'Usage',
|
|
105
|
+
'chipCustomize': 'Customize shortcuts',
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Key domain of the `mobileNav` namespace (zh is the source of truth). */
|
|
109
|
+
export type MobileNavKey = keyof typeof zh
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { defineStore } from '@deepseek-ai/dsh-client-runtime/client'
|
|
2
|
+
import type { WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client'
|
|
3
|
+
|
|
4
|
+
/** The two levels of the phone page stack. */
|
|
5
|
+
export type MobileView = 'home' | 'session'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Session-list workspace filter: a pinned workspace, the explicit "all"
|
|
9
|
+
* choice, or `null` — the untouched default that follows the current
|
|
10
|
+
* session's workspace (resolved in the component, so a workspace that only
|
|
11
|
+
* appears after the first baseline still lands).
|
|
12
|
+
*/
|
|
13
|
+
export type WorkspaceFilter = WorkspaceId | 'all' | null
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `window` event a `conversation.session.header.*` slot (session scope)
|
|
17
|
+
* fires to move the ROOT-scope nav store back to `home` — see
|
|
18
|
+
* {@link GO_HOME_EVENT} below for why a store handle cannot cross this
|
|
19
|
+
* particular scope boundary directly.
|
|
20
|
+
*/
|
|
21
|
+
export const GO_HOME_EVENT = 'dsh-mobile-nav:go-home'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `window` event the header's ⓘ button (`conversation.session.header.utilities`,
|
|
25
|
+
* session scope) fires to open the session-info sheet (S4, MobileSessionInfo.tsx)
|
|
26
|
+
* — a second, sibling entry on the SAME slot. Not a store handle for the same
|
|
27
|
+
* reason {@link GO_HOME_EVENT} isn't: two independent registrations sharing
|
|
28
|
+
* one scope could hold a common handle instead, but keeping the info sheet's
|
|
29
|
+
* open/closed state fully local (a plain `useState`) is simpler than adding a
|
|
30
|
+
* second store, and the event is one line either way.
|
|
31
|
+
*/
|
|
32
|
+
export const SESSION_INFO_EVENT = 'dsh-mobile-nav:session-info'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Phone page-stack store (phone breakpoint only; the tablet/desktop layouts
|
|
36
|
+
* never read it). Deliberately NOT persisted: the spec's launch rule is
|
|
37
|
+
* "always land on the session list", so a reload must reset to `home`.
|
|
38
|
+
*
|
|
39
|
+
* Built by a factory instead of a module-level constant: a module-scope
|
|
40
|
+
* handle is a disguised singleton across plugin reloads (ui-slots docs).
|
|
41
|
+
*
|
|
42
|
+
* One handle IS shared by every registration of one apply() — but only
|
|
43
|
+
* within the SAME slot scope. This handle mounts at `shell.overlay`, a
|
|
44
|
+
* ROOT-scope slot (MobileHome.tsx); declaring the identical handle on a
|
|
45
|
+
* SESSION-scope slot (e.g. `conversation.session.header.actions`) throws at
|
|
46
|
+
* runtime ("store handle mounted under ... is already mounted under scope
|
|
47
|
+
* ...", confirmed 2026-08-17) — the framework creates one live instance per
|
|
48
|
+
* (handle, scope), and root/session are different scopes even for the same
|
|
49
|
+
* handle. A session-scope registration that needs to move the page stack
|
|
50
|
+
* (the S2 header back button) cannot hold `actions.show` directly; it
|
|
51
|
+
* dispatches {@link GO_HOME_EVENT} instead, and MobileHome — already
|
|
52
|
+
* mounted with this store — is the one that calls `actions.show('home')`.
|
|
53
|
+
* @returns a fresh store handle, shared by every SAME-SCOPE registration of
|
|
54
|
+
* one apply().
|
|
55
|
+
*/
|
|
56
|
+
export function createNavStore() {
|
|
57
|
+
return defineStore({
|
|
58
|
+
init: () => ({ view: 'home' as MobileView, workspace: null as WorkspaceFilter }),
|
|
59
|
+
actions: {
|
|
60
|
+
/**
|
|
61
|
+
* Move the page stack.
|
|
62
|
+
* @param draft - store draft.
|
|
63
|
+
* @param view - target level.
|
|
64
|
+
*/
|
|
65
|
+
show: (draft, view: MobileView) => {
|
|
66
|
+
draft.view = view
|
|
67
|
+
},
|
|
68
|
+
/**
|
|
69
|
+
* Pin the session-list workspace filter.
|
|
70
|
+
* @param draft - store draft.
|
|
71
|
+
* @param workspace - workspace id, or 'all'.
|
|
72
|
+
*/
|
|
73
|
+
filter: (draft, workspace: WorkspaceFilter) => {
|
|
74
|
+
draft.workspace = workspace
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
}
|