cowork-os 0.3.21

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 (526) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1638 -0
  3. package/bin/cowork.js +42 -0
  4. package/build/entitlements.mac.plist +16 -0
  5. package/build/icon.icns +0 -0
  6. package/build/icon.png +0 -0
  7. package/dist/electron/electron/activity/ActivityRepository.js +190 -0
  8. package/dist/electron/electron/agent/browser/browser-service.js +639 -0
  9. package/dist/electron/electron/agent/context-manager.js +225 -0
  10. package/dist/electron/electron/agent/custom-skill-loader.js +566 -0
  11. package/dist/electron/electron/agent/daemon.js +975 -0
  12. package/dist/electron/electron/agent/executor.js +3561 -0
  13. package/dist/electron/electron/agent/llm/anthropic-provider.js +155 -0
  14. package/dist/electron/electron/agent/llm/bedrock-provider.js +202 -0
  15. package/dist/electron/electron/agent/llm/gemini-provider.js +375 -0
  16. package/dist/electron/electron/agent/llm/index.js +34 -0
  17. package/dist/electron/electron/agent/llm/ollama-provider.js +263 -0
  18. package/dist/electron/electron/agent/llm/openai-oauth.js +101 -0
  19. package/dist/electron/electron/agent/llm/openai-provider.js +657 -0
  20. package/dist/electron/electron/agent/llm/openrouter-provider.js +232 -0
  21. package/dist/electron/electron/agent/llm/pricing.js +160 -0
  22. package/dist/electron/electron/agent/llm/provider-factory.js +880 -0
  23. package/dist/electron/electron/agent/llm/types.js +178 -0
  24. package/dist/electron/electron/agent/queue-manager.js +378 -0
  25. package/dist/electron/electron/agent/sandbox/docker-sandbox.js +402 -0
  26. package/dist/electron/electron/agent/sandbox/macos-sandbox.js +407 -0
  27. package/dist/electron/electron/agent/sandbox/runner.js +410 -0
  28. package/dist/electron/electron/agent/sandbox/sandbox-factory.js +228 -0
  29. package/dist/electron/electron/agent/sandbox/security-utils.js +258 -0
  30. package/dist/electron/electron/agent/search/brave-provider.js +119 -0
  31. package/dist/electron/electron/agent/search/google-provider.js +100 -0
  32. package/dist/electron/electron/agent/search/index.js +28 -0
  33. package/dist/electron/electron/agent/search/provider-factory.js +395 -0
  34. package/dist/electron/electron/agent/search/serpapi-provider.js +112 -0
  35. package/dist/electron/electron/agent/search/tavily-provider.js +90 -0
  36. package/dist/electron/electron/agent/search/types.js +40 -0
  37. package/dist/electron/electron/agent/security/index.js +12 -0
  38. package/dist/electron/electron/agent/security/input-sanitizer.js +303 -0
  39. package/dist/electron/electron/agent/security/output-filter.js +217 -0
  40. package/dist/electron/electron/agent/skill-eligibility.js +281 -0
  41. package/dist/electron/electron/agent/skill-registry.js +396 -0
  42. package/dist/electron/electron/agent/skills/document.js +878 -0
  43. package/dist/electron/electron/agent/skills/image-generator.js +225 -0
  44. package/dist/electron/electron/agent/skills/organizer.js +141 -0
  45. package/dist/electron/electron/agent/skills/presentation.js +367 -0
  46. package/dist/electron/electron/agent/skills/spreadsheet.js +165 -0
  47. package/dist/electron/electron/agent/tools/browser-tools.js +523 -0
  48. package/dist/electron/electron/agent/tools/builtin-settings.js +384 -0
  49. package/dist/electron/electron/agent/tools/canvas-tools.js +530 -0
  50. package/dist/electron/electron/agent/tools/cron-tools.js +577 -0
  51. package/dist/electron/electron/agent/tools/edit-tools.js +194 -0
  52. package/dist/electron/electron/agent/tools/file-tools.js +719 -0
  53. package/dist/electron/electron/agent/tools/glob-tools.js +283 -0
  54. package/dist/electron/electron/agent/tools/grep-tools.js +387 -0
  55. package/dist/electron/electron/agent/tools/image-tools.js +111 -0
  56. package/dist/electron/electron/agent/tools/mention-tools.js +282 -0
  57. package/dist/electron/electron/agent/tools/node-tools.js +476 -0
  58. package/dist/electron/electron/agent/tools/registry.js +2719 -0
  59. package/dist/electron/electron/agent/tools/search-tools.js +91 -0
  60. package/dist/electron/electron/agent/tools/shell-tools.js +574 -0
  61. package/dist/electron/electron/agent/tools/skill-tools.js +274 -0
  62. package/dist/electron/electron/agent/tools/system-tools.js +578 -0
  63. package/dist/electron/electron/agent/tools/web-fetch-tools.js +444 -0
  64. package/dist/electron/electron/agent/tools/x-tools.js +264 -0
  65. package/dist/electron/electron/agents/AgentRoleRepository.js +420 -0
  66. package/dist/electron/electron/agents/HeartbeatService.js +356 -0
  67. package/dist/electron/electron/agents/MentionRepository.js +197 -0
  68. package/dist/electron/electron/agents/TaskSubscriptionRepository.js +168 -0
  69. package/dist/electron/electron/agents/WorkingStateRepository.js +229 -0
  70. package/dist/electron/electron/canvas/canvas-manager.js +714 -0
  71. package/dist/electron/electron/canvas/canvas-preload.js +53 -0
  72. package/dist/electron/electron/canvas/canvas-protocol.js +195 -0
  73. package/dist/electron/electron/canvas/canvas-store.js +174 -0
  74. package/dist/electron/electron/canvas/index.js +13 -0
  75. package/dist/electron/electron/control-plane/client.js +364 -0
  76. package/dist/electron/electron/control-plane/handlers.js +572 -0
  77. package/dist/electron/electron/control-plane/index.js +41 -0
  78. package/dist/electron/electron/control-plane/node-manager.js +264 -0
  79. package/dist/electron/electron/control-plane/protocol.js +194 -0
  80. package/dist/electron/electron/control-plane/remote-client.js +437 -0
  81. package/dist/electron/electron/control-plane/server.js +640 -0
  82. package/dist/electron/electron/control-plane/settings.js +369 -0
  83. package/dist/electron/electron/control-plane/ssh-tunnel.js +549 -0
  84. package/dist/electron/electron/cron/index.js +30 -0
  85. package/dist/electron/electron/cron/schedule.js +190 -0
  86. package/dist/electron/electron/cron/service.js +614 -0
  87. package/dist/electron/electron/cron/store.js +155 -0
  88. package/dist/electron/electron/cron/types.js +82 -0
  89. package/dist/electron/electron/cron/webhook.js +258 -0
  90. package/dist/electron/electron/database/SecureSettingsRepository.js +444 -0
  91. package/dist/electron/electron/database/TaskLabelRepository.js +120 -0
  92. package/dist/electron/electron/database/repositories.js +1781 -0
  93. package/dist/electron/electron/database/schema.js +978 -0
  94. package/dist/electron/electron/extensions/index.js +33 -0
  95. package/dist/electron/electron/extensions/loader.js +313 -0
  96. package/dist/electron/electron/extensions/registry.js +485 -0
  97. package/dist/electron/electron/extensions/types.js +11 -0
  98. package/dist/electron/electron/gateway/channel-registry.js +1102 -0
  99. package/dist/electron/electron/gateway/channels/bluebubbles-client.js +479 -0
  100. package/dist/electron/electron/gateway/channels/bluebubbles.js +432 -0
  101. package/dist/electron/electron/gateway/channels/discord.js +975 -0
  102. package/dist/electron/electron/gateway/channels/email-client.js +593 -0
  103. package/dist/electron/electron/gateway/channels/email.js +443 -0
  104. package/dist/electron/electron/gateway/channels/google-chat.js +631 -0
  105. package/dist/electron/electron/gateway/channels/imessage-client.js +363 -0
  106. package/dist/electron/electron/gateway/channels/imessage.js +465 -0
  107. package/dist/electron/electron/gateway/channels/index.js +36 -0
  108. package/dist/electron/electron/gateway/channels/line-client.js +470 -0
  109. package/dist/electron/electron/gateway/channels/line.js +479 -0
  110. package/dist/electron/electron/gateway/channels/matrix-client.js +432 -0
  111. package/dist/electron/electron/gateway/channels/matrix.js +592 -0
  112. package/dist/electron/electron/gateway/channels/mattermost-client.js +394 -0
  113. package/dist/electron/electron/gateway/channels/mattermost.js +496 -0
  114. package/dist/electron/electron/gateway/channels/signal-client.js +500 -0
  115. package/dist/electron/electron/gateway/channels/signal.js +582 -0
  116. package/dist/electron/electron/gateway/channels/slack.js +415 -0
  117. package/dist/electron/electron/gateway/channels/teams.js +596 -0
  118. package/dist/electron/electron/gateway/channels/telegram.js +1390 -0
  119. package/dist/electron/electron/gateway/channels/twitch-client.js +502 -0
  120. package/dist/electron/electron/gateway/channels/twitch.js +396 -0
  121. package/dist/electron/electron/gateway/channels/types.js +8 -0
  122. package/dist/electron/electron/gateway/channels/whatsapp.js +953 -0
  123. package/dist/electron/electron/gateway/context-policy.js +268 -0
  124. package/dist/electron/electron/gateway/index.js +1063 -0
  125. package/dist/electron/electron/gateway/infrastructure.js +496 -0
  126. package/dist/electron/electron/gateway/router.js +2700 -0
  127. package/dist/electron/electron/gateway/security.js +375 -0
  128. package/dist/electron/electron/gateway/session.js +115 -0
  129. package/dist/electron/electron/gateway/tunnel.js +503 -0
  130. package/dist/electron/electron/guardrails/guardrail-manager.js +348 -0
  131. package/dist/electron/electron/hooks/gmail-watcher.js +300 -0
  132. package/dist/electron/electron/hooks/index.js +46 -0
  133. package/dist/electron/electron/hooks/mappings.js +381 -0
  134. package/dist/electron/electron/hooks/server.js +480 -0
  135. package/dist/electron/electron/hooks/settings.js +447 -0
  136. package/dist/electron/electron/hooks/types.js +41 -0
  137. package/dist/electron/electron/ipc/canvas-handlers.js +158 -0
  138. package/dist/electron/electron/ipc/handlers.js +3138 -0
  139. package/dist/electron/electron/ipc/mission-control-handlers.js +141 -0
  140. package/dist/electron/electron/main.js +448 -0
  141. package/dist/electron/electron/mcp/client/MCPClientManager.js +330 -0
  142. package/dist/electron/electron/mcp/client/MCPServerConnection.js +437 -0
  143. package/dist/electron/electron/mcp/client/transports/SSETransport.js +304 -0
  144. package/dist/electron/electron/mcp/client/transports/StdioTransport.js +307 -0
  145. package/dist/electron/electron/mcp/client/transports/WebSocketTransport.js +329 -0
  146. package/dist/electron/electron/mcp/host/MCPHostServer.js +354 -0
  147. package/dist/electron/electron/mcp/host/ToolAdapter.js +100 -0
  148. package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +497 -0
  149. package/dist/electron/electron/mcp/settings.js +446 -0
  150. package/dist/electron/electron/mcp/types.js +59 -0
  151. package/dist/electron/electron/memory/MemoryService.js +435 -0
  152. package/dist/electron/electron/notifications/index.js +17 -0
  153. package/dist/electron/electron/notifications/service.js +118 -0
  154. package/dist/electron/electron/notifications/store.js +144 -0
  155. package/dist/electron/electron/preload.js +842 -0
  156. package/dist/electron/electron/reports/StandupReportService.js +272 -0
  157. package/dist/electron/electron/security/concurrency.js +293 -0
  158. package/dist/electron/electron/security/index.js +15 -0
  159. package/dist/electron/electron/security/policy-manager.js +435 -0
  160. package/dist/electron/electron/settings/appearance-manager.js +193 -0
  161. package/dist/electron/electron/settings/personality-manager.js +724 -0
  162. package/dist/electron/electron/settings/x-manager.js +58 -0
  163. package/dist/electron/electron/tailscale/exposure.js +188 -0
  164. package/dist/electron/electron/tailscale/index.js +28 -0
  165. package/dist/electron/electron/tailscale/settings.js +205 -0
  166. package/dist/electron/electron/tailscale/tailscale.js +355 -0
  167. package/dist/electron/electron/tray/QuickInputWindow.js +568 -0
  168. package/dist/electron/electron/tray/TrayManager.js +895 -0
  169. package/dist/electron/electron/tray/index.js +9 -0
  170. package/dist/electron/electron/updater/index.js +6 -0
  171. package/dist/electron/electron/updater/update-manager.js +418 -0
  172. package/dist/electron/electron/utils/env-migration.js +209 -0
  173. package/dist/electron/electron/utils/process.js +102 -0
  174. package/dist/electron/electron/utils/rate-limiter.js +104 -0
  175. package/dist/electron/electron/utils/validation.js +419 -0
  176. package/dist/electron/electron/utils/x-cli.js +177 -0
  177. package/dist/electron/electron/voice/VoiceService.js +507 -0
  178. package/dist/electron/electron/voice/index.js +14 -0
  179. package/dist/electron/electron/voice/voice-settings-manager.js +359 -0
  180. package/dist/electron/shared/channelMessages.js +170 -0
  181. package/dist/electron/shared/types.js +1185 -0
  182. package/package.json +159 -0
  183. package/resources/skills/1password.json +10 -0
  184. package/resources/skills/add-documentation.json +31 -0
  185. package/resources/skills/analyze-csv.json +17 -0
  186. package/resources/skills/apple-notes.json +10 -0
  187. package/resources/skills/apple-reminders.json +10 -0
  188. package/resources/skills/auto-commenter.json +10 -0
  189. package/resources/skills/bear-notes.json +10 -0
  190. package/resources/skills/bird.json +35 -0
  191. package/resources/skills/blogwatcher.json +10 -0
  192. package/resources/skills/blucli.json +10 -0
  193. package/resources/skills/bluebubbles.json +10 -0
  194. package/resources/skills/camsnap.json +10 -0
  195. package/resources/skills/clean-imports.json +18 -0
  196. package/resources/skills/code-review.json +18 -0
  197. package/resources/skills/coding-agent.json +10 -0
  198. package/resources/skills/compare-files.json +23 -0
  199. package/resources/skills/convert-code.json +34 -0
  200. package/resources/skills/create-changelog.json +24 -0
  201. package/resources/skills/debug-error.json +17 -0
  202. package/resources/skills/dependency-check.json +10 -0
  203. package/resources/skills/discord.json +10 -0
  204. package/resources/skills/eightctl.json +10 -0
  205. package/resources/skills/explain-code.json +29 -0
  206. package/resources/skills/extract-todos.json +18 -0
  207. package/resources/skills/food-order.json +10 -0
  208. package/resources/skills/gemini.json +10 -0
  209. package/resources/skills/generate-readme.json +10 -0
  210. package/resources/skills/gifgrep.json +10 -0
  211. package/resources/skills/git-commit.json +10 -0
  212. package/resources/skills/github.json +10 -0
  213. package/resources/skills/gog.json +10 -0
  214. package/resources/skills/goplaces.json +10 -0
  215. package/resources/skills/himalaya.json +10 -0
  216. package/resources/skills/imsg.json +10 -0
  217. package/resources/skills/karpathy-guidelines.json +12 -0
  218. package/resources/skills/last30days.json +26 -0
  219. package/resources/skills/local-places.json +10 -0
  220. package/resources/skills/mcporter.json +10 -0
  221. package/resources/skills/model-usage.json +10 -0
  222. package/resources/skills/nano-banana-pro.json +10 -0
  223. package/resources/skills/nano-pdf.json +10 -0
  224. package/resources/skills/notion.json +10 -0
  225. package/resources/skills/obsidian.json +10 -0
  226. package/resources/skills/openai-image-gen.json +10 -0
  227. package/resources/skills/openai-whisper-api.json +10 -0
  228. package/resources/skills/openai-whisper.json +10 -0
  229. package/resources/skills/openhue.json +10 -0
  230. package/resources/skills/oracle.json +10 -0
  231. package/resources/skills/ordercli.json +10 -0
  232. package/resources/skills/peekaboo.json +10 -0
  233. package/resources/skills/project-structure.json +10 -0
  234. package/resources/skills/proofread.json +17 -0
  235. package/resources/skills/refactor-code.json +31 -0
  236. package/resources/skills/rename-symbol.json +23 -0
  237. package/resources/skills/sag.json +10 -0
  238. package/resources/skills/security-audit.json +18 -0
  239. package/resources/skills/session-logs.json +10 -0
  240. package/resources/skills/sherpa-onnx-tts.json +10 -0
  241. package/resources/skills/skill-creator.json +15 -0
  242. package/resources/skills/skill-hub.json +29 -0
  243. package/resources/skills/slack.json +10 -0
  244. package/resources/skills/songsee.json +10 -0
  245. package/resources/skills/sonoscli.json +10 -0
  246. package/resources/skills/spotify-player.json +10 -0
  247. package/resources/skills/startup-cfo.json +55 -0
  248. package/resources/skills/summarize-folder.json +18 -0
  249. package/resources/skills/summarize.json +10 -0
  250. package/resources/skills/things-mac.json +10 -0
  251. package/resources/skills/tmux.json +10 -0
  252. package/resources/skills/translate.json +36 -0
  253. package/resources/skills/trello.json +10 -0
  254. package/resources/skills/video-frames.json +10 -0
  255. package/resources/skills/voice-call.json +10 -0
  256. package/resources/skills/wacli.json +10 -0
  257. package/resources/skills/weather.json +10 -0
  258. package/resources/skills/write-tests.json +31 -0
  259. package/src/electron/activity/ActivityRepository.ts +238 -0
  260. package/src/electron/agent/browser/browser-service.ts +721 -0
  261. package/src/electron/agent/context-manager.ts +257 -0
  262. package/src/electron/agent/custom-skill-loader.ts +634 -0
  263. package/src/electron/agent/daemon.ts +1097 -0
  264. package/src/electron/agent/executor.ts +4017 -0
  265. package/src/electron/agent/llm/anthropic-provider.ts +175 -0
  266. package/src/electron/agent/llm/bedrock-provider.ts +236 -0
  267. package/src/electron/agent/llm/gemini-provider.ts +422 -0
  268. package/src/electron/agent/llm/index.ts +9 -0
  269. package/src/electron/agent/llm/ollama-provider.ts +347 -0
  270. package/src/electron/agent/llm/openai-oauth.ts +127 -0
  271. package/src/electron/agent/llm/openai-provider.ts +686 -0
  272. package/src/electron/agent/llm/openrouter-provider.ts +273 -0
  273. package/src/electron/agent/llm/pricing.ts +180 -0
  274. package/src/electron/agent/llm/provider-factory.ts +971 -0
  275. package/src/electron/agent/llm/types.ts +291 -0
  276. package/src/electron/agent/queue-manager.ts +408 -0
  277. package/src/electron/agent/sandbox/docker-sandbox.ts +453 -0
  278. package/src/electron/agent/sandbox/macos-sandbox.ts +426 -0
  279. package/src/electron/agent/sandbox/runner.ts +453 -0
  280. package/src/electron/agent/sandbox/sandbox-factory.ts +337 -0
  281. package/src/electron/agent/sandbox/security-utils.ts +251 -0
  282. package/src/electron/agent/search/brave-provider.ts +141 -0
  283. package/src/electron/agent/search/google-provider.ts +131 -0
  284. package/src/electron/agent/search/index.ts +6 -0
  285. package/src/electron/agent/search/provider-factory.ts +450 -0
  286. package/src/electron/agent/search/serpapi-provider.ts +138 -0
  287. package/src/electron/agent/search/tavily-provider.ts +108 -0
  288. package/src/electron/agent/search/types.ts +118 -0
  289. package/src/electron/agent/security/index.ts +20 -0
  290. package/src/electron/agent/security/input-sanitizer.ts +380 -0
  291. package/src/electron/agent/security/output-filter.ts +259 -0
  292. package/src/electron/agent/skill-eligibility.ts +334 -0
  293. package/src/electron/agent/skill-registry.ts +457 -0
  294. package/src/electron/agent/skills/document.ts +1070 -0
  295. package/src/electron/agent/skills/image-generator.ts +272 -0
  296. package/src/electron/agent/skills/organizer.ts +131 -0
  297. package/src/electron/agent/skills/presentation.ts +418 -0
  298. package/src/electron/agent/skills/spreadsheet.ts +166 -0
  299. package/src/electron/agent/tools/browser-tools.ts +546 -0
  300. package/src/electron/agent/tools/builtin-settings.ts +422 -0
  301. package/src/electron/agent/tools/canvas-tools.ts +572 -0
  302. package/src/electron/agent/tools/cron-tools.ts +723 -0
  303. package/src/electron/agent/tools/edit-tools.ts +196 -0
  304. package/src/electron/agent/tools/file-tools.ts +811 -0
  305. package/src/electron/agent/tools/glob-tools.ts +303 -0
  306. package/src/electron/agent/tools/grep-tools.ts +432 -0
  307. package/src/electron/agent/tools/image-tools.ts +126 -0
  308. package/src/electron/agent/tools/mention-tools.ts +371 -0
  309. package/src/electron/agent/tools/node-tools.ts +550 -0
  310. package/src/electron/agent/tools/registry.ts +3052 -0
  311. package/src/electron/agent/tools/search-tools.ts +111 -0
  312. package/src/electron/agent/tools/shell-tools.ts +651 -0
  313. package/src/electron/agent/tools/skill-tools.ts +340 -0
  314. package/src/electron/agent/tools/system-tools.ts +665 -0
  315. package/src/electron/agent/tools/web-fetch-tools.ts +528 -0
  316. package/src/electron/agent/tools/x-tools.ts +267 -0
  317. package/src/electron/agents/AgentRoleRepository.ts +557 -0
  318. package/src/electron/agents/HeartbeatService.ts +469 -0
  319. package/src/electron/agents/MentionRepository.ts +242 -0
  320. package/src/electron/agents/TaskSubscriptionRepository.ts +231 -0
  321. package/src/electron/agents/WorkingStateRepository.ts +278 -0
  322. package/src/electron/canvas/canvas-manager.ts +818 -0
  323. package/src/electron/canvas/canvas-preload.ts +102 -0
  324. package/src/electron/canvas/canvas-protocol.ts +174 -0
  325. package/src/electron/canvas/canvas-store.ts +200 -0
  326. package/src/electron/canvas/index.ts +8 -0
  327. package/src/electron/control-plane/client.ts +527 -0
  328. package/src/electron/control-plane/handlers.ts +723 -0
  329. package/src/electron/control-plane/index.ts +51 -0
  330. package/src/electron/control-plane/node-manager.ts +322 -0
  331. package/src/electron/control-plane/protocol.ts +269 -0
  332. package/src/electron/control-plane/remote-client.ts +517 -0
  333. package/src/electron/control-plane/server.ts +853 -0
  334. package/src/electron/control-plane/settings.ts +401 -0
  335. package/src/electron/control-plane/ssh-tunnel.ts +624 -0
  336. package/src/electron/cron/index.ts +9 -0
  337. package/src/electron/cron/schedule.ts +217 -0
  338. package/src/electron/cron/service.ts +743 -0
  339. package/src/electron/cron/store.ts +165 -0
  340. package/src/electron/cron/types.ts +291 -0
  341. package/src/electron/cron/webhook.ts +303 -0
  342. package/src/electron/database/SecureSettingsRepository.ts +514 -0
  343. package/src/electron/database/TaskLabelRepository.ts +148 -0
  344. package/src/electron/database/repositories.ts +2397 -0
  345. package/src/electron/database/schema.ts +1017 -0
  346. package/src/electron/extensions/index.ts +18 -0
  347. package/src/electron/extensions/loader.ts +336 -0
  348. package/src/electron/extensions/registry.ts +546 -0
  349. package/src/electron/extensions/types.ts +372 -0
  350. package/src/electron/gateway/channel-registry.ts +1267 -0
  351. package/src/electron/gateway/channels/bluebubbles-client.ts +641 -0
  352. package/src/electron/gateway/channels/bluebubbles.ts +509 -0
  353. package/src/electron/gateway/channels/discord.ts +1150 -0
  354. package/src/electron/gateway/channels/email-client.ts +708 -0
  355. package/src/electron/gateway/channels/email.ts +516 -0
  356. package/src/electron/gateway/channels/google-chat.ts +760 -0
  357. package/src/electron/gateway/channels/imessage-client.ts +473 -0
  358. package/src/electron/gateway/channels/imessage.ts +520 -0
  359. package/src/electron/gateway/channels/index.ts +21 -0
  360. package/src/electron/gateway/channels/line-client.ts +598 -0
  361. package/src/electron/gateway/channels/line.ts +559 -0
  362. package/src/electron/gateway/channels/matrix-client.ts +632 -0
  363. package/src/electron/gateway/channels/matrix.ts +655 -0
  364. package/src/electron/gateway/channels/mattermost-client.ts +526 -0
  365. package/src/electron/gateway/channels/mattermost.ts +550 -0
  366. package/src/electron/gateway/channels/signal-client.ts +722 -0
  367. package/src/electron/gateway/channels/signal.ts +666 -0
  368. package/src/electron/gateway/channels/slack.ts +458 -0
  369. package/src/electron/gateway/channels/teams.ts +681 -0
  370. package/src/electron/gateway/channels/telegram.ts +1727 -0
  371. package/src/electron/gateway/channels/twitch-client.ts +665 -0
  372. package/src/electron/gateway/channels/twitch.ts +468 -0
  373. package/src/electron/gateway/channels/types.ts +1002 -0
  374. package/src/electron/gateway/channels/whatsapp.ts +1101 -0
  375. package/src/electron/gateway/context-policy.ts +382 -0
  376. package/src/electron/gateway/index.ts +1274 -0
  377. package/src/electron/gateway/infrastructure.ts +645 -0
  378. package/src/electron/gateway/router.ts +3206 -0
  379. package/src/electron/gateway/security.ts +422 -0
  380. package/src/electron/gateway/session.ts +144 -0
  381. package/src/electron/gateway/tunnel.ts +626 -0
  382. package/src/electron/guardrails/guardrail-manager.ts +380 -0
  383. package/src/electron/hooks/gmail-watcher.ts +355 -0
  384. package/src/electron/hooks/index.ts +30 -0
  385. package/src/electron/hooks/mappings.ts +404 -0
  386. package/src/electron/hooks/server.ts +574 -0
  387. package/src/electron/hooks/settings.ts +466 -0
  388. package/src/electron/hooks/types.ts +245 -0
  389. package/src/electron/ipc/canvas-handlers.ts +223 -0
  390. package/src/electron/ipc/handlers.ts +3661 -0
  391. package/src/electron/ipc/mission-control-handlers.ts +182 -0
  392. package/src/electron/main.ts +496 -0
  393. package/src/electron/mcp/client/MCPClientManager.ts +406 -0
  394. package/src/electron/mcp/client/MCPServerConnection.ts +514 -0
  395. package/src/electron/mcp/client/transports/SSETransport.ts +360 -0
  396. package/src/electron/mcp/client/transports/StdioTransport.ts +355 -0
  397. package/src/electron/mcp/client/transports/WebSocketTransport.ts +384 -0
  398. package/src/electron/mcp/host/MCPHostServer.ts +388 -0
  399. package/src/electron/mcp/host/ToolAdapter.ts +140 -0
  400. package/src/electron/mcp/registry/MCPRegistryManager.ts +565 -0
  401. package/src/electron/mcp/settings.ts +468 -0
  402. package/src/electron/mcp/types.ts +371 -0
  403. package/src/electron/memory/MemoryService.ts +523 -0
  404. package/src/electron/notifications/index.ts +16 -0
  405. package/src/electron/notifications/service.ts +161 -0
  406. package/src/electron/notifications/store.ts +163 -0
  407. package/src/electron/preload.ts +2845 -0
  408. package/src/electron/reports/StandupReportService.ts +356 -0
  409. package/src/electron/security/concurrency.ts +333 -0
  410. package/src/electron/security/index.ts +17 -0
  411. package/src/electron/security/policy-manager.ts +539 -0
  412. package/src/electron/settings/appearance-manager.ts +182 -0
  413. package/src/electron/settings/personality-manager.ts +800 -0
  414. package/src/electron/settings/x-manager.ts +62 -0
  415. package/src/electron/tailscale/exposure.ts +262 -0
  416. package/src/electron/tailscale/index.ts +34 -0
  417. package/src/electron/tailscale/settings.ts +218 -0
  418. package/src/electron/tailscale/tailscale.ts +379 -0
  419. package/src/electron/tray/QuickInputWindow.ts +609 -0
  420. package/src/electron/tray/TrayManager.ts +1005 -0
  421. package/src/electron/tray/index.ts +6 -0
  422. package/src/electron/updater/index.ts +1 -0
  423. package/src/electron/updater/update-manager.ts +447 -0
  424. package/src/electron/utils/env-migration.ts +203 -0
  425. package/src/electron/utils/process.ts +124 -0
  426. package/src/electron/utils/rate-limiter.ts +130 -0
  427. package/src/electron/utils/validation.ts +493 -0
  428. package/src/electron/utils/x-cli.ts +198 -0
  429. package/src/electron/voice/VoiceService.ts +583 -0
  430. package/src/electron/voice/index.ts +9 -0
  431. package/src/electron/voice/voice-settings-manager.ts +403 -0
  432. package/src/renderer/App.tsx +775 -0
  433. package/src/renderer/components/ActivityFeed.tsx +407 -0
  434. package/src/renderer/components/ActivityFeedItem.tsx +285 -0
  435. package/src/renderer/components/AgentRoleCard.tsx +343 -0
  436. package/src/renderer/components/AgentRoleEditor.tsx +805 -0
  437. package/src/renderer/components/AgentSquadSettings.tsx +295 -0
  438. package/src/renderer/components/AgentWorkingStatePanel.tsx +411 -0
  439. package/src/renderer/components/AppearanceSettings.tsx +122 -0
  440. package/src/renderer/components/ApprovalDialog.tsx +100 -0
  441. package/src/renderer/components/BlueBubblesSettings.tsx +505 -0
  442. package/src/renderer/components/BuiltinToolsSettings.tsx +307 -0
  443. package/src/renderer/components/CanvasPreview.tsx +1189 -0
  444. package/src/renderer/components/CommandOutput.tsx +202 -0
  445. package/src/renderer/components/ContextPolicySettings.tsx +523 -0
  446. package/src/renderer/components/ControlPlaneSettings.tsx +1134 -0
  447. package/src/renderer/components/DisclaimerModal.tsx +124 -0
  448. package/src/renderer/components/DiscordSettings.tsx +436 -0
  449. package/src/renderer/components/EmailSettings.tsx +606 -0
  450. package/src/renderer/components/ExtensionsSettings.tsx +542 -0
  451. package/src/renderer/components/FileViewer.tsx +224 -0
  452. package/src/renderer/components/GoogleChatSettings.tsx +535 -0
  453. package/src/renderer/components/GuardrailSettings.tsx +487 -0
  454. package/src/renderer/components/HooksSettings.tsx +581 -0
  455. package/src/renderer/components/ImessageSettings.tsx +484 -0
  456. package/src/renderer/components/LineSettings.tsx +483 -0
  457. package/src/renderer/components/MCPRegistryBrowser.tsx +386 -0
  458. package/src/renderer/components/MCPSettings.tsx +943 -0
  459. package/src/renderer/components/MainContent.tsx +2433 -0
  460. package/src/renderer/components/MatrixSettings.tsx +510 -0
  461. package/src/renderer/components/MattermostSettings.tsx +473 -0
  462. package/src/renderer/components/MemorySettings.tsx +247 -0
  463. package/src/renderer/components/MentionBadge.tsx +87 -0
  464. package/src/renderer/components/MentionInput.tsx +409 -0
  465. package/src/renderer/components/MentionList.tsx +476 -0
  466. package/src/renderer/components/MissionControlPanel.tsx +1995 -0
  467. package/src/renderer/components/NodesSettings.tsx +316 -0
  468. package/src/renderer/components/NotificationPanel.tsx +481 -0
  469. package/src/renderer/components/Onboarding/AwakeningOrb.tsx +44 -0
  470. package/src/renderer/components/Onboarding/Onboarding.tsx +443 -0
  471. package/src/renderer/components/Onboarding/TypewriterText.tsx +102 -0
  472. package/src/renderer/components/Onboarding/index.ts +3 -0
  473. package/src/renderer/components/OnboardingModal.tsx +698 -0
  474. package/src/renderer/components/PairingCodeDisplay.tsx +324 -0
  475. package/src/renderer/components/PersonalitySettings.tsx +597 -0
  476. package/src/renderer/components/QueueSettings.tsx +119 -0
  477. package/src/renderer/components/QuickTaskFAB.tsx +71 -0
  478. package/src/renderer/components/RightPanel.tsx +413 -0
  479. package/src/renderer/components/ScheduledTasksSettings.tsx +1328 -0
  480. package/src/renderer/components/SearchSettings.tsx +328 -0
  481. package/src/renderer/components/Settings.tsx +1504 -0
  482. package/src/renderer/components/Sidebar.tsx +344 -0
  483. package/src/renderer/components/SignalSettings.tsx +673 -0
  484. package/src/renderer/components/SkillHubBrowser.tsx +458 -0
  485. package/src/renderer/components/SkillParameterModal.tsx +185 -0
  486. package/src/renderer/components/SkillsSettings.tsx +451 -0
  487. package/src/renderer/components/SlackSettings.tsx +442 -0
  488. package/src/renderer/components/StandupReportViewer.tsx +614 -0
  489. package/src/renderer/components/TaskBoard.tsx +498 -0
  490. package/src/renderer/components/TaskBoardCard.tsx +357 -0
  491. package/src/renderer/components/TaskBoardColumn.tsx +211 -0
  492. package/src/renderer/components/TaskLabelManager.tsx +472 -0
  493. package/src/renderer/components/TaskQueuePanel.tsx +144 -0
  494. package/src/renderer/components/TaskQuickActions.tsx +492 -0
  495. package/src/renderer/components/TaskTimeline.tsx +216 -0
  496. package/src/renderer/components/TaskView.tsx +162 -0
  497. package/src/renderer/components/TeamsSettings.tsx +518 -0
  498. package/src/renderer/components/TelegramSettings.tsx +421 -0
  499. package/src/renderer/components/Toast.tsx +76 -0
  500. package/src/renderer/components/TraySettings.tsx +189 -0
  501. package/src/renderer/components/TwitchSettings.tsx +511 -0
  502. package/src/renderer/components/UpdateSettings.tsx +295 -0
  503. package/src/renderer/components/VoiceIndicator.tsx +270 -0
  504. package/src/renderer/components/VoiceSettings.tsx +867 -0
  505. package/src/renderer/components/WhatsAppSettings.tsx +721 -0
  506. package/src/renderer/components/WorkingStateEditor.tsx +309 -0
  507. package/src/renderer/components/WorkingStateHistory.tsx +481 -0
  508. package/src/renderer/components/WorkspaceSelector.tsx +150 -0
  509. package/src/renderer/components/XSettings.tsx +311 -0
  510. package/src/renderer/global.d.ts +9 -0
  511. package/src/renderer/hooks/useAgentContext.ts +153 -0
  512. package/src/renderer/hooks/useOnboardingFlow.ts +548 -0
  513. package/src/renderer/hooks/useVoiceInput.ts +268 -0
  514. package/src/renderer/index.html +12 -0
  515. package/src/renderer/main.tsx +10 -0
  516. package/src/renderer/public/cowork-os-logo.png +0 -0
  517. package/src/renderer/quick-input.html +164 -0
  518. package/src/renderer/styles/index.css +14504 -0
  519. package/src/renderer/utils/agentMessages.ts +749 -0
  520. package/src/renderer/utils/voice-directives.ts +169 -0
  521. package/src/shared/channelMessages.ts +213 -0
  522. package/src/shared/types.ts +3608 -0
  523. package/tsconfig.electron.json +26 -0
  524. package/tsconfig.json +26 -0
  525. package/tsconfig.node.json +10 -0
  526. package/vite.config.ts +23 -0
@@ -0,0 +1,1504 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { LLMSettingsData, ThemeMode, AccentColor } from '../../shared/types';
3
+ import { TelegramSettings } from './TelegramSettings';
4
+ import { DiscordSettings } from './DiscordSettings';
5
+ import { SlackSettings } from './SlackSettings';
6
+ import { WhatsAppSettings } from './WhatsAppSettings';
7
+ import { ImessageSettings } from './ImessageSettings';
8
+ import { SignalSettings } from './SignalSettings';
9
+ import { MattermostSettings } from './MattermostSettings';
10
+ import { MatrixSettings } from './MatrixSettings';
11
+ import { TwitchSettings } from './TwitchSettings';
12
+ import { LineSettings } from './LineSettings';
13
+ import { BlueBubblesSettings } from './BlueBubblesSettings';
14
+ import { EmailSettings } from './EmailSettings';
15
+ import { TeamsSettings } from './TeamsSettings';
16
+ import { GoogleChatSettings } from './GoogleChatSettings';
17
+ import { XSettings } from './XSettings';
18
+ import { SearchSettings } from './SearchSettings';
19
+ import { UpdateSettings } from './UpdateSettings';
20
+ import { GuardrailSettings } from './GuardrailSettings';
21
+ import { AppearanceSettings } from './AppearanceSettings';
22
+ import { QueueSettings } from './QueueSettings';
23
+ import { SkillsSettings } from './SkillsSettings';
24
+ import { SkillHubBrowser } from './SkillHubBrowser';
25
+ import { MCPSettings } from './MCPSettings';
26
+ import { BuiltinToolsSettings } from './BuiltinToolsSettings';
27
+ import { TraySettings } from './TraySettings';
28
+ import { ScheduledTasksSettings } from './ScheduledTasksSettings';
29
+ import { HooksSettings } from './HooksSettings';
30
+ import { ControlPlaneSettings } from './ControlPlaneSettings';
31
+ import { PersonalitySettings } from './PersonalitySettings';
32
+ import { NodesSettings } from './NodesSettings';
33
+ import { ExtensionsSettings } from './ExtensionsSettings';
34
+ import { VoiceSettings } from './VoiceSettings';
35
+ import { MissionControlPanel } from './MissionControlPanel';
36
+
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';
38
+
39
+ // Secondary channels shown inside "More Channels" tab
40
+ type SecondaryChannel = 'discord' | 'imessage' | 'signal' | 'mattermost' | 'matrix' | 'twitch' | 'line' | 'bluebubbles' | 'email' | 'googlechat' | 'x';
41
+
42
+ interface SettingsProps {
43
+ onBack: () => void;
44
+ onSettingsChanged?: () => void;
45
+ themeMode: ThemeMode;
46
+ accentColor: AccentColor;
47
+ onThemeChange: (theme: ThemeMode) => void;
48
+ onAccentChange: (accent: AccentColor) => void;
49
+ initialTab?: SettingsTab;
50
+ onShowOnboarding?: () => void;
51
+ onboardingCompletedAt?: string;
52
+ }
53
+
54
+ interface ModelOption {
55
+ key: string;
56
+ displayName: string;
57
+ }
58
+
59
+ interface ProviderInfo {
60
+ type: string;
61
+ name: string;
62
+ configured: boolean;
63
+ }
64
+
65
+ // Helper to format bytes to human-readable size
66
+ function formatBytes(bytes: number): string {
67
+ if (bytes === 0) return '0 B';
68
+ const k = 1024;
69
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
70
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
71
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
72
+ }
73
+
74
+ // Searchable Select Component
75
+ interface SearchableSelectOption {
76
+ value: string;
77
+ label: string;
78
+ description?: string;
79
+ }
80
+
81
+ interface SearchableSelectProps {
82
+ options: SearchableSelectOption[];
83
+ value: string;
84
+ onChange: (value: string) => void;
85
+ placeholder?: string;
86
+ className?: string;
87
+ }
88
+
89
+ function SearchableSelect({ options, value, onChange, placeholder = 'Select...', className = '' }: SearchableSelectProps) {
90
+ const [isOpen, setIsOpen] = useState(false);
91
+ const [search, setSearch] = useState('');
92
+ const [highlightedIndex, setHighlightedIndex] = useState(0);
93
+ const containerRef = useRef<HTMLDivElement>(null);
94
+ const inputRef = useRef<HTMLInputElement>(null);
95
+ const listRef = useRef<HTMLDivElement>(null);
96
+
97
+ const selectedOption = options.find(opt => opt.value === value);
98
+
99
+ const filteredOptions = options.filter(opt =>
100
+ opt.label.toLowerCase().includes(search.toLowerCase()) ||
101
+ opt.value.toLowerCase().includes(search.toLowerCase()) ||
102
+ (opt.description && opt.description.toLowerCase().includes(search.toLowerCase()))
103
+ );
104
+
105
+ // Reset highlighted index when search changes
106
+ useEffect(() => {
107
+ setHighlightedIndex(0);
108
+ }, [search]);
109
+
110
+ // Scroll highlighted option into view
111
+ useEffect(() => {
112
+ if (isOpen && listRef.current) {
113
+ const highlightedEl = listRef.current.querySelector(`[data-index="${highlightedIndex}"]`);
114
+ if (highlightedEl) {
115
+ highlightedEl.scrollIntoView({ block: 'nearest' });
116
+ }
117
+ }
118
+ }, [highlightedIndex, isOpen]);
119
+
120
+ // Close on click outside
121
+ useEffect(() => {
122
+ const handleClickOutside = (e: MouseEvent) => {
123
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
124
+ setIsOpen(false);
125
+ setSearch('');
126
+ }
127
+ };
128
+ document.addEventListener('mousedown', handleClickOutside);
129
+ return () => document.removeEventListener('mousedown', handleClickOutside);
130
+ }, []);
131
+
132
+ const handleKeyDown = (e: React.KeyboardEvent) => {
133
+ if (!isOpen) {
134
+ if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
135
+ e.preventDefault();
136
+ setIsOpen(true);
137
+ }
138
+ return;
139
+ }
140
+
141
+ switch (e.key) {
142
+ case 'ArrowDown':
143
+ e.preventDefault();
144
+ setHighlightedIndex(i => Math.min(i + 1, filteredOptions.length - 1));
145
+ break;
146
+ case 'ArrowUp':
147
+ e.preventDefault();
148
+ setHighlightedIndex(i => Math.max(i - 1, 0));
149
+ break;
150
+ case 'Enter':
151
+ e.preventDefault();
152
+ if (filteredOptions[highlightedIndex]) {
153
+ onChange(filteredOptions[highlightedIndex].value);
154
+ setIsOpen(false);
155
+ setSearch('');
156
+ }
157
+ break;
158
+ case 'Escape':
159
+ e.preventDefault();
160
+ setIsOpen(false);
161
+ setSearch('');
162
+ break;
163
+ }
164
+ };
165
+
166
+ const handleSelect = (optionValue: string) => {
167
+ onChange(optionValue);
168
+ setIsOpen(false);
169
+ setSearch('');
170
+ };
171
+
172
+ return (
173
+ <div ref={containerRef} className={`searchable-select ${className}`}>
174
+ <div
175
+ className={`searchable-select-trigger ${isOpen ? 'open' : ''}`}
176
+ onClick={() => {
177
+ setIsOpen(!isOpen);
178
+ if (!isOpen) {
179
+ setTimeout(() => inputRef.current?.focus(), 0);
180
+ }
181
+ }}
182
+ onKeyDown={handleKeyDown}
183
+ tabIndex={0}
184
+ >
185
+ <span className="searchable-select-value">
186
+ {selectedOption ? selectedOption.label : placeholder}
187
+ </span>
188
+ <svg className="searchable-select-arrow" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
189
+ <path d="M6 9l6 6 6-6" />
190
+ </svg>
191
+ </div>
192
+
193
+ {isOpen && (
194
+ <div className="searchable-select-dropdown">
195
+ <div className="searchable-select-search">
196
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
197
+ <circle cx="11" cy="11" r="8" />
198
+ <path d="M21 21l-4.35-4.35" />
199
+ </svg>
200
+ <input
201
+ ref={inputRef}
202
+ type="text"
203
+ value={search}
204
+ onChange={(e) => setSearch(e.target.value)}
205
+ onKeyDown={handleKeyDown}
206
+ placeholder="Search models..."
207
+ autoFocus
208
+ />
209
+ </div>
210
+ <div ref={listRef} className="searchable-select-options">
211
+ {filteredOptions.length === 0 ? (
212
+ <div className="searchable-select-no-results">No models found</div>
213
+ ) : (
214
+ filteredOptions.map((opt, index) => (
215
+ <div
216
+ key={opt.value}
217
+ data-index={index}
218
+ className={`searchable-select-option ${opt.value === value ? 'selected' : ''} ${index === highlightedIndex ? 'highlighted' : ''}`}
219
+ onClick={() => handleSelect(opt.value)}
220
+ onMouseEnter={() => setHighlightedIndex(index)}
221
+ >
222
+ <span className="searchable-select-option-label">{opt.label}</span>
223
+ {opt.description && (
224
+ <span className="searchable-select-option-desc">{opt.description}</span>
225
+ )}
226
+ </div>
227
+ ))
228
+ )}
229
+ </div>
230
+ </div>
231
+ )}
232
+ </div>
233
+ );
234
+ }
235
+
236
+ // Sidebar navigation items configuration
237
+ const sidebarItems: Array<{ tab: SettingsTab; label: string; icon: React.ReactNode; macOnly?: boolean }> = [
238
+ { tab: 'appearance', label: 'Appearance', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="5" /><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" /></svg> },
239
+ { tab: 'personality', label: 'Personality', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="8" r="5" /><path d="M20 21a8 8 0 0 0-16 0" /></svg> },
240
+ { tab: 'missioncontrol', label: 'Mission Control', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="9" cy="7" r="4" /><path d="M17 7a4 4 0 0 1 0 8" /><path d="M9 15a7 7 0 0 0-7 7h14a7 7 0 0 0-7-7z" /><rect x="14" y="14" width="8" height="8" rx="1" /></svg> },
241
+ { tab: 'tray', label: 'Menu Bar', 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="4" rx="1" /><path d="M12 7v4M8 11h8" /><rect x="4" y="14" width="16" height="7" rx="1" /></svg>, macOnly: true },
242
+ { tab: 'voice', label: 'Voice Mode', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" /><path d="M19 10v2a7 7 0 0 1-14 0v-2" /><line x1="12" y1="19" x2="12" y2="23" /><line x1="8" y1="23" x2="16" y2="23" /></svg> },
243
+ { tab: 'llm', label: 'LLM Provider', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" /></svg> },
244
+ { tab: 'search', label: 'Web Search', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="11" cy="11" r="8" /><path d="M21 21l-4.35-4.35" /></svg> },
245
+ { tab: 'whatsapp', label: 'WhatsApp', 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> },
246
+ { 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
+ { 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
+ { 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> },
249
+ { 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> },
250
+ { 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
+ { 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
+ { 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
+ { 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
+ { 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> },
255
+ { 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
+ { 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
+ { 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> },
258
+ { tab: 'controlplane', label: 'Control Plane', 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" ry="2" /><line x1="8" y1="21" x2="16" y2="21" /><line x1="12" y1="17" x2="12" y2="21" /></svg> },
259
+ { tab: 'nodes', label: 'Mobile Companions', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="5" y="2" width="14" height="20" rx="2" ry="2" /><line x1="12" y1="18" x2="12.01" y2="18" /></svg> },
260
+ { tab: 'extensions', label: 'Extensions', 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> },
261
+ { tab: 'updates', label: 'Updates', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 12a9 9 0 11-6.219-8.56" /><polyline points="21 3 21 9 15 9" /></svg> },
262
+ ];
263
+
264
+ // Secondary channel configuration for "More Channels" tab
265
+ const secondaryChannelItems: Array<{ key: SecondaryChannel; label: string; icon: React.ReactNode }> = [
266
+ { key: 'discord', label: 'Discord', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" /></svg> },
267
+ { key: 'imessage', label: 'iMessage', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" /><circle cx="9" cy="10" r="1" fill="currentColor" /><circle cx="15" cy="10" r="1" fill="currentColor" /></svg> },
268
+ { key: 'signal', label: 'Signal', icon: <svg width="16" height="16" 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" /><path d="M9 12l2 2 4-4" /></svg> },
269
+ { key: 'line', label: 'LINE', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 2C6.48 2 2 5.92 2 10.73c0 3.21 2.11 6.01 5.24 7.52-.06.5-.32 1.83-.37 2.11 0 0-.08.29.15.4.23.11.49.01.49.01 3.1-2.05 3.59-2.32 4.49-2.32 5.52 0 10-3.92 10-8.72C22 5.92 17.52 2 12 2z" /></svg> },
270
+ { key: 'email', label: 'Email', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="2" y="4" width="20" height="16" rx="2" /><path d="M22 6l-10 7L2 6" /></svg> },
271
+ { key: 'googlechat', label: 'Google Chat', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" /><circle cx="8" cy="10" r="1" fill="currentColor" /><circle cx="12" cy="10" r="1" fill="currentColor" /><circle cx="16" cy="10" r="1" fill="currentColor" /></svg> },
272
+ { key: 'mattermost', label: 'Mattermost', 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="2" /><path d="M9 9h6v6H9z" /></svg> },
273
+ { 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
+ { 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
+ { 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
+ ];
278
+
279
+ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, onThemeChange, onAccentChange, initialTab = 'appearance', onShowOnboarding, onboardingCompletedAt }: SettingsProps) {
280
+ const [activeTab, setActiveTab] = useState<SettingsTab>(initialTab);
281
+ const [activeSecondaryChannel, setActiveSecondaryChannel] = useState<SecondaryChannel>('discord');
282
+ const [sidebarSearch, setSidebarSearch] = useState('');
283
+ const [settings, setSettings] = useState<LLMSettingsData>({
284
+ providerType: 'anthropic',
285
+ modelKey: 'sonnet-3-5',
286
+ });
287
+ const [models, setModels] = useState<ModelOption[]>([]);
288
+ const [providers, setProviders] = useState<ProviderInfo[]>([]);
289
+ const [loading, setLoading] = useState(true);
290
+ const [saving, setSaving] = useState(false);
291
+ const [testing, setTesting] = useState(false);
292
+ const [testResult, setTestResult] = useState<{ success: boolean; error?: string } | null>(null);
293
+
294
+ // Form state for credentials (not persisted directly)
295
+ const [anthropicApiKey, setAnthropicApiKey] = useState('');
296
+ const [awsRegion, setAwsRegion] = useState('us-east-1');
297
+ const [awsAccessKeyId, setAwsAccessKeyId] = useState('');
298
+ const [awsSecretAccessKey, setAwsSecretAccessKey] = useState('');
299
+ const [awsProfile, setAwsProfile] = useState('');
300
+ const [useDefaultCredentials, setUseDefaultCredentials] = useState(true);
301
+
302
+ // Ollama state
303
+ const [ollamaBaseUrl, setOllamaBaseUrl] = useState('http://localhost:11434');
304
+ const [ollamaModel, setOllamaModel] = useState('llama3.2');
305
+ const [ollamaApiKey, setOllamaApiKey] = useState('');
306
+ const [ollamaModels, setOllamaModels] = useState<Array<{ name: string; size: number }>>([]);
307
+ const [loadingOllamaModels, setLoadingOllamaModels] = useState(false);
308
+
309
+ // Gemini state
310
+ const [geminiApiKey, setGeminiApiKey] = useState('');
311
+ const [geminiModel, setGeminiModel] = useState('gemini-2.0-flash');
312
+ const [geminiModels, setGeminiModels] = useState<Array<{ name: string; displayName: string; description: string }>>([]);
313
+ const [loadingGeminiModels, setLoadingGeminiModels] = useState(false);
314
+
315
+ // OpenRouter state
316
+ const [openrouterApiKey, setOpenrouterApiKey] = useState('');
317
+ const [openrouterModel, setOpenrouterModel] = useState('anthropic/claude-3.5-sonnet');
318
+ const [openrouterModels, setOpenrouterModels] = useState<Array<{ id: string; name: string; context_length: number }>>([]);
319
+ const [loadingOpenRouterModels, setLoadingOpenRouterModels] = useState(false);
320
+
321
+ // OpenAI state
322
+ const [openaiApiKey, setOpenaiApiKey] = useState('');
323
+ const [openaiModel, setOpenaiModel] = useState('gpt-4o-mini');
324
+ const [openaiModels, setOpenaiModels] = useState<Array<{ id: string; name: string; description: string }>>([]);
325
+ const [loadingOpenAIModels, setLoadingOpenAIModels] = useState(false);
326
+ const [openaiAuthMethod, setOpenaiAuthMethod] = useState<'api_key' | 'oauth'>('api_key');
327
+ const [openaiOAuthConnected, setOpenaiOAuthConnected] = useState(false);
328
+ const [openaiOAuthLoading, setOpenaiOAuthLoading] = useState(false);
329
+
330
+ // Bedrock state
331
+ const [bedrockModel, setBedrockModel] = useState('');
332
+ const [bedrockModels, setBedrockModels] = useState<Array<{ id: string; name: string; description: string }>>([]);
333
+ const [loadingBedrockModels, setLoadingBedrockModels] = useState(false);
334
+
335
+ useEffect(() => {
336
+ loadConfigStatus();
337
+ }, []);
338
+
339
+ const loadConfigStatus = async () => {
340
+ try {
341
+ setLoading(true);
342
+ // Load config status which includes settings, providers, and models
343
+ const configStatus = await window.electronAPI.getLLMConfigStatus();
344
+
345
+ // Set providers
346
+ setProviders(configStatus.providers || []);
347
+ setModels(configStatus.models || []);
348
+
349
+ // Load full settings separately for bedrock config
350
+ const loadedSettings = await window.electronAPI.getLLMSettings();
351
+ setSettings(loadedSettings);
352
+
353
+ // Set form state from loaded settings
354
+ if (loadedSettings.bedrock?.region) {
355
+ setAwsRegion(loadedSettings.bedrock.region);
356
+ }
357
+ if (loadedSettings.bedrock?.profile) {
358
+ setAwsProfile(loadedSettings.bedrock.profile);
359
+ }
360
+ setUseDefaultCredentials(loadedSettings.bedrock?.useDefaultCredentials ?? true);
361
+
362
+ // Set Anthropic form state
363
+ if (loadedSettings.anthropic?.apiKey) {
364
+ setAnthropicApiKey(loadedSettings.anthropic.apiKey);
365
+ }
366
+
367
+ // Set Ollama form state
368
+ if (loadedSettings.ollama?.baseUrl) {
369
+ setOllamaBaseUrl(loadedSettings.ollama.baseUrl);
370
+ }
371
+ if (loadedSettings.ollama?.model) {
372
+ setOllamaModel(loadedSettings.ollama.model);
373
+ }
374
+ if (loadedSettings.ollama?.apiKey) {
375
+ setOllamaApiKey(loadedSettings.ollama.apiKey);
376
+ }
377
+
378
+ // Set Gemini form state
379
+ if (loadedSettings.gemini?.apiKey) {
380
+ setGeminiApiKey(loadedSettings.gemini.apiKey);
381
+ }
382
+ if (loadedSettings.gemini?.model) {
383
+ setGeminiModel(loadedSettings.gemini.model);
384
+ }
385
+
386
+ // Set OpenRouter form state
387
+ if (loadedSettings.openrouter?.apiKey) {
388
+ setOpenrouterApiKey(loadedSettings.openrouter.apiKey);
389
+ }
390
+ if (loadedSettings.openrouter?.model) {
391
+ setOpenrouterModel(loadedSettings.openrouter.model);
392
+ }
393
+
394
+ // Set OpenAI form state
395
+ if (loadedSettings.openai?.apiKey) {
396
+ setOpenaiApiKey(loadedSettings.openai.apiKey);
397
+ }
398
+ if (loadedSettings.openai?.model) {
399
+ setOpenaiModel(loadedSettings.openai.model);
400
+ }
401
+ // Set OpenAI auth method and OAuth status
402
+ if (loadedSettings.openai?.authMethod) {
403
+ setOpenaiAuthMethod(loadedSettings.openai.authMethod);
404
+ // If authMethod is 'oauth', check if tokens are available
405
+ if (loadedSettings.openai.authMethod === 'oauth') {
406
+ if (loadedSettings.openai.accessToken || loadedSettings.openai.refreshToken) {
407
+ // Tokens available - fully connected
408
+ setOpenaiOAuthConnected(true);
409
+ } else {
410
+ // Auth method is OAuth but tokens missing (decryption failed or expired)
411
+ // Keep authMethod as oauth so user knows they configured it, but not connected
412
+ setOpenaiOAuthConnected(false);
413
+ console.log('[Settings] OpenAI OAuth configured but tokens unavailable - re-authentication required');
414
+ }
415
+ }
416
+ } else if (loadedSettings.openai?.accessToken) {
417
+ // Legacy: accessToken present but no authMethod set
418
+ setOpenaiOAuthConnected(true);
419
+ setOpenaiAuthMethod('oauth');
420
+ }
421
+
422
+ // Set Bedrock form state (access key and secret key are set earlier)
423
+ if (loadedSettings.bedrock?.accessKeyId) {
424
+ setAwsAccessKeyId(loadedSettings.bedrock.accessKeyId);
425
+ }
426
+ if (loadedSettings.bedrock?.secretAccessKey) {
427
+ setAwsSecretAccessKey(loadedSettings.bedrock.secretAccessKey);
428
+ }
429
+ if (loadedSettings.bedrock?.model) {
430
+ setBedrockModel(loadedSettings.bedrock.model);
431
+ }
432
+
433
+ // Populate dropdown arrays from cached models
434
+ if (loadedSettings.cachedGeminiModels && loadedSettings.cachedGeminiModels.length > 0) {
435
+ setGeminiModels(loadedSettings.cachedGeminiModels.map((m: any) => ({
436
+ name: m.key,
437
+ displayName: m.displayName,
438
+ description: m.description,
439
+ })));
440
+ }
441
+ if (loadedSettings.cachedOpenRouterModels && loadedSettings.cachedOpenRouterModels.length > 0) {
442
+ setOpenrouterModels(loadedSettings.cachedOpenRouterModels.map((m: any) => ({
443
+ id: m.key,
444
+ name: m.displayName,
445
+ context_length: m.contextLength || 0,
446
+ })));
447
+ }
448
+ if (loadedSettings.cachedOpenAIModels && loadedSettings.cachedOpenAIModels.length > 0) {
449
+ setOpenaiModels(loadedSettings.cachedOpenAIModels.map((m: any) => ({
450
+ id: m.key,
451
+ name: m.displayName,
452
+ description: m.description || '',
453
+ })));
454
+ }
455
+ if (loadedSettings.cachedOllamaModels && loadedSettings.cachedOllamaModels.length > 0) {
456
+ setOllamaModels(loadedSettings.cachedOllamaModels.map((m: any) => ({
457
+ name: m.key,
458
+ size: m.size || 0,
459
+ })));
460
+ }
461
+ if (loadedSettings.cachedBedrockModels && loadedSettings.cachedBedrockModels.length > 0) {
462
+ setBedrockModels(loadedSettings.cachedBedrockModels.map((m: any) => ({
463
+ id: m.key,
464
+ name: m.displayName,
465
+ description: m.description || '',
466
+ })));
467
+ }
468
+ } catch (error) {
469
+ console.error('Failed to load settings:', error);
470
+ } finally {
471
+ setLoading(false);
472
+ }
473
+ };
474
+
475
+ const loadOllamaModels = async (baseUrl?: string) => {
476
+ try {
477
+ setLoadingOllamaModels(true);
478
+ const models = await window.electronAPI.getOllamaModels(baseUrl || ollamaBaseUrl);
479
+ console.log(`[Settings] Loaded ${models?.length || 0} Ollama models`, models);
480
+ setOllamaModels(models || []);
481
+ // If we got models and current model isn't in the list, select the first one
482
+ if (models && models.length > 0 && !models.some(m => m.name === ollamaModel)) {
483
+ setOllamaModel(models[0].name);
484
+ }
485
+ // Notify main page that models were refreshed (they're now cached)
486
+ onSettingsChanged?.();
487
+ } catch (error) {
488
+ console.error('Failed to load Ollama models:', error);
489
+ setOllamaModels([]);
490
+ } finally {
491
+ setLoadingOllamaModels(false);
492
+ }
493
+ };
494
+
495
+ const loadGeminiModels = async (apiKey?: string) => {
496
+ try {
497
+ setLoadingGeminiModels(true);
498
+ const models = await window.electronAPI.getGeminiModels(apiKey || geminiApiKey);
499
+ setGeminiModels(models || []);
500
+ // If we got models and current model isn't in the list, select the first one
501
+ if (models && models.length > 0 && !models.some(m => m.name === geminiModel)) {
502
+ setGeminiModel(models[0].name);
503
+ }
504
+ // Notify main page that models were refreshed (they're now cached)
505
+ onSettingsChanged?.();
506
+ } catch (error) {
507
+ console.error('Failed to load Gemini models:', error);
508
+ setGeminiModels([]);
509
+ } finally {
510
+ setLoadingGeminiModels(false);
511
+ }
512
+ };
513
+
514
+ const loadOpenRouterModels = async (apiKey?: string) => {
515
+ try {
516
+ setLoadingOpenRouterModels(true);
517
+ const models = await window.electronAPI.getOpenRouterModels(apiKey || openrouterApiKey);
518
+ setOpenrouterModels(models || []);
519
+ // If we got models and current model isn't in the list, select the first one
520
+ if (models && models.length > 0 && !models.some(m => m.id === openrouterModel)) {
521
+ setOpenrouterModel(models[0].id);
522
+ }
523
+ // Notify main page that models were refreshed (they're now cached)
524
+ onSettingsChanged?.();
525
+ } catch (error) {
526
+ console.error('Failed to load OpenRouter models:', error);
527
+ setOpenrouterModels([]);
528
+ } finally {
529
+ setLoadingOpenRouterModels(false);
530
+ }
531
+ };
532
+
533
+ const loadOpenAIModels = async (apiKey?: string) => {
534
+ try {
535
+ setLoadingOpenAIModels(true);
536
+ const models = await window.electronAPI.getOpenAIModels(apiKey || openaiApiKey);
537
+ setOpenaiModels(models || []);
538
+ // If we got models and current model isn't in the list, select the first one
539
+ if (models && models.length > 0 && !models.some(m => m.id === openaiModel)) {
540
+ setOpenaiModel(models[0].id);
541
+ }
542
+ // Notify main page that models were refreshed (they're now cached)
543
+ onSettingsChanged?.();
544
+ } catch (error) {
545
+ console.error('Failed to load OpenAI models:', error);
546
+ setOpenaiModels([]);
547
+ } finally {
548
+ setLoadingOpenAIModels(false);
549
+ }
550
+ };
551
+
552
+ const handleOpenAIOAuthLogin = async () => {
553
+ try {
554
+ setOpenaiOAuthLoading(true);
555
+ setTestResult(null);
556
+ const result = await window.electronAPI.openaiOAuthStart();
557
+ if (result.success) {
558
+ setOpenaiOAuthConnected(true);
559
+ setOpenaiAuthMethod('oauth');
560
+ setOpenaiApiKey(''); // Clear API key when using OAuth
561
+ onSettingsChanged?.();
562
+ // Load models after OAuth success
563
+ loadOpenAIModels();
564
+ } else {
565
+ setTestResult({ success: false, error: result.error || 'OAuth failed' });
566
+ }
567
+ } catch (error: any) {
568
+ console.error('OpenAI OAuth error:', error);
569
+ setTestResult({ success: false, error: error.message || 'OAuth failed' });
570
+ } finally {
571
+ setOpenaiOAuthLoading(false);
572
+ }
573
+ };
574
+
575
+ const handleOpenAIOAuthLogout = async () => {
576
+ try {
577
+ setOpenaiOAuthLoading(true);
578
+ await window.electronAPI.openaiOAuthLogout();
579
+ setOpenaiOAuthConnected(false);
580
+ setOpenaiAuthMethod('api_key');
581
+ onSettingsChanged?.();
582
+ } catch (error: any) {
583
+ console.error('OpenAI OAuth logout error:', error);
584
+ } finally {
585
+ setOpenaiOAuthLoading(false);
586
+ }
587
+ };
588
+
589
+ const loadBedrockModels = async () => {
590
+ try {
591
+ setLoadingBedrockModels(true);
592
+ const config = useDefaultCredentials
593
+ ? { region: awsRegion, profile: awsProfile || undefined }
594
+ : { region: awsRegion, accessKeyId: awsAccessKeyId || undefined, secretAccessKey: awsSecretAccessKey || undefined };
595
+ const models = await window.electronAPI.getBedrockModels(config);
596
+ setBedrockModels(models || []);
597
+ // If we got models and current model isn't in the list, select the first one
598
+ if (models && models.length > 0 && !models.some((m: any) => m.id === bedrockModel)) {
599
+ setBedrockModel(models[0].id);
600
+ }
601
+ // Notify main page that models were refreshed (they're now cached)
602
+ onSettingsChanged?.();
603
+ } catch (error) {
604
+ console.error('Failed to load Bedrock models:', error);
605
+ setBedrockModels([]);
606
+ } finally {
607
+ setLoadingBedrockModels(false);
608
+ }
609
+ };
610
+
611
+ const handleSave = async () => {
612
+ try {
613
+ setSaving(true);
614
+ setTestResult(null);
615
+
616
+ // Always save settings for ALL providers to preserve API keys and model selections
617
+ // when switching between providers
618
+ const settingsToSave: LLMSettingsData = {
619
+ ...settings,
620
+ // Always include anthropic settings
621
+ anthropic: {
622
+ apiKey: anthropicApiKey || undefined,
623
+ },
624
+ // Always include bedrock settings
625
+ bedrock: {
626
+ region: awsRegion,
627
+ useDefaultCredentials,
628
+ model: bedrockModel || undefined,
629
+ ...(useDefaultCredentials ? {
630
+ profile: awsProfile || undefined,
631
+ } : {
632
+ accessKeyId: awsAccessKeyId || undefined,
633
+ secretAccessKey: awsSecretAccessKey || undefined,
634
+ }),
635
+ },
636
+ // Always include ollama settings
637
+ ollama: {
638
+ baseUrl: ollamaBaseUrl || undefined,
639
+ model: ollamaModel || undefined,
640
+ apiKey: ollamaApiKey || undefined,
641
+ },
642
+ // Always include gemini settings
643
+ gemini: {
644
+ apiKey: geminiApiKey || undefined,
645
+ model: geminiModel || undefined,
646
+ },
647
+ // Always include openrouter settings
648
+ openrouter: {
649
+ apiKey: openrouterApiKey || undefined,
650
+ model: openrouterModel || undefined,
651
+ },
652
+ // Always include openai settings
653
+ openai: {
654
+ apiKey: openaiAuthMethod === 'api_key' ? (openaiApiKey || undefined) : undefined,
655
+ model: openaiModel || undefined,
656
+ authMethod: openaiAuthMethod,
657
+ },
658
+ };
659
+
660
+ await window.electronAPI.saveLLMSettings(settingsToSave);
661
+ onSettingsChanged?.();
662
+ onBack();
663
+ } catch (error) {
664
+ console.error('Failed to save settings:', error);
665
+ } finally {
666
+ setSaving(false);
667
+ }
668
+ };
669
+
670
+ const handleTestConnection = async () => {
671
+ try {
672
+ setTesting(true);
673
+ setTestResult(null);
674
+
675
+ const testConfig = {
676
+ providerType: settings.providerType,
677
+ modelKey: settings.modelKey,
678
+ anthropic: settings.providerType === 'anthropic' ? {
679
+ apiKey: anthropicApiKey || undefined,
680
+ } : undefined,
681
+ bedrock: settings.providerType === 'bedrock' ? {
682
+ region: awsRegion,
683
+ ...(useDefaultCredentials ? {
684
+ profile: awsProfile || undefined,
685
+ } : {
686
+ accessKeyId: awsAccessKeyId || undefined,
687
+ secretAccessKey: awsSecretAccessKey || undefined,
688
+ }),
689
+ } : undefined,
690
+ ollama: settings.providerType === 'ollama' ? {
691
+ baseUrl: ollamaBaseUrl || undefined,
692
+ model: ollamaModel || undefined,
693
+ apiKey: ollamaApiKey || undefined,
694
+ } : undefined,
695
+ gemini: settings.providerType === 'gemini' ? {
696
+ apiKey: geminiApiKey || undefined,
697
+ model: geminiModel || undefined,
698
+ } : undefined,
699
+ openrouter: settings.providerType === 'openrouter' ? {
700
+ apiKey: openrouterApiKey || undefined,
701
+ model: openrouterModel || undefined,
702
+ } : undefined,
703
+ openai: settings.providerType === 'openai' ? {
704
+ apiKey: openaiAuthMethod === 'api_key' ? (openaiApiKey || undefined) : undefined,
705
+ model: openaiModel || undefined,
706
+ authMethod: openaiAuthMethod,
707
+ // OAuth tokens are handled by the backend from stored settings
708
+ } : undefined,
709
+ };
710
+
711
+ const result = await window.electronAPI.testLLMProvider(testConfig);
712
+ setTestResult(result);
713
+ } catch (error: any) {
714
+ setTestResult({ success: false, error: error.message });
715
+ } finally {
716
+ setTesting(false);
717
+ }
718
+ };
719
+
720
+ return (
721
+ <div className="settings-page">
722
+ <div className="settings-page-header">
723
+ <h1>Settings</h1>
724
+ </div>
725
+
726
+ <div className="settings-page-layout">
727
+ <div className="settings-sidebar">
728
+ <button className="settings-back-btn" onClick={onBack}>
729
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
730
+ <path d="M19 12H5M12 19l-7-7 7-7" />
731
+ </svg>
732
+ Back
733
+ </button>
734
+ <div className="settings-nav-divider" />
735
+ <div className="settings-sidebar-search">
736
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
737
+ <circle cx="11" cy="11" r="8" />
738
+ <path d="M21 21l-4.35-4.35" />
739
+ </svg>
740
+ <input
741
+ type="text"
742
+ placeholder="Search settings..."
743
+ value={sidebarSearch}
744
+ onChange={(e) => setSidebarSearch(e.target.value)}
745
+ />
746
+ {sidebarSearch && (
747
+ <button
748
+ className="settings-sidebar-search-clear"
749
+ onClick={() => setSidebarSearch('')}
750
+ aria-label="Clear search"
751
+ >
752
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
753
+ <line x1="18" y1="6" x2="6" y2="18" />
754
+ <line x1="6" y1="6" x2="18" y2="18" />
755
+ </svg>
756
+ </button>
757
+ )}
758
+ </div>
759
+ <div className="settings-nav-items">
760
+ {sidebarItems
761
+ .filter(item => {
762
+ // Filter by macOnly if applicable
763
+ if (item.macOnly && !navigator.platform.toLowerCase().includes('mac')) {
764
+ return false;
765
+ }
766
+ // Filter by search query
767
+ if (sidebarSearch) {
768
+ return item.label.toLowerCase().includes(sidebarSearch.toLowerCase());
769
+ }
770
+ return true;
771
+ })
772
+ .map(item => (
773
+ <button
774
+ key={item.tab}
775
+ className={`settings-nav-item ${activeTab === item.tab ? 'active' : ''}`}
776
+ onClick={() => setActiveTab(item.tab)}
777
+ >
778
+ {item.icon}
779
+ {item.label}
780
+ </button>
781
+ ))}
782
+ {sidebarSearch && sidebarItems.filter(item => {
783
+ if (item.macOnly && !navigator.platform.toLowerCase().includes('mac')) return false;
784
+ return item.label.toLowerCase().includes(sidebarSearch.toLowerCase());
785
+ }).length === 0 && (
786
+ <div className="settings-nav-no-results">No matching settings</div>
787
+ )}
788
+ </div>
789
+ </div>
790
+
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
+ ))}
834
+ </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 />}
847
+ </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">
886
+ {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';
893
+
894
+ return (
895
+ <label
896
+ key={provider.type}
897
+ className={`provider-option ${settings.providerType === provider.type ? 'selected' : ''}`}
898
+ >
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>
970
+ );
971
+ })}
972
+ </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}
1033
+ >
1034
+ {loadingGeminiModels ? 'Loading...' : 'Refresh Models'}
1035
+ </button>
1036
+ </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
+ )}
1067
+
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">
1079
+ <input
1080
+ type="password"
1081
+ className="settings-input"
1082
+ placeholder="sk-or-..."
1083
+ value={openrouterApiKey}
1084
+ onChange={(e) => setOpenrouterApiKey(e.target.value)}
1085
+ />
1086
+ <button
1087
+ className="button-small button-secondary"
1088
+ onClick={() => loadOpenRouterModels(openrouterApiKey)}
1089
+ disabled={loadingOpenRouterModels}
1090
+ >
1091
+ {loadingOpenRouterModels ? 'Loading...' : 'Refresh Models'}
1092
+ </button>
1093
+ </div>
1094
+ </div>
1095
+
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
+ )}
1127
+
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>
1157
+
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>
1173
+ <button
1174
+ className="button-small button-secondary"
1175
+ onClick={handleOpenAIOAuthLogout}
1176
+ disabled={openaiOAuthLoading}
1177
+ >
1178
+ {openaiOAuthLoading ? 'Disconnecting...' : 'Disconnect Account'}
1179
+ </button>
1180
+ </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>
1186
+ <button
1187
+ className="button-primary oauth-login-btn"
1188
+ onClick={handleOpenAIOAuthLogin}
1189
+ disabled={openaiOAuthLoading}
1190
+ >
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
+ )}
1208
+ </button>
1209
+ </div>
1210
+ )}
1211
+ </div>
1212
+ )}
1213
+
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">
1224
+ <input
1225
+ type="password"
1226
+ className="settings-input"
1227
+ placeholder="sk-..."
1228
+ value={openaiApiKey}
1229
+ onChange={(e) => setOpenaiApiKey(e.target.value)}
1230
+ />
1231
+ <button
1232
+ className="button-small button-secondary"
1233
+ onClick={() => loadOpenAIModels(openaiApiKey)}
1234
+ disabled={loadingOpenAIModels}
1235
+ >
1236
+ {loadingOpenAIModels ? 'Loading...' : 'Refresh Models'}
1237
+ </button>
1238
+ </div>
1239
+ </div>
1240
+ )}
1241
+
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
+ )}
1282
+
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>
1301
+
1302
+ <div className="settings-section">
1303
+ <h3>AWS Credentials</h3>
1304
+
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>
1313
+
1314
+ {useDefaultCredentials ? (
1315
+ <div className="settings-subsection">
1316
+ <p className="settings-description">
1317
+ Uses AWS credentials from environment variables, shared credentials file (~/.aws/credentials), or IAM role.
1318
+ </p>
1319
+ <input
1320
+ type="text"
1321
+ className="settings-input"
1322
+ placeholder="AWS Profile (optional, e.g., 'default')"
1323
+ value={awsProfile}
1324
+ onChange={(e) => setAwsProfile(e.target.value)}
1325
+ />
1326
+ </div>
1327
+ ) : (
1328
+ <div className="settings-subsection">
1329
+ <input
1330
+ type="text"
1331
+ className="settings-input"
1332
+ placeholder="AWS Access Key ID"
1333
+ value={awsAccessKeyId}
1334
+ onChange={(e) => setAwsAccessKeyId(e.target.value)}
1335
+ />
1336
+ <input
1337
+ type="password"
1338
+ className="settings-input"
1339
+ placeholder="AWS Secret Access Key"
1340
+ value={awsSecretAccessKey}
1341
+ onChange={(e) => setAwsSecretAccessKey(e.target.value)}
1342
+ />
1343
+ </div>
1344
+ )}
1345
+ </div>
1346
+
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
+ )}
1387
+
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>
1412
+
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>
1442
+
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
+ )}
1458
+
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!
1468
+ </>
1469
+ ) : (
1470
+ <>
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'}
1477
+ </>
1478
+ )}
1479
+ </div>
1480
+ )}
1481
+
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>
1497
+ </div>
1498
+ </>
1499
+ )}
1500
+ </div>
1501
+ </div>
1502
+ </div>
1503
+ );
1504
+ }