@harness-mix/cli 0.2.0 → 0.2.2

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 (84) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/NOTICE +14 -0
  3. package/README.md +4 -24
  4. package/docs/multi-agent-collaboration.md +1 -1
  5. package/licenses/anthropic-codex-theme-MIT.txt +21 -0
  6. package/licenses/codex-dream-skin-MIT.txt +21 -0
  7. package/output/native-build/renderer-extension.js +703 -58
  8. package/package.json +192 -189
  9. package/scripts/collaboration-test.cjs +262 -200
  10. package/scripts/collaboration-ui-smoke.cjs +85 -77
  11. package/scripts/core-concurrency-test.cjs +23 -1
  12. package/scripts/e2e-pi-cancel.cjs +99 -0
  13. package/scripts/native-file-change-test.cjs +42 -2
  14. package/scripts/native-sidebar-test.cjs +8 -0
  15. package/scripts/native-vendor-adapters-test.cjs +67 -1
  16. package/scripts/pets-test.cjs +63 -0
  17. package/scripts/pi-cancel-race-test.cjs +102 -0
  18. package/scripts/stuck-turn-test.cjs +73 -0
  19. package/scripts/workspace-test.cjs +9 -0
  20. package/src/assets/skins/dream-skin/NOTICE.md +13 -0
  21. package/src/assets/skins/dream-skin/themes/gothic-void-crusade/hero.webp +0 -0
  22. package/src/assets/skins/dream-skin/themes/gothic-void-crusade/theme.json +34 -0
  23. package/src/main/adapters/claude.js +9 -6
  24. package/src/main/adapters/pi-family.js +41 -8
  25. package/src/main/harness-adapter/event-normalizer.js +270 -258
  26. package/src/main/host/collaboration-mcp.cjs +8 -1
  27. package/src/main/host/collaboration-tools.js +1 -1
  28. package/src/main/host/collaboration.js +81 -11
  29. package/src/main/host/runtime.js +1462 -1341
  30. package/src/main/native/pets.js +79 -2
  31. package/src/main/native/protocol.js +1164 -1145
  32. package/src/main/native/thread-list.js +6 -1
  33. package/src/main/workspace/core-review.js +75 -0
  34. package/src/main/workspace/diff.js +5 -1
  35. package/src/main/workspace/file-changes.js +9 -4
  36. package/src/main/workspace/review-controller.js +115 -102
  37. package/src/main/workspace/review.js +90 -88
  38. package/src/native-ui/renderer-extension/dist/contract-audit.js +16637 -0
  39. package/src/native-ui/renderer-extension/dist/index.js +34853 -0
  40. package/src/native-ui/renderer-extension/dist/production.js +34263 -0
  41. package/src/native-ui/renderer-extension/dist/renderer-binding-probe.js +34254 -0
  42. package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
  43. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +11 -0
  44. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
  45. package/src/native-ui/renderer-extension/dist/types/renderer-pet-switcher.d.ts +73 -0
  46. package/src/native-ui/renderer-extension/dist/types/renderer-pet-switcher.d.ts.map +1 -0
  47. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +1 -0
  48. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
  49. package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.d.ts.map +1 -1
  50. package/src/native-ui/renderer-extension/dist/types/settings/collaboration-page.d.ts +12 -0
  51. package/src/native-ui/renderer-extension/dist/types/settings/collaboration-page.d.ts.map +1 -0
  52. package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts +1 -1
  53. package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts.map +1 -1
  54. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
  55. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +5 -3
  56. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
  57. package/src/native-ui/renderer-extension/dist/types/settings/pet-market.d.ts +1 -1
  58. package/src/native-ui/renderer-extension/dist/types/settings/pet-market.d.ts.map +1 -1
  59. package/src/native-ui/renderer-extension/dist/types/settings/pets-client.d.ts +9 -0
  60. package/src/native-ui/renderer-extension/dist/types/settings/pets-client.d.ts.map +1 -1
  61. package/src/native-ui/renderer-extension/dist/types/settings/skin-runtime.d.ts +1 -1
  62. package/src/native-ui/renderer-extension/dist/types/settings/skin-runtime.d.ts.map +1 -1
  63. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  64. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +3181 -3171
  65. package/src/native-ui/renderer-extension/src/renderer-model-client.ts +14 -0
  66. package/src/native-ui/renderer-extension/src/renderer-pet-switcher.ts +308 -0
  67. package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +2 -0
  68. package/src/native-ui/renderer-extension/src/renderer-team-cards.ts +76 -21
  69. package/src/native-ui/renderer-extension/src/settings/collaboration-page.ts +139 -0
  70. package/src/native-ui/renderer-extension/src/settings/icons.ts +155 -153
  71. package/src/native-ui/renderer-extension/src/settings/localization.ts +785 -783
  72. package/src/native-ui/renderer-extension/src/settings/pages.ts +694 -681
  73. package/src/native-ui/renderer-extension/src/settings/pet-market.css +110 -1
  74. package/src/native-ui/renderer-extension/src/settings/pet-market.ts +276 -22
  75. package/src/native-ui/renderer-extension/src/settings/pets-client.ts +32 -0
  76. package/src/native-ui/renderer-extension/src/settings/shell.css +2294 -2255
  77. package/src/native-ui/renderer-extension/src/settings/skin-runtime.ts +51 -1
  78. package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +2 -0
  79. package/src/native-ui/renderer-extension/test/renderer-pet-switcher.test.ts +400 -0
  80. package/src/native-ui/renderer-extension/test/settings/localization.test.ts +80 -79
  81. package/src/native-ui/renderer-extension/test/settings/pet-market.test.ts +378 -0
  82. package/src/native-ui/renderer-extension/test/settings/pets-client.test.ts +83 -0
  83. package/src/native-ui/renderer-extension/test/settings/shell.test.ts +3 -0
  84. package/src/native-ui/renderer-extension/test/settings/skin-runtime.test.ts +15 -1
@@ -134,7 +134,8 @@
134
134
  }
135
135
 
136
136
  .pet-card[data-state="installing"] .pet-card__stage,
137
- .pet-card[data-state="uninstalling"] .pet-card__stage {
137
+ .pet-card[data-state="uninstalling"] .pet-card__stage,
138
+ .pet-card[data-state="selecting"] .pet-card__stage {
138
139
  opacity: 0.55;
139
140
  }
140
141
 
@@ -223,6 +224,12 @@
223
224
  border: 1px solid rgba(16, 163, 127, 0.3);
224
225
  }
225
226
 
227
+ .pet-card__badge--active {
228
+ background: rgba(245, 158, 11, 0.15);
229
+ color: #f59e0b;
230
+ border: 1px solid rgba(245, 158, 11, 0.35);
231
+ }
232
+
226
233
  .pet-card__badge--failed {
227
234
  background: rgba(239, 68, 68, 0.15);
228
235
  color: #f87171;
@@ -341,3 +348,105 @@
341
348
  color: var(--text-tertiary, rgba(255, 255, 255, 0.45));
342
349
  font-size: 13px;
343
350
  }
351
+
352
+ /* 当前桌宠面板 */
353
+ .pet-current {
354
+ display: flex;
355
+ align-items: center;
356
+ flex-wrap: wrap;
357
+ gap: 14px;
358
+ padding: 12px 14px;
359
+ margin-bottom: 18px;
360
+ border-radius: 10px;
361
+ background: var(--bg-secondary, rgba(255, 255, 255, 0.04));
362
+ border: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.08));
363
+ }
364
+
365
+ .pet-current[data-active="true"] {
366
+ border-color: rgba(245, 158, 11, 0.35);
367
+ }
368
+
369
+ .pet-current__label {
370
+ font-size: 11px;
371
+ font-weight: 600;
372
+ text-transform: uppercase;
373
+ letter-spacing: 0.06em;
374
+ color: var(--text-tertiary, rgba(255, 255, 255, 0.45));
375
+ flex-shrink: 0;
376
+ }
377
+
378
+ .pet-current__empty {
379
+ display: flex;
380
+ align-items: center;
381
+ gap: 10px;
382
+ min-height: 64px;
383
+ color: var(--text-tertiary, rgba(255, 255, 255, 0.4));
384
+ }
385
+
386
+ .pet-current__hint {
387
+ font-size: 12px;
388
+ line-height: 1.45;
389
+ color: var(--text-secondary, rgba(255, 255, 255, 0.6));
390
+ }
391
+
392
+ .pet-current__stage {
393
+ display: flex;
394
+ align-items: center;
395
+ justify-content: center;
396
+ width: 64px;
397
+ height: 64px;
398
+ flex-shrink: 0;
399
+ border-radius: 8px;
400
+ background: radial-gradient(circle at center, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.01) 70%);
401
+ border: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.06));
402
+ overflow: hidden;
403
+ user-select: none;
404
+ }
405
+
406
+ .pet-current__sprite {
407
+ width: 48px;
408
+ height: 52px;
409
+ background-repeat: no-repeat;
410
+ image-rendering: pixelated;
411
+ image-rendering: -webkit-optimize-contrast;
412
+ }
413
+
414
+ .pet-current__info {
415
+ display: flex;
416
+ flex-direction: column;
417
+ gap: 2px;
418
+ min-width: 0;
419
+ flex: 1;
420
+ }
421
+
422
+ .pet-current__info .pet-current__label {
423
+ flex-shrink: 1;
424
+ }
425
+
426
+ .pet-current__name {
427
+ font-size: 14px;
428
+ font-weight: 600;
429
+ color: var(--text-primary, #ffffff);
430
+ overflow: hidden;
431
+ text-overflow: ellipsis;
432
+ white-space: nowrap;
433
+ }
434
+
435
+ .pet-current__note {
436
+ font-size: 11.5px;
437
+ line-height: 1.4;
438
+ color: var(--text-tertiary, rgba(255, 255, 255, 0.45));
439
+ }
440
+
441
+ .pet-current__error {
442
+ flex-basis: 100%;
443
+ margin: 4px 0 0;
444
+ padding: 6px 8px;
445
+ border-radius: 6px;
446
+ background: rgba(239, 68, 68, 0.08);
447
+ border: 1px solid rgba(239, 68, 68, 0.25);
448
+ color: #f87171;
449
+ font-size: 11.5px;
450
+ line-height: 1.4;
451
+ word-break: break-word;
452
+ }
@@ -4,7 +4,13 @@ import type {
4
4
  RendererSettingsPageMountContext,
5
5
  } from "./core.js";
6
6
  import { createRendererSettingsIcon } from "./icons.js";
7
- import type { RendererPetsClient, RendererPetItem } from "./pets-client.js";
7
+ import {
8
+ normalizeRendererPetSelection,
9
+ type RendererPetItem,
10
+ type RendererPetSelection,
11
+ type RendererPetsClient,
12
+ } from "./pets-client.js";
13
+ import { switchOfficialCodexPet } from "../renderer-pet-switcher.js";
8
14
 
9
15
  const COPY = {
10
16
  en: {
@@ -31,6 +37,17 @@ const COPY = {
31
37
  empty: "No pets match the current filter.",
32
38
  officialBadge: "Official",
33
39
  communityBadge: "Community",
40
+ nowShowing: "Now showing",
41
+ nowShowingEmpty:
42
+ "No pet selected yet. Hit Use on an installed pet below and Harness Mix switches Codex's official pet for you.",
43
+ panelNote:
44
+ "Mirrors Codex's official pet. Show or hide the mascot with Codex's own Show pet command.",
45
+ usePet: "Use",
46
+ usingPet: "Switching...",
47
+ activeBadge: "Active",
48
+ hidePet: "Clear",
49
+ hidingPet: "Clearing...",
50
+ selectFailed: "Switch failed",
34
51
  },
35
52
  "zh-CN": {
36
53
  title: "桌宠市场",
@@ -56,15 +73,42 @@ const COPY = {
56
73
  empty: "没有找到符合条件的桌宠。",
57
74
  officialBadge: "官方预载",
58
75
  communityBadge: "社区精选",
76
+ nowShowing: "当前桌宠",
77
+ nowShowingEmpty: "尚未选择桌宠。在下方已安装的桌宠上点击「使用」,Harness Mix 会为你切换 Codex 官方桌宠。",
78
+ panelNote: "与 Codex 官方桌宠一致。官方吉祥物的显隐由 Codex 自己的「显示宠物」命令控制。",
79
+ usePet: "使用",
80
+ usingPet: "切换中...",
81
+ activeBadge: "使用中",
82
+ hidePet: "清除",
83
+ hidingPet: "清除中...",
84
+ selectFailed: "切换失败",
59
85
  },
60
86
  } as const;
61
87
 
62
88
  type PetTab = "all" | "official" | "community" | "installed";
63
- type PetOperation = "install" | "uninstall";
89
+ type PetOperation = "install" | "uninstall" | "select";
64
90
  type PetOperationState =
65
- | { readonly status: "installing" | "uninstalling" }
91
+ | { readonly status: "installing" | "uninstalling" | "selecting" }
66
92
  | { readonly status: "failed"; readonly op: PetOperation; readonly error: string };
67
93
 
94
+ function petOperationBusyLabel(
95
+ status: "installing" | "uninstalling" | "selecting",
96
+ copy: (typeof COPY)[keyof typeof COPY],
97
+ ): string {
98
+ if (status === "installing") return copy.installing;
99
+ if (status === "uninstalling") return copy.uninstalling;
100
+ return copy.usingPet;
101
+ }
102
+
103
+ function petOperationFailedLabel(
104
+ op: PetOperation,
105
+ copy: (typeof COPY)[keyof typeof COPY],
106
+ ): string {
107
+ if (op === "install") return copy.installFailed;
108
+ if (op === "uninstall") return copy.uninstallFailed;
109
+ return copy.selectFailed;
110
+ }
111
+
68
112
  const SPRITE_COLUMNS = 8;
69
113
  const SPRITE_FRAMES = 6;
70
114
  const FRAME_DURATION_MS = 220;
@@ -116,7 +160,7 @@ function setupSpriteAnimation(
116
160
  stopLoop();
117
161
  };
118
162
 
119
- const parentCard = element.closest(".pet-card");
163
+ const parentCard = element.closest(".pet-card, .pet-current");
120
164
  if (parentCard) {
121
165
  parentCard.addEventListener("mouseenter", onMouseEnter);
122
166
  parentCard.addEventListener("mouseleave", onMouseLeave);
@@ -181,6 +225,11 @@ export function createPetSettingsPage(
181
225
  safetyBody.append(safetyTitle, safetyDetail);
182
226
  safety.append(safetyBody);
183
227
 
228
+ // 当前桌宠面板(Harness Mix 本地选择状态驱动的展示面,置顶显示)
229
+ const currentPanel = document.createElement("section");
230
+ currentPanel.className = "pet-current";
231
+ currentPanel.dataset.active = "false";
232
+
184
233
  // Toolbar: tabs + search
185
234
  const toolbar = document.createElement("div");
186
235
  toolbar.className = "pet-market__toolbar";
@@ -214,14 +263,21 @@ export function createPetSettingsPage(
214
263
  const grid = document.createElement("div");
215
264
  grid.className = "pet-market-grid";
216
265
 
217
- context.content.append(heading, intro, safety, toolbar, grid);
266
+ context.content.append(heading, intro, safety, currentPanel, toolbar, grid);
218
267
 
219
268
  let allPets: RendererPetItem[] = [];
220
269
  const imageCache = new Map<string, string>(); // id -> dataUrl / url
221
- // 每个 pet 的操作状态机:installing / uninstalling / failed(含错误与失败操作类型)
270
+ // 每个 pet 的操作状态机:installing / uninstalling / selecting / failed(含错误与失败操作类型)
222
271
  const petStates = new Map<string, PetOperationState>();
223
272
  // 渲染世代:重渲染后让在途的 preview 回调失效,避免给已分离的 DOM 节点挂动画
224
273
  let renderEpoch = 0;
274
+ // 当前桌宠面板状态:选择结果 + 隐藏操作进行中 + 面板级错误(选择失败显示在卡片内)
275
+ let currentSelection: RendererPetSelection = { id: null };
276
+ let selectionReady = false;
277
+ let hidePending = false;
278
+ let panelError: string | null = null;
279
+ let panelEpoch = 0;
280
+ const panelDisposers: Array<() => void> = [];
225
281
 
226
282
  const runPetOperation = (pet: RendererPetItem, op: PetOperation) => {
227
283
  const client = getClient();
@@ -243,7 +299,13 @@ export function createPetSettingsPage(
243
299
  );
244
300
  renderGrid();
245
301
  };
246
- const onSuccess = () => settle(op === "install", null);
302
+ const onSuccess = () => {
303
+ settle(op === "install", null);
304
+ // 卸载当前选中的桌宠:Host 端已自动清除持久化选择,这里同步面板与悬浮层
305
+ if (op === "uninstall" && currentSelection.id === pet.id) {
306
+ applySelection({ id: null });
307
+ }
308
+ };
247
309
  const onFailure = (err: unknown) =>
248
310
  settle(pet.installed, err instanceof Error ? err.message : String(err));
249
311
  if (op === "install") {
@@ -324,8 +386,7 @@ export function createPetSettingsPage(
324
386
  if (opState?.status === "failed") {
325
387
  const b = document.createElement("span");
326
388
  b.className = "pet-card__badge pet-card__badge--failed";
327
- b.textContent =
328
- opState.op === "install" ? copy.installFailed : copy.uninstallFailed;
389
+ b.textContent = petOperationFailedLabel(opState.op, copy);
329
390
  badges.append(b);
330
391
  } else if (pet.installed) {
331
392
  const b = document.createElement("span");
@@ -344,6 +405,13 @@ export function createPetSettingsPage(
344
405
  badges.append(b);
345
406
  }
346
407
 
408
+ if (pet.installed && currentSelection.id === pet.id && opState?.status !== "failed") {
409
+ const activeBadge = document.createElement("span");
410
+ activeBadge.className = "pet-card__badge pet-card__badge--active";
411
+ activeBadge.textContent = copy.activeBadge;
412
+ badges.append(activeBadge);
413
+ }
414
+
347
415
  cardHeader.append(title, badges);
348
416
 
349
417
  const desc = document.createElement("p");
@@ -361,30 +429,41 @@ export function createPetSettingsPage(
361
429
  const actions = document.createElement("div");
362
430
  actions.className = "pet-card__actions";
363
431
 
364
- if (opState?.status === "installing" || opState?.status === "uninstalling") {
365
- // 操作进行中:按钮禁用,防止重复点击(runPetOperation 内还有 petStates 防重入)
432
+ if (opState && opState.status !== "failed") {
433
+ // 操作进行中:按钮禁用,防止重复点击(runPetOperation/runSelectOperation 内还有 petStates 防重入)
366
434
  const busyBtn = document.createElement("button");
367
435
  busyBtn.type = "button";
368
436
  busyBtn.className =
369
- opState.status === "installing"
370
- ? "pet-btn pet-btn--primary"
371
- : "pet-btn pet-btn--danger";
437
+ opState.status === "uninstalling"
438
+ ? "pet-btn pet-btn--danger"
439
+ : "pet-btn pet-btn--primary";
372
440
  busyBtn.disabled = true;
373
- busyBtn.textContent =
374
- opState.status === "installing" ? copy.installing : copy.uninstalling;
441
+ busyBtn.textContent = petOperationBusyLabel(opState.status, copy);
375
442
  actions.append(busyBtn);
376
443
  } else if (opState?.status === "failed") {
377
444
  // 失败后错误显示在卡片内,重试按钮恢复对应操作
378
445
  const retryBtn = document.createElement("button");
379
446
  retryBtn.type = "button";
380
447
  retryBtn.className =
381
- opState.op === "install"
382
- ? "pet-btn pet-btn--primary"
383
- : "pet-btn pet-btn--danger";
448
+ opState.op === "uninstall"
449
+ ? "pet-btn pet-btn--danger"
450
+ : "pet-btn pet-btn--primary";
384
451
  retryBtn.textContent = copy.retry;
385
- retryBtn.addEventListener("click", () => runPetOperation(pet, opState.op));
452
+ retryBtn.addEventListener("click", () => {
453
+ if (opState.op === "select") runSelectOperation(pet);
454
+ else runPetOperation(pet, opState.op);
455
+ });
386
456
  actions.append(retryBtn);
387
457
  } else if (pet.installed) {
458
+ // 已安装:可切换为当前桌宠(已在使用中的不再显示 Use,用 Active 徽标表达)
459
+ if (currentSelection.id !== pet.id) {
460
+ const useBtn = document.createElement("button");
461
+ useBtn.type = "button";
462
+ useBtn.className = "pet-btn pet-btn--primary";
463
+ useBtn.textContent = copy.usePet;
464
+ useBtn.addEventListener("click", () => runSelectOperation(pet));
465
+ actions.append(useBtn);
466
+ }
388
467
  const uninstallBtn = document.createElement("button");
389
468
  uninstallBtn.type = "button";
390
469
  uninstallBtn.className = "pet-btn pet-btn--danger";
@@ -415,8 +494,7 @@ export function createPetSettingsPage(
415
494
  } else if (opState) {
416
495
  const statusLine = document.createElement("div");
417
496
  statusLine.className = "pet-card__status";
418
- statusLine.textContent =
419
- opState.status === "installing" ? copy.installing : copy.uninstalling;
497
+ statusLine.textContent = petOperationBusyLabel(opState.status, copy);
420
498
  body.append(cardHeader, desc, statusLine, footer);
421
499
  } else {
422
500
  body.append(cardHeader, desc, footer);
@@ -468,6 +546,162 @@ export function createPetSettingsPage(
468
546
  }
469
547
  };
470
548
 
549
+ // 当前桌宠面板渲染:空态提示 / 选中态(精灵图动画 + 名称 + 隐藏按钮),面板级错误行
550
+ const renderPanel = () => {
551
+ panelEpoch += 1;
552
+ const epoch = panelEpoch;
553
+ for (const d of panelDisposers) d();
554
+ panelDisposers.length = 0;
555
+ currentPanel.replaceChildren();
556
+ currentPanel.dataset.active = String(currentSelection.id !== null);
557
+
558
+ const label = document.createElement("span");
559
+ label.className = "pet-current__label";
560
+ label.textContent = copy.nowShowing;
561
+
562
+ if (currentSelection.id === null) {
563
+ const emptyWrap = document.createElement("div");
564
+ emptyWrap.className = "pet-current__empty";
565
+ emptyWrap.append(createRendererSettingsIcon("pets", 22));
566
+ const hint = document.createElement("span");
567
+ hint.className = "pet-current__hint";
568
+ hint.textContent = selectionReady ? copy.nowShowingEmpty : "";
569
+ emptyWrap.append(hint);
570
+ currentPanel.append(label, emptyWrap);
571
+ } else {
572
+ const selectionId = currentSelection.id;
573
+ const stage = document.createElement("div");
574
+ stage.className = "pet-current__stage";
575
+ const sprite = document.createElement("div");
576
+ sprite.className = "pet-current__sprite";
577
+ stage.append(sprite);
578
+
579
+ const info = document.createElement("div");
580
+ info.className = "pet-current__info";
581
+ const name = document.createElement("strong");
582
+ name.className = "pet-current__name";
583
+ name.textContent = currentSelection.displayName ?? selectionId;
584
+ const note = document.createElement("span");
585
+ note.className = "pet-current__note";
586
+ note.textContent = copy.panelNote;
587
+ info.append(label, name, note);
588
+
589
+ const hideBtn = document.createElement("button");
590
+ hideBtn.type = "button";
591
+ hideBtn.className = "pet-btn pet-btn--secondary";
592
+ hideBtn.disabled = hidePending;
593
+ hideBtn.textContent = hidePending ? copy.hidingPet : copy.hidePet;
594
+ hideBtn.addEventListener("click", () => runHideOperation());
595
+
596
+ currentPanel.append(stage, info, hideBtn);
597
+
598
+ const attachAnimation = (imageUrl: string) => {
599
+ panelDisposers.push(
600
+ setupSpriteAnimation(sprite, imageUrl, currentSelection.spriteVersionNumber),
601
+ );
602
+ };
603
+ const cachedImg = imageCache.get(selectionId);
604
+ if (cachedImg) {
605
+ attachAnimation(cachedImg);
606
+ } else {
607
+ const client = getClient();
608
+ if (client) {
609
+ // 与卡片一致的按需加载;面板重渲染/页面卸载后丢弃过期回调
610
+ void client.preview(selectionId).then(
611
+ (res) => {
612
+ if (epoch !== panelEpoch || !sprite.isConnected) return;
613
+ if (res && res.dataBase64) {
614
+ const dataUrl = `data:${res.mime};base64,${res.dataBase64}`;
615
+ imageCache.set(selectionId, dataUrl);
616
+ attachAnimation(dataUrl);
617
+ }
618
+ },
619
+ () => {
620
+ /* 预览加载失败仅影响动画,面板文字信息仍有效 */
621
+ },
622
+ );
623
+ }
624
+ }
625
+ }
626
+
627
+ if (panelError) {
628
+ const errorBox = document.createElement("div");
629
+ errorBox.className = "pet-current__error";
630
+ errorBox.textContent = panelError;
631
+ currentPanel.append(errorBox);
632
+ }
633
+ };
634
+
635
+ // 选择变更的统一入口:更新面板与卡片徽标
636
+ const applySelection = (selection: RendererPetSelection) => {
637
+ currentSelection = selection;
638
+ selectionReady = true;
639
+ panelError = null;
640
+ renderPanel();
641
+ renderGrid();
642
+ };
643
+
644
+ const runHideOperation = () => {
645
+ const client = getClient();
646
+ if (!client || hidePending || currentSelection.id === null) return;
647
+ hidePending = true;
648
+ panelError = null;
649
+ renderPanel();
650
+ void client.select(null).then(
651
+ () => {
652
+ hidePending = false;
653
+ applySelection({ id: null });
654
+ },
655
+ (err) => {
656
+ hidePending = false;
657
+ panelError = err instanceof Error ? err.message : String(err);
658
+ renderPanel();
659
+ },
660
+ );
661
+ };
662
+
663
+ const runSelectOperation = (pet: RendererPetItem) => {
664
+ const client = getClient();
665
+ const current = petStates.get(pet.id);
666
+ // 操作进行中禁止重复触发;failed 状态允许重试;当前选中的 pet 无需再选
667
+ if (!client || !pet.installed || (current && current.status !== "failed")) return;
668
+ if (currentSelection.id === pet.id) return;
669
+ petStates.set(pet.id, { status: "selecting" });
670
+ renderGrid();
671
+ // 先通过 DOM 自动化切换 Codex 官方桌宠(应用自己的点击链路,不碰账号 API),
672
+ // 确认成功后写入 Harness Mix 本地记录(驱动面板与徽标)
673
+ void switchOfficialCodexPet({ id: pet.id, displayName: pet.displayName })
674
+ .then(() => client.select(pet.id))
675
+ .then(
676
+ (result) => {
677
+ // 切换耗时数秒,页面可能已经卸载:中止后不再触碰已分离的 DOM
678
+ if (context.signal.aborted) return;
679
+ petStates.delete(pet.id);
680
+ const applied = normalizeRendererPetSelection(result);
681
+ applySelection(
682
+ applied.id !== null
683
+ ? applied
684
+ : {
685
+ id: pet.id,
686
+ displayName: pet.displayName,
687
+ ...(pet.spriteVersionNumber !== undefined
688
+ ? { spriteVersionNumber: pet.spriteVersionNumber }
689
+ : {}),
690
+ },
691
+ );
692
+ },
693
+ (err) => {
694
+ if (context.signal.aborted) return;
695
+ petStates.set(pet.id, {
696
+ status: "failed",
697
+ op: "select",
698
+ error: err instanceof Error ? err.message : String(err),
699
+ });
700
+ renderGrid();
701
+ },
702
+ );
703
+ };
704
+
471
705
  for (const t of tabs) {
472
706
  const btn = document.createElement("button");
473
707
  btn.type = "button";
@@ -492,6 +726,7 @@ export function createPetSettingsPage(
492
726
 
493
727
  // Fetch catalog
494
728
  const client = getClient();
729
+ renderPanel(); // 首次渲染(空态),随后拉取选择填充
495
730
  if (client) {
496
731
  void context.runLatest(
497
732
  () => client.catalog(),
@@ -506,10 +741,29 @@ export function createPetSettingsPage(
506
741
  },
507
742
  },
508
743
  );
744
+ // 拉取当前选择填充面板;若期间用户已完成一次切换(panelEpoch 变化),丢弃过期结果
745
+ const selectionFetchEpoch = panelEpoch;
746
+ void client.selection().then(
747
+ (result) => {
748
+ if (selectionFetchEpoch !== panelEpoch) return;
749
+ currentSelection = normalizeRendererPetSelection(result);
750
+ selectionReady = true;
751
+ renderPanel();
752
+ renderGrid(); // 卡片的 Active 徽标 / Use 按钮依赖 currentSelection,需随选择到位重渲染
753
+ },
754
+ () => {
755
+ if (selectionFetchEpoch !== panelEpoch) return;
756
+ selectionReady = true;
757
+ renderPanel();
758
+ },
759
+ );
509
760
  }
510
761
 
511
762
  return () => {
512
763
  renderEpoch += 1; // 使在途 preview 回调失效
764
+ panelEpoch += 1; // 面板动画与在途 selection/preview 回调一并失效
765
+ for (const d of panelDisposers) d();
766
+ panelDisposers.length = 0;
513
767
  for (const d of disposers) d();
514
768
  disposers.length = 0;
515
769
  // 释放图片缓存(blob: URL 需要显式 revoke;data: URL 仅释放引用)
@@ -44,6 +44,32 @@ export interface RendererPetInstallResult {
44
44
  readonly alreadyInstalled?: boolean | undefined;
45
45
  }
46
46
 
47
+ export interface RendererPetSelection {
48
+ readonly id: string | null;
49
+ readonly displayName?: string | undefined;
50
+ readonly spriteVersionNumber?: number | undefined;
51
+ }
52
+
53
+ /** Coerce an arbitrary RPC result into a safe selection shape. */
54
+ export function normalizeRendererPetSelection(input: unknown): RendererPetSelection {
55
+ if (!input || typeof input !== "object" || Array.isArray(input)) return { id: null };
56
+ const record = input as Record<string, unknown>;
57
+ if (typeof record.id !== "string" || !record.id) return { id: null };
58
+ const displayName =
59
+ typeof record.displayName === "string" && record.displayName ? record.displayName : undefined;
60
+ const spriteVersionNumber =
61
+ typeof record.spriteVersionNumber === "number" &&
62
+ Number.isInteger(record.spriteVersionNumber) &&
63
+ record.spriteVersionNumber >= 1
64
+ ? record.spriteVersionNumber
65
+ : undefined;
66
+ return {
67
+ id: record.id,
68
+ ...(displayName !== undefined ? { displayName } : {}),
69
+ ...(spriteVersionNumber !== undefined ? { spriteVersionNumber } : {}),
70
+ };
71
+ }
72
+
47
73
  export interface RendererPetsClient {
48
74
  catalog(): Promise<RendererPetCatalogResult>;
49
75
  community(query?: {
@@ -55,6 +81,8 @@ export interface RendererPetsClient {
55
81
  preview(id: string): Promise<RendererPetPreviewResult>;
56
82
  install(params: RendererPetInstallParams): Promise<RendererPetInstallResult>;
57
83
  uninstall(id: string): Promise<{ id: string; removed: boolean }>;
84
+ selection(): Promise<RendererPetSelection>;
85
+ select(id: string | null): Promise<RendererPetSelection>;
58
86
  }
59
87
 
60
88
  export function createRendererPetsClient(
@@ -70,5 +98,9 @@ export function createRendererPetsClient(
70
98
  (await send("harnessmix/pets/install", params)) as RendererPetInstallResult,
71
99
  uninstall: async (id: string) =>
72
100
  (await send("harnessmix/pets/uninstall", { id })) as { id: string; removed: boolean },
101
+ selection: async () =>
102
+ (await send("harnessmix/pets/selection", {})) as RendererPetSelection,
103
+ select: async (id: string | null) =>
104
+ (await send("harnessmix/pets/select", { id })) as RendererPetSelection,
73
105
  };
74
106
  }