@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.
- package/CHANGELOG.md +17 -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 +1462 -1341
- package/src/main/native/pets.js +79 -2
- package/src/main/native/protocol.js +1164 -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
|
@@ -183,6 +183,8 @@ export interface RendererModelClient extends Partial<RendererSessionImportClient
|
|
|
183
183
|
inspectStorage?(): Promise<import('./settings/storage-page.js').RendererStorageInspection>;
|
|
184
184
|
optimizeStorage?(): Promise<{ before: import('./settings/storage-page.js').RendererStorageInspection; after: import('./settings/storage-page.js').RendererStorageInspection }>;
|
|
185
185
|
listCollaborationAgents?(): Promise<Array<{ id: string; name: string; available: boolean; lead: boolean }>>;
|
|
186
|
+
getCollaborationPreferences?(): Promise<{ collaboration: boolean; agentTeam: boolean }>;
|
|
187
|
+
saveCollaborationPreferences?(input: { collaboration?: boolean; agentTeam?: boolean }): Promise<{ collaboration: boolean; agentTeam: boolean }>;
|
|
186
188
|
currentHostId?(): string | null;
|
|
187
189
|
listHarnessPlugins?(): Promise<HarnessPluginListResult>;
|
|
188
190
|
clientForHost?(hostId: string): RendererModelClient | null;
|
|
@@ -361,6 +363,18 @@ export function createRendererModelClient(
|
|
|
361
363
|
if (!Array.isArray(result) || !result.every(a => a && typeof a.id === 'string' && typeof a.name === 'string' && typeof a.available === 'boolean' && typeof a.lead === 'boolean')) throw new Error('Invalid collaboration catalog');
|
|
362
364
|
return result;
|
|
363
365
|
},
|
|
366
|
+
async getCollaborationPreferences(): Promise<{ collaboration: boolean; agentTeam: boolean }> {
|
|
367
|
+
const result = await manager.sendRequest('harnessmix/collaboration/preferences', {});
|
|
368
|
+
const value = result as { collaboration?: unknown; agentTeam?: unknown } | null;
|
|
369
|
+
if (!value || typeof value.collaboration !== 'boolean' || typeof value.agentTeam !== 'boolean') throw new Error('Invalid collaboration preferences');
|
|
370
|
+
return { collaboration: value.collaboration, agentTeam: value.agentTeam };
|
|
371
|
+
},
|
|
372
|
+
async saveCollaborationPreferences(input: { collaboration?: boolean; agentTeam?: boolean }): Promise<{ collaboration: boolean; agentTeam: boolean }> {
|
|
373
|
+
const result = await manager.sendRequest('harnessmix/collaboration/preferences/save', input);
|
|
374
|
+
const value = result as { collaboration?: unknown; agentTeam?: unknown } | null;
|
|
375
|
+
if (!value || typeof value.collaboration !== 'boolean' || typeof value.agentTeam !== 'boolean') throw new Error('Invalid collaboration preferences');
|
|
376
|
+
return { collaboration: value.collaboration, agentTeam: value.agentTeam };
|
|
377
|
+
},
|
|
364
378
|
...createRendererIntegrationsClient((method, params) => manager.sendRequest(method, params)),
|
|
365
379
|
...createRendererSessionImportClient(async (method, params) =>
|
|
366
380
|
manager.sendRequest(method, params),
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drives Codex Desktop's OFFICIAL pet selection from the Harness Mix pet market.
|
|
3
|
+
*
|
|
4
|
+
* The official pet is the account-level mascot (the app's own Settings → Pets
|
|
5
|
+
* grid). This module performs pure in-page UI automation — genuine DOM clicks
|
|
6
|
+
* on the app's own elements only:
|
|
7
|
+
*
|
|
8
|
+
* 1. open the app's profile menu (avatar button, bottom-left of the sidebar),
|
|
9
|
+
* 2. click its Settings item (the one carrying the Ctrl+, shortcut hint),
|
|
10
|
+
* 3. click the "pets" settings nav (`data-settings-panel-slug="pets"`),
|
|
11
|
+
* 4. click the target pet card (`[data-avatar-id]` inside `button[aria-pressed]`),
|
|
12
|
+
* 5. confirm via the card's `aria-pressed="true"` state,
|
|
13
|
+
* 6. click the settings header back button, leaving every surface it opened closed.
|
|
14
|
+
*
|
|
15
|
+
* HARD CONSTRAINT: this never calls account APIs, never reads tokens or
|
|
16
|
+
* credentials, and never proxies `accessory_id` — the app's own click handler
|
|
17
|
+
* performs the account mutation with the app's own auth.
|
|
18
|
+
*
|
|
19
|
+
* Selector facts LIVE-VERIFIED against the installed Codex Desktop
|
|
20
|
+
* 26.908.4834.0 (via CDP Runtime.evaluate in the app webview):
|
|
21
|
+
* - profile menu trigger: `button[aria-haspopup="menu"]` wrapping an avatar
|
|
22
|
+
* `<img>`, docked at the left edge of the sidebar; opens a Radix menu on
|
|
23
|
+
* pointerdown (hence the pointer sequence, not a bare .click());
|
|
24
|
+
* - the Settings menu item is a `[role="menuitem"]` whose text carries the
|
|
25
|
+
* locale-independent shortcut hint `Ctrl+,`;
|
|
26
|
+
* - settings sections render nav buttons with `data-settings-panel-slug`
|
|
27
|
+
* ("general-settings", "pets", …) — locale-independent;
|
|
28
|
+
* - pet cards are `<button aria-pressed="…">` wrapping artwork with
|
|
29
|
+
* `data-avatar-id="<id>"`; built-in ids are bare (`codex`, `bsod`, …)
|
|
30
|
+
* while pets installed under ~/.codex/pets/<dir>/ appear as `custom:<dir>`;
|
|
31
|
+
* - the settings surface is a `main > header` layout whose first
|
|
32
|
+
* `button[aria-label]` is the back button; clicking it returns to the app.
|
|
33
|
+
* - NOTE: there is NO working Ctrl/Cmd+K command menu in this build — an
|
|
34
|
+
* earlier revision of this module relied on it after static app.asar
|
|
35
|
+
* analysis; live verification proved it never opens (cmdk nodes do not
|
|
36
|
+
* exist in the DOM). Do not reintroduce it without re-verifying live.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export interface RendererPetSwitchTarget {
|
|
40
|
+
/** Harness Mix market id (directory name under ~/.codex/pets for custom pets). */
|
|
41
|
+
readonly id: string;
|
|
42
|
+
readonly displayName: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RendererPetSwitchResult {
|
|
46
|
+
/** The official `data-avatar-id` that ended up selected. */
|
|
47
|
+
readonly avatarId: string;
|
|
48
|
+
/** True when the pet was already the selected one (no click was needed). */
|
|
49
|
+
readonly alreadySelected: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface RendererPetSwitcherTiming {
|
|
53
|
+
/** Poll interval for all waits. */
|
|
54
|
+
readonly pollMs: number;
|
|
55
|
+
/** Waiting for the profile menu / settings surface to open. */
|
|
56
|
+
readonly openTimeoutMs: number;
|
|
57
|
+
/** Waiting for the pets grid after navigation. */
|
|
58
|
+
readonly gridTimeoutMs: number;
|
|
59
|
+
/** Waiting for aria-pressed confirmation after the click. */
|
|
60
|
+
readonly confirmTimeoutMs: number;
|
|
61
|
+
/** Best-effort wait for surfaces to close during cleanup. */
|
|
62
|
+
readonly closeTimeoutMs: number;
|
|
63
|
+
/** Settle delay after opening the profile menu before reading its items. */
|
|
64
|
+
readonly menuSettleMs: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface RendererPetSwitcherOptions {
|
|
68
|
+
readonly ownerDocument?: Document;
|
|
69
|
+
readonly timing?: Partial<RendererPetSwitcherTiming>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const DEFAULT_TIMING: RendererPetSwitcherTiming = {
|
|
73
|
+
pollMs: 120,
|
|
74
|
+
openTimeoutMs: 3500,
|
|
75
|
+
gridTimeoutMs: 5000,
|
|
76
|
+
confirmTimeoutMs: 5000,
|
|
77
|
+
closeTimeoutMs: 2000,
|
|
78
|
+
menuSettleMs: 350,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const PROFILE_MENU_TRIGGER = 'button[aria-haspopup="menu"]';
|
|
82
|
+
const MENU_ITEM = '[role="menuitem"]';
|
|
83
|
+
const SETTINGS_NAV_ITEM = "[data-settings-panel-slug]";
|
|
84
|
+
const PETS_NAV_ITEM = '[data-settings-panel-slug="pets"]';
|
|
85
|
+
const AVATAR_ARTWORK = "[data-avatar-id]";
|
|
86
|
+
/** 设置菜单项:文本里带与语言无关的快捷键提示 Ctrl+, */
|
|
87
|
+
const SETTINGS_SHORTCUT_HINT = /ctrl\s*\+\s*,/i;
|
|
88
|
+
|
|
89
|
+
function sleep(ms: number): Promise<void> {
|
|
90
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function waitFor(
|
|
94
|
+
probe: () => boolean,
|
|
95
|
+
timeoutMs: number,
|
|
96
|
+
pollMs: number,
|
|
97
|
+
description: string,
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
const deadline = Date.now() + timeoutMs;
|
|
100
|
+
for (;;) {
|
|
101
|
+
if (probe()) return;
|
|
102
|
+
if (Date.now() >= deadline) throw new Error(`Timed out: ${description}`);
|
|
103
|
+
await sleep(pollMs);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Radix 下拉菜单在 pointerdown 上展开,因此菜单交互用完整的指针事件序列;
|
|
109
|
+
* 事件构造优先用页面自己的 PointerEvent(测试环境没有就退回普通 Event)。
|
|
110
|
+
*/
|
|
111
|
+
function dispatchPointerSequence(element: HTMLElement): void {
|
|
112
|
+
const view = element.ownerDocument?.defaultView;
|
|
113
|
+
const PointerCtor = view?.PointerEvent;
|
|
114
|
+
for (const type of ["pointerdown", "mousedown", "pointerup", "mouseup", "click"] as const) {
|
|
115
|
+
const event = PointerCtor
|
|
116
|
+
? new PointerCtor(type, { bubbles: true, cancelable: true, view, button: 0, buttons: 1 })
|
|
117
|
+
: new Event(type, { bubbles: true, cancelable: true });
|
|
118
|
+
element.dispatchEvent(event);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 个人菜单按钮:侧栏左下角带头像 <img> 的 aria-haspopup="menu" 按钮。
|
|
124
|
+
* 结构特征定位,不依赖任何语言文案(模型选择器同样带 img,但停靠在右下)。
|
|
125
|
+
*/
|
|
126
|
+
function findProfileMenuButton(ownerDocument: Document): HTMLElement | null {
|
|
127
|
+
let best: { element: HTMLElement; x: number } | null = null;
|
|
128
|
+
for (const button of ownerDocument.querySelectorAll<HTMLElement>(PROFILE_MENU_TRIGGER)) {
|
|
129
|
+
if (button.querySelector("img") === null) continue;
|
|
130
|
+
const rect = button.getBoundingClientRect();
|
|
131
|
+
if (rect.width === 0 && rect.height === 0) continue;
|
|
132
|
+
if (best === null || rect.x < best.x) best = { element: button, x: rect.x };
|
|
133
|
+
}
|
|
134
|
+
return best?.element ?? null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 个人菜单里的设置项:文本带 Ctrl+, 快捷键提示(与语言无关) */
|
|
138
|
+
function findSettingsMenuItem(ownerDocument: Document): HTMLElement | null {
|
|
139
|
+
for (const item of ownerDocument.querySelectorAll<HTMLElement>(MENU_ITEM)) {
|
|
140
|
+
if (SETTINGS_SHORTCUT_HINT.test(item.textContent ?? "")) return item;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 官方设置页的返回按钮:main > header 里的第一个带 aria-label 的按钮 */
|
|
146
|
+
function findSettingsBackButton(ownerDocument: Document): HTMLElement | null {
|
|
147
|
+
const main = ownerDocument.querySelector("main");
|
|
148
|
+
const header = main?.querySelector("header");
|
|
149
|
+
return header?.querySelector<HTMLElement>("button[aria-label]") ?? null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 官方宠物卡片:button[aria-pressed] 包裹 [data-avatar-id] 的 artwork */
|
|
153
|
+
function petCardButtons(ownerDocument: Document): HTMLElement[] {
|
|
154
|
+
const buttons: HTMLElement[] = [];
|
|
155
|
+
for (const artwork of ownerDocument.querySelectorAll(AVATAR_ARTWORK)) {
|
|
156
|
+
const button = artwork.closest("button");
|
|
157
|
+
if (button?.hasAttribute("aria-pressed")) buttons.push(button as HTMLElement);
|
|
158
|
+
}
|
|
159
|
+
return buttons;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function findTargetCard(
|
|
163
|
+
ownerDocument: Document,
|
|
164
|
+
target: RendererPetSwitchTarget,
|
|
165
|
+
): { button: HTMLElement; avatarId: string } | null {
|
|
166
|
+
// 官方预载宠物用裸 id;~/.codex/pets/<dir>/ 下的本地宠物在官方网格里是 custom:<dir>
|
|
167
|
+
for (const avatarId of [`custom:${target.id}`, target.id]) {
|
|
168
|
+
const artwork = ownerDocument.querySelector(`[data-avatar-id="${avatarId}"]`);
|
|
169
|
+
const button = artwork?.closest("button");
|
|
170
|
+
if (button) return { button: button as HTMLElement, avatarId };
|
|
171
|
+
}
|
|
172
|
+
// 兜底:按显示名匹配(官方网格的卡片 label 是 displayName)
|
|
173
|
+
const name = target.displayName.trim().toLowerCase();
|
|
174
|
+
if (name) {
|
|
175
|
+
for (const button of petCardButtons(ownerDocument)) {
|
|
176
|
+
if ((button.textContent ?? "").trim().toLowerCase().includes(name)) {
|
|
177
|
+
const artwork = button.querySelector(AVATAR_ARTWORK);
|
|
178
|
+
return { button, avatarId: artwork?.getAttribute("data-avatar-id") ?? target.id };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function cardIsSelected(ownerDocument: Document, target: RendererPetSwitchTarget): boolean {
|
|
186
|
+
const found = findTargetCard(ownerDocument, target);
|
|
187
|
+
return found?.button.getAttribute("aria-pressed") === "true";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let activeSwitch: Promise<RendererPetSwitchResult> | null = null;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Switch Codex Desktop's official pet by driving the app's own settings UI.
|
|
194
|
+
* Rejects when another switch is still running (the market page additionally
|
|
195
|
+
* disables its buttons while a switch is in flight).
|
|
196
|
+
*/
|
|
197
|
+
export function switchOfficialCodexPet(
|
|
198
|
+
target: RendererPetSwitchTarget,
|
|
199
|
+
options: RendererPetSwitcherOptions = {},
|
|
200
|
+
): Promise<RendererPetSwitchResult> {
|
|
201
|
+
if (activeSwitch) {
|
|
202
|
+
return Promise.reject(new Error("An official pet switch is already in progress"));
|
|
203
|
+
}
|
|
204
|
+
const run = runOfficialCodexPetSwitch(target, options).finally(() => {
|
|
205
|
+
activeSwitch = null;
|
|
206
|
+
});
|
|
207
|
+
activeSwitch = run;
|
|
208
|
+
return run;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function runOfficialCodexPetSwitch(
|
|
212
|
+
target: RendererPetSwitchTarget,
|
|
213
|
+
options: RendererPetSwitcherOptions,
|
|
214
|
+
): Promise<RendererPetSwitchResult> {
|
|
215
|
+
const ownerDocument = options.ownerDocument ?? document;
|
|
216
|
+
const timing: RendererPetSwitcherTiming = { ...DEFAULT_TIMING, ...options.timing };
|
|
217
|
+
const settingsAlreadyOpen = ownerDocument.querySelector(SETTINGS_NAV_ITEM) !== null;
|
|
218
|
+
let openedSettings = false;
|
|
219
|
+
try {
|
|
220
|
+
if (!settingsAlreadyOpen) {
|
|
221
|
+
// 打开个人菜单(侧栏左下角头像按钮),点击其中带 Ctrl+, 快捷键提示的设置项
|
|
222
|
+
const profileButton = findProfileMenuButton(ownerDocument);
|
|
223
|
+
if (!profileButton) throw new Error("Codex profile menu button not found");
|
|
224
|
+
dispatchPointerSequence(profileButton);
|
|
225
|
+
let settingsItem: HTMLElement | null = null;
|
|
226
|
+
try {
|
|
227
|
+
await waitFor(
|
|
228
|
+
() => {
|
|
229
|
+
settingsItem = findSettingsMenuItem(ownerDocument);
|
|
230
|
+
return settingsItem !== null;
|
|
231
|
+
},
|
|
232
|
+
timing.openTimeoutMs,
|
|
233
|
+
timing.pollMs,
|
|
234
|
+
"the Codex profile menu did not open",
|
|
235
|
+
);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
// 菜单可能本就已展开(上次失败残留):再触发一次尝试恢复后仍失败才报错
|
|
238
|
+
dispatchPointerSequence(profileButton);
|
|
239
|
+
await waitFor(
|
|
240
|
+
() => {
|
|
241
|
+
settingsItem = findSettingsMenuItem(ownerDocument);
|
|
242
|
+
return settingsItem !== null;
|
|
243
|
+
},
|
|
244
|
+
timing.menuSettleMs,
|
|
245
|
+
timing.pollMs,
|
|
246
|
+
(error as Error).message,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
dispatchPointerSequence(settingsItem as unknown as HTMLElement);
|
|
250
|
+
await waitFor(
|
|
251
|
+
() => ownerDocument.querySelector(SETTINGS_NAV_ITEM) !== null,
|
|
252
|
+
timing.openTimeoutMs,
|
|
253
|
+
timing.pollMs,
|
|
254
|
+
"the Codex settings did not open",
|
|
255
|
+
);
|
|
256
|
+
openedSettings = true;
|
|
257
|
+
}
|
|
258
|
+
if (!findTargetCard(ownerDocument, target)) {
|
|
259
|
+
// 设置已打开但不在桌宠页:走设置侧边栏的桌宠项(选择器与语言无关)
|
|
260
|
+
const navItem = ownerDocument.querySelector<HTMLElement>(PETS_NAV_ITEM);
|
|
261
|
+
if (navItem) navItem.click();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
await waitFor(
|
|
265
|
+
() => petCardButtons(ownerDocument).length > 0,
|
|
266
|
+
timing.gridTimeoutMs,
|
|
267
|
+
timing.pollMs,
|
|
268
|
+
"the official Pets grid did not render",
|
|
269
|
+
);
|
|
270
|
+
const found = findTargetCard(ownerDocument, target);
|
|
271
|
+
if (!found) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`Pet "${target.displayName}" (${target.id}) was not found in the official Codex Pets settings`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
if (found.button.getAttribute("aria-pressed") === "true") {
|
|
277
|
+
return { avatarId: found.avatarId, alreadySelected: true };
|
|
278
|
+
}
|
|
279
|
+
found.button.click();
|
|
280
|
+
await waitFor(
|
|
281
|
+
// React 可能重渲染出新节点,每次轮询都重新定位卡片
|
|
282
|
+
() => cardIsSelected(ownerDocument, target),
|
|
283
|
+
timing.confirmTimeoutMs,
|
|
284
|
+
timing.pollMs,
|
|
285
|
+
`selecting "${target.displayName}" was not confirmed by the official Pets settings`,
|
|
286
|
+
);
|
|
287
|
+
return { avatarId: found.avatarId, alreadySelected: false };
|
|
288
|
+
} finally {
|
|
289
|
+
// 收场:只关闭本次由我们打开的官方设置(用户自己开着的设置保持原样)。
|
|
290
|
+
// 不用 Escape —— Harness Mix 设置对话框也是 modal <dialog>,Escape 会先关掉我们自己的界面。
|
|
291
|
+
if (openedSettings && ownerDocument.querySelector(SETTINGS_NAV_ITEM) !== null) {
|
|
292
|
+
const backButton = findSettingsBackButton(ownerDocument);
|
|
293
|
+
if (backButton) {
|
|
294
|
+
backButton.click();
|
|
295
|
+
try {
|
|
296
|
+
await waitFor(
|
|
297
|
+
() => ownerDocument.querySelector(SETTINGS_NAV_ITEM) === null,
|
|
298
|
+
timing.closeTimeoutMs,
|
|
299
|
+
timing.pollMs,
|
|
300
|
+
"the official settings did not close",
|
|
301
|
+
);
|
|
302
|
+
} catch {
|
|
303
|
+
// 关闭失败不掩盖主流程结果(设置页留在官方桌宠页,用户可手动返回)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -34,6 +34,7 @@ export interface RendererSettingsLifecycleOptions {
|
|
|
34
34
|
getSessionImportClient?(): RendererSessionImportClient | null;
|
|
35
35
|
getStorageClient?(): RendererStorageClient | null;
|
|
36
36
|
getPetClient?(): RendererPetsClient | null;
|
|
37
|
+
getCollaborationClient?(): import('./settings/pages.js').RendererCollaborationClient | null;
|
|
37
38
|
openImportedThread?: RendererImportedThreadOpener;
|
|
38
39
|
onLocaleChange?(locale: RendererSettingsLocale): void;
|
|
39
40
|
}
|
|
@@ -84,6 +85,7 @@ export function installRendererSettingsLifecycle(
|
|
|
84
85
|
options.getIntegrationsClient,
|
|
85
86
|
options.getStorageClient,
|
|
86
87
|
options.getPetClient,
|
|
88
|
+
options.getCollaborationClient,
|
|
87
89
|
);
|
|
88
90
|
const nextShell = installRendererSettingsShell(definitions, messages, ownerWindow.document);
|
|
89
91
|
const nextTrigger = installRendererSettingsHeaderTrigger({
|
|
@@ -59,11 +59,13 @@ const TEAM_STYLE = `
|
|
|
59
59
|
.harness-mix-team-body{display:grid;grid-template-columns:minmax(0,1fr) clamp(228px,24vw,300px);min-height:0;overflow:hidden}
|
|
60
60
|
.harness-mix-team-lanes{display:flex;gap:16px;min-height:0;box-sizing:border-box;overflow-x:auto;overflow-y:hidden;padding:18px 16px 10px;justify-content:safe center;scroll-snap-type:x proximity}
|
|
61
61
|
.harness-mix-team-lane{position:relative;flex:1 1 0;min-width:216px;max-width:300px;display:flex;flex-direction:column;min-height:0;padding-top:16px;scroll-snap-align:start}
|
|
62
|
-
.harness-mix-team-lane::before{content:'';position:absolute;top:0;left:50%;height:16px;
|
|
63
|
-
.harness-mix-team-lane::after{content:'';position:absolute;top:0;left:-9px;right:-9px;
|
|
62
|
+
.harness-mix-team-lane::before{content:'';position:absolute;top:0;left:50%;width:2px;height:16px;margin-left:-1px;background:repeating-linear-gradient(to bottom,color-mix(in srgb,currentColor 22%,transparent) 0 3px,transparent 3px 7px);animation:harness-mix-team-flow-y 1.4s linear infinite}
|
|
63
|
+
.harness-mix-team-lane::after{content:'';position:absolute;top:0;left:-9px;right:-9px;height:2px;background:repeating-linear-gradient(to right,color-mix(in srgb,currentColor 22%,transparent) 0 3px,transparent 3px 7px);animation:harness-mix-team-flow-x 1.4s linear infinite}
|
|
64
64
|
.harness-mix-team-lane:first-child::after{left:calc(50% - 1px)}
|
|
65
65
|
.harness-mix-team-lane:last-child::after{right:calc(50% - 1px)}
|
|
66
66
|
.harness-mix-team-lane:only-child::after{display:none}
|
|
67
|
+
.harness-mix-team-lane[data-status="working"]::before{background-image:repeating-linear-gradient(to bottom,color-mix(in srgb,var(--lane-color,currentColor) 85%,transparent) 0 3px,transparent 3px 7px);animation-duration:.55s}
|
|
68
|
+
.harness-mix-team-lane[data-status="working"]::after{background-image:repeating-linear-gradient(to right,color-mix(in srgb,var(--lane-color,currentColor) 60%,transparent) 0 3px,transparent 3px 7px);animation-duration:.55s}
|
|
67
69
|
.harness-mix-team-task-list{display:flex;flex-direction:column;gap:6px;flex:1;min-height:0;overflow-y:auto;padding:2px 8px 8px;scrollbar-width:none}
|
|
68
70
|
.harness-mix-team-task-list::-webkit-scrollbar{display:none}
|
|
69
71
|
.harness-mix-team-feed{display:flex;flex-direction:column;min-height:0;min-width:0;border-left:1px solid color-mix(in srgb,currentColor 9%,transparent)}
|
|
@@ -71,11 +73,34 @@ const TEAM_STYLE = `
|
|
|
71
73
|
.harness-mix-team-clamp2{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
72
74
|
.harness-mix-team-clickable{transition:border-color .15s ease,box-shadow .15s ease}
|
|
73
75
|
.harness-mix-team-clickable:hover{border-color:color-mix(in srgb,currentColor 28%,transparent)!important;box-shadow:0 4px 14px color-mix(in srgb,#000 10%,transparent)!important}
|
|
76
|
+
.harness-mix-team-open-failed{border-color:#d14343!important;box-shadow:0 0 0 3px color-mix(in srgb,#d14343 25%,transparent)!important}
|
|
77
|
+
.harness-mix-team-member[data-status="working"],.harness-mix-team-lead[data-status="working"]{border-color:color-mix(in srgb,var(--lane-color,#2878e3) 38%,transparent)!important;box-shadow:0 0 12px color-mix(in srgb,var(--lane-color,#2878e3) 18%,transparent)}
|
|
78
|
+
.harness-mix-team-avatar[data-status="working"]{animation:harness-mix-team-breathe 2s ease-in-out infinite}
|
|
79
|
+
.harness-mix-team-status-dot[data-status="working"]{animation:harness-mix-team-pulse 1.6s ease-in-out infinite}
|
|
80
|
+
@keyframes harness-mix-team-flow-y{to{background-position:0 7px}}
|
|
81
|
+
@keyframes harness-mix-team-flow-x{to{background-position:7px 0}}
|
|
82
|
+
@keyframes harness-mix-team-pulse{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--lane-color,#2878e3) 40%,transparent)}50%{box-shadow:0 0 0 4px color-mix(in srgb,var(--lane-color,#2878e3) 0%,transparent)}}
|
|
83
|
+
@keyframes harness-mix-team-breathe{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--lane-color,#2878e3) 28%,transparent)}50%{box-shadow:0 0 10px 2px color-mix(in srgb,var(--lane-color,#2878e3) 38%,transparent)}}
|
|
74
84
|
@container (max-width:720px){
|
|
75
85
|
.harness-mix-team-body{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr) auto}
|
|
76
86
|
.harness-mix-team-feed{border-left:0;border-top:1px solid color-mix(in srgb,currentColor 9%,transparent);max-height:136px}
|
|
77
87
|
}`;
|
|
78
88
|
|
|
89
|
+
// 成员卡片/chip 的「打开原生会话」接线:可点态 + hover 反馈 + 失败可视提示(不静默)。
|
|
90
|
+
function wireOpen(node: HTMLElement, childId: string | undefined, name: string, openThread?: TeamCardOptions['openThread']) {
|
|
91
|
+
if (!childId || !openThread) return;
|
|
92
|
+
node.style.cursor = 'pointer';
|
|
93
|
+
node.classList.add('harness-mix-team-clickable');
|
|
94
|
+
node.title = `打开 ${name} 的原生 Harness 会话`;
|
|
95
|
+
node.addEventListener('click', async () => {
|
|
96
|
+
try { await openThread(childId); } catch (error) {
|
|
97
|
+
console.warn('[TeamCards] 打开成员会话失败:', error);
|
|
98
|
+
node.classList.add('harness-mix-team-open-failed');
|
|
99
|
+
setTimeout(() => node.classList.remove('harness-mix-team-open-failed'), 1200);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
79
104
|
function metrics(payload: TeamCardPayload) {
|
|
80
105
|
const node = el('div', 'harness-mix-team-metrics', 'display:flex;gap:18px;align-items:center');
|
|
81
106
|
const values: Array<[string, string, string]> = [[`${payload.tasks.filter(task => task.status === 'completed').length}/${payload.tasks.length}`, '完成', '#1f9d68'], [`${payload.tasks.filter(task => task.status === 'in_progress').length}`, '进行中', '#2878e3'], [`${payload.tasks.filter(task => task.status === 'blocked').length}`, '等待', '#c17022']];
|
|
@@ -101,18 +126,20 @@ function headerStats(payload: TeamCardPayload) {
|
|
|
101
126
|
}
|
|
102
127
|
const pillCss = (status?: string) => { const color = stateColor(status); return `font-size:8px;font-weight:750;padding:2px 7px;border-radius:99px;white-space:nowrap;color:${color};background:color-mix(in srgb,${color} 15%,transparent)`; };
|
|
103
128
|
function statusPill(status?: string) { return text('span', stateLabel(status), pillCss(status)); }
|
|
104
|
-
function statusDot(status: string | undefined, size = 7) {
|
|
129
|
+
function statusDot(status: string | undefined, size = 7) { const dot = el('span', 'harness-mix-team-status-dot', `width:${size}px;height:${size}px;border-radius:50%;flex:none;background:${stateColor(status)}`); dot.dataset.status = status ?? ''; return dot; }
|
|
130
|
+
const actionIcon = (action?: string) => ({ team_created: '🎬', task_assigned: '📋', task_updated: '🔄', task_started: '🚀', member_session_ready: '🔌', task_settled: '✅', task_failed: '❌', task_cancelled: '⛔', task_interrupted: '⏸', task_resumed: '▶️', task_followup: '💬' }[action ?? ''] ?? '•');
|
|
105
131
|
|
|
106
|
-
function renderBoard(payload: TeamCardPayload, openThread?: TeamCardOptions['openThread']) {
|
|
132
|
+
function renderBoard(payload: TeamCardPayload, openThread?: TeamCardOptions['openThread'], snapshots: TeamSnapshot[] = []) {
|
|
107
133
|
const board = el('main', 'harness-mix-team-board harness-mix-team-body', 'height:100%;min-height:0;color:inherit');
|
|
108
134
|
const lead = payload.lead ?? { id: 'lead', name: 'Team Lead', role: '协调与验收', agent: 'codex', display_status: 'working' };
|
|
109
135
|
const tasksOf = (memberId: string) => payload.tasks.filter(task => task.assignee === memberId);
|
|
110
136
|
|
|
111
137
|
const leadCard = () => {
|
|
112
138
|
const button = el('button', 'harness-mix-team-lead', 'appearance:none;border:1px solid color-mix(in srgb,#c35b24 34%,transparent);background:color-mix(in srgb,#c35b24 7%,transparent);box-shadow:0 2px 10px color-mix(in srgb,#c35b24 10%,transparent);color:inherit;font:inherit;display:flex;align-items:center;gap:10px;padding:8px 16px;border-radius:13px;cursor:default'); button.type = 'button'; button.dataset.agent = lead.agent;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const avatar = el('span',
|
|
139
|
+
button.dataset.status = lead.display_status ?? '';
|
|
140
|
+
wireOpen(button, lead.childId ?? lead.child_thread_id, lead.name, openThread);
|
|
141
|
+
const avatar = el('span', 'harness-mix-team-avatar', 'position:relative;display:grid;place-items:center;width:42px;height:42px;flex:none;border-radius:12px;border:2px solid color-mix(in srgb,#c35b24 45%,transparent);background:Canvas');
|
|
142
|
+
avatar.dataset.status = lead.display_status ?? '';
|
|
116
143
|
avatar.append(collaborationIcon(lead.agent, lead.name, 32), text('span', '👑', 'position:absolute;top:-12px;right:-10px;font-size:13px;filter:drop-shadow(0 1px 1px rgb(0 0 0/.25))'));
|
|
117
144
|
const copy = el('span', undefined, 'display:grid;min-width:0;text-align:left;line-height:1.3');
|
|
118
145
|
copy.append(text('span', '主导者', 'font-size:8.5px;font-weight:800;letter-spacing:.14em;color:#c35b24'), text('strong', lead.name, 'font-size:13px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap'));
|
|
@@ -123,10 +150,12 @@ function renderBoard(payload: TeamCardPayload, openThread?: TeamCardOptions['ope
|
|
|
123
150
|
const memberCard = (member: TeamMemberPayload, index: number) => {
|
|
124
151
|
const color = memberColor(index), assigned = tasksOf(member.id), done = assigned.filter(task => task.status === 'completed').length;
|
|
125
152
|
const button = el('button', 'harness-mix-team-member', 'appearance:none;width:100%;box-sizing:border-box;border:1px solid color-mix(in srgb,currentColor 10%,transparent);background:color-mix(in srgb,Canvas 92%,transparent);box-shadow:0 2px 8px color-mix(in srgb,#000 6%,transparent);color:inherit;font:inherit;display:grid;grid-template-columns:40px minmax(0,1fr);align-items:center;gap:9px;padding:9px 10px;border-radius:12px;text-align:left;cursor:default'); button.type = 'button'; button.dataset.agent = member.agent;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const avatar = el('span',
|
|
129
|
-
avatar.
|
|
153
|
+
button.dataset.status = member.display_status ?? ''; button.style.setProperty('--lane-color', color ?? '');
|
|
154
|
+
wireOpen(button, member.childId ?? member.child_thread_id, member.name, openThread);
|
|
155
|
+
const avatar = el('span', 'harness-mix-team-avatar', `position:relative;display:grid;place-items:center;width:40px;height:40px;flex:none;border-radius:11px;border:2px solid ${color};background:Canvas`);
|
|
156
|
+
avatar.dataset.status = member.display_status ?? '';
|
|
157
|
+
const dot = el('span', 'harness-mix-team-status-dot', `position:absolute;right:-4px;bottom:-4px;width:11px;height:11px;box-sizing:border-box;border-radius:50%;background:${stateColor(member.display_status)};border:2.5px solid Canvas`); dot.dataset.status = member.display_status ?? '';
|
|
158
|
+
avatar.append(collaborationIcon(member.agent, member.name, 30), dot);
|
|
130
159
|
const copy = el('span', undefined, 'display:grid;min-width:0;line-height:1.25;gap:2px');
|
|
131
160
|
copy.append(text('strong', member.name, 'font-size:12px;font-weight:680;overflow:hidden;text-overflow:ellipsis;white-space:nowrap'));
|
|
132
161
|
const status = el('span', undefined, 'display:flex;align-items:center;gap:5px;min-width:0');
|
|
@@ -138,6 +167,7 @@ function renderBoard(payload: TeamCardPayload, openThread?: TeamCardOptions['ope
|
|
|
138
167
|
const laneOf = (member: TeamMemberPayload, index: number) => {
|
|
139
168
|
const color = memberColor(index), assigned = tasksOf(member.id), done = assigned.filter(task => task.status === 'completed').length;
|
|
140
169
|
const lane = el('div', 'harness-mix-team-lane'); lane.dataset.memberId = member.id;
|
|
170
|
+
lane.dataset.status = member.display_status ?? ''; lane.style.setProperty('--lane-color', color ?? '');
|
|
141
171
|
lane.append(memberCard(member, index));
|
|
142
172
|
const column = el('div', undefined, `margin-top:9px;flex:1;min-height:0;display:flex;flex-direction:column;border-radius:12px;border:1px solid color-mix(in srgb,${color} 30%,transparent);background:color-mix(in srgb,${color} 7%,transparent)`);
|
|
143
173
|
const columnHead = el('div', undefined, 'display:flex;align-items:flex-start;gap:6px;padding:8px 10px 5px');
|
|
@@ -168,10 +198,24 @@ function renderBoard(payload: TeamCardPayload, openThread?: TeamCardOptions['ope
|
|
|
168
198
|
left.append(org, lanes);
|
|
169
199
|
|
|
170
200
|
const feed = el('aside', 'harness-mix-team-feed');
|
|
201
|
+
// 团队动态 = 成员消息 + 团队历史事件(任务分配/开始执行/会话就绪/结算等),
|
|
202
|
+
// 按时间倒序合并;message_sent 事件已由消息条目承载,避免重复。
|
|
203
|
+
const events = snapshots.filter(snapshot => snapshot.action !== 'message_sent');
|
|
204
|
+
const entries = [
|
|
205
|
+
...payload.messages.map(message => ({ at: message.at || 0, kind: 'message' as const, message })),
|
|
206
|
+
...events.map(snapshot => ({ at: snapshot.at || 0, kind: 'event' as const, snapshot })),
|
|
207
|
+
].sort((a, b) => b.at - a.at);
|
|
171
208
|
const feedHead = el('div', undefined, 'display:flex;align-items:center;gap:6px;padding:11px 12px 8px;flex:none');
|
|
172
|
-
feedHead.append(text('span', '团队动态', 'font-size:10px;font-weight:720;letter-spacing:.06em;opacity:.55'), text('span', String(
|
|
209
|
+
feedHead.append(text('span', '团队动态', 'font-size:10px;font-weight:720;letter-spacing:.06em;opacity:.55'), text('span', String(entries.length), 'font-size:8.5px;font-weight:700;opacity:.6;background:color-mix(in srgb,currentColor 8%,transparent);border-radius:99px;padding:1px 7px'));
|
|
173
210
|
const feedList = el('div', 'harness-mix-team-feed-list');
|
|
174
|
-
for (const
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
if (entry.kind === 'event') {
|
|
213
|
+
const item = el('article', undefined, 'display:flex;align-items:center;gap:6px;padding:7px 0;border-top:1px solid color-mix(in srgb,currentColor 7%,transparent)');
|
|
214
|
+
item.append(text('span', actionIcon(entry.snapshot.action), 'font-size:11px;flex:none'), text('span', actionLabel(entry.snapshot.action), 'font-size:10px;font-weight:650;opacity:.7;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap'));
|
|
215
|
+
if (entry.snapshot.at > 0) item.append(text('span', new Date(entry.snapshot.at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), 'font-size:8.5px;opacity:.4;flex:none'));
|
|
216
|
+
feedList.append(item); continue;
|
|
217
|
+
}
|
|
218
|
+
const message = entry.message;
|
|
175
219
|
const item = el('article', undefined, 'padding:9px 0;border-top:1px solid color-mix(in srgb,currentColor 7%,transparent)');
|
|
176
220
|
const sender = message.from === 'lead' ? lead : payload.members.find(member => member.id === message.from), receiver = message.to === 'lead' ? lead : payload.members.find(member => member.id === message.to);
|
|
177
221
|
const route = el('div', undefined, 'display:flex;align-items:center;gap:5px;min-width:0;font-size:10.5px;font-weight:700');
|
|
@@ -182,12 +226,12 @@ function renderBoard(payload: TeamCardPayload, openThread?: TeamCardOptions['ope
|
|
|
182
226
|
if (message.at > 0) route.append(text('span', new Date(message.at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), 'margin-left:auto;font-size:8.5px;font-weight:400;opacity:.4;flex:none'));
|
|
183
227
|
item.append(route, text('div', message.body, 'font-size:11px;line-height:1.5;margin-top:4px;white-space:pre-wrap;overflow-wrap:anywhere;opacity:.88')); feedList.append(item);
|
|
184
228
|
}
|
|
185
|
-
if (!
|
|
229
|
+
if (!entries.length) feedList.append(text('div', '成员交接、审查请求和结果会实时显示在这里。', 'font-size:10px;line-height:1.5;opacity:.45;padding:10px 0'));
|
|
186
230
|
feed.append(feedHead, feedList);
|
|
187
231
|
board.append(left, feed); return board;
|
|
188
232
|
}
|
|
189
233
|
|
|
190
|
-
function renderSummary(payload: TeamCardPayload, open: () => void) {
|
|
234
|
+
function renderSummary(payload: TeamCardPayload, open: () => void, openThread?: TeamCardOptions['openThread']) {
|
|
191
235
|
const panel = el('section', 'harness-mix-team-panel', 'box-sizing:border-box;border:1px solid color-mix(in srgb,currentColor 12%,transparent);border-radius:14px;background:color-mix(in srgb,Canvas 90%,transparent);box-shadow:0 8px 28px color-mix(in srgb,#000 7%,transparent);backdrop-filter:blur(18px);color:inherit;font:13px/1.4 system-ui,-apple-system,"Segoe UI",sans-serif;padding:12px 14px;display:grid;gap:10px;min-width:0'); panel.dataset.teamId = payload.team_id; panel.dataset.teamStatus = payload.status;
|
|
192
236
|
const top = el('div', undefined, 'display:flex;align-items:center;gap:12px;min-width:0');
|
|
193
237
|
const identity = el('div', undefined, 'display:grid;grid-template-columns:38px minmax(0,1fr);align-items:center;gap:10px;min-width:0;flex:1');
|
|
@@ -202,8 +246,13 @@ function renderSummary(payload: TeamCardPayload, open: () => void) {
|
|
|
202
246
|
payload.members.forEach((member, index) => {
|
|
203
247
|
const color = memberColor(index);
|
|
204
248
|
const chip = el('span', undefined, 'display:grid;grid-template-columns:26px minmax(0,1fr);align-items:center;gap:7px;min-width:128px;max-width:190px;padding:5px 9px;border-radius:10px;border:1px solid color-mix(in srgb,currentColor 8%,transparent);background:color-mix(in srgb,currentColor 3%,transparent)'); chip.title = `${member.name} · ${member.role}`;
|
|
205
|
-
|
|
206
|
-
|
|
249
|
+
chip.dataset.status = member.display_status ?? ''; chip.style.setProperty('--lane-color', color ?? '');
|
|
250
|
+
wireOpen(chip, member.childId ?? member.child_thread_id, member.name, openThread);
|
|
251
|
+
if (chip.classList.contains('harness-mix-team-clickable')) chip.title = `${member.name} · ${member.role} · 点击打开原生会话`;
|
|
252
|
+
const icon = el('span', 'harness-mix-team-avatar', `position:relative;display:grid;place-items:center;width:26px;height:26px;flex:none;border-radius:8px;border:1.5px solid ${color};background:Canvas`);
|
|
253
|
+
icon.dataset.status = member.display_status ?? '';
|
|
254
|
+
const dot = el('span', 'harness-mix-team-status-dot', `position:absolute;right:-3px;bottom:-3px;width:8px;height:8px;box-sizing:border-box;border-radius:50%;background:${stateColor(member.display_status)};border:2px solid Canvas`); dot.dataset.status = member.display_status ?? '';
|
|
255
|
+
icon.append(collaborationIcon(member.agent, member.name, 18), dot);
|
|
207
256
|
const label = el('span', undefined, 'display:grid;min-width:0;line-height:1.2');
|
|
208
257
|
label.append(text('strong', member.name, 'font-size:10px;font-weight:650;overflow:hidden;text-overflow:ellipsis;white-space:nowrap'), text('span', member.role, 'font-size:8.5px;opacity:.52;overflow:hidden;text-overflow:ellipsis;white-space:nowrap'), text('span', stateLabel(member.display_status), `font-size:8.5px;color:${stateColor(member.display_status)}`));
|
|
209
258
|
chip.append(icon, label); members.append(chip);
|
|
@@ -246,10 +295,16 @@ export function installTeamCards(options: TeamCardOptions = {}) {
|
|
|
246
295
|
const play = el('button', undefined, 'border:1px solid color-mix(in srgb,currentColor 15%,transparent);border-radius:7px;background:color-mix(in srgb,currentColor 6%,transparent);color:inherit;font:600 10px system-ui;padding:5px 8px;cursor:pointer'); play.type = 'button'; play.textContent = '回放';
|
|
247
296
|
const collapse = el('button', 'harness-mix-team-back', 'border:0;background:transparent;color:inherit;font:650 10px system-ui;cursor:pointer;padding:6px 7px;opacity:.68'); collapse.type = 'button'; collapse.textContent = '收起详情'; collapse.addEventListener('click', close); timeline.append(event, range, live, play, collapse); header.append(title, statsWrap, progressWrap, timeline);
|
|
248
297
|
const content = el('div', undefined, 'min-height:0'); workbench.append(header, content); source.after(workbench);
|
|
298
|
+
let renderedSignature = '';
|
|
249
299
|
const render = () => {
|
|
250
300
|
const snapshot = index >= 0 ? snapshots[index] : undefined;
|
|
251
301
|
const visible = snapshot?.team ?? current;
|
|
252
|
-
|
|
302
|
+
const feedSnapshots = index >= 0 ? snapshots.slice(0, index + 1) : snapshots;
|
|
303
|
+
// 1.5s 轮询下无变化就跳过重渲染,避免面板闪烁、feed/任务列表滚动位置丢失
|
|
304
|
+
const signature = JSON.stringify([visible.updated_at, visible.status, visible.lead?.display_status, visible.members.map(member => member.display_status), visible.tasks.map(task => `${task.id}:${task.status}`), visible.messages.length, snapshots.length, index]);
|
|
305
|
+
if (signature === renderedSignature) return;
|
|
306
|
+
renderedSignature = signature;
|
|
307
|
+
content.replaceChildren(renderBoard(visible, async threadId => { close(); await options.openThread?.(threadId); }, feedSnapshots));
|
|
253
308
|
teamName.textContent = visible.name; subtitle.textContent = visible.goal; subtitle.title = visible.goal;
|
|
254
309
|
teamPill.textContent = stateLabel(visible.status); teamPill.style.cssText = pillCss(visible.status);
|
|
255
310
|
statsWrap.replaceChildren(...headerStats(visible));
|
|
@@ -267,7 +322,7 @@ export function installTeamCards(options: TeamCardOptions = {}) {
|
|
|
267
322
|
const scan = () => {
|
|
268
323
|
if (disposed) return;
|
|
269
324
|
const selector = '[data-testid*="tool"], [data-turn-key], [data-local-conversation-item-target-ids], pre[data-testid*="tool"]';
|
|
270
|
-
for (const candidate of document.querySelectorAll<HTMLElement>(selector)) { if (candidate.closest('.harness-mix-team-panel,.harness-mix-team-workbench')) continue; const payload = parseTeamPayload(candidate.textContent ?? ''); if (!payload) continue; if ([...candidate.querySelectorAll<HTMLElement>(selector)].some(child => parseTeamPayload(child.textContent ?? ''))) continue; const signature = `${payload.updated_at ?? 0}:${payload.tasks.length}:${payload.messages.length}`; if (signatures.get(candidate) === signature) continue; const panel = renderSummary(payload, () => open(payload)); if (candidate.tagName === 'PRE') { if (!candidate.dataset.harnessMixTeamDisplay) candidate.dataset.harnessMixTeamDisplay = candidate.style.display || '__empty__'; candidate.style.display = 'none'; if (candidate.nextElementSibling?.classList.contains('harness-mix-team-panel')) candidate.nextElementSibling.remove(); candidate.after(panel); } else { candidate.querySelector(':scope > .harness-mix-team-panel')?.remove(); candidate.append(panel); } signatures.set(candidate, signature); }
|
|
325
|
+
for (const candidate of document.querySelectorAll<HTMLElement>(selector)) { if (candidate.closest('.harness-mix-team-panel,.harness-mix-team-workbench')) continue; const payload = parseTeamPayload(candidate.textContent ?? ''); if (!payload) continue; if ([...candidate.querySelectorAll<HTMLElement>(selector)].some(child => parseTeamPayload(child.textContent ?? ''))) continue; const signature = `${payload.updated_at ?? 0}:${payload.tasks.length}:${payload.messages.length}:${payload.members.map(member => member.display_status).join(',')}`; if (signatures.get(candidate) === signature) continue; const panel = renderSummary(payload, () => open(payload), options.openThread); if (candidate.tagName === 'PRE') { if (!candidate.dataset.harnessMixTeamDisplay) candidate.dataset.harnessMixTeamDisplay = candidate.style.display || '__empty__'; candidate.style.display = 'none'; if (candidate.nextElementSibling?.classList.contains('harness-mix-team-panel')) candidate.nextElementSibling.remove(); candidate.after(panel); } else { candidate.querySelector(':scope > .harness-mix-team-panel')?.remove(); candidate.append(panel); } signatures.set(candidate, signature); }
|
|
271
326
|
};
|
|
272
327
|
const removeActivePanel = () => { if (workbench?.dataset.teamSource === 'active-thread') close(); activePanel?.remove(); activePanel = null; activeSignature = ''; activeThreadId = ''; };
|
|
273
328
|
const refreshActiveTeam = async () => {
|
|
@@ -282,14 +337,14 @@ export function installTeamCards(options: TeamCardOptions = {}) {
|
|
|
282
337
|
if (!latest || latest.threadId !== context.threadId || !latest.anchor.isConnected) { removeActivePanel(); return; }
|
|
283
338
|
if (!result) { removeActivePanel(); return; }
|
|
284
339
|
const payload = result.team;
|
|
285
|
-
const signature = `${context.threadId}:${payload.team_id}:${payload.updated_at ?? 0}:${payload.tasks.length}:${payload.messages.length}`;
|
|
340
|
+
const signature = `${context.threadId}:${payload.team_id}:${payload.updated_at ?? 0}:${payload.tasks.length}:${payload.messages.length}:${payload.lead?.display_status}:${payload.members.map(member => member.display_status).join(',')}:${payload.tasks.map(task => task.status).join(',')}`;
|
|
286
341
|
const inlinePanel = [...document.querySelectorAll<HTMLElement>('.harness-mix-team-panel')]
|
|
287
342
|
.find(node => node !== activePanel && node.dataset.teamId === payload.team_id);
|
|
288
343
|
if (inlinePanel) { removeActivePanel(); return; }
|
|
289
344
|
if (activePanel?.isConnected && activeSignature === signature && activeThreadId === context.threadId) return;
|
|
290
345
|
const reopen = workbench?.dataset.teamId === payload.team_id && workbench.dataset.teamSource === 'active-thread';
|
|
291
346
|
removeActivePanel();
|
|
292
|
-
activePanel = renderSummary(payload, () => open(payload));
|
|
347
|
+
activePanel = renderSummary(payload, () => open(payload), options.openThread);
|
|
293
348
|
activePanel.classList.add('harness-mix-team-launcher');
|
|
294
349
|
activePanel.dataset.teamSource = 'active-thread';
|
|
295
350
|
activePanel.style.cssText += ';margin:8px 16px 0;flex:none;position:relative;z-index:11';
|