cowork-os 0.3.21 → 0.3.25

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 (252) hide show
  1. package/README.md +372 -10
  2. package/connectors/README.md +20 -0
  3. package/connectors/asana-mcp/README.md +24 -0
  4. package/connectors/asana-mcp/dist/index.js +427 -0
  5. package/connectors/asana-mcp/package.json +15 -0
  6. package/connectors/asana-mcp/src/index.ts +553 -0
  7. package/connectors/asana-mcp/tsconfig.json +13 -0
  8. package/connectors/hubspot-mcp/README.md +35 -0
  9. package/connectors/hubspot-mcp/dist/index.js +454 -0
  10. package/connectors/hubspot-mcp/package.json +15 -0
  11. package/connectors/hubspot-mcp/src/index.ts +562 -0
  12. package/connectors/hubspot-mcp/tsconfig.json +13 -0
  13. package/connectors/jira-mcp/README.md +49 -0
  14. package/connectors/jira-mcp/dist/index.js +588 -0
  15. package/connectors/jira-mcp/package.json +15 -0
  16. package/connectors/jira-mcp/src/index.ts +711 -0
  17. package/connectors/jira-mcp/tsconfig.json +13 -0
  18. package/connectors/linear-mcp/README.md +22 -0
  19. package/connectors/linear-mcp/dist/index.js +402 -0
  20. package/connectors/linear-mcp/package.json +15 -0
  21. package/connectors/linear-mcp/src/index.ts +522 -0
  22. package/connectors/linear-mcp/tsconfig.json +13 -0
  23. package/connectors/okta-mcp/README.md +24 -0
  24. package/connectors/okta-mcp/dist/index.js +411 -0
  25. package/connectors/okta-mcp/package.json +15 -0
  26. package/connectors/okta-mcp/src/index.ts +520 -0
  27. package/connectors/okta-mcp/tsconfig.json +13 -0
  28. package/connectors/salesforce-mcp/README.md +47 -0
  29. package/connectors/salesforce-mcp/dist/index.js +584 -0
  30. package/connectors/salesforce-mcp/package.json +15 -0
  31. package/connectors/salesforce-mcp/src/index.ts +722 -0
  32. package/connectors/salesforce-mcp/tsconfig.json +13 -0
  33. package/connectors/servicenow-mcp/README.md +26 -0
  34. package/connectors/servicenow-mcp/dist/index.js +400 -0
  35. package/connectors/servicenow-mcp/package.json +15 -0
  36. package/connectors/servicenow-mcp/src/index.ts +500 -0
  37. package/connectors/servicenow-mcp/tsconfig.json +13 -0
  38. package/connectors/templates/mcp-connector/README.md +31 -0
  39. package/connectors/templates/mcp-connector/package.json +15 -0
  40. package/connectors/templates/mcp-connector/src/index.ts +330 -0
  41. package/connectors/templates/mcp-connector/tsconfig.json +13 -0
  42. package/connectors/zendesk-mcp/README.md +40 -0
  43. package/connectors/zendesk-mcp/dist/index.js +431 -0
  44. package/connectors/zendesk-mcp/package.json +15 -0
  45. package/connectors/zendesk-mcp/src/index.ts +543 -0
  46. package/connectors/zendesk-mcp/tsconfig.json +13 -0
  47. package/dist/electron/electron/agent/custom-skill-loader.js +31 -1
  48. package/dist/electron/electron/agent/daemon.js +189 -13
  49. package/dist/electron/electron/agent/executor.js +895 -78
  50. package/dist/electron/electron/agent/llm/anthropic-compatible-provider.js +177 -0
  51. package/dist/electron/electron/agent/llm/azure-openai-provider.js +328 -0
  52. package/dist/electron/electron/agent/llm/bedrock-provider.js +49 -9
  53. package/dist/electron/electron/agent/llm/github-copilot-provider.js +97 -0
  54. package/dist/electron/electron/agent/llm/groq-provider.js +33 -0
  55. package/dist/electron/electron/agent/llm/index.js +13 -1
  56. package/dist/electron/electron/agent/llm/kimi-provider.js +33 -0
  57. package/dist/electron/electron/agent/llm/openai-compatible-provider.js +116 -0
  58. package/dist/electron/electron/agent/llm/openai-compatible.js +111 -0
  59. package/dist/electron/electron/agent/llm/openai-oauth.js +2 -1
  60. package/dist/electron/electron/agent/llm/openrouter-provider.js +1 -1
  61. package/dist/electron/electron/agent/llm/provider-factory.js +350 -4
  62. package/dist/electron/electron/agent/llm/types.js +66 -1
  63. package/dist/electron/electron/agent/llm/xai-provider.js +33 -0
  64. package/dist/electron/electron/agent/search/provider-factory.js +38 -2
  65. package/dist/electron/electron/agent/tools/box-tools.js +231 -0
  66. package/dist/electron/electron/agent/tools/builtin-settings.js +28 -0
  67. package/dist/electron/electron/agent/tools/dropbox-tools.js +237 -0
  68. package/dist/electron/electron/agent/tools/file-tools.js +66 -3
  69. package/dist/electron/electron/agent/tools/google-drive-tools.js +227 -0
  70. package/dist/electron/electron/agent/tools/grep-tools.js +90 -10
  71. package/dist/electron/electron/agent/tools/image-tools.js +11 -1
  72. package/dist/electron/electron/agent/tools/notion-tools.js +312 -0
  73. package/dist/electron/electron/agent/tools/onedrive-tools.js +217 -0
  74. package/dist/electron/electron/agent/tools/registry.js +548 -10
  75. package/dist/electron/electron/agent/tools/search-tools.js +28 -10
  76. package/dist/electron/electron/agent/tools/sharepoint-tools.js +243 -0
  77. package/dist/electron/electron/agent/tools/shell-tools.js +12 -3
  78. package/dist/electron/electron/agent/tools/x-tools.js +1 -1
  79. package/dist/electron/electron/agents/agent-dispatch.js +63 -0
  80. package/dist/electron/electron/database/repositories.js +19 -5
  81. package/dist/electron/electron/database/schema.js +8 -0
  82. package/dist/electron/electron/gateway/channels/whatsapp.js +55 -0
  83. package/dist/electron/electron/gateway/index.js +75 -1
  84. package/dist/electron/electron/gateway/router.js +209 -154
  85. package/dist/electron/electron/ipc/canvas-handlers.js +5 -0
  86. package/dist/electron/electron/ipc/handlers.js +763 -267
  87. package/dist/electron/electron/main.js +63 -0
  88. package/dist/electron/electron/mcp/oauth/connector-oauth.js +333 -0
  89. package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +503 -154
  90. package/dist/electron/electron/memory/MemoryService.js +2 -1
  91. package/dist/electron/electron/preload.js +78 -1
  92. package/dist/electron/electron/settings/appearance-manager.js +18 -1
  93. package/dist/electron/electron/settings/box-manager.js +54 -0
  94. package/dist/electron/electron/settings/dropbox-manager.js +54 -0
  95. package/dist/electron/electron/settings/google-drive-manager.js +54 -0
  96. package/dist/electron/electron/settings/notion-manager.js +56 -0
  97. package/dist/electron/electron/settings/onedrive-manager.js +54 -0
  98. package/dist/electron/electron/settings/sharepoint-manager.js +54 -0
  99. package/dist/electron/electron/utils/box-api.js +153 -0
  100. package/dist/electron/electron/utils/dropbox-api.js +144 -0
  101. package/dist/electron/electron/utils/env-migration.js +19 -0
  102. package/dist/electron/electron/utils/google-drive-api.js +152 -0
  103. package/dist/electron/electron/utils/notion-api.js +103 -0
  104. package/dist/electron/electron/utils/onedrive-api.js +113 -0
  105. package/dist/electron/electron/utils/sharepoint-api.js +109 -0
  106. package/dist/electron/electron/utils/validation.js +98 -3
  107. package/dist/electron/electron/utils/x-cli.js +1 -1
  108. package/dist/electron/shared/channelMessages.js +284 -3
  109. package/dist/electron/shared/llm-provider-catalog.js +198 -0
  110. package/dist/electron/shared/types.js +90 -1
  111. package/package.json +14 -3
  112. package/resources/skills/nano-banana-pro.json +4 -4
  113. package/resources/skills/openai-image-gen.json +3 -3
  114. package/resources/skills/scripts/gen.py +163 -0
  115. package/resources/skills/scripts/generate_image.py +91 -0
  116. package/src/electron/agent/custom-skill-loader.ts +34 -1
  117. package/src/electron/agent/daemon.ts +210 -14
  118. package/src/electron/agent/executor.ts +1124 -85
  119. package/src/electron/agent/llm/anthropic-compatible-provider.ts +214 -0
  120. package/src/electron/agent/llm/azure-openai-provider.ts +388 -0
  121. package/src/electron/agent/llm/bedrock-provider.ts +62 -9
  122. package/src/electron/agent/llm/github-copilot-provider.ts +117 -0
  123. package/src/electron/agent/llm/groq-provider.ts +39 -0
  124. package/src/electron/agent/llm/index.ts +6 -0
  125. package/src/electron/agent/llm/kimi-provider.ts +39 -0
  126. package/src/electron/agent/llm/openai-compatible-provider.ts +153 -0
  127. package/src/electron/agent/llm/openai-compatible.ts +133 -0
  128. package/src/electron/agent/llm/openai-oauth.ts +2 -1
  129. package/src/electron/agent/llm/openrouter-provider.ts +2 -1
  130. package/src/electron/agent/llm/provider-factory.ts +459 -6
  131. package/src/electron/agent/llm/types.ts +95 -1
  132. package/src/electron/agent/llm/xai-provider.ts +39 -0
  133. package/src/electron/agent/search/provider-factory.ts +43 -2
  134. package/src/electron/agent/tools/box-tools.ts +239 -0
  135. package/src/electron/agent/tools/builtin-settings.ts +36 -0
  136. package/src/electron/agent/tools/dropbox-tools.ts +237 -0
  137. package/src/electron/agent/tools/file-tools.ts +66 -3
  138. package/src/electron/agent/tools/gmail-tools.ts +240 -0
  139. package/src/electron/agent/tools/google-calendar-tools.ts +258 -0
  140. package/src/electron/agent/tools/google-drive-tools.ts +228 -0
  141. package/src/electron/agent/tools/grep-tools.ts +97 -12
  142. package/src/electron/agent/tools/image-tools.ts +11 -1
  143. package/src/electron/agent/tools/notion-tools.ts +330 -0
  144. package/src/electron/agent/tools/onedrive-tools.ts +217 -0
  145. package/src/electron/agent/tools/registry.ts +794 -10
  146. package/src/electron/agent/tools/search-tools.ts +29 -11
  147. package/src/electron/agent/tools/sharepoint-tools.ts +247 -0
  148. package/src/electron/agent/tools/shell-tools.ts +11 -3
  149. package/src/electron/agent/tools/x-tools.ts +1 -1
  150. package/src/electron/agents/agent-dispatch.ts +79 -0
  151. package/src/electron/database/SecureSettingsRepository.ts +7 -1
  152. package/src/electron/database/repositories.ts +58 -6
  153. package/src/electron/database/schema.ts +8 -0
  154. package/src/electron/gateway/channels/discord.ts +4 -0
  155. package/src/electron/gateway/channels/google-chat.ts +3 -0
  156. package/src/electron/gateway/channels/line.ts +3 -0
  157. package/src/electron/gateway/channels/matrix-client.ts +15 -0
  158. package/src/electron/gateway/channels/matrix.ts +31 -0
  159. package/src/electron/gateway/channels/mattermost.ts +3 -0
  160. package/src/electron/gateway/channels/signal.ts +3 -0
  161. package/src/electron/gateway/channels/slack.ts +9 -4
  162. package/src/electron/gateway/channels/teams.ts +4 -0
  163. package/src/electron/gateway/channels/telegram.ts +2 -0
  164. package/src/electron/gateway/channels/twitch.ts +2 -0
  165. package/src/electron/gateway/channels/types.ts +8 -0
  166. package/src/electron/gateway/channels/whatsapp.ts +66 -0
  167. package/src/electron/gateway/index.ts +95 -2
  168. package/src/electron/gateway/router.ts +231 -161
  169. package/src/electron/gateway/security.ts +21 -9
  170. package/src/electron/ipc/canvas-handlers.ts +10 -0
  171. package/src/electron/ipc/handlers.ts +848 -292
  172. package/src/electron/main.ts +35 -0
  173. package/src/electron/mcp/oauth/connector-oauth.ts +448 -0
  174. package/src/electron/mcp/registry/MCPRegistryManager.ts +343 -12
  175. package/src/electron/memory/MemoryService.ts +7 -1
  176. package/src/electron/preload.ts +200 -5
  177. package/src/electron/settings/appearance-manager.ts +20 -2
  178. package/src/electron/settings/box-manager.ts +58 -0
  179. package/src/electron/settings/dropbox-manager.ts +58 -0
  180. package/src/electron/settings/google-workspace-manager.ts +59 -0
  181. package/src/electron/settings/notion-manager.ts +60 -0
  182. package/src/electron/settings/onedrive-manager.ts +58 -0
  183. package/src/electron/settings/sharepoint-manager.ts +58 -0
  184. package/src/electron/utils/box-api.ts +184 -0
  185. package/src/electron/utils/dropbox-api.ts +171 -0
  186. package/src/electron/utils/env-migration.ts +22 -0
  187. package/src/electron/utils/gmail-api.ts +121 -0
  188. package/src/electron/utils/google-calendar-api.ts +115 -0
  189. package/src/electron/utils/google-workspace-api.ts +228 -0
  190. package/src/electron/utils/google-workspace-auth.ts +109 -0
  191. package/src/electron/utils/google-workspace-oauth.ts +232 -0
  192. package/src/electron/utils/notion-api.ts +126 -0
  193. package/src/electron/utils/onedrive-api.ts +137 -0
  194. package/src/electron/utils/sharepoint-api.ts +132 -0
  195. package/src/electron/utils/validation.ts +128 -1
  196. package/src/electron/utils/x-cli.ts +1 -1
  197. package/src/renderer/App.tsx +119 -8
  198. package/src/renderer/components/ActivityFeedItem.tsx +34 -17
  199. package/src/renderer/components/AgentWorkingStatePanel.tsx +7 -5
  200. package/src/renderer/components/AppearanceSettings.tsx +37 -2
  201. package/src/renderer/components/BlueBubblesSettings.tsx +18 -7
  202. package/src/renderer/components/BoxSettings.tsx +203 -0
  203. package/src/renderer/components/BrowserView.tsx +101 -0
  204. package/src/renderer/components/BuiltinToolsSettings.tsx +105 -0
  205. package/src/renderer/components/CanvasPreview.tsx +68 -1
  206. package/src/renderer/components/ConnectorEnvModal.tsx +116 -0
  207. package/src/renderer/components/ConnectorSetupModal.tsx +566 -0
  208. package/src/renderer/components/ConnectorsSettings.tsx +397 -0
  209. package/src/renderer/components/ControlPlaneSettings.tsx +2 -0
  210. package/src/renderer/components/DiscordSettings.tsx +18 -7
  211. package/src/renderer/components/DropboxSettings.tsx +202 -0
  212. package/src/renderer/components/EmailSettings.tsx +18 -7
  213. package/src/renderer/components/FileViewer.tsx +21 -13
  214. package/src/renderer/components/GoogleChatSettings.tsx +17 -7
  215. package/src/renderer/components/GoogleWorkspaceSettings.tsx +332 -0
  216. package/src/renderer/components/ImessageSettings.tsx +22 -11
  217. package/src/renderer/components/LineIcons.tsx +376 -0
  218. package/src/renderer/components/LineSettings.tsx +18 -7
  219. package/src/renderer/components/MCPSettings.tsx +56 -0
  220. package/src/renderer/components/MainContent.tsx +740 -76
  221. package/src/renderer/components/MatrixSettings.tsx +18 -7
  222. package/src/renderer/components/MattermostSettings.tsx +18 -7
  223. package/src/renderer/components/NodesSettings.tsx +58 -99
  224. package/src/renderer/components/NotificationPanel.tsx +25 -11
  225. package/src/renderer/components/NotionSettings.tsx +231 -0
  226. package/src/renderer/components/Onboarding/Onboarding.tsx +13 -1
  227. package/src/renderer/components/OnboardingModal.tsx +70 -1
  228. package/src/renderer/components/OneDriveSettings.tsx +212 -0
  229. package/src/renderer/components/RightPanel.tsx +141 -28
  230. package/src/renderer/components/ScheduledTasksSettings.tsx +10 -62
  231. package/src/renderer/components/SearchSettings.tsx +118 -114
  232. package/src/renderer/components/Settings.tsx +1425 -651
  233. package/src/renderer/components/SharePointSettings.tsx +224 -0
  234. package/src/renderer/components/Sidebar.tsx +94 -19
  235. package/src/renderer/components/SignalSettings.tsx +18 -7
  236. package/src/renderer/components/SkillHubBrowser.tsx +144 -185
  237. package/src/renderer/components/SlackSettings.tsx +18 -7
  238. package/src/renderer/components/TaskQuickActions.tsx +11 -6
  239. package/src/renderer/components/TaskTimeline.tsx +58 -26
  240. package/src/renderer/components/TeamsSettings.tsx +18 -7
  241. package/src/renderer/components/TelegramSettings.tsx +18 -7
  242. package/src/renderer/components/ThemeIcon.tsx +16 -0
  243. package/src/renderer/components/TwitchSettings.tsx +18 -7
  244. package/src/renderer/components/VoiceSettings.tsx +30 -74
  245. package/src/renderer/components/WhatsAppSettings.tsx +48 -37
  246. package/src/renderer/components/WorkingStateHistory.tsx +7 -5
  247. package/src/renderer/components/WorkspaceSelector.tsx +42 -13
  248. package/src/renderer/hooks/useOnboardingFlow.ts +21 -0
  249. package/src/renderer/styles/index.css +2333 -209
  250. package/src/shared/channelMessages.ts +367 -4
  251. package/src/shared/llm-provider-catalog.ts +217 -0
  252. package/src/shared/types.ts +251 -2
@@ -1,5 +1,6 @@
1
1
  import { useState, useEffect, useRef } from 'react';
2
- import { LLMSettingsData, ThemeMode, AccentColor } from '../../shared/types';
2
+ import { LLMSettingsData, ThemeMode, VisualTheme, AccentColor, type LLMProviderType, type CustomProviderConfig } from '../../shared/types';
3
+ import { CUSTOM_PROVIDER_MAP } from '../../shared/llm-provider-catalog';
3
4
  import { TelegramSettings } from './TelegramSettings';
4
5
  import { DiscordSettings } from './DiscordSettings';
5
6
  import { SlackSettings } from './SlackSettings';
@@ -15,6 +16,12 @@ import { EmailSettings } from './EmailSettings';
15
16
  import { TeamsSettings } from './TeamsSettings';
16
17
  import { GoogleChatSettings } from './GoogleChatSettings';
17
18
  import { XSettings } from './XSettings';
19
+ import { NotionSettings } from './NotionSettings';
20
+ import { BoxSettings } from './BoxSettings';
21
+ import { OneDriveSettings } from './OneDriveSettings';
22
+ import { GoogleWorkspaceSettings } from './GoogleWorkspaceSettings';
23
+ import { DropboxSettings } from './DropboxSettings';
24
+ import { SharePointSettings } from './SharePointSettings';
18
25
  import { SearchSettings } from './SearchSettings';
19
26
  import { UpdateSettings } from './UpdateSettings';
20
27
  import { GuardrailSettings } from './GuardrailSettings';
@@ -23,6 +30,7 @@ import { QueueSettings } from './QueueSettings';
23
30
  import { SkillsSettings } from './SkillsSettings';
24
31
  import { SkillHubBrowser } from './SkillHubBrowser';
25
32
  import { MCPSettings } from './MCPSettings';
33
+ import { ConnectorsSettings } from './ConnectorsSettings';
26
34
  import { BuiltinToolsSettings } from './BuiltinToolsSettings';
27
35
  import { TraySettings } from './TraySettings';
28
36
  import { ScheduledTasksSettings } from './ScheduledTasksSettings';
@@ -34,17 +42,22 @@ import { ExtensionsSettings } from './ExtensionsSettings';
34
42
  import { VoiceSettings } from './VoiceSettings';
35
43
  import { MissionControlPanel } from './MissionControlPanel';
36
44
 
37
- type SettingsTab = 'appearance' | 'personality' | 'missioncontrol' | 'tray' | 'voice' | 'llm' | 'search' | 'telegram' | 'slack' | 'whatsapp' | 'teams' | 'morechannels' | 'updates' | 'guardrails' | 'queue' | 'skills' | 'skillhub' | 'mcp' | 'tools' | 'scheduled' | 'hooks' | 'controlplane' | 'nodes' | 'extensions';
45
+ type SettingsTab = 'appearance' | 'personality' | 'missioncontrol' | 'tray' | 'voice' | 'llm' | 'search' | 'telegram' | 'slack' | 'whatsapp' | 'teams' | 'x' | 'morechannels' | 'integrations' | 'updates' | 'guardrails' | 'queue' | 'skills' | 'skillhub' | 'connectors' | 'mcp' | 'tools' | 'scheduled' | 'hooks' | 'controlplane' | 'nodes' | 'extensions';
38
46
 
39
47
  // Secondary channels shown inside "More Channels" tab
40
- type SecondaryChannel = 'discord' | 'imessage' | 'signal' | 'mattermost' | 'matrix' | 'twitch' | 'line' | 'bluebubbles' | 'email' | 'googlechat' | 'x';
48
+ type SecondaryChannel = 'discord' | 'imessage' | 'signal' | 'mattermost' | 'matrix' | 'twitch' | 'line' | 'bluebubbles' | 'email' | 'googlechat';
49
+
50
+ // App integrations shown inside "Integrations" tab
51
+ type IntegrationChannel = 'notion' | 'box' | 'onedrive' | 'googleworkspace' | 'dropbox' | 'sharepoint';
41
52
 
42
53
  interface SettingsProps {
43
54
  onBack: () => void;
44
55
  onSettingsChanged?: () => void;
45
56
  themeMode: ThemeMode;
57
+ visualTheme: VisualTheme;
46
58
  accentColor: AccentColor;
47
59
  onThemeChange: (theme: ThemeMode) => void;
60
+ onVisualThemeChange: (theme: VisualTheme) => void;
48
61
  onAccentChange: (accent: AccentColor) => void;
49
62
  initialTab?: SettingsTab;
50
63
  onShowOnboarding?: () => void;
@@ -246,12 +259,15 @@ const sidebarItems: Array<{ tab: SettingsTab; label: string; icon: React.ReactNo
246
259
  { tab: 'telegram', label: 'Telegram', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" /></svg> },
247
260
  { tab: 'slack', label: 'Slack', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M14.5 10c-.83 0-1.5-.67-1.5-1.5v-5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5z" /><path d="M20.5 10H19V8.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z" /><path d="M9.5 14c.83 0 1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5S8 21.33 8 20.5v-5c0-.83.67-1.5 1.5-1.5z" /><path d="M3.5 14H5v1.5c0 .83-.67 1.5-1.5 1.5S2 16.33 2 15.5 2.67 14 3.5 14z" /><path d="M14 14.5c0-.83.67-1.5 1.5-1.5h5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-5c-.83 0-1.5-.67-1.5-1.5z" /><path d="M15.5 19H14v1.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5-.67-1.5-1.5-1.5z" /><path d="M10 9.5C10 8.67 9.33 8 8.5 8h-5C2.67 8 2 8.67 2 9.5S2.67 11 3.5 11h5c.83 0 1.5-.67 1.5-1.5z" /><path d="M8.5 5H10V3.5C10 2.67 9.33 2 8.5 2S7 2.67 7 3.5 7.67 5 8.5 5z" /></svg> },
248
261
  { tab: 'teams', label: 'Teams', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M23 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" /></svg> },
262
+ { tab: 'x', label: 'X (Twitter)', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M5 4l14 16" /><path d="M19 4L5 20" /></svg> },
249
263
  { tab: 'morechannels', label: 'More Channels', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" /></svg> },
264
+ { tab: 'integrations', label: 'Integrations', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 2v4" /><path d="M12 18v4" /><path d="M4.93 4.93l2.83 2.83" /><path d="M16.24 16.24l2.83 2.83" /><path d="M2 12h4" /><path d="M18 12h4" /><path d="M4.93 19.07l2.83-2.83" /><path d="M16.24 7.76l2.83-2.83" /><circle cx="12" cy="12" r="3" /></svg> },
250
265
  { tab: 'guardrails', label: 'Guardrails', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" /></svg> },
251
266
  { tab: 'queue', label: 'Task Queue', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="4" width="18" height="4" rx="1" /><rect x="3" y="10" width="18" height="4" rx="1" /><rect x="3" y="16" width="18" height="4" rx="1" /></svg> },
252
267
  { tab: 'skills', label: 'Custom Skills', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" /></svg> },
253
268
  { tab: 'skillhub', label: 'SkillHub', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10" /><path d="M12 16v-4" /><path d="M12 8h.01" /><path d="M8 12h8" /></svg> },
254
269
  { tab: 'scheduled', label: 'Scheduled Tasks', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" /></svg> },
270
+ { tab: 'connectors', label: 'Connectors', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="4" width="7" height="7" rx="1" /><rect x="14" y="4" width="7" height="7" rx="1" /><rect x="3" y="13" width="7" height="7" rx="1" /><rect x="14" y="13" width="7" height="7" rx="1" /></svg> },
255
271
  { tab: 'mcp', label: 'MCP Servers', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="2" y="3" width="20" height="14" rx="2" /><path d="M8 21h8" /><path d="M12 17v4" /><path d="M7 8h2M15 8h2" /><path d="M9 12h6" /></svg> },
256
272
  { tab: 'tools', label: 'Built-in Tools', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" /></svg> },
257
273
  { tab: 'hooks', label: 'Webhooks', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></svg> },
@@ -273,12 +289,100 @@ const secondaryChannelItems: Array<{ key: SecondaryChannel; label: string; icon:
273
289
  { key: 'matrix', label: 'Matrix', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="2" y="2" width="20" height="20" rx="2" /><path d="M7 7h10M7 12h10M7 17h10" /></svg> },
274
290
  { key: 'twitch', label: 'Twitch', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 2H3v16h5v4l4-4h5l4-4V2zM11 11V7M16 11V7" /></svg> },
275
291
  { key: 'bluebubbles', label: 'BlueBubbles', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10" /><path d="M8 14s1.5 2 4 2 4-2 4-2" /><line x1="9" y1="9" x2="9.01" y2="9" /><line x1="15" y1="9" x2="15.01" y2="9" /></svg> },
276
- { key: 'x', label: 'X (Twitter)', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M5 4l14 16" /><path d="M19 4L5 20" /></svg> },
277
292
  ];
278
293
 
279
- export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, onThemeChange, onAccentChange, initialTab = 'appearance', onShowOnboarding, onboardingCompletedAt }: SettingsProps) {
294
+ // App integrations configuration for "Integrations" tab
295
+ const integrationItems: Array<{ key: IntegrationChannel; label: string; icon: React.ReactNode }> = [
296
+ { key: 'notion', label: 'Notion', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="4" y="3" width="16" height="18" rx="2" /><path d="M8 7h8M8 11h8M8 15h6" /></svg> },
297
+ { key: 'sharepoint', label: 'SharePoint', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="4" width="18" height="16" rx="2" /><path d="M7 8h10M7 12h6" /></svg> },
298
+ { key: 'onedrive', label: 'OneDrive', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M7 18h10a4 4 0 0 0 0-8 5 5 0 0 0-9.7-1.6A4 4 0 0 0 7 18z" /></svg> },
299
+ { key: 'googleworkspace', label: 'Google Workspace', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M4 7l4-4h8l4 4-8 14H8L4 7z" /></svg> },
300
+ { key: 'box', label: 'Box', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="3" width="18" height="18" rx="3" /><path d="M7 8h10M7 12h10M7 16h6" /></svg> },
301
+ { key: 'dropbox', label: 'Dropbox', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M7 5l5 3-5 3-5-3 5-3zm10 0l5 3-5 3-5-3 5-3zM7 13l5 3-5 3-5-3 5-3zm10 0l5 3-5 3-5-3 5-3z" /></svg> },
302
+ ];
303
+
304
+ const LLM_PROVIDER_ICONS: Record<string, JSX.Element> = {
305
+ anthropic: (
306
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
307
+ <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
308
+ </svg>
309
+ ),
310
+ openai: (
311
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
312
+ <circle cx="12" cy="12" r="10" />
313
+ <path d="M12 6v6l4 2" />
314
+ </svg>
315
+ ),
316
+ azure: (
317
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
318
+ <path d="M5 16a4 4 0 0 1 2-7.46A5 5 0 0 1 17 9h1a4 4 0 1 1 0 8H6a3 3 0 0 1-1-1z" />
319
+ </svg>
320
+ ),
321
+ gemini: (
322
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
323
+ <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
324
+ </svg>
325
+ ),
326
+ openrouter: (
327
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
328
+ <circle cx="12" cy="12" r="10" />
329
+ <path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
330
+ </svg>
331
+ ),
332
+ ollama: (
333
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
334
+ <rect x="4" y="4" width="16" height="16" rx="2" />
335
+ <path d="M9 9h6v6H9z" />
336
+ </svg>
337
+ ),
338
+ groq: (
339
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
340
+ <path d="M4 12h16" />
341
+ <path d="M12 4v16" />
342
+ <circle cx="12" cy="12" r="9" />
343
+ </svg>
344
+ ),
345
+ xai: (
346
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
347
+ <path d="M4 4l16 16" />
348
+ <path d="M20 4L4 20" />
349
+ </svg>
350
+ ),
351
+ kimi: (
352
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
353
+ <path d="M12 2l3 7 7 3-7 3-3 7-3-7-7-3 7-3 3-7z" />
354
+ </svg>
355
+ ),
356
+ bedrock: (
357
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
358
+ <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
359
+ </svg>
360
+ ),
361
+ };
362
+
363
+ const getLLMProviderIcon = (providerType: string, customEntry?: { compatibility?: string }) => {
364
+ if (LLM_PROVIDER_ICONS[providerType]) {
365
+ return LLM_PROVIDER_ICONS[providerType];
366
+ }
367
+ if (customEntry?.compatibility === 'anthropic') {
368
+ return LLM_PROVIDER_ICONS.anthropic;
369
+ }
370
+ if (customEntry?.compatibility === 'openai') {
371
+ return LLM_PROVIDER_ICONS.openai;
372
+ }
373
+ return (
374
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
375
+ <rect x="3" y="3" width="18" height="18" rx="2" />
376
+ <path d="M8 12h8" />
377
+ <path d="M12 8v8" />
378
+ </svg>
379
+ );
380
+ };
381
+
382
+ export function Settings({ onBack, onSettingsChanged, themeMode, visualTheme, accentColor, onThemeChange, onVisualThemeChange, onAccentChange, initialTab = 'appearance', onShowOnboarding, onboardingCompletedAt }: SettingsProps) {
280
383
  const [activeTab, setActiveTab] = useState<SettingsTab>(initialTab);
281
384
  const [activeSecondaryChannel, setActiveSecondaryChannel] = useState<SecondaryChannel>('discord');
385
+ const [activeIntegration, setActiveIntegration] = useState<IntegrationChannel>('notion');
282
386
  const [sidebarSearch, setSidebarSearch] = useState('');
283
387
  const [settings, setSettings] = useState<LLMSettingsData>({
284
388
  providerType: 'anthropic',
@@ -314,6 +418,7 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
314
418
 
315
419
  // OpenRouter state
316
420
  const [openrouterApiKey, setOpenrouterApiKey] = useState('');
421
+ const [openrouterBaseUrl, setOpenrouterBaseUrl] = useState('');
317
422
  const [openrouterModel, setOpenrouterModel] = useState('anthropic/claude-3.5-sonnet');
318
423
  const [openrouterModels, setOpenrouterModels] = useState<Array<{ id: string; name: string; context_length: number }>>([]);
319
424
  const [loadingOpenRouterModels, setLoadingOpenRouterModels] = useState(false);
@@ -327,6 +432,37 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
327
432
  const [openaiOAuthConnected, setOpenaiOAuthConnected] = useState(false);
328
433
  const [openaiOAuthLoading, setOpenaiOAuthLoading] = useState(false);
329
434
 
435
+ // Azure OpenAI state
436
+ const [azureApiKey, setAzureApiKey] = useState('');
437
+ const [azureEndpoint, setAzureEndpoint] = useState('');
438
+ const [azureDeployment, setAzureDeployment] = useState('');
439
+ const [azureDeploymentsText, setAzureDeploymentsText] = useState('');
440
+ const [azureApiVersion, setAzureApiVersion] = useState('2024-02-15-preview');
441
+
442
+ // Groq state
443
+ const [groqApiKey, setGroqApiKey] = useState('');
444
+ const [groqBaseUrl, setGroqBaseUrl] = useState('');
445
+ const [groqModel, setGroqModel] = useState('llama-3.1-8b-instant');
446
+ const [groqModels, setGroqModels] = useState<Array<{ id: string; name: string }>>([]);
447
+ const [loadingGroqModels, setLoadingGroqModels] = useState(false);
448
+
449
+ // xAI state
450
+ const [xaiApiKey, setXaiApiKey] = useState('');
451
+ const [xaiBaseUrl, setXaiBaseUrl] = useState('');
452
+ const [xaiModel, setXaiModel] = useState('grok-4-fast-non-reasoning');
453
+ const [xaiModels, setXaiModels] = useState<Array<{ id: string; name: string }>>([]);
454
+ const [loadingXaiModels, setLoadingXaiModels] = useState(false);
455
+
456
+ // Kimi state
457
+ const [kimiApiKey, setKimiApiKey] = useState('');
458
+ const [kimiBaseUrl, setKimiBaseUrl] = useState('');
459
+ const [kimiModel, setKimiModel] = useState('kimi-k2.5');
460
+ const [kimiModels, setKimiModels] = useState<Array<{ id: string; name: string }>>([]);
461
+ const [loadingKimiModels, setLoadingKimiModels] = useState(false);
462
+
463
+ // Custom provider state
464
+ const [customProviders, setCustomProviders] = useState<Record<string, CustomProviderConfig>>({});
465
+
330
466
  // Bedrock state
331
467
  const [bedrockModel, setBedrockModel] = useState('');
332
468
  const [bedrockModels, setBedrockModels] = useState<Array<{ id: string; name: string; description: string }>>([]);
@@ -336,6 +472,78 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
336
472
  loadConfigStatus();
337
473
  }, []);
338
474
 
475
+ const resolveCustomProviderId = (providerType: LLMProviderType) =>
476
+ providerType === 'kimi-coding' ? 'kimi-code' : providerType;
477
+
478
+ const updateCustomProvider = (providerType: LLMProviderType, updates: Partial<CustomProviderConfig>) => {
479
+ const resolvedType = resolveCustomProviderId(providerType);
480
+ setCustomProviders((prev) => ({
481
+ ...prev,
482
+ [resolvedType]: {
483
+ ...(prev[resolvedType] || {}),
484
+ ...updates,
485
+ },
486
+ }));
487
+ };
488
+
489
+ const sanitizeCustomProviders = (providers: Record<string, CustomProviderConfig>) => {
490
+ const sanitized: Record<string, CustomProviderConfig> = {};
491
+ Object.entries(providers).forEach(([key, value]) => {
492
+ const apiKey = value.apiKey?.trim();
493
+ const model = value.model?.trim();
494
+ const baseUrl = value.baseUrl?.trim();
495
+ if (apiKey || model || baseUrl) {
496
+ sanitized[key] = {
497
+ ...(apiKey ? { apiKey } : {}),
498
+ ...(model ? { model } : {}),
499
+ ...(baseUrl ? { baseUrl } : {}),
500
+ };
501
+ }
502
+ });
503
+ return Object.keys(sanitized).length > 0 ? sanitized : undefined;
504
+ };
505
+
506
+ const parseAzureDeployments = (value: string): string[] => {
507
+ const seen = new Set<string>();
508
+ return value
509
+ .split(/[\n,]+/)
510
+ .map((entry) => entry.trim())
511
+ .filter(Boolean)
512
+ .filter((entry) => {
513
+ if (seen.has(entry)) {
514
+ return false;
515
+ }
516
+ seen.add(entry);
517
+ return true;
518
+ });
519
+ };
520
+
521
+ const buildAzureSettings = () => {
522
+ const deployments = parseAzureDeployments(azureDeploymentsText);
523
+ let deployment = azureDeployment.trim();
524
+ if (deployment) {
525
+ if (!deployments.includes(deployment)) {
526
+ deployments.unshift(deployment);
527
+ }
528
+ } else if (deployments.length > 0) {
529
+ deployment = deployments[0];
530
+ }
531
+
532
+ return {
533
+ deployment: deployment || undefined,
534
+ deployments: deployments.length > 0 ? deployments : undefined,
535
+ };
536
+ };
537
+
538
+ useEffect(() => {
539
+ if (!azureDeployment) {
540
+ const deployments = parseAzureDeployments(azureDeploymentsText);
541
+ if (deployments[0]) {
542
+ setAzureDeployment(deployments[0]);
543
+ }
544
+ }
545
+ }, [azureDeploymentsText, azureDeployment]);
546
+
339
547
  const loadConfigStatus = async () => {
340
548
  try {
341
549
  setLoading(true);
@@ -349,6 +557,18 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
349
557
  // Load full settings separately for bedrock config
350
558
  const loadedSettings = await window.electronAPI.getLLMSettings();
351
559
  setSettings(loadedSettings);
560
+ if (loadedSettings.customProviders) {
561
+ const normalized = { ...loadedSettings.customProviders };
562
+ if (normalized['kimi-coding'] && !normalized['kimi-code']) {
563
+ normalized['kimi-code'] = normalized['kimi-coding'];
564
+ }
565
+ if (normalized['kimi-coding']) {
566
+ delete normalized['kimi-coding'];
567
+ }
568
+ setCustomProviders(normalized);
569
+ } else {
570
+ setCustomProviders({});
571
+ }
352
572
 
353
573
  // Set form state from loaded settings
354
574
  if (loadedSettings.bedrock?.region) {
@@ -387,6 +607,9 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
387
607
  if (loadedSettings.openrouter?.apiKey) {
388
608
  setOpenrouterApiKey(loadedSettings.openrouter.apiKey);
389
609
  }
610
+ if (loadedSettings.openrouter?.baseUrl) {
611
+ setOpenrouterBaseUrl(loadedSettings.openrouter.baseUrl);
612
+ }
390
613
  if (loadedSettings.openrouter?.model) {
391
614
  setOpenrouterModel(loadedSettings.openrouter.model);
392
615
  }
@@ -419,6 +642,62 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
419
642
  setOpenaiAuthMethod('oauth');
420
643
  }
421
644
 
645
+ // Set Azure OpenAI form state
646
+ if (loadedSettings.azure?.apiKey) {
647
+ setAzureApiKey(loadedSettings.azure.apiKey);
648
+ }
649
+ if (loadedSettings.azure?.endpoint) {
650
+ setAzureEndpoint(loadedSettings.azure.endpoint);
651
+ }
652
+ {
653
+ const loadedDeployments = (loadedSettings.azure?.deployments && loadedSettings.azure.deployments.length > 0)
654
+ ? loadedSettings.azure.deployments
655
+ : (loadedSettings.azure?.deployment ? [loadedSettings.azure.deployment] : []);
656
+ if (loadedDeployments.length > 0) {
657
+ setAzureDeploymentsText(loadedDeployments.join('\n'));
658
+ }
659
+ const selectedDeployment = loadedSettings.azure?.deployment || loadedDeployments[0];
660
+ if (selectedDeployment) {
661
+ setAzureDeployment(selectedDeployment);
662
+ }
663
+ }
664
+ if (loadedSettings.azure?.apiVersion) {
665
+ setAzureApiVersion(loadedSettings.azure.apiVersion);
666
+ }
667
+
668
+ // Set Groq form state
669
+ if (loadedSettings.groq?.apiKey) {
670
+ setGroqApiKey(loadedSettings.groq.apiKey);
671
+ }
672
+ if (loadedSettings.groq?.baseUrl) {
673
+ setGroqBaseUrl(loadedSettings.groq.baseUrl);
674
+ }
675
+ if (loadedSettings.groq?.model) {
676
+ setGroqModel(loadedSettings.groq.model);
677
+ }
678
+
679
+ // Set xAI form state
680
+ if (loadedSettings.xai?.apiKey) {
681
+ setXaiApiKey(loadedSettings.xai.apiKey);
682
+ }
683
+ if (loadedSettings.xai?.baseUrl) {
684
+ setXaiBaseUrl(loadedSettings.xai.baseUrl);
685
+ }
686
+ if (loadedSettings.xai?.model) {
687
+ setXaiModel(loadedSettings.xai.model);
688
+ }
689
+
690
+ // Set Kimi form state
691
+ if (loadedSettings.kimi?.apiKey) {
692
+ setKimiApiKey(loadedSettings.kimi.apiKey);
693
+ }
694
+ if (loadedSettings.kimi?.baseUrl) {
695
+ setKimiBaseUrl(loadedSettings.kimi.baseUrl);
696
+ }
697
+ if (loadedSettings.kimi?.model) {
698
+ setKimiModel(loadedSettings.kimi.model);
699
+ }
700
+
422
701
  // Set Bedrock form state (access key and secret key are set earlier)
423
702
  if (loadedSettings.bedrock?.accessKeyId) {
424
703
  setAwsAccessKeyId(loadedSettings.bedrock.accessKeyId);
@@ -514,7 +793,7 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
514
793
  const loadOpenRouterModels = async (apiKey?: string) => {
515
794
  try {
516
795
  setLoadingOpenRouterModels(true);
517
- const models = await window.electronAPI.getOpenRouterModels(apiKey || openrouterApiKey);
796
+ const models = await window.electronAPI.getOpenRouterModels(apiKey || openrouterApiKey, openrouterBaseUrl || undefined);
518
797
  setOpenrouterModels(models || []);
519
798
  // If we got models and current model isn't in the list, select the first one
520
799
  if (models && models.length > 0 && !models.some(m => m.id === openrouterModel)) {
@@ -549,6 +828,93 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
549
828
  }
550
829
  };
551
830
 
831
+ const loadGroqModels = async (apiKey?: string) => {
832
+ try {
833
+ setLoadingGroqModels(true);
834
+ const models = await window.electronAPI.getGroqModels(apiKey || groqApiKey, groqBaseUrl || undefined);
835
+ setGroqModels(models || []);
836
+ if (models && models.length > 0 && !models.some(m => m.id === groqModel)) {
837
+ setGroqModel(models[0].id);
838
+ }
839
+ onSettingsChanged?.();
840
+ } catch (error) {
841
+ console.error('Failed to load Groq models:', error);
842
+ setGroqModels([]);
843
+ } finally {
844
+ setLoadingGroqModels(false);
845
+ }
846
+ };
847
+
848
+ const loadXAIModels = async (apiKey?: string) => {
849
+ try {
850
+ setLoadingXaiModels(true);
851
+ const models = await window.electronAPI.getXAIModels(apiKey || xaiApiKey, xaiBaseUrl || undefined);
852
+ setXaiModels(models || []);
853
+ if (models && models.length > 0 && !models.some(m => m.id === xaiModel)) {
854
+ setXaiModel(models[0].id);
855
+ }
856
+ onSettingsChanged?.();
857
+ } catch (error) {
858
+ console.error('Failed to load xAI models:', error);
859
+ setXaiModels([]);
860
+ } finally {
861
+ setLoadingXaiModels(false);
862
+ }
863
+ };
864
+
865
+ const loadKimiModels = async (apiKey?: string) => {
866
+ try {
867
+ setLoadingKimiModels(true);
868
+ const models = await window.electronAPI.getKimiModels(apiKey || kimiApiKey, kimiBaseUrl || undefined);
869
+ setKimiModels(models || []);
870
+ if (models && models.length > 0 && !models.some(m => m.id === kimiModel)) {
871
+ setKimiModel(models[0].id);
872
+ }
873
+ onSettingsChanged?.();
874
+ } catch (error) {
875
+ console.error('Failed to load Kimi models:', error);
876
+ setKimiModels([]);
877
+ } finally {
878
+ setLoadingKimiModels(false);
879
+ }
880
+ };
881
+
882
+ const handleProviderSelect = (providerType: LLMProviderType) => {
883
+ setSettings((prev) => ({ ...prev, providerType }));
884
+
885
+ const resolvedCustomType = resolveCustomProviderId(providerType);
886
+ const customEntry = CUSTOM_PROVIDER_MAP.get(resolvedCustomType);
887
+ if (customEntry) {
888
+ setCustomProviders((prev) => {
889
+ const existing = prev[resolvedCustomType] || {};
890
+ const updated: CustomProviderConfig = { ...existing };
891
+ if (!updated.model && customEntry.defaultModel) {
892
+ updated.model = customEntry.defaultModel;
893
+ }
894
+ if (!updated.baseUrl && customEntry.baseUrl) {
895
+ updated.baseUrl = customEntry.baseUrl;
896
+ }
897
+ return { ...prev, [resolvedCustomType]: updated };
898
+ });
899
+ }
900
+
901
+ if (providerType === 'ollama') {
902
+ loadOllamaModels();
903
+ } else if (providerType === 'gemini') {
904
+ loadGeminiModels();
905
+ } else if (providerType === 'openrouter') {
906
+ loadOpenRouterModels();
907
+ } else if (providerType === 'openai') {
908
+ loadOpenAIModels();
909
+ } else if (providerType === 'groq') {
910
+ loadGroqModels();
911
+ } else if (providerType === 'xai') {
912
+ loadXAIModels();
913
+ } else if (providerType === 'kimi') {
914
+ loadKimiModels();
915
+ }
916
+ };
917
+
552
918
  const handleOpenAIOAuthLogin = async () => {
553
919
  try {
554
920
  setOpenaiOAuthLoading(true);
@@ -613,6 +979,22 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
613
979
  setSaving(true);
614
980
  setTestResult(null);
615
981
 
982
+ const sanitizedCustomProviders = sanitizeCustomProviders(customProviders) || {};
983
+ const resolvedProviderTypeForSave = resolveCustomProviderId(settings.providerType as LLMProviderType);
984
+ const selectedCustomEntry = CUSTOM_PROVIDER_MAP.get(resolvedProviderTypeForSave);
985
+ if (selectedCustomEntry) {
986
+ const existing = sanitizedCustomProviders[resolvedProviderTypeForSave] || {};
987
+ const withDefaults: CustomProviderConfig = { ...existing };
988
+ if (!withDefaults.model && selectedCustomEntry.defaultModel) {
989
+ withDefaults.model = selectedCustomEntry.defaultModel;
990
+ }
991
+ if (!withDefaults.baseUrl && selectedCustomEntry.baseUrl) {
992
+ withDefaults.baseUrl = selectedCustomEntry.baseUrl;
993
+ }
994
+ sanitizedCustomProviders[resolvedProviderTypeForSave] = withDefaults;
995
+ }
996
+ const azureSettings = buildAzureSettings();
997
+
616
998
  // Always save settings for ALL providers to preserve API keys and model selections
617
999
  // when switching between providers
618
1000
  const settingsToSave: LLMSettingsData = {
@@ -648,6 +1030,7 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
648
1030
  openrouter: {
649
1031
  apiKey: openrouterApiKey || undefined,
650
1032
  model: openrouterModel || undefined,
1033
+ baseUrl: openrouterBaseUrl || undefined,
651
1034
  },
652
1035
  // Always include openai settings
653
1036
  openai: {
@@ -655,6 +1038,33 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
655
1038
  model: openaiModel || undefined,
656
1039
  authMethod: openaiAuthMethod,
657
1040
  },
1041
+ // Always include Azure OpenAI settings
1042
+ azure: {
1043
+ apiKey: azureApiKey || undefined,
1044
+ endpoint: azureEndpoint || undefined,
1045
+ deployment: azureSettings.deployment,
1046
+ deployments: azureSettings.deployments,
1047
+ apiVersion: azureApiVersion || undefined,
1048
+ },
1049
+ // Always include Groq settings
1050
+ groq: {
1051
+ apiKey: groqApiKey || undefined,
1052
+ model: groqModel || undefined,
1053
+ baseUrl: groqBaseUrl || undefined,
1054
+ },
1055
+ // Always include xAI settings
1056
+ xai: {
1057
+ apiKey: xaiApiKey || undefined,
1058
+ model: xaiModel || undefined,
1059
+ baseUrl: xaiBaseUrl || undefined,
1060
+ },
1061
+ // Always include Kimi settings
1062
+ kimi: {
1063
+ apiKey: kimiApiKey || undefined,
1064
+ model: kimiModel || undefined,
1065
+ baseUrl: kimiBaseUrl || undefined,
1066
+ },
1067
+ customProviders: Object.keys(sanitizedCustomProviders).length > 0 ? sanitizedCustomProviders : undefined,
658
1068
  };
659
1069
 
660
1070
  await window.electronAPI.saveLLMSettings(settingsToSave);
@@ -672,6 +1082,9 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
672
1082
  setTesting(true);
673
1083
  setTestResult(null);
674
1084
 
1085
+ const sanitizedCustomProviders = sanitizeCustomProviders(customProviders) || {};
1086
+ const azureSettings = buildAzureSettings();
1087
+
675
1088
  const testConfig = {
676
1089
  providerType: settings.providerType,
677
1090
  modelKey: settings.modelKey,
@@ -699,6 +1112,7 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
699
1112
  openrouter: settings.providerType === 'openrouter' ? {
700
1113
  apiKey: openrouterApiKey || undefined,
701
1114
  model: openrouterModel || undefined,
1115
+ baseUrl: openrouterBaseUrl || undefined,
702
1116
  } : undefined,
703
1117
  openai: settings.providerType === 'openai' ? {
704
1118
  apiKey: openaiAuthMethod === 'api_key' ? (openaiApiKey || undefined) : undefined,
@@ -706,6 +1120,29 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
706
1120
  authMethod: openaiAuthMethod,
707
1121
  // OAuth tokens are handled by the backend from stored settings
708
1122
  } : undefined,
1123
+ azure: settings.providerType === 'azure' ? {
1124
+ apiKey: azureApiKey || undefined,
1125
+ endpoint: azureEndpoint || undefined,
1126
+ deployment: azureSettings.deployment,
1127
+ deployments: azureSettings.deployments,
1128
+ apiVersion: azureApiVersion || undefined,
1129
+ } : undefined,
1130
+ groq: settings.providerType === 'groq' ? {
1131
+ apiKey: groqApiKey || undefined,
1132
+ model: groqModel || undefined,
1133
+ baseUrl: groqBaseUrl || undefined,
1134
+ } : undefined,
1135
+ xai: settings.providerType === 'xai' ? {
1136
+ apiKey: xaiApiKey || undefined,
1137
+ model: xaiModel || undefined,
1138
+ baseUrl: xaiBaseUrl || undefined,
1139
+ } : undefined,
1140
+ kimi: settings.providerType === 'kimi' ? {
1141
+ apiKey: kimiApiKey || undefined,
1142
+ model: kimiModel || undefined,
1143
+ baseUrl: kimiBaseUrl || undefined,
1144
+ } : undefined,
1145
+ customProviders: Object.keys(sanitizedCustomProviders).length > 0 ? sanitizedCustomProviders : undefined,
709
1146
  };
710
1147
 
711
1148
  const result = await window.electronAPI.testLLMProvider(testConfig);
@@ -717,21 +1154,21 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
717
1154
  }
718
1155
  };
719
1156
 
1157
+ const resolvedProviderType = resolveCustomProviderId(settings.providerType as LLMProviderType);
1158
+ const selectedCustomProvider = CUSTOM_PROVIDER_MAP.get(resolvedProviderType);
1159
+ const selectedCustomConfig = selectedCustomProvider ? (customProviders[resolvedProviderType] || {}) : {};
1160
+
720
1161
  return (
721
1162
  <div className="settings-page">
722
- <div className="settings-page-header">
723
- <h1>Settings</h1>
724
- </div>
725
-
726
1163
  <div className="settings-page-layout">
727
1164
  <div className="settings-sidebar">
1165
+ <h1 className="settings-sidebar-title">Settings</h1>
728
1166
  <button className="settings-back-btn" onClick={onBack}>
729
1167
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
730
1168
  <path d="M19 12H5M12 19l-7-7 7-7" />
731
1169
  </svg>
732
1170
  Back
733
1171
  </button>
734
- <div className="settings-nav-divider" />
735
1172
  <div className="settings-sidebar-search">
736
1173
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
737
1174
  <circle cx="11" cy="11" r="8" />
@@ -783,720 +1220,1057 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
783
1220
  if (item.macOnly && !navigator.platform.toLowerCase().includes('mac')) return false;
784
1221
  return item.label.toLowerCase().includes(sidebarSearch.toLowerCase());
785
1222
  }).length === 0 && (
786
- <div className="settings-nav-no-results">No matching settings</div>
787
- )}
1223
+ <div className="settings-nav-no-results">No matching settings</div>
1224
+ )}
788
1225
  </div>
789
1226
  </div>
790
1227
 
791
- <div className="settings-content">
792
- {activeTab === 'appearance' ? (
793
- <AppearanceSettings
794
- themeMode={themeMode}
795
- accentColor={accentColor}
796
- onThemeChange={onThemeChange}
797
- onAccentChange={onAccentChange}
798
- onShowOnboarding={onShowOnboarding}
799
- onboardingCompletedAt={onboardingCompletedAt}
800
- />
801
- ) : activeTab === 'personality' ? (
802
- <PersonalitySettings onSettingsChanged={onSettingsChanged} />
803
- ) : activeTab === 'missioncontrol' ? (
804
- <MissionControlPanel />
805
- ) : activeTab === 'tray' ? (
806
- <TraySettings />
807
- ) : activeTab === 'voice' ? (
808
- <VoiceSettings />
809
- ) : activeTab === 'telegram' ? (
810
- <TelegramSettings />
811
- ) : activeTab === 'slack' ? (
812
- <SlackSettings />
813
- ) : activeTab === 'whatsapp' ? (
814
- <WhatsAppSettings />
815
- ) : activeTab === 'teams' ? (
816
- <TeamsSettings />
817
- ) : activeTab === 'morechannels' ? (
818
- <div className="more-channels-panel">
819
- <div className="more-channels-header">
820
- <h2>More Channels</h2>
821
- <p className="settings-description">Configure additional messaging platforms and integrations</p>
822
- </div>
823
- <div className="more-channels-tabs">
824
- {secondaryChannelItems.map(item => (
825
- <button
826
- key={item.key}
827
- className={`more-channels-tab ${activeSecondaryChannel === item.key ? 'active' : ''}`}
828
- onClick={() => setActiveSecondaryChannel(item.key)}
829
- >
830
- {item.icon}
831
- <span>{item.label}</span>
832
- </button>
833
- ))}
1228
+ <div className="settings-content-card">
1229
+ <div className="settings-content">
1230
+ {activeTab === 'appearance' ? (
1231
+ <AppearanceSettings
1232
+ themeMode={themeMode}
1233
+ visualTheme={visualTheme}
1234
+ accentColor={accentColor}
1235
+ onThemeChange={onThemeChange}
1236
+ onVisualThemeChange={onVisualThemeChange}
1237
+ onAccentChange={onAccentChange}
1238
+ onShowOnboarding={onShowOnboarding}
1239
+ onboardingCompletedAt={onboardingCompletedAt}
1240
+ />
1241
+ ) : activeTab === 'personality' ? (
1242
+ <PersonalitySettings onSettingsChanged={onSettingsChanged} />
1243
+ ) : activeTab === 'missioncontrol' ? (
1244
+ <MissionControlPanel />
1245
+ ) : activeTab === 'tray' ? (
1246
+ <TraySettings />
1247
+ ) : activeTab === 'voice' ? (
1248
+ <VoiceSettings />
1249
+ ) : activeTab === 'telegram' ? (
1250
+ <TelegramSettings />
1251
+ ) : activeTab === 'slack' ? (
1252
+ <SlackSettings />
1253
+ ) : activeTab === 'whatsapp' ? (
1254
+ <WhatsAppSettings />
1255
+ ) : activeTab === 'teams' ? (
1256
+ <TeamsSettings />
1257
+ ) : activeTab === 'x' ? (
1258
+ <XSettings />
1259
+ ) : activeTab === 'morechannels' ? (
1260
+ <div className="more-channels-panel">
1261
+ <div className="more-channels-header">
1262
+ <h2>More Channels</h2>
1263
+ <p className="settings-description">Configure additional messaging platforms</p>
1264
+ </div>
1265
+ <div className="more-channels-tabs">
1266
+ {secondaryChannelItems.map(item => (
1267
+ <button
1268
+ key={item.key}
1269
+ className={`more-channels-tab ${activeSecondaryChannel === item.key ? 'active' : ''}`}
1270
+ onClick={() => setActiveSecondaryChannel(item.key)}
1271
+ >
1272
+ {item.icon}
1273
+ <span>{item.label}</span>
1274
+ </button>
1275
+ ))}
1276
+ </div>
1277
+ <div className="more-channels-content">
1278
+ {activeSecondaryChannel === 'discord' && <DiscordSettings />}
1279
+ {activeSecondaryChannel === 'imessage' && <ImessageSettings />}
1280
+ {activeSecondaryChannel === 'signal' && <SignalSettings />}
1281
+ {activeSecondaryChannel === 'mattermost' && <MattermostSettings />}
1282
+ {activeSecondaryChannel === 'matrix' && <MatrixSettings />}
1283
+ {activeSecondaryChannel === 'twitch' && <TwitchSettings />}
1284
+ {activeSecondaryChannel === 'line' && <LineSettings />}
1285
+ {activeSecondaryChannel === 'bluebubbles' && <BlueBubblesSettings />}
1286
+ {activeSecondaryChannel === 'email' && <EmailSettings />}
1287
+ {activeSecondaryChannel === 'googlechat' && <GoogleChatSettings />}
1288
+ </div>
834
1289
  </div>
835
- <div className="more-channels-content">
836
- {activeSecondaryChannel === 'discord' && <DiscordSettings />}
837
- {activeSecondaryChannel === 'imessage' && <ImessageSettings />}
838
- {activeSecondaryChannel === 'signal' && <SignalSettings />}
839
- {activeSecondaryChannel === 'mattermost' && <MattermostSettings />}
840
- {activeSecondaryChannel === 'matrix' && <MatrixSettings />}
841
- {activeSecondaryChannel === 'twitch' && <TwitchSettings />}
842
- {activeSecondaryChannel === 'line' && <LineSettings />}
843
- {activeSecondaryChannel === 'bluebubbles' && <BlueBubblesSettings />}
844
- {activeSecondaryChannel === 'email' && <EmailSettings />}
845
- {activeSecondaryChannel === 'googlechat' && <GoogleChatSettings />}
846
- {activeSecondaryChannel === 'x' && <XSettings />}
1290
+ ) : activeTab === 'integrations' ? (
1291
+ <div className="integrations-panel">
1292
+ <div className="integrations-header">
1293
+ <h2>Integrations</h2>
1294
+ <p className="settings-description">Connect productivity and storage tools for the agent</p>
1295
+ </div>
1296
+ <div className="integrations-tabs">
1297
+ {integrationItems.map(item => (
1298
+ <button
1299
+ key={item.key}
1300
+ className={`integrations-tab ${activeIntegration === item.key ? 'active' : ''}`}
1301
+ onClick={() => setActiveIntegration(item.key)}
1302
+ >
1303
+ {item.icon}
1304
+ <span>{item.label}</span>
1305
+ </button>
1306
+ ))}
1307
+ </div>
1308
+ <div className="integrations-content">
1309
+ {activeIntegration === 'notion' && <NotionSettings />}
1310
+ {activeIntegration === 'box' && <BoxSettings />}
1311
+ {activeIntegration === 'onedrive' && <OneDriveSettings />}
1312
+ {activeIntegration === 'googleworkspace' && <GoogleWorkspaceSettings />}
1313
+ {activeIntegration === 'dropbox' && <DropboxSettings />}
1314
+ {activeIntegration === 'sharepoint' && <SharePointSettings />}
1315
+ </div>
847
1316
  </div>
848
- </div>
849
- ) : activeTab === 'search' ? (
850
- <SearchSettings />
851
- ) : activeTab === 'updates' ? (
852
- <UpdateSettings />
853
- ) : activeTab === 'guardrails' ? (
854
- <GuardrailSettings />
855
- ) : activeTab === 'queue' ? (
856
- <QueueSettings />
857
- ) : activeTab === 'skills' ? (
858
- <SkillsSettings />
859
- ) : activeTab === 'skillhub' ? (
860
- <SkillHubBrowser />
861
- ) : activeTab === 'scheduled' ? (
862
- <ScheduledTasksSettings />
863
- ) : activeTab === 'mcp' ? (
864
- <MCPSettings />
865
- ) : activeTab === 'tools' ? (
866
- <BuiltinToolsSettings />
867
- ) : activeTab === 'hooks' ? (
868
- <HooksSettings />
869
- ) : activeTab === 'controlplane' ? (
870
- <ControlPlaneSettings />
871
- ) : activeTab === 'nodes' ? (
872
- <NodesSettings />
873
- ) : activeTab === 'extensions' ? (
874
- <ExtensionsSettings />
875
- ) : loading ? (
876
- <div className="settings-loading">Loading settings...</div>
877
- ) : (
878
- <>
879
- <div className="settings-section">
880
- <h3>LLM Provider</h3>
881
- <p className="settings-description">
882
- Choose which service to use for AI model calls
883
- </p>
884
-
885
- <div className="provider-options">
1317
+ ) : activeTab === 'search' ? (
1318
+ <SearchSettings />
1319
+ ) : activeTab === 'updates' ? (
1320
+ <UpdateSettings />
1321
+ ) : activeTab === 'guardrails' ? (
1322
+ <GuardrailSettings />
1323
+ ) : activeTab === 'queue' ? (
1324
+ <QueueSettings />
1325
+ ) : activeTab === 'skills' ? (
1326
+ <SkillsSettings />
1327
+ ) : activeTab === 'skillhub' ? (
1328
+ <SkillHubBrowser />
1329
+ ) : activeTab === 'scheduled' ? (
1330
+ <ScheduledTasksSettings />
1331
+ ) : activeTab === 'connectors' ? (
1332
+ <ConnectorsSettings />
1333
+ ) : activeTab === 'mcp' ? (
1334
+ <MCPSettings />
1335
+ ) : activeTab === 'tools' ? (
1336
+ <BuiltinToolsSettings />
1337
+ ) : activeTab === 'hooks' ? (
1338
+ <HooksSettings />
1339
+ ) : activeTab === 'controlplane' ? (
1340
+ <ControlPlaneSettings />
1341
+ ) : activeTab === 'nodes' ? (
1342
+ <NodesSettings />
1343
+ ) : activeTab === 'extensions' ? (
1344
+ <ExtensionsSettings />
1345
+ ) : loading ? (
1346
+ <div className="settings-loading">Loading settings...</div>
1347
+ ) : (
1348
+ <div className="llm-provider-panel">
1349
+ <div className="llm-provider-header">
1350
+ <h2>LLM Provider</h2>
1351
+ <p className="settings-description">
1352
+ Choose which service to use for AI model calls
1353
+ </p>
1354
+ </div>
1355
+ <div className="llm-provider-tabs">
886
1356
  {providers.map(provider => {
887
- const isAnthropic = provider.type === 'anthropic';
888
- const isBedrock = provider.type === 'bedrock';
889
- const isOllama = provider.type === 'ollama';
890
- const isGemini = provider.type === 'gemini';
891
- const isOpenRouter = provider.type === 'openrouter';
892
- const isOpenAI = provider.type === 'openai';
1357
+ const providerType = provider.type as LLMProviderType;
1358
+ const resolvedCustomType = resolveCustomProviderId(providerType);
1359
+ const customEntry = CUSTOM_PROVIDER_MAP.get(resolvedCustomType);
1360
+ const icon = getLLMProviderIcon(providerType, customEntry);
893
1361
 
894
1362
  return (
895
- <label
1363
+ <button
896
1364
  key={provider.type}
897
- className={`provider-option ${settings.providerType === provider.type ? 'selected' : ''}`}
1365
+ type="button"
1366
+ className={`llm-provider-tab ${settings.providerType === provider.type ? 'active' : ''} ${provider.configured ? 'configured' : ''}`}
1367
+ onClick={() => handleProviderSelect(providerType)}
898
1368
  >
899
- <input
900
- type="radio"
901
- name="provider"
902
- value={provider.type}
903
- checked={settings.providerType === provider.type}
904
- onChange={() => {
905
- setSettings({ ...settings, providerType: provider.type as 'anthropic' | 'bedrock' | 'ollama' | 'gemini' | 'openrouter' | 'openai' });
906
- // Load models when selecting provider
907
- if (provider.type === 'ollama') {
908
- loadOllamaModels();
909
- } else if (provider.type === 'gemini') {
910
- loadGeminiModels();
911
- } else if (provider.type === 'openrouter') {
912
- loadOpenRouterModels();
913
- } else if (provider.type === 'openai') {
914
- loadOpenAIModels();
915
- }
916
- }}
917
- />
918
- <div className="provider-option-content">
919
- <div className="provider-option-title">
920
- {provider.name}
921
- {provider.configured && (
922
- <span className="provider-configured" title="Credentials detected">
923
- [Configured]
924
- </span>
925
- )}
926
- </div>
927
- <div className="provider-option-description">
928
- {isAnthropic && provider.configured && (
929
- <>API key configured</>
930
- )}
931
- {isAnthropic && !provider.configured && (
932
- <>Enter your Anthropic API key below</>
933
- )}
934
- {isGemini && provider.configured && (
935
- <>API key configured</>
936
- )}
937
- {isGemini && !provider.configured && (
938
- <>Enter your Gemini API key below</>
939
- )}
940
- {isOpenRouter && provider.configured && (
941
- <>API key configured</>
942
- )}
943
- {isOpenRouter && !provider.configured && (
944
- <>Enter your OpenRouter API key below</>
945
- )}
946
- {isOpenAI && provider.configured && openaiOAuthConnected && (
947
- <>Connected via ChatGPT account</>
948
- )}
949
- {isOpenAI && provider.configured && !openaiOAuthConnected && (
950
- <>API key configured</>
951
- )}
952
- {isOpenAI && !provider.configured && (
953
- <>Sign in with ChatGPT or enter API key</>
954
- )}
955
- {isBedrock && provider.configured && (
956
- <>AWS credentials configured</>
957
- )}
958
- {isBedrock && !provider.configured && (
959
- <>Configure your AWS credentials below</>
960
- )}
961
- {isOllama && provider.configured && (
962
- <>Ollama server detected - configure model below</>
963
- )}
964
- {isOllama && !provider.configured && (
965
- <>Run local LLM models with Ollama</>
966
- )}
967
- </div>
968
- </div>
969
- </label>
1369
+ {icon}
1370
+ <span className="llm-provider-tab-label">{provider.name}</span>
1371
+ {provider.configured && (
1372
+ <span className="llm-provider-tab-status" title="Configured" />
1373
+ )}
1374
+ </button>
970
1375
  );
971
1376
  })}
972
1377
  </div>
973
- </div>
974
-
975
- {settings.providerType === 'anthropic' && (
976
- <div className="settings-section">
977
- <h3>Model</h3>
978
- <select
979
- className="settings-select"
980
- value={settings.modelKey}
981
- onChange={(e) => setSettings({ ...settings, modelKey: e.target.value })}
982
- >
983
- {models.map(model => (
984
- <option key={model.key} value={model.key}>
985
- {model.displayName}
986
- </option>
987
- ))}
988
- </select>
989
- </div>
990
- )}
991
-
992
- {settings.providerType === 'anthropic' && (
993
- <div className="settings-section">
994
- <h3>Anthropic API Key</h3>
995
- <p className="settings-description">
996
- Enter your API key from{' '}
997
- <a href="https://console.anthropic.com/" target="_blank" rel="noopener noreferrer">
998
- console.anthropic.com
999
- </a>
1000
- </p>
1001
- <input
1002
- type="password"
1003
- className="settings-input"
1004
- placeholder="sk-ant-..."
1005
- value={anthropicApiKey}
1006
- onChange={(e) => setAnthropicApiKey(e.target.value)}
1007
- />
1008
- </div>
1009
- )}
1010
-
1011
- {settings.providerType === 'gemini' && (
1012
- <>
1013
- <div className="settings-section">
1014
- <h3>Gemini API Key</h3>
1015
- <p className="settings-description">
1016
- Enter your API key from{' '}
1017
- <a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer">
1018
- Google AI Studio
1019
- </a>
1020
- </p>
1021
- <div className="settings-input-group">
1022
- <input
1023
- type="password"
1024
- className="settings-input"
1025
- placeholder="AIza..."
1026
- value={geminiApiKey}
1027
- onChange={(e) => setGeminiApiKey(e.target.value)}
1028
- />
1029
- <button
1030
- className="button-small button-secondary"
1031
- onClick={() => loadGeminiModels(geminiApiKey)}
1032
- disabled={loadingGeminiModels}
1378
+ <div className="llm-provider-content">
1379
+ {settings.providerType === 'anthropic' && (
1380
+ <div className="settings-section">
1381
+ <h3>Model</h3>
1382
+ <select
1383
+ className="settings-select"
1384
+ value={settings.modelKey}
1385
+ onChange={(e) => setSettings({ ...settings, modelKey: e.target.value })}
1033
1386
  >
1034
- {loadingGeminiModels ? 'Loading...' : 'Refresh Models'}
1035
- </button>
1387
+ {models.map(model => (
1388
+ <option key={model.key} value={model.key}>
1389
+ {model.displayName}
1390
+ </option>
1391
+ ))}
1392
+ </select>
1036
1393
  </div>
1037
- </div>
1038
-
1039
- <div className="settings-section">
1040
- <h3>Model</h3>
1041
- <p className="settings-description">
1042
- Select a Gemini model. Enter your API key and click "Refresh Models" to load available models.
1043
- </p>
1044
- {geminiModels.length > 0 ? (
1045
- <SearchableSelect
1046
- options={geminiModels.map(model => ({
1047
- value: model.name,
1048
- label: model.displayName,
1049
- description: model.description,
1050
- }))}
1051
- value={geminiModel}
1052
- onChange={setGeminiModel}
1053
- placeholder="Select a model..."
1054
- />
1055
- ) : (
1056
- <input
1057
- type="text"
1058
- className="settings-input"
1059
- placeholder="gemini-2.0-flash"
1060
- value={geminiModel}
1061
- onChange={(e) => setGeminiModel(e.target.value)}
1062
- />
1063
- )}
1064
- </div>
1065
- </>
1066
- )}
1394
+ )}
1067
1395
 
1068
- {settings.providerType === 'openrouter' && (
1069
- <>
1070
- <div className="settings-section">
1071
- <h3>OpenRouter API Key</h3>
1072
- <p className="settings-description">
1073
- Enter your API key from{' '}
1074
- <a href="https://openrouter.ai/keys" target="_blank" rel="noopener noreferrer">
1075
- OpenRouter
1076
- </a>
1077
- </p>
1078
- <div className="settings-input-group">
1396
+ {settings.providerType === 'anthropic' && (
1397
+ <div className="settings-section">
1398
+ <h3>Anthropic API Key</h3>
1399
+ <p className="settings-description">
1400
+ Enter your API key from{' '}
1401
+ <a href="https://console.anthropic.com/" target="_blank" rel="noopener noreferrer">
1402
+ console.anthropic.com
1403
+ </a>
1404
+ </p>
1079
1405
  <input
1080
1406
  type="password"
1081
1407
  className="settings-input"
1082
- placeholder="sk-or-..."
1083
- value={openrouterApiKey}
1084
- onChange={(e) => setOpenrouterApiKey(e.target.value)}
1408
+ placeholder="sk-ant-..."
1409
+ value={anthropicApiKey}
1410
+ onChange={(e) => setAnthropicApiKey(e.target.value)}
1085
1411
  />
1086
- <button
1087
- className="button-small button-secondary"
1088
- onClick={() => loadOpenRouterModels(openrouterApiKey)}
1089
- disabled={loadingOpenRouterModels}
1090
- >
1091
- {loadingOpenRouterModels ? 'Loading...' : 'Refresh Models'}
1092
- </button>
1093
1412
  </div>
1094
- </div>
1413
+ )}
1095
1414
 
1096
- <div className="settings-section">
1097
- <h3>Model</h3>
1098
- <p className="settings-description">
1099
- Select a model from OpenRouter's catalog. Enter your API key and click "Refresh Models" to load available models.
1100
- </p>
1101
- {openrouterModels.length > 0 ? (
1102
- <SearchableSelect
1103
- options={openrouterModels.map(model => ({
1104
- value: model.id,
1105
- label: model.name,
1106
- description: `${Math.round(model.context_length / 1000)}k context`,
1107
- }))}
1108
- value={openrouterModel}
1109
- onChange={setOpenrouterModel}
1110
- placeholder="Select a model..."
1111
- />
1112
- ) : (
1113
- <input
1114
- type="text"
1115
- className="settings-input"
1116
- placeholder="anthropic/claude-3.5-sonnet"
1117
- value={openrouterModel}
1118
- onChange={(e) => setOpenrouterModel(e.target.value)}
1119
- />
1120
- )}
1121
- <p className="settings-hint">
1122
- OpenRouter provides access to many models from different providers (Claude, GPT-4, Llama, etc.) through a unified API.
1123
- </p>
1124
- </div>
1125
- </>
1126
- )}
1415
+ {settings.providerType === 'gemini' && (
1416
+ <>
1417
+ <div className="settings-section">
1418
+ <h3>Gemini API Key</h3>
1419
+ <p className="settings-description">
1420
+ Enter your API key from{' '}
1421
+ <a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer">
1422
+ Google AI Studio
1423
+ </a>
1424
+ </p>
1425
+ <div className="settings-input-group">
1426
+ <input
1427
+ type="password"
1428
+ className="settings-input"
1429
+ placeholder="AIza..."
1430
+ value={geminiApiKey}
1431
+ onChange={(e) => setGeminiApiKey(e.target.value)}
1432
+ />
1433
+ <button
1434
+ className="button-small button-secondary"
1435
+ onClick={() => loadGeminiModels(geminiApiKey)}
1436
+ disabled={loadingGeminiModels}
1437
+ >
1438
+ {loadingGeminiModels ? 'Loading...' : 'Refresh Models'}
1439
+ </button>
1440
+ </div>
1441
+ </div>
1127
1442
 
1128
- {settings.providerType === 'openai' && (
1129
- <>
1130
- <div className="settings-section">
1131
- <h3>Authentication Method</h3>
1132
- <p className="settings-description">
1133
- Choose how to authenticate with OpenAI
1134
- </p>
1135
- <div className="auth-method-tabs">
1136
- <button
1137
- className={`auth-method-tab ${openaiAuthMethod === 'oauth' ? 'active' : ''}`}
1138
- onClick={() => setOpenaiAuthMethod('oauth')}
1139
- >
1140
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1141
- <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
1142
- <circle cx="12" cy="7" r="4" />
1143
- </svg>
1144
- Sign in with ChatGPT
1145
- </button>
1146
- <button
1147
- className={`auth-method-tab ${openaiAuthMethod === 'api_key' ? 'active' : ''}`}
1148
- onClick={() => setOpenaiAuthMethod('api_key')}
1149
- >
1150
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1151
- <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
1152
- </svg>
1153
- API Key
1154
- </button>
1155
- </div>
1156
- </div>
1443
+ <div className="settings-section">
1444
+ <h3>Model</h3>
1445
+ <p className="settings-description">
1446
+ Select a Gemini model. Enter your API key and click "Refresh Models" to load available models.
1447
+ </p>
1448
+ {geminiModels.length > 0 ? (
1449
+ <SearchableSelect
1450
+ options={geminiModels.map(model => ({
1451
+ value: model.name,
1452
+ label: model.displayName,
1453
+ description: model.description,
1454
+ }))}
1455
+ value={geminiModel}
1456
+ onChange={setGeminiModel}
1457
+ placeholder="Select a model..."
1458
+ />
1459
+ ) : (
1460
+ <input
1461
+ type="text"
1462
+ className="settings-input"
1463
+ placeholder="gemini-2.0-flash"
1464
+ value={geminiModel}
1465
+ onChange={(e) => setGeminiModel(e.target.value)}
1466
+ />
1467
+ )}
1468
+ </div>
1469
+ </>
1470
+ )}
1157
1471
 
1158
- {openaiAuthMethod === 'oauth' && (
1159
- <div className="settings-section">
1160
- <h3>ChatGPT Account</h3>
1161
- {openaiOAuthConnected ? (
1162
- <div className="oauth-connected">
1163
- <div className="oauth-status">
1164
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1165
- <path d="M22 11.08V12a10 10 0 11-5.93-9.14" />
1166
- <path d="M22 4L12 14.01l-3-3" />
1167
- </svg>
1168
- <span>Connected to ChatGPT</span>
1169
- </div>
1170
- <p className="settings-description">
1171
- Your ChatGPT account is connected. You can use GPT-4o and other models with your subscription.
1172
- </p>
1472
+ {settings.providerType === 'openrouter' && (
1473
+ <>
1474
+ <div className="settings-section">
1475
+ <h3>OpenRouter API Key</h3>
1476
+ <p className="settings-description">
1477
+ Enter your API key from{' '}
1478
+ <a href="https://openrouter.ai/keys" target="_blank" rel="noopener noreferrer">
1479
+ OpenRouter
1480
+ </a>
1481
+ </p>
1482
+ <div className="settings-input-group">
1483
+ <input
1484
+ type="password"
1485
+ className="settings-input"
1486
+ placeholder="sk-or-..."
1487
+ value={openrouterApiKey}
1488
+ onChange={(e) => setOpenrouterApiKey(e.target.value)}
1489
+ />
1173
1490
  <button
1174
1491
  className="button-small button-secondary"
1175
- onClick={handleOpenAIOAuthLogout}
1176
- disabled={openaiOAuthLoading}
1492
+ onClick={() => loadOpenRouterModels(openrouterApiKey)}
1493
+ disabled={loadingOpenRouterModels}
1177
1494
  >
1178
- {openaiOAuthLoading ? 'Disconnecting...' : 'Disconnect Account'}
1495
+ {loadingOpenRouterModels ? 'Loading...' : 'Refresh Models'}
1179
1496
  </button>
1180
1497
  </div>
1181
- ) : (
1182
- <div className="oauth-login">
1183
- <p className="settings-description">
1184
- Sign in with your ChatGPT account to use GPT-4o, o1, and other models with your subscription.
1185
- </p>
1498
+ </div>
1499
+
1500
+ <div className="settings-section">
1501
+ <h3>Base URL</h3>
1502
+ <p className="settings-description">
1503
+ Optional override for the OpenRouter API endpoint.
1504
+ </p>
1505
+ <input
1506
+ type="text"
1507
+ className="settings-input"
1508
+ placeholder="https://openrouter.ai/api/v1"
1509
+ value={openrouterBaseUrl}
1510
+ onChange={(e) => setOpenrouterBaseUrl(e.target.value)}
1511
+ />
1512
+ </div>
1513
+
1514
+ <div className="settings-section">
1515
+ <h3>Model</h3>
1516
+ <p className="settings-description">
1517
+ Select a model from OpenRouter's catalog. Enter your API key and click "Refresh Models" to load available models.
1518
+ </p>
1519
+ {openrouterModels.length > 0 ? (
1520
+ <SearchableSelect
1521
+ options={openrouterModels.map(model => ({
1522
+ value: model.id,
1523
+ label: model.name,
1524
+ description: `${Math.round(model.context_length / 1000)}k context`,
1525
+ }))}
1526
+ value={openrouterModel}
1527
+ onChange={setOpenrouterModel}
1528
+ placeholder="Select a model..."
1529
+ />
1530
+ ) : (
1531
+ <input
1532
+ type="text"
1533
+ className="settings-input"
1534
+ placeholder="anthropic/claude-3.5-sonnet"
1535
+ value={openrouterModel}
1536
+ onChange={(e) => setOpenrouterModel(e.target.value)}
1537
+ />
1538
+ )}
1539
+ <p className="settings-hint">
1540
+ OpenRouter provides access to many models from different providers (Claude, GPT-4, Llama, etc.) through a unified API.
1541
+ </p>
1542
+ </div>
1543
+ </>
1544
+ )}
1545
+
1546
+ {settings.providerType === 'openai' && (
1547
+ <>
1548
+ <div className="settings-section">
1549
+ <h3>Authentication Method</h3>
1550
+ <p className="settings-description">
1551
+ Choose how to authenticate with OpenAI
1552
+ </p>
1553
+ <div className="auth-method-tabs">
1186
1554
  <button
1187
- className="button-primary oauth-login-btn"
1188
- onClick={handleOpenAIOAuthLogin}
1189
- disabled={openaiOAuthLoading}
1555
+ className={`auth-method-tab ${openaiAuthMethod === 'oauth' ? 'active' : ''}`}
1556
+ onClick={() => setOpenaiAuthMethod('oauth')}
1190
1557
  >
1191
- {openaiOAuthLoading ? (
1192
- <>
1193
- <svg className="spinner" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1194
- <path d="M21 12a9 9 0 11-6.219-8.56" />
1195
- </svg>
1196
- Connecting...
1197
- </>
1198
- ) : (
1199
- <>
1200
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1201
- <path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
1202
- <polyline points="10 17 15 12 10 7" />
1203
- <line x1="15" y1="12" x2="3" y2="12" />
1204
- </svg>
1205
- Sign in with ChatGPT
1206
- </>
1207
- )}
1558
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1559
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
1560
+ <circle cx="12" cy="7" r="4" />
1561
+ </svg>
1562
+ Sign in with ChatGPT
1563
+ </button>
1564
+ <button
1565
+ className={`auth-method-tab ${openaiAuthMethod === 'api_key' ? 'active' : ''}`}
1566
+ onClick={() => setOpenaiAuthMethod('api_key')}
1567
+ >
1568
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1569
+ <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
1570
+ </svg>
1571
+ API Key
1208
1572
  </button>
1209
1573
  </div>
1574
+ </div>
1575
+
1576
+ {openaiAuthMethod === 'oauth' && (
1577
+ <div className="settings-section">
1578
+ <h3>ChatGPT Account</h3>
1579
+ {openaiOAuthConnected ? (
1580
+ <div className="oauth-connected">
1581
+ <div className="oauth-status">
1582
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1583
+ <path d="M22 11.08V12a10 10 0 11-5.93-9.14" />
1584
+ <path d="M22 4L12 14.01l-3-3" />
1585
+ </svg>
1586
+ <span>Connected to ChatGPT</span>
1587
+ </div>
1588
+ <p className="settings-description">
1589
+ Your ChatGPT account is connected. You can use GPT-4o and other models with your subscription.
1590
+ </p>
1591
+ <button
1592
+ className="button-small button-secondary"
1593
+ onClick={handleOpenAIOAuthLogout}
1594
+ disabled={openaiOAuthLoading}
1595
+ >
1596
+ {openaiOAuthLoading ? 'Disconnecting...' : 'Disconnect Account'}
1597
+ </button>
1598
+ </div>
1599
+ ) : (
1600
+ <div className="oauth-login">
1601
+ <p className="settings-description">
1602
+ Sign in with your ChatGPT account to use GPT-4o, o1, and other models with your subscription.
1603
+ </p>
1604
+ <button
1605
+ className="button-primary oauth-login-btn"
1606
+ onClick={handleOpenAIOAuthLogin}
1607
+ disabled={openaiOAuthLoading}
1608
+ >
1609
+ {openaiOAuthLoading ? (
1610
+ <>
1611
+ <svg className="spinner" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1612
+ <path d="M21 12a9 9 0 11-6.219-8.56" />
1613
+ </svg>
1614
+ Connecting...
1615
+ </>
1616
+ ) : (
1617
+ <>
1618
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1619
+ <path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
1620
+ <polyline points="10 17 15 12 10 7" />
1621
+ <line x1="15" y1="12" x2="3" y2="12" />
1622
+ </svg>
1623
+ Sign in with ChatGPT
1624
+ </>
1625
+ )}
1626
+ </button>
1627
+ </div>
1628
+ )}
1629
+ </div>
1210
1630
  )}
1211
- </div>
1631
+
1632
+ {openaiAuthMethod === 'api_key' && (
1633
+ <div className="settings-section">
1634
+ <h3>OpenAI API Key</h3>
1635
+ <p className="settings-description">
1636
+ Enter your API key from{' '}
1637
+ <a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer">
1638
+ OpenAI Platform
1639
+ </a>
1640
+ </p>
1641
+ <div className="settings-input-group">
1642
+ <input
1643
+ type="password"
1644
+ className="settings-input"
1645
+ placeholder="sk-..."
1646
+ value={openaiApiKey}
1647
+ onChange={(e) => setOpenaiApiKey(e.target.value)}
1648
+ />
1649
+ <button
1650
+ className="button-small button-secondary"
1651
+ onClick={() => loadOpenAIModels(openaiApiKey)}
1652
+ disabled={loadingOpenAIModels}
1653
+ >
1654
+ {loadingOpenAIModels ? 'Loading...' : 'Refresh Models'}
1655
+ </button>
1656
+ </div>
1657
+ </div>
1658
+ )}
1659
+
1660
+ <div className="settings-section">
1661
+ <h3>Model</h3>
1662
+ <p className="settings-description">
1663
+ {openaiAuthMethod === 'oauth' && openaiOAuthConnected
1664
+ ? 'Select a GPT model to use with your ChatGPT subscription.'
1665
+ : 'Select a GPT model. Enter your API key and click "Refresh Models" to load available models.'}
1666
+ </p>
1667
+ {openaiModels.length > 0 ? (
1668
+ <SearchableSelect
1669
+ options={openaiModels.map(model => ({
1670
+ value: model.id,
1671
+ label: model.name,
1672
+ description: model.description,
1673
+ }))}
1674
+ value={openaiModel}
1675
+ onChange={setOpenaiModel}
1676
+ placeholder="Select a model..."
1677
+ />
1678
+ ) : (
1679
+ <input
1680
+ type="text"
1681
+ className="settings-input"
1682
+ placeholder="gpt-4o-mini"
1683
+ value={openaiModel}
1684
+ onChange={(e) => setOpenaiModel(e.target.value)}
1685
+ />
1686
+ )}
1687
+ {openaiAuthMethod === 'oauth' && openaiOAuthConnected && (
1688
+ <button
1689
+ className="button-small button-secondary"
1690
+ onClick={() => loadOpenAIModels()}
1691
+ disabled={loadingOpenAIModels}
1692
+ style={{ marginTop: '8px' }}
1693
+ >
1694
+ {loadingOpenAIModels ? 'Loading...' : 'Refresh Models'}
1695
+ </button>
1696
+ )}
1697
+ </div>
1698
+ </>
1212
1699
  )}
1213
1700
 
1214
- {openaiAuthMethod === 'api_key' && (
1215
- <div className="settings-section">
1216
- <h3>OpenAI API Key</h3>
1217
- <p className="settings-description">
1218
- Enter your API key from{' '}
1219
- <a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer">
1220
- OpenAI Platform
1221
- </a>
1222
- </p>
1223
- <div className="settings-input-group">
1701
+ {settings.providerType === 'azure' && (
1702
+ <>
1703
+ <div className="settings-section">
1704
+ <h3>Azure OpenAI Endpoint</h3>
1705
+ <p className="settings-description">
1706
+ Enter your Azure OpenAI resource endpoint (for example, <code>https://your-resource.openai.azure.com</code>).
1707
+ </p>
1708
+ <input
1709
+ type="text"
1710
+ className="settings-input"
1711
+ placeholder="https://your-resource.openai.azure.com"
1712
+ value={azureEndpoint}
1713
+ onChange={(e) => setAzureEndpoint(e.target.value)}
1714
+ />
1715
+ </div>
1716
+
1717
+ <div className="settings-section">
1718
+ <h3>Azure OpenAI API Key</h3>
1719
+ <p className="settings-description">
1720
+ Enter the API key for your Azure OpenAI resource.
1721
+ </p>
1224
1722
  <input
1225
1723
  type="password"
1226
1724
  className="settings-input"
1227
- placeholder="sk-..."
1228
- value={openaiApiKey}
1229
- onChange={(e) => setOpenaiApiKey(e.target.value)}
1725
+ placeholder="Azure API key"
1726
+ value={azureApiKey}
1727
+ onChange={(e) => setAzureApiKey(e.target.value)}
1230
1728
  />
1231
- <button
1232
- className="button-small button-secondary"
1233
- onClick={() => loadOpenAIModels(openaiApiKey)}
1234
- disabled={loadingOpenAIModels}
1235
- >
1236
- {loadingOpenAIModels ? 'Loading...' : 'Refresh Models'}
1237
- </button>
1238
1729
  </div>
1239
- </div>
1730
+
1731
+ <div className="settings-section">
1732
+ <h3>Deployment Names</h3>
1733
+ <p className="settings-description">
1734
+ Enter one or more deployment names (one per line). These appear in the model selector.
1735
+ </p>
1736
+ <textarea
1737
+ className="settings-input"
1738
+ placeholder="gpt-4o-mini\nmy-other-deployment"
1739
+ rows={3}
1740
+ value={azureDeploymentsText}
1741
+ onChange={(e) => setAzureDeploymentsText(e.target.value)}
1742
+ />
1743
+ </div>
1744
+
1745
+ <div className="settings-section">
1746
+ <h3>Default Deployment</h3>
1747
+ <p className="settings-description">
1748
+ Optional. Used for connection tests and initial selection. You can switch models in the main view.
1749
+ </p>
1750
+ <input
1751
+ type="text"
1752
+ className="settings-input"
1753
+ placeholder="gpt-4o-mini"
1754
+ value={azureDeployment}
1755
+ onChange={(e) => setAzureDeployment(e.target.value)}
1756
+ />
1757
+ </div>
1758
+
1759
+ <div className="settings-section">
1760
+ <h3>API Version</h3>
1761
+ <p className="settings-description">
1762
+ Optional override for the Azure OpenAI API version.
1763
+ </p>
1764
+ <input
1765
+ type="text"
1766
+ className="settings-input"
1767
+ placeholder="2024-02-15-preview"
1768
+ value={azureApiVersion}
1769
+ onChange={(e) => setAzureApiVersion(e.target.value)}
1770
+ />
1771
+ </div>
1772
+ </>
1240
1773
  )}
1241
1774
 
1242
- <div className="settings-section">
1243
- <h3>Model</h3>
1244
- <p className="settings-description">
1245
- {openaiAuthMethod === 'oauth' && openaiOAuthConnected
1246
- ? 'Select a GPT model to use with your ChatGPT subscription.'
1247
- : 'Select a GPT model. Enter your API key and click "Refresh Models" to load available models.'}
1248
- </p>
1249
- {openaiModels.length > 0 ? (
1250
- <SearchableSelect
1251
- options={openaiModels.map(model => ({
1252
- value: model.id,
1253
- label: model.name,
1254
- description: model.description,
1255
- }))}
1256
- value={openaiModel}
1257
- onChange={setOpenaiModel}
1258
- placeholder="Select a model..."
1259
- />
1260
- ) : (
1261
- <input
1262
- type="text"
1263
- className="settings-input"
1264
- placeholder="gpt-4o-mini"
1265
- value={openaiModel}
1266
- onChange={(e) => setOpenaiModel(e.target.value)}
1267
- />
1268
- )}
1269
- {openaiAuthMethod === 'oauth' && openaiOAuthConnected && (
1270
- <button
1271
- className="button-small button-secondary"
1272
- onClick={() => loadOpenAIModels()}
1273
- disabled={loadingOpenAIModels}
1274
- style={{ marginTop: '8px' }}
1275
- >
1276
- {loadingOpenAIModels ? 'Loading...' : 'Refresh Models'}
1277
- </button>
1278
- )}
1279
- </div>
1280
- </>
1281
- )}
1775
+ {settings.providerType === 'groq' && (
1776
+ <>
1777
+ <div className="settings-section">
1778
+ <h3>Groq API Key</h3>
1779
+ <p className="settings-description">
1780
+ Enter your API key from{' '}
1781
+ <a href="https://console.groq.com/keys" target="_blank" rel="noopener noreferrer">
1782
+ Groq Console
1783
+ </a>
1784
+ </p>
1785
+ <div className="settings-input-group">
1786
+ <input
1787
+ type="password"
1788
+ className="settings-input"
1789
+ placeholder="gsk_..."
1790
+ value={groqApiKey}
1791
+ onChange={(e) => setGroqApiKey(e.target.value)}
1792
+ />
1793
+ <button
1794
+ className="button-small button-secondary"
1795
+ onClick={() => loadGroqModels(groqApiKey)}
1796
+ disabled={loadingGroqModels}
1797
+ >
1798
+ {loadingGroqModels ? 'Loading...' : 'Refresh Models'}
1799
+ </button>
1800
+ </div>
1801
+ </div>
1282
1802
 
1283
- {settings.providerType === 'bedrock' && (
1284
- <>
1285
- <div className="settings-section">
1286
- <h3>AWS Region</h3>
1287
- <select
1288
- className="settings-select"
1289
- value={awsRegion}
1290
- onChange={(e) => setAwsRegion(e.target.value)}
1291
- >
1292
- <option value="us-east-1">US East (N. Virginia)</option>
1293
- <option value="us-west-2">US West (Oregon)</option>
1294
- <option value="eu-west-1">Europe (Ireland)</option>
1295
- <option value="eu-central-1">Europe (Frankfurt)</option>
1296
- <option value="ap-northeast-1">Asia Pacific (Tokyo)</option>
1297
- <option value="ap-southeast-1">Asia Pacific (Singapore)</option>
1298
- <option value="ap-southeast-2">Asia Pacific (Sydney)</option>
1299
- </select>
1300
- </div>
1803
+ <div className="settings-section">
1804
+ <h3>Base URL</h3>
1805
+ <p className="settings-description">
1806
+ Optional override for the Groq API endpoint.
1807
+ </p>
1808
+ <input
1809
+ type="text"
1810
+ className="settings-input"
1811
+ placeholder="https://api.groq.com/openai/v1"
1812
+ value={groqBaseUrl}
1813
+ onChange={(e) => setGroqBaseUrl(e.target.value)}
1814
+ />
1815
+ </div>
1301
1816
 
1302
- <div className="settings-section">
1303
- <h3>AWS Credentials</h3>
1817
+ <div className="settings-section">
1818
+ <h3>Model</h3>
1819
+ <p className="settings-description">
1820
+ Select a Groq model. Enter your API key and click "Refresh Models" to load available models.
1821
+ </p>
1822
+ {groqModels.length > 0 ? (
1823
+ <SearchableSelect
1824
+ options={groqModels.map(model => ({
1825
+ value: model.id,
1826
+ label: model.name,
1827
+ }))}
1828
+ value={groqModel}
1829
+ onChange={setGroqModel}
1830
+ placeholder="Select a model..."
1831
+ />
1832
+ ) : (
1833
+ <input
1834
+ type="text"
1835
+ className="settings-input"
1836
+ placeholder="llama-3.1-8b-instant"
1837
+ value={groqModel}
1838
+ onChange={(e) => setGroqModel(e.target.value)}
1839
+ />
1840
+ )}
1841
+ </div>
1842
+ </>
1843
+ )}
1304
1844
 
1305
- <label className="settings-checkbox">
1306
- <input
1307
- type="checkbox"
1308
- checked={useDefaultCredentials}
1309
- onChange={(e) => setUseDefaultCredentials(e.target.checked)}
1310
- />
1311
- <span>Use default credential chain (recommended)</span>
1312
- </label>
1845
+ {settings.providerType === 'xai' && (
1846
+ <>
1847
+ <div className="settings-section">
1848
+ <h3>xAI API Key</h3>
1849
+ <p className="settings-description">
1850
+ Enter your API key from{' '}
1851
+ <a href="https://console.x.ai/" target="_blank" rel="noopener noreferrer">
1852
+ xAI Console
1853
+ </a>
1854
+ </p>
1855
+ <div className="settings-input-group">
1856
+ <input
1857
+ type="password"
1858
+ className="settings-input"
1859
+ placeholder="xai-..."
1860
+ value={xaiApiKey}
1861
+ onChange={(e) => setXaiApiKey(e.target.value)}
1862
+ />
1863
+ <button
1864
+ className="button-small button-secondary"
1865
+ onClick={() => loadXAIModels(xaiApiKey)}
1866
+ disabled={loadingXaiModels}
1867
+ >
1868
+ {loadingXaiModels ? 'Loading...' : 'Refresh Models'}
1869
+ </button>
1870
+ </div>
1871
+ </div>
1313
1872
 
1314
- {useDefaultCredentials ? (
1315
- <div className="settings-subsection">
1873
+ <div className="settings-section">
1874
+ <h3>Base URL</h3>
1316
1875
  <p className="settings-description">
1317
- Uses AWS credentials from environment variables, shared credentials file (~/.aws/credentials), or IAM role.
1876
+ Optional override for the xAI API endpoint.
1318
1877
  </p>
1319
1878
  <input
1320
1879
  type="text"
1321
1880
  className="settings-input"
1322
- placeholder="AWS Profile (optional, e.g., 'default')"
1323
- value={awsProfile}
1324
- onChange={(e) => setAwsProfile(e.target.value)}
1881
+ placeholder="https://api.x.ai/v1"
1882
+ value={xaiBaseUrl}
1883
+ onChange={(e) => setXaiBaseUrl(e.target.value)}
1325
1884
  />
1326
1885
  </div>
1327
- ) : (
1328
- <div className="settings-subsection">
1886
+
1887
+ <div className="settings-section">
1888
+ <h3>Model</h3>
1889
+ <p className="settings-description">
1890
+ Select a Grok model. Enter your API key and click "Refresh Models" to load available models.
1891
+ </p>
1892
+ {xaiModels.length > 0 ? (
1893
+ <SearchableSelect
1894
+ options={xaiModels.map(model => ({
1895
+ value: model.id,
1896
+ label: model.name,
1897
+ }))}
1898
+ value={xaiModel}
1899
+ onChange={setXaiModel}
1900
+ placeholder="Select a model..."
1901
+ />
1902
+ ) : (
1903
+ <input
1904
+ type="text"
1905
+ className="settings-input"
1906
+ placeholder="grok-4-fast-non-reasoning"
1907
+ value={xaiModel}
1908
+ onChange={(e) => setXaiModel(e.target.value)}
1909
+ />
1910
+ )}
1911
+ </div>
1912
+ </>
1913
+ )}
1914
+
1915
+ {settings.providerType === 'kimi' && (
1916
+ <>
1917
+ <div className="settings-section">
1918
+ <h3>Kimi API Key</h3>
1919
+ <p className="settings-description">
1920
+ Enter your API key from{' '}
1921
+ <a href="https://platform.moonshot.ai/" target="_blank" rel="noopener noreferrer">
1922
+ Moonshot Platform
1923
+ </a>
1924
+ </p>
1925
+ <div className="settings-input-group">
1926
+ <input
1927
+ type="password"
1928
+ className="settings-input"
1929
+ placeholder="sk-..."
1930
+ value={kimiApiKey}
1931
+ onChange={(e) => setKimiApiKey(e.target.value)}
1932
+ />
1933
+ <button
1934
+ className="button-small button-secondary"
1935
+ onClick={() => loadKimiModels(kimiApiKey)}
1936
+ disabled={loadingKimiModels}
1937
+ >
1938
+ {loadingKimiModels ? 'Loading...' : 'Refresh Models'}
1939
+ </button>
1940
+ </div>
1941
+ </div>
1942
+
1943
+ <div className="settings-section">
1944
+ <h3>Base URL</h3>
1945
+ <p className="settings-description">
1946
+ Optional override for the Kimi API endpoint.
1947
+ </p>
1329
1948
  <input
1330
1949
  type="text"
1331
1950
  className="settings-input"
1332
- placeholder="AWS Access Key ID"
1333
- value={awsAccessKeyId}
1334
- onChange={(e) => setAwsAccessKeyId(e.target.value)}
1951
+ placeholder="https://api.moonshot.ai/v1"
1952
+ value={kimiBaseUrl}
1953
+ onChange={(e) => setKimiBaseUrl(e.target.value)}
1335
1954
  />
1955
+ </div>
1956
+
1957
+ <div className="settings-section">
1958
+ <h3>Model</h3>
1959
+ <p className="settings-description">
1960
+ Select a Kimi model. Enter your API key and click "Refresh Models" to load available models.
1961
+ </p>
1962
+ {kimiModels.length > 0 ? (
1963
+ <SearchableSelect
1964
+ options={kimiModels.map(model => ({
1965
+ value: model.id,
1966
+ label: model.name,
1967
+ }))}
1968
+ value={kimiModel}
1969
+ onChange={setKimiModel}
1970
+ placeholder="Select a model..."
1971
+ />
1972
+ ) : (
1973
+ <input
1974
+ type="text"
1975
+ className="settings-input"
1976
+ placeholder="kimi-k2.5"
1977
+ value={kimiModel}
1978
+ onChange={(e) => setKimiModel(e.target.value)}
1979
+ />
1980
+ )}
1981
+ </div>
1982
+ </>
1983
+ )}
1984
+
1985
+ {selectedCustomProvider && (
1986
+ <>
1987
+ <div className="settings-section">
1988
+ <h3>{selectedCustomProvider.apiKeyLabel}</h3>
1989
+ {selectedCustomProvider.apiKeyUrl ? (
1990
+ <p className="settings-description">
1991
+ Enter your API key from{' '}
1992
+ <a href={selectedCustomProvider.apiKeyUrl} target="_blank" rel="noopener noreferrer">
1993
+ {selectedCustomProvider.name}
1994
+ </a>
1995
+ </p>
1996
+ ) : selectedCustomProvider.description ? (
1997
+ <p className="settings-description">
1998
+ {selectedCustomProvider.description}
1999
+ </p>
2000
+ ) : null}
1336
2001
  <input
1337
2002
  type="password"
1338
2003
  className="settings-input"
1339
- placeholder="AWS Secret Access Key"
1340
- value={awsSecretAccessKey}
1341
- onChange={(e) => setAwsSecretAccessKey(e.target.value)}
2004
+ placeholder={selectedCustomProvider.apiKeyPlaceholder || 'sk-...'}
2005
+ value={selectedCustomConfig.apiKey || ''}
2006
+ onChange={(e) => updateCustomProvider(resolvedProviderType, { apiKey: e.target.value })}
1342
2007
  />
2008
+ {selectedCustomProvider.apiKeyOptional && (
2009
+ <p className="settings-hint">API key is optional for this provider.</p>
2010
+ )}
1343
2011
  </div>
1344
- )}
1345
- </div>
1346
2012
 
1347
- <div className="settings-section">
1348
- <h3>Model</h3>
1349
- <p className="settings-description">
1350
- Select a Claude model from AWS Bedrock.{' '}
1351
- <button
1352
- className="button-small button-secondary"
1353
- onClick={loadBedrockModels}
1354
- disabled={loadingBedrockModels}
1355
- style={{ marginLeft: '8px' }}
1356
- >
1357
- {loadingBedrockModels ? 'Loading...' : 'Refresh Models'}
1358
- </button>
1359
- </p>
1360
- {bedrockModels.length > 0 ? (
1361
- <SearchableSelect
1362
- options={bedrockModels.map(model => ({
1363
- value: model.id,
1364
- label: model.name,
1365
- description: model.description,
1366
- }))}
1367
- value={bedrockModel}
1368
- onChange={setBedrockModel}
1369
- placeholder="Select a model..."
1370
- />
1371
- ) : (
1372
- <select
1373
- className="settings-select"
1374
- value={settings.modelKey}
1375
- onChange={(e) => setSettings({ ...settings, modelKey: e.target.value })}
1376
- >
1377
- {models.map(model => (
1378
- <option key={model.key} value={model.key}>
1379
- {model.displayName}
1380
- </option>
1381
- ))}
1382
- </select>
1383
- )}
1384
- </div>
1385
- </>
1386
- )}
2013
+ {(selectedCustomProvider.requiresBaseUrl || selectedCustomProvider.baseUrl) && (
2014
+ <div className="settings-section">
2015
+ <h3>Base URL</h3>
2016
+ <p className="settings-description">
2017
+ {selectedCustomProvider.requiresBaseUrl
2018
+ ? 'Base URL is required for this provider.'
2019
+ : 'Override the default base URL if needed.'}
2020
+ </p>
2021
+ <input
2022
+ type="text"
2023
+ className="settings-input"
2024
+ placeholder={selectedCustomProvider.baseUrl || 'https://...'}
2025
+ value={selectedCustomConfig.baseUrl || ''}
2026
+ onChange={(e) => updateCustomProvider(resolvedProviderType, { baseUrl: e.target.value })}
2027
+ />
2028
+ </div>
2029
+ )}
1387
2030
 
1388
- {settings.providerType === 'ollama' && (
1389
- <>
1390
- <div className="settings-section">
1391
- <h3>Ollama Server URL</h3>
1392
- <p className="settings-description">
1393
- URL of your Ollama server. Default is http://localhost:11434 for local installations.
1394
- </p>
1395
- <div className="settings-input-group">
1396
- <input
1397
- type="text"
1398
- className="settings-input"
1399
- placeholder="http://localhost:11434"
1400
- value={ollamaBaseUrl}
1401
- onChange={(e) => setOllamaBaseUrl(e.target.value)}
1402
- />
1403
- <button
1404
- className="button-small button-secondary"
1405
- onClick={() => loadOllamaModels(ollamaBaseUrl)}
1406
- disabled={loadingOllamaModels}
1407
- >
1408
- {loadingOllamaModels ? 'Loading...' : 'Refresh Models'}
1409
- </button>
1410
- </div>
1411
- </div>
2031
+ <div className="settings-section">
2032
+ <h3>Model</h3>
2033
+ <p className="settings-description">
2034
+ Enter the model ID to use for {selectedCustomProvider.name}.
2035
+ </p>
2036
+ <input
2037
+ type="text"
2038
+ className="settings-input"
2039
+ placeholder={selectedCustomProvider.defaultModel || 'model-id'}
2040
+ value={selectedCustomConfig.model || ''}
2041
+ onChange={(e) => updateCustomProvider(resolvedProviderType, { model: e.target.value })}
2042
+ />
2043
+ </div>
2044
+ </>
2045
+ )}
1412
2046
 
1413
- <div className="settings-section">
1414
- <h3>Model</h3>
1415
- <p className="settings-description">
1416
- Select from models available on your Ollama server, or enter a custom model name.
1417
- </p>
1418
- {ollamaModels.length > 0 ? (
1419
- <SearchableSelect
1420
- options={ollamaModels.map(model => ({
1421
- value: model.name,
1422
- label: model.name,
1423
- description: formatBytes(model.size),
1424
- }))}
1425
- value={ollamaModel}
1426
- onChange={setOllamaModel}
1427
- placeholder="Select a model..."
1428
- />
1429
- ) : (
1430
- <input
1431
- type="text"
1432
- className="settings-input"
1433
- placeholder="llama3.2"
1434
- value={ollamaModel}
1435
- onChange={(e) => setOllamaModel(e.target.value)}
1436
- />
1437
- )}
1438
- <p className="settings-hint">
1439
- Don't have models? Run <code>ollama pull llama3.2</code> to download a model.
1440
- </p>
1441
- </div>
2047
+ {settings.providerType === 'bedrock' && (
2048
+ <>
2049
+ <div className="settings-section">
2050
+ <h3>AWS Region</h3>
2051
+ <select
2052
+ className="settings-select"
2053
+ value={awsRegion}
2054
+ onChange={(e) => setAwsRegion(e.target.value)}
2055
+ >
2056
+ <option value="us-east-1">US East (N. Virginia)</option>
2057
+ <option value="us-west-2">US West (Oregon)</option>
2058
+ <option value="eu-west-1">Europe (Ireland)</option>
2059
+ <option value="eu-central-1">Europe (Frankfurt)</option>
2060
+ <option value="ap-northeast-1">Asia Pacific (Tokyo)</option>
2061
+ <option value="ap-southeast-1">Asia Pacific (Singapore)</option>
2062
+ <option value="ap-southeast-2">Asia Pacific (Sydney)</option>
2063
+ </select>
2064
+ </div>
1442
2065
 
1443
- <div className="settings-section">
1444
- <h3>API Key (Optional)</h3>
1445
- <p className="settings-description">
1446
- Only needed if connecting to a remote Ollama server that requires authentication.
1447
- </p>
1448
- <input
1449
- type="password"
1450
- className="settings-input"
1451
- placeholder="Optional API key for remote servers"
1452
- value={ollamaApiKey}
1453
- onChange={(e) => setOllamaApiKey(e.target.value)}
1454
- />
1455
- </div>
1456
- </>
1457
- )}
2066
+ <div className="settings-section">
2067
+ <h3>AWS Credentials</h3>
2068
+
2069
+ <label className="settings-checkbox">
2070
+ <input
2071
+ type="checkbox"
2072
+ checked={useDefaultCredentials}
2073
+ onChange={(e) => setUseDefaultCredentials(e.target.checked)}
2074
+ />
2075
+ <span>Use default credential chain (recommended)</span>
2076
+ </label>
2077
+
2078
+ {useDefaultCredentials ? (
2079
+ <div className="settings-subsection">
2080
+ <p className="settings-description">
2081
+ Uses AWS credentials from environment variables, shared credentials file (~/.aws/credentials), or IAM role.
2082
+ </p>
2083
+ <input
2084
+ type="text"
2085
+ className="settings-input"
2086
+ placeholder="AWS Profile (optional, e.g., 'default')"
2087
+ value={awsProfile}
2088
+ onChange={(e) => setAwsProfile(e.target.value)}
2089
+ />
2090
+ </div>
2091
+ ) : (
2092
+ <div className="settings-subsection">
2093
+ <input
2094
+ type="text"
2095
+ className="settings-input"
2096
+ placeholder="AWS Access Key ID"
2097
+ value={awsAccessKeyId}
2098
+ onChange={(e) => setAwsAccessKeyId(e.target.value)}
2099
+ />
2100
+ <input
2101
+ type="password"
2102
+ className="settings-input"
2103
+ placeholder="AWS Secret Access Key"
2104
+ value={awsSecretAccessKey}
2105
+ onChange={(e) => setAwsSecretAccessKey(e.target.value)}
2106
+ />
2107
+ </div>
2108
+ )}
2109
+ </div>
1458
2110
 
1459
- {testResult && (
1460
- <div className={`test-result ${testResult.success ? 'success' : 'error'}`}>
1461
- {testResult.success ? (
1462
- <>
1463
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1464
- <path d="M22 11.08V12a10 10 0 11-5.93-9.14" />
1465
- <path d="M22 4L12 14.01l-3-3" />
1466
- </svg>
1467
- Connection successful!
2111
+ <div className="settings-section">
2112
+ <h3>Model</h3>
2113
+ <p className="settings-description">
2114
+ Select a Claude model from AWS Bedrock.{' '}
2115
+ <button
2116
+ className="button-small button-secondary"
2117
+ onClick={loadBedrockModels}
2118
+ disabled={loadingBedrockModels}
2119
+ style={{ marginLeft: '8px' }}
2120
+ >
2121
+ {loadingBedrockModels ? 'Loading...' : 'Refresh Models'}
2122
+ </button>
2123
+ </p>
2124
+ {bedrockModels.length > 0 ? (
2125
+ <SearchableSelect
2126
+ options={bedrockModels.map(model => ({
2127
+ value: model.id,
2128
+ label: model.name,
2129
+ description: model.description,
2130
+ }))}
2131
+ value={bedrockModel}
2132
+ onChange={setBedrockModel}
2133
+ placeholder="Select a model..."
2134
+ />
2135
+ ) : (
2136
+ <select
2137
+ className="settings-select"
2138
+ value={settings.modelKey}
2139
+ onChange={(e) => setSettings({ ...settings, modelKey: e.target.value })}
2140
+ >
2141
+ {models.map(model => (
2142
+ <option key={model.key} value={model.key}>
2143
+ {model.displayName}
2144
+ </option>
2145
+ ))}
2146
+ </select>
2147
+ )}
2148
+ </div>
1468
2149
  </>
1469
- ) : (
2150
+ )}
2151
+
2152
+ {settings.providerType === 'ollama' && (
1470
2153
  <>
1471
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1472
- <circle cx="12" cy="12" r="10" />
1473
- <line x1="15" y1="9" x2="9" y2="15" />
1474
- <line x1="9" y1="9" x2="15" y2="15" />
1475
- </svg>
1476
- {testResult.error || 'Connection failed'}
2154
+ <div className="settings-section">
2155
+ <h3>Ollama Server URL</h3>
2156
+ <p className="settings-description">
2157
+ URL of your Ollama server. Default is http://localhost:11434 for local installations.
2158
+ </p>
2159
+ <div className="settings-input-group">
2160
+ <input
2161
+ type="text"
2162
+ className="settings-input"
2163
+ placeholder="http://localhost:11434"
2164
+ value={ollamaBaseUrl}
2165
+ onChange={(e) => setOllamaBaseUrl(e.target.value)}
2166
+ />
2167
+ <button
2168
+ className="button-small button-secondary"
2169
+ onClick={() => loadOllamaModels(ollamaBaseUrl)}
2170
+ disabled={loadingOllamaModels}
2171
+ >
2172
+ {loadingOllamaModels ? 'Loading...' : 'Refresh Models'}
2173
+ </button>
2174
+ </div>
2175
+ </div>
2176
+
2177
+ <div className="settings-section">
2178
+ <h3>Model</h3>
2179
+ <p className="settings-description">
2180
+ Select from models available on your Ollama server, or enter a custom model name.
2181
+ </p>
2182
+ {ollamaModels.length > 0 ? (
2183
+ <SearchableSelect
2184
+ options={ollamaModels.map(model => ({
2185
+ value: model.name,
2186
+ label: model.name,
2187
+ description: formatBytes(model.size),
2188
+ }))}
2189
+ value={ollamaModel}
2190
+ onChange={setOllamaModel}
2191
+ placeholder="Select a model..."
2192
+ />
2193
+ ) : (
2194
+ <input
2195
+ type="text"
2196
+ className="settings-input"
2197
+ placeholder="llama3.2"
2198
+ value={ollamaModel}
2199
+ onChange={(e) => setOllamaModel(e.target.value)}
2200
+ />
2201
+ )}
2202
+ <p className="settings-hint">
2203
+ Don't have models? Run <code>ollama pull llama3.2</code> to download a model.
2204
+ </p>
2205
+ </div>
2206
+
2207
+ <div className="settings-section">
2208
+ <h3>API Key (Optional)</h3>
2209
+ <p className="settings-description">
2210
+ Only needed if connecting to a remote Ollama server that requires authentication.
2211
+ </p>
2212
+ <input
2213
+ type="password"
2214
+ className="settings-input"
2215
+ placeholder="Optional API key for remote servers"
2216
+ value={ollamaApiKey}
2217
+ onChange={(e) => setOllamaApiKey(e.target.value)}
2218
+ />
2219
+ </div>
1477
2220
  </>
1478
2221
  )}
1479
- </div>
1480
- )}
1481
2222
 
1482
- <div className="settings-actions">
1483
- <button
1484
- className="button-secondary"
1485
- onClick={handleTestConnection}
1486
- disabled={loading || testing}
1487
- >
1488
- {testing ? 'Testing...' : 'Test Connection'}
1489
- </button>
1490
- <button
1491
- className="button-primary"
1492
- onClick={handleSave}
1493
- disabled={loading || saving}
1494
- >
1495
- {saving ? 'Saving...' : 'Save Settings'}
1496
- </button>
2223
+ {testResult && (
2224
+ <div className={`test-result ${testResult.success ? 'success' : 'error'}`}>
2225
+ {testResult.success ? (
2226
+ <>
2227
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
2228
+ <path d="M22 11.08V12a10 10 0 11-5.93-9.14" />
2229
+ <path d="M22 4L12 14.01l-3-3" />
2230
+ </svg>
2231
+ Connection successful!
2232
+ </>
2233
+ ) : (
2234
+ <>
2235
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
2236
+ <circle cx="12" cy="12" r="10" />
2237
+ <line x1="15" y1="9" x2="9" y2="15" />
2238
+ <line x1="9" y1="9" x2="15" y2="15" />
2239
+ </svg>
2240
+ <span title={testResult.error}>
2241
+ {(() => {
2242
+ const error = testResult.error || 'Connection failed';
2243
+ // Extract meaningful part before JSON details
2244
+ const jsonStart = error.indexOf(' [{');
2245
+ const truncated = jsonStart > 0 ? error.slice(0, jsonStart) : error;
2246
+ return truncated.length > 200 ? truncated.slice(0, 200) + '...' : truncated;
2247
+ })()}
2248
+ </span>
2249
+ </>
2250
+ )}
2251
+ </div>
2252
+ )}
2253
+
2254
+ <div className="settings-actions">
2255
+ <button
2256
+ className="button-secondary"
2257
+ onClick={handleTestConnection}
2258
+ disabled={loading || testing}
2259
+ >
2260
+ {testing ? 'Testing...' : 'Test Connection'}
2261
+ </button>
2262
+ <button
2263
+ className="button-primary"
2264
+ onClick={handleSave}
2265
+ disabled={loading || saving}
2266
+ >
2267
+ {saving ? 'Saving...' : 'Save Settings'}
2268
+ </button>
2269
+ </div>
2270
+ </div>
1497
2271
  </div>
1498
- </>
1499
- )}
2272
+ )}
2273
+ </div>
1500
2274
  </div>
1501
2275
  </div>
1502
2276
  </div>