harness-mix 0.1.2 → 0.1.3
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 +10 -1
- package/README.md +60 -15
- package/docs/images/codex-desktop-home.png +0 -0
- package/docs/images/codex-desktop-session.png +0 -0
- package/docs/multi-agent-collaboration.md +68 -68
- package/output/native-build/renderer-extension.js +2225 -1761
- package/package.json +1 -2
- package/scripts/collaboration-recovery-test.cjs +22 -2
- package/scripts/collaboration-ui-smoke.cjs +10 -5
- package/scripts/native-protocol-test.cjs +20 -0
- package/scripts/workspace-test.cjs +2 -14
- package/src/main/adapters/antigravity.js +49 -18
- package/src/main/adapters/claude.js +3 -2
- package/src/main/adapters/codex.js +10 -8
- package/src/main/adapters/dsh.js +3 -2
- package/src/main/adapters/opencode.js +21 -13
- package/src/main/adapters/pi-family.js +3 -3
- package/src/main/host/collaboration-worktree.js +63 -4
- package/src/main/host/collaboration.js +28 -2
- package/src/main/host/runtime.js +22 -1
- package/src/main/native/protocol.js +250 -24
- package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/index.d.ts +2 -0
- package/src/native-ui/renderer-extension/dist/types/index.d.ts.map +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 +40 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-collab-cards.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-credits-control.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +8 -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-usage-control.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/connections-page.d.ts +3 -1
- package/src/native-ui/renderer-extension/dist/types/settings/connections-page.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/harness-accounts.d.ts +8 -1
- package/src/native-ui/renderer-extension/dist/types/settings/harness-accounts.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts +16 -0
- package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/renderer-extension/src/assets/README.md +8 -38
- package/src/native-ui/renderer-extension/src/harness-mix-settings.ts +9 -1
- package/src/native-ui/renderer-extension/src/index.ts +9 -0
- package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +67 -6
- package/src/native-ui/renderer-extension/src/renderer-collab-cards.ts +198 -0
- package/src/native-ui/renderer-extension/src/renderer-credits-control.ts +32 -12
- package/src/native-ui/renderer-extension/src/renderer-harness-mentions.ts +139 -46
- package/src/native-ui/renderer-extension/src/renderer-model-client.ts +7 -2
- package/src/native-ui/renderer-extension/src/renderer-usage-control.ts +34 -3
- package/src/native-ui/renderer-extension/src/settings/accounts.css +80 -4
- package/src/native-ui/renderer-extension/src/settings/connections-page.ts +68 -29
- package/src/native-ui/renderer-extension/src/settings/harness-accounts.ts +89 -28
- package/src/native-ui/renderer-extension/src/settings/localization.ts +48 -0
- package/src/native-ui/renderer-extension/src/settings/pages.ts +19 -2
- package/src/native-ui/renderer-extension/src/settings/shell.css +62 -0
- package/src/native-ui/renderer-extension/src/settings/shell.ts +1 -1
- package/src/native-ui/renderer-extension/test/renderer-collab-cards.test.ts +45 -0
- package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +1 -0
- package/src/native-ui/shared-contracts/dist/harness-accounts.d.ts +20 -6
- package/src/native-ui/shared-contracts/dist/harness-accounts.d.ts.map +1 -1
- package/src/native-ui/shared-contracts/dist/harness-accounts.js +4 -1
- package/src/native-ui/shared-contracts/dist/harness-accounts.js.map +1 -1
- package/src/native-ui/shared-contracts/dist/harness-models.d.ts +22 -0
- package/src/native-ui/shared-contracts/dist/harness-models.d.ts.map +1 -1
- package/src/native-ui/shared-contracts/dist/harness-models.js +2 -1
- package/src/native-ui/shared-contracts/dist/harness-models.js.map +1 -1
- package/src/native-ui/shared-contracts/dist/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/shared-contracts/src/harness-accounts.ts +4 -1
- package/src/native-ui/shared-contracts/src/harness-models.ts +6 -1
- package/docs/images/harness-mix-home.png +0 -0
- package/docs/images/harness-mix-session.png +0 -0
- package/scripts/git-test.cjs +0 -33
- package/src/assets/icons/activity-compact.svg +0 -1
- package/src/assets/icons/activity-edit.svg +0 -1
- package/src/assets/icons/activity-terminal.svg +0 -1
- package/src/assets/icons/commands.svg +0 -1
- package/src/assets/icons/home-chat.svg +0 -1
- package/src/assets/icons/home-folder.svg +0 -1
- package/src/assets/icons/thread-fork.svg +0 -1
- package/src/main/workspace/git.js +0 -73
- package/src/main/workspace/ipc.js +0 -50
- package/src/main/workspace/terminal.js +0 -34
- package/src/native-ui/renderer-extension/src/assets/claude-agent.svg +0 -3
- package/src/native-ui/renderer-extension/src/assets/codex-logo-bright.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/codex-logo-transparent.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/codex-logo.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/codexhost-icon.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/codexhost-logo-transparent.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/codexhost-logo.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/codexhost-readme.svg +0 -8
- package/src/native-ui/renderer-extension/src/assets/pi-agent.svg +0 -4
- package/src/native-ui/renderer-extension/src/settings/accounts-quota-interactive.prototype.html +0 -1418
- package/src/native-ui/renderer-extension/src/settings/update-notes.preview.html +0 -491
- package/src/native-ui/renderer-extension/src/settings/windows-update.preview.html +0 -2135
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harness-mix",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"license": "Apache-2.0",
|
|
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": [
|
|
@@ -109,7 +109,6 @@
|
|
|
109
109
|
"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/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/switch-harness-test.cjs && node scripts/native-acp-depth-test.cjs && node scripts/thread-title-test.cjs && node scripts/native-job-object-test.cjs",
|
|
110
110
|
"test:transcript": "node scripts/transcript-test.cjs",
|
|
111
111
|
"test:workspace": "node scripts/workspace-test.cjs",
|
|
112
|
-
"test:git": "node scripts/git-test.cjs",
|
|
113
112
|
"e2e:pi": "node scripts/e2e-pi.cjs",
|
|
114
113
|
"e2e:shadow-pi": "node scripts/e2e-shadow.cjs pi",
|
|
115
114
|
"e2e:shadow-claude": "node scripts/e2e-shadow.cjs claude",
|
|
@@ -2,7 +2,7 @@ const assert = require('node:assert/strict');
|
|
|
2
2
|
const fs = require('node:fs/promises');
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
const { randomUUID } = require('node:crypto');
|
|
5
|
-
const { createWorkspace, reviewWorkspace, applyWorkspace, git } = require('../src/main/host/collaboration-worktree');
|
|
5
|
+
const { createWorkspace, reviewWorkspace, applyWorkspace, discardWorkspace, pushWorkspace, git } = require('../src/main/host/collaboration-worktree');
|
|
6
6
|
const { Collaboration } = require('../src/main/host/collaboration');
|
|
7
7
|
const { HostRuntime } = require('../src/main/host/runtime');
|
|
8
8
|
const { SessionHistory } = require('../src/main/host/session-history');
|
|
@@ -29,7 +29,10 @@ async function main() {
|
|
|
29
29
|
assert.doesNotMatch(review.patch, /untracked.txt/);
|
|
30
30
|
await assert.rejects(applyWorkspace(workspace, 'stale'), /重新审查/);
|
|
31
31
|
await fs.writeFile(path.join(repo, 'file.txt'), 'conflicting edit\n');
|
|
32
|
-
await
|
|
32
|
+
const conflictRev = await reviewWorkspace(workspace);
|
|
33
|
+
assert.equal(conflictRev.hasConflict, true);
|
|
34
|
+
assert.deepEqual(conflictRev.conflictingFiles, ['file.txt']);
|
|
35
|
+
await assert.rejects(applyWorkspace(workspace, review.digest), /冲突文件/);
|
|
33
36
|
assert.equal(await fs.readFile(path.join(repo, 'file.txt'), 'utf8'), 'conflicting edit\n');
|
|
34
37
|
assert.equal(await fs.stat(path.join(repo, 'new.txt')).catch(() => null), null, 'Conflict applies no partial patch');
|
|
35
38
|
await fs.writeFile(path.join(repo, 'file.txt'), 'unstaged\n');
|
|
@@ -37,6 +40,23 @@ async function main() {
|
|
|
37
40
|
assert.equal(await fs.readFile(path.join(repo, 'file.txt'), 'utf8'), 'worker\n');
|
|
38
41
|
assert.equal(await git(repo, ['diff', '--cached', '--binary']), staged);
|
|
39
42
|
|
|
43
|
+
// 隔离工作区冲突排查、远程推送与分支丢弃
|
|
44
|
+
const workspace2 = await createWorkspace(repo, randomUUID());
|
|
45
|
+
await fs.writeFile(path.join(workspace2.cwd, 'file2.txt'), 'w2 content\n');
|
|
46
|
+
const review2 = await reviewWorkspace(workspace2);
|
|
47
|
+
assert.equal(review2.hasConflict, false);
|
|
48
|
+
const remoteBare = path.join(root, 'remote-bare.git');
|
|
49
|
+
await git(repo, ['init', '--bare', remoteBare]);
|
|
50
|
+
await git(repo, ['remote', 'add', 'origin', remoteBare]);
|
|
51
|
+
const pushRes = await pushWorkspace(workspace2, 'origin');
|
|
52
|
+
assert.equal(pushRes.pushed, true);
|
|
53
|
+
assert.equal(pushRes.remote, 'origin');
|
|
54
|
+
assert.equal(pushRes.branch, workspace2.branch);
|
|
55
|
+
const discardRes = await discardWorkspace(workspace2);
|
|
56
|
+
assert.equal(discardRes.discarded, true);
|
|
57
|
+
assert.equal(discardRes.branch, workspace2.branch);
|
|
58
|
+
assert.equal(await fs.stat(workspace2.root).catch(() => null), null, 'Worktree directory cleaned');
|
|
59
|
+
|
|
40
60
|
const rt = new HostRuntime({ dataDirectory: path.join(root, 'data') }); await rt.store.load();
|
|
41
61
|
await rt.collaboration.initialize();
|
|
42
62
|
rt.collaboration.jobs.set('saved', { id: 'saved', owner: 'parent', agent: 'pi', childId: 'child', status: 'running', task: 'continue', workspace });
|
|
@@ -36,12 +36,17 @@ app.whenReady().then(async () => {
|
|
|
36
36
|
editor.value='@旧';editor.setSelectionRange(2,2);editor.dispatchEvent(new Event('input',{bubbles:true}));await new Promise(r=>setTimeout(r,30));
|
|
37
37
|
document.querySelector('[data-harness-mix-mentions] [role="option"]').click();
|
|
38
38
|
const sessionBadge=[...document.querySelectorAll('[data-harness-mix-selected-mentions] span')].some(b=>b.title.startsWith('历史会话'));
|
|
39
|
-
|
|
39
|
+
const root = editor.closest('[data-codex-composer-root]');
|
|
40
|
+
root.dispatchEvent(new Event('submit', { bubbles: true }));
|
|
41
|
+
const submittedText = editor.value;
|
|
42
|
+
return {filtered,text,submittedText,richText,badge,icons,disabled,sessionText:editor.value,sessionBadge};
|
|
40
43
|
})()`);
|
|
41
|
-
assert.equal(result.filtered, 1);
|
|
42
|
-
assert.
|
|
43
|
-
assert.
|
|
44
|
-
assert.
|
|
44
|
+
assert.equal(result.filtered, 1);
|
|
45
|
+
assert.equal(result.text, '你负责后端, ');
|
|
46
|
+
assert.match(result.submittedText, /@claude/);
|
|
47
|
+
assert.equal(result.disabled, 1);
|
|
48
|
+
assert.equal(result.badge, 'harnesses/claude'); assert.equal(result.icons, 3);
|
|
49
|
+
assert.equal(result.sessionBadge, true);
|
|
45
50
|
const out = path.resolve('output/collaboration-ui'); await fs.mkdir(out, { recursive: true });
|
|
46
51
|
await new Promise(r => setTimeout(r,100));
|
|
47
52
|
await fs.writeFile(path.join(out, 'mentions.png'), (await win.webContents.capturePage()).toPNG());
|
|
@@ -232,6 +232,26 @@ async function main() {
|
|
|
232
232
|
await assert.rejects(bridge.request('thread/fork', { threadId, threadSource: 'thread_description' }), /Ephemeral fork is not supported/);
|
|
233
233
|
await assert.rejects(bridge.request('thread/fork', { threadId, excludeTurns: true }), /Ephemeral fork is not supported/);
|
|
234
234
|
await assert.rejects(bridge.request('thread/start', { cwd: root, model: routeModel('pi'), ephemeral: true, threadSource: 'thread_description' }), /Ephemeral background thread is not supported/);
|
|
235
|
+
// Worktree 隔离工作区丢弃与推送协议接口
|
|
236
|
+
await assert.rejects(bridge.request('codexhost/thread/workspace/discard', { threadId }), /该任务未使用 Worktree 隔离工作区/);
|
|
237
|
+
await assert.rejects(bridge.request('codexhost/thread/workspace/push', { threadId }), /该任务未使用 Worktree 隔离工作区/);
|
|
238
|
+
const dummyWorktreeThread = await bridge.request('thread/start', { cwd: root, model: routeModel('pi') });
|
|
239
|
+
const dummyThreadId = dummyWorktreeThread.thread.id;
|
|
240
|
+
const dummyWorkspaceDir = path.join(root, 'dummy-wt');
|
|
241
|
+
await fs.mkdir(dummyWorkspaceDir, { recursive: true });
|
|
242
|
+
const dummyTargetThread = runtime.threads.find(t => t.id === dummyThreadId);
|
|
243
|
+
dummyTargetThread.workspace = {
|
|
244
|
+
mode: 'worktree',
|
|
245
|
+
root: dummyWorkspaceDir,
|
|
246
|
+
cwd: dummyWorkspaceDir,
|
|
247
|
+
source: root,
|
|
248
|
+
branch: 'codexhost/test-wt-branch',
|
|
249
|
+
};
|
|
250
|
+
await assert.rejects(bridge.request('codexhost/thread/workspace/push', { threadId: dummyThreadId }), /git/i);
|
|
251
|
+
const discardResult = await bridge.request('codexhost/thread/workspace/discard', { threadId: dummyThreadId });
|
|
252
|
+
assert.equal(discardResult.discarded, true);
|
|
253
|
+
assert.equal(discardResult.branch, 'codexhost/test-wt-branch');
|
|
254
|
+
assert.equal(dummyTargetThread.workspace, undefined, 'Discard deletes thread.workspace');
|
|
235
255
|
console.log('PASS: native protocol, external steering, command execution and Usage projection');
|
|
236
256
|
} finally { bridge.close(); await runtime.close(); }
|
|
237
257
|
}
|
|
@@ -6,7 +6,6 @@ const os = require('node:os');
|
|
|
6
6
|
const { ReviewStore } = require('../src/main/workspace/review');
|
|
7
7
|
const { readText, resolveFile } = require('../src/main/workspace/files');
|
|
8
8
|
const { diff } = require('../src/main/workspace/diff');
|
|
9
|
-
const { CommandTerminal } = require('../src/main/workspace/terminal');
|
|
10
9
|
const { HostRuntime } = require('../src/main/host/runtime');
|
|
11
10
|
|
|
12
11
|
(async () => {
|
|
@@ -50,17 +49,6 @@ const { HostRuntime } = require('../src/main/host/runtime');
|
|
|
50
49
|
await fs.symlink(path.join(directory, 'outside'), path.join(root, 'link'), 'junction');
|
|
51
50
|
await assert.rejects(resolveFile(root, 'link/file.txt'), /不跟随/);
|
|
52
51
|
const empty = diff('', ''); assert.equal(empty.rows.length, 0);
|
|
53
|
-
const terminal = new CommandTerminal(() => {});
|
|
54
|
-
const s = terminal.run(root, "Write-Output 'terminal-ok'");
|
|
55
|
-
for (let i = 0; i < 100 && terminal.sessions.get(s.id).running; i++) await new Promise(r => setTimeout(r, 50));
|
|
56
|
-
assert.match(terminal.sessions.get(s.id).output, /terminal-ok/);
|
|
57
|
-
assert.equal(terminal.sessions.get(s.id).exitCode, 0);
|
|
58
|
-
const long = terminal.run(root, 'Start-Sleep -Seconds 30');
|
|
59
|
-
await new Promise(r => setTimeout(r, 300));
|
|
60
|
-
await terminal.stop(long.id);
|
|
61
|
-
for (let i = 0; i < 50 && terminal.sessions.get(long.id).running; i++) await new Promise(r => setTimeout(r, 50));
|
|
62
|
-
assert.equal(terminal.sessions.get(long.id).running, false);
|
|
63
|
-
await terminal.close();
|
|
64
52
|
const rt = new HostRuntime({ observer: new ParityObserver(), dataDirectory: path.join(directory, 'runtime') });
|
|
65
53
|
const updates = [];
|
|
66
54
|
rt.subscribe(e => { if (e.type === 'turn/diff/updated') updates.push(e); });
|
|
@@ -72,7 +60,7 @@ const { HostRuntime } = require('../src/main/host/runtime');
|
|
|
72
60
|
emit({ kind: 'text-delta', text: 'progress' });
|
|
73
61
|
emit({ kind: 'tool', toolCallId: 'one', title: 'edit', state: 'running' });
|
|
74
62
|
await fs.writeFile(path.join(root, 'dirty.js'), 'native tool edit\n');
|
|
75
|
-
await new Promise(r => setTimeout(r,
|
|
63
|
+
await new Promise(r => setTimeout(r, 3200));
|
|
76
64
|
emit({ kind: 'tool', toolCallId: 'one', title: 'edit', state: 'done' });
|
|
77
65
|
emit({ kind: 'text-delta', text: 'conclusion' });
|
|
78
66
|
emit({ kind: 'completed', finalAnswer: true });
|
|
@@ -94,5 +82,5 @@ const { HostRuntime } = require('../src/main/host/runtime');
|
|
|
94
82
|
await rt.undoFile(t.id, answer.id, 'dirty.js');
|
|
95
83
|
assert.match(await fs.readFile(path.join(root, 'dirty.js'), 'utf8'), /user preexisting change/);
|
|
96
84
|
await rt.close();
|
|
97
|
-
console.log('baseline preserves dirty edits; add/delete/modify undo; conflict, traversal, junction guards; persisted review
|
|
85
|
+
console.log('baseline preserves dirty edits; add/delete/modify undo; conflict, traversal, junction guards; persisted review passed');
|
|
98
86
|
})().catch(e => { console.error(e); process.exit(1); });
|
|
@@ -14,6 +14,7 @@ const manifest = {
|
|
|
14
14
|
icon: 'antigravity-color.svg',
|
|
15
15
|
aliases: ['agy', 'antigravity'],
|
|
16
16
|
capabilities: {
|
|
17
|
+
collaborationTools: true,
|
|
17
18
|
streaming: true,
|
|
18
19
|
thinking: true,
|
|
19
20
|
tools: true,
|
|
@@ -249,18 +250,18 @@ function formatAntigravityResultError(result) {
|
|
|
249
250
|
return status ? `Antigravity 回合失败(${status})` : 'Antigravity 回合失败(未知错误)';
|
|
250
251
|
}
|
|
251
252
|
|
|
252
|
-
function resultSucceeded(result) {
|
|
253
|
-
return errorText(result?.status).toUpperCase() === 'SUCCESS';
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function modelSupportsEffort(model) {
|
|
257
|
-
const id = String(model?.id ?? '').trim();
|
|
258
|
-
if (Array.isArray(model?.efforts)) return model.efforts.length > 0;
|
|
259
|
-
// Claude models expose built-in thinking in agy and reject --effort.
|
|
260
|
-
return !/^claude(?:[-_.]|$)/i.test(id);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function formatPrompt(text) {
|
|
253
|
+
function resultSucceeded(result) {
|
|
254
|
+
return errorText(result?.status).toUpperCase() === 'SUCCESS';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function modelSupportsEffort(model) {
|
|
258
|
+
const id = String(model?.id ?? '').trim();
|
|
259
|
+
if (Array.isArray(model?.efforts)) return model.efforts.length > 0;
|
|
260
|
+
// Claude models expose built-in thinking in agy and reject --effort.
|
|
261
|
+
return !/^claude(?:[-_.]|$)/i.test(id);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function formatPrompt(text) {
|
|
264
265
|
if (!text) return '';
|
|
265
266
|
if (text.startsWith('/') || text.includes('ArtifactMetadata')) return text;
|
|
266
267
|
return `${ANTIGRAVITY_WORKSPACE_FILE_INSTRUCTION}${text}`;
|
|
@@ -638,6 +639,29 @@ class QuestionBridge {
|
|
|
638
639
|
CODEXHOST_AGY_QUESTION_CLIENT: `"${clientPath.replaceAll('\\', '/')}"`,
|
|
639
640
|
};
|
|
640
641
|
|
|
642
|
+
if (options.collaboration) {
|
|
643
|
+
const pluginDir = path.join(dir, '.agents', 'plugins', 'harness-mix');
|
|
644
|
+
const plainPluginDir = path.join(dir, 'plugins', 'harness-mix');
|
|
645
|
+
await fs.promises.mkdir(pluginDir, { recursive: true });
|
|
646
|
+
await fs.promises.mkdir(plainPluginDir, { recursive: true });
|
|
647
|
+
const pluginManifest = JSON.stringify({ name: 'harness-mix-collaboration', description: 'Harness Mix Multi-agent Collaboration' }, null, 2);
|
|
648
|
+
const mcpConfig = JSON.stringify({
|
|
649
|
+
mcpServers: {
|
|
650
|
+
'harness-mix': {
|
|
651
|
+
command: options.collaboration.command,
|
|
652
|
+
args: options.collaboration.args,
|
|
653
|
+
env: options.collaboration.env,
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
}, null, 2);
|
|
657
|
+
await fs.promises.writeFile(path.join(pluginDir, 'plugin.json'), pluginManifest, 'utf8');
|
|
658
|
+
await fs.promises.writeFile(path.join(pluginDir, 'mcp_config.json'), mcpConfig, 'utf8');
|
|
659
|
+
await fs.promises.writeFile(path.join(plainPluginDir, 'plugin.json'), pluginManifest, 'utf8');
|
|
660
|
+
await fs.promises.writeFile(path.join(plainPluginDir, 'mcp_config.json'), mcpConfig, 'utf8');
|
|
661
|
+
await fs.promises.writeFile(path.join(dir, '.agents', 'mcp_config.json'), mcpConfig, 'utf8');
|
|
662
|
+
Object.assign(bridge.environment, options.collaboration.env || {});
|
|
663
|
+
}
|
|
664
|
+
|
|
641
665
|
return bridge;
|
|
642
666
|
}
|
|
643
667
|
|
|
@@ -792,13 +816,14 @@ function create(emit, options = {}) {
|
|
|
792
816
|
if (error) {
|
|
793
817
|
resolve({ available: false, detail: '未找到 Antigravity CLI(agy)' });
|
|
794
818
|
} else {
|
|
819
|
+
if (!cachedQuota) fetchAntigravityQuota().then((q) => { if (q) cachedQuota = q; }).catch(() => {});
|
|
795
820
|
resolve({ available: true, detail: `agy ${String(stdout).trim()}` });
|
|
796
821
|
}
|
|
797
822
|
});
|
|
798
823
|
});
|
|
799
824
|
},
|
|
800
825
|
|
|
801
|
-
async open({ thread, emit: emitEvent, diagnostic }) {
|
|
826
|
+
async open({ thread, emit: emitEvent, diagnostic, collaboration }) {
|
|
802
827
|
// Do not start a second agy process while a real turn may be starting.
|
|
803
828
|
// Quota is loaded only by the explicit account/usage refresh path below.
|
|
804
829
|
await restoreLegacyClipboardAttachments(thread);
|
|
@@ -811,6 +836,8 @@ function create(emit, options = {}) {
|
|
|
811
836
|
model,
|
|
812
837
|
thinkingLevel,
|
|
813
838
|
permissionMode,
|
|
839
|
+
collaboration: collaboration || null,
|
|
840
|
+
collaborationEnabled: !!collaboration,
|
|
814
841
|
activeTurn: null,
|
|
815
842
|
bridge: null,
|
|
816
843
|
usage: undefined,
|
|
@@ -831,10 +858,14 @@ function create(emit, options = {}) {
|
|
|
831
858
|
const bridge = await QuestionBridge.create({
|
|
832
859
|
approvals,
|
|
833
860
|
emit: emitEvent,
|
|
861
|
+
collaboration: session.collaboration,
|
|
834
862
|
});
|
|
835
863
|
session.bridge = bridge;
|
|
836
864
|
|
|
837
865
|
let effectivePrompt = prompt ?? '';
|
|
866
|
+
if (session.collaborationEnabled) {
|
|
867
|
+
effectivePrompt = `[多 Agent 协作系统已就绪。你作为主协调者,可调用已注入的 MCP 工具:delegate_to_agent(agent_type, task) 向其他已注册的 Harness(如 @pi、@claude-code、@codex、@deepseek-harness 等)委派子任务,使用 get_delegation_status(task_ids) 等待结果,使用 update_agent_plan(steps) 设定与更新分步计划。]\n\n${effectivePrompt}`;
|
|
868
|
+
}
|
|
838
869
|
const { imageEntries, extraDirs } = await prepareImageAttachments(attachments?.images, session.cwd);
|
|
839
870
|
if (imageEntries.length) {
|
|
840
871
|
const imageNotice = [
|
|
@@ -859,9 +890,9 @@ function create(emit, options = {}) {
|
|
|
859
890
|
if (session.model?.id) {
|
|
860
891
|
args.push('--model', session.model.id);
|
|
861
892
|
}
|
|
862
|
-
if (session.thinkingLevel && modelSupportsEffort(session.model)) {
|
|
863
|
-
args.push('--effort', session.thinkingLevel);
|
|
864
|
-
}
|
|
893
|
+
if (session.thinkingLevel && modelSupportsEffort(session.model)) {
|
|
894
|
+
args.push('--effort', session.thinkingLevel);
|
|
895
|
+
}
|
|
865
896
|
if (['skip', 'desktop', 'dangerously-skip-permissions', 'desktop-approvals'].includes(session.permissionMode)) {
|
|
866
897
|
args.push('--dangerously-skip-permissions');
|
|
867
898
|
}
|
|
@@ -1323,8 +1354,8 @@ module.exports = {
|
|
|
1323
1354
|
create,
|
|
1324
1355
|
parseModelsOutput,
|
|
1325
1356
|
parseUsage,
|
|
1326
|
-
formatAntigravityResultError,
|
|
1327
|
-
modelSupportsEffort,
|
|
1357
|
+
formatAntigravityResultError,
|
|
1358
|
+
modelSupportsEffort,
|
|
1328
1359
|
formatPrompt,
|
|
1329
1360
|
prepareImageAttachments,
|
|
1330
1361
|
mergePendingStep,
|
|
@@ -25,7 +25,8 @@ const CLAUDE_PERMISSION_MODES = [
|
|
|
25
25
|
|
|
26
26
|
function summarizeInput(input) {
|
|
27
27
|
if (!input || typeof input !== "object") return "";
|
|
28
|
-
|
|
28
|
+
if (input.agent_type && input.task) return `@${input.agent_type}: ${input.task}`.split("\n")[0].slice(0, 120);
|
|
29
|
+
const value = input.command ?? input.file_path ?? input.pattern ?? input.task ?? input.description ?? Object.values(input)[0];
|
|
29
30
|
return typeof value === "string" ? value.split("\n")[0].slice(0, 120) : "";
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -152,7 +153,7 @@ function usageFromResult(event, planLimit) {
|
|
|
152
153
|
const usage = event?.usage;
|
|
153
154
|
if (!usage || typeof usage !== "object") return undefined;
|
|
154
155
|
const models = Object.values(event.modelUsage ?? {});
|
|
155
|
-
const window = models.find((m) => Number.isFinite(m?.contextWindow))?.contextWindow ??
|
|
156
|
+
const window = models.find((m) => Number.isFinite(m?.contextWindow))?.contextWindow ?? 200_000;
|
|
156
157
|
const input = usage.input_tokens;
|
|
157
158
|
const output = usage.output_tokens;
|
|
158
159
|
const cacheRead = usage.cache_read_input_tokens;
|
|
@@ -106,7 +106,7 @@ function usageView(tokenUsage) {
|
|
|
106
106
|
const last = tokenUsage.last ?? {};
|
|
107
107
|
const total = tokenUsage.total ?? {};
|
|
108
108
|
const tokens = Number.isFinite(last.totalTokens) ? last.totalTokens : null;
|
|
109
|
-
const contextWindow = Number.isFinite(tokenUsage.modelContextWindow) ? tokenUsage.modelContextWindow :
|
|
109
|
+
const contextWindow = Number.isFinite(tokenUsage.modelContextWindow) ? tokenUsage.modelContextWindow : 128_000;
|
|
110
110
|
return {
|
|
111
111
|
tokens,
|
|
112
112
|
contextWindow,
|
|
@@ -290,13 +290,15 @@ function queueRequest(message, session, emit) {
|
|
|
290
290
|
});
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
if (
|
|
293
|
+
if (method.endsWith('/requestApproval') || method.includes('requestApproval')) {
|
|
294
294
|
const requestId = `codex-${id}`;
|
|
295
295
|
return new Promise((resolve) => {
|
|
296
296
|
session.pendingApprovals.set(requestId, { method, resolve, params });
|
|
297
297
|
const title = method.includes('commandExecution') ? 'Codex 请求运行命令'
|
|
298
|
-
: method.includes('fileChange') ? 'Codex 请求修改文件'
|
|
299
|
-
|
|
298
|
+
: method.includes('fileChange') ? 'Codex 请求修改文件'
|
|
299
|
+
: (method.includes('mcp') || method.includes('tool')) ? `Codex 请求调用工具 · ${params.tool ?? params.toolName ?? params.serverName ?? 'MCP'}`
|
|
300
|
+
: 'Codex 请求权限审批';
|
|
301
|
+
const messageText = params.reason ?? params.command ?? params.tool ?? (params.grantRoot ? `允许写入 ${params.grantRoot}` : text(params.permissions));
|
|
300
302
|
emit({
|
|
301
303
|
kind: 'approval', requestId, title, message: messageText,
|
|
302
304
|
options: approvalOptions(method, params),
|
|
@@ -473,8 +475,8 @@ function create() {
|
|
|
473
475
|
models,
|
|
474
476
|
thinkingLevels: selected?.efforts ?? [],
|
|
475
477
|
permissionModes: [
|
|
476
|
-
{ id: 'default', label: '原生默认',
|
|
477
|
-
...profiles.filter((profile) => profile.allowed).map((profile) => ({ id: profile.id, label: profile.id,
|
|
478
|
+
{ id: 'default', label: '原生默认', description: '使用 Codex 当前配置的权限策略' },
|
|
479
|
+
...profiles.filter((profile) => profile.allowed).map((profile) => ({ id: profile.id, label: profile.id, description: profile.description })),
|
|
478
480
|
],
|
|
479
481
|
};
|
|
480
482
|
} finally { host.release(); }
|
|
@@ -489,8 +491,8 @@ function create() {
|
|
|
489
491
|
models,
|
|
490
492
|
thinkingLevels: selected?.efforts ?? [],
|
|
491
493
|
permissionModes: [
|
|
492
|
-
{ id: 'default', label: '原生默认',
|
|
493
|
-
...profiles.filter((profile) => profile.allowed).map((profile) => ({ id: profile.id, label: profile.id,
|
|
494
|
+
{ id: 'default', label: '原生默认', description: '使用 Codex 当前配置的权限策略' },
|
|
495
|
+
...profiles.filter((profile) => profile.allowed).map((profile) => ({ id: profile.id, label: profile.id, description: profile.description })),
|
|
494
496
|
],
|
|
495
497
|
};
|
|
496
498
|
},
|
package/src/main/adapters/dsh.js
CHANGED
|
@@ -161,8 +161,9 @@ function projectWireEvent(frame, session) {
|
|
|
161
161
|
function usageFrom(usage, contextWindow) {
|
|
162
162
|
if (!usage) return undefined;
|
|
163
163
|
const tokens = (usage.inputTokens ?? 0) + (usage.cacheReadTokens ?? 0) || null;
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
const window = contextWindow ?? 64_000;
|
|
165
|
+
if (!tokens && !window) return undefined;
|
|
166
|
+
return { tokens, contextWindow: window, contextPercent: tokens && window ? 100 * tokens / window : null };
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
/** approval/request waterfall → Host 审批卡片 */
|
|
@@ -2,7 +2,7 @@ const { execFile } = require('node:child_process');
|
|
|
2
2
|
const { OpenCodeServer, executable } = require('./opencode-server');
|
|
3
3
|
const manifest = { id: 'opencode', name: 'OpenCode', icon: 'opencode-color.svg', aliases: ['opencode'], capabilities: {
|
|
4
4
|
collaborationTools: true, streaming: true, thinking: true, tools: true, approvals: true, questions: true, models: true, thinkingLevels: true,
|
|
5
|
-
permissionModes: true, resume: true, fork: true, forkFromMessage: true, compaction: true, usage: true, contextUsage:
|
|
5
|
+
permissionModes: true, resume: true, fork: true, forkFromMessage: true, compaction: true, usage: true, contextUsage: true, attachments: true,
|
|
6
6
|
} };
|
|
7
7
|
const enc = encodeURIComponent;
|
|
8
8
|
const route = (s, suffix = '') => `/session/${enc(s.nativeSessionId)}${suffix}`;
|
|
@@ -142,18 +142,26 @@ function create() {
|
|
|
142
142
|
await session.host.request('POST', route(session, '/summarize'), { providerID: model.provider, modelID: model.id }, 30 * 60 * 1000);
|
|
143
143
|
hooks.emit({ kind: 'compaction', state: 'completed', outcome: 'succeeded' });
|
|
144
144
|
hooks.emit({ kind: 'completed', finalAnswer: false });
|
|
145
|
-
}
|
|
146
|
-
finally { session.state.active = false; }
|
|
147
|
-
},
|
|
148
|
-
async getContextUsage(session) {
|
|
149
|
-
const info = await session.host.request('GET', route(session)), t = info.tokens || {};
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
145
|
+
}
|
|
146
|
+
finally { session.state.active = false; }
|
|
147
|
+
},
|
|
148
|
+
async getContextUsage(session) {
|
|
149
|
+
const info = await session.host.request('GET', route(session)), t = info.tokens || {};
|
|
150
|
+
const model = session.state.models?.find(m => m.id === session.model?.id);
|
|
151
|
+
const contextWindow = model?.contextWindow || 128_000;
|
|
152
|
+
const tokens = (t.input || 0) + (t.output || 0) + (t.cache?.read || 0);
|
|
153
|
+
return {
|
|
154
|
+
inputTokens: t.input, outputTokens: t.output, reasoningOutputTokens: t.reasoning,
|
|
155
|
+
cacheRead: t.cache?.read, cacheWrite: t.cache?.write, cost: info.cost,
|
|
156
|
+
tokens, contextWindow,
|
|
157
|
+
contextPercent: tokens && contextWindow ? (100 * tokens / contextWindow) : null,
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
async fork(source, context) {
|
|
161
|
+
const host = await new OpenCodeServer(source.cwd).start();
|
|
162
|
+
try {
|
|
163
|
+
const history = await host.request('GET', `/session/${enc(source.nativeSessionId)}/message`), boundary = context.message?.coreTurn?.nativeTurnRef?.checkpointId;
|
|
164
|
+
if (context.message && !boundary) throw new Error('OpenCode reply has no native message boundary');
|
|
157
165
|
const index = boundary ? history.findIndex(m => m.info.id === boundary) : -1;
|
|
158
166
|
if (boundary && index < 0) throw new Error('OpenCode fork boundary no longer exists');
|
|
159
167
|
const next = boundary && history[index + 1]?.info.id;
|
|
@@ -45,9 +45,9 @@ function emitAll(emitEvent, mapped) {
|
|
|
45
45
|
|
|
46
46
|
/** Pi 家族(Pi / Oh My Pi)的权限模型 = 项目信任(project trust):启动时用 --approve / --no-approve 覆盖一次 */
|
|
47
47
|
const PI_PERMISSION_MODES = [
|
|
48
|
-
{ id: "default", label: "默认",
|
|
49
|
-
{ id: "approve", label: "信任项目",
|
|
50
|
-
{ id: "no-approve", label: "忽略项目资源",
|
|
48
|
+
{ id: "default", label: "默认", description: "按全局设置处理项目级设置与扩展(defaultProjectTrust)" },
|
|
49
|
+
{ id: "approve", label: "信任项目", description: "本次信任该项目的设置与扩展(--approve)" },
|
|
50
|
+
{ id: "no-approve", label: "忽略项目资源", description: "本次不加载项目级设置与扩展(--no-approve)" },
|
|
51
51
|
];
|
|
52
52
|
|
|
53
53
|
/**
|
|
@@ -62,19 +62,48 @@ async function reviewWorkspace(workspace) {
|
|
|
62
62
|
if (workspace?.mode !== 'worktree') throw new Error('此任务使用共享目录,请在任务文件变更中审查');
|
|
63
63
|
const currentTree = await tree(workspace.root);
|
|
64
64
|
const patch = await git(workspace.root, ['diff', '--binary', '--no-ext-diff', '--no-textconv', workspace.baseTree, currentTree, '--']);
|
|
65
|
-
|
|
65
|
+
const digest = createHash('sha256').update(patch).digest('hex');
|
|
66
|
+
const result = { patch, digest, branch: workspace.branch, cwd: workspace.cwd, hasConflict: false, conflictingFiles: [] };
|
|
67
|
+
if (patch) {
|
|
68
|
+
const file = path.join(os.tmpdir(), `harness-mix-check-${randomUUID()}`);
|
|
69
|
+
try {
|
|
70
|
+
await fs.writeFile(file, patch);
|
|
71
|
+
await git(workspace.source, ['apply', '--check', '--binary', file]);
|
|
72
|
+
} catch (checkErr) {
|
|
73
|
+
result.hasConflict = true;
|
|
74
|
+
const stderr = String(checkErr.stderr || checkErr.message || '');
|
|
75
|
+
const files = [...stderr.matchAll(/error:\s*patch failed:\s*([^:\r\n]+)/g)].map(m => m[1].trim());
|
|
76
|
+
result.conflictingFiles = files.length ? [...new Set(files)] : [];
|
|
77
|
+
result.conflictReason = stderr.slice(0, 500);
|
|
78
|
+
} finally {
|
|
79
|
+
await fs.rm(file, { force: true }).catch(() => {});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
66
83
|
}
|
|
67
84
|
|
|
68
85
|
async function applyWorkspace(workspace, expectedDigest) {
|
|
69
86
|
const review = await reviewWorkspace(workspace);
|
|
70
87
|
if (!expectedDigest || review.digest !== expectedDigest) throw new Error('子任务改动已变化,请重新审查');
|
|
71
88
|
if (!review.patch) return review;
|
|
89
|
+
if (review.hasConflict) {
|
|
90
|
+
const fileList = review.conflictingFiles.length
|
|
91
|
+
? review.conflictingFiles.map(f => ` - ${f}`).join('\n')
|
|
92
|
+
: ' (主工作区当前状态与补丁不兼容)';
|
|
93
|
+
throw new Error(
|
|
94
|
+
`无法应用工作区改动:检测到文件合并冲突。\n冲突文件:\n${fileList}\n` +
|
|
95
|
+
`建议操作:\n1. 可在子任务独立工作区 (${workspace.cwd}) 中手动合并或解决冲突;\n` +
|
|
96
|
+
`2. 使用 pushWorkspace 将隔离分支 (${workspace.branch}) 推送至 Git 远程仓库以发起 PR;\n` +
|
|
97
|
+
`3. 或使用 discardWorkspace 放弃并删除该分支。`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
72
100
|
const file = path.join(os.tmpdir(), `harness-mix-patch-${randomUUID()}`);
|
|
73
101
|
try {
|
|
74
102
|
await fs.writeFile(file, review.patch);
|
|
75
|
-
await git(workspace.source, ['apply', '--check', '--binary', file]);
|
|
76
103
|
await git(workspace.source, ['apply', '--binary', file]);
|
|
77
|
-
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
throw new Error(`应用改动失败:${error.stderr || error.message}`);
|
|
106
|
+
} finally { await fs.rm(file, { force: true }).catch(() => {}); }
|
|
78
107
|
return review;
|
|
79
108
|
}
|
|
80
109
|
|
|
@@ -89,4 +118,34 @@ async function removeWorkspace(workspace) {
|
|
|
89
118
|
await fs.rm(workspace.root, { recursive: true, force: true }).catch(() => {});
|
|
90
119
|
}
|
|
91
120
|
|
|
92
|
-
|
|
121
|
+
async function discardWorkspace(workspace) {
|
|
122
|
+
if (!workspace || workspace.mode !== 'worktree') return { discarded: false };
|
|
123
|
+
await removeWorkspace(workspace);
|
|
124
|
+
return { discarded: true, branch: workspace.branch };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function pushWorkspace(workspace, remote = 'origin', remoteBranch = workspace.branch) {
|
|
128
|
+
if (!workspace || workspace.mode !== 'worktree') throw new Error('仅隔离工作区支持推送远程分支');
|
|
129
|
+
const currentTree = await tree(workspace.root);
|
|
130
|
+
let parentCommit = workspace.baseCommit;
|
|
131
|
+
try {
|
|
132
|
+
parentCommit = (await git(workspace.source, ['rev-parse', workspace.branch])).trim();
|
|
133
|
+
} catch {}
|
|
134
|
+
const commitMsg = `Harness Mix collaboration: ${workspace.branch}`;
|
|
135
|
+
const newCommit = (await git(workspace.source, [
|
|
136
|
+
'-c', 'user.name=Harness Mix',
|
|
137
|
+
'-c', 'user.email=harness-mix@localhost',
|
|
138
|
+
'commit-tree', currentTree,
|
|
139
|
+
'-p', parentCommit,
|
|
140
|
+
'-m', commitMsg,
|
|
141
|
+
])).trim();
|
|
142
|
+
await git(workspace.source, ['update-ref', `refs/heads/${workspace.branch}`, newCommit]);
|
|
143
|
+
const remotes = (await git(workspace.source, ['remote'])).split(/\r?\n/).map(r => r.trim()).filter(Boolean);
|
|
144
|
+
if (!remotes.includes(remote)) {
|
|
145
|
+
throw new Error(`Git 远程 '${remote}' 不存在。可用远程:${remotes.join(', ') || '无'}`);
|
|
146
|
+
}
|
|
147
|
+
await git(workspace.source, ['push', remote, `${workspace.branch}:${remoteBranch}`]);
|
|
148
|
+
return { remote, branch: remoteBranch, commit: newCommit, pushed: true };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { createWorkspace, reviewWorkspace, applyWorkspace, removeWorkspace, discardWorkspace, pushWorkspace, git, tree };
|
|
@@ -4,7 +4,7 @@ const path = require('node:path');
|
|
|
4
4
|
const { tools } = require('./collaboration-tools');
|
|
5
5
|
const { z } = require('zod');
|
|
6
6
|
const { Store } = require('./store');
|
|
7
|
-
const { createWorkspace, reviewWorkspace, applyWorkspace } = require('./collaboration-worktree');
|
|
7
|
+
const { createWorkspace, reviewWorkspace, applyWorkspace, discardWorkspace, pushWorkspace } = require('./collaboration-worktree');
|
|
8
8
|
const validators = new Map(tools.map(tool => [tool.name, z.fromJSONSchema(tool.inputSchema)]));
|
|
9
9
|
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
10
10
|
|
|
@@ -58,6 +58,24 @@ class Collaboration {
|
|
|
58
58
|
} finally { delete job.applying; }
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
async discard(id) {
|
|
62
|
+
const job = this.jobs.get(id);
|
|
63
|
+
if (!job) throw new Error('子任务不存在');
|
|
64
|
+
if (job.status === 'running') throw new Error('子任务正在运行,请先取消');
|
|
65
|
+
const result = await discardWorkspace(job.workspace);
|
|
66
|
+
job.status = 'cancelled';
|
|
67
|
+
delete job.workspace;
|
|
68
|
+
await this.save();
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async push(id, { remote = 'origin', branch } = {}) {
|
|
73
|
+
const job = this.jobs.get(id);
|
|
74
|
+
if (!job) throw new Error('子任务不存在');
|
|
75
|
+
if (job.status === 'running') throw new Error('请等待子任务完成后再推送分支');
|
|
76
|
+
return pushWorkspace(job.workspace, remote, branch);
|
|
77
|
+
}
|
|
78
|
+
|
|
61
79
|
async connection(thread) {
|
|
62
80
|
await this.initialize();
|
|
63
81
|
if (!this.starting) this.starting = new Promise((resolve, reject) => {
|
|
@@ -94,7 +112,8 @@ class Collaboration {
|
|
|
94
112
|
const pending = job.childId ? this.runtime.core.interactions?.pending(job.childId)?.[0] : null;
|
|
95
113
|
return { task_id: job.id, parent_thread_id: job.owner, child_thread_id: job.childId, agent_type: job.agent, status: job.status,
|
|
96
114
|
display_status: pending ? 'waiting_approval' : job.status, attention: pending ? { type: pending.type, title: pending.title, message: pending.message } : undefined,
|
|
97
|
-
task: job.task, workspace: job.workspace, applied: !!job.appliedDigest, result: job.result, error: job.error
|
|
115
|
+
task: job.task, workspace: job.workspace, applied: !!job.appliedDigest, result: job.result, error: job.error,
|
|
116
|
+
diff: job.diff, digest: job.digest, branch: job.workspace?.branch };
|
|
98
117
|
}
|
|
99
118
|
|
|
100
119
|
async call(owner, name, args) {
|
|
@@ -193,6 +212,13 @@ class Collaboration {
|
|
|
193
212
|
const messages = rt.core.getItemsForTurn(turn.id).filter(i => i.type === 'agent_message');
|
|
194
213
|
const finals = messages.filter(i => i.phase === 'final');
|
|
195
214
|
job.result = (finals.length ? finals : messages).map(i => i.content || '').join('\n').slice(0, 48000);
|
|
215
|
+
if (job.workspace?.mode === 'worktree' && job.status === 'completed') {
|
|
216
|
+
try {
|
|
217
|
+
const rev = await reviewWorkspace(job.workspace);
|
|
218
|
+
job.diff = rev.patch;
|
|
219
|
+
job.digest = rev.digest;
|
|
220
|
+
} catch {}
|
|
221
|
+
}
|
|
196
222
|
} catch (error) { if (job.status === 'running') { job.status = 'failed'; job.error = error.message; } }
|
|
197
223
|
finally { await this.save(); emit({ kind: 'tool', toolCallId, state: job.status === 'completed' ? 'done' : 'error', output: JSON.stringify(this.view(job)) }); }
|
|
198
224
|
}
|
package/src/main/host/runtime.js
CHANGED
|
@@ -11,7 +11,7 @@ const { CoreSession } = require('./core-session');
|
|
|
11
11
|
const { Collaboration, mentionedAgents } = require('./collaboration');
|
|
12
12
|
const { SessionHistory } = require('./session-history');
|
|
13
13
|
const { buildHandoffContext, composeHandoffEnvelope } = require('./handoff');
|
|
14
|
-
const { createWorkspace, reviewWorkspace, applyWorkspace, removeWorkspace } = require('./collaboration-worktree');
|
|
14
|
+
const { createWorkspace, reviewWorkspace, applyWorkspace, removeWorkspace, discardWorkspace, pushWorkspace } = require('./collaboration-worktree');
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Host Runtime:harness-mix 的核心职责 —— 自研 Desktop 背后的
|
|
@@ -227,6 +227,27 @@ class HostRuntime {
|
|
|
227
227
|
return result;
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
async discardThreadWorkspace(threadId) {
|
|
231
|
+
const thread = this.#requireThread(threadId);
|
|
232
|
+
if (thread.workspace?.mode !== 'worktree') throw new Error('该任务未使用 Worktree 隔离工作区');
|
|
233
|
+
if (this.execution.isRunning(thread.id) || thread.reviewPending) throw new Error('请等待任务完成后再丢弃隔离分支');
|
|
234
|
+
const result = await discardWorkspace(thread.workspace);
|
|
235
|
+
delete thread.workspace;
|
|
236
|
+
this.#notify('status', `已丢弃并删除隔离分支 ${result.branch || ''}`, thread.id);
|
|
237
|
+
await this.#save();
|
|
238
|
+
this.#broadcast();
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async pushThreadWorkspace(threadId, { remote = 'origin', branch } = {}) {
|
|
243
|
+
const thread = this.#requireThread(threadId);
|
|
244
|
+
if (thread.workspace?.mode !== 'worktree') throw new Error('该任务未使用 Worktree 隔离工作区');
|
|
245
|
+
if (this.execution.isRunning(thread.id) || thread.reviewPending) throw new Error('请等待任务完成后再推送分支');
|
|
246
|
+
const result = await pushWorkspace(thread.workspace, remote, branch);
|
|
247
|
+
this.#notify('status', `已成功将分支 ${result.branch} 推送到远程 ${result.remote}`, thread.id);
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
|
|
230
251
|
async send(threadId, text, { commandId, attachments, delegateOf, collaborationOf, isolated = false } = {}) {
|
|
231
252
|
// Reserve before opening a native session: two submissions can otherwise both
|
|
232
253
|
// pass isRunning() while awaiting the same opening promise.
|