@harness-mix/cli 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +26 -96
  3. package/docs/chatgpt-web-sidebar.md +7 -1
  4. package/docs/cross-platform.md +15 -0
  5. package/docs/harness-handoff-design.md +377 -0
  6. package/docs/harness-management.md +28 -1
  7. package/docs/multi-agent-collaboration.md +9 -9
  8. package/docs/native-codex.md +21 -10
  9. package/output/native-build/harness-mix-shim.exe +0 -0
  10. package/output/native-build/renderer-extension.js +2596 -135
  11. package/package.json +20 -10
  12. package/scripts/chatgpt-context-ui-smoke.cjs +16 -0
  13. package/scripts/codex-accounts-test.cjs +81 -0
  14. package/scripts/codex-accounts-ui-smoke.cjs +70 -0
  15. package/scripts/collaboration-test.cjs +8 -3
  16. package/scripts/collaboration-ui-smoke.cjs +40 -26
  17. package/scripts/e2e-integrations.cjs +52 -0
  18. package/scripts/e2e-native-host.cjs +39 -0
  19. package/scripts/handoff-checkpoint-test.cjs +83 -0
  20. package/scripts/install-native.cjs +4 -1
  21. package/scripts/integrations-test.cjs +123 -0
  22. package/scripts/integrations-ui-smoke.cjs +124 -0
  23. package/scripts/native-protocol-test.cjs +180 -5
  24. package/scripts/native-secret-test.cjs +2 -2
  25. package/scripts/native-shim-test.cjs +20 -4
  26. package/scripts/native-sidebar-test.cjs +7 -0
  27. package/scripts/native-skill-roots-test.cjs +90 -0
  28. package/scripts/native-ui-smoke.cjs +58 -2
  29. package/scripts/publish-native-package.cjs +8 -5
  30. package/scripts/support/integrations-mcp-fixture.cjs +15 -0
  31. package/scripts/switch-harness-test.cjs +27 -1
  32. package/scripts/test-live-mentions.cjs +152 -0
  33. package/src/main/adapters/acp.js +3 -3
  34. package/src/main/adapters/antigravity.js +17 -17
  35. package/src/main/adapters/claude.js +50 -10
  36. package/src/main/adapters/codebuddy.js +1 -0
  37. package/src/main/adapters/codex-app-server.js +20 -13
  38. package/src/main/adapters/codex.js +25 -5
  39. package/src/main/adapters/cursor.js +5 -1
  40. package/src/main/adapters/dsh.js +9 -1
  41. package/src/main/adapters/grok.js +7 -3
  42. package/src/main/adapters/hermes.js +8 -0
  43. package/src/main/adapters/kiro.js +2 -1
  44. package/src/main/adapters/managed-mcp.js +25 -0
  45. package/src/main/adapters/native-acp.js +4 -4
  46. package/src/main/adapters/omp.js +11 -0
  47. package/src/main/adapters/openclaw.js +6 -0
  48. package/src/main/adapters/opencode.js +13 -3
  49. package/src/main/adapters/pi.js +5 -0
  50. package/src/main/adapters/qoder.js +1 -0
  51. package/src/main/adapters/trae.js +4 -0
  52. package/src/main/adapters/zcode.js +3 -0
  53. package/src/main/harness-adapter/manifest.js +17 -2
  54. package/src/main/host/collaboration.js +57 -11
  55. package/src/main/host/handoff-access.js +53 -0
  56. package/src/main/host/handoff-checkpoints.js +206 -0
  57. package/src/main/host/handoff-mcp.cjs +38 -0
  58. package/src/main/host/handoff-tools.js +13 -0
  59. package/src/main/host/handoff.js +28 -6
  60. package/src/main/host/integrations.js +339 -0
  61. package/src/main/host/runtime.js +95 -15
  62. package/src/main/host/store.js +4 -4
  63. package/src/main/native/codex-accounts.js +260 -0
  64. package/src/main/native/host.js +1 -1
  65. package/src/main/native/launcher.js +2 -2
  66. package/src/main/native/protocol.js +176 -14
  67. package/src/main/native/redact.js +1 -1
  68. package/src/main/native/rs/crates/shim/src/main.rs +155 -12
  69. package/src/main/native/thread-list.js +3 -0
  70. package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
  71. package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts.map +1 -1
  72. package/src/native-ui/renderer-extension/dist/types/index.d.ts +4 -0
  73. package/src/native-ui/renderer-extension/dist/types/index.d.ts.map +1 -1
  74. package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts +2 -0
  75. package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts.map +1 -1
  76. package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
  77. package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts +36 -0
  78. package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts.map +1 -0
  79. package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts +1 -0
  80. package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts.map +1 -1
  81. package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts +3 -1
  82. package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts.map +1 -1
  83. package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts +53 -0
  84. package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts.map +1 -0
  85. package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts +11 -0
  86. package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts.map +1 -1
  87. package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts +105 -0
  88. package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts.map +1 -0
  89. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +7 -1
  90. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
  91. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +1 -0
  92. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
  93. package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts +2 -0
  94. package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts.map +1 -1
  95. package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts +1 -0
  96. package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts.map +1 -1
  97. package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts +6 -0
  98. package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts.map +1 -0
  99. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts +5 -0
  100. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
  101. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +8 -4
  102. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
  103. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  104. package/src/native-ui/renderer-extension/src/harness-mix-settings.ts +4 -1
  105. package/src/native-ui/renderer-extension/src/index.ts +28 -0
  106. package/src/native-ui/renderer-extension/src/renderer-agent-icon.ts +4 -4
  107. package/src/native-ui/renderer-extension/src/renderer-agent-picker.ts +46 -6
  108. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +115 -18
  109. package/src/native-ui/renderer-extension/src/renderer-chatgpt-context.ts +87 -0
  110. package/src/native-ui/renderer-extension/src/renderer-codex-account-state.ts +10 -0
  111. package/src/native-ui/renderer-extension/src/renderer-composer-dom.ts +10 -0
  112. package/src/native-ui/renderer-extension/src/renderer-harness-handoff.ts +330 -0
  113. package/src/native-ui/renderer-extension/src/renderer-harness-mentions.ts +556 -123
  114. package/src/native-ui/renderer-extension/src/renderer-integrations-client.ts +59 -0
  115. package/src/native-ui/renderer-extension/src/renderer-model-client.ts +17 -1
  116. package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +2 -0
  117. package/src/native-ui/renderer-extension/src/settings/accounts-list.ts +16 -1
  118. package/src/native-ui/renderer-extension/src/settings/accounts-page.ts +32 -3
  119. package/src/native-ui/renderer-extension/src/settings/integrations-page.ts +1474 -0
  120. package/src/native-ui/renderer-extension/src/settings/localization.ts +32 -13
  121. package/src/native-ui/renderer-extension/src/settings/pages.ts +49 -18
  122. package/src/native-ui/renderer-extension/src/settings/shell.css +103 -0
  123. package/src/native-ui/renderer-extension/test/renderer-agent-picker.test.ts +44 -13
  124. package/src/native-ui/renderer-extension/test/renderer-chatgpt-context.test.ts +21 -0
  125. package/src/native-ui/renderer-extension/test/renderer-codex-account-state.test.ts +13 -1
  126. package/src/native-ui/renderer-extension/test/renderer-harness-handoff.test.ts +20 -0
  127. package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +9 -2
  128. package/src/native-ui/renderer-extension/test/settings/localization.test.ts +1 -1
  129. package/src/native-ui/renderer-extension/test/settings/pages.test.ts +77 -8
  130. package/src/native-ui/renderer-extension/test/settings/shell.test.ts +6 -0
  131. package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts +19 -4
  132. package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts.map +1 -1
  133. package/src/native-ui/shared-contracts/dist/codex-accounts.js +2 -0
  134. package/src/native-ui/shared-contracts/dist/codex-accounts.js.map +1 -1
  135. package/src/native-ui/shared-contracts/dist/index.d.ts +2 -2
  136. package/src/native-ui/shared-contracts/dist/index.d.ts.map +1 -1
  137. package/src/native-ui/shared-contracts/dist/index.js +1 -1
  138. package/src/native-ui/shared-contracts/dist/index.js.map +1 -1
  139. package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts +29 -0
  140. package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts.map +1 -1
  141. package/src/native-ui/shared-contracts/dist/thread-harness-switch.js +13 -0
  142. package/src/native-ui/shared-contracts/dist/thread-harness-switch.js.map +1 -1
  143. package/src/native-ui/shared-contracts/dist/tsconfig.tsbuildinfo +1 -1
  144. package/src/native-ui/shared-contracts/src/codex-accounts.ts +2 -0
  145. package/src/native-ui/shared-contracts/src/index.ts +8 -1
  146. package/src/native-ui/shared-contracts/src/thread-harness-switch.ts +17 -0
@@ -0,0 +1,1474 @@
1
+ import type { RendererSettingsPageDefinition, RendererSettingsPageMountContext } from './core.js';
2
+ import type { RendererSettingsMessages } from './localization.js';
3
+ import type { DroppedSkillFile, RendererIntegrationsClient, IntegrationSnapshot, IntegrationScope, ManagedServer } from '../renderer-integrations-client.js';
4
+
5
+ type IntegrationPageKind = 'mcp' | 'skills';
6
+ type WebkitEntry = {
7
+ readonly isFile: boolean;
8
+ readonly isDirectory: boolean;
9
+ readonly name: string;
10
+ file?(success: (file: File) => void, failure?: (error: DOMException) => void): void;
11
+ createReader?(): { readEntries(success: (entries: WebkitEntry[]) => void, failure?: (error: DOMException) => void): void };
12
+ };
13
+
14
+ function createIntegrationSettingsPage(kind: IntegrationPageKind, messages: RendererSettingsMessages, getClient: () => RendererIntegrationsClient | null): RendererSettingsPageDefinition {
15
+ const zh = messages.locale === 'zh-CN';
16
+ const tr = (cn: string, en: string) => zh ? cn : en;
17
+ return Object.freeze({
18
+ id: kind,
19
+ label: kind === 'mcp' ? 'MCP' : 'Skills',
20
+ icon: kind === 'mcp' ? 'connections' : 'session-import',
21
+ mount(context: RendererSettingsPageMountContext) {
22
+ const { content } = context;
23
+ const lifecycle = { signal: context.signal, runLatest: context.runLatest, get disposed() { return context.signal.aborted; } };
24
+ const doc = content.ownerDocument;
25
+ const el = <K extends keyof HTMLElementTagNameMap>(tag: K, text = '') => { const node = doc.createElement(tag); node.textContent = text; return node; };
26
+ const title = el('h2', kind === 'mcp' ? 'MCP' : 'Skills');
27
+ const note = el('p', kind === 'mcp'
28
+ ? tr('按 Harness 管理 MCP 服务器。配置会在下次打开原生会话时传入。', 'Manage MCP servers per Harness. Configuration is passed to the next native session.')
29
+ : tr('把技能文件或完整技能文件夹拖到这里,直接安装到原生技能目录。', 'Drop a skill file or a complete skill folder here to install it in the native skill directory.'));
30
+ note.className = 'settings-page-description';
31
+ const controls = el('div'); controls.className = 'settings-integrations-controls';
32
+ const harness = el('select'); harness.setAttribute('aria-label', 'Harness');
33
+ const level = el('select'); level.setAttribute('aria-label', tr('作用范围', 'Scope'));
34
+ for (const [value, label] of [['global', tr('全局', 'Global')], ['project', tr('项目', 'Project')]]) { const option = el('option', label); option.value = value!; level.append(option); }
35
+ const cwd = el('input'); cwd.placeholder = tr('项目绝对路径', 'Absolute project path'); cwd.setAttribute('aria-label', cwd.placeholder); cwd.hidden = true;
36
+ const refreshButton = el('button', tr('刷新', 'Refresh')); refreshButton.type = 'button'; refreshButton.className = 'settings-command-button settings-command-button--secondary';
37
+ const status = el('p'); status.className = 'settings-integrations-status'; status.setAttribute('role', 'status'); status.setAttribute('aria-live', 'polite');
38
+ const body = el('div'); body.className = 'settings-integrations-body';
39
+ let busy = false;
40
+ const selectedScope = (): IntegrationScope => ({ harnessId: harness.value, scope: level.value === 'project' ? 'project' : 'global', ...(level.value === 'project' ? { cwd: cwd.value.trim() } : {}) });
41
+ const client = () => { const value = getClient(); if (!value) throw new Error(tr('Host 尚未连接,请连接后刷新。', 'Host is not connected. Connect and refresh.')); return value; };
42
+ const fail = (error: unknown) => { status.textContent = error instanceof Error ? error.message : String(error); status.setAttribute('role', 'alert'); };
43
+ const setBusy = (value: boolean) => {
44
+ busy = value;
45
+ content.dataset.integrationBusy = String(value);
46
+ for (const node of content.querySelectorAll<HTMLButtonElement | HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>('button, input, select, textarea')) node.disabled = value || node.dataset.readonly === 'true';
47
+ };
48
+ const button = (label: string, action: () => void, primary = false) => {
49
+ const node = el('button', label); node.type = 'button'; node.className = `settings-command-button ${primary ? 'settings-command-button--primary' : 'settings-command-button--secondary'}`;
50
+ node.addEventListener('click', action, { signal: lifecycle.signal }); return node;
51
+ };
52
+ const mutate = async (operation: (value: RendererIntegrationsClient, selected: IntegrationScope) => Promise<unknown>) => {
53
+ if (busy) return;
54
+ const selected = selectedScope(); setBusy(true); status.textContent = tr('正在保存…', 'Saving…'); status.setAttribute('role', 'status');
55
+ try {
56
+ await operation(client(), selected);
57
+ if (!lifecycle.disposed) { setBusy(false); await refresh(); status.textContent = tr('已保存,下次打开原生会话时生效。', 'Saved. Applies on the next native session open.'); }
58
+ } catch (error) { if (!lifecycle.disposed) { setBusy(false); fail(error); } }
59
+ };
60
+ const mutateCustom = async (operation: (value: RendererIntegrationsClient) => Promise<string | void>) => {
61
+ if (busy) return;
62
+ setBusy(true); status.textContent = tr('正在保存…', 'Saving…'); status.setAttribute('role', 'status');
63
+ try {
64
+ const message = await operation(client());
65
+ if (!lifecycle.disposed) { setBusy(false); await refresh(); status.textContent = message || tr('已保存,下次打开原生会话时生效。', 'Saved. Applies on the next native session open.'); }
66
+ } catch (error) { if (!lifecycle.disposed) { setBusy(false); fail(error); } }
67
+ };
68
+ const bytesToBase64 = (buffer: ArrayBuffer) => {
69
+ const bytes = new Uint8Array(buffer); let binary = '';
70
+ for (let offset = 0; offset < bytes.length; offset += 0x8000) binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
71
+ return btoa(binary);
72
+ };
73
+ const fileFromEntry = (entry: WebkitEntry) => new Promise<File>((resolve, reject) => entry.file?.(resolve, reject));
74
+ const entriesFromReader = async (entry: WebkitEntry) => {
75
+ const reader = entry.createReader?.(); if (!reader) return [];
76
+ const all: WebkitEntry[] = [];
77
+ while (true) { const batch = await new Promise<WebkitEntry[]>((resolve, reject) => reader.readEntries(resolve, reject)); if (!batch.length) break; all.push(...batch); }
78
+ return all;
79
+ };
80
+ const walkEntry = async (entry: WebkitEntry, prefix: string, output: Array<{ path: string; file: File }>) => {
81
+ if (entry.isFile) { output.push({ path: `${prefix}${entry.name}`, file: await fileFromEntry(entry) }); return; }
82
+ if (entry.isDirectory) for (const child of await entriesFromReader(entry)) await walkEntry(child, `${prefix}${entry.name}/`, output);
83
+ };
84
+ const parseDroppedSkills = async (incoming: Array<{ path: string; file: File }>) => {
85
+ if (!incoming.length) throw new Error(tr('没有读取到文件。', 'No files were read.'));
86
+ if (incoming.length > 500 || incoming.reduce((sum, item) => sum + item.file.size, 0) > 10 * 1024 * 1024) throw new Error(tr('技能超过 10 MB 或 500 个文件。', 'Skill exceeds 10 MB or 500 files.'));
87
+ let normalized = incoming;
88
+ if (incoming.length === 1) {
89
+ if (!/\.md$/i.test(incoming[0]!.file.name)) throw new Error(tr('单文件技能必须是 Markdown 文件。', 'A single-file skill must be a Markdown file.'));
90
+ normalized = [{ path: 'SKILL.md', file: incoming[0]!.file }];
91
+ } else {
92
+ const paths = incoming.map(item => item.path.replaceAll('\\', '/'));
93
+ const first = paths[0]!.split('/')[0];
94
+ if (first && paths.every(value => value.startsWith(`${first}/`))) normalized = incoming.map((item, index) => ({ ...item, path: paths[index]!.slice(first.length + 1) }));
95
+ if (!normalized.some(item => item.path === 'SKILL.md')) throw new Error(tr('技能文件夹根目录必须包含 SKILL.md。', 'The skill folder root must contain SKILL.md.'));
96
+ }
97
+ const skillFile = normalized.find(item => item.path === 'SKILL.md')!;
98
+ const markdown = await skillFile.file.text();
99
+ const declared = markdown.match(/^---[\s\S]*?^name:\s*["']?([^\r\n"']+)/m)?.[1]?.trim();
100
+ const fallback = incoming.length === 1 ? incoming[0]!.file.name.replace(/\.md$/i, '') : incoming[0]!.path.split(/[\\/]/)[0];
101
+ const name = (declared || fallback || 'skill').toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64);
102
+ if (!name) throw new Error(tr('无法从文件中确定有效的技能名称。', 'Could not determine a valid skill name from the files.'));
103
+ const files: DroppedSkillFile[] = [];
104
+ for (const item of normalized) files.push({ path: item.path.replaceAll('\\', '/'), contentBase64: bytesToBase64(await item.file.arrayBuffer()) });
105
+ return { name, files };
106
+ };
107
+ const field = (form: HTMLElement, label: string, node: HTMLElement) => { const wrap = el('label', label); wrap.className = 'settings-integrations-field'; wrap.append(node); form.append(wrap); };
108
+
109
+ let targetEditServerName: string | undefined = undefined;
110
+ const editInHarness = (harnessId: string, serverName?: string) => {
111
+ harness.value = harnessId;
112
+ targetEditServerName = serverName;
113
+ void refresh();
114
+ };
115
+ const toManagedForCopy = (s: Partial<ManagedServer> & { name: string; enabled?: boolean }, newEnabled?: boolean): ManagedServer => {
116
+ const isHttp = s.transportType === 'streamable_http' || (typeof s.url === 'string' && s.url.trim().length > 0);
117
+ if (isHttp) {
118
+ return {
119
+ name: s.name,
120
+ transportType: 'streamable_http',
121
+ enabled: newEnabled !== undefined ? newEnabled : (s.enabled ?? true),
122
+ url: s.url || '',
123
+ ...(s.bearer_token_env_var?.trim() ? { bearer_token_env_var: s.bearer_token_env_var.trim() } : {}),
124
+ ...(s.http_headers && Object.keys(s.http_headers).length ? { http_headers: s.http_headers } : {}),
125
+ ...(s.env_http_headers && Object.keys(s.env_http_headers).length ? { env_http_headers: s.env_http_headers } : {}),
126
+ };
127
+ }
128
+ return {
129
+ name: s.name,
130
+ transportType: 'stdio',
131
+ enabled: newEnabled !== undefined ? newEnabled : (s.enabled ?? true),
132
+ command: s.command || '',
133
+ ...(s.args?.length ? { args: s.args } : {}),
134
+ ...(s.env && Object.keys(s.env).length ? { env: s.env } : {}),
135
+ ...(s.env_vars?.length ? { env_vars: s.env_vars } : {}),
136
+ ...(s.cwd?.trim() ? { cwd: s.cwd.trim() } : {}),
137
+ };
138
+ };
139
+
140
+ const renderMcp = (snapshot: IntegrationSnapshot) => {
141
+ const toolbar = el('div'); toolbar.className = 'settings-integrations-toolbar';
142
+ const search = el('input'); search.type = 'search'; search.placeholder = tr('搜索 MCP 服务器', 'Search MCP servers'); search.setAttribute('aria-label', search.placeholder);
143
+ const list = el('div'); list.className = 'settings-integrations-list';
144
+ const editor = el('div'); editor.className = 'settings-mcp-editor'; editor.hidden = true;
145
+ const showList = () => { editor.hidden = true; list.hidden = false; toolbar.hidden = false; };
146
+ const showEditor = (server?: IntegrationSnapshot['servers'][number]) => {
147
+ editor.replaceChildren(); editor.hidden = false; list.hidden = true; toolbar.hidden = true;
148
+ const heading = el('div'); heading.className = 'settings-integrations-editor-head';
149
+ const backBtn = button(tr('‹ 返回', '‹ Back'), showList);
150
+ const title = el('h3', server ? tr(`更新 ${server.name} MCP`, `Update ${server.name} MCP`) : tr('连接至自定义 MCP', 'Connect to a custom MCP'));
151
+ const docsLink = el('a', tr('文档 ↗', 'Docs ↗'));
152
+ docsLink.href = 'https://modelcontextprotocol.io/introduction';
153
+ docsLink.target = '_blank';
154
+ docsLink.rel = 'noreferrer noopener';
155
+ docsLink.className = 'settings-mcp-docs-link';
156
+ heading.append(backBtn, title, docsLink);
157
+ if (server?.editable) {
158
+ const uninstallBtn = button(tr('卸载', 'Uninstall'), () => {
159
+ void mutate(async (value, selected) => {
160
+ await value.removeMcp({ ...selected, id: server.id });
161
+ if (!lifecycle.disposed) showList();
162
+ });
163
+ });
164
+ uninstallBtn.classList.add('settings-command-button--danger');
165
+ heading.append(uninstallBtn);
166
+ }
167
+
168
+ const createTrashBtn = (onRemove: () => void) => {
169
+ const btn = el('button'); btn.type = 'button'; btn.className = 'settings-mcp-icon-btn';
170
+ btn.setAttribute('aria-label', tr('移除', 'Remove')); btn.title = tr('移除', 'Remove');
171
+ btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>';
172
+ btn.addEventListener('click', onRemove);
173
+ return btn;
174
+ };
175
+
176
+ let transportType: 'stdio' | 'streamable_http' = (server?.transportType === 'streamable_http' || !!server?.url) ? 'streamable_http' : 'stdio';
177
+
178
+ const form = el('form'); form.className = 'settings-integrations-form';
179
+ const basicCard = el('div'); basicCard.className = 'settings-integrations-form--card';
180
+ const name = el('input'); name.required = true; name.pattern = '[a-zA-Z0-9](?:[a-zA-Z0-9_]|-){0,63}';
181
+ name.placeholder = 'MCP server name'; name.value = server?.name ?? '';
182
+ field(basicCard, tr('名称', 'Name'), name);
183
+
184
+ const typeField = el('div'); typeField.className = 'settings-integrations-field';
185
+ typeField.append(el('span', tr('类型', 'Type')));
186
+ const toggle = el('div'); toggle.className = 'settings-mcp-type-toggle';
187
+ const stdioBtn = el('button', 'STDIO'); stdioBtn.type = 'button'; stdioBtn.className = 'settings-mcp-type-btn';
188
+ const httpBtn = el('button', tr('流式 HTTP', 'Streamable HTTP')); httpBtn.type = 'button'; httpBtn.className = 'settings-mcp-type-btn';
189
+ toggle.append(stdioBtn, httpBtn);
190
+ typeField.append(toggle);
191
+ basicCard.append(typeField);
192
+ if (server) {
193
+ const switchNotice = el('p', tr('如需切换 MCP 服务器类型,请先卸载当前配置。', 'If you would like to switch MCP server type, please uninstall first.'));
194
+ switchNotice.className = 'settings-mcp-notice';
195
+ basicCard.append(switchNotice);
196
+ }
197
+
198
+ const transportCard = el('div'); transportCard.className = 'settings-integrations-form--card settings-mcp-transport-card';
199
+ const stdioSection = el('div'); stdioSection.className = 'settings-mcp-section';
200
+ const command = el('input'); command.placeholder = 'openai-dev-mcp serve-sqlite'; command.value = server?.command ?? '';
201
+ field(stdioSection, tr('启动命令', 'Command to launch'), command);
202
+
203
+ const argsField = el('div'); argsField.className = 'settings-integrations-field'; argsField.append(el('span', tr('参数', 'Arguments')));
204
+ const argsList = el('div'); argsList.className = 'settings-mcp-args';
205
+ const addArg = (value = '') => {
206
+ const row = el('div'); row.className = 'settings-mcp-arg';
207
+ const input = el('input'); input.value = value; input.placeholder = tr('命令参数', 'Command argument');
208
+ row.append(input, createTrashBtn(() => row.remove())); argsList.append(row);
209
+ };
210
+ for (const value of (server?.args?.length ? server.args : (transportType === 'stdio' ? [''] : []))) addArg(value);
211
+ const addArgBtn = button(tr('+ 添加参数', '+ Add argument'), () => addArg()); addArgBtn.classList.add('settings-mcp-add-arg');
212
+ argsField.append(argsList, addArgBtn); stdioSection.append(argsField);
213
+
214
+ const envField = el('div'); envField.className = 'settings-integrations-field'; envField.append(el('span', tr('环境变量', 'Environment variables')));
215
+ const envList = el('div'); envList.className = 'settings-mcp-args';
216
+ const addEnv = (key = '', val = '') => {
217
+ const row = el('div'); row.className = 'settings-mcp-kv-row';
218
+ const keyInput = el('input'); keyInput.value = key; keyInput.placeholder = tr('键', 'Key');
219
+ const valInput = el('input'); valInput.value = val; valInput.placeholder = tr('值', 'Value');
220
+ row.append(keyInput, valInput, createTrashBtn(() => row.remove())); envList.append(row);
221
+ };
222
+ if (server?.env) { for (const [k, v] of Object.entries(server.env)) addEnv(k, String(v ?? '')); }
223
+ const addEnvBtn = button(tr('+ 添加环境变量', '+ Add environment variable'), () => addEnv()); addEnvBtn.classList.add('settings-mcp-add-arg');
224
+ envField.append(envList, addEnvBtn); stdioSection.append(envField);
225
+
226
+ const envPassField = el('div'); envPassField.className = 'settings-integrations-field'; envPassField.append(el('span', tr('环境变量传递', 'Environment variable passthrough')));
227
+ const envPassList = el('div'); envPassList.className = 'settings-mcp-args';
228
+ const addEnvPass = (value = '') => {
229
+ const row = el('div'); row.className = 'settings-mcp-arg';
230
+ const input = el('input'); input.value = value; input.placeholder = tr('变量名,例如:GITHUB_TOKEN', 'Variable name, e.g. GITHUB_TOKEN');
231
+ row.append(input, createTrashBtn(() => row.remove())); envPassList.append(row);
232
+ };
233
+ if (server?.env_vars) { for (const v of server.env_vars) addEnvPass(v); }
234
+ const addEnvPassBtn = button(tr('+ 添加变量', '+ Add variable'), () => addEnvPass()); addEnvPassBtn.classList.add('settings-mcp-add-arg');
235
+ envPassField.append(envPassList, addEnvPassBtn); stdioSection.append(envPassField);
236
+
237
+ const cwd = el('input'); cwd.placeholder = '~/code'; cwd.value = server?.cwd ?? '';
238
+ field(stdioSection, tr('工作目录', 'Working directory'), cwd);
239
+
240
+ const httpSection = el('div'); httpSection.className = 'settings-mcp-section';
241
+ const url = el('input'); url.placeholder = 'https://mcp.example.com/mcp'; url.value = server?.url ?? '';
242
+ field(httpSection, 'URL', url);
243
+
244
+ const bearerToken = el('input'); bearerToken.placeholder = 'MCP_BEARER_TOKEN'; bearerToken.value = server?.bearer_token_env_var ?? '';
245
+ field(httpSection, tr('Bearer 令牌环境变量', 'Bearer token env var'), bearerToken);
246
+
247
+ const headersField = el('div'); headersField.className = 'settings-integrations-field'; headersField.append(el('span', tr('标头', 'Headers')));
248
+ const headersList = el('div'); headersList.className = 'settings-mcp-args';
249
+ const addHeader = (key = '', val = '') => {
250
+ const row = el('div'); row.className = 'settings-mcp-kv-row';
251
+ const keyInput = el('input'); keyInput.value = key; keyInput.placeholder = tr('键', 'Key');
252
+ const valInput = el('input'); valInput.value = val; valInput.placeholder = tr('值', 'Value');
253
+ row.append(keyInput, valInput, createTrashBtn(() => row.remove())); headersList.append(row);
254
+ };
255
+ if (server?.http_headers) { for (const [k, v] of Object.entries(server.http_headers)) addHeader(k, String(v ?? '')); }
256
+ const addHeaderBtn = button(tr('+ 添加标头', '+ Add header'), () => addHeader()); addHeaderBtn.classList.add('settings-mcp-add-arg');
257
+ headersField.append(headersList, addHeaderBtn); httpSection.append(headersField);
258
+
259
+ const envHeadersField = el('div'); envHeadersField.className = 'settings-integrations-field'; envHeadersField.append(el('span', tr('来自环境变量的标头', 'Headers from environment variables')));
260
+ const envHeadersList = el('div'); envHeadersList.className = 'settings-mcp-args';
261
+ const addEnvHeader = (key = '', val = '') => {
262
+ const row = el('div'); row.className = 'settings-mcp-kv-row';
263
+ const keyInput = el('input'); keyInput.value = key; keyInput.placeholder = tr('标头名称', 'Header name');
264
+ const valInput = el('input'); valInput.value = val; valInput.placeholder = tr('环境变量名称', 'Environment variable name');
265
+ row.append(keyInput, valInput, createTrashBtn(() => row.remove())); envHeadersList.append(row);
266
+ };
267
+ if (server?.env_http_headers) { for (const [k, v] of Object.entries(server.env_http_headers)) addEnvHeader(k, String(v ?? '')); }
268
+ const addEnvHeaderBtn = button(tr('+ 添加变量', '+ Add variable'), () => addEnvHeader()); addEnvHeaderBtn.classList.add('settings-mcp-add-arg');
269
+ envHeadersField.append(envHeadersList, addEnvHeaderBtn); httpSection.append(envHeadersField);
270
+
271
+ const updateTypeView = () => {
272
+ stdioBtn.dataset.active = String(transportType === 'stdio');
273
+ httpBtn.dataset.active = String(transportType === 'streamable_http');
274
+ stdioSection.hidden = transportType !== 'stdio';
275
+ httpSection.hidden = transportType !== 'streamable_http';
276
+ command.required = transportType === 'stdio';
277
+ url.required = transportType === 'streamable_http';
278
+ };
279
+ stdioBtn.addEventListener('click', () => { transportType = 'stdio'; updateTypeView(); });
280
+ httpBtn.addEventListener('click', () => { transportType = 'streamable_http'; updateTypeView(); });
281
+ updateTypeView();
282
+ transportCard.append(stdioSection, httpSection);
283
+
284
+ const security = el('p', tr('凭据和环境变量将在原生 Harness 运行环境中解析与注入,不会泄露在宿主进程中。', 'Credentials and environment variables are resolved and injected within the native Harness environment.'));
285
+ security.className = 'settings-integrations-callout';
286
+
287
+ const save = button(tr('保存 MCP', 'Save MCP'), () => {}, true); save.type = 'submit';
288
+ form.append(basicCard, transportCard, security, save);
289
+
290
+ form.addEventListener('submit', event => {
291
+ event.preventDefault();
292
+ if (transportType === 'stdio') {
293
+ const args = [...argsList.querySelectorAll<HTMLInputElement>('input')].map(input => input.value.trim()).filter(value => value.length > 0);
294
+ const env: Record<string, string> = {};
295
+ for (const row of envList.querySelectorAll<HTMLElement>('.settings-mcp-kv-row')) {
296
+ const [k, v] = row.querySelectorAll<HTMLInputElement>('input');
297
+ if (k?.value.trim() && v?.value) env[k.value.trim()] = v.value;
298
+ }
299
+ const env_vars = [...envPassList.querySelectorAll<HTMLInputElement>('input')].map(input => input.value.trim()).filter(v => v.length > 0);
300
+ const cwdVal = cwd.value.trim();
301
+ void mutate((value, selected) => value.saveMcp({
302
+ ...selected,
303
+ server: {
304
+ ...(server?.id ? { id: server.id } : {}),
305
+ name: name.value.trim(),
306
+ transportType: 'stdio',
307
+ command: command.value.trim(),
308
+ args,
309
+ ...(Object.keys(env).length ? { env } : {}),
310
+ ...(env_vars.length ? { env_vars } : {}),
311
+ ...(cwdVal ? { cwd: cwdVal } : {}),
312
+ enabled: server?.enabled ?? true,
313
+ },
314
+ }));
315
+ } else {
316
+ const http_headers: Record<string, string> = {};
317
+ for (const row of headersList.querySelectorAll<HTMLElement>('.settings-mcp-kv-row')) {
318
+ const [k, v] = row.querySelectorAll<HTMLInputElement>('input');
319
+ if (k?.value.trim() && v?.value) http_headers[k.value.trim()] = v.value;
320
+ }
321
+ const env_http_headers: Record<string, string> = {};
322
+ for (const row of envHeadersList.querySelectorAll<HTMLElement>('.settings-mcp-kv-row')) {
323
+ const [k, v] = row.querySelectorAll<HTMLInputElement>('input');
324
+ if (k?.value.trim() && v?.value.trim()) env_http_headers[k.value.trim()] = v.value.trim();
325
+ }
326
+ const token = bearerToken.value.trim();
327
+ void mutate((value, selected) => value.saveMcp({
328
+ ...selected,
329
+ server: {
330
+ ...(server?.id ? { id: server.id } : {}),
331
+ name: name.value.trim(),
332
+ transportType: 'streamable_http',
333
+ url: url.value.trim(),
334
+ ...(token ? { bearer_token_env_var: token } : {}),
335
+ ...(Object.keys(http_headers).length ? { http_headers } : {}),
336
+ ...(Object.keys(env_http_headers).length ? { env_http_headers } : {}),
337
+ enabled: server?.enabled ?? true,
338
+ },
339
+ }));
340
+ }
341
+ }, { signal: lifecycle.signal });
342
+ editor.append(heading, form); name.focus();
343
+ };
344
+ if (targetEditServerName) {
345
+ const target = targetEditServerName === '__new__' ? undefined : snapshot.servers.find(s => s.name === targetEditServerName);
346
+ targetEditServerName = undefined;
347
+ showEditor(target);
348
+ body.append(toolbar, list, editor);
349
+ return;
350
+ }
351
+ const addButton = button(tr('+ 添加 MCP', '+ Add MCP'), () => showEditor(), true); toolbar.append(search, addButton);
352
+ for (const native of snapshot.native) {
353
+ const row = el('section'); row.className = 'settings-integrations-row settings-integrations-row--native';
354
+ row.append(el('strong', `${tr('原生状态', 'Native status')} · ${native.name || native.sessionId}`), el('span', native.status), el('code', native.tools.join(', '))); list.append(row);
355
+ }
356
+ const toManaged = (s: IntegrationSnapshot['servers'][number], newEnabled: boolean): ManagedServer => ({
357
+ id: s.id,
358
+ name: s.name,
359
+ enabled: newEnabled,
360
+ ...(s.transportType ? { transportType: s.transportType } : {}),
361
+ ...(s.command ? { command: s.command } : {}),
362
+ ...(s.args?.length ? { args: s.args } : {}),
363
+ ...(s.env ? { env: s.env } : {}),
364
+ ...(s.env_vars?.length ? { env_vars: s.env_vars } : {}),
365
+ ...(s.cwd ? { cwd: s.cwd } : {}),
366
+ ...(s.url ? { url: s.url } : {}),
367
+ ...(s.bearer_token_env_var ? { bearer_token_env_var: s.bearer_token_env_var } : {}),
368
+ ...(s.http_headers ? { http_headers: s.http_headers } : {}),
369
+ ...(s.env_http_headers ? { env_http_headers: s.env_http_headers } : {}),
370
+ });
371
+ const serverRows: HTMLElement[] = [];
372
+ for (const server of snapshot.servers) {
373
+ const row = el('section'); row.className = 'settings-integrations-row';
374
+ const isHttp = server.transportType === 'streamable_http' || !!server.url;
375
+ row.dataset.search = `${server.name} ${server.command || ''} ${server.url || ''}`.toLowerCase();
376
+ const detail = el('div');
377
+ const state = !server.effective ? tr('被项目配置覆盖', 'Overridden by project') : !server.enabled ? tr('已停用', 'Disabled') : server.appliedSessions ? tr(`已传入 ${server.appliedSessions} 个会话`, `Passed to ${server.appliedSessions} sessions`) : tr('已配置', 'Configured');
378
+ const typeTag = isHttp ? ' [HTTP]' : '';
379
+ const summary = isHttp ? (server.url || '') : [server.command, ...(server.args || [])].filter(Boolean).join(' ');
380
+ detail.append(el('strong', `${server.name}${typeTag}`), el('p', `${server.scope === 'global' ? tr('全局', 'Global') : tr('项目', 'Project')} · ${state}`), el('code', summary));
381
+ row.append(detail);
382
+ if (server.editable) {
383
+ const editBtn = button(tr('编辑', 'Edit'), () => showEditor(server));
384
+ const toggleBtn = button(server.enabled ? tr('停用', 'Disable') : tr('启用', 'Enable'), () => {
385
+ void mutate((value, selected) => value.saveMcp({ ...selected, server: toManaged(server, !server.enabled) }));
386
+ });
387
+ const syncBtn = button(tr('同步…', 'Sync…'), () => {
388
+ void client().integrationCatalog().then(catalog => {
389
+ const targets = catalog.harnesses.filter(h => h.mcp && h.id !== harness.value);
390
+ showMcpSyncDialog(server, { id: harness.value, name: harness.selectedOptions[0]?.text || harness.value }, targets);
391
+ });
392
+ });
393
+ const delBtn = button(tr('删除', 'Delete'), () => {
394
+ void mutate((value, selected) => value.removeMcp({ ...selected, id: server.id }));
395
+ });
396
+ row.append(editBtn, toggleBtn, syncBtn, delBtn);
397
+ }
398
+ list.append(row); serverRows.push(row);
399
+ }
400
+ if (!snapshot.servers.length) { const empty = el('div', tr('还没有 MCP 服务器,点击“添加 MCP”开始配置。', 'No MCP servers yet. Select “Add MCP” to configure one.')); empty.className = 'settings-integrations-empty'; list.append(empty); }
401
+ search.addEventListener('input', () => { const query = search.value.trim().toLowerCase(); for (const row of serverRows) row.hidden = !!query && !row.dataset.search!.includes(query); }, { signal: lifecycle.signal });
402
+ if (!snapshot.mcpSupported) { addButton.disabled = true; const unavailable = el('div', tr('此 Harness 尚无已确认的 MCP 配置接口。', 'This Harness has no verified MCP configuration interface.')); unavailable.className = 'settings-integrations-callout'; list.prepend(unavailable); }
403
+ body.append(toolbar, list, editor);
404
+ };
405
+
406
+ type HarnessCatalogItem = {
407
+ id: string;
408
+ name: string;
409
+ available: boolean;
410
+ mcp: boolean;
411
+ skills: boolean;
412
+ };
413
+
414
+ type OverviewItem = {
415
+ harness: HarnessCatalogItem;
416
+ snapshot: IntegrationSnapshot | null;
417
+ error: string | null;
418
+ };
419
+
420
+ const showMcpSyncDialog = (
421
+ server: ManagedServer | IntegrationSnapshot['servers'][number],
422
+ sourceHarness: { id: string; name: string },
423
+ targetHarnesses: Array<HarnessCatalogItem>
424
+ ) => {
425
+ if (!targetHarnesses.length) {
426
+ fail(new Error(tr('没有其他支持 MCP 的原生 Harness 可供同步。', 'No other native Harnesses support MCP for sync.')));
427
+ return;
428
+ }
429
+ const modal = el('dialog');
430
+ modal.className = 'settings-skills-sync-modal settings-mcp-sync-modal';
431
+ const mTitle = el('h3', tr(`同步 MCP【${server.name}】`, `Sync MCP [${server.name}]`));
432
+ const isHttp = server.transportType === 'streamable_http' || !!server.url;
433
+ const summary = isHttp ? (server.url || '') : [server.command, ...(server.args || [])].filter(Boolean).join(' ');
434
+ const mDesc = el('p', tr(
435
+ `从 ${sourceHarness.name} 复制该 MCP 配置(${isHttp ? 'HTTP' : 'stdio'}:${summary})到选中的 Harness:`,
436
+ `Copy MCP configuration (${isHttp ? 'HTTP' : 'stdio'}: ${summary}) from ${sourceHarness.name} to selected Harnesses:`
437
+ ));
438
+ const form = el('form');
439
+ const checkList = el('div');
440
+ checkList.style.display = 'grid';
441
+ checkList.style.gap = '8px';
442
+ checkList.style.margin = '12px 0';
443
+ checkList.style.maxHeight = '240px';
444
+ checkList.style.overflowY = 'auto';
445
+ const checkboxes: HTMLInputElement[] = [];
446
+ for (const h of targetHarnesses) {
447
+ const label = el('label');
448
+ label.style.display = 'flex';
449
+ label.style.gap = '8px';
450
+ label.style.alignItems = 'center';
451
+ label.style.cursor = 'pointer';
452
+ const cb = el('input');
453
+ cb.type = 'checkbox';
454
+ cb.value = h.id;
455
+ cb.checked = true;
456
+ checkboxes.push(cb);
457
+ label.append(cb, el('span', h.name));
458
+ checkList.append(label);
459
+ }
460
+ const btnRow = el('div');
461
+ btnRow.style.display = 'flex';
462
+ btnRow.style.gap = '8px';
463
+ btnRow.style.justifyContent = 'flex-end';
464
+ btnRow.style.marginTop = '14px';
465
+ const cancelBtn = button(tr('取消', 'Cancel'), () => { modal.close(); modal.remove(); });
466
+ const confirmBtn = button(tr('确认同步', 'Confirm Sync'), () => {
467
+ const selectedHarnesses = checkboxes.filter(cb => cb.checked).map(cb => cb.value);
468
+ modal.close();
469
+ modal.remove();
470
+ if (!selectedHarnesses.length) return;
471
+ void mutateCustom(async c => {
472
+ const scopeType = level.value === 'project' ? 'project' : 'global';
473
+ const currentCwd = level.value === 'project' ? cwd.value.trim() : undefined;
474
+ const successes: string[] = [];
475
+ const errors: string[] = [];
476
+ const cleanConfig = toManagedForCopy(server);
477
+ for (const targetId of selectedHarnesses) {
478
+ const targetH = targetHarnesses.find(h => h.id === targetId);
479
+ try {
480
+ await c.saveMcp({
481
+ harnessId: targetId,
482
+ scope: scopeType,
483
+ ...(currentCwd ? { cwd: currentCwd } : {}),
484
+ server: cleanConfig,
485
+ });
486
+ if (targetH) successes.push(targetH.name);
487
+ } catch (err) {
488
+ errors.push(`${targetH?.name || targetId}: ${err instanceof Error ? err.message : String(err)}`);
489
+ }
490
+ }
491
+ if (!successes.length && errors.length) throw new Error(errors.join('; '));
492
+ return tr(`同步完成!已配置到:${successes.join('、')}${errors.length ? `(跳过: ${errors.join('; ')})` : ''}`,
493
+ `Sync complete! Configured on: ${successes.join(', ')}${errors.length ? ` (Skipped: ${errors.join('; ')})` : ''}`);
494
+ });
495
+ }, true);
496
+ btnRow.append(cancelBtn, confirmBtn);
497
+ form.append(checkList, btnRow);
498
+ modal.append(mTitle, mDesc, form);
499
+ content.append(modal);
500
+ modal.showModal();
501
+ };
502
+
503
+ const showMcpDistributeDialog = (targetHarnesses: Array<HarnessCatalogItem>) => {
504
+ if (!targetHarnesses.length) {
505
+ fail(new Error(tr('没有支持 MCP 注入的原生 Harness。', 'No native Harnesses support MCP injection.')));
506
+ return;
507
+ }
508
+ const modal = el('dialog');
509
+ modal.className = 'settings-mcp-distribute-modal';
510
+ const mTitle = el('h3', tr('新建并批量分发 MCP', 'Create & Distribute MCP'));
511
+ const mDesc = el('p', tr('配置 MCP 服务器,并一次性保存分发至选中的所有 Harness。', 'Configure MCP server once and distribute to all selected Harnesses.'));
512
+
513
+ const form = el('form');
514
+ form.className = 'settings-integrations-form';
515
+
516
+ let transportType: 'stdio' | 'streamable_http' = 'stdio';
517
+ const typeToggle = el('div'); typeToggle.className = 'settings-mcp-type-toggle';
518
+ const stdioBtn = el('button', tr('标准输入输出 (stdio)', 'Standard I/O (stdio)'));
519
+ stdioBtn.type = 'button'; stdioBtn.className = 'settings-mcp-type-btn'; stdioBtn.dataset.active = 'true';
520
+ const httpBtn = el('button', tr('流式 HTTP (SSE)', 'Streamable HTTP (SSE)'));
521
+ httpBtn.type = 'button'; httpBtn.className = 'settings-mcp-type-btn';
522
+ typeToggle.append(stdioBtn, httpBtn);
523
+
524
+ const nameInput = el('input'); nameInput.required = true; nameInput.placeholder = 'my-mcp-server';
525
+ nameInput.pattern = '[a-zA-Z0-9](?:[a-zA-Z0-9_]|-){0,63}';
526
+ field(form, tr('MCP 服务器名称(必填)', 'MCP server name (required)'), nameInput);
527
+
528
+ const typeField = el('div'); typeField.className = 'settings-integrations-field';
529
+ typeField.append(el('span', tr('传输协议', 'Transport Protocol')), typeToggle);
530
+ form.append(typeField);
531
+
532
+ const stdioCard = el('div'); stdioCard.className = 'settings-integrations-form--card';
533
+ const cmdInput = el('input'); cmdInput.placeholder = 'npx / uvx / node / ...';
534
+ field(stdioCard, tr('可执行命令 (Command)', 'Executable Command'), cmdInput);
535
+ const argsInput = el('input'); argsInput.placeholder = '-y @modelcontextprotocol/server-filesystem D:/data';
536
+ field(stdioCard, tr('参数(空格分隔,支持引号)', 'Arguments (space separated, quotes supported)'), argsInput);
537
+ const envInput = el('input'); envInput.placeholder = 'KEY1=val1, KEY2=val2';
538
+ field(stdioCard, tr('环境变量(可选,KEY=VAL 逗号或换行分隔)', 'Environment variables (optional, KEY=VAL separated)'), envInput);
539
+
540
+ const httpCard = el('div'); httpCard.className = 'settings-integrations-form--card';
541
+ httpCard.hidden = true;
542
+ const urlInput = el('input'); urlInput.type = 'url'; urlInput.placeholder = 'https://mcp.example.com/sse';
543
+ field(httpCard, tr('MCP 服务器 URL', 'MCP Server URL'), urlInput);
544
+ const tokenInput = el('input'); tokenInput.placeholder = 'API_KEY_ENV_NAME';
545
+ field(httpCard, tr('Bearer Token 环境变量名(可选)', 'Bearer token env var name (optional)'), tokenInput);
546
+
547
+ stdioBtn.addEventListener('click', () => {
548
+ transportType = 'stdio'; stdioBtn.dataset.active = 'true'; delete httpBtn.dataset.active;
549
+ stdioCard.hidden = false; httpCard.hidden = true;
550
+ });
551
+ httpBtn.addEventListener('click', () => {
552
+ transportType = 'streamable_http'; httpBtn.dataset.active = 'true'; delete stdioBtn.dataset.active;
553
+ stdioCard.hidden = true; httpCard.hidden = false;
554
+ });
555
+
556
+ form.append(stdioCard, httpCard);
557
+
558
+ const targetSection = el('div'); targetSection.className = 'settings-integrations-form--card';
559
+ const targetHead = el('div');
560
+ targetHead.style.display = 'flex'; targetHead.style.justifyContent = 'space-between'; targetHead.style.alignItems = 'center'; targetHead.style.marginBottom = '8px';
561
+ const targetTitle = el('strong', tr('分发目标 Harness:', 'Target Harnesses:'));
562
+ targetTitle.style.fontSize = '12px';
563
+ const toggleAllBtn = el('button', tr('全选 / 取消全选', 'Select / Deselect All'));
564
+ toggleAllBtn.type = 'button'; toggleAllBtn.className = 'settings-command-button settings-command-button--secondary';
565
+ toggleAllBtn.style.fontSize = '11px'; toggleAllBtn.style.padding = '3px 8px';
566
+ targetHead.append(targetTitle, toggleAllBtn);
567
+ targetSection.append(targetHead);
568
+
569
+ const checkList = el('div');
570
+ checkList.style.display = 'grid'; checkList.style.gridTemplateColumns = 'repeat(auto-fill, minmax(130px, 1fr))'; checkList.style.gap = '8px';
571
+ const checkboxes: HTMLInputElement[] = [];
572
+ for (const h of targetHarnesses) {
573
+ const label = el('label'); label.style.display = 'flex'; label.style.gap = '6px'; label.style.alignItems = 'center'; label.style.cursor = 'pointer'; label.style.fontSize = '12px';
574
+ const cb = el('input'); cb.type = 'checkbox'; cb.value = h.id; cb.checked = true;
575
+ checkboxes.push(cb);
576
+ label.append(cb, el('span', h.name));
577
+ checkList.append(label);
578
+ }
579
+ targetSection.append(checkList);
580
+ form.append(targetSection);
581
+
582
+ toggleAllBtn.addEventListener('click', () => {
583
+ const anyChecked = checkboxes.some(cb => cb.checked);
584
+ for (const cb of checkboxes) cb.checked = !anyChecked;
585
+ });
586
+
587
+ const btnRow = el('div'); btnRow.style.display = 'flex'; btnRow.style.gap = '8px'; btnRow.style.justifyContent = 'flex-end'; btnRow.style.marginTop = '14px';
588
+ const cancelBtn = button(tr('取消', 'Cancel'), () => { modal.close(); modal.remove(); });
589
+ const submitBtn = el('button', tr('确认并分发', 'Confirm & Distribute'));
590
+ submitBtn.type = 'submit'; submitBtn.className = 'settings-command-button settings-command-button--primary';
591
+ btnRow.append(cancelBtn, submitBtn);
592
+ form.append(btnRow);
593
+
594
+ form.addEventListener('submit', event => {
595
+ event.preventDefault();
596
+ const serverName = nameInput.value.trim();
597
+ if (!serverName) return;
598
+ const selectedHarnesses = checkboxes.filter(cb => cb.checked).map(cb => cb.value);
599
+ if (!selectedHarnesses.length) {
600
+ fail(new Error(tr('请至少选择一个目标 Harness。', 'Please select at least one target Harness.')));
601
+ return;
602
+ }
603
+
604
+ let serverConfig: ManagedServer;
605
+ if (transportType === 'streamable_http') {
606
+ const url = urlInput.value.trim();
607
+ if (!url) { fail(new Error(tr('流式 HTTP 模式必须填写 URL。', 'URL is required for streamable HTTP.'))); return; }
608
+ serverConfig = {
609
+ name: serverName,
610
+ transportType: 'streamable_http',
611
+ enabled: true,
612
+ url,
613
+ ...(tokenInput.value.trim() ? { bearer_token_env_var: tokenInput.value.trim() } : {}),
614
+ };
615
+ } else {
616
+ const cmd = cmdInput.value.trim();
617
+ if (!cmd) { fail(new Error(tr('stdio 模式必须填写命令。', 'Command is required for stdio mode.'))); return; }
618
+ const rawArgs = argsInput.value.trim().match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map(s => s.replace(/^['"]|['"]$/g, '')) || [];
619
+ const envMap: Record<string, string> = {};
620
+ for (const part of envInput.value.split(/[,;\n]/)) {
621
+ const eq = part.indexOf('=');
622
+ if (eq > 0) {
623
+ const k = part.slice(0, eq).trim();
624
+ const v = part.slice(eq + 1).trim();
625
+ if (k) envMap[k] = v;
626
+ }
627
+ }
628
+ serverConfig = {
629
+ name: serverName,
630
+ transportType: 'stdio',
631
+ enabled: true,
632
+ command: cmd,
633
+ ...(rawArgs.length ? { args: rawArgs } : {}),
634
+ ...(Object.keys(envMap).length ? { env: envMap } : {}),
635
+ };
636
+ }
637
+
638
+ modal.close();
639
+ modal.remove();
640
+
641
+ void mutateCustom(async c => {
642
+ const scopeType = level.value === 'project' ? 'project' : 'global';
643
+ const currentCwd = level.value === 'project' ? cwd.value.trim() : undefined;
644
+ const successes: string[] = [];
645
+ const errors: string[] = [];
646
+ for (const targetId of selectedHarnesses) {
647
+ const targetH = targetHarnesses.find(h => h.id === targetId);
648
+ try {
649
+ await c.saveMcp({
650
+ harnessId: targetId,
651
+ scope: scopeType,
652
+ ...(currentCwd ? { cwd: currentCwd } : {}),
653
+ server: serverConfig,
654
+ });
655
+ if (targetH) successes.push(targetH.name);
656
+ } catch (err) {
657
+ errors.push(`${targetH?.name || targetId}: ${err instanceof Error ? err.message : String(err)}`);
658
+ }
659
+ }
660
+ if (!successes.length && errors.length) throw new Error(errors.join('; '));
661
+ return tr(`已成功分发【${serverName}】至:${successes.join('、')}${errors.length ? `(跳过: ${errors.join('; ')})` : ''}`,
662
+ `Distributed [${serverName}] to: ${successes.join(', ')}${errors.length ? ` (Skipped: ${errors.join('; ')})` : ''}`);
663
+ });
664
+ });
665
+
666
+ modal.append(mTitle, mDesc, form);
667
+ content.append(modal);
668
+ modal.showModal();
669
+ };
670
+
671
+ const renderMcpOverview = (
672
+ _catalog: Awaited<ReturnType<RendererIntegrationsClient['integrationCatalog']>>,
673
+ items: OverviewItem[]
674
+ ) => {
675
+ const supported = items.filter(i => i.harness.mcp);
676
+ const uniqueServers = new Map<string, Array<{ harness: HarnessCatalogItem; server: IntegrationSnapshot['servers'][number] }>>();
677
+ let totalServersCount = 0;
678
+ let totalNativeCount = 0;
679
+
680
+ for (const item of items) {
681
+ if (item.snapshot?.servers) {
682
+ totalServersCount += item.snapshot.servers.length;
683
+ for (const s of item.snapshot.servers) {
684
+ const list = uniqueServers.get(s.name) || [];
685
+ list.push({ harness: item.harness, server: s });
686
+ uniqueServers.set(s.name, list);
687
+ }
688
+ }
689
+ if (item.snapshot?.native) {
690
+ totalNativeCount += item.snapshot.native.length;
691
+ }
692
+ }
693
+
694
+ const statsBar = el('div'); statsBar.className = 'settings-skills-overview-stats settings-mcp-overview-stats';
695
+ const statsSummary = el('span', tr(
696
+ `📊 共 ${items.length} 个 Harness · ${supported.length} 个支持原生 MCP 注入 · 已配置 ${uniqueServers.size} 种独立 MCP(共 ${totalServersCount} 个实例)${totalNativeCount ? ` · ${totalNativeCount} 个活跃原生连接` : ''}`,
697
+ `📊 ${items.length} Harnesses · ${supported.length} with native MCP injection · ${uniqueServers.size} unique MCPs (${totalServersCount} instances)${totalNativeCount ? ` · ${totalNativeCount} active native connections` : ''}`
698
+ ));
699
+ statsBar.append(statsSummary);
700
+
701
+ const toolbar = el('div'); toolbar.className = 'settings-integrations-toolbar';
702
+ const searchInput = el('input'); searchInput.type = 'search';
703
+ searchInput.placeholder = tr('按名称、命令或 URL 过滤 MCP 服务器…', 'Filter MCP servers by name, command, or URL…');
704
+ searchInput.setAttribute('aria-label', searchInput.placeholder);
705
+
706
+ const tabsWrap = el('div'); tabsWrap.className = 'settings-skills-tabs settings-mcp-tabs';
707
+ const btnByHarness = el('button', tr('按 Harness 查看', 'By Harness'));
708
+ btnByHarness.type = 'button'; btnByHarness.className = 'settings-skills-tab-btn';
709
+ btnByHarness.dataset.active = 'true';
710
+ const btnByServer = el('button', tr('按 Server 汇总', 'By Server'));
711
+ btnByServer.type = 'button'; btnByServer.className = 'settings-skills-tab-btn';
712
+ tabsWrap.append(btnByHarness, btnByServer);
713
+
714
+ const distributeBtn = button(tr('+ 新建并分发 MCP', '+ New & Distribute MCP'), () => {
715
+ showMcpDistributeDialog(supported.map(i => i.harness));
716
+ }, true);
717
+
718
+ toolbar.append(searchInput, tabsWrap, distributeBtn);
719
+
720
+ const viewsContainer = el('div'); viewsContainer.className = 'settings-skills-views settings-mcp-views';
721
+ let activeTab: 'harness' | 'server' = 'harness';
722
+
723
+ const updateView = () => {
724
+ viewsContainer.replaceChildren();
725
+ const query = searchInput.value.trim().toLowerCase();
726
+
727
+ if (activeTab === 'harness') {
728
+ const grid = el('div'); grid.className = 'settings-skills-overview-grid settings-mcp-overview-grid';
729
+ for (const item of items) {
730
+ const allServers = item.snapshot?.servers || [];
731
+ const allNative = item.snapshot?.native || [];
732
+ const matchingServers = query
733
+ ? allServers.filter(s =>
734
+ s.name.toLowerCase().includes(query) ||
735
+ (s.command && s.command.toLowerCase().includes(query)) ||
736
+ (s.url && s.url.toLowerCase().includes(query)) ||
737
+ (s.args && s.args.some(a => a.toLowerCase().includes(query)))
738
+ )
739
+ : allServers;
740
+ const matchingNative = query
741
+ ? allNative.filter(n =>
742
+ (n.name && n.name.toLowerCase().includes(query)) ||
743
+ (n.sessionId && n.sessionId.toLowerCase().includes(query)) ||
744
+ (n.tools && n.tools.some(t => t.toLowerCase().includes(query)))
745
+ )
746
+ : allNative;
747
+
748
+ if (query && !item.harness.name.toLowerCase().includes(query) && matchingServers.length === 0 && matchingNative.length === 0) {
749
+ continue;
750
+ }
751
+
752
+ const card = el('section'); card.className = 'settings-skills-harness-card settings-mcp-harness-card';
753
+ const head = el('div'); head.className = 'settings-skills-harness-head settings-mcp-harness-head';
754
+ const hTitle = el('strong', item.harness.name);
755
+ const badge = el('span');
756
+ badge.className = `settings-skills-badge ${item.harness.mcp ? 'settings-skills-badge--active' : ''}`;
757
+ badge.textContent = item.harness.mcp
758
+ ? tr(`${allServers.length} 个 MCP`, `${allServers.length} MCPs`)
759
+ : tr('未提供 MCP 接口', 'No native MCP interface');
760
+ head.append(hTitle, badge);
761
+ card.append(head);
762
+
763
+ if (!item.harness.mcp) {
764
+ const note = el('p', tr('此 Harness 尚无已确认的 MCP 配置接口。', 'This Harness has no verified MCP configuration interface.'));
765
+ note.className = 'settings-mcp-notice';
766
+ card.append(note);
767
+ } else if (item.error) {
768
+ const errNote = el('p', item.error);
769
+ errNote.className = 'settings-mcp-notice';
770
+ errNote.style.color = '#ef4444';
771
+ card.append(errNote);
772
+ } else {
773
+ if (matchingNative.length > 0) {
774
+ const nativeBox = el('div'); nativeBox.className = 'settings-mcp-native-box';
775
+ const nativeTitle = el('div'); nativeTitle.className = 'settings-mcp-native-title';
776
+ nativeTitle.textContent = tr(`🟢 原生运行时活跃 (${matchingNative.length})`, `🟢 Active Native Sessions (${matchingNative.length})`);
777
+ nativeBox.append(nativeTitle);
778
+ for (const n of matchingNative) {
779
+ const nRow = el('div'); nRow.className = 'settings-mcp-native-item';
780
+ const nHeader = el('div');
781
+ nHeader.append(
782
+ el('strong', n.name || n.sessionId),
783
+ el('span', ` · ${n.status}${n.tools?.length ? ` · ${n.tools.length} ${tr('工具', 'tools')}` : ''}`)
784
+ );
785
+ nRow.append(nHeader);
786
+ if (n.tools?.length) {
787
+ const toolPreview = n.tools.slice(0, 4).join(', ') + (n.tools.length > 4 ? ` +${n.tools.length - 4}` : '');
788
+ nRow.append(el('code', toolPreview));
789
+ }
790
+ nativeBox.append(nRow);
791
+ }
792
+ card.append(nativeBox);
793
+ }
794
+
795
+ if (matchingServers.length === 0) {
796
+ const empty = el('p', query ? tr('无匹配 MCP 服务器', 'No matching MCP servers') : tr('暂未配置 MCP 服务器', 'No MCP servers configured'));
797
+ empty.className = 'settings-mcp-notice';
798
+ card.append(empty);
799
+ } else {
800
+ const list = el('div'); list.className = 'settings-skills-list-compact settings-mcp-list-compact';
801
+ for (const s of matchingServers) {
802
+ const row = el('div'); row.className = 'settings-skills-item-row settings-mcp-item-row';
803
+ const isHttp = s.transportType === 'streamable_http' || !!s.url;
804
+ const summary = isHttp ? (s.url || '') : [s.command, ...(s.args || [])].filter(Boolean).join(' ');
805
+ const stateText = !s.effective ? tr(' · 被项目覆盖', ' · Overridden') : !s.enabled ? tr(' · 已停用', ' · Disabled') : s.appliedSessions ? tr(` · 传入 ${s.appliedSessions} 会话`, ` · ${s.appliedSessions} sessions`) : tr(' · 已配置', ' · Configured');
806
+ const info = el('div');
807
+ info.append(
808
+ el('strong', `${s.name}${isHttp ? ' [HTTP]' : ' [stdio]'}`),
809
+ el('span', stateText),
810
+ el('code', summary)
811
+ );
812
+ const actions = el('div'); actions.className = 'settings-skills-item-actions settings-mcp-item-actions';
813
+ actions.append(button(s.enabled ? tr('停用', 'Disable') : tr('启用', 'Enable'), () => {
814
+ void mutateCustom(async c => {
815
+ const scopeType = level.value === 'project' ? 'project' : 'global';
816
+ const currentCwd = level.value === 'project' ? cwd.value.trim() : undefined;
817
+ await c.saveMcp({
818
+ harnessId: item.harness.id,
819
+ scope: scopeType,
820
+ ...(currentCwd ? { cwd: currentCwd } : {}),
821
+ server: toManagedForCopy(s, !s.enabled),
822
+ });
823
+ return tr(`已${s.enabled ? '停用' : '启用'}【${s.name}】(${item.harness.name})。`,
824
+ `${s.enabled ? 'Disabled' : 'Enabled'} [${s.name}] (${item.harness.name}).`);
825
+ });
826
+ }));
827
+ const otherSupported = supported.filter(h => h.harness.id !== item.harness.id);
828
+ if (otherSupported.length > 0) {
829
+ actions.append(button(tr('同步…', 'Sync…'), () => {
830
+ showMcpSyncDialog(s, item.harness, otherSupported.map(h => h.harness));
831
+ }));
832
+ }
833
+ actions.append(button(tr('编辑', 'Edit'), () => {
834
+ editInHarness(item.harness.id, s.name);
835
+ }));
836
+ row.append(info, actions);
837
+ list.append(row);
838
+ }
839
+ card.append(list);
840
+ }
841
+
842
+ const cardFooter = el('div'); cardFooter.className = 'settings-mcp-card-footer';
843
+ const addBtn = button(tr('+ 添加 MCP', '+ Add MCP'), () => {
844
+ editInHarness(item.harness.id, '__new__');
845
+ });
846
+ cardFooter.append(addBtn);
847
+ card.append(cardFooter);
848
+ }
849
+ grid.append(card);
850
+ }
851
+ viewsContainer.append(grid);
852
+ } else {
853
+ const matrixList = el('div'); matrixList.className = 'settings-skills-matrix-list settings-mcp-matrix-list';
854
+ let matchedCount = 0;
855
+ for (const [serverName, installations] of uniqueServers.entries()) {
856
+ const firstServer = installations[0]!.server;
857
+ const isHttp = firstServer.transportType === 'streamable_http' || !!firstServer.url;
858
+ const summary = isHttp ? (firstServer.url || '') : [firstServer.command, ...(firstServer.args || [])].filter(Boolean).join(' ');
859
+
860
+ if (query && !serverName.toLowerCase().includes(query) && !summary.toLowerCase().includes(query) && !installations.some(i => i.harness.name.toLowerCase().includes(query))) {
861
+ continue;
862
+ }
863
+ matchedCount++;
864
+ const card = el('section'); card.className = 'settings-skills-matrix-card settings-mcp-matrix-card';
865
+ const head = el('div'); head.className = 'settings-skills-matrix-head settings-mcp-matrix-head';
866
+ const titleWrap = el('div');
867
+ titleWrap.style.display = 'flex'; titleWrap.style.alignItems = 'center'; titleWrap.style.gap = '8px';
868
+ titleWrap.append(el('strong', serverName), el('span', `[${isHttp ? 'HTTP' : 'stdio'}]`));
869
+ head.append(
870
+ titleWrap,
871
+ el('span', tr(`已在 ${installations.length} 个 Harness 配置`, `Configured in ${installations.length} harnesses`))
872
+ );
873
+
874
+ const codeSummary = el('code');
875
+ codeSummary.style.fontSize = '11px'; codeSummary.style.color = 'var(--settings-muted)'; codeSummary.style.overflowWrap = 'anywhere';
876
+ codeSummary.textContent = summary;
877
+
878
+ const tags = el('div'); tags.className = 'settings-skills-tags settings-mcp-tags';
879
+ for (const inst of installations) {
880
+ const tag = el('span');
881
+ tag.className = `settings-skills-tag settings-mcp-tag ${inst.server.enabled ? '' : 'settings-mcp-tag--disabled'}`;
882
+ tag.textContent = `✓ ${inst.harness.name}${inst.server.enabled ? '' : tr(' (已停用)', ' (disabled)')}`;
883
+ tags.append(tag);
884
+ }
885
+
886
+ const installedIds = new Set(installations.map(i => i.harness.id));
887
+ const missingHarnesses = supported.filter(h => !installedIds.has(h.harness.id));
888
+
889
+ const syncRow = el('div'); syncRow.className = 'settings-skills-sync-actions settings-mcp-sync-actions';
890
+ if (missingHarnesses.length > 0) {
891
+ const label = el('span', tr('未配置于:', 'Not configured in: '));
892
+ label.style.fontSize = '11px'; label.style.color = 'var(--settings-muted)';
893
+ syncRow.append(label);
894
+
895
+ for (const missing of missingHarnesses) {
896
+ const installBtn = button(`+ ${missing.harness.name}`, () => {
897
+ void mutateCustom(async c => {
898
+ const scopeType = level.value === 'project' ? 'project' : 'global';
899
+ const currentCwd = level.value === 'project' ? cwd.value.trim() : undefined;
900
+ await c.saveMcp({
901
+ harnessId: missing.harness.id,
902
+ scope: scopeType,
903
+ ...(currentCwd ? { cwd: currentCwd } : {}),
904
+ server: toManagedForCopy(firstServer),
905
+ });
906
+ return tr(`已将 MCP【${serverName}】同步到 ${missing.harness.name}。`, `Configured [${serverName}] to ${missing.harness.name}.`);
907
+ });
908
+ });
909
+ syncRow.append(installBtn);
910
+ }
911
+
912
+ if (missingHarnesses.length > 1) {
913
+ const installAllBtn = button(tr('一键同步到全部支持的 Harness', 'Sync to all supported'), () => {
914
+ void mutateCustom(async c => {
915
+ const scopeType = level.value === 'project' ? 'project' : 'global';
916
+ const currentCwd = level.value === 'project' ? cwd.value.trim() : undefined;
917
+ const successes: string[] = [];
918
+ for (const missing of missingHarnesses) {
919
+ try {
920
+ await c.saveMcp({
921
+ harnessId: missing.harness.id,
922
+ scope: scopeType,
923
+ ...(currentCwd ? { cwd: currentCwd } : {}),
924
+ server: toManagedForCopy(firstServer),
925
+ });
926
+ successes.push(missing.harness.name);
927
+ } catch { /* skip */ }
928
+ }
929
+ return tr(`已将 MCP【${serverName}】同步到:${successes.join('、')}。`, `Synced [${serverName}] to: ${successes.join(', ')}.`);
930
+ });
931
+ }, true);
932
+ syncRow.append(installAllBtn);
933
+ }
934
+ } else {
935
+ const complete = el('span', tr('✓ 已在所有支持的 Harness 中配置就绪', '✓ Ready across all supported Harnesses'));
936
+ complete.style.fontSize = '11px'; complete.style.color = '#10a37f';
937
+ syncRow.append(complete);
938
+ }
939
+
940
+ card.append(head, codeSummary, tags, syncRow);
941
+ matrixList.append(card);
942
+ }
943
+ if (matchedCount === 0) {
944
+ const empty = el('div', tr('未匹配到任何 MCP 服务器。', 'No MCP servers matched the search.'));
945
+ empty.className = 'settings-integrations-empty';
946
+ matrixList.append(empty);
947
+ }
948
+ viewsContainer.append(matrixList);
949
+ }
950
+ };
951
+
952
+ btnByHarness.addEventListener('click', () => {
953
+ activeTab = 'harness';
954
+ btnByHarness.dataset.active = 'true';
955
+ delete btnByServer.dataset.active;
956
+ updateView();
957
+ }, { signal: lifecycle.signal });
958
+
959
+ btnByServer.addEventListener('click', () => {
960
+ activeTab = 'server';
961
+ btnByServer.dataset.active = 'true';
962
+ delete btnByHarness.dataset.active;
963
+ updateView();
964
+ }, { signal: lifecycle.signal });
965
+
966
+ searchInput.addEventListener('input', () => {
967
+ updateView();
968
+ }, { signal: lifecycle.signal });
969
+
970
+ updateView();
971
+ body.append(statsBar, toolbar, viewsContainer);
972
+ };
973
+
974
+ const showSyncDialog = (
975
+ skill: { name: string; path: string },
976
+ sourceHarness: { id: string; name: string },
977
+ targetHarnesses: Array<{ id: string; name: string }>
978
+ ) => {
979
+ const sourceDir = skill.path.replace(/[/\\][^/\\]+$/, '');
980
+ if (!targetHarnesses.length) {
981
+ fail(new Error(tr('没有其他支持技能的原生 Harness 可供同步。', 'No other native Harnesses support skills for sync.')));
982
+ return;
983
+ }
984
+ const modal = el('dialog');
985
+ modal.className = 'settings-skills-sync-modal';
986
+ const mTitle = el('h3', tr(`同步技能【${skill.name}】`, `Sync skill [${skill.name}]`));
987
+ const mDesc = el('p', tr(`从 ${sourceHarness.name} 复制该技能到以下选中的 Harness:`, `Copy skill from ${sourceHarness.name} to selected Harnesses:`));
988
+ const form = el('form');
989
+ const checkList = el('div');
990
+ checkList.style.display = 'grid';
991
+ checkList.style.gap = '8px';
992
+ checkList.style.margin = '12px 0';
993
+ const checkboxes: HTMLInputElement[] = [];
994
+ for (const h of targetHarnesses) {
995
+ const label = el('label');
996
+ label.style.display = 'flex';
997
+ label.style.gap = '8px';
998
+ label.style.alignItems = 'center';
999
+ label.style.cursor = 'pointer';
1000
+ const cb = el('input');
1001
+ cb.type = 'checkbox';
1002
+ cb.value = h.id;
1003
+ cb.checked = true;
1004
+ checkboxes.push(cb);
1005
+ label.append(cb, el('span', h.name));
1006
+ checkList.append(label);
1007
+ }
1008
+ const btnRow = el('div');
1009
+ btnRow.style.display = 'flex';
1010
+ btnRow.style.gap = '8px';
1011
+ btnRow.style.justifyContent = 'flex-end';
1012
+ btnRow.style.marginTop = '14px';
1013
+ const cancelBtn = button(tr('取消', 'Cancel'), () => { modal.close(); modal.remove(); });
1014
+ const confirmBtn = button(tr('确认同步', 'Confirm Sync'), () => {
1015
+ const selectedHarnesses = checkboxes.filter(cb => cb.checked).map(cb => cb.value);
1016
+ modal.close();
1017
+ modal.remove();
1018
+ if (!selectedHarnesses.length) return;
1019
+ void mutateCustom(async c => {
1020
+ const scopeType = level.value === 'project' ? 'project' : 'global';
1021
+ const currentCwd = level.value === 'project' ? cwd.value.trim() : undefined;
1022
+ const successes: string[] = [];
1023
+ const errors: string[] = [];
1024
+ for (const targetId of selectedHarnesses) {
1025
+ const targetH = targetHarnesses.find(h => h.id === targetId);
1026
+ try {
1027
+ await c.changeSkill({
1028
+ harnessId: targetId,
1029
+ scope: scopeType,
1030
+ ...(currentCwd ? { cwd: currentCwd } : {}),
1031
+ name: skill.name,
1032
+ source: sourceDir,
1033
+ action: 'install',
1034
+ });
1035
+ if (targetH) successes.push(targetH.name);
1036
+ } catch (err) {
1037
+ errors.push(`${targetH?.name || targetId}: ${err instanceof Error ? err.message : String(err)}`);
1038
+ }
1039
+ }
1040
+ return tr(`同步完成!已安装到:${successes.join('、')}${errors.length ? `(跳过: ${errors.join('; ')})` : ''}`,
1041
+ `Sync complete! Installed to: ${successes.join(', ')}${errors.length ? ` (Skipped: ${errors.join('; ')})` : ''}`);
1042
+ });
1043
+ }, true);
1044
+ btnRow.append(cancelBtn, confirmBtn);
1045
+ form.append(checkList, btnRow);
1046
+ modal.append(mTitle, mDesc, form);
1047
+ content.append(modal);
1048
+ modal.showModal();
1049
+ };
1050
+
1051
+ const renderSkills = (snapshot: IntegrationSnapshot) => {
1052
+ const drop = el('div'); drop.className = 'settings-skill-dropzone'; drop.tabIndex = 0; drop.setAttribute('role', 'button');
1053
+ const dropTitle = el('strong', tr('拖拽技能到这里', 'Drop a skill here'));
1054
+ const dropHint = el('span', tr('支持单个 Markdown 文件或包含 SKILL.md 的文件夹 · 最大 10 MB / 500 个文件', 'Supports one Markdown file or a folder containing SKILL.md · up to 10 MB / 500 files'));
1055
+ const picker = el('input'); picker.type = 'file'; picker.multiple = true; picker.hidden = true; picker.setAttribute('webkitdirectory', '');
1056
+ const choose = button(tr('选择技能文件夹', 'Choose skill folder'), () => picker.click());
1057
+ drop.append(dropTitle, dropHint, choose, picker);
1058
+ const list = el('div'); list.className = 'settings-integrations-list';
1059
+
1060
+ const installFiles = async (incoming: Array<{ path: string; file: File }>) => {
1061
+ const parsed = await parseDroppedSkills(incoming);
1062
+ await mutate((value, selected) => value.changeSkill({ ...selected, name: parsed.name, files: parsed.files, action: 'install' }));
1063
+ };
1064
+
1065
+ const receiveFiles = async (transfer: DataTransfer | FileList) => {
1066
+ try {
1067
+ let incoming: Array<{ path: string; file: File }> = [];
1068
+ if ('items' in transfer && transfer.items.length) {
1069
+ const entries: WebkitEntry[] = [...transfer.items]
1070
+ .map(item => (item.webkitGetAsEntry?.() ?? null) as WebkitEntry | null)
1071
+ .filter((entry): entry is WebkitEntry => entry !== null);
1072
+ if (entries.length) for (const entry of entries) await walkEntry(entry, '', incoming);
1073
+ }
1074
+ const fileList = 'files' in transfer ? transfer.files : transfer;
1075
+ if (!incoming.length) incoming = [...fileList].map(file => ({ path: file.webkitRelativePath || file.name, file }));
1076
+ await installFiles(incoming);
1077
+ } catch (error) { fail(error); }
1078
+ };
1079
+
1080
+ for (const eventName of ['dragenter', 'dragover']) drop.addEventListener(eventName, event => { event.preventDefault(); if (!busy) drop.dataset.dragOver = 'true'; }, { signal: lifecycle.signal });
1081
+ drop.addEventListener('dragleave', () => { delete drop.dataset.dragOver; }, { signal: lifecycle.signal });
1082
+ drop.addEventListener('drop', event => { event.preventDefault(); delete drop.dataset.dragOver; if (!busy && event.dataTransfer) void receiveFiles(event.dataTransfer); }, { signal: lifecycle.signal });
1083
+ picker.addEventListener('change', () => { if (picker.files?.length) void receiveFiles(picker.files); picker.value = ''; }, { signal: lifecycle.signal });
1084
+ if (!snapshot.skillsSupported) { drop.dataset.disabled = 'true'; choose.disabled = true; dropHint.textContent = tr('此 Harness 的原生技能目录尚未确认。', 'This Harness has no verified native skill directory.'); }
1085
+ for (const skill of snapshot.skills) {
1086
+ const row = el('section'); row.className = 'settings-integrations-row';
1087
+ const detail = el('div'); detail.append(el('strong', skill.name), el('p', skill.enabled ? tr('已安装 · 加载状态未报告', 'Installed · load status unreported') : tr('已停用 · 文件已保留', 'Disabled · files retained')), el('code', skill.path)); row.append(detail);
1088
+ if (skill.writable) {
1089
+ row.append(button(skill.enabled ? tr('停用', 'Disable') : tr('恢复', 'Restore'), () => { void mutate((value, selected) => value.changeSkill({ ...selected, name: skill.name, root: skill.root, action: skill.enabled ? 'disable' : 'enable' })); }));
1090
+ } else {
1091
+ row.append(el('span', tr('链接目录,只读', 'Linked directory, read-only')));
1092
+ }
1093
+ const syncBtn = button(tr('同步到…', 'Sync to…'), () => {
1094
+ void lifecycle.runLatest(() => client().integrationCatalog(), {
1095
+ success: cat => {
1096
+ const targetHarnesses = cat.harnesses.filter(h => h.skills && h.id !== snapshot.harnessId);
1097
+ showSyncDialog(skill, { id: snapshot.harnessId, name: snapshot.harnessId }, targetHarnesses);
1098
+ },
1099
+ failure: fail,
1100
+ });
1101
+ });
1102
+ row.append(syncBtn);
1103
+ list.append(row);
1104
+ }
1105
+ if (!snapshot.skills.length) { const empty = el('div', tr('还没有发现技能。拖入文件即可安装。', 'No skills found. Drop files above to install one.')); empty.className = 'settings-integrations-empty'; list.append(empty); }
1106
+ body.append(drop, list);
1107
+ };
1108
+
1109
+ const renderSkillsOverview = (catalog: { harnesses: Array<{ id: string; name: string; available: boolean; skills: boolean }> }, items: OverviewItem[]) => {
1110
+ const drop = el('div'); drop.className = 'settings-skill-dropzone'; drop.tabIndex = 0; drop.setAttribute('role', 'button');
1111
+ const dropTitle = el('strong', tr('拖拽技能到这里,一键分发安装', 'Drop a skill here to distribute and install'));
1112
+ const dropHint = el('span', tr('支持单个 Markdown 文件或包含 SKILL.md 的文件夹 · 自动安装至所选 Harness', 'Supports one Markdown file or folder with SKILL.md · Installs to target Harnesses'));
1113
+
1114
+ const targetRow = el('div'); targetRow.className = 'settings-skills-target-row';
1115
+ const targetLabel = el('span', tr('安装目标:', 'Target: '));
1116
+ const targetSelect = el('select'); targetSelect.setAttribute('aria-label', tr('选择目标 Harness', 'Select target Harness'));
1117
+ const allOption = el('option', tr('全部支持的 Harness', 'All supported Harnesses'));
1118
+ allOption.value = '__all__';
1119
+ targetSelect.append(allOption);
1120
+ for (const item of items) {
1121
+ if (item.harness.skills) {
1122
+ const opt = el('option', item.harness.name);
1123
+ opt.value = item.harness.id;
1124
+ targetSelect.append(opt);
1125
+ }
1126
+ }
1127
+ const picker = el('input'); picker.type = 'file'; picker.multiple = true; picker.hidden = true; picker.setAttribute('webkitdirectory', '');
1128
+ const choose = button(tr('选择技能文件夹', 'Choose skill folder'), () => picker.click());
1129
+ targetRow.append(targetLabel, targetSelect, choose, picker);
1130
+ drop.append(dropTitle, dropHint, targetRow);
1131
+
1132
+ const receiveOverviewFiles = async (transfer: DataTransfer | FileList) => {
1133
+ try {
1134
+ let incoming: Array<{ path: string; file: File }> = [];
1135
+ if ('items' in transfer && transfer.items.length) {
1136
+ const entries: WebkitEntry[] = [...transfer.items]
1137
+ .map(item => (item.webkitGetAsEntry?.() ?? null) as WebkitEntry | null)
1138
+ .filter((entry): entry is WebkitEntry => entry !== null);
1139
+ if (entries.length) for (const entry of entries) await walkEntry(entry, '', incoming);
1140
+ }
1141
+ const fileList = 'files' in transfer ? transfer.files : transfer;
1142
+ if (!incoming.length) incoming = [...fileList].map(file => ({ path: file.webkitRelativePath || file.name, file }));
1143
+ const parsed = await parseDroppedSkills(incoming);
1144
+ await mutateCustom(async c => {
1145
+ const scopeType = level.value === 'project' ? 'project' : 'global';
1146
+ const currentCwd = level.value === 'project' ? cwd.value.trim() : undefined;
1147
+ const targets = targetSelect.value === '__all__'
1148
+ ? items.filter(i => i.harness.skills).map(i => i.harness)
1149
+ : items.filter(i => i.harness.id === targetSelect.value).map(i => i.harness);
1150
+ if (!targets.length) throw new Error(tr('没有可安装的目标 Harness。', 'No available target Harnesses.'));
1151
+
1152
+ const successNames: string[] = [];
1153
+ const errors: string[] = [];
1154
+ for (const target of targets) {
1155
+ try {
1156
+ await c.changeSkill({
1157
+ harnessId: target.id,
1158
+ scope: scopeType,
1159
+ ...(currentCwd ? { cwd: currentCwd } : {}),
1160
+ name: parsed.name,
1161
+ files: parsed.files,
1162
+ action: 'install',
1163
+ });
1164
+ successNames.push(target.name);
1165
+ } catch (err) {
1166
+ errors.push(`${target.name}: ${err instanceof Error ? err.message : String(err)}`);
1167
+ }
1168
+ }
1169
+ if (!successNames.length && errors.length) throw new Error(errors.join('; '));
1170
+ return tr(`已成功将技能【${parsed.name}】安装到:${successNames.join('、')}${errors.length ? `(跳过: ${errors.join('; ')})` : ''}`,
1171
+ `Successfully installed skill [${parsed.name}] to: ${successNames.join(', ')}${errors.length ? ` (Skipped: ${errors.join('; ')})` : ''}`);
1172
+ });
1173
+ } catch (error) { fail(error); }
1174
+ };
1175
+
1176
+ for (const eventName of ['dragenter', 'dragover']) drop.addEventListener(eventName, event => { event.preventDefault(); if (!busy) drop.dataset.dragOver = 'true'; }, { signal: lifecycle.signal });
1177
+ drop.addEventListener('dragleave', () => { delete drop.dataset.dragOver; }, { signal: lifecycle.signal });
1178
+ drop.addEventListener('drop', event => { event.preventDefault(); delete drop.dataset.dragOver; if (!busy && event.dataTransfer) void receiveOverviewFiles(event.dataTransfer); }, { signal: lifecycle.signal });
1179
+ picker.addEventListener('change', () => { if (picker.files?.length) void receiveOverviewFiles(picker.files); picker.value = ''; }, { signal: lifecycle.signal });
1180
+
1181
+ const supported = items.filter(i => i.harness.skills);
1182
+ const uniqueSkills = new Map<string, Array<{ harness: { id: string; name: string }; skill: IntegrationSnapshot['skills'][number] }>>();
1183
+ for (const item of items) {
1184
+ if (!item.snapshot?.skills) continue;
1185
+ for (const s of item.snapshot.skills) {
1186
+ const list = uniqueSkills.get(s.name) || [];
1187
+ list.push({ harness: item.harness, skill: s });
1188
+ uniqueSkills.set(s.name, list);
1189
+ }
1190
+ }
1191
+
1192
+ const statsBar = el('div'); statsBar.className = 'settings-skills-overview-stats';
1193
+ const statsSummary = el('span', tr(
1194
+ `📊 共 ${items.length} 个 Harness · ${supported.length} 个已配置原生技能目录 · 发现 ${uniqueSkills.size} 个去重技能`,
1195
+ `📊 ${items.length} Harnesses · ${supported.length} with native skills · ${uniqueSkills.size} unique skills found`
1196
+ ));
1197
+ statsBar.append(statsSummary);
1198
+
1199
+ const toolbar = el('div'); toolbar.className = 'settings-integrations-toolbar';
1200
+ const searchInput = el('input'); searchInput.type = 'search';
1201
+ searchInput.placeholder = tr('按名称过滤技能…', 'Filter skills by name…');
1202
+ searchInput.setAttribute('aria-label', searchInput.placeholder);
1203
+
1204
+ const tabsWrap = el('div'); tabsWrap.className = 'settings-skills-tabs';
1205
+ const btnByHarness = el('button', tr('按 Harness 查看', 'By Harness'));
1206
+ btnByHarness.type = 'button'; btnByHarness.className = 'settings-skills-tab-btn';
1207
+ btnByHarness.dataset.active = 'true';
1208
+ const btnBySkill = el('button', tr('按 Skill 汇总', 'By Skill'));
1209
+ btnBySkill.type = 'button'; btnBySkill.className = 'settings-skills-tab-btn';
1210
+ tabsWrap.append(btnByHarness, btnBySkill);
1211
+ toolbar.append(searchInput, tabsWrap);
1212
+
1213
+ const viewsContainer = el('div'); viewsContainer.className = 'settings-skills-views';
1214
+ let activeTab: 'harness' | 'skill' = 'harness';
1215
+
1216
+ const updateView = () => {
1217
+ viewsContainer.replaceChildren();
1218
+ const query = searchInput.value.trim().toLowerCase();
1219
+
1220
+ if (activeTab === 'harness') {
1221
+ const grid = el('div'); grid.className = 'settings-skills-overview-grid';
1222
+ for (const item of items) {
1223
+ const allSkills = item.snapshot?.skills || [];
1224
+ const matchingSkills = query
1225
+ ? allSkills.filter(s => s.name.toLowerCase().includes(query) || s.path.toLowerCase().includes(query))
1226
+ : allSkills;
1227
+
1228
+ if (query && !item.harness.name.toLowerCase().includes(query) && matchingSkills.length === 0) {
1229
+ continue;
1230
+ }
1231
+
1232
+ const card = el('section'); card.className = 'settings-skills-harness-card';
1233
+ const head = el('div'); head.className = 'settings-skills-harness-head';
1234
+ const hTitle = el('strong', item.harness.name);
1235
+ const badge = el('span');
1236
+ badge.className = `settings-skills-badge ${item.harness.skills ? 'settings-skills-badge--active' : ''}`;
1237
+ badge.textContent = item.harness.skills
1238
+ ? tr(`${allSkills.length} 个技能`, `${allSkills.length} skills`)
1239
+ : tr('未配置目录', 'No native directory');
1240
+ head.append(hTitle, badge);
1241
+ card.append(head);
1242
+
1243
+ if (!item.harness.skills) {
1244
+ const note = el('p', tr('此 Harness 暂未配置原生技能目录。', 'This Harness has no verified native skill directory.'));
1245
+ note.className = 'settings-mcp-notice';
1246
+ card.append(note);
1247
+ } else if (matchingSkills.length === 0) {
1248
+ const empty = el('p', query ? tr('无匹配技能', 'No matching skills') : tr('暂未安装技能', 'No skills installed'));
1249
+ empty.className = 'settings-mcp-notice';
1250
+ card.append(empty);
1251
+ } else {
1252
+ const list = el('div'); list.className = 'settings-skills-list-compact';
1253
+ for (const s of matchingSkills) {
1254
+ const row = el('div'); row.className = 'settings-skills-item-row';
1255
+ const info = el('div');
1256
+ info.append(
1257
+ el('strong', s.name),
1258
+ el('span', s.enabled ? tr(' · 已安装', ' · Installed') : tr(' · 已停用', ' · Disabled')),
1259
+ el('code', s.path)
1260
+ );
1261
+ const actions = el('div'); actions.className = 'settings-skills-item-actions';
1262
+ if (s.writable) {
1263
+ actions.append(button(s.enabled ? tr('停用', 'Disable') : tr('恢复', 'Restore'), () => {
1264
+ void mutateCustom(async c => {
1265
+ const scopeType = level.value === 'project' ? 'project' : 'global';
1266
+ const currentCwd = level.value === 'project' ? cwd.value.trim() : undefined;
1267
+ await c.changeSkill({
1268
+ harnessId: item.harness.id,
1269
+ scope: scopeType,
1270
+ ...(currentCwd ? { cwd: currentCwd } : {}),
1271
+ name: s.name,
1272
+ root: s.root,
1273
+ action: s.enabled ? 'disable' : 'enable',
1274
+ });
1275
+ });
1276
+ }));
1277
+ }
1278
+ const otherSupported = supported.filter(h => h.harness.id !== item.harness.id);
1279
+ if (otherSupported.length > 0) {
1280
+ const syncBtn = button(tr('同步…', 'Sync…'), () => {
1281
+ showSyncDialog(s, item.harness, otherSupported.map(h => h.harness));
1282
+ });
1283
+ actions.append(syncBtn);
1284
+ }
1285
+ row.append(info, actions);
1286
+ list.append(row);
1287
+ }
1288
+ card.append(list);
1289
+ }
1290
+ grid.append(card);
1291
+ }
1292
+ viewsContainer.append(grid);
1293
+ } else {
1294
+ const matrixList = el('div'); matrixList.className = 'settings-skills-matrix-list';
1295
+ let matchedCount = 0;
1296
+ for (const [skillName, installations] of uniqueSkills.entries()) {
1297
+ if (query && !skillName.toLowerCase().includes(query) && !installations.some(i => i.skill.path.toLowerCase().includes(query))) {
1298
+ continue;
1299
+ }
1300
+ matchedCount++;
1301
+ const card = el('section'); card.className = 'settings-skills-matrix-card';
1302
+ const head = el('div'); head.className = 'settings-skills-matrix-head';
1303
+ head.append(
1304
+ el('strong', skillName),
1305
+ el('span', tr(`已在 ${installations.length} 个 Harness 安装`, `Installed in ${installations.length} harnesses`))
1306
+ );
1307
+
1308
+ const tags = el('div'); tags.className = 'settings-skills-tags';
1309
+ for (const inst of installations) {
1310
+ const tag = el('span'); tag.className = 'settings-skills-tag';
1311
+ tag.textContent = `✓ ${inst.harness.name}${inst.skill.enabled ? '' : tr(' (已停用)', ' (disabled)')}`;
1312
+ tags.append(tag);
1313
+ }
1314
+
1315
+ const installedIds = new Set(installations.map(i => i.harness.id));
1316
+ const missingHarnesses = supported.filter(h => !installedIds.has(h.harness.id));
1317
+
1318
+ const syncRow = el('div'); syncRow.className = 'settings-skills-sync-actions';
1319
+ if (missingHarnesses.length > 0) {
1320
+ const label = el('span', tr('未安装于:', 'Not installed in: '));
1321
+ label.style.fontSize = '11px'; label.style.color = 'var(--settings-muted)';
1322
+ syncRow.append(label);
1323
+
1324
+ const sourceSkill = installations[0]!.skill;
1325
+ const sourceDir = sourceSkill.path.replace(/[/\\][^/\\]+$/, '');
1326
+
1327
+ for (const missing of missingHarnesses) {
1328
+ const installBtn = button(`+ ${missing.harness.name}`, () => {
1329
+ void mutateCustom(async c => {
1330
+ const scopeType = level.value === 'project' ? 'project' : 'global';
1331
+ const currentCwd = level.value === 'project' ? cwd.value.trim() : undefined;
1332
+ await c.changeSkill({
1333
+ harnessId: missing.harness.id,
1334
+ scope: scopeType,
1335
+ ...(currentCwd ? { cwd: currentCwd } : {}),
1336
+ name: skillName,
1337
+ source: sourceDir,
1338
+ action: 'install',
1339
+ });
1340
+ return tr(`已将【${skillName}】同步安装到 ${missing.harness.name}。`, `Installed [${skillName}] to ${missing.harness.name}.`);
1341
+ });
1342
+ });
1343
+ syncRow.append(installBtn);
1344
+ }
1345
+
1346
+ if (missingHarnesses.length > 1) {
1347
+ const installAllBtn = button(tr('一键同步到全部', 'Sync to all'), () => {
1348
+ void mutateCustom(async c => {
1349
+ const scopeType = level.value === 'project' ? 'project' : 'global';
1350
+ const currentCwd = level.value === 'project' ? cwd.value.trim() : undefined;
1351
+ const successes: string[] = [];
1352
+ for (const missing of missingHarnesses) {
1353
+ try {
1354
+ await c.changeSkill({
1355
+ harnessId: missing.harness.id,
1356
+ scope: scopeType,
1357
+ ...(currentCwd ? { cwd: currentCwd } : {}),
1358
+ name: skillName,
1359
+ source: sourceDir,
1360
+ action: 'install',
1361
+ });
1362
+ successes.push(missing.harness.name);
1363
+ } catch { /* skip */ }
1364
+ }
1365
+ return tr(`已将【${skillName}】同步安装到:${successes.join('、')}。`, `Synced [${skillName}] to: ${successes.join(', ')}.`);
1366
+ });
1367
+ }, true);
1368
+ syncRow.append(installAllBtn);
1369
+ }
1370
+ } else {
1371
+ const complete = el('span', tr('已在所有支持的 Harness 中就绪', 'Ready across all supported Harnesses'));
1372
+ complete.style.fontSize = '11px'; complete.style.color = '#10a37f';
1373
+ syncRow.append(complete);
1374
+ }
1375
+
1376
+ card.append(head, tags, syncRow);
1377
+ matrixList.append(card);
1378
+ }
1379
+ if (matchedCount === 0) {
1380
+ const empty = el('div', tr('未匹配到任何技能。', 'No skills matched the search.'));
1381
+ empty.className = 'settings-integrations-empty';
1382
+ matrixList.append(empty);
1383
+ }
1384
+ viewsContainer.append(matrixList);
1385
+ }
1386
+ };
1387
+
1388
+ btnByHarness.addEventListener('click', () => {
1389
+ activeTab = 'harness';
1390
+ btnByHarness.dataset.active = 'true';
1391
+ delete btnBySkill.dataset.active;
1392
+ updateView();
1393
+ }, { signal: lifecycle.signal });
1394
+
1395
+ btnBySkill.addEventListener('click', () => {
1396
+ activeTab = 'skill';
1397
+ btnBySkill.dataset.active = 'true';
1398
+ delete btnByHarness.dataset.active;
1399
+ updateView();
1400
+ }, { signal: lifecycle.signal });
1401
+
1402
+ searchInput.addEventListener('input', () => {
1403
+ updateView();
1404
+ }, { signal: lifecycle.signal });
1405
+
1406
+ updateView();
1407
+ body.append(drop, statsBar, toolbar, viewsContainer);
1408
+ };
1409
+
1410
+ const render = (snapshot: IntegrationSnapshot) => { body.replaceChildren(); if (kind === 'mcp') renderMcp(snapshot); else renderSkills(snapshot); };
1411
+ const refresh = async () => {
1412
+ if (busy || lifecycle.disposed) return;
1413
+ if (level.value === 'project' && !cwd.value.trim()) { body.replaceChildren(); status.textContent = tr('填写项目路径后即可读取。', 'Enter the project path to continue.'); cwd.focus(); return; }
1414
+ setBusy(true); status.textContent = tr('正在读取…', 'Loading…'); status.setAttribute('role', 'status');
1415
+ if (harness.value === '__all__') {
1416
+ await lifecycle.runLatest(async () => {
1417
+ const catalog = await client().integrationCatalog();
1418
+ const scopeType = level.value === 'project' ? 'project' : 'global';
1419
+ const currentCwd = level.value === 'project' ? cwd.value.trim() : undefined;
1420
+ const items = await Promise.all(
1421
+ catalog.harnesses.map(async h => {
1422
+ try {
1423
+ const snap = await client().listIntegrations({
1424
+ harnessId: h.id,
1425
+ scope: scopeType,
1426
+ ...(currentCwd ? { cwd: currentCwd } : {}),
1427
+ });
1428
+ return { harness: h, snapshot: snap, error: null };
1429
+ } catch (err) {
1430
+ return { harness: h, snapshot: null, error: err instanceof Error ? err.message : String(err) };
1431
+ }
1432
+ })
1433
+ );
1434
+ return { catalog, items };
1435
+ }, {
1436
+ success: data => {
1437
+ body.replaceChildren();
1438
+ if (kind === 'mcp') {
1439
+ renderMcpOverview(data.catalog, data.items);
1440
+ } else {
1441
+ renderSkillsOverview(data.catalog, data.items);
1442
+ }
1443
+ status.textContent = '';
1444
+ },
1445
+ failure: fail,
1446
+ });
1447
+ } else {
1448
+ await lifecycle.runLatest(() => client().listIntegrations(selectedScope()), { success: data => { render(data); status.textContent = ''; }, failure: fail });
1449
+ }
1450
+ if (!lifecycle.disposed) setBusy(false);
1451
+ };
1452
+ refreshButton.addEventListener('click', () => { void refresh(); }, { signal: lifecycle.signal });
1453
+ controls.append(harness, level, cwd, refreshButton); content.append(title, note, controls, status, body);
1454
+ harness.addEventListener('change', () => { void refresh(); }, { signal: lifecycle.signal });
1455
+ level.addEventListener('change', () => { cwd.hidden = level.value !== 'project'; void refresh(); }, { signal: lifecycle.signal });
1456
+ cwd.addEventListener('change', () => { void refresh(); }, { signal: lifecycle.signal });
1457
+ void lifecycle.runLatest(() => client().integrationCatalog(), { success: data => {
1458
+ const allOption = el('option', tr('全部 Harness(总览)', 'All Harnesses (Overview)'));
1459
+ allOption.value = '__all__';
1460
+ harness.append(allOption);
1461
+ for (const item of data.harnesses) {
1462
+ const option = el('option', `${item.name}${item.available ? '' : tr(' · 未就绪', ' · not ready')}`);
1463
+ option.value = item.id;
1464
+ harness.append(option);
1465
+ }
1466
+ void refresh();
1467
+ }, failure: fail });
1468
+ return undefined;
1469
+ },
1470
+ });
1471
+ }
1472
+
1473
+ export const createMcpSettingsPage = (messages: RendererSettingsMessages, getClient: () => RendererIntegrationsClient | null) => createIntegrationSettingsPage('mcp', messages, getClient);
1474
+ export const createSkillsSettingsPage = (messages: RendererSettingsMessages, getClient: () => RendererIntegrationsClient | null) => createIntegrationSettingsPage('skills', messages, getClient);