@linxin666/dsh-pet 0.1.13 → 0.1.15

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 (57) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +11 -7
  3. package/README.zh.md +11 -7
  4. package/lib/client.js +38 -17
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +393 -150
  7. package/lib/invariant.js +1 -1
  8. package/lib/{state-C9EyycwI.js → state-CFyJv0sQ.js} +22 -7
  9. package/lib/types/affinity.d.ts +15 -0
  10. package/lib/types/affinity.d.ts.map +1 -1
  11. package/lib/types/affinity.js +14 -0
  12. package/lib/types/client/PluginSettingsCard.d.ts +26 -11
  13. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  14. package/lib/types/client/PluginSettingsCard.js +27 -7
  15. package/lib/types/client/WhalePet.d.ts.map +1 -1
  16. package/lib/types/client/WhalePet.js +2 -1
  17. package/lib/types/client/index.d.ts +1 -1
  18. package/lib/types/client/index.js +3 -3
  19. package/lib/types/client/settings-form.d.ts +14 -5
  20. package/lib/types/client/settings-form.d.ts.map +1 -1
  21. package/lib/types/client/settings-form.js +38 -10
  22. package/lib/types/dsh-home.d.ts +17 -0
  23. package/lib/types/dsh-home.d.ts.map +1 -0
  24. package/lib/types/dsh-home.js +34 -0
  25. package/lib/types/event-projection.d.ts +36 -0
  26. package/lib/types/event-projection.d.ts.map +1 -0
  27. package/lib/types/event-projection.js +98 -0
  28. package/lib/types/ledger.d.ts +77 -0
  29. package/lib/types/ledger.d.ts.map +1 -0
  30. package/lib/types/ledger.js +139 -0
  31. package/lib/types/persist.d.ts +5 -1
  32. package/lib/types/persist.d.ts.map +1 -1
  33. package/lib/types/persist.js +7 -3
  34. package/lib/types/service.d.ts +24 -44
  35. package/lib/types/service.d.ts.map +1 -1
  36. package/lib/types/service.js +98 -131
  37. package/lib/types/state.d.ts +10 -12
  38. package/lib/types/state.d.ts.map +1 -1
  39. package/lib/types/state.js +14 -10
  40. package/package.json +1 -1
  41. package/src/affinity.ts +33 -0
  42. package/src/client/PluginSettingsCard.tsx +83 -17
  43. package/src/client/WhalePet.test.tsx +25 -1
  44. package/src/client/WhalePet.tsx +6 -0
  45. package/src/client/index.ts +3 -3
  46. package/src/client/pet.module.css +9 -0
  47. package/src/client/settings-card.module.css +6 -0
  48. package/src/client/settings-form.ts +41 -9
  49. package/src/dsh-home.test.ts +35 -0
  50. package/src/dsh-home.ts +36 -0
  51. package/src/event-projection.ts +128 -0
  52. package/src/ledger.test.ts +67 -0
  53. package/src/ledger.ts +183 -0
  54. package/src/persist.ts +7 -3
  55. package/src/service.ts +110 -171
  56. package/src/state.test.ts +4 -1
  57. package/src/state.ts +17 -13
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: 6de6b9e5520f85dba7c98490157a939963e2865d
6
- README.zh.md: ad16832b07def470629863af0f1701ad9ef90f94
5
+ README.md: 437e1068038cb013e2880441e69cf1c259f51e79
6
+ README.zh.md: 46511480ae653cc8d6c9b164e9e84ff5645f5945
package/README.md CHANGED
@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
4
4
 
5
5
  > A soft, healing whale-girl who works alongside you in DeepSeek Harness.
6
6
 
7
- While the model thinks, you wait — she swims. She follows the model's working state and switches animations working, waiting, thinking, celebrating completion; you can also pat her head, feed her dried fish, and watch her grow from a baby whale into your deep-sea bond.
7
+ While the model thinks, you wait — she swims. She follows official session activity and switches animations while waiting, thinking, using tools, composing a reply, celebrating completion, or reporting failure; you can also pat her head, feed her dried fish, and watch her grow from a baby whale into your deep-sea bond.
8
8
 
9
9
  Re-implemented from the pet feature of the Codex desktop app, as an official DSH plugin shape (cordis bundle: host half + client half in one package).
10
10
 
@@ -12,7 +12,7 @@ Re-implemented from the pet feature of the Codex desktop app, as an official DSH
12
12
 
13
13
  | Feature | Description |
14
14
  |---|---|
15
- | State animation | Model state → whale-girl animation: `thinking/tool → working`, `waiting → waiting`, `done → jumping celebration`, idle breathes standby |
15
+ | State animation | Official session activity → whale-girl animation: `thinking → running`, `tool → running-right`, `review → review`, `waiting → waiting`, `done → jumping`, `failed failed` |
16
16
  | Head-pat interaction | Click the whale-girl → bubble feedback + affinity +1 (10s cooldown) |
17
17
  | Feeding | Hover panel "喂食" (Feed) → consumes 1 dried fish + affinity +5 (30s cooldown) |
18
18
  | Treat economy | Dried-fish stock (cap 20): +1 every 3 rounds of work, +1 every 30 minutes; when low it prompts "多陪鲸鱼娘工作一会儿" (Work with the whale-girl a bit more) |
@@ -20,7 +20,8 @@ Re-implemented from the pet feature of the Codex desktop app, as an official DSH
20
20
  | Custom naming | Hover panel "改名" (Rename) → 1–20 characters, persisted, echoed in the summon button/panel |
21
21
  | Dragging | Hold and drag the whale-girl to reposition; position persisted |
22
22
  | Hide/Summon | Hover panel "隐藏" (Hide); after hiding, a "召唤{name}" (Summon {name}) button appears in the input selector row |
23
- | Status bubble | While working, shows the model's current status phrase |
23
+ | Status bubble | Shows the current session stage or tool name; transient interaction feedback temporarily takes priority |
24
+ | 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 |
24
25
 
25
26
  ## Animation preview
26
27
 
@@ -41,7 +42,7 @@ dsh-pet/
41
42
  |-- src/
42
43
  | |-- index.ts # host half: plugin entry (cordis apply, route registration)
43
44
  | |-- service.ts # PetService: pet state machine + affinity + config (HTTP API service face)
44
- | |-- state.ts # pet state machine: activity/status phase → 9 state animations
45
+ | |-- state.ts # pet state machine: projected session activity → 9 state animations
45
46
  | |-- affinity.ts # affinity ledger (pure functions + cooldowns)
46
47
  | |-- treats.ts # dried-fish stock ledger
47
48
  | |-- persist.ts # persistence ($DSH_HOME/pet.json, atomic write)
@@ -59,14 +60,17 @@ dsh-pet/
59
60
  ### Data flow
60
61
 
61
62
  ```
62
- activity/status session events (published by the former working-activity plugin) --> PetService (host)
63
+ official session events (turn/step/chunk/tool) ----\
64
+ > PetService (host)
65
+ optional legacy activity/status ------------------/
63
66
  | /api/pet/* JSON
64
67
  global React root (createRoot → document.body) <-- polling 800ms -- pet-client (browser)
65
68
  |
66
69
  WhalePet floating layer (portal + rAF)
67
70
  ```
68
71
 
69
- - **Status source**: listens to the `activity/status` session events (phase: idle/waiting/thinking/tool/done + status phrase), consumed by the host half; the event used to be published by the working-activity plugin, which has been removed from this repo, so without it the pet only changes with the session lifecycle.
72
+ - **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.
73
+ - **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.
70
74
  - **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.
71
75
  - **Rendering**: CSS sprite (background-position) per-frame animation, frame durations from the track definitions in `spritesheet.ts`.
72
76
  - **Communication**: browser ↔ host over the same-origin `/api/pet/*` JSON endpoints (state/interact/set-visible/set-config); the atlas loads from `/pet/whale/spritesheet.webp` — both the RPC domain and the `/plugins/` static service are platform-registered, and the plugin self-sufficiently provides its own API and assets (the same pattern as dsh-remote-web-ui's `/api/pair`).
@@ -93,7 +97,7 @@ After installing, **restart `dsh web`** — the whale-girl appears at the bottom
93
97
 
94
98
  ```sh
95
99
  pnpm build # tsc -b (types+declarations) && tsdown (node half + browser bundle)
96
- pnpm test # vitest unit tests (affinity / treats / persist / state)
100
+ pnpm test # vitest unit/component tests (event projection / state / UI / ledgers)
97
101
  pnpm prepare # transpile-only build (no type checking, for consumer installs)
98
102
  pnpm typecheck # type check only
99
103
  ```
package/README.zh.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  > 一只软萌治愈的鲸鱼娘,陪你在 DeepSeek Harness 里工作。
6
6
 
7
- 模型思考的时候你在等,她在游。她会跟着模型的工作状态切换动画——干活、等待、思考、庆祝完成;你还可以摸头、喂小鱼干,看着她从幼鲸慢慢长成你的深海羁绊。
7
+ 模型思考的时候你在等,她在游。她会跟随官方会话活动,在等待、思考、使用工具、整理回复、庆祝完成或报告失败时切换动画;你还可以摸头、喂小鱼干,看着她从幼鲸慢慢长成你的深海羁绊。
8
8
 
9
9
  复刻自 Codex 桌面版的宠物功能,以 DSH 官方插件形态实现(cordis bundle:host 半区 + client 半区单包)。
10
10
 
@@ -12,7 +12,7 @@
12
12
 
13
13
  | 功能 | 说明 |
14
14
  |---|---|
15
- | 状态动画 | 模型状态 → 鲸鱼娘动画:`thinking/tool → 工作`、`waiting → 等待`、`done → 跳跃庆祝`、空闲 `idle` 呼吸待机 |
15
+ | 状态动画 | 官方会话活动 → 鲸鱼娘动画:`thinking → running`、`tool → running-right`、`review → review`、`waiting → waiting`、`done → jumping`、`failed → failed` |
16
16
  | 摸头互动 | 点击鲸鱼娘 → 气泡反馈 + 亲密度 +1(10s 冷却) |
17
17
  | 喂食 | 悬浮面板「喂食」→ 消耗 1 条小鱼干 + 亲密度 +5(30s 冷却) |
18
18
  | 饲料经济 | 小鱼干库存(上限 20):工作每 3 回合 +1 条、每 30 分钟 +1 条;库存不足会提示「多陪鲸鱼娘工作一会儿」 |
@@ -20,7 +20,8 @@
20
20
  | 自定义命名 | 悬浮面板「改名」→ 1–20 字符,持久化,召唤按钮/面板同步显示 |
21
21
  | 拖动 | 按住鲸鱼娘拖动重新摆放,位置持久化 |
22
22
  | 隐藏/召唤 | 悬浮面板「隐藏」;隐藏后输入选择行出现「召唤{名字}」按钮 |
23
- | 状态气泡 | 工作时显示模型当前状态短语 |
23
+ | 状态气泡 | 显示当前会话阶段或工具名;短暂的交互反馈会临时优先显示 |
24
+ | 多会话活动 | 宠物是 host 全局的:最近一条有效事件决定显示,所有会话的完成回合都会计入亲密度与小鱼干 |
24
25
 
25
26
  ## 动画演示
26
27
 
@@ -41,7 +42,7 @@ dsh-pet/
41
42
  |-- src/
42
43
  | |-- index.ts # host 半区:插件入口(cordis apply,注册路由)
43
44
  | |-- service.ts # PetService:宠物状态机 + 亲密度 + 配置(HTTP API 服务面)
44
- | |-- state.ts # 宠物状态机:activity/status phase → 9 状态动画
45
+ | |-- state.ts # 宠物状态机:投影后的会话活动 → 9 状态动画
45
46
  | |-- affinity.ts # 亲密度账本(纯函数 + 冷却)
46
47
  | |-- treats.ts # 小鱼干库存账本
47
48
  | |-- persist.ts # 持久化($DSH_HOME/pet.json,原子写入)
@@ -59,14 +60,17 @@ dsh-pet/
59
60
  ### 数据流
60
61
 
61
62
  ```
62
- activity/status session 事件(原 working-activity 插件发布) --> PetService(host)
63
+ 官方会话事件(turn/step/chunk/tool) ----\
64
+ > PetService(host)
65
+ 可选的兼容事件 activity/status --------------/
63
66
  | /api/pet/* JSON
64
67
  global React root(createRoot → document.body) <-- 轮询 800ms -- pet-client(浏览器)
65
68
  |
66
69
  WhalePet 浮层(portal + rAF)
67
70
  ```
68
71
 
69
- - **状态源**:监听 `activity/status` 会话事件(phase: idle/waiting/thinking/tool/done + 状态短语),由 host 半区消费;该事件曾由 working-activity 插件发布,插件已从本仓库移除,未安装时宠物只随会话生命周期变化。
72
+ - **状态源**:host 将官方 `turn/start`、`step/start`、`assistant/chunk`、`assistant/message`、`tool/call`、`tool/result` `turn/end` 事件投影为 waiting/thinking/tool/review/done/failed 状态。可选的旧版 `activity/status` 事件仍作为兼容输入。
73
+ - **多会话语义**:API 和浏览器挂载是 host 全局的,也不提供前台会话标识,因此最近一条有效事件决定显示。各会话的完成回合仍独立奖励,销毁非当前会话不会重置可见状态。
70
74
  - **挂载点**:`document.body`(全局 React root,无会话/新会话/会话中全程显示——旧挂载点 `conversation.composer.dock` 只在活跃会话渲染,导致新会话界面看不到宠物),组件内部 `createPortal` 渲染全局浮层。
71
75
  - **渲染**:CSS sprite(background-position)逐帧动画,帧时长来自 `spritesheet.ts` 的轨道定义。
72
76
  - **通信**:浏览器 ↔ host 走同源 `/api/pet/*` JSON 端点(state/interact/set-visible/set-config),图集从 `/pet/whale/spritesheet.webp` 加载——RPC 域与 `/plugins/` 静态服务都是平台注册的,插件自足地提供自己的 API 与素材(与 dsh-remote-web-ui 的 `/api/pair` 同一模式)。
@@ -93,7 +97,7 @@ dsh plugin --profile web add link:$(pwd)/packages/dsh-pet
93
97
 
94
98
  ```sh
95
99
  pnpm build # tsc -b(类型+声明)&& tsdown(node 半区 + 浏览器 bundle)
96
- pnpm test # vitest 单元测试(affinity / treats / persist / state)
100
+ pnpm test # vitest 单元/组件测试(事件投影 / state / UI / 账本)
97
101
  pnpm prepare # 仅转译构建(无类型检查,供消费者安装)
98
102
  pnpm typecheck # 仅类型检查
99
103
  ```
package/lib/client.js CHANGED
@@ -305,7 +305,7 @@ window.__ModuleLoader__.load({
305
305
  }
306
306
  //#endregion
307
307
  //#region \0dsh-css:packages/dsh-pet/src/client/pet.module.css.mjs
308
- const css$1 = ".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}@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}}";
308
+ const css$1 = ".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}}";
309
309
  const tagId$1 = "@linxin666/dsh-pet/pet.module.css";
310
310
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
311
311
  const tag = document.createElement("style");
@@ -320,6 +320,7 @@ window.__ModuleLoader__.load({
320
320
  "bubble": "kz2Bea_bubble",
321
321
  "bubbleFeed": "kz2Bea_bubbleFeed",
322
322
  "bubblePet": "kz2Bea_bubblePet",
323
+ "bubbleStatus": "kz2Bea_bubbleStatus",
323
324
  "float": "kz2Bea_float",
324
325
  "nameCell": "kz2Bea_nameCell",
325
326
  "nameInput": "kz2Bea_nameInput",
@@ -487,6 +488,7 @@ window.__ModuleLoader__.load({
487
488
  };
488
489
  const spriteWidth = Math.round(192 * spriteScale);
489
490
  const spriteHeight = Math.round(208 * spriteScale);
491
+ const statusBubble = feedback === null && !hovered ? snapshot?.bubble : void 0;
490
492
  return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
491
493
  ref: floatRef,
492
494
  className: pet_module_css_default.float,
@@ -532,6 +534,12 @@ window.__ModuleLoader__.load({
532
534
  className: `${pet_module_css_default.bubble} ${feedback.kind === "feed" ? pet_module_css_default.bubbleFeed : pet_module_css_default.bubblePet}`,
533
535
  children: feedback.text
534
536
  }, feedback.at),
537
+ statusBubble !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
538
+ className: `${pet_module_css_default.bubble} ${pet_module_css_default.bubbleStatus}`,
539
+ role: "status",
540
+ "aria-live": "polite",
541
+ children: statusBubble
542
+ }),
535
543
  hovered && dragRef.current === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
536
544
  className: pet_module_css_default.panel,
537
545
  onPointerEnter: () => {
@@ -678,7 +686,7 @@ window.__ModuleLoader__.load({
678
686
  }
679
687
  //#endregion
680
688
  //#region \0dsh-css:packages/dsh-pet/src/client/settings-card.module.css.mjs
681
- const css = ".kKk9aW_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.kKk9aW_card:hover{border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.kKk9aW_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.kKk9aW_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.kKk9aW_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.kKk9aW_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.kKk9aW_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.kKk9aW_chevronOpen{transform:rotate(180deg)}.kKk9aW_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.kKk9aW_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.kKk9aW_failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}.kKk9aW_discard,.kKk9aW_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.kKk9aW_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.kKk9aW_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.kKk9aW_discard:disabled,.kKk9aW_save:disabled{opacity:.4;cursor:default}.kKk9aW_discard:focus-visible,.kKk9aW_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.kKk9aW_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.kKk9aW_field+.kKk9aW_field{border-top:1px solid var(--dsw-alias-border-l2)}.kKk9aW_head{align-items:center;gap:8px;display:flex}.kKk9aW_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.kKk9aW_badges{align-items:center;gap:8px;display:inline-flex}.kKk9aW_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.kKk9aW_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.kKk9aW_reset:disabled{cursor:default}.kKk9aW_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.kKk9aW_input,.kKk9aW_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_input:focus-visible,.kKk9aW_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.kKk9aW_input:disabled,.kKk9aW_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.kKk9aW_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.kKk9aW_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.kKk9aW_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.kKk9aW_card,.kKk9aW_header,.kKk9aW_chevron,.kKk9aW_chevronOpen,.kKk9aW_discard,.kKk9aW_save{transition:none}}";
689
+ const css = ".kKk9aW_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.kKk9aW_card:hover{border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.kKk9aW_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.kKk9aW_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.kKk9aW_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.kKk9aW_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.kKk9aW_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.kKk9aW_chevronOpen{transform:rotate(180deg)}.kKk9aW_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.kKk9aW_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.kKk9aW_failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}.kKk9aW_discard,.kKk9aW_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.kKk9aW_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.kKk9aW_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.kKk9aW_discard:disabled,.kKk9aW_save:disabled{opacity:.4;cursor:default}.kKk9aW_discard:focus-visible,.kKk9aW_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.kKk9aW_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.kKk9aW_field+.kKk9aW_field{border-top:1px solid var(--dsw-alias-border-l2)}.kKk9aW_head{align-items:center;gap:8px;display:flex}.kKk9aW_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.kKk9aW_badges{align-items:center;gap:8px;display:inline-flex}.kKk9aW_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.kKk9aW_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.kKk9aW_reset:disabled{cursor:default}.kKk9aW_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.kKk9aW_input,.kKk9aW_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_input:focus-visible,.kKk9aW_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.kKk9aW_input:disabled,.kKk9aW_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.kKk9aW_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.kKk9aW_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.kKk9aW_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.kKk9aW_card,.kKk9aW_header,.kKk9aW_chevron,.kKk9aW_chevronOpen,.kKk9aW_discard,.kKk9aW_save{transition:none}}";
682
690
  const tagId = "@linxin666/dsh-pet/settings-card.module.css";
683
691
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
684
692
  const tag = document.createElement("style");
@@ -719,12 +727,12 @@ window.__ModuleLoader__.load({
719
727
  //#endregion
720
728
  //#region src/client/PluginSettingsCard.tsx
721
729
  /**
722
- * Shared chrome for the plugin settings card: a disclosure header naming the
723
- * plugin and what its settings govern, the controls inside, and the save that
724
- * writes them. Renders nothing while the namespace is unavailable — a
730
+ * Family-shared chrome for plugin settings cards: a disclosure header naming
731
+ * the plugin and what its settings govern, the controls inside, and the save
732
+ * that writes them. Renders nothing while the namespace is unavailable — a
725
733
  * deployment that does not compose the owning plugin should show no trace of
726
- * it. Mirrors the official ui-plugin-config PluginCard in a self-contained
727
- * slice (this package must not depend on a sibling UI package).
734
+ * it. Inlined into each consumer's client bundle; mirrors the official
735
+ * ui-plugin-config PluginCard in a self-contained slice.
728
736
  */
729
737
  /**
730
738
  * Render one plugin settings card.
@@ -736,6 +744,7 @@ window.__ModuleLoader__.load({
736
744
  const { state } = props;
737
745
  if (!state.available) return null;
738
746
  const title = props.t(props.titleKey);
747
+ const description = props.t(props.descriptionKey);
739
748
  const blocked = !state.dirty || state.invalid || state.saving;
740
749
  const cardClass = open ? `${settings_card_module_css_default.cardOpen} ${settings_card_module_css_default.card}` : settings_card_module_css_default.card;
741
750
  if (!state.exposed) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
@@ -752,10 +761,12 @@ window.__ModuleLoader__.load({
752
761
  className: settings_card_module_css_default.headText,
753
762
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
754
763
  className: settings_card_module_css_default.name,
764
+ title,
755
765
  children: title
756
766
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
757
767
  className: settings_card_module_css_default.description,
758
- children: props.t(props.descriptionKey)
768
+ title: description,
769
+ children: description
759
770
  })]
760
771
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
761
772
  width: "14",
@@ -793,14 +804,17 @@ window.__ModuleLoader__.load({
793
804
  className: settings_card_module_css_default.headText,
794
805
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
795
806
  className: settings_card_module_css_default.name,
807
+ title,
796
808
  children: title
797
809
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
798
810
  className: settings_card_module_css_default.description,
799
- children: props.t(props.descriptionKey)
811
+ title: description,
812
+ children: description
800
813
  })]
801
814
  }),
802
815
  state.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
803
816
  className: settings_card_module_css_default.pending,
817
+ title: props.t("settings.unsaved"),
804
818
  children: props.t("settings.unsaved")
805
819
  }) : null,
806
820
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
@@ -955,8 +969,9 @@ window.__ModuleLoader__.load({
955
969
  }
956
970
  //#endregion
957
971
  //#region src/client/settings-form.ts
958
- /** A whole-number field. An empty draft clears the field; any other draft that is not a finite number blocks the save. */
959
- function numberField(field) {
972
+ /** A whole- or decimal-number field. An empty draft clears the field; any other draft that is not a finite number within the constraints blocks the save. */
973
+ function numberField(field, constraints = {}) {
974
+ const { integer = false, min } = constraints;
960
975
  return {
961
976
  field,
962
977
  format: (value) => typeof value === "number" ? String(value) : "",
@@ -964,10 +979,13 @@ window.__ModuleLoader__.load({
964
979
  const trimmed = text.trim();
965
980
  if (trimmed === "") return { kind: "clear" };
966
981
  const parsed = Number(trimmed);
967
- return Number.isFinite(parsed) ? {
982
+ if (!Number.isFinite(parsed)) return void 0;
983
+ if (integer && !Number.isInteger(parsed)) return void 0;
984
+ if (min !== void 0 && parsed < min) return void 0;
985
+ return {
968
986
  kind: "set",
969
987
  value: parsed
970
- } : void 0;
988
+ };
971
989
  }
972
990
  };
973
991
  }
@@ -991,11 +1009,13 @@ window.__ModuleLoader__.load({
991
1009
  field,
992
1010
  format: (value) => typeof value === "boolean" ? String(value) : "",
993
1011
  parse: (text) => {
994
- if (text === "true") return {
1012
+ const trimmed = text.trim();
1013
+ if (trimmed === "") return { kind: "clear" };
1014
+ if (trimmed === "true") return {
995
1015
  kind: "set",
996
1016
  value: true
997
1017
  };
998
- if (text === "false") return {
1018
+ if (trimmed === "false") return {
999
1019
  kind: "set",
1000
1020
  value: false
1001
1021
  };
@@ -1097,12 +1117,13 @@ window.__ModuleLoader__.load({
1097
1117
  const plan = this.plan();
1098
1118
  const writes = plan.flatMap((item) => item.run === void 0 ? [] : [item.run]);
1099
1119
  if (plan.length === 0 || this.saving || writes.length !== plan.length) return;
1120
+ const fields = new Set(plan.map((item) => item.field));
1100
1121
  this.saving = true;
1101
1122
  this.failed = false;
1102
1123
  this.publish();
1103
1124
  let landed = true;
1104
1125
  for (const write of writes) landed = await write() && landed;
1105
- if (landed) this.staged.clear();
1126
+ if (landed) for (const field of fields) this.staged.delete(field);
1106
1127
  this.saving = false;
1107
1128
  this.failed = !landed;
1108
1129
  this.publish();
@@ -1464,7 +1485,7 @@ window.__ModuleLoader__.load({
1464
1485
  setName: (name) => petFetch("/api/pet/set-name", { name })
1465
1486
  };
1466
1487
  /** Poll interval for the host snapshot. */
1467
- const POLL_MS = 800;
1488
+ const POLL_MS = 2e3;
1468
1489
  /** Settings namespace the pet settings card edits (the Host plugin registers it). */
1469
1490
  const PET_SETTINGS_NS = "pet";
1470
1491
  /** Required services. */