@harness-mix/cli 0.1.3 → 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.
- package/CHANGELOG.md +8 -0
- package/README.md +26 -96
- package/docs/chatgpt-web-sidebar.md +7 -1
- package/docs/cross-platform.md +15 -0
- package/docs/harness-handoff-design.md +377 -0
- package/docs/harness-management.md +28 -1
- package/docs/native-codex.md +21 -10
- package/output/native-build/harness-mix-shim.exe +0 -0
- package/output/native-build/renderer-extension.js +972 -35
- package/package.json +21 -3
- package/scripts/chatgpt-context-ui-smoke.cjs +16 -0
- package/scripts/codex-accounts-test.cjs +81 -0
- package/scripts/codex-accounts-ui-smoke.cjs +70 -0
- package/scripts/e2e-integrations.cjs +52 -0
- package/scripts/e2e-native-host.cjs +39 -0
- package/scripts/handoff-checkpoint-test.cjs +83 -0
- package/scripts/install-native.cjs +29 -0
- package/scripts/integrations-test.cjs +64 -0
- package/scripts/integrations-ui-smoke.cjs +63 -0
- package/scripts/native-protocol-test.cjs +78 -4
- package/scripts/native-secret-test.cjs +2 -2
- package/scripts/native-shim-test.cjs +20 -4
- package/scripts/native-ui-smoke.cjs +58 -2
- package/scripts/publish-native-package.cjs +37 -0
- package/scripts/support/integrations-mcp-fixture.cjs +15 -0
- package/scripts/switch-harness-test.cjs +27 -1
- package/src/main/adapters/acp.js +3 -3
- package/src/main/adapters/antigravity.js +10 -13
- package/src/main/adapters/claude.js +10 -3
- package/src/main/adapters/codex-app-server.js +20 -13
- package/src/main/adapters/codex.js +6 -4
- package/src/main/adapters/dsh.js +2 -1
- package/src/main/adapters/grok.js +4 -3
- package/src/main/adapters/managed-mcp.js +12 -0
- package/src/main/adapters/native-acp.js +4 -4
- package/src/main/adapters/opencode.js +9 -3
- package/src/main/adapters/pi.js +1 -0
- package/src/main/harness-adapter/manifest.js +17 -2
- package/src/main/host/handoff-access.js +53 -0
- package/src/main/host/handoff-checkpoints.js +206 -0
- package/src/main/host/handoff-mcp.cjs +38 -0
- package/src/main/host/handoff-tools.js +13 -0
- package/src/main/host/handoff.js +28 -6
- package/src/main/host/integrations.js +201 -0
- package/src/main/host/runtime.js +75 -35
- package/src/main/host/store.js +4 -4
- package/src/main/native/codex-accounts.js +260 -0
- package/src/main/native/host.js +1 -1
- package/src/main/native/launcher.js +2 -2
- package/src/main/native/protocol.js +74 -23
- package/src/main/native/redact.js +1 -1
- package/src/main/native/rs/crates/shim/src/main.rs +155 -12
- 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 +4 -0
- package/src/native-ui/renderer-extension/dist/types/index.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts +2 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.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-chatgpt-context.d.ts +36 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts +3 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts +53 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts +80 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +7 -1
- 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-settings-lifecycle.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts +2 -0
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts +5 -0
- package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts +5 -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 +8 -4
- 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/harness-mix-settings.ts +3 -1
- package/src/native-ui/renderer-extension/src/index.ts +28 -0
- package/src/native-ui/renderer-extension/src/renderer-agent-icon.ts +4 -4
- package/src/native-ui/renderer-extension/src/renderer-agent-picker.ts +46 -6
- package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +159 -40
- package/src/native-ui/renderer-extension/src/renderer-chatgpt-context.ts +83 -0
- package/src/native-ui/renderer-extension/src/renderer-codex-account-state.ts +10 -0
- package/src/native-ui/renderer-extension/src/renderer-composer-dom.ts +10 -0
- package/src/native-ui/renderer-extension/src/renderer-harness-handoff.ts +330 -0
- package/src/native-ui/renderer-extension/src/renderer-integrations-client.ts +26 -0
- package/src/native-ui/renderer-extension/src/renderer-model-client.ts +17 -1
- package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +2 -0
- package/src/native-ui/renderer-extension/src/settings/accounts-list.ts +16 -1
- package/src/native-ui/renderer-extension/src/settings/accounts-page.ts +32 -3
- package/src/native-ui/renderer-extension/src/settings/integrations-page.ts +102 -0
- package/src/native-ui/renderer-extension/src/settings/localization.ts +30 -13
- package/src/native-ui/renderer-extension/src/settings/pages.ts +47 -18
- package/src/native-ui/renderer-extension/src/settings/shell.css +13 -0
- package/src/native-ui/renderer-extension/test/renderer-agent-picker.test.ts +44 -13
- package/src/native-ui/renderer-extension/test/renderer-chatgpt-context.test.ts +18 -0
- package/src/native-ui/renderer-extension/test/renderer-codex-account-state.test.ts +13 -1
- package/src/native-ui/renderer-extension/test/renderer-harness-handoff.test.ts +20 -0
- package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +9 -2
- package/src/native-ui/renderer-extension/test/settings/localization.test.ts +1 -1
- package/src/native-ui/renderer-extension/test/settings/pages.test.ts +77 -8
- package/src/native-ui/renderer-extension/test/settings/shell.test.ts +3 -0
- package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts +19 -4
- package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts.map +1 -1
- package/src/native-ui/shared-contracts/dist/codex-accounts.js +2 -0
- package/src/native-ui/shared-contracts/dist/codex-accounts.js.map +1 -1
- package/src/native-ui/shared-contracts/dist/index.d.ts +2 -2
- package/src/native-ui/shared-contracts/dist/index.d.ts.map +1 -1
- package/src/native-ui/shared-contracts/dist/index.js +1 -1
- package/src/native-ui/shared-contracts/dist/index.js.map +1 -1
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts +29 -0
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts.map +1 -1
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.js +13 -0
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.js.map +1 -1
- package/src/native-ui/shared-contracts/dist/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/shared-contracts/src/codex-accounts.ts +2 -0
- package/src/native-ui/shared-contracts/src/index.ts +8 -1
- 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: "
|
|
303
|
-
accountTaskHint:
|
|
304
|
-
"
|
|
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: "
|
|
558
|
-
accountTaskHint: "
|
|
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
|
-
|
|
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(
|
|
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 = "
|
|
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
|
-
|
|
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("
|
|
116
|
+
it("turns a submitted external Agent picker into a Harness handoff picker", () => {
|
|
117
117
|
expect(
|
|
118
|
-
rendererAgentPickerView(
|
|
119
|
-
pi: "
|
|
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:
|
|
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 {
|
|
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
|
});
|