dsh-zen-remote 1.1.11 → 1.1.13
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 +24 -0
- package/README.md +7 -3
- package/README.zh-CN.md +7 -3
- package/docs/interface.md +5 -2
- package/dsh-push.mjs +25 -1
- package/lan-gate.mjs +16 -1
- package/lib/client.js +684 -43
- package/lib/lan-gate-server.cjs +107 -4
- package/lib/types/client/MobileHome.d.ts +7 -2
- package/lib/types/client/MobileHome.d.ts.map +1 -1
- package/lib/types/client/MobileSessionInfo.d.ts +2 -2
- package/lib/types/client/MobileSessionInfo.d.ts.map +1 -1
- package/lib/types/client/compat/store.d.ts +62 -0
- package/lib/types/client/compat/store.d.ts.map +1 -0
- package/lib/types/client/compat/types.d.ts +84 -0
- package/lib/types/client/compat/types.d.ts.map +1 -0
- package/lib/types/client/debug.d.ts +1 -1
- package/lib/types/client/debug.d.ts.map +1 -1
- package/lib/types/client/effects/aionui-compat.d.ts +1 -1
- package/lib/types/client/effects/aionui-compat.d.ts.map +1 -1
- package/lib/types/client/effects/gestures.d.ts +1 -1
- package/lib/types/client/effects/gestures.d.ts.map +1 -1
- package/lib/types/client/effects/header-status.d.ts +1 -1
- package/lib/types/client/effects/header-status.d.ts.map +1 -1
- package/lib/types/client/effects/keyboard-avoid.d.ts +1 -1
- package/lib/types/client/effects/keyboard-avoid.d.ts.map +1 -1
- package/lib/types/client/effects/keyboard-guard.d.ts +1 -1
- package/lib/types/client/effects/keyboard-guard.d.ts.map +1 -1
- package/lib/types/client/effects/modal-back.d.ts +1 -1
- package/lib/types/client/effects/modal-back.d.ts.map +1 -1
- package/lib/types/client/effects/model-sheet-extras.d.ts +33 -0
- package/lib/types/client/effects/model-sheet-extras.d.ts.map +1 -0
- package/lib/types/client/effects/native-trigger-overlay.d.ts +1 -1
- package/lib/types/client/effects/native-trigger-overlay.d.ts.map +1 -1
- package/lib/types/client/effects/phone-chrome.d.ts +1 -1
- package/lib/types/client/effects/phone-chrome.d.ts.map +1 -1
- package/lib/types/client/effects/turn-fold.d.ts +1 -1
- package/lib/types/client/effects/turn-fold.d.ts.map +1 -1
- package/lib/types/client/effects/welcome-notice.d.ts +1 -1
- package/lib/types/client/effects/welcome-notice.d.ts.map +1 -1
- package/lib/types/client/effects/workbench-ref-close.d.ts +1 -1
- package/lib/types/client/effects/workbench-ref-close.d.ts.map +1 -1
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/nav-store.d.ts +2 -2
- package/lib/types/client/nav-store.d.ts.map +1 -1
- package/lib/types/client/session-dot.d.ts +11 -3
- package/lib/types/client/session-dot.d.ts.map +1 -1
- package/lib/types/client/styles/compat.css.d.ts +1 -1
- package/lib/types/client/styles/compat.css.d.ts.map +1 -1
- package/lib/types/client/styles/composer.css.d.ts +1 -1
- package/lib/types/client/styles/composer.css.d.ts.map +1 -1
- package/lib/types/client/styles/info.css.d.ts +1 -1
- package/lib/types/client/styles/info.css.d.ts.map +1 -1
- package/package.json +49 -26
- package/pwa/app.css +47 -7
- package/scripts/check-client-externals.mjs +48 -0
- package/scripts/check-compat-store.mjs +113 -0
- package/scripts/check-host-shadow.mjs +145 -0
- package/scripts/check-info-sheet-stacking.mjs +55 -0
- package/src/client/MobileAttachButton.tsx +1 -1
- package/src/client/MobileHome.tsx +33 -7
- package/src/client/MobileSessionInfo.tsx +6 -4
- package/src/client/chips-store.ts +1 -1
- package/src/client/compat/store.ts +205 -0
- package/src/client/compat/types.ts +115 -0
- package/src/client/debug.ts +1 -1
- package/src/client/effects/aionui-compat.ts +1 -1
- package/src/client/effects/gestures.ts +1 -1
- package/src/client/effects/header-status.ts +22 -3
- package/src/client/effects/keyboard-avoid.ts +1 -1
- package/src/client/effects/keyboard-guard.ts +1 -1
- package/src/client/effects/modal-back.ts +1 -1
- package/src/client/effects/model-sheet-extras.ts +159 -0
- package/src/client/effects/native-trigger-overlay.ts +1 -1
- package/src/client/effects/phone-chrome.ts +1 -1
- package/src/client/effects/turn-fold.ts +1 -1
- package/src/client/effects/welcome-notice.ts +1 -1
- package/src/client/effects/workbench-ref-close.ts +1 -1
- package/src/client/index.tsx +70 -6
- package/src/client/nav-store.ts +2 -2
- package/src/client/session-dot.ts +13 -4
- package/src/client/styles/compat.css.ts +43 -11
- package/src/client/styles/composer.css.ts +184 -9
- package/src/client/styles/info.css.ts +21 -0
|
@@ -7,8 +7,9 @@ import {
|
|
|
7
7
|
IconDownloadOutline16,
|
|
8
8
|
IconEditOutline16,
|
|
9
9
|
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
10
|
-
import { workspaceTitleOf } from '
|
|
11
|
-
import
|
|
10
|
+
import { workspaceTitleOf } from './compat/store.ts'
|
|
11
|
+
import { agentPresetOf } from './compat/types.ts'
|
|
12
|
+
import type { RenameResult, SessionId } from './compat/types.ts'
|
|
12
13
|
import { NS } from './locales.ts'
|
|
13
14
|
import { GO_HOME_EVENT, SESSION_INFO_EVENT } from './nav-store.ts'
|
|
14
15
|
import { hasLayer, popLayer, pushLayer } from './history-nav.ts'
|
|
@@ -36,7 +37,7 @@ export type MobileSessionInfoProps =
|
|
|
36
37
|
/** Bound ctx.sessions.open(id) — lands on the freshly forked session. */
|
|
37
38
|
openSession: (id: SessionId) => void
|
|
38
39
|
/** Bound ctx.sessions.binding(id)?.session.rename(title); undefined when the binding is gone. */
|
|
39
|
-
renameSession: (sessionId: SessionId, title: string) =>
|
|
40
|
+
renameSession: (sessionId: SessionId, title: string) => RenameResult | undefined
|
|
40
41
|
/** Bound ctx.workspaces.archiveSession(sessionId). */
|
|
41
42
|
archiveSession: (sessionId: SessionId) => Promise<void>
|
|
42
43
|
/** Bound ctx.sessionLogDownload.download(sessionId) — owns its own progress/result modal. */
|
|
@@ -198,6 +199,7 @@ export function MobileSessionInfo({
|
|
|
198
199
|
const cacheHit = usage === undefined ? null : cacheHitPercent(usage)
|
|
199
200
|
const tokensEmpty = usage === undefined || (billedInputTokens(usage) === 0 && usage.outputTokens === 0)
|
|
200
201
|
const cwd = row?.cwd === undefined || row.cwd === '' ? undefined : workspaceTitleOf(row.cwd)
|
|
202
|
+
const preset = agentPresetOf(row)
|
|
201
203
|
|
|
202
204
|
const cells: Array<{ label: string; value: string; sub: string | undefined }> = [
|
|
203
205
|
{ label: t('infoStatTurns'), value: stats === undefined ? NA : String(stats.turns), sub: undefined },
|
|
@@ -279,7 +281,7 @@ export function MobileSessionInfo({
|
|
|
279
281
|
</div>
|
|
280
282
|
|
|
281
283
|
<div data-mobile-nav="info-badges">
|
|
282
|
-
{
|
|
284
|
+
{preset !== undefined && <span data-mobile-nav="info-badge">{preset}</span>}
|
|
283
285
|
{subagentCount > 0 && (
|
|
284
286
|
<span data-mobile-nav="info-badge">{t('infoSubagents', { count: subagentCount })}</span>
|
|
285
287
|
)}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// Compat: self-written store engine + workspace-title helper, replacing the
|
|
2
|
+
// three runtime values this plugin used to import from
|
|
3
|
+
// `@deepseek-ai/dsh-client-runtime/client` (createSnapshotStore in
|
|
4
|
+
// chips-store.ts, defineStore in nav-store.ts, workspaceTitleOf in
|
|
5
|
+
// MobileSessionInfo.tsx). DSH 0.1.2 dropped that package entirely: a browser
|
|
6
|
+
// `require` of it now throws and the plugin would not load at all. The store
|
|
7
|
+
// pair moved to `@deepseek-ai/dsh-client-store` and `workspaceTitleOf`
|
|
8
|
+
// exists nowhere, so instead of try/catching two package names at runtime
|
|
9
|
+
// (and re-coupling to whichever upstream renames next), the functions are
|
|
10
|
+
// written here — they are small, and their contract types come from
|
|
11
|
+
// `@deepseek-ai/dsh-client-ui-slots`, which both DSH versions ship.
|
|
12
|
+
//
|
|
13
|
+
// Both frameworks consume a store handle structurally — they call
|
|
14
|
+
// `handle.create(scopeKey?)` and read `getSnapshot / subscribe / actions /
|
|
15
|
+
// clearPersisted` off the instance, with no `instanceof` and no brand check
|
|
16
|
+
// (0.1.1 dsh-client-runtime/lib/client.js:171,302; 0.1.2
|
|
17
|
+
// dsh-client-ui-renderer/lib/client.js:1341,1350) — so these stand-ins are
|
|
18
|
+
// accepted as-is by the slot engine on either version.
|
|
19
|
+
import type {
|
|
20
|
+
ActionsDecl,
|
|
21
|
+
BakedActions,
|
|
22
|
+
StoreHandle,
|
|
23
|
+
StoreInstance,
|
|
24
|
+
StoreSpec,
|
|
25
|
+
} from '@deepseek-ai/dsh-client-ui-slots'
|
|
26
|
+
|
|
27
|
+
/** Writable snapshot source: read + subscribe + mutate/wholesale-set. */
|
|
28
|
+
export interface SnapshotStore<T> {
|
|
29
|
+
/** Read the current state. Same reference every time until an update. */
|
|
30
|
+
getSnapshot(): T
|
|
31
|
+
/**
|
|
32
|
+
* Subscribe to state changes.
|
|
33
|
+
* @param fn - invalidation callback.
|
|
34
|
+
* @returns unsubscribe function.
|
|
35
|
+
*/
|
|
36
|
+
subscribe(fn: () => void): () => void
|
|
37
|
+
/**
|
|
38
|
+
* Mutate the state through a draft.
|
|
39
|
+
* @param mutator - draft mutator.
|
|
40
|
+
*/
|
|
41
|
+
update(mutator: (draft: T) => void): void
|
|
42
|
+
/** Replace the state wholesale. @param next - next state. */
|
|
43
|
+
set(next: T): void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create a snapshot store with optional whole-value localStorage persistence.
|
|
48
|
+
* The 0.1.1 `opts.flush: 'raf' | 'sync'` mode is deliberately not
|
|
49
|
+
* implemented: this plugin never used it (chips-store.ts persists
|
|
50
|
+
* synchronously and React reads via useSyncExternalStore).
|
|
51
|
+
* @param init - initial state.
|
|
52
|
+
* @param opts - optional persistence key.
|
|
53
|
+
* @returns the store.
|
|
54
|
+
*/
|
|
55
|
+
export function createSnapshotStore<T>(
|
|
56
|
+
init: T,
|
|
57
|
+
opts?: { persist?: { name: string } },
|
|
58
|
+
): SnapshotStore<T> {
|
|
59
|
+
let state = init
|
|
60
|
+
const listeners = new Set<() => void>()
|
|
61
|
+
const persistName = opts?.persist?.name
|
|
62
|
+
|
|
63
|
+
// Rehydrate once at construction: a persisted whole value replaces the
|
|
64
|
+
// initial state wholesale. Failures (quota, private mode, corrupted JSON)
|
|
65
|
+
// only log — a broken persisted value must not take the store down, and a
|
|
66
|
+
// store that cannot persist must still work in memory.
|
|
67
|
+
if (persistName !== undefined && typeof localStorage !== 'undefined') {
|
|
68
|
+
try {
|
|
69
|
+
const raw = localStorage.getItem(persistName)
|
|
70
|
+
if (raw !== null) state = JSON.parse(raw) as T
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(`snapshot store '${persistName}' rehydration failed:`, error)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const persist = (): void => {
|
|
77
|
+
if (persistName === undefined || typeof localStorage === 'undefined') return
|
|
78
|
+
try {
|
|
79
|
+
localStorage.setItem(persistName, JSON.stringify(state))
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error(`snapshot store '${persistName}' persistence failed:`, error)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const notify = (): void => {
|
|
86
|
+
// Iterate a snapshot of the set so a callback that unsubscribes cannot
|
|
87
|
+
// make the loop skip listeners registered after it.
|
|
88
|
+
for (const fn of [...listeners]) fn()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
getSnapshot: (): T => state,
|
|
93
|
+
subscribe: (fn: () => void): (() => void) => {
|
|
94
|
+
listeners.add(fn)
|
|
95
|
+
return () => {
|
|
96
|
+
listeners.delete(fn)
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
update: (mutator: (draft: T) => void): void => {
|
|
100
|
+
// ponytail: official uses immer's structural sharing; here we copy one
|
|
101
|
+
// level shallowly (array spread / object spread) and let the mutator
|
|
102
|
+
// edit that copy before swapping it in wholesale. That only supports
|
|
103
|
+
// one-level-deep draft writes — fine while both plugin stores stay
|
|
104
|
+
// flat ({view, workspace}, Record<string, boolean>); if a state ever
|
|
105
|
+
// nests, switch this to immer instead of deepening the copy.
|
|
106
|
+
const draft = (Array.isArray(state) ? [...state] : { ...(state as object) }) as T
|
|
107
|
+
mutator(draft)
|
|
108
|
+
state = draft
|
|
109
|
+
notify()
|
|
110
|
+
persist()
|
|
111
|
+
},
|
|
112
|
+
set: (next: T): void => {
|
|
113
|
+
state = next
|
|
114
|
+
notify()
|
|
115
|
+
persist()
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A live `create()` product: the ui-slots contract instance plus the raw
|
|
122
|
+
* snapshot store it was built on. The official engine instance carries the
|
|
123
|
+
* same `store` member (dsh-client-store's EngineStoreInstance); the
|
|
124
|
+
* framework-neutral `StoreInstance` contract omits it, so this file widens
|
|
125
|
+
* the return of {@link defineStore}'s create with a local subtype.
|
|
126
|
+
*/
|
|
127
|
+
interface SnapshotStoreInstance<T, A extends ActionsDecl<T>> extends StoreInstance<T, A> {
|
|
128
|
+
/** The underlying snapshot store. */
|
|
129
|
+
readonly store: SnapshotStore<T>
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Declare a store: an init lambda (fresh state per instance), an optional
|
|
134
|
+
* persistence key, and the full write set as pure draft mutators. Returns
|
|
135
|
+
* the registration currency of the slot system's store seat.
|
|
136
|
+
*
|
|
137
|
+
* The `A & ActionsDecl<T>` actions position is load-bearing: T resolves from
|
|
138
|
+
* `init` in the first inference round, and the intersection then
|
|
139
|
+
* contextually types each mutator's draft parameter, so call sites write
|
|
140
|
+
* `(draft, view: MobileView) => { … }` without annotating the draft. Keep
|
|
141
|
+
* the intersection — dropping it makes nav-store.ts fail to typecheck.
|
|
142
|
+
*
|
|
143
|
+
* Persist-key rules mirror the official defineStore: `decl.persist` alone at
|
|
144
|
+
* root scope, suffixed with `.{scopeKey}` per session scope, none when
|
|
145
|
+
* `decl.persist` is absent.
|
|
146
|
+
* @param decl - init/persist/actions declaration.
|
|
147
|
+
* @returns the store handle.
|
|
148
|
+
*/
|
|
149
|
+
export function defineStore<T, A extends ActionsDecl<T>>(
|
|
150
|
+
decl: StoreSpec<T, A> & { actions: A & ActionsDecl<T> },
|
|
151
|
+
): StoreHandle<T, A> {
|
|
152
|
+
return {
|
|
153
|
+
spec: decl,
|
|
154
|
+
create(scopeKey?: string): SnapshotStoreInstance<T, A> {
|
|
155
|
+
const persistKey =
|
|
156
|
+
decl.persist === undefined
|
|
157
|
+
? undefined
|
|
158
|
+
: scopeKey === undefined
|
|
159
|
+
? decl.persist
|
|
160
|
+
: `${decl.persist}.${scopeKey}`
|
|
161
|
+
const store = createSnapshotStore<T>(
|
|
162
|
+
decl.init(),
|
|
163
|
+
persistKey !== undefined ? { persist: { name: persistKey } } : undefined,
|
|
164
|
+
)
|
|
165
|
+
// TS cannot verify an object that is filled key-by-key from
|
|
166
|
+
// Object.keys(decl.actions) satisfies the BakedActions mapped type, so
|
|
167
|
+
// the map is built loosely and narrowed once at the contract boundary;
|
|
168
|
+
// at runtime every declared action key is present (the declaration's
|
|
169
|
+
// own keys are the ones iterated).
|
|
170
|
+
const actions = {} as Record<string, (...params: unknown[]) => void>
|
|
171
|
+
for (const [key, mutate] of Object.entries(decl.actions)) {
|
|
172
|
+
actions[key] = (...params: unknown[]): void => {
|
|
173
|
+
store.update((draft) => {
|
|
174
|
+
mutate(draft, ...params)
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
actions: actions as BakedActions<T, A>,
|
|
180
|
+
getSnapshot: (): T => store.getSnapshot(),
|
|
181
|
+
subscribe: (fn: () => void): (() => void) => store.subscribe(fn),
|
|
182
|
+
store,
|
|
183
|
+
clearPersisted: (): void => {
|
|
184
|
+
if (persistKey === undefined || typeof localStorage === 'undefined') return
|
|
185
|
+
try {
|
|
186
|
+
localStorage.removeItem(persistKey)
|
|
187
|
+
} catch {
|
|
188
|
+
// Storage gone or denied: nothing to clean up anyway.
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Last path segment of a workspace directory, for showing "which workspace
|
|
198
|
+
* is this session in" on the mobile info card. 0.1.1's one-line
|
|
199
|
+
* implementation, kept verbatim.
|
|
200
|
+
* @param cwd - a workspace path (posix or windows separators).
|
|
201
|
+
* @returns the trailing name, or '' when no non-empty segment exists.
|
|
202
|
+
*/
|
|
203
|
+
export function workspaceTitleOf(cwd: string): string {
|
|
204
|
+
return cwd.replace(/[/\\]+$/, '').split(/[/\\]/).pop() ?? ''
|
|
205
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Compat: type re-homing + a structural session-row mirror.
|
|
2
|
+
//
|
|
3
|
+
// Most of src/client used to `import type { … } from
|
|
4
|
+
// '@deepseek-ai/dsh-client-runtime/client'`, a package DSH 0.1.2 removed.
|
|
5
|
+
// Type-only imports vanish at runtime, so each moved type just needs a new
|
|
6
|
+
// home; keeping those homes here concentrates every import-path swap in one
|
|
7
|
+
// file, and a future upstream rename touches only this file again.
|
|
8
|
+
//
|
|
9
|
+
// The one type that cannot be imported from either version is the session
|
|
10
|
+
// row: 0.1.1 declares `SessionSummary` in dsh-client-runtime, 0.1.2 in
|
|
11
|
+
// dsh-api-session-controller, and the two field sets differ (0.1.2 dropped
|
|
12
|
+
// `agentPreset` and `pendingInteraction`). Mirroring the handful of fields
|
|
13
|
+
// this plugin actually reads as a structural type (the same trick
|
|
14
|
+
// dsh-plugin-subscriptions' upstream compat.ts uses for RpcResult) lets rows
|
|
15
|
+
// from both versions assign in, instead of picking one version's type and
|
|
16
|
+
// breaking the other.
|
|
17
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
18
|
+
// Canonical homes for the two branded ids are the packages' `./types`
|
|
19
|
+
// subpaths. This tsconfig still resolves with node10 `moduleResolution:
|
|
20
|
+
// "node"`, which ignores package.json `exports` — the subpath only resolves
|
|
21
|
+
// because tsconfig.client.json maps it through `paths` to the exact .d.ts
|
|
22
|
+
// files; keep those two entries in sync if either package moves.
|
|
23
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
|
24
|
+
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Browser-half cordis context. 0.1.1's `dsh-client-runtime` exported
|
|
28
|
+
* `ClientContext` as the bare `Context` type (lib/types/client/index.d.ts:55);
|
|
29
|
+
* 0.1.2 has no such alias. Re-declared here under the same name so the
|
|
30
|
+
* dozen files that used it only change their import path, not the
|
|
31
|
+
* identifier.
|
|
32
|
+
*/
|
|
33
|
+
export type ClientContext = Context
|
|
34
|
+
|
|
35
|
+
/** Session id. Both versions ship it in `@deepseek-ai/dsh-session/types`; re-exported to collect import points. */
|
|
36
|
+
export type { SessionId }
|
|
37
|
+
|
|
38
|
+
/** Workspace id. 0.1.2 lives in `@deepseek-ai/dsh-workspace/types`. */
|
|
39
|
+
export type { WorkspaceId }
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Structural mirror of the session-row fields this plugin reads, valid
|
|
43
|
+
* against both versions' `SessionSummary`. Deliberately NOT imported from
|
|
44
|
+
* either version (see the file header): `projectionValues` stays `unknown`
|
|
45
|
+
* rather than copying the host's
|
|
46
|
+
* `Readonly<Partial<SessionProjectionMap>>` — that type differs between
|
|
47
|
+
* versions and fights assignments under `exactOptionalPropertyTypes`. The
|
|
48
|
+
* single read site (MobileHome.tsx:311) already casts.
|
|
49
|
+
*
|
|
50
|
+
* `agentPreset` and `pendingInteraction` are intentionally absent: their
|
|
51
|
+
* real readers (MobileHome.tsx, MobileSessionInfo.tsx) receive rows typed by
|
|
52
|
+
* the host's devDeps (0.1.2), not by this mirror, so fields here would not
|
|
53
|
+
* help them — use {@link agentPresetOf} / {@link hasPendingInteraction}
|
|
54
|
+
* instead.
|
|
55
|
+
*/
|
|
56
|
+
export interface MobileSessionRow {
|
|
57
|
+
id: SessionId
|
|
58
|
+
title?: string
|
|
59
|
+
displayTitle: string
|
|
60
|
+
cwd?: string
|
|
61
|
+
parentId?: SessionId
|
|
62
|
+
origin?: 'subagent'
|
|
63
|
+
running: boolean
|
|
64
|
+
completed?: boolean
|
|
65
|
+
blank: boolean
|
|
66
|
+
updatedAt: number
|
|
67
|
+
/** 宿主投影值;本插件只读 sessionStats.turns,读的地方自己 cast。 */
|
|
68
|
+
projectionValues?: unknown
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 0.1.1's session row carries `agentPreset`; 0.1.2 dropped it. Reading it
|
|
73
|
+
* must not depend on which version's types are in scope in the current
|
|
74
|
+
* file, so narrow through unknown.
|
|
75
|
+
* @param row - a session row of either version.
|
|
76
|
+
* @returns the agent preset name, or undefined when absent.
|
|
77
|
+
*/
|
|
78
|
+
export function agentPresetOf(row: unknown): string | undefined {
|
|
79
|
+
const agentPreset = (row as { agentPreset?: unknown } | null | undefined)?.agentPreset
|
|
80
|
+
return typeof agentPreset === 'string' ? agentPreset : undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 0.1.1's session row carries `pendingInteraction` (the pending-interaction
|
|
85
|
+
* state behind the sidebar's yellow dot); 0.1.2 serves it from the
|
|
86
|
+
* uiSession service instead. This answers only "does this row carry a
|
|
87
|
+
* pending interaction".
|
|
88
|
+
* @param row - a session row of either version.
|
|
89
|
+
* @returns true when the row has a pending interaction.
|
|
90
|
+
*/
|
|
91
|
+
export function hasPendingInteraction(row: unknown): boolean {
|
|
92
|
+
const pending = (row as { pendingInteraction?: unknown } | null | undefined)?.pendingInteraction
|
|
93
|
+
return pending !== undefined && pending !== null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* `ctx.sessions.binding(id)?.session.rename(title)` 的返回值。
|
|
98
|
+
* 两版的包裹类型不同(0.1.1 是 RpcResult、0.1.2 是 RemoteResult),但判别式
|
|
99
|
+
* 联合的形状一致:成功分支带 ok:true(还有个我们不读的 value),失败分支带
|
|
100
|
+
* ok:false + error。这里只镜像本插件真正读到的两个字段(ok、error.message),
|
|
101
|
+
* 两版的真类型都能结构赋值进来。
|
|
102
|
+
*/
|
|
103
|
+
export type RenameResult = Promise<
|
|
104
|
+
| { readonly ok: true }
|
|
105
|
+
| { readonly ok: false; readonly error: { readonly message: string } }
|
|
106
|
+
>
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 待处理交互的选择器 hook(0.1.2 的标准 prop `useSessionPendingInteraction`);
|
|
110
|
+
* 0.1.1 没有这个 prop,运行时读到 undefined。本地镜像只描述本插件读到的形状
|
|
111
|
+
* (按会话 id 的只读 Map + 选择器调用),不从
|
|
112
|
+
* `@deepseek-ai/dsh-client-ui-session/client` 拿类型——那个包 0.1.1 装不到。
|
|
113
|
+
*/
|
|
114
|
+
export type UsePendingInteractions =
|
|
115
|
+
<S>(sel: (m: ReadonlyMap<SessionId, unknown>) => S, eq?: (a: S, b: S) => boolean) => S
|
package/src/client/debug.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ClientContext } from '
|
|
1
|
+
import type { ClientContext } from '../compat/types.ts'
|
|
2
2
|
import { dotState } from '../session-dot.ts'
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -20,11 +20,30 @@ export function installHeaderStatusDot(ctx: ClientContext): void {
|
|
|
20
20
|
if (frame === null) return
|
|
21
21
|
const { current, byId } = ctx.sessions.list.getSnapshot()
|
|
22
22
|
const row = current === undefined ? undefined : byId[current]
|
|
23
|
-
|
|
23
|
+
// 0.1.2 的待处理交互来源,每次现查:本插件的 inject 不含 uiSession(加了
|
|
24
|
+
// 0.1.1 就不激活),apply 此刻服务未必注册好;每次算 dot 时现查,注册顺序
|
|
25
|
+
// 就不再是问题。0.1.1 上没有这个服务,这里恒 undefined,黄点只由行字段决定。
|
|
26
|
+
const uiSession = ctx.get('uiSession')
|
|
27
|
+
const pending = current === undefined ? false
|
|
28
|
+
: uiSession?.pendingInteractions.getSnapshot().has(current) === true
|
|
29
|
+
const state = row === undefined ? undefined : dotState(row, pending)
|
|
24
30
|
if (state === undefined) frame.removeAttribute('data-mobile-nav-dot')
|
|
25
31
|
else frame.setAttribute('data-mobile-nav-dot', state)
|
|
26
32
|
}
|
|
27
33
|
apply()
|
|
28
|
-
|
|
34
|
+
// Two sources can push the dot now (session list + the 0.1.2 pending
|
|
35
|
+
// map); probe uiSession once more right here and subscribe to whichever
|
|
36
|
+
// exist. 0.1.1 only ever has the first. Degradation note: if uiSession
|
|
37
|
+
// has not registered by this moment (this plugin ran before it), only
|
|
38
|
+
// the list subscription is attached — the dot stays correct because
|
|
39
|
+
// every later list change re-runs apply(), whose fresh probe then sees
|
|
40
|
+
// the service; a pending-only change before any list change would be
|
|
41
|
+
// missed until then.
|
|
42
|
+
const unsubscribes: Array<() => void> = [ctx.sessions.list.subscribe(apply)]
|
|
43
|
+
const pendingSource = ctx.get('uiSession')?.pendingInteractions
|
|
44
|
+
if (pendingSource !== undefined) unsubscribes.push(pendingSource.subscribe(apply))
|
|
45
|
+
return () => {
|
|
46
|
+
for (const off of unsubscribes) off()
|
|
47
|
+
}
|
|
29
48
|
}, 'dsh-mobile-nav: header status dot')
|
|
30
49
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ClientContext } from '
|
|
1
|
+
import type { ClientContext } from '../compat/types.ts'
|
|
2
2
|
import { clientConfig, KEYBOARD_DEFAULTS, type KeyboardTuning } from '../client-config.ts'
|
|
3
3
|
|
|
4
4
|
/** Phone breakpoint — same query every phone-only effect in this plugin uses. */
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* dialogs keep their own behaviour untouched (the repo's desktop no-op rule).
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import type { ClientContext } from '
|
|
21
|
+
import type { ClientContext } from '../compat/types.ts'
|
|
22
22
|
import { hasLayer, popLayer, pushLayer } from '../history-nav.ts'
|
|
23
23
|
|
|
24
24
|
const PHONE = '(max-width: 767px)'
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { ClientContext } from '../compat/types.ts'
|
|
2
|
+
|
|
3
|
+
/** Phone breakpoint — same query every phone-only effect in this plugin uses. */
|
|
4
|
+
const PHONE_QUERY = '(max-width: 767px)'
|
|
5
|
+
|
|
6
|
+
/** The model pill itself. Absent in a subagent session — see `sync`. */
|
|
7
|
+
const MODEL_SEAT_SELECTOR = '[data-slot="conversation.input.model"]'
|
|
8
|
+
|
|
9
|
+
/** The model pill's own popup, which section 4 of composer.css.ts turns into a bottom sheet. */
|
|
10
|
+
const MENU_SELECTOR = '[data-slot="conversation.input.model"] [class$="_menu"]'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The third-party composer controls this effect relocates. Both register into
|
|
14
|
+
* `conversation.input.right`; moving the slot container rather than its
|
|
15
|
+
* children keeps them together and leaves exactly one node to put back.
|
|
16
|
+
*/
|
|
17
|
+
const EXTRAS_SELECTOR = '[data-slot="conversation.input.right"]'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* dsh-vision-router's own mount marker. Used here only to answer "is there
|
|
21
|
+
* anything in this slot BESIDES the vision toggle" — see `worthMoving`.
|
|
22
|
+
*/
|
|
23
|
+
const VISION_SELECTOR = '[data-vision-router-mode-toggle]'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Set on the container while this effect manages it, in the row and in the
|
|
27
|
+
* sheet alike. It is the styling hook for both halves in composer.css.ts, and
|
|
28
|
+
* it is deliberately what the row's `display: none` keys off: without the
|
|
29
|
+
* marker the controls stay exactly where the host put them, so a version of
|
|
30
|
+
* this file that declines to act — or never runs at all — cannot leave them
|
|
31
|
+
* hidden in the row and absent from the sheet, i.e. unreachable.
|
|
32
|
+
*/
|
|
33
|
+
const MANAGED = 'data-zen-sheet-extras'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Is the slot holding anything that actually costs the row width?
|
|
37
|
+
*
|
|
38
|
+
* The row is one nowrap flex line whose only elastic item is the model pill
|
|
39
|
+
* (composer.css.ts section 1), so entries here are paid for out of the model
|
|
40
|
+
* name. But that is only a problem once something WIDE lands in the slot —
|
|
41
|
+
* today dsh-plugin-subscriptions' speed chip, which renders "速度 · 标准" and
|
|
42
|
+
* eats ~70px. dsh-vision-router's toggle alone is a 28px icon that the row
|
|
43
|
+
* accommodates fine, and moving it on its own would cost a tap (it becomes
|
|
44
|
+
* two: open the sheet, then toggle) to buy width nobody needed.
|
|
45
|
+
*
|
|
46
|
+
* So: relocate only when the slot holds an entry that is not the vision
|
|
47
|
+
* toggle. With no such entry — subscriptions not installed, or installed but
|
|
48
|
+
* not contributing on this model — nothing moves and the row keeps its one
|
|
49
|
+
* icon, which is the behaviour asked for (user review, 2026-09-06).
|
|
50
|
+
*
|
|
51
|
+
* Why this rather than sniffing for subscriptions by name: the slot system
|
|
52
|
+
* adds no per-registration marker (measured — entries render straight into the
|
|
53
|
+
* container) and subscriptions marks nothing of its own, so naming it would
|
|
54
|
+
* mean adding a marker to our fork of that plugin and anchoring on it. That
|
|
55
|
+
* marker dies the day the fork is dropped, and the feature would go quiet
|
|
56
|
+
* with nothing to catch it. The width question is the real question anyway,
|
|
57
|
+
* and this asks it directly.
|
|
58
|
+
*/
|
|
59
|
+
function worthMoving(extras: Element): boolean {
|
|
60
|
+
return [...extras.children].some(child => !child.matches(VISION_SELECTOR))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Phone: park the composer row's third-party controls inside the model sheet.
|
|
65
|
+
*
|
|
66
|
+
* With the speed chip and the vision toggle both present on a GPT model the
|
|
67
|
+
* row runs out of width. Both are model-scoped settings, so the model sheet —
|
|
68
|
+
* which already holds 模型 and 推理等级 — is where they belong (user request,
|
|
69
|
+
* 2026-09-06).
|
|
70
|
+
*
|
|
71
|
+
* Why move the real controls instead of drawing our own rows that drive them:
|
|
72
|
+
* the same reason native-trigger-overlay.ts moves the real trigger. A stand-in
|
|
73
|
+
* has to script the original, which is a road this plugin does not go down —
|
|
74
|
+
* beyond the trusted-input wall that effect documents, our own rows would have
|
|
75
|
+
* to mirror every piece of state (speed tier, disabled, aria-pressed, whether
|
|
76
|
+
* a vision twin exists for the current model) and would rot on the next
|
|
77
|
+
* upstream restyle. Moving the node keeps one source of truth: their React
|
|
78
|
+
* roots go on owning and updating these elements, we only change where they
|
|
79
|
+
* hang.
|
|
80
|
+
*
|
|
81
|
+
* Measured 2026-09-06 (DSH 0.1.2, 390px) before writing this — the two things
|
|
82
|
+
* that would have killed the approach both hold: a relocated control survives
|
|
83
|
+
* host re-renders (typing into the composer re-renders the row and does not
|
|
84
|
+
* yank it back) and stays hit-testable at its new position.
|
|
85
|
+
*
|
|
86
|
+
* Restoring on close is not cosmetic. The sheet unmounts when it closes and
|
|
87
|
+
* anything still inside goes with it, so the controls would vanish from the
|
|
88
|
+
* page while their React roots kept updating detached nodes. The observer
|
|
89
|
+
* therefore watches the menu leaving too, and the parked node is held as a
|
|
90
|
+
* reference rather than looked up — once the menu is detached a document
|
|
91
|
+
* query can no longer find it.
|
|
92
|
+
*/
|
|
93
|
+
export function installModelSheetExtras(ctx: ClientContext): void {
|
|
94
|
+
ctx.effect(() => {
|
|
95
|
+
const phone = window.matchMedia(PHONE_QUERY)
|
|
96
|
+
/** The container while parked in the sheet, plus where to put it back. */
|
|
97
|
+
let parked: { node: Element, parent: Element, next: Node | null } | null = null
|
|
98
|
+
|
|
99
|
+
const park = (node: Element, menu: Element): void => {
|
|
100
|
+
if (parked !== null || node.parentElement === null) return
|
|
101
|
+
parked = { node, parent: node.parentElement, next: node.nextSibling }
|
|
102
|
+
menu.appendChild(node)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const unpark = (): void => {
|
|
106
|
+
if (parked === null) return
|
|
107
|
+
const { node, parent, next } = parked
|
|
108
|
+
parked = null
|
|
109
|
+
// The menu may already be detached, having taken the container with it.
|
|
110
|
+
// The node itself is still live either way; re-home it only if its old
|
|
111
|
+
// parent is still on the page (when the whole composer went away, the
|
|
112
|
+
// owning React roots rebuild the slot on the next mount).
|
|
113
|
+
if (parent.isConnected) parent.insertBefore(node, next)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Hand the container back to the host, marker and all. */
|
|
117
|
+
const release = (node: Element | null): void => {
|
|
118
|
+
unpark()
|
|
119
|
+
node?.removeAttribute(MANAGED)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const sync = (): void => {
|
|
123
|
+
const extras = document.querySelector(EXTRAS_SELECTOR)
|
|
124
|
+
if (extras === null) { unpark(); return }
|
|
125
|
+
// No model pill, no sheet to park into — a subagent session has none.
|
|
126
|
+
// Claiming the slot there would hide the controls from the row with
|
|
127
|
+
// nowhere to show them instead, which is the unreachable state the
|
|
128
|
+
// marker is meant to prevent.
|
|
129
|
+
const seat = document.querySelector(MODEL_SEAT_SELECTOR)
|
|
130
|
+
if (!phone.matches || seat === null || !worthMoving(extras)) { release(extras); return }
|
|
131
|
+
extras.setAttribute(MANAGED, '')
|
|
132
|
+
const menu = document.querySelector(MENU_SELECTOR)
|
|
133
|
+
if (menu === null) unpark()
|
|
134
|
+
else park(extras, menu)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Synchronous in the observer callback, never behind rAF: the callback is
|
|
138
|
+
// a microtask and runs before this frame paints, so the controls are in
|
|
139
|
+
// the sheet the first time it is drawn. A rAF would land a frame late and
|
|
140
|
+
// show one frame of the sheet without them (AGENTS.md's "晚一帧" rule).
|
|
141
|
+
// Body-level childList is the same shape modal-back.ts uses; aria-expanded
|
|
142
|
+
// is watched too so a close that reuses the node still re-syncs.
|
|
143
|
+
const observer = new MutationObserver(sync)
|
|
144
|
+
observer.observe(document.body, {
|
|
145
|
+
childList: true,
|
|
146
|
+
subtree: true,
|
|
147
|
+
attributes: true,
|
|
148
|
+
attributeFilter: ['aria-expanded'],
|
|
149
|
+
})
|
|
150
|
+
phone.addEventListener('change', sync)
|
|
151
|
+
sync()
|
|
152
|
+
|
|
153
|
+
return () => {
|
|
154
|
+
observer.disconnect()
|
|
155
|
+
phone.removeEventListener('change', sync)
|
|
156
|
+
release(document.querySelector(EXTRAS_SELECTOR))
|
|
157
|
+
}
|
|
158
|
+
}, 'dsh-mobile-nav: model sheet extras')
|
|
159
|
+
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* tap can only ever reach the real thing — no double handling.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import type { ClientContext } from '
|
|
24
|
+
import type { ClientContext } from '../compat/types.ts'
|
|
25
25
|
|
|
26
26
|
const PHONE = '(max-width: 767px)'
|
|
27
27
|
const HEADER = '[data-phase] header'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ClientContext } from '
|
|
1
|
+
import type { ClientContext } from '../compat/types.ts'
|
|
2
2
|
|
|
3
3
|
/** Per-browser opt-out flag; localStorage because remote (non-loopback)
|
|
4
4
|
connections never persist the acknowledgement host-side. */
|