cowork-os 0.3.21 → 0.3.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/README.md +372 -10
  2. package/connectors/README.md +20 -0
  3. package/connectors/asana-mcp/README.md +24 -0
  4. package/connectors/asana-mcp/dist/index.js +427 -0
  5. package/connectors/asana-mcp/package.json +15 -0
  6. package/connectors/asana-mcp/src/index.ts +553 -0
  7. package/connectors/asana-mcp/tsconfig.json +13 -0
  8. package/connectors/hubspot-mcp/README.md +35 -0
  9. package/connectors/hubspot-mcp/dist/index.js +454 -0
  10. package/connectors/hubspot-mcp/package.json +15 -0
  11. package/connectors/hubspot-mcp/src/index.ts +562 -0
  12. package/connectors/hubspot-mcp/tsconfig.json +13 -0
  13. package/connectors/jira-mcp/README.md +49 -0
  14. package/connectors/jira-mcp/dist/index.js +588 -0
  15. package/connectors/jira-mcp/package.json +15 -0
  16. package/connectors/jira-mcp/src/index.ts +711 -0
  17. package/connectors/jira-mcp/tsconfig.json +13 -0
  18. package/connectors/linear-mcp/README.md +22 -0
  19. package/connectors/linear-mcp/dist/index.js +402 -0
  20. package/connectors/linear-mcp/package.json +15 -0
  21. package/connectors/linear-mcp/src/index.ts +522 -0
  22. package/connectors/linear-mcp/tsconfig.json +13 -0
  23. package/connectors/okta-mcp/README.md +24 -0
  24. package/connectors/okta-mcp/dist/index.js +411 -0
  25. package/connectors/okta-mcp/package.json +15 -0
  26. package/connectors/okta-mcp/src/index.ts +520 -0
  27. package/connectors/okta-mcp/tsconfig.json +13 -0
  28. package/connectors/salesforce-mcp/README.md +47 -0
  29. package/connectors/salesforce-mcp/dist/index.js +584 -0
  30. package/connectors/salesforce-mcp/package.json +15 -0
  31. package/connectors/salesforce-mcp/src/index.ts +722 -0
  32. package/connectors/salesforce-mcp/tsconfig.json +13 -0
  33. package/connectors/servicenow-mcp/README.md +26 -0
  34. package/connectors/servicenow-mcp/dist/index.js +400 -0
  35. package/connectors/servicenow-mcp/package.json +15 -0
  36. package/connectors/servicenow-mcp/src/index.ts +500 -0
  37. package/connectors/servicenow-mcp/tsconfig.json +13 -0
  38. package/connectors/templates/mcp-connector/README.md +31 -0
  39. package/connectors/templates/mcp-connector/package.json +15 -0
  40. package/connectors/templates/mcp-connector/src/index.ts +330 -0
  41. package/connectors/templates/mcp-connector/tsconfig.json +13 -0
  42. package/connectors/zendesk-mcp/README.md +40 -0
  43. package/connectors/zendesk-mcp/dist/index.js +431 -0
  44. package/connectors/zendesk-mcp/package.json +15 -0
  45. package/connectors/zendesk-mcp/src/index.ts +543 -0
  46. package/connectors/zendesk-mcp/tsconfig.json +13 -0
  47. package/dist/electron/electron/agent/custom-skill-loader.js +31 -1
  48. package/dist/electron/electron/agent/daemon.js +189 -13
  49. package/dist/electron/electron/agent/executor.js +895 -78
  50. package/dist/electron/electron/agent/llm/anthropic-compatible-provider.js +177 -0
  51. package/dist/electron/electron/agent/llm/azure-openai-provider.js +328 -0
  52. package/dist/electron/electron/agent/llm/bedrock-provider.js +49 -9
  53. package/dist/electron/electron/agent/llm/github-copilot-provider.js +97 -0
  54. package/dist/electron/electron/agent/llm/groq-provider.js +33 -0
  55. package/dist/electron/electron/agent/llm/index.js +13 -1
  56. package/dist/electron/electron/agent/llm/kimi-provider.js +33 -0
  57. package/dist/electron/electron/agent/llm/openai-compatible-provider.js +116 -0
  58. package/dist/electron/electron/agent/llm/openai-compatible.js +111 -0
  59. package/dist/electron/electron/agent/llm/openai-oauth.js +2 -1
  60. package/dist/electron/electron/agent/llm/openrouter-provider.js +1 -1
  61. package/dist/electron/electron/agent/llm/provider-factory.js +350 -4
  62. package/dist/electron/electron/agent/llm/types.js +66 -1
  63. package/dist/electron/electron/agent/llm/xai-provider.js +33 -0
  64. package/dist/electron/electron/agent/search/provider-factory.js +38 -2
  65. package/dist/electron/electron/agent/tools/box-tools.js +231 -0
  66. package/dist/electron/electron/agent/tools/builtin-settings.js +28 -0
  67. package/dist/electron/electron/agent/tools/dropbox-tools.js +237 -0
  68. package/dist/electron/electron/agent/tools/file-tools.js +66 -3
  69. package/dist/electron/electron/agent/tools/google-drive-tools.js +227 -0
  70. package/dist/electron/electron/agent/tools/grep-tools.js +90 -10
  71. package/dist/electron/electron/agent/tools/image-tools.js +11 -1
  72. package/dist/electron/electron/agent/tools/notion-tools.js +312 -0
  73. package/dist/electron/electron/agent/tools/onedrive-tools.js +217 -0
  74. package/dist/electron/electron/agent/tools/registry.js +548 -10
  75. package/dist/electron/electron/agent/tools/search-tools.js +28 -10
  76. package/dist/electron/electron/agent/tools/sharepoint-tools.js +243 -0
  77. package/dist/electron/electron/agent/tools/shell-tools.js +12 -3
  78. package/dist/electron/electron/agent/tools/x-tools.js +1 -1
  79. package/dist/electron/electron/agents/agent-dispatch.js +63 -0
  80. package/dist/electron/electron/database/repositories.js +19 -5
  81. package/dist/electron/electron/database/schema.js +8 -0
  82. package/dist/electron/electron/gateway/channels/whatsapp.js +55 -0
  83. package/dist/electron/electron/gateway/index.js +75 -1
  84. package/dist/electron/electron/gateway/router.js +209 -154
  85. package/dist/electron/electron/ipc/canvas-handlers.js +5 -0
  86. package/dist/electron/electron/ipc/handlers.js +763 -267
  87. package/dist/electron/electron/main.js +63 -0
  88. package/dist/electron/electron/mcp/oauth/connector-oauth.js +333 -0
  89. package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +503 -154
  90. package/dist/electron/electron/memory/MemoryService.js +2 -1
  91. package/dist/electron/electron/preload.js +78 -1
  92. package/dist/electron/electron/settings/appearance-manager.js +18 -1
  93. package/dist/electron/electron/settings/box-manager.js +54 -0
  94. package/dist/electron/electron/settings/dropbox-manager.js +54 -0
  95. package/dist/electron/electron/settings/google-drive-manager.js +54 -0
  96. package/dist/electron/electron/settings/notion-manager.js +56 -0
  97. package/dist/electron/electron/settings/onedrive-manager.js +54 -0
  98. package/dist/electron/electron/settings/sharepoint-manager.js +54 -0
  99. package/dist/electron/electron/utils/box-api.js +153 -0
  100. package/dist/electron/electron/utils/dropbox-api.js +144 -0
  101. package/dist/electron/electron/utils/env-migration.js +19 -0
  102. package/dist/electron/electron/utils/google-drive-api.js +152 -0
  103. package/dist/electron/electron/utils/notion-api.js +103 -0
  104. package/dist/electron/electron/utils/onedrive-api.js +113 -0
  105. package/dist/electron/electron/utils/sharepoint-api.js +109 -0
  106. package/dist/electron/electron/utils/validation.js +98 -3
  107. package/dist/electron/electron/utils/x-cli.js +1 -1
  108. package/dist/electron/shared/channelMessages.js +284 -3
  109. package/dist/electron/shared/llm-provider-catalog.js +198 -0
  110. package/dist/electron/shared/types.js +90 -1
  111. package/package.json +14 -3
  112. package/resources/skills/nano-banana-pro.json +4 -4
  113. package/resources/skills/openai-image-gen.json +3 -3
  114. package/resources/skills/scripts/gen.py +163 -0
  115. package/resources/skills/scripts/generate_image.py +91 -0
  116. package/src/electron/agent/custom-skill-loader.ts +34 -1
  117. package/src/electron/agent/daemon.ts +210 -14
  118. package/src/electron/agent/executor.ts +1124 -85
  119. package/src/electron/agent/llm/anthropic-compatible-provider.ts +214 -0
  120. package/src/electron/agent/llm/azure-openai-provider.ts +388 -0
  121. package/src/electron/agent/llm/bedrock-provider.ts +62 -9
  122. package/src/electron/agent/llm/github-copilot-provider.ts +117 -0
  123. package/src/electron/agent/llm/groq-provider.ts +39 -0
  124. package/src/electron/agent/llm/index.ts +6 -0
  125. package/src/electron/agent/llm/kimi-provider.ts +39 -0
  126. package/src/electron/agent/llm/openai-compatible-provider.ts +153 -0
  127. package/src/electron/agent/llm/openai-compatible.ts +133 -0
  128. package/src/electron/agent/llm/openai-oauth.ts +2 -1
  129. package/src/electron/agent/llm/openrouter-provider.ts +2 -1
  130. package/src/electron/agent/llm/provider-factory.ts +459 -6
  131. package/src/electron/agent/llm/types.ts +95 -1
  132. package/src/electron/agent/llm/xai-provider.ts +39 -0
  133. package/src/electron/agent/search/provider-factory.ts +43 -2
  134. package/src/electron/agent/tools/box-tools.ts +239 -0
  135. package/src/electron/agent/tools/builtin-settings.ts +36 -0
  136. package/src/electron/agent/tools/dropbox-tools.ts +237 -0
  137. package/src/electron/agent/tools/file-tools.ts +66 -3
  138. package/src/electron/agent/tools/gmail-tools.ts +240 -0
  139. package/src/electron/agent/tools/google-calendar-tools.ts +258 -0
  140. package/src/electron/agent/tools/google-drive-tools.ts +228 -0
  141. package/src/electron/agent/tools/grep-tools.ts +97 -12
  142. package/src/electron/agent/tools/image-tools.ts +11 -1
  143. package/src/electron/agent/tools/notion-tools.ts +330 -0
  144. package/src/electron/agent/tools/onedrive-tools.ts +217 -0
  145. package/src/electron/agent/tools/registry.ts +794 -10
  146. package/src/electron/agent/tools/search-tools.ts +29 -11
  147. package/src/electron/agent/tools/sharepoint-tools.ts +247 -0
  148. package/src/electron/agent/tools/shell-tools.ts +11 -3
  149. package/src/electron/agent/tools/x-tools.ts +1 -1
  150. package/src/electron/agents/agent-dispatch.ts +79 -0
  151. package/src/electron/database/SecureSettingsRepository.ts +7 -1
  152. package/src/electron/database/repositories.ts +58 -6
  153. package/src/electron/database/schema.ts +8 -0
  154. package/src/electron/gateway/channels/discord.ts +4 -0
  155. package/src/electron/gateway/channels/google-chat.ts +3 -0
  156. package/src/electron/gateway/channels/line.ts +3 -0
  157. package/src/electron/gateway/channels/matrix-client.ts +15 -0
  158. package/src/electron/gateway/channels/matrix.ts +31 -0
  159. package/src/electron/gateway/channels/mattermost.ts +3 -0
  160. package/src/electron/gateway/channels/signal.ts +3 -0
  161. package/src/electron/gateway/channels/slack.ts +9 -4
  162. package/src/electron/gateway/channels/teams.ts +4 -0
  163. package/src/electron/gateway/channels/telegram.ts +2 -0
  164. package/src/electron/gateway/channels/twitch.ts +2 -0
  165. package/src/electron/gateway/channels/types.ts +8 -0
  166. package/src/electron/gateway/channels/whatsapp.ts +66 -0
  167. package/src/electron/gateway/index.ts +95 -2
  168. package/src/electron/gateway/router.ts +231 -161
  169. package/src/electron/gateway/security.ts +21 -9
  170. package/src/electron/ipc/canvas-handlers.ts +10 -0
  171. package/src/electron/ipc/handlers.ts +848 -292
  172. package/src/electron/main.ts +35 -0
  173. package/src/electron/mcp/oauth/connector-oauth.ts +448 -0
  174. package/src/electron/mcp/registry/MCPRegistryManager.ts +343 -12
  175. package/src/electron/memory/MemoryService.ts +7 -1
  176. package/src/electron/preload.ts +200 -5
  177. package/src/electron/settings/appearance-manager.ts +20 -2
  178. package/src/electron/settings/box-manager.ts +58 -0
  179. package/src/electron/settings/dropbox-manager.ts +58 -0
  180. package/src/electron/settings/google-workspace-manager.ts +59 -0
  181. package/src/electron/settings/notion-manager.ts +60 -0
  182. package/src/electron/settings/onedrive-manager.ts +58 -0
  183. package/src/electron/settings/sharepoint-manager.ts +58 -0
  184. package/src/electron/utils/box-api.ts +184 -0
  185. package/src/electron/utils/dropbox-api.ts +171 -0
  186. package/src/electron/utils/env-migration.ts +22 -0
  187. package/src/electron/utils/gmail-api.ts +121 -0
  188. package/src/electron/utils/google-calendar-api.ts +115 -0
  189. package/src/electron/utils/google-workspace-api.ts +228 -0
  190. package/src/electron/utils/google-workspace-auth.ts +109 -0
  191. package/src/electron/utils/google-workspace-oauth.ts +232 -0
  192. package/src/electron/utils/notion-api.ts +126 -0
  193. package/src/electron/utils/onedrive-api.ts +137 -0
  194. package/src/electron/utils/sharepoint-api.ts +132 -0
  195. package/src/electron/utils/validation.ts +128 -1
  196. package/src/electron/utils/x-cli.ts +1 -1
  197. package/src/renderer/App.tsx +119 -8
  198. package/src/renderer/components/ActivityFeedItem.tsx +34 -17
  199. package/src/renderer/components/AgentWorkingStatePanel.tsx +7 -5
  200. package/src/renderer/components/AppearanceSettings.tsx +37 -2
  201. package/src/renderer/components/BlueBubblesSettings.tsx +18 -7
  202. package/src/renderer/components/BoxSettings.tsx +203 -0
  203. package/src/renderer/components/BrowserView.tsx +101 -0
  204. package/src/renderer/components/BuiltinToolsSettings.tsx +105 -0
  205. package/src/renderer/components/CanvasPreview.tsx +68 -1
  206. package/src/renderer/components/ConnectorEnvModal.tsx +116 -0
  207. package/src/renderer/components/ConnectorSetupModal.tsx +566 -0
  208. package/src/renderer/components/ConnectorsSettings.tsx +397 -0
  209. package/src/renderer/components/ControlPlaneSettings.tsx +2 -0
  210. package/src/renderer/components/DiscordSettings.tsx +18 -7
  211. package/src/renderer/components/DropboxSettings.tsx +202 -0
  212. package/src/renderer/components/EmailSettings.tsx +18 -7
  213. package/src/renderer/components/FileViewer.tsx +21 -13
  214. package/src/renderer/components/GoogleChatSettings.tsx +17 -7
  215. package/src/renderer/components/GoogleWorkspaceSettings.tsx +332 -0
  216. package/src/renderer/components/ImessageSettings.tsx +22 -11
  217. package/src/renderer/components/LineIcons.tsx +376 -0
  218. package/src/renderer/components/LineSettings.tsx +18 -7
  219. package/src/renderer/components/MCPSettings.tsx +56 -0
  220. package/src/renderer/components/MainContent.tsx +740 -76
  221. package/src/renderer/components/MatrixSettings.tsx +18 -7
  222. package/src/renderer/components/MattermostSettings.tsx +18 -7
  223. package/src/renderer/components/NodesSettings.tsx +58 -99
  224. package/src/renderer/components/NotificationPanel.tsx +25 -11
  225. package/src/renderer/components/NotionSettings.tsx +231 -0
  226. package/src/renderer/components/Onboarding/Onboarding.tsx +13 -1
  227. package/src/renderer/components/OnboardingModal.tsx +70 -1
  228. package/src/renderer/components/OneDriveSettings.tsx +212 -0
  229. package/src/renderer/components/RightPanel.tsx +141 -28
  230. package/src/renderer/components/ScheduledTasksSettings.tsx +10 -62
  231. package/src/renderer/components/SearchSettings.tsx +118 -114
  232. package/src/renderer/components/Settings.tsx +1425 -651
  233. package/src/renderer/components/SharePointSettings.tsx +224 -0
  234. package/src/renderer/components/Sidebar.tsx +94 -19
  235. package/src/renderer/components/SignalSettings.tsx +18 -7
  236. package/src/renderer/components/SkillHubBrowser.tsx +144 -185
  237. package/src/renderer/components/SlackSettings.tsx +18 -7
  238. package/src/renderer/components/TaskQuickActions.tsx +11 -6
  239. package/src/renderer/components/TaskTimeline.tsx +58 -26
  240. package/src/renderer/components/TeamsSettings.tsx +18 -7
  241. package/src/renderer/components/TelegramSettings.tsx +18 -7
  242. package/src/renderer/components/ThemeIcon.tsx +16 -0
  243. package/src/renderer/components/TwitchSettings.tsx +18 -7
  244. package/src/renderer/components/VoiceSettings.tsx +30 -74
  245. package/src/renderer/components/WhatsAppSettings.tsx +48 -37
  246. package/src/renderer/components/WorkingStateHistory.tsx +7 -5
  247. package/src/renderer/components/WorkspaceSelector.tsx +42 -13
  248. package/src/renderer/hooks/useOnboardingFlow.ts +21 -0
  249. package/src/renderer/styles/index.css +2333 -209
  250. package/src/shared/channelMessages.ts +367 -4
  251. package/src/shared/llm-provider-catalog.ts +217 -0
  252. package/src/shared/types.ts +251 -2
@@ -4,6 +4,8 @@ import ReactMarkdown from 'react-markdown';
4
4
  import remarkGfm from 'remark-gfm';
5
5
  import { FileViewerResult } from '../../electron/preload';
6
6
  import { useAgentContext } from '../hooks/useAgentContext';
7
+ import { ThemeIcon } from './ThemeIcon';
8
+ import { AlertTriangleIcon, CodeIcon, FileIcon, FileTextIcon, GlobeIcon, ImageIcon, PresentationIcon } from './LineIcons';
7
9
 
8
10
  interface FileViewerProps {
9
11
  filePath: string;
@@ -66,17 +68,17 @@ export function FileViewer({ filePath, workspacePath, onClose }: FileViewerProps
66
68
  };
67
69
 
68
70
  // Get file icon based on type
69
- const getFileIcon = (type?: string): string => {
71
+ const getFileIcon = (type?: string): React.ReactNode => {
70
72
  switch (type) {
71
- case 'markdown': return '📝';
72
- case 'code': return '💻';
73
- case 'text': return '📄';
74
- case 'docx': return '📘';
75
- case 'pdf': return '📕';
76
- case 'image': return '🖼️';
77
- case 'pptx': return '📊';
78
- case 'html': return '🌐';
79
- default: return '📁';
73
+ case 'markdown': return <ThemeIcon emoji="📝" icon={<FileTextIcon size={16} />} />;
74
+ case 'code': return <ThemeIcon emoji="💻" icon={<CodeIcon size={16} />} />;
75
+ case 'text': return <ThemeIcon emoji="📄" icon={<FileIcon size={16} />} />;
76
+ case 'docx': return <ThemeIcon emoji="📘" icon={<FileTextIcon size={16} />} />;
77
+ case 'pdf': return <ThemeIcon emoji="📕" icon={<FileTextIcon size={16} />} />;
78
+ case 'image': return <ThemeIcon emoji="🖼️" icon={<ImageIcon size={16} />} />;
79
+ case 'pptx': return <ThemeIcon emoji="📊" icon={<PresentationIcon size={16} />} />;
80
+ case 'html': return <ThemeIcon emoji="🌐" icon={<GlobeIcon size={16} />} />;
81
+ default: return <ThemeIcon emoji="📁" icon={<FileIcon size={16} />} />;
80
82
  }
81
83
  };
82
84
 
@@ -141,7 +143,9 @@ export function FileViewer({ filePath, workspacePath, onClose }: FileViewerProps
141
143
  case 'pptx':
142
144
  return (
143
145
  <div className="file-viewer-placeholder">
144
- <span className="file-viewer-placeholder-icon">📊</span>
146
+ <span className="file-viewer-placeholder-icon">
147
+ <ThemeIcon emoji="📊" icon={<PresentationIcon size={28} />} />
148
+ </span>
145
149
  <p>PowerPoint preview is not available.</p>
146
150
  <button onClick={handleOpenExternal} className="file-viewer-open-btn">
147
151
  Open in PowerPoint
@@ -152,7 +156,9 @@ export function FileViewer({ filePath, workspacePath, onClose }: FileViewerProps
152
156
  default:
153
157
  return (
154
158
  <div className="file-viewer-placeholder">
155
- <span className="file-viewer-placeholder-icon">📁</span>
159
+ <span className="file-viewer-placeholder-icon">
160
+ <ThemeIcon emoji="📁" icon={<FileIcon size={28} />} />
161
+ </span>
156
162
  <p>This file type cannot be previewed.</p>
157
163
  <button onClick={handleOpenExternal} className="file-viewer-open-btn">
158
164
  Open with Default App
@@ -207,7 +213,9 @@ export function FileViewer({ filePath, workspacePath, onClose }: FileViewerProps
207
213
 
208
214
  {error && (
209
215
  <div className="file-viewer-error">
210
- <span className="file-viewer-error-icon">⚠️</span>
216
+ <span className="file-viewer-error-icon">
217
+ <ThemeIcon emoji="⚠️" icon={<AlertTriangleIcon size={18} />} />
218
+ </span>
211
219
  <p>{error}</p>
212
220
  <button onClick={handleOpenExternal} className="file-viewer-open-btn">
213
221
  Try Opening with Default App
@@ -1,4 +1,4 @@
1
- import { useState, useEffect } from 'react';
1
+ import { useState, useEffect, useCallback } from 'react';
2
2
  import { ChannelData, ChannelUserData, SecurityMode, ContextType, ContextPolicy } from '../../shared/types';
3
3
  import { PairingCodeDisplay } from './PairingCodeDisplay';
4
4
  import { ContextPolicySettings } from './ContextPolicySettings';
@@ -32,11 +32,7 @@ export function GoogleChatSettings({ onStatusChange }: GoogleChatSettingsProps)
32
32
  const [contextPolicies, setContextPolicies] = useState<Record<ContextType, ContextPolicy>>({} as Record<ContextType, ContextPolicy>);
33
33
  const [savingPolicy, setSavingPolicy] = useState(false);
34
34
 
35
- useEffect(() => {
36
- loadChannel();
37
- }, []);
38
-
39
- const loadChannel = async () => {
35
+ const loadChannel = useCallback(async () => {
40
36
  try {
41
37
  setLoading(true);
42
38
  const channels = await window.electronAPI.getGatewayChannels();
@@ -65,8 +61,22 @@ export function GoogleChatSettings({ onStatusChange }: GoogleChatSettingsProps)
65
61
  } finally {
66
62
  setLoading(false);
67
63
  }
68
- };
64
+ }, [onStatusChange]);
69
65
 
66
+ useEffect(() => {
67
+ loadChannel();
68
+ }, [loadChannel]);
69
+
70
+ useEffect(() => {
71
+ const unsubscribe = window.electronAPI?.onGatewayUsersUpdated?.((data) => {
72
+ if (data?.channelType !== 'googlechat') return;
73
+ if (channel && data?.channelId && data.channelId !== channel.id) return;
74
+ loadChannel();
75
+ });
76
+ return () => {
77
+ if (unsubscribe) unsubscribe();
78
+ };
79
+ }, [channel?.id, loadChannel]);
70
80
 
71
81
  const handleAddChannel = async () => {
72
82
  if (!serviceAccountKeyPath.trim()) return;
@@ -0,0 +1,332 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { GoogleWorkspaceSettingsData } from '../../shared/types';
3
+
4
+ const DEFAULT_SCOPES = [
5
+ 'https://www.googleapis.com/auth/drive',
6
+ 'https://www.googleapis.com/auth/gmail.readonly',
7
+ 'https://www.googleapis.com/auth/gmail.send',
8
+ 'https://www.googleapis.com/auth/calendar',
9
+ ];
10
+
11
+ const DEFAULT_TIMEOUT_MS = 20000;
12
+
13
+ const scopesToText = (scopes?: string[]) => (scopes && scopes.length > 0 ? scopes.join(' ') : DEFAULT_SCOPES.join(' '));
14
+
15
+ const textToScopes = (value: string) =>
16
+ value
17
+ .split(/\s+/)
18
+ .map((scope) => scope.trim())
19
+ .filter(Boolean);
20
+
21
+ export function GoogleWorkspaceSettings() {
22
+ const [settings, setSettings] = useState<GoogleWorkspaceSettingsData | null>(null);
23
+ const [saving, setSaving] = useState(false);
24
+ const [testing, setTesting] = useState(false);
25
+ const [testResult, setTestResult] = useState<{ success: boolean; error?: string; name?: string; userId?: string; email?: string } | null>(null);
26
+ const [status, setStatus] = useState<{ configured: boolean; connected: boolean; name?: string; error?: string } | null>(null);
27
+ const [statusLoading, setStatusLoading] = useState(false);
28
+ const [oauthBusy, setOauthBusy] = useState(false);
29
+ const [oauthError, setOauthError] = useState<string | null>(null);
30
+
31
+ useEffect(() => {
32
+ loadSettings();
33
+ refreshStatus();
34
+ }, []);
35
+
36
+ const loadSettings = async () => {
37
+ try {
38
+ const loaded = await window.electronAPI.getGoogleWorkspaceSettings();
39
+ setSettings(loaded);
40
+ } catch (error) {
41
+ console.error('Failed to load Google Workspace settings:', error);
42
+ }
43
+ };
44
+
45
+ const updateSettings = (updates: Partial<GoogleWorkspaceSettingsData>) => {
46
+ if (!settings) return;
47
+ setSettings({ ...settings, ...updates });
48
+ };
49
+
50
+ const handleSave = async () => {
51
+ if (!settings) return;
52
+ setSaving(true);
53
+ setTestResult(null);
54
+ try {
55
+ const payload: GoogleWorkspaceSettingsData = { ...settings };
56
+ await window.electronAPI.saveGoogleWorkspaceSettings(payload);
57
+ setSettings(payload);
58
+ await refreshStatus();
59
+ } catch (error) {
60
+ console.error('Failed to save Google Workspace settings:', error);
61
+ } finally {
62
+ setSaving(false);
63
+ }
64
+ };
65
+
66
+ const refreshStatus = async () => {
67
+ try {
68
+ setStatusLoading(true);
69
+ const result = await window.electronAPI.getGoogleWorkspaceStatus();
70
+ setStatus(result);
71
+ } catch (error) {
72
+ console.error('Failed to load Google Workspace status:', error);
73
+ } finally {
74
+ setStatusLoading(false);
75
+ }
76
+ };
77
+
78
+ const handleTestConnection = async () => {
79
+ setTesting(true);
80
+ setTestResult(null);
81
+ try {
82
+ const result = await window.electronAPI.testGoogleWorkspaceConnection();
83
+ setTestResult(result);
84
+ await refreshStatus();
85
+ } catch (error: any) {
86
+ setTestResult({ success: false, error: error.message || 'Failed to test connection' });
87
+ } finally {
88
+ setTesting(false);
89
+ }
90
+ };
91
+
92
+ const handleOAuthConnect = async () => {
93
+ if (!settings?.clientId) {
94
+ setOauthError('Client ID is required to start OAuth.');
95
+ return;
96
+ }
97
+
98
+ setOauthBusy(true);
99
+ setOauthError(null);
100
+
101
+ try {
102
+ const scopes = settings.scopes && settings.scopes.length > 0 ? settings.scopes : DEFAULT_SCOPES;
103
+ const result = await window.electronAPI.startGoogleWorkspaceOAuth({
104
+ clientId: settings.clientId,
105
+ clientSecret: settings.clientSecret || undefined,
106
+ scopes,
107
+ });
108
+
109
+ const tokenExpiresAt = result.expiresIn ? Date.now() + result.expiresIn * 1000 : settings.tokenExpiresAt;
110
+
111
+ const payload: GoogleWorkspaceSettingsData = {
112
+ ...settings,
113
+ enabled: true,
114
+ accessToken: result.accessToken,
115
+ refreshToken: result.refreshToken || settings.refreshToken,
116
+ tokenExpiresAt,
117
+ scopes: result.scopes || scopes,
118
+ };
119
+
120
+ await window.electronAPI.saveGoogleWorkspaceSettings(payload);
121
+ setSettings(payload);
122
+ await refreshStatus();
123
+ } catch (error: any) {
124
+ setOauthError(error.message || 'Google Workspace OAuth failed');
125
+ } finally {
126
+ setOauthBusy(false);
127
+ }
128
+ };
129
+
130
+ if (!settings) {
131
+ return <div className="settings-loading">Loading Google Workspace settings...</div>;
132
+ }
133
+
134
+ const statusLabel = !status?.configured
135
+ ? 'Missing Token'
136
+ : status.connected
137
+ ? 'Connected'
138
+ : 'Configured';
139
+
140
+ const statusClass = !status?.configured
141
+ ? 'missing'
142
+ : status.connected
143
+ ? 'connected'
144
+ : 'configured';
145
+
146
+ return (
147
+ <div className="google-workspace-settings">
148
+ <div className="settings-section">
149
+ <div className="settings-section-header">
150
+ <div className="settings-title-with-badge">
151
+ <h3>Connect Google Workspace</h3>
152
+ {status && (
153
+ <span
154
+ className={`google-workspace-status-badge ${statusClass}`}
155
+ title={!status.configured ? 'Tokens not configured' : status.connected ? 'Connected to Google Workspace' : 'Configured'}
156
+ >
157
+ {statusLabel}
158
+ </span>
159
+ )}
160
+ {statusLoading && !status && (
161
+ <span className="google-workspace-status-badge configured">Checking…</span>
162
+ )}
163
+ </div>
164
+ <button className="btn-secondary btn-sm" onClick={refreshStatus} disabled={statusLoading}>
165
+ {statusLoading ? 'Checking...' : 'Refresh Status'}
166
+ </button>
167
+ </div>
168
+ <p className="settings-description">
169
+ Connect Gmail, Calendar, and Drive with a single Google Workspace OAuth flow. After connecting, use
170
+ `google_drive_action`, `gmail_action`, and `calendar_action` tools in tasks.
171
+ </p>
172
+ {status?.error && (
173
+ <p className="settings-hint">Status check: {status.error}</p>
174
+ )}
175
+ {oauthError && (
176
+ <p className="settings-hint">OAuth error: {oauthError}</p>
177
+ )}
178
+ <div className="settings-actions">
179
+ <button
180
+ className="btn-secondary btn-sm"
181
+ onClick={() => window.electronAPI.openExternal('https://console.cloud.google.com/apis/credentials')}
182
+ >
183
+ Open Google Cloud Console
184
+ </button>
185
+ <button
186
+ className="btn-primary btn-sm"
187
+ onClick={handleOAuthConnect}
188
+ disabled={oauthBusy}
189
+ >
190
+ {oauthBusy ? 'Connecting...' : 'Connect'}
191
+ </button>
192
+ </div>
193
+ </div>
194
+
195
+ <div className="settings-section">
196
+ <div className="settings-field">
197
+ <label>Enable Integration</label>
198
+ <label className="settings-toggle">
199
+ <input
200
+ type="checkbox"
201
+ checked={settings.enabled}
202
+ onChange={(e) => updateSettings({ enabled: e.target.checked })}
203
+ />
204
+ <span className="toggle-slider" />
205
+ </label>
206
+ </div>
207
+
208
+ <div className="settings-field">
209
+ <label>Client ID</label>
210
+ <input
211
+ type="text"
212
+ className="settings-input"
213
+ placeholder="Google OAuth client ID"
214
+ value={settings.clientId || ''}
215
+ onChange={(e) => updateSettings({ clientId: e.target.value || undefined })}
216
+ />
217
+ </div>
218
+
219
+ <div className="settings-field">
220
+ <label>Client Secret (optional)</label>
221
+ <input
222
+ type="password"
223
+ className="settings-input"
224
+ placeholder="Google OAuth client secret"
225
+ value={settings.clientSecret || ''}
226
+ onChange={(e) => updateSettings({ clientSecret: e.target.value || undefined })}
227
+ />
228
+ <p className="settings-hint">Use an OAuth client configured for Desktop or Web applications.</p>
229
+ </div>
230
+
231
+ <div className="settings-field">
232
+ <label>Scopes</label>
233
+ <textarea
234
+ className="settings-input"
235
+ rows={3}
236
+ value={scopesToText(settings.scopes)}
237
+ onChange={(e) => updateSettings({ scopes: textToScopes(e.target.value) })}
238
+ />
239
+ <p className="settings-hint">Space-separated scopes used during OAuth.</p>
240
+ </div>
241
+
242
+ <div className="settings-field">
243
+ <label>Access Token</label>
244
+ <input
245
+ type="password"
246
+ className="settings-input"
247
+ placeholder="Google OAuth access token"
248
+ value={settings.accessToken || ''}
249
+ onChange={(e) => updateSettings({ accessToken: e.target.value || undefined })}
250
+ />
251
+ </div>
252
+
253
+ <div className="settings-field">
254
+ <label>Refresh Token</label>
255
+ <input
256
+ type="password"
257
+ className="settings-input"
258
+ placeholder="Google OAuth refresh token"
259
+ value={settings.refreshToken || ''}
260
+ onChange={(e) => updateSettings({ refreshToken: e.target.value || undefined })}
261
+ />
262
+ </div>
263
+
264
+ <div className="settings-field">
265
+ <label>Token Expires At (ms)</label>
266
+ <input
267
+ type="number"
268
+ className="settings-input"
269
+ min={0}
270
+ value={settings.tokenExpiresAt ?? ''}
271
+ onChange={(e) => updateSettings({ tokenExpiresAt: Number(e.target.value) || undefined })}
272
+ />
273
+ <p className="settings-hint">Used for auto-refresh; set automatically after OAuth.</p>
274
+ </div>
275
+
276
+ <div className="settings-field">
277
+ <label>Timeout (ms)</label>
278
+ <input
279
+ type="number"
280
+ className="settings-input"
281
+ min={1000}
282
+ max={120000}
283
+ value={settings.timeoutMs ?? DEFAULT_TIMEOUT_MS}
284
+ onChange={(e) => updateSettings({ timeoutMs: Number(e.target.value) })}
285
+ />
286
+ </div>
287
+
288
+ <div className="settings-actions">
289
+ <button className="btn-secondary btn-sm" onClick={handleTestConnection} disabled={testing}>
290
+ {testing ? 'Testing...' : 'Test Connection'}
291
+ </button>
292
+ <button className="btn-primary btn-sm" onClick={handleSave} disabled={saving}>
293
+ {saving ? 'Saving...' : 'Save Settings'}
294
+ </button>
295
+ </div>
296
+
297
+ {testResult && (
298
+ <div className={`test-result ${testResult.success ? 'success' : 'error'}`}>
299
+ {testResult.success ? (
300
+ <span>Connected{testResult.name ? ` as ${testResult.name}` : ''}</span>
301
+ ) : (
302
+ <span>Connection failed: {testResult.error}</span>
303
+ )}
304
+ </div>
305
+ )}
306
+ </div>
307
+
308
+ <div className="settings-section">
309
+ <h4>Quick Usage</h4>
310
+ <pre className="settings-info-box">{`// Search Drive files
311
+ google_drive_action({
312
+ action: "list_files",
313
+ query: "modifiedTime > '2026-02-01T00:00:00Z'",
314
+ page_size: 10
315
+ });
316
+
317
+ // Search Gmail
318
+ gmail_action({
319
+ action: "list_messages",
320
+ query: "from:me newer_than:7d"
321
+ });
322
+
323
+ // List upcoming calendar events
324
+ calendar_action({
325
+ action: "list_events",
326
+ time_min: "2026-02-05T00:00:00Z",
327
+ max_results: 10
328
+ });`}</pre>
329
+ </div>
330
+ </div>
331
+ );
332
+ }
@@ -1,4 +1,4 @@
1
- import { useState, useEffect } from 'react';
1
+ import { useState, useEffect, useCallback } from 'react';
2
2
  import { ChannelData, ChannelUserData, SecurityMode } from '../../shared/types';
3
3
 
4
4
  interface ImessageSettingsProps {
@@ -31,15 +31,7 @@ export function ImessageSettings({ onStatusChange }: ImessageSettingsProps) {
31
31
  // Check if we're on macOS
32
32
  const [isMacOS, setIsMacOS] = useState(true);
33
33
 
34
- useEffect(() => {
35
- // Check platform
36
- const platform = navigator.platform.toLowerCase();
37
- setIsMacOS(platform.includes('mac'));
38
-
39
- loadChannel();
40
- }, []);
41
-
42
- const loadChannel = async () => {
34
+ const loadChannel = useCallback(async () => {
43
35
  try {
44
36
  setLoading(true);
45
37
  const channels = await window.electronAPI.getGatewayChannels();
@@ -70,7 +62,26 @@ export function ImessageSettings({ onStatusChange }: ImessageSettingsProps) {
70
62
  } finally {
71
63
  setLoading(false);
72
64
  }
73
- };
65
+ }, [onStatusChange]);
66
+
67
+ useEffect(() => {
68
+ // Check platform
69
+ const platform = navigator.platform.toLowerCase();
70
+ setIsMacOS(platform.includes('mac'));
71
+
72
+ loadChannel();
73
+ }, [loadChannel]);
74
+
75
+ useEffect(() => {
76
+ const unsubscribe = window.electronAPI?.onGatewayUsersUpdated?.((data) => {
77
+ if (data?.channelType !== 'imessage') return;
78
+ if (channel && data?.channelId && data.channelId !== channel.id) return;
79
+ loadChannel();
80
+ });
81
+ return () => {
82
+ if (unsubscribe) unsubscribe();
83
+ };
84
+ }, [channel?.id, loadChannel]);
74
85
 
75
86
  const handleAddChannel = async () => {
76
87
  try {