@harness-mix/cli 0.1.12 → 0.2.1
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.
- package/CHANGELOG.md +15 -2
- package/NOTICE +14 -0
- package/README.md +4 -24
- package/docs/multi-agent-collaboration.md +1 -1
- package/licenses/anthropic-codex-theme-MIT.txt +21 -0
- package/licenses/codex-dream-skin-MIT.txt +21 -0
- package/output/native-build/renderer-extension.js +703 -58
- package/package.json +192 -189
- package/scripts/collaboration-test.cjs +262 -200
- package/scripts/collaboration-ui-smoke.cjs +85 -77
- package/scripts/core-concurrency-test.cjs +23 -1
- package/scripts/e2e-pi-cancel.cjs +99 -0
- package/scripts/native-file-change-test.cjs +42 -2
- package/scripts/native-sidebar-test.cjs +8 -0
- package/scripts/native-vendor-adapters-test.cjs +67 -1
- package/scripts/pets-test.cjs +63 -0
- package/scripts/pi-cancel-race-test.cjs +102 -0
- package/scripts/stuck-turn-test.cjs +73 -0
- package/scripts/workspace-test.cjs +9 -0
- package/src/assets/skins/dream-skin/NOTICE.md +13 -0
- package/src/assets/skins/dream-skin/themes/gothic-void-crusade/hero.webp +0 -0
- package/src/assets/skins/dream-skin/themes/gothic-void-crusade/theme.json +34 -0
- package/src/main/adapters/claude.js +9 -6
- package/src/main/adapters/pi-family.js +41 -8
- package/src/main/harness-adapter/event-normalizer.js +270 -258
- package/src/main/host/collaboration-mcp.cjs +8 -1
- package/src/main/host/collaboration-tools.js +1 -1
- package/src/main/host/collaboration.js +81 -11
- package/src/main/host/runtime.js +1420 -1341
- package/src/main/native/pets.js +79 -2
- package/src/main/native/protocol.js +1160 -1145
- package/src/main/native/thread-list.js +6 -1
- package/src/main/workspace/core-review.js +75 -0
- package/src/main/workspace/diff.js +5 -1
- package/src/main/workspace/file-changes.js +9 -4
- package/src/main/workspace/review-controller.js +115 -102
- package/src/main/workspace/review.js +90 -88
- package/src/native-ui/renderer-extension/dist/contract-audit.js +16637 -0
- package/src/native-ui/renderer-extension/dist/index.js +34853 -0
- package/src/native-ui/renderer-extension/dist/production.js +34263 -0
- package/src/native-ui/renderer-extension/dist/renderer-binding-probe.js +34254 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +11 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-pet-switcher.d.ts +73 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-pet-switcher.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/collaboration-page.d.ts +12 -0
- package/src/native-ui/renderer-extension/dist/types/settings/collaboration-page.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +5 -3
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/pet-market.d.ts +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/pet-market.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/pets-client.d.ts +9 -0
- package/src/native-ui/renderer-extension/dist/types/settings/pets-client.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/skin-runtime.d.ts +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/skin-runtime.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +3181 -3171
- package/src/native-ui/renderer-extension/src/renderer-model-client.ts +14 -0
- package/src/native-ui/renderer-extension/src/renderer-pet-switcher.ts +308 -0
- package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +2 -0
- package/src/native-ui/renderer-extension/src/renderer-team-cards.ts +76 -21
- package/src/native-ui/renderer-extension/src/settings/collaboration-page.ts +139 -0
- package/src/native-ui/renderer-extension/src/settings/icons.ts +155 -153
- package/src/native-ui/renderer-extension/src/settings/localization.ts +785 -783
- package/src/native-ui/renderer-extension/src/settings/pages.ts +694 -681
- package/src/native-ui/renderer-extension/src/settings/pet-market.css +110 -1
- package/src/native-ui/renderer-extension/src/settings/pet-market.ts +276 -22
- package/src/native-ui/renderer-extension/src/settings/pets-client.ts +32 -0
- package/src/native-ui/renderer-extension/src/settings/shell.css +2294 -2255
- package/src/native-ui/renderer-extension/src/settings/skin-runtime.ts +51 -1
- package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +2 -0
- package/src/native-ui/renderer-extension/test/renderer-pet-switcher.test.ts +400 -0
- package/src/native-ui/renderer-extension/test/settings/localization.test.ts +80 -79
- package/src/native-ui/renderer-extension/test/settings/pet-market.test.ts +378 -0
- package/src/native-ui/renderer-extension/test/settings/pets-client.test.ts +83 -0
- package/src/native-ui/renderer-extension/test/settings/shell.test.ts +3 -0
- 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
|
|
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 = () =>
|
|
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
|
|
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 === "
|
|
370
|
-
? "pet-btn pet-btn--
|
|
371
|
-
: "pet-btn pet-btn--
|
|
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 === "
|
|
382
|
-
? "pet-btn pet-btn--
|
|
383
|
-
: "pet-btn pet-btn--
|
|
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", () =>
|
|
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
|
}
|