@harness-mix/cli 0.1.4 → 0.1.5

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 (126) 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/native-codex.md +21 -10
  8. package/output/native-build/harness-mix-shim.exe +0 -0
  9. package/output/native-build/renderer-extension.js +972 -35
  10. package/package.json +18 -9
  11. package/scripts/chatgpt-context-ui-smoke.cjs +16 -0
  12. package/scripts/codex-accounts-test.cjs +81 -0
  13. package/scripts/codex-accounts-ui-smoke.cjs +70 -0
  14. package/scripts/e2e-integrations.cjs +52 -0
  15. package/scripts/e2e-native-host.cjs +39 -0
  16. package/scripts/handoff-checkpoint-test.cjs +83 -0
  17. package/scripts/install-native.cjs +4 -1
  18. package/scripts/integrations-test.cjs +64 -0
  19. package/scripts/integrations-ui-smoke.cjs +63 -0
  20. package/scripts/native-protocol-test.cjs +78 -4
  21. package/scripts/native-secret-test.cjs +2 -2
  22. package/scripts/native-shim-test.cjs +20 -4
  23. package/scripts/native-ui-smoke.cjs +58 -2
  24. package/scripts/publish-native-package.cjs +8 -5
  25. package/scripts/support/integrations-mcp-fixture.cjs +15 -0
  26. package/scripts/switch-harness-test.cjs +27 -1
  27. package/src/main/adapters/acp.js +3 -3
  28. package/src/main/adapters/antigravity.js +10 -13
  29. package/src/main/adapters/claude.js +10 -3
  30. package/src/main/adapters/codex-app-server.js +20 -13
  31. package/src/main/adapters/codex.js +6 -4
  32. package/src/main/adapters/dsh.js +2 -1
  33. package/src/main/adapters/grok.js +4 -3
  34. package/src/main/adapters/managed-mcp.js +12 -0
  35. package/src/main/adapters/native-acp.js +4 -4
  36. package/src/main/adapters/opencode.js +9 -3
  37. package/src/main/adapters/pi.js +1 -0
  38. package/src/main/harness-adapter/manifest.js +17 -2
  39. package/src/main/host/handoff-access.js +53 -0
  40. package/src/main/host/handoff-checkpoints.js +206 -0
  41. package/src/main/host/handoff-mcp.cjs +38 -0
  42. package/src/main/host/handoff-tools.js +13 -0
  43. package/src/main/host/handoff.js +28 -6
  44. package/src/main/host/integrations.js +201 -0
  45. package/src/main/host/runtime.js +75 -35
  46. package/src/main/host/store.js +4 -4
  47. package/src/main/native/codex-accounts.js +260 -0
  48. package/src/main/native/host.js +1 -1
  49. package/src/main/native/launcher.js +2 -2
  50. package/src/main/native/protocol.js +74 -23
  51. package/src/main/native/redact.js +1 -1
  52. package/src/main/native/rs/crates/shim/src/main.rs +155 -12
  53. package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
  54. package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts.map +1 -1
  55. package/src/native-ui/renderer-extension/dist/types/index.d.ts +4 -0
  56. package/src/native-ui/renderer-extension/dist/types/index.d.ts.map +1 -1
  57. package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts +2 -0
  58. package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts.map +1 -1
  59. package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
  60. package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts +36 -0
  61. package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts.map +1 -0
  62. package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts +1 -0
  63. package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts.map +1 -1
  64. package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts +3 -1
  65. package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts.map +1 -1
  66. package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts +53 -0
  67. package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts.map +1 -0
  68. package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts +80 -0
  69. package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts.map +1 -0
  70. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +7 -1
  71. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
  72. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +1 -0
  73. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
  74. package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts +2 -0
  75. package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts.map +1 -1
  76. package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts +1 -0
  77. package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts.map +1 -1
  78. package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts +5 -0
  79. package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts.map +1 -0
  80. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts +5 -0
  81. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
  82. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +8 -4
  83. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
  84. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  85. package/src/native-ui/renderer-extension/src/harness-mix-settings.ts +3 -1
  86. package/src/native-ui/renderer-extension/src/index.ts +28 -0
  87. package/src/native-ui/renderer-extension/src/renderer-agent-icon.ts +4 -4
  88. package/src/native-ui/renderer-extension/src/renderer-agent-picker.ts +46 -6
  89. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +159 -40
  90. package/src/native-ui/renderer-extension/src/renderer-chatgpt-context.ts +83 -0
  91. package/src/native-ui/renderer-extension/src/renderer-codex-account-state.ts +10 -0
  92. package/src/native-ui/renderer-extension/src/renderer-composer-dom.ts +10 -0
  93. package/src/native-ui/renderer-extension/src/renderer-harness-handoff.ts +330 -0
  94. package/src/native-ui/renderer-extension/src/renderer-integrations-client.ts +26 -0
  95. package/src/native-ui/renderer-extension/src/renderer-model-client.ts +17 -1
  96. package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +2 -0
  97. package/src/native-ui/renderer-extension/src/settings/accounts-list.ts +16 -1
  98. package/src/native-ui/renderer-extension/src/settings/accounts-page.ts +32 -3
  99. package/src/native-ui/renderer-extension/src/settings/integrations-page.ts +102 -0
  100. package/src/native-ui/renderer-extension/src/settings/localization.ts +30 -13
  101. package/src/native-ui/renderer-extension/src/settings/pages.ts +47 -18
  102. package/src/native-ui/renderer-extension/src/settings/shell.css +13 -0
  103. package/src/native-ui/renderer-extension/test/renderer-agent-picker.test.ts +44 -13
  104. package/src/native-ui/renderer-extension/test/renderer-chatgpt-context.test.ts +18 -0
  105. package/src/native-ui/renderer-extension/test/renderer-codex-account-state.test.ts +13 -1
  106. package/src/native-ui/renderer-extension/test/renderer-harness-handoff.test.ts +20 -0
  107. package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +9 -2
  108. package/src/native-ui/renderer-extension/test/settings/localization.test.ts +1 -1
  109. package/src/native-ui/renderer-extension/test/settings/pages.test.ts +77 -8
  110. package/src/native-ui/renderer-extension/test/settings/shell.test.ts +3 -0
  111. package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts +19 -4
  112. package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts.map +1 -1
  113. package/src/native-ui/shared-contracts/dist/codex-accounts.js +2 -0
  114. package/src/native-ui/shared-contracts/dist/codex-accounts.js.map +1 -1
  115. package/src/native-ui/shared-contracts/dist/index.d.ts +2 -2
  116. package/src/native-ui/shared-contracts/dist/index.d.ts.map +1 -1
  117. package/src/native-ui/shared-contracts/dist/index.js +1 -1
  118. package/src/native-ui/shared-contracts/dist/index.js.map +1 -1
  119. package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts +29 -0
  120. package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts.map +1 -1
  121. package/src/native-ui/shared-contracts/dist/thread-harness-switch.js +13 -0
  122. package/src/native-ui/shared-contracts/dist/thread-harness-switch.js.map +1 -1
  123. package/src/native-ui/shared-contracts/dist/tsconfig.tsbuildinfo +1 -1
  124. package/src/native-ui/shared-contracts/src/codex-accounts.ts +2 -0
  125. package/src/native-ui/shared-contracts/src/index.ts +8 -1
  126. package/src/native-ui/shared-contracts/src/thread-harness-switch.ts +17 -0
@@ -0,0 +1,102 @@
1
+ import type { RendererSettingsPageDefinition, RendererSettingsPageMountContext } from './core.js';
2
+ import type { RendererSettingsMessages } from './localization.js';
3
+ import type { RendererIntegrationsClient, IntegrationSnapshot, IntegrationScope } from '../renderer-integrations-client.js';
4
+
5
+ export function createIntegrationsSettingsPage(messages: RendererSettingsMessages, getClient: () => RendererIntegrationsClient | null): RendererSettingsPageDefinition {
6
+ const zh = messages.locale === 'zh-CN';
7
+ const tr = (cn: string, en: string) => zh ? cn : en;
8
+ return Object.freeze({
9
+ id: 'integrations', label: 'MCP / Skills', icon: 'connections',
10
+ mount(context: RendererSettingsPageMountContext) {
11
+ const { content } = context;
12
+ const lifecycle = { signal: context.signal, runLatest: context.runLatest, get disposed() { return context.signal.aborted; } };
13
+ const doc = content.ownerDocument;
14
+ const el = <K extends keyof HTMLElementTagNameMap>(tag: K, text = '') => { const node = doc.createElement(tag); node.textContent = text; return node; };
15
+ const title = el('h2', 'MCP / Skills');
16
+ const note = el('p', tr('为每个 Harness 配置工具与技能。变更在下次打开原生会话时生效;正在运行的会话保持当前配置。', 'Configure tools and skills per Harness. Changes apply the next time a native session opens; running sessions keep their configuration.'));
17
+ note.className = 'settings-page-description';
18
+ const controls = el('div'); controls.className = 'settings-integrations-controls';
19
+ const harness = el('select'); harness.setAttribute('aria-label', 'Harness');
20
+ const level = el('select'); level.setAttribute('aria-label', tr('作用范围', 'Scope'));
21
+ for (const [value, label] of [['global', tr('全局', 'Global')], ['project', tr('项目', 'Project')]]) { const option = el('option', label); option.value = value!; level.append(option); }
22
+ const cwd = el('input'); cwd.placeholder = tr('项目绝对路径', 'Absolute project path'); cwd.setAttribute('aria-label', cwd.placeholder); cwd.hidden = true;
23
+ const body = el('div'); body.className = 'settings-integrations-body';
24
+ const status = el('p'); status.setAttribute('role', 'status'); status.setAttribute('aria-live', 'polite');
25
+ let busy = false;
26
+ const button = (label: string, action: () => void) => { const b = el('button', label); b.type = 'button'; b.className = 'settings-command-button settings-command-button--secondary'; b.addEventListener('click', action, { signal: lifecycle.signal }); return b; };
27
+ const input = (): IntegrationScope => ({ harnessId: harness.value, scope: level.value === 'project' ? 'project' : 'global', ...(level.value === 'project' ? { cwd: cwd.value.trim() } : {}) });
28
+ const fail = (error: unknown) => { status.textContent = error instanceof Error ? error.message : String(error); status.setAttribute('role', 'alert'); };
29
+ const client = () => { const c = getClient(); if (!c) throw new Error(tr('Host 尚未连接,请连接后刷新。', 'Host is not connected. Connect and refresh.')); return c; };
30
+ const setBusy = (value: boolean) => { busy = value; for (const node of content.querySelectorAll<HTMLButtonElement | HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>('button, input, select, textarea')) node.disabled = value || node.dataset.readonly === 'true'; };
31
+ const refresh = async () => {
32
+ if (busy || lifecycle.disposed) return;
33
+ setBusy(true); status.textContent = tr('正在读取…', 'Loading…');
34
+ await lifecycle.runLatest(() => client().listIntegrations(input()), { success: data => { render(data); status.textContent = ''; }, failure: fail });
35
+ if (!lifecycle.disposed) setBusy(false);
36
+ };
37
+ const mutate = async (operation: (c: RendererIntegrationsClient, selected: IntegrationScope) => Promise<unknown>) => {
38
+ if (busy) return;
39
+ const selected = input(); setBusy(true);
40
+ try { await operation(client(), selected); if (!lifecycle.disposed) { setBusy(false); await refresh(); status.textContent = tr('已保存,下次打开原生会话时生效。', 'Saved. Applies on the next native session open.'); } }
41
+ catch (error) { if (!lifecycle.disposed) { setBusy(false); fail(error); } }
42
+ };
43
+ const field = (form: HTMLElement, label: string, node: HTMLElement) => { const wrap = el('label', label); wrap.className = 'settings-integrations-field'; wrap.append(node); form.append(wrap); };
44
+ const render = (snapshot: IntegrationSnapshot) => {
45
+ body.replaceChildren();
46
+ body.append(el('h3', 'MCP'));
47
+ for (const native of snapshot.native) {
48
+ const row = el('section'); row.className = 'settings-integrations-row';
49
+ row.append(el('strong', `${tr('原生报告', 'Native report')} · ${native.name || native.sessionId} · ${native.status}`), el('code', native.tools.join(', ')));
50
+ body.append(row);
51
+ }
52
+ body.append(el('p', snapshot.mcpSupported ? tr('本页管理的 stdio 服务会传入原生 Harness。保留原有 MCP 配置;认证由原生环境管理。', 'Managed stdio servers are passed to the native Harness. Existing MCP configuration is retained; authentication stays in the native environment.') : tr('此 Harness 尚无已接入的原生 MCP 配置接口。', 'No supported native MCP configuration interface for this Harness.')));
53
+ for (const server of snapshot.servers) {
54
+ const row = el('section'); row.className = 'settings-integrations-row';
55
+ const detail = el('div');
56
+ detail.append(el('strong', server.name), el('p', `${server.scope === 'global' ? tr('全局', 'Global') : tr('项目', 'Project')} · ${!server.effective ? tr('被项目配置覆盖', 'Overridden by project') : !server.enabled ? tr('已停用', 'Disabled') : server.appliedSessions ? tr(`已传入 ${server.appliedSessions} 个会话(连接状态未报告)`, `Passed to ${server.appliedSessions} sessions (connection unreported)`) : tr('已配置,尚未传入会话', 'Configured; not yet passed to a session')}`), el('code', `${server.command} ${JSON.stringify(server.args)}`));
57
+ row.append(detail);
58
+ if (server.editable) {
59
+ row.append(button(server.enabled ? tr('停用', 'Disable') : tr('启用', 'Enable'), () => { void mutate((c, selected) => c.saveMcp({ ...selected, server: { name: server.name, command: server.command, args: server.args, enabled: !server.enabled } })); }),
60
+ button(tr('编辑', 'Edit'), () => { name.value = server.name; command.value = server.command; args.value = JSON.stringify(server.args, null, 2); name.focus(); }),
61
+ button(tr('移除配置', 'Remove'), () => { void mutate((c, selected) => c.removeMcp({ ...selected, id: server.id })); }));
62
+ }
63
+ body.append(row);
64
+ }
65
+ const form = el('form'); form.className = 'settings-integrations-form';
66
+ const name = el('input'); name.required = true; name.pattern = '[a-zA-Z0-9](?:[a-zA-Z0-9_]|-){0,63}';
67
+ const command = el('input'); command.required = true; command.placeholder = tr('可执行文件,如 node', 'Executable, e.g. node');
68
+ const args = el('textarea'); args.value = '[]'; args.rows = 3;
69
+ if (snapshot.mcpSupported) {
70
+ field(form, tr('服务名称(同名保存即更新)', 'Server name (save the same name to update)'), name);
71
+ field(form, tr('可执行文件', 'Executable'), command); field(form, tr('参数(JSON 字符串数组;不要填写密钥)', 'Arguments (JSON string array; no credentials)'), args);
72
+ const save = button(tr('保存 MCP', 'Save MCP'), () => {}); save.type = 'submit'; form.append(save);
73
+ form.addEventListener('submit', event => { event.preventDefault(); try { const parsed: unknown = JSON.parse(args.value); if (!Array.isArray(parsed) || !parsed.every(a => typeof a === 'string')) throw new Error(tr('参数必须是字符串数组', 'Arguments must be a string array')); const server = { name: name.value.trim(), command: command.value.trim(), args: parsed as string[], enabled: true }; void mutate((c, selected) => c.saveMcp({ ...selected, server })); } catch (error) { fail(error); } }, { signal: lifecycle.signal });
74
+ body.append(form);
75
+ }
76
+ body.append(el('h3', 'Skills'), el('p', snapshot.skillsSupported ? tr('从原生技能目录发现,不代表当前会话已加载。停用会保留文件;共享目录的变更会影响所有读取该目录的 Harness。', 'Discovered in native skill directories, not necessarily loaded in the current session. Disabling retains files. Changes to shared directories affect every Harness that reads them.') : tr('此 Harness 的原生技能目录尚未确认,暂不写入。', 'Native skill directories are not yet verified for this Harness.')));
77
+ for (const skill of snapshot.skills) {
78
+ const row = el('section'); row.className = 'settings-integrations-row';
79
+ const detail = el('div'); detail.append(el('strong', skill.name), el('p', skill.enabled ? tr('已发现 · 加载状态未报告', 'Discovered · load status unreported') : tr('已停用 · 文件已保留', 'Disabled · files retained')), el('code', skill.path)); row.append(detail);
80
+ if (skill.writable) row.append(button(skill.enabled ? tr('停用', 'Disable') : tr('恢复', 'Restore'), () => { void mutate((c, selected) => c.changeSkill({ ...selected, name: skill.name, root: skill.root, action: skill.enabled ? 'disable' : 'enable' })); }));
81
+ else row.append(el('span', tr('链接目录,只读', 'Linked directory, read-only')));
82
+ body.append(row);
83
+ }
84
+ if (snapshot.skillsSupported) {
85
+ const install = el('form'); install.className = 'settings-integrations-form';
86
+ const source = el('input'); source.required = true; source.placeholder = tr('包含 SKILL.md 的本地目录', 'Local directory containing SKILL.md');
87
+ const skillName = el('input'); skillName.required = true;
88
+ field(install, tr('技能名称', 'Skill name'), skillName); field(install, tr('源目录绝对路径', 'Absolute source directory'), source);
89
+ const save = button(tr('安装到当前 Harness', 'Install for this Harness'), () => {}); save.type = 'submit'; install.append(save);
90
+ install.addEventListener('submit', event => { event.preventDefault(); const change = { name: skillName.value.trim(), source: source.value.trim(), action: 'install' as const }; void mutate((c, selected) => c.changeSkill({ ...selected, ...change })); }, { signal: lifecycle.signal }); body.append(install);
91
+ }
92
+ };
93
+ const reload = button(tr('刷新', 'Refresh'), () => { void refresh(); });
94
+ controls.append(harness, level, cwd, reload); content.append(title, note, controls, status, body);
95
+ harness.addEventListener('change', () => { void refresh(); }, { signal: lifecycle.signal });
96
+ level.addEventListener('change', () => { cwd.hidden = level.value !== 'project'; body.replaceChildren(); if (!cwd.hidden && !cwd.value) { cwd.focus(); status.textContent = tr('填写项目路径,然后刷新。', 'Enter the project path, then refresh.'); } else void refresh(); }, { signal: lifecycle.signal });
97
+ cwd.addEventListener('change', () => { void refresh(); }, { signal: lifecycle.signal });
98
+ void lifecycle.runLatest(() => client().integrationCatalog(), { success: data => { for (const item of data.harnesses) { const option = el('option', `${item.name}${item.available ? '' : tr(' · 未就绪', ' · not ready')}`); option.value = item.id; harness.append(option); } void refresh(); }, failure: fail });
99
+ return undefined;
100
+ },
101
+ });
102
+ }
@@ -88,7 +88,12 @@ export interface RendererSettingsMessages {
88
88
  readonly accountDeleteFailed: string;
89
89
  readonly accountActive: string;
90
90
  readonly accountUse: string;
91
- readonly accountSignIn: string;
91
+ readonly accountSignIn: string;
92
+ readonly accountSignOut: string;
93
+ readonly accountSignOutConfirm: string;
94
+ readonly accountSigningOut: string;
95
+ readonly accountSignedOut: string;
96
+ readonly accountSignOutFailed: string;
92
97
  readonly accountSigningIn: string;
93
98
  readonly accountDeviceCodePrerequisite: string;
94
99
  readonly accountVerificationDescription: string;
@@ -299,9 +304,9 @@ const ENGLISH_MESSAGES: RendererSettingsMessages = Object.freeze({
299
304
  sessionImportRetrying: "Opening...",
300
305
  connectionsDescription:
301
306
  "View runtime status by Host. Select an item to inspect details or complete its setup.",
302
- accountsDescription: "Choose the default account for new Codex tasks and check account limits.",
303
- accountTaskHint:
304
- "Existing tasks keep the account they were created with. Multi-account is currently available for Codex.",
307
+ accountsDescription: "Manage isolated native Codex accounts and compare each account's live limits.",
308
+ accountTaskHint:
309
+ "Signing in or switching accounts does not affect tasks already running. New tasks use the selected account.",
305
310
  accountConnected: "Connected accounts",
306
311
  accountDefaultBadge: "Default",
307
312
  accountAdd: "Add Account",
@@ -327,7 +332,12 @@ const ENGLISH_MESSAGES: RendererSettingsMessages = Object.freeze({
327
332
  accountDeleteFailed: "Could not delete the Account.",
328
333
  accountActive: "Default",
329
334
  accountUse: "Set as default",
330
- accountSignIn: "Sign in",
335
+ accountSignIn: "Sign in",
336
+ accountSignOut: "Sign out",
337
+ accountSignOutConfirm: "Sign out of the current official Codex account?",
338
+ accountSigningOut: "Signing out of official Codex...",
339
+ accountSignedOut: "Signed out of official Codex.",
340
+ accountSignOutFailed: "Could not sign out of official Codex.",
331
341
  accountSigningIn: "Starting device sign-in...",
332
342
  accountDeviceCodePrerequisite:
333
343
  "Before signing in, enable “Enable device code authorization for Codex” in Web Settings → Account security & sign-in.",
@@ -498,9 +508,10 @@ const ENGLISH_MESSAGES: RendererSettingsMessages = Object.freeze({
498
508
  aboutOpenSource: "Harness Mix is an open-source project. The source code is available at:",
499
509
  aboutStarCallout: "⭐ If this project helps you, please give us a Star! ⭐",
500
510
  aboutRepository: "Open-source repository",
501
- pageLabels: Object.freeze({
502
- connections: "Connections",
503
- accounts: "Accounts",
511
+ pageLabels: Object.freeze({
512
+ connections: "Connections",
513
+ accounts: "Accounts",
514
+ integrations: 'MCP / Skills',
504
515
  "session-import": "Session Import",
505
516
  updates: "Updates",
506
517
  about: "About",
@@ -554,8 +565,8 @@ const CHINESE_MESSAGES: RendererSettingsMessages = Object.freeze({
554
565
  sessionImportRetryOpen: "重试打开",
555
566
  sessionImportRetrying: "正在打开……",
556
567
  connectionsDescription: "按 Host 查看运行时状态。选择一项,在右侧检查详情或完成配置。",
557
- accountsDescription: "选择新建 Codex 任务的默认账号,并查看各账号额度。",
558
- accountTaskHint: "切换默认账号不会影响已开始的任务。目前仅支持 Codex 多账号。",
568
+ accountsDescription: "管理相互隔离的原生 Codex 账号,并对比每个账号的实时额度。",
569
+ accountTaskHint: "登录或切换账号不会影响已开始的任务;新任务使用当前选中的账号。",
559
570
  accountConnected: "已连接账号",
560
571
  accountDefaultBadge: "默认",
561
572
  accountAdd: "添加账号",
@@ -581,7 +592,12 @@ const CHINESE_MESSAGES: RendererSettingsMessages = Object.freeze({
581
592
  accountDeleteFailed: "删除账号失败。",
582
593
  accountActive: "默认账号",
583
594
  accountUse: "设为默认",
584
- accountSignIn: "登录",
595
+ accountSignIn: "登录",
596
+ accountSignOut: "退出登录",
597
+ accountSignOutConfirm: "确定退出当前官方 Codex 账号吗?",
598
+ accountSigningOut: "正在退出官方 Codex...",
599
+ accountSignedOut: "已退出官方 Codex。",
600
+ accountSignOutFailed: "退出官方 Codex 失败。",
585
601
  accountSigningIn: "正在启动设备登录...",
586
602
  accountDeviceCodePrerequisite:
587
603
  "登录前,请先在 Web 端的“设置 → 账号安全与登录”中开启“为 Codex 启用设备代码授权”。",
@@ -750,8 +766,9 @@ const CHINESE_MESSAGES: RendererSettingsMessages = Object.freeze({
750
766
  aboutRepository: "开源仓库",
751
767
  pageLabels: Object.freeze({
752
768
  connections: "连接",
753
- accounts: "账号",
754
- "session-import": "会话导入",
769
+ accounts: "账号",
770
+ integrations: 'MCP / Skills',
771
+ "session-import": "会话导入",
755
772
  updates: "更新",
756
773
  about: "关于",
757
774
  }),
@@ -26,7 +26,9 @@ import {
26
26
  type RendererSessionImportClient,
27
27
  type RendererImportedThreadOpener,
28
28
  } from "./session-import-page.js";
29
- import { createReleaseNotesElement } from "./release-notes.js";
29
+ import { createReleaseNotesElement } from "./release-notes.js";
30
+ import { createIntegrationsSettingsPage } from './integrations-page.js';
31
+ import type { RendererIntegrationsClient } from '../renderer-integrations-client.js';
30
32
  import { createAccountsSettingsPage, type RendererCodexAccountClient } from "./accounts-page.js";
31
33
 
32
34
  export type {
@@ -71,7 +73,8 @@ function windowsInstallerDownloadUrl(window: Window | null | undefined, version:
71
73
 
72
74
  export const DEFAULT_RENDERER_SETTINGS_PAGE_IDS = [
73
75
  "connections",
74
- "accounts",
76
+ "accounts",
77
+ "integrations",
75
78
  "session-import",
76
79
  "updates",
77
80
  "about",
@@ -79,11 +82,12 @@ export const DEFAULT_RENDERER_SETTINGS_PAGE_IDS = [
79
82
 
80
83
  export type DefaultRendererSettingsPageId = (typeof DEFAULT_RENDERER_SETTINGS_PAGE_IDS)[number];
81
84
 
82
- export interface RendererUpdateClient {
83
- checkUpdate(): Promise<UpdateCheckResult>;
84
- startUpdate(): Promise<UpdateStartResult>;
85
- readUpdateStatus(): Promise<UpdateStatusResult>;
86
- }
85
+ export interface RendererUpdateClient {
86
+ checkUpdate(): Promise<UpdateCheckResult>;
87
+ startUpdate(): Promise<UpdateStartResult>;
88
+ readUpdateStatus(): Promise<UpdateStatusResult>;
89
+ readCurrentVersion?(): Promise<{ version: string }>;
90
+ }
87
91
 
88
92
  function panelIconName(view: string): RendererSettingsIconName {
89
93
  if (view === "failed" || view === "error") return "alert";
@@ -154,7 +158,10 @@ function formatUpdateBytes(value: number): string {
154
158
  return `${scaled.toFixed(scaled >= 10 ? 0 : 1)} ${unit}`;
155
159
  }
156
160
 
157
- export function aboutPage(messages: RendererSettingsMessages): RendererSettingsPageDefinition {
161
+ export function aboutPage(
162
+ messages: RendererSettingsMessages,
163
+ getClient: () => RendererUpdateClient | null = () => null,
164
+ ): RendererSettingsPageDefinition {
158
165
  return Object.freeze({
159
166
  id: "about",
160
167
  label: messages.pageLabels.about,
@@ -177,7 +184,7 @@ export function aboutPage(messages: RendererSettingsMessages): RendererSettingsP
177
184
  versionRow.className = "settings-about-version-row";
178
185
  const versionBadge = document.createElement("span");
179
186
  versionBadge.className = "settings-about-version-badge";
180
- versionBadge.textContent = "v0.1.2";
187
+ versionBadge.textContent = "v…";
181
188
  const checkUpdateBtn = document.createElement("button");
182
189
  checkUpdateBtn.type = "button";
183
190
  checkUpdateBtn.className = "settings-command-button settings-command-button--secondary";
@@ -217,9 +224,27 @@ export function aboutPage(messages: RendererSettingsMessages): RendererSettingsP
217
224
  repositoryUrl,
218
225
  );
219
226
  repositorySection.append(openSource, repository);
220
- panel.append(product, tagline, versionRow, introduction, starCallout, repositorySection);
221
- context.content.append(heading, panel);
222
- return undefined;
227
+ panel.append(product, tagline, versionRow, introduction, starCallout, repositorySection);
228
+ context.content.append(heading, panel);
229
+ const client = getClient();
230
+ const readVersion = client?.readCurrentVersion
231
+ ? () => client.readCurrentVersion!()
232
+ : client
233
+ ? () => client.checkUpdate().then(({ currentVersion }) => ({ version: currentVersion }))
234
+ : null;
235
+ if (readVersion) {
236
+ void context.runLatest(readVersion, {
237
+ success(result) {
238
+ versionBadge.textContent = `v${result.version}`;
239
+ },
240
+ failure() {
241
+ versionBadge.textContent = "v—";
242
+ },
243
+ });
244
+ } else {
245
+ versionBadge.textContent = "v—";
246
+ }
247
+ return undefined;
223
248
  },
224
249
  });
225
250
  }
@@ -597,15 +622,17 @@ export function createDefaultRendererSettingsPages(
597
622
  getDiagnostics: () => RendererConnectionDiagnostics | null = () => null,
598
623
  getAccountClient: () => RendererCodexAccountClient | null = () => null,
599
624
  getSessionImportClient: () => RendererSessionImportClient | null = () => null,
600
- openImportedThread: RendererImportedThreadOpener = () =>
601
- Promise.reject(new Error("Imported Thread navigation is unavailable")),
625
+ openImportedThread: RendererImportedThreadOpener = () =>
626
+ Promise.reject(new Error("Imported Thread navigation is unavailable")),
627
+ getIntegrationsClient: () => RendererIntegrationsClient | null = () => null,
602
628
  ): readonly RendererSettingsPageDefinition[] {
603
629
  return Object.freeze([
604
630
  createConnectionsSettingsPage(messages, getDiagnostics),
605
- createAccountsSettingsPage(messages, getAccountClient),
631
+ createAccountsSettingsPage(messages, getAccountClient),
632
+ createIntegrationsSettingsPage(messages, getIntegrationsClient),
606
633
  createSessionImportSettingsPage(messages, getSessionImportClient, openImportedThread),
607
634
  updatesPage(messages, getUpdateClient),
608
- aboutPage(messages),
635
+ aboutPage(messages, getUpdateClient),
609
636
  ]);
610
637
  }
611
638
 
@@ -615,7 +642,8 @@ export function createDefaultRendererSettingsRegistry(
615
642
  getDiagnostics: () => RendererConnectionDiagnostics | null = () => null,
616
643
  getAccountClient: () => RendererCodexAccountClient | null = () => null,
617
644
  getSessionImportClient: () => RendererSessionImportClient | null = () => null,
618
- openImportedThread?: RendererImportedThreadOpener,
645
+ openImportedThread?: RendererImportedThreadOpener,
646
+ getIntegrationsClient?: () => RendererIntegrationsClient | null,
619
647
  ): RendererSettingsPageRegistry {
620
648
  return createRendererSettingsPageRegistry(
621
649
  createDefaultRendererSettingsPages(
@@ -624,7 +652,8 @@ export function createDefaultRendererSettingsRegistry(
624
652
  getDiagnostics,
625
653
  getAccountClient,
626
654
  getSessionImportClient,
627
- openImportedThread,
655
+ openImportedThread,
656
+ getIntegrationsClient,
628
657
  ),
629
658
  );
630
659
  }
@@ -2236,3 +2236,16 @@ button {
2236
2236
  .settings-install-steps li {
2237
2237
  margin-bottom: 2px;
2238
2238
  }
2239
+ /* MCP / Skills share the existing settings shell and its theme. */
2240
+ .settings-integrations-controls { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin: 18px 0; }
2241
+ .settings-integrations-controls input { flex: 1; min-width: 180px; }
2242
+ .settings-integrations-controls input[hidden] { display: none; }
2243
+ .settings-integrations-controls select, .settings-integrations-controls input,
2244
+ .settings-integrations-field input, .settings-integrations-field textarea { box-sizing: border-box; padding: 8px 10px; border: 1px solid var(--border, #8885); border-radius: 7px; color: inherit; background: transparent; font: inherit; }
2245
+ .settings-integrations-body p { font-size: 12px; line-height: 1.6; opacity: .8; }
2246
+ .settings-integrations-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; border: 1px solid var(--border, #8885); padding: 14px; margin: 10px 0; border-radius: 10px; }
2247
+ .settings-integrations-row > div { flex: 1; min-width: 180px; }
2248
+ .settings-integrations-row code { display: block; overflow-wrap: anywhere; font-size: 11px; }
2249
+ .settings-integrations-form { display: grid; gap: 12px; margin: 16px 0 28px; }
2250
+ .settings-integrations-field { display: grid; gap: 6px; font-size: 12px; }
2251
+ .settings-integrations-form > button { justify-self: start; }
@@ -79,7 +79,7 @@ describe("Renderer Agent picker presentation", () => {
79
79
  isDefault: false,
80
80
  },
81
81
  ),
82
- ).toBe("Agent: Codex · reviewer@example.com (locked)");
82
+ ).toBe("Agent: Codex(官方原生) · reviewer@example.com (locked)");
83
83
  expect(rendererAgentPickerTooltip({ agent: "claude-code", phase: "locked" }, undefined)).toBe(
84
84
  "Agent: Claude Code (locked)",
85
85
  );
@@ -94,7 +94,7 @@ describe("Renderer Agent picker presentation", () => {
94
94
  "grok",
95
95
  ]),
96
96
  ).toEqual({
97
- label: "Codex",
97
+ label: "Codex(官方原生)",
98
98
  triggerDisabled: false,
99
99
  nativeModelHidden: false,
100
100
  optionDisabled: { codex: false, pi: true, "claude-code": true, grok: true },
@@ -107,27 +107,58 @@ describe("Renderer Agent picker presentation", () => {
107
107
  expect(
108
108
  rendererAgentPickerView({ agent: "codex", phase: "draft" }, "ready", false, ["codex"], {}, 2),
109
109
  ).toMatchObject({
110
- label: "Codex",
110
+ label: "Codex(官方原生)",
111
111
  triggerDisabled: false,
112
112
  optionDisabled: { codex: false },
113
113
  });
114
114
  });
115
115
 
116
- it("hides the native Model for an external Agent and locks submitted selection", () => {
116
+ it("turns a submitted external Agent picker into a Harness handoff picker", () => {
117
117
  expect(
118
- rendererAgentPickerView({ agent: "pi", phase: "locked" }, "ready", false, ["codex", "pi"], {
119
- pi: "ready",
120
- }),
118
+ rendererAgentPickerView(
119
+ { agent: "pi", phase: "locked" },
120
+ "ready",
121
+ false,
122
+ ["codex", "pi", "claude-code"],
123
+ { pi: "ready", "claude-code": "ready" },
124
+ ),
121
125
  ).toEqual({
122
126
  label: "Pi",
123
- triggerDisabled: true,
127
+ triggerDisabled: false,
124
128
  nativeModelHidden: true,
125
- optionDisabled: { codex: true, pi: true },
126
- downloadVisible: { pi: false },
127
- errorVisible: { pi: false },
129
+ optionDisabled: { codex: true, pi: true, "claude-code": false },
130
+ downloadVisible: { pi: false, "claude-code": false },
131
+ errorVisible: { pi: false, "claude-code": false },
128
132
  });
129
133
  });
130
134
 
135
+ it("offers another ready external Harness while keeping official Codex unavailable", () => {
136
+ expect(
137
+ rendererAgentPickerView(
138
+ { agent: "pi", phase: "locked" },
139
+ "ready",
140
+ false,
141
+ ["codex", "pi", "claude-code", "grok"],
142
+ { pi: "ready", "claude-code": "ready", grok: "unavailable" },
143
+ ),
144
+ ).toMatchObject({
145
+ triggerDisabled: false,
146
+ optionDisabled: { codex: true, pi: true, "claude-code": false, grok: true },
147
+ });
148
+ });
149
+
150
+ it("keeps an official Codex conversation locked because HostRuntime does not own it", () => {
151
+ expect(
152
+ rendererAgentPickerView(
153
+ { agent: "codex", phase: "locked" },
154
+ "ready",
155
+ false,
156
+ ["codex", "pi"],
157
+ { pi: "ready" },
158
+ ),
159
+ ).toMatchObject({ triggerDisabled: true, optionDisabled: { codex: true, pi: true } });
160
+ });
161
+
131
162
  it("hides the native Model and disables all choices while switching", () => {
132
163
  expect(
133
164
  rendererAgentPickerView({ agent: "codex", phase: "draft" }, "ready", true, ["codex", "pi"]),
@@ -145,7 +176,7 @@ describe("Renderer Agent picker presentation", () => {
145
176
  pi: "notInstalled",
146
177
  }),
147
178
  ).toEqual({
148
- label: "Codex",
179
+ label: "Codex(官方原生)",
149
180
  triggerDisabled: false,
150
181
  nativeModelHidden: false,
151
182
  optionDisabled: { codex: false, pi: true },
@@ -160,7 +191,7 @@ describe("Renderer Agent picker presentation", () => {
160
191
  pi: "error",
161
192
  }),
162
193
  ).toEqual({
163
- label: "Codex",
194
+ label: "Codex(官方原生)",
164
195
  triggerDisabled: false,
165
196
  nativeModelHidden: false,
166
197
  optionDisabled: { codex: false, pi: true },
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { CHATGPT_CONTEXT_MAX_LENGTH, buildChatGptContextDraft, redactChatGptContextText, sanitizeChatGptSourceUrl } from "../src/renderer-chatgpt-context.js";
3
+
4
+ describe("ChatGPT context package", () => {
5
+ it("marks imported text as untrusted context", () => {
6
+ const draft = buildChatGptContextDraft({ title: "架构讨论", sourceUrl: "https://chatgpt.com/c/example?utm_source=secret#turn", mode: "loaded-messages", messages: [{ role: "user", text: "给一个迁移方案" }, { role: "assistant", text: "先建立共享契约" }], truncated: false });
7
+ expect(draft).toContain("不可信历史资料"); expect(draft).toContain("不恢复原聊天"); expect(draft).toContain("User: 给一个迁移方案"); expect(draft).toContain("Assistant: 先建立共享契约"); expect(draft).toContain("https://chatgpt.com/c/example"); expect(draft).not.toContain("utm_source");
8
+ });
9
+ it("redacts credentials and caps the package", () => {
10
+ expect(redactChatGptContextText("Authorization: Bearer abcdefghijklmnopqrstuvwxyz sk-abcdefghijklmnopqrstuvwxyz123456")).not.toContain("abcdefghijklmnopqrstuvwxyz");
11
+ const draft = buildChatGptContextDraft({ title: "大段聊天", mode: "selection", messages: [{ role: "user", text: "x".repeat(CHATGPT_CONTEXT_MAX_LENGTH * 2) }], truncated: true });
12
+ expect(draft.length).toBeLessThanOrEqual(CHATGPT_CONTEXT_MAX_LENGTH); expect(draft).toContain("已截断");
13
+ });
14
+ it("accepts only sanitized HTTPS ChatGPT links", () => {
15
+ expect(sanitizeChatGptSourceUrl("https://chatgpt.com/c/abc?token=nope#part")).toBe("https://chatgpt.com/c/abc");
16
+ expect(sanitizeChatGptSourceUrl("http://chatgpt.com/c/abc")).toBeUndefined(); expect(sanitizeChatGptSourceUrl("https://example.com/c/abc")).toBeUndefined();
17
+ });
18
+ });
@@ -1,5 +1,8 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
- import { RendererCodexAccountState } from "../src/renderer-codex-account-state.js";
2
+ import {
3
+ codexAccountRouteOverride,
4
+ RendererCodexAccountState,
5
+ } from "../src/renderer-codex-account-state.js";
3
6
  import { createRendererModelClient } from "../src/renderer-model-client.js";
4
7
 
5
8
  const account = (accountId: string, active = false) => ({
@@ -17,6 +20,15 @@ function createState(sendRequest: (method: string, params: unknown) => Promise<u
17
20
  }
18
21
 
19
22
  describe("Host-scoped Codex Account state", () => {
23
+ it("routes isolated Accounts through Harness Mix even when they are the active default", () => {
24
+ const accounts = [
25
+ { ...account("official"), management: "native" as const },
26
+ { ...account("work", true), management: "isolated" as const },
27
+ ];
28
+ expect(codexAccountRouteOverride(accounts, "work")).toBe("work");
29
+ expect(codexAccountRouteOverride(accounts, "official")).toBeNull();
30
+ });
31
+
20
32
  it("coalesces concurrent refreshes on the same concrete client", async () => {
21
33
  const response = Promise.withResolvers<unknown>();
22
34
  const sendRequest = vi.fn(() => response.promise);
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ HARNESS_HANDOFF_NOTE_MAX_LENGTH,
5
+ rendererHarnessHandoffMessages,
6
+ } from "../src/renderer-harness-handoff.js";
7
+
8
+ describe("Renderer Harness handoff", () => {
9
+ it("uses a bounded note compatible with the Host switch contract", () => {
10
+ expect(HARNESS_HANDOFF_NOTE_MAX_LENGTH).toBe(2000);
11
+ });
12
+
13
+ it("explains that the task and working tree stay in place", () => {
14
+ const messages = rendererHarnessHandoffMessages("zh-CN");
15
+ expect(messages.title).toBe("接力当前任务");
16
+ expect(messages.description).toMatch(/任务窗口.*对话记录.*文件现场/);
17
+ expect(messages.context).toMatch(/近期对话.*最新方案.*计划.*变更文件/);
18
+ expect(messages.running).toMatch(/先停止或等待完成/);
19
+ });
20
+ });
@@ -278,12 +278,13 @@ describe("Renderer fixed Model request client", () => {
278
278
  },
279
279
  })
280
280
  .mockResolvedValueOnce({ status: null })
281
- .mockResolvedValueOnce({ threadId: "thread-1" });
281
+ .mockResolvedValueOnce({ threadId: "thread-1", checkpointId: "handoff-1" });
282
282
  const client = createRendererModelClient([{ addNotificationCallback, sendRequest }]);
283
283
  if (!client) throw new Error("Synthetic Model client was not created");
284
284
  expect(Object.keys(client).sort()).toEqual([
285
285
  "activateCodexAccount",
286
286
  "cancelCodexAccountLogin",
287
+ "changeSkill",
287
288
  "checkUpdate",
288
289
  "consumeCodexAccountResetCredit",
289
290
  "createCodexAccount",
@@ -299,18 +300,24 @@ describe("Renderer fixed Model request client", () => {
299
300
  "inspectThreadCommands",
300
301
  "inspectThreadUsage",
301
302
  "installHarness",
303
+ "integrationCatalog",
302
304
  "listCodexAccounts",
303
305
  "listCollaborationAgents",
304
306
  "listHarnessAccounts",
305
307
  "listHarnessPlugins",
306
308
  "listHarnessSessions",
309
+ "listIntegrations",
307
310
  "listSessionImportSources",
308
311
  "listThreadOwnership",
309
312
  "loginHarnessAccount",
313
+ "logoutCodexAccount",
310
314
  "messageThread",
311
315
  "openHarnessWebUi",
316
+ "readCurrentVersion",
312
317
  "readUpdateStatus",
313
318
  "refreshCodexAccounts",
319
+ "removeMcp",
320
+ "saveMcp",
314
321
  "selectThreadModel",
315
322
  "selectThreadPermissionMode",
316
323
  "selectThreadThinking",
@@ -444,7 +451,7 @@ describe("Renderer fixed Model request client", () => {
444
451
  harnessId: "claude-code",
445
452
  note: "继续补测试",
446
453
  }),
447
- ).resolves.toEqual({ threadId: "thread-1" });
454
+ ).resolves.toEqual({ threadId: "thread-1", checkpointId: "handoff-1" });
448
455
  expect(sendRequest).toHaveBeenNthCalledWith(13, THREAD_HARNESS_SWITCH_METHOD, {
449
456
  threadId: "thread-1",
450
457
  harnessId: "claude-code",
@@ -75,6 +75,6 @@ describe("Renderer settings localization", () => {
75
75
  createDefaultRendererSettingsPages(rendererSettingsMessages("zh-CN")).map(
76
76
  ({ label }) => label,
77
77
  ),
78
- ).toEqual(["连接", "账号", "会话导入", "更新", "关于"]);
78
+ ).toEqual(["连接", "账号", "MCP / Skills", "会话导入", "更新", "关于"]);
79
79
  });
80
80
  });