@linxin666/dsh-pet 0.1.19 → 0.1.20
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/README.i18n.yaml +2 -2
- package/README.md +3 -3
- package/README.zh.md +3 -3
- package/lib/client.js +54 -22
- package/lib/client.js.map +1 -1
- package/lib/index.js +44 -1
- package/lib/types/affinity.d.ts +18 -18
- package/lib/types/affinity.d.ts.map +1 -1
- package/lib/types/affinity.js +1 -1
- package/lib/types/client/PetDockEntry.d.ts +2 -0
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +2 -2
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +2 -2
- package/lib/types/client/PetSprite.d.ts +2 -0
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +17 -16
- package/lib/types/client/PluginSettingsCard.d.ts +1 -1
- package/lib/types/client/PluginSettingsCard.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/index.js +31 -6
- package/lib/types/client/locales.d.ts +82 -80
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +2 -0
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/spritesheet.d.ts +1 -1
- package/lib/types/client/spritesheet.d.ts.map +1 -1
- package/lib/types/client/spritesheet.js +3 -12
- package/lib/types/index.d.ts +2 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +15 -13
- package/lib/types/invariant.js +2 -2
- package/lib/types/ledger.d.ts.map +1 -1
- package/lib/types/ledger.js +3 -3
- package/lib/types/mount-once.d.ts +25 -0
- package/lib/types/mount-once.d.ts.map +1 -0
- package/lib/types/mount-once.js +42 -0
- package/lib/types/persist.js +3 -3
- package/lib/types/registry.js +1 -1
- package/lib/types/remarks.d.ts.map +1 -1
- package/lib/types/routes.js +2 -2
- package/lib/types/service.d.ts +10 -6
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +11 -6
- package/lib/types/state.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/PetDockEntry.tsx +3 -0
- package/src/client/PetSprite.test.tsx +56 -3
- package/src/client/PetSprite.tsx +26 -17
- package/src/client/index.ts +26 -3
- package/src/client/locales.ts +2 -0
- package/src/client/pet.module.css +35 -12
- package/src/client/spritesheet.ts +3 -13
- package/src/index.ts +4 -1
- package/src/mount-once.ts +48 -0
- package/src/service.ts +15 -7
package/src/client/index.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @module @linxin666/dsh-pet/client
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type { ClientContext, SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-runtime/client'
|
|
14
|
+
import type { ClientContext, ISessions, SessionId, SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-runtime/client'
|
|
15
15
|
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
|
|
16
16
|
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
17
17
|
// Type-only: pulls the settings-surface Context merge (ctx.settingsScope).
|
|
@@ -72,8 +72,8 @@ const POLL_MS = 2000
|
|
|
72
72
|
/** Settings namespace the pet settings card edits (the Host plugin registers it). */
|
|
73
73
|
const PET_SETTINGS_NS = 'pet'
|
|
74
74
|
|
|
75
|
-
/** Required services. */
|
|
76
|
-
export const inject = ['slots', 'locale', 'connection', 'settingsScope', 'remote']
|
|
75
|
+
/** Required services (sessions powers bubble-to-session navigation). */
|
|
76
|
+
export const inject = ['slots', 'locale', 'connection', 'settingsScope', 'remote', 'sessions']
|
|
77
77
|
|
|
78
78
|
/** Re-exported for consumers that type against the injected face. */
|
|
79
79
|
export type { PetInjected, PetDockEntryProps } from './PetDockEntry.tsx'
|
|
@@ -152,6 +152,11 @@ export function apply(ctx: ClientContext): void {
|
|
|
152
152
|
// tick tries again. After it lands, one list feeds both the sprite and
|
|
153
153
|
// the settings card's choices.
|
|
154
154
|
let petsLoaded = false
|
|
155
|
+
// Latest-wins guard: the 2s tick, visibility recovery, and
|
|
156
|
+
// interaction-triggered refreshes can overlap; only the newest
|
|
157
|
+
// response may publish, so a slow older one can never roll the
|
|
158
|
+
// snapshot back.
|
|
159
|
+
let stateSeq = 0
|
|
155
160
|
const pollNow = (): void => {
|
|
156
161
|
if (!petsLoaded) {
|
|
157
162
|
petApi.pets().then((list) => {
|
|
@@ -161,9 +166,13 @@ export function apply(ctx: ClientContext): void {
|
|
|
161
166
|
// Retry on the next poll tick.
|
|
162
167
|
})
|
|
163
168
|
}
|
|
169
|
+
const seq = stateSeq + 1
|
|
170
|
+
stateSeq = seq
|
|
164
171
|
petApi.state().then((snapshot) => {
|
|
172
|
+
if (seq !== stateSeq) return
|
|
165
173
|
setSnapshot(snapshot)
|
|
166
174
|
}, () => {
|
|
175
|
+
if (seq !== stateSeq) return
|
|
167
176
|
setState('error', 'pet.state transport error')
|
|
168
177
|
})
|
|
169
178
|
}
|
|
@@ -202,9 +211,23 @@ export function apply(ctx: ClientContext): void {
|
|
|
202
211
|
}
|
|
203
212
|
}, 'pet: poll')
|
|
204
213
|
|
|
214
|
+
// Clicking a session bubble jumps the GUI to that session. A bubble
|
|
215
|
+
// can outlive its disposed session by one poll tick, and the sessions
|
|
216
|
+
// service fails loud on unknown ids, so consult the live list first.
|
|
217
|
+
// The pet's type program also loads the host-side dsh-session package
|
|
218
|
+
// through the service types, whose Context merge declares a different
|
|
219
|
+
// 'sessions' face; pin the browser runtime's outward face here.
|
|
220
|
+
const sessions = ctx.sessions as unknown as ISessions
|
|
221
|
+
const openSession = (sessionId: string): void => {
|
|
222
|
+
const list = sessions.list.getSnapshot()
|
|
223
|
+
if (list.byId[sessionId as SessionId] === undefined) return
|
|
224
|
+
sessions.open(sessionId as SessionId)
|
|
225
|
+
}
|
|
226
|
+
|
|
205
227
|
const injected = (): PetInjected => ({
|
|
206
228
|
store: petStore,
|
|
207
229
|
ensure: pollNow,
|
|
230
|
+
openSession,
|
|
208
231
|
pet: () => {
|
|
209
232
|
petApi.interact('pet').then((result) => {
|
|
210
233
|
setFeedback({
|
package/src/client/locales.ts
CHANGED
|
@@ -19,6 +19,7 @@ export const zh = {
|
|
|
19
19
|
'pet.treats': '小鱼干 ×{n}',
|
|
20
20
|
'pet.state.loading': '宠物正在赶来…',
|
|
21
21
|
'pet.state.error': '宠物迷路了(连接失败)',
|
|
22
|
+
'pet.openSessionHint': '点击跳转到对应会话',
|
|
22
23
|
// 一级设置页(settings.section 席位)。
|
|
23
24
|
'settings.title': '宠物',
|
|
24
25
|
'settings.description': '选择宠物并调整它的显示布局。',
|
|
@@ -64,6 +65,7 @@ export const en = {
|
|
|
64
65
|
'pet.treats': 'Treats ×{n}',
|
|
65
66
|
'pet.state.loading': 'The pet is on its way…',
|
|
66
67
|
'pet.state.error': 'The pet is lost (connection failed)',
|
|
68
|
+
'pet.openSessionHint': 'Click to jump to this session',
|
|
67
69
|
// First-level settings section (the `settings.section` seat).
|
|
68
70
|
'settings.title': 'Pet',
|
|
69
71
|
'settings.description': 'Pick a pet and tune its display layout.',
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
|
|
49
49
|
/* 多会话气泡栈:并行会话各自一个气泡,自精灵头顶向上堆叠,
|
|
50
50
|
最近更新的会话在最上。栈内气泡改流入列布局,不再绝对定位
|
|
51
|
-
|
|
51
|
+
互相覆盖。气泡可点击:点击跳转到对应会话。 */
|
|
52
52
|
.bubbleStack {
|
|
53
53
|
position: absolute;
|
|
54
54
|
bottom: 100%;
|
|
@@ -56,13 +56,32 @@
|
|
|
56
56
|
flex-direction: column;
|
|
57
57
|
align-items: center;
|
|
58
58
|
gap: 4px;
|
|
59
|
-
pointer-events:
|
|
59
|
+
pointer-events: auto;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
.bubbleStack .bubble {
|
|
63
63
|
position: relative;
|
|
64
64
|
bottom: auto;
|
|
65
65
|
margin-bottom: 0;
|
|
66
|
+
pointer-events: auto;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.bubbleClickable {
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
font: inherit;
|
|
72
|
+
text-align: center;
|
|
73
|
+
appearance: none;
|
|
74
|
+
transition: filter 120ms ease, box-shadow 120ms ease;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.bubbleClickable:hover {
|
|
78
|
+
filter: brightness(1.12);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.bubbleClickable:focus-visible {
|
|
82
|
+
outline: none;
|
|
83
|
+
/* 2px accent ring on the pet's own palette, theme-agnostic. */
|
|
84
|
+
box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.85);
|
|
66
85
|
}
|
|
67
86
|
|
|
68
87
|
@keyframes pet-bubble-pop {
|
|
@@ -86,9 +105,14 @@
|
|
|
86
105
|
}
|
|
87
106
|
}
|
|
88
107
|
|
|
108
|
+
/* 悬浮面板位于精灵左侧(bottom 对齐),不再占据精灵上方的空间:
|
|
109
|
+
会话气泡栈独占精灵头顶区域,两者互不遮挡 —— 气泡因此可以始终
|
|
110
|
+
显示并保持可点击,不必在 hover 时让位给面板。 */
|
|
89
111
|
.panel {
|
|
90
112
|
position: absolute;
|
|
91
|
-
bottom:
|
|
113
|
+
bottom: 0;
|
|
114
|
+
right: 100%;
|
|
115
|
+
margin-right: 8px;
|
|
92
116
|
background: rgba(15, 23, 42, 0.92);
|
|
93
117
|
border: 1px solid rgba(148, 163, 184, 0.35);
|
|
94
118
|
border-radius: 10px;
|
|
@@ -103,18 +127,17 @@
|
|
|
103
127
|
min-width: 132px;
|
|
104
128
|
}
|
|
105
129
|
|
|
106
|
-
/*
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
使指针在图标与按钮之间始终停留在 hover 区域内。 */
|
|
130
|
+
/* 面板右缘与图标左缘之间的 hover 桥接(issue #49 的水平版):
|
|
131
|
+
面板在图标左侧留有 8px 间隙,指针滑过间隙时会触发容器的
|
|
132
|
+
pointerleave,面板会在指针抵达按钮前收起。这段透明伪元素把命中区
|
|
133
|
+
向右延伸、覆盖间隙,使指针在图标与按钮之间始终停留在 hover 区域。 */
|
|
111
134
|
.panel::after {
|
|
112
135
|
content: '';
|
|
113
136
|
position: absolute;
|
|
114
|
-
top:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
137
|
+
top: 0;
|
|
138
|
+
bottom: 0;
|
|
139
|
+
left: 100%;
|
|
140
|
+
width: 14px;
|
|
118
141
|
}
|
|
119
142
|
|
|
120
143
|
.rankRow {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module @linxin666/dsh-pet/client/spritesheet
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type
|
|
10
|
+
import { rowOf, type PetAnimation } from '../state.ts'
|
|
11
11
|
import type { PetCell, PetTrackDef } from '../registry.ts'
|
|
12
12
|
|
|
13
13
|
/** Animation track shape the frame loop consumes. */
|
|
@@ -15,18 +15,8 @@ export type TrackDef = PetTrackDef
|
|
|
15
15
|
|
|
16
16
|
/** Row index of one animation track (the fixed 9-row contract). */
|
|
17
17
|
export function rowOfTrack(animation: PetAnimation): number {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
'running-right': 1,
|
|
21
|
-
'running-left': 2,
|
|
22
|
-
waving: 3,
|
|
23
|
-
jumping: 4,
|
|
24
|
-
failed: 5,
|
|
25
|
-
waiting: 6,
|
|
26
|
-
running: 7,
|
|
27
|
-
review: 8,
|
|
28
|
-
}
|
|
29
|
-
return rows[animation]
|
|
18
|
+
// The table itself lives in state.ts (rowOf) — the single source of truth.
|
|
19
|
+
return rowOf(animation)
|
|
30
20
|
}
|
|
31
21
|
|
|
32
22
|
/**
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { PetService, PET_SETTINGS_NAMESPACE, type PetConfig, type PetSettingsSec
|
|
|
19
19
|
import { makePetRoutes } from './routes.ts'
|
|
20
20
|
import { loadPetRegistry, petPackageRoot } from './registry.ts'
|
|
21
21
|
import { DISPLAY_INSET_MAX, DISPLAY_SIZE_MAX, DISPLAY_SIZE_MIN } from './persist.ts'
|
|
22
|
+
import { mountOnce } from './mount-once.ts'
|
|
22
23
|
|
|
23
24
|
export { PetService, MAX_SESSION_BUBBLES } from './service.ts'
|
|
24
25
|
export type {
|
|
@@ -137,7 +138,9 @@ export function makePetSettingsSchema(fallbackPetId: string) {
|
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
/** Register the pet service and its API + asset routes on the context. */
|
|
140
|
-
export
|
|
141
|
+
export const apply = mountOnce('@linxin666/dsh-pet', applyImpl)
|
|
142
|
+
|
|
143
|
+
function applyImpl(ctx: Context, config: PetConfig = {}): void {
|
|
141
144
|
const registry = config.registry
|
|
142
145
|
?? loadPetRegistry({
|
|
143
146
|
packageRoot: petPackageRoot(import.meta.url),
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/host/mount-once.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
/**
|
|
3
|
+
* Host single-instance guard shared by the plugin family. The family bundle
|
|
4
|
+
* (dsh-web-ui-all / dsh-skins) namespaces every child row id (web-ui-*), so
|
|
5
|
+
* the loader accepts a standalone install of the same package side by side;
|
|
6
|
+
* without this guard the second instance would still re-register the same
|
|
7
|
+
* webserver routes, tools, settings namespaces, and system-prompt sections
|
|
8
|
+
* and fail the boot. mountOnce makes the second host apply a no-op for the
|
|
9
|
+
* lifetime of the first instance (the browser half is already deduped by
|
|
10
|
+
* package name in the client module host).
|
|
11
|
+
*
|
|
12
|
+
* The registry rides a global symbol so two module instances of the same
|
|
13
|
+
* package (npm copy vs repository link) still share one verdict. cordis
|
|
14
|
+
* `ctx.effect` runs its callback immediately and treats the callback's
|
|
15
|
+
* return value as the fiber disposer, so the unmarker is returned, not run.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const MOUNTED = Symbol.for('dsh-web-ui.mounted-plugins')
|
|
19
|
+
|
|
20
|
+
interface MountRegistry {
|
|
21
|
+
[MOUNTED]?: Set<string>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function mountedSet(): Set<string> {
|
|
25
|
+
const registry = globalThis as MountRegistry
|
|
26
|
+
return (registry[MOUNTED] ??= new Set())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Wrap a cordis plugin apply so the package runs at most once per process.
|
|
31
|
+
* The first mount registers normally and unmarks when its fiber disposes;
|
|
32
|
+
* any later mount of the same package name is a no-op.
|
|
33
|
+
* @param packageName - npm package identity shared by every install source.
|
|
34
|
+
* @param fn - the original plugin apply.
|
|
35
|
+
* @returns an apply of the same shape.
|
|
36
|
+
*/
|
|
37
|
+
export function mountOnce<T extends (...args: any[]) => unknown>(packageName: string, fn: T): T {
|
|
38
|
+
return ((...args: unknown[]) => {
|
|
39
|
+
const mounted = mountedSet()
|
|
40
|
+
if (mounted.has(packageName)) return
|
|
41
|
+
mounted.add(packageName)
|
|
42
|
+
const ctx = args[0] as { effect?: (effect: () => unknown) => unknown } | undefined
|
|
43
|
+
ctx?.effect?.(() => () => {
|
|
44
|
+
mounted.delete(packageName)
|
|
45
|
+
})
|
|
46
|
+
return fn(...args)
|
|
47
|
+
}) as T
|
|
48
|
+
}
|
package/src/service.ts
CHANGED
|
@@ -96,9 +96,12 @@ export interface PetSettingsSection {
|
|
|
96
96
|
export const PET_SETTINGS_NAMESPACE = 'pet'
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
|
-
* One active session as the pet displays it. Sessions run in
|
|
100
|
-
* each gets its own bubble while the sprite itself follows the
|
|
101
|
-
* meaningful event (the display session).
|
|
99
|
+
* One active TOP-LEVEL session as the pet displays it. Sessions run in
|
|
100
|
+
* parallel, so each gets its own bubble while the sprite itself follows the
|
|
101
|
+
* most recent meaningful event (the display session). Subagent children
|
|
102
|
+
* report no bubble of their own: their work is already reflected by the
|
|
103
|
+
* bubble of the conversation that spawned them, and the bubble buttons
|
|
104
|
+
* navigate to GUI sessions, which subagents are not.
|
|
102
105
|
*/
|
|
103
106
|
export interface PetSessionView {
|
|
104
107
|
/** Session identity (stringified for the wire; never exposed as a key). */
|
|
@@ -121,9 +124,10 @@ export interface PetStateView {
|
|
|
121
124
|
phase: PetStateSnapshot['phase']
|
|
122
125
|
sessionActive: boolean
|
|
123
126
|
/**
|
|
124
|
-
* Per-session bubbles for every concurrently active session,
|
|
125
|
-
* first; optional so older hosts without the multi-session
|
|
126
|
-
* consumable. The single 'bubble' above mirrors the display
|
|
127
|
+
* Per-session bubbles for every concurrently active TOP-LEVEL session,
|
|
128
|
+
* most recent first; optional so older hosts without the multi-session
|
|
129
|
+
* view stay consumable. The single 'bubble' above mirrors the display
|
|
130
|
+
* session.
|
|
127
131
|
*/
|
|
128
132
|
sessions?: PetSessionView[]
|
|
129
133
|
/** Affinity ledger snapshot. */
|
|
@@ -472,12 +476,16 @@ export class PetService extends Service {
|
|
|
472
476
|
private view(): PetStateView {
|
|
473
477
|
const snapshot = this.machine.render()
|
|
474
478
|
const entry = this.activeEntry()
|
|
475
|
-
// One bubble per concurrently active session, most recent
|
|
479
|
+
// One bubble per concurrently active TOP-LEVEL session, most recent
|
|
480
|
+
// first. Subagent children render no bubble of their own (their activity
|
|
481
|
+
// already shows through the spawning conversation's bubble/display, and
|
|
482
|
+
// the bubble buttons navigate to GUI sessions, which subagents are not).
|
|
476
483
|
// Sessions whose own machine has settled (no bubble copy) drop out, so a
|
|
477
484
|
// finished turn does not leave a stale bubble behind.
|
|
478
485
|
const sessions: PetSessionView[] = []
|
|
479
486
|
for (const [session, activity] of [...this.sessionActivity.entries()].reverse()) {
|
|
480
487
|
if (sessions.length >= MAX_SESSION_BUBBLES) break
|
|
488
|
+
if (session.header?.origin === 'subagent') continue
|
|
481
489
|
const perSession = activity.machine.render()
|
|
482
490
|
if (perSession.bubble === undefined) continue
|
|
483
491
|
sessions.push({
|