@yancyyu/openhermit 1.6.37 → 1.6.39

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 (205) hide show
  1. package/dist-renderer/assets/ProjectEditorOverlay-krO5vQxX.js +58 -0
  2. package/dist-renderer/assets/{TeamGraphOverlay-DYT3bwFR.js → TeamGraphOverlay-DqhQzcTr.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-Dbt_EU-e.js → _basePickBy-B7kSYPxr.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-DWo68sXI.js → _baseUniq-CnjxqwAk.js} +1 -1
  5. package/dist-renderer/assets/{arc-DXH1iZQK.js → arc-CLeZuINP.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-cjffS2Qr.js → architectureDiagram-VXUJARFQ-QKtqaqdY.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-BKdZF02Y.js → blockDiagram-VD42YOAC-BqdrzO_f.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-CN27pqaI.js → c4Diagram-YG6GDRKO-gwPlCxDC.js} +1 -1
  9. package/dist-renderer/assets/channel-DpMHF50r.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-CXPCI7g_.js → chunk-4BX2VUAB-C6XLurL4.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-BGAXQZRC.js → chunk-55IACEB6-Ds6quhEP.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-TPDaA_KQ.js → chunk-B4BG7PRW-5UlA1_e9.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-D1ADe_tq.js → chunk-DI55MBZ5-ywFrqIsY.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-Beimtg3a.js → chunk-FMBD7UC4-C7ifUA17.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-OjNBu854.js → chunk-QN33PNHL-BxGCo80U.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-DinqvbH8.js → chunk-QZHKN3VN-B2CuaZs6.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-BfFtlPSZ.js → chunk-TZMSLE5B-Ds1hInvp.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-CBYCBVRl.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CBYCBVRl.js +1 -0
  20. package/dist-renderer/assets/clone-DcMF6Psb.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-D9z9Dgt7.js → cose-bilkent-S5V4N54A-Cz1GVtLp.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-n1g-DhEE.js → dagre-6UL2VRFP-BrmR-P4h.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-BvxFq-BE.js → diagram-PSM6KHXK-DbNjC5Rg.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-wVnJuwza.js → diagram-QEK2KX5R-qkRX5_Mq.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-B707WJQw.js → diagram-S2PKOQOG-CyL5rCv2.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-C-_1dGHs.js → erDiagram-Q2GNP2WA-Dox3-bA5.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-CMTSi3H6.js → flowDiagram-NV44I4VS-BtkaxlDL.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-DZ0bNrAA.js → ganttDiagram-JELNMOA3-Dhy_d9GK.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DNVfGooQ.js → gitGraphDiagram-V2S2FVAM-B5XRhIQA.js} +1 -1
  30. package/dist-renderer/assets/{graph-865j_tM_.js → graph-CsoEwUhS.js} +1 -1
  31. package/dist-renderer/assets/{index-C_F9N5x-.js → index-BWPWmJNo.js} +1 -1
  32. package/dist-renderer/assets/{index-LwDIsXJN.js → index-Bu2R-Se7.js} +586 -740
  33. package/dist-renderer/assets/index-CnWV3BhG.css +32 -0
  34. package/dist-renderer/assets/{index-DuUaf8at.js → index-D-3KgskL.js} +1 -1
  35. package/dist-renderer/assets/{index-BTx1nc4T.js → index-DGEBzLNT.js} +1 -1
  36. package/dist-renderer/assets/{index-2EW-eu3q.js → index-NhHNs2Oo.js} +1 -1
  37. package/dist-renderer/assets/{index-4dEMStJj.js → index-h17WuEyf.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-CyqtElLq.js → infoDiagram-HS3SLOUP-hMGmNojH.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-BvjQ0Hm0.js → journeyDiagram-XKPGCS4Q-DXV2rBDl.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-CJJ-k0zT.js → kanban-definition-3W4ZIXB7-Bf99WLRy.js} +1 -1
  41. package/dist-renderer/assets/{layout-CnV6rQAG.js → layout-C3XWrpwo.js} +1 -1
  42. package/dist-renderer/assets/{linear-Cw3UQgyX.js → linear-OEEcn8KN.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-C5tDaGSK.js → mindmap-definition-VGOIOE7T-Dpi3S2x4.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-CiIpPsau.js → pieDiagram-ADFJNKIX-xTPPhtNx.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-C3gtowNj.js → quadrantDiagram-AYHSOK5B-euniyDlz.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-CXBTrAnU.js → requirementDiagram-UZGBJVZJ-D9Uiw4kF.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-wziX77xG.js → sankeyDiagram-TZEHDZUN-CySU4nED.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-sYqopcrj.js → sequenceDiagram-WL72ISMW-JVGpET6V.js} +1 -1
  49. package/dist-renderer/assets/splashScene-D0YB9uxm.js +17 -0
  50. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-Bl1-0_Cp.js → stateDiagram-FKZM4ZOC-B2FY5qqi.js} +1 -1
  51. package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-DcoMiR8H.js +1 -0
  52. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-CIRjJUBo.js → timeline-definition-IT6M3QCI-DmycNUUe.js} +1 -1
  53. package/dist-renderer/assets/{treemap-GDKQZRPO-CVPuNe1n.js → treemap-GDKQZRPO-DPq4gZuB.js} +1 -1
  54. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-3nT9yHwp.js → xychartDiagram-PRI3JC2R-J6VVJzRq.js} +1 -1
  55. package/dist-renderer/index.html +20 -53
  56. package/package.json +25 -18
  57. package/src/main/ipc/extensions.ts +30 -50
  58. package/src/main/server.ts +890 -247
  59. package/src/main/services/extensions/ExtensionFacadeService.ts +4 -56
  60. package/src/main/services/extensions/catalog/PluginCatalogService.ts +4 -2
  61. package/src/main/services/extensions/library/McpLibraryService.ts +243 -0
  62. package/src/main/services/session-intelligence/ConversationTelemetryService.ts +1101 -0
  63. package/src/main/services/session-intelligence/LocalSessionScanner.ts +512 -0
  64. package/src/main/services/session-intelligence/SessionUsageParser.ts +4 -4
  65. package/src/main/services/session-intelligence/UsageTelemetryService.ts +14 -1
  66. package/src/main/services/system-manager/SystemManagerConfigService.ts +122 -0
  67. package/src/main/services/system-manager/SystemManagerPtyService.ts +233 -0
  68. package/src/main/services/system-manager/WorkflowPromptService.ts +75 -0
  69. package/src/main/services/teams-mvp/TaskDispatchService.ts +32 -8
  70. package/src/main/services/teams-mvp/TeamProvisioningService.ts +39 -2
  71. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +22 -4
  72. package/src/main/utils/teamProjectResolution.ts +15 -0
  73. package/src/renderer/App.tsx +8 -4
  74. package/src/renderer/api/httpClient.ts +174 -38
  75. package/src/renderer/api/providers.ts +23 -2
  76. package/src/renderer/assets/participant-avatars/01.svg +3 -0
  77. package/src/renderer/assets/participant-avatars/02.svg +3 -0
  78. package/src/renderer/assets/participant-avatars/03.svg +3 -0
  79. package/src/renderer/assets/participant-avatars/04.svg +3 -0
  80. package/src/renderer/assets/participant-avatars/05.svg +3 -0
  81. package/src/renderer/assets/participant-avatars/06.svg +3 -0
  82. package/src/renderer/assets/participant-avatars/07.svg +3 -0
  83. package/src/renderer/assets/participant-avatars/08.svg +3 -0
  84. package/src/renderer/assets/participant-avatars/09.svg +3 -0
  85. package/src/renderer/assets/participant-avatars/10.svg +3 -0
  86. package/src/renderer/assets/participant-avatars/11.svg +3 -0
  87. package/src/renderer/assets/participant-avatars/12.svg +3 -0
  88. package/src/renderer/assets/participant-avatars/13.svg +3 -0
  89. package/src/renderer/components/common/TerminalPane.tsx +213 -0
  90. package/src/renderer/components/dashboard/DashboardView.tsx +9 -36
  91. package/src/renderer/components/extensions/ExtensionStoreView.tsx +12 -221
  92. package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
  93. package/src/renderer/components/extensions/mcp/McpLibraryEnableDialog.tsx +305 -0
  94. package/src/renderer/components/extensions/mcp/McpLibraryEntryDialog.tsx +418 -0
  95. package/src/renderer/components/extensions/mcp/McpLibraryPanel.tsx +404 -0
  96. package/src/renderer/components/extensions/plugins/PluginCard.tsx +10 -2
  97. package/src/renderer/components/extensions/plugins/PluginsPanel.tsx +40 -22
  98. package/src/renderer/components/extensions/skills/SkillsLibraryPanel.tsx +335 -0
  99. package/src/renderer/components/layout/PaneContent.tsx +8 -1
  100. package/src/renderer/components/layout/Sidebar.tsx +11 -54
  101. package/src/renderer/components/layout/SortableTab.tsx +20 -31
  102. package/src/renderer/components/layout/TabBar.tsx +1 -1
  103. package/src/renderer/components/layout/TabContextMenu.tsx +1 -1
  104. package/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +768 -157
  105. package/src/renderer/components/schedules/SchedulesView.tsx +51 -462
  106. package/src/renderer/components/schedules/calendar/CalendarDayView.tsx +173 -0
  107. package/src/renderer/components/schedules/calendar/CalendarEventBlock.tsx +113 -0
  108. package/src/renderer/components/schedules/calendar/CalendarHeader.tsx +148 -0
  109. package/src/renderer/components/schedules/calendar/CalendarMonthView.tsx +142 -0
  110. package/src/renderer/components/schedules/calendar/CalendarWeekView.tsx +219 -0
  111. package/src/renderer/components/schedules/calendar/ScheduleCalendarBoard.tsx +41 -0
  112. package/src/renderer/components/schedules/calendar/TeamGanttView.tsx +405 -0
  113. package/src/renderer/components/schedules/calendar/computeOccurrences.ts +234 -0
  114. package/src/renderer/components/schedules/calendar/index.ts +2 -0
  115. package/src/renderer/components/schedules/calendar/types.ts +44 -0
  116. package/src/renderer/components/settings/SettingsTabs.tsx +50 -55
  117. package/src/renderer/components/settings/SettingsView.tsx +30 -35
  118. package/src/renderer/components/settings/components/SettingsSectionHeader.tsx +5 -1
  119. package/src/renderer/components/settings/components/SettingsSelect.tsx +5 -3
  120. package/src/renderer/components/settings/components/SettingsToggle.tsx +2 -2
  121. package/src/renderer/components/settings/sections/AdvancedSection.tsx +11 -42
  122. package/src/renderer/components/settings/sections/CliStatusSection.tsx +71 -112
  123. package/src/renderer/components/settings/sections/ConfigEditorDialog.tsx +1 -1
  124. package/src/renderer/components/settings/sections/GeneralSection.tsx +11 -3
  125. package/src/renderer/components/settings/sections/HarnessSection.tsx +18 -14
  126. package/src/renderer/components/settings/sections/PlatformsSection.tsx +3 -3
  127. package/src/renderer/components/settings/sections/TaskBusSection.tsx +33 -40
  128. package/src/renderer/components/settings/sections/index.ts +0 -1
  129. package/src/renderer/components/sidebar/SidebarSessions.tsx +182 -4
  130. package/src/renderer/components/sidebar/SidebarTaskItem.tsx +4 -4
  131. package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +39 -4
  132. package/src/renderer/components/splash/splashScene.ts +121 -929
  133. package/src/renderer/components/system-manager/FolderBrowser.tsx +163 -0
  134. package/src/renderer/components/system-manager/SystemManagerView.tsx +351 -0
  135. package/src/renderer/components/tasks/TasksView.tsx +112 -134
  136. package/src/renderer/components/team/CcSessionsSection.tsx +431 -89
  137. package/src/renderer/components/team/CollapsibleTeamSection.tsx +17 -32
  138. package/src/renderer/components/team/TeamDetailView.tsx +325 -114
  139. package/src/renderer/components/team/TeamListView.tsx +108 -123
  140. package/src/renderer/components/team/dialogs/CreateTaskDialog.tsx +2 -2
  141. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +84 -306
  142. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +259 -342
  143. package/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx +1 -1
  144. package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +17 -15
  145. package/src/renderer/components/team/dialogs/PlatformBindingDialog.tsx +221 -0
  146. package/src/renderer/components/team/dialogs/PlatformManualForm.tsx +7 -0
  147. package/src/renderer/components/team/dialogs/PlatformSetupQR.tsx +1 -1
  148. package/src/renderer/components/team/dialogs/RuntimeConfigDialog.tsx +361 -0
  149. package/src/renderer/components/team/dialogs/platformMeta.ts +122 -11
  150. package/src/renderer/components/team/dialogs/useTeamEditForm.ts +17 -5
  151. package/src/renderer/components/team/kanban/KanbanBoard.tsx +9 -9
  152. package/src/renderer/components/team/members/MemberCard.tsx +14 -47
  153. package/src/renderer/components/team/members/MemberDetailDialog.tsx +3 -95
  154. package/src/renderer/components/team/members/MemberDetailStats.tsx +50 -65
  155. package/src/renderer/components/team/messages/MessageComposer.tsx +8 -110
  156. package/src/renderer/components/team/messages/MessagesPanel.tsx +131 -114
  157. package/src/renderer/components/team/schedule/ScheduleStatusBadge.tsx +2 -2
  158. package/src/renderer/components/team/tools/AddMcpInline.tsx +57 -0
  159. package/src/renderer/components/team/tools/AddSkillInline.tsx +61 -0
  160. package/src/renderer/components/team/tools/McpChip.tsx +45 -0
  161. package/src/renderer/components/team/tools/SkillChip.tsx +35 -0
  162. package/src/renderer/components/team/tools/ToolsSection.tsx +556 -0
  163. package/src/renderer/hooks/useExtensionsTabState.ts +3 -114
  164. package/src/renderer/index.css +39 -22
  165. package/src/renderer/index.html +17 -50
  166. package/src/renderer/store/index.ts +2 -1
  167. package/src/renderer/store/slices/scheduleSlice.ts +1 -1
  168. package/src/renderer/store/slices/teamSlice.ts +45 -168
  169. package/src/renderer/utils/claudeCodeOnlyProviders.ts +3 -10
  170. package/src/renderer/utils/memberHelpers.ts +5 -17
  171. package/src/renderer/utils/openCodeRuntimeDeliveryDiagnostics.ts +4 -2
  172. package/src/renderer/utils/providerSlashCommands.ts +0 -5
  173. package/src/renderer/utils/scheduleFormatters.ts +3 -1
  174. package/src/renderer/utils/teamMessageFiltering.ts +14 -1
  175. package/src/renderer/utils/teamModelAvailability.ts +18 -2
  176. package/src/shared/types/api.ts +121 -2
  177. package/src/shared/types/ccConnect.ts +2 -0
  178. package/src/shared/types/extensions/api.ts +9 -0
  179. package/src/shared/types/extensions/index.ts +4 -0
  180. package/src/shared/types/extensions/mcp.ts +41 -0
  181. package/src/shared/types/index.ts +3 -0
  182. package/src/shared/types/systemManager.ts +49 -0
  183. package/src/shared/types/team.ts +29 -0
  184. package/src/shared/types/terminal.ts +4 -2
  185. package/src/shared/utils/extensionNormalizers.ts +29 -0
  186. package/src/shared/utils/providerExtensionCapabilities.ts +2 -2
  187. package/dist-renderer/assets/ProjectEditorOverlay-Va_Vz-zz.js +0 -52
  188. package/dist-renderer/assets/channel-5dJIx68e.js +0 -1
  189. package/dist-renderer/assets/classDiagram-2ON5EDUG-BMGXWJ2d.js +0 -1
  190. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-BMGXWJ2d.js +0 -1
  191. package/dist-renderer/assets/clone-D7FWfGY9.js +0 -1
  192. package/dist-renderer/assets/index-B2z_IyRH.css +0 -1
  193. package/dist-renderer/assets/splashScene-C8lWNnm4.js +0 -1
  194. package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-DOYYvDbi.js +0 -1
  195. package/src/main/services/extensions/catalog/GlamaMcpEnrichmentService.ts +0 -190
  196. package/src/main/services/extensions/catalog/McpCatalogAggregator.ts +0 -150
  197. package/src/main/services/extensions/catalog/OfficialMcpRegistryService.ts +0 -381
  198. package/src/main/services/extensions/install/McpInstallService.ts +0 -407
  199. package/src/main/services/extensions/state/McpInstallationStateService.ts +0 -42
  200. package/src/renderer/components/extensions/mcp/McpServerCard.tsx +0 -314
  201. package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +0 -765
  202. package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +0 -593
  203. package/src/renderer/components/extensions/skills/SkillDetailDialog.tsx +0 -372
  204. package/src/renderer/components/extensions/skills/SkillImportDialog.tsx +0 -343
  205. package/src/renderer/components/extensions/skills/SkillsPanel.tsx +0 -659
@@ -1,7 +1,11 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from 'react';
2
2
 
3
3
  import { providersApi } from '@renderer/api/providers';
4
+ import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
5
+ import { AGENT_TYPE_LABELS, ALL_AGENT_TYPES } from '@renderer/components/team/HarnessCards';
6
+ import { Badge } from '@renderer/components/ui/badge';
4
7
  import { Button } from '@renderer/components/ui/button';
8
+ import { Checkbox } from '@renderer/components/ui/checkbox';
5
9
  import {
6
10
  Dialog,
7
11
  DialogContent,
@@ -10,11 +14,18 @@ import {
10
14
  DialogTitle,
11
15
  } from '@renderer/components/ui/dialog';
12
16
  import { Input } from '@renderer/components/ui/input';
17
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/ui/tabs';
13
18
  import { emitOpenHermitEvent, OPEN_HERMIT_EVENTS } from '@renderer/utils/openHermitEvents';
14
- import { Loader2, RefreshCw } from 'lucide-react';
19
+ import { CheckCircle2, Download, Loader2, Pencil, Plus, RefreshCw, Trash2 } from 'lucide-react';
15
20
 
16
21
  import type { CliProviderId, CliProviderStatus } from '@shared/types';
17
- import type { AgentType, GlobalProvider } from '@shared/types/providers';
22
+ import type {
23
+ AgentType,
24
+ CCSwitchProvider,
25
+ GlobalProvider,
26
+ ProviderModelEntry,
27
+ ProviderPreset,
28
+ } from '@shared/types/providers';
18
29
 
19
30
  interface Props {
20
31
  readonly open: boolean;
@@ -29,6 +40,20 @@ interface Props {
29
40
  readonly onRequestLogin?: (providerId: CliProviderId) => void;
30
41
  }
31
42
 
43
+ interface ProviderFormState {
44
+ name: string;
45
+ apiKey: string;
46
+ baseUrl: string;
47
+ model: string;
48
+ modelsText: string;
49
+ thinking: string;
50
+ agentTypes: AgentType[];
51
+ endpoints: Partial<Record<AgentType, string>>;
52
+ agentModels: Partial<Record<AgentType, string>>;
53
+ codexWireApi: string;
54
+ codexHeadersText: string;
55
+ }
56
+
32
57
  const AGENT_TYPE_BY_CLI_PROVIDER: Record<CliProviderId, AgentType> = {
33
58
  anthropic: 'claudecode',
34
59
  codex: 'codex',
@@ -43,6 +68,32 @@ const CLI_PROVIDER_LABELS: Record<CliProviderId, string> = {
43
68
  opencode: 'OpenCode',
44
69
  };
45
70
 
71
+ const CORE_AGENT_TYPES: AgentType[] = ['claudecode', 'codex', 'gemini', 'opencode'];
72
+
73
+ const PRESET_AGENT_KEY_MAP: Record<string, AgentType> = {
74
+ claude: 'claudecode',
75
+ anthropic: 'claudecode',
76
+ claudecode: 'claudecode',
77
+ codex: 'codex',
78
+ openai: 'codex',
79
+ gemini: 'gemini',
80
+ opencode: 'opencode',
81
+ };
82
+
83
+ function normalizeAgentType(value: string): AgentType | null {
84
+ const mapped = PRESET_AGENT_KEY_MAP[value.toLowerCase()];
85
+ if (mapped) return mapped;
86
+ return (ALL_AGENT_TYPES as readonly string[]).includes(value) ? (value as AgentType) : null;
87
+ }
88
+
89
+ function providerSupportsAgent(provider: GlobalProvider, agentType: AgentType): boolean {
90
+ return (
91
+ !provider.agent_types ||
92
+ provider.agent_types.length === 0 ||
93
+ provider.agent_types.includes(agentType)
94
+ );
95
+ }
96
+
46
97
  function resolveProviderEndpoint(provider: GlobalProvider, agentType: AgentType): string {
47
98
  return provider.endpoints?.[agentType] ?? provider.base_url ?? '默认端点';
48
99
  }
@@ -56,6 +107,161 @@ function resolveProviderModel(provider: GlobalProvider, agentType: AgentType): s
56
107
  );
57
108
  }
58
109
 
110
+ function formatModels(models?: ProviderModelEntry[]): string {
111
+ return (models ?? [])
112
+ .map((entry) => (entry.alias ? `${entry.model}:${entry.alias}` : entry.model))
113
+ .join(', ');
114
+ }
115
+
116
+ function parseModels(text: string): ProviderModelEntry[] | undefined {
117
+ const entries = text
118
+ .split(/[\n,]/)
119
+ .map((part) => part.trim())
120
+ .filter(Boolean)
121
+ .map((part) => {
122
+ const [model, alias] = part.split(':').map((segment) => segment.trim());
123
+ return alias ? { model, alias } : { model };
124
+ })
125
+ .filter((entry) => entry.model.length > 0);
126
+ return entries.length > 0 ? entries : undefined;
127
+ }
128
+
129
+ function parseKeyValueText(text: string): Record<string, string> | undefined {
130
+ const out: Record<string, string> = {};
131
+ for (const line of text.split('\n')) {
132
+ const trimmed = line.trim();
133
+ if (!trimmed) continue;
134
+ const separatorIndex = trimmed.indexOf('=');
135
+ if (separatorIndex <= 0) continue;
136
+ const key = trimmed.slice(0, separatorIndex).trim();
137
+ const value = trimmed.slice(separatorIndex + 1).trim();
138
+ if (key) out[key] = value;
139
+ }
140
+ return Object.keys(out).length > 0 ? out : undefined;
141
+ }
142
+
143
+ function formatKeyValue(record?: Record<string, string>): string {
144
+ return Object.entries(record ?? {})
145
+ .map(([key, value]) => `${key}=${value}`)
146
+ .join('\n');
147
+ }
148
+
149
+ function emptyForm(agentType: AgentType): ProviderFormState {
150
+ return {
151
+ name: '',
152
+ apiKey: '',
153
+ baseUrl: '',
154
+ model: '',
155
+ modelsText: '',
156
+ thinking: '',
157
+ agentTypes: [agentType],
158
+ endpoints: {},
159
+ agentModels: {},
160
+ codexWireApi: '',
161
+ codexHeadersText: '',
162
+ };
163
+ }
164
+
165
+ function formFromProvider(
166
+ provider: GlobalProvider,
167
+ fallbackAgentType: AgentType
168
+ ): ProviderFormState {
169
+ return {
170
+ name: provider.name,
171
+ apiKey: provider.api_key ?? '',
172
+ baseUrl: provider.base_url ?? '',
173
+ model: provider.model ?? '',
174
+ modelsText: formatModels(provider.models),
175
+ thinking: provider.thinking ?? '',
176
+ agentTypes: provider.agent_types?.length ? provider.agent_types : [fallbackAgentType],
177
+ endpoints: provider.endpoints ?? {},
178
+ agentModels: provider.agent_models ?? {},
179
+ codexWireApi: provider.codex?.wire_api ?? '',
180
+ codexHeadersText: formatKeyValue(provider.codex?.http_headers),
181
+ };
182
+ }
183
+
184
+ function formFromPreset(preset: ProviderPreset, fallbackAgentType: AgentType): ProviderFormState {
185
+ const agentEntries = Object.entries(preset.agents ?? {})
186
+ .map(([rawKey, config]) => ({ agentType: normalizeAgentType(rawKey), config }))
187
+ .filter(
188
+ (
189
+ entry
190
+ ): entry is { agentType: AgentType; config: NonNullable<ProviderPreset['agents'][string]> } =>
191
+ entry.agentType != null
192
+ );
193
+ const agentTypes =
194
+ agentEntries.length > 0 ? agentEntries.map((entry) => entry.agentType) : [fallbackAgentType];
195
+ const firstConfig = agentEntries[0]?.config;
196
+ const endpoints: Partial<Record<AgentType, string>> = {};
197
+ const agentModels: Partial<Record<AgentType, string>> = {};
198
+ for (const entry of agentEntries) {
199
+ if (entry.config.base_url) endpoints[entry.agentType] = entry.config.base_url;
200
+ if (entry.config.model) agentModels[entry.agentType] = entry.config.model;
201
+ }
202
+ return {
203
+ name: preset.display_name || preset.name,
204
+ apiKey: '',
205
+ baseUrl: firstConfig?.base_url ?? '',
206
+ model: firstConfig?.model ?? '',
207
+ modelsText: (firstConfig?.models ?? []).join(', '),
208
+ thinking: preset.thinking ?? '',
209
+ agentTypes,
210
+ endpoints,
211
+ agentModels,
212
+ codexWireApi: preset.agents?.codex?.codex_config?.wire_api ?? '',
213
+ codexHeadersText: formatKeyValue(preset.agents?.codex?.codex_config?.http_headers),
214
+ };
215
+ }
216
+
217
+ function formFromCCSwitch(
218
+ provider: CCSwitchProvider,
219
+ fallbackAgentType: AgentType
220
+ ): ProviderFormState {
221
+ const agentType = normalizeAgentType(provider.app_type) ?? fallbackAgentType;
222
+ return {
223
+ ...emptyForm(agentType),
224
+ name: provider.name,
225
+ apiKey: provider.api_key ?? '',
226
+ baseUrl: provider.base_url ?? '',
227
+ model: provider.model ?? '',
228
+ agentTypes: [agentType],
229
+ agentModels: provider.model ? { [agentType]: provider.model } : {},
230
+ endpoints: provider.base_url ? { [agentType]: provider.base_url } : {},
231
+ };
232
+ }
233
+
234
+ function formToProvider(form: ProviderFormState, originalName?: string): GlobalProvider {
235
+ const agentTypes = form.agentTypes.length > 0 ? form.agentTypes : undefined;
236
+ const endpoints = Object.fromEntries(
237
+ Object.entries(form.endpoints).filter(([, value]) => value?.trim())
238
+ ) as Partial<Record<AgentType, string>>;
239
+ const agentModels = Object.fromEntries(
240
+ Object.entries(form.agentModels).filter(([, value]) => value?.trim())
241
+ ) as Partial<Record<AgentType, string>>;
242
+ const codexHeaders = parseKeyValueText(form.codexHeadersText);
243
+ const codex =
244
+ form.codexWireApi.trim() || codexHeaders
245
+ ? {
246
+ ...(form.codexWireApi.trim() ? { wire_api: form.codexWireApi.trim() } : {}),
247
+ ...(codexHeaders ? { http_headers: codexHeaders } : {}),
248
+ }
249
+ : undefined;
250
+
251
+ return {
252
+ name: form.name.trim() || originalName || '',
253
+ ...(form.apiKey.trim() ? { api_key: form.apiKey.trim() } : {}),
254
+ ...(form.baseUrl.trim() ? { base_url: form.baseUrl.trim() } : {}),
255
+ ...(form.model.trim() ? { model: form.model.trim() } : {}),
256
+ ...(form.thinking.trim() ? { thinking: form.thinking.trim() } : {}),
257
+ ...(agentTypes ? { agent_types: agentTypes } : {}),
258
+ ...(parseModels(form.modelsText) ? { models: parseModels(form.modelsText) } : {}),
259
+ ...(Object.keys(endpoints).length > 0 ? { endpoints } : {}),
260
+ ...(Object.keys(agentModels).length > 0 ? { agent_models: agentModels } : {}),
261
+ ...(codex ? { codex } : {}),
262
+ };
263
+ }
264
+
59
265
  export const ProviderRuntimeSettingsDialog = ({
60
266
  open,
61
267
  onOpenChange,
@@ -64,17 +270,20 @@ export const ProviderRuntimeSettingsDialog = ({
64
270
  const agentType = AGENT_TYPE_BY_CLI_PROVIDER[initialProviderId];
65
271
  const harnessLabel = CLI_PROVIDER_LABELS[initialProviderId] ?? initialProviderId;
66
272
  const [providers, setProviders] = useState<GlobalProvider[]>([]);
273
+ const [presets, setPresets] = useState<ProviderPreset[]>([]);
274
+ const [ccSwitchProviders, setCcSwitchProviders] = useState<CCSwitchProvider[]>([]);
275
+ const [ccSwitchAvailable, setCcSwitchAvailable] = useState<boolean | null>(null);
67
276
  const [loading, setLoading] = useState(false);
277
+ const [presetsLoading, setPresetsLoading] = useState(false);
278
+ const [ccSwitchLoading, setCcSwitchLoading] = useState(false);
279
+ const [saving, setSaving] = useState(false);
68
280
  const [error, setError] = useState<string | null>(null);
69
- const [newProviderName, setNewProviderName] = useState('');
70
- const [newProviderModel, setNewProviderModel] = useState('');
71
- const [newProviderBaseUrl, setNewProviderBaseUrl] = useState('');
72
- const [newProviderApiKey, setNewProviderApiKey] = useState('');
73
- const [adding, setAdding] = useState(false);
74
- const [addError, setAddError] = useState<string | null>(null);
75
-
76
- const harnessProviders = useMemo(
77
- () => providers.filter((provider) => provider.agent_types?.includes(agentType)),
281
+ const [formError, setFormError] = useState<string | null>(null);
282
+ const [editingName, setEditingName] = useState<string | null>(null);
283
+ const [form, setForm] = useState<ProviderFormState>(() => emptyForm(agentType));
284
+
285
+ const compatibleProviders = useMemo(
286
+ () => providers.filter((provider) => providerSupportsAgent(provider, agentType)),
78
287
  [agentType, providers]
79
288
  );
80
289
 
@@ -92,194 +301,596 @@ export const ProviderRuntimeSettingsDialog = ({
92
301
  }
93
302
  }, []);
94
303
 
95
- useEffect(() => {
96
- if (open) {
97
- void refreshProviders();
304
+ const refreshPresets = useCallback(async (): Promise<void> => {
305
+ setPresetsLoading(true);
306
+ try {
307
+ const result = await providersApi.fetchPresets();
308
+ setPresets(result.providers ?? []);
309
+ } catch {
310
+ setPresets([]);
311
+ } finally {
312
+ setPresetsLoading(false);
98
313
  }
99
- }, [open, refreshProviders]);
314
+ }, []);
100
315
 
101
- useEffect(() => {
102
- if (!open) {
103
- setNewProviderName('');
104
- setNewProviderModel('');
105
- setNewProviderBaseUrl('');
106
- setNewProviderApiKey('');
107
- setAddError(null);
316
+ const refreshCCSwitch = useCallback(async (): Promise<void> => {
317
+ setCcSwitchLoading(true);
318
+ try {
319
+ const result = await providersApi.listCCSwitch();
320
+ setCcSwitchProviders(result.providers ?? []);
321
+ setCcSwitchAvailable(result.available);
322
+ } catch {
323
+ setCcSwitchProviders([]);
324
+ setCcSwitchAvailable(false);
325
+ } finally {
326
+ setCcSwitchLoading(false);
108
327
  }
109
- }, [open]);
328
+ }, []);
329
+
330
+ useEffect(() => {
331
+ if (!open) return;
332
+ setForm(emptyForm(agentType));
333
+ setEditingName(null);
334
+ setFormError(null);
335
+ void refreshProviders();
336
+ void refreshPresets();
337
+ void refreshCCSwitch();
338
+ }, [agentType, open, refreshCCSwitch, refreshPresets, refreshProviders]);
339
+
340
+ const updateForm = (patch: Partial<ProviderFormState>): void => {
341
+ setForm((prev) => ({ ...prev, ...patch }));
342
+ setFormError(null);
343
+ };
344
+
345
+ const toggleAgentType = (nextAgentType: AgentType): void => {
346
+ setForm((prev) => {
347
+ const exists = prev.agentTypes.includes(nextAgentType);
348
+ const next = exists
349
+ ? prev.agentTypes.filter((value) => value !== nextAgentType)
350
+ : [...prev.agentTypes, nextAgentType];
351
+ return { ...prev, agentTypes: next.length > 0 ? next : [nextAgentType] };
352
+ });
353
+ setFormError(null);
354
+ };
110
355
 
111
- const handleAddProvider = async (): Promise<void> => {
112
- if (!newProviderName.trim()) {
113
- setAddError('请填写 Provider 名称');
356
+ const handleSave = async (): Promise<void> => {
357
+ if (!form.name.trim()) {
358
+ setFormError('请填写 Provider 名称');
114
359
  return;
115
360
  }
116
- setAdding(true);
117
- setAddError(null);
361
+ setSaving(true);
362
+ setFormError(null);
118
363
  try {
119
- await providersApi.add({
120
- name: newProviderName.trim(),
121
- model: newProviderModel.trim() || undefined,
122
- base_url: newProviderBaseUrl.trim() || undefined,
123
- api_key: newProviderApiKey.trim() || undefined,
124
- agent_types: [agentType],
125
- });
126
- setNewProviderName('');
127
- setNewProviderModel('');
128
- setNewProviderBaseUrl('');
129
- setNewProviderApiKey('');
364
+ const payload = formToProvider(form, editingName ?? undefined);
365
+ if (editingName) {
366
+ const existingProvider = providers.find((provider) => provider.name === editingName);
367
+ const { name: _ignoredName, ...patch } = payload;
368
+ await providersApi.update(editingName, {
369
+ ...patch,
370
+ ...(existingProvider?.env ? { env: existingProvider.env } : {}),
371
+ ...(existingProvider?.api_key && !patch.api_key ? { api_key: undefined } : {}),
372
+ ...(existingProvider?.base_url && !patch.base_url ? { base_url: undefined } : {}),
373
+ ...(existingProvider?.model && !patch.model ? { model: undefined } : {}),
374
+ ...(existingProvider?.thinking && !patch.thinking ? { thinking: undefined } : {}),
375
+ ...(existingProvider?.models && !patch.models ? { models: undefined } : {}),
376
+ ...(existingProvider?.endpoints && !patch.endpoints ? { endpoints: undefined } : {}),
377
+ ...(existingProvider?.agent_models && !patch.agent_models
378
+ ? { agent_models: undefined }
379
+ : {}),
380
+ ...(existingProvider?.codex && !patch.codex ? { codex: undefined } : {}),
381
+ });
382
+ } else {
383
+ await providersApi.add(payload);
384
+ }
385
+ setForm(emptyForm(agentType));
386
+ setEditingName(null);
130
387
  await refreshProviders();
131
388
  emitOpenHermitEvent(OPEN_HERMIT_EVENTS.providersChanged);
132
389
  } catch (err) {
133
- setAddError(err instanceof Error ? err.message : '添加 Provider 失败');
390
+ setFormError(err instanceof Error ? err.message : '保存 Provider 失败');
134
391
  } finally {
135
- setAdding(false);
392
+ setSaving(false);
393
+ }
394
+ };
395
+
396
+ const handleDelete = async (providerName: string): Promise<void> => {
397
+ setSaving(true);
398
+ try {
399
+ await providersApi.remove(providerName);
400
+ if (editingName === providerName) {
401
+ setEditingName(null);
402
+ setForm(emptyForm(agentType));
403
+ }
404
+ await refreshProviders();
405
+ emitOpenHermitEvent(OPEN_HERMIT_EVENTS.providersChanged);
406
+ } catch (err) {
407
+ setError(err instanceof Error ? err.message : '删除 Provider 失败');
408
+ } finally {
409
+ setSaving(false);
410
+ }
411
+ };
412
+
413
+ const handleImportCCSwitch = async (providerName: string): Promise<void> => {
414
+ setSaving(true);
415
+ try {
416
+ await providersApi.importCCSwitch([providerName]);
417
+ await refreshProviders();
418
+ emitOpenHermitEvent(OPEN_HERMIT_EVENTS.providersChanged);
419
+ } catch (err) {
420
+ setError(err instanceof Error ? err.message : '导入 cc-switch Provider 失败');
421
+ } finally {
422
+ setSaving(false);
136
423
  }
137
424
  };
138
425
 
139
426
  return (
140
427
  <Dialog open={open} onOpenChange={onOpenChange}>
141
- <DialogContent className="w-[min(92vw,760px)] max-w-[min(92vw,760px)]">
428
+ <DialogContent className="flex max-h-[88vh] w-[min(96vw,1120px)] max-w-[min(96vw,1120px)] flex-col overflow-hidden">
142
429
  <DialogHeader>
143
- <DialogTitle>{harnessLabel} 配置</DialogTitle>
430
+ <DialogTitle>{harnessLabel} / 全局 Provider</DialogTitle>
144
431
  <DialogDescription>
145
- 统一管理当前 Harness 可用的 Provider。账号、模型、端点都通过 Provider 配置维护。
432
+ Provider 是全局资源:先在这里配置网关、模型和适用 Harness,再在团队里选择绑定。
146
433
  </DialogDescription>
147
434
  </DialogHeader>
148
435
 
149
- <div className="space-y-4">
150
- <div
151
- className="space-y-3 rounded-lg border p-3"
152
- style={{ borderColor: 'var(--color-border-subtle)' }}
153
- >
154
- <div>
155
- <div className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
156
- 新增 Provider
157
- </div>
158
- <div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
159
- 保存后会自动绑定到 {harnessLabel}。
160
- </div>
161
- </div>
162
- <div className="grid gap-2 sm:grid-cols-2">
163
- <Input
164
- value={newProviderName}
165
- onChange={(event) => setNewProviderName(event.target.value)}
166
- placeholder="Provider 名称,例如 deepseek"
167
- />
168
- <Input
169
- value={newProviderModel}
170
- onChange={(event) => setNewProviderModel(event.target.value)}
171
- placeholder="默认模型(可选)"
172
- />
173
- <Input
174
- value={newProviderBaseUrl}
175
- onChange={(event) => setNewProviderBaseUrl(event.target.value)}
176
- placeholder="Base URL(可选)"
177
- />
178
- <Input
179
- type="password"
180
- value={newProviderApiKey}
181
- onChange={(event) => setNewProviderApiKey(event.target.value)}
182
- placeholder="API Key(可选)"
183
- />
184
- </div>
185
- {addError ? <div className="text-xs text-red-400">{addError}</div> : null}
186
- <div className="flex justify-end">
187
- <Button
188
- size="sm"
189
- variant="outline"
190
- disabled={adding}
191
- onClick={() => void handleAddProvider()}
192
- >
193
- {adding ? <Loader2 className="mr-1 size-3.5 animate-spin" /> : null}
194
- 添加 Provider
195
- </Button>
196
- </div>
436
+ <div className="grid min-h-0 flex-1 gap-4 overflow-hidden lg:grid-cols-[minmax(0,1.05fr)_minmax(360px,0.95fr)]">
437
+ <div className="min-h-0 overflow-y-auto pr-1">
438
+ <Tabs defaultValue="providers" className="min-h-0">
439
+ <TabsList className="mb-3">
440
+ <TabsTrigger value="providers">Provider 库</TabsTrigger>
441
+ <TabsTrigger value="presets">预设</TabsTrigger>
442
+ <TabsTrigger value="cc-switch">cc-switch</TabsTrigger>
443
+ </TabsList>
444
+
445
+ <TabsContent value="providers" className="mt-0 space-y-3">
446
+ <div className="flex items-center justify-between gap-3">
447
+ <div>
448
+ <div className="text-sm font-medium text-[var(--color-text)]">
449
+ 全局 Provider
450
+ </div>
451
+ <div className="text-xs text-[var(--color-text-muted)]">
452
+ 当前 {compatibleProviders.length} 个适用于 {harnessLabel},共{' '}
453
+ {providers.length} 个。
454
+ </div>
455
+ </div>
456
+ <Button
457
+ size="sm"
458
+ variant="outline"
459
+ disabled={loading}
460
+ onClick={() => void refreshProviders()}
461
+ >
462
+ <RefreshCw
463
+ className={loading ? 'mr-1 size-3.5 animate-spin' : 'mr-1 size-3.5'}
464
+ />
465
+ 刷新
466
+ </Button>
467
+ </div>
468
+
469
+ {error ? (
470
+ <div className="rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-400">
471
+ {error}
472
+ </div>
473
+ ) : null}
474
+
475
+ {loading && providers.length === 0 ? (
476
+ <div className="flex items-center gap-2 text-xs text-[var(--color-text-muted)]">
477
+ <Loader2 className="size-3 animate-spin" />
478
+ 正在加载 Provider...
479
+ </div>
480
+ ) : providers.length === 0 ? (
481
+ <div className="rounded-xl border border-dashed border-[var(--color-border)] p-6 text-center text-sm text-[var(--color-text-muted)]">
482
+ 还没有全局 Provider。可以从右侧新建,或从预设/cc-switch 导入。
483
+ </div>
484
+ ) : (
485
+ <div className="space-y-2">
486
+ {providers.map((provider) => {
487
+ const isCompatible = providerSupportsAgent(provider, agentType);
488
+ return (
489
+ <div
490
+ key={provider.name}
491
+ className={`rounded-xl border px-3 py-3 ${
492
+ isCompatible
493
+ ? 'border-[var(--color-border-subtle)] bg-white/[0.025]'
494
+ : 'border-[var(--color-border)] bg-black/10 opacity-70'
495
+ }`}
496
+ >
497
+ <div className="flex flex-wrap items-start justify-between gap-2">
498
+ <div className="min-w-0">
499
+ <div className="flex flex-wrap items-center gap-2">
500
+ <span className="truncate text-sm font-medium text-[var(--color-text)]">
501
+ {provider.name}
502
+ </span>
503
+ {isCompatible ? (
504
+ <Badge
505
+ variant="secondary"
506
+ className="text-[10px] text-emerald-300"
507
+ >
508
+ 适用于当前 Harness
509
+ </Badge>
510
+ ) : null}
511
+ <Badge variant="outline" className="text-[10px]">
512
+ {provider.api_key ? 'Key 已配置' : '未配置 Key'}
513
+ </Badge>
514
+ </div>
515
+ <div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-[var(--color-text-muted)]">
516
+ <span>端点:{resolveProviderEndpoint(provider, agentType)}</span>
517
+ <span>模型:{resolveProviderModel(provider, agentType)}</span>
518
+ </div>
519
+ <div className="mt-2 flex flex-wrap gap-1.5">
520
+ {(provider.agent_types?.length
521
+ ? provider.agent_types
522
+ : CORE_AGENT_TYPES
523
+ ).map((type) => (
524
+ <Badge key={type} variant="secondary" className="text-[10px]">
525
+ {AGENT_TYPE_LABELS[type] ?? type}
526
+ </Badge>
527
+ ))}
528
+ </div>
529
+ </div>
530
+ <div className="flex shrink-0 items-center gap-1">
531
+ <Button
532
+ size="sm"
533
+ variant="ghost"
534
+ className="h-7 px-2 text-xs"
535
+ onClick={() => {
536
+ setEditingName(provider.name);
537
+ setForm(formFromProvider(provider, agentType));
538
+ setFormError(null);
539
+ }}
540
+ >
541
+ <Pencil className="mr-1 size-3" />
542
+ 编辑
543
+ </Button>
544
+ <Button
545
+ size="sm"
546
+ variant="ghost"
547
+ className="h-7 px-2 text-xs text-red-400 hover:bg-red-500/10 hover:text-red-300"
548
+ disabled={saving}
549
+ onClick={() => void handleDelete(provider.name)}
550
+ >
551
+ <Trash2 className="mr-1 size-3" />
552
+ 删除
553
+ </Button>
554
+ </div>
555
+ </div>
556
+ </div>
557
+ );
558
+ })}
559
+ </div>
560
+ )}
561
+ </TabsContent>
562
+
563
+ <TabsContent value="presets" className="mt-0 space-y-3">
564
+ <div className="flex items-center justify-between gap-3">
565
+ <div>
566
+ <div className="text-sm font-medium text-[var(--color-text)]">从预设开始</div>
567
+ <div className="text-xs text-[var(--color-text-muted)]">
568
+ 参考 cc-switch 的交互:先选网关预设,再补 Key、模型和适用 Harness。
569
+ </div>
570
+ </div>
571
+ <Button
572
+ size="sm"
573
+ variant="outline"
574
+ disabled={presetsLoading}
575
+ onClick={() => void refreshPresets()}
576
+ >
577
+ <RefreshCw
578
+ className={presetsLoading ? 'mr-1 size-3.5 animate-spin' : 'mr-1 size-3.5'}
579
+ />
580
+ 刷新
581
+ </Button>
582
+ </div>
583
+ <div className="grid gap-2 sm:grid-cols-2">
584
+ <button
585
+ type="button"
586
+ className="rounded-xl border border-dashed border-[var(--color-border)] p-3 text-left transition hover:border-[var(--color-border-emphasis)] hover:bg-white/[0.035]"
587
+ onClick={() => {
588
+ setEditingName(null);
589
+ setForm({ ...emptyForm(agentType), agentTypes: CORE_AGENT_TYPES });
590
+ setFormError(null);
591
+ }}
592
+ >
593
+ <div className="flex items-center gap-2 text-sm font-medium text-[var(--color-text)]">
594
+ <Plus className="size-4" />
595
+ 自定义网关
596
+ </div>
597
+ <p className="mt-1 text-xs text-[var(--color-text-muted)]">
598
+ 手动配置一个可用于多个 Harness 的 API 网关。
599
+ </p>
600
+ </button>
601
+ {presets.map((preset) => (
602
+ <button
603
+ key={preset.name}
604
+ type="button"
605
+ className="rounded-xl border border-[var(--color-border-subtle)] bg-white/[0.025] p-3 text-left transition hover:border-[var(--color-border-emphasis)] hover:bg-white/[0.045]"
606
+ onClick={() => {
607
+ setEditingName(null);
608
+ setForm(formFromPreset(preset, agentType));
609
+ setFormError(null);
610
+ }}
611
+ >
612
+ <div className="flex items-start justify-between gap-2">
613
+ <div className="text-sm font-medium text-[var(--color-text)]">
614
+ {preset.display_name || preset.name}
615
+ </div>
616
+ {preset.featured ? <Badge className="text-[10px]">推荐</Badge> : null}
617
+ </div>
618
+ <p className="mt-1 line-clamp-2 text-xs text-[var(--color-text-muted)]">
619
+ {preset.description_zh ||
620
+ preset.description ||
621
+ preset.website ||
622
+ 'Provider 预设'}
623
+ </p>
624
+ </button>
625
+ ))}
626
+ </div>
627
+ </TabsContent>
628
+
629
+ <TabsContent value="cc-switch" className="mt-0 space-y-3">
630
+ <div className="flex items-center justify-between gap-3">
631
+ <div>
632
+ <div className="text-sm font-medium text-[var(--color-text)]">
633
+ 从 cc-switch 导入
634
+ </div>
635
+ <div className="text-xs text-[var(--color-text-muted)]">
636
+ 可导入已有 cc-switch Provider,再在右侧按 Hermit 字段调整。
637
+ </div>
638
+ </div>
639
+ <Button
640
+ size="sm"
641
+ variant="outline"
642
+ disabled={ccSwitchLoading}
643
+ onClick={() => void refreshCCSwitch()}
644
+ >
645
+ <RefreshCw
646
+ className={ccSwitchLoading ? 'mr-1 size-3.5 animate-spin' : 'mr-1 size-3.5'}
647
+ />
648
+ 刷新
649
+ </Button>
650
+ </div>
651
+ {ccSwitchAvailable === false ? (
652
+ <div className="rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-300">
653
+ 没有检测到可导入的 Provider,或服务未返回导入数据。
654
+ </div>
655
+ ) : null}
656
+ <div className="space-y-2">
657
+ {ccSwitchProviders.map((provider) => (
658
+ <div
659
+ key={`${provider.app_type}:${provider.name}`}
660
+ className="rounded-xl border border-[var(--color-border-subtle)] bg-white/[0.025] px-3 py-3"
661
+ >
662
+ <div className="flex flex-wrap items-start justify-between gap-2">
663
+ <div className="min-w-0">
664
+ <div className="flex items-center gap-2 text-sm font-medium text-[var(--color-text)]">
665
+ {provider.is_current ? (
666
+ <CheckCircle2 className="size-3.5 text-emerald-400" />
667
+ ) : null}
668
+ {provider.name}
669
+ </div>
670
+ <div className="mt-1 text-[11px] text-[var(--color-text-muted)]">
671
+ {provider.app_type} · {provider.base_url || '默认端点'} ·{' '}
672
+ {provider.model || '未指定模型'}
673
+ </div>
674
+ </div>
675
+ <div className="flex items-center gap-1">
676
+ <Button
677
+ size="sm"
678
+ variant="ghost"
679
+ className="h-7 px-2 text-xs"
680
+ onClick={() => {
681
+ setEditingName(null);
682
+ setForm(formFromCCSwitch(provider, agentType));
683
+ setFormError(null);
684
+ }}
685
+ >
686
+ 填入表单
687
+ </Button>
688
+ <Button
689
+ size="sm"
690
+ variant="outline"
691
+ className="h-7 px-2 text-xs"
692
+ disabled={saving}
693
+ onClick={() => void handleImportCCSwitch(provider.name)}
694
+ >
695
+ <Download className="mr-1 size-3" />
696
+ 直接导入
697
+ </Button>
698
+ </div>
699
+ </div>
700
+ </div>
701
+ ))}
702
+ {ccSwitchProviders.length === 0 && !ccSwitchLoading ? (
703
+ <div className="rounded-xl border border-dashed border-[var(--color-border)] p-5 text-center text-xs text-[var(--color-text-muted)]">
704
+ 暂无 cc-switch Provider 可导入。
705
+ </div>
706
+ ) : null}
707
+ </div>
708
+ </TabsContent>
709
+ </Tabs>
197
710
  </div>
198
711
 
199
- <div
200
- className="space-y-3 rounded-lg border p-3"
201
- style={{ borderColor: 'var(--color-border-subtle)' }}
202
- >
203
- <div className="flex items-center justify-between gap-3">
712
+ <div className="min-h-0 overflow-y-auto rounded-xl border border-[var(--color-border-subtle)] bg-white/[0.025] p-4">
713
+ <div className="flex items-start justify-between gap-3">
204
714
  <div>
205
- <div className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
206
- 已绑定 Provider
207
- </div>
208
- <div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
209
- Agent 类型:{agentType}
715
+ <div className="text-sm font-semibold text-[var(--color-text)]">
716
+ {editingName ? `编辑 ${editingName}` : 'Provider 表单'}
210
717
  </div>
718
+ <p className="mt-0.5 text-xs text-[var(--color-text-muted)]">
719
+ 这里配置全局 Provider;团队里只选择启用,不重复填写。
720
+ </p>
211
721
  </div>
212
722
  <Button
213
723
  size="sm"
214
724
  variant="ghost"
215
- disabled={loading}
216
- onClick={() => void refreshProviders()}
725
+ className="h-7 px-2 text-xs"
726
+ onClick={() => {
727
+ setEditingName(null);
728
+ setForm(emptyForm(agentType));
729
+ setFormError(null);
730
+ }}
217
731
  >
218
- <RefreshCw className={loading ? 'mr-1 size-3.5 animate-spin' : 'mr-1 size-3.5'} />
219
- 刷新
732
+ 清空
220
733
  </Button>
221
734
  </div>
222
735
 
223
- {error ? (
224
- <div className="rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-400">
225
- {error}
736
+ <div className="mt-4 space-y-4">
737
+ <div className="grid gap-2 sm:grid-cols-2">
738
+ <label className="space-y-1 text-xs text-[var(--color-text-secondary)]">
739
+ <span>Provider 名称</span>
740
+ <Input
741
+ value={form.name}
742
+ disabled={editingName != null}
743
+ onChange={(event) => updateForm({ name: event.target.value })}
744
+ placeholder="NewAPI / n1n.ai / custom"
745
+ />
746
+ </label>
747
+ <label className="space-y-1 text-xs text-[var(--color-text-secondary)]">
748
+ <span>默认模型</span>
749
+ <Input
750
+ value={form.model}
751
+ onChange={(event) => updateForm({ model: event.target.value })}
752
+ placeholder="claude-sonnet-4 / gpt-4o / gemini-2.5-pro"
753
+ />
754
+ </label>
755
+ <label className="space-y-1 text-xs text-[var(--color-text-secondary)]">
756
+ <span>Base URL</span>
757
+ <Input
758
+ value={form.baseUrl}
759
+ onChange={(event) => updateForm({ baseUrl: event.target.value })}
760
+ placeholder="https://api.example.com/v1"
761
+ />
762
+ </label>
763
+ <label className="space-y-1 text-xs text-[var(--color-text-secondary)]">
764
+ <span>API Key</span>
765
+ <Input
766
+ type="password"
767
+ value={form.apiKey}
768
+ onChange={(event) => updateForm({ apiKey: event.target.value })}
769
+ placeholder="sk-..."
770
+ />
771
+ </label>
226
772
  </div>
227
- ) : null}
228
773
 
229
- {loading && harnessProviders.length === 0 ? (
230
- <div className="flex items-center gap-2 text-xs text-[var(--color-text-muted)]">
231
- <Loader2 className="size-3 animate-spin" />
232
- 正在加载 Provider...
774
+ <div className="space-y-2">
775
+ <div className="text-xs font-medium text-[var(--color-text-secondary)]">
776
+ 适用 Harness
777
+ </div>
778
+ <div className="grid gap-2 sm:grid-cols-2">
779
+ {ALL_AGENT_TYPES.map((type) => (
780
+ <label
781
+ key={type}
782
+ className="flex cursor-pointer items-center gap-2 rounded-lg border border-[var(--color-border-subtle)] px-2 py-1.5 text-xs text-[var(--color-text-secondary)] hover:bg-white/[0.03]"
783
+ >
784
+ <Checkbox
785
+ checked={form.agentTypes.includes(type as AgentType)}
786
+ onCheckedChange={() => toggleAgentType(type as AgentType)}
787
+ />
788
+ <span>{AGENT_TYPE_LABELS[type] ?? type}</span>
789
+ </label>
790
+ ))}
791
+ </div>
233
792
  </div>
234
- ) : harnessProviders.length > 0 ? (
793
+
794
+ <label className="block space-y-1 text-xs text-[var(--color-text-secondary)]">
795
+ <span>模型列表(逗号或换行分隔,支持 model:alias)</span>
796
+ <textarea
797
+ value={form.modelsText}
798
+ onChange={(event) => updateForm({ modelsText: event.target.value })}
799
+ className="min-h-16 w-full rounded-md border border-[var(--color-border)] bg-transparent px-3 py-2 text-sm text-[var(--color-text)] outline-none focus:border-[var(--color-border-emphasis)]"
800
+ placeholder="claude-sonnet-4, claude-opus-4:Opus"
801
+ />
802
+ </label>
803
+
235
804
  <div className="space-y-2">
236
- {harnessProviders.map((provider) => (
237
- <div
238
- key={provider.name}
239
- className="rounded-lg border px-3 py-2"
240
- style={{
241
- borderColor: 'var(--color-border-subtle)',
242
- backgroundColor: 'rgba(255, 255, 255, 0.025)',
243
- }}
244
- >
245
- <div className="flex flex-wrap items-center justify-between gap-2">
246
- <div className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
247
- {provider.name}
248
- </div>
249
- <span
250
- className="rounded-full px-2 py-0.5 text-[11px]"
251
- style={{
252
- color: provider.api_key ? '#86efac' : '#fbbf24',
253
- backgroundColor: provider.api_key
254
- ? 'rgba(74, 222, 128, 0.14)'
255
- : 'rgba(245, 158, 11, 0.12)',
256
- }}
257
- >
258
- {provider.api_key ? 'API Key 已配置' : '未配置 Key'}
259
- </span>
260
- </div>
805
+ <div className="text-xs font-medium text-[var(--color-text-secondary)]">
806
+ 每个 Harness 的覆盖配置
807
+ </div>
808
+ <div className="space-y-2">
809
+ {form.agentTypes.map((type) => (
261
810
  <div
262
- className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11px]"
263
- style={{ color: 'var(--color-text-muted)' }}
811
+ key={type}
812
+ className="rounded-lg border border-[var(--color-border-subtle)] p-2"
264
813
  >
265
- <span>端点:{resolveProviderEndpoint(provider, agentType)}</span>
266
- <span>模型:{resolveProviderModel(provider, agentType)}</span>
267
- {provider.thinking ? <span>Thinking:{provider.thinking}</span> : null}
814
+ <div className="mb-2 flex items-center gap-2 text-xs font-medium text-[var(--color-text)]">
815
+ {type === 'claudecode' ? (
816
+ <ProviderBrandLogo providerId="anthropic" className="size-3.5" />
817
+ ) : null}
818
+ {type === 'codex' ? (
819
+ <ProviderBrandLogo providerId="codex" className="size-3.5" />
820
+ ) : null}
821
+ {type === 'gemini' ? (
822
+ <ProviderBrandLogo providerId="gemini" className="size-3.5" />
823
+ ) : null}
824
+ {type === 'opencode' ? (
825
+ <ProviderBrandLogo providerId="opencode" className="size-3.5" />
826
+ ) : null}
827
+ {AGENT_TYPE_LABELS[type] ?? type}
828
+ </div>
829
+ <div className="grid gap-2 sm:grid-cols-2">
830
+ <Input
831
+ value={form.endpoints[type] ?? ''}
832
+ onChange={(event) =>
833
+ updateForm({
834
+ endpoints: { ...form.endpoints, [type]: event.target.value },
835
+ })
836
+ }
837
+ placeholder="专用 endpoint(可选)"
838
+ />
839
+ <Input
840
+ value={form.agentModels[type] ?? ''}
841
+ onChange={(event) =>
842
+ updateForm({
843
+ agentModels: { ...form.agentModels, [type]: event.target.value },
844
+ })
845
+ }
846
+ placeholder="专用默认模型(可选)"
847
+ />
848
+ </div>
268
849
  </div>
269
- </div>
270
- ))}
850
+ ))}
851
+ </div>
271
852
  </div>
272
- ) : (
273
- <div
274
- className="rounded-md border px-3 py-2 text-xs"
275
- style={{
276
- borderColor: 'var(--color-border-subtle)',
277
- color: 'var(--color-text-muted)',
278
- }}
279
- >
280
- 当前还没有绑定到 {harnessLabel} Provider。请在上方添加一个 Provider。
853
+
854
+ {form.agentTypes.includes('codex') ? (
855
+ <div className="space-y-2 rounded-lg border border-[var(--color-border-subtle)] p-3">
856
+ <div className="text-xs font-medium text-[var(--color-text-secondary)]">
857
+ Codex 高级配置
858
+ </div>
859
+ <Input
860
+ value={form.codexWireApi}
861
+ onChange={(event) => updateForm({ codexWireApi: event.target.value })}
862
+ placeholder="wire_api(可选)"
863
+ />
864
+ <textarea
865
+ value={form.codexHeadersText}
866
+ onChange={(event) => updateForm({ codexHeadersText: event.target.value })}
867
+ className="min-h-14 w-full rounded-md border border-[var(--color-border)] bg-transparent px-3 py-2 text-sm text-[var(--color-text)] outline-none focus:border-[var(--color-border-emphasis)]"
868
+ placeholder="HTTP headers,每行 KEY=VALUE"
869
+ />
870
+ </div>
871
+ ) : null}
872
+
873
+ <label className="block space-y-1 text-xs text-[var(--color-text-secondary)]">
874
+ <span>Thinking 设置(可选)</span>
875
+ <Input
876
+ value={form.thinking}
877
+ onChange={(event) => updateForm({ thinking: event.target.value })}
878
+ placeholder="enabled / disabled / 留空"
879
+ />
880
+ </label>
881
+
882
+ {formError ? <div className="text-xs text-red-400">{formError}</div> : null}
883
+
884
+ <div className="flex justify-end gap-2 border-t border-[var(--color-border-subtle)] pt-3">
885
+ <Button variant="outline" disabled={saving} onClick={() => onOpenChange(false)}>
886
+ 关闭
887
+ </Button>
888
+ <Button disabled={saving} onClick={() => void handleSave()}>
889
+ {saving ? <Loader2 className="mr-1 size-3.5 animate-spin" /> : null}
890
+ {editingName ? '保存修改' : '保存 Provider'}
891
+ </Button>
281
892
  </div>
282
- )}
893
+ </div>
283
894
  </div>
284
895
  </div>
285
896
  </DialogContent>