@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
|
@@ -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
|
|
25253
|
-
|
|
25254
|
-
|
|
25255
|
-
|
|
25256
|
-
|
|
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
|
-
|
|
25275
|
-
|
|
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
|
-
|
|
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
|
|
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 ||
|
|
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.
|
|
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
|
-
|
|
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; });
|