@harness-mix/cli 0.1.8 → 0.1.10
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 +7 -0
- package/NOTICE +12 -0
- package/README.md +74 -71
- package/docs/harness-management.md +1 -1
- package/docs/native-acp.md +7 -2
- package/docs/task-handoff-design.md +65 -0
- package/licenses/codex-styler-Apache-2.0.txt +193 -0
- package/licenses/heige-codex-skin-studio-MIT.txt +22 -0
- package/output/native-build/desktop-controller.mjs +3 -2
- package/output/native-build/renderer-extension.js +1350 -46
- package/package.json +189 -188
- package/scripts/acp-capabilities-probe.cjs +1 -0
- package/scripts/build-native.cjs +1 -1
- package/scripts/codex-adapter-test.cjs +44 -0
- package/scripts/collaboration-recovery-test.cjs +3 -0
- package/scripts/e2e-codex.cjs +3 -1
- package/scripts/e2e-native-acp.cjs +1 -1
- package/scripts/error-kind-test.cjs +107 -0
- package/scripts/handoff-checkpoint-test.cjs +38 -0
- package/scripts/integrations-test.cjs +5 -2
- package/scripts/kiro-cursor-adapters-test.cjs +18 -0
- package/scripts/native-acp-depth-test.cjs +1 -1
- package/scripts/native-config-test.cjs +32 -2
- package/scripts/native-protocol-test.cjs +60 -4
- package/scripts/native-ui-smoke.cjs +127 -2
- package/scripts/pets-test.cjs +146 -0
- package/scripts/switch-harness-test.cjs +79 -12
- package/scripts/tool-concurrency-regression-test.cjs +8 -6
- package/src/assets/icons/cline-color.svg +1 -0
- package/src/assets/skins/codex-styler/ASSET_LICENSES.md +36 -0
- package/src/assets/skins/codex-styler/NOTICE +11 -0
- package/src/assets/skins/codex-styler/themes/gilded-grandeur/hero.webp +0 -0
- package/src/assets/skins/codex-styler/themes/merry-big-top/hero.webp +0 -0
- package/src/assets/skins/codex-styler/themes/nocturne-studio/hero.webp +0 -0
- package/src/assets/skins/codex-styler/themes/quiet-garden/hero.webp +0 -0
- package/src/assets/skins/heige/ASSET_PROVENANCE.md +56 -0
- package/src/assets/skins/heige/NOTICE.md +13 -0
- package/src/assets/skins/heige/themes/dalao-dianyan/hero.webp +0 -0
- package/src/assets/skins/heige/themes/dalao-dianyan/theme.json +14 -0
- package/src/assets/skins/heige/themes/deepspace-dawn/hero.webp +0 -0
- package/src/assets/skins/heige/themes/deepspace-dawn/theme.json +14 -0
- package/src/assets/skins/heige/themes/deepspace-star/hero.webp +0 -0
- package/src/assets/skins/heige/themes/deepspace-star/theme.json +14 -0
- package/src/assets/skins/heige/themes/dragonball-nimbus/hero.webp +0 -0
- package/src/assets/skins/heige/themes/dragonball-nimbus/theme.json +16 -0
- package/src/assets/skins/heige/themes/dragonball-super-saiyan/hero.webp +0 -0
- package/src/assets/skins/heige/themes/dragonball-super-saiyan/theme.json +16 -0
- package/src/assets/skins/heige/themes/genshin-dawn/hero.webp +0 -0
- package/src/assets/skins/heige/themes/genshin-dawn/theme.json +14 -0
- package/src/assets/skins/heige/themes/genshin-night/hero.webp +0 -0
- package/src/assets/skins/heige/themes/genshin-night/theme.json +14 -0
- package/src/assets/skins/heige/themes/miku-488137/hero.webp +0 -0
- package/src/assets/skins/heige/themes/miku-488137/logo.webp +0 -0
- package/src/assets/skins/heige/themes/miku-488137/polaroid.webp +0 -0
- package/src/assets/skins/heige/themes/miku-488137/theme.json +16 -0
- package/src/assets/skins/heige/themes/naruto-hokage/hero.webp +0 -0
- package/src/assets/skins/heige/themes/naruto-hokage/theme.json +14 -0
- package/src/assets/skins/heige/themes/naruto-sasuke/hero.webp +0 -0
- package/src/assets/skins/heige/themes/naruto-sasuke/theme.json +14 -0
- package/src/assets/skins/heige/themes/wuthering-echo/hero.webp +0 -0
- package/src/assets/skins/heige/themes/wuthering-echo/theme.json +14 -0
- package/src/assets/skins/heige/themes/wuthering-tide/hero.webp +0 -0
- package/src/assets/skins/heige/themes/wuthering-tide/theme.json +14 -0
- package/src/main/adapters/cline.js +22 -0
- package/src/main/adapters/codex.js +78 -8
- package/src/main/adapters/index.js +2 -1
- package/src/main/adapters/native-acp-command.js +1 -0
- package/src/main/harness-adapter/error-kind.js +82 -0
- package/src/main/harness-adapter/event-normalizer.js +7 -1
- package/src/main/host/core-session.js +1 -1
- package/src/main/host/handoff-access.js +23 -4
- package/src/main/host/handoff-checkpoints.js +9 -2
- package/src/main/host/handoff-tools.js +3 -0
- package/src/main/host/handoff.js +11 -2
- package/src/main/host/runtime.js +272 -163
- package/src/main/host/session-history.js +54 -15
- package/src/main/native/icons.js +3 -2
- package/src/main/native/launcher.js +49 -2
- package/src/main/native/pets.js +395 -0
- package/src/main/native/protocol.js +197 -55
- package/src/main/protocol-core/projector.js +44 -41
- package/src/native-ui/desktop-control/dist/production-controller.d.ts.map +1 -1
- package/src/native-ui/desktop-control/dist/production-controller.js +1 -0
- package/src/native-ui/desktop-control/dist/production-controller.js.map +1 -1
- package/src/native-ui/desktop-control/dist/renderer-draft-prewarm-runtime.js +4 -4
- package/src/native-ui/desktop-control/dist/renderer-draft-prewarm-runtime.js.map +1 -1
- package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/desktop-control/src/production-controller.ts +1 -0
- package/src/native-ui/desktop-control/src/renderer-draft-prewarm-runtime.ts +4 -4
- package/src/native-ui/desktop-control/test/production-controller.test.ts +1 -0
- package/src/native-ui/desktop-control/test/renderer-draft-prewarm-policy.test.ts +7 -0
- package/src/native-ui/renderer-extension/dist/types/agent-selection-state.d.ts +5 -2
- package/src/native-ui/renderer-extension/dist/types/agent-selection-state.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-agent-icon.d.ts.map +1 -1
- 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 +1 -0
- 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-harness-mentions.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +2 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +2 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-sidebar-agent-icons.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/connections-page.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts.map +1 -1
- 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 +5 -3
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/pet-market.d.ts +5 -0
- package/src/native-ui/renderer-extension/dist/types/settings/pet-market.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/pets-client.d.ts +57 -0
- package/src/native-ui/renderer-extension/dist/types/settings/pets-client.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/shell.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/skin-market.d.ts +4 -0
- package/src/native-ui/renderer-extension/dist/types/settings/skin-market.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/skin-runtime.d.ts +31 -0
- package/src/native-ui/renderer-extension/dist/types/settings/skin-runtime.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/renderer-extension/dist/types/versioned-renderer-adapter.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/package.json +1 -1
- package/src/native-ui/renderer-extension/src/agent-selection-state.ts +61 -14
- package/src/native-ui/renderer-extension/src/assets.d.ts +5 -0
- package/src/native-ui/renderer-extension/src/renderer-agent-icon.ts +5 -4
- package/src/native-ui/renderer-extension/src/renderer-agent-picker.ts +1 -0
- package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +78 -33
- package/src/native-ui/renderer-extension/src/renderer-harness-handoff.ts +2 -2
- package/src/native-ui/renderer-extension/src/renderer-harness-mentions.ts +2 -0
- package/src/native-ui/renderer-extension/src/renderer-model-client.ts +6 -0
- package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +5 -0
- package/src/native-ui/renderer-extension/src/renderer-sidebar-agent-icons.ts +1 -0
- package/src/native-ui/renderer-extension/src/settings/connections-page.ts +2 -0
- package/src/native-ui/renderer-extension/src/settings/icons.ts +15 -6
- package/src/native-ui/renderer-extension/src/settings/localization.ts +6 -2
- package/src/native-ui/renderer-extension/src/settings/pages.ts +72 -61
- package/src/native-ui/renderer-extension/src/settings/pet-market.css +310 -0
- package/src/native-ui/renderer-extension/src/settings/pet-market.ts +447 -0
- package/src/native-ui/renderer-extension/src/settings/pets-client.ts +74 -0
- package/src/native-ui/renderer-extension/src/settings/shell.css +27 -8
- package/src/native-ui/renderer-extension/src/settings/shell.ts +3 -1
- package/src/native-ui/renderer-extension/src/settings/skin-market.css +213 -0
- package/src/native-ui/renderer-extension/src/settings/skin-market.ts +174 -0
- package/src/native-ui/renderer-extension/src/settings/skin-runtime.ts +630 -0
- package/src/native-ui/renderer-extension/src/versioned-renderer-adapter.ts +2 -1
- package/src/native-ui/renderer-extension/test/agent-selection-state.test.ts +58 -0
- package/src/native-ui/renderer-extension/test/renderer-binding-probe.test.ts +12 -0
- package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +1 -0
- package/src/native-ui/renderer-extension/test/settings/localization.test.ts +12 -1
- package/src/native-ui/renderer-extension/test/settings/shell.test.ts +6 -0
- package/src/native-ui/renderer-extension/test/settings/skin-runtime.test.ts +162 -0
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts +7 -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 +3 -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/thread-harness-switch.ts +3 -0
|
@@ -3,6 +3,7 @@ const fs = require('node:fs/promises');
|
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
const { HandoffCheckpoints } = require('../src/main/host/handoff-checkpoints');
|
|
5
5
|
const { HandoffAccess } = require('../src/main/host/handoff-access');
|
|
6
|
+
const { SessionHistory } = require('../src/main/host/session-history');
|
|
6
7
|
const { dispatch } = require('../src/main/host/handoff-mcp.cjs');
|
|
7
8
|
|
|
8
9
|
async function main() {
|
|
@@ -47,6 +48,17 @@ async function main() {
|
|
|
47
48
|
thread.messages[0].text = 'mutated after handoff';
|
|
48
49
|
assert.doesNotMatch(JSON.stringify(runtime.handoffs.owned(thread.id, checkpoint.checkpointId)), /mutated after handoff/);
|
|
49
50
|
|
|
51
|
+
// 引用会话只读工具:pi 原生历史存根(30 轮),用户消息里显式给出 harness-mix://session/ 链接才可读
|
|
52
|
+
const sessionRef = Buffer.from(JSON.stringify(['pi', 'past-1'])).toString('base64url');
|
|
53
|
+
thread.messages.push({ id: 'user-2', role: 'user', text: `看看 [旧会话](harness-mix://session/${sessionRef}) 的结论` });
|
|
54
|
+
runtime.adapters.set('pi', { manifest: { integrations: { mcp: false } } });
|
|
55
|
+
runtime.threads = [thread];
|
|
56
|
+
runtime.execution = { isRunning: () => false };
|
|
57
|
+
runtime.history = new SessionHistory(runtime, {
|
|
58
|
+
async listNative() { return [{ nativeSessionId: 'past-1', title: '旧会话', cwd: process.cwd(), updatedAt: 5, running: null }]; },
|
|
59
|
+
async readNative() { return Array.from({ length: 30 }, (_, i) => ({ id: `m${i}`, role: i % 2 ? 'assistant' : 'user', text: `turn-${i}`, at: i })); },
|
|
60
|
+
});
|
|
61
|
+
|
|
50
62
|
const access = new HandoffAccess(runtime);
|
|
51
63
|
const connection = await access.connection({ ...thread, harnessId: 'target', pendingHandoff: { checkpointId: checkpoint.checkpointId } });
|
|
52
64
|
assert.ok(connection);
|
|
@@ -59,6 +71,7 @@ async function main() {
|
|
|
59
71
|
assert.deepEqual(catalog.tools.map(tool => tool.name), [
|
|
60
72
|
'get_handoff_checkpoint', 'list_handoff_conversation', 'list_handoff_evidence',
|
|
61
73
|
'read_handoff_evidence', 'list_handoff_files', 'read_handoff_plan',
|
|
74
|
+
'get_session_info', 'list_session_messages',
|
|
62
75
|
]);
|
|
63
76
|
const result = await dispatch({ id: 2, method: 'tools/call', params: { name: 'get_handoff_checkpoint', arguments: { checkpoint_id: checkpoint.checkpointId } } });
|
|
64
77
|
const summary = JSON.parse(result.content[0].text);
|
|
@@ -67,6 +80,31 @@ async function main() {
|
|
|
67
80
|
const denied = await dispatch({ id: 3, method: 'tools/call', params: { name: 'get_handoff_checkpoint', arguments: { checkpoint_id: 'handoff_outside_scope' } } });
|
|
68
81
|
assert.equal(denied.isError, true);
|
|
69
82
|
assert.match(denied.content[0].text, /outside this native session scope/);
|
|
83
|
+
|
|
84
|
+
// 引用会话:元数据廉价读取(无内嵌消息时 messageCount 为 null)
|
|
85
|
+
const info = JSON.parse((await dispatch({ id: 4, method: 'tools/call', params: { name: 'get_session_info', arguments: { session_id: sessionRef } } })).content[0].text);
|
|
86
|
+
assert.equal(info.harnessId, 'pi');
|
|
87
|
+
assert.equal(info.title, '旧会话');
|
|
88
|
+
assert.equal(info.messageCount, null);
|
|
89
|
+
assert.equal(info.branch, null);
|
|
90
|
+
// 引用会话:正文分页(offset 0 = 最近一页,nextOffset 逐页向更早翻)
|
|
91
|
+
const recent = JSON.parse((await dispatch({ id: 5, method: 'tools/call', params: { name: 'list_session_messages', arguments: { session_id: sessionRef, limit: 12 } } })).content[0].text);
|
|
92
|
+
assert.equal(recent.messageCount, 30);
|
|
93
|
+
assert.equal(recent.returned, 12);
|
|
94
|
+
assert.equal(recent.hasMore, true);
|
|
95
|
+
assert.equal(recent.nextOffset, 12);
|
|
96
|
+
assert.match(recent.transcript, /User: turn-28/);
|
|
97
|
+
assert.doesNotMatch(recent.transcript, /turn-17/);
|
|
98
|
+
const oldest = JSON.parse((await dispatch({ id: 6, method: 'tools/call', params: { name: 'list_session_messages', arguments: { session_id: sessionRef, offset: 24, limit: 12 } } })).content[0].text);
|
|
99
|
+
assert.equal(oldest.returned, 6);
|
|
100
|
+
assert.equal(oldest.hasMore, false);
|
|
101
|
+
assert.equal(oldest.nextOffset, null);
|
|
102
|
+
assert.match(oldest.transcript, /User: turn-0/);
|
|
103
|
+
// 未被用户引用的会话拒绝读取
|
|
104
|
+
const otherRef = Buffer.from(JSON.stringify(['pi', 'past-2'])).toString('base64url');
|
|
105
|
+
const deniedSession = await dispatch({ id: 7, method: 'tools/call', params: { name: 'get_session_info', arguments: { session_id: otherRef } } });
|
|
106
|
+
assert.equal(deniedSession.isError, true);
|
|
107
|
+
assert.match(deniedSession.content[0].text, /not referenced/);
|
|
70
108
|
} finally {
|
|
71
109
|
if (oldUrl === undefined) delete process.env.HARNESS_MIX_HANDOFF_URL; else process.env.HARNESS_MIX_HANDOFF_URL = oldUrl;
|
|
72
110
|
if (oldKey === undefined) delete process.env.HARNESS_MIX_HANDOFF_KEY; else process.env.HARNESS_MIX_HANDOFF_KEY = oldKey;
|
|
@@ -78,7 +78,7 @@ async function main() {
|
|
|
78
78
|
const acp = acpServers(selected.servers, collab);
|
|
79
79
|
assert.equal(acp.length, 2); assert.equal(acp[1].name, 'harness-mix'); assert.deepEqual(acp[0].env, []);
|
|
80
80
|
assert.ok(namedServers(selected.servers, collab)['hm-user-probe']);
|
|
81
|
-
const catalog = await manager.catalog(); assert.equal(catalog.harnesses.length,
|
|
81
|
+
const catalog = await manager.catalog(); assert.equal(catalog.harnesses.length, 17);
|
|
82
82
|
for (const a of adapters.values()) {
|
|
83
83
|
const entry = catalog.harnesses.find(h => h.id === a.manifest.id);
|
|
84
84
|
assert.equal(entry.mcp, a.manifest.integrations?.mcp === true);
|
|
@@ -93,6 +93,9 @@ async function main() {
|
|
|
93
93
|
assert.deepEqual(manager.roots(adapters.get('zcode'), { scope: 'project', cwd: project }), [path.join(project, '.zcode/skills'), path.join(project, '.agents/skills')]);
|
|
94
94
|
// Trae IDE (.trae) and TraeCode CLI (.traecli) share one managed root set.
|
|
95
95
|
assert.deepEqual(manager.roots(adapters.get('trae'), { scope: 'project', cwd: project }), [path.join(project, '.trae/skills'), path.join(project, '.traecli/skills'), path.join(project, '.agents/skills')]);
|
|
96
|
+
// Cline 官方文档:全局 ~/.cline/skills 与项目 .cline/skills。
|
|
97
|
+
assert.deepEqual(manager.roots(adapters.get('cline'), { scope: 'global', cwd: null }), [path.join(home, '.cline/skills')]);
|
|
98
|
+
assert.deepEqual(manager.roots(adapters.get('cline'), { scope: 'project', cwd: project }), [path.join(project, '.cline/skills')]);
|
|
96
99
|
// Hermes documents no project-scope skills directory, so only its global root is managed.
|
|
97
100
|
assert.deepEqual(manager.roots(adapters.get('hermes'), { scope: 'project', cwd: project }), []);
|
|
98
101
|
assert.deepEqual(manager.roots(adapters.get('hermes'), { scope: 'global', cwd: null }), [path.join(home, '.hermes/skills')]);
|
|
@@ -116,7 +119,7 @@ async function main() {
|
|
|
116
119
|
const globalOnly = await blocked.ensureSkillRoots({ cwd: 'relative-not-absolute' }, adapters.get('codex'), {});
|
|
117
120
|
assert.equal(globalOnly.length, 1);
|
|
118
121
|
// Exercise the real protocol dispatcher without starting native processes.
|
|
119
|
-
assert.equal((await NativeProtocol.prototype.request.call({ runtime }, 'codexhost/integrations/catalog', {})).harnesses.length,
|
|
122
|
+
assert.equal((await NativeProtocol.prototype.request.call({ runtime }, 'codexhost/integrations/catalog', {})).harnesses.length, 17);
|
|
120
123
|
assert.equal((await NativeProtocol.prototype.request.call({ runtime }, 'codexhost/integrations/list', local)).skills.length, 3);
|
|
121
124
|
console.log('integrations: scope precedence, persistence, native status projection, no credential fields, unsupported capabilities, skill install/disable/restore, links, and protocol PASS');
|
|
122
125
|
}
|
|
@@ -57,6 +57,24 @@ if (process.argv.includes('--fixture')) {
|
|
|
57
57
|
assert.deepEqual(nativeCommand(module.manifest.id, ['acp']), { command: process.execPath, args: ['acp'] });
|
|
58
58
|
} finally { if (previous === undefined) delete process.env[key]; else process.env[key] = previous; }
|
|
59
59
|
}
|
|
60
|
+
// Cline:官方 `cline --acp`;诚实能力声明(无 plan/原生 diff/thinking 档/独立提问),
|
|
61
|
+
// 保留 resume 与 plan/act 权限模式;命令只认 cline 与 HARNESS_MIX_CLINE_EXECUTABLE。
|
|
62
|
+
{
|
|
63
|
+
const module = require('../src/main/adapters/cline');
|
|
64
|
+
const caps = module.manifest.capabilities;
|
|
65
|
+
assert.deepEqual(module.manifest.integrations.skills, { global: ['.cline/skills'], project: ['.cline/skills'] });
|
|
66
|
+
for (const [capability, expected] of [['fork', false], ['questions', false], ['thinkingLevels', false], ['plan', false], ['nativeDiff', false], ['usage', false], ['resume', true], ['permissionModes', true], ['models', true], ['attachments', true], ['approvals', true]])
|
|
67
|
+
assert.equal(caps[capability], expected, `cline ${capability}`);
|
|
68
|
+
const previous = process.env.HARNESS_MIX_CLINE_EXECUTABLE;
|
|
69
|
+
try {
|
|
70
|
+
delete process.env.HARNESS_MIX_CLINE_EXECUTABLE;
|
|
71
|
+
process.env.HARNESS_MIX_CLINE_EXECUTABLE = __filename + '.missing';
|
|
72
|
+
assert.throws(() => nativeCommand('cline', ['--acp']), /未安装/);
|
|
73
|
+
assert.equal((await module.create().inspect()).available, false);
|
|
74
|
+
process.env.HARNESS_MIX_CLINE_EXECUTABLE = process.execPath;
|
|
75
|
+
assert.deepEqual(nativeCommand('cline', ['--acp']), { command: process.execPath, args: ['--acp'] });
|
|
76
|
+
} finally { if (previous === undefined) delete process.env.HARNESS_MIX_CLINE_EXECUTABLE; else process.env.HARNESS_MIX_CLINE_EXECUTABLE = previous; }
|
|
77
|
+
}
|
|
60
78
|
const adapter = acpAdapter({ id: 'fixture', name: 'Fixture', bin: () => ({ command: process.execPath, args: [__filename, '--fixture'] }), args: [], requestTimeoutMs: 5000 }).create();
|
|
61
79
|
const events = [];
|
|
62
80
|
let session;
|
|
@@ -47,7 +47,7 @@ if (process.argv.includes('--fixture')) {
|
|
|
47
47
|
});
|
|
48
48
|
} else {
|
|
49
49
|
(async () => {
|
|
50
|
-
for (const vendor of ['codebuddy', 'kiro-cli', 'cursor-cli', 'qoder', 'zcode', 'trae']) {
|
|
50
|
+
for (const vendor of ['codebuddy', 'kiro-cli', 'cursor-cli', 'qoder', 'zcode', 'trae', 'cline']) {
|
|
51
51
|
const module = nativeAcp({ id: vendor, name: vendor, args: [], timeoutMs: 2000, command: () => ({ command: process.execPath, args: [__filename, '--fixture'] }) });
|
|
52
52
|
const adapter = module.create(), events = [];
|
|
53
53
|
let s;
|
|
@@ -3,6 +3,7 @@ const fs = require('node:fs');
|
|
|
3
3
|
const os = require('node:os');
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const { nativeEnvironment, nativePaths, saveNativeSettings } = require('../src/main/native/config');
|
|
6
|
+
const { isCodexTaskEnvironment, readLiveHostInstance } = require('../src/main/native/launcher');
|
|
6
7
|
|
|
7
8
|
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-mix-config-'));
|
|
8
9
|
try {
|
|
@@ -18,8 +19,37 @@ try {
|
|
|
18
19
|
assert.equal(nativeEnvironment({ CODEXHOST_DATA_DIR: directory, CODEXHOST_PI_COMMAND: 'D:\\other\\pi.cmd' }).CODEXHOST_PI_COMMAND,
|
|
19
20
|
'D:\\other\\pi.cmd', 'Explicit environment takes priority');
|
|
20
21
|
for (const file of Object.values(nativePaths())) assert.ok(fs.statSync(file).isFile(), file);
|
|
22
|
+
const runtimeDirectory = path.join(directory, 'runtime');
|
|
23
|
+
fs.mkdirSync(runtimeDirectory, { recursive: true });
|
|
24
|
+
const instanceFile = path.join(runtimeDirectory, 'instance.json');
|
|
25
|
+
fs.writeFileSync(instanceFile, JSON.stringify({
|
|
26
|
+
pid: 4242,
|
|
27
|
+
beatAt: 1_000_000,
|
|
28
|
+
mode: 'native-host',
|
|
29
|
+
}));
|
|
30
|
+
assert.deepEqual(
|
|
31
|
+
readLiveHostInstance(directory, { now: 1_010_000, isPidAlive: pid => pid === 4242 }),
|
|
32
|
+
{ pid: 4242, beatAt: 1_000_000, mode: 'native-host' },
|
|
33
|
+
'A live Host makes repeated npm start idempotent',
|
|
34
|
+
);
|
|
35
|
+
assert.equal(
|
|
36
|
+
readLiveHostInstance(directory, { now: 1_020_000, isPidAlive: () => true }),
|
|
37
|
+
null,
|
|
38
|
+
'A stale Host heartbeat does not block a real launch',
|
|
39
|
+
);
|
|
40
|
+
assert.equal(
|
|
41
|
+
readLiveHostInstance(directory, { now: 1_010_000, isPidAlive: () => false }),
|
|
42
|
+
null,
|
|
43
|
+
'A dead Host PID does not block a real launch',
|
|
44
|
+
);
|
|
45
|
+
assert.equal(isCodexTaskEnvironment({ CODEX_THREAD_ID: 'thread-1' }), true);
|
|
46
|
+
assert.equal(isCodexTaskEnvironment({ CODEX_SESSION_ID: 'session-1' }), true);
|
|
47
|
+
assert.equal(
|
|
48
|
+
isCodexTaskEnvironment({ CODEX_INTERNAL_ORIGINATOR_OVERRIDE: 'Codex Desktop' }),
|
|
49
|
+
true,
|
|
50
|
+
);
|
|
51
|
+
assert.equal(isCodexTaskEnvironment({}), false);
|
|
21
52
|
console.log('Native configuration, AppX environment recovery, credential exclusion and packaged resources passed');
|
|
22
53
|
} finally {
|
|
23
|
-
fs.rmSync(
|
|
24
|
-
fs.rmdirSync(directory);
|
|
54
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
25
55
|
}
|
|
@@ -16,13 +16,20 @@ async function main() {
|
|
|
16
16
|
const adapter = { manifest: { id: 'pi', name: 'Pi', capabilities: { streaming: true, models: true, approvals: true, questions: true, resume: true } },
|
|
17
17
|
async open(input) { emits.push(input.emit); emit = input.emit; return {}; },
|
|
18
18
|
async describe() { return { models: [{ id: 'demo', name: 'Demo', provider: 'test' }], thinkingLevels: [{ id: 'high', label: 'High', default: true }, { id: 'low', label: 'Low' }], permissionModes: [] }; },
|
|
19
|
-
async send() {}, async cancel() {}, async close() {},
|
|
19
|
+
async send(session, text, hooks, extras) { sendExtras.push(extras); }, async cancel() {}, async close() {},
|
|
20
20
|
async respond(session, id, answer) { answers.push({ id, answer }); } };
|
|
21
|
+
const sendExtras = [];
|
|
21
22
|
runtime.adapters.set('pi', adapter); runtime.status.pi = { available: true };
|
|
22
23
|
const events = [];
|
|
24
|
+
let observeQueueOrder = false;
|
|
25
|
+
let queueResponseResolved = false;
|
|
26
|
+
let queueNotificationBeforeResponse = false;
|
|
23
27
|
const section = { id: 'test-pinned-section', name: 'Pinned', appearance: null };
|
|
24
28
|
const officialRequests = [];
|
|
25
|
-
const bridge = new NativeProtocol(runtime, event =>
|
|
29
|
+
const bridge = new NativeProtocol(runtime, event => {
|
|
30
|
+
if (observeQueueOrder && event?.method === 'thread/queue/changed' && !queueResponseResolved) queueNotificationBeforeResponse = true;
|
|
31
|
+
events.push(event);
|
|
32
|
+
}, async (method, params) => {
|
|
26
33
|
officialRequests.push({ method, params });
|
|
27
34
|
if (method === 'threadSection/list') return { data: [section], nextCursor: null };
|
|
28
35
|
if (method === 'account/read') return { account: { type: 'chatgpt', email: 'native@example.com', planType: 'plus' }, requiresOpenaiAuth: true };
|
|
@@ -149,13 +156,18 @@ async function main() {
|
|
|
149
156
|
await wait(() => !runtime.threads.find(t => t.id === prewarmed.thread.id).reviewPending && !runtime.sending.has(prewarmed.thread.id));
|
|
150
157
|
emit = emits[0]; // 恢复主线程的事件源(open 顺序:主线程序,预热线程后)
|
|
151
158
|
schemas.threadInspectionSchema.parse(await bridge.request('codexhost/thread/inspect', { threadId }));
|
|
152
|
-
const turn = await bridge.request('turn/start', { threadId, input: [{ type: 'text', text: 'test' }] });
|
|
159
|
+
const turn = await bridge.request('turn/start', { threadId, input: [{ type: 'text', text: 'test' }], approvalPolicy: 'never', sandboxPolicy: { type: 'dangerFullAccess' } });
|
|
160
|
+
// turn/start 在 currentTurn 建立后即返回,adapter.send 的调用在其后;等待透传到达
|
|
161
|
+
await wait(() => sendExtras.at(-1)?.turnPermissions != null);
|
|
162
|
+
assert.deepEqual(sendExtras.at(-1).turnPermissions, { approvalPolicy: 'never', sandboxPolicy: { type: 'dangerFullAccess' } }, 'turn/start 权限参数透传到适配器');
|
|
153
163
|
assert.ok(runtime.core.getTurn(turn.turn.id), 'Native turn IDs come from the existing ProtocolCore');
|
|
154
164
|
emit({ kind: 'thinking-delta', text: 'reason' });
|
|
155
165
|
emit({ kind: 'text-delta', text: 'hello' });
|
|
156
166
|
emit({ kind: 'text-delta', text: ' world' });
|
|
157
167
|
emit({ kind: 'tool', toolCallId: 't', title: 'Read', state: 'running', input: '{}' });
|
|
158
168
|
emit({ kind: 'tool', toolCallId: 't', title: 'Read', state: 'completed', output: 'done' });
|
|
169
|
+
emit({ kind: 'tool', toolCallId: 'sh', title: 'Bash', state: 'running', input: '{"command":"cat package.json"}' });
|
|
170
|
+
emit({ kind: 'tool', toolCallId: 'sh', title: 'Bash', state: 'completed', output: '{}' });
|
|
159
171
|
emit({ kind: 'approval', requestId: 'permission', method: 'confirm', title: 'Allow?' });
|
|
160
172
|
const approval = events.find(e => e.id?.startsWith('harness-mix:approval:'));
|
|
161
173
|
assert.equal(answers.length, 0, 'Approval is not fabricated');
|
|
@@ -169,7 +181,13 @@ async function main() {
|
|
|
169
181
|
emit({ kind: 'completed', finalAnswer: true });
|
|
170
182
|
await wait(() => !runtime.threads.find(t => t.id === threadId).reviewPending && !runtime.sending.has(threadId));
|
|
171
183
|
assert.equal(events.filter(e => e.method === 'item/agentMessage/delta').map(e => e.params.delta).join(''), 'hello world');
|
|
172
|
-
|
|
184
|
+
// 非终端工具投影为 dynamicToolCall(摘要显示真实工具名),终端命令投影为原生 commandExecution
|
|
185
|
+
assert.ok(events.some(e => e.method === 'item/completed' && e.params.item.type === 'dynamicToolCall' && e.params.item.tool === 'Read'));
|
|
186
|
+
const execItem = events.filter(e => e.method === 'item/completed').map(e => e.params.item).find(i => i.type === 'commandExecution');
|
|
187
|
+
assert.equal(execItem?.command, 'cat package.json', 'Shell tool projects the real command text');
|
|
188
|
+
assert.equal(execItem?.commandActions?.[0]?.type, 'read', 'Simple cat command classifies as a read action');
|
|
189
|
+
assert.equal(execItem?.commandActions?.[0]?.path, 'package.json');
|
|
190
|
+
assert.equal(typeof execItem?.durationMs, 'number', 'Command execution carries a real duration');
|
|
173
191
|
// Desktop 以 `diff --git a/x b/x` 切分文件并提取路径,且只在 @@ hunk 头之后计数增删行
|
|
174
192
|
const turnDiff = events.filter(e => e.method === 'turn/diff/updated').at(-1).params.diff;
|
|
175
193
|
assert.ok(turnDiff.includes('diff --git a/a.txt b/a.txt'), 'turn diff carries git-style file headers');
|
|
@@ -177,11 +195,26 @@ async function main() {
|
|
|
177
195
|
assert.ok(turnDiff.includes('@@ -0,0 +1,1 @@') && turnDiff.includes('+hello'), 'turn diff carries a countable hunk');
|
|
178
196
|
assert.ok(events.some(e => e.method === 'turn/diff/updated' && e.params.diff.includes('a.txt')));
|
|
179
197
|
assert.equal(events.filter(e => e.method === 'turn/completed' && e.params.threadId === threadId).length, 1);
|
|
198
|
+
// Desktop「已处理/用时」计时契约:item/started 带 startedAtMs、item/completed 带
|
|
199
|
+
// completedAtMs,终态 Turn 带 startedAt/completedAt(epoch 秒)与 durationMs(毫秒);
|
|
200
|
+
// 缺这些字段时 Desktop 无法合成 worked-for 计时项,完成回合只显示集成摘要。
|
|
201
|
+
const completedTurn = events.filter(e => e.method === 'turn/completed' && e.params.threadId === threadId && e.params.turn.status === 'completed').at(-1).params.turn;
|
|
202
|
+
assert.equal(typeof completedTurn.durationMs, 'number', 'Completed turn carries durationMs for the Desktop worked-for timer');
|
|
203
|
+
assert.ok(Number.isInteger(completedTurn.startedAt) && Number.isInteger(completedTurn.completedAt), 'Completed turn carries epoch-second startedAt/completedAt');
|
|
204
|
+
assert.ok(completedTurn.durationMs >= 0 && completedTurn.completedAt >= completedTurn.startedAt, 'Turn timing fields are coherent');
|
|
205
|
+
assert.ok(events.filter(e => e.method === 'item/started').every(e => typeof e.params.startedAtMs === 'number'), 'item/started carries startedAtMs');
|
|
206
|
+
assert.ok(events.filter(e => e.method === 'item/completed').every(e => typeof e.params.completedAtMs === 'number'), 'item/completed carries completedAtMs');
|
|
180
207
|
const history = await bridge.request('thread/read', { threadId });
|
|
181
208
|
assert.equal(history.thread.turns[0].status, 'completed');
|
|
209
|
+
assert.equal(typeof history.thread.turns[0].durationMs, 'number', 'Restored turns keep durationMs');
|
|
182
210
|
const projectedChange = history.thread.turns[0].items.flatMap(i => i.type === 'fileChange' ? i.changes : []).find(change => change.path === 'a.txt');
|
|
183
211
|
assert.equal(projectedChange?.kind.type, 'add', 'fileChange item projects the added kind');
|
|
184
212
|
assert.ok(projectedChange?.diff.includes('diff --git a/a.txt b/a.txt') && projectedChange.diff.includes('@@ -0,0 +1,1 @@'), 'fileChange item carries a full unified diff');
|
|
213
|
+
await bridge.request('thread/settings/update', { threadId, approvalPolicy: 'never', sandboxPolicy: { type: 'dangerFullAccess' } });
|
|
214
|
+
assert.deepEqual(runtime.threads.find(t => t.id === threadId).options.turnPermissions, { approvalPolicy: 'never', sandboxPolicy: { type: 'dangerFullAccess' } }, 'Desktop 权限菜单设置存储到线程');
|
|
215
|
+
const resumed = await bridge.request('thread/resume', { threadId });
|
|
216
|
+
assert.equal(resumed.approvalPolicy, 'never', 'thread/resume 回显实际生效的权限');
|
|
217
|
+
assert.deepEqual(resumed.sandbox, { type: 'dangerFullAccess' }, 'thread/resume 回显实际生效的沙箱');
|
|
185
218
|
await bridge.request('thread/name/set', { threadId, name: 'Local Core' });
|
|
186
219
|
await bridge.request('thread/archive', { threadId });
|
|
187
220
|
assert.equal(runtime.threads.find(t => t.id === threadId).archived, true);
|
|
@@ -341,13 +374,18 @@ async function main() {
|
|
|
341
374
|
const qThread = await bridge.request('thread/start', { cwd: root, model: routeModel('pi') });
|
|
342
375
|
const qThreadId = qThread.thread.id;
|
|
343
376
|
assert.deepEqual(await bridge.request('thread/queue/list', { threadId: qThreadId }), { data: [], nextCursor: null });
|
|
377
|
+
observeQueueOrder = true;
|
|
344
378
|
const q1 = await bridge.request('thread/queue/add', {
|
|
345
379
|
threadId: qThreadId,
|
|
346
380
|
input: [{ type: 'text', text: '排队补充需求 1' }],
|
|
347
381
|
clientUserMessageId: 'client-msg-1',
|
|
348
382
|
});
|
|
383
|
+
queueResponseResolved = true;
|
|
349
384
|
assert.equal(typeof q1.queuedSubmission.id, 'string');
|
|
350
385
|
assert.equal(q1.queuedSubmission.clientUserMessageId, 'client-msg-1');
|
|
386
|
+
await wait(() => events.at(-1)?.method === 'thread/queue/changed' && events.at(-1)?.params?.threadId === qThreadId);
|
|
387
|
+
assert.equal(queueNotificationBeforeResponse, false, 'Queue mutation response precedes changed notification so Desktop edits keep the current id');
|
|
388
|
+
observeQueueOrder = false;
|
|
351
389
|
assert.equal(events.at(-1)?.method, 'thread/queue/changed');
|
|
352
390
|
assert.equal(events.at(-1)?.params?.threadId, qThreadId);
|
|
353
391
|
|
|
@@ -388,6 +426,24 @@ async function main() {
|
|
|
388
426
|
assert.equal(qList.data.length, 1);
|
|
389
427
|
assert.equal(qList.data[0].id, q1.queuedSubmission.id);
|
|
390
428
|
|
|
429
|
+
// 输入准备或原生启动失败时不得丢失排队项,仍可编辑后重试。
|
|
430
|
+
await bridge.request('thread/queue/update', {
|
|
431
|
+
threadId: qThreadId,
|
|
432
|
+
queuedSubmissionId: q1.queuedSubmission.id,
|
|
433
|
+
input: [{ type: 'unsupported-test-input' }],
|
|
434
|
+
});
|
|
435
|
+
await assert.rejects(bridge.request('thread/queue/start', {
|
|
436
|
+
threadId: qThreadId,
|
|
437
|
+
queuedSubmissionId: q1.queuedSubmission.id,
|
|
438
|
+
}), /Unsupported native input type/);
|
|
439
|
+
qList = await bridge.request('thread/queue/list', { threadId: qThreadId });
|
|
440
|
+
assert.equal(qList.data[0]?.id, q1.queuedSubmission.id, 'Failed queue start keeps the item for editing or retry');
|
|
441
|
+
await bridge.request('thread/queue/update', {
|
|
442
|
+
threadId: qThreadId,
|
|
443
|
+
queuedSubmissionId: q1.queuedSubmission.id,
|
|
444
|
+
input: [{ type: 'text', text: '修正后的排队消息' }],
|
|
445
|
+
});
|
|
446
|
+
|
|
391
447
|
// 空闲状态启动排队消息
|
|
392
448
|
const startedQ = await bridge.request('thread/queue/start', {
|
|
393
449
|
threadId: qThreadId,
|
|
@@ -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 { 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 { installRendererSettingsLifecycle } from './src/native-ui/renderer-extension/src/harness-mix-settings.ts'; export { createAccountsSettingsPage } from './src/native-ui/renderer-extension/src/settings/accounts-page.ts'; export { createStorageSettingsPage } from './src/native-ui/renderer-extension/src/settings/storage-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' });
|
|
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 { installRendererSettingsLifecycle } from './src/native-ui/renderer-extension/src/harness-mix-settings.ts'; export { createAccountsSettingsPage } from './src/native-ui/renderer-extension/src/settings/accounts-page.ts'; export { createStorageSettingsPage } from './src/native-ui/renderer-extension/src/settings/storage-page.ts'; export { createSkinSettingsPage } from './src/native-ui/renderer-extension/src/settings/skin-market.ts'; export { applyRendererSkin } from './src/native-ui/renderer-extension/src/settings/skin-runtime.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', '.webp': '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}`);
|
|
@@ -22,7 +22,7 @@ app.whenReady().then(async () => {
|
|
|
22
22
|
lifecycle.dispose();
|
|
23
23
|
return pageIds;
|
|
24
24
|
})()`);
|
|
25
|
-
assert.deepEqual(liveSettingsPages, ['connections', 'accounts', 'mcp', 'skills', 'session-import', 'storage', 'updates', 'about']);
|
|
25
|
+
assert.deepEqual(liveSettingsPages, ['connections', 'accounts', 'mcp', 'skills', 'session-import', 'storage', 'skins', 'pets', 'updates', 'about']);
|
|
26
26
|
const { NativeProtocol } = require('../src/main/native/protocol');
|
|
27
27
|
const nativeThreads = ['pi', 'claude', 'dsh', 'antigravity'].map((harnessId, index) => ({ id: 'sidebar-' + index, harnessId, model: { id: 'model-' + index, provider: 'native' }, options: {} }));
|
|
28
28
|
const protocol = new NativeProtocol({ threads: nativeThreads, getThread: id => nativeThreads.find(thread => thread.id === id), subscribe: () => () => {}, core: { subscribe: () => () => {} }, describe: async () => ({}), getCapabilities: () => ({}) }, () => {});
|
|
@@ -137,6 +137,7 @@ app.whenReady().then(async () => {
|
|
|
137
137
|
inspectStorage:async () => ({storageSchemaVersion:3,threadCount:12,loadedThreadCount:2,indexBytes:2048,recordBytes:5242880,recordCount:12,legacyBytes:0}),
|
|
138
138
|
optimizeStorage:async () => ({before:{storageSchemaVersion:3,threadCount:12,loadedThreadCount:2,indexBytes:2048,recordBytes:5242880,recordCount:12,legacyBytes:0},after:{storageSchemaVersion:3,threadCount:12,loadedThreadCount:2,indexBytes:2048,recordBytes:4194304,recordCount:12,legacyBytes:0}}),
|
|
139
139
|
})),
|
|
140
|
+
NativeUI.createSkinSettingsPage(messages),
|
|
140
141
|
NativeUI.aboutPage(messages, () => ({ readCurrentVersion:async () => ({version:'0.1.4'}), checkUpdate:async () => { throw new Error('not expected'); } })),
|
|
141
142
|
], messages, document);
|
|
142
143
|
shell.openSettings(undefined, 'accounts');
|
|
@@ -156,6 +157,35 @@ app.whenReady().then(async () => {
|
|
|
156
157
|
assert.match(storageText, /Schema v3/);
|
|
157
158
|
assert.match(storageText, /12 个任务/);
|
|
158
159
|
fs.writeFileSync(path.join(out, 'settings-storage.png'), (await win.webContents.capturePage()).toPNG());
|
|
160
|
+
const skinResult = await win.webContents.executeJavaScript(`(async () => {
|
|
161
|
+
globalThis.__codexhostSettingsShellV1.openSettings(undefined, 'skins');
|
|
162
|
+
const shadow = globalThis.__codexhostSettingsShellV1.root.shadowRoot;
|
|
163
|
+
const firstButton = shadow.querySelector('button[data-skin-action="styler-nocturne-studio"]');
|
|
164
|
+
const mikuButton = shadow.querySelector('button[data-skin-action="miku-488137"]');
|
|
165
|
+
firstButton.click();
|
|
166
|
+
mikuButton.click();
|
|
167
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
168
|
+
return {
|
|
169
|
+
text: shadow.textContent,
|
|
170
|
+
active: document.documentElement.getAttribute('data-harness-mix-skin'),
|
|
171
|
+
style: Boolean(document.getElementById('harness-mix-renderer-skin')),
|
|
172
|
+
cardCount: shadow.querySelectorAll('.skin-card').length,
|
|
173
|
+
bundledPreviewCount: [...shadow.querySelectorAll('.skin-preview')].filter(preview => preview.style.backgroundImage.includes('data:image/webp')).length,
|
|
174
|
+
};
|
|
175
|
+
})()`);
|
|
176
|
+
assert.match(skinResult.text, /Native Codex.*Miku 488137.*原神 · 晨曦.*鸣潮 · 共鸣.*龙珠 · 筋斗云.*金辉盛境.*夜曲工作室.*静谧花园/s);
|
|
177
|
+
assert.equal(skinResult.active, 'miku-488137');
|
|
178
|
+
assert.equal(skinResult.style, true);
|
|
179
|
+
assert.equal(skinResult.cardCount, 17);
|
|
180
|
+
assert.equal(skinResult.bundledPreviewCount, 16);
|
|
181
|
+
fs.writeFileSync(path.join(out, 'settings-skins.png'), (await win.webContents.capturePage()).toPNG());
|
|
182
|
+
await win.webContents.executeJavaScript(`(() => {
|
|
183
|
+
const shadow = globalThis.__codexhostSettingsShellV1.root.shadowRoot;
|
|
184
|
+
const page = shadow.querySelector('.settings-page');
|
|
185
|
+
page.scrollTop = page.scrollHeight;
|
|
186
|
+
})()`);
|
|
187
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
188
|
+
fs.writeFileSync(path.join(out, 'settings-skins-discovery.png'), (await win.webContents.capturePage()).toPNG());
|
|
159
189
|
const aboutText = await win.webContents.executeJavaScript(`(async () => {
|
|
160
190
|
globalThis.__codexhostSettingsShellV1.openSettings(undefined, 'about');
|
|
161
191
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
@@ -163,6 +193,101 @@ app.whenReady().then(async () => {
|
|
|
163
193
|
})()`);
|
|
164
194
|
assert.match(aboutText, /v0\.1\.4/);
|
|
165
195
|
fs.writeFileSync(path.join(out, 'settings-about.png'), (await win.webContents.capturePage()).toPNG());
|
|
196
|
+
await win.setSize(1055, 616);
|
|
197
|
+
const skinShowcase = await win.webContents.executeJavaScript(`(async () => {
|
|
198
|
+
document.body.innerHTML = '<div id="root"><aside class="app-shell-left-panel"><button aria-haspopup="menu" aria-label="Open Codex menu"><span>Codex</span></button><nav><strong>新建任务</strong><span>拉取请求</span><span>云端</span><span>已安排</span><span>插件</span><small>项目</small><span class="active" data-app-action-sidebar-thread-active="true">Miku Codex</span><span>开源项目</span><span>Harness Mix</span></nav><footer>Harness Mix</footer></aside><main class="main-surface" data-app-shell-main-surface="default"><section class="welcome"><h1>你想让我们在 Miku Codex 中构建什么?</h1><div class="suggestions"><button class="bg-card">探索并理解代码</button><button class="bg-card">构建新功能、应用或工具</button><button class="bg-card">审查代码并提出修改建议</button><button class="bg-card">修复问题和失败</button></div></section><section class="composer-surface-chrome"><small>Miku Codex 本地 main</small><div>随心输入</div><footer>+ 完全访问 <span>5.6 Luna ↑</span></footer></section></main></div>';
|
|
199
|
+
const fixtureStyle = document.createElement('style');
|
|
200
|
+
fixtureStyle.textContent = '*{box-sizing:border-box}html,body,#root{width:100%;height:100%;margin:0}body{padding:0!important;overflow:hidden;font-family:Arial,"Microsoft YaHei",sans-serif}#root{display:flex}.app-shell-left-panel{display:flex;flex:0 0 204px;flex-direction:column;padding:6px 10px 12px}.app-shell-left-panel button{border:0;color:inherit;font:700 16px inherit;text-align:left}.app-shell-left-panel nav{display:flex;flex-direction:column;gap:12px;padding:5px 2px;font-size:13px}.app-shell-left-panel nav small{margin-top:6px;opacity:.55}.app-shell-left-panel nav .active{padding:7px;border-radius:7px}.app-shell-left-panel>footer{margin-top:auto;font-size:12px}.main-surface{position:relative;display:flex;flex:1;align-items:center;justify-content:center;overflow:hidden}.welcome{width:min(660px,70vw);text-align:center;transform:translateY(-35px)}.welcome h1{margin:0 0 26px;font-size:23px;font-weight:500}.suggestions{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.suggestions button{min-height:76px;padding:12px;border:1px solid #fff8;border-radius:9px;color:inherit;text-align:left;font-weight:600}.composer-surface-chrome{position:absolute;right:13%;bottom:12px;left:13%;min-height:94px;padding:12px 18px;border:1px solid transparent;border-radius:16px}.composer-surface-chrome>div{padding:13px 0;color:#52617a99}.composer-surface-chrome footer{font-size:11px}.composer-surface-chrome footer span{float:right}';
|
|
201
|
+
document.head.append(fixtureStyle);
|
|
202
|
+
NativeUI.applyRendererSkin('miku-488137', document);
|
|
203
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
204
|
+
const root = getComputedStyle(document.getElementById('root'));
|
|
205
|
+
const main = getComputedStyle(document.querySelector('.main-surface'));
|
|
206
|
+
const logo = getComputedStyle(document.querySelector('.app-shell-left-panel button'));
|
|
207
|
+
const polaroid = getComputedStyle(document.body, '::after');
|
|
208
|
+
return { rootBackground:root.backgroundImage, mainBackground:main.backgroundImage, logoBackground:logo.backgroundImage, polaroidBackground:polaroid.backgroundImage };
|
|
209
|
+
})()`);
|
|
210
|
+
assert.match(skinShowcase.rootBackground, /data:image\/webp/);
|
|
211
|
+
assert.match(skinShowcase.mainBackground, /linear-gradient/);
|
|
212
|
+
assert.match(skinShowcase.logoBackground, /data:image\/webp/);
|
|
213
|
+
assert.match(skinShowcase.polaroidBackground, /data:image\/webp/);
|
|
214
|
+
fs.writeFileSync(path.join(out, 'skin-miku-full.png'), (await win.webContents.capturePage()).toPNG());
|
|
215
|
+
await win.setSize(1024, 716);
|
|
216
|
+
const responsiveHome = await win.webContents.executeJavaScript(`(async () => {
|
|
217
|
+
document.body.innerHTML = '<div id="root"><aside class="app-shell-left-panel"></aside><main class="main-surface" data-app-shell-main-surface="default"><div data-app-shell-main-content-top-fade="full-bleed"></div><header data-pip-obstacle="app-shell-header"><div data-testid="app-shell-header-context-menu-surface"><div data-app-shell-page-header="true"><div data-app-shell-header-toolbar="true"><div class="thread-title-surface"><div>新任务</div></div><button class="header-action" aria-label="聊天操作" aria-haspopup="menu" data-state="open">...</button></div></div></div></header><div role="main" style="container-type:size;container-name:home-main-content;width:100%;height:100%;display:flex;flex-direction:column"><section class="_Hero_smoke_2"><h1 data-feature="game-source">你想让我们在 harness-mix 中构建什么?</h1></section><section class="[--thread-content-max-width:42rem]"><div data-composer-placement="home" class="composer-surface-chrome">随心输入<table><tbody><tr><td>响应式验证</td></tr></tbody></table></div></section></div></main></div>';
|
|
218
|
+
const fixtureStyle = document.createElement('style');
|
|
219
|
+
fixtureStyle.textContent = 'html,body,#root{width:100%;height:100%;margin:0}#root{display:flex}.app-shell-left-panel{flex:0 0 240px}.main-surface{position:relative;min-width:0;flex:1}.main-surface>header{position:absolute;z-index:2;width:100%;height:44px}.main-surface>header>div{display:flex;justify-content:space-between;padding:8px 12px}._Hero_smoke_2{display:flex;flex:0 0 44%;align-items:flex-end;justify-content:center;min-block-size:260px;padding-bottom:32px}h1{font-size:32px}.\\[--thread-content-max-width\\:42rem\\]{width:var(--thread-content-max-width);max-width:100%;margin:auto}.composer-surface-chrome{padding:24px}';
|
|
220
|
+
document.head.append(fixtureStyle);
|
|
221
|
+
NativeUI.applyRendererSkin('miku-488137', document);
|
|
222
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
223
|
+
const hero = getComputedStyle(document.querySelector('._Hero_smoke_2'));
|
|
224
|
+
const heading = getComputedStyle(document.querySelector('[data-feature="game-source"]'));
|
|
225
|
+
const header = getComputedStyle(document.querySelector('[data-pip-obstacle="app-shell-header"]'));
|
|
226
|
+
const headerAction = getComputedStyle(document.querySelector('.header-action'));
|
|
227
|
+
const titleSurface = getComputedStyle(document.querySelector('.thread-title-surface'));
|
|
228
|
+
const topFade = getComputedStyle(document.querySelector('[data-app-shell-main-content-top-fade]'));
|
|
229
|
+
const table = getComputedStyle(document.querySelector('table'));
|
|
230
|
+
return { flexBasis:hero.flexBasis, minBlockSize:parseFloat(hero.minBlockSize), fontSize:parseFloat(heading.fontSize), headerBackground:header.backgroundColor, headerActionBackground:headerAction.backgroundColor, titleBackground:titleSurface.backgroundColor, topFadeImage:topFade.backgroundImage, topFadeOpacity:topFade.opacity, tableBackground:table.backgroundColor };
|
|
231
|
+
})()`);
|
|
232
|
+
assert.equal(responsiveHome.flexBasis, 'auto');
|
|
233
|
+
assert.ok(responsiveHome.minBlockSize <= 200, `responsive hero remained too tall: ${responsiveHome.minBlockSize}px`);
|
|
234
|
+
assert.ok(responsiveHome.fontSize <= 28, `responsive heading remained too large: ${responsiveHome.fontSize}px`);
|
|
235
|
+
assert.equal(responsiveHome.headerBackground, 'rgba(0, 0, 0, 0)');
|
|
236
|
+
assert.equal(responsiveHome.headerActionBackground, 'rgba(0, 0, 0, 0)');
|
|
237
|
+
assert.equal(responsiveHome.titleBackground, 'rgba(0, 0, 0, 0)');
|
|
238
|
+
assert.equal(responsiveHome.topFadeImage, 'none');
|
|
239
|
+
assert.equal(responsiveHome.topFadeOpacity, '0');
|
|
240
|
+
assert.notEqual(responsiveHome.tableBackground, 'rgba(0, 0, 0, 0)');
|
|
241
|
+
fs.writeFileSync(path.join(out, 'skin-responsive-home.png'), (await win.webContents.capturePage()).toPNG());
|
|
242
|
+
await win.setSize(1920, 1000);
|
|
243
|
+
const wideThreadWidth = await win.webContents.executeJavaScript(`(() => getComputedStyle(document.querySelector('[class*="thread-content-max-width"]')).getPropertyValue('--thread-content-max-width').trim())()`);
|
|
244
|
+
assert.match(wideThreadWidth, /72rem/);
|
|
245
|
+
await win.setSize(1020, 716);
|
|
246
|
+
const readableConversation = await win.webContents.executeJavaScript(`(async () => {
|
|
247
|
+
document.body.innerHTML = '<div id="root"><aside class="app-shell-left-panel"><button aria-haspopup="menu" aria-label="Open Codex menu"><span>Codex</span></button><nav><strong>新建任务</strong><span>Pull Request</span><span>定时任务</span><span>插件</span><small>项目</small><span>snipaste-pro</span><span>harness-mix</span><span class="active" data-app-action-sidebar-thread-active="true">查找换肤界面差异原因</span></nav><footer>Harness Mix</footer></aside><main class="main-surface chat" data-app-shell-main-surface="default"><header>查找换肤界面差异原因</header><div class="chat-scroll"><article data-local-conversation-final-assistant><div data-response-annotation-conversation><small>用时 19秒</small><p>是的,准确说是修改了 Codex UI 的运行时表现层。</p><p>它会:</p><ul><li>注入 CSS、替换颜色变量和背景图。</li><li>把部分原生面板变成半透明。</li><li>增加 Logo、拍立得等纯视觉装饰。</li><li>保留原有按钮、输入框、会话和交互逻辑。</li></ul><p>它不会修改 Codex 的安装包、模型调用、账号、权限或会话归属。</p></div></article></div><section class="composer-surface-chrome"><div>随心输入</div><footer>+ 完全访问 <span>GPT-5.6 Sol ↑</span></footer></section></main></div>';
|
|
248
|
+
const chatStyle = document.createElement('style');
|
|
249
|
+
chatStyle.textContent = '.app-shell-left-panel button{background:transparent}.chat.main-surface{display:block;padding:0 22px}.chat>header{height:56px;padding:18px 4px;border-bottom:1px solid #6b789633;font-size:14px}.chat-scroll{height:calc(100% - 180px);overflow:auto;padding:42px 8px 80px}.chat article{max-width:760px;margin:0 auto;font-size:15px;line-height:1.7}.chat article small{opacity:.55}.chat article p{margin:13px 0}.chat article li{margin:4px 0}.chat .composer-surface-chrome{right:22px;bottom:14px;left:22px;background:#fff}';
|
|
250
|
+
document.head.append(chatStyle);
|
|
251
|
+
NativeUI.applyRendererSkin('dragonball-nimbus', document);
|
|
252
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
253
|
+
const response = getComputedStyle(document.querySelector('[data-response-annotation-conversation]'));
|
|
254
|
+
return { background:response.backgroundColor, color:response.color, padding:response.padding, radius:response.borderRadius };
|
|
255
|
+
})()`);
|
|
256
|
+
assert.notEqual(readableConversation.background, 'rgba(0, 0, 0, 0)');
|
|
257
|
+
assert.notEqual(readableConversation.background, 'transparent');
|
|
258
|
+
assert.equal(readableConversation.padding, '14px 16px 12px');
|
|
259
|
+
assert.equal(readableConversation.radius, '18px');
|
|
260
|
+
fs.writeFileSync(path.join(out, 'skin-conversation-readable.png'), (await win.webContents.capturePage()).toPNG());
|
|
261
|
+
const darkSurfaceResult = await win.webContents.executeJavaScript(`(async () => {
|
|
262
|
+
document.body.innerHTML = '<div id="root"><aside class="app-shell-left-panel"><button aria-haspopup="menu" aria-label="Open Codex menu"><span>Codex</span></button><nav><strong>新对话</strong><span>Pull Request</span><span>定时任务</span><span>插件</span><small>项目</small><span class="active" data-app-action-sidebar-thread-active="true">暗色皮肤全界面适配</span></nav></aside><main class="main-surface dark-fixture" data-app-shell-main-surface="default"><header>暗色皮肤全界面适配</header><div data-user-message-bubble="true"><div data-markdown-text-tone="user-message"><p data-markdown-han-text="true">暗色皮肤里的用户消息必须清晰可读。</p></div></div><article data-response-annotation-conversation><p>正文、工具状态、评审卡和输入区都需要保持清晰。</p><section class="review-card"><div>已编辑 3 个文件</div><button class="review-action">审核</button><button class="review-row">src/settings/skin-runtime.ts</button><button class="review-row">scripts/native-ui-smoke.cjs</button></section></article><section class="composer-real" data-composer-layout="multiline"><input placeholder="随心输入"><button>完全访问</button><button>GPT-5.6 Sol</button></section></main></div>';
|
|
263
|
+
const darkFixtureStyle = document.createElement('style');
|
|
264
|
+
darkFixtureStyle.textContent = 'html,body,#root{width:100%;height:100%;margin:0}.dark-fixture{position:relative;display:block;padding:24px}.dark-fixture>header{padding:12px 0}.dark-fixture>[data-user-message-bubble]{max-width:520px;margin:24px 0 0 auto;padding:12px 16px;border-radius:16px}.dark-fixture article{max-width:700px;margin:60px auto}.review-card{margin-top:24px;padding:14px;border:1px solid var(--color-token-border-default);border-radius:12px;background:color-mix(in srgb,var(--color-surface-elevated-secondary) 50%,transparent)}.review-card button{color:var(--color-text-primary)}.review-action{float:right;background:var(--color-background-primary-soft-alpha)}.review-row{display:block;width:100%;margin-top:10px;padding:10px;text-align:left;border:0;background:color-mix(in srgb,var(--color-surface) 70%,transparent)}.composer-real{position:absolute;right:24px;bottom:16px;left:24px;padding:18px;border-radius:20px;background:color-mix(in srgb,var(--color-surface-elevated-secondary) 86%,transparent)}.composer-real input{width:70%;padding:8px;color:var(--color-text-primary);background:transparent;border:0}.composer-real button{margin-left:8px;color:var(--color-text-secondary);background:transparent;border:0}';
|
|
265
|
+
document.head.append(darkFixtureStyle);
|
|
266
|
+
NativeUI.applyRendererSkin('dragonball-nimbus', document);
|
|
267
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
268
|
+
const before = { buttons:document.querySelectorAll('button').length, inputs:document.querySelectorAll('input').length };
|
|
269
|
+
const lightReviewBackground = getComputedStyle(document.querySelector('.review-card')).backgroundColor;
|
|
270
|
+
NativeUI.applyRendererSkin('genshin-night', document);
|
|
271
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
272
|
+
const review = getComputedStyle(document.querySelector('.review-card'));
|
|
273
|
+
const row = getComputedStyle(document.querySelector('.review-row'));
|
|
274
|
+
const composer = getComputedStyle(document.querySelector('.composer-real'));
|
|
275
|
+
const userBubble = getComputedStyle(document.querySelector('[data-user-message-bubble]'));
|
|
276
|
+
const userText = getComputedStyle(document.querySelector('[data-markdown-text-tone="user-message"]'));
|
|
277
|
+
const after = { buttons:document.querySelectorAll('button').length, inputs:document.querySelectorAll('input').length };
|
|
278
|
+
return { before, after, active:document.documentElement.getAttribute('data-harness-mix-skin'), lightReviewBackground, reviewBackground:review.backgroundColor, rowBackground:row.backgroundColor, composerBackground:composer.backgroundColor, color:review.color, userBubbleColor:userBubble.color, userTextColor:userText.color, userBubbleBackground:userBubble.backgroundColor };
|
|
279
|
+
})()`);
|
|
280
|
+
assert.deepEqual(darkSurfaceResult.after, darkSurfaceResult.before);
|
|
281
|
+
assert.equal(darkSurfaceResult.active, 'genshin-night');
|
|
282
|
+
assert.notEqual(darkSurfaceResult.reviewBackground, darkSurfaceResult.lightReviewBackground);
|
|
283
|
+
assert.doesNotMatch(darkSurfaceResult.reviewBackground, /255, 255, 255/);
|
|
284
|
+
assert.doesNotMatch(darkSurfaceResult.rowBackground, /255, 255, 255/);
|
|
285
|
+
assert.doesNotMatch(darkSurfaceResult.composerBackground, /255, 255, 255/);
|
|
286
|
+
assert.equal(darkSurfaceResult.color, 'rgb(240, 230, 200)');
|
|
287
|
+
assert.equal(darkSurfaceResult.userBubbleColor, 'rgb(240, 230, 200)');
|
|
288
|
+
assert.equal(darkSurfaceResult.userTextColor, 'rgb(240, 230, 200)');
|
|
289
|
+
assert.notEqual(darkSurfaceResult.userBubbleBackground, 'rgba(0, 0, 0, 0)');
|
|
290
|
+
fs.writeFileSync(path.join(out, 'skin-dark-all-surfaces.png'), (await win.webContents.capturePage()).toPNG());
|
|
166
291
|
win.destroy();
|
|
167
292
|
console.log('PASS: real Electron native picker, graphical Harness handoff, official Account/Usage, sharded Storage metrics, live About version and all project Harness/model SVGs load');
|
|
168
293
|
app.quit();
|