cowork-os 0.3.21 → 0.3.23

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 (170) hide show
  1. package/README.md +293 -6
  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/daemon.js +25 -0
  48. package/dist/electron/electron/agent/executor.js +181 -26
  49. package/dist/electron/electron/agent/llm/anthropic-compatible-provider.js +177 -0
  50. package/dist/electron/electron/agent/llm/github-copilot-provider.js +97 -0
  51. package/dist/electron/electron/agent/llm/groq-provider.js +33 -0
  52. package/dist/electron/electron/agent/llm/index.js +11 -1
  53. package/dist/electron/electron/agent/llm/kimi-provider.js +33 -0
  54. package/dist/electron/electron/agent/llm/openai-compatible-provider.js +116 -0
  55. package/dist/electron/electron/agent/llm/openai-compatible.js +111 -0
  56. package/dist/electron/electron/agent/llm/openai-oauth.js +2 -1
  57. package/dist/electron/electron/agent/llm/openrouter-provider.js +1 -1
  58. package/dist/electron/electron/agent/llm/provider-factory.js +318 -4
  59. package/dist/electron/electron/agent/llm/types.js +66 -1
  60. package/dist/electron/electron/agent/llm/xai-provider.js +33 -0
  61. package/dist/electron/electron/agent/tools/box-tools.js +231 -0
  62. package/dist/electron/electron/agent/tools/builtin-settings.js +28 -0
  63. package/dist/electron/electron/agent/tools/dropbox-tools.js +237 -0
  64. package/dist/electron/electron/agent/tools/google-drive-tools.js +227 -0
  65. package/dist/electron/electron/agent/tools/notion-tools.js +312 -0
  66. package/dist/electron/electron/agent/tools/onedrive-tools.js +217 -0
  67. package/dist/electron/electron/agent/tools/registry.js +541 -0
  68. package/dist/electron/electron/agent/tools/sharepoint-tools.js +243 -0
  69. package/dist/electron/electron/agent/tools/shell-tools.js +12 -3
  70. package/dist/electron/electron/agent/tools/x-tools.js +1 -1
  71. package/dist/electron/electron/gateway/index.js +1 -0
  72. package/dist/electron/electron/gateway/router.js +123 -143
  73. package/dist/electron/electron/ipc/canvas-handlers.js +5 -0
  74. package/dist/electron/electron/ipc/handlers.js +627 -158
  75. package/dist/electron/electron/main.js +63 -0
  76. package/dist/electron/electron/mcp/oauth/connector-oauth.js +333 -0
  77. package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +503 -154
  78. package/dist/electron/electron/memory/MemoryService.js +1 -1
  79. package/dist/electron/electron/preload.js +74 -1
  80. package/dist/electron/electron/settings/box-manager.js +54 -0
  81. package/dist/electron/electron/settings/dropbox-manager.js +54 -0
  82. package/dist/electron/electron/settings/google-drive-manager.js +54 -0
  83. package/dist/electron/electron/settings/notion-manager.js +56 -0
  84. package/dist/electron/electron/settings/onedrive-manager.js +54 -0
  85. package/dist/electron/electron/settings/sharepoint-manager.js +54 -0
  86. package/dist/electron/electron/utils/box-api.js +153 -0
  87. package/dist/electron/electron/utils/dropbox-api.js +144 -0
  88. package/dist/electron/electron/utils/env-migration.js +19 -0
  89. package/dist/electron/electron/utils/google-drive-api.js +152 -0
  90. package/dist/electron/electron/utils/notion-api.js +103 -0
  91. package/dist/electron/electron/utils/onedrive-api.js +113 -0
  92. package/dist/electron/electron/utils/sharepoint-api.js +109 -0
  93. package/dist/electron/electron/utils/validation.js +82 -3
  94. package/dist/electron/electron/utils/x-cli.js +1 -1
  95. package/dist/electron/shared/channelMessages.js +284 -3
  96. package/dist/electron/shared/llm-provider-catalog.js +198 -0
  97. package/dist/electron/shared/types.js +88 -1
  98. package/package.json +12 -2
  99. package/src/electron/agent/executor.ts +205 -28
  100. package/src/electron/agent/llm/anthropic-compatible-provider.ts +214 -0
  101. package/src/electron/agent/llm/github-copilot-provider.ts +117 -0
  102. package/src/electron/agent/llm/groq-provider.ts +39 -0
  103. package/src/electron/agent/llm/index.ts +5 -0
  104. package/src/electron/agent/llm/kimi-provider.ts +39 -0
  105. package/src/electron/agent/llm/openai-compatible-provider.ts +153 -0
  106. package/src/electron/agent/llm/openai-compatible.ts +133 -0
  107. package/src/electron/agent/llm/openai-oauth.ts +2 -1
  108. package/src/electron/agent/llm/openrouter-provider.ts +2 -1
  109. package/src/electron/agent/llm/provider-factory.ts +414 -6
  110. package/src/electron/agent/llm/types.ts +90 -1
  111. package/src/electron/agent/llm/xai-provider.ts +39 -0
  112. package/src/electron/agent/tools/box-tools.ts +239 -0
  113. package/src/electron/agent/tools/builtin-settings.ts +34 -0
  114. package/src/electron/agent/tools/dropbox-tools.ts +237 -0
  115. package/src/electron/agent/tools/google-drive-tools.ts +228 -0
  116. package/src/electron/agent/tools/notion-tools.ts +330 -0
  117. package/src/electron/agent/tools/onedrive-tools.ts +217 -0
  118. package/src/electron/agent/tools/registry.ts +565 -0
  119. package/src/electron/agent/tools/sharepoint-tools.ts +247 -0
  120. package/src/electron/agent/tools/shell-tools.ts +11 -3
  121. package/src/electron/agent/tools/x-tools.ts +1 -1
  122. package/src/electron/database/SecureSettingsRepository.ts +7 -1
  123. package/src/electron/gateway/index.ts +1 -0
  124. package/src/electron/gateway/router.ts +134 -149
  125. package/src/electron/ipc/canvas-handlers.ts +10 -0
  126. package/src/electron/ipc/handlers.ts +673 -153
  127. package/src/electron/main.ts +35 -0
  128. package/src/electron/mcp/oauth/connector-oauth.ts +448 -0
  129. package/src/electron/mcp/registry/MCPRegistryManager.ts +343 -12
  130. package/src/electron/memory/MemoryService.ts +5 -1
  131. package/src/electron/preload.ts +167 -4
  132. package/src/electron/settings/box-manager.ts +58 -0
  133. package/src/electron/settings/dropbox-manager.ts +58 -0
  134. package/src/electron/settings/google-drive-manager.ts +58 -0
  135. package/src/electron/settings/notion-manager.ts +60 -0
  136. package/src/electron/settings/onedrive-manager.ts +58 -0
  137. package/src/electron/settings/sharepoint-manager.ts +58 -0
  138. package/src/electron/utils/box-api.ts +184 -0
  139. package/src/electron/utils/dropbox-api.ts +171 -0
  140. package/src/electron/utils/env-migration.ts +22 -0
  141. package/src/electron/utils/google-drive-api.ts +183 -0
  142. package/src/electron/utils/notion-api.ts +126 -0
  143. package/src/electron/utils/onedrive-api.ts +137 -0
  144. package/src/electron/utils/sharepoint-api.ts +132 -0
  145. package/src/electron/utils/validation.ts +102 -1
  146. package/src/electron/utils/x-cli.ts +1 -1
  147. package/src/renderer/App.tsx +20 -2
  148. package/src/renderer/components/BoxSettings.tsx +203 -0
  149. package/src/renderer/components/BrowserView.tsx +101 -0
  150. package/src/renderer/components/BuiltinToolsSettings.tsx +105 -0
  151. package/src/renderer/components/CanvasPreview.tsx +68 -1
  152. package/src/renderer/components/ConnectorEnvModal.tsx +116 -0
  153. package/src/renderer/components/ConnectorSetupModal.tsx +566 -0
  154. package/src/renderer/components/ConnectorsSettings.tsx +397 -0
  155. package/src/renderer/components/DropboxSettings.tsx +202 -0
  156. package/src/renderer/components/GoogleDriveSettings.tsx +201 -0
  157. package/src/renderer/components/MCPSettings.tsx +56 -0
  158. package/src/renderer/components/MainContent.tsx +270 -34
  159. package/src/renderer/components/NotionSettings.tsx +231 -0
  160. package/src/renderer/components/Onboarding/Onboarding.tsx +13 -1
  161. package/src/renderer/components/OnboardingModal.tsx +70 -1
  162. package/src/renderer/components/OneDriveSettings.tsx +212 -0
  163. package/src/renderer/components/Settings.tsx +611 -8
  164. package/src/renderer/components/SharePointSettings.tsx +224 -0
  165. package/src/renderer/components/Sidebar.tsx +25 -9
  166. package/src/renderer/hooks/useOnboardingFlow.ts +21 -0
  167. package/src/renderer/styles/index.css +438 -25
  168. package/src/shared/channelMessages.ts +367 -4
  169. package/src/shared/llm-provider-catalog.ts +217 -0
  170. package/src/shared/types.ts +226 -1
@@ -1,5 +1,6 @@
1
1
  import { useState, useEffect, useRef } from 'react';
2
- import { LLMSettingsData, ThemeMode, AccentColor } from '../../shared/types';
2
+ import { LLMSettingsData, ThemeMode, 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 { GoogleDriveSettings } from './GoogleDriveSettings';
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,10 +42,13 @@ 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' | 'googledrive' | 'dropbox' | 'sharepoint';
41
52
 
42
53
  interface SettingsProps {
43
54
  onBack: () => void;
@@ -246,12 +257,15 @@ const sidebarItems: Array<{ tab: SettingsTab; label: string; icon: React.ReactNo
246
257
  { 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
258
  { 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
259
  { 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> },
260
+ { 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
261
  { 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> },
262
+ { 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
263
  { 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
264
  { 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
265
  { 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
266
  { 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
267
  { 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> },
268
+ { 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
269
  { 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
270
  { 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
271
  { 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 +287,22 @@ const secondaryChannelItems: Array<{ key: SecondaryChannel; label: string; icon:
273
287
  { 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
288
  { 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
289
  { 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> },
290
+ ];
291
+
292
+ // App integrations configuration for "Integrations" tab
293
+ const integrationItems: Array<{ key: IntegrationChannel; label: string; icon: React.ReactNode }> = [
294
+ { 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> },
295
+ { 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> },
296
+ { 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> },
297
+ { key: 'googledrive', label: 'Google Drive', 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> },
298
+ { 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> },
299
+ { 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> },
277
300
  ];
278
301
 
279
302
  export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, onThemeChange, onAccentChange, initialTab = 'appearance', onShowOnboarding, onboardingCompletedAt }: SettingsProps) {
280
303
  const [activeTab, setActiveTab] = useState<SettingsTab>(initialTab);
281
304
  const [activeSecondaryChannel, setActiveSecondaryChannel] = useState<SecondaryChannel>('discord');
305
+ const [activeIntegration, setActiveIntegration] = useState<IntegrationChannel>('notion');
282
306
  const [sidebarSearch, setSidebarSearch] = useState('');
283
307
  const [settings, setSettings] = useState<LLMSettingsData>({
284
308
  providerType: 'anthropic',
@@ -314,6 +338,7 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
314
338
 
315
339
  // OpenRouter state
316
340
  const [openrouterApiKey, setOpenrouterApiKey] = useState('');
341
+ const [openrouterBaseUrl, setOpenrouterBaseUrl] = useState('');
317
342
  const [openrouterModel, setOpenrouterModel] = useState('anthropic/claude-3.5-sonnet');
318
343
  const [openrouterModels, setOpenrouterModels] = useState<Array<{ id: string; name: string; context_length: number }>>([]);
319
344
  const [loadingOpenRouterModels, setLoadingOpenRouterModels] = useState(false);
@@ -327,6 +352,30 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
327
352
  const [openaiOAuthConnected, setOpenaiOAuthConnected] = useState(false);
328
353
  const [openaiOAuthLoading, setOpenaiOAuthLoading] = useState(false);
329
354
 
355
+ // Groq state
356
+ const [groqApiKey, setGroqApiKey] = useState('');
357
+ const [groqBaseUrl, setGroqBaseUrl] = useState('');
358
+ const [groqModel, setGroqModel] = useState('llama-3.1-8b-instant');
359
+ const [groqModels, setGroqModels] = useState<Array<{ id: string; name: string }>>([]);
360
+ const [loadingGroqModels, setLoadingGroqModels] = useState(false);
361
+
362
+ // xAI state
363
+ const [xaiApiKey, setXaiApiKey] = useState('');
364
+ const [xaiBaseUrl, setXaiBaseUrl] = useState('');
365
+ const [xaiModel, setXaiModel] = useState('grok-4-fast-non-reasoning');
366
+ const [xaiModels, setXaiModels] = useState<Array<{ id: string; name: string }>>([]);
367
+ const [loadingXaiModels, setLoadingXaiModels] = useState(false);
368
+
369
+ // Kimi state
370
+ const [kimiApiKey, setKimiApiKey] = useState('');
371
+ const [kimiBaseUrl, setKimiBaseUrl] = useState('');
372
+ const [kimiModel, setKimiModel] = useState('kimi-k2.5');
373
+ const [kimiModels, setKimiModels] = useState<Array<{ id: string; name: string }>>([]);
374
+ const [loadingKimiModels, setLoadingKimiModels] = useState(false);
375
+
376
+ // Custom provider state
377
+ const [customProviders, setCustomProviders] = useState<Record<string, CustomProviderConfig>>({});
378
+
330
379
  // Bedrock state
331
380
  const [bedrockModel, setBedrockModel] = useState('');
332
381
  const [bedrockModels, setBedrockModels] = useState<Array<{ id: string; name: string; description: string }>>([]);
@@ -336,6 +385,37 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
336
385
  loadConfigStatus();
337
386
  }, []);
338
387
 
388
+ const resolveCustomProviderId = (providerType: LLMProviderType) =>
389
+ providerType === 'kimi-coding' ? 'kimi-code' : providerType;
390
+
391
+ const updateCustomProvider = (providerType: LLMProviderType, updates: Partial<CustomProviderConfig>) => {
392
+ const resolvedType = resolveCustomProviderId(providerType);
393
+ setCustomProviders((prev) => ({
394
+ ...prev,
395
+ [resolvedType]: {
396
+ ...(prev[resolvedType] || {}),
397
+ ...updates,
398
+ },
399
+ }));
400
+ };
401
+
402
+ const sanitizeCustomProviders = (providers: Record<string, CustomProviderConfig>) => {
403
+ const sanitized: Record<string, CustomProviderConfig> = {};
404
+ Object.entries(providers).forEach(([key, value]) => {
405
+ const apiKey = value.apiKey?.trim();
406
+ const model = value.model?.trim();
407
+ const baseUrl = value.baseUrl?.trim();
408
+ if (apiKey || model || baseUrl) {
409
+ sanitized[key] = {
410
+ ...(apiKey ? { apiKey } : {}),
411
+ ...(model ? { model } : {}),
412
+ ...(baseUrl ? { baseUrl } : {}),
413
+ };
414
+ }
415
+ });
416
+ return Object.keys(sanitized).length > 0 ? sanitized : undefined;
417
+ };
418
+
339
419
  const loadConfigStatus = async () => {
340
420
  try {
341
421
  setLoading(true);
@@ -349,6 +429,18 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
349
429
  // Load full settings separately for bedrock config
350
430
  const loadedSettings = await window.electronAPI.getLLMSettings();
351
431
  setSettings(loadedSettings);
432
+ if (loadedSettings.customProviders) {
433
+ const normalized = { ...loadedSettings.customProviders };
434
+ if (normalized['kimi-coding'] && !normalized['kimi-code']) {
435
+ normalized['kimi-code'] = normalized['kimi-coding'];
436
+ }
437
+ if (normalized['kimi-coding']) {
438
+ delete normalized['kimi-coding'];
439
+ }
440
+ setCustomProviders(normalized);
441
+ } else {
442
+ setCustomProviders({});
443
+ }
352
444
 
353
445
  // Set form state from loaded settings
354
446
  if (loadedSettings.bedrock?.region) {
@@ -387,6 +479,9 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
387
479
  if (loadedSettings.openrouter?.apiKey) {
388
480
  setOpenrouterApiKey(loadedSettings.openrouter.apiKey);
389
481
  }
482
+ if (loadedSettings.openrouter?.baseUrl) {
483
+ setOpenrouterBaseUrl(loadedSettings.openrouter.baseUrl);
484
+ }
390
485
  if (loadedSettings.openrouter?.model) {
391
486
  setOpenrouterModel(loadedSettings.openrouter.model);
392
487
  }
@@ -419,6 +514,39 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
419
514
  setOpenaiAuthMethod('oauth');
420
515
  }
421
516
 
517
+ // Set Groq form state
518
+ if (loadedSettings.groq?.apiKey) {
519
+ setGroqApiKey(loadedSettings.groq.apiKey);
520
+ }
521
+ if (loadedSettings.groq?.baseUrl) {
522
+ setGroqBaseUrl(loadedSettings.groq.baseUrl);
523
+ }
524
+ if (loadedSettings.groq?.model) {
525
+ setGroqModel(loadedSettings.groq.model);
526
+ }
527
+
528
+ // Set xAI form state
529
+ if (loadedSettings.xai?.apiKey) {
530
+ setXaiApiKey(loadedSettings.xai.apiKey);
531
+ }
532
+ if (loadedSettings.xai?.baseUrl) {
533
+ setXaiBaseUrl(loadedSettings.xai.baseUrl);
534
+ }
535
+ if (loadedSettings.xai?.model) {
536
+ setXaiModel(loadedSettings.xai.model);
537
+ }
538
+
539
+ // Set Kimi form state
540
+ if (loadedSettings.kimi?.apiKey) {
541
+ setKimiApiKey(loadedSettings.kimi.apiKey);
542
+ }
543
+ if (loadedSettings.kimi?.baseUrl) {
544
+ setKimiBaseUrl(loadedSettings.kimi.baseUrl);
545
+ }
546
+ if (loadedSettings.kimi?.model) {
547
+ setKimiModel(loadedSettings.kimi.model);
548
+ }
549
+
422
550
  // Set Bedrock form state (access key and secret key are set earlier)
423
551
  if (loadedSettings.bedrock?.accessKeyId) {
424
552
  setAwsAccessKeyId(loadedSettings.bedrock.accessKeyId);
@@ -514,7 +642,7 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
514
642
  const loadOpenRouterModels = async (apiKey?: string) => {
515
643
  try {
516
644
  setLoadingOpenRouterModels(true);
517
- const models = await window.electronAPI.getOpenRouterModels(apiKey || openrouterApiKey);
645
+ const models = await window.electronAPI.getOpenRouterModels(apiKey || openrouterApiKey, openrouterBaseUrl || undefined);
518
646
  setOpenrouterModels(models || []);
519
647
  // If we got models and current model isn't in the list, select the first one
520
648
  if (models && models.length > 0 && !models.some(m => m.id === openrouterModel)) {
@@ -549,6 +677,57 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
549
677
  }
550
678
  };
551
679
 
680
+ const loadGroqModels = async (apiKey?: string) => {
681
+ try {
682
+ setLoadingGroqModels(true);
683
+ const models = await window.electronAPI.getGroqModels(apiKey || groqApiKey, groqBaseUrl || undefined);
684
+ setGroqModels(models || []);
685
+ if (models && models.length > 0 && !models.some(m => m.id === groqModel)) {
686
+ setGroqModel(models[0].id);
687
+ }
688
+ onSettingsChanged?.();
689
+ } catch (error) {
690
+ console.error('Failed to load Groq models:', error);
691
+ setGroqModels([]);
692
+ } finally {
693
+ setLoadingGroqModels(false);
694
+ }
695
+ };
696
+
697
+ const loadXAIModels = async (apiKey?: string) => {
698
+ try {
699
+ setLoadingXaiModels(true);
700
+ const models = await window.electronAPI.getXAIModels(apiKey || xaiApiKey, xaiBaseUrl || undefined);
701
+ setXaiModels(models || []);
702
+ if (models && models.length > 0 && !models.some(m => m.id === xaiModel)) {
703
+ setXaiModel(models[0].id);
704
+ }
705
+ onSettingsChanged?.();
706
+ } catch (error) {
707
+ console.error('Failed to load xAI models:', error);
708
+ setXaiModels([]);
709
+ } finally {
710
+ setLoadingXaiModels(false);
711
+ }
712
+ };
713
+
714
+ const loadKimiModels = async (apiKey?: string) => {
715
+ try {
716
+ setLoadingKimiModels(true);
717
+ const models = await window.electronAPI.getKimiModels(apiKey || kimiApiKey, kimiBaseUrl || undefined);
718
+ setKimiModels(models || []);
719
+ if (models && models.length > 0 && !models.some(m => m.id === kimiModel)) {
720
+ setKimiModel(models[0].id);
721
+ }
722
+ onSettingsChanged?.();
723
+ } catch (error) {
724
+ console.error('Failed to load Kimi models:', error);
725
+ setKimiModels([]);
726
+ } finally {
727
+ setLoadingKimiModels(false);
728
+ }
729
+ };
730
+
552
731
  const handleOpenAIOAuthLogin = async () => {
553
732
  try {
554
733
  setOpenaiOAuthLoading(true);
@@ -613,6 +792,21 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
613
792
  setSaving(true);
614
793
  setTestResult(null);
615
794
 
795
+ const sanitizedCustomProviders = sanitizeCustomProviders(customProviders) || {};
796
+ const resolvedProviderTypeForSave = resolveCustomProviderId(settings.providerType as LLMProviderType);
797
+ const selectedCustomEntry = CUSTOM_PROVIDER_MAP.get(resolvedProviderTypeForSave);
798
+ if (selectedCustomEntry) {
799
+ const existing = sanitizedCustomProviders[resolvedProviderTypeForSave] || {};
800
+ const withDefaults: CustomProviderConfig = { ...existing };
801
+ if (!withDefaults.model && selectedCustomEntry.defaultModel) {
802
+ withDefaults.model = selectedCustomEntry.defaultModel;
803
+ }
804
+ if (!withDefaults.baseUrl && selectedCustomEntry.baseUrl) {
805
+ withDefaults.baseUrl = selectedCustomEntry.baseUrl;
806
+ }
807
+ sanitizedCustomProviders[resolvedProviderTypeForSave] = withDefaults;
808
+ }
809
+
616
810
  // Always save settings for ALL providers to preserve API keys and model selections
617
811
  // when switching between providers
618
812
  const settingsToSave: LLMSettingsData = {
@@ -648,6 +842,7 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
648
842
  openrouter: {
649
843
  apiKey: openrouterApiKey || undefined,
650
844
  model: openrouterModel || undefined,
845
+ baseUrl: openrouterBaseUrl || undefined,
651
846
  },
652
847
  // Always include openai settings
653
848
  openai: {
@@ -655,6 +850,25 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
655
850
  model: openaiModel || undefined,
656
851
  authMethod: openaiAuthMethod,
657
852
  },
853
+ // Always include Groq settings
854
+ groq: {
855
+ apiKey: groqApiKey || undefined,
856
+ model: groqModel || undefined,
857
+ baseUrl: groqBaseUrl || undefined,
858
+ },
859
+ // Always include xAI settings
860
+ xai: {
861
+ apiKey: xaiApiKey || undefined,
862
+ model: xaiModel || undefined,
863
+ baseUrl: xaiBaseUrl || undefined,
864
+ },
865
+ // Always include Kimi settings
866
+ kimi: {
867
+ apiKey: kimiApiKey || undefined,
868
+ model: kimiModel || undefined,
869
+ baseUrl: kimiBaseUrl || undefined,
870
+ },
871
+ customProviders: Object.keys(sanitizedCustomProviders).length > 0 ? sanitizedCustomProviders : undefined,
658
872
  };
659
873
 
660
874
  await window.electronAPI.saveLLMSettings(settingsToSave);
@@ -672,6 +886,8 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
672
886
  setTesting(true);
673
887
  setTestResult(null);
674
888
 
889
+ const sanitizedCustomProviders = sanitizeCustomProviders(customProviders) || {};
890
+
675
891
  const testConfig = {
676
892
  providerType: settings.providerType,
677
893
  modelKey: settings.modelKey,
@@ -699,6 +915,7 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
699
915
  openrouter: settings.providerType === 'openrouter' ? {
700
916
  apiKey: openrouterApiKey || undefined,
701
917
  model: openrouterModel || undefined,
918
+ baseUrl: openrouterBaseUrl || undefined,
702
919
  } : undefined,
703
920
  openai: settings.providerType === 'openai' ? {
704
921
  apiKey: openaiAuthMethod === 'api_key' ? (openaiApiKey || undefined) : undefined,
@@ -706,6 +923,22 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
706
923
  authMethod: openaiAuthMethod,
707
924
  // OAuth tokens are handled by the backend from stored settings
708
925
  } : undefined,
926
+ groq: settings.providerType === 'groq' ? {
927
+ apiKey: groqApiKey || undefined,
928
+ model: groqModel || undefined,
929
+ baseUrl: groqBaseUrl || undefined,
930
+ } : undefined,
931
+ xai: settings.providerType === 'xai' ? {
932
+ apiKey: xaiApiKey || undefined,
933
+ model: xaiModel || undefined,
934
+ baseUrl: xaiBaseUrl || undefined,
935
+ } : undefined,
936
+ kimi: settings.providerType === 'kimi' ? {
937
+ apiKey: kimiApiKey || undefined,
938
+ model: kimiModel || undefined,
939
+ baseUrl: kimiBaseUrl || undefined,
940
+ } : undefined,
941
+ customProviders: Object.keys(sanitizedCustomProviders).length > 0 ? sanitizedCustomProviders : undefined,
709
942
  };
710
943
 
711
944
  const result = await window.electronAPI.testLLMProvider(testConfig);
@@ -717,6 +950,10 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
717
950
  }
718
951
  };
719
952
 
953
+ const resolvedProviderType = resolveCustomProviderId(settings.providerType as LLMProviderType);
954
+ const selectedCustomProvider = CUSTOM_PROVIDER_MAP.get(resolvedProviderType);
955
+ const selectedCustomConfig = selectedCustomProvider ? (customProviders[resolvedProviderType] || {}) : {};
956
+
720
957
  return (
721
958
  <div className="settings-page">
722
959
  <div className="settings-page-header">
@@ -814,11 +1051,13 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
814
1051
  <WhatsAppSettings />
815
1052
  ) : activeTab === 'teams' ? (
816
1053
  <TeamsSettings />
1054
+ ) : activeTab === 'x' ? (
1055
+ <XSettings />
817
1056
  ) : activeTab === 'morechannels' ? (
818
1057
  <div className="more-channels-panel">
819
1058
  <div className="more-channels-header">
820
1059
  <h2>More Channels</h2>
821
- <p className="settings-description">Configure additional messaging platforms and integrations</p>
1060
+ <p className="settings-description">Configure additional messaging platforms</p>
822
1061
  </div>
823
1062
  <div className="more-channels-tabs">
824
1063
  {secondaryChannelItems.map(item => (
@@ -843,7 +1082,33 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
843
1082
  {activeSecondaryChannel === 'bluebubbles' && <BlueBubblesSettings />}
844
1083
  {activeSecondaryChannel === 'email' && <EmailSettings />}
845
1084
  {activeSecondaryChannel === 'googlechat' && <GoogleChatSettings />}
846
- {activeSecondaryChannel === 'x' && <XSettings />}
1085
+ </div>
1086
+ </div>
1087
+ ) : activeTab === 'integrations' ? (
1088
+ <div className="integrations-panel">
1089
+ <div className="integrations-header">
1090
+ <h2>Integrations</h2>
1091
+ <p className="settings-description">Connect productivity and storage tools for the agent</p>
1092
+ </div>
1093
+ <div className="integrations-tabs">
1094
+ {integrationItems.map(item => (
1095
+ <button
1096
+ key={item.key}
1097
+ className={`integrations-tab ${activeIntegration === item.key ? 'active' : ''}`}
1098
+ onClick={() => setActiveIntegration(item.key)}
1099
+ >
1100
+ {item.icon}
1101
+ <span>{item.label}</span>
1102
+ </button>
1103
+ ))}
1104
+ </div>
1105
+ <div className="integrations-content">
1106
+ {activeIntegration === 'notion' && <NotionSettings />}
1107
+ {activeIntegration === 'box' && <BoxSettings />}
1108
+ {activeIntegration === 'onedrive' && <OneDriveSettings />}
1109
+ {activeIntegration === 'googledrive' && <GoogleDriveSettings />}
1110
+ {activeIntegration === 'dropbox' && <DropboxSettings />}
1111
+ {activeIntegration === 'sharepoint' && <SharePointSettings />}
847
1112
  </div>
848
1113
  </div>
849
1114
  ) : activeTab === 'search' ? (
@@ -860,6 +1125,8 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
860
1125
  <SkillHubBrowser />
861
1126
  ) : activeTab === 'scheduled' ? (
862
1127
  <ScheduledTasksSettings />
1128
+ ) : activeTab === 'connectors' ? (
1129
+ <ConnectorsSettings />
863
1130
  ) : activeTab === 'mcp' ? (
864
1131
  <MCPSettings />
865
1132
  ) : activeTab === 'tools' ? (
@@ -890,6 +1157,13 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
890
1157
  const isGemini = provider.type === 'gemini';
891
1158
  const isOpenRouter = provider.type === 'openrouter';
892
1159
  const isOpenAI = provider.type === 'openai';
1160
+ const isGroq = provider.type === 'groq';
1161
+ const isXAI = provider.type === 'xai';
1162
+ const isKimi = provider.type === 'kimi';
1163
+ const providerType = provider.type as LLMProviderType;
1164
+ const resolvedCustomType = resolveCustomProviderId(providerType);
1165
+ const customEntry = CUSTOM_PROVIDER_MAP.get(resolvedCustomType);
1166
+ const isCustom = !!customEntry;
893
1167
 
894
1168
  return (
895
1169
  <label
@@ -902,7 +1176,20 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
902
1176
  value={provider.type}
903
1177
  checked={settings.providerType === provider.type}
904
1178
  onChange={() => {
905
- setSettings({ ...settings, providerType: provider.type as 'anthropic' | 'bedrock' | 'ollama' | 'gemini' | 'openrouter' | 'openai' });
1179
+ setSettings({ ...settings, providerType: providerType });
1180
+ if (customEntry) {
1181
+ setCustomProviders((prev) => {
1182
+ const existing = prev[resolvedCustomType] || {};
1183
+ const updated: CustomProviderConfig = { ...existing };
1184
+ if (!updated.model && customEntry.defaultModel) {
1185
+ updated.model = customEntry.defaultModel;
1186
+ }
1187
+ if (!updated.baseUrl && customEntry.baseUrl) {
1188
+ updated.baseUrl = customEntry.baseUrl;
1189
+ }
1190
+ return { ...prev, [resolvedCustomType]: updated };
1191
+ });
1192
+ }
906
1193
  // Load models when selecting provider
907
1194
  if (provider.type === 'ollama') {
908
1195
  loadOllamaModels();
@@ -912,6 +1199,12 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
912
1199
  loadOpenRouterModels();
913
1200
  } else if (provider.type === 'openai') {
914
1201
  loadOpenAIModels();
1202
+ } else if (provider.type === 'groq') {
1203
+ loadGroqModels();
1204
+ } else if (provider.type === 'xai') {
1205
+ loadXAIModels();
1206
+ } else if (provider.type === 'kimi') {
1207
+ loadKimiModels();
915
1208
  }
916
1209
  }}
917
1210
  />
@@ -952,6 +1245,30 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
952
1245
  {isOpenAI && !provider.configured && (
953
1246
  <>Sign in with ChatGPT or enter API key</>
954
1247
  )}
1248
+ {isGroq && provider.configured && (
1249
+ <>API key configured</>
1250
+ )}
1251
+ {isGroq && !provider.configured && (
1252
+ <>Enter your Groq API key below</>
1253
+ )}
1254
+ {isXAI && provider.configured && (
1255
+ <>API key configured</>
1256
+ )}
1257
+ {isXAI && !provider.configured && (
1258
+ <>Enter your xAI API key below</>
1259
+ )}
1260
+ {isKimi && provider.configured && (
1261
+ <>API key configured</>
1262
+ )}
1263
+ {isKimi && !provider.configured && (
1264
+ <>Enter your Kimi API key below</>
1265
+ )}
1266
+ {isCustom && provider.configured && (
1267
+ <>API key configured</>
1268
+ )}
1269
+ {isCustom && !provider.configured && (
1270
+ <>{customEntry?.description || `Configure ${provider.name} below`}</>
1271
+ )}
955
1272
  {isBedrock && provider.configured && (
956
1273
  <>AWS credentials configured</>
957
1274
  )}
@@ -1093,6 +1410,20 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
1093
1410
  </div>
1094
1411
  </div>
1095
1412
 
1413
+ <div className="settings-section">
1414
+ <h3>Base URL</h3>
1415
+ <p className="settings-description">
1416
+ Optional override for the OpenRouter API endpoint.
1417
+ </p>
1418
+ <input
1419
+ type="text"
1420
+ className="settings-input"
1421
+ placeholder="https://openrouter.ai/api/v1"
1422
+ value={openrouterBaseUrl}
1423
+ onChange={(e) => setOpenrouterBaseUrl(e.target.value)}
1424
+ />
1425
+ </div>
1426
+
1096
1427
  <div className="settings-section">
1097
1428
  <h3>Model</h3>
1098
1429
  <p className="settings-description">
@@ -1280,6 +1611,278 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
1280
1611
  </>
1281
1612
  )}
1282
1613
 
1614
+ {settings.providerType === 'groq' && (
1615
+ <>
1616
+ <div className="settings-section">
1617
+ <h3>Groq API Key</h3>
1618
+ <p className="settings-description">
1619
+ Enter your API key from{' '}
1620
+ <a href="https://console.groq.com/keys" target="_blank" rel="noopener noreferrer">
1621
+ Groq Console
1622
+ </a>
1623
+ </p>
1624
+ <div className="settings-input-group">
1625
+ <input
1626
+ type="password"
1627
+ className="settings-input"
1628
+ placeholder="gsk_..."
1629
+ value={groqApiKey}
1630
+ onChange={(e) => setGroqApiKey(e.target.value)}
1631
+ />
1632
+ <button
1633
+ className="button-small button-secondary"
1634
+ onClick={() => loadGroqModels(groqApiKey)}
1635
+ disabled={loadingGroqModels}
1636
+ >
1637
+ {loadingGroqModels ? 'Loading...' : 'Refresh Models'}
1638
+ </button>
1639
+ </div>
1640
+ </div>
1641
+
1642
+ <div className="settings-section">
1643
+ <h3>Base URL</h3>
1644
+ <p className="settings-description">
1645
+ Optional override for the Groq API endpoint.
1646
+ </p>
1647
+ <input
1648
+ type="text"
1649
+ className="settings-input"
1650
+ placeholder="https://api.groq.com/openai/v1"
1651
+ value={groqBaseUrl}
1652
+ onChange={(e) => setGroqBaseUrl(e.target.value)}
1653
+ />
1654
+ </div>
1655
+
1656
+ <div className="settings-section">
1657
+ <h3>Model</h3>
1658
+ <p className="settings-description">
1659
+ Select a Groq model. Enter your API key and click "Refresh Models" to load available models.
1660
+ </p>
1661
+ {groqModels.length > 0 ? (
1662
+ <SearchableSelect
1663
+ options={groqModels.map(model => ({
1664
+ value: model.id,
1665
+ label: model.name,
1666
+ }))}
1667
+ value={groqModel}
1668
+ onChange={setGroqModel}
1669
+ placeholder="Select a model..."
1670
+ />
1671
+ ) : (
1672
+ <input
1673
+ type="text"
1674
+ className="settings-input"
1675
+ placeholder="llama-3.1-8b-instant"
1676
+ value={groqModel}
1677
+ onChange={(e) => setGroqModel(e.target.value)}
1678
+ />
1679
+ )}
1680
+ </div>
1681
+ </>
1682
+ )}
1683
+
1684
+ {settings.providerType === 'xai' && (
1685
+ <>
1686
+ <div className="settings-section">
1687
+ <h3>xAI API Key</h3>
1688
+ <p className="settings-description">
1689
+ Enter your API key from{' '}
1690
+ <a href="https://console.x.ai/" target="_blank" rel="noopener noreferrer">
1691
+ xAI Console
1692
+ </a>
1693
+ </p>
1694
+ <div className="settings-input-group">
1695
+ <input
1696
+ type="password"
1697
+ className="settings-input"
1698
+ placeholder="xai-..."
1699
+ value={xaiApiKey}
1700
+ onChange={(e) => setXaiApiKey(e.target.value)}
1701
+ />
1702
+ <button
1703
+ className="button-small button-secondary"
1704
+ onClick={() => loadXAIModels(xaiApiKey)}
1705
+ disabled={loadingXaiModels}
1706
+ >
1707
+ {loadingXaiModels ? 'Loading...' : 'Refresh Models'}
1708
+ </button>
1709
+ </div>
1710
+ </div>
1711
+
1712
+ <div className="settings-section">
1713
+ <h3>Base URL</h3>
1714
+ <p className="settings-description">
1715
+ Optional override for the xAI API endpoint.
1716
+ </p>
1717
+ <input
1718
+ type="text"
1719
+ className="settings-input"
1720
+ placeholder="https://api.x.ai/v1"
1721
+ value={xaiBaseUrl}
1722
+ onChange={(e) => setXaiBaseUrl(e.target.value)}
1723
+ />
1724
+ </div>
1725
+
1726
+ <div className="settings-section">
1727
+ <h3>Model</h3>
1728
+ <p className="settings-description">
1729
+ Select a Grok model. Enter your API key and click "Refresh Models" to load available models.
1730
+ </p>
1731
+ {xaiModels.length > 0 ? (
1732
+ <SearchableSelect
1733
+ options={xaiModels.map(model => ({
1734
+ value: model.id,
1735
+ label: model.name,
1736
+ }))}
1737
+ value={xaiModel}
1738
+ onChange={setXaiModel}
1739
+ placeholder="Select a model..."
1740
+ />
1741
+ ) : (
1742
+ <input
1743
+ type="text"
1744
+ className="settings-input"
1745
+ placeholder="grok-4-fast-non-reasoning"
1746
+ value={xaiModel}
1747
+ onChange={(e) => setXaiModel(e.target.value)}
1748
+ />
1749
+ )}
1750
+ </div>
1751
+ </>
1752
+ )}
1753
+
1754
+ {settings.providerType === 'kimi' && (
1755
+ <>
1756
+ <div className="settings-section">
1757
+ <h3>Kimi API Key</h3>
1758
+ <p className="settings-description">
1759
+ Enter your API key from{' '}
1760
+ <a href="https://platform.moonshot.ai/" target="_blank" rel="noopener noreferrer">
1761
+ Moonshot Platform
1762
+ </a>
1763
+ </p>
1764
+ <div className="settings-input-group">
1765
+ <input
1766
+ type="password"
1767
+ className="settings-input"
1768
+ placeholder="sk-..."
1769
+ value={kimiApiKey}
1770
+ onChange={(e) => setKimiApiKey(e.target.value)}
1771
+ />
1772
+ <button
1773
+ className="button-small button-secondary"
1774
+ onClick={() => loadKimiModels(kimiApiKey)}
1775
+ disabled={loadingKimiModels}
1776
+ >
1777
+ {loadingKimiModels ? 'Loading...' : 'Refresh Models'}
1778
+ </button>
1779
+ </div>
1780
+ </div>
1781
+
1782
+ <div className="settings-section">
1783
+ <h3>Base URL</h3>
1784
+ <p className="settings-description">
1785
+ Optional override for the Kimi API endpoint.
1786
+ </p>
1787
+ <input
1788
+ type="text"
1789
+ className="settings-input"
1790
+ placeholder="https://api.moonshot.ai/v1"
1791
+ value={kimiBaseUrl}
1792
+ onChange={(e) => setKimiBaseUrl(e.target.value)}
1793
+ />
1794
+ </div>
1795
+
1796
+ <div className="settings-section">
1797
+ <h3>Model</h3>
1798
+ <p className="settings-description">
1799
+ Select a Kimi model. Enter your API key and click "Refresh Models" to load available models.
1800
+ </p>
1801
+ {kimiModels.length > 0 ? (
1802
+ <SearchableSelect
1803
+ options={kimiModels.map(model => ({
1804
+ value: model.id,
1805
+ label: model.name,
1806
+ }))}
1807
+ value={kimiModel}
1808
+ onChange={setKimiModel}
1809
+ placeholder="Select a model..."
1810
+ />
1811
+ ) : (
1812
+ <input
1813
+ type="text"
1814
+ className="settings-input"
1815
+ placeholder="kimi-k2.5"
1816
+ value={kimiModel}
1817
+ onChange={(e) => setKimiModel(e.target.value)}
1818
+ />
1819
+ )}
1820
+ </div>
1821
+ </>
1822
+ )}
1823
+
1824
+ {selectedCustomProvider && (
1825
+ <>
1826
+ <div className="settings-section">
1827
+ <h3>{selectedCustomProvider.apiKeyLabel}</h3>
1828
+ {selectedCustomProvider.apiKeyUrl ? (
1829
+ <p className="settings-description">
1830
+ Enter your API key from{' '}
1831
+ <a href={selectedCustomProvider.apiKeyUrl} target="_blank" rel="noopener noreferrer">
1832
+ {selectedCustomProvider.name}
1833
+ </a>
1834
+ </p>
1835
+ ) : selectedCustomProvider.description ? (
1836
+ <p className="settings-description">
1837
+ {selectedCustomProvider.description}
1838
+ </p>
1839
+ ) : null}
1840
+ <input
1841
+ type="password"
1842
+ className="settings-input"
1843
+ placeholder={selectedCustomProvider.apiKeyPlaceholder || 'sk-...'}
1844
+ value={selectedCustomConfig.apiKey || ''}
1845
+ onChange={(e) => updateCustomProvider(resolvedProviderType, { apiKey: e.target.value })}
1846
+ />
1847
+ {selectedCustomProvider.apiKeyOptional && (
1848
+ <p className="settings-hint">API key is optional for this provider.</p>
1849
+ )}
1850
+ </div>
1851
+
1852
+ {(selectedCustomProvider.requiresBaseUrl || selectedCustomProvider.baseUrl) && (
1853
+ <div className="settings-section">
1854
+ <h3>Base URL</h3>
1855
+ <p className="settings-description">
1856
+ {selectedCustomProvider.requiresBaseUrl
1857
+ ? 'Base URL is required for this provider.'
1858
+ : 'Override the default base URL if needed.'}
1859
+ </p>
1860
+ <input
1861
+ type="text"
1862
+ className="settings-input"
1863
+ placeholder={selectedCustomProvider.baseUrl || 'https://...'}
1864
+ value={selectedCustomConfig.baseUrl || ''}
1865
+ onChange={(e) => updateCustomProvider(resolvedProviderType, { baseUrl: e.target.value })}
1866
+ />
1867
+ </div>
1868
+ )}
1869
+
1870
+ <div className="settings-section">
1871
+ <h3>Model</h3>
1872
+ <p className="settings-description">
1873
+ Enter the model ID to use for {selectedCustomProvider.name}.
1874
+ </p>
1875
+ <input
1876
+ type="text"
1877
+ className="settings-input"
1878
+ placeholder={selectedCustomProvider.defaultModel || 'model-id'}
1879
+ value={selectedCustomConfig.model || ''}
1880
+ onChange={(e) => updateCustomProvider(resolvedProviderType, { model: e.target.value })}
1881
+ />
1882
+ </div>
1883
+ </>
1884
+ )}
1885
+
1283
1886
  {settings.providerType === 'bedrock' && (
1284
1887
  <>
1285
1888
  <div className="settings-section">