@linxin666/dsh-pet 0.1.7 → 0.1.8

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.
@@ -1,12 +1,12 @@
1
1
  /**
2
- * dsh-pet browser half — registers the whale-girl into the conversation
3
- * input selector row (the same every-phase row the git branch chip uses) and
4
- * drives it from the host's same-origin `/api/pet/*` JSON endpoints: poll the
5
- * host snapshot (~800 ms), forward interactions, persist drag positions. The
6
- * row anchor mounts the floating pet via portal; when the pet is hidden the
7
- * anchor becomes the summon button. Anchoring in the selector row (rather
8
- * than the session-only composer dock band) keeps the pet floating on the
9
- * new-conversation screen too, where no session exists to scope a slot by.
2
+ * dsh-pet browser half — mounts the whale-girl as a global floating surface
3
+ * and drives it from the host's same-origin `/api/pet/*` JSON endpoints: poll
4
+ * the host snapshot (~800 ms), forward interactions, persist drag positions.
5
+ * The pet is host-global (no session dimension), so it mounts directly onto
6
+ * `document.body` via a single React root rather than a session-scoped slot
7
+ * on the new-conversation screen no session exists, and a dock-mounted pet
8
+ * would vanish there (issue #48). When the pet is hidden the entry becomes a
9
+ * fixed-position summon button.
10
10
  * @module @linxin666/dsh-pet/client
11
11
  */
12
12
  import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
@@ -37,9 +37,9 @@ export interface SettingsPluginItemOwnerProps {
37
37
  children?: never;
38
38
  }
39
39
  /**
40
- * Client plugin body: register dictionaries, mount the dock entry and poll
41
- * loop while the plugin is enabled, and seat the settings card in the Web UI
42
- * plugin group.
40
+ * Client plugin body: register dictionaries, mount the global pet entry and
41
+ * poll loop while the plugin is enabled, and seat the settings card in the
42
+ * Web UI plugin group.
43
43
  * @param ctx - client root context.
44
44
  */
45
45
  export declare function apply(ctx: ClientContext): void;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAsD3E,yBAAyB;AACzB,eAAO,MAAM,MAAM,UAA+D,CAAA;AAElF,qEAAqE;AACrE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACxE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAEtF,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,OAAO;QACf;;;;;WAKG;QACH,oBAAoB,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,4BAA4B,CAAA;SAAE,CAAA;KAC3F;CACF;AAED,sEAAsE;AACtE,MAAM,WAAW,4BAA4B;IAC3C,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,KAAK,CAAA;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAmK9C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAwD3E,yBAAyB;AACzB,eAAO,MAAM,MAAM,UAA+D,CAAA;AAElF,qEAAqE;AACrE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACxE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAEtF,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,OAAO;QACf;;;;;WAKG;QACH,oBAAoB,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,4BAA4B,CAAA;SAAE,CAAA;KAC3F;CACF;AAED,sEAAsE;AACtE,MAAM,WAAW,4BAA4B;IAC3C,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,KAAK,CAAA;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAmK9C"}
@@ -1,18 +1,20 @@
1
1
  /**
2
- * dsh-pet browser half — registers the whale-girl into the conversation
3
- * input selector row (the same every-phase row the git branch chip uses) and
4
- * drives it from the host's same-origin `/api/pet/*` JSON endpoints: poll the
5
- * host snapshot (~800 ms), forward interactions, persist drag positions. The
6
- * row anchor mounts the floating pet via portal; when the pet is hidden the
7
- * anchor becomes the summon button. Anchoring in the selector row (rather
8
- * than the session-only composer dock band) keeps the pet floating on the
9
- * new-conversation screen too, where no session exists to scope a slot by.
2
+ * dsh-pet browser half — mounts the whale-girl as a global floating surface
3
+ * and drives it from the host's same-origin `/api/pet/*` JSON endpoints: poll
4
+ * the host snapshot (~800 ms), forward interactions, persist drag positions.
5
+ * The pet is host-global (no session dimension), so it mounts directly onto
6
+ * `document.body` via a single React root rather than a session-scoped slot
7
+ * on the new-conversation screen no session exists, and a dock-mounted pet
8
+ * would vanish there (issue #48). When the pet is hidden the entry becomes a
9
+ * fixed-position summon button.
10
10
  * @module @linxin666/dsh-pet/client
11
11
  */
12
+ import { createElement } from 'react';
13
+ import { createRoot } from 'react-dom/client';
12
14
  import { createPetStore } from "./pet-store.js";
13
15
  import { PetDockEntry } from "./PetDockEntry.js";
14
16
  import { PetSettingsCard, PetSettingsCardController } from "./PetSettingsCard.js";
15
- import { NS, en, zh } from "./locales.js";
17
+ import { NS, en, zh, t } from "./locales.js";
16
18
  /** Same-origin JSON fetch helper (GET without body, POST with JSON body). */
17
19
  async function petFetch(path, body) {
18
20
  const response = await fetch(path, body === undefined
@@ -42,9 +44,9 @@ const PET_SETTINGS_NS = 'pet';
42
44
  /** Required services. */
43
45
  export const inject = ['slots', 'locale', 'connection', 'settingsScope', 'remote'];
44
46
  /**
45
- * Client plugin body: register dictionaries, mount the dock entry and poll
46
- * loop while the plugin is enabled, and seat the settings card in the Web UI
47
- * plugin group.
47
+ * Client plugin body: register dictionaries, mount the global pet entry and
48
+ * poll loop while the plugin is enabled, and seat the settings card in the
49
+ * Web UI plugin group.
48
50
  * @param ctx - client root context.
49
51
  */
50
52
  export function apply(ctx) {
@@ -66,8 +68,8 @@ export function apply(ctx) {
66
68
  locale: NS,
67
69
  inject: () => petSettings.inject(),
68
70
  }, PetSettingsCard));
69
- // The dock entry, its store, and the poll loop live while the plugin is
70
- // enabled; toggling the setting off hides the pet and stops polling.
71
+ // The global pet entry, its store, and the poll loop live while the plugin
72
+ // is enabled; toggling the setting off hides the pet and stops polling.
71
73
  let disposeUi;
72
74
  const syncUi = () => {
73
75
  if (enabled() && disposeUi === undefined) {
@@ -179,20 +181,21 @@ export function apply(ctx) {
179
181
  setFeedback(null);
180
182
  },
181
183
  });
182
- // The legacy selector-context hole (conversation.input.selector.context)
183
- // is no longer declared by newer shells (rc.6 dropped it), so the pet
184
- // mounts on the composer dock band instead. The dock is session-scoped,
185
- // so the pet appears once a session is active (accepted downgrade, see
186
- // PR #17).
187
- const disposeDock = ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
188
- name: 'conversation.input.dock',
189
- id: 'pet',
190
- order: 110,
191
- inject: injected,
192
- locale: NS,
193
- }, PetDockEntry));
184
+ // The pet is host-global (its state/display/interactions have no session
185
+ // dimension), and the official rc.6 shell declares no root-scoped slot
186
+ // for a global floating surface the dock is session-scoped, so a pet
187
+ // mounted there would vanish on the new-conversation screen (issue #48).
188
+ // The entry therefore mounts straight onto document.body via a single
189
+ // React root for the page lifetime: WhalePet portals itself to body when
190
+ // visible, and the hidden-state summon button is fixed-positioned.
191
+ const container = document.createElement('div');
192
+ container.dataset.dshPetRoot = '';
193
+ document.body.appendChild(container);
194
+ const petRoot = createRoot(container);
195
+ petRoot.render(createElement(PetDockEntry, { ...injected(), t }));
194
196
  disposeUi = () => {
195
- disposeDock();
197
+ petRoot.unmount();
198
+ container.remove();
196
199
  disposePoll();
197
200
  disposeUi = undefined;
198
201
  };
@@ -92,6 +92,22 @@ export declare const en: {
92
92
  export type PetKey = keyof typeof zh;
93
93
  /** The settings-card slice of the pet dictionary. */
94
94
  export type SettingsCardKey = PetKey;
95
+ /**
96
+ * Active dictionary, picked by the document language at call time. The pet
97
+ * mounts as a global floating surface (not a session-scoped slot), so it has
98
+ * no framework locale seat and resolves its copy the same tiny way the
99
+ * task-board's DOM-injected surface does.
100
+ */
101
+ export declare function dictionary(): Record<PetKey, string>;
102
+ /**
103
+ * Translate a key with optional `{name}` template params. Mirrors the slot
104
+ * `Translate` contract `(key, params?) => string` so it can be handed to the
105
+ * same components that used to receive the framework-injected `t` seat. The
106
+ * key is typed loosely (`string`) so the function is assignable to the slot's
107
+ * `TranslateNS<'pet'>` (whose key domain also spans the shared common
108
+ * vocabulary); a missing key degrades to the key itself rather than throwing.
109
+ */
110
+ export declare function t(key: string, params?: Record<string, unknown>): string;
95
111
  declare module '@deepseek-ai/dsh-client-ui-slots' {
96
112
  interface LocaleNamespaceMap {
97
113
  /** dsh-pet UI copy. */
@@ -1 +1 @@
1
- {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mDAAmD;AACnD,eAAO,MAAM,EAAE,QAAQ,CAAA;AAEvB,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCL,CAAA;AAEV,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCL,CAAA;AAEV,oCAAoC;AACpC,MAAM,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,CAAA;AAEpC,qDAAqD;AACrD,MAAM,MAAM,eAAe,GAAG,MAAM,CAAA;AAEpC,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,uBAAuB;QACvB,GAAG,EAAE,MAAM,CAAA;KACZ;CACF"}
1
+ {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mDAAmD;AACnD,eAAO,MAAM,EAAE,QAAQ,CAAA;AAEvB,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCL,CAAA;AAEV,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCL,CAAA;AAEV,oCAAoC;AACpC,MAAM,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,CAAA;AAEpC,qDAAqD;AACrD,MAAM,MAAM,eAAe,GAAG,MAAM,CAAA;AAEpC;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGnD;AAED;;;;;;;GAOG;AACH,wBAAgB,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAQvE;AAED,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,uBAAuB;QACvB,GAAG,EAAE,MAAM,CAAA;KACZ;CACF"}
@@ -90,3 +90,30 @@ export const en = {
90
90
  'settings.saveFailed': 'The deployment did not accept these values; they were left for you to correct.',
91
91
  'settings.invalidNumber': 'Enter a number, or leave blank to use the default.',
92
92
  };
93
+ /**
94
+ * Active dictionary, picked by the document language at call time. The pet
95
+ * mounts as a global floating surface (not a session-scoped slot), so it has
96
+ * no framework locale seat and resolves its copy the same tiny way the
97
+ * task-board's DOM-injected surface does.
98
+ */
99
+ export function dictionary() {
100
+ const lang = typeof document !== 'undefined' ? document.documentElement.lang : 'zh';
101
+ return lang.toLowerCase().startsWith('en') ? en : zh;
102
+ }
103
+ /**
104
+ * Translate a key with optional `{name}` template params. Mirrors the slot
105
+ * `Translate` contract `(key, params?) => string` so it can be handed to the
106
+ * same components that used to receive the framework-injected `t` seat. The
107
+ * key is typed loosely (`string`) so the function is assignable to the slot's
108
+ * `TranslateNS<'pet'>` (whose key domain also spans the shared common
109
+ * vocabulary); a missing key degrades to the key itself rather than throwing.
110
+ */
111
+ export function t(key, params) {
112
+ let text = dictionary()[key] ?? key;
113
+ if (params !== undefined) {
114
+ for (const [name, value] of Object.entries(params)) {
115
+ text = text.replaceAll(`{${name}}`, String(value));
116
+ }
117
+ }
118
+ return text;
119
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-pet",
3
3
  "description": "鲸鱼娘宠物插件 for the dsh web GUI: a soft healing whale-girl companion that reacts to model activity (idle/waiting/thinking/tool/done), with petting/feeding interactions and an affinity score",
4
- "version": "0.1.7",
4
+ "version": "0.1.8",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -1,14 +1,16 @@
1
1
  /**
2
- * Dock anchor inside the official SDK's `conversation.input.dock` seat
3
- * (the ambient readout band above the composer card, listed so several
4
- * additions can stack). While visible it mounts the floating WhalePet
5
- * (portal); while hidden it renders the summon button.
2
+ * Global floating pet entry. The pet is host-global (its state, display and
3
+ * interactions live on `/api/pet/*` endpoints with no session dimension), so
4
+ * it must not ride a session-scoped slot — on the new-conversation screen no
5
+ * session exists to scope a slot by, and the pet would vanish (issue #48).
6
+ * The client half therefore mounts this entry straight onto `document.body`
7
+ * (see index.ts): while visible it renders the floating WhalePet (a portal),
8
+ * while hidden it renders a fixed-position summon button.
6
9
  * @module @linxin666/dsh-pet/client/PetDockEntry
7
10
  */
8
11
 
9
12
  import { useEffect, useSyncExternalStore, type ReactElement } from 'react'
10
- import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
11
- import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
13
+ import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
12
14
  import type { PetDisplayConfig } from '../persist.ts'
13
15
  import type { PetStoreInstance } from './pet-store.ts'
14
16
  import { WhalePet } from './WhalePet.tsx'
@@ -37,10 +39,9 @@ export interface PetInjected {
37
39
  feedbackDone: () => void
38
40
  }
39
41
 
40
- /** Composed props of the dock entry (runtime + locale + injected). */
42
+ /** Composed props of the global pet entry (locale + injected; no slot runtime share). */
41
43
  export type PetDockEntryProps =
42
- PropsRuntime<'conversation.input.dock'>
43
- & PetInjected
44
+ PetInjected
44
45
  & PropsLocale<typeof NS>
45
46
 
46
47
  const DEFAULT_DISPLAY: PetDisplayConfig = { visible: true, size: 160, right: 24, bottom: 20 }
@@ -81,10 +82,17 @@ export function PetDockEntry(props: PetDockEntryProps): ReactElement {
81
82
  </span>
82
83
  )
83
84
  }
85
+ const display = snapshot?.display ?? DEFAULT_DISPLAY
84
86
  return (
85
87
  <button
86
88
  type="button"
87
89
  className={styles.summon}
90
+ style={{
91
+ position: 'fixed',
92
+ right: display.right,
93
+ bottom: display.bottom,
94
+ zIndex: 2147483000,
95
+ }}
88
96
  onClick={props.summon}
89
97
  data-testid="pet-summon"
90
98
  >
@@ -243,11 +243,13 @@ export function WhalePet(props: WhalePetProps): ReactPortal {
243
243
  // above the sprite), so moving onto them fires pointerleave on the
244
244
  // container. Treat a target still inside the container's DOM (the
245
245
  // overflowed panel) as "still hovering"; otherwise give the pointer a
246
- // short grace period to reach the panel across the gap above it.
246
+ // short grace period to reach the panel across the gap above it. The
247
+ // bridge (`.panel::after`) keeps the pointer inside the hit area, and
248
+ // the grace period covers a slow mouse crossing the remaining sliver.
247
249
  const next = e.relatedTarget
248
250
  if (next instanceof Node && floatRef.current?.contains(next)) return
249
251
  clearHideTimer()
250
- hideTimerRef.current = window.setTimeout(() => setHovered(false), 200)
252
+ hideTimerRef.current = window.setTimeout(() => setHovered(false), 300)
251
253
  }}
252
254
  >
253
255
  <div
@@ -280,7 +282,15 @@ export function WhalePet(props: WhalePetProps): ReactPortal {
280
282
  </div>
281
283
  )}
282
284
  {hovered && dragRef.current === null && (
283
- <div className={styles.panel}>
285
+ <div
286
+ className={styles.panel}
287
+ onPointerEnter={() => {
288
+ // Reaching the panel (or its bridge) must cancel any hide timer
289
+ // the container's pointerleave may have armed while the pointer
290
+ // crossed the sliver between the sprite and the panel.
291
+ clearHideTimer()
292
+ }}
293
+ >
284
294
  {renaming ? (
285
295
  <div className={styles.renameRow}>
286
296
  <input
@@ -1,12 +1,12 @@
1
1
  /**
2
- * dsh-pet browser half — registers the whale-girl into the conversation
3
- * input selector row (the same every-phase row the git branch chip uses) and
4
- * drives it from the host's same-origin `/api/pet/*` JSON endpoints: poll the
5
- * host snapshot (~800 ms), forward interactions, persist drag positions. The
6
- * row anchor mounts the floating pet via portal; when the pet is hidden the
7
- * anchor becomes the summon button. Anchoring in the selector row (rather
8
- * than the session-only composer dock band) keeps the pet floating on the
9
- * new-conversation screen too, where no session exists to scope a slot by.
2
+ * dsh-pet browser half — mounts the whale-girl as a global floating surface
3
+ * and drives it from the host's same-origin `/api/pet/*` JSON endpoints: poll
4
+ * the host snapshot (~800 ms), forward interactions, persist drag positions.
5
+ * The pet is host-global (no session dimension), so it mounts directly onto
6
+ * `document.body` via a single React root rather than a session-scoped slot
7
+ * on the new-conversation screen no session exists, and a dock-mounted pet
8
+ * would vanish there (issue #48). When the pet is hidden the entry becomes a
9
+ * fixed-position summon button.
10
10
  * @module @linxin666/dsh-pet/client
11
11
  */
12
12
 
@@ -20,10 +20,12 @@ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
20
20
  import type { PetDisplayConfig } from '../persist.ts'
21
21
  import type { PetInteractResult, PetStateView } from '../service.ts'
22
22
  import type { PetInteraction } from '../affinity.ts'
23
+ import { createElement } from 'react'
24
+ import { createRoot } from 'react-dom/client'
23
25
  import { createPetStore, type PetStoreInstance } from './pet-store.ts'
24
26
  import { PetDockEntry, type PetInjected } from './PetDockEntry.tsx'
25
27
  import { PetSettingsCard, PetSettingsCardController, type PetSettings } from './PetSettingsCard.tsx'
26
- import { NS, en, zh } from './locales.ts'
28
+ import { NS, en, zh, t } from './locales.ts'
27
29
 
28
30
  /** The host pet API as the browser sees it (same-origin JSON endpoints). */
29
31
  interface PetHttpApi {
@@ -91,9 +93,9 @@ export interface SettingsPluginItemOwnerProps {
91
93
  }
92
94
 
93
95
  /**
94
- * Client plugin body: register dictionaries, mount the dock entry and poll
95
- * loop while the plugin is enabled, and seat the settings card in the Web UI
96
- * plugin group.
96
+ * Client plugin body: register dictionaries, mount the global pet entry and
97
+ * poll loop while the plugin is enabled, and seat the settings card in the
98
+ * Web UI plugin group.
97
99
  * @param ctx - client root context.
98
100
  */
99
101
  export function apply(ctx: ClientContext): void {
@@ -118,8 +120,8 @@ export function apply(ctx: ClientContext): void {
118
120
  inject: () => petSettings.inject(),
119
121
  }, PetSettingsCard))
120
122
 
121
- // The dock entry, its store, and the poll loop live while the plugin is
122
- // enabled; toggling the setting off hides the pet and stops polling.
123
+ // The global pet entry, its store, and the poll loop live while the plugin
124
+ // is enabled; toggling the setting off hides the pet and stops polling.
123
125
  let disposeUi: (() => void) | undefined
124
126
  const syncUi = (): void => {
125
127
  if (enabled() && disposeUi === undefined) {
@@ -233,22 +235,22 @@ export function apply(ctx: ClientContext): void {
233
235
  },
234
236
  })
235
237
 
236
- // The legacy selector-context hole (conversation.input.selector.context)
237
- // is no longer declared by newer shells (rc.6 dropped it), so the pet
238
- // mounts on the composer dock band instead. The dock is session-scoped,
239
- // so the pet appears once a session is active (accepted downgrade, see
240
- // PR #17).
241
- const disposeDock = ctx.slots.inject('conversation.input.dock', () =>
242
- ctx.slots.register({
243
- name: 'conversation.input.dock',
244
- id: 'pet',
245
- order: 110,
246
- inject: injected,
247
- locale: NS,
248
- }, PetDockEntry))
238
+ // The pet is host-global (its state/display/interactions have no session
239
+ // dimension), and the official rc.6 shell declares no root-scoped slot
240
+ // for a global floating surface the dock is session-scoped, so a pet
241
+ // mounted there would vanish on the new-conversation screen (issue #48).
242
+ // The entry therefore mounts straight onto document.body via a single
243
+ // React root for the page lifetime: WhalePet portals itself to body when
244
+ // visible, and the hidden-state summon button is fixed-positioned.
245
+ const container = document.createElement('div')
246
+ container.dataset.dshPetRoot = ''
247
+ document.body.appendChild(container)
248
+ const petRoot = createRoot(container)
249
+ petRoot.render(createElement(PetDockEntry, { ...injected(), t }))
249
250
 
250
251
  disposeUi = () => {
251
- disposeDock()
252
+ petRoot.unmount()
253
+ container.remove()
252
254
  disposePoll()
253
255
  disposeUi = undefined
254
256
  }
@@ -100,6 +100,35 @@ export type PetKey = keyof typeof zh
100
100
  /** The settings-card slice of the pet dictionary. */
101
101
  export type SettingsCardKey = PetKey
102
102
 
103
+ /**
104
+ * Active dictionary, picked by the document language at call time. The pet
105
+ * mounts as a global floating surface (not a session-scoped slot), so it has
106
+ * no framework locale seat and resolves its copy the same tiny way the
107
+ * task-board's DOM-injected surface does.
108
+ */
109
+ export function dictionary(): Record<PetKey, string> {
110
+ const lang = typeof document !== 'undefined' ? document.documentElement.lang : 'zh'
111
+ return lang.toLowerCase().startsWith('en') ? en : zh
112
+ }
113
+
114
+ /**
115
+ * Translate a key with optional `{name}` template params. Mirrors the slot
116
+ * `Translate` contract `(key, params?) => string` so it can be handed to the
117
+ * same components that used to receive the framework-injected `t` seat. The
118
+ * key is typed loosely (`string`) so the function is assignable to the slot's
119
+ * `TranslateNS<'pet'>` (whose key domain also spans the shared common
120
+ * vocabulary); a missing key degrades to the key itself rather than throwing.
121
+ */
122
+ export function t(key: string, params?: Record<string, unknown>): string {
123
+ let text: string = (dictionary() as Record<string, string>)[key] ?? key
124
+ if (params !== undefined) {
125
+ for (const [name, value] of Object.entries(params)) {
126
+ text = text.replaceAll(`{${name}}`, String(value))
127
+ }
128
+ }
129
+ return text
130
+ }
131
+
103
132
  declare module '@deepseek-ai/dsh-client-ui-slots' {
104
133
  interface LocaleNamespaceMap {
105
134
  /** dsh-pet UI copy. */
@@ -75,6 +75,20 @@
75
75
  min-width: 132px;
76
76
  }
77
77
 
78
+ /* 面板底边与图标顶边之间的 hover 桥接(issue #49):
79
+ 面板绝对定位在图标上方(bottom: 100%),若两者之间存在哪怕几个
80
+ 像素的间隙,指针滑过间隙时会触发容器的 pointerleave,面板在指针
81
+ 抵达按钮前就收起。这段透明伪元素把命中区向下延伸、覆盖图标顶部,
82
+ 使指针在图标与按钮之间始终停留在 hover 区域内。 */
83
+ .panel::after {
84
+ content: '';
85
+ position: absolute;
86
+ top: 100%;
87
+ left: 0;
88
+ right: 0;
89
+ height: 14px;
90
+ }
91
+
78
92
  .rankRow {
79
93
  display: flex;
80
94
  justify-content: space-between;