@harness-mix/cli 0.2.4 → 0.3.0
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 +11 -0
- package/README.md +4 -4
- package/docs/harness-management.md +1 -1
- package/docs/multi-agent-collaboration.md +3 -1
- package/docs/native-acp.md +138 -127
- package/output/native-build/renderer-extension.js +149 -19
- package/package.json +4 -2
- package/scripts/acp-image-test.cjs +69 -0
- package/scripts/adapters-test.cjs +25 -0
- package/scripts/codex-accounts-test.cjs +34 -0
- package/scripts/codex-adapter-test.cjs +77 -1
- package/scripts/collaboration-test.cjs +304 -1
- package/scripts/collaboration-ui-smoke.cjs +25 -3
- package/scripts/e2e-delegate.cjs +1 -1
- package/scripts/e2e-hermes-image.cjs +60 -0
- package/scripts/openclaw-adapter-test.cjs +121 -3
- package/scripts/openclaw-mcp-probe.cjs +100 -0
- package/scripts/openclaw-mcp-tool-probe.cjs +55 -0
- package/scripts/openclaw-thinking-probe.cjs +73 -0
- package/scripts/storage-verification-test.cjs +11 -0
- package/src/main/adapters/claude.js +15 -7
- package/src/main/adapters/codex-app-server.js +29 -11
- package/src/main/adapters/codex.js +39 -5
- package/src/main/adapters/omp.js +7 -2
- package/src/main/adapters/openclaw.js +534 -343
- package/src/main/adapters/pi-family.js +35 -8
- package/src/main/adapters/zcode.js +12 -8
- package/src/main/host/collaboration-tools.js +1 -1
- package/src/main/host/collaboration.js +285 -36
- package/src/main/host/runtime.js +9 -3
- package/src/main/host/verification-gates.js +14 -2
- package/src/main/native/codex-accounts.js +15 -4
- package/src/main/native/protocol.js +6 -0
- package/src/native-ui/desktop-control/dist/renderer-cdp-control-session.js +3 -1
- package/src/native-ui/desktop-control/dist/renderer-cdp-control-session.js.map +1 -1
- package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
- 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-collab-cards.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-collab-cards.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +29 -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-team-cards.d.ts +4 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.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 +20 -7
- package/src/native-ui/renderer-extension/src/renderer-collab-cards.ts +25 -0
- package/src/native-ui/renderer-extension/src/renderer-model-client.ts +27 -0
- package/src/native-ui/renderer-extension/src/renderer-team-cards.ts +93 -13
- package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +47 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { collaborationIcon } from './collaboration-icon.js';
|
|
2
|
+
import type { CollaborationUserActionInput } from './renderer-model-client.js';
|
|
2
3
|
|
|
3
|
-
export interface TeamMemberPayload { id: string; name: string; role: string; agent: string; childId?: string; child_thread_id?: string; display_status?: string }
|
|
4
|
+
export interface TeamMemberPayload { id: string; name: string; role: string; agent: string; childId?: string; child_thread_id?: string; display_status?: string; unread?: number }
|
|
4
5
|
export interface TeamTaskPayload { id: string; title: string; assignee: string; dependsOn?: string[]; status: string }
|
|
5
6
|
export interface TeamMessagePayload { id: string; fromName?: string; from: string; to: string; body: string; at: number }
|
|
6
7
|
export interface TeamCardPayload {
|
|
@@ -10,10 +11,12 @@ export interface TeamCardPayload {
|
|
|
10
11
|
interface TeamSnapshot { id: string; action: string; at: number; team: TeamCardPayload }
|
|
11
12
|
interface TeamInspection { team: TeamCardPayload; snapshots: TeamSnapshot[] }
|
|
12
13
|
interface ActiveTeamContext { threadId: string; anchor: Element }
|
|
14
|
+
export type TeamUserAction = (input: CollaborationUserActionInput) => Promise<unknown>;
|
|
13
15
|
interface TeamCardOptions {
|
|
14
16
|
inspectTeam?: (threadId: string, teamId?: string) => Promise<unknown>;
|
|
15
17
|
openThread?: (threadId: string) => Promise<unknown> | unknown;
|
|
16
18
|
activeThread?: () => ActiveTeamContext | null;
|
|
19
|
+
userAction?: TeamUserAction;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
function jsonObjectAt(value: string, start: number): string | null {
|
|
@@ -46,7 +49,7 @@ function parseInspection(value: unknown): TeamInspection | null {
|
|
|
46
49
|
}
|
|
47
50
|
const stateColor = (status?: string) => ({ ready: '#8b8b8b', pending: '#8b8b8b', working: '#2878e3', in_progress: '#2878e3', active: '#2878e3', completed: '#1f9d68', blocked: '#c17022', failed: '#d14343', interrupted: '#c17022' }[status ?? ''] ?? '#8b8b8b');
|
|
48
51
|
const stateLabel = (status?: string) => ({ ready: '就绪', pending: '待开始', working: '工作中', in_progress: '进行中', completed: '已完成', blocked: '等待依赖', failed: '失败', interrupted: '已中断', active: '协作中' }[status ?? ''] ?? status ?? '未知');
|
|
49
|
-
const actionLabel = (action?: string) => ({ team_created: '团队建立', task_assigned: '任务分配', task_updated: '任务更新', message_sent: '团队通信', task_started: '开始执行', member_session_ready: '会话就绪', task_settled: '任务结算', task_failed: '任务失败', task_cancelled: '任务取消', task_interrupted: '任务中断', task_resumed: '恢复执行', task_followup: '继续执行' }[action ?? ''] ?? action ?? '实时状态');
|
|
52
|
+
const actionLabel = (action?: string) => ({ team_created: '团队建立', task_assigned: '任务分配', task_updated: '任务更新', message_sent: '团队通信', task_started: '开始执行', member_session_ready: '会话就绪', task_settled: '任务结算', task_failed: '任务失败', task_cancelled: '任务取消', task_interrupted: '任务中断', task_resumed: '恢复执行', task_followup: '继续执行', task_reassigned: '任务改派', task_retry: '自动重试' }[action ?? ''] ?? action ?? '实时状态');
|
|
50
53
|
const memberPalette = ['#3b82f6', '#f59e0b', '#10b981', '#8b5cf6', '#f97316', '#ec4899', '#14b8a6', '#6366f1'];
|
|
51
54
|
const memberColor = (index: number) => memberPalette[index % memberPalette.length];
|
|
52
55
|
function el<K extends keyof HTMLElementTagNameMap>(tag: K, className?: string, css?: string): HTMLElementTagNameMap[K] { const node = document.createElement(tag); if (className) node.className = className; if (css) node.style.cssText = css; return node; }
|
|
@@ -101,6 +104,31 @@ function wireOpen(node: HTMLElement, childId: string | undefined, name: string,
|
|
|
101
104
|
});
|
|
102
105
|
}
|
|
103
106
|
|
|
107
|
+
// 看板操作按钮:失败时红框闪烁并保留错误 title(与 wireOpen 同一失败可视语义)。
|
|
108
|
+
function actionButton(label: string, title: string, onClick: () => void, tone?: string) {
|
|
109
|
+
const node = el('button', 'harness-mix-team-action', `border:1px solid color-mix(in srgb,${tone ?? 'currentColor'} 26%,transparent);border-radius:6px;background:color-mix(in srgb,${tone ?? 'currentColor'} 7%,transparent);color:${tone ?? 'inherit'};font:650 9px system-ui;padding:3px 8px;cursor:pointer;white-space:nowrap${tone ? '' : ';opacity:.85'}`);
|
|
110
|
+
node.type = 'button'; node.textContent = label; node.title = title;
|
|
111
|
+
node.addEventListener('click', event => { event.stopPropagation(); event.preventDefault(); onClick(); });
|
|
112
|
+
return node;
|
|
113
|
+
}
|
|
114
|
+
async function runUserAction(userAction: TeamUserAction, node: HTMLElement, input: CollaborationUserActionInput) {
|
|
115
|
+
node.classList.remove('harness-mix-team-open-failed');
|
|
116
|
+
try { await userAction(input); } catch (error) {
|
|
117
|
+
console.warn('[TeamCards] 看板操作失败:', error);
|
|
118
|
+
node.classList.add('harness-mix-team-open-failed');
|
|
119
|
+
node.title = `操作失败:${(error as Error)?.message ?? String(error)}`;
|
|
120
|
+
setTimeout(() => node.classList.remove('harness-mix-team-open-failed'), 1800);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function continueButton(payload: TeamCardPayload, userAction?: TeamUserAction) {
|
|
124
|
+
const busy = payload.lead?.display_status === 'working';
|
|
125
|
+
const node = actionButton('继续协作', busy ? '主导者回合进行中,结束后再继续' : '把中断的委派恢复为新的主导者回合', () => {
|
|
126
|
+
if (payload.lead_thread_id) void runUserAction(userAction!, node, { action: 'continue', threadId: payload.lead_thread_id! });
|
|
127
|
+
});
|
|
128
|
+
if (busy) { node.disabled = true; node.style.opacity = '.45'; node.style.cursor = 'not-allowed'; }
|
|
129
|
+
return node;
|
|
130
|
+
}
|
|
131
|
+
|
|
104
132
|
function metrics(payload: TeamCardPayload) {
|
|
105
133
|
const node = el('div', 'harness-mix-team-metrics', 'display:flex;gap:18px;align-items:center');
|
|
106
134
|
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']];
|
|
@@ -127,9 +155,9 @@ function headerStats(payload: TeamCardPayload) {
|
|
|
127
155
|
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)`; };
|
|
128
156
|
function statusPill(status?: string) { return text('span', stateLabel(status), pillCss(status)); }
|
|
129
157
|
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 ?? ''] ?? '•');
|
|
158
|
+
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: '💬', task_reassigned: '🔁', task_retry: '♻️' }[action ?? ''] ?? '•');
|
|
131
159
|
|
|
132
|
-
function renderBoard(payload: TeamCardPayload, openThread?: TeamCardOptions['openThread'], snapshots: TeamSnapshot[] = []) {
|
|
160
|
+
function renderBoard(payload: TeamCardPayload, openThread?: TeamCardOptions['openThread'], snapshots: TeamSnapshot[] = [], userAction?: TeamUserAction) {
|
|
133
161
|
const board = el('main', 'harness-mix-team-board harness-mix-team-body', 'height:100%;min-height:0;color:inherit');
|
|
134
162
|
const lead = payload.lead ?? { id: 'lead', name: 'Team Lead', role: '协调与验收', agent: 'codex', display_status: 'working' };
|
|
135
163
|
const tasksOf = (memberId: string) => payload.tasks.filter(task => task.assignee === memberId);
|
|
@@ -147,11 +175,12 @@ function renderBoard(payload: TeamCardPayload, openThread?: TeamCardOptions['ope
|
|
|
147
175
|
status.append(statusDot(lead.display_status), text('span', stateLabel(lead.display_status), `font-size:9px;color:${stateColor(lead.display_status)}`));
|
|
148
176
|
copy.append(status); button.append(avatar, copy); return button;
|
|
149
177
|
};
|
|
178
|
+
// div 而非 button:成员卡内嵌「追问」操作按钮,交互元素不可嵌套
|
|
150
179
|
const memberCard = (member: TeamMemberPayload, index: number) => {
|
|
151
180
|
const color = memberColor(index), assigned = tasksOf(member.id), done = assigned.filter(task => task.status === 'completed').length;
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
wireOpen(
|
|
181
|
+
const card = el('div', 'harness-mix-team-member', 'position:relative;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'); card.dataset.agent = member.agent;
|
|
182
|
+
card.dataset.status = member.display_status ?? ''; card.style.setProperty('--lane-color', color ?? '');
|
|
183
|
+
wireOpen(card, member.childId ?? member.child_thread_id, member.name, openThread);
|
|
155
184
|
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
185
|
avatar.dataset.status = member.display_status ?? '';
|
|
157
186
|
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 ?? '';
|
|
@@ -160,9 +189,26 @@ function renderBoard(payload: TeamCardPayload, openThread?: TeamCardOptions['ope
|
|
|
160
189
|
copy.append(text('strong', member.name, 'font-size:12px;font-weight:680;overflow:hidden;text-overflow:ellipsis;white-space:nowrap'));
|
|
161
190
|
const status = el('span', undefined, 'display:flex;align-items:center;gap:5px;min-width:0');
|
|
162
191
|
status.append(text('span', stateLabel(member.display_status), `font-size:9px;color:${stateColor(member.display_status)};white-space:nowrap`), text('span', `${done}/${assigned.length}`, 'font-size:9px;font-weight:650;opacity:.5;margin-left:auto'));
|
|
192
|
+
if (member.unread) status.append(text('span', `💬${member.unread}`, 'font-size:8.5px;font-weight:700;color:#2878e3;background:color-mix(in srgb,#2878e3 12%,transparent);border-radius:99px;padding:1px 6px;flex:none'));
|
|
163
193
|
copy.append(status);
|
|
164
194
|
const progress = el('progress'); progress.max = Math.max(1, assigned.length); progress.value = done; progress.style.cssText = `width:100%;height:4px;margin:0;accent-color:${color}`; progress.title = `${done}/${assigned.length} 个任务完成`;
|
|
165
|
-
copy.append(progress);
|
|
195
|
+
copy.append(progress); card.append(avatar, copy);
|
|
196
|
+
if (userAction && payload.lead_thread_id) {
|
|
197
|
+
const askRow = el('span', undefined, 'position:absolute;top:5px;right:6px;display:flex;align-items:center;gap:4px;max-width:72%');
|
|
198
|
+
const askButton = actionButton('追问', `以主导者身份向 ${member.name} 发送团队消息`, () => {
|
|
199
|
+
const input = el('input', undefined, 'font:600 10px system-ui;padding:3px 6px;border-radius:6px;border:1px solid color-mix(in srgb,currentColor 22%,transparent);color:inherit;background:Canvas;min-width:96px;flex:1');
|
|
200
|
+
input.placeholder = `向 ${member.name} 说…`;
|
|
201
|
+
const send = actionButton('发送', '发送团队消息', () => {
|
|
202
|
+
const message = input.value.trim();
|
|
203
|
+
if (message) void runUserAction(userAction, send, { action: 'message/send', threadId: payload.lead_thread_id!, teamId: payload.team_id, to: member.id, message });
|
|
204
|
+
});
|
|
205
|
+
input.addEventListener('keydown', event => { if (event.key === 'Enter') send.click(); });
|
|
206
|
+
askRow.replaceChildren(input, send, actionButton('×', '取消', () => askRow.replaceChildren(askButton)));
|
|
207
|
+
input.focus();
|
|
208
|
+
});
|
|
209
|
+
askRow.append(askButton); card.append(askRow);
|
|
210
|
+
}
|
|
211
|
+
return card;
|
|
166
212
|
};
|
|
167
213
|
const laneOf = (member: TeamMemberPayload, index: number) => {
|
|
168
214
|
const color = memberColor(index), assigned = tasksOf(member.id), done = assigned.filter(task => task.status === 'completed').length;
|
|
@@ -183,6 +229,34 @@ function renderBoard(payload: TeamCardPayload, openThread?: TeamCardOptions['ope
|
|
|
183
229
|
const title = text('div', task.title, 'font-size:10.5px;font-weight:620;line-height:1.35'); title.className = 'harness-mix-team-clamp2'; title.title = task.title;
|
|
184
230
|
card.append(topRow, title);
|
|
185
231
|
if ((task.dependsOn?.length ?? 0) > 0) card.append(text('div', `依赖 ${task.dependsOn!.length} 项任务`, 'font-size:8px;opacity:.45'));
|
|
232
|
+
if (userAction && payload.lead_thread_id) {
|
|
233
|
+
const assignee = payload.members.find(member => member.id === task.assignee);
|
|
234
|
+
const row = el('div', undefined, 'display:flex;align-items:center;gap:5px;flex-wrap:wrap;min-width:0');
|
|
235
|
+
const renderDefault = () => {
|
|
236
|
+
row.replaceChildren();
|
|
237
|
+
if (task.status === 'in_progress') {
|
|
238
|
+
row.append(actionButton('取消', '取消该任务并停止成员会话', () => {
|
|
239
|
+
if (window.confirm(`取消任务「${task.title}」?运行中的成员会话将被停止。`)) void runUserAction(userAction, row, { action: 'task/cancel', threadId: payload.lead_thread_id!, teamId: payload.team_id, taskId: task.id });
|
|
240
|
+
}, '#d14343'));
|
|
241
|
+
} else if (task.status === 'failed' || task.status === 'interrupted') {
|
|
242
|
+
row.append(
|
|
243
|
+
actionButton(task.status === 'failed' ? '重试' : '恢复', `把任务重新派给 ${assignee?.name ?? '原成员'}`, () => {
|
|
244
|
+
void runUserAction(userAction, row, { action: 'task/reassign', threadId: payload.lead_thread_id!, teamId: payload.team_id, taskId: task.id, memberId: task.assignee });
|
|
245
|
+
}, '#c17022'),
|
|
246
|
+
actionButton('改派', '改派给其他成员', () => {
|
|
247
|
+
const select = el('select', undefined, 'font:600 9px system-ui;padding:3px 4px;border-radius:6px;border:1px solid color-mix(in srgb,currentColor 22%,transparent);color:inherit;background:Canvas;max-width:120px;min-width:0');
|
|
248
|
+
select.append(new Option('改派给…', ''));
|
|
249
|
+
for (const candidate of payload.members) if (candidate.id !== task.assignee) select.append(new Option(candidate.name, candidate.id));
|
|
250
|
+
const go = actionButton('确定', '执行改派', () => {
|
|
251
|
+
if (select.value) void runUserAction(userAction, row, { action: 'task/reassign', threadId: payload.lead_thread_id!, teamId: payload.team_id, taskId: task.id, memberId: select.value });
|
|
252
|
+
});
|
|
253
|
+
row.replaceChildren(select, go, actionButton('×', '取消改派', renderDefault));
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
renderDefault();
|
|
258
|
+
if (row.childElementCount) card.append(row);
|
|
259
|
+
}
|
|
186
260
|
list.append(card);
|
|
187
261
|
}
|
|
188
262
|
if (!assigned.length) list.append(text('div', '等待主导者分配任务', 'font-size:9px;opacity:.42;padding:10px 2px;text-align:center'));
|
|
@@ -231,7 +305,7 @@ function renderBoard(payload: TeamCardPayload, openThread?: TeamCardOptions['ope
|
|
|
231
305
|
board.append(left, feed); return board;
|
|
232
306
|
}
|
|
233
307
|
|
|
234
|
-
function renderSummary(payload: TeamCardPayload, open: () => void, openThread?: TeamCardOptions['openThread']) {
|
|
308
|
+
function renderSummary(payload: TeamCardPayload, open: () => void, openThread?: TeamCardOptions['openThread'], userAction?: TeamUserAction) {
|
|
235
309
|
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;
|
|
236
310
|
const top = el('div', undefined, 'display:flex;align-items:center;gap:12px;min-width:0');
|
|
237
311
|
const identity = el('div', undefined, 'display:grid;grid-template-columns:38px minmax(0,1fr);align-items:center;gap:10px;min-width:0;flex:1');
|
|
@@ -242,6 +316,7 @@ function renderSummary(payload: TeamCardPayload, open: () => void, openThread?:
|
|
|
242
316
|
const goal = text('span', payload.goal, 'font-size:10px;opacity:.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap'); goal.title = payload.goal;
|
|
243
317
|
copy.append(eyebrow, title, goal); identity.append(leadIcon, copy);
|
|
244
318
|
const button = el('button', 'harness-mix-team-open', 'border:1px solid color-mix(in srgb,currentColor 13%,transparent);border-radius:8px;background:color-mix(in srgb,currentColor 5%,transparent);color:inherit;font:650 11px system-ui;padding:7px 10px;cursor:pointer;white-space:nowrap'); button.type = 'button'; button.textContent = '展开详情'; button.addEventListener('click', open); top.append(identity, metrics(payload), button);
|
|
319
|
+
if (userAction && payload.lead_thread_id && payload.tasks.some(task => task.status === 'interrupted')) top.append(continueButton(payload, userAction));
|
|
245
320
|
const members = el('div', 'harness-mix-team-summary-members harness-mix-team-scroll', 'display:flex;align-items:center;gap:6px;min-width:0;overflow-x:auto;padding-bottom:1px');
|
|
246
321
|
payload.members.forEach((member, index) => {
|
|
247
322
|
const color = memberColor(index);
|
|
@@ -255,6 +330,7 @@ function renderSummary(payload: TeamCardPayload, open: () => void, openThread?:
|
|
|
255
330
|
icon.append(collaborationIcon(member.agent, member.name, 18), dot);
|
|
256
331
|
const label = el('span', undefined, 'display:grid;min-width:0;line-height:1.2');
|
|
257
332
|
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)}`));
|
|
333
|
+
if (member.unread) label.append(text('span', `💬 ${member.unread} 条未读`, 'font-size:8.5px;font-weight:700;color:#2878e3'));
|
|
258
334
|
chip.append(icon, label); members.append(chip);
|
|
259
335
|
});
|
|
260
336
|
const completed = payload.tasks.filter(task => task.status === 'completed').length, progress = el('progress'); progress.max = Math.max(1, payload.tasks.length); progress.value = completed; progress.style.cssText = 'width:72px;height:5px;accent-color:#1f9d68;margin-left:auto;flex:none'; progress.title = `${completed}/${payload.tasks.length} 个任务完成`; members.append(progress, text('span', `${completed}/${payload.tasks.length}`, 'font-size:9px;font-weight:700;opacity:.52;white-space:nowrap'));
|
|
@@ -293,7 +369,10 @@ export function installTeamCards(options: TeamCardOptions = {}) {
|
|
|
293
369
|
const timeline = el('div', 'harness-mix-team-timeline', 'display:flex;align-items:center;gap:6px;margin-left:auto;min-width:0'), event = text('span', '实时状态', 'font-size:9px;opacity:.55;min-width:60px;text-align:right'), range = el('input'); range.type = 'range'; range.min = '0'; range.max = '0'; range.value = '0'; range.style.cssText = 'width:min(120px,16vw);accent-color:#2878e3';
|
|
294
370
|
const live = el('button', undefined, 'border:1px solid color-mix(in srgb,currentColor 15%,transparent);border-radius:7px;background:transparent;color:inherit;font:600 10px system-ui;padding:5px 7px;cursor:pointer'); live.type = 'button'; live.textContent = '实时';
|
|
295
371
|
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 = '回放';
|
|
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);
|
|
372
|
+
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);
|
|
373
|
+
// 用户操作位(继续协作等):随 render() 按 visible 状态重建,避免静态按钮过期
|
|
374
|
+
const headerActions = el('div', undefined, 'display:flex;align-items:center;gap:6px;flex:none');
|
|
375
|
+
header.append(title, statsWrap, progressWrap, timeline, headerActions);
|
|
297
376
|
const content = el('div', undefined, 'min-height:0'); workbench.append(header, content); source.after(workbench);
|
|
298
377
|
let renderedSignature = '';
|
|
299
378
|
const render = () => {
|
|
@@ -304,10 +383,11 @@ export function installTeamCards(options: TeamCardOptions = {}) {
|
|
|
304
383
|
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
384
|
if (signature === renderedSignature) return;
|
|
306
385
|
renderedSignature = signature;
|
|
307
|
-
content.replaceChildren(renderBoard(visible, async threadId => { close(); await options.openThread?.(threadId); }, feedSnapshots));
|
|
386
|
+
content.replaceChildren(renderBoard(visible, async threadId => { close(); await options.openThread?.(threadId); }, feedSnapshots, options.userAction));
|
|
308
387
|
teamName.textContent = visible.name; subtitle.textContent = visible.goal; subtitle.title = visible.goal;
|
|
309
388
|
teamPill.textContent = stateLabel(visible.status); teamPill.style.cssText = pillCss(visible.status);
|
|
310
389
|
statsWrap.replaceChildren(...headerStats(visible));
|
|
390
|
+
headerActions.replaceChildren(...(options.userAction && visible.tasks.some(task => task.status === 'interrupted') ? [continueButton(visible, options.userAction)] : []));
|
|
311
391
|
const completion = completionOf(visible); progressBar.max = Math.max(1, completion.total); progressBar.value = completion.done; progressPct.textContent = `${completion.pct}%`;
|
|
312
392
|
range.max = String(Math.max(0, snapshots.length - 1));
|
|
313
393
|
range.value = String(index >= 0 ? index : Math.max(0, snapshots.length - 1));
|
|
@@ -322,7 +402,7 @@ export function installTeamCards(options: TeamCardOptions = {}) {
|
|
|
322
402
|
const scan = () => {
|
|
323
403
|
if (disposed) return;
|
|
324
404
|
const selector = '[data-testid*="tool"], [data-turn-key], [data-local-conversation-item-target-ids], pre[data-testid*="tool"]';
|
|
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); }
|
|
405
|
+
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, options.userAction); 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); }
|
|
326
406
|
};
|
|
327
407
|
const removeActivePanel = () => { if (workbench?.dataset.teamSource === 'active-thread') close(); activePanel?.remove(); activePanel = null; activeSignature = ''; activeThreadId = ''; };
|
|
328
408
|
const refreshActiveTeam = async () => {
|
|
@@ -344,7 +424,7 @@ export function installTeamCards(options: TeamCardOptions = {}) {
|
|
|
344
424
|
if (activePanel?.isConnected && activeSignature === signature && activeThreadId === context.threadId) return;
|
|
345
425
|
const reopen = workbench?.dataset.teamId === payload.team_id && workbench.dataset.teamSource === 'active-thread';
|
|
346
426
|
removeActivePanel();
|
|
347
|
-
activePanel = renderSummary(payload, () => open(payload), options.openThread);
|
|
427
|
+
activePanel = renderSummary(payload, () => open(payload), options.openThread, options.userAction);
|
|
348
428
|
activePanel.classList.add('harness-mix-team-launcher');
|
|
349
429
|
activePanel.dataset.teamSource = 'active-thread';
|
|
350
430
|
activePanel.style.cssText += ';margin:8px 16px 0;flex:none;position:relative;z-index:11';
|
|
@@ -28,6 +28,9 @@ import {
|
|
|
28
28
|
THREAD_PERMISSION_MODE_SELECT_METHOD,
|
|
29
29
|
THREAD_THINKING_SELECT_METHOD,
|
|
30
30
|
THREAD_OWNERSHIP_LIST_METHOD,
|
|
31
|
+
THREAD_TEAM_TASK_CANCEL_METHOD,
|
|
32
|
+
THREAD_TEAM_TASK_REASSIGN_METHOD,
|
|
33
|
+
THREAD_COLLABORATION_CONTINUE_METHOD,
|
|
31
34
|
THREAD_TOKEN_USAGE_UPDATED_METHOD,
|
|
32
35
|
THREAD_USAGE_INSPECT_METHOD,
|
|
33
36
|
THREAD_USAGE_UPDATED_METHOD,
|
|
@@ -278,7 +281,10 @@ describe("Renderer fixed Model request client", () => {
|
|
|
278
281
|
},
|
|
279
282
|
})
|
|
280
283
|
.mockResolvedValueOnce({ status: null })
|
|
281
|
-
.mockResolvedValueOnce({ threadId: "thread-1", checkpointId: "handoff-1" })
|
|
284
|
+
.mockResolvedValueOnce({ threadId: "thread-1", checkpointId: "handoff-1" })
|
|
285
|
+
.mockResolvedValueOnce({ dispatched: true })
|
|
286
|
+
.mockResolvedValueOnce({ dispatched: true })
|
|
287
|
+
.mockResolvedValueOnce({ dispatched: true });
|
|
282
288
|
const client = createRendererModelClient([{ addNotificationCallback, sendRequest }]);
|
|
283
289
|
if (!client) throw new Error("Synthetic Model client was not created");
|
|
284
290
|
expect(Object.keys(client).sort()).toEqual([
|
|
@@ -286,6 +292,7 @@ describe("Renderer fixed Model request client", () => {
|
|
|
286
292
|
"cancelCodexAccountLogin",
|
|
287
293
|
"changeSkill",
|
|
288
294
|
"checkUpdate",
|
|
295
|
+
"collaborationUserAction",
|
|
289
296
|
"consumeCodexAccountResetCredit",
|
|
290
297
|
"createCodexAccount",
|
|
291
298
|
"delegateThread",
|
|
@@ -463,6 +470,45 @@ describe("Renderer fixed Model request client", () => {
|
|
|
463
470
|
harnessId: "claude-code",
|
|
464
471
|
note: "继续补测试",
|
|
465
472
|
});
|
|
473
|
+
await expect(
|
|
474
|
+
client.collaborationUserAction?.({
|
|
475
|
+
action: "task/cancel",
|
|
476
|
+
threadId: hostThreadIdSchema.parse("thread-1"),
|
|
477
|
+
teamId: "team-1",
|
|
478
|
+
taskId: "task-1",
|
|
479
|
+
}),
|
|
480
|
+
).resolves.toEqual({ dispatched: true });
|
|
481
|
+
await expect(
|
|
482
|
+
client.collaborationUserAction?.({
|
|
483
|
+
action: "task/reassign",
|
|
484
|
+
threadId: hostThreadIdSchema.parse("thread-1"),
|
|
485
|
+
teamId: "team-1",
|
|
486
|
+
taskId: "task-1",
|
|
487
|
+
memberId: "member-2",
|
|
488
|
+
note: "换人重做",
|
|
489
|
+
}),
|
|
490
|
+
).resolves.toEqual({ dispatched: true });
|
|
491
|
+
await expect(
|
|
492
|
+
client.collaborationUserAction?.({
|
|
493
|
+
action: "continue",
|
|
494
|
+
threadId: hostThreadIdSchema.parse("thread-1"),
|
|
495
|
+
}),
|
|
496
|
+
).resolves.toEqual({ dispatched: true });
|
|
497
|
+
expect(sendRequest).toHaveBeenNthCalledWith(14, THREAD_TEAM_TASK_CANCEL_METHOD, {
|
|
498
|
+
threadId: "thread-1",
|
|
499
|
+
teamId: "team-1",
|
|
500
|
+
taskId: "task-1",
|
|
501
|
+
});
|
|
502
|
+
expect(sendRequest).toHaveBeenNthCalledWith(15, THREAD_TEAM_TASK_REASSIGN_METHOD, {
|
|
503
|
+
threadId: "thread-1",
|
|
504
|
+
teamId: "team-1",
|
|
505
|
+
taskId: "task-1",
|
|
506
|
+
memberId: "member-2",
|
|
507
|
+
note: "换人重做",
|
|
508
|
+
});
|
|
509
|
+
expect(sendRequest).toHaveBeenNthCalledWith(16, THREAD_COLLABORATION_CONTINUE_METHOD, {
|
|
510
|
+
threadId: "thread-1",
|
|
511
|
+
});
|
|
466
512
|
});
|
|
467
513
|
|
|
468
514
|
it("uses fixed generic Session import methods and never accepts a browser-supplied locator", async () => {
|