@linxin666/dsh-pet 0.1.19 → 0.2.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.
Files changed (95) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +19 -12
  3. package/README.zh.md +19 -12
  4. package/assets/whale/pet.json +9 -0
  5. package/lib/client.js +202 -38
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +897 -42
  8. package/lib/invariant.js +1 -1
  9. package/lib/{state-Diiq8vjk.js → state-Ch3f4luZ.js} +51 -23
  10. package/lib/types/affinity.d.ts +22 -18
  11. package/lib/types/affinity.d.ts.map +1 -1
  12. package/lib/types/affinity.js +32 -6
  13. package/lib/types/chatter.d.ts +111 -0
  14. package/lib/types/chatter.d.ts.map +1 -0
  15. package/lib/types/chatter.js +666 -0
  16. package/lib/types/client/PetDockEntry.d.ts +2 -0
  17. package/lib/types/client/PetDockEntry.d.ts.map +1 -1
  18. package/lib/types/client/PetDockEntry.js +2 -2
  19. package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
  20. package/lib/types/client/PetSettingsCard.js +2 -2
  21. package/lib/types/client/PetSprite.d.ts +2 -0
  22. package/lib/types/client/PetSprite.d.ts.map +1 -1
  23. package/lib/types/client/PetSprite.js +100 -25
  24. package/lib/types/client/PluginSettingsCard.d.ts +7 -1
  25. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  26. package/lib/types/client/PluginSettingsCard.js +5 -3
  27. package/lib/types/client/index.d.ts +1 -1
  28. package/lib/types/client/index.d.ts.map +1 -1
  29. package/lib/types/client/index.js +31 -6
  30. package/lib/types/client/locales.d.ts +82 -80
  31. package/lib/types/client/locales.d.ts.map +1 -1
  32. package/lib/types/client/locales.js +2 -0
  33. package/lib/types/client/sequences.d.ts +10 -0
  34. package/lib/types/client/sequences.d.ts.map +1 -0
  35. package/lib/types/client/sequences.js +20 -0
  36. package/lib/types/client/settings-form.d.ts.map +1 -1
  37. package/lib/types/client/spritesheet.d.ts +1 -1
  38. package/lib/types/client/spritesheet.d.ts.map +1 -1
  39. package/lib/types/client/spritesheet.js +3 -12
  40. package/lib/types/event-projection.d.ts +14 -1
  41. package/lib/types/event-projection.d.ts.map +1 -1
  42. package/lib/types/event-projection.js +38 -17
  43. package/lib/types/index.d.ts +2 -1
  44. package/lib/types/index.d.ts.map +1 -1
  45. package/lib/types/index.js +15 -13
  46. package/lib/types/invariant.js +2 -2
  47. package/lib/types/ledger.d.ts.map +1 -1
  48. package/lib/types/ledger.js +18 -10
  49. package/lib/types/mount-once.d.ts +25 -0
  50. package/lib/types/mount-once.d.ts.map +1 -0
  51. package/lib/types/mount-once.js +42 -0
  52. package/lib/types/persist.d.ts.map +1 -1
  53. package/lib/types/persist.js +6 -4
  54. package/lib/types/registry.d.ts +7 -1
  55. package/lib/types/registry.d.ts.map +1 -1
  56. package/lib/types/registry.js +37 -1
  57. package/lib/types/remarks.d.ts +2 -0
  58. package/lib/types/remarks.d.ts.map +1 -1
  59. package/lib/types/remarks.js +5 -0
  60. package/lib/types/routes.js +2 -2
  61. package/lib/types/service.d.ts +16 -6
  62. package/lib/types/service.d.ts.map +1 -1
  63. package/lib/types/service.js +26 -8
  64. package/lib/types/state.d.ts +7 -4
  65. package/lib/types/state.d.ts.map +1 -1
  66. package/lib/types/state.js +20 -20
  67. package/package.json +9 -9
  68. package/src/affinity.test.ts +4 -2
  69. package/src/affinity.ts +37 -6
  70. package/src/chatter.test.ts +154 -0
  71. package/src/chatter.ts +697 -0
  72. package/src/client/PetDockEntry.tsx +3 -0
  73. package/src/client/PetSprite.test.tsx +246 -4
  74. package/src/client/PetSprite.tsx +132 -36
  75. package/src/client/PluginSettingsCard.tsx +10 -0
  76. package/src/client/index.ts +26 -3
  77. package/src/client/locales.ts +2 -0
  78. package/src/client/pet-css.test.ts +42 -0
  79. package/src/client/pet.module.css +173 -22
  80. package/src/client/sequences.test.ts +33 -0
  81. package/src/client/sequences.ts +33 -0
  82. package/src/client/spritesheet.ts +3 -13
  83. package/src/event-projection.ts +49 -16
  84. package/src/index.ts +4 -1
  85. package/src/ledger.test.ts +42 -1
  86. package/src/ledger.ts +15 -7
  87. package/src/mount-once.ts +48 -0
  88. package/src/persist.test.ts +18 -1
  89. package/src/persist.ts +3 -1
  90. package/src/registry.test.ts +50 -3
  91. package/src/registry.ts +47 -3
  92. package/src/remarks.ts +6 -0
  93. package/src/service.ts +42 -9
  94. package/src/state.test.ts +13 -0
  95. package/src/state.ts +24 -18
@@ -12,7 +12,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
12
12
  * @module @linxin666/dsh-pet/client/PetDockEntry
13
13
  */
14
14
  import { useEffect, useSyncExternalStore } from 'react';
15
- import { PetSprite } from "./PetSprite.js";
15
+ import { PetSprite } from './PetSprite.js';
16
16
  import styles from './pet.module.css';
17
17
  const DEFAULT_DISPLAY = { visible: true, size: 160, right: 24, bottom: 20 };
18
18
  /**
@@ -35,7 +35,7 @@ export function PetDockEntry(props) {
35
35
  if (visible) {
36
36
  return (_jsx("span", { "data-pet-dock": true, "data-testid": "pet-dock", children: snapshot === null || definition === null
37
37
  ? null
38
- : (_jsx(PetSprite, { snapshot: snapshot, definition: definition, display: snapshot.display, feedback: feedback, onPet: props.pet, onFeed: props.feed, onHide: props.hide, onDragEnd: props.dragEnd, onRename: props.rename, onFeedbackDone: props.feedbackDone, t: props.t })) }));
38
+ : (_jsx(PetSprite, { snapshot: snapshot, definition: definition, display: snapshot.display, feedback: feedback, onPet: props.pet, onFeed: props.feed, onHide: props.hide, onDragEnd: props.dragEnd, onRename: props.rename, onOpenSession: props.openSession, onFeedbackDone: props.feedbackDone, t: props.t })) }));
39
39
  }
40
40
  const display = snapshot?.display ?? DEFAULT_DISPLAY;
41
41
  return (_jsx("button", { type: "button", className: styles.summon, style: {
@@ -1 +1 @@
1
- {"version":3,"file":"PetSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/PetSettingsCard.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAC7F,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAI1F,OAAO,EAAoD,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,IAAI,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAG1J,+EAA+E;AAC/E,MAAM,WAAW,WAAW;IAC1B,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,qBAAqB;IACrB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,0CAA0C;AAC1C,MAAM,WAAW,oBAAqB,SAAQ,SAAS;IACrD,4BAA4B;IAC5B,OAAO,EAAE,cAAc,CAAA;IACvB,qBAAqB;IACrB,OAAO,EAAE,cAAc,CAAA;IACvB,iBAAiB;IACjB,IAAI,EAAE,cAAc,CAAA;IACpB,mBAAmB;IACnB,KAAK,EAAE,cAAc,CAAA;IACrB,oBAAoB;IACpB,MAAM,EAAE,cAAc,CAAA;IACtB,oBAAoB;IACpB,KAAK,EAAE,cAAc,CAAA;IACrB,wEAAwE;IACxE,UAAU,EAAE,SAAS;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CACxD;AAED,gEAAgE;AAChE,MAAM,WAAW,mBAAoB,SAAQ,WAAW;IACtD,KAAK,EAAE;QACL,iEAAiE;QACjE,eAAe,EAAE,aAAa,CAAC,oBAAoB,CAAC,CAAA;KACrD,CAAA;CACF;AAeD,2DAA2D;AAC3D,qBAAa,yBAAyB;IACpC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAuB;IAC5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqC;IAI3D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAe;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA4B;IACtD,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,QAAQ,CAAI;IAEpB,uEAAuE;gBAC3D,KAAK,EAAE,aAAa,CAAC,WAAW,CAAC;IAa7C,0EAA0E;YAC5D,QAAQ;IAgBtB,OAAO,CAAC,UAAU;IAalB;;;OAGG;IACH,MAAM,IAAI,mBAAmB;IAI7B;;;OAGG;IACH,OAAO,IAAI,IAAI;CAGhB;AAED,0DAA0D;AAC1D,MAAM,MAAM,oBAAoB,GAC9B,WAAW,CAAC,KAAK,CAAC,GAChB,UAAU,CAAC,mBAAmB,CAAC,CAAA;AAEnC;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,+BAuF1D;AAED,8DAA8D;AAC9D,MAAM,MAAM,uBAAuB,GACjC,YAAY,CAAC,kBAAkB,CAAC,GAC9B,WAAW,CAAC,KAAK,CAAC,GAClB,UAAU,CAAC,mBAAmB,CAAC,CAAA;AAEnC,mEAAmE;AACnE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,GAAG,SAAS,CAO5E"}
1
+ {"version":3,"file":"PetSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/PetSettingsCard.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAC7F,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAI1F,OAAO,EAAoD,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,IAAI,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAG1J,+EAA+E;AAC/E,MAAM,WAAW,WAAW;IAC1B,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,qBAAqB;IACrB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,0CAA0C;AAC1C,MAAM,WAAW,oBAAqB,SAAQ,SAAS;IACrD,4BAA4B;IAC5B,OAAO,EAAE,cAAc,CAAA;IACvB,qBAAqB;IACrB,OAAO,EAAE,cAAc,CAAA;IACvB,iBAAiB;IACjB,IAAI,EAAE,cAAc,CAAA;IACpB,mBAAmB;IACnB,KAAK,EAAE,cAAc,CAAA;IACrB,oBAAoB;IACpB,MAAM,EAAE,cAAc,CAAA;IACtB,oBAAoB;IACpB,KAAK,EAAE,cAAc,CAAA;IACrB,wEAAwE;IACxE,UAAU,EAAE,SAAS;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CACxD;AAED,gEAAgE;AAChE,MAAM,WAAW,mBAAoB,SAAQ,WAAW;IACtD,KAAK,EAAE;QACL,iEAAiE;QACjE,eAAe,EAAE,aAAa,CAAC,oBAAoB,CAAC,CAAA;KACrD,CAAA;CACF;AAeD,2DAA2D;AAC3D,qBAAa,yBAAyB;IACpC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAuB;IAC5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqC;IAI3D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAe;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA4B;IACtD,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,QAAQ,CAAI;IAEpB,uEAAuE;IACvE,YAAY,KAAK,EAAE,aAAa,CAAC,WAAW,CAAC,EAW5C;IAED,0EAA0E;YAC5D,QAAQ;IAgBtB,OAAO,CAAC,UAAU;IAalB;;;OAGG;IACH,MAAM,IAAI,mBAAmB,CAE5B;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;CACF;AAED,0DAA0D;AAC1D,MAAM,MAAM,oBAAoB,GAC9B,WAAW,CAAC,KAAK,CAAC,GAChB,UAAU,CAAC,mBAAmB,CAAC,CAAA;AAEnC;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,+BAuF1D;AAED,8DAA8D;AAC9D,MAAM,MAAM,uBAAuB,GACjC,YAAY,CAAC,kBAAkB,CAAC,GAC9B,WAAW,CAAC,KAAK,CAAC,GAClB,UAAU,CAAC,mBAAmB,CAAC,CAAA;AAEnC,mEAAmE;AACnE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,GAAG,SAAS,CAO5E"}
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { PluginSettingsCard, ValueField, BooleanField, ChoiceField } from "./PluginSettingsCard.js";
3
- import { CardForm, booleanField, choiceField, numberField } from "./settings-form.js";
2
+ import { PluginSettingsCard, ValueField, BooleanField, ChoiceField } from './PluginSettingsCard.js';
3
+ import { CardForm, booleanField, choiceField, numberField } from './settings-form.js';
4
4
  import sectionCss from './settings-section.module.css';
5
5
  /** Fetch the registry list (the same data the sprite renders from). */
6
6
  async function fetchPetChoices() {
@@ -35,6 +35,8 @@ export interface PetSpriteProps {
35
35
  onDragEnd: (right: number, bottom: number) => void;
36
36
  /** Rename the selected pet (persisted by the host). */
37
37
  onRename: (name: string) => void;
38
+ /** Navigate to the session one status bubble reports on. */
39
+ onOpenSession: (sessionId: string) => void;
38
40
  /** Clear the reaction bubble (after its CSS animation). */
39
41
  onFeedbackDone: () => void;
40
42
  /** Locale translate seat (namespace-bound). */
@@ -1 +1 @@
1
- {"version":3,"file":"PetSprite.d.ts","sourceRoot":"","sources":["../../../src/client/PetSprite.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAqC,WAAW,EAAE,MAAM,OAAO,CAAA;AAG3E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAGjD,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAGjC,wEAAwE;AACxE,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAA;IAC7B,8EAA8E;IAC9E,UAAU,EAAE,aAAa,CAAA;IACzB,qDAAqD;IACrD,OAAO,EAAE,gBAAgB,CAAA;IACzB,sCAAsC;IACtC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAA;IAC5B,8BAA8B;IAC9B,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,sCAAsC;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,sCAAsC;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,+BAA+B;IAC/B,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAClD,uDAAuD;IACvD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,2DAA2D;IAC3D,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,+CAA+C;IAC/C,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;CAC1B;AAOD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,WAAW,CAsT5D"}
1
+ {"version":3,"file":"PetSprite.d.ts","sourceRoot":"","sources":["../../../src/client/PetSprite.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAoD,WAAW,EAAE,MAAM,OAAO,CAAA;AAG1F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAIjD,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAGjC,wEAAwE;AACxE,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAA;IAC7B,8EAA8E;IAC9E,UAAU,EAAE,aAAa,CAAA;IACzB,qDAAqD;IACrD,OAAO,EAAE,gBAAgB,CAAA;IACzB,sCAAsC;IACtC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAA;IAC5B,8BAA8B;IAC9B,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,sCAAsC;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,sCAAsC;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,+BAA+B;IAC/B,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAClD,uDAAuD;IACvD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,4DAA4D;IAC5D,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,2DAA2D;IAC3D,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,+CAA+C;IAC/C,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;CAC1B;AAOD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,WAAW,CAmZ5D"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * Pet sprite companion component — the browser half's centerpiece. Renders a
4
4
  * fixed-position floating sprite (React portal onto document.body), plays
@@ -9,10 +9,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
9
9
  * pet id), so one component renders every registry entry.
10
10
  * @module @linxin666/dsh-pet/client/PetSprite
11
11
  */
12
- import { useEffect, useRef, useState } from 'react';
12
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
13
13
  import { createPortal } from 'react-dom';
14
14
  import clsx from 'clsx';
15
- import { framePosition, rowOfTrack, trimTrack } from "./spritesheet.js";
15
+ import { framePosition, rowOfTrack, trimTrack } from './spritesheet.js';
16
+ import { sequenceFrameAt } from './sequences.js';
17
+ import { animationForPhase } from '../state.js';
16
18
  import styles from './pet.module.css';
17
19
  /** Clamp a drag offset inside the viewport with a margin. */
18
20
  function clampOffset(value, max) {
@@ -28,10 +30,22 @@ export function PetSprite(props) {
28
30
  const { snapshot, definition, display, feedback } = props;
29
31
  const spriteRef = useRef(null);
30
32
  const floatRef = useRef(null);
33
+ const panelRef = useRef(null);
34
+ // Whichever bubble surface is currently rendered (feedback, the session
35
+ // stack, or the legacy status bubble) — only one exists at a time.
36
+ const bubbleRef = useRef(null);
31
37
  const [imageReady, setImageReady] = useState(false);
32
38
  const [hovered, setHovered] = useState(false);
33
39
  const [renaming, setRenaming] = useState(false);
40
+ const [panelAbove, setPanelAbove] = useState(false);
41
+ // Extra margin-bottom for the above-panel so it stacks clear of the
42
+ // bubbles instead of overlapping them (both anchor at the sprite's top).
43
+ const [panelLift, setPanelLift] = useState(0);
34
44
  const [nameDraft, setNameDraft] = useState('');
45
+ // Explicit IME composition tracking: some input methods (WeChat IME on
46
+ // Windows) report keydowns with isComposing === false mid-composition, so
47
+ // the native flag alone is not a safe submit/cancel guard (#303).
48
+ const composingRef = useRef(false);
35
49
  const [dragPos, setDragPos] = useState(null);
36
50
  const dragRef = useRef(null);
37
51
  const hideTimerRef = useRef(null);
@@ -44,6 +58,7 @@ export function PetSprite(props) {
44
58
  const columns = definition.columns;
45
59
  const rows = definition.rows;
46
60
  const tracks = definition.tracks;
61
+ const sequences = definition.sequences;
47
62
  // Load the atlas once; the definition carries the authoritative per-row
48
63
  // frame counts and per-track durations, so nothing else is fetched.
49
64
  useEffect(() => {
@@ -66,14 +81,17 @@ export function PetSprite(props) {
66
81
  // its track's first frame instead of animating (presentation-only; the
67
82
  // animation state machine is untouched).
68
83
  const spriteScale = display.size / cell.height;
84
+ const phase = snapshot?.phase ?? 'idle';
69
85
  const animation = snapshot?.animation ?? 'idle';
70
86
  const scaleRef = useRef(spriteScale);
71
87
  scaleRef.current = spriteScale;
72
88
  useEffect(() => {
73
89
  const reduceMotion = typeof window !== 'undefined'
74
90
  && window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true;
75
- const row = rowOfTrack(animation);
76
- const track = trimTrack(tracks[animation], rows[row] ?? tracks[animation].frames.length);
91
+ const sequence = animation === animationForPhase(phase) ? sequences?.[phase] : undefined;
92
+ const leadAnimation = sequence?.[0] ?? animation;
93
+ const row = rowOfTrack(leadAnimation);
94
+ const track = trimTrack(tracks[leadAnimation], rows[row] ?? tracks[leadAnimation].frames.length);
77
95
  // Paint one static sprite frame up front either way, so the pet is never
78
96
  // blank while the loop heat-up runs.
79
97
  const leadCol = track.frames[0];
@@ -85,13 +103,26 @@ export function PetSprite(props) {
85
103
  return;
86
104
  let raf = 0;
87
105
  let last = performance.now();
106
+ let sequenceElapsed = 0;
88
107
  const tick = (ts) => {
89
108
  const delta = ts - last;
90
109
  last = ts;
91
- // Trim the track to the row's real frame count (transparent cells
92
- // would render as a vanishing pet).
93
- const row = rowOfTrack(animation);
94
- const track = trimTrack(tracks[animation], rows[row] ?? tracks[animation].frames.length);
110
+ if (sequence !== undefined) {
111
+ sequenceElapsed += delta;
112
+ const current = sequenceFrameAt(sequence, tracks, sequenceElapsed);
113
+ const currentRow = rowOfTrack(current.animation);
114
+ const currentTrack = trimTrack(tracks[current.animation], rows[currentRow] ?? tracks[current.animation].frames.length);
115
+ const col = currentTrack.frames[current.frameIndex];
116
+ const pos = framePosition(cell, columns, currentRow, col, scaleRef.current);
117
+ if (spriteRef.current !== null) {
118
+ spriteRef.current.style.backgroundPosition = pos.x + 'px ' + pos.y + 'px';
119
+ }
120
+ raf = requestAnimationFrame(tick);
121
+ return;
122
+ }
123
+ // row/track come from the effect scope: they were computed once above
124
+ // and this effect re-runs when animation/tracks/rows change, so the
125
+ // per-frame recompute (trimTrack slices fresh arrays) is pure waste.
95
126
  const st = frameRef.current;
96
127
  if (st.track !== animation) {
97
128
  st.track = animation;
@@ -122,7 +153,7 @@ export function PetSprite(props) {
122
153
  };
123
154
  raf = requestAnimationFrame(tick);
124
155
  return () => cancelAnimationFrame(raf);
125
- }, [animation, cell, columns, rows, tracks]);
156
+ }, [animation, phase, cell, columns, rows, tracks, sequences]);
126
157
  // Auto-clear the feedback bubble after its CSS animation. The callback
127
158
  // rides a ref so re-renders never reset the timer: the 2s poll rebuilds
128
159
  // `props` every tick, and depending on it would starve the timeout.
@@ -176,33 +207,70 @@ export function PetSprite(props) {
176
207
  const spriteHeight = Math.round(cell.height * spriteScale);
177
208
  // Concurrent sessions each render their own bubble (stacked above the
178
209
  // sprite); the legacy single 'bubble' is the fallback when the host serves
179
- // no per-session list.
210
+ // no per-session list. The hover panel normally sits below the sprite, so
211
+ // the bubbles stay visible and clickable while hovering — no region swap.
180
212
  const sessionBubbles = snapshot?.sessions ?? [];
181
- const statusBubble = feedback === null && !hovered && sessionBubbles.length === 0
213
+ const statusBubble = feedback === null && sessionBubbles.length === 0
182
214
  ? snapshot?.bubble
183
215
  : undefined;
216
+ // The display session's inner whisper (碎碎念) — short inner-voice copy
217
+ // woken by the model's output. Interaction feedback takes over the whole
218
+ // bubble area while it plays, so whispers yield to it like status copy.
219
+ const whisper = feedback === null ? snapshot?.whisper : undefined;
220
+ const bubblePresent = feedback !== null || sessionBubbles.length > 0 || statusBubble !== undefined || whisper !== undefined;
184
221
  const displayName = snapshot?.name ?? definition.displayName;
222
+ useLayoutEffect(() => {
223
+ if (!hovered) {
224
+ setPanelAbove(false);
225
+ setPanelLift(0);
226
+ return;
227
+ }
228
+ const updatePanelPlacement = () => {
229
+ const sprite = spriteRef.current;
230
+ const panel = panelRef.current;
231
+ if (sprite === null || panel === null)
232
+ return;
233
+ const availableBelow = window.innerHeight - sprite.getBoundingClientRect().bottom;
234
+ const above = availableBelow < panel.getBoundingClientRect().height + 8;
235
+ setPanelAbove(above);
236
+ // The fallback above-placement shares the sprite's top edge with the
237
+ // bubble(s); lift the panel by the bubble area's height so the two
238
+ // never overlap (8px base gap + 6px clearance above the top bubble).
239
+ const bubbleHeight = above ? bubbleRef.current?.getBoundingClientRect().height ?? 0 : 0;
240
+ setPanelLift(bubbleHeight > 0 ? Math.ceil(bubbleHeight) + 14 : 0);
241
+ };
242
+ updatePanelPlacement();
243
+ window.addEventListener('resize', updatePanelPlacement);
244
+ return () => window.removeEventListener('resize', updatePanelPlacement);
245
+ }, [hovered, renaming, pos.right, pos.bottom, display.size, bubblePresent, sessionBubbles.length, feedback]);
185
246
  const float = (_jsxs("div", { ref: floatRef, className: styles.float, style: { right: pos.right, bottom: pos.bottom, zIndex: 2147483000 }, onPointerEnter: () => {
186
247
  clearHideTimer();
187
248
  setHovered(true);
188
249
  }, onPointerLeave: (e) => {
189
- // The panel and bubble render OUTSIDE the container's box (absolute,
190
- // above the sprite), so moving onto them fires pointerleave on the
191
- // container. Treat a target still inside the container's DOM (the
192
- // overflowed panel) as "still hovering"; otherwise give the pointer a
193
- // short grace period to reach the panel across the gap above it. The
194
- // bridge ('.panel::after') keeps the pointer inside the hit area, and
195
- // the grace period covers a slow mouse crossing the remaining sliver.
250
+ // The panel renders OUTSIDE the container's box (absolute, below
251
+ // the sprite), so moving onto it fires pointerleave on the container.
252
+ // Treat a target still inside the container's DOM (the overflowed
253
+ // panel) as "still hovering"; otherwise give the pointer a short
254
+ // grace period to reach the panel across the gap below the sprite.
255
+ // The bridge ('.panel::after') keeps the pointer inside the hit
256
+ // area, and the grace period covers a slow mouse crossing the
257
+ // remaining sliver.
196
258
  const next = e.relatedTarget;
197
259
  if (next instanceof Node && floatRef.current?.contains(next))
198
260
  return;
261
+ // Never auto-hide while the rename box is open: moving the pointer
262
+ // onto an IME candidate window (an OS-level window outside the
263
+ // webview) fires pointerleave, and unmounting the input mid-IME-
264
+ // composition crashes some input methods / the renderer (#303).
265
+ if (renaming)
266
+ return;
199
267
  clearHideTimer();
200
268
  hideTimerRef.current = window.setTimeout(() => setHovered(false), 300);
201
269
  }, children: [_jsx("div", { ref: spriteRef, className: styles.sprite, style: {
202
270
  width: spriteWidth,
203
271
  height: spriteHeight,
204
272
  backgroundImage: imageReady ? 'url(' + definition.atlasUrl + ')' : undefined,
205
- backgroundSize: (cell.width * columns * spriteScale) + 'px ' + (cell.height * rows.length * spriteScale) + 'px',
273
+ backgroundSize: (cell.width * columns * spriteScale) + 'px ' + (cell.height * (definition.atlasRows ?? rows.length) * spriteScale) + 'px',
206
274
  backgroundRepeat: 'no-repeat',
207
275
  backgroundPosition: '0 0',
208
276
  cursor: dragRef.current === null ? 'grab' : 'grabbing',
@@ -212,17 +280,21 @@ export function PetSprite(props) {
212
280
  if (draggedRef.current)
213
281
  return;
214
282
  props.onPet();
215
- }, role: "button", "aria-label": definition.displayName }), feedback !== null && (_jsx("div", { className: clsx(styles.bubble, feedback.kind === 'feed' ? styles.bubbleFeed : styles.bubblePet), children: feedback.text }, feedback.at)), feedback === null && !hovered && sessionBubbles.length > 0 && (_jsx("div", { className: styles.bubbleStack, role: "status", "aria-live": "polite", children: sessionBubbles.map(session => (_jsx("div", { className: clsx(styles.bubble, styles.bubbleStatus), children: session.bubble }, session.sessionId))) })), statusBubble !== undefined && (_jsx("div", { className: clsx(styles.bubble, styles.bubbleStatus), role: "status", "aria-live": "polite", children: statusBubble })), hovered && dragRef.current === null && (_jsx("div", { className: styles.panel, onPointerEnter: () => {
283
+ }, role: "button", "aria-label": definition.displayName }), feedback !== null && (_jsx("div", { ref: bubbleRef, className: clsx(styles.bubble, feedback.kind === 'feed' ? styles.bubbleFeed : styles.bubblePet), children: feedback.text }, feedback.at)), feedback === null && (sessionBubbles.length > 0 || statusBubble !== undefined || whisper !== undefined) && (_jsxs("div", { ref: bubbleRef, className: styles.bubbleStack, children: [sessionBubbles.map(session => (_jsx("button", { type: "button", className: clsx(styles.bubble, styles.bubbleStatus, styles.bubbleClickable), title: props.t('pet.openSessionHint'), onClick: () => { props.onOpenSession(session.sessionId); }, children: session.bubble }, session.sessionId))), sessionBubbles.length === 0 && statusBubble !== undefined && (_jsx("div", { className: clsx(styles.bubble, styles.bubbleStatus), role: "status", "aria-live": "polite", children: statusBubble })), whisper !== undefined && (_jsx("div", { className: styles.bubbleWhisper, children: whisper }, whisper))] })), hovered && dragRef.current === null && (_jsx("div", { ref: panelRef, className: clsx(styles.panel, panelAbove && styles.panelAbove), "data-placement": panelAbove ? 'above' : 'below', style: panelAbove && panelLift > 0
284
+ ? { marginBottom: panelLift }
285
+ : undefined, onPointerEnter: () => {
216
286
  // Reaching the panel (or its bridge) must cancel any hide timer
217
287
  // the container's pointerleave may have armed while the pointer
218
288
  // crossed the sliver between the sprite and the panel.
219
289
  clearHideTimer();
220
- }, children: renaming ? (_jsxs("div", { className: styles.renameRow, children: [_jsx("input", { className: styles.nameInput, value: nameDraft, maxLength: 20, placeholder: props.t('pet.namePlaceholder'), autoFocus: true, onChange: (e) => setNameDraft(e.target.value), onKeyDown: (e) => {
290
+ }, children: renaming ? (_jsxs("div", { className: styles.renameRow, children: [_jsx("input", { className: styles.nameInput, value: nameDraft, maxLength: 20, placeholder: props.t('pet.namePlaceholder'), autoFocus: true, onChange: (e) => setNameDraft(e.target.value), onCompositionStart: () => { composingRef.current = true; }, onCompositionEnd: () => { composingRef.current = false; }, onKeyDown: (e) => {
221
291
  // While an IME composition is active (e.g. selecting a
222
292
  // Chinese candidate), Enter/Escape keydowns belong to the
223
293
  // input method: ignore them so candidate selection can
224
- // neither submit the draft nor close the rename box.
225
- if (e.nativeEvent.isComposing)
294
+ // neither submit the draft nor close the rename box. The
295
+ // explicit ref and the 'Process' key cover IMEs that mark
296
+ // composition keydowns with isComposing === false (#303).
297
+ if (composingRef.current || e.nativeEvent.isComposing || e.key === 'Process')
226
298
  return;
227
299
  if (e.key === 'Enter') {
228
300
  const trimmed = nameDraft.trim();
@@ -240,7 +312,10 @@ export function PetSprite(props) {
240
312
  props.onRename(trimmed);
241
313
  setRenaming(false);
242
314
  }
243
- }, children: props.t('pet.confirm') })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.rankRow, children: [_jsx("span", { className: styles.nameCell, children: displayName }), _jsx("span", { children: props.t('pet.rank', { rank: snapshot?.affinity.rank ?? '?' }) })] }), _jsxs("div", { className: styles.rankRow, children: [_jsx("span", { children: props.t('pet.treats', { n: snapshot?.treats.stocked ?? 0 }) }), _jsx("span", { children: props.t('pet.points', { points: snapshot?.affinity.points ?? 0 }) })] }), _jsxs("div", { className: styles.actions, children: [_jsx("button", { type: "button", className: styles.action, onClick: props.onFeed, children: props.t('pet.feed') }), _jsx("button", { type: "button", className: styles.action, onClick: () => {
315
+ }, children: props.t('pet.confirm') })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.rankRow, children: [_jsx("span", { className: styles.nameCell, children: displayName }), _jsx("span", { className: styles.statRank, children: props.t('pet.rank', { rank: snapshot?.affinity.rank ?? '?' }) })] }), _jsxs("div", { className: styles.rankRow, children: [_jsx("span", { className: styles.statTreats, children: props.t('pet.treats', { n: snapshot?.treats.stocked ?? 0 }) }), _jsx("span", { className: styles.statPoints, children: props.t('pet.points', { points: snapshot?.affinity.points ?? 0 }) })] }), _jsxs("div", { className: styles.actions, children: [_jsx("button", { type: "button", className: styles.action, onClick: props.onFeed, children: props.t('pet.feed') }), _jsx("button", { type: "button", className: styles.action, onClick: () => {
316
+ // Cancel any pending hide so the rename box cannot
317
+ // unmount right as the user starts typing (#303).
318
+ clearHideTimer();
244
319
  setNameDraft(displayName);
245
320
  setRenaming(true);
246
321
  }, children: props.t('pet.rename') }), _jsx("button", { type: "button", className: styles.action, onClick: props.onHide, children: props.t('pet.hide') })] })] })) }))] }));
@@ -9,7 +9,7 @@
9
9
  import { type ReactNode } from 'react';
10
10
  import type { CardShell } from './settings-form.ts';
11
11
  /** Copy keys the card chrome itself reads; every consumer locale carries this shared vocabulary. */
12
- export declare const CARD_COPY_KEYS: readonly ["settings.collapse", "settings.expand", "settings.notExposed", "settings.unsaved", "settings.readOnly", "settings.saveFailed", "settings.discard", "settings.save", "settings.saving"];
12
+ export declare const CARD_COPY_KEYS: readonly ['settings.collapse', 'settings.expand', 'settings.notExposed', 'settings.unsaved', 'settings.readOnly', 'settings.saveFailed', 'settings.discard', 'settings.save', 'settings.saving'];
13
13
  /** Copy key the card chrome itself reads. */
14
14
  export type CardCopyKey = (typeof CARD_COPY_KEYS)[number];
15
15
  /** Key domain for the plugin's own copy (inferred per consumer). */
@@ -39,6 +39,12 @@ export interface PluginSettingsCardProps<TKey extends string = string> {
39
39
  * already provides the selection.
40
40
  */
41
41
  alwaysOpen?: boolean;
42
+ /**
43
+ * Hide the save/discard footer: cards whose body applies its own changes
44
+ * immediately (an embedded external settings section) have no staged
45
+ * edits, so the footer would sit there permanently disabled.
46
+ */
47
+ hideFooter?: boolean;
42
48
  /** The plugin's controls. */
43
49
  children: ReactNode;
44
50
  }
@@ -1 +1 @@
1
- {"version":3,"file":"PluginSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/PluginSettingsCard.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PluginSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/PluginSettingsCard.tsx"],"names":[],"mappings":"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"}
@@ -52,9 +52,11 @@ export function PluginSettingsCard(props) {
52
52
  : null] }));
53
53
  }
54
54
  return (_jsxs("li", { className: cardClass, children: [header, expanded
55
- ? (_jsxs("div", { className: css.body, children: [!state.writable ? _jsx("p", { className: css.readOnly, role: "status", children: props.t('settings.readOnly') }) : null, props.children, _jsxs("div", { className: css.footer, children: [state.failed
56
- ? (_jsxs("p", { className: css.failed, role: "status", children: [props.t('settings.saveFailed'), state.failedReason ? ' - ' + state.failedReason : ''] }))
57
- : null, _jsx("button", { type: "button", className: css.discard, disabled: !state.dirty || state.saving, onClick: props.onDiscard, children: props.t('settings.discard') }), _jsx("button", { type: "button", className: css.save, disabled: blocked, onClick: props.onSave, children: props.t(!state.saving ? 'settings.save' : 'settings.saving') })] })] }))
55
+ ? (_jsxs("div", { className: css.body, children: [!state.writable ? _jsx("p", { className: css.readOnly, role: "status", children: props.t('settings.readOnly') }) : null, props.children, props.hideFooter === true
56
+ ? null
57
+ : (_jsxs("div", { className: css.footer, children: [state.failed
58
+ ? (_jsxs("p", { className: css.failed, role: "status", children: [props.t('settings.saveFailed'), state.failedReason ? ' - ' + state.failedReason : ''] }))
59
+ : null, _jsx("button", { type: "button", className: css.discard, disabled: !state.dirty || state.saving, onClick: props.onDiscard, children: props.t('settings.discard') }), _jsx("button", { type: "button", className: css.save, disabled: blocked, onClick: props.onSave, children: props.t(!state.saving ? 'settings.save' : 'settings.saving') })] }))] }))
58
60
  : null] }));
59
61
  }
60
62
  /** A staged value field. `numeric` only hints the keypad: which drafts a field accepts is decided by its spec. */
@@ -11,7 +11,7 @@
11
11
  * @module @linxin666/dsh-pet/client
12
12
  */
13
13
  import type { ClientContext, SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-runtime/client';
14
- /** Required services. */
14
+ /** Required services (sessions powers bubble-to-session navigation). */
15
15
  export declare const inject: string[];
16
16
  /** Re-exported for consumers that type against the injected face. */
17
17
  export type { PetInjected, PetDockEntryProps } from './PetDockEntry.tsx';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;AA6D7G,yBAAyB;AACzB,eAAO,MAAM,MAAM,UAA+D,CAAA;AAElF,qEAAqE;AACrE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACxE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACtF,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AACpE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CA0L9C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAwB,aAAa,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;AA6DnI,wEAAwE;AACxE,eAAO,MAAM,MAAM,UAA2E,CAAA;AAE9F,qEAAqE;AACrE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACxE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACtF,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AACpE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAiN9C"}
@@ -12,10 +12,10 @@
12
12
  */
13
13
  import { createElement } from 'react';
14
14
  import { createRoot } from 'react-dom/client';
15
- import { createPetStore } from "./pet-store.js";
16
- import { PetDockEntry } from "./PetDockEntry.js";
17
- import { PetSettingsSection, PetSettingsCardController } from "./PetSettingsCard.js";
18
- import { NS, en, zh, t } from "./locales.js";
15
+ import { createPetStore } from './pet-store.js';
16
+ import { PetDockEntry } from './PetDockEntry.js';
17
+ import { PetSettingsSection, PetSettingsCardController } from './PetSettingsCard.js';
18
+ import { NS, en, zh, t } from './locales.js';
19
19
  /** Same-origin JSON fetch helper (GET without body, POST with JSON body). */
20
20
  async function petFetch(path, body) {
21
21
  const response = await fetch(path, body === undefined
@@ -44,8 +44,8 @@ const petApi = {
44
44
  const POLL_MS = 2000;
45
45
  /** Settings namespace the pet settings card edits (the Host plugin registers it). */
46
46
  const PET_SETTINGS_NS = 'pet';
47
- /** Required services. */
48
- export const inject = ['slots', 'locale', 'connection', 'settingsScope', 'remote'];
47
+ /** Required services (sessions powers bubble-to-session navigation). */
48
+ export const inject = ['slots', 'locale', 'connection', 'settingsScope', 'remote', 'sessions'];
49
49
  /**
50
50
  * Client plugin body: register dictionaries, mount the global pet entry and
51
51
  * poll loop while the plugin is enabled, and seat the settings card as a
@@ -100,6 +100,11 @@ export function apply(ctx) {
100
100
  // tick tries again. After it lands, one list feeds both the sprite and
101
101
  // the settings card's choices.
102
102
  let petsLoaded = false;
103
+ // Latest-wins guard: the 2s tick, visibility recovery, and
104
+ // interaction-triggered refreshes can overlap; only the newest
105
+ // response may publish, so a slow older one can never roll the
106
+ // snapshot back.
107
+ let stateSeq = 0;
103
108
  const pollNow = () => {
104
109
  if (!petsLoaded) {
105
110
  petApi.pets().then((list) => {
@@ -109,9 +114,15 @@ export function apply(ctx) {
109
114
  // Retry on the next poll tick.
110
115
  });
111
116
  }
117
+ const seq = stateSeq + 1;
118
+ stateSeq = seq;
112
119
  petApi.state().then((snapshot) => {
120
+ if (seq !== stateSeq)
121
+ return;
113
122
  setSnapshot(snapshot);
114
123
  }, () => {
124
+ if (seq !== stateSeq)
125
+ return;
115
126
  setState('error', 'pet.state transport error');
116
127
  });
117
128
  };
@@ -149,9 +160,23 @@ export function apply(ctx) {
149
160
  document.removeEventListener('visibilitychange', onVisibility);
150
161
  };
151
162
  }, 'pet: poll');
163
+ // Clicking a session bubble jumps the GUI to that session. A bubble
164
+ // can outlive its disposed session by one poll tick, and the sessions
165
+ // service fails loud on unknown ids, so consult the live list first.
166
+ // The pet's type program also loads the host-side dsh-session package
167
+ // through the service types, whose Context merge declares a different
168
+ // 'sessions' face; pin the browser runtime's outward face here.
169
+ const sessions = ctx.sessions;
170
+ const openSession = (sessionId) => {
171
+ const list = sessions.list.getSnapshot();
172
+ if (list.byId[sessionId] === undefined)
173
+ return;
174
+ sessions.open(sessionId);
175
+ };
152
176
  const injected = () => ({
153
177
  store: petStore,
154
178
  ensure: pollNow,
179
+ openSession,
155
180
  pet: () => {
156
181
  petApi.interact('pet').then((result) => {
157
182
  setFeedback({