cowork-os 0.3.21 → 0.3.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/README.md +372 -10
  2. package/connectors/README.md +20 -0
  3. package/connectors/asana-mcp/README.md +24 -0
  4. package/connectors/asana-mcp/dist/index.js +427 -0
  5. package/connectors/asana-mcp/package.json +15 -0
  6. package/connectors/asana-mcp/src/index.ts +553 -0
  7. package/connectors/asana-mcp/tsconfig.json +13 -0
  8. package/connectors/hubspot-mcp/README.md +35 -0
  9. package/connectors/hubspot-mcp/dist/index.js +454 -0
  10. package/connectors/hubspot-mcp/package.json +15 -0
  11. package/connectors/hubspot-mcp/src/index.ts +562 -0
  12. package/connectors/hubspot-mcp/tsconfig.json +13 -0
  13. package/connectors/jira-mcp/README.md +49 -0
  14. package/connectors/jira-mcp/dist/index.js +588 -0
  15. package/connectors/jira-mcp/package.json +15 -0
  16. package/connectors/jira-mcp/src/index.ts +711 -0
  17. package/connectors/jira-mcp/tsconfig.json +13 -0
  18. package/connectors/linear-mcp/README.md +22 -0
  19. package/connectors/linear-mcp/dist/index.js +402 -0
  20. package/connectors/linear-mcp/package.json +15 -0
  21. package/connectors/linear-mcp/src/index.ts +522 -0
  22. package/connectors/linear-mcp/tsconfig.json +13 -0
  23. package/connectors/okta-mcp/README.md +24 -0
  24. package/connectors/okta-mcp/dist/index.js +411 -0
  25. package/connectors/okta-mcp/package.json +15 -0
  26. package/connectors/okta-mcp/src/index.ts +520 -0
  27. package/connectors/okta-mcp/tsconfig.json +13 -0
  28. package/connectors/salesforce-mcp/README.md +47 -0
  29. package/connectors/salesforce-mcp/dist/index.js +584 -0
  30. package/connectors/salesforce-mcp/package.json +15 -0
  31. package/connectors/salesforce-mcp/src/index.ts +722 -0
  32. package/connectors/salesforce-mcp/tsconfig.json +13 -0
  33. package/connectors/servicenow-mcp/README.md +26 -0
  34. package/connectors/servicenow-mcp/dist/index.js +400 -0
  35. package/connectors/servicenow-mcp/package.json +15 -0
  36. package/connectors/servicenow-mcp/src/index.ts +500 -0
  37. package/connectors/servicenow-mcp/tsconfig.json +13 -0
  38. package/connectors/templates/mcp-connector/README.md +31 -0
  39. package/connectors/templates/mcp-connector/package.json +15 -0
  40. package/connectors/templates/mcp-connector/src/index.ts +330 -0
  41. package/connectors/templates/mcp-connector/tsconfig.json +13 -0
  42. package/connectors/zendesk-mcp/README.md +40 -0
  43. package/connectors/zendesk-mcp/dist/index.js +431 -0
  44. package/connectors/zendesk-mcp/package.json +15 -0
  45. package/connectors/zendesk-mcp/src/index.ts +543 -0
  46. package/connectors/zendesk-mcp/tsconfig.json +13 -0
  47. package/dist/electron/electron/agent/custom-skill-loader.js +31 -1
  48. package/dist/electron/electron/agent/daemon.js +189 -13
  49. package/dist/electron/electron/agent/executor.js +895 -78
  50. package/dist/electron/electron/agent/llm/anthropic-compatible-provider.js +177 -0
  51. package/dist/electron/electron/agent/llm/azure-openai-provider.js +328 -0
  52. package/dist/electron/electron/agent/llm/bedrock-provider.js +49 -9
  53. package/dist/electron/electron/agent/llm/github-copilot-provider.js +97 -0
  54. package/dist/electron/electron/agent/llm/groq-provider.js +33 -0
  55. package/dist/electron/electron/agent/llm/index.js +13 -1
  56. package/dist/electron/electron/agent/llm/kimi-provider.js +33 -0
  57. package/dist/electron/electron/agent/llm/openai-compatible-provider.js +116 -0
  58. package/dist/electron/electron/agent/llm/openai-compatible.js +111 -0
  59. package/dist/electron/electron/agent/llm/openai-oauth.js +2 -1
  60. package/dist/electron/electron/agent/llm/openrouter-provider.js +1 -1
  61. package/dist/electron/electron/agent/llm/provider-factory.js +350 -4
  62. package/dist/electron/electron/agent/llm/types.js +66 -1
  63. package/dist/electron/electron/agent/llm/xai-provider.js +33 -0
  64. package/dist/electron/electron/agent/search/provider-factory.js +38 -2
  65. package/dist/electron/electron/agent/tools/box-tools.js +231 -0
  66. package/dist/electron/electron/agent/tools/builtin-settings.js +28 -0
  67. package/dist/electron/electron/agent/tools/dropbox-tools.js +237 -0
  68. package/dist/electron/electron/agent/tools/file-tools.js +66 -3
  69. package/dist/electron/electron/agent/tools/google-drive-tools.js +227 -0
  70. package/dist/electron/electron/agent/tools/grep-tools.js +90 -10
  71. package/dist/electron/electron/agent/tools/image-tools.js +11 -1
  72. package/dist/electron/electron/agent/tools/notion-tools.js +312 -0
  73. package/dist/electron/electron/agent/tools/onedrive-tools.js +217 -0
  74. package/dist/electron/electron/agent/tools/registry.js +548 -10
  75. package/dist/electron/electron/agent/tools/search-tools.js +28 -10
  76. package/dist/electron/electron/agent/tools/sharepoint-tools.js +243 -0
  77. package/dist/electron/electron/agent/tools/shell-tools.js +12 -3
  78. package/dist/electron/electron/agent/tools/x-tools.js +1 -1
  79. package/dist/electron/electron/agents/agent-dispatch.js +63 -0
  80. package/dist/electron/electron/database/repositories.js +19 -5
  81. package/dist/electron/electron/database/schema.js +8 -0
  82. package/dist/electron/electron/gateway/channels/whatsapp.js +55 -0
  83. package/dist/electron/electron/gateway/index.js +75 -1
  84. package/dist/electron/electron/gateway/router.js +209 -154
  85. package/dist/electron/electron/ipc/canvas-handlers.js +5 -0
  86. package/dist/electron/electron/ipc/handlers.js +763 -267
  87. package/dist/electron/electron/main.js +63 -0
  88. package/dist/electron/electron/mcp/oauth/connector-oauth.js +333 -0
  89. package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +503 -154
  90. package/dist/electron/electron/memory/MemoryService.js +2 -1
  91. package/dist/electron/electron/preload.js +78 -1
  92. package/dist/electron/electron/settings/appearance-manager.js +18 -1
  93. package/dist/electron/electron/settings/box-manager.js +54 -0
  94. package/dist/electron/electron/settings/dropbox-manager.js +54 -0
  95. package/dist/electron/electron/settings/google-drive-manager.js +54 -0
  96. package/dist/electron/electron/settings/notion-manager.js +56 -0
  97. package/dist/electron/electron/settings/onedrive-manager.js +54 -0
  98. package/dist/electron/electron/settings/sharepoint-manager.js +54 -0
  99. package/dist/electron/electron/utils/box-api.js +153 -0
  100. package/dist/electron/electron/utils/dropbox-api.js +144 -0
  101. package/dist/electron/electron/utils/env-migration.js +19 -0
  102. package/dist/electron/electron/utils/google-drive-api.js +152 -0
  103. package/dist/electron/electron/utils/notion-api.js +103 -0
  104. package/dist/electron/electron/utils/onedrive-api.js +113 -0
  105. package/dist/electron/electron/utils/sharepoint-api.js +109 -0
  106. package/dist/electron/electron/utils/validation.js +98 -3
  107. package/dist/electron/electron/utils/x-cli.js +1 -1
  108. package/dist/electron/shared/channelMessages.js +284 -3
  109. package/dist/electron/shared/llm-provider-catalog.js +198 -0
  110. package/dist/electron/shared/types.js +90 -1
  111. package/package.json +14 -3
  112. package/resources/skills/nano-banana-pro.json +4 -4
  113. package/resources/skills/openai-image-gen.json +3 -3
  114. package/resources/skills/scripts/gen.py +163 -0
  115. package/resources/skills/scripts/generate_image.py +91 -0
  116. package/src/electron/agent/custom-skill-loader.ts +34 -1
  117. package/src/electron/agent/daemon.ts +210 -14
  118. package/src/electron/agent/executor.ts +1124 -85
  119. package/src/electron/agent/llm/anthropic-compatible-provider.ts +214 -0
  120. package/src/electron/agent/llm/azure-openai-provider.ts +388 -0
  121. package/src/electron/agent/llm/bedrock-provider.ts +62 -9
  122. package/src/electron/agent/llm/github-copilot-provider.ts +117 -0
  123. package/src/electron/agent/llm/groq-provider.ts +39 -0
  124. package/src/electron/agent/llm/index.ts +6 -0
  125. package/src/electron/agent/llm/kimi-provider.ts +39 -0
  126. package/src/electron/agent/llm/openai-compatible-provider.ts +153 -0
  127. package/src/electron/agent/llm/openai-compatible.ts +133 -0
  128. package/src/electron/agent/llm/openai-oauth.ts +2 -1
  129. package/src/electron/agent/llm/openrouter-provider.ts +2 -1
  130. package/src/electron/agent/llm/provider-factory.ts +459 -6
  131. package/src/electron/agent/llm/types.ts +95 -1
  132. package/src/electron/agent/llm/xai-provider.ts +39 -0
  133. package/src/electron/agent/search/provider-factory.ts +43 -2
  134. package/src/electron/agent/tools/box-tools.ts +239 -0
  135. package/src/electron/agent/tools/builtin-settings.ts +36 -0
  136. package/src/electron/agent/tools/dropbox-tools.ts +237 -0
  137. package/src/electron/agent/tools/file-tools.ts +66 -3
  138. package/src/electron/agent/tools/gmail-tools.ts +240 -0
  139. package/src/electron/agent/tools/google-calendar-tools.ts +258 -0
  140. package/src/electron/agent/tools/google-drive-tools.ts +228 -0
  141. package/src/electron/agent/tools/grep-tools.ts +97 -12
  142. package/src/electron/agent/tools/image-tools.ts +11 -1
  143. package/src/electron/agent/tools/notion-tools.ts +330 -0
  144. package/src/electron/agent/tools/onedrive-tools.ts +217 -0
  145. package/src/electron/agent/tools/registry.ts +794 -10
  146. package/src/electron/agent/tools/search-tools.ts +29 -11
  147. package/src/electron/agent/tools/sharepoint-tools.ts +247 -0
  148. package/src/electron/agent/tools/shell-tools.ts +11 -3
  149. package/src/electron/agent/tools/x-tools.ts +1 -1
  150. package/src/electron/agents/agent-dispatch.ts +79 -0
  151. package/src/electron/database/SecureSettingsRepository.ts +7 -1
  152. package/src/electron/database/repositories.ts +58 -6
  153. package/src/electron/database/schema.ts +8 -0
  154. package/src/electron/gateway/channels/discord.ts +4 -0
  155. package/src/electron/gateway/channels/google-chat.ts +3 -0
  156. package/src/electron/gateway/channels/line.ts +3 -0
  157. package/src/electron/gateway/channels/matrix-client.ts +15 -0
  158. package/src/electron/gateway/channels/matrix.ts +31 -0
  159. package/src/electron/gateway/channels/mattermost.ts +3 -0
  160. package/src/electron/gateway/channels/signal.ts +3 -0
  161. package/src/electron/gateway/channels/slack.ts +9 -4
  162. package/src/electron/gateway/channels/teams.ts +4 -0
  163. package/src/electron/gateway/channels/telegram.ts +2 -0
  164. package/src/electron/gateway/channels/twitch.ts +2 -0
  165. package/src/electron/gateway/channels/types.ts +8 -0
  166. package/src/electron/gateway/channels/whatsapp.ts +66 -0
  167. package/src/electron/gateway/index.ts +95 -2
  168. package/src/electron/gateway/router.ts +231 -161
  169. package/src/electron/gateway/security.ts +21 -9
  170. package/src/electron/ipc/canvas-handlers.ts +10 -0
  171. package/src/electron/ipc/handlers.ts +848 -292
  172. package/src/electron/main.ts +35 -0
  173. package/src/electron/mcp/oauth/connector-oauth.ts +448 -0
  174. package/src/electron/mcp/registry/MCPRegistryManager.ts +343 -12
  175. package/src/electron/memory/MemoryService.ts +7 -1
  176. package/src/electron/preload.ts +200 -5
  177. package/src/electron/settings/appearance-manager.ts +20 -2
  178. package/src/electron/settings/box-manager.ts +58 -0
  179. package/src/electron/settings/dropbox-manager.ts +58 -0
  180. package/src/electron/settings/google-workspace-manager.ts +59 -0
  181. package/src/electron/settings/notion-manager.ts +60 -0
  182. package/src/electron/settings/onedrive-manager.ts +58 -0
  183. package/src/electron/settings/sharepoint-manager.ts +58 -0
  184. package/src/electron/utils/box-api.ts +184 -0
  185. package/src/electron/utils/dropbox-api.ts +171 -0
  186. package/src/electron/utils/env-migration.ts +22 -0
  187. package/src/electron/utils/gmail-api.ts +121 -0
  188. package/src/electron/utils/google-calendar-api.ts +115 -0
  189. package/src/electron/utils/google-workspace-api.ts +228 -0
  190. package/src/electron/utils/google-workspace-auth.ts +109 -0
  191. package/src/electron/utils/google-workspace-oauth.ts +232 -0
  192. package/src/electron/utils/notion-api.ts +126 -0
  193. package/src/electron/utils/onedrive-api.ts +137 -0
  194. package/src/electron/utils/sharepoint-api.ts +132 -0
  195. package/src/electron/utils/validation.ts +128 -1
  196. package/src/electron/utils/x-cli.ts +1 -1
  197. package/src/renderer/App.tsx +119 -8
  198. package/src/renderer/components/ActivityFeedItem.tsx +34 -17
  199. package/src/renderer/components/AgentWorkingStatePanel.tsx +7 -5
  200. package/src/renderer/components/AppearanceSettings.tsx +37 -2
  201. package/src/renderer/components/BlueBubblesSettings.tsx +18 -7
  202. package/src/renderer/components/BoxSettings.tsx +203 -0
  203. package/src/renderer/components/BrowserView.tsx +101 -0
  204. package/src/renderer/components/BuiltinToolsSettings.tsx +105 -0
  205. package/src/renderer/components/CanvasPreview.tsx +68 -1
  206. package/src/renderer/components/ConnectorEnvModal.tsx +116 -0
  207. package/src/renderer/components/ConnectorSetupModal.tsx +566 -0
  208. package/src/renderer/components/ConnectorsSettings.tsx +397 -0
  209. package/src/renderer/components/ControlPlaneSettings.tsx +2 -0
  210. package/src/renderer/components/DiscordSettings.tsx +18 -7
  211. package/src/renderer/components/DropboxSettings.tsx +202 -0
  212. package/src/renderer/components/EmailSettings.tsx +18 -7
  213. package/src/renderer/components/FileViewer.tsx +21 -13
  214. package/src/renderer/components/GoogleChatSettings.tsx +17 -7
  215. package/src/renderer/components/GoogleWorkspaceSettings.tsx +332 -0
  216. package/src/renderer/components/ImessageSettings.tsx +22 -11
  217. package/src/renderer/components/LineIcons.tsx +376 -0
  218. package/src/renderer/components/LineSettings.tsx +18 -7
  219. package/src/renderer/components/MCPSettings.tsx +56 -0
  220. package/src/renderer/components/MainContent.tsx +740 -76
  221. package/src/renderer/components/MatrixSettings.tsx +18 -7
  222. package/src/renderer/components/MattermostSettings.tsx +18 -7
  223. package/src/renderer/components/NodesSettings.tsx +58 -99
  224. package/src/renderer/components/NotificationPanel.tsx +25 -11
  225. package/src/renderer/components/NotionSettings.tsx +231 -0
  226. package/src/renderer/components/Onboarding/Onboarding.tsx +13 -1
  227. package/src/renderer/components/OnboardingModal.tsx +70 -1
  228. package/src/renderer/components/OneDriveSettings.tsx +212 -0
  229. package/src/renderer/components/RightPanel.tsx +141 -28
  230. package/src/renderer/components/ScheduledTasksSettings.tsx +10 -62
  231. package/src/renderer/components/SearchSettings.tsx +118 -114
  232. package/src/renderer/components/Settings.tsx +1425 -651
  233. package/src/renderer/components/SharePointSettings.tsx +224 -0
  234. package/src/renderer/components/Sidebar.tsx +94 -19
  235. package/src/renderer/components/SignalSettings.tsx +18 -7
  236. package/src/renderer/components/SkillHubBrowser.tsx +144 -185
  237. package/src/renderer/components/SlackSettings.tsx +18 -7
  238. package/src/renderer/components/TaskQuickActions.tsx +11 -6
  239. package/src/renderer/components/TaskTimeline.tsx +58 -26
  240. package/src/renderer/components/TeamsSettings.tsx +18 -7
  241. package/src/renderer/components/TelegramSettings.tsx +18 -7
  242. package/src/renderer/components/ThemeIcon.tsx +16 -0
  243. package/src/renderer/components/TwitchSettings.tsx +18 -7
  244. package/src/renderer/components/VoiceSettings.tsx +30 -74
  245. package/src/renderer/components/WhatsAppSettings.tsx +48 -37
  246. package/src/renderer/components/WorkingStateHistory.tsx +7 -5
  247. package/src/renderer/components/WorkspaceSelector.tsx +42 -13
  248. package/src/renderer/hooks/useOnboardingFlow.ts +21 -0
  249. package/src/renderer/styles/index.css +2333 -209
  250. package/src/shared/channelMessages.ts +367 -4
  251. package/src/shared/llm-provider-catalog.ts +217 -0
  252. package/src/shared/types.ts +251 -2
@@ -1,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';
@@ -33,11 +33,7 @@ export function MatrixSettings({ onStatusChange }: MatrixSettingsProps) {
33
33
  const [contextPolicies, setContextPolicies] = useState<Record<ContextType, ContextPolicy>>({} as Record<ContextType, ContextPolicy>);
34
34
  const [savingPolicy, setSavingPolicy] = useState(false);
35
35
 
36
- useEffect(() => {
37
- loadChannel();
38
- }, []);
39
-
40
- const loadChannel = async () => {
36
+ const loadChannel = useCallback(async () => {
41
37
  try {
42
38
  setLoading(true);
43
39
  const channels = await window.electronAPI.getGatewayChannels();
@@ -76,7 +72,22 @@ export function MatrixSettings({ onStatusChange }: MatrixSettingsProps) {
76
72
  } finally {
77
73
  setLoading(false);
78
74
  }
79
- };
75
+ }, [onStatusChange]);
76
+
77
+ useEffect(() => {
78
+ loadChannel();
79
+ }, [loadChannel]);
80
+
81
+ useEffect(() => {
82
+ const unsubscribe = window.electronAPI?.onGatewayUsersUpdated?.((data) => {
83
+ if (data?.channelType !== 'matrix') return;
84
+ if (channel && data?.channelId && data.channelId !== channel.id) return;
85
+ loadChannel();
86
+ });
87
+ return () => {
88
+ if (unsubscribe) unsubscribe();
89
+ };
90
+ }, [channel?.id, loadChannel]);
80
91
 
81
92
  const handleAddChannel = async () => {
82
93
  if (!homeserver.trim() || !userId.trim() || !accessToken.trim()) {
@@ -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';
@@ -31,11 +31,7 @@ export function MattermostSettings({ onStatusChange }: MattermostSettingsProps)
31
31
  const [contextPolicies, setContextPolicies] = useState<Record<ContextType, ContextPolicy>>({} as Record<ContextType, ContextPolicy>);
32
32
  const [savingPolicy, setSavingPolicy] = useState(false);
33
33
 
34
- useEffect(() => {
35
- loadChannel();
36
- }, []);
37
-
38
- const loadChannel = async () => {
34
+ const loadChannel = useCallback(async () => {
39
35
  try {
40
36
  setLoading(true);
41
37
  const channels = await window.electronAPI.getGatewayChannels();
@@ -71,7 +67,22 @@ export function MattermostSettings({ onStatusChange }: MattermostSettingsProps)
71
67
  } finally {
72
68
  setLoading(false);
73
69
  }
74
- };
70
+ }, [onStatusChange]);
71
+
72
+ useEffect(() => {
73
+ loadChannel();
74
+ }, [loadChannel]);
75
+
76
+ useEffect(() => {
77
+ const unsubscribe = window.electronAPI?.onGatewayUsersUpdated?.((data) => {
78
+ if (data?.channelType !== 'mattermost') return;
79
+ if (channel && data?.channelId && data.channelId !== channel.id) return;
80
+ loadChannel();
81
+ });
82
+ return () => {
83
+ if (unsubscribe) unsubscribe();
84
+ };
85
+ }, [channel?.id, loadChannel]);
75
86
 
76
87
  const handleAddChannel = async () => {
77
88
  if (!serverUrl.trim() || !token.trim()) {
@@ -118,101 +118,75 @@ export function NodesSettings({ compact = false }: NodesSettingsProps) {
118
118
  };
119
119
 
120
120
  if (loading) {
121
- return (
122
- <div className="flex items-center justify-center py-8">
123
- <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-accent-500" />
124
- </div>
125
- );
121
+ return <div className="settings-loading">Loading companions...</div>;
126
122
  }
127
123
 
128
124
  return (
129
- <div className="space-y-4">
130
- {/* Header */}
125
+ <div className="nodes-settings">
131
126
  {!compact && (
132
- <div className="flex items-center justify-between">
133
- <div>
134
- <h3 className="text-lg font-medium text-neutral-100">Mobile Companions</h3>
135
- <p className="text-sm text-neutral-400 mt-1">
136
- Connect iOS or Android devices as mobile companions for device-specific actions.
137
- </p>
127
+ <div className="settings-section nodes-header-section">
128
+ <div className="settings-section-header">
129
+ <div>
130
+ <h3>Mobile Companions</h3>
131
+ <p className="settings-description">
132
+ Connect iOS or Android devices as mobile companions for device-specific actions.
133
+ </p>
134
+ </div>
135
+ <button className="button-secondary button-small" onClick={loadNodes}>
136
+ Refresh
137
+ </button>
138
138
  </div>
139
- <button
140
- onClick={loadNodes}
141
- className="px-3 py-1.5 text-sm text-neutral-300 hover:text-neutral-100 bg-neutral-800 hover:bg-neutral-700 rounded-md transition-colors"
142
- >
143
- Refresh
144
- </button>
145
139
  </div>
146
140
  )}
147
141
 
148
- {/* Connection Instructions */}
149
142
  {nodes.length === 0 && (
150
- <div className="bg-neutral-800/50 rounded-lg p-4 border border-neutral-700">
151
- <h4 className="text-sm font-medium text-neutral-200 mb-2">How to Connect</h4>
152
- <ol className="text-sm text-neutral-400 space-y-2 list-decimal list-inside">
143
+ <div className="nodes-empty-card">
144
+ <h4>How to Connect</h4>
145
+ <ol>
153
146
  <li>Make sure the Control Plane is enabled and running</li>
154
147
  <li>Install the companion app on your iOS or Android device</li>
155
148
  <li>Enter the gateway URL and authentication token in the app</li>
156
149
  <li>The device will appear here once connected</li>
157
150
  </ol>
158
- <div className="mt-4 p-3 bg-neutral-900/50 rounded-md">
159
- <p className="text-xs text-neutral-500 font-mono">
160
- For local network: ws://{'<your-mac-ip>'}:18789
151
+ <div className="nodes-connection-note">
152
+ <p>
153
+ <code>For local network: ws://{'<your-mac-ip>'}:18789</code>
161
154
  </p>
162
- <p className="text-xs text-neutral-500 mt-1">
155
+ <p>
163
156
  For remote access: Enable Tailscale or SSH tunnel in Control Plane settings
164
157
  </p>
165
158
  </div>
166
159
  </div>
167
160
  )}
168
161
 
169
- {/* Node List */}
170
162
  {nodes.length > 0 && (
171
- <div className="space-y-3">
163
+ <div className="nodes-list">
172
164
  {nodes.map((node) => (
173
165
  <div
174
166
  key={node.id}
175
- className={`bg-neutral-800/50 rounded-lg p-4 border transition-colors cursor-pointer ${
176
- selectedNode === node.id
177
- ? 'border-accent-500'
178
- : 'border-neutral-700 hover:border-neutral-600'
179
- }`}
167
+ className={`nodes-card ${selectedNode === node.id ? 'is-active' : ''}`}
180
168
  onClick={() => setSelectedNode(selectedNode === node.id ? null : node.id)}
181
169
  >
182
- {/* Node Header */}
183
- <div className="flex items-center justify-between">
184
- <div className="flex items-center gap-3">
185
- <span className="text-2xl">{getPlatformIcon(node.platform)}</span>
170
+ <div className="nodes-card-header">
171
+ <div className="nodes-card-info">
172
+ <span className="nodes-platform-icon">{getPlatformIcon(node.platform)}</span>
186
173
  <div>
187
- <h4 className="text-sm font-medium text-neutral-100">{node.displayName}</h4>
188
- <p className="text-xs text-neutral-500">
174
+ <h4 className="nodes-card-title">{node.displayName}</h4>
175
+ <p className="nodes-card-subtitle">
189
176
  {node.platform.toUpperCase()} · v{node.version}
190
177
  </p>
191
178
  </div>
192
179
  </div>
193
- <div className="flex items-center gap-2">
194
- <span
195
- className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs ${
196
- node.isForeground
197
- ? 'bg-green-500/20 text-green-400'
198
- : 'bg-yellow-500/20 text-yellow-400'
199
- }`}
200
- >
201
- {node.isForeground ? 'Foreground' : 'Background'}
202
- </span>
203
- </div>
180
+ <span className={`nodes-status ${node.isForeground ? 'foreground' : 'background'}`}>
181
+ {node.isForeground ? 'Foreground' : 'Background'}
182
+ </span>
204
183
  </div>
205
184
 
206
- {/* Capabilities */}
207
- <div className="mt-3 flex flex-wrap gap-2">
185
+ <div className="nodes-capabilities">
208
186
  {node.capabilities.map((cap) => (
209
187
  <span
210
188
  key={cap}
211
- className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs ${
212
- node.permissions?.[cap]
213
- ? 'bg-neutral-700 text-neutral-300'
214
- : 'bg-neutral-800 text-neutral-500'
215
- }`}
189
+ className={`nodes-capability ${node.permissions?.[cap] ? '' : 'denied'}`}
216
190
  title={node.permissions?.[cap] ? 'Permission granted' : 'Permission not granted'}
217
191
  >
218
192
  {getCapabilityIcon(cap)} {cap}
@@ -221,33 +195,30 @@ export function NodesSettings({ compact = false }: NodesSettingsProps) {
221
195
  ))}
222
196
  </div>
223
197
 
224
- {/* Expanded Details */}
225
198
  {selectedNode === node.id && (
226
- <div className="mt-4 pt-4 border-t border-neutral-700 space-y-4">
227
- {/* Connection Info */}
228
- <div className="grid grid-cols-2 gap-4 text-xs">
229
- <div>
230
- <span className="text-neutral-500">Node ID</span>
231
- <p className="text-neutral-300 font-mono mt-0.5 truncate">{node.id}</p>
199
+ <div className="nodes-details">
200
+ <div className="nodes-detail-grid">
201
+ <div className="nodes-detail">
202
+ <span className="nodes-detail-label">Node ID</span>
203
+ <span className="nodes-detail-value nodes-detail-mono">{node.id}</span>
232
204
  </div>
233
- <div>
234
- <span className="text-neutral-500">Device ID</span>
235
- <p className="text-neutral-300 font-mono mt-0.5 truncate">{node.deviceId || 'N/A'}</p>
205
+ <div className="nodes-detail">
206
+ <span className="nodes-detail-label">Device ID</span>
207
+ <span className="nodes-detail-value nodes-detail-mono">{node.deviceId || 'N/A'}</span>
236
208
  </div>
237
- <div>
238
- <span className="text-neutral-500">Connected</span>
239
- <p className="text-neutral-300 mt-0.5">{formatTimestamp(node.connectedAt)}</p>
209
+ <div className="nodes-detail">
210
+ <span className="nodes-detail-label">Connected</span>
211
+ <span className="nodes-detail-value">{formatTimestamp(node.connectedAt)}</span>
240
212
  </div>
241
- <div>
242
- <span className="text-neutral-500">Last Activity</span>
243
- <p className="text-neutral-300 mt-0.5">{formatTimestamp(node.lastActivityAt)}</p>
213
+ <div className="nodes-detail">
214
+ <span className="nodes-detail-label">Last Activity</span>
215
+ <span className="nodes-detail-value">{formatTimestamp(node.lastActivityAt)}</span>
244
216
  </div>
245
217
  </div>
246
218
 
247
- {/* Test Commands */}
248
- <div>
249
- <h5 className="text-xs font-medium text-neutral-400 mb-2">Test Commands</h5>
250
- <div className="flex flex-wrap gap-2">
219
+ <div className="nodes-test">
220
+ <h5>Test Commands</h5>
221
+ <div className="nodes-test-actions">
251
222
  {node.commands.slice(0, 6).map((cmd) => (
252
223
  <button
253
224
  key={cmd}
@@ -256,7 +227,7 @@ export function NodesSettings({ compact = false }: NodesSettingsProps) {
256
227
  handleTestCommand(node.id, cmd);
257
228
  }}
258
229
  disabled={testingCommand === `${node.id}:${cmd}`}
259
- className="px-2 py-1 text-xs bg-neutral-700 hover:bg-neutral-600 text-neutral-300 rounded disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
230
+ className="nodes-command-btn"
260
231
  >
261
232
  {testingCommand === `${node.id}:${cmd}` ? 'Testing...' : cmd}
262
233
  </button>
@@ -264,25 +235,16 @@ export function NodesSettings({ compact = false }: NodesSettingsProps) {
264
235
  </div>
265
236
  </div>
266
237
 
267
- {/* Test Result */}
268
238
  {testResult && testResult.nodeId === node.id && (
269
- <div
270
- className={`p-3 rounded text-xs ${
271
- testResult.result.ok
272
- ? 'bg-green-500/10 border border-green-500/30 text-green-400'
273
- : 'bg-red-500/10 border border-red-500/30 text-red-400'
274
- }`}
275
- >
276
- <p className="font-medium">
239
+ <div className={`nodes-result ${testResult.result.ok ? 'success' : 'error'}`}>
240
+ <p className="nodes-result-title">
277
241
  {testResult.command}: {testResult.result.ok ? 'Success' : 'Failed'}
278
242
  </p>
279
243
  {testResult.result.error && (
280
- <p className="mt-1 text-red-300">
281
- {testResult.result.error.message}
282
- </p>
244
+ <p className="nodes-result-error">{testResult.result.error.message}</p>
283
245
  )}
284
246
  {testResult.result.ok && testResult.result.payload != null && (
285
- <pre className="mt-2 p-2 bg-neutral-900/50 rounded overflow-auto max-h-32">
247
+ <pre className="nodes-result-pre">
286
248
  {(() => {
287
249
  try {
288
250
  return JSON.stringify(testResult.result.payload, null, 2).slice(0, 500);
@@ -301,14 +263,11 @@ export function NodesSettings({ compact = false }: NodesSettingsProps) {
301
263
  </div>
302
264
  )}
303
265
 
304
- {/* Status Summary */}
305
266
  {nodes.length > 0 && !compact && (
306
- <div className="mt-4 p-3 bg-neutral-800/30 rounded-lg border border-neutral-700/50">
307
- <p className="text-xs text-neutral-400">
308
- <span className="text-accent-400 font-medium">{nodes.length}</span> companion{nodes.length !== 1 ? 's' : ''} connected
309
- {' · '}
310
- <span className="text-green-400">{nodes.filter(n => n.isForeground).length}</span> in foreground
311
- </p>
267
+ <div className="nodes-summary">
268
+ <span className="nodes-summary-count">{nodes.length}</span> companion{nodes.length !== 1 ? 's' : ''} connected
269
+ {' · '}
270
+ <span className="nodes-summary-highlight">{nodes.filter(n => n.isForeground).length}</span> in foreground
312
271
  </div>
313
272
  )}
314
273
  </div>
@@ -1,9 +1,11 @@
1
1
  import { useState, useEffect, useRef } from 'react';
2
+ import { ThemeIcon } from './ThemeIcon';
3
+ import { AlertTriangleIcon, CheckIcon, ClockIcon, InfoIcon, XIcon } from './LineIcons';
2
4
 
3
5
  // Define types inline for the renderer
4
6
  interface AppNotification {
5
7
  id: string;
6
- type: 'task_completed' | 'task_failed' | 'scheduled_task' | 'info' | 'warning' | 'error';
8
+ type: 'task_completed' | 'task_failed' | 'scheduled_task' | 'input_required' | 'info' | 'warning' | 'error';
7
9
  title: string;
8
10
  message: string;
9
11
  read: boolean;
@@ -236,13 +238,14 @@ const Icons = {
236
238
  ),
237
239
  };
238
240
 
239
- const typeIcons: Record<string, { icon: string; bg: string; color: string }> = {
240
- task_completed: { icon: '', bg: 'rgba(34, 197, 94, 0.15)', color: 'rgb(34, 197, 94)' },
241
- task_failed: { icon: '', bg: 'rgba(239, 68, 68, 0.15)', color: 'rgb(239, 68, 68)' },
242
- scheduled_task: { icon: '', bg: 'var(--color-accent-glass)', color: 'var(--color-accent)' },
243
- info: { icon: 'ℹ️', bg: 'rgba(59, 130, 246, 0.15)', color: 'rgb(59, 130, 246)' },
244
- warning: { icon: '⚠️', bg: 'rgba(245, 158, 11, 0.15)', color: 'rgb(245, 158, 11)' },
245
- error: { icon: '🚨', bg: 'rgba(239, 68, 68, 0.15)', color: 'rgb(239, 68, 68)' },
241
+ const typeIcons: Record<string, { icon: React.ReactNode; bg: string; color: string }> = {
242
+ task_completed: { icon: <ThemeIcon emoji="" icon={<CheckIcon size={14} />} />, bg: 'rgba(34, 197, 94, 0.15)', color: 'rgb(34, 197, 94)' },
243
+ task_failed: { icon: <ThemeIcon emoji="" icon={<XIcon size={14} />} />, bg: 'rgba(239, 68, 68, 0.15)', color: 'rgb(239, 68, 68)' },
244
+ scheduled_task: { icon: <ThemeIcon emoji="" icon={<ClockIcon size={14} />} />, bg: 'var(--color-accent-glass)', color: 'var(--color-accent)' },
245
+ input_required: { icon: <ThemeIcon emoji="📝" icon={<InfoIcon size={14} />} />, bg: 'rgba(245, 158, 11, 0.15)', color: 'rgb(245, 158, 11)' },
246
+ info: { icon: <ThemeIcon emoji="ℹ️" icon={<InfoIcon size={14} />} />, bg: 'rgba(59, 130, 246, 0.15)', color: 'rgb(59, 130, 246)' },
247
+ warning: { icon: <ThemeIcon emoji="⚠️" icon={<AlertTriangleIcon size={14} />} />, bg: 'rgba(245, 158, 11, 0.15)', color: 'rgb(245, 158, 11)' },
248
+ error: { icon: <ThemeIcon emoji="🚨" icon={<AlertTriangleIcon size={14} />} />, bg: 'rgba(239, 68, 68, 0.15)', color: 'rgb(239, 68, 68)' },
246
249
  };
247
250
 
248
251
  function formatRelativeTime(timestamp: number): string {
@@ -260,6 +263,10 @@ function formatRelativeTime(timestamp: number): string {
260
263
  return new Date(timestamp).toLocaleDateString();
261
264
  }
262
265
 
266
+ function stripLeadingEmoji(text: string): string {
267
+ return text.replace(/^[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}][\uFE0F\uFE0E]?\s*/u, '');
268
+ }
269
+
263
270
  export function NotificationPanel({ onNotificationClick }: NotificationPanelProps) {
264
271
  const [isOpen, setIsOpen] = useState(false);
265
272
  const [notifications, setNotifications] = useState<AppNotification[]>([]);
@@ -340,8 +347,14 @@ export function NotificationPanel({ onNotificationClick }: NotificationPanelProp
340
347
  };
341
348
 
342
349
  const handleNotificationClick = async (notification: AppNotification) => {
343
- // Mark as read
344
- if (!notification.read) {
350
+ const shouldAutoClear = notification.type === 'input_required';
351
+ if (shouldAutoClear) {
352
+ try {
353
+ await window.electronAPI.deleteNotification(notification.id);
354
+ } catch (error) {
355
+ console.error('Failed to delete notification:', error);
356
+ }
357
+ } else if (!notification.read) {
345
358
  try {
346
359
  await window.electronAPI.markNotificationRead(notification.id);
347
360
  } catch (error) {
@@ -424,6 +437,7 @@ export function NotificationPanel({ onNotificationClick }: NotificationPanelProp
424
437
  notifications.map((notification) => {
425
438
  const typeConfig = typeIcons[notification.type] || typeIcons.info;
426
439
  const isHovered = hoveredId === notification.id;
440
+ const modernTitle = stripLeadingEmoji(notification.title);
427
441
 
428
442
  return (
429
443
  <div
@@ -451,7 +465,7 @@ export function NotificationPanel({ onNotificationClick }: NotificationPanelProp
451
465
  {typeConfig.icon}
452
466
  </div>
453
467
  <div style={styles.notificationContent}>
454
- <p style={styles.notificationTitle}>{notification.title}</p>
468
+ <p style={styles.notificationTitle}>{modernTitle}</p>
455
469
  <p style={styles.notificationMessage}>{notification.message}</p>
456
470
  <span style={styles.notificationTime}>
457
471
  {formatRelativeTime(notification.createdAt)}
@@ -0,0 +1,231 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { NotionSettingsData } from '../../shared/types';
3
+
4
+ export function NotionSettings() {
5
+ const [settings, setSettings] = useState<NotionSettingsData | null>(null);
6
+ const [saving, setSaving] = useState(false);
7
+ const [testing, setTesting] = useState(false);
8
+ const [testResult, setTestResult] = useState<{ success: boolean; error?: string; name?: string; userId?: string } | null>(null);
9
+ const [status, setStatus] = useState<{ configured: boolean; connected: boolean; name?: string; error?: string } | null>(null);
10
+ const [statusLoading, setStatusLoading] = useState(false);
11
+
12
+ useEffect(() => {
13
+ loadSettings();
14
+ refreshStatus();
15
+ }, []);
16
+
17
+ const loadSettings = async () => {
18
+ try {
19
+ const loaded = await window.electronAPI.getNotionSettings();
20
+ setSettings(loaded);
21
+ } catch (error) {
22
+ console.error('Failed to load Notion settings:', error);
23
+ }
24
+ };
25
+
26
+ const updateSettings = (updates: Partial<NotionSettingsData>) => {
27
+ if (!settings) return;
28
+ setSettings({ ...settings, ...updates });
29
+ };
30
+
31
+ const handleSave = async () => {
32
+ if (!settings) return;
33
+ setSaving(true);
34
+ setTestResult(null);
35
+ try {
36
+ const payload: NotionSettingsData = { ...settings };
37
+ await window.electronAPI.saveNotionSettings(payload);
38
+ setSettings(payload);
39
+ await refreshStatus();
40
+ } catch (error) {
41
+ console.error('Failed to save Notion settings:', error);
42
+ } finally {
43
+ setSaving(false);
44
+ }
45
+ };
46
+
47
+ const refreshStatus = async () => {
48
+ try {
49
+ setStatusLoading(true);
50
+ const result = await window.electronAPI.getNotionStatus();
51
+ setStatus(result);
52
+ } catch (error) {
53
+ console.error('Failed to load Notion status:', error);
54
+ } finally {
55
+ setStatusLoading(false);
56
+ }
57
+ };
58
+
59
+ const handleTestConnection = async () => {
60
+ setTesting(true);
61
+ setTestResult(null);
62
+ try {
63
+ const result = await window.electronAPI.testNotionConnection();
64
+ setTestResult(result);
65
+ await refreshStatus();
66
+ } catch (error: any) {
67
+ setTestResult({ success: false, error: error.message || 'Failed to test connection' });
68
+ } finally {
69
+ setTesting(false);
70
+ }
71
+ };
72
+
73
+ if (!settings) {
74
+ return <div className="settings-loading">Loading Notion settings...</div>;
75
+ }
76
+
77
+ const statusLabel = !status?.configured
78
+ ? 'Missing Key'
79
+ : status.connected
80
+ ? 'Connected'
81
+ : 'Configured';
82
+
83
+ const statusClass = !status?.configured
84
+ ? 'missing'
85
+ : status.connected
86
+ ? 'connected'
87
+ : 'configured';
88
+
89
+ return (
90
+ <div className="notion-settings">
91
+ <div className="settings-section">
92
+ <div className="settings-section-header">
93
+ <div className="settings-title-with-badge">
94
+ <h3>Connect Notion</h3>
95
+ {status && (
96
+ <span
97
+ className={`notion-status-badge ${statusClass}`}
98
+ title={!status.configured ? 'API key not configured' : status.connected ? 'Connected to Notion' : 'Configured'}
99
+ >
100
+ {statusLabel}
101
+ </span>
102
+ )}
103
+ {statusLoading && !status && (
104
+ <span className="notion-status-badge configured">Checking…</span>
105
+ )}
106
+ </div>
107
+ <button className="btn-secondary btn-sm" onClick={refreshStatus} disabled={statusLoading}>
108
+ {statusLoading ? 'Checking...' : 'Refresh Status'}
109
+ </button>
110
+ </div>
111
+ <p className="settings-description">
112
+ Connect the agent to Notion using an integration API key, then use the built-in `notion_action` tool to search,
113
+ read, and update pages or data sources.
114
+ </p>
115
+ {status?.error && (
116
+ <p className="settings-hint">Status check: {status.error}</p>
117
+ )}
118
+ <div className="settings-actions">
119
+ <button
120
+ className="btn-secondary btn-sm"
121
+ onClick={() => window.electronAPI.openExternal('https://notion.so/my-integrations')}
122
+ >
123
+ Open Integrations
124
+ </button>
125
+ </div>
126
+ </div>
127
+
128
+ <div className="settings-section">
129
+ <div className="settings-field">
130
+ <label>Enable Integration</label>
131
+ <label className="settings-toggle">
132
+ <input
133
+ type="checkbox"
134
+ checked={settings.enabled}
135
+ onChange={(e) => updateSettings({ enabled: e.target.checked })}
136
+ />
137
+ <span className="toggle-slider" />
138
+ </label>
139
+ </div>
140
+
141
+ <div className="settings-field">
142
+ <label>API Key</label>
143
+ <input
144
+ type="password"
145
+ className="settings-input"
146
+ placeholder="ntn_..."
147
+ value={settings.apiKey || ''}
148
+ onChange={(e) => updateSettings({ apiKey: e.target.value || undefined })}
149
+ />
150
+ <p className="settings-hint">Create an integration and copy the key that starts with <code>ntn_</code> or <code>secret_</code>.</p>
151
+ </div>
152
+
153
+ <div className="settings-field">
154
+ <label>Notion Version</label>
155
+ <input
156
+ type="text"
157
+ className="settings-input"
158
+ placeholder="2025-09-03"
159
+ value={settings.notionVersion || ''}
160
+ onChange={(e) => updateSettings({ notionVersion: e.target.value || undefined })}
161
+ />
162
+ <p className="settings-hint">Use the latest API version unless support requests a pin.</p>
163
+ </div>
164
+
165
+ <div className="settings-field">
166
+ <label>Timeout (ms)</label>
167
+ <input
168
+ type="number"
169
+ className="settings-input"
170
+ min={1000}
171
+ max={120000}
172
+ value={settings.timeoutMs ?? 20000}
173
+ onChange={(e) => updateSettings({ timeoutMs: Number(e.target.value) })}
174
+ />
175
+ </div>
176
+
177
+ <div className="settings-actions">
178
+ <button className="btn-secondary btn-sm" onClick={handleTestConnection} disabled={testing}>
179
+ {testing ? 'Testing...' : 'Test Connection'}
180
+ </button>
181
+ <button className="btn-primary btn-sm" onClick={handleSave} disabled={saving}>
182
+ {saving ? 'Saving...' : 'Save Settings'}
183
+ </button>
184
+ </div>
185
+
186
+ {testResult && (
187
+ <div className={`test-result ${testResult.success ? 'success' : 'error'}`}>
188
+ {testResult.success ? (
189
+ <span>Connected{testResult.name ? ` as ${testResult.name}` : ''}</span>
190
+ ) : (
191
+ <span>Connection failed: {testResult.error}</span>
192
+ )}
193
+ </div>
194
+ )}
195
+ </div>
196
+
197
+ <div className="settings-section">
198
+ <h4>Setup Tips</h4>
199
+ <ol className="settings-hint">
200
+ <li>Create an integration and copy its API key.</li>
201
+ <li>Share the target pages or databases with the integration.</li>
202
+ <li>Save settings, then click “Test Connection”.</li>
203
+ </ol>
204
+ </div>
205
+
206
+ <div className="settings-section">
207
+ <h4>Quick Usage</h4>
208
+ <pre className="settings-info-box">{`// Search for pages or data sources
209
+ notion_action({
210
+ action: "search",
211
+ query: "Roadmap"
212
+ });
213
+
214
+ // Read a page
215
+ notion_action({
216
+ action: "get_page",
217
+ page_id: "YOUR_PAGE_ID"
218
+ });
219
+
220
+ // Create a page in a database
221
+ notion_action({
222
+ action: "create_page",
223
+ database_id: "YOUR_DATABASE_ID",
224
+ properties: {
225
+ Name: { title: [{ text: { content: "New item" } }] }
226
+ }
227
+ });`}</pre>
228
+ </div>
229
+ </div>
230
+ );
231
+ }