@linxin666/dsh-pet 0.3.6 → 0.3.10

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 (52) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +4 -0
  3. package/README.zh.md +4 -0
  4. package/lib/client.js +187 -88
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +89 -11
  7. package/lib/live2d-vendor.js +106 -95
  8. package/lib/live2d-vendor.js.map +1 -1
  9. package/lib/types/announce.d.ts +53 -0
  10. package/lib/types/announce.d.ts.map +1 -0
  11. package/lib/types/announce.js +80 -0
  12. package/lib/types/client/PetDockEntry.d.ts +14 -5
  13. package/lib/types/client/PetDockEntry.d.ts.map +1 -1
  14. package/lib/types/client/PetDockEntry.js +6 -5
  15. package/lib/types/client/PetSettingsCard.d.ts +2 -1
  16. package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
  17. package/lib/types/client/PetSprite.d.ts +8 -0
  18. package/lib/types/client/PetSprite.d.ts.map +1 -1
  19. package/lib/types/client/PetSprite.js +29 -4
  20. package/lib/types/client/index.d.ts +8 -5
  21. package/lib/types/client/index.d.ts.map +1 -1
  22. package/lib/types/client/index.js +11 -5
  23. package/lib/types/client/locales.d.ts +2 -2
  24. package/lib/types/client/locales.js +2 -2
  25. package/lib/types/client/pet-store.d.ts +1 -1
  26. package/lib/types/client/pet-store.d.ts.map +1 -1
  27. package/lib/types/client/pet-store.js +1 -1
  28. package/lib/types/client/settings-form.d.ts +35 -43
  29. package/lib/types/client/settings-form.d.ts.map +1 -1
  30. package/lib/types/client/settings-form.js +60 -53
  31. package/lib/types/index.d.ts.map +1 -1
  32. package/lib/types/index.js +10 -9
  33. package/lib/types/service.d.ts +18 -0
  34. package/lib/types/service.d.ts.map +1 -1
  35. package/lib/types/service.js +22 -0
  36. package/package.json +17 -13
  37. package/src/announce.ts +103 -0
  38. package/src/client/PetDockEntry.test.tsx +1 -1
  39. package/src/client/PetDockEntry.tsx +15 -4
  40. package/src/client/PetSettingsCard.tsx +2 -1
  41. package/src/client/PetSprite.test.tsx +18 -0
  42. package/src/client/PetSprite.tsx +66 -3
  43. package/src/client/gameplay-hud.test.tsx +1 -1
  44. package/src/client/index.test.tsx +2 -2
  45. package/src/client/index.ts +17 -6
  46. package/src/client/locales.ts +2 -2
  47. package/src/client/pet-store.ts +2 -2
  48. package/src/client/pet.module.css +104 -0
  49. package/src/client/settings-form.ts +78 -92
  50. package/src/dsh-home.ts +1 -1
  51. package/src/index.ts +17 -15
  52. package/src/service.ts +28 -1
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: 2501ae7db35246c4348ed08fd169ee58a7569a95
6
- README.zh.md: a490c37210773c3da3eec1d9c61807864f56b6aa
5
+ README.md: 836e699d1b34cdd8708ccee4f97b3c07c0c4b73a
6
+ README.zh.md: 6d5cbc8d68853105df294255cb76cbbf5c2757c8
package/README.md CHANGED
@@ -196,6 +196,10 @@ A status bubble can carry a small ornament ahead of its text (built-in: the spou
196
196
  - Structure is fail-closed (unknown fields, out-of-range geometry, non-PNG/WebP entries reject with diagnostics); segment content is warn-and-drop. The machine-readable twin lives at contracts/status-decoration-v1.schema.json; the authoritative validator is src/decoration.ts.
197
197
  - Sources: built-in assets/decorations/ plus the user directory $DSH_HOME/pets/decorations/<id>/ (same id overrides the built-in). Assets ride /api/pet/decoration/<id>/<file> with the same containment and allow-lists as pet assets.
198
198
  - Switch: Settings > Pet > Status decoration (on by default). The built-in whale derives from the DeepSeek wordmark (MIT); see THIRD_PARTY_NOTICES.md.
199
+ ## Announcement bubbles (pet.announce, dsh-usage linkage)
200
+
201
+ Host-side sibling plugins can push one structured announcement through the `pet` cordis service (`ctx.pet.announce({ source, kind, title, ... })`); the browser half renders it as a dedicated, specially styled bubble riding the top of the session bubble stack — its own glass style with a tone accent (`ok`/`warn`/`low`), an amount pill for balance or today-spend kinds, a mini meter for plan-percent windows, and the reset instant. The payload is validated into a bounded shape (src/announce.ts): unknown fields drop, text truncates, TTL clamps to 1 s - 2 h (default 10 s; a repeating announcer declares its poll cadence so an always-mode bubble stays continuous, and the ceiling still unmounts a dead source's bubble within one missed refresh), and a malformed announcement is dropped silently. Announcements are in-memory only: the freshest one wins, an expired one stops rendering, and neither pet.json nor the ledger is touched. There is deliberately no HTTP surface — the API is the in-process service, per the cross-plugin collaboration rule.
202
+
199
203
  ## Built-in pets
200
204
 
201
205
  | Registry id | Selector label | Source |
package/README.zh.md CHANGED
@@ -196,6 +196,10 @@ frames2d 宠物可声明 `gameplay` 块——从 miku 桌宠泛化而来的可
196
196
  - 结构 fail-closed(未知字段、越界尺寸、非 PNG/WebP 入口直接拒载并进诊断),帧段内容 warn-and-drop。机器可读 schema 见 contracts/status-decoration-v1.schema.json,权威校验器为 src/decoration.ts。
197
197
  - 来源:内置 assets/decorations/ + 用户目录 $DSH_HOME/pets/decorations/<id>/(同 id 覆盖内置)。资产经 /api/pet/decoration/<id>/<file> 路由,containment 与白名单与宠物资产同构。
198
198
  - 开关:设置 → 宠物 → 状态装饰(默认开)。内置鲸鱼素材派生自 DeepSeek wordmark(MIT),完整声明见 THIRD_PARTY_NOTICES.md。
199
+ ## 公告气泡(pet.announce,dsh-usage 联动)
200
+
201
+ 宿主侧的兄弟插件可通过 `pet` cordis 服务推送一条结构化公告(`ctx.pet.announce({ source, kind, title, ... })`);客户端半区将其渲染为一只专用、特别设计的气泡,挂在会话气泡栈顶部——独立玻璃样式与色调描边(`ok`/`warn`/`low`)、余额与今日消费类金额胶囊、套餐百分比窗口带微型计量条、展示重置时间。载荷经有界校验(src/announce.ts):未知字段丢弃、超长文本截断、TTL 收敛到 1 秒 - 2 小时(默认 10 秒;周期型发布方按自身轮询节奏声明 TTL,常驻气泡因此跨轮询连续,上限仍保证失活来源的气泡至多一个刷新周期内消失),畸形公告静默丢弃。公告仅存内存:最新一条生效,过期即不再渲染,不触碰 pet.json 与台账。刻意不提供 HTTP 面——按跨插件协作规则,API 就是进程内服务。
202
+
199
203
  ## 内置宠物
200
204
 
201
205
  | 注册表 id | 选择器名称 | 来源 |
package/lib/client.js CHANGED
@@ -6,9 +6,21 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  let react_dom_client = require("react-dom/client");
9
- let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
10
9
  let react_dom = require("react-dom");
11
10
  let react_jsx_runtime = require("react/jsx-runtime");
11
+ //#region \0dsh-store-engine
12
+ const platform = ["@deepseek-ai/dsh-client", "-store"].join("");
13
+ const legacy = ["@deepseek-ai/dsh-client-runtime", "/client"].join("");
14
+ let engine;
15
+ try {
16
+ engine = require(platform);
17
+ } catch {
18
+ engine = require(legacy);
19
+ }
20
+ const createSnapshotStore = engine.createSnapshotStore;
21
+ const defineStore = engine.defineStore;
22
+ engine.shallowEqual;
23
+ //#endregion
12
24
  //#region src/client/pet-store.ts
13
25
  /**
14
26
  * Browser-side pet store: the pet state snapshot plus transient UI feedback
@@ -19,7 +31,7 @@ window.__ModuleLoader__.load({
19
31
  */
20
32
  /** Create the pet store handle (apply world only; never module-level). */
21
33
  function createPetStore() {
22
- return (0, _deepseek_ai_dsh_client_runtime_client.defineStore)({
34
+ return defineStore({
23
35
  init: () => ({
24
36
  snapshot: null,
25
37
  pets: [],
@@ -68,6 +80,12 @@ window.__ModuleLoader__.load({
68
80
  return n;
69
81
  }
70
82
  //#endregion
83
+ //#region src/announce.ts
84
+ /** Whether an announcement is still fresh at `now`. */
85
+ function announcementFresh(announcement, now) {
86
+ return now - announcement.at < announcement.ttlMs;
87
+ }
88
+ //#endregion
71
89
  //#region src/state.ts
72
90
  /**
73
91
  * Map one activity phase onto the animation contract.
@@ -171,7 +189,7 @@ window.__ModuleLoader__.load({
171
189
  }
172
190
  //#endregion
173
191
  //#region \0dsh-css:packages/dsh-pet/src/client/pet.module.css.mjs
174
- const css$2 = ".kz2Bea_float{pointer-events:auto;-webkit-user-select:none;user-select:none;will-change:transform;contain:layout style;flex-direction:column;align-items:center;display:flex;position:fixed}.kz2Bea_sprite{image-rendering:auto;touch-action:none;contain:paint}.kz2Bea_spriteWrap{flex:none;position:relative}.kz2Bea_bubble{white-space:nowrap;color:#f4f7ff;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:#4d6bfef2}.kz2Bea_bubbleFeed{background:#0369a1f2}.kz2Bea_bubbleStatus{text-overflow:ellipsis;backdrop-filter:blur(8px);letter-spacing:.02em;background:linear-gradient(160deg,#131c36e6,#070b1af0);border:1px solid #7e98ff73;max-width:min(280px,100vw - 24px);padding:5px 12px;animation:.24s ease-out kz2Bea_pet-bubble-in;overflow:hidden;box-shadow:0 4px 12px #02061759,inset 0 1px #e2e8ff1a,0 0 12px #4d6bfe2e}.kz2Bea_bubbleWhisper{letter-spacing:.03em;animation:.32s cubic-bezier(.22,1,.36,1) kz2Bea_pet-whisper-in}.kz2Bea_bubbleWhisper:before{content:\"「\";opacity:.7;margin-right:2px}.kz2Bea_bubbleWhisper:after{content:\"」\";opacity:.7;margin-left:2px}.kz2Bea_bubbleStack{pointer-events:auto;flex-direction:column-reverse;align-items:center;gap:4px;display:flex;position:absolute;bottom:100%}.kz2Bea_bubbleAnchor{display:inline-flex;position:relative}.kz2Bea_bubbleMore{z-index:1;color:#eef2ff;cursor:pointer;background:linear-gradient(160deg,#4a68f5f2,#2f44b8f2);border:1px solid #7e98ff8c;border-radius:999px;justify-content:center;align-items:center;min-width:18px;height:18px;padding:0 5px;font-size:10px;font-weight:600;line-height:1;transition:filter .12s;display:inline-flex;position:absolute;top:-9px;right:-10px;box-shadow:0 2px 6px #02061766}.kz2Bea_bubbleMore:hover{filter:brightness(1.15)}.kz2Bea_bubbleMore:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98ffe6}.kz2Bea_bubbleStack .kz2Bea_bubble{pointer-events:auto;margin-bottom:0;position:relative;bottom:auto}.kz2Bea_bubbleClickable{cursor:pointer;font:inherit;text-align:center;appearance:none;transition:filter .12s,box-shadow .12s}.kz2Bea_bubbleClickable:hover{filter:brightness(1.12)}.kz2Bea_bubbleClickable:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98ffe6}@keyframes kz2Bea_pet-bubble-in{0%{opacity:0;transform:translateY(6px)scale(.96)}to{opacity:1;transform:translateY(0)scale(1)}}@keyframes kz2Bea_pet-whisper-in{0%{opacity:0;transform:translateY(8px)scale(.9)}60%{transform:translateY(-1px)scale(1.02)}to{opacity:1;transform:translateY(0)scale(1)}}@keyframes kz2Bea_pet-panel-in{0%{opacity:0;transform:translateY(4px)scale(.97)}to{opacity:1;transform:translateY(0)scale(1)}}@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:#e6ebf8;backdrop-filter:blur(10px);transform-origin:50% 0;background:linear-gradient(165deg,#131c36f2,#070b1af7);border:1px solid #7e98ff4d;border-radius:12px;flex-direction:column;gap:7px;min-width:148px;margin-top:8px;padding:10px 12px;font-size:12px;animation:.2s cubic-bezier(.22,1,.36,1) kz2Bea_pet-panel-in;display:flex;position:absolute;top:100%;box-shadow:0 8px 24px #02061773,0 0 0 1px #4d6bfe1a,inset 0 1px #e2e8ff14}.kz2Bea_panel:after{content:\"\";height:14px;position:absolute;bottom:100%;left:0;right:0}.kz2Bea_panelAbove{transform-origin:50% 100%;margin-top:0;margin-bottom:8px;top:auto;bottom:100%}.kz2Bea_panelAbove:after{top:100%;bottom:auto}.kz2Bea_rankRow{white-space:nowrap;justify-content:space-between;gap:10px;display:flex}.kz2Bea_nameCell{letter-spacing:.02em;background:linear-gradient(90deg,#a9c1ff,#6c8bff 60%,#4d6bfe);color:#0000;-webkit-background-clip:text;background-clip:text;font-weight:600}.kz2Bea_statRank{color:#9db4ff;font-weight:600}.kz2Bea_statTreats{color:#7dd3fc;font-weight:600}.kz2Bea_statPoints{color:#fcd34d;font-weight:600}.kz2Bea_renameRow{align-items:center;gap:6px;display:flex}.kz2Bea_nameInput{color:#e6ebf8;background:#131c36e6;border:1px solid #7e98ff80;border-radius:6px;outline:none;flex:1;min-width:0;padding:3px 6px;font-size:12px}.kz2Bea_nameInput:focus{border-color:#4d6bfe;box-shadow:0 0 0 2px #4d6bfe73}.kz2Bea_actions{gap:6px;display:flex}.kz2Bea_action{cursor:pointer;color:#fff;background:linear-gradient(#4a68f5,#3a55e0);border:none;border-radius:6px;flex:1;padding:4px 8px;font-size:12px;font-weight:600;transition:filter .12s,box-shadow .12s,transform .12s;box-shadow:0 2px 6px #4d6bfe4d}.kz2Bea_action:hover{filter:brightness(1.08);transform:translateY(-1px);box-shadow:0 4px 10px #4d6bfe66}.kz2Bea_action:active{filter:brightness(.94);transform:translateY(0);box-shadow:0 1px 4px #4d6bfe4d}.kz2Bea_action:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98ffe6}.kz2Bea_summon{color:#8ea6ff;cursor:pointer;background:#070b1abf;border:1px dashed #7e98ff99;border-radius:999px;padding:2px 10px;font-size:11px;transition:border-color .12s,color .12s,background .12s,box-shadow .12s}.kz2Bea_summon:hover{color:#c3d3ff;background:#070b1ae6;border-color:#7e98fff2}.kz2Bea_summon:active{color:#8ea6ff;border-color:#7e98ffcc}.kz2Bea_summon:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98ffe6}@media (prefers-reduced-motion:reduce){.kz2Bea_bubble,.kz2Bea_bubbleStatus,.kz2Bea_bubbleWhisper,.kz2Bea_panel{opacity:1;animation:none}.kz2Bea_action,.kz2Bea_summon,.kz2Bea_bubbleMore{transition:none}}.kz2Bea_gameplayHud{width:0;height:0;position:absolute;bottom:0;left:0}.kz2Bea_gameplayModeChip{color:#cdd7ff;white-space:nowrap;pointer-events:none;background:#7e98ff2e;border-radius:999px;padding:1px 8px;font-size:10px;position:absolute;top:0;left:0;transform:translateY(-100%)translateY(-4px)}.kz2Bea_gameplayCard{color:#dfe6ff;background:#070b1aeb;border:1px solid #7e98ff40;border-radius:10px;min-width:180px;max-height:340px;padding:10px 12px;font-size:12px;position:absolute;bottom:0;left:0;overflow-y:auto;transform:none;box-shadow:0 8px 24px #0006}.kz2Bea_gameplayBars{flex-direction:column;gap:4px;margin-bottom:8px;display:flex}.kz2Bea_gameplayBarRow{align-items:center;gap:6px;display:flex}.kz2Bea_gameplayBarLabel{color:#9aa8d8;flex-shrink:0;width:34px}.kz2Bea_gameplayBarTrack{background:#7e98ff26;border-radius:3px;flex:1;height:6px;overflow:hidden}.kz2Bea_gameplayBarFill{background:linear-gradient(90deg,#7e98ff,#9db4ff);border-radius:3px;height:100%;transition:width .3s;display:block}.kz2Bea_gameplayActions{flex-wrap:wrap;gap:6px;display:flex}.kz2Bea_gameplayShopItems{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-bottom:8px;display:grid}.kz2Bea_gameplayShopItem{color:inherit;cursor:pointer;background:#7e98ff14;border:1px solid #7e98ff40;border-radius:8px;flex-direction:column;align-items:center;gap:4px;padding:8px 6px;display:flex}.kz2Bea_gameplayShopItem:hover{background:#7e98ff29;border-color:#7e98ffb3}.kz2Bea_gameplayShopItemImage{object-fit:contain;width:36px;height:36px;image-rendering:pixelated;pointer-events:none}.kz2Bea_gameplayShopItemLabel{text-align:center;font-size:11px}.kz2Bea_gameplayShopItemPrice{color:#ffd27e;font-size:10px}.kz2Bea_gameplayClose{color:#9aa8d8;cursor:pointer;background:0 0;border:none;padding:0 2px;font-size:14px;position:absolute;top:4px;right:8px}.kz2Bea_gameplayClose:hover{color:#fff}.kz2Bea_gameplayFloat{color:#3a2a00;white-space:nowrap;pointer-events:none;background:#ffd27eeb;border-radius:999px;padding:2px 10px;font-size:11px;animation:1.1s ease-out forwards kz2Bea_pet-gameplay-float;position:absolute;top:-8px;left:50%;transform:translate(-50%)}@keyframes kz2Bea_pet-gameplay-float{0%{opacity:0;transform:translate(-50%)translateY(6px)}20%{opacity:1}to{opacity:0;transform:translate(-50%)translateY(-18px)}}@media (prefers-reduced-motion:reduce){.kz2Bea_gameplayFloat{opacity:1;animation:none}.kz2Bea_gameplayBarFill{transition:none}}";
192
+ const css$2 = ".kz2Bea_float{pointer-events:auto;-webkit-user-select:none;user-select:none;will-change:transform;contain:layout style;flex-direction:column;align-items:center;display:flex;position:fixed}.kz2Bea_sprite{image-rendering:auto;touch-action:none;contain:paint}.kz2Bea_spriteWrap{flex:none;position:relative}.kz2Bea_bubble{white-space:nowrap;color:#f4f7ff;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:#4d6bfef2}.kz2Bea_bubbleFeed{background:#0369a1f2}.kz2Bea_bubbleStatus{text-overflow:ellipsis;backdrop-filter:blur(8px);letter-spacing:.02em;background:linear-gradient(160deg,#131c36e6,#070b1af0);border:1px solid #7e98ff73;max-width:min(280px,100vw - 24px);padding:5px 12px;animation:.24s ease-out kz2Bea_pet-bubble-in;overflow:hidden;box-shadow:0 4px 12px #02061759,inset 0 1px #e2e8ff1a,0 0 12px #4d6bfe2e}.kz2Bea_bubbleWhisper{letter-spacing:.03em;animation:.32s cubic-bezier(.22,1,.36,1) kz2Bea_pet-whisper-in}.kz2Bea_bubbleWhisper:before{content:\"「\";opacity:.7;margin-right:2px}.kz2Bea_bubbleWhisper:after{content:\"」\";opacity:.7;margin-left:2px}.kz2Bea_bubbleStack{pointer-events:auto;flex-direction:column-reverse;align-items:center;gap:4px;display:flex;position:absolute;bottom:100%}.kz2Bea_bubbleAnchor{display:inline-flex;position:relative}.kz2Bea_bubbleMore{z-index:1;color:#eef2ff;cursor:pointer;background:linear-gradient(160deg,#4a68f5f2,#2f44b8f2);border:1px solid #7e98ff8c;border-radius:999px;justify-content:center;align-items:center;min-width:18px;height:18px;padding:0 5px;font-size:10px;font-weight:600;line-height:1;transition:filter .12s;display:inline-flex;position:absolute;top:-9px;right:-10px;box-shadow:0 2px 6px #02061766}.kz2Bea_bubbleMore:hover{filter:brightness(1.15)}.kz2Bea_bubbleMore:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98ffe6}.kz2Bea_bubbleStack .kz2Bea_bubble{pointer-events:auto;margin-bottom:0;position:relative;bottom:auto}.kz2Bea_bubbleClickable{cursor:pointer;font:inherit;text-align:center;appearance:none;transition:filter .12s,box-shadow .12s}.kz2Bea_bubbleClickable:hover{filter:brightness(1.12)}.kz2Bea_bubbleClickable:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98ffe6}@keyframes kz2Bea_pet-bubble-in{0%{opacity:0;transform:translateY(6px)scale(.96)}to{opacity:1;transform:translateY(0)scale(1)}}@keyframes kz2Bea_pet-whisper-in{0%{opacity:0;transform:translateY(8px)scale(.9)}60%{transform:translateY(-1px)scale(1.02)}to{opacity:1;transform:translateY(0)scale(1)}}@keyframes kz2Bea_pet-panel-in{0%{opacity:0;transform:translateY(4px)scale(.97)}to{opacity:1;transform:translateY(0)scale(1)}}@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:#e6ebf8;backdrop-filter:blur(10px);transform-origin:50% 0;background:linear-gradient(165deg,#131c36f2,#070b1af7);border:1px solid #7e98ff4d;border-radius:12px;flex-direction:column;gap:7px;min-width:148px;margin-top:8px;padding:10px 12px;font-size:12px;animation:.2s cubic-bezier(.22,1,.36,1) kz2Bea_pet-panel-in;display:flex;position:absolute;top:100%;box-shadow:0 8px 24px #02061773,0 0 0 1px #4d6bfe1a,inset 0 1px #e2e8ff14}.kz2Bea_panel:after{content:\"\";height:14px;position:absolute;bottom:100%;left:0;right:0}.kz2Bea_panelAbove{transform-origin:50% 100%;margin-top:0;margin-bottom:8px;top:auto;bottom:100%}.kz2Bea_panelAbove:after{top:100%;bottom:auto}.kz2Bea_rankRow{white-space:nowrap;justify-content:space-between;gap:10px;display:flex}.kz2Bea_nameCell{letter-spacing:.02em;background:linear-gradient(90deg,#a9c1ff,#6c8bff 60%,#4d6bfe);color:#0000;-webkit-background-clip:text;background-clip:text;font-weight:600}.kz2Bea_statRank{color:#9db4ff;font-weight:600}.kz2Bea_statTreats{color:#7dd3fc;font-weight:600}.kz2Bea_statPoints{color:#fcd34d;font-weight:600}.kz2Bea_renameRow{align-items:center;gap:6px;display:flex}.kz2Bea_nameInput{color:#e6ebf8;background:#131c36e6;border:1px solid #7e98ff80;border-radius:6px;outline:none;flex:1;min-width:0;padding:3px 6px;font-size:12px}.kz2Bea_nameInput:focus{border-color:#4d6bfe;box-shadow:0 0 0 2px #4d6bfe73}.kz2Bea_actions{gap:6px;display:flex}.kz2Bea_action{cursor:pointer;color:#fff;background:linear-gradient(#4a68f5,#3a55e0);border:none;border-radius:6px;flex:1;padding:4px 8px;font-size:12px;font-weight:600;transition:filter .12s,box-shadow .12s,transform .12s;box-shadow:0 2px 6px #4d6bfe4d}.kz2Bea_action:hover{filter:brightness(1.08);transform:translateY(-1px);box-shadow:0 4px 10px #4d6bfe66}.kz2Bea_action:active{filter:brightness(.94);transform:translateY(0);box-shadow:0 1px 4px #4d6bfe4d}.kz2Bea_action:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98ffe6}.kz2Bea_summon{color:#8ea6ff;cursor:pointer;background:#070b1abf;border:1px dashed #7e98ff99;border-radius:999px;padding:2px 10px;font-size:11px;transition:border-color .12s,color .12s,background .12s,box-shadow .12s}.kz2Bea_summon:hover{color:#c3d3ff;background:#070b1ae6;border-color:#7e98fff2}.kz2Bea_summon:active{color:#8ea6ff;border-color:#7e98ffcc}.kz2Bea_summon:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98ffe6}@media (prefers-reduced-motion:reduce){.kz2Bea_bubble,.kz2Bea_bubbleStatus,.kz2Bea_bubbleWhisper,.kz2Bea_panel{opacity:1;animation:none}.kz2Bea_action,.kz2Bea_summon,.kz2Bea_bubbleMore{transition:none}}.kz2Bea_gameplayHud{width:0;height:0;position:absolute;bottom:0;left:0}.kz2Bea_gameplayModeChip{color:#cdd7ff;white-space:nowrap;pointer-events:none;background:#7e98ff2e;border-radius:999px;padding:1px 8px;font-size:10px;position:absolute;top:0;left:0;transform:translateY(-100%)translateY(-4px)}.kz2Bea_gameplayCard{color:#dfe6ff;background:#070b1aeb;border:1px solid #7e98ff40;border-radius:10px;min-width:180px;max-height:340px;padding:10px 12px;font-size:12px;position:absolute;bottom:0;left:0;overflow-y:auto;transform:none;box-shadow:0 8px 24px #0006}.kz2Bea_gameplayBars{flex-direction:column;gap:4px;margin-bottom:8px;display:flex}.kz2Bea_gameplayBarRow{align-items:center;gap:6px;display:flex}.kz2Bea_gameplayBarLabel{color:#9aa8d8;flex-shrink:0;width:34px}.kz2Bea_gameplayBarTrack{background:#7e98ff26;border-radius:3px;flex:1;height:6px;overflow:hidden}.kz2Bea_gameplayBarFill{background:linear-gradient(90deg,#7e98ff,#9db4ff);border-radius:3px;height:100%;transition:width .3s;display:block}.kz2Bea_gameplayActions{flex-wrap:wrap;gap:6px;display:flex}.kz2Bea_gameplayShopItems{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-bottom:8px;display:grid}.kz2Bea_gameplayShopItem{color:inherit;cursor:pointer;background:#7e98ff14;border:1px solid #7e98ff40;border-radius:8px;flex-direction:column;align-items:center;gap:4px;padding:8px 6px;display:flex}.kz2Bea_gameplayShopItem:hover{background:#7e98ff29;border-color:#7e98ffb3}.kz2Bea_gameplayShopItemImage{object-fit:contain;width:36px;height:36px;image-rendering:pixelated;pointer-events:none}.kz2Bea_gameplayShopItemLabel{text-align:center;font-size:11px}.kz2Bea_gameplayShopItemPrice{color:#ffd27e;font-size:10px}.kz2Bea_gameplayClose{color:#9aa8d8;cursor:pointer;background:0 0;border:none;padding:0 2px;font-size:14px;position:absolute;top:4px;right:8px}.kz2Bea_gameplayClose:hover{color:#fff}.kz2Bea_gameplayFloat{color:#3a2a00;white-space:nowrap;pointer-events:none;background:#ffd27eeb;border-radius:999px;padding:2px 10px;font-size:11px;animation:1.1s ease-out forwards kz2Bea_pet-gameplay-float;position:absolute;top:-8px;left:50%;transform:translate(-50%)}@keyframes kz2Bea_pet-gameplay-float{0%{opacity:0;transform:translate(-50%)translateY(6px)}20%{opacity:1}to{opacity:0;transform:translate(-50%)translateY(-18px)}}@media (prefers-reduced-motion:reduce){.kz2Bea_gameplayFloat{opacity:1;animation:none}.kz2Bea_gameplayBarFill{transition:none}}.kz2Bea_bubbleUsage{white-space:nowrap;backdrop-filter:blur(8px);letter-spacing:.02em;background:linear-gradient(160deg,#131c36eb,#070b1af2);border:1px solid #7e98ff73;flex-direction:column;align-items:stretch;gap:3px;min-width:128px;max-width:min(280px,100vw - 24px);padding:6px 12px;animation:.28s cubic-bezier(.22,1,.36,1) kz2Bea_pet-usage-in;display:flex;box-shadow:0 4px 12px #02061759,inset 0 1px #e2e8ff1a,0 0 12px #4d6bfe2e}.kz2Bea_bubbleUsageHead{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.kz2Bea_bubbleUsageTitle{text-overflow:ellipsis;opacity:.85;overflow:hidden}.kz2Bea_bubbleUsageValue{font-variant-numeric:tabular-nums;flex-shrink:0;font-weight:600}.kz2Bea_bubbleUsageNote{text-overflow:ellipsis;opacity:.55;font-size:10px;overflow:hidden}.kz2Bea_bubbleUsageMeter{background:#e2e8ff24;border-radius:999px;height:4px;display:block;overflow:hidden}.kz2Bea_bubbleUsageMeterFill{background:#6ee7b7;border-radius:999px;height:100%;transition:width .3s;display:block}.kz2Bea_bubbleUsageOk{border-color:#7e98ff73}.kz2Bea_bubbleUsageWarn{border-color:#fbbf248c}.kz2Bea_bubbleUsageWarn .kz2Bea_bubbleUsageMeterFill{background:#fbbf24}.kz2Bea_bubbleUsageLow{border-color:#f8717199}.kz2Bea_bubbleUsageLow .kz2Bea_bubbleUsageMeterFill{background:#f87171}@keyframes kz2Bea_pet-usage-in{0%{opacity:0;transform:translateY(6px)scale(.96)}to{opacity:1;transform:translateY(0)scale(1)}}@media (prefers-reduced-motion:reduce){.kz2Bea_bubbleUsage{animation:none}}";
175
193
  const tagId$2 = "@linxin666/dsh-pet/pet.module.css";
176
194
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
177
195
  const tag = document.createElement("style");
@@ -191,6 +209,16 @@ window.__ModuleLoader__.load({
191
209
  "bubblePet": "kz2Bea_bubblePet",
192
210
  "bubbleStack": "kz2Bea_bubbleStack",
193
211
  "bubbleStatus": "kz2Bea_bubbleStatus",
212
+ "bubbleUsage": "kz2Bea_bubbleUsage",
213
+ "bubbleUsageHead": "kz2Bea_bubbleUsageHead",
214
+ "bubbleUsageLow": "kz2Bea_bubbleUsageLow",
215
+ "bubbleUsageMeter": "kz2Bea_bubbleUsageMeter",
216
+ "bubbleUsageMeterFill": "kz2Bea_bubbleUsageMeterFill",
217
+ "bubbleUsageNote": "kz2Bea_bubbleUsageNote",
218
+ "bubbleUsageOk": "kz2Bea_bubbleUsageOk",
219
+ "bubbleUsageTitle": "kz2Bea_bubbleUsageTitle",
220
+ "bubbleUsageValue": "kz2Bea_bubbleUsageValue",
221
+ "bubbleUsageWarn": "kz2Bea_bubbleUsageWarn",
194
222
  "bubbleWhisper": "kz2Bea_bubbleWhisper",
195
223
  "float": "kz2Bea_float",
196
224
  "gameplayActions": "kz2Bea_gameplayActions",
@@ -217,6 +245,7 @@ window.__ModuleLoader__.load({
217
245
  "pet-bubble-pop": "kz2Bea_pet-bubble-pop",
218
246
  "pet-gameplay-float": "kz2Bea_pet-gameplay-float",
219
247
  "pet-panel-in": "kz2Bea_pet-panel-in",
248
+ "pet-usage-in": "kz2Bea_pet-usage-in",
220
249
  "pet-whisper-in": "kz2Bea_pet-whisper-in",
221
250
  "rankRow": "kz2Bea_rankRow",
222
251
  "renameRow": "kz2Bea_renameRow",
@@ -319,6 +348,54 @@ window.__ModuleLoader__.load({
319
348
  });
320
349
  }
321
350
  /**
351
+ * The announcement bubble (dsh-usage linkage): a dedicated, specially
352
+ * designed surface for sibling-plugin facts — a balance or today-spend pill,
353
+ * or a plan-quota card with a tone-tinted accent, a mini meter for percent
354
+ * windows, and the reset instant. It rides the top of the session bubble
355
+ * stack (column-reverse puts the DOM-last child farthest from the sprite)
356
+ * and persists for its TTL instead of the short feedback pop.
357
+ */
358
+ function UsageAnnouncementBubble(props) {
359
+ const { announcement } = props;
360
+ const tone = announcement.tone === "low" ? pet_module_css_default.bubbleUsageLow : announcement.tone === "warn" ? pet_module_css_default.bubbleUsageWarn : pet_module_css_default.bubbleUsageOk;
361
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
362
+ className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleUsage, tone),
363
+ role: "status",
364
+ "aria-live": "polite",
365
+ "data-dsh-pet-announcement": announcement.source,
366
+ children: [
367
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
368
+ className: pet_module_css_default.bubbleUsageHead,
369
+ children: [
370
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
371
+ className: pet_module_css_default.bubbleUsageTitle,
372
+ children: announcement.title
373
+ }),
374
+ (announcement.kind === "balance" || announcement.kind === "cost") && announcement.amount !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
375
+ className: pet_module_css_default.bubbleUsageValue,
376
+ children: announcement.amount
377
+ }),
378
+ announcement.kind === "plan" && announcement.percent !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
379
+ className: pet_module_css_default.bubbleUsageValue,
380
+ children: Math.round(announcement.percent) + "%"
381
+ })
382
+ ]
383
+ }),
384
+ announcement.kind === "plan" && announcement.percent !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
385
+ className: pet_module_css_default.bubbleUsageMeter,
386
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
387
+ className: pet_module_css_default.bubbleUsageMeterFill,
388
+ style: { width: Math.min(100, Math.max(0, announcement.percent)) + "%" }
389
+ })
390
+ }),
391
+ announcement.note !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
392
+ className: pet_module_css_default.bubbleUsageNote,
393
+ children: announcement.note
394
+ })
395
+ ]
396
+ });
397
+ }
398
+ /**
322
399
  * The floating pet. The spritesheet frame advances on requestAnimationFrame
323
400
  * with per-frame durations from the definition's tracks; the atlas image is
324
401
  * loaded once and the background position is written straight to the sprite
@@ -517,7 +594,9 @@ window.__ModuleLoader__.load({
517
594
  const stackOpen = stackPeek || stackPinned;
518
595
  const visibleSessions = !stackOpen && sessionBubbles.length > 1 ? sessionBubbles.slice(0, 1) : sessionBubbles;
519
596
  const statusBubble = feedback === null && sessionBubbles.length === 0 ? snapshot?.bubble : void 0;
520
- const bubblePresent = feedback !== null || sessionBubbles.length > 0 || statusBubble !== void 0;
597
+ const announcement = snapshot?.announcement;
598
+ const usageAnnouncement = feedback === null && announcement !== void 0 && announcementFresh(announcement, Date.now()) ? announcement : void 0;
599
+ const bubblePresent = feedback !== null || sessionBubbles.length > 0 || statusBubble !== void 0 || usageAnnouncement !== void 0;
521
600
  const displayName = snapshot?.name ?? definition.displayName;
522
601
  const decoration = snapshot?.decoration;
523
602
  (0, react.useEffect)(() => {
@@ -614,50 +693,54 @@ window.__ModuleLoader__.load({
614
693
  className: clsx(pet_module_css_default.bubble, feedback.kind === "feed" ? pet_module_css_default.bubbleFeed : pet_module_css_default.bubblePet),
615
694
  children: feedback.text
616
695
  }, feedback.at),
617
- feedback === null && (sessionBubbles.length > 0 || statusBubble !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
696
+ feedback === null && (sessionBubbles.length > 0 || statusBubble !== void 0 || usageAnnouncement !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
618
697
  ref: bubbleRef,
619
698
  className: pet_module_css_default.bubbleStack,
620
699
  onPointerEnter: () => setStackPeek(true),
621
700
  onPointerLeave: () => setStackPeek(false),
622
- children: [visibleSessions.map((session, index) => {
623
- const speaksWhisper = session.whisper !== void 0;
624
- const bubble = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
625
- type: "button",
626
- className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus, pet_module_css_default.bubbleClickable, speaksWhisper && pet_module_css_default.bubbleWhisper),
627
- title: props.t("pet.openSessionHint"),
628
- onClick: () => {
629
- props.onOpenSession(session.sessionId);
630
- },
631
- children: [index === 0 && !speaksWhisper && decoration !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusOrnament, {
632
- decoration,
633
- phase
634
- }), session.whisper ?? session.bubble]
635
- }, speaksWhisper ? "whisper:" + session.whisper : session.sessionId);
636
- if (index !== 0 || sessionBubbles.length <= 1) return bubble;
637
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
638
- className: pet_module_css_default.bubbleAnchor,
639
- children: [bubble, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
701
+ children: [
702
+ visibleSessions.map((session, index) => {
703
+ const speaksWhisper = session.whisper !== void 0;
704
+ const bubble = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
640
705
  type: "button",
641
- className: pet_module_css_default.bubbleMore,
642
- title: stackOpen ? props.t("pet.collapseSessions") : props.t("pet.moreSessions", { n: sessionBubbles.length - 1 }),
643
- "aria-label": stackOpen ? props.t("pet.collapseSessions") : props.t("pet.moreSessions", { n: sessionBubbles.length - 1 }),
644
- "aria-expanded": stackOpen,
645
- onClick: (e) => {
646
- e.stopPropagation();
647
- setStackPinned((open) => !open);
706
+ className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus, pet_module_css_default.bubbleClickable, speaksWhisper && pet_module_css_default.bubbleWhisper),
707
+ title: props.t("pet.openSessionHint"),
708
+ onClick: () => {
709
+ props.onOpenSession(session.sessionId);
648
710
  },
649
- children: stackOpen ? "×" : "+" + String(sessionBubbles.length - 1)
650
- })]
651
- }, "primary");
652
- }), sessionBubbles.length === 0 && statusBubble !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
653
- className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus),
654
- role: "status",
655
- "aria-live": "polite",
656
- children: [decoration !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusOrnament, {
657
- decoration,
658
- phase
659
- }), statusBubble]
660
- }, "status")]
711
+ children: [index === 0 && !speaksWhisper && decoration !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusOrnament, {
712
+ decoration,
713
+ phase
714
+ }), session.whisper ?? session.bubble]
715
+ }, speaksWhisper ? "whisper:" + session.whisper : session.sessionId);
716
+ if (index !== 0 || sessionBubbles.length <= 1) return bubble;
717
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
718
+ className: pet_module_css_default.bubbleAnchor,
719
+ children: [bubble, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
720
+ type: "button",
721
+ className: pet_module_css_default.bubbleMore,
722
+ title: stackOpen ? props.t("pet.collapseSessions") : props.t("pet.moreSessions", { n: sessionBubbles.length - 1 }),
723
+ "aria-label": stackOpen ? props.t("pet.collapseSessions") : props.t("pet.moreSessions", { n: sessionBubbles.length - 1 }),
724
+ "aria-expanded": stackOpen,
725
+ onClick: (e) => {
726
+ e.stopPropagation();
727
+ setStackPinned((open) => !open);
728
+ },
729
+ children: stackOpen ? "×" : "+" + String(sessionBubbles.length - 1)
730
+ })]
731
+ }, "primary");
732
+ }),
733
+ sessionBubbles.length === 0 && statusBubble !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
734
+ className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus),
735
+ role: "status",
736
+ "aria-live": "polite",
737
+ children: [decoration !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusOrnament, {
738
+ decoration,
739
+ phase
740
+ }), statusBubble]
741
+ }, "status"),
742
+ usageAnnouncement !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageAnnouncementBubble, { announcement: usageAnnouncement })
743
+ ]
661
744
  }),
662
745
  hovered && dragRef.current === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
663
746
  ref: panelRef,
@@ -766,7 +849,7 @@ window.__ModuleLoader__.load({
766
849
  ] })
767
850
  })
768
851
  ]
769
- }), document.body);
852
+ }), props.portalTarget ?? document.body);
770
853
  }
771
854
  //#endregion
772
855
  //#region src/client/drag-stream.ts
@@ -1436,10 +1519,11 @@ window.__ModuleLoader__.load({
1436
1519
  * it must not ride a session-scoped slot — on the new-conversation screen no
1437
1520
  * session exists to scope a slot by, and the pet would vanish (issue #48).
1438
1521
  * The client half therefore mounts this entry straight onto 'document.body'
1439
- * (see index.ts): while visible it renders the floating PetSprite (a
1440
- * portal), while hidden it renders a fixed-position summon button. Which
1441
- * sprite renders is decided by the host snapshot's pet id resolved against
1442
- * the registry list — no per-pet component exists.
1522
+ * (see index.ts): while visible it renders the floating PetSprite (a portal
1523
+ * into the plugin root, so the root owns the whole surface), while hidden it
1524
+ * renders a fixed-position summon button. Which sprite renders is decided by
1525
+ * the host snapshot's pet id resolved against the registry list — no per-pet
1526
+ * component exists.
1443
1527
  * @module @linxin666/dsh-pet/client/PetDockEntry
1444
1528
  */
1445
1529
  const DEFAULT_DISPLAY = {
@@ -1497,6 +1581,7 @@ window.__ModuleLoader__.load({
1497
1581
  onRename: props.rename,
1498
1582
  onOpenSession: props.openSession,
1499
1583
  onFeedbackDone: props.feedbackDone,
1584
+ portalTarget: props.portalTarget,
1500
1585
  dragDisabled: snapshot.gameplay?.mode === "work",
1501
1586
  ...gameplay === void 0 || aux === null ? {} : {
1502
1587
  onGameplayTap: (fx, fy) => aux.bus.tap?.(fx, fy),
@@ -2694,7 +2779,7 @@ window.__ModuleLoader__.load({
2694
2779
  }
2695
2780
  /** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
2696
2781
  bind(project) {
2697
- const store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(project());
2782
+ const store = createSnapshotStore(project());
2698
2783
  this.listeners.add(() => {
2699
2784
  store.set(project());
2700
2785
  });
@@ -2709,7 +2794,7 @@ window.__ModuleLoader__.load({
2709
2794
  exposed: snapshot.status === "ready",
2710
2795
  writable: snapshot.writable,
2711
2796
  dirty: plan.length > 0,
2712
- invalid: plan.some((item) => item.run === void 0),
2797
+ invalid: plan.some((item) => item.judge === void 0),
2713
2798
  saving: this.saving,
2714
2799
  failed: this.failed,
2715
2800
  ...this.failedReason === void 0 ? {} : { failedReason: this.failedReason }
@@ -2759,45 +2844,52 @@ window.__ModuleLoader__.load({
2759
2844
  };
2760
2845
  }
2761
2846
  /**
2762
- * Write every staged edit, then re-seed from what the Host accepted.
2847
+ * Write every staged edit in one atomic scope mutation, then re-seed from
2848
+ * what the Host accepted.
2763
2849
  *
2764
- * When the scope carries the optional batch surface (the dsh-web
2765
- * bridge scope), every planned write rides one mutation so cross-field
2766
- * validate hooks (baseURL+model) judge the batch as a unit instead of
2767
- * deadlocking on per-field writes. Otherwise the per-field loop runs.
2768
- * A field lands only when the Host reports it held the staged value; a
2769
- * landed field's draft is dropped, a failed one stays staged for the user.
2770
- * @returns settlement after every write and the read-back.
2850
+ * The whole batch rides one mutate, so cross-field validate hooks
2851
+ * (baseURL+model) judge it as a unit: the Host either applies every write
2852
+ * or refuses the batch. The 0.1.2 scope contract never rejects a refused
2853
+ * mutation the scope recovers with a fresh Host view and resolves — so
2854
+ * resolution alone proves nothing: the outcome is judged by reading the
2855
+ * settled snapshot back, one planned write at a time, and one missed write
2856
+ * fails the whole save. A scope that still rejects on refusal (the dsh-web
2857
+ * bridge scope) reports through the same failure path with its rejection
2858
+ * message. A save that did not land keeps its drafts, so the user can
2859
+ * correct them instead of retyping.
2860
+ * @returns settlement after the mutation and the read-back.
2771
2861
  */
2772
2862
  async save() {
2773
2863
  const plan = this.plan();
2774
- const valid = plan.filter((item) => item.run !== void 0);
2864
+ const valid = plan.filter((item) => item.judge !== void 0);
2775
2865
  if (plan.length === 0 || this.saving || valid.length !== plan.length) return;
2776
- const plannedWrites = valid.map((item) => item.op);
2777
2866
  const pending = /* @__PURE__ */ new Map();
2778
2867
  for (const item of plan) pending.set(item.field, this.staged.get(item.field));
2779
2868
  this.saving = true;
2780
2869
  this.failed = false;
2781
2870
  this.failedReason = void 0;
2782
2871
  this.publish();
2783
- const landed = /* @__PURE__ */ new Set();
2784
- const batch = this.batchedScope();
2785
- if (batch !== void 0) {
2786
- const result = await batch.mutate(plannedWrites);
2787
- if (result.ok) {
2788
- for (const field of result.fields) if (field.landed) landed.add(field.field);
2789
- } else this.failedReason = result.message;
2790
- } else for (const item of valid) if (await item.run()) landed.add(item.field);
2791
- for (const [field, before] of pending) if (landed.has(field) && this.staged.get(field) === before) this.staged.delete(field);
2872
+ const ops = valid.map((item) => item.op.op === "set" ? {
2873
+ op: "set",
2874
+ path: [item.field],
2875
+ value: item.op.value
2876
+ } : {
2877
+ op: "unset",
2878
+ path: [item.field]
2879
+ });
2880
+ let failedReason;
2881
+ try {
2882
+ await this.scope.mutate(ops);
2883
+ } catch (error) {
2884
+ failedReason = error instanceof Error ? error.message : String(error);
2885
+ }
2886
+ const landed = failedReason === void 0 && valid.every((item) => item.judge());
2887
+ for (const [field, before] of pending) if (landed && this.staged.get(field) === before) this.staged.delete(field);
2792
2888
  this.saving = false;
2793
- this.failed = landed.size !== pending.size;
2889
+ this.failed = !landed;
2890
+ this.failedReason = failedReason;
2794
2891
  this.publish();
2795
2892
  }
2796
- /** The scope's batch surface when it supports one; undefined conservatively otherwise. */
2797
- batchedScope() {
2798
- const candidate = this.scope;
2799
- return typeof candidate?.mutate === "function" ? candidate : void 0;
2800
- }
2801
2893
  /**
2802
2894
  * Every staged edit a save would write. An entry whose draft is not a value
2803
2895
  * its field accepts carries no write: the form is still dirty, and the save
@@ -2816,7 +2908,7 @@ window.__ModuleLoader__.load({
2816
2908
  field,
2817
2909
  op: "unset"
2818
2910
  },
2819
- run: () => this.clear(field)
2911
+ judge: () => this.landedUnset(field)
2820
2912
  });
2821
2913
  continue;
2822
2914
  }
@@ -2828,7 +2920,7 @@ window.__ModuleLoader__.load({
2828
2920
  field,
2829
2921
  op: "unset"
2830
2922
  },
2831
- run: void 0
2923
+ judge: void 0
2832
2924
  });
2833
2925
  else if (write.kind === "clear") plan.push({
2834
2926
  field,
@@ -2836,7 +2928,7 @@ window.__ModuleLoader__.load({
2836
2928
  field,
2837
2929
  op: "unset"
2838
2930
  },
2839
- run: () => this.clear(field)
2931
+ judge: () => this.landedUnset(field)
2840
2932
  });
2841
2933
  else plan.push({
2842
2934
  field,
@@ -2845,20 +2937,26 @@ window.__ModuleLoader__.load({
2845
2937
  op: "set",
2846
2938
  value: write.value
2847
2939
  },
2848
- run: () => this.store(field, write.value)
2940
+ judge: () => this.landedSet(field, write.value)
2849
2941
  });
2850
2942
  }
2851
2943
  return plan;
2852
2944
  }
2853
- async clear(field) {
2854
- await this.scope.unset(field);
2855
- return !this.stored(field);
2856
- }
2857
- async store(field, value) {
2858
- await this.scope.set(field, value);
2945
+ /**
2946
+ * Read-back judgment for a planned set: the user layer must hold the
2947
+ * intended value once the mutation has settled.
2948
+ */
2949
+ landedSet(field, value) {
2859
2950
  if (this.specOf(field).secret) return true;
2860
2951
  return this.userLayer()?.[field] === value;
2861
2952
  }
2953
+ /**
2954
+ * Read-back judgment for a planned unset: the field must be gone from the
2955
+ * user layer once the mutation has settled.
2956
+ */
2957
+ landedUnset(field) {
2958
+ return !this.stored(field);
2959
+ }
2862
2960
  stage(field, edit) {
2863
2961
  this.staged.set(field, edit);
2864
2962
  this.failed = false;
@@ -3250,7 +3348,7 @@ window.__ModuleLoader__.load({
3250
3348
  "settings.off": "关",
3251
3349
  "settings.overridden": "已覆盖",
3252
3350
  "settings.reset": "恢复默认",
3253
- "settings.notExposed": "当前 DSH 版本未向设置页暴露本插件的配置命名空间,表单不可用。可编辑 ~/.dsh/settings.yaml 直接配置,或为 dsh-host-apiproxy 的 WEB_SETTINGS_NAMESPACES 白名单补充本命名空间后重启。",
3351
+ "settings.notExposed": "当前 DSH 版本未向设置页暴露本插件的配置命名空间,表单不可用。可编辑 $DSH_HOME/settings.yaml 直接配置,或确认提供该命名空间的插件已挂载其设置域并重启。",
3254
3352
  "settings.readOnly": "当前部署的设置只读。",
3255
3353
  "settings.expand": "展开设置",
3256
3354
  "settings.collapse": "收起设置",
@@ -3329,7 +3427,7 @@ window.__ModuleLoader__.load({
3329
3427
  "settings.off": "Off",
3330
3428
  "settings.overridden": "Overridden",
3331
3429
  "settings.reset": "Reset to default",
3332
- "settings.notExposed": "This DSH version does not expose this plugin's settings namespace to the configuration page, so the form is unavailable. Edit ~/.dsh/settings.yaml directly, or add the namespace to dsh-host-apiproxy's WEB_SETTINGS_NAMESPACES allowlist and restart.",
3430
+ "settings.notExposed": "This DSH version does not expose this plugin's settings namespace to the configuration page, so the form is unavailable. Edit $DSH_HOME/settings.yaml directly, or confirm that the plugin owning the namespace is mounted with its settings domain and restart.",
3333
3431
  "settings.readOnly": "This deployment stores settings read-only.",
3334
3432
  "settings.expand": "Show settings",
3335
3433
  "settings.collapse": "Hide settings",
@@ -3370,7 +3468,7 @@ window.__ModuleLoader__.load({
3370
3468
  /** The building package's version, when the bundle carries it. */
3371
3469
  function bakedVersion() {
3372
3470
  try {
3373
- return "0.3.6";
3471
+ return "0.3.10";
3374
3472
  } catch {
3375
3473
  return;
3376
3474
  }
@@ -3652,7 +3750,8 @@ window.__ModuleLoader__.load({
3652
3750
  const petRoot = (0, react_dom_client.createRoot)(container);
3653
3751
  petRoot.render((0, react.createElement)(PetDockEntry, {
3654
3752
  ...injected(),
3655
- t
3753
+ t,
3754
+ portalTarget: container
3656
3755
  }));
3657
3756
  let uiGone = false;
3658
3757
  disposeUi = () => {