@linxin666/dsh-pet 0.1.18 → 0.1.19

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 (66) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +12 -5
  3. package/README.zh.md +12 -5
  4. package/lib/client.js +48 -11
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +100 -30
  7. package/lib/invariant.js +2 -2
  8. package/lib/state-Diiq8vjk.js +394 -0
  9. package/lib/types/affinity.d.ts +31 -3
  10. package/lib/types/affinity.d.ts.map +1 -1
  11. package/lib/types/affinity.js +21 -7
  12. package/lib/types/client/PetSettingsCard.d.ts +5 -0
  13. package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
  14. package/lib/types/client/PetSettingsCard.js +7 -0
  15. package/lib/types/client/PetSprite.d.ts.map +1 -1
  16. package/lib/types/client/PetSprite.js +8 -2
  17. package/lib/types/client/index.d.ts.map +1 -1
  18. package/lib/types/client/index.js +14 -8
  19. package/lib/types/client/settings-form.d.ts +8 -0
  20. package/lib/types/client/settings-form.d.ts.map +1 -1
  21. package/lib/types/client/settings-form.js +15 -1
  22. package/lib/types/event-projection.d.ts.map +1 -1
  23. package/lib/types/event-projection.js +3 -1
  24. package/lib/types/index.d.ts +4 -2
  25. package/lib/types/index.d.ts.map +1 -1
  26. package/lib/types/index.js +2 -1
  27. package/lib/types/ledger.d.ts +16 -1
  28. package/lib/types/ledger.d.ts.map +1 -1
  29. package/lib/types/ledger.js +29 -4
  30. package/lib/types/registry.d.ts +10 -0
  31. package/lib/types/registry.d.ts.map +1 -1
  32. package/lib/types/registry.js +3 -0
  33. package/lib/types/remarks.d.ts +56 -0
  34. package/lib/types/remarks.d.ts.map +1 -0
  35. package/lib/types/remarks.js +157 -0
  36. package/lib/types/service.d.ts +40 -3
  37. package/lib/types/service.d.ts.map +1 -1
  38. package/lib/types/service.js +90 -18
  39. package/lib/types/state.js +5 -5
  40. package/lib/types/treats.d.ts +3 -0
  41. package/lib/types/treats.d.ts.map +1 -1
  42. package/lib/types/treats.js +5 -2
  43. package/package.json +1 -1
  44. package/src/affinity.test.ts +19 -0
  45. package/src/affinity.ts +22 -7
  46. package/src/client/PetSettingsCard.tsx +8 -0
  47. package/src/client/PetSprite.test.tsx +33 -0
  48. package/src/client/PetSprite.tsx +16 -1
  49. package/src/client/index.ts +14 -8
  50. package/src/client/pet.module.css +19 -0
  51. package/src/client/settings-form.ts +15 -1
  52. package/src/event-projection.ts +3 -1
  53. package/src/index.ts +12 -1
  54. package/src/ledger.test.ts +40 -9
  55. package/src/ledger.ts +33 -4
  56. package/src/persist.test.ts +1 -1
  57. package/src/registry.test.ts +27 -0
  58. package/src/registry.ts +12 -0
  59. package/src/remarks.test.ts +89 -0
  60. package/src/remarks.ts +177 -0
  61. package/src/service.ts +116 -18
  62. package/src/state.test.ts +9 -0
  63. package/src/state.ts +5 -5
  64. package/src/treats.test.ts +13 -11
  65. package/src/treats.ts +5 -2
  66. package/lib/state-CFyJv0sQ.js +0 -213
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # node scripts/verify-docs.mjs --write <dir>
5
- README.md: ea1c6ac0d9ff57a9072ac6872a8147e5dc389435
6
- README.zh.md: 5146dc5a8fbe4861c1e2367546a5a592dd916476
5
+ README.md: 75de064887be4289018547fba722851afd98dd5a
6
+ README.zh.md: 9e396eca9b5a47425aeee972ec7a95a7e7da81ac
package/README.md CHANGED
@@ -18,12 +18,13 @@ Re-implemented from the pet feature of the Codex desktop app, as an official DSH
18
18
  | State animation | Official session activity → 9-state animation: `thinking → running`, `tool → running-right`, `review → review`, `waiting → waiting`, `done → jumping`, `failed → failed` |
19
19
  | Head-pat interaction | Click the pet → bubble feedback + affinity +1 (10s cooldown) |
20
20
  | Feeding | Hover panel 喂食 (Feed) → consumes 1 dried fish + affinity +5 (30s cooldown) |
21
- | Treat economy | Dried-fish stock (cap 20): +1 every 3 rounds of work, +1 every 30 minutes |
22
- | Affinity | +1 per round completed; 4 levels: 幼鲸 → 伙伴 → 挚友 → 深海羁绊 (capped at 100) |
21
+ | Treat economy | Dried-fish stock (cap 20): +1 every 30 rounds of work, +1 every 300 minutes (5 hours) — 10x rarer than the original cadence |
22
+ | Affinity | +1 per round completed; 9 levels: 幼鲸 → 伙伴 → 挚友 → 深海羁绊 → 心有灵犀 → 传说羁绊 → 神话羁绊 → 永恒之契 → 鲸生共渡 (capped at 999,999,999) |
23
23
  | Dragging | Hold and drag the pet to reposition; position persisted |
24
24
  | Hide/Summon | Hover panel 隐藏 (Hide); after hiding, a 召唤{name} (Summon {name}) button appears |
25
- | Status bubble | Shows the current session stage or tool name; transient interaction feedback temporarily takes priority |
26
- | Multi-session activity | The pet is host-global: the most recent meaningful event controls its display, while completed turns from every session contribute affinity and treats |
25
+ | Witty remarks | Built-in remark library (10 lines per event) plus per-pet custom lines from a manifest remarks block — community PRs give their pet its own voice |
26
+ | Status bubbles | Each concurrently active session gets its own bubble, stacked above the pet (up to 12); transient interaction feedback temporarily takes priority |
27
+ | Multi-session activity | The pet is host-global: the most recent meaningful event drives the sprite animation while every active session reports its own state in a separate bubble; completed turns from every session contribute affinity and treats |
27
28
 
28
29
  ## Pet contract
29
30
 
@@ -40,11 +41,16 @@ A pet is a directory holding one `pet.json` manifest and one atlas image. Nothin
40
41
  "frames": [6, 8, 8, 4, 5, 8, 6, 6, 6], // optional per-row frame counts
41
42
  "tracks": { // optional per-track rhythm overrides
42
43
  "idle": { "durations": [400, 400, 500, 400, 400, 500] }
44
+ },
45
+ "remarks": { // optional witty remarks (one line or a pool per slot)
46
+ "pet": "摸摸水獭的头~",
47
+ "feed": ["小鱼干真香", "再来一条~"]
43
48
  }
44
49
  }
45
50
  ```
46
51
 
47
52
  - The atlas is an 8-column × 9-row grid (192×208 cells by default); rows are fixed in this order: 0 idle, 1 running-right, 2 running-left, 3 waving, 4 jumping, 5 failed, 6 waiting, 7 running, 8 review. Unused cells stay fully transparent.
53
+ - The optional remarks block overrides the reaction bubbles the pet speaks on pet / petCooldown / feed / feedCooldown / noTreats events. Each slot accepts one line or a pool of lines (cycled round-robin); a declared slot replaces the built-in pool for that slot only. This is how community contributions give their pet its own witty voice.
48
54
  - `frames` counts the used columns per row (defaults to the hatch-pet contract table `[6, 8, 8, 4, 5, 8, 6, 6, 6]`); `tracks` overrides per-frame durations (cycled to the row's frame count), `loop`, and `fallback` per animation (defaults: everything loops; `jumping` and `failed` hold their last frame, then fall back to `idle`).
49
55
 
50
56
  Where pets come from (later sources override earlier ones on id collision):
@@ -76,6 +82,7 @@ dsh-pet/
76
82
  | |-- registry.ts # multi-pet contract: manifest scan + normalization (assets + custom pets)
77
83
  | |-- service.ts # PetService: pet selection + state machine + affinity + config
78
84
  | |-- state.ts # pet state machine: projected session activity → 9 state animations
85
+ | |-- remarks.ts # witty-remark library: built-in pools + per-pet overrides + picker
79
86
  | |-- affinity.ts # affinity ledger (pure functions + cooldowns)
80
87
  | |-- treats.ts # dried-fish stock ledger
81
88
  | |-- persist.ts # persistence ($DSH_HOME/pet.json: selection + per-pet names, atomic write)
@@ -106,7 +113,7 @@ global React root (createRoot → document.body) <-- polling 2s -- pet-client (b
106
113
  - **Status source**: the host projects official `turn/start`, `step/start`, `assistant/chunk`, `assistant/message`, `tool/call`, `tool/result`, and `turn/end` events into waiting/thinking/tool/review/done/failed states. Optional legacy `activity/status` events remain a compatibility input.
107
114
  - **Registry**: the host normalizes every manifest into a full render definition (geometry, per-row frame counts, per-track durations) and serves it over `/api/pet/pets`; the browser half renders any entry from that definition and carries no per-pet code.
108
115
  - **Selection & naming**: `petId` lives in the settings namespace; per-pet names live in `pet.json` under `names`, edited through the hover-panel rename of the active pet. Legacy installs migrate their flat `name` onto the whale girl.
109
- - **Multi-session semantics**: the API and browser mount are host-global and expose no foreground-session identity, so the most recent meaningful event wins the display. Every session's completed turns are still rewarded independently, and disposing a non-current session does not reset the visible state.
116
+ - **Multi-session semantics**: the API and browser mount are host-global and expose no foreground-session identity. Concurrent sessions each keep their own projected state: the most recent meaningful event drives the sprite animation, while every active session reports its stage in its own bubble (the state view's sessions list, capped at 12 most-recent). Every session's completed turns are still rewarded independently; disposing a session removes its bubble, and disposing the display session falls back to the most recent remaining one.
110
117
  - **Mount point**: `document.body` (global React root, always shown: no session / new session / mid-session — the old mount point `conversation.composer.dock` only rendered in an active session, hiding the pet in new sessions); the component uses `createPortal` internally to render the global floating layer.
111
118
  - **Rendering**: CSS sprite (background-position) per-frame animation; frame durations come from the served definition's tracks.
112
119
  - **Communication**: browser ↔ host over the same-origin `/api/pet/*` JSON endpoints (state/pets/interact/set-visible/set-config/set-name/set-pet); each pet's atlas loads from `/pet/<id>/<spritesheetPath>` — the plugin self-sufficiently provides its own API and assets (the same pattern as dsh-remote-web-ui's `/api/pair`).
package/README.zh.md CHANGED
@@ -18,12 +18,13 @@
18
18
  | 状态动画 | 官方会话活动 → 9 态动画:`thinking → running`、`tool → running-right`、`review → review`、`waiting → waiting`、`done → jumping`、`failed → failed` |
19
19
  | 摸头互动 | 点击宠物 → 气泡反馈 + 亲密度 +1(10s 冷却) |
20
20
  | 喂食 | 悬浮面板 喂食 → 消耗 1 条小鱼干 + 亲密度 +5(30s 冷却) |
21
- | 小鱼干经济 | 库存(上限 20):每工作 3 轮 +1,每 30 分钟 +1 |
22
- | 亲密度 | 每完成一轮 +1;4 级:幼鲸 → 伙伴 → 挚友 → 深海羁绊(上限 100) |
21
+ | 小鱼干经济 | 库存(上限 20):每工作 30 轮 +1,每 300 分钟(5 小时)+1 —— 获取难度为原来的 10 倍 |
22
+ | 亲密度 | 每完成一轮 +1;9 级:幼鲸 → 伙伴 → 挚友 → 深海羁绊 → 心有灵犀 → 传说羁绊 → 神话羁绊 → 永恒之契 → 鲸生共渡(上限 999,999,999) |
23
23
  | 拖动 | 按住拖动宠物换位置;位置持久化 |
24
24
  | 隐藏/召唤 | 悬浮面板 隐藏;隐藏后出现 召唤{name} 按钮 |
25
- | 状态气泡 | 显示当前会话阶段或工具名;瞬时互动反馈临时优先 |
26
- | 多会话活动 | 宠物是宿主全局的:最近一次有意义事件驱动显示,每个会话完成的轮次都计入亲密度与小鱼干 |
25
+ | 妙语库 | 内置默认妙语库(每类事件 10 句)+ 宠物 manifest 的 remarks 块自定义台词——社区 PR 可以给自家宠物配上专属妙语 |
26
+ | 状态气泡 | 并行会话各自一个气泡,在宠物头顶堆叠(最多 12 个);瞬时互动反馈临时优先 |
27
+ | 多会话活动 | 宠物是宿主全局的:最近一次有意义事件驱动精灵动画,同时每个活动会话用自己的气泡报告各自状态;每个会话完成的轮次都计入亲密度与小鱼干 |
27
28
 
28
29
  ## 宠物契约
29
30
 
@@ -40,11 +41,16 @@
40
41
  "frames": [6, 8, 8, 4, 5, 8, 6, 6, 6], // 可选的每行帧数
41
42
  "tracks": { // 可选的每轨节奏覆盖
42
43
  "idle": { "durations": [400, 400, 500, 400, 400, 500] }
44
+ },
45
+ "remarks": { // 可选妙语(每个槽位一行或多行)
46
+ "pet": "摸摸水獭的头~",
47
+ "feed": ["小鱼干真香", "再来一条~"]
43
48
  }
44
49
  }
45
50
  ```
46
51
 
47
52
  - 图集是 8 列 × 9 行网格(默认 192×208 单元格);行序固定:0 idle、1 running-right、2 running-left、3 waving、4 jumping、5 failed、6 waiting、7 running、8 review。未使用的格子保持全透明。
53
+ - 可选 remarks 块覆盖宠物在 pet(摸头)/ petCooldown / feed(喂食)/ feedCooldown / noTreats(缺粮)事件上的气泡台词。每个槽位接受一句或一组台词(轮询循环);声明过的槽位只替换该槽位的内置默认池。社区贡献就是这样给自家宠物配上专属妙语的。
48
54
  - `frames` 记录每行用到的列数(缺省按 hatch-pet 契约表 `[6, 8, 8, 4, 5, 8, 6, 6, 6]`);`tracks` 按动画覆盖每帧时长(按该行帧数循环补足)、`loop` 与 `fallback`(默认:全部循环;`jumping` 与 `failed` 停在最后一帧后回到 `idle`)。
49
55
 
50
56
  宠物的来源(后注册的来源在同 id 冲突时覆盖前者):
@@ -76,6 +82,7 @@ dsh-pet/
76
82
  | |-- registry.ts # 多宠物契约:manifest 扫描 + 归一化(内置 + 自定义宠物)
77
83
  | |-- service.ts # PetService:宠物选择 + 状态机 + 亲密度 + 配置
78
84
  | |-- state.ts # 宠物状态机:会话活动投影 → 9 态动画
85
+ | |-- remarks.ts # 妙语库:内置默认池 + 每宠物覆盖 + 轮询选取
79
86
  | |-- affinity.ts # 亲密度账本(纯函数 + 冷却)
80
87
  | |-- treats.ts # 小鱼干库存账本
81
88
  | |-- persist.ts # 持久化($DSH_HOME/pet.json:选择 + 每宠物名字,原子写入)
@@ -106,7 +113,7 @@ dsh-pet/
106
113
  - **状态来源**:宿主把官方 `turn/start`、`step/start`、`assistant/chunk`、`assistant/message`、`tool/call`、`tool/result`、`turn/end` 事件投影为 waiting/thinking/tool/review/done/failed 状态。可选兼容 `activity/status` 事件仍作为输入。
107
114
  - **注册表**:宿主把每份 manifest 归一化为完整渲染定义(几何、每行帧数、每轨时长),经 `/api/pet/pets` 下发;浏览器半区用该定义渲染任意条目,不携带任何宠物专属代码。
108
115
  - **选择与命名**:`petId` 存于设置命名空间;每只宠物的名字存于 `pet.json` 的 `names`,通过悬浮面板对当前宠物改名编辑。旧版安装的平铺 `name` 自动迁移到鲸鱼娘名下。
109
- - **多会话语义**:API 与浏览器挂载都是宿主全局的,不暴露前台会话身份,最近一次有意义事件赢得显示;每个会话完成的轮次仍独立计奖,销毁非当前会话不会重置可见状态。
116
+ - **多会话语义**:API 与浏览器挂载都是宿主全局的,不暴露前台会话身份。并行会话各自保留投影状态:最近一次有意义事件驱动精灵动画,同时每个活动会话在独立气泡里报告自己的阶段(state 视图的 sessions 列表,最多保留最近 12 个)。每个会话完成的轮次仍独立计奖;销毁会话移除它的气泡,销毁当前显示会话则回退到最近仍在活动的会话。
110
117
  - **挂载点**:`document.body`(全局 React 根,始终显示:无会话 / 新会话 / 会话中都可见——旧挂载点 `conversation.composer.dock` 只在活动会话里渲染,新会话里宠物消失);组件内部用 `createPortal` 渲染全局浮层。
111
118
  - **渲染**:CSS 精灵(background-position)逐帧动画;帧时长来自下发定义里的轨道表。
112
119
  - **通信**:浏览器 ↔ 宿主走同源 `/api/pet/*` JSON 端点(state/pets/interact/set-visible/set-config/set-name/set-pet);每只宠物的图集从 `/pet/<id>/<spritesheetPath>` 加载——插件自给自足地提供自己的 API 与资源(与 dsh-remote-web-ui 的 `/api/pair` 同一模式)。
package/lib/client.js CHANGED
@@ -107,7 +107,7 @@ window.__ModuleLoader__.load({
107
107
  }
108
108
  //#endregion
109
109
  //#region \0dsh-css:packages/dsh-pet/src/client/pet.module.css.mjs
110
- const css$2 = ".kz2Bea_float{pointer-events:auto;-webkit-user-select:none;user-select:none;flex-direction:column;align-items:center;display:flex;position:fixed}.kz2Bea_sprite{image-rendering:auto;touch-action:none;position:relative}.kz2Bea_bubble{white-space:nowrap;color:#fff;pointer-events:none;border-radius:999px;margin-bottom:6px;padding:4px 10px;font-size:12px;line-height:1.4;animation:2.6s ease-out forwards kz2Bea_pet-bubble-pop;position:absolute;bottom:100%;box-shadow:0 2px 8px #00000040}.kz2Bea_bubblePet{background:#f472b6eb}.kz2Bea_bubbleFeed{background:#38bdf8eb}.kz2Bea_bubbleStatus{text-overflow:ellipsis;background:#0f172ae6;border:1px solid #7dd3fc80;max-width:min(280px,100vw - 24px);animation:none;overflow:hidden}@keyframes kz2Bea_pet-bubble-pop{0%{opacity:0;transform:translateY(6px)scale(.85)}15%{opacity:1;transform:translateY(0)scale(1.05)}25%{transform:translateY(0)scale(1)}75%{opacity:1}to{opacity:0;transform:translateY(-8px)scale(.95)}}.kz2Bea_panel{color:#e2e8f0;backdrop-filter:blur(6px);background:#0f172aeb;border:1px solid #94a3b859;border-radius:10px;flex-direction:column;gap:6px;min-width:132px;padding:8px 10px;font-size:12px;display:flex;position:absolute;bottom:100%;box-shadow:0 4px 16px #00000059}.kz2Bea_panel:after{content:\"\";height:14px;position:absolute;top:100%;left:0;right:0}.kz2Bea_rankRow{white-space:nowrap;justify-content:space-between;gap:10px;display:flex}.kz2Bea_nameCell{font-weight:600}.kz2Bea_renameRow{align-items:center;gap:6px;display:flex}.kz2Bea_nameInput{color:#e2e8f0;background:#1e293be6;border:1px solid #7dd3fc80;border-radius:6px;outline:none;flex:1;min-width:0;padding:3px 6px;font-size:12px}.kz2Bea_nameInput:focus{border-color:#38bdf8;box-shadow:0 0 0 2px #38bdf873}.kz2Bea_actions{gap:6px;display:flex}.kz2Bea_action{cursor:pointer;color:#0f172a;background:linear-gradient(#7dd3fc,#38bdf8);border:none;border-radius:6px;flex:1;padding:4px 8px;font-size:12px;transition:filter .12s,box-shadow .12s}.kz2Bea_action:hover{filter:brightness(1.08)}.kz2Bea_action:active{filter:brightness(.94)}.kz2Bea_action:focus-visible{outline:none;box-shadow:0 0 0 2px #38bdf8d9}.kz2Bea_summon{color:#7dd3fc;cursor:pointer;background:#0f172abf;border:1px dashed #7dd3fc99;border-radius:999px;padding:2px 10px;font-size:11px;transition:border-color .12s,color .12s,background .12s,box-shadow .12s}.kz2Bea_summon:hover{color:#bae6fd;background:#0f172ae6;border-color:#7dd3fcf2}.kz2Bea_summon:active{color:#7dd3fc;border-color:#7dd3fccc}.kz2Bea_summon:focus-visible{outline:none;box-shadow:0 0 0 2px #38bdf8d9}@media (prefers-reduced-motion:reduce){.kz2Bea_bubble{opacity:1;animation:none}.kz2Bea_action,.kz2Bea_summon{transition:none}}";
110
+ const css$2 = ".kz2Bea_float{pointer-events:auto;-webkit-user-select:none;user-select:none;flex-direction:column;align-items:center;display:flex;position:fixed}.kz2Bea_sprite{image-rendering:auto;touch-action:none;position:relative}.kz2Bea_bubble{white-space:nowrap;color:#fff;pointer-events:none;border-radius:999px;margin-bottom:6px;padding:4px 10px;font-size:12px;line-height:1.4;animation:2.6s ease-out forwards kz2Bea_pet-bubble-pop;position:absolute;bottom:100%;box-shadow:0 2px 8px #00000040}.kz2Bea_bubblePet{background:#f472b6eb}.kz2Bea_bubbleFeed{background:#38bdf8eb}.kz2Bea_bubbleStatus{text-overflow:ellipsis;background:#0f172ae6;border:1px solid #7dd3fc80;max-width:min(280px,100vw - 24px);animation:none;overflow:hidden}.kz2Bea_bubbleStack{pointer-events:none;flex-direction:column;align-items:center;gap:4px;display:flex;position:absolute;bottom:100%}.kz2Bea_bubbleStack .kz2Bea_bubble{margin-bottom:0;position:relative;bottom:auto}@keyframes kz2Bea_pet-bubble-pop{0%{opacity:0;transform:translateY(6px)scale(.85)}15%{opacity:1;transform:translateY(0)scale(1.05)}25%{transform:translateY(0)scale(1)}75%{opacity:1}to{opacity:0;transform:translateY(-8px)scale(.95)}}.kz2Bea_panel{color:#e2e8f0;backdrop-filter:blur(6px);background:#0f172aeb;border:1px solid #94a3b859;border-radius:10px;flex-direction:column;gap:6px;min-width:132px;padding:8px 10px;font-size:12px;display:flex;position:absolute;bottom:100%;box-shadow:0 4px 16px #00000059}.kz2Bea_panel:after{content:\"\";height:14px;position:absolute;top:100%;left:0;right:0}.kz2Bea_rankRow{white-space:nowrap;justify-content:space-between;gap:10px;display:flex}.kz2Bea_nameCell{font-weight:600}.kz2Bea_renameRow{align-items:center;gap:6px;display:flex}.kz2Bea_nameInput{color:#e2e8f0;background:#1e293be6;border:1px solid #7dd3fc80;border-radius:6px;outline:none;flex:1;min-width:0;padding:3px 6px;font-size:12px}.kz2Bea_nameInput:focus{border-color:#38bdf8;box-shadow:0 0 0 2px #38bdf873}.kz2Bea_actions{gap:6px;display:flex}.kz2Bea_action{cursor:pointer;color:#0f172a;background:linear-gradient(#7dd3fc,#38bdf8);border:none;border-radius:6px;flex:1;padding:4px 8px;font-size:12px;transition:filter .12s,box-shadow .12s}.kz2Bea_action:hover{filter:brightness(1.08)}.kz2Bea_action:active{filter:brightness(.94)}.kz2Bea_action:focus-visible{outline:none;box-shadow:0 0 0 2px #38bdf8d9}.kz2Bea_summon{color:#7dd3fc;cursor:pointer;background:#0f172abf;border:1px dashed #7dd3fc99;border-radius:999px;padding:2px 10px;font-size:11px;transition:border-color .12s,color .12s,background .12s,box-shadow .12s}.kz2Bea_summon:hover{color:#bae6fd;background:#0f172ae6;border-color:#7dd3fcf2}.kz2Bea_summon:active{color:#7dd3fc;border-color:#7dd3fccc}.kz2Bea_summon:focus-visible{outline:none;box-shadow:0 0 0 2px #38bdf8d9}@media (prefers-reduced-motion:reduce){.kz2Bea_bubble{opacity:1;animation:none}.kz2Bea_action,.kz2Bea_summon{transition:none}}";
111
111
  const tagId$2 = "@linxin666/dsh-pet/pet.module.css";
112
112
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
113
113
  const tag = document.createElement("style");
@@ -122,6 +122,7 @@ window.__ModuleLoader__.load({
122
122
  "bubble": "kz2Bea_bubble",
123
123
  "bubbleFeed": "kz2Bea_bubbleFeed",
124
124
  "bubblePet": "kz2Bea_bubblePet",
125
+ "bubbleStack": "kz2Bea_bubbleStack",
125
126
  "bubbleStatus": "kz2Bea_bubbleStatus",
126
127
  "float": "kz2Bea_float",
127
128
  "nameCell": "kz2Bea_nameCell",
@@ -288,7 +289,8 @@ window.__ModuleLoader__.load({
288
289
  };
289
290
  const spriteWidth = Math.round(cell.width * spriteScale);
290
291
  const spriteHeight = Math.round(cell.height * spriteScale);
291
- const statusBubble = feedback === null && !hovered ? snapshot?.bubble : void 0;
292
+ const sessionBubbles = snapshot?.sessions ?? [];
293
+ const statusBubble = feedback === null && !hovered && sessionBubbles.length === 0 ? snapshot?.bubble : void 0;
292
294
  const displayName = snapshot?.name ?? definition.displayName;
293
295
  return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
294
296
  ref: floatRef,
@@ -335,6 +337,15 @@ window.__ModuleLoader__.load({
335
337
  className: clsx(pet_module_css_default.bubble, feedback.kind === "feed" ? pet_module_css_default.bubbleFeed : pet_module_css_default.bubblePet),
336
338
  children: feedback.text
337
339
  }, feedback.at),
340
+ feedback === null && !hovered && sessionBubbles.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
341
+ className: pet_module_css_default.bubbleStack,
342
+ role: "status",
343
+ "aria-live": "polite",
344
+ children: sessionBubbles.map((session) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
345
+ className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus),
346
+ children: session.bubble
347
+ }, session.sessionId))
348
+ }),
338
349
  statusBubble !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
339
350
  className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus),
340
351
  role: "status",
@@ -878,6 +889,9 @@ window.__ModuleLoader__.load({
878
889
  specs;
879
890
  staged = /* @__PURE__ */ new Map();
880
891
  listeners = /* @__PURE__ */ new Set();
892
+ /** The scope subscription installed in the constructor; released by dispose(). */
893
+ disposeScope;
894
+ disposed = false;
881
895
  saving = false;
882
896
  failed = false;
883
897
  failedReason;
@@ -885,10 +899,20 @@ window.__ModuleLoader__.load({
885
899
  constructor(scope, specs) {
886
900
  this.scope = scope;
887
901
  this.specs = new Map(specs.map((spec) => [spec.field, spec]));
888
- scope.subscribe(() => {
902
+ this.disposeScope = scope.subscribe(() => {
889
903
  this.publish();
890
904
  });
891
905
  }
906
+ /**
907
+ * Release the scope subscription and every bound store listener. The card
908
+ * must call this on teardown; later calls are no-ops.
909
+ */
910
+ dispose() {
911
+ if (this.disposed) return;
912
+ this.disposed = true;
913
+ this.disposeScope();
914
+ this.listeners.clear();
915
+ }
892
916
  /** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
893
917
  bind(project) {
894
918
  const store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(project());
@@ -1168,6 +1192,13 @@ window.__ModuleLoader__.load({
1168
1192
  ...this.form.actions()
1169
1193
  };
1170
1194
  }
1195
+ /**
1196
+ * Release the card's scope subscription and bound stores; the slot
1197
+ * disposer calls this on teardown.
1198
+ */
1199
+ dispose() {
1200
+ this.form.dispose();
1201
+ }
1171
1202
  };
1172
1203
  /**
1173
1204
  * Render the pet settings card.
@@ -1461,14 +1492,20 @@ window.__ModuleLoader__.load({
1461
1492
  return snapshot.status === "ready" ? snapshot.value?.enabled ?? true : snapshot.status === "unavailable";
1462
1493
  };
1463
1494
  const petSettings = new PetSettingsCardController(settingsScope);
1464
- ctx.slots.inject("settings.section", () => ctx.slots.register({
1465
- name: "settings.section",
1466
- id: "pet",
1467
- order: 130,
1468
- label: () => ctx.locale.bind("pet")("settings.title"),
1469
- locale: "pet",
1470
- inject: () => petSettings.inject()
1471
- }, PetSettingsSection));
1495
+ ctx.slots.inject("settings.section", () => {
1496
+ const unregister = ctx.slots.register({
1497
+ name: "settings.section",
1498
+ id: "pet",
1499
+ order: 130,
1500
+ label: () => ctx.locale.bind("pet")("settings.title"),
1501
+ locale: "pet",
1502
+ inject: () => petSettings.inject()
1503
+ }, PetSettingsSection);
1504
+ return () => {
1505
+ petSettings.dispose();
1506
+ unregister();
1507
+ };
1508
+ });
1472
1509
  let disposeUi;
1473
1510
  const syncUi = () => {
1474
1511
  if (enabled() && disposeUi === void 0) {