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,1134 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import type {
3
+ ControlPlaneSettingsData,
4
+ ControlPlaneStatus,
5
+ TailscaleAvailability,
6
+ RemoteGatewayStatus,
7
+ ControlPlaneConnectionMode,
8
+ SSHTunnelStatus,
9
+ SSHTunnelConfig,
10
+ } from '../../shared/types';
11
+
12
+ export function ControlPlaneSettings() {
13
+ const [settings, setSettings] = useState<ControlPlaneSettingsData | null>(null);
14
+ const [status, setStatus] = useState<ControlPlaneStatus | null>(null);
15
+ const [remoteStatus, setRemoteStatus] = useState<RemoteGatewayStatus | null>(null);
16
+ const [tailscaleAvailability, setTailscaleAvailability] = useState<TailscaleAvailability | null>(null);
17
+ const [loading, setLoading] = useState(true);
18
+ const [saving, setSaving] = useState(false);
19
+ const [testing, setTesting] = useState(false);
20
+ const [testResult, setTestResult] = useState<{ success: boolean; message: string; latencyMs?: number } | null>(null);
21
+ const [connectionMode, setConnectionMode] = useState<ControlPlaneConnectionMode>('local');
22
+ const [showToken, setShowToken] = useState(false);
23
+ const [showRemoteToken, setShowRemoteToken] = useState(false);
24
+ const [allowLAN, setAllowLAN] = useState(false);
25
+
26
+ // Remote config form state
27
+ const [remoteUrl, setRemoteUrl] = useState('ws://127.0.0.1:18789');
28
+ const [remoteToken, setRemoteToken] = useState('');
29
+ const [remoteDeviceName, setRemoteDeviceName] = useState('CoWork Remote Client');
30
+
31
+ // SSH Tunnel state
32
+ const [sshTunnelStatus, setSshTunnelStatus] = useState<SSHTunnelStatus | null>(null);
33
+ const [sshTunnelEnabled, setSshTunnelEnabled] = useState(false);
34
+ const [sshHost, setSshHost] = useState('');
35
+ const [sshUsername, setSshUsername] = useState('');
36
+ const [sshPort, setSshPort] = useState(22);
37
+ const [sshKeyPath, setSshKeyPath] = useState('');
38
+ const [sshLocalPort, setSshLocalPort] = useState(18789);
39
+ const [sshRemotePort, setSshRemotePort] = useState(18789);
40
+ const [testingSshTunnel, setTestingSshTunnel] = useState(false);
41
+ const [sshTestResult, setSshTestResult] = useState<{ success: boolean; message: string; latencyMs?: number } | null>(null);
42
+
43
+ // Helper to build SSH tunnel config
44
+ const getSshTunnelConfig = useCallback((): SSHTunnelConfig => ({
45
+ enabled: sshTunnelEnabled,
46
+ host: sshHost,
47
+ username: sshUsername,
48
+ sshPort: sshPort,
49
+ keyPath: sshKeyPath || undefined,
50
+ localPort: sshLocalPort,
51
+ remotePort: sshRemotePort,
52
+ autoReconnect: true,
53
+ reconnectDelayMs: 5000,
54
+ maxReconnectAttempts: 10,
55
+ }), [sshTunnelEnabled, sshHost, sshUsername, sshPort, sshKeyPath, sshLocalPort, sshRemotePort]);
56
+
57
+ const loadData = useCallback(async () => {
58
+ try {
59
+ const [settingsData, statusData, tailscale, remoteStatusData, sshStatus] = await Promise.all([
60
+ window.electronAPI?.getControlPlaneSettings?.() || null,
61
+ window.electronAPI?.getControlPlaneStatus?.() || null,
62
+ window.electronAPI?.checkTailscaleAvailability?.() || null,
63
+ window.electronAPI?.getRemoteGatewayStatus?.() || null,
64
+ window.electronAPI?.getSSHTunnelStatus?.() || null,
65
+ ]);
66
+
67
+ setSettings(settingsData);
68
+ setStatus(statusData);
69
+ setTailscaleAvailability(tailscale);
70
+ setRemoteStatus(remoteStatusData);
71
+ setSshTunnelStatus(sshStatus);
72
+
73
+ // Set connection mode from settings
74
+ if (settingsData?.connectionMode) {
75
+ setConnectionMode(settingsData.connectionMode);
76
+ }
77
+
78
+ // Set LAN access from settings (host = 0.0.0.0 means LAN is enabled)
79
+ if (settingsData?.host) {
80
+ setAllowLAN(settingsData.host === '0.0.0.0');
81
+ }
82
+
83
+ // Set remote config from settings
84
+ if (settingsData?.remote) {
85
+ setRemoteUrl(settingsData.remote.url || 'ws://127.0.0.1:18789');
86
+ setRemoteToken(settingsData.remote.token || '');
87
+ setRemoteDeviceName(settingsData.remote.deviceName || 'CoWork Remote Client');
88
+
89
+ // Set SSH tunnel config from settings
90
+ const remoteSshTunnel = (settingsData.remote as { sshTunnel?: SSHTunnelConfig }).sshTunnel;
91
+ if (remoteSshTunnel) {
92
+ const tunnel = remoteSshTunnel;
93
+ setSshTunnelEnabled(tunnel.enabled || false);
94
+ setSshHost(tunnel.host || '');
95
+ setSshUsername(tunnel.username || '');
96
+ setSshPort(tunnel.sshPort || 22);
97
+ setSshKeyPath(tunnel.keyPath || '');
98
+ setSshLocalPort(tunnel.localPort || 18789);
99
+ setSshRemotePort(tunnel.remotePort || 18789);
100
+ }
101
+ }
102
+ } catch (error) {
103
+ console.error('Failed to load control plane data:', error);
104
+ } finally {
105
+ setLoading(false);
106
+ }
107
+ }, []);
108
+
109
+ useEffect(() => {
110
+ loadData();
111
+
112
+ // Poll status every 5 seconds
113
+ const interval = setInterval(() => {
114
+ loadData();
115
+ }, 5000);
116
+
117
+ return () => clearInterval(interval);
118
+ }, [loadData]);
119
+
120
+ const handleToggleEnabled = async () => {
121
+ if (!settings) return;
122
+
123
+ setSaving(true);
124
+ try {
125
+ if (settings.enabled) {
126
+ await window.electronAPI?.disableControlPlane?.();
127
+ } else {
128
+ await window.electronAPI?.enableControlPlane?.();
129
+ }
130
+ await loadData();
131
+ } catch (error) {
132
+ console.error('Failed to toggle control plane:', error);
133
+ } finally {
134
+ setSaving(false);
135
+ }
136
+ };
137
+
138
+ const handleStartStop = async () => {
139
+ setSaving(true);
140
+ try {
141
+ if (status?.running) {
142
+ await window.electronAPI?.stopControlPlane?.();
143
+ } else {
144
+ await window.electronAPI?.startControlPlane?.();
145
+ }
146
+ await loadData();
147
+ } catch (error) {
148
+ console.error('Failed to start/stop control plane:', error);
149
+ } finally {
150
+ setSaving(false);
151
+ }
152
+ };
153
+
154
+ const handleRegenerateToken = async () => {
155
+ setSaving(true);
156
+ try {
157
+ await window.electronAPI?.regenerateControlPlaneToken?.();
158
+ await loadData();
159
+ } catch (error) {
160
+ console.error('Failed to regenerate token:', error);
161
+ } finally {
162
+ setSaving(false);
163
+ }
164
+ };
165
+
166
+ const handleToggleLAN = async () => {
167
+ setSaving(true);
168
+ try {
169
+ const newAllowLAN = !allowLAN;
170
+ await window.electronAPI?.saveControlPlaneSettings?.({
171
+ host: newAllowLAN ? '0.0.0.0' : '127.0.0.1',
172
+ });
173
+ setAllowLAN(newAllowLAN);
174
+ // Need to restart server for host change to take effect
175
+ if (status?.running) {
176
+ await window.electronAPI?.stopControlPlane?.();
177
+ await window.electronAPI?.startControlPlane?.();
178
+ }
179
+ await loadData();
180
+ } catch (error) {
181
+ console.error('Failed to toggle LAN access:', error);
182
+ } finally {
183
+ setSaving(false);
184
+ }
185
+ };
186
+
187
+ const handleTailscaleModeChange = async (mode: 'off' | 'serve' | 'funnel') => {
188
+ setSaving(true);
189
+ try {
190
+ await window.electronAPI?.setTailscaleMode?.(mode);
191
+ await loadData();
192
+ } catch (error) {
193
+ console.error('Failed to set Tailscale mode:', error);
194
+ } finally {
195
+ setSaving(false);
196
+ }
197
+ };
198
+
199
+ const handleConnectionModeChange = async (mode: ControlPlaneConnectionMode) => {
200
+ setConnectionMode(mode);
201
+
202
+ if (mode === 'local') {
203
+ // Disconnect from remote if connected
204
+ if (remoteStatus?.state === 'connected') {
205
+ await window.electronAPI?.disconnectRemoteGateway?.();
206
+ }
207
+ }
208
+ };
209
+
210
+ const handleSaveRemoteConfig = async () => {
211
+ setSaving(true);
212
+ try {
213
+ await window.electronAPI?.saveRemoteGatewayConfig?.({
214
+ url: remoteUrl,
215
+ token: remoteToken,
216
+ deviceName: remoteDeviceName,
217
+ });
218
+ await loadData();
219
+ } catch (error) {
220
+ console.error('Failed to save remote config:', error);
221
+ } finally {
222
+ setSaving(false);
223
+ }
224
+ };
225
+
226
+ const handleTestRemoteConnection = async () => {
227
+ setTesting(true);
228
+ setTestResult(null);
229
+ try {
230
+ const result = await window.electronAPI?.testRemoteGatewayConnection?.({
231
+ url: remoteUrl,
232
+ token: remoteToken,
233
+ deviceName: remoteDeviceName,
234
+ });
235
+
236
+ if (result?.ok) {
237
+ setTestResult({
238
+ success: true,
239
+ message: `Connection successful`,
240
+ latencyMs: result.latencyMs,
241
+ });
242
+ } else {
243
+ setTestResult({
244
+ success: false,
245
+ message: result?.error || 'Connection failed',
246
+ });
247
+ }
248
+ } catch (error: any) {
249
+ setTestResult({
250
+ success: false,
251
+ message: error.message || 'Connection failed',
252
+ });
253
+ } finally {
254
+ setTesting(false);
255
+ }
256
+ };
257
+
258
+ const handleConnectRemote = async () => {
259
+ setSaving(true);
260
+ try {
261
+ const result = await window.electronAPI?.connectRemoteGateway?.({
262
+ url: remoteUrl,
263
+ token: remoteToken,
264
+ deviceName: remoteDeviceName,
265
+ });
266
+
267
+ if (!result?.ok) {
268
+ setTestResult({
269
+ success: false,
270
+ message: result?.error || 'Connection failed',
271
+ });
272
+ }
273
+ await loadData();
274
+ } catch (error: any) {
275
+ setTestResult({
276
+ success: false,
277
+ message: error.message || 'Connection failed',
278
+ });
279
+ } finally {
280
+ setSaving(false);
281
+ }
282
+ };
283
+
284
+ const handleDisconnectRemote = async () => {
285
+ setSaving(true);
286
+ try {
287
+ await window.electronAPI?.disconnectRemoteGateway?.();
288
+ await loadData();
289
+ } catch (error) {
290
+ console.error('Failed to disconnect:', error);
291
+ } finally {
292
+ setSaving(false);
293
+ }
294
+ };
295
+
296
+ // SSH Tunnel Handlers
297
+ const handleTestSshTunnel = async () => {
298
+ setTestingSshTunnel(true);
299
+ setSshTestResult(null);
300
+ try {
301
+ const result = await window.electronAPI?.testSSHTunnelConnection?.(getSshTunnelConfig());
302
+ if (result?.ok) {
303
+ setSshTestResult({
304
+ success: true,
305
+ message: 'SSH connection successful',
306
+ latencyMs: result.latencyMs,
307
+ });
308
+ } else {
309
+ setSshTestResult({
310
+ success: false,
311
+ message: result?.error || 'SSH connection failed',
312
+ });
313
+ }
314
+ } catch (error: unknown) {
315
+ const errorMessage = error instanceof Error ? error.message : 'SSH connection failed';
316
+ setSshTestResult({
317
+ success: false,
318
+ message: errorMessage,
319
+ });
320
+ } finally {
321
+ setTestingSshTunnel(false);
322
+ }
323
+ };
324
+
325
+ const handleConnectSshTunnel = async () => {
326
+ setSaving(true);
327
+ setSshTestResult(null);
328
+ try {
329
+ const config = getSshTunnelConfig();
330
+ config.enabled = true;
331
+
332
+ const result = await window.electronAPI?.connectSSHTunnel?.(config);
333
+ if (!result?.ok) {
334
+ setSshTestResult({
335
+ success: false,
336
+ message: result?.error || 'Failed to create SSH tunnel',
337
+ });
338
+ } else {
339
+ // Update the remote URL to use the local tunnel endpoint
340
+ setRemoteUrl(`ws://127.0.0.1:${sshLocalPort}`);
341
+ setSshTunnelEnabled(true);
342
+ }
343
+ await loadData();
344
+ } catch (error: unknown) {
345
+ const errorMessage = error instanceof Error ? error.message : 'Failed to create SSH tunnel';
346
+ setSshTestResult({
347
+ success: false,
348
+ message: errorMessage,
349
+ });
350
+ } finally {
351
+ setSaving(false);
352
+ }
353
+ };
354
+
355
+ const handleDisconnectSshTunnel = async () => {
356
+ setSaving(true);
357
+ try {
358
+ await window.electronAPI?.disconnectSSHTunnel?.();
359
+ setSshTunnelEnabled(false);
360
+ await loadData();
361
+ } catch (error) {
362
+ console.error('Failed to disconnect SSH tunnel:', error);
363
+ } finally {
364
+ setSaving(false);
365
+ }
366
+ };
367
+
368
+ const copyToClipboard = (text: string) => {
369
+ navigator.clipboard.writeText(text);
370
+ };
371
+
372
+ if (loading) {
373
+ return <div className="settings-loading">Loading control plane settings...</div>;
374
+ }
375
+
376
+ return (
377
+ <div className="settings-section">
378
+ <h2>Control Plane</h2>
379
+ <p className="settings-description">
380
+ WebSocket gateway for remote management. Connect via SSH tunnel, Tailscale, or direct network.
381
+ </p>
382
+
383
+ {/* Connection Mode Selector */}
384
+ <div className="settings-subsection">
385
+ <h3>Connection Mode</h3>
386
+ <div className="connection-mode-selector">
387
+ <label className={`mode-option ${connectionMode === 'local' ? 'selected' : ''}`}>
388
+ <input
389
+ type="radio"
390
+ name="connectionMode"
391
+ value="local"
392
+ checked={connectionMode === 'local'}
393
+ onChange={() => handleConnectionModeChange('local')}
394
+ />
395
+ <div className="mode-content">
396
+ <span className="mode-title">Local Server</span>
397
+ <span className="mode-description">Host the Control Plane on this machine</span>
398
+ </div>
399
+ </label>
400
+ <label className={`mode-option ${connectionMode === 'remote' ? 'selected' : ''}`}>
401
+ <input
402
+ type="radio"
403
+ name="connectionMode"
404
+ value="remote"
405
+ checked={connectionMode === 'remote'}
406
+ onChange={() => handleConnectionModeChange('remote')}
407
+ />
408
+ <div className="mode-content">
409
+ <span className="mode-title">Remote Gateway</span>
410
+ <span className="mode-description">Connect to a Control Plane on another machine</span>
411
+ </div>
412
+ </label>
413
+ </div>
414
+ </div>
415
+
416
+ {connectionMode === 'local' ? (
417
+ <>
418
+ {/* Local Server Settings */}
419
+ <div className="settings-subsection">
420
+ <h3>Server Status</h3>
421
+ <div className="settings-row">
422
+ <label>
423
+ <input
424
+ type="checkbox"
425
+ checked={settings?.enabled || false}
426
+ onChange={handleToggleEnabled}
427
+ disabled={saving}
428
+ />
429
+ Enable Control Plane
430
+ </label>
431
+ </div>
432
+
433
+ {settings?.enabled && (
434
+ <div className="settings-row">
435
+ <label>
436
+ <input
437
+ type="checkbox"
438
+ checked={allowLAN}
439
+ onChange={handleToggleLAN}
440
+ disabled={saving}
441
+ />
442
+ Allow LAN Connections (Mobile Companions)
443
+ </label>
444
+ <p className="hint" style={{ marginLeft: '1.5rem', marginTop: '0.25rem' }}>
445
+ Enable this to allow connections from other devices on your local network (required for iOS/Android companion apps)
446
+ </p>
447
+ </div>
448
+ )}
449
+
450
+ {settings?.enabled && (
451
+ <>
452
+ <div className="status-card">
453
+ <div className="status-indicator">
454
+ <span className={`status-dot ${status?.running ? 'running' : 'stopped'}`} />
455
+ <span>{status?.running ? 'Running' : 'Stopped'}</span>
456
+ </div>
457
+ {status?.running && status.address && (
458
+ <div className="status-details">
459
+ <div className="detail-row">
460
+ <span className="label">Local URL:</span>
461
+ <code>{status.address.wsUrl}</code>
462
+ <button
463
+ className="copy-btn"
464
+ onClick={() => copyToClipboard(status.address!.wsUrl)}
465
+ title="Copy"
466
+ >
467
+ Copy
468
+ </button>
469
+ </div>
470
+ <div className="detail-row">
471
+ <span className="label">Clients:</span>
472
+ <span>{status.clients.authenticated} authenticated, {status.clients.pending} pending</span>
473
+ </div>
474
+ </div>
475
+ )}
476
+ {!status?.running && (
477
+ <div className="status-details">
478
+ <p className="hint" style={{ margin: 0 }}>
479
+ Server is not running. Click the button below to start it.
480
+ </p>
481
+ </div>
482
+ )}
483
+ </div>
484
+
485
+ <div className="button-row" style={{ marginTop: '1rem' }}>
486
+ <button
487
+ onClick={handleStartStop}
488
+ disabled={saving}
489
+ className={status?.running ? 'btn-secondary' : 'btn-primary btn-large'}
490
+ style={!status?.running ? { padding: '0.75rem 1.5rem', fontSize: '1rem', fontWeight: 500 } : {}}
491
+ >
492
+ {saving ? 'Please wait...' : status?.running ? 'Stop Server' : '▶ Start Server'}
493
+ </button>
494
+ </div>
495
+ </>
496
+ )}
497
+ </div>
498
+
499
+ {/* Token Management */}
500
+ {settings?.enabled && (
501
+ <div className="settings-subsection">
502
+ <h3>Authentication Token</h3>
503
+ <div className="token-display">
504
+ <input
505
+ type={showToken ? 'text' : 'password'}
506
+ value={settings.token || ''}
507
+ readOnly
508
+ className="token-input"
509
+ />
510
+ <button
511
+ className="btn-icon"
512
+ onClick={() => setShowToken(!showToken)}
513
+ title={showToken ? 'Hide' : 'Show'}
514
+ >
515
+ {showToken ? 'Hide' : 'Show'}
516
+ </button>
517
+ <button
518
+ className="btn-icon"
519
+ onClick={() => copyToClipboard(settings.token || '')}
520
+ title="Copy"
521
+ >
522
+ Copy
523
+ </button>
524
+ </div>
525
+ <button
526
+ onClick={handleRegenerateToken}
527
+ disabled={saving}
528
+ className="btn-secondary"
529
+ >
530
+ Regenerate Token
531
+ </button>
532
+ <p className="hint">
533
+ Warning: Regenerating the token will disconnect all existing clients.
534
+ </p>
535
+ </div>
536
+ )}
537
+
538
+ {/* Tailscale Integration */}
539
+ {settings?.enabled && (
540
+ <div className="settings-subsection">
541
+ <h3>Remote Access (Tailscale)</h3>
542
+ {!tailscaleAvailability?.installed ? (
543
+ <p className="hint">
544
+ Tailscale is not installed. Install from{' '}
545
+ <a href="https://tailscale.com" target="_blank" rel="noopener noreferrer">
546
+ tailscale.com
547
+ </a>{' '}
548
+ for remote access.
549
+ </p>
550
+ ) : (
551
+ <>
552
+ <div className="settings-row">
553
+ <label>Exposure Mode:</label>
554
+ <select
555
+ value={settings.tailscale?.mode || 'off'}
556
+ onChange={(e) => handleTailscaleModeChange(e.target.value as any)}
557
+ disabled={saving}
558
+ >
559
+ <option value="off">Off (Local only)</option>
560
+ <option value="serve">Serve (Tailnet only)</option>
561
+ <option value="funnel" disabled={!tailscaleAvailability.funnelAvailable}>
562
+ Funnel (Public Internet)
563
+ {!tailscaleAvailability.funnelAvailable && ' - Not available'}
564
+ </option>
565
+ </select>
566
+ </div>
567
+
568
+ {status?.tailscale?.active && status.tailscale.wssUrl && (
569
+ <div className="status-card">
570
+ <div className="detail-row">
571
+ <span className="label">Remote URL:</span>
572
+ <code>{status.tailscale.wssUrl}</code>
573
+ <button
574
+ className="copy-btn"
575
+ onClick={() => copyToClipboard(status.tailscale.wssUrl!)}
576
+ title="Copy"
577
+ >
578
+ Copy
579
+ </button>
580
+ </div>
581
+ </div>
582
+ )}
583
+ </>
584
+ )}
585
+ </div>
586
+ )}
587
+
588
+ {/* SSH Tunnel Instructions */}
589
+ {settings?.enabled && (
590
+ <div className="settings-subsection">
591
+ <h3>SSH Tunnel (Alternative)</h3>
592
+ <p className="hint">
593
+ Use SSH port forwarding to access the Control Plane remotely:
594
+ </p>
595
+ <div className="code-block">
596
+ <code>ssh -N -L 18789:127.0.0.1:{settings.port || 18789} user@remote-host</code>
597
+ <button
598
+ className="copy-btn"
599
+ onClick={() => copyToClipboard(`ssh -N -L 18789:127.0.0.1:${settings.port || 18789} user@remote-host`)}
600
+ >
601
+ Copy
602
+ </button>
603
+ </div>
604
+ </div>
605
+ )}
606
+ </>
607
+ ) : (
608
+ <>
609
+ {/* Remote Gateway Settings */}
610
+ <div className="settings-subsection">
611
+ <h3>Remote Gateway Configuration</h3>
612
+ <p className="hint">
613
+ Connect to a Control Plane server running on another machine via SSH tunnel or Tailscale.
614
+ </p>
615
+
616
+ <div className="settings-row">
617
+ <label>Gateway URL:</label>
618
+ <input
619
+ type="text"
620
+ value={remoteUrl}
621
+ onChange={(e) => setRemoteUrl(e.target.value)}
622
+ placeholder="ws://127.0.0.1:18789"
623
+ className="settings-input"
624
+ />
625
+ </div>
626
+
627
+ <div className="settings-row">
628
+ <label>Token:</label>
629
+ <div className="token-display">
630
+ <input
631
+ type={showRemoteToken ? 'text' : 'password'}
632
+ value={remoteToken}
633
+ onChange={(e) => setRemoteToken(e.target.value)}
634
+ placeholder="Enter authentication token"
635
+ className="token-input"
636
+ />
637
+ <button
638
+ className="btn-icon"
639
+ onClick={() => setShowRemoteToken(!showRemoteToken)}
640
+ title={showRemoteToken ? 'Hide' : 'Show'}
641
+ >
642
+ {showRemoteToken ? 'Hide' : 'Show'}
643
+ </button>
644
+ </div>
645
+ </div>
646
+
647
+ <div className="settings-row">
648
+ <label>Device Name:</label>
649
+ <input
650
+ type="text"
651
+ value={remoteDeviceName}
652
+ onChange={(e) => setRemoteDeviceName(e.target.value)}
653
+ placeholder="CoWork Remote Client"
654
+ className="settings-input"
655
+ />
656
+ </div>
657
+
658
+ {testResult && (
659
+ <div className={`test-result ${testResult.success ? 'success' : 'error'}`}>
660
+ {testResult.success ? (
661
+ <>Connection successful{testResult.latencyMs && ` (${testResult.latencyMs}ms)`}</>
662
+ ) : (
663
+ testResult.message
664
+ )}
665
+ </div>
666
+ )}
667
+
668
+ <div className="button-row">
669
+ <button
670
+ onClick={handleTestRemoteConnection}
671
+ disabled={testing || !remoteUrl || !remoteToken}
672
+ className="btn-secondary"
673
+ >
674
+ {testing ? 'Testing...' : 'Test Connection'}
675
+ </button>
676
+ <button
677
+ onClick={handleSaveRemoteConfig}
678
+ disabled={saving}
679
+ className="btn-secondary"
680
+ >
681
+ Save Config
682
+ </button>
683
+ </div>
684
+ </div>
685
+
686
+ {/* Remote Connection Status */}
687
+ <div className="settings-subsection">
688
+ <h3>Connection Status</h3>
689
+ <div className="status-card">
690
+ <div className="status-indicator">
691
+ <span className={`status-dot ${remoteStatus?.state === 'connected' ? 'running' : remoteStatus?.state === 'connecting' || remoteStatus?.state === 'authenticating' ? 'connecting' : 'stopped'}`} />
692
+ <span className="status-text">
693
+ {remoteStatus?.state === 'connected' && 'Connected'}
694
+ {remoteStatus?.state === 'connecting' && 'Connecting...'}
695
+ {remoteStatus?.state === 'authenticating' && 'Authenticating...'}
696
+ {remoteStatus?.state === 'reconnecting' && `Reconnecting (attempt ${remoteStatus.reconnectAttempts})...`}
697
+ {remoteStatus?.state === 'error' && `Error: ${remoteStatus.error}`}
698
+ {remoteStatus?.state === 'disconnected' && 'Disconnected'}
699
+ </span>
700
+ </div>
701
+ {remoteStatus?.state === 'connected' && (
702
+ <div className="status-details">
703
+ <div className="detail-row">
704
+ <span className="label">Client ID:</span>
705
+ <code>{remoteStatus.clientId}</code>
706
+ </div>
707
+ <div className="detail-row">
708
+ <span className="label">Connected:</span>
709
+ <span>{remoteStatus.connectedAt ? new Date(remoteStatus.connectedAt).toLocaleTimeString() : 'Unknown'}</span>
710
+ </div>
711
+ </div>
712
+ )}
713
+ </div>
714
+
715
+ <div className="button-row">
716
+ {remoteStatus?.state === 'connected' ? (
717
+ <button
718
+ onClick={handleDisconnectRemote}
719
+ disabled={saving}
720
+ className="btn-secondary"
721
+ >
722
+ Disconnect
723
+ </button>
724
+ ) : (
725
+ <button
726
+ onClick={handleConnectRemote}
727
+ disabled={saving || !remoteUrl || !remoteToken}
728
+ className="btn-primary"
729
+ >
730
+ {saving ? 'Connecting...' : 'Connect'}
731
+ </button>
732
+ )}
733
+ </div>
734
+ </div>
735
+
736
+ {/* SSH Tunnel Configuration */}
737
+ <div className="settings-subsection">
738
+ <h3>SSH Tunnel</h3>
739
+ <p className="hint">
740
+ Automatically create an SSH tunnel to connect to the remote gateway securely.
741
+ </p>
742
+
743
+ {/* SSH Tunnel Status */}
744
+ {sshTunnelStatus && sshTunnelStatus.state !== 'disconnected' && (
745
+ <div className="status-card">
746
+ <div className="status-indicator">
747
+ <span className={`status-dot ${sshTunnelStatus.state === 'connected' ? 'running' : sshTunnelStatus.state === 'connecting' || sshTunnelStatus.state === 'reconnecting' ? 'connecting' : 'stopped'}`} />
748
+ <span className="status-text">
749
+ {sshTunnelStatus.state === 'connected' && 'Tunnel Connected'}
750
+ {sshTunnelStatus.state === 'connecting' && 'Creating Tunnel...'}
751
+ {sshTunnelStatus.state === 'reconnecting' && `Reconnecting (attempt ${sshTunnelStatus.reconnectAttempts})...`}
752
+ {sshTunnelStatus.state === 'error' && `Error: ${sshTunnelStatus.error}`}
753
+ </span>
754
+ </div>
755
+ {sshTunnelStatus.state === 'connected' && sshTunnelStatus.localEndpoint && (
756
+ <div className="status-details">
757
+ <div className="detail-row">
758
+ <span className="label">Local Endpoint:</span>
759
+ <code>{sshTunnelStatus.localEndpoint}</code>
760
+ </div>
761
+ {sshTunnelStatus.pid && (
762
+ <div className="detail-row">
763
+ <span className="label">Process ID:</span>
764
+ <span>{sshTunnelStatus.pid}</span>
765
+ </div>
766
+ )}
767
+ </div>
768
+ )}
769
+ </div>
770
+ )}
771
+
772
+ <div className="settings-row">
773
+ <label>SSH Host:</label>
774
+ <input
775
+ type="text"
776
+ value={sshHost}
777
+ onChange={(e) => setSshHost(e.target.value)}
778
+ placeholder="remote-server.com"
779
+ className="settings-input"
780
+ disabled={sshTunnelStatus?.state === 'connected'}
781
+ />
782
+ </div>
783
+
784
+ <div className="settings-row-group">
785
+ <div className="settings-row half">
786
+ <label>Username:</label>
787
+ <input
788
+ type="text"
789
+ value={sshUsername}
790
+ onChange={(e) => setSshUsername(e.target.value)}
791
+ placeholder="username"
792
+ className="settings-input"
793
+ disabled={sshTunnelStatus?.state === 'connected'}
794
+ />
795
+ </div>
796
+ <div className="settings-row half">
797
+ <label>SSH Port:</label>
798
+ <input
799
+ type="number"
800
+ value={sshPort}
801
+ onChange={(e) => setSshPort(parseInt(e.target.value) || 22)}
802
+ className="settings-input"
803
+ disabled={sshTunnelStatus?.state === 'connected'}
804
+ />
805
+ </div>
806
+ </div>
807
+
808
+ <div className="settings-row">
809
+ <label>SSH Key Path (optional):</label>
810
+ <input
811
+ type="text"
812
+ value={sshKeyPath}
813
+ onChange={(e) => setSshKeyPath(e.target.value)}
814
+ placeholder="~/.ssh/id_rsa"
815
+ className="settings-input"
816
+ disabled={sshTunnelStatus?.state === 'connected'}
817
+ />
818
+ </div>
819
+
820
+ <div className="settings-row-group">
821
+ <div className="settings-row half">
822
+ <label>Local Port:</label>
823
+ <input
824
+ type="number"
825
+ value={sshLocalPort}
826
+ onChange={(e) => setSshLocalPort(parseInt(e.target.value) || 18789)}
827
+ className="settings-input"
828
+ disabled={sshTunnelStatus?.state === 'connected'}
829
+ />
830
+ </div>
831
+ <div className="settings-row half">
832
+ <label>Remote Port:</label>
833
+ <input
834
+ type="number"
835
+ value={sshRemotePort}
836
+ onChange={(e) => setSshRemotePort(parseInt(e.target.value) || 18789)}
837
+ className="settings-input"
838
+ disabled={sshTunnelStatus?.state === 'connected'}
839
+ />
840
+ </div>
841
+ </div>
842
+
843
+ {sshTestResult && (
844
+ <div className={`test-result ${sshTestResult.success ? 'success' : 'error'}`}>
845
+ {sshTestResult.success ? (
846
+ <>SSH connection successful{sshTestResult.latencyMs && ` (${sshTestResult.latencyMs}ms)`}</>
847
+ ) : (
848
+ sshTestResult.message
849
+ )}
850
+ </div>
851
+ )}
852
+
853
+ <div className="button-row">
854
+ {sshTunnelStatus?.state === 'connected' ? (
855
+ <button
856
+ onClick={handleDisconnectSshTunnel}
857
+ disabled={saving}
858
+ className="btn-secondary"
859
+ >
860
+ Disconnect Tunnel
861
+ </button>
862
+ ) : (
863
+ <>
864
+ <button
865
+ onClick={handleTestSshTunnel}
866
+ disabled={testingSshTunnel || !sshHost || !sshUsername}
867
+ className="btn-secondary"
868
+ >
869
+ {testingSshTunnel ? 'Testing...' : 'Test SSH'}
870
+ </button>
871
+ <button
872
+ onClick={handleConnectSshTunnel}
873
+ disabled={saving || !sshHost || !sshUsername}
874
+ className="btn-primary"
875
+ >
876
+ {saving ? 'Creating Tunnel...' : 'Create Tunnel'}
877
+ </button>
878
+ </>
879
+ )}
880
+ </div>
881
+
882
+ {sshTunnelStatus?.state !== 'connected' && (
883
+ <p className="hint" style={{ marginTop: '0.75rem' }}>
884
+ <strong>Manual alternative:</strong> Run <code>ssh -N -L {sshLocalPort}:127.0.0.1:{sshRemotePort} {sshUsername || 'user'}@{sshHost || 'remote-host'}</code>
885
+ </p>
886
+ )}
887
+ </div>
888
+ </>
889
+ )}
890
+
891
+ <style>{`
892
+ .connection-mode-selector {
893
+ display: flex;
894
+ gap: 1rem;
895
+ margin-bottom: 1rem;
896
+ }
897
+
898
+ .mode-option {
899
+ flex: 1;
900
+ display: flex;
901
+ align-items: flex-start;
902
+ gap: 0.75rem;
903
+ padding: 1rem;
904
+ border: 1px solid var(--border-color);
905
+ border-radius: 8px;
906
+ cursor: pointer;
907
+ transition: all 0.2s;
908
+ }
909
+
910
+ .mode-option:hover {
911
+ border-color: var(--accent-color);
912
+ }
913
+
914
+ .mode-option.selected {
915
+ border-color: var(--accent-color);
916
+ background: var(--accent-color-light, rgba(var(--accent-rgb), 0.1));
917
+ }
918
+
919
+ .mode-option input {
920
+ margin-top: 4px;
921
+ }
922
+
923
+ .mode-content {
924
+ display: flex;
925
+ flex-direction: column;
926
+ gap: 0.25rem;
927
+ }
928
+
929
+ .mode-title {
930
+ font-weight: 500;
931
+ }
932
+
933
+ .mode-description {
934
+ font-size: 0.85rem;
935
+ color: var(--text-secondary);
936
+ }
937
+
938
+ .status-card {
939
+ background: var(--bg-secondary);
940
+ border-radius: 8px;
941
+ padding: 1rem;
942
+ margin: 0.5rem 0;
943
+ }
944
+
945
+ .status-indicator {
946
+ display: flex;
947
+ align-items: center;
948
+ gap: 0.5rem;
949
+ margin-bottom: 0.5rem;
950
+ }
951
+
952
+ .status-dot {
953
+ width: 10px;
954
+ height: 10px;
955
+ border-radius: 50%;
956
+ background: var(--text-secondary);
957
+ }
958
+
959
+ .status-dot.running {
960
+ background: #22c55e;
961
+ }
962
+
963
+ .status-dot.stopped {
964
+ background: #6b7280;
965
+ }
966
+
967
+ .status-dot.connecting {
968
+ background: #f59e0b;
969
+ animation: pulse 1.5s infinite;
970
+ }
971
+
972
+ @keyframes pulse {
973
+ 0%, 100% { opacity: 1; }
974
+ 50% { opacity: 0.5; }
975
+ }
976
+
977
+ .status-details {
978
+ margin-top: 0.5rem;
979
+ font-size: 0.9rem;
980
+ }
981
+
982
+ .detail-row {
983
+ display: flex;
984
+ align-items: center;
985
+ gap: 0.5rem;
986
+ margin-bottom: 0.25rem;
987
+ }
988
+
989
+ .detail-row .label {
990
+ color: var(--text-secondary);
991
+ min-width: 100px;
992
+ }
993
+
994
+ .detail-row code {
995
+ background: var(--bg-tertiary);
996
+ padding: 0.25rem 0.5rem;
997
+ border-radius: 4px;
998
+ font-size: 0.85rem;
999
+ }
1000
+
1001
+ .token-display {
1002
+ display: flex;
1003
+ gap: 0.5rem;
1004
+ margin-bottom: 0.5rem;
1005
+ }
1006
+
1007
+ .token-input {
1008
+ flex: 1;
1009
+ font-family: monospace;
1010
+ }
1011
+
1012
+ .code-block {
1013
+ display: flex;
1014
+ align-items: center;
1015
+ gap: 0.5rem;
1016
+ background: var(--bg-secondary);
1017
+ padding: 0.75rem 1rem;
1018
+ border-radius: 6px;
1019
+ margin: 0.5rem 0;
1020
+ }
1021
+
1022
+ .code-block code {
1023
+ flex: 1;
1024
+ font-size: 0.85rem;
1025
+ word-break: break-all;
1026
+ }
1027
+
1028
+ .copy-btn {
1029
+ padding: 0.25rem 0.5rem;
1030
+ font-size: 0.75rem;
1031
+ background: var(--bg-tertiary);
1032
+ border: none;
1033
+ border-radius: 4px;
1034
+ cursor: pointer;
1035
+ color: var(--text-secondary);
1036
+ }
1037
+
1038
+ .copy-btn:hover {
1039
+ background: var(--accent-color);
1040
+ color: white;
1041
+ }
1042
+
1043
+ .button-row {
1044
+ display: flex;
1045
+ gap: 0.5rem;
1046
+ margin-top: 1rem;
1047
+ }
1048
+
1049
+ .btn-primary {
1050
+ background: var(--accent-color);
1051
+ color: white;
1052
+ border: none;
1053
+ padding: 0.5rem 1rem;
1054
+ border-radius: 6px;
1055
+ cursor: pointer;
1056
+ }
1057
+
1058
+ .btn-primary:hover:not(:disabled) {
1059
+ opacity: 0.9;
1060
+ }
1061
+
1062
+ .btn-secondary {
1063
+ background: var(--bg-secondary);
1064
+ color: var(--text-primary);
1065
+ border: 1px solid var(--border-color);
1066
+ padding: 0.5rem 1rem;
1067
+ border-radius: 6px;
1068
+ cursor: pointer;
1069
+ }
1070
+
1071
+ .btn-secondary:hover:not(:disabled) {
1072
+ background: var(--bg-tertiary);
1073
+ }
1074
+
1075
+ .btn-icon {
1076
+ padding: 0.5rem;
1077
+ background: var(--bg-secondary);
1078
+ border: 1px solid var(--border-color);
1079
+ border-radius: 4px;
1080
+ cursor: pointer;
1081
+ font-size: 0.75rem;
1082
+ }
1083
+
1084
+ .test-result {
1085
+ padding: 0.75rem 1rem;
1086
+ border-radius: 6px;
1087
+ margin: 0.5rem 0;
1088
+ }
1089
+
1090
+ .test-result.success {
1091
+ background: rgba(34, 197, 94, 0.1);
1092
+ color: #22c55e;
1093
+ border: 1px solid rgba(34, 197, 94, 0.3);
1094
+ }
1095
+
1096
+ .test-result.error {
1097
+ background: rgba(239, 68, 68, 0.1);
1098
+ color: #ef4444;
1099
+ border: 1px solid rgba(239, 68, 68, 0.3);
1100
+ }
1101
+
1102
+ .hint {
1103
+ font-size: 0.85rem;
1104
+ color: var(--text-secondary);
1105
+ margin: 0.5rem 0;
1106
+ }
1107
+
1108
+ .settings-input {
1109
+ width: 100%;
1110
+ padding: 0.5rem;
1111
+ border: 1px solid var(--border-color);
1112
+ border-radius: 4px;
1113
+ background: var(--bg-primary);
1114
+ color: var(--text-primary);
1115
+ }
1116
+
1117
+ .settings-input:disabled {
1118
+ opacity: 0.6;
1119
+ cursor: not-allowed;
1120
+ }
1121
+
1122
+ .settings-row-group {
1123
+ display: flex;
1124
+ gap: 1rem;
1125
+ margin-bottom: 0.75rem;
1126
+ }
1127
+
1128
+ .settings-row.half {
1129
+ flex: 1;
1130
+ }
1131
+ `}</style>
1132
+ </div>
1133
+ );
1134
+ }