@harness-mix/cli 0.2.0 → 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 +13 -0
- 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
|
@@ -16,6 +16,7 @@ import gildedGrandeurHero from "../../../../assets/skins/codex-styler/themes/gil
|
|
|
16
16
|
import merryBigTopHero from "../../../../assets/skins/codex-styler/themes/merry-big-top/hero.webp";
|
|
17
17
|
import nocturneStudioHero from "../../../../assets/skins/codex-styler/themes/nocturne-studio/hero.webp";
|
|
18
18
|
import quietGardenHero from "../../../../assets/skins/codex-styler/themes/quiet-garden/hero.webp";
|
|
19
|
+
import gothicVoidCrusadeHero from "../../../../assets/skins/dream-skin/themes/gothic-void-crusade/hero.webp";
|
|
19
20
|
|
|
20
21
|
export const RENDERER_SKIN_STORAGE_KEY = "harness-mix.renderer-skin.v1";
|
|
21
22
|
export const RENDERER_SKIN_STYLE_ID = "harness-mix-renderer-skin";
|
|
@@ -26,7 +27,11 @@ export const RENDERER_SKIN_IDS = [
|
|
|
26
27
|
"wuthering-tide", "naruto-hokage", "naruto-sasuke", "deepspace-dawn",
|
|
27
28
|
"deepspace-star", "dragonball-nimbus", "dragonball-super-saiyan", "dalao-dianyan",
|
|
28
29
|
"styler-gilded-grandeur", "styler-merry-big-top", "styler-nocturne-studio",
|
|
29
|
-
"styler-quiet-garden",
|
|
30
|
+
"styler-quiet-garden", "dream-gothic-void-crusade",
|
|
31
|
+
"palette-catppuccin-latte", "palette-catppuccin-mocha", "palette-claude-desktop-dark",
|
|
32
|
+
"palette-claude-desktop-light", "palette-gruvbox-dark", "palette-gruvbox-light",
|
|
33
|
+
"palette-nord-dark", "palette-nord-light", "palette-one-dark", "palette-one-light",
|
|
34
|
+
"palette-tokyo-night-dark", "palette-tokyo-night-light",
|
|
30
35
|
] as const;
|
|
31
36
|
|
|
32
37
|
export type RendererSkinId = (typeof RENDERER_SKIN_IDS)[number];
|
|
@@ -47,6 +52,8 @@ export interface RendererSkinDefinition {
|
|
|
47
52
|
|
|
48
53
|
const HEIGE_SOURCE_URL = "https://github.com/HeiGeAi/heige-codex-skin-studio";
|
|
49
54
|
const CODEX_STYLER_SOURCE_URL = "https://github.com/xuhuanstudio/codex-styler";
|
|
55
|
+
const DREAM_SKIN_SOURCE_URL = "https://github.com/Fei-Away/Codex-Dream-Skin";
|
|
56
|
+
const ANTHROPIC_THEME_SOURCE_URL = "https://github.com/miniLV/Anthropic-codex-theme";
|
|
50
57
|
|
|
51
58
|
function heigeSkin(
|
|
52
59
|
id: Exclude<RendererSkinId, "native">,
|
|
@@ -83,6 +90,36 @@ function codexStylerSkin(
|
|
|
83
90
|
});
|
|
84
91
|
}
|
|
85
92
|
|
|
93
|
+
function dreamSkin(
|
|
94
|
+
id: Exclude<RendererSkinId, "native">,
|
|
95
|
+
name: string,
|
|
96
|
+
heroUrl: string,
|
|
97
|
+
palette: RendererSkinDefinition["palette"],
|
|
98
|
+
focus: string,
|
|
99
|
+
): RendererSkinDefinition {
|
|
100
|
+
return Object.freeze({
|
|
101
|
+
id, name, heroUrl, logoUrl: null, polaroidUrl: null, palette, focus, dark: true,
|
|
102
|
+
sourceName: "Codex Dream Skin · MIT",
|
|
103
|
+
sourceUrl: DREAM_SKIN_SOURCE_URL,
|
|
104
|
+
preview: "linear-gradient(rgb(3 7 12 / 16%), transparent)",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function paletteSkin(
|
|
109
|
+
id: Exclude<RendererSkinId, "native">,
|
|
110
|
+
name: string,
|
|
111
|
+
palette: RendererSkinDefinition["palette"],
|
|
112
|
+
panel: string,
|
|
113
|
+
dark: boolean,
|
|
114
|
+
): RendererSkinDefinition {
|
|
115
|
+
return Object.freeze({
|
|
116
|
+
id, name, heroUrl: null, logoUrl: null, polaroidUrl: null, palette, focus: "50% 50%", dark,
|
|
117
|
+
sourceName: "Anthropic Codex Theme · MIT",
|
|
118
|
+
sourceUrl: ANTHROPIC_THEME_SOURCE_URL,
|
|
119
|
+
preview: `linear-gradient(155deg, ${palette[0]} 0 58%, ${panel} 58% 100%)`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
86
123
|
export const RENDERER_SKINS: readonly RendererSkinDefinition[] = Object.freeze([
|
|
87
124
|
Object.freeze({
|
|
88
125
|
id: "native", name: "Native Codex", sourceName: "OpenAI Codex", sourceUrl: null,
|
|
@@ -115,6 +152,19 @@ export const RENDERER_SKINS: readonly RendererSkinDefinition[] = Object.freeze([
|
|
|
115
152
|
codexStylerSkin("styler-merry-big-top", "欢乐大帐篷", merryBigTopHero, ["#100b14", "#5a3d4b", "#ff755e", "#fff2dc"], "55% 48%"),
|
|
116
153
|
codexStylerSkin("styler-nocturne-studio", "夜曲工作室", nocturneStudioHero, ["#090b0d", "#34383a", "#e9a066", "#f5efe6"], "58% 46%"),
|
|
117
154
|
codexStylerSkin("styler-quiet-garden", "静谧花园", quietGardenHero, ["#101612", "#364339", "#9fc29a", "#edf2e8"], "52% 50%"),
|
|
155
|
+
dreamSkin("dream-gothic-void-crusade", "哥特虚空远征", gothicVoidCrusadeHero, ["#0d0d0e", "#b5a386", "#c8a55a", "#f3ead7"], "76% 45%"),
|
|
156
|
+
paletteSkin("palette-catppuccin-latte", "Catppuccin · 拿铁", ["#EFF1F5", "#5C5F77", "#FE640B", "#4C4F69"], "#E6E9EF", false),
|
|
157
|
+
paletteSkin("palette-catppuccin-mocha", "Catppuccin · 摩卡", ["#181825", "#A6ADC8", "#FAB387", "#CDD6F4"], "#1E1E2E", true),
|
|
158
|
+
paletteSkin("palette-claude-desktop-dark", "Claude · 墨夜", ["#1A1918", "#A8A49C", "#CA7554", "#E8E4DC"], "#222120", true),
|
|
159
|
+
paletteSkin("palette-claude-desktop-light", "Claude · 素纸", ["#F8F6F1", "#605B54", "#CA7554", "#38342E"], "#F3EFE7", false),
|
|
160
|
+
paletteSkin("palette-gruvbox-dark", "Gruvbox · 暖夜", ["#282828", "#D5C4A1", "#FE8019", "#EBDBB2"], "#32302F", true),
|
|
161
|
+
paletteSkin("palette-gruvbox-light", "Gruvbox · 暖昼", ["#FBF1C7", "#504945", "#D65D0E", "#3C3836"], "#F2E5BC", false),
|
|
162
|
+
paletteSkin("palette-nord-dark", "Nord · 极夜", ["#2E3440", "#C3CBD9", "#88C0D0", "#D8DEE9"], "#3B4252", true),
|
|
163
|
+
paletteSkin("palette-nord-light", "Nord · 雪原", ["#ECEFF4", "#3B4252", "#5E81AC", "#2E3440"], "#E5E9F0", false),
|
|
164
|
+
paletteSkin("palette-one-dark", "One Dark · 子夜", ["#282C34", "#9DA5B4", "#61AFEF", "#ABB2BF"], "#21252B", true),
|
|
165
|
+
paletteSkin("palette-one-light", "One Light · 皓昼", ["#FAFAFA", "#4F525E", "#4078F2", "#383A42"], "#F0F0F1", false),
|
|
166
|
+
paletteSkin("palette-tokyo-night-dark", "Tokyo Night · 夜", ["#1A1B26", "#A9B1D6", "#7AA2F7", "#C0CAF5"], "#16161E", true),
|
|
167
|
+
paletteSkin("palette-tokyo-night-light", "Tokyo Night · 晨", ["#E1E2E7", "#4C505E", "#2E7DE9", "#343B59"], "#D5D6DB", false),
|
|
118
168
|
]);
|
|
119
169
|
|
|
120
170
|
interface RendererSkinStorage {
|
|
@@ -292,6 +292,7 @@ describe("Renderer fixed Model request client", () => {
|
|
|
292
292
|
"deleteCodexAccount",
|
|
293
293
|
"executeThreadCommand",
|
|
294
294
|
"forkThread",
|
|
295
|
+
"getCollaborationPreferences",
|
|
295
296
|
"importHarnessSession",
|
|
296
297
|
"inspectCodexAccountUsage",
|
|
297
298
|
"inspectHarness",
|
|
@@ -321,6 +322,7 @@ describe("Renderer fixed Model request client", () => {
|
|
|
321
322
|
"readUpdateStatus",
|
|
322
323
|
"refreshCodexAccounts",
|
|
323
324
|
"removeMcp",
|
|
325
|
+
"saveCollaborationPreferences",
|
|
324
326
|
"saveMcp",
|
|
325
327
|
"selectThreadModel",
|
|
326
328
|
"selectThreadPermissionMode",
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { switchOfficialCodexPet } from "../src/renderer-pet-switcher.js";
|
|
3
|
+
|
|
4
|
+
const FAST_TIMING = {
|
|
5
|
+
pollMs: 10,
|
|
6
|
+
openTimeoutMs: 400,
|
|
7
|
+
gridTimeoutMs: 400,
|
|
8
|
+
confirmTimeoutMs: 250,
|
|
9
|
+
closeTimeoutMs: 300,
|
|
10
|
+
menuSettleMs: 40,
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
/** Minimal DOM node covering exactly what the switcher touches. */
|
|
14
|
+
class FakeElement {
|
|
15
|
+
readonly attributes = new Map<string, string>();
|
|
16
|
+
readonly children: FakeElement[] = [];
|
|
17
|
+
readonly listeners = new Map<string, Array<(event: Event) => void>>();
|
|
18
|
+
parent: FakeElement | null = null;
|
|
19
|
+
text = "";
|
|
20
|
+
rect = { x: 0, y: 0, width: 12, height: 12, top: 0, left: 0, right: 12, bottom: 12 };
|
|
21
|
+
|
|
22
|
+
constructor(readonly tagName: string) {}
|
|
23
|
+
|
|
24
|
+
get ownerDocument(): FakeDocument | null {
|
|
25
|
+
let node: FakeElement | null = this;
|
|
26
|
+
while (node) {
|
|
27
|
+
if (node instanceof FakeDocument) return node;
|
|
28
|
+
node = node.parent;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get textContent(): string {
|
|
34
|
+
return this.text + this.children.map((child) => child.textContent).join("");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getBoundingClientRect(): FakeElement["rect"] {
|
|
38
|
+
return this.rect;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
append(...children: FakeElement[]): void {
|
|
42
|
+
for (const child of children) {
|
|
43
|
+
child.parent = this;
|
|
44
|
+
this.children.push(child);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
remove(): void {
|
|
49
|
+
this.parent?.removeChild(this);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
removeChild(child: FakeElement): void {
|
|
53
|
+
const index = this.children.indexOf(child);
|
|
54
|
+
if (index !== -1) this.children.splice(index, 1);
|
|
55
|
+
child.parent = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setAttribute(name: string, value: string): void {
|
|
59
|
+
this.attributes.set(name, value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getAttribute(name: string): string | null {
|
|
63
|
+
return this.attributes.get(name) ?? null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
hasAttribute(name: string): boolean {
|
|
67
|
+
return this.attributes.has(name);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
addEventListener(type: string, listener: (event: Event) => void): void {
|
|
71
|
+
const list = this.listeners.get(type) ?? [];
|
|
72
|
+
list.push(listener);
|
|
73
|
+
this.listeners.set(type, list);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
dispatchEvent(event: Event): boolean {
|
|
77
|
+
for (const listener of this.listeners.get(event.type) ?? []) listener(event);
|
|
78
|
+
if (event.bubbles && this.parent) this.parent.dispatchEvent(event);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
click(): void {
|
|
83
|
+
this.dispatchEvent(new Event("click", { bubbles: true }));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
closest(selector: string): FakeElement | null {
|
|
87
|
+
let node: FakeElement | null = this;
|
|
88
|
+
while (node) {
|
|
89
|
+
if (matchesSelector(node, selector)) return node;
|
|
90
|
+
node = node.parent;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
querySelector(selector: string): FakeElement | null {
|
|
96
|
+
return this.querySelectorAll(selector)[0] ?? null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
querySelectorAll(selector: string): FakeElement[] {
|
|
100
|
+
const out: FakeElement[] = [];
|
|
101
|
+
const walk = (node: FakeElement): void => {
|
|
102
|
+
for (const child of node.children) {
|
|
103
|
+
if (matchesSelector(child, selector)) out.push(child);
|
|
104
|
+
walk(child);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
walk(this);
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function matchesSelector(element: FakeElement, selector: string): boolean {
|
|
113
|
+
return selector.split(",").some((part) => {
|
|
114
|
+
const trimmed = part.trim();
|
|
115
|
+
const attrMatch = /^(?:([a-z]+))?\[([a-zA-Z-]+)(?:="([^"]*)")?\]$/.exec(trimmed);
|
|
116
|
+
if (attrMatch) {
|
|
117
|
+
const [, tag, attr, value] = attrMatch;
|
|
118
|
+
if (tag && element.tagName !== tag) return false;
|
|
119
|
+
if (!element.hasAttribute(attr!)) return false;
|
|
120
|
+
if (value !== undefined && element.getAttribute(attr!) !== value) return false;
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return element.tagName === trimmed;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
class FakeDocument extends FakeElement {
|
|
128
|
+
readonly defaultView = { navigator: { platform: "Win32" }, PointerEvent: undefined };
|
|
129
|
+
|
|
130
|
+
constructor() {
|
|
131
|
+
super("#document");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let fakeDocument: FakeDocument;
|
|
136
|
+
|
|
137
|
+
interface FakeAppOptions {
|
|
138
|
+
/** Pet avatar ids in the official grid (already mapped form, e.g. custom:rush / bsod). */
|
|
139
|
+
readonly petIds: readonly string[];
|
|
140
|
+
/** Whether clicking a pet card confirms the selection (the app's account mutation). */
|
|
141
|
+
readonly confirmOnClick?: boolean;
|
|
142
|
+
/** Avatar id that starts out selected. */
|
|
143
|
+
readonly initiallySelected?: string;
|
|
144
|
+
/** Whether the profile menu exists at all. */
|
|
145
|
+
readonly withProfileMenu?: boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Simulates the official Codex app's reactions to the switcher's genuine DOM
|
|
150
|
+
* events, mirroring the live-verified structure: profile menu (Radix) with a
|
|
151
|
+
* "…Ctrl+," settings item → settings surface with data-settings-panel-slug
|
|
152
|
+
* nav + main>header back button → pet grid of button[aria-pressed] cards.
|
|
153
|
+
*/
|
|
154
|
+
function installFakeOfficialApp(options: FakeAppOptions) {
|
|
155
|
+
const state = {
|
|
156
|
+
profileMenuOpen: false,
|
|
157
|
+
settingsOpen: false,
|
|
158
|
+
settingsSection: null as string | null,
|
|
159
|
+
selectedPetId: options.initiallySelected ?? null,
|
|
160
|
+
profileClicks: 0,
|
|
161
|
+
clickedPetIds: [] as string[],
|
|
162
|
+
backClicks: 0,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const menuRoot = new FakeElement("div");
|
|
166
|
+
const settingsRoot = new FakeElement("div");
|
|
167
|
+
|
|
168
|
+
const openProfileMenu = () => {
|
|
169
|
+
state.profileMenuOpen = true;
|
|
170
|
+
const menu = new FakeElement("div");
|
|
171
|
+
const settingsItem = new FakeElement("div");
|
|
172
|
+
settingsItem.setAttribute("role", "menuitem");
|
|
173
|
+
settingsItem.text = "设置Ctrl+,";
|
|
174
|
+
settingsItem.addEventListener("click", () => {
|
|
175
|
+
closeProfileMenu();
|
|
176
|
+
openSettings("general-settings");
|
|
177
|
+
});
|
|
178
|
+
const usageItem = new FakeElement("div");
|
|
179
|
+
usageItem.setAttribute("role", "menuitem");
|
|
180
|
+
usageItem.text = "剩余用量";
|
|
181
|
+
menu.append(settingsItem, usageItem);
|
|
182
|
+
menuRoot.append(menu);
|
|
183
|
+
};
|
|
184
|
+
const closeProfileMenu = () => {
|
|
185
|
+
state.profileMenuOpen = false;
|
|
186
|
+
menuRoot.children.splice(0);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const openSettings = (section: string) => {
|
|
190
|
+
state.settingsOpen = true;
|
|
191
|
+
state.settingsSection = section;
|
|
192
|
+
renderSettings();
|
|
193
|
+
};
|
|
194
|
+
const closeSettings = () => {
|
|
195
|
+
state.settingsOpen = false;
|
|
196
|
+
state.settingsSection = null;
|
|
197
|
+
settingsRoot.children.splice(0);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const renderSettings = () => {
|
|
201
|
+
settingsRoot.children.splice(0);
|
|
202
|
+
const main = new FakeElement("main");
|
|
203
|
+
const header = new FakeElement("header");
|
|
204
|
+
const back = new FakeElement("button");
|
|
205
|
+
back.setAttribute("aria-label", "返回 ChatGPT");
|
|
206
|
+
back.addEventListener("click", () => {
|
|
207
|
+
state.backClicks += 1;
|
|
208
|
+
closeSettings();
|
|
209
|
+
});
|
|
210
|
+
header.append(back);
|
|
211
|
+
main.append(header);
|
|
212
|
+
const nav = new FakeElement("nav");
|
|
213
|
+
for (const slug of ["general-settings", "pets"]) {
|
|
214
|
+
const item = new FakeElement("button");
|
|
215
|
+
item.setAttribute("data-settings-panel-slug", slug);
|
|
216
|
+
item.text = slug;
|
|
217
|
+
item.addEventListener("click", () => openSettings(slug));
|
|
218
|
+
nav.append(item);
|
|
219
|
+
}
|
|
220
|
+
main.append(nav);
|
|
221
|
+
if (state.settingsSection === "pets") {
|
|
222
|
+
const grid = new FakeElement("div");
|
|
223
|
+
for (const petId of options.petIds) {
|
|
224
|
+
const card = new FakeElement("button");
|
|
225
|
+
card.setAttribute("aria-pressed", String(state.selectedPetId === petId));
|
|
226
|
+
card.text = petId;
|
|
227
|
+
const artwork = new FakeElement("div");
|
|
228
|
+
artwork.setAttribute("data-avatar-id", petId);
|
|
229
|
+
card.append(artwork);
|
|
230
|
+
card.addEventListener("click", () => {
|
|
231
|
+
state.clickedPetIds.push(petId);
|
|
232
|
+
if (options.confirmOnClick !== false) {
|
|
233
|
+
state.selectedPetId = petId;
|
|
234
|
+
renderSettings(); // the app re-renders after the account mutation
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
grid.append(card);
|
|
238
|
+
}
|
|
239
|
+
main.append(grid);
|
|
240
|
+
}
|
|
241
|
+
settingsRoot.append(main);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
if (options.withProfileMenu !== false) {
|
|
245
|
+
const profileButton = new FakeElement("button");
|
|
246
|
+
profileButton.setAttribute("aria-haspopup", "menu");
|
|
247
|
+
profileButton.rect = { x: 8, y: 680, width: 40, height: 40, top: 680, left: 8, right: 48, bottom: 720 };
|
|
248
|
+
const avatarImg = new FakeElement("img");
|
|
249
|
+
profileButton.append(avatarImg);
|
|
250
|
+
profileButton.addEventListener("click", () => {
|
|
251
|
+
state.profileClicks += 1;
|
|
252
|
+
if (state.profileMenuOpen) closeProfileMenu();
|
|
253
|
+
else openProfileMenu();
|
|
254
|
+
});
|
|
255
|
+
// 另一个带 img 的菜单按钮(模型选择器),停靠在右下:必须不被误选
|
|
256
|
+
const modelButton = new FakeElement("button");
|
|
257
|
+
modelButton.setAttribute("aria-haspopup", "menu");
|
|
258
|
+
modelButton.rect = { x: 748, y: 666, width: 90, height: 32, top: 666, left: 748, right: 838, bottom: 698 };
|
|
259
|
+
modelButton.append(new FakeElement("img"));
|
|
260
|
+
fakeDocument.append(profileButton, modelButton);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fakeDocument.append(menuRoot, settingsRoot);
|
|
264
|
+
return state;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const runSwitch = (target: { id: string; displayName: string }) =>
|
|
268
|
+
switchOfficialCodexPet(target, {
|
|
269
|
+
ownerDocument: fakeDocument as unknown as Document,
|
|
270
|
+
timing: FAST_TIMING,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("switchOfficialCodexPet", () => {
|
|
274
|
+
it("drives the official UI: profile menu → Settings item → pets nav → pet card → confirmed → settings closed", async () => {
|
|
275
|
+
fakeDocument = new FakeDocument();
|
|
276
|
+
const app = installFakeOfficialApp({ petIds: ["custom:rush", "bsod"] });
|
|
277
|
+
|
|
278
|
+
const result = await runSwitch({ id: "rush", displayName: "Rush" });
|
|
279
|
+
|
|
280
|
+
expect(result).toEqual({ avatarId: "custom:rush", alreadySelected: false });
|
|
281
|
+
expect(app.profileClicks).toBe(1); // opened the profile menu once
|
|
282
|
+
expect(app.clickedPetIds).toEqual(["custom:rush"]); // genuine click on the official card
|
|
283
|
+
expect(app.selectedPetId).toBe("custom:rush"); // confirmed by aria-pressed
|
|
284
|
+
expect(app.settingsOpen).toBe(false); // settings left closed afterwards
|
|
285
|
+
expect(app.backClicks).toBe(1);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("maps built-in pets to their bare avatar id", async () => {
|
|
289
|
+
fakeDocument = new FakeDocument();
|
|
290
|
+
const app = installFakeOfficialApp({ petIds: ["bsod", "codex"] });
|
|
291
|
+
const result = await runSwitch({ id: "bsod", displayName: "BSOD" });
|
|
292
|
+
expect(result).toEqual({ avatarId: "bsod", alreadySelected: false });
|
|
293
|
+
expect(app.selectedPetId).toBe("bsod");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("picks the left-most avatar menu button (not the model picker)", async () => {
|
|
297
|
+
fakeDocument = new FakeDocument();
|
|
298
|
+
const app = installFakeOfficialApp({ petIds: ["custom:rush"] });
|
|
299
|
+
await runSwitch({ id: "rush", displayName: "Rush" });
|
|
300
|
+
expect(app.profileClicks).toBe(1); // only the profile button was triggered
|
|
301
|
+
expect(app.selectedPetId).toBe("custom:rush");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("does not click when the pet is already selected", async () => {
|
|
305
|
+
fakeDocument = new FakeDocument();
|
|
306
|
+
const app = installFakeOfficialApp({
|
|
307
|
+
petIds: ["custom:rush"],
|
|
308
|
+
initiallySelected: "custom:rush",
|
|
309
|
+
});
|
|
310
|
+
const result = await runSwitch({ id: "rush", displayName: "Rush" });
|
|
311
|
+
expect(result).toEqual({ avatarId: "custom:rush", alreadySelected: true });
|
|
312
|
+
expect(app.clickedPetIds).toEqual([]);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("fails cleanly when the pet is missing from the official grid", async () => {
|
|
316
|
+
fakeDocument = new FakeDocument();
|
|
317
|
+
const app = installFakeOfficialApp({ petIds: ["bsod"] });
|
|
318
|
+
await expect(runSwitch({ id: "ghost", displayName: "Ghost" })).rejects.toThrow(
|
|
319
|
+
/not found in the official Codex Pets settings/,
|
|
320
|
+
);
|
|
321
|
+
expect(app.settingsOpen).toBe(false); // settings closed on failure
|
|
322
|
+
expect(app.profileMenuOpen).toBe(false);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("times out with a clear error when the click is never confirmed", async () => {
|
|
326
|
+
fakeDocument = new FakeDocument();
|
|
327
|
+
const app = installFakeOfficialApp({ petIds: ["custom:rush"], confirmOnClick: false });
|
|
328
|
+
await expect(runSwitch({ id: "rush", displayName: "Rush" })).rejects.toThrow(/not confirmed/);
|
|
329
|
+
expect(app.settingsOpen).toBe(false);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("rejects when the profile menu button is absent", async () => {
|
|
333
|
+
fakeDocument = new FakeDocument();
|
|
334
|
+
installFakeOfficialApp({ petIds: ["custom:rush"], withProfileMenu: false });
|
|
335
|
+
await expect(runSwitch({ id: "rush", displayName: "Rush" })).rejects.toThrow(
|
|
336
|
+
/profile menu button not found/,
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("recovers when the profile menu was already open (toggle-and-retry)", async () => {
|
|
341
|
+
fakeDocument = new FakeDocument();
|
|
342
|
+
const app = installFakeOfficialApp({ petIds: ["custom:rush"] });
|
|
343
|
+
// 手动点开菜单模拟残留状态:第一次 pointer 序列会把它关掉,重试路径再打开
|
|
344
|
+
const profileButton = fakeDocument.querySelector('button[aria-haspopup="menu"]') as FakeElement;
|
|
345
|
+
profileButton.click();
|
|
346
|
+
expect(app.profileMenuOpen).toBe(true);
|
|
347
|
+
const result = await runSwitch({ id: "rush", displayName: "Rush" });
|
|
348
|
+
expect(result.alreadySelected).toBe(false);
|
|
349
|
+
expect(app.profileClicks).toBeGreaterThan(1);
|
|
350
|
+
expect(app.selectedPetId).toBe("custom:rush");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("guards against concurrent switch runs", async () => {
|
|
354
|
+
fakeDocument = new FakeDocument();
|
|
355
|
+
installFakeOfficialApp({ petIds: ["custom:rush"], confirmOnClick: false });
|
|
356
|
+
const first = runSwitch({ id: "rush", displayName: "Rush" });
|
|
357
|
+
await expect(runSwitch({ id: "bsod", displayName: "BSOD" })).rejects.toThrow(/already in progress/);
|
|
358
|
+
await expect(first).rejects.toThrow(/not confirmed/);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("uses the settings sidebar when the official settings are already open and leaves them open", async () => {
|
|
362
|
+
fakeDocument = new FakeDocument();
|
|
363
|
+
const app = installFakeOfficialApp({ petIds: ["custom:rush"] });
|
|
364
|
+
// 用户自己开着官方设置(停在别的分组):直接走 data-settings-panel-slug 导航
|
|
365
|
+
const main = new FakeElement("main");
|
|
366
|
+
const header = new FakeElement("header");
|
|
367
|
+
const back = new FakeElement("button");
|
|
368
|
+
back.setAttribute("aria-label", "返回 ChatGPT");
|
|
369
|
+
header.append(back);
|
|
370
|
+
main.append(header);
|
|
371
|
+
const nav = new FakeElement("nav");
|
|
372
|
+
const generalItem = new FakeElement("button");
|
|
373
|
+
generalItem.setAttribute("data-settings-panel-slug", "general-settings");
|
|
374
|
+
const petsItem = new FakeElement("button");
|
|
375
|
+
petsItem.setAttribute("data-settings-panel-slug", "pets");
|
|
376
|
+
nav.append(generalItem, petsItem);
|
|
377
|
+
main.append(nav);
|
|
378
|
+
const gridHolder = new FakeElement("div");
|
|
379
|
+
main.append(gridHolder);
|
|
380
|
+
fakeDocument.append(main);
|
|
381
|
+
petsItem.addEventListener("click", () => {
|
|
382
|
+
const card = new FakeElement("button");
|
|
383
|
+
card.setAttribute("aria-pressed", "false");
|
|
384
|
+
const artwork = new FakeElement("div");
|
|
385
|
+
artwork.setAttribute("data-avatar-id", "custom:rush");
|
|
386
|
+
card.append(artwork);
|
|
387
|
+
card.addEventListener("click", () => {
|
|
388
|
+
app.clickedPetIds.push("custom:rush");
|
|
389
|
+
card.setAttribute("aria-pressed", "true");
|
|
390
|
+
});
|
|
391
|
+
gridHolder.append(card);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const result = await runSwitch({ id: "rush", displayName: "Rush" });
|
|
395
|
+
expect(result.alreadySelected).toBe(false);
|
|
396
|
+
expect(app.clickedPetIds).toEqual(["custom:rush"]);
|
|
397
|
+
expect(app.profileClicks).toBe(0); // 没有动用个人菜单
|
|
398
|
+
expect(app.backClicks).toBe(0); // 用户自己开着的设置保持原样
|
|
399
|
+
});
|
|
400
|
+
});
|
|
@@ -1,82 +1,83 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
codexLocaleOverrideForSettingsSelection,
|
|
5
|
-
rendererSettingsLanguageSelection,
|
|
6
|
-
rendererSettingsMessages,
|
|
7
|
-
resolveRendererSettingsLocale,
|
|
8
|
-
} from "../../src/settings/localization.js";
|
|
9
|
-
import { createDefaultRendererSettingsPages } from "../../src/settings/pages.js";
|
|
10
|
-
|
|
11
|
-
describe("Renderer settings localization", () => {
|
|
12
|
-
it("negotiates English and Chinese language tags with an English fallback", () => {
|
|
13
|
-
expect(resolveRendererSettingsLocale(["zh-CN"])).toBe("zh-CN");
|
|
14
|
-
expect(resolveRendererSettingsLocale(["zh-TW"])).toBe("zh-CN");
|
|
15
|
-
expect(resolveRendererSettingsLocale(["en-GB"])).toBe("en");
|
|
16
|
-
expect(resolveRendererSettingsLocale(["fr-FR", "zh-CN"])).toBe("zh-CN");
|
|
17
|
-
expect(resolveRendererSettingsLocale(["fr-FR"])).toBe("en");
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("maps the bounded selector to Codex locale override values", () => {
|
|
21
|
-
expect(rendererSettingsLanguageSelection(null)).toBe("automatic");
|
|
22
|
-
expect(rendererSettingsLanguageSelection("en-GB")).toBe("en");
|
|
23
|
-
expect(rendererSettingsLanguageSelection("zh-TW")).toBe("zh-CN");
|
|
24
|
-
expect(rendererSettingsLanguageSelection("ja-JP")).toBe("other");
|
|
25
|
-
expect(codexLocaleOverrideForSettingsSelection("automatic")).toBeNull();
|
|
26
|
-
expect(codexLocaleOverrideForSettingsSelection("en")).toBe("en-US");
|
|
27
|
-
expect(codexLocaleOverrideForSettingsSelection("zh-CN")).toBe("zh-CN");
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("provides complete immutable English and Chinese settings catalogs", () => {
|
|
31
|
-
const english = rendererSettingsMessages("en");
|
|
32
|
-
const chinese = rendererSettingsMessages("zh-CN");
|
|
33
|
-
|
|
34
|
-
expect(english.title).toBe("Settings");
|
|
35
|
-
expect(chinese.title).toBe("设置");
|
|
36
|
-
expect(chinese.openSettings).toBe("打开 Harness Mix 设置");
|
|
37
|
-
expect(chinese.updateInstallation).toBe("安装方式");
|
|
38
|
-
expect(chinese.updateInstallationWindowsInstaller).toBe("Windows 安装程序");
|
|
39
|
-
expect(english.updateInstallationMacOsDmg).toBe("macOS DMG");
|
|
40
|
-
expect(chinese.updateWaitingForExit).toBe("正在等待应用退出...");
|
|
41
|
-
expect(chinese.updateInstallingNpm).toBe("正在通过 npm 安装...");
|
|
42
|
-
expect(chinese.updateAndRestart).toBe("更新");
|
|
43
|
-
expect(chinese.updateWindowsManualRequired).toContain("Windows 暂不支持自动更新");
|
|
44
|
-
expect(chinese.updateWindowsInstallerDescription).toContain("适用于当前系统的安装包");
|
|
45
|
-
expect(chinese.updateManualNpmDescription).toBe(
|
|
46
|
-
"如需手动更新,请在终端运行以下命令。更新完成后,请退出 Codex 并通过 Harness Mix 重新启动。",
|
|
47
|
-
);
|
|
48
|
-
expect(english.updateInstalling).toBe("Installing update...");
|
|
49
|
-
expect(english.updateDownloadFromReleases).toBe("Download from GitHub Releases");
|
|
50
|
-
expect(chinese.updateDownloadFromReleases).toBe("前往 GitHub Releases 下载");
|
|
51
|
-
expect(chinese.updateDownloadWindowsInstaller).toBe("下载 Windows 安装包");
|
|
52
|
-
expect(chinese.pageLabels.about).toBe("关于");
|
|
53
|
-
expect(chinese.pageLabels["session-import"]).toBe("会话导入");
|
|
54
|
-
expect(chinese.sessionImportAvailabilityNote).toContain("可选 Harness 来自本地 Host");
|
|
55
|
-
expect(chinese.sessionImportAvailabilityNote).toContain("先在原生客户端关闭该会话再导入");
|
|
56
|
-
expect(chinese.sessionImportAvailabilityNote).toContain("避免同时写入");
|
|
57
|
-
expect(english.sessionImportAvailabilityNote).toContain(
|
|
58
|
-
"Available Harnesses come from the local Host",
|
|
59
|
-
);
|
|
60
|
-
expect(english.sessionImportAvailabilityNote).toContain(
|
|
61
|
-
"close the session in its native client before importing",
|
|
62
|
-
);
|
|
63
|
-
expect(english.sessionImportAvailabilityNote).toContain("avoid concurrent writes");
|
|
64
|
-
expect(chinese.aboutTagline).toBe("在 Codex Desktop 中运行各类原生 Harness");
|
|
65
|
-
expect(chinese.aboutParagraphs).toHaveLength(3);
|
|
66
|
-
expect(chinese.aboutStarCallout).toContain("请给我们一个 Star");
|
|
67
|
-
expect(chinese.aboutRepository).toBe("开源仓库");
|
|
68
|
-
expect(Object.keys(chinese.pageLabels)).toEqual(Object.keys(english.pageLabels));
|
|
69
|
-
expect(Object.isFrozen(english)).toBe(true);
|
|
70
|
-
expect(Object.isFrozen(chinese.pageLabels)).toBe(true);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("localizes every default page descriptor", () => {
|
|
74
|
-
expect(
|
|
75
|
-
createDefaultRendererSettingsPages(rendererSettingsMessages("zh-CN")).map(
|
|
76
|
-
({ label }) => label,
|
|
77
|
-
),
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
codexLocaleOverrideForSettingsSelection,
|
|
5
|
+
rendererSettingsLanguageSelection,
|
|
6
|
+
rendererSettingsMessages,
|
|
7
|
+
resolveRendererSettingsLocale,
|
|
8
|
+
} from "../../src/settings/localization.js";
|
|
9
|
+
import { createDefaultRendererSettingsPages } from "../../src/settings/pages.js";
|
|
10
|
+
|
|
11
|
+
describe("Renderer settings localization", () => {
|
|
12
|
+
it("negotiates English and Chinese language tags with an English fallback", () => {
|
|
13
|
+
expect(resolveRendererSettingsLocale(["zh-CN"])).toBe("zh-CN");
|
|
14
|
+
expect(resolveRendererSettingsLocale(["zh-TW"])).toBe("zh-CN");
|
|
15
|
+
expect(resolveRendererSettingsLocale(["en-GB"])).toBe("en");
|
|
16
|
+
expect(resolveRendererSettingsLocale(["fr-FR", "zh-CN"])).toBe("zh-CN");
|
|
17
|
+
expect(resolveRendererSettingsLocale(["fr-FR"])).toBe("en");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("maps the bounded selector to Codex locale override values", () => {
|
|
21
|
+
expect(rendererSettingsLanguageSelection(null)).toBe("automatic");
|
|
22
|
+
expect(rendererSettingsLanguageSelection("en-GB")).toBe("en");
|
|
23
|
+
expect(rendererSettingsLanguageSelection("zh-TW")).toBe("zh-CN");
|
|
24
|
+
expect(rendererSettingsLanguageSelection("ja-JP")).toBe("other");
|
|
25
|
+
expect(codexLocaleOverrideForSettingsSelection("automatic")).toBeNull();
|
|
26
|
+
expect(codexLocaleOverrideForSettingsSelection("en")).toBe("en-US");
|
|
27
|
+
expect(codexLocaleOverrideForSettingsSelection("zh-CN")).toBe("zh-CN");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("provides complete immutable English and Chinese settings catalogs", () => {
|
|
31
|
+
const english = rendererSettingsMessages("en");
|
|
32
|
+
const chinese = rendererSettingsMessages("zh-CN");
|
|
33
|
+
|
|
34
|
+
expect(english.title).toBe("Settings");
|
|
35
|
+
expect(chinese.title).toBe("设置");
|
|
36
|
+
expect(chinese.openSettings).toBe("打开 Harness Mix 设置");
|
|
37
|
+
expect(chinese.updateInstallation).toBe("安装方式");
|
|
38
|
+
expect(chinese.updateInstallationWindowsInstaller).toBe("Windows 安装程序");
|
|
39
|
+
expect(english.updateInstallationMacOsDmg).toBe("macOS DMG");
|
|
40
|
+
expect(chinese.updateWaitingForExit).toBe("正在等待应用退出...");
|
|
41
|
+
expect(chinese.updateInstallingNpm).toBe("正在通过 npm 安装...");
|
|
42
|
+
expect(chinese.updateAndRestart).toBe("更新");
|
|
43
|
+
expect(chinese.updateWindowsManualRequired).toContain("Windows 暂不支持自动更新");
|
|
44
|
+
expect(chinese.updateWindowsInstallerDescription).toContain("适用于当前系统的安装包");
|
|
45
|
+
expect(chinese.updateManualNpmDescription).toBe(
|
|
46
|
+
"如需手动更新,请在终端运行以下命令。更新完成后,请退出 Codex 并通过 Harness Mix 重新启动。",
|
|
47
|
+
);
|
|
48
|
+
expect(english.updateInstalling).toBe("Installing update...");
|
|
49
|
+
expect(english.updateDownloadFromReleases).toBe("Download from GitHub Releases");
|
|
50
|
+
expect(chinese.updateDownloadFromReleases).toBe("前往 GitHub Releases 下载");
|
|
51
|
+
expect(chinese.updateDownloadWindowsInstaller).toBe("下载 Windows 安装包");
|
|
52
|
+
expect(chinese.pageLabels.about).toBe("关于");
|
|
53
|
+
expect(chinese.pageLabels["session-import"]).toBe("会话导入");
|
|
54
|
+
expect(chinese.sessionImportAvailabilityNote).toContain("可选 Harness 来自本地 Host");
|
|
55
|
+
expect(chinese.sessionImportAvailabilityNote).toContain("先在原生客户端关闭该会话再导入");
|
|
56
|
+
expect(chinese.sessionImportAvailabilityNote).toContain("避免同时写入");
|
|
57
|
+
expect(english.sessionImportAvailabilityNote).toContain(
|
|
58
|
+
"Available Harnesses come from the local Host",
|
|
59
|
+
);
|
|
60
|
+
expect(english.sessionImportAvailabilityNote).toContain(
|
|
61
|
+
"close the session in its native client before importing",
|
|
62
|
+
);
|
|
63
|
+
expect(english.sessionImportAvailabilityNote).toContain("avoid concurrent writes");
|
|
64
|
+
expect(chinese.aboutTagline).toBe("在 Codex Desktop 中运行各类原生 Harness");
|
|
65
|
+
expect(chinese.aboutParagraphs).toHaveLength(3);
|
|
66
|
+
expect(chinese.aboutStarCallout).toContain("请给我们一个 Star");
|
|
67
|
+
expect(chinese.aboutRepository).toBe("开源仓库");
|
|
68
|
+
expect(Object.keys(chinese.pageLabels)).toEqual(Object.keys(english.pageLabels));
|
|
69
|
+
expect(Object.isFrozen(english)).toBe(true);
|
|
70
|
+
expect(Object.isFrozen(chinese.pageLabels)).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("localizes every default page descriptor", () => {
|
|
74
|
+
expect(
|
|
75
|
+
createDefaultRendererSettingsPages(rendererSettingsMessages("zh-CN")).map(
|
|
76
|
+
({ label }) => label,
|
|
77
|
+
),
|
|
78
78
|
).toEqual([
|
|
79
79
|
"连接",
|
|
80
|
+
"协作",
|
|
80
81
|
"账号",
|
|
81
82
|
"MCP",
|
|
82
83
|
"Skills",
|
|
@@ -87,5 +88,5 @@ describe("Renderer settings localization", () => {
|
|
|
87
88
|
"更新",
|
|
88
89
|
"关于",
|
|
89
90
|
]);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
91
|
+
});
|
|
92
|
+
});
|