cowork-os 0.3.21 → 0.3.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +293 -6
  2. package/connectors/README.md +20 -0
  3. package/connectors/asana-mcp/README.md +24 -0
  4. package/connectors/asana-mcp/dist/index.js +427 -0
  5. package/connectors/asana-mcp/package.json +15 -0
  6. package/connectors/asana-mcp/src/index.ts +553 -0
  7. package/connectors/asana-mcp/tsconfig.json +13 -0
  8. package/connectors/hubspot-mcp/README.md +35 -0
  9. package/connectors/hubspot-mcp/dist/index.js +454 -0
  10. package/connectors/hubspot-mcp/package.json +15 -0
  11. package/connectors/hubspot-mcp/src/index.ts +562 -0
  12. package/connectors/hubspot-mcp/tsconfig.json +13 -0
  13. package/connectors/jira-mcp/README.md +49 -0
  14. package/connectors/jira-mcp/dist/index.js +588 -0
  15. package/connectors/jira-mcp/package.json +15 -0
  16. package/connectors/jira-mcp/src/index.ts +711 -0
  17. package/connectors/jira-mcp/tsconfig.json +13 -0
  18. package/connectors/linear-mcp/README.md +22 -0
  19. package/connectors/linear-mcp/dist/index.js +402 -0
  20. package/connectors/linear-mcp/package.json +15 -0
  21. package/connectors/linear-mcp/src/index.ts +522 -0
  22. package/connectors/linear-mcp/tsconfig.json +13 -0
  23. package/connectors/okta-mcp/README.md +24 -0
  24. package/connectors/okta-mcp/dist/index.js +411 -0
  25. package/connectors/okta-mcp/package.json +15 -0
  26. package/connectors/okta-mcp/src/index.ts +520 -0
  27. package/connectors/okta-mcp/tsconfig.json +13 -0
  28. package/connectors/salesforce-mcp/README.md +47 -0
  29. package/connectors/salesforce-mcp/dist/index.js +584 -0
  30. package/connectors/salesforce-mcp/package.json +15 -0
  31. package/connectors/salesforce-mcp/src/index.ts +722 -0
  32. package/connectors/salesforce-mcp/tsconfig.json +13 -0
  33. package/connectors/servicenow-mcp/README.md +26 -0
  34. package/connectors/servicenow-mcp/dist/index.js +400 -0
  35. package/connectors/servicenow-mcp/package.json +15 -0
  36. package/connectors/servicenow-mcp/src/index.ts +500 -0
  37. package/connectors/servicenow-mcp/tsconfig.json +13 -0
  38. package/connectors/templates/mcp-connector/README.md +31 -0
  39. package/connectors/templates/mcp-connector/package.json +15 -0
  40. package/connectors/templates/mcp-connector/src/index.ts +330 -0
  41. package/connectors/templates/mcp-connector/tsconfig.json +13 -0
  42. package/connectors/zendesk-mcp/README.md +40 -0
  43. package/connectors/zendesk-mcp/dist/index.js +431 -0
  44. package/connectors/zendesk-mcp/package.json +15 -0
  45. package/connectors/zendesk-mcp/src/index.ts +543 -0
  46. package/connectors/zendesk-mcp/tsconfig.json +13 -0
  47. package/dist/electron/electron/agent/daemon.js +25 -0
  48. package/dist/electron/electron/agent/executor.js +181 -26
  49. package/dist/electron/electron/agent/llm/anthropic-compatible-provider.js +177 -0
  50. package/dist/electron/electron/agent/llm/github-copilot-provider.js +97 -0
  51. package/dist/electron/electron/agent/llm/groq-provider.js +33 -0
  52. package/dist/electron/electron/agent/llm/index.js +11 -1
  53. package/dist/electron/electron/agent/llm/kimi-provider.js +33 -0
  54. package/dist/electron/electron/agent/llm/openai-compatible-provider.js +116 -0
  55. package/dist/electron/electron/agent/llm/openai-compatible.js +111 -0
  56. package/dist/electron/electron/agent/llm/openai-oauth.js +2 -1
  57. package/dist/electron/electron/agent/llm/openrouter-provider.js +1 -1
  58. package/dist/electron/electron/agent/llm/provider-factory.js +318 -4
  59. package/dist/electron/electron/agent/llm/types.js +66 -1
  60. package/dist/electron/electron/agent/llm/xai-provider.js +33 -0
  61. package/dist/electron/electron/agent/tools/box-tools.js +231 -0
  62. package/dist/electron/electron/agent/tools/builtin-settings.js +28 -0
  63. package/dist/electron/electron/agent/tools/dropbox-tools.js +237 -0
  64. package/dist/electron/electron/agent/tools/google-drive-tools.js +227 -0
  65. package/dist/electron/electron/agent/tools/notion-tools.js +312 -0
  66. package/dist/electron/electron/agent/tools/onedrive-tools.js +217 -0
  67. package/dist/electron/electron/agent/tools/registry.js +541 -0
  68. package/dist/electron/electron/agent/tools/sharepoint-tools.js +243 -0
  69. package/dist/electron/electron/agent/tools/shell-tools.js +12 -3
  70. package/dist/electron/electron/agent/tools/x-tools.js +1 -1
  71. package/dist/electron/electron/gateway/index.js +1 -0
  72. package/dist/electron/electron/gateway/router.js +123 -143
  73. package/dist/electron/electron/ipc/canvas-handlers.js +5 -0
  74. package/dist/electron/electron/ipc/handlers.js +627 -158
  75. package/dist/electron/electron/main.js +63 -0
  76. package/dist/electron/electron/mcp/oauth/connector-oauth.js +333 -0
  77. package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +503 -154
  78. package/dist/electron/electron/memory/MemoryService.js +1 -1
  79. package/dist/electron/electron/preload.js +74 -1
  80. package/dist/electron/electron/settings/box-manager.js +54 -0
  81. package/dist/electron/electron/settings/dropbox-manager.js +54 -0
  82. package/dist/electron/electron/settings/google-drive-manager.js +54 -0
  83. package/dist/electron/electron/settings/notion-manager.js +56 -0
  84. package/dist/electron/electron/settings/onedrive-manager.js +54 -0
  85. package/dist/electron/electron/settings/sharepoint-manager.js +54 -0
  86. package/dist/electron/electron/utils/box-api.js +153 -0
  87. package/dist/electron/electron/utils/dropbox-api.js +144 -0
  88. package/dist/electron/electron/utils/env-migration.js +19 -0
  89. package/dist/electron/electron/utils/google-drive-api.js +152 -0
  90. package/dist/electron/electron/utils/notion-api.js +103 -0
  91. package/dist/electron/electron/utils/onedrive-api.js +113 -0
  92. package/dist/electron/electron/utils/sharepoint-api.js +109 -0
  93. package/dist/electron/electron/utils/validation.js +82 -3
  94. package/dist/electron/electron/utils/x-cli.js +1 -1
  95. package/dist/electron/shared/channelMessages.js +284 -3
  96. package/dist/electron/shared/llm-provider-catalog.js +198 -0
  97. package/dist/electron/shared/types.js +88 -1
  98. package/package.json +12 -2
  99. package/src/electron/agent/executor.ts +205 -28
  100. package/src/electron/agent/llm/anthropic-compatible-provider.ts +214 -0
  101. package/src/electron/agent/llm/github-copilot-provider.ts +117 -0
  102. package/src/electron/agent/llm/groq-provider.ts +39 -0
  103. package/src/electron/agent/llm/index.ts +5 -0
  104. package/src/electron/agent/llm/kimi-provider.ts +39 -0
  105. package/src/electron/agent/llm/openai-compatible-provider.ts +153 -0
  106. package/src/electron/agent/llm/openai-compatible.ts +133 -0
  107. package/src/electron/agent/llm/openai-oauth.ts +2 -1
  108. package/src/electron/agent/llm/openrouter-provider.ts +2 -1
  109. package/src/electron/agent/llm/provider-factory.ts +414 -6
  110. package/src/electron/agent/llm/types.ts +90 -1
  111. package/src/electron/agent/llm/xai-provider.ts +39 -0
  112. package/src/electron/agent/tools/box-tools.ts +239 -0
  113. package/src/electron/agent/tools/builtin-settings.ts +34 -0
  114. package/src/electron/agent/tools/dropbox-tools.ts +237 -0
  115. package/src/electron/agent/tools/google-drive-tools.ts +228 -0
  116. package/src/electron/agent/tools/notion-tools.ts +330 -0
  117. package/src/electron/agent/tools/onedrive-tools.ts +217 -0
  118. package/src/electron/agent/tools/registry.ts +565 -0
  119. package/src/electron/agent/tools/sharepoint-tools.ts +247 -0
  120. package/src/electron/agent/tools/shell-tools.ts +11 -3
  121. package/src/electron/agent/tools/x-tools.ts +1 -1
  122. package/src/electron/database/SecureSettingsRepository.ts +7 -1
  123. package/src/electron/gateway/index.ts +1 -0
  124. package/src/electron/gateway/router.ts +134 -149
  125. package/src/electron/ipc/canvas-handlers.ts +10 -0
  126. package/src/electron/ipc/handlers.ts +673 -153
  127. package/src/electron/main.ts +35 -0
  128. package/src/electron/mcp/oauth/connector-oauth.ts +448 -0
  129. package/src/electron/mcp/registry/MCPRegistryManager.ts +343 -12
  130. package/src/electron/memory/MemoryService.ts +5 -1
  131. package/src/electron/preload.ts +167 -4
  132. package/src/electron/settings/box-manager.ts +58 -0
  133. package/src/electron/settings/dropbox-manager.ts +58 -0
  134. package/src/electron/settings/google-drive-manager.ts +58 -0
  135. package/src/electron/settings/notion-manager.ts +60 -0
  136. package/src/electron/settings/onedrive-manager.ts +58 -0
  137. package/src/electron/settings/sharepoint-manager.ts +58 -0
  138. package/src/electron/utils/box-api.ts +184 -0
  139. package/src/electron/utils/dropbox-api.ts +171 -0
  140. package/src/electron/utils/env-migration.ts +22 -0
  141. package/src/electron/utils/google-drive-api.ts +183 -0
  142. package/src/electron/utils/notion-api.ts +126 -0
  143. package/src/electron/utils/onedrive-api.ts +137 -0
  144. package/src/electron/utils/sharepoint-api.ts +132 -0
  145. package/src/electron/utils/validation.ts +102 -1
  146. package/src/electron/utils/x-cli.ts +1 -1
  147. package/src/renderer/App.tsx +20 -2
  148. package/src/renderer/components/BoxSettings.tsx +203 -0
  149. package/src/renderer/components/BrowserView.tsx +101 -0
  150. package/src/renderer/components/BuiltinToolsSettings.tsx +105 -0
  151. package/src/renderer/components/CanvasPreview.tsx +68 -1
  152. package/src/renderer/components/ConnectorEnvModal.tsx +116 -0
  153. package/src/renderer/components/ConnectorSetupModal.tsx +566 -0
  154. package/src/renderer/components/ConnectorsSettings.tsx +397 -0
  155. package/src/renderer/components/DropboxSettings.tsx +202 -0
  156. package/src/renderer/components/GoogleDriveSettings.tsx +201 -0
  157. package/src/renderer/components/MCPSettings.tsx +56 -0
  158. package/src/renderer/components/MainContent.tsx +270 -34
  159. package/src/renderer/components/NotionSettings.tsx +231 -0
  160. package/src/renderer/components/Onboarding/Onboarding.tsx +13 -1
  161. package/src/renderer/components/OnboardingModal.tsx +70 -1
  162. package/src/renderer/components/OneDriveSettings.tsx +212 -0
  163. package/src/renderer/components/Settings.tsx +611 -8
  164. package/src/renderer/components/SharePointSettings.tsx +224 -0
  165. package/src/renderer/components/Sidebar.tsx +25 -9
  166. package/src/renderer/hooks/useOnboardingFlow.ts +21 -0
  167. package/src/renderer/styles/index.css +438 -25
  168. package/src/shared/channelMessages.ts +367 -4
  169. package/src/shared/llm-provider-catalog.ts +217 -0
  170. package/src/shared/types.ts +226 -1
@@ -12,6 +12,7 @@ import { getMessage } from '../utils/agentMessages';
12
12
  const VERBOSE_STEPS_KEY = 'cowork:verboseSteps';
13
13
  const TASK_TITLE_MAX_LENGTH = 50;
14
14
  const TITLE_ELLIPSIS_REGEX = /(\.\.\.|\u2026)$/u;
15
+ const MAX_ATTACHMENTS = 10;
15
16
 
16
17
  // Important event types shown in non-verbose mode
17
18
  // These are high-level steps that represent meaningful progress
@@ -49,6 +50,46 @@ const buildTaskTitle = (text: string): string => {
49
50
  return `${trimmed.slice(0, TASK_TITLE_MAX_LENGTH)}...`;
50
51
  };
51
52
 
53
+ type SelectedFileInfo = {
54
+ path: string;
55
+ name: string;
56
+ size: number;
57
+ mimeType?: string;
58
+ };
59
+
60
+ type PendingAttachment = SelectedFileInfo & {
61
+ id: string;
62
+ };
63
+
64
+ type ImportedAttachment = {
65
+ relativePath: string;
66
+ fileName: string;
67
+ size: number;
68
+ mimeType?: string;
69
+ };
70
+
71
+ const formatFileSize = (size: number): string => {
72
+ if (size < 1024) return `${size} B`;
73
+ const kb = size / 1024;
74
+ if (kb < 1024) return `${kb.toFixed(1)} KB`;
75
+ const mb = kb / 1024;
76
+ return `${mb.toFixed(1)} MB`;
77
+ };
78
+
79
+ const buildAttachmentSummary = (attachments: ImportedAttachment[]): string => {
80
+ if (attachments.length === 0) return '';
81
+ const lines = attachments.map((attachment) => (
82
+ `- ${attachment.fileName} (${attachment.relativePath})`
83
+ ));
84
+ return `Attached files (relative to workspace):\n${lines.join('\n')}`;
85
+ };
86
+
87
+ const composeMessageWithAttachments = (text: string, attachments: ImportedAttachment[]): string => {
88
+ const base = text.trim() || 'Please review the attached files.';
89
+ const summary = buildAttachmentSummary(attachments);
90
+ return summary ? `${base}\n\n${summary}` : base;
91
+ };
92
+
52
93
  type MentionOption = {
53
94
  type: 'agent' | 'everyone';
54
95
  id: string;
@@ -526,7 +567,7 @@ interface GoalModeOptions {
526
567
  maxAttempts?: number;
527
568
  }
528
569
 
529
- type SettingsTab = 'appearance' | 'llm' | 'search' | 'telegram' | 'slack' | 'whatsapp' | 'teams' | 'morechannels' | 'updates' | 'guardrails' | 'queue' | 'skills' | 'voice';
570
+ type SettingsTab = 'appearance' | 'llm' | 'search' | 'telegram' | 'slack' | 'whatsapp' | 'teams' | 'x' | 'morechannels' | 'integrations' | 'updates' | 'guardrails' | 'queue' | 'skills' | 'voice';
530
571
 
531
572
  interface MainContentProps {
532
573
  task: Task | undefined;
@@ -539,6 +580,7 @@ interface MainContentProps {
539
580
  onSelectWorkspace?: (workspace: Workspace) => void;
540
581
  onOpenSettings?: (tab?: SettingsTab) => void;
541
582
  onStopTask?: () => void;
583
+ onOpenBrowserView?: (url?: string) => void;
542
584
  selectedModel: string;
543
585
  availableModels: LLMModelInfo[];
544
586
  onModelChange: (model: string) => void;
@@ -553,11 +595,15 @@ interface ActiveCommand {
553
595
  startTimestamp: number; // When the command started, for positioning in timeline
554
596
  }
555
597
 
556
- export function MainContent({ task, selectedTaskId, workspace, events, onSendMessage, onCreateTask, onChangeWorkspace, onSelectWorkspace, onOpenSettings, onStopTask, selectedModel, availableModels, onModelChange }: MainContentProps) {
598
+ export function MainContent({ task, selectedTaskId, workspace, events, onSendMessage, onCreateTask, onChangeWorkspace, onSelectWorkspace, onOpenSettings, onStopTask, onOpenBrowserView, selectedModel, availableModels, onModelChange }: MainContentProps) {
557
599
  // Agent personality context for personalized messages
558
600
  const agentContext = useAgentContext();
559
601
  const [pendingApproval, setPendingApproval] = useState<ApprovalRequest | null>(null);
560
602
  const [inputValue, setInputValue] = useState('');
603
+ const [pendingAttachments, setPendingAttachments] = useState<PendingAttachment[]>([]);
604
+ const [attachmentError, setAttachmentError] = useState<string | null>(null);
605
+ const [isDraggingFiles, setIsDraggingFiles] = useState(false);
606
+ const [isUploadingAttachments, setIsUploadingAttachments] = useState(false);
561
607
  const [agentRoles, setAgentRoles] = useState<AgentRoleData[]>([]);
562
608
  const [mentionQuery, setMentionQuery] = useState('');
563
609
  const [mentionTarget, setMentionTarget] = useState<{ start: number; end: number } | null>(null);
@@ -1220,37 +1266,192 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
1220
1266
  }
1221
1267
  };
1222
1268
 
1223
- const handleSend = () => {
1224
- if (inputValue.trim()) {
1225
- // Use selectedTaskId to determine if we should follow-up or create new task
1226
- // This fixes the bug where old tasks (beyond the 100 most recent) would create new tasks
1227
- // instead of sending follow-up messages
1228
- if (!selectedTaskId && onCreateTask) {
1229
- // No task selected - create new task with optional Goal Mode options
1230
- const trimmedInput = inputValue.trim();
1231
- const title = buildTaskTitle(trimmedInput);
1232
- const options: GoalModeOptions | undefined = goalModeEnabled && verificationCommand
1233
- ? {
1234
- successCriteria: { type: 'shell_command' as const, command: verificationCommand },
1235
- maxAttempts,
1236
- }
1237
- : undefined;
1238
- onCreateTask(title, trimmedInput, options);
1239
- // Reset Goal Mode state
1240
- setGoalModeEnabled(false);
1241
- setVerificationCommand('');
1242
- setMaxAttempts(3);
1243
- } else {
1244
- // Task is selected (even if not in current list) - send follow-up message
1245
- onSendMessage(inputValue.trim());
1269
+ const reportAttachmentError = (message: string) => {
1270
+ setAttachmentError(message);
1271
+ window.setTimeout(() => setAttachmentError(null), 5000);
1272
+ };
1273
+
1274
+ const appendPendingAttachments = (files: SelectedFileInfo[]) => {
1275
+ if (files.length === 0) return;
1276
+ setPendingAttachments((prev) => {
1277
+ const existingPaths = new Set(prev.map((attachment) => attachment.path));
1278
+ const next = [...prev];
1279
+ for (const file of files) {
1280
+ if (existingPaths.has(file.path)) continue;
1281
+ if (next.length >= MAX_ATTACHMENTS) {
1282
+ reportAttachmentError(`You can attach up to ${MAX_ATTACHMENTS} files.`);
1283
+ break;
1284
+ }
1285
+ next.push({
1286
+ ...file,
1287
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1288
+ });
1289
+ existingPaths.add(file.path);
1246
1290
  }
1247
- setInputValue('');
1248
- setMentionOpen(false);
1249
- setMentionQuery('');
1250
- setMentionTarget(null);
1291
+ return next;
1292
+ });
1293
+ };
1294
+
1295
+ const handleAttachFiles = async () => {
1296
+ try {
1297
+ const files = await window.electronAPI.selectFiles();
1298
+ if (!files || files.length === 0) return;
1299
+ appendPendingAttachments(files);
1300
+ } catch (error) {
1301
+ console.error('Failed to select files:', error);
1302
+ reportAttachmentError('Failed to add attachments. Please try again.');
1251
1303
  }
1252
1304
  };
1253
1305
 
1306
+ const handleRemoveAttachment = (id: string) => {
1307
+ setPendingAttachments((prev) => prev.filter((attachment) => attachment.id !== id));
1308
+ };
1309
+
1310
+ const isFileDrag = (event: React.DragEvent) =>
1311
+ Array.from(event.dataTransfer.types || []).includes('Files');
1312
+
1313
+ const handleDragOver = (event: React.DragEvent) => {
1314
+ if (!isFileDrag(event)) return;
1315
+ event.preventDefault();
1316
+ setIsDraggingFiles(true);
1317
+ };
1318
+
1319
+ const handleDragLeave = (event: React.DragEvent) => {
1320
+ if (!isFileDrag(event)) return;
1321
+ event.preventDefault();
1322
+ setIsDraggingFiles(false);
1323
+ };
1324
+
1325
+ const handleDrop = (event: React.DragEvent) => {
1326
+ if (!isFileDrag(event)) return;
1327
+ event.preventDefault();
1328
+ setIsDraggingFiles(false);
1329
+
1330
+ const droppedFiles = Array.from(event.dataTransfer.files || []);
1331
+ const files: SelectedFileInfo[] = [];
1332
+ let missingPath = false;
1333
+
1334
+ droppedFiles.forEach((file) => {
1335
+ const filePath = (file as File & { path?: string }).path;
1336
+ if (!filePath) {
1337
+ missingPath = true;
1338
+ return;
1339
+ }
1340
+ files.push({
1341
+ path: filePath,
1342
+ name: file.name,
1343
+ size: file.size,
1344
+ mimeType: file.type || undefined,
1345
+ });
1346
+ });
1347
+
1348
+ if (missingPath) {
1349
+ reportAttachmentError('Drag-and-drop is not supported for these files. Use the attach button instead.');
1350
+ }
1351
+ appendPendingAttachments(files);
1352
+ };
1353
+
1354
+ const renderAttachmentPanel = () => {
1355
+ if (pendingAttachments.length === 0 && !attachmentError) return null;
1356
+ return (
1357
+ <div className="attachment-panel">
1358
+ {attachmentError && <div className="attachment-error">{attachmentError}</div>}
1359
+ {pendingAttachments.length > 0 && (
1360
+ <div className="attachment-list">
1361
+ {pendingAttachments.map((attachment) => (
1362
+ <div className="attachment-chip" key={attachment.id}>
1363
+ <span className="attachment-icon">
1364
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1365
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
1366
+ <path d="M14 2v6h6" />
1367
+ </svg>
1368
+ </span>
1369
+ <span className="attachment-name" title={attachment.name}>{attachment.name}</span>
1370
+ <span className="attachment-size">{formatFileSize(attachment.size)}</span>
1371
+ <button
1372
+ className="attachment-remove"
1373
+ onClick={() => handleRemoveAttachment(attachment.id)}
1374
+ title="Remove attachment"
1375
+ disabled={isUploadingAttachments}
1376
+ >
1377
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1378
+ <path d="M18 6L6 18M6 6l12 12" />
1379
+ </svg>
1380
+ </button>
1381
+ </div>
1382
+ ))}
1383
+ </div>
1384
+ )}
1385
+ </div>
1386
+ );
1387
+ };
1388
+
1389
+ const importAttachmentsToWorkspace = async (): Promise<ImportedAttachment[]> => {
1390
+ if (pendingAttachments.length === 0) return [];
1391
+ if (!workspace) {
1392
+ throw new Error('Select a workspace before attaching files.');
1393
+ }
1394
+ return window.electronAPI.importFilesToWorkspace({
1395
+ workspaceId: workspace.id,
1396
+ files: pendingAttachments.map((attachment) => attachment.path),
1397
+ });
1398
+ };
1399
+
1400
+ const handleSend = async () => {
1401
+ const trimmedInput = inputValue.trim();
1402
+ const hasAttachments = pendingAttachments.length > 0;
1403
+
1404
+ if (!trimmedInput && !hasAttachments) return;
1405
+
1406
+ let importedAttachments: ImportedAttachment[] = [];
1407
+
1408
+ if (hasAttachments) {
1409
+ setIsUploadingAttachments(true);
1410
+ try {
1411
+ importedAttachments = await importAttachmentsToWorkspace();
1412
+ } catch (error) {
1413
+ console.error('Failed to import attachments:', error);
1414
+ reportAttachmentError(error instanceof Error ? error.message : 'Failed to upload attachments.');
1415
+ setIsUploadingAttachments(false);
1416
+ return;
1417
+ } finally {
1418
+ setIsUploadingAttachments(false);
1419
+ }
1420
+ }
1421
+
1422
+ const message = composeMessageWithAttachments(trimmedInput, importedAttachments);
1423
+
1424
+ // Use selectedTaskId to determine if we should follow-up or create new task
1425
+ // This fixes the bug where old tasks (beyond the 100 most recent) would create new tasks
1426
+ // instead of sending follow-up messages
1427
+ if (!selectedTaskId && onCreateTask) {
1428
+ // No task selected - create new task with optional Goal Mode options
1429
+ const titleSource = trimmedInput || (pendingAttachments[0]?.name ? `Review ${pendingAttachments[0].name}` : 'New task');
1430
+ const title = buildTaskTitle(titleSource);
1431
+ const options: GoalModeOptions | undefined = goalModeEnabled && verificationCommand
1432
+ ? {
1433
+ successCriteria: { type: 'shell_command' as const, command: verificationCommand },
1434
+ maxAttempts,
1435
+ }
1436
+ : undefined;
1437
+ onCreateTask(title, message, options);
1438
+ // Reset Goal Mode state
1439
+ setGoalModeEnabled(false);
1440
+ setVerificationCommand('');
1441
+ setMaxAttempts(3);
1442
+ } else {
1443
+ // Task is selected (even if not in current list) - send follow-up message
1444
+ onSendMessage(message);
1445
+ }
1446
+
1447
+ setInputValue('');
1448
+ setPendingAttachments([]);
1449
+ setAttachmentError(null);
1450
+ setMentionOpen(false);
1451
+ setMentionQuery('');
1452
+ setMentionTarget(null);
1453
+ };
1454
+
1254
1455
  const handleClearQueue = () => {
1255
1456
  setQueuedMessage(null);
1256
1457
  };
@@ -1442,7 +1643,7 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
1442
1643
 
1443
1644
  if (e.key === 'Enter' && !e.shiftKey) {
1444
1645
  e.preventDefault();
1445
- handleSend();
1646
+ void handleSend();
1446
1647
  }
1447
1648
  };
1448
1649
 
@@ -1550,7 +1751,14 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
1550
1751
  </div>
1551
1752
 
1552
1753
  {/* Input Area */}
1553
- <div className="welcome-input-container cli-input-container">
1754
+ {renderAttachmentPanel()}
1755
+ <div
1756
+ className={`welcome-input-container cli-input-container ${isDraggingFiles ? 'drag-over' : ''}`}
1757
+ onDragOver={handleDragOver}
1758
+ onDragEnter={handleDragOver}
1759
+ onDragLeave={handleDragLeave}
1760
+ onDrop={handleDrop}
1761
+ >
1554
1762
  {showVoiceNotConfigured && (
1555
1763
  <div className="voice-not-configured-banner">
1556
1764
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
@@ -1779,6 +1987,16 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
1779
1987
  </div>
1780
1988
  )}
1781
1989
  </div>
1990
+ <button
1991
+ className="attachment-btn"
1992
+ onClick={handleAttachFiles}
1993
+ disabled={isUploadingAttachments}
1994
+ title="Attach files"
1995
+ >
1996
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1997
+ <path d="M21.44 11.05l-8.49 8.49a5 5 0 0 1-7.07-7.07l8.49-8.49a3 3 0 0 1 4.24 4.24l-8.49 8.49a1 1 0 0 1-1.41-1.41l7.78-7.78" />
1998
+ </svg>
1999
+ </button>
1782
2000
  <button
1783
2001
  className={`voice-input-btn ${voiceInput.state}`}
1784
2002
  onClick={voiceInput.toggleRecording}
@@ -1813,7 +2031,7 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
1813
2031
  <button
1814
2032
  className="lets-go-btn lets-go-btn-sm"
1815
2033
  onClick={handleSend}
1816
- disabled={!inputValue.trim()}
2034
+ disabled={(!inputValue.trim() && pendingAttachments.length === 0) || isUploadingAttachments}
1817
2035
  >
1818
2036
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1819
2037
  <path d="M12 19V5M5 12l7-7 7 7" />
@@ -1925,6 +2143,7 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
1925
2143
  session={item.session}
1926
2144
  onClose={() => handleCanvasClose(item.session.id)}
1927
2145
  forceSnapshot={item.forceSnapshot}
2146
+ onOpenBrowser={onOpenBrowserView}
1928
2147
  />
1929
2148
  );
1930
2149
  }
@@ -2080,7 +2299,14 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
2080
2299
 
2081
2300
  {/* Footer with Input */}
2082
2301
  <div className="main-footer">
2083
- <div className="input-container">
2302
+ {renderAttachmentPanel()}
2303
+ <div
2304
+ className={`input-container ${isDraggingFiles ? 'drag-over' : ''}`}
2305
+ onDragOver={handleDragOver}
2306
+ onDragEnter={handleDragOver}
2307
+ onDragLeave={handleDragLeave}
2308
+ onDrop={handleDrop}
2309
+ >
2084
2310
  {/* Queued message display */}
2085
2311
  {queuedMessage && (
2086
2312
  <div className="queued-message-frame">
@@ -2171,6 +2397,16 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
2171
2397
  selectedModel={selectedModel}
2172
2398
  onModelChange={onModelChange}
2173
2399
  />
2400
+ <button
2401
+ className="attachment-btn"
2402
+ onClick={handleAttachFiles}
2403
+ disabled={isUploadingAttachments}
2404
+ title="Attach files"
2405
+ >
2406
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
2407
+ <path d="M21.44 11.05l-8.49 8.49a5 5 0 0 1-7.07-7.07l8.49-8.49a3 3 0 0 1 4.24 4.24l-8.49 8.49a1 1 0 0 1-1.41-1.41l7.78-7.78" />
2408
+ </svg>
2409
+ </button>
2174
2410
  <button
2175
2411
  className={`voice-input-btn ${voiceInput.state}`}
2176
2412
  onClick={voiceInput.toggleRecording}
@@ -2205,7 +2441,7 @@ export function MainContent({ task, selectedTaskId, workspace, events, onSendMes
2205
2441
  <button
2206
2442
  className="lets-go-btn lets-go-btn-sm"
2207
2443
  onClick={handleSend}
2208
- disabled={!inputValue.trim()}
2444
+ disabled={(!inputValue.trim() && pendingAttachments.length === 0) || isUploadingAttachments}
2209
2445
  title="Send message"
2210
2446
  >
2211
2447
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
@@ -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
+ }
@@ -20,6 +20,9 @@ const PROVIDERS: {
20
20
  { id: 'gemini', name: 'Gemini', requiresKey: true },
21
21
  { id: 'ollama', name: 'Ollama', requiresKey: false },
22
22
  { id: 'openrouter', name: 'OpenRouter', requiresKey: true },
23
+ { id: 'groq', name: 'Groq', requiresKey: true },
24
+ { id: 'xai', name: 'Grok', requiresKey: true },
25
+ { id: 'kimi', name: 'Kimi', requiresKey: true },
23
26
  { id: 'bedrock', name: 'AWS Bedrock', requiresKey: false },
24
27
  ];
25
28
 
@@ -29,6 +32,9 @@ const PROVIDER_URLS: Record<string, string> = {
29
32
  openai: 'https://platform.openai.com/api-keys',
30
33
  gemini: 'https://aistudio.google.com/app/apikey',
31
34
  openrouter: 'https://openrouter.ai/keys',
35
+ groq: 'https://console.groq.com/keys',
36
+ xai: 'https://console.x.ai/',
37
+ kimi: 'https://platform.moonshot.ai/',
32
38
  };
33
39
 
34
40
  export function Onboarding({ onComplete }: OnboardingProps) {
@@ -252,7 +258,13 @@ export function Onboarding({ onComplete }: OnboardingProps) {
252
258
  ? 'OpenAI'
253
259
  : provider === 'gemini'
254
260
  ? 'Google AI Studio'
255
- : 'OpenRouter'}
261
+ : provider === 'openrouter'
262
+ ? 'OpenRouter'
263
+ : provider === 'groq'
264
+ ? 'Groq Console'
265
+ : provider === 'xai'
266
+ ? 'xAI Console'
267
+ : 'Moonshot Platform'}
256
268
  </a>
257
269
  </p>
258
270
  )}