@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.
Files changed (49) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +4 -4
  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/renderer-extension.js +149 -19
  7. package/package.json +4 -2
  8. package/scripts/acp-image-test.cjs +69 -0
  9. package/scripts/adapters-test.cjs +25 -0
  10. package/scripts/codex-accounts-test.cjs +34 -0
  11. package/scripts/codex-adapter-test.cjs +77 -1
  12. package/scripts/collaboration-test.cjs +304 -1
  13. package/scripts/collaboration-ui-smoke.cjs +25 -3
  14. package/scripts/e2e-delegate.cjs +1 -1
  15. package/scripts/e2e-hermes-image.cjs +60 -0
  16. package/scripts/openclaw-adapter-test.cjs +121 -3
  17. package/scripts/openclaw-mcp-probe.cjs +100 -0
  18. package/scripts/openclaw-mcp-tool-probe.cjs +55 -0
  19. package/scripts/openclaw-thinking-probe.cjs +73 -0
  20. package/scripts/storage-verification-test.cjs +11 -0
  21. package/src/main/adapters/claude.js +15 -7
  22. package/src/main/adapters/codex-app-server.js +29 -11
  23. package/src/main/adapters/codex.js +39 -5
  24. package/src/main/adapters/omp.js +7 -2
  25. package/src/main/adapters/openclaw.js +534 -343
  26. package/src/main/adapters/pi-family.js +35 -8
  27. package/src/main/adapters/zcode.js +12 -8
  28. package/src/main/host/collaboration-tools.js +1 -1
  29. package/src/main/host/collaboration.js +285 -36
  30. package/src/main/host/runtime.js +9 -3
  31. package/src/main/host/verification-gates.js +14 -2
  32. package/src/main/native/codex-accounts.js +15 -4
  33. package/src/main/native/protocol.js +6 -0
  34. package/src/native-ui/desktop-control/dist/renderer-cdp-control-session.js +3 -1
  35. package/src/native-ui/desktop-control/dist/renderer-cdp-control-session.js.map +1 -1
  36. package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
  37. package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
  38. package/src/native-ui/renderer-extension/dist/types/renderer-collab-cards.d.ts +1 -0
  39. package/src/native-ui/renderer-extension/dist/types/renderer-collab-cards.d.ts.map +1 -1
  40. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +29 -0
  41. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
  42. package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.d.ts +4 -0
  43. package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.d.ts.map +1 -1
  44. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  45. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +20 -7
  46. package/src/native-ui/renderer-extension/src/renderer-collab-cards.ts +25 -0
  47. package/src/native-ui/renderer-extension/src/renderer-model-client.ts +27 -0
  48. package/src/native-ui/renderer-extension/src/renderer-team-cards.ts +93 -13
  49. package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +47 -1
@@ -23131,6 +23131,10 @@ ${error51.stderrTail}`] : []
23131
23131
  var THREAD_OWNERSHIP_LIST_METHOD = "harnessmix/thread/ownership/list";
23132
23132
  var THREAD_USAGE_INSPECT_METHOD = "harnessmix/thread/usage/inspect";
23133
23133
  var THREAD_TEAM_INSPECT_METHOD = "harnessmix/thread/team/inspect";
23134
+ var THREAD_TEAM_TASK_CANCEL_METHOD = "harnessmix/thread/team/task/cancel";
23135
+ var THREAD_TEAM_TASK_REASSIGN_METHOD = "harnessmix/thread/team/task/reassign";
23136
+ var THREAD_TEAM_MESSAGE_SEND_METHOD = "harnessmix/thread/team/message/send";
23137
+ var THREAD_COLLABORATION_CONTINUE_METHOD = "harnessmix/thread/collaboration/continue";
23134
23138
  var THREAD_USAGE_UPDATED_METHOD = "harnessmix/thread/usage/updated";
23135
23139
  var THREAD_TOKEN_USAGE_UPDATED_METHOD = "thread/tokenUsage/updated";
23136
23140
  var UPDATE_CHECK_METHOD = "harnessmix/update/check";
@@ -23361,6 +23365,15 @@ ${error51.stderrTail}`] : []
23361
23365
  const threadId = hostThreadIdSchema.parse(input.threadId);
23362
23366
  return manager.sendRequest(THREAD_TEAM_INSPECT_METHOD, { threadId, ...input.teamId ? { teamId: input.teamId } : {} });
23363
23367
  },
23368
+ async collaborationUserAction(input) {
23369
+ const threadId = hostThreadIdSchema.parse(input.threadId);
23370
+ const method = input.action === "task/cancel" ? THREAD_TEAM_TASK_CANCEL_METHOD : input.action === "task/reassign" ? THREAD_TEAM_TASK_REASSIGN_METHOD : input.action === "message/send" ? THREAD_TEAM_MESSAGE_SEND_METHOD : THREAD_COLLABORATION_CONTINUE_METHOD;
23371
+ const params = { threadId };
23372
+ for (const [key, value] of Object.entries(input)) {
23373
+ if (key !== "action" && key !== "threadId" && value !== void 0) params[key] = value;
23374
+ }
23375
+ return manager.sendRequest(method, params);
23376
+ },
23364
23377
  subscribeThreadUsage(listener) {
23365
23378
  const notifications = notificationTarget(source);
23366
23379
  if (!notifications?.addNotificationCallback) {
@@ -24966,6 +24979,28 @@ ${error51.stderrTail}`] : []
24966
24979
  const agent = payload.agent_type || "agent";
24967
24980
  const icon2 = collaborationIcon(agent, agent, 18);
24968
24981
  strip.append(icon2);
24982
+ if (payload.status === "interrupted" && payload.parent_thread_id && options.continueCollab) {
24983
+ const resumeBtn = document.createElement("button");
24984
+ resumeBtn.type = "button";
24985
+ resumeBtn.className = "harness-mix-collab-resume";
24986
+ 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";
24987
+ resumeBtn.textContent = "\u25B6 \u6062\u590D\u6B64\u4EFB\u52A1";
24988
+ resumeBtn.title = "\u6062\u590D\u4E2D\u65AD\u7684\u59D4\u6D3E\uFF1A\u4E3B\u5BFC\u8005\u4F1A\u6536\u5230\u6062\u590D\u6307\u4EE4\uFF08\u4E0D\u91CD\u653E\u5DF2\u5B8C\u6210\u526F\u4F5C\u7528\uFF09";
24989
+ resumeBtn.addEventListener("click", async (e) => {
24990
+ e.preventDefault();
24991
+ e.stopPropagation();
24992
+ resumeBtn.disabled = true;
24993
+ try {
24994
+ await options.continueCollab(payload.parent_thread_id, payload.task_id);
24995
+ resumeBtn.textContent = "\u2713 \u5DF2\u4E0B\u53D1\u6062\u590D\u6307\u4EE4";
24996
+ } catch (err) {
24997
+ resumeBtn.disabled = false;
24998
+ resumeBtn.textContent = "\u25B6 \u6062\u590D\u5931\u8D25\uFF08\u91CD\u8BD5\uFF09";
24999
+ resumeBtn.title = `\u6062\u590D\u5931\u8D25\uFF1A${err?.message || err}`;
25000
+ }
25001
+ });
25002
+ strip.append(resumeBtn);
25003
+ }
24969
25004
  if (payload.child_thread_id) {
24970
25005
  const childId = payload.child_thread_id;
24971
25006
  const jumpBtn = document.createElement("button");
@@ -25120,7 +25155,7 @@ ${error51.stderrTail}`] : []
25120
25155
  }
25121
25156
  var stateColor = (status) => ({ ready: "#8b8b8b", pending: "#8b8b8b", working: "#2878e3", in_progress: "#2878e3", active: "#2878e3", completed: "#1f9d68", blocked: "#c17022", failed: "#d14343", interrupted: "#c17022" })[status ?? ""] ?? "#8b8b8b";
25122
25157
  var stateLabel = (status) => ({ ready: "\u5C31\u7EEA", pending: "\u5F85\u5F00\u59CB", working: "\u5DE5\u4F5C\u4E2D", in_progress: "\u8FDB\u884C\u4E2D", completed: "\u5DF2\u5B8C\u6210", blocked: "\u7B49\u5F85\u4F9D\u8D56", failed: "\u5931\u8D25", interrupted: "\u5DF2\u4E2D\u65AD", active: "\u534F\u4F5C\u4E2D" })[status ?? ""] ?? status ?? "\u672A\u77E5";
25123
- var actionLabel = (action) => ({ team_created: "\u56E2\u961F\u5EFA\u7ACB", task_assigned: "\u4EFB\u52A1\u5206\u914D", task_updated: "\u4EFB\u52A1\u66F4\u65B0", message_sent: "\u56E2\u961F\u901A\u4FE1", task_started: "\u5F00\u59CB\u6267\u884C", member_session_ready: "\u4F1A\u8BDD\u5C31\u7EEA", task_settled: "\u4EFB\u52A1\u7ED3\u7B97", task_failed: "\u4EFB\u52A1\u5931\u8D25", task_cancelled: "\u4EFB\u52A1\u53D6\u6D88", task_interrupted: "\u4EFB\u52A1\u4E2D\u65AD", task_resumed: "\u6062\u590D\u6267\u884C", task_followup: "\u7EE7\u7EED\u6267\u884C" })[action ?? ""] ?? action ?? "\u5B9E\u65F6\u72B6\u6001";
25158
+ var actionLabel = (action) => ({ team_created: "\u56E2\u961F\u5EFA\u7ACB", task_assigned: "\u4EFB\u52A1\u5206\u914D", task_updated: "\u4EFB\u52A1\u66F4\u65B0", message_sent: "\u56E2\u961F\u901A\u4FE1", task_started: "\u5F00\u59CB\u6267\u884C", member_session_ready: "\u4F1A\u8BDD\u5C31\u7EEA", task_settled: "\u4EFB\u52A1\u7ED3\u7B97", task_failed: "\u4EFB\u52A1\u5931\u8D25", task_cancelled: "\u4EFB\u52A1\u53D6\u6D88", task_interrupted: "\u4EFB\u52A1\u4E2D\u65AD", task_resumed: "\u6062\u590D\u6267\u884C", task_followup: "\u7EE7\u7EED\u6267\u884C", task_reassigned: "\u4EFB\u52A1\u6539\u6D3E", task_retry: "\u81EA\u52A8\u91CD\u8BD5" })[action ?? ""] ?? action ?? "\u5B9E\u65F6\u72B6\u6001";
25124
25159
  var memberPalette = ["#3b82f6", "#f59e0b", "#10b981", "#8b5cf6", "#f97316", "#ec4899", "#14b8a6", "#6366f1"];
25125
25160
  var memberColor = (index) => memberPalette[index % memberPalette.length];
25126
25161
  function el(tag, className, css) {
@@ -25182,6 +25217,41 @@ ${error51.stderrTail}`] : []
25182
25217
  }
25183
25218
  });
25184
25219
  }
25220
+ function actionButton(label, title, onClick, tone) {
25221
+ 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"}`);
25222
+ node.type = "button";
25223
+ node.textContent = label;
25224
+ node.title = title;
25225
+ node.addEventListener("click", (event) => {
25226
+ event.stopPropagation();
25227
+ event.preventDefault();
25228
+ onClick();
25229
+ });
25230
+ return node;
25231
+ }
25232
+ async function runUserAction(userAction, node, input) {
25233
+ node.classList.remove("harness-mix-team-open-failed");
25234
+ try {
25235
+ await userAction(input);
25236
+ } catch (error51) {
25237
+ console.warn("[TeamCards] \u770B\u677F\u64CD\u4F5C\u5931\u8D25:", error51);
25238
+ node.classList.add("harness-mix-team-open-failed");
25239
+ node.title = `\u64CD\u4F5C\u5931\u8D25\uFF1A${error51?.message ?? String(error51)}`;
25240
+ setTimeout(() => node.classList.remove("harness-mix-team-open-failed"), 1800);
25241
+ }
25242
+ }
25243
+ function continueButton(payload, userAction) {
25244
+ const busy = payload.lead?.display_status === "working";
25245
+ const node = actionButton("\u7EE7\u7EED\u534F\u4F5C", busy ? "\u4E3B\u5BFC\u8005\u56DE\u5408\u8FDB\u884C\u4E2D\uFF0C\u7ED3\u675F\u540E\u518D\u7EE7\u7EED" : "\u628A\u4E2D\u65AD\u7684\u59D4\u6D3E\u6062\u590D\u4E3A\u65B0\u7684\u4E3B\u5BFC\u8005\u56DE\u5408", () => {
25246
+ if (payload.lead_thread_id) void runUserAction(userAction, node, { action: "continue", threadId: payload.lead_thread_id });
25247
+ });
25248
+ if (busy) {
25249
+ node.disabled = true;
25250
+ node.style.opacity = ".45";
25251
+ node.style.cursor = "not-allowed";
25252
+ }
25253
+ return node;
25254
+ }
25185
25255
  function metrics(payload) {
25186
25256
  const node = el("div", "harness-mix-team-metrics", "display:flex;gap:18px;align-items:center");
25187
25257
  const values = [[`${payload.tasks.filter((task) => task.status === "completed").length}/${payload.tasks.length}`, "\u5B8C\u6210", "#1f9d68"], [`${payload.tasks.filter((task) => task.status === "in_progress").length}`, "\u8FDB\u884C\u4E2D", "#2878e3"], [`${payload.tasks.filter((task) => task.status === "blocked").length}`, "\u7B49\u5F85", "#c17022"]];
@@ -25225,8 +25295,8 @@ ${error51.stderrTail}`] : []
25225
25295
  dot.dataset.status = status ?? "";
25226
25296
  return dot;
25227
25297
  }
25228
- var actionIcon = (action) => ({ team_created: "\u{1F3AC}", task_assigned: "\u{1F4CB}", task_updated: "\u{1F504}", task_started: "\u{1F680}", member_session_ready: "\u{1F50C}", task_settled: "\u2705", task_failed: "\u274C", task_cancelled: "\u26D4", task_interrupted: "\u23F8", task_resumed: "\u25B6\uFE0F", task_followup: "\u{1F4AC}" })[action ?? ""] ?? "\u2022";
25229
- function renderBoard(payload, openThread, snapshots = []) {
25298
+ var actionIcon = (action) => ({ team_created: "\u{1F3AC}", task_assigned: "\u{1F4CB}", task_updated: "\u{1F504}", task_started: "\u{1F680}", member_session_ready: "\u{1F50C}", task_settled: "\u2705", task_failed: "\u274C", task_cancelled: "\u26D4", task_interrupted: "\u23F8", task_resumed: "\u25B6\uFE0F", task_followup: "\u{1F4AC}", task_reassigned: "\u{1F501}", task_retry: "\u267B\uFE0F" })[action ?? ""] ?? "\u2022";
25299
+ function renderBoard(payload, openThread, snapshots = [], userAction) {
25230
25300
  const board = el("main", "harness-mix-team-board harness-mix-team-body", "height:100%;min-height:0;color:inherit");
25231
25301
  const lead = payload.lead ?? { id: "lead", name: "Team Lead", role: "\u534F\u8C03\u4E0E\u9A8C\u6536", agent: "codex", display_status: "working" };
25232
25302
  const tasksOf = (memberId) => payload.tasks.filter((task) => task.assignee === memberId);
@@ -25249,12 +25319,11 @@ ${error51.stderrTail}`] : []
25249
25319
  };
25250
25320
  const memberCard = (member, index) => {
25251
25321
  const color = memberColor(index), assigned = tasksOf(member.id), done = assigned.filter((task) => task.status === "completed").length;
25252
- 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");
25253
- button.type = "button";
25254
- button.dataset.agent = member.agent;
25255
- button.dataset.status = member.display_status ?? "";
25256
- button.style.setProperty("--lane-color", color ?? "");
25257
- wireOpen(button, member.childId ?? member.child_thread_id, member.name, openThread);
25322
+ 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");
25323
+ card.dataset.agent = member.agent;
25324
+ card.dataset.status = member.display_status ?? "";
25325
+ card.style.setProperty("--lane-color", color ?? "");
25326
+ wireOpen(card, member.childId ?? member.child_thread_id, member.name, openThread);
25258
25327
  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`);
25259
25328
  avatar.dataset.status = member.display_status ?? "";
25260
25329
  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`);
@@ -25264,6 +25333,7 @@ ${error51.stderrTail}`] : []
25264
25333
  copy.append(text("strong", member.name, "font-size:12px;font-weight:680;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"));
25265
25334
  const status = el("span", void 0, "display:flex;align-items:center;gap:5px;min-width:0");
25266
25335
  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"));
25336
+ if (member.unread) status.append(text("span", `\u{1F4AC}${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"));
25267
25337
  copy.append(status);
25268
25338
  const progress = el("progress");
25269
25339
  progress.max = Math.max(1, assigned.length);
@@ -25271,8 +25341,26 @@ ${error51.stderrTail}`] : []
25271
25341
  progress.style.cssText = `width:100%;height:4px;margin:0;accent-color:${color}`;
25272
25342
  progress.title = `${done}/${assigned.length} \u4E2A\u4EFB\u52A1\u5B8C\u6210`;
25273
25343
  copy.append(progress);
25274
- button.append(avatar, copy);
25275
- return button;
25344
+ card.append(avatar, copy);
25345
+ if (userAction && payload.lead_thread_id) {
25346
+ const askRow = el("span", void 0, "position:absolute;top:5px;right:6px;display:flex;align-items:center;gap:4px;max-width:72%");
25347
+ const askButton = actionButton("\u8FFD\u95EE", `\u4EE5\u4E3B\u5BFC\u8005\u8EAB\u4EFD\u5411 ${member.name} \u53D1\u9001\u56E2\u961F\u6D88\u606F`, () => {
25348
+ const input = el("input", void 0, "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");
25349
+ input.placeholder = `\u5411 ${member.name} \u8BF4\u2026`;
25350
+ const send = actionButton("\u53D1\u9001", "\u53D1\u9001\u56E2\u961F\u6D88\u606F", () => {
25351
+ const message = input.value.trim();
25352
+ if (message) void runUserAction(userAction, send, { action: "message/send", threadId: payload.lead_thread_id, teamId: payload.team_id, to: member.id, message });
25353
+ });
25354
+ input.addEventListener("keydown", (event) => {
25355
+ if (event.key === "Enter") send.click();
25356
+ });
25357
+ askRow.replaceChildren(input, send, actionButton("\xD7", "\u53D6\u6D88", () => askRow.replaceChildren(askButton)));
25358
+ input.focus();
25359
+ });
25360
+ askRow.append(askButton);
25361
+ card.append(askRow);
25362
+ }
25363
+ return card;
25276
25364
  };
25277
25365
  const laneOf = (member, index) => {
25278
25366
  const color = memberColor(index), assigned = tasksOf(member.id), done = assigned.filter((task) => task.status === "completed").length;
@@ -25302,6 +25390,35 @@ ${error51.stderrTail}`] : []
25302
25390
  title.title = task.title;
25303
25391
  card.append(topRow, title);
25304
25392
  if ((task.dependsOn?.length ?? 0) > 0) card.append(text("div", `\u4F9D\u8D56 ${task.dependsOn.length} \u9879\u4EFB\u52A1`, "font-size:8px;opacity:.45"));
25393
+ if (userAction && payload.lead_thread_id) {
25394
+ const assignee = payload.members.find((member2) => member2.id === task.assignee);
25395
+ const row = el("div", void 0, "display:flex;align-items:center;gap:5px;flex-wrap:wrap;min-width:0");
25396
+ const renderDefault = () => {
25397
+ row.replaceChildren();
25398
+ if (task.status === "in_progress") {
25399
+ row.append(actionButton("\u53D6\u6D88", "\u53D6\u6D88\u8BE5\u4EFB\u52A1\u5E76\u505C\u6B62\u6210\u5458\u4F1A\u8BDD", () => {
25400
+ if (window.confirm(`\u53D6\u6D88\u4EFB\u52A1\u300C${task.title}\u300D\uFF1F\u8FD0\u884C\u4E2D\u7684\u6210\u5458\u4F1A\u8BDD\u5C06\u88AB\u505C\u6B62\u3002`)) void runUserAction(userAction, row, { action: "task/cancel", threadId: payload.lead_thread_id, teamId: payload.team_id, taskId: task.id });
25401
+ }, "#d14343"));
25402
+ } else if (task.status === "failed" || task.status === "interrupted") {
25403
+ row.append(
25404
+ actionButton(task.status === "failed" ? "\u91CD\u8BD5" : "\u6062\u590D", `\u628A\u4EFB\u52A1\u91CD\u65B0\u6D3E\u7ED9 ${assignee?.name ?? "\u539F\u6210\u5458"}`, () => {
25405
+ void runUserAction(userAction, row, { action: "task/reassign", threadId: payload.lead_thread_id, teamId: payload.team_id, taskId: task.id, memberId: task.assignee });
25406
+ }, "#c17022"),
25407
+ actionButton("\u6539\u6D3E", "\u6539\u6D3E\u7ED9\u5176\u4ED6\u6210\u5458", () => {
25408
+ const select = el("select", void 0, "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");
25409
+ select.append(new Option("\u6539\u6D3E\u7ED9\u2026", ""));
25410
+ for (const candidate of payload.members) if (candidate.id !== task.assignee) select.append(new Option(candidate.name, candidate.id));
25411
+ const go = actionButton("\u786E\u5B9A", "\u6267\u884C\u6539\u6D3E", () => {
25412
+ 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 });
25413
+ });
25414
+ row.replaceChildren(select, go, actionButton("\xD7", "\u53D6\u6D88\u6539\u6D3E", renderDefault));
25415
+ })
25416
+ );
25417
+ }
25418
+ };
25419
+ renderDefault();
25420
+ if (row.childElementCount) card.append(row);
25421
+ }
25305
25422
  list.append(card);
25306
25423
  }
25307
25424
  if (!assigned.length) list.append(text("div", "\u7B49\u5F85\u4E3B\u5BFC\u8005\u5206\u914D\u4EFB\u52A1", "font-size:9px;opacity:.42;padding:10px 2px;text-align:center"));
@@ -25350,7 +25467,7 @@ ${error51.stderrTail}`] : []
25350
25467
  board.append(left, feed);
25351
25468
  return board;
25352
25469
  }
25353
- function renderSummary(payload, open, openThread) {
25470
+ function renderSummary(payload, open, openThread, userAction) {
25354
25471
  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');
25355
25472
  panel.dataset.teamId = payload.team_id;
25356
25473
  panel.dataset.teamStatus = payload.status;
@@ -25370,6 +25487,7 @@ ${error51.stderrTail}`] : []
25370
25487
  button.textContent = "\u5C55\u5F00\u8BE6\u60C5";
25371
25488
  button.addEventListener("click", open);
25372
25489
  top.append(identity, metrics(payload), button);
25490
+ if (userAction && payload.lead_thread_id && payload.tasks.some((task) => task.status === "interrupted")) top.append(continueButton(payload, userAction));
25373
25491
  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");
25374
25492
  payload.members.forEach((member, index) => {
25375
25493
  const color = memberColor(index);
@@ -25386,6 +25504,7 @@ ${error51.stderrTail}`] : []
25386
25504
  icon2.append(collaborationIcon(member.agent, member.name, 18), dot);
25387
25505
  const label = el("span", void 0, "display:grid;min-width:0;line-height:1.2");
25388
25506
  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)}`));
25507
+ if (member.unread) label.append(text("span", `\u{1F4AC} ${member.unread} \u6761\u672A\u8BFB`, "font-size:8.5px;font-weight:700;color:#2878e3"));
25389
25508
  chip.append(icon2, label);
25390
25509
  members.append(chip);
25391
25510
  });
@@ -25463,7 +25582,8 @@ ${error51.stderrTail}`] : []
25463
25582
  collapse.textContent = "\u6536\u8D77\u8BE6\u60C5";
25464
25583
  collapse.addEventListener("click", close);
25465
25584
  timeline.append(event, range, live, play, collapse);
25466
- header.append(title, statsWrap, progressWrap, timeline);
25585
+ const headerActions = el("div", void 0, "display:flex;align-items:center;gap:6px;flex:none");
25586
+ header.append(title, statsWrap, progressWrap, timeline, headerActions);
25467
25587
  const content = el("div", void 0, "min-height:0");
25468
25588
  workbench.append(header, content);
25469
25589
  source.after(workbench);
@@ -25478,13 +25598,14 @@ ${error51.stderrTail}`] : []
25478
25598
  content.replaceChildren(renderBoard(visible, async (threadId) => {
25479
25599
  close();
25480
25600
  await options.openThread?.(threadId);
25481
- }, feedSnapshots));
25601
+ }, feedSnapshots, options.userAction));
25482
25602
  teamName.textContent = visible.name;
25483
25603
  subtitle.textContent = visible.goal;
25484
25604
  subtitle.title = visible.goal;
25485
25605
  teamPill.textContent = stateLabel(visible.status);
25486
25606
  teamPill.style.cssText = pillCss(visible.status);
25487
25607
  statsWrap.replaceChildren(...headerStats(visible));
25608
+ headerActions.replaceChildren(...options.userAction && visible.tasks.some((task) => task.status === "interrupted") ? [continueButton(visible, options.userAction)] : []);
25488
25609
  const completion = completionOf(visible);
25489
25610
  progressBar.max = Math.max(1, completion.total);
25490
25611
  progressBar.value = completion.done;
@@ -25550,7 +25671,7 @@ ${error51.stderrTail}`] : []
25550
25671
  if ([...candidate.querySelectorAll(selector)].some((child) => parseTeamPayload(child.textContent ?? ""))) continue;
25551
25672
  const signature = `${payload.updated_at ?? 0}:${payload.tasks.length}:${payload.messages.length}:${payload.members.map((member) => member.display_status).join(",")}`;
25552
25673
  if (signatures.get(candidate) === signature) continue;
25553
- const panel = renderSummary(payload, () => open(payload), options.openThread);
25674
+ const panel = renderSummary(payload, () => open(payload), options.openThread, options.userAction);
25554
25675
  if (candidate.tagName === "PRE") {
25555
25676
  if (!candidate.dataset.harnessMixTeamDisplay) candidate.dataset.harnessMixTeamDisplay = candidate.style.display || "__empty__";
25556
25677
  candidate.style.display = "none";
@@ -25600,7 +25721,7 @@ ${error51.stderrTail}`] : []
25600
25721
  if (activePanel?.isConnected && activeSignature === signature && activeThreadId === context.threadId) return;
25601
25722
  const reopen = workbench?.dataset.teamId === payload.team_id && workbench.dataset.teamSource === "active-thread";
25602
25723
  removeActivePanel();
25603
- activePanel = renderSummary(payload, () => open(payload), options.openThread);
25724
+ activePanel = renderSummary(payload, () => open(payload), options.openThread, options.userAction);
25604
25725
  activePanel.classList.add("harness-mix-team-launcher");
25605
25726
  activePanel.dataset.teamSource = "active-thread";
25606
25727
  activePanel.style.cssText += ";margin:8px 16px 0;flex:none;position:relative;z-index:11";
@@ -32795,6 +32916,10 @@ ${pet_market_default}`;
32795
32916
  applyWorkspace: async (threadId, digest) => {
32796
32917
  const client = modelClientForHost("local");
32797
32918
  return client?.applyThreadWorkspace?.({ threadId, digest }) ?? { patch: "", digest: "" };
32919
+ },
32920
+ continueCollab: async (threadId, taskId) => {
32921
+ const client = modelClientForHost("local");
32922
+ return client?.collaborationUserAction?.({ action: "continue", threadId, ...taskId ? { taskId } : {} });
32798
32923
  }
32799
32924
  });
32800
32925
  const teamCards = installTeamCards({
@@ -32802,6 +32927,10 @@ ${pet_market_default}`;
32802
32927
  const client = modelClientForHost("local");
32803
32928
  return client?.inspectThreadTeam?.({ threadId, ...teamId ? { teamId } : {} }) ?? null;
32804
32929
  },
32930
+ userAction: async (input) => {
32931
+ const client = modelClientForHost("local");
32932
+ return client?.collaborationUserAction?.(input) ?? null;
32933
+ },
32805
32934
  openThread: (threadId) => openRendererThread(hostThreadIdSchema.parse(threadId), { hostId: "local" }),
32806
32935
  activeThread: () => {
32807
32936
  const mounted = [...mountedByComposer.values()].find((candidate) => {
@@ -32991,14 +33120,15 @@ ${pet_market_default}`;
32991
33120
  const refreshCommands = async (mounted) => {
32992
33121
  const generation = ++mounted.commandRequestGeneration;
32993
33122
  const agent = controller.get(mounted.composer).agent;
32994
- const hostId = threadIdFromComposerModelTarget(mounted.modelTarget) ? mounted.hostId : activeModelHostId();
33123
+ const threadId = threadIdFromComposerModelTarget(mounted.modelTarget);
33124
+ const hostId = threadId ? mounted.hostId : activeModelHostId();
32995
33125
  const requestControl = modelControl;
32996
33126
  const client = modelClientForHostFrom(requestControl, hostId);
32997
33127
  mounted.control.harnessCommands.setCommands([]);
32998
33128
  if (agent === "codex" || !client) return;
32999
33129
  try {
33000
- const catalog = await client.inspectHarnessCommands({ harnessId: externalHarnessIds[agent] });
33001
- if (disposed || mountedByComposer.get(mounted.composer) !== mounted || mounted.commandRequestGeneration !== generation || requestControl !== modelControl || (threadIdFromComposerModelTarget(mounted.modelTarget) ? mounted.hostId : activeModelHostId()) !== hostId || controller.get(mounted.composer).agent !== agent)
33130
+ const catalog = threadId ? await client.inspectThreadCommands({ threadId }) : await client.inspectHarnessCommands({ harnessId: externalHarnessIds[agent] });
33131
+ if (disposed || mountedByComposer.get(mounted.composer) !== mounted || mounted.commandRequestGeneration !== generation || requestControl !== modelControl || threadIdFromComposerModelTarget(mounted.modelTarget) !== threadId || (threadId ? mounted.hostId : activeModelHostId()) !== hostId || controller.get(mounted.composer).agent !== agent)
33002
33132
  return;
33003
33133
  mounted.control.harnessCommands.setCommands(
33004
33134
  catalog.commands,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-mix/cli",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "license": "(Apache-2.0 OR MIT)",
5
5
  "description": "Harness Mix native Codex UI bridge for Codex, Pi, Claude Code, DeepSeek Harness, Antigravity, CodeBuddy, Kiro CLI, Cursor CLI and other native coding harnesses.",
6
6
  "keywords": [
@@ -78,6 +78,7 @@
78
78
  "test:codebuddy-migration": "node scripts/codebuddy-migration-test.cjs",
79
79
  "e2e:native-acp": "node scripts/e2e-native-acp.cjs --live",
80
80
  "test:native-acp-depth": "node scripts/native-acp-depth-test.cjs",
81
+ "test:acp-image": "node scripts/acp-image-test.cjs",
81
82
  "test:kiro-cursor": "node scripts/kiro-cursor-adapters-test.cjs",
82
83
  "test:docs": "node scripts/docs-command-test.cjs",
83
84
  "typecheck:native-ui": "tsc -b src/native-ui/shared-contracts/tsconfig.json src/native-ui/desktop-control/tsconfig.json src/native-ui/renderer-extension/tsconfig.json --pretty false",
@@ -124,7 +125,7 @@
124
125
  "test:core-concurrency": "node scripts/core-concurrency-test.cjs",
125
126
  "test:thread-title": "node scripts/thread-title-test.cjs",
126
127
  "test:handoff": "node scripts/handoff-checkpoint-test.cjs && node scripts/switch-harness-test.cjs",
127
- "test:core-all": "node scripts/contracts-test.cjs && node scripts/projector-test.cjs && node scripts/turn-manager-test.cjs && node scripts/sequence-test.cjs && node scripts/shadow-test.cjs && node scripts/architecture-test.cjs && node scripts/adapters-test.cjs && node scripts/dsh-adapter-test.cjs && node scripts/codex-adapter-test.cjs && node scripts/codex-accounts-test.cjs && node scripts/antigravity-adapter-test.cjs && node scripts/openclaw-adapter-test.cjs && node scripts/core-replay-test.cjs && node scripts/determinism-test.cjs && node scripts/core-runtime-test.cjs && node scripts/core-services-test.cjs && node scripts/native-file-change-test.cjs && node scripts/native-updater-test.cjs && node scripts/native-update-apply-test.cjs && node scripts/native-vendor-adapters-test.cjs && node scripts/core-concurrency-test.cjs && node scripts/concurrency-layers-test.cjs && node scripts/tool-concurrency-regression-test.cjs && node scripts/handoff-checkpoint-test.cjs && node scripts/switch-harness-test.cjs && node scripts/native-acp-depth-test.cjs && node scripts/thread-title-test.cjs && node scripts/native-job-object-test.cjs && node scripts/integrations-test.cjs && node scripts/storage-verification-test.cjs && node scripts/stuck-turn-test.cjs && node scripts/pi-cancel-race-test.cjs && node scripts/send-cancel-race-test.cjs && node scripts/delegation-await-test.cjs && node scripts/core-review-test.cjs && node scripts/zcode-adapter-test.cjs && node scripts/jsonl-stdin-test.cjs && node scripts/send-pre-turn-cancel-test.cjs",
128
+ "test:core-all": "node scripts/contracts-test.cjs && node scripts/projector-test.cjs && node scripts/turn-manager-test.cjs && node scripts/sequence-test.cjs && node scripts/shadow-test.cjs && node scripts/architecture-test.cjs && node scripts/adapters-test.cjs && node scripts/dsh-adapter-test.cjs && node scripts/codex-adapter-test.cjs && node scripts/codex-accounts-test.cjs && node scripts/antigravity-adapter-test.cjs && node scripts/openclaw-adapter-test.cjs && node scripts/acp-image-test.cjs && node scripts/core-replay-test.cjs && node scripts/determinism-test.cjs && node scripts/core-runtime-test.cjs && node scripts/core-services-test.cjs && node scripts/native-file-change-test.cjs && node scripts/native-updater-test.cjs && node scripts/native-update-apply-test.cjs && node scripts/native-vendor-adapters-test.cjs && node scripts/core-concurrency-test.cjs && node scripts/concurrency-layers-test.cjs && node scripts/tool-concurrency-regression-test.cjs && node scripts/handoff-checkpoint-test.cjs && node scripts/switch-harness-test.cjs && node scripts/native-acp-depth-test.cjs && node scripts/thread-title-test.cjs && node scripts/native-job-object-test.cjs && node scripts/integrations-test.cjs && node scripts/storage-verification-test.cjs && node scripts/stuck-turn-test.cjs && node scripts/pi-cancel-race-test.cjs && node scripts/send-cancel-race-test.cjs && node scripts/delegation-await-test.cjs && node scripts/core-review-test.cjs && node scripts/zcode-adapter-test.cjs && node scripts/jsonl-stdin-test.cjs && node scripts/send-pre-turn-cancel-test.cjs && node scripts/collaboration-test.cjs && node scripts/collaboration-recovery-test.cjs",
128
129
  "test:codex-accounts": "node scripts/codex-accounts-test.cjs",
129
130
  "smoke:codex-accounts-ui": "electron scripts/codex-accounts-ui-smoke.cjs",
130
131
  "test:transcript": "node scripts/transcript-test.cjs",
@@ -137,6 +138,7 @@
137
138
  "e2e:dsh": "node scripts/e2e-dsh.cjs",
138
139
  "e2e:openclaw": "node scripts/e2e-openclaw.cjs",
139
140
  "e2e:openclaw:image": "node scripts/e2e-openclaw-image.cjs",
141
+ "e2e:hermes:image": "node scripts/e2e-hermes-image.cjs",
140
142
  "e2e:codex": "node scripts/e2e-codex.cjs",
141
143
  "e2e:delegate": "node scripts/e2e-delegate.cjs",
142
144
  "test:determinism": "node scripts/determinism-test.cjs",
@@ -0,0 +1,69 @@
1
+ const assert = require('node:assert/strict');
2
+ const { acpAdapter } = require('../src/main/adapters/acp');
3
+ const { randomUUID } = require('node:crypto');
4
+
5
+ // acpAdapter(Hermes 所在 ACP 家族工厂)图片附件路径单元测试:
6
+ // initialize 实测声明 image 支持 → session/prompt 携带 {type:'image',data,mimeType} 块;
7
+ // 声明不支持 → send 直接拒绝("原生 ACP 不支持图片"),不静默丢图。
8
+ // fixture 子进程模式仿 native-acp-depth-test.cjs。
9
+
10
+ if (process.argv.includes('--fixture')) {
11
+ const write = value => process.stdout.write(JSON.stringify({ jsonrpc: '2.0', ...value }) + '\n');
12
+ const noImages = process.argv.includes('--no-images');
13
+ let sid, sawImageBlock = null;
14
+ require('node:readline').createInterface({ input: process.stdin }).on('line', line => {
15
+ const r = JSON.parse(line);
16
+ let result = {};
17
+ if (r.method === 'initialize') result = { agentCapabilities: { loadSession: true, promptCapabilities: { image: !noImages } } };
18
+ if (r.method === 'session/new' || r.method === 'session/load') {
19
+ sid = randomUUID();
20
+ result = { sessionId: sid, configOptions: [{ id: 'model', currentValue: 'native-model', options: [{ value: 'native-model', name: 'Native' }] }] };
21
+ }
22
+ if (r.method === 'session/prompt') {
23
+ if (r.params.prompt[0]?.text === 'image') {
24
+ sawImageBlock = r.params.prompt[1];
25
+ // 形状断言在 fixture 内完成:不匹配直接以错误回包,让适配器侧 reject
26
+ const ok = JSON.stringify(sawImageBlock) === JSON.stringify({ type: 'image', data: 'AA==', mimeType: 'image/png' });
27
+ if (!ok) { write({ id: r.id, error: { message: `image block shape mismatch: ${JSON.stringify(sawImageBlock)}` } }); return; }
28
+ }
29
+ result = { stopReason: 'end_turn', userMessageId: 'fixture-user' };
30
+ }
31
+ if (r.id !== undefined) write({ id: r.id, result });
32
+ if (r.method === 'session/prompt' && sawImageBlock) process.stderr.write(`IMAGE_BLOCK_OK:${JSON.stringify(sawImageBlock)}\n`);
33
+ });
34
+ } else {
35
+ (async () => {
36
+ const build = () => acpAdapter({
37
+ id: 'hermes', name: 'Hermes', bin: 'hermes-fixture', executable: true, args: ['acp'],
38
+ images: true, fork: true, thinking: false, permissions: false, questions: false, compaction: false, usage: false, contextUsage: false,
39
+ resolveCommand: () => ({ command: process.execPath, args: [__filename, '--fixture'] }),
40
+ }).create();
41
+
42
+ // 1. 握手声明 image 支持 → 图片块按 ACP 形状进入 session/prompt
43
+ const adapter = build();
44
+ const session = await adapter.open({ thread: { cwd: process.cwd() }, emit: () => {} });
45
+ try {
46
+ assert.equal(session.state.agentCapabilities?.promptCapabilities?.image, true, '握手能力必须被采集');
47
+ await adapter.send(session, 'image', { emit: () => {} }, { images: [{ data: 'AA==', mime: 'image/png' }] });
48
+ } finally { await adapter.close(session); }
49
+
50
+ // 2. 纯文本回合不受影响
51
+ const textAdapter = build();
52
+ const textSession = await textAdapter.open({ thread: { cwd: process.cwd() }, emit: () => {} });
53
+ try { await textAdapter.send(textSession, 'plain', { emit: () => {} }); }
54
+ finally { await textAdapter.close(textSession); }
55
+
56
+ // 3. 旧版上游(initialize 不声明 image)→ send 拒绝而不是静默丢图
57
+ const old = acpAdapter({
58
+ id: 'hermes', name: 'Hermes', bin: 'hermes-fixture', executable: true, args: ['acp'],
59
+ images: true, fork: true, thinking: false, permissions: false, questions: false, compaction: false, usage: false, contextUsage: false,
60
+ resolveCommand: () => ({ command: process.execPath, args: [__filename, '--fixture', '--no-images'] }),
61
+ }).create();
62
+ const oldSession = await old.open({ thread: { cwd: process.cwd() }, emit: () => {} });
63
+ try {
64
+ await assert.rejects(old.send(oldSession, 'image', { emit: () => {} }, { images: [{ data: 'AA==', mime: 'image/png' }] }), /Hermes 原生 ACP 不支持图片/);
65
+ } finally { await old.close(oldSession); }
66
+
67
+ console.log('acp image: prompt block shape, handshake gating and loud rejection PASS');
68
+ })().catch(error => { console.error(error); process.exitCode = 1; });
69
+ }
@@ -1,4 +1,5 @@
1
1
  // PR 5 验收:对注册表中全部 Adapter 执行同一组契约测试(无 Electron、不启动原生进程)。
2
+ const assert = require('node:assert/strict');
2
3
  const { buildAdapters } = require('../src/main/adapters');
3
4
  const { runAdapterContractTests } = require('../src/main/harness-adapter/contract-test');
4
5
  const { normalizeCapabilities } = require('../src/main/harness-adapter/manifest');
@@ -22,4 +23,28 @@ for (const adapter of adapters) {
22
23
  });
23
24
  }
24
25
 
26
+ // Pi 家族权限目录按成员原生面分开(诚实面):Pi=项目信任旗标,OMP=--approval-mode 三档。
27
+ // 实测依据:pi 0.84.2 `pi --help`(--approve/-a、--no-approve/-na);
28
+ // @oh-my-pi/pi-coding-agent 18.1.19 flag-tables.ts/settings-schema.ts(always-ask|write|yolo)。
29
+ {
30
+ const { PI_PERMISSION_MODES, OMP_APPROVAL_MODES, piPermissionLaunchArgs, ompPermissionLaunchArgs } = require('../src/main/adapters/pi-family');
31
+ test('[pi] 权限目录 = 项目信任三档(default/approve/no-approve),不携带 OMP 档', () => {
32
+ assert.deepEqual(PI_PERMISSION_MODES.map((m) => m.id), ['default', 'approve', 'no-approve']);
33
+ assert.deepEqual(piPermissionLaunchArgs('approve'), ['--approve']);
34
+ assert.deepEqual(piPermissionLaunchArgs('no-approve'), ['--no-approve']);
35
+ assert.deepEqual(piPermissionLaunchArgs('yolo'), [], 'Pi 不认 OMP 的档位 id');
36
+ });
37
+ test('[omp] 权限目录 = --approval-mode 三档(always-ask/write/yolo),不携带 Pi 旗标', () => {
38
+ assert.deepEqual(OMP_APPROVAL_MODES.map((m) => m.id), ['always-ask', 'write', 'yolo']);
39
+ for (const mode of ['always-ask', 'write', 'yolo']) assert.deepEqual(ompPermissionLaunchArgs(mode), ['--approval-mode', mode]);
40
+ assert.deepEqual(ompPermissionLaunchArgs('no-approve'), [], 'OMP 无 --no-approve 旗标,不虚构');
41
+ });
42
+ const omp = require('../src/main/adapters/omp');
43
+ const pi = require('../src/main/adapters/pi');
44
+ test('[pi/omp] 工厂注入各自的权限目录(describe 之外也可校验)', () => {
45
+ assert.deepEqual(pi.permissionModes.map((m) => m.id), ['default', 'approve', 'no-approve']);
46
+ assert.deepEqual(omp.permissionModes.map((m) => m.id), ['always-ask', 'write', 'yolo'], 'OMP 不再共用 Pi 的项目信任目录');
47
+ });
48
+ }
49
+
25
50
  console.log(`adapters: ${passed} passed${failed ? `, ${failed} failed` : ''} (${adapters.length} adapters)`);
@@ -71,6 +71,40 @@ async function main() {
71
71
  assert.equal(registry.includes('work@example.com'), false, 'Registry must not persist isolated identity or credentials');
72
72
  await manager.delete(created.account.accountId);
73
73
  assert.equal(fs.existsSync(context.codexHome), false);
74
+
75
+ // ── 旧结构账号兼容:显式 codexHome 必须被尊重(曾因改写到不存在的 profiles/<id>
76
+ // 导致 codex app-server 拒绝启动,新会话静默消失)
77
+ const legacyHome = path.join(root, 'real-codex-home');
78
+ fs.mkdirSync(legacyHome, { recursive: true });
79
+ fs.writeFileSync(path.join(root, 'codex-accounts', 'accounts.json'), JSON.stringify({
80
+ version: 1,
81
+ activeAccountId: 'default',
82
+ accounts: [
83
+ { accountId: 'default', label: 'Default Codex Account', codexHome: legacyHome },
84
+ { accountId: 'account-broken', label: 'Broken', codexHome: 'relative/not/absolute' },
85
+ { accountId: 'account-managed', label: 'Managed' },
86
+ ],
87
+ }));
88
+ const legacyManager = new CodexAccountManager({ dataDirectory: root, requestOfficial: official, emit: () => {}, acquireServer });
89
+ const legacyContext = legacyManager.executionContext('default');
90
+ assert.equal(legacyContext.codexHome, legacyHome, 'Legacy explicit codexHome must be honored, not rewritten to profiles/<id>');
91
+ assert.equal(fs.existsSync(path.join(root, 'codex-accounts', 'profiles', 'default')), false, 'No phantom profiles/default dir may be created for a legacy account');
92
+
93
+ // 相对路径残留 → 回落托管 profile 并自愈建目录
94
+ const brokenContext = legacyManager.executionContext('account-broken');
95
+ assert.ok(brokenContext.codexHome.startsWith(path.join(root, 'codex-accounts', 'profiles') + path.sep));
96
+ assert.equal(fs.existsSync(brokenContext.codexHome), true, 'Missing managed profile dir must be self-healed (codex refuses a missing CODEX_HOME)');
97
+
98
+ // 无 codexHome 的托管条目同样自愈
99
+ const managedContext = legacyManager.executionContext('account-managed');
100
+ assert.equal(fs.existsSync(managedContext.codexHome), true);
101
+
102
+ // 删除指向外部目录的旧账号:只移除账号槽位,不动外部目录
103
+ await legacyManager.delete('default');
104
+ assert.equal(fs.existsSync(legacyHome), true, 'External codexHome must never be deleted with the account');
105
+ const afterDelete = await legacyManager.list();
106
+ assert.equal(afterDelete.accounts.some(account => account.accountId === 'default'), false, 'Legacy account slot must be removed from the registry');
107
+ legacyManager.close();
74
108
  } finally {
75
109
  manager.close();
76
110
  fs.rmSync(root, { recursive: true, force: true });
@@ -77,6 +77,46 @@ function fakeSession() {
77
77
  assert.equal(compactDone.nativeRef.checkpointId, 'compact-turn');
78
78
  assert.ok(compactEvents.some(event => event.kind === 'text-delta' && /Codex 压缩/.test(event.text)));
79
79
 
80
+ // listCommands:无会话返回静态目录;skills/list → 插入型命令(slug 化、与 compact 去重、
81
+ // enabled!==false 过滤、id 满足 UI 契约字符集);旧版 app-server 无该 RPC 时回落静态目录
82
+ const staticList = await adapter.listCommands(null);
83
+ assert.deepEqual(staticList, [{ id: 'compact', label: '压缩上下文', description: '由 Codex 原生 app-server 压缩当前 Thread', action: 'execute' }]);
84
+
85
+ const skillsSession = fakeSession();
86
+ skillsSession.cwd = 'E:\\harness-mix';
87
+ const skillsRequests = [];
88
+ skillsSession.host = { async request(method, params) {
89
+ skillsRequests.push({ method, params });
90
+ return { data: [{ skills: [
91
+ { name: 'Agent Browser', description: 'Drive a real browser end to end.', shortDescription: '浏览器自动化', enabled: true, scope: 'user', interface: { displayName: 'Agent Browser', defaultPrompt: 'Browse' } },
92
+ { name: 'commit', description: 'Create a git commit', enabled: true, scope: 'repo' },
93
+ { name: 'Compact', description: 'should dedupe against compact', enabled: true, scope: 'user' },
94
+ { name: 'turned-off', description: 'disabled by config', enabled: false, scope: 'user' },
95
+ { name: '重复技能', description: 'slug 与重复技能相同', enabled: true, scope: 'system' },
96
+ ] }], nextCursor: null };
97
+ } };
98
+ const listed = await adapter.listCommands(skillsSession);
99
+ assert.deepEqual(skillsRequests, [{ method: 'skills/list', params: { cwds: ['E:\\harness-mix'] } }]);
100
+ assert.equal(listed[0].id, 'compact');
101
+ assert.equal(listed[0].action, 'execute', 'compact execute 条目保持首位');
102
+ const browser = listed.find(c => c.id === 'agent-browser');
103
+ assert.ok(browser, '"Agent Browser" slug 化为 agent-browser');
104
+ assert.equal(browser.label, '/Agent Browser', 'label 优先 interface.displayName');
105
+ assert.equal(browser.text, '/agent-browser ', '插入文本用 slug 触发原生技能');
106
+ assert.equal(browser.action, 'insert');
107
+ assert.ok(browser.description.includes('浏览器自动化') && browser.description.includes('Codex 技能·user'), '描述含短述与来源标注');
108
+ const commit = listed.find(c => c.id === 'commit');
109
+ assert.ok(commit.description.includes('Codex 技能·repo'), 'scope 标注随条目');
110
+ assert.ok(!commit.description.includes('displayName'), '无 shortDescription 时回落 description');
111
+ assert.equal(listed.filter(c => c.id === 'compact').length, 1, 'slug 化的 Compact 与静态 compact 去重');
112
+ assert.deepEqual(listed.map(c => c.id), ['compact', 'agent-browser', 'commit'], 'enabled:false 过滤;非 ASCII name slug 化为空即跳过');
113
+ assert.ok(listed.every(c => /^[A-Za-z0-9._:-]+$/.test(c.id)), 'id 满足 UI 契约字符集');
114
+
115
+ const legacySession = fakeSession();
116
+ legacySession.cwd = 'E:\\harness-mix';
117
+ legacySession.host = { async request(method) { throw new Error(`unknown method ${method}`); } };
118
+ assert.deepEqual(await adapter.listCommands(legacySession), staticList, '旧版 app-server 缺 skills/list 时回落静态目录');
119
+
80
120
  assert.equal(usageView({ last: { totalTokens: 50 }, total: {}, modelContextWindow: 200 }).contextPercent, 25);
81
121
  assert.deepEqual(modelView({ model: 'gpt-x', displayName: 'GPT X', supportedReasoningEfforts: [], isDefault: true }).id, 'gpt-x');
82
122
 
@@ -158,5 +198,41 @@ function fakeSession() {
158
198
  assert.deepEqual(interrupts, ['turn/interrupt']);
159
199
  assert.equal(interruptSession.state.turn, null);
160
200
 
161
- console.log('codex adapter: native notifications, usage, multi-question, approvals and cancel shapes passed');
201
+ // startTurnAfterNativeSettlement:busy 瞬时占用按指数退避重试;其他错误必须立刻可见
202
+ {
203
+ const { startTurnAfterNativeSettlement } = require('../src/main/adapters/codex');
204
+ let busyCalls = 0;
205
+ const busyHost = { request: async () => { if (++busyCalls <= 2) throw new Error('Agent is already processing.'); return { turn: { id: 'turn-ok' } }; } };
206
+ assert.deepEqual(await startTurnAfterNativeSettlement(busyHost, { threadId: 't' }), { turn: { id: 'turn-ok' } });
207
+ assert.equal(busyCalls, 3);
208
+ let strictCalls = 0;
209
+ const strictHost = { request: async () => { strictCalls++; throw new Error('model not found'); } };
210
+ await assert.rejects(() => startTurnAfterNativeSettlement(strictHost, { threadId: 't' }), /model not found/);
211
+ assert.equal(strictCalls, 1, '非 busy 错误不得重试');
212
+ }
213
+
214
+ // 握手超时:app-server 拉起后永不应答 initialize 时,acquire 必须限时终止并报错,
215
+ // 而不是让 thread/start 永久 pending(Desktop 端表现为新对话一直"在执行"、无会话产生)。
216
+ // Windows 夹具:cmd.exe 无 /c 时进入交互态等待 stdin,永不输出 JSON-RPC。
217
+ if (process.platform === 'win32') {
218
+ const { CodexAppServer } = require('../src/main/adapters/codex-app-server');
219
+ const previousExecutable = process.env.HARNESS_MIX_CODEX_EXECUTABLE;
220
+ const previousStock = process.env.HARNESSMIX_STOCK_CODEX_PATH;
221
+ const previousTimeout = process.env.HARNESS_MIX_CODEX_HANDSHAKE_TIMEOUT_MS;
222
+ process.env.HARNESS_MIX_CODEX_EXECUTABLE = process.env.ComSpec || 'cmd.exe';
223
+ process.env.HARNESS_MIX_CODEX_HANDSHAKE_TIMEOUT_MS = '600';
224
+ delete process.env.HARNESSMIX_STOCK_CODEX_PATH;
225
+ try {
226
+ const startedAt = Date.now();
227
+ await assert.rejects(() => CodexAppServer.acquire(), /完成初始化握手/);
228
+ const elapsed = Date.now() - startedAt;
229
+ assert.ok(elapsed >= 500 && elapsed < 5_000, `握手超时应接近配置窗口(实际 ${elapsed}ms)`);
230
+ } finally {
231
+ if (previousExecutable) process.env.HARNESS_MIX_CODEX_EXECUTABLE = previousExecutable; else delete process.env.HARNESS_MIX_CODEX_EXECUTABLE;
232
+ if (previousStock) process.env.HARNESSMIX_STOCK_CODEX_PATH = previousStock; else delete process.env.HARNESSMIX_STOCK_CODEX_PATH;
233
+ if (previousTimeout) process.env.HARNESS_MIX_CODEX_HANDSHAKE_TIMEOUT_MS = previousTimeout; else delete process.env.HARNESS_MIX_CODEX_HANDSHAKE_TIMEOUT_MS;
234
+ }
235
+ }
236
+
237
+ console.log('codex adapter: native notifications, usage, multi-question, approvals, cancel shapes, busy retry and handshake timeout passed');
162
238
  })().catch(error => { console.error(error); process.exitCode = 1; });