@harness-mix/cli 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +26 -96
  3. package/docs/chatgpt-web-sidebar.md +7 -1
  4. package/docs/cross-platform.md +15 -0
  5. package/docs/harness-handoff-design.md +377 -0
  6. package/docs/harness-management.md +28 -1
  7. package/docs/multi-agent-collaboration.md +9 -9
  8. package/docs/native-codex.md +21 -10
  9. package/output/native-build/harness-mix-shim.exe +0 -0
  10. package/output/native-build/renderer-extension.js +2596 -135
  11. package/package.json +20 -10
  12. package/scripts/chatgpt-context-ui-smoke.cjs +16 -0
  13. package/scripts/codex-accounts-test.cjs +81 -0
  14. package/scripts/codex-accounts-ui-smoke.cjs +70 -0
  15. package/scripts/collaboration-test.cjs +8 -3
  16. package/scripts/collaboration-ui-smoke.cjs +40 -26
  17. package/scripts/e2e-integrations.cjs +52 -0
  18. package/scripts/e2e-native-host.cjs +39 -0
  19. package/scripts/handoff-checkpoint-test.cjs +83 -0
  20. package/scripts/install-native.cjs +4 -1
  21. package/scripts/integrations-test.cjs +123 -0
  22. package/scripts/integrations-ui-smoke.cjs +124 -0
  23. package/scripts/native-protocol-test.cjs +180 -5
  24. package/scripts/native-secret-test.cjs +2 -2
  25. package/scripts/native-shim-test.cjs +20 -4
  26. package/scripts/native-sidebar-test.cjs +7 -0
  27. package/scripts/native-skill-roots-test.cjs +90 -0
  28. package/scripts/native-ui-smoke.cjs +58 -2
  29. package/scripts/publish-native-package.cjs +8 -5
  30. package/scripts/support/integrations-mcp-fixture.cjs +15 -0
  31. package/scripts/switch-harness-test.cjs +27 -1
  32. package/scripts/test-live-mentions.cjs +152 -0
  33. package/src/main/adapters/acp.js +3 -3
  34. package/src/main/adapters/antigravity.js +17 -17
  35. package/src/main/adapters/claude.js +50 -10
  36. package/src/main/adapters/codebuddy.js +1 -0
  37. package/src/main/adapters/codex-app-server.js +20 -13
  38. package/src/main/adapters/codex.js +25 -5
  39. package/src/main/adapters/cursor.js +5 -1
  40. package/src/main/adapters/dsh.js +9 -1
  41. package/src/main/adapters/grok.js +7 -3
  42. package/src/main/adapters/hermes.js +8 -0
  43. package/src/main/adapters/kiro.js +2 -1
  44. package/src/main/adapters/managed-mcp.js +25 -0
  45. package/src/main/adapters/native-acp.js +4 -4
  46. package/src/main/adapters/omp.js +11 -0
  47. package/src/main/adapters/openclaw.js +6 -0
  48. package/src/main/adapters/opencode.js +13 -3
  49. package/src/main/adapters/pi.js +5 -0
  50. package/src/main/adapters/qoder.js +1 -0
  51. package/src/main/adapters/trae.js +4 -0
  52. package/src/main/adapters/zcode.js +3 -0
  53. package/src/main/harness-adapter/manifest.js +17 -2
  54. package/src/main/host/collaboration.js +57 -11
  55. package/src/main/host/handoff-access.js +53 -0
  56. package/src/main/host/handoff-checkpoints.js +206 -0
  57. package/src/main/host/handoff-mcp.cjs +38 -0
  58. package/src/main/host/handoff-tools.js +13 -0
  59. package/src/main/host/handoff.js +28 -6
  60. package/src/main/host/integrations.js +339 -0
  61. package/src/main/host/runtime.js +95 -15
  62. package/src/main/host/store.js +4 -4
  63. package/src/main/native/codex-accounts.js +260 -0
  64. package/src/main/native/host.js +1 -1
  65. package/src/main/native/launcher.js +2 -2
  66. package/src/main/native/protocol.js +176 -14
  67. package/src/main/native/redact.js +1 -1
  68. package/src/main/native/rs/crates/shim/src/main.rs +155 -12
  69. package/src/main/native/thread-list.js +3 -0
  70. package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
  71. package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts.map +1 -1
  72. package/src/native-ui/renderer-extension/dist/types/index.d.ts +4 -0
  73. package/src/native-ui/renderer-extension/dist/types/index.d.ts.map +1 -1
  74. package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts +2 -0
  75. package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts.map +1 -1
  76. package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
  77. package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts +36 -0
  78. package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts.map +1 -0
  79. package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts +1 -0
  80. package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts.map +1 -1
  81. package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts +3 -1
  82. package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts.map +1 -1
  83. package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts +53 -0
  84. package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts.map +1 -0
  85. package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts +11 -0
  86. package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts.map +1 -1
  87. package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts +105 -0
  88. package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts.map +1 -0
  89. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +7 -1
  90. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
  91. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +1 -0
  92. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
  93. package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts +2 -0
  94. package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts.map +1 -1
  95. package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts +1 -0
  96. package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts.map +1 -1
  97. package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts +6 -0
  98. package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts.map +1 -0
  99. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts +5 -0
  100. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
  101. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +8 -4
  102. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
  103. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  104. package/src/native-ui/renderer-extension/src/harness-mix-settings.ts +4 -1
  105. package/src/native-ui/renderer-extension/src/index.ts +28 -0
  106. package/src/native-ui/renderer-extension/src/renderer-agent-icon.ts +4 -4
  107. package/src/native-ui/renderer-extension/src/renderer-agent-picker.ts +46 -6
  108. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +115 -18
  109. package/src/native-ui/renderer-extension/src/renderer-chatgpt-context.ts +87 -0
  110. package/src/native-ui/renderer-extension/src/renderer-codex-account-state.ts +10 -0
  111. package/src/native-ui/renderer-extension/src/renderer-composer-dom.ts +10 -0
  112. package/src/native-ui/renderer-extension/src/renderer-harness-handoff.ts +330 -0
  113. package/src/native-ui/renderer-extension/src/renderer-harness-mentions.ts +556 -123
  114. package/src/native-ui/renderer-extension/src/renderer-integrations-client.ts +59 -0
  115. package/src/native-ui/renderer-extension/src/renderer-model-client.ts +17 -1
  116. package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +2 -0
  117. package/src/native-ui/renderer-extension/src/settings/accounts-list.ts +16 -1
  118. package/src/native-ui/renderer-extension/src/settings/accounts-page.ts +32 -3
  119. package/src/native-ui/renderer-extension/src/settings/integrations-page.ts +1474 -0
  120. package/src/native-ui/renderer-extension/src/settings/localization.ts +32 -13
  121. package/src/native-ui/renderer-extension/src/settings/pages.ts +49 -18
  122. package/src/native-ui/renderer-extension/src/settings/shell.css +103 -0
  123. package/src/native-ui/renderer-extension/test/renderer-agent-picker.test.ts +44 -13
  124. package/src/native-ui/renderer-extension/test/renderer-chatgpt-context.test.ts +21 -0
  125. package/src/native-ui/renderer-extension/test/renderer-codex-account-state.test.ts +13 -1
  126. package/src/native-ui/renderer-extension/test/renderer-harness-handoff.test.ts +20 -0
  127. package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +9 -2
  128. package/src/native-ui/renderer-extension/test/settings/localization.test.ts +1 -1
  129. package/src/native-ui/renderer-extension/test/settings/pages.test.ts +77 -8
  130. package/src/native-ui/renderer-extension/test/settings/shell.test.ts +6 -0
  131. package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts +19 -4
  132. package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts.map +1 -1
  133. package/src/native-ui/shared-contracts/dist/codex-accounts.js +2 -0
  134. package/src/native-ui/shared-contracts/dist/codex-accounts.js.map +1 -1
  135. package/src/native-ui/shared-contracts/dist/index.d.ts +2 -2
  136. package/src/native-ui/shared-contracts/dist/index.d.ts.map +1 -1
  137. package/src/native-ui/shared-contracts/dist/index.js +1 -1
  138. package/src/native-ui/shared-contracts/dist/index.js.map +1 -1
  139. package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts +29 -0
  140. package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts.map +1 -1
  141. package/src/native-ui/shared-contracts/dist/thread-harness-switch.js +13 -0
  142. package/src/native-ui/shared-contracts/dist/thread-harness-switch.js.map +1 -1
  143. package/src/native-ui/shared-contracts/dist/tsconfig.tsbuildinfo +1 -1
  144. package/src/native-ui/shared-contracts/src/codex-accounts.ts +2 -0
  145. package/src/native-ui/shared-contracts/src/index.ts +8 -1
  146. package/src/native-ui/shared-contracts/src/thread-harness-switch.ts +17 -0
@@ -0,0 +1,90 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs/promises');
3
+ const os = require('node:os');
4
+ const path = require('node:path');
5
+ const { ParityObserver } = require('./support/parity-observer.cjs');
6
+ const { HostRuntime } = require('../src/main/host/runtime');
7
+
8
+ // 用户可见契约:打开原生会话时,先为每个 Harness 创建缺失的原生技能根目录,
9
+ // 这样刚安装、从未运行过的 Harness 也能立即被 Harness Mix 发现并安装技能。
10
+ // 单个目录创建失败只提示、不阻断会话;项目作用域不可用时退化为全局根目录。
11
+
12
+ const skills = { global: ['.test-harness/skills'], project: ['.agents/skills'] };
13
+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
14
+ async function until(fn) { for (let i = 0; i < 200; i++) { if (fn()) return; await sleep(10); } throw new Error('timed out'); }
15
+
16
+ function stubAdapter(id, onOpen) {
17
+ return {
18
+ manifest: { id, name: 'Test Harness', capabilities: {}, integrations: { mcp: false, skills: JSON.parse(JSON.stringify(skills)) } },
19
+ async open(input) { onOpen?.(input); return {}; },
20
+ async send() {}, async cancel() {}, async respond() {}, async close() {},
21
+ };
22
+ }
23
+
24
+ async function main() {
25
+ const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'hm-skill-roots-'));
26
+ const workspace = path.join(directory, 'workspace');
27
+ const home = path.join(directory, 'home');
28
+ await fs.mkdir(workspace); await fs.mkdir(home);
29
+
30
+ const rt = new HostRuntime({ observer: new ParityObserver(), dataDirectory: directory });
31
+ await rt.store.load();
32
+ rt.integrations.home = home;
33
+ rt.integrations.environment = {};
34
+
35
+ // 1) 缺失的技能目录在会话打开时被创建(全局 + 项目)。
36
+ let opened = 0, emit = null;
37
+ const adapter = stubAdapter('test-harness', input => { opened++; emit = input.emit; });
38
+ rt.adapters.set(adapter.manifest.id, adapter);
39
+ rt.status[adapter.manifest.id] = { available: true };
40
+ const thread = await rt.createThread({ harnessId: adapter.manifest.id, cwd: workspace });
41
+ const toasts = [];
42
+ rt.subscribe(event => { if (event.type === 'toast') toasts.push(event); });
43
+ await rt.send(thread.id, 'hello');
44
+ assert.equal(opened, 1, 'native session opened');
45
+ for (const root of [path.join(home, '.test-harness', 'skills'), path.join(workspace, '.agents', 'skills')]) {
46
+ assert.equal((await fs.stat(root)).isDirectory(), true, `${root} created on launch`);
47
+ }
48
+ assert.deepEqual(toasts, [], 'a successful launch reports no skill-root failure');
49
+ emit({ kind: 'completed', finalAnswer: true });
50
+ await until(() => thread.status === 'ready' && !thread.reviewPending);
51
+
52
+ // 2) 已存在的根目录被复用,重复打开不会失败。
53
+ await fs.mkdir(path.join(home, '.test-harness', 'skills', 'existing'), { recursive: true });
54
+ await rt.send(thread.id, 'again');
55
+ assert.equal(opened, 1, 'same thread reuses the native session');
56
+ emit({ kind: 'completed', finalAnswer: true });
57
+ await until(() => thread.status === 'ready' && !thread.reviewPending);
58
+ assert.equal((await fs.stat(path.join(home, '.test-harness', 'skills', 'existing'))).isDirectory(), true, 'existing skill content untouched');
59
+
60
+ // 3) 无法创建的根目录只提示并跳过,会话照常打开。
61
+ const blockedHome = path.join(directory, 'blocked-home');
62
+ await fs.mkdir(path.join(blockedHome, '.test-harness'), { recursive: true });
63
+ await fs.writeFile(path.join(blockedHome, '.test-harness', 'skills'), 'not a directory');
64
+ const blockedWorkspace = path.join(directory, 'blocked-workspace');
65
+ await fs.mkdir(blockedWorkspace);
66
+ const blocked = new HostRuntime({ observer: new ParityObserver(), dataDirectory: path.join(directory, 'blocked-data') });
67
+ await blocked.store.load();
68
+ blocked.integrations.home = blockedHome;
69
+ blocked.integrations.environment = {};
70
+ let blockedOpened = 0;
71
+ const blockedAdapter = stubAdapter('test-harness', () => { blockedOpened++; });
72
+ blocked.adapters.set(blockedAdapter.manifest.id, blockedAdapter);
73
+ blocked.status[blockedAdapter.manifest.id] = { available: true };
74
+ const blockedThread = await blocked.createThread({ harnessId: blockedAdapter.manifest.id, cwd: blockedWorkspace });
75
+ const blockedToasts = [];
76
+ console.log('DBG blocked.integrations.home =', blocked.integrations.home);
77
+ console.log('DBG global root stat type =', (await fs.lstat(path.join(blockedHome, '.test-harness', 'skills'))).isFile() ? 'FILE' : 'other');
78
+ blocked.subscribe(event => { if (event.type === 'toast') blockedToasts.push(event); });
79
+ await blocked.send(blockedThread.id, 'hello');
80
+ assert.equal(blockedOpened, 1, 'session still opens when a skill root cannot be created');
81
+ assert.equal(blocked.sessions.has(blockedThread.id), true, 'native session attached despite the uncreatable root');
82
+ assert.equal(blockedThread.error, undefined, 'no session error from the uncreatable root');
83
+ assert.equal((await fs.stat(path.join(blockedWorkspace, '.agents', 'skills'))).isDirectory(), true, 'the remaining root is still created');
84
+ console.log('TOASTS:', JSON.stringify(blockedToasts.map(t => ({ type: t.type, level: t.level, text: String(t.text).slice(0, 140) }))));
85
+ assert.equal(blockedToasts.filter(event => /原生技能目录准备失败/.test(event.text)).length, 1, 'the skipped root is reported once');
86
+
87
+ console.log('native skill roots: created on session open, reused when present, and never fatal PASS');
88
+ await fs.rm(directory, { recursive: true, force: true });
89
+ }
90
+ main().catch(error => { console.error(error); process.exitCode = 1; });
@@ -12,7 +12,7 @@ app.whenReady().then(async () => {
12
12
  const icons = getAllIconsDictionary();
13
13
  icons.harnesses['claude-code'] = icons.harnesses.claude;
14
14
  icons.harnesses['deepseek-harness'] = icons.harnesses.dsh;
15
- const bundle = await esbuild.build({ stdin: { contents: `export { createRendererAgentIcon } from './src/native-ui/renderer-extension/src/renderer-agent-icon.ts'; export { mountRendererModelPicker, renderRendererModelPicker } from './src/native-ui/renderer-extension/src/renderer-model-picker.ts'; export { restoredThreadOwnership } from './src/native-ui/renderer-extension/src/renderer-binding-probe.ts'; export { installRendererSidebarAgentIcons } from './src/native-ui/renderer-extension/src/renderer-sidebar-agent-icons.ts';`, resolveDir: process.cwd() }, bundle: true, alias: { '@codexhost/shared-contracts': path.resolve('src/native-ui/shared-contracts/src/index.ts') }, platform: 'browser', format: 'iife', globalName: 'NativeUI', write: false, loader: { '.svg': 'dataurl', '.png': 'dataurl', '.css': 'text' }, logLevel: 'silent' });
15
+ const bundle = await esbuild.build({ stdin: { contents: `export { createRendererAgentIcon } from './src/native-ui/renderer-extension/src/renderer-agent-icon.ts'; export { mountRendererAgentPicker, renderRendererAgentPicker } from './src/native-ui/renderer-extension/src/renderer-agent-picker.ts'; export { mountRendererModelPicker, renderRendererModelPicker } from './src/native-ui/renderer-extension/src/renderer-model-picker.ts'; export { mountRendererHarnessHandoff } from './src/native-ui/renderer-extension/src/renderer-harness-handoff.ts'; export { restoredThreadOwnership } from './src/native-ui/renderer-extension/src/renderer-binding-probe.ts'; export { installRendererSidebarAgentIcons } from './src/native-ui/renderer-extension/src/renderer-sidebar-agent-icons.ts'; export { createAccountsSettingsPage } from './src/native-ui/renderer-extension/src/settings/accounts-page.ts'; export { aboutPage } from './src/native-ui/renderer-extension/src/settings/pages.ts'; export { rendererSettingsMessages } from './src/native-ui/renderer-extension/src/settings/localization.ts'; export { installRendererSettingsShell } from './src/native-ui/renderer-extension/src/settings/shell.ts';`, resolveDir: process.cwd() }, bundle: true, alias: { '@codexhost/shared-contracts': path.resolve('src/native-ui/shared-contracts/src/index.ts') }, platform: 'browser', format: 'iife', globalName: 'NativeUI', write: false, loader: { '.svg': 'dataurl', '.png': 'dataurl', '.css': 'text' }, logLevel: 'silent' });
16
16
  const win = new BrowserWindow({ show: false, width: 900, height: 650, webPreferences: { contextIsolation: true, sandbox: true, offscreen: true, backgroundThrottling: false } });
17
17
  await win.loadURL('data:text/html,<html><head><style>body{background:%2317191d;color:white;font:16px Arial;padding:40px}button{background:%23272a30;color:white;border:0;padding:12px}img{vertical-align:middle}main{display:flex;gap:25px;margin-bottom:40px}</style></head><body><h2>Harness Mix native components</h2><main></main></body></html>');
18
18
  await win.webContents.executeJavaScript(`globalThis.__HARNESS_MIX_ICONS__=${JSON.stringify(icons)};globalThis.__HARNESS_MIX_MODEL_FAMILIES__=${JSON.stringify(MODEL_FAMILIES.map(f => ({ id: f.id, pattern: f.regex.source })))};${bundle.outputFiles[0].text}`);
@@ -82,13 +82,69 @@ app.whenReady().then(async () => {
82
82
  assert.deepEqual(result.restored.map(item => item.agent), ['pi','claude-code','deepseek-harness','antigravity']);
83
83
  assert.deepEqual(result.restored.map(item => item.model), inspections.map(item => item.effectiveModel));
84
84
  for (const id of ['harnesses/codex','harnesses/pi','harnesses/claude-code','harnesses/deepseek-harness','harnesses/antigravity','models/claude','models/deepseek','models/gemini']) assert.ok(result.icons.includes(id), id);
85
+ const handoffView = await win.webContents.executeJavaScript(`(() => {
86
+ globalThis.handoffRequest = null;
87
+ globalThis.testHandoff = NativeUI.mountRendererHarnessHandoff('smoke', request => { globalThis.handoffRequest = request; });
88
+ globalThis.testAgentPicker = NativeUI.mountRendererAgentPicker('handoff-entry', ['codex','pi','claude-code'], target => testHandoff.open('pi', target, 'zh-CN'), () => {}, () => {});
89
+ document.body.append(testAgentPicker.root);
90
+ NativeUI.renderRendererAgentPicker(testAgentPicker, {agent:'pi',phase:'locked'}, 'ready', false, {pi:'ready','claude-code':'ready'});
91
+ testAgentPicker.trigger.click();
92
+ testAgentPicker.options['claude-code'].button.click();
93
+ testHandoff.note.value = '按上一位给出的方案继续执行';
94
+ return { open:testHandoff.dialog.open, title:testHandoff.dialog.querySelector('h2').textContent, text:testHandoff.dialog.textContent, pickerLabel:testAgentPicker.trigger.getAttribute('aria-label'), targetEnabled:!testAgentPicker.options['claude-code'].button.disabled };
95
+ })()`);
96
+ assert.equal(handoffView.open, true);
97
+ assert.equal(handoffView.title, '接力当前任务');
98
+ assert.match(handoffView.text, /Pi.*Claude Code/s);
99
+ assert.match(handoffView.text, /对话记录和文件现场/);
100
+ assert.match(handoffView.pickerLabel, /Hand off task from Pi/);
101
+ assert.equal(handoffView.targetEnabled, true);
102
+ await new Promise(resolve => setTimeout(resolve, 100));
103
+ fs.writeFileSync(path.join(out, 'harness-handoff.png'), (await win.webContents.capturePage()).toPNG());
104
+ const handoffRequest = await win.webContents.executeJavaScript(`(() => { testHandoff.confirm.click(); const request = globalThis.handoffRequest; testHandoff.close(); testHandoff.dispose(); return request; })()`);
105
+ assert.deepEqual(handoffRequest, {
106
+ from:'pi', to:'claude-code', note:'按上一位给出的方案继续执行', intent:'continue',
107
+ includes:{ conversation:true, plan:true, evidence:true, files:true, unresolved:true },
108
+ });
109
+ await win.webContents.executeJavaScript('testAgentPicker.dispose()');
85
110
  await win.webContents.executeJavaScript('testModelPicker.trigger.click();document.querySelector("main").style.flexWrap="wrap"');
86
111
  await new Promise(resolve => setTimeout(resolve, 300));
87
112
  fs.writeFileSync(path.join(out, 'native-components.png'), (await win.webContents.capturePage()).toPNG());
88
113
  await win.webContents.executeJavaScript(`document.body.style.background='#fff';document.body.style.color='#17191d';document.querySelector('main').style.flexWrap='wrap'`);
89
114
  await new Promise(resolve => setTimeout(resolve, 100));
90
115
  fs.writeFileSync(path.join(out, 'native-components-light.png'), (await win.webContents.capturePage()).toPNG());
116
+ const settingsResult = await win.webContents.executeJavaScript(`(async () => {
117
+ document.body.innerHTML = '';
118
+ const messages = NativeUI.rendererSettingsMessages('zh-CN');
119
+ const account = { accountId:'official-codex', label:'native@example.com', email:'native@example.com', planType:'plus', codexHome:'C:/Users/test/.codex', active:true, isDefault:true, authenticated:true, management:'native' };
120
+ const client = {
121
+ listCodexAccounts: async () => ({ accounts:[account] }),
122
+ refreshCodexAccounts: async () => ({ accounts:[account] }),
123
+ inspectCodexAccountUsage: async () => ({ accountId:account.accountId, usage:{planFiveHourUsedPercent:33,planSevenDayUsedPercent:29}, accountCredits:{usedPercent:33,periodType:'five_hour',productUsage:[{product:'7-day window',usagePercent:29}]} }),
124
+ createCodexAccount: async () => ({account}), deleteCodexAccount: async () => ({deletedAccountId:account.accountId}), activateCodexAccount: async () => ({account}),
125
+ startCodexAccountLogin: async () => { throw new Error('not expected'); }, cancelCodexAccountLogin: async () => ({cancelled:true}), logoutCodexAccount: async () => ({account:{...account,email:undefined,authenticated:false}}),
126
+ };
127
+ const shell = NativeUI.installRendererSettingsShell([
128
+ NativeUI.createAccountsSettingsPage(messages, () => client),
129
+ NativeUI.aboutPage(messages, () => ({ readCurrentVersion:async () => ({version:'0.1.4'}), checkUpdate:async () => { throw new Error('not expected'); } })),
130
+ ], messages, document);
131
+ shell.openSettings(undefined, 'accounts');
132
+ await new Promise(resolve => setTimeout(resolve, 150));
133
+ const shadow = shell.root.shadowRoot;
134
+ return { text:shadow.textContent, addHidden:[...shadow.querySelectorAll('button')].find(button => button.textContent.includes('添加账号'))?.hidden };
135
+ })()`);
136
+ assert.match(settingsResult.text, /native.*example\.com/s);
137
+ assert.match(settingsResult.text, /Plus/);
138
+ assert.equal(settingsResult.addHidden, true);
139
+ fs.writeFileSync(path.join(out, 'settings-account.png'), (await win.webContents.capturePage()).toPNG());
140
+ const aboutText = await win.webContents.executeJavaScript(`(async () => {
141
+ globalThis.__codexhostSettingsShellV1.openSettings(undefined, 'about');
142
+ await new Promise(resolve => setTimeout(resolve, 100));
143
+ return globalThis.__codexhostSettingsShellV1.root.shadowRoot.textContent;
144
+ })()`);
145
+ assert.match(aboutText, /v0\.1\.4/);
146
+ fs.writeFileSync(path.join(out, 'settings-about.png'), (await win.webContents.capturePage()).toPNG());
91
147
  win.destroy();
92
- console.log('PASS: real Electron native picker selection and all project Harness/model SVGs load');
148
+ console.log('PASS: real Electron native picker, graphical Harness handoff, official Account/Usage, live About version and all project Harness/model SVGs load');
93
149
  app.quit();
94
150
  }).catch(error => { console.error(error); app.exit(1); });
@@ -14,13 +14,16 @@ const pkg = path.join(work, 'package');
14
14
  fs.mkdirSync(path.join(pkg, 'bin'), { recursive: true });
15
15
  const version = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version;
16
16
  const names = platform === 'win32' ? ['harness-mix-shim.exe', 'harness-mix-appx.exe', 'harness-mix-secret.exe'] : ['harness-mix-shim'];
17
- for (const name of names) {
18
- const source = path.join(output, name);
19
- if (!fs.existsSync(source)) throw new Error(`Missing ${source}; build the target runtime first`);
20
- fs.copyFileSync(source, path.join(pkg, 'bin', name));
17
+ const sourceOnly = flags.includes('--source-only');
18
+ if (!sourceOnly) {
19
+ for (const name of names) {
20
+ const source = path.join(output, name);
21
+ if (!fs.existsSync(source)) throw new Error(`Missing ${source}; build the target runtime first (or use --source-only)`);
22
+ fs.copyFileSync(source, path.join(pkg, 'bin', name));
23
+ }
21
24
  }
22
25
  fs.writeFileSync(path.join(pkg, 'package.json'), JSON.stringify({
23
- name: `@harness-mix/native-${key}`, version, description: `Harness Mix native runtime for ${key}`,
26
+ name: `@harness-mix/native-${key}`, version, description: `Harness Mix native runtime for ${key}${sourceOnly ? ' (source-only; build locally)' : ''}`,
24
27
  license: 'Apache-2.0', os: [platform], cpu: [arch], files: ['bin'], publishConfig: { access: 'public' },
25
28
  }, null, 2) + '\n');
26
29
  fs.writeFileSync(path.join(pkg, 'README.md'), `# @harness-mix/native-${key}\n\nNative runtime selected automatically by @harness-mix/cli.\n`);
@@ -0,0 +1,15 @@
1
+ // Local read-only MCP server used by the native integration acceptance probe.
2
+ const readline = require('node:readline');
3
+ const fs = require('node:fs');
4
+ readline.createInterface({ input: process.stdin }).on('line', line => {
5
+ let request; try { request = JSON.parse(line); } catch { return; }
6
+ if (process.argv[2]) fs.appendFileSync(process.argv[2], `${request.method}\n`);
7
+ if (request.id === undefined) return;
8
+ let result;
9
+ if (request.method === 'initialize') result = { protocolVersion: request.params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: 'harness-mix-integration-probe', version: '1.0.0' } };
10
+ else if (request.method === 'tools/list') result = { tools: [{ name: 'integration_probe', description: 'Return a fixed test token without side effects.', inputSchema: { type: 'object', properties: {}, additionalProperties: false }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false } }] };
11
+ else if (request.method === 'tools/call') result = { content: [{ type: 'text', text: 'HARNESS_MIX_MCP_OK' }] };
12
+ else if (request.method === 'ping') result = {};
13
+ else { process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32601, message: 'Unknown method' } })}\n`); return; }
14
+ process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id: request.id, result })}\n`);
15
+ });
@@ -2,6 +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 { HostRuntime } = require('../src/main/host/runtime');
5
+ const { CAP, buildHandoffContext } = require('../src/main/host/handoff');
5
6
 
6
7
  /**
7
8
  * 跨 Harness 原地切换(switchHarness)脚本级测试:
@@ -33,6 +34,19 @@ function fakeHarness(id, name) {
33
34
  }
34
35
 
35
36
  async function main() {
37
+ const longSolution = `方案开头-${'x'.repeat(5000)}-方案结尾`;
38
+ const optimized = buildHandoffContext({
39
+ cwd: 'E:/project', title: '登录重构', messages: [
40
+ { role: 'user', text: '先分析问题' },
41
+ { role: 'assistant', text: '初步判断' },
42
+ { role: 'user', text: '给出完整方案' },
43
+ { role: 'assistant', text: longSolution },
44
+ ],
45
+ });
46
+ assert.deepEqual(optimized.conversationTail.map(message => message.role), ['user', 'assistant', 'user', 'assistant']);
47
+ assert.equal(optimized.conversationTail.at(-1).text, longSolution, '最新方案应获得更大的交接预算');
48
+ assert.ok(optimized.conversationTail.reduce((sum, message) => sum + message.text.length, 0) <= CAP.totalChars);
49
+
36
50
  await fs.mkdir('output', { recursive: true });
37
51
  const root = await fs.mkdtemp(path.resolve('output/switch-harness-'));
38
52
  const rt = new HostRuntime({ dataDirectory: path.join(root, 'data') });
@@ -71,6 +85,12 @@ async function main() {
71
85
  assert.equal(thread.harnessChain[0].nativeSessionId, aNativeId);
72
86
  assert.equal(thread.pendingHandoff.fromHarnessId, 'a');
73
87
  assert.equal(thread.pendingHandoff.note, '优先把测试补完');
88
+ assert.match(thread.pendingHandoff.checkpointId, /^handoff_[a-f0-9]{24}$/);
89
+ const firstCheckpointId = thread.pendingHandoff.checkpointId;
90
+ const checkpointBeforeSend = rt.handoffs.get(thread.id, firstCheckpointId);
91
+ assert.equal(checkpointBeforeSend.intent, 'continue');
92
+ assert.equal(checkpointBeforeSend.fileState.files.some(file => file.path === 'src/login.ts' && file.changeType === 'added'), true);
93
+ assert.equal(rt.handoffs.owned(thread.id, firstCheckpointId).status, 'checkpoint-created');
74
94
  assert.equal(thread.restore, false, '新 Harness 上是全新原生会话');
75
95
  assert.equal(rt.sessions.has(thread.id), false, '切换后旧原生会话已关闭');
76
96
  assert.equal(a.state.closed, 1);
@@ -87,11 +107,12 @@ async function main() {
87
107
  assert.match(b.state.sent[0], /untrusted historical data/);
88
108
  assert.match(b.state.sent[0], /帮我重构登录模块/);
89
109
  assert.match(b.state.sent[0], /Alpha 回复/);
90
- assert.match(b.state.sent[0], /added src\/login\.ts/);
110
+ assert.match(b.state.sent[0], /"path":"src\/login\.ts","changeType":"added"/);
91
111
  assert.match(b.state.sent[0], /优先把测试补完/);
92
112
  const visible = thread.messages.filter(m => m.role === 'user').at(-1);
93
113
  assert.equal(visible.text, '继续', '信封不进用户可见消息');
94
114
  assert.equal(thread.pendingHandoff, undefined, '信封发送成功后标记已清除');
115
+ assert.equal(rt.handoffs.owned(thread.id, firstCheckpointId).status, 'active');
95
116
 
96
117
  // 5) 第二轮起不再注入
97
118
  await rt.send(thread.id, '第二步');
@@ -123,6 +144,9 @@ async function main() {
123
144
  await settle(thread.id);
124
145
  await assert.rejects(rt.switchHarness(thread.id, 'a'), /已经在该 Harness/);
125
146
  await assert.rejects(rt.switchHarness(thread.id, 'Nope'), /未知 Harness/);
147
+ rt.status.b = { available: false, detail: '测试连接不可用' };
148
+ await assert.rejects(rt.switchHarness(thread.id, 'b'), /测试连接不可用/);
149
+ rt.status.b = { available: true };
126
150
  const draft = await rt.createThread({ harnessId: 'a', cwd: root, ephemeral: true });
127
151
  await assert.rejects(rt.switchHarness(draft.id, 'b'), /草稿任务/);
128
152
  const child = await rt.createThread({ harnessId: 'a', cwd: root, parentThreadId: thread.id });
@@ -138,6 +162,8 @@ async function main() {
138
162
  // 重启恢复的真正契约是持久化的 harnessId + nativeSessionId(指向 A 的原生会话)。
139
163
  assert.equal(persisted.nativeSessionId, aNativeId);
140
164
  assert.equal(persisted.restore, undefined);
165
+ const persistedCheckpoints = JSON.parse(await fs.readFile(path.join(root, 'data', 'handoff', 'checkpoints.json'), 'utf8'));
166
+ assert.equal(persistedCheckpoints.some(row => row.checkpointId === firstCheckpointId && row.status === 'active'), true);
141
167
 
142
168
  console.log('PASS: in-place harness switch → one-shot context envelope → switch-back native resume, guards and persistence');
143
169
  } finally { await rt.close(); }
@@ -0,0 +1,152 @@
1
+ const http = require('node:http');
2
+ const fs = require('node:fs/promises');
3
+ const path = require('node:path');
4
+
5
+ function getJson(url) {
6
+ return new Promise((resolve, reject) => {
7
+ http.get(url, res => {
8
+ let data = '';
9
+ res.on('data', c => (data += c));
10
+ res.on('end', () => { try { resolve(JSON.parse(data)); } catch (e) { reject(e); } });
11
+ }).on('error', reject);
12
+ });
13
+ }
14
+
15
+ async function main() {
16
+ const port = process.argv[2] || '57989';
17
+ const targets = await getJson(`http://127.0.0.1:${port}/json/list`);
18
+ const page = targets.find(t => t.type === 'page' && t.url === 'app://-/index.html');
19
+ if (!page) throw new Error('main page not found');
20
+ const ws = new WebSocket(page.webSocketDebuggerUrl);
21
+ await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; });
22
+
23
+ let id = 0;
24
+ const pending = new Map();
25
+ ws.onmessage = ev => {
26
+ const msg = JSON.parse(ev.data);
27
+ if (msg.id && pending.has(msg.id)) { pending.get(msg.id)(msg); pending.delete(msg.id); }
28
+ };
29
+ const send = (method, params) => new Promise(res => {
30
+ const i = ++id;
31
+ pending.set(i, res);
32
+ ws.send(JSON.stringify({ id: i, method, params }));
33
+ });
34
+
35
+ const evaluate = async expr => {
36
+ const result = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true });
37
+ if (result.result?.exceptionDetails) {
38
+ throw new Error(JSON.stringify(result.result.exceptionDetails));
39
+ }
40
+ return result.result?.result?.value;
41
+ };
42
+
43
+ console.log('Waiting for probe to mount...');
44
+ await evaluate(`(async () => {
45
+ for (let i = 0; i < 50; i++) {
46
+ if (window.__codexhostRendererBindingProbeV1 && document.querySelector('.ProseMirror')) break;
47
+ await new Promise(r => setTimeout(r, 100));
48
+ }
49
+ })()`);
50
+
51
+ const directClientTest = await evaluate(`(async () => {
52
+ try {
53
+ const probe = window.__codexhostRendererBindingProbeV1;
54
+ return {
55
+ hasProbe: !!probe,
56
+ probeKeys: probe ? Object.keys(probe) : []
57
+ };
58
+ } catch (e) {
59
+ return { error: e.message, stack: e.stack };
60
+ }
61
+ })()`);
62
+ console.log('Direct client test:', JSON.stringify(directClientTest, null, 2));
63
+
64
+ console.log('Focusing editor and positioning caret at end...');
65
+ await evaluate(`(() => {
66
+ const editor = document.querySelector('.ProseMirror');
67
+ if (!editor) return;
68
+ editor.focus();
69
+ const p = editor.querySelector('p') || editor;
70
+ const sel = window.getSelection();
71
+ const range = document.createRange();
72
+ range.selectNodeContents(p);
73
+ range.collapse(false);
74
+ sel?.removeAllRanges();
75
+ sel?.addRange(range);
76
+ })()`);
77
+
78
+ // Send input via CDP
79
+ console.log('Sending CDP input for #...');
80
+ await send('Input.insertText', { text: '#' });
81
+ await new Promise(r => setTimeout(r, 600));
82
+
83
+ const menuInfo = await evaluate(`(() => {
84
+ const menu = document.querySelector('[data-harness-mix-mentions]');
85
+ const tabs = [...document.querySelectorAll('[data-harness-mix-mentions] [role="tab"]')].map(t => t.textContent.trim());
86
+ const options = [...document.querySelectorAll('[data-harness-mix-mentions] [role="option"]')].map(opt => {
87
+ const name = opt.querySelector('span[style*="font-weight:600"]')?.textContent || opt.innerText;
88
+ const desc = opt.querySelector('span[style*="font-size:11px"]')?.textContent || '';
89
+ return { name, desc, disabled: opt.hasAttribute('disabled') };
90
+ });
91
+ return {
92
+ lastMentionDebug: window.__lastMentionDebug,
93
+ menuHidden: menu ? menu.hidden : true,
94
+ menuDisplay: menu ? menu.style.display : null,
95
+ menuRect: menu ? { left: menu.style.left, top: menu.style.top, bottom: menu.style.bottom } : null,
96
+ tabs,
97
+ optionCount: options.length,
98
+ sampleOptions: options.slice(0, 8)
99
+ };
100
+ })()`);
101
+ console.log('Menu state after typing #:', JSON.stringify(menuInfo, null, 2));
102
+
103
+ // Capture screenshot with popup open
104
+ const shot1 = await send('Page.captureScreenshot', { format: 'png' });
105
+ const data1 = shot1.result?.data || shot1.data;
106
+ if (data1) {
107
+ await fs.mkdir('output', { recursive: true });
108
+ await fs.writeFile(path.resolve('output/live-hash-mentions.png'), Buffer.from(data1, 'base64'));
109
+ console.log('Saved output/live-hash-mentions.png');
110
+ }
111
+
112
+ // 2. Select an agent (e.g. click first option) and verify badge styling
113
+ const state2 = await evaluate(`(async () => {
114
+ const firstOption = document.querySelector('[data-harness-mix-mentions] [role="option"]:not([disabled])');
115
+ if (firstOption) {
116
+ firstOption.click();
117
+ await new Promise(r => setTimeout(r, 200));
118
+ }
119
+ const badges = [...document.querySelectorAll('[data-harness-mix-selected-mentions] [data-harness-mix-mention-badge]')].map(b => ({
120
+ text: b.textContent.trim(),
121
+ title: b.title,
122
+ color: b.style.color
123
+ }));
124
+ const containerHasAttr = document.querySelector('[data-harness-mix-has-mentions="true"]') !== null;
125
+ return { badges, containerHasAttr };
126
+ })()`);
127
+ console.log('State after selecting agent:', JSON.stringify(state2, null, 2));
128
+
129
+ // Capture screenshot with badge in input box
130
+ const shot2 = await send('Page.captureScreenshot', { format: 'png' });
131
+ const data2 = shot2.result?.data || shot2.data;
132
+ if (data2) {
133
+ await fs.writeFile(path.resolve('output/live-hash-selected-badge.png'), Buffer.from(data2, 'base64'));
134
+ console.log('Saved output/live-hash-selected-badge.png');
135
+ }
136
+
137
+ // 3. Clear badge so we leave the UI clean
138
+ await evaluate(`(() => {
139
+ const badge = document.querySelector('[data-harness-mix-selected-mentions] [data-harness-mix-mention-badge]');
140
+ if (badge) badge.click();
141
+ const editor = document.querySelector('[contenteditable="true"], .ProseMirror, textarea');
142
+ if (editor && !editor.value) editor.textContent = '';
143
+ })()`);
144
+
145
+ ws.close();
146
+ console.log('Done!');
147
+ }
148
+
149
+ main().catch(err => {
150
+ console.error(err);
151
+ process.exit(1);
152
+ });
@@ -53,11 +53,10 @@ function acpAdapter({ id, name, bin, args, executable = false, images = false, f
53
53
  }
54
54
  },
55
55
  async describe() { return { models: null, thinkingLevels: [], permissionModes: [] }; },
56
- async open({ thread, emit, diagnostic = () => {}, collaboration }) {
56
+ async open({ thread, emit, diagnostic = () => {}, collaboration, managedMcp = [] }) {
57
57
  const cli = command(args);
58
58
  const session = { cwd: thread.cwd, nativeSessionId: null, collaborationEnabled: !!collaboration,
59
- mcpServers: collaboration ? [{ name: 'harness-mix', command: collaboration.command, args: collaboration.args,
60
- env: Object.entries(collaboration.env).map(([name, value]) => ({ name, value })) }] : [],
59
+ mcpServers: require('./managed-mcp').acpServers(managedMcp, collaboration),
61
60
  state: { configOptions: [], models: null, modes: null, commands: [], usage: undefined, loading: true }, pendingApprovals: new Map(), tools: new Map(), emit };
62
61
  session.process = new JsonlProcess(cli.command, cli.args, { cwd: thread.cwd }, {
63
62
  onDiagnostic: diagnostic,
@@ -186,6 +185,7 @@ function acpAdapter({ id, name, bin, args, executable = false, images = false, f
186
185
  };
187
186
  return adapter;
188
187
  }
188
+ manifest.integrations = { mcp: true };
189
189
  return { manifest, create };
190
190
  }
191
191
  module.exports = { acpAdapter, catalog };
@@ -639,27 +639,21 @@ class QuestionBridge {
639
639
  CODEXHOST_AGY_QUESTION_CLIENT: `"${clientPath.replaceAll('\\', '/')}"`,
640
640
  };
641
641
 
642
- if (options.collaboration) {
642
+ if (options.collaboration || options.managedMcp?.length) {
643
643
  const pluginDir = path.join(dir, '.agents', 'plugins', 'harness-mix');
644
644
  const plainPluginDir = path.join(dir, 'plugins', 'harness-mix');
645
645
  await fs.promises.mkdir(pluginDir, { recursive: true });
646
646
  await fs.promises.mkdir(plainPluginDir, { recursive: true });
647
647
  const pluginManifest = JSON.stringify({ name: 'harness-mix-collaboration', description: 'Harness Mix Multi-agent Collaboration' }, null, 2);
648
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
- },
649
+ mcpServers: require('./managed-mcp').namedServers(options.managedMcp, options.collaboration),
656
650
  }, null, 2);
657
651
  await fs.promises.writeFile(path.join(pluginDir, 'plugin.json'), pluginManifest, 'utf8');
658
652
  await fs.promises.writeFile(path.join(pluginDir, 'mcp_config.json'), mcpConfig, 'utf8');
659
653
  await fs.promises.writeFile(path.join(plainPluginDir, 'plugin.json'), pluginManifest, 'utf8');
660
654
  await fs.promises.writeFile(path.join(plainPluginDir, 'mcp_config.json'), mcpConfig, 'utf8');
661
655
  await fs.promises.writeFile(path.join(dir, '.agents', 'mcp_config.json'), mcpConfig, 'utf8');
662
- Object.assign(bridge.environment, options.collaboration.env || {});
656
+ Object.assign(bridge.environment, options.collaboration?.env || {});
663
657
  }
664
658
 
665
659
  return bridge;
@@ -823,7 +817,7 @@ function create(emit, options = {}) {
823
817
  });
824
818
  },
825
819
 
826
- async open({ thread, emit: emitEvent, diagnostic, collaboration }) {
820
+ async open({ thread, emit: emitEvent, diagnostic, collaboration, managedMcp = [] }) {
827
821
  // Do not start a second agy process while a real turn may be starting.
828
822
  // Quota is loaded only by the explicit account/usage refresh path below.
829
823
  await restoreLegacyClipboardAttachments(thread);
@@ -837,6 +831,7 @@ function create(emit, options = {}) {
837
831
  thinkingLevel,
838
832
  permissionMode,
839
833
  collaboration: collaboration || null,
834
+ managedMcp,
840
835
  collaborationEnabled: !!collaboration,
841
836
  activeTurn: null,
842
837
  bridge: null,
@@ -859,13 +854,11 @@ function create(emit, options = {}) {
859
854
  approvals,
860
855
  emit: emitEvent,
861
856
  collaboration: session.collaboration,
857
+ managedMcp: session.managedMcp,
862
858
  });
863
859
  session.bridge = bridge;
864
860
 
865
861
  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
- }
869
862
  const { imageEntries, extraDirs } = await prepareImageAttachments(attachments?.images, session.cwd);
870
863
  if (imageEntries.length) {
871
864
  const imageNotice = [
@@ -1215,15 +1208,18 @@ function create(emit, options = {}) {
1215
1208
  },
1216
1209
 
1217
1210
  async cancel(session) {
1218
- if (session.activeTurn?.child) {
1219
- void terminateTree(session.activeTurn.child.pid);
1211
+ if (session.activeTurn) {
1212
+ session.activeTurn.completed = true;
1213
+ if (session.activeTurn.child) {
1214
+ void terminateTree(session.activeTurn.child.pid);
1215
+ }
1216
+ session.activeTurn.resolve?.();
1217
+ session.activeTurn = null;
1220
1218
  }
1221
1219
  if (session.bridge) {
1222
1220
  await session.bridge.dispose().catch(() => {});
1223
1221
  session.bridge = null;
1224
1222
  }
1225
- session.activeTurn?.resolve?.();
1226
- session.activeTurn = null;
1227
1223
  },
1228
1224
 
1229
1225
  async respond(session, requestId, response) {
@@ -1349,6 +1345,10 @@ function create(emit, options = {}) {
1349
1345
  };
1350
1346
  }
1351
1347
 
1348
+ // https://antigravity.google/docs/skills/#where-skills-live
1349
+ // Single assignment: an earlier declaration used to be silently overwritten here,
1350
+ // which dropped Antigravity's native skill roots from the manifest.
1351
+ manifest.integrations = { mcp: true, skills: { global: ['.gemini/config/skills'], project: ['.agents/skills'] } };
1352
1352
  module.exports = {
1353
1353
  manifest,
1354
1354
  create,