@harness-mix/cli 0.2.3 → 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.
Files changed (72) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +469 -467
  3. package/docs/harness-management.md +1 -1
  4. package/docs/multi-agent-collaboration.md +3 -1
  5. package/docs/native-acp.md +138 -127
  6. package/output/native-build/desktop-controller.mjs +1 -1
  7. package/output/native-build/renderer-extension.js +172 -23
  8. package/package.json +13 -9
  9. package/scripts/acp-image-test.cjs +69 -0
  10. package/scripts/adapters-test.cjs +25 -0
  11. package/scripts/antigravity-adapter-test.cjs +647 -626
  12. package/scripts/codex-accounts-test.cjs +34 -0
  13. package/scripts/codex-adapter-test.cjs +238 -127
  14. package/scripts/collaboration-test.cjs +577 -262
  15. package/scripts/collaboration-ui-smoke.cjs +25 -3
  16. package/scripts/e2e-delegate.cjs +1 -1
  17. package/scripts/e2e-hermes-image.cjs +60 -0
  18. package/scripts/jsonl-stdin-test.cjs +40 -31
  19. package/scripts/kiro-cursor-adapters-test.cjs +124 -100
  20. package/scripts/native-acp-depth-test.cjs +30 -5
  21. package/scripts/native-update-apply-test.cjs +269 -215
  22. package/scripts/native-update.cjs +78 -0
  23. package/scripts/native-vendor-adapters-test.cjs +196 -154
  24. package/scripts/openclaw-adapter-test.cjs +121 -3
  25. package/scripts/openclaw-mcp-probe.cjs +100 -0
  26. package/scripts/openclaw-mcp-tool-probe.cjs +55 -0
  27. package/scripts/openclaw-thinking-probe.cjs +73 -0
  28. package/scripts/salvage-rollout-writes.cjs +72 -0
  29. package/scripts/storage-verification-test.cjs +11 -0
  30. package/scripts/zcode-adapter-test.cjs +329 -0
  31. package/scripts/zcode-live-probe.cjs +66 -0
  32. package/src/main/adapters/antigravity.js +1428 -1418
  33. package/src/main/adapters/claude.js +15 -7
  34. package/src/main/adapters/codex-app-server.js +29 -11
  35. package/src/main/adapters/codex.js +690 -649
  36. package/src/main/adapters/native-acp-command.js +51 -48
  37. package/src/main/adapters/native-acp.js +47 -12
  38. package/src/main/adapters/omp.js +7 -2
  39. package/src/main/adapters/openclaw.js +534 -343
  40. package/src/main/adapters/pi-family.js +35 -8
  41. package/src/main/adapters/qoder.js +12 -8
  42. package/src/main/adapters/zcode.js +925 -10
  43. package/src/main/host/collaboration-tools.js +1 -1
  44. package/src/main/host/collaboration.js +971 -714
  45. package/src/main/host/jsonl.js +130 -120
  46. package/src/main/host/runtime.js +9 -3
  47. package/src/main/host/verification-gates.js +14 -2
  48. package/src/main/native/codex-accounts.js +15 -4
  49. package/src/main/native/config.js +9 -9
  50. package/src/main/native/launcher.js +252 -237
  51. package/src/main/native/process-utils.js +157 -57
  52. package/src/main/native/protocol.js +1227 -1187
  53. package/src/main/native/update-state.js +123 -110
  54. package/src/main/native/updater.js +460 -394
  55. package/src/native-ui/desktop-control/dist/renderer-cdp-control-session.js +3 -1
  56. package/src/native-ui/desktop-control/dist/renderer-cdp-control-session.js.map +1 -1
  57. package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
  58. package/src/native-ui/desktop-control/src/renderer-cdp-control-session.ts +358 -358
  59. package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
  60. package/src/native-ui/renderer-extension/dist/types/renderer-collab-cards.d.ts +1 -0
  61. package/src/native-ui/renderer-extension/dist/types/renderer-collab-cards.d.ts.map +1 -1
  62. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +29 -0
  63. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
  64. package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.d.ts +4 -0
  65. package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.d.ts.map +1 -1
  66. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  67. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +3224 -3181
  68. package/src/native-ui/renderer-extension/src/renderer-collab-cards.ts +25 -0
  69. package/src/native-ui/renderer-extension/src/renderer-model-client.ts +27 -0
  70. package/src/native-ui/renderer-extension/src/renderer-team-cards.ts +93 -13
  71. package/src/native-ui/renderer-extension/src/settings/connections-page.ts +2 -2
  72. package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +47 -1
@@ -22,6 +22,7 @@ export interface CollabCardOptions {
22
22
  openThread?: (threadId: HostThreadId) => Promise<void>;
23
23
  reviewWorkspace?: (threadId: string) => Promise<{ patch: string; digest: string; hasConflict?: boolean; conflictingFiles?: string[] }>;
24
24
  applyWorkspace?: (threadId: string, digest?: string) => Promise<{ patch: string; digest: string }>;
25
+ continueCollab?: (threadId: string, taskId?: string) => Promise<unknown>;
25
26
  }
26
27
 
27
28
  export function parseCollabPayload(text: string): CollabCardPayload | null {
@@ -79,6 +80,30 @@ export function installCollabCards(options: CollabCardOptions = {}) {
79
80
  const icon = collaborationIcon(agent, agent, 18);
80
81
  strip.append(icon);
81
82
 
83
+ // 中断的委派:一键恢复(Host 会以用户身份向 lead 线程注入恢复指令回合)
84
+ if (payload.status === 'interrupted' && payload.parent_thread_id && options.continueCollab) {
85
+ const resumeBtn = document.createElement('button');
86
+ resumeBtn.type = 'button';
87
+ resumeBtn.className = 'harness-mix-collab-resume';
88
+ resumeBtn.style.cssText = 'display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:6px;border:1px solid #c1702266;background:#c170221a;color:#c17022;font:inherit;cursor:pointer;font-size:12px;font-weight:500';
89
+ resumeBtn.textContent = '▶ 恢复此任务';
90
+ resumeBtn.title = '恢复中断的委派:主导者会收到恢复指令(不重放已完成副作用)';
91
+ resumeBtn.addEventListener('click', async (e) => {
92
+ e.preventDefault();
93
+ e.stopPropagation();
94
+ resumeBtn.disabled = true;
95
+ try {
96
+ await options.continueCollab!(payload.parent_thread_id!, payload.task_id);
97
+ resumeBtn.textContent = '✓ 已下发恢复指令';
98
+ } catch (err: any) {
99
+ resumeBtn.disabled = false;
100
+ resumeBtn.textContent = '▶ 恢复失败(重试)';
101
+ resumeBtn.title = `恢复失败:${err?.message || err}`;
102
+ }
103
+ });
104
+ strip.append(resumeBtn);
105
+ }
106
+
82
107
  if (payload.child_thread_id) {
83
108
  const childId = payload.child_thread_id;
84
109
  const jumpBtn = document.createElement('button');
@@ -128,6 +128,17 @@ export const THREAD_PERMISSION_MODE_SELECT_METHOD = "harnessmix/thread/permissio
128
128
  export const THREAD_OWNERSHIP_LIST_METHOD = "harnessmix/thread/ownership/list";
129
129
  export const THREAD_USAGE_INSPECT_METHOD = "harnessmix/thread/usage/inspect";
130
130
  export const THREAD_TEAM_INSPECT_METHOD = "harnessmix/thread/team/inspect";
131
+ export const THREAD_TEAM_TASK_CANCEL_METHOD = "harnessmix/thread/team/task/cancel";
132
+ export const THREAD_TEAM_TASK_REASSIGN_METHOD = "harnessmix/thread/team/task/reassign";
133
+ export const THREAD_TEAM_MESSAGE_SEND_METHOD = "harnessmix/thread/team/message/send";
134
+ export const THREAD_COLLABORATION_CONTINUE_METHOD = "harnessmix/thread/collaboration/continue";
135
+
136
+ // 看板用户操作:principal 是用户;授权与语义统一在 Host 的 collaboration.userAction 裁决。
137
+ export type CollaborationUserActionInput =
138
+ | { action: "task/cancel"; threadId: string; teamId: string; taskId: string }
139
+ | { action: "task/reassign"; threadId: string; teamId: string; taskId: string; memberId: string; note?: string }
140
+ | { action: "message/send"; threadId: string; teamId: string; to?: string; message: string; kind?: string }
141
+ | { action: "continue"; threadId: string; taskId?: string };
131
142
  export const THREAD_USAGE_UPDATED_METHOD = "harnessmix/thread/usage/updated";
132
143
  export const THREAD_TOKEN_USAGE_UPDATED_METHOD = "thread/tokenUsage/updated";
133
144
  export const UPDATE_CHECK_METHOD = "harnessmix/update/check";
@@ -201,6 +212,7 @@ export interface RendererModelClient extends Partial<RendererSessionImportClient
201
212
  listThreadOwnership(input: ThreadOwnershipListParams): Promise<ThreadOwnershipListResult>;
202
213
  inspectThreadUsage(input: ThreadUsageInspectionParams): Promise<ThreadUsageInspection>;
203
214
  inspectThreadTeam?(input: { threadId: string; teamId?: string }): Promise<unknown>;
215
+ collaborationUserAction?(input: CollaborationUserActionInput): Promise<unknown>;
204
216
  subscribeThreadUsage?(listener: (update: ThreadUsageInspection) => void): () => void;
205
217
  selectThreadModel(input: ThreadModelSelectParams): Promise<HarnessModelSelectionState>;
206
218
  selectThreadThinking(input: ThreadThinkingSelectParams): Promise<HarnessModelSelectionState>;
@@ -475,6 +487,21 @@ export function createRendererModelClient(
475
487
  const threadId = hostThreadIdSchema.parse(input.threadId);
476
488
  return manager.sendRequest(THREAD_TEAM_INSPECT_METHOD, { threadId, ...(input.teamId ? { teamId: input.teamId } : {}) });
477
489
  },
490
+ async collaborationUserAction(input: CollaborationUserActionInput): Promise<unknown> {
491
+ const threadId = hostThreadIdSchema.parse(input.threadId);
492
+ const method = input.action === "task/cancel"
493
+ ? THREAD_TEAM_TASK_CANCEL_METHOD
494
+ : input.action === "task/reassign"
495
+ ? THREAD_TEAM_TASK_REASSIGN_METHOD
496
+ : input.action === "message/send"
497
+ ? THREAD_TEAM_MESSAGE_SEND_METHOD
498
+ : THREAD_COLLABORATION_CONTINUE_METHOD;
499
+ const params: Record<string, unknown> = { threadId };
500
+ for (const [key, value] of Object.entries(input)) {
501
+ if (key !== "action" && key !== "threadId" && value !== undefined) params[key] = value;
502
+ }
503
+ return manager.sendRequest(method, params);
504
+ },
478
505
  subscribeThreadUsage(listener: (update: ThreadUsageInspection) => void): () => void {
479
506
  const notifications = notificationTarget(source);
480
507
  if (!notifications?.addNotificationCallback) {
@@ -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 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;
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);
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); button.append(avatar, copy); return button;
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); header.append(title, statsWrap, progressWrap, timeline);
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';
@@ -26,9 +26,9 @@ export const HARNESS_INSTALL_COMMANDS: Readonly<Partial<Record<ExternalRendererA
26
26
  "claude-code": { command: "npm install -g @anthropic-ai/claude-code" },
27
27
  "deepseek-harness": { command: "pip install deepseek-harness" },
28
28
  opencode: { command: "npm install -g opencode-ai" },
29
- grok: { command: "npm install -g @xai/grok-cli" },
29
+ grok: { command: "powershell -NoProfile -ExecutionPolicy Bypass -Command iex (irm https://x.ai/cli/install.ps1)" },
30
30
  omp: { command: "npm install -g @oh-my-pi/pi-coding-agent" },
31
- antigravity: { command: "npm install -g @google/antigravity-cli" },
31
+ antigravity: { command: "powershell -NoProfile -ExecutionPolicy Bypass -Command iex (irm https://antigravity.google/cli/install.ps1)" },
32
32
  openclaw: { command: "npm install -g openclaw" },
33
33
  hermes: { command: "pip install hermes-agent" },
34
34
  cline: { command: "npm install -g cline" },
@@ -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 () => {